996 lines
29 KiB
Dart
996 lines
29 KiB
Dart
import 'dart:io';
|
|
import 'dart:math' as math;
|
|
|
|
import 'package:cached_network_image/cached_network_image.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter_screenutil/flutter_screenutil.dart';
|
|
import 'package:flutter_smart_dialog/flutter_smart_dialog.dart';
|
|
import 'package:provider/provider.dart';
|
|
import 'package:yumi/services/general/sc_app_general_manager.dart';
|
|
import 'package:yumi/services/payment/google_payment_manager.dart';
|
|
import 'package:yumi/shared/business_logic/models/res/sc_first_recharge_reward_res.dart';
|
|
import 'package:yumi/shared/tools/sc_loading_manager.dart';
|
|
import 'package:yumi/ui_kit/components/sc_debounce_widget.dart';
|
|
import 'package:yumi/ui_kit/components/sc_tts.dart';
|
|
|
|
enum FirstRechargeRewardStyle { coin, prop }
|
|
|
|
class FirstRechargeReward {
|
|
const FirstRechargeReward({
|
|
required this.title,
|
|
required this.style,
|
|
this.badgeText = '',
|
|
this.cover = '',
|
|
});
|
|
|
|
final String title;
|
|
final FirstRechargeRewardStyle style;
|
|
final String badgeText;
|
|
final String cover;
|
|
|
|
bool get isCoinReward => style == FirstRechargeRewardStyle.coin;
|
|
}
|
|
|
|
class FirstRechargePackage {
|
|
const FirstRechargePackage({
|
|
required this.priceText,
|
|
required this.rewards,
|
|
this.discountText = '',
|
|
this.totalValueText = '',
|
|
this.rechargeAmountCents = 0,
|
|
this.productPackage = '',
|
|
});
|
|
|
|
final String priceText;
|
|
final String discountText;
|
|
final String totalValueText;
|
|
final int rechargeAmountCents;
|
|
final String productPackage;
|
|
final List<FirstRechargeReward> rewards;
|
|
}
|
|
|
|
class FirstRechargeDialogData {
|
|
const FirstRechargeDialogData({
|
|
required this.packages,
|
|
this.timeRemainingText = '',
|
|
this.initialPackageIndex = 0,
|
|
});
|
|
|
|
final List<FirstRechargePackage> packages;
|
|
final String timeRemainingText;
|
|
final int initialPackageIndex;
|
|
|
|
factory FirstRechargeDialogData.mock() {
|
|
return const FirstRechargeDialogData(
|
|
timeRemainingText: 'Time remaining: 6 D 59 H 59 M 59S',
|
|
packages: [
|
|
FirstRechargePackage(
|
|
priceText: r'$0.99',
|
|
discountText: '70%',
|
|
totalValueText: 'Total value of gifts: xxx gold coins',
|
|
rewards: [
|
|
FirstRechargeReward(
|
|
title: 'NameName',
|
|
badgeText: '80000',
|
|
cover: _FirstRechargeAssets.rewardAvatarFrame,
|
|
style: FirstRechargeRewardStyle.prop,
|
|
),
|
|
FirstRechargeReward(
|
|
title: 'NameName',
|
|
badgeText: '240000',
|
|
cover: _FirstRechargeAssets.rewardAvatarFrame,
|
|
style: FirstRechargeRewardStyle.prop,
|
|
),
|
|
FirstRechargeReward(
|
|
title: '240,000 Coins',
|
|
cover: _FirstRechargeAssets.rewardCoinStack,
|
|
style: FirstRechargeRewardStyle.coin,
|
|
),
|
|
FirstRechargeReward(
|
|
title: 'Vehicle',
|
|
badgeText: '3d',
|
|
cover: _FirstRechargeAssets.rewardVehicle,
|
|
style: FirstRechargeRewardStyle.prop,
|
|
),
|
|
FirstRechargeReward(
|
|
title: 'Frame',
|
|
badgeText: '3d',
|
|
cover: _FirstRechargeAssets.rewardFrame,
|
|
style: FirstRechargeRewardStyle.prop,
|
|
),
|
|
FirstRechargeReward(
|
|
title: 'Gift',
|
|
badgeText: 'x1',
|
|
cover: _FirstRechargeAssets.rewardGift,
|
|
style: FirstRechargeRewardStyle.prop,
|
|
),
|
|
],
|
|
),
|
|
FirstRechargePackage(
|
|
priceText: r'$2.99',
|
|
discountText: '60%',
|
|
totalValueText: 'Total value of gifts: 720,000 gold coins',
|
|
rewards: [
|
|
FirstRechargeReward(
|
|
title: 'Avatar Frame',
|
|
badgeText: '7d',
|
|
cover: _FirstRechargeAssets.rewardAvatarFrame,
|
|
style: FirstRechargeRewardStyle.prop,
|
|
),
|
|
FirstRechargeReward(
|
|
title: 'Room Frame',
|
|
badgeText: '7d',
|
|
cover: _FirstRechargeAssets.rewardFrame,
|
|
style: FirstRechargeRewardStyle.prop,
|
|
),
|
|
FirstRechargeReward(
|
|
title: '720,000 Coins',
|
|
cover: _FirstRechargeAssets.rewardCoinStack,
|
|
style: FirstRechargeRewardStyle.coin,
|
|
),
|
|
FirstRechargeReward(
|
|
title: 'Vehicle',
|
|
badgeText: '7d',
|
|
cover: _FirstRechargeAssets.rewardVehicle,
|
|
style: FirstRechargeRewardStyle.prop,
|
|
),
|
|
FirstRechargeReward(
|
|
title: 'Gift',
|
|
badgeText: 'x3',
|
|
cover: _FirstRechargeAssets.rewardGift,
|
|
style: FirstRechargeRewardStyle.prop,
|
|
),
|
|
FirstRechargeReward(
|
|
title: 'Profile Frame',
|
|
badgeText: '7d',
|
|
cover: _FirstRechargeAssets.rewardAvatarFrame,
|
|
style: FirstRechargeRewardStyle.prop,
|
|
),
|
|
],
|
|
),
|
|
],
|
|
);
|
|
}
|
|
|
|
factory FirstRechargeDialogData.fromRewardHome(
|
|
SCFirstRechargeRewardHomeRes home,
|
|
) {
|
|
final levels = home.availableLevelConfigs.take(2).toList();
|
|
final packages = <FirstRechargePackage>[];
|
|
var selectedPackageIndex = 0;
|
|
for (final level in levels) {
|
|
final rewards =
|
|
level.rewardItems.where(_isDisplayableRewardItem).map((item) {
|
|
return FirstRechargeReward(
|
|
title: _rewardTitle(item),
|
|
style:
|
|
item.isCoinReward
|
|
? FirstRechargeRewardStyle.coin
|
|
: FirstRechargeRewardStyle.prop,
|
|
badgeText: item.displayBadgeText,
|
|
cover: item.cover,
|
|
);
|
|
}).toList();
|
|
if (rewards.isEmpty) {
|
|
continue;
|
|
}
|
|
if (level.current) {
|
|
selectedPackageIndex = packages.length;
|
|
}
|
|
packages.add(
|
|
FirstRechargePackage(
|
|
priceText: _formatPriceText(level),
|
|
discountText: level.discountText,
|
|
totalValueText: level.totalValueText,
|
|
rechargeAmountCents: level.rechargeAmountCents,
|
|
productPackage: level.productPackage,
|
|
rewards: rewards,
|
|
),
|
|
);
|
|
}
|
|
|
|
return FirstRechargeDialogData(
|
|
packages: packages,
|
|
initialPackageIndex: selectedPackageIndex,
|
|
);
|
|
}
|
|
|
|
static bool _isDisplayableRewardItem(SCFirstRechargeRewardItem item) {
|
|
if (item.isCoinReward) {
|
|
return item.coinAmount > 0;
|
|
}
|
|
return item.displayTitle.trim().isNotEmpty || item.cover.trim().isNotEmpty;
|
|
}
|
|
|
|
static String _rewardTitle(SCFirstRechargeRewardItem item) {
|
|
if (item.isCoinReward) {
|
|
return item.displayTitle;
|
|
}
|
|
return item.displayTitle;
|
|
}
|
|
|
|
static String _formatPriceText(SCFirstRechargeLevelConfig level) {
|
|
final amount = level.rechargeAmount.trim();
|
|
if (amount.isNotEmpty) {
|
|
return amount.startsWith(r'$') ? amount : '\$$amount';
|
|
}
|
|
if (level.rechargeAmountCents > 0) {
|
|
return '\$${(level.rechargeAmountCents / 100).toStringAsFixed(2)}';
|
|
}
|
|
return r'$0.00';
|
|
}
|
|
}
|
|
|
|
class FirstRechargeDialog extends StatefulWidget {
|
|
const FirstRechargeDialog({super.key, required this.data, this.onBuyNow});
|
|
|
|
static const String dialogTag = 'showFirstRechargeDialog';
|
|
static const double _designWidth = 340;
|
|
static const double _designHeight = 528;
|
|
|
|
final FirstRechargeDialogData data;
|
|
final ValueChanged<FirstRechargePackage>? onBuyNow;
|
|
|
|
static Future<void> show(
|
|
BuildContext context, {
|
|
required FirstRechargeDialogData data,
|
|
ValueChanged<FirstRechargePackage>? onBuyNow,
|
|
}) async {
|
|
SmartDialog.dismiss(tag: dialogTag);
|
|
await SmartDialog.show(
|
|
tag: dialogTag,
|
|
alignment: Alignment.center,
|
|
animationType: SmartAnimationType.fade,
|
|
clickMaskDismiss: true,
|
|
maskColor: Colors.black.withValues(alpha: 0.5),
|
|
builder: (_) => FirstRechargeDialog(data: data, onBuyNow: onBuyNow),
|
|
);
|
|
}
|
|
|
|
static Future<void> showFromRemote(
|
|
BuildContext context, {
|
|
bool forceRefresh = false,
|
|
}) async {
|
|
try {
|
|
SCLoadingManager.show(context: context);
|
|
final home = await Provider.of<SCAppGeneralManager>(
|
|
context,
|
|
listen: false,
|
|
).refreshFirstRechargeRewardHome(forceRefresh: forceRefresh);
|
|
if (!context.mounted) {
|
|
return;
|
|
}
|
|
if (home == null || !home.shouldShowRewardDialog) {
|
|
SmartDialog.dismiss(tag: dialogTag);
|
|
return;
|
|
}
|
|
final data = FirstRechargeDialogData.fromRewardHome(home);
|
|
if (data.packages.isEmpty) {
|
|
SmartDialog.dismiss(tag: dialogTag);
|
|
return;
|
|
}
|
|
SCLoadingManager.hide();
|
|
await show(
|
|
context,
|
|
data: data,
|
|
onBuyNow: (package) {
|
|
_startGooglePay(context, package);
|
|
},
|
|
);
|
|
} catch (e) {
|
|
SCTts.show('Failed to load first recharge rewards');
|
|
} finally {
|
|
SCLoadingManager.hide();
|
|
}
|
|
}
|
|
|
|
static Future<void> _startGooglePay(
|
|
BuildContext context,
|
|
FirstRechargePackage package,
|
|
) async {
|
|
if (!Platform.isAndroid) {
|
|
SCTts.show('Google Play payment is only available on Android');
|
|
return;
|
|
}
|
|
SmartDialog.dismiss(tag: dialogTag);
|
|
final processor = Provider.of<AndroidPaymentProcessor>(
|
|
context,
|
|
listen: false,
|
|
);
|
|
await processor.processFirstRechargePurchase(
|
|
context: context,
|
|
rechargeAmountCents: package.rechargeAmountCents,
|
|
productPackage: package.productPackage,
|
|
);
|
|
}
|
|
|
|
@override
|
|
State<FirstRechargeDialog> createState() => _FirstRechargeDialogState();
|
|
}
|
|
|
|
class _FirstRechargeDialogState extends State<FirstRechargeDialog> {
|
|
late int _selectedPackageIndex;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_selectedPackageIndex = widget.data.initialPackageIndex;
|
|
}
|
|
|
|
@override
|
|
void didUpdateWidget(covariant FirstRechargeDialog oldWidget) {
|
|
super.didUpdateWidget(oldWidget);
|
|
if (oldWidget.data != widget.data) {
|
|
_selectedPackageIndex = _clampedPackageIndex(_selectedPackageIndex);
|
|
}
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
if (widget.data.packages.isEmpty) {
|
|
return const SizedBox.shrink();
|
|
}
|
|
|
|
final screenWidth = MediaQuery.sizeOf(context).width;
|
|
final dialogWidth = math.min(
|
|
screenWidth - 35.w,
|
|
FirstRechargeDialog._designWidth.w,
|
|
);
|
|
|
|
return Material(
|
|
color: Colors.transparent,
|
|
child: Transform.translate(
|
|
offset: Offset(0, 11.w),
|
|
child: SizedBox(
|
|
width: dialogWidth,
|
|
child: AspectRatio(
|
|
aspectRatio:
|
|
FirstRechargeDialog._designWidth /
|
|
FirstRechargeDialog._designHeight,
|
|
child: LayoutBuilder(
|
|
builder: (context, constraints) {
|
|
final scale =
|
|
constraints.maxWidth / FirstRechargeDialog._designWidth;
|
|
final selectedPackage =
|
|
widget.data.packages[_clampedPackageIndex(
|
|
_selectedPackageIndex,
|
|
)];
|
|
|
|
return Stack(
|
|
clipBehavior: Clip.none,
|
|
children: [
|
|
_positionedImage(
|
|
asset: _FirstRechargeAssets.panelTop,
|
|
left: 0,
|
|
top: 0,
|
|
width: 340,
|
|
height: 219,
|
|
scale: scale,
|
|
),
|
|
_positionedImage(
|
|
asset: _FirstRechargeAssets.panelMiddle,
|
|
left: 0,
|
|
top: 219,
|
|
width: 340,
|
|
height: 166,
|
|
scale: scale,
|
|
),
|
|
_positionedImage(
|
|
asset: _FirstRechargeAssets.panelBottom,
|
|
left: 0,
|
|
top: 351,
|
|
width: 340,
|
|
height: 177,
|
|
scale: scale,
|
|
),
|
|
_positionedImage(
|
|
asset: _FirstRechargeAssets.title,
|
|
left: 45,
|
|
top: 26,
|
|
width: 246,
|
|
height: 116,
|
|
scale: scale,
|
|
),
|
|
Positioned(
|
|
top: 146 * scale,
|
|
left: 0,
|
|
right: 0,
|
|
child: _SingleLineText(
|
|
widget.data.timeRemainingText,
|
|
height: 12 * scale,
|
|
fontSize: 10 * scale,
|
|
fontWeight: FontWeight.w400,
|
|
color: Colors.black,
|
|
),
|
|
),
|
|
Positioned(
|
|
left: 50 * scale,
|
|
top: 168 * scale,
|
|
width: 240 * scale,
|
|
height: 31 * scale,
|
|
child: _PriceSelector(
|
|
packages: widget.data.packages,
|
|
selectedIndex: _clampedPackageIndex(
|
|
_selectedPackageIndex,
|
|
),
|
|
scale: scale,
|
|
onSelected: (index) {
|
|
setState(() {
|
|
_selectedPackageIndex = index;
|
|
});
|
|
},
|
|
),
|
|
),
|
|
..._buildRewardCards(selectedPackage.rewards, scale),
|
|
Positioned(
|
|
top: 425 * scale,
|
|
left: 0,
|
|
right: 0,
|
|
child: _SingleLineText(
|
|
selectedPackage.totalValueText,
|
|
height: 12 * scale,
|
|
fontSize: 10 * scale,
|
|
fontWeight: FontWeight.w400,
|
|
color: Colors.black,
|
|
),
|
|
),
|
|
Positioned(
|
|
left: 70 * scale,
|
|
top: 447 * scale,
|
|
width: 200 * scale,
|
|
height: 42 * scale,
|
|
child: SCDebounceWidget(
|
|
onTap: () => widget.onBuyNow?.call(selectedPackage),
|
|
child: Image.asset(
|
|
_FirstRechargeAssets.buyNowButton,
|
|
fit: BoxFit.fill,
|
|
),
|
|
),
|
|
),
|
|
if (selectedPackage.discountText.trim().isNotEmpty)
|
|
Positioned(
|
|
left: 238 * scale,
|
|
top: 435 * scale,
|
|
width: 38 * scale,
|
|
height: 38 * scale,
|
|
child: _DiscountBadge(
|
|
text: selectedPackage.discountText,
|
|
scale: scale,
|
|
),
|
|
),
|
|
Positioned(
|
|
left: 294 * scale,
|
|
top: 14 * scale,
|
|
width: 44 * scale,
|
|
height: 44 * scale,
|
|
child: SCDebounceWidget(
|
|
onTap:
|
|
() => SmartDialog.dismiss(
|
|
tag: FirstRechargeDialog.dialogTag,
|
|
),
|
|
child: Center(
|
|
child: Image.asset(
|
|
_FirstRechargeAssets.close,
|
|
width: 24 * scale,
|
|
height: 25 * scale,
|
|
fit: BoxFit.fill,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
},
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
|
|
int _clampedPackageIndex(int value) {
|
|
return value.clamp(0, widget.data.packages.length - 1).toInt();
|
|
}
|
|
|
|
List<Widget> _buildRewardCards(
|
|
List<FirstRechargeReward> rewards,
|
|
double scale,
|
|
) {
|
|
const positions = [
|
|
Offset(20, 211),
|
|
Offset(124, 211),
|
|
Offset(227, 211),
|
|
Offset(20, 316),
|
|
Offset(124, 316),
|
|
Offset(227, 316),
|
|
];
|
|
|
|
final visibleRewards = rewards.take(positions.length).toList();
|
|
return List<Widget>.generate(visibleRewards.length, (index) {
|
|
final reward = visibleRewards[index];
|
|
final position = positions[index];
|
|
return Positioned(
|
|
left: position.dx * scale,
|
|
top: position.dy * scale,
|
|
width: 93 * scale,
|
|
height: 97 * scale,
|
|
child: _RewardCard(reward: reward, scale: scale),
|
|
);
|
|
});
|
|
}
|
|
|
|
Widget _positionedImage({
|
|
required String asset,
|
|
required double left,
|
|
required double top,
|
|
required double width,
|
|
required double height,
|
|
required double scale,
|
|
}) {
|
|
return Positioned(
|
|
left: left * scale,
|
|
top: top * scale,
|
|
width: width * scale,
|
|
height: height * scale,
|
|
child: Image.asset(asset, fit: BoxFit.fill),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _PriceSelector extends StatelessWidget {
|
|
const _PriceSelector({
|
|
required this.packages,
|
|
required this.selectedIndex,
|
|
required this.scale,
|
|
required this.onSelected,
|
|
});
|
|
|
|
final List<FirstRechargePackage> packages;
|
|
final int selectedIndex;
|
|
final double scale;
|
|
final ValueChanged<int> onSelected;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final visiblePackages = packages.take(2).toList();
|
|
final normalizedSelectedIndex = selectedIndex.clamp(0, 1).toInt();
|
|
final selectedIsTrailing = normalizedSelectedIndex == 1;
|
|
|
|
return Stack(
|
|
children: [
|
|
Positioned.fill(
|
|
child: Image.asset(
|
|
_FirstRechargeAssets.priceTabsBg,
|
|
fit: BoxFit.fill,
|
|
),
|
|
),
|
|
AnimatedPositioned(
|
|
duration: const Duration(milliseconds: 180),
|
|
curve: Curves.easeOutCubic,
|
|
left: (selectedIsTrailing ? 120 : 5) * scale,
|
|
top: 2 * scale,
|
|
width: (selectedIsTrailing ? 120 : 110) * scale,
|
|
height: 27 * scale,
|
|
child: _PriceSelectedBackground(
|
|
scale: scale,
|
|
trailing: selectedIsTrailing,
|
|
),
|
|
),
|
|
for (var index = 0; index < visiblePackages.length; index++)
|
|
Positioned(
|
|
left: (index == 0 ? 5 : 125) * scale,
|
|
top: 7 * scale,
|
|
width: 110 * scale,
|
|
height: 16 * scale,
|
|
child: SCDebounceWidget(
|
|
onTap: () => onSelected(index),
|
|
child: _SingleLineText(
|
|
visiblePackages[index].priceText,
|
|
height: 16 * scale,
|
|
fontSize: 14 * scale,
|
|
fontWeight: FontWeight.w500,
|
|
color:
|
|
selectedIndex == index
|
|
? Colors.white
|
|
: const Color(0xFF6F4C2B),
|
|
),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|
|
|
|
class _PriceSelectedBackground extends StatelessWidget {
|
|
const _PriceSelectedBackground({required this.scale, required this.trailing});
|
|
|
|
final double scale;
|
|
final bool trailing;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final texture = SizedBox(
|
|
width: 188.25 * scale,
|
|
height: 39.52 * scale,
|
|
child: Image.asset(
|
|
_FirstRechargeAssets.priceSelectedTexture,
|
|
fit: BoxFit.fill,
|
|
),
|
|
);
|
|
|
|
return ClipPath(
|
|
clipper: _PriceSelectionClipper(trailing: trailing),
|
|
child: Stack(
|
|
clipBehavior: Clip.hardEdge,
|
|
children: [
|
|
Positioned(
|
|
left: trailing ? null : -19.58 * scale,
|
|
right: trailing ? -19.58 * scale : null,
|
|
top: -5.37 * scale,
|
|
child:
|
|
trailing
|
|
? Transform(
|
|
alignment: Alignment.center,
|
|
transform: Matrix4.diagonal3Values(-1.0, 1.0, 1.0),
|
|
child: texture,
|
|
)
|
|
: texture,
|
|
),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _PriceSelectionClipper extends CustomClipper<Path> {
|
|
const _PriceSelectionClipper({required this.trailing});
|
|
|
|
final bool trailing;
|
|
|
|
@override
|
|
Path getClip(Size size) {
|
|
final w = size.width;
|
|
final h = size.height;
|
|
final curveX = w * (11.2769 / 110);
|
|
final centerY = h * (13.3043 / 27);
|
|
|
|
if (trailing) {
|
|
return Path()
|
|
..moveTo(0, h)
|
|
..lineTo(0, 0)
|
|
..lineTo(w - curveX, 0)
|
|
..cubicTo(
|
|
w - w * (7.51795 / 110),
|
|
h * (0.521738 / 27),
|
|
w,
|
|
h * (3.91303 / 27),
|
|
w,
|
|
centerY,
|
|
)
|
|
..cubicTo(
|
|
w,
|
|
h * (22.6956 / 27),
|
|
w - w * (7.51795 / 110),
|
|
h * (26.3478 / 27),
|
|
w - curveX,
|
|
h,
|
|
)
|
|
..lineTo(0, h)
|
|
..close();
|
|
}
|
|
|
|
return Path()
|
|
..moveTo(w, h)
|
|
..lineTo(w, 0)
|
|
..lineTo(curveX, 0)
|
|
..cubicTo(
|
|
w * (7.51795 / 110),
|
|
h * (0.521738 / 27),
|
|
0,
|
|
h * (3.91303 / 27),
|
|
0,
|
|
centerY,
|
|
)
|
|
..cubicTo(
|
|
0,
|
|
h * (22.6956 / 27),
|
|
w * (7.51795 / 110),
|
|
h * (26.3478 / 27),
|
|
curveX,
|
|
h,
|
|
)
|
|
..lineTo(w, h)
|
|
..close();
|
|
}
|
|
|
|
@override
|
|
bool shouldReclip(covariant _PriceSelectionClipper oldClipper) {
|
|
return oldClipper.trailing != trailing;
|
|
}
|
|
}
|
|
|
|
class _RewardCard extends StatelessWidget {
|
|
const _RewardCard({required this.reward, required this.scale});
|
|
|
|
final FirstRechargeReward reward;
|
|
final double scale;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Stack(
|
|
children: [
|
|
Positioned.fill(
|
|
child: Image.asset(
|
|
reward.isCoinReward
|
|
? _FirstRechargeAssets.rewardCardCoin
|
|
: _FirstRechargeAssets.rewardCard,
|
|
fit: BoxFit.fill,
|
|
),
|
|
),
|
|
if (!reward.isCoinReward && reward.badgeText.trim().isNotEmpty)
|
|
Positioned(
|
|
left: 8 * scale,
|
|
top: 3 * scale,
|
|
width: 42 * scale,
|
|
height: 10 * scale,
|
|
child: _SingleLineText(
|
|
reward.badgeText,
|
|
height: 10 * scale,
|
|
fontSize: 8 * scale,
|
|
fontWeight: FontWeight.w400,
|
|
color: Colors.white,
|
|
alignment: Alignment.centerLeft,
|
|
),
|
|
),
|
|
Positioned(
|
|
left: 17 * scale,
|
|
top: 14 * scale,
|
|
width: 59 * scale,
|
|
height: 59 * scale,
|
|
child: _RewardCover(reward: reward, scale: scale),
|
|
),
|
|
Positioned(
|
|
left: 5 * scale,
|
|
right: 5 * scale,
|
|
top: 75 * scale,
|
|
height: 15 * scale,
|
|
child: _SingleLineText(
|
|
reward.title,
|
|
height: 15 * scale,
|
|
fontSize: 12 * scale,
|
|
fontWeight: FontWeight.w400,
|
|
color: const Color(0xFF6F4C2B),
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|
|
|
|
class _RewardCover extends StatelessWidget {
|
|
const _RewardCover({required this.reward, required this.scale});
|
|
|
|
final FirstRechargeReward reward;
|
|
final double scale;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
final cover = reward.cover.trim();
|
|
if (cover.startsWith('http://') || cover.startsWith('https://')) {
|
|
return CachedNetworkImage(
|
|
imageUrl: cover,
|
|
fit: BoxFit.contain,
|
|
placeholder: (_, __) => _fallbackCover(),
|
|
errorWidget: (_, __, ___) => _fallbackCover(),
|
|
);
|
|
}
|
|
|
|
if (cover.isNotEmpty) {
|
|
return Image.asset(
|
|
cover,
|
|
fit: BoxFit.contain,
|
|
errorBuilder: (_, __, ___) => _fallbackCover(),
|
|
);
|
|
}
|
|
|
|
return _fallbackCover();
|
|
}
|
|
|
|
Widget _fallbackCover() {
|
|
if (reward.isCoinReward) {
|
|
return Image.asset(
|
|
_FirstRechargeAssets.yumiCoin,
|
|
width: 42 * scale,
|
|
height: 42 * scale,
|
|
fit: BoxFit.contain,
|
|
);
|
|
}
|
|
|
|
final fallbackAsset = _fallbackAsset;
|
|
if (fallbackAsset.isNotEmpty) {
|
|
return Image.asset(fallbackAsset, fit: BoxFit.contain);
|
|
}
|
|
|
|
return Container(
|
|
width: 48 * scale,
|
|
height: 48 * scale,
|
|
decoration: const BoxDecoration(
|
|
shape: BoxShape.circle,
|
|
gradient: LinearGradient(
|
|
begin: Alignment.topCenter,
|
|
end: Alignment.bottomCenter,
|
|
colors: [Color(0xFFFFE5A8), Color(0xFFFFB04F)],
|
|
),
|
|
),
|
|
child: Icon(
|
|
Icons.auto_awesome_rounded,
|
|
size: 28 * scale,
|
|
color: const Color(0xFF6F4C2B),
|
|
),
|
|
);
|
|
}
|
|
|
|
String get _fallbackAsset {
|
|
final title = reward.title.trim().toLowerCase();
|
|
if (title.contains('vehicle')) {
|
|
return _FirstRechargeAssets.rewardVehicle;
|
|
}
|
|
if (title.contains('frame')) {
|
|
return _FirstRechargeAssets.rewardFrame;
|
|
}
|
|
if (title == 'vip' || title.contains('vip')) {
|
|
return _FirstRechargeAssets.rewardVip;
|
|
}
|
|
if (title.contains('gift')) {
|
|
return _FirstRechargeAssets.rewardGift;
|
|
}
|
|
if (title.contains('badge') || title.contains('medal')) {
|
|
return _FirstRechargeAssets.rewardBadge;
|
|
}
|
|
return '';
|
|
}
|
|
}
|
|
|
|
class _DiscountBadge extends StatelessWidget {
|
|
const _DiscountBadge({required this.text, required this.scale});
|
|
|
|
final String text;
|
|
final double scale;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Stack(
|
|
alignment: Alignment.center,
|
|
children: [
|
|
Positioned.fill(
|
|
child: Image.asset(
|
|
_FirstRechargeAssets.discountBadge,
|
|
fit: BoxFit.fill,
|
|
),
|
|
),
|
|
Positioned(
|
|
left: 5 * scale,
|
|
right: 5 * scale,
|
|
top: 8 * scale,
|
|
height: 11 * scale,
|
|
child: _SingleLineText(
|
|
text,
|
|
height: 11 * scale,
|
|
fontSize: 10 * scale,
|
|
fontWeight: FontWeight.w700,
|
|
color: const Color(0xFFFCF0B7),
|
|
shadows: [
|
|
Shadow(
|
|
color: const Color(0xFF01378F).withValues(alpha: 0.89),
|
|
offset: Offset(0, 1 * scale),
|
|
blurRadius: 1 * scale,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
Positioned(
|
|
left: 10 * scale,
|
|
right: 10 * scale,
|
|
top: 20 * scale,
|
|
height: 9 * scale,
|
|
child: _SingleLineText(
|
|
'off',
|
|
height: 9 * scale,
|
|
fontSize: 8 * scale,
|
|
fontWeight: FontWeight.w500,
|
|
color: const Color(0xFFFCF0B7),
|
|
shadows: [
|
|
Shadow(
|
|
color: const Color(0xFF01378F).withValues(alpha: 0.89),
|
|
offset: Offset(0, 1 * scale),
|
|
blurRadius: 1 * scale,
|
|
),
|
|
],
|
|
),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|
|
|
|
class _SingleLineText extends StatelessWidget {
|
|
const _SingleLineText(
|
|
this.text, {
|
|
required this.height,
|
|
required this.fontSize,
|
|
required this.fontWeight,
|
|
required this.color,
|
|
this.alignment = Alignment.center,
|
|
this.shadows,
|
|
});
|
|
|
|
final String text;
|
|
final double height;
|
|
final double fontSize;
|
|
final FontWeight fontWeight;
|
|
final Color color;
|
|
final Alignment alignment;
|
|
final List<Shadow>? shadows;
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Align(
|
|
alignment: alignment,
|
|
child: FittedBox(
|
|
fit: BoxFit.scaleDown,
|
|
alignment: alignment,
|
|
child: SizedBox(
|
|
height: height,
|
|
child: Text(
|
|
text,
|
|
maxLines: 1,
|
|
overflow: TextOverflow.visible,
|
|
textAlign:
|
|
alignment == Alignment.centerLeft
|
|
? TextAlign.left
|
|
: TextAlign.center,
|
|
style: TextStyle(
|
|
height: 1,
|
|
fontSize: fontSize,
|
|
fontWeight: fontWeight,
|
|
color: color,
|
|
letterSpacing: 0,
|
|
shadows: shadows,
|
|
),
|
|
),
|
|
),
|
|
),
|
|
);
|
|
}
|
|
}
|
|
|
|
class _FirstRechargeAssets {
|
|
static const String panelTop = 'sc_images/first_recharge/panel_top.png';
|
|
static const String panelMiddle = 'sc_images/first_recharge/panel_middle.png';
|
|
static const String panelBottom = 'sc_images/first_recharge/panel_bottom.png';
|
|
static const String close = 'sc_images/first_recharge/close.png';
|
|
static const String title = 'sc_images/first_recharge/title.png';
|
|
static const String priceTabsBg =
|
|
'sc_images/first_recharge/price_tabs_bg.png';
|
|
static const String priceSelectedTexture =
|
|
'sc_images/first_recharge/price_selected_texture.png';
|
|
static const String rewardCard = 'sc_images/first_recharge/reward_card.png';
|
|
static const String rewardCardCoin =
|
|
'sc_images/first_recharge/reward_card_coin.png';
|
|
static const String rewardAvatarFrame =
|
|
'sc_images/first_recharge/reward_avatar_frame_1.png';
|
|
static const String rewardVehicle =
|
|
'sc_images/first_recharge/reward_vehicle.png';
|
|
static const String rewardFrame = 'sc_images/first_recharge/reward_frame.png';
|
|
static const String rewardCoinStack =
|
|
'sc_images/first_recharge/reward_coin_stack.png';
|
|
static const String rewardGift = 'sc_images/first_recharge/reward_gift.png';
|
|
static const String rewardVip = 'sc_images/vip/sc_vip_badge_1.png';
|
|
static const String rewardBadge = 'sc_images/index/sc_icon_medals.png';
|
|
static const String buyNowButton =
|
|
'sc_images/first_recharge/buy_now_button.png';
|
|
static const String discountBadge =
|
|
'sc_images/first_recharge/discount_badge.png';
|
|
static const String yumiCoin = 'sc_images/register_reward/yumi_coin.png';
|
|
}
|