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:
opl-
2020-11-08 06:09:51 +01:00
parent 59826aae6d
commit 7078e1f033
4 changed files with 345 additions and 252 deletions

View File

@@ -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);
} }
/* */ /* */

View File

@@ -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,37 +1640,28 @@ 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 // YouTube player time display
for (let i = 0; i < allSponsorTimes.length; i++) { const display = document.querySelector(".ytp-time-display.notranslate");
// If an end time exists if (!display) return;
if (allSponsorTimes[i].segment[1]) {
skipDuration += allSponsorTimes[i].segment[1] - allSponsorTimes[i].segment[0];
}
} const durationID = "sponsorBlockDurationAfterSkips";
// YouTube player time display
const display = document.getElementsByClassName("ytp-time-display notranslate")[0];
if (!display) return;
const formatedTime = utils.getFormattedTime(video.duration - skipDuration);
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");
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) + ")";
} }

View File

@@ -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,179 +9,218 @@ 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;
onMobileYouTube: boolean; tooltipContainer?: HTMLElement;
onInvidious: boolean;
timestamps: number[][]; parent: HTMLElement;
types: string[]; onMobileYouTube: boolean;
onInvidious: boolean;
constructor(parent: any, onMobileYouTube: boolean, onInvidious: boolean) { segments: PreviewBarSegment[] = [];
this.container = document.createElement('ul'); videoDuration = 0;
this.container.id = 'previewbar';
this.parent = parent;
this.onMobileYouTube = onMobileYouTube; constructor(parent: HTMLElement, onMobileYouTube: boolean, onInvidious: boolean) {
this.onInvidious = onInvidious; this.container = document.createElement('ul');
this.container.id = 'previewbar';
this.updatePosition(parent); this.parent = parent;
this.onMobileYouTube = onMobileYouTube;
this.onInvidious = onInvidious;
this.setupHoverText(); this.updatePosition(parent);
}
setupHoverText(): void { this.setupHoverText();
if (this.onMobileYouTube || this.onInvidious) return; }
const seekBar = document.querySelector(".ytp-progress-bar-container"); setupHoverText(): void {
if (this.onMobileYouTube || this.onInvidious) return;
// 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;
let mouseOnSeekBar = false; // 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;
seekBar.addEventListener("mouseenter", (event) => { tooltipTextWrapper.insertBefore(this.categoryTooltip, titleTooltip.nextSibling);
mouseOnSeekBar = true;
});
seekBar.addEventListener("mouseleave", (event) => { const seekBar = document.querySelector(".ytp-progress-bar-container");
mouseOnSeekBar = false; if (!seekBar) return;
categoryTooltip.classList.add("sbHidden");
});
const observer = new MutationObserver((mutations, observer) => { let mouseOnSeekBar = false;
if (!mouseOnSeekBar) return;
// See if mutation observed is only this ID (if so, ignore) seekBar.addEventListener("mouseenter", () => {
if (mutations.length == 1 && (mutations[0].target as HTMLElement).id === "sponsor-block-category-tooltip") { mouseOnSeekBar = true;
return; });
}
const tooltips = document.querySelectorAll(".ytp-tooltip-text"); seekBar.addEventListener("mouseleave", () => {
for (const tooltip of tooltips) { mouseOnSeekBar = false;
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 const observer = new MutationObserver((mutations) => {
let category = null; if (!mouseOnSeekBar || !this.categoryTooltip || !this.tooltipContainer) return;
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")) { // If the mutation observed is only for our tooltip text, ignore
categoryTooltip.classList.add("sbHidden"); if (mutations.length === 1 && (mutations[0].target as HTMLElement).classList.contains("sponsorCategoryTooltip")) {
tooltipTextWrapper.classList.remove("sbTooltipTwoTitleThumbnailOffset"); return;
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 const tooltipTextElements = tooltipTextWrapper.querySelectorAll(".ytp-tooltip-text");
tooltip.classList.remove("ytp-tooltip-text-no-title"); let timeInSeconds: number | null = null;
let noYoutubeChapters = false;
// Add the correct offset for the number of titles there are for (const tooltipTextElement of tooltipTextElements) {
if (titleTooltip.textContent !== "") { if (tooltipTextElement.classList.contains('ytp-tooltip-text-no-title')) noYoutubeChapters = true;
if (!tooltipTextWrapper.classList.contains("sbTooltipTwoTitleThumbnailOffset")) {
tooltipTextWrapper.classList.add("sbTooltipTwoTitleThumbnailOffset");
}
} else if (!tooltipTextWrapper.classList.contains("sbTooltipOneTitleThumbnailOffset")) {
tooltipTextWrapper.classList.add("sbTooltipOneTitleThumbnailOffset");
}
}
break; const tooltipText = tooltipTextElement.textContent;
} if (tooltipText === null || tooltipText.length === 0) continue;
}
});
observer.observe(tooltipTextWrapper, { timeInSeconds = utils.getFormattedTimeToSeconds(tooltipText);
childList: true,
subtree: true
});
}
updatePosition(parent: any): void { if (timeInSeconds !== null) break;
//below the seek bar }
// this.parent.insertAdjacentElement("afterEnd", this.container);
this.parent = parent; if (timeInSeconds === null) return;
if (this.onMobileYouTube) { // Find the segment at that location, using the shortest if multiple found
parent.style.backgroundColor = "rgba(255, 255, 255, 0.3)"; let segment: PreviewBarSegment | null = null;
parent.style.opacity = "1"; let currentSegmentLength = Infinity;
this.container.style.transform = "none"; for (const seg of this.segments) {
} if (seg.timestamps[0] <= timeInSeconds && seg.timestamps[1] > timeInSeconds) {
const segmentLength = seg.timestamps[1] - seg.timestamps[0];
//on the seek bar if (segmentLength < currentSegmentLength) {
this.parent.insertAdjacentElement("afterBegin", this.container); currentSegmentLength = segmentLength;
} segment = seg;
}
}
}
updateColor(segment: string, color: string, opacity: string): void { if (segment === null && this.tooltipContainer.classList.contains(TOOLTIP_VISIBLE_CLASS)) {
const bars = <NodeListOf<HTMLElement>> document.querySelectorAll('[data-vs-segment-type=' + segment + ']'); this.tooltipContainer.classList.remove(TOOLTIP_VISIBLE_CLASS);
for (const bar of bars) { } else if (segment !== null) {
bar.style.backgroundColor = color; this.tooltipContainer.classList.add(TOOLTIP_VISIBLE_CLASS);
bar.style.opacity = opacity;
}
}
set(timestamps: number[][], types: string[], duration: number): void { if (segment.preview) {
while (this.container.firstChild) { this.categoryTooltip.textContent = chrome.i18n.getMessage("preview") + " " + utils.shortCategoryName(segment.category);
this.container.removeChild(this.container.firstChild); } else {
} this.categoryTooltip.textContent = utils.shortCategoryName(segment.category);
}
if (!timestamps || !types) { // Use the class if the timestamp text uses it to prevent overlapping
return; this.categoryTooltip.classList.toggle("ytp-tooltip-text-no-title", noYoutubeChapters);
} }
});
this.timestamps = timestamps; observer.observe(tooltipTextWrapper, {
this.types = types; childList: true,
subtree: true,
});
}
// to avoid rounding error resulting in width more than 100% updatePosition(parent: HTMLElement): void {
duration = Math.floor(duration * 100) / 100; this.parent = parent;
let width;
for (let i = 0; i < timestamps.length; i++) {
if (types[i] == null) continue;
width = (timestamps[i][1] - timestamps[i][0]) / duration * 100; if (this.onMobileYouTube) {
width = Math.floor(width * 100) / 100; parent.style.backgroundColor = "rgba(255, 255, 255, 0.3)";
parent.style.opacity = "1";
const bar = this.createBar(); this.container.style.transform = "none";
bar.setAttribute('data-vs-segment-type', types[i]); }
bar.style.backgroundColor = Config.config.barTypes[types[i]].color; // On the seek bar
if (!this.onMobileYouTube) bar.style.opacity = Config.config.barTypes[types[i]].opacity; this.parent.prepend(this.container);
bar.style.width = width + '%'; }
bar.style.left = (timestamps[i][0] / duration * 100) + "%";
bar.style.position = "absolute"
this.container.insertAdjacentElement("beforeend", bar); // TODO: call on config changes
} updateColor(segmentType: string, color: string, opacity: number): void {
} const bars = <NodeListOf<HTMLElement>> document.querySelectorAll('[data-vs-segment-type=' + segmentType + ']');
createBar(): HTMLLIElement { for (const bar of bars) {
const bar = document.createElement('li'); bar.style.backgroundColor = color;
bar.classList.add('previewbar'); bar.style.opacity = String(opacity);
bar.innerHTML = '&nbsp;'; }
return bar; }
}
remove(): void { clear(): void {
this.container.remove(); this.videoDuration = 0;
this.container = undefined; 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 = '&nbsp;';
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; export default PreviewBar;

View File

@@ -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);
}
return segments; // 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]);
}
// <[]> 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 {