1204 lines
33 KiB
Dart
1204 lines
33 KiB
Dart
import 'dart:math' as math;
|
|
|
|
import 'package:flutter/material.dart';
|
|
import 'package:yumi/shared/business_logic/models/res/sc_room_rocket_status_res.dart';
|
|
import 'package:yumi/ui_kit/components/custom_cached_image.dart';
|
|
|
|
class RoomRocketDialog extends StatelessWidget {
|
|
const RoomRocketDialog({
|
|
super.key,
|
|
this.status,
|
|
this.progressStage,
|
|
this.bottomStage,
|
|
this.levels,
|
|
this.selectedLevel = 0,
|
|
this.onLevelSelected,
|
|
this.rewards,
|
|
this.crew,
|
|
this.onClose,
|
|
this.onOpenRank,
|
|
this.onRewardSelected,
|
|
this.onOpenRecord,
|
|
this.onOpenNote,
|
|
});
|
|
|
|
final SCRoomRocketStatusRes? status;
|
|
final RoomRocketProgressStage? progressStage;
|
|
final RoomRocketBottomStage? bottomStage;
|
|
final List<RoomRocketLevelItem>? levels;
|
|
final int selectedLevel;
|
|
final ValueChanged<int>? onLevelSelected;
|
|
final List<RoomRocketRewardItem>? rewards;
|
|
final List<RoomRocketCrewMember>? crew;
|
|
final VoidCallback? onClose;
|
|
final VoidCallback? onOpenRank;
|
|
final ValueChanged<RoomRocketRewardItem>? onRewardSelected;
|
|
final VoidCallback? onOpenRecord;
|
|
final VoidCallback? onOpenNote;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final resolvedLevels = _normalizeLevels(levels);
|
|
final resolvedRewards = _normalizeRewards(rewards);
|
|
final resolvedCrew = _normalizeCrew(crew, status);
|
|
final percent = _normalizePercent(status?.energyPercent);
|
|
final resolvedProgressStage =
|
|
progressStage ?? RoomRocketProgressStage.fromPercent(percent);
|
|
final resolvedBottomStage =
|
|
bottomStage ??
|
|
(resolvedCrew.isEmpty
|
|
? RoomRocketBottomStage.empty
|
|
: RoomRocketBottomStage.active);
|
|
final round = (status?.roundNo ?? status?.level ?? 1).clamp(1, 99).toInt();
|
|
|
|
return Directionality(
|
|
textDirection: TextDirection.ltr,
|
|
child: Material(
|
|
color: Colors.transparent,
|
|
child: Stack(
|
|
children: [
|
|
Positioned.fill(
|
|
child: GestureDetector(
|
|
behavior: HitTestBehavior.opaque,
|
|
onTap: onClose,
|
|
child: Container(color: Colors.black.withValues(alpha: 0.60)),
|
|
),
|
|
),
|
|
Positioned.fill(
|
|
child: LayoutBuilder(
|
|
builder: (context, constraints) {
|
|
final scale = math.min(
|
|
constraints.maxWidth / _designWidth,
|
|
constraints.maxHeight / _designHeight,
|
|
);
|
|
return Align(
|
|
alignment: Alignment.topCenter,
|
|
child: SizedBox(
|
|
width: _designWidth * scale,
|
|
height: _designHeight * scale,
|
|
child: _RocketCanvas(
|
|
scale: scale,
|
|
percent: percent,
|
|
progressStage: resolvedProgressStage,
|
|
bottomStage: resolvedBottomStage,
|
|
round: round,
|
|
levels: resolvedLevels,
|
|
selectedLevel: selectedLevel,
|
|
onLevelSelected: onLevelSelected,
|
|
rewards: resolvedRewards,
|
|
crew: resolvedCrew,
|
|
onClose: onClose,
|
|
onOpenRank: onOpenRank,
|
|
onRewardSelected: onRewardSelected,
|
|
onOpenRecord: onOpenRecord,
|
|
onOpenNote: onOpenNote,
|
|
),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
static List<RoomRocketLevelItem> _normalizeLevels(
|
|
List<RoomRocketLevelItem>? levels,
|
|
) {
|
|
final source =
|
|
levels
|
|
?.where((item) => item.label.trim().isNotEmpty)
|
|
.toList(growable: false) ??
|
|
const <RoomRocketLevelItem>[];
|
|
if (source.isNotEmpty) {
|
|
return source;
|
|
}
|
|
return List<RoomRocketLevelItem>.generate(
|
|
6,
|
|
(index) => RoomRocketLevelItem(label: 'Lv.${index + 1}'),
|
|
growable: false,
|
|
);
|
|
}
|
|
|
|
static List<RoomRocketRewardItem> _normalizeRewards(
|
|
List<RoomRocketRewardItem>? rewards,
|
|
) {
|
|
final source =
|
|
rewards?.where((item) => item.amount.trim().isNotEmpty).toList() ??
|
|
const <RoomRocketRewardItem>[];
|
|
if (source.isNotEmpty) {
|
|
return source;
|
|
}
|
|
return const [
|
|
RoomRocketRewardItem(asset: _RocketAssets.rewardThumb, amount: '10000'),
|
|
RoomRocketRewardItem(asset: _RocketAssets.rewardThumb, amount: '10000'),
|
|
RoomRocketRewardItem(asset: _RocketAssets.rewardThumb, amount: '10000'),
|
|
RoomRocketRewardItem(asset: _RocketAssets.rewardThumb, amount: '10000'),
|
|
];
|
|
}
|
|
|
|
static List<RoomRocketCrewMember> _normalizeCrew(
|
|
List<RoomRocketCrewMember>? crew,
|
|
SCRoomRocketStatusRes? status,
|
|
) {
|
|
final hasNoContributors =
|
|
status != null && (status.totalContributors ?? 0) <= 0;
|
|
if (hasNoContributors) {
|
|
return const <RoomRocketCrewMember>[];
|
|
}
|
|
final source =
|
|
crew?.where((item) => item.asset != null || item.imageUrl != null) ??
|
|
const <RoomRocketCrewMember>[];
|
|
if (source.isNotEmpty) {
|
|
return source.take(3).toList();
|
|
}
|
|
return const <RoomRocketCrewMember>[];
|
|
}
|
|
|
|
static double _normalizePercent(num? value) {
|
|
if (value == null) {
|
|
return 100;
|
|
}
|
|
final raw = value.toDouble();
|
|
final percent = raw > 0 && raw <= 1 ? raw * 100 : raw;
|
|
return percent.clamp(0, 100).toDouble();
|
|
}
|
|
}
|
|
|
|
class RoomRocketRewardItem {
|
|
const RoomRocketRewardItem({this.asset, this.imageUrl, required this.amount});
|
|
|
|
final String? asset;
|
|
final String? imageUrl;
|
|
final String amount;
|
|
}
|
|
|
|
class RoomRocketLevelItem {
|
|
const RoomRocketLevelItem({
|
|
required this.label,
|
|
this.asset,
|
|
this.imageUrl,
|
|
this.enabled = true,
|
|
});
|
|
|
|
final String label;
|
|
final String? asset;
|
|
final String? imageUrl;
|
|
final bool enabled;
|
|
}
|
|
|
|
class RoomRocketCrewMember {
|
|
const RoomRocketCrewMember({this.asset, this.imageUrl});
|
|
|
|
final String? asset;
|
|
final String? imageUrl;
|
|
}
|
|
|
|
enum RoomRocketProgressStage {
|
|
empty,
|
|
charging,
|
|
full;
|
|
|
|
static RoomRocketProgressStage fromPercent(double percent) {
|
|
if (percent <= 0) {
|
|
return RoomRocketProgressStage.empty;
|
|
}
|
|
if (percent >= 99.5) {
|
|
return RoomRocketProgressStage.full;
|
|
}
|
|
return RoomRocketProgressStage.charging;
|
|
}
|
|
}
|
|
|
|
enum RoomRocketBottomStage { empty, active }
|
|
|
|
class _RocketCanvas extends StatelessWidget {
|
|
const _RocketCanvas({
|
|
required this.scale,
|
|
required this.percent,
|
|
required this.progressStage,
|
|
required this.bottomStage,
|
|
required this.round,
|
|
required this.levels,
|
|
required this.selectedLevel,
|
|
this.onLevelSelected,
|
|
required this.rewards,
|
|
required this.crew,
|
|
this.onClose,
|
|
this.onOpenRank,
|
|
this.onRewardSelected,
|
|
this.onOpenRecord,
|
|
this.onOpenNote,
|
|
});
|
|
|
|
final double scale;
|
|
final double percent;
|
|
final RoomRocketProgressStage progressStage;
|
|
final RoomRocketBottomStage bottomStage;
|
|
final int round;
|
|
final List<RoomRocketLevelItem> levels;
|
|
final int selectedLevel;
|
|
final ValueChanged<int>? onLevelSelected;
|
|
final List<RoomRocketRewardItem> rewards;
|
|
final List<RoomRocketCrewMember> crew;
|
|
final VoidCallback? onClose;
|
|
final VoidCallback? onOpenRank;
|
|
final ValueChanged<RoomRocketRewardItem>? onRewardSelected;
|
|
final VoidCallback? onOpenRecord;
|
|
final VoidCallback? onOpenNote;
|
|
|
|
double s(double value) => value * scale;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Stack(
|
|
clipBehavior: Clip.hardEdge,
|
|
children: [
|
|
_image(_RocketAssets.rocketStage, 15, 361, 344, 97),
|
|
_image(_RocketAssets.rocketBody, 88, 111, 200, 432),
|
|
_image(_RocketAssets.sidePanel, 309, 202, 66, 207),
|
|
_image(_RocketAssets.sidePanel, 0, 202, 66, 207),
|
|
_buildLevelRail(),
|
|
_buildProgress(),
|
|
_buildSideActions(),
|
|
_buildRoundTitle(),
|
|
_buildResetTime(),
|
|
_buildRewardPanel(),
|
|
_buildBottomStrip(),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _image(
|
|
String asset,
|
|
double left,
|
|
double top,
|
|
double width,
|
|
double height, {
|
|
double opacity = 1,
|
|
BoxFit fit = BoxFit.fill,
|
|
}) {
|
|
return Positioned(
|
|
left: s(left),
|
|
top: s(top),
|
|
width: s(width),
|
|
height: s(height),
|
|
child: Opacity(
|
|
opacity: opacity,
|
|
child: Image.asset(asset, fit: fit, gaplessPlayback: true),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildRoundTitle() {
|
|
return Positioned(
|
|
left: 0,
|
|
top: s(120),
|
|
width: s(_designWidth),
|
|
child: _GradientText(
|
|
'Round $round Today',
|
|
fontSize: s(16),
|
|
fontWeight: FontWeight.w600,
|
|
textAlign: TextAlign.center,
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildLevelRail() {
|
|
return Positioned(
|
|
left: s(13),
|
|
top: s(212),
|
|
width: s(40),
|
|
height: s(190),
|
|
child: ScrollConfiguration(
|
|
behavior: const _NoGlowScrollBehavior(),
|
|
child: ListView.separated(
|
|
padding: EdgeInsets.zero,
|
|
primary: false,
|
|
physics: const BouncingScrollPhysics(),
|
|
itemCount: levels.length,
|
|
separatorBuilder: (_, __) => SizedBox(height: s(4)),
|
|
itemBuilder: (context, index) {
|
|
final item = levels[index];
|
|
return GestureDetector(
|
|
behavior: HitTestBehavior.opaque,
|
|
onTap: item.enabled ? () => onLevelSelected?.call(index) : null,
|
|
child: SizedBox(
|
|
width: s(40),
|
|
height: s(52),
|
|
child: _LevelBadge(
|
|
scale: scale,
|
|
item: item,
|
|
selected: index == selectedLevel,
|
|
),
|
|
),
|
|
);
|
|
},
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
Widget _buildProgress() {
|
|
final asset = switch (progressStage) {
|
|
RoomRocketProgressStage.empty => _RocketAssets.progressEmpty,
|
|
RoomRocketProgressStage.charging => _RocketAssets.progressPartial,
|
|
RoomRocketProgressStage.full => _RocketAssets.progressFull,
|
|
};
|
|
return Stack(
|
|
children: [
|
|
_image(asset, 320, 227, 44, 134),
|
|
Positioned(
|
|
left: s(316),
|
|
top: s(373),
|
|
width: s(56),
|
|
child: _GradientText(
|
|
'${percent.round()}%',
|
|
fontSize: s(16),
|
|
fontWeight: FontWeight.w600,
|
|
textAlign: TextAlign.center,
|
|
colors: const [Color(0xFFA4FFE3), Color(0xFF21C593)],
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildSideActions() {
|
|
return Stack(
|
|
children: [
|
|
_image(_RocketAssets.pillBg, 306, 122, 220, 33),
|
|
Positioned(
|
|
left: s(335),
|
|
top: s(127),
|
|
width: s(22),
|
|
height: s(22),
|
|
child: GestureDetector(
|
|
behavior: HitTestBehavior.opaque,
|
|
onTap: onOpenNote,
|
|
child: DecoratedBox(
|
|
decoration: BoxDecoration(
|
|
shape: BoxShape.circle,
|
|
border: Border.all(
|
|
color: const Color(0xFFA4FFE3).withValues(alpha: 0.95),
|
|
width: s(1),
|
|
),
|
|
),
|
|
child: Center(
|
|
child: Text(
|
|
'?',
|
|
textScaler: TextScaler.noScaling,
|
|
style: TextStyle(
|
|
color: Colors.white,
|
|
fontSize: s(15),
|
|
height: 1,
|
|
fontWeight: FontWeight.w700,
|
|
decoration: TextDecoration.none,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
_image(_RocketAssets.pillBg, 306, 159, 220, 33),
|
|
Positioned(
|
|
left: s(306),
|
|
top: s(159),
|
|
width: s(69),
|
|
height: s(33),
|
|
child: GestureDetector(
|
|
behavior: HitTestBehavior.opaque,
|
|
onTap: onOpenRecord,
|
|
child: Center(
|
|
child: _GradientText(
|
|
'Record',
|
|
fontSize: s(14),
|
|
fontWeight: FontWeight.w600,
|
|
textAlign: TextAlign.center,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildResetTime() {
|
|
return Stack(
|
|
children: [
|
|
_image(_RocketAssets.pillBg, 78, 458, 220, 33),
|
|
Positioned(
|
|
left: s(109),
|
|
top: s(468),
|
|
child: _RocketText(
|
|
'Reset Time',
|
|
fontSize: s(12),
|
|
color: Colors.white,
|
|
),
|
|
),
|
|
for (final item in const [
|
|
_TimerToken('23', 193),
|
|
_TimerToken('23', 223),
|
|
_TimerToken('23', 253),
|
|
])
|
|
Positioned(
|
|
left: s(item.left),
|
|
top: s(468),
|
|
child: _RocketText(
|
|
item.value,
|
|
fontSize: s(12),
|
|
color: Color(0xFF2DFFC1),
|
|
),
|
|
),
|
|
for (final left in const [209.0, 239.0])
|
|
Positioned(
|
|
left: s(left),
|
|
top: s(468),
|
|
child: _RocketText(
|
|
':',
|
|
fontSize: s(12),
|
|
color: const Color(0xFF2DFFC1),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildRewardPanel() {
|
|
return Stack(
|
|
children: [
|
|
_image(_RocketAssets.rewardTop, 8, 499, 360, 63),
|
|
_image(_RocketAssets.rewardMiddle, 8, 559, 360, 59),
|
|
_image(_RocketAssets.rewardMiddle, 8, 594, 360, 59),
|
|
_image(_RocketAssets.rewardBottom, 8, 653, 360, 76),
|
|
Positioned(
|
|
left: 0,
|
|
top: s(519),
|
|
width: s(_designWidth),
|
|
child: const _GradientText(
|
|
'Reward',
|
|
fontSize: 16,
|
|
fontWeight: FontWeight.w600,
|
|
textAlign: TextAlign.center,
|
|
).scaled(scale),
|
|
),
|
|
Positioned(
|
|
left: s(28),
|
|
top: s(543),
|
|
width: s(320),
|
|
height: math.max(1, s(1)),
|
|
child: DecoratedBox(
|
|
decoration: BoxDecoration(
|
|
color: const Color(0xFFB2FFE7).withValues(alpha: 0.16),
|
|
),
|
|
),
|
|
),
|
|
_RewardTab(
|
|
scale: scale,
|
|
left: 31,
|
|
top: 552,
|
|
label: 'Top1',
|
|
active: true,
|
|
),
|
|
_RewardTab(scale: scale, left: 147, top: 552, label: 'Set off'),
|
|
_RewardTab(scale: scale, left: 252, top: 552, label: 'In the room'),
|
|
Positioned(
|
|
left: s(28),
|
|
top: s(578),
|
|
width: s(336),
|
|
height: s(135),
|
|
child: ClipRect(
|
|
child: ScrollConfiguration(
|
|
behavior: const _NoGlowScrollBehavior(),
|
|
child: SingleChildScrollView(
|
|
scrollDirection: Axis.horizontal,
|
|
physics: const BouncingScrollPhysics(),
|
|
child: Row(
|
|
children: [
|
|
for (var index = 0; index < rewards.length; index++) ...[
|
|
SizedBox(
|
|
width: s(97),
|
|
height: s(135),
|
|
child: _RewardCard(
|
|
scale: scale,
|
|
item: rewards[index],
|
|
onTap: () => onRewardSelected?.call(rewards[index]),
|
|
),
|
|
),
|
|
if (index != rewards.length - 1) SizedBox(width: s(12)),
|
|
],
|
|
],
|
|
),
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
Widget _buildBottomStrip() {
|
|
final activeBottom = bottomStage == RoomRocketBottomStage.active;
|
|
final showCrew = activeBottom && crew.isNotEmpty;
|
|
return Stack(
|
|
children: [
|
|
Positioned(
|
|
left: s(0),
|
|
top: s(729),
|
|
width: s(375),
|
|
height: s(83),
|
|
child: Transform.rotate(
|
|
angle: math.pi,
|
|
child: Image.asset(
|
|
activeBottom
|
|
? _RocketAssets.bottomStripActive
|
|
: _RocketAssets.bottomStripEmpty,
|
|
fit: BoxFit.fill,
|
|
gaplessPlayback: true,
|
|
),
|
|
),
|
|
),
|
|
_image(_RocketAssets.bottomTrophy, 31, 740, 52, 69),
|
|
if (!activeBottom)
|
|
Positioned(
|
|
left: s(108),
|
|
top: s(750),
|
|
width: s(169),
|
|
height: s(36),
|
|
child: FittedBox(
|
|
fit: BoxFit.scaleDown,
|
|
alignment: Alignment.center,
|
|
child: Text(
|
|
'Not up to Rocket rank; no\n“Rocket King” title',
|
|
textScaler: TextScaler.noScaling,
|
|
maxLines: 2,
|
|
softWrap: false,
|
|
overflow: TextOverflow.visible,
|
|
textAlign: TextAlign.center,
|
|
style: TextStyle(
|
|
color: Colors.white,
|
|
fontSize: s(14),
|
|
height: 16 / 14,
|
|
fontWeight: FontWeight.w400,
|
|
decoration: TextDecoration.none,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
if (showCrew)
|
|
for (var index = 0; index < crew.length.clamp(0, 3); index++)
|
|
Positioned(
|
|
left: s(97 + 71.0 * index),
|
|
top: s(753),
|
|
width: s(50),
|
|
height: s(50),
|
|
child: _CrewAvatar(scale: scale, member: crew[index]),
|
|
),
|
|
Positioned(
|
|
left: s(315),
|
|
top: s(753),
|
|
width: s(52),
|
|
height: s(52),
|
|
child: GestureDetector(
|
|
behavior: HitTestBehavior.opaque,
|
|
onTap: onOpenRank ?? onClose,
|
|
child: Center(
|
|
child: Image.asset(
|
|
_RocketAssets.backArrow,
|
|
width: s(32),
|
|
height: s(32),
|
|
fit: BoxFit.fill,
|
|
gaplessPlayback: true,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
Positioned(
|
|
left: s(0),
|
|
top: s(729),
|
|
width: s(375),
|
|
height: s(83),
|
|
child: GestureDetector(
|
|
behavior: HitTestBehavior.opaque,
|
|
onTap: onOpenRank ?? onClose,
|
|
child: const SizedBox.expand(),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|
|
|
|
class _RewardCard extends StatelessWidget {
|
|
const _RewardCard({required this.scale, required this.item, this.onTap});
|
|
|
|
final double scale;
|
|
final RoomRocketRewardItem item;
|
|
final VoidCallback? onTap;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return GestureDetector(
|
|
behavior: HitTestBehavior.opaque,
|
|
onTap: onTap,
|
|
child: Stack(
|
|
children: [
|
|
Positioned.fill(child: CustomPaint(painter: _RewardCardPainter())),
|
|
Positioned(
|
|
left: 23 * scale,
|
|
top: 24 * scale,
|
|
width: 50 * scale,
|
|
height: 50 * scale,
|
|
child: _RocketImage(
|
|
asset: item.asset,
|
|
imageUrl: item.imageUrl,
|
|
fit: BoxFit.cover,
|
|
),
|
|
),
|
|
Positioned(
|
|
left: 21 * scale,
|
|
top: 106 * scale,
|
|
width: 14 * scale,
|
|
height: 14 * scale,
|
|
child: Image.asset(_RocketAssets.coin, fit: BoxFit.contain),
|
|
),
|
|
Positioned(
|
|
left: 37 * scale,
|
|
top: 105 * scale,
|
|
width: 46 * scale,
|
|
child: _RocketText(
|
|
item.amount,
|
|
fontSize: 14 * scale,
|
|
color: Colors.white,
|
|
textAlign: TextAlign.left,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _LevelBadge extends StatelessWidget {
|
|
const _LevelBadge({
|
|
required this.scale,
|
|
required this.item,
|
|
required this.selected,
|
|
});
|
|
|
|
final double scale;
|
|
final RoomRocketLevelItem item;
|
|
final bool selected;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return SizedBox(
|
|
width: 40 * scale,
|
|
height: 52 * scale,
|
|
child: Stack(
|
|
children: [
|
|
Positioned.fill(
|
|
child: CustomPaint(
|
|
painter: _LevelBadgePainter(
|
|
selected: selected,
|
|
enabled: item.enabled,
|
|
),
|
|
),
|
|
),
|
|
Positioned(
|
|
left: 10 * scale,
|
|
top: 6 * scale,
|
|
width: 20 * scale,
|
|
height: 29 * scale,
|
|
child: ClipRRect(
|
|
borderRadius: BorderRadius.circular(4 * scale),
|
|
child:
|
|
item.imageUrl != null || item.asset != null
|
|
? _RocketImage(
|
|
asset: item.asset,
|
|
imageUrl: item.imageUrl,
|
|
fit: BoxFit.cover,
|
|
)
|
|
: Stack(
|
|
clipBehavior: Clip.hardEdge,
|
|
children: [
|
|
Positioned(
|
|
left: -5.45 * scale,
|
|
top: -10.92 * scale,
|
|
width: 30.86 * scale,
|
|
height: 66.82 * scale,
|
|
child: Image.asset(
|
|
_RocketAssets.rocketBody,
|
|
fit: BoxFit.cover,
|
|
gaplessPlayback: true,
|
|
),
|
|
),
|
|
],
|
|
),
|
|
),
|
|
),
|
|
Positioned(
|
|
left: 0,
|
|
right: 0,
|
|
top: 38 * scale,
|
|
child: _RocketText(
|
|
item.label,
|
|
fontSize: 10 * scale,
|
|
color:
|
|
item.enabled
|
|
? Colors.white
|
|
: Colors.white.withValues(alpha: 0.52),
|
|
),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _CrewAvatar extends StatelessWidget {
|
|
const _CrewAvatar({required this.scale, required this.member});
|
|
|
|
final double scale;
|
|
final RoomRocketCrewMember member;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Stack(
|
|
alignment: Alignment.center,
|
|
children: [
|
|
Image.asset(
|
|
_RocketAssets.bottomAvatarRing,
|
|
width: 40 * scale,
|
|
height: 40 * scale,
|
|
fit: BoxFit.fill,
|
|
),
|
|
ClipOval(
|
|
child: SizedBox(
|
|
width: 42 * scale,
|
|
height: 42 * scale,
|
|
child: _RocketImage(
|
|
asset: member.asset,
|
|
imageUrl: member.imageUrl,
|
|
fit: BoxFit.cover,
|
|
),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|
|
|
|
class _RocketImage extends StatelessWidget {
|
|
const _RocketImage({this.asset, this.imageUrl, this.fit = BoxFit.cover});
|
|
|
|
final String? asset;
|
|
final String? imageUrl;
|
|
final BoxFit fit;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
if (imageUrl != null && imageUrl!.trim().isNotEmpty) {
|
|
return CustomCachedImage(imageUrl: imageUrl!, fit: fit);
|
|
}
|
|
if (asset != null && asset!.trim().isNotEmpty) {
|
|
return Image.asset(asset!, fit: fit, gaplessPlayback: true);
|
|
}
|
|
return const SizedBox.shrink();
|
|
}
|
|
}
|
|
|
|
class _RewardTab extends StatelessWidget {
|
|
const _RewardTab({
|
|
required this.scale,
|
|
required this.left,
|
|
required this.top,
|
|
required this.label,
|
|
this.active = false,
|
|
});
|
|
|
|
final double scale;
|
|
final double left;
|
|
final double top;
|
|
final String label;
|
|
final bool active;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final color =
|
|
active
|
|
? const Color(0xFF75F3CB)
|
|
: const Color(0xFF75F3CB).withValues(alpha: 0.52);
|
|
final width =
|
|
label == 'In the room'
|
|
? 93.0
|
|
: label == 'Set off'
|
|
? 76.0
|
|
: 68.0;
|
|
return Positioned(
|
|
left: left * scale,
|
|
top: top * scale,
|
|
width: width * scale,
|
|
height: 14 * scale,
|
|
child: Row(
|
|
mainAxisAlignment: MainAxisAlignment.center,
|
|
children: [
|
|
_TabSlash(color: color, scale: scale),
|
|
SizedBox(width: 3 * scale),
|
|
Flexible(
|
|
child: Text(
|
|
label,
|
|
textScaler: TextScaler.noScaling,
|
|
maxLines: 1,
|
|
overflow: TextOverflow.visible,
|
|
textAlign: TextAlign.center,
|
|
style: TextStyle(
|
|
color: color,
|
|
fontSize: 12 * scale,
|
|
height: 1,
|
|
decoration: TextDecoration.none,
|
|
),
|
|
),
|
|
),
|
|
SizedBox(width: 3 * scale),
|
|
Transform.rotate(
|
|
angle: math.pi,
|
|
child: _TabSlash(color: color, scale: scale),
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _TabSlash extends StatelessWidget {
|
|
const _TabSlash({required this.color, required this.scale});
|
|
|
|
final Color color;
|
|
final double scale;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Text(
|
|
'///',
|
|
textScaler: TextScaler.noScaling,
|
|
style: TextStyle(
|
|
color: color,
|
|
fontSize: 10 * scale,
|
|
height: 1,
|
|
letterSpacing: 0,
|
|
decoration: TextDecoration.none,
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _NoGlowScrollBehavior extends ScrollBehavior {
|
|
const _NoGlowScrollBehavior();
|
|
|
|
@override
|
|
Widget buildOverscrollIndicator(
|
|
BuildContext context,
|
|
Widget child,
|
|
ScrollableDetails details,
|
|
) {
|
|
return child;
|
|
}
|
|
}
|
|
|
|
class _LevelBadgePainter extends CustomPainter {
|
|
const _LevelBadgePainter({required this.selected, required this.enabled});
|
|
|
|
final bool selected;
|
|
final bool enabled;
|
|
|
|
@override
|
|
void paint(Canvas canvas, Size size) {
|
|
final path = _scaledPath(
|
|
const [
|
|
Offset(35.625, 0.5),
|
|
Offset(39.5, 4.39941),
|
|
Offset(39.5, 47.5996),
|
|
Offset(35.625, 51.5),
|
|
Offset(5.18164, 51.5),
|
|
Offset(0.5, 47.5732),
|
|
Offset(0.5, 4.39941),
|
|
Offset(4.375, 0.5),
|
|
],
|
|
size,
|
|
const Size(40, 52),
|
|
);
|
|
canvas.drawPath(
|
|
path,
|
|
Paint()
|
|
..shader = const LinearGradient(
|
|
begin: Alignment.topCenter,
|
|
end: Alignment.bottomCenter,
|
|
colors: [Color(0xFF077C60), Color(0xFF08251E)],
|
|
).createShader(Offset.zero & size),
|
|
);
|
|
canvas.drawPath(
|
|
path,
|
|
Paint()
|
|
..style = PaintingStyle.stroke
|
|
..strokeWidth = selected ? size.width / 22 : size.width / 40
|
|
..shader = LinearGradient(
|
|
begin: Alignment.topCenter,
|
|
end: Alignment.bottomCenter,
|
|
colors: [
|
|
enabled
|
|
? const Color(0xFFB2FBCC)
|
|
: const Color(0xFFB2FBCC).withValues(alpha: 0.42),
|
|
const Color(0xFFB2FBCC).withValues(alpha: 0),
|
|
],
|
|
).createShader(Offset.zero & size),
|
|
);
|
|
}
|
|
|
|
@override
|
|
bool shouldRepaint(covariant _LevelBadgePainter oldDelegate) {
|
|
return oldDelegate.selected != selected || oldDelegate.enabled != enabled;
|
|
}
|
|
}
|
|
|
|
class _RewardCardPainter extends CustomPainter {
|
|
@override
|
|
void paint(Canvas canvas, Size size) {
|
|
final panelPath = _scaledPath(
|
|
const [
|
|
Offset(88.293, 0.5),
|
|
Offset(96.5, 8.70703),
|
|
Offset(96.5, 126.293),
|
|
Offset(88.293, 134.5),
|
|
Offset(8.70703, 134.5),
|
|
Offset(0.5, 126.293),
|
|
Offset(0.5, 8.70703),
|
|
Offset(8.70703, 0.5),
|
|
],
|
|
size,
|
|
const Size(97, 135),
|
|
);
|
|
canvas.drawPath(
|
|
panelPath,
|
|
Paint()
|
|
..shader = const LinearGradient(
|
|
begin: Alignment.topCenter,
|
|
end: Alignment.bottomCenter,
|
|
colors: [Color(0xFF077C60), Color(0xFF08251E)],
|
|
).createShader(Offset.zero & size),
|
|
);
|
|
canvas.drawPath(
|
|
panelPath,
|
|
Paint()
|
|
..style = PaintingStyle.stroke
|
|
..strokeWidth = size.width / 97
|
|
..shader = LinearGradient(
|
|
begin: Alignment.topCenter,
|
|
end: Alignment.bottomCenter,
|
|
colors: [
|
|
const Color(0xFFB2FBCC),
|
|
const Color(0xFFB2FBCC).withValues(alpha: 0),
|
|
],
|
|
).createShader(Offset.zero & size),
|
|
);
|
|
|
|
final pillPath = _scaledPath(
|
|
const [
|
|
Offset(8, 107.367),
|
|
Offset(14.5, 101),
|
|
Offset(82.5, 101),
|
|
Offset(89, 107.367),
|
|
Offset(89, 118.633),
|
|
Offset(82.5, 125),
|
|
Offset(14.5, 125),
|
|
Offset(8, 118.633),
|
|
],
|
|
size,
|
|
const Size(97, 135),
|
|
);
|
|
canvas.drawPath(
|
|
pillPath,
|
|
Paint()
|
|
..shader = const LinearGradient(
|
|
begin: Alignment.centerLeft,
|
|
end: Alignment.centerRight,
|
|
colors: [Color(0xFF136648), Color(0xFF06412B), Color(0xFF136648)],
|
|
).createShader(Offset.zero & size),
|
|
);
|
|
canvas.drawPath(
|
|
pillPath,
|
|
Paint()
|
|
..style = PaintingStyle.stroke
|
|
..strokeWidth = size.width / 97
|
|
..color = const Color(0xFF8BF9FE).withValues(alpha: 0.85),
|
|
);
|
|
|
|
final circleCenter = Offset(
|
|
size.width * 48.5 / 97,
|
|
size.height * 48.5 / 135,
|
|
);
|
|
final circleRadius = math.min(size.width / 97, size.height / 135) * 37;
|
|
canvas.drawCircle(
|
|
circleCenter,
|
|
circleRadius,
|
|
Paint()
|
|
..style = PaintingStyle.stroke
|
|
..strokeWidth = size.width / 97
|
|
..maskFilter = MaskFilter.blur(BlurStyle.normal, size.width / 24)
|
|
..color = const Color(0xFFEFFBFF).withValues(alpha: 0.6),
|
|
);
|
|
canvas.drawCircle(
|
|
circleCenter,
|
|
circleRadius,
|
|
Paint()
|
|
..style = PaintingStyle.stroke
|
|
..strokeWidth = size.width / 97
|
|
..color = const Color(0xFF75F3CB),
|
|
);
|
|
}
|
|
|
|
@override
|
|
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
|
|
}
|
|
|
|
Path _scaledPath(List<Offset> points, Size size, Size viewBox) {
|
|
final path = Path();
|
|
for (var index = 0; index < points.length; index++) {
|
|
final point = Offset(
|
|
points[index].dx / viewBox.width * size.width,
|
|
points[index].dy / viewBox.height * size.height,
|
|
);
|
|
if (index == 0) {
|
|
path.moveTo(point.dx, point.dy);
|
|
} else {
|
|
path.lineTo(point.dx, point.dy);
|
|
}
|
|
}
|
|
return path..close();
|
|
}
|
|
|
|
class _RocketText extends StatelessWidget {
|
|
const _RocketText(
|
|
this.text, {
|
|
required this.fontSize,
|
|
required this.color,
|
|
this.textAlign = TextAlign.center,
|
|
});
|
|
|
|
final String text;
|
|
final double fontSize;
|
|
final Color color;
|
|
final TextAlign textAlign;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Text(
|
|
text,
|
|
textScaler: TextScaler.noScaling,
|
|
maxLines: 1,
|
|
overflow: TextOverflow.ellipsis,
|
|
textAlign: textAlign,
|
|
style: TextStyle(
|
|
color: color,
|
|
fontSize: fontSize,
|
|
height: 1,
|
|
decoration: TextDecoration.none,
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _GradientText extends StatelessWidget {
|
|
const _GradientText(
|
|
this.text, {
|
|
required this.fontSize,
|
|
this.fontWeight = FontWeight.w400,
|
|
this.textAlign = TextAlign.left,
|
|
this.colors = const [Colors.white, Color(0xFFA4FFE3)],
|
|
});
|
|
|
|
final String text;
|
|
final double fontSize;
|
|
final FontWeight fontWeight;
|
|
final TextAlign textAlign;
|
|
final List<Color> colors;
|
|
|
|
_GradientText scaled(double scale) {
|
|
return _GradientText(
|
|
text,
|
|
fontSize: fontSize * scale,
|
|
fontWeight: fontWeight,
|
|
textAlign: textAlign,
|
|
colors: colors,
|
|
);
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return ShaderMask(
|
|
shaderCallback:
|
|
(bounds) => LinearGradient(
|
|
begin: Alignment.topCenter,
|
|
end: Alignment.bottomCenter,
|
|
colors: colors,
|
|
).createShader(bounds),
|
|
blendMode: BlendMode.srcIn,
|
|
child: Text(
|
|
text,
|
|
textScaler: TextScaler.noScaling,
|
|
maxLines: 1,
|
|
textAlign: textAlign,
|
|
overflow: TextOverflow.ellipsis,
|
|
style: TextStyle(
|
|
color: Colors.white,
|
|
fontSize: fontSize,
|
|
height: 1,
|
|
fontWeight: fontWeight,
|
|
decoration: TextDecoration.none,
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _TimerToken {
|
|
const _TimerToken(this.value, this.left);
|
|
|
|
final String value;
|
|
final double left;
|
|
}
|
|
|
|
class _RocketAssets {
|
|
const _RocketAssets._();
|
|
|
|
static const String rocketStage = 'sc_images/room/rocket/rocket_stage.png';
|
|
static const String rocketBody = 'sc_images/room/rocket/rocket_body.png';
|
|
static const String sidePanel = 'sc_images/room/rocket/side_panel.png';
|
|
static const String bottomStripActive =
|
|
'sc_images/room/rocket/bottom_strip_active.png';
|
|
static const String bottomStripEmpty =
|
|
'sc_images/room/rocket/bottom_strip_active.png';
|
|
static const String bottomTrophy = 'sc_images/room/rocket/bottom_trophy.png';
|
|
static const String backArrow = 'sc_images/room/rocket/back_arrow.png';
|
|
static const String bottomAvatarRing =
|
|
'sc_images/room/rocket/bottom_avatar_ring.png';
|
|
static const String pillBg = 'sc_images/room/rocket/pill_bg.png';
|
|
static const String progressFull = 'sc_images/room/rocket/progress_full.png';
|
|
static const String progressPartial =
|
|
'sc_images/room/rocket/progress_partial.png';
|
|
static const String progressEmpty =
|
|
'sc_images/room/rocket/progress_empty.png';
|
|
static const String rewardTop = 'sc_images/room/rocket/reward_dialog_top.png';
|
|
static const String rewardMiddle =
|
|
'sc_images/room/rocket/reward_dialog_middle.png';
|
|
static const String rewardBottom =
|
|
'sc_images/room/rocket/reward_dialog_bottom.png';
|
|
static const String rewardThumb = 'sc_images/room/rocket/reward_thumb.png';
|
|
static const String coin = 'sc_images/register_reward/yumi_coin.png';
|
|
}
|
|
|
|
const double _designWidth = 375;
|
|
const double _designHeight = 812;
|