Selaa lähdekoodia

feat(ui): progressive enhancement for iOS 9 / iPad 2 compatibility

CSS:
- #seek-row and #controls-row: flexbox base (flex: 1 on children,
  margin-left gaps) + CSS Grid enhancement via @supports (display: grid)
- #playlist-overlay: replaced inset: 0 with explicit top/right/bottom/left
  (inset shorthand not available before iOS 14.5)
- All flex-gap usages: adjacent-sibling margin fallbacks as the base;
  gap values restored via a single @supports (gap: 1px) block at the end
  (flex gap not available before iOS 14.5 / Safari 14.1)
- Added -webkit- prefixes for user-select, flex, transition, transform
  throughout to be safe on older WebKit

JS:
- Added apiFetch() wrapper: uses native fetch() when available (iOS 10.3+),
  falls back to a minimal XMLHttpRequest shim for iOS 9 and older.
  Matches the exact subset of the fetch API the app uses: .json(),
  method, headers, body.
- Replaced all four fetch() call sites with apiFetch()

Result: layout and all API calls (rating, killist, playlist) work on
iOS 9 / iPad 2. Modern browsers get the exact same behaviour as before
via the @supports enhancements.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
master
Jan Svabenik 1 kuukausi sitten
vanhempi
commit
b501197177
2 muutettua tiedostoa jossa 165 lisäystä ja 25 poistoa
  1. +29
    -4
      web/static/app.js
  2. +136
    -21
      web/static/style.css

+ 29
- 4
web/static/app.js Näytä tiedosto

@@ -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 = '<li id="playlist-loading">Fehler beim Laden</li>';
return;


+ 136
- 21
web/static/style.css Näytä tiedosto

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

Loading…
Peruuta
Tallenna