feat:add frames

This commit is contained in:
2026-07-08 12:15:06 +03:30
parent 46a649166b
commit df2ae2a215
9 changed files with 379 additions and 70 deletions
@@ -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: [