From fd8a685d9db205433e5da5aebbf9be72e97a94de Mon Sep 17 00:00:00 2001 From: AmirrezaChegini Date: Wed, 3 Dec 2025 13:49:16 +0330 Subject: [PATCH 1/2] fix: scroll text title --- .../presentation/ui/language_page.dart | 4 +-- .../presentation/bloc/question_bloc.dart | 17 ++++++++++- .../ui/screens/question_screen.dart | 28 +++++++++++-------- 3 files changed, 34 insertions(+), 15 deletions(-) diff --git a/lib/features/language/presentation/ui/language_page.dart b/lib/features/language/presentation/ui/language_page.dart index bdaeba2..811c017 100644 --- a/lib/features/language/presentation/ui/language_page.dart +++ b/lib/features/language/presentation/ui/language_page.dart @@ -43,8 +43,8 @@ class LanguagePage extends StatelessWidget { ), child: Padding( padding: EdgeInsets.only( - left: setSize(context: context, mobile: 60, tablet: 0.3.w) ?? 0, - right: setSize(context: context, mobile: 60, tablet: 0.3.w) ?? 0, + left: setSize(context: context, mobile: 50, tablet: 0.3.w) ?? 0, + right: setSize(context: context, mobile: 50, tablet: 0.3.w) ?? 0, bottom: MySpaces.s40, top: 100, ), diff --git a/lib/features/question/presentation/bloc/question_bloc.dart b/lib/features/question/presentation/bloc/question_bloc.dart index f02823f..c633716 100644 --- a/lib/features/question/presentation/bloc/question_bloc.dart +++ b/lib/features/question/presentation/bloc/question_bloc.dart @@ -48,6 +48,7 @@ class QuestionBloc extends Bloc { _backgroundAudioService.dispose(); answerAnimationController.dispose(); imageAnimationController.dispose(); + titleController.dispose(); return super.close(); } @@ -79,6 +80,7 @@ class QuestionBloc extends Bloc { final AudioService _effectAudioService; late final AnimationController answerAnimationController; late final AnimationController imageAnimationController; + final ScrollController titleController = ScrollController(); /// ------------Functions------------ void registerShowCase() { @@ -118,6 +120,15 @@ class QuestionBloc extends Bloc { } } + void startScrollTitle({Duration? audioDuration}) { + if (audioDuration == null || audioDuration == Duration.zero) return; + titleController.animateTo( + titleController.position.maxScrollExtent, + duration: audioDuration, + curve: Curves.linear + ); + } + void showHadith({required BuildContext context}) { showHadithDialog( context: context, @@ -165,7 +176,11 @@ class QuestionBloc extends Bloc { } Future playQuestionAudio() async { - await _mainAudioService.setAudio(filePath: state.currentQuestion?.audio); + if(titleController.hasClients){ + titleController.jumpTo(0); + } + Duration? duration = await _mainAudioService.setAudio(filePath: state.currentQuestion?.audio); + startScrollTitle(audioDuration: duration); await _mainAudioService.play(); } diff --git a/lib/features/question/presentation/ui/screens/question_screen.dart b/lib/features/question/presentation/ui/screens/question_screen.dart index 6736a4e..d0202bf 100644 --- a/lib/features/question/presentation/ui/screens/question_screen.dart +++ b/lib/features/question/presentation/ui/screens/question_screen.dart @@ -1,4 +1,3 @@ -import 'package:auto_size_text/auto_size_text.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:hadi_hoda_flutter/common_ui/resources/my_assets.dart'; @@ -135,17 +134,22 @@ class _QuestionScreenState extends State with TickerProviderStat previous.currentQuestion?.id != current.currentQuestion?.id, builder: (context, state) => FadeAnim( - child: AutoSizeText( - state.currentQuestion?.title ?? '', - textAlign: TextAlign.center, - maxLines: 4, - maxFontSize: 20, - minFontSize: 16, - style: MYTextStyle.titr1.copyWith( - shadows: [ - BoxShadow( - offset: Offset(0, 2), - color: MyColors.black.withValues(alpha: 0.25), + child: SizedBox( + height: 100, + child: ListView( + controller: context.read().titleController, + children: [ + Text( + state.currentQuestion?.title ?? '', + textAlign: TextAlign.center, + style: MYTextStyle.titr1.copyWith( + shadows: [ + BoxShadow( + offset: Offset(0, 2), + color: MyColors.black.withValues(alpha: 0.25), + ), + ], + ), ), ], ), From 72afb18c341d4d7fe5950117664cbfb39594a8de Mon Sep 17 00:00:00 2001 From: AmirrezaChegini Date: Wed, 3 Dec 2025 15:42:10 +0330 Subject: [PATCH 2/2] add: guider page --- lib/core/middlewares/my_middlewares.dart | 10 + lib/core/routers/my_routes.dart | 15 ++ lib/core/widgets/answer_box/answer_box.dart | 57 ++-- .../answer_box/answer_box_showcase.dart | 111 ++++++++ .../widgets/showcase/my_showcase_widget.dart | 6 +- .../data/datasource/guider_datasource.dart | 33 +++ .../guider_repository_impl.dart | 28 ++ .../domain/repository/guider_repository.dart | 7 + .../usecases/get_first_level_usecase.dart | 17 ++ .../guider/presentation/bloc/guider_bloc.dart | 131 ++++++++++ .../presentation/bloc/guider_event.dart | 8 + .../presentation/bloc/guider_state.dart | 27 ++ .../guider/presentation/ui/guider_page.dart | 247 ++++++++++++++++++ .../presentation/bloc/question_bloc.dart | 57 +--- .../ui/screens/question_screen.dart | 55 ++-- .../sample/presentation/ui/sample_page.dart | 5 +- lib/init_bindings.dart | 9 + 17 files changed, 695 insertions(+), 128 deletions(-) create mode 100644 lib/core/widgets/answer_box/answer_box_showcase.dart create mode 100644 lib/features/guider/data/datasource/guider_datasource.dart create mode 100644 lib/features/guider/data/repository_impl/guider_repository_impl.dart create mode 100644 lib/features/guider/domain/repository/guider_repository.dart create mode 100644 lib/features/guider/domain/usecases/get_first_level_usecase.dart create mode 100644 lib/features/guider/presentation/bloc/guider_bloc.dart create mode 100644 lib/features/guider/presentation/bloc/guider_event.dart create mode 100644 lib/features/guider/presentation/bloc/guider_state.dart create mode 100644 lib/features/guider/presentation/ui/guider_page.dart diff --git a/lib/core/middlewares/my_middlewares.dart b/lib/core/middlewares/my_middlewares.dart index 09b7f82..0778906 100644 --- a/lib/core/middlewares/my_middlewares.dart +++ b/lib/core/middlewares/my_middlewares.dart @@ -30,4 +30,14 @@ class MyMiddlewares { return null; } } + + static FutureOr question(BuildContext context, GoRouterState state) { + final String? firstShowCase = LocalStorage.readData( + key: MyConstants.firstShowcase); + if (firstShowCase == 'true') { + return null; + } else { + return '${Routes.guiderPage}/${state.pathParameters['id']}'; + } + } } diff --git a/lib/core/routers/my_routes.dart b/lib/core/routers/my_routes.dart index 2b604c5..bab2866 100644 --- a/lib/core/routers/my_routes.dart +++ b/lib/core/routers/my_routes.dart @@ -6,6 +6,9 @@ import 'package:hadi_hoda_flutter/core/utils/my_context.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/ui/download_page.dart'; +import 'package:hadi_hoda_flutter/features/guider/presentation/bloc/guider_bloc.dart'; +import 'package:hadi_hoda_flutter/features/guider/presentation/bloc/guider_event.dart'; +import 'package:hadi_hoda_flutter/features/guider/presentation/ui/guider_page.dart'; import 'package:hadi_hoda_flutter/features/home/presentation/bloc/home_bloc.dart'; import 'package:hadi_hoda_flutter/features/home/presentation/ui/home_page.dart'; import 'package:hadi_hoda_flutter/features/intro/presentation/bloc/intro_bloc.dart'; @@ -37,6 +40,7 @@ class Routes { static const String languagePage = '/language_page'; static const String homePage = '/home_page'; static const String questionPage = '/question_page'; + static const String guiderPage = '/guider_page'; static const String levelPage = '/level_page'; } @@ -118,9 +122,20 @@ GoRouter _appPages() => GoRouter( child: const LevelPage(), ), ), + GoRoute( + name: Routes.guiderPage, + path: '${Routes.guiderPage}/:id', + builder: (context, state) => BlocProvider( + create: (context) => GuiderBloc(locator())..add(GetFirstLevelEvent( + id: state.pathParameters['id'], + )), + child: const GuiderPage(), + ), + ), GoRoute( name: Routes.questionPage, path: '${Routes.questionPage}/:id', + redirect: MyMiddlewares.question, builder: (context, state) => BlocProvider( create: (context) => QuestionBloc( diff --git a/lib/core/widgets/answer_box/answer_box.dart b/lib/core/widgets/answer_box/answer_box.dart index 148e81c..9f0f510 100644 --- a/lib/core/widgets/answer_box/answer_box.dart +++ b/lib/core/widgets/answer_box/answer_box.dart @@ -1,12 +1,10 @@ import 'package:flutter/material.dart'; import 'package:hadi_hoda_flutter/common_ui/resources/my_assets.dart'; import 'package:hadi_hoda_flutter/common_ui/resources/my_spaces.dart'; -import 'package:hadi_hoda_flutter/core/utils/my_localization.dart'; import 'package:hadi_hoda_flutter/core/utils/set_platform_size.dart'; import 'package:hadi_hoda_flutter/core/widgets/answer_box/styles/picture_box.dart'; import 'package:hadi_hoda_flutter/core/widgets/answer_box/styles/text_box.dart'; import 'package:hadi_hoda_flutter/core/widgets/images/my_image.dart'; -import 'package:hadi_hoda_flutter/core/widgets/showcase/my_showcase_widget.dart'; import 'package:hadi_hoda_flutter/features/question/domain/entity/answer_entity.dart'; class AnswerBox extends StatefulWidget { @@ -15,8 +13,6 @@ class AnswerBox extends StatefulWidget { required this.answer, required this.correctAnswer, required this.index, - required this.globalKey, - required this.answerGlobalKey, this.onTap, this.onNotifTap, }); @@ -26,8 +22,6 @@ class AnswerBox extends StatefulWidget { final void Function(bool isCorrect, int correctAnswer)? onTap; final int index; final Function(AnswerEntity answer)? onNotifTap; - final GlobalKey globalKey; - final GlobalKey answerGlobalKey; @override State createState() => _AnswerBoxState(); @@ -57,18 +51,14 @@ class _AnswerBoxState extends State { child: Stack( alignment: Alignment.topCenter, children: [ - MyShowcaseWidget( - globalKey: widget.answerGlobalKey, - description: context.translate.showcase_answer, - child: AnswerPictureBox( - selected: selected, - index: widget.index, - image: widget.answer.image ?? '', - correctAnswer: widget.correctAnswer, - onTap: () { - widget.onNotifTap?.call(widget.answer); - }, - ), + AnswerPictureBox( + selected: selected, + index: widget.index, + image: widget.answer.image ?? '', + correctAnswer: widget.correctAnswer, + onTap: () { + widget.onNotifTap?.call(widget.answer); + }, ), Positioned( left: 0, @@ -77,20 +67,23 @@ class _AnswerBoxState extends State { child: AnswerTextBox(text: widget.answer.title ?? ''), ), PositionedDirectional( - top: setSize(context: context, mobile: MySpaces.s12, tablet: MySpaces.s20), - end: setSize(context: context, mobile: MySpaces.s8, tablet: MySpaces.s20), - child: MyShowcaseWidget( - globalKey: widget.globalKey, - type: ShowcaseTooltipType.bottom, - description: context.translate.showcase_notif, - child: GestureDetector( - onTap: () { - widget.onNotifTap?.call(widget.answer); - }, - child: MyImage( - image: MyAssets.iconNotif, - size: setSize(context: context, tablet: 50), - ), + top: setSize( + context: context, + mobile: MySpaces.s12, + tablet: MySpaces.s20, + ), + end: setSize( + context: context, + mobile: MySpaces.s8, + tablet: MySpaces.s20, + ), + child: GestureDetector( + onTap: () { + widget.onNotifTap?.call(widget.answer); + }, + child: MyImage( + image: MyAssets.iconNotif, + size: setSize(context: context, tablet: 50), ), ), ), diff --git a/lib/core/widgets/answer_box/answer_box_showcase.dart b/lib/core/widgets/answer_box/answer_box_showcase.dart new file mode 100644 index 0000000..d0d1423 --- /dev/null +++ b/lib/core/widgets/answer_box/answer_box_showcase.dart @@ -0,0 +1,111 @@ +import 'package:flutter/material.dart'; +import 'package:hadi_hoda_flutter/common_ui/resources/my_assets.dart'; +import 'package:hadi_hoda_flutter/common_ui/resources/my_spaces.dart'; +import 'package:hadi_hoda_flutter/core/utils/my_localization.dart'; +import 'package:hadi_hoda_flutter/core/utils/set_platform_size.dart'; +import 'package:hadi_hoda_flutter/core/widgets/answer_box/styles/picture_box.dart'; +import 'package:hadi_hoda_flutter/core/widgets/answer_box/styles/text_box.dart'; +import 'package:hadi_hoda_flutter/core/widgets/images/my_image.dart'; +import 'package:hadi_hoda_flutter/core/widgets/showcase/my_showcase_widget.dart'; +import 'package:hadi_hoda_flutter/features/question/domain/entity/answer_entity.dart'; + +class AnswerBoxShowCase extends StatefulWidget { + const AnswerBoxShowCase({ + super.key, + required this.answer, + required this.correctAnswer, + required this.index, + this.globalKey, + this.answerGlobalKey, + this.onTap, + this.onNotifTap, + }); + + final AnswerEntity answer; + final int correctAnswer; + final void Function(bool isCorrect, int correctAnswer)? onTap; + final int index; + final Function(AnswerEntity answer)? onNotifTap; + final GlobalKey? globalKey; + final GlobalKey? answerGlobalKey; + + @override + State createState() => _AnswerBoxShowCaseState(); +} + +class _AnswerBoxShowCaseState extends State { + bool selected = false; + + @override + Widget build(BuildContext context) { + return Hero( + tag: 'Hero_answer_${widget.answer.id}', + child: Material( + type: MaterialType.transparency, + child: GestureDetector( + onTap: !selected + ? () { + setState(() { + selected = true; + }); + widget.onTap?.call( + widget.index == widget.correctAnswer, + widget.correctAnswer, + ); + } + : null, + child: Stack( + alignment: Alignment.topCenter, + children: [ + MyShowcaseWidget( + globalKey: widget.answerGlobalKey, + description: context.translate.showcase_answer, + child: AnswerPictureBox( + selected: selected, + index: widget.index, + image: widget.answer.image ?? '', + correctAnswer: widget.correctAnswer, + onTap: () { + widget.onNotifTap?.call(widget.answer); + }, + ), + ), + Positioned( + left: 0, + right: 0, + bottom: 0, + child: AnswerTextBox(text: widget.answer.title ?? ''), + ), + PositionedDirectional( + top: setSize( + context: context, + mobile: MySpaces.s12, + tablet: MySpaces.s20, + ), + end: setSize( + context: context, + mobile: MySpaces.s8, + tablet: MySpaces.s20, + ), + child: MyShowcaseWidget( + globalKey: widget.globalKey, + type: ShowcaseTooltipType.bottom, + description: context.translate.showcase_notif, + child: GestureDetector( + onTap: () { + widget.onNotifTap?.call(widget.answer); + }, + child: MyImage( + image: MyAssets.iconNotif, + size: setSize(context: context, tablet: 50), + ), + ), + ), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/core/widgets/showcase/my_showcase_widget.dart b/lib/core/widgets/showcase/my_showcase_widget.dart index 75ec9d5..31c7858 100644 --- a/lib/core/widgets/showcase/my_showcase_widget.dart +++ b/lib/core/widgets/showcase/my_showcase_widget.dart @@ -82,13 +82,13 @@ enum ShowcaseTooltipType { class MyShowcaseWidget extends StatelessWidget { const MyShowcaseWidget({ super.key, - required this.globalKey, required this.child, + this.globalKey, this.description, this.type = ShowcaseTooltipType.bottom, }); - final GlobalKey globalKey; + final GlobalKey? globalKey; final String? description; final Widget child; final ShowcaseTooltipType type; @@ -96,7 +96,7 @@ class MyShowcaseWidget extends StatelessWidget { @override Widget build(BuildContext context) { return Showcase( - key: globalKey, + key: globalKey ?? GlobalKey(), disableBarrierInteraction: false, targetShapeBorder: CircleBorder(), overlayColor: Color(0XFF0F0041), diff --git a/lib/features/guider/data/datasource/guider_datasource.dart b/lib/features/guider/data/datasource/guider_datasource.dart new file mode 100644 index 0000000..b2c2a12 --- /dev/null +++ b/lib/features/guider/data/datasource/guider_datasource.dart @@ -0,0 +1,33 @@ +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/utils/local_storage.dart'; +import 'package:hadi_hoda_flutter/features/level/domain/entity/level_entity.dart'; +import 'package:hadi_hoda_flutter/features/level/domain/entity/total_data_entity.dart'; +import 'package:hive/hive.dart'; + +abstract class IGuiderDatasource { + Future getLevel(); +} + +/// Local +class GuiderDatasourceImpl implements IGuiderDatasource { + const GuiderDatasourceImpl(); + + @override + Future getLevel() async { + try { + final String selectedLanguage = + LocalStorage.readData(key: MyConstants.selectLanguage) ?? + MyConstants.defaultLanguage; + final Box levelBox = Hive.box(MyConstants.levelBox); + final TotalDataEntity findData = levelBox.values.singleWhere( + (e) => e.code == selectedLanguage, + orElse: () => TotalDataEntity(), + ); + final LevelEntity? findLevel = findData.levels?.first; + return findLevel ?? LevelEntity(); + } catch (e) { + throw MyException(errorMessage: '$e'); + } + } +} diff --git a/lib/features/guider/data/repository_impl/guider_repository_impl.dart b/lib/features/guider/data/repository_impl/guider_repository_impl.dart new file mode 100644 index 0000000..941b6fd --- /dev/null +++ b/lib/features/guider/data/repository_impl/guider_repository_impl.dart @@ -0,0 +1,28 @@ +import 'package:flutter/foundation.dart'; +import 'package:hadi_hoda_flutter/core/error_handler/my_exception.dart'; +import 'package:hadi_hoda_flutter/core/utils/data_state.dart'; +import 'package:hadi_hoda_flutter/features/guider/data/datasource/guider_datasource.dart'; +import 'package:hadi_hoda_flutter/features/guider/domain/repository/guider_repository.dart'; +import 'package:hadi_hoda_flutter/features/level/domain/entity/level_entity.dart'; + +class GuiderRepositoryImpl implements IGuiderRepository { + final IGuiderDatasource datasource; + + const GuiderRepositoryImpl(this.datasource); + + @override + Future> getLevel() async { + try { + final LevelEntity response = await datasource.getLevel(); + return DataState.success(response); + } on MyException catch (e) { + return DataState.error(e); + } catch (e) { + if (kDebugMode) { + rethrow; + } else { + return DataState.error(MyException(errorMessage: '$e')); + } + } + } +} diff --git a/lib/features/guider/domain/repository/guider_repository.dart b/lib/features/guider/domain/repository/guider_repository.dart new file mode 100644 index 0000000..575a9bd --- /dev/null +++ b/lib/features/guider/domain/repository/guider_repository.dart @@ -0,0 +1,7 @@ +import 'package:hadi_hoda_flutter/core/error_handler/my_exception.dart'; +import 'package:hadi_hoda_flutter/core/utils/data_state.dart'; +import 'package:hadi_hoda_flutter/features/level/domain/entity/level_entity.dart'; + +abstract class IGuiderRepository { + Future> getLevel(); +} diff --git a/lib/features/guider/domain/usecases/get_first_level_usecase.dart b/lib/features/guider/domain/usecases/get_first_level_usecase.dart new file mode 100644 index 0000000..77d3eef --- /dev/null +++ b/lib/features/guider/domain/usecases/get_first_level_usecase.dart @@ -0,0 +1,17 @@ +import 'package:hadi_hoda_flutter/core/error_handler/my_exception.dart'; +import 'package:hadi_hoda_flutter/core/params/no_params.dart'; +import 'package:hadi_hoda_flutter/core/usecase/usecase.dart'; +import 'package:hadi_hoda_flutter/core/utils/data_state.dart'; +import 'package:hadi_hoda_flutter/features/guider/domain/repository/guider_repository.dart'; +import 'package:hadi_hoda_flutter/features/level/domain/entity/level_entity.dart'; + +class GetFirstLevelUseCase implements UseCase { + final IGuiderRepository repository; + + const GetFirstLevelUseCase(this.repository); + + @override + Future> call(NoParams params) { + return repository.getLevel(); + } +} diff --git a/lib/features/guider/presentation/bloc/guider_bloc.dart b/lib/features/guider/presentation/bloc/guider_bloc.dart new file mode 100644 index 0000000..96c3750 --- /dev/null +++ b/lib/features/guider/presentation/bloc/guider_bloc.dart @@ -0,0 +1,131 @@ +import 'dart:async'; + +import 'package:bloc/bloc.dart'; +import 'package:flutter/material.dart'; +import 'package:go_router/go_router.dart'; +import 'package:hadi_hoda_flutter/core/constants/my_constants.dart'; +import 'package:hadi_hoda_flutter/core/params/no_params.dart'; +import 'package:hadi_hoda_flutter/core/routers/my_routes.dart'; +import 'package:hadi_hoda_flutter/core/status/base_status.dart'; +import 'package:hadi_hoda_flutter/core/utils/local_storage.dart'; +import 'package:hadi_hoda_flutter/core/utils/my_context.dart'; +import 'package:hadi_hoda_flutter/features/guider/domain/usecases/get_first_level_usecase.dart'; +import 'package:hadi_hoda_flutter/features/guider/presentation/bloc/guider_event.dart'; +import 'package:hadi_hoda_flutter/features/guider/presentation/bloc/guider_state.dart'; +import 'package:hadi_hoda_flutter/features/level/domain/entity/level_entity.dart'; +import 'package:hadi_hoda_flutter/features/question/domain/entity/question_entity.dart'; +import 'package:showcaseview/showcaseview.dart'; + +class GuiderBloc extends Bloc { + /// ------------constructor------------ + GuiderBloc(this._getFirstLevelUseCase) : super(const GuiderState()) { + registerShowCase(); + on(_getFirstLevelEvent); + } + + @override + Future close() { + unRegisterShowCase(); + animationController.dispose(); + return super.close(); + } + + /// ------------UseCases------------ + final GetFirstLevelUseCase _getFirstLevelUseCase; + + /// ------------Variables------------ + final Map showCaseKey = { + 'answer_key_0': GlobalKey(), + 'answer_key_1': GlobalKey(), + 'answer_key_2': GlobalKey(), + 'answer_key_3': GlobalKey(), + 'notif_key_0': GlobalKey(), + 'notif_key_1': GlobalKey(), + 'notif_key_2': GlobalKey(), + 'notif_key_3': GlobalKey(), + 'stepper_key': GlobalKey(), + 'hadith_key': GlobalKey(), + 'guide_key': GlobalKey(), + }; + String? id; + + /// ------------Controllers------------ + late final AnimationController animationController; + + /// ------------Functions------------ + void registerShowCase() { + try { + ShowcaseView.register( + onStart: (showcaseIndex, key) { + LocalStorage.saveData(key: MyConstants.firstShowcase, value: 'true'); + }, + onDismiss: (onDismiss) { + MyContext.get.goNamed( + Routes.questionPage, + pathParameters: {'id': id ?? ''}, + ); + }, + onFinish: () { + MyContext.get.goNamed( + Routes.questionPage, + pathParameters: {'id': id ?? ''}, + ); + }, + ); + } catch (_) {} + } + + void unRegisterShowCase() { + try { + ShowcaseView.get().unregister(); + } catch (_) {} + } + + void startShowcase() { + if (LocalStorage.readData(key: MyConstants.firstShowcase) != 'true') { + try { + ShowcaseView.get().startShowCase([ + showCaseKey['answer_key_1']!, + showCaseKey['notif_key_0']!, + showCaseKey['stepper_key']!, + showCaseKey['hadith_key']!, + showCaseKey['guide_key']!, + ]); + } catch (_) {} + } + } + + /// ------------Event Calls------------ + + FutureOr _getFirstLevelEvent( + GetFirstLevelEvent event, + Emitter emit, + ) async { + id = event.id; + await _getFirstLevelUseCase(NoParams()).then( + (value) => value.fold((data) async { + final LevelEntity level = LevelEntity( + id: data.id, + order: data.order, + title: data.title, + questions: [ + ...?data.questions, + QuestionEntity(order: (data.questions?.length ?? 0) + 1), + ], + ); + emit( + state.copyWith( + getQuestionStatus: BaseComplete(''), + levelEntity: level, + currentQuestion: data.questions?.first, + ), + ); + await Future.delayed(Duration(milliseconds: 300), () { + animationController.forward().then((value) { + startShowcase(); + }); + }); + }, (error) {}), + ); + } +} diff --git a/lib/features/guider/presentation/bloc/guider_event.dart b/lib/features/guider/presentation/bloc/guider_event.dart new file mode 100644 index 0000000..0f2c910 --- /dev/null +++ b/lib/features/guider/presentation/bloc/guider_event.dart @@ -0,0 +1,8 @@ +sealed class GuiderEvent { + const GuiderEvent(); +} + +class GetFirstLevelEvent extends GuiderEvent { + final String? id; + const GetFirstLevelEvent({this.id}); +} diff --git a/lib/features/guider/presentation/bloc/guider_state.dart b/lib/features/guider/presentation/bloc/guider_state.dart new file mode 100644 index 0000000..2f399c5 --- /dev/null +++ b/lib/features/guider/presentation/bloc/guider_state.dart @@ -0,0 +1,27 @@ +import 'package:hadi_hoda_flutter/core/status/base_status.dart'; +import 'package:hadi_hoda_flutter/features/level/domain/entity/level_entity.dart'; +import 'package:hadi_hoda_flutter/features/question/domain/entity/question_entity.dart'; + +class GuiderState { + final BaseStatus getQuestionStatus; + final LevelEntity? levelEntity; + final QuestionEntity? currentQuestion; + + const GuiderState({ + this.getQuestionStatus = const BaseInit(), + this.levelEntity, + this.currentQuestion, + }); + + GuiderState copyWith({ + BaseStatus? getQuestionStatus, + LevelEntity? levelEntity, + QuestionEntity? currentQuestion, + }) { + return GuiderState( + getQuestionStatus: getQuestionStatus ?? this.getQuestionStatus, + levelEntity: levelEntity ?? this.levelEntity, + currentQuestion: currentQuestion ?? this.currentQuestion, + ); + } +} diff --git a/lib/features/guider/presentation/ui/guider_page.dart b/lib/features/guider/presentation/ui/guider_page.dart new file mode 100644 index 0000000..5058822 --- /dev/null +++ b/lib/features/guider/presentation/ui/guider_page.dart @@ -0,0 +1,247 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.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'; +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/utils/gap.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/fade_anim.dart'; +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'; +import 'package:hadi_hoda_flutter/features/guider/presentation/bloc/guider_state.dart'; +import 'package:hadi_hoda_flutter/features/question/domain/entity/answer_entity.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_stepper.dart'; +import 'package:hadi_hoda_flutter/features/question/presentation/ui/widgets/question_title.dart'; + +class GuiderPage extends StatefulWidget { + const GuiderPage({super.key}); + + @override + State createState() => _GuiderPageState(); +} + +class _GuiderPageState extends State with TickerProviderStateMixin { + + @override + void initState() { + super.initState(); + context + .read() + .animationController = AnimationController( + vsync: this, + duration: Duration(milliseconds: 500), + reverseDuration: Duration(milliseconds: 500), + ); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + body: MyPopScope( + child: Directionality( + textDirection: TextDirection.ltr, + child: Container( + height: context.heightScreen, + width: context.widthScreen, + padding: EdgeInsets.symmetric( + horizontal: + setSize( + context: context, + mobile: MySpaces.s16, + tablet: MySpaces.s30, + ) ?? + 0, + ), + decoration: BoxDecoration( + gradient: LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [Color(0XFF6930DA), Color(0XFF263AA1)], + ), + image: DecorationImage( + image: AssetImage(MyAssets.pattern), + scale: 3, + repeat: ImageRepeat.repeat, + colorFilter: ColorFilter.mode( + Colors.black.withValues(alpha: 0.3), + BlendMode.srcIn, + ), + ), + ), + child: Column( + children: [ + setPlatform( + android: MySpaces.s20, + iOS: 50, + )?.gapHeight ?? + SizedBox.shrink(), + _topButtons(context), + MySpaces.s10.gapHeight, + Expanded( + child: Column( + children: [ + _stepper(context), + _titles(context), + MySpaces.s20.gapHeight, + Expanded(child: _answers(context)), + _bottom(context), + ], + ), + ), + setPlatform(android: MySpaces.s20)?.gapHeight ?? + SizedBox.shrink(), + ], + ), + ), + ), + ), + ); + } + + Widget _topButtons(BuildContext context) { + return SlideDownFade( + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + GlassyButton( + image: MyAssets.home, + audio: MyAudios.back, + onTap: () {}, + ), + BlocBuilder( + builder: (context, state) => + QuestionTitle( + step: state.levelEntity?.order, + currentQuestion: state.currentQuestion?.order, + questionLength: state.levelEntity?.questions?.length, + ), + ), + GlassyButton(image: MyAssets.music, onTap: () {}), + ], + ), + ); + } + + Widget _stepper(BuildContext context) { + return FadeAnim( + child: MyShowcaseWidget( + globalKey: context + .read() + .showCaseKey['stepper_key']!, + description: context.translate.showcase_stepper, + child: QuestionStepper(length: 4, currentStep: 1), + ), + ); + } + + Widget _titles(BuildContext context) { + return BlocBuilder( + builder: (context, state) => + FadeAnim( + child: Text( + state.currentQuestion?.title ?? '', + textAlign: TextAlign.center, + maxLines: 3, + style: MYTextStyle.titr1.copyWith( + shadows: [ + BoxShadow( + offset: Offset(0, 2), + color: MyColors.black.withValues(alpha: 0.25), + ), + ], + ), + ), + ), + ); + } + + Widget _answers(BuildContext context) { + return BlocBuilder( + builder: (context, state) => + GridView.builder( + itemCount: state.currentQuestion?.answers?.length ?? 0, + physics: NeverScrollableScrollPhysics(), + shrinkWrap: true, + clipBehavior: Clip.none, + padding: EdgeInsets.symmetric( + horizontal: setSize(context: context, tablet: 70) ?? 0, + ), + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 2, + crossAxisSpacing: MySpaces.s20, + mainAxisSpacing: 20, + childAspectRatio: 0.75, + ), + itemBuilder: (context, index) => + state.currentQuestion?.answers?[index].imageId == null + ? SizedBox.shrink() + : SlideAnim( + controller: context.read().animationController, + index: index, + child: AnswerBoxShowCase( + globalKey: context + .read() + .showCaseKey['notif_key_$index']!, + answerGlobalKey: context + .read() + .showCaseKey['answer_key_$index']!, + index: state.currentQuestion?.answers?[index].order ?? 1, + answer: + state.currentQuestion?.answers?[index] ?? AnswerEntity(), + correctAnswer: 0, + onNotifTap: (AnswerEntity answer) {}, + onTap: (isCorrect, correctAnswer) {}, + ), + ), + ), + ); + } + + Widget _bottom(BuildContext context) { + return SlideUpFade( + child: SizedBox( + width: context.widthScreen, + child: Stack( + alignment: AlignmentDirectional.centerStart, + children: [ + MyShowcaseWidget( + globalKey: context + .read() + .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), + ), + ), + PositionedDirectional( + end: 0, + child: MyShowcaseWidget( + globalKey: context + .read() + .showCaseKey['hadith_key']!, + type: ShowcaseTooltipType.topLeft, + description: context.translate.showcase_hadith, + child: GlassyButton(image: MyAssets.leaf, onTap: () {}), + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/features/question/presentation/bloc/question_bloc.dart b/lib/features/question/presentation/bloc/question_bloc.dart index c633716..4fe07d3 100644 --- a/lib/features/question/presentation/bloc/question_bloc.dart +++ b/lib/features/question/presentation/bloc/question_bloc.dart @@ -21,7 +21,6 @@ import 'package:hadi_hoda_flutter/features/question/domain/usecases/get_next_lev import 'package:hadi_hoda_flutter/features/question/presentation/bloc/question_event.dart'; import 'package:hadi_hoda_flutter/features/question/presentation/bloc/question_state.dart'; import 'package:hadi_hoda_flutter/features/question/presentation/ui/screens/answer_screen.dart'; -import 'package:showcaseview/showcaseview.dart'; class QuestionBloc extends Bloc { /// ------------constructor------------ @@ -34,14 +33,12 @@ class QuestionBloc extends Bloc { volumeStream = _mainAudioService.volumeStream(); playingStream = _mainAudioService.playingStream(); initAudios(); - registerShowCase(); on(_getLevelEvent); on(_chooseAnswerEvent); } @override Future close() { - unRegisterShowCase(); if (_mainAudioService.audioVolume != 0) { _mainAudioService.setVolume(volume: MyConstants.musicAudioVolume); } @@ -83,43 +80,6 @@ class QuestionBloc extends Bloc { final ScrollController titleController = ScrollController(); /// ------------Functions------------ - void registerShowCase() { - try { - ShowcaseView.register( - onStart: (showcaseIndex, key) { - LocalStorage.saveData(key: MyConstants.firstShowcase, value: 'true'); - }, - onDismiss: (onDismiss) async { - await playQuestionAudio(); - }, - onFinish: () async { - await playQuestionAudio(); - }, - ); - } catch (_) {} - } - - void unRegisterShowCase() { - try { - ShowcaseView.get().unregister(); - } catch (_) {} - } - - - void startShowcase() { - if (LocalStorage.readData(key: MyConstants.firstShowcase) != 'true') { - try { - ShowcaseView.get().startShowCase([ - showCaseKey['answer_key_1']!, - showCaseKey['notif_key_0']!, - showCaseKey['stepper_key']!, - showCaseKey['hadith_key']!, - showCaseKey['guide_key']!, - ]); - } catch (_) {} - } - } - void startScrollTitle({Duration? audioDuration}) { if (audioDuration == null || audioDuration == Duration.zero) return; titleController.animateTo( @@ -283,18 +243,11 @@ class QuestionBloc extends Bloc { levelEntity: level, currentQuestion: data.questions?.first, )); - if(LocalStorage.readData(key: MyConstants.firstShowcase) != 'true'){ - await Future.delayed(Duration(milliseconds: 500)); - answerAnimationController.forward().then((value) { - startShowcase(); - }); - } else { - await playQuestionAudio(); - imageAnimationController.reverse(); - answerAnimationController.forward().then((value) { - showQueueAnswer(); - }); - } + await playQuestionAudio(); + imageAnimationController.reverse(); + answerAnimationController.forward().then((value) { + showQueueAnswer(); + }); }, (error) { emit(state.copyWith(getQuestionStatus: BaseError(error.errorMessage))); diff --git a/lib/features/question/presentation/ui/screens/question_screen.dart b/lib/features/question/presentation/ui/screens/question_screen.dart index d0202bf..21ddf89 100644 --- a/lib/features/question/presentation/ui/screens/question_screen.dart +++ b/lib/features/question/presentation/ui/screens/question_screen.dart @@ -18,7 +18,6 @@ import 'package:hadi_hoda_flutter/core/widgets/animations/slide_up_fade.dart'; import 'package:hadi_hoda_flutter/core/widgets/answer_box/answer_box.dart'; import 'package:hadi_hoda_flutter/core/widgets/answer_box/styles/picture_box.dart'; import 'package:hadi_hoda_flutter/core/widgets/images/my_image.dart'; -import 'package:hadi_hoda_flutter/core/widgets/showcase/my_showcase_widget.dart'; import 'package:hadi_hoda_flutter/features/question/domain/entity/answer_entity.dart'; import 'package:hadi_hoda_flutter/features/question/presentation/bloc/question_bloc.dart'; import 'package:hadi_hoda_flutter/features/question/presentation/bloc/question_event.dart'; @@ -115,14 +114,10 @@ class _QuestionScreenState extends State with TickerProviderStat return BlocBuilder( buildWhen: (previous, current) => previous.currentQuestion?.id != current.currentQuestion?.id, - builder: (context, state) => MyShowcaseWidget( - globalKey: context.read().showCaseKey['stepper_key']!, - description: context.translate.showcase_stepper, - child: FadeAnim( - child: QuestionStepper( - length: state.levelEntity?.questions?.length ?? 0, - currentStep: state.currentQuestion?.order ?? 1, - ), + builder: (context, state) => FadeAnim( + child: QuestionStepper( + length: state.levelEntity?.questions?.length ?? 0, + currentStep: state.currentQuestion?.order ?? 1, ), ), ); @@ -184,12 +179,8 @@ class _QuestionScreenState extends State with TickerProviderStat controller: context.read().answerAnimationController, index: index, child: AnswerBox( - globalKey: context.read().showCaseKey['notif_key_$index']!, - answerGlobalKey: context.read().showCaseKey['answer_key_$index']!, index: state.currentQuestion?.answers?[index].order ?? 1, - answer: - state.currentQuestion?.answers?[index] ?? - AnswerEntity(), + answer: state.currentQuestion?.answers?[index] ?? AnswerEntity(), correctAnswer: state.currentQuestion?.correctAnswer ?? 0, onNotifTap: (AnswerEntity answer) { context.read().showAnswerDialog( @@ -214,20 +205,15 @@ class _QuestionScreenState extends State with TickerProviderStat child: Stack( alignment: AlignmentDirectional.centerStart, children: [ - MyShowcaseWidget( - globalKey: context.read().showCaseKey['guide_key']!, - description: context.translate.showcase_guide, - type: ShowcaseTooltipType.top, - child: StreamBuilder( - initialData: false, - stream: context.read().playingStream, - builder: (context, snapshot) => GlobeAnimation( - state: snapshot.data ?? false, - child: MyImage( - image: MyAssets.globe, - fit: BoxFit.cover, - size: setSize(context: context, tablet: 120), - ), + StreamBuilder( + initialData: false, + stream: context.read().playingStream, + builder: (context, snapshot) => GlobeAnimation( + state: snapshot.data ?? false, + child: MyImage( + image: MyAssets.globe, + fit: BoxFit.cover, + size: setSize(context: context, tablet: 120), ), ), ), @@ -255,15 +241,10 @@ class _QuestionScreenState extends State with TickerProviderStat ), PositionedDirectional( end: 0, - child: MyShowcaseWidget( - globalKey: context.read().showCaseKey['hadith_key']!, - type: ShowcaseTooltipType.topLeft, - description: context.translate.showcase_hadith, - child: GlassyButton( - image: MyAssets.leaf, - onTap: () => - context.read().showHadith(context: context), - ), + child: GlassyButton( + image: MyAssets.leaf, + onTap: () => + context.read().showHadith(context: context), ), ), ], diff --git a/lib/features/sample/presentation/ui/sample_page.dart b/lib/features/sample/presentation/ui/sample_page.dart index 5378b9f..848565d 100644 --- a/lib/features/sample/presentation/ui/sample_page.dart +++ b/lib/features/sample/presentation/ui/sample_page.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:lottie/lottie.dart'; class SamplePage extends StatelessWidget { const SamplePage({super.key}); @@ -8,9 +7,7 @@ class SamplePage extends StatelessWidget { Widget build(BuildContext context) { return Scaffold( body: Center( - child: Lottie.asset( - 'assets/animations/Celebration.json', - ), + child: Text('Sample Page'), ), ); } diff --git a/lib/init_bindings.dart b/lib/init_bindings.dart index 234c512..c950e18 100644 --- a/lib/init_bindings.dart +++ b/lib/init_bindings.dart @@ -13,6 +13,10 @@ import 'package:hadi_hoda_flutter/features/download/domain/usecases/get_audios_u import 'package:hadi_hoda_flutter/features/download/domain/usecases/get_images_usecase.dart'; import 'package:hadi_hoda_flutter/features/download/domain/usecases/loading_stream_usecase.dart'; import 'package:hadi_hoda_flutter/features/download/domain/usecases/save_levels_usecase.dart'; +import 'package:hadi_hoda_flutter/features/guider/data/datasource/guider_datasource.dart'; +import 'package:hadi_hoda_flutter/features/guider/data/repository_impl/guider_repository_impl.dart'; +import 'package:hadi_hoda_flutter/features/guider/domain/repository/guider_repository.dart'; +import 'package:hadi_hoda_flutter/features/guider/domain/usecases/get_first_level_usecase.dart'; import 'package:hadi_hoda_flutter/features/level/data/datasource/level_datasource.dart'; import 'package:hadi_hoda_flutter/features/level/data/repository_impl/level_repository_impl.dart'; import 'package:hadi_hoda_flutter/features/level/domain/entity/level_entity.dart'; @@ -63,6 +67,11 @@ void initBindings() { locator.registerLazySingleton(() => SaveLevelsUseCase(locator())); locator.registerLazySingleton(() => LoadingStreamUseCase(locator())); + /// Guider Feature + locator.registerLazySingleton(() => GuiderDatasourceImpl()); + locator.registerLazySingleton(() => GuiderRepositoryImpl(locator())); + locator.registerLazySingleton(() => GetFirstLevelUseCase(locator())); + /// Question Feature locator.registerLazySingleton(() => QuestionDatasourceImpl()); locator.registerLazySingleton(() => QuestionRepositoryImpl(locator()));