Add public/app.js
This commit is contained in:
525
public/app.js
Normal file
525
public/app.js
Normal 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();
|
||||
})();
|
||||
Reference in New Issue
Block a user