diff --git a/lib/app.dart b/lib/app.dart index 8fa8daf..f5bb4c1 100644 --- a/lib/app.dart +++ b/lib/app.dart @@ -17,6 +17,7 @@ import 'feature/game/presentation/screen/private_entry_screen.dart'; import 'feature/game/presentation/screen/private_table_screen.dart'; import 'feature/game/presentation/screen/tier_list_screen.dart'; import 'feature/legal/terms_screen.dart'; +import 'feature/tournament/presentation/screen/tournaments_screen.dart'; import 'feature/profile/presentation/bloc/profile_bloc.dart'; import 'feature/profile/presentation/bloc/profile_event.dart'; import 'feature/profile/presentation/screen/profile_screen.dart'; @@ -43,6 +44,9 @@ class HakemApp extends StatelessWidget { GoRoute(path: '/setup', builder: (_, __) => const ProfileSetupScreen()), GoRoute(path: '/lobby', builder: (_, __) => const LobbyScreen()), GoRoute(path: '/terms', builder: (_, __) => const TermsScreen()), + GoRoute( + path: '/tournaments', + builder: (_, __) => const TournamentsScreen()), GoRoute( path: '/profile', builder: (_, __) => BlocProvider( diff --git a/lib/core/locator/locator.dart b/lib/core/locator/locator.dart index fe214b2..997defd 100644 --- a/lib/core/locator/locator.dart +++ b/lib/core/locator/locator.dart @@ -32,6 +32,7 @@ import '../../feature/ranked/domain/use_cases/get_leaderboard_usecase.dart'; import '../../feature/ranked/presentation/bloc/leaderboard_bloc.dart'; import '../../feature/shop/data/data_source/remote/shop_api_provider.dart'; import '../../feature/shop/data/data_source/remote/carpet_api_provider.dart'; +import '../../feature/tournament/data/data_source/remote/tournament_api_provider.dart'; import '../../feature/shop/data/repository/shop_repository_impl.dart'; import '../../feature/shop/domain/repository/shop_repository.dart'; import '../../feature/shop/domain/use_cases/ad_reward_usecase.dart'; @@ -71,6 +72,7 @@ Future setupLocator() async { locator.registerSingleton(ProfileApiProvider()); locator.registerSingleton(RankedApiProvider()); locator.registerSingleton(ChatApiProvider()); + locator.registerSingleton(TournamentApiProvider()); locator.registerSingleton(GameApiProvider()); locator.registerSingleton(GameWsProvider(locator())); diff --git a/lib/feature/game/presentation/screen/game_screen.dart b/lib/feature/game/presentation/screen/game_screen.dart index 0cb5553..2001adf 100644 --- a/lib/feature/game/presentation/screen/game_screen.dart +++ b/lib/feature/game/presentation/screen/game_screen.dart @@ -6,9 +6,12 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; import 'package:random_avatar/random_avatar.dart'; +import '../../../../core/locator/locator.dart'; import '../../../../core/network/ws_client.dart'; import '../../../../core/service/app_sounds.dart'; import '../../../../core/theme/app_theme.dart'; +import '../../../tournament/data/data_source/remote/tournament_api_provider.dart'; +import '../../../tournament/domain/entities/tournament.dart'; import '../../../wallet/presentation/bloc/wallet_bloc.dart'; import '../../../wallet/presentation/bloc/wallet_event.dart'; import '../../../wallet/presentation/bloc/wallet_state.dart'; @@ -35,6 +38,7 @@ class _GameScreenState extends State { Timer? _introTimer; bool _introHidden = false; int _countdown = 0; // شمارشِ معکوسِ شروعِ بازی پس از یافتنِ حریفان (۳…۲…۱) + Tournament? _tournament; // تورنومنتِ فعالِ ثبت‌نام‌شده (اگر باشد) — برای نشانِ درون‌بازی @override void initState() { @@ -47,6 +51,19 @@ class _GameScreenState extends State { skin: skin.isEmpty ? 'simple' : skin, carpet: _carpet.isEmpty ? 'classic' : _carpet, ); + _loadTournament(); + } + + /// بررسی می‌کند آیا کاربر در تورنومنتِ فعالی ثبت‌نام کرده تا نشانِ درون‌بازی و + /// امتیازِ پایانِ بازی نمایش داده شود (این بازیِ عمومی به تورنومنت امتیاز می‌دهد). + Future _loadTournament() async { + try { + final list = await locator().getTournaments(); + final active = list.where((t) => t.joined && t.isActive).toList(); + if (mounted && active.isNotEmpty) { + setState(() => _tournament = active.first); + } + } catch (_) {/* بی‌اهمیت */} } @override @@ -128,6 +145,8 @@ class _GameScreenState extends State { onChat: () => _openChat(context), ), if (state.connection == WsStatus.disconnected) _connBanner(), + if (_tournament != null && !_showSearch(state)) + _tournamentBadge(), if (_showSearch(state)) _searchPanel(state), // دیالوگِ انتخابِ حکم و بنرِ نوبت فقط پس از پایانِ انیمیشنِ // پخشِ کارت نمایش داده می‌شوند (نه وسطِ بُر زدن/پخش). @@ -558,6 +577,44 @@ class _GameScreenState extends State { ); } + // نشانِ درون‌بازیِ تورنومنت: به کاربر می‌فهماند این بازیِ عمومی به تورنومنت + // امتیاز می‌دهد (بالای صحنه، وسط). + Widget _tournamentBadge() => Positioned( + top: 6, + left: 0, + right: 0, + child: IgnorePointer( + child: Center( + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 5), + decoration: BoxDecoration( + gradient: const LinearGradient( + colors: [Color(0xFF3A2A6E), AppColors.lapisInk], + ), + borderRadius: BorderRadius.circular(20), + border: Border.all(color: AppColors.gold, width: 1.2), + boxShadow: [ + BoxShadow( + color: AppColors.gold.withValues(alpha: 0.3), + blurRadius: 8), + ], + ), + child: Row(mainAxisSize: MainAxisSize.min, children: [ + const Icon(Icons.emoji_events, color: AppColors.gold, size: 15), + const SizedBox(width: 5), + Text( + 'تورنومنت: ${_tournament!.title}', + style: const TextStyle( + color: AppColors.gold, + fontSize: 12, + fontWeight: FontWeight.bold), + ), + ]), + ), + ), + ), + ); + Widget _gameOver(BuildContext context, GameUiState s) { final g = s.gameOver!; final mySeat = s.state?.yourSeat ?? 0; @@ -581,6 +638,29 @@ class _GameScreenState extends State { 'نتیجه نهایی: ${g.scores.isNotEmpty ? g.scores[0] : 0} - ${g.scores.length > 1 ? g.scores[1] : 0}', style: const TextStyle(color: Colors.white70, fontSize: 18), ), + // امتیازِ کسب‌شده‌ی تورنومنت (برد ۱۰۰، شرکت ۲۵ — قانونِ ثابتِ سرور). + if (_tournament != null) ...[ + const SizedBox(height: 16), + Container( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), + decoration: BoxDecoration( + color: AppColors.gold.withValues(alpha: 0.12), + borderRadius: BorderRadius.circular(14), + border: Border.all(color: AppColors.gold), + ), + child: Row(mainAxisSize: MainAxisSize.min, children: [ + const Icon(Icons.emoji_events, color: AppColors.gold, size: 20), + const SizedBox(width: 8), + Text( + '+${won ? 100 : 25} امتیاز تورنومنت «${_tournament!.title}»', + style: const TextStyle( + color: AppColors.gold, + fontSize: 14, + fontWeight: FontWeight.bold), + ), + ]), + ), + ], const SizedBox(height: 28), SizedBox( width: 220, diff --git a/lib/feature/game/presentation/screen/tier_list_screen.dart b/lib/feature/game/presentation/screen/tier_list_screen.dart index 5b2d332..e027596 100644 --- a/lib/feature/game/presentation/screen/tier_list_screen.dart +++ b/lib/feature/game/presentation/screen/tier_list_screen.dart @@ -50,9 +50,36 @@ class TierListScreen extends StatelessWidget { final tiers = state.tiers; return ListView.separated( padding: const EdgeInsets.fromLTRB(16, 4, 16, 20), - itemCount: tiers.length, + itemCount: tiers.length + 1, separatorBuilder: (_, __) => const SizedBox(height: 14), - itemBuilder: (_, i) => _TierCard(tier: tiers[i], index: i), + itemBuilder: (_, i) { + if (i == 0) { + return Container( + margin: const EdgeInsets.only(bottom: 2), + padding: const EdgeInsets.symmetric( + horizontal: 12, vertical: 10), + decoration: BoxDecoration( + color: AppColors.gold.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(12), + border: Border.all(color: AppColors.goldFaint), + ), + child: const Row(children: [ + Icon(Icons.military_tech, + color: AppColors.gold, size: 18), + SizedBox(width: 8), + Expanded( + child: Text( + 'با بردن در هر میز، امتیازِ رتبه می‌گیرید و در جدولِ قهرمانان بالا می‌روید.', + style: TextStyle( + color: Colors.white, fontSize: 12.5), + ), + ), + ]), + ); + } + final t = tiers[i - 1]; + return _TierCard(tier: t, index: i - 1); + }, ); }, ), @@ -78,7 +105,6 @@ class _TierCard extends StatelessWidget { @override Widget build(BuildContext context) { - if (tier.isRanked) return _rankedCard(context); final colors = _palettes[index % _palettes.length]; return GestureDetector( onTap: () => context.push('/game/${tier.id}?prize=${tier.prize}'), @@ -137,141 +163,10 @@ class _TierCard extends StatelessWidget { _badge(Icons.star, 'XP ${tier.xp}'), const SizedBox(height: 6), _badge(Icons.emoji_events, '${tier.trophy}'), - ]), - ], - ), - ), - ); - } - - // میزِ رتبه‌بندی: طرحِ ویژه‌ی سلطنتی (تاج، درخششِ طلایی، توضیح) تا از میزهای - // معمولی متمایز باشد و کاربر بداند این میز چه‌کاری می‌کند. - Widget _rankedCard(BuildContext context) { - return GestureDetector( - onTap: () => context.push('/game/${tier.id}?prize=${tier.prize}'), - child: Container( - padding: const EdgeInsets.all(14), - decoration: BoxDecoration( - gradient: const LinearGradient( - begin: Alignment.topLeft, - end: Alignment.bottomRight, - colors: [Color(0xFF3A2A6E), Color(0xFF15193F), AppColors.lapisInk], - ), - borderRadius: BorderRadius.circular(18), - border: Border.all(color: AppColors.gold, width: 2.4), - boxShadow: [ - BoxShadow( - color: AppColors.gold.withValues(alpha: 0.32), blurRadius: 16), - const BoxShadow( - color: Colors.black54, blurRadius: 10, offset: Offset(0, 5)), - ], - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Container( - padding: const EdgeInsets.all(9), - decoration: const BoxDecoration( - gradient: LinearGradient( - colors: [AppColors.gold, AppColors.goldDark]), - shape: BoxShape.circle, - ), - child: const Icon(Icons.workspace_premium, - color: AppColors.lapisInk, size: 24), - ), - const SizedBox(width: 10), - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row(children: [ - Flexible( - child: Text(tier.title, - maxLines: 1, - overflow: TextOverflow.ellipsis, - style: const TextStyle( - color: AppColors.gold, - fontSize: 21, - fontWeight: FontWeight.bold, - shadows: [ - Shadow(color: Colors.black54, blurRadius: 3) - ], - )), - ), - const SizedBox(width: 8), - Container( - padding: const EdgeInsets.symmetric( - horizontal: 7, vertical: 2), - decoration: BoxDecoration( - color: AppColors.accent, - borderRadius: BorderRadius.circular(6), - border: Border.all( - color: Colors.white.withValues(alpha: 0.3)), - ), - child: const Text('ویژه', - style: TextStyle( - color: Colors.white, - fontSize: 10, - fontWeight: FontWeight.bold)), - ), - ]), - const SizedBox(height: 2), - const Text('میزِ قهرمانان', - style: - TextStyle(color: Colors.white70, fontSize: 12)), - ], - ), - ), - Container( - padding: - const EdgeInsets.symmetric(horizontal: 10, vertical: 6), - decoration: BoxDecoration( - color: AppColors.gold.withValues(alpha: 0.14), - borderRadius: BorderRadius.circular(10), - border: Border.all(color: AppColors.gold), - ), - child: Column(children: [ - Row(mainAxisSize: MainAxisSize.min, children: [ - const Icon(Icons.military_tech, - color: AppColors.gold, size: 16), - const SizedBox(width: 3), - Text('+${tier.rankReward}', - style: const TextStyle( - color: AppColors.gold, - fontSize: 16, - fontWeight: FontWeight.bold)), - ]), - const Text('امتیاز رتبه', - style: TextStyle(color: Colors.white70, fontSize: 10)), - ]), - ), + if (tier.rankReward > 0) ...[ + const SizedBox(height: 6), + _badge(Icons.military_tech, '+${tier.rankReward} رتبه'), ], - ), - const SizedBox(height: 12), - Container( - width: double.infinity, - padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 10), - decoration: BoxDecoration( - color: Colors.black.withValues(alpha: 0.25), - borderRadius: BorderRadius.circular(10), - border: Border.all(color: AppColors.goldFaint), - ), - child: const Text( - 'با هر برد در این میز امتیازِ رتبه می‌گیرید و در جدولِ قهرمانان بالا می‌روید. ' - 'باخت امتیاز کم می‌کند — رقابتِ واقعی برای حرفه‌ای‌ها!', - style: TextStyle( - color: Colors.white, fontSize: 12.5, height: 1.6), - ), - ), - const SizedBox(height: 12), - Row(children: [ - _chip(Icons.style, '${tier.hands} دست'), - const SizedBox(width: 8), - _chip(Icons.login, 'ورودی ${tier.entry}'), - const SizedBox(width: 8), - _chip(Icons.star, 'XP ${tier.xp}'), ]), ], ), @@ -279,21 +174,6 @@ class _TierCard extends StatelessWidget { ); } - Widget _chip(IconData icon, String text) => Container( - padding: const EdgeInsets.symmetric(horizontal: 9, vertical: 5), - decoration: BoxDecoration( - color: Colors.white.withValues(alpha: 0.08), - borderRadius: BorderRadius.circular(8), - border: Border.all(color: Colors.white.withValues(alpha: 0.14)), - ), - child: Row(mainAxisSize: MainAxisSize.min, children: [ - Icon(icon, size: 14, color: AppColors.gold), - const SizedBox(width: 5), - Text(text, - style: const TextStyle(color: Colors.white, fontSize: 12)), - ]), - ); - Widget _stat(IconData icon, String label, int value) => Padding( padding: const EdgeInsets.symmetric(vertical: 2), child: Row(mainAxisSize: MainAxisSize.min, children: [ diff --git a/lib/feature/tournament/data/data_source/remote/tournament_api_provider.dart b/lib/feature/tournament/data/data_source/remote/tournament_api_provider.dart new file mode 100644 index 0000000..e47f52c --- /dev/null +++ b/lib/feature/tournament/data/data_source/remote/tournament_api_provider.dart @@ -0,0 +1,48 @@ +import '../../../../../core/locator/locator.dart'; +import '../../../../../core/network/api_provider_imp.dart'; +import '../../../domain/entities/tournament.dart'; + +/// دادهٔ تورنومنت‌ها از backend (فهرست، رده‌بندی، ثبت‌نام). +class TournamentApiProvider { + ApiProviderImp get _api => locator(); + + Future> getTournaments() async { + final res = await _api.get('/tournaments'); + if (res.statusCode != 200) { + throw Exception('tournaments: ${res.statusCode}'); + } + final list = (res.data['tournaments'] as List?) ?? const []; + return list + .map((e) => Tournament.fromJson(Map.from(e as Map))) + .toList(); + } + + Future> getStandings(int id) async { + final res = await _api.get('/tournaments/standings', query: {'id': '$id'}); + if (res.statusCode != 200) { + throw Exception('standings: ${res.statusCode}'); + } + final list = (res.data['standings'] as List?) ?? const []; + return list + .map((e) => + TournamentStanding.fromJson(Map.from(e as Map))) + .toList(); + } + + /// ثبت‌نام؛ null یعنی موفق، وگرنه پیامِ خطای فارسی. + Future join(int id) async { + final res = await _api.post('/tournaments/join', body: {'id': id}); + if (res.statusCode == 200) return null; + final msg = (res.data is Map) ? res.data['message']?.toString() : null; + switch (res.statusCode) { + case 402: + return 'سکهٔ کافی برای ثبت‌نام ندارید'; + case 409: + return msg ?? 'امکان ثبت‌نام نیست'; + case 404: + return 'تورنومنت یافت نشد'; + default: + return msg ?? 'ثبت‌نام ناموفق بود'; + } + } +} diff --git a/lib/feature/tournament/domain/entities/tournament.dart b/lib/feature/tournament/domain/entities/tournament.dart new file mode 100644 index 0000000..bfb4334 --- /dev/null +++ b/lib/feature/tournament/domain/entities/tournament.dart @@ -0,0 +1,99 @@ +/// یک تورنومنتِ امتیازی (از GET /api/tournaments). +class Tournament { + final int id; + final String title; + final String description; + final int entryFee; + final List prizes; + final int prizePool; + final String status; // upcoming | active | ended + final String startsAt; + final String endsAt; + final String startsLocal; // به وقتِ تهران (نمایشی) + final String endsLocal; + final int players; + final bool joined; + final int myPoints; + final int myRank; // ۰ یعنی رتبه‌ای ندارد + + const Tournament({ + required this.id, + required this.title, + required this.description, + required this.entryFee, + required this.prizes, + required this.prizePool, + required this.status, + required this.startsAt, + required this.endsAt, + required this.startsLocal, + required this.endsLocal, + required this.players, + required this.joined, + required this.myPoints, + required this.myRank, + }); + + bool get isActive => status == 'active'; + bool get isUpcoming => status == 'upcoming'; + bool get isEnded => status == 'ended'; + bool get canJoin => !joined && !isEnded; + + factory Tournament.fromJson(Map j) => Tournament( + id: (j['id'] ?? 0) as int, + title: (j['title'] ?? '') as String, + description: (j['description'] ?? '') as String, + entryFee: (j['entry_fee'] ?? 0) as int, + prizes: ((j['prizes'] as List?) ?? const []) + .map((e) => (e ?? 0) as int) + .toList(), + prizePool: (j['prize_pool'] ?? 0) as int, + status: (j['status'] ?? 'ended') as String, + startsAt: (j['starts_at'] ?? '') as String, + endsAt: (j['ends_at'] ?? '') as String, + startsLocal: (j['starts_local'] ?? '') as String, + endsLocal: (j['ends_local'] ?? '') as String, + players: (j['players'] ?? 0) as int, + joined: (j['joined'] ?? false) as bool, + myPoints: (j['my_points'] ?? 0) as int, + myRank: (j['my_rank'] ?? 0) as int, + ); +} + +/// یک ردیفِ جدولِ رده‌بندیِ تورنومنت. +class TournamentStanding { + final int rank; + final int userId; + final String name; + final String avatar; + final int points; + final int wins; + final int games; + final int prize; + + const TournamentStanding({ + required this.rank, + required this.userId, + required this.name, + required this.avatar, + required this.points, + required this.wins, + required this.games, + required this.prize, + }); + + /// seedِ آواتار: آواتارِ انتخابی، وگرنه نام. + String get avatarSeed => avatar.isNotEmpty ? avatar : name; + + factory TournamentStanding.fromJson(Map j) => + TournamentStanding( + rank: (j['rank'] ?? 0) as int, + userId: (j['user_id'] ?? 0) as int, + name: (j['name'] ?? '') as String, + avatar: (j['avatar'] ?? '') as String, + points: (j['points'] ?? 0) as int, + wins: (j['wins'] ?? 0) as int, + games: (j['games'] ?? 0) as int, + prize: (j['prize'] ?? 0) as int, + ); +} diff --git a/lib/feature/tournament/presentation/screen/tournament_standings_screen.dart b/lib/feature/tournament/presentation/screen/tournament_standings_screen.dart new file mode 100644 index 0000000..ed211fc --- /dev/null +++ b/lib/feature/tournament/presentation/screen/tournament_standings_screen.dart @@ -0,0 +1,203 @@ +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:random_avatar/random_avatar.dart'; + +import '../../../../core/locator/locator.dart'; +import '../../../../core/theme/app_theme.dart'; +import '../../../../core/widgets/game_ui.dart'; +import '../../data/data_source/remote/tournament_api_provider.dart'; +import '../../domain/entities/tournament.dart'; + +/// جدولِ رده‌بندیِ یک تورنومنت + خلاصه‌ی جوایز. +class TournamentStandingsScreen extends StatefulWidget { + final Tournament tournament; + const TournamentStandingsScreen({super.key, required this.tournament}); + + @override + State createState() => + _TournamentStandingsScreenState(); +} + +class _TournamentStandingsScreenState extends State { + final _api = locator(); + late Future> _future; + + @override + void initState() { + super.initState(); + _future = _api.getStandings(widget.tournament.id); + } + + @override + Widget build(BuildContext context) { + final t = widget.tournament; + return Scaffold( + body: GameBackground( + child: SafeArea( + child: Column( + children: [ + Padding( + padding: const EdgeInsets.all(12), + child: Row(children: [ + const AppBackButton(), + const Spacer(), + Flexible(child: GlowText(t.title, size: 20)), + const Spacer(), + const SizedBox(width: 48), + ]), + ), + _prizeBar(t), + Expanded( + child: FutureBuilder>( + future: _future, + builder: (context, snap) { + if (snap.connectionState != ConnectionState.done) { + return const Center( + child: + CircularProgressIndicator(color: AppColors.gold)); + } + final rows = snap.data ?? const []; + if (rows.isEmpty) { + return const Center( + child: Text('هنوز کسی امتیازی نگرفته است', + style: TextStyle(color: Colors.white54))); + } + return ListView.separated( + padding: const EdgeInsets.fromLTRB(14, 8, 14, 24), + itemCount: rows.length, + separatorBuilder: (_, __) => const SizedBox(height: 8), + itemBuilder: (_, i) => _row(rows[i]), + ); + }, + ), + ), + // اگر تورنومنت فعال و کاربر ثبت‌نام کرده ⇒ راهِ مستقیم به بازی برای + // کسبِ امتیاز (تا کاربر رابطه‌ی «بازی ⇒ امتیازِ تورنومنت» را حس کند). + if (t.isActive && t.joined) + Padding( + padding: const EdgeInsets.fromLTRB(14, 4, 14, 12), + child: GameButton( + label: 'بازی کن و امتیاز بگیر', + icon: Icons.sports_esports, + width: double.infinity, + colors: const [AppColors.gold, AppColors.goldDark], + onTap: () { + Navigator.of(context).pop(); // به لیستِ تورنومنت‌ها + context.push('/game/tiers'); + }, + ), + ), + ], + ), + ), + ), + ); + } + + Widget _prizeBar(Tournament t) { + return Container( + margin: const EdgeInsets.symmetric(horizontal: 14), + padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10), + decoration: BoxDecoration( + color: AppColors.gold.withValues(alpha: 0.12), + borderRadius: BorderRadius.circular(12), + border: Border.all(color: AppColors.goldFaint), + ), + child: Row(children: [ + const Icon(Icons.emoji_events, color: AppColors.gold, size: 20), + const SizedBox(width: 8), + Text('جایزه کل: ${t.prizePool} سکه', + style: const TextStyle( + color: AppColors.gold, fontWeight: FontWeight.bold)), + const Spacer(), + if (t.prizes.isNotEmpty) + Text( + t.prizes + .asMap() + .entries + .map((e) => '${e.key + 1}: ${e.value}') + .join(' • '), + style: const TextStyle(color: Colors.white60, fontSize: 11), + ), + ]), + ); + } + + Widget _row(TournamentStanding s) { + final medal = s.rank <= 3; + final medalColor = switch (s.rank) { + 1 => const Color(0xFFFFD700), + 2 => const Color(0xFFC0C0C0), + 3 => const Color(0xFFCD7F32), + _ => Colors.transparent, + }; + return Container( + padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8), + decoration: BoxDecoration( + gradient: const LinearGradient( + colors: [AppColors.lapisHi, AppColors.lapisLo], + ), + borderRadius: BorderRadius.circular(12), + border: Border.all( + color: medal ? medalColor : AppColors.goldFaint, + width: medal ? 1.6 : 1, + ), + ), + child: Row(children: [ + SizedBox( + width: 30, + child: medal + ? Icon(Icons.emoji_events, color: medalColor, size: 22) + : Text('${s.rank}', + textAlign: TextAlign.center, + style: const TextStyle( + color: Colors.white70, fontWeight: FontWeight.bold)), + ), + const SizedBox(width: 6), + ClipOval( + child: SizedBox( + width: 38, + height: 38, + child: RandomAvatar(s.avatarSeed), + ), + ), + const SizedBox(width: 10), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(s.name, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + color: Colors.white, fontWeight: FontWeight.bold)), + Text('${s.wins} برد • ${s.games} بازی', + style: const TextStyle(color: Colors.white54, fontSize: 11)), + ], + ), + ), + if (s.prize > 0) ...[ + Row(mainAxisSize: MainAxisSize.min, children: [ + const Icon(Icons.monetization_on, color: AppColors.gold, size: 15), + const SizedBox(width: 3), + Text('${s.prize}', + style: const TextStyle( + color: AppColors.gold, + fontSize: 12, + fontWeight: FontWeight.bold)), + ]), + const SizedBox(width: 10), + ], + Column(children: [ + Text('${s.points}', + style: const TextStyle( + color: AppColors.gold, + fontSize: 16, + fontWeight: FontWeight.bold)), + const Text('امتیاز', + style: TextStyle(color: Colors.white54, fontSize: 10)), + ]), + ]), + ); + } +} diff --git a/lib/feature/tournament/presentation/screen/tournaments_screen.dart b/lib/feature/tournament/presentation/screen/tournaments_screen.dart new file mode 100644 index 0000000..3d2c274 --- /dev/null +++ b/lib/feature/tournament/presentation/screen/tournaments_screen.dart @@ -0,0 +1,392 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:go_router/go_router.dart'; + +import '../../../../core/locator/locator.dart'; +import '../../../../core/theme/app_theme.dart'; +import '../../../../core/widgets/game_ui.dart'; +import '../../../wallet/presentation/bloc/wallet_bloc.dart'; +import '../../../wallet/presentation/bloc/wallet_event.dart'; +import '../../../wallet/presentation/bloc/wallet_state.dart'; +import '../../../wallet/presentation/bloc/wallet_status.dart'; +import '../../data/data_source/remote/tournament_api_provider.dart'; +import '../../domain/entities/tournament.dart'; +import '../widgets/tournament_guide.dart'; +import 'tournament_standings_screen.dart'; + +/// فهرستِ تورنومنت‌ها: جایزه، ورودی، وضعیت و دکمه‌ی ثبت‌نام / جدول. +class TournamentsScreen extends StatefulWidget { + const TournamentsScreen({super.key}); + + @override + State createState() => _TournamentsScreenState(); +} + +class _TournamentsScreenState extends State { + final _api = locator(); + late Future> _future; + int? _busyId; + + @override + void initState() { + super.initState(); + _future = _api.getTournaments(); + } + + void _reload() => setState(() { + _future = _api.getTournaments(); + }); + + Future _join(Tournament t) async { + setState(() => _busyId = t.id); + final err = await _api.join(t.id); + if (!mounted) return; + setState(() => _busyId = null); + if (err == null) { + context.read().add(LoadWalletEvent()); + ScaffoldMessenger.of(context).showSnackBar( + const SnackBar(content: Text('ثبت‌نام انجام شد ✓')), + ); + _reload(); + } else { + ScaffoldMessenger.of(context).showSnackBar(SnackBar(content: Text(err))); + } + } + + void _openStandings(Tournament t) { + Navigator.of(context).push(MaterialPageRoute( + builder: (_) => TournamentStandingsScreen(tournament: t), + )); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: GameBackground( + child: SafeArea( + child: Column( + children: [ + Padding( + padding: const EdgeInsets.all(12), + child: Row(children: [ + const AppBackButton(), + const Spacer(), + const GlowText('تورنومنت‌ها', size: 24), + const Spacer(), + IconButton( + tooltip: 'راهنما', + onPressed: () => TournamentGuide.show(context), + icon: const Icon(Icons.help_outline, color: AppColors.gold), + ), + BlocBuilder( + builder: (context, s) { + final w = s.walletStatus is WalletLoaded + ? (s.walletStatus as WalletLoaded).wallet + : null; + return StatChip( + icon: Icons.monetization_on, + value: '${w?.coins ?? 0}', + ); + }, + ), + ]), + ), + Expanded( + child: RefreshIndicator( + color: AppColors.gold, + onRefresh: () async => _reload(), + child: FutureBuilder>( + future: _future, + builder: (context, snap) { + if (snap.connectionState != ConnectionState.done) { + return const Center( + child: + CircularProgressIndicator(color: AppColors.gold)); + } + if (snap.hasError) { + return _retry(); + } + final items = snap.data ?? const []; + if (items.isEmpty) { + return _empty(); + } + return ListView.separated( + padding: const EdgeInsets.fromLTRB(16, 4, 16, 24), + itemCount: items.length, + separatorBuilder: (_, __) => const SizedBox(height: 14), + itemBuilder: (_, i) => _card(items[i]), + ); + }, + ), + ), + ), + ], + ), + ), + ), + ); + } + + Widget _retry() => ListView(children: [ + const SizedBox(height: 120), + const Center( + child: Text('خطا در دریافت تورنومنت‌ها', + style: TextStyle(color: Colors.white70))), + const SizedBox(height: 8), + Center( + child: TextButton( + onPressed: _reload, + child: const Text('تلاش مجدد', + style: TextStyle(color: AppColors.gold)), + ), + ), + ]); + + Widget _empty() => ListView(children: const [ + SizedBox(height: 140), + Icon(Icons.emoji_events_outlined, color: Colors.white24, size: 60), + SizedBox(height: 12), + Center( + child: Text('در حال حاضر تورنومنتی برگزار نمی‌شود', + style: TextStyle(color: Colors.white54))), + ]); + + Widget _card(Tournament t) { + final busy = _busyId == t.id; + return GestureDetector( + onTap: () => _openStandings(t), + child: Container( + padding: const EdgeInsets.all(14), + decoration: BoxDecoration( + gradient: const LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [Color(0xFF3A2A6E), Color(0xFF15193F), AppColors.lapisInk], + ), + borderRadius: BorderRadius.circular(18), + border: Border.all(color: AppColors.gold, width: 1.6), + boxShadow: [ + BoxShadow( + color: AppColors.gold.withValues(alpha: 0.18), blurRadius: 12), + ], + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row(children: [ + Container( + padding: const EdgeInsets.all(9), + decoration: const BoxDecoration( + gradient: + LinearGradient(colors: [AppColors.gold, AppColors.goldDark]), + shape: BoxShape.circle, + ), + child: const Icon(Icons.emoji_events, + color: AppColors.lapisInk, size: 22), + ), + const SizedBox(width: 10), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(t.title, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + color: AppColors.gold, + fontSize: 18, + fontWeight: FontWeight.bold)), + if (t.description.isNotEmpty) + Text(t.description, + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + color: Colors.white60, fontSize: 12)), + ], + ), + ), + _statusBadge(t.status), + ]), + const SizedBox(height: 12), + Row(children: [ + _info(Icons.emoji_events, 'جایزه کل', '${t.prizePool}', + gold: true), + const SizedBox(width: 8), + _info(Icons.login, 'ورودی', + t.entryFee == 0 ? 'رایگان' : '${t.entryFee}'), + const SizedBox(width: 8), + _info(Icons.people, 'شرکت‌کننده', '${t.players}'), + ]), + if (t.startsLocal.isNotEmpty) ...[ + const SizedBox(height: 8), + Row(children: [ + const Icon(Icons.schedule, color: Colors.white38, size: 13), + const SizedBox(width: 5), + Text( + t.isUpcoming + ? 'شروع: ${t.startsLocal}' + : 'پایان: ${t.endsLocal}', + style: const TextStyle(color: Colors.white54, fontSize: 11), + ), + ]), + ], + const SizedBox(height: 12), + _action(t, busy), + ], + ), + ), + ); + } + + Widget _statusBadge(String status) { + late Color c; + late String label; + switch (status) { + case 'active': + c = AppColors.success; + label = 'در حال برگزاری'; + break; + case 'upcoming': + c = const Color(0xFFE9952F); + label = 'به‌زودی'; + break; + default: + c = Colors.white38; + label = 'پایان‌یافته'; + } + return Container( + padding: const EdgeInsets.symmetric(horizontal: 9, vertical: 4), + decoration: BoxDecoration( + color: c.withValues(alpha: 0.18), + borderRadius: BorderRadius.circular(8), + border: Border.all(color: c), + ), + child: Text(label, + style: + TextStyle(color: c, fontSize: 11, fontWeight: FontWeight.bold)), + ); + } + + Widget _info(IconData icon, String label, String value, {bool gold = false}) { + return Expanded( + child: Container( + padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 6), + decoration: BoxDecoration( + color: Colors.white.withValues(alpha: 0.05), + borderRadius: BorderRadius.circular(10), + border: Border.all(color: Colors.white.withValues(alpha: 0.1)), + ), + child: Column(children: [ + Icon(icon, size: 16, color: gold ? AppColors.gold : Colors.white70), + const SizedBox(height: 3), + Text(value, + style: TextStyle( + color: gold ? AppColors.gold : Colors.white, + fontSize: 14, + fontWeight: FontWeight.bold)), + Text(label, + style: const TextStyle(color: Colors.white54, fontSize: 10)), + ]), + ), + ); + } + + Widget _action(Tournament t, bool busy) { + if (t.joined) { + return Column(children: [ + Container( + width: double.infinity, + padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 10), + alignment: Alignment.center, + decoration: BoxDecoration( + color: AppColors.success.withValues(alpha: 0.15), + borderRadius: BorderRadius.circular(12), + border: Border.all(color: AppColors.success), + ), + child: Text( + t.myRank > 0 + ? 'رتبه شما: ${t.myRank} • امتیاز: ${t.myPoints}' + : 'ثبت‌نام شده • امتیاز: ${t.myPoints}', + style: const TextStyle( + color: Colors.white, fontWeight: FontWeight.bold, fontSize: 13), + ), + ), + const SizedBox(height: 8), + Row(children: [ + if (t.isActive) + Expanded( + child: GameButton( + label: 'بازی کن و امتیاز بگیر', + icon: Icons.sports_esports, + colors: const [AppColors.gold, AppColors.goldDark], + onTap: () async { + await context.push('/game/tiers'); + if (context.mounted) _reload(); + }, + ), + ) + else + Expanded( + child: OutlinedButton.icon( + style: OutlinedButton.styleFrom( + side: const BorderSide(color: AppColors.gold), + padding: const EdgeInsets.symmetric(vertical: 10), + ), + onPressed: () => _openStandings(t), + icon: const Icon(Icons.leaderboard, + color: AppColors.gold, size: 18), + label: const Text('جدول نتایج', + style: TextStyle(color: AppColors.gold)), + ), + ), + const SizedBox(width: 8), + _tableButton(t), + ]), + ]); + } + if (t.canJoin) { + return Row(children: [ + Expanded( + child: GameButton( + label: busy + ? '...' + : (t.entryFee == 0 + ? 'ثبت‌نام رایگان' + : 'ثبت‌نام (${t.entryFee} سکه)'), + icon: Icons.how_to_reg, + colors: const [AppColors.success, AppColors.successDark], + onTap: busy ? null : () => _join(t), + ), + ), + const SizedBox(width: 8), + _tableButton(t), + ]); + } + // پایان‌یافته و ثبت‌نام‌نشده ⇒ فقط جدول. + return SizedBox( + width: double.infinity, + child: OutlinedButton.icon( + style: OutlinedButton.styleFrom( + side: const BorderSide(color: AppColors.gold), + padding: const EdgeInsets.symmetric(vertical: 10), + ), + onPressed: () => _openStandings(t), + icon: const Icon(Icons.leaderboard, color: AppColors.gold, size: 18), + label: const Text('جدول نتایج', + style: TextStyle(color: AppColors.gold)), + ), + ); + } + + Widget _tableButton(Tournament t) => Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(12), + border: Border.all(color: AppColors.gold), + ), + child: IconButton( + tooltip: 'جدول', + onPressed: () => _openStandings(t), + icon: const Icon(Icons.leaderboard, color: AppColors.gold), + ), + ); +} diff --git a/lib/feature/tournament/presentation/widgets/tournament_guide.dart b/lib/feature/tournament/presentation/widgets/tournament_guide.dart new file mode 100644 index 0000000..2d94afd --- /dev/null +++ b/lib/feature/tournament/presentation/widgets/tournament_guide.dart @@ -0,0 +1,218 @@ +import 'package:flutter/material.dart'; + +import '../../../../core/theme/app_theme.dart'; + +/// راهنمای تورنومنت: توضیحِ گام‌به‌گامِ نحوه‌ی کار (ثبت‌نام، کسبِ امتیاز، جایزه). +class TournamentGuide { + static const _steps = <(IconData, String, String)>[ + ( + Icons.how_to_reg, + '۱. ثبت‌نام کن', + 'با پرداختِ ورودی (سکه) در تورنومنت ثبت‌نام کن. ثبت‌نام تا پیش از پایانِ ' + 'تورنومنت باز است.', + ), + ( + Icons.sports_esports, + '۲. بازی کن و امتیاز بگیر', + 'در بازه‌ی زمانیِ تورنومنت، عادی بازی کن. هر بازی که ببری ۱۰۰ امتیاز و هر ' + 'بازی که شرکت کنی ۲۵ امتیاز می‌گیری. هرچه بیشتر ببری، امتیازت بیشتر می‌شود.', + ), + ( + Icons.leaderboard, + '۳. در جدول بالا برو', + 'امتیازِ همه در جدولِ رده‌بندی ثبت می‌شود؛ بیشترین امتیاز بالاتر می‌ایستد. ' + 'در تساوی، تعدادِ بردِ بیشتر و ثبت‌نامِ زودتر ملاک است.', + ), + ( + Icons.emoji_events, + '۴. جایزه بگیر', + 'وقتی تورنومنت تمام شد، نفراتِ برتر به‌ترتیبِ رتبه جایزه‌ی سکه می‌گیرند. ' + 'جوایز به‌صورتِ خودکار به کیف‌پولت اضافه می‌شود.', + ), + ]; + + static Future show(BuildContext context) { + return showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: Colors.transparent, + builder: (_) => const _GuideSheet(), + ); + } +} + +class _GuideSheet extends StatelessWidget { + const _GuideSheet(); + + @override + Widget build(BuildContext context) { + return SafeArea( + top: false, + child: DraggableScrollableSheet( + initialChildSize: 0.72, + minChildSize: 0.4, + maxChildSize: 0.92, + expand: false, + builder: + (context, controller) => Container( + decoration: BoxDecoration( + gradient: const LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [AppColors.lapisMid, AppColors.lapisLo], + ), + borderRadius: const BorderRadius.vertical( + top: Radius.circular(24), + ), + border: Border.all(color: AppColors.gold, width: 1.6), + ), + child: ListView( + controller: controller, + padding: const EdgeInsets.fromLTRB(18, 12, 18, 28), + children: [ + Center( + child: Container( + width: 44, + height: 4, + margin: const EdgeInsets.only(bottom: 14), + decoration: BoxDecoration( + color: Colors.white24, + borderRadius: BorderRadius.circular(2), + ), + ), + ), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: const [ + Icon(Icons.emoji_events, color: AppColors.gold, size: 24), + SizedBox(width: 8), + Text( + 'راهنمای تورنومنت', + style: TextStyle( + color: AppColors.gold, + fontSize: 19, + fontWeight: FontWeight.bold, + ), + ), + ], + ), + const SizedBox(height: 6), + const Text( + 'تورنومنتِ امتیازی؛ بیشتر ببر، بالاتر بایست، جایزه بگیر!', + textAlign: TextAlign.center, + style: TextStyle(color: Colors.white60, fontSize: 12.5), + ), + const SizedBox(height: 18), + for (final (icon, title, body) in TournamentGuide._steps) + _stepTile(icon, title, body), + const SizedBox(height: 10), + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: AppColors.gold.withValues(alpha: 0.1), + borderRadius: BorderRadius.circular(12), + border: Border.all(color: AppColors.goldFaint), + ), + child: const Row( + children: [ + Icon( + Icons.lightbulb_outline, + color: AppColors.gold, + size: 18, + ), + SizedBox(width: 8), + Expanded( + child: Text( + 'نکته: می‌توانی در هر لحظه ثبت‌نام کنی، اما هرچه زودتر شروع ' + 'کنی فرصتِ بیشتری برای کسبِ امتیاز داری.', + style: TextStyle( + color: Colors.white, + fontSize: 12.5, + height: 1.7, + ), + ), + ), + ], + ), + ), + const SizedBox(height: 16), + SizedBox( + width: double.infinity, + child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: AppColors.gold, + padding: const EdgeInsets.symmetric(vertical: 12), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(14), + ), + ), + onPressed: () => Navigator.pop(context), + child: const Text( + 'فهمیدم', + style: TextStyle( + color: AppColors.lapisInk, + fontWeight: FontWeight.bold, + fontSize: 15, + ), + ), + ), + ), + ], + ), + ), + ), + ); + } + + Widget _stepTile(IconData icon, String title, String body) { + return Container( + margin: const EdgeInsets.only(bottom: 12), + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: Colors.white.withValues(alpha: 0.05), + borderRadius: BorderRadius.circular(14), + border: Border.all(color: Colors.white.withValues(alpha: 0.1)), + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + padding: const EdgeInsets.all(9), + decoration: const BoxDecoration( + gradient: LinearGradient( + colors: [AppColors.gold, AppColors.goldDark], + ), + shape: BoxShape.circle, + ), + child: Icon(icon, color: AppColors.lapisInk, size: 20), + ), + const SizedBox(width: 12), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + title, + style: const TextStyle( + color: AppColors.gold, + fontSize: 14.5, + fontWeight: FontWeight.bold, + ), + ), + const SizedBox(height: 4), + Text( + body, + style: const TextStyle( + color: Colors.white, + fontSize: 12.5, + height: 1.8, + ), + ), + ], + ), + ), + ], + ), + ); + } +} diff --git a/lib/feature/wallet/presentation/screen/lobby_screen.dart b/lib/feature/wallet/presentation/screen/lobby_screen.dart index 6c13504..00f4ce8 100644 --- a/lib/feature/wallet/presentation/screen/lobby_screen.dart +++ b/lib/feature/wallet/presentation/screen/lobby_screen.dart @@ -52,99 +52,96 @@ class _LobbyScreenState extends State { builder: (context, state) { final st = state.walletStatus; final wallet = st is WalletLoaded ? st.wallet : null; - return Column( - children: [ - _TopBar(wallet: wallet, onCoinTap: () => _openShop(context)), - Expanded( - child: Center( + return SafeArea( + child: Column( + children: [ + _TopBar(wallet: wallet, onCoinTap: () => _openShop(context)), + Expanded( child: SingleChildScrollView( - padding: const EdgeInsets.symmetric(horizontal: 28), + padding: const EdgeInsets.fromLTRB(16, 6, 16, 8), child: Column( - mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - const GlowText('حاکم ایرانی', size: 44), - const SizedBox(height: 44), - GameButton( - label: 'بازی', - icon: Icons.style, - width: double.infinity, - colors: const [ - Color(0xFFC2185B), - Color(0xFF6A0D38), - ], + const SizedBox(height: 6), + const _LobbyHeader(), + const SizedBox(height: 18), + // اکشنِ اصلی: بازیِ سریع. + _PrimaryPlay( onTap: () async { await context.push('/game/tiers'); if (context.mounted) _reload(); }, ), - const SizedBox(height: 16), - GameButton( - label: 'دورهمی', - icon: Icons.group_add, - width: double.infinity, - colors: const [ - Color(0xFF1565C0), - Color(0xFF0A2E57), + const SizedBox(height: 14), + // اکشن‌های ثانویه در گریدِ دوستونه. + GridView.count( + crossAxisCount: 2, + shrinkWrap: true, + physics: const NeverScrollableScrollPhysics(), + mainAxisSpacing: 12, + crossAxisSpacing: 12, + childAspectRatio: 1.55, + children: [ + _ActionTile( + icon: Icons.group_add, + label: 'دورهمی', + sub: 'بازی با دوستان', + accent: AppColors.lapis, + onTap: () async { + await context.push('/private'); + if (context.mounted) _reload(); + }, + ), + _ActionTile( + icon: Icons.emoji_events, + label: 'تورنومنت', + sub: 'جایزه بگیر', + accent: const Color(0xFF9B7BE0), + onTap: () async { + await context.push('/tournaments'); + if (context.mounted) _reload(); + }, + ), + _ActionTile( + icon: Icons.leaderboard, + label: 'رتبه‌بندی', + sub: 'جدول قهرمانان', + accent: AppColors.gold, + onTap: () => context.push('/leaderboard'), + ), + _ActionTile( + icon: Icons.storefront, + label: 'فروشگاه', + sub: 'سکه و آیتم', + accent: AppColors.success, + onTap: () => _openShop(context), + ), ], - onTap: () async { - await context.push('/private'); - if (context.mounted) _reload(); - }, ), - const SizedBox(height: 16), - GameButton( - label: 'رتبه‌بندی', - icon: Icons.leaderboard, - width: double.infinity, - colors: const [ - Color(0xFFE9952F), - Color(0xFF9C5A00), - ], - onTap: () => context.push('/leaderboard'), - ), - const SizedBox(height: 16), - GameButton( - label: 'فروشگاه', - icon: Icons.storefront, - width: double.infinity, - colors: const [ - Color(0xFF7B1FA2), - Color(0xFF3E0A57), - ], - onTap: () => _openShop(context), - ), - const SizedBox(height: 16), - GameButton( - label: 'سکه روزانه', - icon: Icons.monetization_on, - width: double.infinity, - colors: const [ - AppColors.success, - AppColors.successDark, - ], - onTap: - () => context.read().add( - ClaimDailyEvent(), - ), + const SizedBox(height: 14), + _DailyCard( + onTap: () => context + .read() + .add(ClaimDailyEvent()), ), ], ), ), ), - ), - TextButton.icon( - onPressed: () { - context.read().add(LogoutEvent()); - context.go('/login'); - }, - icon: const Icon(Icons.logout, color: Colors.white54), - label: const Text( - 'خروج', - style: TextStyle(color: Colors.white54), + TextButton.icon( + onPressed: () { + context.read().add(LogoutEvent()); + context.go('/login'); + }, + icon: const Icon(Icons.logout, + color: Colors.white38, size: 18), + label: const Text( + 'خروج از حساب', + style: TextStyle(color: Colors.white38, fontSize: 13), + ), ), - ), - const SizedBox(height: 8), - ], + ], + ), ); }, ), @@ -158,6 +155,203 @@ class _LobbyScreenState extends State { } } +/// عنوانِ لابی: تاجِ طلایی + نامِ بازی + زیرعنوان. +class _LobbyHeader extends StatelessWidget { + const _LobbyHeader(); + @override + Widget build(BuildContext context) { + return Column( + children: const [ + Icon(Icons.workspace_premium, color: AppColors.gold, size: 34), + SizedBox(height: 4), + GlowText('حاکم ایرانی', size: 36), + SizedBox(height: 2), + Text('بازیِ آنلاینِ حکم', + style: TextStyle(color: Colors.white54, fontSize: 13)), + ], + ); + } +} + +/// اکشنِ اصلی: بازیِ سریع (میزِ عمومی) — کارتِ بزرگِ برجسته. +class _PrimaryPlay extends StatelessWidget { + final VoidCallback onTap; + const _PrimaryPlay({required this.onTap}); + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: onTap, + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 18), + decoration: BoxDecoration( + gradient: const LinearGradient( + begin: Alignment.topLeft, + end: Alignment.bottomRight, + colors: [AppColors.lapis, AppColors.lapisHi, AppColors.lapisLo], + ), + borderRadius: BorderRadius.circular(20), + border: Border.all(color: AppColors.gold, width: 2), + boxShadow: [ + BoxShadow( + color: AppColors.gold.withValues(alpha: 0.28), blurRadius: 16), + const BoxShadow( + color: Colors.black54, blurRadius: 10, offset: Offset(0, 5)), + ], + ), + child: Row( + children: [ + Container( + width: 54, + height: 54, + decoration: const BoxDecoration( + gradient: + LinearGradient(colors: [AppColors.gold, AppColors.goldDark]), + shape: BoxShape.circle, + ), + child: + const Icon(Icons.play_arrow_rounded, color: AppColors.lapisInk, size: 36), + ), + const SizedBox(width: 14), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: const [ + Text('بازی سریع', + style: TextStyle( + color: Colors.white, + fontSize: 22, + fontWeight: FontWeight.bold)), + SizedBox(height: 2), + Text('ورود به میزِ عمومی', + style: TextStyle(color: Colors.white70, fontSize: 13)), + ], + ), + const Spacer(), + const Icon(Icons.arrow_back_ios_new, + color: AppColors.gold, size: 18), + ], + ), + ), + ); + } +} + +/// کاشیِ اکشنِ ثانویه در گرید (آیکن + عنوان + زیرعنوان). +class _ActionTile extends StatelessWidget { + final IconData icon; + final String label; + final String sub; + final Color accent; + final VoidCallback onTap; + const _ActionTile({ + required this.icon, + required this.label, + required this.sub, + required this.accent, + required this.onTap, + }); + + @override + Widget build(BuildContext context) { + return Material( + color: Colors.transparent, + borderRadius: BorderRadius.circular(18), + child: InkWell( + borderRadius: BorderRadius.circular(18), + onTap: onTap, + child: Ink( + padding: const EdgeInsets.all(14), + decoration: BoxDecoration( + gradient: const LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [AppColors.lapisHi, AppColors.lapisLo], + ), + borderRadius: BorderRadius.circular(18), + border: Border.all(color: AppColors.goldDark, width: 1.2), + ), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + padding: const EdgeInsets.all(9), + decoration: BoxDecoration( + color: accent.withValues(alpha: 0.18), + shape: BoxShape.circle, + border: Border.all(color: accent.withValues(alpha: 0.6)), + ), + child: Icon(icon, color: accent, size: 22), + ), + const Spacer(), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Text(label, + style: const TextStyle( + color: Colors.white, + fontSize: 16, + fontWeight: FontWeight.bold)), + const SizedBox(height: 2), + Text(sub, + style: + const TextStyle(color: Colors.white54, fontSize: 11)), + ], + ), + ], + ), + ), + ), + ); + } +} + +/// کارتِ سکه‌ی روزانه (اکشنِ برجسته‌ی سبز-طلایی). +class _DailyCard extends StatelessWidget { + final VoidCallback onTap; + const _DailyCard({required this.onTap}); + @override + Widget build(BuildContext context) { + return Material( + color: Colors.transparent, + borderRadius: BorderRadius.circular(16), + child: InkWell( + borderRadius: BorderRadius.circular(16), + onTap: onTap, + child: Ink( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14), + decoration: BoxDecoration( + gradient: const LinearGradient( + colors: [AppColors.success, AppColors.successDark], + ), + borderRadius: BorderRadius.circular(16), + border: Border.all(color: AppColors.gold, width: 1.4), + ), + child: Row( + children: const [ + Icon(Icons.card_giftcard, color: Colors.white, size: 26), + SizedBox(width: 12), + Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text('هدیه‌ی روزانه', + style: TextStyle( + color: Colors.white, + fontSize: 16, + fontWeight: FontWeight.bold)), + Text('هر روز سکه‌ی رایگان بگیر', + style: TextStyle(color: Colors.white70, fontSize: 12)), + ], + ), + Spacer(), + Icon(Icons.redeem, color: AppColors.gold, size: 22), + ], + ), + ), + ), + ); + } +} + class _TopBar extends StatelessWidget { final WalletEntity? wallet; final VoidCallback onCoinTap;