diff --git a/assets/images/button_3.svg b/assets/images/button_3.svg new file mode 100644 index 0000000..81bec97 --- /dev/null +++ b/assets/images/button_3.svg @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/images/diamond_big.png b/assets/images/diamond_big.png new file mode 100644 index 0000000..40c9ec3 Binary files /dev/null and b/assets/images/diamond_big.png differ diff --git a/assets/images/happy_persons.png b/assets/images/happy_persons.png new file mode 100644 index 0000000..529898e Binary files /dev/null and b/assets/images/happy_persons.png differ diff --git a/assets/images/shiny.png b/assets/images/shiny.png new file mode 100644 index 0000000..684f16a Binary files /dev/null and b/assets/images/shiny.png differ diff --git a/assets/images/ship.png b/assets/images/ship.png new file mode 100644 index 0000000..cfadb1a Binary files /dev/null and b/assets/images/ship.png differ diff --git a/lib/common_ui/resources/my_assets.dart b/lib/common_ui/resources/my_assets.dart index 7eb12af..33e746f 100644 --- a/lib/common_ui/resources/my_assets.dart +++ b/lib/common_ui/resources/my_assets.dart @@ -10,6 +10,7 @@ class MyAssets { static const String musicOn = 'assets/images/music_on.svg'; static const String button = 'assets/images/button.svg'; static const String button2 = 'assets/images/button_2.svg'; + static const String button3 = 'assets/images/button_3.svg'; static const String theme = 'assets/images/theme.svg'; static const String facebook = 'assets/images/facebook.svg'; static const String whatsapp = 'assets/images/whatsapp.svg'; @@ -40,4 +41,8 @@ class MyAssets { static const String lang = 'assets/images/lang.svg'; static const String error = 'assets/images/error.png'; static const String leaf = 'assets/images/leaf.png'; + static const String happyPersons = 'assets/images/happy_persons.png'; + static const String diamondBig = 'assets/images/diamond_big.png'; + static const String ship = 'assets/images/ship.png'; + static const String shiny = 'assets/images/shiny.png'; } \ No newline at end of file diff --git a/lib/common_ui/resources/my_text_style.dart b/lib/common_ui/resources/my_text_style.dart index c9ca2d8..c8d64c9 100644 --- a/lib/common_ui/resources/my_text_style.dart +++ b/lib/common_ui/resources/my_text_style.dart @@ -23,6 +23,11 @@ class DinoKids { fontSize: 35, fontWeight: FontWeight.w400, ); + static const TextStyle regular40 = TextStyle( + fontFamily: fontFamily, + fontSize: 40, + fontWeight: FontWeight.w400, + ); static const TextStyle regular45 = TextStyle( fontFamily: fontFamily, fontSize: 45, @@ -87,4 +92,9 @@ class Marhey { fontSize: 14, fontWeight: FontWeight.w700, ); + static const TextStyle bold26 = TextStyle( + fontFamily: fontFamily, + fontSize: 26, + fontWeight: FontWeight.w700, + ); } diff --git a/lib/core/routers/my_routes.dart b/lib/core/routers/my_routes.dart index 1260c79..a4cf4a9 100644 --- a/lib/core/routers/my_routes.dart +++ b/lib/core/routers/my_routes.dart @@ -80,7 +80,7 @@ GoRouter get appPages => GoRouter( path: '${Routes.questionPage}/:id', builder: (context, state) => BlocProvider( create: (context) => - QuestionBloc(locator()) + QuestionBloc(locator(), locator()) ..add(GetLevelEvent(state.pathParameters['id'])), child: const QuestionPage(), ), diff --git a/lib/core/widgets/answer_box/answer_box.dart b/lib/core/widgets/answer_box/answer_box.dart index 1f36cd1..947e77c 100644 --- a/lib/core/widgets/answer_box/answer_box.dart +++ b/lib/core/widgets/answer_box/answer_box.dart @@ -15,7 +15,7 @@ class AnswerBox extends StatefulWidget { final AnswerEntity answer; final int correctAnswer; - final VoidCallback? onTap; + final void Function(bool isCorrect)? onTap; final int index; @override @@ -26,7 +26,6 @@ class _AnswerBoxState extends State { bool selected = false; - @override Widget build(BuildContext context) { return GestureDetector( @@ -34,9 +33,7 @@ class _AnswerBoxState extends State { setState(() { selected = true; }); - if (selected && (widget.index == widget.correctAnswer)) { - widget.onTap?.call(); - } + widget.onTap?.call(widget.index == widget.correctAnswer); } : null, child: SizedBox( child: Stack( diff --git a/lib/core/widgets/confetti/my_confetti.dart b/lib/core/widgets/confetti/my_confetti.dart new file mode 100644 index 0000000..1a101f9 --- /dev/null +++ b/lib/core/widgets/confetti/my_confetti.dart @@ -0,0 +1,62 @@ +import 'dart:math'; + +import 'package:confetti/confetti.dart'; +import 'package:flutter/material.dart'; + +class MyConfetti extends StatelessWidget { + const MyConfetti({super.key, required this.controller}); + + final ConfettiController controller; + + @override + Widget build(BuildContext context) { + return ConfettiWidget( + confettiController: controller, + shouldLoop: false, + blastDirectionality: BlastDirectionality.explosive, + blastDirection: pi / 2, + // ↓ down + gravity: 0.2, + // let pieces fall + emissionFrequency: 0.5, + numberOfParticles: 15, + particleDrag: 0.1, + colors: const [ + Colors.red, + Colors.orange, + Colors.yellow, + Colors.green, + Colors.blue, + Colors.purple, + Colors.pink, + Colors.cyan, + ], + createParticlePath: (size) { + double degToRad(double deg) => deg * (pi / 180.0); + + const numberOfPoints = 5; + final halfWidth = size.width / 2; + final externalRadius = halfWidth; + final internalRadius = halfWidth / 2.5; + final degreesPerStep = degToRad(360 / numberOfPoints); + final halfDegreesPerStep = degreesPerStep / 2; + final path = Path(); + final fullAngle = degToRad(360); + path.moveTo(size.width, halfWidth); + + for (double step = 0; step < fullAngle; step += degreesPerStep) { + path.lineTo( + halfWidth + externalRadius * cos(step), + halfWidth + externalRadius * sin(step), + ); + path.lineTo( + halfWidth + internalRadius * cos(step + halfDegreesPerStep), + halfWidth + internalRadius * sin(step + halfDegreesPerStep), + ); + } + path.close(); + return path; + }, + ); + } +} diff --git a/lib/features/home/presentation/bloc/home_bloc.dart b/lib/features/home/presentation/bloc/home_bloc.dart index 342cc3c..c9c59a8 100644 --- a/lib/features/home/presentation/bloc/home_bloc.dart +++ b/lib/features/home/presentation/bloc/home_bloc.dart @@ -1,4 +1,5 @@ import 'dart:async'; + import 'package:bloc/bloc.dart'; import 'package:flutter/cupertino.dart'; import 'package:go_router/go_router.dart'; diff --git a/lib/features/level/presentation/bloc/level_bloc.dart b/lib/features/level/presentation/bloc/level_bloc.dart index ee6461e..36e8d40 100644 --- a/lib/features/level/presentation/bloc/level_bloc.dart +++ b/lib/features/level/presentation/bloc/level_bloc.dart @@ -70,7 +70,7 @@ class LevelBloc extends Bloc { /// ------------Functions------------ void goToQuestionPage(BuildContext context, LevelEntity level){ - context.pushNamed( + context.pushReplacementNamed( Routes.questionPage, pathParameters: { 'id': '${level.id}', @@ -109,10 +109,17 @@ class LevelBloc extends Bloc { if(data.length > 8){ top12LevelList.addAll(data.sublist(8, data.length)); } - emit(state.copyWith( - getLevelStatus: const BaseComplete(''), - chooseLevel: data.singleWhere((e) => e.order == currentLevel), - )); + try { + emit(state.copyWith( + getLevelStatus: const BaseComplete(''), + chooseLevel: data.singleWhere((e) => e.order == currentLevel), + )); + } catch (e) { + emit(state.copyWith( + getLevelStatus: const BaseComplete(''), + chooseLevel: LevelEntity(), + )); + } add(StartScrollEvent()); }, (error) {}, diff --git a/lib/features/level/presentation/ui/level_page.dart b/lib/features/level/presentation/ui/level_page.dart index 08ba410..fd6c9ab 100644 --- a/lib/features/level/presentation/ui/level_page.dart +++ b/lib/features/level/presentation/ui/level_page.dart @@ -43,16 +43,22 @@ class LevelPage extends StatelessWidget { buildWhen: (previous, current) => previous.chooseLevel?.id != current.chooseLevel?.id, builder: (context, state) { - return Positioned( - bottom: MediaQuery.viewPaddingOf(context).bottom + MySpaces.s10, - right: MySpaces.s16, - left: MySpaces.s16, - child: HintLevelWidget( - level: state.chooseLevel ?? LevelEntity(), - onTap: (level) => - context.read().goToQuestionPage(context, level), - ), - ); + if (state.chooseLevel?.id != null) { + return Positioned( + bottom: MediaQuery + .viewPaddingOf(context) + .bottom + MySpaces.s10, + right: MySpaces.s16, + left: MySpaces.s16, + child: HintLevelWidget( + level: state.chooseLevel ?? LevelEntity(), + onTap: (level) => + context.read().goToQuestionPage(context, level), + ), + ); + } else { + return SizedBox.shrink(); + } } ); } diff --git a/lib/features/question/data/datasource/question_datasource.dart b/lib/features/question/data/datasource/question_datasource.dart index cb4357c..c5e8b37 100644 --- a/lib/features/question/data/datasource/question_datasource.dart +++ b/lib/features/question/data/datasource/question_datasource.dart @@ -8,6 +8,7 @@ import 'package:hive/hive.dart'; abstract class IQuestionDatasource { Future getLevel({required QuestionParams params}); + Future getNextLevel({required QuestionParams params}); } /// Local @@ -33,4 +34,29 @@ class QuestionDatasourceImpl implements IQuestionDatasource { throw MyException(errorMessage: '$e'); } } + + @override + Future getNextLevel({required QuestionParams params}) async { + try { + final String selectedLanguage = LocalStorage.readData( + key: MyConstants.selectLanguage) ?? 'fa'; + final int index = int.parse( + LocalStorage.readData(key: MyConstants.currentLevel) ?? '1'); + final Box levelBox = Hive.box(MyConstants.levelBox); + final TotalDataEntity findData = levelBox.values.singleWhere( + (e) => e.code == selectedLanguage, + orElse: () => TotalDataEntity(), + ); + if(index > (findData.levels?.length ?? 0)){ + throw MyException(); + } + final LevelEntity? findLevel = findData.levels?.singleWhere( + (e) => e.order == index, + orElse: () => LevelEntity(), + ); + return findLevel ?? LevelEntity(); + } catch (e) { + throw MyException(errorMessage: '$e'); + } + } } diff --git a/lib/features/question/data/repository_impl/question_repository_impl.dart b/lib/features/question/data/repository_impl/question_repository_impl.dart index ffdf456..d1c8df7 100644 --- a/lib/features/question/data/repository_impl/question_repository_impl.dart +++ b/lib/features/question/data/repository_impl/question_repository_impl.dart @@ -1,6 +1,6 @@ -import 'package:hadi_hoda_flutter/core/params/question_params.dart'; import 'package:flutter/foundation.dart'; import 'package:hadi_hoda_flutter/core/error_handler/my_exception.dart'; +import 'package:hadi_hoda_flutter/core/params/question_params.dart'; import 'package:hadi_hoda_flutter/core/utils/data_state.dart'; import 'package:hadi_hoda_flutter/features/level/domain/entity/level_entity.dart'; import 'package:hadi_hoda_flutter/features/question/data/datasource/question_datasource.dart'; @@ -26,4 +26,20 @@ class QuestionRepositoryImpl implements IQuestionRepository { } } } + + @override + Future> getNextLevel({required QuestionParams params}) async { + try { + final LevelEntity response = await datasource.getNextLevel(params: params); + 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/question/domain/entity/answer_entity.dart b/lib/features/question/domain/entity/answer_entity.dart index b883bbd..82d0269 100644 --- a/lib/features/question/domain/entity/answer_entity.dart +++ b/lib/features/question/domain/entity/answer_entity.dart @@ -1,5 +1,3 @@ -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/storage_path.dart'; import 'package:hadi_hoda_flutter/features/question/domain/entity/file_entity.dart'; import 'package:hive/hive.dart'; @@ -32,7 +30,6 @@ class AnswerEntity extends HiveObject { this.isActive, this.image, }){ - image = - '${StoragePath.documentDir.path}/${LocalStorage.readData(key: MyConstants.selectLanguage)}/files/images/${imageInfo?.filename ?? ''}'; + image = '${StoragePath.documentDir.path}/images/${imageInfo?.filename}'; } } diff --git a/lib/features/question/domain/entity/question_entity.dart b/lib/features/question/domain/entity/question_entity.dart index 3407e84..beec745 100644 --- a/lib/features/question/domain/entity/question_entity.dart +++ b/lib/features/question/domain/entity/question_entity.dart @@ -1,3 +1,6 @@ +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/storage_path.dart'; import 'package:hadi_hoda_flutter/features/question/domain/entity/answer_entity.dart'; import 'package:hadi_hoda_flutter/features/question/domain/entity/file_entity.dart'; import 'package:hive/hive.dart'; @@ -22,6 +25,8 @@ class QuestionEntity extends HiveObject { bool? isActive; @HiveField(7) List? answers; + @HiveField(8) + String? audio; QuestionEntity({ this.id, @@ -32,5 +37,8 @@ class QuestionEntity extends HiveObject { this.correctAnswer, this.isActive, this.answers, - }); + }){ + audio = + '${StoragePath.documentDir.path}/${LocalStorage.readData(key: MyConstants.selectLanguage)}/audio/${audioInfo?.filename}'; + } } diff --git a/lib/features/question/domain/entity/question_entity.g.dart b/lib/features/question/domain/entity/question_entity.g.dart index db9d48a..b66b41c 100644 --- a/lib/features/question/domain/entity/question_entity.g.dart +++ b/lib/features/question/domain/entity/question_entity.g.dart @@ -25,13 +25,13 @@ class QuestionEntityAdapter extends TypeAdapter { correctAnswer: fields[5] as int?, isActive: fields[6] as bool?, answers: (fields[7] as List?)?.cast(), - ); + )..audio = fields[8] as String?; } @override void write(BinaryWriter writer, QuestionEntity obj) { writer - ..writeByte(8) + ..writeByte(9) ..writeByte(0) ..write(obj.id) ..writeByte(1) @@ -47,7 +47,9 @@ class QuestionEntityAdapter extends TypeAdapter { ..writeByte(6) ..write(obj.isActive) ..writeByte(7) - ..write(obj.answers); + ..write(obj.answers) + ..writeByte(8) + ..write(obj.audio); } @override diff --git a/lib/features/question/domain/repository/question_repository.dart b/lib/features/question/domain/repository/question_repository.dart index 9c2fd60..507d38b 100644 --- a/lib/features/question/domain/repository/question_repository.dart +++ b/lib/features/question/domain/repository/question_repository.dart @@ -5,4 +5,5 @@ import 'package:hadi_hoda_flutter/features/level/domain/entity/level_entity.dart abstract class IQuestionRepository { Future> getLevel({required QuestionParams params}); + Future> getNextLevel({required QuestionParams params}); } diff --git a/lib/features/question/domain/usecases/get_next_level_usecase.dart b/lib/features/question/domain/usecases/get_next_level_usecase.dart new file mode 100644 index 0000000..3882c1e --- /dev/null +++ b/lib/features/question/domain/usecases/get_next_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/question_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/level/domain/entity/level_entity.dart'; +import 'package:hadi_hoda_flutter/features/question/domain/repository/question_repository.dart'; + +class GetNextLevelUseCase implements UseCase { + final IQuestionRepository repository; + + const GetNextLevelUseCase(this.repository); + + @override + Future> call(QuestionParams params) { + return repository.getNextLevel(params: params); + } +} diff --git a/lib/features/question/presentation/bloc/question_bloc.dart b/lib/features/question/presentation/bloc/question_bloc.dart index e270d2e..5c0a87f 100644 --- a/lib/features/question/presentation/bloc/question_bloc.dart +++ b/lib/features/question/presentation/bloc/question_bloc.dart @@ -1,11 +1,20 @@ import 'dart:async'; + import 'package:bloc/bloc.dart'; +import 'package:confetti/confetti.dart'; import 'package:flutter/cupertino.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/question_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/context_provider.dart'; +import 'package:hadi_hoda_flutter/core/utils/local_storage.dart'; import 'package:hadi_hoda_flutter/core/widgets/hadith_dialog/hadith_dialog.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:hadi_hoda_flutter/features/question/domain/usecases/get_level_usecase.dart'; +import 'package:hadi_hoda_flutter/features/question/domain/usecases/get_next_level_usecase.dart'; 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:showcaseview/showcaseview.dart'; @@ -14,13 +23,22 @@ class QuestionBloc extends Bloc { /// ------------constructor------------ QuestionBloc( this._getLevelUseCase, - ) : super(const QuestionState()) { + this._getNextLevelUseCase, + ) : super(QuestionState()) { on(_getLevelEvent); - on(_changeQuestionEvent); + on(_chooseAnswerEvent); + on(_getNextLevelEvent); + } + + @override + Future close() { + confettiController.dispose(); + return super.close(); } /// ------------UseCases------------ final GetLevelUseCase _getLevelUseCase; + final GetNextLevelUseCase _getNextLevelUseCase; /// ------------Variables------------ final List keys = [ @@ -32,6 +50,9 @@ class QuestionBloc extends Bloc { /// ------------Controllers------------ + final ConfettiController confettiController = ConfettiController( + duration: Duration(seconds: 1), + ); /// ------------Functions------------ void startShowCase({required BuildContext context}) { @@ -42,15 +63,28 @@ class QuestionBloc extends Bloc { showHadithDialog(context: context); } + void goToLevelPage({required BuildContext context}) { + context.pushReplacement(Routes.levelPage); + } + /// ------------Event Calls------------ FutureOr _getLevelEvent(GetLevelEvent event, Emitter emit) async { await _getLevelUseCase(QuestionParams(id: int.parse(event.id ?? '0'))).then( (value) { value.fold( (data) { + 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: data, + levelEntity: level, currentQuestion: data.questions?.first, )); }, @@ -62,17 +96,50 @@ class QuestionBloc extends Bloc { ); } - FutureOr _changeQuestionEvent( - ChangeQuestionEvent event, - Emitter emit, - ) async { - await Future.delayed(Duration(seconds: 1), () { - final QuestionEntity? findPreQuestion = state.currentQuestion; - final int findPreIndex = (findPreQuestion?.order ?? 1) - 1; - final int newIndex = findPreIndex + 1; - emit( - state.copyWith(currentQuestion: state.levelEntity?.questions?[newIndex]), - ); - }); + FutureOr _chooseAnswerEvent(ChooseAnswerEvent event, + Emitter emit,) async { + emit(state.copyWith(correctAnswer: event.chooseCorrectAnswer)); + + if (event.chooseCorrectAnswer) { + confettiController.play(); + await Future.delayed(Duration(seconds: 2), () 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) { + int currentLevel = int.parse( + LocalStorage.readData(key: MyConstants.currentLevel) ?? '1'); + ++currentLevel; + await LocalStorage.saveData( + key: MyConstants.currentLevel, + value: '$currentLevel', + ); + } + }); + } + } + + FutureOr _getNextLevelEvent(GetNextLevelEvent event, + Emitter emit) async { + await _getNextLevelUseCase(QuestionParams()).then((value) => + value.fold( + (data) { + ContextProvider.context.pushReplacementNamed( + Routes.questionPage, + pathParameters: { + 'id': '${data.id}' + }, + ); + }, + (error) { + goToLevelPage(context: ContextProvider.context); + }, + ), + ); } } diff --git a/lib/features/question/presentation/bloc/question_event.dart b/lib/features/question/presentation/bloc/question_event.dart index cf7aa05..e16f525 100644 --- a/lib/features/question/presentation/bloc/question_event.dart +++ b/lib/features/question/presentation/bloc/question_event.dart @@ -7,5 +7,11 @@ class GetLevelEvent extends QuestionEvent { const GetLevelEvent(this.id); } -class ChangeQuestionEvent extends QuestionEvent { +class ChooseAnswerEvent extends QuestionEvent { + final bool chooseCorrectAnswer; + const ChooseAnswerEvent(this.chooseCorrectAnswer); +} + +class GetNextLevelEvent extends QuestionEvent { + const GetNextLevelEvent(); } diff --git a/lib/features/question/presentation/bloc/question_state.dart b/lib/features/question/presentation/bloc/question_state.dart index 3a1d8b6..c73a0de 100644 --- a/lib/features/question/presentation/bloc/question_state.dart +++ b/lib/features/question/presentation/bloc/question_state.dart @@ -6,22 +6,26 @@ class QuestionState { final BaseStatus getQuestionStatus; final LevelEntity? levelEntity; final QuestionEntity? currentQuestion; + final bool? correctAnswer; const QuestionState({ this.getQuestionStatus = const BaseInit(), this.levelEntity, this.currentQuestion, + this.correctAnswer, }); QuestionState copyWith({ BaseStatus? getQuestionStatus, LevelEntity? levelEntity, QuestionEntity? currentQuestion, + bool? correctAnswer, }) { return QuestionState( getQuestionStatus: getQuestionStatus ?? this.getQuestionStatus, levelEntity: levelEntity ?? this.levelEntity, currentQuestion: currentQuestion ?? this.currentQuestion, + correctAnswer: correctAnswer, ); } } diff --git a/lib/features/question/presentation/ui/question_page.dart b/lib/features/question/presentation/ui/question_page.dart index f0037a8..9a7912b 100644 --- a/lib/features/question/presentation/ui/question_page.dart +++ b/lib/features/question/presentation/ui/question_page.dart @@ -5,20 +5,15 @@ 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_image.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/answer_box/answer_box.dart'; -import 'package:hadi_hoda_flutter/core/widgets/showcase/question_showcase.dart'; -import 'package:hadi_hoda_flutter/features/question/domain/entity/answer_entity.dart'; +import 'package:hadi_hoda_flutter/core/widgets/confetti/my_confetti.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'; import 'package:hadi_hoda_flutter/features/question/presentation/bloc/question_state.dart'; +import 'package:hadi_hoda_flutter/features/question/presentation/ui/screens/diamond_screen.dart'; +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/left_blob.dart'; import 'package:hadi_hoda_flutter/features/question/presentation/ui/widgets/question_stepper.dart'; -import 'package:hadi_hoda_flutter/features/question/presentation/ui/widgets/refresh_button.dart'; -import 'package:hadi_hoda_flutter/features/question/presentation/ui/widgets/right_blob.dart'; import 'package:showcaseview/showcaseview.dart'; class QuestionPage extends StatelessWidget { @@ -53,14 +48,28 @@ class QuestionPage extends StatelessWidget { padding: const EdgeInsets.symmetric(horizontal: MySpaces.s16), child: Column( children: [ + MyConfetti( + controller: context.read().confettiController, + ), MySpaces.s4.gapHeight, _topButtons(context), MySpaces.s10.gapHeight, _stepper(), - _titles(), - MySpaces.s14.gapHeight, - _answers(), - _bottomDetail(context), + Expanded( + child: BlocBuilder( + buildWhen: (previous, current) => + (previous.currentQuestion?.order != + current.currentQuestion?.order), + builder: (context, state) { + if (state.currentQuestion?.order == + state.levelEntity?.questions?.length) { + return DiamondScreen(); + } else { + return QuestionScreen(); + } + }, + ), + ), ], ), ), @@ -77,7 +86,10 @@ class QuestionPage extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center, children: [ - GlassyButton(image: MyAssets.home, onTap: () {}), + GlassyButton( + image: MyAssets.home, + onTap: () => context.read().goToLevelPage(context: context), + ), Spacer(), BlocBuilder( builder: (context, state) => Text( @@ -106,103 +118,4 @@ class QuestionPage extends StatelessWidget { ), ); } - - - Column _titles() { - return Column( - spacing: MySpaces.s4, - children: [ - BlocBuilder( - builder: (context, state) => Text( - '${context.translate.question} ${state.currentQuestion?.order ?? 1} / ${state.levelEntity?.questions?.length ?? 0}', - style: Marhey.medium12.copyWith( - color: MyColors.white.withValues(alpha: 0.5), - shadows: [ - Shadow( - offset: Offset(0, 1), - blurRadius: 1, - color: Color(0xFF000000).withValues(alpha: 0.25), - ), - ], - ), - ), - ), - BlocBuilder( - builder: (context, state) => Text( - state.currentQuestion?.title ?? '', - textAlign: TextAlign.center, - style: Marhey.semiBold22.copyWith( - color: MyColors.white, - shadows: [ - Shadow( - offset: Offset(0, 1), - blurRadius: 1, - color: Color(0xFF000000).withValues(alpha: 0.25), - ), - ], - ), - ), - ), - ], - ); - } - - Expanded _answers() { - return Expanded( - child: GridView.builder( - itemCount: 4, - gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( - crossAxisCount: 2, - crossAxisSpacing: MySpaces.s20, - mainAxisSpacing: 50, - ), - itemBuilder: (context, index) => QuestionShowcase( - globalKey: context.read().keys[index], - description: context.translate.tap_to_select, - child: BlocBuilder( - builder: (context, state) => AnswerBox( - key: Key('${state.currentQuestion?.id}'), - index: state.currentQuestion?.answers?[index].order ?? 1, - answer: state.currentQuestion?.answers?[index] ?? AnswerEntity(), - correctAnswer: state.currentQuestion?.correctAnswer ?? 0, - onTap: () => context.read().add(ChangeQuestionEvent()), - ), - ), - ), - ), - ); - } - - Widget _bottomDetail(BuildContext context) { - return SizedBox( - width: context.widthScreen, - child: Stack( - clipBehavior: Clip.none, - alignment: Alignment.center, - children: [ - PositionedDirectional( - start: 0, - top: -10, - child: LeftBlob(), - ), - Padding( - padding: const EdgeInsetsDirectional.only(end: 60), - child: MyImage(image: MyAssets.persons), - ), - PositionedDirectional( - start: 210, - top: -20, - child: RightBlob(), - ), - PositionedDirectional( - end: 0, - bottom: 10, - child: RefreshButton( - onTap: () {}, - ), - ), - ], - ), - ); - } } diff --git a/lib/features/question/presentation/ui/screens/diamond_screen.dart b/lib/features/question/presentation/ui/screens/diamond_screen.dart new file mode 100644 index 0000000..4cd796c --- /dev/null +++ b/lib/features/question/presentation/ui/screens/diamond_screen.dart @@ -0,0 +1,164 @@ +import 'dart:ui'; + +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_colors.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_image.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/button/enum/button_type.dart'; +import 'package:hadi_hoda_flutter/core/widgets/button/my_button.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'; + +class DiamondScreen extends StatelessWidget { + const DiamondScreen({super.key}); + + @override + Widget build(BuildContext context) { + return Column( + mainAxisAlignment: MainAxisAlignment.spaceAround, + mainAxisSize: MainAxisSize.max, + children: [ + _title(context), + 80.0.gapHeight, + Column( + children: [ + _diamonds(context), + _mainText(context), + ], + ), + Spacer(), + Stack( + alignment: Alignment.center, + clipBehavior: Clip.none, + children: [ + _ship(context), + _btns(context), + ], + ), + ], + ); + } + + Text _title(BuildContext context) { + return Text( + context.translate.you_win, + style: Marhey.semiBold22.copyWith(color: MyColors.white), + ); + } + + SizedBox _diamonds(BuildContext context) { + return SizedBox( + width: context.widthScreen, + height: context.heightScreen / 3, + child: Stack( + alignment: Alignment.center, + children: [ + PositionedDirectional( + start: 20, + top: 0, + child: SizedBox( + height: 50, + width: 50, + child: Transform.rotate( + angle: -0.5, + child: Stack( + children: [ + MyImage(image: MyAssets.diamondBig), + ClipRRect( + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 4, sigmaY: 4), + child: SizedBox(width: 50, height: 50), + ), + ), + ], + ), + ), + ), + ), + PositionedDirectional( + end: 0, + top: 30, + child: Transform.rotate( + angle: 0.5, + child: Stack( + children: [ + MyImage(image: MyAssets.diamondBig, size: 60), + ClipRRect( + child: BackdropFilter( + filter: ImageFilter.blur(sigmaX: 3, sigmaY: 3), + child: SizedBox(width: 100, height: 100), + ), + ), + ], + ), + ), + ), + Positioned(top: 100, child: MyImage(image: MyAssets.diamondBig)), + ], + ), + ); + } + + Widget _mainText(BuildContext context){ + return ShaderMask( + blendMode: BlendMode.srcIn, + shaderCallback: (bounds) => LinearGradient( + begin: Alignment.centerLeft, + end: Alignment.centerRight, + colors: [MyColors.white, Color(0XFF63D4F9)], + ).createShader(bounds), + child: Text( + context.translate.you_got_diamond, + style: Marhey + .bold26, // The color here will be overridden by the ShaderMask + ), + ); + } + + PositionedDirectional _ship(BuildContext context) { + return PositionedDirectional( + end: context.widthScreen / 10, + top: -80, + child: MyImage(image: MyAssets.ship), + ); + } + + Row _btns(BuildContext context) { + return Row( + children: [ + Expanded( + child: InkWell( + onTap: () => context.read().goToLevelPage(context: context), + child: Stack( + alignment: Alignment.center, + children: [ + MyImage(image: MyAssets.button3, size: 84), + Positioned( + top: 10, + child: Text( + context.translate.view_map, + style: DinoKids.regular35.copyWith( + color: Color(0XFFD93D16), + ), + ), + ), + ], + ), + ), + ), + Expanded( + child: MyButton( + onTap: () => context.read().add(GetNextLevelEvent()), + title: context.translate.go_next, + type: ButtonType.type2, + ), + ), + ], + ); + } +} diff --git a/lib/features/question/presentation/ui/screens/question_screen.dart b/lib/features/question/presentation/ui/screens/question_screen.dart new file mode 100644 index 0000000..dc069aa --- /dev/null +++ b/lib/features/question/presentation/ui/screens/question_screen.dart @@ -0,0 +1,141 @@ +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_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_image.dart'; +import 'package:hadi_hoda_flutter/core/utils/my_localization.dart'; +import 'package:hadi_hoda_flutter/core/widgets/answer_box/answer_box.dart'; +import 'package:hadi_hoda_flutter/core/widgets/showcase/question_showcase.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'; +import 'package:hadi_hoda_flutter/features/question/presentation/bloc/question_state.dart'; +import 'package:hadi_hoda_flutter/features/question/presentation/ui/widgets/left_blob.dart'; +import 'package:hadi_hoda_flutter/features/question/presentation/ui/widgets/refresh_button.dart'; +import 'package:hadi_hoda_flutter/features/question/presentation/ui/widgets/right_blob.dart'; + +class QuestionScreen extends StatelessWidget { + const QuestionScreen({super.key}); + + @override + Widget build(BuildContext context) { + return Column( + children: [ + _titles(), + MySpaces.s14.gapHeight, + _answers(), + _bottomDetail(context), + ], + ); + } + + Column _titles() { + return Column( + spacing: MySpaces.s4, + children: [ + BlocBuilder( + builder: (context, state) => Text( + '${context.translate.question} ${state.currentQuestion?.order ?? 1} / ${(state.levelEntity?.questions?.length ?? 0) - 1}', + style: Marhey.medium12.copyWith( + color: MyColors.white.withValues(alpha: 0.5), + shadows: [ + Shadow( + offset: Offset(0, 1), + blurRadius: 1, + color: Color(0xFF000000).withValues(alpha: 0.25), + ), + ], + ), + ), + ), + BlocBuilder( + builder: (context, state) => Text( + state.currentQuestion?.title ?? '', + textAlign: TextAlign.center, + style: Marhey.semiBold22.copyWith( + color: MyColors.white, + shadows: [ + Shadow( + offset: Offset(0, 1), + blurRadius: 1, + color: Color(0xFF000000).withValues(alpha: 0.25), + ), + ], + ), + ), + ), + ], + ); + } + + Expanded _answers() { + return Expanded( + child: GridView.builder( + itemCount: 4, + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + crossAxisCount: 2, + crossAxisSpacing: MySpaces.s20, + mainAxisSpacing: 50, + ), + itemBuilder: (context, index) => QuestionShowcase( + globalKey: context.read().keys[index], + description: context.translate.tap_to_select, + child: BlocBuilder( + builder: (context, state) => AnswerBox( + key: Key('${state.currentQuestion?.id}'), + index: state.currentQuestion?.answers?[index].order ?? 1, + answer: state.currentQuestion?.answers?[index] ?? AnswerEntity(), + correctAnswer: state.currentQuestion?.correctAnswer ?? 0, + onTap: (isCorrect) => + context.read().add( + ChooseAnswerEvent(isCorrect), + ), + ), + ), + ), + ), + ); + } + + Widget _bottomDetail(BuildContext context) { + return Row( + children: [ + Spacer(), + BlocBuilder( + builder: (context, state) => Stack( + clipBehavior: Clip.none, + children: [ + if(state.correctAnswer == false) + PositionedDirectional( + start: -100, + top: -10, + child: LeftBlob(), + ), + MyImage( + image: state.correctAnswer == true + ? MyAssets.happyPersons + : MyAssets.persons, + fit: BoxFit.contain, + size: 110, + ), + if(state.correctAnswer == false) + PositionedDirectional( + top: -30, + end: -90, + child: RightBlob(), + ), + ], + ), + ), + Spacer(), + RefreshButton( + onTap: () {}, + ), + ], + ); + } +} + diff --git a/lib/features/question/presentation/ui/widgets/left_blob.dart b/lib/features/question/presentation/ui/widgets/left_blob.dart index 66f16f3..f09256b 100644 --- a/lib/features/question/presentation/ui/widgets/left_blob.dart +++ b/lib/features/question/presentation/ui/widgets/left_blob.dart @@ -2,6 +2,7 @@ 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_text_style.dart'; import 'package:hadi_hoda_flutter/core/utils/my_image.dart'; +import 'package:hadi_hoda_flutter/core/utils/my_localization.dart'; class LeftBlob extends StatelessWidget { const LeftBlob({super.key}); @@ -13,7 +14,7 @@ class LeftBlob extends StatelessWidget { children: [ MyImage(image: MyAssets.bubbleChatLeft), Text( - 'Your answer\nwas not correct.', + context.translate.wrong_answer, textAlign: TextAlign.center, style: Marhey.medium12.copyWith( color: Color(0XFFB5AEEE), diff --git a/lib/features/question/presentation/ui/widgets/question_stepper.dart b/lib/features/question/presentation/ui/widgets/question_stepper.dart index 555861d..bcb4a39 100644 --- a/lib/features/question/presentation/ui/widgets/question_stepper.dart +++ b/lib/features/question/presentation/ui/widgets/question_stepper.dart @@ -4,7 +4,7 @@ import 'package:hadi_hoda_flutter/common_ui/resources/my_assets.dart'; import 'package:hadi_hoda_flutter/core/utils/my_image.dart'; class QuestionStepper extends StatelessWidget { - const QuestionStepper({super.key, this.length = 0, this.currentStep = 0}); + const QuestionStepper({super.key, required this.length ,required this.currentStep}); final int length; final int currentStep; @@ -32,9 +32,9 @@ class QuestionStepper extends StatelessWidget { padding: EdgeInsets.all(0), enableStepTapping: false, steps: List.generate( - length + 1, + length, (index) => EasyStep( - customStep: index == length + customStep: index == length - 1 ? MyImage(image: MyAssets.diamond, size: 50) : ClipPath( clipper: _StepperClipper(), diff --git a/lib/features/question/presentation/ui/widgets/right_blob.dart b/lib/features/question/presentation/ui/widgets/right_blob.dart index 7ed2aa8..03f4268 100644 --- a/lib/features/question/presentation/ui/widgets/right_blob.dart +++ b/lib/features/question/presentation/ui/widgets/right_blob.dart @@ -2,6 +2,7 @@ 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_text_style.dart'; import 'package:hadi_hoda_flutter/core/utils/my_image.dart'; +import 'package:hadi_hoda_flutter/core/utils/my_localization.dart'; class RightBlob extends StatelessWidget { const RightBlob({super.key}); @@ -13,7 +14,7 @@ class RightBlob extends StatelessWidget { children: [ MyImage(image: MyAssets.bubbleChatRight), Text( - 'Be more\ncareful.', + context.translate.be_cureful, textAlign: TextAlign.center, style: Marhey.medium12.copyWith( color: Color(0XFFB5AEEE), diff --git a/lib/init_bindings.dart b/lib/init_bindings.dart index e936c1b..84d2bfb 100644 --- a/lib/init_bindings.dart +++ b/lib/init_bindings.dart @@ -28,6 +28,7 @@ import 'package:hadi_hoda_flutter/features/question/domain/entity/file_entity.da import 'package:hadi_hoda_flutter/features/question/domain/entity/question_entity.dart'; import 'package:hadi_hoda_flutter/features/question/domain/repository/question_repository.dart'; import 'package:hadi_hoda_flutter/features/question/domain/usecases/get_level_usecase.dart'; +import 'package:hadi_hoda_flutter/features/question/domain/usecases/get_next_level_usecase.dart'; import 'package:hadi_hoda_flutter/features/sample/data/datasource/sample_datasource.dart'; import 'package:hadi_hoda_flutter/features/sample/data/repository_impl/sample_repository_impl.dart'; import 'package:hadi_hoda_flutter/features/sample/domain/repository/sample_repository.dart'; @@ -67,6 +68,7 @@ void initBindings() { locator.registerLazySingleton(() => QuestionDatasourceImpl()); locator.registerLazySingleton(() => QuestionRepositoryImpl(locator())); locator.registerLazySingleton(() => GetLevelUseCase(locator())); + locator.registerLazySingleton(() => GetNextLevelUseCase(locator())); /// Level Feature locator.registerLazySingleton(() => LocalLevelDatasourceImpl()); diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index c2e8258..a4e49ea 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -11,5 +11,11 @@ "connected_to_internet": "You must be connected to the internet to download the initial game data.", "start": "Start", "step": "Step", - "question": "Question" + "question": "Question", + "be_cureful": "Be more\ncareful.", + "wrong_answer": "Your answer\nwas not correct.", + "you_got_diamond": "You got the diamond", + "view_map": "View Map", + "go_next": "Go Next", + "you_win": "You Win!" } \ No newline at end of file diff --git a/lib/l10n/app_localizations.dart b/lib/l10n/app_localizations.dart index a2a4745..ac161b7 100644 --- a/lib/l10n/app_localizations.dart +++ b/lib/l10n/app_localizations.dart @@ -171,6 +171,42 @@ abstract class AppLocalizations { /// In en, this message translates to: /// **'Question'** String get question; + + /// No description provided for @be_cureful. + /// + /// In en, this message translates to: + /// **'Be more\ncareful.'** + String get be_cureful; + + /// No description provided for @wrong_answer. + /// + /// In en, this message translates to: + /// **'Your answer\nwas not correct.'** + String get wrong_answer; + + /// No description provided for @you_got_diamond. + /// + /// In en, this message translates to: + /// **'You got the diamond'** + String get you_got_diamond; + + /// No description provided for @view_map. + /// + /// In en, this message translates to: + /// **'View Map'** + String get view_map; + + /// No description provided for @go_next. + /// + /// In en, this message translates to: + /// **'Go Next'** + String get go_next; + + /// No description provided for @you_win. + /// + /// In en, this message translates to: + /// **'You Win!'** + String get you_win; } class _AppLocalizationsDelegate diff --git a/lib/l10n/app_localizations_en.dart b/lib/l10n/app_localizations_en.dart index a855516..04b092b 100644 --- a/lib/l10n/app_localizations_en.dart +++ b/lib/l10n/app_localizations_en.dart @@ -48,4 +48,22 @@ class AppLocalizationsEn extends AppLocalizations { @override String get question => 'Question'; + + @override + String get be_cureful => 'Be more\ncareful.'; + + @override + String get wrong_answer => 'Your answer\nwas not correct.'; + + @override + String get you_got_diamond => 'You got the diamond'; + + @override + String get view_map => 'View Map'; + + @override + String get go_next => 'Go Next'; + + @override + String get you_win => 'You Win!'; } diff --git a/pubspec.lock b/pubspec.lock index 857e00e..b77f400 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -161,6 +161,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.19.1" + confetti: + dependency: "direct main" + description: + name: confetti + sha256: "79376a99648efbc3f23582f5784ced0fe239922bd1a0fb41f582051eba750751" + url: "https://pub.dev" + source: hosted + version: "0.8.0" convert: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 0061d6c..8f6a09e 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -8,6 +8,7 @@ environment: dependencies: bloc: ^9.0.0 + confetti: ^0.8.0 dio: ^5.9.0 easy_stepper: ^0.8.5+1 equatable: ^2.0.7