From 6b9b9a0f3a979ec6aa6df4d60589bca7d0ce6168 Mon Sep 17 00:00:00 2001 From: Amirmahdi Nourkazemi Date: Wed, 8 Jul 2026 13:48:29 +0330 Subject: [PATCH] fix: frames --- lib/core/theme/frames.dart | 53 +- lib/core/theme/ranks.dart | 51 ++ lib/core/widgets/rank_badge.dart | 17 +- .../game/presentation/screen/game_screen.dart | 2 + .../game/presentation/widgets/table_hud.dart | 22 +- .../presentation/screen/profile_screen.dart | 492 ++++++++++++------ .../remote/frame_api_provider.dart | 34 +- lib/feature/shop/domain/entities/frame.dart | 6 + 8 files changed, 461 insertions(+), 216 deletions(-) create mode 100644 lib/core/theme/ranks.dart diff --git a/lib/core/theme/frames.dart b/lib/core/theme/frames.dart index f203d39..a74d051 100644 --- a/lib/core/theme/frames.dart +++ b/lib/core/theme/frames.dart @@ -1,24 +1,39 @@ import 'package:flutter/material.dart'; -import 'app_theme.dart'; +/// کشِ رنگِ قاب‌ها که از سرور می‌آید (id → گرادیان). با هر بار خواندنِ کاتالوگِ +/// قاب‌ها پر می‌شود؛ هیچ رنگی سمتِ کلاینت hardcode نیست (تا کش پر نشود، null). +class FrameStyles { + FrameStyles._(); + static final FrameStyles I = FrameStyles._(); -/// گرادیانِ هر قابِ آواتار (کازمتیک). شناسه‌ها با کاتالوگِ سرور جور است. -/// null یعنی «بدون قابِ اختصاصی» ⇒ از قابِ رتبه استفاده شود. -List? frameGradient(String id) { - switch (id) { - case 'gold': - return const [Color(0xFFF3D27A), AppColors.goldDark]; - case 'fire': - return const [Color(0xFFFF9A3C), Color(0xFFB81D2A)]; - case 'emerald': - return const [Color(0xFF7BEAA5), Color(0xFF1B7A4A)]; - case 'ocean': - return const [Color(0xFF8FD3F0), Color(0xFF1E5FA8)]; - case 'rose': - return const [Color(0xFFF7A8C4), Color(0xFFA83060)]; - case 'royal': - return const [Color(0xFFCE9BEA), Color(0xFF6A2FA0)]; - default: // none / '' / ناشناخته - return null; + final Map> _cache = {}; + + /// کشِ رنگ‌ها را از کاتالوگِ سرور پر می‌کند. هر آیتم باید id و دو رنگِ hex بدهد. + void setFromCatalog(Iterable<({String id, String c1, String c2})> frames) { + for (final f in frames) { + final g = _parse(f.c1, f.c2); + if (g != null) _cache[f.id] = g; + } + } + + List? gradient(String id) { + if (id.isEmpty || id == 'none') return null; + return _cache[id]; + } + + static List? _parse(String c1, String c2) { + final a = _hex(c1), b = _hex(c2); + if (a == null || b == null) return null; + return [a, b]; + } + + static Color? _hex(String s) { + s = s.trim().replaceAll('#', ''); + if (s.length != 6) return null; + final v = int.tryParse(s, radix: 16); + return v == null ? null : Color(0xFF000000 | v); } } + +/// گرادیانِ قابِ آواتار (از کشِ سرور). null ⇒ قابِ رتبه. +List? frameGradient(String id) => FrameStyles.I.gradient(id); diff --git a/lib/core/theme/ranks.dart b/lib/core/theme/ranks.dart new file mode 100644 index 0000000..65fd5bb --- /dev/null +++ b/lib/core/theme/ranks.dart @@ -0,0 +1,51 @@ +import 'package:flutter/material.dart'; + +import 'app_theme.dart'; + +/// کشِ رنگ و برچسبِ رتبه‌ها که از سرور می‌آید (tier → گرادیان/برچسب). با هر بار +/// خواندنِ کاتالوگِ قاب‌ها پر می‌شود؛ تا آن لحظه از fallbackِ خنثی استفاده می‌شود. +class RankStyles { + RankStyles._(); + static final RankStyles I = RankStyles._(); + + final Map> _grad = {}; + final Map _label = {}; + + /// کش را از کاتالوگِ سرور پر می‌کند. هر آیتم id/label/c1/c2 دارد. + void setFromCatalog( + Iterable<({String id, String label, String c1, String c2})> tiers, + ) { + for (final t in tiers) { + final g = _parse(t.c1, t.c2); + if (g != null) _grad[t.id] = g; + if (t.label.isNotEmpty) _label[t.id] = t.label; + } + } + + /// گرادیانِ رتبه (از سرور، وگرنه fallbackِ خنثیِ طلایی). + List gradient(String tier) => _grad[tier] ?? _fallbackGrad; + + /// برچسبِ فارسیِ رتبه (از سرور، وگرنه خودِ شناسه). + String label(String tier) => _label[tier] ?? tier; + + static const _fallbackGrad = [AppColors.gold, AppColors.goldDark]; + + static List? _parse(String c1, String c2) { + final a = _hex(c1), b = _hex(c2); + if (a == null || b == null) return null; + return [a, b]; + } + + static Color? _hex(String s) { + s = s.trim().replaceAll('#', ''); + if (s.length != 6) return null; + final v = int.tryParse(s, radix: 16); + return v == null ? null : Color(0xFF000000 | v); + } +} + +/// گرادیانِ رنگیِ رتبه (از کشِ سرور). +List rankGradient(String tier) => RankStyles.I.gradient(tier); + +/// برچسبِ فارسیِ رتبه (از کشِ سرور). +String rankLabel(String tier) => RankStyles.I.label(tier); diff --git a/lib/core/widgets/rank_badge.dart b/lib/core/widgets/rank_badge.dart index 05d3f0f..21bf2b2 100644 --- a/lib/core/widgets/rank_badge.dart +++ b/lib/core/widgets/rank_badge.dart @@ -1,27 +1,18 @@ import 'package:flutter/material.dart'; -import '../theme/app_theme.dart'; +import '../theme/ranks.dart'; -/// نشانِ رتبه (برنز/نقره/طلا/الماس/پادشاه) با رنگ و برچسبِ فارسی. +/// نشانِ رتبه (برنز/نقره/طلا/الماس/پادشاه) با رنگ و برچسبِ فارسی از سرور. class RankBadge extends StatelessWidget { final String tier; // bronze..king final int? points; // در صورت نمایش امتیاز final double size; const RankBadge({super.key, required this.tier, this.points, this.size = 16}); - static const _tiers = { - 'bronze': ('برنز', [Color(0xFFCD7F32), Color(0xFF8C5A2B)]), - 'silver': ('نقره', [Color(0xFFBFC6CC), Color(0xFF8A949B)]), - 'gold': ('طلا', [Color(0xFFFFD54F), AppColors.goldDark]), - 'diamond': ('الماس', [Color(0xFF5BE0E6), Color(0xFF1E8A99)]), - 'king': ('پادشاه', [Color(0xFFB388FF), Color(0xFF6A1B9A)]), - }; - @override Widget build(BuildContext context) { - final t = _tiers[tier] ?? _tiers['bronze']!; - final label = t.$1; - final colors = t.$2; + final label = rankLabel(tier); + final colors = rankGradient(tier); return Container( padding: EdgeInsets.symmetric(horizontal: size * 0.6, vertical: size * 0.25), decoration: BoxDecoration( diff --git a/lib/feature/game/presentation/screen/game_screen.dart b/lib/feature/game/presentation/screen/game_screen.dart index 7f059e6..b329305 100644 --- a/lib/feature/game/presentation/screen/game_screen.dart +++ b/lib/feature/game/presentation/screen/game_screen.dart @@ -11,6 +11,7 @@ import '../../../../core/locator/locator.dart'; import '../../../../core/network/ws_client.dart'; import '../../../../core/service/app_sounds.dart'; import '../../../../core/theme/app_theme.dart'; +import '../../../shop/data/data_source/remote/frame_api_provider.dart'; import '../../../tournament/data/data_source/remote/tournament_api_provider.dart'; import '../../../tournament/domain/entities/tournament.dart'; import '../../../wallet/presentation/bloc/wallet_bloc.dart'; @@ -54,6 +55,7 @@ class _GameScreenState extends State { carpet: _carpet.isEmpty ? 'classic' : _carpet, ); _loadTournament(); + locator().warmCache(); // رنگِ قاب‌ها برای HUD (best-effort) } /// بررسی می‌کند آیا کاربر در تورنومنتِ فعالی ثبت‌نام کرده تا نشانِ درون‌بازی و diff --git a/lib/feature/game/presentation/widgets/table_hud.dart b/lib/feature/game/presentation/widgets/table_hud.dart index 621041e..9f42bb1 100644 --- a/lib/feature/game/presentation/widgets/table_hud.dart +++ b/lib/feature/game/presentation/widgets/table_hud.dart @@ -4,6 +4,7 @@ import 'package:random_avatar/random_avatar.dart'; import '../../../../core/network/ws_client.dart'; import '../../../../core/theme/app_theme.dart'; import '../../../../core/theme/frames.dart'; +import '../../../../core/theme/ranks.dart'; import '../../domain/entities/game_entities.dart'; import '../bloc/game_state.dart'; @@ -212,25 +213,10 @@ class TableHud extends StatelessWidget { ); } -/// گرادیانِ قابِ آواتار بر اساسِ نشانِ رتبه (برنز→پادشاه). پیش‌فرض: طلایی. -List _rankFrame(String tier) { - switch (tier) { - case 'bronze': - return const [Color(0xFFCD9B6A), Color(0xFF7A4A22)]; - case 'silver': - return const [Color(0xFFEDF1F4), Color(0xFF98A2AD)]; - case 'diamond': - return const [Color(0xFF9BE7F0), Color(0xFF2E8BA0)]; - case 'king': - return const [Color(0xFFCE9BEA), Color(0xFF6A2FA0)]; - default: // gold و ناشناخته/بات - return const [Color(0xFFF3D27A), AppColors.goldDark]; - } -} - -/// قابِ نمایشِ آواتارِ یک بازیکن: قابِ اختصاصیِ خریداری‌شده (اگر باشد)، وگرنه قابِ رتبه. +/// قابِ نمایشِ آواتارِ یک بازیکن: قابِ اختصاصیِ خریداری‌شده (اگر باشد)، وگرنه قابِ +/// رتبه (رنگ از سرور). بات‌ها رتبه ندارند ⇒ رنگِ پیش‌فرض. List _seatFrame(GamePlayer p) => - frameGradient(p.frame) ?? _rankFrame(p.bot ? '' : p.rankTier); + frameGradient(p.frame) ?? rankGradient(p.bot ? '' : p.rankTier); class _PlayerSeat extends StatelessWidget { final GamePlayer player; diff --git a/lib/feature/profile/presentation/screen/profile_screen.dart b/lib/feature/profile/presentation/screen/profile_screen.dart index c8a2196..90840af 100644 --- a/lib/feature/profile/presentation/screen/profile_screen.dart +++ b/lib/feature/profile/presentation/screen/profile_screen.dart @@ -6,6 +6,7 @@ import 'package:random_avatar/random_avatar.dart'; import '../../../../core/settings/app_settings.dart'; import '../../../../core/theme/app_theme.dart'; +import '../../../../core/theme/ranks.dart'; import '../../../../core/widgets/game_ui.dart'; import '../../../../core/widgets/rank_badge.dart'; import '../../../wallet/presentation/bloc/wallet_bloc.dart'; @@ -88,8 +89,9 @@ class ProfileScreen extends StatelessWidget { Widget _content(BuildContext context, ProfileEntity d) { return SingleChildScrollView( - padding: const EdgeInsets.all(16), + padding: const EdgeInsets.fromLTRB(16, 8, 16, 28), child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, children: [ Row( children: [ @@ -100,117 +102,23 @@ class ProfileScreen extends StatelessWidget { const SizedBox(width: 48), ], ), - const SizedBox(height: 8), - GamePanel( - child: Column( - children: [ - Stack( - children: [ - Container( - padding: const EdgeInsets.all(6), - decoration: BoxDecoration( - shape: BoxShape.circle, - border: Border.all(color: AppColors.gold, width: 2.5), - ), - child: RandomAvatar(d.avatar, height: 92, width: 92), - ), - Positioned( - bottom: 0, - right: 0, - child: GestureDetector( - onTap: () => _editProfile(context, d), - child: Container( - padding: const EdgeInsets.all(6), - decoration: const BoxDecoration( - shape: BoxShape.circle, - gradient: LinearGradient( - colors: [Color(0xFFFFD54F), AppColors.goldDark], - ), - ), - child: const Icon( - Icons.edit, - color: AppColors.bg, - size: 18, - ), - ), - ), - ), - ], - ), - const SizedBox(height: 10), - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Flexible(child: GlowText(d.name, size: 22)), - if (d.vip) ...[const SizedBox(width: 8), const _VipBadge()], - ], - ), - const SizedBox(height: 8), - RankBadge(tier: d.rankTier, points: d.rankPoints), - if (d.mobile.isNotEmpty) - Text( - d.mobile, - style: const TextStyle(color: Colors.white38, fontSize: 12), - ), - const SizedBox(height: 14), - Row( - children: [ - Expanded( - child: _MiniStat( - icon: Icons.star, - label: 'سطح', - value: '${d.level}', - ), - ), - Expanded( - child: _MiniStat( - icon: Icons.emoji_events, - label: 'جام', - value: '${d.trophies}', - ), - ), - ], - ), - const SizedBox(height: 10), - ClipRRect( - borderRadius: BorderRadius.circular(5), - child: LinearProgressIndicator( - value: d.xpNext == 0 ? 0 : d.xpInto / d.xpNext, - minHeight: 8, - backgroundColor: Colors.white10, - valueColor: const AlwaysStoppedAnimation(AppColors.gold), - ), - ), - Padding( - padding: const EdgeInsets.only(top: 4), - child: Text( - '${d.xpInto} / ${d.xpNext} XP', - style: const TextStyle(color: Colors.white38, fontSize: 11), - ), - ), - ], - ), - ), - const SizedBox(height: 16), - const Align( - alignment: Alignment.centerRight, - child: GlowText('آمار بازی', size: 18), - ), - const SizedBox(height: 8), + const SizedBox(height: 12), + _HeroCard(profile: d, onEdit: () => _editProfile(context, d)), + const SizedBox(height: 14), + _HighlightStrip(profile: d), + const SizedBox(height: 20), + _SectionHeader(icon: Icons.query_stats, title: 'آمار بازی'), + const SizedBox(height: 10), _statsSection(context, d), - const SizedBox(height: 16), + const SizedBox(height: 20), + _SectionHeader(icon: Icons.tune, title: 'تنظیمات'), + const SizedBox(height: 10), const _GraphicsToggle(), - const SizedBox(height: 8), - Align( - alignment: Alignment.center, - child: TextButton.icon( - onPressed: () => context.push('/terms'), - icon: const Icon(Icons.gavel, color: AppColors.gold, size: 18), - label: const Text( - 'شرایط و قوانین', - style: TextStyle(color: AppColors.gold, fontSize: 14), - ), - ), + const SizedBox(height: 10), + _SettingsTile( + icon: Icons.gavel, + label: 'شرایط و قوانین', + onTap: () => context.push('/terms'), ), ], ), @@ -219,32 +127,45 @@ class ProfileScreen extends StatelessWidget { Widget _statsSection(BuildContext context, ProfileEntity d) { final s = d.stats; - final rows = [ - _StatRow('بازی کل', s?.games, Icons.casino), - _StatRow('برد کل', s?.wins, Icons.thumb_up), - _StatRow('باخت کل', s?.losses, Icons.thumb_down), - _StatRow('کُت کردن', s?.kotMade, Icons.flash_on), - _StatRow('کُت شدن', s?.kotReceived, Icons.flash_off), - _StatRow('بریدن', s?.cuts, Icons.bolt), - _StatRow('دست حاکم', s?.hakemCount, Icons.workspace_premium), + final rate = + (s != null && s.games > 0) + ? '${(s.wins * 100 / s.games).round()}٪' + : '—'; + final tiles = [ + _StatTile('نرخ برد', rate, Icons.percent, accent: true), + _StatTile('بازی کل', '${s?.games ?? '—'}', Icons.casino), + _StatTile('برد', '${s?.wins ?? '—'}', Icons.thumb_up), + _StatTile('باخت', '${s?.losses ?? '—'}', Icons.thumb_down), + _StatTile('کُت کردن', '${s?.kotMade ?? '—'}', Icons.flash_on), + _StatTile('کُت شدن', '${s?.kotReceived ?? '—'}', Icons.flash_off), + _StatTile('بریدن', '${s?.cuts ?? '—'}', Icons.bolt), + _StatTile('دست حاکم', '${s?.hakemCount ?? '—'}', Icons.workspace_premium), ]; - final panel = GamePanel(child: Column(children: rows)); - if (d.vip) return panel; + final grid = GridView.count( + crossAxisCount: 2, + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + mainAxisSpacing: 10, + crossAxisSpacing: 10, + childAspectRatio: 2.2, + children: tiles, + ); + if (d.vip) return grid; return Stack( children: [ - Opacity(opacity: 0.35, child: IgnorePointer(child: panel)), + Opacity(opacity: 0.3, child: IgnorePointer(child: grid)), Positioned.fill( child: Container( decoration: BoxDecoration( - color: Colors.black.withValues(alpha: 0.45), + color: Colors.black.withValues(alpha: 0.5), borderRadius: BorderRadius.circular(16), border: Border.all(color: AppColors.goldDark), ), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ - const Icon(Icons.lock, color: AppColors.gold, size: 36), + const Icon(Icons.lock, color: AppColors.gold, size: 34), const SizedBox(height: 8), const Padding( padding: EdgeInsets.symmetric(horizontal: 24), @@ -275,30 +196,115 @@ class ProfileScreen extends StatelessWidget { } } -class _StatRow extends StatelessWidget { - final String label; - final Object? value; - final IconData icon; - const _StatRow(this.label, this.value, this.icon); +/// کارتِ سرآمدِ پروفایل: آواتار با حلقه‌ی رتبه، نام، رتبه و نوارِ سطح/XP. +class _HeroCard extends StatelessWidget { + final ProfileEntity profile; + final VoidCallback onEdit; + const _HeroCard({required this.profile, required this.onEdit}); + @override Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.symmetric(vertical: 7), - child: Row( + final d = profile; + final grad = rankGradient(d.rankTier); + final pct = d.xpNext == 0 ? 0.0 : (d.xpInto / d.xpNext).clamp(0.0, 1.0); + return GamePanel( + child: Column( children: [ - Icon(icon, color: AppColors.gold, size: 20), - const SizedBox(width: 10), - Text( - label, - style: const TextStyle(color: Colors.white, fontSize: 15), + Stack( + children: [ + Container( + padding: const EdgeInsets.all(4), + decoration: BoxDecoration( + shape: BoxShape.circle, + gradient: LinearGradient( + colors: grad, + begin: Alignment.topLeft, + end: Alignment.bottomRight, + ), + boxShadow: [ + BoxShadow( + color: grad.first.withValues(alpha: 0.5), + blurRadius: 14, + spreadRadius: 1, + ), + ], + ), + child: Container( + padding: const EdgeInsets.all(3), + decoration: const BoxDecoration( + shape: BoxShape.circle, + color: AppColors.bgDark, + ), + child: RandomAvatar(d.avatar, height: 90, width: 90), + ), + ), + Positioned( + bottom: 0, + right: 0, + child: GestureDetector( + onTap: onEdit, + child: Container( + padding: const EdgeInsets.all(7), + decoration: BoxDecoration( + shape: BoxShape.circle, + gradient: const LinearGradient( + colors: [Color(0xFFFFD54F), AppColors.goldDark], + ), + border: Border.all(color: AppColors.bgDark, width: 2), + ), + child: const Icon( + Icons.edit, + color: AppColors.bg, + size: 16, + ), + ), + ), + ), + ], ), - const Spacer(), - Text( - '${value ?? '—'}', - style: const TextStyle( - color: AppColors.gold, - fontSize: 16, - fontWeight: FontWeight.bold, + const SizedBox(height: 12), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Flexible(child: GlowText(d.name, size: 22)), + if (d.vip) ...[const SizedBox(width: 8), const _VipBadge()], + ], + ), + const SizedBox(height: 8), + RankBadge(tier: d.rankTier, points: d.rankPoints), + if (d.mobile.isNotEmpty) ...[ + const SizedBox(height: 6), + Text( + d.mobile, + style: const TextStyle(color: Colors.white38, fontSize: 12), + ), + ], + const SizedBox(height: 16), + Row( + children: [ + Text( + 'سطح ${d.level}', + style: const TextStyle( + color: AppColors.gold, + fontSize: 13, + fontWeight: FontWeight.bold, + ), + ), + const Spacer(), + Text( + '${d.xpInto} / ${d.xpNext} XP', + style: const TextStyle(color: Colors.white38, fontSize: 11), + ), + ], + ), + const SizedBox(height: 6), + ClipRRect( + borderRadius: BorderRadius.circular(6), + child: LinearProgressIndicator( + value: pct.toDouble(), + minHeight: 9, + backgroundColor: Colors.white10, + valueColor: const AlwaysStoppedAnimation(AppColors.gold), ), ), ], @@ -307,34 +313,198 @@ class _StatRow extends StatelessWidget { } } -class _MiniStat extends StatelessWidget { +/// نوارِ سه‌گانه‌ی برجسته: سطح، جام، امتیازِ رتبه. +class _HighlightStrip extends StatelessWidget { + final ProfileEntity profile; + const _HighlightStrip({required this.profile}); + + @override + Widget build(BuildContext context) { + return Row( + children: [ + Expanded( + child: _Highlight( + icon: Icons.star, + value: '${profile.level}', + label: 'سطح', + ), + ), + const SizedBox(width: 10), + Expanded( + child: _Highlight( + icon: Icons.emoji_events, + value: '${profile.trophies}', + label: 'جام', + ), + ), + const SizedBox(width: 10), + Expanded( + child: _Highlight( + icon: Icons.military_tech, + value: '${profile.rankPoints}', + label: 'امتیاز', + ), + ), + ], + ); + } +} + +class _Highlight extends StatelessWidget { final IconData icon; - final String label; final String value; - const _MiniStat({ + final String label; + const _Highlight({ required this.icon, - required this.label, required this.value, + required this.label, }); @override Widget build(BuildContext context) { - return Column( + return Container( + padding: const EdgeInsets.symmetric(vertical: 12), + decoration: BoxDecoration( + color: AppColors.panel.withValues(alpha: 0.55), + borderRadius: BorderRadius.circular(14), + border: Border.all(color: AppColors.goldFaint), + ), + child: Column( + children: [ + Icon(icon, color: AppColors.gold, size: 22), + const SizedBox(height: 4), + Text( + value, + style: const TextStyle( + color: Colors.white, + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + Text( + label, + style: const TextStyle(color: Colors.white54, fontSize: 11), + ), + ], + ), + ); + } +} + +/// عنوانِ بخش با آیکون (راست‌چین). +class _SectionHeader extends StatelessWidget { + final IconData icon; + final String title; + const _SectionHeader({required this.icon, required this.title}); + @override + Widget build(BuildContext context) { + return Row( children: [ - Icon(icon, color: AppColors.gold, size: 22), - const SizedBox(height: 2), - Text( - value, - style: const TextStyle( - color: Colors.white, - fontSize: 18, - fontWeight: FontWeight.bold, + Icon(icon, color: AppColors.gold, size: 18), + const SizedBox(width: 8), + GlowText(title, size: 18), + ], + ); + } +} + +/// کاشیِ آماری (آیکون + مقدار + برچسب). accent ⇒ برجسته با قابِ طلایی. +class _StatTile extends StatelessWidget { + final String label; + final String value; + final IconData icon; + final bool accent; + const _StatTile(this.label, this.value, this.icon, {this.accent = false}); + @override + Widget build(BuildContext context) { + return Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + decoration: BoxDecoration( + color: + accent + ? AppColors.gold.withValues(alpha: 0.14) + : AppColors.panel.withValues(alpha: 0.5), + borderRadius: BorderRadius.circular(14), + border: Border.all( + color: accent ? AppColors.gold : AppColors.goldFaint, + width: accent ? 1.4 : 1, + ), + ), + child: Row( + children: [ + Icon(icon, color: AppColors.gold, size: 22), + const SizedBox(width: 10), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text( + value, + style: const TextStyle( + color: Colors.white, + fontSize: 17, + fontWeight: FontWeight.bold, + ), + ), + Text( + label, + style: const TextStyle( + color: Colors.white54, + fontSize: 11, + ), + ), + ], + ), + ), + ], + ), + ); + } +} + +/// ردیفِ تنظیمات (آیکون + برچسب + فلش) با ظاهرِ هماهنگ با سوئیچِ گرافیک. +class _SettingsTile extends StatelessWidget { + final IconData icon; + final String label; + final VoidCallback onTap; + const _SettingsTile({ + required this.icon, + required this.label, + required this.onTap, + }); + @override + Widget build(BuildContext context) { + return Material( + color: Colors.transparent, + child: InkWell( + borderRadius: BorderRadius.circular(12), + onTap: onTap, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 14), + decoration: BoxDecoration( + color: AppColors.panel.withValues(alpha: 0.5), + borderRadius: BorderRadius.circular(12), + border: Border.all(color: AppColors.goldFaint), + ), + child: Row( + children: [ + Icon(icon, color: AppColors.gold, size: 20), + const SizedBox(width: 10), + Expanded( + child: Text( + label, + style: const TextStyle(color: Colors.white, fontSize: 14), + ), + ), + const Icon( + Icons.chevron_left, + color: Colors.white38, + size: 20, + ), + ], ), ), - Text( - label, - style: const TextStyle(color: Colors.white54, fontSize: 12), - ), - ], + ), ); } } diff --git a/lib/feature/shop/data/data_source/remote/frame_api_provider.dart b/lib/feature/shop/data/data_source/remote/frame_api_provider.dart index 5a2a5fc..e5587c1 100644 --- a/lib/feature/shop/data/data_source/remote/frame_api_provider.dart +++ b/lib/feature/shop/data/data_source/remote/frame_api_provider.dart @@ -1,5 +1,7 @@ import '../../../../../core/locator/locator.dart'; import '../../../../../core/network/api_provider_imp.dart'; +import '../../../../../core/theme/frames.dart'; +import '../../../../../core/theme/ranks.dart'; import '../../../domain/entities/frame.dart'; /// دادهٔ قاب‌های آواتار از backend (دریافت، خرید، انتخاب). @@ -12,12 +14,34 @@ class FrameApiProvider { throw Exception('frames: ${res.statusCode}'); } final list = (res.data['frames'] as List?) ?? const []; - return FrameCatalog( - frames: list - .map((e) => AvatarFrame.fromJson(Map.from(e as Map))) - .toList(), - selected: (res.data['selected'] ?? '') as String, + final frames = list + .map((e) => AvatarFrame.fromJson(Map.from(e as Map))) + .toList(); + // کشِ رنگِ قاب‌ها را به‌روزرسانی کن تا HUD رنگِ درست را از سرور بگیرد. + FrameStyles.I.setFromCatalog( + frames.map((f) => (id: f.id, c1: f.c1, c2: f.c2)), ); + // کشِ رنگ/برچسبِ رتبه‌ها هم از همین پاسخ پر می‌شود. + final tiers = (res.data['rank_tiers'] as List?) ?? const []; + RankStyles.I.setFromCatalog( + tiers.map((e) { + final m = Map.from(e as Map); + return ( + id: (m['id'] ?? '') as String, + label: (m['label'] ?? '') as String, + c1: (m['c1'] ?? '') as String, + c2: (m['c2'] ?? '') as String, + ); + }), + ); + return FrameCatalog(frames: frames, selected: (res.data['selected'] ?? '') as String); + } + + /// کشِ رنگِ قاب‌ها را از سرور گرم می‌کند (best-effort؛ برای رندرِ قابِ دیگران). + Future warmCache() async { + try { + await getFrames(); + } catch (_) {} } /// خرید؛ null یعنی موفق، وگرنه پیامِ خطای فارسی. diff --git a/lib/feature/shop/domain/entities/frame.dart b/lib/feature/shop/domain/entities/frame.dart index 57bd2bd..c914d11 100644 --- a/lib/feature/shop/domain/entities/frame.dart +++ b/lib/feature/shop/domain/entities/frame.dart @@ -4,6 +4,8 @@ class AvatarFrame { final String title; final int priceCoins; final bool vip; + final String c1; // رنگِ گرادیان (hex بدونِ #)؛ خالی ⇒ قابِ رتبه + final String c2; final bool owned; const AvatarFrame({ @@ -11,6 +13,8 @@ class AvatarFrame { required this.title, required this.priceCoins, required this.vip, + required this.c1, + required this.c2, required this.owned, }); @@ -19,6 +23,8 @@ class AvatarFrame { title: (j['title'] ?? '') as String, priceCoins: (j['price_coins'] ?? 0) as int, vip: (j['vip'] ?? false) as bool, + c1: (j['c1'] ?? '') as String, + c2: (j['c2'] ?? '') as String, owned: (j['owned'] ?? false) as bool, ); }