Files
front-hokm/lib/feature/shop/presentation/screen/shop_screen.dart
T

623 lines
19 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 'package:go_router/go_router.dart';
import '../../../../core/config.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 '../../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: 5,
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: [Color(0xFF4A0C16), Color(0xFF2A0710)],
),
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),
_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: [
GestureDetector(
onTap: () => context.pop(),
child: Container(
width: 46,
height: 46,
decoration: BoxDecoration(
color: AppColors.panel,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: AppColors.gold, width: 1.5),
),
child: const Icon(Icons.arrow_back, color: AppColors.gold),
),
),
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: [Color(0xFFC62828), Color(0xFF7B0E14)],
),
borderRadius: BorderRadius.vertical(bottom: 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: 'VIP'),
],
);
}
}
// ===== تب‌ها =====
class _CoinsTab extends StatelessWidget {
final List<CoinPackage> packages;
const _CoinsTab({required this.packages});
@override
Widget build(BuildContext context) {
return _grid(
note: 'با داشتن اشتراک VIP در هر خرید ۱۰٪ سکه اضافه هدیه می‌گیرید.',
children: [
_ItemCard(
title: 'سکه رایگان',
glowColor: const Color(0xFF1B5E20),
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: const Color(0xFF1B5E20),
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)),
),
),
],
);
}
}
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),
),
),
),
],
);
}
}
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),
),
),
),
],
);
}
}
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)),
),
),
],
);
}
}
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)),
);
}
}
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: [Color(0xFF6E1322), Color(0xFF3A0A12)],
),
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: const Color(0xFF2E7D32),
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: [Color(0xFF12508F), 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 [Color(0xFF3FA34D), Color(0xFF1B5E20)]
: const [Color(0xFFD83A4A), Color(0xFF8E0E1B)];
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,
),
),
),
],
),
),
),
);
}
}