Spaces:
Running
Running
Upload index.html
Browse files- index.html +517 -947
index.html
CHANGED
|
@@ -1,991 +1,561 @@
|
|
| 1 |
-
<!
|
| 2 |
<html lang="ar" dir="rtl">
|
| 3 |
<head>
|
| 4 |
-
<meta charset="
|
| 5 |
-
<meta name="viewport" content="width=device-width,
|
| 6 |
-
<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 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
}
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
}
|
| 36 |
-
.
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
}
|
| 42 |
-
.
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
.
|
| 47 |
-
position:
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
.
|
| 57 |
-
position:
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
.
|
| 69 |
-
.
|
| 70 |
-
.
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
}
|
| 90 |
-
|
| 91 |
-
|
| 92 |
-
.
|
| 93 |
-
.
|
| 94 |
-
.
|
| 95 |
-
|
| 96 |
-
|
| 97 |
-
}
|
| 98 |
-
|
| 99 |
-
.
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
.
|
| 107 |
-
.
|
| 108 |
-
.
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
| 116 |
-
|
| 117 |
-
.
|
| 118 |
-
.
|
| 119 |
-
.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 120 |
</style>
|
| 121 |
</head>
|
| 122 |
<body>
|
| 123 |
-
|
| 124 |
-
<
|
| 125 |
-
|
| 126 |
-
|
| 127 |
-
<
|
| 128 |
-
<div id="
|
| 129 |
-
<div
|
| 130 |
-
|
| 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
|
| 152 |
-
<
|
| 153 |
-
|
| 154 |
-
|
| 155 |
-
|
| 156 |
-
<
|
| 157 |
-
<
|
| 158 |
-
<
|
| 159 |
-
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
|
| 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="
|
| 171 |
-
<
|
| 172 |
-
<
|
| 173 |
-
|
| 174 |
-
|
| 175 |
-
|
| 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
|
| 195 |
-
|
| 196 |
-
</div>
|
| 197 |
-
|
| 198 |
-
<
|
| 199 |
-
|
|
|
|
| 200 |
</div>
|
| 201 |
|
| 202 |
-
<!-- نافذة الإذاعات الإسلامية المستقلة -->
|
| 203 |
-
<div id="islamicRadioModal" class="modal-overlay">
|
| 204 |
-
<div class="modal-content">
|
| 205 |
-
<button class="close-btn" onclick="closeModal('islamicRadioModal')">×</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')">×</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')">×</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 |
-
|
| 251 |
-
|
| 252 |
-
const
|
| 253 |
-
|
| 254 |
-
|
| 255 |
-
|
| 256 |
-
|
| 257 |
-
|
| 258 |
-
|
| 259 |
-
|
| 260 |
-
|
| 261 |
-
|
| 262 |
-
|
| 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 |
-
|
| 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 |
-
|
| 550 |
-
|
| 551 |
-
|
| 552 |
-
|
| 553 |
-
queue.push({key:'friday_dua_hour', msg:"🤲 أنت الآن في ساعة الإجابة المرجوّة يوم الجمعة، أكثر من الدعاء.", repeatHours:1});
|
| 554 |
}
|
|
|
|
| 555 |
}
|
| 556 |
-
|
| 557 |
-
|
| 558 |
-
|
| 559 |
-
|
|
|
|
|
|
|
| 560 |
}
|
| 561 |
-
|
| 562 |
-
|
| 563 |
-
|
| 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 |
-
|
| 574 |
-
|
| 575 |
-
|
| 576 |
-
|
| 577 |
-
|
| 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 |
-
|
| 591 |
-
|
| 592 |
-
|
| 593 |
-
|
| 594 |
-
|
| 595 |
-
|
| 596 |
-
|
| 597 |
-
|
| 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 |
-
|
| 610 |
-
bar.classList.remove('show');
|
| 611 |
-
activeNoticeKey = null;
|
| 612 |
}
|
| 613 |
-
|
| 614 |
-
|
| 615 |
-
|
| 616 |
-
|
| 617 |
-
|
| 618 |
-
|
| 619 |
-
|
| 620 |
-
|
| 621 |
-
|
| 622 |
-
|
| 623 |
}
|
| 624 |
-
|
| 625 |
-
|
| 626 |
-
}
|
| 627 |
-
|
| 628 |
-
|
| 629 |
-
|
| 630 |
-
|
| 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 |
-
|
| 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 |
-
|
| 747 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 748 |
}
|
| 749 |
-
|
| 750 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 751 |
}
|
| 752 |
-
|
| 753 |
-
|
| 754 |
-
|
| 755 |
-
|
| 756 |
-
|
| 757 |
-
|
| 758 |
-
|
| 759 |
-
|
| 760 |
-
|
| 761 |
-
|
| 762 |
-
|
| 763 |
-
|
| 764 |
-
|
| 765 |
-
|
| 766 |
-
|
| 767 |
-
|
| 768 |
-
|
| 769 |
-
|
| 770 |
-
|
| 771 |
-
|
| 772 |
-
|
| 773 |
-
|
| 774 |
-
|
| 775 |
-
|
| 776 |
-
|
| 777 |
-
|
| 778 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 779 |
}
|
| 780 |
-
|
| 781 |
-
|
| 782 |
-
|
| 783 |
-
|
| 784 |
-
|
| 785 |
-
|
| 786 |
-
|
| 787 |
-
|
| 788 |
-
|
| 789 |
-
|
| 790 |
-
saveSetting('khatm_progress', progress);
|
| 791 |
-
refreshKhatmUI();
|
| 792 |
-
if (progress.completedJuz.length===30){
|
| 793 |
-
alert('🎉 مبارك الختمة! تقبّل الله منك.');
|
| 794 |
}
|
| 795 |
-
|
| 796 |
-
|
| 797 |
-
|
| 798 |
-
|
| 799 |
-
|
| 800 |
-
|
| 801 |
-
|
| 802 |
-
|
| 803 |
-
|
| 804 |
-
|
| 805 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 806 |
}
|
| 807 |
-
|
| 808 |
-
|
| 809 |
-
/
|
| 810 |
-
|
| 811 |
-
|
| 812 |
-
|
| 813 |
-
|
| 814 |
-
|
| 815 |
-
|
| 816 |
-
|
| 817 |
-
|
| 818 |
-
|
| 819 |
-
|
| 820 |
-
|
| 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 |
-
|
| 851 |
-
|
| 852 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 853 |
}
|
| 854 |
-
|
| 855 |
-
|
| 856 |
-
|
| 857 |
-
|
| 858 |
-
|
| 859 |
-
|
| 860 |
-
|
| 861 |
-
|
| 862 |
-
|
| 863 |
-
|
| 864 |
-
|
| 865 |
-
|
| 866 |
-
|
| 867 |
-
|
| 868 |
-
|
| 869 |
-
|
| 870 |
-
|
| 871 |
-
|
| 872 |
-
|
| 873 |
-
|
| 874 |
-
|
| 875 |
-
|
| 876 |
-
|
| 877 |
-
|
| 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,""")}<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&®.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&®.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>
|