Files
TaroTime/app/ui-settings.js
T

635 lines
20 KiB
JavaScript

(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
};
})();