diff --git a/src/components/SponsorTimeEditComponent.tsx b/src/components/SponsorTimeEditComponent.tsx index ed379105..b125ce72 100644 --- a/src/components/SponsorTimeEditComponent.tsx +++ b/src/components/SponsorTimeEditComponent.tsx @@ -6,6 +6,7 @@ import Utils from "../utils"; import SubmissionNoticeComponent from "./SubmissionNoticeComponent"; import { RectangleTooltip } from "../render/RectangleTooltip"; import SelectorComponent, { SelectorOption } from "./SelectorComponent"; +import { GenericUtils } from "../utils/genericUtils"; const utils = new Utils(); @@ -289,8 +290,8 @@ class SponsorTimeEditComponent extends React.Component getExistingChapters(sponsorVideoID, video.duration), + 5000, 100, (c) => c?.length > 0).then((chapters) => { + if (chapters?.length > 0) { + sponsorTimes = sponsorTimes.concat(...chapters); + updatePreviewBar(); + } + }); + } } else if (response?.status === 404) { retryFetch(); } diff --git a/src/js-components/previewBar.ts b/src/js-components/previewBar.ts index 6858035b..9de31e95 100644 --- a/src/js-components/previewBar.ts +++ b/src/js-components/previewBar.ts @@ -9,6 +9,7 @@ import Config from "../config"; import { ActionType, Category, SegmentContainer, SponsorTime } from "../types"; import Utils from "../utils"; import { partition } from "../utils/arrayUtils"; +import { GenericUtils } from "../utils/genericUtils"; const utils = new Utils(); const TOOLTIP_VISIBLE_CLASS = 'sponsorCategoryTooltipVisible'; @@ -111,7 +112,7 @@ class PreviewBar { const tooltipText = tooltipTextElement.textContent; if (tooltipText === null || tooltipText.length === 0) continue; - timeInSeconds = utils.getFormattedTimeToSeconds(tooltipText); + timeInSeconds = GenericUtils.getFormattedTimeToSeconds(tooltipText); if (timeInSeconds !== null) break; } @@ -389,6 +390,8 @@ class PreviewBar { const chapterBar = document.querySelector(".ytp-chapters-container:not(.sponsorBlockChapterBar)") as HTMLElement; if (!progressBar || !chapterBar) return; + // todo: Can this same mutation observer be reused to import chapters + // maybe not, looks like it has to be imported from the skipping to chapters page const observer = new MutationObserver((mutations) => { const changes: Record = {}; for (const mutation of mutations) { diff --git a/src/types.ts b/src/types.ts index a9fb161c..edb566da 100644 --- a/src/types.ts +++ b/src/types.ts @@ -71,7 +71,8 @@ export type Category = string & { __categoryBrand: unknown }; export enum SponsorSourceType { Server = undefined, - Local = 1 + Local = 1, + YouTube = 2 } export interface SegmentContainer { diff --git a/src/utils.ts b/src/utils.ts index 7ee3be4c..7b6fb387 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -29,7 +29,7 @@ export default class Utils { this.backgroundScriptContainer = backgroundScriptContainer; } - async wait(condition: () => T | false, timeout = 5000, check = 100): Promise { + async wait(condition: () => T, timeout = 5000, check = 100): Promise { return GenericUtils.wait(condition, timeout, check); } @@ -442,20 +442,6 @@ export default class Utils { return formatted; } - getFormattedTimeToSeconds(formatted: string): number | null { - const fragments = /^(?:(?:(\d+):)?(\d+):)?(\d*(?:[.,]\d+)?)$/.exec(formatted); - - if (fragments === null) { - return null; - } - - const hours = fragments[1] ? parseInt(fragments[1]) : 0; - const minutes = fragments[2] ? parseInt(fragments[2] || '0') : 0; - const seconds = fragments[3] ? parseFloat(fragments[3].replace(',', '.')) : 0; - - return hours * 3600 + minutes * 60 + seconds; - } - shortCategoryName(categoryName: string): string { return chrome.i18n.getMessage("category_" + categoryName + "_short") || chrome.i18n.getMessage("category_" + categoryName); } @@ -529,4 +515,8 @@ export default class Utils { Config.forceLocalUpdate("downvotedSegments"); } + + chaptersEnabled(): boolean { + return Config.config.renderAsChapters && !!this.getCategorySelection("chapter"); + } } diff --git a/src/utils/genericUtils.ts b/src/utils/genericUtils.ts index 3451c738..dd97d8a7 100644 --- a/src/utils/genericUtils.ts +++ b/src/utils/genericUtils.ts @@ -1,5 +1,5 @@ /** Function that can be used to wait for a condition before returning. */ -async function wait(condition: () => T | false, timeout = 5000, check = 100): Promise { +async function wait(condition: () => T, timeout = 5000, check = 100, predicate?: (obj: T) => boolean): Promise { return await new Promise((resolve, reject) => { setTimeout(() => { clearInterval(interval); @@ -8,7 +8,7 @@ async function wait(condition: () => T | false, timeout = 5000, check = 100): const intervalCheck = () => { const result = condition(); - if (result) { + if (predicate ? predicate(result) : result) { resolve(result); clearInterval(interval); } @@ -21,6 +21,20 @@ async function wait(condition: () => T | false, timeout = 5000, check = 100): }); } +function getFormattedTimeToSeconds(formatted: string): number | null { + const fragments = /^(?:(?:(\d+):)?(\d+):)?(\d*(?:[.,]\d+)?)$/.exec(formatted); + + if (fragments === null) { + return null; + } + + const hours = fragments[1] ? parseInt(fragments[1]) : 0; + const minutes = fragments[2] ? parseInt(fragments[2] || '0') : 0; + const seconds = fragments[3] ? parseFloat(fragments[3].replace(',', '.')) : 0; + + return hours * 3600 + minutes * 60 + seconds; +} + /** * Gets the error message in a nice string * @@ -64,10 +78,11 @@ function hexToRgb(hex: string): {r: number, g: number, b: number} { g: parseInt(result[2], 16), b: parseInt(result[3], 16) } : null; - } +} export const GenericUtils = { wait, + getFormattedTimeToSeconds, getErrorMessage, getLuminance } \ No newline at end of file diff --git a/src/utils/pageUtils.ts b/src/utils/pageUtils.ts index 8f484d73..da3a2f00 100644 --- a/src/utils/pageUtils.ts +++ b/src/utils/pageUtils.ts @@ -1,4 +1,7 @@ -export function getControls(): HTMLElement | false { +import { ActionType, Category, SponsorSourceType, SponsorTime, VideoID } from "../types"; +import { GenericUtils } from "./genericUtils"; + +export function getControls(): HTMLElement { const controlsSelectors = [ // YouTube ".ytp-right-controls", @@ -16,7 +19,7 @@ export function getControls(): HTMLElement | false { } } - return false; + return null; } export function isVisible(element: HTMLElement): boolean { @@ -61,4 +64,42 @@ export function getHashParams(): Record { } return {}; +} + +export function getExistingChapters(currentVideoID: VideoID, duration: number): SponsorTime[] { + const chaptersBox = document.querySelector("ytd-macro-markers-list-renderer"); + + const chapters: SponsorTime[] = []; + if (chaptersBox) { + let lastSegment: SponsorTime = null; + const links = chaptersBox.querySelectorAll("ytd-macro-markers-list-item-renderer > a"); + for (const link of links) { + const timeElement = link.querySelector("#time") as HTMLElement; + const description = link.querySelector("#details h4") as HTMLElement; + if (timeElement && description?.innerText?.length > 0 && link.getAttribute("href")?.includes(currentVideoID)) { + const time = GenericUtils.getFormattedTimeToSeconds(timeElement.innerText); + + if (lastSegment) { + lastSegment.segment[1] = time; + chapters.push(lastSegment); + } + + lastSegment = { + segment: [time, null], + category: "chapter" as Category, + actionType: ActionType.Chapter, + description: description.innerText, + source: SponsorSourceType.YouTube, + UUID: null + }; + } + } + + if (lastSegment) { + lastSegment.segment[1] = duration; + chapters.push(lastSegment); + } + } + + return chapters; } \ No newline at end of file