Spaces:
Running
Running
Upload 7 files
Browse files- quran_audio.js +52 -21
- quran_audio_registry.json +105 -15
- quran_khatma.js +47 -29
- quran_khatma_registry.json +12 -2
- quran_reader.css +22 -3
- quran_reader.js +37 -15
- quran_reader_registry.json +93 -7
quran_audio.js
CHANGED
|
@@ -32,7 +32,7 @@
|
|
| 32 |
})().catch(error => {configPromise = null; throw error;});
|
| 33 |
return configPromise;
|
| 34 |
}
|
| 35 |
-
function createPlayer(c, verses, notify) {
|
| 36 |
const byId = new Map(verses.map((v, index) => [v.id, index]));
|
| 37 |
let saved;
|
| 38 |
try {saved = JSON.parse(localStorage.getItem(c.storage_key));} catch { /* Preferences are optional. */ }
|
|
@@ -40,15 +40,20 @@
|
|
| 40 |
reciter: c.reciters.some(r => r.id === saved?.reciter) ? saved.reciter : c.defaults.reciter,
|
| 41 |
rate: c.rates.includes(saved?.rate) ? saved.rate : c.defaults.rate,
|
| 42 |
repeat: c.repeat_options.some(r => r.value === saved?.repeat) ? saved.repeat : c.defaults.repeat,
|
| 43 |
-
advance: typeof saved?.advance === 'boolean' ? saved.advance : c.defaults.advance
|
|
|
|
| 44 |
};
|
| 45 |
let index = 0, media = null, token = 0, playRequest = 0, disposed = false, wantsPlay = false, repetitions = settings.repeat;
|
| 46 |
let phase = 'idle', currentTime = 0, duration = 0, loadTimer, removeEvents = () => {};
|
| 47 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 48 |
const emit = () => {if (!disposed) notify(snapshot());};
|
| 49 |
const clearTimer = () => {clearTimeout(loadTimer);loadTimer = null;};
|
| 50 |
function detach(release = false) {
|
| 51 |
-
clearTimer(); token++; playRequest++; wantsPlay = false;
|
| 52 |
removeEvents(); removeEvents = () => {};
|
| 53 |
if (media) {media.pause(); media.removeAttribute('src'); media.load(); if (release) media = null;}
|
| 54 |
}
|
|
@@ -78,7 +83,7 @@
|
|
| 78 |
}
|
| 79 |
function playAt(id, resetRepeat = true) {
|
| 80 |
if (disposed || !byId.has(id)) return;
|
| 81 |
-
detach(); index = byId.get(id); currentTime = 0; duration = 0;
|
| 82 |
if (resetRepeat) repetitions = settings.repeat;
|
| 83 |
// Reuse the element unlocked by the user's first play gesture.
|
| 84 |
const activeMedia = media || new Audio(); media = activeMedia;
|
|
@@ -103,18 +108,26 @@
|
|
| 103 |
if (activeMedia.ended || !activeMedia.paused) return;
|
| 104 |
if (phase === 'playing') {wantsPlay = false; clearTimer(); phase = 'paused'; emit();}
|
| 105 |
});
|
| 106 |
-
on('loadedmetadata', () => {
|
|
|
|
|
|
|
|
|
|
|
|
|
| 107 |
on('durationchange', () => {duration = Number.isFinite(activeMedia.duration) ? activeMedia.duration : 0; emit();});
|
| 108 |
on('timeupdate', () => {currentTime = activeMedia.currentTime || 0; emit();});
|
| 109 |
on('error', () => {if (activeMedia.error) fail();});
|
| 110 |
on('ended', () => {
|
| 111 |
if (!wantsPlay || !activeMedia.ended) return;
|
| 112 |
clearTimer();
|
|
|
|
| 113 |
if (repetitions === 0 || repetitions > 1) {
|
| 114 |
if (repetitions > 1) repetitions--;
|
| 115 |
-
|
| 116 |
-
} else if (settings.advance && index <
|
| 117 |
-
|
|
|
|
|
|
|
|
|
|
| 118 |
});
|
| 119 |
try {activeMedia.src = urlFor(verses[index]); startMedia();} catch {fail();}
|
| 120 |
}
|
|
@@ -124,11 +137,12 @@
|
|
| 124 |
}
|
| 125 |
function pause() {
|
| 126 |
if (!media || disposed) return;
|
| 127 |
-
wantsPlay = false; playRequest++; clearTimer(); media.pause(); phase = 'paused'; emit();
|
| 128 |
}
|
| 129 |
function stop() {detach(); phase = 'stopped'; currentTime = 0; duration = 0; emit();}
|
| 130 |
function toggle() {
|
| 131 |
if (phase === 'playing' || phase === 'loading') pause();
|
|
|
|
| 132 |
else if (media && ['paused','blocked'].includes(phase)) startMedia();
|
| 133 |
else playAt(verses[index].id);
|
| 134 |
}
|
|
@@ -138,6 +152,7 @@
|
|
| 138 |
if (c.rates.includes(patch.rate)) settings.rate = patch.rate;
|
| 139 |
if (c.repeat_options.some(r => r.value === patch.repeat)) {settings.repeat = patch.repeat;repetitions = patch.repeat;}
|
| 140 |
if (typeof patch.advance === 'boolean') settings.advance = patch.advance;
|
|
|
|
| 141 |
if (oldReciter !== settings.reciter) {stop(); if (running) playAt(verses[index].id);}
|
| 142 |
if (media) media.playbackRate = settings.rate;
|
| 143 |
let stored = true;
|
|
@@ -149,13 +164,19 @@
|
|
| 149 |
try {media.currentTime = Math.min(duration, Math.max(0, seconds));currentTime = media.currentTime;emit();} catch { /* Metadata may be changing. */ }
|
| 150 |
}
|
| 151 |
}
|
| 152 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 153 |
playContinuous: () => {configure({advance:true,repeat:1});playAt(verses[index].id);},
|
| 154 |
previous: () => {if (index > 0) playAt(verses[index - 1].id);},
|
| 155 |
-
next: () => {if (index <
|
| 156 |
dispose: () => {disposed = true;detach(true);}};
|
| 157 |
}
|
| 158 |
-
async function mount(host, {verses, surahs, initialId, onState, isActive = () => host.isConnected}) {
|
| 159 |
const c = await configuration();
|
| 160 |
if (!isActive()) return null;
|
| 161 |
const ui = c.ui, wrap = element('section', null, 'quran-audio'); wrap.setAttribute('aria-label',ui.title);
|
|
@@ -171,34 +192,44 @@
|
|
| 171 |
const reciter = select(ui.reciter,c.reciters.map(r=>[r.id,r.name]),value=>set({reciter:value}));
|
| 172 |
const rate = select(ui.rate,c.rates.map(r=>[r,r+'×']),value=>set({rate:Number(value)}));
|
| 173 |
const repeat = select(ui.repeat,c.repeat_options.map(r=>[r.value,r.label]),value=>set({repeat:Number(value)}));
|
|
|
|
| 174 |
const advanceLabel=element('label',ui.advance), advance=element('input');advance.type='checkbox';
|
| 175 |
advance.addEventListener('change',()=>set({advance:advance.checked}));advanceLabel.prepend(advance);
|
| 176 |
const actions=element('div',null,'quran-audio-actions');
|
| 177 |
const previous=button(ui.previous,()=>player.previous()), play=button(ui.play,()=>player.toggle());
|
| 178 |
const stop=button(ui.stop,()=>player.stop()), next=button(ui.next,()=>player.next());actions.append(previous,play,stop,next);
|
| 179 |
const continuous=button(ui.play_continuous,()=>player.playContinuous());
|
| 180 |
-
const current=element('p',null,'quran-meta'), state=element('p',null,'quran-meta');
|
|
|
|
|
|
|
| 181 |
const seekLabel=element('label',ui.seek), seek=element('input');seek.type='range';seek.min=0;seek.max=0;seek.step=0.1;seek.value=0;seek.disabled=true;
|
| 182 |
seek.addEventListener('change',()=>player.seek(Number(seek.value)));seekLabel.append(seek);
|
| 183 |
-
const clock=element('output');clock.dir='ltr';
|
| 184 |
-
const time=s=>Math.floor(s/60)+':'+String(Math.floor(s%60)).padStart(2,'0');
|
| 185 |
const source=element('a',ui.source);source.href=c.source.url;source.target='_blank';source.rel='noopener noreferrer';source.className='quran-meta';
|
| 186 |
let lastStatus='';
|
| 187 |
player=createPlayer(c,verses,s=>{
|
| 188 |
if (!isActive()) return;
|
| 189 |
reciter.value=s.settings.reciter;rate.value=s.settings.rate;repeat.value=s.settings.repeat;advance.checked=s.settings.advance;
|
| 190 |
-
|
| 191 |
-
if (
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 192 |
play.textContent=['playing','loading'].includes(s.phase)?ui.pause:s.phase==='paused'?ui.resume:ui.play;
|
| 193 |
previous.disabled=s.verse.id===verses[0].id;next.disabled=s.verse.id===verses[verses.length-1].id;
|
| 194 |
seek.max=s.duration;seek.disabled=s.duration<=0;
|
| 195 |
if (document.activeElement!==seek) seek.value=s.currentTime;
|
| 196 |
-
|
|
|
|
| 197 |
onState(s);
|
| 198 |
-
});
|
| 199 |
const options=element('details'),summary=element('summary',ui.options);
|
| 200 |
options.append(summary,controls,source);
|
| 201 |
-
wrap.append(current,continuous,actions,advanceLabel,seekLabel,clock,state,options,storageStatus);host.replaceChildren(wrap);
|
| 202 |
player.select(initialId);
|
| 203 |
// Initialize controls even when the requested verse is already the first one.
|
| 204 |
player.configure({});
|
|
|
|
| 32 |
})().catch(error => {configPromise = null; throw error;});
|
| 33 |
return configPromise;
|
| 34 |
}
|
| 35 |
+
function createPlayer(c, verses, notify, endId) {
|
| 36 |
const byId = new Map(verses.map((v, index) => [v.id, index]));
|
| 37 |
let saved;
|
| 38 |
try {saved = JSON.parse(localStorage.getItem(c.storage_key));} catch { /* Preferences are optional. */ }
|
|
|
|
| 40 |
reciter: c.reciters.some(r => r.id === saved?.reciter) ? saved.reciter : c.defaults.reciter,
|
| 41 |
rate: c.rates.includes(saved?.rate) ? saved.rate : c.defaults.rate,
|
| 42 |
repeat: c.repeat_options.some(r => r.value === saved?.repeat) ? saved.repeat : c.defaults.repeat,
|
| 43 |
+
advance: typeof saved?.advance === 'boolean' ? saved.advance : c.defaults.advance,
|
| 44 |
+
gap: (c.gap_options || []).some(g => g.value === saved?.gap) ? saved.gap : (c.defaults.gap_ms ?? 250)
|
| 45 |
};
|
| 46 |
let index = 0, media = null, token = 0, playRequest = 0, disposed = false, wantsPlay = false, repetitions = settings.repeat;
|
| 47 |
let phase = 'idle', currentTime = 0, duration = 0, loadTimer, removeEvents = () => {};
|
| 48 |
+
// played: the person actually started playback (used by the reader to save a resume point).
|
| 49 |
+
let played = false, gapTimer = null, pending = null, seekOnLoad = 0;
|
| 50 |
+
const lastIndex = Number.isInteger(byId.get(endId)) ? byId.get(endId) : verses.length - 1;
|
| 51 |
+
const gapMs = () => Number.isFinite(settings.gap) ? settings.gap : 250;
|
| 52 |
+
const snapshot = () => ({verse:verses[index], phase, currentTime, duration, played, settings:{...settings}});
|
| 53 |
const emit = () => {if (!disposed) notify(snapshot());};
|
| 54 |
const clearTimer = () => {clearTimeout(loadTimer);loadTimer = null;};
|
| 55 |
function detach(release = false) {
|
| 56 |
+
clearTimer(); clearTimeout(gapTimer); gapTimer = null; pending = null; token++; playRequest++; wantsPlay = false;
|
| 57 |
removeEvents(); removeEvents = () => {};
|
| 58 |
if (media) {media.pause(); media.removeAttribute('src'); media.load(); if (release) media = null;}
|
| 59 |
}
|
|
|
|
| 83 |
}
|
| 84 |
function playAt(id, resetRepeat = true) {
|
| 85 |
if (disposed || !byId.has(id)) return;
|
| 86 |
+
detach(); index = byId.get(id); currentTime = 0; duration = 0; played = true;
|
| 87 |
if (resetRepeat) repetitions = settings.repeat;
|
| 88 |
// Reuse the element unlocked by the user's first play gesture.
|
| 89 |
const activeMedia = media || new Audio(); media = activeMedia;
|
|
|
|
| 108 |
if (activeMedia.ended || !activeMedia.paused) return;
|
| 109 |
if (phase === 'playing') {wantsPlay = false; clearTimer(); phase = 'paused'; emit();}
|
| 110 |
});
|
| 111 |
+
on('loadedmetadata', () => {
|
| 112 |
+
duration = Number.isFinite(activeMedia.duration) ? activeMedia.duration : 0;
|
| 113 |
+
if (seekOnLoad > 0 && seekOnLoad < duration - 0.5) {try {activeMedia.currentTime = seekOnLoad;} catch { /* ignore */ }}
|
| 114 |
+
seekOnLoad = 0; emit();
|
| 115 |
+
});
|
| 116 |
on('durationchange', () => {duration = Number.isFinite(activeMedia.duration) ? activeMedia.duration : 0; emit();});
|
| 117 |
on('timeupdate', () => {currentTime = activeMedia.currentTime || 0; emit();});
|
| 118 |
on('error', () => {if (activeMedia.error) fail();});
|
| 119 |
on('ended', () => {
|
| 120 |
if (!wantsPlay || !activeMedia.ended) return;
|
| 121 |
clearTimer();
|
| 122 |
+
let step = null;
|
| 123 |
if (repetitions === 0 || repetitions > 1) {
|
| 124 |
if (repetitions > 1) repetitions--;
|
| 125 |
+
step = {id: verses[index].id, reset: false};
|
| 126 |
+
} else if (settings.advance && index < lastIndex) step = {id: verses[index + 1].id, reset: true};
|
| 127 |
+
if (!step) {wantsPlay = false; phase = 'ended'; emit(); return;}
|
| 128 |
+
const go = () => {gapTimer = null; pending = null; if (!disposed) playAt(step.id, step.reset);};
|
| 129 |
+
if (gapMs() <= 0) go();
|
| 130 |
+
else {pending = step; gapTimer = setTimeout(go, gapMs());}
|
| 131 |
});
|
| 132 |
try {activeMedia.src = urlFor(verses[index]); startMedia();} catch {fail();}
|
| 133 |
}
|
|
|
|
| 137 |
}
|
| 138 |
function pause() {
|
| 139 |
if (!media || disposed) return;
|
| 140 |
+
wantsPlay = false; playRequest++; clearTimer(); clearTimeout(gapTimer); gapTimer = null; media.pause(); phase = 'paused'; emit();
|
| 141 |
}
|
| 142 |
function stop() {detach(); phase = 'stopped'; currentTime = 0; duration = 0; emit();}
|
| 143 |
function toggle() {
|
| 144 |
if (phase === 'playing' || phase === 'loading') pause();
|
| 145 |
+
else if (pending && phase === 'paused') {const step = pending; pending = null; playAt(step.id, step.reset);}
|
| 146 |
else if (media && ['paused','blocked'].includes(phase)) startMedia();
|
| 147 |
else playAt(verses[index].id);
|
| 148 |
}
|
|
|
|
| 152 |
if (c.rates.includes(patch.rate)) settings.rate = patch.rate;
|
| 153 |
if (c.repeat_options.some(r => r.value === patch.repeat)) {settings.repeat = patch.repeat;repetitions = patch.repeat;}
|
| 154 |
if (typeof patch.advance === 'boolean') settings.advance = patch.advance;
|
| 155 |
+
if ((c.gap_options || []).some(g => g.value === patch.gap)) settings.gap = patch.gap;
|
| 156 |
if (oldReciter !== settings.reciter) {stop(); if (running) playAt(verses[index].id);}
|
| 157 |
if (media) media.playbackRate = settings.rate;
|
| 158 |
let stored = true;
|
|
|
|
| 164 |
try {media.currentTime = Math.min(duration, Math.max(0, seconds));currentTime = media.currentTime;emit();} catch { /* Metadata may be changing. */ }
|
| 165 |
}
|
| 166 |
}
|
| 167 |
+
function resumeFrom(point) {
|
| 168 |
+
if (!point || !byId.has(point.verseId)) return;
|
| 169 |
+
if (c.reciters.some(r => r.id === point.reciter) && point.reciter !== settings.reciter) configure({reciter:point.reciter});
|
| 170 |
+
playAt(point.verseId);
|
| 171 |
+
seekOnLoad = Number.isFinite(point.time) ? point.time : 0;
|
| 172 |
+
}
|
| 173 |
+
return {playAt, select, pause, stop, toggle, configure, seek, snapshot, resumeFrom,
|
| 174 |
playContinuous: () => {configure({advance:true,repeat:1});playAt(verses[index].id);},
|
| 175 |
previous: () => {if (index > 0) playAt(verses[index - 1].id);},
|
| 176 |
+
next: () => {if (index < lastIndex) playAt(verses[index + 1].id);},
|
| 177 |
dispose: () => {disposed = true;detach(true);}};
|
| 178 |
}
|
| 179 |
+
async function mount(host, {verses, surahs, initialId, endId, onState, isActive = () => host.isConnected}) {
|
| 180 |
const c = await configuration();
|
| 181 |
if (!isActive()) return null;
|
| 182 |
const ui = c.ui, wrap = element('section', null, 'quran-audio'); wrap.setAttribute('aria-label',ui.title);
|
|
|
|
| 192 |
const reciter = select(ui.reciter,c.reciters.map(r=>[r.id,r.name]),value=>set({reciter:value}));
|
| 193 |
const rate = select(ui.rate,c.rates.map(r=>[r,r+'×']),value=>set({rate:Number(value)}));
|
| 194 |
const repeat = select(ui.repeat,c.repeat_options.map(r=>[r.value,r.label]),value=>set({repeat:Number(value)}));
|
| 195 |
+
const gap=select(ui.gap||'الفاصل بين الآيات',(c.gap_options||[{value:250,label:'ربع ثانية'}]).map(g=>[g.value,g.label]),value=>set({gap:Number(value)}));
|
| 196 |
const advanceLabel=element('label',ui.advance), advance=element('input');advance.type='checkbox';
|
| 197 |
advance.addEventListener('change',()=>set({advance:advance.checked}));advanceLabel.prepend(advance);
|
| 198 |
const actions=element('div',null,'quran-audio-actions');
|
| 199 |
const previous=button(ui.previous,()=>player.previous()), play=button(ui.play,()=>player.toggle());
|
| 200 |
const stop=button(ui.stop,()=>player.stop()), next=button(ui.next,()=>player.next());actions.append(previous,play,stop,next);
|
| 201 |
const continuous=button(ui.play_continuous,()=>player.playContinuous());
|
| 202 |
+
const current=element('p',null,'quran-meta'), state=element('p',null,'quran-meta');
|
| 203 |
+
state.setAttribute('aria-live','off');
|
| 204 |
+
const announcer=element('p',null,'quran-sr-only');announcer.setAttribute('role','status');announcer.setAttribute('aria-live','polite');announcer.setAttribute('aria-atomic','true');
|
| 205 |
const seekLabel=element('label',ui.seek), seek=element('input');seek.type='range';seek.min=0;seek.max=0;seek.step=0.1;seek.value=0;seek.disabled=true;
|
| 206 |
seek.addEventListener('change',()=>player.seek(Number(seek.value)));seekLabel.append(seek);
|
| 207 |
+
const clock=element('output');clock.dir='ltr';clock.setAttribute('role','timer');clock.setAttribute('aria-live','off');
|
| 208 |
+
const time=s=>(s=Number.isFinite(s)?s:0,Math.floor(s/60))+':'+String(Math.floor(s%60)).padStart(2,'0');
|
| 209 |
const source=element('a',ui.source);source.href=c.source.url;source.target='_blank';source.rel='noopener noreferrer';source.className='quran-meta';
|
| 210 |
let lastStatus='';
|
| 211 |
player=createPlayer(c,verses,s=>{
|
| 212 |
if (!isActive()) return;
|
| 213 |
reciter.value=s.settings.reciter;rate.value=s.settings.rate;repeat.value=s.settings.repeat;advance.checked=s.settings.advance;
|
| 214 |
+
const label=interpolate(ui.current,{surah:surahs[s.verse.surah-1].name,ayah:s.verse.ayah});
|
| 215 |
+
if (current.textContent!==label) current.textContent=label;
|
| 216 |
+
gap.value=s.settings.gap;
|
| 217 |
+
if (lastStatus !== s.phase) {
|
| 218 |
+
state.textContent=ui[s.phase]||ui.idle;lastStatus=s.phase;
|
| 219 |
+
// Spoken only for states the person must know about; not on every verse change.
|
| 220 |
+
announcer.textContent=['paused','stopped','ended','error','blocked'].includes(s.phase)?(ui[s.phase]||''):'';
|
| 221 |
+
}
|
| 222 |
play.textContent=['playing','loading'].includes(s.phase)?ui.pause:s.phase==='paused'?ui.resume:ui.play;
|
| 223 |
previous.disabled=s.verse.id===verses[0].id;next.disabled=s.verse.id===verses[verses.length-1].id;
|
| 224 |
seek.max=s.duration;seek.disabled=s.duration<=0;
|
| 225 |
if (document.activeElement!==seek) seek.value=s.currentTime;
|
| 226 |
+
const clockText=time(s.currentTime)+' / '+time(s.duration);
|
| 227 |
+
clock.textContent=clockText;seek.setAttribute('aria-valuetext',clockText);
|
| 228 |
onState(s);
|
| 229 |
+
},endId);
|
| 230 |
const options=element('details'),summary=element('summary',ui.options);
|
| 231 |
options.append(summary,controls,source);
|
| 232 |
+
wrap.append(current,continuous,actions,advanceLabel,seekLabel,clock,state,announcer,options,storageStatus);host.replaceChildren(wrap);
|
| 233 |
player.select(initialId);
|
| 234 |
// Initialize controls even when the requested verse is already the first one.
|
| 235 |
player.configure({});
|
quran_audio_registry.json
CHANGED
|
@@ -11,19 +11,86 @@
|
|
| 11 |
"note": "Verse filenames use three digits for the surah and three for the ayah. Recordings are linked, not redistributed or licensed as part of AL-Khatma."
|
| 12 |
},
|
| 13 |
"reciters": [
|
| 14 |
-
{
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
{
|
| 20 |
-
|
| 21 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 22 |
],
|
| 23 |
-
"defaults": {
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 27 |
"ui": {
|
| 28 |
"title": "الاستماع",
|
| 29 |
"options": "القارئ وخيارات التلاوة",
|
|
@@ -49,6 +116,29 @@
|
|
| 49 |
"error": "تعذّر تشغيل المقطع. تحقق من الإنترنت أو اختر قارئًا آخر ثم اضغط تشغيل.",
|
| 50 |
"blocked": "اضغط تشغيل لاستكمال التلاوة؛ المتصفح يحتاج ضغطة منك.",
|
| 51 |
"storage_error": "تعذّر حفظ تفضيلات الصوت على هذا الجهاز.",
|
| 52 |
-
"source": "مصدر التلاوة: EveryAyah"
|
| 53 |
-
|
| 54 |
-
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
"note": "Verse filenames use three digits for the surah and three for the ayah. Recordings are linked, not redistributed or licensed as part of AL-Khatma."
|
| 12 |
},
|
| 13 |
"reciters": [
|
| 14 |
+
{
|
| 15 |
+
"id": "husary",
|
| 16 |
+
"name": "محمود خليل الحصري",
|
| 17 |
+
"folder": "Husary_128kbps"
|
| 18 |
+
},
|
| 19 |
+
{
|
| 20 |
+
"id": "minshawy",
|
| 21 |
+
"name": "محمد صديق المنشاوي",
|
| 22 |
+
"folder": "Minshawy_Murattal_128kbps"
|
| 23 |
+
},
|
| 24 |
+
{
|
| 25 |
+
"id": "abdulbasit",
|
| 26 |
+
"name": "عبد الباسط عبد الصمد — مرتل",
|
| 27 |
+
"folder": "Abdul_Basit_Murattal_192kbps"
|
| 28 |
+
},
|
| 29 |
+
{
|
| 30 |
+
"id": "sudais",
|
| 31 |
+
"name": "عبد الرحمن السديس",
|
| 32 |
+
"folder": "Abdurrahmaan_As-Sudais_192kbps"
|
| 33 |
+
},
|
| 34 |
+
{
|
| 35 |
+
"id": "shuraym",
|
| 36 |
+
"name": "سعود الشريم",
|
| 37 |
+
"folder": "Saood_ash-Shuraym_128kbps"
|
| 38 |
+
},
|
| 39 |
+
{
|
| 40 |
+
"id": "maher",
|
| 41 |
+
"name": "ماهر المعيقلي",
|
| 42 |
+
"folder": "MaherAlMuaiqly128kbps"
|
| 43 |
+
},
|
| 44 |
+
{
|
| 45 |
+
"id": "hudhaify",
|
| 46 |
+
"name": "علي الحذيفي",
|
| 47 |
+
"folder": "Hudhaify_128kbps"
|
| 48 |
+
},
|
| 49 |
+
{
|
| 50 |
+
"id": "dussary",
|
| 51 |
+
"name": "ياسر الدوسري",
|
| 52 |
+
"folder": "Yasser_Ad-Dussary_128kbps"
|
| 53 |
+
}
|
| 54 |
],
|
| 55 |
+
"defaults": {
|
| 56 |
+
"reciter": "husary",
|
| 57 |
+
"rate": 1,
|
| 58 |
+
"repeat": 1,
|
| 59 |
+
"advance": true,
|
| 60 |
+
"gap_ms": 250
|
| 61 |
+
},
|
| 62 |
+
"rates": [
|
| 63 |
+
0.75,
|
| 64 |
+
1,
|
| 65 |
+
1.25,
|
| 66 |
+
1.5
|
| 67 |
+
],
|
| 68 |
+
"repeat_options": [
|
| 69 |
+
{
|
| 70 |
+
"value": 1,
|
| 71 |
+
"label": "مرة واحدة"
|
| 72 |
+
},
|
| 73 |
+
{
|
| 74 |
+
"value": 2,
|
| 75 |
+
"label": "مرتان"
|
| 76 |
+
},
|
| 77 |
+
{
|
| 78 |
+
"value": 3,
|
| 79 |
+
"label": "3 مرات"
|
| 80 |
+
},
|
| 81 |
+
{
|
| 82 |
+
"value": 5,
|
| 83 |
+
"label": "5 مرات"
|
| 84 |
+
},
|
| 85 |
+
{
|
| 86 |
+
"value": 0,
|
| 87 |
+
"label": "تكرار مستمر"
|
| 88 |
+
}
|
| 89 |
+
],
|
| 90 |
+
"limits": {
|
| 91 |
+
"request_timeout_ms": 15000,
|
| 92 |
+
"loading_timeout_ms": 30000
|
| 93 |
+
},
|
| 94 |
"ui": {
|
| 95 |
"title": "الاستماع",
|
| 96 |
"options": "القارئ وخيارات التلاوة",
|
|
|
|
| 116 |
"error": "تعذّر تشغيل المقطع. تحقق من الإنترنت أو اختر قارئًا آخر ثم اضغط تشغيل.",
|
| 117 |
"blocked": "اضغط تشغيل لاستكمال التلاوة؛ المتصفح يحتاج ضغطة منك.",
|
| 118 |
"storage_error": "تعذّر حفظ تفضيلات الصوت على هذا الجهاز.",
|
| 119 |
+
"source": "مصدر التلاوة: EveryAyah",
|
| 120 |
+
"gap": "الفاصل بين الآيات"
|
| 121 |
+
},
|
| 122 |
+
"gap_options": [
|
| 123 |
+
{
|
| 124 |
+
"value": 0,
|
| 125 |
+
"label": "بدون فاصل"
|
| 126 |
+
},
|
| 127 |
+
{
|
| 128 |
+
"value": 250,
|
| 129 |
+
"label": "ربع ثانية"
|
| 130 |
+
},
|
| 131 |
+
{
|
| 132 |
+
"value": 500,
|
| 133 |
+
"label": "نصف ثانية"
|
| 134 |
+
},
|
| 135 |
+
{
|
| 136 |
+
"value": 1000,
|
| 137 |
+
"label": "ثانية"
|
| 138 |
+
},
|
| 139 |
+
{
|
| 140 |
+
"value": 2000,
|
| 141 |
+
"label": "ثانيتان"
|
| 142 |
+
}
|
| 143 |
+
]
|
| 144 |
+
}
|
quran_khatma.js
CHANGED
|
@@ -50,59 +50,77 @@
|
|
| 50 |
function savePosition(ctx,kind,value){if(!settings||!ctx)return true;try{const s=read(),p=s.plan;if(p?.id!==ctx.planId||!valid(p))return true;const r=resolvedRange(p,ctx.dayIndex);if(!Number.isInteger(value.verseId)||value.verseId<r.startId||value.verseId>r.endId)return true;p.positions??={};p.positions[ctx.dayIndex]??={};p.positions[ctx.dayIndex][kind]=value;write(s);ctx.progress??={};ctx.progress[kind]=value;return true;}catch{return false;}}
|
| 51 |
async function openReminder(item,{at,back}={}){const mode=item.openQuran===undefined?(item.type==='quran'?'wird':''):item.openQuran;if(mode==='wird')return openWird({at,back});return window.MuslimClockQuran.open({resumeReading:true,back});}
|
| 52 |
async function sync(plan){if(typeof RegistryApp==='undefined'||!RegistryApp.syncQuranPlan)throw Error('reminder');RegistryApp.syncQuranPlan(plan,settings.ui.reminder_title);}
|
| 53 |
-
async function show({back}={}){
|
| 54 |
const request=revision;let c;try{c=await ready();}catch{if(request===revision)await window.MuslimClockQuran.open({back,khatmaUnavailable:true});return;}
|
| 55 |
if(request!==revision)return;
|
| 56 |
const ui=c.ui,root=el('div');root.className='quran-khatma';let state;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 57 |
try{state=read();}catch{root.append(el('p',ui.storage_error));RegistryApp.openPanel('worship',{title:ui.title,compact:true},()=>document.getElementById('adaptivePopupBody').append(root));return;}
|
| 58 |
const plan=valid(state.plan)?state.plan:null;
|
| 59 |
-
|
| 60 |
function rangeLabel(r,day){return fmt(ui.range_exact,{day,start:r.startKey,end:r.endKey,first:r.startPage,last:r.endPage});}
|
| 61 |
-
function paint(){
|
| 62 |
-
|
|
|
|
| 63 |
const idx=current(plan),r=resolvedRange(plan,Math.max(0,Math.min(plan.days-1,idx)));
|
| 64 |
-
|
| 65 |
-
if(!plan.paused)
|
| 66 |
-
const label=el('label',ui.choose_day),select=el('select');for(let i=0;i<plan.days;i++){const
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
|
|
|
| 71 |
}
|
| 72 |
-
function form(old){
|
| 73 |
-
|
|
|
|
|
|
|
| 74 |
const unitLabel=el('label',ui.unit),unit=el('select');
|
| 75 |
if(old&&!old.layout){const opt=el('option',ui.legacy_note);opt.value='legacy_pages';unit.append(opt);}
|
| 76 |
-
for(const u of source.partitions.units){const opt=el('option',u.label);opt.value=u.id;unit.append(opt);}unit.value=old?(old.layout?old.unit:'legacy_pages'):'balanced_pages';unitLabel.append(unit);
|
| 77 |
-
const days=field('days','number',old?.days??c.defaults.days);days.min=1;days.max=6236;days.step=1;
|
| 78 |
-
const start=field('start','date',old?.startDate??dateKey()
|
| 79 |
-
const
|
|
|
|
| 80 |
const customStart=field('custom_start','text',old?.startKey??'1:1'),customEnd=field('custom_end','text',old?.endKey??'114:6');customStart.dir=customEnd.dir='ltr';
|
| 81 |
-
const hint=el('p',ui.custom_hint);
|
| 82 |
-
function controls(){const balanced=['balanced_pages','legacy_pages'].includes(unit.value),custom=unit.value==='custom',u=source.partitions.units.find(u=>u.id===unit.value);days.parentElement.hidden=!balanced&&!custom;page.parentElement.hidden=!balanced;amount.parentElement.hidden=startUnit.parentElement.hidden=balanced||custom;customStart.parentElement.hidden=customEnd.parentElement.hidden=hint.hidden=!custom;if(u){amount.max=startUnit.max=u.count;}
|
| 83 |
-
unit.onchange=controls;controls();
|
| 84 |
const reminder=field('reminder','checkbox','');reminder.checked=old?.reminder===true;
|
| 85 |
-
const time=field('time','time',old?.time??c.defaults.reminder_time);
|
| 86 |
-
|
|
|
|
|
|
|
|
|
|
| 87 |
const next={id:old?.id||('khatma-'+Date.now().toString(36)),days:Number(days.value),startDate:start.value,startPage:Number(page.value),time:time.value,reminder:reminder.checked,paused:false,positions:old?.positions||{}};
|
| 88 |
if(unit.value!=='legacy_pages'){
|
| 89 |
next.layout=source.partitions.revision;next.unit=unit.value;
|
| 90 |
if(unit.value==='custom'){next.startKey=customStart.value.trim();next.endKey=customEnd.value.trim();next.startPage=source.byKey.get(next.startKey)?.page;}
|
| 91 |
else if(unit.value!=='balanced_pages'){
|
| 92 |
const u=source.partitions.units.find(u=>u.id===unit.value);next.amount=Number(amount.value);next.startUnit=Number(startUnit.value);
|
| 93 |
-
if(!u||!Number.isInteger(next.startUnit)||next.startUnit<1||next.startUnit>u.count||!Number.isInteger(next.amount)||next.amount<1||next.amount>u.count){
|
| 94 |
next.days=Math.ceil((u.count-next.startUnit+1)/next.amount);next.startPage=source.verses[unitRange(next.unit,next.startUnit).startId-1].page;
|
| 95 |
}
|
| 96 |
}
|
| 97 |
-
if(!valid(next)){
|
| 98 |
const reset=plan&&(!old||['days','startDate','startPage','layout','unit','amount','startUnit','startKey','endKey'].some(k=>plan[k]!==next[k]));
|
| 99 |
if(reset&&!window.confirm(ui.replace_confirm))return;if(reset)next.positions={};
|
| 100 |
-
try{const s=read();s.plan=next;write(s);}catch{
|
| 101 |
-
try{await sync(next);}catch{
|
| 102 |
-
show({back});
|
| 103 |
-
})
|
|
|
|
|
|
|
| 104 |
}
|
| 105 |
-
RegistryApp.openPanel('worship',{title:ui.title,compact:true},()=>{document.getElementById('adaptivePopupBody').append(root);if(back)document.getElementById('adaptivePopupActions').append(button(ui.back,back));});
|
|
|
|
|
|
|
| 106 |
}
|
| 107 |
window.MuslimClockKhatma=Object.freeze({configuration,show,openWird,openReminder,savePosition,context,range,dateKey,dayNumber});
|
| 108 |
})();
|
|
|
|
| 50 |
function savePosition(ctx,kind,value){if(!settings||!ctx)return true;try{const s=read(),p=s.plan;if(p?.id!==ctx.planId||!valid(p))return true;const r=resolvedRange(p,ctx.dayIndex);if(!Number.isInteger(value.verseId)||value.verseId<r.startId||value.verseId>r.endId)return true;p.positions??={};p.positions[ctx.dayIndex]??={};p.positions[ctx.dayIndex][kind]=value;write(s);ctx.progress??={};ctx.progress[kind]=value;return true;}catch{return false;}}
|
| 51 |
async function openReminder(item,{at,back}={}){const mode=item.openQuran===undefined?(item.type==='quran'?'wird':''):item.openQuran;if(mode==='wird')return openWird({at,back});return window.MuslimClockQuran.open({resumeReading:true,back});}
|
| 52 |
async function sync(plan){if(typeof RegistryApp==='undefined'||!RegistryApp.syncQuranPlan)throw Error('reminder');RegistryApp.syncQuranPlan(plan,settings.ui.reminder_title);}
|
| 53 |
+
async function show({back,message,focus}={}){
|
| 54 |
const request=revision;let c;try{c=await ready();}catch{if(request===revision)await window.MuslimClockQuran.open({back,khatmaUnavailable:true});return;}
|
| 55 |
if(request!==revision)return;
|
| 56 |
const ui=c.ui,root=el('div');root.className='quran-khatma';let state;
|
| 57 |
+
// Persistent live region: it is never removed, so changes are always announced.
|
| 58 |
+
const status=el('p');status.setAttribute('role','status');status.setAttribute('aria-live','polite');status.setAttribute('aria-atomic','true');status.tabIndex=-1;
|
| 59 |
+
const view=el('div');
|
| 60 |
+
const say=(text,{moveFocus=false}={})=>{status.textContent='';setTimeout(()=>{status.textContent=text||'';if(moveFocus&&text)status.focus();},40);};
|
| 61 |
+
// After every re-render, put the screen reader on the new heading instead of losing its place.
|
| 62 |
+
const landOn=heading=>{heading.tabIndex=-1;setTimeout(()=>heading.focus(),60);};
|
| 63 |
try{state=read();}catch{root.append(el('p',ui.storage_error));RegistryApp.openPanel('worship',{title:ui.title,compact:true},()=>document.getElementById('adaptivePopupBody').append(root));return;}
|
| 64 |
const plan=valid(state.plan)?state.plan:null;
|
| 65 |
+
root.append(view,status);
|
| 66 |
function rangeLabel(r,day){return fmt(ui.range_exact,{day,start:r.startKey,end:r.endKey,first:r.startPage,last:r.endPage});}
|
| 67 |
+
function paint(land=true){view.replaceChildren();
|
| 68 |
+
const h=el('h3',ui.plan_heading||ui.title);view.append(h,el('p',ui.note));
|
| 69 |
+
if(plan){view.append(el('p',plan.layout?fmt(ui.summary_exact,{days:plan.days,quantity:plan.amount||'',unit:source.partitions.units.find(u=>u.id===plan.unit)?.label||'',start:plan.startDate}):fmt(ui.summary,{days:plan.days,page:plan.startPage,start:plan.startDate})));if(!plan.layout)view.append(el('p',ui.legacy_note));
|
| 70 |
const idx=current(plan),r=resolvedRange(plan,Math.max(0,Math.min(plan.days-1,idx)));
|
| 71 |
+
view.append(el('p',plan.paused?ui.paused:idx<0?ui.before:idx>=plan.days?ui.after:rangeLabel(r,idx+1)));
|
| 72 |
+
if(!plan.paused)view.append(button(ui.open,()=>openWird({back:()=>show({back})})));
|
| 73 |
+
const label=el('label',ui.choose_day),select=el('select');for(let i=0;i<plan.days;i++){const rr=resolvedRange(plan,i),o=el('option',rangeLabel(rr,i+1));o.value=i;select.append(o);}select.value=Math.max(0,Math.min(plan.days-1,idx));label.append(select);
|
| 74 |
+
view.append(label,button(ui.open_selected||ui.open,async()=>{const dayIndex=Number(select.value),rr=resolvedRange(plan,dayIndex);window.MuslimClockQuran.open({wird:{planId:plan.id,dayIndex,...rr,progress:plan.positions?.[dayIndex]||{}},page:rr.startPage,back:()=>show({back})});}));
|
| 75 |
+
view.append(button(plan.paused?ui.resume:ui.stop,async()=>{try{const s=read();s.plan.paused=!s.plan.paused;write(s);try{await sync(s.plan);}catch{say(ui.reminder_error,{moveFocus:true});return;}show({back,message:s.plan.paused?ui.stopped_done:ui.resumed_done});}catch{say(ui.storage_error,{moveFocus:true});}}));
|
| 76 |
+
view.append(button(ui.edit,()=>form(plan)),button(ui.new,()=>form(null)));
|
| 77 |
+
}else {view.append(el('p',ui.empty));form(null,land);return;}
|
| 78 |
+
if(land)landOn(h);
|
| 79 |
}
|
| 80 |
+
function form(old,land=true){view.replaceChildren();
|
| 81 |
+
const h=el('h3',old?ui.edit:ui.new);view.append(h,el('p',ui.note));
|
| 82 |
+
const described=(input,text,id)=>{const d=el('p',text);d.id=id;d.className='quran-meta';input.setAttribute('aria-describedby',id);return d;};
|
| 83 |
+
function field(name,type,value,hintText){const l=el('label',ui[name]),input=el('input');input.type=type;input.value=value;l.append(input);view.append(l);if(hintText){const d=described(input,hintText,'khatma-hint-'+name);l.append(d);}return input;}
|
| 84 |
const unitLabel=el('label',ui.unit),unit=el('select');
|
| 85 |
if(old&&!old.layout){const opt=el('option',ui.legacy_note);opt.value='legacy_pages';unit.append(opt);}
|
| 86 |
+
for(const u of source.partitions.units){const opt=el('option',u.label);opt.value=u.id;unit.append(opt);}unit.value=old?(old.layout?old.unit:'legacy_pages'):'balanced_pages';unitLabel.append(unit);view.append(unitLabel);
|
| 87 |
+
const days=field('days','number',old?.days??c.defaults.days);days.min=1;days.max=6236;days.step=1;days.inputMode='numeric';
|
| 88 |
+
const start=field('start','date',old?.startDate??dateKey(),ui.date_hint);
|
| 89 |
+
const page=field('page','number',old?.startPage??c.defaults.start_page);page.min=1;page.max=604;page.step=1;page.inputMode='numeric';
|
| 90 |
+
const amount=field('amount','number',old?.amount??1),startUnit=field('unit_start','number',old?.startUnit??1);amount.min=startUnit.min=1;amount.step=startUnit.step=1;amount.inputMode=startUnit.inputMode='numeric';
|
| 91 |
const customStart=field('custom_start','text',old?.startKey??'1:1'),customEnd=field('custom_end','text',old?.endKey??'114:6');customStart.dir=customEnd.dir='ltr';
|
| 92 |
+
const hint=el('p',ui.custom_hint);view.append(hint);
|
| 93 |
+
function controls(){const balanced=['balanced_pages','legacy_pages'].includes(unit.value),custom=unit.value==='custom',u=source.partitions.units.find(u=>u.id===unit.value);days.parentElement.hidden=!balanced&&!custom;page.parentElement.hidden=!balanced;amount.parentElement.hidden=startUnit.parentElement.hidden=balanced||custom;customStart.parentElement.hidden=customEnd.parentElement.hidden=hint.hidden=!custom;if(u){amount.max=startUnit.max=u.count;amount.setAttribute('aria-description',fmt(ui.range_hint||'',{max:u.count}));startUnit.setAttribute('aria-description',fmt(ui.range_hint||'',{max:u.count}));}say('');}
|
| 94 |
+
unit.onchange=()=>{controls();say(fmt(ui.fields_changed||'',{unit:unit.selectedOptions[0]?.textContent||''}));};controls();
|
| 95 |
const reminder=field('reminder','checkbox','');reminder.checked=old?.reminder===true;
|
| 96 |
+
const time=field('time','time',old?.time??c.defaults.reminder_time,ui.time_hint);
|
| 97 |
+
view.append(el('p',ui.scope));
|
| 98 |
+
const fail=(text,input)=>{say(text,{moveFocus:false});setTimeout(()=>{(input||status).focus();},80);};
|
| 99 |
+
view.append(button(ui.save,async()=>{
|
| 100 |
+
if(!start.value){fail(ui.invalid,start);return;}
|
| 101 |
const next={id:old?.id||('khatma-'+Date.now().toString(36)),days:Number(days.value),startDate:start.value,startPage:Number(page.value),time:time.value,reminder:reminder.checked,paused:false,positions:old?.positions||{}};
|
| 102 |
if(unit.value!=='legacy_pages'){
|
| 103 |
next.layout=source.partitions.revision;next.unit=unit.value;
|
| 104 |
if(unit.value==='custom'){next.startKey=customStart.value.trim();next.endKey=customEnd.value.trim();next.startPage=source.byKey.get(next.startKey)?.page;}
|
| 105 |
else if(unit.value!=='balanced_pages'){
|
| 106 |
const u=source.partitions.units.find(u=>u.id===unit.value);next.amount=Number(amount.value);next.startUnit=Number(startUnit.value);
|
| 107 |
+
if(!u||!Number.isInteger(next.startUnit)||next.startUnit<1||next.startUnit>u.count||!Number.isInteger(next.amount)||next.amount<1||next.amount>u.count){fail(ui.invalid,amount);return;}
|
| 108 |
next.days=Math.ceil((u.count-next.startUnit+1)/next.amount);next.startPage=source.verses[unitRange(next.unit,next.startUnit).startId-1].page;
|
| 109 |
}
|
| 110 |
}
|
| 111 |
+
if(!valid(next)){fail(ui.invalid,days);return;}
|
| 112 |
const reset=plan&&(!old||['days','startDate','startPage','layout','unit','amount','startUnit','startKey','endKey'].some(k=>plan[k]!==next[k]));
|
| 113 |
if(reset&&!window.confirm(ui.replace_confirm))return;if(reset)next.positions={};
|
| 114 |
+
try{const s=read();s.plan=next;write(s);}catch{fail(ui.storage_error);return;}
|
| 115 |
+
try{await sync(next);}catch{fail(ui.reminder_error);return;}
|
| 116 |
+
show({back,message:ui.saved});
|
| 117 |
+
}));
|
| 118 |
+
if(plan)view.append(button(ui.cancel||ui.back,()=>{paint();say(ui.cancelled||'');}));
|
| 119 |
+
if(land)landOn(h);
|
| 120 |
}
|
| 121 |
+
RegistryApp.openPanel('worship',{title:ui.title,compact:true},()=>{document.getElementById('adaptivePopupBody').append(root);if(back)document.getElementById('adaptivePopupActions').append(button(ui.back,back));});
|
| 122 |
+
paint(!message);
|
| 123 |
+
if(message){const heading=view.querySelector('h3');if(heading)landOn(heading);say(message);}
|
| 124 |
}
|
| 125 |
window.MuslimClockKhatma=Object.freeze({configuration,show,openWird,openReminder,savePosition,context,range,dateKey,dayNumber});
|
| 126 |
})();
|
quran_khatma_registry.json
CHANGED
|
@@ -43,6 +43,16 @@
|
|
| 43 |
"retry": "إعادة المحاولة",
|
| 44 |
"load_error": "تعذّر تحميل إعدادات الختمة.",
|
| 45 |
"choose_day": "عرض ورد يوم آخر",
|
| 46 |
-
"scope": "التذكير يستخدم نظام الساعة المحلي الحالي؛ لا يضمن التنبيه بعد إغلاق المتصفح."
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 47 |
}
|
| 48 |
-
}
|
|
|
|
| 43 |
"retry": "إعادة المحاولة",
|
| 44 |
"load_error": "تعذّر تحميل إعدادات الختمة.",
|
| 45 |
"choose_day": "عرض ورد يوم آخر",
|
| 46 |
+
"scope": "التذكير يستخدم نظام الساعة المحلي الحالي؛ لا يضمن التنبيه بعد إغلاق المتصفح.",
|
| 47 |
+
"plan_heading": "خطة الختمة",
|
| 48 |
+
"open_selected": "فتح ورد اليوم المحدد في القائمة",
|
| 49 |
+
"cancel": "إلغاء التعديل والعودة للخطة",
|
| 50 |
+
"cancelled": "أُلغي التعديل.",
|
| 51 |
+
"stopped_done": "أُوقفت الخطة وتذكيرها.",
|
| 52 |
+
"resumed_done": "استؤنفت الخطة.",
|
| 53 |
+
"date_hint": "أدخل التاريخ بصيغة سنة-شهر-يوم، مثل 2026-10-15، أو استخدم منتقي التاريخ.",
|
| 54 |
+
"time_hint": "أدخل الوقت بصيغة ساعات:دقائق بنظام 24 ساعة، مثل 18:00.",
|
| 55 |
+
"range_hint": "الحد الأعلى {max}.",
|
| 56 |
+
"fields_changed": "تغيّرت الحقول حسب الطريقة المختارة: {unit}."
|
| 57 |
}
|
| 58 |
+
}
|
quran_reader.css
CHANGED
|
@@ -47,13 +47,13 @@
|
|
| 47 |
#adaptivePopupBody .quran-nav button{flex:1;text-align:center;min-width:0}
|
| 48 |
#adaptivePopupBody .quran-reader button:disabled{opacity:.5;cursor:default}
|
| 49 |
#adaptivePopupBody .quran-page-title{font-size:1.15rem;text-align:center;margin:18px 0 8px}
|
| 50 |
-
#adaptivePopupBody .quran-page-text{font-
|
| 51 |
#adaptivePopupBody .quran-verse{scroll-margin-block:20px}
|
| 52 |
#adaptivePopupBody .quran-verse:focus{outline:2px solid currentColor;outline-offset:3px;border-radius:3px}
|
| 53 |
#adaptivePopupBody .quran-verse-number{font-family:inherit;font-size:.8em;white-space:nowrap}
|
| 54 |
-
#adaptivePopupBody .quran-basmala{text-align:center;font-
|
| 55 |
#adaptivePopupBody .quran-meta{font-size:.85rem;line-height:1.6;margin:10px 0}
|
| 56 |
-
#adaptivePopupBody .quran-result-text{display:block;font-
|
| 57 |
#adaptivePopupBody .quran-settings input[type="range"]{width:100%;padding:0;min-height:44px}
|
| 58 |
#adaptivePopupBody .quran-settings input[type="checkbox"]{width:auto}
|
| 59 |
#adaptivePopupCard[data-quran-theme]{background:var(--quran-bg);color:var(--quran-fg);border-color:var(--quran-border);color-scheme:light}
|
|
@@ -86,3 +86,22 @@
|
|
| 86 |
#adaptivePopupBody .quran-nav button{font-size:14px;padding:8px}
|
| 87 |
#adaptivePopupBody .quran-toolbar{gap:6px}
|
| 88 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 47 |
#adaptivePopupBody .quran-nav button{flex:1;text-align:center;min-width:0}
|
| 48 |
#adaptivePopupBody .quran-reader button:disabled{opacity:.5;cursor:default}
|
| 49 |
#adaptivePopupBody .quran-page-title{font-size:1.15rem;text-align:center;margin:18px 0 8px}
|
| 50 |
+
#adaptivePopupBody .quran-page-text{font-size:var(--quran-font-size,26px);margin:8px 0}
|
| 51 |
#adaptivePopupBody .quran-verse{scroll-margin-block:20px}
|
| 52 |
#adaptivePopupBody .quran-verse:focus{outline:2px solid currentColor;outline-offset:3px;border-radius:3px}
|
| 53 |
#adaptivePopupBody .quran-verse-number{font-family:inherit;font-size:.8em;white-space:nowrap}
|
| 54 |
+
#adaptivePopupBody .quran-basmala{text-align:center;font-size:var(--quran-font-size,26px)}
|
| 55 |
#adaptivePopupBody .quran-meta{font-size:.85rem;line-height:1.6;margin:10px 0}
|
| 56 |
+
#adaptivePopupBody .quran-result-text{display:block;font-size:var(--quran-font-size,26px)}
|
| 57 |
#adaptivePopupBody .quran-settings input[type="range"]{width:100%;padding:0;min-height:44px}
|
| 58 |
#adaptivePopupBody .quran-settings input[type="checkbox"]{width:auto}
|
| 59 |
#adaptivePopupCard[data-quran-theme]{background:var(--quran-bg);color:var(--quran-fg);border-color:var(--quran-border);color-scheme:light}
|
|
|
|
| 86 |
#adaptivePopupBody .quran-nav button{font-size:14px;padding:8px}
|
| 87 |
#adaptivePopupBody .quran-toolbar{gap:6px}
|
| 88 |
}
|
| 89 |
+
|
| 90 |
+
|
| 91 |
+
/* ===== إضافات: روابط التفسير وأسباب النزول ومعاني الكلمات + إتاحة قارئ الشاشة ===== */
|
| 92 |
+
.quran-sr-only{position:absolute!important;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0}
|
| 93 |
+
#adaptivePopupBody .quran-link-group{margin:6px 0}
|
| 94 |
+
#adaptivePopupBody .quran-link-group summary{cursor:pointer;min-height:44px;padding:10px 0;font-weight:bold}
|
| 95 |
+
#adaptivePopupBody .quran-link-list{list-style:none;margin:0;padding:0}
|
| 96 |
+
#adaptivePopupBody .quran-link-list li{margin:0}
|
| 97 |
+
#adaptivePopupBody .quran-link-list a{display:block;min-height:44px;padding:10px 4px;text-decoration:underline;overflow-wrap:anywhere}
|
| 98 |
+
#adaptivePopupBody .quran-khatma h3[tabindex="-1"]:focus,
|
| 99 |
+
#adaptivePopupBody .quran-khatma p[tabindex="-1"]:focus{outline:2px solid currentColor;outline-offset:3px}
|
| 100 |
+
#adaptivePopupBody .quran-khatma button,
|
| 101 |
+
#adaptivePopupBody .quran-khatma input,
|
| 102 |
+
#adaptivePopupBody .quran-khatma select{min-height:44px}
|
| 103 |
+
#adaptivePopupBody .quran-khatma [hidden]{display:none!important}
|
| 104 |
+
#adaptivePopupBody .quran-khatma :focus-visible,
|
| 105 |
+
#adaptivePopupBody .quran-ayah-actions :focus-visible,
|
| 106 |
+
#adaptivePopupBody .quran-audio :focus-visible{outline:2px solid currentColor;outline-offset:2px}
|
| 107 |
+
@media (prefers-reduced-motion:reduce){#adaptivePopupBody *{scroll-behavior:auto!important;transition:none!important}}
|
quran_reader.js
CHANGED
|
@@ -227,11 +227,11 @@
|
|
| 227 |
let audioAttempt = 0;
|
| 228 |
cleanup = () => {saveAudio(player?.snapshot(),true);mounted=false;audioAttempt++;player?.dispose();window.removeEventListener('pagehide',flushAudio);document.removeEventListener('visibilitychange',flushAudio);};
|
| 229 |
const toolbar = node('div', null, 'quran-toolbar');
|
| 230 |
-
const
|
| 231 |
const navigatorTitle = node('summary', ui.navigate);
|
| 232 |
const navigatorGrid = node('div', null, 'quran-navigator-grid');
|
| 233 |
-
|
| 234 |
-
toolbar.append(
|
| 235 |
const field = (label, input) => { const wrap = node('label', label); wrap.append(input); navigatorGrid.append(wrap); return input; };
|
| 236 |
const select = (items, label) => {
|
| 237 |
const input = node('select');
|
|
@@ -243,13 +243,20 @@
|
|
| 243 |
const surahSearchLabel=node('label',ui.search_surah);surahSearchLabel.append(surahSearch);
|
| 244 |
navigatorGrid.insertBefore(surahSearchLabel,surahSelect.parentElement);
|
| 245 |
surahSearch.addEventListener('input',()=>{
|
| 246 |
-
const term=normalize(surahSearch.value);
|
| 247 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 248 |
});
|
| 249 |
const juzSelect = select(Array.from({length: source.counts.juz}, (_, i) => [i + 1, i + 1]), ui.juz);
|
| 250 |
const hizbSelect=select(source.partitions.hizb.map(r=>[r.id,r.id]),ui.hizb);
|
| 251 |
const rubSelect=select(source.partitions.rub_el_hizb.map(r=>[r.id,r.id]),ui.rub_el_hizb);
|
| 252 |
-
const position=node('p',null,'quran-meta');position.setAttribute('aria-live','
|
| 253 |
function positionText(v){return format(ui.partition_position,{juz:v.juz,hizb:v.hizb,rub:v.rub_el_hizb,page:v.page});}
|
| 254 |
const pageForm = node('form');
|
| 255 |
const pageLabel = node('label', ui.page);
|
|
@@ -284,16 +291,31 @@
|
|
| 284 |
const planOK=window.MuslimClockKhatma?.savePosition(wird,'reading',point)!==false;
|
| 285 |
actionStatus.textContent=ok&&planOK?ui.bookmark_saved:ui.progress_error;updateBookmarks();
|
| 286 |
});
|
| 287 |
-
const
|
| 288 |
-
|
| 289 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 290 |
function selectVerse(verse, origin) {
|
| 291 |
actionVerse=verse;actionOrigin=origin;
|
| 292 |
-
|
| 293 |
-
for(const item of c.tafsirs||[]){
|
| 294 |
-
try{const url=new URL(format(item.url,{surah:verse.surah,ayah:verse.ayah}));if(url.protocol!=='https:')continue;
|
| 295 |
-
const link=node('a',item.name+' ↗');link.href=url.href;link.target='_blank';link.rel='noopener noreferrer';link.addEventListener('click',()=>player?.pause());tafsirLinks.append(link);}catch{}
|
| 296 |
-
}
|
| 297 |
actionTitle.textContent=source.surahs[verse.surah-1].name+' · '+ui.ayah+' '+verse.ayah+' · '+positionText(verse);
|
| 298 |
actionStatus.replaceChildren();actions.hidden=false;(listen.disabled?copy:listen).focus();
|
| 299 |
}
|
|
@@ -401,7 +423,7 @@
|
|
| 401 |
}
|
| 402 |
const span = node('span', verse.text, 'quran-verse');
|
| 403 |
span.id = 'quran-verse-' + verse.id; span.tabIndex = 0;span.setAttribute('role','button');
|
| 404 |
-
span.setAttribute('aria-label',verse.text+'
|
| 405 |
span.addEventListener('click',()=>selectVerse(verse,span));
|
| 406 |
span.addEventListener('keydown',event=>{if(event.key==='Enter'||event.key===' '){event.preventDefault();selectVerse(verse,span);}});
|
| 407 |
const number = node('span', ' (' + verse.ayah + ') ', 'quran-verse-number');
|
|
|
|
| 227 |
let audioAttempt = 0;
|
| 228 |
cleanup = () => {saveAudio(player?.snapshot(),true);mounted=false;audioAttempt++;player?.dispose();window.removeEventListener('pagehide',flushAudio);document.removeEventListener('visibilitychange',flushAudio);};
|
| 229 |
const toolbar = node('div', null, 'quran-toolbar');
|
| 230 |
+
const navPanel = node('details', null, 'quran-navigator');
|
| 231 |
const navigatorTitle = node('summary', ui.navigate);
|
| 232 |
const navigatorGrid = node('div', null, 'quran-navigator-grid');
|
| 233 |
+
navPanel.append(navigatorTitle, navigatorGrid);
|
| 234 |
+
toolbar.append(navPanel);
|
| 235 |
const field = (label, input) => { const wrap = node('label', label); wrap.append(input); navigatorGrid.append(wrap); return input; };
|
| 236 |
const select = (items, label) => {
|
| 237 |
const input = node('select');
|
|
|
|
| 243 |
const surahSearchLabel=node('label',ui.search_surah);surahSearchLabel.append(surahSearch);
|
| 244 |
navigatorGrid.insertBefore(surahSearchLabel,surahSelect.parentElement);
|
| 245 |
surahSearch.addEventListener('input',()=>{
|
| 246 |
+
const term=normalize(surahSearch.value),keep=surahSelect.value;
|
| 247 |
+
surahSelect.replaceChildren();
|
| 248 |
+
for(const sr of source.surahs){
|
| 249 |
+
const text=sr.id+'. '+sr.name;
|
| 250 |
+
if(term&&!normalize(text).includes(term))continue;
|
| 251 |
+
const option=node('option',text);option.value=sr.id;surahSelect.append(option);
|
| 252 |
+
}
|
| 253 |
+
if([...surahSelect.options].some(o=>o.value===keep))surahSelect.value=keep;
|
| 254 |
+
status.textContent=surahSelect.options.length?'':ui.no_results;
|
| 255 |
});
|
| 256 |
const juzSelect = select(Array.from({length: source.counts.juz}, (_, i) => [i + 1, i + 1]), ui.juz);
|
| 257 |
const hizbSelect=select(source.partitions.hizb.map(r=>[r.id,r.id]),ui.hizb);
|
| 258 |
const rubSelect=select(source.partitions.rub_el_hizb.map(r=>[r.id,r.id]),ui.rub_el_hizb);
|
| 259 |
+
const position=node('p',null,'quran-meta');position.setAttribute('aria-live','off');
|
| 260 |
function positionText(v){return format(ui.partition_position,{juz:v.juz,hizb:v.hizb,rub:v.rub_el_hizb,page:v.page});}
|
| 261 |
const pageForm = node('form');
|
| 262 |
const pageLabel = node('label', ui.page);
|
|
|
|
| 291 |
const planOK=window.MuslimClockKhatma?.savePosition(wird,'reading',point)!==false;
|
| 292 |
actionStatus.textContent=ok&&planOK?ui.bookmark_saved:ui.progress_error;updateBookmarks();
|
| 293 |
});
|
| 294 |
+
const linkGroups=[
|
| 295 |
+
{key:'tafsir',items:c.tafsirs,note:'tafsir_note'},
|
| 296 |
+
{key:'asbab',items:c.asbab_nuzul,note:'asbab_note'},
|
| 297 |
+
{key:'meanings',items:c.word_meanings,note:'meanings_note'}
|
| 298 |
+
].filter(g=>Array.isArray(g.items)&&g.items.length).map(g=>{
|
| 299 |
+
const box=node('details',null,'quran-link-group'),title=node('summary',ui[g.key]),list=node('ul',null,'quran-link-list');
|
| 300 |
+
box.append(title,node('p',ui[g.note],'quran-meta'),list);return {...g,box,list};
|
| 301 |
+
});
|
| 302 |
+
actions.append(actionTitle,listen,copy,bookmark,...linkGroups.map(g=>g.box),button(ui.close_actions,()=>{actions.hidden=true;actionOrigin?.focus();}),actionStatus);
|
| 303 |
+
function fillLinks(verse){
|
| 304 |
+
for(const g of linkGroups){
|
| 305 |
+
g.list.replaceChildren();g.box.open=false;
|
| 306 |
+
for(const item of g.items){
|
| 307 |
+
try{
|
| 308 |
+
const url=new URL(format(item.url,{surah:verse.surah,ayah:verse.ayah}));if(url.protocol!=='https:')continue;
|
| 309 |
+
const li=node('li'),link=node('a',item.name+' ↗');link.href=url.href;link.target='_blank';link.rel='noopener noreferrer';
|
| 310 |
+
link.setAttribute('aria-label',ui[g.key]+' — '+source.surahs[verse.surah-1].name+' '+ui.ayah+' '+verse.ayah+' — '+item.name+' — '+ui.external_link);
|
| 311 |
+
link.addEventListener('click',()=>player?.pause());li.append(link);g.list.append(li);
|
| 312 |
+
}catch{}
|
| 313 |
+
}
|
| 314 |
+
}
|
| 315 |
+
}
|
| 316 |
function selectVerse(verse, origin) {
|
| 317 |
actionVerse=verse;actionOrigin=origin;
|
| 318 |
+
fillLinks(verse);
|
|
|
|
|
|
|
|
|
|
|
|
|
| 319 |
actionTitle.textContent=source.surahs[verse.surah-1].name+' · '+ui.ayah+' '+verse.ayah+' · '+positionText(verse);
|
| 320 |
actionStatus.replaceChildren();actions.hidden=false;(listen.disabled?copy:listen).focus();
|
| 321 |
}
|
|
|
|
| 423 |
}
|
| 424 |
const span = node('span', verse.text, 'quran-verse');
|
| 425 |
span.id = 'quran-verse-' + verse.id; span.tabIndex = 0;span.setAttribute('role','button');
|
| 426 |
+
span.setAttribute('aria-label',verse.text+' — '+ui.ayah+' '+verse.ayah+' — '+ui.verse_actions);span.setAttribute('aria-haspopup','true');
|
| 427 |
span.addEventListener('click',()=>selectVerse(verse,span));
|
| 428 |
span.addEventListener('keydown',event=>{if(event.key==='Enter'||event.key===' '){event.preventDefault();selectVerse(verse,span);}});
|
| 429 |
const number = node('span', ' (' + verse.ayah + ') ', 'quran-verse-number');
|
quran_reader_registry.json
CHANGED
|
@@ -3,12 +3,38 @@
|
|
| 3 |
"core_file": "quran_core_registry.json",
|
| 4 |
"source_file": "al_khatma_source_attribution.md",
|
| 5 |
"storage_key": "muslim_clock_quran_reader_v1",
|
| 6 |
-
"defaults": {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
"themes": {
|
| 8 |
-
"light": {
|
| 9 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
},
|
| 11 |
-
"limits": {"font_min": 20, "font_max": 40, "search_batch": 40, "request_timeout_ms": 30000},
|
| 12 |
"ui": {
|
| 13 |
"title": "القرآن الكريم",
|
| 14 |
"open": "قراءة القرآن الكريم",
|
|
@@ -52,6 +78,66 @@
|
|
| 52 |
"close_actions": "إغلاق إجراءات الآية",
|
| 53 |
"audio_loading": "جارٍ تجهيز مشغل التلاوة…",
|
| 54 |
"audio_unavailable": "تعذّر تجهيز الصوت. القراءة متاحة؛ أعد محاولة تحميل المشغّل.",
|
| 55 |
-
"search_surah": "بحث عن سورة"
|
| 56 |
-
|
| 57 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3 |
"core_file": "quran_core_registry.json",
|
| 4 |
"source_file": "al_khatma_source_attribution.md",
|
| 5 |
"storage_key": "muslim_clock_quran_reader_v1",
|
| 6 |
+
"defaults": {
|
| 7 |
+
"font_size": 26,
|
| 8 |
+
"remember_page": true,
|
| 9 |
+
"theme": "auto",
|
| 10 |
+
"follow_audio": true,
|
| 11 |
+
"show_partition_status": true,
|
| 12 |
+
"show_swipe_hint": true
|
| 13 |
+
},
|
| 14 |
"themes": {
|
| 15 |
+
"light": {
|
| 16 |
+
"bg": "#ffffff",
|
| 17 |
+
"fg": "#19241d",
|
| 18 |
+
"surface": "#f1f5f2",
|
| 19 |
+
"border": "#7b8980",
|
| 20 |
+
"highlight": "#c5e9d0",
|
| 21 |
+
"highlight_fg": "#102b1a"
|
| 22 |
+
},
|
| 23 |
+
"dark": {
|
| 24 |
+
"bg": "#171b18",
|
| 25 |
+
"fg": "#f0f5f1",
|
| 26 |
+
"surface": "#252c27",
|
| 27 |
+
"border": "#87978c",
|
| 28 |
+
"highlight": "#365a40",
|
| 29 |
+
"highlight_fg": "#ffffff"
|
| 30 |
+
}
|
| 31 |
+
},
|
| 32 |
+
"limits": {
|
| 33 |
+
"font_min": 20,
|
| 34 |
+
"font_max": 40,
|
| 35 |
+
"search_batch": 40,
|
| 36 |
+
"request_timeout_ms": 30000
|
| 37 |
},
|
|
|
|
| 38 |
"ui": {
|
| 39 |
"title": "القرآن الكريم",
|
| 40 |
"open": "قراءة القرآن الكريم",
|
|
|
|
| 78 |
"close_actions": "إغلاق إجراءات الآية",
|
| 79 |
"audio_loading": "جارٍ تجهيز مشغل التلاوة…",
|
| 80 |
"audio_unavailable": "تعذّر تجهيز الصوت. القراءة متاحة؛ أعد محاولة تحميل المشغّل.",
|
| 81 |
+
"search_surah": "بحث عن سورة",
|
| 82 |
+
"navigate": "التنقل بين السور والأجزاء والصفحات",
|
| 83 |
+
"settings_short": "الإعدادات",
|
| 84 |
+
"bookmark": "حفظ علامة قراءة هنا",
|
| 85 |
+
"bookmark_saved": "حُفظت علامة القراءة عند هذه الآية.",
|
| 86 |
+
"progress_error": "تعذّر حفظ موضعك على هذا الجهاز.",
|
| 87 |
+
"tafsir": "التفسير",
|
| 88 |
+
"tafsir_note": "روابط خارجية تفتح على هذه الآية نفسها، وتتوقف التلاوة عند فتحها.",
|
| 89 |
+
"asbab": "أسباب النزول",
|
| 90 |
+
"asbab_note": "رابط خارجي يفتح على أسباب نزول هذه الآية. لا يوجد سبب نزول مروي لكل آية؛ إن لم يظهر شيء فالآية ليس لها سبب نزول مأثور.",
|
| 91 |
+
"meanings": "معاني الكلمات",
|
| 92 |
+
"meanings_note": "رابط خارجي يفتح على هذه الآية ويبيّن معنى كل كلمة.",
|
| 93 |
+
"external_link": "يفتح في نافذة جديدة",
|
| 94 |
+
"resume_reading": "المتابعة من علامة القراءة",
|
| 95 |
+
"resume_audio": "استكمال الاستماع من آخر موضع",
|
| 96 |
+
"khatma": "خطة الختمة وورد اليوم",
|
| 97 |
+
"reading_position": "علامة القراءة: {surah} · الآية {ayah} · الصفحة {page}",
|
| 98 |
+
"saved_page": "آخر صفحة محفوظة: {page}",
|
| 99 |
+
"audio_position": "آخر استماع: {surah} · الآية {ayah} · بعد {seconds} ثانية",
|
| 100 |
+
"quran_features_error": "تعذّر تحميل ميزات الختمة. القراءة متاحة.",
|
| 101 |
+
"wird_audio_end": "انتهى ورد اليوم.",
|
| 102 |
+
"wird_range": "ورد اليوم {day}: الصفحات {start}–{end}",
|
| 103 |
+
"wird_resume_audio": "استكمال الاستماع من آخر موضع في ورد اليوم",
|
| 104 |
+
"swipe_hint": "للتنقل بين الصفحات اسحب أفقيًا، أو استخدم زرَّي الصفحة السابقة والتالية.",
|
| 105 |
+
"show_partition_status": "إظهار موضع القراءة (الجزء والحزب والربع)",
|
| 106 |
+
"show_swipe_hint": "إظهار تلميح السحب بين الصفحات",
|
| 107 |
+
"verse_no": "الآية {ayah}",
|
| 108 |
+
"page_changed": "الصفحة {page}"
|
| 109 |
+
},
|
| 110 |
+
"progress_storage_key": "muslim_clock_quran_progress_v1",
|
| 111 |
+
"swipe": {
|
| 112 |
+
"min_distance_px": 70,
|
| 113 |
+
"horizontal_ratio": 2,
|
| 114 |
+
"max_duration_ms": 700,
|
| 115 |
+
"click_suppression_ms": 400
|
| 116 |
+
},
|
| 117 |
+
"tafsirs": [
|
| 118 |
+
{
|
| 119 |
+
"id": "quran_com_tafsir",
|
| 120 |
+
"name": "التفسير (Quran.com) — يفتح على هذه الآية",
|
| 121 |
+
"url": "https://quran.com/{surah}:{ayah}/tafsirs"
|
| 122 |
+
}
|
| 123 |
+
],
|
| 124 |
+
"asbab_nuzul": [
|
| 125 |
+
{
|
| 126 |
+
"id": "altafsir_asbab",
|
| 127 |
+
"name": "أسباب النزول — التفسير الكبير (altafsir) لهذه الآية",
|
| 128 |
+
"url": "https://www.altafsir.com/AsbabAlnuzol.asp?SoraName={surah}&Ayah={ayah}&search=yes&img=A&LanguageID=1"
|
| 129 |
+
}
|
| 130 |
+
],
|
| 131 |
+
"word_meanings": [
|
| 132 |
+
{
|
| 133 |
+
"id": "corpus_wbw",
|
| 134 |
+
"name": "معاني الكلمات كلمةً كلمة (Quranic Arabic Corpus)",
|
| 135 |
+
"url": "https://corpus.quran.com/wordbyword.jsp?chapter={surah}&verse={ayah}"
|
| 136 |
+
},
|
| 137 |
+
{
|
| 138 |
+
"id": "quran_com_verse",
|
| 139 |
+
"name": "صفحة ��لآية مع معاني الكلمات (Quran.com)",
|
| 140 |
+
"url": "https://quran.com/{surah}:{ayah}"
|
| 141 |
+
}
|
| 142 |
+
]
|
| 143 |
+
}
|