@moustafahelmi/react-native-quran-app 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.bundle/config +2 -0
- package/.eslintrc.js +4 -0
- package/.prettierrc.js +7 -0
- package/.watchmanconfig +1 -0
- package/App.tsx +23 -0
- package/Gemfile +9 -0
- package/Gemfile.lock +105 -0
- package/MIGRATION.md +163 -0
- package/README.md +210 -0
- package/ReactotronConfig.js +7 -0
- package/__tests__/App.test.tsx +17 -0
- package/android/app/build.gradle +118 -0
- package/android/app/debug.keystore +0 -0
- package/android/app/proguard-rules.pro +10 -0
- package/android/app/src/debug/AndroidManifest.xml +9 -0
- package/android/app/src/main/AndroidManifest.xml +25 -0
- package/android/app/src/main/assets/fonts/Cairo.ttf +0 -0
- package/android/app/src/main/assets/fonts/QCF_BSML.ttf +0 -0
- package/android/app/src/main/assets/fonts/QCF_P001.ttf +0 -0
- package/android/app/src/main/java/com/quranapp/MainActivity.kt +22 -0
- package/android/app/src/main/java/com/quranapp/MainApplication.kt +43 -0
- package/android/app/src/main/res/drawable/rn_edit_text_material.xml +37 -0
- package/android/app/src/main/res/mipmap-hdpi/ic_launcher.png +0 -0
- package/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png +0 -0
- package/android/app/src/main/res/mipmap-mdpi/ic_launcher.png +0 -0
- package/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png +0 -0
- package/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png +0 -0
- package/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png +0 -0
- package/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png +0 -0
- package/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png +0 -0
- package/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png +0 -0
- package/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png +0 -0
- package/android/app/src/main/res/values/strings.xml +3 -0
- package/android/app/src/main/res/values/styles.xml +9 -0
- package/android/build.gradle +21 -0
- package/android/gradle/wrapper/gradle-wrapper.jar +0 -0
- package/android/gradle/wrapper/gradle-wrapper.properties +7 -0
- package/android/gradle.properties +41 -0
- package/android/gradlew +249 -0
- package/android/gradlew.bat +92 -0
- package/android/link-assets-manifest.json +17 -0
- package/android/settings.gradle +4 -0
- package/app.json +4 -0
- package/babel.config.js +3 -0
- package/index.js +11 -0
- package/ios/.xcode.env +11 -0
- package/ios/Podfile +40 -0
- package/ios/Podfile.lock +1460 -0
- package/ios/QuranApp/AppDelegate.h +6 -0
- package/ios/QuranApp/AppDelegate.mm +31 -0
- package/ios/QuranApp/Images.xcassets/AppIcon.appiconset/Contents.json +53 -0
- package/ios/QuranApp/Images.xcassets/Contents.json +6 -0
- package/ios/QuranApp/Info.plist +57 -0
- package/ios/QuranApp/LaunchScreen.storyboard +47 -0
- package/ios/QuranApp/PrivacyInfo.xcprivacy +38 -0
- package/ios/QuranApp/main.m +10 -0
- package/ios/QuranApp.xcodeproj/project.pbxproj +729 -0
- package/ios/QuranApp.xcodeproj/xcshareddata/xcschemes/QuranApp.xcscheme +88 -0
- package/ios/QuranApp.xcworkspace/contents.xcworkspacedata +10 -0
- package/ios/QuranAppTests/Info.plist +24 -0
- package/ios/QuranAppTests/QuranAppTests.m +66 -0
- package/ios/link-assets-manifest.json +17 -0
- package/jest.config.js +3 -0
- package/metro.config.js +11 -0
- package/package.json +54 -0
- package/react-native.config.js +7 -0
- package/screenshots/1.png +0 -0
- package/screenshots/2.png +0 -0
- package/screenshots/3.png +0 -0
- package/screenshots/4.png +0 -0
- package/screenshots/5.png +0 -0
- package/src/assets/fonts/Cairo.ttf +0 -0
- package/src/assets/fonts/QCF_BSML.ttf +0 -0
- package/src/assets/fonts/QCF_P001.ttf +0 -0
- package/src/assets/images/bookmark.png +0 -0
- package/src/assets/images/close.png +0 -0
- package/src/assets/images/copy.png +0 -0
- package/src/assets/images/down-chevron.png +0 -0
- package/src/assets/images/mushafFrame.png +0 -0
- package/src/assets/images/pause.png +0 -0
- package/src/assets/images/play-button.png +0 -0
- package/src/assets/images/play.svg +16 -0
- package/src/assets/images/playNext.png +0 -0
- package/src/assets/images/surahNameFrame.png +0 -0
- package/src/common/chapters.ts +1346 -0
- package/src/common/constants.ts +27 -0
- package/src/common/images.ts +13 -0
- package/src/common/index.ts +8 -0
- package/src/common/juzs.ts +411 -0
- package/src/common/priorityPages.ts +46 -0
- package/src/common/themes.ts +7 -0
- package/src/components/index.ts +3 -0
- package/src/components/lists/index.ts +3 -0
- package/src/components/lists/pageVersesList.tsx +220 -0
- package/src/components/lists/verseLinesWordsList.tsx +90 -0
- package/src/components/modals/index.ts +3 -0
- package/src/components/modals/optionsModal.tsx +126 -0
- package/src/components/modals/recitersModal.tsx +118 -0
- package/src/components/sections/audioPlayer.tsx +185 -0
- package/src/components/sections/audioPlayerControls.tsx +100 -0
- package/src/components/sections/index.ts +4 -0
- package/src/components/sections/loader.tsx +33 -0
- package/src/helpers/index.ts +1 -0
- package/src/helpers/quranHelpers.tsx +17 -0
- package/src/hooks/apis/index.ts +10 -0
- package/src/hooks/apis/useGetChapterAudio.ts +111 -0
- package/src/hooks/apis/useGetChapterByPage.ts +166 -0
- package/src/hooks/apis/useGetChapterLookup.ts +31 -0
- package/src/hooks/apis/useGetReciters.ts +44 -0
- package/src/hooks/controllers/index.ts +25 -0
- package/src/hooks/controllers/useAudioPlayerController.tsx +63 -0
- package/src/hooks/controllers/useOptionsModalController.ts +99 -0
- package/src/hooks/controllers/usePageFontFileController.ts +255 -0
- package/src/hooks/controllers/usePageLineController.ts +108 -0
- package/src/hooks/helpers/index.ts +6 -0
- package/src/hooks/helpers/useQuranFontPreloader.ts +225 -0
- package/src/hooks/index.ts +3 -0
- package/src/index.ts +5 -0
- package/src/layouts/bismillahText.tsx +18 -0
- package/src/layouts/index.ts +4 -0
- package/src/layouts/quranChapterHeader.tsx +49 -0
- package/src/layouts/quranPageLayout.tsx +178 -0
- package/src/types/global.d.ts +7 -0
- package/src/types/index.ts +212 -0
- package/src/utils/axiosInstance.ts +7 -0
- package/src/utils/fileHandlers.ts +96 -0
- package/src/utils/handleBeforeAndAfterCurrentVerse.ts +41 -0
- package/src/utils/index.ts +6 -0
- package/src/utils/matrics.ts +37 -0
- package/tsconfig.json +3 -0
- package/videos/1.gif +0 -0
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import {useEffect, useState} from 'react';
|
|
2
|
+
import {QURAN_API} from '../../common';
|
|
3
|
+
import {axiosInstance} from '../../utils';
|
|
4
|
+
import {IReciter, QuranTypesEnums} from '../../types';
|
|
5
|
+
import useGetChapterAudio from './useGetChapterAudio';
|
|
6
|
+
import {I18nManager} from 'react-native';
|
|
7
|
+
|
|
8
|
+
const useGetReciters = ({
|
|
9
|
+
chapterId,
|
|
10
|
+
type,
|
|
11
|
+
}: {
|
|
12
|
+
chapterId: number;
|
|
13
|
+
type: QuranTypesEnums;
|
|
14
|
+
}) => {
|
|
15
|
+
const [allReciters, setAllReciters] = useState<IReciter[]>([]);
|
|
16
|
+
const {getChapterAudionUrl} = useGetChapterAudio();
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
getReciters();
|
|
19
|
+
}, []);
|
|
20
|
+
|
|
21
|
+
const getReciters = async () => {
|
|
22
|
+
const queryParams = {
|
|
23
|
+
locale: I18nManager.isRTL ? 'ar' : 'en',
|
|
24
|
+
// fields: undefined,
|
|
25
|
+
};
|
|
26
|
+
const queryString = Object.entries(queryParams)
|
|
27
|
+
.map(
|
|
28
|
+
([key, value]) =>
|
|
29
|
+
`${encodeURIComponent(key)}=${encodeURIComponent(value as any)}`,
|
|
30
|
+
)
|
|
31
|
+
.join('&');
|
|
32
|
+
try {
|
|
33
|
+
const url = `${QURAN_API}/audio/reciters?${queryString}`;
|
|
34
|
+
const response = await axiosInstance.get(url);
|
|
35
|
+
const reciters: IReciter[] = response.data?.reciters;
|
|
36
|
+
if (type === QuranTypesEnums.chapter)
|
|
37
|
+
getChapterAudionUrl({reciterId: reciters[0]?.id, chapterId});
|
|
38
|
+
setAllReciters(reciters);
|
|
39
|
+
} catch (error) {}
|
|
40
|
+
};
|
|
41
|
+
return {allReciters};
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
export default useGetReciters;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import usePageLineController from './usePageLineController';
|
|
2
|
+
import usePageFontFileController, {
|
|
3
|
+
downoladThePageFont,
|
|
4
|
+
downloadMultiplePageFonts,
|
|
5
|
+
isFontCached,
|
|
6
|
+
checkMultipleFontCache,
|
|
7
|
+
getFontCacheStats,
|
|
8
|
+
clearFontCache,
|
|
9
|
+
} from './usePageFontFileController';
|
|
10
|
+
import useAudioPlayerController from './useAudioPlayerController';
|
|
11
|
+
import useOptionsModalController from './useOptionsModalController';
|
|
12
|
+
|
|
13
|
+
export {
|
|
14
|
+
usePageLineController,
|
|
15
|
+
usePageFontFileController,
|
|
16
|
+
useAudioPlayerController,
|
|
17
|
+
useOptionsModalController,
|
|
18
|
+
// Font management functions
|
|
19
|
+
downoladThePageFont,
|
|
20
|
+
downloadMultiplePageFonts,
|
|
21
|
+
isFontCached,
|
|
22
|
+
checkMultipleFontCache,
|
|
23
|
+
getFontCacheStats,
|
|
24
|
+
clearFontCache,
|
|
25
|
+
};
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import {ActivityIndicator, Image, StyleSheet} from 'react-native';
|
|
2
|
+
import TrackPlayer from 'react-native-track-player';
|
|
3
|
+
import {IMAGES} from '../../common';
|
|
4
|
+
import {usePlaybackState, State} from 'react-native-track-player';
|
|
5
|
+
import {IModalRef} from '../../types';
|
|
6
|
+
import {useRef} from 'react';
|
|
7
|
+
|
|
8
|
+
const useAudioPlayerController = () => {
|
|
9
|
+
const recitersModalRef = useRef<IModalRef>();
|
|
10
|
+
|
|
11
|
+
const playbackState = usePlaybackState();
|
|
12
|
+
const changeHandler = (val: number) => {
|
|
13
|
+
TrackPlayer.seekTo(val);
|
|
14
|
+
};
|
|
15
|
+
const renderplayPauseBtn = () => {
|
|
16
|
+
switch (playbackState?.state) {
|
|
17
|
+
case State?.Playing as any:
|
|
18
|
+
return <Image source={IMAGES.pause} style={styles.img} />;
|
|
19
|
+
case State?.Ready as any:
|
|
20
|
+
return <Image source={IMAGES.playIcon} style={styles.img} />;
|
|
21
|
+
case State.Paused as any:
|
|
22
|
+
return <Image source={IMAGES.playIcon} style={styles.img} />;
|
|
23
|
+
default:
|
|
24
|
+
return <ActivityIndicator size={30} color="#2B3F4B" />;
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
const onPlayPause = async () => {
|
|
28
|
+
if (playbackState?.state === State.Playing) {
|
|
29
|
+
TrackPlayer.pause();
|
|
30
|
+
} else if (
|
|
31
|
+
playbackState?.state === State.Paused ||
|
|
32
|
+
playbackState?.state === State.Ready
|
|
33
|
+
) {
|
|
34
|
+
TrackPlayer.play();
|
|
35
|
+
} else {
|
|
36
|
+
TrackPlayer.reset();
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
const openReciterModal = () => {
|
|
40
|
+
recitersModalRef?.current?.openModal();
|
|
41
|
+
};
|
|
42
|
+
const formatTime = (secs: number) => {
|
|
43
|
+
let hours = Math.floor(secs / 3600);
|
|
44
|
+
let minutes = Math.floor((secs % 3600) / 60);
|
|
45
|
+
let seconds: any = Math.ceil(secs % 60);
|
|
46
|
+
if (seconds < 10) seconds = `0${seconds}`;
|
|
47
|
+
return `${hours}:${minutes}:${seconds}`;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
return {
|
|
51
|
+
changeHandler,
|
|
52
|
+
renderplayPauseBtn,
|
|
53
|
+
onPlayPause,
|
|
54
|
+
openReciterModal,
|
|
55
|
+
recitersModalRef,
|
|
56
|
+
formatTime,
|
|
57
|
+
};
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
export default useAudioPlayerController;
|
|
61
|
+
const styles = StyleSheet.create({
|
|
62
|
+
img: {width: 20, height: 20},
|
|
63
|
+
});
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import Clipboard from '@react-native-clipboard/clipboard';
|
|
2
|
+
import {Dimensions, I18nManager} from 'react-native';
|
|
3
|
+
import {IReciter, ISelectedVerseLocation, ISurahVerse} from '../../types';
|
|
4
|
+
import {useGetChapterAudio} from '../apis';
|
|
5
|
+
import TrackPlayer from 'react-native-track-player';
|
|
6
|
+
|
|
7
|
+
const OPTION_CONTAINER_WIDTH = 130;
|
|
8
|
+
const OPTION_CONTAINER_HEIGHT = 50;
|
|
9
|
+
const {width} = Dimensions.get('screen');
|
|
10
|
+
interface IProps {
|
|
11
|
+
selectedVerseLocation: ISelectedVerseLocation | undefined;
|
|
12
|
+
selectedVerse: ISurahVerse;
|
|
13
|
+
selectedReciter: IReciter | undefined;
|
|
14
|
+
handlePlayPress: () => void;
|
|
15
|
+
setIsVisible: (value: boolean) => void;
|
|
16
|
+
seSelectedVerse: (value: ISurahVerse) => void;
|
|
17
|
+
}
|
|
18
|
+
const useOptionsModalController = ({
|
|
19
|
+
selectedVerseLocation,
|
|
20
|
+
selectedVerse,
|
|
21
|
+
setIsVisible,
|
|
22
|
+
seSelectedVerse,
|
|
23
|
+
}: IProps) => {
|
|
24
|
+
const {getVerseAudio, isVersePositionLoading, getChapterAudionUrl} =
|
|
25
|
+
useGetChapterAudio();
|
|
26
|
+
|
|
27
|
+
const onPlayerPress = async ({
|
|
28
|
+
reciterId,
|
|
29
|
+
handlePlayPress,
|
|
30
|
+
verse_key,
|
|
31
|
+
chapterId,
|
|
32
|
+
autoCompleteAudioAfterPlayingVerse,
|
|
33
|
+
}: {
|
|
34
|
+
reciterId: number;
|
|
35
|
+
verse_key: string;
|
|
36
|
+
handlePlayPress?: () => void;
|
|
37
|
+
chapterId: number;
|
|
38
|
+
autoCompleteAudioAfterPlayingVerse?: boolean;
|
|
39
|
+
}) => {
|
|
40
|
+
const trackData = await TrackPlayer.getActiveTrack();
|
|
41
|
+
if (trackData?.chapter_id != chapterId) {
|
|
42
|
+
await TrackPlayer.pause();
|
|
43
|
+
await getChapterAudionUrl({
|
|
44
|
+
reciterId: reciterId,
|
|
45
|
+
chapterId,
|
|
46
|
+
verse_key: selectedVerse?.verse_key,
|
|
47
|
+
autoCompleteAudioAfterPlayingVerse,
|
|
48
|
+
callback: handlePlayPress,
|
|
49
|
+
});
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
getVerseAudio(
|
|
53
|
+
reciterId as number,
|
|
54
|
+
verse_key,
|
|
55
|
+
handlePlayPress,
|
|
56
|
+
autoCompleteAudioAfterPlayingVerse,
|
|
57
|
+
);
|
|
58
|
+
};
|
|
59
|
+
const copyVerseToClipBoard = () => {
|
|
60
|
+
Clipboard?.setString(selectedVerse?.text_uthmani);
|
|
61
|
+
console.log('copied');
|
|
62
|
+
};
|
|
63
|
+
const _renderOptionContainerPosition = ():
|
|
64
|
+
| {
|
|
65
|
+
translateY: number;
|
|
66
|
+
translateX: number;
|
|
67
|
+
}
|
|
68
|
+
| undefined => {
|
|
69
|
+
if (selectedVerseLocation)
|
|
70
|
+
return {
|
|
71
|
+
translateX:
|
|
72
|
+
selectedVerseLocation?.itemLocationX + OPTION_CONTAINER_WIDTH < width
|
|
73
|
+
? I18nManager.isRTL
|
|
74
|
+
? -selectedVerseLocation?.itemLocationX
|
|
75
|
+
: selectedVerseLocation?.itemLocationX
|
|
76
|
+
: I18nManager.isRTL
|
|
77
|
+
? -(selectedVerseLocation?.itemLocationX - 100)
|
|
78
|
+
: selectedVerseLocation?.itemLocationX - 100,
|
|
79
|
+
translateY:
|
|
80
|
+
selectedVerseLocation?.itemLocationY + OPTION_CONTAINER_HEIGHT < width
|
|
81
|
+
? selectedVerseLocation?.itemLocationY
|
|
82
|
+
: selectedVerseLocation?.itemLocationY - 100,
|
|
83
|
+
};
|
|
84
|
+
};
|
|
85
|
+
const onRequestClose = () => {
|
|
86
|
+
setIsVisible(false);
|
|
87
|
+
seSelectedVerse({} as ISurahVerse);
|
|
88
|
+
};
|
|
89
|
+
return {
|
|
90
|
+
_renderOptionContainerPosition,
|
|
91
|
+
OPTION_CONTAINER_WIDTH,
|
|
92
|
+
copyVerseToClipBoard,
|
|
93
|
+
onPlayerPress,
|
|
94
|
+
onRequestClose,
|
|
95
|
+
isVersePositionLoading,
|
|
96
|
+
};
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
export default useOptionsModalController;
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import RNFS from 'react-native-fs';
|
|
2
|
+
import {loadFont} from 'react-native-dynamic-fonts';
|
|
3
|
+
import {isFileExists} from '../../utils';
|
|
4
|
+
|
|
5
|
+
const _renderPageNumber = (pageNumber: number) => {
|
|
6
|
+
let pageNumberFormat = '';
|
|
7
|
+
if (pageNumber < 10) pageNumberFormat = `00${pageNumber}`;
|
|
8
|
+
else if (pageNumber >= 10 && pageNumber < 100)
|
|
9
|
+
pageNumberFormat = `0${pageNumber}`;
|
|
10
|
+
else pageNumberFormat = `${pageNumber}`;
|
|
11
|
+
return pageNumberFormat;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const _fontFileFormatGenerator = (currentPageNumber: number) =>
|
|
15
|
+
`QCF_P${_renderPageNumber(currentPageNumber)}`;
|
|
16
|
+
const _filePathFormatGenerator = (targetFont: string) =>
|
|
17
|
+
RNFS.DocumentDirectoryPath + `/${targetFont}.ttf`;
|
|
18
|
+
|
|
19
|
+
const isFontFileExistsBefore = async (currentPageNumber: number) => {
|
|
20
|
+
const targetFont = _fontFileFormatGenerator(currentPageNumber);
|
|
21
|
+
const fontFilePath = _filePathFormatGenerator(targetFont);
|
|
22
|
+
return await isFileExists(fontFilePath);
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
export const downoladThePageFont = async (
|
|
26
|
+
currentPageNumber: number,
|
|
27
|
+
onFontLoaded: () => void,
|
|
28
|
+
quranFontApi: string,
|
|
29
|
+
) => {
|
|
30
|
+
if (!quranFontApi) throw new Error('you must add fonts link');
|
|
31
|
+
|
|
32
|
+
const targetFont = _fontFileFormatGenerator(currentPageNumber);
|
|
33
|
+
const url = `${quranFontApi}${targetFont}.TTF`;
|
|
34
|
+
const filePath = _filePathFormatGenerator(targetFont);
|
|
35
|
+
const ifFileSavedBefore = await isFileExists(filePath);
|
|
36
|
+
if (ifFileSavedBefore) {
|
|
37
|
+
loadFontFamily(filePath, targetFont, onFontLoaded);
|
|
38
|
+
} else
|
|
39
|
+
return RNFS.downloadFile({
|
|
40
|
+
fromUrl: url,
|
|
41
|
+
toFile: filePath,
|
|
42
|
+
background: true, // Enable downloading in the background (iOS only)
|
|
43
|
+
discretionary: true, // Allow the OS to control the timing and speed (iOS only)
|
|
44
|
+
progress: res => {
|
|
45
|
+
// Handle download progress updates if needed
|
|
46
|
+
const progress = (res.bytesWritten / res.contentLength) * 100;
|
|
47
|
+
},
|
|
48
|
+
})
|
|
49
|
+
.promise.then(res => {
|
|
50
|
+
console.log('res' + `${targetFont}`, JSON.stringify(res));
|
|
51
|
+
return loadFontFamily(filePath, targetFont, onFontLoaded);
|
|
52
|
+
})
|
|
53
|
+
.catch(err => {
|
|
54
|
+
console.log('Download error:', err);
|
|
55
|
+
return err;
|
|
56
|
+
});
|
|
57
|
+
};
|
|
58
|
+
const loadFontFamily = async (
|
|
59
|
+
fontFilePath: string,
|
|
60
|
+
targetFont: string,
|
|
61
|
+
onFontLoaded: () => void,
|
|
62
|
+
) => {
|
|
63
|
+
const base64 = await RNFS.readFile(fontFilePath, {encoding: 'base64'});
|
|
64
|
+
return loadFont(targetFont, base64, 'ttf').then((name: string) => {
|
|
65
|
+
console.log('Loaded font successfully. Font name is: ', name);
|
|
66
|
+
onFontLoaded();
|
|
67
|
+
return name;
|
|
68
|
+
});
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Download multiple Quran page fonts in parallel
|
|
73
|
+
* @param pageNumbers - Array of page numbers to download (1-604)
|
|
74
|
+
* @param quranFontApi - Base URL for font downloads (e.g., "https://raw.githubusercontent.com/...")
|
|
75
|
+
* @param onProgress - Optional callback for progress updates (current, total)
|
|
76
|
+
* @param concurrentDownloads - Max parallel downloads (default: 5)
|
|
77
|
+
* @returns Promise with download results for each page
|
|
78
|
+
*/
|
|
79
|
+
export const downloadMultiplePageFonts = async (
|
|
80
|
+
pageNumbers: number[],
|
|
81
|
+
quranFontApi: string,
|
|
82
|
+
onProgress?: (current: number, total: number, page: number) => void,
|
|
83
|
+
concurrentDownloads: number = 5,
|
|
84
|
+
): Promise<Array<{success: boolean; page: number; cached: boolean; error?: any}>> => {
|
|
85
|
+
if (!quranFontApi) throw new Error('you must add fonts link');
|
|
86
|
+
|
|
87
|
+
console.log(`📥 Starting batch download of ${pageNumbers.length} Quran fonts...`);
|
|
88
|
+
|
|
89
|
+
const total = pageNumbers.length;
|
|
90
|
+
let completed = 0;
|
|
91
|
+
const results: Array<{success: boolean; page: number; cached: boolean; error?: any}> = [];
|
|
92
|
+
|
|
93
|
+
// Process in batches to avoid overwhelming the network
|
|
94
|
+
for (let i = 0; i < pageNumbers.length; i += concurrentDownloads) {
|
|
95
|
+
const batch = pageNumbers.slice(i, i + concurrentDownloads);
|
|
96
|
+
|
|
97
|
+
const batchResults = await Promise.all(
|
|
98
|
+
batch.map(async (pageNumber) => {
|
|
99
|
+
try {
|
|
100
|
+
const targetFont = _fontFileFormatGenerator(pageNumber);
|
|
101
|
+
const url = `${quranFontApi}${targetFont}.TTF`;
|
|
102
|
+
const filePath = _filePathFormatGenerator(targetFont);
|
|
103
|
+
|
|
104
|
+
// Check if already cached
|
|
105
|
+
const ifFileSavedBefore = await isFileExists(filePath);
|
|
106
|
+
if (ifFileSavedBefore) {
|
|
107
|
+
completed++;
|
|
108
|
+
onProgress?.(completed, total, pageNumber);
|
|
109
|
+
return { success: true, page: pageNumber, cached: true };
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
// Download the font
|
|
113
|
+
await RNFS.downloadFile({
|
|
114
|
+
fromUrl: url,
|
|
115
|
+
toFile: filePath,
|
|
116
|
+
background: true,
|
|
117
|
+
discretionary: true,
|
|
118
|
+
progressInterval: 1000,
|
|
119
|
+
}).promise;
|
|
120
|
+
|
|
121
|
+
completed++;
|
|
122
|
+
onProgress?.(completed, total, pageNumber);
|
|
123
|
+
console.log(`✅ Downloaded font for page ${pageNumber} (${completed}/${total})`);
|
|
124
|
+
|
|
125
|
+
return { success: true, page: pageNumber, cached: false };
|
|
126
|
+
} catch (err) {
|
|
127
|
+
completed++;
|
|
128
|
+
onProgress?.(completed, total, pageNumber);
|
|
129
|
+
console.error(`❌ Failed to download page ${pageNumber}:`, err);
|
|
130
|
+
return { success: false, page: pageNumber, cached: false, error: err };
|
|
131
|
+
}
|
|
132
|
+
})
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
results.push(...batchResults);
|
|
136
|
+
|
|
137
|
+
// Small delay between batches to prevent overwhelming the system
|
|
138
|
+
if (i + concurrentDownloads < pageNumbers.length) {
|
|
139
|
+
await new Promise(resolve => setTimeout(resolve, 100));
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const successful = results.filter(r => r.success).length;
|
|
144
|
+
const cached = results.filter(r => r.cached).length;
|
|
145
|
+
console.log(`✨ Batch download complete: ${successful}/${total} fonts ready (${cached} from cache)`);
|
|
146
|
+
|
|
147
|
+
return results;
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Check if a font is already cached locally
|
|
152
|
+
* @param pageNumber - Page number to check (1-604)
|
|
153
|
+
* @returns Promise<boolean> - true if cached, false otherwise
|
|
154
|
+
*/
|
|
155
|
+
export const isFontCached = async (pageNumber: number): Promise<boolean> => {
|
|
156
|
+
const targetFont = _fontFileFormatGenerator(pageNumber);
|
|
157
|
+
const fontFilePath = _filePathFormatGenerator(targetFont);
|
|
158
|
+
return await isFileExists(fontFilePath);
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Check cache status for multiple pages
|
|
163
|
+
* @param pageNumbers - Array of page numbers to check
|
|
164
|
+
* @returns Promise with object mapping page numbers to cache status
|
|
165
|
+
*/
|
|
166
|
+
export const checkMultipleFontCache = async (
|
|
167
|
+
pageNumbers: number[]
|
|
168
|
+
): Promise<Record<number, boolean>> => {
|
|
169
|
+
const results: Record<number, boolean> = {};
|
|
170
|
+
|
|
171
|
+
await Promise.all(
|
|
172
|
+
pageNumbers.map(async (pageNumber) => {
|
|
173
|
+
results[pageNumber] = await isFontCached(pageNumber);
|
|
174
|
+
})
|
|
175
|
+
);
|
|
176
|
+
|
|
177
|
+
return results;
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Get cache statistics
|
|
182
|
+
* @returns Promise with cache info (total cached, size, etc.)
|
|
183
|
+
*/
|
|
184
|
+
export const getFontCacheStats = async (): Promise<{
|
|
185
|
+
totalCached: number;
|
|
186
|
+
cachedPages: number[];
|
|
187
|
+
totalSizeBytes: number;
|
|
188
|
+
}> => {
|
|
189
|
+
try {
|
|
190
|
+
const files = await RNFS.readDir(RNFS.DocumentDirectoryPath);
|
|
191
|
+
const fontFiles = files.filter(file => file.name.startsWith('QCF_P') && file.name.endsWith('.ttf'));
|
|
192
|
+
|
|
193
|
+
const cachedPages = fontFiles
|
|
194
|
+
.map(file => {
|
|
195
|
+
const match = file.name.match(/QCF_P(\d+)\.ttf/);
|
|
196
|
+
return match ? parseInt(match[1], 10) : null;
|
|
197
|
+
})
|
|
198
|
+
.filter(page => page !== null) as number[];
|
|
199
|
+
|
|
200
|
+
const totalSizeBytes = fontFiles.reduce((sum, file) => sum + (file.size || 0), 0);
|
|
201
|
+
|
|
202
|
+
return {
|
|
203
|
+
totalCached: fontFiles.length,
|
|
204
|
+
cachedPages: cachedPages.sort((a, b) => a - b),
|
|
205
|
+
totalSizeBytes,
|
|
206
|
+
};
|
|
207
|
+
} catch (error) {
|
|
208
|
+
console.error('Error getting cache stats:', error);
|
|
209
|
+
return {
|
|
210
|
+
totalCached: 0,
|
|
211
|
+
cachedPages: [],
|
|
212
|
+
totalSizeBytes: 0,
|
|
213
|
+
};
|
|
214
|
+
}
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Clear font cache (delete all downloaded fonts)
|
|
219
|
+
* @param pageNumbers - Optional array of specific pages to clear. If not provided, clears all.
|
|
220
|
+
* @returns Promise with number of fonts deleted
|
|
221
|
+
*/
|
|
222
|
+
export const clearFontCache = async (pageNumbers?: number[]): Promise<number> => {
|
|
223
|
+
try {
|
|
224
|
+
const files = await RNFS.readDir(RNFS.DocumentDirectoryPath);
|
|
225
|
+
let fontFiles = files.filter(file => file.name.startsWith('QCF_P') && file.name.endsWith('.ttf'));
|
|
226
|
+
|
|
227
|
+
// If specific pages provided, filter to only those
|
|
228
|
+
if (pageNumbers && pageNumbers.length > 0) {
|
|
229
|
+
const pageSet = new Set(pageNumbers.map(p => _fontFileFormatGenerator(p) + '.ttf'));
|
|
230
|
+
fontFiles = fontFiles.filter(file => pageSet.has(file.name));
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
await Promise.all(fontFiles.map(file => RNFS.unlink(file.path)));
|
|
234
|
+
|
|
235
|
+
console.log(`🗑️ Cleared ${fontFiles.length} font files from cache`);
|
|
236
|
+
return fontFiles.length;
|
|
237
|
+
} catch (error) {
|
|
238
|
+
console.error('Error clearing cache:', error);
|
|
239
|
+
return 0;
|
|
240
|
+
}
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
const usePageFontFileController = () => {
|
|
244
|
+
return {
|
|
245
|
+
downoladThePageFont,
|
|
246
|
+
downloadMultiplePageFonts,
|
|
247
|
+
isFontCached,
|
|
248
|
+
checkMultipleFontCache,
|
|
249
|
+
getFontCacheStats,
|
|
250
|
+
clearFontCache,
|
|
251
|
+
_fontFileFormatGenerator,
|
|
252
|
+
};
|
|
253
|
+
};
|
|
254
|
+
|
|
255
|
+
export default usePageFontFileController;
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import {useMemo} from 'react';
|
|
2
|
+
import {ILineNumber, ISurahVerse, IVerseWord} from '../../types';
|
|
3
|
+
|
|
4
|
+
const loopOnEachWord = (
|
|
5
|
+
verseWords: IVerseWord[],
|
|
6
|
+
lineNumbers: ILineNumber[],
|
|
7
|
+
verse: ISurahVerse,
|
|
8
|
+
) => {
|
|
9
|
+
for (let x = 0; x < verseWords.length; x++) {
|
|
10
|
+
const wordObj = verseWords[x];
|
|
11
|
+
const wordLineNumber = wordObj?.line_number;
|
|
12
|
+
const lineNumberIndex = lineNumbers.findIndex(
|
|
13
|
+
item => item?.lineNumber == wordLineNumber,
|
|
14
|
+
);
|
|
15
|
+
lineNumbers = addOrUpdatePageLineNumber(
|
|
16
|
+
lineNumberIndex,
|
|
17
|
+
lineNumbers,
|
|
18
|
+
wordLineNumber,
|
|
19
|
+
wordObj,
|
|
20
|
+
verse,
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
return lineNumbers;
|
|
24
|
+
};
|
|
25
|
+
const _renderWordObjectFormat = (word: IVerseWord) => ({
|
|
26
|
+
id: word?.id,
|
|
27
|
+
verse_key: word?.verse_key,
|
|
28
|
+
code_v1: word?.code_v1,
|
|
29
|
+
code_v2: word?.code_v2,
|
|
30
|
+
});
|
|
31
|
+
const _renderVerseObjectFormat = (verse: ISurahVerse) => ({
|
|
32
|
+
id: verse?.id,
|
|
33
|
+
verse_number: verse?.verse_number,
|
|
34
|
+
verse_key: verse?.verse_key,
|
|
35
|
+
chapter_id: verse?.chapter_id,
|
|
36
|
+
page_number: verse?.page_number,
|
|
37
|
+
text_uthmani: verse?.text_uthmani,
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
const addOrUpdatePageLineNumber = (
|
|
41
|
+
lineNumberIndex: number,
|
|
42
|
+
lineNumbers: ILineNumber[],
|
|
43
|
+
wordLineNumber: number,
|
|
44
|
+
wordObj: IVerseWord,
|
|
45
|
+
verse: ISurahVerse,
|
|
46
|
+
) => {
|
|
47
|
+
// to know if the first line property detected before or not
|
|
48
|
+
const isFirstLineAssignedBefore = lineNumbers?.some(
|
|
49
|
+
(item: ILineNumber) =>
|
|
50
|
+
item?.chapter_id == verse?.chapter_id && item?.isFirstLine,
|
|
51
|
+
);
|
|
52
|
+
if (lineNumberIndex === -1) {
|
|
53
|
+
const wordCodeV1 = wordObj?.code_v1;
|
|
54
|
+
lineNumbers.push({
|
|
55
|
+
lineNumber: wordLineNumber,
|
|
56
|
+
isFirstLine: isFirstLineAssignedBefore ? false : verse?.verse_number == 1,
|
|
57
|
+
chapter_id: verse?.chapter_id,
|
|
58
|
+
words: [
|
|
59
|
+
{
|
|
60
|
+
...(_renderWordObjectFormat(wordObj) as IVerseWord),
|
|
61
|
+
page_number: wordObj?.page_number,
|
|
62
|
+
verseData: _renderVerseObjectFormat(verse) as ISurahVerse,
|
|
63
|
+
char_type_name: wordObj?.char_type_name,
|
|
64
|
+
text: wordObj?.text_uthmani,
|
|
65
|
+
},
|
|
66
|
+
],
|
|
67
|
+
wordCodeV1,
|
|
68
|
+
page_number: wordObj?.page_number,
|
|
69
|
+
});
|
|
70
|
+
} else {
|
|
71
|
+
const wordCodeV1 = `${lineNumbers[lineNumberIndex]?.wordCodeV1}${wordObj?.code_v1}`;
|
|
72
|
+
lineNumbers[lineNumberIndex] = {
|
|
73
|
+
lineNumber: wordLineNumber,
|
|
74
|
+
chapter_id: verse?.chapter_id,
|
|
75
|
+
isFirstLine: lineNumbers[lineNumberIndex]?.isFirstLine,
|
|
76
|
+
words: [
|
|
77
|
+
...(lineNumbers[lineNumberIndex]?.words as any),
|
|
78
|
+
{
|
|
79
|
+
...(_renderWordObjectFormat(wordObj) as IVerseWord),
|
|
80
|
+
page_number: wordObj?.page_number,
|
|
81
|
+
char_type_name: wordObj?.char_type_name,
|
|
82
|
+
verseData: _renderVerseObjectFormat(verse) as ISurahVerse,
|
|
83
|
+
text: wordObj?.text_uthmani,
|
|
84
|
+
},
|
|
85
|
+
],
|
|
86
|
+
wordCodeV1,
|
|
87
|
+
page_number: wordObj?.page_number,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
return lineNumbers;
|
|
91
|
+
};
|
|
92
|
+
const _renderVersesNewForm = ({pageVerses}: {pageVerses: ISurahVerse[]}) => {
|
|
93
|
+
if (!pageVerses?.length) return;
|
|
94
|
+
let lineNumbers: {lineNumber: number; words: IVerseWord[]}[] = [];
|
|
95
|
+
// loop on each verse
|
|
96
|
+
for (let i = 0; i < pageVerses.length; i++) {
|
|
97
|
+
const verseWords = pageVerses[i].words;
|
|
98
|
+
lineNumbers = loopOnEachWord(verseWords, lineNumbers, pageVerses[i]);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return lineNumbers;
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const usePageLineController = () => {
|
|
105
|
+
return {_renderVersesNewForm};
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
export default usePageLineController;
|