diff --git a/lib/feature/game/presentation/bloc/game_bloc.dart b/lib/feature/game/presentation/bloc/game_bloc.dart index ea781f9..6375573 100644 --- a/lib/feature/game/presentation/bloc/game_bloc.dart +++ b/lib/feature/game/presentation/bloc/game_bloc.dart @@ -45,7 +45,10 @@ class GameBloc extends Bloc { on( (event, emit) => repository.send({'type': 'play_card', 'card': event.card})); on((event, emit) => repository.send({'type': 'leave'})); - on((event, emit) => repository.send({'type': 'start_table'})); + on((event, emit) => + repository.send({'type': 'start_table', 'hands': event.hands})); + on((event, emit) => + repository.send({'type': 'rotate_table', 'dir': event.dir})); on((event, emit) => repository.send({'type': 'leave_table'})); on((event, emit) => emit(state.copyWith(clearNotice: true))); } @@ -82,7 +85,8 @@ class GameBloc extends Bloc { void chooseTrump(String suit) => add(ChooseTrumpEvent(suit)); void playCard(String card) => add(PlayCardEvent(card)); void leave() => add(LeaveGameEvent()); - void startTable() => add(StartTableEvent()); + void startTable(int hands) => add(StartTableEvent(hands)); + void rotateTable(int dir) => add(RotateTableEvent(dir)); void leaveTable() => add(LeaveTableEvent()); void clearNotice() => add(ClearNoticeEvent()); diff --git a/lib/feature/game/presentation/bloc/game_event.dart b/lib/feature/game/presentation/bloc/game_event.dart index 2f2e617..e9f6588 100644 --- a/lib/feature/game/presentation/bloc/game_event.dart +++ b/lib/feature/game/presentation/bloc/game_event.dart @@ -32,7 +32,15 @@ class PlayCardEvent extends GameEvent { class LeaveGameEvent extends GameEvent {} -class StartTableEvent extends GameEvent {} +class StartTableEvent extends GameEvent { + final int hands; // تعداد دستِ انتخابی (۳/۵/۷) + StartTableEvent(this.hands); +} + +class RotateTableEvent extends GameEvent { + final int dir; // +۱/−۱ جهتِ چرخشِ جایگاه‌ها + RotateTableEvent(this.dir); +} class LeaveTableEvent extends GameEvent {} diff --git a/lib/feature/game/presentation/bloc/game_state.dart b/lib/feature/game/presentation/bloc/game_state.dart index c5a02b2..1c4de6c 100644 --- a/lib/feature/game/presentation/bloc/game_state.dart +++ b/lib/feature/game/presentation/bloc/game_state.dart @@ -7,7 +7,8 @@ import '../../domain/entities/game_entities.dart'; class LobbyPlayer { final String name; final bool host; - const LobbyPlayer(this.name, this.host); + final int seat; // جایگاهِ مطلق (۰..۳) + const LobbyPlayer(this.name, this.host, this.seat); } /// وضعیت اتاق انتظارِ میز خصوصی (دورهمی). @@ -15,12 +16,14 @@ class TableLobby { final String code; final List players; final bool isHost; + final int youSeat; // جایگاهِ مطلقِ خودِ کاربر (برای چیدمانِ صحنه) final int remaining; final bool unlimited; const TableLobby({ required this.code, required this.players, required this.isHost, + required this.youSeat, required this.remaining, required this.unlimited, }); @@ -29,15 +32,19 @@ class TableLobby { code: (j['code'] ?? '') as String, players: ((j['players'] as List?) ?? []) .map((e) => LobbyPlayer( - (e['name'] ?? '') as String, (e['host'] ?? false) as bool)) + (e['name'] ?? '') as String, + (e['host'] ?? false) as bool, + (e['seat'] ?? 0) as int, + )) .toList(), isHost: (j['host'] ?? false) as bool, + youSeat: (j['you_seat'] ?? 0) as int, remaining: (j['remaining'] ?? 0) as int, unlimited: (j['unlimited'] ?? false) as bool, ); - String get sig => '$code|$isHost|$remaining|$unlimited|' - '${players.map((p) => '${p.name}${p.host ? '*' : ''}').join(',')}'; + String get sig => '$code|$isHost|$youSeat|$remaining|$unlimited|' + '${players.map((p) => '${p.name}${p.host ? '*' : ''}@${p.seat}').join(',')}'; } class GameUiState extends Equatable { diff --git a/lib/feature/game/presentation/screen/private_table_screen.dart b/lib/feature/game/presentation/screen/private_table_screen.dart index 3972a99..35ed1d1 100644 --- a/lib/feature/game/presentation/screen/private_table_screen.dart +++ b/lib/feature/game/presentation/screen/private_table_screen.dart @@ -4,6 +4,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; +import 'package:random_avatar/random_avatar.dart'; import '../../../../core/network/ws_client.dart'; import '../../../../core/theme/app_theme.dart'; @@ -44,21 +45,32 @@ class PrivateTableScreen extends StatelessWidget { } } -class _LobbyView extends StatelessWidget { +class _LobbyView extends StatefulWidget { final GameUiState state; const _LobbyView({required this.state}); + @override + State<_LobbyView> createState() => _LobbyViewState(); +} + +class _LobbyViewState extends State<_LobbyView> { + int _hands = 3; // تعداد دستِ انتخابیِ میزبان + + void _leave() { + context.read().leaveTable(); + if (context.canPop()) context.pop(); + } + @override Widget build(BuildContext context) { + final state = widget.state; final lobby = state.lobby; final connecting = state.connection != WsStatus.connected || lobby == null; return PopScope( canPop: false, onPopInvokedWithResult: (didPop, _) { - if (didPop) return; - context.read().leaveTable(); - if (context.canPop()) context.pop(); + if (!didPop) _leave(); }, child: Scaffold( body: GameBackground( @@ -67,30 +79,7 @@ class _LobbyView extends StatelessWidget { children: [ Column( children: [ - Align( - alignment: Alignment.centerRight, - child: Padding( - padding: const EdgeInsets.all(10), - child: GestureDetector( - onTap: () { - context.read().leaveTable(); - if (context.canPop()) context.pop(); - }, - child: Container( - width: 46, - height: 46, - decoration: BoxDecoration( - color: AppColors.panel, - borderRadius: BorderRadius.circular(12), - border: - Border.all(color: AppColors.gold, width: 1.5), - ), - child: const Icon(Icons.arrow_back, - color: AppColors.gold), - ), - ), - ), - ), + _topBar(context, lobby), Expanded( child: connecting ? const Center( @@ -110,42 +99,111 @@ class _LobbyView extends StatelessWidget { ); } + // نوار بالا: بازگشت (چپ) و اشتراک‌گذاریِ کد (راست). + Widget _topBar(BuildContext context, TableLobby? lobby) { + Widget btn(IconData icon, VoidCallback onTap) => GestureDetector( + onTap: onTap, + child: Container( + width: 46, + height: 46, + decoration: BoxDecoration( + color: AppColors.panel, + borderRadius: BorderRadius.circular(12), + border: Border.all(color: AppColors.gold, width: 1.5), + ), + child: Icon(icon, color: AppColors.gold), + ), + ); + return Padding( + padding: const EdgeInsets.all(10), + child: Directionality( + textDirection: TextDirection.ltr, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + btn(Icons.arrow_back, _leave), + if (lobby != null) + btn(Icons.share, () { + Clipboard.setData(ClipboardData(text: lobby.code)); + ScaffoldMessenger.of(context).showSnackBar(const SnackBar( + content: Text('کد میز کپی شد؛ برای دوستانت بفرست'))); + }), + ], + ), + ), + ); + } + Widget _content(BuildContext context, TableLobby lobby) { + final youSeat = lobby.youSeat; + // نگاشتِ جایگاهِ مطلق → بازیکن (خالی = بات هنگام شروع). + final bySeat = {for (final p in lobby.players) p.seat: p}; + // چیدمان از نگاهِ خودِ کاربر: «شما» پایین، هم‌تیمی (روبه‌رو) بالا، بقیه کنار. + LobbyPlayer? seatAt(int rel) => bySeat[(youSeat + rel) % 4]; + final me = seatAt(0); + final top = seatAt(2); // هم‌تیمی (روبه‌رو) + final left = seatAt(1); + final right = seatAt(3); + return SingleChildScrollView( - padding: const EdgeInsets.symmetric(horizontal: 20), + padding: const EdgeInsets.symmetric(horizontal: 16), child: Column( children: [ - const GlowText('میز دورهمی', size: 26), - const SizedBox(height: 16), - GamePanel( - child: Column(children: [ - const Text('شماره میز', style: TextStyle(color: Colors.white70)), - const SizedBox(height: 6), - Row(mainAxisAlignment: MainAxisAlignment.center, children: [ - SelectableText(lobby.code, - style: const TextStyle( - color: AppColors.gold, - fontSize: 40, - fontWeight: FontWeight.bold, - letterSpacing: 8)), - IconButton( - onPressed: () { - Clipboard.setData(ClipboardData(text: lobby.code)); - ScaffoldMessenger.of(context).showSnackBar( - const SnackBar(content: Text('کد کپی شد'))); - }, - icon: const Icon(Icons.copy, color: AppColors.gold), + const Text('شماره میز', style: TextStyle(color: Colors.white70)), + const SizedBox(height: 4), + GlowText(lobby.code, size: 36), + const SizedBox(height: 8), + // میز با چهار جایگاه. + SizedBox( + height: 340, + child: Stack( + children: [ + Padding( + padding: const EdgeInsets.symmetric( + horizontal: 28, vertical: 36), + child: Container( + decoration: BoxDecoration( + gradient: const RadialGradient( + colors: [Color(0xFF1E7E3A), Color(0xFF0C5022)], + radius: 0.9, + ), + borderRadius: BorderRadius.circular(160), + border: Border.all(color: const Color(0xFF8A5A12), width: 6), + ), + child: const Center( + child: GlowText('سلطان حکم', size: 18), + ), + ), ), - ]), - const Text('این کد را برای دوستانت بفرست', - style: TextStyle(color: Colors.white54, fontSize: 12)), - ]), + Align(alignment: Alignment.topCenter, child: _seat(top, false)), + Align(alignment: Alignment.centerLeft, child: _seat(left, false)), + Align( + alignment: Alignment.centerRight, child: _seat(right, false)), + Align(alignment: Alignment.bottomCenter, child: _seat(me, true)), + // چرخشِ جایگاه‌ها (فقط میزبان) — تغییرِ تیم‌بندی. + if (lobby.isHost) ...[ + Align( + alignment: const Alignment(-1, -0.35), + child: _rotateBtn(Icons.rotate_left, + () => context.read().rotateTable(-1)), + ), + Align( + alignment: const Alignment(1, -0.35), + child: _rotateBtn(Icons.rotate_right, + () => context.read().rotateTable(1)), + ), + ], + ], + ), ), - const SizedBox(height: 16), - GamePanel( - child: Column(children: [ - for (var i = 0; i < 4; i++) _seatRow(i, lobby), - ]), + const SizedBox(height: 8), + const Text('تعداد دست', style: TextStyle(color: Colors.white70)), + const SizedBox(height: 10), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + for (final h in [3, 5, 7]) _handChip(h, lobby.isHost), + ], ), const SizedBox(height: 20), if (lobby.isHost) @@ -154,41 +212,127 @@ class _LobbyView extends StatelessWidget { icon: Icons.play_arrow, width: double.infinity, colors: const [Color(0xFF3FA34D), Color(0xFF1B5E20)], - onTap: () => context.read().startTable(), + onTap: () => context.read().startTable(_hands), ) else const Text('در انتظار شروع توسط میزبان…', style: TextStyle(color: AppColors.gold, fontSize: 15)), - const SizedBox(height: 8), + const SizedBox(height: 6), if (lobby.isHost) const Text('جای‌های خالی با ربات پر می‌شوند', style: TextStyle(color: Colors.white54, fontSize: 12)), - const SizedBox(height: 24), + const SizedBox(height: 20), ], ), ); } - Widget _seatRow(int i, TableLobby lobby) { - final filled = i < lobby.players.length; - final p = filled ? lobby.players[i] : null; - return Padding( - padding: const EdgeInsets.symmetric(vertical: 6), - child: Row(children: [ - Icon(filled ? Icons.person : Icons.person_outline, - color: filled ? AppColors.gold : Colors.white24, size: 24), - const SizedBox(width: 10), - Text( - filled ? p!.name : 'در انتظار بازیکن…', - style: TextStyle( - color: filled ? Colors.white : Colors.white38, - fontSize: 15, - fontWeight: filled ? FontWeight.bold : FontWeight.normal), + // یک جایگاهِ بازیکن دورِ میز (آواتار + ستاره + نام). + Widget _seat(LobbyPlayer? p, bool isYou) { + final empty = p == null; + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + Stack( + clipBehavior: Clip.none, + children: [ + Container( + width: 62, + height: 62, + padding: const EdgeInsets.all(3), + decoration: BoxDecoration( + color: AppColors.bgDark, + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: isYou ? AppColors.gold : AppColors.goldDark, + width: 2), + ), + child: empty + ? const Icon(Icons.person_add_alt_1, color: Colors.white24) + : ClipRRect( + borderRadius: BorderRadius.circular(9), + child: RandomAvatar(p.name), + ), + ), + if (!empty) + const Positioned( + bottom: -6, + left: -4, + child: Icon(Icons.star, color: AppColors.gold, size: 22), + ), + ], + ), + const SizedBox(height: 6), + Text( + empty ? 'در انتظار' : (isYou ? 'شما' : p.name), + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: TextStyle( + color: empty ? Colors.white38 : Colors.white, + fontSize: 12, + fontWeight: empty ? FontWeight.normal : FontWeight.bold, + ), ), - const Spacer(), if (p?.host == true) - const Icon(Icons.star, color: AppColors.gold, size: 18), - ]), + const Text('میزبان', + style: TextStyle(color: AppColors.gold, fontSize: 10)), + ], + ); + } + + // دکمه‌ی چرخشِ جایگاه‌ها (کنارِ میز). + Widget _rotateBtn(IconData icon, VoidCallback onTap) { + return GestureDetector( + onTap: onTap, + child: Container( + width: 44, + height: 44, + decoration: BoxDecoration( + gradient: const LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [Color(0xFF8E1B2A), Color(0xFF5A0E18)], + ), + borderRadius: BorderRadius.circular(10), + border: Border.all(color: AppColors.gold, width: 1.5), + ), + child: Icon(icon, color: AppColors.gold, size: 26), + ), + ); + } + + Widget _handChip(int h, bool editable) { + const fa = {3: '۳', 5: '۵', 7: '۷'}; + final selected = _hands == h; + return Padding( + padding: const EdgeInsets.symmetric(horizontal: 8), + child: GestureDetector( + onTap: editable ? () => setState(() => _hands = h) : null, + child: Container( + width: 52, + height: 52, + alignment: Alignment.center, + decoration: BoxDecoration( + shape: BoxShape.circle, + gradient: selected + ? const LinearGradient( + colors: [Color(0xFFE9B949), Color(0xFFB8860B)]) + : null, + color: selected ? null : AppColors.panel, + border: Border.all( + color: selected ? AppColors.gold : AppColors.goldDark, + width: 2), + ), + child: Text( + fa[h]!, + style: TextStyle( + color: selected ? const Color(0xFF3A0A12) : Colors.white, + fontSize: 22, + fontWeight: FontWeight.bold, + ), + ), + ), + ), ); } } diff --git a/lib/feature/profile/presentation/screen/profile_screen.dart b/lib/feature/profile/presentation/screen/profile_screen.dart index b165b6c..69267f8 100644 --- a/lib/feature/profile/presentation/screen/profile_screen.dart +++ b/lib/feature/profile/presentation/screen/profile_screen.dart @@ -30,28 +30,38 @@ class ProfileScreen extends StatelessWidget { if (s is ProfileSaveSuccess) { context.read().add(LoadWalletEvent()); } else if (s is ProfileSaveError) { - ScaffoldMessenger.of(context) - .showSnackBar(SnackBar(content: Text(s.message))); + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text(s.message))); } }, builder: (context, state) { final st = state.loadStatus; if (st is ProfileLoadError) { return Center( - child: Column(mainAxisSize: MainAxisSize.min, children: [ - Text(st.message, - style: const TextStyle(color: Colors.white70)), - const SizedBox(height: 12), - GameButton( + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text( + st.message, + style: const TextStyle(color: Colors.white70), + ), + const SizedBox(height: 12), + GameButton( label: 'تلاش دوباره', - onTap: () => - context.read().add(LoadProfileEvent())), - ]), + onTap: + () => context.read().add( + LoadProfileEvent(), + ), + ), + ], + ), ); } if (st is! ProfileLoadLoaded) { return const Center( - child: CircularProgressIndicator(color: AppColors.gold)); + child: CircularProgressIndicator(color: AppColors.gold), + ); } return _content(context, st.profile); }, @@ -69,9 +79,9 @@ class ProfileScreen extends StatelessWidget { builder: (_) => _EditProfileSheet(name: d.name, avatar: d.avatar), ); if (result == null || !context.mounted) return; - context - .read() - .add(SaveProfileEvent(result['name']!, result['avatar']!)); + context.read().add( + SaveProfileEvent(result['name']!, result['avatar']!), + ); } Widget _content(BuildContext context, ProfileEntity d) { @@ -79,87 +89,112 @@ class ProfileScreen extends StatelessWidget { padding: const EdgeInsets.all(16), child: Column( children: [ - Row(children: [ - IconButton( - onPressed: () => context.pop(), - icon: const Icon(Icons.arrow_back, color: AppColors.gold), - ), - const Spacer(), - const GlowText('پروفایل', size: 24), - const Spacer(), - const SizedBox(width: 48), - ]), + Row( + children: [ + IconButton( + onPressed: () => context.pop(), + icon: const Icon(Icons.arrow_back, color: AppColors.gold), + ), + const Spacer(), + const GlowText('پروفایل', size: 24), + const Spacer(), + 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( + child: Column( + children: [ + Stack( + children: [ + Container( padding: const EdgeInsets.all(6), - decoration: const BoxDecoration( + decoration: BoxDecoration( shape: BoxShape.circle, - gradient: LinearGradient( - colors: [Color(0xFFFFD54F), Color(0xFFB8860B)]), + border: Border.all(color: AppColors.gold, width: 2.5), ), - child: const Icon(Icons.edit, - color: Color(0xFF3A0A12), size: 18), + 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), Color(0xFFB8860B)], + ), + ), + child: const Icon( + Icons.edit, + color: Color(0xFF3A0A12), + 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()], - ]), - 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( + 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()], + ], + ), + 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), + value: '${d.trophies}', + ), + ), + ], ), - ), - 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: 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)), + alignment: Alignment.centerRight, + child: GlowText('آمار بازی', size: 18), + ), const SizedBox(height: 8), _statsSection(context, d), ], @@ -181,43 +216,47 @@ class ProfileScreen extends StatelessWidget { final panel = GamePanel(child: Column(children: rows)); if (d.vip) return panel; - return Stack(children: [ - Opacity(opacity: 0.35, child: IgnorePointer(child: panel)), - Positioned.fill( - child: Container( - decoration: BoxDecoration( - color: Colors.black.withValues(alpha: 0.45), - 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 SizedBox(height: 8), - const Padding( - padding: EdgeInsets.symmetric(horizontal: 24), - child: Text('مشاهده‌ی آمار ویژه‌ی کاربران VIP است', + return Stack( + children: [ + Opacity(opacity: 0.35, child: IgnorePointer(child: panel)), + Positioned.fill( + child: Container( + decoration: BoxDecoration( + color: Colors.black.withValues(alpha: 0.45), + 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 SizedBox(height: 8), + const Padding( + padding: EdgeInsets.symmetric(horizontal: 24), + child: Text( + 'مشاهده‌ی آمار ویژه‌ی کاربران VIP است', textAlign: TextAlign.center, - style: TextStyle(color: Colors.white, fontSize: 14)), - ), - const SizedBox(height: 12), - GameButton( - label: 'تهیه اشتراک VIP', - icon: Icons.workspace_premium, - colors: const [Color(0xFFFFC107), Color(0xFFB8860B)], - onTap: () async { - await context.push('/vip'); - if (context.mounted) { - context.read().add(LoadProfileEvent()); - } - }, - ), - ], + style: TextStyle(color: Colors.white, fontSize: 14), + ), + ), + const SizedBox(height: 12), + GameButton( + label: 'تهیه اشتراک VIP', + icon: Icons.workspace_premium, + colors: const [Color(0xFFFFC107), Color(0xFFB8860B)], + onTap: () async { + await context.push('/vip'); + if (context.mounted) { + context.read().add(LoadProfileEvent()); + } + }, + ), + ], + ), ), ), - ), - ]); + ], + ); } } @@ -230,17 +269,25 @@ class _StatRow extends StatelessWidget { Widget build(BuildContext context) { return Padding( padding: const EdgeInsets.symmetric(vertical: 7), - child: Row(children: [ - Icon(icon, color: AppColors.gold, size: 20), - const SizedBox(width: 10), - Text(label, style: const TextStyle(color: Colors.white, fontSize: 15)), - const Spacer(), - Text('${value ?? '—'}', + child: Row( + children: [ + Icon(icon, color: AppColors.gold, size: 20), + const SizedBox(width: 10), + Text( + label, + style: const TextStyle(color: Colors.white, fontSize: 15), + ), + const Spacer(), + Text( + '${value ?? '—'}', style: const TextStyle( - color: AppColors.gold, - fontSize: 16, - fontWeight: FontWeight.bold)), - ]), + color: AppColors.gold, + fontSize: 16, + fontWeight: FontWeight.bold, + ), + ), + ], + ), ); } } @@ -249,18 +296,31 @@ class _MiniStat extends StatelessWidget { final IconData icon; final String label; final String value; - const _MiniStat( - {required this.icon, required this.label, required this.value}); + const _MiniStat({ + required this.icon, + required this.label, + required this.value, + }); @override Widget build(BuildContext context) { - return Column(children: [ - Icon(icon, color: AppColors.gold, size: 22), - const SizedBox(height: 2), - Text(value, + return Column( + 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)), - Text(label, style: const TextStyle(color: Colors.white54, fontSize: 12)), - ]); + color: Colors.white, + fontSize: 18, + fontWeight: FontWeight.bold, + ), + ), + Text( + label, + style: const TextStyle(color: Colors.white54, fontSize: 12), + ), + ], + ); } } @@ -271,15 +331,19 @@ class _VipBadge extends StatelessWidget { return Container( padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 3), decoration: BoxDecoration( - gradient: - const LinearGradient(colors: [Color(0xFFFFD54F), Color(0xFFB8860B)]), + gradient: const LinearGradient( + colors: [Color(0xFFFFD54F), Color(0xFFB8860B)], + ), borderRadius: BorderRadius.circular(8), ), - child: const Text('VIP', - style: TextStyle( - color: Color(0xFF3A0A12), - fontWeight: FontWeight.bold, - fontSize: 12)), + child: const Text( + 'VIP', + style: TextStyle( + color: Color(0xFF3A0A12), + fontWeight: FontWeight.bold, + fontSize: 12, + ), + ), ); } } @@ -318,81 +382,97 @@ class _EditProfileSheetState extends State<_EditProfileSheet> { @override Widget build(BuildContext context) { - return Padding( - padding: EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom), - child: Container( - decoration: const BoxDecoration( - color: AppColors.bgDark, - borderRadius: BorderRadius.vertical(top: Radius.circular(20)), - border: Border(top: BorderSide(color: AppColors.gold, width: 2)), + return SafeArea( + top: false, + child: Padding( + padding: EdgeInsets.only( + bottom: MediaQuery.of(context).viewInsets.bottom, ), - padding: const EdgeInsets.all(18), - child: SingleChildScrollView( - child: Column(mainAxisSize: MainAxisSize.min, children: [ - const GlowText('ویرایش پروفایل', size: 20), - const SizedBox(height: 14), - Container( - padding: const EdgeInsets.all(4), - decoration: BoxDecoration( - shape: BoxShape.circle, - border: Border.all(color: AppColors.gold, width: 2), - ), - child: RandomAvatar(_selected, height: 72, width: 72), - ), - const SizedBox(height: 12), - TextField( - controller: _name, - textAlign: TextAlign.center, - maxLength: 20, - inputFormatters: [LengthLimitingTextInputFormatter(20)], - decoration: const InputDecoration( - hintText: 'نام نمایشی (۲ تا ۲۰ حرف)', counterText: ''), - onChanged: (_) => setState(() {}), - ), - const SizedBox(height: 10), - const Align( - alignment: Alignment.centerRight, - child: - Text('انتخاب آواتار', style: TextStyle(color: AppColors.gold)), - ), - const SizedBox(height: 8), - GridView.count( - crossAxisCount: 4, - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - mainAxisSpacing: 10, - crossAxisSpacing: 10, + child: Container( + decoration: const BoxDecoration( + color: AppColors.bgDark, + borderRadius: BorderRadius.vertical(top: Radius.circular(20)), + border: Border(top: BorderSide(color: AppColors.gold, width: 2)), + ), + padding: const EdgeInsets.all(18), + child: SingleChildScrollView( + child: Column( + mainAxisSize: MainAxisSize.min, children: [ - for (final s in _seeds) - GestureDetector( - onTap: () => setState(() => _selected = s), - child: Container( - decoration: BoxDecoration( - shape: BoxShape.circle, - color: AppColors.panel, - border: Border.all( - color: _selected == s - ? AppColors.gold - : Colors.transparent, - width: 2.5, + const GlowText('ویرایش پروفایل', size: 20), + const SizedBox(height: 14), + Container( + padding: const EdgeInsets.all(4), + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all(color: AppColors.gold, width: 2), + ), + child: RandomAvatar(_selected, height: 72, width: 72), + ), + const SizedBox(height: 12), + TextField( + controller: _name, + textAlign: TextAlign.center, + maxLength: 20, + inputFormatters: [LengthLimitingTextInputFormatter(20)], + decoration: const InputDecoration( + hintText: 'نام نمایشی (۲ تا ۲۰ حرف)', + counterText: '', + ), + onChanged: (_) => setState(() {}), + ), + const SizedBox(height: 10), + const Align( + alignment: Alignment.centerRight, + child: Text( + 'انتخاب آواتار', + style: TextStyle(color: AppColors.gold), + ), + ), + const SizedBox(height: 8), + GridView.count( + crossAxisCount: 4, + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + mainAxisSpacing: 10, + crossAxisSpacing: 10, + children: [ + for (final s in _seeds) + GestureDetector( + onTap: () => setState(() => _selected = s), + child: Container( + decoration: BoxDecoration( + shape: BoxShape.circle, + color: AppColors.panel, + border: Border.all( + color: + _selected == s + ? AppColors.gold + : Colors.transparent, + width: 2.5, + ), + ), + child: RandomAvatar(s), ), ), - child: RandomAvatar(s), - ), - ), + ], + ), + const SizedBox(height: 16), + GameButton( + label: 'تأیید', + width: double.infinity, + colors: const [Color(0xFF3FA34D), Color(0xFF1B5E20)], + onTap: + _valid + ? () => Navigator.pop(context, { + 'name': _name.text.trim(), + 'avatar': _selected, + }) + : null, + ), ], ), - const SizedBox(height: 16), - GameButton( - label: 'تأیید', - width: double.infinity, - colors: const [Color(0xFF3FA34D), Color(0xFF1B5E20)], - onTap: _valid - ? () => Navigator.pop( - context, {'name': _name.text.trim(), 'avatar': _selected}) - : null, - ), - ]), + ), ), ), );