From 94793b530d0be253830607dc61c7e7c0e5eac0b2 Mon Sep 17 00:00:00 2001 From: Florian Zumpe Date: Sat, 3 Oct 2026 14:23:12 +0200 Subject: [PATCH] add offline switch to in-app help --- public/index.html | 10 ++++++++++ public/js/offline-cache.js | 39 ++++++++++++++++++++++---------------- test/offline-cache.test.js | 27 ++++++++++++++++++++++---- 3 files changed, 56 insertions(+), 20 deletions(-) diff --git a/public/index.html b/public/index.html index 64908fe..eb0e959 100644 --- a/public/index.html +++ b/public/index.html @@ -381,6 +381,16 @@

Ohne Internet unterwegs

+ + +

Mit „App ohne Internet öffnen“ speicherst du Wegwichtel auf diesem Gerät. Danach kannst du einzelne Strecken mit „Für unterwegs speichern“ mitnehmen. Warte vor dem Losgehen, bis alles gespeichert ist.

Mit „Weg als Datei herunterladen“ kannst du den Weg in einer anderen Wander-App öffnen. Bilder und Tonaufnahmen sind in dieser Datei nicht enthalten.

Deine eigenen Strecken

diff --git a/public/js/offline-cache.js b/public/js/offline-cache.js index fd39cda..64a2b35 100644 --- a/public/js/offline-cache.js +++ b/public/js/offline-cache.js @@ -2,9 +2,9 @@ globalThis.Wegwichtel ??= {}; (function (ns) { 'use strict'; - const toggle = document.getElementById('offline-cache-toggle'); - const status = document.getElementById('offline-cache-status'); - const refresh = document.getElementById('offline-cache-refresh'); + const toggles = ['offline-cache-toggle', 'info-offline-cache-toggle'].map(id => document.getElementById(id)).filter(Boolean); + const statuses = ['offline-cache-status', 'info-offline-cache-status'].map(id => document.getElementById(id)).filter(Boolean); + const refreshButtons = ['offline-cache-refresh', 'info-offline-cache-refresh'].map(id => document.getElementById(id)).filter(Boolean); const enter = document.getElementById('bootstrap-enter'); let controller = null; let task = null; @@ -15,16 +15,20 @@ globalThis.Wegwichtel ??= {}; const supported = () => Boolean(globalThis.isSecureContext && navigator.serviceWorker && globalThis.caches); function announce(message) { - if (status) status.textContent = message; + for (const status of statuses) status.textContent = message; + } + + function setToggleState(enabled) { + for (const toggle of toggles) { + toggle.checked = enabled; + toggle.setAttribute('aria-checked', String(enabled)); + } } async function updateControls() { const saved = supported() ? await ns.OfflineStore.state() : null; - if (toggle) { - toggle.checked = Boolean(saved); - toggle.setAttribute('aria-checked', String(Boolean(saved))); - } - if (refresh) { + setToggleState(Boolean(saved)); + for (const refresh of refreshButtons) { refresh.hidden = !saved; refresh.disabled = Boolean(task); } @@ -100,7 +104,7 @@ globalThis.Wegwichtel ??= {}; } })(); if (enter) enter.disabled = true; - if (refresh) refresh.disabled = true; + for (const refresh of refreshButtons) refresh.disabled = true; return task; } @@ -225,7 +229,7 @@ globalThis.Wegwichtel ??= {}; }); } if (!supported()) { - if (toggle) toggle.disabled = true; + for (const toggle of toggles) toggle.disabled = true; announce(ns.I18n.t("offline.error.unsupported")); return; } @@ -240,11 +244,14 @@ globalThis.Wegwichtel ??= {}; } else { announce(ns.I18n.t("offline.error.save-app-first")); } - toggle?.addEventListener('change', () => { - toggle.setAttribute('aria-checked', String(toggle.checked)); - void (toggle.checked ? download() : disable()).catch(() => {}); - }); - refresh?.addEventListener('click', () => { void download().catch(() => {}); }); + for (const toggle of toggles) { + toggle.addEventListener('change', () => { + void (toggle.checked ? download() : disable()).catch(() => {}); + }); + } + for (const refresh of refreshButtons) { + refresh.addEventListener('click', () => { void download().catch(() => {}); }); + } } ns.OfflineCache = Object.freeze({ diff --git a/test/offline-cache.test.js b/test/offline-cache.test.js index 3d77acf..ec002cc 100644 --- a/test/offline-cache.test.js +++ b/test/offline-cache.test.js @@ -196,7 +196,8 @@ test('offline worker serves stored audio ranges including seeks, rejects invalid async function managerHarness({ supported = true, saved = false } = {}) { const { context, store } = await loadStore(true); const elements = new Map(); - for (const id of ['offline-cache-toggle', 'offline-cache-status', 'offline-cache-refresh', 'bootstrap-enter']) { + for (const id of ['offline-cache-toggle', 'offline-cache-status', 'offline-cache-refresh', + 'info-offline-cache-toggle', 'info-offline-cache-status', 'info-offline-cache-refresh', 'bootstrap-enter']) { const element = new EventTarget(); element.attributes = new Map(); element.setAttribute = (name, value) => element.attributes.set(name, value); @@ -236,8 +237,9 @@ test('offline switch prepares all resources once, persists opt-in and clears onl const { manager, store, elements, downloaded } = await managerHarness(); await manager.initialize(); assert.equal(elements.get('offline-cache-toggle').checked, false); - elements.get('offline-cache-toggle').checked = true; - elements.get('offline-cache-toggle').dispatchEvent(new Event('change')); + assert.equal(elements.get('info-offline-cache-toggle').checked, false); + elements.get('info-offline-cache-toggle').checked = true; + elements.get('info-offline-cache-toggle').dispatchEvent(new Event('change')); const pending = manager.download(); assert.equal(manager.busy, true); assert.equal(elements.get('bootstrap-enter').disabled, true); @@ -246,13 +248,28 @@ test('offline switch prepares all resources once, persists opt-in and clears onl assert.equal(elements.get('bootstrap-enter').disabled, false); assert.equal(downloaded.length, 3, 'duplicate resources must not be stored twice'); assert.equal(elements.get('offline-cache-toggle').checked, true); + assert.equal(elements.get('info-offline-cache-toggle').checked, true); + assert.equal(elements.get('info-offline-cache-toggle').attributes.get('aria-checked'), 'true'); + assert.equal(elements.get('info-offline-cache-status').textContent, elements.get('offline-cache-status').textContent); + assert.equal(elements.get('info-offline-cache-refresh').hidden, false); assert.equal(elements.get('offline-cache-refresh').hidden, false); assert.match(elements.get('offline-cache-status').textContent, /Wegwichtel ist für unterwegs bereit/); assert.ok(await store.match('/image.png')); - await manager.disable(); + elements.get('info-offline-cache-refresh').dispatchEvent(new Event('click')); + const refreshTask = manager.download(); + assert.equal(elements.get('offline-cache-refresh').disabled, true); + assert.equal(elements.get('info-offline-cache-refresh').disabled, true); + await refreshTask; + assert.equal(downloaded.length, 6); + assert.equal(elements.get('info-offline-cache-refresh').disabled, false); + elements.get('offline-cache-toggle').checked = false; + elements.get('offline-cache-toggle').dispatchEvent(new Event('change')); + await new Promise(resolve => setImmediate(resolve)); assert.equal(await store.state(), null); assert.equal(elements.get('offline-cache-toggle').checked, false); assert.equal(elements.get('offline-cache-refresh').hidden, true); + assert.equal(elements.get('info-offline-cache-toggle').checked, false); + assert.equal(elements.get('info-offline-cache-refresh').hidden, true); }); test('failed refresh preserves the old snapshot and download can be cancelled with the switch', async () => { @@ -285,6 +302,7 @@ test('offline UI handles unsupported browsers and malformed manifests without en const unsupported = await managerHarness({ supported: false }); await unsupported.manager.initialize(); assert.equal(unsupported.elements.get('offline-cache-toggle').disabled, true); + assert.equal(unsupported.elements.get('info-offline-cache-toggle').disabled, true); assert.match(unsupported.elements.get('offline-cache-status').textContent, /nicht für unterwegs speichern/); const setup = await managerHarness(); await setup.manager.initialize(); @@ -294,6 +312,7 @@ test('offline UI handles unsupported browsers and malformed manifests without en setup.context.fetch = async () => new Response('error', { status: 500 }); await assert.rejects(setup.manager.download(), /nicht für unterwegs geladen/); assert.equal(setup.elements.get('offline-cache-toggle').checked, false); + assert.equal(setup.elements.get('info-offline-cache-toggle').checked, false); }); for (const filesystem of [false, true]) {