You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 

340 lines
9.4 KiB

import 'dart:async';
import 'dart:io';
import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter_archive/flutter_archive.dart';
import 'package:hadi_hoda_flutter/core/constants/my_api.dart';
import 'package:hadi_hoda_flutter/core/constants/my_constants.dart';
import 'package:hadi_hoda_flutter/core/error_handler/my_exception.dart';
import 'package:hadi_hoda_flutter/core/network/http_request.dart';
import 'package:hadi_hoda_flutter/core/response/base_response.dart';
import 'package:hadi_hoda_flutter/core/utils/local_storage.dart';
import 'package:hadi_hoda_flutter/core/utils/storage_path.dart';
import 'package:hadi_hoda_flutter/features/download/domain/entities/download_entity.dart';
import 'package:hadi_hoda_flutter/features/level/data/model/node_model.dart';
import 'package:hadi_hoda_flutter/features/level/domain/entity/node_entity.dart';
import 'package:hadi_hoda_flutter/features/level/domain/entity/total_data_entity.dart';
import 'package:hive/hive.dart';
abstract class IDownloadDatasource {
Future<void> saveLevels();
Future<void> batchDownload(int toLevel);
Future<int> getLastDownloadedLevel();
Stream<DownloadEntity> loadingStream();
void cancelDownload();
}
class DownloadDatasourceImpl implements IDownloadDatasource {
final IHttpRequest httpRequest;
final StreamController<DownloadEntity> streamController =
StreamController<DownloadEntity>.broadcast();
CancelToken? _audioCancelToken;
CancelToken? _imageCancelToken;
bool _isBatchDownloading = false;
DownloadDatasourceImpl(this.httpRequest);
@override
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);
}
}
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,
savePath: filePath,
queryParameters: {'batch_start': level, 'batch_size': 1},
cancelToken: _imageCancelToken,
onReceive: (count, total) {
debugPrint("Started _getLevelImage($level) : ${(count / total) * 100}");
},
);
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;
}
throw MyException(errorMessage: '$e');
}
_imageCompleted++;
_emitProgress();
}
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 (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,
savePath: filePath,
queryParameters: {
'batch_start': level,
'batch_size': 1,
'lang': lang,
},
cancelToken: _audioCancelToken,
onReceive: (count, total) {
debugPrint("Started _getLevelAudio($level) : ${(count / total) * 100}");
},
);
if (response is Response && response.statusCode == 204) {
debugPrint("No audio content for level $level (204)");
_audioCompleted++;
_emitProgress();
return;
}
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;
}
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
Future<void> saveLevels() async {
final String selectedLanguage =
LocalStorage.readData(key: MyConstants.selectLanguage) ??
MyConstants.defaultLanguage;
final Box<TotalDataEntity> data = Hive.box(MyConstants.levelBox);
// Check if data already exists for this language
bool dataExists = false;
try {
data.values.firstWhere((e) => e.code == selectedLanguage);
dataExists = true;
} catch (_) {
dataExists = false;
}
if (!dataExists) {
final response = await httpRequest.get(
path: MyApi.levels,
queryParameters: {'lang': selectedLanguage},
);
final List<NodeEntity> levels = BaseResponse.getDataList<NodeEntity>(
response?['path'],
(json) => NodeModel.fromJson(json),
);
LocalStorage.saveData(key: MyConstants.maxLevelCount, value: '${levels.length}');
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
Stream<DownloadEntity> loadingStream() => streamController.stream;
}