diff --git a/android/app/src/main/kotlin/com/org/yumiparty/MainActivity.kt b/android/app/src/main/kotlin/com/org/yumiparty/MainActivity.kt index ccda86e..15a39dc 100644 --- a/android/app/src/main/kotlin/com/org/yumiparty/MainActivity.kt +++ b/android/app/src/main/kotlin/com/org/yumiparty/MainActivity.kt @@ -5,6 +5,7 @@ import com.android.installreferrer.api.InstallReferrerStateListener import io.flutter.embedding.android.FlutterActivity import io.flutter.embedding.engine.FlutterEngine import io.flutter.plugin.common.MethodChannel +import java.util.TimeZone class MainActivity : FlutterActivity() { private var installReferrerClient: InstallReferrerClient? = null @@ -20,6 +21,15 @@ class MainActivity : FlutterActivity() { else -> result.notImplemented() } } + MethodChannel( + flutterEngine.dartExecutor.binaryMessenger, + MOBILE_CONTEXT_CHANNEL + ).setMethodCallHandler { call, result -> + when (call.method) { + "getTimeZoneIdentifier" -> result.success(TimeZone.getDefault().id) + else -> result.notImplemented() + } + } } override fun onDestroy() { @@ -79,5 +89,6 @@ class MainActivity : FlutterActivity() { companion object { private const val INSTALL_REFERRER_CHANNEL = "com.org.yumiparty/install_referrer" + private const val MOBILE_CONTEXT_CHANNEL = "com.org.yumiparty/mobile_context" } } diff --git a/docs/voice-room-big-gift-global-floating-plan.md b/docs/voice-room-big-gift-global-floating-plan.md new file mode 100644 index 0000000..3e90073 --- /dev/null +++ b/docs/voice-room-big-gift-global-floating-plan.md @@ -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`,按 `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`,保存最近 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` 或本地化的“语音房”。 + diff --git a/ios/Runner/AppDelegate.swift b/ios/Runner/AppDelegate.swift index a6c7c3f..5660c58 100644 --- a/ios/Runner/AppDelegate.swift +++ b/ios/Runner/AppDelegate.swift @@ -10,9 +10,28 @@ import UIKit ) -> Bool { GeneratedPluginRegistrant.register(with: self) registerDurableAuthStorageChannel() + registerMobileContextChannel() return super.application(application, didFinishLaunchingWithOptions: launchOptions) } + private func registerMobileContextChannel() { + guard let controller = window?.rootViewController as? FlutterViewController else { + return + } + let channel = FlutterMethodChannel( + name: "com.org.yumiparty/mobile_context", + binaryMessenger: controller.binaryMessenger + ) + channel.setMethodCallHandler { call, result in + switch call.method { + case "getTimeZoneIdentifier": + result(TimeZone.current.identifier) + default: + result(FlutterMethodNotImplemented) + } + } + } + private func registerDurableAuthStorageChannel() { guard let controller = window?.rootViewController as? FlutterViewController else { return diff --git a/lib/modules/room/background/room_background_apply_service.dart b/lib/modules/room/background/room_background_apply_service.dart new file mode 100644 index 0000000..c9b8d78 --- /dev/null +++ b/lib/modules/room/background/room_background_apply_service.dart @@ -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 apply(BuildContext context, String sourcePath) async { + final normalizedSource = sourcePath.trim(); + if (normalizedSource.isEmpty) { + throw StateError("Room background source is empty"); + } + + final rtcProvider = context.read(); + final rtmProvider = context.read(); + final roomManager = context.read(); + 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 _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 _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(); + } +} diff --git a/lib/modules/room/background/room_background_assets.dart b/lib/modules/room/background/room_background_assets.dart new file mode 100644 index 0000000..aa6699b --- /dev/null +++ b/lib/modules/room/background/room_background_assets.dart @@ -0,0 +1,14 @@ +const List 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()); +} diff --git a/lib/modules/room/background/room_background_history.dart b/lib/modules/room/background/room_background_history.dart new file mode 100644 index 0000000..6b2f34e --- /dev/null +++ b/lib/modules/room/background/room_background_history.dart @@ -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 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 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)); + } +} diff --git a/lib/modules/room/background/room_background_select_page.dart b/lib/modules/room/background/room_background_select_page.dart index 3b0a8d0..a6491b0 100644 --- a/lib/modules/room/background/room_background_select_page.dart +++ b/lib/modules/room/background/room_background_select_page.dart @@ -2,10 +2,17 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:provider/provider.dart'; import 'package:yumi/app/constants/sc_global_config.dart'; import 'package:yumi/app_localizations.dart'; +import 'package:yumi/modules/room/background/room_background_apply_service.dart'; +import 'package:yumi/modules/room/background/room_background_assets.dart'; +import 'package:yumi/modules/room/background/room_background_history.dart'; import 'package:yumi/modules/room/voice_room_route.dart'; +import 'package:yumi/services/audio/rtc_manager.dart'; import 'package:yumi/shared/business_logic/usecases/sc_fixed_width_tabIndicator.dart'; +import 'package:yumi/shared/tools/sc_loading_manager.dart'; +import 'package:yumi/ui_kit/components/sc_tts.dart'; import 'package:yumi/ui_kit/components/appbar/socialchat_appbar.dart'; import 'package:yumi/ui_kit/theme/socialchat_theme.dart'; @@ -30,13 +37,32 @@ class _RoomBackgroundSelectPageState extends State super.initState(); _tabController = TabController(length: 2, vsync: this); _tabController.addListener(_handleTabChange); - _officialItems = [ - _RoomBackgroundItem(inUse: true), - _RoomBackgroundItem(), - _RoomBackgroundItem(), - _RoomBackgroundItem(), - ]; - _mineItems = [_RoomBackgroundItem(), _RoomBackgroundItem()]; + final currentBackground = _currentRoomBackground(); + _officialItems = + roomBackgroundExampleAssets + .map( + (path) => _RoomBackgroundItem( + imagePath: path, + inUse: path == currentBackground, + ), + ) + .toList(); + final historyItems = RoomBackgroundHistory.load(); + if (currentBackground.isNotEmpty && + !isRoomBackgroundExampleAsset(currentBackground) && + !historyItems.contains(currentBackground)) { + historyItems.insert(0, currentBackground); + RoomBackgroundHistory.add(currentBackground); + } + _mineItems = + historyItems + .map( + (path) => _RoomBackgroundItem( + imagePath: path, + inUse: path == currentBackground, + ), + ) + .toList(); } @override @@ -62,35 +88,83 @@ class _RoomBackgroundSelectPageState extends State } setState(() { - for (final item in _mineItems) { - item.inUse = false; - } - _mineItems.insert( - 0, - _RoomBackgroundItem(localImagePath: uploadedPath, inUse: true), - ); + _markInUse(uploadedPath!); + _upsertMineItem(uploadedPath, inUse: true); }); _tabController.animateTo(1); } - void _selectItem(List<_RoomBackgroundItem> items, int index) { - setState(() { - for (final item in items) { - item.inUse = false; - } - items[index].inUse = true; - }); - } - Future _previewItem(List<_RoomBackgroundItem> items, int index) async { final shouldUse = await VoiceRoomRoute.openRoomBackgroundPreview( context, - backgroundPath: items[index].localImagePath, + backgroundPath: items[index].imagePath, ); if (!mounted || shouldUse != true) { return; } - _selectItem(items, index); + await _applyItem(items, index); + } + + Future _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() + .currenRoom + ?.roomProfile + ?.roomProfile + ?.roomBackground + ?.trim() ?? + ""; + } + + void _markInUse(String imagePath) { + final normalized = imagePath.trim(); + for (final item in [..._officialItems, ..._mineItems]) { + item.inUse = item.imagePath == normalized; + } + } + + void _upsertMineItem(String imagePath, {bool inUse = false}) { + final normalized = imagePath.trim(); + if (normalized.isEmpty) { + return; + } + _mineItems.removeWhere((item) => item.imagePath == normalized); + _mineItems.insert(0, _RoomBackgroundItem(imagePath: normalized)); + if (inUse) { + _markInUse(normalized); + } } @override @@ -371,13 +445,32 @@ class _RoomBackgroundCard extends StatelessWidget { Widget _buildPreview(BuildContext context) { final localizations = SCAppLocalizations.of(context)!; - if ((item.localImagePath ?? "").isNotEmpty) { - final file = File(item.localImagePath!); + final imagePath = item.imagePath.trim(); + if (imagePath.startsWith("http://") || imagePath.startsWith("https://")) { + return Image.network( + imagePath, + fit: BoxFit.cover, + errorBuilder: (_, __, ___) => _buildEmptyPreview(localizations), + ); + } + if (imagePath.startsWith("assets/") || imagePath.startsWith("sc_images/")) { + return Image.asset( + imagePath, + fit: BoxFit.cover, + errorBuilder: (_, __, ___) => _buildEmptyPreview(localizations), + ); + } + if (imagePath.isNotEmpty) { + final file = File(imagePath); if (file.existsSync()) { return Image.file(file, fit: BoxFit.cover); } } + return _buildEmptyPreview(localizations); + } + + Widget _buildEmptyPreview(SCAppLocalizations localizations) { return DecoratedBox( decoration: BoxDecoration( gradient: LinearGradient( @@ -416,8 +509,8 @@ class _RoomBackgroundCard extends StatelessWidget { } class _RoomBackgroundItem { - _RoomBackgroundItem({this.localImagePath, this.inUse = false}); + _RoomBackgroundItem({required this.imagePath, this.inUse = false}); - final String? localImagePath; + final String imagePath; bool inUse; } diff --git a/lib/modules/room/background/room_background_upload_page.dart b/lib/modules/room/background/room_background_upload_page.dart index 5298037..8b25f57 100644 --- a/lib/modules/room/background/room_background_upload_page.dart +++ b/lib/modules/room/background/room_background_upload_page.dart @@ -6,6 +6,10 @@ import 'package:image_picker/image_picker.dart'; import 'package:yumi/app/constants/sc_global_config.dart'; import 'package:yumi/app/routes/sc_fluro_navigator.dart'; import 'package:yumi/app_localizations.dart'; +import 'package:yumi/modules/room/background/room_background_apply_service.dart'; +import 'package:yumi/modules/room/background/room_background_assets.dart'; +import 'package:yumi/shared/tools/sc_loading_manager.dart'; +import 'package:yumi/ui_kit/components/sc_tts.dart'; import 'package:yumi/ui_kit/components/appbar/socialchat_appbar.dart'; import 'package:yumi/ui_kit/theme/socialchat_theme.dart'; @@ -20,8 +24,12 @@ class RoomBackgroundUploadPage extends StatefulWidget { class _RoomBackgroundUploadPageState extends State { final ImagePicker _picker = ImagePicker(); File? _selectedImage; + bool _isSaving = false; Future _pickImage() async { + if (_isSaving) { + return; + } final pickedFile = await _picker.pickImage( source: ImageSource.gallery, imageQuality: 90, @@ -37,6 +45,42 @@ class _RoomBackgroundUploadPageState extends State { }); } + Future _saveBackground() async { + final selectedImage = _selectedImage; + if (selectedImage == null || _isSaving) { + return; + } + final localizations = SCAppLocalizations.of(context)!; + setState(() { + _isSaving = true; + }); + SCLoadingManager.show(context: context); + try { + final roomBackground = await RoomBackgroundApplyService.apply( + context, + selectedImage.path, + ); + if (!mounted) { + return; + } + SCTts.show(localizations.operationSuccessful); + SCNavigatorUtils.goBackWithParams(context, roomBackground); + } catch (e, stackTrace) { + debugPrint("[Room Background Save] failed error=$e"); + debugPrint("[Room Background Save] stackTrace=$stackTrace"); + if (mounted) { + SCTts.show(localizations.operationFail); + } + } finally { + SCLoadingManager.hide(); + if (mounted) { + setState(() { + _isSaving = false; + }); + } + } + } + @override Widget build(BuildContext context) { final localizations = SCAppLocalizations.of(context)!; @@ -125,12 +169,18 @@ class _RoomBackgroundUploadPageState extends State { ), ), SizedBox(height: 16.w), - Row( - children: [ - Expanded(child: _RoomBackgroundExampleCard()), - SizedBox(width: 16.w), - Expanded(child: _RoomBackgroundExampleCard()), - ], + SizedBox( + height: 300.w, + child: ListView.separated( + scrollDirection: Axis.horizontal, + padding: EdgeInsets.symmetric(horizontal: 28.w), + itemCount: roomBackgroundExampleAssets.length, + separatorBuilder: (_, __) => SizedBox(width: 16.w), + itemBuilder: + (context, index) => _RoomBackgroundExampleCard( + assetPath: roomBackgroundExampleAssets[index], + ), + ), ), SizedBox(height: 16.w), Text( @@ -144,15 +194,9 @@ class _RoomBackgroundUploadPageState extends State { SizedBox(height: 28.w), GestureDetector( behavior: HitTestBehavior.opaque, - onTap: - _selectedImage == null - ? null - : () => SCNavigatorUtils.goBackWithParams( - context, - _selectedImage!.path, - ), + onTap: _selectedImage == null ? null : _saveBackground, child: Opacity( - opacity: _selectedImage == null ? 0.45 : 1, + opacity: _selectedImage == null || _isSaving ? 0.45 : 1, child: Container( height: 48.w, width: double.infinity, @@ -188,50 +232,63 @@ class _RoomBackgroundUploadPageState extends State { } class _RoomBackgroundExampleCard extends StatelessWidget { + const _RoomBackgroundExampleCard({required this.assetPath}); + + final String assetPath; + @override Widget build(BuildContext context) { final localizations = SCAppLocalizations.of(context)!; - return Container( + return SizedBox( + width: 140.w, height: 300.w, - decoration: BoxDecoration( + child: ClipRRect( borderRadius: BorderRadius.circular(14.w), - color: const Color(0xff143B34), - ), - child: Stack( - children: [ - Positioned.fill( - child: Icon( - Icons.image_outlined, - size: 42.w, - color: Colors.white.withValues(alpha: 0.12), - ), - ), - Positioned( - left: 12.w, - right: 12.w, - bottom: 18.w, - child: Container( - padding: EdgeInsets.symmetric(horizontal: 10.w, vertical: 8.w), - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(999.w), - border: Border.all( - color: SocialChatTheme.primaryLight, - width: 1.w, - ), - color: const Color(0xff1B4A40), + child: Stack( + children: [ + Positioned.fill( + child: Image.asset( + assetPath, + fit: BoxFit.cover, + errorBuilder: + (_, __, ___) => Container( + color: const Color(0xff143B34), + alignment: Alignment.center, + child: Icon( + Icons.image_outlined, + size: 42.w, + color: Colors.white.withValues(alpha: 0.12), + ), + ), ), - child: Text( - localizations.staticOrGifImage, - textAlign: TextAlign.center, - style: TextStyle( - color: Colors.white, - fontSize: 11.sp, - fontWeight: FontWeight.w500, + ), + Positioned( + left: 12.w, + right: 12.w, + bottom: 18.w, + child: Container( + padding: EdgeInsets.symmetric(horizontal: 10.w, vertical: 8.w), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(999.w), + border: Border.all( + color: SocialChatTheme.primaryLight, + width: 1.w, + ), + color: const Color(0xff1B4A40).withValues(alpha: 0.82), + ), + child: Text( + localizations.staticOrGifImage, + textAlign: TextAlign.center, + style: TextStyle( + color: Colors.white, + fontSize: 11.sp, + fontWeight: FontWeight.w500, + ), ), ), ), - ), - ], + ], + ), ), ); } diff --git a/lib/modules/room/voice_room_page.dart b/lib/modules/room/voice_room_page.dart index c6936b5..c2dee9e 100644 --- a/lib/modules/room/voice_room_page.dart +++ b/lib/modules/room/voice_room_page.dart @@ -19,6 +19,7 @@ import 'package:yumi/services/gift/gift_system_manager.dart'; import 'package:yumi/services/music/room_music_manager.dart'; import 'package:yumi/services/audio/rtm_manager.dart'; import 'package:yumi/shared/tools/sc_gift_vap_svga_manager.dart'; +import 'package:yumi/shared/tools/sc_lucky_gift_win_sound_player.dart'; import 'package:yumi/shared/tools/sc_network_image_utils.dart'; import 'package:yumi/shared/tools/sc_path_utils.dart'; import 'package:yumi/shared/tools/sc_room_effect_scheduler.dart'; @@ -252,7 +253,12 @@ class _VoiceRoomPageState extends State } } - String? _resolveRoomThemeBackground(JoinRoomRes? room) { + String? _resolveRoomBackground(JoinRoomRes? room) { + final roomBackground = + room?.roomProfile?.roomProfile?.roomBackground?.trim() ?? ""; + if (roomBackground.isNotEmpty) { + return roomBackground; + } final roomTheme = room?.roomProps?.roomTheme; final themeBack = roomTheme?.themeBack ?? ""; if (themeBack.isEmpty) { @@ -291,11 +297,11 @@ class _VoiceRoomPageState extends State Selector( selector: (context, provider) => - _resolveRoomThemeBackground(provider.currenRoom), - builder: (context, roomThemeBackground, child) { - return roomThemeBackground != null + _resolveRoomBackground(provider.currenRoom), + builder: (context, roomBackground, child) { + return roomBackground != null ? netImage( - url: roomThemeBackground, + url: roomBackground, width: ScreenUtil().screenWidth, height: ScreenUtil().screenHeight, noDefaultImg: true, @@ -327,7 +333,13 @@ class _VoiceRoomPageState extends State const RoomBottomWidget(showGiftComboButton: false), ], ), - LGiftAnimalPage(), + PositionedDirectional( + start: 0, + end: 0, + top: 0, + bottom: RoomBottomWidget.floatingButtonHostHeight.w, + child: const IgnorePointer(child: LGiftAnimalPage()), + ), Transform.translate( offset: Offset(0, -20), child: RoomAnimationQueueScreen(), @@ -569,6 +581,16 @@ class _VoiceRoomPageState extends State context, listen: false, ).enqueueGiftAnimation(giftModel); + unawaited( + SCLuckyGiftWinSoundPlayer.play( + eventKey: SCLuckyGiftWinSoundPlayer.buildEventKey( + giftId: msg.gift?.id, + userId: msg.user?.id, + toUserId: msg.toUser?.id, + awardAmount: awardAmount, + ), + ), + ); } String _formatLuckyRewardAmount(num awardAmount) { diff --git a/lib/modules/room/voice_room_route.dart b/lib/modules/room/voice_room_route.dart index e4b9815..2db64dc 100644 --- a/lib/modules/room/voice_room_route.dart +++ b/lib/modules/room/voice_room_route.dart @@ -3,6 +3,7 @@ import 'dart:io'; import 'package:fluro/fluro.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; import 'package:yumi/modules/room/background/room_background_preview_page.dart'; import 'package:yumi/modules/room/background/room_background_select_page.dart'; import 'package:yumi/modules/room/background/room_background_upload_page.dart'; @@ -11,6 +12,13 @@ import 'package:yumi/modules/room/music/room_music_page.dart'; import 'package:yumi/modules/room/them/room_theme_page.dart'; import 'package:yumi/modules/room/voice_room_page.dart'; import 'package:yumi/app/routes/sc_router_init.dart'; +import 'package:yumi/services/audio/rtc_manager.dart'; +import 'package:yumi/services/audio/rtm_manager.dart'; +import 'package:yumi/services/gift/gift_animation_manager.dart'; +import 'package:yumi/shared/data_sources/sources/local/floating_screen_manager.dart'; +import 'package:yumi/shared/tools/sc_gift_vap_svga_manager.dart'; +import 'package:yumi/shared/tools/sc_room_effect_scheduler.dart'; +import 'package:yumi/ui_kit/widgets/room/anim/room_entrance_widget.dart'; class VoiceRoomRoute implements SCIRouterProvider { static String voiceRoom = '/room'; @@ -51,6 +59,49 @@ class VoiceRoomRoute implements SCIRouterProvider { ); } + static Future _pushBackgroundRoute( + BuildContext context, + Route route, { + bool rootNavigator = false, + }) { + final restoreEffects = _pauseRoomVisualEffects(context); + return Navigator.of( + context, + rootNavigator: rootNavigator, + ).push(route).whenComplete(restoreEffects); + } + + static VoidCallback _pauseRoomVisualEffects(BuildContext context) { + final rtcProvider = context.read(); + final rtmProvider = context.read(); + final giftAnimationManager = context.read(); + 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 openVoiceRoom( BuildContext context, { bool replace = false, @@ -107,11 +158,13 @@ class VoiceRoomRoute implements SCIRouterProvider { BuildContext context, { bool rootNavigator = false, }) { - return Navigator.of(context, rootNavigator: rootNavigator).push( + return _pushBackgroundRoute( + context, _buildDarkRoute( const RoomBackgroundSelectPage(), settings: RouteSettings(name: roomBackgroundSelect), ), + rootNavigator: rootNavigator, ); } @@ -119,11 +172,13 @@ class VoiceRoomRoute implements SCIRouterProvider { BuildContext context, { bool rootNavigator = false, }) { - return Navigator.of(context, rootNavigator: rootNavigator).push( + return _pushBackgroundRoute( + context, _buildDarkRoute( const RoomBackgroundUploadPage(), settings: RouteSettings(name: roomBackgroundUpload), ), + rootNavigator: rootNavigator, ); } @@ -132,11 +187,13 @@ class VoiceRoomRoute implements SCIRouterProvider { String? backgroundPath, bool rootNavigator = false, }) { - return Navigator.of(context, rootNavigator: rootNavigator).push( + return _pushBackgroundRoute( + context, _buildDarkRoute( RoomBackgroundPreviewPage(backgroundPath: backgroundPath), settings: RouteSettings(name: roomBackgroundPreview), ), + rootNavigator: rootNavigator, ); } diff --git a/lib/modules/user/profile/person_detail_page.dart b/lib/modules/user/profile/person_detail_page.dart index c329dfe..d292b35 100644 --- a/lib/modules/user/profile/person_detail_page.dart +++ b/lib/modules/user/profile/person_detail_page.dart @@ -1,1636 +1,1554 @@ -import 'dart:convert'; -import 'dart:ui'; -import 'package:carousel_slider/carousel_slider.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_screenutil/flutter_screenutil.dart'; -import 'package:extended_nested_scroll_view/extended_nested_scroll_view.dart'; -import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; -import 'package:yumi/modules/user/profile/profile/sc_profile_page.dart'; -import 'package:yumi/modules/user/profile/props/sc_giftwall_page.dart'; -import 'package:yumi/ui_kit/components/sc_debounce_widget.dart'; -import 'package:yumi/ui_kit/components/text/sc_text.dart'; -import 'package:yumi/shared/tools/sc_room_utils.dart'; -import 'package:yumi/shared/tools/sc_user_utils.dart'; -import 'package:yumi/modules/index/main_route.dart'; -import 'package:provider/provider.dart'; -import 'package:tencent_cloud_chat_sdk/enum/conversation_type.dart'; -import 'package:tencent_cloud_chat_sdk/models/v2_tim_conversation.dart'; -import 'package:yumi/app_localizations.dart'; -import 'package:yumi/ui_kit/components/appbar/socialchat_appbar.dart'; -import 'package:yumi/ui_kit/components/dialog/dialog_base.dart'; -import 'package:yumi/ui_kit/components/sc_compontent.dart'; -import 'package:yumi/ui_kit/components/sc_tts.dart'; -import 'package:yumi/app/routes/sc_routes.dart'; -import 'package:yumi/app/routes/sc_fluro_navigator.dart'; -import 'package:yumi/shared/tools/sc_loading_manager.dart'; -import 'package:yumi/shared/data_sources/sources/local/user_manager.dart'; -import 'package:yumi/shared/data_sources/sources/repositories/sc_user_repository_impl.dart'; -import 'package:yumi/shared/business_logic/models/res/login_res.dart'; -import 'package:yumi/services/audio/rtm_manager.dart'; -import 'package:yumi/services/auth/user_profile_manager.dart'; -import 'package:yumi/ui_kit/theme/socialchat_theme.dart'; - -import '../../../app/constants/sc_screen.dart'; -import '../../../shared/business_logic/models/res/sc_user_counter_res.dart'; -import '../../../shared/business_logic/models/res/sc_user_identity_res.dart'; -import '../../../shared/business_logic/usecases/sc_fixed_width_tabIndicator.dart'; -import '../../../ui_kit/widgets/id/sc_special_id_badge.dart'; -import '../../chat/chat_route.dart'; - -class PersonDetailPage extends StatefulWidget { - final String isMe; - final String tageId; - - const PersonDetailPage({Key? key, required this.isMe, required this.tageId}) - : super(key: key); - - @override - _PersonDetailPageState createState() => _PersonDetailPageState(); -} - -class _PersonDetailPageState extends State - with SingleTickerProviderStateMixin { - late TabController _tabController; - final List _pages = []; - final List _tabs = []; - SCUserIdentityRes? userIdentity; - - bool isFollow = false; - bool isLoading = true; - bool isBlacklistLoading = true; - bool isBlacklist = false; - - Map counterMap = {}; - - // 添加滚动控制器 - final ScrollController _scrollController = ScrollController(); - double _opacity = 0.0; - - @override - void initState() { - super.initState(); - - // _pages.add(PersonDynamicListPage(false, widget.tageId)); - _pages.add(SCProfilePage(false, widget.tageId)); - _pages.add(SCGiftwallPage(widget.tageId)); - // _pages.add(RelationShipPage(false, widget.tageId)); - _tabController = TabController( - initialIndex: 0, - length: _pages.length, - vsync: this, - ); - _tabController.addListener(() { - setState(() {}); // 刷新UI - }); // 监听切换 - - // 监听滚动 - _scrollController.addListener(() { - if (_scrollController.hasClients) { - final offset = _scrollController.offset; - // 当滚动到一定位置时,头像和昵称应该完全显示在AppBar上 - // 这里计算透明度,可以根据需要调整 - final newOpacity = (offset / 320).clamp(0.0, 1.0); - if (newOpacity != _opacity) { - setState(() { - _opacity = newOpacity; - }); - } - } - }); - - Provider.of(context, listen: false) - .userProfile = null; - Provider.of( - context, - listen: false, - ).getUserInfoById(widget.tageId); - SCAccountRepository().userIdentity(userId: widget.tageId).then((v) { - userIdentity = v; - setState(() {}); - }); - if (widget.isMe != "true") { - SCAccountRepository() - .followCheck(widget.tageId) - .then((v) { - isFollow = v; - isLoading = false; - setState(() {}); - }) - .catchError((e) { - setState(() { - isLoading = false; - }); - }); - SCAccountRepository() - .blacklistCheck(widget.tageId) - .then((v) { - isBlacklist = v; - isBlacklistLoading = false; - setState(() {}); - if (isBlacklist) { - SCTts.show( - SCAppLocalizations.of(context)!.thisUserHasBeenBlacklisted, - ); - } - }) - .catchError((e) { - setState(() { - isBlacklistLoading = false; - }); - }); - } else { - isBlacklist = false; - isBlacklistLoading = false; - } - userCounter(widget.tageId); - } - - void userCounter(String userId) async { - counterMap.clear(); - var userCounterList = await SCAccountRepository().userCounter(userId); - counterMap = Map.fromEntries( - userCounterList.map( - (counter) => MapEntry(counter.counterType ?? "", counter), - ), - ); - setState(() {}); - } - - Widget _buildTab(int index, String text) { - final isSelected = _tabController.index == index; - return Tab(text: text); - } - - @override - void dispose() { - _tabController.dispose(); - _scrollController.dispose(); - super.dispose(); - } - - Widget _buildProfileHeader( - SocialChatUserProfileManager ref, - List backgroundPhotos, - ) { - return SizedBox( - height: 450.w, - child: Stack( - children: [ - SizedBox( - height: 300.w, - width: ScreenUtil().screenWidth, - child: - backgroundPhotos.isNotEmpty - ? CarouselSlider( - options: CarouselOptions( - height: 300.w, - autoPlay: backgroundPhotos.length > 1, - enlargeCenterPage: false, - aspectRatio: 1 / 1, - enableInfiniteScroll: true, - autoPlayAnimationDuration: Duration(milliseconds: 800), - viewportFraction: 1, - onPageChanged: (index, reason) { - setState(() {}); - }, - ), - items: - backgroundPhotos.map((item) { - return GestureDetector( - child: netImage( - url: item.url ?? "", - width: ScreenUtil().screenWidth, - height: 300.w, - fit: BoxFit.cover, - ), - onTap: () {}, - ); - }).toList(), - ) - : Image.asset( - 'sc_images/person/sc_icon_my_head_bg_defalt.png', - width: ScreenUtil().screenWidth, - height: 300.w, - fit: BoxFit.cover, - ), - ), - Positioned( - top: 250.w, - left: 0, - right: 0, - bottom: 0, - child: Container( - decoration: BoxDecoration( - color: Color(0xff083b2f), - borderRadius: BorderRadius.only( - topLeft: Radius.circular(8.w), - topRight: Radius.circular(8.w), - ), - ), - ), - ), - Positioned( - top: 210.w, - left: 0, - right: 0, - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - SizedBox(width: 15.w), - (ref.userProfile?.cpList?.isNotEmpty ?? false) - ? SizedBox( - height: 100.w, - child: Stack( - alignment: Alignment.center, - children: [ - Transform.translate( - offset: Offset(0, -5), - child: Row( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - GestureDetector( - child: netImage( - url: - ref - .userProfile - ?.cpList - ?.first - .meUserAvatar ?? - "", - defaultImg: - "sc_images/general/sc_icon_avar_defalt.png", - width: 56.w, - shape: BoxShape.circle, - ), - onTap: () { - String encodedUrls = - Uri.encodeComponent( - jsonEncode([ - ref - .userProfile - ?.cpList - ?.first - .meUserAvatar, - ]), - ); - SCNavigatorUtils.push( - context, - "${SCMainRoute.imagePreview}?imageUrls=$encodedUrls&initialIndex=0", - ); - }, - ), - SizedBox(width: 32.w), - GestureDetector( - onTap: () { - SCNavigatorUtils.push( - context, - replace: true, - "${SCMainRoute.person}?isMe=${AccountStorage().getCurrentUser()?.userProfile?.id == ref.userProfile?.cpList?.first.cpUserId}&tageId=${ref.userProfile?.cpList?.first.cpUserId}", - ); - }, - child: netImage( - url: - ref - .userProfile - ?.cpList - ?.first - .cpUserAvatar ?? - "", - defaultImg: - "sc_images/general/sc_icon_avar_defalt.png", - width: 56.w, - shape: BoxShape.circle, - ), - ), - ], - ), - ), - IgnorePointer( - child: Transform.translate( - offset: Offset(0, -15), - child: Image.asset( - "sc_images/person/sc_icon_send_cp_requst_dialog_head.png", - ), - ), - ), - ], - ), - ) - : GestureDetector( - child: head( - url: ref.userProfile?.userAvatar ?? "", - width: 88.w, - headdress: - ref.userProfile?.getHeaddress()?.sourceUrl, - ), - onTap: () { - String encodedUrls = Uri.encodeComponent( - jsonEncode([ref.userProfile?.userAvatar]), - ); - SCNavigatorUtils.push( - context, - "${SCMainRoute.imagePreview}?imageUrls=$encodedUrls&initialIndex=0", - ); - }, - ), - ], - ), - SizedBox(height: 6.w), - Row( - children: [ - SizedBox(width: 25.w), - socialchatNickNameText( - maxWidth: 135.w, - ref.userProfile?.userNickname ?? "", - fontSize: 18.sp, - textColor: Colors.white, - fontWeight: FontWeight.w600, - type: ref.userProfile?.getVIP()?.name ?? "", - needScroll: - (ref.userProfile?.userNickname?.characters.length ?? - 0) > - 13, - ), - SizedBox(width: 3.w), - getVIPBadge( - ref.userProfile?.getVIP()?.name, - width: 45.w, - height: 25.w, - ), - Row( - children: [ - SizedBox(width: 5.w), - Container( - width: - (ref.userProfile?.age ?? 0) > 999 ? 58.w : 48.w, - height: 24.w, - decoration: BoxDecoration( - image: DecorationImage( - image: AssetImage( - ref.userProfile?.userSex == 0 - ? "sc_images/login/sc_icon_sex_woman_bg.png" - : "sc_images/login/sc_icon_sex_man_bg.png", - ), - ), - ), - child: Row( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - xb(ref.userProfile?.userSex), - text( - "${ref.userProfile?.age}", - textColor: Colors.white, - fontSize: 14.sp, - fontWeight: FontWeight.w600, - ), - SizedBox(width: 3.w), - ], - ), - ), - ], - ), - ], - ), - SizedBox(height: 5.w), - Row( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - SizedBox(width: 25.w), - SCSpecialIdBadge( - idText: ref.userProfile?.getID() ?? "", - showAnimated: ref.userProfile?.hasSpecialId() ?? false, - assetPath: SCSpecialIdAssets.userIdLarge, - animationWidth: 80.w, - animationHeight: 32.w, - showTextBesideAnimated: true, - animatedTextSpacing: 0, - showAnimatedGradientText: - ref.userProfile?.shouldShowColoredSpecialIdText() ?? - false, - animationTextStyle: TextStyle( - color: Colors.white, - fontSize: 16.sp, - fontWeight: FontWeight.w700, - ), - normalTextStyle: TextStyle( - color: Colors.white, - fontSize: 16.sp, - fontWeight: FontWeight.w400, - ), - animationFit: BoxFit.contain, - ), - ], - ), - SizedBox(height: 5.w), - Consumer( - builder: (context, ref, child) { - return Container( - height: 72.w, - alignment: AlignmentDirectional.center, - width: ScreenUtil().screenWidth, - margin: EdgeInsets.symmetric(horizontal: 18.w), - padding: EdgeInsets.symmetric(horizontal: 35.w), - child: Row( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - GestureDetector( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - text( - "${counterMap["INTERVIEW"]?.quantity ?? 0}", - fontSize: 17.sp, - textColor: Colors.white, - fontWeight: FontWeight.bold, - ), - text( - SCAppLocalizations.of(context)!.vistors, - fontSize: 14.sp, - textColor: Color(0xffB1B1B1), - ), - ], - ), - onTap: () { - if (widget.isMe == "true") { - SCNavigatorUtils.push( - context, - SCMainRoute.vistors, - ); - } - }, - ), - Container( - decoration: BoxDecoration( - gradient: LinearGradient( - colors: [ - Color(0xff333333).withOpacity(0.0), - Color(0xff333333), - Color(0xff333333).withOpacity(0.0), - ], - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - ), - ), - height: 25.w, - width: 1.w, - ), - GestureDetector( - onTap: () { - if (widget.isMe == "true") { - SCNavigatorUtils.push( - context, - SCMainRoute.follow, - ); - } - }, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - text( - "${counterMap["SUBSCRIPTION"]?.quantity ?? 0}", - fontSize: 17.sp, - textColor: Colors.white, - fontWeight: FontWeight.bold, - ), - text( - SCAppLocalizations.of(context)!.follow, - fontSize: 14.sp, - textColor: Color(0xffB1B1B1), - ), - ], - ), - ), - Container( - decoration: BoxDecoration( - gradient: LinearGradient( - colors: [ - Color(0xff333333).withOpacity(0.0), - Color(0xff333333), - Color(0xff333333).withOpacity(0.0), - ], - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - ), - ), - height: 25.w, - width: 1.w, - ), - GestureDetector( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - text( - "${counterMap["FANS"]?.quantity ?? 0}", - fontSize: 17.sp, - textColor: Colors.white, - fontWeight: FontWeight.bold, - ), - text( - SCAppLocalizations.of(context)!.fans, - fontSize: 14.sp, - textColor: Color(0xffB1B1B1), - ), - ], - ), - onTap: () { - if (widget.isMe == "true") { - SCNavigatorUtils.push( - context, - SCMainRoute.fans, - ); - } - }, - ), - ], - ), - ); - }, - ), - SizedBox(height: 6.w), - ], - ), - ), - ], - ), - ); - } - - Widget _buildSkeletonPiece({ - double? width, - required double height, - BorderRadius? borderRadius, - BoxShape shape = BoxShape.rectangle, - }) { - return Container( - width: width, - height: height, - decoration: BoxDecoration( - color: Colors.white.withOpacity(0.14), - borderRadius: - shape == BoxShape.circle - ? null - : (borderRadius ?? BorderRadius.circular(8.w)), - shape: shape, - ), - ); - } - - Widget _buildProfileSkeleton() { - return SingleChildScrollView( - physics: const NeverScrollableScrollPhysics(), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - SizedBox(height: 210.w), - Padding( - padding: EdgeInsets.only(left: 15.w), - child: _buildSkeletonPiece( - width: 88.w, - height: 88.w, - shape: BoxShape.circle, - ), - ), - SizedBox(height: 10.w), - Padding( - padding: EdgeInsets.only(left: 25.w), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _buildSkeletonPiece(width: 150.w, height: 24.w), - SizedBox(height: 10.w), - _buildSkeletonPiece(width: 110.w, height: 18.w), - ], - ), - ), - SizedBox(height: 12.w), - Container( - height: 72.w, - alignment: AlignmentDirectional.center, - width: ScreenUtil().screenWidth, - margin: EdgeInsets.symmetric(horizontal: 18.w), - padding: EdgeInsets.symmetric(horizontal: 35.w), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: List.generate( - 3, - (_) => Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - _buildSkeletonPiece(width: 24.w, height: 22.w), - SizedBox(height: 8.w), - _buildSkeletonPiece(width: 58.w, height: 14.w), - ], - ), - ), - ), - ), - SizedBox(height: 6.w), - Container( - padding: EdgeInsets.symmetric(horizontal: 3.w), - decoration: const BoxDecoration( - gradient: LinearGradient( - colors: [Color(0xff083b2f), Color(0xff083b2f)], - begin: AlignmentDirectional.topStart, - end: AlignmentDirectional.bottomEnd, - ), - ), - child: Column( - children: [ - SizedBox(height: 10.w), - Padding( - padding: EdgeInsets.symmetric(horizontal: 10.w), - child: Row( - children: [ - _buildSkeletonPiece(width: 92.w, height: 18.w), - SizedBox(width: 14.w), - _buildSkeletonPiece(width: 72.w, height: 18.w), - ], - ), - ), - SizedBox(height: 24.w), - Padding( - padding: EdgeInsets.symmetric(horizontal: 18.w), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _buildSkeletonPiece(width: 100.w, height: 22.w), - SizedBox(height: 18.w), - _buildSkeletonPiece(width: 220.w, height: 16.w), - SizedBox(height: 14.w), - _buildSkeletonPiece(width: 190.w, height: 16.w), - SizedBox(height: 14.w), - _buildSkeletonPiece(width: 240.w, height: 16.w), - SizedBox(height: 14.w), - _buildSkeletonPiece(width: 200.w, height: 16.w), - SizedBox(height: 14.w), - _buildSkeletonPiece(width: 160.w, height: 16.w), - ], - ), - ), - SizedBox(height: 220.w), - ], - ), - ), - ], - ), - ); - } - - @override - Widget build(BuildContext context) { - _tabs.clear(); - // _tabs.add(_buildTab(0, SCAppLocalizations.of(context)!.dynamicT)); - _tabs.add(_buildTab(0, SCAppLocalizations.of(context)!.aboutMe)); - _tabs.add(_buildTab(1, SCAppLocalizations.of(context)!.giftwall)); - // _tabs.add(_buildTab(3, SCAppLocalizations.of(context)!.relationShip)); - return Consumer( - builder: (context, ref, child) { - final bool isProfileLoading = ref.userProfile == null; - List backgroundPhotos = []; - backgroundPhotos = - (isProfileLoading - ? [] - : (ref.userProfile?.backgroundPhotos ?? [])) - .where((t) => t.status == 1) - .toList(); - return Directionality( - textDirection: - window.locale.languageCode == "ar" - ? TextDirection.rtl - : TextDirection.ltr, - child: SafeArea( - top: false, - child: Stack( - children: [ - Positioned.fill( - child: Column( - children: [ - SizedBox( - height: 300.w, - width: ScreenUtil().screenWidth, - child: - backgroundPhotos.isNotEmpty - ? netImage( - url: backgroundPhotos.first.url ?? "", - width: ScreenUtil().screenWidth, - height: 300.w, - fit: BoxFit.cover, - ) - : Image.asset( - 'sc_images/person/sc_icon_my_head_bg_defalt.png', - width: ScreenUtil().screenWidth, - height: 300.w, - fit: BoxFit.cover, - ), - ), - Expanded( - child: Container(color: const Color(0xff083b2f)), - ), - ], - ), - ), - Scaffold( - extendBodyBehindAppBar: true, - resizeToAvoidBottomInset: false, - backgroundColor: Colors.transparent, - appBar: SocialChatStandardAppBar( - backButtonColor: Colors.white, - title: "", - leading: GestureDetector( - behavior: HitTestBehavior.opaque, - onTap: () { - Navigator.pop(context); - }, - child: Container( - width: width(50), - height: height(30), - alignment: AlignmentDirectional.centerStart, - padding: EdgeInsetsDirectional.only(start: width(15)), - child: Icon( - window.locale.languageCode == "ar" - ? Icons.keyboard_arrow_right - : Icons.keyboard_arrow_left, - size: 28.w, - color: Colors.white, - ), - ), - ), - actions: [ - // 在AppBar中显示头像和昵称 - if (_opacity > 0.5 && !isProfileLoading) ...[ - SizedBox(width: 45.w), - head( - url: ref.userProfile?.userAvatar ?? "", - width: 50.w, - height: 50.w, - ), - SizedBox(width: 8.w), - socialchatNickNameText( - maxWidth: 135.w, - textColor: Colors.white, - ref.userProfile?.userNickname ?? "", - fontSize: 16.sp, - fontWeight: FontWeight.w600, - type: ref.userProfile?.getVIP()?.name ?? "", - needScroll: - (ref - .userProfile - ?.userNickname - ?.characters - .length ?? - 0) > - 13, - ), - SizedBox(width: 8.w), - ], - Spacer(), - Builder( - builder: (ct) { - return IconButton( - icon: - widget.isMe == "true" - ? Image.asset( - "sc_images/person/sc_icon_edit_user_info2.png", - width: 22.w, - color: Colors.white, - ) - : Icon( - Icons.more_horiz, - size: 22.w, - color: Colors.white, - ), - onPressed: () { - if (widget.isMe == "true") { - SCNavigatorUtils.push( - context, - SCMainRoute.edit, - replace: false, - ); - return; - } - SmartDialog.showAttach( - tag: "showUserInfoOptMenu", - targetContext: ct, - alignment: Alignment.bottomCenter, - maskColor: Colors.transparent, - animationType: SmartAnimationType.fade, - scalePointBuilder: - (selfSize) => Offset(selfSize.width, 10), - builder: (_) { - return GestureDetector( - child: Transform.translate( - offset: Offset(0, -10), - child: Container( - width: 163.w, - margin: EdgeInsetsDirectional.only( - end: 8.w, - ), - decoration: BoxDecoration( - color: Colors.white30, - borderRadius: BorderRadius.circular( - 4, - ), - ), - padding: EdgeInsets.symmetric( - horizontal: 8.w, - vertical: 5.w, - ), - child: Column( - mainAxisSize: MainAxisSize.min, - children: [ - GestureDetector( - child: text( - SCAppLocalizations.of( - context, - )!.report, - textColor: Colors.white, - fontSize: 12.sp, - ), - onTap: () { - SmartDialog.dismiss( - tag: "showUserInfoOptMenu", - ); - SCNavigatorUtils.push( - context, - "${SCMainRoute.report}?type=user&tageId=${ref.userProfile?.id}", - replace: false, - ); - }, - ), - ((!(userIdentity?.admin ?? false) && - !(userIdentity - ?.superAdmin ?? - false)) && - ((Provider.of< - SocialChatUserProfileManager - >( - context, - listen: false, - ).userIdentity?.admin ?? - false))) - ? GestureDetector( - child: Container( - margin: EdgeInsets.only( - top: 5.w, - ), - child: text( - SCAppLocalizations.of( - context, - )!.userEditing, - letterSpacing: 0.1, - textColor: Colors.white, - fontSize: 12.sp, - ), - ), - onTap: () { - SmartDialog.dismiss( - tag: - "showUserInfoOptMenu", - ); - SCNavigatorUtils.push( - context, - "${SCMainRoute.editingUserRoomAdmin}?type=User&profile=${Uri.encodeComponent(jsonEncode(ref.userProfile?.toJson()))}", - ); - }, - ) - : Container(), - ((ref.userIdentity?.admin ?? - false) || - (ref - .userIdentity - ?.superFreightAgent ?? - false)) && - !((userIdentity?.admin ?? - false) || - (userIdentity - ?.superFreightAgent ?? - false)) - ? SizedBox(height: 5.w) - : Container(), - ((ref.userIdentity?.admin ?? - false) || - (ref - .userIdentity - ?.superFreightAgent ?? - false)) && - !((userIdentity?.admin ?? - false) || - (userIdentity - ?.superFreightAgent ?? - false)) - ? GestureDetector( - child: text( - letterSpacing: 0.1, - SCAppLocalizations.of( - context, - )!.becomeAgent, - textColor: Colors.white, - fontSize: 12.sp, - ), - onTap: () { - SmartDialog.dismiss( - tag: - "showUserInfoOptMenu", - ); - SCAccountRepository() - .teamCreate( - ref.userProfile - ?.getID() ?? - "", - ); - }, - ) - : Container(), - (ref.userProfile?.isCpRelation ?? - false) - ? GestureDetector( - behavior: - HitTestBehavior.opaque, - child: Container( - padding: EdgeInsets.only( - top: 3.w, - ), - child: text( - letterSpacing: 0.1, - SCAppLocalizations.of( - context, - )!.partWays, - textColor: Colors.white, - fontSize: 12.sp, - ), - ), - onTap: () { - SmartDialog.dismiss( - tag: - "showUserInfoOptMenu", - ); - _showPartWaysDialog(ref); - }, - ) - : Container(), - isBlacklistLoading - ? Container() - : GestureDetector( - behavior: - HitTestBehavior.opaque, - child: Container( - padding: EdgeInsets.only( - top: 3.w, - ), - child: text( - letterSpacing: 0.1, - isBlacklist - ? SCAppLocalizations.of( - context, - )!.removeFromBlacklist - : SCAppLocalizations.of( - context, - )!.moveToBlacklist, - textColor: Colors.white, - fontSize: 12.sp, - ), - ), - onTap: () { - if ((ref - .userProfile - ?.isCpRelation ?? - false)) { - SmartDialog.show( - tag: - "showConfirmDialog", - alignment: - Alignment.center, - debounce: true, - animationType: - SmartAnimationType - .fade, - builder: (_) { - return MsgDialog( - title: - SCAppLocalizations.of( - context, - )!.tips, - msg: - SCAppLocalizations.of( - context, - )!.youAreCurrentlyCPRelationshipPleaseDissolve, - btnText: - SCAppLocalizations.of( - context, - )!.confirm, - onEnsure: () {}, - ); - }, - ); - return; - } - SCAccountHelper.optBlacklist( - widget.tageId, - isBlacklist, - context, - (isOptBlacklist) { - isBlacklist = - isOptBlacklist; - setState(() {}); - }, - ); - }, - ), - ], - ), - ), - ), - onTap: () { - SmartDialog.dismiss( - tag: "showUserInfoOptMenu", - ); - }, - ); - }, - ); - }, - ); - }, - ), - ], - ), - body: - isBlacklist - ? Container() - : (isProfileLoading || isBlacklistLoading - ? _buildProfileSkeleton() - : ExtendedNestedScrollView( - controller: _scrollController, - onlyOneScrollInBody: true, - headerSliverBuilder: ( - BuildContext context, - bool innerBoxIsScrolled, - ) { - return [ - SliverToBoxAdapter( - child: _buildProfileHeader( - ref, - backgroundPhotos, - ), - ), - ]; - }, - body: Container( - padding: EdgeInsets.symmetric( - horizontal: 3.w, - ), - decoration: BoxDecoration( - gradient: - ref.userProfile?.userSex == 0 - ? LinearGradient( - colors: [ - Color(0xff083b2f), - Color(0xff083b2f), - ], - begin: - AlignmentDirectional.topStart, - end: - AlignmentDirectional - .bottomEnd, - ) - : LinearGradient( - colors: [ - Color(0xff083b2f), - Color(0xff083b2f), - ], - begin: - AlignmentDirectional.topStart, - end: - AlignmentDirectional - .bottomEnd, - ), - ), - child: Column( - children: [ - Row( - children: [ - SizedBox(width: 6.w), - SizedBox( - height: 35.w, - child: TabBar( - tabAlignment: TabAlignment.start, - indicator: - SCFixedWidthTabIndicator( - width: 20.w, - height: 4.w, - gradient: - ref - .userProfile - ?.userSex == - 0 - ? LinearGradient( - colors: [ - Color( - 0xffFFD800, - ), - Color( - 0xffFFD800, - ), - ], - ) - : LinearGradient( - colors: [ - Color( - 0xffFFD800, - ), - Color( - 0xffFFD800, - ), - ], - ), - ), - labelPadding: - EdgeInsets.symmetric( - horizontal: 12.w, - ), - labelColor: Colors.white, - isScrollable: true, - unselectedLabelColor: Color( - 0xffB1B1B1, - ), - labelStyle: TextStyle( - fontSize: 15.sp, - fontWeight: FontWeight.bold, - ), - unselectedLabelStyle: TextStyle( - fontSize: 13.sp, - ), - indicatorColor: - Colors.transparent, - dividerColor: Colors.transparent, - controller: _tabController, - tabs: _tabs, - ), - ), - SizedBox(width: 6.w), - ], - ), - Expanded( - child: TabBarView( - controller: _tabController, - children: _pages, - ), - ), - SizedBox(height: 25.w), - ], - ), - ), - )), - ), - // 底部按钮区域 - isBlacklistLoading || isBlacklist || isProfileLoading - ? Container() - : Positioned( - bottom: 0, - left: 0, - right: 0, - child: - widget.isMe == "true" - ? Container() - : (isLoading - ? Container() - : Container( - alignment: AlignmentDirectional.center, - width: ScreenUtil().screenWidth, - height: 75.w, - decoration: BoxDecoration( - color: Color(0xff18F2B1).withOpacity(0.1), - borderRadius: BorderRadius.only( - topLeft: Radius.circular(10.w), - topRight: Radius.circular(10.w), - ), - ), - child: Column( - children: [ - SizedBox(height: 10.w), - Row( - mainAxisAlignment: - MainAxisAlignment.center, - children: [ - SCDebounceWidget( - debounceTime: Duration( - milliseconds: 800, - ), - child: SizedBox( - width: 110.w, - child: Column( - mainAxisAlignment: - MainAxisAlignment.center, - children: [ - Image.asset( - "sc_images/person/sc_icon_person_in_room.png", - width: 23.w, - ), - SizedBox(height: 5.w), - Container( - margin: EdgeInsets.only( - bottom: 0.w, - ), - child: text( - SCAppLocalizations.of( - context, - )!.inRoom, - textColor: - SocialChatTheme - .primaryLight, - fontWeight: - FontWeight.w600, - fontSize: 14.sp, - ), - ), - ], - ), - ), - onTap: () { - if ((ref - .userProfile - ?.inRoomId ?? - "") - .isNotEmpty) { - SCRoomUtils.goRoom( - ref.userProfile?.inRoomId ?? - "", - context, - ); - } - }, - ), - SizedBox(width: 10.w), - SCDebounceWidget( - debounceTime: Duration( - milliseconds: 800, - ), - child: Container( - width: 110.w, - child: Column( - mainAxisAlignment: - MainAxisAlignment.center, - children: [ - Image.asset( - "sc_images/person/sc_icon_person_tochat.png", - width: 23.w, - ), - SizedBox(height: 5.w), - Container( - margin: EdgeInsets.only( - bottom: 0.w, - ), - child: text( - SCAppLocalizations.of( - context, - )!.message, - textColor: - SocialChatTheme - .primaryLight, - fontWeight: - FontWeight.w600, - fontSize: 14.sp, - ), - ), - ], - ), - ), - onTap: () async { - var conversation = - V2TimConversation( - type: - ConversationType - .V2TIM_C2C, - userID: widget.tageId, - conversationID: '', - ); - var bool = await Provider.of< - RtmProvider - >( - context, - listen: false, - ).startConversation( - conversation, - ); - if (!bool) return; - var json = jsonEncode( - conversation.toJson(), - ); - SCNavigatorUtils.push( - context, - "${SCChatRouter.chat}?conversation=${Uri.encodeComponent(json)}", - ); - }, - ), - SizedBox(width: 10.w), - SCDebounceWidget( - debounceTime: Duration( - milliseconds: 800, - ), - child: Container( - width: 110.w, - child: Column( - mainAxisAlignment: - MainAxisAlignment.center, - children: [ - Image.asset( - "sc_images/person/sc_icon_person_follow.png", - width: 23.w, - ), - SizedBox(height: 5.w), - Container( - margin: EdgeInsets.only( - bottom: 0.w, - ), - child: text( - isFollow - ? SCAppLocalizations.of( - context, - )!.following - : SCAppLocalizations.of( - context, - )!.follow, - textColor: - SocialChatTheme - .primaryLight, - fontWeight: - FontWeight.w600, - fontSize: 14.sp, - ), - ), - ], - ), - ), - onTap: () { - SCAccountRepository() - .followUser(widget.tageId) - .then((v) { - isFollow = !isFollow; - setState(() {}); - }); - }, - ), - ], - ), - ], - ), - )), - ), - ], - ), - ), - ); - }, - ); - } - - void _showPartWaysDialog(SocialChatUserProfileManager ref) { - SmartDialog.show( - tag: "showPartWaysDialog", - alignment: Alignment.center, - debounce: true, - animationType: SmartAnimationType.fade, - builder: (_) { - return Container( - height: 530.w, - width: ScreenUtil().screenWidth * 0.9, - decoration: BoxDecoration( - image: DecorationImage( - image: AssetImage( - "sc_images/person/sc_icon_send_cp_requst_dialog_bg.png", - ), - fit: BoxFit.fill, - ), - ), - child: Column( - children: [ - SizedBox(height: 58.w), - Container( - child: text( - SCAppLocalizations.of(context)!.partWays, - fontSize: 22.sp, - textColor: Color(0xffDB5872), - fontWeight: FontWeight.bold, - ), - ), - Transform.translate( - offset: Offset(0, -25), - child: Stack( - alignment: AlignmentDirectional.center, - children: [ - PositionedDirectional( - top: 28.w, - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - head( - url: - AccountStorage() - .getCurrentUser() - ?.userProfile - ?.userAvatar ?? - "", - width: 90.w, - ), - SizedBox(width: 15.w), - head( - url: ref.userProfile?.userAvatar ?? "", - width: 90.w, - ), - ], - ), - ), - Image.asset( - "sc_images/person/sc_icon_send_cp_requst_dialog_head2.png", - height: 120.w, - fit: BoxFit.fill, - ), - ], - ), - ), - Transform.translate( - offset: Offset(0, -23), - child: Container( - height: 25.w, - width: ScreenUtil().screenWidth * 0.6, - decoration: BoxDecoration( - image: DecorationImage( - image: AssetImage( - "sc_images/person/sc_icon_send_cp_requst_username_bg.png", - ), - fit: BoxFit.fill, - ), - ), - child: Row( - children: [ - socialchatNickNameText( - maxWidth: 100.w, - AccountStorage() - .getCurrentUser() - ?.userProfile - ?.userNickname ?? - "", - fontSize: 10.sp, - fontWeight: FontWeight.w600, - type: - AccountStorage() - .getCurrentUser() - ?.userProfile - ?.getVIP() - ?.name ?? - "", - needScroll: - (AccountStorage() - .getCurrentUser() - ?.userProfile - ?.userNickname - ?.characters - .length ?? - 0) > - 8, - ), - SizedBox(width: 20.w), - socialchatNickNameText( - maxWidth: 100.w, - ref.userProfile?.userNickname ?? "", - fontSize: 10.sp, - fontWeight: FontWeight.w600, - type: ref.userProfile?.getVIP()?.name ?? "", - needScroll: - (ref.userProfile?.userNickname?.characters.length ?? - 0) > - 8, - ), - ], - ), - ), - ), - - Container( - padding: EdgeInsets.only(bottom: 12.w), - margin: EdgeInsets.symmetric(horizontal: 18.w), - decoration: BoxDecoration( - image: DecorationImage( - image: AssetImage( - "sc_images/person/sc_icon_send_cp_requst_dialog_content.png", - ), - fit: BoxFit.fill, - ), - ), - child: Column( - children: [ - Container( - margin: EdgeInsets.symmetric( - horizontal: 35.w, - ).copyWith(top: 45.w), - child: text( - SCAppLocalizations.of( - context, - )!.areYouSureYouWantToPartWaysWithYourCP, - fontWeight: FontWeight.w500, - textColor: Color(0xffFF79A1), - maxLines: 3, - fontSize: 14.sp, - ), - ), - Container( - margin: EdgeInsets.symmetric(horizontal: 20.w), - alignment: AlignmentDirectional.center, - child: text( - SCAppLocalizations.of(context)!.partWaysTips, - fontSize: 10.w, - textColor: Color(0xffFE91B0), - maxLines: 6, - fontWeight: FontWeight.w500, - ), - ), - ], - ), - ), - SizedBox(height: 10.w), - Row( - mainAxisSize: MainAxisSize.min, - children: [ - GestureDetector( - child: Container( - padding: EdgeInsets.only(top: 7.w), - alignment: AlignmentDirectional.center, - width: 130.w, - height: 48.w, - decoration: BoxDecoration( - image: DecorationImage( - image: AssetImage( - "sc_images/person/sc_icon_send_cp_requst_ok_bg.png", - ), - fit: BoxFit.fill, - ), - ), - child: text( - SCAppLocalizations.of(context)!.cancel, - fontSize: 18.sp, - fontWeight: FontWeight.bold, - textColor: Color(0xffDB5872), - ), - ), - onTap: () { - SmartDialog.dismiss(tag: "showPartWaysDialog"); - }, - ), - SizedBox(width: 25.w), - GestureDetector( - child: Container( - padding: EdgeInsets.only(top: 7.w), - alignment: AlignmentDirectional.center, - width: 130.w, - height: 48.w, - decoration: BoxDecoration( - image: DecorationImage( - image: AssetImage( - "sc_images/person/sc_icon_send_cp_requst_cancel_bg.png", - ), - fit: BoxFit.fill, - ), - ), - child: text( - SCAppLocalizations.of(context)!.confirm, - fontSize: 18.sp, - fontWeight: FontWeight.bold, - textColor: Colors.white, - ), - ), - onTap: () { - SCLoadingManager.show(); - SCAccountRepository() - .cpRelationshipDismissApply(ref.userProfile?.id ?? "") - .then((result) { - SmartDialog.dismiss(tag: "showPartWaysDialog"); - SCTts.show( - SCAppLocalizations.of( - context, - )!.operationSuccessful, - ); - SCLoadingManager.hide(); - SCNavigatorUtils.popUntil( - context, - ModalRoute.withName(SCRoutes.home), - ); - }) - .catchError((e) { - SCLoadingManager.hide(); - }); - }, - ), - ], - ), - ], - ), - ); - }, - ); - } -} +import 'dart:convert'; +import 'dart:ui'; +import 'package:carousel_slider/carousel_slider.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; +import 'package:flutter/services.dart'; +import 'package:extended_nested_scroll_view/extended_nested_scroll_view.dart'; +import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; +import 'package:yumi/modules/user/profile/profile/sc_profile_page.dart'; +import 'package:yumi/modules/user/profile/props/sc_giftwall_page.dart'; +import 'package:yumi/ui_kit/components/sc_debounce_widget.dart'; +import 'package:yumi/ui_kit/components/text/sc_text.dart'; +import 'package:yumi/shared/tools/sc_room_utils.dart'; +import 'package:yumi/shared/tools/sc_user_utils.dart'; +import 'package:yumi/modules/index/main_route.dart'; +import 'package:provider/provider.dart'; +import 'package:tencent_cloud_chat_sdk/enum/conversation_type.dart'; +import 'package:tencent_cloud_chat_sdk/models/v2_tim_conversation.dart'; +import 'package:yumi/app_localizations.dart'; +import 'package:yumi/ui_kit/components/appbar/socialchat_appbar.dart'; +import 'package:yumi/ui_kit/components/dialog/dialog_base.dart'; +import 'package:yumi/ui_kit/components/sc_compontent.dart'; +import 'package:yumi/ui_kit/components/sc_tts.dart'; +import 'package:yumi/app/routes/sc_routes.dart'; +import 'package:yumi/app/routes/sc_fluro_navigator.dart'; +import 'package:yumi/shared/tools/sc_loading_manager.dart'; +import 'package:yumi/shared/data_sources/sources/local/user_manager.dart'; +import 'package:yumi/shared/data_sources/sources/repositories/sc_user_repository_impl.dart'; +import 'package:yumi/shared/business_logic/models/res/login_res.dart'; +import 'package:yumi/services/audio/rtm_manager.dart'; +import 'package:yumi/services/auth/user_profile_manager.dart'; +import 'package:yumi/services/general/sc_app_general_manager.dart'; +import 'package:yumi/ui_kit/theme/socialchat_theme.dart'; + +import '../../../app/constants/sc_screen.dart'; +import '../../../shared/business_logic/models/res/sc_user_counter_res.dart'; +import '../../../shared/business_logic/models/res/sc_user_identity_res.dart'; +import '../../../shared/business_logic/usecases/sc_fixed_width_tabIndicator.dart'; +import '../../../ui_kit/widgets/id/sc_special_id_badge.dart'; +import '../../chat/chat_route.dart'; + +class PersonDetailPage extends StatefulWidget { + final String isMe; + final String tageId; + + const PersonDetailPage({Key? key, required this.isMe, required this.tageId}) + : super(key: key); + + @override + _PersonDetailPageState createState() => _PersonDetailPageState(); +} + +class _PersonDetailPageState extends State + with SingleTickerProviderStateMixin { + late TabController _tabController; + final List _pages = []; + final List _tabs = []; + SCUserIdentityRes? userIdentity; + + bool isFollow = false; + bool isLoading = true; + bool isBlacklistLoading = true; + bool isBlacklist = false; + + Map counterMap = {}; + + // 添加滚动控制器 + final ScrollController _scrollController = ScrollController(); + double _opacity = 0.0; + + @override + void initState() { + super.initState(); + + // _pages.add(PersonDynamicListPage(false, widget.tageId)); + _pages.add(SCProfilePage(false, widget.tageId)); + _pages.add(SCGiftwallPage(widget.tageId)); + // _pages.add(RelationShipPage(false, widget.tageId)); + _tabController = TabController( + initialIndex: 0, + length: _pages.length, + vsync: this, + ); + _tabController.addListener(() { + setState(() {}); // 刷新UI + }); // 监听切换 + + // 监听滚动 + _scrollController.addListener(() { + if (_scrollController.hasClients) { + final offset = _scrollController.offset; + // 当滚动到一定位置时,头像和昵称应该完全显示在AppBar上 + // 这里计算透明度,可以根据需要调整 + final newOpacity = (offset / 320).clamp(0.0, 1.0); + if (newOpacity != _opacity) { + setState(() { + _opacity = newOpacity; + }); + } + } + }); + + Provider.of(context, listen: false) + .userProfile = null; + Provider.of( + context, + listen: false, + ).getUserInfoById(widget.tageId); + SCAccountRepository().userIdentity(userId: widget.tageId).then((v) { + userIdentity = v; + setState(() {}); + }); + if (widget.isMe != "true") { + SCAccountRepository() + .followCheck(widget.tageId) + .then((v) { + isFollow = v; + isLoading = false; + setState(() {}); + }) + .catchError((e) { + setState(() { + isLoading = false; + }); + }); + SCAccountRepository() + .blacklistCheck(widget.tageId) + .then((v) { + isBlacklist = v; + isBlacklistLoading = false; + setState(() {}); + if (isBlacklist) { + SCTts.show( + SCAppLocalizations.of(context)!.thisUserHasBeenBlacklisted, + ); + } + }) + .catchError((e) { + setState(() { + isBlacklistLoading = false; + }); + }); + } else { + isBlacklist = false; + isBlacklistLoading = false; + } + userCounter(widget.tageId); + } + + void userCounter(String userId) async { + counterMap.clear(); + var userCounterList = await SCAccountRepository().userCounter(userId); + counterMap = Map.fromEntries( + userCounterList.map( + (counter) => MapEntry(counter.counterType ?? "", counter), + ), + ); + setState(() {}); + } + + Widget _buildTab(int index, String text) { + return Tab(text: text); + } + + @override + void dispose() { + _tabController.dispose(); + _scrollController.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( + context, + listen: false, + ).findCountryByName(countryName)?.nationalFlag ?? + ""; + } catch (_) { + flag = ""; + } + if (flag.isEmpty) return const SizedBox.shrink(); + return ClipRRect( + borderRadius: BorderRadius.circular(2.w), + child: netImage( + url: flag, + width: 24.w, + height: 16.w, + fit: BoxFit.cover, + noDefaultImg: true, + ), + ); + } + + Widget _buildProfileHeader( + SocialChatUserProfileManager ref, + List backgroundPhotos, + ) { + return SizedBox( + height: 450.w, + child: Stack( + children: [ + SizedBox( + height: 300.w, + width: ScreenUtil().screenWidth, + child: + backgroundPhotos.isNotEmpty + ? CarouselSlider( + options: CarouselOptions( + height: 300.w, + autoPlay: backgroundPhotos.length > 1, + enlargeCenterPage: false, + aspectRatio: 1 / 1, + enableInfiniteScroll: true, + autoPlayAnimationDuration: Duration(milliseconds: 800), + viewportFraction: 1, + onPageChanged: (index, reason) { + setState(() {}); + }, + ), + items: + backgroundPhotos.map((item) { + return GestureDetector( + child: netImage( + url: item.url ?? "", + width: ScreenUtil().screenWidth, + height: 300.w, + fit: BoxFit.cover, + ), + onTap: () {}, + ); + }).toList(), + ) + : Image.asset( + 'sc_images/person/sc_icon_profile_card_default_bg.png', + width: ScreenUtil().screenWidth, + height: 300.w, + fit: BoxFit.cover, + ), + ), + Positioned( + top: 250.w, + left: 0, + right: 0, + bottom: 0, + child: Container(color: Color(0xff083b2f)), + ), + Positioned( + top: 210.w, + left: 0, + right: 0, + child: Column( + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + _buildHeaderAvatar(ref), + SizedBox(height: 6.w), + _buildHeaderName(ref), + SizedBox(height: 5.w), + _buildHeaderMeta(ref), + SizedBox(height: 5.w), + Consumer( + builder: (context, ref, child) { + return Container( + height: 72.w, + alignment: AlignmentDirectional.center, + width: ScreenUtil().screenWidth, + margin: EdgeInsets.symmetric(horizontal: 18.w), + padding: EdgeInsets.symmetric(horizontal: 35.w), + child: Row( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + GestureDetector( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + text( + "${counterMap["INTERVIEW"]?.quantity ?? 0}", + fontSize: 17.sp, + textColor: Colors.white, + fontWeight: FontWeight.bold, + ), + text( + SCAppLocalizations.of(context)!.vistors, + fontSize: 14.sp, + textColor: Color(0xffB1B1B1), + ), + ], + ), + onTap: () { + if (widget.isMe == "true") { + SCNavigatorUtils.push( + context, + SCMainRoute.vistors, + ); + } + }, + ), + Container( + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + Color(0xff333333).withOpacity(0.0), + Color(0xff333333), + Color(0xff333333).withOpacity(0.0), + ], + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + ), + ), + height: 25.w, + width: 1.w, + ), + GestureDetector( + onTap: () { + if (widget.isMe == "true") { + SCNavigatorUtils.push( + context, + SCMainRoute.follow, + ); + } + }, + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + text( + "${counterMap["SUBSCRIPTION"]?.quantity ?? 0}", + fontSize: 17.sp, + textColor: Colors.white, + fontWeight: FontWeight.bold, + ), + text( + SCAppLocalizations.of(context)!.follow, + fontSize: 14.sp, + textColor: Color(0xffB1B1B1), + ), + ], + ), + ), + Container( + decoration: BoxDecoration( + gradient: LinearGradient( + colors: [ + Color(0xff333333).withOpacity(0.0), + Color(0xff333333), + Color(0xff333333).withOpacity(0.0), + ], + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + ), + ), + height: 25.w, + width: 1.w, + ), + GestureDetector( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + text( + "${counterMap["FANS"]?.quantity ?? 0}", + fontSize: 17.sp, + textColor: Colors.white, + fontWeight: FontWeight.bold, + ), + text( + SCAppLocalizations.of(context)!.fans, + fontSize: 14.sp, + textColor: Color(0xffB1B1B1), + ), + ], + ), + onTap: () { + if (widget.isMe == "true") { + SCNavigatorUtils.push( + context, + SCMainRoute.fans, + ); + } + }, + ), + ], + ), + ); + }, + ), + SizedBox(height: 6.w), + ], + ), + ), + ], + ), + ); + } + + @override + Widget build(BuildContext context) { + _tabs.clear(); + // _tabs.add(_buildTab(0, SCAppLocalizations.of(context)!.dynamicT)); + _tabs.add(_buildTab(0, SCAppLocalizations.of(context)!.aboutMe)); + _tabs.add(_buildTab(1, SCAppLocalizations.of(context)!.giftwall)); + // _tabs.add(_buildTab(3, SCAppLocalizations.of(context)!.relationShip)); + return Consumer( + builder: (context, ref, child) { + final bool isProfileLoading = ref.userProfile == null; + List backgroundPhotos = []; + backgroundPhotos = + (isProfileLoading + ? [] + : (ref.userProfile?.backgroundPhotos ?? [])) + .where((t) => t.status == 1) + .toList(); + return Directionality( + textDirection: + window.locale.languageCode == "ar" + ? TextDirection.rtl + : TextDirection.ltr, + child: SafeArea( + top: false, + child: Stack( + children: [ + Positioned.fill( + child: Column( + children: [ + SizedBox( + height: 300.w, + width: ScreenUtil().screenWidth, + child: + backgroundPhotos.isNotEmpty + ? netImage( + url: backgroundPhotos.first.url ?? "", + width: ScreenUtil().screenWidth, + height: 300.w, + fit: BoxFit.cover, + ) + : Image.asset( + 'sc_images/person/sc_icon_profile_card_default_bg.png', + width: ScreenUtil().screenWidth, + height: 300.w, + fit: BoxFit.cover, + ), + ), + Expanded( + child: Container(color: const Color(0xff083b2f)), + ), + ], + ), + ), + Scaffold( + extendBodyBehindAppBar: true, + resizeToAvoidBottomInset: false, + backgroundColor: Colors.transparent, + appBar: SocialChatStandardAppBar( + backButtonColor: Colors.white, + title: "", + leading: GestureDetector( + behavior: HitTestBehavior.opaque, + onTap: () { + Navigator.pop(context); + }, + child: Container( + width: width(50), + height: height(30), + alignment: AlignmentDirectional.centerStart, + padding: EdgeInsetsDirectional.only(start: width(15)), + child: Icon( + window.locale.languageCode == "ar" + ? Icons.keyboard_arrow_right + : Icons.keyboard_arrow_left, + size: 28.w, + color: Colors.white, + ), + ), + ), + actions: [ + // 在AppBar中显示头像和昵称 + if (_opacity > 0.5 && !isProfileLoading) ...[ + SizedBox(width: 45.w), + head( + url: ref.userProfile?.userAvatar ?? "", + width: 50.w, + height: 50.w, + ), + SizedBox(width: 8.w), + socialchatNickNameText( + maxWidth: 135.w, + textColor: Colors.white, + ref.userProfile?.userNickname ?? "", + fontSize: 16.sp, + fontWeight: FontWeight.w600, + type: ref.userProfile?.getVIP()?.name ?? "", + needScroll: + (ref + .userProfile + ?.userNickname + ?.characters + .length ?? + 0) > + 13, + ), + SizedBox(width: 8.w), + ], + Spacer(), + Builder( + builder: (ct) { + return IconButton( + icon: + widget.isMe == "true" + ? Image.asset( + "sc_images/person/sc_icon_edit_user_info2.png", + width: 22.w, + color: Colors.white, + ) + : Icon( + Icons.more_horiz, + size: 22.w, + color: Colors.white, + ), + onPressed: () { + if (widget.isMe == "true") { + SCNavigatorUtils.push( + context, + SCMainRoute.edit, + replace: false, + ); + return; + } + SmartDialog.showAttach( + tag: "showUserInfoOptMenu", + targetContext: ct, + alignment: Alignment.bottomCenter, + maskColor: Colors.transparent, + animationType: SmartAnimationType.fade, + scalePointBuilder: + (selfSize) => Offset(selfSize.width, 10), + builder: (_) { + return GestureDetector( + child: Transform.translate( + offset: Offset(0, -10), + child: Container( + width: 163.w, + margin: EdgeInsetsDirectional.only( + end: 8.w, + ), + decoration: BoxDecoration( + color: Colors.white30, + borderRadius: BorderRadius.circular( + 4, + ), + ), + padding: EdgeInsets.symmetric( + horizontal: 8.w, + vertical: 5.w, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + GestureDetector( + child: text( + SCAppLocalizations.of( + context, + )!.report, + textColor: Colors.white, + fontSize: 12.sp, + ), + onTap: () { + SmartDialog.dismiss( + tag: "showUserInfoOptMenu", + ); + SCNavigatorUtils.push( + context, + "${SCMainRoute.report}?type=user&tageId=${ref.userProfile?.id}", + replace: false, + ); + }, + ), + ((!(userIdentity?.admin ?? false) && + !(userIdentity + ?.superAdmin ?? + false)) && + ((Provider.of< + SocialChatUserProfileManager + >( + context, + listen: false, + ).userIdentity?.admin ?? + false))) + ? GestureDetector( + child: Container( + margin: EdgeInsets.only( + top: 5.w, + ), + child: text( + SCAppLocalizations.of( + context, + )!.userEditing, + letterSpacing: 0.1, + textColor: Colors.white, + fontSize: 12.sp, + ), + ), + onTap: () { + SmartDialog.dismiss( + tag: + "showUserInfoOptMenu", + ); + SCNavigatorUtils.push( + context, + "${SCMainRoute.editingUserRoomAdmin}?type=User&profile=${Uri.encodeComponent(jsonEncode(ref.userProfile?.toJson()))}", + ); + }, + ) + : Container(), + ((ref.userIdentity?.admin ?? + false) || + (ref + .userIdentity + ?.superFreightAgent ?? + false)) && + !((userIdentity?.admin ?? + false) || + (userIdentity + ?.superFreightAgent ?? + false)) + ? SizedBox(height: 5.w) + : Container(), + ((ref.userIdentity?.admin ?? + false) || + (ref + .userIdentity + ?.superFreightAgent ?? + false)) && + !((userIdentity?.admin ?? + false) || + (userIdentity + ?.superFreightAgent ?? + false)) + ? GestureDetector( + child: text( + letterSpacing: 0.1, + SCAppLocalizations.of( + context, + )!.becomeAgent, + textColor: Colors.white, + fontSize: 12.sp, + ), + onTap: () { + SmartDialog.dismiss( + tag: + "showUserInfoOptMenu", + ); + SCAccountRepository() + .teamCreate( + ref.userProfile + ?.getID() ?? + "", + ); + }, + ) + : Container(), + (ref.userProfile?.isCpRelation ?? + false) + ? GestureDetector( + behavior: + HitTestBehavior.opaque, + child: Container( + padding: EdgeInsets.only( + top: 3.w, + ), + child: text( + letterSpacing: 0.1, + SCAppLocalizations.of( + context, + )!.partWays, + textColor: Colors.white, + fontSize: 12.sp, + ), + ), + onTap: () { + SmartDialog.dismiss( + tag: + "showUserInfoOptMenu", + ); + _showPartWaysDialog(ref); + }, + ) + : Container(), + isBlacklistLoading + ? Container() + : GestureDetector( + behavior: + HitTestBehavior.opaque, + child: Container( + padding: EdgeInsets.only( + top: 3.w, + ), + child: text( + letterSpacing: 0.1, + isBlacklist + ? SCAppLocalizations.of( + context, + )!.removeFromBlacklist + : SCAppLocalizations.of( + context, + )!.moveToBlacklist, + textColor: Colors.white, + fontSize: 12.sp, + ), + ), + onTap: () { + if ((ref + .userProfile + ?.isCpRelation ?? + false)) { + SmartDialog.show( + tag: + "showConfirmDialog", + alignment: + Alignment.center, + debounce: true, + animationType: + SmartAnimationType + .fade, + builder: (_) { + return MsgDialog( + title: + SCAppLocalizations.of( + context, + )!.tips, + msg: + SCAppLocalizations.of( + context, + )!.youAreCurrentlyCPRelationshipPleaseDissolve, + btnText: + SCAppLocalizations.of( + context, + )!.confirm, + onEnsure: () {}, + ); + }, + ); + return; + } + SCAccountHelper.optBlacklist( + widget.tageId, + isBlacklist, + context, + (isOptBlacklist) { + isBlacklist = + isOptBlacklist; + setState(() {}); + }, + ); + }, + ), + ], + ), + ), + ), + onTap: () { + SmartDialog.dismiss( + tag: "showUserInfoOptMenu", + ); + }, + ); + }, + ); + }, + ); + }, + ), + ], + ), + body: + isBlacklist + ? Container() + : (isProfileLoading || isBlacklistLoading + ? const SizedBox.shrink() + : ExtendedNestedScrollView( + controller: _scrollController, + onlyOneScrollInBody: true, + headerSliverBuilder: ( + BuildContext context, + bool innerBoxIsScrolled, + ) { + return [ + SliverToBoxAdapter( + child: _buildProfileHeader( + ref, + backgroundPhotos, + ), + ), + ]; + }, + body: Container( + padding: EdgeInsets.symmetric( + horizontal: 3.w, + ), + decoration: BoxDecoration( + gradient: + ref.userProfile?.userSex == 0 + ? LinearGradient( + colors: [ + Color(0xff083b2f), + Color(0xff083b2f), + ], + begin: + AlignmentDirectional.topStart, + end: + AlignmentDirectional + .bottomEnd, + ) + : LinearGradient( + colors: [ + Color(0xff083b2f), + Color(0xff083b2f), + ], + begin: + AlignmentDirectional.topStart, + end: + AlignmentDirectional + .bottomEnd, + ), + ), + child: Column( + children: [ + Row( + children: [ + SizedBox(width: 6.w), + SizedBox( + height: 35.w, + child: TabBar( + tabAlignment: TabAlignment.start, + indicator: + SCFixedWidthTabIndicator( + width: 20.w, + height: 4.w, + gradient: + ref + .userProfile + ?.userSex == + 0 + ? LinearGradient( + colors: [ + Color( + 0xffFFD800, + ), + Color( + 0xffFFD800, + ), + ], + ) + : LinearGradient( + colors: [ + Color( + 0xffFFD800, + ), + Color( + 0xffFFD800, + ), + ], + ), + ), + labelPadding: + EdgeInsets.symmetric( + horizontal: 12.w, + ), + labelColor: Colors.white, + isScrollable: true, + unselectedLabelColor: Color( + 0xffB1B1B1, + ), + labelStyle: TextStyle( + fontSize: 15.sp, + fontWeight: FontWeight.bold, + ), + unselectedLabelStyle: TextStyle( + fontSize: 13.sp, + ), + indicatorColor: + Colors.transparent, + dividerColor: Colors.transparent, + controller: _tabController, + tabs: _tabs, + ), + ), + SizedBox(width: 6.w), + ], + ), + Expanded( + child: TabBarView( + controller: _tabController, + children: _pages, + ), + ), + SizedBox(height: 25.w), + ], + ), + ), + )), + ), + // 底部按钮区域 + isBlacklistLoading || isBlacklist || isProfileLoading + ? Container() + : Positioned( + bottom: 0, + left: 0, + right: 0, + child: + widget.isMe == "true" + ? Container() + : (isLoading + ? Container() + : Container( + alignment: AlignmentDirectional.center, + width: ScreenUtil().screenWidth, + height: 75.w, + decoration: BoxDecoration( + color: Color(0xff18F2B1).withOpacity(0.1), + borderRadius: BorderRadius.only( + topLeft: Radius.circular(10.w), + topRight: Radius.circular(10.w), + ), + ), + child: Column( + children: [ + SizedBox(height: 10.w), + Row( + mainAxisAlignment: + MainAxisAlignment.center, + children: [ + SCDebounceWidget( + debounceTime: Duration( + milliseconds: 800, + ), + child: SizedBox( + width: 110.w, + child: Column( + mainAxisAlignment: + MainAxisAlignment.center, + children: [ + Image.asset( + "sc_images/person/sc_icon_person_in_room.png", + width: 23.w, + ), + SizedBox(height: 5.w), + Container( + margin: EdgeInsets.only( + bottom: 0.w, + ), + child: text( + SCAppLocalizations.of( + context, + )!.inRoom, + textColor: + SocialChatTheme + .primaryLight, + fontWeight: + FontWeight.w600, + fontSize: 14.sp, + ), + ), + ], + ), + ), + onTap: () { + if ((ref + .userProfile + ?.inRoomId ?? + "") + .isNotEmpty) { + SCRoomUtils.goRoom( + ref.userProfile?.inRoomId ?? + "", + context, + ); + } + }, + ), + SizedBox(width: 10.w), + SCDebounceWidget( + debounceTime: Duration( + milliseconds: 800, + ), + child: Container( + width: 110.w, + child: Column( + mainAxisAlignment: + MainAxisAlignment.center, + children: [ + Image.asset( + "sc_images/person/sc_icon_person_tochat.png", + width: 23.w, + ), + SizedBox(height: 5.w), + Container( + margin: EdgeInsets.only( + bottom: 0.w, + ), + child: text( + SCAppLocalizations.of( + context, + )!.message, + textColor: + SocialChatTheme + .primaryLight, + fontWeight: + FontWeight.w600, + fontSize: 14.sp, + ), + ), + ], + ), + ), + onTap: () async { + var conversation = + V2TimConversation( + type: + ConversationType + .V2TIM_C2C, + userID: widget.tageId, + conversationID: '', + ); + var bool = await Provider.of< + RtmProvider + >( + context, + listen: false, + ).startConversation( + conversation, + ); + if (!bool) return; + var json = jsonEncode( + conversation.toJson(), + ); + SCNavigatorUtils.push( + context, + "${SCChatRouter.chat}?conversation=${Uri.encodeComponent(json)}", + ); + }, + ), + SizedBox(width: 10.w), + SCDebounceWidget( + debounceTime: Duration( + milliseconds: 800, + ), + child: Container( + width: 110.w, + child: Column( + mainAxisAlignment: + MainAxisAlignment.center, + children: [ + Image.asset( + "sc_images/person/sc_icon_person_follow.png", + width: 23.w, + ), + SizedBox(height: 5.w), + Container( + margin: EdgeInsets.only( + bottom: 0.w, + ), + child: text( + isFollow + ? SCAppLocalizations.of( + context, + )!.following + : SCAppLocalizations.of( + context, + )!.follow, + textColor: + SocialChatTheme + .primaryLight, + fontWeight: + FontWeight.w600, + fontSize: 14.sp, + ), + ), + ], + ), + ), + onTap: () { + SCAccountRepository() + .followUser(widget.tageId) + .then((v) { + isFollow = !isFollow; + setState(() {}); + }); + }, + ), + ], + ), + ], + ), + )), + ), + ], + ), + ), + ); + }, + ); + } + + void _showPartWaysDialog(SocialChatUserProfileManager ref) { + SmartDialog.show( + tag: "showPartWaysDialog", + alignment: Alignment.center, + debounce: true, + animationType: SmartAnimationType.fade, + builder: (_) { + return Container( + height: 530.w, + width: ScreenUtil().screenWidth * 0.9, + decoration: BoxDecoration( + image: DecorationImage( + image: AssetImage( + "sc_images/person/sc_icon_send_cp_requst_dialog_bg.png", + ), + fit: BoxFit.fill, + ), + ), + child: Column( + children: [ + SizedBox(height: 58.w), + Container( + child: text( + SCAppLocalizations.of(context)!.partWays, + fontSize: 22.sp, + textColor: Color(0xffDB5872), + fontWeight: FontWeight.bold, + ), + ), + Transform.translate( + offset: Offset(0, -25), + child: Stack( + alignment: AlignmentDirectional.center, + children: [ + PositionedDirectional( + top: 28.w, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + head( + url: + AccountStorage() + .getCurrentUser() + ?.userProfile + ?.userAvatar ?? + "", + width: 90.w, + ), + SizedBox(width: 15.w), + head( + url: ref.userProfile?.userAvatar ?? "", + width: 90.w, + ), + ], + ), + ), + Image.asset( + "sc_images/person/sc_icon_send_cp_requst_dialog_head2.png", + height: 120.w, + fit: BoxFit.fill, + ), + ], + ), + ), + Transform.translate( + offset: Offset(0, -23), + child: Container( + height: 25.w, + width: ScreenUtil().screenWidth * 0.6, + decoration: BoxDecoration( + image: DecorationImage( + image: AssetImage( + "sc_images/person/sc_icon_send_cp_requst_username_bg.png", + ), + fit: BoxFit.fill, + ), + ), + child: Row( + children: [ + socialchatNickNameText( + maxWidth: 100.w, + AccountStorage() + .getCurrentUser() + ?.userProfile + ?.userNickname ?? + "", + fontSize: 10.sp, + fontWeight: FontWeight.w600, + type: + AccountStorage() + .getCurrentUser() + ?.userProfile + ?.getVIP() + ?.name ?? + "", + needScroll: + (AccountStorage() + .getCurrentUser() + ?.userProfile + ?.userNickname + ?.characters + .length ?? + 0) > + 8, + ), + SizedBox(width: 20.w), + socialchatNickNameText( + maxWidth: 100.w, + ref.userProfile?.userNickname ?? "", + fontSize: 10.sp, + fontWeight: FontWeight.w600, + type: ref.userProfile?.getVIP()?.name ?? "", + needScroll: + (ref.userProfile?.userNickname?.characters.length ?? + 0) > + 8, + ), + ], + ), + ), + ), + + Container( + padding: EdgeInsets.only(bottom: 12.w), + margin: EdgeInsets.symmetric(horizontal: 18.w), + decoration: BoxDecoration( + image: DecorationImage( + image: AssetImage( + "sc_images/person/sc_icon_send_cp_requst_dialog_content.png", + ), + fit: BoxFit.fill, + ), + ), + child: Column( + children: [ + Container( + margin: EdgeInsets.symmetric( + horizontal: 35.w, + ).copyWith(top: 45.w), + child: text( + SCAppLocalizations.of( + context, + )!.areYouSureYouWantToPartWaysWithYourCP, + fontWeight: FontWeight.w500, + textColor: Color(0xffFF79A1), + maxLines: 3, + fontSize: 14.sp, + ), + ), + Container( + margin: EdgeInsets.symmetric(horizontal: 20.w), + alignment: AlignmentDirectional.center, + child: text( + SCAppLocalizations.of(context)!.partWaysTips, + fontSize: 10.w, + textColor: Color(0xffFE91B0), + maxLines: 6, + fontWeight: FontWeight.w500, + ), + ), + ], + ), + ), + SizedBox(height: 10.w), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + GestureDetector( + child: Container( + padding: EdgeInsets.only(top: 7.w), + alignment: AlignmentDirectional.center, + width: 130.w, + height: 48.w, + decoration: BoxDecoration( + image: DecorationImage( + image: AssetImage( + "sc_images/person/sc_icon_send_cp_requst_ok_bg.png", + ), + fit: BoxFit.fill, + ), + ), + child: text( + SCAppLocalizations.of(context)!.cancel, + fontSize: 18.sp, + fontWeight: FontWeight.bold, + textColor: Color(0xffDB5872), + ), + ), + onTap: () { + SmartDialog.dismiss(tag: "showPartWaysDialog"); + }, + ), + SizedBox(width: 25.w), + GestureDetector( + child: Container( + padding: EdgeInsets.only(top: 7.w), + alignment: AlignmentDirectional.center, + width: 130.w, + height: 48.w, + decoration: BoxDecoration( + image: DecorationImage( + image: AssetImage( + "sc_images/person/sc_icon_send_cp_requst_cancel_bg.png", + ), + fit: BoxFit.fill, + ), + ), + child: text( + SCAppLocalizations.of(context)!.confirm, + fontSize: 18.sp, + fontWeight: FontWeight.bold, + textColor: Colors.white, + ), + ), + onTap: () { + SCLoadingManager.show(); + SCAccountRepository() + .cpRelationshipDismissApply(ref.userProfile?.id ?? "") + .then((result) { + SmartDialog.dismiss(tag: "showPartWaysDialog"); + SCTts.show( + SCAppLocalizations.of( + context, + )!.operationSuccessful, + ); + SCLoadingManager.hide(); + SCNavigatorUtils.popUntil( + context, + ModalRoute.withName(SCRoutes.home), + ); + }) + .catchError((e) { + SCLoadingManager.hide(); + }); + }, + ), + ], + ), + ], + ), + ); + }, + ); + } +} diff --git a/lib/modules/user/vip/vip_detail_page.dart b/lib/modules/user/vip/vip_detail_page.dart index 5e17a8b..d762d43 100644 --- a/lib/modules/user/vip/vip_detail_page.dart +++ b/lib/modules/user/vip/vip_detail_page.dart @@ -517,17 +517,20 @@ class _VipDetailPageState extends State { mainAxisSize: MainAxisSize.min, children: [ SizedBox(height: 4.w), - Text( - config?.displayNameText ?? 'VIP$level', - style: TextStyle( - color: - selected - ? Colors.white - : Colors.white.withValues( - alpha: enabled ? 0.42 : 0.20, - ), - fontSize: 15.sp, - fontWeight: FontWeight.w500, + Directionality( + textDirection: TextDirection.ltr, + child: Text( + config?.displayNameText ?? 'VIP$level', + style: TextStyle( + color: + selected + ? Colors.white + : Colors.white.withValues( + alpha: enabled ? 0.42 : 0.20, + ), + fontSize: 15.sp, + fontWeight: FontWeight.w500, + ), ), ), SizedBox(height: 6.w), @@ -1147,12 +1150,15 @@ class _VipDetailPageState extends State { ), ), SizedBox(width: 6.w), - Text( - _priceText, - style: TextStyle( - color: const Color(0xFFFFE8A7), - fontSize: 16.sp, - fontWeight: FontWeight.w700, + Directionality( + textDirection: TextDirection.ltr, + child: Text( + _priceText, + style: TextStyle( + color: const Color(0xFFFFE8A7), + fontSize: 16.sp, + fontWeight: FontWeight.w700, + ), ), ), const Spacer(), @@ -1208,31 +1214,34 @@ class _VipDetailPageState extends State { return Transform.scale( scale: scale, alignment: Alignment.centerLeft, - child: Row( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Image.asset( - 'sc_images/vip/sc_vip_letter_v.png', - width: 28.w, - height: 24.7.w, - fit: BoxFit.fill, - ), - Image.asset( - 'sc_images/vip/sc_vip_letter_i.png', - width: 14.w, - height: 24.7.w, - fit: BoxFit.fill, - ), - Image.asset( - 'sc_images/vip/sc_vip_letter_p.png', - width: 28.w, - height: 24.7.w, - fit: BoxFit.fill, - ), - SizedBox(width: 3.w), - _buildVipLevelNumber(level), - ], + child: Directionality( + textDirection: TextDirection.ltr, + child: Row( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Image.asset( + 'sc_images/vip/sc_vip_letter_v.png', + width: 28.w, + height: 24.7.w, + fit: BoxFit.fill, + ), + Image.asset( + 'sc_images/vip/sc_vip_letter_i.png', + width: 14.w, + height: 24.7.w, + fit: BoxFit.fill, + ), + Image.asset( + 'sc_images/vip/sc_vip_letter_p.png', + width: 28.w, + height: 24.7.w, + fit: BoxFit.fill, + ), + SizedBox(width: 3.w), + _buildVipLevelNumber(level), + ], + ), ), ); } @@ -1362,7 +1371,8 @@ class _VipDetailPageState extends State { preview?.payableGold ?? config?.payableGold ?? config?.priceGold; final days = (preview?.durationDays ?? config?.durationDays ?? 30).toInt(); final amountText = amount == null ? '--' : _formatGold(amount); - return '$amountText / $days Days'; + final daysLabel = SCAppLocalizations.of(context)?.days ?? 'Days'; + return '$amountText / $days $daysLabel'; } String _formatGold(num value) { diff --git a/lib/services/audio/rtc_manager.dart b/lib/services/audio/rtc_manager.dart index 51eec2f..670d816 100644 --- a/lib/services/audio/rtc_manager.dart +++ b/lib/services/audio/rtc_manager.dart @@ -99,6 +99,7 @@ class RoomEntryPreviewData { this.roomAccount, this.userId, this.roomCover, + this.roomBackground, this.roomName, this.roomDesc, this.event, @@ -124,6 +125,7 @@ class RoomEntryPreviewData { roomAccount: room.roomAccount, userId: room.userId, roomCover: room.roomCover, + roomBackground: room.roomBackground, roomName: room.roomName, roomDesc: room.roomDesc, event: room.event, @@ -145,6 +147,7 @@ class RoomEntryPreviewData { roomAccount: roomProfile?.roomAccount, userId: roomProfile?.userId, roomCover: roomProfile?.roomCover, + roomBackground: roomProfile?.roomBackground, roomName: roomProfile?.roomName, roomDesc: roomProfile?.roomDesc, event: roomProfile?.event, @@ -168,6 +171,7 @@ class RoomEntryPreviewData { roomAccount: room.roomAccount, userId: room.userId, roomCover: room.roomCover, + roomBackground: room.roomBackground, roomName: room.roomName, roomDesc: room.roomDesc, event: room.event, @@ -192,6 +196,7 @@ class RoomEntryPreviewData { final String? roomAccount; final String? userId; final String? roomCover; + final String? roomBackground; final String? roomName; final String? roomDesc; final String? event; @@ -241,6 +246,7 @@ class RoomEntryPreviewData { nationalFlag: nationalFlag, roomAccount: roomAccount, roomCover: roomCover, + roomBackground: roomBackground, roomDesc: roomDesc, roomName: roomName, sysOrigin: sysOrigin, @@ -1964,6 +1970,7 @@ class RealTimeCommunicationManager extends ChangeNotifier { void updateCurrentRoomBasicInfo({ String? roomCover, + String? roomBackground, String? roomName, String? roomDesc, }) { @@ -1975,6 +1982,10 @@ class RealTimeCommunicationManager extends ChangeNotifier { roomProfile: currenRoom?.roomProfile?.copyWith( roomProfile: currentRoomProfile.copyWith( roomCover: _preferNonEmpty(roomCover, currentRoomProfile.roomCover), + roomBackground: _preferNonEmpty( + roomBackground, + currentRoomProfile.roomBackground, + ), roomName: _preferNonEmpty(roomName, currentRoomProfile.roomName), roomDesc: _preferNonEmpty(roomDesc, currentRoomProfile.roomDesc), ), @@ -1983,6 +1994,7 @@ class RealTimeCommunicationManager extends ChangeNotifier { SCRoomProfileCache.saveRoomProfile( roomId: currentRoomProfile.id ?? "", roomCover: roomCover, + roomBackground: roomBackground, roomName: roomName, roomDesc: roomDesc, ); @@ -3601,7 +3613,7 @@ class RealTimeCommunicationManager extends ChangeNotifier { final value = await SCChatRoomRepository().specific(roomId); final currentRoomProfile = currenRoom?.roomProfile?.roomProfile; debugPrint( - "[Room Cover Sync] loadRoomInfo fetched roomId=${value.id ?? roomId} roomCover=${value.roomCover ?? ""} roomName=${value.roomName ?? ""}", + "[Room Cover Sync] loadRoomInfo fetched roomId=${value.id ?? roomId} roomCover=${value.roomCover ?? ""} roomBackground=${value.roomBackground ?? ""} roomName=${value.roomName ?? ""}", ); currenRoom = currenRoom?.copyWith( roomProfile: currenRoom?.roomProfile?.copyWith( @@ -3612,6 +3624,10 @@ class RealTimeCommunicationManager extends ChangeNotifier { value.roomCover, currentRoomProfile.roomCover, ), + roomBackground: _preferNonEmpty( + value.roomBackground, + currentRoomProfile.roomBackground, + ), roomName: _preferNonEmpty( value.roomName, currentRoomProfile.roomName, @@ -3624,7 +3640,7 @@ class RealTimeCommunicationManager extends ChangeNotifier { ), ); debugPrint( - "[Room Cover Sync] loadRoomInfo applied roomId=$roomId roomCover=${currenRoom?.roomProfile?.roomProfile?.roomCover ?? ""}", + "[Room Cover Sync] loadRoomInfo applied roomId=$roomId roomCover=${currenRoom?.roomProfile?.roomProfile?.roomCover ?? ""} roomBackground=${currenRoom?.roomProfile?.roomProfile?.roomBackground ?? ""}", ); ///如果是游客禁止上麦,已经在麦上的游客需要下麦 diff --git a/lib/services/gift/gift_animation_manager.dart b/lib/services/gift/gift_animation_manager.dart index 576ea2a..37a2f63 100644 --- a/lib/services/gift/gift_animation_manager.dart +++ b/lib/services/gift/gift_animation_manager.dart @@ -6,6 +6,7 @@ import 'package:flutter/cupertino.dart'; import 'package:yumi/ui_kit/widgets/room/anim/l_gift_animal_view.dart'; class GiftAnimationManager extends ChangeNotifier { + static const int maxVisibleSlots = 4; static const int _maxPendingAnimations = 24; static const Duration _activeComboIdleDismissDelay = Duration( milliseconds: 3200, @@ -17,10 +18,16 @@ class GiftAnimationManager extends ChangeNotifier { //每个控件正在播放的动画 Map giftMap = {0: null, 1: null, 2: null, 3: null}; + int _visibleSlotCount = 0; + bool get _controllersReady => - animationControllerList.length >= giftMap.length; + _visibleSlotCount <= 0 || + animationControllerList.length >= _visibleSlotCount; GiftAnimationSlotSnapshot? slotSnapshotAt(int index) { + if (index < 0 || index >= _visibleSlotCount) { + return null; + } final gift = giftMap[index]; if (gift == null || animationControllerList.length <= index) { return null; @@ -43,6 +50,31 @@ class GiftAnimationManager extends ChangeNotifier { ); } + void setVisibleSlotCount(int count) { + final nextCount = count.clamp(0, maxVisibleSlots).toInt(); + if (nextCount == _visibleSlotCount) { + return; + } + + final oldCount = _visibleSlotCount; + if (nextCount < oldCount) { + for (var index = oldCount - 1; index >= nextCount; index -= 1) { + final gift = giftMap[index]; + if (gift == null) { + continue; + } + _cancelSlotDismissTimer(index); + giftMap[index] = null; + pendingAnimationsQueue.addFirst(gift); + } + _trimPendingAnimationsFromEnd(); + } + + _visibleSlotCount = nextCount; + notifyListeners(); + proceedToNextAnimation(); + } + void enqueueGiftAnimation(LGiftModel giftModel) { if (_mergeIntoActiveAnimation(giftModel)) { return; @@ -55,21 +87,42 @@ class GiftAnimationManager extends ChangeNotifier { proceedToNextAnimation(); } + void clearActiveAnimations() { + pendingAnimationsQueue.clear(); + var changed = false; + for (final index in giftMap.keys.toList()) { + _cancelSlotDismissTimer(index); + if (giftMap[index] != null) { + giftMap[index] = null; + changed = true; + } + } + if (changed) { + notifyListeners(); + } + } + void _trimPendingAnimations() { while (pendingAnimationsQueue.length >= _maxPendingAnimations) { pendingAnimationsQueue.removeFirst(); } } + void _trimPendingAnimationsFromEnd() { + while (pendingAnimationsQueue.length > _maxPendingAnimations) { + pendingAnimationsQueue.removeLast(); + } + } + bool _mergeIntoActiveAnimation(LGiftModel incoming) { - for (final entry in giftMap.entries) { - final current = entry.value; + for (var index = 0; index < _visibleSlotCount; index += 1) { + final current = giftMap[index]; if (current == null || current.labelId != incoming.labelId) { continue; } _mergeGiftModel(target: current, incoming: incoming); notifyListeners(); - _refreshSlotAnimation(entry.key, restartEntry: false); + _refreshSlotAnimation(index, restartEntry: false); return true; } return false; @@ -119,41 +172,60 @@ class GiftAnimationManager extends ChangeNotifier { ///开始播放 proceedToNextAnimation() { - if (pendingAnimationsQueue.isEmpty || !_controllersReady) { + if (pendingAnimationsQueue.isEmpty || + !_controllersReady || + _visibleSlotCount <= 0) { return; } - var playGift = pendingAnimationsQueue.first; - for (var key in giftMap.keys) { - var value = giftMap[key]; - if (value == null) { - giftMap[key] = playGift; - pendingAnimationsQueue.removeFirst(); - notifyListeners(); - _refreshSlotAnimation(key, restartEntry: true); - break; - } else { + + var changed = false; + while (pendingAnimationsQueue.isNotEmpty) { + final playGift = pendingAnimationsQueue.first; + var consumed = false; + for (var index = 0; index < _visibleSlotCount; index += 1) { + final value = giftMap[index]; + if (value == null) { + giftMap[index] = playGift; + pendingAnimationsQueue.removeFirst(); + changed = true; + consumed = true; + _refreshSlotAnimation(index, restartEntry: true); + break; + } if (value.labelId == playGift.labelId) { _mergeGiftModel(target: value, incoming: playGift); pendingAnimationsQueue.removeFirst(); - notifyListeners(); - _refreshSlotAnimation(key, restartEntry: false); + changed = true; + consumed = true; + _refreshSlotAnimation(index, restartEntry: false); break; } } + if (!consumed) { + break; + } + } + + if (changed) { + notifyListeners(); } } void attachAnimationControllers(List anins) { animationControllerList = anins; + if (_visibleSlotCount > animationControllerList.length) { + setVisibleSlotCount(animationControllerList.length); + return; + } proceedToNextAnimation(); } void cleanupAnimationResources() { pendingAnimationsQueue.clear(); - giftMap[0] = null; - giftMap[1] = null; - giftMap[2] = null; - giftMap[3] = null; + for (final key in giftMap.keys) { + giftMap[key] = null; + } + _visibleSlotCount = 0; for (var element in animationControllerList) { element.dismissTimer?.cancel(); element.controller.dispose(); diff --git a/lib/services/room/rc_room_manager.dart b/lib/services/room/rc_room_manager.dart index 07ba832..234d76c 100644 --- a/lib/services/room/rc_room_manager.dart +++ b/lib/services/room/rc_room_manager.dart @@ -43,6 +43,9 @@ class SocialChatRoomManager extends ChangeNotifier { if ((roomInfo.roomCover ?? "").trim().isNotEmpty) { myRoom?.setRoomCover = roomInfo.roomCover!; } + if ((roomInfo.roomBackground ?? "").trim().isNotEmpty) { + myRoom?.setRoomBackground = roomInfo.roomBackground!; + } if (roomInfo.roomName != null) { myRoom?.setRoomName = roomInfo.roomName!; } diff --git a/lib/shared/business_logic/models/res/follow_room_res.dart b/lib/shared/business_logic/models/res/follow_room_res.dart index a5bd4b8..6a580ba 100644 --- a/lib/shared/business_logic/models/res/follow_room_res.dart +++ b/lib/shared/business_logic/models/res/follow_room_res.dart @@ -65,6 +65,7 @@ class RoomProfile { String? roomAccount, List? roomBadgeIcons, String? roomCover, + String? roomBackground, String? roomDesc, String? roomGameIcon, String? roomName, @@ -85,6 +86,7 @@ class RoomProfile { _roomAccount = roomAccount; _roomBadgeIcons = roomBadgeIcons; _roomCover = roomCover; + _roomBackground = roomBackground; _roomDesc = roomDesc; _roomGameIcon = roomGameIcon; _roomName = roomName; @@ -114,6 +116,7 @@ class RoomProfile { ? json['roomBadgeIcons'].cast() : []; _roomCover = json['roomCover'] ?? json['cover']; + _roomBackground = json['roomBackground']; _roomDesc = json['roomDesc']; _roomGameIcon = json['roomGameIcon']; _roomName = json['roomName']; @@ -146,6 +149,7 @@ class RoomProfile { String? _roomAccount; List? _roomBadgeIcons; String? _roomCover; + String? _roomBackground; String? _roomDesc; String? _roomGameIcon; String? _roomName; @@ -166,6 +170,7 @@ class RoomProfile { String? roomAccount, List? roomBadgeIcons, String? roomCover, + String? roomBackground, String? roomDesc, String? roomGameIcon, String? roomName, @@ -186,6 +191,7 @@ class RoomProfile { roomAccount: roomAccount ?? _roomAccount, roomBadgeIcons: roomBadgeIcons ?? _roomBadgeIcons, roomCover: roomCover ?? _roomCover, + roomBackground: roomBackground ?? _roomBackground, roomDesc: roomDesc ?? _roomDesc, roomGameIcon: roomGameIcon ?? _roomGameIcon, roomName: roomName ?? _roomName, @@ -206,6 +212,7 @@ class RoomProfile { String? get roomAccount => _roomAccount; List? get roomBadgeIcons => _roomBadgeIcons; String? get roomCover => _roomCover; + String? get roomBackground => _roomBackground; String? get roomDesc => _roomDesc; String? get roomGameIcon => _roomGameIcon; String? get roomName => _roomName; @@ -243,6 +250,7 @@ class RoomProfile { map['roomAccount'] = _roomAccount; map['roomBadgeIcons'] = _roomBadgeIcons; map['roomCover'] = _roomCover; + map['roomBackground'] = _roomBackground; map['roomDesc'] = _roomDesc; map['roomGameIcon'] = _roomGameIcon; map['roomName'] = _roomName; diff --git a/lib/shared/business_logic/models/res/join_room_res.dart b/lib/shared/business_logic/models/res/join_room_res.dart index 575353a..3ac0c4c 100644 --- a/lib/shared/business_logic/models/res/join_room_res.dart +++ b/lib/shared/business_logic/models/res/join_room_res.dart @@ -716,6 +716,7 @@ class RoomProfile2 { String? nationalFlag, String? roomAccount, String? roomCover, + String? roomBackground, String? roomDesc, String? roomName, String? sysOrigin, @@ -733,6 +734,7 @@ class RoomProfile2 { _nationalFlag = nationalFlag; _roomAccount = roomAccount; _roomCover = roomCover; + _roomBackground = roomBackground; _roomDesc = roomDesc; _roomName = roomName; _sysOrigin = sysOrigin; @@ -752,6 +754,7 @@ class RoomProfile2 { _nationalFlag = json['nationalFlag']; _roomAccount = json['roomAccount']; _roomCover = json['roomCover'] ?? json['cover']; + _roomBackground = json['roomBackground']; _roomDesc = json['roomDesc']; _roomName = json['roomName']; _sysOrigin = json['sysOrigin']; @@ -770,6 +773,7 @@ class RoomProfile2 { String? _nationalFlag; String? _roomAccount; String? _roomCover; + String? _roomBackground; String? _roomDesc; String? _roomName; String? _sysOrigin; @@ -788,6 +792,7 @@ class RoomProfile2 { String? nationalFlag, String? roomAccount, String? roomCover, + String? roomBackground, String? roomDesc, String? roomName, String? sysOrigin, @@ -805,6 +810,7 @@ class RoomProfile2 { nationalFlag: nationalFlag ?? _nationalFlag, roomAccount: roomAccount ?? _roomAccount, roomCover: roomCover ?? _roomCover, + roomBackground: roomBackground ?? _roomBackground, roomDesc: roomDesc ?? _roomDesc, roomName: roomName ?? _roomName, sysOrigin: sysOrigin ?? _sysOrigin, @@ -833,6 +839,7 @@ class RoomProfile2 { String? get roomAccount => _roomAccount; String? get roomCover => _roomCover; + String? get roomBackground => _roomBackground; String? get roomDesc => _roomDesc; @@ -857,6 +864,7 @@ class RoomProfile2 { map['nationalFlag'] = _nationalFlag; map['roomAccount'] = _roomAccount; map['roomCover'] = _roomCover; + map['roomBackground'] = _roomBackground; map['roomDesc'] = _roomDesc; map['roomName'] = _roomName; map['sysOrigin'] = _sysOrigin; diff --git a/lib/shared/business_logic/models/res/my_room_res.dart b/lib/shared/business_logic/models/res/my_room_res.dart index ef782e3..288603a 100644 --- a/lib/shared/business_logic/models/res/my_room_res.dart +++ b/lib/shared/business_logic/models/res/my_room_res.dart @@ -29,6 +29,7 @@ class MyRoomRes { String? roomAccount, String? userId, String? roomCover, + String? roomBackground, String? roomName, String? roomDesc, String? event, @@ -49,6 +50,7 @@ class MyRoomRes { _roomAccount = roomAccount; _userId = userId; _roomCover = roomCover; + _roomBackground = roomBackground; _roomName = roomName; _roomDesc = roomDesc; _event = event; @@ -71,6 +73,7 @@ class MyRoomRes { _roomAccount = json['roomAccount']; _userId = json['userId']; _roomCover = json['roomCover'] ?? json['cover']; + _roomBackground = json['roomBackground']; _roomName = json['roomName']; _roomDesc = json['roomDesc']; _event = json['event']; @@ -98,6 +101,7 @@ class MyRoomRes { String? _roomAccount; String? _userId; String? _roomCover; + String? _roomBackground; String? _roomName; String? _roomDesc; String? _event; @@ -118,6 +122,7 @@ class MyRoomRes { String? roomAccount, String? userId, String? roomCover, + String? roomBackground, String? roomName, String? roomDesc, String? event, @@ -138,6 +143,7 @@ class MyRoomRes { roomAccount: roomAccount ?? _roomAccount, userId: userId ?? _userId, roomCover: roomCover ?? _roomCover, + roomBackground: roomBackground ?? _roomBackground, roomName: roomName ?? _roomName, roomDesc: roomDesc ?? _roomDesc, event: event ?? _event, @@ -158,6 +164,7 @@ class MyRoomRes { String? get roomAccount => _roomAccount; String? get userId => _userId; String? get roomCover => _roomCover; + String? get roomBackground => _roomBackground; String? get roomName => _roomName; String? get roomDesc => _roomDesc; String? get event => _event; @@ -174,6 +181,7 @@ class MyRoomRes { RoomCounter? get counter => _counter; List? get useBadges => _useBadges; set setRoomCover(String value) => _roomCover = value; + set setRoomBackground(String value) => _roomBackground = value; set setRoomName(String value) => _roomName = value; set setRoomDesc(String value) => _roomDesc = value; @@ -183,6 +191,7 @@ class MyRoomRes { map['roomAccount'] = _roomAccount; map['userId'] = _userId; map['roomCover'] = _roomCover; + map['roomBackground'] = _roomBackground; map['roomName'] = _roomName; map['roomDesc'] = _roomDesc; map['event'] = _event; diff --git a/lib/shared/business_logic/models/res/room_res.dart b/lib/shared/business_logic/models/res/room_res.dart index fd5715c..8bb7918 100644 --- a/lib/shared/business_logic/models/res/room_res.dart +++ b/lib/shared/business_logic/models/res/room_res.dart @@ -30,6 +30,7 @@ class SocialChatRoomRes { String? roomAccount, List? roomBadgeIcons, String? roomCover, + String? roomBackground, String? roomDesc, String? roomGameIcon, String? roomName, @@ -50,6 +51,7 @@ class SocialChatRoomRes { _roomAccount = roomAccount; _roomBadgeIcons = roomBadgeIcons; _roomCover = roomCover; + _roomBackground = roomBackground; _roomDesc = roomDesc; _roomGameIcon = roomGameIcon; _roomName = roomName; @@ -81,6 +83,7 @@ class SocialChatRoomRes { .toList() : null; _roomCover = json['roomCover'] ?? json['cover']; + _roomBackground = json['roomBackground']; _roomDesc = json['roomDesc']; _roomGameIcon = json['roomGameIcon']; _roomName = json['roomName']; @@ -114,6 +117,7 @@ class SocialChatRoomRes { String? _roomAccount; List? _roomBadgeIcons; String? _roomCover; + String? _roomBackground; String? _roomDesc; String? _roomGameIcon; String? _roomName; @@ -135,6 +139,7 @@ class SocialChatRoomRes { String? roomAccount, List? roomBadgeIcons, String? roomCover, + String? roomBackground, String? roomDesc, String? roomGameIcon, String? roomName, @@ -155,6 +160,7 @@ class SocialChatRoomRes { roomAccount: roomAccount ?? _roomAccount, roomBadgeIcons: roomBadgeIcons ?? _roomBadgeIcons, roomCover: roomCover ?? _roomCover, + roomBackground: roomBackground ?? _roomBackground, roomDesc: roomDesc ?? _roomDesc, roomGameIcon: roomGameIcon ?? _roomGameIcon, roomName: roomName ?? _roomName, @@ -185,6 +191,7 @@ class SocialChatRoomRes { List? get roomBadgeIcons => _roomBadgeIcons; String? get roomCover => _roomCover; + String? get roomBackground => _roomBackground; String? get roomDesc => _roomDesc; @@ -228,6 +235,7 @@ class SocialChatRoomRes { map['roomAccount'] = _roomAccount; map['roomBadgeIcons'] = _roomBadgeIcons; map['roomCover'] = _roomCover; + map['roomBackground'] = _roomBackground; map['roomDesc'] = _roomDesc; map['roomGameIcon'] = _roomGameIcon; map['roomName'] = _roomName; diff --git a/lib/shared/business_logic/models/res/sc_edit_room_info_res.dart b/lib/shared/business_logic/models/res/sc_edit_room_info_res.dart index b1ea990..069cc7a 100644 --- a/lib/shared/business_logic/models/res/sc_edit_room_info_res.dart +++ b/lib/shared/business_logic/models/res/sc_edit_room_info_res.dart @@ -21,6 +21,7 @@ class SCEditRoomInfoRes { String? roomAccount, String? userId, String? roomCover, + String? roomBackground, String? roomName, String? roomDesc, String? event, @@ -38,6 +39,7 @@ class SCEditRoomInfoRes { _roomAccount = roomAccount; _userId = userId; _roomCover = roomCover; + _roomBackground = roomBackground; _roomName = roomName; _roomDesc = roomDesc; _event = event; @@ -57,6 +59,7 @@ class SCEditRoomInfoRes { _roomAccount = json['roomAccount']; _userId = json['userId']; _roomCover = json['roomCover'] ?? json['cover']; + _roomBackground = json['roomBackground']; _roomName = json['roomName']; _roomDesc = json['roomDesc']; _event = json['event']; @@ -74,6 +77,7 @@ class SCEditRoomInfoRes { String? _roomAccount; String? _userId; String? _roomCover; + String? _roomBackground; String? _roomName; String? _roomDesc; String? _event; @@ -91,6 +95,7 @@ class SCEditRoomInfoRes { String? roomAccount, String? userId, String? roomCover, + String? roomBackground, String? roomName, String? roomDesc, String? event, @@ -108,6 +113,7 @@ class SCEditRoomInfoRes { roomAccount: roomAccount ?? _roomAccount, userId: userId ?? _userId, roomCover: roomCover ?? _roomCover, + roomBackground: roomBackground ?? _roomBackground, roomName: roomName ?? _roomName, roomDesc: roomDesc ?? _roomDesc, event: event ?? _event, @@ -125,6 +131,7 @@ class SCEditRoomInfoRes { String? get roomAccount => _roomAccount; String? get userId => _userId; String? get roomCover => _roomCover; + String? get roomBackground => _roomBackground; String? get roomName => _roomName; String? get roomDesc => _roomDesc; String? get event => _event; @@ -144,6 +151,7 @@ class SCEditRoomInfoRes { map['roomAccount'] = _roomAccount; map['userId'] = _userId; map['roomCover'] = _roomCover; + map['roomBackground'] = _roomBackground; map['roomName'] = _roomName; map['roomDesc'] = _roomDesc; map['event'] = _event; diff --git a/lib/shared/business_logic/repositories/user_repository.dart b/lib/shared/business_logic/repositories/user_repository.dart index 1e43a97..03d4fbb 100644 --- a/lib/shared/business_logic/repositories/user_repository.dart +++ b/lib/shared/business_logic/repositories/user_repository.dart @@ -90,6 +90,12 @@ abstract class SocialChatUserRepository { String event, ); + ///修改房间背景图 + Future updateRoomBackground( + String roomId, + String roomBackground, + ); + ///获取指定用户信息 Future loadUserInfo(String userId); diff --git a/lib/shared/data_sources/sources/repositories/sc_room_repository_imp.dart b/lib/shared/data_sources/sources/repositories/sc_room_repository_imp.dart index 55b1dab..931ddd6 100644 --- a/lib/shared/data_sources/sources/repositories/sc_room_repository_imp.dart +++ b/lib/shared/data_sources/sources/repositories/sc_room_repository_imp.dart @@ -76,6 +76,10 @@ class SCChatRoomRepository implements SocialChatRoomRepository { specificRoom.roomCover, cachedRoom.roomCover, ), + roomBackground: SCRoomProfileCache.preferNonEmpty( + specificRoom.roomBackground, + cachedRoom.roomBackground, + ), roomName: SCRoomProfileCache.preferNonEmpty( specificRoom.roomName, cachedRoom.roomName, @@ -137,11 +141,12 @@ class SCChatRoomRepository implements SocialChatRoomRepository { fromJson: (json) => MyRoomRes.fromJson(json), ); debugPrint( - "[Room Cover][specific] roomId=${result.id ?? roomId} roomCover=${result.roomCover ?? ""} roomName=${result.roomName ?? ""}", + "[Room Cover][specific] roomId=${result.id ?? roomId} roomCover=${result.roomCover ?? ""} roomBackground=${result.roomBackground ?? ""} roomName=${result.roomName ?? ""}", ); await SCRoomProfileCache.saveRoomProfile( roomId: result.id ?? roomId, roomCover: result.roomCover, + roomBackground: result.roomBackground, roomName: result.roomName, roomDesc: result.roomDesc, ); diff --git a/lib/shared/data_sources/sources/repositories/sc_user_repository_impl.dart b/lib/shared/data_sources/sources/repositories/sc_user_repository_impl.dart index 7a69592..a6f578e 100644 --- a/lib/shared/data_sources/sources/repositories/sc_user_repository_impl.dart +++ b/lib/shared/data_sources/sources/repositories/sc_user_repository_impl.dart @@ -33,6 +33,7 @@ import 'package:yumi/shared/business_logic/models/res/sc_sign_in_res.dart'; import 'package:yumi/shared/business_logic/models/res/sc_violation_handle_res.dart'; import 'package:yumi/shared/business_logic/repositories/user_repository.dart'; import 'package:yumi/shared/data_sources/sources/remote/net/network_client.dart'; +import 'package:yumi/shared/tools/sc_mobile_login_context.dart'; import 'package:yumi/shared/tools/sc_room_profile_cache.dart'; class SCAccountRepository implements SocialChatUserRepository { @@ -81,9 +82,11 @@ class SCAccountRepository implements SocialChatUserRepository { ///auth/account/login @override Future loginForAccount(String account, String pwd) async { + final data = {"account": account, "pwd": pwd}; + data.addAll(await SCMobileLoginContext.loginBodyFields()); final result = await http.post( "e64f27b9ba6b37881120f4584a5444a5c684d8491b703d0af953e5cc6a5f4cec", - data: {"account": account, "pwd": pwd}, + data: data, fromJson: (json) => SocialChatLoginRes.fromJson(json), ); return result; @@ -95,9 +98,11 @@ class SCAccountRepository implements SocialChatUserRepository { String authType, String openId, ) async { + final data = {"authType": authType, "openId": openId}; + data.addAll(await SCMobileLoginContext.loginBodyFields()); final result = await http.post( "e64f27b9ba6b37881120f4584a5444a5531a33eb137749a5decad584f58aaa44", - data: {"authType": authType, "openId": openId}, + data: data, fromJson: (json) => SocialChatLoginRes.fromJson(json), ); return result; @@ -275,6 +280,34 @@ class SCAccountRepository implements SocialChatUserRepository { return result; } + ///room/profile/background + @override + Future updateRoomBackground( + String roomId, + String roomBackground, + ) async { + final payload = {"roomId": roomId, "roomBackground": roomBackground}; + debugPrint("[Room Background Save] request payload=$payload"); + final result = await http.put( + "/room/profile/background", + data: payload, + fromJson: (json) => SCEditRoomInfoRes.fromJson(json), + ); + final resolvedBackground = + (result.roomBackground ?? "").trim().isNotEmpty + ? result.roomBackground + : roomBackground; + debugPrint("[Room Background Save] response=${result.toJson()}"); + await SCRoomProfileCache.saveRoomProfile( + roomId: result.id ?? roomId, + roomCover: result.roomCover, + roomBackground: resolvedBackground, + roomName: result.roomName, + roomDesc: result.roomDesc, + ); + return result.copyWith(roomBackground: resolvedBackground); + } + ///user/user-profile @override Future loadUserInfo(String userId) async { diff --git a/lib/shared/tools/sc_gift_vap_svga_manager.dart b/lib/shared/tools/sc_gift_vap_svga_manager.dart index 1be8426..ed705dd 100644 --- a/lib/shared/tools/sc_gift_vap_svga_manager.dart +++ b/lib/shared/tools/sc_gift_vap_svga_manager.dart @@ -61,7 +61,12 @@ class SCGiftVapSvgaManager { void setMute(bool muteMusic) { _mute = muteMusic; - DataPersistence.setPlayGiftMusic(_mute); + _rsc?.muted = _mute; + final vapController = _rgc; + if (vapController != null) { + unawaited(vapController.setMute(_mute)); + } + unawaited(DataPersistence.setPlayGiftMusic(_mute)); } bool getMute() { @@ -415,6 +420,7 @@ class SCGiftVapSvgaManager { _mute = DataPersistence.getPlayGiftMusic(); _dis = false; _rgc = vapController; + unawaited(vapController.setMute(_mute)); _log( 'bindVapCtrl hasVapCtrl=${_rgc != null} ' 'hasSvgaCtrl=${_rsc != null} queue=$_queueSummary mute=$_mute', @@ -439,8 +445,10 @@ class SCGiftVapSvgaManager { } void bindSvgaCtrl(SVGAAnimationController svgaController) { + _mute = DataPersistence.getPlayGiftMusic(); _dis = false; _rsc = svgaController; + _rsc?.muted = _mute; _log( 'bindSvgaCtrl hasSvgaCtrl=${_rsc != null} ' 'hasVapCtrl=${_rgc != null} queue=$_queueSummary', @@ -565,6 +573,7 @@ class SCGiftVapSvgaManager { return; } _log('use prepared asset svga path=${task.path}'); + _rsc?.muted = _mute; _rsc?.videoItem = entity; _rsc?.reset(); _rsc?.forward(); @@ -591,6 +600,7 @@ class SCGiftVapSvgaManager { return; } _log('play local svga file path=${task.path}'); + _rsc?.muted = _mute; _rsc?.videoItem = entity; _rsc?.reset(); _rsc?.forward(); @@ -634,6 +644,7 @@ class SCGiftVapSvgaManager { return; } _log('use prepared network svga path=${task.path}'); + _rsc?.muted = _mute; _rsc?.videoItem = entity; _rsc?.reset(); _rsc?.forward(); diff --git a/lib/shared/tools/sc_lucky_gift_win_sound_player.dart b/lib/shared/tools/sc_lucky_gift_win_sound_player.dart new file mode 100644 index 0000000..66fc24c --- /dev/null +++ b/lib/shared/tools/sc_lucky_gift_win_sound_player.dart @@ -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 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(); + } +} diff --git a/lib/shared/tools/sc_mobile_login_context.dart b/lib/shared/tools/sc_mobile_login_context.dart new file mode 100644 index 0000000..dc73ae9 --- /dev/null +++ b/lib/shared/tools/sc_mobile_login_context.dart @@ -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> loginBodyFields() async { + return { + '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 mobileZone() async { + try { + final zone = await _channel.invokeMethod('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'; + } +} diff --git a/lib/shared/tools/sc_room_profile_cache.dart b/lib/shared/tools/sc_room_profile_cache.dart index f4f6fed..64fc812 100644 --- a/lib/shared/tools/sc_room_profile_cache.dart +++ b/lib/shared/tools/sc_room_profile_cache.dart @@ -57,6 +57,7 @@ class SCRoomProfileCache { static Future saveRoomProfile({ required String roomId, String? roomCover, + String? roomBackground, String? roomName, String? roomDesc, }) async { @@ -66,6 +67,8 @@ class SCRoomProfileCache { final current = getRoomProfile(roomId); final payload = { "roomCover": preferNonEmpty(roomCover, current["roomCover"]) ?? "", + "roomBackground": + preferNonEmpty(roomBackground, current["roomBackground"]) ?? "", "roomName": preferNonEmpty(roomName, current["roomName"]) ?? "", "roomDesc": preferNonEmpty(roomDesc, current["roomDesc"]) ?? "", }; @@ -79,6 +82,10 @@ class SCRoomProfileCache { } return room.copyWith( roomCover: preferCachedValue(cache["roomCover"], room.roomCover), + roomBackground: preferCachedValue( + cache["roomBackground"], + room.roomBackground, + ), roomName: preferCachedValue(cache["roomName"], room.roomName), roomDesc: preferCachedValue(cache["roomDesc"], room.roomDesc), ); @@ -96,6 +103,10 @@ class SCRoomProfileCache { return room.copyWith( roomProfile: roomProfile.copyWith( roomCover: preferCachedValue(cache["roomCover"], roomProfile.roomCover), + roomBackground: preferCachedValue( + cache["roomBackground"], + roomProfile.roomBackground, + ), roomName: preferCachedValue(cache["roomName"], roomProfile.roomName), roomDesc: preferCachedValue(cache["roomDesc"], roomProfile.roomDesc), ), @@ -109,6 +120,10 @@ class SCRoomProfileCache { } return room.copyWith( roomCover: preferCachedValue(cache["roomCover"], room.roomCover), + roomBackground: preferCachedValue( + cache["roomBackground"], + room.roomBackground, + ), roomName: preferCachedValue(cache["roomName"], room.roomName), roomDesc: preferCachedValue(cache["roomDesc"], room.roomDesc), ); @@ -131,6 +146,10 @@ class SCRoomProfileCache { cache["roomCover"], basicRoomProfile.roomCover, ), + roomBackground: preferCachedValue( + cache["roomBackground"], + basicRoomProfile.roomBackground, + ), roomName: preferCachedValue( cache["roomName"], basicRoomProfile.roomName, diff --git a/lib/ui_kit/widgets/room/anim/l_gift_animal_view.dart b/lib/ui_kit/widgets/room/anim/l_gift_animal_view.dart index 51a0dcd..c5b68c2 100644 --- a/lib/ui_kit/widgets/room/anim/l_gift_animal_view.dart +++ b/lib/ui_kit/widgets/room/anim/l_gift_animal_view.dart @@ -21,16 +21,34 @@ class LGiftAnimalPage extends StatefulWidget { class _GiftAnimalPageState extends State with TickerProviderStateMixin { + static const double _tickerHeight = 52; + static const double _slotExtent = 70; + static const double _entryOffset = 60; + late final GiftAnimationManager _giftAnimationManager; @override Widget build(BuildContext context) { - return SizedBox( - height: 332.w, - child: Stack( - clipBehavior: Clip.none, - children: List.generate(4, (index) => _GiftTickerSlot(index: index)), - ), + return LayoutBuilder( + builder: (context, constraints) { + final availableHeight = + constraints.hasBoundedHeight ? constraints.maxHeight : 332.w; + final visibleSlotCount = _resolveVisibleSlotCount(availableHeight); + _syncVisibleSlotCount(visibleSlotCount); + if (visibleSlotCount <= 0) { + return const SizedBox.shrink(); + } + return SizedBox( + height: _requiredHeightForSlots(visibleSlotCount), + child: Stack( + clipBehavior: Clip.none, + children: List.generate( + visibleSlotCount, + (index) => _GiftTickerSlot(index: index), + ), + ), + ); + }, ); } @@ -52,8 +70,8 @@ class _GiftAnimalPageState extends State void initAnimal() { List beans = []; - double top = 60; - for (int i = 0; i < 4; i++) { + for (int i = 0; i < GiftAnimationManager.maxVisibleSlots; i++) { + final targetTop = i * _slotExtent; var bean = LGiftScrollingScreenAnimsBean(); var controller = AnimationController( value: 0, @@ -66,10 +84,10 @@ class _GiftAnimalPageState extends State duration: const Duration(milliseconds: 280), vsync: this, ); - bean.luckyGiftPinnedMargin = EdgeInsets.only(top: top); + bean.luckyGiftPinnedMargin = EdgeInsets.only(top: targetTop.w); bean.verticalAnimation = EdgeInsetsTween( - begin: EdgeInsets.only(top: top), - end: EdgeInsets.only(top: 0), + begin: EdgeInsets.only(top: (targetTop + _entryOffset).w), + end: EdgeInsets.only(top: targetTop.w), ).animate( CurvedAnimation(parent: controller, curve: Curves.easeOutCubic), ); @@ -95,11 +113,36 @@ class _GiftAnimalPageState extends State ), ); beans.add(bean); - top = top + 70; } _giftAnimationManager.attachAnimationControllers(beans); } + int _resolveVisibleSlotCount(double availableHeight) { + if (!availableHeight.isFinite || availableHeight <= 0) { + return GiftAnimationManager.maxVisibleSlots; + } + final tickerHeight = _tickerHeight.w; + if (availableHeight < tickerHeight) { + return 0; + } + final slotExtent = _slotExtent.w; + final count = ((availableHeight - tickerHeight) / slotExtent).floor() + 1; + return count.clamp(1, GiftAnimationManager.maxVisibleSlots).toInt(); + } + + double _requiredHeightForSlots(int slotCount) { + return _tickerHeight.w + ((slotCount - 1) * _slotExtent.w); + } + + void _syncVisibleSlotCount(int visibleSlotCount) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (!mounted) { + return; + } + _giftAnimationManager.setVisibleSlotCount(visibleSlotCount); + }); + } + String getBg(String type) { return "sc_images/room/sc_icon_room_gift_left_no_vip_bg.png"; } diff --git a/lib/ui_kit/widgets/room/floating/floating_luck_gift_screen_widget.dart b/lib/ui_kit/widgets/room/floating/floating_luck_gift_screen_widget.dart index df1e0e6..f58762b 100644 --- a/lib/ui_kit/widgets/room/floating/floating_luck_gift_screen_widget.dart +++ b/lib/ui_kit/widgets/room/floating/floating_luck_gift_screen_widget.dart @@ -9,6 +9,7 @@ import 'package:provider/provider.dart'; import 'package:yumi/app/constants/sc_global_config.dart'; import 'package:yumi/services/general/sc_app_general_manager.dart'; import 'package:yumi/shared/tools/sc_network_image_utils.dart'; +import 'package:yumi/shared/tools/sc_lucky_gift_win_sound_player.dart'; import 'package:yumi/shared/tools/sc_room_utils.dart'; import 'package:yumi/main.dart'; import 'package:yumi/ui_kit/widgets/svga/sc_svga_asset_widget.dart'; @@ -83,6 +84,17 @@ class _FloatingLuckGiftScreenWidgetState end: const Offset(-1.5, 0.0), // 到屏幕左侧外 ).animate(CurvedAnimation(parent: _swipeController, curve: Curves.easeIn)); + unawaited( + SCLuckyGiftWinSoundPlayer.play( + eventKey: SCLuckyGiftWinSoundPlayer.buildEventKey( + giftId: widget.message.giftId, + userId: widget.message.userId, + toUserId: widget.message.toUserId, + awardAmount: widget.message.coins, + ), + ), + ); + // 监听主动画完成 _controller.addStatusListener((status) { if (status == AnimationStatus.completed && !_isSwipeAnimating) { diff --git a/lib/ui_kit/widgets/room/room_bottom_widget.dart b/lib/ui_kit/widgets/room/room_bottom_widget.dart index 8e2dad6..c1a1b72 100644 --- a/lib/ui_kit/widgets/room/room_bottom_widget.dart +++ b/lib/ui_kit/widgets/room/room_bottom_widget.dart @@ -24,6 +24,8 @@ import '../../components/sc_debounce_widget.dart'; class RoomBottomWidget extends StatefulWidget { const RoomBottomWidget({super.key, this.showGiftComboButton = true}); + static const double floatingButtonHostHeight = _floatingButtonHostHeight; + final bool showGiftComboButton; @override diff --git a/lib/ui_kit/widgets/room/room_menu_dialog.dart b/lib/ui_kit/widgets/room/room_menu_dialog.dart index 1e89fc8..144dd39 100644 --- a/lib/ui_kit/widgets/room/room_menu_dialog.dart +++ b/lib/ui_kit/widgets/room/room_menu_dialog.dart @@ -38,7 +38,7 @@ class _RoomMenuDialogState extends State { static const int _menuReport = 5; static const int _menuBackground = 6; static const int _menuMusic = 7; - static bool get _showBackgroundFeature => false; + static bool get _showBackgroundFeature => true; List items2 = []; diff --git a/local_packages/flutter_svga-0.0.13-patched/CHANGELOG.md b/local_packages/flutter_svga-0.0.13-patched/CHANGELOG.md new file mode 100644 index 0000000..8d20f38 --- /dev/null +++ b/local_packages/flutter_svga-0.0.13-patched/CHANGELOG.md @@ -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**. diff --git a/local_packages/flutter_svga-0.0.13-patched/LICENSE b/local_packages/flutter_svga-0.0.13-patched/LICENSE new file mode 100644 index 0000000..704c7bb --- /dev/null +++ b/local_packages/flutter_svga-0.0.13-patched/LICENSE @@ -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. diff --git a/local_packages/flutter_svga-0.0.13-patched/README.md b/local_packages/flutter_svga-0.0.13-patched/README.md new file mode 100644 index 0000000..9cedc1b --- /dev/null +++ b/local_packages/flutter_svga-0.0.13-patched/README.md @@ -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. + +

+ + +

+ +--- + +## 🎯 **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 + 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!** 🚀 + diff --git a/local_packages/flutter_svga-0.0.13-patched/analysis_options.yaml b/local_packages/flutter_svga-0.0.13-patched/analysis_options.yaml new file mode 100644 index 0000000..2007ae0 --- /dev/null +++ b/local_packages/flutter_svga-0.0.13-patched/analysis_options.yaml @@ -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 diff --git a/local_packages/flutter_svga-0.0.13-patched/lib/flutter_svga.dart b/local_packages/flutter_svga-0.0.13-patched/lib/flutter_svga.dart new file mode 100644 index 0000000..7ab677c --- /dev/null +++ b/local_packages/flutter_svga-0.0.13-patched/lib/flutter_svga.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; diff --git a/local_packages/flutter_svga-0.0.13-patched/lib/src/audio_layer.dart b/local_packages/flutter_svga-0.0.13-patched/lib/src/audio_layer.dart new file mode 100644 index 0000000..c160b32 --- /dev/null +++ b/local_packages/flutter_svga-0.0.13-patched/lib/src/audio_layer.dart @@ -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 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 dispose() async { + if (_disposed) return; + if (isPlaying() || isPaused()) { + await _player.stop(); + } + _disposed = true; + await _player.dispose(); + } +} diff --git a/local_packages/flutter_svga-0.0.13-patched/lib/src/cache.dart b/local_packages/flutter_svga-0.0.13-patched/lib/src/cache.dart new file mode 100644 index 0000000..ed5b3d4 --- /dev/null +++ b/local_packages/flutter_svga-0.0.13-patched/lib/src/cache.dart @@ -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 _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 _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 _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 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 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 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 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 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 _cleanupCache() async { + if (!_enabled || _cacheDir == null) return; + + try { + await _ensureCacheDir(); + if (_cacheDir == null || !await _cacheDir!.exists()) return; + + final files = []; + 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 = {}; + 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> 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, + }; + } +} \ No newline at end of file diff --git a/local_packages/flutter_svga-0.0.13-patched/lib/src/dynamic_entity.dart b/local_packages/flutter_svga-0.0.13-patched/lib/src/dynamic_entity.dart new file mode 100644 index 0000000..793f508 --- /dev/null +++ b/local_packages/flutter_svga-0.0.13-patched/lib/src/dynamic_entity.dart @@ -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 dynamicHidden = {}; + final Map dynamicImages = {}; + final Map dynamicText = {}; + final Map dynamicDrawer = {}; + + void setHidden(bool value, String forKey) { + dynamicHidden[forKey] = value; + } + + void setImage(ui.Image image, String forKey) { + dynamicImages[forKey] = image; + } + + Future 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(); + } +} diff --git a/local_packages/flutter_svga-0.0.13-patched/lib/src/easy_player.dart b/local_packages/flutter_svga-0.0.13-patched/lib/src/easy_player.dart new file mode 100644 index 0000000..184174e --- /dev/null +++ b/local_packages/flutter_svga-0.0.13-patched/lib/src/easy_player.dart @@ -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 createState() { + return _SVGAEasyPlayerState(); + } +} + +class _SVGAEasyPlayerState extends State + 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 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), + ], + ), + ); + }, + ); + } +} diff --git a/local_packages/flutter_svga-0.0.13-patched/lib/src/painter.dart b/local_packages/flutter_svga-0.0.13-patched/lib/src/painter.dart new file mode 100644 index 0000000..f5be380 --- /dev/null +++ b/local_packages/flutter_svga-0.0.13-patched/lib/src/painter.dart @@ -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([ + 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 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([ + 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 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); + } +} diff --git a/local_packages/flutter_svga-0.0.13-patched/lib/src/parser.dart b/local_packages/flutter_svga-0.0.13-patched/lib/src/parser.dart new file mode 100644 index 0000000..b4163aa --- /dev/null +++ b/local_packages/flutter_svga-0.0.13-patched/lib/src/parser.dart @@ -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 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 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 decodeFromBuffer(List 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? 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 _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 _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; + } +} diff --git a/local_packages/flutter_svga-0.0.13-patched/lib/src/player.dart b/local_packages/flutter_svga-0.0.13-patched/lib/src/player.dart new file mode 100644 index 0000000..39241ec --- /dev/null +++ b/local_packages/flutter_svga-0.0.13-patched/lib/src/player.dart @@ -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 createState() => _SVGAImageState(); + + @override + void debugFillProperties(DiagnosticPropertiesBuilder properties) { + super.debugFillProperties(properties); + properties.add(DiagnosticsProperty('controller', _controller)); + } +} + +class SVGAAnimationController extends AnimationController { + MovieEntity? _videoItem; + final List _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.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 { + 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, + ), + ); + } +} diff --git a/local_packages/flutter_svga-0.0.13-patched/lib/src/proto/svga.pb.dart b/local_packages/flutter_svga-0.0.13-patched/lib/src/proto/svga.pb.dart new file mode 100644 index 0000000..3cb5910 --- /dev/null +++ b/local_packages/flutter_svga-0.0.13-patched/lib/src/proto/svga.pb.dart @@ -0,0 +1,2189 @@ +// 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,no_leading_underscores_for_local_identifiers + +import 'dart:core' as $core; +import 'dart:core' show int, bool, double, String, List, Map, override; +import 'dart:ui' as ui show Image, Path; +import 'package:protobuf/protobuf.dart' as $pb; +import 'dart:typed_data'; +import 'svga.pbenum.dart'; + +export 'svga.pbenum.dart'; +import '../dynamic_entity.dart'; + +class MovieParams extends $pb.GeneratedMessage { + static final $pb.BuilderInfo _i = + $pb.BuilderInfo( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'MovieParams', + package: const $pb.PackageName( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'com.opensource.svga', + ), + createEmptyInstance: create, + ) + ..a<$core.double>( + 1, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'viewBoxWidth', + $pb.PbFieldType.OF, + protoName: 'viewBoxWidth', + ) + ..a<$core.double>( + 2, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'viewBoxHeight', + $pb.PbFieldType.OF, + protoName: 'viewBoxHeight', + ) + ..a<$core.int>( + 3, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'fps', + $pb.PbFieldType.O3, + ) + ..a<$core.int>( + 4, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'frames', + $pb.PbFieldType.O3, + ) + ..hasRequiredFields = false; + + MovieParams._() : super(); + factory MovieParams({ + $core.double? viewBoxWidth, + $core.double? viewBoxHeight, + $core.int? fps, + $core.int? frames, + }) { + final _result = create(); + if (viewBoxWidth != null) { + _result.viewBoxWidth = viewBoxWidth; + } + if (viewBoxHeight != null) { + _result.viewBoxHeight = viewBoxHeight; + } + if (fps != null) { + _result.fps = fps; + } + if (frames != null) { + _result.frames = frames; + } + return _result; + } + factory MovieParams.fromBuffer( + $core.List<$core.int> i, [ + $pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY, + ]) => create()..mergeFromBuffer(i, r); + factory MovieParams.fromJson( + $core.String i, [ + $pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY, + ]) => create()..mergeFromJson(i, r); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version', + ) + MovieParams clone() => MovieParams()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version', + ) + MovieParams copyWith(void Function(MovieParams) updates) => + super.copyWith((message) => updates(message as MovieParams)) + as MovieParams; // ignore: deprecated_member_use + $pb.BuilderInfo get info_ => _i; + @$core.pragma('dart2js:noInline') + static MovieParams create() => MovieParams._(); + MovieParams createEmptyInstance() => create(); + static $core.List createRepeated() => []; + @$core.pragma('dart2js:noInline') + static MovieParams getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static MovieParams? _defaultInstance; + + @$pb.TagNumber(1) + $core.double get viewBoxWidth => $_getN(0); + @$pb.TagNumber(1) + set viewBoxWidth($core.double v) { + $_setFloat(0, v); + } + + @$pb.TagNumber(1) + $core.bool hasViewBoxWidth() => $_has(0); + @$pb.TagNumber(1) + void clearViewBoxWidth() => clearField(1); + + @$pb.TagNumber(2) + $core.double get viewBoxHeight => $_getN(1); + @$pb.TagNumber(2) + set viewBoxHeight($core.double v) { + $_setFloat(1, v); + } + + @$pb.TagNumber(2) + $core.bool hasViewBoxHeight() => $_has(1); + @$pb.TagNumber(2) + void clearViewBoxHeight() => clearField(2); + + @$pb.TagNumber(3) + $core.int get fps => $_getIZ(2); + @$pb.TagNumber(3) + set fps($core.int v) { + $_setSignedInt32(2, v); + } + + @$pb.TagNumber(3) + $core.bool hasFps() => $_has(2); + @$pb.TagNumber(3) + void clearFps() => clearField(3); + + @$pb.TagNumber(4) + $core.int get frames => $_getIZ(3); + @$pb.TagNumber(4) + set frames($core.int v) { + $_setSignedInt32(3, v); + } + + @$pb.TagNumber(4) + $core.bool hasFrames() => $_has(3); + @$pb.TagNumber(4) + void clearFrames() => clearField(4); +} + +class SpriteEntity extends $pb.GeneratedMessage { + static final $pb.BuilderInfo _i = + $pb.BuilderInfo( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'SpriteEntity', + package: const $pb.PackageName( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'com.opensource.svga', + ), + createEmptyInstance: create, + ) + ..aOS( + 1, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'imageKey', + protoName: 'imageKey', + ) + ..pc( + 2, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'frames', + $pb.PbFieldType.PM, + subBuilder: FrameEntity.create, + ) + ..aOS( + 3, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'matteKey', + protoName: 'matteKey', + ) + ..hasRequiredFields = false; + + SpriteEntity._() : super(); + factory SpriteEntity({ + $core.String? imageKey, + $core.Iterable? frames, + $core.String? matteKey, + }) { + final _result = create(); + if (imageKey != null) { + _result.imageKey = imageKey; + } + if (frames != null) { + _result.frames.addAll(frames); + } + if (matteKey != null) { + _result.matteKey = matteKey; + } + return _result; + } + factory SpriteEntity.fromBuffer( + $core.List<$core.int> i, [ + $pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY, + ]) => create()..mergeFromBuffer(i, r); + factory SpriteEntity.fromJson( + $core.String i, [ + $pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY, + ]) => create()..mergeFromJson(i, r); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version', + ) + SpriteEntity clone() => SpriteEntity()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version', + ) + SpriteEntity copyWith(void Function(SpriteEntity) updates) => + super.copyWith((message) => updates(message as SpriteEntity)) + as SpriteEntity; // ignore: deprecated_member_use + $pb.BuilderInfo get info_ => _i; + @$core.pragma('dart2js:noInline') + static SpriteEntity create() => SpriteEntity._(); + SpriteEntity createEmptyInstance() => create(); + static $core.List createRepeated() => []; + @$core.pragma('dart2js:noInline') + static SpriteEntity getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static SpriteEntity? _defaultInstance; + + @$pb.TagNumber(1) + $core.String get imageKey => $_getSZ(0); + @$pb.TagNumber(1) + set imageKey($core.String v) { + $_setString(0, v); + } + + @$pb.TagNumber(1) + $core.bool hasImageKey() => $_has(0); + @$pb.TagNumber(1) + void clearImageKey() => clearField(1); + + @$pb.TagNumber(2) + $core.List get frames => $_getList(1); + + @$pb.TagNumber(3) + $core.String get matteKey => $_getSZ(2); + @$pb.TagNumber(3) + set matteKey($core.String v) { + $_setString(2, v); + } + + @$pb.TagNumber(3) + $core.bool hasMatteKey() => $_has(2); + @$pb.TagNumber(3) + void clearMatteKey() => clearField(3); +} + +class AudioEntity extends $pb.GeneratedMessage { + static final $pb.BuilderInfo _i = + $pb.BuilderInfo( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'AudioEntity', + package: const $pb.PackageName( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'com.opensource.svga', + ), + createEmptyInstance: create, + ) + ..aOS( + 1, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'audioKey', + protoName: 'audioKey', + ) + ..a<$core.int>( + 2, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'startFrame', + $pb.PbFieldType.O3, + protoName: 'startFrame', + ) + ..a<$core.int>( + 3, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'endFrame', + $pb.PbFieldType.O3, + protoName: 'endFrame', + ) + ..a<$core.int>( + 4, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'startTime', + $pb.PbFieldType.O3, + protoName: 'startTime', + ) + ..a<$core.int>( + 5, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'totalTime', + $pb.PbFieldType.O3, + protoName: 'totalTime', + ) + ..hasRequiredFields = false; + + AudioEntity._() : super(); + factory AudioEntity({ + $core.String? audioKey, + $core.int? startFrame, + $core.int? endFrame, + $core.int? startTime, + $core.int? totalTime, + }) { + final _result = create(); + if (audioKey != null) { + _result.audioKey = audioKey; + } + if (startFrame != null) { + _result.startFrame = startFrame; + } + if (endFrame != null) { + _result.endFrame = endFrame; + } + if (startTime != null) { + _result.startTime = startTime; + } + if (totalTime != null) { + _result.totalTime = totalTime; + } + return _result; + } + factory AudioEntity.fromBuffer( + $core.List<$core.int> i, [ + $pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY, + ]) => create()..mergeFromBuffer(i, r); + factory AudioEntity.fromJson( + $core.String i, [ + $pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY, + ]) => create()..mergeFromJson(i, r); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version', + ) + AudioEntity clone() => AudioEntity()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version', + ) + AudioEntity copyWith(void Function(AudioEntity) updates) => + super.copyWith((message) => updates(message as AudioEntity)) + as AudioEntity; // ignore: deprecated_member_use + $pb.BuilderInfo get info_ => _i; + @$core.pragma('dart2js:noInline') + static AudioEntity create() => AudioEntity._(); + AudioEntity createEmptyInstance() => create(); + static $core.List createRepeated() => []; + @$core.pragma('dart2js:noInline') + static AudioEntity getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static AudioEntity? _defaultInstance; + + @$pb.TagNumber(1) + $core.String get audioKey => $_getSZ(0); + @$pb.TagNumber(1) + set audioKey($core.String v) { + $_setString(0, v); + } + + @$pb.TagNumber(1) + $core.bool hasAudioKey() => $_has(0); + @$pb.TagNumber(1) + void clearAudioKey() => clearField(1); + + @$pb.TagNumber(2) + $core.int get startFrame => $_getIZ(1); + @$pb.TagNumber(2) + set startFrame($core.int v) { + $_setSignedInt32(1, v); + } + + @$pb.TagNumber(2) + $core.bool hasStartFrame() => $_has(1); + @$pb.TagNumber(2) + void clearStartFrame() => clearField(2); + + @$pb.TagNumber(3) + $core.int get endFrame => $_getIZ(2); + @$pb.TagNumber(3) + set endFrame($core.int v) { + $_setSignedInt32(2, v); + } + + @$pb.TagNumber(3) + $core.bool hasEndFrame() => $_has(2); + @$pb.TagNumber(3) + void clearEndFrame() => clearField(3); + + @$pb.TagNumber(4) + $core.int get startTime => $_getIZ(3); + @$pb.TagNumber(4) + set startTime($core.int v) { + $_setSignedInt32(3, v); + } + + @$pb.TagNumber(4) + $core.bool hasStartTime() => $_has(3); + @$pb.TagNumber(4) + void clearStartTime() => clearField(4); + + @$pb.TagNumber(5) + $core.int get totalTime => $_getIZ(4); + @$pb.TagNumber(5) + set totalTime($core.int v) { + $_setSignedInt32(4, v); + } + + @$pb.TagNumber(5) + $core.bool hasTotalTime() => $_has(4); + @$pb.TagNumber(5) + void clearTotalTime() => clearField(5); +} + +class Layout extends $pb.GeneratedMessage { + static final $pb.BuilderInfo _i = + $pb.BuilderInfo( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'Layout', + package: const $pb.PackageName( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'com.opensource.svga', + ), + createEmptyInstance: create, + ) + ..a<$core.double>( + 1, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'x', + $pb.PbFieldType.OF, + ) + ..a<$core.double>( + 2, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'y', + $pb.PbFieldType.OF, + ) + ..a<$core.double>( + 3, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'width', + $pb.PbFieldType.OF, + ) + ..a<$core.double>( + 4, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'height', + $pb.PbFieldType.OF, + ) + ..hasRequiredFields = false; + + Layout._() : super(); + factory Layout({ + $core.double? x, + $core.double? y, + $core.double? width, + $core.double? height, + }) { + final _result = create(); + if (x != null) { + _result.x = x; + } + if (y != null) { + _result.y = y; + } + if (width != null) { + _result.width = width; + } + if (height != null) { + _result.height = height; + } + return _result; + } + factory Layout.fromBuffer( + $core.List<$core.int> i, [ + $pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY, + ]) => create()..mergeFromBuffer(i, r); + factory Layout.fromJson( + $core.String i, [ + $pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY, + ]) => create()..mergeFromJson(i, r); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version', + ) + Layout clone() => Layout()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version', + ) + Layout copyWith(void Function(Layout) updates) => + super.copyWith((message) => updates(message as Layout)) as Layout; // ignore: deprecated_member_use + $pb.BuilderInfo get info_ => _i; + @$core.pragma('dart2js:noInline') + static Layout create() => Layout._(); + Layout createEmptyInstance() => create(); + static $core.List createRepeated() => []; + @$core.pragma('dart2js:noInline') + static Layout getDefault() => + _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static Layout? _defaultInstance; + + @$pb.TagNumber(1) + $core.double get x => $_getN(0); + @$pb.TagNumber(1) + set x($core.double v) { + $_setFloat(0, v); + } + + @$pb.TagNumber(1) + $core.bool hasX() => $_has(0); + @$pb.TagNumber(1) + void clearX() => clearField(1); + + @$pb.TagNumber(2) + $core.double get y => $_getN(1); + @$pb.TagNumber(2) + set y($core.double v) { + $_setFloat(1, v); + } + + @$pb.TagNumber(2) + $core.bool hasY() => $_has(1); + @$pb.TagNumber(2) + void clearY() => clearField(2); + + @$pb.TagNumber(3) + $core.double get width => $_getN(2); + @$pb.TagNumber(3) + set width($core.double v) { + $_setFloat(2, v); + } + + @$pb.TagNumber(3) + $core.bool hasWidth() => $_has(2); + @$pb.TagNumber(3) + void clearWidth() => clearField(3); + + @$pb.TagNumber(4) + $core.double get height => $_getN(3); + @$pb.TagNumber(4) + set height($core.double v) { + $_setFloat(3, v); + } + + @$pb.TagNumber(4) + $core.bool hasHeight() => $_has(3); + @$pb.TagNumber(4) + void clearHeight() => clearField(4); +} + +class Transform extends $pb.GeneratedMessage { + static final $pb.BuilderInfo _i = + $pb.BuilderInfo( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'Transform', + package: const $pb.PackageName( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'com.opensource.svga', + ), + createEmptyInstance: create, + ) + ..a<$core.double>( + 1, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'a', + $pb.PbFieldType.OF, + ) + ..a<$core.double>( + 2, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'b', + $pb.PbFieldType.OF, + ) + ..a<$core.double>( + 3, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'c', + $pb.PbFieldType.OF, + ) + ..a<$core.double>( + 4, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'd', + $pb.PbFieldType.OF, + ) + ..a<$core.double>( + 5, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'tx', + $pb.PbFieldType.OF, + ) + ..a<$core.double>( + 6, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'ty', + $pb.PbFieldType.OF, + ) + ..hasRequiredFields = false; + + Transform._() : super(); + factory Transform({ + $core.double? a, + $core.double? b, + $core.double? c, + $core.double? d, + $core.double? tx, + $core.double? ty, + }) { + final _result = create(); + if (a != null) { + _result.a = a; + } + if (b != null) { + _result.b = b; + } + if (c != null) { + _result.c = c; + } + if (d != null) { + _result.d = d; + } + if (tx != null) { + _result.tx = tx; + } + if (ty != null) { + _result.ty = ty; + } + return _result; + } + factory Transform.fromBuffer( + $core.List<$core.int> i, [ + $pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY, + ]) => create()..mergeFromBuffer(i, r); + factory Transform.fromJson( + $core.String i, [ + $pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY, + ]) => create()..mergeFromJson(i, r); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version', + ) + Transform clone() => Transform()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version', + ) + Transform copyWith(void Function(Transform) updates) => + super.copyWith((message) => updates(message as Transform)) as Transform; // ignore: deprecated_member_use + $pb.BuilderInfo get info_ => _i; + @$core.pragma('dart2js:noInline') + static Transform create() => Transform._(); + Transform createEmptyInstance() => create(); + static $core.List createRepeated() => []; + @$core.pragma('dart2js:noInline') + static Transform getDefault() => + _defaultInstance ??= $pb.GeneratedMessage.$_defaultFor(create); + static Transform? _defaultInstance; + + @$pb.TagNumber(1) + $core.double get a => $_getN(0); + @$pb.TagNumber(1) + set a($core.double v) { + $_setFloat(0, v); + } + + @$pb.TagNumber(1) + $core.bool hasA() => $_has(0); + @$pb.TagNumber(1) + void clearA() => clearField(1); + + @$pb.TagNumber(2) + $core.double get b => $_getN(1); + @$pb.TagNumber(2) + set b($core.double v) { + $_setFloat(1, v); + } + + @$pb.TagNumber(2) + $core.bool hasB() => $_has(1); + @$pb.TagNumber(2) + void clearB() => clearField(2); + + @$pb.TagNumber(3) + $core.double get c => $_getN(2); + @$pb.TagNumber(3) + set c($core.double v) { + $_setFloat(2, v); + } + + @$pb.TagNumber(3) + $core.bool hasC() => $_has(2); + @$pb.TagNumber(3) + void clearC() => clearField(3); + + @$pb.TagNumber(4) + $core.double get d => $_getN(3); + @$pb.TagNumber(4) + set d($core.double v) { + $_setFloat(3, v); + } + + @$pb.TagNumber(4) + $core.bool hasD() => $_has(3); + @$pb.TagNumber(4) + void clearD() => clearField(4); + + @$pb.TagNumber(5) + $core.double get tx => $_getN(4); + @$pb.TagNumber(5) + set tx($core.double v) { + $_setFloat(4, v); + } + + @$pb.TagNumber(5) + $core.bool hasTx() => $_has(4); + @$pb.TagNumber(5) + void clearTx() => clearField(5); + + @$pb.TagNumber(6) + $core.double get ty => $_getN(5); + @$pb.TagNumber(6) + set ty($core.double v) { + $_setFloat(5, v); + } + + @$pb.TagNumber(6) + $core.bool hasTy() => $_has(5); + @$pb.TagNumber(6) + void clearTy() => clearField(6); +} + +class ShapeEntity_ShapeArgs extends $pb.GeneratedMessage { + static final $pb.BuilderInfo _i = + $pb.BuilderInfo( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'ShapeEntity.ShapeArgs', + package: const $pb.PackageName( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'com.opensource.svga', + ), + createEmptyInstance: create, + ) + ..aOS( + 1, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'd', + ) + ..hasRequiredFields = false; + + ShapeEntity_ShapeArgs._() : super(); + factory ShapeEntity_ShapeArgs({$core.String? d}) { + final _result = create(); + if (d != null) { + _result.d = d; + } + return _result; + } + factory ShapeEntity_ShapeArgs.fromBuffer( + $core.List<$core.int> i, [ + $pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY, + ]) => create()..mergeFromBuffer(i, r); + factory ShapeEntity_ShapeArgs.fromJson( + $core.String i, [ + $pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY, + ]) => create()..mergeFromJson(i, r); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version', + ) + ShapeEntity_ShapeArgs clone() => + ShapeEntity_ShapeArgs()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version', + ) + ShapeEntity_ShapeArgs copyWith( + void Function(ShapeEntity_ShapeArgs) updates, + ) => + super.copyWith((message) => updates(message as ShapeEntity_ShapeArgs)) + as ShapeEntity_ShapeArgs; // ignore: deprecated_member_use + $pb.BuilderInfo get info_ => _i; + @$core.pragma('dart2js:noInline') + static ShapeEntity_ShapeArgs create() => ShapeEntity_ShapeArgs._(); + ShapeEntity_ShapeArgs createEmptyInstance() => create(); + static $core.List createRepeated() => + []; + @$core.pragma('dart2js:noInline') + static ShapeEntity_ShapeArgs getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static ShapeEntity_ShapeArgs? _defaultInstance; + + @$pb.TagNumber(1) + $core.String get d => $_getSZ(0); + @$pb.TagNumber(1) + set d($core.String v) { + $_setString(0, v); + } + + @$pb.TagNumber(1) + $core.bool hasD() => $_has(0); + @$pb.TagNumber(1) + void clearD() => clearField(1); +} + +class ShapeEntity_RectArgs extends $pb.GeneratedMessage { + static final $pb.BuilderInfo _i = + $pb.BuilderInfo( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'ShapeEntity.RectArgs', + package: const $pb.PackageName( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'com.opensource.svga', + ), + createEmptyInstance: create, + ) + ..a<$core.double>( + 1, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'x', + $pb.PbFieldType.OF, + ) + ..a<$core.double>( + 2, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'y', + $pb.PbFieldType.OF, + ) + ..a<$core.double>( + 3, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'width', + $pb.PbFieldType.OF, + ) + ..a<$core.double>( + 4, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'height', + $pb.PbFieldType.OF, + ) + ..a<$core.double>( + 5, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'cornerRadius', + $pb.PbFieldType.OF, + protoName: 'cornerRadius', + ) + ..hasRequiredFields = false; + + ShapeEntity_RectArgs._() : super(); + factory ShapeEntity_RectArgs({ + $core.double? x, + $core.double? y, + $core.double? width, + $core.double? height, + $core.double? cornerRadius, + }) { + final _result = create(); + if (x != null) { + _result.x = x; + } + if (y != null) { + _result.y = y; + } + if (width != null) { + _result.width = width; + } + if (height != null) { + _result.height = height; + } + if (cornerRadius != null) { + _result.cornerRadius = cornerRadius; + } + return _result; + } + factory ShapeEntity_RectArgs.fromBuffer( + $core.List<$core.int> i, [ + $pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY, + ]) => create()..mergeFromBuffer(i, r); + factory ShapeEntity_RectArgs.fromJson( + $core.String i, [ + $pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY, + ]) => create()..mergeFromJson(i, r); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version', + ) + ShapeEntity_RectArgs clone() => + ShapeEntity_RectArgs()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version', + ) + ShapeEntity_RectArgs copyWith(void Function(ShapeEntity_RectArgs) updates) => + super.copyWith((message) => updates(message as ShapeEntity_RectArgs)) + as ShapeEntity_RectArgs; // ignore: deprecated_member_use + $pb.BuilderInfo get info_ => _i; + @$core.pragma('dart2js:noInline') + static ShapeEntity_RectArgs create() => ShapeEntity_RectArgs._(); + ShapeEntity_RectArgs createEmptyInstance() => create(); + static $core.List createRepeated() => + []; + @$core.pragma('dart2js:noInline') + static ShapeEntity_RectArgs getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static ShapeEntity_RectArgs? _defaultInstance; + + @$pb.TagNumber(1) + $core.double get x => $_getN(0); + @$pb.TagNumber(1) + set x($core.double v) { + $_setFloat(0, v); + } + + @$pb.TagNumber(1) + $core.bool hasX() => $_has(0); + @$pb.TagNumber(1) + void clearX() => clearField(1); + + @$pb.TagNumber(2) + $core.double get y => $_getN(1); + @$pb.TagNumber(2) + set y($core.double v) { + $_setFloat(1, v); + } + + @$pb.TagNumber(2) + $core.bool hasY() => $_has(1); + @$pb.TagNumber(2) + void clearY() => clearField(2); + + @$pb.TagNumber(3) + $core.double get width => $_getN(2); + @$pb.TagNumber(3) + set width($core.double v) { + $_setFloat(2, v); + } + + @$pb.TagNumber(3) + $core.bool hasWidth() => $_has(2); + @$pb.TagNumber(3) + void clearWidth() => clearField(3); + + @$pb.TagNumber(4) + $core.double get height => $_getN(3); + @$pb.TagNumber(4) + set height($core.double v) { + $_setFloat(3, v); + } + + @$pb.TagNumber(4) + $core.bool hasHeight() => $_has(3); + @$pb.TagNumber(4) + void clearHeight() => clearField(4); + + @$pb.TagNumber(5) + $core.double get cornerRadius => $_getN(4); + @$pb.TagNumber(5) + set cornerRadius($core.double v) { + $_setFloat(4, v); + } + + @$pb.TagNumber(5) + $core.bool hasCornerRadius() => $_has(4); + @$pb.TagNumber(5) + void clearCornerRadius() => clearField(5); +} + +class ShapeEntity_EllipseArgs extends $pb.GeneratedMessage { + static final $pb.BuilderInfo _i = + $pb.BuilderInfo( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'ShapeEntity.EllipseArgs', + package: const $pb.PackageName( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'com.opensource.svga', + ), + createEmptyInstance: create, + ) + ..a<$core.double>( + 1, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'x', + $pb.PbFieldType.OF, + ) + ..a<$core.double>( + 2, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'y', + $pb.PbFieldType.OF, + ) + ..a<$core.double>( + 3, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'radiusX', + $pb.PbFieldType.OF, + protoName: 'radiusX', + ) + ..a<$core.double>( + 4, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'radiusY', + $pb.PbFieldType.OF, + protoName: 'radiusY', + ) + ..hasRequiredFields = false; + + ShapeEntity_EllipseArgs._() : super(); + factory ShapeEntity_EllipseArgs({ + $core.double? x, + $core.double? y, + $core.double? radiusX, + $core.double? radiusY, + }) { + final _result = create(); + if (x != null) { + _result.x = x; + } + if (y != null) { + _result.y = y; + } + if (radiusX != null) { + _result.radiusX = radiusX; + } + if (radiusY != null) { + _result.radiusY = radiusY; + } + return _result; + } + factory ShapeEntity_EllipseArgs.fromBuffer( + $core.List<$core.int> i, [ + $pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY, + ]) => create()..mergeFromBuffer(i, r); + factory ShapeEntity_EllipseArgs.fromJson( + $core.String i, [ + $pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY, + ]) => create()..mergeFromJson(i, r); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version', + ) + ShapeEntity_EllipseArgs clone() => + ShapeEntity_EllipseArgs()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version', + ) + ShapeEntity_EllipseArgs copyWith( + void Function(ShapeEntity_EllipseArgs) updates, + ) => + super.copyWith((message) => updates(message as ShapeEntity_EllipseArgs)) + as ShapeEntity_EllipseArgs; // ignore: deprecated_member_use + $pb.BuilderInfo get info_ => _i; + @$core.pragma('dart2js:noInline') + static ShapeEntity_EllipseArgs create() => ShapeEntity_EllipseArgs._(); + ShapeEntity_EllipseArgs createEmptyInstance() => create(); + static $core.List createRepeated() => + []; + @$core.pragma('dart2js:noInline') + static ShapeEntity_EllipseArgs getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static ShapeEntity_EllipseArgs? _defaultInstance; + + @$pb.TagNumber(1) + $core.double get x => $_getN(0); + @$pb.TagNumber(1) + set x($core.double v) { + $_setFloat(0, v); + } + + @$pb.TagNumber(1) + $core.bool hasX() => $_has(0); + @$pb.TagNumber(1) + void clearX() => clearField(1); + + @$pb.TagNumber(2) + $core.double get y => $_getN(1); + @$pb.TagNumber(2) + set y($core.double v) { + $_setFloat(1, v); + } + + @$pb.TagNumber(2) + $core.bool hasY() => $_has(1); + @$pb.TagNumber(2) + void clearY() => clearField(2); + + @$pb.TagNumber(3) + $core.double get radiusX => $_getN(2); + @$pb.TagNumber(3) + set radiusX($core.double v) { + $_setFloat(2, v); + } + + @$pb.TagNumber(3) + $core.bool hasRadiusX() => $_has(2); + @$pb.TagNumber(3) + void clearRadiusX() => clearField(3); + + @$pb.TagNumber(4) + $core.double get radiusY => $_getN(3); + @$pb.TagNumber(4) + set radiusY($core.double v) { + $_setFloat(3, v); + } + + @$pb.TagNumber(4) + $core.bool hasRadiusY() => $_has(3); + @$pb.TagNumber(4) + void clearRadiusY() => clearField(4); +} + +class ShapeEntity_ShapeStyle_RGBAColor extends $pb.GeneratedMessage { + static final $pb.BuilderInfo _i = + $pb.BuilderInfo( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'ShapeEntity.ShapeStyle.RGBAColor', + package: const $pb.PackageName( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'com.opensource.svga', + ), + createEmptyInstance: create, + ) + ..a<$core.double>( + 1, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'r', + $pb.PbFieldType.OF, + ) + ..a<$core.double>( + 2, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'g', + $pb.PbFieldType.OF, + ) + ..a<$core.double>( + 3, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'b', + $pb.PbFieldType.OF, + ) + ..a<$core.double>( + 4, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'a', + $pb.PbFieldType.OF, + ) + ..hasRequiredFields = false; + + ShapeEntity_ShapeStyle_RGBAColor._() : super(); + factory ShapeEntity_ShapeStyle_RGBAColor({ + $core.double? r, + $core.double? g, + $core.double? b, + $core.double? a, + }) { + final _result = create(); + if (r != null) { + _result.r = r; + } + if (g != null) { + _result.g = g; + } + if (b != null) { + _result.b = b; + } + if (a != null) { + _result.a = a; + } + return _result; + } + factory ShapeEntity_ShapeStyle_RGBAColor.fromBuffer( + $core.List<$core.int> i, [ + $pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY, + ]) => create()..mergeFromBuffer(i, r); + factory ShapeEntity_ShapeStyle_RGBAColor.fromJson( + $core.String i, [ + $pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY, + ]) => create()..mergeFromJson(i, r); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version', + ) + ShapeEntity_ShapeStyle_RGBAColor clone() => + ShapeEntity_ShapeStyle_RGBAColor()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version', + ) + ShapeEntity_ShapeStyle_RGBAColor copyWith( + void Function(ShapeEntity_ShapeStyle_RGBAColor) updates, + ) => + super.copyWith( + (message) => updates(message as ShapeEntity_ShapeStyle_RGBAColor), + ) + as ShapeEntity_ShapeStyle_RGBAColor; // ignore: deprecated_member_use + $pb.BuilderInfo get info_ => _i; + @$core.pragma('dart2js:noInline') + static ShapeEntity_ShapeStyle_RGBAColor create() => + ShapeEntity_ShapeStyle_RGBAColor._(); + ShapeEntity_ShapeStyle_RGBAColor createEmptyInstance() => create(); + static $core.List createRepeated() => + []; + @$core.pragma('dart2js:noInline') + static ShapeEntity_ShapeStyle_RGBAColor getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor( + create, + ); + static ShapeEntity_ShapeStyle_RGBAColor? _defaultInstance; + + @$pb.TagNumber(1) + $core.double get r => $_getN(0); + @$pb.TagNumber(1) + set r($core.double v) { + $_setFloat(0, v); + } + + @$pb.TagNumber(1) + $core.bool hasR() => $_has(0); + @$pb.TagNumber(1) + void clearR() => clearField(1); + + @$pb.TagNumber(2) + $core.double get g => $_getN(1); + @$pb.TagNumber(2) + set g($core.double v) { + $_setFloat(1, v); + } + + @$pb.TagNumber(2) + $core.bool hasG() => $_has(1); + @$pb.TagNumber(2) + void clearG() => clearField(2); + + @$pb.TagNumber(3) + $core.double get b => $_getN(2); + @$pb.TagNumber(3) + set b($core.double v) { + $_setFloat(2, v); + } + + @$pb.TagNumber(3) + $core.bool hasB() => $_has(2); + @$pb.TagNumber(3) + void clearB() => clearField(3); + + @$pb.TagNumber(4) + $core.double get a => $_getN(3); + @$pb.TagNumber(4) + set a($core.double v) { + $_setFloat(3, v); + } + + @$pb.TagNumber(4) + $core.bool hasA() => $_has(3); + @$pb.TagNumber(4) + void clearA() => clearField(4); +} + +class ShapeEntity_ShapeStyle extends $pb.GeneratedMessage { + static final $pb.BuilderInfo _i = + $pb.BuilderInfo( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'ShapeEntity.ShapeStyle', + package: const $pb.PackageName( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'com.opensource.svga', + ), + createEmptyInstance: create, + ) + ..aOM( + 1, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'fill', + subBuilder: ShapeEntity_ShapeStyle_RGBAColor.create, + ) + ..aOM( + 2, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'stroke', + subBuilder: ShapeEntity_ShapeStyle_RGBAColor.create, + ) + ..a<$core.double>( + 3, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'strokeWidth', + $pb.PbFieldType.OF, + protoName: 'strokeWidth', + ) + ..e( + 4, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'lineCap', + $pb.PbFieldType.OE, + protoName: 'lineCap', + defaultOrMaker: ShapeEntity_ShapeStyle_LineCap.LineCap_BUTT, + valueOf: ShapeEntity_ShapeStyle_LineCap.valueOf, + enumValues: ShapeEntity_ShapeStyle_LineCap.values, + ) + ..e( + 5, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'lineJoin', + $pb.PbFieldType.OE, + protoName: 'lineJoin', + defaultOrMaker: ShapeEntity_ShapeStyle_LineJoin.LineJoin_MITER, + valueOf: ShapeEntity_ShapeStyle_LineJoin.valueOf, + enumValues: ShapeEntity_ShapeStyle_LineJoin.values, + ) + ..a<$core.double>( + 6, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'miterLimit', + $pb.PbFieldType.OF, + protoName: 'miterLimit', + ) + ..a<$core.double>( + 7, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'lineDashI', + $pb.PbFieldType.OF, + protoName: 'lineDashI', + ) + ..a<$core.double>( + 8, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'lineDashII', + $pb.PbFieldType.OF, + protoName: 'lineDashII', + ) + ..a<$core.double>( + 9, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'lineDashIII', + $pb.PbFieldType.OF, + protoName: 'lineDashIII', + ) + ..hasRequiredFields = false; + + ShapeEntity_ShapeStyle._() : super(); + factory ShapeEntity_ShapeStyle({ + ShapeEntity_ShapeStyle_RGBAColor? fill, + ShapeEntity_ShapeStyle_RGBAColor? stroke, + $core.double? strokeWidth, + ShapeEntity_ShapeStyle_LineCap? lineCap, + ShapeEntity_ShapeStyle_LineJoin? lineJoin, + $core.double? miterLimit, + $core.double? lineDashI, + $core.double? lineDashII, + $core.double? lineDashIII, + }) { + final _result = create(); + if (fill != null) { + _result.fill = fill; + } + if (stroke != null) { + _result.stroke = stroke; + } + if (strokeWidth != null) { + _result.strokeWidth = strokeWidth; + } + if (lineCap != null) { + _result.lineCap = lineCap; + } + if (lineJoin != null) { + _result.lineJoin = lineJoin; + } + if (miterLimit != null) { + _result.miterLimit = miterLimit; + } + if (lineDashI != null) { + _result.lineDashI = lineDashI; + } + if (lineDashII != null) { + _result.lineDashII = lineDashII; + } + if (lineDashIII != null) { + _result.lineDashIII = lineDashIII; + } + return _result; + } + factory ShapeEntity_ShapeStyle.fromBuffer( + $core.List<$core.int> i, [ + $pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY, + ]) => create()..mergeFromBuffer(i, r); + factory ShapeEntity_ShapeStyle.fromJson( + $core.String i, [ + $pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY, + ]) => create()..mergeFromJson(i, r); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version', + ) + ShapeEntity_ShapeStyle clone() => + ShapeEntity_ShapeStyle()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version', + ) + ShapeEntity_ShapeStyle copyWith( + void Function(ShapeEntity_ShapeStyle) updates, + ) => + super.copyWith((message) => updates(message as ShapeEntity_ShapeStyle)) + as ShapeEntity_ShapeStyle; // ignore: deprecated_member_use + $pb.BuilderInfo get info_ => _i; + @$core.pragma('dart2js:noInline') + static ShapeEntity_ShapeStyle create() => ShapeEntity_ShapeStyle._(); + ShapeEntity_ShapeStyle createEmptyInstance() => create(); + static $core.List createRepeated() => + []; + @$core.pragma('dart2js:noInline') + static ShapeEntity_ShapeStyle getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static ShapeEntity_ShapeStyle? _defaultInstance; + + @$pb.TagNumber(1) + ShapeEntity_ShapeStyle_RGBAColor get fill => $_getN(0); + @$pb.TagNumber(1) + set fill(ShapeEntity_ShapeStyle_RGBAColor v) { + setField(1, v); + } + + @$pb.TagNumber(1) + $core.bool hasFill() => $_has(0); + @$pb.TagNumber(1) + void clearFill() => clearField(1); + @$pb.TagNumber(1) + ShapeEntity_ShapeStyle_RGBAColor ensureFill() => $_ensure(0); + + @$pb.TagNumber(2) + ShapeEntity_ShapeStyle_RGBAColor get stroke => $_getN(1); + @$pb.TagNumber(2) + set stroke(ShapeEntity_ShapeStyle_RGBAColor v) { + setField(2, v); + } + + @$pb.TagNumber(2) + $core.bool hasStroke() => $_has(1); + @$pb.TagNumber(2) + void clearStroke() => clearField(2); + @$pb.TagNumber(2) + ShapeEntity_ShapeStyle_RGBAColor ensureStroke() => $_ensure(1); + + @$pb.TagNumber(3) + $core.double get strokeWidth => $_getN(2); + @$pb.TagNumber(3) + set strokeWidth($core.double v) { + $_setFloat(2, v); + } + + @$pb.TagNumber(3) + $core.bool hasStrokeWidth() => $_has(2); + @$pb.TagNumber(3) + void clearStrokeWidth() => clearField(3); + + @$pb.TagNumber(4) + ShapeEntity_ShapeStyle_LineCap get lineCap => $_getN(3); + @$pb.TagNumber(4) + set lineCap(ShapeEntity_ShapeStyle_LineCap v) { + setField(4, v); + } + + @$pb.TagNumber(4) + $core.bool hasLineCap() => $_has(3); + @$pb.TagNumber(4) + void clearLineCap() => clearField(4); + + @$pb.TagNumber(5) + ShapeEntity_ShapeStyle_LineJoin get lineJoin => $_getN(4); + @$pb.TagNumber(5) + set lineJoin(ShapeEntity_ShapeStyle_LineJoin v) { + setField(5, v); + } + + @$pb.TagNumber(5) + $core.bool hasLineJoin() => $_has(4); + @$pb.TagNumber(5) + void clearLineJoin() => clearField(5); + + @$pb.TagNumber(6) + $core.double get miterLimit => $_getN(5); + @$pb.TagNumber(6) + set miterLimit($core.double v) { + $_setFloat(5, v); + } + + @$pb.TagNumber(6) + $core.bool hasMiterLimit() => $_has(5); + @$pb.TagNumber(6) + void clearMiterLimit() => clearField(6); + + @$pb.TagNumber(7) + $core.double get lineDashI => $_getN(6); + @$pb.TagNumber(7) + set lineDashI($core.double v) { + $_setFloat(6, v); + } + + @$pb.TagNumber(7) + $core.bool hasLineDashI() => $_has(6); + @$pb.TagNumber(7) + void clearLineDashI() => clearField(7); + + @$pb.TagNumber(8) + $core.double get lineDashII => $_getN(7); + @$pb.TagNumber(8) + set lineDashII($core.double v) { + $_setFloat(7, v); + } + + @$pb.TagNumber(8) + $core.bool hasLineDashII() => $_has(7); + @$pb.TagNumber(8) + void clearLineDashII() => clearField(8); + + @$pb.TagNumber(9) + $core.double get lineDashIII => $_getN(8); + @$pb.TagNumber(9) + set lineDashIII($core.double v) { + $_setFloat(8, v); + } + + @$pb.TagNumber(9) + $core.bool hasLineDashIII() => $_has(8); + @$pb.TagNumber(9) + void clearLineDashIII() => clearField(9); +} + +enum ShapeEntity_Args { shape, rect, ellipse, notSet } + +class ShapeEntity extends $pb.GeneratedMessage { + static const $core.Map<$core.int, ShapeEntity_Args> _ShapeEntity_ArgsByTag = { + 2: ShapeEntity_Args.shape, + 3: ShapeEntity_Args.rect, + 4: ShapeEntity_Args.ellipse, + 0: ShapeEntity_Args.notSet, + }; + static final $pb.BuilderInfo _i = + $pb.BuilderInfo( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'ShapeEntity', + package: const $pb.PackageName( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'com.opensource.svga', + ), + createEmptyInstance: create, + ) + ..oo(0, [2, 3, 4]) + ..e( + 1, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'type', + $pb.PbFieldType.OE, + defaultOrMaker: ShapeEntity_ShapeType.SHAPE, + valueOf: ShapeEntity_ShapeType.valueOf, + enumValues: ShapeEntity_ShapeType.values, + ) + ..aOM( + 2, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'shape', + subBuilder: ShapeEntity_ShapeArgs.create, + ) + ..aOM( + 3, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'rect', + subBuilder: ShapeEntity_RectArgs.create, + ) + ..aOM( + 4, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'ellipse', + subBuilder: ShapeEntity_EllipseArgs.create, + ) + ..aOM( + 10, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'styles', + subBuilder: ShapeEntity_ShapeStyle.create, + ) + ..aOM( + 11, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'transform', + subBuilder: Transform.create, + ) + ..hasRequiredFields = false; + + ShapeEntity._() : super(); + factory ShapeEntity({ + ShapeEntity_ShapeType? type, + ShapeEntity_ShapeArgs? shape, + ShapeEntity_RectArgs? rect, + ShapeEntity_EllipseArgs? ellipse, + ShapeEntity_ShapeStyle? styles, + Transform? transform, + }) { + final _result = create(); + if (type != null) { + _result.type = type; + } + if (shape != null) { + _result.shape = shape; + } + if (rect != null) { + _result.rect = rect; + } + if (ellipse != null) { + _result.ellipse = ellipse; + } + if (styles != null) { + _result.styles = styles; + } + if (transform != null) { + _result.transform = transform; + } + return _result; + } + factory ShapeEntity.fromBuffer( + $core.List<$core.int> i, [ + $pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY, + ]) => create()..mergeFromBuffer(i, r); + factory ShapeEntity.fromJson( + $core.String i, [ + $pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY, + ]) => create()..mergeFromJson(i, r); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version', + ) + ShapeEntity clone() => ShapeEntity()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version', + ) + ShapeEntity copyWith(void Function(ShapeEntity) updates) => + super.copyWith((message) => updates(message as ShapeEntity)) + as ShapeEntity; // ignore: deprecated_member_use + $pb.BuilderInfo get info_ => _i; + @$core.pragma('dart2js:noInline') + static ShapeEntity create() => ShapeEntity._(); + ShapeEntity createEmptyInstance() => create(); + static $core.List createRepeated() => []; + @$core.pragma('dart2js:noInline') + static ShapeEntity getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static ShapeEntity? _defaultInstance; + + ShapeEntity_Args whichArgs() => _ShapeEntity_ArgsByTag[$_whichOneof(0)]!; + void clearArgs() => clearField($_whichOneof(0)); + + @$pb.TagNumber(1) + ShapeEntity_ShapeType get type => $_getN(0); + @$pb.TagNumber(1) + set type(ShapeEntity_ShapeType v) { + setField(1, v); + } + + @$pb.TagNumber(1) + $core.bool hasType() => $_has(0); + @$pb.TagNumber(1) + void clearType() => clearField(1); + + @$pb.TagNumber(2) + ShapeEntity_ShapeArgs get shape => $_getN(1); + @$pb.TagNumber(2) + set shape(ShapeEntity_ShapeArgs v) { + setField(2, v); + } + + @$pb.TagNumber(2) + $core.bool hasShape() => $_has(1); + @$pb.TagNumber(2) + void clearShape() => clearField(2); + @$pb.TagNumber(2) + ShapeEntity_ShapeArgs ensureShape() => $_ensure(1); + + @$pb.TagNumber(3) + ShapeEntity_RectArgs get rect => $_getN(2); + @$pb.TagNumber(3) + set rect(ShapeEntity_RectArgs v) { + setField(3, v); + } + + @$pb.TagNumber(3) + $core.bool hasRect() => $_has(2); + @$pb.TagNumber(3) + void clearRect() => clearField(3); + @$pb.TagNumber(3) + ShapeEntity_RectArgs ensureRect() => $_ensure(2); + + @$pb.TagNumber(4) + ShapeEntity_EllipseArgs get ellipse => $_getN(3); + @$pb.TagNumber(4) + set ellipse(ShapeEntity_EllipseArgs v) { + setField(4, v); + } + + @$pb.TagNumber(4) + $core.bool hasEllipse() => $_has(3); + @$pb.TagNumber(4) + void clearEllipse() => clearField(4); + @$pb.TagNumber(4) + ShapeEntity_EllipseArgs ensureEllipse() => $_ensure(3); + + @$pb.TagNumber(10) + ShapeEntity_ShapeStyle get styles => $_getN(4); + @$pb.TagNumber(10) + set styles(ShapeEntity_ShapeStyle v) { + setField(10, v); + } + + @$pb.TagNumber(10) + $core.bool hasStyles() => $_has(4); + @$pb.TagNumber(10) + void clearStyles() => clearField(10); + @$pb.TagNumber(10) + ShapeEntity_ShapeStyle ensureStyles() => $_ensure(4); + + @$pb.TagNumber(11) + Transform get transform => $_getN(5); + @$pb.TagNumber(11) + set transform(Transform v) { + setField(11, v); + } + + @$pb.TagNumber(11) + $core.bool hasTransform() => $_has(5); + @$pb.TagNumber(11) + void clearTransform() => clearField(11); + @$pb.TagNumber(11) + Transform ensureTransform() => $_ensure(5); +} + +class FrameEntity extends $pb.GeneratedMessage { + static final $pb.BuilderInfo _i = + $pb.BuilderInfo( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'FrameEntity', + package: const $pb.PackageName( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'com.opensource.svga', + ), + createEmptyInstance: create, + ) + ..a<$core.double>( + 1, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'alpha', + $pb.PbFieldType.OF, + ) + ..aOM( + 2, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'layout', + subBuilder: Layout.create, + ) + ..aOM( + 3, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'transform', + subBuilder: Transform.create, + ) + ..aOS( + 4, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'clipPath', + protoName: 'clipPath', + ) + ..pc( + 5, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'shapes', + $pb.PbFieldType.PM, + subBuilder: ShapeEntity.create, + ) + ..hasRequiredFields = false; + + FrameEntity._() : super(); + factory FrameEntity({ + $core.double? alpha, + Layout? layout, + Transform? transform, + $core.String? clipPath, + $core.Iterable? shapes, + }) { + final _result = create(); + if (alpha != null) { + _result.alpha = alpha; + } + if (layout != null) { + _result.layout = layout; + } + if (transform != null) { + _result.transform = transform; + } + if (clipPath != null) { + _result.clipPath = clipPath; + } + if (shapes != null) { + _result.shapes.addAll(shapes); + } + return _result; + } + factory FrameEntity.fromBuffer( + $core.List<$core.int> i, [ + $pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY, + ]) => create()..mergeFromBuffer(i, r); + factory FrameEntity.fromJson( + $core.String i, [ + $pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY, + ]) => create()..mergeFromJson(i, r); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version', + ) + FrameEntity clone() => FrameEntity()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version', + ) + FrameEntity copyWith(void Function(FrameEntity) updates) => + super.copyWith((message) => updates(message as FrameEntity)) + as FrameEntity; // ignore: deprecated_member_use + $pb.BuilderInfo get info_ => _i; + @$core.pragma('dart2js:noInline') + static FrameEntity create() => FrameEntity._(); + FrameEntity createEmptyInstance() => create(); + static $core.List createRepeated() => []; + @$core.pragma('dart2js:noInline') + static FrameEntity getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static FrameEntity? _defaultInstance; + + @$pb.TagNumber(1) + $core.double get alpha => $_getN(0); + @$pb.TagNumber(1) + set alpha($core.double v) { + $_setFloat(0, v); + } + + @$pb.TagNumber(1) + $core.bool hasAlpha() => $_has(0); + @$pb.TagNumber(1) + void clearAlpha() => clearField(1); + + @$pb.TagNumber(2) + Layout get layout => $_getN(1); + @$pb.TagNumber(2) + set layout(Layout v) { + setField(2, v); + } + + @$pb.TagNumber(2) + $core.bool hasLayout() => $_has(1); + @$pb.TagNumber(2) + void clearLayout() => clearField(2); + @$pb.TagNumber(2) + Layout ensureLayout() => $_ensure(1); + + @$pb.TagNumber(3) + Transform get transform => $_getN(2); + @$pb.TagNumber(3) + set transform(Transform v) { + setField(3, v); + } + + @$pb.TagNumber(3) + $core.bool hasTransform() => $_has(2); + @$pb.TagNumber(3) + void clearTransform() => clearField(3); + @$pb.TagNumber(3) + Transform ensureTransform() => $_ensure(2); + + @$pb.TagNumber(4) + $core.String get clipPath => $_getSZ(3); + @$pb.TagNumber(4) + set clipPath($core.String v) { + $_setString(3, v); + } + + @$pb.TagNumber(4) + $core.bool hasClipPath() => $_has(3); + @$pb.TagNumber(4) + void clearClipPath() => clearField(4); + + @$pb.TagNumber(5) + $core.List get shapes => this._shapes ?? $_getList(4); + + List? _shapes; + set shapes(List? value) => this._shapes = value; +} + +class MovieEntity extends $pb.GeneratedMessage { + static final $pb.BuilderInfo _i = + $pb.BuilderInfo( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'MovieEntity', + package: const $pb.PackageName( + const $core.bool.fromEnvironment('protobuf.omit_message_names') + ? '' + : 'com.opensource.svga', + ), + createEmptyInstance: create, + ) + ..aOS( + 1, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'version', + ) + ..aOM( + 2, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'params', + subBuilder: MovieParams.create, + ) + ..m<$core.String, $core.List<$core.int>>( + 3, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'images', + entryClassName: 'MovieEntity.ImagesEntry', + keyFieldType: $pb.PbFieldType.OS, + valueFieldType: $pb.PbFieldType.OY, + packageName: const $pb.PackageName('com.opensource.svga'), + ) + ..pc( + 4, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'sprites', + $pb.PbFieldType.PM, + subBuilder: SpriteEntity.create, + ) + ..pc( + 5, + const $core.bool.fromEnvironment('protobuf.omit_field_names') + ? '' + : 'audios', + $pb.PbFieldType.PM, + subBuilder: AudioEntity.create, + ) + ..hasRequiredFields = false; + + MovieEntity._() : super(); + factory MovieEntity({ + $core.String? version, + MovieParams? params, + $core.Map<$core.String, $core.List<$core.int>>? images, + $core.Iterable? sprites, + $core.Iterable? audios, + }) { + final _result = create(); + if (version != null) { + _result.version = version; + } + if (params != null) { + _result.params = params; + } + if (images != null) { + _result.images.addAll(images); + } + if (sprites != null) { + _result.sprites.addAll(sprites); + } + if (audios != null) { + _result.audios.addAll(audios); + } + return _result; + } + factory MovieEntity.fromBuffer( + $core.List<$core.int> i, [ + $pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY, + ]) => create()..mergeFromBuffer(i, r); + factory MovieEntity.fromJson( + $core.String i, [ + $pb.ExtensionRegistry r = $pb.ExtensionRegistry.EMPTY, + ]) => create()..mergeFromJson(i, r); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.deepCopy] instead. ' + 'Will be removed in next major version', + ) + MovieEntity clone() => MovieEntity()..mergeFromMessage(this); + @$core.Deprecated( + 'Using this can add significant overhead to your binary. ' + 'Use [GeneratedMessageGenericExtensions.rebuild] instead. ' + 'Will be removed in next major version', + ) + MovieEntity copyWith(void Function(MovieEntity) updates) => + super.copyWith((message) => updates(message as MovieEntity)) + as MovieEntity; // ignore: deprecated_member_use + $pb.BuilderInfo get info_ => _i; + @$core.pragma('dart2js:noInline') + static MovieEntity create() => MovieEntity._(); + MovieEntity createEmptyInstance() => create(); + static $core.List createRepeated() => []; + @$core.pragma('dart2js:noInline') + static MovieEntity getDefault() => _defaultInstance ??= + $pb.GeneratedMessage.$_defaultFor(create); + static MovieEntity? _defaultInstance; + + @$pb.TagNumber(1) + $core.String get version => $_getSZ(0); + @$pb.TagNumber(1) + set version($core.String v) { + $_setString(0, v); + } + + @$pb.TagNumber(1) + $core.bool hasVersion() => $_has(0); + @$pb.TagNumber(1) + void clearVersion() => clearField(1); + + @$pb.TagNumber(2) + MovieParams get params => $_getN(1); + @$pb.TagNumber(2) + set params(MovieParams v) { + setField(2, v); + } + + @$pb.TagNumber(2) + $core.bool hasParams() => $_has(1); + @$pb.TagNumber(2) + void clearParams() => clearField(2); + @$pb.TagNumber(2) + MovieParams ensureParams() => $_ensure(1); + + @$pb.TagNumber(3) + $core.Map<$core.String, $core.List<$core.int>> get images => $_getMap(2); + + @$pb.TagNumber(4) + $core.List get sprites => $_getList(3); + + @$pb.TagNumber(5) + $core.List get audios => $_getList(4); + + bool autorelease = true; + SVGADynamicEntity dynamicItem = SVGADynamicEntity(); + Map bitmapCache = {}; + Map pathCache = {}; + Map audiosData = {}; + bool _memoryReleased = false; + + /// Releases protobuf internal structures to reduce memory usage. + /// Call this after parsing is complete and all resources have been prepared. + /// After calling, the raw `images` data is cleared (already processed into + /// `bitmapCache` and `audiosData`). + void releaseMemory() { + if (_memoryReleased) return; + _memoryReleased = true; + // Clear the raw images map - data already extracted to bitmapCache/audiosData + images.clear(); + } + + /// Returns true if memory has been released. + bool get isMemoryReleased => _memoryReleased; + + void dispose() { + bitmapCache.values.forEach((element) { + element.dispose(); + }); + bitmapCache.clear(); + pathCache.clear(); + audiosData.clear(); + } +} diff --git a/local_packages/flutter_svga-0.0.13-patched/lib/src/proto/svga.pbenum.dart b/local_packages/flutter_svga-0.0.13-patched/lib/src/proto/svga.pbenum.dart new file mode 100644 index 0000000..2d35e7f --- /dev/null +++ b/local_packages/flutter_svga-0.0.13-patched/lib/src/proto/svga.pbenum.dart @@ -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 values = + [ + 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 values = + [ + 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 values = + [ + 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); +} diff --git a/local_packages/flutter_svga-0.0.13-patched/lib/src/proto/svga.pbjson.dart b/local_packages/flutter_svga-0.0.13-patched/lib/src/proto/svga.pbjson.dart new file mode 100644 index 0000000..fcdf40a --- /dev/null +++ b/local_packages/flutter_svga-0.0.13-patched/lib/src/proto/svga.pbjson.dart @@ -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'); diff --git a/local_packages/flutter_svga-0.0.13-patched/lib/src/proto/svga.pbserver.dart b/local_packages/flutter_svga-0.0.13-patched/lib/src/proto/svga.pbserver.dart new file mode 100644 index 0000000..8f6edcc --- /dev/null +++ b/local_packages/flutter_svga-0.0.13-patched/lib/src/proto/svga.pbserver.dart @@ -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'; diff --git a/local_packages/flutter_svga-0.0.13-patched/pubspec.yaml b/local_packages/flutter_svga-0.0.13-patched/pubspec.yaml new file mode 100644 index 0000000..afd01a9 --- /dev/null +++ b/local_packages/flutter_svga-0.0.13-patched/pubspec.yaml @@ -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 + diff --git a/local_packages/flutter_svga-0.0.13-patched/test/flutter_svga_test.dart b/local_packages/flutter_svga-0.0.13-patched/test/flutter_svga_test.dart new file mode 100644 index 0000000..c26323d --- /dev/null +++ b/local_packages/flutter_svga-0.0.13-patched/test/flutter_svga_test.dart @@ -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); + }); + }); +} diff --git a/pubspec.lock b/pubspec.lock index 9e5e689..97547cc 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -74,7 +74,7 @@ packages: source: hosted version: "2.13.1" audioplayers: - dependency: transitive + dependency: "direct main" description: name: audioplayers sha256: a72dd459d1a48f61a6fb9c0134dba26597c9236af40639ff0eb70eb4e0baab70 @@ -638,10 +638,9 @@ packages: flutter_svga: dependency: "direct main" description: - name: flutter_svga - sha256: c914ba2aee5e2d53775b64c7ff6530b4cdc3c27fd9348debb0d86fd68b869c39 - url: "https://pub.flutter-io.cn" - source: hosted + path: "local_packages/flutter_svga-0.0.13-patched" + relative: true + source: path version: "0.0.13" flutter_test: dependency: "direct dev" diff --git a/pubspec.yaml b/pubspec.yaml index 5ee95fe..82bfcab 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -93,11 +93,12 @@ dependencies: #vap特效 # flutter_vap_plus: ^1.2.10 # tancent_vap: ^1.0.0+1 - tancent_vap: - path: ./local_packages/tancent_vap-1.0.0+1 - #svga特效 - flutter_svga: ^0.0.5 - #banner + tancent_vap: + path: ./local_packages/tancent_vap-1.0.0+1 + #svga特效 + flutter_svga: + path: ./local_packages/flutter_svga-0.0.13-patched + #banner carousel_slider: ^5.1.1 #谷歌支付 in_app_purchase: ^3.2.3 @@ -114,9 +115,10 @@ dependencies: # flutter_sound: 9.28.0 #图片压缩 flutter_image_compress: ^2.4.0 - extended_text: ^15.0.2 - #缩略图 - video_thumbnail: ^0.5.6 + extended_text: ^15.0.2 + audioplayers: ^6.5.1 + #缩略图 + video_thumbnail: ^0.5.6 badges: ^3.1.2 #本地路径 path_provider: ^2.1.4 @@ -164,6 +166,7 @@ flutter: - sc_images/daily_sign_in/ - sc_images/register_reward/ - sc_images/room/ + - sc_images/room/background_examples/ - sc_images/room/emoji/ - sc_images/room/entrance/ - sc_images/room/anim/ diff --git a/sc_images/person/sc_icon_profile_card_default_bg.png b/sc_images/person/sc_icon_profile_card_default_bg.png new file mode 100644 index 0000000..8519c61 Binary files /dev/null and b/sc_images/person/sc_icon_profile_card_default_bg.png differ diff --git a/sc_images/person/sc_icon_profile_chat.png b/sc_images/person/sc_icon_profile_chat.png new file mode 100644 index 0000000..01d72b7 Binary files /dev/null and b/sc_images/person/sc_icon_profile_chat.png differ diff --git a/sc_images/person/sc_icon_profile_edit.png b/sc_images/person/sc_icon_profile_edit.png new file mode 100644 index 0000000..70dcee6 Binary files /dev/null and b/sc_images/person/sc_icon_profile_edit.png differ diff --git a/sc_images/room/anim/luck_gift/lucky_gift_win.mp3 b/sc_images/room/anim/luck_gift/lucky_gift_win.mp3 new file mode 100644 index 0000000..974ce34 Binary files /dev/null and b/sc_images/room/anim/luck_gift/lucky_gift_win.mp3 differ diff --git a/sc_images/room/background_examples/bg_example_1.png b/sc_images/room/background_examples/bg_example_1.png new file mode 100644 index 0000000..b1d26a8 Binary files /dev/null and b/sc_images/room/background_examples/bg_example_1.png differ diff --git a/sc_images/room/background_examples/bg_example_2.png b/sc_images/room/background_examples/bg_example_2.png new file mode 100644 index 0000000..f38e1c7 Binary files /dev/null and b/sc_images/room/background_examples/bg_example_2.png differ diff --git a/sc_images/room/background_examples/bg_example_3.png b/sc_images/room/background_examples/bg_example_3.png new file mode 100644 index 0000000..6bb1f8e Binary files /dev/null and b/sc_images/room/background_examples/bg_example_3.png differ diff --git a/sc_images/room/background_examples/bg_example_4.png b/sc_images/room/background_examples/bg_example_4.png new file mode 100644 index 0000000..5f2ec1a Binary files /dev/null and b/sc_images/room/background_examples/bg_example_4.png differ diff --git a/sc_images/room/background_examples/bg_example_5.png b/sc_images/room/background_examples/bg_example_5.png new file mode 100644 index 0000000..977f3ff Binary files /dev/null and b/sc_images/room/background_examples/bg_example_5.png differ diff --git a/sc_images/room/background_examples/bg_example_6.png b/sc_images/room/background_examples/bg_example_6.png new file mode 100644 index 0000000..d5d6aca Binary files /dev/null and b/sc_images/room/background_examples/bg_example_6.png differ diff --git a/sc_images/room/background_examples/bg_example_7.png b/sc_images/room/background_examples/bg_example_7.png new file mode 100644 index 0000000..d8395cb Binary files /dev/null and b/sc_images/room/background_examples/bg_example_7.png differ diff --git a/sc_images/room/background_examples/bg_example_8.png b/sc_images/room/background_examples/bg_example_8.png new file mode 100644 index 0000000..e47264f Binary files /dev/null and b/sc_images/room/background_examples/bg_example_8.png differ diff --git a/需求进度.md b/需求进度.md index 984cef6..b2225ce 100644 --- a/需求进度.md +++ b/需求进度.md @@ -4,6 +4,13 @@ - 控制当前 Flutter Android 发包体积,持续定位冗余组件、超大资源和不合理构建配置,并把每一步处理结果落盘记录。 - 按桌面《Android音乐添加流程.md》在 Android 语言房内补齐本地 MP3 音乐功能,并持续记录素材接入、扫描添加、列表管理、播放器和房间播放链路进度。 +## 本轮个人资料页改版(分步重做) +- 已按 2026-05-06 最新反馈回到旧版个人资料页结构上分步处理,不再一次性重做整页。 +- 第一步已去掉个人资料页加载阶段的骨架屏:`ref.userProfile` 或拉黑状态还没返回时,正文区域改为空占位,不再展示灰色骨架块;资料返回后仍按原来的 `Header + About/Giftwall Tab` 结构渲染。 +- 第二步已将个人资料页上方默认背景图从旧的 `sc_images/person/sc_icon_my_head_bg_defalt.png` 切换为新素材 `sc_images/person/sc_icon_profile_card_default_bg.png`;用户已有可用 `backgroundPhotos` 时仍优先展示用户自定义背景。 +- 第三步已调整头部资料区对齐:深绿资料区去掉顶部圆角改为矩形;头像/头像框移动到中间;用户名居中单独一行;ID、复制按钮、性别年龄和国旗收成同一行居中展示,复制按钮点击后仍写入剪贴板并提示 copied。 +- 验证进度:`dart format lib/modules/user/profile/person_detail_page.dart` 已执行;`flutter analyze --no-fatal-infos lib/modules/user/profile/person_detail_page.dart` 无 error/warning,仅保留该旧文件原有的 info 级代码风格提示。下一步再继续逐项对齐资料页 UI。 + ## 本轮 Android 语言房音乐功能(进行中) - 已查阅桌面《Android音乐添加流程.md》,并产出可执行方案文档 `docs/android-room-music-execution-plan.md`,覆盖功能范围、模块拆分、Android 权限、声网混音链路、UI 流程、风险点和 GPT 5.5 预估开发工时。 - 已导入桌面 `音乐素材` 文件夹内的 12 张音乐相关 PNG,并统一落到 `sc_images/room/sc_music_material_*`,当前用于房间菜单入口、播放器按钮、模式切换、音量、文件夹、更多、删除和编辑等 UI 位。