added privacy switches and documents, assets and local storage for already visited POIs
This commit is contained in:
+79
-7
@@ -9,13 +9,61 @@
|
||||
routeState: 'idle',
|
||||
activePage: null,
|
||||
routeProgressIndex: null,
|
||||
deviceHeading: null
|
||||
deviceHeading: null,
|
||||
viewed: new Set(),
|
||||
playedAudio: new Set(),
|
||||
activePoiId: null,
|
||||
persistTimer: null
|
||||
};
|
||||
|
||||
function escapeHtml(value) {
|
||||
return $('<div>').text(value ?? '').html();
|
||||
}
|
||||
|
||||
|
||||
function persistRouteProgress() {
|
||||
if (!state.route) return;
|
||||
if (state.persistTimer != null) {
|
||||
globalThis.clearTimeout(state.persistTimer);
|
||||
state.persistTimer = null;
|
||||
}
|
||||
|
||||
ns.PrivacyStorage.saveRouteState(
|
||||
state.route,
|
||||
state.routeState,
|
||||
state.routeProgressIndex,
|
||||
[...state.triggered],
|
||||
[...state.viewed],
|
||||
[...state.playedAudio]
|
||||
);
|
||||
}
|
||||
|
||||
function scheduleRouteProgressPersist() {
|
||||
if (!state.route || state.persistTimer != null) return;
|
||||
state.persistTimer = globalThis.setTimeout(persistRouteProgress, 1200);
|
||||
}
|
||||
|
||||
function restoreRouteProgress(route) {
|
||||
const saved = ns.PrivacyStorage.activateRoute(route);
|
||||
state.triggered = new Set(saved?.visitedPoiIds ?? []);
|
||||
state.viewed = new Set(saved?.viewedPoiIds ?? []);
|
||||
state.playedAudio = new Set(saved?.playedAudioPoiIds ?? []);
|
||||
state.routeProgressIndex = Number.isInteger(saved?.routeProgressIndex)
|
||||
? saved.routeProgressIndex
|
||||
: null;
|
||||
state.routeState = saved?.routeState === 'running' || saved?.routeState === 'paused'
|
||||
? 'paused'
|
||||
: 'idle';
|
||||
|
||||
if (state.routeState === 'paused') {
|
||||
$('#tracking-status')
|
||||
.prop('hidden', false)
|
||||
.text('Gespeicherter Streckenfortschritt gefunden. Die Route kann fortgesetzt werden.');
|
||||
} else {
|
||||
$('#tracking-status').prop('hidden', true).text('');
|
||||
}
|
||||
}
|
||||
|
||||
function activate(selector, handler) {
|
||||
$(document)
|
||||
.on('pointerup', selector, function (event) {
|
||||
@@ -185,8 +233,12 @@
|
||||
function endRoute({ announce = true, returnToRoutes = false } = {}) {
|
||||
const wasActive = state.routeState !== 'idle';
|
||||
state.routeState = 'idle';
|
||||
persistRouteProgress();
|
||||
state.routeProgressIndex = null;
|
||||
state.triggered.clear();
|
||||
state.viewed.clear();
|
||||
state.playedAudio.clear();
|
||||
state.activePoiId = null;
|
||||
stopRouteSensors();
|
||||
ns.AudioPlayer.unload();
|
||||
setActivePoi(null);
|
||||
@@ -210,6 +262,7 @@
|
||||
if (state.routeState !== 'running') return;
|
||||
|
||||
state.routeState = 'paused';
|
||||
persistRouteProgress();
|
||||
stopRouteSensors();
|
||||
ns.AudioPlayer.pause();
|
||||
$('#tracking-status')
|
||||
@@ -223,15 +276,14 @@
|
||||
if (state.routeState !== 'idle') endRoute({ announce: false });
|
||||
|
||||
state.route = route;
|
||||
state.triggered.clear();
|
||||
state.routeProgressIndex = null;
|
||||
state.activePoiId = null;
|
||||
restoreRouteProgress(route);
|
||||
setActivePoi(null);
|
||||
$('#route-title').text(route.name);
|
||||
$('#route-description').text(route.description || '');
|
||||
$('#route-distance').text(ns.Distance.format(route.distanceM));
|
||||
$('#route-elevation').text(`${Math.round(route.elevationGainM)} m`);
|
||||
$('#route-poi-count').text(route.pois.length);
|
||||
$('#tracking-status').prop('hidden', true).text('');
|
||||
updateRouteControls();
|
||||
|
||||
const list = $('#poi-list').empty();
|
||||
@@ -261,6 +313,9 @@
|
||||
}
|
||||
|
||||
function showPoi(poi) {
|
||||
state.activePoiId = Number(poi.id);
|
||||
state.viewed.add(state.activePoiId);
|
||||
persistRouteProgress();
|
||||
setActivePoi(poi.id);
|
||||
$('#poi-title').text(poi.title);
|
||||
$('#poi-description').text(poi.description || '');
|
||||
@@ -345,6 +400,7 @@
|
||||
}
|
||||
|
||||
function updateRouteProgress(position) {
|
||||
const previousProgressIndex = state.routeProgressIndex;
|
||||
const points = state.route?.points || [];
|
||||
if (!points.length) return null;
|
||||
|
||||
@@ -370,9 +426,11 @@
|
||||
|
||||
if (targetIndex === lastIndex && distance <= reachedRadius) {
|
||||
state.routeProgressIndex = lastIndex;
|
||||
if (state.routeProgressIndex !== previousProgressIndex) scheduleRouteProgressPersist();
|
||||
return { reached: true, target, distance: 0 };
|
||||
}
|
||||
|
||||
if (state.routeProgressIndex !== previousProgressIndex) scheduleRouteProgressPersist();
|
||||
return { reached: false, target, distance };
|
||||
}
|
||||
|
||||
@@ -464,6 +522,7 @@
|
||||
|
||||
if (nearest && nearest.distance <= nearest.poi.triggerRadiusM) {
|
||||
state.triggered.add(nearest.poi.id);
|
||||
persistRouteProgress();
|
||||
ns.Vibration.start(ns.Vibration.Patterns.ACTIVE_POI);
|
||||
showPoi(nearest.poi);
|
||||
updateNavigation(position);
|
||||
@@ -481,6 +540,7 @@
|
||||
|
||||
state.routeState = 'running';
|
||||
state.deviceHeading = null;
|
||||
persistRouteProgress();
|
||||
updateRouteControls();
|
||||
$('#tracking-status')
|
||||
.prop('hidden', false)
|
||||
@@ -505,9 +565,9 @@
|
||||
|
||||
function routesFromResponse(response) {
|
||||
if (!Array.isArray(response?.routes)) {
|
||||
throw new Error('Die Routen-API hat keine gültige Routenliste geliefert.');
|
||||
throw new TypeError('Die Routen-API hat keine gültige Routenliste geliefert.');
|
||||
}
|
||||
return response.routes.filter(route => route && route.id != null && route.name != null);
|
||||
return response.routes.filter(route => route?.id != null && route?.name != null);
|
||||
}
|
||||
|
||||
function updateRouteProximity(position) {
|
||||
@@ -584,6 +644,17 @@
|
||||
endRoute({ returnToRoutes: true });
|
||||
});
|
||||
|
||||
$('#poi-audio').on('play', function () {
|
||||
if (!state.route || state.activePoiId == null) return;
|
||||
state.playedAudio.add(state.activePoiId);
|
||||
persistRouteProgress();
|
||||
});
|
||||
|
||||
document.addEventListener('wegwichtel:progress-storage-changed', event => {
|
||||
if (event.detail?.enabled) persistRouteProgress();
|
||||
});
|
||||
|
||||
globalThis.addEventListener('pagehide', persistRouteProgress);
|
||||
globalThis.addEventListener('popstate', event => {
|
||||
const page = event.state?.page || '#routes-page';
|
||||
navigate(page, { updateHistory: false, instant: true });
|
||||
@@ -592,6 +663,7 @@
|
||||
globalThis.addEventListener('pageshow', restoreRouteLists);
|
||||
document.addEventListener('visibilitychange', () => {
|
||||
if (document.visibilityState === 'visible') restoreRouteLists();
|
||||
else persistRouteProgress();
|
||||
});
|
||||
}
|
||||
|
||||
@@ -612,7 +684,7 @@
|
||||
updateRouteControls();
|
||||
hideNavigation();
|
||||
|
||||
void initializeLocation(context);
|
||||
await initializeLocation(context);
|
||||
}
|
||||
};
|
||||
}(globalThis.Wegwichtel, globalThis.jQuery));
|
||||
|
||||
Vendored
+56
-6
@@ -1,11 +1,16 @@
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
const versions = Object.freeze({ app: '0.12.12', jquery: '4.0.0', jqueryUi: '1.14.2' });
|
||||
const versions = Object.freeze({ app: '0.12.14', jquery: '4.0.0', jqueryUi: '1.14.2' });
|
||||
const steps = ['jquery', 'jquery-ui', 'modules', 'server', 'routes', 'location'];
|
||||
const screen = document.getElementById('bootstrap-screen');
|
||||
const progress = document.getElementById('bootstrap-progress');
|
||||
const errorBox = document.getElementById('bootstrap-error');
|
||||
const retry = document.getElementById('bootstrap-retry');
|
||||
const enter = document.getElementById('bootstrap-enter');
|
||||
const readyText = document.getElementById('bootstrap-ready');
|
||||
const cleanupStatus = document.getElementById('storage-cleanup-status');
|
||||
let ready = false;
|
||||
|
||||
const versioned = path => `${path}${path.includes('?') ? '&' : '?'}v=${encodeURIComponent(versions.app)}`;
|
||||
|
||||
@@ -34,17 +39,61 @@
|
||||
function initializeJqueryUi() {
|
||||
const $ = globalThis.jQuery;
|
||||
$('#bootstrap-retry').button({ icon: 'ui-icon-refresh' });
|
||||
$('#bootstrap-enter').button();
|
||||
$('.back-button').button();
|
||||
$('#clear-route-search').button({ icon: 'ui-icon-close', showLabel: false });
|
||||
$('#slide-prev').button();
|
||||
$('#slide-next').button();
|
||||
}
|
||||
|
||||
function updateCleanupProgress(removed, total) {
|
||||
cleanupStatus.textContent = `Entferne alte Streckendaten aus dem Speicher (${removed} / ${total})`;
|
||||
cleanupStatus.hidden = false;
|
||||
}
|
||||
|
||||
async function cleanupOldProgress() {
|
||||
cleanupStatus.hidden = true;
|
||||
await globalThis.Wegwichtel.PrivacyStorage.cleanupExpired(updateCleanupProgress);
|
||||
}
|
||||
|
||||
function isActivation(event) {
|
||||
if (event.type === 'keydown') return event.key === 'Enter' || event.key === ' ';
|
||||
if (event.type !== 'pointerup') return false;
|
||||
if (event.isPrimary === false) return false;
|
||||
return event.pointerType !== 'mouse' || event.button === 0;
|
||||
}
|
||||
|
||||
function openApplication(event) {
|
||||
if (!ready || !isActivation(event)) return;
|
||||
if (event.target !== enter && event.target.closest('button, input, label, a, dialog')) return;
|
||||
|
||||
event.preventDefault();
|
||||
screen.hidden = true;
|
||||
document.getElementById('app-shell').hidden = false;
|
||||
globalThis.Wegwichtel.App.navigate('#routes-page', { replace: true });
|
||||
}
|
||||
|
||||
function showReadyState() {
|
||||
ready = true;
|
||||
screen.setAttribute('aria-busy', 'false');
|
||||
screen.classList.add('ready');
|
||||
readyText.hidden = false;
|
||||
enter.hidden = false;
|
||||
enter.focus();
|
||||
}
|
||||
|
||||
async function boot() {
|
||||
ready = false;
|
||||
screen.setAttribute('aria-busy', 'true');
|
||||
screen.classList.remove('ready');
|
||||
readyText.hidden = true;
|
||||
enter.hidden = true;
|
||||
errorBox.hidden = true;
|
||||
retry.hidden = true;
|
||||
|
||||
try {
|
||||
await cleanupOldProgress();
|
||||
|
||||
await loadScript(
|
||||
`vendor/jquery/jquery-${versions.jquery}.min.js`,
|
||||
() => globalThis.jQuery?.fn?.jquery === versions.jquery
|
||||
@@ -64,9 +113,7 @@
|
||||
mark('modules', 'done');
|
||||
|
||||
await globalThis.Wegwichtel.App.initialize({ markStep: mark });
|
||||
document.getElementById('bootstrap-screen').hidden = true;
|
||||
document.getElementById('app-shell').hidden = false;
|
||||
globalThis.Wegwichtel.App.navigate('#routes-page', { replace: true });
|
||||
showReadyState();
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
const current = steps.find(step => !document.querySelector(`[data-step="${step}"]`)?.classList.contains('done'));
|
||||
@@ -78,13 +125,16 @@
|
||||
}
|
||||
|
||||
function retryBoot(event) {
|
||||
if (event.type === 'keydown' && event.key !== 'Enter' && event.key !== ' ') return;
|
||||
if (event.type === 'pointerup' && event.pointerType === 'mouse' && event.button !== 0) return;
|
||||
if (!isActivation(event)) return;
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
location.reload();
|
||||
}
|
||||
|
||||
retry.addEventListener('pointerup', retryBoot);
|
||||
retry.addEventListener('keydown', retryBoot);
|
||||
enter.addEventListener('pointerup', openApplication);
|
||||
enter.addEventListener('keydown', openApplication);
|
||||
screen.addEventListener('pointerup', openApplication);
|
||||
boot();
|
||||
}());
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
(function (ns) {
|
||||
'use strict';
|
||||
|
||||
const dialog = document.getElementById('info-dialog');
|
||||
const title = document.getElementById('info-dialog-title');
|
||||
const menu = document.getElementById('info-menu');
|
||||
const backButton = document.getElementById('info-back');
|
||||
const closeButton = document.getElementById('info-close');
|
||||
const infoButton = document.getElementById('info-button');
|
||||
const pageTitles = Object.freeze({
|
||||
privacy: 'Datenschutzerklärung',
|
||||
imprint: 'Impressum',
|
||||
technology: 'Cookies und Technik'
|
||||
});
|
||||
|
||||
function isActivation(event) {
|
||||
if (event.type === 'keydown') return event.key === 'Enter' || event.key === ' ';
|
||||
if (event.type !== 'pointerup') return false;
|
||||
if (event.isPrimary === false) return false;
|
||||
return event.pointerType !== 'mouse' || event.button === 0;
|
||||
}
|
||||
|
||||
function activate(element, handler) {
|
||||
element.addEventListener('pointerup', event => {
|
||||
if (!isActivation(event)) return;
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
handler(event);
|
||||
});
|
||||
element.addEventListener('keydown', event => {
|
||||
if (!isActivation(event)) return;
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
handler(event);
|
||||
});
|
||||
}
|
||||
|
||||
function syncStorageToggles() {
|
||||
const enabled = ns.PrivacyStorage.isProgressStorageEnabled();
|
||||
document.querySelectorAll('.progress-storage-toggle').forEach(toggle => {
|
||||
toggle.checked = enabled;
|
||||
});
|
||||
document.querySelectorAll('.cookie-toggle').forEach(toggle => {
|
||||
toggle.checked = false;
|
||||
});
|
||||
}
|
||||
|
||||
function showMenu() {
|
||||
title.textContent = 'Informationen';
|
||||
menu.hidden = false;
|
||||
backButton.hidden = true;
|
||||
document.querySelectorAll('[data-info-page]').forEach(page => {
|
||||
page.hidden = true;
|
||||
});
|
||||
}
|
||||
|
||||
function showPage(name) {
|
||||
const page = document.querySelector(`[data-info-page="${name}"]`);
|
||||
if (!page) return;
|
||||
|
||||
title.textContent = pageTitles[name] ?? 'Informationen';
|
||||
menu.hidden = true;
|
||||
backButton.hidden = false;
|
||||
document.querySelectorAll('[data-info-page]').forEach(item => {
|
||||
item.hidden = item !== page;
|
||||
});
|
||||
const content = document.querySelector('.info-dialog-content');
|
||||
if (content) content.scrollTop = 0;
|
||||
}
|
||||
|
||||
function openDialog() {
|
||||
if (dialog.open) return;
|
||||
showMenu();
|
||||
syncStorageToggles();
|
||||
if (typeof dialog.showModal === 'function') dialog.showModal();
|
||||
else dialog.setAttribute('open', '');
|
||||
closeButton.focus();
|
||||
}
|
||||
|
||||
function closeDialog() {
|
||||
if (typeof dialog.close === 'function') dialog.close();
|
||||
else dialog.removeAttribute('open');
|
||||
infoButton.focus();
|
||||
}
|
||||
|
||||
function bindStorageToggles() {
|
||||
document.querySelectorAll('.progress-storage-toggle').forEach(toggle => {
|
||||
toggle.addEventListener('change', event => {
|
||||
const enabled = ns.PrivacyStorage.setProgressStorageEnabled(event.currentTarget.checked);
|
||||
syncStorageToggles();
|
||||
document.dispatchEvent(new CustomEvent('wegwichtel:progress-storage-changed', {
|
||||
detail: { enabled }
|
||||
}));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
activate(infoButton, openDialog);
|
||||
activate(closeButton, closeDialog);
|
||||
activate(backButton, showMenu);
|
||||
document.querySelectorAll('[data-info-target]').forEach(button => {
|
||||
activate(button, () => showPage(button.dataset.infoTarget));
|
||||
});
|
||||
|
||||
dialog.addEventListener('pointerup', event => {
|
||||
if (event.target !== dialog || !isActivation(event)) return;
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
closeDialog();
|
||||
});
|
||||
dialog.addEventListener('cancel', event => {
|
||||
event.preventDefault();
|
||||
closeDialog();
|
||||
});
|
||||
|
||||
bindStorageToggles();
|
||||
syncStorageToggles();
|
||||
|
||||
ns.LegalUi = Object.freeze({ open: openDialog, close: closeDialog, syncStorageToggles });
|
||||
}(globalThis.Wegwichtel ??= {}));
|
||||
@@ -0,0 +1,251 @@
|
||||
(function (ns) {
|
||||
'use strict';
|
||||
|
||||
const SETTINGS_KEY = 'wegwichtel.privacy.v1';
|
||||
const PROGRESS_KEY = 'wegwichtel.route-progress.v1';
|
||||
const COOKIE_PREFIX = 'wegwichtel_';
|
||||
const MAX_PROGRESS_AGE_MS = 24 * 60 * 60 * 1000;
|
||||
const STORE_VERSION = 1;
|
||||
|
||||
function storage() {
|
||||
try {
|
||||
return globalThis.localStorage ?? null;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function nowIso() {
|
||||
return new Date().toISOString();
|
||||
}
|
||||
|
||||
function readJson(key, fallback) {
|
||||
const backend = storage();
|
||||
if (!backend) return fallback;
|
||||
|
||||
try {
|
||||
const raw = backend.getItem(key);
|
||||
return raw ? JSON.parse(raw) : fallback;
|
||||
} catch {
|
||||
return fallback;
|
||||
}
|
||||
}
|
||||
|
||||
function writeJson(key, value) {
|
||||
const backend = storage();
|
||||
if (!backend) return false;
|
||||
|
||||
try {
|
||||
backend.setItem(key, JSON.stringify(value));
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function removeItem(key) {
|
||||
const backend = storage();
|
||||
if (!backend) return;
|
||||
|
||||
try {
|
||||
backend.removeItem(key);
|
||||
} catch {
|
||||
// Private browsing modes may reject storage access. The app remains usable without persistence.
|
||||
}
|
||||
}
|
||||
|
||||
function defaultSettings() {
|
||||
return { progressStorage: false, updatedAt: null };
|
||||
}
|
||||
|
||||
function settings() {
|
||||
const value = readJson(SETTINGS_KEY, defaultSettings());
|
||||
return {
|
||||
progressStorage: value?.progressStorage === true,
|
||||
updatedAt: typeof value?.updatedAt === 'string' ? value.updatedAt : null
|
||||
};
|
||||
}
|
||||
|
||||
function saveSettings(progressStorage) {
|
||||
return writeJson(SETTINGS_KEY, {
|
||||
progressStorage: progressStorage === true,
|
||||
updatedAt: nowIso()
|
||||
});
|
||||
}
|
||||
|
||||
function defaultProgressStore() {
|
||||
return { version: STORE_VERSION, currentRouteId: null, routes: {} };
|
||||
}
|
||||
|
||||
function progressStore() {
|
||||
const value = readJson(PROGRESS_KEY, defaultProgressStore());
|
||||
if (value?.version !== STORE_VERSION || typeof value?.routes !== 'object' || value.routes === null) {
|
||||
return defaultProgressStore();
|
||||
}
|
||||
|
||||
return {
|
||||
version: STORE_VERSION,
|
||||
currentRouteId: value.currentRouteId ?? null,
|
||||
routes: value.routes
|
||||
};
|
||||
}
|
||||
|
||||
function persistProgressStore(value) {
|
||||
if (!settings().progressStorage) return false;
|
||||
return writeJson(PROGRESS_KEY, value);
|
||||
}
|
||||
|
||||
function uniqueIds(values) {
|
||||
return [...new Set((Array.isArray(values) ? values : [])
|
||||
.map(value => Number(value))
|
||||
.filter(Number.isFinite))];
|
||||
}
|
||||
|
||||
function routeRecord(route, previous = {}) {
|
||||
const timestamp = nowIso();
|
||||
return {
|
||||
routeId: route.id,
|
||||
routeName: String(route.name ?? previous.routeName ?? ''),
|
||||
routeState: previous.routeState ?? 'idle',
|
||||
routeProgressIndex: Number.isInteger(previous.routeProgressIndex)
|
||||
? previous.routeProgressIndex
|
||||
: null,
|
||||
visitedPoiIds: uniqueIds(previous.visitedPoiIds),
|
||||
viewedPoiIds: uniqueIds(previous.viewedPoiIds),
|
||||
playedAudioPoiIds: uniqueIds(previous.playedAudioPoiIds),
|
||||
startedAt: previous.startedAt ?? timestamp,
|
||||
updatedAt: timestamp
|
||||
};
|
||||
}
|
||||
|
||||
function updateRoute(routeId, updater) {
|
||||
if (!settings().progressStorage || routeId == null) return null;
|
||||
|
||||
const store = progressStore();
|
||||
const key = String(routeId);
|
||||
const current = store.routes[key];
|
||||
if (!current) return null;
|
||||
|
||||
const updated = updater({ ...current });
|
||||
updated.updatedAt = nowIso();
|
||||
store.currentRouteId = routeId;
|
||||
store.routes[key] = updated;
|
||||
persistProgressStore(store);
|
||||
return updated;
|
||||
}
|
||||
|
||||
function markPoi(routeId, field, poiId) {
|
||||
const normalizedId = Number(poiId);
|
||||
if (!Number.isFinite(normalizedId)) return null;
|
||||
|
||||
return updateRoute(routeId, record => {
|
||||
record[field] = uniqueIds([...(record[field] ?? []), normalizedId]);
|
||||
return record;
|
||||
});
|
||||
}
|
||||
|
||||
function clearApplicationCookies() {
|
||||
const cookieText = globalThis.document?.cookie;
|
||||
if (!cookieText) return;
|
||||
|
||||
cookieText.split(';').forEach(item => {
|
||||
const name = item.split('=', 1)[0].trim();
|
||||
if (!name.startsWith(COOKIE_PREFIX)) return;
|
||||
globalThis.document.cookie = `${name}=; Max-Age=0; Path=/; SameSite=Lax`;
|
||||
});
|
||||
}
|
||||
|
||||
function setProgressStorageEnabled(enabled) {
|
||||
const active = enabled === true;
|
||||
const saved = saveSettings(active);
|
||||
if (!active) removeItem(PROGRESS_KEY);
|
||||
return saved && settings().progressStorage;
|
||||
}
|
||||
|
||||
function activateRoute(route) {
|
||||
if (!settings().progressStorage || route?.id == null) return null;
|
||||
|
||||
const store = progressStore();
|
||||
const key = String(route.id);
|
||||
const record = routeRecord(route, store.routes[key]);
|
||||
store.currentRouteId = route.id;
|
||||
store.routes[key] = record;
|
||||
persistProgressStore(store);
|
||||
return record;
|
||||
}
|
||||
|
||||
function saveRouteState(route, routeState, routeProgressIndex, visitedPoiIds, viewedPoiIds, playedAudioPoiIds) {
|
||||
if (route?.id == null) return null;
|
||||
|
||||
return updateRoute(route.id, record => ({
|
||||
...record,
|
||||
routeName: String(route.name ?? record.routeName ?? ''),
|
||||
routeState: String(routeState ?? 'idle'),
|
||||
routeProgressIndex: Number.isInteger(routeProgressIndex) ? routeProgressIndex : null,
|
||||
visitedPoiIds: uniqueIds(visitedPoiIds),
|
||||
viewedPoiIds: uniqueIds(viewedPoiIds),
|
||||
playedAudioPoiIds: uniqueIds(playedAudioPoiIds)
|
||||
}));
|
||||
}
|
||||
|
||||
function loadRoute(routeId) {
|
||||
if (!settings().progressStorage || routeId == null) return null;
|
||||
return progressStore().routes[String(routeId)] ?? null;
|
||||
}
|
||||
|
||||
function nextTurn() {
|
||||
return new Promise(resolve => globalThis.setTimeout(resolve, 0));
|
||||
}
|
||||
|
||||
async function cleanupExpired(onProgress = () => {}) {
|
||||
if (!settings().progressStorage) {
|
||||
removeItem(PROGRESS_KEY);
|
||||
return { removed: 0, total: 0 };
|
||||
}
|
||||
|
||||
const store = progressStore();
|
||||
const cutoff = Date.now() - MAX_PROGRESS_AGE_MS;
|
||||
const expiredKeys = Object.entries(store.routes)
|
||||
.filter(([, route]) => {
|
||||
const timestamp = Date.parse(route?.updatedAt ?? '');
|
||||
return !Number.isFinite(timestamp) || timestamp <= cutoff;
|
||||
})
|
||||
.map(([key]) => key);
|
||||
|
||||
if (!expiredKeys.length) return { removed: 0, total: 0 };
|
||||
|
||||
let removed = 0;
|
||||
onProgress(removed, expiredKeys.length);
|
||||
|
||||
for (const key of expiredKeys) {
|
||||
delete store.routes[key];
|
||||
if (String(store.currentRouteId) === key) store.currentRouteId = null;
|
||||
removed += 1;
|
||||
persistProgressStore(store);
|
||||
onProgress(removed, expiredKeys.length);
|
||||
await nextTurn();
|
||||
}
|
||||
|
||||
return { removed, total: expiredKeys.length };
|
||||
}
|
||||
|
||||
clearApplicationCookies();
|
||||
|
||||
ns.PrivacyStorage = Object.freeze({
|
||||
SETTINGS_KEY,
|
||||
PROGRESS_KEY,
|
||||
MAX_PROGRESS_AGE_MS,
|
||||
cookiesEnabled: () => false,
|
||||
clearApplicationCookies,
|
||||
isProgressStorageEnabled: () => settings().progressStorage,
|
||||
setProgressStorageEnabled,
|
||||
cleanupExpired,
|
||||
activateRoute,
|
||||
loadRoute,
|
||||
saveRouteState,
|
||||
markVisited: (routeId, poiId) => markPoi(routeId, 'visitedPoiIds', poiId),
|
||||
markViewed: (routeId, poiId) => markPoi(routeId, 'viewedPoiIds', poiId),
|
||||
markAudioPlayed: (routeId, poiId) => markPoi(routeId, 'playedAudioPoiIds', poiId),
|
||||
clearProgress: () => removeItem(PROGRESS_KEY)
|
||||
});
|
||||
}(globalThis.Wegwichtel ??= {}));
|
||||
Reference in New Issue
Block a user