Elevate a single video in the gallery with a dimmed overlay and full keyboard controls.
// ==UserScript==
// @name CreateAI GIF Gallery Viewer
// @namespace https://createporn.com/
// @version 0.1.0
// @description Elevate a single video in the gallery with a dimmed overlay and full keyboard controls.
// @match https://www.createporn.com/*
// @match https://www.createhentai.com/*
// @match https://www.createaimilf.com/*
// @match https://www.createailatina.com/*
// @match https://www.createaibbw.com/*
// @match https://www.createaishemale.com/*
// @match https://www.createaiasian.com/*
// @run-at document-idle
// @icon data:image/png;base64,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
// @grant none
// @license MIT
// ==/UserScript==
(() => {
"use strict";
const STYLE_ID = "gjs-focus-style";
const OVERLAY_ID = "gjs-overlay";
const PROGRESS_ID = "gjs-progress";
const PLAY_INDICATOR_ID = "gjs-play-indicator";
const FOCUS_LAYER_ID = "gjs-focus-layer";
const FOCUS_LINK_ID = "gjs-focus-link";
const BODY_ACTIVE_CLASS = "gjs-overlay-active";
const BODY_FULLSCREEN_CLASS = "gjs-overlay-fullscreen";
const FOCUSED_CLASS = "gjs-focused-video";
const state = {
active: false,
current: null,
videos: [],
fullscreen: false,
wheelCooldownMs: 450,
lastWheelAt: 0,
lazyCooldownMs: 900,
lastLazyAt: 0,
lazyDistance: 2200,
lazySuppressedUntil: 0,
autoAdvanceNext: false,
};
const rectCache = new Map();
const relocations = new Map();
const isEditableTarget = (el) => {
if (!el) return false;
const tag = el.tagName ? el.tagName.toLowerCase() : "";
return (
tag === "input" ||
tag === "textarea" ||
tag === "select" ||
el.isContentEditable
);
};
const ensureStyles = () => {
if (document.getElementById(STYLE_ID)) return;
const style = document.createElement("style");
style.id = STYLE_ID;
style.textContent = `
#${OVERLAY_ID} {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.72);
opacity: 0;
pointer-events: none;
transition: opacity 120ms ease-out;
z-index: 999990;
}
#${FOCUS_LAYER_ID} {
position: fixed;
inset: 0;
z-index: 1000000;
pointer-events: none;
}
#${FOCUS_LAYER_ID} .${FOCUSED_CLASS} {
pointer-events: auto;
}
body.${BODY_ACTIVE_CLASS} #${OVERLAY_ID} {
opacity: 1;
}
body.${BODY_ACTIVE_CLASS} .${FOCUSED_CLASS} {
position: fixed !important;
top: 50% !important;
left: 50% !important;
transform: translate(-50%, -50%) !important;
width: 90vw !important;
height: 90vh !important;
max-width: 90vw !important;
max-height: 90vh !important;
z-index: 1000001 !important;
box-shadow: 0 24px 80px rgba(0,0,0,0.65);
background: #000;
object-fit: contain !important;
}
body.${BODY_FULLSCREEN_CLASS} .${FOCUSED_CLASS} {
width: 100dvw !important;
height: 100dvh !important;
max-width: 100dvw !important;
max-height: 100dvh !important;
border-radius: 0 !important;
box-shadow: none !important;
}
body.${BODY_ACTIVE_CLASS} video:not(.${FOCUSED_CLASS}) {
filter: saturate(0.85);
}
#${PROGRESS_ID} {
position: fixed;
height: 4px;
background: rgba(255, 255, 255, 0.15);
z-index: 1000002;
pointer-events: none;
}
#${PROGRESS_ID} .gjs-progress-fill {
height: 100%;
width: 0%;
background: #ff3b30;
transition: width 80ms linear;
}
#${PLAY_INDICATOR_ID} {
position: fixed;
left: 50%;
top: 50%;
width: 72px;
height: 72px;
border-radius: 50%;
display: none;
align-items: center;
justify-content: center;
background: rgba(0, 0, 0, 0.6);
border: 1px solid rgba(255, 255, 255, 0.45);
box-shadow: 0 12px 40px rgba(0, 0, 0, 0.45);
opacity: 0;
transform: translate(-50%, -50%) scale(0.92);
transition: opacity 120ms ease-out, transform 120ms ease-out;
pointer-events: none;
z-index: 1000002;
}
#${PLAY_INDICATOR_ID} svg {
width: 52%;
height: 52%;
fill: #ffffff;
margin-left: 6%;
filter: drop-shadow(0 2px 6px rgba(0, 0, 0, 0.45));
}
#${FOCUS_LINK_ID} {
position: fixed;
z-index: 1000003;
display: none;
color: #ffffff;
font: 600 12px/1 "Trebuchet MS", "Verdana", sans-serif;
letter-spacing: 0.04em;
text-transform: uppercase;
text-decoration: none;
pointer-events: auto;
}
#${FOCUS_LINK_ID} .gjs-open-label {
position: absolute;
top: 10px;
left: 10px;
padding: 6px 8px;
border-radius: 999px;
background: rgba(0, 0, 0, 0.65);
border: 1px solid rgba(255, 255, 255, 0.35);
box-shadow: 0 6px 18px rgba(0, 0, 0, 0.35);
pointer-events: none;
}
`;
document.head.appendChild(style);
};
const ensureOverlay = () => {
if (document.getElementById(OVERLAY_ID)) return;
const overlay = document.createElement("div");
overlay.id = OVERLAY_ID;
document.body.appendChild(overlay);
};
const ensureFocusLayer = () => {
if (document.getElementById(FOCUS_LAYER_ID)) return;
const layer = document.createElement("div");
layer.id = FOCUS_LAYER_ID;
document.body.appendChild(layer);
};
const ensureProgress = () => {
let bar = document.getElementById(PROGRESS_ID);
if (!bar) {
bar = document.createElement("div");
bar.id = PROGRESS_ID;
const fill = document.createElement("div");
fill.className = "gjs-progress-fill";
bar.appendChild(fill);
}
const layer = document.getElementById(FOCUS_LAYER_ID);
const host = layer || document.body;
if (bar.parentNode !== host) {
host.appendChild(bar);
}
};
const ensurePlayIndicator = () => {
let indicator = document.getElementById(PLAY_INDICATOR_ID);
if (!indicator) {
indicator = document.createElement("div");
indicator.id = PLAY_INDICATOR_ID;
indicator.setAttribute("aria-hidden", "true");
indicator.innerHTML = `
<svg viewBox="0 0 100 100" aria-hidden="true" focusable="false">
<polygon points="38,28 78,50 38,72"></polygon>
</svg>
`;
}
const layer = document.getElementById(FOCUS_LAYER_ID);
const host = layer || document.body;
if (indicator.parentNode !== host) {
host.appendChild(indicator);
}
};
const ensureFocusLink = () => {
if (document.getElementById(FOCUS_LINK_ID)) return;
const link = document.createElement("a");
link.id = FOCUS_LINK_ID;
link.setAttribute("aria-label", "Open focused video page");
link.target = "_blank";
link.rel = "noopener noreferrer";
const label = document.createElement("span");
label.className = "gjs-open-label";
label.textContent = "Open";
link.appendChild(label);
document.body.appendChild(link);
};
const relocateVideo = (video) => {
if (relocations.has(video)) return;
const rect = video.getBoundingClientRect();
const placeholder = document.createElement("div");
const display = window.getComputedStyle(video).display;
placeholder.style.width = `${rect.width}px`;
placeholder.style.height = `${rect.height}px`;
placeholder.style.display = display === "inline" ? "inline-block" : display;
const parent = video.parentNode;
const nextSibling = video.nextSibling;
if (!parent) return;
parent.insertBefore(placeholder, video);
const layer = document.getElementById(FOCUS_LAYER_ID);
if (layer) layer.appendChild(video);
relocations.set(video, { parent, nextSibling, placeholder });
rectCache.set(video, rect);
};
const restoreVideo = (video) => {
const info = relocations.get(video);
if (!info) return;
const { parent, nextSibling, placeholder } = info;
if (placeholder && placeholder.parentNode) {
placeholder.parentNode.replaceChild(video, placeholder);
} else if (nextSibling && nextSibling.parentNode === parent) {
parent.insertBefore(video, nextSibling);
} else {
parent.appendChild(video);
}
relocations.delete(video);
};
const blockVideoDownload = (video) => {
if (!video) return;
if (video.dataset.gjsBlocked === "1") return;
let hasSource = false;
const src = video.getAttribute("src");
if (src) {
video.dataset.gjsSrc = src;
hasSource = true;
}
const sources = Array.from(video.querySelectorAll("source"));
for (const source of sources) {
const sourceSrc = source.getAttribute("src");
if (sourceSrc) {
source.dataset.gjsSrc = sourceSrc;
hasSource = true;
}
source.removeAttribute("src");
}
if (hasSource) {
video.removeAttribute("src");
video.preload = "none";
try {
video.pause();
} catch (_) {
// Ignore pause errors on restrictive players.
}
try {
video.load();
} catch (_) {
// Ignore load errors on restrictive players.
}
video.dataset.gjsBlocked = "1";
}
};
const restoreVideoDownload = (video) => {
if (!video) return;
if (video.dataset.gjsBlocked !== "1") return;
let restored = false;
if (video.dataset.gjsSrc && !video.getAttribute("src")) {
video.setAttribute("src", video.dataset.gjsSrc);
restored = true;
}
const sources = Array.from(video.querySelectorAll("source"));
for (const source of sources) {
if (source.dataset.gjsSrc && !source.getAttribute("src")) {
source.setAttribute("src", source.dataset.gjsSrc);
restored = true;
}
}
if (restored) {
video.preload = "metadata";
try {
video.load();
} catch (_) {
// Ignore load errors on restrictive players.
}
}
delete video.dataset.gjsBlocked;
};
const refreshVideos = () => {
state.videos = Array.from(document.querySelectorAll("video"));
};
const disableLoopAll = () => {
for (const video of state.videos) {
video.loop = false;
video.removeAttribute("loop");
}
};
const getNearestVideo = () => {
refreshVideos();
if (!state.videos.length) return null;
const viewportCenter = window.scrollY + window.innerHeight / 2;
let closest = null;
let closestDelta = Number.POSITIVE_INFINITY;
for (const video of state.videos) {
const rect = video.getBoundingClientRect();
const center = window.scrollY + rect.top + rect.height / 2;
const delta = Math.abs(center - viewportCenter);
if (delta < closestDelta) {
closest = video;
closestDelta = delta;
}
}
return closest;
};
const activateOverlay = () => {
if (state.active) return;
state.active = true;
document.body.classList.add(BODY_ACTIVE_CLASS);
};
const deactivateOverlay = () => {
if (!state.active) return;
state.active = false;
document.body.classList.remove(BODY_ACTIVE_CLASS);
exitNativeFullscreen();
syncFullscreenState(false);
state.lazySuppressedUntil = Date.now() + 1000;
if (state.current) {
restoreViewportPosition(state.current);
try {
state.current.pause();
} catch (_) {
// Ignore pause errors on restrictive players.
}
detachVideoListeners(state.current);
state.current.classList.remove(FOCUSED_CLASS);
restoreVideo(state.current);
state.current = null;
}
rectCache.clear();
updateProgressVisibility(false);
};
const focusVideo = (video) => {
if (!video) return;
ensureStyles();
ensureOverlay();
ensureFocusLayer();
ensureProgress();
ensurePlayIndicator();
ensureFocusLink();
activateOverlay();
refreshVideos();
disableLoopAll();
restoreVideoDownload(video);
const autoAdvance = state.autoAdvanceNext;
state.autoAdvanceNext = false;
if (state.current && state.current !== video) {
detachVideoListeners(state.current);
state.current.classList.remove(FOCUSED_CLASS);
restoreVideo(state.current);
}
state.current = video;
rectCache.clear();
relocateVideo(video);
video.classList.add(FOCUSED_CLASS);
const ordered = getOrderedVideos();
const index = ordered.findIndex((item) => item.video === video);
if (index !== -1) {
console.log(
"[gjs] focused video index:",
index + 1,
"/",
ordered.length
);
}
resetVideoStart(video);
attemptPlay(video);
scheduleAutoplayRetry(video, autoAdvance);
attachVideoListeners(video);
video.scrollIntoView({ behavior: "smooth", block: "center" });
updateProgressVisibility(true);
updateProgress();
updatePlayIndicator();
maybeTriggerLazyLoad();
setFullscreen(true);
};
const focusNearest = () => {
const nearest = getNearestVideo();
if (nearest) focusVideo(nearest);
};
const getVideoLayout = () => {
refreshVideos();
return state.videos.map((video) => {
const relocation = relocations.get(video);
const rectSource =
relocation && relocation.placeholder
? relocation.placeholder
: video === state.current && rectCache.has(video)
? rectCache.get(video)
: video;
const rect =
rectSource instanceof DOMRect
? rectSource
: rectSource.getBoundingClientRect();
return {
video,
rect,
centerX: rect.left + rect.width / 2,
centerY: rect.top + rect.height / 2,
};
});
};
const clusterByCenter = (items, axis, threshold) => {
const sorted = [...items].sort((a, b) => a[axis] - b[axis]);
const clusters = [];
for (const item of sorted) {
const last = clusters[clusters.length - 1];
if (!last || Math.abs(item[axis] - last.center) > threshold) {
clusters.push({ center: item[axis], items: [item] });
} else {
last.items.push(item);
last.center =
last.items.reduce((sum, entry) => sum + entry[axis], 0) /
last.items.length;
}
}
return clusters;
};
const getOrderedVideos = () => {
const layout = getVideoLayout();
if (!layout.length) return [];
const heights = layout
.map((item) => item.rect.height)
.filter((h) => h > 0)
.sort((a, b) => a - b);
const medianHeight =
heights.length ? heights[Math.floor(heights.length / 2)] : 80;
const rowThreshold = Math.max(40, medianHeight * 0.6);
const rowClusters = clusterByCenter(layout, "centerY", rowThreshold);
const rows = rowClusters
.sort((a, b) => a.center - b.center)
.map((cluster) =>
cluster.items.sort((a, b) => a.centerX - b.centerX)
);
return rows.flatMap((row) => row);
};
const focusByDirection = (direction) => {
const ordered = getOrderedVideos();
if (!ordered.length) return;
if (!state.current) {
focusNearest();
return;
}
const idx = ordered.findIndex((item) => item.video === state.current);
if (idx === -1) {
focusNearest();
return;
}
const nextIndex = direction === "next" ? idx + 1 : idx - 1;
const next = ordered[nextIndex];
if (next) {
blockVideoDownload(state.current);
focusVideo(next.video);
return;
}
maybeTriggerLazyLoad(true);
setTimeout(() => {
const retryOrdered = getOrderedVideos();
const retryIdx = retryOrdered.findIndex(
(item) => item.video === state.current
);
const retryNext =
retryIdx === -1
? null
: retryOrdered[direction === "next" ? retryIdx + 1 : retryIdx - 1];
if (retryNext) {
blockVideoDownload(state.current);
focusVideo(retryNext.video);
}
}, 400);
};
const togglePlay = () => {
if (!state.current) return;
if (state.current.paused) {
state.current.play();
} else {
state.current.pause();
}
};
const seekBy = (seconds) => {
if (!state.current || isNaN(state.current.duration)) return;
state.current.currentTime = Math.max(
0,
Math.min(state.current.duration, state.current.currentTime + seconds)
);
};
const adjustVolume = (delta) => {
if (!state.current) return;
const next = Math.max(0, Math.min(1, state.current.volume + delta));
state.current.volume = next;
};
const toggleMute = () => {
if (!state.current) return;
state.current.muted = !state.current.muted;
};
const updateProgressVisibility = (visible) => {
const bar = document.getElementById(PROGRESS_ID);
if (!bar) return;
bar.style.display = visible ? "block" : "none";
const link = document.getElementById(FOCUS_LINK_ID);
if (link) link.style.display = visible ? "block" : "none";
const indicator = document.getElementById(PLAY_INDICATOR_ID);
if (indicator) indicator.style.display = visible ? "flex" : "none";
};
const updateProgressPosition = () => {
const bar = document.getElementById(PROGRESS_ID);
if (!bar || !state.current) return;
bar.style.zIndex = "1000002";
const rect = state.current.getBoundingClientRect();
bar.style.left = `${rect.left}px`;
bar.style.top = `${rect.bottom - 4}px`;
bar.style.width = `${rect.width}px`;
const link = document.getElementById(FOCUS_LINK_ID);
if (link) {
link.style.left = `${rect.left}px`;
link.style.top = `${rect.top}px`;
link.style.width = `${rect.width}px`;
link.style.height = `${rect.height}px`;
const url = getVideoPageUrl();
if (url) link.href = url;
}
updatePlayIndicatorPosition();
};
const updatePlayIndicatorPosition = () => {
const indicator = document.getElementById(PLAY_INDICATOR_ID);
if (!indicator || !state.current) return;
const rect = state.current.getBoundingClientRect();
const size = Math.max(
48,
Math.min(96, Math.min(rect.width, rect.height) * 0.18)
);
indicator.style.left = `${rect.left + rect.width / 2}px`;
indicator.style.top = `${rect.top + rect.height / 2}px`;
indicator.style.width = `${size}px`;
indicator.style.height = `${size}px`;
};
const updatePlayIndicator = () => {
const indicator = document.getElementById(PLAY_INDICATOR_ID);
if (!indicator) return;
if (!state.current || !state.active) {
indicator.style.opacity = "0";
indicator.style.transform = "translate(-50%, -50%) scale(0.92)";
indicator.setAttribute("aria-hidden", "true");
return;
}
const shouldShow = state.current.paused || state.current.ended;
indicator.style.opacity = shouldShow ? "1" : "0";
indicator.style.transform = shouldShow
? "translate(-50%, -50%) scale(1)"
: "translate(-50%, -50%) scale(0.92)";
indicator.setAttribute("aria-hidden", shouldShow ? "false" : "true");
updatePlayIndicatorPosition();
};
const handleEnded = (event) => {
if (!state.active) return;
const target = event.target;
if (!(target instanceof HTMLVideoElement)) return;
if (target !== state.current) return;
updatePlayIndicator();
state.autoAdvanceNext = true;
focusByDirection("next");
};
const attachVideoListeners = (video) => {
if (!video) return;
video.addEventListener("timeupdate", updateProgress);
video.addEventListener("play", updatePlayIndicator);
video.addEventListener("pause", updatePlayIndicator);
video.addEventListener("ended", handleEnded);
};
const detachVideoListeners = (video) => {
if (!video) return;
video.removeEventListener("timeupdate", updateProgress);
video.removeEventListener("play", updatePlayIndicator);
video.removeEventListener("pause", updatePlayIndicator);
video.removeEventListener("ended", handleEnded);
};
const updateProgress = () => {
const bar = document.getElementById(PROGRESS_ID);
if (!bar || !state.current) return;
const fill = bar.querySelector(".gjs-progress-fill");
const duration = state.current.duration;
if (isNaN(duration) || duration <= 0) {
fill.style.width = "0%";
return;
}
const percent = (state.current.currentTime / duration) * 100;
fill.style.width = `${Math.max(0, Math.min(100, percent))}%`;
updateProgressPosition();
};
const restoreViewportPosition = (video) => {
const relocation = relocations.get(video);
const placeholder = relocation ? relocation.placeholder : null;
if (!placeholder) return;
const rect = placeholder.getBoundingClientRect();
const targetTop =
window.scrollY + rect.top - (window.innerHeight - rect.height) / 2;
window.scrollTo({ top: Math.max(0, targetTop), left: 0, behavior: "auto" });
};
const resetVideoStart = (video) => {
if (!video) return;
try {
video.currentTime = 0;
} catch (_) {
// Some videos may not allow seeking until metadata is loaded.
}
};
const syncFullscreenState = (enabled) => {
state.fullscreen = enabled;
document.body.classList.toggle(BODY_FULLSCREEN_CLASS, enabled);
if (!state.current) return;
updateProgressPosition();
updateProgress();
};
const setFullscreen = (enabled) => {
if (!state.current && enabled) {
focusNearest();
}
if (!state.current) return;
syncFullscreenState(enabled);
};
const toggleFullscreen = () => {
setFullscreen(!state.fullscreen);
};
const getVideoPageUrl = () => {
if (!state.current) return null;
const relocation = relocations.get(state.current);
const relocationRoot = relocation
? relocation.placeholder || relocation.parent
: null;
const container =
(relocationRoot && relocationRoot.closest
? relocationRoot.closest("div.h-full.w-full")
: null) || state.current.closest("div.h-full.w-full");
const scopedAnchor = container
? container.querySelector("a[href^=\"/gif/\"]")
: null;
const anchor = scopedAnchor || state.current.closest("a[href]");
if (anchor && anchor.href) return anchor.href;
const dataHref =
state.current.getAttribute("data-href") ||
state.current.getAttribute("data-link") ||
state.current.getAttribute("data-url");
if (dataHref) return dataHref;
return null;
};
const openVideoPage = () => {
const url = getVideoPageUrl();
if (!url) return;
const link = document.createElement("a");
link.href = url;
link.target = "_blank";
link.rel = "noopener noreferrer";
document.body.appendChild(link);
link.click();
link.remove();
setTimeout(() => {
try {
window.focus();
} catch (_) {
// Ignore focus errors in restrictive browsers.
}
}, 50);
};
const maybeTriggerLazyLoad = (force = false) => {
if (!state.active) return;
if (Date.now() < state.lazySuppressedUntil) return;
const now = Date.now();
if (now - state.lastLazyAt < state.lazyCooldownMs) return;
const doc = document.documentElement;
const remaining = doc.scrollHeight - (window.scrollY + window.innerHeight);
if (!force && remaining > state.lazyDistance) return;
state.lastLazyAt = now;
const sentinel = document.querySelector("div.-mt-\\[55vh\\]");
if (sentinel) {
sentinel.scrollIntoView({ behavior: "smooth", block: "end" });
window.dispatchEvent(new Event("scroll"));
return;
}
const target = Math.max(doc.scrollHeight - window.innerHeight - 1, 0);
window.scrollTo({ top: target, left: 0, behavior: "smooth" });
window.dispatchEvent(new Event("scroll"));
};
const attemptPlay = (video) => {
if (!video) return;
const playPromise = video.play();
if (playPromise && typeof playPromise.catch === "function") {
playPromise.catch(() => {
const wasMuted = video.muted;
video.muted = true;
const retry = video.play();
if (retry && typeof retry.catch === "function") {
retry.catch(() => {
video.muted = wasMuted;
});
}
});
}
};
const scheduleAutoplayRetry = (video, autoAdvance) => {
if (!video) return;
const hadSound = !video.muted;
if (autoAdvance && hadSound) {
video.muted = true;
}
const maybeRestoreSound = () => {
if (!autoAdvance || !hadSound) return;
if (video.paused || video.ended) return;
video.muted = false;
};
video.addEventListener("playing", maybeRestoreSound, { once: true });
let attempts = 0;
const retry = () => {
if (!state.active || state.current !== video) return;
if (!video.paused && !video.ended) return;
attempts += 1;
attemptPlay(video);
updatePlayIndicator();
if (attempts < 6) {
setTimeout(retry, 200);
}
};
setTimeout(retry, 180);
video.addEventListener("canplay", retry, { once: true });
video.addEventListener("loadedmetadata", retry, { once: true });
};
const handleKeydown = (event) => {
if (isEditableTarget(event.target)) return;
if (event.key === "Escape") {
if (state.active) {
event.preventDefault();
deactivateOverlay();
}
return;
}
if (event.key === "`") {
event.preventDefault();
toggleFullscreen();
return;
}
if (!state.active) return;
switch (event.key) {
case " ":
event.preventDefault();
togglePlay();
break;
case "ArrowRight":
case "d":
event.preventDefault();
seekBy(3);
break;
case "ArrowLeft":
case "a":
event.preventDefault();
seekBy(-3);
break;
case "ArrowUp":
event.preventDefault();
adjustVolume(0.05);
break;
case "ArrowDown":
event.preventDefault();
adjustVolume(-0.05);
break;
case "m":
event.preventDefault();
toggleMute();
break;
case "PageDown":
case "s":
event.preventDefault();
focusByDirection("next");
break;
case "PageUp":
case "w":
event.preventDefault();
focusByDirection("prev");
break;
case "g":
if (event.altKey) {
event.preventDefault();
openVideoPage();
console.log('video opened')
}
break;
default:
break;
}
};
const handleWheel = (event) => {
if (!state.active) return;
const now = Date.now();
if (now - state.lastWheelAt < state.wheelCooldownMs) return;
state.lastWheelAt = now;
if (event.deltaY > 0) {
focusByDirection("next");
} else if (event.deltaY < 0) {
focusByDirection("prev");
}
};
const handleLeftMouseDown = (event) => {
if (!event.isTrusted) return;
const target = event.target;
if (event.button !== 0) return;
if (state.active) {
if (target instanceof HTMLElement && target.closest(`#${FOCUS_LINK_ID}`)) {
return;
}
event.preventDefault();
deactivateOverlay();
return;
}
if (!(target instanceof HTMLVideoElement)) return;
event.preventDefault();
focusVideo(target);
};
const handleRightClick = (event) => {
if (event.button !== 2) return;
if (!event.shiftKey) return;
const target = event.target;
if (!(target instanceof HTMLVideoElement)) return;
event.preventDefault();
event.stopPropagation();
target.click();
};
const handleContextMenu = (event) => {
const target = event.target;
if (target.id === "gjs-focus-link") {
event.preventDefault();
} else {
handleRightClick(event);
}
};
const init = () => {
ensureStyles();
ensureOverlay();
refreshVideos();
disableLoopAll();
document.addEventListener("keydown", handleKeydown, true);
document.addEventListener("wheel", handleWheel, { passive: true });
document.addEventListener("mousedown", handleLeftMouseDown, true);
document.addEventListener("mousedown", handleRightClick, true);
document.addEventListener("contextmenu", handleContextMenu, true);
window.addEventListener("resize", updateProgressPosition, { passive: true });
document.addEventListener("scroll", updateProgressPosition, { passive: true });
document.addEventListener("scroll", maybeTriggerLazyLoad, { passive: true });
};
init();
})();