From ed19a428cb5eef84c8cf3fed5fda3afd5fc96305 Mon Sep 17 00:00:00 2001 From: metamuffin Date: Sun, 18 Jan 2026 23:43:12 +0100 Subject: Move client scripts to build-crate --- ui/client-scripts/src/transition.ts | 108 ++++++++++++++++++++++++++++++++++++ 1 file changed, 108 insertions(+) create mode 100644 ui/client-scripts/src/transition.ts (limited to 'ui/client-scripts/src/transition.ts') diff --git a/ui/client-scripts/src/transition.ts b/ui/client-scripts/src/transition.ts new file mode 100644 index 0000000..dadb266 --- /dev/null +++ b/ui/client-scripts/src/transition.ts @@ -0,0 +1,108 @@ +/* + This file is part of jellything (https://codeberg.org/metamuffin/jellything) + which is licensed under the GNU Affero General Public License (version 3); see /COPYING. + Copyright (C) 2026 metamuffin +*/ +/// + +import { e } from "./jshelper/src/element.ts"; + +interface HistoryState { + top: number +} + +const DURATION = 200 +globalThis.addEventListener("DOMContentLoaded", () => { + patch_page() +}) + +globalThis.addEventListener("popstate", async e => { + await transition_to(globalThis.location.href, e.state) +}) + +let disable_transition = false +globalThis.addEventListener("navigationrequiresreload", () => { + disable_transition = true +}) + +function patch_page() { + document.querySelectorAll("a").forEach(el => { + if (!el.href.startsWith("http")) return + if (el.target && el.target != "_self") return + el.addEventListener("click", async ev => { + ev.preventDefault() + await transition_to(el.href) + }) + }) +} + +async function transition_to(href: string, state?: HistoryState) { + stop() // nice! + if (disable_transition) return globalThis.location.href = href + const trigger_load = prepare_load(href, state) + await fade(false) + trigger_load() + disable_transition = false; +} + +function show_message(mesg: string, mode: "error" | "success" = "error") { + clear_spinner() + disable_transition = true + document.body.append(e("span", { class: ["jst-message", mode] }, mesg)) +} + +let i = 0; +function prepare_load(href: string, state?: HistoryState) { + const r_promise = fetch(href, { headers: { accept: "text/html" }, redirect: "manual" }) + return async () => { + let rt = "" + try { + const r = await r_promise + if (r.type == "opaqueredirect") { + globalThis.location.href = href + show_message("Native Player Started.", "success") + setTimeout(() => globalThis.location.reload(), 500) + return + } + if (!r.ok) return show_message("Error response. Try again.") + rt = await r.text() + } catch (e) { + if (e instanceof TypeError) return show_message("Navigation failed. Check your connection.") + return show_message("unknown error when fetching page") + } + const [head, body] = rt.split("")[1].split("") + globalThis.history.replaceState({ top: globalThis.scrollY, index: i++ } as HistoryState, "") + if (!state) globalThis.history.pushState({}, "", href) + clear_spinner() + document.head.innerHTML = head + document.body.outerHTML = body + globalThis.dispatchEvent(new Event("DOMContentLoaded")) + globalThis.scrollTo({ top: state?.top ?? 0 }); + fade(true) + } +} + +let spinner_timeout: number | undefined, spinner_element: HTMLElement | undefined; +function clear_spinner() { + if (spinner_timeout) clearTimeout(spinner_timeout) + if (spinner_element) spinner_element.remove() + spinner_element = spinner_timeout = undefined; +} +function fade(dir: boolean) { + const overlay = document.createElement("div") + overlay.classList.add("jst-fade") + overlay.style.backgroundColor = dir ? "black" : "transparent" + overlay.style.animationName = dir ? "jst-fadeout" : "jst-fadein" + overlay.style.animationFillMode = "forwards" + overlay.style.animationDuration = `${DURATION}ms` + document.body.appendChild(overlay) + return new Promise(res => { + setTimeout(() => { + if (dir) document.body.removeChild(overlay) + spinner_timeout = setTimeout(() => { + overlay.append(spinner_element = e("div", { class: "jst-spinner" }, "This is a spinner.")) + }, 500) + res() + }, DURATION) + }) +} -- cgit v1.3