Asem75 commited on
Commit
74b24df
·
verified ·
1 Parent(s): 418c353

Upload index.html

Browse files
Files changed (1) hide show
  1. index.html +517 -947
index.html CHANGED
@@ -1,991 +1,561 @@
1
- <!DOCTYPE html>
2
  <html lang="ar" dir="rtl">
3
  <head>
4
- <meta charset="UTF-8">
5
- <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
- <title>الخط الزمني الروحي</title>
 
7
  <style>
8
- :root{
9
- --sky-gradient: linear-gradient(135deg, #1a237e, #283593);
10
- --card-bg: rgba(255,255,255,0.1);
11
- --card-border: rgba(255,255,255,0.2);
12
- --text-color:#ffffff;
13
- --accent-color:#ffd700;
14
- --forbidden-bg: rgba(244,67,54,0.35);
15
- --prayer-bg: rgba(76,175,80,0.35);
16
- }
17
  *{box-sizing:border-box}
 
18
  body{
19
- margin:0; padding:16px; font-family:'Segoe UI',Tahoma,Geneva,Verdana,sans-serif;
20
- background:var(--sky-gradient); color:var(--text-color); min-height:100vh;
21
- transition:background 1.1s linear;
22
- }
23
- .container{max-width:1100px;margin:0 auto;}
24
- header{
25
- text-align:center; margin-bottom:16px; background:rgba(0,0,0,0.28);
26
- padding:16px; border-radius:16px; backdrop-filter:blur(6px);
27
- }
28
- header h1{margin:0 0 6px; font-size:22px;}
29
- #date-display{margin:2px 0; font-size:14px; opacity:.9;}
30
- #moon-container{font-size:15px; margin-top:6px;}
31
- .location-row{display:flex; gap:8px; justify-content:center; margin-top:10px; flex-wrap:wrap;}
32
- .location-row select, .location-row button{
33
- background:rgba(255,255,255,0.12); border:1px solid var(--card-border); color:#fff;
34
- padding:6px 10px; border-radius:8px; cursor:pointer; font-size:13px;
35
- }
36
- .smart-ticker-bar{
37
- background:rgba(0,0,0,0.4); border:1px solid var(--accent-color); border-radius:12px;
38
- padding:12px 18px; margin-bottom:16px; text-align:center; font-size:15px; font-weight:bold;
39
- box-shadow:0 0 15px rgba(255,215,0,0.2); animation:pulseGlow 3s infinite; display:none;
40
- align-items:center; justify-content:space-between; gap:10px;
41
- }
42
- .smart-ticker-bar.show{display:flex;}
43
- .smart-ticker-bar button{background:none;border:none;color:#ff8a80;font-size:18px;cursor:pointer;}
44
- @keyframes pulseGlow{0%{border-color:rgba(255,215,0,.4)}50%{border-color:rgba(255,215,0,.9)}100%{border-color:rgba(255,215,0,.4)}}
45
-
46
- .timeline-indicator-bar{
47
- position:relative; height:38px; background:rgba(0,0,0,0.4); border-radius:20px;
48
- margin-bottom:16px; overflow:hidden;
49
- }
50
- .current-time-pointer{
51
- position:absolute; top:2px; background:var(--accent-color); color:#000; padding:4px 12px;
52
- border-radius:15px; font-weight:bold; font-size:13px; box-shadow:0 0 10px rgba(255,215,0,.7);
53
- transition:left .5s ease;
54
- }
55
-
56
- .qibla-mini-btn{
57
- position:fixed; bottom:20px; right:20px; background:var(--accent-color); color:#000;
58
- border:none; width:52px;height:52px;border-radius:50%; font-size:22px; cursor:pointer;
59
- box-shadow:0 4px 15px rgba(0,0,0,.4); z-index:100;
60
- }
61
- .quran-fab-btn{
62
- position:fixed; bottom:20px; left:20px; background:var(--accent-color); color:#000;
63
- border:none; padding:12px 20px; border-radius:30px; font-weight:bold; cursor:pointer;
64
- box-shadow:0 4px 15px rgba(0,0,0,.4); z-index:100;
65
- }
66
-
67
- .sections-grid{display:grid; grid-template-columns:repeat(auto-fit,minmax(250px,1fr)); gap:16px;}
68
- .day-section{background:rgba(0,0,0,0.25); border-radius:15px; padding:14px; border:1px solid var(--card-border); backdrop-filter:blur(5px);}
69
- .day-section h3{margin-top:0; border-bottom:2px solid var(--accent-color); padding-bottom:8px; font-size:17px; color:var(--accent-color);}
70
- .boxes-grid{display:grid; grid-template-columns:repeat(2,1fr); gap:10px; margin-top:10px;}
71
- .time-box{
72
- position:relative; background:var(--card-bg); border:1px solid var(--card-border); border-radius:10px;
73
- padding:12px; text-align:center; cursor:pointer; overflow:hidden; transition:transform .3s ease;
74
- }
75
- .time-box:hover{transform:translateY(-3px); background:rgba(255,255,255,.2);}
76
- .time-box.forbidden{background:var(--forbidden-bg); border-color:#f44336;}
77
- .time-box.prayer{background:var(--prayer-bg); border-color:#4caf50;}
78
- .hour-label{font-size:15px; font-weight:bold; margin-bottom:4px; color:var(--accent-color);}
79
- .desc-label{font-size:11px; opacity:.9;}
80
-
81
- .modal-overlay{
82
- position:fixed; inset:0; background:rgba(0,0,0,.75); display:none; justify-content:center;
83
- align-items:center; z-index:1000; backdrop-filter:blur(5px); padding:16px;
84
- }
85
- .modal-overlay.open{display:flex;}
86
- .modal-content{
87
- background:#1e1e2f; padding:22px; border-radius:15px; width:100%; max-width:480px; color:#fff;
88
- border:1px solid rgba(255,255,255,.2); max-height:85vh; overflow-y:auto;
89
- }
90
- .close-btn{float:left; font-size:22px; cursor:pointer; color:#ff5252; background:none; border:none;}
91
- .tabs{display:flex; gap:8px; margin-bottom:16px; flex-wrap:wrap;}
92
- .tab-btn{flex:1 1 auto; padding:8px; background:rgba(255,255,255,.1); border:none; color:#fff; border-radius:6px; cursor:pointer; font-size:13px;}
93
- .tab-btn.active{background:var(--accent-color); color:#000; font-weight:bold;}
94
- .selector-group select, .radio-list button, .action-btn{
95
- width:100%; padding:10px; margin-bottom:10px; background:rgba(255,255,255,.1);
96
- border:1px solid rgba(255,255,255,.2); color:#fff; border-radius:6px; cursor:pointer; text-align:right; font-size:13px;
97
- }
98
- .action-btn{background:#4caf50; font-weight:bold; text-align:center;}
99
- .tab-content{display:none;}
100
- .tab-content.active{display:block;}
101
- .compass-box{width:170px;height:170px;margin:15px auto;border:3px solid var(--accent-color);border-radius:50%;position:relative;}
102
- .qibla-arrow{position:absolute;top:50%;left:50%;width:4px;height:75px;background:var(--accent-color);transform-origin:bottom center;transform:translate(-50%,-100%) rotate(0deg); transition:transform .2s linear;}
103
- .qibla-arrow::before{content:"";width:0;height:0;border-left:8px solid transparent;border-right:8px solid transparent;border-bottom:14px solid var(--accent-color);position:absolute;top:-14px;left:-6px;}
104
- .rawatib-box{background:rgba(255,255,255,.06); border-radius:8px; padding:10px; margin:8px 0; font-size:13px; line-height:1.7;}
105
- details summary{cursor:pointer; color:var(--accent-color); margin:6px 0; font-size:13px;}
106
- .settings-check{display:flex; align-items:center; gap:8px; margin:8px 0; font-size:13px; cursor:pointer;}
107
- .progress-outer{background:rgba(255,255,255,.15); border-radius:8px; height:10px; overflow:hidden; margin:6px 0;}
108
- .progress-inner{background:var(--accent-color); height:100%; width:0%; transition:width .4s ease;}
109
- small.muted{opacity:.7; display:block; margin-top:6px;}
110
-
111
- .radio-tabs{display:flex;gap:6px;overflow-x:auto;margin-bottom:10px;padding-bottom:4px}
112
- .radio-filter{white-space:nowrap;padding:7px 10px;border:1px solid rgba(255,255,255,.2);background:rgba(255,255,255,.08);color:#fff;border-radius:18px;cursor:pointer}
113
- .radio-filter.active{background:var(--accent-color);color:#000;font-weight:bold}
114
- .radio-card{display:grid;grid-template-columns:1fr auto auto;gap:8px;align-items:center;width:100%;padding:10px;margin-bottom:8px;background:rgba(255,255,255,.07);border:1px solid rgba(255,255,255,.14);border-radius:10px}
115
- .radio-card .radio-name{text-align:right;font-weight:bold}
116
- .radio-card small{display:block;opacity:.7;margin-top:3px}
117
- .radio-card button{border:0;background:transparent;color:#fff;cursor:pointer;font-size:18px}
118
- .radio-card.playing{border-color:var(--accent-color);box-shadow:0 0 10px rgba(255,215,0,.15)}
119
- .radio-status{font-size:12px;opacity:.75;margin:8px 0}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
120
  </style>
121
  </head>
122
  <body>
123
-
124
- <div class="container">
125
- <header>
126
- <h1>الخط الزمني الروحي</h1>
127
- <p id="date-display">جاري تحميل التاريخ...</p>
128
- <div id="moon-container"></div>
129
- <div class="location-row">
130
- <select id="citySelect" onchange="onCityChange()"></select>
131
- <button onclick="useGPSLocation()">📍 موقعي الحالي</button>
132
- <button onclick="openModal('settingsModal')">⚙️ إعدادات</button>
133
- </div>
134
- </header>
135
-
136
- <div id="smartTicker" class="smart-ticker-bar">
137
- <span id="noticeContent"></span>
138
- <button onclick="dismissActiveNotice()">✕</button>
139
- </div>
140
-
141
- <div class="timeline-indicator-bar">
142
- <div id="time-pointer" class="current-time-pointer" style="left:0%;">--:--</div>
143
- </div>
144
-
145
- <div class="sections-grid" id="sectionsGrid">
146
- <!-- تُبنى ديناميكياً بواسطة JS بناءً على مواقيت الصلاة المحسوبة -->
147
  </div>
 
 
 
 
 
 
 
148
  </div>
149
-
150
- <!-- زر القبلة -->
151
- <button class="qibla-mini-btn" onclick="openModal('qiblaModal')" title="اتجاه القبلة">🧭</button>
152
- <!-- زر القرآن والإذاعة -->
153
- <button class="quran-fab-btn" onclick="openModal('quranHubModal')">📖 القرآن الكريم</button>
154
- <button class="quran-fab-btn" onclick="openModal('islamicRadioModal')">📻 الإذاعات الإسلامية</button>
155
-
156
- <!-- نافذة القبلة -->
157
- <div id="qiblaModal" class="modal-overlay">
158
- <div class="modal-content" style="text-align:center;">
159
- <button class="close-btn" onclick="closeModal('qiblaModal')">&times;</button>
160
- <h2>اتجاه القبلة 🕋</h2>
161
- <p style="font-size:13px;opacity:.85">وجّه أعلى جهازك نحو السهم بعد تفعيل البوصلة</p>
162
- <div class="compass-box"><div id="qiblaArrow" class="qibla-arrow"></div></div>
163
- <p id="qiblaAngleText">جاري تحديد الزاوية...</p>
164
- <button class="action-btn" onclick="enableCompass()">تفعيل البوصلة الحية</button>
165
- <small class="muted">تعمل البوصلة بدقة أفضل على الهاتف مع تفعيل إذن الحساسات، وتحتاج اتصالاً آمناً (https).</small>
166
  </div>
 
 
 
 
 
 
 
 
167
  </div>
168
-
169
- <!-- نافذة القرآن والإذاعة والقبلة وختم القرآن -->
170
- <div id="quranHubModal" class="modal-overlay">
171
- <div class="modal-content">
172
- <button class="close-btn" onclick="closeModal('quranHubModal')">&times;</button>
173
- <h2>الركن القرآني والإذاعي</h2>
174
- <div class="tabs">
175
- <button class="tab-btn active" onclick="switchHubTab(this,'quranSection')">الورد اليومي</button>
176
- <button class="tab-btn" onclick="switchHubTab(this,'khatmSection')">متابعة الختم</button>
177
- </div>
178
-
179
- <div id="quranSection" class="tab-content active">
180
- <div class="selector-group">
181
- <select id="reciterSelect" onchange="onReciterChange()"><option>جاري تحميل القرّاء...</option></select>
182
- <select id="surahSelect"></select>
183
- </div>
184
- <button class="action-btn" onclick="playQuranAudio()">تشغيل السورة 🎧</button>
185
- <small class="muted">التشغيل يعتمد على واجهة mp3quran.net المفتوحة، ويحتاج اتصالاً بالإنترنت.</small>
186
- </div>
187
-
188
- <div id="khatmSection" class="tab-content">
189
- <p id="khatmProgressText">لم تبدأ بعد بمتابعة ختمتك.</p>
190
- <div class="progress-outer"><div id="khatmProgressBar" class="progress-inner"></div></div>
191
- <div class="selector-group">
192
- <select id="khatmJuzSelect"></select>
193
  </div>
194
- <button class="action-btn" style="background:var(--accent-color);color:#000" onclick="markJuzAsRead()">تحديد هذا الجزء كمقروء اليوم ✅</button>
195
- <button class="action-btn" style="background:#c62828" onclick="resetKhatm()">بدء ختمة جديدة</button>
196
- </div>
197
-
198
- <audio id="mainAudioPlayer" controls style="width:100%; margin-top:15px;"></audio>
199
- </div>
 
200
  </div>
201
 
202
- <!-- نافذة الإذاعات الإسلامية المستقلة -->
203
- <div id="islamicRadioModal" class="modal-overlay">
204
- <div class="modal-content">
205
- <button class="close-btn" onclick="closeModal('islamicRadioModal')">&times;</button>
206
- <h2>📻 الإذاعات الإسلامية</h2>
207
- <div class="radio-tabs">
208
- <button class="radio-filter active" onclick="filterRadioStations('all',this)">الكل</button>
209
- <button class="radio-filter" onclick="filterRadioStations('quran',this)">📖 القرآن</button>
210
- <button class="radio-filter" onclick="filterRadioStations('seerah_history',this)">📜 السيرة والتاريخ</button>
211
- <button class="radio-filter" onclick="filterRadioStations('faith_hadith_history',this)">📿 إيماني</button>
212
- </div>
213
- <div id="radioFavorites" class="radio-list"></div>
214
- <div id="radioList" class="radio-list"></div>
215
- <div id="radioStatus" class="radio-status" role="status" aria-live="polite">جاري تحميل الإذاعات...</div>
216
- <small class="muted">⭐ التثبيت يُحفظ محليًا. القرآن والإذاعة والأذان يستخدمون المشغل الصوتي الموحد.</small>
217
- </div>
218
- </div>
219
 
220
- <!-- نافذة تفاصيل المربع الزمني -->
221
- <div id="boxDetailModal" class="modal-overlay">
222
- <div class="modal-content">
223
- <button class="close-btn" onclick="closeModal('boxDetailModal')">&times;</button>
224
- <h2 id="boxDetailTitle"></h2>
225
- <div id="boxDetailBody"></div>
226
- </div>
227
- </div>
228
 
229
- <!-- نافذة الإعدادات -->
230
- <div id="settingsModal" class="modal-overlay">
231
- <div class="modal-content">
232
- <button class="close-btn" onclick="closeModal('settingsModal')">&times;</button>
233
- <h2>⚙️ الإعدادات</h2>
234
- <label class="settings-check">
235
- <input type="checkbox" id="toggleExternalNews" onchange="saveAppSettings()">
236
- تفعيل الأخبار والمصادر الخارجية في ركن التنبيهات
237
- </label>
238
- <label class="settings-check">
239
- <input type="checkbox" id="toggleSound" onchange="saveAppSettings()">
240
- تشغيل صوت تنبيه عند ظهور إشعار جديد
241
- </label>
242
- <small class="muted">جميع إعداداتك تُحفظ محلياً على جهازك فقط، بدون أي تسجيل دخول أو خادم مركزي.</small>
243
- </div>
244
- </div>
245
-
246
- <audio id="noticeSound" preload="auto"></audio>
247
 
 
 
 
 
 
 
 
 
 
248
  <script>
249
- /* ============================================================
250
- 1) قائمة المدن المدعومة يدوياً (خط الطول/العرض/فرق التوقيت)
251
- ============================================================ */
252
- const CITIES = [
253
- { name:"عمّان", lat:31.9454, lng:35.9284, tz:3 },
254
- { name:"الرياض", lat:24.7136, lng:46.6753, tz:3 },
255
- { name:"مكة المكرمة", lat:21.3891, lng:39.8579, tz:3 },
256
- { name:"القاهرة", lat:30.0444, lng:31.2357, tz:2 },
257
- { name:"دبي", lat:25.2048, lng:55.2708, tz:4 },
258
- { name:"إسطنبول", lat:41.0082, lng:28.9784, tz:3 },
259
- { name:"الدار البيضاء", lat:33.5731, lng:-7.5898, tz:1 },
260
- { name:"لندن", lat:51.5074, lng:-0.1278, tz:1 },
261
- ];
262
- const KAABA_LAT = 21.4225, KAABA_LNG = 39.8262;
263
-
264
- let currentLocation = null; // {lat,lng,tz,name}
265
-
266
- function populateCitySelect(){
267
- const sel = document.getElementById('citySelect');
268
- sel.innerHTML = '<option value="">اختر مدينة يدوياً...</option>' +
269
- CITIES.map((c,i)=>`<option value="${i}">${c.name}</option>`).join('');
270
- }
271
-
272
- function loadSavedLocation(){
273
- const saved = getSetting('user_location', null);
274
- if (saved) { currentLocation = saved; return true; }
275
- return false;
276
- }
277
-
278
- function onCityChange(){
279
- const idx = document.getElementById('citySelect').value;
280
- if (idx === "") return;
281
- const c = CITIES[idx];
282
- currentLocation = { lat:c.lat, lng:c.lng, tz:c.tz, name:c.name };
283
- saveSetting('user_location', currentLocation);
284
- refreshEverything();
285
- }
286
-
287
- function useGPSLocation(){
288
- if (!navigator.geolocation){ alert("المتصفح لا يدعم تحديد الموقع."); return; }
289
- navigator.geolocation.getCurrentPosition(pos=>{
290
- const tzOffsetHours = -(new Date().getTimezoneOffset())/60;
291
- currentLocation = { lat:pos.coords.latitude, lng:pos.coords.longitude, tz:tzOffsetHours, name:"موقعي الحالي (GPS)" };
292
- saveSetting('user_location', currentLocation);
293
- refreshEverything();
294
- }, err=>{
295
- alert("تعذر الوصول للموقع، الرجاء اختيار مدينة من القائمة يدوياً.");
296
- });
297
- }
298
-
299
- /* ============================================================
300
- 2) تخزين وإعدادات محلية موحّدة
301
- ============================================================ */
302
- function saveSetting(key, value){
303
- try { localStorage.setItem(key, JSON.stringify(value)); } catch(e){ console.error(e); }
304
- }
305
- function getSetting(key, fallback=null){
306
- try { const v = localStorage.getItem(key); return v ? JSON.parse(v) : fallback; }
307
- catch(e){ return fallback; }
308
- }
309
-
310
- function saveAppSettings(){
311
- const s = {
312
- externalNews: document.getElementById('toggleExternalNews').checked,
313
- sound: document.getElementById('toggleSound').checked
314
- };
315
- saveSetting('app_settings', s);
316
- }
317
- function loadAppSettingsUI(){
318
- const s = getSetting('app_settings', { externalNews:false, sound:true });
319
- document.getElementById('toggleExternalNews').checked = s.externalNews;
320
- document.getElementById('toggleSound').checked = s.sound;
321
- }
322
-
323
- /* ============================================================
324
- 3) تحويل التاريخ الهجري (خوارزمية جدولية تقريبية) + طور القمر
325
- ============================================================ */
326
- function gregorianToHijri(date){
327
- // خوارزمية تقويم أم القرى التقريبية (حسابية) - قد تختلف يوماً واحداً عن الرؤية الفعلية
328
- const jd = Math.floor((1461*(date.getFullYear()+4800+Math.floor((date.getMonth()+1-14)/12)))/4
329
- + Math.floor((367*(date.getMonth()+1-2-12*Math.floor((date.getMonth()+1-14)/12)))/12)
330
- - Math.floor((3*Math.floor((date.getFullYear()+4900+Math.floor((date.getMonth()+1-14)/12))/100))/4)
331
- + date.getDate() - 32075);
332
-
333
- let l = jd - 1948440 + 10632;
334
- const n = Math.floor((l-1)/10631);
335
- l = l - 10631*n + 354;
336
- const j = (Math.floor((10985-l)/5316))*(Math.floor((50*l)/17719)) + (Math.floor(l/5670))*(Math.floor((43*l)/15238));
337
- l = l - (Math.floor((30-j)/15))*(Math.floor((17719*j)/50)) - (Math.floor(j/16))*(Math.floor((15238*j)/43)) + 29;
338
- const hMonth = Math.floor((24*l)/709);
339
- const hDay = l - Math.floor((709*hMonth)/24);
340
- const hYear = 30*n + j - 30;
341
- return { year:hYear, month:hMonth, day:hDay };
342
- }
343
-
344
- const HIJRI_MONTHS = ["محرم","صفر","ربيع الأول","ربيع الآخر","جمادى الأولى","جمادى الآخرة",
345
- "رجب","شعبان","رمضان","شوال","ذو القعدة","ذو الحجة"];
346
-
347
- function getMoonPhase(hijriDay){
348
- if ([1,2,29,30].includes(hijriDay)) return {icon:"🌙", name:"هلال"};
349
- if (hijriDay>=13 && hijriDay<=15) return {icon:"🌕", name:"بدر (أيام البيض)"};
350
- if (hijriDay>15 && hijriDay<=27) return {icon:"🌖", name:"قمر متناقص"};
351
- if (hijriDay>2 && hijriDay<13) return {icon:"🌔", name:"قمر متزايد"};
352
- return {icon:"🌑", name:"محاق"};
353
- }
354
-
355
- /* ============================================================
356
- 4) حساب مواقيت الصلاة فلكياً (صيغ فلكية عامة - طريقة رابطة العالم الإسلامي)
357
- ============================================================ */
358
- const FAJR_ANGLE = 18, ISHA_ANGLE = 17;
359
-
360
- function dsin(d){return Math.sin(d*Math.PI/180);}
361
- function dcos(d){return Math.cos(d*Math.PI/180);}
362
- function dtan(d){return Math.tan(d*Math.PI/180);}
363
- function darcsin(x){return Math.asin(x)*180/Math.PI;}
364
- function darccos(x){return Math.acos(x)*180/Math.PI;}
365
- function darctan2(y,x){return Math.atan2(y,x)*180/Math.PI;}
366
- function fixAngle(a){a=a%360; return a<0?a+360:a;}
367
- function fixHour(h){h=h%24; return h<0?h+24:h;}
368
-
369
- function julianDate(y,m,d){
370
- if (m<=2){ y-=1; m+=12; }
371
- const A = Math.floor(y/100);
372
- const B = 2 - A + Math.floor(A/4);
373
- return Math.floor(365.25*(y+4716)) + Math.floor(30.6001*(m+1)) + d + B - 1524.5;
374
- }
375
-
376
- function sunPosition(jd){
377
- const D = jd - 2451545.0;
378
- const g = fixAngle(357.529 + 0.98560028*D);
379
- const q = fixAngle(280.459 + 0.98564736*D);
380
- const L = fixAngle(q + 1.915*dsin(g) + 0.020*dsin(2*g));
381
- const e = 23.439 - 0.00000036*D;
382
- const RA = darctan2(dcos(e)*dsin(L), dcos(L)) / 15;
383
- const eqt = q/15 - fixHour(RA);
384
- const decl = darcsin(dsin(e)*dsin(L));
385
- return { decl, eqt };
386
- }
387
-
388
- function computeAngleTime(angle, jd, lat, dhuhrHour, eqt, direction){
389
- // direction: -1 قبل الظهر (فجر)، +1 بعد الظهر (عشاء/غروب)
390
- const sp = sunPosition(jd);
391
- const decl = sp.decl;
392
- const cosH = (-dsin(angle) - dsin(lat)*dsin(decl)) / (dcos(lat)*dcos(decl));
393
- if (cosH > 1 || cosH < -1) return null; // لا يوجد حل (مناطق قطبية)
394
- const H = darccos(cosH) / 15;
395
- return dhuhrHour + direction*H;
396
- }
397
-
398
- function hoursToDate(baseDate, hours){
399
- const d = new Date(baseDate);
400
- d.setHours(0,0,0,0);
401
- d.setMinutes(Math.round(hours*60));
402
- return d;
403
- }
404
-
405
- function computePrayerTimes(date, lat, lng, tz){
406
- const jd = julianDate(date.getFullYear(), date.getMonth()+1, date.getDate());
407
- const sp = sunPosition(jd);
408
- const dhuhrHour = 12 + tz - lng/15 - sp.eqt/60;
409
-
410
- const fajrHour = computeAngleTime(FAJR_ANGLE, jd, lat, dhuhrHour, sp.eqt, -1);
411
- const sunriseHour = computeAngleTime(0.833, jd, lat, dhuhrHour, sp.eqt, -1);
412
- const asrHour = computeAsr(jd, lat, dhuhrHour, sp.eqt, lat, sp.decl);
413
- const maghribHour = computeAngleTime(0.833, jd, lat, dhuhrHour, sp.eqt, 1);
414
- const ishaHour = computeAngleTime(ISHA_ANGLE, jd, lat, dhuhrHour, sp.eqt, 1);
415
-
416
- return {
417
- fajr: hoursToDate(date, fajrHour),
418
- sunrise: hoursToDate(date, sunriseHour),
419
- dhuhr: hoursToDate(date, dhuhrHour),
420
- asr: hoursToDate(date, asrHour),
421
- maghrib: hoursToDate(date, maghribHour),
422
- isha: hoursToDate(date, ishaHour),
423
- };
424
- }
425
-
426
- function computeAsr(jd, lat, dhuhrHour, eqt, latDeg, decl){
427
- // زاوية العصر بمذهب الشافعي (معامل الظل = 1): asrAngle = -arccot(1 + tan|lat - decl|)
428
- const shadowFactor = 1;
429
- const asrAngle = -darccot(shadowFactor + dtan(Math.abs(latDeg-decl)));
430
- return computeAngleTime(asrAngle, jd, latDeg, dhuhrHour, eqt, 1);
431
- }
432
- function darccot(x){ return darctan2(1, x); }
433
-
434
- /* ============================================================
435
- 5) أثلاث الليل الحقيقية (بديل الوقت الثابت القديم 02:00)
436
- ============================================================ */
437
- function computeNightThirds(maghrib, fajrNextDay){
438
- const nightMs = fajrNextDay - maghrib;
439
- const third = nightMs/3;
440
- return {
441
- firstThirdEnd: new Date(maghrib.getTime()+third),
442
- secondThirdEnd: new Date(maghrib.getTime()+2*third),
443
- lastThirdStart: new Date(maghrib.getTime()+2*third), // بداية الثلث الأخير المستحب فيه القيام والدعاء
444
- };
445
- }
446
-
447
- /* ============================================================
448
- 6) اتجاه القبلة
449
- ============================================================ */
450
- function getQiblaDirection(lat, lng){
451
- const lat1 = lat*Math.PI/180;
452
- const lat2 = KAABA_LAT*Math.PI/180;
453
- const diffLng = (KAABA_LNG-lng)*Math.PI/180;
454
- const x = Math.sin(diffLng);
455
- const y = Math.cos(lat1)*Math.tan(lat2) - Math.sin(lat1)*Math.cos(diffLng);
456
- const bearing = Math.atan2(x,y)*180/Math.PI;
457
- return (bearing+360)%360;
458
- }
459
- let qiblaAngleFromNorth = null;
460
- function updateQiblaUI(){
461
- if (!currentLocation) return;
462
- qiblaAngleFromNorth = getQiblaDirection(currentLocation.lat, currentLocation.lng);
463
- document.getElementById('qiblaAngleText').innerText = `زاوية القبلة: ${qiblaAngleFromNorth.toFixed(0)}° من الشمال`;
464
- }
465
- function enableCompass(){
466
- if (typeof DeviceOrientationEvent !== 'undefined' && typeof DeviceOrientationEvent.requestPermission === 'function'){
467
- DeviceOrientationEvent.requestPermission().then(r=>{
468
- if (r==='granted') window.addEventListener('deviceorientation', handleOrientation);
469
- else alert("لم يُمنح إذن الوصول لحساسات الاتجاه.");
470
- }).catch(()=>alert("تعذر تفعيل حساس الاتجاه على هذا الجهاز."));
471
- } else {
472
- window.addEventListener('deviceorientation', handleOrientation);
473
  }
474
- }
475
- function handleOrientation(e){
476
- if (qiblaAngleFromNorth===null) return;
477
- const heading = e.webkitCompassHeading!==undefined ? e.webkitCompassHeading : (360 - e.alpha);
478
- const rotation = qiblaAngleFromNorth - heading;
479
- document.getElementById('qiblaArrow').style.transform = `translate(-50%,-100%) rotate(${rotation}deg)`;
480
- }
481
-
482
- /* ============================================================
483
- 7) السنن الرواتب + أذكار ما بعد الصلاة
484
- ============================================================ */
485
- const RAWATIB = {
486
- fajr: [{when:"قبلية", rakaat:2, note:"خير من الدنيا وما فيها"}],
487
- dhuhr: [{when:"قبلية", rakaat:4}, {when:"بعدية", rakaat:2}],
488
- asr: [],
489
- maghrib:[{when:"بعدية", rakaat:2}],
490
- isha: [{when:"بعدية", rakaat:2}],
491
- };
492
- const ADHKAR_AFTER_PRAYER = [
493
- "أستغفر الله (3 مرات)",
494
- "اللهم أنت السلام ومنك السلام تباركت يا ذا الجلال والإكرام",
495
- "سبحان الله (33)، الحمد لله (33)، الله أكبر (34)، وتمام المئة: لا إله إلا الله وحده لا شريك له",
496
- "آية الكرسي"
497
- ];
498
-
499
- /* ============================================================
500
- 8) المناسبات الهجرية والصيام المسنون وساعة إجابة الجمعة
501
- ============================================================ */
502
- const HIJRI_OCCASIONS = [
503
- {month:1, day:1, msg:"🌅 رأس السنة الهجرية، بداية عام جديد بالتوبة والعزيمة."},
504
- {month:1, day:9, msg:"📿 غداً عاشوراء، يُستحب صيام التاسع والعاشر معاً."},
505
- {month:1, day:10, msg:"📿 اليوم عاشوراء، يوم صيام مستحب."},
506
- {month:3, day:12, msg:"🌟 ذكرى المولد النبوي الشريف."},
507
- {month:7, day:27, msg:"🌙 ذكرى الإسراء والمعراج."},
508
- {month:8, day:15, msg:"🌕 ليلة النصف من شعبان."},
509
- {month:9, day:1, msg:"🌙 مرحباً رمضان! شهر الصيام والقرآن والقيام."},
510
- {month:9, day:21, msg:"✨ من الليالي الوترية المرجوة لليلة القدر."},
511
- {month:9, day:23, msg:"✨ من الليالي الوترية المرجوة لليلة القدر."},
512
- {month:9, day:25, msg:"✨ من الليالي الوترية المرجوة لليلة القدر."},
513
- {month:9, day:27, msg:"✨ من أرجى ليالي القدر."},
514
- {month:9, day:29, msg:"✨ من الليالي الوترية المرجوة لليلة القدر."},
515
- {month:10, day:1, msg:"🎉 عيد الفطر المبارك، تقبل الله منا ومنكم."},
516
- {month:12, day:1, msg:"🕋 بداية عشر ذي الحجة، أفضل أيام الدنيا للعمل الصالح."},
517
- {month:12, day:9, msg:"🌄 يوم عرفة، يُستحب صيامه لغير الحاج."},
518
- {month:12, day:10, msg:"🎉 عيد الأضحى المبارك."},
519
- ];
520
- const ZAKAT_FITR_REMINDER = {month:9, startDay:25, endDay:29, msg:"💰 لا تنسَ إخراج زكاة الفطر قبل صلاة العيد."};
521
-
522
- function getSunnahFastingMessages(jsDate, hijriDay){
523
- const msgs = [];
524
- const wd = jsDate.getDay(); // 0=أحد...5=جمعة...
525
- if (wd===1) msgs.push("🌙 اليوم الاثنين، من أيام الصيام المسنونة.");
526
- if (wd===4) msgs.push("🌙 اليوم الخميس، من أيام الصيام المسنونة.");
527
- if (hijriDay>=13 && hijriDay<=15) msgs.push("⚪ اليوم من الأيام البيض، يُستحب صيامها.");
528
- return msgs;
529
- }
530
-
531
- /* ============================================================
532
- 9) محرك التنبيهات ذو الأولوية (يعرض عنصراً واحداً فقط في كل مرة)
533
- ============================================================ */
534
- let activeNoticeKey = null;
535
-
536
- function buildNoticeQueue(hijri, jsDate, prayerTimes){
537
- const queue = [];
538
-
539
- // أولوية 1: مناسبة هجرية كبرى
540
- const occ = HIJRI_OCCASIONS.find(o=>o.month===hijri.month && o.day===hijri.day);
541
- if (occ) queue.push({key:`occ_${hijri.month}_${hijri.day}`, msg:occ.msg, dailyReset:true});
542
-
543
- // أولوية 2: زكاة الفطر
544
- const zf = ZAKAT_FITR_REMINDER;
545
- if (hijri.month===zf.month && hijri.day>=zf.startDay && hijri.day<=zf.endDay){
546
- queue.push({key:'zakat_fitr', msg:zf.msg, dailyReset:true});
547
  }
548
-
549
- // أولوية 3: ساعة إجابة الجمعة (آخر ساعة قبل المغرب)
550
- if (jsDate.getDay()===5 && prayerTimes){
551
- const minutesToMaghrib = (prayerTimes.maghrib - jsDate)/60000;
552
- if (minutesToMaghrib>0 && minutesToMaghrib<=60){
553
- queue.push({key:'friday_dua_hour', msg:"🤲 أنت الآن في ساعة الإجابة المرجوّة يوم الجمعة، أكثر من الدعاء.", repeatHours:1});
554
  }
 
555
  }
556
-
557
- // أولوية 4: تنبيه الجمعة الثابت (سورة الكهف)
558
- if (jsDate.getDay()===5){
559
- queue.push({key:'friday_special', msg:"✨ تذكير الجمعة: لا تنسَ قراءة سورة الكهف والإكثار من الصلاة على النبي ﷺ.", repeatHours:4});
 
 
560
  }
561
-
562
- // أولوية 5: صيام مسنون
563
- getSunnahFastingMessages(jsDate, hijri.day).forEach((m,i)=>{
564
- queue.push({key:`fasting_${hijri.day}_${i}`, msg:m, dailyReset:true});
565
- });
566
-
567
- // أولوية 6: الأخبار الخارجية (إن فُعّلت)
568
- const settings = getSetting('app_settings', {externalNews:false});
569
- if (settings.externalNews){
570
- queue.push({key:'external_news', msg:"📰 توجد أخبار جديدة من مصادرك الخارجية المفعّلة.", repeatHours:2});
571
  }
572
-
573
- return queue;
574
- }
575
-
576
- function pickNoticeToShow(queue){
577
- const now = Date.now();
578
- for (const item of queue){
579
- const dismissKey = `dismissed_${item.key}`;
580
- const lastDismissed = getSetting(dismissKey, null);
581
- if (item.dailyReset){
582
- const todayStr = new Date().toISOString().split('T')[0];
583
- if (lastDismissed === todayStr) continue;
584
- } else if (item.repeatHours && lastDismissed){
585
- const hoursPassed = (now - lastDismissed)/3600000;
586
- if (hoursPassed < item.repeatHours) continue;
587
- }
588
- return item;
589
  }
590
- return null;
591
- }
592
-
593
- function refreshNoticeBar(hijri, jsDate, prayerTimes){
594
- const queue = buildNoticeQueue(hijri, jsDate, prayerTimes);
595
- const toShow = pickNoticeToShow(queue);
596
- const bar = document.getElementById('smartTicker');
597
- if (toShow){
598
- activeNoticeKey = toShow;
599
- document.getElementById('noticeContent').innerText = toShow.msg;
600
- if (!bar.classList.contains('show')){
601
- bar.classList.add('show');
602
- const settings = getSetting('app_settings', {sound:true});
603
- if (settings.sound){
604
- const snd = document.getElementById('noticeSound');
605
- snd.src = "https://assets.mixkit.co/active_storage/sfx/2869/2869-preview.mp3";
606
- snd.play().catch(()=>{});
607
- }
608
  }
609
- } else {
610
- bar.classList.remove('show');
611
- activeNoticeKey = null;
612
  }
613
- }
614
-
615
- function dismissActiveNotice(){
616
- if (!activeNoticeKey) return;
617
- const item = activeNoticeKey;
618
- const dismissKey = `dismissed_${item.key}`;
619
- if (item.dailyReset){
620
- saveSetting(dismissKey, new Date().toISOString().split('T')[0]);
621
- } else {
622
- saveSetting(dismissKey, Date.now());
623
  }
624
- document.getElementById('smartTicker').classList.remove('show');
625
- activeNoticeKey = null;
626
- }
627
-
628
- /* ============================================================
629
- 10) الخلفية المتدرجة التفاعلية المستمرة (مبنية على مواقيت الصلاة الحقيقية)
630
- بدل القفز بين 6 ألوان ثابتة كل ساعة، تُحسب درجة اللون بالتدرّج الدقيق
631
- بين كل مناسبة فلكية والتي تليها (فجر، شروق، عصر، مغرب، عشاء...)
632
- ============================================================ */
633
- const SKY_COLORS = {
634
- deepNight: [[3,7,30], [17,27,46], [11,9,10]],
635
- lastThird: [[10,5,40], [25,10,60], [15,8,35]], // تلوين خاص لبداية الثلث الأخير من الليل
636
- preDawn: [[13,27,42], [65,90,119],[119,141,169]],
637
- sunrise: [[255,123,0],[255,183,3],[251,133,0]],
638
- midDay: [[33,158,188],[142,202,230],[2,48,71]],
639
- preSunset: [[217,4,41], [239,35,60],[255,183,3]],
640
- sunset: [[58,12,163],[114,9,183],[247,37,133]],
641
- postSunset: [[30,10,60], [60,20,90], [20,8,40]],
642
- };
643
-
644
- function addMinutes(date, mins){ return new Date(date.getTime() + mins*60000); }
645
-
646
- function lerp(a,b,t){ return a + (b-a)*t; }
647
- function interpolateColor(c1,c2,t){ return c1.map((v,i)=> Math.round(lerp(v, c2[i], t))); }
648
- function rgbStr(c){ return `rgb(${c[0]},${c[1]},${c[2]})`; }
649
-
650
- let cachedSkyAnchors = null;
651
-
652
- function buildSkyAnchors(ptYesterday, ptToday, ptTomorrow, nightThirdsBeforeToday, nightThirdsAfterToday){
653
- return [
654
- { t: addMinutes(ptYesterday.isha, 30), color: SKY_COLORS.deepNight },
655
- { t: nightThirdsBeforeToday.lastThirdStart, color: SKY_COLORS.lastThird },
656
- { t: addMinutes(ptToday.fajr, -20), color: SKY_COLORS.lastThird },
657
- { t: ptToday.fajr, color: SKY_COLORS.preDawn },
658
- { t: ptToday.sunrise, color: SKY_COLORS.sunrise },
659
- { t: addMinutes(ptToday.sunrise, 90), color: SKY_COLORS.midDay },
660
- { t: ptToday.asr, color: SKY_COLORS.midDay },
661
- { t: addMinutes(ptToday.maghrib, -45), color: SKY_COLORS.preSunset },
662
- { t: ptToday.maghrib, color: SKY_COLORS.sunset },
663
- { t: addMinutes(ptToday.maghrib, 60), color: SKY_COLORS.postSunset },
664
- { t: ptToday.isha, color: SKY_COLORS.deepNight },
665
- { t: addMinutes(ptToday.isha, 30), color: SKY_COLORS.deepNight },
666
- { t: nightThirdsAfterToday.lastThirdStart, color: SKY_COLORS.lastThird },
667
- { t: addMinutes(ptTomorrow.fajr, -20), color: SKY_COLORS.lastThird },
668
- { t: ptTomorrow.fajr, color: SKY_COLORS.preDawn },
669
- ];
670
- }
671
-
672
- function updateSkyGradientContinuous(now){
673
- if (!cachedSkyAnchors) return;
674
- const anchors = cachedSkyAnchors;
675
- for (let i=0;i<anchors.length-1;i++){
676
- const a = anchors[i], b = anchors[i+1];
677
- if (now >= a.t && now <= b.t){
678
- const total = b.t - a.t;
679
- const frac = total > 0 ? (now - a.t)/total : 0;
680
- const stops = a.color.map((c,idx)=> interpolateColor(c, b.color[idx], frac));
681
- document.documentElement.style.setProperty('--sky-gradient', `linear-gradient(135deg, ${stops.map(rgbStr).join(', ')})`);
682
- return;
683
  }
684
  }
685
- // خارج النطاق المحسوب (نادر): استخدم أقرب لون طرفي
686
- const edge = now < anchors[0].t ? anchors[0] : anchors[anchors.length-1];
687
- document.documentElement.style.setProperty('--sky-gradient', `linear-gradient(135deg, ${edge.color.map(rgbStr).join(', ')})`);
688
- }
689
-
690
- /* ============================================================
691
- 11) بناء المربعات الزمنية من مواقيت حقيقية
692
- ============================================================ */
693
- function fmtTime(d){ return d.toLocaleTimeString('ar-EG',{hour:'2-digit',minute:'2-digit'}); }
694
-
695
- function buildSectionsHTML(pt, nightThirds){
696
- const grid = document.getElementById('sectionsGrid');
697
- const sections = [
698
- { title:"الفجر والشروق", boxes:[
699
- {key:'fajr', label:fmtTime(pt.fajr), desc:'صلاة الفجر', cls:'prayer'},
700
- {key:'sunrise_forbidden', label:fmtTime(pt.sunrise), desc:'وقت نهي (الشروق)', cls:'forbidden'},
701
- ]},
702
- { title:"الضحى والظهيرة", boxes:[
703
- {key:'dhuhr', label:fmtTime(pt.dhuhr), desc:'صلاة الظهر', cls:'prayer'},
704
- {key:'istiwa_forbidden', label:fmtTime(pt.dhuhr), desc:'وقت الاستواء (نهي قبيل الظهر)', cls:'forbidden'},
705
- ]},
706
- { title:"العصر والمغرب", boxes:[
707
- {key:'asr', label:fmtTime(pt.asr), desc:'صلاة العصر', cls:'prayer'},
708
- {key:'maghrib', label:fmtTime(pt.maghrib), desc:'صلاة المغرب', cls:'prayer'},
709
- ]},
710
- { title:"الليل والمساء", boxes:[
711
- {key:'isha', label:fmtTime(pt.isha), desc:'صلاة العشاء', cls:'prayer'},
712
- {key:'qiyam', label: nightThirds? fmtTime(nightThirds.lastThirdStart):'--:--', desc:'بداية الثلث الأخير (قيام الليل)', cls:''},
713
- ]},
714
- ];
715
-
716
- grid.innerHTML = sections.map(s=>`
717
- <div class="day-section">
718
- <h3>${s.title}</h3>
719
- <div class="boxes-grid">
720
- ${s.boxes.map(b=>`
721
- <div class="time-box ${b.cls}" onclick="openBoxDetail('${b.key}','${b.desc}','${b.label}')">
722
- <div class="hour-label">${b.label}</div>
723
- <div class="desc-label">${b.desc}</div>
724
- </div>`).join('')}
725
- </div>
726
- </div>
727
- `).join('');
728
- }
729
-
730
- function openBoxDetail(key, desc, timeLabel){
731
- document.getElementById('boxDetailTitle').innerText = `${desc} — ${timeLabel}`;
732
- let html = '';
733
-
734
- const prayerMap = {fajr:'fajr', dhuhr:'dhuhr', asr:'asr', maghrib:'maghrib', isha:'isha'};
735
- if (prayerMap[key]){
736
- const raw = RAWATIB[prayerMap[key]] || [];
737
- if (raw.length){
738
- html += `<div class="rawatib-box"><b>السنن الرواتب:</b><br>` +
739
- raw.map(r=>`🕋 ${r.when}: ${r.rakaat} ركعات ${r.note?'- '+r.note:''}`).join('<br>') +
740
- `</div>`;
741
- }
742
- html += `<details><summary>📿 أذكار ما بعد الصلاة</summary><ul>` +
743
- ADHKAR_AFTER_PRAYER.map(a=>`<li style="margin-bottom:6px;">${a}</li>`).join('') + `</ul></details>`;
744
  }
745
-
746
- if (key === 'qiyam'){
747
- html += `<p style="font-size:13px;line-height:1.8;">الثلث الأخير من الليل من أفضل أوقات الدعاء والاستغفار وصلاة الوتر وقيام الليل. يبدأ هذا الوقت حسابياً بين المغرب والفجر بناءً على موقعك الحالي.</p>`;
 
 
 
 
748
  }
749
- if (key === 'sunrise_forbidden' || key === 'istiwa_forbidden'){
750
- html += `<p style="font-size:13px;line-height:1.8;">هذا من أوقات النهي عن الصلاة (بلا سبب)، يُستحسن تجنّب أداء نوافل الصلاة في هذا الوقت تحديداً.</p>`;
 
 
 
 
 
 
 
 
 
 
 
 
 
751
  }
752
-
753
- const savedReminder = getSetting(`reminder_${key}`, '');
754
- html += `<hr style="border-color:rgba(255,255,255,.15); margin:14px 0;">
755
- <label style="font-size:13px;">تذكير خاص بهذا الوقت (يُحفظ على جهازك):</label>
756
- <input id="reminderInput" type="text" value="${savedReminder}" placeholder="مثال: ورد القرآن، أذكار..."
757
- style="width:100%; padding:8px; margin-top:6px; border-radius:6px; border:1px solid var(--card-border); background:rgba(255,255,255,.1); color:#fff;">
758
- <button class="action-btn" style="margin-top:10px;" onclick="saveReminder('${key}')">حفظ التذكير</button>`;
759
-
760
- document.getElementById('boxDetailBody').innerHTML = html;
761
- openModal('boxDetailModal');
762
- }
763
- function saveReminder(key){
764
- const val = document.getElementById('reminderInput').value;
765
- saveSetting(`reminder_${key}`, val);
766
- alert('تم حفظ التذكير على جهازك.');
767
- }
768
-
769
- /* ============================================================
770
- 12) متابعة ختم القرآن (بقائمة أجزاء مستقلة عن قائمة السور)
771
- ============================================================ */
772
- function populateKhatmJuzSelect(){
773
- const sel = document.getElementById('khatmJuzSelect');
774
- sel.innerHTML = '';
775
- for (let i=1;i<=30;i++){
776
- const opt = document.createElement('option');
777
- opt.value = i; opt.innerText = `الجزء رقم ${i}`;
778
- sel.appendChild(opt);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
779
  }
780
- }
781
- function playRadio(url){
782
- if (window.unifiedAudioManager) return window.unifiedAudioManager.playStream(url,{title:"إذاعة إسلامية",artist:"ساعة المسلم",kind:"radio"});
783
- const p=document.getElementById('mainAudioPlayer'); p.src=url;
784
- return p.play().catch(()=>alert('تعذر تشغيل البث، الرجاء المحاولة لاحقاً أو التأكد من اتصال الإنترنت.'));
785
- }
786
- function markJuzAsRead(){
787
- const num = parseInt(document.getElementById('khatmJuzSelect').value, 10);
788
- let progress = getSetting('khatm_progress', {completedJuz:[], startDate:new Date().toISOString()});
789
- if (!progress.completedJuz.includes(num)) progress.completedJuz.push(num);
790
- saveSetting('khatm_progress', progress);
791
- refreshKhatmUI();
792
- if (progress.completedJuz.length===30){
793
- alert('🎉 مبارك الختمة! تقبّل الله منك.');
794
  }
795
- }
796
- function refreshKhatmUI(){
797
- const progress = getSetting('khatm_progress', {completedJuz:[]});
798
- const percent = Math.round((progress.completedJuz.length/30)*100);
799
- document.getElementById('khatmProgressText').innerText = `أنجزت ${progress.completedJuz.length} من 30 جزءاً (${percent}%)`;
800
- document.getElementById('khatmProgressBar').style.width = percent+'%';
801
- }
802
- function resetKhatm(){
803
- if (confirm('هل تريد بدء ختمة جديدة من الصفر؟')){
804
- saveSetting('khatm_progress', {completedJuz:[], startDate:new Date().toISOString()});
805
- refreshKhatmUI();
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
806
  }
807
- }
808
-
809
- /* ============================================================
810
- 13) أدوات عامة: نوافذ منبثقة وتبويبات
811
- ============================================================ */
812
- function openModal(id){ document.getElementById(id).classList.add('open'); }
813
- function closeModal(id){
814
- document.getElementById(id).classList.remove('open');
815
- /* المشغل الموحد يستمر عند إغلاق النافذة */
816
- }
817
- /* ============================================================
818
- 13.5) القرّاء الحقيقيون وتشغيل السور مباشرة (mp3quran.net API)
819
- ============================================================ */
820
- const SURAH_NAMES = ["الفاتحة","البقرة","آل عمران","النساء","المائدة","الأنعام","الأعراف","الأنفال","التوبة","يونس",
821
- "هود","يوسف","الرعد","إبراهيم","الحجر","النحل","الإسراء","الكهف","مريم","طه",
822
- "الأنبياء","الحج","المؤمنون","النور","الفرقان","الشعراء","النمل","القصص","العنكبوت","الروم",
823
- "لقمان","السجدة","الأحزاب","سبأ","فاطر","يس","الصافات","ص","الزمر","غافر",
824
- "فصلت","الشورى","الزخرف","الدخان","الجاثية","الأحقاف","محمد","الفتح","الحجرات","ق",
825
- "الذاريات","الطور","النجم","القمر","الرحمن","الواقعة","الحديد","المجادلة","الحشر","الممتحنة",
826
- "الصف","الجمعة","المنافقون","التغابن","الطلاق","التحريم","الملك","القلم","الحاقة","المعارج",
827
- "نوح","الجن","المزمل","المدثر","القيامة","الإنسان","المرسلات","النبأ","النازعات","عبس",
828
- "التكوير","الإنفطار","المطففين","الإنشقاق","البروج","الطارق","الأعلى","الغاشية","الفجر","البلد",
829
- "الشمس","الليل","الضحى","الشرح","التين","العلق","القدر","البينة","الزلزلة","العاديات",
830
- "القارعة","التكاثر","العصر","الهمزة","الفيل","قريش","الماعون","الكوثر","الكافرون","النصر",
831
- "المسد","الإخلاص","الفلق","الناس"];
832
-
833
- // بديل احتياطي موثّق (بصيغة server مؤكدة من mp3quran.net) في حال تعذّر جلب القائمة الكاملة من الإنترنت
834
- const FALLBACK_RECITERS = [
835
- { name:"مشاري راشد العفاسي", moshaf:[{ server:"https://server8.mp3quran.net/afs/", surah_list: Array.from({length:114},(_,i)=>i+1).join(',') }] },
836
- { name:"محمد صديق المنشاوي (مجوّد)", moshaf:[{ server:"https://server11.mp3quran.net/mhsny/", surah_list: Array.from({length:114},(_,i)=>i+1).join(',') }] },
837
- ];
838
-
839
- let recitersData = [];
840
-
841
- async function loadReciters(){
842
- try {
843
- const res = await fetch('https://www.mp3quran.net/api/v3/reciters?language=ar');
844
- const data = await res.json();
845
- if (data.reciters && data.reciters.length){
846
- recitersData = data.reciters;
847
- } else {
848
- recitersData = FALLBACK_RECITERS;
849
  }
850
- } catch(e){
851
- console.warn('تعذر الاتصال بواجهة mp3quran.net، سيتم استخدام قائمة قراء احتياطية محدودة.', e);
852
- recitersData = FALLBACK_RECITERS;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
853
  }
854
- populateReciterSelect();
855
- }
856
-
857
- function populateReciterSelect(){
858
- const sel = document.getElementById('reciterSelect');
859
- sel.innerHTML = recitersData.map((r,i)=>`<option value="${i}">${r.name}</option>`).join('');
860
- onReciterChange();
861
- }
862
-
863
- function onReciterChange(){
864
- const idx = document.getElementById('reciterSelect').value;
865
- const reciter = recitersData[idx];
866
- if (!reciter || !reciter.moshaf || !reciter.moshaf.length) return;
867
- const moshaf = reciter.moshaf[0];
868
- const allowedSurahs = (moshaf.surah_list || '').split(',').map(n=>parseInt(n,10)).filter(Boolean);
869
-
870
- const surahSel = document.getElementById('surahSelect');
871
- surahSel.innerHTML = allowedSurahs.map(n=>`<option value="${n}">${n}. ${SURAH_NAMES[n-1] || 'سورة '+n}</option>`).join('');
872
- }
873
-
874
- function playQuranAudio(){
875
- const reciterIdx = document.getElementById('reciterSelect').value;
876
- const reciter = recitersData[reciterIdx];
877
- if (!reciter){ alert('لم يتم تحميل قائمة القراء بعد، حاول مجدداً بعد قليل.'); return; }
878
- const moshaf = reciter.moshaf[0];
879
- const surahNum = parseInt(document.getElementById('surahSelect').value, 10);
880
- if (!surahNum){ alert('الرجاء اختيار سورة أولاً.'); return; }
881
- const padded = String(surahNum).padStart(3,'0');
882
- const url = moshaf.server + padded + '.mp3';
883
-
884
- if (window.unifiedAudioManager) return window.unifiedAudioManager.playStream(url,{title:SURAH_NAMES[surahNum-1] || ('سورة '+surahNum),artist:reciter.name,kind:'quran'});
885
- const player=document.getElementById('mainAudioPlayer'); player.src=url;
886
- player.play().catch(()=>alert('تعذر تشغيل الصوت، تحقق من اتصال الإنترنت أو جرّب قارئاً آخر.'));
887
- }
888
-
889
- function switchHubTab(btn, sectionId){
890
- document.querySelectorAll('#quranHubModal .tab-btn').forEach(b=>b.classList.remove('active'));
891
- document.querySelectorAll('#quranHubModal .tab-content').forEach(c=>c.classList.remove('active'));
892
- btn.classList.add('active');
893
- document.getElementById(sectionId).classList.add('active');
894
- }
895
-
896
- /* ============================================================
897
- 14) المحرك الرئيسي: يشغّل كل شيء ويحدّثه دورياً
898
- ============================================================ */
899
- let cachedPrayerTimes = null;
900
- let cachedNightThirds = null;
901
-
902
- function refreshEverything(){
903
- if (!currentLocation) return;
904
- const now = new Date();
905
-
906
- const hijri = gregorianToHijri(now);
907
- const moon = getMoonPhase(hijri.day);
908
- document.getElementById('date-display').innerText =
909
- `${now.toLocaleDateString('ar-EG')} م — ${hijri.day} ${HIJRI_MONTHS[hijri.month-1]} ${hijri.year} هـ • ${currentLocation.name}`;
910
-
911
- const { lat, lng, tz } = currentLocation;
912
- const yesterday = addMinutes(now, -1440);
913
- const tomorrow = addMinutes(now, 1440);
914
-
915
- const ptYesterday = computePrayerTimes(yesterday, lat, lng, tz);
916
- const ptToday = computePrayerTimes(now, lat, lng, tz);
917
- const ptTomorrow = computePrayerTimes(tomorrow, lat, lng, tz);
918
-
919
- cachedPrayerTimes = ptToday;
920
- const nightThirdsBeforeToday = computeNightThirds(ptYesterday.maghrib, ptToday.fajr);
921
- const nightThirdsAfterToday = computeNightThirds(ptToday.maghrib, ptTomorrow.fajr);
922
- cachedNightThirds = nightThirdsAfterToday;
923
-
924
- cachedSkyAnchors = buildSkyAnchors(ptYesterday, ptToday, ptTomorrow, nightThirdsBeforeToday, nightThirdsAfterToday);
925
-
926
- // عرض حالة الليل/النهار بناءً على المواقيت الفعلية بدل ساعات ثابتة
927
- const isNight = now >= ptToday.maghrib || now < ptToday.fajr;
928
- document.getElementById('moon-container').innerText = isNight
929
- ? `${moon.icon} ${moon.name}`
930
- : `☀️ نهار مبارك`;
931
-
932
- buildSectionsHTML(cachedPrayerTimes, cachedNightThirds);
933
- updateQiblaUI();
934
- refreshNoticeBar(hijri, now, cachedPrayerTimes);
935
- }
936
-
937
- function tick(){
938
- const now = new Date();
939
- const totalMinutes = now.getHours()*60 + now.getMinutes();
940
- updateSkyGradientContinuous(now);
941
-
942
- document.getElementById('time-pointer').innerText = fmtTime(now);
943
- const pct = Math.min((totalMinutes/1440)*94, 94);
944
- document.getElementById('time-pointer').style.left = pct+'%';
945
- }
946
-
947
- function initApp(){
948
- populateCitySelect();
949
- loadAppSettingsUI();
950
- populateKhatmJuzSelect();
951
- loadReciters();
952
- refreshKhatmUI();
953
-
954
- if (!loadSavedLocation()){
955
- // موقع افتراضي مبدئي (مكة المكرمة) حتى يختار المستخدم أو يفعّل GPS
956
- currentLocation = { lat:CITIES[0].lat, lng:CITIES[0].lng, tz:CITIES[0].tz, name:CITIES[0].name };
957
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
958
 
959
- refreshEverything();
960
- tick();
961
- setInterval(tick, 1000);
962
- setInterval(refreshEverything, 5*60*1000); // تحديث المواقيت والتنبيهات كل 5 دقائق
963
- }
964
-
965
- window.addEventListener('DOMContentLoaded', initApp);
966
- </script>
967
- \n<script type="module">
968
- class UnifiedAudioManager{
969
- constructor(audio){this.audio=audio;this.kind=null;this.title="";this.stationId=null;}
970
- async playStream(url,meta={}){try{this.audio.pause()}catch{} this.audio.src=url;this.kind=meta.kind||"audio";this.title=meta.title||"";this.stationId=meta.stationId||null;this.setMedia(meta);try{await this.audio.play();return true}catch(e){alert("تعذر تشغيل الصوت. تحقق من اتصال الإنترنت أو اضغط تشغيل مرة أخرى.");return false}}
971
- stop(){try{this.audio.pause()}catch{} this.audio.removeAttribute("src");try{this.audio.load()}catch{} this.kind=null;this.title="";this.stationId=null}
972
- pause(){this.audio.pause()} resume(){return this.audio.play()}
973
- setMedia(meta){if(!("mediaSession" in navigator)||typeof MediaMetadata==="undefined")return;try{navigator.mediaSession.metadata=new MediaMetadata({title:meta.title||"ساعة المسلم",artist:meta.artist||"ساعة المسلم",album:meta.kind==="radio"?"الإذاعات الإسلامية":"القرآن الكريم"});for(const [n,h] of [["play",()=>this.resume()],["pause",()=>this.pause()],["stop",()=>this.stop()]]){try{navigator.mediaSession.setActionHandler(n,h)}catch{}}}catch{}}
974
- }
975
- window.unifiedAudioManager=new UnifiedAudioManager(document.getElementById("mainAudioPlayer"));
976
- let islamicRadioRegistry={stations:[]},radioCategory="all";
977
- function radioFavorites(){try{return JSON.parse(localStorage.getItem("muslim_clock_radio_favorites_v1")||"[]")}catch{return[]}}
978
- function radioStations(){const f=radioFavorites();return (islamicRadioRegistry.stations||[]).map(s=>({...s,favorite:f.includes(s.id)}))}
979
- window.toggleRadioFavorite=function(id){const k="muslim_clock_radio_favorites_v1";let f=radioFavorites();const i=f.indexOf(id);i>=0?f.splice(i,1):f.push(id);localStorage.setItem(k,JSON.stringify(f));renderRadioStations()}
980
- window.filterRadioStations=function(c,b){radioCategory=c;document.querySelectorAll("#islamicRadioModal .radio-filter").forEach(x=>x.classList.remove("active"));if(b)b.classList.add("active");renderRadioStations()}
981
- window.playIslamicStation=async function(id){const s=(islamicRadioRegistry.stations||[]).find(x=>x.id===id);if(!s)return;const url=s.streamUrl||s.stream_url||"";const status=document.getElementById("radioStatus");if(!url){status.textContent="لا يوجد رابط بث مباشر مسجل لهذه الإذاعة؛ سيتم فتح المصدر الرسمي.";if(s.officialUrl)window.open(s.officialUrl,"_blank","noopener");return}status.textContent="جاري تشغيل "+s.name+"...";const ok=await window.unifiedAudioManager.playStream(url,{title:s.name,artist:"ساعة المسلم",stationId:s.id,kind:"radio"});status.textContent=ok?"يعمل الآن: "+s.name:"تعذر تشغيل بث هذه الإذاعة من الرابط الحالي. جرّب المصدر الرسمي."}
982
- function renderRadioStations(){const all=radioStations(),f=radioCategory==="all"?all:all.filter(s=>s.category===radioCategory),fav=f.filter(s=>s.favorite),rest=f.filter(s=>!s.favorite);const card=s=>`<div class="radio-card"><div class="radio-name">${String(s.name||"إذاعة إسلامية").replace(/"/g,"&quot;")}<small>${s.country||""}</small></div><button onclick="toggleRadioFavorite('${s.id}')">${s.favorite?"⭐":"☆"}</button><button onclick="playIslamicStation('${s.id}')">${s.streamUrl?"▶️":"🔗"}</button></div>`;document.getElementById("radioFavorites").innerHTML=fav.length?`<div class="muted">⭐ الإذاعات المثبتة</div>${fav.map(card).join("")}`:"";document.getElementById("radioList").innerHTML=rest.map(card).join("")||`<div class="muted">لا توجد إذاعات في هذا التصنيف.</div>`}
983
- async function loadRadioRegistry(){let data=null;for(const path of ["islamic_radio_registry.json","islamic_radio_registry.json.txt"]){try{const r=await fetch(path,{cache:"no-store"});if(!r.ok)continue;data=await r.json();break}catch{}}if(!data){document.getElementById("radioStatus").textContent="تعذر تحميل قائمة الإذاعات. تأكد من وجود ملف Registry.";return}islamicRadioRegistry=data;renderRadioStations();document.getElementById("radioStatus").textContent="تم تحميل الإذاعات."}
984
- loadRadioRegistry();
985
- </script>\n<script>
986
- /* ===== الأذان من Registry مستقل، مع نفس المشغل الموحد ===== */
987
- window.playAdhanFromRegistry=async function(prayerKey){try{const r=await fetch("adhan_audio_registry.json",{cache:"no-store"});if(!r.ok)throw 0;const reg=await r.json();const id=(reg.prayer_sources&&reg.prayer_sources[prayerKey])||reg.default_sound_id;const s=(reg.sounds||[]).find(x=>x.id===id)||((reg.sounds||[]).find(x=>x.prayer===prayerKey));if(!s)return false;return window.unifiedAudioManager.playStream(s.url||s.audio_url||s.src,{title:s.name||"الأذان",artist:s.artist||"الأذان",kind:"adhan"});}catch{return false}};
988
- window.playAdhanFromRadio=async function(prayerKey){try{const r=await fetch("islamic_radio_registry.json",{cache:"no-store"});if(!r.ok)throw 0;const reg=await r.json();const b=reg.adhan_sources&&reg.adhan_sources[prayerKey];if(!b)return false;const id=typeof b==="string"?b:b.station_id;const s=(reg.stations||[]).find(x=>x.id===id);if(!s)return false;const url=s.streamUrl||s.stream_url;if(!url)return false;return window.unifiedAudioManager.playStream(url,{title:s.name||"أذان من الإذاعة",artist:"ساعة المسلم",stationId:s.id,kind:"adhan"});}catch{return false}};
989
  </script>
990
  </body>
991
  </html>
 
1
+ <!doctype html>
2
  <html lang="ar" dir="rtl">
3
  <head>
4
+ <meta charset="utf-8">
5
+ <meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover">
6
+ <title></title>
7
+ <link rel="stylesheet" href="quran_reader.css?v=20261001-nav2">
8
  <style>
 
 
 
 
 
 
 
 
 
9
  *{box-sizing:border-box}
10
+ html,body{margin:0;min-height:100%;font-family:system-ui,-apple-system,"Noto Sans Arabic",sans-serif}
11
  body{
12
+ background:linear-gradient(180deg,var(--paint-1),var(--paint-2));
13
+ color:var(--paint-3);padding:22px 14px;
14
+ }
15
+ main{max-width:900px;margin:auto}
16
+ h1{text-align:center;font-size:1.5rem;margin:12px 0 28px}
17
+ .ruler-shell{
18
+ direction:ltr;
19
+ width:100%;
20
+ overflow-x:auto;
21
+ overflow-y:hidden;
22
+ border:1px solid var(--paint-4);
23
+ border-radius:18px;
24
+ background:var(--paint-5);
25
+ -webkit-overflow-scrolling:touch;
26
+ scrollbar-width:thin;
27
+ touch-action:pan-x;
28
+ }
29
+ .ruler{
30
+ position:relative;
31
+ height:190px;
32
+ width:3600px;
33
+ min-width:3600px;
34
+ }
35
+ .axis{
36
+ position:absolute;left:0;right:0;top:86px;height:4px;
37
+ background:var(--paint-6);
38
+ }
39
+ .hour{
40
+ position:absolute;top:55px;width:150px;height:105px;
41
+ transform:translateX(-75px);
42
+ border:0;background:var(--paint-7);color:var(--paint-3);padding:0;
43
+ font:inherit;
44
+ }
45
+ .tick{
46
+ position:absolute;left:50%;top:28px;width:2px;height:36px;
47
+ background:var(--paint-8)
48
+ }
49
+ .label{
50
+ position:absolute;top:72px;left:50%;transform:translateX(-50%);
51
+ white-space:nowrap;font-size:16px;
52
+ }
53
+ .day-boundary{
54
+ position:absolute;top:26px;height:118px;width:2px;
55
+ background:var(--paint-9)
56
+ }
57
+ .day-label{
58
+ position:absolute;top:14px;transform:translateX(-50%);
59
+ font-size:12px;opacity:.75;white-space:nowrap
60
+ }
61
+ .hour:focus-visible{outline:3px solid var(--paint-3);outline-offset:-5px;border-radius:10px}
62
+ .hint{text-align:center;opacity:.8;margin-top:15px;font-size:.9rem}
63
+ .timeline-navigation{text-align:center;margin:10px 0}.timeline-navigation button{font:inherit;color:inherit;background:var(--cell-surface);border:1px solid var(--cell-border);border-radius:8px;padding:8px}
64
+
65
+ /* ===== مدير النوافذ المتكيفة ===== */
66
+ #adaptivePopupActions button{
67
+ min-height:44px;margin:4px;border-radius:10px;border:1px solid var(--paint-11);
68
+ background:var(--paint-12);color:var(--paint-3);padding:8px 12px;font:inherit
69
+ }
70
+ #adaptivePopup[hidden]{display:none!important}
71
+ #adaptivePopup{
72
+ position:fixed;inset:0;z-index:3000;background:var(--paint-13);
73
+ display:flex;align-items:center;justify-content:center;padding:14px
74
+ }
75
+ #adaptivePopupCard{
76
+ width:min(92vw,620px);
77
+ /* الحجم يتبع المحتوى حتى هذا الحد فقط */
78
+ max-height:88vh;max-height:88dvh;overflow:hidden;display:flex;flex-direction:column;min-height:0;
79
+ background:var(--paint-14);color:var(--paint-3);border-radius:20px;padding:18px;
80
+ border:1px solid var(--paint-4);box-shadow:0 16px 55px var(--paint-15)
81
+ }
82
+ #adaptivePopupCard[data-size="short"]{width:min(90vw,440px)}
83
+ #adaptivePopupCard[data-size="medium"]{width:min(92vw,560px)}
84
+ #adaptivePopupCard[data-size="long"]{width:min(94vw,680px)}
85
+ .adaptive-head{flex-shrink:0;display:flex;align-items:flex-start;justify-content:space-between;gap:12px}
86
+ .adaptive-head h2{margin:3px 0 0;font-size:1.45rem}
87
+ .adaptive-kind{font-size:.82rem;opacity:.75}
88
+ #adaptivePopupClose{min-width:48px;min-height:48px;font-size:23px}
89
+ #adaptivePopupMedia:empty{display:none}
90
+ #adaptivePopupMedia{margin:5px 0;text-align:center;font-size:28px;flex-shrink:0}
91
+ #adaptivePopupBody{
92
+ white-space:pre-wrap;line-height:1.8;font-size:17px;margin:10px 0;overflow-y:auto;min-height:0;overscroll-behavior:contain;
93
+ overflow-wrap:anywhere
94
+ }
95
+ #adaptivePopupActions{flex-shrink:0;display:flex;flex-wrap:wrap;gap:6px}
96
+
97
+
98
+ body{padding-bottom:190px}
99
+ .ruler{height:240px;min-width:0}
100
+ .hour{top:26px;height:190px;transform:none;border-inline-end:1px solid var(--cell-border);border-radius:14px;background:var(--cell-surface)}
101
+ .tick{top:70px}.label{top:122px;font-size:21px}.axis{top:115px}
102
+ .day-boundary,.day-label{display:none}
103
+ #loadStatus{text-align:center;overflow-wrap:anywhere}
104
+ #adaptivePopupBody button{display:block;width:100%;text-align:start;margin:8px 0;padding:12px;min-height:48px;border-radius:10px;border:1px solid var(--cell-border);background:var(--cell-surface);color:inherit;font:inherit;cursor:pointer}
105
+ #adaptivePopupBody p{margin:8px 0}
106
+ #adaptivePopupBody a{color:inherit;display:block;margin:12px 0;overflow-wrap:anywhere}
107
+ #adaptivePopupClose{position:sticky;top:0;flex-shrink:0}
108
+ #adaptivePopupMedia img{max-width:100%;max-height:220px;border-radius:12px}
109
+
110
+ .solar-point{position:absolute;top:0;width:3px;height:100%;z-index:1;pointer-events:none}
111
+ .time-track{position:absolute;left:2px;right:2px;height:17px;pointer-events:none}.time-band{position:absolute;top:0;height:100%;pointer-events:none;border-radius:4px}.state-swatch{display:inline-block;width:22px;height:14px;margin-inline-end:8px;border-radius:3px}.untimed-markers{text-align:center;margin:10px 0}.untimed-markers button{font:inherit;padding:10px;border:1px solid var(--cell-border);border-radius:10px;background:var(--cell-surface);color:inherit;cursor:pointer}.timeline-legend{display:flex;flex-wrap:wrap;justify-content:center;gap:10px;margin:12px 0;font-size:14px}
112
+ .hour-icons{position:absolute;top:6px;left:4px;right:4px;display:flex;flex-wrap:wrap;justify-content:center;gap:3px;font-size:16px;pointer-events:none}.prayer-entry{direction:rtl;white-space:nowrap}
113
+ .hour .tick{top:102px;height:16px}.hour .label{top:132px}
114
+ .now-marker{position:absolute;top:0;z-index:2;transform:translateX(-50%);font-size:13px;padding:4px;border-radius:7px;white-space:nowrap;pointer-events:none}
115
+ .now-marker::after{content:"";position:absolute;top:100%;left:50%;height:172px;border-left:2px solid currentColor}
116
+ #solarStatus{white-space:pre-line;text-align:center;line-height:1.7;font-size:14px}
117
+ #locationButton{display:block;margin:12px auto;padding:10px 18px;border:1px solid var(--cell-border);border-radius:10px;color:inherit;background:var(--cell-surface);font:inherit}
118
+ #adaptivePopupBody label{display:block;margin:12px 0}
119
+ #adaptivePopupBody label[hidden]{display:none!important}
120
+ #adaptivePopupKind[hidden],#adaptivePopupMedia[hidden]{display:none!important}
121
+ #adhkarButton{display:block;margin:10px auto;padding:12px;font:inherit;background:var(--cell-surface);color:inherit;border:1px solid var(--cell-border);border-radius:10px}
122
+ #nowMarker{pointer-events:none}
123
+ #adaptivePopupBody input,#adaptivePopupBody select{display:block;width:100%;padding:10px;font:inherit;margin:8px 0}
124
+ #adaptivePopupClose{border-radius:10px;border:1px solid var(--cell-border);color:inherit;background:var(--paint-12)}
125
+ #reminderButton,#reminderNotice{display:block;margin:10px auto;padding:12px;font:inherit;background:var(--cell-surface);color:inherit;border:1px solid var(--cell-border);border-radius:10px}#reminderNotice[hidden]{display:none}.personal-hour{position:absolute;bottom:8px;left:0;right:0;text-align:center;font-size:22px}#adaptivePopupBody textarea{display:block;width:100%;min-height:110px;font:inherit;padding:10px}#adaptivePopupActions{max-height:30vh;overflow:auto}
126
+ #adaptivePopupBody input[type="checkbox"]{width:auto;display:inline-block;margin-inline-start:12px}
127
+
128
+ .ruler{height:390px}.hour{top:95px;height:270px;transform:none}.axis{top:197px}
129
+ .hour-hit{position:absolute;inset:0;width:100%;border:0;background:transparent;color:inherit;font:inherit;cursor:pointer}
130
+ .hour .tick{left:0}.hour .label{left:8px;transform:none;pointer-events:none}
131
+ .time-track{left:0;right:0;z-index:2}.time-band,.solar-point{pointer-events:auto;cursor:pointer;border:0;padding:0;min-width:3px}
132
+ .hour-icons{z-index:3;pointer-events:none}.prayer-entry{pointer-events:auto;font:inherit;color:inherit;background:var(--cell-surface);border:1px solid var(--cell-border);border-radius:6px;padding:4px;cursor:pointer}
133
+ .boundary-entry{position:absolute;left:3px;right:3px;min-height:36px;max-height:38px;overflow-y:auto;z-index:3;font:inherit;font-size:12px;color:inherit;background:var(--cell-surface);border:1px solid var(--cell-border);border-radius:5px;padding:4px;cursor:pointer;direction:rtl}
134
+ .event-marker,.personal-hour{position:absolute;left:3px;right:3px;height:64px;overflow-y:auto;z-index:3;font:inherit;font-size:14px;color:inherit;background:var(--cell-surface);border:1px solid var(--cell-border);border-radius:7px;padding:6px;cursor:pointer;direction:rtl}
135
+ .reminder-axis{position:absolute;left:0;right:0;border-top:1px solid var(--cell-border);pointer-events:none}
136
+ .now-marker{top:0;padding:0;transform:none;background:transparent!important;z-index:4}
137
+ .now-marker::after{top:76px;left:0;height:300px;border-left-width:2px}
138
+ .now-marker-label{position:absolute;top:0;left:0;width:270px;max-width:calc(100vw - 32px);white-space:pre-line;text-align:center;line-height:1.65;background:var(--cell-surface);border:1px solid currentColor;border-radius:8px;padding:4px;direction:rtl}
139
+ .personal-hour{pointer-events:auto;z-index:3;border:1px solid var(--cell-border);border-radius:5px;color:inherit;background:var(--cell-surface);font:inherit;cursor:pointer}
140
+ #reminderNotice{position:fixed;top:8px;left:12px;right:12px;z-index:2900;max-height:24vh;overflow:auto;margin:0;border:2px solid currentColor}#reminderDialogNotice{flex-shrink:0;padding:12px;font:inherit;background:var(--cell-surface);color:inherit;border:2px solid currentColor;max-height:20vh;overflow:auto}#reminderDialogNotice[hidden]{display:none}
141
+ /* Simulation stays in document flow, below dialogs and reminder notices. */
142
+ #celestialSection{margin:16px 0 24px}
143
+ #celestialSection h2{font-size:1.1rem;margin:0 0 10px}
144
+ #celestialControls{display:flex;flex-wrap:wrap;gap:8px;margin:10px 0}
145
+ #celestialControls button{font:inherit;min-height:44px;padding:8px 12px;border:1px solid var(--cell-border,#888);border-radius:8px;background:var(--cell-surface,#fff);color:inherit;cursor:pointer}
146
+ #celestialControls button:disabled{opacity:.6;cursor:default}
147
+ #celestialScene{position:relative;height:240px;overflow:hidden;isolation:isolate;direction:ltr}
148
+ #celestialScene[hidden]{display:none}
149
+ .celestial-body{position:absolute;border-radius:50%;pointer-events:none;opacity:0}
150
+ #celestialSun{background:#ffd45c;box-shadow:0 0 20px #ffd45c99}
151
+ #celestialGround{position:absolute;inset:82% 0 0;background:#477552;border-top:1px solid #ffffff80;pointer-events:none}
152
+ #celestialStick{position:absolute;left:50%;bottom:18%;height:38px;width:5px;background:#eee;z-index:1}
153
+ #celestialShadow{position:absolute;left:50%;bottom:18%;height:5px;background:#111;transform-origin:left center}
154
+ #celestialQibla{position:absolute;left:50%;top:50%;width:36px;height:40px;background:#191919;border:1px solid #eee;border-radius:3px;pointer-events:none}
155
+ #celestialQibla::before{content:"";position:absolute;left:0;right:0;top:8px;height:6px;background:#d7b56d}
156
+ #celestialInfo,#celestialLocation,#celestialStatus{margin:8px 0;line-height:1.7;overflow-wrap:anywhere;white-space:pre-line;font-size:14px}
157
+ #celestialStatus:empty{display:none}
158
+ [hidden]#celestialSettingsPanel{display:none!important}
159
+ .qibla-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}
160
+ #celestialSettingsPanel{white-space:normal}
161
+ #celestialSettingsButton,#celestialBootRetry{font:inherit;min-height:44px;padding:8px 12px;border:1px solid var(--cell-border,#888);border-radius:8px;background:var(--cell-surface,#fff);color:inherit;cursor:pointer}
162
+ #celestialBootStatus{white-space:pre-line;overflow-wrap:anywhere}
163
  </style>
164
  </head>
165
  <body>
166
+ <main>
167
+ <h1 data-ui="title"></h1>
168
+ <section id="celestialSection">
169
+ <div id="celestialScene" hidden role="img">
170
+ <div id="celestialSun" class="celestial-body"></div>
171
+ <div id="celestialMoon" class="celestial-body"></div>
172
+ <div id="celestialGround"></div><div id="celestialShadow"></div>
173
+ <div id="celestialStick"></div><div id="celestialQibla" hidden></div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
174
  </div>
175
+ <button id="celestialSettingsButton" type="button" disabled hidden></button>
176
+ <p id="celestialBootStatus" role="status" hidden></p>
177
+ <button id="celestialBootRetry" type="button" hidden></button>
178
+ </section>
179
+ <div class="timeline-navigation"><span id="timelineDates"></span> <button id="timelineToday" type="button" data-ui="timelineToday" hidden></button></div>
180
+ <div id="shell" class="ruler-shell" data-aria="ruler">
181
+ <div id="ruler" class="ruler"></div>
182
  </div>
183
+ <p class="hint" data-ui="hint"></p><button id="locationButton" type="button"></button><p id="solarStatus" aria-live="off"></p><p id="loadStatus" role="status"></p>
184
+ <button id="adhkarButton" type="button"></button>
185
+ <button id="reminderButton" type="button"></button><button id="reminderNotice" type="button" hidden aria-live="assertive" aria-atomic="true"></button>
186
+ </main>
187
+
188
+
189
+ <div id="celestialSettingsStorage" hidden>
190
+ <div id="celestialSettingsPanel" hidden>
191
+ <p id="celestialHelp" data-qibla-text="help"></p>
192
+ <p id="celestialLocation"></p>
193
+ <div id="celestialControls">
194
+ <button id="celestialLocate" type="button" disabled data-qibla-text="locate"></button>
195
+ <button id="celestialCompass" type="button" disabled data-qibla-text="start"></button>
196
+ <button id="celestialRetry" type="button" hidden data-qibla-text="retry"></button>
 
 
 
197
  </div>
198
+ <label><input id="qiblaScreenReader" type="checkbox"><span data-qibla-text="screen_reader"></span></label>
199
+ <label><input id="qiblaVibration" type="checkbox"><span data-qibla-text="vibration"></span></label>
200
+ <p id="celestialStatus" aria-live="off"></p>
201
+ <p id="qiblaDirection" aria-live="off"></p>
202
+ <p id="qiblaCapability"></p>
203
+ <p id="qiblaSaveStatus"></p>
204
+ <p id="celestialInfo" aria-live="off"></p>
205
+ </div>
206
  </div>
207
+ <div id="qiblaAnnouncementOutside" class="qibla-sr-only" role="status" aria-live="polite" aria-atomic="true"></div>
208
+
209
+ <div id="adaptivePopup" hidden role="dialog" aria-modal="true" aria-labelledby="adaptivePopupTitle">
210
+ <article id="adaptivePopupCard">
211
+ <header class="adaptive-head">
212
+ <div>
213
+ <div id="adaptivePopupKind" class="adaptive-kind"></div>
214
+ <h2 id="adaptivePopupTitle"></h2>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
215
  </div>
216
+ <button id="adaptivePopupClose" type="button" data-aria="close" data-ui="closeSymbol"></button>
217
+ </header>
218
+ <div id="adaptivePopupMedia"></div>
219
+ <div id="adaptivePopupBody"></div>
220
+ <div id="adaptivePopupActions"></div>
221
+ <div id="qiblaAnnouncementInside" class="qibla-sr-only" role="status" aria-live="polite" aria-atomic="true"></div>
222
+ </article>
223
  </div>
224
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
225
 
 
 
 
 
 
 
 
 
226
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
227
 
228
+ <script src="astronomy.browser.min.js?v=20260926-ui2"></script>
229
+ <script src="solar_engine.js?v=20260926-ui2"></script>
230
+ <script src="personal_reminder_engine.js?v=20260929-complete1"></script>
231
+ <script src="hijri_calendar_engine.js?v=20260926-worship1"></script>
232
+ <script src="worship_event_engine.js?v=20260926-worship1"></script>
233
+ <script src="quran_khatma.js?v=20261001-nav2"></script>
234
+ <script src="quran_audio.js?v=20261001-nav2"></script>
235
+ <script src="quran_reader.js?v=20261001-nav2"></script>
236
+ <script src="registry_app.js?v=20260929-complete1"></script>
237
  <script>
238
+ (function () {
239
+ 'use strict';
240
+ const $ = id => document.getElementById(id);
241
+ const scene = $('celestialScene'), status = $('celestialStatus');
242
+ const locate = $('celestialLocate'), compass = $('celestialCompass');
243
+ const panel = $('celestialSettingsPanel');
244
+ let controller, registry, config, ui, timer, sensorTimer, loading = false;
245
+ let listening = false, requesting = false, generation = 0, locating = false;
246
+ let confirmedLocation = false, pendingLocation, options;
247
+ let aligned = false, stableSince = null, candidate = '', candidateSince = 0;
248
+ let lastGuidance = '', lastAnnouncement = -Infinity, lastHeadingAt = 0;
249
+
250
+ function validLocation(value) {
251
+ return value && Number.isFinite(value.lat) && Number.isFinite(value.lng)
252
+ && Math.abs(value.lat) <= 90 && Math.abs(value.lng) <= 180;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
253
  }
254
+ function saved(key) {
255
+ try { return JSON.parse(localStorage.getItem(key)); } catch (_) { return null; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
256
  }
257
+ function initialLocation() {
258
+ const app = typeof RegistryApp !== 'undefined' ? RegistryApp.solarState().observer : null;
259
+ for (const value of [pendingLocation, saved('muslim_clock_location'), app, window.MUSLIM_CLOCK_LOCATION,
260
+ window.ISLAMIC_CLOCK_LOCATION, saved('clock_observer_v1')]) {
261
+ if (validLocation(value)) return { value, confirmed: true, label: ui?.location_saved || '' };
 
262
  }
263
+ return { value: { lat: 31.9539, lng: 35.9106 }, confirmed: false, label: ui?.location_default || '' };
264
  }
265
+ function announce(message) {
266
+ if (!options?.screen_reader || !message || document.hidden) return;
267
+ const inside = !$('adaptivePopup').hidden;
268
+ const live = $(inside ? 'qiblaAnnouncementInside' : 'qiblaAnnouncementOutside');
269
+ $(inside ? 'qiblaAnnouncementOutside' : 'qiblaAnnouncementInside').textContent = '';
270
+ if (live.textContent !== message) live.textContent = message;
271
  }
272
+ function message(text, speak = true) {
273
+ status.textContent = text || '';
274
+ if (speak) announce(text);
 
 
 
 
 
 
 
275
  }
276
+ function resetGuidance() {
277
+ aligned = false; stableSince = null; candidate = ''; candidateSince = 0;
278
+ lastGuidance = ''; lastAnnouncement = -Infinity;
279
+ $('qiblaDirection').textContent = '';
280
+ $('qiblaAnnouncementInside').textContent = '';
281
+ $('qiblaAnnouncementOutside').textContent = '';
 
 
 
 
 
 
 
 
 
 
 
282
  }
283
+ function clearHeading() {
284
+ if (!controller) return;
285
+ controller.setHeading(null);
286
+ const state = controller.lastState;
287
+ if (state) {
288
+ state.headingDeg = 0; state.compassEnabled = false;
289
+ state.qibla = window.CelestialSimulation.QiblaEngine.calculate(controller.location, null);
290
+ controller.renderer.render(state);
 
 
 
 
 
 
 
 
 
 
291
  }
292
+ $('celestialQibla').hidden = true;
 
 
293
  }
294
+ function stop(text, speak = true) {
295
+ generation++; requesting = false; listening = false;
296
+ clearTimeout(sensorTimer);
297
+ window.removeEventListener('deviceorientationabsolute', orientation, true);
298
+ window.removeEventListener('deviceorientation', orientation, true);
299
+ resetGuidance(); clearHeading();
300
+ compass.disabled = !controller;
301
+ compass.textContent = ui?.start || '';
302
+ compass.setAttribute('aria-pressed', 'false');
303
+ if (text) message(text, speak);
304
  }
305
+ function update() {
306
+ if (!controller) return;
307
+ try { controller.update(); }
308
+ catch (error) {
309
+ stop(ui?.updating_error);
310
+ $('celestialRetry').hidden = false;
311
+ console.error('Celestial update:', error);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
312
  }
313
  }
314
+ function showLocation(location, label) {
315
+ $('celestialLocation').textContent = `${label} · ${location.lat.toFixed(4)}, ${location.lng.toFixed(4)}`;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
316
  }
317
+ function applyLocation(location, label) {
318
+ if (!controller || !validLocation(location)) return;
319
+ const changed = !confirmedLocation || location.lat !== controller.location.lat || location.lng !== controller.location.lng;
320
+ controller.setLocation(location); confirmedLocation = true;
321
+ if (changed) { resetGuidance(); clearHeading(); }
322
+ showLocation(location, label);
323
+ update();
324
  }
325
+ function configureUI() {
326
+ $('celestialSettingsButton').textContent = ui.settings;
327
+ $('celestialBootRetry').textContent = ui.retry;
328
+ document.querySelectorAll('[data-qibla-text]').forEach(el => {
329
+ el.textContent = ui[el.dataset.qiblaText] || '';
330
+ });
331
+ scene.setAttribute('aria-label', ui.scene);
332
+ const stored = saved(config.storage_key);
333
+ options = {
334
+ screen_reader: typeof stored?.screen_reader === 'boolean' ? stored.screen_reader : config.defaults.screen_reader,
335
+ vibration: typeof stored?.vibration === 'boolean' ? stored.vibration : config.defaults.vibration
336
+ };
337
+ $('qiblaScreenReader').checked = options.screen_reader;
338
+ $('qiblaVibration').checked = options.vibration;
339
+ $('qiblaCapability').textContent = typeof navigator.vibrate === 'function' ? '' : ui.vibration_unavailable;
340
  }
341
+ async function boot() {
342
+ if (loading) return;
343
+ loading = true; $('celestialRetry').hidden = true; $('celestialBootRetry').hidden = true;
344
+ $('celestialBootStatus').hidden = true;
345
+ if (controller) stop();
346
+ clearInterval(timer);
347
+ try {
348
+ const response = await fetch('./sky_simulation_registry.json', { cache: 'no-store' });
349
+ if (!response.ok) throw new Error('sky_simulation_registry.json: ' + response.status);
350
+ registry = await response.json();
351
+ if (!registry.sky_color_model?.gradient_stops?.length || !registry.objects?.kaaba) throw new Error('Invalid celestial registry');
352
+ config = registry.qibla_accessibility; ui = config?.ui;
353
+ const qiblaReady = Boolean(ui?.settings && config?.defaults && config?.alignment);
354
+ if (qiblaReady) configureUI();
355
+ message(ui?.loading, false);
356
+ if (!window.CelestialSimulation) {
357
+ await import('./celestial_simulation.js?v=20260928-qibla1');
358
+ }
359
+ const api = window.CelestialSimulation;
360
+ if (!api) throw new Error('Missing CelestialSimulation');
361
+ const renderer = new api.SkySimulationRenderer({
362
+ container: scene, sun: $('celestialSun'), moon: $('celestialMoon'),
363
+ shadow: $('celestialShadow'), info: $('celestialInfo'), kaaba: $('celestialQibla')
364
+ }, registry);
365
+ const render = renderer.render.bind(renderer);
366
+ renderer.render = state => {
367
+ render(state);
368
+ for (const id of ['celestialSun', 'celestialMoon']) {
369
+ const element = $(id), inset = parseFloat(element.style.width) / 2 + 2;
370
+ element.style.left = `clamp(${inset}px, ${element.style.left}, calc(100% - ${inset}px))`;
371
+ }
372
+ $('celestialQibla').hidden = !confirmedLocation || !state.compassEnabled;
373
+ // No changing spoken description or tooltip on the main visual scene.
374
+ for (const id of ['celestialQibla', 'celestialShadow', 'celestialSun', 'celestialMoon']) $(id).removeAttribute('title');
375
+ };
376
+ const selected = initialLocation();
377
+ confirmedLocation = selected.confirmed;
378
+ controller = new api.CelestialSimulationController({ location: selected.value, registry, renderer });
379
+ window.MuslimClockCelestialController = controller;
380
+ showLocation(selected.value, selected.label);
381
+ scene.hidden = false;
382
+ update(); timer = setInterval(update, 60000);
383
+ locate.disabled = !qiblaReady; compass.disabled = !qiblaReady;
384
+ $('celestialSettingsButton').disabled = !qiblaReady;
385
+ $('celestialSettingsButton').hidden = !qiblaReady;
386
+ message(confirmedLocation ? ui?.location_ready : ui?.location_required, false);
387
+ if (!qiblaReady) {
388
+ $('celestialBootStatus').textContent = ui?.settings_missing || 'sky_simulation_registry.json: qibla_accessibility';
389
+ $('celestialBootStatus').hidden = false;
390
+ }
391
+ } catch (error) {
392
+ message(ui?.load_error || 'sky_simulation_registry.json / celestial_simulation.js', false);
393
+ // If a retry fails, keep a working location control only when a controller exists.
394
+ locate.disabled = !controller; compass.disabled = !controller;
395
+ $('celestialRetry').hidden = false;
396
+ $('celestialBootStatus').textContent = ui?.load_error || 'sky_simulation_registry.json / celestial_simulation.js';
397
+ $('celestialBootStatus').hidden = false;
398
+ $('celestialBootRetry').textContent = ui?.retry || '↻';
399
+ $('celestialBootRetry').hidden = false;
400
+ console.error('Celestial boot:', error);
401
+ } finally { loading = false; }
402
  }
403
+ function storeOptions() {
404
+ if (!options) return;
405
+ options.screen_reader = $('qiblaScreenReader').checked;
406
+ options.vibration = $('qiblaVibration').checked;
407
+ $('qiblaAnnouncementInside').textContent = '';
408
+ $('qiblaAnnouncementOutside').textContent = '';
409
+ try {
410
+ localStorage.setItem(config.storage_key, JSON.stringify(options));
411
+ $('qiblaSaveStatus').textContent = '';
412
+ } catch (_) { $('qiblaSaveStatus').textContent = ui.save_failed; }
 
 
 
 
413
  }
414
+ function guidance(qibla, now) {
415
+ if (!confirmedLocation || !Number.isFinite(qibla.deltaDeg)) return;
416
+ const distance = Math.abs(qibla.deltaDeg), policy = config.alignment;
417
+ if (aligned) {
418
+ if (distance <= policy.exit_threshold_deg) return;
419
+ aligned = false; stableSince = null; candidate = ''; lastGuidance = '';
420
+ $('qiblaDirection').textContent = '';
421
+ $('qiblaAnnouncementInside').textContent = '';
422
+ $('qiblaAnnouncementOutside').textContent = '';
423
+ }
424
+ if (distance <= registry.objects.kaaba.alignment_threshold_deg) {
425
+ candidate = '';
426
+ if (stableSince === null) stableSince = now;
427
+ if (now - stableSince < policy.stable_ms) return;
428
+ aligned = true; lastGuidance = 'aligned'; lastAnnouncement = now;
429
+ $('qiblaDirection').textContent = ui.aligned;
430
+ announce(ui.aligned);
431
+ if (options.vibration && typeof navigator.vibrate === 'function') {
432
+ try {
433
+ if (!navigator.vibrate(config.vibration_pattern_ms)) $('qiblaCapability').textContent = ui.vibration_unavailable;
434
+ } catch (_) { $('qiblaCapability').textContent = ui.vibration_unavailable; }
435
+ }
436
+ return;
437
+ }
438
+ stableSince = null;
439
+ const key = (qibla.deltaDeg > 0 ? 'right' : 'left')
440
+ + (distance > registry.objects.kaaba.near_alignment_threshold_deg ? '_far' : '');
441
+ if (candidate !== key) { candidate = key; candidateSince = now; }
442
+ if (now - candidateSince < policy.direction_stable_ms || key === lastGuidance
443
+ || now - lastAnnouncement < policy.direction_interval_ms) return;
444
+ lastGuidance = key; lastAnnouncement = now;
445
+ $('qiblaDirection').textContent = ui[key]; announce(ui[key]);
446
  }
447
+ function orientation(event) {
448
+ if (!listening || document.hidden || !controller) return;
449
+ // Relative alpha alone cannot establish north. Never use it for alignment.
450
+ const hasWebkitHeading = Number.isFinite(event.webkitCompassHeading);
451
+ if (hasWebkitHeading && (event.webkitCompassHeading < 0 || event.webkitCompassHeading >= 360)) return;
452
+ const heading = hasWebkitHeading ? event.webkitCompassHeading
453
+ : event.absolute === true && Number.isFinite(event.alpha) && event.alpha >= 0 && event.alpha <= 360 ? 360 - event.alpha : null;
454
+ if (!Number.isFinite(heading)) return;
455
+ const policy = config.alignment;
456
+ const inaccurate = Number.isFinite(event.webkitCompassAccuracy)
457
+ && (event.webkitCompassAccuracy < 0 || event.webkitCompassAccuracy > policy.max_reported_accuracy_deg);
458
+ const tilted = (Number.isFinite(event.beta) && Math.abs(event.beta) > policy.max_tilt_deg)
459
+ || (Number.isFinite(event.gamma) && Math.abs(event.gamma) > policy.max_tilt_deg);
460
+ if (inaccurate || tilted) {
461
+ const warning = inaccurate ? ui.calibrate : ui.flat_phone;
462
+ if (status.textContent !== warning) { resetGuidance(); clearHeading(); message(warning); }
463
+ return;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
464
  }
465
+ const now = performance.now();
466
+ // Some phones send both event types for the same sample.
467
+ if (lastHeadingAt && now - lastHeadingAt < 40) return;
468
+ lastHeadingAt = now;
469
+ clearTimeout(sensorTimer);
470
+ sensorTimer = setTimeout(() => stop(ui.sensor_timeout), policy.sensor_timeout_ms);
471
+ message('', false);
472
+ const angle = Number.isFinite(window.screen?.orientation?.angle) ? window.screen.orientation.angle
473
+ : Number.isFinite(window.orientation) ? window.orientation : 0;
474
+ controller.setHeading(heading + angle);
475
+ const state = controller.lastState;
476
+ if (!state) return;
477
+ state.headingDeg = controller.headingDeg; state.compassEnabled = true;
478
+ state.qibla = window.CelestialSimulation.QiblaEngine.calculate(controller.location,
479
+ controller.headingDeg, { alignmentThresholdDeg: registry.objects.kaaba.alignment_threshold_deg });
480
+ controller.renderer.render(state);
481
+ guidance(state.qibla, now);
482
  }
483
+ async function startCompass() {
484
+ if (!ui || !controller) return;
485
+ if (listening || requesting) { stop(ui.stopped); return; }
486
+ if (!confirmedLocation) { message(ui.location_required); return; }
487
+ if (!window.isSecureContext || typeof DeviceOrientationEvent === 'undefined') {
488
+ message(ui.sensor_unavailable); return;
489
+ }
490
+ const token = ++generation;
491
+ requesting = true; compass.disabled = true;
492
+ try {
493
+ // Called directly from the user's click, before any geolocation await.
494
+ if (typeof DeviceOrientationEvent.requestPermission === 'function'
495
+ && await DeviceOrientationEvent.requestPermission(true) !== 'granted') throw new Error('permission');
496
+ if (token !== generation || document.hidden) return;
497
+ resetGuidance(); lastHeadingAt = 0;
498
+ window.addEventListener('deviceorientationabsolute', orientation, true);
499
+ window.addEventListener('deviceorientation', orientation, true);
500
+ listening = true; compass.textContent = ui.stop; compass.setAttribute('aria-pressed', 'true');
501
+ message(ui.waiting);
502
+ sensorTimer = setTimeout(() => stop(ui.sensor_timeout), config.alignment.sensor_timeout_ms);
503
+ } catch (_) { message(ui.permission_denied); }
504
+ finally {
505
+ if (token === generation) { requesting = false; compass.disabled = false; }
506
+ }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
507
  }
508
+ locate.addEventListener('click', () => {
509
+ if (!controller || !ui || locating) return;
510
+ if (!navigator.geolocation) { message(ui.location_unavailable); return; }
511
+ stop(); locating = true; locate.disabled = true; message(ui.locating);
512
+ navigator.geolocation.getCurrentPosition(position => {
513
+ locating = false; locate.disabled = false;
514
+ const location = { lat: position.coords.latitude, lng: position.coords.longitude };
515
+ if (!validLocation(location)) { message(ui.location_failed); return; }
516
+ pendingLocation = location;
517
+ applyLocation(location, ui.location_device);
518
+ try { localStorage.setItem('muslim_clock_location', JSON.stringify(location)); }
519
+ catch (_) { $('qiblaSaveStatus').textContent = ui.save_failed; }
520
+ message(ui.location_ready);
521
+ }, () => {
522
+ locating = false; locate.disabled = false; message(ui.location_failed);
523
+ }, { enableHighAccuracy: true, timeout: 15000, maximumAge: 60000 });
524
+ });
525
+ compass.addEventListener('click', startCompass);
526
+ $('qiblaScreenReader').addEventListener('change', storeOptions);
527
+ $('qiblaVibration').addEventListener('change', storeOptions);
528
+ $('celestialRetry').addEventListener('click', boot);
529
+ $('celestialBootRetry').addEventListener('click', boot);
530
+ $('celestialSettingsButton').addEventListener('click', async () => {
531
+ await window.registryReady;
532
+ if (typeof RegistryApp !== 'undefined') RegistryApp.qiblaSettings(Date.now());
533
+ });
534
+ window.MuslimClockQibla = Object.freeze({ mountSettings(host) {
535
+ host.appendChild(panel); panel.hidden = false;
536
+ } });
537
+ window.addEventListener('muslim-clock-popup-leave', () => {
538
+ panel.hidden = true; $('celestialSettingsStorage').appendChild(panel);
539
+ $('qiblaAnnouncementInside').textContent = '';
540
+ });
541
+ window.addEventListener('muslim-clock-location-change', event => {
542
+ if (!validLocation(event.detail)) return;
543
+ // This event is sent only when the user selects a location in prayer settings.
544
+ const changed = !pendingLocation || event.detail.lat !== pendingLocation.lat || event.detail.lng !== pendingLocation.lng;
545
+ if (changed) {
546
+ pendingLocation = { lat: event.detail.lat, lng: event.detail.lng };
547
+ try { localStorage.setItem('muslim_clock_location', JSON.stringify(pendingLocation)); } catch (_) {}
548
+ if (controller) applyLocation(pendingLocation, ui?.location_app || '');
549
+ }
550
+ });
551
+ document.addEventListener('visibilitychange', () => {
552
+ if (document.hidden) stop(ui?.stopped, false);
553
+ else update();
554
+ });
555
+ window.addEventListener('pagehide', () => stop(null, false));
556
+ boot();
557
+ })();
558
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
559
  </script>
560
  </body>
561
  </html>