feat: add characters
This commit is contained in:
@@ -10,6 +10,7 @@ class _ShopTabs extends StatelessWidget {
|
||||
(Icons.style, 'کارت'),
|
||||
(Icons.casino, 'فرش'),
|
||||
(Icons.filter_center_focus, 'قاب'),
|
||||
(Icons.face, 'شخصیت'),
|
||||
(Icons.bolt, 'تجهیزات'),
|
||||
(Icons.workspace_premium, 'VIP'),
|
||||
];
|
||||
@@ -352,7 +353,7 @@ class _CarpetCard extends StatelessWidget {
|
||||
),
|
||||
)
|
||||
: Image.network(
|
||||
c.imageUrl,
|
||||
c.img,
|
||||
fit: BoxFit.cover,
|
||||
errorBuilder:
|
||||
(_, __, ___) => Container(
|
||||
@@ -430,8 +431,8 @@ class _FramesTabState extends State<_FramesTab> {
|
||||
}
|
||||
|
||||
void _reload() => setState(() {
|
||||
_future = _api.getFrames();
|
||||
});
|
||||
_future = _api.getFrames();
|
||||
});
|
||||
|
||||
Future<void> _act(AvatarFrame f, {required bool buy}) async {
|
||||
setState(() => _busy = f.id);
|
||||
@@ -453,12 +454,15 @@ class _FramesTabState extends State<_FramesTab> {
|
||||
builder: (context, snap) {
|
||||
if (snap.connectionState != ConnectionState.done) {
|
||||
return const Center(
|
||||
child: CircularProgressIndicator(color: AppColors.gold));
|
||||
child: CircularProgressIndicator(color: AppColors.gold),
|
||||
);
|
||||
}
|
||||
if (!snap.hasData) {
|
||||
return Center(
|
||||
child: TextButton(
|
||||
onPressed: _reload, child: const Text('تلاش مجدد')),
|
||||
onPressed: _reload,
|
||||
child: const Text('تلاش مجدد'),
|
||||
),
|
||||
);
|
||||
}
|
||||
final cat = snap.data!;
|
||||
@@ -471,7 +475,8 @@ class _FramesTabState extends State<_FramesTab> {
|
||||
_FrameCard(
|
||||
frame: f,
|
||||
avatarSeed: seed.isEmpty ? 'hakem-1' : seed,
|
||||
selected: cat.selected == f.id ||
|
||||
selected:
|
||||
cat.selected == f.id ||
|
||||
(cat.selected.isEmpty && f.id == 'none'),
|
||||
busy: _busy == f.id,
|
||||
onSelect: () => _act(f, buy: false),
|
||||
@@ -503,19 +508,23 @@ class _FrameCard extends StatelessWidget {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final f = frame;
|
||||
final grad = frameGradient(f.id) ?? const [Color(0xFFF3D27A), AppColors.goldDark];
|
||||
final grad =
|
||||
frameGradient(f.id) ?? const [Color(0xFFF3D27A), AppColors.goldDark];
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(10),
|
||||
decoration: _shopCardDeco(highlight: selected),
|
||||
child: Column(
|
||||
children: [
|
||||
Text(f.title,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
color: AppColors.gold,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 14)),
|
||||
Text(
|
||||
f.title,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
color: AppColors.gold,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
// پیشنمایش: آواتار درونِ قاب با گرادیانِ همان قاب.
|
||||
Expanded(
|
||||
@@ -536,9 +545,7 @@ class _FrameCard extends StatelessWidget {
|
||||
),
|
||||
),
|
||||
child: ClipOval(
|
||||
child: SizedBox.expand(
|
||||
child: RandomAvatar(avatarSeed),
|
||||
),
|
||||
child: SizedBox.expand(child: RandomAvatar(avatarSeed)),
|
||||
),
|
||||
);
|
||||
},
|
||||
@@ -549,8 +556,10 @@ class _FrameCard extends StatelessWidget {
|
||||
if (f.vip && !f.owned)
|
||||
const Padding(
|
||||
padding: EdgeInsets.only(bottom: 4),
|
||||
child: Text('ویژهی VIP',
|
||||
style: TextStyle(color: AppColors.gold, fontSize: 11)),
|
||||
child: Text(
|
||||
'ویژهی VIP',
|
||||
style: TextStyle(color: AppColors.gold, fontSize: 11),
|
||||
),
|
||||
),
|
||||
SizedBox(width: double.infinity, child: _action(f)),
|
||||
],
|
||||
@@ -560,8 +569,12 @@ class _FrameCard extends StatelessWidget {
|
||||
|
||||
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);
|
||||
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,
|
||||
@@ -571,6 +584,213 @@ class _FrameCard extends StatelessWidget {
|
||||
}
|
||||
}
|
||||
|
||||
// ===== تبِ شخصیتهای آواتار =====
|
||||
|
||||
class _AvatarsTab extends StatefulWidget {
|
||||
const _AvatarsTab();
|
||||
@override
|
||||
State<_AvatarsTab> createState() => _AvatarsTabState();
|
||||
}
|
||||
|
||||
class _AvatarsTabState extends State<_AvatarsTab> {
|
||||
final _api = locator<AvatarApiProvider>();
|
||||
late Future<AvatarCatalog> _future;
|
||||
String? _busy;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_future = _api.getAvatars();
|
||||
}
|
||||
|
||||
void _reload() => setState(() {
|
||||
_future = _api.getAvatars();
|
||||
});
|
||||
|
||||
Future<void> _act(AvatarCharacter a, {required bool buy}) async {
|
||||
setState(() => _busy = a.id);
|
||||
final err =
|
||||
buy ? await _api.buyAvatar(a.id) : await _api.selectAvatar(a.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)));
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _clear() async {
|
||||
setState(() => _busy = '__none__');
|
||||
await _api.selectAvatar(''); // بازگشت به آواتارِ تصادفی
|
||||
if (!mounted) return;
|
||||
setState(() => _busy = null);
|
||||
context.read<WalletBloc>().add(LoadWalletEvent());
|
||||
_reload();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return FutureBuilder<AvatarCatalog>(
|
||||
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';
|
||||
if (cat.avatars.isEmpty) {
|
||||
return const Center(
|
||||
child: Padding(
|
||||
padding: EdgeInsets.all(24),
|
||||
child: Text(
|
||||
'بهزودی شخصیتهای تازه اضافه میشوند.',
|
||||
style: TextStyle(color: AppColors.gold),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
return _grid(
|
||||
note:
|
||||
'شخصیتِ آواتارت را انتخاب کن؛ در کلِ بازی (سرِ میز، پروفایل و لابی) دیده میشود.',
|
||||
children: [
|
||||
// «پیشفرض»: بازگشت به آواتارِ تصادفی.
|
||||
_AvatarCard.defaultRandom(
|
||||
avatarSeed: seed.isEmpty ? 'hakem-1' : seed,
|
||||
selected: cat.selected.isEmpty,
|
||||
busy: _busy == '__none__',
|
||||
onSelect: _clear,
|
||||
),
|
||||
for (final a in cat.avatars)
|
||||
_AvatarCard(
|
||||
avatar: a,
|
||||
avatarSeed: seed.isEmpty ? 'hakem-1' : seed,
|
||||
selected: cat.selected == a.id,
|
||||
busy: _busy == a.id,
|
||||
onSelect: () => _act(a, buy: false),
|
||||
onBuy: () => _act(a, buy: true),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _AvatarCard extends StatelessWidget {
|
||||
final AvatarCharacter? avatar; // null ⇒ کارتِ «پیشفرض» (آواتارِ تصادفی)
|
||||
final String avatarSeed;
|
||||
final bool selected;
|
||||
final bool busy;
|
||||
final VoidCallback onSelect;
|
||||
final VoidCallback? onBuy;
|
||||
const _AvatarCard({
|
||||
required this.avatar,
|
||||
required this.avatarSeed,
|
||||
required this.selected,
|
||||
required this.busy,
|
||||
required this.onSelect,
|
||||
required this.onBuy,
|
||||
});
|
||||
|
||||
const _AvatarCard.defaultRandom({
|
||||
required this.avatarSeed,
|
||||
required this.selected,
|
||||
required this.busy,
|
||||
required this.onSelect,
|
||||
}) : avatar = null,
|
||||
onBuy = null;
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final a = avatar;
|
||||
final title = a?.title ?? 'پیشفرض';
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(10),
|
||||
decoration: _shopCardDeco(highlight: selected),
|
||||
child: Column(
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
maxLines: 1,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
color: AppColors.gold,
|
||||
fontWeight: FontWeight.bold,
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Expanded(
|
||||
child: Center(
|
||||
child: LayoutBuilder(
|
||||
builder: (_, box) {
|
||||
final d = box.maxHeight.clamp(40.0, 96.0);
|
||||
return Container(
|
||||
width: d,
|
||||
height: d,
|
||||
padding: EdgeInsets.all(d * 0.06),
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: AppColors.bgDark,
|
||||
border: Border.all(
|
||||
color: selected ? AppColors.gold : AppColors.goldDark,
|
||||
width: selected ? 2 : 1.4,
|
||||
),
|
||||
),
|
||||
child: PlayerAvatar(
|
||||
seed: avatarSeed,
|
||||
imageUrl: a?.img ?? '',
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 6),
|
||||
if (a != null && a.vip && !a.owned)
|
||||
const Padding(
|
||||
padding: EdgeInsets.only(bottom: 4),
|
||||
child: Text(
|
||||
'ویژهی VIP',
|
||||
style: TextStyle(color: AppColors.gold, fontSize: 11),
|
||||
),
|
||||
),
|
||||
SizedBox(width: double.infinity, child: _action(a)),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _action(AvatarCharacter? a) {
|
||||
if (busy) return const _PriceButton(label: '...', disabled: true);
|
||||
if (selected) {
|
||||
return const _PriceButton(label: 'انتخاب شده', disabled: true);
|
||||
}
|
||||
if (a == null || a.owned) {
|
||||
return _PriceButton(label: 'انتخاب', green: true, onTap: onSelect);
|
||||
}
|
||||
return _PriceButton(
|
||||
label: '${a.priceCoins} سکه',
|
||||
green: true,
|
||||
coin: true,
|
||||
onTap: onBuy,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Widget _grid({required String note, required List<Widget> children}) {
|
||||
return Column(
|
||||
children: [
|
||||
@@ -618,24 +838,26 @@ Widget _grid({required String note, required List<Widget> children}) {
|
||||
/// قابِ مشترکِ کارتهای فروشگاه (سکه/کارت/فرش/…): گرادیانِ لاجوردی، حاشیهی طلایی
|
||||
/// و عمقِ سایه. [highlight] برای آیتمِ انتخابشده هالهی طلایی اضافه میکند.
|
||||
BoxDecoration _shopCardDeco({bool highlight = false}) => BoxDecoration(
|
||||
gradient: const LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: [Color(0xFF1C4A80), Color(0xFF0B2646)],
|
||||
),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(
|
||||
color: highlight ? AppColors.gold : AppColors.goldDark,
|
||||
width: highlight ? 2 : 1.3,
|
||||
),
|
||||
boxShadow: [
|
||||
const BoxShadow(
|
||||
color: Colors.black54, blurRadius: 10, offset: Offset(0, 5)),
|
||||
if (highlight)
|
||||
BoxShadow(
|
||||
color: AppColors.gold.withValues(alpha: 0.3), blurRadius: 14),
|
||||
],
|
||||
);
|
||||
gradient: const LinearGradient(
|
||||
begin: Alignment.topLeft,
|
||||
end: Alignment.bottomRight,
|
||||
colors: [Color(0xFF1C4A80), Color(0xFF0B2646)],
|
||||
),
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
border: Border.all(
|
||||
color: highlight ? AppColors.gold : AppColors.goldDark,
|
||||
width: highlight ? 2 : 1.3,
|
||||
),
|
||||
boxShadow: [
|
||||
const BoxShadow(
|
||||
color: Colors.black54,
|
||||
blurRadius: 10,
|
||||
offset: Offset(0, 5),
|
||||
),
|
||||
if (highlight)
|
||||
BoxShadow(color: AppColors.gold.withValues(alpha: 0.3), blurRadius: 14),
|
||||
],
|
||||
);
|
||||
|
||||
/// عددِ موجودی با جداکنندهی هزارگان (مثلِ ۱۲٬۵۰۰) برای خواناییِ بهتر.
|
||||
String _fmt(int n) {
|
||||
@@ -761,9 +983,10 @@ class _ItemCard extends StatelessWidget {
|
||||
padding: const EdgeInsets.symmetric(horizontal: 28, vertical: 4),
|
||||
decoration: BoxDecoration(
|
||||
gradient: LinearGradient(
|
||||
colors: isBonus
|
||||
? const [AppColors.success, AppColors.successDark]
|
||||
: const [Color(0xFFFFD54F), AppColors.goldDark],
|
||||
colors:
|
||||
isBonus
|
||||
? const [AppColors.success, AppColors.successDark]
|
||||
: const [Color(0xFFFFD54F), AppColors.goldDark],
|
||||
),
|
||||
boxShadow: const [
|
||||
BoxShadow(color: Colors.black54, blurRadius: 4),
|
||||
@@ -915,14 +1138,15 @@ class _PriceButtonState extends State<_PriceButton> {
|
||||
Widget build(BuildContext context) {
|
||||
final busy = context.select((ShopBloc c) => c.state.busy);
|
||||
final enabled = !(widget.disabled || busy) && widget.onTap != null;
|
||||
final colors = widget.disabled
|
||||
? const [Color(0xFF555555), Color(0xFF333333)]
|
||||
: widget.green
|
||||
final colors =
|
||||
widget.disabled
|
||||
? const [Color(0xFF555555), Color(0xFF333333)]
|
||||
: widget.green
|
||||
? const [AppColors.success, AppColors.successDark]
|
||||
: const [
|
||||
AppColors.accent,
|
||||
Color(0xFF7A1019),
|
||||
]; // شنگرفِ ایرانی (همرنگِ تم)
|
||||
AppColors.accent,
|
||||
Color(0xFF7A1019),
|
||||
]; // شنگرفِ ایرانی (همرنگِ تم)
|
||||
return GestureDetector(
|
||||
onTapDown: enabled ? (_) => setState(() => _down = true) : null,
|
||||
onTapUp: enabled ? (_) => setState(() => _down = false) : null,
|
||||
@@ -945,15 +1169,16 @@ class _PriceButtonState extends State<_PriceButton> {
|
||||
),
|
||||
borderRadius: BorderRadius.circular(11),
|
||||
border: Border.all(color: AppColors.gold, width: 1.2),
|
||||
boxShadow: enabled
|
||||
? [
|
||||
BoxShadow(
|
||||
color: colors.last.withValues(alpha: 0.55),
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 3),
|
||||
),
|
||||
]
|
||||
: null,
|
||||
boxShadow:
|
||||
enabled
|
||||
? [
|
||||
BoxShadow(
|
||||
color: colors.last.withValues(alpha: 0.55),
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, 3),
|
||||
),
|
||||
]
|
||||
: null,
|
||||
),
|
||||
child: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
|
||||
Reference in New Issue
Block a user