feat: add carpet

This commit is contained in:
2026-07-03 21:56:34 +03:30
parent 5160deb973
commit 7acd72c286
21 changed files with 536 additions and 156 deletions
@@ -1,14 +1,16 @@
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/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';
@@ -22,7 +24,7 @@ class ShopScreen extends StatelessWidget {
@override
Widget build(BuildContext context) {
return DefaultTabController(
length: 5,
length: 6,
child: Scaffold(
backgroundColor: Colors.transparent,
body: GameBackground(
@@ -85,6 +87,7 @@ class ShopScreen extends StatelessWidget {
_CoinsTab(packages: d.coinPackages),
_TicketsTab(packages: d.ticketPackages),
_CardsTab(data: d),
const _CarpetsTab(),
_BoostersTab(boosters: d.boosters),
_VipTab(packages: d.vipPackages),
],
@@ -114,19 +117,7 @@ class ShopScreen extends StatelessWidget {
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 AppBackButton(),
const Spacer(),
BlocBuilder<WalletBloc, WalletBlocState>(
builder: (context, s) {
@@ -164,7 +155,7 @@ class _ShopTabs extends StatelessWidget {
gradient: LinearGradient(
begin: Alignment.topCenter,
end: Alignment.bottomCenter,
colors: [Color(0xFFC62828), Color(0xFF7B0E14)],
colors: [AppColors.panel, AppColors.lapis],
),
borderRadius: BorderRadius.vertical(bottom: Radius.circular(12)),
),
@@ -179,6 +170,7 @@ class _ShopTabs extends StatelessWidget {
Tab(text: 'سکه'),
Tab(text: 'بلیط'),
Tab(text: 'کارت'),
Tab(text: 'فرش'),
Tab(text: 'تجهیزات'),
Tab(text: 'VIP'),
],
@@ -349,6 +341,179 @@ class _CardsTab extends StatelessWidget {
}
}
/// تبِ فرش‌ها: پیش‌نمایشِ تصویرِ هر فرش از 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: [Color(0xFF6E1322), Color(0xFF3A0A12)],
),
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: const Color(0xFF2A0710),
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: [
@@ -506,13 +671,16 @@ class _CardSkinArt extends StatelessWidget {
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)),
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)),
),
),
),