425 lines
12 KiB
Dart
425 lines
12 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.maxWrapRows,
|
|
this.scrollLastWrapRow = 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 int? maxWrapRows;
|
|
final bool scrollLastWrapRow;
|
|
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) {
|
|
if (snapshot.hasData) {
|
|
return _buildBadges(_remoteBadges(snapshot.data), stripHeight);
|
|
}
|
|
return _buildBadges(_fallbackBadges, 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) {
|
|
if (widget.maxWrapRows != null) {
|
|
return _buildLimitedWrapBadges(
|
|
badges,
|
|
stripHeight: stripHeight,
|
|
badgeHeight: badgeHeight,
|
|
spacing: spacing,
|
|
);
|
|
}
|
|
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 _buildLimitedWrapBadges(
|
|
List<WearBadge> badges, {
|
|
required double stripHeight,
|
|
required double badgeHeight,
|
|
required double spacing,
|
|
}) {
|
|
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<double>(
|
|
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 = <Widget>[];
|
|
|
|
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, 5.w),
|
|
);
|
|
},
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildBadgeRow(
|
|
List<WearBadge> 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<WearBadge> 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<WearBadge> badges,
|
|
double badgeHeight,
|
|
double spacing,
|
|
) {
|
|
if (badges.isEmpty) {
|
|
return 0;
|
|
}
|
|
final badgeWidth = badges.fold<double>(
|
|
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<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;
|
|
}
|
|
|
|
List<Widget> _withVerticalSpacing(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(height: spacing));
|
|
}
|
|
spaced.add(children[i]);
|
|
}
|
|
return spaced;
|
|
}
|
|
}
|
|
|
|
class _BadgeFutureCacheEntry {
|
|
const _BadgeFutureCacheEntry(this.future, this.createdAt);
|
|
|
|
final Future<SCUserBadgeRes> future;
|
|
final DateTime createdAt;
|
|
}
|