(function () { "use strict"; const SETTINGS_STORAGE_KEY = "tarot-time-settings-v1"; let config = { defaultSettings: { latitude: 51.5074, longitude: -0.1278, timeFormat: "minutes", birthDate: "", tarotDeck: "ceremonial-magick", stellariumBackgroundEnabled: false, hasExplicitLocation: false }, onSettingsApplied: null, onSyncSkyBackground: null, onStatus: null, onConnectionSaved: null, onReopenActiveSection: null, setActiveSection: null, getActiveSection: null, onRenderWeek: null }; let lastNonSettingsSection = "home"; function getElements() { return { openSettingsEl: document.getElementById("open-settings"), closeSettingsEl: document.getElementById("close-settings"), latEl: document.getElementById("lat"), lngEl: document.getElementById("lng"), timeFormatEl: document.getElementById("time-format"), birthDateEl: document.getElementById("birth-date"), tarotDeckEl: document.getElementById("tarot-deck"), tarotDeckCacheStatusEl: document.getElementById("tarot-deck-cache-status"), stellariumBackgroundEl: document.getElementById("stellarium-background"), stellariumBackgroundHintEl: document.getElementById("stellarium-background-hint"), apiBaseUrlEl: document.getElementById("api-base-url"), apiKeyEl: document.getElementById("api-key"), saveSettingsEl: document.getElementById("save-settings"), useLocationEl: document.getElementById("use-location") }; } function setLocationEntryState(isExplicit) { const { latEl, lngEl } = getElements(); const normalizedValue = isExplicit ? "true" : "false"; if (latEl) { latEl.dataset.explicitLocation = normalizedValue; } if (lngEl) { lngEl.dataset.explicitLocation = normalizedValue; } } function hasExplicitLocationEntry() { const { latEl } = getElements(); return latEl?.dataset.explicitLocation === "true"; } function syncStellariumBackgroundAvailability() { const { stellariumBackgroundEl, stellariumBackgroundHintEl } = getElements(); if (!stellariumBackgroundEl) { return; } const hasExplicitLocation = hasExplicitLocationEntry(); stellariumBackgroundEl.disabled = !hasExplicitLocation; if (!hasExplicitLocation) { stellariumBackgroundEl.checked = false; } if (stellariumBackgroundHintEl) { stellariumBackgroundHintEl.textContent = hasExplicitLocation ? "Uses your saved location to load the live sky background." : "Enter a location or use My Location before enabling the live sky background."; } } function markLocationAsExplicit() { setLocationEntryState(true); syncStellariumBackgroundAvailability(); } function getConnectionSettings() { return window.TarotAppConfig?.getConnectionSettings?.() || { apiBaseUrl: String(window.TarotAppConfig?.apiBaseUrl || "").trim(), apiKey: String(window.TarotAppConfig?.apiKey || "").trim() }; } function syncConnectionInputs() { const { apiBaseUrlEl, apiKeyEl } = getElements(); const connectionSettings = getConnectionSettings(); if (apiBaseUrlEl) { apiBaseUrlEl.value = String(connectionSettings.apiBaseUrl || ""); } if (apiKeyEl) { apiKeyEl.value = String(connectionSettings.apiKey || ""); } } function hasConnectionChanged(previous, next) { return String(previous?.apiBaseUrl || "").trim() !== String(next?.apiBaseUrl || "").trim() || String(previous?.apiKey || "").trim() !== String(next?.apiKey || "").trim(); } function setStatus(text) { if (typeof config.onStatus === "function") { config.onStatus(text); } } function applyExternalSettings(settings) { if (typeof config.onSettingsApplied === "function") { config.onSettingsApplied(settings); } } function formatDeckCacheStatus(status) { const activeDeckId = String(status?.activeDeckId || normalizeTarotDeck(getElements().tarotDeckEl?.value)).trim().toLowerCase(); const totalDeckCount = Number(status?.totalDeckCount) || 0; const warmedDeckCount = Number(status?.warmedDeckCount) || 0; const warmedAllDecks = totalDeckCount > 0 && warmedDeckCount >= totalDeckCount; if (status?.selectedDeckPhase === "loading") { return "Caching selected deck images to this browser..."; } if (status?.selectedDeckPhase === "error") { return "Selected deck cache warmup hit an error. Images will still load on demand."; } if (status?.backgroundPhase === "loading") { if (warmedDeckCount > 0 && totalDeckCount > 0) { return `Selected deck cached. Warming remaining decks in background (${warmedDeckCount}/${totalDeckCount}).`; } return "Selected deck cached. Warming remaining decks in background..."; } if (status?.backgroundPhase === "error") { return "Selected deck cached. Background warmup for other decks stopped early."; } if (status?.selectedDeckPhase === "ready" || warmedAllDecks) { if (warmedAllDecks) { return "Selected deck cached. All available decks are warmed in this browser."; } return `Selected deck cached and ready for fullscreen use (${activeDeckId}).`; } return "Deck cache idle."; } function syncDeckCacheStatus(status) { const { tarotDeckCacheStatusEl } = getElements(); if (!tarotDeckCacheStatusEl) { return; } tarotDeckCacheStatusEl.textContent = formatDeckCacheStatus(status); } function syncSky(geo, options) { if (typeof config.onSyncSkyBackground === "function") { config.onSyncSkyBackground(geo, options); } } function normalizeTimeFormat(value) { if (value === "hours") { return "hours"; } if (value === "seconds") { return "seconds"; } return "minutes"; } function normalizeBirthDate(value) { const normalized = String(value || "").trim(); if (!normalized) { return ""; } return /^\d{4}-\d{2}-\d{2}$/.test(normalized) ? normalized : ""; } function getKnownTarotDeckIds() { const knownDeckIds = new Set(); const deckOptions = window.TarotCardImages?.getDeckOptions?.(); if (Array.isArray(deckOptions)) { deckOptions.forEach((option) => { const id = String(option?.id || "").trim().toLowerCase(); if (id) { knownDeckIds.add(id); } }); } if (!knownDeckIds.size) { knownDeckIds.add(String(config.defaultSettings?.tarotDeck || "ceremonial-magick").trim().toLowerCase()); } return knownDeckIds; } function getFallbackTarotDeckId() { const deckOptions = window.TarotCardImages?.getDeckOptions?.(); if (Array.isArray(deckOptions)) { for (let i = 0; i < deckOptions.length; i += 1) { const id = String(deckOptions[i]?.id || "").trim().toLowerCase(); if (id) { return id; } } } return String(config.defaultSettings?.tarotDeck || "ceremonial-magick").trim().toLowerCase(); } function normalizeTarotDeck(value) { const normalized = String(value || "").trim().toLowerCase(); const knownDeckIds = getKnownTarotDeckIds(); if (knownDeckIds.has(normalized)) { return normalized; } return getFallbackTarotDeckId(); } function parseStoredNumber(value, fallback) { const parsed = Number(value); return Number.isFinite(parsed) ? parsed : fallback; } function parseStoredBoolean(value, fallback = false) { return typeof value === "boolean" ? value : fallback; } function hasLegacyExplicitLocation(settings, normalizedLatitude, normalizedLongitude) { const hasStoredCoordinates = Number.isFinite(Number(settings?.latitude)) && Number.isFinite(Number(settings?.longitude)); if (!hasStoredCoordinates) { return false; } return Math.abs(normalizedLatitude - Number(config.defaultSettings.latitude)) > 0.00005 || Math.abs(normalizedLongitude - Number(config.defaultSettings.longitude)) > 0.00005; } function normalizeSettings(settings) { const latitude = parseStoredNumber(settings?.latitude, config.defaultSettings.latitude); const longitude = parseStoredNumber(settings?.longitude, config.defaultSettings.longitude); const hasExplicitLocation = typeof settings?.hasExplicitLocation === "boolean" ? settings.hasExplicitLocation : hasLegacyExplicitLocation(settings, latitude, longitude); return { latitude, longitude, timeFormat: normalizeTimeFormat(settings?.timeFormat), birthDate: normalizeBirthDate(settings?.birthDate), tarotDeck: normalizeTarotDeck(settings?.tarotDeck), stellariumBackgroundEnabled: parseStoredBoolean(settings?.stellariumBackgroundEnabled, false) && hasExplicitLocation, hasExplicitLocation }; } function getResolvedTimeZone() { try { const timeZone = Intl.DateTimeFormat().resolvedOptions().timeZone; return String(timeZone || ""); } catch { return ""; } } function buildBirthDateParts(birthDate) { const normalized = normalizeBirthDate(birthDate); if (!normalized) { return null; } const [year, month, day] = normalized.split("-").map((value) => Number(value)); if (!year || !month || !day) { return null; } const localNoon = new Date(year, month - 1, day, 12, 0, 0, 0); const utcNoon = new Date(Date.UTC(year, month - 1, day, 12, 0, 0, 0)); return { year, month, day, isoDate: normalized, localNoonIso: localNoon.toISOString(), utcNoonIso: utcNoon.toISOString(), timezoneOffsetMinutesAtNoon: localNoon.getTimezoneOffset() }; } function buildNatalContext(settings) { const normalized = normalizeSettings(settings); const birthDateParts = buildBirthDateParts(normalized.birthDate); const timeZone = getResolvedTimeZone(); return { latitude: normalized.latitude, longitude: normalized.longitude, birthDate: normalized.birthDate || null, birthDateParts, timeZone: timeZone || "UTC", timezoneOffsetMinutesNow: new Date().getTimezoneOffset(), timezoneOffsetMinutesAtBirthDateNoon: birthDateParts?.timezoneOffsetMinutesAtNoon ?? null }; } function emitSettingsUpdated(settings) { const normalized = normalizeSettings(settings); const natalContext = buildNatalContext(normalized); document.dispatchEvent(new CustomEvent("settings:updated", { detail: { settings: normalized, natalContext } })); } function loadSavedSettings() { try { const raw = window.localStorage.getItem(SETTINGS_STORAGE_KEY); if (!raw) { return { ...config.defaultSettings }; } const parsed = JSON.parse(raw); return normalizeSettings(parsed); } catch { return { ...config.defaultSettings }; } } function saveSettings(settings) { try { const normalized = normalizeSettings(settings); window.localStorage.setItem(SETTINGS_STORAGE_KEY, JSON.stringify(normalized)); return true; } catch { return false; } } function syncTarotDeckInputOptions() { const { tarotDeckEl } = getElements(); if (!tarotDeckEl) { return; } const deckOptions = window.TarotCardImages?.getDeckOptions?.(); const previousValue = String(tarotDeckEl.value || "").trim().toLowerCase(); tarotDeckEl.innerHTML = ""; if (!Array.isArray(deckOptions) || !deckOptions.length) { const emptyOption = document.createElement("option"); emptyOption.value = String(config.defaultSettings?.tarotDeck || "ceremonial-magick").trim().toLowerCase(); emptyOption.textContent = "No deck manifests found"; tarotDeckEl.appendChild(emptyOption); tarotDeckEl.disabled = true; return; } tarotDeckEl.disabled = false; deckOptions.forEach((option) => { const id = String(option?.id || "").trim().toLowerCase(); if (!id) { return; } const label = String(option?.label || id); const optionEl = document.createElement("option"); optionEl.value = id; optionEl.textContent = label; tarotDeckEl.appendChild(optionEl); }); tarotDeckEl.value = normalizeTarotDeck(previousValue); } function applySettingsToInputs(settings) { const { latEl, lngEl, timeFormatEl, birthDateEl, tarotDeckEl, stellariumBackgroundEl } = getElements(); syncTarotDeckInputOptions(); syncConnectionInputs(); const normalized = normalizeSettings(settings); latEl.value = String(normalized.latitude); lngEl.value = String(normalized.longitude); timeFormatEl.value = normalized.timeFormat; birthDateEl.value = normalized.birthDate; if (tarotDeckEl) { tarotDeckEl.value = normalized.tarotDeck; } setLocationEntryState(normalized.hasExplicitLocation); if (stellariumBackgroundEl) { stellariumBackgroundEl.checked = normalized.stellariumBackgroundEnabled; } syncStellariumBackgroundAvailability(); if (window.TarotCardImages?.setActiveDeck) { window.TarotCardImages.setActiveDeck(normalized.tarotDeck); } syncDeckCacheStatus(window.TarotCardImages?.getDeckPreloadStatus?.()); applyExternalSettings(normalized); return normalized; } function getSettingsFromInputs() { const { latEl, lngEl, timeFormatEl, birthDateEl, tarotDeckEl, stellariumBackgroundEl } = getElements(); const latitudeText = String(latEl?.value || "").trim(); const longitudeText = String(lngEl?.value || "").trim(); if (!latitudeText || !longitudeText) { throw new Error("Latitude/Longitude must be entered before saving settings."); } const latitude = Number(latitudeText); const longitude = Number(longitudeText); if (Number.isNaN(latitude) || Number.isNaN(longitude)) { throw new Error("Latitude/Longitude must be valid numbers."); } return normalizeSettings({ latitude, longitude, timeFormat: normalizeTimeFormat(timeFormatEl.value), birthDate: normalizeBirthDate(birthDateEl.value), tarotDeck: normalizeTarotDeck(tarotDeckEl?.value), stellariumBackgroundEnabled: Boolean(stellariumBackgroundEl?.checked), hasExplicitLocation: hasExplicitLocationEntry() }); } function getConnectionSettingsFromInputs() { const { apiBaseUrlEl, apiKeyEl } = getElements(); return { apiBaseUrl: String(apiBaseUrlEl?.value || "").trim(), apiKey: String(apiKeyEl?.value || "").trim() }; } function openSettingsPopup() { const activeSection = typeof config.getActiveSection === "function" ? config.getActiveSection() : "home"; if (activeSection && activeSection !== "settings") { lastNonSettingsSection = activeSection; } applySettingsToInputs(loadSavedSettings()); config.setActiveSection?.("settings"); } function closeSettingsPopup() { config.setActiveSection?.(lastNonSettingsSection || "home"); } async function handleSaveSettings() { try { const settings = getSettingsFromInputs(); const previousConnectionSettings = getConnectionSettings(); const connectionSettings = getConnectionSettingsFromInputs(); const connectionChanged = hasConnectionChanged(previousConnectionSettings, connectionSettings); const connectionResult = window.TarotAppConfig?.updateConnectionSettings?.(connectionSettings) || { didPersist: true }; const normalized = applySettingsToInputs(settings); syncSky( { latitude: normalized.latitude, longitude: normalized.longitude }, { force: true, backgroundEnabled: normalized.stellariumBackgroundEnabled, hasExplicitLocation: normalized.hasExplicitLocation } ); const didPersist = saveSettings(normalized); emitSettingsUpdated(normalized); if (typeof config.getActiveSection === "function" && config.getActiveSection() !== "home" && config.getActiveSection() !== "settings") { config.onReopenActiveSection?.(config.getActiveSection()); } if (connectionChanged && typeof config.onConnectionSaved === "function") { await config.onConnectionSaved(connectionResult, connectionSettings); } else if (typeof config.onRenderWeek === "function") { await config.onRenderWeek(); } if (!didPersist || connectionResult.didPersist === false) { setStatus("Settings applied for this session. Browser storage is unavailable."); } else { setStatus("Settings saved."); } } catch (error) { setStatus(error?.message || "Unable to save settings."); } } function requestGeoLocation() { const { latEl, lngEl } = getElements(); if (!navigator.geolocation) { setStatus("Geolocation not available in this browser."); return; } setStatus("Getting your location..."); navigator.geolocation.getCurrentPosition( ({ coords }) => { latEl.value = coords.latitude.toFixed(4); lngEl.value = coords.longitude.toFixed(4); markLocationAsExplicit(); syncSky( { latitude: coords.latitude, longitude: coords.longitude }, { force: true, backgroundEnabled: Boolean(getElements().stellariumBackgroundEl?.checked), hasExplicitLocation: true } ); setStatus("Location set from browser. Save Settings to use it across the app."); }, (err) => { const detail = err?.message || `code ${err?.code ?? "unknown"}`; setStatus(`Could not get location (${detail}).`); }, { enableHighAccuracy: true, timeout: 10000 } ); } function bindInteractions() { const { saveSettingsEl, useLocationEl, openSettingsEl, closeSettingsEl, latEl, lngEl } = getElements(); if (saveSettingsEl) { saveSettingsEl.addEventListener("click", () => { void handleSaveSettings(); }); } if (useLocationEl) { useLocationEl.addEventListener("click", requestGeoLocation); } [latEl, lngEl].forEach((inputEl) => { if (!inputEl) { return; } inputEl.addEventListener("input", markLocationAsExplicit); inputEl.addEventListener("change", markLocationAsExplicit); }); if (openSettingsEl) { openSettingsEl.addEventListener("click", (event) => { event.stopPropagation(); if ((config.getActiveSection?.() || "home") !== "settings") { openSettingsPopup(); } else { closeSettingsPopup(); } }); } if (closeSettingsEl) { closeSettingsEl.addEventListener("click", closeSettingsPopup); } document.addEventListener("keydown", (event) => { if (event.key === "Escape" && (config.getActiveSection?.() || "home") === "settings") { closeSettingsPopup(); } }); document.addEventListener("connection:updated", () => { syncConnectionInputs(); syncTarotDeckInputOptions(); syncDeckCacheStatus(window.TarotCardImages?.getDeckPreloadStatus?.()); }); document.addEventListener("tarot:deck-cache-status", (event) => { syncDeckCacheStatus(event?.detail); }); } function init(nextConfig = {}) { config = { ...config, ...nextConfig, defaultSettings: { ...config.defaultSettings, ...(nextConfig.defaultSettings || {}) } }; bindInteractions(); } function loadInitialSettingsAndApply() { const initialSettings = loadSavedSettings(); const normalized = applySettingsToInputs(initialSettings); emitSettingsUpdated(normalized); return normalized; } window.TarotSettingsUi = { ...(window.TarotSettingsUi || {}), init, openSettingsPopup, closeSettingsPopup, loadInitialSettingsAndApply, buildNatalContext, normalizeSettings }; })();