refactor: all screens refactor

This commit is contained in:
2026-07-08 20:12:04 +03:30
parent 2496580e86
commit adc2d1fc47
16 changed files with 3546 additions and 3351 deletions
@@ -0,0 +1,845 @@
// ویجت‌های خصوصیِ فروشگاه (تب‌ها و کارت‌های آیتم).
part of 'shop_screen.dart';
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: 'تجهیزات'),
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,
);
}
}
// ===== تبِ قاب‌های آواتار =====
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(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(f.title,
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: const TextStyle(
color: AppColors.gold, fontWeight: FontWeight.bold)),
const SizedBox(height: 6),
// پیش‌نمایش: آواتار درونِ قاب با گرادیانِ همان قاب.
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,
);
}
}
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,
),
),
),
],
),
),
),
);
}
}