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'; enum SCUserBadgeDisplayScope { short, long, all } class SCUserBadgeStrip extends StatefulWidget { const SCUserBadgeStrip({ super.key, this.userId, this.fallbackBadges = const [], this.displayScope = SCUserBadgeDisplayScope.short, this.height, this.badgeHeight, this.longBadgeWidth, this.spacing, this.runSpacing, this.maxBadges, this.wrap = false, this.maxWrapRows, this.scrollLastWrapRow = false, this.reserveSpace = false, this.alignment = WrapAlignment.center, }); final String? userId; final List fallbackBadges; final SCUserBadgeDisplayScope displayScope; final double? height; final double? badgeHeight; final double? longBadgeWidth; final double? spacing; final double? runSpacing; final int? maxBadges; final bool wrap; final int? maxWrapRows; final bool scrollLastWrapRow; final bool reserveSpace; final WrapAlignment alignment; @override State createState() => _SCUserBadgeStripState(); } class _SCUserBadgeStripState extends State { static const Duration _cacheTtl = Duration(minutes: 1); static final Map _badgeFutures = {}; Future? _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? _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( future: future, builder: (context, snapshot) { if (snapshot.hasData) { return _buildBadges(_remoteBadges(snapshot.data), stripHeight); } return _buildBadges(_fallbackBadges, stripHeight); }, ); } List get _fallbackBadges { return widget.fallbackBadges .where(_matchesDisplayScope) .where((badge) => _badgeUrl(badge).isNotEmpty) .toList(); } List _remoteBadges(SCUserBadgeRes? data) { if (data == null) { return const []; } switch (widget.displayScope) { case SCUserBadgeDisplayScope.long: return data.longDisplayBadges; case SCUserBadgeDisplayScope.all: return data.allBadges .where((badge) => _badgeUrl(badge).isNotEmpty) .toList(); case SCUserBadgeDisplayScope.short: return data.shortDisplayBadges; } } bool _matchesDisplayScope(WearBadge badge) { switch (widget.displayScope) { case SCUserBadgeDisplayScope.long: return SCUserBadgeRes.isLongDisplayBadge(badge); case SCUserBadgeDisplayScope.all: return true; case SCUserBadgeDisplayScope.short: return SCUserBadgeRes.isShortDisplayBadge(badge); } } Widget _buildBadges(List 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 runSpacing = widget.runSpacing ?? 5.w; final children = badges .map((badge) => _buildBadge(badge, badgeHeight: badgeHeight)) .toList(); if (widget.wrap) { if (widget.maxWrapRows != null) { return _buildLimitedWrapBadges( badges, stripHeight: stripHeight, badgeHeight: badgeHeight, spacing: spacing, runSpacing: runSpacing, ); } return SizedBox( height: stripHeight, child: Center( child: Wrap( alignment: widget.alignment, spacing: spacing, runSpacing: runSpacing, children: children, ), ), ); } return SizedBox( height: stripHeight, child: SingleChildScrollView( scrollDirection: Axis.horizontal, physics: const BouncingScrollPhysics(), child: Row(children: _withSpacing(children, spacing)), ), ); } Widget _buildLimitedWrapBadges( List badges, { required double stripHeight, required double badgeHeight, required double spacing, required double runSpacing, }) { final maxRows = widget.maxWrapRows ?? 1; if (maxRows <= 1) { return _buildScrollableBadgeRow( badges, stripHeight: stripHeight, badgeHeight: badgeHeight, spacing: spacing, ); } return SizedBox( height: stripHeight, child: LayoutBuilder( builder: (context, constraints) { final maxWidth = constraints.maxWidth.isFinite ? constraints.maxWidth : _badgesTotalWidth(badges, badgeHeight, spacing); final widestBadge = badges .map((badge) => _badgeWidth(badge, badgeHeight)) .fold( 0, (current, width) => width > current ? width : current, ); final perRow = widestBadge <= 0 ? badges.length : ((maxWidth + spacing) / (widestBadge + spacing)) .floor() .clamp(1, badges.length); final firstRows = maxRows - 1; final fixedCount = (perRow * firstRows).clamp(0, badges.length); final fixedBadges = badges.take(fixedCount).toList(); final lastRowBadges = badges.skip(fixedCount).toList(); final rows = []; for (var start = 0; start < fixedBadges.length; start += perRow) { final end = start + perRow > fixedBadges.length ? fixedBadges.length : start + perRow; rows.add( _buildBadgeRow( fixedBadges.sublist(start, end), badgeHeight: badgeHeight, spacing: spacing, ), ); } if (lastRowBadges.isNotEmpty) { rows.add( widget.scrollLastWrapRow ? _buildScrollableBadgeRow( lastRowBadges, stripHeight: badgeHeight, badgeHeight: badgeHeight, spacing: spacing, ) : _buildBadgeRow( lastRowBadges.take(perRow).toList(), badgeHeight: badgeHeight, spacing: spacing, ), ); } if (rows.isEmpty) { return const SizedBox.shrink(); } return Column( mainAxisAlignment: MainAxisAlignment.center, children: _withVerticalSpacing(rows, runSpacing), ); }, ), ); } Widget _buildBadgeRow( List badges, { required double badgeHeight, required double spacing, }) { return SizedBox( height: badgeHeight, child: Row( mainAxisSize: MainAxisSize.min, mainAxisAlignment: _rowAlignment, children: _withSpacing( badges .map((badge) => _buildBadge(badge, badgeHeight: badgeHeight)) .toList(), spacing, ), ), ); } Widget _buildScrollableBadgeRow( List badges, { required double stripHeight, required double badgeHeight, required double spacing, }) { return SizedBox( height: stripHeight, child: SingleChildScrollView( scrollDirection: Axis.horizontal, physics: const BouncingScrollPhysics(), child: Row( mainAxisSize: MainAxisSize.min, children: _withSpacing( badges .map((badge) => _buildBadge(badge, badgeHeight: badgeHeight)) .toList(), spacing, ), ), ), ); } Widget _buildBadge(WearBadge badge, {required double badgeHeight}) { final width = _badgeWidth(badge, badgeHeight); return netImage( url: _badgeUrl(badge), width: width, height: badgeHeight, fit: BoxFit.contain, noDefaultImg: true, loadingWidget: const SizedBox.shrink(), errorWidget: const SizedBox.shrink(), ); } double _badgeWidth(WearBadge badge, double badgeHeight) { final type = badge.type?.trim().toUpperCase() ?? ""; final sourceType = badge.sourceType?.trim().toUpperCase() ?? ""; final isLongBadge = type == "LONG" || type == "VIP" || sourceType == "VIP"; return isLongBadge ? (widget.longBadgeWidth ?? badgeHeight * 2.2) : badgeHeight; } double _badgesTotalWidth( List badges, double badgeHeight, double spacing, ) { if (badges.isEmpty) { return 0; } final badgeWidth = badges.fold( 0, (sum, badge) => sum + _badgeWidth(badge, badgeHeight), ); return badgeWidth + spacing * (badges.length - 1); } MainAxisAlignment get _rowAlignment { switch (widget.alignment) { case WrapAlignment.start: return MainAxisAlignment.start; case WrapAlignment.end: return MainAxisAlignment.end; case WrapAlignment.spaceBetween: return MainAxisAlignment.spaceBetween; case WrapAlignment.spaceAround: return MainAxisAlignment.spaceAround; case WrapAlignment.spaceEvenly: return MainAxisAlignment.spaceEvenly; case WrapAlignment.center: return MainAxisAlignment.center; } } String _badgeUrl(WearBadge badge) { return (badge.selectUrl ?? "").trim(); } List _withSpacing(List children, double spacing) { if (children.length < 2) { return children; } final spaced = []; for (var i = 0; i < children.length; i++) { if (i > 0) { spaced.add(SizedBox(width: spacing)); } spaced.add(children[i]); } return spaced; } List _withVerticalSpacing(List children, double spacing) { if (children.length < 2) { return children; } final spaced = []; for (var i = 0; i < children.length; i++) { if (i > 0) { spaced.add(SizedBox(height: spacing)); } spaced.add(children[i]); } return spaced; } } class _BadgeFutureCacheEntry { const _BadgeFutureCacheEntry(this.future, this.createdAt); final Future future; final DateTime createdAt; }