mirror of
https://github.com/ajayyy/SponsorBlock.git
synced 2025-12-08 04:27:15 +03:00
Clean up Preview Bar
Fixes: - Segments hidden by longer segments - Duration with skips not accounting for segment overlaps - Duration with skips not accounting for user's skip choices - Segment category text in preview tooltip overlaps the seek bar - Segment category text in preview tooltip breaks for timestamps over one hour - `previewBar.ts` lacks function argument and return types - Tooltip label not cleaned up on remove - General code style issues
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
/*
|
||||
This is based on code from VideoSegments.
|
||||
https://github.com/videosegments/videosegments/commits/f1e111bdfe231947800c6efdd51f62a4e7fef4d4/segmentsbar/segmentsbar.js
|
||||
This is based on code from VideoSegments.
|
||||
https://github.com/videosegments/videosegments/commits/f1e111bdfe231947800c6efdd51f62a4e7fef4d4/segmentsbar/segmentsbar.js
|
||||
*/
|
||||
|
||||
'use strict';
|
||||
@@ -9,179 +9,218 @@ import Config from "../config";
|
||||
import Utils from "../utils";
|
||||
const utils = new Utils();
|
||||
|
||||
class PreviewBar {
|
||||
container: HTMLUListElement;
|
||||
parent: any;
|
||||
onMobileYouTube: boolean;
|
||||
onInvidious: boolean;
|
||||
const TOOLTIP_VISIBLE_CLASS = 'sponsorCategoryTooltipVisible';
|
||||
|
||||
timestamps: number[][];
|
||||
types: string[];
|
||||
|
||||
constructor(parent: any, onMobileYouTube: boolean, onInvidious: boolean) {
|
||||
this.container = document.createElement('ul');
|
||||
this.container.id = 'previewbar';
|
||||
this.parent = parent;
|
||||
|
||||
this.onMobileYouTube = onMobileYouTube;
|
||||
this.onInvidious = onInvidious;
|
||||
|
||||
this.updatePosition(parent);
|
||||
|
||||
this.setupHoverText();
|
||||
}
|
||||
|
||||
setupHoverText(): void {
|
||||
if (this.onMobileYouTube || this.onInvidious) return;
|
||||
|
||||
const seekBar = document.querySelector(".ytp-progress-bar-container");
|
||||
|
||||
// Create label placeholder
|
||||
const tooltipTextWrapper = document.querySelector(".ytp-tooltip-text-wrapper");
|
||||
const titleTooltip = document.querySelector(".ytp-tooltip-title");
|
||||
const categoryTooltip = document.createElement("div");
|
||||
categoryTooltip.className = "sbHidden ytp-tooltip-title";
|
||||
categoryTooltip.id = "sponsor-block-category-tooltip"
|
||||
|
||||
tooltipTextWrapper.insertBefore(categoryTooltip, titleTooltip.nextSibling);
|
||||
|
||||
let mouseOnSeekBar = false;
|
||||
|
||||
seekBar.addEventListener("mouseenter", (event) => {
|
||||
mouseOnSeekBar = true;
|
||||
});
|
||||
|
||||
seekBar.addEventListener("mouseleave", (event) => {
|
||||
mouseOnSeekBar = false;
|
||||
categoryTooltip.classList.add("sbHidden");
|
||||
});
|
||||
|
||||
const observer = new MutationObserver((mutations, observer) => {
|
||||
if (!mouseOnSeekBar) return;
|
||||
|
||||
// See if mutation observed is only this ID (if so, ignore)
|
||||
if (mutations.length == 1 && (mutations[0].target as HTMLElement).id === "sponsor-block-category-tooltip") {
|
||||
return;
|
||||
}
|
||||
|
||||
const tooltips = document.querySelectorAll(".ytp-tooltip-text");
|
||||
for (const tooltip of tooltips) {
|
||||
const splitData = tooltip.textContent.split(":");
|
||||
if (splitData.length === 2 && !isNaN(parseInt(splitData[0])) && !isNaN(parseInt(splitData[1]))) {
|
||||
// Add label
|
||||
const timeInSeconds = parseInt(splitData[0]) * 60 + parseInt(splitData[1]);
|
||||
|
||||
// Find category at that location
|
||||
let category = null;
|
||||
for (let i = 0; i < this.timestamps?.length; i++) {
|
||||
if (this.timestamps[i][0] < timeInSeconds && this.timestamps[i][1] > timeInSeconds){
|
||||
category = this.types[i];
|
||||
}
|
||||
}
|
||||
|
||||
if (category === null && !categoryTooltip.classList.contains("sbHidden")) {
|
||||
categoryTooltip.classList.add("sbHidden");
|
||||
tooltipTextWrapper.classList.remove("sbTooltipTwoTitleThumbnailOffset");
|
||||
tooltipTextWrapper.classList.remove("sbTooltipOneTitleThumbnailOffset");
|
||||
} else if (category !== null) {
|
||||
categoryTooltip.classList.remove("sbHidden");
|
||||
categoryTooltip.textContent = utils.shortCategoryName(category)
|
||||
|| (chrome.i18n.getMessage("preview") + " " + utils.shortCategoryName(category.split("preview-")[1]));
|
||||
|
||||
// There is a title now
|
||||
tooltip.classList.remove("ytp-tooltip-text-no-title");
|
||||
|
||||
// Add the correct offset for the number of titles there are
|
||||
if (titleTooltip.textContent !== "") {
|
||||
if (!tooltipTextWrapper.classList.contains("sbTooltipTwoTitleThumbnailOffset")) {
|
||||
tooltipTextWrapper.classList.add("sbTooltipTwoTitleThumbnailOffset");
|
||||
}
|
||||
} else if (!tooltipTextWrapper.classList.contains("sbTooltipOneTitleThumbnailOffset")) {
|
||||
tooltipTextWrapper.classList.add("sbTooltipOneTitleThumbnailOffset");
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
observer.observe(tooltipTextWrapper, {
|
||||
childList: true,
|
||||
subtree: true
|
||||
});
|
||||
}
|
||||
|
||||
updatePosition(parent: any): void {
|
||||
//below the seek bar
|
||||
// this.parent.insertAdjacentElement("afterEnd", this.container);
|
||||
|
||||
this.parent = parent;
|
||||
|
||||
if (this.onMobileYouTube) {
|
||||
parent.style.backgroundColor = "rgba(255, 255, 255, 0.3)";
|
||||
parent.style.opacity = "1";
|
||||
|
||||
this.container.style.transform = "none";
|
||||
}
|
||||
|
||||
//on the seek bar
|
||||
this.parent.insertAdjacentElement("afterBegin", this.container);
|
||||
}
|
||||
|
||||
updateColor(segment: string, color: string, opacity: string): void {
|
||||
const bars = <NodeListOf<HTMLElement>> document.querySelectorAll('[data-vs-segment-type=' + segment + ']');
|
||||
for (const bar of bars) {
|
||||
bar.style.backgroundColor = color;
|
||||
bar.style.opacity = opacity;
|
||||
}
|
||||
}
|
||||
|
||||
set(timestamps: number[][], types: string[], duration: number): void {
|
||||
while (this.container.firstChild) {
|
||||
this.container.removeChild(this.container.firstChild);
|
||||
}
|
||||
|
||||
if (!timestamps || !types) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.timestamps = timestamps;
|
||||
this.types = types;
|
||||
|
||||
// to avoid rounding error resulting in width more than 100%
|
||||
duration = Math.floor(duration * 100) / 100;
|
||||
let width;
|
||||
for (let i = 0; i < timestamps.length; i++) {
|
||||
if (types[i] == null) continue;
|
||||
|
||||
width = (timestamps[i][1] - timestamps[i][0]) / duration * 100;
|
||||
width = Math.floor(width * 100) / 100;
|
||||
|
||||
const bar = this.createBar();
|
||||
bar.setAttribute('data-vs-segment-type', types[i]);
|
||||
|
||||
bar.style.backgroundColor = Config.config.barTypes[types[i]].color;
|
||||
if (!this.onMobileYouTube) bar.style.opacity = Config.config.barTypes[types[i]].opacity;
|
||||
bar.style.width = width + '%';
|
||||
bar.style.left = (timestamps[i][0] / duration * 100) + "%";
|
||||
bar.style.position = "absolute"
|
||||
|
||||
this.container.insertAdjacentElement("beforeend", bar);
|
||||
}
|
||||
}
|
||||
|
||||
createBar(): HTMLLIElement {
|
||||
const bar = document.createElement('li');
|
||||
bar.classList.add('previewbar');
|
||||
bar.innerHTML = ' ';
|
||||
return bar;
|
||||
}
|
||||
|
||||
remove(): void {
|
||||
this.container.remove();
|
||||
this.container = undefined;
|
||||
}
|
||||
export interface PreviewBarSegment {
|
||||
timestamps: [number, number];
|
||||
category: string;
|
||||
preview: boolean;
|
||||
}
|
||||
|
||||
export default PreviewBar;
|
||||
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.updatePosition(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) {
|
||||
if (seg.timestamps[0] <= timeInSeconds && seg.timestamps[1] > timeInSeconds) {
|
||||
const segmentLength = seg.timestamps[1] - seg.timestamps[0];
|
||||
|
||||
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.preview) {
|
||||
this.categoryTooltip.textContent = chrome.i18n.getMessage("preview") + " " + 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,
|
||||
});
|
||||
}
|
||||
|
||||
updatePosition(parent: HTMLElement): void {
|
||||
this.parent = parent;
|
||||
|
||||
if (this.onMobileYouTube) {
|
||||
parent.style.backgroundColor = "rgba(255, 255, 255, 0.3)";
|
||||
parent.style.opacity = "1";
|
||||
|
||||
this.container.style.transform = "none";
|
||||
}
|
||||
|
||||
// On the seek bar
|
||||
this.parent.prepend(this.container);
|
||||
}
|
||||
|
||||
// TODO: call on config changes
|
||||
updateColor(segmentType: string, color: string, opacity: number): void {
|
||||
const bars = <NodeListOf<HTMLElement>> document.querySelectorAll('[data-vs-segment-type=' + segmentType + ']');
|
||||
|
||||
for (const bar of bars) {
|
||||
bar.style.backgroundColor = color;
|
||||
bar.style.opacity = String(opacity);
|
||||
}
|
||||
}
|
||||
|
||||
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(({timestamps: a}, {timestamps: 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, preview, timestamps}: PreviewBarSegment): HTMLLIElement {
|
||||
const bar = document.createElement('li');
|
||||
bar.classList.add('previewbar');
|
||||
bar.innerHTML = ' ';
|
||||
|
||||
const barSegmentType = (preview ? 'preview-' : '') + category;
|
||||
|
||||
bar.setAttribute('data-vs-segment-type', barSegmentType);
|
||||
|
||||
bar.style.backgroundColor = Config.config.barTypes[barSegmentType].color;
|
||||
if (!this.onMobileYouTube) bar.style.opacity = Config.config.barTypes[barSegmentType].opacity;
|
||||
|
||||
bar.style.position = "absolute";
|
||||
bar.style.width = this.timeToPercentage(timestamps[1] - timestamps[0]);
|
||||
bar.style.left = this.timeToPercentage(timestamps[0]);
|
||||
|
||||
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) + '%';
|
||||
}
|
||||
}
|
||||
|
||||
export default PreviewBar;
|
||||
|
||||
Reference in New Issue
Block a user