This commit is contained in:
2026-08-23 14:13:18 -05:00
parent 789bf91eed
commit 866f243f1b
37 changed files with 1761 additions and 58 deletions
+21
View File
@@ -178,6 +178,21 @@ body {
text-align: right;
}
.vs-song-art-cell {
width: 48px;
padding-left: 0 !important;
padding-right: 0 !important;
}
.vs-song-art {
display: block;
width: 40px;
height: 40px;
border-radius: 0.25rem;
object-fit: cover;
background: var(--bs-secondary-bg);
}
/* --- Player bar --- */
.vs-player {
@@ -352,3 +367,9 @@ body {
display: none;
}
}
/* Inline page transitions keep the persistent player mounted. */
.vs-main[aria-busy="true"] {
opacity: .65;
transition: opacity 120ms ease;
pointer-events: none;
}
+129 -1
View File
@@ -474,7 +474,134 @@
form.appendChild(input);
});
document.body.appendChild(form);
form.submit();
form.requestSubmit();
});
}
// --- Inline navigation ---
// Fetch server-rendered pages and replace only the main content area. The
// audio element and player live outside that area, so playback continues
// uninterrupted while the URL, title, and active sidebar item still track
// the current page normally.
function initInlineNavigation() {
var pending = null;
var currentURL = location.href;
history.replaceState({ vsInline: true, scrollY: window.scrollY }, "", currentURL);
function isLocalHTTP(url) {
return (url.protocol === "http:" || url.protocol === "https:") && url.origin === location.origin;
}
function shouldHandleLink(link, ev) {
if (!link || ev.defaultPrevented || ev.button !== 0) return false;
if (ev.metaKey || ev.ctrlKey || ev.shiftKey || ev.altKey) return false;
if (link.target || link.hasAttribute("download") || link.dataset.noInline != null) return false;
var url = new URL(link.href, location.href);
if (!isLocalHTTP(url)) return false;
if (url.pathname.indexOf("/static/") === 0 || url.pathname.indexOf("/stream/") === 0 ||
url.pathname.indexOf("/cover-art/") === 0 || url.pathname.indexOf("/share/") === 0 ||
/\/export$/.test(url.pathname)) return false;
// Let the browser handle an in-page fragment without a network request.
if (url.pathname === location.pathname && url.search === location.search && url.hash) return false;
return true;
}
function setBusy(busy) {
var main = document.querySelector(".vs-main");
if (!main) return;
if (busy) main.setAttribute("aria-busy", "true");
else main.removeAttribute("aria-busy");
}
function applyPage(html, responseURL, push, restoreScroll) {
var parsed = new DOMParser().parseFromString(html, "text/html");
var nextMain = parsed.querySelector(".vs-main");
var oldMain = document.querySelector(".vs-main");
// Login, setup, shared, and error documents are intentionally full-page.
if (!nextMain || !oldMain) return false;
oldMain.replaceWith(nextMain);
document.title = parsed.title || document.title;
var tokenScript = parsed.querySelector("script[data-vs-csrf]");
if (tokenScript) window.vsCSRFToken = tokenScript.dataset.vsCsrf;
var activeHref = parsed.querySelector(".vs-sidebar .nav-link.active");
document.querySelectorAll(".vs-sidebar .nav-link").forEach(function (link) {
link.classList.toggle("active", !!activeHref && link.getAttribute("href") === activeHref.getAttribute("href"));
});
currentURL = responseURL;
if (push) history.pushState({ vsInline: true, scrollY: 0 }, "", responseURL);
var top = typeof restoreScroll === "number" ? restoreScroll : 0;
window.scrollTo(0, top);
var autofocus = nextMain.querySelector("[autofocus]");
if (autofocus) autofocus.focus({ preventScroll: true });
return true;
}
function navigate(url, options) {
options = options || {};
if (pending) pending.abort();
var controller = new AbortController();
pending = controller;
setBusy(true);
var request = {
method: options.method || "GET",
body: options.body || null,
signal: controller.signal,
headers: { "X-VibeSonic-Inline": "1" },
};
return fetch(url, request).then(function (response) {
if (!response.ok || !(response.headers.get("Content-Type") || "").includes("text/html")) {
throw new Error("not an inline page");
}
return response.text().then(function (html) {
if (!applyPage(html, response.url, options.push !== false, options.restoreScroll)) {
location.assign(response.url);
}
});
}).catch(function (err) {
if (err.name !== "AbortError") {
if ((options.method || "GET") === "GET") location.assign(url);
else location.reload();
}
}).finally(function () {
if (pending === controller) {
pending = null;
setBusy(false);
}
});
}
document.addEventListener("click", function (ev) {
var link = ev.target.closest("a[href]");
if (!shouldHandleLink(link, ev)) return;
ev.preventDefault();
history.replaceState({ vsInline: true, scrollY: window.scrollY }, "", currentURL);
navigate(link.href);
});
document.addEventListener("submit", function (ev) {
var form = ev.target;
if (ev.defaultPrevented || !(form instanceof HTMLFormElement) || form.dataset.noInline != null) return;
var url = new URL(form.action || location.href, location.href);
if (!isLocalHTTP(url) || url.pathname === "/logout") return;
ev.preventDefault();
var data = new FormData(form);
var method = (form.method || "GET").toUpperCase();
if (method === "GET") {
url.search = new URLSearchParams(data).toString();
navigate(url.href);
} else {
navigate(url.href, { method: method, body: data });
}
});
window.addEventListener("popstate", function (ev) {
navigate(location.href, {
push: false,
restoreScroll: ev.state && typeof ev.state.scrollY === "number" ? ev.state.scrollY : 0,
});
});
}
@@ -483,5 +610,6 @@
var player = initPlayer();
initKeyboardShortcuts(player);
initStarButtons();
initInlineNavigation();
});
})();