globalThis.Wegwichtel = globalThis.Wegwichtel || {}; (function (ns) { 'use strict'; const PRIVACY_KEY = 'wegwichtel.privacy.v1'; const PROGRESS_KEY = 'wegwichtel.route-progress.v1'; const MAX_PROGRESS_AGE_MS = 24 * 60 * 60 * 1000; function storage() { try { return globalThis.localStorage ?? null; } catch { return null; } } function now() { return Date.now(); } function readJson(key, fallback) { const store = storage(); if (!store) return fallback; try { const raw = store.getItem(key); if (!raw) return fallback; const value = JSON.parse(raw); return value && typeof value === 'object' ? value : fallback; } catch { return fallback; } } function writeJson(key, value) { const store = storage(); if (!store) return false; try { store.setItem(key, JSON.stringify(value)); return true; } catch { return false; } } function removeItem(key) { const store = storage(); if (!store) return false; try { store.removeItem(key); return true; } catch { return false; } } function normalizeSettings(value) { return { progressStorage: value?.progressStorage === true, cookies: false, updatedAt: Number(value?.updatedAt) || now() }; } function readSettings() { return normalizeSettings(readJson(PRIVACY_KEY, {})); } function writeSettings(settings) { const value = normalizeSettings({ ...settings, updatedAt: now() }); return writeJson(PRIVACY_KEY, value); } function isProgressStorageEnabled() { return readSettings().progressStorage === true; } function setProgressStorageEnabled(enabled) { const nextValue = Boolean(enabled); writeSettings({ progressStorage: nextValue, cookies: false }); if (!nextValue) removeItem(PROGRESS_KEY); return nextValue; } function routeId(route) { const id = route?.id ?? route?.routeId ?? route; if (id === null || id === undefined || id === '') return null; return String(id); } function numericIdSet(values) { if (!Array.isArray(values)) return []; return [...new Set(values.map(Number).filter(Number.isFinite))]; } function addNumericId(values, value) { return numericIdSet([...values, value]); } function isRouteStateObject(value) { return value && typeof value === 'object' && ( Object.hasOwn(value, 'routeId') || Object.hasOwn(value, 'routeState') || Object.hasOwn(value, 'routeProgressIndex') || Object.hasOwn(value, 'visitedPoiIds') || Object.hasOwn(value, 'viewedPoiIds') || Object.hasOwn(value, 'playedAudioPoiIds') ); } function progressEnvelope(value) { if (!value || typeof value !== 'object') return {}; if (value.routes && typeof value.routes === 'object') return value.routes; if (isRouteStateObject(value)) { const id = routeId(value); return id ? { [id]: value } : {}; } return value; } function readProgressStore() { return progressEnvelope(readJson(PROGRESS_KEY, {})); } function writeProgressStore(routes) { return writeJson(PROGRESS_KEY, { version: 1, updatedAt: now(), routes }); } function stateTimestamp(value) { return Number(value?.updatedAt) || Number(value?.savedAt) || now(); } function normalizeRouteState(value, fallbackRouteId = null) { if (!value || typeof value !== 'object') return null; const id = value.routeId === null || value.routeId === undefined ? fallbackRouteId : value.routeId; return { routeId: id === null || id === undefined ? null : String(id), routeName: String(value.routeName ?? ''), routeState: value.routeState === 'running' || value.routeState === 'paused' ? value.routeState : 'idle', routeProgressIndex: Number.isInteger(value.routeProgressIndex) ? value.routeProgressIndex : null, visitedPoiIds: numericIdSet(value.visitedPoiIds), viewedPoiIds: numericIdSet(value.viewedPoiIds), playedAudioPoiIds: numericIdSet(value.playedAudioPoiIds), savedAt: Number(value.savedAt) || Number(value.updatedAt) || now(), updatedAt: stateTimestamp(value) }; } function freshEmptyRouteState(id, route = null) { const timestamp = now(); return { routeId: id, routeName: route?.name ?? '', routeState: 'idle', routeProgressIndex: null, visitedPoiIds: [], viewedPoiIds: [], playedAudioPoiIds: [], savedAt: timestamp, updatedAt: timestamp }; } function currentRouteState(routes, id, route = null) { const normalized = normalizeRouteState(routes[id], id); return normalized ?? freshEmptyRouteState(id, route); } function isExpired(value, referenceTime = now()) { const timestamp = Number(value?.updatedAt) || Number(value?.savedAt) || now(); return referenceTime - timestamp > MAX_PROGRESS_AGE_MS; } function reportCleanupProgress(callback, processed, total, removed) { if (typeof callback !== 'function') return; callback({ processed, total, removed }); } function updateRouteState(route, updater) { if (!isProgressStorageEnabled()) return false; const id = routeId(route); if (!id || typeof updater !== 'function') return false; const routes = readProgressStore(); const entry = currentRouteState(routes, id, typeof route === 'object' ? route : null); const updated = updater(entry) ?? entry; updated.routeId = id; updated.updatedAt = now(); if (!updated.savedAt) updated.savedAt = updated.updatedAt; routes[id] = normalizeRouteState(updated, id); return writeProgressStore(routes); } function markVisited(route, poiId) { return updateRouteState(route, entry => ({ ...entry, visitedPoiIds: addNumericId(entry.visitedPoiIds, poiId) })); } function markViewed(route, poiId) { return updateRouteState(route, entry => ({ ...entry, viewedPoiIds: addNumericId(entry.viewedPoiIds, poiId) })); } function markAudioPlayed(route, poiId) { return updateRouteState(route, entry => ({ ...entry, playedAudioPoiIds: addNumericId(entry.playedAudioPoiIds, poiId) })); } function saveRouteState(route, routeState, routeProgressIndex, visitedPoiIds = [], viewedPoiIds = [], playedAudioPoiIds = []) { if (!isProgressStorageEnabled()) return false; const id = routeId(route); if (!id) return false; const timestamp = now(); const routes = readProgressStore(); routes[id] = { routeId: id, routeName: route?.name ?? '', routeState: routeState === 'running' || routeState === 'paused' ? routeState : 'idle', routeProgressIndex: Number.isInteger(routeProgressIndex) ? routeProgressIndex : null, visitedPoiIds: numericIdSet(visitedPoiIds), viewedPoiIds: numericIdSet(viewedPoiIds), playedAudioPoiIds: numericIdSet(playedAudioPoiIds), savedAt: timestamp, updatedAt: timestamp }; return writeProgressStore(routes); } function cleanupExpired(progressCallback) { const referenceTime = now(); const routes = readProgressStore(); const entries = Object.entries(routes); const kept = {}; let removed = 0; entries.forEach(([id, value], index) => { const normalized = normalizeRouteState(value, id); if (normalized && !isExpired(normalized, referenceTime)) kept[id] = normalized; else removed += 1; reportCleanupProgress(progressCallback, index + 1, entries.length, removed); }); if (removed > 0) writeProgressStore(kept); return { removed, total: entries.length, routes: kept }; } async function cleanupExpiredProgress(progressCallback) { return cleanupExpired(progressCallback); } function activateRoute(route) { if (!isProgressStorageEnabled()) return null; const id = routeId(route); if (!id) return null; const routes = cleanupExpired().routes; const saved = normalizeRouteState(routes[id], id); return saved && !isExpired(saved) ? saved : null; } ns.PrivacyStorage = Object.freeze({ PRIVACY_KEY, PROGRESS_KEY, MAX_PROGRESS_AGE_MS, activateRoute, cleanExpiredProgress: cleanupExpiredProgress, cleanupExpired, cleanupExpiredProgress, cookiesEnabled: () => false, getPrivacySettings: readSettings, isProgressStorageEnabled, markAudioPlayed, markPlayed: markAudioPlayed, markPlayedAudio: markAudioPlayed, markVisited, markViewed, removeExpiredProgress: cleanupExpiredProgress, removeExpiredRouteProgress: cleanupExpiredProgress, saveRouteState, setProgressStorageEnabled }); }(globalThis.Wegwichtel));