diff --git a/assets/videos/globe_state_speaking.gif b/assets/videos/globe_state_speaking.gif index edf88b0..49b3f20 100644 Binary files a/assets/videos/globe_state_speaking.gif and b/assets/videos/globe_state_speaking.gif differ diff --git a/lib/core/constants/my_api.dart b/lib/core/constants/my_api.dart index a8aebd5..8f92993 100644 --- a/lib/core/constants/my_api.dart +++ b/lib/core/constants/my_api.dart @@ -11,6 +11,8 @@ class MyApi { static const String baseUrl = 'https://hadihoda.newhorizonco.uk/api'; static const String levels = '/quiz/optimized/v3/levels/'; - static const String images = '/quiz/optimized/download-all-files/images/'; - static const String audios = '/quiz/optimized/v2/download-all-files/audio/'; + static const String images = '/quiz/batch-download/images/'; + static const String audios = '/quiz/batch-download/audio/'; + static const String batchDownload = '/quiz/batch-download/'; + static const String languages = '/languages/'; } diff --git a/lib/core/constants/my_constants.dart b/lib/core/constants/my_constants.dart index 59c91ec..41d2335 100644 --- a/lib/core/constants/my_constants.dart +++ b/lib/core/constants/my_constants.dart @@ -1,5 +1,3 @@ -import 'dart:ui'; - import 'package:hadi_hoda_flutter/features/language/domain/entity/language_entity.dart'; class MyConstants { @@ -22,6 +20,9 @@ class MyConstants { static const String effectAudioService = 'EFFECT_AUDIO_SERVICE'; static const String firstIntro = 'FIRST_INTRO'; static const String firstShowcase = 'FIRST_SHOWCASE'; + static const String maxLevelCount = 'MAX_LEVEL_COUNT'; + + static const int firstDownloadBatchCount = 3; /// Other static const double questionAudioVolume = 1.0; @@ -29,11 +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', locale: Locale('en','US')), - LanguageEntity(title: 'German (Germany)', code: 'de', locale: Locale('de','DE')), - // LanguageEntity(title: 'French (Français)', code: 'fr', locale: Locale('fr','FR')), - // LanguageEntity(title: 'Russian (Русский)', code: 'ru', locale: Locale('ru','RU')), - // LanguageEntity(title: 'Turkish (Türkçe)', code: 'tr', locale: Locale('tr','TR')), - LanguageEntity(title: 'Arabic (العربية)', code: 'ar', locale: Locale('ar','AE')), + LanguageEntity(title: 'English (English)', code: 'en'), + LanguageEntity(title: 'German (Germany)', code: 'de'), + LanguageEntity(title: 'Arabic (العربية)', code: 'ar'), ]; -} \ No newline at end of file +} diff --git a/lib/core/middlewares/my_middlewares.dart b/lib/core/middlewares/my_middlewares.dart index 0778906..5427f8f 100644 --- a/lib/core/middlewares/my_middlewares.dart +++ b/lib/core/middlewares/my_middlewares.dart @@ -12,9 +12,9 @@ class MyMiddlewares { factory MyMiddlewares() => _i; static FutureOr splash(BuildContext context, GoRouterState state) { - final String? firstDownload = LocalStorage.readData( - key: MyConstants.firstDownload); - if (firstDownload != 'true') { + final isLanguageSelected = LocalStorage.readData( + key: MyConstants.selectLanguage) != null; + if (!isLanguageSelected) { return Routes.languagePage; } else { return null; diff --git a/lib/core/network/http_request.dart b/lib/core/network/http_request.dart index 2f930ec..a615c71 100644 --- a/lib/core/network/http_request.dart +++ b/lib/core/network/http_request.dart @@ -57,6 +57,7 @@ abstract class IHttpRequest { Object? data, Map? queryParameters, Map? header, + String? method, void Function(int count, int total)? onReceive, CancelToken? cancelToken, }); diff --git a/lib/core/network/http_request_impl.dart b/lib/core/network/http_request_impl.dart index 237a3ef..bb80555 100644 --- a/lib/core/network/http_request_impl.dart +++ b/lib/core/network/http_request_impl.dart @@ -158,19 +158,21 @@ class HttpRequestImpl implements IHttpRequest { Object? data, Map? queryParameters, Map? header, + String? method, void Function(int count, int total)? onReceive, CancelToken? cancelToken, }) async { try { - await _dio.download( + final Response response = await _dio.download( urlPath, savePath, data: data, queryParameters: queryParameters, - options: Options(headers: header), + options: Options(headers: header, method: method), onReceiveProgress: onReceive, cancelToken: cancelToken, ); + return response; } on DioException catch (e) { ErrorHandler.handleError(e); } diff --git a/lib/core/routers/my_routes.dart b/lib/core/routers/my_routes.dart index 0a7b151..526e9dc 100644 --- a/lib/core/routers/my_routes.dart +++ b/lib/core/routers/my_routes.dart @@ -5,8 +5,7 @@ import 'package:hadi_hoda_flutter/core/middlewares/my_middlewares.dart'; import 'package:hadi_hoda_flutter/core/utils/my_context.dart'; import 'package:hadi_hoda_flutter/core/widgets/page_transition/my_page_transition.dart'; import 'package:hadi_hoda_flutter/core/widgets/video/my_video_player.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/domain/entities/download_entity.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'; @@ -32,7 +31,9 @@ import 'package:hadi_hoda_flutter/init_bindings.dart'; class Routes { static const Routes _i = Routes._internal(); + const Routes._internal(); + factory Routes() => _i; static const String samplePage = '/sample_page'; @@ -73,15 +74,13 @@ GoRouter _appPages() => GoRouter( GoRoute( name: Routes.downloadPage, path: Routes.downloadPage, - pageBuilder: (context, state) => myPageTransition( - key: state.pageKey, - child: BlocProvider( - create: (context) => - DownloadBloc(locator(), locator(), locator(), locator()) - ..add(GetImagesEvent()), - child: const DownloadPage(), - ), - ), + pageBuilder: (context, state) { + final config = state.extra as DownloadPageConfig; + return myPageTransition( + key: state.pageKey, + child: DownloadPage(config: config), + ); + }, ), GoRoute( name: Routes.introPage, @@ -101,7 +100,9 @@ GoRouter _appPages() => GoRouter( pageBuilder: (context, state) => myPageTransition( key: state.pageKey, child: BlocProvider( - create: (context) => LanguageBloc()..add(const InitLanguageEvent()), + create: (context) => + LanguageBloc(locator(), locator(), locator()) + ..add(const GetLanguagesEvent()), child: const LanguagePage(), ), ), @@ -115,6 +116,7 @@ GoRouter _appPages() => GoRouter( create: (context) => HomeBloc( locator(instanceName: MyConstants.mainAudioService), locator(instanceName: MyConstants.effectAudioService), + locator(), ), child: const HomePage(), ), @@ -130,6 +132,7 @@ GoRouter _appPages() => GoRouter( locator(), locator(instanceName: MyConstants.mainAudioService), locator(instanceName: MyConstants.effectAudioService), + locator() )..add(SetCurrentLevelEvent()), child: const LevelPage(), ), diff --git a/lib/core/widgets/answer_box/answer_box.dart b/lib/core/widgets/answer_box/answer_box.dart index a9b672a..7a2a583 100644 --- a/lib/core/widgets/answer_box/answer_box.dart +++ b/lib/core/widgets/answer_box/answer_box.dart @@ -35,7 +35,7 @@ class _AnswerBoxState extends State { @override Widget build(BuildContext context) { - return Hero( + return Hero( tag: 'Hero_answer_${widget.answer.id}', child: Material( type: MaterialType.transparency, diff --git a/lib/core/widgets/answer_box/styles/picture_box.dart b/lib/core/widgets/answer_box/styles/picture_box.dart index bec195b..e350b67 100644 --- a/lib/core/widgets/answer_box/styles/picture_box.dart +++ b/lib/core/widgets/answer_box/styles/picture_box.dart @@ -32,6 +32,7 @@ class AnswerPictureBox extends StatelessWidget { @override Widget build(BuildContext context) { + debugPrint('image $index : $image'); return CustomPaint( painter: _CustomShapePainter(), child: ClipPath( diff --git a/lib/features/app/presentation/bloc/app_bloc.dart b/lib/features/app/presentation/bloc/app_bloc.dart index def780c..1c5c206 100644 --- a/lib/features/app/presentation/bloc/app_bloc.dart +++ b/lib/features/app/presentation/bloc/app_bloc.dart @@ -1,4 +1,5 @@ import 'dart:async'; +import 'dart:ui'; import 'package:bloc/bloc.dart'; import 'package:hadi_hoda_flutter/core/constants/my_constants.dart'; @@ -8,28 +9,21 @@ import 'package:hadi_hoda_flutter/features/app/presentation/bloc/app_state.dart' import 'package:hadi_hoda_flutter/features/language/domain/entity/language_entity.dart'; class AppBloc extends Bloc { - /// ------------constructor------------ AppBloc() : super(const AppState()) { on(_initLocaleEvent); on(_changeLocaleEvent); } - /// ------------UseCases------------ - - /// ------------Variables------------ - - /// ------------Controllers------------ - - /// ------------Functions------------ - - - /// ------------Event Calls------------ FutureOr _initLocaleEvent(InitLocaleEvent event, Emitter emit) { final String selectLanguage = LocalStorage.readData(key: MyConstants.selectLanguage) ?? MyConstants.defaultLanguage; - final LanguageEntity findLanguage = MyConstants.languages.singleWhere( + + // 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: ''), ); + 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 1b452fb..c125f3c 100644 --- a/lib/features/download/data/datasource/download_datasource.dart +++ b/lib/features/download/data/datasource/download_datasource.dart @@ -1,6 +1,8 @@ import 'dart:async'; import 'dart:io'; +import 'package:dio/dio.dart'; +import 'package:flutter/foundation.dart'; import 'package:flutter_archive/flutter_archive.dart'; import 'package:hadi_hoda_flutter/core/constants/my_api.dart'; import 'package:hadi_hoda_flutter/core/constants/my_constants.dart'; @@ -16,132 +18,291 @@ import 'package:hadi_hoda_flutter/features/level/domain/entity/total_data_entity import 'package:hive/hive.dart'; abstract class IDownloadDatasource { - Future getImages(); - Future getAudios(); Future saveLevels(); + + Future batchDownload(int toLevel); + + Future getLastDownloadedLevel(); + Stream loadingStream(); + + void cancelDownload(); } class DownloadDatasourceImpl implements IDownloadDatasource { final IHttpRequest httpRequest; - final StreamController streamController = StreamController.broadcast(); + final StreamController streamController = + StreamController.broadcast(); + CancelToken? _audioCancelToken; + CancelToken? _imageCancelToken; + bool _isBatchDownloading = false; DownloadDatasourceImpl(this.httpRequest); @override - Future getImages() async { - final String filePath = '${StoragePath.documentDir.path}/images.zip'; + void cancelDownload() { + _isBatchDownloading = false; + _cancelAudioDownload(); + _cancelImageDownload(); + } + + void _cancelAudioDownload() { + _audioCancelToken?.cancel('Download cancelled by user.'); + _audioCancelToken = null; + } + + void _cancelImageDownload() { + _imageCancelToken?.cancel('Download cancelled by user.'); + _imageCancelToken = null; + } + + void _createAudioNewCancelToken() { + _audioCancelToken = CancelToken(); + } + + void _createImageNewCancelToken() { + _imageCancelToken = CancelToken(); + } + + int _imageCompleted = 0; + int _audioCompleted = 0; + int _targetLevel = 0; + + void _emitProgress() { + final int totalTasks = _targetLevel * 2; + final int completedTasks = _imageCompleted + _audioCompleted; + + final double percent = totalTasks == 0 + ? 0 + : (completedTasks / totalTasks) * 100; + + streamController.add( + DownloadEntity( + downloadedLevels: completedTasks ~/ 2, // optional + percent: percent, + ), + ); + } + + Future _isLevelImagesDownloaded(int level) async { + final String levelPath = '${StoragePath.documentDir.path}/$level/'; + + 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; + } + + Future _getLevelsImages(int toLevel) async { + for (int level = 1; level <= toLevel; level++) { + if (!_isBatchDownloading) return; + final bool isDownloaded = await _isLevelImagesDownloaded(level); + + if (isDownloaded) { + _imageCompleted++; + _emitProgress(); + continue; + } + + await _getLevelImage(level); + } + } - if (LocalStorage.readData(key: MyConstants.downloadedImage) != 'true') { - await httpRequest.download( + Future _getLevelImage(int level) async { + if (!_isBatchDownloading) return; + debugPrint("Started _getLevelImage($level)"); + final String filePath = '${StoragePath.documentDir.path}/images.zip'; + _createImageNewCancelToken(); + try { + final response = await httpRequest.download( urlPath: MyApi.images, savePath: filePath, + queryParameters: {'batch_start': level, 'batch_size': 1}, + cancelToken: _imageCancelToken, onReceive: (count, total) { - streamController.add(DownloadEntity( - count: count / 1, - total: total / 1, - percent: (count / total) * 100, - )); + debugPrint("Started _getLevelImage($level) : ${(count / total) * 100}"); }, - ).then((value) async { - await LocalStorage.saveData( - key: MyConstants.downloadedImage, - value: 'true', - ); - }); - } + ); - try{ - if (LocalStorage.readData(key: MyConstants.extractedImage) != 'true') { - final File file = File(filePath); - final Directory directory = Directory('${StoragePath.documentDir.path}/'); - await ZipFile.extractToDirectory( - zipFile: file, - destinationDir: directory, - onExtracting: (zipEntry, progress) { - return ZipFileOperation.includeItem; - }, - ).then((value) async { - await Future.wait([ - LocalStorage.saveData( - key: MyConstants.extractedImage, - value: 'true', - ), - file.delete(recursive: true), - ]); - }); + if (response is Response && response.statusCode == 204) { + debugPrint("No image content for level $level (204)"); + _imageCompleted++; + _emitProgress(); + return; + } + + final File file = File(filePath); + if (!await file.exists()) { + _imageCompleted++; + _emitProgress(); + return; + } + + final Directory directory = Directory( + '${StoragePath.documentDir.path}/$level/', + ); + await ZipFile.extractToDirectory( + zipFile: file, + destinationDir: directory, + onExtracting: (zipEntry, progress) { + return ZipFileOperation.includeItem; + }, + ); + if (await file.exists()) { + await file.delete(recursive: true); + } + } catch (e) { + if (e is DioException && CancelToken.isCancel(e)) { + return; } - } catch (e){ throw MyException(errorMessage: '$e'); } + _imageCompleted++; + _emitProgress(); } - @override - Future getAudios() async { - final String filePath = '${StoragePath.documentDir.path}/audios.zip'; - final String selectedLanguage = - LocalStorage.readData(key: MyConstants.selectLanguage) ?? MyConstants.defaultLanguage; + Future _isLevelAudiosDownloaded(int level) async { + final lang = + LocalStorage.readData(key: MyConstants.selectLanguage) ?? + MyConstants.defaultLanguage; + final String levelPath = '${StoragePath.documentDir.path}/$lang/$level/'; + + 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; + } + + Future _getLevelsAudios(int toLevel) async { + for (int level = 1; level <= toLevel; level++) { + if (!_isBatchDownloading) return; + final bool isDownloaded = await _isLevelAudiosDownloaded(level); - if(LocalStorage.readData(key: MyConstants.downloadedAudio) != 'true'){ - await httpRequest.download( + if (isDownloaded) { + _audioCompleted++; + _emitProgress(); + continue; + } + + await _getLevelAudio(level); + } + } + + Future _getLevelAudio(int level) async { + if (!_isBatchDownloading) return; + debugPrint("Started _getLevelAudio($level)"); + + final String filePath = '${StoragePath.documentDir.path}/audios.zip'; + _createAudioNewCancelToken(); + final lang = + LocalStorage.readData(key: MyConstants.selectLanguage) ?? + MyConstants.defaultLanguage; + try { + final response = await httpRequest.download( urlPath: MyApi.audios, savePath: filePath, queryParameters: { - 'lang': selectedLanguage, + 'batch_start': level, + 'batch_size': 1, + 'lang': lang, }, + cancelToken: _audioCancelToken, onReceive: (count, total) { - // streamController.add(DownloadEntity( - // count: count / 1, - // total: total / 1, - // percent: (count / total) * 100, - // )); + debugPrint("Started _getLevelAudio($level) : ${(count / total) * 100}"); }, - ).then((value) async { - await LocalStorage.saveData( - key: MyConstants.downloadedAudio, - value: 'true', - ); - }); - } + ); + + if (response is Response && response.statusCode == 204) { + debugPrint("No audio content for level $level (204)"); + _audioCompleted++; + _emitProgress(); + return; + } - try{ - if (LocalStorage.readData(key: MyConstants.extractedAudio) != 'true') { - final File file = File(filePath); - final Directory directory = Directory( - '${StoragePath.documentDir.path}/$selectedLanguage/', - ); - await ZipFile.extractToDirectory( - zipFile: file, - destinationDir: directory, - onExtracting: (zipEntry, progress) { - return ZipFileOperation.includeItem; - }, - ).then((value) async { - await Future.wait([ - LocalStorage.saveData( - key: MyConstants.extractedAudio, - value: 'true', - ), - file.delete(recursive: true), - ]); - }); + final File file = File(filePath); + if (!await file.exists()) { + _audioCompleted++; + _emitProgress(); + return; + } + + final Directory directory = Directory( + '${StoragePath.documentDir.path}/$lang/$level/', + ); + await ZipFile.extractToDirectory( + zipFile: file, + destinationDir: directory, + onExtracting: (zipEntry, progress) { + return ZipFileOperation.includeItem; + }, + ); + if (await file.exists()) { + await file.delete(recursive: true); + } + } catch (e) { + if (e is DioException && CancelToken.isCancel(e)) { + return; } - } catch (e){ throw MyException(errorMessage: '$e'); } + _audioCompleted++; + _emitProgress(); + } + + @override + Future getLastDownloadedLevel() async { + int lastCompleteLevel = 0; + + for (int level = 1; level <= 50; level++) { + final bool imageExists = await _isLevelImagesDownloaded(level); + + final bool audioExists = await _isLevelAudiosDownloaded(level); + + if (imageExists && audioExists) { + lastCompleteLevel = level; + } else { + break; // stop at first incomplete level + } + } + + return lastCompleteLevel; } @override Future saveLevels() async { final String selectedLanguage = - LocalStorage.readData(key: MyConstants.selectLanguage) ?? MyConstants.defaultLanguage; + LocalStorage.readData(key: MyConstants.selectLanguage) ?? + MyConstants.defaultLanguage; final Box data = Hive.box(MyConstants.levelBox); - final TotalDataEntity findData = data.values.singleWhere( - (e) => e.code == selectedLanguage, - orElse: () => TotalDataEntity(), - ); - if (findData.code != selectedLanguage) { + // Check if data already exists for this language + bool dataExists = false; + try { + data.values.firstWhere((e) => e.code == selectedLanguage); + dataExists = true; + } catch (_) { + dataExists = false; + } + + if (!dataExists) { final response = await httpRequest.get( path: MyApi.levels, queryParameters: {'lang': selectedLanguage}, @@ -150,10 +311,30 @@ class DownloadDatasourceImpl implements IDownloadDatasource { response?['path'], (json) => NodeModel.fromJson(json), ); + LocalStorage.saveData(key: MyConstants.maxLevelCount, value: '${levels.length}'); await data.add(TotalDataEntity(code: selectedLanguage, nodes: levels)); } } + @override + Future batchDownload(int toLevel) async { + debugPrint("Started batchDownload($toLevel)"); + + // Cancel any existing batch download before starting a new one + cancelDownload(); + + _isBatchDownloading = true; + _targetLevel = toLevel; + _imageCompleted = 0; + _audioCompleted = 0; + + try { + await Future.wait([_getLevelsImages(toLevel), _getLevelsAudios(toLevel)]); + } finally { + _isBatchDownloading = false; + } + } + @override Stream loadingStream() => streamController.stream; } diff --git a/lib/features/download/data/repository_impl/download_repository_impl.dart b/lib/features/download/data/repository_impl/download_repository_impl.dart index 13917f6..0bd50b0 100644 --- a/lib/features/download/data/repository_impl/download_repository_impl.dart +++ b/lib/features/download/data/repository_impl/download_repository_impl.dart @@ -11,10 +11,42 @@ class DownloadRepositoryImpl implements IDownloadRepository { const DownloadRepositoryImpl(this.datasource); + // @override + // Future> getImages() async { + // try { + // await datasource.getImages(); + // return DataState.success(NoParams()); + // } on MyException catch (e) { + // return DataState.error(e); + // } catch (e) { + // if (kDebugMode) { + // rethrow; + // } else { + // return DataState.error(MyException(errorMessage: '$e')); + // } + // } + // } + + // @override + // Future> getAudios() async { + // try { + // await datasource.getAudios(); + // return DataState.success(NoParams()); + // } on MyException catch (e) { + // return DataState.error(e); + // } catch (e) { + // if (kDebugMode) { + // rethrow; + // } else { + // return DataState.error(MyException(errorMessage: '$e')); + // } + // } + // } + @override - Future> getImages() async { + Future> saveLevels() async { try { - await datasource.getImages(); + await datasource.saveLevels(); return DataState.success(NoParams()); } on MyException catch (e) { return DataState.error(e); @@ -28,9 +60,9 @@ class DownloadRepositoryImpl implements IDownloadRepository { } @override - Future> getAudios() async { + Future> batchDownload(int toLevel) async { try { - await datasource.getAudios(); + await datasource.batchDownload(toLevel); return DataState.success(NoParams()); } on MyException catch (e) { return DataState.error(e); @@ -44,10 +76,20 @@ class DownloadRepositoryImpl implements IDownloadRepository { } @override - Future> saveLevels() async { + Stream loadingStream() { + return datasource.loadingStream(); + } + + @override + void cancelDownload() { + datasource.cancelDownload(); + } + + @override + Future> getLastDownloadLevel() async { try { - await datasource.saveLevels(); - return DataState.success(NoParams()); + final data = await datasource.getLastDownloadedLevel(); + return DataState.success(data); } on MyException catch (e) { return DataState.error(e); } catch (e) { @@ -58,9 +100,4 @@ class DownloadRepositoryImpl implements IDownloadRepository { } } } - - @override - Stream loadingStream() { - return datasource.loadingStream(); - } } diff --git a/lib/features/download/domain/entities/download_entity.dart b/lib/features/download/domain/entities/download_entity.dart index 589fd5a..1fb979c 100644 --- a/lib/features/download/domain/entities/download_entity.dart +++ b/lib/features/download/domain/entities/download_entity.dart @@ -1,7 +1,26 @@ class DownloadEntity { - final double? count; - final double? total; - final double? percent; + final int downloadedLevels; + final double percent; + + const DownloadEntity({ + required this.downloadedLevels, + required this.percent, + }); + + factory DownloadEntity.empty() { + return const DownloadEntity(downloadedLevels: 0, percent: 0); + } +} + +class DownloadPageConfig { + final int downloadToLevel; + final String redirectTo; + final Map routeParams; + + const DownloadPageConfig({ + required this.downloadToLevel, + required this.redirectTo, + this.routeParams = const {}, + }); - const DownloadEntity({this.count, this.total, this.percent}); } diff --git a/lib/features/download/domain/repository/download_repository.dart b/lib/features/download/domain/repository/download_repository.dart index ab53e49..974eb9a 100644 --- a/lib/features/download/domain/repository/download_repository.dart +++ b/lib/features/download/domain/repository/download_repository.dart @@ -4,8 +4,11 @@ import 'package:hadi_hoda_flutter/core/utils/data_state.dart'; import 'package:hadi_hoda_flutter/features/download/domain/entities/download_entity.dart'; abstract class IDownloadRepository { - Future> getImages(); - Future> getAudios(); + // Future> getImages(); + // Future> getAudios(); Future> saveLevels(); + Future> batchDownload(int toLevel); + Future> getLastDownloadLevel(); Stream loadingStream(); + void cancelDownload(); } diff --git a/lib/features/download/domain/usecases/batch_download_usecase.dart b/lib/features/download/domain/usecases/batch_download_usecase.dart new file mode 100644 index 0000000..1d771a7 --- /dev/null +++ b/lib/features/download/domain/usecases/batch_download_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/download/domain/entities/download_entity.dart'; +import 'package:hadi_hoda_flutter/features/download/domain/repository/download_repository.dart'; + +class BatchDownloadUseCase implements UseCase { + final IDownloadRepository repository; + + const BatchDownloadUseCase(this.repository); + + @override + Future> call(int toLevel) { + return repository.batchDownload(toLevel); + } +} diff --git a/lib/features/download/domain/usecases/cancel_download_usecase.dart b/lib/features/download/domain/usecases/cancel_download_usecase.dart new file mode 100644 index 0000000..e6d6120 --- /dev/null +++ b/lib/features/download/domain/usecases/cancel_download_usecase.dart @@ -0,0 +1,12 @@ +import 'package:hadi_hoda_flutter/core/params/no_params.dart'; +import 'package:hadi_hoda_flutter/features/download/domain/repository/download_repository.dart'; + +class CancelDownloadUseCase { + final IDownloadRepository repository; + + CancelDownloadUseCase(this.repository); + + void call(NoParams params) { + repository.cancelDownload(); + } +} diff --git a/lib/features/download/domain/usecases/get_audios_usecase.dart b/lib/features/download/domain/usecases/get_audios_usecase.dart index a113927..2d7d508 100644 --- a/lib/features/download/domain/usecases/get_audios_usecase.dart +++ b/lib/features/download/domain/usecases/get_audios_usecase.dart @@ -1,16 +1,16 @@ -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/download/domain/repository/download_repository.dart'; - -class GetAudiosUseCase implements UseCase { - final IDownloadRepository repository; - - const GetAudiosUseCase(this.repository); - - @override - Future> call(NoParams params) { - return repository.getAudios(); - } -} +// 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/download/domain/repository/download_repository.dart'; +// +// class GetAudiosUseCase implements UseCase { +// final IDownloadRepository repository; +// +// const GetAudiosUseCase(this.repository); +// +// @override +// Future> call(NoParams params) { +// return repository.getAudios(); +// } +// } diff --git a/lib/features/download/domain/usecases/get_images_usecase.dart b/lib/features/download/domain/usecases/get_images_usecase.dart index f91e1d6..179e2f5 100644 --- a/lib/features/download/domain/usecases/get_images_usecase.dart +++ b/lib/features/download/domain/usecases/get_images_usecase.dart @@ -1,16 +1,16 @@ -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/download/domain/repository/download_repository.dart'; - -class GetImagesUseCase implements UseCase { - final IDownloadRepository repository; - - const GetImagesUseCase(this.repository); - - @override - Future> call(NoParams params) { - return repository.getImages(); - } -} +// 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/download/domain/repository/download_repository.dart'; +// +// class GetImagesUseCase implements UseCase { +// final IDownloadRepository repository; +// +// const GetImagesUseCase(this.repository); +// +// @override +// Future> call(NoParams params) { +// return repository.getImages(); +// } +// } diff --git a/lib/features/download/domain/usecases/get_last_downloaded_level.dart b/lib/features/download/domain/usecases/get_last_downloaded_level.dart new file mode 100644 index 0000000..67739d8 --- /dev/null +++ b/lib/features/download/domain/usecases/get_last_downloaded_level.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/download/domain/entities/download_entity.dart'; +import 'package:hadi_hoda_flutter/features/download/domain/repository/download_repository.dart'; + +class GetLastDownloadedLevel implements UseCase { + final IDownloadRepository repository; + + const GetLastDownloadedLevel(this.repository); + + @override + Future> call(NoParams noParams) { + return repository.getLastDownloadLevel(); + } +} diff --git a/lib/features/download/presentation/bloc/download_bloc.dart b/lib/features/download/presentation/bloc/download_bloc.dart index af7064e..55f5852 100644 --- a/lib/features/download/presentation/bloc/download_bloc.dart +++ b/lib/features/download/presentation/bloc/download_bloc.dart @@ -1,17 +1,13 @@ import 'dart:async'; import 'package:bloc/bloc.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/core/utils/pre_cache_image.dart'; import 'package:hadi_hoda_flutter/features/download/domain/entities/download_entity.dart'; -import 'package:hadi_hoda_flutter/features/download/domain/usecases/get_audios_usecase.dart'; -import 'package:hadi_hoda_flutter/features/download/domain/usecases/get_images_usecase.dart'; +import 'package:hadi_hoda_flutter/features/download/domain/usecases/batch_download_usecase.dart'; +import 'package:hadi_hoda_flutter/features/download/domain/usecases/cancel_download_usecase.dart'; +import 'package:hadi_hoda_flutter/features/download/domain/usecases/get_last_downloaded_level.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/download/presentation/bloc/download_event.dart'; @@ -20,82 +16,225 @@ import 'package:hadi_hoda_flutter/features/download/presentation/bloc/download_s class DownloadBloc extends Bloc { /// ------------constructor------------ DownloadBloc( - this._getImagesUseCase, - this._getAudiosUseCase, - this._loadingStreamUseCase, - this._saveLevelsUseCase, - ) - : super(const DownloadState()) { + this._loadingStreamUseCase, + this._saveLevelsUseCase, + this._cancelDownloadUseCase, + this._getLastDownloadedLevel, + this._batchDownloadUseCase, + ) : super(const DownloadState()) { preCacheImages(); - on(_getImagesEvent); - on(_getAudiosEvent); + on(_startDownloadEvent); on(_saveLevelsEvent); + on(_cancelDownloadEvent); loadingStream = _loadingStreamUseCase(); } /// ------------UseCases------------ - final GetImagesUseCase _getImagesUseCase; - final GetAudiosUseCase _getAudiosUseCase; final SaveLevelsUseCase _saveLevelsUseCase; + final BatchDownloadUseCase _batchDownloadUseCase; final LoadingStreamUseCase _loadingStreamUseCase; + final CancelDownloadUseCase _cancelDownloadUseCase; + final GetLastDownloadedLevel _getLastDownloadedLevel; - /// ------------Variables------------ Stream loadingStream = const Stream.empty(); + bool _isDownloading = false; + bool _isCancelled = false; - /// ------------Controllers------------ - - /// ------------Functions------------ - - /// ------------Api Calls------------ - FutureOr _getImagesEvent( - GetImagesEvent event, - Emitter emit, - ) async { - emit(state.copyWith(getFilesStatus: const BaseInit())); - await _getImagesUseCase(NoParams()).then((value) { - value.fold( - (data) { - add(GetAudiosEvent()); - }, - (error) async { - emit(state.copyWith(getFilesStatus: BaseError(error.errorMessage))); - }, - ); - }); - } + bool get isDownloading => _isDownloading; - FutureOr _getAudiosEvent( - GetAudiosEvent event, + FutureOr _startDownloadEvent( + StartDownloadEvent event, Emitter emit, ) async { - await _getAudiosUseCase(NoParams()).then((value) { - value.fold( - (data) async { - add(SaveLevelsEvent()); - }, - (error) async { - emit(state.copyWith(getFilesStatus: BaseError(error.errorMessage))); - }, - ); - }); + _isCancelled = false; + _isDownloading = true; + emit(state.copyWith(status: const BaseLoading())); + + final levelResult = await _saveLevelsUseCase(NoParams()); + + if (levelResult.isError) { + _isDownloading = false; + emit(state.copyWith(status: BaseError(levelResult.error!.errorMessage))); + return; + } + + final downloadResult = await _batchDownloadUseCase(event.toLevel); + + downloadResult.fold( + (_) { + _isDownloading = false; + emit(state.copyWith(status: const BaseComplete(''))); + }, + (e) { + _isDownloading = false; + emit(state.copyWith(status: BaseError(e.errorMessage))); + }, + ); + + // if (event.types.contains(DownloadType.levels)) { + // await LocalStorage.saveData( + // key: MyConstants.firstDownload, + // value: 'true', + // ); + // } + + // if (MyContext.get.mounted && event.destinationRoute != null) { + // MyContext.get.goNamed(event.destinationRoute!); + // } } + Future lastDownloadedLevel() async => + (await _getLastDownloadedLevel(NoParams())).data ?? 0; + + /// Orchestrates batch download (level-by-level). + /// If a download is already in progress, just updates the destination + /// so the running download navigates there when it finishes. + // FutureOr _startBatchDownloadEvent( + // StartBatchDownloadEvent event, + // Emitter emit, + // ) async { + // _isCancelled = false; + // final String lang = event.batchParams.lang; + // final int alreadyDownloaded = int.tryParse( + // LocalStorage.readData(key: '${MyConstants.downloadedLevelCount}_$lang') ?? '0', + // ) ?? 0; + // final int neededUpTo = + // event.batchParams.batchStart + event.batchParams.batchSize - 1; + // + // // Already downloaded — skip straight to destination. + // if (alreadyDownloaded >= neededUpTo) { + // emit(state.copyWith(getFilesStatus: const BaseComplete(''))); + // if (MyContext.get.mounted && event.destinationRoute != null) { + // MyContext.get.goNamed( + // event.destinationRoute!, + // pathParameters: event.destinationPathParameters ?? {}, + // ); + // } + // return; + // } + // + // // A download is already running — just update the destination so the + // // ongoing download navigates there when it finishes. + // if (_isBatchDownloading) { + // emit(state.copyWith( + // destinationRoute: event.destinationRoute, + // destinationPathParameters: event.destinationPathParameters, + // )); + // return; + // } + // + // _isBatchDownloading = true; + // + // emit(state.copyWith( + // getFilesStatus: const BaseLoading(), + // batchParams: event.batchParams, + // destinationRoute: event.destinationRoute, + // destinationPathParameters: event.destinationPathParameters, + // )); + // + // final levelsResult = await _saveLevelsUseCase(NoParams()); + // if (levelsResult.isError) { + // if (_isCancelled) return; + // _isBatchDownloading = false; + // emit(state.copyWith( + // getFilesStatus: BaseError(levelsResult.error!.errorMessage), + // )); + // return; + // } + // + // if (_isCancelled) return; + // final result = await _batchDownloadUseCase(event.batchParams); + // if (result.isError) { + // if (_isCancelled) return; + // _isBatchDownloading = false; + // emit(state.copyWith( + // getFilesStatus: BaseError(result.error!.errorMessage), + // )); + // return; + // } + // + // if (_isCancelled) return; + // _isBatchDownloading = false; + // + // emit(state.copyWith(getFilesStatus: const BaseComplete(''))); + // + // await LocalStorage.saveData( + // key: '${MyConstants.downloadedLevelCount}_$lang', + // value: '$neededUpTo', + // ); + // + // await LocalStorage.saveData( + // key: MyConstants.firstDownload, + // value: 'true', + // ); + // + // // Read destination from state (may have been updated by a later event). + // if (MyContext.get.mounted && state.destinationRoute != null) { + // MyContext.get.goNamed( + // state.destinationRoute!, + // pathParameters: state.destinationPathParameters ?? {}, + // ); + // } + // } + + /// Downloads images only (standalone, no chaining). + // FutureOr _getImagesEvent( + // GetImagesEvent event, + // Emitter emit, + // ) async { + // _isCancelled = false; + // emit(state.copyWith(getFilesStatus: const BaseLoading())); + // final result = await _getImagesUseCase(NoParams()); + // if (result.isError) { + // if (_isCancelled) return; + // emit(state.copyWith(getFilesStatus: BaseError(result.error!.errorMessage))); + // } else { + // if (_isCancelled) return; + // emit(state.copyWith(getFilesStatus: const BaseComplete(''))); + // } + // } + + /// Downloads audios only (standalone, no chaining). + // FutureOr _getAudiosEvent( + // GetAudiosEvent event, + // Emitter emit, + // ) async { + // _isCancelled = false; + // emit(state.copyWith(getFilesStatus: const BaseLoading())); + // final result = await _getAudiosUseCase(NoParams()); + // if (result.isError) { + // if (_isCancelled) return; + // emit(state.copyWith(getFilesStatus: BaseError(result.error!.errorMessage))); + // } else { + // if (_isCancelled) return; + // emit(state.copyWith(getFilesStatus: const BaseComplete(''))); + // } + // } + + /// Saves levels only (standalone, no chaining). FutureOr _saveLevelsEvent( - SaveLevelsEvent event, - Emitter emit, - ) async { - await _saveLevelsUseCase(NoParams()).then((value) => - value.fold( - (data) async { - await LocalStorage.saveData(key: MyConstants.firstDownload, value: 'true'); - if(MyContext.get.mounted){ - MyContext.get.goNamed(Routes.introPage); - } - }, - (error) { - emit(state.copyWith(getFilesStatus: BaseError(error.errorMessage))); - }, - ), - ); + SaveLevelsEvent event, + Emitter emit, + ) async { + _isCancelled = false; + emit(state.copyWith(status: const BaseLoading())); + final result = await _saveLevelsUseCase(NoParams()); + if (result.isError) { + if (_isCancelled) return; + emit(state.copyWith(status: BaseError(result.error!.errorMessage))); + } else { + if (_isCancelled) return; + emit(state.copyWith(status: const BaseComplete(''))); + } + } + + FutureOr _cancelDownloadEvent( + CancelDownloadEvent event, + Emitter emit, + ) { + _isCancelled = true; + _isDownloading = false; + _cancelDownloadUseCase(NoParams()); + emit(state.copyWith(status: const BaseInit())); } } diff --git a/lib/features/download/presentation/bloc/download_event.dart b/lib/features/download/presentation/bloc/download_event.dart index ec62661..712b507 100644 --- a/lib/features/download/presentation/bloc/download_event.dart +++ b/lib/features/download/presentation/bloc/download_event.dart @@ -1,6 +1,28 @@ +import 'package:hadi_hoda_flutter/features/download/domain/entities/download_entity.dart'; + sealed class DownloadEvent { const DownloadEvent(); } -class GetImagesEvent extends DownloadEvent {} -class GetAudiosEvent extends DownloadEvent {} + +class StartDownloadEvent extends DownloadEvent { + final int toLevel; + + const StartDownloadEvent({required this.toLevel}); +} +// +// class StartBatchDownloadEvent extends DownloadEvent { +// final BatchDownloadParams batchParams; +// final String? destinationRoute; +// final Map? destinationPathParameters; +// +// const StartBatchDownloadEvent({ +// required this.batchParams, +// this.destinationRoute, +// this.destinationPathParameters, +// }); +// } + +// class GetImagesEvent extends DownloadEvent {} +// class GetAudiosEvent extends DownloadEvent {} class SaveLevelsEvent extends DownloadEvent {} +class CancelDownloadEvent extends DownloadEvent {} diff --git a/lib/features/download/presentation/bloc/download_state.dart b/lib/features/download/presentation/bloc/download_state.dart index 6ed7abc..7df6d39 100644 --- a/lib/features/download/presentation/bloc/download_state.dart +++ b/lib/features/download/presentation/bloc/download_state.dart @@ -1,15 +1,18 @@ import 'package:hadi_hoda_flutter/core/status/base_status.dart'; +import 'package:hadi_hoda_flutter/features/download/domain/entities/download_entity.dart'; class DownloadState { - final BaseStatus getFilesStatus; + final BaseStatus status; - const DownloadState({this.getFilesStatus = const BaseInit()}); + const DownloadState({ + this.status = const BaseInit(), + }); DownloadState copyWith({ - BaseStatus? getFilesStatus, + BaseStatus? status, }) { return DownloadState( - getFilesStatus: getFilesStatus ?? this.getFilesStatus, + status: status ?? this.status, ); } } diff --git a/lib/features/download/presentation/ui/download_page.dart b/lib/features/download/presentation/ui/download_page.dart index eee353d..9ac0b4d 100644 --- a/lib/features/download/presentation/ui/download_page.dart +++ b/lib/features/download/presentation/ui/download_page.dart @@ -1,8 +1,10 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:go_router/go_router.dart'; import 'package:hadi_hoda_flutter/common_ui/resources/my_assets.dart'; import 'package:hadi_hoda_flutter/common_ui/resources/my_spaces.dart'; import 'package:hadi_hoda_flutter/common_ui/resources/my_text_style.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/convert_size.dart'; import 'package:hadi_hoda_flutter/core/utils/my_localization.dart'; @@ -15,8 +17,34 @@ import 'package:hadi_hoda_flutter/features/download/presentation/bloc/download_e import 'package:hadi_hoda_flutter/features/download/presentation/bloc/download_state.dart'; import 'package:hadi_hoda_flutter/features/download/presentation/ui/widgets/download_loading_widget.dart'; -class DownloadPage extends StatelessWidget { - const DownloadPage({super.key}); +/// Download page that supports downloading any combination of media types. +/// +/// Pass [config] to auto-start downloading specific types when the page opens. +/// If [config] is null, the page assumes the download was already triggered +/// externally and just shows the progress. +class DownloadPage extends StatefulWidget { + final DownloadPageConfig config; + + const DownloadPage({super.key,required this.config}); + + @override + State createState() => _DownloadPageState(); +} + +class _DownloadPageState extends State { + @override + void initState() { + super.initState(); + final bloc = context.read(); + + if (!bloc.isDownloading) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) { + bloc.add(StartDownloadEvent(toLevel: widget.config.downloadToLevel)); + } + }); + } + } @override Widget build(BuildContext context) { @@ -28,10 +56,7 @@ class DownloadPage extends StatelessWidget { gradient: const LinearGradient( begin: Alignment.topCenter, end: Alignment.bottomCenter, - colors: [ - Color(0XFF00154C), - Color(0XFF150532), - ], + colors: [Color(0XFF00154C), Color(0XFF150532)], ), image: DecorationImage( image: const AssetImage(MyAssets.pattern), @@ -43,31 +68,45 @@ class DownloadPage extends StatelessWidget { ), ), ), - child: BlocBuilder( - buildWhen: (previous, current) => - previous.getFilesStatus != current.getFilesStatus, - builder: (context, state) { - if (state.getFilesStatus is BaseError) { - return Padding( - padding: EdgeInsets.symmetric( - vertical: MediaQuery.viewPaddingOf(context).bottom + MySpaces.s16, - horizontal: 60, - ), - child: ErrorState( - onTap: () => context.read().add(GetImagesEvent()), - ), - ); + child: BlocConsumer( + listener: (context, state) { + if(state.status is BaseComplete) { + if(widget.config.redirectTo == Routes.homePage) { + context.goNamed(Routes.homePage); } else { - return Stack( - alignment: Alignment.center, - children: [ - _image(), - _text(context), - _loading(context), - ], + context.pushNamed( + widget.config.redirectTo, + pathParameters: widget.config.routeParams, ); } } + }, + buildWhen: (previous, current) => previous.status != current.status, + builder: (context, state) { + if (state.status is BaseError) { + return Padding( + padding: EdgeInsets.symmetric( + vertical: + MediaQuery.viewPaddingOf(context).bottom + MySpaces.s16, + horizontal: 60, + ), + child: ErrorState( + onTap: () { + context.read().add( + StartDownloadEvent( + toLevel: widget.config.downloadToLevel, + ), + ); + }, + ), + ); + } else { + return Stack( + alignment: Alignment.center, + children: [_image(), _text(context), _loading(context)], + ); + } + }, ), ), ); @@ -76,15 +115,11 @@ class DownloadPage extends StatelessWidget { Widget _image() { return const Stack( children: [ - MyImage( - image: MyAssets.hadiHoda, - ), + MyImage(image: MyAssets.hadiHoda), PositionedDirectional( start: MySpaces.s10, top: MySpaces.s40, - child: MyImage( - image: MyAssets.globe, - ), + child: MyImage(image: MyAssets.globe), ), ], ); @@ -96,16 +131,13 @@ class DownloadPage extends StatelessWidget { child: Column( spacing: MySpaces.s6, children: [ - Text( - context.translate.please_wait, - style: MYTextStyle.titr0, - ), + Text(context.translate.please_wait, style: MYTextStyle.titr0), StreamBuilder( - initialData: const DownloadEntity(), + initialData: DownloadEntity.empty(), stream: context.read().loadingStream, - builder: (context, snapshot) => Text( - '${context.translate.downloading_data} (${snapshot.data?.count.toMB ?? 0.0}mb / ${snapshot.data?.total.toMB ?? 0.0}mb)', - style: MYTextStyle.matn3, + builder: (context, snapshot) => Text( + 'Downloading ...${snapshot.data?.downloadedLevels}/${widget.config.downloadToLevel}', + style: MYTextStyle.matn3, ), ), ], diff --git a/lib/features/download/presentation/ui/widgets/download_loading_widget.dart b/lib/features/download/presentation/ui/widgets/download_loading_widget.dart index c9cd0be..fdaf8df 100644 --- a/lib/features/download/presentation/ui/widgets/download_loading_widget.dart +++ b/lib/features/download/presentation/ui/widgets/download_loading_widget.dart @@ -34,7 +34,7 @@ class DownloadLoadingWidget extends StatelessWidget { ), ), child: StreamBuilder( - initialData: const DownloadEntity(), + initialData: DownloadEntity.empty(), stream: loadingStream, builder: (context, snapshot) { return Row( @@ -72,7 +72,7 @@ class DownloadLoadingWidget extends StatelessWidget { flex: 15, child: Center( child: Text( - '${snapshot.data?.percent?.toInt() ?? 0}%', + '${snapshot.data?.percent.toInt() ?? 0}%', style: MYTextStyle.titr4.copyWith( color: const Color(0XFF6E83A8), ), diff --git a/lib/features/guider/data/datasource/guider_datasource.dart b/lib/features/guider/data/datasource/guider_datasource.dart index 6816cf5..3a07243 100644 --- a/lib/features/guider/data/datasource/guider_datasource.dart +++ b/lib/features/guider/data/datasource/guider_datasource.dart @@ -1,3 +1,4 @@ +import 'package:flutter/foundation.dart'; import 'package:hadi_hoda_flutter/core/constants/my_constants.dart'; import 'package:hadi_hoda_flutter/core/error_handler/my_exception.dart'; import 'package:hadi_hoda_flutter/core/utils/local_storage.dart'; @@ -20,13 +21,15 @@ class GuiderDatasourceImpl implements IGuiderDatasource { LocalStorage.readData(key: MyConstants.selectLanguage) ?? MyConstants.defaultLanguage; final Box levelBox = Hive.box(MyConstants.levelBox); - final TotalDataEntity findData = levelBox.values.singleWhere( + final TotalDataEntity findData = levelBox.values.firstWhere( (e) => e.code == selectedLanguage, orElse: () => TotalDataEntity(), ); final LevelEntity? findLevel = findData.nodes?.first.level; return findLevel ?? LevelEntity(); - } catch (e) { + } catch (e,s) { + debugPrint(e.toString()); + debugPrint(s.toString()); throw MyException(errorMessage: '$e'); } } diff --git a/lib/features/home/presentation/bloc/home_bloc.dart b/lib/features/home/presentation/bloc/home_bloc.dart index 05cd7ff..2132279 100644 --- a/lib/features/home/presentation/bloc/home_bloc.dart +++ b/lib/features/home/presentation/bloc/home_bloc.dart @@ -13,14 +13,16 @@ 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/core/utils/storage_path.dart'; import 'package:hadi_hoda_flutter/core/widgets/dialog/about_us_dialog.dart'; +import 'package:hadi_hoda_flutter/features/download/domain/entities/download_entity.dart'; +import 'package:hadi_hoda_flutter/features/download/domain/usecases/get_last_downloaded_level.dart'; import 'package:hadi_hoda_flutter/features/home/presentation/bloc/home_event.dart'; import 'package:hadi_hoda_flutter/features/home/presentation/bloc/home_state.dart'; -import 'package:hadi_hoda_flutter/features/level/domain/entity/total_data_entity.dart'; -import 'package:hive/hive.dart'; + +import '../../../../core/params/no_params.dart'; class HomeBloc extends Bloc { /// ------------constructor------------ - HomeBloc(this._mainAudioService, this._effectAudioService) + HomeBloc(this._mainAudioService, this._effectAudioService, this._getLastDownloadedLevel) : super(const HomeState()) { volumeStream = _mainAudioService.volumeStream(); playMusic(); @@ -30,7 +32,7 @@ class HomeBloc extends Bloc { } /// ------------UseCases------------ - + final GetLastDownloadedLevel _getLastDownloadedLevel; /// ------------Variables------------ late final Stream volumeStream; @@ -39,22 +41,31 @@ class HomeBloc extends Bloc { final AudioService _effectAudioService; /// ------------Functions------------ - void goToLevelPage(BuildContext context) { - final String? selectedLanguage = LocalStorage.readData( - key: MyConstants.selectLanguage, - ); - final Box dataBox = Hive.box(MyConstants.levelBox); - final TotalDataEntity findData = dataBox.values.singleWhere( - (e) => e.code == selectedLanguage, - orElse: () => TotalDataEntity(), - ); - if (findData.nodes?.isNotEmpty ?? false) { + void goToLevelPage(BuildContext context) async { + final lastDownloadedLevel = + (await _getLastDownloadedLevel(NoParams())).data ?? 0; + final hasFirstDownload = MyConstants.firstDownloadBatchCount <= lastDownloadedLevel; + + if(!context.mounted) return; + + if (hasFirstDownload) { context.goNamed(Routes.levelPage); } else { - context.goNamed(Routes.downloadPage); + context.goNamed( + Routes.downloadPage, + extra: const DownloadPageConfig( + downloadToLevel: MyConstants.firstDownloadBatchCount, + redirectTo: Routes.homePage + ), + ); } } + Future getLastDownloadedLevel() async { + final result = await _getLastDownloadedLevel(NoParams()); + return result.data ?? 0; + } + void goToLanguagePage(BuildContext context) { context.pushNamed(Routes.languagePage); } diff --git a/lib/features/home/presentation/ui/home_page.dart b/lib/features/home/presentation/ui/home_page.dart index 4d6fe87..3c5b8b6 100644 --- a/lib/features/home/presentation/ui/home_page.dart +++ b/lib/features/home/presentation/ui/home_page.dart @@ -1,7 +1,11 @@ import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:go_router/go_router.dart'; import 'package:hadi_hoda_flutter/common_ui/resources/my_assets.dart'; import 'package:hadi_hoda_flutter/common_ui/resources/my_spaces.dart'; +import 'package:hadi_hoda_flutter/core/constants/my_constants.dart'; +import 'package:hadi_hoda_flutter/core/routers/my_routes.dart'; +import 'package:hadi_hoda_flutter/core/utils/local_storage.dart'; import 'package:hadi_hoda_flutter/core/utils/my_localization.dart'; import 'package:hadi_hoda_flutter/core/utils/screen_size.dart'; import 'package:hadi_hoda_flutter/core/utils/set_platform_size.dart'; @@ -11,11 +15,40 @@ import 'package:hadi_hoda_flutter/core/widgets/button/my_yellow_button.dart'; import 'package:hadi_hoda_flutter/core/widgets/images/my_image.dart'; import 'package:hadi_hoda_flutter/core/widgets/inkwell/my_inkwell.dart'; import 'package:hadi_hoda_flutter/core/widgets/pop_scope/my_pop_scope.dart'; +import 'package:hadi_hoda_flutter/features/download/domain/entities/download_entity.dart'; import 'package:hadi_hoda_flutter/features/home/presentation/bloc/home_bloc.dart'; -class HomePage extends StatelessWidget { +class HomePage extends StatefulWidget { const HomePage({super.key}); + @override + State createState() => _HomePageState(); +} + +class _HomePageState extends State { + @override + void initState() { + super.initState(); + WidgetsBinding.instance.addPostFrameCallback((_) { + _checkFirstBatch(); + }); + } + + void _checkFirstBatch() async { + if (!mounted) return; + final bloc = context.read(); + final lastDownloadLevel = await bloc.getLastDownloadedLevel(); + if (lastDownloadLevel < MyConstants.firstDownloadBatchCount && mounted) { + context.goNamed( + Routes.downloadPage, + extra: const DownloadPageConfig( + downloadToLevel: MyConstants.firstDownloadBatchCount, + redirectTo: Routes.homePage, + ), + ); + } + } + @override Widget build(BuildContext context) { return Scaffold( @@ -63,21 +96,16 @@ class HomePage extends StatelessWidget { ); } - Positioned _image(BuildContext context) { return Positioned( top: setSize(context: context, mobile: 0.1.h, tablet: 0.15.h), child: const Stack( children: [ - MyImage( - image: MyAssets.hadiHoda, - ), + MyImage(image: MyAssets.hadiHoda), PositionedDirectional( start: MySpaces.s10, top: MySpaces.s40, - child: MyImage( - image: MyAssets.globe, - ), + child: MyImage(image: MyAssets.globe), ), ], ), diff --git a/lib/features/language/data/datasource/language_datasource.dart b/lib/features/language/data/datasource/language_datasource.dart new file mode 100644 index 0000000..21a60fd --- /dev/null +++ b/lib/features/language/data/datasource/language_datasource.dart @@ -0,0 +1,19 @@ +import 'package:hadi_hoda_flutter/core/constants/my_api.dart'; +import 'package:hadi_hoda_flutter/core/network/http_request.dart'; +import 'package:hadi_hoda_flutter/features/language/data/model/language_model.dart'; + +abstract class ILanguageDatasource { + Future> getLanguages(); +} + +class LanguageDatasourceImpl implements ILanguageDatasource { + final IHttpRequest httpRequest; + + LanguageDatasourceImpl(this.httpRequest); + + @override + Future> getLanguages() async { + final response = await httpRequest.get(path: MyApi.languages); + return (response['results'] as List).map((e) => LanguageModel.fromJson(e)).toList(); + } +} diff --git a/lib/features/language/data/model/language_model.dart b/lib/features/language/data/model/language_model.dart new file mode 100644 index 0000000..3383b39 --- /dev/null +++ b/lib/features/language/data/model/language_model.dart @@ -0,0 +1,12 @@ +import 'package:hadi_hoda_flutter/features/language/domain/entity/language_entity.dart'; + +class LanguageModel extends LanguageEntity { + const LanguageModel({required super.code, required super.title}); + + factory LanguageModel.fromJson(Map json) { + return LanguageModel( + code: json['code'] ?? '', + title: json['name'] ?? '', // API returns 'name', we map to 'title' + ); + } +} diff --git a/lib/features/language/data/repository_impl/language_repository_impl.dart b/lib/features/language/data/repository_impl/language_repository_impl.dart new file mode 100644 index 0000000..d188e21 --- /dev/null +++ b/lib/features/language/data/repository_impl/language_repository_impl.dart @@ -0,0 +1,52 @@ +import 'dart:io'; + +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/data_state.dart'; +import 'package:hadi_hoda_flutter/core/utils/local_storage.dart'; +import 'package:hadi_hoda_flutter/features/language/data/datasource/language_datasource.dart'; +import 'package:hadi_hoda_flutter/features/language/domain/entity/language_entity.dart'; +import 'package:hadi_hoda_flutter/features/language/domain/repository/language_repository.dart'; + +class LanguageRepositoryImpl implements ILanguageRepository { + final ILanguageDatasource languageDatasource; + + LanguageRepositoryImpl(this.languageDatasource); + + @override + Future, MyException>> getLanguages() async { + try { + final languages = await languageDatasource.getLanguages(); + // Explicitly convert to a List to prevent runtime type errors. + return DataState.success(List.from(languages)); + } on MyException catch (e) { + return DataState.error(e); + } + } + + @override + LanguageEntity getSelectedLanguage(List languages) { + final savedLanguageCode = LocalStorage.readData(key: MyConstants.selectLanguage); + + if (savedLanguageCode != null) { + return languages.firstWhere((lang) => lang.code == savedLanguageCode, + orElse: () => _getFallbackLanguage(languages)); + } + + final deviceLanguageCode = Platform.localeName.split('_').first; + return languages.firstWhere((lang) => lang.code == deviceLanguageCode, + orElse: () => _getFallbackLanguage(languages)); + } + + @override + Future saveSelectedLanguage(String code) async { + await LocalStorage.saveData(key: MyConstants.selectLanguage, value: code); + } + + LanguageEntity _getFallbackLanguage(List languages) { + return languages.firstWhere( + (lang) => lang.code == MyConstants.defaultLanguage, + orElse: () => languages.first, + ); + } +} diff --git a/lib/features/language/domain/entity/language_entity.dart b/lib/features/language/domain/entity/language_entity.dart index a60b57e..b5068a1 100644 --- a/lib/features/language/domain/entity/language_entity.dart +++ b/lib/features/language/domain/entity/language_entity.dart @@ -1,21 +1,20 @@ +import 'dart:ui'; import 'package:equatable/equatable.dart'; -import 'package:flutter/material.dart'; class LanguageEntity extends Equatable { - final String? title; - final String? code; - final Locale? locale; + final String code; + final String title; - const LanguageEntity({ - this.title, - this.code, - this.locale, - }); + const LanguageEntity({required this.code, required this.title}); + + Locale get locale { + final parts = code.split('_'); + if (parts.length > 1) { + return Locale(parts[0], parts[1]); + } + return Locale(parts[0]); + } @override - List get props => [ - title, - code, - locale, - ]; + List get props => [code, title]; } diff --git a/lib/features/language/domain/repository/language_repository.dart b/lib/features/language/domain/repository/language_repository.dart new file mode 100644 index 0000000..a0eb3f6 --- /dev/null +++ b/lib/features/language/domain/repository/language_repository.dart @@ -0,0 +1,9 @@ +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/language/domain/entity/language_entity.dart'; + +abstract class ILanguageRepository { + Future, MyException>> getLanguages(); + LanguageEntity getSelectedLanguage(List languages); + Future saveSelectedLanguage(String code); +} diff --git a/lib/features/language/domain/usecases/get_languages_usecase.dart b/lib/features/language/domain/usecases/get_languages_usecase.dart new file mode 100644 index 0000000..aa4530c --- /dev/null +++ b/lib/features/language/domain/usecases/get_languages_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/language/domain/entity/language_entity.dart'; +import 'package:hadi_hoda_flutter/features/language/domain/repository/language_repository.dart'; + +class GetLanguagesUseCase implements UseCase, NoParams> { + final ILanguageRepository repository; + + GetLanguagesUseCase(this.repository); + + @override + Future, MyException>> call(NoParams params) { + return repository.getLanguages(); + } +} diff --git a/lib/features/language/domain/usecases/get_selected_language_usecase.dart b/lib/features/language/domain/usecases/get_selected_language_usecase.dart new file mode 100644 index 0000000..2f1bf7a --- /dev/null +++ b/lib/features/language/domain/usecases/get_selected_language_usecase.dart @@ -0,0 +1,16 @@ +import 'package:hadi_hoda_flutter/core/error_handler/my_exception.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/language/domain/entity/language_entity.dart'; +import 'package:hadi_hoda_flutter/features/language/domain/repository/language_repository.dart'; + +class GetSelectedLanguageUseCase implements UseCase> { + final ILanguageRepository repository; + + GetSelectedLanguageUseCase(this.repository); + + @override + Future> call(List params) async { + return DataState.success(repository.getSelectedLanguage(params)); + } +} diff --git a/lib/features/language/domain/usecases/save_selected_language_usecase.dart b/lib/features/language/domain/usecases/save_selected_language_usecase.dart new file mode 100644 index 0000000..2dbbff6 --- /dev/null +++ b/lib/features/language/domain/usecases/save_selected_language_usecase.dart @@ -0,0 +1,16 @@ +import 'package:hadi_hoda_flutter/core/error_handler/my_exception.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/language/domain/repository/language_repository.dart'; + +class SaveSelectedLanguageUseCase implements UseCase { + final ILanguageRepository repository; + + SaveSelectedLanguageUseCase(this.repository); + + @override + Future> call(String params) async { + await repository.saveSelectedLanguage(params); + return DataState.success(null); + } +} diff --git a/lib/features/language/presentation/bloc/language_bloc.dart b/lib/features/language/presentation/bloc/language_bloc.dart index bf9e56a..82411d0 100644 --- a/lib/features/language/presentation/bloc/language_bloc.dart +++ b/lib/features/language/presentation/bloc/language_bloc.dart @@ -1,82 +1,61 @@ import 'dart:async'; -import 'dart:io'; 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/routers/my_routes.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/core/utils/storage_path.dart'; -import 'package:hadi_hoda_flutter/features/app/presentation/bloc/app_bloc.dart'; -import 'package:hadi_hoda_flutter/features/app/presentation/bloc/app_event.dart'; -import 'package:hadi_hoda_flutter/features/language/domain/entity/language_entity.dart'; +import 'package:hadi_hoda_flutter/core/params/no_params.dart'; +import 'package:hadi_hoda_flutter/core/status/base_status.dart'; +import 'package:hadi_hoda_flutter/features/language/domain/usecases/get_languages_usecase.dart'; +import 'package:hadi_hoda_flutter/features/language/domain/usecases/get_selected_language_usecase.dart'; +import 'package:hadi_hoda_flutter/features/language/domain/usecases/save_selected_language_usecase.dart'; import 'package:hadi_hoda_flutter/features/language/presentation/bloc/language_event.dart'; import 'package:hadi_hoda_flutter/features/language/presentation/bloc/language_state.dart'; -import 'package:hadi_hoda_flutter/init_bindings.dart'; class LanguageBloc extends Bloc { - /// ------------constructor------------ - LanguageBloc() : super(const LanguageState()) { - on(_changeLanguageEvent); - on(_saveLevelsEvent); - on(_initLanguageEvent); + final GetLanguagesUseCase _getLanguagesUseCase; + final GetSelectedLanguageUseCase _getSelectedLanguageUseCase; + final SaveSelectedLanguageUseCase _saveSelectedLanguageUseCase; + + LanguageBloc( + this._getLanguagesUseCase, + this._getSelectedLanguageUseCase, + this._saveSelectedLanguageUseCase, + ) : super(const LanguageState()) { + on(_onGetLanguagesEvent); + on(_onSelectLanguageEvent); } - /// ------------UseCases------------ - - /// ------------Variables------------ - - /// ------------Controllers------------ - - /// ------------Functions------------ - FutureOr _changeLanguageEvent( - ChangeLanguageEvent event, - Emitter emit, - ) { - emit(state.copyWith(selectedLang: event.lang)); - } - - /// ------------Api Calls------------ - FutureOr _saveLevelsEvent( - SaveLevelsEvent event, + FutureOr _onGetLanguagesEvent( + GetLanguagesEvent event, Emitter emit, ) async { - await Future.wait([ - LocalStorage.saveData( - key: MyConstants.selectLanguage, - value: state.selectedLang.code ?? MyConstants.defaultLanguage, - ), - ]); - final AppBloc appBloc = locator(); - appBloc.add( - ChangeLocaleEvent(state.selectedLang.locale ?? const Locale('en', 'US'))); - - if (Directory( - '${StoragePath.documentDir.path}/${state.selectedLang - .code}/answer_audio') - .existsSync() && Directory( - '${StoragePath.documentDir.path}/${state.selectedLang - .code}/question_audio') - .existsSync()) { - if (MyContext.get.mounted) { - MyContext.get.goNamed(Routes.homePage); - } + emit(state.copyWith(getLanguagesStatus: const BaseLoading())); + + final result = await _getLanguagesUseCase(NoParams()); + + if (result.isSuccess) { + final languages = result.data!; + final selectedResult = await _getSelectedLanguageUseCase(languages); + + emit( + state.copyWith( + getLanguagesStatus: const BaseComplete(''), + languages: languages, + selectedLanguage: selectedResult.data, + ), + ); } else { - await Future.wait([ - LocalStorage.deleteData(key: MyConstants.downloadedAudio), - LocalStorage.deleteData(key: MyConstants.extractedAudio), - ]); - if (MyContext.get.mounted) { - MyContext.get.goNamed(Routes.downloadPage); - } + emit( + state.copyWith( + getLanguagesStatus: BaseError(result.error!.errorMessage), + ), + ); } } - FutureOr _initLanguageEvent(InitLanguageEvent event, Emitter emit) { - final String selectedLanguage = LocalStorage.readData( - key: MyConstants.selectLanguage) ?? MyConstants.defaultLanguage; - emit(state.copyWith(selectedLang: LanguageEntity(code: selectedLanguage))); + FutureOr _onSelectLanguageEvent( + SelectLanguageEvent event, + Emitter emit, + ) async { + await _saveSelectedLanguageUseCase(event.language.code); + emit(state.copyWith(selectedLanguage: event.language)); } } diff --git a/lib/features/language/presentation/bloc/language_event.dart b/lib/features/language/presentation/bloc/language_event.dart index accf591..ae55c22 100644 --- a/lib/features/language/presentation/bloc/language_event.dart +++ b/lib/features/language/presentation/bloc/language_event.dart @@ -3,15 +3,13 @@ import 'package:hadi_hoda_flutter/features/language/domain/entity/language_entit sealed class LanguageEvent { const LanguageEvent(); } -class ChangeLanguageEvent extends LanguageEvent { - final LanguageEntity lang; - const ChangeLanguageEvent(this.lang); -} -class SaveLevelsEvent extends LanguageEvent { - const SaveLevelsEvent(); -} -class InitLanguageEvent extends LanguageEvent { - const InitLanguageEvent(); +class GetLanguagesEvent extends LanguageEvent { + const GetLanguagesEvent(); } +class SelectLanguageEvent extends LanguageEvent { + final LanguageEntity language; + + const SelectLanguageEvent(this.language); +} diff --git a/lib/features/language/presentation/bloc/language_state.dart b/lib/features/language/presentation/bloc/language_state.dart index 994f9ce..e008d67 100644 --- a/lib/features/language/presentation/bloc/language_state.dart +++ b/lib/features/language/presentation/bloc/language_state.dart @@ -1,23 +1,26 @@ -import 'package:hadi_hoda_flutter/core/constants/my_constants.dart'; import 'package:hadi_hoda_flutter/core/status/base_status.dart'; import 'package:hadi_hoda_flutter/features/language/domain/entity/language_entity.dart'; class LanguageState { - final BaseStatus saveLevelsStatus; - final LanguageEntity selectedLang; + final BaseStatus getLanguagesStatus; + final List languages; + final LanguageEntity? selectedLanguage; const LanguageState({ - this.saveLevelsStatus = const BaseInit(), - this.selectedLang = const LanguageEntity(code: MyConstants.defaultLanguage), + this.getLanguagesStatus = const BaseInit(), + this.languages = const [], + this.selectedLanguage, }); LanguageState copyWith({ - BaseStatus? saveLevelsStatus, - LanguageEntity? selectedLang, + BaseStatus? getLanguagesStatus, + List? languages, + LanguageEntity? selectedLanguage, }) { return LanguageState( - saveLevelsStatus: saveLevelsStatus ?? this.saveLevelsStatus, - selectedLang: selectedLang ?? this.selectedLang, + getLanguagesStatus: getLanguagesStatus ?? this.getLanguagesStatus, + languages: languages ?? this.languages, + selectedLanguage: selectedLanguage ?? this.selectedLanguage, ); } } diff --git a/lib/features/language/presentation/ui/language_page.dart b/lib/features/language/presentation/ui/language_page.dart index 189bc48..768e99f 100644 --- a/lib/features/language/presentation/ui/language_page.dart +++ b/lib/features/language/presentation/ui/language_page.dart @@ -1,24 +1,52 @@ import 'package:auto_size_text/auto_size_text.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:go_router/go_router.dart'; import 'package:hadi_hoda_flutter/common_ui/resources/my_assets.dart'; import 'package:hadi_hoda_flutter/common_ui/resources/my_colors.dart'; import 'package:hadi_hoda_flutter/common_ui/resources/my_spaces.dart'; import 'package:hadi_hoda_flutter/common_ui/resources/my_text_style.dart'; import 'package:hadi_hoda_flutter/core/constants/my_constants.dart'; +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/my_localization.dart'; import 'package:hadi_hoda_flutter/core/utils/screen_size.dart'; import 'package:hadi_hoda_flutter/core/utils/set_platform_size.dart'; import 'package:hadi_hoda_flutter/core/widgets/button/my_blue_button.dart'; import 'package:hadi_hoda_flutter/core/widgets/images/my_image.dart'; +import 'package:hadi_hoda_flutter/features/app/presentation/bloc/app_bloc.dart'; +import 'package:hadi_hoda_flutter/features/app/presentation/bloc/app_event.dart'; +import 'package:hadi_hoda_flutter/features/download/domain/entities/download_entity.dart'; +import 'package:hadi_hoda_flutter/features/download/presentation/bloc/download_bloc.dart'; +import 'package:hadi_hoda_flutter/features/download/presentation/bloc/download_event.dart'; import 'package:hadi_hoda_flutter/features/language/presentation/bloc/language_bloc.dart'; import 'package:hadi_hoda_flutter/features/language/presentation/bloc/language_event.dart'; import 'package:hadi_hoda_flutter/features/language/presentation/bloc/language_state.dart'; -import 'package:hadi_hoda_flutter/features/language/presentation/ui/widgets/language_widget.dart'; +import 'package:wheel_chooser/wheel_chooser.dart'; -class LanguagePage extends StatelessWidget { +class LanguagePage extends StatefulWidget { const LanguagePage({super.key}); + @override + State createState() => _LanguagePageState(); +} + +class _LanguagePageState extends State { + final controller = FixedExtentScrollController(initialItem: 50); + bool _isEnabled = false; + + @override + void initState() { + super.initState(); + context.read().add(const GetLanguagesEvent()); + } + + @override + void dispose() { + controller.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { return Scaffold( @@ -41,16 +69,117 @@ class LanguagePage extends StatelessWidget { ), ), ), - child: Padding( - padding: EdgeInsets.only( - left: setSize(context: context, mobile: 50, tablet: 0.3.w) ?? 0, - right: setSize(context: context, mobile: 50, tablet: 0.3.w) ?? 0, - bottom: MySpaces.s40, - top: 100, - ), - child: Column( - children: [_title(context), _list(context), _btn(context)], - ), + child: BlocConsumer( + listener: (context, state) async { + if (state.languages.isNotEmpty && !_isEnabled) { + int targetIndex = 0; + if (state.selectedLanguage != null) { + targetIndex = state.languages.indexWhere((l) => l.code == state.selectedLanguage!.code); + if (targetIndex == -1) targetIndex = 0; + } + + await Future.delayed(const Duration(milliseconds: 50)); + if (controller.hasClients) { + await controller.animateToItem( + targetIndex, + duration: const Duration(seconds: 1), + curve: Curves.easeOutCirc, + ); + + if (state.selectedLanguage == null && context.mounted) { + context.read().add( + SelectLanguageEvent(state.languages[targetIndex]), + ); + } + } + setState(() => _isEnabled = true); + } + }, + builder: (context, state) { + if (state.getLanguagesStatus is BaseLoading) { + return const Center(child: CircularProgressIndicator()); + } + + final double itemSize = setSize(context: context, mobile: 45, tablet: 60) ?? 45; + + return Padding( + padding: EdgeInsets.only( + left: setSize(context: context, mobile: 50, tablet: 0.3.w) ?? 0, + right: setSize(context: context, mobile: 50, tablet: 0.3.w) ?? 0, + bottom: MySpaces.s40 + MediaQuery.paddingOf(context).bottom, + top: 100, + ), + child: Column( + children: [ + _title(context), + const SizedBox(height: 72), + Expanded( + child: ShaderMask( + shaderCallback: (rect) { + return const LinearGradient( + begin: Alignment.topCenter, + end: Alignment.bottomCenter, + colors: [ + Color(0x00FFF7FA), + Color(0x1AFFF7FA), + Color(0xFFFFFFFF), + Color(0x1AFEFCFD), + Color(0x00FEFCFD), + ], + ).createShader(rect); + }, + child: Stack( + children: [ + Center( + child: Container( + width: double.infinity, + height: itemSize + 4, + decoration: BoxDecoration( + color: Colors.black.withOpacity(0.5), + borderRadius: BorderRadius.circular(16), + ), + ), + ), + WheelChooser.choices( + controller: controller, + startPosition: null, + perspective: 0.00000001, + listWidth: double.infinity, + listHeight: double.infinity, + itemSize: itemSize, + isInfinite: true, + onChoiceChanged: (value) { + if (value != null && value is! String) { + context.read().add( + SelectLanguageEvent(value), + ); + } + }, + selectTextStyle: MYTextStyle.titr1.copyWith( + fontSize: itemSize * 0.4, + color: Colors.white, + ), + unSelectTextStyle: MYTextStyle.titr1.copyWith( + fontSize: itemSize * 0.4, + color: Colors.white.withOpacity(0.5), + ), + choices: state.languages.map((lang) { + return WheelChoice( + value: lang, + title: lang.title, + ); + }).toList(), + ), + ], + ), + ), + ), + const SizedBox(height: 72), + _btn(context, state), + ], + ), + ); + }, ), ), ); @@ -76,39 +205,43 @@ class LanguagePage extends StatelessWidget { ); } - Expanded _list(BuildContext context) { - return Expanded( - child: Material( - color: MyColors.transparent, - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: List.generate( - MyConstants.languages.length, - (index) => - BlocBuilder( - buildWhen: (previous, current) => - previous.selectedLang.code != current.selectedLang.code, - builder: (context, state) { - final LanguageBloc languageBloc = context.read(); - return LanguageWidget( - selected: state.selectedLang.code == - MyConstants.languages[index].code, - onTap: () { - languageBloc.add(ChangeLanguageEvent(MyConstants.languages[index])); - }, - title: MyConstants.languages[index].title, - ); - } - ), - ), - ), - ), - ); - } - - Widget _btn(BuildContext context) { + Widget _btn(BuildContext context, LanguageState state) { return MyBlueButton( - onTap: () => context.read().add(const SaveLevelsEvent()), + onTap: (_isEnabled && state.selectedLanguage != null) + ? () async { + final downloadBloc = context.read(); + // 1. Cancel any previous downloads. + downloadBloc.add(CancelDownloadEvent()); + + // 2. Update App Locale + context.read().add( + ChangeLocaleEvent(state.selectedLanguage!.locale), + ); + await Future.delayed(const Duration(milliseconds: 80)); + final lastDownloadedLevel = await downloadBloc + .lastDownloadedLevel(); + + if (!context.mounted) return; + + if (lastDownloadedLevel >= MyConstants.firstDownloadBatchCount) { + context.goNamed(Routes.homePage); + } else { + context.read().add( + const StartDownloadEvent( + toLevel: MyConstants.firstDownloadBatchCount, + ), + ); + + context.goNamed( + Routes.downloadPage, + extra: const DownloadPageConfig( + downloadToLevel: MyConstants.firstDownloadBatchCount, + redirectTo: Routes.homePage, + ), + ); + } + } + : null, title: context.translate.select, ); } diff --git a/lib/features/level/data/datasource/level_datasource.dart b/lib/features/level/data/datasource/level_datasource.dart index e102bf5..03f66f8 100644 --- a/lib/features/level/data/datasource/level_datasource.dart +++ b/lib/features/level/data/datasource/level_datasource.dart @@ -1,3 +1,4 @@ +import 'package:flutter/foundation.dart'; import 'package:hadi_hoda_flutter/core/constants/my_constants.dart'; import 'package:hadi_hoda_flutter/core/error_handler/my_exception.dart'; import 'package:hadi_hoda_flutter/core/params/level_params.dart'; @@ -20,12 +21,15 @@ class LocalLevelDatasourceImpl implements ILevelDatasource { final String selectedLanguage = LocalStorage.readData( key: MyConstants.selectLanguage) ?? MyConstants.defaultLanguage; final Box levelBox = Hive.box(MyConstants.levelBox); - final TotalDataEntity findData = levelBox.values.singleWhere( + final TotalDataEntity findData = levelBox.values.firstWhere( (e) => e.code == selectedLanguage, orElse: () => TotalDataEntity(), ); + debugPrint("nodesCount : ${findData.nodes?.first.level?.questions}"); return findData.nodes ?? []; - } catch (_) { + } catch (e,s) { + debugPrint(e.toString()); + debugPrint(s.toString()); throw const MyException(errorMessage: 'Operation Failed'); } } diff --git a/lib/features/level/data/model/level_model.dart b/lib/features/level/data/model/level_model.dart index 2400039..378d6f0 100644 --- a/lib/features/level/data/model/level_model.dart +++ b/lib/features/level/data/model/level_model.dart @@ -16,7 +16,7 @@ class LevelModel extends LevelEntity { order: json['order'], title: json['title'], questions: json['questions'] - ?.map((e) => QuestionModel.fromJson(e)) + ?.map((e) => QuestionModel.fromJson(e, json['order'])) .toList(), ); } diff --git a/lib/features/level/presentation/bloc/level_bloc.dart b/lib/features/level/presentation/bloc/level_bloc.dart index 11e9ac7..54017d9 100644 --- a/lib/features/level/presentation/bloc/level_bloc.dart +++ b/lib/features/level/presentation/bloc/level_bloc.dart @@ -2,10 +2,12 @@ import 'dart:async'; import 'package:bloc/bloc.dart'; import 'package:flutter/cupertino.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; import 'package:hadi_hoda_flutter/common_ui/resources/my_audios.dart'; import 'package:hadi_hoda_flutter/core/constants/my_constants.dart'; import 'package:hadi_hoda_flutter/core/params/level_params.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/services/audio_service.dart'; import 'package:hadi_hoda_flutter/core/status/base_status.dart'; @@ -14,6 +16,10 @@ import 'package:hadi_hoda_flutter/core/utils/my_context.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/dialog/reward_dialog.dart'; +import 'package:hadi_hoda_flutter/features/download/domain/entities/download_entity.dart'; +import 'package:hadi_hoda_flutter/features/download/domain/usecases/get_last_downloaded_level.dart'; +import 'package:hadi_hoda_flutter/features/download/presentation/bloc/download_bloc.dart'; +import 'package:hadi_hoda_flutter/features/download/presentation/bloc/download_state.dart'; import 'package:hadi_hoda_flutter/features/level/domain/entity/level_entity.dart'; import 'package:hadi_hoda_flutter/features/level/domain/entity/level_location.dart'; import 'package:hadi_hoda_flutter/features/level/domain/entity/node_entity.dart'; @@ -26,10 +32,11 @@ import 'package:hadi_hoda_flutter/features/level/presentation/ui/widgets/node_wi class LevelBloc extends Bloc { /// ------------constructor------------ LevelBloc( - this._getLeveslUseCase, - this._mainAudioService, - this._effectAudioService, - ) : super(const LevelState()) { + this._getLeveslUseCase, + this._mainAudioService, + this._effectAudioService, + this._getLastDownloadedLevel, + ) : super(const LevelState()) { volumeStream = _mainAudioService.volumeStream(); playMusic(); on(_getLevelListEvent); @@ -46,6 +53,7 @@ class LevelBloc extends Bloc { /// ------------UseCases------------ final GetLevelsUseCase _getLeveslUseCase; + final GetLastDownloadedLevel _getLastDownloadedLevel; /// ------------Variables------------ final List locationList = [ @@ -176,13 +184,10 @@ class LevelBloc extends Bloc { ), ]; - final List nodeList = []; late final Stream volumeStream; - - /// ------------Controllers------------ final ScrollController scrollController = ScrollController(); final AudioService _mainAudioService; @@ -197,13 +202,37 @@ class LevelBloc extends Bloc { await _mainAudioService.play(); } - void goToQuestionPage(BuildContext context, LevelEntity level){ - context.pushReplacementNamed( - Routes.questionPage, - pathParameters: { - 'id': '${level.id}', - }, - ); + void goToQuestionPage(BuildContext context, LevelEntity level) async { + final int levelOrder = level.order ?? 0; + + final int lastDownloadedLevel = + (await _getLastDownloadedLevel(NoParams())).data ?? 0; + + if (!context.mounted) return; + + final bool isLevelDownloaded = levelOrder <= lastDownloadedLevel; + + if (isLevelDownloaded) { + context.pushReplacementNamed( + Routes.questionPage, + pathParameters: {'id': '${level.id}'}, + ); + } else { + final maxLevelCount = + int.tryParse( + LocalStorage.readData(key: MyConstants.maxLevelCount) ?? '20', + ) ?? + 20; + + context.pushNamed( + Routes.downloadPage, + extra: DownloadPageConfig( + downloadToLevel: maxLevelCount, + routeParams: {'id': '${level.id}'}, + redirectTo: Routes.questionPage, + ), + ); + } } void goToHomePage(BuildContext context) { @@ -246,39 +275,40 @@ class LevelBloc extends Bloc { return currentLevel - 1; } - - void showReward({ - required BuildContext context, - required PrizeEntity prize, - }) { + void showReward({required BuildContext context, required PrizeEntity prize}) { showRewardDialog(context: context, prize: prize); } /// ------------Event Calls------------ - FutureOr _getLevelListEvent(GetLevelListEvent event, - Emitter emit) async { + FutureOr _getLevelListEvent( + GetLevelListEvent event, + Emitter emit, + ) async { final int currentLevel = int.parse( LocalStorage.readData(key: MyConstants.currentLevel) ?? '1', ); await _getLeveslUseCase(LevelParams()).then((value) { - value.fold( - (data) async { - nodeList.addAll(data); - try { - emit(state.copyWith( - getLevelStatus: const BaseComplete(''), - chooseLevel: data.singleWhere((e) => e.level?.order == currentLevel).level, - )); - } catch (e) { - emit(state.copyWith( - getLevelStatus: const BaseComplete(''), - chooseLevel: LevelEntity(), - )); - } - add(StartScrollEvent()); - }, - (error) {}, - ); + value.fold((data) async { + nodeList.addAll(data); + try { + emit( + state.copyWith( + getLevelStatus: const BaseComplete(''), + chooseLevel: data + .firstWhere((e) => e.level?.order == currentLevel) + .level, + ), + ); + } catch (e) { + emit( + state.copyWith( + getLevelStatus: const BaseComplete(''), + chooseLevel: LevelEntity(), + ), + ); + } + add(StartScrollEvent()); + }, (error) {}); }); } @@ -292,37 +322,43 @@ class LevelBloc extends Bloc { await Future.delayed(const Duration(seconds: 1)); if (scrollController.hasClients) { - if(currentLevel >= 14){ - scrollController.animateTo( - scrollController.position.maxScrollExtent, - duration: const Duration(milliseconds: 500), // Note: 500 seconds is very long. - curve: Curves.easeInOut, - ); - } else if( currentLevel > 8 && currentLevel < 14){ - scrollController.animateTo( - scrollController.position.maxScrollExtent / 2, - duration: const Duration(milliseconds: 500), // Note: 500 seconds is very long. - curve: Curves.easeInOut, - ); - } + if (currentLevel >= 14) { + scrollController.animateTo( + scrollController.position.maxScrollExtent, + duration: const Duration(milliseconds: 500), + // Note: 500 seconds is very long. + curve: Curves.easeInOut, + ); + } else if (currentLevel > 8 && currentLevel < 14) { + scrollController.animateTo( + scrollController.position.maxScrollExtent / 2, + duration: const Duration(milliseconds: 500), + // Note: 500 seconds is very long. + curve: Curves.easeInOut, + ); + } } } - FutureOr _setCurrentLevelEvent(SetCurrentLevelEvent event, - Emitter emit) async { + FutureOr _setCurrentLevelEvent( + SetCurrentLevelEvent event, + Emitter emit, + ) async { final String? currentLevel = LocalStorage.readData( - key: MyConstants.currentLevel); + key: MyConstants.currentLevel, + ); if (currentLevel == null || currentLevel.isEmpty) { await LocalStorage.saveData(key: MyConstants.currentLevel, value: '1'); } add(GetLevelListEvent()); } - FutureOr _chooseLevelEvent(ChooseLevelEvent event, - Emitter emit,) { + FutureOr _chooseLevelEvent( + ChooseLevelEvent event, + Emitter emit, + ) { if (event.type != LevelType.unFinished) { emit(state.copyWith(chooseLevel: event.level)); } } - } diff --git a/lib/features/level/presentation/ui/level_page.dart b/lib/features/level/presentation/ui/level_page.dart index 3dfcdc0..462e697 100644 --- a/lib/features/level/presentation/ui/level_page.dart +++ b/lib/features/level/presentation/ui/level_page.dart @@ -3,6 +3,9 @@ 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_spaces.dart'; +import 'package:hadi_hoda_flutter/core/constants/my_constants.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/screen_size.dart'; import 'package:hadi_hoda_flutter/core/utils/set_platform_size.dart'; import 'package:hadi_hoda_flutter/core/widgets/animations/rotation_planet.dart'; @@ -10,6 +13,11 @@ import 'package:hadi_hoda_flutter/core/widgets/animations/ship_anim.dart'; import 'package:hadi_hoda_flutter/core/widgets/images/my_image.dart'; import 'package:hadi_hoda_flutter/core/widgets/inkwell/my_inkwell.dart'; import 'package:hadi_hoda_flutter/core/widgets/pop_scope/my_pop_scope.dart'; +import 'package:hadi_hoda_flutter/features/download/domain/entities/download_entity.dart'; +import 'package:hadi_hoda_flutter/features/download/presentation/bloc/download_bloc.dart'; +import 'package:hadi_hoda_flutter/features/download/presentation/bloc/download_event.dart'; +import 'package:hadi_hoda_flutter/features/download/presentation/bloc/download_state.dart'; +import 'package:hadi_hoda_flutter/features/download/presentation/ui/widgets/download_loading_widget.dart'; import 'package:hadi_hoda_flutter/features/level/domain/entity/level_entity.dart'; import 'package:hadi_hoda_flutter/features/level/presentation/bloc/level_bloc.dart'; import 'package:hadi_hoda_flutter/features/level/presentation/bloc/level_event.dart'; @@ -19,10 +27,27 @@ import 'package:hadi_hoda_flutter/features/level/presentation/ui/widgets/level_p import 'package:hadi_hoda_flutter/features/level/presentation/ui/widgets/node_widget.dart'; import 'package:hadi_hoda_flutter/features/level/presentation/ui/widgets/play_button.dart'; -class LevelPage extends StatelessWidget { +class LevelPage extends StatefulWidget { const LevelPage({super.key}); + @override + State createState() => _LevelPageState(); +} +class _LevelPageState extends State { + @override + void initState() { + super.initState(); + _triggerRemainingLevelsDownload(); + } + + void _triggerRemainingLevelsDownload() { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (!mounted) return; + final maxLevelCount = int.tryParse(LocalStorage.readData(key: MyConstants.maxLevelCount) ?? '20') ?? 20; + context.read().add(StartDownloadEvent(toLevel: maxLevelCount)); + }); + } @override Widget build(BuildContext context) { @@ -44,7 +69,7 @@ class LevelPage extends StatelessWidget { ], ), ), - _topButtons(context), + _topSection(context), Positioned( bottom: 0, child: Stack( @@ -263,42 +288,64 @@ class LevelPage extends StatelessWidget { ); } - Positioned _topButtons(BuildContext context) { + Positioned _topSection(BuildContext context) { return Positioned( left: MySpaces.s16, right: MySpaces.s16, top: setPlatform(android: MySpaces.s20, iOS: 50), - child: Row( - spacing: MySpaces.s16, + child: Column( children: [ - MyInkwell( - onTap: () => context.read().goToHomePage(context), - audio: MyAudios.back, - child: MyImage( - image: MyAssets.homeButton, - size: setSize(context: context, tablet: 80), - ), - ), - const Spacer(), - DiamondLevel( - diamonds: context.read().diamonds, - ), - StreamBuilder( - initialData: 1, - stream: context.read().volumeStream, - builder: (context, snapshot) => MyInkwell( - onTap: () => context.read().changeMute(), - child: MyImage( - image: snapshot.data == 0 ? MyAssets.musicOff : MyAssets.musicOn, - size: setSize(context: context, tablet: 80), + Row( + spacing: MySpaces.s16, + children: [ + MyInkwell( + onTap: () => context.read().goToHomePage(context), + audio: MyAudios.back, + child: MyImage( + image: MyAssets.homeButton, + size: setSize(context: context, tablet: 80), + ), ), - ), + const Spacer(), + DiamondLevel( + diamonds: context.read().diamonds, + ), + StreamBuilder( + initialData: 1, + stream: context.read().volumeStream, + builder: (context, snapshot) => MyInkwell( + onTap: () => context.read().changeMute(), + child: MyImage( + image: snapshot.data == 0 ? MyAssets.musicOff : MyAssets.musicOn, + size: setSize(context: context, tablet: 80), + ), + ), + ), + ], ), + // _downloadIndicator(), ], ), ); } + 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( image: MyAssets.mapBackground, diff --git a/lib/features/question/data/datasource/question_datasource.dart b/lib/features/question/data/datasource/question_datasource.dart index ed5d2dc..e904454 100644 --- a/lib/features/question/data/datasource/question_datasource.dart +++ b/lib/features/question/data/datasource/question_datasource.dart @@ -22,11 +22,11 @@ class QuestionDatasourceImpl implements IQuestionDatasource { final String selectedLanguage = LocalStorage.readData( key: MyConstants.selectLanguage) ?? MyConstants.defaultLanguage; final Box levelBox = Hive.box(MyConstants.levelBox); - final TotalDataEntity findData = levelBox.values.singleWhere( + final TotalDataEntity findData = levelBox.values.firstWhere( (e) => e.code == selectedLanguage, orElse: () => TotalDataEntity(), ); - final NodeEntity? findLevel = findData.nodes?.singleWhere( + final NodeEntity? findLevel = findData.nodes?.firstWhere( (e) => e.level?.id == params.id, orElse: () => NodeEntity(), ); @@ -44,7 +44,7 @@ class QuestionDatasourceImpl implements IQuestionDatasource { final int index = int.parse( LocalStorage.readData(key: MyConstants.currentLevel) ?? '1'); final Box levelBox = Hive.box(MyConstants.levelBox); - final TotalDataEntity findData = levelBox.values.singleWhere( + final TotalDataEntity findData = levelBox.values.firstWhere( (e) => e.code == selectedLanguage, orElse: () => TotalDataEntity(), ); @@ -54,7 +54,7 @@ class QuestionDatasourceImpl implements IQuestionDatasource { if(index > (levelList?.length ?? 0)){ throw const MyException(); } - final NodeEntity? findLevel = findData.nodes?.singleWhere( + final NodeEntity? findLevel = findData.nodes?.firstWhere( (e) => e.level?.order == index, orElse: () => NodeEntity(), ); diff --git a/lib/features/question/data/model/answer_model.dart b/lib/features/question/data/model/answer_model.dart index 3cab02a..44d5331 100644 --- a/lib/features/question/data/model/answer_model.dart +++ b/lib/features/question/data/model/answer_model.dart @@ -11,12 +11,14 @@ class AnswerModel extends AnswerEntity { super.isActive, super.audioID, super.audioInfo, + super.levelOrder, }); - factory AnswerModel.fromJson(Map json) { + factory AnswerModel.fromJson(Map json, int levelOrder) { return AnswerModel( id: json['id'], title: json['title'], + levelOrder: json['level_order'] ?? levelOrder, imageId: json['image_id'], imageInfo: json['image_info'] == null ? null diff --git a/lib/features/question/data/model/question_model.dart b/lib/features/question/data/model/question_model.dart index aa13de8..3717c4a 100644 --- a/lib/features/question/data/model/question_model.dart +++ b/lib/features/question/data/model/question_model.dart @@ -21,9 +21,10 @@ class QuestionModel extends QuestionEntity { super.hadiths, super.imageId, super.imageInfo, + super.levelOrder, }); - factory QuestionModel.fromJson(Map json) { + factory QuestionModel.fromJson(Map json, int levelOrder) { return QuestionModel( id: json['id'], title: json['title'], @@ -35,7 +36,7 @@ class QuestionModel extends QuestionEntity { correctAnswer: json['correct_answer'], isActive: json['is_active'], answers: json['answers'] - ?.map((e) => AnswerModel.fromJson(e)) + ?.map((e) => AnswerModel.fromJson(e,levelOrder)) .toList(), correctAnswerAudioId: json['correct_answer_audio_id'], correctAnswerText: json['correct_answer_text'], @@ -49,6 +50,7 @@ class QuestionModel extends QuestionEntity { imageInfo: json['image_info'] == null ? null : FileModel.fromJson(json['image_info']), + levelOrder: levelOrder ); } } diff --git a/lib/features/question/domain/entity/answer_entity.dart b/lib/features/question/domain/entity/answer_entity.dart index b9bb26b..7ed5e0e 100644 --- a/lib/features/question/domain/entity/answer_entity.dart +++ b/lib/features/question/domain/entity/answer_entity.dart @@ -28,6 +28,8 @@ class AnswerEntity extends HiveObject { FileEntity? audioInfo; @HiveField(9) String? audio; + @HiveField(10) + int? levelOrder; AnswerEntity({ this.id, @@ -38,8 +40,9 @@ class AnswerEntity extends HiveObject { this.isActive, this.audioID, this.audioInfo, + this.levelOrder, }){ - image = '${StoragePath.documentDir.path}/answer_image/${imageInfo?.filename}'; - audio = '${StoragePath.documentDir.path}/${LocalStorage.readData(key: MyConstants.selectLanguage)}/answer_audio/${audioInfo?.filename}'; + image = '${StoragePath.documentDir.path}/$levelOrder/answer_image/${imageInfo?.filename}'; + audio = '${StoragePath.documentDir.path}/${LocalStorage.readData(key: MyConstants.selectLanguage)}/$levelOrder/answer_audio/${audioInfo?.filename}'; } } diff --git a/lib/features/question/domain/entity/question_entity.dart b/lib/features/question/domain/entity/question_entity.dart index 98bb07a..dab05ec 100644 --- a/lib/features/question/domain/entity/question_entity.dart +++ b/lib/features/question/domain/entity/question_entity.dart @@ -44,6 +44,8 @@ class QuestionEntity extends HiveObject { FileEntity? imageInfo; @HiveField(16) String? image; + @HiveField(17) + int? levelOrder; QuestionEntity({ this.id, @@ -60,9 +62,10 @@ class QuestionEntity extends HiveObject { this.correctAnswerAudioInfo, this.imageId, this.imageInfo, + this.levelOrder, }){ - audio = '${StoragePath.documentDir.path}/${LocalStorage.readData(key: MyConstants.selectLanguage)}/question_audio/${audioInfo?.filename}'; - correctAudio = '${StoragePath.documentDir.path}/${LocalStorage.readData(key: MyConstants.selectLanguage)}/correct_answer_audio/${correctAnswerAudioInfo?.filename}'; - image = '${StoragePath.documentDir.path}/question_image/${imageInfo?.filename}'; + audio = '${StoragePath.documentDir.path}/${LocalStorage.readData(key: MyConstants.selectLanguage)}/$levelOrder/question_audio/${audioInfo?.filename}'; + correctAudio = '${StoragePath.documentDir.path}/${LocalStorage.readData(key: MyConstants.selectLanguage)}/$levelOrder/correct_answer_audio/${correctAnswerAudioInfo?.filename}'; + image = '${StoragePath.documentDir.path}/$levelOrder/question_image/${imageInfo?.filename}'; } } diff --git a/lib/features/question/presentation/bloc/question_bloc.dart b/lib/features/question/presentation/bloc/question_bloc.dart index 33098df..07fb5b7 100644 --- a/lib/features/question/presentation/bloc/question_bloc.dart +++ b/lib/features/question/presentation/bloc/question_bloc.dart @@ -1,5 +1,6 @@ import 'dart:async'; +import 'package:collection/collection.dart'; import 'package:flutter/material.dart'; import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:go_router/go_router.dart'; @@ -291,7 +292,7 @@ class QuestionBloc extends Bloc { await showAnswerDialog( context: MyContext.get, correctAudio: state.currentQuestion?.correctAudio, - answerEntity: state.currentQuestion?.answers?.singleWhere((e) => + answerEntity: state.currentQuestion?.answers?.firstWhereOrNull((e) => e.order == event.correctAnswer) ?? AnswerEntity(), showConfetti: true, ); diff --git a/lib/features/question/presentation/ui/question_page.dart b/lib/features/question/presentation/ui/question_page.dart index 20b7379..0ec7332 100644 --- a/lib/features/question/presentation/ui/question_page.dart +++ b/lib/features/question/presentation/ui/question_page.dart @@ -60,6 +60,9 @@ class QuestionPage extends StatelessWidget { (previous.currentQuestion?.order != current.currentQuestion?.order), builder: (context, state) { + debugPrint("current question : ${state.currentQuestion?.order?.toString()}"); + debugPrint("questions length : ${state.levelEntity?.questions?.length}"); + debugPrint(state.currentQuestion?.order?.toString()); if (state.currentQuestion?.order == state.levelEntity?.questions?.length) { return const DiamondScreen(); diff --git a/lib/features/splash/presentation/ui/splash_page.dart b/lib/features/splash/presentation/ui/splash_page.dart index 79ea4a3..2eba72a 100644 --- a/lib/features/splash/presentation/ui/splash_page.dart +++ b/lib/features/splash/presentation/ui/splash_page.dart @@ -53,7 +53,7 @@ class _SplashPageState extends State { child: Stack( alignment: Alignment.center, children: [ - _image(), + // _image(), _loading(context), ], ), diff --git a/lib/init_bindings.dart b/lib/init_bindings.dart index cf4e6e5..a8ce47c 100644 --- a/lib/init_bindings.dart +++ b/lib/init_bindings.dart @@ -9,14 +9,25 @@ import 'package:hadi_hoda_flutter/features/app/presentation/bloc/app_bloc.dart'; import 'package:hadi_hoda_flutter/features/download/data/datasource/download_datasource.dart'; import 'package:hadi_hoda_flutter/features/download/data/repository_impl/download_repository_impl.dart'; import 'package:hadi_hoda_flutter/features/download/domain/repository/download_repository.dart'; +import 'package:hadi_hoda_flutter/features/download/domain/usecases/batch_download_usecase.dart'; +import 'package:hadi_hoda_flutter/features/download/domain/usecases/cancel_download_usecase.dart'; import 'package:hadi_hoda_flutter/features/download/domain/usecases/get_audios_usecase.dart'; import 'package:hadi_hoda_flutter/features/download/domain/usecases/get_images_usecase.dart'; +import 'package:hadi_hoda_flutter/features/download/domain/usecases/get_last_downloaded_level.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/download/presentation/bloc/download_bloc.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/language/data/datasource/language_datasource.dart'; +import 'package:hadi_hoda_flutter/features/language/data/repository_impl/language_repository_impl.dart'; +import 'package:hadi_hoda_flutter/features/language/domain/repository/language_repository.dart'; +import 'package:hadi_hoda_flutter/features/language/domain/usecases/get_languages_usecase.dart'; +import 'package:hadi_hoda_flutter/features/language/domain/usecases/get_selected_language_usecase.dart'; +import 'package:hadi_hoda_flutter/features/language/domain/usecases/save_selected_language_usecase.dart'; +import 'package:hadi_hoda_flutter/features/language/presentation/bloc/language_bloc.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'; @@ -56,6 +67,11 @@ void initBindings() { ); locator.registerSingleton(AppBloc()); + /// Blocs + locator.registerFactory(() => LanguageBloc(locator(), locator(), locator())); + locator.registerFactory( + () => DownloadBloc(locator(), locator(), locator(), locator(), locator())); + /// Sample Feature locator.registerLazySingleton(() => SampleDatasourceImpl(locator())); locator.registerLazySingleton(() => SampleRepositoryImpl(locator())); @@ -64,10 +80,11 @@ void initBindings() { /// Download Feature locator.registerLazySingleton(() => DownloadDatasourceImpl(locator())); locator.registerLazySingleton(() => DownloadRepositoryImpl(locator())); - locator.registerLazySingleton(() => GetImagesUseCase(locator())); - locator.registerLazySingleton(() => GetAudiosUseCase(locator())); locator.registerLazySingleton(() => SaveLevelsUseCase(locator())); + locator.registerLazySingleton(() => BatchDownloadUseCase(locator())); locator.registerLazySingleton(() => LoadingStreamUseCase(locator())); + locator.registerLazySingleton(() => CancelDownloadUseCase(locator())); + locator.registerLazySingleton(() => GetLastDownloadedLevel(locator())); /// Guider Feature locator.registerLazySingleton(() => const GuiderDatasourceImpl()); @@ -84,6 +101,13 @@ void initBindings() { locator.registerLazySingleton(() => const LocalLevelDatasourceImpl()); locator.registerLazySingleton(() => LevelRepositoryImpl(locator())); locator.registerLazySingleton(() => GetLevelsUseCase(locator())); + + /// Language Feature + locator.registerLazySingleton(() => LanguageDatasourceImpl(locator())); + locator.registerLazySingleton(() => LanguageRepositoryImpl(locator())); + locator.registerLazySingleton(() => GetLanguagesUseCase(locator())); + locator.registerLazySingleton(() => GetSelectedLanguageUseCase(locator())); + locator.registerLazySingleton(() => SaveSelectedLanguageUseCase(locator())); } Future initDataBase() async { @@ -101,4 +125,4 @@ Future initDataBase() async { ..registerAdapter(TotalDataEntityAdapter()); await Hive.openBox(MyConstants.levelBox); -} \ No newline at end of file +} diff --git a/lib/main.dart b/lib/main.dart index 1b11c47..804ecae 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -15,6 +15,7 @@ import 'package:hadi_hoda_flutter/init_bindings.dart'; import 'package:hadi_hoda_flutter/l10n/app_localizations.dart'; import 'features/app/presentation/bloc/app_state.dart'; +import 'features/download/presentation/bloc/download_bloc.dart'; Future main() async { WidgetsFlutterBinding.ensureInitialized(); @@ -35,8 +36,21 @@ class MainApp extends StatelessWidget { @override Widget build(BuildContext context) { - return BlocProvider( - create: (context) => locator()..add(InitLocaleEvent()), + return MultiBlocProvider( + providers: [ + BlocProvider( + create: (context) => locator()..add(InitLocaleEvent()), + ), + BlocProvider( + create: (context) => DownloadBloc( + locator(), + locator(), + locator(), + locator(), + locator(), + ), + ), + ], child: BlocBuilder( builder: (context, state) => MaterialApp.router( theme: MyTheme.light, diff --git a/pubspec.yaml b/pubspec.yaml index 1450632..8f93907 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -9,6 +9,7 @@ environment: dependencies: auto_size_text: ^3.0.0 bloc: ^9.0.0 + collection: ^1.19.1 dio: ^5.9.0 easy_stepper: ^0.8.5+1 equatable: ^2.0.7 @@ -32,6 +33,7 @@ dependencies: shared_preferences: ^2.5.3 showcaseview: ^5.0.1 vector_graphics: ^1.1.19 + wheel_chooser: ^1.0.1 dev_dependencies: flutter_test: