CreateAI GIF Gallery Viewer

Elevate a single video in the gallery with a dimmed overlay and full keyboard controls.

이 스크립트를 설치하려면 Tampermonkey, Greasemonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램을 설치해야 합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Violentmonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey 또는 Userscripts와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 Tampermonkey와 같은 확장 프로그램이 필요합니다.

이 스크립트를 설치하려면 유저 스크립트 관리자 확장 프로그램이 필요합니다.

(이미 유저 스크립트 관리자가 설치되어 있습니다. 설치를 진행합니다!)

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 Stylus와 같은 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

이 스타일을 설치하려면 유저 스타일 관리자 확장 프로그램이 필요합니다.

(이미 유저 스타일 관리자가 설치되어 있습니다. 설치를 진행합니다!)

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