diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index 7ceae5e..240e27b 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -44,4 +44,10 @@ + + + + + + diff --git a/assets/audios/incorrect_answers/ar_1.mp3 b/assets/audios/incorrect_answers/ar_1.mp3 new file mode 100644 index 0000000..10d653c Binary files /dev/null and b/assets/audios/incorrect_answers/ar_1.mp3 differ diff --git a/assets/audios/incorrect_answers/ar_2.mp3 b/assets/audios/incorrect_answers/ar_2.mp3 new file mode 100644 index 0000000..4942400 Binary files /dev/null and b/assets/audios/incorrect_answers/ar_2.mp3 differ diff --git a/assets/audios/incorrect_answers/ar_3.mp3 b/assets/audios/incorrect_answers/ar_3.mp3 new file mode 100644 index 0000000..589ec8a Binary files /dev/null and b/assets/audios/incorrect_answers/ar_3.mp3 differ diff --git a/assets/audios/incorrect_answers/ar_4.mp3 b/assets/audios/incorrect_answers/ar_4.mp3 new file mode 100644 index 0000000..6ec06e8 Binary files /dev/null and b/assets/audios/incorrect_answers/ar_4.mp3 differ diff --git a/assets/audios/incorrect_answers/ar_5.mp3 b/assets/audios/incorrect_answers/ar_5.mp3 new file mode 100644 index 0000000..619974e Binary files /dev/null and b/assets/audios/incorrect_answers/ar_5.mp3 differ diff --git a/assets/audios/incorrect_answers/en_1.mp3 b/assets/audios/incorrect_answers/en_1.mp3 new file mode 100644 index 0000000..c4db2fe Binary files /dev/null and b/assets/audios/incorrect_answers/en_1.mp3 differ diff --git a/assets/audios/incorrect_answers/en_2.mp3 b/assets/audios/incorrect_answers/en_2.mp3 new file mode 100644 index 0000000..b2734d5 Binary files /dev/null and b/assets/audios/incorrect_answers/en_2.mp3 differ diff --git a/assets/audios/incorrect_answers/en_3.mp3 b/assets/audios/incorrect_answers/en_3.mp3 new file mode 100644 index 0000000..c0ad942 Binary files /dev/null and b/assets/audios/incorrect_answers/en_3.mp3 differ diff --git a/assets/audios/incorrect_answers/en_4.mp3 b/assets/audios/incorrect_answers/en_4.mp3 new file mode 100644 index 0000000..cfcbb48 Binary files /dev/null and b/assets/audios/incorrect_answers/en_4.mp3 differ diff --git a/assets/audios/incorrect_answers/fa_1.mp3 b/assets/audios/incorrect_answers/fa_1.mp3 new file mode 100644 index 0000000..17ce604 Binary files /dev/null and b/assets/audios/incorrect_answers/fa_1.mp3 differ diff --git a/assets/audios/incorrect_answers/fa_2.mp3 b/assets/audios/incorrect_answers/fa_2.mp3 new file mode 100644 index 0000000..e760d7e Binary files /dev/null and b/assets/audios/incorrect_answers/fa_2.mp3 differ diff --git a/assets/audios/incorrect_answers/fa_3.mp3 b/assets/audios/incorrect_answers/fa_3.mp3 new file mode 100644 index 0000000..dc1a716 Binary files /dev/null and b/assets/audios/incorrect_answers/fa_3.mp3 differ diff --git a/assets/audios/incorrect_answers/fa_4.mp3 b/assets/audios/incorrect_answers/fa_4.mp3 new file mode 100644 index 0000000..9022a98 Binary files /dev/null and b/assets/audios/incorrect_answers/fa_4.mp3 differ diff --git a/assets/audios/incorrect_answers/fa_5.mp3 b/assets/audios/incorrect_answers/fa_5.mp3 new file mode 100644 index 0000000..bf88cd8 Binary files /dev/null and b/assets/audios/incorrect_answers/fa_5.mp3 differ diff --git a/assets/audios/incorrect_answers/fa_6.mp3 b/assets/audios/incorrect_answers/fa_6.mp3 new file mode 100644 index 0000000..0e574b7 Binary files /dev/null and b/assets/audios/incorrect_answers/fa_6.mp3 differ diff --git a/assets/audios/incorrect_answers/incorrect_answer.mp3 b/assets/audios/incorrect_answers/incorrect_answer.mp3 new file mode 100644 index 0000000..17eabf6 Binary files /dev/null and b/assets/audios/incorrect_answers/incorrect_answer.mp3 differ diff --git a/assets/fonts/NotoSansArabic.ttf b/assets/fonts/NotoSansArabic.ttf new file mode 100644 index 0000000..85dd0ae Binary files /dev/null and b/assets/fonts/NotoSansArabic.ttf differ diff --git a/assets/svg/button_3.5.svg b/assets/svg/button_3.5.svg new file mode 100644 index 0000000..799b4a0 --- /dev/null +++ b/assets/svg/button_3.5.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/svg/lock.svg b/assets/svg/lock.svg new file mode 100644 index 0000000..98f5cab --- /dev/null +++ b/assets/svg/lock.svg @@ -0,0 +1,219 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/lib/common_ui/resources/my_assets.dart b/lib/common_ui/resources/my_assets.dart index e43ed0a..e0f0d8c 100644 --- a/lib/common_ui/resources/my_assets.dart +++ b/lib/common_ui/resources/my_assets.dart @@ -49,6 +49,7 @@ class MyAssets { static const String button2 = 'assets/svg/button_2.svg.vec'; static const String button2Tablet = 'assets/svg/button_2_tablet.svg.vec'; static const String button3 = 'assets/svg/button_3.svg.vec'; + static const String button35 = 'assets/svg/button_3.5.svg.vec'; static const String button3Tablet = 'assets/svg/button_3_tablet.svg.vec'; static const String theme = 'assets/svg/theme.svg.vec'; static const String facebook = 'assets/svg/facebook.svg.vec'; @@ -74,6 +75,7 @@ class MyAssets { static const String iconNotif = 'assets/svg/icon_notif.svg.vec'; static const String iconPlayVideo = 'assets/svg/icon_play_video.svg.vec'; static const String check = 'assets/svg/check.svg.vec'; + static const String lock = 'assets/svg/lock.svg.vec'; static final List images = [ backgroundHome, diff --git a/lib/common_ui/resources/my_audios.dart b/lib/common_ui/resources/my_audios.dart index 5fbe749..0f62dd5 100644 --- a/lib/common_ui/resources/my_audios.dart +++ b/lib/common_ui/resources/my_audios.dart @@ -1,6 +1,13 @@ -class MyAudios { +import 'dart:math'; + +import 'package:hadi_hoda_flutter/core/constants/my_constants.dart'; +import 'package:hadi_hoda_flutter/core/utils/local_storage.dart'; + +class MyAudios { static const MyAudios _i = MyAudios._internal(); + const MyAudios._internal(); + factory MyAudios() => _i; static const String back = 'assets/audios/back.mp3'; @@ -8,7 +15,41 @@ class MyAudios { static const String diamondEnd = 'assets/audios/diamond_end.mp3'; static const String diamondIncrease = 'assets/audios/diamond_increase.mp3'; static const String home = 'assets/audios/home.mp3'; - static const String incorrectAnswer = 'assets/audios/incorrect_answer.mp3'; static const String question = 'assets/audios/question.mp3'; static const String rightAnswer = 'assets/audios/right_answer.mp3'; -} \ No newline at end of file + + static String get incorrectAnswer { + final currentLang = + LocalStorage.readData(key: MyConstants.selectLanguage) ?? + MyConstants.defaultLanguage; + if(_incorrectAnswers.keys.contains(currentLang)) { + final audios = _incorrectAnswers[currentLang]; + return audios![Random().nextInt(audios.length)]; + } + return 'assets/audios/incorrect_answer.mp3'; + } + + static const _incorrectAnswers = { + 'en': [ + "assets/audios/incorrect_answers/en_1.mp3", + "assets/audios/incorrect_answers/en_2.mp3", + "assets/audios/incorrect_answers/en_3.mp3", + "assets/audios/incorrect_answers/en_4.mp3", + ], + 'fa': [ + "assets/audios/incorrect_answers/fa_1.mp3", + "assets/audios/incorrect_answers/fa_2.mp3", + "assets/audios/incorrect_answers/fa_3.mp3", + "assets/audios/incorrect_answers/fa_4.mp3", + "assets/audios/incorrect_answers/fa_5.mp3", + "assets/audios/incorrect_answers/fa_6.mp3", + ], + 'ar': [ + "assets/audios/incorrect_answers/ar_1.mp3", + "assets/audios/incorrect_answers/ar_2.mp3", + "assets/audios/incorrect_answers/ar_3.mp3", + "assets/audios/incorrect_answers/ar_4.mp3", + "assets/audios/incorrect_answers/ar_5.mp3", + ], + }; +} diff --git a/lib/common_ui/resources/my_text_style.dart b/lib/common_ui/resources/my_text_style.dart index 7cd4376..c3da204 100644 --- a/lib/common_ui/resources/my_text_style.dart +++ b/lib/common_ui/resources/my_text_style.dart @@ -1,90 +1,137 @@ import 'package:flutter/material.dart'; import 'package:hadi_hoda_flutter/common_ui/resources/my_colors.dart'; +import 'package:hadi_hoda_flutter/core/constants/my_constants.dart'; +import 'package:hadi_hoda_flutter/core/utils/local_storage.dart'; +import 'package:hadi_hoda_flutter/core/utils/my_device.dart'; +import 'package:hadi_hoda_flutter/core/utils/my_device.dart'; +import 'package:hadi_hoda_flutter/core/utils/my_device.dart'; +import 'package:hadi_hoda_flutter/core/utils/my_device.dart'; +import 'package:hadi_hoda_flutter/core/utils/my_device.dart'; +import 'package:hadi_hoda_flutter/core/utils/my_device.dart'; +import 'package:hadi_hoda_flutter/core/utils/my_device.dart'; +import 'package:hadi_hoda_flutter/core/utils/my_device.dart'; +import 'package:hadi_hoda_flutter/core/utils/my_device.dart'; +import 'package:hadi_hoda_flutter/core/utils/my_device.dart'; +import 'package:hadi_hoda_flutter/core/utils/my_device.dart'; class MYTextStyle { static const MYTextStyle _i = MYTextStyle._internal(); + const MYTextStyle._internal(); + factory MYTextStyle() => _i; static const String dinoKids = 'DinoKids'; + static const String notoSansArabic = 'NotoSansArabic'; static const String marhey = 'Marhey'; static const String baloo2 = 'Baloo_2'; static const Color textColor = MyColors.white; - static const TextStyle titr0 = TextStyle( - fontFamily: marhey, - fontWeight: FontWeight.w700, - fontSize: 26, + static bool _isArabicOrPersianLanguage() { + final selectedLanguage = + (LocalStorage.readData(key: MyConstants.selectLanguage) ?? + MyConstants.defaultLanguage) + .toString() + .trim() + .toLowerCase(); + final languageCode = selectedLanguage.split(RegExp(r'[_-]')).first; + return languageCode == 'ar' || languageCode == 'fa'; + } + + static String _fontForLanguage(String defaultFont) { + return _isArabicOrPersianLanguage() ? notoSansArabic : defaultFont; + } + + static FontWeight _fontWeightForLanguage(FontWeight defaultWeight) { + if (!_isArabicOrPersianLanguage()) return defaultWeight; + return switch (defaultWeight) { + FontWeight.w100 => FontWeight.w300, + FontWeight.w200 => FontWeight.w400, + FontWeight.w300 => FontWeight.w500, + FontWeight.w400 => FontWeight.w600, + FontWeight.w500 => FontWeight.w700, + FontWeight.w600 => FontWeight.w800, + FontWeight.w700 => FontWeight.w900, + FontWeight.w800 => FontWeight.w900, + FontWeight.w900 => FontWeight.w900, + _ => defaultWeight, + }; + } + + static TextStyle get titr0 => TextStyle( + fontFamily: _fontForLanguage(marhey), + fontWeight: _fontWeightForLanguage(FontWeight.w700), + fontSize: MyDevice.fontSize(26), color: textColor, ); - static const TextStyle titr1 = TextStyle( - fontFamily: baloo2, - fontWeight: FontWeight.w800, - fontSize: 20, + static TextStyle get titr1 => TextStyle( + fontFamily: _fontForLanguage(baloo2), + fontWeight: _fontWeightForLanguage(FontWeight.w800), + fontSize: MyDevice.fontSize(20), color: textColor, ); - static const TextStyle titr3 = TextStyle( - fontFamily: marhey, - fontWeight: FontWeight.w600, - fontSize: 18, + static TextStyle get titr3 => TextStyle( + fontFamily: _fontForLanguage(marhey), + fontWeight: _fontWeightForLanguage(FontWeight.w600), + fontSize: MyDevice.fontSize(18), color: textColor, ); - static const TextStyle titr4 = TextStyle( - fontFamily: baloo2, - fontWeight: FontWeight.w700, - fontSize: 14, + static TextStyle get titr4 => TextStyle( + fontFamily: _fontForLanguage(marhey), + fontWeight: _fontWeightForLanguage(FontWeight.w700), + fontSize: MyDevice.fontSize(14), color: textColor, ); - static const TextStyle matn1 = TextStyle( - fontFamily: baloo2, - fontWeight: FontWeight.w600, - fontSize: 18, + static TextStyle get matn1 => TextStyle( + fontFamily: _fontForLanguage(baloo2), + fontWeight: _fontWeightForLanguage(FontWeight.w600), + fontSize: MyDevice.fontSize(18), color: textColor, ); - static const TextStyle matn2_3 = TextStyle( - fontFamily: baloo2, - fontWeight: FontWeight.w700, - fontSize: 18, + static TextStyle get matn2_3 => TextStyle( + fontFamily: _fontForLanguage(baloo2), + fontWeight: _fontWeightForLanguage(FontWeight.w700), + fontSize: MyDevice.fontSize(18), color: textColor, ); - static const TextStyle matn2_2 = TextStyle( - fontFamily: baloo2, - fontWeight: FontWeight.w700, - fontSize: 16, + static TextStyle get matn2_2 => TextStyle( + fontFamily: _fontForLanguage(baloo2), + fontWeight: _fontWeightForLanguage(FontWeight.w700), + fontSize: MyDevice.fontSize(16), color: textColor, ); - static const TextStyle matn2 = TextStyle( - fontFamily: baloo2, - fontWeight: FontWeight.w700, - fontSize: 14, + static TextStyle get matn2 => TextStyle( + fontFamily: _fontForLanguage(baloo2), + fontWeight: _fontWeightForLanguage(FontWeight.w700), + fontSize: MyDevice.fontSize(14), color: textColor, ); - static const TextStyle matn3 = TextStyle( - fontFamily: baloo2, - fontWeight: FontWeight.w500, - fontSize: 12, + static TextStyle get matn3 => TextStyle( + fontFamily: _fontForLanguage(baloo2), + fontWeight: _fontWeightForLanguage(FontWeight.w500), + fontSize: MyDevice.fontSize(12), color: textColor, ); - static const TextStyle button1 = TextStyle( - fontFamily: dinoKids, - fontWeight: FontWeight.w400, - fontSize: 45, + static TextStyle get button1 => TextStyle( + fontFamily: _fontForLanguage(dinoKids), + fontWeight: _fontWeightForLanguage(FontWeight.w400), + fontSize: MyDevice.fontSize(45), color: textColor, ); - static const TextStyle button2 = TextStyle( - fontFamily: dinoKids, - fontWeight: FontWeight.w400, - fontSize: 30, + static TextStyle get button2 => TextStyle( + fontFamily: _fontForLanguage(dinoKids), + fontWeight: _fontWeightForLanguage(FontWeight.w400), + fontSize: MyDevice.fontSize(30), color: textColor, ); -} \ No newline at end of file +} diff --git a/lib/core/middlewares/my_middlewares.dart b/lib/core/middlewares/my_middlewares.dart index 5427f8f..e5be772 100644 --- a/lib/core/middlewares/my_middlewares.dart +++ b/lib/core/middlewares/my_middlewares.dart @@ -31,6 +31,16 @@ class MyMiddlewares { } } + static FutureOr download(BuildContext context, GoRouterState state) { + final String? firstIntro = LocalStorage.readData( + key: MyConstants.firstIntro); + if (firstIntro != 'true') { + return Routes.introPage; + } else { + return null; + } + } + static FutureOr question(BuildContext context, GoRouterState state) { final String? firstShowCase = LocalStorage.readData( key: MyConstants.firstShowcase); diff --git a/lib/core/network/interceptors/logging_interceptor.dart b/lib/core/network/interceptors/logging_interceptor.dart index 5311e0d..891acfb 100644 --- a/lib/core/network/interceptors/logging_interceptor.dart +++ b/lib/core/network/interceptors/logging_interceptor.dart @@ -10,7 +10,8 @@ class LoggingInterceptor { requestHeader: true, requestBody: true, responseBody: true, - responseHeader: false, + responseHeader: true, + request: true, error: true, compact: true, maxWidth: 90, diff --git a/lib/core/params/level_params.dart b/lib/core/params/level_params.dart index 1e885f8..7709331 100644 --- a/lib/core/params/level_params.dart +++ b/lib/core/params/level_params.dart @@ -1,13 +1,16 @@ class LevelParams { int? id; + bool forceRemote; - LevelParams({this.id}); + LevelParams({this.id, this.forceRemote = false}); LevelParams copyWith({ int? id, + bool? forceRemote, }) { return LevelParams( id: id ?? this.id, + forceRemote: forceRemote ?? this.forceRemote, ); } } diff --git a/lib/core/routers/my_routes.dart b/lib/core/routers/my_routes.dart index 526e9dc..8888454 100644 --- a/lib/core/routers/my_routes.dart +++ b/lib/core/routers/my_routes.dart @@ -74,6 +74,7 @@ GoRouter _appPages() => GoRouter( GoRoute( name: Routes.downloadPage, path: Routes.downloadPage, + redirect: MyMiddlewares.download, pageBuilder: (context, state) { final config = state.extra as DownloadPageConfig; return myPageTransition( @@ -132,7 +133,7 @@ GoRouter _appPages() => GoRouter( locator(), locator(instanceName: MyConstants.mainAudioService), locator(instanceName: MyConstants.effectAudioService), - locator() + locator(), )..add(SetCurrentLevelEvent()), child: const LevelPage(), ), diff --git a/lib/core/services/audio_service.dart b/lib/core/services/audio_service.dart index 2feb262..09d62a8 100644 --- a/lib/core/services/audio_service.dart +++ b/lib/core/services/audio_service.dart @@ -6,6 +6,7 @@ import 'package:just_audio/just_audio.dart'; class AudioService { final AudioPlayer _player = AudioPlayer(); final StreamController _streamController = StreamController.broadcast(); + static const double _muteThreshold = 0.001; final double volume; AudioService({required this.volume}){ @@ -74,8 +75,9 @@ class AudioService { Future changeMute({double? newVolume}) async { try { - if (_player.volume == 0) { - await _player.setVolume(newVolume ?? volume); + if (isMuted) { + final targetVolume = (newVolume ?? volume).clamp(0.0, 1.0).toDouble(); + await _player.setVolume(targetVolume); } else { await _player.setVolume(0); } @@ -99,6 +101,7 @@ class AudioService { Stream volumeStream() => _player.volumeStream; double get audioVolume => _player.volume; + bool get isMuted => _player.volume <= _muteThreshold; Stream playingStream() async* { _player.processingStateStream.listen((event) { diff --git a/lib/core/utils/app_life_cycle.dart b/lib/core/utils/app_life_cycle.dart index 79f1fec..70a6de3 100644 --- a/lib/core/utils/app_life_cycle.dart +++ b/lib/core/utils/app_life_cycle.dart @@ -4,6 +4,8 @@ import 'package:hadi_hoda_flutter/core/services/audio_service.dart'; import 'package:hadi_hoda_flutter/init_bindings.dart'; class AppLifeCycleController extends WidgetsBindingObserver { + static bool suppressAutoResumeAudio = false; + AppLifeCycleController() { WidgetsBinding.instance.addObserver(this); } @@ -25,6 +27,7 @@ class AppLifeCycleController extends WidgetsBindingObserver { mainAudio.pause(); effect.pause(); } else if (state == AppLifecycleState.resumed) { + if (suppressAutoResumeAudio) return; mainAudio.play(); effect.play(); } diff --git a/lib/core/utils/my_device.dart b/lib/core/utils/my_device.dart index 3bf919a..00feca9 100644 --- a/lib/core/utils/my_device.dart +++ b/lib/core/utils/my_device.dart @@ -1,6 +1,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:hadi_hoda_flutter/core/utils/my_context.dart'; class MyDevice { static const MyDevice _i = MyDevice._internal(); @@ -26,6 +27,23 @@ class MyDevice { return MediaQuery.sizeOf(context).width >= 1200; } + static double fontSize( + double size, { + double mobileScale = 1, + double tabletScale = 1.25, + double desktopScale = 1.35, + }) { + if (isTablet(MyContext.get)) { + return size * tabletScale; + } + + if (isDesktop(MyContext.get)) { + return size * desktopScale; + } + + return size * mobileScale; + } + /// Get device size static double? getDeviceWidth(BuildContext context) { if (isMobile(context)) { diff --git a/lib/core/utils/screen_size.dart b/lib/core/utils/screen_size.dart index 689b620..99eec7c 100644 --- a/lib/core/utils/screen_size.dart +++ b/lib/core/utils/screen_size.dart @@ -11,3 +11,19 @@ extension AdaptiveSize on double { double get w => MyContext.get.widthScreen * this; double get h => MyContext.get.heightScreen * this; } + +extension SizeExtension on num { + double get h { + final screenHeight = MyContext.get.heightScreen; + const designHeight = 812; // your design height (example: iPhone X) + + return (this * screenHeight) / designHeight; + } + + double w(BuildContext context) { + final screenWidth = MediaQuery.sizeOf(context).width; + const designWidth = 375; // your design width + + return (this * screenWidth) / designWidth; + } +} diff --git a/lib/core/widgets/animations/fade_anim.dart b/lib/core/widgets/animations/fade_anim.dart index 2db0e92..a91a7be 100644 --- a/lib/core/widgets/animations/fade_anim.dart +++ b/lib/core/widgets/animations/fade_anim.dart @@ -19,8 +19,8 @@ class _FadeAnimState extends State super.initState(); _controller = AnimationController( vsync: this, - duration: const Duration(milliseconds: 500), - reverseDuration: const Duration(seconds: 500), + duration: const Duration(milliseconds: 200), + reverseDuration: const Duration(seconds: 200), ); _animation = Tween( begin: 0, diff --git a/lib/core/widgets/animations/fade_anim_delayed.dart b/lib/core/widgets/animations/fade_anim_delayed.dart index ea9f655..28fd9a5 100644 --- a/lib/core/widgets/animations/fade_anim_delayed.dart +++ b/lib/core/widgets/animations/fade_anim_delayed.dart @@ -24,8 +24,8 @@ class _FadeAnimDelayedState extends State super.initState(); _controller = AnimationController( vsync: this, - duration: const Duration(milliseconds: 500), - reverseDuration: const Duration(seconds: 500), + duration: const Duration(milliseconds: 200), + reverseDuration: const Duration(seconds: 200), ); _animation = Tween( begin: 0, diff --git a/lib/core/widgets/animations/slide_anim.dart b/lib/core/widgets/animations/slide_anim.dart index e62368d..f6319da 100644 --- a/lib/core/widgets/animations/slide_anim.dart +++ b/lib/core/widgets/animations/slide_anim.dart @@ -20,10 +20,10 @@ class _SlideAnimState extends State with SingleTickerProviderStateMixin { late Animation _animation; final List offsetList = const [ - Offset(-2, -2), - Offset(2, -2), - Offset(-2, 2), - Offset(2, 2), + Offset(-3, -3), + Offset(3, -3), + Offset(-3, 3), + Offset(3, 3), ]; @override diff --git a/lib/core/widgets/animations/slide_down_fade.dart b/lib/core/widgets/animations/slide_down_fade.dart index 0ef73a7..53c7ab2 100644 --- a/lib/core/widgets/animations/slide_down_fade.dart +++ b/lib/core/widgets/animations/slide_down_fade.dart @@ -25,8 +25,8 @@ class _SlideDownFadeState extends State super.initState(); _controller = AnimationController( vsync: this, - duration: const Duration(milliseconds: 500), - reverseDuration: const Duration(milliseconds: 500), + duration: const Duration(milliseconds: 200), + reverseDuration: const Duration(milliseconds: 200), ); _fadeAnim = Tween( diff --git a/lib/core/widgets/animations/slide_up_fade.dart b/lib/core/widgets/animations/slide_up_fade.dart index 8df3866..30abeb5 100644 --- a/lib/core/widgets/animations/slide_up_fade.dart +++ b/lib/core/widgets/animations/slide_up_fade.dart @@ -25,8 +25,8 @@ class _SlideUpFadeState extends State super.initState(); _controller = AnimationController( vsync: this, - duration: const Duration(milliseconds: 500), - reverseDuration: const Duration(milliseconds: 500), + duration: const Duration(milliseconds: 200), + reverseDuration: const Duration(milliseconds: 200), ); _fadeAnim = Tween( diff --git a/lib/core/widgets/answer_box/answer_box.dart b/lib/core/widgets/answer_box/answer_box.dart index 7a2a583..7fc8689 100644 --- a/lib/core/widgets/answer_box/answer_box.dart +++ b/lib/core/widgets/answer_box/answer_box.dart @@ -70,13 +70,13 @@ class _AnswerBoxState extends State { bottom: 0, child: AnswerTextBox(text: widget.answer.title ?? ''), ), - PositionedDirectional( + if(widget.answer.audioInfo?.filename?.isNotEmpty == true)Positioned( top: setSize( context: context, mobile: MySpaces.s12, tablet: MySpaces.s20, ), - end: setSize( + right: setSize( context: context, mobile: MySpaces.s8, tablet: MySpaces.s20, diff --git a/lib/core/widgets/answer_box/answer_box_show.dart b/lib/core/widgets/answer_box/answer_box_show.dart index 75d7bea..658def6 100644 --- a/lib/core/widgets/answer_box/answer_box_show.dart +++ b/lib/core/widgets/answer_box/answer_box_show.dart @@ -41,18 +41,29 @@ class AnswerBoxShow extends StatelessWidget { left: 0, right: 0, bottom: -10, - child: AnswerTextBox(text: answer.title ?? ''), + child: AnswerTextBox( + text: answer.title ?? '', + padding: const EdgeInsetsGeometry.all(14), + ), ), - PositionedDirectional( + Positioned( top: setSize(context: context, mobile: MySpaces.s30, tablet: 60), - end: setSize(context: context, mobile: MySpaces.s20, tablet: 60), + right: setSize( + context: context, + mobile: MySpaces.s20, + tablet: 60, + ), child: GestureDetector( onTap: () { onNotifTap?.call(answer); }, child: MyImage( image: MyAssets.iconNotif, - size: setSize(context: context, mobile: MySpaces.s40, tablet: 60), + size: setSize( + context: context, + mobile: MySpaces.s40, + tablet: 60, + ), ), ), ), diff --git a/lib/core/widgets/answer_box/styles/text_box.dart b/lib/core/widgets/answer_box/styles/text_box.dart index c6f82ec..0112890 100644 --- a/lib/core/widgets/answer_box/styles/text_box.dart +++ b/lib/core/widgets/answer_box/styles/text_box.dart @@ -1,9 +1,11 @@ +import 'package:auto_size_text/auto_size_text.dart'; import 'package:flutter/material.dart'; import 'package:hadi_hoda_flutter/common_ui/resources/my_spaces.dart'; import 'package:hadi_hoda_flutter/common_ui/resources/my_text_style.dart'; class AnswerTextBox extends StatelessWidget { - const AnswerTextBox({super.key, required this.text}); + final EdgeInsetsGeometry? padding; + const AnswerTextBox({super.key, required this.text, this.padding}); final String text; @@ -13,7 +15,7 @@ class AnswerTextBox extends StatelessWidget { clipper: WavyBannerClipper(), child: Container( height: 90, - padding: const EdgeInsets.symmetric(horizontal: MySpaces.s10), + padding: padding ?? const EdgeInsets.symmetric(horizontal: MySpaces.s10), alignment: Alignment.center, decoration: const BoxDecoration( gradient: LinearGradient( @@ -25,14 +27,17 @@ class AnswerTextBox extends StatelessWidget { ], ), ), - child: Text( + child: AutoSizeText( text, textAlign: TextAlign.center, style: MYTextStyle.matn2.copyWith( color: const Color(0XFF322386), height: 1.2, ), + minFontSize: 8, + stepGranularity: 0.5, maxLines: 5, + overflow: TextOverflow.ellipsis, ), ), ); diff --git a/lib/core/widgets/button/my_blue_button.dart b/lib/core/widgets/button/my_blue_button.dart index 7082d11..12ec3e4 100644 --- a/lib/core/widgets/button/my_blue_button.dart +++ b/lib/core/widgets/button/my_blue_button.dart @@ -13,11 +13,14 @@ class MyBlueButton extends StatelessWidget { this.onTap, this.title, this.top, + this.textStyle, + }); final VoidCallback? onTap; final String? title; final double? top; + final TextStyle? textStyle; @override Widget build(BuildContext context) { @@ -38,10 +41,18 @@ class MyBlueButton extends StatelessWidget { ), PositionedDirectional( top: top ?? setSize(context: context, mobile: MySpaces.s6, tablet: MySpaces.s20), - child: Text( - title ?? '', - style: MYTextStyle.button1.copyWith( - color: const Color(0XFF1D6EFF), + child: SizedBox( + width: 170, + height: 62, + child: FittedBox( + child: Text( + title ?? '', + maxLines: 1, + overflow: TextOverflow.ellipsis, + style: textStyle ?? MYTextStyle.button1.copyWith( + color: const Color(0XFF1D6EFF), + ), + ), ), ), ), diff --git a/lib/core/widgets/button/my_white_button.dart b/lib/core/widgets/button/my_white_button.dart index 62f9b61..8c23781 100644 --- a/lib/core/widgets/button/my_white_button.dart +++ b/lib/core/widgets/button/my_white_button.dart @@ -13,11 +13,13 @@ class MyWhiteButton extends StatelessWidget { this.onTap, this.title, this.top, + this.bgSvgMobile, }); final VoidCallback? onTap; final String? title; final double? top; + final String? bgSvgMobile; @override Widget build(BuildContext context) { @@ -31,7 +33,7 @@ class MyWhiteButton extends StatelessWidget { MyImage( image: setSize( context: context, - mobile: MyAssets.button3, + mobile: bgSvgMobile ?? MyAssets.button3, tablet: MyAssets.button3Tablet, ) ?? '', ), diff --git a/lib/core/widgets/dialog/hadith_dialog.dart b/lib/core/widgets/dialog/hadith_dialog.dart index b10e197..8742165 100644 --- a/lib/core/widgets/dialog/hadith_dialog.dart +++ b/lib/core/widgets/dialog/hadith_dialog.dart @@ -38,11 +38,8 @@ class HadithDialog extends StatelessWidget { child: Center( child: Padding( padding: EdgeInsets.symmetric( - horizontal: setSize( - context: context, - mobile: 18, - tablet: 120, - ) ?? 0, + horizontal: + setSize(context: context, mobile: 18, tablet: 120) ?? 0, ), child: Stack( clipBehavior: Clip.none, @@ -50,29 +47,64 @@ class HadithDialog extends StatelessWidget { DialogBackground( child: Builder( builder: (context) { - if(hadith.isNotEmpty){ - return ListView.separated( - itemCount: hadith.length, - separatorBuilder: (context, index) => const Divider( - height: 40, - thickness: 1, - endIndent: MySpaces.s20, - indent: MySpaces.s20, - color: Color(0xFFC2BDE4), - ), - itemBuilder: (context, index) => Text.rich( - TextSpan( - text: '${hadith[index].narratorName ?? ''}:\n', - style: MYTextStyle.titr1.copyWith( - color: const Color(0XFF494178), - ), + if (hadith.isNotEmpty) { + return Scrollbar( + child: ListView.separated( + padding: const EdgeInsets.symmetric(horizontal: 11), + itemCount: hadith.length, + separatorBuilder: (context, index) => const Divider( + height: 40, + thickness: 1, + endIndent: MySpaces.s20, + indent: MySpaces.s20, + color: Color(0xFFC2BDE4), + ), + itemBuilder: (context, index) => Column( + crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - TextSpan( - text: hadith[index].hadithText, - style: MYTextStyle.matn1.copyWith( - color: const Color(0XFF494178), + Text.rich( + textDirection: TextDirection.rtl, + TextSpan( + text: + '${hadith[index].narratorName ?? ''}:\n', + style: MYTextStyle.titr1.copyWith( + color: const Color(0XFF494178), + fontFamily: MYTextStyle.notoSansArabic, + ), + children: [ + TextSpan( + text: hadith[index].hadithText, + style: MYTextStyle.matn1.copyWith( + color: const Color(0XFF494178), + fontFamily: MYTextStyle.notoSansArabic, + fontWeight: FontWeight.w600, + fontSize: 16 + ), + ), + ], ), ), + if(hadith[index].translation?.isNotEmpty == true) ...[ + const SizedBox(height: 6), + Text( + hadith[index].translation!, + style: MYTextStyle.matn1.copyWith( + color: const Color(0XFF494178), + fontWeight: FontWeight.w600, + fontSize: 16 + ), + ), + ], if(hadith[index].sourceName?.isNotEmpty == true) ...[ + const SizedBox(height: 6), + Text( + hadith[index].sourceName!, + style: MYTextStyle.matn1.copyWith( + color: const Color(0XFF494178).withValues(alpha: .5), + fontWeight: FontWeight.w600, + fontSize: 12 + ), + ), + ] ], ), ), diff --git a/lib/core/widgets/dialog/reward_dialog.dart b/lib/core/widgets/dialog/reward_dialog.dart index e38afd5..19ab254 100644 --- a/lib/core/widgets/dialog/reward_dialog.dart +++ b/lib/core/widgets/dialog/reward_dialog.dart @@ -1,5 +1,6 @@ import 'dart:ui'; +import 'package:connectivity_plus/connectivity_plus.dart'; import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:hadi_hoda_flutter/common_ui/resources/my_animations.dart'; @@ -19,6 +20,16 @@ Future showRewardDialog({ required BuildContext context, required PrizeEntity prize, }) async { + final result = await Connectivity().checkConnectivity(); + if (!context.mounted) return; + if (result.contains(ConnectivityResult.none)) { + final messenger = ScaffoldMessenger.of( + context, + ); + messenger.removeCurrentSnackBar(); + messenger.showSnackBar(SnackBar(content: Text(context.translate.lost_connection))); + return; + } await showDialog( context: context, builder: (context) => RewardDialog(prize: prize), @@ -89,17 +100,21 @@ class RewardDialog extends StatelessWidget { children: [ Image.network( prize.imageURL ?? '', - errorBuilder: (context, error, stackTrace) => Container( - height: 300, - width: 300, - color: const Color(0XFFE0E0E0), - ), - loadingBuilder: (context, child, loadingProgress) => Container( - height: 300, - width: 300, - color: const Color(0XFFE0E0E0), - child: child, - ), + errorBuilder: + (context, error, stackTrace) => + Container( + height: 300, + width: 300, + color: const Color(0XFFE0E0E0), + ), + loadingBuilder: + (context, child, loadingProgress) => + Container( + height: 300, + width: 300, + color: const Color(0XFFE0E0E0), + child: child, + ), fit: BoxFit.cover, ), const MyImage( @@ -121,7 +136,11 @@ class RewardDialog extends StatelessWidget { behavior: HitTestBehavior.opaque, child: MyImage( image: MyAssets.closeBtn, - size: setSize(context: context, mobile: 40, tablet: 60), + size: setSize( + context: context, + mobile: 40, + tablet: 60, + ), ), ), ), diff --git a/lib/core/widgets/error/error_state.dart b/lib/core/widgets/error/error_state.dart index 23d2c4c..b821f56 100644 --- a/lib/core/widgets/error/error_state.dart +++ b/lib/core/widgets/error/error_state.dart @@ -15,12 +15,16 @@ class ErrorState extends StatelessWidget { @override Widget build(BuildContext context) { + final retryTitle = context.translate.retry; return Column( children: [ const Spacer(), Text( context.translate.lost_connection, - style: MYTextStyle.button1, + style: MYTextStyle.button1.copyWith(fontSize: 42), + maxLines: 1, + textAlign: TextAlign.center, + overflow: TextOverflow.ellipsis, ), const MyImage(image: MyAssets.error), MySpaces.s40.gapHeight, @@ -31,9 +35,12 @@ class ErrorState extends StatelessWidget { ), const Spacer(), MyBlueButton( - title: context.translate.retry, + title: retryTitle, onTap: onTap, top: setSize(context: context, mobile: MySpaces.s6, tablet: MySpaces.s16), + textStyle: MYTextStyle.button1.copyWith( + color: const Color(0XFF1D6EFF), + ), ), ], ); diff --git a/lib/core/widgets/page_transition/my_page_transition.dart b/lib/core/widgets/page_transition/my_page_transition.dart index 00703ae..14d509f 100644 --- a/lib/core/widgets/page_transition/my_page_transition.dart +++ b/lib/core/widgets/page_transition/my_page_transition.dart @@ -7,11 +7,33 @@ CustomTransitionPage myPageTransition({ }) { return CustomTransitionPage( key: key, + transitionDuration: const Duration(milliseconds: 200), + reverseTransitionDuration: const Duration(milliseconds: 180), child: child, - transitionsBuilder: (context, animation, secondaryAnimation, child) => - FadeTransition( - opacity: animation, + transitionsBuilder: (context, animation, secondaryAnimation, child) { + final curvedAnimation = CurvedAnimation( + parent: animation, + curve: Curves.fastOutSlowIn, + ); + + // Simple Android-like Material transition: fade + slight scale + final opacityAnimation = Tween( + begin: 0.7, + end: 1.0, + ).animate(curvedAnimation); + + final scaleAnimation = Tween( + begin: 0.96, + end: 1.0, + ).animate(curvedAnimation); + + return FadeTransition( + opacity: opacityAnimation, + child: ScaleTransition( + scale: scaleAnimation, child: child, ), + ); + }, ); -} +} \ No newline at end of file diff --git a/lib/core/widgets/video/my_video_player.dart b/lib/core/widgets/video/my_video_player.dart index 365898a..e33955d 100644 --- a/lib/core/widgets/video/my_video_player.dart +++ b/lib/core/widgets/video/my_video_player.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:hadi_hoda_flutter/common_ui/resources/my_colors.dart'; import 'package:hadi_hoda_flutter/core/constants/my_constants.dart'; import 'package:hadi_hoda_flutter/core/services/audio_service.dart'; +import 'package:hadi_hoda_flutter/core/utils/app_life_cycle.dart'; import 'package:hadi_hoda_flutter/core/utils/my_device.dart'; import 'package:hadi_hoda_flutter/init_bindings.dart'; import 'package:pod_player/pod_player.dart'; @@ -16,7 +17,8 @@ class MyVideoPlayer extends StatefulWidget { } class _MyVideoPlayerState extends State { - late final PodPlayerController _controller; + PodPlayerController? _controller; + bool _isPlayerReady = false; final AudioService _mainAudioService = locator( instanceName: MyConstants.mainAudioService, ); @@ -27,8 +29,17 @@ class _MyVideoPlayerState extends State { @override void initState() { super.initState(); - _mainAudioService.stop(); - _effectAudioService.stop(); + _initVideoSession(); + } + + Future _initVideoSession() async { + AppLifeCycleController.suppressAutoResumeAudio = true; + await MyDevice.setLandscape(); + await Future.wait([ + _mainAudioService.pause(), + _effectAudioService.pause(), + ]); + if (!mounted) return; _controller = PodPlayerController( podPlayerConfig: const PodPlayerConfig( autoPlay: false, @@ -37,13 +48,17 @@ class _MyVideoPlayerState extends State { ), playVideoFrom: PlayVideoFrom.network(widget.videoURL ?? ''), )..initialise(); + if (!mounted) return; + setState(() { + _isPlayerReady = true; + }); } @override void dispose() { - _controller.dispose(); + _controller?.dispose(); + AppLifeCycleController.suppressAutoResumeAudio = false; _mainAudioService.play(); - _effectAudioService.play(); MyDevice.setPortrait(); super.dispose(); } @@ -57,20 +72,21 @@ class _MyVideoPlayerState extends State { backgroundColor: MyColors.transparent, foregroundColor: MyColors.white, ), - body: PodVideoPlayer( - controller: _controller, - matchVideoAspectRatioToFrame: true, - matchFrameAspectRatioToVideo: true, - videoAspectRatio: _controller.videoPlayerValue?.aspectRatio ?? 16 / 9, - podProgressBarConfig: const PodProgressBarConfig(), - onToggleFullScreen: (isFullScreen) async { - if (isFullScreen) { - await MyDevice.setAllOrientations(); - } else { - await MyDevice.setPortrait(); - } - }, - ), + body: _isPlayerReady + ? PodVideoPlayer( + controller: _controller!, + matchVideoAspectRatioToFrame: true, + matchFrameAspectRatioToVideo: true, + videoAspectRatio: + _controller?.videoPlayerValue?.aspectRatio ?? 16 / 9, + podProgressBarConfig: const PodProgressBarConfig(), + onToggleFullScreen: (isFullScreen) async { + await MyDevice.setLandscape(); + }, + ) + : const Center( + child: CircularProgressIndicator(), + ), ); } } diff --git a/lib/features/download/presentation/ui/download_page.dart b/lib/features/download/presentation/ui/download_page.dart index daa3b67..578cfc3 100644 --- a/lib/features/download/presentation/ui/download_page.dart +++ b/lib/features/download/presentation/ui/download_page.dart @@ -16,6 +16,7 @@ import 'package:hadi_hoda_flutter/features/download/presentation/bloc/download_b import 'package:hadi_hoda_flutter/features/download/presentation/bloc/download_event.dart'; import 'package:hadi_hoda_flutter/features/download/presentation/bloc/download_state.dart'; import 'package:hadi_hoda_flutter/features/download/presentation/ui/widgets/download_loading_widget.dart'; +import 'package:wakelock_plus/wakelock_plus.dart'; /// Download page that supports downloading any combination of media types. /// @@ -35,6 +36,7 @@ class _DownloadPageState extends State { @override void initState() { super.initState(); + WakelockPlus.enable(); final bloc = context.read(); if (!bloc.isDownloading) { @@ -46,6 +48,12 @@ class _DownloadPageState extends State { } } + @override + void dispose() { + WakelockPlus.disable(); + super.dispose(); + } + @override Widget build(BuildContext context) { return Scaffold( @@ -74,7 +82,7 @@ class _DownloadPageState extends State { if (widget.config.redirectTo == Routes.homePage) { context.goNamed(Routes.homePage); } else { - context.pushNamed( + context.pushReplacementNamed( widget.config.redirectTo, pathParameters: widget.config.routeParams, ); @@ -130,6 +138,8 @@ class _DownloadPageState extends State { Widget _text(BuildContext context) { return PositionedDirectional( bottom: 130, + start: 0, + end: 0, child: Column( spacing: MySpaces.s6, crossAxisAlignment: CrossAxisAlignment.stretch, @@ -149,7 +159,7 @@ class _DownloadPageState extends State { .loadingStream, builder: (context, snapshot) => Text( - 'Downloading ...${snapshot.data?.downloadedLevels}/${widget + '${context.translate.downloading} ...${snapshot.data?.downloadedLevels}/${widget .config.downloadToLevel}', textAlign: TextAlign.center, style: MYTextStyle.matn3, diff --git a/lib/features/guider/presentation/ui/guider_page.dart b/lib/features/guider/presentation/ui/guider_page.dart index b3ddd63..fabff6b 100644 --- a/lib/features/guider/presentation/ui/guider_page.dart +++ b/lib/features/guider/presentation/ui/guider_page.dart @@ -1,5 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:gif/gif.dart'; +import 'package:hadi_hoda_flutter/common_ui/resources/my_animations.dart'; import 'package:hadi_hoda_flutter/common_ui/resources/my_assets.dart'; import 'package:hadi_hoda_flutter/common_ui/resources/my_audios.dart'; import 'package:hadi_hoda_flutter/common_ui/resources/my_colors.dart'; @@ -14,7 +16,6 @@ import 'package:hadi_hoda_flutter/core/widgets/animations/slide_anim.dart'; import 'package:hadi_hoda_flutter/core/widgets/animations/slide_down_fade.dart'; import 'package:hadi_hoda_flutter/core/widgets/animations/slide_up_fade.dart'; import 'package:hadi_hoda_flutter/core/widgets/answer_box/answer_box_showcase.dart'; -import 'package:hadi_hoda_flutter/core/widgets/images/my_image.dart'; import 'package:hadi_hoda_flutter/core/widgets/pop_scope/my_pop_scope.dart'; import 'package:hadi_hoda_flutter/core/widgets/showcase/my_showcase_widget.dart'; import 'package:hadi_hoda_flutter/features/guider/presentation/bloc/guider_bloc.dart'; @@ -223,10 +224,12 @@ class _GuiderPageState extends State with TickerProviderStateMixin { .showCaseKey['guide_key']!, description: context.translate.showcase_guide, type: ShowcaseTooltipType.top, - child: MyImage( - image: MyAssets.globe, - fit: BoxFit.cover, - size: setSize(context: context, tablet: 120), + child: Gif( + image: const AssetImage(MyAnimations.globeStateNormal), + fps: 10, + autostart: Autostart.loop, + width: setSize(context: context, mobile: 80, tablet: 120) ?? 80, + height: setSize(context: context, mobile: 80, tablet: 120) ?? 80, ), ), PositionedDirectional( diff --git a/lib/features/home/presentation/ui/home_page.dart b/lib/features/home/presentation/ui/home_page.dart index 3c5b8b6..587e8bf 100644 --- a/lib/features/home/presentation/ui/home_page.dart +++ b/lib/features/home/presentation/ui/home_page.dart @@ -9,14 +9,14 @@ import 'package:hadi_hoda_flutter/core/utils/local_storage.dart'; import 'package:hadi_hoda_flutter/core/utils/my_localization.dart'; import 'package:hadi_hoda_flutter/core/utils/screen_size.dart'; import 'package:hadi_hoda_flutter/core/utils/set_platform_size.dart'; -import 'package:hadi_hoda_flutter/core/widgets/animations/slide_down_fade.dart'; -import 'package:hadi_hoda_flutter/core/widgets/animations/slide_up_fade.dart'; +import 'package:hadi_hoda_flutter/core/widgets/button/my_white_button.dart'; import 'package:hadi_hoda_flutter/core/widgets/button/my_yellow_button.dart'; import 'package:hadi_hoda_flutter/core/widgets/images/my_image.dart'; import 'package:hadi_hoda_flutter/core/widgets/inkwell/my_inkwell.dart'; import 'package:hadi_hoda_flutter/core/widgets/pop_scope/my_pop_scope.dart'; import 'package:hadi_hoda_flutter/features/download/domain/entities/download_entity.dart'; import 'package:hadi_hoda_flutter/features/home/presentation/bloc/home_bloc.dart'; +import 'package:hadi_hoda_flutter/features/home/presentation/ui/widgets/payment_dialog.dart'; class HomePage extends StatefulWidget { const HomePage({super.key}); @@ -26,6 +26,8 @@ class HomePage extends StatefulWidget { } class _HomePageState extends State { + static const double _muteThreshold = 0.001; + @override void initState() { super.initState(); @@ -51,6 +53,10 @@ class _HomePageState extends State { @override Widget build(BuildContext context) { + final lang = + LocalStorage.readData(key: MyConstants.selectLanguage) ?? + MyConstants.defaultLanguage; + late final isArabic = lang == 'ar'; return Scaffold( body: MyPopScope( child: DecoratedBox( @@ -64,9 +70,9 @@ class _HomePageState extends State { child: Stack( alignment: Alignment.center, children: [ - _music(context), + _topBtns(context, isArabic), _image(context), - _bottomBtns(context), + _bottomBtns(context, isArabic), ], ), ), @@ -76,21 +82,16 @@ class _HomePageState extends State { } Widget _music(BuildContext context) { - return PositionedDirectional( - top: setPlatform(android: MySpaces.s36, iOS: 50), - end: MySpaces.s16, - child: SlideDownFade( - delay: const Duration(milliseconds: 100), - child: StreamBuilder( - initialData: 1, - stream: context.read().volumeStream, - builder: (context, snapshot) => MyInkwell( - onTap: () => context.read().changeMute(), - child: MyImage( - image: snapshot.data == 0 ? MyAssets.musicOff : MyAssets.musicOn, - size: setSize(context: context, tablet: 100), - ), - ), + return StreamBuilder( + initialData: 1, + stream: context.read().volumeStream, + builder: (context, snapshot) => MyInkwell( + onTap: () => context.read().changeMute(), + child: MyImage( + image: (snapshot.data ?? 1) <= _muteThreshold + ? MyAssets.musicOff + : MyAssets.musicOn, + size: setSize(context: context, tablet: 100), ), ), ); @@ -112,39 +113,105 @@ class _HomePageState extends State { ); } - Positioned _bottomBtns(BuildContext context) { + Positioned _topBtns(BuildContext context, bool isArabic) { + return Positioned( + top: setPlatform(android: MySpaces.s36, iOS: 50), + right: 20, + left: 20, + child: !isArabic + ? Align( + alignment: AlignmentDirectional.topStart, + child: _music(context), + ) + : Row( + children: [ + MyInkwell( + onTap: () => context.read().showAboutUs(context), + child: MyImage( + image: MyAssets.theme, + size: setSize(context: context, tablet: 100), + ), + ), + const SizedBox(width: 12), + + MyInkwell( + onTap: () => + context.read().goToLanguagePage(context), + child: MyImage( + image: MyAssets.language, + size: setSize(context: context, tablet: 100), + ), + ), + const Spacer(), + + _music(context), + ], + ), + ); + } + + Positioned _bottomBtns(BuildContext context, bool isArabic) { return Positioned( bottom: setPlatform(android: MySpaces.s40, iOS: MySpaces.s0), left: MySpaces.s16, right: MySpaces.s16, child: SafeArea( - child: SlideUpFade( - delay: const Duration(milliseconds: 100), - child: Row( - crossAxisAlignment: CrossAxisAlignment.end, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - MyInkwell( - onTap: () => context.read().goToLanguagePage(context), - child: MyImage( - image: MyAssets.language, - size: setSize(context: context, tablet: 100), - ), - ), - MyYellowButton( - onTap: () => context.read().goToLevelPage(context), - title: context.translate.start, - ), - MyInkwell( - onTap: () => context.read().showAboutUs(context), - child: MyImage( - image: MyAssets.theme, - size: setSize(context: context, tablet: 100), - ), + child: isArabic + ? Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Expanded( + flex: 4, + child: MyYellowButton( + onTap: () { + context.read().goToLevelPage(context); + }, + title: context.translate.start, + ), + ), + const SizedBox(width: 20), + Expanded( + flex: 3, + child: MyWhiteButton( + title: 'تبرع', + onTap: () { + showDialog( + context: context, + builder: (_) => const PaymentDialog(), + ); + }, + bgSvgMobile: MyAssets.button35, + ), + ), + ], + ) + : Row( + crossAxisAlignment: CrossAxisAlignment.end, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + MyInkwell( + onTap: () => + context.read().goToLanguagePage(context), + child: MyImage( + image: MyAssets.language, + size: setSize(context: context, tablet: 100), + ), + ), + MyYellowButton( + onTap: () { + context.read().goToLevelPage(context); + }, + title: context.translate.start, + ), + MyInkwell( + onTap: () => context.read().showAboutUs(context), + child: MyImage( + image: MyAssets.theme, + size: setSize(context: context, tablet: 100), + ), + ), + ], ), - ], - ), - ), ), ); } diff --git a/lib/features/home/presentation/ui/widgets/payment_dialog.dart b/lib/features/home/presentation/ui/widgets/payment_dialog.dart new file mode 100644 index 0000000..35bf962 --- /dev/null +++ b/lib/features/home/presentation/ui/widgets/payment_dialog.dart @@ -0,0 +1,75 @@ +import 'package:flutter/material.dart'; +import 'package:hadi_hoda_flutter/common_ui/resources/my_text_style.dart'; +import 'package:url_launcher/url_launcher.dart'; + +class PaymentDialog extends StatelessWidget { + const PaymentDialog({super.key}); + + static const String _paymentUrl = + 'https://buy.stripe.com/cNi6oI9EF5Y44oQ5nb4AU0V'; + + Future _launchPayment() async { + final Uri url = Uri.parse(_paymentUrl); + if (await canLaunchUrl(url)) { + await launchUrl(url); + } + } + + @override + Widget build(BuildContext context) { + return Dialog( + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), + child: Padding( + padding: const EdgeInsets.all(20), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.end, + children: [ + const Text( + "ساهم معنا في تطوير مشاريعنا القادمة", + textAlign: TextAlign.center, + style: TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + fontFamily: MYTextStyle.notoSansArabic, + ), + ), + + const SizedBox(height: 16), + + const Text( + '''قال رسول الله (صلى الله عليه وآله): "إذا مات ابن آدم انقطع عمله إلا من ثلاث: صدقة جارية، أو علم ينتفع به، أو ولد صالح يدعو له". + +إن دعمكم المستمر لتطوير مشاريعنا القادمة هو التجسيد الحقيقي لـ «الباقيات الصالحات». + +تخيل أن كل طفلٍ في أي بقعةٍ من العالم، وبأي لسانٍ كان، يتعلم الصلاة أو يكتسب أدباً من آداب الدين بفضل مساهمتكم؛ سيُكتب ذلك في ميزان حسناتكم «علماً يُنتفع به»، وصدقةً جاريةً لا ينقطع ثوابها أبدَ الآبدين.''', + textAlign: TextAlign.right, + style: TextStyle( + fontSize: 15, + height: 1.6, + fontFamily: MYTextStyle.notoSansArabic, + ), + ), + + const SizedBox(height: 24), + + SizedBox( + width: double.infinity, + child: FilledButton( + onPressed: _launchPayment, + child: const Text( + "تبرع الآن", + style: TextStyle( + fontFamily: MYTextStyle.notoSansArabic, + fontSize: 17, + fontWeight: FontWeight.w600, + ), + ), + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/features/language/data/repository_impl/language_repository_impl.dart b/lib/features/language/data/repository_impl/language_repository_impl.dart index d188e21..fa567e6 100644 --- a/lib/features/language/data/repository_impl/language_repository_impl.dart +++ b/lib/features/language/data/repository_impl/language_repository_impl.dart @@ -13,12 +13,18 @@ class LanguageRepositoryImpl implements ILanguageRepository { LanguageRepositoryImpl(this.languageDatasource); + List? _langs; + @override Future, MyException>> getLanguages() async { try { + if(_langs != null) { + return DataState.success(_langs!); + } final languages = await languageDatasource.getLanguages(); // Explicitly convert to a List to prevent runtime type errors. - return DataState.success(List.from(languages)); + _langs = List.from(languages); + return DataState.success(_langs!); } on MyException catch (e) { return DataState.error(e); } diff --git a/lib/features/language/presentation/bloc/language_bloc.dart b/lib/features/language/presentation/bloc/language_bloc.dart index 82411d0..efa4bfb 100644 --- a/lib/features/language/presentation/bloc/language_bloc.dart +++ b/lib/features/language/presentation/bloc/language_bloc.dart @@ -34,7 +34,7 @@ class LanguageBloc extends Bloc { if (result.isSuccess) { final languages = result.data!; final selectedResult = await _getSelectedLanguageUseCase(languages); - + emit( state.copyWith( getLanguagesStatus: const BaseComplete(''), diff --git a/lib/features/language/presentation/ui/language_page.dart b/lib/features/language/presentation/ui/language_page.dart index a934b2a..0427f4b 100644 --- a/lib/features/language/presentation/ui/language_page.dart +++ b/lib/features/language/presentation/ui/language_page.dart @@ -3,7 +3,6 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; import 'package:hadi_hoda_flutter/common_ui/resources/my_assets.dart'; -import 'package:hadi_hoda_flutter/common_ui/resources/my_colors.dart'; import 'package:hadi_hoda_flutter/common_ui/resources/my_spaces.dart'; import 'package:hadi_hoda_flutter/common_ui/resources/my_text_style.dart'; import 'package:hadi_hoda_flutter/core/constants/my_constants.dart'; @@ -13,6 +12,7 @@ import 'package:hadi_hoda_flutter/core/utils/my_localization.dart'; import 'package:hadi_hoda_flutter/core/utils/screen_size.dart'; import 'package:hadi_hoda_flutter/core/utils/set_platform_size.dart'; import 'package:hadi_hoda_flutter/core/widgets/button/my_blue_button.dart'; +import 'package:hadi_hoda_flutter/core/widgets/error/error_state.dart'; import 'package:hadi_hoda_flutter/core/widgets/images/my_image.dart'; import 'package:hadi_hoda_flutter/features/app/presentation/bloc/app_bloc.dart'; import 'package:hadi_hoda_flutter/features/app/presentation/bloc/app_event.dart'; @@ -24,6 +24,9 @@ import 'package:hadi_hoda_flutter/features/language/presentation/bloc/language_e import 'package:hadi_hoda_flutter/features/language/presentation/bloc/language_state.dart'; import 'package:wheel_chooser/wheel_chooser.dart'; +import '../../../../core/widgets/animations/rotation_anim.dart'; +import '../../domain/entity/language_entity.dart'; + class LanguagePage extends StatefulWidget { const LanguagePage({super.key}); @@ -34,10 +37,18 @@ class LanguagePage extends StatefulWidget { class _LanguagePageState extends State { final controller = FixedExtentScrollController(initialItem: 50); bool _isEnabled = false; + LanguageEntity? _selectedLanguage; @override void initState() { super.initState(); + _selectedLanguage = + context.read().state.selectedLanguage ?? + const LanguageEntity( + code: 'en', + title: '', + displayName: 'English (English', + ); context.read().add(const GetLanguagesEvent()); } @@ -74,7 +85,9 @@ class _LanguagePageState extends State { if (state.languages.isNotEmpty && !_isEnabled) { int targetIndex = 0; if (state.selectedLanguage != null) { - targetIndex = state.languages.indexWhere((l) => l.code == state.selectedLanguage!.code); + targetIndex = state.languages.indexWhere( + (l) => l.code == state.selectedLanguage!.code, + ); if (targetIndex == -1) targetIndex = 0; } @@ -88,8 +101,8 @@ class _LanguagePageState extends State { if (state.selectedLanguage == null && context.mounted) { context.read().add( - SelectLanguageEvent(state.languages[targetIndex]), - ); + SelectLanguageEvent(state.languages[targetIndex]), + ); } } setState(() => _isEnabled = true); @@ -97,17 +110,41 @@ class _LanguagePageState extends State { }, builder: (context, state) { if (state.getLanguagesStatus is BaseLoading) { - return const Center(child: CircularProgressIndicator()); + return Center(child: _loading(context)); + } + if (state.getLanguagesStatus is BaseError) { + return Padding( + padding: EdgeInsets.symmetric( + vertical: + MediaQuery.viewPaddingOf(context).bottom + MySpaces.s16, + horizontal: 60, + ), + child: ErrorState( + onTap: () { + context.read().add(const GetLanguagesEvent()); + }, + ), + ); } - final double itemSize = setSize(context: context, mobile: 45, tablet: 60) ?? 45; - + final double itemSize = + setSize(context: context, mobile: 45, tablet: 60) ?? 45; + final selectStyle = MYTextStyle.titr1.copyWith( + // Keep same size as before (WheelChooser.choices) + fontSize: itemSize * 0.34, + color: Colors.white, + ); + final unSelectStyle = MYTextStyle.titr1.copyWith( + fontSize: itemSize * 0.34, + color: Colors.white.withValues(alpha: 0.5), + ); return Padding( padding: EdgeInsets.only( left: setSize(context: context, mobile: 50, tablet: 0.3.w) ?? 0, - right: setSize(context: context, mobile: 50, tablet: 0.3.w) ?? 0, + right: + setSize(context: context, mobile: 50, tablet: 0.3.w) ?? 0, bottom: MySpaces.s40 + MediaQuery.paddingOf(context).bottom, - top: 100, + top: 60, ), child: Column( children: [ @@ -133,43 +170,77 @@ class _LanguagePageState extends State { Center( child: Container( width: double.infinity, - alignment: Alignment.centerLeft, + alignment: Alignment.centerLeft, height: itemSize, - padding: const EdgeInsets.symmetric(horizontal: 18), + padding: const EdgeInsets.symmetric( + horizontal: 18, + ), decoration: BoxDecoration( color: Colors.white.withValues(alpha: .2), borderRadius: BorderRadius.circular(12), ), - child: const MyImage(image: MyAssets.check), + child: Transform.translate( + offset: + (state + .selectedLanguage + ?.displayName + .length ?? + 0) > + 21 + ? const Offset(-10, 0) + : Offset.zero, + child: const MyImage(image: MyAssets.check), + ), ), ), - WheelChooser.choices( + WheelChooser.custom( controller: controller, + datas: null, startPosition: null, perspective: 0.00000001, listWidth: double.infinity, listHeight: double.infinity, itemSize: itemSize, isInfinite: true, - onChoiceChanged: (value) { - if (value != null && value is! String) { - context.read().add( - SelectLanguageEvent(value), - ); - } + onValueChanged: (position) { + if (state.languages.isEmpty) return; + final pos = position is int + ? position + : int.tryParse(position.toString()) ?? 0; + final index = + ((pos % state.languages.length) + + state.languages.length) % + state.languages.length; + final lang = state.languages[index]; + setState(() { + _selectedLanguage = lang; + }); }, - selectTextStyle: MYTextStyle.titr1.copyWith( - fontSize: itemSize * 0.34, - color: Colors.white, - ), - unSelectTextStyle: MYTextStyle.titr1.copyWith( - fontSize: itemSize * 0.34, - color: Colors.white.withOpacity(0.5), - ), - choices: state.languages.map((lang) { - return WheelChoice( - value: lang, - title: lang.displayName, + children: state.languages.map((lang) { + final isSelected = + _selectedLanguage?.code == lang.code; + return Center( + child: Padding( + padding: const EdgeInsets.symmetric( + horizontal: 36, + ), + child: MediaQuery( + data: MediaQuery.of(context).copyWith( + textScaler: const TextScaler.linear(1.5), + ), + child: AutoSizeText( + lang.displayName, + textAlign: TextAlign.center, + maxLines: 2, + minFontSize: 10, + stepGranularity: 0.5, + overflow: TextOverflow.ellipsis, + style: isSelected + ? selectStyle + : unSelectStyle, + ), + ), + ), ); }).toList(), ), @@ -188,6 +259,15 @@ class _LanguagePageState extends State { ); } + Widget _loading(BuildContext context) { + return RotationAnim( + child: MyImage( + image: MyAssets.loading, + size: setSize(context: context, mobile: 110, tablet: 145), + ), + ); + } + Widget _title(BuildContext context) { return Row( spacing: MySpaces.s10, @@ -208,15 +288,18 @@ class _LanguagePageState extends State { Widget _btn(BuildContext context, LanguageState state) { return MyBlueButton( - onTap: (_isEnabled && state.selectedLanguage != null) + onTap: (_isEnabled && _selectedLanguage != null) ? () async { + context.read().add( + SelectLanguageEvent(_selectedLanguage!), + ); final downloadBloc = context.read(); // 1. Cancel any previous downloads. downloadBloc.add(CancelDownloadEvent()); // 2. Update App Locale context.read().add( - ChangeLocaleEvent(state.selectedLanguage!.locale), + ChangeLocaleEvent(_selectedLanguage!.locale), ); await Future.delayed(const Duration(milliseconds: 80)); final lastDownloadedLevel = await downloadBloc diff --git a/lib/features/level/data/datasource/level_datasource.dart b/lib/features/level/data/datasource/level_datasource.dart index 03f66f8..3d5daaf 100644 --- a/lib/features/level/data/datasource/level_datasource.dart +++ b/lib/features/level/data/datasource/level_datasource.dart @@ -1,8 +1,12 @@ import 'package:flutter/foundation.dart'; +import 'package:hadi_hoda_flutter/core/constants/my_api.dart'; import 'package:hadi_hoda_flutter/core/constants/my_constants.dart'; import 'package:hadi_hoda_flutter/core/error_handler/my_exception.dart'; +import 'package:hadi_hoda_flutter/core/network/http_request.dart'; import 'package:hadi_hoda_flutter/core/params/level_params.dart'; +import 'package:hadi_hoda_flutter/core/response/base_response.dart'; import 'package:hadi_hoda_flutter/core/utils/local_storage.dart'; +import 'package:hadi_hoda_flutter/features/level/data/model/node_model.dart'; import 'package:hadi_hoda_flutter/features/level/domain/entity/node_entity.dart'; import 'package:hadi_hoda_flutter/features/level/domain/entity/total_data_entity.dart'; import 'package:hive/hive.dart'; @@ -13,24 +17,66 @@ abstract class ILevelDatasource { /// Local class LocalLevelDatasourceImpl implements ILevelDatasource { - const LocalLevelDatasourceImpl(); + final IHttpRequest httpRequest; + + LocalLevelDatasourceImpl(this.httpRequest); + + String get _selectedLanguage => + LocalStorage.readData(key: MyConstants.selectLanguage) ?? + MyConstants.defaultLanguage; + + Future _syncRemoteLevelsToLocal() async { + final response = await httpRequest.get( + path: MyApi.levels, + queryParameters: {'lang': _selectedLanguage}, + ); + final List levels = BaseResponse.getDataList( + response?['path'], + (json) => NodeModel.fromJson(json), + ); + await LocalStorage.saveData( + key: MyConstants.maxLevelCount, + value: '${levels.length}', + ); + + final Box levelBox = Hive.box(MyConstants.levelBox); + dynamic existingKey; + for (final key in levelBox.keys) { + final item = levelBox.get(key); + if (item?.code == _selectedLanguage) { + existingKey = key; + break; + } + } + + if (existingKey != null) { + await levelBox.put( + existingKey, + TotalDataEntity(code: _selectedLanguage, nodes: levels), + ); + return; + } + + await levelBox.add(TotalDataEntity(code: _selectedLanguage, nodes: levels)); + } @override Future> getLevels({required LevelParams params}) async { try { - final String selectedLanguage = LocalStorage.readData( - key: MyConstants.selectLanguage) ?? MyConstants.defaultLanguage; + if (params.forceRemote) { + await _syncRemoteLevelsToLocal(); + } + final Box levelBox = Hive.box(MyConstants.levelBox); final TotalDataEntity findData = levelBox.values.firstWhere( - (e) => e.code == selectedLanguage, + (e) => e.code == _selectedLanguage, orElse: () => TotalDataEntity(), ); - debugPrint("nodesCount : ${findData.nodes?.first.level?.questions}"); return findData.nodes ?? []; - } catch (e,s) { + } catch (e, s) { debugPrint(e.toString()); debugPrint(s.toString()); - throw const MyException(errorMessage: 'Operation Failed'); + throw const MyException(errorMessage: 'Operation Failed'); } } } \ No newline at end of file diff --git a/lib/features/level/domain/entity/node_entity.dart b/lib/features/level/domain/entity/node_entity.dart index 6c9fa84..ada33d2 100644 --- a/lib/features/level/domain/entity/node_entity.dart +++ b/lib/features/level/domain/entity/node_entity.dart @@ -9,11 +9,14 @@ enum NodeType { @HiveField(0) level, @HiveField(1) - prize; + prize, + @HiveField(2) + comingSoon; static Map get fromJson => { 'level': NodeType.level, 'prize': NodeType.prize, + 'coming_soon': NodeType.comingSoon, }; } diff --git a/lib/features/level/domain/entity/node_entity.g.dart b/lib/features/level/domain/entity/node_entity.g.dart index 41204de..e30471d 100644 --- a/lib/features/level/domain/entity/node_entity.g.dart +++ b/lib/features/level/domain/entity/node_entity.g.dart @@ -57,6 +57,8 @@ class NodeTypeAdapter extends TypeAdapter { return NodeType.level; case 1: return NodeType.prize; + case 2: + return NodeType.comingSoon; default: return NodeType.level; } @@ -71,6 +73,9 @@ class NodeTypeAdapter extends TypeAdapter { case NodeType.prize: writer.writeByte(1); break; + case NodeType.comingSoon: + writer.writeByte(2); + break; } } diff --git a/lib/features/level/presentation/bloc/level_bloc.dart b/lib/features/level/presentation/bloc/level_bloc.dart index 851cbb6..90eb0c5 100644 --- a/lib/features/level/presentation/bloc/level_bloc.dart +++ b/lib/features/level/presentation/bloc/level_bloc.dart @@ -56,7 +56,7 @@ class LevelBloc extends Bloc { final GetLastDownloadedLevel _getLastDownloadedLevel; /// ------------Variables------------ - final List locationList = [ + List locationList = [ LevelLocation( bottom: setSize(context: MyContext.get, mobile: -0.03.h, tablet: -0.03.h), left: setSize(context: MyContext.get, mobile: 0.1.w, tablet: 0.2.w), @@ -64,7 +64,7 @@ class LevelBloc extends Bloc { ), LevelLocation( bottom: setSize(context: MyContext.get, mobile: 0.03.h, tablet: 0.1.h), - left: setSize(context: MyContext.get, mobile: 0.28.w, tablet: 0.4.w), + left: setSize(context: MyContext.get, mobile: 0.28.w, tablet: 0.38.w), index: 1, ), LevelLocation( @@ -73,48 +73,48 @@ class LevelBloc extends Bloc { index: 2, ), LevelLocation( - bottom: setSize(context: MyContext.get, mobile: 0.2.h, tablet: 0.38.h), + bottom: setSize(context: MyContext.get, mobile: 0.2.h, tablet: 0.33.h), left: setSize(context: MyContext.get, mobile: 0.3.w, tablet: 0.3.w), index: 3, ), LevelLocation( - bottom: setSize(context: MyContext.get, mobile: 0.24.h, tablet: 0.47.h), - left: setSize(context: MyContext.get, mobile: 0.5.w, tablet: 0.5.w), + bottom: setSize(context: MyContext.get, mobile: 0.24.h, tablet: 0.4.h), + left: setSize(context: MyContext.get, mobile: 0.5.w, tablet: 0.55.w), index: 4, ), LevelLocation( bottom: setSize(context: MyContext.get, mobile: 0.33.h, tablet: 0.55.h), - left: setSize(context: MyContext.get, mobile: 0.63.w, tablet: 0.8.w), + left: setSize(context: MyContext.get, mobile: 0.659.w, tablet: 0.77.w), index: 5, ), LevelLocation( bottom: setSize(context: MyContext.get, mobile: 0.45.h, tablet: 0.8.h), - left: setSize(context: MyContext.get, mobile: 0.57.w, tablet: 0.8.w), + left: setSize(context: MyContext.get, mobile: 0.59.w, tablet: 0.68.w), index: 6, ), LevelLocation( - bottom: setSize(context: MyContext.get, mobile: 0.55.h, tablet: 1.1.h), - left: setSize(context: MyContext.get, mobile: 0.57.w, tablet: 0.77.w), + bottom: setSize(context: MyContext.get, mobile: 0.55.h, tablet: .951.h), + left: setSize(context: MyContext.get, mobile: 0.584.w, tablet: 0.64.w), index: 7, ), LevelLocation( - bottom: setSize(context: MyContext.get, mobile: 0.6.h, tablet: 1.2.h), - left: setSize(context: MyContext.get, mobile: 0.4.w, tablet: 0.65.w), + bottom: setSize(context: MyContext.get, mobile: 0.61.h, tablet: 1.03.h), + left: setSize(context: MyContext.get, mobile: 0.4.w, tablet: 0.18.w), index: 8, ), LevelLocation( - bottom: setSize(context: MyContext.get, mobile: 0.63.h, tablet: 1.25.h), - left: setSize(context: MyContext.get, mobile: 0.2.w, tablet: 0.3.w), + bottom: setSize(context: MyContext.get, mobile: 0.64.h, tablet: 1.19.h), + left: setSize(context: MyContext.get, mobile: 0.2.w, tablet: 0.22.w), index: 9, ), LevelLocation( - bottom: setSize(context: MyContext.get, mobile: 0.7.h, tablet: 1.38.h), - left: setSize(context: MyContext.get, mobile: 0, tablet: 0.04.w), + bottom: setSize(context: MyContext.get, mobile: 0.7.h, tablet: 1.25.h), + left: setSize(context: MyContext.get, mobile: 0, tablet: 0.53.w), index: 10, ), LevelLocation( - bottom: setSize(context: MyContext.get, mobile: 0.75.h, tablet: 1.46.h), - left: setSize(context: MyContext.get, mobile: 0.15.w, tablet: 0.22.w), + bottom: setSize(context: MyContext.get, mobile: 0.75.h, tablet: 1.39.h), + left: setSize(context: MyContext.get, mobile: 0.15.w, tablet: 0.58.w), index: 11, ), LevelLocation( @@ -287,29 +287,39 @@ class LevelBloc extends Bloc { final int currentLevel = int.parse( LocalStorage.readData(key: MyConstants.currentLevel) ?? '1', ); - await _getLeveslUseCase(LevelParams()).then((value) { - value.fold((data) async { - nodeList.addAll(data); - try { - emit( - state.copyWith( - getLevelStatus: const BaseComplete(''), - chooseLevel: data - .firstWhere((e) => e.level?.order == currentLevel) - .level, - ), - ); - } catch (e) { - emit( - state.copyWith( - getLevelStatus: const BaseComplete(''), - chooseLevel: LevelEntity(), - ), - ); - } - add(StartScrollEvent()); + final localResult = await _getLeveslUseCase(LevelParams()); + localResult.fold((data) async { + nodeList + ..clear() + ..addAll(data); + try { + emit( + state.copyWith( + getLevelStatus: const BaseComplete(''), + chooseLevel: data + .firstWhere((e) => e.level?.order == currentLevel) + .level, + ), + ); + } catch (e) { + emit( + state.copyWith( + getLevelStatus: const BaseComplete(''), + chooseLevel: LevelEntity(), + ), + ); + } + add(StartScrollEvent()); + }, (error) {}); + + if (nodeList.isNotEmpty && nodeList.last.nodeType == NodeType.comingSoon) { + final remoteResult = await _getLeveslUseCase(LevelParams(forceRemote: true)); + remoteResult.fold((data) { + nodeList + ..clear() + ..addAll(data); }, (error) {}); - }); + } } FutureOr _startScrollEvent( diff --git a/lib/features/level/presentation/ui/level_page.dart b/lib/features/level/presentation/ui/level_page.dart index aca3773..4a00d56 100644 --- a/lib/features/level/presentation/ui/level_page.dart +++ b/lib/features/level/presentation/ui/level_page.dart @@ -1,3 +1,4 @@ +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:hadi_hoda_flutter/common_ui/resources/my_assets.dart'; @@ -13,15 +14,14 @@ import 'package:hadi_hoda_flutter/core/widgets/animations/ship_anim.dart'; import 'package:hadi_hoda_flutter/core/widgets/images/my_image.dart'; import 'package:hadi_hoda_flutter/core/widgets/inkwell/my_inkwell.dart'; import 'package:hadi_hoda_flutter/core/widgets/pop_scope/my_pop_scope.dart'; -import 'package:hadi_hoda_flutter/features/download/domain/entities/download_entity.dart'; import 'package:hadi_hoda_flutter/features/download/presentation/bloc/download_bloc.dart'; import 'package:hadi_hoda_flutter/features/download/presentation/bloc/download_event.dart'; -import 'package:hadi_hoda_flutter/features/download/presentation/bloc/download_state.dart'; -import 'package:hadi_hoda_flutter/features/download/presentation/ui/widgets/download_loading_widget.dart'; import 'package:hadi_hoda_flutter/features/level/domain/entity/level_entity.dart'; +import 'package:hadi_hoda_flutter/features/level/domain/entity/node_entity.dart'; import 'package:hadi_hoda_flutter/features/level/presentation/bloc/level_bloc.dart'; import 'package:hadi_hoda_flutter/features/level/presentation/bloc/level_event.dart'; import 'package:hadi_hoda_flutter/features/level/presentation/bloc/level_state.dart'; +import 'package:hadi_hoda_flutter/features/level/presentation/ui/widgets/coming_soon_level.dart'; import 'package:hadi_hoda_flutter/features/level/presentation/ui/widgets/diamond_level.dart'; import 'package:hadi_hoda_flutter/features/level/presentation/ui/widgets/level_path.dart'; import 'package:hadi_hoda_flutter/features/level/presentation/ui/widgets/node_widget.dart'; @@ -35,10 +35,12 @@ class LevelPage extends StatefulWidget { } class _LevelPageState extends State { + static const double _muteThreshold = 0.001; + @override void initState() { super.initState(); - _triggerRemainingLevelsDownload(); + if(!kDebugMode)_triggerRemainingLevelsDownload(); } void _triggerRemainingLevelsDownload() { @@ -64,8 +66,9 @@ class _LevelPageState extends State { alignment: Alignment.center, children: [ _background(context), - _path(context), _planets(context), + _path(context), + ], ), ), @@ -214,12 +217,12 @@ class _LevelPageState extends State { return Positioned.fill( top: 250, bottom: 150, - right: 60, - left: 60, + right: 50, + left: 50, child: Stack( alignment: Alignment.center, children: [ - const Positioned.fill( + const Positioned.fill( child: LevelPath(), ), Positioned.fill(child: _levelLocation(context)), @@ -235,7 +238,18 @@ class _LevelPageState extends State { children: [ ...List.generate( context.read().nodeList.length, - (index) => Positioned( + (index) { + final node = context.read().nodeList[index]; + if(node.nodeType == NodeType.comingSoon) { + if(node.nodeType == NodeType.comingSoon) { + return Positioned( + left: 0, + right: 0, + bottom: (context.read().locationList[index].bottom ?? 0) + 88, + child: const ComingSoonLevel()); + } + } + return Positioned( top: context.read().locationList[index].top, bottom: context.read().locationList[index].bottom, right: context.read().locationList[index].right, @@ -261,7 +275,8 @@ class _LevelPageState extends State { }, ), ), - ), + ); + }, ), ], ), @@ -316,7 +331,9 @@ class _LevelPageState extends State { builder: (context, snapshot) => MyInkwell( onTap: () => context.read().changeMute(), child: MyImage( - image: snapshot.data == 0 ? MyAssets.musicOff : MyAssets.musicOn, + image: (snapshot.data ?? 1) <= _muteThreshold + ? MyAssets.musicOff + : MyAssets.musicOn, size: setSize(context: context, tablet: 80), ), ), diff --git a/lib/features/level/presentation/ui/widgets/coming_soon_level.dart b/lib/features/level/presentation/ui/widgets/coming_soon_level.dart new file mode 100644 index 0000000..5a2b843 --- /dev/null +++ b/lib/features/level/presentation/ui/widgets/coming_soon_level.dart @@ -0,0 +1,41 @@ +import 'package:flutter/material.dart'; +import 'package:hadi_hoda_flutter/common_ui/resources/my_text_style.dart'; + +import '../../../../../common_ui/resources/my_assets.dart'; +import '../../../../../core/utils/my_localization.dart'; +import '../../../../../core/widgets/images/my_image.dart'; + +class ComingSoonLevel extends StatelessWidget { + const ComingSoonLevel({super.key}); + + @override + Widget build(BuildContext context) { + return Stack( + clipBehavior: Clip.none, + children: [ + Container( + alignment: Alignment.center, + padding: const EdgeInsets.symmetric( + horizontal: 16, + vertical: 42, + ).copyWith(bottom: 32), + width: double.infinity, + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(14), + gradient: LinearGradient( + colors: [ + const Color(0xff7E6C81).withValues(alpha: .9), + const Color(0xff48333F).withValues(alpha: .9), + ], + ), + ), + child: Text(context.translate.coming_soon, style: MYTextStyle.titr4, textAlign: TextAlign.center,), + ), + const Positioned( + left: 0, + right: 0, + top: -58, child: MyImage(image: MyAssets.lock)), + ], + ); + } +} diff --git a/lib/features/level/presentation/ui/widgets/level_path.dart b/lib/features/level/presentation/ui/widgets/level_path.dart index d052352..a90f9fe 100644 --- a/lib/features/level/presentation/ui/widgets/level_path.dart +++ b/lib/features/level/presentation/ui/widgets/level_path.dart @@ -2,11 +2,11 @@ import 'package:flutter/material.dart'; import 'package:path_drawing/path_drawing.dart'; class LevelPath extends StatelessWidget { - const LevelPath({ + const LevelPath({ super.key, }); - final double width = 357; + final double width = 377; final double height = 2230; @override diff --git a/lib/features/level/presentation/ui/widgets/node_widget.dart b/lib/features/level/presentation/ui/widgets/node_widget.dart index 93cb885..0750bb3 100644 --- a/lib/features/level/presentation/ui/widgets/node_widget.dart +++ b/lib/features/level/presentation/ui/widgets/node_widget.dart @@ -9,6 +9,8 @@ import 'package:hadi_hoda_flutter/features/level/domain/entity/level_entity.dart import 'package:hadi_hoda_flutter/features/level/domain/entity/node_entity.dart'; import 'package:hadi_hoda_flutter/features/level/domain/entity/prize_entity.dart'; +import 'coming_soon_level.dart'; + enum LevelType { unFinished, finished, @@ -55,7 +57,9 @@ class NodeWidget extends StatelessWidget { Widget build(BuildContext context) { return Builder( builder: (context) { - if (node.nodeType == NodeType.prize) { + if (node.nodeType == NodeType.comingSoon) { + return const ComingSoonLevel(); + } else if (node.nodeType == NodeType.prize) { return MyInkwell( onTap: () { if (getReward(node.prize?.afterLevel ?? 1)) { @@ -75,83 +79,109 @@ class NodeWidget extends StatelessWidget { ), ); } else { - return InkWell( - onTap: () => onTap?.call( - node.level ?? LevelEntity(), - type(node.level?.order ?? 1), - ), - child: Stack( - alignment: Alignment.topCenter, - clipBehavior: Clip.none, - children: [ - MyImage( - image: - LevelType.image[type(node.level?.order ?? 1)] ?? - MyAssets.level, - fit: BoxFit.cover, - size: setSize(context: context, tablet: 70, mobile: 44), - ), - ShaderMask( - blendMode: BlendMode.modulate, - shaderCallback: (bounds) => LinearGradient( - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - colors: [ - const Color(0XFFFFFFFF), - LevelType.textColor[type(node.level?.order ?? 1)] ?? - MyColors.white, - ], - ).createShader(bounds), - child: Text( - '${node.level?.order ?? 0}', - maxLines: 1, - style: MYTextStyle.button1.copyWith( - fontSize: setSize( - context: context, - mobile: 24, - tablet: 34, - ), - shadows: [ - BoxShadow( - color: - LevelType.textShadowColor[type( - node.level?.order ?? 1, - )] ?? + return Transform.translate( + offset: const Offset(-8, 8), + child: InkWell( + onTap: () => onTap?.call( + node.level ?? LevelEntity(), + type(node.level?.order ?? 1), + ), + child: Container( + color: Colors.transparent, + padding: const EdgeInsets.all(8), + child: Stack( + alignment: Alignment.topCenter, + clipBehavior: Clip.none, + children: [ + MyImage( + image: + LevelType.image[type(node.level?.order ?? 1)] ?? + MyAssets.level, + fit: BoxFit.cover, + size: setSize(context: context, tablet: 70, mobile: 44), + ), + ShaderMask( + blendMode: BlendMode.modulate, + shaderCallback: (bounds) => LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + const Color(0XFFFFFFFF), + LevelType.textColor[type(node.level?.order ?? 1)] ?? MyColors.white, - offset: const Offset(0, 2.97), + ], + ).createShader(bounds), + child: Text( + '${node.level?.order ?? 0}', + maxLines: 1, + style: MYTextStyle.button1.copyWith( + fontSize: setSize( + context: context, + mobile: 24, + tablet: 34, + ), + shadows: [ + BoxShadow( + color: + LevelType.textShadowColor[type( + node.level?.order ?? 1, + )] ?? + MyColors.white, + offset: const Offset(0, 2.97), + ), + ], ), - ], - ), - ), - ), - if (node.level?.id == chooseLevel?.id) - Positioned( - top: setSize(context: context, mobile: -20, tablet: -30), - child: MyImage( - image: MyAssets.location, - size: setSize(context: context, mobile: 26, tablet: 40), + ), ), - ), - if (type(node.level?.order ?? 1) == LevelType.finished) - Positioned( - bottom: 0, - child: Container( - height: setSize(context: context, mobile: 17, tablet: 24), - width: setSize(context: context, mobile: 17, tablet: 24), - padding: const EdgeInsets.all(3), - decoration: BoxDecoration( - shape: BoxShape.circle, - border: Border.all(width: 1, color: const Color(0XFF3CFF3C)), - gradient: const LinearGradient( - begin: Alignment.topCenter, - end: Alignment.bottomCenter, - colors: [Color(0XFF48D336), Color(0XFF2D7C23)], + if (node.level?.id == chooseLevel?.id) + Positioned( + top: setSize( + context: context, + mobile: -20, + tablet: -30, + ), + child: MyImage( + image: MyAssets.location, + size: setSize( + context: context, + mobile: 26, + tablet: 40, + ), ), ), - child: const MyImage(image: MyAssets.doneRounded), - ), - ), - ], + if (type(node.level?.order ?? 1) == LevelType.finished) + Positioned( + bottom: 0, + child: Container( + height: setSize( + context: context, + mobile: 17, + tablet: 24, + ), + width: setSize( + context: context, + mobile: 17, + tablet: 24, + ), + padding: const EdgeInsets.all(3), + decoration: BoxDecoration( + shape: BoxShape.circle, + border: Border.all( + width: 1, + color: const Color(0XFF3CFF3C), + ), + gradient: const LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [Color(0XFF48D336), Color(0XFF2D7C23)], + ), + ), + child: const MyImage(image: MyAssets.doneRounded), + ), + ), + ], + ), + ), ), ); } diff --git a/lib/features/level/presentation/ui/widgets/play_button.dart b/lib/features/level/presentation/ui/widgets/play_button.dart index 8200877..b8071ed 100644 --- a/lib/features/level/presentation/ui/widgets/play_button.dart +++ b/lib/features/level/presentation/ui/widgets/play_button.dart @@ -24,7 +24,7 @@ class PlayButton extends StatelessWidget { children: [ const MyImage(image: MyAssets.button2), Positioned( - top: MySpaces.s2, + top: MySpaces.s6, child: Row( spacing: MySpaces.s4, children: [ @@ -54,13 +54,13 @@ class PlayButton extends StatelessWidget { ], ), ), - Positioned( - bottom: MySpaces.s20, - child: Text( - '${context.translate.step} ${level.order ?? 0}', - style: MYTextStyle.matn3.copyWith(color: const Color(0XFFD8490B)), - ), - ), + // Positioned( + // bottom: MySpaces.s20, + // child: Text( + // '', + // style: MYTextStyle.matn3.copyWith(color: const Color(0XFFD8490B)), + // ), + // ), ], ), ); diff --git a/lib/features/question/data/model/hadith_model.dart b/lib/features/question/data/model/hadith_model.dart index 5ffc8c0..0ea21ef 100644 --- a/lib/features/question/data/model/hadith_model.dart +++ b/lib/features/question/data/model/hadith_model.dart @@ -7,6 +7,7 @@ class HadithModel extends HadithEntity { super.narratorName, super.translation, super.status, + super.sourceName, }); factory HadithModel.fromJson(Map json) { @@ -16,6 +17,7 @@ class HadithModel extends HadithEntity { narratorName: json['narrator_name'], translation: json['translation'], status: json['status'], + sourceName: json['source_name'], ); } } diff --git a/lib/features/question/domain/entity/answer_entity.g.dart b/lib/features/question/domain/entity/answer_entity.g.dart index bcc224b..9973dc6 100644 --- a/lib/features/question/domain/entity/answer_entity.g.dart +++ b/lib/features/question/domain/entity/answer_entity.g.dart @@ -25,6 +25,7 @@ class AnswerEntityAdapter extends TypeAdapter { isActive: fields[5] as bool?, audioID: fields[7] as String?, audioInfo: fields[8] as FileEntity?, + levelOrder: fields[10] as int?, ) ..image = fields[6] as String? ..audio = fields[9] as String?; @@ -33,7 +34,7 @@ class AnswerEntityAdapter extends TypeAdapter { @override void write(BinaryWriter writer, AnswerEntity obj) { writer - ..writeByte(10) + ..writeByte(11) ..writeByte(0) ..write(obj.id) ..writeByte(1) @@ -53,7 +54,9 @@ class AnswerEntityAdapter extends TypeAdapter { ..writeByte(8) ..write(obj.audioInfo) ..writeByte(9) - ..write(obj.audio); + ..write(obj.audio) + ..writeByte(10) + ..write(obj.levelOrder); } @override diff --git a/lib/features/question/domain/entity/hadith_entity.dart b/lib/features/question/domain/entity/hadith_entity.dart index 72abd4b..6088af6 100644 --- a/lib/features/question/domain/entity/hadith_entity.dart +++ b/lib/features/question/domain/entity/hadith_entity.dart @@ -14,6 +14,8 @@ class HadithEntity extends HiveObject { String? translation; @HiveField(4) String? status; + @HiveField(5) + String? sourceName; HadithEntity({ this.id, @@ -21,5 +23,6 @@ class HadithEntity extends HiveObject { this.narratorName, this.translation, this.status, + this.sourceName, }); } diff --git a/lib/features/question/domain/entity/question_entity.dart b/lib/features/question/domain/entity/question_entity.dart index dab05ec..28e1986 100644 --- a/lib/features/question/domain/entity/question_entity.dart +++ b/lib/features/question/domain/entity/question_entity.dart @@ -65,7 +65,7 @@ class QuestionEntity extends HiveObject { this.levelOrder, }){ audio = '${StoragePath.documentDir.path}/${LocalStorage.readData(key: MyConstants.selectLanguage)}/$levelOrder/question_audio/${audioInfo?.filename}'; - correctAudio = '${StoragePath.documentDir.path}/${LocalStorage.readData(key: MyConstants.selectLanguage)}/$levelOrder/correct_answer_audio/${correctAnswerAudioInfo?.filename}'; + correctAudio = '${StoragePath.documentDir.path}/${LocalStorage.readData(key: MyConstants.selectLanguage)}/$levelOrder/question_audio/${correctAnswerAudioInfo?.filename}'; image = '${StoragePath.documentDir.path}/$levelOrder/question_image/${imageInfo?.filename}'; } } diff --git a/lib/features/question/domain/entity/question_entity.g.dart b/lib/features/question/domain/entity/question_entity.g.dart index 8b31e5a..77b8866 100644 --- a/lib/features/question/domain/entity/question_entity.g.dart +++ b/lib/features/question/domain/entity/question_entity.g.dart @@ -31,6 +31,7 @@ class QuestionEntityAdapter extends TypeAdapter { correctAnswerAudioInfo: fields[8] as FileEntity?, imageId: fields[14] as String?, imageInfo: fields[15] as FileEntity?, + levelOrder: fields[17] as int?, ) ..audio = fields[12] as String? ..correctAudio = fields[13] as String? @@ -40,7 +41,7 @@ class QuestionEntityAdapter extends TypeAdapter { @override void write(BinaryWriter writer, QuestionEntity obj) { writer - ..writeByte(17) + ..writeByte(18) ..writeByte(0) ..write(obj.id) ..writeByte(1) @@ -74,7 +75,9 @@ class QuestionEntityAdapter extends TypeAdapter { ..writeByte(15) ..write(obj.imageInfo) ..writeByte(16) - ..write(obj.image); + ..write(obj.image) + ..writeByte(17) + ..write(obj.levelOrder); } @override diff --git a/lib/features/question/presentation/bloc/question_bloc.dart b/lib/features/question/presentation/bloc/question_bloc.dart index 07fb5b7..923682a 100644 --- a/lib/features/question/presentation/bloc/question_bloc.dart +++ b/lib/features/question/presentation/bloc/question_bloc.dart @@ -27,11 +27,11 @@ import 'package:hadi_hoda_flutter/features/question/presentation/ui/screens/answ class QuestionBloc extends Bloc { /// ------------constructor------------ QuestionBloc( - this._getLevelUseCase, - this._getNextLevelUseCase, - this._mainAudioService, - this._effectAudioService, - ) : super(const QuestionState()) { + this._getLevelUseCase, + this._getNextLevelUseCase, + this._mainAudioService, + this._effectAudioService, + ) : super(const QuestionState()) { volumeStream = _mainAudioService.volumeStream(); initAudios(); on(_getLevelEvent); @@ -40,7 +40,7 @@ class QuestionBloc extends Bloc { @override Future close() { - if (_mainAudioService.audioVolume != 0) { + if (!_mainAudioService.isMuted) { _mainAudioService.setVolume(volume: MyConstants.musicAudioVolume); } _backgroundAudioService.dispose(); @@ -125,6 +125,11 @@ class QuestionBloc extends Bloc { await _mainAudioService.play(); } + Future playRightAnswerAudio() async { + await _mainAudioService.setAudio(assetPath: MyAudios.rightAnswer); + await _mainAudioService.play(); + } + Future initAudios() async { await Future.wait([ _mainAudioService.stop(), @@ -132,7 +137,7 @@ class QuestionBloc extends Bloc { _backgroundAudioService.setAudio(assetPath: MyAudios.question), _backgroundAudioService.setLoopMode(isLoop: true), ]); - if (_mainAudioService.audioVolume != 0) { + if (!_mainAudioService.isMuted) { await Future.wait([ _mainAudioService.setVolume(volume: MyConstants.questionAudioVolume), _backgroundAudioService.setVolume(volume: 0.1), @@ -142,8 +147,11 @@ class QuestionBloc extends Bloc { } Future playWrongAudio() async { - await _effectAudioService.setAudio(assetPath: MyAudios.incorrectAnswer); - await _effectAudioService.play(); + final audio = MyAudios.incorrectAnswer; + debugPrint('playWrongAudio: $audio'); + + await _mainAudioService.setAudio(assetPath: audio); + await _mainAudioService.play(); } Future playAnswerAudio({String? audio}) async { @@ -165,13 +173,13 @@ class QuestionBloc extends Bloc { } Future showQueueAnswer() async { - if(!showAnswerSequence) return; + if (!showAnswerSequence) return; final List answers = state.currentQuestion?.answers ?? []; if (answers.isNotEmpty) { answers.removeWhere((e) => e.imageId == null); } for (final answer in answers) { - await Future.delayed(const Duration(milliseconds: 500), () async { + await Future.delayed(const Duration(milliseconds: 350), () async { if (MyContext.get.mounted) { await showAnswerDialog( context: MyContext.get, @@ -191,7 +199,7 @@ class QuestionBloc extends Bloc { bool showConfetti = false, bool autoClose = true, }) async { - if(showConfetti == false){ + if (showConfetti == false) { changeGlobeState(key: MyAnimations.globeStateSpeaking); } await Navigator.of(context).push( @@ -210,17 +218,19 @@ class QuestionBloc extends Bloc { } Future getNextLevelEvent({required BuildContext context}) async { - await _getNextLevelUseCase(QuestionParams()).then((value) => - value.fold((data) { - add(GetLevelEvent('${data.id}', context)); - }, (error) { - goToLevelPage(context: MyContext.get); - }, - ), + await _getNextLevelUseCase(QuestionParams()).then( + (value) => value.fold( + (data) { + add(GetLevelEvent('${data.id}', context)); + }, + (error) { + goToLevelPage(context: MyContext.get); + }, + ), ); } - void showingAnswerSequence({required bool show}){ + void showingAnswerSequence({required bool show}) { showAnswerSequence = show; } @@ -246,7 +256,10 @@ class QuestionBloc extends Bloc { } /// ------------Event Calls------------ - FutureOr _getLevelEvent(GetLevelEvent event, Emitter emit) async { + FutureOr _getLevelEvent( + GetLevelEvent event, + Emitter emit, + ) async { await _getLevelUseCase(QuestionParams(id: int.parse(event.id ?? '0'))).then( (value) { value.fold( @@ -257,16 +270,18 @@ class QuestionBloc extends Bloc { title: data.title, questions: [ ...?data.questions, - QuestionEntity(order: (data.questions?.length ?? 0) + 1) + QuestionEntity(order: (data.questions?.length ?? 0) + 1), ], ); - emit(state.copyWith( - getQuestionStatus: const BaseComplete(''), - levelEntity: level, - currentQuestion: data.questions?.first, - showAnswers: false, - correctAnswer: false, - )); + emit( + state.copyWith( + getQuestionStatus: const BaseComplete(''), + levelEntity: level, + currentQuestion: data.questions?.first, + showAnswers: false, + correctAnswer: false, + ), + ); imageAnimationController?.forward(); changeGlobeState(key: MyAnimations.globeStateSpeaking); await playQuestionAudio(); @@ -276,15 +291,19 @@ class QuestionBloc extends Bloc { }); }, (error) { - emit(state.copyWith(getQuestionStatus: BaseError(error.errorMessage))); + emit( + state.copyWith(getQuestionStatus: BaseError(error.errorMessage)), + ); }, ); }, ); } - FutureOr _chooseAnswerEvent(ChooseAnswerEvent event, - Emitter emit,) async { + FutureOr _chooseAnswerEvent( + ChooseAnswerEvent event, + Emitter emit, + ) async { emit(state.copyWith(correctAnswer: event.chooseCorrectAnswer)); if (event.chooseCorrectAnswer) { @@ -292,24 +311,29 @@ class QuestionBloc extends Bloc { await showAnswerDialog( context: MyContext.get, correctAudio: state.currentQuestion?.correctAudio, - answerEntity: state.currentQuestion?.answers?.firstWhereOrNull((e) => - e.order == event.correctAnswer) ?? AnswerEntity(), + answerEntity: + state.currentQuestion?.answers?.firstWhereOrNull( + (e) => e.order == event.correctAnswer, + ) ?? + AnswerEntity(), showConfetti: true, ); answerAnimationController?.reverse(); await Future.delayed(const Duration(seconds: 1), () async { final QuestionEntity? findPreQuestion = state.currentQuestion; final int findIndex = (findPreQuestion?.order ?? 1); - emit( - state.copyWith( - currentQuestion: state.levelEntity?.questions?[findIndex], - ), - ); - if (state.currentQuestion?.order == - state.levelEntity?.questions?.length) { + final newCurrentQuestion = state.levelEntity?.questions?[findIndex]; + final isLastQuestion = + newCurrentQuestion?.order == state.levelEntity?.questions?.length; + if (isLastQuestion) { playDiamondAudio(); + await Future.delayed(const Duration(milliseconds: 400)); + } + emit(state.copyWith(currentQuestion: newCurrentQuestion)); + if (isLastQuestion) { int currentLevel = int.parse( - LocalStorage.readData(key: MyConstants.currentLevel) ?? '1'); + LocalStorage.readData(key: MyConstants.currentLevel) ?? '1', + ); if (state.levelEntity?.order == currentLevel) { ++currentLevel; await LocalStorage.saveData( @@ -334,7 +358,10 @@ class QuestionBloc extends Bloc { changeGlobeState(key: MyAnimations.globeStateWrong); playWrongAudio(); await Future.delayed(const Duration(milliseconds: 1500), () { - changeGlobeState(key: MyAnimations.globeStateAfterWrong); + if (globeAnimationController?.value == + globeStates[MyAnimations.globeStateWrong]) { + changeGlobeState(key: MyAnimations.globeStateAfterWrong); + } }); } } diff --git a/lib/features/question/presentation/ui/question_page.dart b/lib/features/question/presentation/ui/question_page.dart index 0ec7332..94d084c 100644 --- a/lib/features/question/presentation/ui/question_page.dart +++ b/lib/features/question/presentation/ui/question_page.dart @@ -14,9 +14,29 @@ import 'package:hadi_hoda_flutter/features/question/presentation/ui/screens/diam import 'package:hadi_hoda_flutter/features/question/presentation/ui/screens/question_screen.dart'; import 'package:hadi_hoda_flutter/features/question/presentation/ui/widgets/glassy_button.dart'; import 'package:hadi_hoda_flutter/features/question/presentation/ui/widgets/question_title.dart'; +import 'package:wakelock_plus/wakelock_plus.dart'; -class QuestionPage extends StatelessWidget { +class QuestionPage extends StatefulWidget { const QuestionPage({super.key}); + static const double _muteThreshold = 0.001; + + @override + State createState() => _QuestionPageState(); +} + +class _QuestionPageState extends State { + + @override + void initState() { + WakelockPlus.enable(); + super.initState(); + } + + @override + void dispose() { + WakelockPlus.disable(); + super.dispose(); + } @override Widget build(BuildContext context) { @@ -106,7 +126,9 @@ class QuestionPage extends StatelessWidget { initialData: 1, stream: context.read().volumeStream, builder: (context, snapshot) => GlassyButton( - image: snapshot.data == 0 ? MyAssets.unMusic : MyAssets.music, + image: (snapshot.data ?? 1) <= QuestionPage._muteThreshold + ? MyAssets.unMusic + : MyAssets.music, onTap: () => context.read().changeMute(), ), ), diff --git a/lib/features/question/presentation/ui/screens/answer_screen.dart b/lib/features/question/presentation/ui/screens/answer_screen.dart index 46e2066..34a9db8 100644 --- a/lib/features/question/presentation/ui/screens/answer_screen.dart +++ b/lib/features/question/presentation/ui/screens/answer_screen.dart @@ -81,6 +81,7 @@ class _AnswerScreenState extends State { return Stack( alignment: Alignment.center, children: [ + Container(color: Colors.black12), Center( child: Padding( padding: EdgeInsets.symmetric( diff --git a/lib/features/question/presentation/ui/screens/diamond_screen.dart b/lib/features/question/presentation/ui/screens/diamond_screen.dart index 8c8ac35..3318327 100644 --- a/lib/features/question/presentation/ui/screens/diamond_screen.dart +++ b/lib/features/question/presentation/ui/screens/diamond_screen.dart @@ -160,17 +160,17 @@ class DiamondScreen extends StatelessWidget { spacing: MySpaces.s12, children: [ Expanded( - child: MyWhiteButton( + child: MyYellowButton( onTap: () => context.read().goToLevelPage(context: context), title: context.translate.map, ), ), - Expanded( - child: MyYellowButton( - onTap: () => context.read().getNextLevelEvent(context: context), - title: context.translate.next, - ), - ), + // Expanded( + // child: MyYellowButton( + // onTap: () => context.read().getNextLevelEvent(context: context), + // title: context.translate.next, + // ), + // ), ], ); } diff --git a/lib/features/question/presentation/ui/screens/question_screen.dart b/lib/features/question/presentation/ui/screens/question_screen.dart index 2a9170d..7b4dd80 100644 --- a/lib/features/question/presentation/ui/screens/question_screen.dart +++ b/lib/features/question/presentation/ui/screens/question_screen.dart @@ -7,8 +7,10 @@ import 'package:hadi_hoda_flutter/common_ui/resources/my_assets.dart'; import 'package:hadi_hoda_flutter/common_ui/resources/my_colors.dart'; import 'package:hadi_hoda_flutter/common_ui/resources/my_text_style.dart'; import 'package:hadi_hoda_flutter/core/constants/my_constants.dart'; +import 'package:hadi_hoda_flutter/core/utils/app_life_cycle.dart'; import 'package:hadi_hoda_flutter/core/utils/gap.dart'; import 'package:hadi_hoda_flutter/core/utils/local_storage.dart'; +import 'package:hadi_hoda_flutter/core/utils/my_device.dart'; import 'package:hadi_hoda_flutter/core/utils/my_localization.dart'; import 'package:hadi_hoda_flutter/core/utils/screen_size.dart'; import 'package:hadi_hoda_flutter/core/widgets/animations/fade_anim.dart'; @@ -34,6 +36,7 @@ class QuestionScreen extends StatefulWidget { class _QuestionScreenState extends State with TickerProviderStateMixin, WidgetsBindingObserver { + late final isTablet = MyDevice.isTablet(context); @override void initState() { super.initState(); @@ -42,22 +45,22 @@ class _QuestionScreenState extends State .read() .answerAnimationController = AnimationController( vsync: this, - duration: const Duration(milliseconds: 500), - reverseDuration: const Duration(milliseconds: 500), + duration: const Duration(milliseconds: 350), + reverseDuration: const Duration(milliseconds: 350), ); context.read().imageAnimationController = AnimationController( vsync: this, - duration: const Duration(milliseconds: 500), - reverseDuration: const Duration(milliseconds: 500), + duration: const Duration(milliseconds: 200), + reverseDuration: const Duration(milliseconds: 200), ); if (LocalStorage.readData(key: MyConstants.firstShowcase) == 'true') { context.read().imageAnimationController?.forward(); } context.read().globeAnimationController = AnimationController( vsync: this, - duration: const Duration(milliseconds: 500), - reverseDuration: const Duration(milliseconds: 500), + duration: const Duration(milliseconds: 350), + reverseDuration: const Duration(milliseconds: 350), ); } @@ -68,6 +71,7 @@ class _QuestionScreenState extends State state == AppLifecycleState.inactive) { context.read().pauseBackgroundPlaying(); } else if (state == AppLifecycleState.resumed) { + if (AppLifeCycleController.suppressAutoResumeAudio) return; context.read().playBackgroundPlaying(); } } @@ -92,23 +96,20 @@ class _QuestionScreenState extends State } Widget _questionImage(BuildContext context) { - return FadeAnimDelayed( - duration: const Duration(seconds: 1), - child: FadeAnimController( - controller: context.read().imageAnimationController!, - child: SingleChildScrollView( - child: Column( - children: [ - _titles(context), - 20.0.gapHeight, - BlocBuilder( - builder: (context, state) => ImageBox( - key: Key('${state.currentQuestion?.image}'), - image: state.currentQuestion?.image ?? '', - ), + return FadeAnimController( + controller: context.read().imageAnimationController!, + child: SingleChildScrollView( + child: Column( + children: [ + _titles(context), + 20.0.gapHeight, + BlocBuilder( + builder: (context, state) => ImageBox( + key: Key('${state.currentQuestion?.image}'), + image: state.currentQuestion?.image ?? '', ), - ], - ), + ), + ], ), ), ); @@ -189,9 +190,10 @@ class _QuestionScreenState extends State null) { return const SizedBox.shrink(); } else { - return SizedBox( - width: 180, - height: 250, + return Container( + alignment: isTablet ? Alignment.center : null, + width: isTablet ? 320 : 180, + height: isTablet ? 300 :250, child: SlideAnim( controller: context .read() @@ -240,9 +242,10 @@ class _QuestionScreenState extends State null) { return const SizedBox.shrink(); } else { - return SizedBox( - width: 180, - height: 250, + return Container( + alignment: isTablet ? Alignment.center : null, + width: isTablet ? 320 : 180, + height: isTablet ? 300 :250, child: SlideAnim( controller: context .read() @@ -297,10 +300,11 @@ class _QuestionScreenState extends State null) { return const SizedBox.shrink(); } else { - return SizedBox( - width: 180, - height: 250, - child: SlideAnim( + return Container( + alignment: isTablet ? Alignment.center : null, + width: isTablet ? 320 : 180, + height: isTablet ? 300 :250, + child: SlideAnim( controller: context .read() .answerAnimationController!, @@ -354,10 +358,11 @@ class _QuestionScreenState extends State null) { return const SizedBox.shrink(); } else { - return SizedBox( - width: 180, - height: 250, - child: SlideAnim( + return Container( + alignment: isTablet ? Alignment.center : null, + width: isTablet ? 320 : 180, + height: isTablet ? 300 :250, + child: SlideAnim( controller: context .read() .answerAnimationController!, @@ -406,10 +411,11 @@ class _QuestionScreenState extends State null) { return const SizedBox.shrink(); } else { - return SizedBox( - width: 180, - height: 250, - child: SlideAnim( + return Container( + alignment: isTablet ? Alignment.center : null, + width: isTablet ? 320 : 180, + height: isTablet ? 300 :250, + child: SlideAnim( controller: context .read() .answerAnimationController!, diff --git a/lib/features/splash/presentation/bloc/splash_bloc.dart b/lib/features/splash/presentation/bloc/splash_bloc.dart index 266f31f..66a8a1a 100644 --- a/lib/features/splash/presentation/bloc/splash_bloc.dart +++ b/lib/features/splash/presentation/bloc/splash_bloc.dart @@ -24,7 +24,7 @@ class SplashBloc extends Bloc { const Duration(seconds: 2), () { if (context.mounted) { - context.goNamed(Routes.homePage); + context.goNamed(Routes.introPage); } }, ); diff --git a/lib/features/splash/presentation/ui/splash_page.dart b/lib/features/splash/presentation/ui/splash_page.dart index 2eba72a..29031e7 100644 --- a/lib/features/splash/presentation/ui/splash_page.dart +++ b/lib/features/splash/presentation/ui/splash_page.dart @@ -45,7 +45,7 @@ class _SplashPageState extends State { scale: 3, repeat: ImageRepeat.repeat, colorFilter: ColorFilter.mode( - Colors.white.withValues(alpha: 0.2), + Colors.white.withValues(alpha: 0.22), BlendMode.srcIn, ), ), @@ -53,7 +53,7 @@ class _SplashPageState extends State { child: Stack( alignment: Alignment.center, children: [ - // _image(), + Positioned.fill(child: _image()), _loading(context), ], ), @@ -63,15 +63,26 @@ class _SplashPageState extends State { Widget _image() { return const Stack( + alignment: Alignment.center, children: [ MyImage( image: MyAssets.hadiHoda, ), - PositionedDirectional( - start: MySpaces.s10, - top: MySpaces.s40, - child: MyImage( - image: MyAssets.globe, + Positioned( + right: 0, + left: 0, + top: 0, + bottom: 0, + child: Center( + child: Padding( + padding: EdgeInsets.only(bottom: 140, right: 235), + child: Opacity( + opacity: .94, + child: MyImage( + image: MyAssets.globe, + ), + ), + ), ), ), ], @@ -82,6 +93,8 @@ class _SplashPageState extends State { Positioned _loading(BuildContext context) { return Positioned( bottom: MySpaces.s40, + right: 0, + left: 0, child: RotationAnim( child: MyImage( image: MyAssets.loading, diff --git a/lib/init_bindings.dart b/lib/init_bindings.dart index a8ce47c..e4c3e22 100644 --- a/lib/init_bindings.dart +++ b/lib/init_bindings.dart @@ -68,46 +68,99 @@ void initBindings() { locator.registerSingleton(AppBloc()); /// Blocs - locator.registerFactory(() => LanguageBloc(locator(), locator(), locator())); + locator.registerFactory( + () => LanguageBloc(locator(), locator(), locator()), + ); locator.registerFactory( - () => DownloadBloc(locator(), locator(), locator(), locator(), locator())); + () => DownloadBloc(locator(), locator(), locator(), locator(), locator()), + ); /// Sample Feature - locator.registerLazySingleton(() => SampleDatasourceImpl(locator())); - locator.registerLazySingleton(() => SampleRepositoryImpl(locator())); - locator.registerLazySingleton(() => GetSampleUseCase(locator())); + locator.registerLazySingleton( + () => SampleDatasourceImpl(locator()), + ); + locator.registerLazySingleton( + () => SampleRepositoryImpl(locator()), + ); + locator.registerLazySingleton( + () => GetSampleUseCase(locator()), + ); /// Download Feature - locator.registerLazySingleton(() => DownloadDatasourceImpl(locator())); - locator.registerLazySingleton(() => DownloadRepositoryImpl(locator())); - locator.registerLazySingleton(() => SaveLevelsUseCase(locator())); - locator.registerLazySingleton(() => BatchDownloadUseCase(locator())); - locator.registerLazySingleton(() => LoadingStreamUseCase(locator())); - locator.registerLazySingleton(() => CancelDownloadUseCase(locator())); - locator.registerLazySingleton(() => GetLastDownloadedLevel(locator())); + locator.registerLazySingleton( + () => DownloadDatasourceImpl(locator()), + ); + locator.registerLazySingleton( + () => DownloadRepositoryImpl(locator()), + ); + locator.registerLazySingleton( + () => SaveLevelsUseCase(locator()), + ); + locator.registerLazySingleton( + () => BatchDownloadUseCase(locator()), + ); + locator.registerLazySingleton( + () => LoadingStreamUseCase(locator()), + ); + locator.registerLazySingleton( + () => CancelDownloadUseCase(locator()), + ); + locator.registerLazySingleton( + () => GetLastDownloadedLevel(locator()), + ); /// Guider Feature - locator.registerLazySingleton(() => const GuiderDatasourceImpl()); - locator.registerLazySingleton(() => GuiderRepositoryImpl(locator())); - locator.registerLazySingleton(() => GetFirstLevelUseCase(locator())); + locator.registerLazySingleton( + () => const GuiderDatasourceImpl(), + ); + locator.registerLazySingleton( + () => GuiderRepositoryImpl(locator()), + ); + locator.registerLazySingleton( + () => GetFirstLevelUseCase(locator()), + ); /// Question Feature - locator.registerLazySingleton(() => const QuestionDatasourceImpl()); - locator.registerLazySingleton(() => QuestionRepositoryImpl(locator())); - locator.registerLazySingleton(() => GetLevelUseCase(locator())); - locator.registerLazySingleton(() => GetNextLevelUseCase(locator())); + locator.registerLazySingleton( + () => const QuestionDatasourceImpl(), + ); + locator.registerLazySingleton( + () => QuestionRepositoryImpl(locator()), + ); + locator.registerLazySingleton( + () => GetLevelUseCase(locator()), + ); + locator.registerLazySingleton( + () => GetNextLevelUseCase(locator()), + ); /// Level Feature - locator.registerLazySingleton(() => const LocalLevelDatasourceImpl()); - locator.registerLazySingleton(() => LevelRepositoryImpl(locator())); - locator.registerLazySingleton(() => GetLevelsUseCase(locator())); + locator.registerLazySingleton( + () => LocalLevelDatasourceImpl(locator()), + ); + locator.registerLazySingleton( + () => LevelRepositoryImpl(locator()), + ); + locator.registerLazySingleton( + () => GetLevelsUseCase(locator()), + ); /// Language Feature - locator.registerLazySingleton(() => LanguageDatasourceImpl(locator())); - locator.registerLazySingleton(() => LanguageRepositoryImpl(locator())); - locator.registerLazySingleton(() => GetLanguagesUseCase(locator())); - locator.registerLazySingleton(() => GetSelectedLanguageUseCase(locator())); - locator.registerLazySingleton(() => SaveSelectedLanguageUseCase(locator())); + locator.registerLazySingleton( + () => LanguageDatasourceImpl(locator()), + ); + locator.registerLazySingleton( + () => LanguageRepositoryImpl(locator()), + ); + locator.registerLazySingleton( + () => GetLanguagesUseCase(locator()), + ); + locator.registerLazySingleton( + () => GetSelectedLanguageUseCase(locator()), + ); + locator.registerLazySingleton( + () => SaveSelectedLanguageUseCase(locator()), + ); } Future initDataBase() async { diff --git a/lib/l10n/app_ar.arb b/lib/l10n/app_ar.arb index 6999f41..f0a2f1c 100644 --- a/lib/l10n/app_ar.arb +++ b/lib/l10n/app_ar.arb @@ -36,5 +36,6 @@ "showcase_stepper": "هنا سترى الأسئلة الخاصة\nبهذه المرحلة للوصول\nإلى الماسة.", "showcase_hadith": "اطّلع على المصادر والأحاديث\nلهذا السؤال", "showcase_guide": "هذا دليل سيساعدك\nفي رحلتك.", - "reward": "جائزة" + "reward": "جائزة", + "downloading": "جارٍ التنزيل" } diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index 156cbbd..6fa52ec 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -36,5 +36,6 @@ "showcase_stepper": "Hier siehst du die\nFragen für diese\nStufe, um den\nDiamanten zu erreichen.", "showcase_hadith": "Quellen und\nHadithe zu dieser\nFrage ansehen.", "showcase_guide": "Dies ist eine Anleitung,\ndie dir hilft.", - "reward": "belohnen" + "reward": "belohnen", + "downloading": "Wird heruntergeladen" } diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index e4b3a06..ebc30c1 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -36,5 +36,7 @@ "showcase_stepper": "Here you will see the\nquestions for this\nstage to reach the\ndiamond.", "showcase_hadith": "View sources and\nhadiths for this\nquestion", "showcase_guide": "This is a guide that will\nhelp you.", - "reward": "Reward" + "reward": "Reward", + "coming_soon": "Next stages will open soon", + "downloading": "Downloading" } \ No newline at end of file diff --git a/lib/l10n/app_fr.arb b/lib/l10n/app_fr.arb index 5ba2ce2..e687c4e 100644 --- a/lib/l10n/app_fr.arb +++ b/lib/l10n/app_fr.arb @@ -36,5 +36,6 @@ "showcase_stepper": "Ici, tu verras les\nquestions de cette\nétape pour atteindre\nle diamant.", "showcase_hadith": "Consulte les sources et\nles hadiths pour cette\nquestion", "showcase_guide": "Ceci est un guide\nqui t’aidera.", - "reward": "récompense" + "reward": "récompense", + "downloading": "Téléchargement" } diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index 68eec8f..6f69286 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -327,6 +327,18 @@ abstract class AppLocalizations { /// In en, this message translates to: /// **'Reward'** String get reward; + + /// No description provided for @coming_soon. + /// + /// In en, this message translates to: + /// **'Next stages will open soon'** + String get coming_soon; + + /// No description provided for @downloading. + /// + /// In en, this message translates to: + /// **'Downloading'** + String get downloading; } class _AppLocalizationsDelegate diff --git a/lib/l10n/app_localizations_ar.dart b/lib/l10n/app_localizations_ar.dart index 3c5f345..6372b36 100644 --- a/lib/l10n/app_localizations_ar.dart +++ b/lib/l10n/app_localizations_ar.dart @@ -122,4 +122,10 @@ class AppLocalizationsAr extends AppLocalizations { @override String get reward => 'جائزة'; + + @override + String get coming_soon => 'Next stages will open soon'; + + @override + String get downloading => 'جارٍ التنزيل'; } diff --git a/lib/l10n/app_localizations_de.dart b/lib/l10n/app_localizations_de.dart index 49f7a1f..b70c80c 100644 --- a/lib/l10n/app_localizations_de.dart +++ b/lib/l10n/app_localizations_de.dart @@ -129,4 +129,10 @@ class AppLocalizationsDe extends AppLocalizations { @override String get reward => 'belohnen'; + + @override + String get coming_soon => 'Next stages will open soon'; + + @override + String get downloading => 'Wird heruntergeladen'; } diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index 991362a..1facbbd 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -126,4 +126,10 @@ class AppLocalizationsEn extends AppLocalizations { @override String get reward => 'Reward'; + + @override + String get coming_soon => 'Next stages will open soon'; + + @override + String get downloading => 'Downloading'; } diff --git a/lib/l10n/app_localizations_fr.dart b/lib/l10n/app_localizations_fr.dart index 6e6ec23..3c31237 100644 --- a/lib/l10n/app_localizations_fr.dart +++ b/lib/l10n/app_localizations_fr.dart @@ -127,4 +127,10 @@ class AppLocalizationsFr extends AppLocalizations { @override String get reward => 'récompense'; + + @override + String get coming_soon => 'Next stages will open soon'; + + @override + String get downloading => 'Téléchargement'; } diff --git a/lib/l10n/app_localizations_ru.dart b/lib/l10n/app_localizations_ru.dart index e374e4a..076a49d 100644 --- a/lib/l10n/app_localizations_ru.dart +++ b/lib/l10n/app_localizations_ru.dart @@ -127,4 +127,10 @@ class AppLocalizationsRu extends AppLocalizations { @override String get reward => 'награда'; + + @override + String get coming_soon => 'Next stages will open soon'; + + @override + String get downloading => 'Загрузка'; } diff --git a/lib/l10n/app_localizations_tr.dart b/lib/l10n/app_localizations_tr.dart index db16200..967e94f 100644 --- a/lib/l10n/app_localizations_tr.dart +++ b/lib/l10n/app_localizations_tr.dart @@ -126,4 +126,10 @@ class AppLocalizationsTr extends AppLocalizations { @override String get reward => 'ödül'; + + @override + String get coming_soon => 'Next stages will open soon'; + + @override + String get downloading => 'İndiriliyor'; } diff --git a/lib/l10n/app_ru.arb b/lib/l10n/app_ru.arb index 9460133..e546b20 100644 --- a/lib/l10n/app_ru.arb +++ b/lib/l10n/app_ru.arb @@ -36,5 +36,6 @@ "showcase_stepper": "Здесь ты увидишь\nвопросы этого этапа,\nчтобы получить\nалмаз.", "showcase_hadith": "Просмотри источники и\nхадисы по этому вопросу", "showcase_guide": "Это руководство,\nкоторое поможет тебе.", - "reward": "награда" + "reward": "награда", + "downloading": "Загрузка" } diff --git a/lib/l10n/app_tr.arb b/lib/l10n/app_tr.arb index 9e4691c..2cc995b 100644 --- a/lib/l10n/app_tr.arb +++ b/lib/l10n/app_tr.arb @@ -36,5 +36,6 @@ "showcase_stepper": "Burada, elmasa ulaşmak\niçin bu aşamadaki\nsoruları göreceksin.", "showcase_hadith": "Bu soruya ait\nkaynakları ve hadisleri\nincele", "showcase_guide": "Bu, sana yardımcı olacak\nbir rehberdir.", - "reward": "ödül" + "reward": "ödül", + "downloading": "İndiriliyor" } diff --git a/pubspec.yaml b/pubspec.yaml index 8f93907..1d7c8d9 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -10,6 +10,7 @@ dependencies: auto_size_text: ^3.0.0 bloc: ^9.0.0 collection: ^1.19.1 + connectivity_plus: ^7.0.0 dio: ^5.9.0 easy_stepper: ^0.8.5+1 equatable: ^2.0.7 @@ -32,7 +33,9 @@ dependencies: pretty_dio_logger: ^1.4.0 shared_preferences: ^2.5.3 showcaseview: ^5.0.1 + url_launcher: ^6.3.2 vector_graphics: ^1.1.19 + wakelock_plus: ^1.4.0 wheel_chooser: ^1.0.1 dev_dependencies: @@ -52,6 +55,7 @@ flutter: - assets/images/ - assets/animations/ - assets/audios/ + - assets/audios/incorrect_answers/ - assets/videos/ - assets/svg/ @@ -76,4 +80,7 @@ flutter: - asset: assets/fonts/Baloo2-Bold.ttf weight: 700 - asset: assets/fonts/Baloo2-ExtraBold.ttf - weight: 800 \ No newline at end of file + weight: 800 + - family: NotoSansArabic + fonts: + - asset: assets/fonts/NotoSansArabic.ttf \ No newline at end of file