Spaces:
Running
Running
Upload index.html
Browse files- index.html +242 -0
index.html
CHANGED
|
@@ -137,11 +137,47 @@ body{padding-bottom:190px}
|
|
| 137 |
.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}
|
| 138 |
.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}
|
| 139 |
#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}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 140 |
</style>
|
| 141 |
</head>
|
| 142 |
<body>
|
| 143 |
<main>
|
| 144 |
<h1 data-ui="title"></h1>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 145 |
<div class="timeline-navigation"><span id="timelineDates"></span> <button id="timelineToday" type="button" data-ui="timelineToday" hidden></button></div>
|
| 146 |
<div id="shell" class="ruler-shell" data-aria="ruler">
|
| 147 |
<div id="ruler" class="ruler"></div>
|
|
@@ -176,5 +212,211 @@ body{padding-bottom:190px}
|
|
| 176 |
<script src="hijri_calendar_engine.js?v=20260926-worship1"></script>
|
| 177 |
<script src="worship_event_engine.js?v=20260926-worship1"></script>
|
| 178 |
<script src="registry_app.js?v=20260927-markers2"></script>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 179 |
</body>
|
| 180 |
</html>
|
|
|
|
| 137 |
.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}
|
| 138 |
.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}
|
| 139 |
#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}
|
| 140 |
+
/* Simulation stays in document flow, below dialogs and reminder notices. */
|
| 141 |
+
#celestialSection{margin:16px 0 24px}
|
| 142 |
+
#celestialSection h2{font-size:1.1rem;margin:0 0 10px}
|
| 143 |
+
#celestialControls{display:flex;flex-wrap:wrap;gap:8px;margin:10px 0}
|
| 144 |
+
#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}
|
| 145 |
+
#celestialControls button:disabled{opacity:.6;cursor:default}
|
| 146 |
+
#celestialScene{position:relative;height:240px;overflow:hidden;isolation:isolate;direction:ltr}
|
| 147 |
+
#celestialScene[hidden]{display:none}
|
| 148 |
+
.celestial-body{position:absolute;border-radius:50%;pointer-events:none;opacity:0}
|
| 149 |
+
#celestialSun{background:#ffd45c;box-shadow:0 0 20px #ffd45c99}
|
| 150 |
+
#celestialGround{position:absolute;inset:82% 0 0;background:#477552;border-top:1px solid #ffffff80;pointer-events:none}
|
| 151 |
+
#celestialStick{position:absolute;left:50%;bottom:18%;height:38px;width:5px;background:#eee;z-index:1}
|
| 152 |
+
#celestialShadow{position:absolute;left:50%;bottom:18%;height:5px;background:#111;transform-origin:left center}
|
| 153 |
+
#celestialQibla{position:absolute;left:50%;top:50%;width:36px;height:40px;background:#191919;border:1px solid #eee;border-radius:3px;pointer-events:none}
|
| 154 |
+
#celestialQibla::before{content:"";position:absolute;left:0;right:0;top:8px;height:6px;background:#d7b56d}
|
| 155 |
+
#celestialInfo,#celestialLocation,#celestialStatus{margin:8px 0;line-height:1.7;overflow-wrap:anywhere;white-space:pre-line;font-size:14px}
|
| 156 |
+
#celestialStatus:empty{display:none}
|
| 157 |
</style>
|
| 158 |
</head>
|
| 159 |
<body>
|
| 160 |
<main>
|
| 161 |
<h1 data-ui="title"></h1>
|
| 162 |
+
<section id="celestialSection" aria-labelledby="celestialTitle">
|
| 163 |
+
<h2 id="celestialTitle">محاكاة الشمس والقمر والقبلة</h2>
|
| 164 |
+
<div id="celestialScene" hidden role="img" aria-label="محاكاة السماء حسب الموقع والوقت الحالي">
|
| 165 |
+
<div id="celestialSun" class="celestial-body"></div>
|
| 166 |
+
<div id="celestialMoon" class="celestial-body"></div>
|
| 167 |
+
<div id="celestialGround"></div>
|
| 168 |
+
<div id="celestialShadow"></div>
|
| 169 |
+
<div id="celestialStick"></div>
|
| 170 |
+
<div id="celestialQibla" hidden></div>
|
| 171 |
+
</div>
|
| 172 |
+
<p id="celestialLocation"></p>
|
| 173 |
+
<p id="celestialInfo"></p>
|
| 174 |
+
<div id="celestialControls">
|
| 175 |
+
<button id="celestialLocate" type="button" disabled>تحديد موقع المحاكاة</button>
|
| 176 |
+
<button id="celestialCompass" type="button" disabled>تفعيل اتجاه القبلة</button>
|
| 177 |
+
<button id="celestialRetry" type="button" hidden>إعادة المحاولة</button>
|
| 178 |
+
</div>
|
| 179 |
+
<p id="celestialStatus" role="status">جارٍ تحميل المحاكاة...</p>
|
| 180 |
+
</section>
|
| 181 |
<div class="timeline-navigation"><span id="timelineDates"></span> <button id="timelineToday" type="button" data-ui="timelineToday" hidden></button></div>
|
| 182 |
<div id="shell" class="ruler-shell" data-aria="ruler">
|
| 183 |
<div id="ruler" class="ruler"></div>
|
|
|
|
| 212 |
<script src="hijri_calendar_engine.js?v=20260926-worship1"></script>
|
| 213 |
<script src="worship_event_engine.js?v=20260926-worship1"></script>
|
| 214 |
<script src="registry_app.js?v=20260927-markers2"></script>
|
| 215 |
+
<script>
|
| 216 |
+
(function () {
|
| 217 |
+
"use strict";
|
| 218 |
+
const byId = (id) => document.getElementById(id);
|
| 219 |
+
const scene = byId("celestialScene");
|
| 220 |
+
const status = byId("celestialStatus");
|
| 221 |
+
const locate = byId("celestialLocate");
|
| 222 |
+
const compass = byId("celestialCompass");
|
| 223 |
+
const retry = byId("celestialRetry");
|
| 224 |
+
const locationLabel = byId("celestialLocation");
|
| 225 |
+
let controller;
|
| 226 |
+
let timer;
|
| 227 |
+
let compassTimer;
|
| 228 |
+
let compassActive = false;
|
| 229 |
+
let compassListening = false;
|
| 230 |
+
let loading = false;
|
| 231 |
+
let loadAttempt = 0;
|
| 232 |
+
|
| 233 |
+
function validLocation(value) {
|
| 234 |
+
return value && Number.isFinite(value.lat) && Number.isFinite(value.lng)
|
| 235 |
+
&& Math.abs(value.lat) <= 90 && Math.abs(value.lng) <= 180;
|
| 236 |
+
}
|
| 237 |
+
|
| 238 |
+
function initialLocation() {
|
| 239 |
+
const provided = window.MUSLIM_CLOCK_LOCATION || window.ISLAMIC_CLOCK_LOCATION;
|
| 240 |
+
if (validLocation(provided)) return { value: provided, label: "موقع المحاكاة: موقع التطبيق" };
|
| 241 |
+
try {
|
| 242 |
+
const saved = JSON.parse(localStorage.getItem("muslim_clock_location"));
|
| 243 |
+
if (validLocation(saved)) return { value: saved, label: "موقع المحاكاة: الموقع المحفوظ" };
|
| 244 |
+
} catch (_) { /* Storage can be unavailable in embedded browsers. */ }
|
| 245 |
+
return { value: { lat: 31.9539, lng: 35.9106 }, label: "موقع المحاكاة الافتراضي: عمّان (لم يُحدَّد موقع الجهاز)" };
|
| 246 |
+
}
|
| 247 |
+
|
| 248 |
+
function showLocation(value, label) {
|
| 249 |
+
locationLabel.textContent = `${label} · ${value.lat.toFixed(4)}, ${value.lng.toFixed(4)}`;
|
| 250 |
+
}
|
| 251 |
+
|
| 252 |
+
function update() {
|
| 253 |
+
if (!controller) return;
|
| 254 |
+
try {
|
| 255 |
+
controller.update();
|
| 256 |
+
} catch (error) {
|
| 257 |
+
status.textContent = "تعذر تحديث المحاكاة. أعد المحاولة.";
|
| 258 |
+
retry.hidden = false;
|
| 259 |
+
console.error("Celestial simulation update:", error);
|
| 260 |
+
}
|
| 261 |
+
}
|
| 262 |
+
|
| 263 |
+
async function boot() {
|
| 264 |
+
if (loading) return;
|
| 265 |
+
loading = true;
|
| 266 |
+
retry.hidden = true;
|
| 267 |
+
status.textContent = "جارٍ تحميل المحاكاة...";
|
| 268 |
+
try {
|
| 269 |
+
if (!window.CelestialSimulation) {
|
| 270 |
+
try {
|
| 271 |
+
// Module scope prevents collisions with the existing solar engine.
|
| 272 |
+
await import(`./celestial_simulation.js?v=20260928-integration1&attempt=${++loadAttempt}`);
|
| 273 |
+
if (!window.CelestialSimulation) throw new Error("Missing simulation API");
|
| 274 |
+
} catch (_) {
|
| 275 |
+
throw new Error("تعذر تحميل celestial_simulation.js. تأكد من وجوده بجانب index.html وبالاسم نفسه، دون .txt.");
|
| 276 |
+
}
|
| 277 |
+
}
|
| 278 |
+
let registry;
|
| 279 |
+
try {
|
| 280 |
+
const response = await fetch("./sky_simulation_registry.json", { cache: "no-store" });
|
| 281 |
+
if (!response.ok) throw new Error("Registry HTTP " + response.status);
|
| 282 |
+
registry = await response.json();
|
| 283 |
+
if (!Array.isArray(registry.sky_color_model?.gradient_stops)
|
| 284 |
+
|| !registry.sky_color_model.gradient_stops.length || !registry.objects) {
|
| 285 |
+
throw new Error("Invalid sky registry");
|
| 286 |
+
}
|
| 287 |
+
} catch (_) {
|
| 288 |
+
throw new Error("تعذر قراءة sky_simulation_registry.json. تأكد من اسمه ووجوده بجانب index.html وصحة محتواه.");
|
| 289 |
+
}
|
| 290 |
+
const api = window.CelestialSimulation;
|
| 291 |
+
const renderer = new api.SkySimulationRenderer({
|
| 292 |
+
container: scene, sun: byId("celestialSun"), moon: byId("celestialMoon"),
|
| 293 |
+
shadow: byId("celestialShadow"), info: byId("celestialInfo"),
|
| 294 |
+
kaaba: byId("celestialQibla")
|
| 295 |
+
}, registry);
|
| 296 |
+
const render = renderer.render.bind(renderer);
|
| 297 |
+
renderer.render = (state) => {
|
| 298 |
+
render(state);
|
| 299 |
+
// Keep projected discs fully inside the scene at east/west edges.
|
| 300 |
+
for (const id of ["celestialSun", "celestialMoon"]) {
|
| 301 |
+
const element = byId(id);
|
| 302 |
+
const inset = parseFloat(element.style.width) / 2 + 2;
|
| 303 |
+
element.style.left = `clamp(${inset}px, ${element.style.left}, calc(100% - ${inset}px))`;
|
| 304 |
+
}
|
| 305 |
+
const qibla = byId("celestialQibla");
|
| 306 |
+
qibla.hidden = !state.compassEnabled;
|
| 307 |
+
if (!state.compassEnabled) qibla.title = "اتجاه الهاتف غير متاح";
|
| 308 |
+
scene.setAttribute("aria-label", byId("celestialInfo").textContent);
|
| 309 |
+
};
|
| 310 |
+
const selected = controller
|
| 311 |
+
? { value: controller.location, label: "موقع المحاكاة الحالي" }
|
| 312 |
+
: initialLocation();
|
| 313 |
+
const oldHeading = controller?.headingDeg;
|
| 314 |
+
controller = new api.CelestialSimulationController({ location: selected.value, registry, renderer });
|
| 315 |
+
if (Number.isFinite(oldHeading)) controller.setHeading(oldHeading);
|
| 316 |
+
window.MuslimClockCelestialController = controller;
|
| 317 |
+
showLocation(selected.value, selected.label);
|
| 318 |
+
scene.hidden = false;
|
| 319 |
+
controller.update();
|
| 320 |
+
clearInterval(timer);
|
| 321 |
+
timer = setInterval(update, 60000);
|
| 322 |
+
locate.disabled = false;
|
| 323 |
+
compass.disabled = compassListening;
|
| 324 |
+
status.textContent = "";
|
| 325 |
+
} catch (error) {
|
| 326 |
+
status.textContent = error.message;
|
| 327 |
+
retry.hidden = false;
|
| 328 |
+
} finally {
|
| 329 |
+
loading = false;
|
| 330 |
+
}
|
| 331 |
+
}
|
| 332 |
+
|
| 333 |
+
locate.addEventListener("click", () => {
|
| 334 |
+
if (!navigator.geolocation) {
|
| 335 |
+
status.textContent = "تحديد الموقع غير متاح في هذا المتصفح.";
|
| 336 |
+
return;
|
| 337 |
+
}
|
| 338 |
+
locate.disabled = true;
|
| 339 |
+
status.textContent = "جارٍ تحديد موقع الجهاز للمحاكاة...";
|
| 340 |
+
navigator.geolocation.getCurrentPosition((position) => {
|
| 341 |
+
const location = { lat: position.coords.latitude, lng: position.coords.longitude };
|
| 342 |
+
controller.setLocation(location);
|
| 343 |
+
showLocation(location, "موقع المحاكاة: موقع الجهاز");
|
| 344 |
+
status.textContent = "";
|
| 345 |
+
locate.disabled = false;
|
| 346 |
+
update();
|
| 347 |
+
}, () => {
|
| 348 |
+
status.textContent = "تعذر تحديد الموقع أو لم يُمنح الإذن. بقي الموقع المبين أعلاه دون تغيير.";
|
| 349 |
+
locate.disabled = false;
|
| 350 |
+
}, { enableHighAccuracy: true, timeout: 15000, maximumAge: 60000 });
|
| 351 |
+
});
|
| 352 |
+
|
| 353 |
+
function orientation(event) {
|
| 354 |
+
// Relative alpha is not a north-referenced compass bearing.
|
| 355 |
+
const heading = Number.isFinite(event.webkitCompassHeading)
|
| 356 |
+
? event.webkitCompassHeading
|
| 357 |
+
: event.absolute && Number.isFinite(event.alpha) ? 360 - event.alpha : null;
|
| 358 |
+
if (!Number.isFinite(heading) || !controller) return;
|
| 359 |
+
clearTimeout(compassTimer);
|
| 360 |
+
if (!compassActive) {
|
| 361 |
+
compassActive = true;
|
| 362 |
+
status.textContent = "";
|
| 363 |
+
compass.textContent = "اتجاه القبلة مفعّل";
|
| 364 |
+
}
|
| 365 |
+
controller.setHeading(heading);
|
| 366 |
+
// Reproject the current snapshot without repeating astronomy on every sensor event.
|
| 367 |
+
const state = controller.lastState;
|
| 368 |
+
if (state) {
|
| 369 |
+
state.headingDeg = controller.headingDeg;
|
| 370 |
+
state.compassEnabled = true;
|
| 371 |
+
state.qibla = window.CelestialSimulation.QiblaEngine.calculate(controller.location,
|
| 372 |
+
controller.headingDeg, { alignmentThresholdDeg: controller.registry.objects.kaaba?.alignment_threshold_deg });
|
| 373 |
+
controller.renderer.render(state);
|
| 374 |
+
}
|
| 375 |
+
}
|
| 376 |
+
|
| 377 |
+
compass.addEventListener("click", async () => {
|
| 378 |
+
if (!window.isSecureContext || typeof DeviceOrientationEvent === "undefined") {
|
| 379 |
+
status.textContent = "حساس الاتجاه غير متاح. افتح التطبيق عبر HTTPS على هاتف يدعم البوصلة.";
|
| 380 |
+
return;
|
| 381 |
+
}
|
| 382 |
+
compass.disabled = true;
|
| 383 |
+
try {
|
| 384 |
+
if (typeof DeviceOrientationEvent.requestPermission === "function"
|
| 385 |
+
&& await DeviceOrientationEvent.requestPermission() !== "granted") {
|
| 386 |
+
throw new Error("لم يُمنح إذن حساس الاتجاه.");
|
| 387 |
+
}
|
| 388 |
+
if (!compassListening) {
|
| 389 |
+
window.addEventListener("deviceorientationabsolute", orientation, true);
|
| 390 |
+
window.addEventListener("deviceorientation", orientation, true);
|
| 391 |
+
compassListening = true;
|
| 392 |
+
}
|
| 393 |
+
status.textContent = "بانتظار قراءة بوصلة الهاتف...";
|
| 394 |
+
compassTimer = setTimeout(() => {
|
| 395 |
+
if (compassActive) return;
|
| 396 |
+
window.removeEventListener("deviceorientationabsolute", orientation, true);
|
| 397 |
+
window.removeEventListener("deviceorientation", orientation, true);
|
| 398 |
+
compassListening = false;
|
| 399 |
+
compass.disabled = false;
|
| 400 |
+
status.textContent = "لم تصل قراءة اتجاه مطلقة. يبقى اتجاه القبلة الرقمي معروضًا؛ جرّب فتح التطبيق مباشرة خارج الإطار.";
|
| 401 |
+
}, 8000);
|
| 402 |
+
} catch (error) {
|
| 403 |
+
compass.disabled = false;
|
| 404 |
+
status.textContent = error.message || "تعذر تفعيل البوصلة.";
|
| 405 |
+
}
|
| 406 |
+
});
|
| 407 |
+
|
| 408 |
+
retry.addEventListener("click", boot);
|
| 409 |
+
document.addEventListener("visibilitychange", () => {
|
| 410 |
+
if (!document.hidden) update();
|
| 411 |
+
});
|
| 412 |
+
window.addEventListener("muslim-clock-location-change", (event) => {
|
| 413 |
+
if (!controller || !validLocation(event.detail)) return;
|
| 414 |
+
controller.setLocation(event.detail);
|
| 415 |
+
showLocation(event.detail, "موقع المحاكاة: موقع التطبيق");
|
| 416 |
+
update();
|
| 417 |
+
});
|
| 418 |
+
boot();
|
| 419 |
+
})();
|
| 420 |
+
</script>
|
| 421 |
</body>
|
| 422 |
</html>
|