(function () { 'use strict'; const base = new URL('.', document.currentScript.src); const configFile = 'quran_reader_registry.json'; let configPromise, corePromise, preferences, epoch = 0, cleanup = () => {}; const node = (tag, text, className) => { const element = document.createElement(tag); if (text != null) element.textContent = text; if (className) element.className = className; return element; }; const button = (text, action) => { const element = node('button', text); element.type = 'button'; element.addEventListener('click', action); return element; }; const format = (text, values) => text.replace(/\{(\w+)\}/g, (_, key) => String(values[key] ?? '')); // Normalize only the search index; the displayed Quran text is never transformed. const normalize = text => String(text).normalize('NFD').replace(/[\u0610-\u061a\u064b-\u065f\u0670\u06d6-\u06ed\u0300-\u036f\u0640]/g, '') .replace(/[\u0622\u0623\u0625\u0671]/g, '\u0627').replace(/\u0649/g, '\u064a').replace(/\s+/g, ' ').trim(); async function fetchJSON(file, timeout = 30000) { const url = new URL(file, base); if (url.origin !== base.origin) throw Error('Cross-origin Quran registry'); const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), timeout); try { const response = await fetch(url, {signal: controller.signal, cache:file===configFile?'no-store':'default'}); if (!response.ok) throw Error(file + ' HTTP ' + response.status); return await response.json(); } finally { clearTimeout(timer); } } function config() { if (!configPromise) configPromise = fetchJSON(configFile).then(value => { if (value.schema_version !== 1 || !value.ui || !value.core_file || !value.defaults || !value.limits) throw Error('Invalid reader registry'); for (const key of ['title','open','settings','loading','load_error','retry','surah','ayah','page','juz','go','previous','next', 'search','search_submit','search_empty','no_results','results','more','reader','page_status','invalid_page','font_size', 'remember_page','save','saved','storage_error','back','source','theme','theme_auto','theme_light','theme_dark', 'follow_audio','verse_actions','listen','copy','copied','copy_failed','close_actions','audio_loading','audio_unavailable','search_surah']) { if (typeof value.ui[key] !== 'string') throw Error('Missing UI: ' + key); } if (!Number.isInteger(value.limits.search_batch) || value.limits.search_batch < 1) throw Error('Invalid search limit'); return value; }).catch(error => { configPromise = null; throw error; }); return configPromise; } function data(c) { if (!corePromise) corePromise = fetchJSON(c.core_file, c.limits.request_timeout_ms).then(async value => { if (value.schema_version !== 1 || value.counts?.ayahs !== 6236 || value.counts?.pages !== 604 || value.counts?.juz !== 30 || value.counts?.surahs !== 114 || value.surahs?.length !== 114 || value.verses?.length !== 6236) throw Error('Invalid Quran registry'); const pages = Array.from({length: value.counts.pages + 1}, () => []); const juzPages = []; const nextAyah = Array(115).fill(1); value.surahs.forEach((s, i) => { if (s.id !== i + 1 || typeof s.name !== 'string' || !Number.isInteger(s.ayah_count)) throw Error('Invalid surah'); }); let previousPage = 1, previousSurah = 1, previousJuz = 1; value.verses.forEach((v, i) => { if (v.id !== i + 1 || !Number.isInteger(v.page) || !pages[v.page] || v.page < 1 || v.page < previousPage || !Number.isInteger(v.juz) || v.juz < previousJuz || v.juz > 30 || !Number.isInteger(v.surah) || v.surah < previousSurah || v.surah > 114 || v.ayah !== nextAyah[v.surah]++ || typeof v.text !== 'string' || !v.text || typeof v.search_text !== 'string') throw Error('Invalid ayah: ' + (i + 1)); previousPage = v.page; previousSurah = v.surah; previousJuz = v.juz; pages[v.page].push(v); if (!juzPages[v.juz]) juzPages[v.juz] = v.page; }); value.surahs.forEach(s => { if (nextAyah[s.id] - 1 !== s.ayah_count || pages[s.first_page]?.find(v => v.surah === s.id)?.ayah !== 1) throw Error('Surah count mismatch'); }); if (pages.slice(1).some(p => !p.length) || Array.from({length:30}, (_, i) => juzPages[i + 1]).some(p => !p)) throw Error('Missing page or juz'); // Keep old page boundaries for existing plans; overlay metadata by verse key only. const legacyPages=pages.map(row=>row.map(v=>v.id)); const partitions=await fetchJSON('quran_partitions_registry.json'); if(partitions.schema_version!==1||partitions.revision!=='tanzil-20260929'||partitions.surah_ayah_counts?.length!==114|| value.surahs.some((s,i)=>partitions.surah_ayah_counts[i]!==s.ayah_count)||!partitions.reader_ui||!partitions.khatma_ui||!Array.isArray(partitions.units))throw Error('Invalid partitions registry'); const byKey=new Map(value.verses.map(v=>[v.surah+':'+v.ayah,v])); const mapped={}; for(const [field,count] of Object.entries({page:604,juz:30,hizb:60,rub_el_hizb:240})){ const rows=partitions[field];if(!Array.isArray(rows)||rows.length!==count)throw Error('Invalid partition count: '+field); let previous=0; mapped[field]=rows.map((row,i)=>{ const start=byKey.get(row.start),end=byKey.get(row.end); if(row.id!==i+1||!start||!end||start.id!==previous+1||end.id{ if(r.juz!==Math.floor(i/8)+1||r.hizb!==Math.floor(i/4)+1|| r.startIdmapped.juz[r.juz-1].endId|| r.startIdmapped.hizb[r.hizb-1].endId)throw Error('Partition parent mismatch'); }); mapped.hizb.forEach((h,i)=>{if(h.juz!==Math.floor(i/2)+1||JSON.stringify(h.rub_el_hizb)!==JSON.stringify([1,2,3,4].map(n=>i*4+n)))throw Error('Invalid hizb children');}); mapped.juz.forEach((j,i)=>{if(JSON.stringify(j.hizb)!==JSON.stringify([i*2+1,i*2+2])||JSON.stringify(j.rub_el_hizb)!==JSON.stringify(Array.from({length:8},(_,n)=>i*8+n+1)))throw Error('Invalid juz children');}); value.verses.forEach(v=>{v.verse_key=v.surah+':'+v.ayah;}); for(const [field,rows] of Object.entries(mapped))for(const row of rows){ for(let id=row.startId;id<=row.endId;id++){const v=value.verses[id-1];byKey.get(v.verse_key)[field]=row.id;} } for(const row of pages)row.length=0;juzPages.length=0; value.verses.forEach(v=>{pages[v.page].push(v);if(!juzPages[v.juz])juzPages[v.juz]=v.page;}); value.surahs.forEach(s=>{s.first_page=byKey.get(s.id+':1').page;}); Object.assign(c.ui,partitions.reader_ui); return {...value, pages, juzPages, legacyPages, partitions:{...partitions,...mapped}, byKey, searchIndex: value.verses.map(v => normalize(v.search_text))}; }).catch(error => { corePromise = null; throw error; }); return corePromise; } function prefs(c) { if (preferences) return preferences; let saved; try { saved = JSON.parse(localStorage.getItem(c.storage_key)); } catch { /* Storage is optional. */ } preferences = { font_size: Number.isFinite(saved?.font_size) ? Math.max(c.limits.font_min, Math.min(c.limits.font_max, saved.font_size)) : c.defaults.font_size, remember_page: typeof saved?.remember_page === 'boolean' ? saved.remember_page : c.defaults.remember_page, metadata_revision: saved?.metadata_revision, page: Number.isInteger(saved?.page) && saved.page >= 1 && saved.page <= 604 ? saved.page : 1, theme: ['auto','light','dark'].includes(saved?.theme) ? saved.theme : c.defaults.theme, follow_audio: typeof saved?.follow_audio === 'boolean' ? saved.follow_audio : c.defaults.follow_audio, show_partition_status: typeof saved?.show_partition_status === 'boolean' ? saved.show_partition_status : c.defaults.show_partition_status, show_swipe_hint: typeof saved?.show_swipe_hint === 'boolean' ? saved.show_swipe_hint : c.defaults.show_swipe_hint }; return preferences; } function persist(c) { try { localStorage.setItem(c.storage_key, JSON.stringify(preferences)); return true; } catch { return false; } } function progress(c) { try {const value=JSON.parse(localStorage.getItem(c.progress_storage_key));return value&&typeof value==='object'?value:{};} catch {return {};} } function saveProgress(c,kind,value) { try {const raw=localStorage.getItem(c.progress_storage_key),state=raw?JSON.parse(raw):{};if(!state||typeof state!=='object')throw Error('storage');state[kind]=value;localStorage.setItem(c.progress_storage_key,JSON.stringify(state));return true;} catch {return false;} } function panel(title, root, back, ui) { RegistryApp.openPanel('worship', {title, compact: true}, () => { document.getElementById('adaptivePopupBody').append(root); if (back) document.getElementById('adaptivePopupActions').append(button(ui.back, back)); }); document.getElementById('adaptivePopupCard').dataset.size = 'long'; } function applyTheme(c, p) { const card = document.getElementById('adaptivePopupCard'); const mode = p.theme === 'auto' ? (window.matchMedia?.('(prefers-color-scheme: dark)').matches ? 'dark' : 'light') : p.theme; card.dataset.quranTheme = mode; for (const [key,value] of Object.entries(c.themes[mode])) card.style.setProperty('--quran-'+key,value); } function themeField(c, p, onChange) { const label = node('label',c.ui.theme), select = node('select'); for (const key of ['auto','light','dark']) {const option=node('option',c.ui['theme_'+key]);option.value=key;select.append(option);} select.value=p.theme;select.addEventListener('change',()=>onChange(select.value));label.append(select);return label; } async function mount(parent, back, settingsOnly) { const entry = button('…', () => {}); entry.disabled = true; entry.dataset.quranEntry = settingsOnly ? 'settings' : 'reader'; parent.append(entry); try { const c = await config(); if (!entry.isConnected) return; entry.textContent = settingsOnly ? c.ui.settings : c.ui.open; entry.disabled = false; entry.onclick = () => settingsOnly ? openSettings(back) : open({back}); } catch (error) { if (!entry.isConnected) return; entry.textContent = configFile + ': ' + error.message; entry.disabled = false; entry.onclick = () => { entry.remove(); mount(parent, back, settingsOnly); }; } } async function open(options = {}) { const request = ++epoch; let c; try { c = await config(); } catch (error) { if (request !== epoch) return; const root = node('div', configFile + ': ' + error.message); root.append(button('↻', () => open(options))); panel(configFile, root, options.back, {back: '←'}); return; } if (request !== epoch) return; const root = node('div', null, 'quran-reader'); const loading = node('p', c.ui.loading); loading.setAttribute('role', 'status'); root.append(loading); panel(c.ui.title, root, options.back, c.ui); const token = epoch; try { const source = await data(c); if (token !== epoch || !root.isConnected) return; renderReader(root, source, c, options); } catch (error) { if (token !== epoch || !root.isConnected) return; loading.textContent = c.ui.load_error; root.append(node('p', error.message, 'quran-meta'), button(c.ui.retry, () => open(options))); } } function renderReader(root, source, c, options) { const ui = c.ui, p = prefs(c); let legacyRememberedPage=null; if(p.metadata_revision!==source.partitions.revision){ legacyRememberedPage=p.page; const firstId=source.legacyPages[p.page]?.[0]; if(firstId)p.page=source.verses[firstId-1].page; p.metadata_revision=source.partitions.revision;persist(c); } let page = p.remember_page ? p.page : 1; const savedProgress=progress(c),wird=options.wird; let focusVerse = null; const savedReading=source.verses[(wird?.progress?.reading?.verseId||savedProgress.reading?.verseId)-1]; if(!options.page&&!options.surah&&savedReading&&(options.resumeReading||p.remember_page&&(savedReading.page===page||savedProgress.reading?.page===legacyRememberedPage))){focusVerse=savedReading;page=focusVerse.page;} if (Number.isInteger(options.page) && source.pages[options.page]?.length) page = options.page; if (Number.isInteger(options.surah)) { focusVerse = source.verses.find(v => v.surah === options.surah && v.ayah === (options.ayah || 1)); if (focusVerse) page = focusVerse.page; } if(wird){ const point=wird.progress?.reading||wird.progress?.page; const v=source.verses[point?.verseId-1]; const startId=wird.startId||source.pages[wird.startPage][0].id,endId=wird.endId||source.pages[wird.endPage].at(-1).id; focusVerse=v&&v.id>=startId&&v.id<=endId?v:source.verses[startId-1];page=focusVerse.page; } root.replaceChildren(); root.style.setProperty('--quran-font-size', p.font_size + 'px'); applyTheme(c,p); let player = null, mounted = true, audioState = null, lastFollowed = null, actionVerse = null, actionOrigin = null; let audioAttempt = 0; cleanup = () => {saveAudio(player?.snapshot(),true);mounted=false;audioAttempt++;player?.dispose();window.removeEventListener('pagehide',flushAudio);document.removeEventListener('visibilitychange',flushAudio);}; const toolbar = node('div', null, 'quran-toolbar'); const navPanel = node('details', null, 'quran-navigator'); const navigatorTitle = node('summary', ui.navigate); const navigatorGrid = node('div', null, 'quran-navigator-grid'); navPanel.append(navigatorTitle, navigatorGrid); toolbar.append(navPanel); const field = (label, input) => { const wrap = node('label', label); wrap.append(input); navigatorGrid.append(wrap); return input; }; const select = (items, label) => { const input = node('select'); for (const [value, text] of items) { const option = node('option', text); option.value = value; input.append(option); } return field(label, input); }; const surahSelect = select(source.surahs.map(s => [s.id, s.id + '. ' + s.name]), ui.surah); const surahSearch = node('input');surahSearch.type='search';surahSearch.placeholder=ui.search_surah;surahSearch.setAttribute('aria-label',ui.search_surah); const surahSearchLabel=node('label',ui.search_surah);surahSearchLabel.append(surahSearch); navigatorGrid.insertBefore(surahSearchLabel,surahSelect.parentElement); surahSearch.addEventListener('input',()=>{ const term=normalize(surahSearch.value),keep=surahSelect.value; surahSelect.replaceChildren(); for(const sr of source.surahs){ const text=sr.id+'. '+sr.name; if(term&&!normalize(text).includes(term))continue; const option=node('option',text);option.value=sr.id;surahSelect.append(option); } if([...surahSelect.options].some(o=>o.value===keep))surahSelect.value=keep; status.textContent=surahSelect.options.length?'':ui.no_results; }); const juzSelect = select(Array.from({length: source.counts.juz}, (_, i) => [i + 1, i + 1]), ui.juz); const hizbSelect=select(source.partitions.hizb.map(r=>[r.id,r.id]),ui.hizb); const rubSelect=select(source.partitions.rub_el_hizb.map(r=>[r.id,r.id]),ui.rub_el_hizb); const position=node('p',null,'quran-meta');position.setAttribute('aria-live','off'); function positionText(v){return format(ui.partition_position,{juz:v.juz,hizb:v.hizb,rub:v.rub_el_hizb,page:v.page});} const pageForm = node('form'); const pageLabel = node('label', ui.page); const pageInput = node('input'); pageInput.type = 'number'; pageInput.min = 1; pageInput.max = source.counts.pages; pageInput.required = true; pageLabel.append(pageInput); pageForm.append(pageLabel); const go = node('button', ui.go); go.type = 'submit'; pageForm.append(go); navigatorGrid.append(pageForm); toolbar.append(button(ui.settings_short, () => openSettings(() => open({...options, page, surah: undefined, ayah: undefined})))); const searchForm = node('form', null, 'quran-search'); searchForm.setAttribute('role', 'search'); const query = node('input'); query.type = 'search'; query.setAttribute('aria-label', ui.search); query.placeholder = ui.search; const submit = node('button', ui.search_submit); submit.type = 'submit'; searchForm.append(query, submit); const status = node('p', null, 'quran-meta'); status.setAttribute('role', 'status'); const content = node('div',null,'quran-pages'); const audioHost=node('div'); const actions=node('section',null,'quran-ayah-actions');actions.hidden=true;actions.setAttribute('aria-label',ui.verse_actions); const actionTitle=node('h3'), actionStatus=node('p');actionStatus.setAttribute('role','status'); const listen=button(ui.listen,()=>{if(player&&actionVerse){player.playAt(actionVerse.id);actions.hidden=true;actionOrigin?.focus();}}); listen.disabled=true; const copy=button(ui.copy,async()=>{ if(!actionVerse)return; const text=actionVerse.text+'\n['+source.surahs[actionVerse.surah-1].name+': '+actionVerse.ayah+']'; try{await navigator.clipboard.writeText(text);actionStatus.textContent=ui.copied;} catch{ const input=node('textarea',text);input.readOnly=true;input.setAttribute('aria-label',ui.copy); actionStatus.replaceChildren(node('span',ui.copy_failed),input);input.focus();input.select(); } }); const bookmark=button(ui.bookmark,()=>{ if(!actionVerse)return; const point={verseId:actionVerse.id,page:actionVerse.page,at:Date.now()}; const ok=saveProgress(c,'reading',point); const planOK=window.MuslimClockKhatma?.savePosition(wird,'reading',point)!==false; actionStatus.textContent=ok&&planOK?ui.bookmark_saved:ui.progress_error;updateBookmarks(); }); const linkGroups=[ {key:'tafsir',items:c.tafsirs,note:'tafsir_note'}, {key:'asbab',items:c.asbab_nuzul,note:'asbab_note'}, {key:'meanings',items:c.word_meanings,note:'meanings_note'} ].filter(g=>Array.isArray(g.items)&&g.items.length).map(g=>{ const box=node('details',null,'quran-link-group'),title=node('summary',ui[g.key]),list=node('ul',null,'quran-link-list'); box.append(title,node('p',ui[g.note],'quran-meta'),list);return {...g,box,list}; }); actions.append(actionTitle,listen,copy,bookmark,...linkGroups.map(g=>g.box),button(ui.close_actions,()=>{actions.hidden=true;actionOrigin?.focus();}),actionStatus); function fillLinks(verse){ for(const g of linkGroups){ g.list.replaceChildren();g.box.open=false; for(const item of g.items){ try{ const url=new URL(format(item.url,{surah:verse.surah,ayah:verse.ayah}));if(url.protocol!=='https:')continue; const li=node('li'),link=node('a',item.name+' ↗');link.href=url.href;link.target='_blank';link.rel='noopener noreferrer'; link.setAttribute('aria-label',ui[g.key]+' — '+source.surahs[verse.surah-1].name+' '+ui.ayah+' '+verse.ayah+' — '+item.name+' — '+ui.external_link); link.addEventListener('click',()=>player?.pause());li.append(link);g.list.append(li); }catch{} } } } function selectVerse(verse, origin) { actionVerse=verse;actionOrigin=origin; fillLinks(verse); actionTitle.textContent=source.surahs[verse.surah-1].name+' · '+ui.ayah+' '+verse.ayah+' · '+positionText(verse); actionStatus.replaceChildren();actions.hidden=false;(listen.disabled?copy:listen).focus(); } const nav = node('div', null, 'quran-nav'); const previous = button(ui.previous, () => showPage(page - 1)); const next = button(ui.next, () => showPage(page + 1)); nav.append(previous, next); const attribution = node('a', ui.source); attribution.href = new URL(c.source_file, base).href; attribution.target = '_blank'; attribution.rel = 'noopener'; attribution.className = 'quran-meta'; const bookmarks=node('div',null,'quran-bookmarks'),readingStatus=node('p',null,'quran-meta'),audioStatus=node('p',null,'quran-meta'); const resumeReading=button(ui.resume_reading,()=>{const point=progress(c).reading,v=source.verses[point?.verseId-1];if(v)showPage(v.page,v);}); const resumeAudio=button(ui.resume_audio,()=>{const point=progress(c).audio,v=source.verses[point?.verseId-1];if(v&&player){showPage(v.page,v);player.resumeFrom(point);}}); const planButton=button(ui.khatma,()=>{player?.pause();if(window.MuslimClockKhatma)window.MuslimClockKhatma.show({back:()=>open({...options,page})});else status.textContent=ui.quran_features_error;}); bookmarks.append(readingStatus,resumeReading,audioStatus,resumeAudio,planButton); if(wird){ bookmarks.append(node('p',wird.startId?format(ui.wird_exact,{day:wird.dayIndex+1,start:source.verses[wird.startId-1].verse_key,end:source.verses[wird.endId-1].verse_key,first:wird.startPage,last:wird.endPage}):format(ui.wird_range,{day:wird.dayIndex+1,start:wird.startPage,end:wird.endPage}))); const point=wird.progress?.audio; if(point)bookmarks.append(button(ui.wird_resume_audio,()=>{const v=source.verses[point.verseId-1];if(v&&player){showPage(v.page,v);player.resumeFrom(point);}})); } const metadataLink=node('a',ui.partition_source);metadataLink.href=source.partitions.sources[0].url;metadataLink.target='_blank';metadataLink.rel='noopener noreferrer';metadataLink.className='quran-meta';navigatorGrid.append(metadataLink); const swipeHint=node('p',ui.swipe_hint,'quran-meta');swipeHint.hidden=!p.show_swipe_hint;position.hidden=!p.show_partition_status; root.append(toolbar,position,bookmarks,audioHost,searchForm,status,actions,swipeHint,content,nav,attribution); function updateBookmarks(){ const saved=progress(c),v=source.verses[saved.reading?.verseId-1],a=source.verses[saved.audio?.verseId-1]; readingStatus.textContent=v?format(ui.reading_position,{surah:source.surahs[v.surah-1].name,ayah:v.ayah,page:v.page}):format(ui.saved_page,{page:p.page}); resumeReading.hidden=!v; audioStatus.textContent=a?format(ui.audio_position,{surah:source.surahs[a.surah-1].name,ayah:a.ayah,seconds:Math.floor(saved.audio.time||0)}):''; resumeAudio.hidden=!a;resumeAudio.disabled=!player; } let lastSavedAudio=''; function saveAudio(state,force=false){ if(!state?.played||!['playing','paused','ended','loading'].includes(state.phase))return; const signature=[state.verse.id,state.settings.reciter,Math.floor(state.currentTime/2)].join(':'); if(!force&&signature===lastSavedAudio)return; const point={verseId:state.verse.id,page:state.verse.page,time:Math.max(0,state.currentTime||0),reciter:state.settings.reciter,rate:state.settings.rate,at:Date.now()}; const ok=saveProgress(c,'audio',point),planOK=window.MuslimClockKhatma?.savePosition(wird,'audio',point)!==false; if(ok&&planOK)lastSavedAudio=signature;else status.textContent=ui.progress_error; updateBookmarks(); } function flushAudio(){saveAudio(player?.snapshot(),true);} window.addEventListener('pagehide',flushAudio);document.addEventListener('visibilitychange',flushAudio); let touch=null,suppressClickUntil=0; content.addEventListener('touchstart',e=>{touch=e.touches.length===1?{x:e.touches[0].clientX,y:e.touches[0].clientY,at:Date.now()}:null;},{passive:true}); content.addEventListener('touchcancel',()=>{touch=null;},{passive:true}); content.addEventListener('touchmove',e=>{if(e.touches.length!==1)touch=null;},{passive:true}); content.addEventListener('touchend',e=>{ const start=touch;touch=null;if(!start||e.changedTouches.length!==1||!content.querySelector('.quran-page-text')||String(window.getSelection?.()||'').trim())return; const dx=e.changedTouches[0].clientX-start.x,dy=e.changedTouches[0].clientY-start.y,rule=c.swipe; if(!rule||Date.now()-start.at>rule.max_duration_ms||Math.abs(dx)0?1:-1);if(!source.pages[target]?.length)return; suppressClickUntil=Date.now()+rule.click_suppression_ms;showPage(target); },{passive:true}); content.addEventListener('click',e=>{if(Date.now()mounted&&attempt===audioAttempt&&root.isConnected,onState:audioChanged }); if(!mounted||attempt!==audioAttempt){nextPlayer?.dispose();return;} player=nextPlayer;listen.disabled=!player;updateBookmarks(); } catch(error) { if(!mounted||attempt!==audioAttempt)return; audioHost.replaceChildren(node('p',ui.audio_unavailable,'quran-meta'),button(ui.retry,mountAudio)); } } function showPage(value, selectedVerse, fromAudio=false) { if (!Number.isInteger(value) || !source.pages[value]?.length) { status.textContent = format(ui.invalid_page, {total:source.counts.pages}); return; } page = value; const verses = source.pages[page]; content.replaceChildren(); nav.hidden = false;actions.hidden=true; pageInput.value = page; surahSelect.value = selectedVerse?.surah || verses[0].surah; juzSelect.value = selectedVerse?.juz || verses[0].juz; const currentVerse=selectedVerse||verses[0];hizbSelect.value=currentVerse.hizb;rubSelect.value=currentVerse.rub_el_hizb;position.textContent=positionText(currentVerse); const juz = [...new Set(verses.map(v => v.juz))].join('، '); status.textContent = format(ui.page_status, {page, total: source.counts.pages, juz}); previous.disabled = page === 1; next.disabled = page === source.counts.pages; let lastSurah, paragraph; for (const verse of verses) { if (verse.surah !== lastSurah) { content.append(node('h3', source.surahs[verse.surah - 1].name, 'quran-page-title')); if (verse.ayah === 1 && verse.surah !== 1 && verse.surah !== 9) content.append(node('p', source.verses[0].text, 'quran-basmala')); paragraph = node('p', null, 'quran-page-text'); content.append(paragraph); lastSurah = verse.surah; } const span = node('span', verse.text, 'quran-verse'); span.id = 'quran-verse-' + verse.id; span.tabIndex = 0;span.setAttribute('role','button'); span.setAttribute('aria-label',verse.text+' — '+ui.ayah+' '+verse.ayah+' — '+ui.verse_actions);span.setAttribute('aria-haspopup','true'); span.addEventListener('click',()=>selectVerse(verse,span)); span.addEventListener('keydown',event=>{if(event.key==='Enter'||event.key===' '){event.preventDefault();selectVerse(verse,span);}}); const number = node('span', ' (' + verse.ayah + ') ', 'quran-verse-number'); number.setAttribute('aria-label', ui.ayah + ' ' + verse.ayah); span.append(number); paragraph.append(span, document.createTextNode(' ')); } if (p.remember_page&&!fromAudio) { p.page = page; if (!persist(c)) status.textContent += ' ' + ui.storage_error; } if(!fromAudio&&wird&&window.MuslimClockKhatma?.savePosition(wird,'page',{page,verseId:selectedVerse?.id||verses[0].id})===false)status.textContent=ui.progress_error; if(!fromAudio){document.getElementById('adaptivePopupBody').scrollTop = 0;player?.select(selectedVerse?.id||verses[0].id);} paintPlaying();updateBookmarks(); if (selectedVerse) { const target = document.getElementById('quran-verse-' + selectedVerse.id); target?.focus({preventScroll: true}); target?.scrollIntoView({block:'center'}); } } pageForm.addEventListener('submit', event => { event.preventDefault(); showPage(Number(pageInput.value)); }); surahSelect.addEventListener('change', () => { const verse = source.verses.find(v => v.surah === Number(surahSelect.value)); showPage(verse.page, verse); }); juzSelect.addEventListener('change', () => { const verse = source.verses.find(v => v.juz === Number(juzSelect.value)); showPage(verse.page, verse); }); for(const [input,kind] of [[hizbSelect,'hizb'],[rubSelect,'rub_el_hizb']])input.addEventListener('change',()=>{const v=source.byKey.get(source.partitions[kind][Number(input.value)-1].start);showPage(v.page,v);}); searchForm.addEventListener('submit', event => { event.preventDefault(); const text = normalize(query.value); if (!text) { status.textContent = ui.search_empty; query.focus(); return; } const matches = source.verses.filter((_, i) => source.searchIndex[i].includes(text)); player?.pause();actions.hidden=true; content.replaceChildren(); nav.hidden = true; status.textContent = matches.length ? format(ui.results, {count: matches.length}) : ui.no_results; content.append(button(ui.reader, () => showPage(page))); let shown = 0; const more = button(ui.more, appendResults); function appendResults() { more.remove(); for (const verse of matches.slice(shown, shown + c.limits.search_batch)) { const result = button(source.surahs[verse.surah - 1].name + ' · ' + ui.ayah + ' ' + verse.ayah + ' · ' + ui.page + ' ' + verse.page, () => showPage(verse.page, verse)); result.className = 'quran-result'; result.append(node('span', verse.text, 'quran-result-text')); content.append(result); } shown += c.limits.search_batch; if (shown < matches.length) content.append(more); } appendResults(); }); showPage(page, focusVerse); if(options.khatmaUnavailable)status.textContent=ui.quran_features_error; mountAudio(); } async function openSettings(back) { const request = ++epoch; let c; try { c = await config(); } catch { return open({back}); } if (request !== epoch) return; const ui = c.ui, p = prefs(c), root = node('div', null, 'quran-settings'); const label = node('label', ui.font_size), font = node('input'), output = node('output', p.font_size); font.type = 'range'; font.min = c.limits.font_min; font.max = c.limits.font_max; font.step = 1; font.value = p.font_size; label.append(font, output); font.addEventListener('input', () => { output.value = font.value; }); const rememberLabel = node('label', ui.remember_page), remember = node('input'); remember.type = 'checkbox'; remember.checked = p.remember_page; rememberLabel.prepend(remember); const status = node('p'); status.setAttribute('role', 'status'); const theme=themeField(c,p,value=>{p.theme=value;applyTheme(c,p);}); const followLabel=node('label',ui.follow_audio),follow=node('input');follow.type='checkbox';follow.checked=p.follow_audio;followLabel.prepend(follow); const partitionLabel=node('label',ui.show_partition_status),partitionToggle=node('input');partitionToggle.type='checkbox';partitionToggle.checked=p.show_partition_status;partitionLabel.prepend(partitionToggle); const swipeLabel=node('label',ui.show_swipe_hint),swipeToggle=node('input');swipeToggle.type='checkbox';swipeToggle.checked=p.show_swipe_hint;swipeLabel.prepend(swipeToggle); root.append(label, rememberLabel, theme, followLabel, partitionLabel, swipeLabel, button(ui.save, () => { p.font_size = Number(font.value); p.remember_page = remember.checked; p.follow_audio=follow.checked; p.show_partition_status=partitionToggle.checked; p.show_swipe_hint=swipeToggle.checked; if (!p.remember_page) p.page = 1; status.textContent = persist(c) ? ui.saved : ui.storage_error; }), status); panel(ui.settings, root, back, ui); applyTheme(c,p); } window.addEventListener('muslim-clock-popup-leave', () => { epoch++;cleanup();cleanup=()=>{}; const card=document.getElementById('adaptivePopupCard'); if(card){delete card.dataset.quranTheme;for(const key of ['bg','fg','surface','border','highlight','highlight_fg'])card.style.removeProperty('--quran-'+key);} }); window.addEventListener('muslim-clock-quran-open', event => open(event.detail || {})); window.MuslimClockQuran = Object.freeze({open, openSettings, loadData:async()=>data(await config()), mountEntry: (parent, back) => mount(parent, back, false), mountSettingsEntry: (parent, back) => mount(parent, back, true)}); })();