yumi-flutter/lib/modules/user/profile/profile_gift_wall_detail_page.dart
2026-05-13 13:46:50 +08:00

655 lines
18 KiB
Dart

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<ProfileGiftWallDetailPage> createState() =>
_ProfileGiftWallDetailPageState();
}
class _ProfileGiftWallDetailPageState extends State<ProfileGiftWallDetailPage> {
static const Color _profileBg = Color(0xff072121);
static const Color _cardBg = Color(0xff08251E);
static const Color _profileBorder = Color(0xffB2FBCC);
SocialChatUserProfile? _profile;
List<SocialChatGiftRes> _gifts = [];
bool _loadingGifts = true;
String? _playingGiftSvgaUrl;
int _giftEffectSerial = 0;
@override
void initState() {
super.initState();
_primeProfile();
_loadProfile();
_loadGiftWall();
}
void _primeProfile() {
final currentProfile =
Provider.of<SocialChatUserProfileManager>(
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<void> _loadGiftWall() async {
final appGeneralManager = Provider.of<SCAppGeneralManager>(
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<List<SocialChatGiftRes>> _resolveGiftWallMeta(
List<SocialChatGiftRes> 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 = <String>[
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<String?> 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<int>(0, (sum, gift) => sum + _giftQuantity(gift));
}
num get _totalValue {
return _gifts.fold<num>(
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<void> _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<MovieEntity> _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(),
),
);
}
}