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; }