diff --git a/public/index.html b/public/index.html index 38c007b..585b791 100644 --- a/public/index.html +++ b/public/index.html @@ -5,9 +5,9 @@ Wegwichtel - + - +
@@ -308,8 +308,8 @@
- - + + diff --git a/public/js/privacy-storage.js b/public/js/privacy-storage.js index 52c70d9..949199f 100644 --- a/public/js/privacy-storage.js +++ b/public/js/privacy-storage.js @@ -22,6 +22,7 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {}; function readJson(key, fallback) { const store = storage(); if (!store) return fallback; + try { const raw = store.getItem(key); if (!raw) return fallback; @@ -35,6 +36,7 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {}; function writeJson(key, value) { const store = storage(); if (!store) return false; + try { store.setItem(key, JSON.stringify(value)); return true; @@ -46,6 +48,7 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {}; function removeItem(key) { const store = storage(); if (!store) return false; + try { store.removeItem(key); return true; @@ -67,7 +70,8 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {}; } function writeSettings(settings) { - return writeJson(PRIVACY_KEY, normalizeSettings({ ...settings, updatedAt: now() })); + const value = normalizeSettings({ ...settings, updatedAt: now() }); + return writeJson(PRIVACY_KEY, value); } function isProgressStorageEnabled() { @@ -87,12 +91,8 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {}; function routeId(route) { const id = route?.id ?? route?.routeId ?? route; - return id === null || id === undefined ? null : String(id); - } - - function asNumberOrNull(value) { - const number = Number(value); - return Number.isFinite(number) ? number : null; + if (id === null || id === undefined || id === '') return null; + return String(id); } function numericIdSet(values) { @@ -100,10 +100,17 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {}; return [...new Set(values.map(Number).filter(Number.isFinite))]; } + function addNumericId(values, value) { + return numericIdSet([...values, value]); + } + + function progressEnvelope(value) { + const routes = value?.routes && typeof value.routes === 'object' ? value.routes : value; + return routes && typeof routes === 'object' ? routes : {}; + } + function readProgressStore() { - const data = readJson(PROGRESS_KEY, {}); - const entries = data?.routes && typeof data.routes === 'object' ? data.routes : data; - return entries && typeof entries === 'object' ? entries : {}; + return progressEnvelope(readJson(PROGRESS_KEY, {})); } function writeProgressStore(routes) { @@ -114,10 +121,12 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {}; }); } - function normalizeSavedRouteState(value) { + function normalizeRouteState(value) { if (!value || typeof value !== 'object') return null; + return { routeId: value.routeId === null || value.routeId === undefined ? null : String(value.routeId), + 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), @@ -128,17 +137,81 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {}; }; } + 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]); + return normalized ?? freshEmptyRouteState(id, route); + } + function isExpired(value, referenceTime = now()) { - const saved = Number(value?.updatedAt) || Number(value?.savedAt) || 0; - return saved <= 0 || referenceTime - saved > MAX_PROGRESS_AGE_MS; + const timestamp = Number(value?.updatedAt) || Number(value?.savedAt) || 0; + return timestamp <= 0 || 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); + 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 markPlayedAudio(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 routes = readProgressStore(); + const timestamp = now(); + const routes = readProgressStore(); routes[id] = { routeId: id, routeName: route?.name ?? '', @@ -153,16 +226,7 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {}; return writeProgressStore(routes); } - function activateRoute(route) { - if (!isProgressStorageEnabled()) return null; - const id = routeId(route); - if (!id) return null; - const routes = cleanupExpiredEntries().routes; - const saved = normalizeSavedRouteState(routes[id]); - return saved && !isExpired(saved) ? saved : null; - } - - function cleanupExpiredEntries(progressCallback) { + function cleanupExpired(progressCallback) { const referenceTime = now(); const routes = readProgressStore(); const entries = Object.entries(routes); @@ -170,7 +234,7 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {}; let removed = 0; entries.forEach(([id, value], index) => { - const normalized = normalizeSavedRouteState(value); + const normalized = normalizeRouteState(value); if (normalized && !isExpired(normalized, referenceTime)) kept[id] = normalized; else removed += 1; reportCleanupProgress(progressCallback, index + 1, entries.length, removed); @@ -180,13 +244,19 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {}; return { removed, total: entries.length, routes: kept }; } - function reportCleanupProgress(callback, processed, total, removed) { - if (typeof callback !== 'function') return; - callback({ processed, total, removed }); + async function cleanupExpiredProgress(progressCallback) { + return cleanupExpired(progressCallback); } - async function cleanupExpiredProgress(progressCallback) { - return cleanupExpiredEntries(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]); + return saved && !isExpired(saved) ? saved : null; } ns.PrivacyStorage = Object.freeze({ @@ -195,10 +265,15 @@ globalThis.Wegwichtel = globalThis.Wegwichtel || {}; MAX_PROGRESS_AGE_MS, activateRoute, cleanExpiredProgress: cleanupExpiredProgress, + cleanupExpired, cleanupExpiredProgress, - cookiesEnabled: () => false, + cookiesEnabled, getPrivacySettings: readSettings, isProgressStorageEnabled, + markPlayed: markPlayedAudio, + markPlayedAudio, + markVisited, + markViewed, removeExpiredProgress: cleanupExpiredProgress, removeExpiredRouteProgress: cleanupExpiredProgress, saveRouteState,