1214 lines
36 KiB
Dart
1214 lines
36 KiB
Dart
// ویجتهای خصوصیِ فروشگاه (تبها و کارتهای آیتم).
|
||
part of 'shop_screen.dart';
|
||
|
||
class _ShopTabs extends StatelessWidget {
|
||
const _ShopTabs();
|
||
|
||
static const _tabs = <(IconData, String)>[
|
||
(Icons.monetization_on, 'سکه'),
|
||
(Icons.confirmation_number, 'بلیط'),
|
||
(Icons.style, 'کارت'),
|
||
(Icons.casino, 'فرش'),
|
||
(Icons.filter_center_focus, 'قاب'),
|
||
(Icons.face, 'شخصیت'),
|
||
(Icons.bolt, 'تجهیزات'),
|
||
(Icons.workspace_premium, 'VIP'),
|
||
];
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return TabBar(
|
||
isScrollable: true,
|
||
tabAlignment: TabAlignment.center,
|
||
indicatorSize: TabBarIndicatorSize.tab,
|
||
indicator: BoxDecoration(
|
||
gradient: const LinearGradient(
|
||
begin: Alignment.topCenter,
|
||
end: Alignment.bottomCenter,
|
||
colors: [AppColors.lapis, AppColors.suitDark],
|
||
),
|
||
borderRadius: BorderRadius.circular(12),
|
||
border: Border.all(color: AppColors.goldDark),
|
||
boxShadow: [
|
||
BoxShadow(
|
||
color: AppColors.gold.withValues(alpha: 0.25),
|
||
blurRadius: 8,
|
||
),
|
||
],
|
||
),
|
||
labelColor: AppColors.gold,
|
||
unselectedLabelColor: Colors.white60,
|
||
labelStyle: const TextStyle(fontWeight: FontWeight.bold, fontSize: 13),
|
||
dividerColor: Colors.transparent,
|
||
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 6),
|
||
tabs: [
|
||
for (final (icon, label) in _tabs)
|
||
Tab(
|
||
height: 40,
|
||
child: Row(
|
||
mainAxisSize: MainAxisSize.min,
|
||
children: [
|
||
Icon(icon, size: 16),
|
||
const SizedBox(width: 5),
|
||
Text(label),
|
||
],
|
||
),
|
||
),
|
||
],
|
||
);
|
||
}
|
||
}
|
||
|
||
// ===== تبها =====
|
||
|
||
class _CoinsTab extends StatelessWidget {
|
||
final List<CoinPackage> packages;
|
||
const _CoinsTab({required this.packages});
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return _grid(
|
||
note: 'با داشتن اشتراک VIP در هر خرید ۱۰٪ سکه اضافه هدیه میگیرید.',
|
||
children: [
|
||
// «سکه رایگان» (تبلیغِ ویدیویی-جایزهای) فقط وقتی App ID تپسل تنظیم شده باشد.
|
||
if (AppConfig.adsEnabled)
|
||
_ItemCard(
|
||
title: 'سکه رایگان',
|
||
glowColor: AppColors.successDark,
|
||
icon: Icons.card_giftcard,
|
||
subtitle: 'با دیدن تبلیغ',
|
||
action: _PriceButton(
|
||
label: 'رایگان',
|
||
green: true,
|
||
onTap: () => context.read<ShopBloc>().add(AdRewardEvent()),
|
||
),
|
||
),
|
||
for (final p in packages)
|
||
_ItemCard(
|
||
title: p.title,
|
||
glowColor: AppColors.successDark,
|
||
icon: Icons.savings,
|
||
ribbon: p.bonusPct > 0 ? '+${p.bonusPct}٪' : null,
|
||
subtitle:
|
||
'${p.coins} سکه${p.vipDays > 0 ? ' + ${p.vipDays} روز VIP' : ''}',
|
||
action: _PriceButton(
|
||
label: '${p.priceToman} تومان',
|
||
onTap:
|
||
() => context.read<ShopBloc>().add(
|
||
PurchaseEvent('coin', p.id, sku: p.sku),
|
||
),
|
||
),
|
||
),
|
||
],
|
||
);
|
||
}
|
||
}
|
||
|
||
class _TicketsTab extends StatelessWidget {
|
||
final List<TicketPackage> packages;
|
||
const _TicketsTab({required this.packages});
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return _grid(
|
||
note: 'با خرید بلیط میتوانید درخواست بر زدن مجدد در بازیها انجام دهید.',
|
||
children: [
|
||
for (final p in packages)
|
||
_ItemCard(
|
||
title: p.title,
|
||
glowColor: const Color(0xFF8E1B7A),
|
||
icon: Icons.confirmation_number,
|
||
subtitle: '${p.tickets} بلیط',
|
||
action: _PriceButton(
|
||
label: '${p.priceToman} تومان',
|
||
onTap:
|
||
() => context.read<ShopBloc>().add(
|
||
PurchaseEvent('ticket', p.id, sku: p.sku),
|
||
),
|
||
),
|
||
),
|
||
],
|
||
);
|
||
}
|
||
}
|
||
|
||
class _BoostersTab extends StatelessWidget {
|
||
final List<Booster> boosters;
|
||
const _BoostersTab({required this.boosters});
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return _grid(
|
||
note: 'با بستههای تجربه، چند برابر XP بگیرید و سریعتر بالا بروید.',
|
||
children: [
|
||
for (final b in boosters)
|
||
_ItemCard(
|
||
title: b.title,
|
||
glowColor: const Color(0xFF1E3A8A),
|
||
icon: Icons.bolt,
|
||
subtitle: 'تجربه ×${b.multiplier} — ${b.hours} ساعت',
|
||
action: _PriceButton(
|
||
label: '${b.priceToman} تومان',
|
||
onTap:
|
||
() => context.read<ShopBloc>().add(
|
||
PurchaseEvent('booster', b.id, sku: b.sku),
|
||
),
|
||
),
|
||
),
|
||
],
|
||
);
|
||
}
|
||
}
|
||
|
||
class _VipTab extends StatelessWidget {
|
||
final List<VipPackage> packages;
|
||
const _VipTab({required this.packages});
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return _grid(
|
||
note: 'با VIP: میز خصوصی نامحدود، آمار کامل و ۱۰٪ سکهی هدیه.',
|
||
children: [
|
||
for (final p in packages)
|
||
_ItemCard(
|
||
title: p.title,
|
||
glowColor: const Color(0xFF8A6D00),
|
||
icon: Icons.workspace_premium,
|
||
ribbon: p.months >= 6 ? 'بهترین' : null,
|
||
subtitle: '${p.months} ماه اشتراک',
|
||
action: _PriceButton(
|
||
label: '${p.priceToman} تومان',
|
||
onTap:
|
||
() => context.read<ShopBloc>().add(
|
||
PurchaseEvent('vip', p.id, sku: p.sku),
|
||
),
|
||
),
|
||
),
|
||
],
|
||
);
|
||
}
|
||
}
|
||
|
||
class _CardsTab extends StatelessWidget {
|
||
final ShopData data;
|
||
const _CardsTab({required this.data});
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return _grid(
|
||
note: 'با اسکین کارتهای متنوع حالوهوای بازی را عوض کن.',
|
||
children: [
|
||
for (final c in data.cardSkins)
|
||
_ItemCard(
|
||
title: c.title,
|
||
glowColor: const Color(0xFF0E3C73),
|
||
icon: Icons.style,
|
||
art: _CardSkinArt(skin: c.id, selected: data.selectedCard == c.id),
|
||
subtitle: c.priceCoins > 0 ? '${c.priceCoins} سکه' : 'پیشفرض',
|
||
action: _cardAction(context, c, data),
|
||
),
|
||
],
|
||
);
|
||
}
|
||
|
||
Widget _cardAction(BuildContext context, CardSkin c, ShopData d) {
|
||
if (d.selectedCard == c.id) {
|
||
return const _PriceButton(label: 'انتخاب شده', disabled: true);
|
||
}
|
||
if (d.owns(c.id)) {
|
||
return _PriceButton(
|
||
label: 'انتخاب',
|
||
green: true,
|
||
onTap: () => context.read<ShopBloc>().add(SelectCardEvent(c.id)),
|
||
);
|
||
}
|
||
return _PriceButton(
|
||
label: '${c.priceCoins} سکه',
|
||
green: true,
|
||
coin: true,
|
||
onTap: () => context.read<ShopBloc>().add(BuyCardEvent(c.id)),
|
||
);
|
||
}
|
||
}
|
||
|
||
/// تبِ فرشها: پیشنمایشِ تصویرِ هر فرش از backend با خرید (سکه/VIP) و انتخاب.
|
||
/// دادهها مستقل از ShopBloc از /api/carpets گرفته میشوند.
|
||
class _CarpetsTab extends StatefulWidget {
|
||
const _CarpetsTab();
|
||
@override
|
||
State<_CarpetsTab> createState() => _CarpetsTabState();
|
||
}
|
||
|
||
class _CarpetsTabState extends State<_CarpetsTab> {
|
||
final _api = locator<CarpetApiProvider>();
|
||
late Future<CarpetCatalog> _future;
|
||
String? _busy;
|
||
|
||
@override
|
||
void initState() {
|
||
super.initState();
|
||
_future = _api.getCarpets();
|
||
}
|
||
|
||
void _reload() => setState(() {
|
||
_future = _api.getCarpets();
|
||
});
|
||
|
||
Future<void> _act(Carpet c, {required bool buy}) async {
|
||
setState(() => _busy = c.id);
|
||
final err =
|
||
buy ? await _api.buyCarpet(c.id) : await _api.selectCarpet(c.id);
|
||
if (!mounted) return;
|
||
setState(() => _busy = null);
|
||
if (err == null) {
|
||
context.read<WalletBloc>().add(LoadWalletEvent());
|
||
_reload();
|
||
} else {
|
||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(err)));
|
||
}
|
||
}
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return FutureBuilder<CarpetCatalog>(
|
||
future: _future,
|
||
builder: (context, snap) {
|
||
if (snap.connectionState != ConnectionState.done) {
|
||
return const Center(
|
||
child: CircularProgressIndicator(color: AppColors.gold),
|
||
);
|
||
}
|
||
if (!snap.hasData) {
|
||
return Center(
|
||
child: TextButton(
|
||
onPressed: _reload,
|
||
child: const Text('تلاش مجدد'),
|
||
),
|
||
);
|
||
}
|
||
final cat = snap.data!;
|
||
return _grid(
|
||
note: 'فرشِ زیرِ میز را عوض کن؛ بعضی فرشها ویژهی VIP هستند.',
|
||
children: [
|
||
for (final c in cat.carpets)
|
||
_CarpetCard(
|
||
carpet: c,
|
||
selected: cat.selected == c.id,
|
||
busy: _busy == c.id,
|
||
onSelect: () => _act(c, buy: false),
|
||
onBuy: () => _act(c, buy: true),
|
||
),
|
||
],
|
||
);
|
||
},
|
||
);
|
||
}
|
||
}
|
||
|
||
class _CarpetCard extends StatelessWidget {
|
||
final Carpet carpet;
|
||
final bool selected;
|
||
final bool busy;
|
||
final VoidCallback onSelect;
|
||
final VoidCallback onBuy;
|
||
const _CarpetCard({
|
||
required this.carpet,
|
||
required this.selected,
|
||
required this.busy,
|
||
required this.onSelect,
|
||
required this.onBuy,
|
||
});
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final c = carpet;
|
||
return Container(
|
||
padding: const EdgeInsets.all(10),
|
||
decoration: _shopCardDeco(highlight: selected),
|
||
child: Column(
|
||
children: [
|
||
Text(
|
||
c.title,
|
||
maxLines: 1,
|
||
overflow: TextOverflow.ellipsis,
|
||
style: const TextStyle(
|
||
color: AppColors.gold,
|
||
fontWeight: FontWeight.bold,
|
||
fontSize: 14,
|
||
),
|
||
),
|
||
const SizedBox(height: 8),
|
||
Expanded(child: _preview(c)),
|
||
const SizedBox(height: 6),
|
||
SizedBox(width: double.infinity, child: _action(c)),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _preview(Carpet c) {
|
||
final frame = ClipRRect(
|
||
borderRadius: BorderRadius.circular(8),
|
||
child:
|
||
c.isClassic
|
||
? Container(
|
||
color: const Color(0xFF0E3614),
|
||
child: const Center(
|
||
child: Icon(Icons.casino, color: AppColors.gold, size: 36),
|
||
),
|
||
)
|
||
: Image.network(
|
||
c.img,
|
||
fit: BoxFit.cover,
|
||
errorBuilder:
|
||
(_, __, ___) => Container(
|
||
color: AppColors.lapisLo,
|
||
child: const Icon(
|
||
Icons.image_not_supported,
|
||
color: AppColors.goldDark,
|
||
),
|
||
),
|
||
),
|
||
);
|
||
return Stack(
|
||
fit: StackFit.expand,
|
||
children: [
|
||
frame,
|
||
if (c.vip && !c.owned)
|
||
const Positioned(
|
||
top: 4,
|
||
left: 4,
|
||
child: Chip(
|
||
label: Text(
|
||
'VIP',
|
||
style: TextStyle(
|
||
color: Colors.white,
|
||
fontSize: 10,
|
||
fontWeight: FontWeight.bold,
|
||
),
|
||
),
|
||
backgroundColor: AppColors.goldDark,
|
||
padding: EdgeInsets.zero,
|
||
materialTapTargetSize: MaterialTapTargetSize.shrinkWrap,
|
||
),
|
||
),
|
||
],
|
||
);
|
||
}
|
||
|
||
Widget _action(Carpet c) {
|
||
if (busy) {
|
||
return const _PriceButton(label: '...', disabled: true);
|
||
}
|
||
if (selected) {
|
||
return const _PriceButton(label: 'انتخاب شده', disabled: true);
|
||
}
|
||
if (c.owned) {
|
||
return _PriceButton(label: 'انتخاب', green: true, onTap: onSelect);
|
||
}
|
||
// قفل: VIP رایگان برای VIP (که owned میشود)، وگرنه خرید با سکه.
|
||
return _PriceButton(
|
||
label: '${c.priceCoins} سکه',
|
||
green: true,
|
||
coin: true,
|
||
onTap: onBuy,
|
||
);
|
||
}
|
||
}
|
||
|
||
// ===== تبِ قابهای آواتار =====
|
||
|
||
class _FramesTab extends StatefulWidget {
|
||
const _FramesTab();
|
||
@override
|
||
State<_FramesTab> createState() => _FramesTabState();
|
||
}
|
||
|
||
class _FramesTabState extends State<_FramesTab> {
|
||
final _api = locator<FrameApiProvider>();
|
||
late Future<FrameCatalog> _future;
|
||
String? _busy;
|
||
|
||
@override
|
||
void initState() {
|
||
super.initState();
|
||
_future = _api.getFrames();
|
||
}
|
||
|
||
void _reload() => setState(() {
|
||
_future = _api.getFrames();
|
||
});
|
||
|
||
Future<void> _act(AvatarFrame f, {required bool buy}) async {
|
||
setState(() => _busy = f.id);
|
||
final err = buy ? await _api.buyFrame(f.id) : await _api.selectFrame(f.id);
|
||
if (!mounted) return;
|
||
setState(() => _busy = null);
|
||
if (err == null) {
|
||
context.read<WalletBloc>().add(LoadWalletEvent());
|
||
_reload();
|
||
} else {
|
||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(err)));
|
||
}
|
||
}
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return FutureBuilder<FrameCatalog>(
|
||
future: _future,
|
||
builder: (context, snap) {
|
||
if (snap.connectionState != ConnectionState.done) {
|
||
return const Center(
|
||
child: CircularProgressIndicator(color: AppColors.gold),
|
||
);
|
||
}
|
||
if (!snap.hasData) {
|
||
return Center(
|
||
child: TextButton(
|
||
onPressed: _reload,
|
||
child: const Text('تلاش مجدد'),
|
||
),
|
||
);
|
||
}
|
||
final cat = snap.data!;
|
||
final ws = context.watch<WalletBloc>().state.walletStatus;
|
||
final seed = ws is WalletLoaded ? ws.wallet.avatar : 'hakem-1';
|
||
return _grid(
|
||
note: 'قابِ آواتارت را عوض کن؛ بازیکنهای دیگر هم آن را میبینند.',
|
||
children: [
|
||
for (final f in cat.frames)
|
||
_FrameCard(
|
||
frame: f,
|
||
avatarSeed: seed.isEmpty ? 'hakem-1' : seed,
|
||
selected:
|
||
cat.selected == f.id ||
|
||
(cat.selected.isEmpty && f.id == 'none'),
|
||
busy: _busy == f.id,
|
||
onSelect: () => _act(f, buy: false),
|
||
onBuy: () => _act(f, buy: true),
|
||
),
|
||
],
|
||
);
|
||
},
|
||
);
|
||
}
|
||
}
|
||
|
||
class _FrameCard extends StatelessWidget {
|
||
final AvatarFrame frame;
|
||
final String avatarSeed;
|
||
final bool selected;
|
||
final bool busy;
|
||
final VoidCallback onSelect;
|
||
final VoidCallback onBuy;
|
||
const _FrameCard({
|
||
required this.frame,
|
||
required this.avatarSeed,
|
||
required this.selected,
|
||
required this.busy,
|
||
required this.onSelect,
|
||
required this.onBuy,
|
||
});
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final f = frame;
|
||
final grad =
|
||
frameGradient(f.id) ?? const [Color(0xFFF3D27A), AppColors.goldDark];
|
||
return Container(
|
||
padding: const EdgeInsets.all(10),
|
||
decoration: _shopCardDeco(highlight: selected),
|
||
child: Column(
|
||
children: [
|
||
Text(
|
||
f.title,
|
||
maxLines: 1,
|
||
overflow: TextOverflow.ellipsis,
|
||
style: const TextStyle(
|
||
color: AppColors.gold,
|
||
fontWeight: FontWeight.bold,
|
||
fontSize: 14,
|
||
),
|
||
),
|
||
const SizedBox(height: 8),
|
||
// پیشنمایش: آواتار درونِ قاب با گرادیانِ همان قاب.
|
||
Expanded(
|
||
child: Center(
|
||
child: LayoutBuilder(
|
||
builder: (_, box) {
|
||
final d = box.maxHeight.clamp(40.0, 92.0);
|
||
return Container(
|
||
width: d,
|
||
height: d,
|
||
padding: EdgeInsets.all(d * 0.08),
|
||
decoration: BoxDecoration(
|
||
shape: BoxShape.circle,
|
||
gradient: LinearGradient(
|
||
begin: Alignment.topCenter,
|
||
end: Alignment.bottomCenter,
|
||
colors: grad,
|
||
),
|
||
),
|
||
child: ClipOval(
|
||
child: SizedBox.expand(child: RandomAvatar(avatarSeed)),
|
||
),
|
||
);
|
||
},
|
||
),
|
||
),
|
||
),
|
||
const SizedBox(height: 6),
|
||
if (f.vip && !f.owned)
|
||
const Padding(
|
||
padding: EdgeInsets.only(bottom: 4),
|
||
child: Text(
|
||
'ویژهی VIP',
|
||
style: TextStyle(color: AppColors.gold, fontSize: 11),
|
||
),
|
||
),
|
||
SizedBox(width: double.infinity, child: _action(f)),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _action(AvatarFrame f) {
|
||
if (busy) return const _PriceButton(label: '...', disabled: true);
|
||
if (selected) {
|
||
return const _PriceButton(label: 'انتخاب شده', disabled: true);
|
||
}
|
||
if (f.owned) {
|
||
return _PriceButton(label: 'انتخاب', green: true, onTap: onSelect);
|
||
}
|
||
return _PriceButton(
|
||
label: '${f.priceCoins} سکه',
|
||
green: true,
|
||
coin: true,
|
||
onTap: onBuy,
|
||
);
|
||
}
|
||
}
|
||
|
||
// ===== تبِ شخصیتهای آواتار =====
|
||
|
||
class _AvatarsTab extends StatefulWidget {
|
||
const _AvatarsTab();
|
||
@override
|
||
State<_AvatarsTab> createState() => _AvatarsTabState();
|
||
}
|
||
|
||
class _AvatarsTabState extends State<_AvatarsTab> {
|
||
final _api = locator<AvatarApiProvider>();
|
||
late Future<AvatarCatalog> _future;
|
||
String? _busy;
|
||
|
||
@override
|
||
void initState() {
|
||
super.initState();
|
||
_future = _api.getAvatars();
|
||
}
|
||
|
||
void _reload() => setState(() {
|
||
_future = _api.getAvatars();
|
||
});
|
||
|
||
Future<void> _act(AvatarCharacter a, {required bool buy}) async {
|
||
setState(() => _busy = a.id);
|
||
final err =
|
||
buy ? await _api.buyAvatar(a.id) : await _api.selectAvatar(a.id);
|
||
if (!mounted) return;
|
||
setState(() => _busy = null);
|
||
if (err == null) {
|
||
context.read<WalletBloc>().add(LoadWalletEvent());
|
||
_reload();
|
||
} else {
|
||
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(err)));
|
||
}
|
||
}
|
||
|
||
Future<void> _clear() async {
|
||
setState(() => _busy = '__none__');
|
||
await _api.selectAvatar(''); // بازگشت به آواتارِ تصادفی
|
||
if (!mounted) return;
|
||
setState(() => _busy = null);
|
||
context.read<WalletBloc>().add(LoadWalletEvent());
|
||
_reload();
|
||
}
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return FutureBuilder<AvatarCatalog>(
|
||
future: _future,
|
||
builder: (context, snap) {
|
||
if (snap.connectionState != ConnectionState.done) {
|
||
return const Center(
|
||
child: CircularProgressIndicator(color: AppColors.gold),
|
||
);
|
||
}
|
||
if (!snap.hasData) {
|
||
return Center(
|
||
child: TextButton(
|
||
onPressed: _reload,
|
||
child: const Text('تلاش مجدد'),
|
||
),
|
||
);
|
||
}
|
||
final cat = snap.data!;
|
||
final ws = context.watch<WalletBloc>().state.walletStatus;
|
||
final seed = ws is WalletLoaded ? ws.wallet.avatar : 'hakem-1';
|
||
if (cat.avatars.isEmpty) {
|
||
return const Center(
|
||
child: Padding(
|
||
padding: EdgeInsets.all(24),
|
||
child: Text(
|
||
'بهزودی شخصیتهای تازه اضافه میشوند.',
|
||
style: TextStyle(color: AppColors.gold),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
return _grid(
|
||
note:
|
||
'شخصیتِ آواتارت را انتخاب کن؛ در کلِ بازی (سرِ میز، پروفایل و لابی) دیده میشود.',
|
||
children: [
|
||
// «پیشفرض»: بازگشت به آواتارِ تصادفی.
|
||
_AvatarCard.defaultRandom(
|
||
avatarSeed: seed.isEmpty ? 'hakem-1' : seed,
|
||
selected: cat.selected.isEmpty,
|
||
busy: _busy == '__none__',
|
||
onSelect: _clear,
|
||
),
|
||
for (final a in cat.avatars)
|
||
_AvatarCard(
|
||
avatar: a,
|
||
avatarSeed: seed.isEmpty ? 'hakem-1' : seed,
|
||
selected: cat.selected == a.id,
|
||
busy: _busy == a.id,
|
||
onSelect: () => _act(a, buy: false),
|
||
onBuy: () => _act(a, buy: true),
|
||
),
|
||
],
|
||
);
|
||
},
|
||
);
|
||
}
|
||
}
|
||
|
||
class _AvatarCard extends StatelessWidget {
|
||
final AvatarCharacter? avatar; // null ⇒ کارتِ «پیشفرض» (آواتارِ تصادفی)
|
||
final String avatarSeed;
|
||
final bool selected;
|
||
final bool busy;
|
||
final VoidCallback onSelect;
|
||
final VoidCallback? onBuy;
|
||
const _AvatarCard({
|
||
required this.avatar,
|
||
required this.avatarSeed,
|
||
required this.selected,
|
||
required this.busy,
|
||
required this.onSelect,
|
||
required this.onBuy,
|
||
});
|
||
|
||
const _AvatarCard.defaultRandom({
|
||
required this.avatarSeed,
|
||
required this.selected,
|
||
required this.busy,
|
||
required this.onSelect,
|
||
}) : avatar = null,
|
||
onBuy = null;
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final a = avatar;
|
||
final title = a?.title ?? 'پیشفرض';
|
||
return Container(
|
||
padding: const EdgeInsets.all(10),
|
||
decoration: _shopCardDeco(highlight: selected),
|
||
child: Column(
|
||
children: [
|
||
Text(
|
||
title,
|
||
maxLines: 1,
|
||
overflow: TextOverflow.ellipsis,
|
||
style: const TextStyle(
|
||
color: AppColors.gold,
|
||
fontWeight: FontWeight.bold,
|
||
fontSize: 14,
|
||
),
|
||
),
|
||
const SizedBox(height: 8),
|
||
Expanded(
|
||
child: Center(
|
||
child: LayoutBuilder(
|
||
builder: (_, box) {
|
||
final d = box.maxHeight.clamp(40.0, 96.0);
|
||
return Container(
|
||
width: d,
|
||
height: d,
|
||
padding: EdgeInsets.all(d * 0.06),
|
||
decoration: BoxDecoration(
|
||
shape: BoxShape.circle,
|
||
color: AppColors.bgDark,
|
||
border: Border.all(
|
||
color: selected ? AppColors.gold : AppColors.goldDark,
|
||
width: selected ? 2 : 1.4,
|
||
),
|
||
),
|
||
child: PlayerAvatar(
|
||
seed: avatarSeed,
|
||
imageUrl: a?.img ?? '',
|
||
),
|
||
);
|
||
},
|
||
),
|
||
),
|
||
),
|
||
const SizedBox(height: 6),
|
||
if (a != null && a.vip && !a.owned)
|
||
const Padding(
|
||
padding: EdgeInsets.only(bottom: 4),
|
||
child: Text(
|
||
'ویژهی VIP',
|
||
style: TextStyle(color: AppColors.gold, fontSize: 11),
|
||
),
|
||
),
|
||
SizedBox(width: double.infinity, child: _action(a)),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _action(AvatarCharacter? a) {
|
||
if (busy) return const _PriceButton(label: '...', disabled: true);
|
||
if (selected) {
|
||
return const _PriceButton(label: 'انتخاب شده', disabled: true);
|
||
}
|
||
if (a == null || a.owned) {
|
||
return _PriceButton(label: 'انتخاب', green: true, onTap: onSelect);
|
||
}
|
||
return _PriceButton(
|
||
label: '${a.priceCoins} سکه',
|
||
green: true,
|
||
coin: true,
|
||
onTap: onBuy,
|
||
);
|
||
}
|
||
}
|
||
|
||
Widget _grid({required String note, required List<Widget> children}) {
|
||
return Column(
|
||
children: [
|
||
Padding(
|
||
padding: const EdgeInsets.fromLTRB(12, 10, 12, 2),
|
||
child: Container(
|
||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
|
||
decoration: BoxDecoration(
|
||
color: AppColors.gold.withValues(alpha: 0.08),
|
||
borderRadius: BorderRadius.circular(12),
|
||
border: Border.all(color: AppColors.goldFaint),
|
||
),
|
||
child: Row(
|
||
children: [
|
||
const Icon(Icons.info_outline, color: AppColors.gold, size: 16),
|
||
const SizedBox(width: 8),
|
||
Expanded(
|
||
child: Text(
|
||
note,
|
||
style: const TextStyle(
|
||
color: AppColors.gold,
|
||
fontSize: 12.5,
|
||
height: 1.3,
|
||
),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
Expanded(
|
||
child: GridView.count(
|
||
crossAxisCount: 2,
|
||
padding: const EdgeInsets.all(12),
|
||
childAspectRatio: 0.72,
|
||
mainAxisSpacing: 12,
|
||
crossAxisSpacing: 12,
|
||
children: children,
|
||
),
|
||
),
|
||
],
|
||
);
|
||
}
|
||
|
||
/// قابِ مشترکِ کارتهای فروشگاه (سکه/کارت/فرش/…): گرادیانِ لاجوردی، حاشیهی طلایی
|
||
/// و عمقِ سایه. [highlight] برای آیتمِ انتخابشده هالهی طلایی اضافه میکند.
|
||
BoxDecoration _shopCardDeco({bool highlight = false}) => BoxDecoration(
|
||
gradient: const LinearGradient(
|
||
begin: Alignment.topLeft,
|
||
end: Alignment.bottomRight,
|
||
colors: [Color(0xFF1C4A80), Color(0xFF0B2646)],
|
||
),
|
||
borderRadius: BorderRadius.circular(16),
|
||
border: Border.all(
|
||
color: highlight ? AppColors.gold : AppColors.goldDark,
|
||
width: highlight ? 2 : 1.3,
|
||
),
|
||
boxShadow: [
|
||
const BoxShadow(
|
||
color: Colors.black54,
|
||
blurRadius: 10,
|
||
offset: Offset(0, 5),
|
||
),
|
||
if (highlight)
|
||
BoxShadow(color: AppColors.gold.withValues(alpha: 0.3), blurRadius: 14),
|
||
],
|
||
);
|
||
|
||
/// عددِ موجودی با جداکنندهی هزارگان (مثلِ ۱۲٬۵۰۰) برای خواناییِ بهتر.
|
||
String _fmt(int n) {
|
||
final s = n.abs().toString();
|
||
final b = StringBuffer(n < 0 ? '-' : '');
|
||
for (var i = 0; i < s.length; i++) {
|
||
if (i > 0 && (s.length - i) % 3 == 0) b.write('٬');
|
||
b.write(s[i]);
|
||
}
|
||
return b.toString();
|
||
}
|
||
|
||
/// حبابِ موجودی (سکه/بلیط) در سربرگِ فروشگاه — براق و برجسته.
|
||
class _BalancePill extends StatelessWidget {
|
||
final IconData icon;
|
||
final Color iconColor;
|
||
final String value;
|
||
const _BalancePill({
|
||
required this.icon,
|
||
required this.iconColor,
|
||
required this.value,
|
||
});
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return Container(
|
||
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 7),
|
||
decoration: BoxDecoration(
|
||
gradient: const LinearGradient(
|
||
begin: Alignment.topCenter,
|
||
end: Alignment.bottomCenter,
|
||
colors: [Color(0xFF17345C), Color(0xFF0C2848)],
|
||
),
|
||
borderRadius: BorderRadius.circular(20),
|
||
border: Border.all(color: AppColors.goldDark),
|
||
boxShadow: const [
|
||
BoxShadow(color: Colors.black45, blurRadius: 6, offset: Offset(0, 2)),
|
||
],
|
||
),
|
||
child: Row(
|
||
mainAxisSize: MainAxisSize.min,
|
||
children: [
|
||
Icon(icon, color: iconColor, size: 18),
|
||
const SizedBox(width: 6),
|
||
Text(
|
||
value,
|
||
style: const TextStyle(
|
||
color: AppColors.gold,
|
||
fontWeight: FontWeight.bold,
|
||
fontSize: 14,
|
||
),
|
||
),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
// ===== ویجتهای کارت آیتم =====
|
||
|
||
class _ItemCard extends StatelessWidget {
|
||
final String title;
|
||
final IconData icon;
|
||
final Color glowColor;
|
||
final String subtitle;
|
||
final String? ribbon;
|
||
final Widget action;
|
||
final Widget? art; // هنرِ سفارشی (مثل پیشنمایشِ اسکینِ کارت) بهجای آیکن
|
||
const _ItemCard({
|
||
required this.title,
|
||
required this.icon,
|
||
required this.glowColor,
|
||
required this.subtitle,
|
||
required this.action,
|
||
this.ribbon,
|
||
this.art,
|
||
});
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final card = Container(
|
||
padding: const EdgeInsets.all(10),
|
||
decoration: _shopCardDeco(highlight: ribbon != null),
|
||
child: Column(
|
||
children: [
|
||
Text(
|
||
title,
|
||
maxLines: 1,
|
||
overflow: TextOverflow.ellipsis,
|
||
style: const TextStyle(
|
||
color: AppColors.gold,
|
||
fontWeight: FontWeight.bold,
|
||
fontSize: 14,
|
||
),
|
||
),
|
||
const SizedBox(height: 8),
|
||
Expanded(child: art ?? _GlowArt(color: glowColor, icon: icon)),
|
||
const SizedBox(height: 6),
|
||
Text(
|
||
subtitle,
|
||
maxLines: 1,
|
||
overflow: TextOverflow.ellipsis,
|
||
style: const TextStyle(color: Colors.white70, fontSize: 11.5),
|
||
),
|
||
const SizedBox(height: 8),
|
||
SizedBox(width: double.infinity, child: action),
|
||
],
|
||
),
|
||
);
|
||
if (ribbon == null) return card;
|
||
// ریبونِ گوشه: درصدِ هدیه سبز، «بهترین/ویژه» طلایی — جلبِتوجه.
|
||
final isBonus = ribbon!.contains('٪') || ribbon!.contains('%');
|
||
return Stack(
|
||
clipBehavior: Clip.none,
|
||
children: [
|
||
card,
|
||
Positioned(
|
||
top: 10,
|
||
left: -24,
|
||
child: Transform.rotate(
|
||
angle: -0.7,
|
||
child: Container(
|
||
padding: const EdgeInsets.symmetric(horizontal: 28, vertical: 4),
|
||
decoration: BoxDecoration(
|
||
gradient: LinearGradient(
|
||
colors:
|
||
isBonus
|
||
? const [AppColors.success, AppColors.successDark]
|
||
: const [Color(0xFFFFD54F), AppColors.goldDark],
|
||
),
|
||
boxShadow: const [
|
||
BoxShadow(color: Colors.black54, blurRadius: 4),
|
||
],
|
||
),
|
||
child: Text(
|
||
ribbon!,
|
||
style: TextStyle(
|
||
color: isBonus ? Colors.white : AppColors.bg,
|
||
fontSize: 11,
|
||
fontWeight: FontWeight.bold,
|
||
),
|
||
),
|
||
),
|
||
),
|
||
),
|
||
],
|
||
);
|
||
}
|
||
}
|
||
|
||
class _GlowArt extends StatelessWidget {
|
||
final Color color;
|
||
final IconData icon;
|
||
const _GlowArt({required this.color, required this.icon});
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return Container(
|
||
width: double.infinity,
|
||
decoration: BoxDecoration(
|
||
borderRadius: BorderRadius.circular(12),
|
||
gradient: RadialGradient(
|
||
center: const Alignment(-0.15, -0.35),
|
||
radius: 1.05,
|
||
colors: [
|
||
Color.lerp(color, Colors.white, 0.15)!,
|
||
color,
|
||
Colors.black.withValues(alpha: 0.9),
|
||
],
|
||
stops: const [0.0, 0.45, 1.0],
|
||
),
|
||
border: Border.all(color: AppColors.goldFaint),
|
||
),
|
||
child: Center(
|
||
child: Icon(
|
||
icon,
|
||
size: 46,
|
||
color: AppColors.gold,
|
||
shadows: [
|
||
Shadow(color: color.withValues(alpha: 0.9), blurRadius: 18),
|
||
const Shadow(color: Colors.black87, blurRadius: 6),
|
||
],
|
||
),
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
/// پیشنمایشِ اسکینِ کارت در فروشگاه: دو کارت فنشده (پشت + تکخال پیک)
|
||
/// که مستقیماً از backend بارگذاری میشوند تا کاربر هنرِ واقعی را ببیند.
|
||
class _CardSkinArt extends StatelessWidget {
|
||
final String skin;
|
||
final bool selected;
|
||
const _CardSkinArt({required this.skin, required this.selected});
|
||
|
||
String _url(String file) => '${AppConfig.baseUrl}/cards/$skin/$file';
|
||
|
||
Widget _card(String file, {double angle = 0, double dx = 0}) {
|
||
return Transform.translate(
|
||
offset: Offset(dx, 0),
|
||
child: Transform.rotate(
|
||
angle: angle,
|
||
child: AspectRatio(
|
||
aspectRatio: 0.7,
|
||
child: ClipRRect(
|
||
borderRadius: BorderRadius.circular(6),
|
||
child: Image.network(
|
||
_url(file),
|
||
fit: BoxFit.cover,
|
||
errorBuilder:
|
||
(_, __, ___) => Container(
|
||
color: const Color(0xFF0E3C73),
|
||
child: const Icon(Icons.style, color: AppColors.gold),
|
||
),
|
||
loadingBuilder:
|
||
(ctx, child, progress) =>
|
||
progress == null
|
||
? child
|
||
: Container(color: const Color(0xFF0A2547)),
|
||
),
|
||
),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return Container(
|
||
width: double.infinity,
|
||
decoration: BoxDecoration(
|
||
borderRadius: BorderRadius.circular(10),
|
||
gradient: const RadialGradient(
|
||
colors: [AppColors.lapis, Colors.black87],
|
||
radius: 0.95,
|
||
),
|
||
border: Border.all(
|
||
color: selected ? AppColors.gold : Colors.black26,
|
||
width: selected ? 2 : 1,
|
||
),
|
||
),
|
||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||
child: Center(
|
||
child: Stack(
|
||
alignment: Alignment.center,
|
||
clipBehavior: Clip.none,
|
||
children: [
|
||
_card('back.jpg', angle: -0.22, dx: -16),
|
||
_card('AS.png', angle: 0.22, dx: 16),
|
||
],
|
||
),
|
||
),
|
||
);
|
||
}
|
||
}
|
||
|
||
class _PriceButton extends StatefulWidget {
|
||
final String label;
|
||
final VoidCallback? onTap;
|
||
final bool green;
|
||
final bool coin;
|
||
final bool disabled;
|
||
const _PriceButton({
|
||
required this.label,
|
||
this.onTap,
|
||
this.green = false,
|
||
this.coin = false,
|
||
this.disabled = false,
|
||
});
|
||
|
||
@override
|
||
State<_PriceButton> createState() => _PriceButtonState();
|
||
}
|
||
|
||
class _PriceButtonState extends State<_PriceButton> {
|
||
bool _down = false;
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
final busy = context.select((ShopBloc c) => c.state.busy);
|
||
final enabled = !(widget.disabled || busy) && widget.onTap != null;
|
||
final colors =
|
||
widget.disabled
|
||
? const [Color(0xFF555555), Color(0xFF333333)]
|
||
: widget.green
|
||
? const [AppColors.success, AppColors.successDark]
|
||
: const [
|
||
AppColors.accent,
|
||
Color(0xFF7A1019),
|
||
]; // شنگرفِ ایرانی (همرنگِ تم)
|
||
return GestureDetector(
|
||
onTapDown: enabled ? (_) => setState(() => _down = true) : null,
|
||
onTapUp: enabled ? (_) => setState(() => _down = false) : null,
|
||
onTapCancel: enabled ? () => setState(() => _down = false) : null,
|
||
onTap: enabled ? widget.onTap : null,
|
||
child: AnimatedScale(
|
||
scale: _down ? 0.94 : 1,
|
||
duration: const Duration(milliseconds: 90),
|
||
child: Opacity(
|
||
opacity: widget.disabled ? 0.85 : 1,
|
||
child: Container(
|
||
height: 38,
|
||
alignment: Alignment.center,
|
||
padding: const EdgeInsets.symmetric(horizontal: 6),
|
||
decoration: BoxDecoration(
|
||
gradient: LinearGradient(
|
||
begin: Alignment.topCenter,
|
||
end: Alignment.bottomCenter,
|
||
colors: colors,
|
||
),
|
||
borderRadius: BorderRadius.circular(11),
|
||
border: Border.all(color: AppColors.gold, width: 1.2),
|
||
boxShadow:
|
||
enabled
|
||
? [
|
||
BoxShadow(
|
||
color: colors.last.withValues(alpha: 0.55),
|
||
blurRadius: 8,
|
||
offset: const Offset(0, 3),
|
||
),
|
||
]
|
||
: null,
|
||
),
|
||
child: Row(
|
||
mainAxisSize: MainAxisSize.min,
|
||
children: [
|
||
if (widget.coin) ...[
|
||
const Icon(
|
||
Icons.monetization_on,
|
||
color: AppColors.gold,
|
||
size: 16,
|
||
),
|
||
const SizedBox(width: 4),
|
||
],
|
||
Flexible(
|
||
child: Text(
|
||
widget.label,
|
||
maxLines: 1,
|
||
overflow: TextOverflow.ellipsis,
|
||
style: const TextStyle(
|
||
color: Colors.white,
|
||
fontSize: 13,
|
||
fontWeight: FontWeight.bold,
|
||
),
|
||
),
|
||
),
|
||
],
|
||
),
|
||
),
|
||
),
|
||
),
|
||
);
|
||
}
|
||
}
|