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
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;
|
|
}
|