diff --git a/lib/modules/user/profile/person_detail_page.dart b/lib/modules/user/profile/person_detail_page.dart index 286bb38..1a4bf7e 100644 --- a/lib/modules/user/profile/person_detail_page.dart +++ b/lib/modules/user/profile/person_detail_page.dart @@ -587,7 +587,11 @@ class _PersonDetailPageState extends State { return Stack( fit: StackFit.expand, children: [ - SCDataCardResourceView(resource: dataCard, fit: BoxFit.cover), + SCDataCardResourceView( + resource: dataCard, + fit: BoxFit.cover, + fallback: DecoratedBox(decoration: _defaultDataCardDecoration()), + ), DecoratedBox( decoration: BoxDecoration( gradient: LinearGradient( @@ -606,6 +610,21 @@ class _PersonDetailPageState extends State { ); } + BoxDecoration _defaultDataCardDecoration() { + return const BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + Color(0xff18F2B1), + Color(0xffE4FFF6), + Color(0xffF7FFFC), + Color(0xffFFFFFF), + ], + ), + ); + } + PropsResources? _activeDataCard(SocialChatUserProfile? profile) { final now = DateTime.now().millisecondsSinceEpoch; for (final item in profile?.useProps ?? const []) { diff --git a/lib/modules/user/vip/vip_detail_page.dart b/lib/modules/user/vip/vip_detail_page.dart index 6e3ddaa..1ab6276 100644 --- a/lib/modules/user/vip/vip_detail_page.dart +++ b/lib/modules/user/vip/vip_detail_page.dart @@ -13,6 +13,7 @@ import 'package:yumi/shared/tools/sc_loading_manager.dart'; import 'package:yumi/ui_kit/components/sc_compontent.dart'; import 'package:yumi/ui_kit/components/sc_debounce_widget.dart'; import 'package:yumi/ui_kit/components/sc_tts.dart'; +import 'package:yumi/ui_kit/widgets/props/sc_data_card_resource_view.dart'; class VipDetailPage extends StatefulWidget { const VipDetailPage({super.key}); @@ -58,7 +59,6 @@ class _VipDetailPageState extends State { final status = results[1] as SCVipStatusRes; final resolvedStatus = _resolveStatus(status, home.state); final selectedLevel = _resolveInitialLevel(home, resolvedStatus); - _logLoadedVipData(home, resolvedStatus, selectedLevel); if (!mounted) return; setState(() { @@ -141,11 +141,6 @@ class _VipDetailPageState extends State { _selectedLevel = level; _preview = null; }); - _logSelectedLevelResources( - level, - config: _levelConfigFor(level), - status: _status, - ); _loadPreviewForLevel(level); } @@ -201,6 +196,7 @@ class _VipDetailPageState extends State { await _loadVipData(showLoading: false); } } catch (error) { + debugPrint('VIP purchase failed: $error'); } finally { SCLoadingManager.hide(); if (mounted) { @@ -214,56 +210,6 @@ class _VipDetailPageState extends State { return 'vip-buy-$userId-${DateTime.now().millisecondsSinceEpoch}'; } - void _logLoadedVipData( - SCVipHomeRes home, - SCVipStatusRes? status, - int selectedLevel, - ) { - if (home.configured != true) {} - if (home.levels.isEmpty) {} - _logSelectedLevelResources( - selectedLevel, - config: _levelConfigFromHome(home, selectedLevel), - status: status, - ); - } - - void _logSelectedLevelResources( - int level, { - required SCVipLevelConfigRes? config, - required SCVipStatusRes? status, - }) { - final state = status?.levelInt == level ? status : null; - if (config == null && state == null) { - return; - } - final badge = _preferResource(config?.badge, state?.badge); - final avatarFrame = _preferResource( - config?.avatarFrame, - state?.avatarFrame, - ); - final entryEffect = _preferResource( - config?.entryEffect, - state?.entryEffect, - ); - final chatBubble = _preferResource(config?.chatBubble, state?.chatBubble); - final floatPicture = _preferResource( - config?.floatPicture, - state?.floatPicture, - ); - } - - String _resourceLogValue(SCVipResourceRes? resource) { - if (!_hasResource(resource)) { - return 'null'; - } - return '{id:${resource?.resourceId},name:${resource?.name},' - 'hasPreviewUrl:${resource?.hasPreviewUrl},' - 'hasSourceUrl:${resource?.hasSourceUrl},' - 'previewUrl:${resource?.previewUrl},' - 'sourceUrl:${resource?.sourceResourceUrl}}'; - } - @override Widget build(BuildContext context) { return Scaffold( @@ -524,6 +470,8 @@ class _VipDetailPageState extends State { Widget _buildLevelCard() { final assetLevel = _assetLevel; + final backgroundCard = _selectedBackgroundCard; + final effectImage = _selectedEffectImage ?? _selectedLongBadge; return Center( child: SizedBox( width: 351.w, @@ -532,18 +480,23 @@ class _VipDetailPageState extends State { clipBehavior: Clip.none, children: [ Positioned.fill( - child: Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.circular(8.w), - border: Border.all( - color: const Color(0xFFEBCB76), - width: 1.w, - ), - image: DecorationImage( - image: AssetImage( - 'sc_images/vip/sc_vip_card_bg_$assetLevel.png', + child: ClipRRect( + borderRadius: BorderRadius.circular(8.w), + child: _buildVipCardBackground( + resource: backgroundCard, + assetLevel: assetLevel, + ), + ), + ), + Positioned.fill( + child: IgnorePointer( + child: Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(8.w), + border: Border.all( + color: const Color(0xFFEBCB76), + width: 1.w, ), - fit: BoxFit.fill, ), ), ), @@ -575,8 +528,8 @@ class _VipDetailPageState extends State { PositionedDirectional( end: 24.w, top: -36.w, - child: _buildResourceImage( - resource: _selectedBadge, + child: _buildDynamicResourceVisual( + resource: effectImage, width: 135.w, height: 135.w, fallbackIcon: Icons.workspace_premium_outlined, @@ -588,6 +541,27 @@ class _VipDetailPageState extends State { ); } + Widget _buildVipCardBackground({ + required SCVipResourceRes? resource, + required int assetLevel, + }) { + final fallback = Image.asset( + 'sc_images/vip/sc_vip_card_bg_$assetLevel.png', + fit: BoxFit.fill, + width: double.infinity, + height: double.infinity, + ); + if (!_hasResourceUrl(resource)) { + return fallback; + } + return SCDataCardResourceView( + sourceUrl: resource?.sourceResourceUrl, + coverUrl: resource?.previewUrl, + fit: BoxFit.fill, + fallback: fallback, + ); + } + Widget _buildLevelDivider() { final ornamentLayout = _ornamentLayoutForLevel(_assetLevel); return SizedBox( @@ -713,9 +687,14 @@ class _VipDetailPageState extends State { ); } - SCVipResourceRes? get _selectedBadge => _selectedResource( - fromConfig: (config) => config.badge, - fromState: (status) => status.badge, + SCVipResourceRes? get _selectedLongBadge => _selectedResource( + fromConfig: (config) => config.longBadge, + fromState: (status) => status.longBadge, + ); + + SCVipResourceRes? get _selectedShortBadge => _selectedResource( + fromConfig: (config) => config.shortBadge, + fromState: (status) => status.shortBadge, ); SCVipResourceRes? get _selectedAvatarFrame => _selectedResource( @@ -738,6 +717,16 @@ class _VipDetailPageState extends State { fromState: (status) => status.floatPicture, ); + SCVipResourceRes? get _selectedBackgroundCard => _selectedResource( + fromConfig: (config) => config.backgroundCard, + fromState: (status) => status.backgroundCard, + ); + + SCVipResourceRes? get _selectedEffectImage => _selectedResource( + fromConfig: (config) => config.effectImage, + fromState: (status) => status.effectImage, + ); + SCVipResourceRes? _selectedResource({ required SCVipResourceRes? Function(SCVipLevelConfigRes config) fromConfig, required SCVipResourceRes? Function(SCVipStatusRes status) fromState, @@ -768,8 +757,14 @@ class _VipDetailPageState extends State { final items = <_VipFeatureData>[]; _addResourceItem( items, - resource: _selectedBadge, - fallbackTitle: 'VIP Badge', + resource: _selectedLongBadge, + fallbackTitle: 'Long Badge', + fallbackIcon: Icons.workspace_premium_outlined, + ); + _addResourceItem( + items, + resource: _selectedShortBadge, + fallbackTitle: 'Short Badge', fallbackIcon: Icons.workspace_premium_outlined, ); _addResourceItem( @@ -797,12 +792,19 @@ class _VipDetailPageState extends State { fallbackIcon: Icons.airline_seat_recline_extra, wide: true, ); + _addResourceItem( + items, + resource: _selectedBackgroundCard, + fallbackTitle: 'Profile Card', + fallbackIcon: Icons.badge_outlined, + wide: true, + ); return items; } List<_VipFeatureData> get _vipPrivilegeItems { // The VIP home API currently returns resource perks only: - // badge/avatarFrame/entryEffect/chatBubble/floatPicture. + // badges/avatarFrame/entryEffect/chatBubble/floatPicture/backgroundCard. // Do not render demo-only functional privileges without backend data. return const <_VipFeatureData>[]; } @@ -844,6 +846,40 @@ class _VipDetailPageState extends State { return fallbackTitle; } + Widget _buildDynamicResourceVisual({ + required SCVipResourceRes? resource, + required double width, + required double height, + required IconData fallbackIcon, + }) { + final sourceUrl = resource?.sourceResourceUrl?.trim(); + final previewUrl = resource?.previewUrl?.trim(); + final hasSource = sourceUrl != null && sourceUrl.isNotEmpty; + final hasPreview = previewUrl != null && previewUrl.isNotEmpty; + if (hasSource || hasPreview) { + return SCDataCardResourceView( + sourceUrl: sourceUrl, + coverUrl: previewUrl, + width: width, + height: height, + fit: BoxFit.contain, + fallback: _buildResourceImage( + resource: resource, + width: width, + height: height, + fallbackIcon: fallbackIcon, + ), + ); + } + + return _buildResourceImage( + resource: resource, + width: width, + height: height, + fallbackIcon: fallbackIcon, + ); + } + Widget _buildResourceImage({ required SCVipResourceRes? resource, required double width, diff --git a/lib/shared/business_logic/models/res/sc_vip_res.dart b/lib/shared/business_logic/models/res/sc_vip_res.dart index ad941a9..56d945a 100644 --- a/lib/shared/business_logic/models/res/sc_vip_res.dart +++ b/lib/shared/business_logic/models/res/sc_vip_res.dart @@ -110,10 +110,14 @@ class SCVipStatusRes { String? startAt, String? expireAt, SCVipResourceRes? badge, + SCVipResourceRes? longBadge, + SCVipResourceRes? shortBadge, SCVipResourceRes? avatarFrame, SCVipResourceRes? entryEffect, SCVipResourceRes? chatBubble, SCVipResourceRes? floatPicture, + SCVipResourceRes? backgroundCard, + SCVipResourceRes? effectImage, }) { _userId = userId; _sysOrigin = sysOrigin; @@ -125,10 +129,14 @@ class SCVipStatusRes { _startAt = startAt; _expireAt = expireAt; _badge = badge; + _longBadge = longBadge; + _shortBadge = shortBadge; _avatarFrame = avatarFrame; _entryEffect = entryEffect; _chatBubble = chatBubble; _floatPicture = floatPicture; + _backgroundCard = backgroundCard; + _effectImage = effectImage; } SCVipStatusRes.fromJson(dynamic json) { @@ -144,10 +152,14 @@ class SCVipStatusRes { _startAt = _stringValue(map['startAt']); _expireAt = _stringValue(map['expireAt']); _badge = _resourceValue(map['badge']); + _longBadge = _resourceValue(map['longBadge']) ?? _badge; + _shortBadge = _resourceValue(map['shortBadge']); _avatarFrame = _resourceValue(map['avatarFrame']); _entryEffect = _resourceValue(map['entryEffect']); _chatBubble = _resourceValue(map['chatBubble']); _floatPicture = _resourceValue(map['floatPicture']); + _backgroundCard = _resourceValue(map['backgroundCard']); + _effectImage = _resourceValue(map['effectImage']); } String? _userId; @@ -160,10 +172,14 @@ class SCVipStatusRes { String? _startAt; String? _expireAt; SCVipResourceRes? _badge; + SCVipResourceRes? _longBadge; + SCVipResourceRes? _shortBadge; SCVipResourceRes? _avatarFrame; SCVipResourceRes? _entryEffect; SCVipResourceRes? _chatBubble; SCVipResourceRes? _floatPicture; + SCVipResourceRes? _backgroundCard; + SCVipResourceRes? _effectImage; String? get userId => _userId; @@ -183,7 +199,11 @@ class SCVipStatusRes { String? get expireAt => _expireAt; - SCVipResourceRes? get badge => _badge; + SCVipResourceRes? get badge => longBadge; + + SCVipResourceRes? get longBadge => _longBadge ?? _badge; + + SCVipResourceRes? get shortBadge => _shortBadge; SCVipResourceRes? get avatarFrame => _avatarFrame; @@ -193,6 +213,10 @@ class SCVipStatusRes { SCVipResourceRes? get floatPicture => _floatPicture; + SCVipResourceRes? get backgroundCard => _backgroundCard; + + SCVipResourceRes? get effectImage => _effectImage; + bool get isActive => _active == true && (_level ?? 0) > 0; int get levelInt => (_level ?? 0).toInt(); @@ -216,11 +240,17 @@ class SCVipStatusRes { map['displayName'] = _displayName; map['startAt'] = _startAt; map['expireAt'] = _expireAt; - if (_badge != null) map['badge'] = _badge?.toJson(); + if (badge != null) map['badge'] = badge?.toJson(); + if (longBadge != null) map['longBadge'] = longBadge?.toJson(); + if (_shortBadge != null) map['shortBadge'] = _shortBadge?.toJson(); if (_avatarFrame != null) map['avatarFrame'] = _avatarFrame?.toJson(); if (_entryEffect != null) map['entryEffect'] = _entryEffect?.toJson(); if (_chatBubble != null) map['chatBubble'] = _chatBubble?.toJson(); if (_floatPicture != null) map['floatPicture'] = _floatPicture?.toJson(); + if (_backgroundCard != null) { + map['backgroundCard'] = _backgroundCard?.toJson(); + } + if (_effectImage != null) map['effectImage'] = _effectImage?.toJson(); return map; } } @@ -234,10 +264,14 @@ class SCVipLevelConfigRes { num? durationDays, num? priceGold, SCVipResourceRes? badge, + SCVipResourceRes? longBadge, + SCVipResourceRes? shortBadge, SCVipResourceRes? avatarFrame, SCVipResourceRes? entryEffect, SCVipResourceRes? chatBubble, SCVipResourceRes? floatPicture, + SCVipResourceRes? backgroundCard, + SCVipResourceRes? effectImage, bool? canPurchase, num? payableGold, }) { @@ -248,10 +282,14 @@ class SCVipLevelConfigRes { _durationDays = durationDays; _priceGold = priceGold; _badge = badge; + _longBadge = longBadge; + _shortBadge = shortBadge; _avatarFrame = avatarFrame; _entryEffect = entryEffect; _chatBubble = chatBubble; _floatPicture = floatPicture; + _backgroundCard = backgroundCard; + _effectImage = effectImage; _canPurchase = canPurchase; _payableGold = payableGold; } @@ -266,10 +304,14 @@ class SCVipLevelConfigRes { _durationDays = _numValue(map['durationDays']); _priceGold = _numValue(map['priceGold']); _badge = _resourceValue(map['badge']); + _longBadge = _resourceValue(map['longBadge']) ?? _badge; + _shortBadge = _resourceValue(map['shortBadge']); _avatarFrame = _resourceValue(map['avatarFrame']); _entryEffect = _resourceValue(map['entryEffect']); _chatBubble = _resourceValue(map['chatBubble']); _floatPicture = _resourceValue(map['floatPicture']); + _backgroundCard = _resourceValue(map['backgroundCard']); + _effectImage = _resourceValue(map['effectImage']); _canPurchase = _boolValue(map['canPurchase']); _payableGold = _numValue(map['payableGold']); } @@ -281,10 +323,14 @@ class SCVipLevelConfigRes { num? _durationDays; num? _priceGold; SCVipResourceRes? _badge; + SCVipResourceRes? _longBadge; + SCVipResourceRes? _shortBadge; SCVipResourceRes? _avatarFrame; SCVipResourceRes? _entryEffect; SCVipResourceRes? _chatBubble; SCVipResourceRes? _floatPicture; + SCVipResourceRes? _backgroundCard; + SCVipResourceRes? _effectImage; bool? _canPurchase; num? _payableGold; @@ -300,7 +346,11 @@ class SCVipLevelConfigRes { num? get priceGold => _priceGold; - SCVipResourceRes? get badge => _badge; + SCVipResourceRes? get badge => longBadge; + + SCVipResourceRes? get longBadge => _longBadge ?? _badge; + + SCVipResourceRes? get shortBadge => _shortBadge; SCVipResourceRes? get avatarFrame => _avatarFrame; @@ -310,6 +360,10 @@ class SCVipLevelConfigRes { SCVipResourceRes? get floatPicture => _floatPicture; + SCVipResourceRes? get backgroundCard => _backgroundCard; + + SCVipResourceRes? get effectImage => _effectImage; + bool? get canPurchase => _canPurchase; num? get payableGold => _payableGold; @@ -332,11 +386,17 @@ class SCVipLevelConfigRes { map['enabled'] = _enabled; map['durationDays'] = _durationDays; map['priceGold'] = _priceGold; - if (_badge != null) map['badge'] = _badge?.toJson(); + if (badge != null) map['badge'] = badge?.toJson(); + if (longBadge != null) map['longBadge'] = longBadge?.toJson(); + if (_shortBadge != null) map['shortBadge'] = _shortBadge?.toJson(); if (_avatarFrame != null) map['avatarFrame'] = _avatarFrame?.toJson(); if (_entryEffect != null) map['entryEffect'] = _entryEffect?.toJson(); if (_chatBubble != null) map['chatBubble'] = _chatBubble?.toJson(); if (_floatPicture != null) map['floatPicture'] = _floatPicture?.toJson(); + if (_backgroundCard != null) { + map['backgroundCard'] = _backgroundCard?.toJson(); + } + if (_effectImage != null) map['effectImage'] = _effectImage?.toJson(); map['canPurchase'] = _canPurchase; map['payableGold'] = _payableGold; return map; diff --git a/lib/shared/data_sources/models/enum/sc_props_type.dart b/lib/shared/data_sources/models/enum/sc_props_type.dart index 42975a2..adb7a97 100644 --- a/lib/shared/data_sources/models/enum/sc_props_type.dart +++ b/lib/shared/data_sources/models/enum/sc_props_type.dart @@ -1,13 +1,16 @@ -enum SCPropsType { - AVATAR_FRAME, - FLOAT_PICTURE, - DATA_CARD, - CHAT_BUBBLE, - RIDE, - LAYOUT, - THEME, - BADGE, - NOBLE_VIP, - FRAGMENTS, - RED_PACKET, -} +// ignore_for_file: constant_identifier_names + +enum SCPropsType { + AVATAR_FRAME, + FLOAT_PICTURE, + DATA_CARD, + CHAT_BUBBLE, + RIDE, + LAYOUT, + THEME, + BADGE, + NOBLE_VIP, + VIP_EFFECT_IMAGE, + FRAGMENTS, + RED_PACKET, +} diff --git a/lib/ui_kit/widgets/props/sc_data_card_resource_view.dart b/lib/ui_kit/widgets/props/sc_data_card_resource_view.dart index 0a60271..2db127e 100644 --- a/lib/ui_kit/widgets/props/sc_data_card_resource_view.dart +++ b/lib/ui_kit/widgets/props/sc_data_card_resource_view.dart @@ -362,6 +362,7 @@ class _LoopingDataCardVapVideoState extends State<_LoopingDataCardVapVideo> { _controller = controller; controller.setAnimListener( onFailed: (_, __, ___) => _markFailed(), + onVideoComplete: _handleVideoComplete, onVideoStart: () { if (mounted && _failed) { setState(() => _failed = false); @@ -371,6 +372,19 @@ class _LoopingDataCardVapVideoState extends State<_LoopingDataCardVapVideo> { _play(); } + void _handleVideoComplete() { + if (!mounted || !widget.loop || _failed) { + return; + } + unawaited( + Future.delayed(Duration.zero, () { + if (mounted && widget.loop && !_failed) { + return _play(); + } + }), + ); + } + Future _play() async { final controller = _controller; final url = widget.url.trim(); diff --git a/lib/ui_kit/widgets/room/room_user_info_card.dart b/lib/ui_kit/widgets/room/room_user_info_card.dart index bdea8dd..51968a3 100644 --- a/lib/ui_kit/widgets/room/room_user_info_card.dart +++ b/lib/ui_kit/widgets/room/room_user_info_card.dart @@ -270,7 +270,11 @@ class _RoomUserInfoCardState extends State { return Stack( fit: StackFit.expand, children: [ - SCDataCardResourceView(resource: dataCard, fit: BoxFit.cover), + SCDataCardResourceView( + resource: dataCard, + fit: BoxFit.fill, + fallback: DecoratedBox(decoration: _sheetDecoration()), + ), DecoratedBox( decoration: BoxDecoration( gradient: LinearGradient(