<!-----------------------------------------------------------------------------
     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">&#x25b7;</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