stream: add a skip-to-next button and /skip route

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
nemunaire 2026-07-02 23:55:17 +08:00
commit a468d78153
2 changed files with 33 additions and 0 deletions

View file

@ -41,6 +41,16 @@
transition: background .15s; transition: background .15s;
} }
.share button:hover { background: rgba(155,140,255,.3); } .share button:hover { background: rgba(155,140,255,.3); }
.actions { display: flex; gap: .5rem; margin-top: .75rem; }
.actions button, .actions a {
flex: 1; text-align: center; text-decoration: none;
padding: .6rem .9rem; font-size: .85rem; font-weight: 600; cursor: pointer;
color: #f2f0f7; background: rgba(155,140,255,.18);
border: 1px solid rgba(155,140,255,.35); border-radius: 10px;
transition: background .15s;
}
.actions button:hover, .actions a:hover { background: rgba(155,140,255,.3); }
.actions button:disabled { opacity: .5; cursor: default; }
.dot { display: inline-block; width: 8px; height: 8px; border-radius: 50%; .dot { display: inline-block; width: 8px; height: 8px; border-radius: 50%;
background: #4ade80; margin-right: .4rem; vertical-align: middle; background: #4ade80; margin-right: .4rem; vertical-align: middle;
box-shadow: 0 0 0 0 rgba(74,222,128,.6); animation: pulse 2s infinite; } box-shadow: 0 0 0 0 rgba(74,222,128,.6); animation: pulse 2s infinite; }
@ -58,6 +68,9 @@
<div class="title" id="title"></div> <div class="title" id="title"></div>
<div class="artist" id="artist"></div> <div class="artist" id="artist"></div>
<audio id="player" controls autoplay preload="none"></audio> <audio id="player" controls autoplay preload="none"></audio>
<div class="actions">
<button id="skipBtn" type="button">⏭ Suivant</button>
</div>
<div class="share"> <div class="share">
<input id="streamUrl" type="text" readonly> <input id="streamUrl" type="text" readonly>
<button id="copyBtn" type="button">Copier</button> <button id="copyBtn" type="button">Copier</button>
@ -113,6 +126,15 @@
setTimeout(() => { copyBtn.textContent = prev; }, 1500); setTimeout(() => { copyBtn.textContent = prev; }, 1500);
}); });
// Passer au morceau suivant.
const skipBtn = document.getElementById("skipBtn");
skipBtn.addEventListener("click", async () => {
skipBtn.disabled = true;
try { await fetch("/skip", { method: "POST" }); } catch (e) { /* ignore */ }
// Laisser le temps à la bascule, puis rafraîchir l'affichage.
setTimeout(() => { skipBtn.disabled = false; poll(); }, 900);
});
poll(); poll();
setInterval(poll, 5000); setInterval(poll, 5000);
</script> </script>

View file

@ -111,3 +111,14 @@ harbor.http.register(
resp.json({title=m["title"], artist=m["artist"]}) resp.json({title=m["title"], artist=m["artist"]})
end end
) )
# Passer au morceau suivant : on saute le morceau en cours sur la source
# diffusée. request.dynamic a déjà préchargé le suivant, donc l'enchaînement
# est immédiat (le prochain /next est demandé au daemon dans la foulée).
harbor.http.register(
port=8000, method="POST", "/skip",
fun(_, resp) -> begin
source.skip(radio)
resp.json({skipped=true})
end
)