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/routes/sc_fluro_navigator.dart'; import 'package:yumi/app_localizations.dart'; import 'package:yumi/modules/user/vip/vip_route.dart'; import 'package:yumi/services/auth/user_profile_manager.dart'; import 'package:yumi/shared/business_logic/models/res/sc_vip_res.dart'; import 'package:yumi/shared/data_sources/sources/local/user_manager.dart'; import 'package:yumi/shared/data_sources/sources/repositories/sc_vip_repository_imp.dart'; 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}); @override State createState() => _VipDetailPageState(); } class _VipDetailPageState extends State { final SCVipRepositoryImp _repository = SCVipRepositoryImp(); int _selectedLevel = 1; bool _isLoading = false; bool _isPurchasing = false; int? _previewingLevel; Object? _loadError; SCVipHomeRes? _home; SCVipStatusRes? _status; SCVipPreviewRes? _preview; @override void initState() { super.initState(); _loadVipData(); } Future _loadVipData({bool showLoading = true}) async { if (showLoading) { setState(() { _isLoading = true; _loadError = null; }); } try { final results = await Future.wait([ _repository.vipHome(), _repository.vipStatus().catchError((error) { return SCVipStatusRes(); }), ]); final home = results[0] as SCVipHomeRes; final status = results[1] as SCVipStatusRes; final resolvedStatus = _resolveStatus(status, home.state); final selectedLevel = _resolveInitialLevel(home, resolvedStatus); if (!mounted) return; setState(() { _home = home; _status = resolvedStatus; _selectedLevel = selectedLevel; _preview = null; _previewingLevel = null; _isLoading = false; _loadError = null; }); _loadPreviewForLevel(selectedLevel); } catch (error) { if (!mounted) return; setState(() { _loadError = error; _isLoading = false; }); } } SCVipStatusRes? _resolveStatus( SCVipStatusRes status, SCVipStatusRes? homeState, ) { if (homeState != null && homeState.levelInt > 0) { if (status.levelInt <= 0 || status.levelInt == homeState.levelInt) { return homeState; } } return status.levelInt > 0 ? status : homeState; } int _resolveInitialLevel(SCVipHomeRes home, SCVipStatusRes? status) { if (status?.isActive == true && status!.levelInt > 0) { return status.levelInt.clamp(1, 5).toInt(); } for (final level in home.levels) { if (level.enabled != false && level.canPurchase == true) { return level.levelInt.clamp(1, 5).toInt(); } } for (final level in home.levels) { if (level.enabled != false && level.levelInt > 0) { return level.levelInt.clamp(1, 5).toInt(); } } return 1; } SCVipLevelConfigRes? _levelConfigFromHome(SCVipHomeRes? home, int level) { final levels = home?.levels ?? []; for (final config in levels) { if (config.levelInt == level) { return config; } } return null; } SCVipLevelConfigRes? _levelConfigFor(int level) => _levelConfigFromHome(_home, level); SCVipLevelConfigRes? get _selectedLevelConfig => _levelConfigFor(_selectedLevel); SCVipStatusRes? get _selectedLevelState { final status = _status; if (status != null && status.levelInt == _selectedLevel) { return status; } return null; } void _selectLevel(int level) { setState(() { _selectedLevel = level; _preview = null; }); _loadPreviewForLevel(level); } Future _loadPreviewForLevel(int level) async { final config = _levelConfigFor(level); if (_home?.configured == false || config?.enabled == false || config?.canPurchase != true) { return; } setState(() => _previewingLevel = level); try { final preview = await _repository.vipPreview(targetLevel: level); if (!mounted || _selectedLevel != level) return; setState(() { _preview = preview; _previewingLevel = null; }); } catch (_) { if (!mounted || _selectedLevel != level) return; setState(() => _previewingLevel = null); } } Future _purchaseSelectedLevel() async { if (!_canPurchaseSelected || _isPurchasing) { return; } final profileManager = Provider.of( context, listen: false, ); setState(() => _isPurchasing = true); SCLoadingManager.show(); try { if (_preview?.targetLevelInt != _selectedLevel) { _preview = await _repository.vipPreview(targetLevel: _selectedLevel); } if (!mounted) return; final purchase = await _repository.vipPurchase( targetLevel: _selectedLevel, bizIdempotentKey: _buildBizIdempotentKey(), ); if (!mounted) return; if (purchase.success == true) { SCTts.show(SCAppLocalizations.of(context)!.purchaseIsSuccessful); profileManager.fetchUserProfileData(loadGuardCount: false); profileManager.balance(); await _loadVipData(showLoading: false); } } catch (error) { debugPrint('VIP purchase failed: $error'); } finally { SCLoadingManager.hide(); if (mounted) { setState(() => _isPurchasing = false); } } } String _buildBizIdempotentKey() { final userId = AccountStorage().getCurrentUser()?.userProfile?.id ?? '0'; return 'vip-buy-$userId-${DateTime.now().millisecondsSinceEpoch}'; } @override Widget build(BuildContext context) { return Scaffold( backgroundColor: Colors.black, body: Stack( children: [ Positioned.fill( child: DecoratedBox( decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: const [Color(0xFF0D2B22), Color(0xFF0E0E0E)], stops: const [0, 0.40], ), ), ), ), Positioned( left: 0, right: 0, top: 0, child: Image.asset( 'sc_images/vip/sc_vip_page_bg.png', width: ScreenUtil().screenWidth, fit: BoxFit.fitWidth, alignment: Alignment.topCenter, ), ), Positioned( left: 0, right: 0, top: 0, height: 300.w, child: IgnorePointer( child: DecoratedBox( decoration: BoxDecoration( gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [ Colors.black.withValues(alpha: 0.36), Colors.black.withValues(alpha: 0.62), Colors.transparent, ], stops: const [0, 0.52, 1], ), ), ), ), ), SafeArea( bottom: false, child: Column( children: [ _buildTopBar(), _buildLevelTabs(), Expanded(child: _buildBodyContent()), _buildActivateBar(), ], ), ), ], ), ); } Widget _buildBodyContent() { if (_isLoading && _home == null) { return Center( child: CircularProgressIndicator( color: const Color(0xFFEBCB76), strokeWidth: 2.w, ), ); } if (_loadError != null && _home == null) { return Center( child: Column( mainAxisSize: MainAxisSize.min, children: [ Text( 'Failed to load VIP data', style: TextStyle(color: Colors.white, fontSize: 14.sp), ), SizedBox(height: 12.w), SCDebounceWidget( onTap: () => _loadVipData(), child: Container( padding: EdgeInsets.symmetric(horizontal: 18.w, vertical: 8.w), decoration: BoxDecoration( borderRadius: BorderRadius.circular(18.w), border: Border.all( color: const Color(0xFFEBCB76), width: 1.w, ), ), child: Text( 'Retry', style: TextStyle( color: const Color(0xFFFFE8A7), fontSize: 13.sp, fontWeight: FontWeight.w600, ), ), ), ), ], ), ); } final decorationItems = _decorationPrivilegeItems; final privilegeItems = _vipPrivilegeItems; return SingleChildScrollView( physics: const BouncingScrollPhysics(), padding: EdgeInsets.fromLTRB(12.w, 40.w, 12.w, 18.w), child: Column( children: [ _buildLevelCard(), SizedBox(height: 30.w), _buildLevelDivider(), SizedBox(height: 3.w), if (decorationItems.isNotEmpty) ...[ _buildSectionTitle('Decoration Privileges'), SizedBox(height: 10.w), _buildDecorationGrid(decorationItems), SizedBox(height: 20.w), ], if (privilegeItems.isNotEmpty) ...[ _buildSectionTitle('VIP Privileges'), SizedBox(height: 14.w), _buildPrivilegeGrid(privilegeItems), ], SizedBox(height: 76.w), ], ), ); } Widget _buildTopBar() { return SizedBox( height: 44.w, child: Stack( alignment: Alignment.center, children: [ Align( alignment: AlignmentDirectional.centerStart, child: GestureDetector( behavior: HitTestBehavior.opaque, onTap: () => Navigator.pop(context), child: SizedBox( width: 44.w, height: 44.w, child: Icon( SCGlobalConfig.lang == 'ar' ? Icons.keyboard_arrow_right : Icons.keyboard_arrow_left, color: Colors.white, size: 28.w, ), ), ), ), Text( 'VIP', style: TextStyle( color: Colors.white, fontSize: 18.sp, fontWeight: FontWeight.w500, ), ), Align( alignment: AlignmentDirectional.centerEnd, child: GestureDetector( behavior: HitTestBehavior.opaque, onTap: () => SCNavigatorUtils.push(context, VipRoute.instruction), child: Padding( padding: EdgeInsetsDirectional.only(end: 12.w), child: Image.asset( 'sc_images/vip/sc_vip_question.png', width: 24.w, height: 24.w, ), ), ), ), ], ), ); } Widget _buildLevelTabs() { return SizedBox( height: 43.w, child: ListView.separated( scrollDirection: Axis.horizontal, physics: const BouncingScrollPhysics(), padding: EdgeInsets.symmetric(horizontal: 12.w), itemCount: 5, separatorBuilder: (_, __) => SizedBox(width: 30.w), itemBuilder: (context, index) { final level = index + 1; final config = _levelConfigFor(level); final enabled = config?.enabled != false; final selected = level == _selectedLevel; return GestureDetector( behavior: HitTestBehavior.opaque, onTap: () => _selectLevel(level), child: Column( mainAxisSize: MainAxisSize.min, children: [ SizedBox(height: 4.w), 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), SizedBox( width: 0, height: 9.5.w, child: OverflowBox( minWidth: 60.w, maxWidth: 60.w, alignment: Alignment.center, child: selected ? Image.asset( 'sc_images/vip/sc_vip_selected_line.png', width: 60.w, height: 9.5.w, fit: BoxFit.fill, ) : const SizedBox.shrink(), ), ), ], ), ); }, ), ); } Widget _buildLevelCard() { final assetLevel = _assetLevel; final backgroundCard = _selectedBackgroundCard; final effectImage = _selectedEffectImage ?? _selectedLongBadge; return Center( child: SizedBox( width: 351.w, height: 124.5.w, child: Stack( clipBehavior: Clip.none, children: [ Positioned.fill( 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, ), ), ), ), ), PositionedDirectional( start: 20.w, top: 33.w, child: _buildVipLevelMark(_selectedLevel, scale: 1.18), ), PositionedDirectional( start: 22.w, top: 80.w, end: 160.w, child: SizedBox( height: 32.w, child: Text( _selectedLevelStatusText, maxLines: 2, overflow: TextOverflow.ellipsis, style: TextStyle( color: const Color(0xFFF2C766).withValues(alpha: 0.78), fontSize: 12.sp, fontWeight: FontWeight.w500, height: 1.2, ), ), ), ), PositionedDirectional( end: 24.w, top: -36.w, child: _buildDynamicResourceVisual( resource: effectImage, width: 135.w, height: 135.w, fallbackIcon: Icons.workspace_premium_outlined, ), ), ], ), ), ); } 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( height: 38.w, child: Stack( clipBehavior: Clip.none, alignment: Alignment.center, children: [ Positioned( left: -12.w, right: -12.w, top: 0, height: 8.w, child: Image.asset( 'sc_images/vip/sc_vip_top_border_$_assetLevel.png', fit: BoxFit.fill, ), ), Positioned( left: 0, right: 0, top: ornamentLayout.top.w, child: Transform.translate( offset: Offset(ornamentLayout.dx.w, 0), child: Center( child: Image.asset( 'sc_images/vip/sc_vip_card_ornament_$_assetLevel.png', width: ornamentLayout.width.w, height: ornamentLayout.height.w, fit: BoxFit.contain, ), ), ), ), ], ), ); } Widget _buildSectionTitle(String title) { return Row( mainAxisAlignment: MainAxisAlignment.center, children: [ Image.asset( 'sc_images/vip/sc_vip_title_left.png', width: 16.w, height: 20.w, fit: BoxFit.contain, ), SizedBox(width: 8.w), Text( title, style: TextStyle( color: Colors.white, fontSize: 18.sp, fontWeight: FontWeight.w500, ), ), SizedBox(width: 8.w), Image.asset( 'sc_images/vip/sc_vip_title_right.png', width: 16.w, height: 20.w, fit: BoxFit.contain, ), ], ); } Widget _buildDecorationGrid(List<_VipFeatureData> items) { return Wrap( spacing: 10.w, runSpacing: 10.w, children: items .map( (item) => _buildFeatureTile( item, width: item.wide ? 206.w : 98.w, height: 100.w, ), ) .toList(), ); } Widget _buildPrivilegeGrid(List<_VipFeatureData> items) { return Wrap( spacing: 34.w, runSpacing: 24.w, children: items.asMap().entries.map((entry) { final item = entry.value; return SizedBox( width: 74.w, child: Column( children: [ Container( width: 46.w, height: 46.w, decoration: BoxDecoration( shape: BoxShape.circle, color: Colors.white.withValues(alpha: 0.08), ), alignment: Alignment.center, child: _buildFeatureIcon(item, size: 23.w), ), SizedBox(height: 7.w), Text( item.title, textAlign: TextAlign.center, style: TextStyle( color: Colors.white, fontSize: 12.sp, fontWeight: FontWeight.w400, height: 1.1, ), ), ], ), ); }).toList(), ); } 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( fromConfig: (config) => config.avatarFrame, fromState: (status) => status.avatarFrame, ); SCVipResourceRes? get _selectedEntryEffect => _selectedResource( fromConfig: (config) => config.entryEffect, fromState: (status) => status.entryEffect, ); SCVipResourceRes? get _selectedChatBubble => _selectedResource( fromConfig: (config) => config.chatBubble, fromState: (status) => status.chatBubble, ); SCVipResourceRes? get _selectedFloatPicture => _selectedResource( fromConfig: (config) => config.floatPicture, 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, }) { final config = _selectedLevelConfig; final state = _selectedLevelState; return _preferResource( config == null ? null : fromConfig(config), state == null ? null : fromState(state), ); } SCVipResourceRes? _preferResource( SCVipResourceRes? configResource, SCVipResourceRes? stateResource, ) { if (_hasResourceUrl(configResource) || !_hasResourceUrl(stateResource)) { return _hasResource(configResource) ? configResource : stateResource; } return stateResource; } List<_VipFeatureData> get _decorationPrivilegeItems { if (_selectedLevelConfig == null && _selectedLevelState == null) { return const <_VipFeatureData>[]; } final items = <_VipFeatureData>[]; _addResourceItem( items, resource: _selectedLongBadge, fallbackTitle: 'Long Badge', fallbackIcon: Icons.workspace_premium_outlined, ); _addResourceItem( items, resource: _selectedShortBadge, fallbackTitle: 'Short Badge', fallbackIcon: Icons.workspace_premium_outlined, ); _addResourceItem( items, resource: _selectedAvatarFrame, fallbackTitle: 'Avatar Frame', fallbackIcon: Icons.person_pin_circle_outlined, ); _addResourceItem( items, resource: _selectedChatBubble, fallbackTitle: 'Chat Bubble', fallbackIcon: Icons.chat_bubble_outline_rounded, ); _addResourceItem( items, resource: _selectedFloatPicture, fallbackTitle: 'Float Picture', fallbackIcon: Icons.filter_frames_outlined, ); _addResourceItem( items, resource: _selectedEntryEffect, fallbackTitle: 'Entry Effect', 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: // badges/avatarFrame/entryEffect/chatBubble/floatPicture/backgroundCard. // Do not render demo-only functional privileges without backend data. return const <_VipFeatureData>[]; } void _addResourceItem( List<_VipFeatureData> items, { required SCVipResourceRes? resource, required String fallbackTitle, IconData? fallbackIcon, bool wide = false, }) { if (!_hasResource(resource)) { return; } items.add( _VipFeatureData( _resourceTitle(resource, fallbackTitle), null, icon: fallbackIcon, resourceUrl: resource?.previewUrl, wide: wide, ), ); } bool _hasResource(SCVipResourceRes? resource) { return resource?.hasContent == true; } bool _hasResourceUrl(SCVipResourceRes? resource) { return resource?.hasAnyUrl == true; } String _resourceTitle(SCVipResourceRes? resource, String fallbackTitle) { final name = resource?.name?.trim(); if (name != null && name.isNotEmpty) { return name; } 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, required double height, required IconData fallbackIcon, }) { final url = resource?.previewUrl?.trim(); if (url != null && url.isNotEmpty) { return netImage( url: url, width: width, height: height, fit: BoxFit.contain, noDefaultImg: true, loadingWidget: SizedBox( width: width, height: height, child: const Center(child: CircularProgressIndicator(strokeWidth: 2)), ), errorWidget: _buildLargeResourcePlaceholder( fallbackIcon, width: width, height: height, ), ); } if (!_hasResource(resource)) { return SizedBox(width: width, height: height); } return _buildLargeResourcePlaceholder( fallbackIcon, width: width, height: height, ); } Widget _buildLargeResourcePlaceholder( IconData icon, { required double width, required double height, }) { return SizedBox( width: width, height: height, child: Center( child: Container( width: (width * 0.48).clamp(46.w, 74.w).toDouble(), height: (height * 0.48).clamp(46.w, 74.w).toDouble(), decoration: BoxDecoration( shape: BoxShape.circle, color: Colors.black.withValues(alpha: 0.20), border: Border.all( color: const Color(0xFFFFD155).withValues(alpha: 0.45), width: 1.w, ), ), alignment: Alignment.center, child: Icon(icon, color: const Color(0xFFFFD155), size: 32.w), ), ), ); } Widget _buildFeatureIcon(_VipFeatureData item, {required double size}) { final url = item.resourceUrl?.trim(); if (url != null && url.isNotEmpty) { return ClipRRect( borderRadius: BorderRadius.circular(6.w), child: netImage( url: url, width: size, height: size, fit: BoxFit.contain, noDefaultImg: true, errorWidget: Icon( item.icon ?? Icons.auto_awesome, color: const Color(0xFFFF8E3D), size: size, ), ), ); } return Icon( item.icon ?? Icons.auto_awesome, color: const Color(0xFFFF8E3D), size: size, ); } Widget _buildFeatureVisual(_VipFeatureData item, {required bool isWide}) { final url = item.resourceUrl?.trim(); if (url != null && url.isNotEmpty) { return netImage( url: url, fit: BoxFit.contain, noDefaultImg: true, errorWidget: item.assetPath != null ? Image.asset(item.assetPath!, fit: BoxFit.contain) : isWide ? Center(child: _buildWideRewardPlaceholder()) : _buildRewardPlaceholder(item.icon ?? Icons.auto_awesome), ); } if (item.assetPath != null) { return Image.asset(item.assetPath!, fit: BoxFit.contain); } return isWide ? Center(child: _buildWideRewardPlaceholder()) : _buildRewardPlaceholder(item.icon ?? Icons.auto_awesome); } Widget _buildRewardPlaceholder(IconData icon) { return Center( child: Container( width: 46.w, height: 46.w, decoration: BoxDecoration( shape: BoxShape.circle, gradient: RadialGradient( colors: [ const Color(0xFFFFD155).withValues(alpha: 0.28), Colors.white.withValues(alpha: 0.06), ], ), border: Border.all( color: const Color(0xFFFFD155).withValues(alpha: 0.45), width: 0.8.w, ), ), alignment: Alignment.center, child: Icon(icon, color: const Color(0xFFFFD155), size: 24.w), ), ); } Widget _buildWideRewardPlaceholder() { return Container( width: double.infinity, height: 40.w, decoration: BoxDecoration( borderRadius: BorderRadius.circular(20.w), gradient: LinearGradient( colors: [ const Color(0xFFFFD155).withValues(alpha: 0.14), const Color(0xFF7F4DFF).withValues(alpha: 0.16), const Color(0xFFFFD155).withValues(alpha: 0.14), ], ), border: Border.all( color: const Color(0xFFFFD155).withValues(alpha: 0.35), width: 0.8.w, ), ), child: Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ for (int i = 0; i < 5; i++) Container( width: 4.w, height: 4.w, decoration: BoxDecoration( shape: BoxShape.circle, color: Colors.white.withValues(alpha: 0.45), ), ), ], ), ); } Widget _buildFeatureTile( _VipFeatureData item, { required double width, required double height, }) { final isWide = item.wide; return Container( width: width, height: height, decoration: BoxDecoration( borderRadius: BorderRadius.circular(6.w), border: Border.all(color: const Color(0xFFEBCB76), width: 1.w), color: Colors.black.withValues(alpha: 0.38), ), padding: EdgeInsets.symmetric(horizontal: 8.w, vertical: 8.w), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Expanded(child: _buildFeatureVisual(item, isWide: isWide)), SizedBox(height: 5.w), Text( item.title, maxLines: 2, textAlign: TextAlign.center, overflow: TextOverflow.ellipsis, style: TextStyle( color: Colors.white, fontSize: 12.sp, fontWeight: FontWeight.w400, ), ), ], ), ); } Widget _buildActivateBar() { final bottomInset = MediaQuery.of(context).padding.bottom; final actionButton = _actionButtonConfigForLevel(_assetLevel); final barOverflow = 20.w + bottomInset; final canPurchase = _canPurchaseSelected && !_isPurchasing && _previewingLevel != _selectedLevel; return SizedBox( height: 106.5.w, width: double.infinity, child: Stack( clipBehavior: Clip.none, children: [ Positioned( left: 0, right: 0, top: 0, bottom: -barOverflow, child: ClipRRect( borderRadius: BorderRadius.only( topLeft: Radius.circular(20.w), topRight: Radius.circular(20.w), ), child: Image.asset( 'sc_images/vip/sc_vip_card_bg_$_assetLevel.png', fit: BoxFit.fitWidth, ), ), ), Positioned.fill( child: Padding( padding: EdgeInsets.fromLTRB( 30.w, 30.w, 30.w, 10.w + bottomInset, ), child: Row( children: [ Container( width: 22.w, height: 22.w, decoration: const BoxDecoration( shape: BoxShape.circle, color: Color(0xFFFFA629), ), alignment: Alignment.center, child: Icon( Icons.stars_rounded, color: Colors.white, size: 14.w, ), ), SizedBox(width: 6.w), Directionality( textDirection: TextDirection.ltr, child: Text( _priceText, style: TextStyle( color: const Color(0xFFFFE8A7), fontSize: 16.sp, fontWeight: FontWeight.w700, ), ), ), const Spacer(), SCDebounceWidget( onTap: canPurchase ? _purchaseSelectedLevel : () {}, child: Opacity( opacity: canPurchase ? 1 : 0.52, child: Container( width: 106.w, height: 42.w, alignment: Alignment.center, decoration: BoxDecoration( image: DecorationImage( image: AssetImage(actionButton.backgroundAssetPath), fit: BoxFit.fill, ), ), child: _isPurchasing || _previewingLevel == _selectedLevel ? SizedBox( width: 16.w, height: 16.w, child: CircularProgressIndicator( strokeWidth: 2.w, color: actionButton.textColor, ), ) : Text( actionButton.label, textAlign: TextAlign.center, style: TextStyle( color: actionButton.textColor, fontFamily: actionButton.fontFamily, fontSize: actionButton.fontSize.sp, fontStyle: FontStyle.normal, fontWeight: FontWeight.w700, height: 1, ), ), ), ), ), ], ), ), ), ], ), ); } Widget _buildVipLevelMark(int level, {double scale = 1}) { return Transform.scale( scale: scale, alignment: Alignment.centerLeft, 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), ], ), ), ); } Widget _buildVipLevelNumber(int level) { return Image.asset( 'sc_images/vip/sc_vip_number_${level}_filled.png', height: 25.w, fit: BoxFit.fill, color: const Color(0xFFFFD155), colorBlendMode: BlendMode.srcIn, ); } _VipOrnamentLayout _ornamentLayoutForLevel(int level) { switch (level) { case 1: return const _VipOrnamentLayout( width: 176.5, height: 36, top: -10, dx: 0, ); case 2: return const _VipOrnamentLayout( width: 246, height: 61, top: -20, dx: 0, ); case 3: return const _VipOrnamentLayout( width: 277, height: 79.5, top: -35, dx: 0, ); case 4: return const _VipOrnamentLayout( width: 240.5, height: 79.5, top: -35, dx: 0, ); case 5: default: return const _VipOrnamentLayout( width: 282.5, height: 101.5, top: -48, dx: 0, ); } } _VipActionButtonConfig _actionButtonConfigForLevel(int level) { final config = _levelConfigFor(level); if (_home?.configured == false || config?.enabled == false) { return const _VipActionButtonConfig.disabled(label: 'Unavailable'); } final activeLevel = _activeVipLevel; if (_status?.isActive == true) { if (level < activeLevel) { return const _VipActionButtonConfig.disabled(label: 'Activated'); } if (level == activeLevel) { return config?.canPurchase == true ? const _VipActionButtonConfig.renew() : const _VipActionButtonConfig.disabled(label: 'Activated'); } return config?.canPurchase == true ? const _VipActionButtonConfig.update() : const _VipActionButtonConfig.disabled(label: 'Unavailable'); } return config?.canPurchase == true ? const _VipActionButtonConfig.activate() : const _VipActionButtonConfig.disabled(label: 'Unavailable'); } bool get _canPurchaseSelected { final config = _levelConfigFor(_selectedLevel); return _home?.configured != false && config?.enabled != false && config?.canPurchase == true; } int get _activeVipLevel { final status = _status; if (status?.isActive == true) { return status!.levelInt.clamp(1, 5).toInt(); } return 0; } String get _selectedLevelStatusText { if (_home?.configured == false) { return 'Not configured'; } final config = _levelConfigFor(_selectedLevel); if (config?.enabled == false) { return 'Unavailable'; } final activeLevel = _activeVipLevel; if (activeLevel > 0) { if (_selectedLevel == activeLevel) { final expireAt = _shortDate(_status?.expireAt); return expireAt.isEmpty ? 'Activated' : 'Active until $expireAt'; } if (_selectedLevel < activeLevel) { return 'VIP$activeLevel activated'; } return 'Upgrade from VIP$activeLevel'; } return 'Not activated'; } String get _priceText { final config = _levelConfigFor(_selectedLevel); final preview = _preview?.targetLevelInt == _selectedLevel ? _preview : null; final amount = preview?.payableGold ?? config?.payableGold ?? config?.priceGold; final days = (preview?.durationDays ?? config?.durationDays ?? 30).toInt(); final amountText = amount == null ? '--' : _formatGold(amount); final daysLabel = SCAppLocalizations.of(context)?.days ?? 'Days'; return '$amountText / $days $daysLabel'; } String _formatGold(num value) { final doubleValue = value.toDouble(); if (doubleValue >= 1000000) { return '${_trimNumber(doubleValue / 1000000)} M'; } if (doubleValue >= 1000) { return '${_trimNumber(doubleValue / 1000)} K'; } return _trimNumber(doubleValue); } String _trimNumber(double value) { final fixed = value >= 10 ? value.toStringAsFixed(0) : value.toStringAsFixed(1); return fixed.endsWith('.0') ? fixed.substring(0, fixed.length - 2) : fixed; } String _shortDate(String? value) { final date = value?.trim() ?? ''; if (date.length >= 10) { return date.substring(0, 10); } return date; } int get _assetLevel => _selectedLevel.clamp(1, 5).toInt(); } class _VipOrnamentLayout { const _VipOrnamentLayout({ required this.width, required this.height, required this.top, required this.dx, }); final double width; final double height; final double top; final double dx; } class _VipFeatureData { const _VipFeatureData( this.title, this.assetPath, { this.icon, this.resourceUrl, this.wide = false, }); final String title; final String? assetPath; final IconData? icon; final String? resourceUrl; final bool wide; } class _VipActionButtonConfig { const _VipActionButtonConfig({ required this.label, required this.backgroundAssetPath, required this.textColor, required this.fontSize, required this.fontFamily, }); const _VipActionButtonConfig.activate() : this( label: 'Activate', backgroundAssetPath: 'sc_images/vip/sc_vip_activate_button_bg.png', textColor: const Color(0xFF7B4B16), fontSize: 14, fontFamily: null, ); const _VipActionButtonConfig.update() : this( label: 'Update', backgroundAssetPath: 'sc_images/vip/sc_vip_update_button_bg.png', textColor: const Color(0xFF740000), fontSize: 16, fontFamily: 'Source Han Sans SC', ); const _VipActionButtonConfig.renew() : this( label: 'Renew', backgroundAssetPath: 'sc_images/vip/sc_vip_activate_button_bg.png', textColor: const Color(0xFF7B4B16), fontSize: 14, fontFamily: null, ); const _VipActionButtonConfig.disabled({required String label}) : this( label: label, backgroundAssetPath: 'sc_images/vip/sc_vip_activate_button_bg.png', textColor: const Color(0xFF7B4B16), fontSize: 13, fontFamily: null, ); final String label; final String backgroundAssetPath; final Color textColor; final double fontSize; final String? fontFamily; }