mirror of
https://github.com/ajayyy/SponsorBlock.git
synced 2025-12-08 12:37:05 +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:
@@ -11,11 +11,6 @@
|
|||||||
z-index: 40;
|
z-index: 40;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sbHidden {
|
|
||||||
display: none !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
.previewbar {
|
.previewbar {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
@@ -23,12 +18,29 @@
|
|||||||
|
|
||||||
/* Preview Bar page hacks */
|
/* Preview Bar page hacks */
|
||||||
|
|
||||||
.sbTooltipTwoTitleThumbnailOffset {
|
.ytp-tooltip:not(.sponsorCategoryTooltipVisible) .sponsorCategoryTooltip {
|
||||||
bottom: -5px !important;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sbTooltipOneTitleThumbnailOffset {
|
.ytp-tooltip.sponsorCategoryTooltipVisible {
|
||||||
bottom: 10px !important;
|
transform: translateY(-1em);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ytp-big-mode .ytp-tooltip.sponsorCategoryTooltipVisible {
|
||||||
|
transform: translateY(-2em);
|
||||||
|
}
|
||||||
|
|
||||||
|
#movie_player:not(.ytp-big-mode) .ytp-tooltip.sponsorCategoryTooltipVisible > .ytp-tooltip-text-wrapper {
|
||||||
|
transform: translateY(1em);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ytp-big-mode .ytp-tooltip.sponsorCategoryTooltipVisible > .ytp-tooltip-text-wrapper {
|
||||||
|
transform: translateY(0.5em);
|
||||||
|
}
|
||||||
|
|
||||||
|
.ytp-big-mode .ytp-tooltip.sponsorCategoryTooltipVisible > .ytp-tooltip-text-wrapper > .ytp-tooltip-text {
|
||||||
|
display: block;
|
||||||
|
transform: translateY(1em);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* */
|
/* */
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ const utils = new Utils();
|
|||||||
|
|
||||||
import runThePopup from "./popup";
|
import runThePopup from "./popup";
|
||||||
|
|
||||||
import PreviewBar from "./js-components/previewBar";
|
import PreviewBar, {PreviewBarSegment} from "./js-components/previewBar";
|
||||||
import SkipNotice from "./render/SkipNotice";
|
import SkipNotice from "./render/SkipNotice";
|
||||||
import SkipNoticeComponent from "./components/SkipNoticeComponent";
|
import SkipNoticeComponent from "./components/SkipNoticeComponent";
|
||||||
import SubmissionNotice from "./render/SubmissionNotice";
|
import SubmissionNotice from "./render/SubmissionNotice";
|
||||||
@@ -252,7 +252,7 @@ function resetValues() {
|
|||||||
|
|
||||||
//empty the preview bar
|
//empty the preview bar
|
||||||
if (previewBar !== null) {
|
if (previewBar !== null) {
|
||||||
previewBar.set([], [], 0);
|
previewBar.clear();
|
||||||
}
|
}
|
||||||
|
|
||||||
//reset sponsor data found check
|
//reset sponsor data found check
|
||||||
@@ -368,8 +368,6 @@ async function videoIDChange(id) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function handleMobileControlsMutations(): void {
|
function handleMobileControlsMutations(): void {
|
||||||
const mobileYouTubeSelector = ".progress-bar-background";
|
|
||||||
|
|
||||||
updateVisibilityOfPlayerControlsButton().then((createdButtons) => {
|
updateVisibilityOfPlayerControlsButton().then((createdButtons) => {
|
||||||
if (createdButtons) {
|
if (createdButtons) {
|
||||||
if (sponsorTimesSubmitting != null && sponsorTimesSubmitting.length > 0 && sponsorTimesSubmitting[sponsorTimesSubmitting.length - 1].segment.length >= 2) {
|
if (sponsorTimesSubmitting != null && sponsorTimesSubmitting.length > 0 && sponsorTimesSubmitting[sponsorTimesSubmitting.length - 1].segment.length >= 2) {
|
||||||
@@ -384,7 +382,11 @@ function handleMobileControlsMutations(): void {
|
|||||||
|
|
||||||
if (previewBar !== null) {
|
if (previewBar !== null) {
|
||||||
if (document.body.contains(previewBar.container)) {
|
if (document.body.contains(previewBar.container)) {
|
||||||
updatePreviewBarPositionMobile(document.getElementsByClassName(mobileYouTubeSelector)[0]);
|
const progressBarBackground = document.querySelector<HTMLElement>(".progress-bar-background");
|
||||||
|
|
||||||
|
if (progressBarBackground !== null) {
|
||||||
|
updatePreviewBarPositionMobile(progressBarBackground);
|
||||||
|
}
|
||||||
|
|
||||||
return;
|
return;
|
||||||
} else {
|
} else {
|
||||||
@@ -415,10 +417,10 @@ function createPreviewBar(): void {
|
|||||||
];
|
];
|
||||||
|
|
||||||
for (const selector of progressElementSelectors) {
|
for (const selector of progressElementSelectors) {
|
||||||
const el = document.querySelectorAll(selector);
|
const el = document.querySelector<HTMLElement>(selector);
|
||||||
|
|
||||||
if (el && el.length && el[0]) {
|
if (el) {
|
||||||
previewBar = new PreviewBar(el[0], onMobileYouTube, onInvidious);
|
previewBar = new PreviewBar(el, onMobileYouTube, onInvidious);
|
||||||
|
|
||||||
updatePreviewBar();
|
updatePreviewBar();
|
||||||
|
|
||||||
@@ -819,46 +821,58 @@ function getYouTubeVideoID(url: string) {
|
|||||||
/**
|
/**
|
||||||
* This function is required on mobile YouTube and will keep getting called whenever the preview bar disapears
|
* This function is required on mobile YouTube and will keep getting called whenever the preview bar disapears
|
||||||
*/
|
*/
|
||||||
function updatePreviewBarPositionMobile(parent: Element) {
|
function updatePreviewBarPositionMobile(parent: HTMLElement) {
|
||||||
if (document.getElementById("previewbar") === null) {
|
if (document.getElementById("previewbar") === null) {
|
||||||
previewBar.updatePosition(parent);
|
previewBar.updatePosition(parent);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function updatePreviewBar() {
|
function updatePreviewBar() {
|
||||||
if(isAdPlaying) {
|
if (previewBar === null) return;
|
||||||
previewBar.set([], [], 0);
|
|
||||||
|
if (isAdPlaying) {
|
||||||
|
previewBar.clear();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (previewBar === null || video === null) return;
|
if (video === null) return;
|
||||||
|
|
||||||
let localSponsorTimes = sponsorTimes;
|
const previewBarSegments: PreviewBarSegment[] = [];
|
||||||
if (localSponsorTimes == null) localSponsorTimes = [];
|
|
||||||
|
|
||||||
const allSponsorTimes = localSponsorTimes.concat(sponsorTimesSubmitting);
|
if (sponsorTimes) {
|
||||||
|
sponsorTimes.forEach((segment) => {
|
||||||
|
if (segment.hidden !== SponsorHideType.Visible) return;
|
||||||
|
|
||||||
//create an array of the sponsor types
|
previewBarSegments.push({
|
||||||
const types = [];
|
timestamps: segment.segment as [number, number],
|
||||||
for (let i = 0; i < localSponsorTimes.length; i++) {
|
category: segment.category,
|
||||||
if (localSponsorTimes[i].hidden === SponsorHideType.Visible) {
|
preview: false,
|
||||||
types.push(localSponsorTimes[i].category);
|
});
|
||||||
} else {
|
});
|
||||||
// Don't show this sponsor
|
|
||||||
types.push(null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for (let i = 0; i < sponsorTimesSubmitting.length; i++) {
|
|
||||||
types.push("preview-" + sponsorTimesSubmitting[i].category);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
previewBar.set(utils.getSegmentsFromSponsorTimes(allSponsorTimes), types, video.duration)
|
sponsorTimesSubmitting.forEach((segment) => {
|
||||||
|
previewBarSegments.push({
|
||||||
|
timestamps: segment.segment as [number, number],
|
||||||
|
category: segment.category,
|
||||||
|
preview: true,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
previewBar.set(previewBarSegments, video.duration)
|
||||||
|
|
||||||
if (Config.config.showTimeWithSkips) {
|
if (Config.config.showTimeWithSkips) {
|
||||||
showTimeWithoutSkips(allSponsorTimes);
|
const skippedSegments = previewBarSegments.filter((segment) => {
|
||||||
|
// Count the segment only if the category is autoskipped
|
||||||
|
return utils.getCategorySelection(segment.category)?.option === CategorySkipOption.AutoSkip;
|
||||||
|
});
|
||||||
|
|
||||||
|
const skippedDuration = utils.getTimestampsDuration(skippedSegments.map(({timestamps}) => timestamps));
|
||||||
|
|
||||||
|
showTimeWithoutSkips(skippedDuration);
|
||||||
}
|
}
|
||||||
|
|
||||||
//update last video id
|
// Update last video id
|
||||||
lastPreviewBarUpdate = sponsorVideoID;
|
lastPreviewBarUpdate = sponsorVideoID;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1626,31 +1640,22 @@ function updateAdFlag() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function showTimeWithoutSkips(allSponsorTimes): void {
|
function showTimeWithoutSkips(skippedDuration: number): void {
|
||||||
if (onMobileYouTube || onInvidious) return;
|
if (onMobileYouTube || onInvidious) return;
|
||||||
|
|
||||||
let skipDuration = 0;
|
if (isNaN(skippedDuration) || skippedDuration < 0) {
|
||||||
|
skippedDuration = 0;
|
||||||
// Calculate skipDuration based from the segments in the preview bar
|
|
||||||
for (let i = 0; i < allSponsorTimes.length; i++) {
|
|
||||||
// If an end time exists
|
|
||||||
if (allSponsorTimes[i].segment[1]) {
|
|
||||||
skipDuration += allSponsorTimes[i].segment[1] - allSponsorTimes[i].segment[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// YouTube player time display
|
// YouTube player time display
|
||||||
const display = document.getElementsByClassName("ytp-time-display notranslate")[0];
|
const display = document.querySelector(".ytp-time-display.notranslate");
|
||||||
if (!display) return;
|
if (!display) return;
|
||||||
|
|
||||||
const formatedTime = utils.getFormattedTime(video.duration - skipDuration);
|
|
||||||
|
|
||||||
const durationID = "sponsorBlockDurationAfterSkips";
|
const durationID = "sponsorBlockDurationAfterSkips";
|
||||||
let duration = document.getElementById(durationID);
|
let duration = document.getElementById(durationID);
|
||||||
|
|
||||||
// Create span if needed
|
// Create span if needed
|
||||||
if(duration === null) {
|
if (duration === null) {
|
||||||
duration = document.createElement('span');
|
duration = document.createElement('span');
|
||||||
duration.id = durationID;
|
duration.id = durationID;
|
||||||
duration.classList.add("ytp-time-duration");
|
duration.classList.add("ytp-time-duration");
|
||||||
@@ -1658,5 +1663,5 @@ function showTimeWithoutSkips(allSponsorTimes): void {
|
|||||||
display.appendChild(duration);
|
display.appendChild(duration);
|
||||||
}
|
}
|
||||||
|
|
||||||
duration.innerText = (skipDuration <= 0 || isNaN(skipDuration) || formatedTime.includes("NaN")) ? "" : " ("+formatedTime+")";
|
duration.innerText = skippedDuration <= 0 ? "" : " (" + utils.getFormattedTime(video.duration - skippedDuration) + ")";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
/*
|
/*
|
||||||
This is based on code from VideoSegments.
|
This is based on code from VideoSegments.
|
||||||
https://github.com/videosegments/videosegments/commits/f1e111bdfe231947800c6efdd51f62a4e7fef4d4/segmentsbar/segmentsbar.js
|
https://github.com/videosegments/videosegments/commits/f1e111bdfe231947800c6efdd51f62a4e7fef4d4/segmentsbar/segmentsbar.js
|
||||||
*/
|
*/
|
||||||
|
|
||||||
'use strict';
|
'use strict';
|
||||||
@@ -9,20 +9,31 @@ import Config from "../config";
|
|||||||
import Utils from "../utils";
|
import Utils from "../utils";
|
||||||
const utils = new Utils();
|
const utils = new Utils();
|
||||||
|
|
||||||
|
const TOOLTIP_VISIBLE_CLASS = 'sponsorCategoryTooltipVisible';
|
||||||
|
|
||||||
|
export interface PreviewBarSegment {
|
||||||
|
timestamps: [number, number];
|
||||||
|
category: string;
|
||||||
|
preview: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
class PreviewBar {
|
class PreviewBar {
|
||||||
container: HTMLUListElement;
|
container: HTMLUListElement;
|
||||||
parent: any;
|
categoryTooltip?: HTMLDivElement;
|
||||||
|
tooltipContainer?: HTMLElement;
|
||||||
|
|
||||||
|
parent: HTMLElement;
|
||||||
onMobileYouTube: boolean;
|
onMobileYouTube: boolean;
|
||||||
onInvidious: boolean;
|
onInvidious: boolean;
|
||||||
|
|
||||||
timestamps: number[][];
|
segments: PreviewBarSegment[] = [];
|
||||||
types: string[];
|
videoDuration = 0;
|
||||||
|
|
||||||
constructor(parent: any, onMobileYouTube: boolean, onInvidious: boolean) {
|
constructor(parent: HTMLElement, onMobileYouTube: boolean, onInvidious: boolean) {
|
||||||
this.container = document.createElement('ul');
|
this.container = document.createElement('ul');
|
||||||
this.container.id = 'previewbar';
|
this.container.id = 'previewbar';
|
||||||
this.parent = parent;
|
|
||||||
|
|
||||||
|
this.parent = parent;
|
||||||
this.onMobileYouTube = onMobileYouTube;
|
this.onMobileYouTube = onMobileYouTube;
|
||||||
this.onInvidious = onInvidious;
|
this.onInvidious = onInvidious;
|
||||||
|
|
||||||
@@ -34,88 +45,96 @@ class PreviewBar {
|
|||||||
setupHoverText(): void {
|
setupHoverText(): void {
|
||||||
if (this.onMobileYouTube || this.onInvidious) return;
|
if (this.onMobileYouTube || this.onInvidious) return;
|
||||||
|
|
||||||
const seekBar = document.querySelector(".ytp-progress-bar-container");
|
|
||||||
|
|
||||||
// Create label placeholder
|
// Create label placeholder
|
||||||
const tooltipTextWrapper = document.querySelector(".ytp-tooltip-text-wrapper");
|
this.categoryTooltip = document.createElement("div");
|
||||||
const titleTooltip = document.querySelector(".ytp-tooltip-title");
|
this.categoryTooltip.className = "ytp-tooltip-title sponsorCategoryTooltip";
|
||||||
const categoryTooltip = document.createElement("div");
|
|
||||||
categoryTooltip.className = "sbHidden ytp-tooltip-title";
|
|
||||||
categoryTooltip.id = "sponsor-block-category-tooltip"
|
|
||||||
|
|
||||||
tooltipTextWrapper.insertBefore(categoryTooltip, titleTooltip.nextSibling);
|
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;
|
let mouseOnSeekBar = false;
|
||||||
|
|
||||||
seekBar.addEventListener("mouseenter", (event) => {
|
seekBar.addEventListener("mouseenter", () => {
|
||||||
mouseOnSeekBar = true;
|
mouseOnSeekBar = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
seekBar.addEventListener("mouseleave", (event) => {
|
seekBar.addEventListener("mouseleave", () => {
|
||||||
mouseOnSeekBar = false;
|
mouseOnSeekBar = false;
|
||||||
categoryTooltip.classList.add("sbHidden");
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const observer = new MutationObserver((mutations, observer) => {
|
const observer = new MutationObserver((mutations) => {
|
||||||
if (!mouseOnSeekBar) return;
|
if (!mouseOnSeekBar || !this.categoryTooltip || !this.tooltipContainer) return;
|
||||||
|
|
||||||
// See if mutation observed is only this ID (if so, ignore)
|
// If the mutation observed is only for our tooltip text, ignore
|
||||||
if (mutations.length == 1 && (mutations[0].target as HTMLElement).id === "sponsor-block-category-tooltip") {
|
if (mutations.length === 1 && (mutations[0].target as HTMLElement).classList.contains("sponsorCategoryTooltip")) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const tooltips = document.querySelectorAll(".ytp-tooltip-text");
|
const tooltipTextElements = tooltipTextWrapper.querySelectorAll(".ytp-tooltip-text");
|
||||||
for (const tooltip of tooltips) {
|
let timeInSeconds: number | null = null;
|
||||||
const splitData = tooltip.textContent.split(":");
|
let noYoutubeChapters = false;
|
||||||
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
|
for (const tooltipTextElement of tooltipTextElements) {
|
||||||
let category = null;
|
if (tooltipTextElement.classList.contains('ytp-tooltip-text-no-title')) noYoutubeChapters = true;
|
||||||
for (let i = 0; i < this.timestamps?.length; i++) {
|
|
||||||
if (this.timestamps[i][0] < timeInSeconds && this.timestamps[i][1] > timeInSeconds){
|
const tooltipText = tooltipTextElement.textContent;
|
||||||
category = this.types[i];
|
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 (category === null && !categoryTooltip.classList.contains("sbHidden")) {
|
if (segment === null && this.tooltipContainer.classList.contains(TOOLTIP_VISIBLE_CLASS)) {
|
||||||
categoryTooltip.classList.add("sbHidden");
|
this.tooltipContainer.classList.remove(TOOLTIP_VISIBLE_CLASS);
|
||||||
tooltipTextWrapper.classList.remove("sbTooltipTwoTitleThumbnailOffset");
|
} else if (segment !== null) {
|
||||||
tooltipTextWrapper.classList.remove("sbTooltipOneTitleThumbnailOffset");
|
this.tooltipContainer.classList.add(TOOLTIP_VISIBLE_CLASS);
|
||||||
} 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
|
if (segment.preview) {
|
||||||
tooltip.classList.remove("ytp-tooltip-text-no-title");
|
this.categoryTooltip.textContent = chrome.i18n.getMessage("preview") + " " + utils.shortCategoryName(segment.category);
|
||||||
|
} else {
|
||||||
// Add the correct offset for the number of titles there are
|
this.categoryTooltip.textContent = utils.shortCategoryName(segment.category);
|
||||||
if (titleTooltip.textContent !== "") {
|
|
||||||
if (!tooltipTextWrapper.classList.contains("sbTooltipTwoTitleThumbnailOffset")) {
|
|
||||||
tooltipTextWrapper.classList.add("sbTooltipTwoTitleThumbnailOffset");
|
|
||||||
}
|
|
||||||
} else if (!tooltipTextWrapper.classList.contains("sbTooltipOneTitleThumbnailOffset")) {
|
|
||||||
tooltipTextWrapper.classList.add("sbTooltipOneTitleThumbnailOffset");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
break;
|
// 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, {
|
observer.observe(tooltipTextWrapper, {
|
||||||
childList: true,
|
childList: true,
|
||||||
subtree: true
|
subtree: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
updatePosition(parent: any): void {
|
updatePosition(parent: HTMLElement): void {
|
||||||
//below the seek bar
|
|
||||||
// this.parent.insertAdjacentElement("afterEnd", this.container);
|
|
||||||
|
|
||||||
this.parent = parent;
|
this.parent = parent;
|
||||||
|
|
||||||
if (this.onMobileYouTube) {
|
if (this.onMobileYouTube) {
|
||||||
@@ -125,62 +144,82 @@ class PreviewBar {
|
|||||||
this.container.style.transform = "none";
|
this.container.style.transform = "none";
|
||||||
}
|
}
|
||||||
|
|
||||||
//on the seek bar
|
// On the seek bar
|
||||||
this.parent.insertAdjacentElement("afterBegin", this.container);
|
this.parent.prepend(this.container);
|
||||||
}
|
}
|
||||||
|
|
||||||
updateColor(segment: string, color: string, opacity: string): void {
|
// TODO: call on config changes
|
||||||
const bars = <NodeListOf<HTMLElement>> document.querySelectorAll('[data-vs-segment-type=' + segment + ']');
|
updateColor(segmentType: string, color: string, opacity: number): void {
|
||||||
|
const bars = <NodeListOf<HTMLElement>> document.querySelectorAll('[data-vs-segment-type=' + segmentType + ']');
|
||||||
|
|
||||||
for (const bar of bars) {
|
for (const bar of bars) {
|
||||||
bar.style.backgroundColor = color;
|
bar.style.backgroundColor = color;
|
||||||
bar.style.opacity = opacity;
|
bar.style.opacity = String(opacity);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
set(timestamps: number[][], types: string[], duration: number): void {
|
clear(): void {
|
||||||
|
this.videoDuration = 0;
|
||||||
|
this.segments = [];
|
||||||
|
|
||||||
while (this.container.firstChild) {
|
while (this.container.firstChild) {
|
||||||
this.container.removeChild(this.container.firstChild);
|
this.container.removeChild(this.container.firstChild);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!timestamps || !types) {
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.timestamps = timestamps;
|
set(segments: PreviewBarSegment[], videoDuration: number): void {
|
||||||
this.types = types;
|
this.clear();
|
||||||
|
|
||||||
// to avoid rounding error resulting in width more than 100%
|
if (!segments) return;
|
||||||
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;
|
this.segments = segments;
|
||||||
width = Math.floor(width * 100) / 100;
|
this.videoDuration = videoDuration;
|
||||||
|
|
||||||
const bar = this.createBar();
|
this.segments.sort(({timestamps: a}, {timestamps: b}) => {
|
||||||
bar.setAttribute('data-vs-segment-type', types[i]);
|
// 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);
|
||||||
|
|
||||||
bar.style.backgroundColor = Config.config.barTypes[types[i]].color;
|
this.container.appendChild(bar);
|
||||||
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 {
|
createBar({category, preview, timestamps}: PreviewBarSegment): HTMLLIElement {
|
||||||
const bar = document.createElement('li');
|
const bar = document.createElement('li');
|
||||||
bar.classList.add('previewbar');
|
bar.classList.add('previewbar');
|
||||||
bar.innerHTML = ' ';
|
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;
|
return bar;
|
||||||
}
|
}
|
||||||
|
|
||||||
remove(): void {
|
remove(): void {
|
||||||
this.container.remove();
|
this.container.remove();
|
||||||
this.container = undefined;
|
|
||||||
|
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) + '%';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
53
src/utils.ts
53
src/utils.ts
@@ -158,17 +158,54 @@ class Utils {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets just the timestamps from a sponsorTimes array
|
* Merges any overlapping timestamp ranges into single segments and returns them as a new array.
|
||||||
*
|
|
||||||
* @param sponsorTimes
|
|
||||||
*/
|
*/
|
||||||
getSegmentsFromSponsorTimes(sponsorTimes: SponsorTime[]): number[][] {
|
getMergedTimestamps(timestamps: number[][]): [number, number][] {
|
||||||
const segments: number[][] = [];
|
let deduped: [number, number][] = [];
|
||||||
for (const sponsorTime of sponsorTimes) {
|
|
||||||
segments.push(sponsorTime.segment);
|
// Cases ([] = another segment, <> = current range):
|
||||||
|
// [<]>, <[>], <[]>, [<>], [<][>]
|
||||||
|
timestamps.forEach((range) => {
|
||||||
|
// Find segments the current range overlaps
|
||||||
|
const startOverlaps = deduped.findIndex((other) => range[0] >= other[0] && range[0] <= other[1]);
|
||||||
|
const endOverlaps = deduped.findIndex((other) => range[1] >= other[0] && range[1] <= other[1]);
|
||||||
|
|
||||||
|
if (~startOverlaps && ~endOverlaps) {
|
||||||
|
// [<][>] Both the start and end of this range overlap another segment
|
||||||
|
// [<>] This range is already entirely contained within an existing segment
|
||||||
|
if (startOverlaps === endOverlaps) return;
|
||||||
|
|
||||||
|
// Remove the range with the higher index first to avoid the index shifting
|
||||||
|
const other1 = deduped.splice(Math.max(startOverlaps, endOverlaps), 1)[0];
|
||||||
|
const other2 = deduped.splice(Math.min(startOverlaps, endOverlaps), 1)[0];
|
||||||
|
|
||||||
|
// Insert a new segment spanning the start and end of the range
|
||||||
|
deduped.push([Math.min(other1[0], other2[0]), Math.max(other1[1], other2[1])]);
|
||||||
|
} else if (~startOverlaps) {
|
||||||
|
// [<]> The start of this range overlaps another segment, extend its end
|
||||||
|
deduped[startOverlaps][1] = range[1];
|
||||||
|
} else if (~endOverlaps) {
|
||||||
|
// <[>] The end of this range overlaps another segment, extend its beginning
|
||||||
|
deduped[endOverlaps][0] = range[0];
|
||||||
|
} else {
|
||||||
|
// No overlaps, just push in a copy
|
||||||
|
deduped.push(range.slice() as [number, number]);
|
||||||
}
|
}
|
||||||
|
|
||||||
return segments;
|
// <[]> Remove other segments contained within this range
|
||||||
|
deduped = deduped.filter((other) => !(other[0] > range[0] && other[1] < range[1]));
|
||||||
|
});
|
||||||
|
|
||||||
|
return deduped;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the total duration of the timestamps, taking into account overlaps.
|
||||||
|
*/
|
||||||
|
getTimestampsDuration(timestamps: number[][]): number {
|
||||||
|
return this.getMergedTimestamps(timestamps).reduce((acc, range) => {
|
||||||
|
return acc + range[1] - range[0];
|
||||||
|
}, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
getSponsorIndexFromUUID(sponsorTimes: SponsorTime[], UUID: string): number {
|
getSponsorIndexFromUUID(sponsorTimes: SponsorTime[], UUID: string): number {
|
||||||
|
|||||||
Reference in New Issue
Block a user