diff options
author | Ajay Ramachandran <[email protected]> | 2020-12-29 22:51:50 -0500 |
---|---|---|
committer | GitHub <[email protected]> | 2020-12-29 22:51:50 -0500 |
commit | 06f09f5fd96c4273ea3eb22e146dd900a7a5bb8d (patch) | |
tree | 54806c1dd74b8383ad09f17f5080184da8fcb3a4 | |
parent | 6d1c51e7ec13c7cae4fd76c0861fe7f8ae5f6c53 (diff) | |
parent | 83a9526e5263403822e5f31a6dbe2a174123c859 (diff) | |
download | SponsorBlock-06f09f5fd96c4273ea3eb22e146dd900a7a5bb8d.tar.gz SponsorBlock-06f09f5fd96c4273ea3eb22e146dd900a7a5bb8d.zip |
Merge pull request #549 from opl-/feat/preview-bar-cleanup
Clean up Preview Bar
-rw-r--r-- | public/content.css | 30 | ||||
-rw-r--r-- | src/content.ts | 114 | ||||
-rw-r--r-- | src/js-components/previewBar.ts | 389 | ||||
-rw-r--r-- | src/utils.ts | 55 |
4 files changed, 338 insertions, 250 deletions
diff --git a/public/content.css b/public/content.css index 62420f2c..a9a6c6eb 100644 --- a/public/content.css +++ b/public/content.css @@ -11,11 +11,6 @@ z-index: 40; } -.sbHidden { - display: none !important; -} - - .previewbar { display: inline-block; height: 100%; @@ -23,12 +18,29 @@ /* Preview Bar page hacks */ -.sbTooltipTwoTitleThumbnailOffset { - bottom: -5px !important; +.ytp-tooltip:not(.sponsorCategoryTooltipVisible) .sponsorCategoryTooltip { + display: none !important; +} + +.ytp-tooltip.sponsorCategoryTooltipVisible { + transform: translateY(-1em) !important; +} + +.ytp-big-mode .ytp-tooltip.sponsorCategoryTooltipVisible { + transform: translateY(-2em) !important; +} + +#movie_player:not(.ytp-big-mode) .ytp-tooltip.sponsorCategoryTooltipVisible > .ytp-tooltip-text-wrapper { + transform: translateY(1em) !important; +} + +.ytp-big-mode .ytp-tooltip.sponsorCategoryTooltipVisible > .ytp-tooltip-text-wrapper { + transform: translateY(0.5em) !important; } -.sbTooltipOneTitleThumbnailOffset { - bottom: 10px !important; +.ytp-big-mode .ytp-tooltip.sponsorCategoryTooltipVisible > .ytp-tooltip-text-wrapper > .ytp-tooltip-text { + display: block !important; + transform: translateY(1em) !important; } /* */ diff --git a/src/content.ts b/src/content.ts index 91295952..aba53f24 100644 --- a/src/content.ts +++ b/src/content.ts @@ -8,7 +8,7 @@ const utils = new Utils(); import runThePopup from "./popup"; -import PreviewBar from "./js-components/previewBar"; +import PreviewBar, {PreviewBarSegment} from "./js-components/previewBar"; import SkipNotice from "./render/SkipNotice"; import SkipNoticeComponent from "./components/SkipNoticeComponent"; import SubmissionNotice from "./render/SubmissionNotice"; @@ -234,7 +234,7 @@ function resetValues() { //empty the preview bar if (previewBar !== null) { - previewBar.set([], [], 0); + previewBar.clear(); } //reset sponsor data found check @@ -358,11 +358,13 @@ async function videoIDChange(id) { } function handleMobileControlsMutations(): void { - const mobileYouTubeSelector = ".progress-bar-background"; - if (previewBar !== null) { if (document.body.contains(previewBar.container)) { - updatePreviewBarPositionMobile(document.getElementsByClassName(mobileYouTubeSelector)[0] as HTMLElement); + const progressBarBackground = document.querySelector<HTMLElement>(".progress-bar-background"); + + if (progressBarBackground !== null) { + updatePreviewBarPositionMobile(progressBarBackground); + } return; } else { @@ -393,11 +395,11 @@ function createPreviewBar(): void { ]; for (const selector of progressElementSelectors) { - const el = document.querySelectorAll(selector); + const el = document.querySelector<HTMLElement>(selector); + + if (el) { + previewBar = new PreviewBar(el, onMobileYouTube, onInvidious); - if (el && el.length && el[0]) { - previewBar = new PreviewBar(el[0] as HTMLElement, onMobileYouTube, onInvidious); - updatePreviewBar(); break; @@ -812,39 +814,46 @@ function updatePreviewBarPositionMobile(parent: HTMLElement) { } function updatePreviewBar(): void { - if(isAdPlaying) { - previewBar.set([], [], 0); + if (previewBar === null) return; + + if (isAdPlaying) { + previewBar.clear(); return; } - if (previewBar === null || video === null) return; + if (video === null) return; - let localSponsorTimes = sponsorTimes; - if (localSponsorTimes == null) localSponsorTimes = []; + const previewBarSegments: PreviewBarSegment[] = []; - const allSponsorTimes = localSponsorTimes.concat(sponsorTimesSubmitting); - - //create an array of the sponsor types - const types = []; - for (let i = 0; i < localSponsorTimes.length; i++) { - if (localSponsorTimes[i].hidden === SponsorHideType.Visible) { - 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); + if (sponsorTimes) { + sponsorTimes.forEach((segment) => { + if (segment.hidden !== SponsorHideType.Visible) return; + + previewBarSegments.push({ + segment: segment.segment as [number, number], + category: segment.category, + preview: false, + }); + }); } - previewBar.set(utils.getSegmentsFromSponsorTimes(allSponsorTimes), types, video.duration) + sponsorTimesSubmitting.forEach((segment) => { + previewBarSegments.push({ + segment: segment.segment as [number, number], + category: segment.category, + preview: true, + }); + }); + + previewBar.set(previewBarSegments, video.duration) if (Config.config.showTimeWithSkips) { - showTimeWithoutSkips(allSponsorTimes); + const skippedDuration = utils.getTimestampsDuration(previewBarSegments.map(({segment}) => segment)); + + showTimeWithoutSkips(skippedDuration); } - //update last video id + // Update last video id lastPreviewBarUpdate = sponsorVideoID; } @@ -1614,37 +1623,28 @@ function updateAdFlag(): void { } } -function showTimeWithoutSkips(allSponsorTimes): void { +function showTimeWithoutSkips(skippedDuration: number): void { if (onMobileYouTube || onInvidious) return; - let skipDuration = 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 - const display = document.getElementsByClassName("ytp-time-display notranslate")[0]; - if (!display) return; - - const formatedTime = utils.getFormattedTime(video.duration - skipDuration); - - const durationID = "sponsorBlockDurationAfterSkips"; + if (isNaN(skippedDuration) || skippedDuration < 0) { + skippedDuration = 0; + } + + // YouTube player time display + const display = document.querySelector(".ytp-time-display.notranslate"); + if (!display) return; + + const durationID = "sponsorBlockDurationAfterSkips"; let duration = document.getElementById(durationID); - // Create span if needed - if(duration === null) { - duration = document.createElement('span'); + // Create span if needed + if (duration === null) { + duration = document.createElement('span'); duration.id = durationID; duration.classList.add("ytp-time-duration"); - display.appendChild(duration); - } - - duration.innerText = (skipDuration <= 0 || isNaN(skipDuration) || formatedTime.includes("NaN")) ? "" : " ("+formatedTime+")"; + display.appendChild(duration); + } + + duration.innerText = skippedDuration <= 0 ? "" : " (" + utils.getFormattedTime(video.duration - skippedDuration) + ")"; } diff --git a/src/js-components/previewBar.ts b/src/js-components/previewBar.ts index a1f4e2c3..7f011f0d 100644 --- a/src/js-components/previewBar.ts +++ b/src/js-components/previewBar.ts @@ -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(); +const TOOLTIP_VISIBLE_CLASS = 'sponsorCategoryTooltipVisible'; + +export interface PreviewBarSegment { + segment: [number, number]; + category: string; + preview: boolean; +} + class PreviewBar { - container: HTMLUListElement; - parent: HTMLElement; - onMobileYouTube: boolean; - onInvidious: boolean; - - timestamps: number[][]; - types: string[]; - - 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; - - 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", () => { - mouseOnSeekBar = true; - }); - - seekBar.addEventListener("mouseleave", () => { - mouseOnSeekBar = false; - categoryTooltip.classList.add("sbHidden"); - }); - - const observer = new MutationObserver((mutations) => { - 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: HTMLElement): 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; - } + 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.segment[0] <= timeInSeconds && seg.segment[1] > timeInSeconds) { + const segmentLength = seg.segment[1] - seg.segment[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(({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, preview, segment}: 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(segment[1] - segment[0]); + bar.style.left = this.timeToPercentage(segment[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;
\ No newline at end of file +export default PreviewBar; diff --git a/src/utils.ts b/src/utils.ts index 2331b2d4..6d7af44d 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -154,17 +154,54 @@ class Utils { } /** - * Gets just the timestamps from a sponsorTimes array - * - * @param sponsorTimes + * Merges any overlapping timestamp ranges into single segments and returns them as a new array. */ - getSegmentsFromSponsorTimes(sponsorTimes: SponsorTime[]): number[][] { - const segments: number[][] = []; - for (const sponsorTime of sponsorTimes) { - segments.push(sponsorTime.segment); - } + getMergedTimestamps(timestamps: number[][]): [number, number][] { + let deduped: [number, number][] = []; + + // 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; + } - return segments; + /** + * 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 { |