/* Parts of this are inspired from code from VideoSegments, but rewritten and under the LGPLv3 license https://github.com/videosegments/videosegments/commits/f1e111bdfe231947800c6efdd51f62a4e7fef4d4/segmentsbar/segmentsbar.js */ 'use strict'; import Config from "../config"; import { ActionType } from "../types"; import Utils from "../utils"; const utils = new Utils(); const TOOLTIP_VISIBLE_CLASS = 'sponsorCategoryTooltipVisible'; export interface PreviewBarSegment { segment: [number, number]; category: string; unsubmitted: boolean; actionType: ActionType; showLarger: boolean; } class PreviewBar { container: HTMLUListElement; categoryTooltip?: HTMLDivElement; tooltipContainer?: HTMLElement; parent: HTMLElement; onMobileYouTube: boolean; onInvidious: boolean; segments: PreviewBarSegment[] = []; videoDuration = 0; constructor(parent: HTMLElement, onMobileYouTube: boolean, onInvidious: boolean) { this.container = document.createElement('ul'); this.container.id = 'previewbar'; this.parent = parent; this.onMobileYouTube = onMobileYouTube; this.onInvidious = onInvidious; this.createElement(parent); this.setupHoverText(); } setupHoverText(): void { if (this.onMobileYouTube || this.onInvidious) return; // Create label placeholder this.categoryTooltip = document.createElement("div"); this.categoryTooltip.className = "ytp-tooltip-title sponsorCategoryTooltip"; const tooltipTextWrapper = document.querySelector(".ytp-tooltip-text-wrapper"); if (!tooltipTextWrapper || !tooltipTextWrapper.parentElement) return; // Grab the tooltip from the text wrapper as the tooltip doesn't have its classes on init this.tooltipContainer = tooltipTextWrapper.parentElement; const titleTooltip = tooltipTextWrapper.querySelector(".ytp-tooltip-title"); if (!this.tooltipContainer || !titleTooltip) return; tooltipTextWrapper.insertBefore(this.categoryTooltip, titleTooltip.nextSibling); const seekBar = document.querySelector(".ytp-progress-bar-container"); if (!seekBar) return; let mouseOnSeekBar = false; seekBar.addEventListener("mouseenter", () => { mouseOnSeekBar = true; }); seekBar.addEventListener("mouseleave", () => { mouseOnSeekBar = false; }); const observer = new MutationObserver((mutations) => { if (!mouseOnSeekBar || !this.categoryTooltip || !this.tooltipContainer) return; // If the mutation observed is only for our tooltip text, ignore if (mutations.length === 1 && (mutations[0].target as HTMLElement).classList.contains("sponsorCategoryTooltip")) { return; } const tooltipTextElements = tooltipTextWrapper.querySelectorAll(".ytp-tooltip-text"); let timeInSeconds: number | null = null; let noYoutubeChapters = false; for (const tooltipTextElement of tooltipTextElements) { if (tooltipTextElement.classList.contains('ytp-tooltip-text-no-title')) noYoutubeChapters = true; const tooltipText = tooltipTextElement.textContent; if (tooltipText === null || tooltipText.length === 0) continue; timeInSeconds = utils.getFormattedTimeToSeconds(tooltipText); if (timeInSeconds !== null) break; } if (timeInSeconds === null) return; // Find the segment at that location, using the shortest if multiple found let segment: PreviewBarSegment | null = null; let currentSegmentLength = Infinity; for (const seg of this.segments) {// const segmentLength = seg.segment[1] - seg.segment[0]; const minSize = this.getMinimumSize(seg.showLarger); const startTime = segmentLength !== 0 ? seg.segment[0] : Math.floor(seg.segment[0]); const endTime = segmentLength > minSize ? seg.segment[1] : Math.ceil(seg.segment[0] + minSize); if (startTime <= timeInSeconds && endTime >= timeInSeconds) { if (segmentLength < currentSegmentLength) { currentSegmentLength = segmentLength; segment = seg; } } } if (segment === null && this.tooltipContainer.classList.contains(TOOLTIP_VISIBLE_CLASS)) { this.tooltipContainer.classList.remove(TOOLTIP_VISIBLE_CLASS); } else if (segment !== null) { this.tooltipContainer.classList.add(TOOLTIP_VISIBLE_CLASS); if (segment.unsubmitted) { this.categoryTooltip.textContent = chrome.i18n.getMessage("unsubmitted") + " " + utils.shortCategoryName(segment.category); } else { this.categoryTooltip.textContent = utils.shortCategoryName(segment.category); } // Use the class if the timestamp text uses it to prevent overlapping this.categoryTooltip.classList.toggle("ytp-tooltip-text-no-title", noYoutubeChapters); } }); observer.observe(tooltipTextWrapper, { childList: true, subtree: true, }); } createElement(parent: HTMLElement): void { this.parent = parent; if (this.onMobileYouTube) { if (parent.classList.contains("progress-bar-background")) { parent.style.backgroundColor = "rgba(255, 255, 255, 0.3)"; parent.style.opacity = "1"; } this.container.style.transform = "none"; } else if (!this.onInvidious) { // Hover listener this.parent.addEventListener("mouseenter", () => this.container.classList.add("hovered")); this.parent.addEventListener("mouseleave", () => this.container.classList.remove("hovered")); } // On the seek bar this.parent.prepend(this.container); } clear(): void { this.videoDuration = 0; this.segments = []; while (this.container.firstChild) { this.container.removeChild(this.container.firstChild); } } set(segments: PreviewBarSegment[], videoDuration: number): void { this.clear(); if (!segments) return; this.segments = segments; this.videoDuration = videoDuration; this.segments.sort(({segment: a}, {segment: b}) => { // Sort longer segments before short segments to make shorter segments render later return (b[1] - b[0]) - (a[1] - a[0]); }).forEach((segment) => { const bar = this.createBar(segment); this.container.appendChild(bar); }); } createBar({category, unsubmitted, segment, showLarger}: PreviewBarSegment): HTMLLIElement { const bar = document.createElement('li'); bar.classList.add('previewbar'); bar.innerHTML = showLarger ? '  ' : ' '; const fullCategoryName = (unsubmitted ? 'preview-' : '') + category; bar.setAttribute('sponsorblock-category', fullCategoryName); bar.style.backgroundColor = Config.config.barTypes[fullCategoryName]?.color; if (!this.onMobileYouTube) bar.style.opacity = Config.config.barTypes[fullCategoryName]?.opacity; bar.style.position = "absolute"; const duration = Math.min(segment[1], this.videoDuration) - segment[0]; if (duration > 0) bar.style.width = this.timeToPercentage(duration); const time = segment[1] ? Math.min(this.videoDuration, segment[0]) : segment[0]; bar.style.left = this.timeToPercentage(time); return bar; } remove(): void { this.container.remove(); if (this.categoryTooltip) { this.categoryTooltip.remove(); this.categoryTooltip = undefined; } if (this.tooltipContainer) { this.tooltipContainer.classList.remove(TOOLTIP_VISIBLE_CLASS); this.tooltipContainer = undefined; } } timeToPercentage(time: number): string { return Math.min(100, time / this.videoDuration * 100) + '%'; } /* * Approximate size on preview bar for smallest element (due to  ) */ getMinimumSize(showLarger = false): number { return this.videoDuration * (showLarger ? 0.006 : 0.003); } } export default PreviewBar;