Files
Wegwichtel/public/js/privacy-storage.js
T
Florian Zumpe 0a9b19951a
Sonarqube Scanner / Build and analyze (push) Failing after 24s
.
2026-08-31 16:26:56 +02:00

304 lines
8.6 KiB
JavaScript

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