Spaces:
Running
Running
Download index.html from Asem75/Islamic_clock: direct link, hf CLI and curl.
- Browser
- Download file 31.8 kB
-
https://huggingface.co/spaces/Asem75/Islamic_clock/resolve/main/index.html
- Command line
-
hf download hf://spaces/Asem75/Islamic_clock/index.html
-
curl -L -o index.html https://huggingface.co/spaces/Asem75/Islamic_clock/resolve/main/index.html
31.8 kB
| <html lang="ar" dir="rtl"> | |
| <head> | |
| <meta charset="utf-8"> | |
| <meta name="viewport" content="width=device-width,initial-scale=1,viewport-fit=cover"> | |
| <title></title> | |
| <link rel="stylesheet" href="quran_reader.css?v=20261016-radio1"> | |
| <link rel="stylesheet" href="islamic_radio.css?v=20261016-radio1"> | |
| <style> | |
| *{box-sizing:border-box} | |
| html,body{margin:0;min-height:100%;font-family:system-ui,-apple-system,"Noto Sans Arabic",sans-serif} | |
| body{ | |
| background:linear-gradient(180deg,var(--paint-1),var(--paint-2)); | |
| color:var(--paint-3);padding:22px 14px; | |
| } | |
| main{max-width:900px;margin:auto} | |
| h1{text-align:center;font-size:1.5rem;margin:12px 0 28px} | |
| .ruler-shell{ | |
| direction:ltr; | |
| width:100%; | |
| overflow-x:auto; | |
| overflow-y:hidden; | |
| border:1px solid var(--paint-4); | |
| border-radius:18px; | |
| background:var(--paint-5); | |
| -webkit-overflow-scrolling:touch; | |
| scrollbar-width:thin; | |
| touch-action:pan-x; | |
| } | |
| .ruler{ | |
| position:relative; | |
| height:190px; | |
| width:3600px; | |
| min-width:3600px; | |
| } | |
| .axis{ | |
| position:absolute;left:0;right:0;top:86px;height:4px; | |
| background:var(--paint-6); | |
| } | |
| .hour{ | |
| position:absolute;top:55px;width:150px;height:105px; | |
| transform:translateX(-75px); | |
| border:0;background:var(--paint-7);color:var(--paint-3);padding:0; | |
| font:inherit; | |
| } | |
| .tick{ | |
| position:absolute;left:50%;top:28px;width:2px;height:36px; | |
| background:var(--paint-8) | |
| } | |
| .label{ | |
| position:absolute;top:72px;left:50%;transform:translateX(-50%); | |
| white-space:nowrap;font-size:16px; | |
| } | |
| .day-boundary{ | |
| position:absolute;top:26px;height:118px;width:2px; | |
| background:var(--paint-9) | |
| } | |
| .day-label{ | |
| position:absolute;top:14px;transform:translateX(-50%); | |
| font-size:12px;opacity:.75;white-space:nowrap | |
| } | |
| .hour:focus-visible{outline:3px solid var(--paint-3);outline-offset:-5px;border-radius:10px} | |
| .hint{text-align:center;opacity:.8;margin-top:15px;font-size:.9rem} | |
| .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} | |
| /* ===== مدير النوافذ المتكيفة ===== */ | |
| #adaptivePopupActions button{ | |
| min-height:44px;margin:4px;border-radius:10px;border:1px solid var(--paint-11); | |
| background:var(--paint-12);color:var(--paint-3);padding:8px 12px;font:inherit | |
| } | |
| #adaptivePopup[hidden]{display:none} | |
| #adaptivePopup{ | |
| position:fixed;inset:0;z-index:3000;background:var(--paint-13); | |
| display:flex;align-items:center;justify-content:center;padding:14px | |
| } | |
| #adaptivePopupCard{ | |
| width:min(92vw,620px); | |
| /* الحجم يتبع المحتوى حتى هذا الحد فقط */ | |
| max-height:88vh;max-height:88dvh;overflow:hidden;display:flex;flex-direction:column;min-height:0; | |
| background:var(--paint-14);color:var(--paint-3);border-radius:20px;padding:18px; | |
| border:1px solid var(--paint-4);box-shadow:0 16px 55px var(--paint-15) | |
| } | |
| #adaptivePopupCard[data-size="short"]{width:min(90vw,440px)} | |
| #adaptivePopupCard[data-size="medium"]{width:min(92vw,560px)} | |
| #adaptivePopupCard[data-size="long"]{width:min(94vw,680px)} | |
| .adaptive-head{flex-shrink:0;display:flex;align-items:flex-start;justify-content:space-between;gap:12px} | |
| .adaptive-head h2{margin:3px 0 0;font-size:1.45rem} | |
| .adaptive-kind{font-size:.82rem;opacity:.75} | |
| #adaptivePopupClose{min-width:48px;min-height:48px;font-size:23px} | |
| #adaptivePopupMedia:empty{display:none} | |
| #adaptivePopupMedia{margin:5px 0;text-align:center;font-size:28px;flex-shrink:0} | |
| #adaptivePopupBody{ | |
| white-space:pre-wrap;line-height:1.8;font-size:17px;margin:10px 0;overflow-y:auto;min-height:0;overscroll-behavior:contain; | |
| overflow-wrap:anywhere | |
| } | |
| #adaptivePopupActions{flex-shrink:0;display:flex;flex-wrap:wrap;gap:6px} | |
| body{padding-bottom:190px} | |
| .ruler{height:240px;min-width:0} | |
| .hour{top:26px;height:190px;transform:none;border-inline-end:1px solid var(--cell-border);border-radius:14px;background:var(--cell-surface)} | |
| .tick{top:70px}.label{top:122px;font-size:21px}.axis{top:115px} | |
| .day-boundary,.day-label{display:none} | |
| #loadStatus{text-align:center;overflow-wrap:anywhere} | |
| #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} | |
| #adaptivePopupBody p{margin:8px 0} | |
| #adaptivePopupBody a{color:inherit;display:block;margin:12px 0;overflow-wrap:anywhere} | |
| #adaptivePopupClose{position:sticky;top:0;flex-shrink:0} | |
| #adaptivePopupMedia img{max-width:100%;max-height:220px;border-radius:12px} | |
| .solar-point{position:absolute;top:0;width:3px;height:100%;z-index:1;pointer-events:none} | |
| .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} | |
| .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} | |
| .hour .tick{top:102px;height:16px}.hour .label{top:132px} | |
| .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} | |
| .now-marker::after{content:"";position:absolute;top:100%;left:50%;height:172px;border-left:2px solid currentColor} | |
| #solarStatus{white-space:pre-line;text-align:center;line-height:1.7;font-size:14px} | |
| #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} | |
| #adaptivePopupBody label{display:block;margin:12px 0} | |
| #adaptivePopupBody label[hidden]{display:none} | |
| #adaptivePopupKind[hidden],#adaptivePopupMedia[hidden]{display:none} | |
| #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} | |
| #nowMarker{pointer-events:none} | |
| #adaptivePopupBody input,#adaptivePopupBody select{display:block;width:100%;padding:10px;font:inherit;margin:8px 0} | |
| #adaptivePopupClose{border-radius:10px;border:1px solid var(--cell-border);color:inherit;background:var(--paint-12)} | |
| #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} | |
| #adaptivePopupBody input[type="checkbox"]{width:auto;display:inline-block;margin-inline-start:12px} | |
| .ruler{height:390px}.hour{top:95px;height:270px;transform:none}.axis{top:197px} | |
| .hour-hit{position:absolute;inset:0;width:100%;border:0;background:transparent;color:inherit;font:inherit;cursor:pointer} | |
| .hour .tick{left:0}.hour .label{left:8px;transform:none;pointer-events:none} | |
| .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} | |
| .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} | |
| .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} | |
| .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} | |
| .reminder-axis{position:absolute;left:0;right:0;border-top:1px solid var(--cell-border);pointer-events:none} | |
| .now-marker{top:0;padding:0;transform:none;background:transparent;z-index:4} | |
| .now-marker::after{top:76px;left:0;height:300px;border-left-width:2px} | |
| .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} | |
| .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} | |
| #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} | |
| /* Simulation stays in document flow, below dialogs and reminder notices. */ | |
| #celestialSection{margin:16px 0 24px} | |
| #celestialSection h2{font-size:1.1rem;margin:0 0 10px} | |
| #celestialControls{display:flex;flex-wrap:wrap;gap:8px;margin:10px 0} | |
| #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} | |
| #celestialControls button:disabled{opacity:.6;cursor:default} | |
| #celestialScene{position:relative;height:240px;overflow:hidden;isolation:isolate;direction:ltr} | |
| #celestialScene[hidden]{display:none} | |
| .celestial-body{position:absolute;border-radius:50%;pointer-events:none;opacity:0} | |
| #celestialSun{background:#ffd45c;box-shadow:0 0 20px #ffd45c99} | |
| #celestialGround{position:absolute;inset:82% 0 0;background:#477552;border-top:1px solid #ffffff80;pointer-events:none} | |
| #celestialStick{position:absolute;left:50%;bottom:18%;height:38px;width:5px;background:#eee;z-index:1} | |
| #celestialShadow{position:absolute;left:50%;bottom:18%;height:5px;background:#111;transform-origin:left center} | |
| #celestialQibla{position:absolute;left:50%;top:50%;width:36px;height:40px;background:#191919;border:1px solid #eee;border-radius:3px;pointer-events:none} | |
| #celestialQibla::before{content:"";position:absolute;left:0;right:0;top:8px;height:6px;background:#d7b56d} | |
| #celestialInfo,#celestialLocation,#celestialStatus{margin:8px 0;line-height:1.7;overflow-wrap:anywhere;white-space:pre-line;font-size:14px} | |
| #celestialStatus:empty{display:none} | |
| [hidden]#celestialSettingsPanel{display:none} | |
| .qibla-sr-only{position:absolute;width:1px;height:1px;padding:0;margin:-1px;overflow:hidden;clip:rect(0,0,0,0);white-space:nowrap;border:0} | |
| #celestialSettingsPanel{white-space:normal} | |
| #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} | |
| #celestialBootStatus{white-space:pre-line;overflow-wrap:anywhere} | |
| </style> | |
| </head> | |
| <body> | |
| <main> | |
| <h1 data-ui="title"></h1> | |
| <section id="celestialSection"> | |
| <div id="celestialScene" hidden role="img"> | |
| <div id="celestialSun" class="celestial-body"></div> | |
| <div id="celestialMoon" class="celestial-body"></div> | |
| <div id="celestialGround"></div><div id="celestialShadow"></div> | |
| <div id="celestialStick"></div><div id="celestialQibla" hidden></div> | |
| </div> | |
| <button id="celestialSettingsButton" type="button" disabled hidden></button> | |
| <p id="celestialBootStatus" role="status" hidden></p> | |
| <button id="celestialBootRetry" type="button" hidden></button> | |
| </section> | |
| <div class="timeline-navigation"><span id="timelineDates"></span> <button id="timelineToday" type="button" data-ui="timelineToday" hidden></button></div> | |
| <div id="shell" class="ruler-shell" data-aria="ruler"> | |
| <div id="ruler" class="ruler"></div> | |
| </div> | |
| <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> | |
| <button id="adhkarButton" type="button"></button> | |
| <button id="reminderButton" type="button"></button><button id="reminderNotice" type="button" hidden aria-live="assertive" aria-atomic="true"></button> | |
| </main> | |
| <div id="celestialSettingsStorage" hidden> | |
| <div id="celestialSettingsPanel" hidden> | |
| <p id="celestialHelp" data-qibla-text="help"></p> | |
| <p id="celestialLocation"></p> | |
| <div id="celestialControls"> | |
| <button id="celestialLocate" type="button" disabled data-qibla-text="locate"></button> | |
| <button id="celestialCompass" type="button" disabled data-qibla-text="start"></button> | |
| <button id="celestialRetry" type="button" hidden data-qibla-text="retry"></button> | |
| </div> | |
| <label><input id="qiblaScreenReader" type="checkbox"><span data-qibla-text="screen_reader"></span></label> | |
| <label><input id="qiblaVibration" type="checkbox"><span data-qibla-text="vibration"></span></label> | |
| <p id="celestialStatus" aria-live="off"></p> | |
| <p id="qiblaDirection" aria-live="off"></p> | |
| <p id="qiblaCapability"></p> | |
| <p id="qiblaSaveStatus"></p> | |
| <p id="celestialInfo" aria-live="off"></p> | |
| </div> | |
| </div> | |
| <div id="qiblaAnnouncementOutside" class="qibla-sr-only" role="status" aria-live="polite" aria-atomic="true"></div> | |
| <div id="adaptivePopup" hidden role="dialog" aria-modal="true" aria-labelledby="adaptivePopupTitle"> | |
| <article id="adaptivePopupCard"> | |
| <header class="adaptive-head"> | |
| <div> | |
| <div id="adaptivePopupKind" class="adaptive-kind"></div> | |
| <h2 id="adaptivePopupTitle"></h2> | |
| </div> | |
| <button id="adaptivePopupClose" type="button" data-aria="close" data-ui="closeSymbol"></button> | |
| </header> | |
| <div id="adaptivePopupMedia"></div> | |
| <div id="adaptivePopupBody"></div> | |
| <div id="adaptivePopupActions"></div> | |
| <div id="qiblaAnnouncementInside" class="qibla-sr-only" role="status" aria-live="polite" aria-atomic="true"></div> | |
| </article> | |
| </div> | |
| <script src="astronomy.browser.min.js?v=20260926-ui2"></script> | |
| <script src="solar_engine.js?v=20260926-ui2"></script> | |
| <script src="personal_reminder_engine.js?v=20260929-complete1"></script> | |
| <script src="hijri_calendar_engine.js?v=20260926-worship1"></script> | |
| <script src="worship_event_engine.js?v=20260926-worship1"></script> | |
| <script src="quran_khatma.js?v=20261016-radio1"></script> | |
| <script src="quran_audio.js?v=20261016-radio1"></script> | |
| <script src="quran_reader.js?v=20261016-radio1"></script> | |
| <script src="native_bridge.js?v=20261016-native1"></script> | |
| <script src="islamic_radio.js?v=20261016-push1"></script> | |
| <script src="alert_actions.js?v=20261016-push1"></script> | |
| <script src="push_client.js?v=20261016-push1"></script> | |
| <script src="registry_app.js?v=20261016-push1"></script> | |
| <script> | |
| (function () { | |
| 'use strict'; | |
| const $ = id => document.getElementById(id); | |
| const scene = $('celestialScene'), status = $('celestialStatus'); | |
| const locate = $('celestialLocate'), compass = $('celestialCompass'); | |
| const panel = $('celestialSettingsPanel'); | |
| let controller, registry, config, ui, timer, sensorTimer, loading = false; | |
| let listening = false, requesting = false, generation = 0, locating = false; | |
| let confirmedLocation = false, pendingLocation, options; | |
| let aligned = false, stableSince = null, candidate = '', candidateSince = 0; | |
| let lastGuidance = '', lastAnnouncement = -Infinity, lastHeadingAt = 0; | |
| function validLocation(value) { | |
| return value && Number.isFinite(value.lat) && Number.isFinite(value.lng) | |
| && Math.abs(value.lat) <= 90 && Math.abs(value.lng) <= 180; | |
| } | |
| function saved(key) { | |
| try { return JSON.parse(localStorage.getItem(key)); } catch (_) { return null; } | |
| } | |
| function initialLocation() { | |
| const app = typeof RegistryApp !== 'undefined' ? RegistryApp.solarState().observer : null; | |
| for (const value of [pendingLocation, saved('muslim_clock_location'), app, window.MUSLIM_CLOCK_LOCATION, | |
| window.ISLAMIC_CLOCK_LOCATION, saved('clock_observer_v1')]) { | |
| if (validLocation(value)) return { value, confirmed: true, label: ui?.location_saved || '' }; | |
| } | |
| return { value: { lat: 31.9539, lng: 35.9106 }, confirmed: false, label: ui?.location_default || '' }; | |
| } | |
| function announce(message) { | |
| if (!options?.screen_reader || !message || document.hidden) return; | |
| const inside = !$('adaptivePopup').hidden; | |
| const live = $(inside ? 'qiblaAnnouncementInside' : 'qiblaAnnouncementOutside'); | |
| $(inside ? 'qiblaAnnouncementOutside' : 'qiblaAnnouncementInside').textContent = ''; | |
| if (live.textContent !== message) live.textContent = message; | |
| } | |
| function message(text, speak = true) { | |
| status.textContent = text || ''; | |
| if (speak) announce(text); | |
| } | |
| function resetGuidance() { | |
| aligned = false; stableSince = null; candidate = ''; candidateSince = 0; | |
| lastGuidance = ''; lastAnnouncement = -Infinity; | |
| $('qiblaDirection').textContent = ''; | |
| $('qiblaAnnouncementInside').textContent = ''; | |
| $('qiblaAnnouncementOutside').textContent = ''; | |
| } | |
| function clearHeading() { | |
| if (!controller) return; | |
| controller.setHeading(null); | |
| const state = controller.lastState; | |
| if (state) { | |
| state.headingDeg = 0; state.compassEnabled = false; | |
| state.qibla = window.CelestialSimulation.QiblaEngine.calculate(controller.location, null); | |
| controller.renderer.render(state); | |
| } | |
| $('celestialQibla').hidden = true; | |
| } | |
| function stop(text, speak = true) { | |
| generation++; requesting = false; listening = false; | |
| clearTimeout(sensorTimer); | |
| window.removeEventListener('deviceorientationabsolute', orientation, true); | |
| window.removeEventListener('deviceorientation', orientation, true); | |
| resetGuidance(); clearHeading(); | |
| compass.disabled = !controller; | |
| compass.textContent = ui?.start || ''; | |
| compass.setAttribute('aria-pressed', 'false'); | |
| if (text) message(text, speak); | |
| } | |
| function update() { | |
| if (!controller) return; | |
| try { controller.update(); } | |
| catch (error) { | |
| stop(ui?.updating_error); | |
| $('celestialRetry').hidden = false; | |
| console.error('Celestial update:', error); | |
| } | |
| } | |
| function showLocation(location, label) { | |
| $('celestialLocation').textContent = `${label} · ${location.lat.toFixed(4)}, ${location.lng.toFixed(4)}`; | |
| } | |
| function applyLocation(location, label) { | |
| if (!controller || !validLocation(location)) return; | |
| const changed = !confirmedLocation || location.lat !== controller.location.lat || location.lng !== controller.location.lng; | |
| controller.setLocation(location); confirmedLocation = true; | |
| if (changed) { resetGuidance(); clearHeading(); } | |
| showLocation(location, label); | |
| update(); | |
| } | |
| function configureUI() { | |
| $('celestialSettingsButton').textContent = ui.settings; | |
| $('celestialBootRetry').textContent = ui.retry; | |
| document.querySelectorAll('[data-qibla-text]').forEach(el => { | |
| el.textContent = ui[el.dataset.qiblaText] || ''; | |
| }); | |
| scene.setAttribute('aria-label', ui.scene); | |
| const stored = saved(config.storage_key); | |
| options = { | |
| screen_reader: typeof stored?.screen_reader === 'boolean' ? stored.screen_reader : config.defaults.screen_reader, | |
| vibration: typeof stored?.vibration === 'boolean' ? stored.vibration : config.defaults.vibration | |
| }; | |
| $('qiblaScreenReader').checked = options.screen_reader; | |
| $('qiblaVibration').checked = options.vibration; | |
| $('qiblaCapability').textContent = typeof navigator.vibrate === 'function' ? '' : ui.vibration_unavailable; | |
| } | |
| async function boot() { | |
| if (loading) return; | |
| loading = true; $('celestialRetry').hidden = true; $('celestialBootRetry').hidden = true; | |
| $('celestialBootStatus').hidden = true; | |
| if (controller) stop(); | |
| clearInterval(timer); | |
| try { | |
| const response = await fetch('./sky_simulation_registry.json', { cache: 'no-store' }); | |
| if (!response.ok) throw new Error('sky_simulation_registry.json: ' + response.status); | |
| registry = await response.json(); | |
| if (!registry.sky_color_model?.gradient_stops?.length || !registry.objects?.kaaba) throw new Error('Invalid celestial registry'); | |
| config = registry.qibla_accessibility; ui = config?.ui; | |
| const qiblaReady = Boolean(ui?.settings && config?.defaults && config?.alignment); | |
| if (qiblaReady) configureUI(); | |
| message(ui?.loading, false); | |
| if (!window.CelestialSimulation) { | |
| await import('./celestial_simulation.js?v=20260928-qibla1'); | |
| } | |
| const api = window.CelestialSimulation; | |
| if (!api) throw new Error('Missing CelestialSimulation'); | |
| const renderer = new api.SkySimulationRenderer({ | |
| container: scene, sun: $('celestialSun'), moon: $('celestialMoon'), | |
| shadow: $('celestialShadow'), info: $('celestialInfo'), kaaba: $('celestialQibla') | |
| }, registry); | |
| const render = renderer.render.bind(renderer); | |
| renderer.render = state => { | |
| render(state); | |
| for (const id of ['celestialSun', 'celestialMoon']) { | |
| const element = $(id), inset = parseFloat(element.style.width) / 2 + 2; | |
| element.style.left = `clamp(${inset}px, ${element.style.left}, calc(100% - ${inset}px))`; | |
| } | |
| $('celestialQibla').hidden = !confirmedLocation || !state.compassEnabled; | |
| // No changing spoken description or tooltip on the main visual scene. | |
| for (const id of ['celestialQibla', 'celestialShadow', 'celestialSun', 'celestialMoon']) $(id).removeAttribute('title'); | |
| }; | |
| const selected = initialLocation(); | |
| confirmedLocation = selected.confirmed; | |
| controller = new api.CelestialSimulationController({ location: selected.value, registry, renderer }); | |
| window.MuslimClockCelestialController = controller; | |
| showLocation(selected.value, selected.label); | |
| scene.hidden = false; | |
| update(); timer = setInterval(update, 60000); | |
| locate.disabled = !qiblaReady; compass.disabled = !qiblaReady; | |
| $('celestialSettingsButton').disabled = !qiblaReady; | |
| $('celestialSettingsButton').hidden = !qiblaReady; | |
| message(confirmedLocation ? ui?.location_ready : ui?.location_required, false); | |
| if (!qiblaReady) { | |
| $('celestialBootStatus').textContent = ui?.settings_missing || 'sky_simulation_registry.json: qibla_accessibility'; | |
| $('celestialBootStatus').hidden = false; | |
| } | |
| } catch (error) { | |
| message(ui?.load_error || 'sky_simulation_registry.json / celestial_simulation.js', false); | |
| // If a retry fails, keep a working location control only when a controller exists. | |
| locate.disabled = !controller; compass.disabled = !controller; | |
| $('celestialRetry').hidden = false; | |
| $('celestialBootStatus').textContent = ui?.load_error || 'sky_simulation_registry.json / celestial_simulation.js'; | |
| $('celestialBootStatus').hidden = false; | |
| $('celestialBootRetry').textContent = ui?.retry || '↻'; | |
| $('celestialBootRetry').hidden = false; | |
| console.error('Celestial boot:', error); | |
| } finally { loading = false; } | |
| } | |
| function storeOptions() { | |
| if (!options) return; | |
| options.screen_reader = $('qiblaScreenReader').checked; | |
| options.vibration = $('qiblaVibration').checked; | |
| $('qiblaAnnouncementInside').textContent = ''; | |
| $('qiblaAnnouncementOutside').textContent = ''; | |
| try { | |
| localStorage.setItem(config.storage_key, JSON.stringify(options)); | |
| $('qiblaSaveStatus').textContent = ''; | |
| } catch (_) { $('qiblaSaveStatus').textContent = ui.save_failed; } | |
| } | |
| function guidance(qibla, now) { | |
| if (!confirmedLocation || !Number.isFinite(qibla.deltaDeg)) return; | |
| const distance = Math.abs(qibla.deltaDeg), policy = config.alignment; | |
| if (aligned) { | |
| if (distance <= policy.exit_threshold_deg) return; | |
| aligned = false; stableSince = null; candidate = ''; lastGuidance = ''; | |
| $('qiblaDirection').textContent = ''; | |
| $('qiblaAnnouncementInside').textContent = ''; | |
| $('qiblaAnnouncementOutside').textContent = ''; | |
| } | |
| if (distance <= registry.objects.kaaba.alignment_threshold_deg) { | |
| candidate = ''; | |
| if (stableSince === null) stableSince = now; | |
| if (now - stableSince < policy.stable_ms) return; | |
| aligned = true; lastGuidance = 'aligned'; lastAnnouncement = now; | |
| $('qiblaDirection').textContent = ui.aligned; | |
| announce(ui.aligned); | |
| if (options.vibration && typeof navigator.vibrate === 'function') { | |
| try { | |
| if (!navigator.vibrate(config.vibration_pattern_ms)) $('qiblaCapability').textContent = ui.vibration_unavailable; | |
| } catch (_) { $('qiblaCapability').textContent = ui.vibration_unavailable; } | |
| } | |
| return; | |
| } | |
| stableSince = null; | |
| const key = (qibla.deltaDeg > 0 ? 'right' : 'left') | |
| + (distance > registry.objects.kaaba.near_alignment_threshold_deg ? '_far' : ''); | |
| if (candidate !== key) { candidate = key; candidateSince = now; } | |
| if (now - candidateSince < policy.direction_stable_ms || key === lastGuidance | |
| || now - lastAnnouncement < policy.direction_interval_ms) return; | |
| lastGuidance = key; lastAnnouncement = now; | |
| $('qiblaDirection').textContent = ui[key]; announce(ui[key]); | |
| } | |
| function orientation(event) { | |
| if (!listening || document.hidden || !controller) return; | |
| // Relative alpha alone cannot establish north. Never use it for alignment. | |
| const hasWebkitHeading = Number.isFinite(event.webkitCompassHeading); | |
| if (hasWebkitHeading && (event.webkitCompassHeading < 0 || event.webkitCompassHeading >= 360)) return; | |
| const heading = hasWebkitHeading ? event.webkitCompassHeading | |
| : event.absolute === true && Number.isFinite(event.alpha) && event.alpha >= 0 && event.alpha <= 360 ? 360 - event.alpha : null; | |
| if (!Number.isFinite(heading)) return; | |
| const policy = config.alignment; | |
| const inaccurate = Number.isFinite(event.webkitCompassAccuracy) | |
| && (event.webkitCompassAccuracy < 0 || event.webkitCompassAccuracy > policy.max_reported_accuracy_deg); | |
| const tilted = (Number.isFinite(event.beta) && Math.abs(event.beta) > policy.max_tilt_deg) | |
| || (Number.isFinite(event.gamma) && Math.abs(event.gamma) > policy.max_tilt_deg); | |
| if (inaccurate || tilted) { | |
| const warning = inaccurate ? ui.calibrate : ui.flat_phone; | |
| if (status.textContent !== warning) { resetGuidance(); clearHeading(); message(warning); } | |
| return; | |
| } | |
| const now = performance.now(); | |
| // Some phones send both event types for the same sample. | |
| if (lastHeadingAt && now - lastHeadingAt < 40) return; | |
| lastHeadingAt = now; | |
| clearTimeout(sensorTimer); | |
| sensorTimer = setTimeout(() => stop(ui.sensor_timeout), policy.sensor_timeout_ms); | |
| message('', false); | |
| const angle = Number.isFinite(window.screen?.orientation?.angle) ? window.screen.orientation.angle | |
| : Number.isFinite(window.orientation) ? window.orientation : 0; | |
| controller.setHeading(heading + angle); | |
| const state = controller.lastState; | |
| if (!state) return; | |
| state.headingDeg = controller.headingDeg; state.compassEnabled = true; | |
| state.qibla = window.CelestialSimulation.QiblaEngine.calculate(controller.location, | |
| controller.headingDeg, { alignmentThresholdDeg: registry.objects.kaaba.alignment_threshold_deg }); | |
| controller.renderer.render(state); | |
| guidance(state.qibla, now); | |
| } | |
| async function startCompass() { | |
| if (!ui || !controller) return; | |
| if (listening || requesting) { stop(ui.stopped); return; } | |
| if (!confirmedLocation) { message(ui.location_required); return; } | |
| if (!window.isSecureContext || typeof DeviceOrientationEvent === 'undefined') { | |
| message(ui.sensor_unavailable); return; | |
| } | |
| const token = ++generation; | |
| requesting = true; compass.disabled = true; | |
| try { | |
| // Called directly from the user's click, before any geolocation await. | |
| if (typeof DeviceOrientationEvent.requestPermission === 'function' | |
| && await DeviceOrientationEvent.requestPermission(true) !== 'granted') throw new Error('permission'); | |
| if (token !== generation || document.hidden) return; | |
| resetGuidance(); lastHeadingAt = 0; | |
| window.addEventListener('deviceorientationabsolute', orientation, true); | |
| window.addEventListener('deviceorientation', orientation, true); | |
| listening = true; compass.textContent = ui.stop; compass.setAttribute('aria-pressed', 'true'); | |
| message(ui.waiting); | |
| sensorTimer = setTimeout(() => stop(ui.sensor_timeout), config.alignment.sensor_timeout_ms); | |
| } catch (_) { message(ui.permission_denied); } | |
| finally { | |
| if (token === generation) { requesting = false; compass.disabled = false; } | |
| } | |
| } | |
| locate.addEventListener('click', () => { | |
| if (!controller || !ui || locating) return; | |
| if (!navigator.geolocation) { message(ui.location_unavailable); return; } | |
| stop(); locating = true; locate.disabled = true; message(ui.locating); | |
| navigator.geolocation.getCurrentPosition(position => { | |
| locating = false; locate.disabled = false; | |
| const location = { lat: position.coords.latitude, lng: position.coords.longitude }; | |
| if (!validLocation(location)) { message(ui.location_failed); return; } | |
| pendingLocation = location; | |
| applyLocation(location, ui.location_device); | |
| try { localStorage.setItem('muslim_clock_location', JSON.stringify(location)); } | |
| catch (_) { $('qiblaSaveStatus').textContent = ui.save_failed; } | |
| message(ui.location_ready); | |
| }, () => { | |
| locating = false; locate.disabled = false; message(ui.location_failed); | |
| }, { enableHighAccuracy: true, timeout: 15000, maximumAge: 60000 }); | |
| }); | |
| compass.addEventListener('click', startCompass); | |
| $('qiblaScreenReader').addEventListener('change', storeOptions); | |
| $('qiblaVibration').addEventListener('change', storeOptions); | |
| $('celestialRetry').addEventListener('click', boot); | |
| $('celestialBootRetry').addEventListener('click', boot); | |
| $('celestialSettingsButton').addEventListener('click', async () => { | |
| await window.registryReady; | |
| if (typeof RegistryApp !== 'undefined') RegistryApp.qiblaSettings(Date.now()); | |
| }); | |
| window.MuslimClockQibla = Object.freeze({ mountSettings(host) { | |
| host.appendChild(panel); panel.hidden = false; | |
| } }); | |
| window.addEventListener('muslim-clock-popup-leave', () => { | |
| panel.hidden = true; $('celestialSettingsStorage').appendChild(panel); | |
| $('qiblaAnnouncementInside').textContent = ''; | |
| }); | |
| window.addEventListener('muslim-clock-location-change', event => { | |
| if (!validLocation(event.detail)) return; | |
| // This event is sent only when the user selects a location in prayer settings. | |
| const changed = !pendingLocation || event.detail.lat !== pendingLocation.lat || event.detail.lng !== pendingLocation.lng; | |
| if (changed) { | |
| pendingLocation = { lat: event.detail.lat, lng: event.detail.lng }; | |
| try { localStorage.setItem('muslim_clock_location', JSON.stringify(pendingLocation)); } catch (_) {} | |
| if (controller) applyLocation(pendingLocation, ui?.location_app || ''); | |
| } | |
| }); | |
| document.addEventListener('visibilitychange', () => { | |
| if (document.hidden) stop(ui?.stopped, false); | |
| else update(); | |
| }); | |
| window.addEventListener('pagehide', () => stop(null, false)); | |
| boot(); | |
| })(); | |
| </script> | |
| </body> | |
| </html> | |