cleaned
This commit is contained in:
@@ -44,190 +44,9 @@
|
||||
highlightTheme(localStorage.getItem('theme') || 'dark');
|
||||
markActiveNav();
|
||||
}
|
||||
// --- persistent audio player with playlist queue ----------
|
||||
// Survives htmx-boosted navigation: window state persists and
|
||||
// #uw-player carries hx-preserve so <audio> keeps playing.
|
||||
var uwQueue = []; // [{ src, title }]
|
||||
var uwIndex = -1; // index of the current track, -1 when empty
|
||||
|
||||
function uwSave() {
|
||||
try {
|
||||
sessionStorage.setItem('uwQueue', JSON.stringify({ q: uwQueue, i: uwIndex }));
|
||||
} catch (e) {}
|
||||
}
|
||||
function uwRestore() {
|
||||
try {
|
||||
var d = JSON.parse(sessionStorage.getItem('uwQueue') || 'null');
|
||||
if (d && d.q) { uwQueue = d.q; uwIndex = (typeof d.i === 'number' ? d.i : -1); }
|
||||
} catch (e) {}
|
||||
}
|
||||
function uwRenderQueue() {
|
||||
var player = document.getElementById('uw-player');
|
||||
if (player) player.classList.toggle('uw-has-queue', uwQueue.length > 0);
|
||||
var badge = document.getElementById('uw-queue-badge');
|
||||
if (badge) badge.textContent = uwQueue.length;
|
||||
var count = document.getElementById('uw-queue-count');
|
||||
if (count) count.textContent = uwQueue.length + (uwQueue.length === 1 ? ' track' : ' tracks');
|
||||
var list = document.getElementById('uw-queue-list');
|
||||
if (!list) return;
|
||||
list.innerHTML = '';
|
||||
uwQueue.forEach(function (t, idx) {
|
||||
var li = document.createElement('li');
|
||||
li.className = 'uw-queue-item' + (idx === uwIndex ? ' is-current' : '');
|
||||
var jump = document.createElement('button');
|
||||
jump.type = 'button';
|
||||
jump.className = 'uw-queue-jump';
|
||||
jump.setAttribute('data-uw-jump', idx);
|
||||
jump.textContent = (idx === uwIndex ? '▸' : (idx + 1));
|
||||
var name = document.createElement('span');
|
||||
name.className = 'uw-queue-name';
|
||||
name.setAttribute('data-uw-jump', idx);
|
||||
name.textContent = t.title || 'unknown track';
|
||||
var rm = document.createElement('button');
|
||||
rm.type = 'button';
|
||||
rm.className = 'uw-queue-remove';
|
||||
rm.setAttribute('data-uw-remove', idx);
|
||||
rm.setAttribute('aria-label', 'Remove from playlist');
|
||||
rm.textContent = '✕';
|
||||
li.appendChild(jump);
|
||||
li.appendChild(name);
|
||||
li.appendChild(rm);
|
||||
list.appendChild(li);
|
||||
});
|
||||
}
|
||||
// Point <audio> at the current queue entry; play it when asked.
|
||||
function uwLoad(autoplay) {
|
||||
var audio = document.getElementById('uw-audio');
|
||||
var now = document.getElementById('uw-now');
|
||||
if (!audio) return;
|
||||
var t = uwQueue[uwIndex];
|
||||
if (!t) {
|
||||
if (now) now.textContent = '—';
|
||||
audio.pause();
|
||||
audio.removeAttribute('src');
|
||||
document.documentElement.classList.remove('uw-playing');
|
||||
uwRenderQueue();
|
||||
uwSave();
|
||||
return;
|
||||
}
|
||||
document.documentElement.classList.add('uw-playing');
|
||||
if (now) now.textContent = t.title || 'unknown track';
|
||||
if (audio.getAttribute('src') !== t.src) {
|
||||
audio.setAttribute('src', t.src);
|
||||
audio.load();
|
||||
}
|
||||
if (autoplay) {
|
||||
var p = audio.play();
|
||||
if (p && p.catch) p.catch(function () {});
|
||||
}
|
||||
uwRenderQueue();
|
||||
uwSave();
|
||||
}
|
||||
// Replace the whole queue with a fresh set of tracks and play.
|
||||
function uwPlayList(tracks) {
|
||||
if (!tracks || !tracks.length) return;
|
||||
uwQueue = tracks.slice();
|
||||
uwIndex = 0;
|
||||
uwLoad(true);
|
||||
}
|
||||
// Add one track: play it now if idle, otherwise queue it.
|
||||
function uwAdd(src, title) {
|
||||
var audio = document.getElementById('uw-audio');
|
||||
var idle = uwIndex < 0 || !audio || audio.ended || !audio.getAttribute('src');
|
||||
uwQueue.push({ src: src, title: title });
|
||||
if (idle) { uwIndex = uwQueue.length - 1; uwLoad(true); }
|
||||
else { uwRenderQueue(); uwSave(); }
|
||||
}
|
||||
function uwNext() {
|
||||
if (uwIndex >= 0 && uwIndex < uwQueue.length - 1) { uwIndex++; uwLoad(true); }
|
||||
}
|
||||
function uwPrev() {
|
||||
var audio = document.getElementById('uw-audio');
|
||||
if (audio && audio.currentTime > 3) { audio.currentTime = 0; return; }
|
||||
if (uwIndex > 0) { uwIndex--; uwLoad(true); }
|
||||
else if (audio) audio.currentTime = 0;
|
||||
}
|
||||
function uwJump(idx) {
|
||||
if (idx >= 0 && idx < uwQueue.length) { uwIndex = idx; uwLoad(true); }
|
||||
}
|
||||
function uwRemove(idx) {
|
||||
if (idx < 0 || idx >= uwQueue.length) return;
|
||||
var playing = document.documentElement.classList.contains('uw-playing');
|
||||
uwQueue.splice(idx, 1);
|
||||
if (idx < uwIndex) { uwIndex--; uwRenderQueue(); uwSave(); }
|
||||
else if (idx > uwIndex) { uwRenderQueue(); uwSave(); }
|
||||
else {
|
||||
if (uwIndex >= uwQueue.length) uwIndex = uwQueue.length - 1;
|
||||
uwLoad(playing);
|
||||
}
|
||||
}
|
||||
function uwClear() {
|
||||
uwQueue = [];
|
||||
uwIndex = -1;
|
||||
uwLoad(false);
|
||||
var panel = document.getElementById('uw-queue');
|
||||
if (panel) panel.hidden = true;
|
||||
}
|
||||
function uwInit() {
|
||||
var audio = document.getElementById('uw-audio');
|
||||
if (!audio || audio.dataset.uwBound) return;
|
||||
audio.dataset.uwBound = '1';
|
||||
uwRestore();
|
||||
audio.addEventListener('ended', uwNext);
|
||||
uwRenderQueue();
|
||||
if (uwIndex >= 0 && uwQueue[uwIndex]) uwLoad(false);
|
||||
}
|
||||
document.addEventListener('DOMContentLoaded', function () { initPage(); uwInit(); });
|
||||
document.addEventListener('DOMContentLoaded', initPage);
|
||||
document.addEventListener('htmx:afterSwap', initPage);
|
||||
document.addEventListener('click', function (e) {
|
||||
if (!e.target.closest) return;
|
||||
var albumBtn = e.target.closest('.uw-play-album');
|
||||
if (albumBtn) {
|
||||
var sel = albumBtn.getAttribute('data-tracks-from');
|
||||
var scope = (sel && document.querySelector(sel)) || document;
|
||||
var tracks = [];
|
||||
scope.querySelectorAll('.uw-play').forEach(function (b) {
|
||||
tracks.push({ src: b.getAttribute('data-src'), title: b.getAttribute('data-title') });
|
||||
});
|
||||
uwPlayList(tracks);
|
||||
return;
|
||||
}
|
||||
// Play an album straight from the listing: fetch its tracks first.
|
||||
var remoteBtn = e.target.closest('.uw-play-album-remote');
|
||||
if (remoteBtn) {
|
||||
var rurl = remoteBtn.getAttribute('data-album-tracks-url');
|
||||
if (rurl) {
|
||||
remoteBtn.disabled = true;
|
||||
fetch(rurl, { headers: { 'Accept': 'application/json' } })
|
||||
.then(function (r) { return r.json(); })
|
||||
.then(function (d) { if (d && d.tracks) uwPlayList(d.tracks); })
|
||||
.catch(function () {})
|
||||
.then(function () { remoteBtn.disabled = false; });
|
||||
}
|
||||
return;
|
||||
}
|
||||
var playBtn = e.target.closest('.uw-play');
|
||||
if (playBtn) {
|
||||
uwAdd(playBtn.getAttribute('data-src'), playBtn.getAttribute('data-title'));
|
||||
return;
|
||||
}
|
||||
var jumpEl = e.target.closest('[data-uw-jump]');
|
||||
if (jumpEl) { uwJump(parseInt(jumpEl.getAttribute('data-uw-jump'), 10)); return; }
|
||||
var rmEl = e.target.closest('[data-uw-remove]');
|
||||
if (rmEl) { uwRemove(parseInt(rmEl.getAttribute('data-uw-remove'), 10)); return; }
|
||||
if (e.target.closest('#uw-next')) { uwNext(); return; }
|
||||
if (e.target.closest('#uw-prev')) { uwPrev(); return; }
|
||||
if (e.target.closest('#uw-queue-clear')) { uwClear(); return; }
|
||||
if (e.target.closest('#uw-queue-toggle')) {
|
||||
var panel = document.getElementById('uw-queue');
|
||||
if (panel) panel.hidden = !panel.hidden;
|
||||
return;
|
||||
}
|
||||
if (e.target.closest('#uw-close')) { uwClear(); return; }
|
||||
});
|
||||
</script>
|
||||
<link href="/static/css/app.css?v=2026-05-20b" rel="stylesheet" type="text/css">
|
||||
<link href="/static/css/theme.css?v=2026-05-20b" rel="stylesheet" type="text/css">
|
||||
<script src="/static/vendor/htmx/htmx-1.9.12.min.js"></script>
|
||||
<style>
|
||||
@media (min-width: 768px) {
|
||||
@@ -343,24 +162,5 @@
|
||||
<main class="term-main">
|
||||
{% block content %}{% endblock content %}
|
||||
</main>
|
||||
<div id="uw-player" hx-preserve="true">
|
||||
<div id="uw-queue" class="uw-queue" hidden>
|
||||
<div class="uw-queue-head">
|
||||
<span class="uw-queue-title">☰ playlist</span>
|
||||
<span id="uw-queue-count" class="uw-queue-meta">0 tracks</span>
|
||||
<button type="button" id="uw-queue-clear" class="uw-queue-clear">clear</button>
|
||||
</div>
|
||||
<ol id="uw-queue-list" class="uw-queue-list"></ol>
|
||||
</div>
|
||||
<div class="uw-player-inner">
|
||||
<span class="uw-player-tag">▶ now playing</span>
|
||||
<span id="uw-now" class="uw-player-title">—</span>
|
||||
<button type="button" id="uw-prev" class="uw-player-btn" aria-label="Previous track" title="Previous">⏮</button>
|
||||
<audio id="uw-audio" controls preload="none"></audio>
|
||||
<button type="button" id="uw-next" class="uw-player-btn" aria-label="Next track" title="Next">⏭</button>
|
||||
<button type="button" id="uw-queue-toggle" class="uw-player-btn" aria-label="Toggle playlist" title="Playlist">☰<span id="uw-queue-badge" class="uw-queue-badge">0</span></button>
|
||||
<button type="button" id="uw-close" class="uw-player-close" aria-label="Stop playback" title="Stop">✕</button>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user