Files
front-hokm/lib/features/shop/shop_screen.dart
T
2026-06-15 16:42:28 +03:30

385 lines
11 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.
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import '../../core/theme/app_theme.dart';
import '../lobby/wallet_cubit.dart';
import 'shop_cubit.dart';
import 'shop_models.dart';
/// صفحه‌ی فروشگاه با تب‌های سکه/بلیط/کارت/تجهیزات.
class ShopScreen extends StatelessWidget {
const ShopScreen({super.key});
@override
Widget build(BuildContext context) {
return DefaultTabController(
length: 4,
child: Scaffold(
appBar: AppBar(
title: const Text('فروشگاه'),
actions: [
BlocBuilder<WalletCubit, WalletState>(
builder: (context, s) => Center(
child: Padding(
padding: const EdgeInsets.only(left: 12),
child: Row(children: [
const Icon(Icons.monetization_on,
color: AppColors.gold, size: 18),
const SizedBox(width: 4),
Text('${s.wallet?.coins ?? 0}',
style: const TextStyle(color: AppColors.text)),
]),
),
),
),
],
bottom: const TabBar(
isScrollable: true,
labelColor: AppColors.gold,
indicatorColor: AppColors.gold,
tabs: [
Tab(text: 'سکه'),
Tab(text: 'بلیط'),
Tab(text: 'کارت'),
Tab(text: 'تجهیزات'),
],
),
),
body: BlocConsumer<ShopCubit, ShopState>(
listener: (context, state) {},
builder: (context, state) {
if (state.status == ShopStatus.loading ||
state.status == ShopStatus.initial) {
return const Center(child: CircularProgressIndicator());
}
if (state.status == ShopStatus.error || state.data == null) {
return Center(
child: Column(mainAxisSize: MainAxisSize.min, children: [
const Text('خطا در بارگذاری فروشگاه'),
TextButton(
onPressed: () => context.read<ShopCubit>().load(),
child: const Text('تلاش مجدد'),
),
]),
);
}
final d = state.data!;
return TabBarView(
children: [
_CoinsTab(packages: d.coinPackages),
_TicketsTab(packages: d.ticketPackages),
_CardsTab(data: d),
_BoostersTab(boosters: d.boosters),
],
);
},
),
),
);
}
}
/// اجرای یک عملیات فروشگاه و نمایش نتیجه + بازخوانی کیف‌پول.
Future<void> _do(BuildContext context, Future<String> Function() action) async {
final msg = await action();
if (!context.mounted || msg.isEmpty) return;
await context.read<WalletCubit>().load();
if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(msg)));
}
class _CoinsTab extends StatelessWidget {
final List<CoinPackage> packages;
const _CoinsTab({required this.packages});
@override
Widget build(BuildContext context) {
return GridView.count(
crossAxisCount: 2,
padding: const EdgeInsets.all(12),
childAspectRatio: 0.82,
mainAxisSpacing: 12,
crossAxisSpacing: 12,
children: [
_FreeCoinCard(
onTap: () => _do(context, () => context.read<ShopCubit>().claimAd()),
),
for (final p in packages)
_StoreCard(
title: p.title,
badge: p.bonusPct > 0 ? '+${p.bonusPct}%' : null,
lines: [
'${p.coins} سکه',
if (p.vipDays > 0) '${p.vipDays} روز VIP',
],
priceLabel: '${p.priceToman} تومان',
icon: Icons.savings,
onTap: () =>
_do(context, () => context.read<ShopCubit>().purchase('coin', p.id)),
),
],
);
}
}
class _TicketsTab extends StatelessWidget {
final List<TicketPackage> packages;
const _TicketsTab({required this.packages});
@override
Widget build(BuildContext context) {
return GridView.count(
crossAxisCount: 2,
padding: const EdgeInsets.all(12),
childAspectRatio: 0.82,
mainAxisSpacing: 12,
crossAxisSpacing: 12,
children: [
for (final p in packages)
_StoreCard(
title: p.title,
lines: ['${p.tickets} بلیط'],
priceLabel: '${p.priceToman} تومان',
icon: Icons.confirmation_number,
onTap: () =>
_do(context, () => context.read<ShopCubit>().purchase('ticket', p.id)),
),
],
);
}
}
class _BoostersTab extends StatelessWidget {
final List<Booster> boosters;
const _BoostersTab({required this.boosters});
@override
Widget build(BuildContext context) {
return GridView.count(
crossAxisCount: 2,
padding: const EdgeInsets.all(12),
childAspectRatio: 0.82,
mainAxisSpacing: 12,
crossAxisSpacing: 12,
children: [
for (final b in boosters)
_StoreCard(
title: b.title,
lines: ['تجربه ×${b.multiplier}', '${b.hours} ساعت'],
priceLabel: '${b.priceToman} تومان',
icon: Icons.bolt,
onTap: () =>
_do(context, () => context.read<ShopCubit>().purchase('booster', b.id)),
),
],
);
}
}
class _CardsTab extends StatelessWidget {
final ShopData data;
const _CardsTab({required this.data});
@override
Widget build(BuildContext context) {
return GridView.count(
crossAxisCount: 2,
padding: const EdgeInsets.all(12),
childAspectRatio: 0.82,
mainAxisSpacing: 12,
crossAxisSpacing: 12,
children: [
for (final c in data.cardSkins)
_CardSkinCard(
skin: c,
owned: data.owns(c.id),
selected: data.selectedCard == c.id,
),
],
);
}
}
class _CardSkinCard extends StatelessWidget {
final CardSkin skin;
final bool owned;
final bool selected;
const _CardSkinCard(
{required this.skin, required this.owned, required this.selected});
@override
Widget build(BuildContext context) {
final Widget action;
if (selected) {
action = const _Pill(text: 'انتخاب شده', color: AppColors.goldDark);
} else if (owned) {
action = _ActionButton(
label: 'انتخاب',
color: AppColors.green,
onTap: () =>
_do(context, () => context.read<ShopCubit>().selectCard(skin.id)),
);
} else {
action = _ActionButton(
label: '${skin.priceCoins} سکه',
color: AppColors.accent,
onTap: () => _do(context, () => context.read<ShopCubit>().buyCard(skin.id)),
);
}
return _Panel(
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(skin.title,
textAlign: TextAlign.center,
style: const TextStyle(color: AppColors.gold)),
const Icon(Icons.style, size: 48, color: AppColors.text),
SizedBox(width: double.infinity, child: action),
],
),
);
}
}
class _FreeCoinCard extends StatelessWidget {
final VoidCallback onTap;
const _FreeCoinCard({required this.onTap});
@override
Widget build(BuildContext context) {
return _Panel(
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text('سکه رایگان',
textAlign: TextAlign.center,
style: TextStyle(color: AppColors.gold)),
const Icon(Icons.ondemand_video, size: 44, color: AppColors.green),
const Text('با دیدن تبلیغ',
style: TextStyle(color: Colors.white60, fontSize: 12)),
SizedBox(
width: double.infinity,
child: _ActionButton(
label: 'رایگان', color: AppColors.green, onTap: onTap),
),
],
),
);
}
}
class _StoreCard extends StatelessWidget {
final String title;
final List<String> lines;
final String priceLabel;
final IconData icon;
final String? badge;
final VoidCallback onTap;
const _StoreCard({
required this.title,
required this.lines,
required this.priceLabel,
required this.icon,
required this.onTap,
this.badge,
});
@override
Widget build(BuildContext context) {
return Stack(
children: [
_Panel(
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Text(title,
textAlign: TextAlign.center,
style: const TextStyle(color: AppColors.gold)),
Icon(icon, size: 40, color: AppColors.gold),
Column(
children: [
for (final l in lines)
Text(l, style: const TextStyle(color: AppColors.text)),
],
),
SizedBox(
width: double.infinity,
child: _ActionButton(
label: priceLabel, color: AppColors.accent, onTap: onTap),
),
],
),
),
if (badge != null)
Positioned(
top: 4,
left: 4,
child: _Pill(text: badge!, color: AppColors.green),
),
],
);
}
}
class _Panel extends StatelessWidget {
final Widget child;
const _Panel({required this.child});
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
color: AppColors.panel,
borderRadius: BorderRadius.circular(14),
border: Border.all(color: AppColors.goldDark),
),
child: child,
);
}
}
class _ActionButton extends StatelessWidget {
final String label;
final Color color;
final VoidCallback onTap;
const _ActionButton(
{required this.label, required this.color, required this.onTap});
@override
Widget build(BuildContext context) {
final busy = context.select((ShopCubit c) => c.state.busy);
return ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: color,
minimumSize: const Size.fromHeight(38),
padding: const EdgeInsets.symmetric(horizontal: 6),
),
onPressed: busy ? null : onTap,
child: Text(label,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(fontSize: 13)),
);
}
}
class _Pill extends StatelessWidget {
final String text;
final Color color;
const _Pill({required this.text, required this.color});
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 6),
decoration: BoxDecoration(
color: color,
borderRadius: BorderRadius.circular(8),
),
child: Text(text,
style: const TextStyle(color: Colors.white, fontSize: 12)),
);
}
}