Files
front-hokm/lib/feature/shop/presentation/screen/shop_screen.parts.dart
T
2026-07-10 00:56:58 +03:30

1214 lines
36 KiB
Dart
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// ویجت‌های خصوصیِ فروشگاه (تب‌ها و کارت‌های آیتم).
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,
),
),
),
],
),
),
),
),
);
}
}