From 3760951ec58b25bc0adadc88ce5bc371a10b304a Mon Sep 17 00:00:00 2001 From: Reza Hosseinzadeh Date: Mon, 4 May 2026 20:01:00 +0200 Subject: [PATCH 1/9] Translated using Weblate (Persian) Currently translated at 100.0% (33 of 33 strings) Translation: QuranApp/Quranic Topics Translate-URL: https://hosted.weblate.org/projects/QuranApp/quranic-topics/fa/ --- .../main/assets/verses/type0/fa/type0.json | 68 +++++++++---------- 1 file changed, 34 insertions(+), 34 deletions(-) diff --git a/app/src/main/assets/verses/type0/fa/type0.json b/app/src/main/assets/verses/type0/fa/type0.json index 1e71cc35c..b04cb8895 100644 --- a/app/src/main/assets/verses/type0/fa/type0.json +++ b/app/src/main/assets/verses/type0/fa/type0.json @@ -1,35 +1,35 @@ { - "1": "مقابل چشم‌زخم", - "2": "مقابل سحر", - "3": "اضطراب", - "4": "عصبانیت", - "5": "افسردگی", - "6": "اوقات سخت", - "7": "سختی پیدا کردن همسر مناسب", - "8": "تبعیض علیه شما", - "9": "شک", - "10": "روبرو شدن با سختی‌ها", - "11": "احساس حسادت", - "12": "برای فرزندان نافرمان", - "13": "برای اشتغال", - "14": "برای عشق بین زن و شوهر", - "15": "برای موفقیت در امتحانات", - "16": "برای وسعت در روزی", - "17": "سردرد", - "18": "تنهایی", - "19": "نیاز به دوست", - "20": "ناامیدی از بچه‌دار شدن", - "21": "قدردانی نشدن", - "23": "غمگین", - "24": "ترسیده", - "25": "درخواست بخشش", - "27": "گناهکار", - "28": "شکم‌درد", - "29": "مبارزه کردن", - "30": "از بین بردن خشم", - "31": "برای رفع گرفتاری ذهنی", - "32": "رفتار خشن با شما", - "33": "موقع تصمیم‌گیری به خدا توکل کنید", - "34": "ناآرامی در قلب", - "37": "وقتی چیزی گم شده" -} \ No newline at end of file + "1": "مقابل چشم‌زخم", + "2": "مقابل سحر", + "3": "اضطراب", + "4": "عصبانیت", + "5": "افسردگی", + "6": "اوقات سخت", + "7": "سختی پیدا کردن همسر مناسب", + "8": "تبعیض علیه شما", + "9": "شک", + "10": "روبرو شدن با سختی‌ها", + "11": "احساس حسادت", + "12": "برای فرزندان نافرمان", + "13": "برای اشتغال", + "14": "برای عشق بین زن و شوهر", + "15": "برای موفقیت در امتحانات", + "16": "برای وسعت در روزی", + "17": "سردرد", + "18": "تنهایی", + "19": "نیاز به دوست", + "20": "ناامیدی از بچه‌دار شدن", + "21": "قدردانی نشدن", + "23": "غمگین", + "24": "ترسیده", + "25": "درخواست بخشش", + "27": "گناهکار", + "28": "شکم‌درد", + "29": "مبارزه کردن", + "30": "از بین بردن خشم", + "31": "برای رفع گرفتاری ذهنی", + "32": "رفتار خشن با شما", + "33": "موقع تصمیم‌گیری به خدا توکل کنید", + "34": "ناآرامی در قلب", + "37": "وقتی چیزی گم شده" +} From afdcf44b7449b32ab90b91219b9ae688b686993e Mon Sep 17 00:00:00 2001 From: Anonymous Date: Mon, 4 May 2026 20:01:01 +0200 Subject: [PATCH 2/9] Translated using Weblate (Tamil) Currently translated at 100.0% (33 of 33 strings) Translation: QuranApp/Quranic Topics Translate-URL: https://hosted.weblate.org/projects/QuranApp/quranic-topics/ta/ --- .../main/assets/verses/type0/ta/type0.json | 68 +++++++++---------- 1 file changed, 34 insertions(+), 34 deletions(-) diff --git a/app/src/main/assets/verses/type0/ta/type0.json b/app/src/main/assets/verses/type0/ta/type0.json index fc63d2f51..d80926750 100644 --- a/app/src/main/assets/verses/type0/ta/type0.json +++ b/app/src/main/assets/verses/type0/ta/type0.json @@ -1,35 +1,35 @@ { - "1": "கண்ணேறுக்கு எதிராக", - "2": "சூனியத்திற்கு எதிராக", - "3": "கவலை", - "4": "கோபம்", - "5": "மனச்சோர்வு", - "6": "கஷ்டமான நேரங்கள்", - "7": "சரியான துணையைத் தேடுவதில் சிரமம்", - "8": "பாரபட்சத்திற்கு ஆளாகும்போது", - "9": "சந்தேகமான நிலையில்", - "10": "சவால்களை enfrentar எதிர்கொள்ளும்போது", - "11": "பொறாமை உணர்வு", - "12": "கீழ்ப்படியாத பிள்ளைகளுக்காக", - "13": "வேலை வாய்ப்பிற்காக", - "14": "தம்பதியினருக்கு இடையே அன்பு பெருக", - "15": "தேர்வுகளில் வெற்றி பெற", - "16": "வாழ்வாதாரத்தில் விசாலத்திற்காக", - "17": "தலைவலி", - "18": "தனிமை", - "19": "ஒரு நண்பரின் தேவை இருக்கும்போது", - "20": "குழந்தைகள் பிறக்க வாய்ப்பில்லை என்று கவலைப்படும்போது", - "21": "மதிக்கப்படாத நிலை", - "23": "சோகம்", - "24": "பயம்", - "25": "மன்னிப்பு தேட", - "27": "பாவம் செய்யும்போது", - "28": "வயிற்று வலி", - "29": "போராடும்போது", - "30": "கோபத்தை நீக்க", - "31": "மனக் கஷ்டங்களை நீக்க", - "32": "கடுமையாக நடத்தப்படும்போது", - "33": "முடிவெடுக்கும்போது அல்லாஹ்வின் மீது நம்பிக்கை வைக்க", - "34": "உள்ளத்தில் அமைதியின்மை", - "37": "ஏதேனும் தொலைந்து போகும்போது" -} \ No newline at end of file + "1": "கண்ணேறுக்கு எதிராக", + "2": "சூனியத்திற்கு எதிராக", + "3": "கவலை", + "4": "கோபம்", + "5": "மனச்சோர்வு", + "6": "கஷ்டமான நேரங்கள்", + "7": "சரியான துணையைத் தேடுவதில் சிரமம்", + "8": "பாரபட்சத்திற்கு ஆளாகும்போது", + "9": "சந்தேகமான நிலையில்", + "10": "சவால்களை enfrentar எதிர்கொள்ளும்போது", + "11": "பொறாமை உணர்வு", + "12": "கீழ்ப்படியாத பிள்ளைகளுக்காக", + "13": "வேலை வாய்ப்பிற்காக", + "14": "தம்பதியினருக்கு இடையே அன்பு பெருக", + "15": "தேர்வுகளில் வெற்றி பெற", + "16": "வாழ்வாதாரத்தில் விசாலத்திற்காக", + "17": "தலைவலி", + "18": "தனிமை", + "19": "ஒரு நண்பரின் தேவை இருக்கும்போது", + "20": "குழந்தைகள் பிறக்க வாய்ப்பில்லை என்று கவலைப்படும்போது", + "21": "மதிக்கப்படாத நிலை", + "23": "சோகம்", + "24": "பயம்", + "25": "மன்னிப்பு தேட", + "27": "பாவம் செய்யும்போது", + "28": "வயிற்று வலி", + "29": "போராடும்போது", + "30": "கோபத்தை நீக்க", + "31": "மனக் கஷ்டங்களை நீக்க", + "32": "கடுமையாக நடத்தப்படும்போது", + "33": "முடிவெடுக்கும்போது அல்லாஹ்வின் மீது நம்பிக்கை வைக்க", + "34": "உள்ளத்தில் அமைதியின்மை", + "37": "ஏதேனும் தொலைந்து போகும்போது" +} From d1dd06a95a6488351e6f62544e78aea3ba397272 Mon Sep 17 00:00:00 2001 From: Anonymous Date: Mon, 4 May 2026 20:01:01 +0200 Subject: [PATCH 3/9] Translated using Weblate (Chinese (Simplified Han script)) Currently translated at 100.0% (33 of 33 strings) Translation: QuranApp/Quranic Topics Translate-URL: https://hosted.weblate.org/projects/QuranApp/quranic-topics/zh_Hans/ --- app/src/main/assets/verses/type0/zh-rCN/type0.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/assets/verses/type0/zh-rCN/type0.json b/app/src/main/assets/verses/type0/zh-rCN/type0.json index fb719f0d1..21d467c71 100644 --- a/app/src/main/assets/verses/type0/zh-rCN/type0.json +++ b/app/src/main/assets/verses/type0/zh-rCN/type0.json @@ -32,4 +32,4 @@ "33": "信赖真主", "34": "内心不安", "37": "有所失去" -} \ No newline at end of file +} From e68b7caa1beb5f71dfd411b29700d8b13bd58937 Mon Sep 17 00:00:00 2001 From: Anonymous Date: Mon, 4 May 2026 20:01:00 +0200 Subject: [PATCH 4/9] Translated using Weblate (Filipino) Currently translated at 100.0% (33 of 33 strings) Translation: QuranApp/Quranic Topics Translate-URL: https://hosted.weblate.org/projects/QuranApp/quranic-topics/fil/ --- .../main/assets/verses/type0/fil/type0.json | 66 +++++++++---------- 1 file changed, 33 insertions(+), 33 deletions(-) diff --git a/app/src/main/assets/verses/type0/fil/type0.json b/app/src/main/assets/verses/type0/fil/type0.json index 86a0a6949..e1574f948 100644 --- a/app/src/main/assets/verses/type0/fil/type0.json +++ b/app/src/main/assets/verses/type0/fil/type0.json @@ -1,35 +1,35 @@ { - "1": "Laban sa usog (evil eye)", - "2": "Laban sa kulam (witchcraft)", - "3": "Pagkabahala (Anxiety)", - "4": "Galit", - "5": "Depresyon", - "6": "Mahihirap na panahon", - "7": "Paghihirap sa paghahanap ng tamang kapareha", - "8": "Nakakaranas ng diskriminasyon", - "9": "Nag-aalinlangan", - "10": "Nahaharap sa mga paghihirap", - "11": "Nakakaramdam ng selos/inggit", - "12": "Para sa mga suwail na anak", - "13": "Para sa trabaho", - "14": "Para sa pagmamahalan ng mag-asawa", - "15": "Para sa tagumpay sa pagsusulit", - "16": "Para sa kasaganaan ng kabuhayan", - "17": "Sakit ng ulo", - "18": "Nangungulila", - "19": "Nangangailangan ng kaibigan", - "20": "Walang pag-asang magkaanak", - "21": "Hindi pinahahalagahan", - "23": "Malungkot", - "24": "Natatakot", - "25": "Humingi ng kapatawaran", - "27": "Nagkasala", - "28": "Sakit ng tiyan", - "29": "Nahihirapan", - "30": "Upang maalis ang galit", - "31": "Upang maalis ang paghihirap sa isipan", - "32": "Tratuhin nang malupit", - "33": "Magtiwala sa Allah kapag gumagawa ng mga desisyon", - "34": "Kabalisaan sa puso", - "37": "Kapag may nawalang bagay" + "1": "Laban sa usog (evil eye)", + "2": "Laban sa kulam (witchcraft)", + "3": "Pagkabahala (Anxiety)", + "4": "Galit", + "5": "Depresyon", + "6": "Mahihirap na panahon", + "7": "Paghihirap sa paghahanap ng tamang kapareha", + "8": "Nakakaranas ng diskriminasyon", + "9": "Nag-aalinlangan", + "10": "Nahaharap sa mga paghihirap", + "11": "Nakakaramdam ng selos/inggit", + "12": "Para sa mga suwail na anak", + "13": "Para sa trabaho", + "14": "Para sa pagmamahalan ng mag-asawa", + "15": "Para sa tagumpay sa pagsusulit", + "16": "Para sa kasaganaan ng kabuhayan", + "17": "Sakit ng ulo", + "18": "Nangungulila", + "19": "Nangangailangan ng kaibigan", + "20": "Walang pag-asang magkaanak", + "21": "Hindi pinahahalagahan", + "23": "Malungkot", + "24": "Natatakot", + "25": "Humingi ng kapatawaran", + "27": "Nagkasala", + "28": "Sakit ng tiyan", + "29": "Nahihirapan", + "30": "Upang maalis ang galit", + "31": "Upang maalis ang paghihirap sa isipan", + "32": "Tratuhin nang malupit", + "33": "Magtiwala sa Allah kapag gumagawa ng mga desisyon", + "34": "Kabalisaan sa puso", + "37": "Kapag may nawalang bagay" } From 04fe6894f8d9855ae75117508da5880ea023ee2b Mon Sep 17 00:00:00 2001 From: Anonymous Date: Mon, 4 May 2026 20:01:00 +0200 Subject: [PATCH 5/9] Translated using Weblate (Sindhi) Currently translated at 100.0% (33 of 33 strings) Translation: QuranApp/Quranic Topics Translate-URL: https://hosted.weblate.org/projects/QuranApp/quranic-topics/sd/ --- .../main/assets/verses/type0/sd/type0.json | 66 +++++++++---------- 1 file changed, 33 insertions(+), 33 deletions(-) diff --git a/app/src/main/assets/verses/type0/sd/type0.json b/app/src/main/assets/verses/type0/sd/type0.json index ef624d4f7..177b3d627 100644 --- a/app/src/main/assets/verses/type0/sd/type0.json +++ b/app/src/main/assets/verses/type0/sd/type0.json @@ -1,35 +1,35 @@ { - "1": "نظرِ بد جي خلاف", - "2": "جادوگريءَ جي خلاف", - "3": "پريشاني", - "4": "غصو", - "5": "مايوسي (ڊپريشن)", - "6": "ڏکيو وقت", - "7": "صحيح شريڪِ حيات جي ڳولا ۾ ڏکيائي", - "8": "امتيازي سلوڪ جو نشانو بڻجڻ", - "9": "شڪ واري حالت", - "10": "مشڪلاتن جو مقابلو ڪرڻ", - "11": "حسد محسوس ڪرڻ", - "12": "نافرمان ٻارن لاءِ", - "13": "روزگار لاءِ", - "14": "مڙس ۽ زال جي وچ ۾ محبت لاءِ", - "15": "امتحانن ۾ ڪاميابيءَ لاءِ", - "16": "رزق ۾ ڪشادگي لاءِ", - "17": "مٿي جو سور", - "18": "اڪيلائي", - "19": "دوست جي ضرورت", - "20": "ٻارن جي پيدائش جي ڪا به اميد نه هجڻ", - "21": "قدر نه ڪيو وڃڻ", - "23": "غمگين", - "24": "خوفزده", - "25": "معافي گھرڻ", - "27": "گناهه ٿي وڃڻ", - "28": "پيٽ جو سور", - "29": "جدوجهد ڪرڻ", - "30": "غصو ختم ڪرڻ لاءِ", - "31": "ذهني پريشاني ختم ڪرڻ لاءِ", - "32": "سختيءَ سان پيش اچڻ", - "33": "فيصلا ڪرڻ وقت الله تي ڀروسو ڪريو", - "34": "دل جي بي چيني", - "37": "جڏهن ڪا شيءِ گم ٿي وڃي" + "1": "نظرِ بد جي خلاف", + "2": "جادوگريءَ جي خلاف", + "3": "پريشاني", + "4": "غصو", + "5": "مايوسي (ڊپريشن)", + "6": "ڏکيو وقت", + "7": "صحيح شريڪِ حيات جي ڳولا ۾ ڏکيائي", + "8": "امتيازي سلوڪ جو نشانو بڻجڻ", + "9": "شڪ واري حالت", + "10": "مشڪلاتن جو مقابلو ڪرڻ", + "11": "حسد محسوس ڪرڻ", + "12": "نافرمان ٻارن لاءِ", + "13": "روزگار لاءِ", + "14": "مڙس ۽ زال جي وچ ۾ محبت لاءِ", + "15": "امتحانن ۾ ڪاميابيءَ لاءِ", + "16": "رزق ۾ ڪشادگي لاءِ", + "17": "مٿي جو سور", + "18": "اڪيلائي", + "19": "دوست جي ضرورت", + "20": "ٻارن جي پيدائش جي ڪا به اميد نه هجڻ", + "21": "قدر نه ڪيو وڃڻ", + "23": "غمگين", + "24": "خوفزده", + "25": "معافي گھرڻ", + "27": "گناهه ٿي وڃڻ", + "28": "پيٽ جو سور", + "29": "جدوجهد ڪرڻ", + "30": "غصو ختم ڪرڻ لاءِ", + "31": "ذهني پريشاني ختم ڪرڻ لاءِ", + "32": "سختيءَ سان پيش اچڻ", + "33": "فيصلا ڪرڻ وقت الله تي ڀروسو ڪريو", + "34": "دل جي بي چيني", + "37": "جڏهن ڪا شيءِ گم ٿي وڃي" } From d29d2cddf94f7d2d84ce82860f1190af47dc6e7e Mon Sep 17 00:00:00 2001 From: Faisal Khan Date: Mon, 4 May 2026 20:01:01 +0200 Subject: [PATCH 6/9] Translated using Weblate (Urdu) Currently translated at 100.0% (33 of 33 strings) Translation: QuranApp/Quranic Topics Translate-URL: https://hosted.weblate.org/projects/QuranApp/quranic-topics/ur/ --- .../main/assets/verses/type0/ur/type0.json | 66 +++++++++---------- 1 file changed, 33 insertions(+), 33 deletions(-) diff --git a/app/src/main/assets/verses/type0/ur/type0.json b/app/src/main/assets/verses/type0/ur/type0.json index 5958972f6..a05cdef24 100644 --- a/app/src/main/assets/verses/type0/ur/type0.json +++ b/app/src/main/assets/verses/type0/ur/type0.json @@ -1,35 +1,35 @@ { - "10": "مشکلات کا سامنا کرنا", - "12": "نافرمان بچوں کے لیے", - "13": "روزگار کے لیے", - "20": "اولاد کی پیدائش کی کوئی امید نہیں", - "24": "ڈرا ہوا", - "33": "فیصلے کرتے وقت اللہ پر بھروسہ رکھیں", - "37": "جب کوئی چیز کھو جائے", - "1": "نظر بد کے خلاف", - "2": "جادو ٹونے کے خلاف", - "3": "بے چینی", - "4": "غصہ", - "5": "ذہنی دباؤ", - "6": "مشکل اوقات", - "7": "صحیح ساتھی تلاش کرنے میں دشواری", - "8": "امتیازی سلوک", - "9": "مشکوک", - "11": "حسد محسوس کرنا", - "14": "میاں بیوی کے درمیان محبت کے لیے", - "15": "امتحانات میں کامیابی کے لیے", - "16": "رزق میں وسعت کے لیے", - "17": "سر درد", - "18": "تنہائی", - "19": "دوست کی ضرورت", - "21": "تعریف نہ کی جائے", - "23": "اداس", - "25": "بخشش کی طلب", - "27": "گناہ کیا", - "28": "پیٹ کا درد", - "29": "جدوجہد کرنا", - "30": "غصہ دور کرنے کے لیے", - "31": "سر سے مشکل دور کرنے کے لیے", - "32": "سخت سلوک کرنے پر", - "34": "دل میں بے چینی" + "10": "مشکلات کا سامنا کرنا", + "12": "نافرمان بچوں کے لیے", + "13": "روزگار کے لیے", + "20": "اولاد کی پیدائش کی کوئی امید نہیں", + "24": "ڈرا ہوا", + "33": "فیصلے کرتے وقت اللہ پر بھروسہ رکھیں", + "37": "جب کوئی چیز کھو جائے", + "1": "نظر بد کے خلاف", + "2": "جادو ٹونے کے خلاف", + "3": "بے چینی", + "4": "غصہ", + "5": "ذہنی دباؤ", + "6": "مشکل اوقات", + "7": "صحیح ساتھی تلاش کرنے میں دشواری", + "8": "امتیازی سلوک", + "9": "مشکوک", + "11": "حسد محسوس کرنا", + "14": "میاں بیوی کے درمیان محبت کے لیے", + "15": "امتحانات میں کامیابی کے لیے", + "16": "رزق میں وسعت کے لیے", + "17": "سر درد", + "18": "تنہائی", + "19": "دوست کی ضرورت", + "21": "تعریف نہ کی جائے", + "23": "اداس", + "25": "بخشش کی طلب", + "27": "گناہ کیا", + "28": "پیٹ کا درد", + "29": "جدوجہد کرنا", + "30": "غصہ دور کرنے کے لیے", + "31": "سر سے مشکل دور کرنے کے لیے", + "32": "سخت سلوک کرنے پر", + "34": "دل میں بے چینی" } From f319b515a01dc114274480b89a35eeef82cccd21 Mon Sep 17 00:00:00 2001 From: Anonymous Date: Mon, 4 May 2026 20:00:59 +0200 Subject: [PATCH 7/9] Translated using Weblate (Chinese (Traditional Han script)) Currently translated at 100.0% (33 of 33 strings) Translation: QuranApp/Quranic Topics Translate-URL: https://hosted.weblate.org/projects/QuranApp/quranic-topics/zh_Hant/ --- app/src/main/assets/verses/type0/zh-rTW/type0.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/src/main/assets/verses/type0/zh-rTW/type0.json b/app/src/main/assets/verses/type0/zh-rTW/type0.json index e3612b323..0c330bf8e 100644 --- a/app/src/main/assets/verses/type0/zh-rTW/type0.json +++ b/app/src/main/assets/verses/type0/zh-rTW/type0.json @@ -32,4 +32,4 @@ "33": "信賴真主", "34": "內心不安", "37": "有所失去" -} \ No newline at end of file +} From 72ab8629cf7546659843180bd3a0686126ba78b1 Mon Sep 17 00:00:00 2001 From: Sharpentine Date: Mon, 4 May 2026 20:01:01 +0200 Subject: [PATCH 8/9] Translated using Weblate (Malayalam) Currently translated at 100.0% (33 of 33 strings) Translation: QuranApp/Quranic Topics Translate-URL: https://hosted.weblate.org/projects/QuranApp/quranic-topics/ml/ --- .../main/assets/verses/type0/ml/type0.json | 68 +++++++++---------- 1 file changed, 34 insertions(+), 34 deletions(-) diff --git a/app/src/main/assets/verses/type0/ml/type0.json b/app/src/main/assets/verses/type0/ml/type0.json index 2d773e0d3..0658ff2fd 100644 --- a/app/src/main/assets/verses/type0/ml/type0.json +++ b/app/src/main/assets/verses/type0/ml/type0.json @@ -1,35 +1,35 @@ { - "1": "കണ്ണേറിനെതിരെ", - "2": "ക്ഷുദ്രപ്രയോഗങ്ങൾക്കെതിരെ", - "3": "ആകുലത", - "4": "കോപം", - "5": "വിഷാദം", - "6": "പ്രതിസന്ധികൾ", - "7": "അനുയോജ്യമായ ഇണയെ കണ്ടെത്തുന്നതിലെ ബുദ്ധിമുട്ട്", - "8": "വിവേചനത്തിന് ഇരയാകുമ്പോൾ", - "9": "സംശയാവസ്ഥയിൽ", - "10": "ബുദ്ധിമുട്ടുകൾ നേരിടുമ്പോൾ", - "11": "അസൂയ തോന്നുമ്പോൾ", - "12": "അനുസരണക്കേട് കാണിക്കുന്ന മക്കൾക്കായി", - "13": "ജോലിക്കായി", - "14": "ഭാര്യാഭർത്താക്കന്മാർക്കിടയിലുള്ള സ്നേഹത്തിനായി", - "15": "പരീക്ഷകളിൽ വിജയിക്കുന്നതിന്", - "16": "ഉപജീവനത്തിൽ വിശാലത ഉണ്ടാകുന്നതിന്", - "17": "തലവേദന", - "18": "ഏകാന്തത", - "19": "ഒരു സുഹൃത്തിനെ ആവശ്യമായി വരുമ്പോൾ", - "20": "കുട്ടികൾ ഉണ്ടാകില്ലെന്ന് നിരാശപ്പെടുമ്പോൾ", - "21": "അംഗീകരിക്കപ്പെടാത്ത അവസ്ഥ", - "23": "ദുഃഖം", - "24": "ഭയം", - "25": "പാപമോചനം തേടാൻ", - "27": "പാപം ചെയ്തു പോകുമ്പോൾ", - "28": "വയറുവേദന", - "29": "സമരം ചെയ്യുമ്പോൾ", - "30": "കോപം മാറാൻ", - "31": "മനസ്സിലെ വിഷമങ്ങൾ നീങ്ങാൻ", - "32": "ക്രൂരമായി പെരുമാറപ്പെടുമ്പോൾ", - "33": "തീരുമാനങ്ങൾ എടുക്കുമ്പോൾ അല്ലാഹുവിൽ തവക്കുൽ ചെയ്യുക", - "34": "ഹൃദയത്തിലെ അസ്വസ്ഥത", - "37": "എന്തെങ്കിലും നഷ്ടപ്പെടുമ്പോൾ" -} \ No newline at end of file + "1": "കണ്ണേറിനെതിരെ", + "2": "ക്ഷുദ്രപ്രയോഗങ്ങൾക്കെതിരെ", + "3": "ആകുലത", + "4": "കോപം", + "5": "വിഷാദം", + "6": "പ്രതിസന്ധികൾ", + "7": "അനുയോജ്യമായ ഇണയെ കണ്ടെത്തുന്നതിലെ ബുദ്ധിമുട്ട്", + "8": "വിവേചനത്തിന് ഇരയാകുമ്പോൾ", + "9": "സംശയാവസ്ഥയിൽ", + "10": "ബുദ്ധിമുട്ടുകൾ നേരിടുമ്പോൾ", + "11": "അസൂയ തോന്നുമ്പോൾ", + "12": "അനുസരണക്കേട് കാണിക്കുന്ന മക്കൾക്കായി", + "13": "ജോലിക്കായി", + "14": "ഭാര്യാഭർത്താക്കന്മാർക്കിടയിലുള്ള സ്നേഹത്തിനായി", + "15": "പരീക്ഷകളിൽ വിജയിക്കുന്നതിന്", + "16": "ഉപജീവനത്തിൽ വിശാലത ഉണ്ടാകുന്നതിന്", + "17": "തലവേദന", + "18": "ഏകാന്തത", + "19": "ഒരു സുഹൃത്തിനെ ആവശ്യമായി വരുമ്പോൾ", + "20": "കുട്ടികൾ ഉണ്ടാകില്ലെന്ന് നിരാശപ്പെടുമ്പോൾ", + "21": "അംഗീകരിക്കപ്പെടാത്ത അവസ്ഥ", + "23": "ദുഃഖം", + "24": "ഭയം", + "25": "പാപമോചനം തേടാൻ", + "27": "പാപം ചെയ്തു പോകുമ്പോൾ", + "28": "വയറുവേദന", + "29": "സമരം ചെയ്യുമ്പോൾ", + "30": "കോപം മാറാൻ", + "31": "മനസ്സിലെ വിഷമങ്ങൾ നീങ്ങാൻ", + "32": "ക്രൂരമായി പെരുമാറപ്പെടുമ്പോൾ", + "33": "തീരുമാനങ്ങൾ എടുക്കുമ്പോൾ അല്ലാഹുവിൽ തവക്കുൽ ചെയ്യുക", + "34": "ഹൃദയത്തിലെ അസ്വസ്ഥത", + "37": "എന്തെങ്കിലും നഷ്ടപ്പെടുമ്പോൾ" +} From 1e26a665ae2193916df7755b18d388749ed1d30c Mon Sep 17 00:00:00 2001 From: Faisal Khan Date: Tue, 5 May 2026 04:25:44 +0530 Subject: [PATCH 9/9] full chapter wbw audio --- .../3.json | 195 ++++++++++++ .../java/com/quranapp/android/QuranApp.kt | 2 + .../quranapp/android/api/InventoryUrlFetch.kt | 21 ++ .../android/api/models/ResourcesVersions.kt | 3 +- .../compose/components/VerseOfTheDay.kt | 4 +- .../components/reader/ReaderProvider.kt | 48 +-- .../com/quranapp/android/db/dao/WbwDao.kt | 32 +- .../utils/app/ResourceUpdateManager.kt | 15 +- .../quranapp/android/utils/extensions/File.kt | 6 +- .../mediaplayer/RecitationAudioRepository.kt | 14 +- .../utils/mediaplayer/WbwAudioPlayer.kt | 174 +++++------ .../utils/mediaplayer/WbwAudioRepository.kt | 285 ++++++++++++++++++ .../reader/recitation/RecitationUtils.kt | 1 - .../utils/workers/WbwDownloadWorker.kt | 10 +- app/src/main/res/values/strings.xml | 5 +- inventory/versions/resources_versions.json | 3 +- 16 files changed, 687 insertions(+), 131 deletions(-) create mode 100644 app/schemas/com.quranapp.android.db.ExternalQuranDatabase/3.json create mode 100644 app/src/main/java/com/quranapp/android/api/InventoryUrlFetch.kt create mode 100644 app/src/main/java/com/quranapp/android/utils/mediaplayer/WbwAudioRepository.kt diff --git a/app/schemas/com.quranapp.android.db.ExternalQuranDatabase/3.json b/app/schemas/com.quranapp.android.db.ExternalQuranDatabase/3.json new file mode 100644 index 000000000..7734ee486 --- /dev/null +++ b/app/schemas/com.quranapp.android.db.ExternalQuranDatabase/3.json @@ -0,0 +1,195 @@ +{ + "formatVersion": 1, + "database": { + "version": 3, + "identityHash": "57477fc78f1b7668ed58b832b71ff9aa", + "entities": [ + { + "tableName": "wbw_words", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`ayah_id` INTEGER NOT NULL, `word_index` INTEGER NOT NULL, `wbw_id` TEXT NOT NULL, `translation` TEXT, `transliteration` TEXT, PRIMARY KEY(`wbw_id`, `ayah_id`, `word_index`))", + "fields": [ + { + "fieldPath": "ayahId", + "columnName": "ayah_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "wordIndex", + "columnName": "word_index", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "wbwId", + "columnName": "wbw_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "translation", + "columnName": "translation", + "affinity": "TEXT" + }, + { + "fieldPath": "transliteration", + "columnName": "transliteration", + "affinity": "TEXT" + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "wbw_id", + "ayah_id", + "word_index" + ] + }, + "indices": [ + { + "name": "idx_wbw_words_ayah_wbw", + "unique": false, + "columnNames": [ + "ayah_id", + "wbw_id" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `idx_wbw_words_ayah_wbw` ON `${TABLE_NAME}` (`ayah_id`, `wbw_id`)" + } + ] + }, + { + "tableName": "wbw_audio_timing", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`audio_id` TEXT NOT NULL, `ayah_id` INTEGER NOT NULL, `word_index` INTEGER NOT NULL, `start_millis` INTEGER NOT NULL, `end_millis` INTEGER NOT NULL, PRIMARY KEY(`audio_id`, `ayah_id`, `word_index`))", + "fields": [ + { + "fieldPath": "audioId", + "columnName": "audio_id", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "ayahId", + "columnName": "ayah_id", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "wordIndex", + "columnName": "word_index", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "startMillis", + "columnName": "start_millis", + "affinity": "INTEGER", + "notNull": true + }, + { + "fieldPath": "endMillis", + "columnName": "end_millis", + "affinity": "INTEGER", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "audio_id", + "ayah_id", + "word_index" + ] + }, + "indices": [ + { + "name": "idx_wbw_audio_timing_ayah_word_index", + "unique": false, + "columnNames": [ + "ayah_id", + "word_index" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `idx_wbw_audio_timing_ayah_word_index` ON `${TABLE_NAME}` (`ayah_id`, `word_index`)" + } + ] + }, + { + "tableName": "atlas_bundles", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bundle_key` TEXT NOT NULL, `meta_json` TEXT NOT NULL, `layer_json` TEXT NOT NULL, PRIMARY KEY(`bundle_key`))", + "fields": [ + { + "fieldPath": "bundleKey", + "columnName": "bundle_key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "metaJson", + "columnName": "meta_json", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "layerJson", + "columnName": "layer_json", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "bundle_key" + ] + } + }, + { + "tableName": "atlas_word_shapes", + "createSql": "CREATE TABLE IF NOT EXISTS `${TABLE_NAME}` (`bundle_key` TEXT NOT NULL, `word` TEXT NOT NULL, `placements_json` TEXT NOT NULL, PRIMARY KEY(`bundle_key`, `word`))", + "fields": [ + { + "fieldPath": "bundleKey", + "columnName": "bundle_key", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "word", + "columnName": "word", + "affinity": "TEXT", + "notNull": true + }, + { + "fieldPath": "placementsJson", + "columnName": "placements_json", + "affinity": "TEXT", + "notNull": true + } + ], + "primaryKey": { + "autoGenerate": false, + "columnNames": [ + "bundle_key", + "word" + ] + }, + "indices": [ + { + "name": "idx_atlas_word_shapes_bundle", + "unique": false, + "columnNames": [ + "bundle_key" + ], + "orders": [], + "createSql": "CREATE INDEX IF NOT EXISTS `idx_atlas_word_shapes_bundle` ON `${TABLE_NAME}` (`bundle_key`)" + } + ] + } + ], + "setupQueries": [ + "CREATE TABLE IF NOT EXISTS room_master_table (id INTEGER PRIMARY KEY,identity_hash TEXT)", + "INSERT OR REPLACE INTO room_master_table (id,identity_hash) VALUES(42, '57477fc78f1b7668ed58b832b71ff9aa')" + ] + } +} \ No newline at end of file diff --git a/app/src/main/java/com/quranapp/android/QuranApp.kt b/app/src/main/java/com/quranapp/android/QuranApp.kt index 5c25732a1..2d70052a1 100644 --- a/app/src/main/java/com/quranapp/android/QuranApp.kt +++ b/app/src/main/java/com/quranapp/android/QuranApp.kt @@ -15,6 +15,7 @@ import com.quranapp.android.utils.app.DownloadSourceUtils import com.quranapp.android.utils.app.NotificationUtils import com.quranapp.android.utils.exceptions.CustomExceptionHandler import com.quranapp.android.utils.mediaplayer.RecitationModelManager +import com.quranapp.android.utils.mediaplayer.WbwAudioRepository import com.quranapp.android.utils.univ.FileUtils import com.quranapp.android.viewModels.ReaderIndexViewModel import com.quranapp.android.views.reader.startVotdWidgetPreferenceObserver @@ -54,6 +55,7 @@ class QuranApp : Application() { ReaderPreferences.migrateFromLegacyIfNeeded(this) ReaderPreferences.repairStoredPreferencesIfNeeded(applicationContext) RecitationModelManager.get(this).migrateLegacyData() + WbwAudioRepository.migrateLegacyData(applicationContext) ReaderIndexViewModel.migrateFavourites(this) UserDataMigrationManager(this).migrate() diff --git a/app/src/main/java/com/quranapp/android/api/InventoryUrlFetch.kt b/app/src/main/java/com/quranapp/android/api/InventoryUrlFetch.kt new file mode 100644 index 000000000..d027ef8d4 --- /dev/null +++ b/app/src/main/java/com/quranapp/android/api/InventoryUrlFetch.kt @@ -0,0 +1,21 @@ +package com.quranapp.android.api + +import okhttp3.ResponseBody +import retrofit2.Response + +/** + * Resolves inventory URLs for streaming download. + * + * - `ghraw://relative/path` - relative path after the scheme is fetched via [RetrofitInstance.githubLike] + * (mirror root from user settings). + * - Any other string - treated as a full URL and fetched via [RetrofitInstance.any]. + */ +suspend fun fetchInventoryStreamingResponse(url: String): Response { + return if (url.startsWith("ghraw://")) { + RetrofitInstance.githubLike.getRawContent( + url.removePrefix("ghraw://").trimStart('/'), + ) + } else { + RetrofitInstance.any.downloadStreaming(url) + } +} diff --git a/app/src/main/java/com/quranapp/android/api/models/ResourcesVersions.kt b/app/src/main/java/com/quranapp/android/api/models/ResourcesVersions.kt index 7bbbeb657..ddfdd2ee7 100644 --- a/app/src/main/java/com/quranapp/android/api/models/ResourcesVersions.kt +++ b/app/src/main/java/com/quranapp/android/api/models/ResourcesVersions.kt @@ -11,5 +11,6 @@ data class ResourcesVersions( @SerialName("recitations") val recitationsVersion: Long, @SerialName("recitationTranslations") val recitationTranslationsVersion: Long, @SerialName("tafsirs") val tafsirsVersion: Long, - @SerialName("wbw") val wbwVersion: Long + @SerialName("wbw") val wbwVersion: Long, + @SerialName("wbw_audio") val wbwAudioVersion: Long = 0L, ) diff --git a/app/src/main/java/com/quranapp/android/compose/components/VerseOfTheDay.kt b/app/src/main/java/com/quranapp/android/compose/components/VerseOfTheDay.kt index fd489efd7..2e4c9f86d 100644 --- a/app/src/main/java/com/quranapp/android/compose/components/VerseOfTheDay.kt +++ b/app/src/main/java/com/quranapp/android/compose/components/VerseOfTheDay.kt @@ -73,6 +73,7 @@ import com.quranapp.android.compose.utils.LocalAppLocale import com.quranapp.android.compose.utils.formatString import com.quranapp.android.compose.utils.preferences.ReaderPreferences import com.quranapp.android.compose.utils.preferences.VersePreferences +import com.quranapp.android.utils.mediaplayer.WbwAudioPlayer import com.quranapp.android.utils.reader.LocalVerseActions import com.quranapp.android.utils.reader.QuranTextStyleParams import com.quranapp.android.utils.reader.TranslUtils @@ -268,9 +269,8 @@ internal fun VotdContent( val recState = LocalRecitation.current val isVersePlaying = recState.isAnyPlaying && recState.playingVerse.doesEqual(verse) - val wbwState = LocalWbwState.current LaunchedEffect(verse) { - wbwState.warmUpWord(verse.chapterNo, verse.verseNo, 0) + WbwAudioPlayer.warmUp(context) } Column { diff --git a/app/src/main/java/com/quranapp/android/compose/components/reader/ReaderProvider.kt b/app/src/main/java/com/quranapp/android/compose/components/reader/ReaderProvider.kt index a4e2162e4..cef8045c9 100644 --- a/app/src/main/java/com/quranapp/android/compose/components/reader/ReaderProvider.kt +++ b/app/src/main/java/com/quranapp/android/compose/components/reader/ReaderProvider.kt @@ -1,5 +1,6 @@ package com.quranapp.android.compose.components.reader +import android.widget.Toast import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.getValue @@ -12,6 +13,7 @@ import androidx.compose.runtime.staticCompositionLocalOf import androidx.compose.ui.platform.LocalContext import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel +import com.quranapp.android.R import com.quranapp.android.components.reader.ChapterVersePair import com.quranapp.android.compose.components.reader.dialogs.BookmarkViewerData import com.quranapp.android.compose.components.reader.dialogs.BookmarkViewerSheet @@ -27,6 +29,7 @@ import com.quranapp.android.db.entities.quran.AyahWordEntity import com.quranapp.android.db.relations.VerseWithDetails import com.quranapp.android.utils.Log import com.quranapp.android.utils.mediaplayer.RecitationController +import com.quranapp.android.utils.mediaplayer.WbwAudioPlayResult import com.quranapp.android.utils.mediaplayer.WbwAudioPlayer import com.quranapp.android.utils.quran.QuranMeta import com.quranapp.android.utils.reader.LocalVerseActions @@ -35,6 +38,7 @@ import com.quranapp.android.utils.reader.atlas.LocalQuranAtlasBundle import com.quranapp.android.utils.reader.atlas.rememberQuranAtlasBundle import com.quranapp.android.utils.reader.factory.ReaderFactory import com.quranapp.android.utils.reader.wbw.WbwManager +import com.quranapp.android.utils.univ.MessageUtils import com.quranapp.android.utils.univ.StringUtils import com.quranapp.android.viewModels.ReaderProviderViewModel import kotlinx.coroutines.launch @@ -60,7 +64,6 @@ data class LocalWbwStateData( val onDismissTooltip: () -> Unit, val onForcePlay: (AyahWordEntity) -> Unit, val onWordClick: (AyahWordEntity) -> Unit, - val warmUpWord: (Int, Int, Int) -> Unit, val isWbwAudioLoading: (Int, Int, Int) -> Boolean, val toggleWbwSheet: (WbwSheetData?) -> Unit, val isWbwSheetOpen: Boolean, @@ -112,12 +115,33 @@ fun ReaderProvider( wbwWordLoadingKey = key try { - WbwAudioPlayer.play( - context, - chapterNo, - verseNo, - word.wordIndex, - ) + when ( + WbwAudioPlayer.play( + context, + chapterNo, + verseNo, + word.wordIndex, + ) + ) { + WbwAudioPlayResult.Success -> Unit + WbwAudioPlayResult.NoInternet -> + MessageUtils.popNoInternetMessage(context, true, null) + + WbwAudioPlayResult.TimingsNotLoaded -> + MessageUtils.showRemovableToast( + context, + R.string.wbwAudioTimingsCouldNotLoad, + Toast.LENGTH_LONG, + ) + + WbwAudioPlayResult.InvalidTiming, + WbwAudioPlayResult.NoChapterAudio -> + MessageUtils.showRemovableToast( + context, + R.string.wbwAudioCouldNotPlay, + Toast.LENGTH_LONG, + ) + } } finally { if (wbwWordLoadingKey == key) { wbwWordLoadingKey = null @@ -174,16 +198,6 @@ fun ReaderProvider( ), LocalWbwState provides LocalWbwStateData( isWbwRtl = isWbwRtl, - warmUpWord = { chapterNo, verseNo, wordIndex -> - coroutineScope.launch { - WbwAudioPlayer.warmUp( - context, - chapterNo, - verseNo, - wordIndex, - ) - } - }, isWbwAudioLoading = { chapterNo, verseNo, wordIndex -> wbwWordLoadingKey == "$chapterNo:$verseNo:$wordIndex" }, diff --git a/app/src/main/java/com/quranapp/android/db/dao/WbwDao.kt b/app/src/main/java/com/quranapp/android/db/dao/WbwDao.kt index a1c9cc10c..0d459ab55 100644 --- a/app/src/main/java/com/quranapp/android/db/dao/WbwDao.kt +++ b/app/src/main/java/com/quranapp/android/db/dao/WbwDao.kt @@ -5,6 +5,7 @@ import androidx.room.Insert import androidx.room.OnConflictStrategy import androidx.room.Query import androidx.room.Transaction +import com.quranapp.android.db.entities.wbw.WbwAudioTimingEntity import com.quranapp.android.db.entities.wbw.WbwWordEntity @Dao @@ -56,4 +57,33 @@ interface WbwDao { """ ) suspend fun getDownloadedWbwIds(wbwIds: List): List -} \ No newline at end of file + + @Insert(onConflict = OnConflictStrategy.REPLACE) + suspend fun upsertTimings(timings: List) + + @Query("DELETE FROM wbw_audio_timing WHERE audio_id = :audioId") + suspend fun deleteTimingByAudioId(audioId: String) + + @Query( + """ + SELECT * FROM wbw_audio_timing + WHERE audio_id = :audioId AND ayah_id = :ayahId AND word_index = :wordIndex + LIMIT 1 + """, + ) + suspend fun getWordTiming(audioId: String, ayahId: Int, wordIndex: Int): WbwAudioTimingEntity? + + @Query("SELECT COUNT(*) FROM wbw_audio_timing WHERE audio_id = :audioId") + suspend fun getTimingCount(audioId: String): Int + + @Transaction + suspend fun replaceTimingByAudioId(audioId: String, timings: List) { + deleteTimingByAudioId(audioId) + + if (timings.isEmpty()) return + + timings.chunked(750).forEach { chunk -> + upsertTimings(chunk) + } + } +} diff --git a/app/src/main/java/com/quranapp/android/utils/app/ResourceUpdateManager.kt b/app/src/main/java/com/quranapp/android/utils/app/ResourceUpdateManager.kt index 52925e918..b24be1f50 100644 --- a/app/src/main/java/com/quranapp/android/utils/app/ResourceUpdateManager.kt +++ b/app/src/main/java/com/quranapp/android/utils/app/ResourceUpdateManager.kt @@ -7,6 +7,7 @@ import com.quranapp.android.api.models.ResourcesVersions import com.quranapp.android.utils.Log import com.quranapp.android.utils.Logger import com.quranapp.android.utils.mediaplayer.RecitationModelManager +import com.quranapp.android.utils.mediaplayer.WbwAudioRepository import com.quranapp.android.utils.reader.tafsir.TafsirManager import com.quranapp.android.utils.reader.wbw.WbwManager import com.quranapp.android.utils.univ.FileUtils @@ -88,7 +89,8 @@ class ResourceUpdateManager private constructor(private val ctx: Context) { remote.recitationsVersion > local.recitationsVersion || remote.recitationTranslationsVersion > local.recitationTranslationsVersion || remote.tafsirsVersion > local.tafsirsVersion || - remote.wbwVersion > local.wbwVersion + remote.wbwVersion > local.wbwVersion || + remote.wbwAudioVersion > local.wbwAudioVersion } private suspend fun performUpdates( @@ -142,6 +144,17 @@ class ResourceUpdateManager private constructor(private val ctx: Context) { } } } + + // WBW chapter word-audio timings (cleared on inventory version bump; re-fetched on next play) + launch { + if (force || local == null || remote.wbwAudioVersion > local.wbwAudioVersion) { + try { + WbwAudioRepository.clearImportedTimings(ctx) + } catch (e: Exception) { + Log.saveError(e, "ResourceUpdateManager.updateWbwAudio") + } + } + } } } diff --git a/app/src/main/java/com/quranapp/android/utils/extensions/File.kt b/app/src/main/java/com/quranapp/android/utils/extensions/File.kt index 68842a684..bcd14f2c4 100644 --- a/app/src/main/java/com/quranapp/android/utils/extensions/File.kt +++ b/app/src/main/java/com/quranapp/android/utils/extensions/File.kt @@ -5,10 +5,14 @@ import java.io.File fun File.isGzip(): Boolean { inputStream().use { input -> val buffered = input.buffered() + buffered.mark(2) + val byte1 = buffered.read() val byte2 = buffered.read() + buffered.reset() + return byte1 == 0x1f && byte2 == 0x8b } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/quranapp/android/utils/mediaplayer/RecitationAudioRepository.kt b/app/src/main/java/com/quranapp/android/utils/mediaplayer/RecitationAudioRepository.kt index 3bab257be..429fb1393 100644 --- a/app/src/main/java/com/quranapp/android/utils/mediaplayer/RecitationAudioRepository.kt +++ b/app/src/main/java/com/quranapp/android/utils/mediaplayer/RecitationAudioRepository.kt @@ -5,7 +5,7 @@ import android.net.Uri import androidx.core.net.toUri import androidx.work.WorkManager import com.quranapp.android.api.JsonHelper -import com.quranapp.android.api.RetrofitInstance +import com.quranapp.android.api.fetchInventoryStreamingResponse import com.quranapp.android.api.models.mediaplayer.ChapterTimingMetadata import com.quranapp.android.api.models.mediaplayer.RecitationAudioKind import com.quranapp.android.api.models.mediaplayer.RecitationAudioTrack @@ -361,21 +361,11 @@ class RecitationAudioRepository(private val context: Context) { } } - /** - * `ghraw://` is stripped to a relative path and fetched via GithubLikeApi (mirror root from user settings). - * Any other string is treated as a full URL and fetched via AnyApi. - */ private suspend fun downloadTimingMetadata( file: File, timingUrl: String, ) = withContext(Dispatchers.IO) { - val response = if (timingUrl.startsWith("ghraw://")) { - RetrofitInstance.githubLike.getRawContent( - timingUrl.removePrefix("ghraw://").trimStart('/') - ) - } else { - RetrofitInstance.any.downloadStreaming(timingUrl) - } + val response = fetchInventoryStreamingResponse(timingUrl) if (!response.isSuccessful) { if (response.code() == 404) throw HttpNotFoundException() diff --git a/app/src/main/java/com/quranapp/android/utils/mediaplayer/WbwAudioPlayer.kt b/app/src/main/java/com/quranapp/android/utils/mediaplayer/WbwAudioPlayer.kt index c4eaf4b80..dbded2194 100644 --- a/app/src/main/java/com/quranapp/android/utils/mediaplayer/WbwAudioPlayer.kt +++ b/app/src/main/java/com/quranapp/android/utils/mediaplayer/WbwAudioPlayer.kt @@ -1,71 +1,24 @@ package com.quranapp.android.utils.mediaplayer import android.content.Context -import android.net.Uri import androidx.media3.common.AudioAttributes import androidx.media3.common.C import androidx.media3.common.MediaItem import androidx.media3.common.PlaybackException import androidx.media3.common.Player import androidx.media3.exoplayer.ExoPlayer -import com.quranapp.android.api.RetrofitInstance import com.quranapp.android.utils.Log -import com.quranapp.android.utils.univ.StringUtils -import kotlinx.coroutines.Dispatchers +import com.quranapp.android.utils.receivers.NetworkStateReceiver import kotlinx.coroutines.sync.Mutex import kotlinx.coroutines.sync.withLock -import kotlinx.coroutines.withContext -import java.io.File -import java.io.IOException object WbwAudioPlayer { - - private const val BASE = "https://audio.qurancdn.com/wbw/" - private const val CACHE_SUBDIR = "wbw_audio" - private val mutex = Mutex() private var player: ExoPlayer? = null - private fun segment(n: Int): String = StringUtils.formatInvariant("%03d", n) - - private fun buildUrl(chapterNo: Int, verseNo: Int, urlWordIndex: Int): String = - "$BASE${segment(chapterNo)}_${segment(verseNo)}_${segment(urlWordIndex)}.mp3" - - private fun cacheFile(context: Context, chapterNo: Int, verseNo: Int, urlWordIndex: Int): File { - val dir = File(context.cacheDir, CACHE_SUBDIR).apply { mkdirs() } - return File(dir, "${segment(chapterNo)}_${segment(verseNo)}_${segment(urlWordIndex)}.mp3") - } - - private suspend fun downloadIfMissing(file: File, url: String) { - if (file.exists() && file.length() > 0L) return - val dir = file.parentFile ?: return - dir.mkdirs() - val tmp = File(dir, "${file.name}.tmp") - - withContext(Dispatchers.IO) { - val response = RetrofitInstance.any.downloadStreaming(url) - - if (!response.isSuccessful) { - throw IOException("Wbw audio failed: HTTP ${response.code()}") - } - - val body = response.body() ?: throw IOException("Wbw audio body is null") - - body.byteStream().use { input -> - tmp.outputStream().buffered().use { output -> - input.copyTo(output) - } - } - } - - if (!tmp.renameTo(file)) { - tmp.delete() - throw IOException("Wbw audio could not finalize cache file") - } - } - private fun getOrCreatePlayer(context: Context): ExoPlayer { player?.let { return it } + val app = context.applicationContext return ExoPlayer.Builder(app).build().apply { @@ -76,7 +29,9 @@ object WbwAudioPlayer { .build(), true, ) + repeatMode = Player.REPEAT_MODE_OFF + addListener( object : Player.Listener { override fun onPlayerError(error: PlaybackException) { @@ -87,57 +42,106 @@ object WbwAudioPlayer { }.also { player = it } } + private fun isValidTimingWindow(startMs: Long, endMs: Long): Boolean { + if ( + startMs == C.TIME_UNSET || + endMs == C.TIME_UNSET || + startMs < 0L || + endMs < 0L || + endMs <= startMs + ) { + return false + } + + return try { + Math.subtractExact(endMs, startMs) > 0L + } catch (_: ArithmeticException) { + false + } + } + suspend fun play( context: Context, chapterNo: Int, verseNo: Int, appWordIndex: Int, - ) { - val (file, url) = buildUrlAndFile(context, chapterNo, verseNo, appWordIndex) + ): WbwAudioPlayResult { + val app = context.applicationContext + + if ( + WbwAudioRepository.getTimingCount(context) == 0 && + !NetworkStateReceiver.isNetworkConnected(app) + ) { + return WbwAudioPlayResult.NoInternet + } + + val timing = WbwAudioRepository.getWordTiming(context, chapterNo, verseNo, appWordIndex) + + if (timing == null) { + val count = WbwAudioRepository.getTimingCount(context) + return when { + count == 0 && !NetworkStateReceiver.isNetworkConnected(app) -> + WbwAudioPlayResult.NoInternet + + else -> WbwAudioPlayResult.TimingsNotLoaded + } + } + + if (!isValidTimingWindow(timing.startMillis, timing.endMillis)) { + Log.saveError( + Exception("Invalid WBW timing window ${timing.startMillis}–${timing.endMillis}"), + "WbwAudioPlayer.play", + ) + return WbwAudioPlayResult.InvalidTiming + } + + val uri = WbwAudioRepository.resolveChapterAudioUri(context, chapterNo) + + if (uri == null) { + return if (!NetworkStateReceiver.isNetworkConnected(app)) { + WbwAudioPlayResult.NoInternet + } else { + WbwAudioPlayResult.NoChapterAudio + } + } mutex.withLock { - try { - downloadIfMissing(file, url) - } catch (e: Exception) { - Log.saveError(e, "WbwWordAudioPlayer.play") - return + val exo = getOrCreatePlayer(context).apply { + stop() + clearMediaItems() + setMediaItem( + MediaItem.Builder() + .setUri(uri) + .setClippingConfiguration( + MediaItem.ClippingConfiguration.Builder() + .setStartPositionMs(timing.startMillis) + .setEndPositionMs(timing.endMillis) + .build(), + ) + .build(), + ) } - val p = getOrCreatePlayer(context) - p.stop() - p.clearMediaItems() - p.setMediaItem(MediaItem.fromUri(Uri.fromFile(file))) - p.prepare() - p.playWhenReady = true + exo.prepare() + exo.playWhenReady = true } - } - suspend fun warmUp( - context: Context, - chapterNo: Int, - verseNo: Int, - appWordIndex: Int, - ) { - val (file, url) = buildUrlAndFile(context, chapterNo, verseNo, appWordIndex) + return WbwAudioPlayResult.Success + } + suspend fun warmUp(context: Context) { try { - downloadIfMissing(file, url) + WbwAudioRepository.ensureTimingsAvailable(context) } catch (e: Exception) { - Log.saveError(e, "WbwWordAudioPlayer.warmUp") - return + Log.saveError(e, "WbwAudioPlayer.warmUp") } } +} - private fun buildUrlAndFile( - context: Context, - chapterNo: Int, - verseNo: Int, - appWordIndex: Int, - ): Pair { - val urlWordIndex = appWordIndex + 1 - val file = cacheFile(context.applicationContext, chapterNo, verseNo, urlWordIndex) - val url = buildUrl(chapterNo, verseNo, urlWordIndex) - - return file to url - } +sealed class WbwAudioPlayResult { + data object Success : WbwAudioPlayResult() + data object NoInternet : WbwAudioPlayResult() + data object TimingsNotLoaded : WbwAudioPlayResult() + data object InvalidTiming : WbwAudioPlayResult() + data object NoChapterAudio : WbwAudioPlayResult() } diff --git a/app/src/main/java/com/quranapp/android/utils/mediaplayer/WbwAudioRepository.kt b/app/src/main/java/com/quranapp/android/utils/mediaplayer/WbwAudioRepository.kt new file mode 100644 index 000000000..bc6ed1bf6 --- /dev/null +++ b/app/src/main/java/com/quranapp/android/utils/mediaplayer/WbwAudioRepository.kt @@ -0,0 +1,285 @@ +package com.quranapp.android.utils.mediaplayer + +import android.content.Context +import android.net.Uri +import android.util.JsonReader +import androidx.core.net.toUri +import androidx.room.withTransaction +import com.quranapp.android.api.fetchInventoryStreamingResponse +import com.quranapp.android.db.DatabaseProvider +import com.quranapp.android.db.entities.wbw.WbwAudioTimingEntity +import com.quranapp.android.utils.Log +import com.quranapp.android.utils.app.AppUtils +import com.quranapp.android.utils.extensions.isGzip +import com.quranapp.android.utils.mediaplayer.WbwAudioRepository.AUDIO_ID +import com.quranapp.android.utils.reader.recitation.RecitationUtils +import com.quranapp.android.utils.receivers.NetworkStateReceiver +import com.quranapp.android.utils.univ.FileUtils +import com.quranapp.android.utils.univ.StringUtils +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import kotlinx.coroutines.sync.Mutex +import kotlinx.coroutines.sync.withLock +import kotlinx.coroutines.withContext +import java.io.File +import java.io.IOException +import java.io.InputStreamReader +import java.nio.charset.StandardCharsets +import java.util.zip.GZIPInputStream + +object WbwAudioRepository { + private const val DIR_NAME = "wbw_audio" + + private val ROOT_DIR_PATH: String = FileUtils.createPath( + AppUtils.BASE_APP_DOWNLOADED_SAVED_DATA_DIR, + DIR_NAME + ) + + private const val AUDIO_ID: String = "wbw_a1" + + private const val TIMING_URL = + "ghraw://AlfaazPlus/QuranAppInventory/master/wbw_timings/wbw_a1.json.gz" + + private const val AUDIO_URL_TEMPLATE = + "https://github.com/dabatase/wbw_a1/releases/download/v1/{chapNo:%03d}.webm" + + + private val timingLoadMutex = Mutex() + + fun migrateLegacyData(appContext: Context) { + CoroutineScope(Dispatchers.IO).launch { + val dir = File(appContext.cacheDir, "wbw_audio") + + if (dir.exists()) { + dir.deleteRecursively() + } + } + } + + private fun getRootDir(context: Context): File { + val dir = File(context.applicationContext.filesDir, ROOT_DIR_PATH) + + if (!dir.exists()) { + dir.mkdirs() + } + return dir + } + + private fun chapterAudioFile(context: Context, chapterNo: Int): File { + return File(getRootDir(context), StringUtils.formatInvariant("%03d.webm", chapterNo)) + } + + fun prepareChapterAudioUrl(chapterNo: Int): String? { + var url = AUDIO_URL_TEMPLATE + + return try { + var matcher = RecitationUtils.URL_CHAPTER_PATTERN.matcher(url) + + while (matcher.find()) { + val group = matcher.group(1) + + if (group != null) { + url = matcher.replaceFirst(StringUtils.formatInvariant(group, chapterNo)) + matcher.reset(url) + } + } + + url + } catch (e: Exception) { + Log.saveError(e, "WbwAudioRepository.prepareChapterAudioUrl") + null + } + } + + suspend fun getTimingCount(context: Context): Int = + withContext(Dispatchers.IO) { + DatabaseProvider.getExternalQuranDatabase(context.applicationContext) + .wbwDao() + .getTimingCount(AUDIO_ID) + } + + suspend fun resolveChapterAudioUri(context: Context, chapterNo: Int): Uri? = + withContext(Dispatchers.IO) { + val app = context.applicationContext + + val local = chapterAudioFile(app, chapterNo) + + if (local.exists() && local.length() > 0L) { + return@withContext local.toUri() + } + + if (!NetworkStateReceiver.isNetworkConnected(app)) { + return@withContext null + } + + val url = prepareChapterAudioUrl(chapterNo) ?: return@withContext null + + url.toUri() + } + + suspend fun getWordTiming( + context: Context, + chapterNo: Int, + verseNo: Int, + wordIndex: Int, + ): WbwAudioTimingEntity? { + if (!ensureTimingsInDb(context.applicationContext)) { + return null + } + + val ayahId = chapterNo * 1000 + verseNo + + return DatabaseProvider.getExternalQuranDatabase(context.applicationContext) + .wbwDao() + .getWordTiming(AUDIO_ID, ayahId, wordIndex) + } + + suspend fun ensureTimingsAvailable(context: Context) { + ensureTimingsInDb(context.applicationContext) + } + + suspend fun clearImportedTimings(context: Context) { + withContext(Dispatchers.IO) { + DatabaseProvider.getExternalQuranDatabase(context.applicationContext) + .wbwDao() + .deleteTimingByAudioId(AUDIO_ID) + } + } + + /** + * @return true if timing rows exist for [AUDIO_ID] after any required download/import attempt. + */ + private suspend fun ensureTimingsInDb(appContext: Context): Boolean { + val db = DatabaseProvider.getExternalQuranDatabase(appContext) + val dao = db.wbwDao() + + if (dao.getTimingCount(AUDIO_ID) > 0) return true + + return timingLoadMutex.withLock { + if (db.wbwDao().getTimingCount(AUDIO_ID) > 0) return@withLock true + if (!NetworkStateReceiver.isNetworkConnected(appContext)) return@withLock false + + downloadAndImportTimings(appContext) + + db.wbwDao().getTimingCount(AUDIO_ID) > 0 + } + } + + private suspend fun downloadAndImportTimings(appContext: Context) { + val tmp = runCatching { downloadTimingToTemp(appContext) } + .onFailure { Log.saveError(it, "WbwAudioRepository.downloadTimingToTemp") } + .getOrNull() ?: return + + try { + importTimingFromFile(appContext, tmp) + } catch (e: Exception) { + Log.saveError(e, "WbwAudioRepository.importTimingFromFile") + } finally { + tmp.delete() + } + } + + private suspend fun downloadTimingToTemp(appContext: Context): File = + withContext(Dispatchers.IO) { + val dest = File(appContext.cacheDir, "wbw_timing_${System.currentTimeMillis()}.tmp") + + val response = fetchInventoryStreamingResponse(TIMING_URL) + + if (!response.isSuccessful) { + throw IOException("WBW timing download failed: HTTP ${response.code()}") + } + + val body = response.body() ?: throw IOException("WBW timing body is null") + + body.byteStream().use { input -> + dest.outputStream().buffered().use { output -> + input.copyTo(output) + } + } + + dest + } + + private suspend fun importTimingFromFile(appContext: Context, file: File) = + withContext(Dispatchers.IO) { + val INSERT_CHUNK = 750 + + val db = DatabaseProvider.getExternalQuranDatabase(appContext) + val dao = db.wbwDao() + + val rawStream = if (file.isGzip()) { + GZIPInputStream(file.inputStream().buffered()) + } else { + file.inputStream().buffered() + } + + rawStream.use { raw -> + JsonReader(InputStreamReader(raw, StandardCharsets.UTF_8)).use { reader -> + db.withTransaction { + dao.deleteTimingByAudioId(AUDIO_ID) + + val buffer = ArrayList(INSERT_CHUNK) + + reader.beginObject() + + while (reader.hasNext()) { + val key = reader.nextName() + reader.beginArray() + + val startMs = reader.nextLong() + val endMs = reader.nextLong() + + reader.endArray() + + val triple = parseTimingKey(key) ?: continue + + val (chapterNo, verseNo, wordIdx) = triple + + if (endMs <= startMs || startMs < 0L) continue + + val ayahId = chapterNo * 1000 + verseNo + + buffer.add( + WbwAudioTimingEntity( + audioId = AUDIO_ID, + ayahId = ayahId, + wordIndex = wordIdx, + startMillis = startMs, + endMillis = endMs, + ), + ) + + if (buffer.size >= INSERT_CHUNK) { + dao.upsertTimings(ArrayList(buffer)) + + buffer.clear() + } + } + + reader.endObject() + + if (buffer.isNotEmpty()) { + dao.upsertTimings(buffer) + } + } + } + } + } + + /** + * Parses timing map keys shaped like "1_1_0" -> (chapter, verse, wordIndex). + */ + private fun parseTimingKey(key: String): Triple? { + val parts = key.split('_') + if (parts.size < 3) return null + + val chapter = parts[0].toIntOrNull() ?: return null + val verse = parts[1].toIntOrNull() ?: return null + val wordIndex = parts[2].toIntOrNull() ?: return null + + if (chapter <= 0 || verse <= 0) return null + + return Triple(chapter, verse, wordIndex) + } +} diff --git a/app/src/main/java/com/quranapp/android/utils/reader/recitation/RecitationUtils.kt b/app/src/main/java/com/quranapp/android/utils/reader/recitation/RecitationUtils.kt index 989276b09..c67117f45 100644 --- a/app/src/main/java/com/quranapp/android/utils/reader/recitation/RecitationUtils.kt +++ b/app/src/main/java/com/quranapp/android/utils/reader/recitation/RecitationUtils.kt @@ -5,7 +5,6 @@ import com.quranapp.android.utils.univ.FileUtils import java.util.regex.Pattern object RecitationUtils { - @JvmField val DIR_NAME: String = FileUtils.createPath( AppUtils.BASE_APP_DOWNLOADED_SAVED_DATA_DIR, "recitations" diff --git a/app/src/main/java/com/quranapp/android/utils/workers/WbwDownloadWorker.kt b/app/src/main/java/com/quranapp/android/utils/workers/WbwDownloadWorker.kt index 6332b0f68..8e9cf1543 100644 --- a/app/src/main/java/com/quranapp/android/utils/workers/WbwDownloadWorker.kt +++ b/app/src/main/java/com/quranapp/android/utils/workers/WbwDownloadWorker.kt @@ -14,7 +14,7 @@ import androidx.work.workDataOf import com.quranapp.android.R import com.quranapp.android.activities.ActivitySettings import com.quranapp.android.api.JsonHelper -import com.quranapp.android.api.RetrofitInstance +import com.quranapp.android.api.fetchInventoryStreamingResponse import com.quranapp.android.api.models.wbw.WbwLanguageInfo import com.quranapp.android.api.models.wbw.WbwPayloadModel import com.quranapp.android.compose.navigation.SettingRoutes @@ -192,13 +192,7 @@ suspend fun downloadGithubRawContentToFile( dest: File, setProgress: suspend (Int?) -> Unit, ) = withContext(Dispatchers.IO) { - val response = if (url.startsWith("ghraw://")) { - RetrofitInstance.githubLike.getRawContent( - url.removePrefix("ghraw://").trimStart('/') - ) - } else { - RetrofitInstance.any.downloadStreaming(url) - } + val response = fetchInventoryStreamingResponse(url) if (!response.isSuccessful) { throw IOException("WBW download failed: HTTP ${response.code()}") diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 4f0443da3..045a8edb5 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -443,6 +443,9 @@ Select all Translations aren\'t searched while Arabic search is on. No translations downloaded yet. - + %1$s / %2$s + + Word timings could not be loaded. Check your connection and try again. + Could not play word audio. diff --git a/inventory/versions/resources_versions.json b/inventory/versions/resources_versions.json index 75181521f..84f29d10d 100644 --- a/inventory/versions/resources_versions.json +++ b/inventory/versions/resources_versions.json @@ -4,5 +4,6 @@ "recitations": 5, "recitationTranslations": 7, "tafsirs": 1, - "wbw": 2 + "wbw": 2, + "wbw_audio": 1 }