From f04baf1c9c06835cc582ef0f065611bd6f8c0694 Mon Sep 17 00:00:00 2001 From: Jan Svabenik Date: Thu, 28 May 2026 19:05:31 +0200 Subject: [PATCH] fix(ui): NodeList.forEach not available on iOS 9 (Safari < 10) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit NodeList.prototype.forEach was added in Safari 10 / iOS 10. On iOS 9 it throws 'TypeError: forEach is not a function' at init time, before connect() is called — leaving the app stuck on 'Nicht verbunden' despite the async/await fix. Added qsa(selector, root?) helper that wraps querySelectorAll in Array.prototype.slice.call() to produce a real Array, then replaced all three call sites: - document.querySelectorAll('.btn-seek').forEach - starEls (stored NodeList).forEach x2 - playlistList.querySelectorAll('li').forEach Array.prototype.forEach has been safe since iOS 4. Co-Authored-By: Claude Sonnet 4.6 --- web/static/app.js | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/web/static/app.js b/web/static/app.js index b9d450d..7463d09 100644 --- a/web/static/app.js +++ b/web/static/app.js @@ -29,6 +29,12 @@ function apiFetch(url, opts) { // String.prototype.padStart not available in iOS < 10. function pad2(n) { return n < 10 ? '0' + n : '' + n; } +// NodeList.prototype.forEach not available in iOS < 10. +// Wrap querySelectorAll results in a real Array before iterating. +function qsa(selector, root) { + return Array.prototype.slice.call((root || document).querySelectorAll(selector)); +} + // ── DOM refs ────────────────────────────────────────────────────────────────── var $ = function(id) { return document.getElementById(id); }; @@ -172,7 +178,7 @@ $('btn-stop').addEventListener('click', function() { send({ cmd: 'stop' }); }); $('btn-next').addEventListener('click', function() { send({ cmd: 'next' }); }); $('btn-prev').addEventListener('click', function() { send({ cmd: 'prev' }); }); -document.querySelectorAll('.btn-seek').forEach(function(btn) { +qsa('.btn-seek').forEach(function(btn) { btn.addEventListener('click', function() { send({ cmd: 'seek', delta: parseInt(btn.dataset.delta, 10) }); }); @@ -224,7 +230,7 @@ function updateMuteBtn(muted) { } // ── Rating (stars) ──────────────────────────────────────────────────────────── -var starEls = document.querySelectorAll('.star'); +var starEls = qsa('.star'); function fetchRating() { apiFetch('/api/rating') @@ -509,7 +515,7 @@ function openPlaylist() { function updatePlaylistHighlight() { if (playlistOverlay.classList.contains('hidden')) return; - playlistList.querySelectorAll('li').forEach(function(li) { + qsa('li', playlistList).forEach(function(li) { var isCurrent = parseInt(li.dataset.index, 10) === currentPlaylistPos - 1; li.classList.toggle('current', isCurrent); });