feat:add frames
This commit is contained in:
@@ -0,0 +1,43 @@
|
||||
import '../../../../../core/locator/locator.dart';
|
||||
import '../../../../../core/network/api_provider_imp.dart';
|
||||
import '../../../domain/entities/frame.dart';
|
||||
|
||||
/// دادهٔ قابهای آواتار از backend (دریافت، خرید، انتخاب).
|
||||
class FrameApiProvider {
|
||||
ApiProviderImp get _api => locator<ApiProviderImp>();
|
||||
|
||||
Future<FrameCatalog> getFrames() async {
|
||||
final res = await _api.get('/frames');
|
||||
if (res.statusCode != 200) {
|
||||
throw Exception('frames: ${res.statusCode}');
|
||||
}
|
||||
final list = (res.data['frames'] as List?) ?? const [];
|
||||
return FrameCatalog(
|
||||
frames: list
|
||||
.map((e) => AvatarFrame.fromJson(Map<String, dynamic>.from(e as Map)))
|
||||
.toList(),
|
||||
selected: (res.data['selected'] ?? '') as String,
|
||||
);
|
||||
}
|
||||
|
||||
/// خرید؛ null یعنی موفق، وگرنه پیامِ خطای فارسی.
|
||||
Future<String?> buyFrame(String id) => _post('/shop/buy-frame', id);
|
||||
|
||||
/// انتخاب؛ null یعنی موفق، وگرنه پیامِ خطای فارسی.
|
||||
Future<String?> selectFrame(String id) => _post('/shop/select-frame', id);
|
||||
|
||||
Future<String?> _post(String path, String id) async {
|
||||
final res = await _api.post(path, body: {'frame_id': id});
|
||||
if (res.statusCode == 200) return null;
|
||||
switch (res.statusCode) {
|
||||
case 402:
|
||||
return 'سکهٔ کافی ندارید';
|
||||
case 403:
|
||||
return 'این قاب را ندارید';
|
||||
case 409:
|
||||
return 'این قاب را دارید';
|
||||
default:
|
||||
return 'عملیات ناموفق بود';
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
/// یک قابِ آواتار (کازمتیک). owned یعنی قابلِ انتخاب.
|
||||
class AvatarFrame {
|
||||
final String id;
|
||||
final String title;
|
||||
final int priceCoins;
|
||||
final bool vip;
|
||||
final bool owned;
|
||||
|
||||
const AvatarFrame({
|
||||
required this.id,
|
||||
required this.title,
|
||||
required this.priceCoins,
|
||||
required this.vip,
|
||||
required this.owned,
|
||||
});
|
||||
|
||||
factory AvatarFrame.fromJson(Map<String, dynamic> j) => AvatarFrame(
|
||||
id: (j['id'] ?? '') as String,
|
||||
title: (j['title'] ?? '') as String,
|
||||
priceCoins: (j['price_coins'] ?? 0) as int,
|
||||
vip: (j['vip'] ?? false) as bool,
|
||||
owned: (j['owned'] ?? false) as bool,
|
||||
);
|
||||
}
|
||||
|
||||
/// کاتالوگِ قابها + قابِ انتخابیِ کاربر.
|
||||
class FrameCatalog {
|
||||
final List<AvatarFrame> frames;
|
||||
final String selected;
|
||||
const FrameCatalog({required this.frames, required this.selected});
|
||||
}
|
||||
@@ -1,16 +1,20 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_bloc/flutter_bloc.dart';
|
||||
import 'package:random_avatar/random_avatar.dart';
|
||||
|
||||
import '../../../../core/config.dart';
|
||||
import '../../../../core/locator/locator.dart';
|
||||
import '../../../../core/theme/app_theme.dart';
|
||||
import '../../../../core/theme/frames.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 '../../data/data_source/remote/frame_api_provider.dart';
|
||||
import '../../domain/entities/carpet.dart';
|
||||
import '../../domain/entities/frame.dart';
|
||||
import '../../domain/entities/shop_entities.dart';
|
||||
import '../bloc/shop_bloc.dart';
|
||||
import '../bloc/shop_event.dart';
|
||||
@@ -24,7 +28,7 @@ class ShopScreen extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return DefaultTabController(
|
||||
length: 6,
|
||||
length: 7,
|
||||
child: Scaffold(
|
||||
backgroundColor: Colors.transparent,
|
||||
body: GameBackground(
|
||||
@@ -88,6 +92,7 @@ class ShopScreen extends StatelessWidget {
|
||||
_TicketsTab(packages: d.ticketPackages),
|
||||
_CardsTab(data: d),
|
||||
const _CarpetsTab(),
|
||||
const _FramesTab(),
|
||||
_BoostersTab(boosters: d.boosters),
|
||||
_VipTab(packages: d.vipPackages),
|
||||
],
|
||||
@@ -171,6 +176,7 @@ class _ShopTabs extends StatelessWidget {
|
||||
Tab(text: 'بلیط'),
|
||||
Tab(text: 'کارت'),
|
||||
Tab(text: 'فرش'),
|
||||
Tab(text: 'قاب'),
|
||||
Tab(text: 'تجهیزات'),
|
||||
Tab(text: 'VIP'),
|
||||
],
|
||||
@@ -540,6 +546,176 @@ class _CarpetCard extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
// ===== تبِ قابهای آواتار =====
|
||||
|
||||
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: [
|
||||
|
||||
Reference in New Issue
Block a user