Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: download all and single Surah #1355

Merged
merged 17 commits into from
Oct 7, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions lib/l10n/intl_en.arb
Original file line number Diff line number Diff line change
Expand Up @@ -321,6 +321,11 @@
"@noFavoriteReciters": {
"description": "Message shown when there are no favorite reciters"
},
"downloadAllSuwarSuccessfully": "The whole quran is downloaded",
"noSuwarDownload": "No new suwars to download",
"connectDownloadQuran":"Please connect to Internet to download",
"playInOnlineModeQuran": "Please connect to internet to play",
"downloaded": "Downloaded",
"switchQuranType": "Switch to {name}",
"@switchQuranType": {
"description": "Message shown when a reciter is added to favorites",
Expand Down
60 changes: 60 additions & 0 deletions lib/src/data/data_source/quran/recite_remote_data_source.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import 'dart:developer';
import 'dart:isolate';
import 'package:meta/meta.dart';

import 'package:dio/dio.dart';
Expand All @@ -10,6 +11,15 @@ import 'package:mawaqit/src/module/dio_module.dart';

import 'package:mawaqit/src/domain/error/recite_exception.dart';

import '../../../domain/model/quran/audio_file_model.dart';

class _DownloadParams {
final String url;
final SendPort sendPort;

_DownloadParams(this.url, this.sendPort);
}

class ReciteRemoteDataSource {
final Dio _dio;

Expand Down Expand Up @@ -85,6 +95,56 @@ class ReciteRemoteDataSource {
static List<int> _convertSurahListToIntegers(String surahList) {
return surahList.split(',').map(int.parse).toList();
}

static void _downloadAudioFileIsolate(_DownloadParams params) async {
final dio = Dio();
try {
final response = await dio.get<List<int>>(
params.url,
options: Options(responseType: ResponseType.bytes),
onReceiveProgress: (received, total) {
if (total != -1) {
final progress = (received / total) * 100;
params.sendPort.send({'progress': progress});
}
},
);

if (response.statusCode == 200) {
params.sendPort.send({'data': response.data!});
} else {
params.sendPort.send({'error': 'Failed to fetch audio file'});
}
} catch (e) {
params.sendPort.send({'error': e.toString()});
}

Isolate.exit();
}

Future<List<int>> downloadAudioFile(
AudioFileModel audioFile,
Function(double) onProgress,
) async {
final receivePort = ReceivePort();
await Isolate.spawn(_downloadAudioFileIsolate, _DownloadParams(audioFile.url, receivePort.sendPort));

List<int> downloadedList = [];
await for (final message in receivePort) {
if (message is Map) {
if (message.containsKey('progress')) {
onProgress(message['progress']);
} else if (message.containsKey('data')) {
downloadedList = message['data'];
break;
} else if (message.containsKey('error')) {
throw (message['error']);
}
}
}

return downloadedList;
}
}

final reciteRemoteDataSourceProvider = Provider<ReciteRemoteDataSource>((ref) {
Expand Down
92 changes: 92 additions & 0 deletions lib/src/data/data_source/quran/reciter_local_data_source.dart
Original file line number Diff line number Diff line change
@@ -1,11 +1,18 @@
import 'dart:developer';
import 'dart:io';

import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:hive_flutter/adapters.dart';
import 'package:mawaqit/src/const/constants.dart';
import 'package:mawaqit/src/domain/model/quran/reciter_model.dart';

import 'package:mawaqit/src/domain/error/recite_exception.dart';
import 'package:path_provider/path_provider.dart';

import '../../../../main.dart';
import 'package:path/path.dart' as path;

import '../../../domain/model/quran/audio_file_model.dart';

class ReciteLocalDataSource {
final Box<ReciterModel> _reciterBox;
Expand All @@ -29,6 +36,57 @@ class ReciteLocalDataSource {
}
}

Future<String> getSurahPathWithExtension({
required String reciterId,
required String moshafId,
required String surahNumber,
}) async {
final Directory appDocDir = await getApplicationDocumentsDirectory();
final String surahPath = '${appDocDir.path}/$reciterId/$moshafId/$surahNumber.mp3';
return surahPath;
}

Future<bool> isSurahDownloaded({
required String reciterId,
required String moshafId,
required int surahNumber,
}) async {
try {
final surahFilePath = await getSurahPathWithExtension(
reciterId: reciterId,
moshafId: moshafId,
surahNumber: surahNumber.toString(),
);
final file = File(surahFilePath);
final exists = await file.exists();

log('ReciteLocalDataSource: isSurahDownloaded: Surah $surahNumber ${exists ? 'exists' : 'does not exist'} for reciter $reciterId and riwayah $moshafId');
return exists;
} catch (e) {
log('ReciteLocalDataSource: error: isSurahDownloaded: ${e.toString()}');
throw CheckSurahExistenceException(e.toString());
}
}

Future<String> saveAudioFile(AudioFileModel audioFileModel, List<int> bytes) async {
try {
final dir = await getApplicationDocumentsDirectory();
final filePath = path.join(dir.path, audioFileModel.filePath);
final file = File(filePath);

await file.parent.create(recursive: true);

// Save the file
await file.writeAsBytes(bytes);

log('ReciteLocalDataSource: saveAudioFile: Saved audio file at $filePath');
return filePath;
} catch (e) {
log('ReciteLocalDataSource: saveAudioFile: ${e.toString()}');
throw SaveAudioFileException(e.toString());
}
}

Future<List<ReciterModel>> getReciters() async {
try {
final reciters = _reciterBox.values.toList();
Expand Down Expand Up @@ -112,6 +170,40 @@ class ReciteLocalDataSource {
bool isFavoriteReciter(int reciterId) {
return _favoriteReciterBox.values.contains(reciterId);
}

Future<String> getSuwarFolderPath({
required String reciterId,
required String moshafId,
}) async {
final Directory appDocDir = await getApplicationDocumentsDirectory();
final String reciterPath = '${appDocDir.path}/$reciterId/$moshafId';
return reciterPath;
}

Future<List<File>> getDownloadedSurahByReciterAndRiwayah({
required String reciterId,
required String moshafId,
}) async {
List<File> downloadedSuwar = [];
try {
// Get the application documents directory
final path = await getSuwarFolderPath(reciterId: reciterId, moshafId: moshafId);
// Check if the reciter's directory exists
if (await Directory(path).exists()) {
List<FileSystemEntity> files = await Directory(path).list().toList();

// Filter and collect .mp3 files
for (var file in files) {
if (file is File && file.path.endsWith('.mp3')) {
downloadedSuwar.add(file);
}
}
}
} catch (e) {
logger.e('An error occurred while fetching downloaded surahs: $e');
}
return downloadedSuwar;
}
}

final reciteLocalDataSourceProvider = FutureProvider<ReciteLocalDataSource>((ref) async {
Expand Down
48 changes: 48 additions & 0 deletions lib/src/data/repository/quran/recite_impl.dart
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import 'dart:io';

import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:mawaqit/src/data/data_source/quran/recite_remote_data_source.dart';
import 'package:mawaqit/src/data/data_source/quran/reciter_local_data_source.dart';
Expand All @@ -6,6 +8,8 @@ import 'package:mawaqit/src/domain/model/quran/reciter_model.dart';

import 'package:mawaqit/src/domain/repository/quran/recite_repository.dart';

import '../../../domain/model/quran/audio_file_model.dart';

class ReciteImpl implements ReciteRepository {
final ReciteRemoteDataSource _remoteDataSource;
final ReciteLocalDataSource _localDataSource;
Expand Down Expand Up @@ -50,6 +54,50 @@ class ReciteImpl implements ReciteRepository {
Future<void> clearAllReciters() async {
await _localDataSource.clearAllReciters();
}

@override
Future<String> getLocalSurahPath({
required String reciterId,
required String moshafId,
required String surahNumber,
}) async {
return await _localDataSource.getSurahPathWithExtension(
moshafId: moshafId,
surahNumber: surahNumber,
reciterId: reciterId,
);
}

@override
Future<bool> isSurahDownloaded({
required String reciterId,
required String moshafId,
required int surahNumber,
}) async {
return await _localDataSource.isSurahDownloaded(
reciterId: reciterId,
moshafId: moshafId,
surahNumber: surahNumber,
);
}

@override
Future<String> downloadAudio(AudioFileModel audioFile, Function(double p1) onProgress) async {
final downloadedList = await _remoteDataSource.downloadAudioFile(audioFile, onProgress);
final path = await _localDataSource.saveAudioFile(audioFile, downloadedList);
return path;
}

@override
Future<List<File>> getDownloadedSuwarByReciterAndRiwayah({
required String reciterId,
required String moshafId,
}) async {
return _localDataSource.getDownloadedSurahByReciterAndRiwayah(
moshafId: moshafId,
reciterId: reciterId,
);
}
}

final reciteImplProvider = FutureProvider<ReciteImpl>((ref) async {
Expand Down
29 changes: 29 additions & 0 deletions lib/src/domain/error/recite_exception.dart
Original file line number Diff line number Diff line change
Expand Up @@ -96,3 +96,32 @@ class FetchFavoriteRecitersException implements Exception {
final String message;
FetchFavoriteRecitersException(this.message);
}

class FetchAudioFileFailedException extends ReciterException {
FetchAudioFileFailedException(String message)
: super('Error occurred while fetching audio file: $message', 'FETCH_AUDIO_FILE_ERROR');
}

class CheckSurahExistenceException extends ReciterException {
CheckSurahExistenceException(String message)
: super('Error occurred while saving audio file: $message', 'CHECK_SURAH_EXISTENCE_ERROR');
}

class SaveAudioFileException extends ReciterException {
SaveAudioFileException(String message)
: super('Error occurred while saving audio file: $message', 'SAVE_AUDIO_FILE_ERROR');
}

class FetchAudioFileException extends ReciterException {
FetchAudioFileException(String message)
: super('Error occurred while fetching audio file: $message', 'FETCH_AUDIO_FILE_ERROR');
}

class AudioFileNotFoundInCacheException extends ReciterException {
AudioFileNotFoundInCacheException() : super('Audio file not found in cache', 'AUDIO_FILE_NOT_FOUND_IN_CACHE_ERROR');
}

class FetchLocalAudioFileException extends ReciterException {
FetchLocalAudioFileException(String message)
: super('Audio file not found in local $message', 'AUDIO_FILE_NOT_FOUND_LOCAL_ERROR');
}
49 changes: 49 additions & 0 deletions lib/src/domain/model/quran/audio_file_model.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import 'package:dart_mappable/dart_mappable.dart';

part 'audio_file_model.mapper.dart';

@MappableClass()
class AudioFileModel {
final String reciterId;
final String moshafId;
final String surahId;
final String url;

AudioFileModel(this.reciterId, this.moshafId, this.surahId, this.url);

String get filePath => '$reciterId/$moshafId/$surahId.mp3';

factory AudioFileModel.fromJson(Map<String, dynamic> map) => _ensureContainer.fromMap<AudioFileModel>(map);

factory AudioFileModel.fromString(String json) => _ensureContainer.fromJson<AudioFileModel>(json);

Map<String, dynamic> toJson() {
return _ensureContainer.toMap(this);
}

@override
String toString() {
return _ensureContainer.toJson(this);
}

@override
bool operator ==(Object other) {
return identical(this, other) || (runtimeType == other.runtimeType && _ensureContainer.isEqual(this, other));
}

@override
int get hashCode {
return _ensureContainer.hash(this);
}

AudioFileModelCopyWith<AudioFileModel, AudioFileModel, AudioFileModel> get copyWith {
return _AudioFileModelCopyWithImpl(this, $identity, $identity);
}

static final MapperContainer _ensureContainer = () {
AudioFileModelMapper.ensureInitialized();
return MapperContainer.globals;
}();

static AudioFileModelMapper ensureInitialized() => AudioFileModelMapper.ensureInitialized();
}
Loading
Loading