Islamic_clock / index.html
Asem75's picture
Upload index.html
463c4fe verified
Raw History Blame Contribute Delete
31.8 kB
<!doctype html>
<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!important}
#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!important}
#adaptivePopupKind[hidden],#adaptivePopupMedia[hidden]{display:none!important}
#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!important;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!important}
.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}
#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>