diff --git a/web/static/app.js b/web/static/app.js index 604be97..f71e889 100644 --- a/web/static/app.js +++ b/web/static/app.js @@ -1,5 +1,30 @@ 'use strict'; +// ── Compatibility: fetch polyfill via XMLHttpRequest ───────────────────────── +// fetch() is available from Safari 10.1 / iOS 10.3 onwards. +// For iOS 9 (iPad 2) we fall back to a minimal XHR wrapper that matches the +// subset of the fetch API we actually use: .json() and { method, headers, body }. +function apiFetch(url, opts) { + if (window.fetch) return fetch(url, opts); + opts = opts || {}; + return new Promise(function(resolve, reject) { + var xhr = new XMLHttpRequest(); + xhr.open(opts.method || 'GET', url); + var headers = opts.headers || {}; + Object.keys(headers).forEach(function(k) { xhr.setRequestHeader(k, headers[k]); }); + xhr.onload = function() { + var text = xhr.responseText; + resolve({ + ok: xhr.status >= 200 && xhr.status < 300, + json: function() { return JSON.parse(text); }, + text: function() { return text; } + }); + }; + xhr.onerror = function() { reject(new Error('XHR error')); }; + xhr.send(opts.body || null); + }); +} + // ── DOM refs ────────────────────────────────────────────────────────────────── const $ = id => document.getElementById(id); @@ -200,7 +225,7 @@ const starEls = document.querySelectorAll('.star'); async function fetchRating() { try { - const r = await fetch('/api/rating').then(r => r.json()); + const r = await apiFetch('/api/rating').then(r => r.json()); currentRating = r.stars ?? 0; renderStars(currentRating); } catch { @@ -225,7 +250,7 @@ starEls.forEach(s => { const newRating = v === currentRating ? 0 : v; renderStars(newRating); try { - await fetch('/api/rating', { + await apiFetch('/api/rating', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ stars: newRating }), @@ -244,7 +269,7 @@ $('btn-kill').addEventListener('click', () => { }); $('btn-show-killist').addEventListener('click', async () => { - const list = await fetch('/api/killist').then(r => r.json()).catch(() => []); + const list = await apiFetch('/api/killist').then(r => r.json()).catch(() => []); killistItems.innerHTML = ''; (list || []).forEach(title => { const li = document.createElement('li'); @@ -447,7 +472,7 @@ async function openPlaylist() { let tracks; try { - tracks = await fetch('/api/playlist').then(r => r.json()); + tracks = await apiFetch('/api/playlist').then(r => r.json()); } catch { playlistList.innerHTML = '
  • Fehler beim Laden
  • '; return; diff --git a/web/static/style.css b/web/static/style.css index 06ed11f..b48e2bf 100644 --- a/web/static/style.css +++ b/web/static/style.css @@ -17,6 +17,7 @@ html, body { color: var(--text); font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; -webkit-tap-highlight-color: transparent; + -webkit-user-select: none; user-select: none; } @@ -24,20 +25,25 @@ html, body { max-width: 600px; margin: 0 auto; padding: 16px; + display: -webkit-flex; display: flex; + -webkit-flex-direction: column; flex-direction: column; - gap: 16px; min-height: 100vh; } +/* gap fallback: margin between every direct child */ +#app > * + * { margin-top: 16px; } /* Status bar */ #status-bar { + display: -webkit-flex; display: flex; + -webkit-align-items: center; align-items: center; - gap: 8px; font-size: 12px; color: var(--text-dim); } +#status-bar > * + * { margin-left: 8px; } #winamp-status { font-size: 16px; } #winamp-status.ok { color: #4caf50; } #winamp-status.err { color: var(--accent); } @@ -62,15 +68,17 @@ html, body { letter-spacing: 4px; cursor: pointer; -webkit-tap-highlight-color: transparent; + -webkit-user-select: none; user-select: none; } .star { color: #333; + -webkit-transition: color 0.12s, -webkit-transform 0.1s; transition: color 0.12s, transform 0.1s; display: inline-block; } .star.lit { color: #f5a623; } -.star:active { transform: scale(1.25); } +.star:active { -webkit-transform: scale(1.25); transform: scale(1.25); } #playlist-pos { font-size: 12px; @@ -90,10 +98,12 @@ html, body { /* Progress */ #progress-wrap { + display: -webkit-flex; display: flex; + -webkit-flex-direction: column; flex-direction: column; - gap: 4px; } +#progress-wrap > * + * { margin-top: 4px; } #progress-bar { height: 8px; background: var(--accent2); @@ -105,11 +115,14 @@ html, body { height: 100%; background: var(--accent); width: 0%; + -webkit-transition: width 0.5s linear; transition: width 0.5s linear; border-radius: 4px; } #time-display { + display: -webkit-flex; display: flex; + -webkit-justify-content: space-between; justify-content: space-between; font-size: 12px; color: var(--text-dim); @@ -123,32 +136,70 @@ html, body { border-radius: var(--radius); font-size: 22px; cursor: pointer; + -webkit-transition: background 0.15s, -webkit-transform 0.08s; transition: background 0.15s, transform 0.08s; + display: -webkit-flex; display: flex; + -webkit-align-items: center; align-items: center; + -webkit-justify-content: center; justify-content: center; touch-action: manipulation; } -.btn:active { transform: scale(0.93); background: var(--accent2); } +.btn:active { -webkit-transform: scale(0.93); transform: scale(0.93); background: var(--accent2); } -/* Seek row */ +/* ── Seek row ────────────────────────────────────────────────────────────── */ +/* Base: flexbox (iOS 9 and up) */ #seek-row { - display: grid; - grid-template-columns: repeat(4, 1fr); - gap: 8px; + display: -webkit-flex; + display: flex; +} +#seek-row > .btn-seek { + -webkit-flex: 1; + flex: 1; +} +#seek-row > .btn-seek + .btn-seek { margin-left: 8px; } + +/* Enhancement: CSS Grid (iOS 10.3+, all modern browsers) */ +@supports (display: grid) { + #seek-row { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 8px; + } + #seek-row > .btn-seek { -webkit-flex: none; flex: none; } + #seek-row > .btn-seek + .btn-seek { margin-left: 0; } } + .btn-seek { height: 52px; font-size: 14px; font-weight: 600; } -/* Controls row */ +/* ── Controls row ───────────────────────────────────────────────────────── */ +/* Base: flexbox */ #controls-row { - display: grid; - grid-template-columns: repeat(4, 1fr); - gap: 8px; + display: -webkit-flex; + display: flex; } +#controls-row > .btn-ctrl { + -webkit-flex: 1; + flex: 1; +} +#controls-row > .btn-ctrl + .btn-ctrl { margin-left: 8px; } + +/* Enhancement: CSS Grid */ +@supports (display: grid) { + #controls-row { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 8px; + } + #controls-row > .btn-ctrl { -webkit-flex: none; flex: none; } + #controls-row > .btn-ctrl + .btn-ctrl { margin-left: 0; } +} + .btn-ctrl { height: var(--btn-h); font-size: 28px; } .btn-play { background: var(--accent); @@ -158,14 +209,24 @@ html, body { /* Volume row */ #volume-row { + display: -webkit-flex; display: flex; + -webkit-align-items: center; align-items: center; - gap: 10px; } -.btn-vol { width: 48px; height: 48px; flex-shrink: 0; font-size: 18px; } +#volume-row > * + * { margin-left: 10px; } +.btn-vol { width: 48px; height: 48px; -webkit-flex-shrink: 0; flex-shrink: 0; font-size: 18px; } #btn-mute { font-size: 22px; } #btn-mute.muted { color: var(--accent); } -#volume-bar-wrap { flex: 1; display: flex; flex-direction: column; gap: 4px; } +#volume-bar-wrap { + -webkit-flex: 1; + flex: 1; + display: -webkit-flex; + display: flex; + -webkit-flex-direction: column; + flex-direction: column; +} +#volume-bar-wrap > * + * { margin-top: 4px; } #volume-bar { height: 8px; background: var(--accent2); @@ -177,6 +238,7 @@ html, body { height: 100%; background: #4caf50; width: 70%; + -webkit-transition: width 0.2s; transition: width 0.2s; border-radius: 4px; } @@ -189,10 +251,11 @@ html, body { /* Killist */ #killist-row { + display: -webkit-flex; display: flex; - gap: 8px; } -.btn-kill { flex: 1; height: 52px; font-size: 16px; background: #3a1a1a; } +#killist-row > * + * { margin-left: 8px; } +.btn-kill { -webkit-flex: 1; flex: 1; height: 52px; font-size: 16px; background: #3a1a1a; } .btn-kill:active { background: var(--accent); } .btn-kill-list { width: 64px; height: 52px; font-size: 14px; } .btn-action { width: 52px; height: 52px; font-size: 20px; } @@ -203,10 +266,20 @@ html, body { padding: 16px; } #killist-panel h3 { margin-bottom: 12px; } -#killist-items { list-style: none; display: flex; flex-direction: column; gap: 8px; } +#killist-items { + list-style: none; + display: -webkit-flex; + display: flex; + -webkit-flex-direction: column; + flex-direction: column; +} +#killist-items li + li { margin-top: 8px; } #killist-items li { + display: -webkit-flex; display: flex; + -webkit-justify-content: space-between; justify-content: space-between; + -webkit-align-items: center; align-items: center; background: var(--bg); border-radius: 8px; @@ -226,19 +299,29 @@ html, body { /* ── Playlist overlay ────────────────────────────────────────────────────── */ #playlist-overlay { position: fixed; - inset: 0; + /* inset: 0 fallback for browsers without inset support (iOS < 14.5) */ + top: 0; + right: 0; + bottom: 0; + left: 0; z-index: 200; background: var(--bg); + display: -webkit-flex; display: flex; + -webkit-flex-direction: column; flex-direction: column; } #playlist-header { + display: -webkit-flex; display: flex; + -webkit-align-items: center; align-items: center; + -webkit-justify-content: space-between; justify-content: space-between; padding: 16px 20px; background: var(--surface); border-bottom: 1px solid #ffffff12; + -webkit-flex-shrink: 0; flex-shrink: 0; } #playlist-title-label { @@ -261,20 +344,24 @@ html, body { #playlist-list { list-style: none; overflow-y: auto; + -webkit-flex: 1; flex: 1; padding: 8px 0; -webkit-overflow-scrolling: touch; } #playlist-list li { + display: -webkit-flex; display: flex; + -webkit-align-items: center; align-items: center; - gap: 12px; padding: 12px 20px; cursor: pointer; border-radius: 0; + -webkit-transition: background 0.1s; transition: background 0.1s; -webkit-tap-highlight-color: transparent; } +#playlist-list li > * + * { margin-left: 12px; } #playlist-list li:active { background: var(--accent2); } #playlist-list li.current { background: #e9456018; @@ -286,6 +373,7 @@ html, body { color: var(--text-dim); min-width: 32px; text-align: right; + -webkit-flex-shrink: 0; flex-shrink: 0; } .pl-title { @@ -303,4 +391,31 @@ li.current .pl-title { color: var(--accent); font-weight: 600; } font-size: 14px; } +/* ── gap enhancement for all flex containers (iOS 14.5+ / modern browsers) */ +@supports (gap: 1px) { + #app { gap: 16px; } + #app > * + * { margin-top: 0; } + + #status-bar { gap: 8px; } + #status-bar > * + * { margin-left: 0; } + + #progress-wrap { gap: 4px; } + #progress-wrap > * + * { margin-top: 0; } + + #volume-row { gap: 10px; } + #volume-row > * + * { margin-left: 0; } + + #volume-bar-wrap { gap: 4px; } + #volume-bar-wrap > * + * { margin-top: 0; } + + #killist-row { gap: 8px; } + #killist-row > * + * { margin-left: 0; } + + #killist-items { gap: 8px; } + #killist-items li + li { margin-top: 0; } + + #playlist-list li { gap: 12px; } + #playlist-list li > * + * { margin-left: 0; } +} + .hidden { display: none !important; }