Add public/app.js

This commit is contained in:
2026-06-22 16:24:06 +00:00
parent f777514d1a
commit 990926db0d

525
public/app.js Normal file
View File

@@ -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();
})();