feat: profile and subs

This commit is contained in:
2026-06-17 11:32:18 +03:30
parent 943ba13872
commit e9f294fa19
19 changed files with 1762 additions and 34 deletions
+16
View File
@@ -56,12 +56,26 @@ class Booster {
priceToman = (j['price_toman'] ?? 0) as int;
}
class VIPPackage {
final String id;
final String title;
final int months;
final int priceToman;
VIPPackage.fromJson(Map<String, dynamic> j)
: id = j['id'] as String,
title = j['title'] as String,
months = (j['months'] ?? 1) as int,
priceToman = (j['price_toman'] ?? 0) as int;
}
/// داده‌ی کامل فروشگاه: کاتالوگ + کارت‌های متعلق به کاربر + کارت انتخابی.
class ShopData {
final List<CoinPackage> coinPackages;
final List<TicketPackage> ticketPackages;
final List<CardSkin> cardSkins;
final List<Booster> boosters;
final List<VIPPackage> vipPackages;
final List<String> ownedCards;
final String selectedCard;
@@ -70,6 +84,7 @@ class ShopData {
required this.ticketPackages,
required this.cardSkins,
required this.boosters,
required this.vipPackages,
required this.ownedCards,
required this.selectedCard,
});
@@ -85,6 +100,7 @@ class ShopData {
ticketPackages: parse('ticket_packages', TicketPackage.fromJson),
cardSkins: parse('card_skins', CardSkin.fromJson),
boosters: parse('boosters', Booster.fromJson),
vipPackages: parse('vip_packages', VIPPackage.fromJson),
ownedCards: ((j['owned_cards'] as List?) ?? []).map((e) => e as String).toList(),
selectedCard: (j['selected_card'] ?? 'simple') as String,
);
+93 -1
View File
@@ -15,7 +15,7 @@ class ShopScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return DefaultTabController(
length: 4,
length: 5,
child: Scaffold(
backgroundColor: Colors.transparent,
body: GameBackground(
@@ -68,6 +68,7 @@ class ShopScreen extends StatelessWidget {
_TicketsTab(packages: d.ticketPackages),
_CardsTab(data: d),
_BoostersTab(boosters: d.boosters),
_VipTab(packages: d.vipPackages),
],
);
},
@@ -140,6 +141,7 @@ class _ShopTabs extends StatelessWidget {
Tab(text: 'بلیط'),
Tab(text: 'کارت'),
Tab(text: 'تجهیزات'),
Tab(text: 'VIP'),
],
);
}
@@ -243,6 +245,96 @@ class _BoostersTab extends StatelessWidget {
}
}
class _VipTab extends StatelessWidget {
final List<VIPPackage> packages;
const _VipTab({required this.packages});
@override
Widget build(BuildContext context) {
final isVip = context.select((WalletCubit c) => c.state.wallet?.vip ?? false);
return Column(
children: [
Padding(
padding: const EdgeInsets.fromLTRB(16, 12, 16, 4),
child: Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
gradient: const LinearGradient(
colors: [Color(0xFF5A3A00), Color(0xFF2A1A00)]),
borderRadius: BorderRadius.circular(12),
border: Border.all(color: AppColors.gold, width: 1.3),
),
child: Column(
children: [
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.workspace_premium,
color: AppColors.gold, size: 22),
const SizedBox(width: 6),
Text(isVip ? 'شما کاربر VIP هستید' : 'مزایای اشتراک VIP',
style: const TextStyle(
color: AppColors.gold,
fontWeight: FontWeight.bold,
fontSize: 15)),
],
),
const SizedBox(height: 8),
const _Benefit('میزهای خصوصی نامحدود'),
const _Benefit('مشاهده‌ی کامل آمار بازی در پروفایل'),
const _Benefit('۱۰٪ سکه‌ی هدیه در هر خرید'),
],
),
),
),
Expanded(
child: GridView.count(
crossAxisCount: 2,
padding: const EdgeInsets.all(12),
childAspectRatio: 0.74,
mainAxisSpacing: 12,
crossAxisSpacing: 12,
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: () => _do(context,
() => context.read<ShopCubit>().purchase('vip', p.id)),
),
),
],
),
),
],
);
}
}
class _Benefit extends StatelessWidget {
final String text;
const _Benefit(this.text);
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 2),
child: Row(
children: [
const Icon(Icons.check_circle, color: Color(0xFF6FCF7A), size: 16),
const SizedBox(width: 6),
Expanded(
child: Text(text,
style: const TextStyle(color: Colors.white, fontSize: 13))),
],
),
);
}
}
class _CardsTab extends StatelessWidget {
final ShopData data;
const _CardsTab({required this.data});
+201
View File
@@ -0,0 +1,201 @@
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart';
import '../../core/theme/app_theme.dart';
import '../../core/widgets/game_ui.dart';
import '../lobby/wallet_cubit.dart';
import 'shop_cubit.dart';
/// صفحه‌ی اشتراک VIP: نمایش بسته‌ها و خرید.
class VipScreen extends StatelessWidget {
const VipScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
body: GameBackground(
child: SafeArea(
child: BlocBuilder<ShopCubit, ShopState>(
builder: (context, state) {
if (state.status == ShopStatus.loading ||
state.status == ShopStatus.initial) {
return const Center(
child: CircularProgressIndicator(color: AppColors.gold));
}
if (state.status == ShopStatus.error || state.data == null) {
return Center(
child: Column(mainAxisSize: MainAxisSize.min, children: [
const Text('خطا در بارگذاری',
style: TextStyle(color: Colors.white70)),
TextButton(
onPressed: () => context.read<ShopCubit>().load(),
child: const Text('تلاش مجدد')),
]),
);
}
final packages = state.data!.vipPackages;
final isVip =
context.select((WalletCubit c) => c.state.wallet?.vip ?? false);
return Column(
children: [
Row(
children: [
IconButton(
onPressed: () => context.pop(),
icon: const Icon(Icons.arrow_back, color: AppColors.gold),
),
const Spacer(),
const GlowText('اشتراک VIP', size: 24),
const Spacer(),
const SizedBox(width: 48),
],
),
Expanded(
child: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
children: [
Container(
padding: const EdgeInsets.all(14),
decoration: BoxDecoration(
gradient: const LinearGradient(
colors: [Color(0xFF5A3A00), Color(0xFF2A1A00)]),
borderRadius: BorderRadius.circular(14),
border:
Border.all(color: AppColors.gold, width: 1.3),
),
child: Column(
children: [
const Icon(Icons.workspace_premium,
color: AppColors.gold, size: 40),
const SizedBox(height: 6),
Text(
isVip
? 'شما کاربر VIP هستید'
: 'با VIP بازی حرفه‌ای‌تری داشته باش',
style: const TextStyle(
color: AppColors.gold,
fontWeight: FontWeight.bold,
fontSize: 16)),
const SizedBox(height: 10),
const _Benefit('میزهای خصوصی نامحدود'),
const _Benefit('مشاهده‌ی کامل آمار بازی'),
const _Benefit('۱۰٪ سکه‌ی هدیه در هر خرید'),
],
),
),
const SizedBox(height: 18),
for (final p in packages)
Padding(
padding: const EdgeInsets.only(bottom: 12),
child: _VipPackageTile(
title: p.title,
months: p.months,
price: p.priceToman,
busy: state.busy,
onBuy: () => _buy(context, p.id),
),
),
if (packages.isEmpty)
const Padding(
padding: EdgeInsets.only(top: 30),
child: Text('فعلاً بسته‌ای موجود نیست',
style: TextStyle(color: Colors.white54)),
),
],
),
),
),
],
);
},
),
),
),
);
}
Future<void> _buy(BuildContext context, String id) async {
final msg = await context.read<ShopCubit>().purchase('vip', id);
if (!context.mounted || msg.isEmpty) return;
await context.read<WalletCubit>().load();
if (!context.mounted) return;
ScaffoldMessenger.of(context)
.showSnackBar(SnackBar(content: Text(msg)));
}
}
class _VipPackageTile extends StatelessWidget {
final String title;
final int months;
final int price;
final bool busy;
final VoidCallback onBuy;
const _VipPackageTile({
required this.title,
required this.months,
required this.price,
required this.busy,
required this.onBuy,
});
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(14),
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.4),
),
child: Row(
children: [
const Icon(Icons.workspace_premium, color: AppColors.gold, size: 34),
const SizedBox(width: 12),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(title,
style: const TextStyle(
color: AppColors.gold,
fontWeight: FontWeight.bold,
fontSize: 16)),
Text('$months ماه اشتراک',
style: const TextStyle(color: Colors.white70, fontSize: 12)),
],
),
const Spacer(),
GameButton(
label: '$price تومان',
onTap: busy ? null : onBuy,
),
],
),
);
}
}
class _Benefit extends StatelessWidget {
final String text;
const _Benefit(this.text);
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 2),
child: Row(
children: [
const Icon(Icons.check_circle, color: Color(0xFF6FCF7A), size: 16),
const SizedBox(width: 6),
Expanded(
child: Text(text,
style: const TextStyle(color: Colors.white, fontSize: 13))),
],
),
);
}
}