yumi-flutter/lib/modules/room/rocket/room_rocket_rank_page.dart
roxy bd6d35ee1b 🚀 UI
2026-05-14 17:10:53 +08:00

1098 lines
29 KiB
Dart

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/user_manager.dart';
import 'package:yumi/shared/data_sources/sources/repositories/sc_room_repository_imp.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<List<RoomRocketRankEntry>>? levelRanks;
final RoomRocketRankEntry? currentUser;
final int initialLevel;
@override
State<RoomRocketRankPage> 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<RoomRocketRankPage> {
late int _selectedLevel;
final Map<int, List<RoomRocketRankEntry>> _loadedRanks = {};
final Map<int, int> _loadedRoundNos = {};
final Map<int, RoomRocketRankEntry> _loadedCurrentUsers = {};
final Set<int> _loadingLevels = {};
static const int _levelCount = 6;
@override
void initState() {
super.initState();
_selectedLevel = widget.initialLevel.clamp(0, _levelCount - 1);
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) {
_loadLevel(_selectedLevel);
}
});
}
@override
Widget build(BuildContext context) {
return AnnotatedRegion<SystemUiOverlayStyle>(
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,
ranksByLevel: _normalizedRanks,
currentUser:
widget.currentUser ??
_currentUserEntryForLevel(_selectedLevel),
onLevelChanged: (index) {
setState(() => _selectedLevel = index);
_loadLevel(index);
},
),
),
),
);
},
),
),
);
}
List<List<RoomRocketRankEntry>> get _normalizedRanks {
final providedRanks = widget.levelRanks;
if (providedRanks == null) {
final roomId = _currentRoomId();
if (roomId.isEmpty) {
return List<List<RoomRocketRankEntry>>.generate(
_levelCount,
(_) => const <RoomRocketRankEntry>[],
);
}
return List<List<RoomRocketRankEntry>>.generate(_levelCount, (index) {
return _loadedRanks[index] ?? const <RoomRocketRankEntry>[];
});
}
return List<List<RoomRocketRankEntry>>.generate(_levelCount, (index) {
if (index >= providedRanks.length) {
return const <RoomRocketRankEntry>[];
}
return providedRanks[index];
});
}
String _currentRoomId() {
final propRoomId = widget.roomId?.trim() ?? '';
if (propRoomId.isNotEmpty) {
return propRoomId;
}
try {
return Provider.of<RtcProvider>(
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;
}
_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 <RoomRocketRankEntry>[];
});
})
.whenComplete(() {
_loadingLevels.remove(index);
});
}
List<RoomRocketRankEntry> _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<RoomRocketRankEntry> 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 <RoomRocketRankEntry>[];
final matched = _findCurrentUserInEntries(ranks);
if (matched != null) {
return matched;
}
return _currentAccountEntry(rank: 0);
}
RoomRocketRankEntry? _findCurrentUserInEntries(
List<RoomRocketRankEntry> 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.ranksByLevel,
required this.currentUser,
required this.onLevelChanged,
});
final int selectedLevel;
final bool showRound2Today;
final int roundNo;
final List<List<RoomRocketRankEntry>> ranksByLevel;
final RoomRocketRankEntry currentUser;
final ValueChanged<int> onLevelChanged;
@override
Widget build(BuildContext context) {
final entries =
selectedLevel < ranksByLevel.length
? ranksByLevel[selectedLevel]
: const <RoomRocketRankEntry>[];
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 (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<int> 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<RoomRocketRankEntry> 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 _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/general/sc_no_data.png';
}