diff --git a/assets/svg/check.svg b/assets/svg/check.svg new file mode 100644 index 0000000..9b0d0d5 --- /dev/null +++ b/assets/svg/check.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/assets/videos/globe_state_after_wrong.gif b/assets/videos/globe_state_after_wrong.gif index 60070d6..cc7614b 100644 Binary files a/assets/videos/globe_state_after_wrong.gif and b/assets/videos/globe_state_after_wrong.gif differ diff --git a/assets/videos/globe_state_normal.gif b/assets/videos/globe_state_normal.gif index 2b22c94..f2e6cae 100644 Binary files a/assets/videos/globe_state_normal.gif and b/assets/videos/globe_state_normal.gif differ diff --git a/assets/videos/globe_state_speaking.gif b/assets/videos/globe_state_speaking.gif index 49b3f20..5e3f3c5 100644 Binary files a/assets/videos/globe_state_speaking.gif and b/assets/videos/globe_state_speaking.gif differ diff --git a/assets/videos/globe_state_success.gif b/assets/videos/globe_state_success.gif index 4c41ce3..6ed1fe3 100644 Binary files a/assets/videos/globe_state_success.gif and b/assets/videos/globe_state_success.gif differ diff --git a/assets/videos/globe_state_wrong.gif b/assets/videos/globe_state_wrong.gif index 845abf5..1d0cb72 100644 Binary files a/assets/videos/globe_state_wrong.gif and b/assets/videos/globe_state_wrong.gif differ diff --git a/lib/common_ui/resources/my_assets.dart b/lib/common_ui/resources/my_assets.dart index 8e16d4c..e43ed0a 100644 --- a/lib/common_ui/resources/my_assets.dart +++ b/lib/common_ui/resources/my_assets.dart @@ -73,6 +73,7 @@ class MyAssets { static const String iconPlay = 'assets/svg/icon_play.svg.vec'; 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 final List images = [ backgroundHome, diff --git a/lib/core/constants/my_constants.dart b/lib/core/constants/my_constants.dart index 41d2335..a8813e7 100644 --- a/lib/core/constants/my_constants.dart +++ b/lib/core/constants/my_constants.dart @@ -30,8 +30,8 @@ class MyConstants { static const double effectAudioVolume = 0.2; static const String defaultLanguage = 'en'; static const List languages = [ - LanguageEntity(title: 'English (English)', code: 'en'), - LanguageEntity(title: 'German (Germany)', code: 'de'), - LanguageEntity(title: 'Arabic (العربية)', code: 'ar'), + LanguageEntity(displayName: 'English (English)', code: 'en', title: ''), + LanguageEntity(displayName: 'German (Germany)', code: 'de', title: ''), + LanguageEntity(displayName: 'Arabic (العربية)', code: 'ar', title: ''), ]; } diff --git a/lib/core/widgets/answer_box/answer_box_showcase.dart b/lib/core/widgets/answer_box/answer_box_showcase.dart index d0d1423..b1bd522 100644 --- a/lib/core/widgets/answer_box/answer_box_showcase.dart +++ b/lib/core/widgets/answer_box/answer_box_showcase.dart @@ -89,6 +89,7 @@ class _AnswerBoxShowCaseState extends State { ), child: MyShowcaseWidget( globalKey: widget.globalKey, + highlightRadiusIncrease: 15, type: ShowcaseTooltipType.bottom, description: context.translate.showcase_notif, child: GestureDetector( diff --git a/lib/core/widgets/showcase/my_showcase_widget.dart b/lib/core/widgets/showcase/my_showcase_widget.dart index 4b762f1..0b22112 100644 --- a/lib/core/widgets/showcase/my_showcase_widget.dart +++ b/lib/core/widgets/showcase/my_showcase_widget.dart @@ -86,12 +86,14 @@ class MyShowcaseWidget extends StatelessWidget { this.globalKey, this.description, this.type = ShowcaseTooltipType.bottom, + this.highlightRadiusIncrease = 0, }); final GlobalKey? globalKey; final String? description; final Widget child; final ShowcaseTooltipType type; + final double highlightRadiusIncrease; @override Widget build(BuildContext context) { @@ -99,6 +101,7 @@ class MyShowcaseWidget extends StatelessWidget { key: globalKey ?? GlobalKey(), disableBarrierInteraction: false, targetShapeBorder: const CircleBorder(), + targetPadding: EdgeInsets.all(highlightRadiusIncrease), overlayColor: const Color(0XFF0F0041), overlayOpacity: 0.82, /// ToolTip diff --git a/lib/features/app/presentation/bloc/app_bloc.dart b/lib/features/app/presentation/bloc/app_bloc.dart index 1c5c206..eebdb70 100644 --- a/lib/features/app/presentation/bloc/app_bloc.dart +++ b/lib/features/app/presentation/bloc/app_bloc.dart @@ -21,7 +21,7 @@ class AppBloc extends Bloc { // Try to find in constants first, or create a temporary one to get locale final LanguageEntity findLanguage = MyConstants.languages.firstWhere( (e) => e.code == selectLanguage, - orElse: () => LanguageEntity(code: selectLanguage, title: ''), + orElse: () => LanguageEntity(code: selectLanguage, title: '', displayName: ''), ); emit(state.copyWith(locale: findLanguage.locale)); diff --git a/lib/features/download/data/datasource/download_datasource.dart b/lib/features/download/data/datasource/download_datasource.dart index c125f3c..bb41a2b 100644 --- a/lib/features/download/data/datasource/download_datasource.dart +++ b/lib/features/download/data/datasource/download_datasource.dart @@ -68,6 +68,13 @@ class DownloadDatasourceImpl implements IDownloadDatasource { int _audioCompleted = 0; int _targetLevel = 0; + Future _ensureLevelFolderExists(String levelPath) async { + final Directory directory = Directory(levelPath); + if (!await directory.exists()) { + await directory.create(recursive: true); + } + } + void _emitProgress() { final int totalTasks = _targetLevel * 2; final int completedTasks = _imageCompleted + _audioCompleted; @@ -89,17 +96,7 @@ class DownloadDatasourceImpl implements IDownloadDatasource { final Directory directory = Directory(levelPath); - if (!await directory.exists()) { - return false; - } - - final List files = directory.listSync(recursive: false); - - if (files.isEmpty) { - return false; - } - - return true; + return directory.exists(); } Future _getLevelsImages(int toLevel) async { @@ -135,6 +132,9 @@ class DownloadDatasourceImpl implements IDownloadDatasource { if (response is Response && response.statusCode == 204) { debugPrint("No image content for level $level (204)"); + await _ensureLevelFolderExists( + '${StoragePath.documentDir.path}/$level/', + ); _imageCompleted++; _emitProgress(); return; @@ -178,17 +178,7 @@ class DownloadDatasourceImpl implements IDownloadDatasource { final Directory directory = Directory(levelPath); - if (!await directory.exists()) { - return false; - } - - final List files = directory.listSync(recursive: false); - - if (files.isEmpty) { - return false; - } - - return true; + return directory.exists(); } Future _getLevelsAudios(int toLevel) async { @@ -232,6 +222,9 @@ class DownloadDatasourceImpl implements IDownloadDatasource { if (response is Response && response.statusCode == 204) { debugPrint("No audio content for level $level (204)"); + await _ensureLevelFolderExists( + '${StoragePath.documentDir.path}/$lang/$level/', + ); _audioCompleted++; _emitProgress(); return; diff --git a/lib/features/download/presentation/ui/download_page.dart b/lib/features/download/presentation/ui/download_page.dart index 9ac0b4d..daa3b67 100644 --- a/lib/features/download/presentation/ui/download_page.dart +++ b/lib/features/download/presentation/ui/download_page.dart @@ -25,7 +25,7 @@ import 'package:hadi_hoda_flutter/features/download/presentation/ui/widgets/down class DownloadPage extends StatefulWidget { final DownloadPageConfig config; - const DownloadPage({super.key,required this.config}); + const DownloadPage({super.key, required this.config}); @override State createState() => _DownloadPageState(); @@ -70,8 +70,8 @@ class _DownloadPageState extends State { ), child: BlocConsumer( listener: (context, state) { - if(state.status is BaseComplete) { - if(widget.config.redirectTo == Routes.homePage) { + if (state.status is BaseComplete) { + if (widget.config.redirectTo == Routes.homePage) { context.goNamed(Routes.homePage); } else { context.pushNamed( @@ -87,7 +87,9 @@ class _DownloadPageState extends State { return Padding( padding: EdgeInsets.symmetric( vertical: - MediaQuery.viewPaddingOf(context).bottom + MySpaces.s16, + MediaQuery + .viewPaddingOf(context) + .bottom + MySpaces.s16, horizontal: 60, ), child: ErrorState( @@ -130,15 +132,28 @@ class _DownloadPageState extends State { bottom: 130, child: Column( spacing: MySpaces.s6, + crossAxisAlignment: CrossAxisAlignment.stretch, children: [ - Text(context.translate.please_wait, style: MYTextStyle.titr0), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: Text(context.translate.please_wait, + style: MYTextStyle.titr0.copyWith(fontSize: 24), + textAlign: TextAlign.center, + maxLines: 1, + overflow: TextOverflow.ellipsis), + ), StreamBuilder( initialData: DownloadEntity.empty(), - stream: context.read().loadingStream, - builder: (context, snapshot) => Text( - 'Downloading ...${snapshot.data?.downloadedLevels}/${widget.config.downloadToLevel}', - style: MYTextStyle.matn3, - ), + stream: context + .read() + .loadingStream, + builder: (context, snapshot) => + Text( + 'Downloading ...${snapshot.data?.downloadedLevels}/${widget + .config.downloadToLevel}', + textAlign: TextAlign.center, + style: MYTextStyle.matn3, + ), ), ], ), @@ -149,7 +164,9 @@ class _DownloadPageState extends State { return Positioned( bottom: MySpaces.s40, child: DownloadLoadingWidget( - loadingStream: context.read().loadingStream, + loadingStream: context + .read() + .loadingStream, ), ); } diff --git a/lib/features/language/data/model/language_model.dart b/lib/features/language/data/model/language_model.dart index 3383b39..fa3fc52 100644 --- a/lib/features/language/data/model/language_model.dart +++ b/lib/features/language/data/model/language_model.dart @@ -1,12 +1,13 @@ import 'package:hadi_hoda_flutter/features/language/domain/entity/language_entity.dart'; class LanguageModel extends LanguageEntity { - const LanguageModel({required super.code, required super.title}); + const LanguageModel({required super.code,required super.displayName, required super.title}); factory LanguageModel.fromJson(Map json) { return LanguageModel( code: json['code'] ?? '', title: json['name'] ?? '', // API returns 'name', we map to 'title' + displayName: json['display_name'] ?? '', // API returns 'name', we map to 'title' ); } } diff --git a/lib/features/language/domain/entity/language_entity.dart b/lib/features/language/domain/entity/language_entity.dart index b5068a1..b43153f 100644 --- a/lib/features/language/domain/entity/language_entity.dart +++ b/lib/features/language/domain/entity/language_entity.dart @@ -4,8 +4,9 @@ import 'package:equatable/equatable.dart'; class LanguageEntity extends Equatable { final String code; final String title; + final String displayName; - const LanguageEntity({required this.code, required this.title}); + const LanguageEntity({required this.code, required this.title, required this.displayName}); Locale get locale { final parts = code.split('_'); diff --git a/lib/features/language/presentation/ui/language_page.dart b/lib/features/language/presentation/ui/language_page.dart index 768e99f..a934b2a 100644 --- a/lib/features/language/presentation/ui/language_page.dart +++ b/lib/features/language/presentation/ui/language_page.dart @@ -133,11 +133,14 @@ class _LanguagePageState extends State { Center( child: Container( width: double.infinity, - height: itemSize + 4, + alignment: Alignment.centerLeft, + height: itemSize, + padding: const EdgeInsets.symmetric(horizontal: 18), decoration: BoxDecoration( - color: Colors.black.withOpacity(0.5), - borderRadius: BorderRadius.circular(16), + color: Colors.white.withValues(alpha: .2), + borderRadius: BorderRadius.circular(12), ), + child: const MyImage(image: MyAssets.check), ), ), WheelChooser.choices( @@ -156,17 +159,17 @@ class _LanguagePageState extends State { } }, selectTextStyle: MYTextStyle.titr1.copyWith( - fontSize: itemSize * 0.4, + fontSize: itemSize * 0.34, color: Colors.white, ), unSelectTextStyle: MYTextStyle.titr1.copyWith( - fontSize: itemSize * 0.4, + fontSize: itemSize * 0.34, color: Colors.white.withOpacity(0.5), ), choices: state.languages.map((lang) { return WheelChoice( value: lang, - title: lang.title, + title: lang.displayName, ); }).toList(), ), @@ -191,15 +194,13 @@ class _LanguagePageState extends State { mainAxisAlignment: MainAxisAlignment.center, children: [ const MyImage(image: MyAssets.lang, size: 28), - Expanded( - child: AutoSizeText( - context.translate.select_language, - minFontSize: 12, - maxFontSize: 20, - maxLines: 1, - textAlign: TextAlign.center, - style: MYTextStyle.titr0.copyWith(color: const Color(0XFF847AC4)), - ), + AutoSizeText( + context.translate.select_language, + minFontSize: 12, + maxFontSize: 20, + maxLines: 1, + textAlign: TextAlign.center, + style: MYTextStyle.titr0.copyWith(color: const Color(0XFF847AC4)), ), ], ); diff --git a/lib/features/level/presentation/bloc/level_bloc.dart b/lib/features/level/presentation/bloc/level_bloc.dart index 54017d9..851cbb6 100644 --- a/lib/features/level/presentation/bloc/level_bloc.dart +++ b/lib/features/level/presentation/bloc/level_bloc.dart @@ -84,7 +84,7 @@ class LevelBloc extends Bloc { ), LevelLocation( bottom: setSize(context: MyContext.get, mobile: 0.33.h, tablet: 0.55.h), - left: setSize(context: MyContext.get, mobile: 0.65.w, tablet: 0.8.w), + left: setSize(context: MyContext.get, mobile: 0.63.w, tablet: 0.8.w), index: 5, ), LevelLocation( diff --git a/lib/features/level/presentation/ui/level_page.dart b/lib/features/level/presentation/ui/level_page.dart index 462e697..aca3773 100644 --- a/lib/features/level/presentation/ui/level_page.dart +++ b/lib/features/level/presentation/ui/level_page.dart @@ -329,22 +329,22 @@ class _LevelPageState extends State { ); } - Widget _downloadIndicator() { - return BlocBuilder( - buildWhen: (prev, curr) => prev.status != curr.status, - builder: (context, downloadState) { - if (downloadState.status is! BaseLoading) { - return const SizedBox.shrink(); - } - return Padding( - padding: const EdgeInsets.only(top: MySpaces.s10), - child: DownloadLoadingWidget( - loadingStream: context.read().loadingStream, - ), - ); - }, - ); - } + // Widget _downloadIndicator() { + // return BlocBuilder( + // buildWhen: (prev, curr) => prev.status != curr.status, + // builder: (context, downloadState) { + // if (downloadState.status is! BaseLoading) { + // return const SizedBox.shrink(); + // } + // return Padding( + // padding: const EdgeInsets.only(top: MySpaces.s10), + // child: DownloadLoadingWidget( + // loadingStream: context.read().loadingStream, + // ), + // ); + // }, + // ); + // } Widget _background(BuildContext context) { return const MyImage( diff --git a/lib/features/level/presentation/ui/widgets/play_button.dart b/lib/features/level/presentation/ui/widgets/play_button.dart index 1a121b3..8200877 100644 --- a/lib/features/level/presentation/ui/widgets/play_button.dart +++ b/lib/features/level/presentation/ui/widgets/play_button.dart @@ -15,6 +15,8 @@ class PlayButton extends StatelessWidget { @override Widget build(BuildContext context) { + final playTitle = context.translate.play; + final playTitleLength = playTitle.length; return MyInkwell( onTap: () => onTap?.call(level), child: Stack( @@ -35,9 +37,11 @@ class PlayButton extends StatelessWidget { colors: [Color(0XFFF9601F), Color(0XFFD93D16)], ).createShader(bounds), child: Text( - context.translate.play, + playTitle, maxLines: 1, style: MYTextStyle.button1.copyWith( + fontSize: playTitleLength > 5 ? 36: null, + height: playTitleLength > 5 ? 1.75: null, shadows: [ BoxShadow( color: const Color(0XFFFFFFAB).withValues(alpha: 0.40), diff --git a/lib/features/question/presentation/ui/screens/question_screen.dart b/lib/features/question/presentation/ui/screens/question_screen.dart index 738782b..2a9170d 100644 --- a/lib/features/question/presentation/ui/screens/question_screen.dart +++ b/lib/features/question/presentation/ui/screens/question_screen.dart @@ -96,17 +96,19 @@ class _QuestionScreenState extends State duration: const Duration(seconds: 1), child: FadeAnimController( controller: context.read().imageAnimationController!, - child: Column( - children: [ - _titles(context), - 20.0.gapHeight, - BlocBuilder( - builder: (context, state) => ImageBox( - key: Key('${state.currentQuestion?.image}'), - image: state.currentQuestion?.image ?? '', + child: SingleChildScrollView( + child: Column( + children: [ + _titles(context), + 20.0.gapHeight, + BlocBuilder( + builder: (context, state) => ImageBox( + key: Key('${state.currentQuestion?.image}'), + image: state.currentQuestion?.image ?? '', + ), ), - ), - ], + ], + ), ), ), ); @@ -134,7 +136,6 @@ class _QuestionScreenState extends State textAlign: TextAlign.center, minFontSize: 16, maxFontSize: 20, - maxLines: 8, style: MYTextStyle.titr1.copyWith( shadows: [ BoxShadow( @@ -148,23 +149,34 @@ class _QuestionScreenState extends State } Widget _answers(BuildContext context) { - return ListView( - controller: context.read().scrollController, - padding: const EdgeInsets.only(top: 10), - children: [ - FadeAnimController( - controller: context.read().answerAnimationController!, - child: _titles(context), - ), - 50.0.gapHeight, - SizedBox( - width: context.widthScreen, - child: BlocBuilder( - buildWhen: (previous, current) => - previous.currentQuestion?.id != current.currentQuestion?.id, - builder: (context, state) => Column( - spacing: 30, - children: [ + return AnimatedBuilder( + animation: context.read().imageAnimationController!, + builder: (context, child) => IgnorePointer( + ignoring: + context + .read() + .imageAnimationController + ?.isForwardOrCompleted ?? + false, + child: child, + ), + child: ListView( + controller: context.read().scrollController, + padding: const EdgeInsets.only(top: 10), + children: [ + FadeAnimController( + controller: context.read().answerAnimationController!, + child: _titles(context), + ), + 50.0.gapHeight, + SizedBox( + width: context.widthScreen, + child: BlocBuilder( + buildWhen: (previous, current) => + previous.currentQuestion?.id != current.currentQuestion?.id, + builder: (context, state) => Column( + spacing: 30, + children: [ Row( key: Key('${state.currentQuestion?.id}answer0'), spacing: 20, @@ -440,11 +452,12 @@ class _QuestionScreenState extends State ), ], ), - ], + ], + ), ), ), - ), - ], + ], + ), ); }