修改火箭

This commit is contained in:
zhx 2026-05-16 04:39:07 +08:00
parent 671f925232
commit 409d2fc898
7 changed files with 537 additions and 289 deletions

View File

@ -32,7 +32,6 @@ import 'package:yumi/services/audio/rtm_manager.dart';
import 'package:yumi/services/auth/user_profile_manager.dart'; import 'package:yumi/services/auth/user_profile_manager.dart';
import 'package:yumi/services/general/sc_app_general_manager.dart'; import 'package:yumi/services/general/sc_app_general_manager.dart';
import 'package:yumi/ui_kit/widgets/badge/sc_user_badge_strip.dart'; import 'package:yumi/ui_kit/widgets/badge/sc_user_badge_strip.dart';
import 'package:yumi/ui_kit/widgets/props/sc_data_card_resource_view.dart';
import '../../../app/constants/sc_screen.dart'; import '../../../app/constants/sc_screen.dart';
import '../../../shared/business_logic/models/res/sc_user_counter_res.dart'; import '../../../shared/business_logic/models/res/sc_user_counter_res.dart';
@ -580,49 +579,8 @@ class _PersonDetailPageState extends State<PersonDetailPage> {
); );
} }
Widget _buildProfileDataCardBackground(PropsResources? dataCard) { Widget _buildProfileDataCardBackground(PropsResources? _) {
if (dataCard == null) { return const ColoredBox(color: _profileBg);
return const ColoredBox(color: _profileBg);
}
return Stack(
fit: StackFit.expand,
children: [
SCDataCardResourceView(
resource: dataCard,
fit: BoxFit.cover,
fallback: DecoratedBox(decoration: _defaultDataCardDecoration()),
),
DecoratedBox(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Colors.black.withValues(alpha: 0.26),
_profileBg.withValues(alpha: 0.54),
_profileBg.withValues(alpha: 0.78),
],
stops: const [0, 0.45, 1],
),
),
),
],
);
}
BoxDecoration _defaultDataCardDecoration() {
return const BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Color(0xff18F2B1),
Color(0xffE4FFF6),
Color(0xffF7FFFC),
Color(0xffFFFFFF),
],
),
);
} }
PropsResources? _activeDataCard(SocialChatUserProfile? profile) { PropsResources? _activeDataCard(SocialChatUserProfile? profile) {

View File

@ -1,6 +1,7 @@
import 'dart:async'; import 'dart:async';
import 'package:dio/dio.dart'; import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart'; import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
import 'package:yumi/app_localizations.dart'; import 'package:yumi/app_localizations.dart';
@ -343,6 +344,7 @@ class RealTimeCommunicationManager extends ChangeNotifier {
Timer? _onlineUsersPollingTimer; Timer? _onlineUsersPollingTimer;
Timer? _roomEntryEffectTimer; Timer? _roomEntryEffectTimer;
Timer? _roomRocketLaunchAnimationTimer; Timer? _roomRocketLaunchAnimationTimer;
ValueNotifier<String?>? _roomRocketLaunchTopAvatarNotifier;
Timer? _roomRocketGiftRefreshTimer; Timer? _roomRocketGiftRefreshTimer;
final List<Timer> _roomRocketPostLaunchRefreshTimers = []; final List<Timer> _roomRocketPostLaunchRefreshTimers = [];
Timer? _roomRedPacketPresenceTimer; Timer? _roomRedPacketPresenceTimer;
@ -490,6 +492,7 @@ class RealTimeCommunicationManager extends ChangeNotifier {
_roomEntryEffectTimer = null; _roomEntryEffectTimer = null;
_roomRocketLaunchAnimationTimer?.cancel(); _roomRocketLaunchAnimationTimer?.cancel();
_roomRocketLaunchAnimationTimer = null; _roomRocketLaunchAnimationTimer = null;
_releaseRoomRocketLaunchTopAvatarNotifier();
_roomRocketGiftRefreshTimer?.cancel(); _roomRocketGiftRefreshTimer?.cancel();
_roomRocketGiftRefreshTimer = null; _roomRocketGiftRefreshTimer = null;
_cancelRoomRocketPostLaunchStatusRefresh(); _cancelRoomRocketPostLaunchStatusRefresh();
@ -2980,6 +2983,7 @@ class RealTimeCommunicationManager extends ChangeNotifier {
} }
_roomRocketLaunchAnimationTimer?.cancel(); _roomRocketLaunchAnimationTimer?.cancel();
_releaseRoomRocketLaunchTopAvatarNotifier();
if (shouldShowRoomVisualEffects && isVoiceRoomRouteVisible) { if (shouldShowRoomVisualEffects && isVoiceRoomRouteVisible) {
final animationUrl = final animationUrl =
@ -2997,6 +3001,7 @@ class RealTimeCommunicationManager extends ChangeNotifier {
_roomRocketLaunchAnimationTimer = Timer(launch.displayDuration, () { _roomRocketLaunchAnimationTimer = Timer(launch.displayDuration, () {
_roomRocketLaunchAnimationTimer = null; _roomRocketLaunchAnimationTimer = null;
SmartDialog.dismiss(tag: _roomRocketPagLaunchDialogTag); SmartDialog.dismiss(tag: _roomRocketPagLaunchDialogTag);
_releaseRoomRocketLaunchTopAvatarNotifier();
unawaited(refreshRoomRocketStatus(roomId: launch.roomId)); unawaited(refreshRoomRocketStatus(roomId: launch.roomId));
scheduleRoomRocketPostLaunchStatusRefresh(roomId: launch.roomId); scheduleRoomRocketPostLaunchStatusRefresh(roomId: launch.roomId);
}); });
@ -3022,6 +3027,11 @@ class RealTimeCommunicationManager extends ChangeNotifier {
required RoomRocketLaunchBroadcastMessage launch, required RoomRocketLaunchBroadcastMessage launch,
}) { }) {
if (RoomRocketPagEffectOverlay.isPag(animationUrl)) { if (RoomRocketPagEffectOverlay.isPag(animationUrl)) {
final topAvatarNotifier = ValueNotifier<String?>(
_roomRocketTop1AvatarUrl(launch.safeLevel),
);
_roomRocketLaunchTopAvatarNotifier = topAvatarNotifier;
unawaited(_loadRoomRocketTop1Avatar(launch, topAvatarNotifier));
SmartDialog.dismiss(tag: _roomRocketPagLaunchDialogTag); SmartDialog.dismiss(tag: _roomRocketPagLaunchDialogTag);
SmartDialog.show( SmartDialog.show(
tag: _roomRocketPagLaunchDialogTag, tag: _roomRocketPagLaunchDialogTag,
@ -3033,11 +3043,12 @@ class RealTimeCommunicationManager extends ChangeNotifier {
builder: builder:
(_) => RoomRocketPagEffectOverlay( (_) => RoomRocketPagEffectOverlay(
resource: animationUrl, resource: animationUrl,
topAvatarUrl: _roomRocketTop1AvatarUrl(), topAvatarUrlListenable: topAvatarNotifier,
onCompleted: () { onCompleted: () {
SmartDialog.dismiss(tag: _roomRocketPagLaunchDialogTag); SmartDialog.dismiss(tag: _roomRocketPagLaunchDialogTag);
_roomRocketLaunchAnimationTimer?.cancel(); _roomRocketLaunchAnimationTimer?.cancel();
_roomRocketLaunchAnimationTimer = null; _roomRocketLaunchAnimationTimer = null;
_releaseRoomRocketLaunchTopAvatarNotifier(topAvatarNotifier);
unawaited(refreshRoomRocketStatus(roomId: launch.roomId)); unawaited(refreshRoomRocketStatus(roomId: launch.roomId));
scheduleRoomRocketPostLaunchStatusRefresh( scheduleRoomRocketPostLaunchStatusRefresh(
roomId: launch.roomId, roomId: launch.roomId,
@ -3054,8 +3065,21 @@ class RealTimeCommunicationManager extends ChangeNotifier {
); );
} }
String? _roomRocketTop1AvatarUrl() { String? _roomRocketTop1AvatarUrl(int level) {
final kings = roomRocketStatus?.rocketKings ?? const []; final kings =
roomRocketStatus?.rocketKingsForLevel(level) ??
const <SCRoomRocketKingUserRes>[];
return _roomRocketTop1AvatarUrlFromUsers(kings);
}
String? _roomRocketTop1AvatarUrlFromKing(SCRoomRocketKingRes king) {
final source = king.podium.isNotEmpty ? king.podium : king.displayRecords;
return _roomRocketTop1AvatarUrlFromUsers(source);
}
String? _roomRocketTop1AvatarUrlFromUsers(
List<SCRoomRocketKingUserRes> kings,
) {
if (kings.isEmpty) { if (kings.isEmpty) {
return null; return null;
} }
@ -3072,6 +3096,90 @@ class RealTimeCommunicationManager extends ChangeNotifier {
return null; return null;
} }
Future<void> _loadRoomRocketTop1Avatar(
RoomRocketLaunchBroadcastMessage launch,
ValueNotifier<String?> notifier,
) async {
final roomId = launch.roomId.trim();
if (roomId.isEmpty) {
return;
}
try {
_roomRocketDebug(
'request launch top1 avatar roomId=$roomId '
'level=${launch.safeLevel} roundNo=${launch.roundNo}',
);
var king = await SCChatRoomRepository().roomRocketKing(
roomId,
level: launch.safeLevel,
roundNo: launch.roundNo,
cursor: 1,
limit: 3,
);
var avatar = _roomRocketTop1AvatarUrlFromKing(king);
if (avatar == null && launch.roundNo > 0) {
_roomRocketDebug(
'launch top1 avatar retry without roundNo '
'level=${launch.safeLevel} roundNo=${launch.roundNo}',
);
king = await SCChatRoomRepository().roomRocketKing(
roomId,
level: launch.safeLevel,
cursor: 1,
limit: 3,
);
avatar = _roomRocketTop1AvatarUrlFromKing(king);
}
if (_roomRocketLaunchTopAvatarNotifier != notifier) {
return;
}
_roomRocketDebug(
'launch top1 avatar loaded level=${launch.safeLevel} '
'roundNo=${launch.roundNo} avatar=${_shortRoomRocketDebugUrl(avatar)} '
'podium=${king.podium.length} records=${king.records.length}',
);
if (avatar != null && avatar != notifier.value) {
notifier.value = avatar;
}
} catch (error) {
_roomRocketDebug(
'launch top1 avatar load failed level=${launch.safeLevel} '
'roundNo=${launch.roundNo}: $error',
);
}
}
void _releaseRoomRocketLaunchTopAvatarNotifier([
ValueNotifier<String?>? expected,
]) {
final notifier = _roomRocketLaunchTopAvatarNotifier;
if (notifier == null || (expected != null && notifier != expected)) {
return;
}
_roomRocketLaunchTopAvatarNotifier = null;
WidgetsBinding.instance.addPostFrameCallback((_) {
notifier.dispose();
});
}
void _roomRocketDebug(String message) {
if (!kDebugMode) {
return;
}
debugPrint('[RoomRocket] $message');
}
String _shortRoomRocketDebugUrl(String? url) {
final value = url?.trim() ?? '';
if (value.isEmpty) {
return '-';
}
if (value.length <= 96) {
return value;
}
return '${value.substring(0, 48)}...${value.substring(value.length - 24)}';
}
RoomRocketRewardUserProfile? roomRocketRewardUserProfileForRecord( RoomRocketRewardUserProfile? roomRocketRewardUserProfileForRecord(
SCRoomRocketRewardRecordRes record, SCRoomRocketRewardRecordRes record,
) { ) {
@ -3620,6 +3728,7 @@ class RealTimeCommunicationManager extends ChangeNotifier {
_roomEntryEffectTimer = null; _roomEntryEffectTimer = null;
_roomRocketLaunchAnimationTimer?.cancel(); _roomRocketLaunchAnimationTimer?.cancel();
_roomRocketLaunchAnimationTimer = null; _roomRocketLaunchAnimationTimer = null;
_releaseRoomRocketLaunchTopAvatarNotifier();
_roomRocketGiftRefreshTimer?.cancel(); _roomRocketGiftRefreshTimer?.cancel();
_roomRocketGiftRefreshTimer = null; _roomRocketGiftRefreshTimer = null;
_cancelRoomRocketPostLaunchStatusRefresh(); _cancelRoomRocketPostLaunchStatusRefresh();

View File

@ -23,6 +23,8 @@ class SCDataCardResourceView extends StatelessWidget {
this.fit = BoxFit.cover, this.fit = BoxFit.cover,
this.loop = true, this.loop = true,
this.fallback, this.fallback,
this.showCoverFallback = true,
this.allowCoverAsResource = true,
}); });
final PropsResources? resource; final PropsResources? resource;
@ -33,6 +35,8 @@ class SCDataCardResourceView extends StatelessWidget {
final BoxFit fit; final BoxFit fit;
final bool loop; final bool loop;
final Widget? fallback; final Widget? fallback;
final bool showCoverFallback;
final bool allowCoverAsResource;
static bool isSupported(String? url) { static bool isSupported(String? url) {
return _isSvga(url) || return _isSvga(url) ||
@ -50,15 +54,22 @@ class SCDataCardResourceView extends StatelessWidget {
final resolvedHeight = _resolvedSize(height, constraints.maxHeight); final resolvedHeight = _resolvedSize(height, constraints.maxHeight);
final cover = _firstNonBlank([coverUrl, resource?.cover]); final cover = _firstNonBlank([coverUrl, resource?.cover]);
final source = _firstNonBlank([sourceUrl, resource?.sourceUrl]); final source = _firstNonBlank([sourceUrl, resource?.sourceUrl]);
final resourceUrl = source.isNotEmpty ? source : cover; final resourceUrl =
source.isNotEmpty || !allowCoverAsResource ? source : cover;
final emptyFallback = SizedBox(
width: resolvedWidth,
height: resolvedHeight,
);
final fallbackView = final fallbackView =
fallback ?? fallback ??
_buildImage( (showCoverFallback
cover, ? _buildImage(
width: resolvedWidth, cover,
height: resolvedHeight, width: resolvedWidth,
fit: fit, height: resolvedHeight,
); fit: fit,
)
: emptyFallback);
if (resourceUrl.isEmpty) { if (resourceUrl.isEmpty) {
return fallbackView; return fallbackView;
@ -330,10 +341,12 @@ class _LoopingDataCardVapVideo extends StatefulWidget {
class _LoopingDataCardVapVideoState extends State<_LoopingDataCardVapVideo> { class _LoopingDataCardVapVideoState extends State<_LoopingDataCardVapVideo> {
static const int _legacyRgbLeftAlphaRightMode = 3; static const int _legacyRgbLeftAlphaRightMode = 3;
static const int _longRunningLoopCount = 999999;
VapController? _controller; VapController? _controller;
int _playToken = 0; int _playToken = 0;
bool _failed = false; bool _failed = false;
bool _started = false;
@override @override
void didUpdateWidget(covariant _LoopingDataCardVapVideo oldWidget) { void didUpdateWidget(covariant _LoopingDataCardVapVideo oldWidget) {
@ -343,8 +356,11 @@ class _LoopingDataCardVapVideoState extends State<_LoopingDataCardVapVideo> {
oldWidget.fit != widget.fit) { oldWidget.fit != widget.fit) {
_playToken++; _playToken++;
_stop(); _stop();
if (_failed) { if (_failed || _started) {
setState(() => _failed = false); setState(() {
_failed = false;
_started = false;
});
} }
_play(); _play();
} }
@ -364,8 +380,11 @@ class _LoopingDataCardVapVideoState extends State<_LoopingDataCardVapVideo> {
onFailed: (_, __, ___) => _markFailed(), onFailed: (_, __, ___) => _markFailed(),
onVideoComplete: _handleVideoComplete, onVideoComplete: _handleVideoComplete,
onVideoStart: () { onVideoStart: () {
if (mounted && _failed) { if (mounted && (_failed || !_started)) {
setState(() => _failed = false); setState(() {
_failed = false;
_started = true;
});
} }
}, },
); );
@ -404,7 +423,7 @@ class _LoopingDataCardVapVideoState extends State<_LoopingDataCardVapVideo> {
await controller.setMute(true); await controller.setMute(true);
await controller.enableVersion1(true); await controller.enableVersion1(true);
await controller.setVideoMode(_legacyRgbLeftAlphaRightMode); await controller.setVideoMode(_legacyRgbLeftAlphaRightMode);
await controller.setLoop(widget.loop ? -1 : 0); await controller.setLoop(widget.loop ? _longRunningLoopCount : 0);
await controller.setScaleType(_scaleTypeForFit(widget.fit).key); await controller.setScaleType(_scaleTypeForFit(widget.fit).key);
if (!_isCurrent(token, controller)) { if (!_isCurrent(token, controller)) {
return; return;
@ -465,7 +484,10 @@ class _LoopingDataCardVapVideoState extends State<_LoopingDataCardVapVideo> {
if (!mounted || _failed) { if (!mounted || _failed) {
return; return;
} }
setState(() => _failed = true); setState(() {
_failed = true;
_started = false;
});
} }
ScaleType _scaleTypeForFit(BoxFit fit) { ScaleType _scaleTypeForFit(BoxFit fit) {
@ -492,11 +514,14 @@ class _LoopingDataCardVapVideoState extends State<_LoopingDataCardVapVideo> {
child: Stack( child: Stack(
fit: StackFit.expand, fit: StackFit.expand,
children: [ children: [
widget.fallback, if (_failed || !_started)
widget.fallback
else
const ColoredBox(color: Colors.black),
if (!_failed) if (!_failed)
VapView( VapView(
scaleType: _scaleTypeForFit(widget.fit), scaleType: _scaleTypeForFit(widget.fit),
repeat: widget.loop ? -1 : 0, repeat: widget.loop ? _longRunningLoopCount : 0,
mute: true, mute: true,
onViewCreated: _handleViewCreated, onViewCreated: _handleViewCreated,
), ),

View File

@ -11,17 +11,21 @@ import 'package:yumi/ui_kit/components/custom_cached_image.dart';
import 'package:yumi/ui_kit/components/sc_rotating_dots_loading.dart'; import 'package:yumi/ui_kit/components/sc_rotating_dots_loading.dart';
const Duration _pagMemoryTtl = Duration(minutes: 5); const Duration _pagMemoryTtl = Duration(minutes: 5);
const Size _roomRocketFallbackPagRawSize = Size(750, 1624);
const Rect _roomRocketTopAvatarRawRect = Rect.fromLTWH(291, 164, 168, 168);
class RoomRocketPagEffectOverlay extends StatefulWidget { class RoomRocketPagEffectOverlay extends StatefulWidget {
const RoomRocketPagEffectOverlay({ const RoomRocketPagEffectOverlay({
super.key, super.key,
required this.resource, required this.resource,
this.topAvatarUrl, this.topAvatarUrl,
this.topAvatarUrlListenable,
this.onCompleted, this.onCompleted,
}); });
final String resource; final String resource;
final String? topAvatarUrl; final String? topAvatarUrl;
final ValueListenable<String?>? topAvatarUrlListenable;
final VoidCallback? onCompleted; final VoidCallback? onCompleted;
static bool isPag(String? resource) { static bool isPag(String? resource) {
@ -50,6 +54,7 @@ class RoomRocketPagEffectOverlay extends StatefulWidget {
class _RoomRocketPagEffectOverlayState class _RoomRocketPagEffectOverlayState
extends State<RoomRocketPagEffectOverlay> { extends State<RoomRocketPagEffectOverlay> {
bool _completed = false; bool _completed = false;
Size? _pagRawSize;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -65,10 +70,7 @@ class _RoomRocketPagEffectOverlayState
child: Stack( child: Stack(
clipBehavior: Clip.none, clipBehavior: Clip.none,
children: [ children: [
_RoomRocketPagTopAvatar( _buildTopAvatar(size),
avatarUrl: widget.topAvatarUrl,
canvasSize: size,
),
Positioned.fill( Positioned.fill(
child: _RoomRocketPagView( child: _RoomRocketPagView(
resource: resource, resource: resource,
@ -79,6 +81,7 @@ class _RoomRocketPagEffectOverlayState
displayScale: 1, displayScale: 1,
clipBehavior: Clip.hardEdge, clipBehavior: Clip.hardEdge,
memoryTtl: _pagMemoryTtl, memoryTtl: _pagMemoryTtl,
onReady: _updatePagRawSize,
onCompleted: _complete, onCompleted: _complete,
defaultBuilder: defaultBuilder:
(_) => const Center(child: SCRotatingDotsLoading()), (_) => const Center(child: SCRotatingDotsLoading()),
@ -99,16 +102,48 @@ class _RoomRocketPagEffectOverlayState
_completed = true; _completed = true;
widget.onCompleted?.call(); widget.onCompleted?.call();
} }
void _updatePagRawSize(Size rawSize) {
if (_pagRawSize == rawSize) {
return;
}
setState(() => _pagRawSize = rawSize);
}
Widget _buildTopAvatar(Size canvasSize) {
final listenable = widget.topAvatarUrlListenable;
if (listenable == null) {
return _RoomRocketPagTopAvatar(
avatarUrl: widget.topAvatarUrl,
canvasSize: canvasSize,
pagRawSize: _pagRawSize,
);
}
return ValueListenableBuilder<String?>(
valueListenable: listenable,
builder:
(context, avatarUrl, _) => _RoomRocketPagTopAvatar(
avatarUrl: _firstNonBlankPagAvatar([
avatarUrl,
widget.topAvatarUrl,
]),
canvasSize: canvasSize,
pagRawSize: _pagRawSize,
),
);
}
} }
class _RoomRocketPagTopAvatar extends StatelessWidget { class _RoomRocketPagTopAvatar extends StatelessWidget {
const _RoomRocketPagTopAvatar({ const _RoomRocketPagTopAvatar({
required this.avatarUrl, required this.avatarUrl,
required this.canvasSize, required this.canvasSize,
required this.pagRawSize,
}); });
final String? avatarUrl; final String? avatarUrl;
final Size canvasSize; final Size canvasSize;
final Size? pagRawSize;
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -116,12 +151,13 @@ class _RoomRocketPagTopAvatar extends StatelessWidget {
if (url.isEmpty) { if (url.isEmpty) {
return const SizedBox.shrink(); return const SizedBox.shrink();
} }
final size = (canvasSize.width * 0.15).clamp(46.0, 66.0).toDouble(); final rect = _mapRoomRocketPagRawRectToCoverCanvas(
return Positioned( rawRect: _roomRocketTopAvatarRawRect,
left: (canvasSize.width - size) / 2, rawSize: pagRawSize ?? _roomRocketFallbackPagRawSize,
top: canvasSize.height * 0.098, canvasSize: canvasSize,
width: size, );
height: size, return Positioned.fromRect(
rect: rect,
child: ClipOval( child: ClipOval(
child: CustomCachedImage( child: CustomCachedImage(
imageUrl: url, imageUrl: url,
@ -200,6 +236,7 @@ class _RoomRocketPagView extends StatefulWidget {
required this.clipBehavior, required this.clipBehavior,
required this.memoryTtl, required this.memoryTtl,
this.underlayBuilder, this.underlayBuilder,
this.onReady,
this.onCompleted, this.onCompleted,
this.defaultBuilder, this.defaultBuilder,
}); });
@ -213,6 +250,7 @@ class _RoomRocketPagView extends StatefulWidget {
final Clip clipBehavior; final Clip clipBehavior;
final Duration memoryTtl; final Duration memoryTtl;
final WidgetBuilder? underlayBuilder; final WidgetBuilder? underlayBuilder;
final ValueChanged<Size>? onReady;
final VoidCallback? onCompleted; final VoidCallback? onCompleted;
final WidgetBuilder? defaultBuilder; final WidgetBuilder? defaultBuilder;
@ -414,6 +452,7 @@ class _RoomRocketPagViewState extends State<_RoomRocketPagView> {
'viewSize=${widget.width?.toStringAsFixed(1)}x' 'viewSize=${widget.width?.toStringAsFixed(1)}x'
'${widget.height?.toStringAsFixed(1)} fit=${widget.fit.name}', '${widget.height?.toStringAsFixed(1)} fit=${widget.fit.name}',
); );
widget.onReady?.call(Size(rawWidth, rawHeight));
unawaited(_debugLayers(state, rawWidth, rawHeight)); unawaited(_debugLayers(state, rawWidth, rawHeight));
if (!_initialized) { if (!_initialized) {
setState(() => _initialized = true); setState(() => _initialized = true);
@ -587,6 +626,55 @@ Timer? _schedulePagMemoryReleaseTimer(
return Timer(memoryTtl, onRelease); return Timer(memoryTtl, onRelease);
} }
@visibleForTesting
Rect debugMapRoomRocketPagRawRectToCoverCanvas({
required Rect rawRect,
required Size rawSize,
required Size canvasSize,
}) {
return _mapRoomRocketPagRawRectToCoverCanvas(
rawRect: rawRect,
rawSize: rawSize,
canvasSize: canvasSize,
);
}
Rect _mapRoomRocketPagRawRectToCoverCanvas({
required Rect rawRect,
required Size rawSize,
required Size canvasSize,
}) {
if (rawSize.width <= 0 ||
rawSize.height <= 0 ||
canvasSize.width <= 0 ||
canvasSize.height <= 0) {
return Rect.zero;
}
final scaleX = canvasSize.width / rawSize.width;
final scaleY = canvasSize.height / rawSize.height;
final scale = scaleX > scaleY ? scaleX : scaleY;
final fittedWidth = rawSize.width * scale;
final fittedHeight = rawSize.height * scale;
final offsetX = (canvasSize.width - fittedWidth) / 2;
final offsetY = (canvasSize.height - fittedHeight) / 2;
return Rect.fromLTWH(
offsetX + rawRect.left * scale,
offsetY + rawRect.top * scale,
rawRect.width * scale,
rawRect.height * scale,
);
}
String? _firstNonBlankPagAvatar(Iterable<String?> values) {
for (final value in values) {
final text = value?.trim() ?? '';
if (text.isNotEmpty) {
return text;
}
}
return null;
}
@visibleForTesting @visibleForTesting
String debugNormalizeRoomRocketPagResource(String resource) { String debugNormalizeRoomRocketPagResource(String resource) {
return _normalizePagResource(resource); return _normalizePagResource(resource);

View File

@ -49,6 +49,11 @@ class RoomUserInfoCard extends StatefulWidget {
class _RoomUserInfoCardState extends State<RoomUserInfoCard> { class _RoomUserInfoCardState extends State<RoomUserInfoCard> {
static const String _leaveMicActionIconAsset = static const String _leaveMicActionIconAsset =
"sc_images/room/sc_icon_room_user_card_leave_mic.png"; "sc_images/room/sc_icon_room_user_card_leave_mic.png";
static const String _defaultProfileCardBackgroundAsset =
"sc_images/room/sc_bg_room_user_profile_card_default.png";
static const double _profileSheetHeightFactor = 0.4;
static const double _dataCardTranslateDownFactor = 0.4;
static const double _defaultBackgroundImageFactor = 0.68;
SocialChatUserProfileManager? userProvider; SocialChatUserProfileManager? userProvider;
String roomId = ""; String roomId = "";
@ -85,7 +90,8 @@ class _RoomUserInfoCardState extends State<RoomUserInfoCard> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final sheetHeight = MediaQuery.of(context).size.height * 0.6; final sheetHeight =
MediaQuery.of(context).size.height * _profileSheetHeightFactor;
return Consumer<SocialChatUserProfileManager>( return Consumer<SocialChatUserProfileManager>(
builder: (context, ref, child) { builder: (context, ref, child) {
@ -123,19 +129,21 @@ class _RoomUserInfoCardState extends State<RoomUserInfoCard> {
height: sheetHeight, height: sheetHeight,
child: ClipRRect( child: ClipRRect(
borderRadius: BorderRadius.vertical(top: Radius.circular(18.w)), borderRadius: BorderRadius.vertical(top: Radius.circular(18.w)),
child: DecoratedBox( child: Stack(
decoration: _sheetDecoration(), children: [
child: Center( Positioned.fill(child: _buildDefaultSheetBackground()),
child: Container( Center(
width: 55.w, child: Container(
height: 55.w, width: 55.w,
decoration: BoxDecoration( height: 55.w,
color: Colors.black26, decoration: BoxDecoration(
borderRadius: BorderRadius.circular(8.w), color: Colors.black26,
borderRadius: BorderRadius.circular(8.w),
),
child: CupertinoActivityIndicator(color: Colors.white24),
), ),
child: CupertinoActivityIndicator(color: Colors.white24),
), ),
), ],
), ),
), ),
); );
@ -160,135 +168,209 @@ class _RoomUserInfoCardState extends State<RoomUserInfoCard> {
ref.userCardInfo?.roomRole != SCRoomRolesType.HOMEOWNER.name && ref.userCardInfo?.roomRole != SCRoomRolesType.HOMEOWNER.name &&
(rtcProvider.isFz() || rtcProvider.isGL()); (rtcProvider.isFz() || rtcProvider.isGL());
final profileContent = _buildProfileContentLayer(
context: context,
ref: ref,
rtcProvider: rtcProvider,
profile: profile,
bottomPadding: bottomPadding,
isSelf: isSelf,
canLeaveMic: canLeaveMic,
currentMicIndex: currentMicIndex,
canShowReport: canShowReport,
canShowSetting: canShowSetting,
);
if (dataCard == null) {
return SizedBox(
width: double.infinity,
height: sheetHeight,
child: ClipRRect(
borderRadius: BorderRadius.vertical(top: Radius.circular(18.w)),
child: Stack(
children: [
Positioned.fill(child: _buildDefaultSheetBackground()),
Positioned.fill(child: profileContent),
],
),
),
);
}
return _buildDataCardFrame(
sheetHeight: sheetHeight,
dataCard: dataCard,
child: profileContent,
);
}
Widget _buildDataCardFrame({
required double sheetHeight,
required PropsResources dataCard,
required Widget child,
}) {
return SizedBox( return SizedBox(
width: double.infinity, width: double.infinity,
height: sheetHeight, height: sheetHeight,
child: ClipRRect( child: Stack(
borderRadius: BorderRadius.vertical(top: Radius.circular(18.w)), clipBehavior: Clip.none,
child: Stack( children: [
children: [ Positioned.fill(
Positioned.fill(child: _buildSheetBackground(dataCard)), child: ClipRRect(
Positioned.fill( borderRadius: BorderRadius.vertical(top: Radius.circular(18.w)),
child: SingleChildScrollView( child: DecoratedBox(decoration: _sheetDecoration()),
physics: const ClampingScrollPhysics(),
padding: EdgeInsets.fromLTRB(
18.w,
16.w,
18.w,
bottomPadding + 10.w,
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
_buildProfileHeader(context, profile, ref),
SizedBox(height: 6.w),
_buildInfoChips(context, profile),
SizedBox(height: 9.w),
_buildBadgeStrip(
profile?.id ?? widget.userId,
ref,
profile,
),
SizedBox(height: 10.w),
_buildActions(
context: context,
ref: ref,
rtcProvider: rtcProvider,
isSelf: isSelf,
canLeaveMic: canLeaveMic,
currentMicIndex: currentMicIndex,
),
],
),
),
), ),
if (canShowReport) ),
Positioned( Positioned(
top: 14.w, left: 0,
right: 16.w, right: 0,
child: _buildHeaderIconButton( top: sheetHeight * _dataCardTranslateDownFactor,
iconAsset: "sc_images/room/sc_icon_user_card_report.png", height: sheetHeight,
onTap: () { child: IgnorePointer(child: _buildSheetBackground(dataCard)),
final route = ),
"${SCMainRoute.report}?type=user&tageId=${widget.userId}"; Positioned.fill(
Navigator.of(context).pop(); child: ClipRRect(
SCNavigatorUtils.push( borderRadius: BorderRadius.vertical(top: Radius.circular(18.w)),
navigatorKey.currentState?.context ?? context, child: child,
route, ),
replace: false, ),
); ],
},
),
),
if (canShowSetting)
Positioned(
top: 14.w,
right: 52.w,
child: _buildHeaderIconButton(
iconAsset:
"sc_images/room/sc_icon_room_user_card_setting.png",
onTap: () {
if (userProvider?.userCardInfo == null) {
return;
}
Navigator.of(context).pop();
showBottomInBottomDialog(
navigatorKey.currentState?.context ?? context,
RoomUserCardSetting(
roomId: roomId,
userCardInfo: userProvider?.userCardInfo?.copyWith(),
),
);
},
),
),
],
),
), ),
); );
} }
Widget _buildProfileContentLayer({
required BuildContext context,
required SocialChatUserProfileManager ref,
required RtcProvider rtcProvider,
required SocialChatUserProfile? profile,
required double bottomPadding,
required bool isSelf,
required bool canLeaveMic,
required num currentMicIndex,
required bool canShowReport,
required bool canShowSetting,
}) {
return Stack(
children: [
Positioned.fill(
child: LayoutBuilder(
builder: (context, constraints) {
return Align(
alignment: Alignment.topCenter,
child: FittedBox(
fit: BoxFit.scaleDown,
alignment: Alignment.topCenter,
child: SizedBox(
width: constraints.maxWidth,
child: Padding(
padding: EdgeInsets.fromLTRB(
18.w,
10.w,
18.w,
bottomPadding > 6.w ? bottomPadding : 6.w,
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
_buildProfileHeader(context, profile, ref),
SizedBox(height: 6.w),
_buildBadgeStrip(
profile?.id ?? widget.userId,
ref,
profile,
),
SizedBox(height: 8.w),
_buildActions(
context: context,
ref: ref,
rtcProvider: rtcProvider,
isSelf: isSelf,
canLeaveMic: canLeaveMic,
currentMicIndex: currentMicIndex,
),
],
),
),
),
),
);
},
),
),
if (canShowReport)
Positioned(
top: 14.w,
right: 16.w,
child: _buildHeaderIconButton(
iconAsset: "sc_images/room/sc_icon_user_card_report.png",
onTap: () {
final route =
"${SCMainRoute.report}?type=user&tageId=${widget.userId}";
Navigator.of(context).pop();
SCNavigatorUtils.push(
navigatorKey.currentState?.context ?? context,
route,
replace: false,
);
},
),
),
if (canShowSetting)
Positioned(
top: 14.w,
right: 52.w,
child: _buildHeaderIconButton(
iconAsset: "sc_images/room/sc_icon_room_user_card_setting.png",
onTap: () {
if (userProvider?.userCardInfo == null) {
return;
}
Navigator.of(context).pop();
showBottomInBottomDialog(
navigatorKey.currentState?.context ?? context,
RoomUserCardSetting(
roomId: roomId,
userCardInfo: userProvider?.userCardInfo?.copyWith(),
),
);
},
),
),
],
);
}
BoxDecoration _sheetDecoration() { BoxDecoration _sheetDecoration() {
return const BoxDecoration( return const BoxDecoration(color: Color(0xff061F1E));
gradient: LinearGradient( }
begin: Alignment.topCenter,
end: Alignment.bottomCenter, Widget _buildDefaultSheetBackground() {
colors: [ return DecoratedBox(
Color(0xff18F2B1), decoration: _sheetDecoration(),
Color(0xffE4FFF6), child: Center(
Color(0xffF7FFFC), child: FractionallySizedBox(
Color(0xffFFFFFF), widthFactor: _defaultBackgroundImageFactor,
], heightFactor: _defaultBackgroundImageFactor,
child: Image.asset(
_defaultProfileCardBackgroundAsset,
fit: BoxFit.contain,
),
),
), ),
); );
} }
Widget _buildSheetBackground(PropsResources? dataCard) { Widget _buildSheetBackground(PropsResources? dataCard) {
if (dataCard == null) { if (dataCard == null) {
return DecoratedBox(decoration: _sheetDecoration()); return _buildDefaultSheetBackground();
} }
return Stack(
fit: StackFit.expand, return SCDataCardResourceView(
children: [ resource: dataCard,
SCDataCardResourceView( fit: BoxFit.cover,
resource: dataCard, showCoverFallback: false,
fit: BoxFit.fill, allowCoverAsResource: false,
fallback: DecoratedBox(decoration: _sheetDecoration()),
),
DecoratedBox(
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [
Colors.white.withValues(alpha: 0.08),
Colors.white.withValues(alpha: 0.22),
Colors.white.withValues(alpha: 0.58),
],
),
),
),
],
); );
} }
@ -329,8 +411,8 @@ class _RoomUserInfoCardState extends State<RoomUserInfoCard> {
onTap: () => _openProfile(context), onTap: () => _openProfile(context),
child: head( child: head(
url: profile?.userAvatar ?? "", url: profile?.userAvatar ?? "",
width: 70.w, width: 105.w,
border: Border.all(color: Colors.white, width: 2), border: Border.all(color: Colors.white, width: 3),
headdress: profile?.getHeaddress()?.sourceUrl, headdress: profile?.getHeaddress()?.sourceUrl,
headdressCover: profile?.getHeaddress()?.cover, headdressCover: profile?.getHeaddress()?.cover,
), ),
@ -349,7 +431,7 @@ class _RoomUserInfoCardState extends State<RoomUserInfoCard> {
maxWidth: 116.w, maxWidth: 116.w,
profile?.userNickname ?? "", profile?.userNickname ?? "",
fontSize: 18.sp, fontSize: 18.sp,
textColor: Colors.black, textColor: Colors.white,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
type: "", type: "",
needScroll: (profile?.userNickname?.characters.length ?? 0) > 12, needScroll: (profile?.userNickname?.characters.length ?? 0) > 12,
@ -381,6 +463,22 @@ class _RoomUserInfoCardState extends State<RoomUserInfoCard> {
], ],
), ),
SizedBox(height: 2.w), SizedBox(height: 2.w),
_buildSpecialIdMetaRow(context, profile),
],
);
}
Widget _buildSpecialIdMetaRow(
BuildContext context,
SocialChatUserProfile? profile,
) {
final flagUrl = _countryFlagUrl(context, profile);
final hasSex = profile?.userSex != null;
return Row(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: [
SCSpecialIdBadge( SCSpecialIdBadge(
idText: profile?.getID() ?? "", idText: profile?.getID() ?? "",
showAnimated: profile?.hasSpecialId() ?? false, showAnimated: profile?.hasSpecialId() ?? false,
@ -392,106 +490,62 @@ class _RoomUserInfoCardState extends State<RoomUserInfoCard> {
showAnimatedGradientText: showAnimatedGradientText:
profile?.shouldShowColoredSpecialIdText() ?? false, profile?.shouldShowColoredSpecialIdText() ?? false,
animationTextStyle: TextStyle( animationTextStyle: TextStyle(
color: Colors.black, color: Colors.white,
fontSize: 12.sp, fontSize: 12.sp,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
), ),
normalTextStyle: TextStyle( normalTextStyle: TextStyle(
color: Colors.black, color: Colors.white,
fontSize: 12.sp, fontSize: 12.sp,
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
), ),
animationFit: BoxFit.contain, animationFit: BoxFit.contain,
), ),
if (flagUrl.isNotEmpty) ...[
SizedBox(width: 8.w),
_buildCountryFlag(flagUrl),
],
if (hasSex) ...[SizedBox(width: 7.w), _buildSexIcon(profile)],
], ],
); );
} }
Widget _buildInfoChips(BuildContext context, SocialChatUserProfile? profile) { String _countryFlagUrl(BuildContext context, SocialChatUserProfile? profile) {
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
_buildCountryChip(context, profile),
SizedBox(width: 7.w),
_buildSexAgeChip(profile),
],
);
}
Widget _buildCountryChip(
BuildContext context,
SocialChatUserProfile? profile,
) {
final countryName = profile?.countryName ?? ""; final countryName = profile?.countryName ?? "";
final flagUrl = try {
Provider.of<SCAppGeneralManager>( return Provider.of<SCAppGeneralManager>(
context, context,
listen: false, listen: false,
).findCountryByName(countryName)?.nationalFlag ?? ).findCountryByName(countryName)?.nationalFlag ??
""; "";
} catch (_) {
return "";
}
}
return Container( Widget _buildCountryFlag(String flagUrl) {
constraints: BoxConstraints(maxWidth: 145.w), final trimmedUrl = flagUrl.trim();
padding: EdgeInsets.symmetric(horizontal: 10.w, vertical: 5.w), if (trimmedUrl.isEmpty) {
decoration: BoxDecoration( return const SizedBox.shrink();
color: Colors.white.withValues(alpha: 0.55), }
borderRadius: BorderRadius.circular(16.w),
border: Border.all( return ClipRRect(
color: const Color(0xff18F2B1).withValues(alpha: 0.18), borderRadius: BorderRadius.circular(2.w),
), child: netImage(
), url: trimmedUrl,
child: Row( width: 22.w,
mainAxisSize: MainAxisSize.min, height: 15.w,
children: [ fit: BoxFit.cover,
netImage( noDefaultImg: true,
url: flagUrl,
borderRadius: BorderRadius.all(Radius.circular(3.w)),
width: 19.w,
height: 14.w,
),
SizedBox(width: 5.w),
Flexible(
child: text(
countryName,
maxLines: 1,
textColor: Colors.black,
fontSize: 12.sp,
fontWeight: FontWeight.bold,
),
),
],
), ),
); );
} }
Widget _buildSexAgeChip(SocialChatUserProfile? profile) { Widget _buildSexIcon(SocialChatUserProfile? profile) {
return Container( if (profile?.userSex == null) {
height: 26.w, return const SizedBox.shrink();
padding: EdgeInsets.symmetric(horizontal: 10.w), }
decoration: BoxDecoration( return xb(profile?.userSex, height: 15.w, color: null);
image: DecorationImage(
image: AssetImage(
profile?.userSex == 0
? "sc_images/login/sc_icon_sex_woman_bg.png"
: "sc_images/login/sc_icon_sex_man_bg.png",
),
fit: BoxFit.fill,
),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
xb(profile?.userSex),
SizedBox(width: 3.w),
text(
"${profile?.age ?? 0}",
textColor: Colors.white,
fontSize: 12.sp,
fontWeight: FontWeight.bold,
),
],
),
);
} }
Widget _buildBadgeStrip( Widget _buildBadgeStrip(
@ -502,12 +556,12 @@ class _RoomUserInfoCardState extends State<RoomUserInfoCard> {
return SCUserBadgeStrip( return SCUserBadgeStrip(
userId: userId, userId: userId,
fallbackBadges: _roomCardFallbackBadges(ref, profile), fallbackBadges: _roomCardFallbackBadges(ref, profile),
height: 82.w, height: 70.w,
badgeHeight: 34.5.w, badgeHeight: 57.5.w,
longBadgeWidth: 87.w, longBadgeWidth: 145.w,
spacing: 10.w, spacing: 20.w,
maxBadges: 12, maxBadges: 12,
wrap: true, wrap: false,
reserveSpace: true, reserveSpace: true,
); );
} }
@ -525,7 +579,7 @@ class _RoomUserInfoCardState extends State<RoomUserInfoCard> {
return wearBadges; return wearBadges;
} }
return ref.userCardInfo?.useBadge return ref.userCardInfo?.useBadge
?.map(_roomUseBadgeToWearBadge) ?.map<WearBadge>(_roomUseBadgeToWearBadge)
.where(_hasBadgeUrl) .where(_hasBadgeUrl)
.toList() ?? .toList() ??
const <WearBadge>[]; const <WearBadge>[];
@ -640,7 +694,7 @@ class _RoomUserInfoCardState extends State<RoomUserInfoCard> {
label, label,
maxLines: 2, maxLines: 2,
textAlign: TextAlign.center, textAlign: TextAlign.center,
textColor: const Color(0xff666666), textColor: Colors.white,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
fontSize: 12.sp, fontSize: 12.sp,
lineHeight: 1.1, lineHeight: 1.1,
@ -695,7 +749,7 @@ class _RoomUserInfoCardState extends State<RoomUserInfoCard> {
: SCAppLocalizations.of(context)!.follow, : SCAppLocalizations.of(context)!.follow,
maxLines: 2, maxLines: 2,
textAlign: TextAlign.center, textAlign: TextAlign.center,
textColor: const Color(0xff666666), textColor: Colors.white,
fontWeight: FontWeight.w600, fontWeight: FontWeight.w600,
fontSize: 12.sp, fontSize: 12.sp,
lineHeight: 1.1, lineHeight: 1.1,

Binary file not shown.

After

Width:  |  Height:  |  Size: 311 KiB

View File

@ -1,4 +1,5 @@
import 'package:flutter_test/flutter_test.dart'; import 'package:flutter_test/flutter_test.dart';
import 'package:flutter/widgets.dart';
import 'package:yumi/app/config/app_config.dart'; import 'package:yumi/app/config/app_config.dart';
import 'package:yumi/app/constants/sc_global_config.dart'; import 'package:yumi/app/constants/sc_global_config.dart';
import 'package:yumi/shared/data_sources/models/message/room_rocket_launch_broadcast_message.dart'; import 'package:yumi/shared/data_sources/models/message/room_rocket_launch_broadcast_message.dart';
@ -60,6 +61,19 @@ void main() {
timer?.cancel(); timer?.cancel();
}); });
test('maps top avatar frame from pag raw canvas using cover fit', () {
final rect = debugMapRoomRocketPagRawRectToCoverCanvas(
rawRect: const Rect.fromLTWH(291, 164, 168, 168),
rawSize: const Size(750, 1624),
canvasSize: const Size(392.7272727, 829.090909),
);
expect(rect.left, closeTo(152.38, 0.01));
expect(rect.top, closeTo(75.23, 0.01));
expect(rect.width, closeTo(87.97, 0.01));
expect(rect.height, closeTo(87.97, 0.01));
});
test('extracts launch pag urls from nested resource payloads', () { test('extracts launch pag urls from nested resource payloads', () {
final launch = RoomRocketLaunchBroadcastMessage.fromJson({ final launch = RoomRocketLaunchBroadcastMessage.fromJson({
'rocketAnimationResource': { 'rocketAnimationResource': {