yumi-flutter/lib/ui_kit/widgets/badge/sc_user_badge_strip.dart
2026-05-16 01:04:49 +08:00

203 lines
5.9 KiB
Dart

import 'package:flutter/material.dart';
import 'package:flutter_screenutil/flutter_screenutil.dart';
import 'package:yumi/shared/business_logic/models/res/login_res.dart';
import 'package:yumi/shared/business_logic/models/res/sc_user_badge_res.dart';
import 'package:yumi/shared/data_sources/sources/local/user_manager.dart';
import 'package:yumi/shared/data_sources/sources/repositories/sc_user_repository_impl.dart';
import 'package:yumi/ui_kit/components/sc_compontent.dart';
class SCUserBadgeStrip extends StatefulWidget {
const SCUserBadgeStrip({
super.key,
this.userId,
this.fallbackBadges = const <WearBadge>[],
this.height,
this.badgeHeight,
this.longBadgeWidth,
this.spacing,
this.maxBadges,
this.wrap = false,
this.reserveSpace = false,
this.alignment = WrapAlignment.center,
});
final String? userId;
final List<WearBadge> fallbackBadges;
final double? height;
final double? badgeHeight;
final double? longBadgeWidth;
final double? spacing;
final int? maxBadges;
final bool wrap;
final bool reserveSpace;
final WrapAlignment alignment;
@override
State<SCUserBadgeStrip> createState() => _SCUserBadgeStripState();
}
class _SCUserBadgeStripState extends State<SCUserBadgeStrip> {
static const Duration _cacheTtl = Duration(minutes: 1);
static final Map<String, _BadgeFutureCacheEntry> _badgeFutures = {};
Future<SCUserBadgeRes>? _future;
@override
void initState() {
super.initState();
_future = _resolveFuture();
}
@override
void didUpdateWidget(covariant SCUserBadgeStrip oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.userId != widget.userId) {
_future = _resolveFuture();
}
}
Future<SCUserBadgeRes>? _resolveFuture() {
final userId = _normalizedUserId;
if (userId.isEmpty) {
return null;
}
final currentUserId =
AccountStorage().getCurrentUser()?.userProfile?.id?.trim() ?? "";
final isCurrentUser = currentUserId.isNotEmpty && userId == currentUserId;
final cacheKey = isCurrentUser ? "current:$userId" : "other:$userId";
final now = DateTime.now();
_badgeFutures.removeWhere(
(_, entry) => now.difference(entry.createdAt) >= _cacheTtl,
);
final cacheEntry = _badgeFutures[cacheKey];
if (cacheEntry != null &&
now.difference(cacheEntry.createdAt) < _cacheTtl) {
return cacheEntry.future;
}
final future = () async {
try {
return isCurrentUser
? await SCAccountRepository().currentUserBadges()
: await SCAccountRepository().otherUserBadges(userId);
} catch (_) {
_badgeFutures.remove(cacheKey);
return SCUserBadgeRes.empty(userId: userId);
}
}();
_badgeFutures[cacheKey] = _BadgeFutureCacheEntry(future, now);
return future;
}
String get _normalizedUserId => widget.userId?.trim() ?? "";
@override
Widget build(BuildContext context) {
final stripHeight = widget.height ?? 25.w;
final future = _future;
if (future == null) {
return _buildBadges(_fallbackBadges, stripHeight);
}
return FutureBuilder<SCUserBadgeRes>(
future: future,
builder: (context, snapshot) {
final remoteBadges =
snapshot.data?.displayBadges ?? const <WearBadge>[];
final badges = remoteBadges.isNotEmpty ? remoteBadges : _fallbackBadges;
return _buildBadges(badges, stripHeight);
},
);
}
List<WearBadge> get _fallbackBadges {
return widget.fallbackBadges
.where((badge) => _badgeUrl(badge).isNotEmpty)
.toList();
}
Widget _buildBadges(List<WearBadge> rawBadges, double stripHeight) {
final badges =
widget.maxBadges == null
? rawBadges
: rawBadges.take(widget.maxBadges!).toList();
if (badges.isEmpty) {
return widget.reserveSpace
? SizedBox(height: stripHeight)
: const SizedBox.shrink();
}
final badgeHeight = widget.badgeHeight ?? stripHeight;
final spacing = widget.spacing ?? 5.w;
final children =
badges
.map((badge) => _buildBadge(badge, badgeHeight: badgeHeight))
.toList();
if (widget.wrap) {
return SizedBox(
height: stripHeight,
child: Center(
child: Wrap(
alignment: widget.alignment,
spacing: spacing,
runSpacing: 5.w,
children: children,
),
),
);
}
return SizedBox(
height: stripHeight,
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
physics: const BouncingScrollPhysics(),
child: Row(children: _withSpacing(children, spacing)),
),
);
}
Widget _buildBadge(WearBadge badge, {required double badgeHeight}) {
final type = badge.type?.trim().toUpperCase() ?? "";
final sourceType = badge.sourceType?.trim().toUpperCase() ?? "";
final isLongBadge = type == "LONG" || type == "VIP" || sourceType == "VIP";
final width =
isLongBadge
? (widget.longBadgeWidth ?? badgeHeight * 2.2)
: badgeHeight;
return netImage(
url: _badgeUrl(badge),
width: width,
height: badgeHeight,
fit: BoxFit.contain,
noDefaultImg: true,
loadingWidget: const SizedBox.shrink(),
errorWidget: const SizedBox.shrink(),
);
}
String _badgeUrl(WearBadge badge) {
return (badge.selectUrl ?? "").trim();
}
List<Widget> _withSpacing(List<Widget> children, double spacing) {
if (children.length < 2) {
return children;
}
final spaced = <Widget>[];
for (var i = 0; i < children.length; i++) {
if (i > 0) {
spaced.add(SizedBox(width: spacing));
}
spaced.add(children[i]);
}
return spaced;
}
}
class _BadgeFutureCacheEntry {
const _BadgeFutureCacheEntry(this.future, this.createdAt);
final Future<SCUserBadgeRes> future;
final DateTime createdAt;
}