/* VibeSonic — theme toggle + bottom player. Deliberately minimal * vanilla JS: no bundler, no framework. */ (function () { "use strict"; // --- Theme toggle --- function initTheme() { var toggle = document.querySelector("[data-theme-toggle]"); if (!toggle) return; toggle.addEventListener("click", function () { var root = document.documentElement; var current = root.getAttribute("data-bs-theme") || "dark"; var next = current === "dark" ? "light" : "dark"; root.setAttribute("data-bs-theme", next); try { localStorage.setItem("vs-theme", next); } catch (e) { /* storage may be unavailable (private browsing); theme just won't persist */ } }); } // --- Player --- function VibeSonicPlayer(audioEl, ui) { this.audio = audioEl; this.ui = ui; this.queue = []; this.index = -1; this.audio.addEventListener("timeupdate", this._onTimeUpdate.bind(this)); this.audio.addEventListener("loadedmetadata", this._onTimeUpdate.bind(this)); this.audio.addEventListener("ended", this.next.bind(this)); this.audio.addEventListener("play", this._syncPlayIcon.bind(this)); this.audio.addEventListener("pause", this._syncPlayIcon.bind(this)); } VibeSonicPlayer.prototype.playQueue = function (songs, startIndex) { this.queue = songs; this.index = startIndex || 0; this._loadCurrent(); this.audio.play().catch(function () {}); }; VibeSonicPlayer.prototype._loadCurrent = function () { var song = this.queue[this.index]; if (!song) return; this.audio.src = "/stream/" + encodeURIComponent(song.id); this.ui.title.textContent = song.title || ""; this.ui.artist.textContent = song.artist || ""; this.ui.art.src = song.cover ? "/cover-art/" + encodeURIComponent(song.cover) + "?size=100" : "/static/img/default-art.svg"; this.ui.starBtn.dataset.itemType = "song"; this.ui.starBtn.dataset.itemId = song.id; this.ui.starBtn.classList.toggle("active", !!song.starred); this.ui.starBtn.innerHTML = song.starred ? '' : ''; if ("mediaSession" in navigator) { navigator.mediaSession.metadata = new MediaMetadata({ title: song.title || "", artist: song.artist || "", album: song.album || "", }); } }; VibeSonicPlayer.prototype.playPause = function () { if (!this.queue.length) return; if (this.audio.paused) { this.audio.play().catch(function () {}); } else { this.audio.pause(); } }; VibeSonicPlayer.prototype.next = function () { if (this.index + 1 < this.queue.length) { this.index++; this._loadCurrent(); this.audio.play().catch(function () {}); } }; VibeSonicPlayer.prototype.prev = function () { if (this.audio.currentTime > 3) { this.audio.currentTime = 0; return; } if (this.index > 0) { this.index--; this._loadCurrent(); this.audio.play().catch(function () {}); } }; VibeSonicPlayer.prototype.seekTo = function (fraction) { if (this.audio.duration) { this.audio.currentTime = fraction * this.audio.duration; } }; VibeSonicPlayer.prototype.seekBy = function (deltaSeconds) { this.audio.currentTime = Math.max(0, this.audio.currentTime + deltaSeconds); }; VibeSonicPlayer.prototype.setVolume = function (v) { this.audio.volume = Math.min(1, Math.max(0, v)); this.ui.volumeSlider.value = this.audio.volume; }; VibeSonicPlayer.prototype.toggleMute = function () { this.audio.muted = !this.audio.muted; }; VibeSonicPlayer.prototype._syncPlayIcon = function () { this.ui.playBtn.innerHTML = this.audio.paused ? '' : ''; }; VibeSonicPlayer.prototype._onTimeUpdate = function () { var cur = this.audio.currentTime || 0; var dur = this.audio.duration || 0; this.ui.currentTime.textContent = formatTime(cur); this.ui.duration.textContent = formatTime(dur); if (dur > 0) { this.ui.progress.value = String((cur / dur) * 100); } }; function formatTime(seconds) { if (!isFinite(seconds) || seconds < 0) return "0:00"; var m = Math.floor(seconds / 60); var s = Math.floor(seconds % 60); return m + ":" + (s < 10 ? "0" : "") + s; } function collectQueueFrom(el) { var container = el.closest("[data-playable-list]") || document; var nodes = container.querySelectorAll(".vs-playable"); var songs = []; var startIndex = 0; nodes.forEach(function (node, i) { if (node === el || node.contains(el)) startIndex = i; songs.push({ id: node.dataset.songId, title: node.dataset.title, artist: node.dataset.artist, album: node.dataset.album, cover: node.dataset.cover, starred: node.dataset.starred === "1", }); }); return { songs: songs, startIndex: startIndex }; } function initPlayer() { var audio = document.getElementById("vs-audio"); if (!audio) return null; var ui = { art: document.getElementById("vs-player-art"), title: document.getElementById("vs-player-title"), artist: document.getElementById("vs-player-artist"), playBtn: document.getElementById("vs-play-btn"), currentTime: document.getElementById("vs-current-time"), duration: document.getElementById("vs-duration"), progress: document.getElementById("vs-progress"), volumeSlider: document.getElementById("vs-volume"), starBtn: document.getElementById("vs-now-star"), }; var player = new VibeSonicPlayer(audio, ui); document.getElementById("vs-play-btn").addEventListener("click", function () { player.playPause(); }); document.getElementById("vs-next-btn").addEventListener("click", function () { player.next(); }); document.getElementById("vs-prev-btn").addEventListener("click", function () { player.prev(); }); ui.progress.addEventListener("input", function () { player.seekTo(Number(ui.progress.value) / 100); }); ui.volumeSlider.addEventListener("input", function () { player.setVolume(Number(ui.volumeSlider.value)); }); document.getElementById("vs-mute-btn").addEventListener("click", function () { player.toggleMute(); }); document.addEventListener("click", function (ev) { var el = ev.target.closest(".vs-playable"); if (!el) return; ev.preventDefault(); var q = collectQueueFrom(el); player.playQueue(q.songs, q.startIndex); }); if ("mediaSession" in navigator) { navigator.mediaSession.setActionHandler("play", function () { player.playPause(); }); navigator.mediaSession.setActionHandler("pause", function () { player.playPause(); }); navigator.mediaSession.setActionHandler("previoustrack", function () { player.prev(); }); navigator.mediaSession.setActionHandler("nexttrack", function () { player.next(); }); } player.setVolume(1); return player; } function isTypingTarget(el) { if (!el) return false; var tag = el.tagName; return tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT" || el.isContentEditable; } function initKeyboardShortcuts(player) { document.addEventListener("keydown", function (ev) { if (isTypingTarget(document.activeElement)) return; if (ev.metaKey || ev.ctrlKey || ev.altKey) return; switch (ev.key) { case " ": case "k": case "K": ev.preventDefault(); if (player) player.playPause(); break; case "j": case "J": if (player) player.prev(); break; case "l": case "L": if (player) player.next(); break; case "ArrowLeft": if (player) player.seekBy(-5); break; case "ArrowRight": if (player) player.seekBy(5); break; case "ArrowUp": ev.preventDefault(); if (player) player.setVolume(player.audio.volume + 0.1); break; case "ArrowDown": ev.preventDefault(); if (player) player.setVolume(player.audio.volume - 0.1); break; case "m": case "M": if (player) player.toggleMute(); break; case "/": var search = document.getElementById("vs-search-input"); if (search) { ev.preventDefault(); search.focus(); } break; case "?": var helpModalEl = document.getElementById("vs-shortcuts-modal"); if (helpModalEl && window.bootstrap) { new window.bootstrap.Modal(helpModalEl).show(); } break; } }); } function initStarButtons() { document.addEventListener("click", function (ev) { var btn = ev.target.closest("[data-star-toggle]"); if (!btn) return; ev.preventDefault(); var itemType = btn.dataset.itemType; var itemId = btn.dataset.itemId; var active = btn.classList.contains("active"); var form = document.createElement("form"); form.method = "POST"; form.action = active ? "/favorites/unstar" : "/favorites/star"; form.style.display = "none"; [["item_type", itemType], ["item_id", itemId], ["csrf_token", window.vsCSRFToken || ""], ["next", location.pathname]].forEach(function (pair) { var input = document.createElement("input"); input.name = pair[0]; input.value = pair[1]; form.appendChild(input); }); document.body.appendChild(form); form.submit(); }); } document.addEventListener("DOMContentLoaded", function () { initTheme(); var player = initPlayer(); initKeyboardShortcuts(player); initStarButtons(); }); })();