From 990926db0d371e3f19fc2bb095afeeba45539419 Mon Sep 17 00:00:00 2001 From: casper Date: Mon, 22 Jun 2026 16:24:06 +0000 Subject: [PATCH] Add public/app.js --- public/app.js | 525 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 525 insertions(+) create mode 100644 public/app.js diff --git a/public/app.js b/public/app.js new file mode 100644 index 0000000..80e5d61 --- /dev/null +++ b/public/app.js @@ -0,0 +1,525 @@ +(() => { + const cfg = window.PLAYER_CONFIG || {}; + + const VIDEO_LIST = Array.isArray(cfg.videos) ? cfg.videos : []; + + const DEF_VOL = Number.isFinite(cfg.defaultVolume) + ? Math.max(0, Math.min(1, cfg.defaultVolume)) + : 0.3; + + const LSV = cfg.localStorageVolumeKey || "vpref"; + const LSM = cfg.localStorageMutedKey || "mpref"; + + const $ = id => document.getElementById(id); + + const v = $("video"); + const p = $("play"); + const m = $("mute"); + const s = $("seek"); + const o = $("volume"); + const c = $("controls"); + const r = $("player"); + const t = $("tapUnmute"); + const x = $("rotate"); + const ba = $("prevBtn"); + const bb = $("nextBtn"); + + const F = VIDEO_LIST.filter(item => item && item.src && item.chance > 0); + + const D = F.length + ? F + : [ + { + src: "https://s1ne.com/buttery-stealthx.webm", + chance: 100 + } + ]; + + const HASLS = (() => { + try { + const key = "__video_player_storage_test__"; + localStorage.setItem(key, "1"); + localStorage.removeItem(key); + return true; + } catch { + return false; + } + })(); + + let i = 0; + let h; + let n; + let l = -1; + let u = false; + let y = false; + let q = []; + let blocked = false; + let sideControls = false; + let sideEdge = false; + + const coarsePointer = matchMedia("(pointer:coarse)").matches; + const EDGE_HOVER = 92; + + const storedVolume = HASLS ? Number(localStorage.getItem(LSV)) : NaN; + const storedMuted = HASLS ? localStorage.getItem(LSM) : null; + + function isMobile() { + return coarsePointer || innerWidth <= 768; + } + + function updateRangeProgress(el) { + const min = Number(el.min) || 0; + const max = Number(el.max) || 100; + const val = Number(el.value) || 0; + const progress = max === min ? 0 : ((val - min) * 100) / (max - min); + + el.style.setProperty("--range-progress", `${progress}%`); + } + + function weightedPick(seed) { + let total = 0; + + for (const item of D) { + total += Number(item.chance) || 0; + } + + if (total <= 0) { + return 0; + } + + let roll = seed == null ? Math.random() * total : seed % total; + + for (let index = 0; index < D.length; index++) { + roll -= Number(D[index].chance) || 0; + + if (roll < 0) { + return index; + } + } + + return D.length - 1; + } + + function loopIndex(index) { + return ((index % D.length) + D.length) % D.length; + } + + function preferredVolume() { + return Number.isFinite(storedVolume) + ? Math.max(0, Math.min(1, storedVolume)) + : DEF_VOL; + } + + function preferredMuted() { + return storedMuted === null ? false : storedMuted === "1"; + } + + function savePref() { + if (!HASLS) return; + + try { + localStorage.setItem(LSV, String(v.volume || 0)); + localStorage.setItem(LSM, v.muted ? "1" : "0"); + } catch {} + } + + function sideAllowed() { + return !isMobile() && innerWidth > 768; + } + + function updateSide() { + r.classList.toggle("side-visible", sideAllowed() && (sideControls || sideEdge)); + } + + function setSideControls(show) { + sideControls = Boolean(show); + updateSide(); + } + + function setSideEdge(show) { + sideEdge = Boolean(show); + updateSide(); + } + + function edgeHover(event) { + if (!sideAllowed()) { + setSideEdge(false); + return; + } + + const nearEdge = + event.clientX <= EDGE_HOVER || + event.clientX >= innerWidth - EDGE_HOVER; + + setSideEdge(nearEdge); + } + + function setTap(show) { + t.classList.toggle("visible", Boolean(show)); + } + + function loadVideo(index) { + i = loopIndex(index); + v.src = D[i].src; + blocked = false; + setTap(false); + } + + function trimQueue(arr) { + q = arr.slice(-3); + } + + function nextCleanIndex(index) { + if (!D.length) return 0; + + let candidate = loopIndex(index); + let tries = 0; + + while (tries < D.length && q.includes(candidate)) { + candidate = loopIndex(candidate + 1); + tries++; + } + + return candidate; + } + + function updatePlayButton() { + p.textContent = v.paused ? "x" : "▶"; + } + + function updateRotateButton() { + x.textContent = y ? "⟲" : "⟳"; + } + + function updateMuteButton() { + m.textContent = v.muted || v.volume === 0 ? "u" : "a"; + o.value = v.muted ? 0 : v.volume; + updateRangeProgress(o); + } + + function showControls() { + if (!u) { + c.classList.add("visible"); + u = true; + } + + setSideControls(true); + clearTimeout(h); + + h = setTimeout(() => { + c.classList.remove("visible"); + u = false; + setSideControls(false); + }, isMobile() ? 2200 : 1700); + } + + function resizeVideo() { + const videoWidth = v.videoWidth; + const videoHeight = v.videoHeight; + const windowWidth = innerWidth; + const windowHeight = innerHeight; + + if (!videoWidth || !videoHeight) { + v.style.objectFit = "cover"; + v.style.transform = "translateZ(0) scale(1.01)"; + return; + } + + if (y) { + v.style.objectFit = "contain"; + v.style.transform = "translateZ(0) rotate(90deg) scale(1.15)"; + return; + } + + const videoRatio = videoWidth / videoHeight; + const windowRatio = windowWidth / windowHeight; + + v.style.objectFit = videoRatio > windowRatio ? "contain" : "cover"; + v.style.transform = "translateZ(0) scale(1)"; + } + + function toggleRotate() { + y = !y; + updateRotateButton(); + resizeVideo(); + showControls(); + } + + function initVolume() { + const volume = preferredVolume(); + const muted = preferredMuted(); + + v.muted = muted; + v.volume = muted ? 0 : volume; + + savePref(); + } + + async function start() { + blocked = false; + + initVolume(); + updateMuteButton(); + updatePlayButton(); + updateRotateButton(); + + try { + await v.play(); + setTap(!(v.muted === false && v.volume > 0)); + return true; + } catch { + v.muted = true; + v.volume = 0; + updateMuteButton(); + + try { + await v.play(); + blocked = true; + setTap(true); + return false; + } catch { + blocked = true; + setTap(true); + return false; + } + } + } + + function unmuteFromTap() { + v.muted = false; + v.volume = Math.max(v.volume || 0, 0.5); + + blocked = false; + + setTap(false); + updateMuteButton(); + updatePlayButton(); + savePref(); + + v.play().catch(() => {}); + } + + function playNextByIndex(index) { + loadVideo(nextCleanIndex(index)); + q.push(i); + trimQueue(q); + showControls(); + start(); + } + + function loadRandomNext() { + let index = weightedPick(); + + for ( + let attempt = 0; + attempt < 8 && (index === i || q.includes(index)); + attempt++ + ) { + index = loopIndex(index + 1); + } + + loadVideo(index); + q.push(i); + trimQueue(q); + showControls(); + start(); + } + + v.addEventListener("error", () => { + playNextByIndex(i + 1); + }); + + v.addEventListener("ended", loadRandomNext); + + v.addEventListener("play", () => { + updatePlayButton(); + showControls(); + setTap(!(v.muted === false && v.volume > 0)); + }); + + v.addEventListener("pause", () => { + updatePlayButton(); + showControls(); + }); + + v.addEventListener("loadedmetadata", () => { + resizeVideo(); + + const clearText = () => { + const el = document.querySelector(".video-text"); + if (el) { + el.textContent = ""; + } + }; + + if (window.requestIdleCallback) { + window.requestIdleCallback(clearText); + } else { + setTimeout(clearText, 0); + } + + updateMuteButton(); + updatePlayButton(); + }); + + v.addEventListener("loadeddata", () => { + setTap(!(v.muted === false && v.volume > 0)); + }); + + v.addEventListener("volumechange", savePref); + + v.addEventListener("timeupdate", () => { + if (!v.duration) return; + + const percent = (100 * v.currentTime / v.duration) | 0; + + if (percent !== l) { + l = percent; + s.value = percent; + updateRangeProgress(s); + } + }); + + p.addEventListener("click", async event => { + event.stopPropagation(); + + if (v.paused) { + await v.play().catch(() => {}); + } else { + v.pause(); + } + }); + + m.addEventListener("click", event => { + event.stopPropagation(); + + if (v.muted || v.volume === 0) { + v.muted = false; + + if (v.volume === 0) { + v.volume = 0.5; + } + + blocked = false; + setTap(false); + } else { + v.muted = true; + } + + updateMuteButton(); + savePref(); + }); + + s.addEventListener("input", event => { + event.stopPropagation(); + updateRangeProgress(event.target); + + if (v.duration) { + v.currentTime = event.target.value / 100 * v.duration; + } + }); + + o.addEventListener("input", event => { + event.stopPropagation(); + updateRangeProgress(event.target); + + v.volume = parseFloat(event.target.value); + v.muted = v.volume === 0; + + if (v.volume > 0) { + blocked = false; + setTap(false); + } + + updateMuteButton(); + savePref(); + }); + + x.addEventListener("click", event => { + event.stopPropagation(); + toggleRotate(); + }); + + t.addEventListener("click", event => { + event.stopPropagation(); + unmuteFromTap(); + }); + + c.addEventListener("click", event => { + event.stopPropagation(); + }); + + r.addEventListener( + "click", + () => { + showControls(); + + if (blocked && v.muted) { + unmuteFromTap(); + } + }, + { passive: true } + ); + + r.addEventListener("mousemove", event => { + showControls(); + edgeHover(event); + }); + + r.addEventListener("mouseenter", event => { + showControls(); + edgeHover(event); + }); + + r.addEventListener("mouseleave", () => { + setSideEdge(false); + }); + + r.addEventListener("touchstart", showControls, { passive: true }); + + ba.addEventListener("mouseenter", () => { + setSideEdge(true); + }); + + bb.addEventListener("mouseenter", () => { + setSideEdge(true); + }); + + ba.addEventListener("click", event => { + event.stopPropagation(); + playNextByIndex(i - 1); + }); + + bb.addEventListener("click", event => { + event.stopPropagation(); + playNextByIndex(i + 1); + }); + + addEventListener("resize", () => { + clearTimeout(n); + + n = setTimeout(() => { + resizeVideo(); + updateSide(); + }, 100); + }); + + addEventListener("orientationchange", () => { + resizeVideo(); + updateSide(); + }); + + updateRangeProgress(s); + updateRangeProgress(o); + + loadVideo(weightedPick()); + + q = [i]; + + updatePlayButton(); + updateMuteButton(); + updateRotateButton(); + updateSide(); + + start(); +})(); \ No newline at end of file