diff --git a/assets/branding/apple-touch-icon.png b/assets/branding/apple-touch-icon.png new file mode 100644 index 0000000..8ce5636 Binary files /dev/null and b/assets/branding/apple-touch-icon.png differ diff --git a/assets/branding/favicon-16.png b/assets/branding/favicon-16.png new file mode 100644 index 0000000..068d34d Binary files /dev/null and b/assets/branding/favicon-16.png differ diff --git a/assets/branding/favicon-32.png b/assets/branding/favicon-32.png new file mode 100644 index 0000000..6ddb1ab Binary files /dev/null and b/assets/branding/favicon-32.png differ diff --git a/assets/branding/icon-192.png b/assets/branding/icon-192.png new file mode 100644 index 0000000..8404a8c Binary files /dev/null and b/assets/branding/icon-192.png differ diff --git a/assets/branding/icon-512.png b/assets/branding/icon-512.png new file mode 100644 index 0000000..0223822 Binary files /dev/null and b/assets/branding/icon-512.png differ diff --git a/assets/css/app.css b/assets/css/app.css index 1bb9a87..bdcba0b 100644 --- a/assets/css/app.css +++ b/assets/css/app.css @@ -128,6 +128,34 @@ body { color: var(--text); } +.pull-refresh-indicator { + position: fixed; + top: max(0.85rem, env(safe-area-inset-top)); + left: 50%; + z-index: 30; + padding: 0.72rem 1rem; + border-radius: 999px; + opacity: 0; + pointer-events: none; + transform: translate(-50%, -0.9rem) scale(0.96); + transition: opacity 160ms ease, transform 160ms ease; + color: var(--muted); + font-size: 0.92rem; + letter-spacing: 0.01em; +} + +body.is-pull-refreshing .pull-refresh-indicator, +body.is-pull-refresh-ready .pull-refresh-indicator, +body.is-pull-refresh-reloading .pull-refresh-indicator { + opacity: 1; + transform: translate(-50%, 0) scale(1); +} + +body.is-pull-refresh-ready .pull-refresh-indicator, +body.is-pull-refresh-reloading .pull-refresh-indicator { + color: var(--text); +} + a { color: inherit; text-decoration: none; diff --git a/assets/js/app.js b/assets/js/app.js index 5258972..e50a8ec 100644 --- a/assets/js/app.js +++ b/assets/js/app.js @@ -992,6 +992,125 @@ return window.matchMedia("(display-mode: standalone)").matches || window.navigator.standalone === true; } + function isAppleTouchDevice() { + return /iPhone|iPad|iPod/i.test(window.navigator.userAgent) + || (window.navigator.platform === "MacIntel" && window.navigator.maxTouchPoints > 1); + } + + function initPullToRefresh() { + if (!isStandaloneMode() || !isAppleTouchDevice()) { + return; + } + + const indicator = document.querySelector("[data-pull-refresh-indicator]"); + const body = document.body; + const threshold = 96; + let isTracking = false; + let isReady = false; + let startY = 0; + + const setIndicator = message => { + if (indicator) { + indicator.textContent = message; + } + }; + + const resetState = () => { + body.classList.remove("is-pull-refreshing", "is-pull-refresh-ready"); + isTracking = false; + isReady = false; + startY = 0; + setIndicator("Zum Aktualisieren ziehen"); + }; + + const scrollTop = () => Math.max( + window.scrollY || 0, + document.documentElement.scrollTop || 0, + document.body.scrollTop || 0 + ); + + const canStart = target => { + if (scrollTop() > 0) { + return false; + } + + if (!(target instanceof Element)) { + return true; + } + + return !target.closest("input, textarea, select, button"); + }; + + window.addEventListener("touchstart", event => { + if (event.touches.length !== 1 || !canStart(event.target)) { + resetState(); + return; + } + + isTracking = true; + startY = event.touches[0].clientY; + }, { passive: true }); + + window.addEventListener("touchmove", event => { + if (!isTracking || event.touches.length !== 1) { + return; + } + + if (scrollTop() > 0) { + resetState(); + return; + } + + const delta = event.touches[0].clientY - startY; + + if (delta <= 0) { + body.classList.remove("is-pull-refreshing", "is-pull-refresh-ready"); + isReady = false; + setIndicator("Zum Aktualisieren ziehen"); + return; + } + + if (delta > 18) { + body.classList.add("is-pull-refreshing"); + event.preventDefault(); + } + + if (delta >= threshold) { + if (!isReady) { + body.classList.add("is-pull-refresh-ready"); + setIndicator("Loslassen zum Aktualisieren"); + isReady = true; + } + return; + } + + if (isReady) { + body.classList.remove("is-pull-refresh-ready"); + isReady = false; + } + + setIndicator("Zum Aktualisieren ziehen"); + }, { passive: false }); + + window.addEventListener("touchend", () => { + if (!isTracking) { + return; + } + + if (isReady) { + body.classList.remove("is-pull-refreshing", "is-pull-refresh-ready"); + body.classList.add("is-pull-refresh-reloading"); + setIndicator("Wird aktualisiert ..."); + window.location.reload(); + return; + } + + resetState(); + }, { passive: true }); + + window.addEventListener("touchcancel", resetState, { passive: true }); + } + function initPushControls() { const panel = document.querySelector("[data-push-panel]"); if (!panel) { @@ -1147,5 +1266,6 @@ initDashboardCharts(); initSportTypeManager(); initPwaShell(); + initPullToRefresh(); initPushControls(); })(); diff --git a/favicon.ico b/favicon.ico new file mode 100644 index 0000000..87efbf3 Binary files /dev/null and b/favicon.ico differ diff --git a/manifest.webmanifest b/manifest.webmanifest index 9c7a79b..62f63b3 100644 --- a/manifest.webmanifest +++ b/manifest.webmanifest @@ -12,15 +12,15 @@ "theme_color": "#0b1e2e", "icons": [ { - "src": "/assets/branding/logo-mark.svg", - "sizes": "any", - "type": "image/svg+xml", + "src": "/assets/branding/icon-192.png", + "sizes": "192x192", + "type": "image/png", "purpose": "any maskable" }, { - "src": "/assets/branding/apple-touch-icon.svg", - "sizes": "any", - "type": "image/svg+xml", + "src": "/assets/branding/icon-512.png", + "sizes": "512x512", + "type": "image/png", "purpose": "any" } ] diff --git a/templates/layout.php b/templates/layout.php index 3e2a791..78c5e3c 100644 --- a/templates/layout.php +++ b/templates/layout.php @@ -27,9 +27,11 @@ $brandSubtitle = match ($page) {