diff --git a/lib/shared/business_logic/models/res/sc_room_rocket_api_res.dart b/lib/shared/business_logic/models/res/sc_room_rocket_api_res.dart index 32f0ffb..f38200c 100644 --- a/lib/shared/business_logic/models/res/sc_room_rocket_api_res.dart +++ b/lib/shared/business_logic/models/res/sc_room_rocket_api_res.dart @@ -10,6 +10,7 @@ class SCRoomRocketLevelRes { required this.shakeThresholdPercent, required this.enabled, required this.sort, + this.rewards = const [], this.rewardPreview, this.rocketKings = const [], }); @@ -194,6 +195,7 @@ class SCRoomRocketLevelRes { 95, enabled: _boolValue(data['enabled'], fallback: true), sort: _intValue(data['sort']), + rewards: SCRoomRocketRewardPreviewRes._rewardList(data['rewards']), rewardPreview: SCRoomRocketRewardPreviewRes.tryFromJson( _firstPayload([ data['rewardPreview'], @@ -241,6 +243,7 @@ class SCRoomRocketLevelRes { final num shakeThresholdPercent; final bool enabled; final int sort; + final List rewards; final SCRoomRocketRewardPreviewRes? rewardPreview; final List rocketKings; @@ -256,6 +259,7 @@ class SCRoomRocketLevelRes { 'shakeThresholdPercent': shakeThresholdPercent, 'enabled': enabled, 'sort': sort, + 'rewards': rewards.map((item) => item.toJson()).toList(), 'rewardPreview': rewardPreview == null ? null @@ -270,6 +274,71 @@ class SCRoomRocketLevelRes { 'rocketKings': rocketKings.map((item) => item.toJson()).toList(), }; } + + static List listFromJson(dynamic value) { + if (value is! Iterable || value is String) { + return const []; + } + return value.map(SCRoomRocketLevelRes.fromJson).toList(growable: false); + } +} + +class SCRoomRocketLevelContentsRes { + SCRoomRocketLevelContentsRes({ + required this.configured, + required this.sysOrigin, + required this.enabled, + required this.timezone, + required this.maxLevel, + required this.levels, + required this.updateTime, + }); + + factory SCRoomRocketLevelContentsRes.fromJson(dynamic json) { + final data = _asMap(json); + return SCRoomRocketLevelContentsRes( + configured: _boolValue(data['configured']), + sysOrigin: _stringValue(data['sysOrigin']), + enabled: _boolValue(data['enabled']), + timezone: _stringValue(data['timezone'] ?? data['timeZone']), + maxLevel: _intValue(data['maxLevel']), + levels: SCRoomRocketLevelRes.listFromJson(data['levels']), + updateTime: _stringValue(data['updateTime']), + ); + } + + final bool configured; + final String sysOrigin; + final bool enabled; + final String timezone; + final int maxLevel; + final List levels; + final String updateTime; + + SCRoomRocketLevelRes? levelFor(int level) { + for (final item in levels) { + if (item.level == level) { + return item; + } + } + return null; + } + + SCRoomRocketRewardPreviewRes? rewardPreviewForLevel(int level) { + return levelFor(level)?.rewardPreview; + } + + Map toJson() { + return { + 'configured': configured, + 'sysOrigin': sysOrigin, + 'enabled': enabled, + 'timezone': timezone, + 'maxLevel': maxLevel, + 'levels': levels.map((item) => item.toJson()).toList(), + 'updateTime': updateTime, + }; + } } class SCRoomRocketRewardRes { diff --git a/lib/shared/business_logic/repositories/room_repository.dart b/lib/shared/business_logic/repositories/room_repository.dart index 70021ce..101eb22 100644 --- a/lib/shared/business_logic/repositories/room_repository.dart +++ b/lib/shared/business_logic/repositories/room_repository.dart @@ -202,7 +202,10 @@ abstract class SocialChatRoomRepository { }); ///查询房间火箭状态 - Future rocketStatus(String roomId, {int? level}); + Future rocketStatus(String roomId, {int? level}); + + ///查询火箭等级内容 + Future roomRocketLevelContents(); ///查询火箭王榜单 Future roomRocketKing( 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 782880d..6ba11a7 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 @@ -1027,6 +1027,23 @@ class SCChatRoomRepository implements SocialChatRoomRepository { return result; } + ///go/app/voice-room/rocket/level-contents + @override + Future roomRocketLevelContents() async { + const path = "/go/app/voice-room/rocket/level-contents"; + _rocketApiDebug('request GET $path'); + final result = await http.get( + path, + extra: {BaseNetworkClient.silentErrorToastKey: true}, + fromJson: (json) { + _rocketApiDebug('GET $path', raw: json); + return SCRoomRocketLevelContentsRes.fromJson(json); + }, + ); + _rocketApiDebug('parsed $path', result: result); + return result; + } + ///go/app/voice-room/rocket/king @override Future roomRocketKing( diff --git a/lib/ui_kit/widgets/room/rocket/room_rocket_api_mapper.dart b/lib/ui_kit/widgets/room/rocket/room_rocket_api_mapper.dart index 6840f12..3e6710d 100644 --- a/lib/ui_kit/widgets/room/rocket/room_rocket_api_mapper.dart +++ b/lib/ui_kit/widgets/room/rocket/room_rocket_api_mapper.dart @@ -13,7 +13,18 @@ class RoomRocketApiMapper { static List? levelsFromStatus( SCRoomRocketStatusRes? status, ) { - final levels = status?.levels; + return levelsFromApiLevels(status?.levels); + } + + static List? levelsFromLevelContents( + SCRoomRocketLevelContentsRes? contents, + ) { + return levelsFromApiLevels(contents?.levels); + } + + static List? levelsFromApiLevels( + List? levels, + ) { if (levels == null || levels.isEmpty) { return null; } @@ -65,6 +76,26 @@ class RoomRocketApiMapper { return rewardGroupsFromPreview(preview); } + static RoomRocketRewardGroups? rewardGroupsFromLevelContents( + SCRoomRocketLevelContentsRes? contents, { + required int level, + }) { + return rewardGroupsFromPreview(contents?.rewardPreviewForLevel(level)); + } + + static Map rewardGroupsByLevelFromLevelContents( + SCRoomRocketLevelContentsRes? contents, + ) { + final result = {}; + for (final item in contents?.levels ?? const []) { + final groups = rewardGroupsFromPreview(item.rewardPreview); + if (groups != null) { + result[item.level] = groups; + } + } + return result; + } + static RoomRocketRewardGroups? rewardGroupsFromPreview( SCRoomRocketRewardPreviewRes? preview, ) { diff --git a/lib/ui_kit/widgets/room/rocket/room_rocket_floating_entry.dart b/lib/ui_kit/widgets/room/rocket/room_rocket_floating_entry.dart index 07db494..721a850 100644 --- a/lib/ui_kit/widgets/room/rocket/room_rocket_floating_entry.dart +++ b/lib/ui_kit/widgets/room/rocket/room_rocket_floating_entry.dart @@ -350,6 +350,7 @@ class _RoomRocketDialogLoader extends StatefulWidget { class _RoomRocketDialogLoaderState extends State<_RoomRocketDialogLoader> { SCRoomRocketStatusRes? _status; + SCRoomRocketLevelContentsRes? _levelContents; List? _noteSections; Map _configRewardGroupsByLevel = {}; final Map _statusRewardGroupsByLevel = {}; @@ -421,7 +422,31 @@ class _RoomRocketDialogLoaderState extends State<_RoomRocketDialogLoader> { Future _loadConfigs() async { try { - _rocketUiDebug('request enabled rocket configs'); + _rocketUiDebug('request rocket level contents'); + final contents = await SCChatRoomRepository().roomRocketLevelContents(); + if (!mounted) { + return; + } + final groups = RoomRocketApiMapper.rewardGroupsByLevelFromLevelContents( + contents, + ); + setState(() { + _levelContents = contents; + _configRewardGroupsByLevel = groups; + }); + _rocketUiDebug( + 'rocket level contents loaded levels=${contents.levels.length} ' + 'rewardLevels=${groups.keys.toList()}', + ); + } catch (error) { + _rocketUiDebug('rocket level contents request failed: $error'); + unawaited(_loadLegacyConfigs()); + } + } + + Future _loadLegacyConfigs() async { + try { + _rocketUiDebug('request legacy enabled rocket configs'); final configs = await SCChatRoomRepository().rocketConfigEnabled(); if (!mounted) { return; @@ -433,10 +458,10 @@ class _RoomRocketDialogLoaderState extends State<_RoomRocketDialogLoader> { _configRewardGroupsByLevel = groups; }); _rocketUiDebug( - 'rocket configs loaded rewardLevels=${groups.keys.toList()}', + 'legacy rocket configs loaded rewardLevels=${groups.keys.toList()}', ); } catch (error) { - _rocketUiDebug('rocket configs request failed: $error'); + _rocketUiDebug('legacy rocket configs request failed: $error'); } } @@ -456,10 +481,15 @@ class _RoomRocketDialogLoaderState extends State<_RoomRocketDialogLoader> { final rewardGroups = selectedLevelNumber == currentLevelNumber ? _mergeRewardGroups(rewardGroupsFromStatus, configRewardGroups) - : rewardGroupsFromStatus ?? const RoomRocketRewardGroups(); + : rewardGroupsFromStatus ?? + configRewardGroups ?? + const RoomRocketRewardGroups(); + final levelItems = + RoomRocketApiMapper.levelsFromLevelContents(_levelContents) ?? + RoomRocketApiMapper.levelsFromStatus(status); return RoomRocketDialog( status: status, - levels: RoomRocketApiMapper.levelsFromStatus(status), + levels: levelItems, selectedLevel: selectedLevel, onLevelSelected: _onLevelSelected, rewards: @@ -555,6 +585,7 @@ class _RoomRocketDialogLoaderState extends State<_RoomRocketDialogLoader> { Future _loadRewardForLevel(int level) async { final normalizedLevel = level.clamp(1, 99).toInt(); if (_statusRewardGroupsByLevel.containsKey(normalizedLevel) || + _configRewardGroupsByLevel.containsKey(normalizedLevel) || _loadingRewardLevels.contains(normalizedLevel)) { return; } diff --git a/lib/ui_kit/widgets/room/rocket/room_rocket_pag_effect_overlay.dart b/lib/ui_kit/widgets/room/rocket/room_rocket_pag_effect_overlay.dart index 7eb4d11..2bd5610 100644 --- a/lib/ui_kit/widgets/room/rocket/room_rocket_pag_effect_overlay.dart +++ b/lib/ui_kit/widgets/room/rocket/room_rocket_pag_effect_overlay.dart @@ -493,10 +493,7 @@ class _RoomRocketPagViewState extends State<_RoomRocketPagView> { void _scheduleMemoryRelease(String path) { _memoryReleaseTimer?.cancel(); - if (widget.memoryTtl <= Duration.zero) { - return; - } - _memoryReleaseTimer = Timer(widget.memoryTtl, () { + _memoryReleaseTimer = _schedulePagMemoryReleaseTimer(widget.memoryTtl, () { if (!mounted || widget.resource.trim() != path || _pagBytes == null) { return; } @@ -572,6 +569,24 @@ Duration debugRoomRocketPagMemoryTtl() { return _pagMemoryTtl; } +@visibleForTesting +Timer? debugScheduleRoomRocketPagMemoryRelease({ + Duration memoryTtl = _pagMemoryTtl, + required VoidCallback onRelease, +}) { + return _schedulePagMemoryReleaseTimer(memoryTtl, onRelease); +} + +Timer? _schedulePagMemoryReleaseTimer( + Duration memoryTtl, + VoidCallback onRelease, +) { + if (memoryTtl <= Duration.zero) { + return null; + } + return Timer(memoryTtl, onRelease); +} + @visibleForTesting String debugNormalizeRoomRocketPagResource(String resource) { return _normalizePagResource(resource); diff --git a/test/room_rocket_pag_resource_test.dart b/test/room_rocket_pag_resource_test.dart index 87d5733..588ee74 100644 --- a/test/room_rocket_pag_resource_test.dart +++ b/test/room_rocket_pag_resource_test.dart @@ -45,4 +45,17 @@ void main() { test('keeps pag bytes in memory for five minutes by default', () { expect(debugRoomRocketPagMemoryTtl(), const Duration(minutes: 5)); }); + + test('releases pag bytes when the configured ttl expires', () async { + var released = false; + final timer = debugScheduleRoomRocketPagMemoryRelease( + memoryTtl: const Duration(milliseconds: 10), + onRelease: () => released = true, + ); + + expect(released, isFalse); + await Future.delayed(const Duration(milliseconds: 30)); + expect(released, isTrue); + timer?.cancel(); + }); } diff --git a/test/room_rocket_status_test.dart b/test/room_rocket_status_test.dart index ed212af..9f9414b 100644 --- a/test/room_rocket_status_test.dart +++ b/test/room_rocket_status_test.dart @@ -1,4 +1,5 @@ import 'package:flutter_test/flutter_test.dart'; +import 'package:yumi/shared/business_logic/models/res/sc_room_rocket_api_res.dart'; import 'package:yumi/shared/business_logic/models/res/sc_room_rocket_config_res.dart'; import 'package:yumi/shared/business_logic/models/res/sc_room_rocket_status_res.dart'; import 'package:yumi/ui_kit/widgets/room/rocket/room_rocket_api_mapper.dart'; @@ -137,4 +138,74 @@ void main() { expect(groups[2]?.inRoom.single.imageUrl, 'cover-2'); expect(groups[2]?.inRoom.single.expireDays, 7); }); + + test('parses rocket level contents response from go api', () { + final contents = SCRoomRocketLevelContentsRes.fromJson({ + 'configured': true, + 'sysOrigin': 'LIKEI', + 'enabled': true, + 'timezone': 'Asia/Riyadh', + 'maxLevel': 6, + 'levels': [ + { + 'level': 1, + 'needEnergy': 100, + 'rocketIconUrl': 'icon-1', + 'previewRocketUrl': 'preview-1', + 'rocketAnimationUrl': 'animation-1', + 'rewardPreview': { + 'top1': [ + { + 'rewardScene': 'TOP1', + 'rewardType': 'GOLD', + 'rewardAmount': 100, + 'rewardName': 'Lv1 top1', + }, + ], + }, + }, + { + 'level': 2, + 'needEnergy': 200, + 'rocketIconUrl': 'icon-2', + 'previewRocketUrl': 'preview-2', + 'rocketAnimationUrl': 'animation-2', + 'rewards': [ + { + 'rewardScene': 'IGNITE', + 'rewardType': 'GOLD', + 'rewardAmount': 200, + 'rewardName': 'Lv2 ignite', + }, + { + 'rewardScene': 'IN_ROOM', + 'rewardType': 'NONE', + 'rewardName': '轮空', + }, + ], + }, + ], + }); + + expect(contents.enabled, isTrue); + expect(contents.levels, hasLength(2)); + expect(contents.levelFor(1)?.rocketIconUrl, 'icon-1'); + expect(contents.rewardPreviewForLevel(1)?.top1.single.rewardAmount, 100); + expect(contents.rewardPreviewForLevel(2)?.ignite.single.rewardAmount, 200); + expect(contents.levelFor(2)?.rewards, hasLength(2)); + + final levels = RoomRocketApiMapper.levelsFromLevelContents(contents); + final allGroups = RoomRocketApiMapper.rewardGroupsByLevelFromLevelContents( + contents, + ); + final groups = RoomRocketApiMapper.rewardGroupsFromLevelContents( + contents, + level: 2, + ); + + expect(levels?.first.imageUrl, 'icon-1'); + expect(allGroups[1]?.top1.single.amount, '100'); + expect(allGroups[2]?.setOff.single.amount, '200'); + expect(groups?.setOff.single.amount, '200'); + }); }