<!-----------------------------------------------------------------------------
Copyright 2014-2025 Jesper Larsson
This file is part of Klipspringer, <https://klipspringer.avadeaux.net/>
Klipspringer is free software: you can redistribute it and/or modify it
under the terms of the GNU General Public License as published by the Free
Software Foundation, either version 3 of the License, or (at your option)
any later version.
Klipspringer is distributed in the hope that it will be useful, but
WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License
for more details.
You should have received a copy of the GNU General Public License along
with Klipspringer. If not, see <https://www.gnu.org/licenses/>.
--------------------------------------------------------------------------->
<script>
'use strict';
let playStatus = { initializing: true }
let sourcePath;
// Puts the time on the page, or empties the time element if t is undefined.
const showTime = (t) => {
t = Math.round(t*60)/60;
let d = "";
if (t) {
const h = Math.trunc(t/3600);
if (h) {
d += h + ":";
t -= h * 3600;
}
const m = Math.trunc(t/60);
d += (d && m < 10 ? "0"+m : m) + ":";
const s = Math.trunc(t - m*60);
d += (s < 10 ? "0"+s : s);
}
timeElt.innerHTML = "";
if (d.length) { timeElt.appendChild(document.createTextNode(d+" ")) }
}
let playerInfoId = "0"; // info_id when images set
let playerTrackNames = [];
let firstTrackNo;
let playerTrackElts = [];
let pauseDisplayed = false, recordDisplayed = false, errorDisplayed = false;
// Blanks all the information fields.
const blankInfo = () => {
timeElt.innerHTML = "";
imagesElt.innerHTML = "";
bitsElt.innerHTML = "";
sampleRateElt.innerHTML = "";
bitrateElt.innerHTML = "";
channelsElt.innerHTML = "";
tracklistElt.innerHTML = "";
trackNumberElt.innerHTML = "";
trackTitleElt.innerHTML = "";
artistAlbumElt.innerHTML = "";
trackdispElt.classList.add("hidden");
playerInfoId = "0";
playerTrackNames = [];
playerTrackElts = [];
toggleTracklist(false);
}
// Updates the paused, error, and record notifications based on given status.
const updateNotifs = (status) => {
if (pauseDisplayed !== (!status.err && !!status.paused)) {
pauseDisplayed = !pauseDisplayed;
pausedElt.classList.toggle("hidden");
}
if (errorDisplayed !== !!status.err) {
errorDisplayed = !errorDisplayed;
errorElt.classList.toggle("hidden");
if (errorDisplayed) { errorAnimateElt.beginElement() }
}
if (recordDisplayed !== (status.rec_time !== undefined)) {
recordDisplayed = !recordDisplayed;
recordElt.classList.toggle("hidden");
}
}
// Puts track info (number, title, bitrate, not artist/album) onto the page based on given status.
const updateTrackInfo = (status) => {
updateNotifs(status);
if (status.track_number !== undefined) { // condition for blink reduction
bitsElt.innerHTML = status.bits_per_sample ? status.bits_per_sample+"-bit" : "";
sampleRateElt.innerHTML = (status.sample_rate || "").replace(/_/g, ' ');
let q = [];
if (status.quality) q.push(status.quality);
if (status.bitrate) q.push("streaming at "+status.bitrate);
bitrateElt.innerHTML = q.length ? "("+q.join(", ")+")" : "";
if (status.monomix) {
channelsElt.innerHTML = "Mono";
channelsElt.classList.add("alert");
monomixDialogElt = monomixOffDialogElt;
} else {
channelsElt.innerHTML = (status.channels || "").replace(/_/g, ' ');
channelsElt.classList.remove("alert");
monomixDialogElt = monomixOnDialogElt;
}
trackNumberElt.innerHTML = isNaN(status.track_number) ? "" : +(status.track_number);
trackTitleElt.innerHTML = "";
const ix = typeof status.track_ix === 'number'
? status.track_ix
: status.track_number - (typeof status.first_track === 'number' ? status.first_track : 1);
trackTitleElt.appendChild(document.createTextNode(playerTrackNames[ix] || ""));
}
}
// Updates the page with info from the given status, if it contains new info.
const updatePlayerInfo = (status) => {
if (document.visibilityState !== 'visible') { return }
if (status.new_info) { // set server-side if there has been an update
playerInfoId = status.info_id; // the serial number of the new info
sourcePath = status.path;
// Show images, if status has image refs.
if (status.images) {
imagesElt.innerHTML = "";
for (const fnam of status.images.files) {
const img = document.createElement('img');
img.src = "/img?source="+status.images.source+"&path="+encodeURIComponent(sourcePath)+"&file="+encodeURIComponent(fnam);
img.alt = fnam;
imagesElt.appendChild(img);
}
} else {
imagesElt.innerHTML = "";
}
// Track list.
tracklistElt.innerHTML = "";
firstTrackNo = status.first_track === undefined ? 1 : status.first_track;
playerTrackElts = [];
for (let i = 0; i < status.tracks.length; i++) {
const span = document.createElement('span');
playerTrackElts[i] = span;
span.innerHTML = '<span class="itnumber">'+(i+firstTrackNo)+'</span>';
span.appendChild(document.createTextNode(status.tracks[i]));
span.classList.add("track");
span.classList.add("click-choose");
span.addEventListener("click", () => {
blinkButton(span);
jumpTrack(i);
});
tracklistElt.appendChild(span);
}
// Artist/album
let artistAlbum = "";
if (status.artist) { artistAlbum += status.artist+": " }
if (status.album) { artistAlbum += status.album+" " }
if (status.genre) { artistAlbum += status.genre+" " }
if (status.now_playing) { artistAlbum += status.now_playing+" " }
playerTrackNames = status.tracks;
artistAlbumElt.innerHTML = "";
artistAlbumElt.appendChild(document.createTextNode(artistAlbum));
if (status.tracks.length < 2) {
toggleTracklist(false);
trackdispElt.classList.add("hidden");
trackNumberElt.classList.add("hidden");
} else {
trackdispElt.classList.remove("hidden");
trackNumberElt.classList.remove("hidden");
}
}
}
// Tracklist expansion.
let tracklistExpanded = false;
const toggleTracklist = (force) => {
tracklistExpanded = force === undefined ? !tracklistExpanded : force;
if (tracklistExpanded) {
trackdispElt.style.transform = "rotate(90deg)";
tracklistElt.style['max-height'] = tracklistElt.scrollHeight + "px";
} else {
tracklistElt.style['max-height'] = "0px";
trackdispElt.style.transform = "rotate(0)";
}
}
// Button animation.
const blinkButton = (elt) => {
elt.classList.add("blinking");
clearTimeout(elt.tout);
elt.tout = setTimeout(() => {
elt.classList.remove("blinking");
}, 100);
}
</script>
<div>
$(buttons)
</div>
<script>
'use strict';
const defineButtons = (actions) => {
for (const [b, a] of Object.entries(actions)) {
const elt = document.getElementById("b:"+b);
if (elt) {
if (a) {
elt.addEventListener(pressevent, (ev) => {
blinkButton(elt);
a();
});
} else {
elt.classList.add("hidden");
}
}
}
}
</script>
<div class="info">
<div class="info-line">
<span id="artist-album" class="info-unit"></span>
</div>
<div class="info-line">
<span id="trackdisp" class="info-unit trackdisp hidden">▷</span>
<span id="track-number" class="info-unit hidden"></span>
<span id="track-title" class="info-unit"></span><br>
</div>
<div id="tracklist" class="tracklist"></div>
<div class="info-line notif-line">
<span id="time" class="info-unit"></span>
<span id="paused" class="info-unit notif-icon click-choose hidden">
<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22">
<path stroke="none" d="M 1.8 4 h 5.4 v 18 h -5.4 z M 10.8 4 l 9.4 9 l -9.4 9 z">
<animateTransform attributeName="transform" type="translate" values="-0.5 0;0.5 0;-0.5 0" dur="0.2s" repeatCount="indefinite" />'
</path>
</svg>
</span>
<span id="record" class="info-unit notif-icon hidden">
<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22">
<circle stroke="none" fill="red" cx="11" cy="13">
<animate attributeName="r" values="3;6;3" dur="1s" repeatCount="indefinite" />
</circle>
</svg>
</span>
<span id="error" class="info-unit notif-icon click-choose hidden">
<svg xmlns="http://www.w3.org/2000/svg" width="22" height="22">
<path stroke="#9c0000" fill="#9c0000" stroke-linejoin="round" d="M 4.07 19 L 11 7 L 17.93 19 z">
<animate id="error-animate" attributeName="stroke-width" values="3;6;3" dur="0.2s" repeatCount="1" fill="freeze" />
</path>
<circle stroke="none" fill="white" cx="11" cy="17" r="1" />
<path stroke="none" fill="white" d="M 11 15.25 Q 10 11.75 10 10.25 a 1 1 0 1 1 2 0 Q 12 11.75 11 15.25" />
</svg>
</span>
<dialog id="error-dialog">
<div id="error-explain" class="dialog-message"></div>
<div>
<input type="button" class="click-choose dialog-button" id="error-hide" value="OK" />
<input type="button" class="click-choose dialog-button" id="error-reload" value="Reload" />
</div>
</dialog>
<div class="info-line audio">
<span id="bits" class="info-unit"></span>
<span id="sample-rate" class="info-unit"></span>
<span id="channels" class="info-unit click-set"></span>
<span id="bitrate" class="info-unit"></span>
</div>
<dialog id="monomix-on-dialog">
<div class="dialog-content">Mono mix</div>
<div>
<input type="button" class="click-choose dialog-button" id="monomix-on-yes" value="On" />
<input type="button" class="click-choose dialog-button" id="monomix-on-cancel" value="Cancel" />
</div>
</dialog>
<dialog id="monomix-off-dialog">
<div class="dialog-content">Mono mix</div>
<div>
<input type="button" class="click-choose dialog-button" id="monomix-off-yes" value="Off" />
<input type="button" class="click-choose dialog-button" id="monomix-off-cancel" value="Cancel" />
</div>
</dialog>
<div class="recording-menus hidden" id="sample-menus">
Sample:
<select id="bitsselect" class="recording-menu hidden"></select>
<select id="rateselect" class="recording-menu hidden"></select>
<select id="channelsselect" class="recording-menu hidden"></select>
</select>
</div>
<div class="recording-menus hidden" id="time-limits">
Stop recording after
<input id="time-limit" class="recording-menu" />
minutes
</div>
</div>
</div>
<div id="images"></div>
<script>
'use strict;'
const timeElt = document.getElementById("time");
const imagesElt = document.getElementById("images");
const bitsElt = document.getElementById("bits");
const sampleRateElt = document.getElementById("sample-rate");
const bitrateElt = document.getElementById("bitrate");
const channelsElt = document.getElementById("channels");
const tracklistElt = document.getElementById("tracklist");
const trackNumberElt = document.getElementById("track-number");
const trackTitleElt = document.getElementById("track-title");
const artistAlbumElt = document.getElementById("artist-album");
const trackdispElt = document.getElementById("trackdisp");
const pausedElt = document.getElementById("paused");
const errorElt = document.getElementById("error");
const errorAnimateElt = document.getElementById("error-animate");
const recordElt = document.getElementById("record");
const bitsselectElt = document.getElementById("bitsselect");
const rateselectElt = document.getElementById("rateselect");
const channelsselectElt = document.getElementById("channelsselect");
const errorDialogElt = document.getElementById("error-dialog");
const errorExplainElt = document.getElementById("error-explain");
const errorHideElt = document.getElementById("error-hide");
const errorReloadElt = document.getElementById("error-reload");
const sampleMenusElt = document.getElementById("sample-menus");
const timeLimitsElt = document.getElementById("time-limits");
const timeLimitElt = document.getElementById("time-limit");
const monomixOnDialogElt = document.getElementById("monomix-on-dialog");
const monomixOnYesElt = document.getElementById("monomix-on-yes");
const monomixOnCancelElt = document.getElementById("monomix-on-cancel");
const monomixOffDialogElt = document.getElementById("monomix-off-dialog");
const monomixOffYesElt = document.getElementById("monomix-off-yes");
const monomixOffCancelElt = document.getElementById("monomix-off-cancel");
let monomixDialogElt = monomixOnDialogElt;
const touchdevice = 'ontouchstart' in window || navigator.maxTouchPoints > 0 || navigator.msMaxTouchPoints > 0;
const pressevent = touchdevice ? "click" : "mousedown";
const normalpress = touchdevice ? (ev) => true : (ev) => ev.buttons === 1;
trackdispElt.addEventListener(pressevent, (ev) => {
toggleTracklist();
});
// Event listeners for the paused/error notifiers.
pausedElt.addEventListener(pressevent, (ev) => { if (normalpress(ev)) { play() } });
errorElt.addEventListener(pressevent, (ev) => {
if (!normalpress(ev)) { return }
errorExplainElt.innerHTML = playStatus.err === 'string' && playStatus.err || JSON.stringify(playStatus.err);
errorDialogElt.showModal();
});
errorHideElt.addEventListener(pressevent, (ev) => { if (normalpress(ev)) { errorDialogElt.close() } });
errorReloadElt.addEventListener(pressevent, (ev) => { if (normalpress(ev)) { window.location.reload() } });
// Monomix
channelsElt.addEventListener(pressevent, (ev) => { if (normalpress(ev)) { monomixDialogElt.showModal() } });
monomixOnYesElt.addEventListener(pressevent, (ev) => {
if (normalpress(ev)) {
setMonomix(true);
monomixDialogElt.close();
}
});
monomixOffYesElt.addEventListener(pressevent, (ev) => {
if (normalpress(ev)) {
setMonomix(false);
monomixDialogElt.close();
}
});
const monomixCancel = (ev) => { if (normalpress(ev)) { monomixDialogElt.close() } }
monomixOnCancelElt.addEventListener(pressevent, monomixCancel);
monomixOffCancelElt.addEventListener(pressevent, monomixCancel);
</script>
Version: v4.3.2.2 (2026-05-16T17:03:34+02:00)
Raw file
Source code overview
Klipspringer home