add offline switch to in-app help
This commit is contained in:
@@ -381,6 +381,16 @@
|
|||||||
<span class="switch-track" aria-hidden="true"></span>
|
<span class="switch-track" aria-hidden="true"></span>
|
||||||
</label>
|
</label>
|
||||||
<h3 data-i18n="storage-help.offline-title">Ohne Internet unterwegs</h3>
|
<h3 data-i18n="storage-help.offline-title">Ohne Internet unterwegs</h3>
|
||||||
|
<label for="info-offline-cache-toggle" class="switch-row compact-switch">
|
||||||
|
<span>
|
||||||
|
<strong data-i18n="offline.open-app">App ohne Internet öffnen</strong>
|
||||||
|
<small data-i18n="offline.open-app-hint">Damit kannst du Wegwichtel auch ohne Internet öffnen. Wähle danach die Strecken aus, die du mitnehmen möchtest.</small>
|
||||||
|
</span>
|
||||||
|
<input id="info-offline-cache-toggle" type="checkbox" role="switch" aria-checked="false" aria-describedby="info-offline-cache-status">
|
||||||
|
<span class="switch-track" aria-hidden="true"></span>
|
||||||
|
</label>
|
||||||
|
<output id="info-offline-cache-status" aria-live="polite"></output>
|
||||||
|
<button id="info-offline-cache-refresh" type="button" hidden data-i18n="offline.renew-app">Gespeicherte App erneuern</button>
|
||||||
<p data-i18n="storage-help.offline-data">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.</p>
|
<p data-i18n="storage-help.offline-data">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.</p>
|
||||||
<p data-i18n="storage-help.export-data">Mit „Weg als Datei herunterladen“ kannst du den Weg in einer anderen Wander-App öffnen. Bilder und Tonaufnahmen sind in dieser Datei nicht enthalten.</p>
|
<p data-i18n="storage-help.export-data">Mit „Weg als Datei herunterladen“ kannst du den Weg in einer anderen Wander-App öffnen. Bilder und Tonaufnahmen sind in dieser Datei nicht enthalten.</p>
|
||||||
<h3 data-i18n="storage-help.drafts-title">Deine eigenen Strecken</h3>
|
<h3 data-i18n="storage-help.drafts-title">Deine eigenen Strecken</h3>
|
||||||
|
|||||||
+23
-16
@@ -2,9 +2,9 @@ globalThis.Wegwichtel ??= {};
|
|||||||
(function (ns) {
|
(function (ns) {
|
||||||
'use strict';
|
'use strict';
|
||||||
|
|
||||||
const toggle = document.getElementById('offline-cache-toggle');
|
const toggles = ['offline-cache-toggle', 'info-offline-cache-toggle'].map(id => document.getElementById(id)).filter(Boolean);
|
||||||
const status = document.getElementById('offline-cache-status');
|
const statuses = ['offline-cache-status', 'info-offline-cache-status'].map(id => document.getElementById(id)).filter(Boolean);
|
||||||
const refresh = document.getElementById('offline-cache-refresh');
|
const refreshButtons = ['offline-cache-refresh', 'info-offline-cache-refresh'].map(id => document.getElementById(id)).filter(Boolean);
|
||||||
const enter = document.getElementById('bootstrap-enter');
|
const enter = document.getElementById('bootstrap-enter');
|
||||||
let controller = null;
|
let controller = null;
|
||||||
let task = null;
|
let task = null;
|
||||||
@@ -15,16 +15,20 @@ globalThis.Wegwichtel ??= {};
|
|||||||
const supported = () => Boolean(globalThis.isSecureContext && navigator.serviceWorker && globalThis.caches);
|
const supported = () => Boolean(globalThis.isSecureContext && navigator.serviceWorker && globalThis.caches);
|
||||||
|
|
||||||
function announce(message) {
|
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() {
|
async function updateControls() {
|
||||||
const saved = supported() ? await ns.OfflineStore.state() : null;
|
const saved = supported() ? await ns.OfflineStore.state() : null;
|
||||||
if (toggle) {
|
setToggleState(Boolean(saved));
|
||||||
toggle.checked = Boolean(saved);
|
for (const refresh of refreshButtons) {
|
||||||
toggle.setAttribute('aria-checked', String(Boolean(saved)));
|
|
||||||
}
|
|
||||||
if (refresh) {
|
|
||||||
refresh.hidden = !saved;
|
refresh.hidden = !saved;
|
||||||
refresh.disabled = Boolean(task);
|
refresh.disabled = Boolean(task);
|
||||||
}
|
}
|
||||||
@@ -100,7 +104,7 @@ globalThis.Wegwichtel ??= {};
|
|||||||
}
|
}
|
||||||
})();
|
})();
|
||||||
if (enter) enter.disabled = true;
|
if (enter) enter.disabled = true;
|
||||||
if (refresh) refresh.disabled = true;
|
for (const refresh of refreshButtons) refresh.disabled = true;
|
||||||
return task;
|
return task;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -225,7 +229,7 @@ globalThis.Wegwichtel ??= {};
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (!supported()) {
|
if (!supported()) {
|
||||||
if (toggle) toggle.disabled = true;
|
for (const toggle of toggles) toggle.disabled = true;
|
||||||
announce(ns.I18n.t("offline.error.unsupported"));
|
announce(ns.I18n.t("offline.error.unsupported"));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -240,11 +244,14 @@ globalThis.Wegwichtel ??= {};
|
|||||||
} else {
|
} else {
|
||||||
announce(ns.I18n.t("offline.error.save-app-first"));
|
announce(ns.I18n.t("offline.error.save-app-first"));
|
||||||
}
|
}
|
||||||
toggle?.addEventListener('change', () => {
|
for (const toggle of toggles) {
|
||||||
toggle.setAttribute('aria-checked', String(toggle.checked));
|
toggle.addEventListener('change', () => {
|
||||||
void (toggle.checked ? download() : disable()).catch(() => {});
|
void (toggle.checked ? download() : disable()).catch(() => {});
|
||||||
});
|
});
|
||||||
refresh?.addEventListener('click', () => { void download().catch(() => {}); });
|
}
|
||||||
|
for (const refresh of refreshButtons) {
|
||||||
|
refresh.addEventListener('click', () => { void download().catch(() => {}); });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ns.OfflineCache = Object.freeze({
|
ns.OfflineCache = Object.freeze({
|
||||||
|
|||||||
@@ -196,7 +196,8 @@ test('offline worker serves stored audio ranges including seeks, rejects invalid
|
|||||||
async function managerHarness({ supported = true, saved = false } = {}) {
|
async function managerHarness({ supported = true, saved = false } = {}) {
|
||||||
const { context, store } = await loadStore(true);
|
const { context, store } = await loadStore(true);
|
||||||
const elements = new Map();
|
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();
|
const element = new EventTarget();
|
||||||
element.attributes = new Map();
|
element.attributes = new Map();
|
||||||
element.setAttribute = (name, value) => element.attributes.set(name, value);
|
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();
|
const { manager, store, elements, downloaded } = await managerHarness();
|
||||||
await manager.initialize();
|
await manager.initialize();
|
||||||
assert.equal(elements.get('offline-cache-toggle').checked, false);
|
assert.equal(elements.get('offline-cache-toggle').checked, false);
|
||||||
elements.get('offline-cache-toggle').checked = true;
|
assert.equal(elements.get('info-offline-cache-toggle').checked, false);
|
||||||
elements.get('offline-cache-toggle').dispatchEvent(new Event('change'));
|
elements.get('info-offline-cache-toggle').checked = true;
|
||||||
|
elements.get('info-offline-cache-toggle').dispatchEvent(new Event('change'));
|
||||||
const pending = manager.download();
|
const pending = manager.download();
|
||||||
assert.equal(manager.busy, true);
|
assert.equal(manager.busy, true);
|
||||||
assert.equal(elements.get('bootstrap-enter').disabled, 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(elements.get('bootstrap-enter').disabled, false);
|
||||||
assert.equal(downloaded.length, 3, 'duplicate resources must not be stored twice');
|
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('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.equal(elements.get('offline-cache-refresh').hidden, false);
|
||||||
assert.match(elements.get('offline-cache-status').textContent, /Wegwichtel ist für unterwegs bereit/);
|
assert.match(elements.get('offline-cache-status').textContent, /Wegwichtel ist für unterwegs bereit/);
|
||||||
assert.ok(await store.match('/image.png'));
|
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(await store.state(), null);
|
||||||
assert.equal(elements.get('offline-cache-toggle').checked, false);
|
assert.equal(elements.get('offline-cache-toggle').checked, false);
|
||||||
assert.equal(elements.get('offline-cache-refresh').hidden, true);
|
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 () => {
|
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 });
|
const unsupported = await managerHarness({ supported: false });
|
||||||
await unsupported.manager.initialize();
|
await unsupported.manager.initialize();
|
||||||
assert.equal(unsupported.elements.get('offline-cache-toggle').disabled, true);
|
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/);
|
assert.match(unsupported.elements.get('offline-cache-status').textContent, /nicht für unterwegs speichern/);
|
||||||
const setup = await managerHarness();
|
const setup = await managerHarness();
|
||||||
await setup.manager.initialize();
|
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 });
|
setup.context.fetch = async () => new Response('error', { status: 500 });
|
||||||
await assert.rejects(setup.manager.download(), /nicht für unterwegs geladen/);
|
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('offline-cache-toggle').checked, false);
|
||||||
|
assert.equal(setup.elements.get('info-offline-cache-toggle').checked, false);
|
||||||
});
|
});
|
||||||
|
|
||||||
for (const filesystem of [false, true]) {
|
for (const filesystem of [false, true]) {
|
||||||
|
|||||||
Reference in New Issue
Block a user