655 lines
18 KiB
Dart
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(),
|
|
),
|
|
);
|
|
}
|
|
}
|