Files
front-hokm/lib/feature/shop/presentation/screen/shop_screen.dart
T
2026-07-05 19:49:59 +03:30

820 lines
24 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/config.dart';
import '../../../../core/locator/locator.dart';
import '../../../../core/theme/app_theme.dart';
import '../../../../core/widgets/game_ui.dart';
import '../../../wallet/presentation/bloc/wallet_bloc.dart';
import '../../../wallet/presentation/bloc/wallet_event.dart';
import '../../../wallet/presentation/bloc/wallet_state.dart';
import '../../../wallet/presentation/bloc/wallet_status.dart';
import '../../data/data_source/remote/carpet_api_provider.dart';
import '../../domain/entities/carpet.dart';
import '../../domain/entities/shop_entities.dart';
import '../bloc/shop_bloc.dart';
import '../bloc/shop_event.dart';
import '../bloc/shop_state.dart';
import '../bloc/shop_status.dart';
/// فروشگاه با تب‌های سکه/بلیط/کارت/تجهیزات/VIP.
class ShopScreen extends StatelessWidget {
const ShopScreen({super.key});
@override
Widget build(BuildContext context) {
return DefaultTabController(
length: 6,
child: Scaffold(
backgroundColor: Colors.transparent,
body: GameBackground(
child: BlocConsumer<ShopBloc, ShopBlocState>(
listenWhen: (a, b) => a.actionStatus != b.actionStatus,
listener: (context, state) {
final s = state.actionStatus;
if (s is ActionSuccess) {
context.read<WalletBloc>().add(LoadWalletEvent());
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text(s.message)));
} else if (s is ActionError) {
ScaffoldMessenger.of(
context,
).showSnackBar(SnackBar(content: Text(s.message)));
}
},
builder: (context, state) {
return Column(
children: [
_header(context),
Expanded(
child: Container(
margin: const EdgeInsets.fromLTRB(8, 0, 8, 8),
decoration: BoxDecoration(
gradient: const LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [AppColors.lapisMid, AppColors.lapisLo],
),
borderRadius: BorderRadius.circular(16),
border: Border.all(
color: AppColors.goldDark,
width: 1.5,
),
),
child: Column(
children: [
const _ShopTabs(),
Expanded(child: _body(context, state.loadStatus)),
],
),
),
),
],
);
},
),
),
),
);
}
Widget _body(BuildContext context, ShopLoadStatus status) {
if (status is ShopLoaded) {
final d = status.data;
return TabBarView(
children: [
_CoinsTab(packages: d.coinPackages),
_TicketsTab(packages: d.ticketPackages),
_CardsTab(data: d),
const _CarpetsTab(),
_BoostersTab(boosters: d.boosters),
_VipTab(packages: d.vipPackages),
],
);
}
if (status is ShopLoadError) {
return Center(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(status.message),
TextButton(
onPressed: () => context.read<ShopBloc>().add(LoadShopEvent()),
child: const Text('تلاش مجدد'),
),
],
),
);
}
return const Center(
child: CircularProgressIndicator(color: AppColors.gold),
);
}
Widget _header(BuildContext context) {
return Padding(
padding: const EdgeInsets.all(8),
child: Row(
children: [
const AppBackButton(),
const Spacer(),
BlocBuilder<WalletBloc, WalletBlocState>(
builder: (context, s) {
final w =
s.walletStatus is WalletLoaded
? (s.walletStatus as WalletLoaded).wallet
: null;
return Row(
children: [
StatChip(
icon: Icons.confirmation_number,
value: '${w?.tickets ?? 0}',
),
const SizedBox(width: 8),
StatChip(
icon: Icons.monetization_on,
value: '${w?.coins ?? 0}',
),
],
);
},
),
],
),
);
}
}
class _ShopTabs extends StatelessWidget {
const _ShopTabs();
@override
Widget build(BuildContext context) {
return const TabBar(
indicator: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [AppColors.lapis, AppColors.suitDark],
),
borderRadius: BorderRadius.all(Radius.circular(12)),
),
indicatorSize: TabBarIndicatorSize.tab,
labelColor: AppColors.gold,
unselectedLabelColor: Colors.white60,
labelStyle: TextStyle(fontWeight: FontWeight.bold),
dividerColor: Colors.transparent,
isScrollable: true,
tabAlignment: TabAlignment.center,
tabs: [
Tab(text: 'سکه'),
Tab(text: 'بلیط'),
Tab(text: 'کارت'),
Tab(text: 'فرش'),
Tab(text: 'تجهیزات'),
Tab(text: 'VIP'),
],
);
}
}
// ===== تب‌ها =====
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(8),
decoration: BoxDecoration(
gradient: const LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [AppColors.lapisHi, AppColors.lapisLo],
),
borderRadius: BorderRadius.circular(14),
border: Border.all(
color: selected ? AppColors.gold : AppColors.goldDark,
width: selected ? 2 : 1.4,
),
),
child: Column(
children: [
Text(
c.title,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
color: AppColors.gold,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 6),
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.imageUrl,
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,
);
}
}
Widget _grid({required String note, required List<Widget> children}) {
return Column(
children: [
Padding(
padding: const EdgeInsets.fromLTRB(16, 12, 16, 6),
child: Text(
note,
textAlign: TextAlign.center,
style: const TextStyle(color: AppColors.gold, fontSize: 13),
),
),
Expanded(
child: GridView.count(
crossAxisCount: 2,
padding: const EdgeInsets.all(12),
childAspectRatio: 0.74,
mainAxisSpacing: 12,
crossAxisSpacing: 12,
children: children,
),
),
],
);
}
// ===== ویجت‌های کارت آیتم =====
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(8),
decoration: BoxDecoration(
gradient: const LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [AppColors.lapisHi, AppColors.lapisLo],
),
borderRadius: BorderRadius.circular(14),
border: Border.all(color: AppColors.gold, width: 1.6),
boxShadow: const [
BoxShadow(color: Colors.black54, blurRadius: 8, offset: Offset(0, 4)),
],
),
child: Column(
children: [
Text(
title,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
color: AppColors.gold,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 6),
Expanded(child: art ?? _GlowArt(color: glowColor, icon: icon)),
const SizedBox(height: 4),
Text(
subtitle,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(color: Colors.white70, fontSize: 11),
),
const SizedBox(height: 6),
SizedBox(width: double.infinity, child: action),
],
),
);
if (ribbon == null) return card;
return Stack(
clipBehavior: Clip.none,
children: [
card,
Positioned(
top: 8,
left: -22,
child: Transform.rotate(
angle: -0.7,
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 26, vertical: 3),
color: AppColors.green,
child: Text(
ribbon!,
style: const TextStyle(
color: Colors.white,
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(10),
gradient: RadialGradient(
colors: [color, Colors.black.withValues(alpha: 0.85)],
radius: 0.9,
),
border: Border.all(color: Colors.black26),
),
child: Center(child: Icon(icon, size: 44, color: AppColors.gold)),
);
}
}
/// پیش‌نمایشِ اسکینِ کارت در فروشگاه: دو کارت فن‌شده (پشت + تک‌خال پیک)
/// که مستقیماً از 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 StatelessWidget {
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
Widget build(BuildContext context) {
final busy = context.select((ShopBloc c) => c.state.busy);
final colors =
disabled
? const [Color(0xFF555555), Color(0xFF333333)]
: green
? const [AppColors.success, AppColors.successDark]
: const [
AppColors.accent,
Color(0xFF7A1019),
]; // شنگرفِ ایرانی (هم‌رنگِ تم)
return Opacity(
opacity: disabled ? 0.85 : 1,
child: GestureDetector(
onTap: (disabled || busy) ? null : onTap,
child: Container(
height: 36,
alignment: Alignment.center,
padding: const EdgeInsets.symmetric(horizontal: 6),
decoration: BoxDecoration(
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: colors,
),
borderRadius: BorderRadius.circular(10),
border: Border.all(color: AppColors.gold, width: 1.2),
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
if (coin) ...[
const Icon(
Icons.monetization_on,
color: AppColors.gold,
size: 16,
),
const SizedBox(width: 4),
],
Flexible(
child: Text(
label,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
color: Colors.white,
fontSize: 13,
fontWeight: FontWeight.bold,
),
),
),
],
),
),
),
);
}
}