From e94f03c499b2dc7a8bca43d622c80066edcb109a Mon Sep 17 00:00:00 2001 From: Amirmahdi Nourkazemi Date: Sun, 5 Jul 2026 18:40:53 +0330 Subject: [PATCH] fix: some problems are fixed --- lib/core/locator/locator.dart | 4 +- lib/core/payment/bazaar_payment_gateway.dart | 4 +- lib/core/payment/dev_payment_gateway.dart | 2 +- lib/core/payment/myket_payment_gateway.dart | 8 +- lib/core/payment/payment_gateway.dart | 4 +- lib/core/storage/token_storage.dart | 13 +- .../data_source/local/auth_local_data.dart | 3 + .../data/repository/auth_repository_impl.dart | 1 + .../chat/presentation/widgets/chat_panel.dart | 435 +++++++++++++----- .../game/domain/entities/game_entities.dart | 7 +- .../game/presentation/bloc/game_state.dart | 7 +- .../game/presentation/screen/game_screen.dart | 2 +- .../screen/private_table_screen.dart | 2 +- .../presentation/widgets/flame/hokm_game.dart | 32 +- .../game/presentation/widgets/table_hud.dart | 3 +- .../shop/presentation/bloc/shop_bloc.dart | 9 +- 16 files changed, 384 insertions(+), 152 deletions(-) diff --git a/lib/core/locator/locator.dart b/lib/core/locator/locator.dart index bd80926..fe214b2 100644 --- a/lib/core/locator/locator.dart +++ b/lib/core/locator/locator.dart @@ -114,8 +114,8 @@ Future setupLocator() async { locator.registerFactory( () => AuthBloc(locator(), locator(), locator(), locator())); locator.registerFactory(() => WalletBloc(locator(), locator())); - locator.registerFactory(() => ShopBloc( - locator(), locator(), locator(), locator(), locator(), locator())); + locator.registerFactory(() => ShopBloc(locator(), locator(), + locator(), locator(), locator(), locator(), locator())); locator.registerFactory( () => ProfileBloc(locator(), locator())); locator.registerFactory(() => LeaderboardBloc(locator())); diff --git a/lib/core/payment/bazaar_payment_gateway.dart b/lib/core/payment/bazaar_payment_gateway.dart index 54f2441..2ac19a5 100644 --- a/lib/core/payment/bazaar_payment_gateway.dart +++ b/lib/core/payment/bazaar_payment_gateway.dart @@ -21,12 +21,12 @@ class BazaarPaymentGateway implements PaymentGateway { } @override - Future purchase(String productSku) async { + Future purchase(String productSku, {String payload = ''}) async { try { await connect(); final info = await FlutterPoolakey.purchase( productSku, - payload: 'hakemsho', + payload: payload.isNotEmpty ? payload : 'hakemsho', ); if (info.purchaseToken.isEmpty) { return const PurchaseResult.failure(); diff --git a/lib/core/payment/dev_payment_gateway.dart b/lib/core/payment/dev_payment_gateway.dart index c4f4e3d..a7666a2 100644 --- a/lib/core/payment/dev_payment_gateway.dart +++ b/lib/core/payment/dev_payment_gateway.dart @@ -11,7 +11,7 @@ class DevPaymentGateway implements PaymentGateway { Future connect() async {} @override - Future purchase(String productSku) async { + Future purchase(String productSku, {String payload = ''}) async { final stamp = DateTime.now().millisecondsSinceEpoch; return PurchaseResult.success( token: 'dev-$productSku-$stamp', diff --git a/lib/core/payment/myket_payment_gateway.dart b/lib/core/payment/myket_payment_gateway.dart index fb1436a..cc93678 100644 --- a/lib/core/payment/myket_payment_gateway.dart +++ b/lib/core/payment/myket_payment_gateway.dart @@ -19,10 +19,12 @@ class MyketPaymentGateway implements PaymentGateway { } @override - Future purchase(String productSku) async { + Future purchase(String productSku, {String payload = ''}) async { await connect(); - final Map result = - await MyketIAP.launchPurchaseFlow(sku: productSku, payload: 'hakemsho'); + final Map result = await MyketIAP.launchPurchaseFlow( + sku: productSku, + payload: payload.isNotEmpty ? payload : 'hakemsho', + ); final Purchase? purchase = result[MyketIAP.PURCHASE] as Purchase?; if (purchase == null || purchase.mToken.isEmpty) { diff --git a/lib/core/payment/payment_gateway.dart b/lib/core/payment/payment_gateway.dart index 98e3d40..800faa2 100644 --- a/lib/core/payment/payment_gateway.dart +++ b/lib/core/payment/payment_gateway.dart @@ -33,7 +33,9 @@ abstract class PaymentGateway { String get store; /// جریانِ خریدِ [productSku] (همان UUIDِ محصول در پنلِ فروشگاه) را باز می‌کند. - Future purchase(String productSku); + /// [payload] در فروشگاه ذخیره و در پنلِ توسعه‌دهنده نمایش داده می‌شود؛ اینجا + /// شماره‌ی موبایلِ کاربر را می‌فرستیم تا خرید قابلِ ردیابی باشد. + Future purchase(String productSku, {String payload = ''}); /// اتصالِ اولیه (در صورتِ نیاز). پیش از اولین خرید صدا زده می‌شود. Future connect() async {} diff --git a/lib/core/storage/token_storage.dart b/lib/core/storage/token_storage.dart index 9c4be52..3c98a48 100644 --- a/lib/core/storage/token_storage.dart +++ b/lib/core/storage/token_storage.dart @@ -1,8 +1,9 @@ import 'package:flutter_secure_storage/flutter_secure_storage.dart'; -/// ذخیره‌ی امن توکن JWT. +/// ذخیره‌ی امن توکن JWT و شماره‌ی موبایلِ کاربر. class TokenStorage { static const _key = 'jwt_token'; + static const _mobileKey = 'user_mobile'; final FlutterSecureStorage _storage; TokenStorage([FlutterSecureStorage? storage]) @@ -12,5 +13,13 @@ class TokenStorage { Future write(String token) => _storage.write(key: _key, value: token); - Future clear() => _storage.delete(key: _key); + Future readMobile() => _storage.read(key: _mobileKey); + + Future writeMobile(String mobile) => + _storage.write(key: _mobileKey, value: mobile); + + Future clear() async { + await _storage.delete(key: _key); + await _storage.delete(key: _mobileKey); + } } diff --git a/lib/feature/auth/data/data_source/local/auth_local_data.dart b/lib/feature/auth/data/data_source/local/auth_local_data.dart index 25db814..0251a86 100644 --- a/lib/feature/auth/data/data_source/local/auth_local_data.dart +++ b/lib/feature/auth/data/data_source/local/auth_local_data.dart @@ -9,6 +9,9 @@ class AuthLocalData { Future readToken() => _storage.read(); Future clearToken() => _storage.clear(); + Future saveMobile(String mobile) => _storage.writeMobile(mobile); + Future readMobile() => _storage.readMobile(); + Future hasToken() async { final t = await _storage.read(); return t != null && t.isNotEmpty; diff --git a/lib/feature/auth/data/repository/auth_repository_impl.dart b/lib/feature/auth/data/repository/auth_repository_impl.dart index b6e57bf..14be051 100644 --- a/lib/feature/auth/data/repository/auth_repository_impl.dart +++ b/lib/feature/auth/data/repository/auth_repository_impl.dart @@ -32,6 +32,7 @@ class AuthRepositoryImpl extends AuthRepository { return const DataError('پاسخ نامعتبر از سرور'); } await local.saveToken(token); + await local.saveMobile(params.mobile); // برای payloadِ خریدِ فروشگاهی return DataSuccess(UserModel.fromJson( Map.from(res.data['user'] as Map))); } diff --git a/lib/feature/chat/presentation/widgets/chat_panel.dart b/lib/feature/chat/presentation/widgets/chat_panel.dart index 9094242..cd66441 100644 --- a/lib/feature/chat/presentation/widgets/chat_panel.dart +++ b/lib/feature/chat/presentation/widgets/chat_panel.dart @@ -1,3 +1,5 @@ +import 'dart:ui'; + import 'package:flutter/material.dart'; import '../../../../core/locator/locator.dart'; @@ -5,8 +7,9 @@ import '../../../../core/theme/app_theme.dart'; import '../../data/data_source/remote/chat_api_provider.dart'; import '../../domain/entities/chat_pack.dart'; -/// پنلِ «بسته چت» شبیه تصویرِ مرجع: هدرِ ربانِ طلایی، تب برای هر بسته، و گریدِ -/// پیام‌ها/شکلک‌ها. بسته‌های قفل (نخریده) با دکمه‌ی خرید (سکه/VIP) نشان داده می‌شوند. +/// پنلِ «بسته چت»: قابِ لاجوردیِ طلاکوب، تبِ حبّه‌ایِ هر بسته (با قفلِ بسته‌های +/// نخریده)، و گریدِ پیام/شکلک. بسته‌های قفل با محوکردنِ گرید و کارتِ خرید نمایش +/// داده می‌شوند (سکه/VIP). class ChatPanel extends StatefulWidget { final void Function(String text) onText; final void Function(String emoji) onEmoji; @@ -26,7 +29,7 @@ class ChatPanel extends StatefulWidget { }) { return showDialog( context: context, - barrierColor: Colors.black87, + barrierColor: Colors.black.withValues(alpha: 0.6), builder: (_) => ChatPanel( onText: onText, onEmoji: onEmoji, @@ -83,75 +86,137 @@ class _ChatPanelState extends State { return Dialog( backgroundColor: Colors.transparent, insetPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 40), - child: FutureBuilder>( - future: _future, - builder: (context, snap) { - if (snap.connectionState != ConnectionState.done) { - return const SizedBox( - height: 200, - child: Center( - child: CircularProgressIndicator(color: AppColors.gold)), - ); - } - final packs = snap.data ?? const []; - if (packs.isEmpty) { - return _shell(const Center( - child: Text('بسته‌ای موجود نیست', - style: TextStyle(color: Colors.white70)))); - } - return DefaultTabController( - length: packs.length, - child: _shell( - Column( - mainAxisSize: MainAxisSize.min, - children: [ - TabBar( - isScrollable: true, - tabAlignment: TabAlignment.center, - indicator: BoxDecoration( - color: AppColors.lapis, - borderRadius: BorderRadius.circular(10), - border: Border.all(color: AppColors.gold, width: 1.4), + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 440), + child: FutureBuilder>( + future: _future, + builder: (context, snap) { + if (snap.connectionState != ConnectionState.done) { + return _shell(const SizedBox( + height: 260, + child: Center( + child: CircularProgressIndicator(color: AppColors.gold)), + )); + } + final packs = snap.data ?? const []; + if (packs.isEmpty) { + return _shell(const SizedBox( + height: 200, + child: Center( + child: Text('بسته‌ای موجود نیست', + style: TextStyle(color: Colors.white70))), + )); + } + return DefaultTabController( + length: packs.length, + child: _shell( + Column( + mainAxisSize: MainAxisSize.min, + children: [ + _tabs(packs), + const SizedBox(height: 12), + SizedBox( + height: 320, + child: TabBarView( + children: [for (final p in packs) _packView(p)], + ), ), - indicatorSize: TabBarIndicatorSize.tab, - labelColor: AppColors.gold, - unselectedLabelColor: Colors.white70, - labelStyle: const TextStyle(fontWeight: FontWeight.bold), - dividerColor: Colors.transparent, - tabs: [for (final p in packs) Tab(text: p.title)], - ), - const SizedBox(height: 10), - SizedBox( - height: 320, - child: TabBarView( - children: [for (final p in packs) _packView(p)], - ), - ), - ], + ], + ), ), - ), - ); - }, + ); + }, + ), ), ); } - // قابِ قرمزِ پنل با ربانِ طلاییِ «بسته چت» و دکمه‌ی بستن. + // تبِ حبّه‌ای؛ بسته‌های قفل یک قفلِ کوچک کنارِ عنوان دارند. + Widget _tabs(List packs) { + return SizedBox( + height: 40, + child: TabBar( + isScrollable: true, + tabAlignment: TabAlignment.start, + padding: const EdgeInsets.symmetric(horizontal: 2), + indicator: BoxDecoration( + gradient: const LinearGradient( + colors: [AppColors.lapis, AppColors.lapisHi], + ), + borderRadius: BorderRadius.circular(20), + border: Border.all(color: AppColors.gold, width: 1.3), + boxShadow: [ + BoxShadow( + color: AppColors.gold.withValues(alpha: 0.25), + blurRadius: 8, + ), + ], + ), + indicatorSize: TabBarIndicatorSize.tab, + labelColor: AppColors.gold, + unselectedLabelColor: Colors.white60, + labelStyle: const TextStyle(fontWeight: FontWeight.bold, fontSize: 13), + unselectedLabelStyle: const TextStyle(fontSize: 13), + dividerColor: Colors.transparent, + overlayColor: WidgetStateProperty.all(Colors.transparent), + tabs: [ + for (final p in packs) + Tab( + height: 36, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon( + p.isEmoji + ? Icons.emoji_emotions_outlined + : Icons.chat_bubble_outline, + size: 15, + ), + const SizedBox(width: 5), + Text(p.title), + if (!p.owned) ...[ + const SizedBox(width: 5), + Icon( + p.vip ? Icons.workspace_premium : Icons.lock, + size: 13, + color: AppColors.gold, + ), + ], + ], + ), + ), + ], + ), + ); + } + + // قابِ لاجوردیِ پنل با ربانِ طلاییِ «بسته چت» و دکمه‌ی بستن. Widget _shell(Widget child) { return Stack( clipBehavior: Clip.none, children: [ Container( - margin: const EdgeInsets.only(top: 18), - padding: const EdgeInsets.fromLTRB(12, 30, 12, 14), + margin: const EdgeInsets.only(top: 20), + padding: const EdgeInsets.fromLTRB(14, 34, 14, 16), decoration: BoxDecoration( gradient: const LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, colors: [AppColors.lapisMid, AppColors.lapisLo], ), - borderRadius: BorderRadius.circular(16), - border: Border.all(color: AppColors.gold, width: 2), + borderRadius: BorderRadius.circular(22), + border: Border.all(color: AppColors.gold, width: 1.6), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.5), + blurRadius: 24, + offset: const Offset(0, 12), + ), + BoxShadow( + color: AppColors.gold.withValues(alpha: 0.12), + blurRadius: 20, + ), + ], ), child: child, ), @@ -163,31 +228,57 @@ class _ChatPanelState extends State { child: Center( child: Container( padding: - const EdgeInsets.symmetric(horizontal: 28, vertical: 6), + const EdgeInsets.symmetric(horizontal: 26, vertical: 8), decoration: BoxDecoration( gradient: const LinearGradient( colors: [AppColors.gold, AppColors.goldDark], ), - borderRadius: BorderRadius.circular(12), - border: Border.all(color: Colors.white24), + borderRadius: BorderRadius.circular(14), + border: Border.all(color: Colors.white.withValues(alpha: 0.35)), + boxShadow: [ + BoxShadow( + color: AppColors.goldDark.withValues(alpha: 0.5), + blurRadius: 10, + offset: const Offset(0, 4), + ), + ], + ), + child: const Row( + mainAxisSize: MainAxisSize.min, + children: [ + Icon(Icons.forum_rounded, + color: AppColors.lapisInk, size: 18), + SizedBox(width: 7), + Text('بسته چت', + style: TextStyle( + color: AppColors.lapisInk, + fontSize: 16, + fontWeight: FontWeight.bold)), + ], ), - child: const Text('بسته چت', - style: TextStyle( - color: AppColors.lapisInk, - fontSize: 17, - fontWeight: FontWeight.bold)), ), ), ), Positioned( - top: 14, - right: 6, + top: 12, + right: 4, child: GestureDetector( onTap: () => Navigator.pop(context), - child: const CircleAvatar( - radius: 15, - backgroundColor: AppColors.accent, - child: Icon(Icons.close, color: Colors.white, size: 18), + child: Container( + width: 30, + height: 30, + decoration: BoxDecoration( + color: AppColors.accent, + shape: BoxShape.circle, + border: Border.all(color: Colors.white.withValues(alpha: 0.4)), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.4), + blurRadius: 6, + ), + ], + ), + child: const Icon(Icons.close, color: Colors.white, size: 18), ), ), ), @@ -198,57 +289,116 @@ class _ChatPanelState extends State { Widget _packView(ChatPack p) { final grid = p.isEmoji ? _emojiGrid(p) : _textGrid(p); if (p.owned) return grid; - // بسته‌ی قفل: گرید کم‌رنگ + دکمه‌ی خرید روی آن. - return Stack( - children: [ - IgnorePointer(child: Opacity(opacity: 0.35, child: grid)), - Center(child: _buyCard(p)), - ], + // بسته‌ی قفل: گریدِ محو (blur) + کارتِ خرید روی آن. + return ClipRRect( + borderRadius: BorderRadius.circular(14), + child: Stack( + fit: StackFit.expand, + children: [ + IgnorePointer(child: Opacity(opacity: 0.5, child: grid)), + BackdropFilter( + filter: ImageFilter.blur(sigmaX: 3, sigmaY: 3), + child: Container(color: AppColors.lapisLo.withValues(alpha: 0.25)), + ), + Center(child: _buyCard(p)), + ], + ), ); } Widget _buyCard(ChatPack p) { final busy = _busyPack == p.id; return Container( - padding: const EdgeInsets.all(16), - margin: const EdgeInsets.symmetric(horizontal: 24), + padding: const EdgeInsets.fromLTRB(20, 22, 20, 20), + margin: const EdgeInsets.symmetric(horizontal: 28), decoration: BoxDecoration( - color: AppColors.lapisLo.withValues(alpha: 0.9), - borderRadius: BorderRadius.circular(14), + gradient: const LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [AppColors.lapisHi, AppColors.lapisLo], + ), + borderRadius: BorderRadius.circular(18), border: Border.all(color: AppColors.gold, width: 1.5), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.45), + blurRadius: 16, + offset: const Offset(0, 8), + ), + ], ), child: Column( mainAxisSize: MainAxisSize.min, children: [ + Container( + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: AppColors.gold.withValues(alpha: 0.12), + shape: BoxShape.circle, + border: Border.all(color: AppColors.goldFaint), + ), + child: Icon( + p.vip ? Icons.workspace_premium : Icons.lock, + color: AppColors.gold, + size: 28, + ), + ), + const SizedBox(height: 12), + Text( + p.vip ? 'ویژه‌ی اعضای VIP' : 'این بسته قفل است', + textAlign: TextAlign.center, + style: const TextStyle( + color: Colors.white, + fontSize: 14, + fontWeight: FontWeight.bold, + ), + ), if (p.vip) const Padding( - padding: EdgeInsets.only(bottom: 10), - child: Text('این بسته برای بازیکنان VIP رایگان است!', + padding: EdgeInsets.only(top: 4), + child: Text('برای اعضای VIP رایگان است', textAlign: TextAlign.center, - style: TextStyle(color: AppColors.gold, fontSize: 13)), + style: TextStyle(color: AppColors.gold, fontSize: 12)), ), - ElevatedButton.icon( - style: ElevatedButton.styleFrom( - backgroundColor: AppColors.success, - minimumSize: const Size(160, 46), - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(12), - side: const BorderSide(color: AppColors.gold, width: 1.4), + const SizedBox(height: 16), + SizedBox( + width: double.infinity, + child: ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: AppColors.success, + padding: const EdgeInsets.symmetric(vertical: 12), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(14), + side: const BorderSide(color: AppColors.gold, width: 1.3), + ), ), + onPressed: busy ? null : () => _buy(p), + child: busy + ? const SizedBox( + width: 20, + height: 20, + child: CircularProgressIndicator( + strokeWidth: 2.2, color: Colors.white)) + : Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text('خرید', + style: TextStyle( + color: Colors.white, + fontSize: 15, + fontWeight: FontWeight.bold)), + const SizedBox(width: 8), + const Icon(Icons.monetization_on, + color: AppColors.gold, size: 20), + const SizedBox(width: 3), + Text('${p.priceCoins}', + style: const TextStyle( + color: Colors.white, + fontSize: 15, + fontWeight: FontWeight.bold)), + ], + ), ), - onPressed: busy ? null : () => _buy(p), - icon: busy - ? const SizedBox( - width: 18, - height: 18, - child: CircularProgressIndicator( - strokeWidth: 2, color: Colors.white)) - : const Icon(Icons.monetization_on, color: AppColors.gold), - label: Text('${p.priceCoins}', - style: const TextStyle( - color: Colors.white, - fontSize: 16, - fontWeight: FontWeight.bold)), ), ], ), @@ -257,32 +407,51 @@ class _ChatPanelState extends State { Widget _textGrid(ChatPack p) => GridView.count( crossAxisCount: 2, - padding: const EdgeInsets.all(4), + padding: const EdgeInsets.symmetric(horizontal: 2, vertical: 4), childAspectRatio: 2.7, mainAxisSpacing: 10, crossAxisSpacing: 10, + physics: const BouncingScrollPhysics(), children: [ for (final m in p.messages) - GestureDetector( - onTap: p.owned ? () => _pick(p, m) : null, - child: Container( - alignment: Alignment.center, - padding: const EdgeInsets.symmetric(horizontal: 6), - decoration: BoxDecoration( - gradient: const LinearGradient( - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - colors: [AppColors.lapisHi, AppColors.lapisLo], + Material( + color: Colors.transparent, + borderRadius: BorderRadius.circular(12), + child: InkWell( + borderRadius: BorderRadius.circular(12), + onTap: p.owned ? () => _pick(p, m) : null, + child: Ink( + decoration: BoxDecoration( + gradient: const LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [AppColors.lapisHi, AppColors.lapisLo], + ), + borderRadius: BorderRadius.circular(12), + border: + Border.all(color: AppColors.goldDark, width: 1.1), + boxShadow: [ + BoxShadow( + color: Colors.black.withValues(alpha: 0.3), + blurRadius: 5, + offset: const Offset(0, 3), + ), + ], + ), + child: Center( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 8), + child: Text(m, + maxLines: 2, + textAlign: TextAlign.center, + overflow: TextOverflow.ellipsis, + style: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: 13.5)), + ), ), - borderRadius: BorderRadius.circular(10), - border: Border.all(color: AppColors.goldDark, width: 1.2), ), - child: Text(m, - maxLines: 2, - textAlign: TextAlign.center, - overflow: TextOverflow.ellipsis, - style: const TextStyle( - color: Colors.white, fontWeight: FontWeight.bold)), ), ), ], @@ -291,13 +460,27 @@ class _ChatPanelState extends State { Widget _emojiGrid(ChatPack p) => GridView.count( crossAxisCount: 5, padding: const EdgeInsets.all(6), + mainAxisSpacing: 8, + crossAxisSpacing: 8, + physics: const BouncingScrollPhysics(), children: [ for (final e in p.messages) - InkWell( - borderRadius: BorderRadius.circular(12), - onTap: p.owned ? () => _pick(p, e) : null, - child: - Center(child: Text(e, style: const TextStyle(fontSize: 32))), + Material( + color: Colors.transparent, + borderRadius: BorderRadius.circular(14), + child: InkWell( + borderRadius: BorderRadius.circular(14), + onTap: p.owned ? () => _pick(p, e) : null, + child: Ink( + decoration: BoxDecoration( + color: AppColors.lapisHi.withValues(alpha: 0.55), + borderRadius: BorderRadius.circular(14), + border: Border.all(color: AppColors.goldFaint), + ), + child: Center( + child: Text(e, style: const TextStyle(fontSize: 30))), + ), + ), ), ], ); diff --git a/lib/feature/game/domain/entities/game_entities.dart b/lib/feature/game/domain/entities/game_entities.dart index 60e7e3d..9d815f1 100644 --- a/lib/feature/game/domain/entities/game_entities.dart +++ b/lib/feature/game/domain/entities/game_entities.dart @@ -7,6 +7,7 @@ class GamePlayer { final bool connected; final int coins; final String rankTier; // bronze..king (خالی برای بات) + final String avatar; // seed آواتارِ انتخابیِ کاربر (خالی برای بات) GamePlayer.fromJson(Map j) : seat = (j['seat'] ?? 0) as int, @@ -14,7 +15,11 @@ class GamePlayer { bot = (j['bot'] ?? false) as bool, connected = (j['connected'] ?? false) as bool, coins = (j['coins'] ?? 0) as int, - rankTier = (j['rank_tier'] ?? '') as String; + rankTier = (j['rank_tier'] ?? '') as String, + avatar = (j['avatar'] ?? '') as String; + + /// seedِ آواتار برای نمایش: آواتارِ انتخابی، وگرنه نام (fallback برای بات). + String get avatarSeed => avatar.isNotEmpty ? avatar : name; } class TrickCard { diff --git a/lib/feature/game/presentation/bloc/game_state.dart b/lib/feature/game/presentation/bloc/game_state.dart index 76d7546..3b6bd76 100644 --- a/lib/feature/game/presentation/bloc/game_state.dart +++ b/lib/feature/game/presentation/bloc/game_state.dart @@ -6,9 +6,13 @@ import '../../domain/entities/game_entities.dart'; /// یک بازیکن در اتاق انتظارِ میز خصوصی. class LobbyPlayer { final String name; + final String avatar; // seed آواتارِ انتخابیِ کاربر final bool host; final int seat; // جایگاهِ مطلق (۰..۳) - const LobbyPlayer(this.name, this.host, this.seat); + const LobbyPlayer(this.name, this.avatar, this.host, this.seat); + + /// seedِ آواتار برای نمایش: آواتارِ انتخابی، وگرنه نام. + String get avatarSeed => avatar.isNotEmpty ? avatar : name; } /// وضعیت اتاق انتظارِ میز خصوصی (دورهمی). @@ -35,6 +39,7 @@ class TableLobby { players: ((j['players'] as List?) ?? []) .map((e) => LobbyPlayer( (e['name'] ?? '') as String, + (e['avatar'] ?? '') as String, (e['host'] ?? false) as bool, (e['seat'] ?? 0) as int, )) diff --git a/lib/feature/game/presentation/screen/game_screen.dart b/lib/feature/game/presentation/screen/game_screen.dart index 13e7194..0cb5553 100644 --- a/lib/feature/game/presentation/screen/game_screen.dart +++ b/lib/feature/game/presentation/screen/game_screen.dart @@ -393,7 +393,7 @@ class _GameScreenState extends State { ) : ClipRRect( borderRadius: BorderRadius.circular(8), - child: RandomAvatar(p.name), + child: RandomAvatar(p.avatarSeed), ), ), const SizedBox(height: 2), diff --git a/lib/feature/game/presentation/screen/private_table_screen.dart b/lib/feature/game/presentation/screen/private_table_screen.dart index 64167cc..227e603 100644 --- a/lib/feature/game/presentation/screen/private_table_screen.dart +++ b/lib/feature/game/presentation/screen/private_table_screen.dart @@ -301,7 +301,7 @@ class _LobbyViewState extends State<_LobbyView> { ) : ClipRRect( borderRadius: BorderRadius.circular(9), - child: RandomAvatar(p.name), + child: RandomAvatar(p.avatarSeed), ), ), if (!empty) diff --git a/lib/feature/game/presentation/widgets/flame/hokm_game.dart b/lib/feature/game/presentation/widgets/flame/hokm_game.dart index 8b6b727..38fff43 100644 --- a/lib/feature/game/presentation/widgets/flame/hokm_game.dart +++ b/lib/feature/game/presentation/widgets/flame/hokm_game.dart @@ -68,6 +68,7 @@ class HokmGame extends FlameGame { bool _beatPlayed = false; // اولین ضربان در پنجره‌ی پایانیِ نوبت پخش شد double _hbAccum = 0; // انباشتِ زمان برای تکرارِ منظمِ ضربان تا پایانِ نوبت bool _wasMyTurn = false; + String _turnSig = ''; // امضای نوبت (نوبت|تعدادِ کارتِ زمین|فاز) — هم‌گام با HUD /// فرشِ انتخابی؛ 'classic' یعنی سطحِ میزِ ترسیمی. فرشِ تصویری به‌عنوانِ سطحِ /// میز (بریده در شکلِ میز) روی زمینِ لاجوردی کشیده می‌شود. @@ -271,34 +272,47 @@ class HokmGame extends FlameGame { // ضربانِ قلب فقط در ثانیه‌های پایانیِ نوبتِ خودِ کاربر؛ هنگامِ پایانِ نوبت // (بازی‌شدنِ کارت توسط کاربر یا سیستم) بلافاصله قطع می‌شود. + // + // زمان‌سنجِ نوبت باید دقیقاً هم‌زمان با «حلقه‌ی زمانِ نوبت» در HUD شروع شود؛ + // آن حلقه با تغییرِ امضای نوبت (نوبت|تعدادِ کارتِ زمین|فاز) ریست می‌شود و به + // انیمیشنِ بُر/پخش کاری ندارد. پس اینجا هم شمارش را به همان امضا می‌بندیم و + // فقط «پخشِ صدا» را در حینِ انیمیشن/اینترو خاموش می‌کنیم تا صدا و تصویر یکی شوند. void _updateHeartBeat(double dt) { final s = _s; - final myTurn = !_silent && - !_introActive && - !_shuffling && - s != null && + final isMyTurn = s != null && ((s.phase == 'playing' && s.turn == s.yourSeat) || (s.phase == 'choose_trump' && s.hakem == s.yourSeat)); - if (!myTurn) { + if (!isMyTurn || _silent) { if (_wasMyTurn) { AppSounds.stopHeartBeat(); // نوبت تمام شد ⇒ توقفِ فوریِ صدا _wasMyTurn = false; } + _turnSig = s == null ? '' : '${s.turn}|${s.trick.length}|${s.phase}'; return; } - if (!_wasMyTurn) { - // شروعِ نوبتِ من ⇒ شمارش از صفر. + // امضای نوبت — همانی که حلقه‌ی HUD استفاده می‌کند. + final sig = '${s.turn}|${s.trick.length}|${s.phase}'; + if (!_wasMyTurn || sig != _turnSig) { + // شروعِ نوبتِ من (یا نوبتِ جدید) ⇒ شمارش از صفر، هم‌زمان با حلقه‌ی HUD. _wasMyTurn = true; + _turnSig = sig; _turnElapsed = 0; _beatPlayed = false; _hbAccum = 0; - // صدای «نوبتِ شما» فقط هنگامِ بازیِ کارت (نه پخش/انتخابِ حکم). - if (s.phase == 'playing' && !_dealing) AppSounds.turn(); + // صدای «نوبتِ شما» فقط هنگامِ بازیِ کارت و بیرون از انیمیشن‌ها. + if (s.phase == 'playing' && !_introActive && !_shuffling && !_dealing) { + AppSounds.turn(); + } } _turnElapsed += dt; + // در حینِ اینترو/بُر/پخش صدایی پخش نمی‌کنیم (اما شمارش ادامه دارد تا با + // حلقه‌ی HUD هم‌گام بماند). این انیمیشن‌ها در ابتدای نوبت‌اند و با پنجره‌ی + // پایانیِ ضربان هم‌پوشانی ندارند. + if (_introActive || _shuffling || _dealing) return; + final dur = (s.turnTimeoutMs > 0 ? s.turnTimeoutMs / 1000.0 : 30.0); final remaining = dur - _turnElapsed; final warn = math.min(7.0, dur * 0.4); diff --git a/lib/feature/game/presentation/widgets/table_hud.dart b/lib/feature/game/presentation/widgets/table_hud.dart index c80f40f..78ccdf5 100644 --- a/lib/feature/game/presentation/widgets/table_hud.dart +++ b/lib/feature/game/presentation/widgets/table_hud.dart @@ -297,7 +297,8 @@ class _PlayerSeat extends StatelessWidget { color: Colors.white70, size: diameter * 0.5, ) - : RandomAvatar(player.name.isEmpty ? '?' : player.name), + : RandomAvatar( + player.avatarSeed.isEmpty ? '?' : player.avatarSeed), ), ), ), diff --git a/lib/feature/shop/presentation/bloc/shop_bloc.dart b/lib/feature/shop/presentation/bloc/shop_bloc.dart index 9c6a38f..6a1b3bf 100644 --- a/lib/feature/shop/presentation/bloc/shop_bloc.dart +++ b/lib/feature/shop/presentation/bloc/shop_bloc.dart @@ -1,6 +1,7 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import '../../../../core/payment/payment_gateway.dart'; +import '../../../../core/storage/token_storage.dart'; import '../../../../core/resources/data_state.dart'; import '../../../../core/usecase/use_case.dart'; import '../../domain/use_cases/ad_reward_usecase.dart'; @@ -19,6 +20,7 @@ class ShopBloc extends Bloc { final PurchaseUseCase purchaseUseCase; final AdRewardUseCase adRewardUseCase; final PaymentGateway gateway; + final TokenStorage storage; ShopBloc( this.getShopUseCase, @@ -27,6 +29,7 @@ class ShopBloc extends Bloc { this.purchaseUseCase, this.adRewardUseCase, this.gateway, + this.storage, ) : super(ShopBlocState.initial()) { on((event, emit) => _load(emit)); @@ -59,9 +62,13 @@ class ShopBloc extends Bloc { // شناسه‌ی کاتالوگ استفاده می‌کنیم (حالتِ توسعه/تک‌فروشگاهی). final sku = event.sku.isNotEmpty ? event.sku : event.productId; + // شماره‌ی موبایل را به‌عنوان payload می‌فرستیم تا خرید در پنلِ فروشگاه + // قابلِ ردیابی باشد. + final mobile = await storage.readMobile() ?? ''; + final PurchaseResult result; try { - result = await gateway.purchase(sku); + result = await gateway.purchase(sku, payload: mobile); } catch (e) { return DataError('خطا در پرداخت: $e'); }