diff --git a/src/content.ts b/src/content.ts index 2b719591..285382e1 100644 --- a/src/content.ts +++ b/src/content.ts @@ -17,7 +17,7 @@ import { getCategoryActionType } from "./utils/categoryUtils"; import { SkipButtonControlBar } from "./js-components/skipButtonControlBar"; import { Tooltip } from "./render/Tooltip"; import { getStartTimeFromUrl } from "./utils/urlParser"; -import { getControls } from "./utils/pageUtils"; +import { findValidElement, getControls, isVisible } from "./utils/pageUtils"; import { CategoryPill } from "./render/CategoryPill"; import { AnimationUtils } from "./utils/animationUtils"; import { GenericUtils } from "./utils/genericUtils"; @@ -91,7 +91,8 @@ let controls: HTMLElement | null = null; const playerButtons: Record = {}; // Direct Links after the config is loaded -utils.wait(() => Config.config !== null, 1000, 1).then(() => videoIDChange(getYouTubeVideoID(document.URL))); +utils.wait(() => Config.config !== null, 1000, 1).then(() => videoIDChange(getYouTubeVideoID(document))); +addPageListeners(); addHotkeyListener(); //the amount of times the sponsor lookup has retried @@ -142,7 +143,7 @@ function messageListener(request: Message, sender: unknown, sendResponse: (respo //messages from popup script switch(request.message){ case "update": - videoIDChange(getYouTubeVideoID(document.URL)); + videoIDChange(getYouTubeVideoID(document)); break; case "sponsorStart": startOrEndTimingNewSegment() @@ -272,8 +273,8 @@ function resetValues() { } async function videoIDChange(id) { - //if the id has not changed return - if (sponsorVideoID === id) return; + //if the id has not changed return unless the video element has changed + if (sponsorVideoID === id && isVisible(video)) return; //set the global videoID sponsorVideoID = id; @@ -383,7 +384,7 @@ function createPreviewBar(): void { ]; for (const selector of progressElementSelectors) { - const el = document.querySelector(selector); + const el = findValidElement(document.querySelectorAll(selector)); if (el) { previewBar = new PreviewBar(el, onMobileYouTube, onInvidious); @@ -404,6 +405,16 @@ function durationChangeListener(): void { updatePreviewBar(); } +/** + * Triggered once the video is ready. + * This is mainly to attach to embedded players who don't have a video element visible. + */ +function videoOnReadyListener(): void { + createPreviewBar(); + updatePreviewBar(); + createButtons(); +} + function cancelSponsorSchedule(): void { if (currentSkipSchedule !== null) { clearTimeout(currentSkipSchedule); @@ -515,7 +526,7 @@ function inMuteSegment(currentTime: number): boolean { * This makes sure the videoID is still correct and if the sponsorTime is included */ function incorrectVideoCheck(videoID?: string, sponsorTime?: SponsorTime): boolean { - const currentVideoID = getYouTubeVideoID(document.URL); + const currentVideoID = getYouTubeVideoID(document); if (currentVideoID !== (videoID || sponsorVideoID) || (sponsorTime && (!sponsorTimes || !sponsorTimes?.some((time) => time.segment === sponsorTime.segment)) && !sponsorTimesSubmitting.some((time) => time.segment === sponsorTime.segment))) { @@ -546,7 +557,7 @@ function setupVideoMutationListener() { } function refreshVideoAttachments() { - const newVideo = document.querySelector('video'); + const newVideo = findValidElement(document.querySelectorAll('video')) as HTMLVideoElement; if (newVideo && newVideo !== video) { video = newVideo; @@ -557,11 +568,20 @@ function refreshVideoAttachments() { setupSkipButtonControlBar(); setupCategoryPill(); } + + // Create a new bar in the new video element + if (previewBar && !utils.findReferenceNode()?.contains(previewBar.container)) { + previewBar.remove(); + previewBar = null; + + createPreviewBar(); + } } } function setupVideoListeners() { //wait until it is loaded + video.addEventListener('loadstart', videoOnReadyListener) video.addEventListener('durationchange', durationChangeListener); if (!Config.config.disableSkipping) { @@ -653,7 +673,7 @@ function setupCategoryPill() { } async function sponsorsLookup(id: string, keepOldSubmissions = true) { - if (!video) refreshVideoAttachments(); + if (!video || !isVisible(video)) refreshVideoAttachments(); //there is still no video here if (!video) { setTimeout(() => sponsorsLookup(id), 100); @@ -924,8 +944,30 @@ async function getVideoInfo(): Promise { } } -function getYouTubeVideoID(url: string): string | boolean { - // For YouTube TV support +function getYouTubeVideoID(document: Document): string | boolean { + const url = document.URL; + // skip to URL if matches youtube watch or invidious or matches youtube pattern + if ((!url.includes("youtube.com")) || url.includes("/watch") || url.includes("/shorts/") || url.includes("playlist")) return getYouTubeVideoIDFromURL(url); + // skip to document and don't hide if on /embed/ + if (url.includes("/embed/")) return getYouTubeVideoIDFromDocument(document, false); + // skip to document if matches pattern + if (url.includes("/channel/") || url.includes("/user/") || url.includes("/c/")) return getYouTubeVideoIDFromDocument(document); + // not sure, try URL then document + return getYouTubeVideoIDFromURL(url) || getYouTubeVideoIDFromDocument(document); +} + +function getYouTubeVideoIDFromDocument(document: Document, hideIcon = true): string | boolean { + // get ID from document (channel trailer / embedded playlist) + const videoURL = document.querySelector("[data-sessionlink='feature=player-title']")?.getAttribute("href"); + if (videoURL) { + onInvidious = hideIcon; + return getYouTubeVideoIDFromURL(videoURL); + } else { + return false + } +} + +function getYouTubeVideoIDFromURL(url: string): string | boolean { if(url.startsWith("https://www.youtube.com/tv#/")) url = url.replace("#", ""); //Attempt to parse url @@ -945,7 +987,7 @@ function getYouTubeVideoID(url: string): string | boolean { } else if (!["m.youtube.com", "www.youtube.com", "www.youtube-nocookie.com", "music.youtube.com"].includes(urlObject.host)) { if (!Config.config) { // Call this later, in case this is an Invidious tab - utils.wait(() => Config.config !== null).then(() => videoIDChange(getYouTubeVideoID(url))); + utils.wait(() => Config.config !== null).then(() => videoIDChange(getYouTubeVideoIDFromURL(url))); } return false @@ -963,7 +1005,7 @@ function getYouTubeVideoID(url: string): string | boolean { console.error("[SB] Video ID not valid for " + url); return false; } - } + } return false; } @@ -1862,6 +1904,16 @@ function getSegmentsMessage(sponsorTimes: SponsorTime[]): string { return sponsorTimesMessage; } +function addPageListeners(): void { + const refreshListners = () => { + if (!isVisible(video)) { + refreshVideoAttachments(); + } + }; + + document.addEventListener("yt-navigate-finish", refreshListners); +} + function addHotkeyListener(): void { document.addEventListener("keydown", hotkeyListener); } diff --git a/src/utils.ts b/src/utils.ts index 760b2d65..f7bf748c 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -2,6 +2,7 @@ import Config from "./config"; import { CategorySelection, SponsorTime, FetchResponse, BackgroundScriptContainer, Registration } from "./types"; import * as CompileConfig from "../config.json"; +import { findValidElementFromSelector } from "./utils/pageUtils"; import { GenericUtils } from "./utils/genericUtils"; export default class Utils { @@ -329,11 +330,15 @@ export default class Utils { } findReferenceNode(): HTMLElement { - let referenceNode = document.getElementById("player-container-id") - ?? document.getElementById("movie_player") - ?? document.querySelector("#main-panel.ytmusic-player-page") // YouTube music - ?? document.querySelector("#player-container .video-js") // Invidious - ?? document.querySelector(".main-video-section > .video-container"); // Cloudtube + const selectors = [ + "#player-container-id", + "#movie_player", + "#c4-player", // Channel Trailer + "#main-panel.ytmusic-player-page", // YouTube music + "#player-container .video-js", // Invidious + ".main-video-section > .video-container" // Cloudtube + ] + let referenceNode = findValidElementFromSelector(selectors) if (referenceNode == null) { //for embeds const player = document.getElementById("player"); diff --git a/src/utils/pageUtils.ts b/src/utils/pageUtils.ts index e88f7cc9..d31ead47 100644 --- a/src/utils/pageUtils.ts +++ b/src/utils/pageUtils.ts @@ -17,4 +17,27 @@ export function getControls(): HTMLElement | false { } return false; +} + +export function isVisible(element: HTMLElement): boolean { + return element && element.offsetWidth > 0 && element.offsetHeight > 0; +} + +export function findValidElementFromSelector(selectors: string[]): HTMLElement { + return findValidElementFromGenerator(selectors, (selector) => document.querySelector(selector)); +} + +export function findValidElement(elements: HTMLElement[] | NodeListOf): HTMLElement { + return findValidElementFromGenerator(elements); +} + +function findValidElementFromGenerator(objects: T[] | NodeListOf, generator?: (obj: T) => HTMLElement): HTMLElement { + for (const obj of objects) { + const element = generator ? generator(obj as T) : obj as HTMLElement; + if (element && isVisible(element)) { + return element; + } + } + + return null; } \ No newline at end of file