234 lines
6.8 KiB
Dart
234 lines
6.8 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';
|
|
|
|
enum SCUserBadgeDisplayScope { short, long, all }
|
|
|
|
class SCUserBadgeStrip extends StatefulWidget {
|
|
const SCUserBadgeStrip({
|
|
super.key,
|
|
this.userId,
|
|
this.fallbackBadges = const <WearBadge>[],
|
|
this.displayScope = SCUserBadgeDisplayScope.short,
|
|
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 SCUserBadgeDisplayScope displayScope;
|
|
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 = _remoteBadges(snapshot.data);
|
|
final badges = remoteBadges.isNotEmpty ? remoteBadges : _fallbackBadges;
|
|
return _buildBadges(badges, stripHeight);
|
|
},
|
|
);
|
|
}
|
|
|
|
List<WearBadge> get _fallbackBadges {
|
|
return widget.fallbackBadges
|
|
.where(_matchesDisplayScope)
|
|
.where((badge) => _badgeUrl(badge).isNotEmpty)
|
|
.toList();
|
|
}
|
|
|
|
List<WearBadge> _remoteBadges(SCUserBadgeRes? data) {
|
|
if (data == null) {
|
|
return const <WearBadge>[];
|
|
}
|
|
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<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;
|
|
}
|