CreateAI GIF Gallery Viewer

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

Du musst eine Erweiterung wie Tampermonkey, Greasemonkey oder Violentmonkey installieren, um dieses Skript zu installieren.

You will need to install an extension such as Tampermonkey to install this script.

You will need to install an extension such as Tampermonkey or Violentmonkey to install this script.

You will need to install an extension such as Tampermonkey or Userscripts to install this script.

You will need to install an extension such as Tampermonkey to install this script.

Sie müssten eine Skript Manager Erweiterung installieren damit sie dieses Skript installieren können

(Ich habe schon ein Skript Manager, Lass mich es installieren!)

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install an extension such as Stylus to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

You will need to install a user style manager extension to install this style.

(I already have a user style manager, let me install it!)

// ==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,iVBORw0KGgoAAAANSUhEUgAAAQAAAAEACAMAAABrrFhUAAAAJFBMVEVHcEzdBnLdBnLdBnLdB3L////cAGv74e30u9PnY5ztjLXiO4hUJmkqAAAABHRSTlMA2LJSY5/+8AAAB/FJREFUeJztndu6ozAIhWt01Nr3f9/xUHdPkrASKlVYN/ubi0nhD5CDMV4uSdV1CFXVHExVFUJdp71LOR8O5/mrqlAAoT6486uqLAZn8X4RHAfncn8SFAbnc38SH0HQNvVbCrzu1zbzi+IEwZn9H5UkcMrsf1YiDU7v/5gGMf+1jdtFEQLapu0lw/G/iIgBM/4TlfC0058tbRA4+fj/ro/5gDH/PwuhoQKwqDIeAO9JoG2NhsyOAKuC6QSYZDwAnkLAaAA8QsBoADxCQNsONVXGM2CdC5jNgDUHtK3QlPEMWHLAcAYsAMytA58VjJeAuQhom6Ar4zVwKgLmAZgeBBzAOAyYB2B6GuAAHIADcAAOQNsEXTkAB+AAtE3QlQNwAA5A2wRdOQAH4AC0TdCVA3AADkDbBF05AAfgALRN0JUDcAAOQNsEXTkAB+AAtE3QlQNwAA5A2wRdOQBpAO2sl38VN7fdtoxEAYzW3a7D0HXdv0Vd1/fD9ZZp9dJc/9LcMFwbUQhyAEazrv1q6qu6frIaba4hmxuucgyEAMzmblr7sPrGN3rs+mHb+VUZSLclAmDq+6i5q9E8m3nNdT1AlJYAgNHeeG89Gc1A0DYDszU20ZiKAQDuzwiGuM2A+3eiZeYXA2hviPspm0Gak8ZEKHKgDADYX6vNDWFze+OUkncNVHMslQDI6K+7NoOgbXNojupKgqAIQKbBo/rPStA2Od2/aMgnkA+gxOCNXsOLybM2gH4bQJnB/97ToL2WtdblFoJsALcyg98IFGTTqsxCkAmgtMNmPTJXwP9cAnkARPx/EGhLqslDWQSyALTl8b9oISDS/5NyCOQAEPN/qQNC4TQpoxKGnOsDxAyeOk3Q/3EsgFXBANq2cPx7N1mwtXE+sAMAmYr1Z7Ikzow5IQxAMmK/IbgQwhGg7WFKIAA0AoQT4AtCywAGgJUA3bQVPmna0y73CG0OTAIQQNKCfmiWpxft/UEGa7+Ubu56fxoyeTX9TewWj+ogAFgKJOdsWzu1edtGs4bPve82vW2E7RNiEZBwn9irb/MqB7V/mlqJYyGAAEgEQGy3E989iGx0pTbPkBDAUiDmRWJLAg2C6B5PGwcKhQAAIMo9Ofpga77UjC6eVUAIIBEQGwIYoy9CgONBhAAyF+ADiK2CWb/IJ8Ca0cdigO0UFAG0/Tzi7MGAu6KhIxJYEwEAyN/jLsOZK2luALf0qAyUQTaAyCwYmHwy/OfvasRM4rbBTwE6g4GSwykDQAmnc4qfA3wAVPgioy4jCbDVXHkrfADlsBvOchJazNERxW2FnQK05VCPJUMAXc6THLkNsAFQrMFduFQIgEc+yCrANYsfAdQvwZtwcQDoaTqKJzuS2ACI0MXWnk1qVQTva1N2cRviD4OFP/SwODoSwoeeKJ7sjuECoBYC+DGt6IM1tDGaJ7cBJgAy19jV9q+lWBGAE6rYMG4KkL+DP46MDYQZACie3J75LQD4sz1ygsbNzUIAeJdFh4H9AbBTgKg1PwCACCjpCDgrgNIi+AMAiKZ2qgGHL4KHHwbJtcVewyA8EYquhvafCLEBiE2F42fMwMbKp8K+GDr6cri4Y6xviPB3hKjIBfsstTG+95YYf1dYZlM0echm701RfgT85rY4WVB2fDACDV2yD0boIZXdij8a4z8dpicw7KjlHbRnWyTxcFTk8TjbZNnH45ERlT87AQ5IRMKXafLRD0j81hGZ6FNGvlPQKbH9DkkxCAjtq0CnxOLH5OI/Cp4WTR6Ti55ZRKaTCIBoDYsflIRPy8aByh2UhE6KJk+oUj+c82bUTkdlsbPC0bjbPiu+/L+sw9LEPQOJc7LgCv37x+XbgtfMP4/Lt8rH5dMzmfm6nCel7sNJqL/CzX3zhQn2KzPzSy5XuVdm5uaGb7wyc76XpsAzSxX47nD06f4vCN2jRAGc7sVJ/O1xsZfdF/WyOQW/Opvx+vy5Xp7OAZCYDkEaI1buNoKcR4unukAh4/oA5Ss0ljmbWFXJuUrnNy5RkSGw3yUqWau7Tz3m7Ie7Rie1IAf9lwip3Kuk9K7Seu2w412llftGNGVw6WVqmV7oXaf32VoB0IKLNYtulMzutW2Dc9NA7ULFJjMIyCs1o+8Dk0pc05
// @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();
})();