@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.
Files changed (131) hide show
  1. package/.bundle/config +2 -0
  2. package/.eslintrc.js +4 -0
  3. package/.prettierrc.js +7 -0
  4. package/.watchmanconfig +1 -0
  5. package/App.tsx +23 -0
  6. package/Gemfile +9 -0
  7. package/Gemfile.lock +105 -0
  8. package/MIGRATION.md +163 -0
  9. package/README.md +210 -0
  10. package/ReactotronConfig.js +7 -0
  11. package/__tests__/App.test.tsx +17 -0
  12. package/android/app/build.gradle +118 -0
  13. package/android/app/debug.keystore +0 -0
  14. package/android/app/proguard-rules.pro +10 -0
  15. package/android/app/src/debug/AndroidManifest.xml +9 -0
  16. package/android/app/src/main/AndroidManifest.xml +25 -0
  17. package/android/app/src/main/assets/fonts/Cairo.ttf +0 -0
  18. package/android/app/src/main/assets/fonts/QCF_BSML.ttf +0 -0
  19. package/android/app/src/main/assets/fonts/QCF_P001.ttf +0 -0
  20. package/android/app/src/main/java/com/quranapp/MainActivity.kt +22 -0
  21. package/android/app/src/main/java/com/quranapp/MainApplication.kt +43 -0
  22. package/android/app/src/main/res/drawable/rn_edit_text_material.xml +37 -0
  23. package/android/app/src/main/res/mipmap-hdpi/ic_launcher.png +0 -0
  24. package/android/app/src/main/res/mipmap-hdpi/ic_launcher_round.png +0 -0
  25. package/android/app/src/main/res/mipmap-mdpi/ic_launcher.png +0 -0
  26. package/android/app/src/main/res/mipmap-mdpi/ic_launcher_round.png +0 -0
  27. package/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png +0 -0
  28. package/android/app/src/main/res/mipmap-xhdpi/ic_launcher_round.png +0 -0
  29. package/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png +0 -0
  30. package/android/app/src/main/res/mipmap-xxhdpi/ic_launcher_round.png +0 -0
  31. package/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png +0 -0
  32. package/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.png +0 -0
  33. package/android/app/src/main/res/values/strings.xml +3 -0
  34. package/android/app/src/main/res/values/styles.xml +9 -0
  35. package/android/build.gradle +21 -0
  36. package/android/gradle/wrapper/gradle-wrapper.jar +0 -0
  37. package/android/gradle/wrapper/gradle-wrapper.properties +7 -0
  38. package/android/gradle.properties +41 -0
  39. package/android/gradlew +249 -0
  40. package/android/gradlew.bat +92 -0
  41. package/android/link-assets-manifest.json +17 -0
  42. package/android/settings.gradle +4 -0
  43. package/app.json +4 -0
  44. package/babel.config.js +3 -0
  45. package/index.js +11 -0
  46. package/ios/.xcode.env +11 -0
  47. package/ios/Podfile +40 -0
  48. package/ios/Podfile.lock +1460 -0
  49. package/ios/QuranApp/AppDelegate.h +6 -0
  50. package/ios/QuranApp/AppDelegate.mm +31 -0
  51. package/ios/QuranApp/Images.xcassets/AppIcon.appiconset/Contents.json +53 -0
  52. package/ios/QuranApp/Images.xcassets/Contents.json +6 -0
  53. package/ios/QuranApp/Info.plist +57 -0
  54. package/ios/QuranApp/LaunchScreen.storyboard +47 -0
  55. package/ios/QuranApp/PrivacyInfo.xcprivacy +38 -0
  56. package/ios/QuranApp/main.m +10 -0
  57. package/ios/QuranApp.xcodeproj/project.pbxproj +729 -0
  58. package/ios/QuranApp.xcodeproj/xcshareddata/xcschemes/QuranApp.xcscheme +88 -0
  59. package/ios/QuranApp.xcworkspace/contents.xcworkspacedata +10 -0
  60. package/ios/QuranAppTests/Info.plist +24 -0
  61. package/ios/QuranAppTests/QuranAppTests.m +66 -0
  62. package/ios/link-assets-manifest.json +17 -0
  63. package/jest.config.js +3 -0
  64. package/metro.config.js +11 -0
  65. package/package.json +54 -0
  66. package/react-native.config.js +7 -0
  67. package/screenshots/1.png +0 -0
  68. package/screenshots/2.png +0 -0
  69. package/screenshots/3.png +0 -0
  70. package/screenshots/4.png +0 -0
  71. package/screenshots/5.png +0 -0
  72. package/src/assets/fonts/Cairo.ttf +0 -0
  73. package/src/assets/fonts/QCF_BSML.ttf +0 -0
  74. package/src/assets/fonts/QCF_P001.ttf +0 -0
  75. package/src/assets/images/bookmark.png +0 -0
  76. package/src/assets/images/close.png +0 -0
  77. package/src/assets/images/copy.png +0 -0
  78. package/src/assets/images/down-chevron.png +0 -0
  79. package/src/assets/images/mushafFrame.png +0 -0
  80. package/src/assets/images/pause.png +0 -0
  81. package/src/assets/images/play-button.png +0 -0
  82. package/src/assets/images/play.svg +16 -0
  83. package/src/assets/images/playNext.png +0 -0
  84. package/src/assets/images/surahNameFrame.png +0 -0
  85. package/src/common/chapters.ts +1346 -0
  86. package/src/common/constants.ts +27 -0
  87. package/src/common/images.ts +13 -0
  88. package/src/common/index.ts +8 -0
  89. package/src/common/juzs.ts +411 -0
  90. package/src/common/priorityPages.ts +46 -0
  91. package/src/common/themes.ts +7 -0
  92. package/src/components/index.ts +3 -0
  93. package/src/components/lists/index.ts +3 -0
  94. package/src/components/lists/pageVersesList.tsx +220 -0
  95. package/src/components/lists/verseLinesWordsList.tsx +90 -0
  96. package/src/components/modals/index.ts +3 -0
  97. package/src/components/modals/optionsModal.tsx +126 -0
  98. package/src/components/modals/recitersModal.tsx +118 -0
  99. package/src/components/sections/audioPlayer.tsx +185 -0
  100. package/src/components/sections/audioPlayerControls.tsx +100 -0
  101. package/src/components/sections/index.ts +4 -0
  102. package/src/components/sections/loader.tsx +33 -0
  103. package/src/helpers/index.ts +1 -0
  104. package/src/helpers/quranHelpers.tsx +17 -0
  105. package/src/hooks/apis/index.ts +10 -0
  106. package/src/hooks/apis/useGetChapterAudio.ts +111 -0
  107. package/src/hooks/apis/useGetChapterByPage.ts +166 -0
  108. package/src/hooks/apis/useGetChapterLookup.ts +31 -0
  109. package/src/hooks/apis/useGetReciters.ts +44 -0
  110. package/src/hooks/controllers/index.ts +25 -0
  111. package/src/hooks/controllers/useAudioPlayerController.tsx +63 -0
  112. package/src/hooks/controllers/useOptionsModalController.ts +99 -0
  113. package/src/hooks/controllers/usePageFontFileController.ts +255 -0
  114. package/src/hooks/controllers/usePageLineController.ts +108 -0
  115. package/src/hooks/helpers/index.ts +6 -0
  116. package/src/hooks/helpers/useQuranFontPreloader.ts +225 -0
  117. package/src/hooks/index.ts +3 -0
  118. package/src/index.ts +5 -0
  119. package/src/layouts/bismillahText.tsx +18 -0
  120. package/src/layouts/index.ts +4 -0
  121. package/src/layouts/quranChapterHeader.tsx +49 -0
  122. package/src/layouts/quranPageLayout.tsx +178 -0
  123. package/src/types/global.d.ts +7 -0
  124. package/src/types/index.ts +212 -0
  125. package/src/utils/axiosInstance.ts +7 -0
  126. package/src/utils/fileHandlers.ts +96 -0
  127. package/src/utils/handleBeforeAndAfterCurrentVerse.ts +41 -0
  128. package/src/utils/index.ts +6 -0
  129. package/src/utils/matrics.ts +37 -0
  130. package/tsconfig.json +3 -0
  131. 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;
@@ -0,0 +1,6 @@
1
+ export { useQuranFontPreloader } from './useQuranFontPreloader';
2
+ export type {
3
+ UseQuranFontPreloaderOptions,
4
+ UseQuranFontPreloaderResult,
5
+ PreloadProgress
6
+ } from './useQuranFontPreloader';