Browse Source

fix

develop
sina 3 days ago
parent
commit
108c5fe397
  1. BIN
      assets/videos/globe_state_speaking.gif
  2. 6
      lib/core/constants/my_api.dart
  3. 16
      lib/core/constants/my_constants.dart
  4. 6
      lib/core/middlewares/my_middlewares.dart
  5. 1
      lib/core/network/http_request.dart
  6. 6
      lib/core/network/http_request_impl.dart
  7. 27
      lib/core/routers/my_routes.dart
  8. 2
      lib/core/widgets/answer_box/answer_box.dart
  9. 1
      lib/core/widgets/answer_box/styles/picture_box.dart
  10. 18
      lib/features/app/presentation/bloc/app_bloc.dart
  11. 355
      lib/features/download/data/datasource/download_datasource.dart
  12. 61
      lib/features/download/data/repository_impl/download_repository_impl.dart
  13. 27
      lib/features/download/domain/entities/download_entity.dart
  14. 7
      lib/features/download/domain/repository/download_repository.dart
  15. 17
      lib/features/download/domain/usecases/batch_download_usecase.dart
  16. 12
      lib/features/download/domain/usecases/cancel_download_usecase.dart
  17. 32
      lib/features/download/domain/usecases/get_audios_usecase.dart
  18. 32
      lib/features/download/domain/usecases/get_images_usecase.dart
  19. 17
      lib/features/download/domain/usecases/get_last_downloaded_level.dart
  20. 273
      lib/features/download/presentation/bloc/download_bloc.dart
  21. 26
      lib/features/download/presentation/bloc/download_event.dart
  22. 11
      lib/features/download/presentation/bloc/download_state.dart
  23. 114
      lib/features/download/presentation/ui/download_page.dart
  24. 4
      lib/features/download/presentation/ui/widgets/download_loading_widget.dart
  25. 7
      lib/features/guider/data/datasource/guider_datasource.dart
  26. 41
      lib/features/home/presentation/bloc/home_bloc.dart
  27. 44
      lib/features/home/presentation/ui/home_page.dart
  28. 19
      lib/features/language/data/datasource/language_datasource.dart
  29. 12
      lib/features/language/data/model/language_model.dart
  30. 52
      lib/features/language/data/repository_impl/language_repository_impl.dart
  31. 27
      lib/features/language/domain/entity/language_entity.dart
  32. 9
      lib/features/language/domain/repository/language_repository.dart
  33. 17
      lib/features/language/domain/usecases/get_languages_usecase.dart
  34. 16
      lib/features/language/domain/usecases/get_selected_language_usecase.dart
  35. 16
      lib/features/language/domain/usecases/save_selected_language_usecase.dart
  36. 109
      lib/features/language/presentation/bloc/language_bloc.dart
  37. 16
      lib/features/language/presentation/bloc/language_event.dart
  38. 21
      lib/features/language/presentation/bloc/language_state.dart
  39. 221
      lib/features/language/presentation/ui/language_page.dart
  40. 8
      lib/features/level/data/datasource/level_datasource.dart
  41. 2
      lib/features/level/data/model/level_model.dart
  42. 152
      lib/features/level/presentation/bloc/level_bloc.dart
  43. 99
      lib/features/level/presentation/ui/level_page.dart
  44. 8
      lib/features/question/data/datasource/question_datasource.dart
  45. 4
      lib/features/question/data/model/answer_model.dart
  46. 6
      lib/features/question/data/model/question_model.dart
  47. 7
      lib/features/question/domain/entity/answer_entity.dart
  48. 9
      lib/features/question/domain/entity/question_entity.dart
  49. 3
      lib/features/question/presentation/bloc/question_bloc.dart
  50. 3
      lib/features/question/presentation/ui/question_page.dart
  51. 2
      lib/features/splash/presentation/ui/splash_page.dart
  52. 30
      lib/init_bindings.dart
  53. 18
      lib/main.dart
  54. 2
      pubspec.yaml

BIN
assets/videos/globe_state_speaking.gif

Before

Width: 320  |  Height: 320  |  Size: 2.2 MiB

After

Width: 800  |  Height: 800  |  Size: 6.3 MiB

6
lib/core/constants/my_api.dart

@ -11,6 +11,8 @@ class MyApi {
static const String baseUrl = 'https://hadihoda.newhorizonco.uk/api'; static const String baseUrl = 'https://hadihoda.newhorizonco.uk/api';
static const String levels = '/quiz/optimized/v3/levels/'; 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/';
} }

16
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'; import 'package:hadi_hoda_flutter/features/language/domain/entity/language_entity.dart';
class MyConstants { class MyConstants {
@ -22,6 +20,9 @@ class MyConstants {
static const String effectAudioService = 'EFFECT_AUDIO_SERVICE'; static const String effectAudioService = 'EFFECT_AUDIO_SERVICE';
static const String firstIntro = 'FIRST_INTRO'; static const String firstIntro = 'FIRST_INTRO';
static const String firstShowcase = 'FIRST_SHOWCASE'; static const String firstShowcase = 'FIRST_SHOWCASE';
static const String maxLevelCount = 'MAX_LEVEL_COUNT';
static const int firstDownloadBatchCount = 3;
/// Other /// Other
static const double questionAudioVolume = 1.0; static const double questionAudioVolume = 1.0;
@ -29,11 +30,8 @@ class MyConstants {
static const double effectAudioVolume = 0.2; static const double effectAudioVolume = 0.2;
static const String defaultLanguage = 'en'; static const String defaultLanguage = 'en';
static const List<LanguageEntity> languages = [ static const List<LanguageEntity> 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'),
]; ];
}
}

6
lib/core/middlewares/my_middlewares.dart

@ -12,9 +12,9 @@ class MyMiddlewares {
factory MyMiddlewares() => _i; factory MyMiddlewares() => _i;
static FutureOr<String?> splash(BuildContext context, GoRouterState state) { static FutureOr<String?> 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; return Routes.languagePage;
} else { } else {
return null; return null;

1
lib/core/network/http_request.dart

@ -57,6 +57,7 @@ abstract class IHttpRequest {
Object? data, Object? data,
Map<String, dynamic>? queryParameters, Map<String, dynamic>? queryParameters,
Map<String, dynamic>? header, Map<String, dynamic>? header,
String? method,
void Function(int count, int total)? onReceive, void Function(int count, int total)? onReceive,
CancelToken? cancelToken, CancelToken? cancelToken,
}); });

6
lib/core/network/http_request_impl.dart

@ -158,19 +158,21 @@ class HttpRequestImpl implements IHttpRequest {
Object? data, Object? data,
Map<String, dynamic>? queryParameters, Map<String, dynamic>? queryParameters,
Map<String, dynamic>? header, Map<String, dynamic>? header,
String? method,
void Function(int count, int total)? onReceive, void Function(int count, int total)? onReceive,
CancelToken? cancelToken, CancelToken? cancelToken,
}) async { }) async {
try { try {
await _dio.download(
final Response response = await _dio.download(
urlPath, urlPath,
savePath, savePath,
data: data, data: data,
queryParameters: queryParameters, queryParameters: queryParameters,
options: Options(headers: header),
options: Options(headers: header, method: method),
onReceiveProgress: onReceive, onReceiveProgress: onReceive,
cancelToken: cancelToken, cancelToken: cancelToken,
); );
return response;
} on DioException catch (e) { } on DioException catch (e) {
ErrorHandler.handleError(e); ErrorHandler.handleError(e);
} }

27
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/utils/my_context.dart';
import 'package:hadi_hoda_flutter/core/widgets/page_transition/my_page_transition.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/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/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_bloc.dart';
import 'package:hadi_hoda_flutter/features/guider/presentation/bloc/guider_event.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 { class Routes {
static const Routes _i = Routes._internal(); static const Routes _i = Routes._internal();
const Routes._internal(); const Routes._internal();
factory Routes() => _i; factory Routes() => _i;
static const String samplePage = '/sample_page'; static const String samplePage = '/sample_page';
@ -73,15 +74,13 @@ GoRouter _appPages() => GoRouter(
GoRoute( GoRoute(
name: Routes.downloadPage, name: Routes.downloadPage,
path: 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( GoRoute(
name: Routes.introPage, name: Routes.introPage,
@ -101,7 +100,9 @@ GoRouter _appPages() => GoRouter(
pageBuilder: (context, state) => myPageTransition( pageBuilder: (context, state) => myPageTransition(
key: state.pageKey, key: state.pageKey,
child: BlocProvider( child: BlocProvider(
create: (context) => LanguageBloc()..add(const InitLanguageEvent()),
create: (context) =>
LanguageBloc(locator(), locator(), locator())
..add(const GetLanguagesEvent()),
child: const LanguagePage(), child: const LanguagePage(),
), ),
), ),
@ -115,6 +116,7 @@ GoRouter _appPages() => GoRouter(
create: (context) => HomeBloc( create: (context) => HomeBloc(
locator(instanceName: MyConstants.mainAudioService), locator(instanceName: MyConstants.mainAudioService),
locator(instanceName: MyConstants.effectAudioService), locator(instanceName: MyConstants.effectAudioService),
locator(),
), ),
child: const HomePage(), child: const HomePage(),
), ),
@ -130,6 +132,7 @@ GoRouter _appPages() => GoRouter(
locator(), locator(),
locator(instanceName: MyConstants.mainAudioService), locator(instanceName: MyConstants.mainAudioService),
locator(instanceName: MyConstants.effectAudioService), locator(instanceName: MyConstants.effectAudioService),
locator()
)..add(SetCurrentLevelEvent()), )..add(SetCurrentLevelEvent()),
child: const LevelPage(), child: const LevelPage(),
), ),

2
lib/core/widgets/answer_box/answer_box.dart

@ -35,7 +35,7 @@ class _AnswerBoxState extends State<AnswerBox> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Hero(
return Hero(
tag: 'Hero_answer_${widget.answer.id}', tag: 'Hero_answer_${widget.answer.id}',
child: Material( child: Material(
type: MaterialType.transparency, type: MaterialType.transparency,

1
lib/core/widgets/answer_box/styles/picture_box.dart

@ -32,6 +32,7 @@ class AnswerPictureBox extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
debugPrint('image $index : $image');
return CustomPaint( return CustomPaint(
painter: _CustomShapePainter(), painter: _CustomShapePainter(),
child: ClipPath( child: ClipPath(

18
lib/features/app/presentation/bloc/app_bloc.dart

@ -1,4 +1,5 @@
import 'dart:async'; import 'dart:async';
import 'dart:ui';
import 'package:bloc/bloc.dart'; import 'package:bloc/bloc.dart';
import 'package:hadi_hoda_flutter/core/constants/my_constants.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'; import 'package:hadi_hoda_flutter/features/language/domain/entity/language_entity.dart';
class AppBloc extends Bloc<AppEvent, AppState> { class AppBloc extends Bloc<AppEvent, AppState> {
/// ------------constructor------------
AppBloc() : super(const AppState()) { AppBloc() : super(const AppState()) {
on<InitLocaleEvent>(_initLocaleEvent); on<InitLocaleEvent>(_initLocaleEvent);
on<ChangeLocaleEvent>(_changeLocaleEvent); on<ChangeLocaleEvent>(_changeLocaleEvent);
} }
/// ------------UseCases------------
/// ------------Variables------------
/// ------------Controllers------------
/// ------------Functions------------
/// ------------Event Calls------------
FutureOr<void> _initLocaleEvent(InitLocaleEvent event, FutureOr<void> _initLocaleEvent(InitLocaleEvent event,
Emitter<AppState> emit) { Emitter<AppState> emit) {
final String selectLanguage = LocalStorage.readData(key: MyConstants.selectLanguage) ?? MyConstants.defaultLanguage; 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, (e) => e.code == selectLanguage,
orElse: () => LanguageEntity(code: selectLanguage, title: ''),
); );
emit(state.copyWith(locale: findLanguage.locale)); emit(state.copyWith(locale: findLanguage.locale));
} }

355
lib/features/download/data/datasource/download_datasource.dart

@ -1,6 +1,8 @@
import 'dart:async'; import 'dart:async';
import 'dart:io'; import 'dart:io';
import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_archive/flutter_archive.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_api.dart';
import 'package:hadi_hoda_flutter/core/constants/my_constants.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'; import 'package:hive/hive.dart';
abstract class IDownloadDatasource { abstract class IDownloadDatasource {
Future<void> getImages();
Future<void> getAudios();
Future<void> saveLevels(); Future<void> saveLevels();
Future<void> batchDownload(int toLevel);
Future<int> getLastDownloadedLevel();
Stream<DownloadEntity> loadingStream(); Stream<DownloadEntity> loadingStream();
void cancelDownload();
} }
class DownloadDatasourceImpl implements IDownloadDatasource { class DownloadDatasourceImpl implements IDownloadDatasource {
final IHttpRequest httpRequest; final IHttpRequest httpRequest;
final StreamController<DownloadEntity> streamController = StreamController<DownloadEntity>.broadcast();
final StreamController<DownloadEntity> streamController =
StreamController<DownloadEntity>.broadcast();
CancelToken? _audioCancelToken;
CancelToken? _imageCancelToken;
bool _isBatchDownloading = false;
DownloadDatasourceImpl(this.httpRequest); DownloadDatasourceImpl(this.httpRequest);
@override @override
Future<void> 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<bool> _isLevelImagesDownloaded(int level) async {
final String levelPath = '${StoragePath.documentDir.path}/$level/';
final Directory directory = Directory(levelPath);
if (!await directory.exists()) {
return false;
}
final List<FileSystemEntity> files = directory.listSync(recursive: false);
if (files.isEmpty) {
return false;
}
return true;
}
Future<void> _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<void> _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, urlPath: MyApi.images,
savePath: filePath, savePath: filePath,
queryParameters: {'batch_start': level, 'batch_size': 1},
cancelToken: _imageCancelToken,
onReceive: (count, total) { 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'); throw MyException(errorMessage: '$e');
} }
_imageCompleted++;
_emitProgress();
} }
@override
Future<void> getAudios() async {
final String filePath = '${StoragePath.documentDir.path}/audios.zip';
final String selectedLanguage =
LocalStorage.readData(key: MyConstants.selectLanguage) ?? MyConstants.defaultLanguage;
Future<bool> _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<FileSystemEntity> files = directory.listSync(recursive: false);
if (files.isEmpty) {
return false;
}
return true;
}
Future<void> _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<void> _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, urlPath: MyApi.audios,
savePath: filePath, savePath: filePath,
queryParameters: { queryParameters: {
'lang': selectedLanguage,
'batch_start': level,
'batch_size': 1,
'lang': lang,
}, },
cancelToken: _audioCancelToken,
onReceive: (count, total) { 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'); throw MyException(errorMessage: '$e');
} }
_audioCompleted++;
_emitProgress();
}
@override
Future<int> 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 @override
Future<void> saveLevels() async { Future<void> saveLevels() async {
final String selectedLanguage = final String selectedLanguage =
LocalStorage.readData(key: MyConstants.selectLanguage) ?? MyConstants.defaultLanguage;
LocalStorage.readData(key: MyConstants.selectLanguage) ??
MyConstants.defaultLanguage;
final Box<TotalDataEntity> data = Hive.box(MyConstants.levelBox); final Box<TotalDataEntity> 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( final response = await httpRequest.get(
path: MyApi.levels, path: MyApi.levels,
queryParameters: {'lang': selectedLanguage}, queryParameters: {'lang': selectedLanguage},
@ -150,10 +311,30 @@ class DownloadDatasourceImpl implements IDownloadDatasource {
response?['path'], response?['path'],
(json) => NodeModel.fromJson(json), (json) => NodeModel.fromJson(json),
); );
LocalStorage.saveData(key: MyConstants.maxLevelCount, value: '${levels.length}');
await data.add(TotalDataEntity(code: selectedLanguage, nodes: levels)); await data.add(TotalDataEntity(code: selectedLanguage, nodes: levels));
} }
} }
@override
Future<void> 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 @override
Stream<DownloadEntity> loadingStream() => streamController.stream; Stream<DownloadEntity> loadingStream() => streamController.stream;
} }

61
lib/features/download/data/repository_impl/download_repository_impl.dart

@ -11,10 +11,42 @@ class DownloadRepositoryImpl implements IDownloadRepository {
const DownloadRepositoryImpl(this.datasource); const DownloadRepositoryImpl(this.datasource);
// @override
// Future<DataState<NoParams, MyException>> 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<DataState<NoParams, MyException>> 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 @override
Future<DataState<NoParams, MyException>> getImages() async {
Future<DataState<NoParams, MyException>> saveLevels() async {
try { try {
await datasource.getImages();
await datasource.saveLevels();
return DataState.success(NoParams()); return DataState.success(NoParams());
} on MyException catch (e) { } on MyException catch (e) {
return DataState.error(e); return DataState.error(e);
@ -28,9 +60,9 @@ class DownloadRepositoryImpl implements IDownloadRepository {
} }
@override @override
Future<DataState<NoParams, MyException>> getAudios() async {
Future<DataState<NoParams, MyException>> batchDownload(int toLevel) async {
try { try {
await datasource.getAudios();
await datasource.batchDownload(toLevel);
return DataState.success(NoParams()); return DataState.success(NoParams());
} on MyException catch (e) { } on MyException catch (e) {
return DataState.error(e); return DataState.error(e);
@ -44,10 +76,20 @@ class DownloadRepositoryImpl implements IDownloadRepository {
} }
@override @override
Future<DataState<NoParams, MyException>> saveLevels() async {
Stream<DownloadEntity> loadingStream() {
return datasource.loadingStream();
}
@override
void cancelDownload() {
datasource.cancelDownload();
}
@override
Future<DataState<int, MyException>> getLastDownloadLevel() async {
try { try {
await datasource.saveLevels();
return DataState.success(NoParams());
final data = await datasource.getLastDownloadedLevel();
return DataState.success(data);
} on MyException catch (e) { } on MyException catch (e) {
return DataState.error(e); return DataState.error(e);
} catch (e) { } catch (e) {
@ -58,9 +100,4 @@ class DownloadRepositoryImpl implements IDownloadRepository {
} }
} }
} }
@override
Stream<DownloadEntity> loadingStream() {
return datasource.loadingStream();
}
} }

27
lib/features/download/domain/entities/download_entity.dart

@ -1,7 +1,26 @@
class DownloadEntity { 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<String, String> routeParams;
const DownloadPageConfig({
required this.downloadToLevel,
required this.redirectTo,
this.routeParams = const {},
});
const DownloadEntity({this.count, this.total, this.percent});
} }

7
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'; import 'package:hadi_hoda_flutter/features/download/domain/entities/download_entity.dart';
abstract class IDownloadRepository { abstract class IDownloadRepository {
Future<DataState<NoParams, MyException>> getImages();
Future<DataState<NoParams, MyException>> getAudios();
// Future<DataState<NoParams, MyException>> getImages();
// Future<DataState<NoParams, MyException>> getAudios();
Future<DataState<NoParams, MyException>> saveLevels(); Future<DataState<NoParams, MyException>> saveLevels();
Future<DataState<NoParams, MyException>> batchDownload(int toLevel);
Future<DataState<int, MyException>> getLastDownloadLevel();
Stream<DownloadEntity> loadingStream(); Stream<DownloadEntity> loadingStream();
void cancelDownload();
} }

17
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<NoParams, int> {
final IDownloadRepository repository;
const BatchDownloadUseCase(this.repository);
@override
Future<DataState<NoParams, MyException>> call(int toLevel) {
return repository.batchDownload(toLevel);
}
}

12
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();
}
}

32
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<NoParams, NoParams> {
final IDownloadRepository repository;
const GetAudiosUseCase(this.repository);
@override
Future<DataState<NoParams, MyException>> 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<NoParams, NoParams> {
// final IDownloadRepository repository;
//
// const GetAudiosUseCase(this.repository);
//
// @override
// Future<DataState<NoParams, MyException>> call(NoParams params) {
// return repository.getAudios();
// }
// }

32
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<NoParams, NoParams> {
final IDownloadRepository repository;
const GetImagesUseCase(this.repository);
@override
Future<DataState<NoParams, MyException>> 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<NoParams, NoParams> {
// final IDownloadRepository repository;
//
// const GetImagesUseCase(this.repository);
//
// @override
// Future<DataState<NoParams, MyException>> call(NoParams params) {
// return repository.getImages();
// }
// }

17
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<int, NoParams> {
final IDownloadRepository repository;
const GetLastDownloadedLevel(this.repository);
@override
Future<DataState<int, MyException>> call(NoParams noParams) {
return repository.getLastDownloadLevel();
}
}

273
lib/features/download/presentation/bloc/download_bloc.dart

@ -1,17 +1,13 @@
import 'dart:async'; import 'dart:async';
import 'package:bloc/bloc.dart'; 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/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/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/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/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/loading_stream_usecase.dart';
import 'package:hadi_hoda_flutter/features/download/domain/usecases/save_levels_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'; 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<DownloadEvent, DownloadState> { class DownloadBloc extends Bloc<DownloadEvent, DownloadState> {
/// ------------constructor------------ /// ------------constructor------------
DownloadBloc( 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(); preCacheImages();
on<GetImagesEvent>(_getImagesEvent);
on<GetAudiosEvent>(_getAudiosEvent);
on<StartDownloadEvent>(_startDownloadEvent);
on<SaveLevelsEvent>(_saveLevelsEvent); on<SaveLevelsEvent>(_saveLevelsEvent);
on<CancelDownloadEvent>(_cancelDownloadEvent);
loadingStream = _loadingStreamUseCase(); loadingStream = _loadingStreamUseCase();
} }
/// ------------UseCases------------ /// ------------UseCases------------
final GetImagesUseCase _getImagesUseCase;
final GetAudiosUseCase _getAudiosUseCase;
final SaveLevelsUseCase _saveLevelsUseCase; final SaveLevelsUseCase _saveLevelsUseCase;
final BatchDownloadUseCase _batchDownloadUseCase;
final LoadingStreamUseCase _loadingStreamUseCase; final LoadingStreamUseCase _loadingStreamUseCase;
final CancelDownloadUseCase _cancelDownloadUseCase;
final GetLastDownloadedLevel _getLastDownloadedLevel;
/// ------------Variables------------
Stream<DownloadEntity> loadingStream = const Stream.empty(); Stream<DownloadEntity> loadingStream = const Stream.empty();
bool _isDownloading = false;
bool _isCancelled = false;
/// ------------Controllers------------
/// ------------Functions------------
/// ------------Api Calls------------
FutureOr<void> _getImagesEvent(
GetImagesEvent event,
Emitter<DownloadState> 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<void> _getAudiosEvent(
GetAudiosEvent event,
FutureOr<void> _startDownloadEvent(
StartDownloadEvent event,
Emitter<DownloadState> emit, Emitter<DownloadState> emit,
) async { ) 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<int> 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<void> _startBatchDownloadEvent(
// StartBatchDownloadEvent event,
// Emitter<DownloadState> 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<void> _getImagesEvent(
// GetImagesEvent event,
// Emitter<DownloadState> 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<void> _getAudiosEvent(
// GetAudiosEvent event,
// Emitter<DownloadState> 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<void> _saveLevelsEvent( FutureOr<void> _saveLevelsEvent(
SaveLevelsEvent event,
Emitter<DownloadState> 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<DownloadState> 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<void> _cancelDownloadEvent(
CancelDownloadEvent event,
Emitter<DownloadState> emit,
) {
_isCancelled = true;
_isDownloading = false;
_cancelDownloadUseCase(NoParams());
emit(state.copyWith(status: const BaseInit()));
} }
} }

26
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 { sealed class DownloadEvent {
const 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<String, String>? destinationPathParameters;
//
// const StartBatchDownloadEvent({
// required this.batchParams,
// this.destinationRoute,
// this.destinationPathParameters,
// });
// }
// class GetImagesEvent extends DownloadEvent {}
// class GetAudiosEvent extends DownloadEvent {}
class SaveLevelsEvent extends DownloadEvent {} class SaveLevelsEvent extends DownloadEvent {}
class CancelDownloadEvent extends DownloadEvent {}

11
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/core/status/base_status.dart';
import 'package:hadi_hoda_flutter/features/download/domain/entities/download_entity.dart';
class DownloadState { class DownloadState {
final BaseStatus getFilesStatus;
final BaseStatus status;
const DownloadState({this.getFilesStatus = const BaseInit()});
const DownloadState({
this.status = const BaseInit(),
});
DownloadState copyWith({ DownloadState copyWith({
BaseStatus? getFilesStatus,
BaseStatus? status,
}) { }) {
return DownloadState( return DownloadState(
getFilesStatus: getFilesStatus ?? this.getFilesStatus,
status: status ?? this.status,
); );
} }
} }

114
lib/features/download/presentation/ui/download_page.dart

@ -1,8 +1,10 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.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_assets.dart';
import 'package:hadi_hoda_flutter/common_ui/resources/my_spaces.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/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/status/base_status.dart';
import 'package:hadi_hoda_flutter/core/utils/convert_size.dart'; import 'package:hadi_hoda_flutter/core/utils/convert_size.dart';
import 'package:hadi_hoda_flutter/core/utils/my_localization.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/bloc/download_state.dart';
import 'package:hadi_hoda_flutter/features/download/presentation/ui/widgets/download_loading_widget.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<DownloadPage> createState() => _DownloadPageState();
}
class _DownloadPageState extends State<DownloadPage> {
@override
void initState() {
super.initState();
final bloc = context.read<DownloadBloc>();
if (!bloc.isDownloading) {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (mounted) {
bloc.add(StartDownloadEvent(toLevel: widget.config.downloadToLevel));
}
});
}
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -28,10 +56,7 @@ class DownloadPage extends StatelessWidget {
gradient: const LinearGradient( gradient: const LinearGradient(
begin: Alignment.topCenter, begin: Alignment.topCenter,
end: Alignment.bottomCenter, end: Alignment.bottomCenter,
colors: [
Color(0XFF00154C),
Color(0XFF150532),
],
colors: [Color(0XFF00154C), Color(0XFF150532)],
), ),
image: DecorationImage( image: DecorationImage(
image: const AssetImage(MyAssets.pattern), image: const AssetImage(MyAssets.pattern),
@ -43,31 +68,45 @@ class DownloadPage extends StatelessWidget {
), ),
), ),
), ),
child: BlocBuilder<DownloadBloc, DownloadState>(
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<DownloadBloc>().add(GetImagesEvent()),
),
);
child: BlocConsumer<DownloadBloc, DownloadState>(
listener: (context, state) {
if(state.status is BaseComplete) {
if(widget.config.redirectTo == Routes.homePage) {
context.goNamed(Routes.homePage);
} else { } 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<DownloadBloc>().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() { Widget _image() {
return const Stack( return const Stack(
children: [ children: [
MyImage(
image: MyAssets.hadiHoda,
),
MyImage(image: MyAssets.hadiHoda),
PositionedDirectional( PositionedDirectional(
start: MySpaces.s10, start: MySpaces.s10,
top: MySpaces.s40, top: MySpaces.s40,
child: MyImage(
image: MyAssets.globe,
),
child: MyImage(image: MyAssets.globe),
), ),
], ],
); );
@ -96,16 +131,13 @@ class DownloadPage extends StatelessWidget {
child: Column( child: Column(
spacing: MySpaces.s6, spacing: MySpaces.s6,
children: [ children: [
Text(
context.translate.please_wait,
style: MYTextStyle.titr0,
),
Text(context.translate.please_wait, style: MYTextStyle.titr0),
StreamBuilder<DownloadEntity>( StreamBuilder<DownloadEntity>(
initialData: const DownloadEntity(),
initialData: DownloadEntity.empty(),
stream: context.read<DownloadBloc>().loadingStream, stream: context.read<DownloadBloc>().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,
), ),
), ),
], ],

4
lib/features/download/presentation/ui/widgets/download_loading_widget.dart

@ -34,7 +34,7 @@ class DownloadLoadingWidget extends StatelessWidget {
), ),
), ),
child: StreamBuilder<DownloadEntity>( child: StreamBuilder<DownloadEntity>(
initialData: const DownloadEntity(),
initialData: DownloadEntity.empty(),
stream: loadingStream, stream: loadingStream,
builder: (context, snapshot) { builder: (context, snapshot) {
return Row( return Row(
@ -72,7 +72,7 @@ class DownloadLoadingWidget extends StatelessWidget {
flex: 15, flex: 15,
child: Center( child: Center(
child: Text( child: Text(
'${snapshot.data?.percent?.toInt() ?? 0}%',
'${snapshot.data?.percent.toInt() ?? 0}%',
style: MYTextStyle.titr4.copyWith( style: MYTextStyle.titr4.copyWith(
color: const Color(0XFF6E83A8), color: const Color(0XFF6E83A8),
), ),

7
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/constants/my_constants.dart';
import 'package:hadi_hoda_flutter/core/error_handler/my_exception.dart'; import 'package:hadi_hoda_flutter/core/error_handler/my_exception.dart';
import 'package:hadi_hoda_flutter/core/utils/local_storage.dart'; import 'package:hadi_hoda_flutter/core/utils/local_storage.dart';
@ -20,13 +21,15 @@ class GuiderDatasourceImpl implements IGuiderDatasource {
LocalStorage.readData(key: MyConstants.selectLanguage) ?? LocalStorage.readData(key: MyConstants.selectLanguage) ??
MyConstants.defaultLanguage; MyConstants.defaultLanguage;
final Box<TotalDataEntity> levelBox = Hive.box(MyConstants.levelBox); final Box<TotalDataEntity> levelBox = Hive.box(MyConstants.levelBox);
final TotalDataEntity findData = levelBox.values.singleWhere(
final TotalDataEntity findData = levelBox.values.firstWhere(
(e) => e.code == selectedLanguage, (e) => e.code == selectedLanguage,
orElse: () => TotalDataEntity(), orElse: () => TotalDataEntity(),
); );
final LevelEntity? findLevel = findData.nodes?.first.level; final LevelEntity? findLevel = findData.nodes?.first.level;
return findLevel ?? LevelEntity(); return findLevel ?? LevelEntity();
} catch (e) {
} catch (e,s) {
debugPrint(e.toString());
debugPrint(s.toString());
throw MyException(errorMessage: '$e'); throw MyException(errorMessage: '$e');
} }
} }

41
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/my_context.dart';
import 'package:hadi_hoda_flutter/core/utils/storage_path.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/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_event.dart';
import 'package:hadi_hoda_flutter/features/home/presentation/bloc/home_state.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<HomeEvent, HomeState> { class HomeBloc extends Bloc<HomeEvent, HomeState> {
/// ------------constructor------------ /// ------------constructor------------
HomeBloc(this._mainAudioService, this._effectAudioService)
HomeBloc(this._mainAudioService, this._effectAudioService, this._getLastDownloadedLevel)
: super(const HomeState()) { : super(const HomeState()) {
volumeStream = _mainAudioService.volumeStream(); volumeStream = _mainAudioService.volumeStream();
playMusic(); playMusic();
@ -30,7 +32,7 @@ class HomeBloc extends Bloc<HomeEvent, HomeState> {
} }
/// ------------UseCases------------ /// ------------UseCases------------
final GetLastDownloadedLevel _getLastDownloadedLevel;
/// ------------Variables------------ /// ------------Variables------------
late final Stream<double> volumeStream; late final Stream<double> volumeStream;
@ -39,22 +41,31 @@ class HomeBloc extends Bloc<HomeEvent, HomeState> {
final AudioService _effectAudioService; final AudioService _effectAudioService;
/// ------------Functions------------ /// ------------Functions------------
void goToLevelPage(BuildContext context) {
final String? selectedLanguage = LocalStorage.readData(
key: MyConstants.selectLanguage,
);
final Box<TotalDataEntity> 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); context.goNamed(Routes.levelPage);
} else { } else {
context.goNamed(Routes.downloadPage);
context.goNamed(
Routes.downloadPage,
extra: const DownloadPageConfig(
downloadToLevel: MyConstants.firstDownloadBatchCount,
redirectTo: Routes.homePage
),
);
} }
} }
Future<int> getLastDownloadedLevel() async {
final result = await _getLastDownloadedLevel(NoParams());
return result.data ?? 0;
}
void goToLanguagePage(BuildContext context) { void goToLanguagePage(BuildContext context) {
context.pushNamed(Routes.languagePage); context.pushNamed(Routes.languagePage);
} }

44
lib/features/home/presentation/ui/home_page.dart

@ -1,7 +1,11 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.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_assets.dart';
import 'package:hadi_hoda_flutter/common_ui/resources/my_spaces.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/my_localization.dart';
import 'package:hadi_hoda_flutter/core/utils/screen_size.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/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/images/my_image.dart';
import 'package:hadi_hoda_flutter/core/widgets/inkwell/my_inkwell.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/core/widgets/pop_scope/my_pop_scope.dart';
import 'package:hadi_hoda_flutter/features/download/domain/entities/download_entity.dart';
import 'package:hadi_hoda_flutter/features/home/presentation/bloc/home_bloc.dart'; import 'package:hadi_hoda_flutter/features/home/presentation/bloc/home_bloc.dart';
class HomePage extends StatelessWidget {
class HomePage extends StatefulWidget {
const HomePage({super.key}); const HomePage({super.key});
@override
State<HomePage> createState() => _HomePageState();
}
class _HomePageState extends State<HomePage> {
@override
void initState() {
super.initState();
WidgetsBinding.instance.addPostFrameCallback((_) {
_checkFirstBatch();
});
}
void _checkFirstBatch() async {
if (!mounted) return;
final bloc = context.read<HomeBloc>();
final lastDownloadLevel = await bloc.getLastDownloadedLevel();
if (lastDownloadLevel < MyConstants.firstDownloadBatchCount && mounted) {
context.goNamed(
Routes.downloadPage,
extra: const DownloadPageConfig(
downloadToLevel: MyConstants.firstDownloadBatchCount,
redirectTo: Routes.homePage,
),
);
}
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
@ -63,21 +96,16 @@ class HomePage extends StatelessWidget {
); );
} }
Positioned _image(BuildContext context) { Positioned _image(BuildContext context) {
return Positioned( return Positioned(
top: setSize(context: context, mobile: 0.1.h, tablet: 0.15.h), top: setSize(context: context, mobile: 0.1.h, tablet: 0.15.h),
child: const Stack( child: const Stack(
children: [ children: [
MyImage(
image: MyAssets.hadiHoda,
),
MyImage(image: MyAssets.hadiHoda),
PositionedDirectional( PositionedDirectional(
start: MySpaces.s10, start: MySpaces.s10,
top: MySpaces.s40, top: MySpaces.s40,
child: MyImage(
image: MyAssets.globe,
),
child: MyImage(image: MyAssets.globe),
), ),
], ],
), ),

19
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<List<LanguageModel>> getLanguages();
}
class LanguageDatasourceImpl implements ILanguageDatasource {
final IHttpRequest httpRequest;
LanguageDatasourceImpl(this.httpRequest);
@override
Future<List<LanguageModel>> getLanguages() async {
final response = await httpRequest.get(path: MyApi.languages);
return (response['results'] as List).map((e) => LanguageModel.fromJson(e)).toList();
}
}

12
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<String, dynamic> json) {
return LanguageModel(
code: json['code'] ?? '',
title: json['name'] ?? '', // API returns 'name', we map to 'title'
);
}
}

52
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<DataState<List<LanguageEntity>, MyException>> getLanguages() async {
try {
final languages = await languageDatasource.getLanguages();
// Explicitly convert to a List<LanguageEntity> to prevent runtime type errors.
return DataState.success(List<LanguageEntity>.from(languages));
} on MyException catch (e) {
return DataState.error(e);
}
}
@override
LanguageEntity getSelectedLanguage(List<LanguageEntity> 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<void> saveSelectedLanguage(String code) async {
await LocalStorage.saveData(key: MyConstants.selectLanguage, value: code);
}
LanguageEntity _getFallbackLanguage(List<LanguageEntity> languages) {
return languages.firstWhere(
(lang) => lang.code == MyConstants.defaultLanguage,
orElse: () => languages.first,
);
}
}

27
lib/features/language/domain/entity/language_entity.dart

@ -1,21 +1,20 @@
import 'dart:ui';
import 'package:equatable/equatable.dart'; import 'package:equatable/equatable.dart';
import 'package:flutter/material.dart';
class LanguageEntity extends Equatable { 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 @override
List<Object?> get props => [
title,
code,
locale,
];
List<Object?> get props => [code, title];
} }

9
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<DataState<List<LanguageEntity>, MyException>> getLanguages();
LanguageEntity getSelectedLanguage(List<LanguageEntity> languages);
Future<void> saveSelectedLanguage(String code);
}

17
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<List<LanguageEntity>, NoParams> {
final ILanguageRepository repository;
GetLanguagesUseCase(this.repository);
@override
Future<DataState<List<LanguageEntity>, MyException>> call(NoParams params) {
return repository.getLanguages();
}
}

16
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<LanguageEntity, List<LanguageEntity>> {
final ILanguageRepository repository;
GetSelectedLanguageUseCase(this.repository);
@override
Future<DataState<LanguageEntity, MyException>> call(List<LanguageEntity> params) async {
return DataState.success(repository.getSelectedLanguage(params));
}
}

16
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<void, String> {
final ILanguageRepository repository;
SaveSelectedLanguageUseCase(this.repository);
@override
Future<DataState<void, MyException>> call(String params) async {
await repository.saveSelectedLanguage(params);
return DataState.success(null);
}
}

109
lib/features/language/presentation/bloc/language_bloc.dart

@ -1,82 +1,61 @@
import 'dart:async'; import 'dart:async';
import 'dart:io';
import 'package:bloc/bloc.dart'; 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_event.dart';
import 'package:hadi_hoda_flutter/features/language/presentation/bloc/language_state.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<LanguageEvent, LanguageState> { class LanguageBloc extends Bloc<LanguageEvent, LanguageState> {
/// ------------constructor------------
LanguageBloc() : super(const LanguageState()) {
on<ChangeLanguageEvent>(_changeLanguageEvent);
on<SaveLevelsEvent>(_saveLevelsEvent);
on<InitLanguageEvent>(_initLanguageEvent);
final GetLanguagesUseCase _getLanguagesUseCase;
final GetSelectedLanguageUseCase _getSelectedLanguageUseCase;
final SaveSelectedLanguageUseCase _saveSelectedLanguageUseCase;
LanguageBloc(
this._getLanguagesUseCase,
this._getSelectedLanguageUseCase,
this._saveSelectedLanguageUseCase,
) : super(const LanguageState()) {
on<GetLanguagesEvent>(_onGetLanguagesEvent);
on<SelectLanguageEvent>(_onSelectLanguageEvent);
} }
/// ------------UseCases------------
/// ------------Variables------------
/// ------------Controllers------------
/// ------------Functions------------
FutureOr<void> _changeLanguageEvent(
ChangeLanguageEvent event,
Emitter<LanguageState> emit,
) {
emit(state.copyWith(selectedLang: event.lang));
}
/// ------------Api Calls------------
FutureOr<void> _saveLevelsEvent(
SaveLevelsEvent event,
FutureOr<void> _onGetLanguagesEvent(
GetLanguagesEvent event,
Emitter<LanguageState> emit, Emitter<LanguageState> emit,
) async { ) 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 { } 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<void> _initLanguageEvent(InitLanguageEvent event, Emitter<LanguageState> emit) {
final String selectedLanguage = LocalStorage.readData(
key: MyConstants.selectLanguage) ?? MyConstants.defaultLanguage;
emit(state.copyWith(selectedLang: LanguageEntity(code: selectedLanguage)));
FutureOr<void> _onSelectLanguageEvent(
SelectLanguageEvent event,
Emitter<LanguageState> emit,
) async {
await _saveSelectedLanguageUseCase(event.language.code);
emit(state.copyWith(selectedLanguage: event.language));
} }
} }

16
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 { sealed class LanguageEvent {
const 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);
}

21
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/core/status/base_status.dart';
import 'package:hadi_hoda_flutter/features/language/domain/entity/language_entity.dart'; import 'package:hadi_hoda_flutter/features/language/domain/entity/language_entity.dart';
class LanguageState { class LanguageState {
final BaseStatus saveLevelsStatus;
final LanguageEntity selectedLang;
final BaseStatus getLanguagesStatus;
final List<LanguageEntity> languages;
final LanguageEntity? selectedLanguage;
const LanguageState({ const LanguageState({
this.saveLevelsStatus = const BaseInit(),
this.selectedLang = const LanguageEntity(code: MyConstants.defaultLanguage),
this.getLanguagesStatus = const BaseInit(),
this.languages = const [],
this.selectedLanguage,
}); });
LanguageState copyWith({ LanguageState copyWith({
BaseStatus? saveLevelsStatus,
LanguageEntity? selectedLang,
BaseStatus? getLanguagesStatus,
List<LanguageEntity>? languages,
LanguageEntity? selectedLanguage,
}) { }) {
return LanguageState( return LanguageState(
saveLevelsStatus: saveLevelsStatus ?? this.saveLevelsStatus,
selectedLang: selectedLang ?? this.selectedLang,
getLanguagesStatus: getLanguagesStatus ?? this.getLanguagesStatus,
languages: languages ?? this.languages,
selectedLanguage: selectedLanguage ?? this.selectedLanguage,
); );
} }
} }

221
lib/features/language/presentation/ui/language_page.dart

@ -1,24 +1,52 @@
import 'package:auto_size_text/auto_size_text.dart'; import 'package:auto_size_text/auto_size_text.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.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_assets.dart';
import 'package:hadi_hoda_flutter/common_ui/resources/my_colors.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_spaces.dart';
import 'package:hadi_hoda_flutter/common_ui/resources/my_text_style.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/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/my_localization.dart';
import 'package:hadi_hoda_flutter/core/utils/screen_size.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/utils/set_platform_size.dart';
import 'package:hadi_hoda_flutter/core/widgets/button/my_blue_button.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/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_bloc.dart';
import 'package:hadi_hoda_flutter/features/language/presentation/bloc/language_event.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/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}); const LanguagePage({super.key});
@override
State<LanguagePage> createState() => _LanguagePageState();
}
class _LanguagePageState extends State<LanguagePage> {
final controller = FixedExtentScrollController(initialItem: 50);
bool _isEnabled = false;
@override
void initState() {
super.initState();
context.read<LanguageBloc>().add(const GetLanguagesEvent());
}
@override
void dispose() {
controller.dispose();
super.dispose();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( 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<LanguageBloc, LanguageState>(
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<LanguageBloc>().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<LanguageBloc>().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<LanguageBloc, LanguageState>(
buildWhen: (previous, current) =>
previous.selectedLang.code != current.selectedLang.code,
builder: (context, state) {
final LanguageBloc languageBloc = context.read<LanguageBloc>();
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( return MyBlueButton(
onTap: () => context.read<LanguageBloc>().add(const SaveLevelsEvent()),
onTap: (_isEnabled && state.selectedLanguage != null)
? () async {
final downloadBloc = context.read<DownloadBloc>();
// 1. Cancel any previous downloads.
downloadBloc.add(CancelDownloadEvent());
// 2. Update App Locale
context.read<AppBloc>().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<DownloadBloc>().add(
const StartDownloadEvent(
toLevel: MyConstants.firstDownloadBatchCount,
),
);
context.goNamed(
Routes.downloadPage,
extra: const DownloadPageConfig(
downloadToLevel: MyConstants.firstDownloadBatchCount,
redirectTo: Routes.homePage,
),
);
}
}
: null,
title: context.translate.select, title: context.translate.select,
); );
} }

8
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/constants/my_constants.dart';
import 'package:hadi_hoda_flutter/core/error_handler/my_exception.dart'; import 'package:hadi_hoda_flutter/core/error_handler/my_exception.dart';
import 'package:hadi_hoda_flutter/core/params/level_params.dart'; import 'package:hadi_hoda_flutter/core/params/level_params.dart';
@ -20,12 +21,15 @@ class LocalLevelDatasourceImpl implements ILevelDatasource {
final String selectedLanguage = LocalStorage.readData( final String selectedLanguage = LocalStorage.readData(
key: MyConstants.selectLanguage) ?? MyConstants.defaultLanguage; key: MyConstants.selectLanguage) ?? MyConstants.defaultLanguage;
final Box<TotalDataEntity> levelBox = Hive.box(MyConstants.levelBox); final Box<TotalDataEntity> levelBox = Hive.box(MyConstants.levelBox);
final TotalDataEntity findData = levelBox.values.singleWhere(
final TotalDataEntity findData = levelBox.values.firstWhere(
(e) => e.code == selectedLanguage, (e) => e.code == selectedLanguage,
orElse: () => TotalDataEntity(), orElse: () => TotalDataEntity(),
); );
debugPrint("nodesCount : ${findData.nodes?.first.level?.questions}");
return findData.nodes ?? []; return findData.nodes ?? [];
} catch (_) {
} catch (e,s) {
debugPrint(e.toString());
debugPrint(s.toString());
throw const MyException(errorMessage: 'Operation Failed'); throw const MyException(errorMessage: 'Operation Failed');
} }
} }

2
lib/features/level/data/model/level_model.dart

@ -16,7 +16,7 @@ class LevelModel extends LevelEntity {
order: json['order'], order: json['order'],
title: json['title'], title: json['title'],
questions: json['questions'] questions: json['questions']
?.map<QuestionEntity>((e) => QuestionModel.fromJson(e))
?.map<QuestionEntity>((e) => QuestionModel.fromJson(e, json['order']))
.toList(), .toList(),
); );
} }

152
lib/features/level/presentation/bloc/level_bloc.dart

@ -2,10 +2,12 @@ import 'dart:async';
import 'package:bloc/bloc.dart'; import 'package:bloc/bloc.dart';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
import 'package:hadi_hoda_flutter/common_ui/resources/my_audios.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/constants/my_constants.dart';
import 'package:hadi_hoda_flutter/core/params/level_params.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/routers/my_routes.dart';
import 'package:hadi_hoda_flutter/core/services/audio_service.dart'; import 'package:hadi_hoda_flutter/core/services/audio_service.dart';
import 'package:hadi_hoda_flutter/core/status/base_status.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/screen_size.dart';
import 'package:hadi_hoda_flutter/core/utils/set_platform_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/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_entity.dart';
import 'package:hadi_hoda_flutter/features/level/domain/entity/level_location.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'; 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<LevelEvent, LevelState> { class LevelBloc extends Bloc<LevelEvent, LevelState> {
/// ------------constructor------------ /// ------------constructor------------
LevelBloc( LevelBloc(
this._getLeveslUseCase,
this._mainAudioService,
this._effectAudioService,
) : super(const LevelState()) {
this._getLeveslUseCase,
this._mainAudioService,
this._effectAudioService,
this._getLastDownloadedLevel,
) : super(const LevelState()) {
volumeStream = _mainAudioService.volumeStream(); volumeStream = _mainAudioService.volumeStream();
playMusic(); playMusic();
on<GetLevelListEvent>(_getLevelListEvent); on<GetLevelListEvent>(_getLevelListEvent);
@ -46,6 +53,7 @@ class LevelBloc extends Bloc<LevelEvent, LevelState> {
/// ------------UseCases------------ /// ------------UseCases------------
final GetLevelsUseCase _getLeveslUseCase; final GetLevelsUseCase _getLeveslUseCase;
final GetLastDownloadedLevel _getLastDownloadedLevel;
/// ------------Variables------------ /// ------------Variables------------
final List<LevelLocation> locationList = [ final List<LevelLocation> locationList = [
@ -176,13 +184,10 @@ class LevelBloc extends Bloc<LevelEvent, LevelState> {
), ),
]; ];
final List<NodeEntity> nodeList = []; final List<NodeEntity> nodeList = [];
late final Stream<double> volumeStream; late final Stream<double> volumeStream;
/// ------------Controllers------------ /// ------------Controllers------------
final ScrollController scrollController = ScrollController(); final ScrollController scrollController = ScrollController();
final AudioService _mainAudioService; final AudioService _mainAudioService;
@ -197,13 +202,37 @@ class LevelBloc extends Bloc<LevelEvent, LevelState> {
await _mainAudioService.play(); 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) { void goToHomePage(BuildContext context) {
@ -246,39 +275,40 @@ class LevelBloc extends Bloc<LevelEvent, LevelState> {
return currentLevel - 1; return currentLevel - 1;
} }
void showReward({
required BuildContext context,
required PrizeEntity prize,
}) {
void showReward({required BuildContext context, required PrizeEntity prize}) {
showRewardDialog(context: context, prize: prize); showRewardDialog(context: context, prize: prize);
} }
/// ------------Event Calls------------ /// ------------Event Calls------------
FutureOr<void> _getLevelListEvent(GetLevelListEvent event,
Emitter<LevelState> emit) async {
FutureOr<void> _getLevelListEvent(
GetLevelListEvent event,
Emitter<LevelState> emit,
) async {
final int currentLevel = int.parse( final int currentLevel = int.parse(
LocalStorage.readData(key: MyConstants.currentLevel) ?? '1', LocalStorage.readData(key: MyConstants.currentLevel) ?? '1',
); );
await _getLeveslUseCase(LevelParams()).then((value) { 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<LevelEvent, LevelState> {
await Future.delayed(const Duration(seconds: 1)); await Future.delayed(const Duration(seconds: 1));
if (scrollController.hasClients) { 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<void> _setCurrentLevelEvent(SetCurrentLevelEvent event,
Emitter<LevelState> emit) async {
FutureOr<void> _setCurrentLevelEvent(
SetCurrentLevelEvent event,
Emitter<LevelState> emit,
) async {
final String? currentLevel = LocalStorage.readData( final String? currentLevel = LocalStorage.readData(
key: MyConstants.currentLevel);
key: MyConstants.currentLevel,
);
if (currentLevel == null || currentLevel.isEmpty) { if (currentLevel == null || currentLevel.isEmpty) {
await LocalStorage.saveData(key: MyConstants.currentLevel, value: '1'); await LocalStorage.saveData(key: MyConstants.currentLevel, value: '1');
} }
add(GetLevelListEvent()); add(GetLevelListEvent());
} }
FutureOr<void> _chooseLevelEvent(ChooseLevelEvent event,
Emitter<LevelState> emit,) {
FutureOr<void> _chooseLevelEvent(
ChooseLevelEvent event,
Emitter<LevelState> emit,
) {
if (event.type != LevelType.unFinished) { if (event.type != LevelType.unFinished) {
emit(state.copyWith(chooseLevel: event.level)); emit(state.copyWith(chooseLevel: event.level));
} }
} }
} }

99
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_assets.dart';
import 'package:hadi_hoda_flutter/common_ui/resources/my_audios.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/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/screen_size.dart';
import 'package:hadi_hoda_flutter/core/utils/set_platform_size.dart'; import 'package:hadi_hoda_flutter/core/utils/set_platform_size.dart';
import 'package:hadi_hoda_flutter/core/widgets/animations/rotation_planet.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/images/my_image.dart';
import 'package:hadi_hoda_flutter/core/widgets/inkwell/my_inkwell.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/core/widgets/pop_scope/my_pop_scope.dart';
import 'package:hadi_hoda_flutter/features/download/domain/entities/download_entity.dart';
import 'package:hadi_hoda_flutter/features/download/presentation/bloc/download_bloc.dart';
import 'package:hadi_hoda_flutter/features/download/presentation/bloc/download_event.dart';
import 'package:hadi_hoda_flutter/features/download/presentation/bloc/download_state.dart';
import 'package:hadi_hoda_flutter/features/download/presentation/ui/widgets/download_loading_widget.dart';
import 'package:hadi_hoda_flutter/features/level/domain/entity/level_entity.dart'; import 'package:hadi_hoda_flutter/features/level/domain/entity/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_bloc.dart';
import 'package:hadi_hoda_flutter/features/level/presentation/bloc/level_event.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/node_widget.dart';
import 'package:hadi_hoda_flutter/features/level/presentation/ui/widgets/play_button.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}); const LevelPage({super.key});
@override
State<LevelPage> createState() => _LevelPageState();
}
class _LevelPageState extends State<LevelPage> {
@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<DownloadBloc>().add(StartDownloadEvent(toLevel: maxLevelCount));
});
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
@ -44,7 +69,7 @@ class LevelPage extends StatelessWidget {
], ],
), ),
), ),
_topButtons(context),
_topSection(context),
Positioned( Positioned(
bottom: 0, bottom: 0,
child: Stack( child: Stack(
@ -263,42 +288,64 @@ class LevelPage extends StatelessWidget {
); );
} }
Positioned _topButtons(BuildContext context) {
Positioned _topSection(BuildContext context) {
return Positioned( return Positioned(
left: MySpaces.s16, left: MySpaces.s16,
right: MySpaces.s16, right: MySpaces.s16,
top: setPlatform(android: MySpaces.s20, iOS: 50), top: setPlatform(android: MySpaces.s20, iOS: 50),
child: Row(
spacing: MySpaces.s16,
child: Column(
children: [ children: [
MyInkwell(
onTap: () => context.read<LevelBloc>().goToHomePage(context),
audio: MyAudios.back,
child: MyImage(
image: MyAssets.homeButton,
size: setSize(context: context, tablet: 80),
),
),
const Spacer(),
DiamondLevel(
diamonds: context.read<LevelBloc>().diamonds,
),
StreamBuilder<double>(
initialData: 1,
stream: context.read<LevelBloc>().volumeStream,
builder: (context, snapshot) => MyInkwell(
onTap: () => context.read<LevelBloc>().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<LevelBloc>().goToHomePage(context),
audio: MyAudios.back,
child: MyImage(
image: MyAssets.homeButton,
size: setSize(context: context, tablet: 80),
),
), ),
),
const Spacer(),
DiamondLevel(
diamonds: context.read<LevelBloc>().diamonds,
),
StreamBuilder<double>(
initialData: 1,
stream: context.read<LevelBloc>().volumeStream,
builder: (context, snapshot) => MyInkwell(
onTap: () => context.read<LevelBloc>().changeMute(),
child: MyImage(
image: snapshot.data == 0 ? MyAssets.musicOff : MyAssets.musicOn,
size: setSize(context: context, tablet: 80),
),
),
),
],
), ),
// _downloadIndicator(),
], ],
), ),
); );
} }
Widget _downloadIndicator() {
return BlocBuilder<DownloadBloc, DownloadState>(
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<DownloadBloc>().loadingStream,
),
);
},
);
}
Widget _background(BuildContext context) { Widget _background(BuildContext context) {
return const MyImage( return const MyImage(
image: MyAssets.mapBackground, image: MyAssets.mapBackground,

8
lib/features/question/data/datasource/question_datasource.dart

@ -22,11 +22,11 @@ class QuestionDatasourceImpl implements IQuestionDatasource {
final String selectedLanguage = LocalStorage.readData( final String selectedLanguage = LocalStorage.readData(
key: MyConstants.selectLanguage) ?? MyConstants.defaultLanguage; key: MyConstants.selectLanguage) ?? MyConstants.defaultLanguage;
final Box<TotalDataEntity> levelBox = Hive.box(MyConstants.levelBox); final Box<TotalDataEntity> levelBox = Hive.box(MyConstants.levelBox);
final TotalDataEntity findData = levelBox.values.singleWhere(
final TotalDataEntity findData = levelBox.values.firstWhere(
(e) => e.code == selectedLanguage, (e) => e.code == selectedLanguage,
orElse: () => TotalDataEntity(), orElse: () => TotalDataEntity(),
); );
final NodeEntity? findLevel = findData.nodes?.singleWhere(
final NodeEntity? findLevel = findData.nodes?.firstWhere(
(e) => e.level?.id == params.id, (e) => e.level?.id == params.id,
orElse: () => NodeEntity(), orElse: () => NodeEntity(),
); );
@ -44,7 +44,7 @@ class QuestionDatasourceImpl implements IQuestionDatasource {
final int index = int.parse( final int index = int.parse(
LocalStorage.readData(key: MyConstants.currentLevel) ?? '1'); LocalStorage.readData(key: MyConstants.currentLevel) ?? '1');
final Box<TotalDataEntity> levelBox = Hive.box(MyConstants.levelBox); final Box<TotalDataEntity> levelBox = Hive.box(MyConstants.levelBox);
final TotalDataEntity findData = levelBox.values.singleWhere(
final TotalDataEntity findData = levelBox.values.firstWhere(
(e) => e.code == selectedLanguage, (e) => e.code == selectedLanguage,
orElse: () => TotalDataEntity(), orElse: () => TotalDataEntity(),
); );
@ -54,7 +54,7 @@ class QuestionDatasourceImpl implements IQuestionDatasource {
if(index > (levelList?.length ?? 0)){ if(index > (levelList?.length ?? 0)){
throw const MyException(); throw const MyException();
} }
final NodeEntity? findLevel = findData.nodes?.singleWhere(
final NodeEntity? findLevel = findData.nodes?.firstWhere(
(e) => e.level?.order == index, (e) => e.level?.order == index,
orElse: () => NodeEntity(), orElse: () => NodeEntity(),
); );

4
lib/features/question/data/model/answer_model.dart

@ -11,12 +11,14 @@ class AnswerModel extends AnswerEntity {
super.isActive, super.isActive,
super.audioID, super.audioID,
super.audioInfo, super.audioInfo,
super.levelOrder,
}); });
factory AnswerModel.fromJson(Map<String, dynamic> json) {
factory AnswerModel.fromJson(Map<String, dynamic> json, int levelOrder) {
return AnswerModel( return AnswerModel(
id: json['id'], id: json['id'],
title: json['title'], title: json['title'],
levelOrder: json['level_order'] ?? levelOrder,
imageId: json['image_id'], imageId: json['image_id'],
imageInfo: json['image_info'] == null imageInfo: json['image_info'] == null
? null ? null

6
lib/features/question/data/model/question_model.dart

@ -21,9 +21,10 @@ class QuestionModel extends QuestionEntity {
super.hadiths, super.hadiths,
super.imageId, super.imageId,
super.imageInfo, super.imageInfo,
super.levelOrder,
}); });
factory QuestionModel.fromJson(Map<String, dynamic> json) {
factory QuestionModel.fromJson(Map<String, dynamic> json, int levelOrder) {
return QuestionModel( return QuestionModel(
id: json['id'], id: json['id'],
title: json['title'], title: json['title'],
@ -35,7 +36,7 @@ class QuestionModel extends QuestionEntity {
correctAnswer: json['correct_answer'], correctAnswer: json['correct_answer'],
isActive: json['is_active'], isActive: json['is_active'],
answers: json['answers'] answers: json['answers']
?.map<AnswerEntity>((e) => AnswerModel.fromJson(e))
?.map<AnswerEntity>((e) => AnswerModel.fromJson(e,levelOrder))
.toList(), .toList(),
correctAnswerAudioId: json['correct_answer_audio_id'], correctAnswerAudioId: json['correct_answer_audio_id'],
correctAnswerText: json['correct_answer_text'], correctAnswerText: json['correct_answer_text'],
@ -49,6 +50,7 @@ class QuestionModel extends QuestionEntity {
imageInfo: json['image_info'] == null imageInfo: json['image_info'] == null
? null ? null
: FileModel.fromJson(json['image_info']), : FileModel.fromJson(json['image_info']),
levelOrder: levelOrder
); );
} }
} }

7
lib/features/question/domain/entity/answer_entity.dart

@ -28,6 +28,8 @@ class AnswerEntity extends HiveObject {
FileEntity? audioInfo; FileEntity? audioInfo;
@HiveField(9) @HiveField(9)
String? audio; String? audio;
@HiveField(10)
int? levelOrder;
AnswerEntity({ AnswerEntity({
this.id, this.id,
@ -38,8 +40,9 @@ class AnswerEntity extends HiveObject {
this.isActive, this.isActive,
this.audioID, this.audioID,
this.audioInfo, 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}';
} }
} }

9
lib/features/question/domain/entity/question_entity.dart

@ -44,6 +44,8 @@ class QuestionEntity extends HiveObject {
FileEntity? imageInfo; FileEntity? imageInfo;
@HiveField(16) @HiveField(16)
String? image; String? image;
@HiveField(17)
int? levelOrder;
QuestionEntity({ QuestionEntity({
this.id, this.id,
@ -60,9 +62,10 @@ class QuestionEntity extends HiveObject {
this.correctAnswerAudioInfo, this.correctAnswerAudioInfo,
this.imageId, this.imageId,
this.imageInfo, 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}';
} }
} }

3
lib/features/question/presentation/bloc/question_bloc.dart

@ -1,5 +1,6 @@
import 'dart:async'; import 'dart:async';
import 'package:collection/collection.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:go_router/go_router.dart'; import 'package:go_router/go_router.dart';
@ -291,7 +292,7 @@ class QuestionBloc extends Bloc<QuestionEvent, QuestionState> {
await showAnswerDialog( await showAnswerDialog(
context: MyContext.get, context: MyContext.get,
correctAudio: state.currentQuestion?.correctAudio, correctAudio: state.currentQuestion?.correctAudio,
answerEntity: state.currentQuestion?.answers?.singleWhere((e) =>
answerEntity: state.currentQuestion?.answers?.firstWhereOrNull((e) =>
e.order == event.correctAnswer) ?? AnswerEntity(), e.order == event.correctAnswer) ?? AnswerEntity(),
showConfetti: true, showConfetti: true,
); );

3
lib/features/question/presentation/ui/question_page.dart

@ -60,6 +60,9 @@ class QuestionPage extends StatelessWidget {
(previous.currentQuestion?.order != (previous.currentQuestion?.order !=
current.currentQuestion?.order), current.currentQuestion?.order),
builder: (context, state) { 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 == if (state.currentQuestion?.order ==
state.levelEntity?.questions?.length) { state.levelEntity?.questions?.length) {
return const DiamondScreen(); return const DiamondScreen();

2
lib/features/splash/presentation/ui/splash_page.dart

@ -53,7 +53,7 @@ class _SplashPageState extends State<SplashPage> {
child: Stack( child: Stack(
alignment: Alignment.center, alignment: Alignment.center,
children: [ children: [
_image(),
// _image(),
_loading(context), _loading(context),
], ],
), ),

30
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/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/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/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_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_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/loading_stream_usecase.dart';
import 'package:hadi_hoda_flutter/features/download/domain/usecases/save_levels_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/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/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/repository/guider_repository.dart';
import 'package:hadi_hoda_flutter/features/guider/domain/usecases/get_first_level_usecase.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/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/data/repository_impl/level_repository_impl.dart';
import 'package:hadi_hoda_flutter/features/level/domain/entity/level_entity.dart'; import 'package:hadi_hoda_flutter/features/level/domain/entity/level_entity.dart';
@ -56,6 +67,11 @@ void initBindings() {
); );
locator.registerSingleton<AppBloc>(AppBloc()); locator.registerSingleton<AppBloc>(AppBloc());
/// Blocs
locator.registerFactory<LanguageBloc>(() => LanguageBloc(locator(), locator(), locator()));
locator.registerFactory<DownloadBloc>(
() => DownloadBloc(locator(), locator(), locator(), locator(), locator()));
/// Sample Feature /// Sample Feature
locator.registerLazySingleton<ISampleDatasource>(() => SampleDatasourceImpl(locator())); locator.registerLazySingleton<ISampleDatasource>(() => SampleDatasourceImpl(locator()));
locator.registerLazySingleton<ISampleRepository>(() => SampleRepositoryImpl(locator())); locator.registerLazySingleton<ISampleRepository>(() => SampleRepositoryImpl(locator()));
@ -64,10 +80,11 @@ void initBindings() {
/// Download Feature /// Download Feature
locator.registerLazySingleton<IDownloadDatasource>(() => DownloadDatasourceImpl(locator())); locator.registerLazySingleton<IDownloadDatasource>(() => DownloadDatasourceImpl(locator()));
locator.registerLazySingleton<IDownloadRepository>(() => DownloadRepositoryImpl(locator())); locator.registerLazySingleton<IDownloadRepository>(() => DownloadRepositoryImpl(locator()));
locator.registerLazySingleton<GetImagesUseCase>(() => GetImagesUseCase(locator()));
locator.registerLazySingleton<GetAudiosUseCase>(() => GetAudiosUseCase(locator()));
locator.registerLazySingleton<SaveLevelsUseCase>(() => SaveLevelsUseCase(locator())); locator.registerLazySingleton<SaveLevelsUseCase>(() => SaveLevelsUseCase(locator()));
locator.registerLazySingleton<BatchDownloadUseCase>(() => BatchDownloadUseCase(locator()));
locator.registerLazySingleton<LoadingStreamUseCase>(() => LoadingStreamUseCase(locator())); locator.registerLazySingleton<LoadingStreamUseCase>(() => LoadingStreamUseCase(locator()));
locator.registerLazySingleton<CancelDownloadUseCase>(() => CancelDownloadUseCase(locator()));
locator.registerLazySingleton<GetLastDownloadedLevel>(() => GetLastDownloadedLevel(locator()));
/// Guider Feature /// Guider Feature
locator.registerLazySingleton<IGuiderDatasource>(() => const GuiderDatasourceImpl()); locator.registerLazySingleton<IGuiderDatasource>(() => const GuiderDatasourceImpl());
@ -84,6 +101,13 @@ void initBindings() {
locator.registerLazySingleton<ILevelDatasource>(() => const LocalLevelDatasourceImpl()); locator.registerLazySingleton<ILevelDatasource>(() => const LocalLevelDatasourceImpl());
locator.registerLazySingleton<ILevelRepository>(() => LevelRepositoryImpl(locator())); locator.registerLazySingleton<ILevelRepository>(() => LevelRepositoryImpl(locator()));
locator.registerLazySingleton<GetLevelsUseCase>(() => GetLevelsUseCase(locator())); locator.registerLazySingleton<GetLevelsUseCase>(() => GetLevelsUseCase(locator()));
/// Language Feature
locator.registerLazySingleton<ILanguageDatasource>(() => LanguageDatasourceImpl(locator()));
locator.registerLazySingleton<ILanguageRepository>(() => LanguageRepositoryImpl(locator()));
locator.registerLazySingleton<GetLanguagesUseCase>(() => GetLanguagesUseCase(locator()));
locator.registerLazySingleton<GetSelectedLanguageUseCase>(() => GetSelectedLanguageUseCase(locator()));
locator.registerLazySingleton<SaveSelectedLanguageUseCase>(() => SaveSelectedLanguageUseCase(locator()));
} }
Future<void> initDataBase() async { Future<void> initDataBase() async {
@ -101,4 +125,4 @@ Future<void> initDataBase() async {
..registerAdapter<TotalDataEntity>(TotalDataEntityAdapter()); ..registerAdapter<TotalDataEntity>(TotalDataEntityAdapter());
await Hive.openBox<TotalDataEntity>(MyConstants.levelBox); await Hive.openBox<TotalDataEntity>(MyConstants.levelBox);
}
}

18
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 'package:hadi_hoda_flutter/l10n/app_localizations.dart';
import 'features/app/presentation/bloc/app_state.dart'; import 'features/app/presentation/bloc/app_state.dart';
import 'features/download/presentation/bloc/download_bloc.dart';
Future<void> main() async { Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
@ -35,8 +36,21 @@ class MainApp extends StatelessWidget {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return BlocProvider(
create: (context) => locator<AppBloc>()..add(InitLocaleEvent()),
return MultiBlocProvider(
providers: [
BlocProvider(
create: (context) => locator<AppBloc>()..add(InitLocaleEvent()),
),
BlocProvider(
create: (context) => DownloadBloc(
locator(),
locator(),
locator(),
locator(),
locator(),
),
),
],
child: BlocBuilder<AppBloc, AppState>( child: BlocBuilder<AppBloc, AppState>(
builder: (context, state) => MaterialApp.router( builder: (context, state) => MaterialApp.router(
theme: MyTheme.light, theme: MyTheme.light,

2
pubspec.yaml

@ -9,6 +9,7 @@ environment:
dependencies: dependencies:
auto_size_text: ^3.0.0 auto_size_text: ^3.0.0
bloc: ^9.0.0 bloc: ^9.0.0
collection: ^1.19.1
dio: ^5.9.0 dio: ^5.9.0
easy_stepper: ^0.8.5+1 easy_stepper: ^0.8.5+1
equatable: ^2.0.7 equatable: ^2.0.7
@ -32,6 +33,7 @@ dependencies:
shared_preferences: ^2.5.3 shared_preferences: ^2.5.3
showcaseview: ^5.0.1 showcaseview: ^5.0.1
vector_graphics: ^1.1.19 vector_graphics: ^1.1.19
wheel_chooser: ^1.0.1
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:

Loading…
Cancel
Save