import 'dart:math' as math; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:provider/provider.dart'; import 'package:yumi/services/audio/rtc_manager.dart'; import 'package:yumi/shared/business_logic/models/res/sc_room_rocket_api_res.dart'; import 'package:yumi/shared/data_sources/sources/local/floating_screen_manager.dart'; import 'package:yumi/shared/data_sources/sources/local/user_manager.dart'; import 'package:yumi/shared/data_sources/sources/repositories/sc_room_repository_imp.dart'; import 'package:yumi/shared/tools/sc_gift_vap_svga_manager.dart'; import 'package:yumi/shared/tools/sc_room_effect_scheduler.dart'; import 'package:yumi/ui_kit/components/sc_rotating_dots_loading.dart'; import 'package:yumi/ui_kit/widgets/room/anim/room_entrance_widget.dart'; class RoomRocketRankPage extends StatefulWidget { const RoomRocketRankPage({ super.key, this.roomId, this.showRound2Today = true, this.levelRanks, this.currentUser, this.initialLevel = 0, }); final String? roomId; final bool showRound2Today; final List>? levelRanks; final RoomRocketRankEntry? currentUser; final int initialLevel; @override State createState() => _RoomRocketRankPageState(); } class RoomRocketRankEntry { const RoomRocketRankEntry({ required this.rank, required this.nickname, this.userId, this.account, this.avatarUrl, this.avatarAsset, }); final int rank; final String nickname; final String? userId; final String? account; final String? avatarUrl; final String? avatarAsset; RoomRocketRankEntry copyWith({ int? rank, String? nickname, String? userId, String? account, String? avatarUrl, String? avatarAsset, }) { return RoomRocketRankEntry( rank: rank ?? this.rank, nickname: nickname ?? this.nickname, userId: userId ?? this.userId, account: account ?? this.account, avatarUrl: avatarUrl ?? this.avatarUrl, avatarAsset: avatarAsset ?? this.avatarAsset, ); } } class _RoomRocketRankPageState extends State { static const String _effectSuppressReason = 'room_rocket_rank'; static const int _levelCount = 6; late int _selectedLevel; final Map> _loadedRanks = {}; final Map _loadedRoundNos = {}; final Map _loadedCurrentUsers = {}; final Set _loadingLevels = {}; RtcProvider? _suppressedRtcProvider; String _suppressedRoomId = ''; bool _roomEffectsSuppressed = false; bool _restoreRoomVisualEffects = false; bool _restoreVoiceRoomRouteVisible = false; @override void initState() { super.initState(); _selectedLevel = widget.initialLevel.clamp(0, _levelCount - 1); _suppressRoomEffects(); WidgetsBinding.instance.addPostFrameCallback((_) { if (mounted) { _loadLevel(_selectedLevel); } }); } @override void dispose() { _releaseRoomEffects(); super.dispose(); } @override Widget build(BuildContext context) { return AnnotatedRegion( value: const SystemUiOverlayStyle( statusBarColor: Colors.transparent, statusBarIconBrightness: Brightness.light, statusBarBrightness: Brightness.dark, systemNavigationBarColor: Color(0xFF010200), systemNavigationBarIconBrightness: Brightness.light, ), child: Scaffold( backgroundColor: const Color(0xFF010200), extendBodyBehindAppBar: true, body: LayoutBuilder( builder: (context, constraints) { return SizedBox.expand( child: FittedBox( fit: BoxFit.contain, child: SizedBox( width: _RocketRankLayout.designWidth, height: _RocketRankLayout.designHeight, child: _RocketRankCanvas( selectedLevel: _selectedLevel, showRound2Today: widget.showRound2Today, roundNo: _loadedRoundNos[_selectedLevel] ?? 0, isLoading: widget.levelRanks == null && _loadingLevels.contains(_selectedLevel), ranksByLevel: _normalizedRanks, currentUser: widget.currentUser ?? _currentUserEntryForLevel(_selectedLevel), onLevelChanged: (index) { setState(() => _selectedLevel = index); _loadLevel(index); }, ), ), ), ); }, ), ), ); } void _suppressRoomEffects() { if (_roomEffectsSuppressed) { return; } _roomEffectsSuppressed = true; try { final rtcProvider = Provider.of(context, listen: false); _suppressedRtcProvider = rtcProvider; _suppressedRoomId = rtcProvider.currenRoom?.roomProfile?.roomProfile?.id?.trim() ?? ''; _restoreRoomVisualEffects = rtcProvider.roomVisualEffectsEnabled; _restoreVoiceRoomRouteVisible = rtcProvider.isVoiceRoomRouteVisible; rtcProvider.setVoiceRoomRouteVisible(false); rtcProvider.setRoomVisualEffectsEnabled(false); } catch (_) {} RoomEntranceHelper.clearQueue(); OverlayManager().beginSuppressFloatingScreens( reason: _effectSuppressReason, ); SCRoomEffectScheduler().clearDeferredTasks(reason: _effectSuppressReason); SCGiftVapSvgaManager().stopPlayback(); SCGiftVapSvgaManager().pauseAnim(); } void _releaseRoomEffects() { if (!_roomEffectsSuppressed) { return; } _roomEffectsSuppressed = false; SCGiftVapSvgaManager().stopPlayback(); OverlayManager().endSuppressFloatingScreens(reason: _effectSuppressReason); final rtcProvider = _suppressedRtcProvider; if (rtcProvider != null && _isSameSuppressedRoom(rtcProvider)) { if (_restoreVoiceRoomRouteVisible) { rtcProvider.setVoiceRoomRouteVisible(true); } if (_restoreRoomVisualEffects) { rtcProvider.setRoomVisualEffectsEnabled(true); } } SCGiftVapSvgaManager().resumeAnim(); _suppressedRtcProvider = null; } bool _isSameSuppressedRoom(RtcProvider rtcProvider) { final currentRoomId = rtcProvider.currenRoom?.roomProfile?.roomProfile?.id?.trim() ?? ''; if (currentRoomId.isEmpty) { return false; } return _suppressedRoomId.isEmpty || currentRoomId == _suppressedRoomId; } List> get _normalizedRanks { final providedRanks = widget.levelRanks; if (providedRanks == null) { final roomId = _currentRoomId(); if (roomId.isEmpty) { return List>.generate( _levelCount, (_) => const [], ); } return List>.generate(_levelCount, (index) { return _loadedRanks[index] ?? const []; }); } return List>.generate(_levelCount, (index) { if (index >= providedRanks.length) { return const []; } return providedRanks[index]; }); } String _currentRoomId() { final propRoomId = widget.roomId?.trim() ?? ''; if (propRoomId.isNotEmpty) { return propRoomId; } try { return Provider.of( context, listen: false, ).currenRoom?.roomProfile?.roomProfile?.id?.trim() ?? ''; } catch (_) { return ''; } } void _loadLevel(int index) { if (widget.levelRanks != null || _loadingLevels.contains(index)) { _rocketRankDebug( 'skip king request level=${index + 1} provided=${widget.levelRanks != null} loading=${_loadingLevels.contains(index)}', ); return; } final roomId = _currentRoomId(); if (roomId.isEmpty) { _rocketRankDebug('skip king request because roomId is empty'); return; } setState(() { _loadingLevels.add(index); }); _rocketRankDebug('request king roomId=$roomId level=${index + 1}'); SCChatRoomRepository() .roomRocketKing(roomId, level: index + 1) .then((res) { if (!mounted || roomId != _currentRoomId()) { return; } final entries = _rankEntriesFromKing(res); final currentUser = _currentUserEntryFromKing(res, entries); setState(() { _loadedRanks[index] = entries; _loadedCurrentUsers[index] = currentUser; _loadedRoundNos[index] = res.sourceRoundNo > 0 ? res.sourceRoundNo : res.roundNo; }); }) .catchError((_) { _rocketRankDebug('king request failed level=${index + 1}'); if (!mounted) { return; } setState(() { _loadedRanks[index] = const []; }); }) .whenComplete(() { if (!mounted) { _loadingLevels.remove(index); return; } setState(() { _loadingLevels.remove(index); }); }); } List _rankEntriesFromKing(SCRoomRocketKingRes king) { return king.displayRecords .map( (item) => RoomRocketRankEntry( rank: item.rank, nickname: item.displayName, userId: item.userId, account: item.account, avatarUrl: item.userAvatar.isEmpty ? null : item.userAvatar, ), ) .toList(growable: false); } RoomRocketRankEntry _currentUserEntryFromKing( SCRoomRocketKingRes king, List entries, ) { final apiCurrentUser = king.currentUser; if (apiCurrentUser != null) { return _rankEntryFromKingUser(apiCurrentUser); } final matched = _findCurrentUserInEntries(entries); if (matched != null) { return matched; } return _currentAccountEntry(rank: 0); } RoomRocketRankEntry _currentUserEntryForLevel(int levelIndex) { final loadedCurrentUser = _loadedCurrentUsers[levelIndex]; if (loadedCurrentUser != null) { return loadedCurrentUser; } final ranks = levelIndex < _normalizedRanks.length ? _normalizedRanks[levelIndex] : const []; final matched = _findCurrentUserInEntries(ranks); if (matched != null) { return matched; } return _currentAccountEntry(rank: 0); } RoomRocketRankEntry? _findCurrentUserInEntries( List entries, ) { final currentProfile = AccountStorage().getCurrentUser()?.userProfile; final currentUserId = (currentProfile?.id ?? '').trim(); final currentAccount = (currentProfile?.account ?? '').trim(); for (final entry in entries) { final entryUserId = (entry.userId ?? '').trim(); final entryAccount = (entry.account ?? '').trim(); if (currentUserId.isNotEmpty && entryUserId == currentUserId) { return entry; } if (currentAccount.isNotEmpty && entryAccount == currentAccount) { return entry; } } return null; } RoomRocketRankEntry _rankEntryFromKingUser(SCRoomRocketKingUserRes user) { return RoomRocketRankEntry( rank: user.rank, nickname: user.displayName.isNotEmpty ? user.displayName : 'You', userId: user.userId, account: user.account, avatarUrl: user.userAvatar.isEmpty ? null : user.userAvatar, ); } RoomRocketRankEntry _currentAccountEntry({required int rank}) { final currentProfile = AccountStorage().getCurrentUser()?.userProfile; final nicknameCandidates = [ currentProfile?.userNickname, currentProfile?.account, currentProfile?.id, ]; final nickname = nicknameCandidates .map((item) => item?.trim() ?? '') .firstWhere((item) => item.isNotEmpty, orElse: () => 'You'); final avatarUrl = currentProfile?.userAvatar?.trim() ?? ''; return RoomRocketRankEntry( rank: rank, nickname: nickname, userId: currentProfile?.id, account: currentProfile?.account, avatarUrl: avatarUrl.isEmpty ? null : avatarUrl, ); } } void _rocketRankDebug(String message) { if (kDebugMode) { debugPrint('[RoomRocketRank] $message'); } } class _RocketRankCanvas extends StatelessWidget { const _RocketRankCanvas({ required this.selectedLevel, required this.showRound2Today, required this.roundNo, required this.isLoading, required this.ranksByLevel, required this.currentUser, required this.onLevelChanged, }); final int selectedLevel; final bool showRound2Today; final int roundNo; final bool isLoading; final List> ranksByLevel; final RoomRocketRankEntry currentUser; final ValueChanged onLevelChanged; @override Widget build(BuildContext context) { final entries = selectedLevel < ranksByLevel.length ? ranksByLevel[selectedLevel] : const []; return DecoratedBox( decoration: const BoxDecoration(color: Color(0xFF010200)), child: Stack( clipBehavior: Clip.none, children: [ const _RocketRankBackground(), const _RocketRankNavigation(), Positioned( left: 0, top: 100, width: _RocketRankLayout.designWidth, child: Visibility( visible: showRound2Today && roundNo > 1, child: _GradientText('Round ${roundNo > 0 ? roundNo : 1} Today'), ), ), Positioned( left: 0, top: 124, width: _RocketRankLayout.designWidth, height: 22, child: _RocketLevelTabs( selectedIndex: selectedLevel, onChanged: onLevelChanged, ), ), if (isLoading) const _RocketRankLoadingState() else if (entries.isEmpty) const _RocketRankEmptyState() else _RocketRankListContent(entries: entries), _RocketRankBottomBar(entry: currentUser), const _RocketRankBackButton(), ], ), ); } } class _RocketRankBackground extends StatelessWidget { const _RocketRankBackground(); @override Widget build(BuildContext context) { return Stack( children: [ Positioned( left: 0, top: -1, width: _RocketRankLayout.designWidth, height: 665, child: Opacity( opacity: 0.24, child: Transform.rotate( angle: math.pi, child: Image.asset( _RocketRankAssets.background, fit: BoxFit.cover, filterQuality: FilterQuality.high, ), ), ), ), ], ); } } class _RocketRankNavigation extends StatelessWidget { const _RocketRankNavigation(); @override Widget build(BuildContext context) { return const Stack( children: [ Positioned( left: 0, top: 57, width: _RocketRankLayout.designWidth, child: Text( 'Rocket King', textAlign: TextAlign.center, textScaler: TextScaler.noScaling, style: TextStyle( color: Colors.white, fontSize: 18, height: 1, fontWeight: FontWeight.w500, decoration: TextDecoration.none, ), ), ), ], ); } } class _RocketRankBackButton extends StatelessWidget { const _RocketRankBackButton(); @override Widget build(BuildContext context) { return Positioned( left: 0, top: 46, width: 56, height: 44, child: GestureDetector( behavior: HitTestBehavior.opaque, onTap: () { final navigator = Navigator.of(context); if (navigator.canPop()) { navigator.pop(); } }, child: const Align( alignment: Alignment.center, child: Icon( Icons.arrow_back_ios_new_rounded, color: Colors.white, size: 24, ), ), ), ); } } class _GradientText extends StatelessWidget { const _GradientText(this.text); final String text; @override Widget build(BuildContext context) { return ShaderMask( shaderCallback: (bounds) => const LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [Colors.white, Color(0xFFA4FFE3)], ).createShader(bounds), blendMode: BlendMode.srcIn, child: Text( text, textAlign: TextAlign.center, textScaler: TextScaler.noScaling, style: const TextStyle( color: Colors.white, fontSize: 16, height: 1, fontWeight: FontWeight.w500, decoration: TextDecoration.none, ), ), ); } } class _RocketLevelTabs extends StatelessWidget { const _RocketLevelTabs({ required this.selectedIndex, required this.onChanged, }); final int selectedIndex; final ValueChanged onChanged; @override Widget build(BuildContext context) { return SingleChildScrollView( scrollDirection: Axis.horizontal, physics: const BouncingScrollPhysics(), child: Padding( padding: const EdgeInsets.only(left: 8, right: 16), child: Row( children: List.generate(_RoomRocketRankPageState._levelCount, ( index, ) { return Padding( padding: EdgeInsets.only( right: index == _RoomRocketRankPageState._levelCount - 1 ? 0 : 7, ), child: GestureDetector( behavior: HitTestBehavior.opaque, onTap: () => onChanged(index), child: SizedBox( width: 54, height: 22, child: _RocketLevelTab( label: 'Lv.${index + 1}', selected: index == selectedIndex, ), ), ), ); }), ), ), ); } } class _RocketLevelTab extends StatelessWidget { const _RocketLevelTab({required this.label, required this.selected}); final String label; final bool selected; @override Widget build(BuildContext context) { final color = selected ? const Color(0xFF75F3CB) : Colors.white.withValues(alpha: 0.52); return Stack( clipBehavior: Clip.none, alignment: Alignment.center, children: [ if (selected) const Positioned(left: 0, top: 8, child: _LevelSlashGroup()), if (selected) const Positioned( right: 0, top: 8, child: _LevelSlashGroup(flipped: true), ), Text( label, textAlign: TextAlign.center, textScaler: TextScaler.noScaling, style: TextStyle( color: color, fontSize: 12, height: 1, fontWeight: FontWeight.w400, decoration: TextDecoration.none, ), ), ], ); } } class _LevelSlashGroup extends StatelessWidget { const _LevelSlashGroup({this.flipped = false}); final bool flipped; @override Widget build(BuildContext context) { final painter = _LevelSlashPainter(flipped: flipped); return CustomPaint(size: const Size(12, 6), painter: painter); } } class _LevelSlashPainter extends CustomPainter { const _LevelSlashPainter({required this.flipped}); final bool flipped; @override void paint(Canvas canvas, Size size) { final paint = Paint() ..color = const Color(0xFF75F3CB) ..strokeWidth = 1.3 ..strokeCap = StrokeCap.round; for (var i = 0; i < 3; i++) { final x = i * 3.2 + 1; final start = flipped ? Offset(size.width - x, 1) : Offset(x, size.height - 1); final end = flipped ? Offset(size.width - x - 2.2, 5) : Offset(x + 2.2, 1); canvas.drawLine(start, end, paint); } } @override bool shouldRepaint(covariant _LevelSlashPainter oldDelegate) { return oldDelegate.flipped != flipped; } } class _RocketRankListContent extends StatelessWidget { const _RocketRankListContent({required this.entries}); final List entries; @override Widget build(BuildContext context) { final topEntries = entries.take(3).toList(); final listEntries = entries.skip(3).toList(); return Stack( children: [ if (topEntries.isNotEmpty) _RankOne(entry: topEntries[0]), if (topEntries.length > 1) _SideRank( entry: topEntries[1], left: 34, avatarLeft: 53, frameAsset: _RocketRankAssets.rankTwoFrame, ), if (topEntries.length > 2) _SideRank( entry: topEntries[2], left: 231, avatarLeft: 250, frameAsset: _RocketRankAssets.rankThreeFrame, ), Positioned( left: 11, top: 405, width: 353, height: 324, child: MediaQuery.removePadding( context: context, removeTop: true, removeBottom: true, child: ScrollConfiguration( behavior: const _NoGlowScrollBehavior(), child: ListView.builder( padding: EdgeInsets.zero, physics: const BouncingScrollPhysics(), itemExtent: 47, itemCount: listEntries.length, itemBuilder: (context, index) { return _RocketRankRow(entry: listEntries[index]); }, ), ), ), ), ], ); } } class _RankOne extends StatelessWidget { const _RankOne({required this.entry}); final RoomRocketRankEntry entry; @override Widget build(BuildContext context) { return Stack( children: [ Positioned( left: 106, top: 219, width: 162, height: 46, child: Image.asset( _RocketRankAssets.podium, fit: BoxFit.cover, filterQuality: FilterQuality.high, ), ), Positioned( left: 143, top: 157, width: 88, height: 88, child: _FramedRankAvatar( entry: entry, frameAsset: _RocketRankAssets.rankOneFrame, imageInset: 8.8, imageSize: 70.4, ), ), Positioned( left: 82, top: 267, width: 211, child: _RocketSingleLineText( entry.nickname, fontSize: 16, textAlign: TextAlign.center, ), ), ], ); } } class _SideRank extends StatelessWidget { const _SideRank({ required this.entry, required this.left, required this.avatarLeft, required this.frameAsset, }); final RoomRocketRankEntry entry; final double left; final double avatarLeft; final String frameAsset; @override Widget build(BuildContext context) { return Stack( children: [ Positioned( left: left, top: 324, width: 110, height: 31, child: Opacity( opacity: 0.65, child: Image.asset( _RocketRankAssets.podium, fit: BoxFit.cover, filterQuality: FilterQuality.high, ), ), ), Positioned( left: avatarLeft, top: 272, width: 72, height: 72, child: _FramedRankAvatar( entry: entry, frameAsset: frameAsset, imageInset: 7.2, imageSize: 57.6, ), ), Positioned( left: left, top: 359, width: 110, child: _RocketSingleLineText( entry.nickname, fontSize: 16, textAlign: TextAlign.center, ), ), ], ); } } class _FramedRankAvatar extends StatelessWidget { const _FramedRankAvatar({ required this.entry, required this.frameAsset, required this.imageInset, required this.imageSize, }); final RoomRocketRankEntry entry; final String frameAsset; final double imageInset; final double imageSize; @override Widget build(BuildContext context) { return Stack( children: [ Positioned( left: imageInset, top: imageInset, width: imageSize, height: imageSize, child: _CircularAvatar(entry: entry), ), Positioned.fill( child: Image.asset( frameAsset, fit: BoxFit.contain, filterQuality: FilterQuality.high, ), ), ], ); } } class _RocketRankRow extends StatelessWidget { const _RocketRankRow({required this.entry}); final RoomRocketRankEntry entry; @override Widget build(BuildContext context) { return SizedBox( width: 353, height: 47, child: Stack( children: [ Positioned( left: 14, top: 7, width: 18, child: _RocketSingleLineText( '${entry.rank}', fontSize: 14, textAlign: TextAlign.left, ), ), Positioned( left: 42, top: 0, width: 30, height: 30, child: _CircularAvatar(entry: entry), ), Positioned( left: 82, top: 7, width: 170, child: _RocketSingleLineText( entry.nickname, fontSize: 14, textAlign: TextAlign.left, ), ), const Positioned( left: 0, top: 38, width: 353, height: 1, child: _RocketRankDivider(), ), ], ), ); } } class _RocketRankBottomBar extends StatelessWidget { const _RocketRankBottomBar({required this.entry}); final RoomRocketRankEntry entry; @override Widget build(BuildContext context) { return Stack( children: [ Positioned( left: 0, top: 729, width: _RocketRankLayout.designWidth, height: 83, child: Transform.rotate( angle: math.pi, child: Image.asset( _RocketRankAssets.bottomBar, fit: BoxFit.cover, filterQuality: FilterQuality.high, ), ), ), Positioned( left: 36, top: 768, width: 32, child: _RocketSingleLineText( entry.rank > 0 ? '${entry.rank}' : '--', fontSize: 16, textAlign: TextAlign.left, ), ), Positioned( left: 75, top: 758, width: 38, height: 38, child: _CircularAvatar( entry: RoomRocketRankEntry( rank: entry.rank, nickname: entry.nickname, userId: entry.userId, account: entry.account, avatarUrl: entry.avatarUrl, avatarAsset: entry.avatarAsset, ), ), ), Positioned( left: 122, top: 768, width: 190, child: _RocketSingleLineText( entry.nickname, fontSize: 16, textAlign: TextAlign.left, ), ), ], ); } } class _RocketRankLoadingState extends StatelessWidget { const _RocketRankLoadingState(); @override Widget build(BuildContext context) { return const Positioned( left: 28, top: 208, width: 319, height: 430, child: Center( child: SCRotatingDotsLoading(size: 34, color: Color(0xFF75F3CB)), ), ); } } class _RocketRankEmptyState extends StatelessWidget { const _RocketRankEmptyState(); @override Widget build(BuildContext context) { return Positioned( left: 28, top: 208, width: 319, height: 430, child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ Image.asset( _RocketRankAssets.noData, width: 118, height: 118, fit: BoxFit.contain, ), const SizedBox(height: 14), Text( 'Not rocket-level; no “Rocket King”', textAlign: TextAlign.center, textScaler: TextScaler.noScaling, style: TextStyle( color: Colors.white.withValues(alpha: 0.62), fontSize: 14, height: 1.25, fontWeight: FontWeight.w400, decoration: TextDecoration.none, ), ), ], ), ); } } class _CircularAvatar extends StatelessWidget { const _CircularAvatar({required this.entry}); final RoomRocketRankEntry entry; @override Widget build(BuildContext context) { final avatarUrl = entry.avatarUrl?.trim() ?? ''; final avatarAsset = entry.avatarAsset?.trim() ?? ''; final image = avatarUrl.isNotEmpty ? Image.network( avatarUrl, fit: BoxFit.cover, filterQuality: FilterQuality.high, errorBuilder: (_, _, _) => const _AvatarPlaceholder(), ) : avatarAsset.isNotEmpty ? Image.asset(avatarAsset, fit: BoxFit.cover) : const _AvatarPlaceholder(); return ClipOval(child: image); } } class _AvatarPlaceholder extends StatelessWidget { const _AvatarPlaceholder(); @override Widget build(BuildContext context) { return const DecoratedBox( decoration: BoxDecoration( shape: BoxShape.circle, gradient: LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [Color(0xFF0F6B55), Color(0xFF031F19)], ), ), child: Icon(Icons.person, color: Color(0xFF8CFFE3), size: 18), ); } } class _RocketSingleLineText extends StatelessWidget { const _RocketSingleLineText( this.text, { required this.fontSize, required this.textAlign, }); final String text; final double fontSize; final TextAlign textAlign; @override Widget build(BuildContext context) { return Text( text, maxLines: 1, overflow: TextOverflow.ellipsis, textAlign: textAlign, textScaler: TextScaler.noScaling, style: TextStyle( color: Colors.white, fontSize: fontSize, height: 1, fontWeight: FontWeight.w400, decoration: TextDecoration.none, ), ); } } class _RocketRankDivider extends StatelessWidget { const _RocketRankDivider(); @override Widget build(BuildContext context) { return DecoratedBox( decoration: BoxDecoration( gradient: LinearGradient( colors: [ Colors.white.withValues(alpha: 0), Colors.white.withValues(alpha: 0.28), Colors.white.withValues(alpha: 0), ], stops: const [0, 0.5, 1], ), ), ); } } class _NoGlowScrollBehavior extends ScrollBehavior { const _NoGlowScrollBehavior(); @override Widget buildOverscrollIndicator( BuildContext context, Widget child, ScrollableDetails details, ) { return child; } } class _RocketRankLayout { const _RocketRankLayout._(); static const double designWidth = 375; static const double designHeight = 812; } class _RocketRankAssets { const _RocketRankAssets._(); static const String background = 'sc_images/room/rocket/rocket_rank_bg.png'; static const String bottomBar = 'sc_images/room/rocket/bottom_strip_active.png'; static const String podium = 'sc_images/room/rocket/rocket_stage.png'; static const String rankOneFrame = 'sc_images/room/rocket/rocket_rank_frame_1.png'; static const String rankTwoFrame = 'sc_images/room/rocket/rocket_rank_frame_2.png'; static const String rankThreeFrame = 'sc_images/room/rocket/rocket_rank_frame_3.png'; static const String noData = 'sc_images/room/sc_icon_room_music_empty.png'; }