From f4d99043c4f5f074aa1f59df8c8ab3445b8aace9 Mon Sep 17 00:00:00 2001 From: Amirmahdi Nourkazemi Date: Thu, 18 Jun 2026 21:57:28 +0330 Subject: [PATCH] feat: add sounds --- .vscode/launch.json | 2 +- ...dio-button-press-382713.mp3 => button.mp3} | Bin ...mftx-carddrop-447013.mp3 => card_drop.mp3} | Bin ...-more-on-my-patreon-339824.mp3 => cut.mp3} | Bin ...playing-card-522514.mp3 => place_card.mp3} | Bin ...le-card-shuffle-104313.mp3 => shuffle.mp3} | Bin ...-playing-card-522520.mp3 => take_card.mp3} | Bin ...4-winner-game-sound-404167.mp3 => win.mp3} | Bin lib/core/config.dart | 2 +- lib/core/service/app_sounds.dart | 51 ++++++++++ lib/core/widgets/game_ui.dart | 8 +- .../auth/presentation/screen/otp_screen.dart | 66 ++++++++---- .../game/presentation/screen/game_screen.dart | 33 ++++-- .../presentation/widgets/flame/hokm_game.dart | 84 +++++++++++----- .../widgets/flame/shuffle_animation.dart | 94 ++++++++++++++++++ lib/main.dart | 2 + pubspec.lock | 24 +++++ pubspec.yaml | 1 + 18 files changed, 312 insertions(+), 55 deletions(-) rename assets/audio/{dragon-studio-button-press-382713.mp3 => button.mp3} (100%) rename assets/audio/{u_56l164mftx-carddrop-447013.mp3 => card_drop.mp3} (100%) rename assets/audio/{cyberwave-orchestra-fantasy-game-sword-cut-sound-effect-get-more-on-my-patreon-339824.mp3 => cut.mp3} (100%) rename assets/audio/{oxidvideos-placing-playing-card-522514.mp3 => place_card.mp3} (100%) rename assets/audio/{freesound_community-riffle-card-shuffle-104313.mp3 => shuffle.mp3} (100%) rename assets/audio/{oxidvideos-taking-playing-card-522520.mp3 => take_card.mp3} (100%) rename assets/audio/{puyopuyomegafan1234-winner-game-sound-404167.mp3 => win.mp3} (100%) create mode 100644 lib/core/service/app_sounds.dart create mode 100644 lib/feature/game/presentation/widgets/flame/shuffle_animation.dart diff --git a/.vscode/launch.json b/.vscode/launch.json index 19ac997..9ae82f1 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -7,7 +7,7 @@ "type": "dart", "program": "lib/main.dart", "args": [ - "--dart-define=BASE_URL=http://192.168.100.6:8080", + "--dart-define=BASE_URL=http://192.168.1.105:8080", "--target-platform=android-arm64" ] }, diff --git a/assets/audio/dragon-studio-button-press-382713.mp3 b/assets/audio/button.mp3 similarity index 100% rename from assets/audio/dragon-studio-button-press-382713.mp3 rename to assets/audio/button.mp3 diff --git a/assets/audio/u_56l164mftx-carddrop-447013.mp3 b/assets/audio/card_drop.mp3 similarity index 100% rename from assets/audio/u_56l164mftx-carddrop-447013.mp3 rename to assets/audio/card_drop.mp3 diff --git a/assets/audio/cyberwave-orchestra-fantasy-game-sword-cut-sound-effect-get-more-on-my-patreon-339824.mp3 b/assets/audio/cut.mp3 similarity index 100% rename from assets/audio/cyberwave-orchestra-fantasy-game-sword-cut-sound-effect-get-more-on-my-patreon-339824.mp3 rename to assets/audio/cut.mp3 diff --git a/assets/audio/oxidvideos-placing-playing-card-522514.mp3 b/assets/audio/place_card.mp3 similarity index 100% rename from assets/audio/oxidvideos-placing-playing-card-522514.mp3 rename to assets/audio/place_card.mp3 diff --git a/assets/audio/freesound_community-riffle-card-shuffle-104313.mp3 b/assets/audio/shuffle.mp3 similarity index 100% rename from assets/audio/freesound_community-riffle-card-shuffle-104313.mp3 rename to assets/audio/shuffle.mp3 diff --git a/assets/audio/oxidvideos-taking-playing-card-522520.mp3 b/assets/audio/take_card.mp3 similarity index 100% rename from assets/audio/oxidvideos-taking-playing-card-522520.mp3 rename to assets/audio/take_card.mp3 diff --git a/assets/audio/puyopuyomegafan1234-winner-game-sound-404167.mp3 b/assets/audio/win.mp3 similarity index 100% rename from assets/audio/puyopuyomegafan1234-winner-game-sound-404167.mp3 rename to assets/audio/win.mp3 diff --git a/lib/core/config.dart b/lib/core/config.dart index de54b3d..901693b 100644 --- a/lib/core/config.dart +++ b/lib/core/config.dart @@ -8,7 +8,7 @@ class AppConfig { /// با تغییر شبکه/IP مک، مقدار dart-define یا همین پیش‌فرض را عوض کنید. static const String baseUrl = String.fromEnvironment( 'BASE_URL', - defaultValue: 'http://192.168.100.6:8080', + defaultValue: 'http://192.168.1.105:8080', ); static String get apiUrl => '$baseUrl/api'; diff --git a/lib/core/service/app_sounds.dart b/lib/core/service/app_sounds.dart new file mode 100644 index 0000000..7cf99a1 --- /dev/null +++ b/lib/core/service/app_sounds.dart @@ -0,0 +1,51 @@ +import 'package:flame_audio/flame_audio.dart'; + +/// سرویسِ متمرکزِ افکت‌های صوتی (فایل‌ها در assets/audio/). +/// +/// از AudioPool استفاده می‌شود: برای هر صدا یک مجموعه‌ی محدود از پخش‌کننده‌ها +/// ساخته و بازاستفاده می‌شود. این کار از نشتِ AudioPlayerها (که روی اندروید +/// باعثِ قطعِ کاملِ صدا و پرشِ فریم می‌شد) جلوگیری می‌کند. +class AppSounds { + AppSounds._(); + + static const _shuffle = 'shuffle.mp3'; // بُر زدن/پخشِ کارت‌ها (شروع دست) + static const _placeCard = 'place_card.mp3'; // گذاشتنِ کارت روی میز (همه‌ی بازیکنان) + static const _takeCard = 'take_card.mp3'; // جمع‌شدنِ دستِ کامل + static const _cut = 'cut.mp3'; // بریدن با حکم (شمشیر) + static const _win = 'win.mp3'; // برد بازی + static const _button = 'button.mp3'; // فشردنِ دکمه‌ها + + static const _all = [_shuffle, _placeCard, _takeCard, _cut, _win, _button]; + + static final Map _pools = {}; + static bool muted = false; + + /// ساختِ Poolها (یک‌بار، در main). تا آماده‌شدن، پخش‌ها بی‌اثرند. + static Future preload() async { + for (final f in _all) { + try { + _pools[f] = await FlameAudio.createPool(f, maxPlayers: 3); + } catch (_) {/* نبودِ فایل ⇒ بی‌صدا */} + } + } + + /// پخش از Pool؛ اگر cutoffMs داده شود، صدا پس از آن مدت قطع می‌شود (کلیکِ کوتاه). + static void _play(String file, double volume, {int? cutoffMs}) { + if (muted) return; + final pool = _pools[file]; + if (pool == null) return; + pool.start(volume: volume).then((stop) { + if (cutoffMs != null) { + Future.delayed(Duration(milliseconds: cutoffMs), () => stop()); + } + }).catchError((_) {}); + } + + /// صدای بُر زدن؛ cutoffMs را برابرِ مدتِ انیمیشنِ بُر بده تا هم‌زمان تمام شوند. + static void shuffle({int? cutoffMs}) => _play(_shuffle, 0.7, cutoffMs: cutoffMs); + static void placeCard() => _play(_placeCard, 0.8); + static void takeCard() => _play(_takeCard, 0.7); + static void cut() => _play(_cut, 0.9); + static void win() => _play(_win, 0.9); + static void button() => _play(_button, 0.5, cutoffMs: 130); // کلیکِ کوتاه +} diff --git a/lib/core/widgets/game_ui.dart b/lib/core/widgets/game_ui.dart index cbde4f2..c1cfa52 100644 --- a/lib/core/widgets/game_ui.dart +++ b/lib/core/widgets/game_ui.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; +import '../service/app_sounds.dart'; import '../theme/app_theme.dart'; /// مجموعه‌ی ویجت‌های گرافیکیِ مشترک برای ظاهرِ بازیِ (Unity-style): @@ -111,7 +112,12 @@ class _GameButtonState extends State { onTapDown: enabled ? (_) => setState(() => _down = true) : null, onTapUp: enabled ? (_) => setState(() => _down = false) : null, onTapCancel: enabled ? () => setState(() => _down = false) : null, - onTap: widget.onTap, + onTap: enabled + ? () { + AppSounds.button(); + widget.onTap!(); + } + : null, child: AnimatedScale( scale: _down ? 0.96 : 1.0, duration: const Duration(milliseconds: 90), diff --git a/lib/feature/auth/presentation/screen/otp_screen.dart b/lib/feature/auth/presentation/screen/otp_screen.dart index fce644f..4b5ba53 100644 --- a/lib/feature/auth/presentation/screen/otp_screen.dart +++ b/lib/feature/auth/presentation/screen/otp_screen.dart @@ -1,7 +1,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:pinput/pinput.dart'; import '../../../../core/theme/app_theme.dart'; import '../../../../core/widgets/game_ui.dart'; @@ -10,7 +10,7 @@ import '../bloc/auth_event.dart'; import '../bloc/auth_state.dart'; import '../bloc/otp_status.dart'; -/// صفحه‌ی ورود کد یک‌بارمصرف (۵ رقمی). +/// صفحه‌ی ورود کد یک‌بارمصرف (۵ رقمی) با pinput و تکمیل خودکارِ پیامک. class OtpScreen extends StatefulWidget { const OtpScreen({super.key}); @@ -29,8 +29,26 @@ class _OtpScreenState extends State { bool get _valid => _controller.text.trim().length == 5; + void _submit(BuildContext context) { + if (_valid) { + context.read().add(CheckOtpEvent(_controller.text.trim())); + } + } + @override Widget build(BuildContext context) { + final defaultPin = PinTheme( + width: 52, + height: 60, + textStyle: const TextStyle( + color: Colors.white, fontSize: 24, fontWeight: FontWeight.bold), + decoration: BoxDecoration( + color: AppColors.bgDark, + borderRadius: BorderRadius.circular(10), + border: Border.all(color: AppColors.goldDark, width: 1.5), + ), + ); + return Scaffold( body: GameBackground( child: BlocConsumer( @@ -62,31 +80,39 @@ class _OtpScreenState extends State { Text('کد پیامک‌شده به ${state.mobile} را وارد کنید', textAlign: TextAlign.center, style: const TextStyle(color: Colors.white70)), - const SizedBox(height: 20), - TextField( - controller: _controller, - keyboardType: TextInputType.number, - textAlign: TextAlign.center, - style: const TextStyle( - fontSize: 28, letterSpacing: 12), - inputFormatters: [ - FilteringTextInputFormatter.digitsOnly, - LengthLimitingTextInputFormatter(5), - ], - decoration: - const InputDecoration(hintText: '- - - - -'), - onChanged: (_) => setState(() {}), + const SizedBox(height: 22), + Directionality( + textDirection: TextDirection.ltr, + child: Pinput( + length: 5, + controller: _controller, + autofocus: true, + keyboardType: TextInputType.number, + closeKeyboardWhenCompleted: true, + defaultPinTheme: defaultPin, + focusedPinTheme: defaultPin.copyWith( + decoration: defaultPin.decoration!.copyWith( + border: + Border.all(color: AppColors.gold, width: 2), + ), + ), + separatorBuilder: (_) => const SizedBox(width: 8), + pinAnimationType: PinAnimationType.fade, + // تکمیل خودکارِ کد از پیامک (Android: SMS User Consent). + androidSmsAutofillMethod: + AndroidSmsAutofillMethod.smsUserConsentApi, + onChanged: (_) => setState(() {}), + onCompleted: (_) => _submit(context), + ), ), - const SizedBox(height: 20), + const SizedBox(height: 22), GameButton( label: 'ورود', width: double.infinity, colors: const [Color(0xFF3FA34D), Color(0xFF1B5E20)], onTap: (!_valid || loading) ? null - : () => context - .read() - .add(CheckOtpEvent(_controller.text.trim())), + : () => _submit(context), ), TextButton( onPressed: loading ? null : () => context.pop(), diff --git a/lib/feature/game/presentation/screen/game_screen.dart b/lib/feature/game/presentation/screen/game_screen.dart index 1e91c22..f59255b 100644 --- a/lib/feature/game/presentation/screen/game_screen.dart +++ b/lib/feature/game/presentation/screen/game_screen.dart @@ -6,6 +6,7 @@ import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; import '../../../../core/network/ws_client.dart'; +import '../../../../core/service/app_sounds.dart'; import '../../../../core/theme/app_theme.dart'; import '../../../wallet/presentation/bloc/wallet_bloc.dart'; import '../../../wallet/presentation/bloc/wallet_event.dart'; @@ -51,19 +52,32 @@ class _GameScreenState extends State { }, child: Scaffold( body: BlocConsumer( - listenWhen: (a, b) => a.notice != b.notice && b.notice != null, + listenWhen: (a, b) => + (a.notice != b.notice && b.notice != null) || + (a.gameOver == null && b.gameOver != null), listener: (context, state) { - ScaffoldMessenger.of(context).showSnackBar( - SnackBar( - content: Text(state.notice!), - duration: const Duration(seconds: 2)), - ); - context.read().clearNotice(); + if (state.gameOver != null) { + final won = state.gameOver!.winnerTeam == + (state.state?.yourSeat ?? 0) % 2; + if (won) AppSounds.win(); + } + if (state.notice != null) { + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text(state.notice!), + duration: const Duration(seconds: 2)), + ); + context.read().clearNotice(); + } }, builder: (context, state) { if (state.state != null && _introTimer == null) { - _introTimer = Timer(const Duration(milliseconds: 1600), () { - if (mounted) setState(() => _introHidden = true); + // کمی «حریفان پیدا شد» نشان بده، بعد اوورلی را کنار بزن و همان + // لحظه انیمیشنِ بُر زدن (با صدا) را اجرا کن. + _introTimer = Timer(const Duration(milliseconds: 700), () { + if (!mounted) return; + setState(() => _introHidden = true); + _game.endIntro(); }); } return Stack( @@ -130,6 +144,7 @@ class _GameScreenState extends State { } void _exitToLobby(BuildContext context) { + _game.silence(); // پس از خروج، صدای کارت‌های بات پخش نشود context.read().add(LoadWalletEvent()); context.go('/lobby'); } diff --git a/lib/feature/game/presentation/widgets/flame/hokm_game.dart b/lib/feature/game/presentation/widgets/flame/hokm_game.dart index e462c4f..360d818 100644 --- a/lib/feature/game/presentation/widgets/flame/hokm_game.dart +++ b/lib/feature/game/presentation/widgets/flame/hokm_game.dart @@ -4,13 +4,14 @@ import 'dart:math' as math; import 'package:flame/components.dart'; import 'package:flame/effects.dart'; import 'package:flame/game.dart'; -import 'package:flame_audio/flame_audio.dart'; import 'package:flutter/material.dart'; +import '../../../../../core/service/app_sounds.dart'; import '../../../domain/entities/game_entities.dart'; import '../../bloc/game_bloc.dart'; import 'card_codes.dart'; import 'card_component.dart'; +import 'shuffle_animation.dart'; import 'table_pieces.dart'; /// صحنه‌ی میز حکم با انیمیشن. شما همیشه پایین میز (rel=0) هستید. @@ -42,6 +43,9 @@ class HokmGame extends FlameGame { double _t = 0; List _prevTrick = const []; int _prevHandSize = 0; // برای تشخیصِ پخشِ کارت (دستِ جدید) جهت صدای بُر زدن + bool _silent = false; // هنگام خروج از میز، دیگر صدایی پخش نشود + bool _introActive = true; // تا پایانِ نمایشِ «جستجوی حریف» + bool _pendingShuffle = false; // بُرِ دستِ اول که تا پایانِ اینترو به تعویق افتاده HokmGame(this.cubit); @@ -51,14 +55,20 @@ class HokmGame extends FlameGame { @override Future onLoad() async { add(Felt()); - // پیش‌بارگذاریِ صدای بُر زدن (در صورت نبودِ فایل، بی‌صدا رد می‌شود). - FlameAudio.audioCache.load('shuffle.mp3').catchError((_) => Uri()); _sub = cubit.stream.listen((ui) { if (ui.state == null) return; final s = ui.state!; - _checkCut(s); - // شروعِ دستِ جدید (پخشِ کارت): صدای بُر زدن. - if (_prevHandSize == 0 && s.yourHand.isNotEmpty) _playShuffle(); + _checkTrickSounds(s); + // شروعِ دستِ جدید (پخشِ کارت): انیمیشن و صدای بُر زدن. + // دستِ اول تا پایانِ نمایشِ «جستجوی حریف» به تعویق می‌افتد تا انیمیشن + // زیرِ اوورلی پنهان نشود و صدا با تصویر هماهنگ باشد. + if (_prevHandSize == 0 && s.yourHand.isNotEmpty && !_silent) { + if (_introActive) { + _pendingShuffle = true; + } else { + _shuffleNow(); + } + } _prevHandSize = s.yourHand.length; _s = s; _relayout(); @@ -71,11 +81,6 @@ class HokmGame extends FlameGame { } } - void _playShuffle() { - // اگر فایل صدا نباشد، خطا نادیده گرفته می‌شود (بازی بی‌صدا). - FlameAudio.play('shuffle.mp3', volume: 0.7).then((_) {}, onError: (_) {}); - } - @override void onGameResize(Vector2 size) { super.onGameResize(size); @@ -88,24 +93,57 @@ class HokmGame extends FlameGame { super.onRemove(); } - // ===== افکتِ بریدن با حکم (تکان + رعد) ===== + // ===== صداها و افکتِ بریدن با حکم (تکان + رعد) ===== - // اگر دقیقاً یک کارتِ تازه به زمین اضافه شده و آن کارت «بُرِش با حکم» باشد - // (خالِ زمینه آتو نیست ولی کارتِ تازه آتوست)، همان لحظه تکان + رعد. - void _checkCut(GameState s) { + // با هر تغییرِ زمین: افزودنِ کارت ⇒ صدای گذاشتن/انداختن (و بریدن با حکم ⇒ + // شمشیر + تکان + رعد)؛ خالی‌شدنِ زمین ⇒ صدای جمع‌شدنِ دست. + void _checkTrickSounds(GameState s) { final now = s.trick.map((t) => t.card).toList(); - final addedOne = now.length == _prevTrick.length + 1 && _isPrefix(_prevTrick, now); - if (addedOne && - s.trump != null && - s.leadSuit != null && - s.leadSuit != s.trump && - suitName(now.last) == s.trump) { - _shake = 1.0; - add(Lightning()); + if (_silent) { + _prevTrick = now; + return; + } + final addedOne = + now.length == _prevTrick.length + 1 && _isPrefix(_prevTrick, now); + if (addedOne) { + final card = s.trick.last; + final isCut = s.trump != null && + s.leadSuit != null && + s.leadSuit != s.trump && + suitName(card.card) == s.trump; + if (isCut) { + _shake = 1.0; + add(Lightning()); + AppSounds.cut(); // بریدن با حکم (شمشیر) + } else { + AppSounds.placeCard(); // کارت روی میز نشست (همه‌ی بازیکنان) + } + } else if (now.isEmpty && _prevTrick.isNotEmpty) { + AppSounds.takeCard(); // دستِ کامل جمع شد } _prevTrick = now; } + /// قطعِ صداهای بازی هنگام خروج از میز (جلوگیری از پخشِ صدای کارت پس از ترک). + void silence() => _silent = true; + + /// پایانِ نمایشِ «جستجوی حریف»؛ اگر بُرِ دستِ اول به تعویق افتاده بود، اکنون + /// (که اوورلی کنار رفته) با صدا و تصویرِ هماهنگ اجرا می‌شود. + void endIntro() { + _introActive = false; + if (_pendingShuffle && !_silent) { + _pendingShuffle = false; + _shuffleNow(); + } + } + + /// اجرای هم‌زمانِ انیمیشن و صدای بُر زدن (صدا با پایانِ انیمیشن قطع می‌شود). + void _shuffleNow() { + if (_silent) return; + AppSounds.shuffle(cutoffMs: ShuffleAnimation.totalMs); + add(ShuffleAnimation(size)); + } + static bool _isPrefix(List a, List b) { for (var i = 0; i < a.length; i++) { if (a[i] != b[i]) return false; diff --git a/lib/feature/game/presentation/widgets/flame/shuffle_animation.dart b/lib/feature/game/presentation/widgets/flame/shuffle_animation.dart new file mode 100644 index 0000000..2042d85 --- /dev/null +++ b/lib/feature/game/presentation/widgets/flame/shuffle_animation.dart @@ -0,0 +1,94 @@ +import 'package:flame/components.dart'; +import 'package:flame/effects.dart'; +import 'package:flutter/material.dart'; + +/// انیمیشنِ بُر زدن: دو دسته کارتِ پشت‌رو در مرکز ریفل می‌شوند، کمی بالا/پایین +/// می‌روند (بُر) و سپس به سمت چهار بازیکن پخش می‌شوند. مدتِ کل [totalMs] است +/// تا صدای بُر زدن دقیقاً هم‌زمان با پایانِ انیمیشن قطع شود. +class ShuffleAnimation extends PositionComponent { + final Vector2 area; + ShuffleAnimation(this.area); + + /// مدتِ کلِ انیمیشن (مرجعِ هماهنگیِ صدا). + static const int totalMs = 2000; + + @override + Future onLoad() async { + priority = 6000; + final cw = area.x * 0.16; + final ch = cw * 1.45; + final center = Vector2(area.x / 2, area.y / 2); + const n = 10; + + // جهتِ پخش به سمتِ ۴ بازیکن (پایین، چپ، بالا، راست). + final dirs = [ + Vector2(0, area.y * 0.42), + Vector2(-area.x * 0.42, 0), + Vector2(0, -area.y * 0.42), + Vector2(area.x * 0.42, 0), + ]; + + for (var i = 0; i < n; i++) { + final left = i.isEven; + final stack = center + Vector2((i - n / 2) * cw * 0.05, 0); + final delay = i * 0.02; // ریفلِ پلکانی + + final card = _Back(Vector2(cw, ch)) + ..anchor = Anchor.center + ..position = center + Vector2((left ? -1 : 1) * cw, 0) + ..angle = (left ? -1 : 1) * 0.22; + add(card); + + // صاف‌شدنِ زاویه هم‌زمان با ریفل. + card.add(RotateEffect.to( + 0, EffectController(duration: 0.55, startDelay: delay))); + + // دنباله: ریفل به مرکز ← بالا (بُر) ← نشستن ← پخش به سمت بازیکن. + card.add(SequenceEffect([ + MoveToEffect(stack, + EffectController(duration: 0.55, curve: Curves.easeOut, startDelay: delay)), + MoveToEffect(stack + Vector2(0, -ch * 0.18), + EffectController(duration: 0.35, curve: Curves.easeInOut)), + MoveToEffect(stack, + EffectController(duration: 0.35, curve: Curves.easeInOut)), + MoveToEffect(center + dirs[i % 4], + EffectController(duration: 0.55, curve: Curves.easeIn)), + ])); + + // کوچک‌شدن هنگامِ پخش (هم‌زمان با حرکتِ آخرِ دنباله). + card.add(ScaleEffect.to( + Vector2.all(0.5), + EffectController(duration: 0.55, startDelay: 1.25 + delay), + )); + } + + add(RemoveEffect(delay: totalMs / 1000)); + } +} + +/// پشتِ یک کارت (مستطیلِ گرد با حاشیه‌ی طلایی). +class _Back extends PositionComponent { + _Back(Vector2 s) : super(size: s); + + static final _fill = Paint()..color = const Color(0xFF7A1020); + static final _border = Paint() + ..style = PaintingStyle.stroke + ..strokeWidth = 2 + ..color = const Color(0xFFE9C76A); + static final _inner = Paint() + ..style = PaintingStyle.stroke + ..strokeWidth = 1.5 + ..color = const Color(0x88E9C76A); + + @override + void render(Canvas canvas) { + final rect = size.toRect(); + canvas.drawRRect( + RRect.fromRectAndRadius(rect, const Radius.circular(8)), _fill); + canvas.drawRRect( + RRect.fromRectAndRadius(rect, const Radius.circular(8)), _border); + canvas.drawRRect( + RRect.fromRectAndRadius(rect.deflate(6), const Radius.circular(6)), + _inner); + } +} diff --git a/lib/main.dart b/lib/main.dart index 606e64a..d7d50f7 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -2,11 +2,13 @@ import 'package:flutter/material.dart'; import 'app.dart'; import 'core/locator/locator.dart'; +import 'core/service/app_sounds.dart'; import 'feature/auth/domain/repository/auth_repository.dart'; Future main() async { WidgetsFlutterBinding.ensureInitialized(); await setupLocator(); + AppSounds.preload(); // پیش‌بارگذاریِ صداها (بدون انتظار) final loggedIn = await locator().isLoggedIn(); runApp(HakemApp(loggedIn: loggedIn)); } diff --git a/pubspec.lock b/pubspec.lock index 7940c0f..b1bc2ed 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -480,6 +480,14 @@ packages: url: "https://pub.myket.ir" source: hosted version: "7.0.2" + pinput: + dependency: "direct main" + description: + name: pinput + sha256: "6d571e38a484f7515a52e89024ef416f11fa6171ac6f32303701374ab9890efa" + url: "https://pub.myket.ir" + source: hosted + version: "4.0.0" platform: dependency: transitive description: @@ -517,6 +525,14 @@ packages: description: flutter source: sdk version: "0.0.0" + smart_auth: + dependency: transitive + description: + name: smart_auth + sha256: "88aa8fe66e951c78a307f26d1c29672dce2e9eb3da2e12e853864d0e615a73ad" + url: "https://pub.myket.ir" + source: hosted + version: "2.0.0" source_span: dependency: transitive description: @@ -581,6 +597,14 @@ packages: url: "https://pub.myket.ir" source: hosted version: "1.4.0" + universal_platform: + dependency: transitive + description: + name: universal_platform + sha256: "64e16458a0ea9b99260ceb5467a214c1f298d647c659af1bff6d3bf82536b1ec" + url: "https://pub.myket.ir" + source: hosted + version: "1.1.0" uuid: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 4623f43..fa8bd31 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -41,6 +41,7 @@ dependencies: flutter_secure_storage: ^10.3.1 equatable: ^2.0.8 get_it: ^8.0.3 + pinput: ^4.0.0 flame: ^1.30.1 flame_audio: ^2.11.14 random_avatar: ^0.0.8