import 'dart:io'; import 'package:flutter/material.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; import 'package:flutter_svga/flutter_svga.dart'; import 'package:provider/provider.dart'; import 'package:yumi/app/routes/sc_fluro_navigator.dart'; import 'package:yumi/services/auth/user_profile_manager.dart'; import 'package:yumi/shared/business_logic/models/res/gift_res.dart'; import 'package:yumi/shared/business_logic/models/res/login_res.dart'; import 'package:yumi/shared/data_sources/sources/repositories/sc_gift_repository_imp.dart'; import 'package:yumi/shared/data_sources/sources/repositories/sc_user_repository_impl.dart'; import 'package:yumi/services/general/sc_app_general_manager.dart'; import 'package:yumi/shared/tools/sc_gift_vap_svga_manager.dart'; import 'package:yumi/shared/tools/sc_path_utils.dart'; import 'package:yumi/ui_kit/components/sc_compontent.dart'; import 'package:yumi/ui_kit/components/text/sc_text.dart'; class ProfileGiftWallDetailPage extends StatefulWidget { const ProfileGiftWallDetailPage({super.key, required this.tageId}); final String tageId; @override State createState() => _ProfileGiftWallDetailPageState(); } class _ProfileGiftWallDetailPageState extends State { static const Color _profileBg = Color(0xff072121); static const Color _cardBg = Color(0xff08251E); static const Color _profileBorder = Color(0xffB2FBCC); SocialChatUserProfile? _profile; List _gifts = []; bool _loadingGifts = true; String? _playingGiftSvgaUrl; int _giftEffectSerial = 0; @override void initState() { super.initState(); _primeProfile(); _loadProfile(); _loadGiftWall(); } void _primeProfile() { final currentProfile = Provider.of( context, listen: false, ).userProfile; if (currentProfile?.id == widget.tageId) { _profile = currentProfile; } } void _loadProfile() { SCAccountRepository() .loadUserInfo(widget.tageId) .then((profile) { if (!mounted) return; setState(() { _profile = profile; }); }) .catchError((_) {}); } Future _loadGiftWall() async { final appGeneralManager = Provider.of( context, listen: false, ); try { final result = await SCGiftRepositoryImp().giftWall(widget.tageId); final resolvedGifts = await _resolveGiftWallMeta( result, appGeneralManager, ); if (!mounted) return; setState(() { _gifts = resolvedGifts; _loadingGifts = false; }); } catch (_) { if (!mounted) return; setState(() { _loadingGifts = false; }); } } Future> _resolveGiftWallMeta( List gifts, SCAppGeneralManager appGeneralManager, ) async { if (gifts.isEmpty) return gifts; if (appGeneralManager.giftResList.isEmpty) { await appGeneralManager.giftList(includeCustomized: false); } return gifts .map((gift) => _mergeGiftMeta(gift, appGeneralManager)) .toList(); } SocialChatGiftRes _mergeGiftMeta( SocialChatGiftRes gift, SCAppGeneralManager appGeneralManager, ) { final meta = _findGiftMeta(gift, appGeneralManager); if (meta == null) return gift; return gift.copyWith( giftName: _firstNonBlank([gift.giftName, meta.giftName]), giftCandy: gift.giftCandy ?? meta.giftCandy, giftIntegral: gift.giftIntegral ?? meta.giftIntegral, giftPhoto: _firstNonBlank([gift.giftPhoto, meta.giftPhoto]), giftSourceUrl: _firstNonBlank([gift.giftSourceUrl, meta.giftSourceUrl]), giftCode: _firstNonBlank([gift.giftCode, meta.giftCode]), standardId: _firstNonBlank([gift.standardId, meta.standardId]), ); } SocialChatGiftRes? _findGiftMeta( SocialChatGiftRes gift, SCAppGeneralManager appGeneralManager, ) { final idCandidates = [ gift.standardId ?? "", gift.id ?? "", gift.giftCode ?? "", ].where((id) => id.trim().isNotEmpty && id.trim() != "0"); for (final id in idCandidates) { final meta = appGeneralManager.getGiftByIdOrStandardId(id); if (meta != null) return meta; } final giftPhoto = (gift.giftPhoto ?? "").trim(); final giftCode = (gift.giftCode ?? "").trim(); for (final meta in appGeneralManager.giftResList) { if (giftPhoto.isNotEmpty && giftPhoto == (meta.giftPhoto ?? "").trim()) { return meta; } if (giftCode.isNotEmpty && giftCode == (meta.giftCode ?? "").trim()) { return meta; } } return null; } String? _firstNonBlank(Iterable values) { for (final value in values) { final text = value?.trim(); if (text != null && text.isNotEmpty) { return text; } } return null; } int get _receivedCount { return _gifts.fold(0, (sum, gift) => sum + _giftQuantity(gift)); } num get _totalValue { return _gifts.fold( 0, (sum, gift) => sum + (_giftAmount(gift) * _giftQuantity(gift)), ); } int _giftQuantity(SocialChatGiftRes gift) { return int.tryParse(gift.quantity ?? "") ?? 0; } num _giftAmount(SocialChatGiftRes gift) { return gift.giftCandy ?? gift.giftIntegral ?? 0; } String _formatNumber(num value) { if (value % 1 == 0) return value.toInt().toString(); return value.toStringAsFixed(1); } bool _isSvgaGift(SocialChatGiftRes gift) { final sourceUrl = (gift.giftSourceUrl ?? "").trim(); return sourceUrl.isNotEmpty && SCPathUtils.getFileExtension(sourceUrl).toLowerCase() == ".svga"; } void _playGiftSvgaOnce(SocialChatGiftRes gift) { final sourceUrl = (gift.giftSourceUrl ?? "").trim(); if (!_isSvgaGift(gift) || _playingGiftSvgaUrl != null) { return; } SCGiftVapSvgaManager().preload(sourceUrl, highPriority: true); setState(() { _playingGiftSvgaUrl = sourceUrl; _giftEffectSerial++; }); } void _clearGiftSvgaEffect() { if (!mounted || _playingGiftSvgaUrl == null) { return; } setState(() { _playingGiftSvgaUrl = null; }); } Widget _buildGradientBorder({ double? width, double? height, required double radius, required Widget child, Color backgroundColor = _cardBg, double endAlpha = 0.42, }) { final borderRadius = BorderRadius.circular(radius); return Container( width: width, height: height, decoration: BoxDecoration( borderRadius: borderRadius, gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [_profileBorder, _profileBorder.withValues(alpha: endAlpha)], ), ), padding: EdgeInsets.all(1.w), child: Container( decoration: BoxDecoration( color: backgroundColor, borderRadius: BorderRadius.circular((radius - 1.w).clamp(0, radius)), ), child: child, ), ); } Widget _buildTopBar() { return SizedBox( width: double.infinity, height: 54.w, child: Stack( alignment: Alignment.center, children: [ PositionedDirectional( start: 8.w, child: GestureDetector( behavior: HitTestBehavior.opaque, onTap: () => SCNavigatorUtils.goBack(context), child: SizedBox( width: 52.w, height: 52.w, child: Icon( Icons.keyboard_arrow_left_rounded, color: Colors.white, size: 38.w, ), ), ), ), Text( "Gift Wall", maxLines: 1, overflow: TextOverflow.ellipsis, style: TextStyle( color: Colors.white, fontSize: 19.sp, fontWeight: FontWeight.w600, ), ), ], ), ); } Widget _buildProfileHeader() { return Column( children: [ SizedBox(height: 12.w), head( url: _profile?.userAvatar ?? "", width: 104.w, headdress: _profile?.getHeaddress()?.sourceUrl, headdressCover: _profile?.getHeaddress()?.cover, ), SizedBox(height: 10.w), socialchatNickNameText( _profile?.userNickname ?? "", maxWidth: 300.w, fontSize: 20, textColor: Colors.white, fontWeight: FontWeight.w400, type: _profile?.getVIP()?.name ?? "", needScroll: (_profile?.userNickname?.characters.length ?? 0) > 13, ), SizedBox(height: 18.w), _buildSummaryCard(), ], ); } Widget _buildSummaryCard() { return _buildGradientBorder( width: 355.w, height: 51.w, radius: 8.w, backgroundColor: _cardBg, endAlpha: 0.52, child: Row( children: [ _buildSummaryItem( value: _formatNumber(_receivedCount), label: "Received", ), _buildSummaryDivider(), _buildSummaryItem( value: _formatNumber(_totalValue), label: "Total Value", showCoin: true, ), ], ), ); } Widget _buildSummaryItem({ required String value, required String label, bool showCoin = false, }) { return Expanded( child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Row( mainAxisSize: MainAxisSize.min, children: [ if (showCoin) ...[_buildCoinIcon(14.w), SizedBox(width: 3.w)], text( value, fontSize: 17, textColor: Colors.white, fontWeight: FontWeight.bold, ), ], ), text(label, fontSize: 14, textColor: const Color(0xffB1B1B1)), ], ), ); } Widget _buildSummaryDivider() { return Container( height: 28.w, width: 1.w, decoration: const BoxDecoration( gradient: LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [ Color.fromRGBO(255, 255, 255, 0), Colors.white, Color.fromRGBO(255, 255, 255, 0), ], stops: [0, 0.4862, 1], ), ), ); } Widget _buildGiftGrid() { if (_loadingGifts) { return SliverToBoxAdapter( child: Padding( padding: EdgeInsets.only(top: 80.w), child: Center( child: SizedBox( width: 24.w, height: 24.w, child: const CircularProgressIndicator( strokeWidth: 2, color: _profileBorder, ), ), ), ), ); } if (_gifts.isEmpty) { return SliverToBoxAdapter( child: Padding( padding: EdgeInsets.only(top: 90.w), child: Opacity( opacity: 0.42, child: Image.asset( "sc_images/room/sc_icon_room_music_empty.png", width: 72.w, height: 72.w, fit: BoxFit.contain, ), ), ), ); } return SliverPadding( padding: EdgeInsets.fromLTRB(10.w, 34.w, 10.w, 0), sliver: SliverGrid( delegate: SliverChildBuilderDelegate( (context, index) => _buildGiftItem(_gifts[index]), childCount: _gifts.length, ), gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( crossAxisCount: 3, mainAxisSpacing: 25.w, crossAxisSpacing: 22.w, childAspectRatio: 104 / 130, ), ), ); } Widget _buildGiftItem(SocialChatGiftRes gift) { final quantity = _giftQuantity(gift); final giftName = (gift.giftName ?? "").trim().isEmpty ? "--" : gift.giftName!.trim(); return GestureDetector( behavior: HitTestBehavior.opaque, onTap: () => _playGiftSvgaOnce(gift), child: _buildGradientBorder( radius: 8.w, backgroundColor: _cardBg, endAlpha: 0.62, child: Padding( padding: EdgeInsets.fromLTRB(10.w, 10.w, 10.w, 8.w), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded( child: Center( child: netImage( url: gift.giftPhoto ?? "", width: 82.w, height: 74.w, fit: BoxFit.contain, noDefaultImg: true, ), ), ), SizedBox(height: 7.w), text( "$giftName*$quantity", fontSize: 13, textColor: Colors.white, maxLines: 1, ), SizedBox(height: 5.w), Row( mainAxisAlignment: MainAxisAlignment.center, children: [ _buildCoinIcon(12.w), SizedBox(width: 4.w), Flexible( child: text( _formatNumber(_giftAmount(gift)), fontSize: 11, textColor: Colors.white, maxLines: 1, ), ), ], ), ], ), ), ), ); } Widget _buildCoinIcon(double size) { return SizedBox( width: size, height: size, child: ClipRect( child: FittedBox( fit: BoxFit.cover, alignment: Alignment.bottomCenter, child: Image.asset("sc_images/room/sc_icon_luckgift_coins_anim.webp"), ), ), ); } @override Widget build(BuildContext context) { final bottomPadding = MediaQuery.of(context).padding.bottom + 22.w; final playingGiftSvgaUrl = _playingGiftSvgaUrl; return Scaffold( backgroundColor: _profileBg, body: Stack( children: [ SafeArea( bottom: false, child: CustomScrollView( physics: const BouncingScrollPhysics(), slivers: [ SliverToBoxAdapter( child: Column( children: [_buildTopBar(), _buildProfileHeader()], ), ), _buildGiftGrid(), SliverToBoxAdapter( child: Padding( padding: EdgeInsets.only(top: 54.w, bottom: bottomPadding), child: Center( child: text( "No Further Data Available~", fontSize: 13, textColor: const Color(0xffB1B1B1), ), ), ), ), ], ), ), if (playingGiftSvgaUrl != null) Positioned.fill( child: _GiftWallSvgaEffectOverlay( key: ValueKey("$playingGiftSvgaUrl-$_giftEffectSerial"), sourceUrl: playingGiftSvgaUrl, onFinished: _clearGiftSvgaEffect, ), ), ], ), ); } } class _GiftWallSvgaEffectOverlay extends StatefulWidget { const _GiftWallSvgaEffectOverlay({ super.key, required this.sourceUrl, required this.onFinished, }); final String sourceUrl; final VoidCallback onFinished; @override State<_GiftWallSvgaEffectOverlay> createState() => _GiftWallSvgaEffectOverlayState(); } class _GiftWallSvgaEffectOverlayState extends State<_GiftWallSvgaEffectOverlay> with SingleTickerProviderStateMixin { late final SVGAAnimationController _controller; bool _loaded = false; bool _finished = false; @override void initState() { super.initState(); _controller = SVGAAnimationController(vsync: this); _controller.addStatusListener(_handleStatusChanged); _loadAndPlay(); } void _handleStatusChanged(AnimationStatus status) { if (status == AnimationStatus.completed) { _finish(); } } Future _loadAndPlay() async { try { final movieEntity = await _loadMovieEntity(widget.sourceUrl); if (!mounted || _finished) return; setState(() { _loaded = true; _controller.videoItem = movieEntity; }); _controller ..reset() ..repeat(count: 1); } catch (error) { _finish(); } } Future _loadMovieEntity(String sourceUrl) async { final cache = SCGiftVapSvgaManager().videoItemCache; final cached = cache[sourceUrl]; if (cached != null) return cached; final pathType = SCPathUtils.getPathType(sourceUrl); late final MovieEntity movieEntity; if (pathType == PathType.network) { movieEntity = await SVGAParser.shared.decodeFromURL(sourceUrl); } else if (pathType == PathType.asset) { movieEntity = await SVGAParser.shared.decodeFromAssets(sourceUrl); } else if (pathType == PathType.file) { final bytes = await File(sourceUrl).readAsBytes(); movieEntity = await SVGAParser.shared.decodeFromBuffer(bytes); } else { throw Exception("Unsupported SVGA source: $sourceUrl"); } movieEntity.autorelease = false; cache[sourceUrl] = movieEntity; return movieEntity; } void _finish() { if (_finished) return; _finished = true; widget.onFinished(); } @override void dispose() { _controller.removeStatusListener(_handleStatusChanged); _controller.dispose(); super.dispose(); } @override Widget build(BuildContext context) { return AbsorbPointer( child: Container( color: Colors.black.withValues(alpha: 0.18), alignment: Alignment.center, child: _loaded ? SVGAImage( _controller, fit: BoxFit.contain, allowDrawingOverflow: false, ) : const SizedBox.shrink(), ), ); } }