import { isOnInvidious, parseYouTubeVideoIDFromURL } from "../../maze-utils/src/video"; import Config from "../config"; import { getVideoLabel } from "./videoLabels"; import { getThumbnailSelector, setThumbnailListener } from "../../maze-utils/src/thumbnailManagement"; import { VideoID } from "../types"; import { getSegmentsForVideo } from "./segmentData"; export async function handleThumbnails(thumbnails: HTMLImageElement[]): Promise { await Promise.all(thumbnails.map((t) => { labelThumbnail(t); setupThumbnailHover(t); })); } export async function labelThumbnail(thumbnail: HTMLImageElement): Promise { if (!Config.config?.fullVideoSegments || !Config.config?.fullVideoLabelsOnThumbnails) { hideThumbnailLabel(thumbnail); return null; } const videoID = extractVideoID(thumbnail); if (!videoID) { hideThumbnailLabel(thumbnail); return null; } const category = await getVideoLabel(videoID); if (!category) { hideThumbnailLabel(thumbnail); return null; } const { overlay, text } = createOrGetThumbnail(thumbnail); overlay.style.setProperty('--category-color', `var(--sb-category-preview-${category}, var(--sb-category-${category}))`); overlay.style.setProperty('--category-text-color', `var(--sb-category-text-preview-${category}, var(--sb-category-text-${category}))`); text.innerText = chrome.i18n.getMessage(`category_${category}`); overlay.classList.add("sponsorThumbnailLabelVisible"); return overlay; } export async function setupThumbnailHover(thumbnail: HTMLImageElement): Promise { // Cache would be reset every load due to no SPA if (isOnInvidious()) return; const mainElement = thumbnail.closest("#dismissible") as HTMLElement; if (mainElement) { mainElement.removeEventListener("mouseenter", thumbnailHoverListener); mainElement.addEventListener("mouseenter", thumbnailHoverListener); } } function thumbnailHoverListener(e: MouseEvent) { if (!chrome.runtime?.id) return; const thumbnail = (e.target as HTMLElement).querySelector(getThumbnailSelector()) as HTMLImageElement; if (!thumbnail) return; // Pre-fetch data for this video const videoID = extractVideoID(thumbnail); if (videoID) { void getSegmentsForVideo(videoID, false); } } function extractVideoID(thumbnail: HTMLImageElement): VideoID | null { const link = (isOnInvidious() ? thumbnail.parentElement : thumbnail.querySelector("#thumbnail")) as HTMLAnchorElement if (!link || link.nodeName !== "A" || !link.href) return null; // no link found return parseYouTubeVideoIDFromURL(link.href)?.videoID; } function getOldThumbnailLabel(thumbnail: HTMLImageElement): HTMLElement | null { return thumbnail.querySelector(".sponsorThumbnailLabel") as HTMLElement | null; } function hideThumbnailLabel(thumbnail: HTMLImageElement): void { const oldLabel = getOldThumbnailLabel(thumbnail); if (oldLabel) { oldLabel.classList.remove("sponsorThumbnailLabelVisible"); } } function createOrGetThumbnail(thumbnail: HTMLImageElement): { overlay: HTMLElement; text: HTMLElement } { const oldElement = getOldThumbnailLabel(thumbnail); if (oldElement) { return { overlay: oldElement as HTMLElement, text: oldElement.querySelector("span") as HTMLElement }; } const overlay = document.createElement("div") as HTMLElement; overlay.classList.add("sponsorThumbnailLabel"); // Disable hover autoplay overlay.addEventListener("pointerenter", (e) => { e.stopPropagation(); thumbnail.dispatchEvent(new PointerEvent("pointerleave", { bubbles: true })); }); overlay.addEventListener("pointerleave", (e) => { e.stopPropagation(); thumbnail.dispatchEvent(new PointerEvent("pointerenter", { bubbles: true })); }); const icon = createSBIconElement(); const text = document.createElement("span"); overlay.appendChild(icon); overlay.appendChild(text); thumbnail.appendChild(overlay); return { overlay, text }; } function createSBIconElement(): SVGSVGElement { const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg"); svg.setAttribute("viewBox", "0 0 565.15 568"); const use = document.createElementNS("http://www.w3.org/2000/svg", "use"); use.setAttribute("href", "#SponsorBlockIcon"); svg.appendChild(use); return svg; } // Inserts the icon svg definition, so it can be used elsewhere function insertSBIconDefinition() { const container = document.createElement("span"); // svg from /public/icons/PlayerStartIconSponsorBlocker.svg, with useless stuff removed container.innerHTML = ` `; document.body.appendChild(container.children[0]); } export function setupThumbnailListener(): void { setThumbnailListener(handleThumbnails, () => { insertSBIconDefinition(); }, () => Config.isReady()); }