diff --git a/.coveragerc b/.coveragerc deleted file mode 100644 index 3b3d1dd..0000000 --- a/.coveragerc +++ /dev/null @@ -1,11 +0,0 @@ -[run] -branch = True -relative_files = True -source = tools/python - -[report] -show_missing = True -skip_empty = True - -[xml] -output = coverage/python/coverage.xml diff --git a/package-lock.json b/package-lock.json index 6396f2f..5d291d1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "wegwichtel-next", - "version": "0.12.10", + "version": "0.12.11", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "wegwichtel-next", - "version": "0.12.10", + "version": "0.12.11", "hasInstallScript": true, "dependencies": { "@file-type/av": "0.2.0", diff --git a/package.json b/package.json index 7eebc74..4aec595 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "wegwichtel-next", - "version": "0.12.10", + "version": "0.12.11", "private": true, "description": "GPS-gestützte Lern- und Wanderwege mit GPX, POIs, Bildern und Audioansagen.", "type": "module", diff --git a/public/css/app.css b/public/css/app.css index f869a0c..130a77f 100644 --- a/public/css/app.css +++ b/public/css/app.css @@ -327,7 +327,7 @@ button, margin: 0 0 1rem; } -#route-page fieldset button { +#route-page fieldset button.ui-button { display: grid; place-items: center; min-width: 0; @@ -341,18 +341,18 @@ button, touch-action: manipulation; } -#route-page fieldset button:focus-visible { +#route-page fieldset button.ui-button:focus-visible { outline: .2rem solid var(--focus); outline-offset: .15rem; } -#route-page fieldset button:disabled { +#route-page fieldset button.ui-button:disabled { cursor: default; filter: grayscale(.65); opacity: .42; } -#route-page fieldset img { +#route-page fieldset button.ui-button img { width: 2rem; height: 2rem; pointer-events: none; @@ -397,17 +397,18 @@ button, margin: 0; } -#slideshow img { +#slide-image { display: block; width: 100%; + height: min(55vh, 28rem); height: min(55dvh, 28rem); object-fit: contain; background: #111; } #slideshow > fieldset { - display: flex; - flex-flow: row nowrap; + display: grid; + grid-template-columns: var(--icon-button-size) minmax(0, 1fr) var(--icon-button-size); align-items: center; gap: .5rem; width: 100%; @@ -419,8 +420,6 @@ button, #slideshow > fieldset > button.ui-button { display: grid; - flex: 0 0 var(--icon-button-size); - align-self: center; place-items: center; width: var(--icon-button-size); height: var(--icon-button-size); @@ -438,7 +437,6 @@ button, #slideshow > fieldset > div { display: flex; - flex: 1 1 0; align-self: stretch; min-width: 0; min-height: var(--icon-button-size); @@ -466,10 +464,16 @@ button, } #poi-audio { + display: block; width: 100%; + max-width: 100%; margin-top: 1rem; } +#poi-audio[hidden] { + display: none; +} + #navigation-footer { position: fixed; right: 0; diff --git a/public/index.html b/public/index.html index 0b1d64d..096ed2c 100644 --- a/public/index.html +++ b/public/index.html @@ -5,7 +5,7 @@ Wegwichtel - +
@@ -84,13 +84,13 @@
Routensteuerung
@@ -117,20 +117,20 @@
Bildnavigation

0 / 0

- + @@ -145,6 +145,6 @@ - + diff --git a/public/js/audio-player.js b/public/js/audio-player.js index 2fd03a7..9f69af7 100644 --- a/public/js/audio-player.js +++ b/public/js/audio-player.js @@ -5,12 +5,18 @@ return $('#poi-audio')[0]; } + function mediaUrl(url) { + if (!url) return ''; + const separator = url.includes('?') ? '&' : '?'; + return `${url}${separator}v=${Date.now()}`; + } + function unload() { const audio = element(); audio.pause(); audio.removeAttribute('src'); audio.load(); - $(audio).prop('hidden', true); + $(audio).attr('hidden', '').hide(); } ns.AudioPlayer = { @@ -19,8 +25,8 @@ if (!url) return; const audio = element(); - audio.src = url; - $(audio).prop('hidden', false); + audio.src = mediaUrl(url); + $(audio).removeAttr('hidden').css('display', 'block'); audio.load(); }, diff --git a/public/js/bootstrap-loader.js b/public/js/bootstrap-loader.js index b948167..600885c 100644 --- a/public/js/bootstrap-loader.js +++ b/public/js/bootstrap-loader.js @@ -1,12 +1,14 @@ (function () { 'use strict'; - const versions = Object.freeze({ jquery: '4.0.0', jqueryUi: '1.14.2' }); + const versions = Object.freeze({ app: '0.12.11', jquery: '4.0.0', jqueryUi: '1.14.2' }); const steps = ['jquery', 'jquery-ui', 'modules', 'server', 'routes', 'location']; const progress = document.getElementById('bootstrap-progress'); const errorBox = document.getElementById('bootstrap-error'); const retry = document.getElementById('bootstrap-retry'); + const versioned = path => `${path}${path.includes('?') ? '&' : '?'}v=${encodeURIComponent(versions.app)}`; + function mark(step, state) { const item = document.querySelector(`[data-step="${step}"]`); if (!item) return; @@ -18,7 +20,7 @@ function loadScript(src, test) { return new Promise((resolve, reject) => { const script = document.createElement('script'); - script.src = src; + script.src = versioned(src); script.async = false; script.onload = () => { if (test && !test()) reject(new Error(`Selbsttest fehlgeschlagen: ${src}`)); @@ -33,7 +35,7 @@ return new Promise((resolve, reject) => { const link = document.createElement('link'); link.rel = 'stylesheet'; - link.href = href; + link.href = versioned(href); link.onload = resolve; link.onerror = () => reject(new Error(`Stylesheet konnte nicht geladen werden: ${href}`)); document.head.appendChild(link); diff --git a/public/js/slideshow.js b/public/js/slideshow.js index 035b7a0..588ae68 100644 --- a/public/js/slideshow.js +++ b/public/js/slideshow.js @@ -5,6 +5,12 @@ let index = 0; let swipe = null; + function mediaUrl(url) { + if (!url) return ''; + const separator = url.includes('?') ? '&' : '?'; + return `${url}${separator}v=${Date.now()}`; + } + function activate(selector, handler) { $(document) .on('pointerup', selector, function (event) { @@ -33,8 +39,12 @@ const image = images[index]; $('#slide-image') - .attr('src', image.url) - .attr('alt', image.caption || 'Bild zur Station'); + .attr('src', mediaUrl(image.url)) + .attr('alt', image.caption || 'Bild zur Station') + .off('error.wegwichtel') + .on('error.wegwichtel', function () { + $(this).removeAttr('src').attr('alt', 'Bild konnte nicht geladen werden'); + }); $('#slide-caption').text(image.caption || ''); $('#slide-position').text(`${index + 1} / ${images.length}`); refreshButton('#slide-prev', images.length < 2); diff --git a/requirements-dev.txt b/requirements-dev.txt deleted file mode 100644 index 18f2c70..0000000 --- a/requirements-dev.txt +++ /dev/null @@ -1 +0,0 @@ -coverage==7.13.3 diff --git a/server.js b/server.js index bafc6bd..b1bcab4 100644 --- a/server.js +++ b/server.js @@ -18,7 +18,19 @@ app.use(express.json({ })); app.use(express.urlencoded({ extended: true, limit: '2mb' })); app.use('/api', createApiRouter(db)); -app.use(express.static(path.join(__dirname, 'public'), { extensions: ['html'] })); +app.use(express.static(path.join(__dirname, 'public'), { + extensions: ['html'], + etag: true, + lastModified: true, + maxAge: 0, + setHeaders(res, filePath) { + if (filePath.endsWith('.html')) { + res.setHeader('Cache-Control', 'no-cache, no-store, must-revalidate'); + return; + } + res.setHeader('Cache-Control', 'public, max-age=0, must-revalidate'); + } +})); app.use(notFoundHandler); app.use(errorHandler); diff --git a/src/routes/api.js b/src/routes/api.js index ee1bbcf..0b47a29 100644 --- a/src/routes/api.js +++ b/src/routes/api.js @@ -20,11 +20,12 @@ function sendFileResource(res, file, contentType = null) { if (contentType) res.type(contentType); res.setHeader('Content-Disposition', `inline; filename*=UTF-8''${encodeURIComponent(file.filename)}`); res.setHeader('X-Content-Type-Options', 'nosniff'); + res.setHeader('Cache-Control', 'no-cache, max-age=0, must-revalidate'); return res.sendFile(file.absolutePath, { acceptRanges: true, cacheControl: true, immutable: false, - maxAge: '1h' + maxAge: 0 }); } diff --git a/test/mobile-navigation-ui.test.js b/test/mobile-navigation-ui.test.js index bbf82ec..9a89801 100644 --- a/test/mobile-navigation-ui.test.js +++ b/test/mobile-navigation-ui.test.js @@ -168,7 +168,9 @@ test('reaching a POI preloads audio without autoplay and uses the vibration wrap assert.doesNotMatch(app, /navigator\.vibrate/); assert.match(app, /function stopRouteSensors\(\)[\s\S]*ns\.Vibration\.stop\(\)/); assert.match(audio, /audio\.load\(\)/); - assert.match(html, /