fix: frames
This commit is contained in:
+34
-19
@@ -1,24 +1,39 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import 'app_theme.dart';
|
/// کشِ رنگِ قابها که از سرور میآید (id → گرادیان). با هر بار خواندنِ کاتالوگِ
|
||||||
|
/// قابها پر میشود؛ هیچ رنگی سمتِ کلاینت hardcode نیست (تا کش پر نشود، null).
|
||||||
|
class FrameStyles {
|
||||||
|
FrameStyles._();
|
||||||
|
static final FrameStyles I = FrameStyles._();
|
||||||
|
|
||||||
/// گرادیانِ هر قابِ آواتار (کازمتیک). شناسهها با کاتالوگِ سرور جور است.
|
final Map<String, List<Color>> _cache = {};
|
||||||
/// null یعنی «بدون قابِ اختصاصی» ⇒ از قابِ رتبه استفاده شود.
|
|
||||||
List<Color>? frameGradient(String id) {
|
/// کشِ رنگها را از کاتالوگِ سرور پر میکند. هر آیتم باید id و دو رنگِ hex بدهد.
|
||||||
switch (id) {
|
void setFromCatalog(Iterable<({String id, String c1, String c2})> frames) {
|
||||||
case 'gold':
|
for (final f in frames) {
|
||||||
return const [Color(0xFFF3D27A), AppColors.goldDark];
|
final g = _parse(f.c1, f.c2);
|
||||||
case 'fire':
|
if (g != null) _cache[f.id] = g;
|
||||||
return const [Color(0xFFFF9A3C), Color(0xFFB81D2A)];
|
}
|
||||||
case 'emerald':
|
}
|
||||||
return const [Color(0xFF7BEAA5), Color(0xFF1B7A4A)];
|
|
||||||
case 'ocean':
|
List<Color>? gradient(String id) {
|
||||||
return const [Color(0xFF8FD3F0), Color(0xFF1E5FA8)];
|
if (id.isEmpty || id == 'none') return null;
|
||||||
case 'rose':
|
return _cache[id];
|
||||||
return const [Color(0xFFF7A8C4), Color(0xFFA83060)];
|
}
|
||||||
case 'royal':
|
|
||||||
return const [Color(0xFFCE9BEA), Color(0xFF6A2FA0)];
|
static List<Color>? _parse(String c1, String c2) {
|
||||||
default: // none / '' / ناشناخته
|
final a = _hex(c1), b = _hex(c2);
|
||||||
return null;
|
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<Color>? frameGradient(String id) => FrameStyles.I.gradient(id);
|
||||||
|
|||||||
@@ -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<String, List<Color>> _grad = {};
|
||||||
|
final Map<String, String> _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<Color> gradient(String tier) => _grad[tier] ?? _fallbackGrad;
|
||||||
|
|
||||||
|
/// برچسبِ فارسیِ رتبه (از سرور، وگرنه خودِ شناسه).
|
||||||
|
String label(String tier) => _label[tier] ?? tier;
|
||||||
|
|
||||||
|
static const _fallbackGrad = [AppColors.gold, AppColors.goldDark];
|
||||||
|
|
||||||
|
static List<Color>? _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<Color> rankGradient(String tier) => RankStyles.I.gradient(tier);
|
||||||
|
|
||||||
|
/// برچسبِ فارسیِ رتبه (از کشِ سرور).
|
||||||
|
String rankLabel(String tier) => RankStyles.I.label(tier);
|
||||||
@@ -1,27 +1,18 @@
|
|||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
|
||||||
import '../theme/app_theme.dart';
|
import '../theme/ranks.dart';
|
||||||
|
|
||||||
/// نشانِ رتبه (برنز/نقره/طلا/الماس/پادشاه) با رنگ و برچسبِ فارسی.
|
/// نشانِ رتبه (برنز/نقره/طلا/الماس/پادشاه) با رنگ و برچسبِ فارسی از سرور.
|
||||||
class RankBadge extends StatelessWidget {
|
class RankBadge extends StatelessWidget {
|
||||||
final String tier; // bronze..king
|
final String tier; // bronze..king
|
||||||
final int? points; // در صورت نمایش امتیاز
|
final int? points; // در صورت نمایش امتیاز
|
||||||
final double size;
|
final double size;
|
||||||
const RankBadge({super.key, required this.tier, this.points, this.size = 16});
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
final t = _tiers[tier] ?? _tiers['bronze']!;
|
final label = rankLabel(tier);
|
||||||
final label = t.$1;
|
final colors = rankGradient(tier);
|
||||||
final colors = t.$2;
|
|
||||||
return Container(
|
return Container(
|
||||||
padding: EdgeInsets.symmetric(horizontal: size * 0.6, vertical: size * 0.25),
|
padding: EdgeInsets.symmetric(horizontal: size * 0.6, vertical: size * 0.25),
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import '../../../../core/locator/locator.dart';
|
|||||||
import '../../../../core/network/ws_client.dart';
|
import '../../../../core/network/ws_client.dart';
|
||||||
import '../../../../core/service/app_sounds.dart';
|
import '../../../../core/service/app_sounds.dart';
|
||||||
import '../../../../core/theme/app_theme.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/data/data_source/remote/tournament_api_provider.dart';
|
||||||
import '../../../tournament/domain/entities/tournament.dart';
|
import '../../../tournament/domain/entities/tournament.dart';
|
||||||
import '../../../wallet/presentation/bloc/wallet_bloc.dart';
|
import '../../../wallet/presentation/bloc/wallet_bloc.dart';
|
||||||
@@ -54,6 +55,7 @@ class _GameScreenState extends State<GameScreen> {
|
|||||||
carpet: _carpet.isEmpty ? 'classic' : _carpet,
|
carpet: _carpet.isEmpty ? 'classic' : _carpet,
|
||||||
);
|
);
|
||||||
_loadTournament();
|
_loadTournament();
|
||||||
|
locator<FrameApiProvider>().warmCache(); // رنگِ قابها برای HUD (best-effort)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// بررسی میکند آیا کاربر در تورنومنتِ فعالی ثبتنام کرده تا نشانِ درونبازی و
|
/// بررسی میکند آیا کاربر در تورنومنتِ فعالی ثبتنام کرده تا نشانِ درونبازی و
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import 'package:random_avatar/random_avatar.dart';
|
|||||||
import '../../../../core/network/ws_client.dart';
|
import '../../../../core/network/ws_client.dart';
|
||||||
import '../../../../core/theme/app_theme.dart';
|
import '../../../../core/theme/app_theme.dart';
|
||||||
import '../../../../core/theme/frames.dart';
|
import '../../../../core/theme/frames.dart';
|
||||||
|
import '../../../../core/theme/ranks.dart';
|
||||||
import '../../domain/entities/game_entities.dart';
|
import '../../domain/entities/game_entities.dart';
|
||||||
import '../bloc/game_state.dart';
|
import '../bloc/game_state.dart';
|
||||||
|
|
||||||
@@ -212,25 +213,10 @@ class TableHud extends StatelessWidget {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// گرادیانِ قابِ آواتار بر اساسِ نشانِ رتبه (برنز→پادشاه). پیشفرض: طلایی.
|
/// قابِ نمایشِ آواتارِ یک بازیکن: قابِ اختصاصیِ خریداریشده (اگر باشد)، وگرنه قابِ
|
||||||
List<Color> _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<Color> _seatFrame(GamePlayer p) =>
|
List<Color> _seatFrame(GamePlayer p) =>
|
||||||
frameGradient(p.frame) ?? _rankFrame(p.bot ? '' : p.rankTier);
|
frameGradient(p.frame) ?? rankGradient(p.bot ? '' : p.rankTier);
|
||||||
|
|
||||||
class _PlayerSeat extends StatelessWidget {
|
class _PlayerSeat extends StatelessWidget {
|
||||||
final GamePlayer player;
|
final GamePlayer player;
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import 'package:random_avatar/random_avatar.dart';
|
|||||||
|
|
||||||
import '../../../../core/settings/app_settings.dart';
|
import '../../../../core/settings/app_settings.dart';
|
||||||
import '../../../../core/theme/app_theme.dart';
|
import '../../../../core/theme/app_theme.dart';
|
||||||
|
import '../../../../core/theme/ranks.dart';
|
||||||
import '../../../../core/widgets/game_ui.dart';
|
import '../../../../core/widgets/game_ui.dart';
|
||||||
import '../../../../core/widgets/rank_badge.dart';
|
import '../../../../core/widgets/rank_badge.dart';
|
||||||
import '../../../wallet/presentation/bloc/wallet_bloc.dart';
|
import '../../../wallet/presentation/bloc/wallet_bloc.dart';
|
||||||
@@ -88,8 +89,9 @@ class ProfileScreen extends StatelessWidget {
|
|||||||
|
|
||||||
Widget _content(BuildContext context, ProfileEntity d) {
|
Widget _content(BuildContext context, ProfileEntity d) {
|
||||||
return SingleChildScrollView(
|
return SingleChildScrollView(
|
||||||
padding: const EdgeInsets.all(16),
|
padding: const EdgeInsets.fromLTRB(16, 8, 16, 28),
|
||||||
child: Column(
|
child: Column(
|
||||||
|
crossAxisAlignment: CrossAxisAlignment.stretch,
|
||||||
children: [
|
children: [
|
||||||
Row(
|
Row(
|
||||||
children: [
|
children: [
|
||||||
@@ -100,117 +102,23 @@ class ProfileScreen extends StatelessWidget {
|
|||||||
const SizedBox(width: 48),
|
const SizedBox(width: 48),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 12),
|
||||||
GamePanel(
|
_HeroCard(profile: d, onEdit: () => _editProfile(context, d)),
|
||||||
child: Column(
|
const SizedBox(height: 14),
|
||||||
children: [
|
_HighlightStrip(profile: d),
|
||||||
Stack(
|
const SizedBox(height: 20),
|
||||||
children: [
|
_SectionHeader(icon: Icons.query_stats, title: 'آمار بازی'),
|
||||||
Container(
|
const SizedBox(height: 10),
|
||||||
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),
|
|
||||||
_statsSection(context, d),
|
_statsSection(context, d),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 20),
|
||||||
|
_SectionHeader(icon: Icons.tune, title: 'تنظیمات'),
|
||||||
|
const SizedBox(height: 10),
|
||||||
const _GraphicsToggle(),
|
const _GraphicsToggle(),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 10),
|
||||||
Align(
|
_SettingsTile(
|
||||||
alignment: Alignment.center,
|
icon: Icons.gavel,
|
||||||
child: TextButton.icon(
|
label: 'شرایط و قوانین',
|
||||||
onPressed: () => context.push('/terms'),
|
onTap: () => context.push('/terms'),
|
||||||
icon: const Icon(Icons.gavel, color: AppColors.gold, size: 18),
|
|
||||||
label: const Text(
|
|
||||||
'شرایط و قوانین',
|
|
||||||
style: TextStyle(color: AppColors.gold, fontSize: 14),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -219,32 +127,45 @@ class ProfileScreen extends StatelessWidget {
|
|||||||
|
|
||||||
Widget _statsSection(BuildContext context, ProfileEntity d) {
|
Widget _statsSection(BuildContext context, ProfileEntity d) {
|
||||||
final s = d.stats;
|
final s = d.stats;
|
||||||
final rows = <Widget>[
|
final rate =
|
||||||
_StatRow('بازی کل', s?.games, Icons.casino),
|
(s != null && s.games > 0)
|
||||||
_StatRow('برد کل', s?.wins, Icons.thumb_up),
|
? '${(s.wins * 100 / s.games).round()}٪'
|
||||||
_StatRow('باخت کل', s?.losses, Icons.thumb_down),
|
: '—';
|
||||||
_StatRow('کُت کردن', s?.kotMade, Icons.flash_on),
|
final tiles = <Widget>[
|
||||||
_StatRow('کُت شدن', s?.kotReceived, Icons.flash_off),
|
_StatTile('نرخ برد', rate, Icons.percent, accent: true),
|
||||||
_StatRow('بریدن', s?.cuts, Icons.bolt),
|
_StatTile('بازی کل', '${s?.games ?? '—'}', Icons.casino),
|
||||||
_StatRow('دست حاکم', s?.hakemCount, Icons.workspace_premium),
|
_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));
|
final grid = GridView.count(
|
||||||
if (d.vip) return panel;
|
crossAxisCount: 2,
|
||||||
|
shrinkWrap: true,
|
||||||
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
|
mainAxisSpacing: 10,
|
||||||
|
crossAxisSpacing: 10,
|
||||||
|
childAspectRatio: 2.2,
|
||||||
|
children: tiles,
|
||||||
|
);
|
||||||
|
if (d.vip) return grid;
|
||||||
|
|
||||||
return Stack(
|
return Stack(
|
||||||
children: [
|
children: [
|
||||||
Opacity(opacity: 0.35, child: IgnorePointer(child: panel)),
|
Opacity(opacity: 0.3, child: IgnorePointer(child: grid)),
|
||||||
Positioned.fill(
|
Positioned.fill(
|
||||||
child: Container(
|
child: Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: Colors.black.withValues(alpha: 0.45),
|
color: Colors.black.withValues(alpha: 0.5),
|
||||||
borderRadius: BorderRadius.circular(16),
|
borderRadius: BorderRadius.circular(16),
|
||||||
border: Border.all(color: AppColors.goldDark),
|
border: Border.all(color: AppColors.goldDark),
|
||||||
),
|
),
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
children: [
|
children: [
|
||||||
const Icon(Icons.lock, color: AppColors.gold, size: 36),
|
const Icon(Icons.lock, color: AppColors.gold, size: 34),
|
||||||
const SizedBox(height: 8),
|
const SizedBox(height: 8),
|
||||||
const Padding(
|
const Padding(
|
||||||
padding: EdgeInsets.symmetric(horizontal: 24),
|
padding: EdgeInsets.symmetric(horizontal: 24),
|
||||||
@@ -275,30 +196,115 @@ class ProfileScreen extends StatelessWidget {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class _StatRow extends StatelessWidget {
|
/// کارتِ سرآمدِ پروفایل: آواتار با حلقهی رتبه، نام، رتبه و نوارِ سطح/XP.
|
||||||
final String label;
|
class _HeroCard extends StatelessWidget {
|
||||||
final Object? value;
|
final ProfileEntity profile;
|
||||||
final IconData icon;
|
final VoidCallback onEdit;
|
||||||
const _StatRow(this.label, this.value, this.icon);
|
const _HeroCard({required this.profile, required this.onEdit});
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Padding(
|
final d = profile;
|
||||||
padding: const EdgeInsets.symmetric(vertical: 7),
|
final grad = rankGradient(d.rankTier);
|
||||||
child: Row(
|
final pct = d.xpNext == 0 ? 0.0 : (d.xpInto / d.xpNext).clamp(0.0, 1.0);
|
||||||
|
return GamePanel(
|
||||||
|
child: Column(
|
||||||
children: [
|
children: [
|
||||||
Icon(icon, color: AppColors.gold, size: 20),
|
Stack(
|
||||||
const SizedBox(width: 10),
|
children: [
|
||||||
Text(
|
Container(
|
||||||
label,
|
padding: const EdgeInsets.all(4),
|
||||||
style: const TextStyle(color: Colors.white, fontSize: 15),
|
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(),
|
const SizedBox(height: 12),
|
||||||
Text(
|
Row(
|
||||||
'${value ?? '—'}',
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
style: const TextStyle(
|
children: [
|
||||||
color: AppColors.gold,
|
Flexible(child: GlowText(d.name, size: 22)),
|
||||||
fontSize: 16,
|
if (d.vip) ...[const SizedBox(width: 8), const _VipBadge()],
|
||||||
fontWeight: FontWeight.bold,
|
],
|
||||||
|
),
|
||||||
|
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 IconData icon;
|
||||||
final String label;
|
|
||||||
final String value;
|
final String value;
|
||||||
const _MiniStat({
|
final String label;
|
||||||
|
const _Highlight({
|
||||||
required this.icon,
|
required this.icon,
|
||||||
required this.label,
|
|
||||||
required this.value,
|
required this.value,
|
||||||
|
required this.label,
|
||||||
});
|
});
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
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: [
|
children: [
|
||||||
Icon(icon, color: AppColors.gold, size: 22),
|
Icon(icon, color: AppColors.gold, size: 18),
|
||||||
const SizedBox(height: 2),
|
const SizedBox(width: 8),
|
||||||
Text(
|
GlowText(title, size: 18),
|
||||||
value,
|
],
|
||||||
style: const TextStyle(
|
);
|
||||||
color: Colors.white,
|
}
|
||||||
fontSize: 18,
|
}
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
|
/// کاشیِ آماری (آیکون + مقدار + برچسب). 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),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import '../../../../../core/locator/locator.dart';
|
import '../../../../../core/locator/locator.dart';
|
||||||
import '../../../../../core/network/api_provider_imp.dart';
|
import '../../../../../core/network/api_provider_imp.dart';
|
||||||
|
import '../../../../../core/theme/frames.dart';
|
||||||
|
import '../../../../../core/theme/ranks.dart';
|
||||||
import '../../../domain/entities/frame.dart';
|
import '../../../domain/entities/frame.dart';
|
||||||
|
|
||||||
/// دادهٔ قابهای آواتار از backend (دریافت، خرید، انتخاب).
|
/// دادهٔ قابهای آواتار از backend (دریافت، خرید، انتخاب).
|
||||||
@@ -12,12 +14,34 @@ class FrameApiProvider {
|
|||||||
throw Exception('frames: ${res.statusCode}');
|
throw Exception('frames: ${res.statusCode}');
|
||||||
}
|
}
|
||||||
final list = (res.data['frames'] as List?) ?? const [];
|
final list = (res.data['frames'] as List?) ?? const [];
|
||||||
return FrameCatalog(
|
final frames = list
|
||||||
frames: list
|
.map((e) => AvatarFrame.fromJson(Map<String, dynamic>.from(e as Map)))
|
||||||
.map((e) => AvatarFrame.fromJson(Map<String, dynamic>.from(e as Map)))
|
.toList();
|
||||||
.toList(),
|
// کشِ رنگِ قابها را بهروزرسانی کن تا HUD رنگِ درست را از سرور بگیرد.
|
||||||
selected: (res.data['selected'] ?? '') as String,
|
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<String, dynamic>.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<void> warmCache() async {
|
||||||
|
try {
|
||||||
|
await getFrames();
|
||||||
|
} catch (_) {}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// خرید؛ null یعنی موفق، وگرنه پیامِ خطای فارسی.
|
/// خرید؛ null یعنی موفق، وگرنه پیامِ خطای فارسی.
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ class AvatarFrame {
|
|||||||
final String title;
|
final String title;
|
||||||
final int priceCoins;
|
final int priceCoins;
|
||||||
final bool vip;
|
final bool vip;
|
||||||
|
final String c1; // رنگِ گرادیان (hex بدونِ #)؛ خالی ⇒ قابِ رتبه
|
||||||
|
final String c2;
|
||||||
final bool owned;
|
final bool owned;
|
||||||
|
|
||||||
const AvatarFrame({
|
const AvatarFrame({
|
||||||
@@ -11,6 +13,8 @@ class AvatarFrame {
|
|||||||
required this.title,
|
required this.title,
|
||||||
required this.priceCoins,
|
required this.priceCoins,
|
||||||
required this.vip,
|
required this.vip,
|
||||||
|
required this.c1,
|
||||||
|
required this.c2,
|
||||||
required this.owned,
|
required this.owned,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -19,6 +23,8 @@ class AvatarFrame {
|
|||||||
title: (j['title'] ?? '') as String,
|
title: (j['title'] ?? '') as String,
|
||||||
priceCoins: (j['price_coins'] ?? 0) as int,
|
priceCoins: (j['price_coins'] ?? 0) as int,
|
||||||
vip: (j['vip'] ?? false) as bool,
|
vip: (j['vip'] ?? false) as bool,
|
||||||
|
c1: (j['c1'] ?? '') as String,
|
||||||
|
c2: (j['c2'] ?? '') as String,
|
||||||
owned: (j['owned'] ?? false) as bool,
|
owned: (j['owned'] ?? false) as bool,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user