diff --git a/package-lock.json b/package-lock.json index bcb81a5b..ac39d095 100644 --- a/package-lock.json +++ b/package-lock.json @@ -27,7 +27,7 @@ ], "license": "LGPL-3.0-or-later", "dependencies": { - "@ajayyy/maze-utils": "^1.0.3", + "@ajayyy/maze-utils": "^1.1.0", "content-scripts-register-polyfill": "^4.0.2", "react": "^18.2.0", "react-dom": "^18.2.0" @@ -68,9 +68,9 @@ } }, "node_modules/@ajayyy/maze-utils": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@ajayyy/maze-utils/-/maze-utils-1.0.3.tgz", - "integrity": "sha512-sdQyU/2VAmJ9FiyUIdjE8FbO5b5IofN9vK/7lkZiUw91V+NZi7aSG/LSYMqmQ3OuTYRE5PLN9Jyknuo2ZnljjA==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@ajayyy/maze-utils/-/maze-utils-1.1.0.tgz", + "integrity": "sha512-Dc63B4Qbad14R590837b1ST0WFT8wWy4YFUmwheMiVABiG+G2m7o6wdq7Ln12pT/QUQO6h4/oKijEF3/ojXDVg==", "funding": [ { "type": "individual", @@ -13420,9 +13420,9 @@ }, "dependencies": { "@ajayyy/maze-utils": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/@ajayyy/maze-utils/-/maze-utils-1.0.3.tgz", - "integrity": "sha512-sdQyU/2VAmJ9FiyUIdjE8FbO5b5IofN9vK/7lkZiUw91V+NZi7aSG/LSYMqmQ3OuTYRE5PLN9Jyknuo2ZnljjA==" + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@ajayyy/maze-utils/-/maze-utils-1.1.0.tgz", + "integrity": "sha512-Dc63B4Qbad14R590837b1ST0WFT8wWy4YFUmwheMiVABiG+G2m7o6wdq7Ln12pT/QUQO6h4/oKijEF3/ojXDVg==" }, "@ampproject/remapping": { "version": "2.2.0", diff --git a/package.json b/package.json index d2492c38..4a9f3949 100644 --- a/package.json +++ b/package.json @@ -4,7 +4,7 @@ "description": "", "main": "background.js", "dependencies": { - "@ajayyy/maze-utils": "^1.0.3", + "@ajayyy/maze-utils": "^1.1.0", "content-scripts-register-polyfill": "^4.0.2", "react": "^18.2.0", "react-dom": "^18.2.0" diff --git a/src/config.ts b/src/config.ts index c1ab5aee..d0487aec 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1,7 +1,8 @@ import * as CompileConfig from "../config.json"; import * as invidiousList from "../ci/invidiouslist.json"; -import { Category, CategorySelection, CategorySkipOption, NoticeVisbilityMode, PreviewBarOption, SponsorTime, StorageChangesObject, Keybind, HashedValue, VideoID, SponsorHideType } from "./types"; +import { Category, CategorySelection, CategorySkipOption, NoticeVisbilityMode, PreviewBarOption, SponsorTime, Keybind, HashedValue, VideoID, SponsorHideType } from "./types"; import { isSafari, keybindEquals } from "./utils/configUtils"; +import { ProtoConfig, StorageChangesObject } from "@ajayyy/maze-utils/lib/config"; export interface Permission { canSubmit: boolean; @@ -121,7 +122,7 @@ interface SBConfig { }; } -export type VideoDownvotes = { segments: { uuid: HashedValue; hidden: SponsorHideType }[] ; lastAccess: number }; +export type VideoDownvotes = { segments: { uuid: HashedValue; hidden: SponsorHideType }[]; lastAccess: number }; interface SBStorage { /* VideoID prefixes to UUID prefixes */ @@ -143,322 +144,16 @@ export interface SBObject { resetToDefault(): void; } -const Config: SBObject = { - /** - * Callback function when an option is updated - */ - configLocalListeners: [], - configSyncListeners: [], - syncDefaults: { - userID: null, - isVip: false, - permissions: {}, - unsubmittedSegments: {}, - defaultCategory: "chooseACategory" as Category, - renderSegmentsAsChapters: false, - whitelistedChannels: [], - forceChannelCheck: false, - minutesSaved: 0, - skipCount: 0, - sponsorTimesContributed: 0, - submissionCountSinceCategories: 0, - showTimeWithSkips: true, - disableSkipping: false, - muteSegments: true, - fullVideoSegments: true, - manualSkipOnFullVideo: false, - trackViewCount: true, - trackViewCountInPrivate: true, - trackDownvotes: true, - dontShowNotice: false, - noticeVisibilityMode: NoticeVisbilityMode.FadedForAutoSkip, - hideVideoPlayerControls: false, - hideInfoButtonPlayerControls: false, - hideDeleteButtonPlayerControls: false, - hideUploadButtonPlayerControls: false, - hideSkipButtonPlayerControls: false, - hideDiscordLaunches: 0, - hideDiscordLink: false, - invidiousInstances: ["invidious.snopyta.org"], // leave as default - supportInvidious: false, - serverAddress: CompileConfig.serverAddress, - minDuration: 0, - skipNoticeDuration: 4, - audioNotificationOnSkip: false, - checkForUnlistedVideos: false, - testingServer: false, - refetchWhenNotFound: true, - ytInfoPermissionGranted: false, - allowExpirements: true, - showDonationLink: true, - showPopupDonationCount: 0, - showUpsells: true, - donateClicked: 0, - autoHideInfoButton: true, - autoSkipOnMusicVideos: false, - scrollToEditTimeUpdate: false, // false means the tooltip will be shown - categoryPillUpdate: false, - showChapterInfoMessage: true, - darkMode: true, - showCategoryGuidelines: true, - showCategoryWithoutPermission: false, - showSegmentNameInChapterBar: true, - useVirtualTime: true, - showSegmentFailedToFetchWarning: true, - allowScrollingToEdit: true, - - categoryPillColors: {}, - - /** - * Default keybinds should not set "code" as that's gonna be different based on the user's locale. They should also only use EITHER ctrl OR alt modifiers (or none). - * Using ctrl+alt, or shift may produce a different character that we will not be able to recognize in different locales. - * The exception for shift is letters, where it only capitalizes. So shift+A is fine, but shift+1 isn't. - * Don't forget to add the new keybind to the checks in "KeybindDialogComponent.isKeybindAvailable()" and in "migrateOldFormats()"! - * TODO: Find a way to skip having to update these checks. Maybe storing keybinds in a Map? - */ - skipKeybind: {key: "Enter"}, - startSponsorKeybind: {key: ";"}, - submitKeybind: {key: "'"}, - nextChapterKeybind: {key: "ArrowRight", ctrl: true}, - previousChapterKeybind: {key: "ArrowLeft", ctrl: true}, - - categorySelections: [{ - name: "sponsor" as Category, - option: CategorySkipOption.AutoSkip - }, { - name: "poi_highlight" as Category, - option: CategorySkipOption.ManualSkip - }, { - name: "exclusive_access" as Category, - option: CategorySkipOption.ShowOverlay - }], - - payments: { - licenseKey: null, - lastCheck: 0, - lastFreeCheck: 0, - freeAccess: false, - chaptersAllowed: false - }, - - colorPalette: { - red: "#780303", - white: "#ffffff", - locked: "#ffc83d" - }, - - // Preview bar - barTypes: { - "preview-chooseACategory": { - color: "#ffffff", - opacity: "0.7" - }, - "sponsor": { - color: "#00d400", - opacity: "0.7" - }, - "preview-sponsor": { - color: "#007800", - opacity: "0.7" - }, - "selfpromo": { - color: "#ffff00", - opacity: "0.7" - }, - "preview-selfpromo": { - color: "#bfbf35", - opacity: "0.7" - }, - "exclusive_access": { - color: "#008a5c", - opacity: "0.7" - }, - "interaction": { - color: "#cc00ff", - opacity: "0.7" - }, - "preview-interaction": { - color: "#6c0087", - opacity: "0.7" - }, - "intro": { - color: "#00ffff", - opacity: "0.7" - }, - "preview-intro": { - color: "#008080", - opacity: "0.7" - }, - "outro": { - color: "#0202ed", - opacity: "0.7" - }, - "preview-outro": { - color: "#000070", - opacity: "0.7" - }, - "preview": { - color: "#008fd6", - opacity: "0.7" - }, - "preview-preview": { - color: "#005799", - opacity: "0.7" - }, - "music_offtopic": { - color: "#ff9900", - opacity: "0.7" - }, - "preview-music_offtopic": { - color: "#a6634a", - opacity: "0.7" - }, - "poi_highlight": { - color: "#ff1684", - opacity: "0.7" - }, - "preview-poi_highlight": { - color: "#9b044c", - opacity: "0.7" - }, - "filler": { - color: "#7300FF", - opacity: "0.9" - }, - "preview-filler": { - color: "#2E0066", - opacity: "0.7" - } - } - }, - localDefaults: { - downvotedSegments: {}, - navigationApiAvailable: null - }, - cachedSyncConfig: null, - cachedLocalStorage: null, - config: null, - local: null, - forceSyncUpdate, - forceLocalUpdate, - resetToDefault -}; - -// Function setup - -function configProxy(): { sync: SBConfig; local: SBStorage } { - chrome.storage.onChanged.addListener((changes: {[key: string]: chrome.storage.StorageChange}, areaName) => { - if (areaName === "sync") { - for (const key in changes) { - Config.cachedSyncConfig[key] = changes[key].newValue; - } - - for (const callback of Config.configSyncListeners) { - callback(changes); - } - } else if (areaName === "local") { - for (const key in changes) { - Config.cachedLocalStorage[key] = changes[key].newValue; - } - - for (const callback of Config.configLocalListeners) { - callback(changes); - } - } - }); - - const syncHandler: ProxyHandler = { - set(obj: SBConfig, prop: K, value: SBConfig[K]) { - Config.cachedSyncConfig[prop] = value; - - chrome.storage.sync.set({ - [prop]: value - }); - - return true; - }, - - get(obj: SBConfig, prop: K): SBConfig[K] { - const data = Config.cachedSyncConfig[prop]; - - return obj[prop] || data; - }, - - deleteProperty(obj: SBConfig, prop: keyof SBConfig) { - chrome.storage.sync.remove( prop); - - return true; - } - - }; - - const localHandler: ProxyHandler = { - set(obj: SBStorage, prop: K, value: SBStorage[K]) { - Config.cachedLocalStorage[prop] = value; - - chrome.storage.local.set({ - [prop]: value - }); - - return true; - }, - - get(obj: SBStorage, prop: K): SBStorage[K] { - const data = Config.cachedLocalStorage[prop]; - - return obj[prop] || data; - }, - - deleteProperty(obj: SBStorage, prop: keyof SBStorage) { - chrome.storage.local.remove( prop); - - return true; - } - - }; - - return { - sync: new Proxy({ handler: syncHandler } as unknown as SBConfig, syncHandler), - local: new Proxy({ handler: localHandler } as unknown as SBStorage, localHandler) - }; -} - -function forceSyncUpdate(prop: string): void { - const value = Config.cachedSyncConfig[prop]; - if (prop === "unsubmittedSegments") { - // Early to be safe - if (JSON.stringify(value).length + prop.length > 8000) { - for (const key in value) { - if (!value[key] || value[key].length <= 0) { - delete value[key]; - } - } - } +class ConfigClass extends ProtoConfig { + resetToDefault() { + chrome.storage.sync.set({ + ...this.syncDefaults, + userID: this.config.userID, + minutesSaved: this.config.minutesSaved, + skipCount: this.config.skipCount, + sponsorTimesContributed: this.config.sponsorTimesContributed + }); } - - chrome.storage.sync.set({ - [prop]: value - }); -} - -function forceLocalUpdate(prop: string): void { - chrome.storage.local.set({ - [prop]: Config.cachedLocalStorage[prop] - }); -} - -async function fetchConfig(): Promise { - await Promise.all([new Promise((resolve) => { - chrome.storage.sync.get(null, function(items) { - Config.cachedSyncConfig = items; - resolve(); - }); - }), new Promise((resolve) => { - chrome.storage.local.get(null, function(items) { - Config.cachedLocalStorage = items; - resolve(); - }); - })]); } function migrateOldSyncFormats(config: SBConfig) { @@ -500,7 +195,7 @@ function migrateOldSyncFormats(config: SBConfig) { config["autoSkipOnMusicVideosUpdate"] = true; for (const selection of config.categorySelections) { if (selection.name === "music_offtopic" - && selection.option === CategorySkipOption.AutoSkip) { + && selection.option === CategorySkipOption.AutoSkip) { config.autoSkipOnMusicVideos = true; break; @@ -519,20 +214,20 @@ function migrateOldSyncFormats(config: SBConfig) { } if (typeof config["skipKeybind"] == "string") { - config["skipKeybind"] = {key: config["skipKeybind"]}; + config["skipKeybind"] = { key: config["skipKeybind"] }; } if (typeof config["startSponsorKeybind"] == "string") { - config["startSponsorKeybind"] = {key: config["startSponsorKeybind"]}; + config["startSponsorKeybind"] = { key: config["startSponsorKeybind"] }; } if (typeof config["submitKeybind"] == "string") { - config["submitKeybind"] = {key: config["submitKeybind"]}; + config["submitKeybind"] = { key: config["submitKeybind"] }; } // Unbind key if it matches a previous one set by the user (should be ordered oldest to newest) const keybinds = ["skipKeybind", "startSponsorKeybind", "submitKeybind"]; - for (let i = keybinds.length-1; i >= 0; i--) { + for (let i = keybinds.length - 1; i >= 0; i--) { for (let j = 0; j < keybinds.length; j++) { if (i == j) continue; @@ -554,56 +249,199 @@ function migrateOldSyncFormats(config: SBConfig) { if ((isSafari() || !config["supportInvidious"]) && config["invidiousInstances"].length !== invidiousList.length) { config["invidiousInstances"] = invidiousList; } - + if (config["lastIsVipUpdate"]) { chrome.storage.sync.remove("lastIsVipUpdate"); } } -async function setupConfig() { - if (typeof(chrome) === "undefined") return; +const syncDefaults = { + userID: null, + isVip: false, + permissions: {}, + unsubmittedSegments: {}, + defaultCategory: "chooseACategory" as Category, + renderSegmentsAsChapters: false, + whitelistedChannels: [], + forceChannelCheck: false, + minutesSaved: 0, + skipCount: 0, + sponsorTimesContributed: 0, + submissionCountSinceCategories: 0, + showTimeWithSkips: true, + disableSkipping: false, + muteSegments: true, + fullVideoSegments: true, + manualSkipOnFullVideo: false, + trackViewCount: true, + trackViewCountInPrivate: true, + trackDownvotes: true, + dontShowNotice: false, + noticeVisibilityMode: NoticeVisbilityMode.FadedForAutoSkip, + hideVideoPlayerControls: false, + hideInfoButtonPlayerControls: false, + hideDeleteButtonPlayerControls: false, + hideUploadButtonPlayerControls: false, + hideSkipButtonPlayerControls: false, + hideDiscordLaunches: 0, + hideDiscordLink: false, + invidiousInstances: ["invidious.snopyta.org"], // leave as default + supportInvidious: false, + serverAddress: CompileConfig.serverAddress, + minDuration: 0, + skipNoticeDuration: 4, + audioNotificationOnSkip: false, + checkForUnlistedVideos: false, + testingServer: false, + refetchWhenNotFound: true, + ytInfoPermissionGranted: false, + allowExpirements: true, + showDonationLink: true, + showPopupDonationCount: 0, + showUpsells: true, + donateClicked: 0, + autoHideInfoButton: true, + autoSkipOnMusicVideos: false, + scrollToEditTimeUpdate: false, // false means the tooltip will be shown + categoryPillUpdate: false, + showChapterInfoMessage: true, + darkMode: true, + showCategoryGuidelines: true, + showCategoryWithoutPermission: false, + showSegmentNameInChapterBar: true, + useVirtualTime: true, + showSegmentFailedToFetchWarning: true, + allowScrollingToEdit: true, - await fetchConfig(); - addDefaults(); - const config = configProxy(); - migrateOldSyncFormats(config.sync); + categoryPillColors: {}, - Config.config = config.sync; - Config.local = config.local; -} + /** + * Default keybinds should not set "code" as that's gonna be different based on the user's locale. They should also only use EITHER ctrl OR alt modifiers (or none). + * Using ctrl+alt, or shift may produce a different character that we will not be able to recognize in different locales. + * The exception for shift is letters, where it only capitalizes. So shift+A is fine, but shift+1 isn't. + * Don't forget to add the new keybind to the checks in "KeybindDialogComponent.isKeybindAvailable()" and in "migrateOldFormats()"! + * TODO: Find a way to skip having to update these checks. Maybe storing keybinds in a Map? + */ + skipKeybind: { key: "Enter" }, + startSponsorKeybind: { key: ";" }, + submitKeybind: { key: "'" }, + nextChapterKeybind: { key: "ArrowRight", ctrl: true }, + previousChapterKeybind: { key: "ArrowLeft", ctrl: true }, -// Add defaults -function addDefaults() { - for (const key in Config.syncDefaults) { - if(!Object.prototype.hasOwnProperty.call(Config.cachedSyncConfig, key)) { - Config.cachedSyncConfig[key] = Config.syncDefaults[key]; - } else if (key === "barTypes") { - for (const key2 in Config.syncDefaults[key]) { - if(!Object.prototype.hasOwnProperty.call(Config.cachedSyncConfig[key], key2)) { - Config.cachedSyncConfig[key][key2] = Config.syncDefaults[key][key2]; - } - } + categorySelections: [{ + name: "sponsor" as Category, + option: CategorySkipOption.AutoSkip + }, { + name: "poi_highlight" as Category, + option: CategorySkipOption.ManualSkip + }, { + name: "exclusive_access" as Category, + option: CategorySkipOption.ShowOverlay + }], + + payments: { + licenseKey: null, + lastCheck: 0, + lastFreeCheck: 0, + freeAccess: false, + chaptersAllowed: false + }, + + colorPalette: { + red: "#780303", + white: "#ffffff", + locked: "#ffc83d" + }, + + // Preview bar + barTypes: { + "preview-chooseACategory": { + color: "#ffffff", + opacity: "0.7" + }, + "sponsor": { + color: "#00d400", + opacity: "0.7" + }, + "preview-sponsor": { + color: "#007800", + opacity: "0.7" + }, + "selfpromo": { + color: "#ffff00", + opacity: "0.7" + }, + "preview-selfpromo": { + color: "#bfbf35", + opacity: "0.7" + }, + "exclusive_access": { + color: "#008a5c", + opacity: "0.7" + }, + "interaction": { + color: "#cc00ff", + opacity: "0.7" + }, + "preview-interaction": { + color: "#6c0087", + opacity: "0.7" + }, + "intro": { + color: "#00ffff", + opacity: "0.7" + }, + "preview-intro": { + color: "#008080", + opacity: "0.7" + }, + "outro": { + color: "#0202ed", + opacity: "0.7" + }, + "preview-outro": { + color: "#000070", + opacity: "0.7" + }, + "preview": { + color: "#008fd6", + opacity: "0.7" + }, + "preview-preview": { + color: "#005799", + opacity: "0.7" + }, + "music_offtopic": { + color: "#ff9900", + opacity: "0.7" + }, + "preview-music_offtopic": { + color: "#a6634a", + opacity: "0.7" + }, + "poi_highlight": { + color: "#ff1684", + opacity: "0.7" + }, + "preview-poi_highlight": { + color: "#9b044c", + opacity: "0.7" + }, + "filler": { + color: "#7300FF", + opacity: "0.9" + }, + "preview-filler": { + color: "#2E0066", + opacity: "0.7" } } +}; - for (const key in Config.localDefaults) { - if(!Object.prototype.hasOwnProperty.call(Config.cachedLocalStorage, key)) { - Config.cachedLocalStorage[key] = Config.localDefaults[key]; - } - } -} +const localDefaults = { + downvotedSegments: {}, + navigationApiAvailable: null +}; -function resetToDefault() { - chrome.storage.sync.set({ - ...Config.syncDefaults, - userID: Config.config.userID, - minutesSaved: Config.config.minutesSaved, - skipCount: Config.config.skipCount, - sponsorTimesContributed: Config.config.sponsorTimesContributed - }); -} - -// Sync config -setupConfig(); - -export default Config; +const Config = new ConfigClass(syncDefaults, localDefaults, migrateOldSyncFormats); +export default Config; \ No newline at end of file diff --git a/src/content.ts b/src/content.ts index d1887bf0..3c0a60a5 100644 --- a/src/content.ts +++ b/src/content.ts @@ -8,14 +8,12 @@ import { ContentContainer, HashedValue, Keybind, - PageType, ScheduledTime, SegmentUUID, SkipToTimeParams, SponsorHideType, SponsorSourceType, SponsorTime, - StorageChangesObject, ToggleSkippable, VideoID, VideoInfo, @@ -28,7 +26,7 @@ import SubmissionNotice from "./render/SubmissionNotice"; import { Message, MessageResponse, VoteResponse } from "./messageTypes"; import { SkipButtonControlBar } from "./js-components/skipButtonControlBar"; import { getStartTimeFromUrl } from "./utils/urlParser"; -import { findValidElement, getControls, getExistingChapters, getHashParams, isVisible } from "./utils/pageUtils"; +import { getControls, getExistingChapters, getHashParams, isVisible } from "./utils/pageUtils"; import { isSafari, keybindEquals } from "./utils/configUtils"; import { CategoryPill } from "./render/CategoryPill"; import { AnimationUtils } from "./utils/animationUtils"; @@ -41,11 +39,14 @@ import { Tooltip } from "./render/Tooltip"; import { noRefreshFetchingChaptersAllowed } from "./utils/licenseKey"; import { waitFor } from "@ajayyy/maze-utils"; import { getFormattedTime } from "@ajayyy/maze-utils/lib/formating"; +import { setupVideoMutationListener, getChannelIDInfo, getVideo, refreshVideoAttachments, getIsAdPlaying, getIsLivePremiere, setIsAdPlaying, checkVideoIDChange, getVideoID, getYouTubeVideoID, setupVideoModule, checkIfNewVideoID } from "@ajayyy/maze-utils/lib/video"; +import { StorageChangesObject } from "@ajayyy/maze-utils/lib/config"; +import { findValidElement } from "@ajayyy/maze-utils/lib/dom" const utils = new Utils(); // Hack to get the CSS loaded on permission-based sites (Invidious) -utils.wait(() => Config.config !== null, 5000, 10).then(addCSS); +utils.wait(() => Config.isReady(), 5000, 10).then(addCSS); const skipBuffer = 0.003; @@ -54,8 +55,6 @@ let sponsorDataFound = false; //the actual sponsorTimes if loaded and UUIDs associated with them let sponsorTimes: SponsorTime[] = []; let existingChaptersImported = false; -//what video id are these sponsors for -let sponsorVideoID: VideoID = null; // List of open skip notices const skipNotices: SkipNotice[] = []; let activeSkipKeybindElement: ToggleSkippable = null; @@ -64,12 +63,6 @@ let shownSegmentFailedToFetchWarning = false; // JSON video info let videoInfo: VideoInfo = null; -// Page Type - browse/watch etc... -let pageType: PageType; -// if video is live or premiere -let isLivePremiere: boolean -// The channel this video is about -let channelIDInfo: ChannelIDInfo; // Locked Categories in this tab, like: ["sponsor","intro","outro"] let lockedCategories: Category[] = []; // Used to calculate a more precise "virtual" video time @@ -96,14 +89,36 @@ let currentVirtualTimeInterval: NodeJS.Timeout = null; /** Has the sponsor been skipped */ let sponsorSkipped: boolean[] = []; -//the video -let video: HTMLVideoElement; let videoMuted = false; // Has it been attempted to be muted -let videoMutationObserver: MutationObserver = null; -let waitingForNewVideo = false; -// List of videos that have had event listeners added to them -const videosWithEventListeners: HTMLVideoElement[] = []; -const controlsWithEventListeners: HTMLElement[] = [] +const controlsWithEventListeners: HTMLElement[] = []; + +setupVideoModule({ + videoIDChange, + channelIDChange, + videoElementChange: (newVideo) => { + if (newVideo) { + setupVideoListeners(); + setupSkipButtonControlBar(); + setupCategoryPill(); + } + + if (previewBar && !utils.findReferenceNode()?.contains(previewBar.container)) { + previewBar.remove(); + previewBar = null; + + createPreviewBar(); + } + }, + playerInit: () => { + previewBar = null; // remove old previewbar + createPreviewBar(); + }, + updatePlayerBar: () => { + updatePreviewBar(); + updateVisibilityOfPlayerControlsButton(); + }, + resetValues +}, () => Config); // This misleading variable name will be fixed soon let onInvidious: boolean; @@ -122,7 +137,6 @@ let lastCheckVideoTime = -1; //is this channel whitelised from getting sponsors skipped let channelWhitelisted = false; -let waitingForChannelID = false; let previewBar: PreviewBar = null; // Skip to highlight button @@ -136,13 +150,6 @@ let controls: HTMLElement | null = null; /** Contains buttons created by `createButton()`. */ const playerButtons: Record = {}; -// Direct Links after the config is loaded -utils.wait(() => Config.config !== null, 1000, 1).then(() => videoIDChange(getYouTubeVideoID(document))); -// wait for hover preview to appear, and refresh attachments if ever found -utils.waitForElement(".ytp-inline-preview-ui").then(() => refreshVideoAttachments()); -utils.waitForElement("a.ytp-title-link[data-sessionlink='feature=player-title']") - .then(() => videoIDChange(getYouTubeVideoID(document))); -addPageListeners(); addHotkeyListener(); /** Segments created by the user which have not yet been submitted. */ @@ -155,9 +162,6 @@ let popupInitialised = false; let submissionNotice: SubmissionNotice = null; -// If there is an advert playing (or about to be played), this is true -let isAdPlaying = false; - let lastResponseStatus: number; let retryCount = 0; let lookupWaiting = false; @@ -170,8 +174,8 @@ const skipNoticeContentContainer: ContentContainer = () => ({ sponsorTimes, sponsorTimesSubmitting, skipNotices, - v: video, - sponsorVideoID, + v: getVideo(), + sponsorVideoID: getVideoID(), reskipSponsorTime, updatePreviewBar, onMobileYouTube, @@ -182,7 +186,7 @@ const skipNoticeContentContainer: ContentContainer = () => ({ videoInfo, getRealCurrentTime: getRealCurrentTime, lockedCategories, - channelIDInfo + channelIDInfo: getChannelIDInfo() }); // value determining when to count segment as skipped and send telemetry to server (percent based) @@ -195,7 +199,7 @@ function messageListener(request: Message, sender: unknown, sendResponse: (respo //messages from popup script switch(request.message){ case "update": - videoIDChange(getYouTubeVideoID(document)); + checkVideoIDChange(); break; case "sponsorStart": startOrEndTimingNewSegment() @@ -211,7 +215,7 @@ function messageListener(request: Message, sender: unknown, sendResponse: (respo found: sponsorDataFound, status: lastResponseStatus, sponsorTimes: sponsorTimes, - time: video.currentTime, + time: getVideo().currentTime, onMobileYouTube }); @@ -224,13 +228,13 @@ function messageListener(request: Message, sender: unknown, sendResponse: (respo break; case "getVideoID": sendResponse({ - videoID: sponsorVideoID, + videoID: getVideoID(), }); break; case "getChannelID": sendResponse({ - channelID: channelIDInfo.id + channelID: getChannelIDInfo().id }); break; @@ -250,7 +254,7 @@ function messageListener(request: Message, sender: unknown, sendResponse: (respo break; case "refreshSegments": // update video on refresh if videoID invalid - if (!sponsorVideoID) videoIDChange(getYouTubeVideoID(document)); + if (!getVideoID()) checkVideoIDChange(); // fetch segments sponsorsLookup(false); @@ -266,7 +270,7 @@ function messageListener(request: Message, sender: unknown, sendResponse: (respo return true; case "hideSegment": utils.getSponsorTimeFromUUID(sponsorTimes, request.UUID).hidden = request.type; - utils.addHiddenSegment(sponsorVideoID, request.UUID, request.type); + utils.addHiddenSegment(getVideoID(), request.UUID, request.type); updatePreviewBar(); if (skipButtonControlBar?.isEnabled() @@ -281,7 +285,7 @@ function messageListener(request: Message, sender: unknown, sendResponse: (respo navigator.clipboard.writeText(request.text); break; case "importSegments": { - const importedSegments = importTimes(request.data, video.duration); + const importedSegments = importTimes(request.data, getVideo().duration); let addedSegments = false; for (const segment of importedSegments) { if (!sponsorTimesSubmitting.some( @@ -299,7 +303,7 @@ function messageListener(request: Message, sender: unknown, sendResponse: (respo } if (addedSegments) { - Config.config.unsubmittedSegments[sponsorVideoID] = sponsorTimesSubmitting; + Config.config.unsubmittedSegments[getVideoID()] = sponsorTimesSubmitting; Config.forceSyncUpdate("unsubmittedSegments"); updateEditButtonsOnPlayer(); @@ -362,16 +366,9 @@ function resetValues() { lastResponseStatus = 0; shownSegmentFailedToFetchWarning = false; - sponsorVideoID = null; videoInfo = null; - pageType = null; channelWhitelisted = false; - channelIDInfo = { - status: ChannelIDStatus.Fetching, - id: null - }; lockedCategories = []; - isLivePremiere = false; //empty the preview bar if (previewBar !== null) { @@ -389,9 +386,6 @@ function resetValues() { logDebug("Setting switching videos to true (reset data)"); } - // Reset advert playing flag - isAdPlaying = false; - skipButtonControlBar?.disable(); categoryPill?.setVisibility(false); @@ -400,38 +394,7 @@ function resetValues() { } } -async function videoIDChange(id: string): Promise { - // don't switch to invalid value - if (!id && sponsorVideoID && !document?.URL?.includes("youtube.com/clip/")) return; - //if the id has not changed return unless the video element has changed - if (sponsorVideoID === id && (isVisible(video) || !video)) return; - - resetValues(); - sponsorVideoID = id; - - //id is not valid - if (!id) return; - - // Wait for options to be ready - await utils.wait(() => Config.config !== null, 5000, 1); - - // If enabled, it will check if this video is private or unlisted and double check with the user if the sponsors should be looked up - if (Config.config.checkForUnlistedVideos) { - const shouldContinue = confirm("SponsorBlock: You have the setting 'Ignore Unlisted/Private Videos' enabled." - + " Due to a change in how segment fetching works, this setting is not needed anymore as it cannot leak your video ID to the server." - + " It instead sends just the first 4 characters of a longer hash of the videoID to the server, and filters through a subset of the database." - + " More info about this implementation can be found here: https://github.com/ajayyy/SponsorBlockServer/issues/25" - + "\n\nPlease click okay to confirm that you acknowledge this and continue using SponsorBlock."); - if (shouldContinue) { - Config.config.checkForUnlistedVideos = false; - } else { - return; - } - } - - // Update whitelist data when the video data is loaded - whitelistCheck(); - +function videoIDChange(): void { //setup the preview bar if (previewBar === null) { if (onMobileYouTube) { @@ -457,7 +420,7 @@ async function videoIDChange(id: string): Promise { // Notify the popup about the video change chrome.runtime.sendMessage({ message: "videoChanged", - videoID: sponsorVideoID, + videoID: getVideoID(), whitelisted: channelWhitelisted }); @@ -588,7 +551,7 @@ function startSponsorSchedule(includeIntersectingSegments = false, currentTime?: cancelSponsorSchedule(); // Don't skip if advert playing and reset last checked time - if (isAdPlaying) { + if (getIsAdPlaying()) { // Reset lastCheckVideoTime lastCheckVideoTime = -1; lastCheckTime = 0; @@ -597,15 +560,13 @@ function startSponsorSchedule(includeIntersectingSegments = false, currentTime?: return; } - // ensure we are on the correct video - const newVideoID = getYouTubeVideoID(document); - if (newVideoID !== sponsorVideoID) { - videoIDChange(newVideoID); + // Give up if video changed, and trigger a videoID change if so + if (checkIfNewVideoID()) { return; } - logDebug(`Considering to start skipping: ${!video}, ${video?.paused}`); - if (!video) return; + logDebug(`Considering to start skipping: ${!getVideo()}, ${getVideo()?.paused}`); + if (!getVideo()) return; if (currentTime === undefined || currentTime === null) { currentTime = getVirtualTime(); } @@ -614,17 +575,17 @@ function startSponsorSchedule(includeIntersectingSegments = false, currentTime?: updateActiveSegment(currentTime); - if (video.paused) return; + if (getVideo().paused) return; const skipInfo = getNextSkipIndex(currentTime, includeIntersectingSegments, includeNonIntersectingSegments); const currentSkip = skipInfo.array[skipInfo.index]; const skipTime: number[] = [currentSkip?.scheduledTime, skipInfo.array[skipInfo.endIndex]?.segment[1]]; const timeUntilSponsor = skipTime?.[0] - currentTime; - const videoID = sponsorVideoID; + const videoID = getVideoID(); if (videoMuted && !inMuteSegment(currentTime, skipInfo.index !== -1 && timeUntilSponsor < skipBuffer && shouldAutoSkip(currentSkip))) { - video.muted = false; + getVideo().muted = false; videoMuted = false; for (const notice of skipNotices) { @@ -636,7 +597,7 @@ function startSponsorSchedule(includeIntersectingSegments = false, currentTime?: logDebug(`Ready to start skipping: ${skipInfo.index} at ${currentTime}`); if (skipInfo.index === -1) return; - if (Config.config.disableSkipping || channelWhitelisted || (channelIDInfo.status === ChannelIDStatus.Fetching && Config.config.forceChannelCheck)){ + if (Config.config.disableSkipping || channelWhitelisted || (getChannelIDInfo().status === ChannelIDStatus.Fetching && Config.config.forceChannelCheck)){ return; } @@ -664,12 +625,12 @@ function startSponsorSchedule(includeIntersectingSegments = false, currentTime?: let forcedIncludeNonIntersectingSegments = true; if (incorrectVideoCheck(videoID, currentSkip)) return; - forceVideoTime ||= Math.max(video.currentTime, getVirtualTime()); + forceVideoTime ||= Math.max(getVideo().currentTime, getVirtualTime()); if ((shouldSkip(currentSkip) || sponsorTimesSubmitting?.some((segment) => segment.segment === currentSkip.segment))) { if (forceVideoTime >= skipTime[0] - skipBuffer && forceVideoTime < skipTime[1]) { skipToTime({ - v: video, + v: getVideo(), skipTime, skippingSegments, openNotice: skipInfo.openNotice @@ -680,7 +641,7 @@ function startSponsorSchedule(includeIntersectingSegments = false, currentTime?: const extraSkip = skipInfo.array[extra]; if (shouldSkip(extraSkip)) { skipToTime({ - v: video, + v: getVideo(), skipTime: [extraSkip.scheduledTime, extraSkip.segment[1]], skippingSegments: [extraSkip], openNotice: skipInfo.openNotice @@ -709,40 +670,40 @@ function startSponsorSchedule(includeIntersectingSegments = false, currentTime?: if (timeUntilSponsor < skipBuffer) { skippingFunction(currentTime); } else { - const delayTime = timeUntilSponsor * 1000 * (1 / video.playbackRate); + const delayTime = timeUntilSponsor * 1000 * (1 / getVideo().playbackRate); if (delayTime < 300) { // Use interval instead of timeout near the end to combat imprecise video time const startIntervalTime = performance.now(); - const startVideoTime = Math.max(currentTime, video.currentTime); + const startVideoTime = Math.max(currentTime, getVideo().currentTime); let startWaitingForReportedTimeToChange = true; - const reportedVideoTimeAtStart = video.currentTime; - logDebug(`Starting setInterval skipping ${video.currentTime} to skip at ${skipTime[0]}`); + const reportedVideoTimeAtStart = getVideo().currentTime; + logDebug(`Starting setInterval skipping ${getVideo().currentTime} to skip at ${skipTime[0]}`); currentSkipInterval = setInterval(() => { // Estimate delay, but only take the current time right after a change // Current time remains the same for many "frames" on Firefox if (utils.isFirefox() && !lastKnownVideoTime.fromPause && startWaitingForReportedTimeToChange - && reportedVideoTimeAtStart !== video.currentTime) { + && reportedVideoTimeAtStart !== getVideo().currentTime) { startWaitingForReportedTimeToChange = false; - const delay = getVirtualTime() - video.currentTime; + const delay = getVirtualTime() - getVideo().currentTime; if (delay > 0) lastKnownVideoTime.approximateDelay = delay; } const intervalDuration = performance.now() - startIntervalTime; - if (intervalDuration + skipBuffer * 1000 >= delayTime || video.currentTime >= skipTime[0]) { + if (intervalDuration + skipBuffer * 1000 >= delayTime || getVideo().currentTime >= skipTime[0]) { clearInterval(currentSkipInterval); - if (!utils.isFirefox() && !video.muted) { + if (!utils.isFirefox() && !getVideo().muted) { // Workaround for more accurate skipping on Chromium - video.muted = true; - video.muted = false; + getVideo().muted = true; + getVideo().muted = false; } - skippingFunction(Math.max(video.currentTime, startVideoTime + video.playbackRate * Math.max(delayTime, intervalDuration) / 1000)); + skippingFunction(Math.max(getVideo().currentTime, startVideoTime + getVideo().playbackRate * Math.max(delayTime, intervalDuration) / 1000)); } }, 1); } else { - logDebug(`Starting timeout to skip ${video.currentTime} to skip at ${skipTime[0]}`); + logDebug(`Starting timeout to skip ${getVideo().currentTime} to skip at ${skipTime[0]}`); // Schedule for right before to be more precise than normal timeout currentSkipSchedule = setTimeout(skippingFunction, Math.max(0, delayTime - 150)); @@ -752,13 +713,13 @@ function startSponsorSchedule(includeIntersectingSegments = false, currentTime?: function getVirtualTime(): number { const virtualTime = lastTimeFromWaitingEvent ?? (lastKnownVideoTime.videoTime !== null ? - (performance.now() - lastKnownVideoTime.preciseTime) * video.playbackRate / 1000 + lastKnownVideoTime.videoTime : null); + (performance.now() - lastKnownVideoTime.preciseTime) * getVideo().playbackRate / 1000 + lastKnownVideoTime.videoTime : null); if (Config.config.useVirtualTime && !isSafari() && virtualTime - && Math.abs(virtualTime - video.currentTime) < 0.2 && video.currentTime !== 0) { + && Math.abs(virtualTime - getVideo().currentTime) < 0.2 && getVideo().currentTime !== 0) { return virtualTime; } else { - return video.currentTime; + return getVideo().currentTime; } } @@ -773,16 +734,16 @@ function inMuteSegment(currentTime: number, includeOverlap: boolean): boolean { * This makes sure the videoID is still correct and if the sponsorTime is included */ function incorrectVideoCheck(videoID?: string, sponsorTime?: SponsorTime): boolean { - const currentVideoID = getYouTubeVideoID(document); - if (currentVideoID !== (videoID || sponsorVideoID) || (sponsorTime + const currentVideoID = getYouTubeVideoID(); + if (currentVideoID !== (videoID || getVideoID()) || (sponsorTime && (!sponsorTimes || !sponsorTimes?.some((time) => time.segment === sponsorTime.segment)) && !sponsorTimesSubmitting.some((time) => time.segment === sponsorTime.segment))) { // Something has really gone wrong console.error("[SponsorBlock] The videoID recorded when trying to skip is different than what it should be."); - console.error("[SponsorBlock] VideoID recorded: " + sponsorVideoID + ". Actual VideoID: " + currentVideoID); + console.error("[SponsorBlock] VideoID recorded: " + getVideoID() + ". Actual VideoID: " + currentVideoID); // Video ID change occured - videoIDChange(currentVideoID); + checkVideoIDChange(); return true; } else { @@ -790,49 +751,10 @@ function incorrectVideoCheck(videoID?: string, sponsorTime?: SponsorTime): boole } } -function setupVideoMutationListener() { - const videoContainer = document.querySelector(".html5-video-container"); - if (!videoContainer || videoMutationObserver !== null || onInvidious) return; - - videoMutationObserver = new MutationObserver(refreshVideoAttachments); - - videoMutationObserver.observe(videoContainer, { - attributes: true, - childList: true, - subtree: true - }); -} - -async function refreshVideoAttachments(): Promise { - if (waitingForNewVideo) return; - - waitingForNewVideo = true; - const newVideo = await utils.waitForElement("video", true) as HTMLVideoElement; - waitingForNewVideo = false; - - video = newVideo; - if (!videosWithEventListeners.includes(video)) { - videosWithEventListeners.push(video); - - setupVideoListeners(); - setupSkipButtonControlBar(); - setupCategoryPill(); - } - - if (previewBar && !utils.findReferenceNode()?.contains(previewBar.container)) { - previewBar.remove(); - previewBar = null; - - createPreviewBar(); - } - - videoIDChange(getYouTubeVideoID(document)); -} - function setupVideoListeners() { //wait until it is loaded - video.addEventListener('loadstart', videoOnReadyListener) - video.addEventListener('durationchange', durationChangeListener); + getVideo().addEventListener('loadstart', videoOnReadyListener) + getVideo().addEventListener('durationchange', durationChangeListener); if (!Config.config.disableSkipping) { switchingVideos = false; @@ -840,12 +762,12 @@ function setupVideoListeners() { let startedWaiting = false; let lastPausedAtZero = true; - video.addEventListener('play', () => { + getVideo().addEventListener('play', () => { // If it is not the first event, then the only way to get to 0 is if there is a seek event // This check makes sure that changing the video resolution doesn't cause the extension to think it // gone back to the begining - if (video.readyState <= HTMLMediaElement.HAVE_CURRENT_DATA - && video.currentTime === 0) return; + if (getVideo().readyState <= HTMLMediaElement.HAVE_CURRENT_DATA + && getVideo().currentTime === 0) return; updateVirtualTime(); @@ -863,23 +785,23 @@ function setupVideoListeners() { updateAdFlag(); // Make sure it doesn't get double called with the playing event - if (Math.abs(lastCheckVideoTime - video.currentTime) > 0.3 - || (lastCheckVideoTime !== video.currentTime && Date.now() - lastCheckTime > 2000)) { + if (Math.abs(lastCheckVideoTime - getVideo().currentTime) > 0.3 + || (lastCheckVideoTime !== getVideo().currentTime && Date.now() - lastCheckTime > 2000)) { lastCheckTime = Date.now(); - lastCheckVideoTime = video.currentTime; + lastCheckVideoTime = getVideo().currentTime; startSponsorSchedule(); } }); - video.addEventListener('playing', () => { + getVideo().addEventListener('playing', () => { updateVirtualTime(); lastPausedAtZero = false; if (startedWaiting) { startedWaiting = false; - logDebug(`[SB] Playing event after buffering: ${Math.abs(lastCheckVideoTime - video.currentTime) > 0.3 - || (lastCheckVideoTime !== video.currentTime && Date.now() - lastCheckTime > 2000)}`); + logDebug(`[SB] Playing event after buffering: ${Math.abs(lastCheckVideoTime - getVideo().currentTime) > 0.3 + || (lastCheckVideoTime !== getVideo().currentTime && Date.now() - lastCheckTime > 2000)}`); } if (switchingVideos) { @@ -891,42 +813,42 @@ function setupVideoListeners() { } // Make sure it doesn't get double called with the play event - if (Math.abs(lastCheckVideoTime - video.currentTime) > 0.3 - || (lastCheckVideoTime !== video.currentTime && Date.now() - lastCheckTime > 2000)) { + if (Math.abs(lastCheckVideoTime - getVideo().currentTime) > 0.3 + || (lastCheckVideoTime !== getVideo().currentTime && Date.now() - lastCheckTime > 2000)) { lastCheckTime = Date.now(); - lastCheckVideoTime = video.currentTime; + lastCheckVideoTime = getVideo().currentTime; startSponsorSchedule(); } }); - video.addEventListener('seeking', () => { + getVideo().addEventListener('seeking', () => { lastKnownVideoTime.fromPause = false; - if (!video.paused){ + if (!getVideo().paused){ // Reset lastCheckVideoTime lastCheckTime = Date.now(); - lastCheckVideoTime = video.currentTime; + lastCheckVideoTime = getVideo().currentTime; updateVirtualTime(); clearWaitingTime(); startSponsorSchedule(); } else { - updateActiveSegment(video.currentTime); + updateActiveSegment(getVideo().currentTime); - if (video.currentTime === 0) { + if (getVideo().currentTime === 0) { lastPausedAtZero = true; } } }); - video.addEventListener('ratechange', () => { + getVideo().addEventListener('ratechange', () => { updateVirtualTime(); clearWaitingTime(); startSponsorSchedule(); }); // Used by videospeed extension (https://github.com/igrigorik/videospeed/pull/740) - video.addEventListener('videoSpeed_ratechange', () => { + getVideo().addEventListener('videoSpeed_ratechange', () => { updateVirtualTime(); clearWaitingTime(); @@ -943,12 +865,12 @@ function setupVideoListeners() { cancelSponsorSchedule(); }; - video.addEventListener('pause', () => { + getVideo().addEventListener('pause', () => { lastKnownVideoTime.fromPause = true; stoppedPlayback(); }); - video.addEventListener('waiting', () => { + getVideo().addEventListener('waiting', () => { logDebug("[SB] Not skipping due to buffering"); startedWaiting = true; @@ -962,7 +884,7 @@ function setupVideoListeners() { function updateVirtualTime() { if (currentVirtualTimeInterval) clearInterval(currentVirtualTimeInterval); - lastKnownVideoTime.videoTime = video.currentTime; + lastKnownVideoTime.videoTime = getVideo().currentTime; lastKnownVideoTime.preciseTime = performance.now(); // If on Firefox, wait for the second time change (time remains fixed for many "frames" for privacy reasons) @@ -970,16 +892,16 @@ function updateVirtualTime() { let count = 0; let lastTime = lastKnownVideoTime.videoTime; currentVirtualTimeInterval = setInterval(() => { - if (lastTime !== video.currentTime) { + if (lastTime !== getVideo().currentTime) { count++; - lastTime = video.currentTime; + lastTime = getVideo().currentTime; } if (count > 1) { const delay = lastKnownVideoTime.fromPause && lastKnownVideoTime.approximateDelay ? lastKnownVideoTime.approximateDelay : 0; - lastKnownVideoTime.videoTime = video.currentTime + delay; + lastKnownVideoTime.videoTime = getVideo().currentTime + delay; lastKnownVideoTime.preciseTime = performance.now(); clearInterval(currentVirtualTimeInterval); @@ -990,7 +912,7 @@ function updateVirtualTime() { } function updateWaitingTime(): void { - lastTimeFromWaitingEvent = video.currentTime; + lastTimeFromWaitingEvent = getVideo().currentTime; } function clearWaitingTime(): void { @@ -1001,7 +923,7 @@ function setupSkipButtonControlBar() { if (!skipButtonControlBar) { skipButtonControlBar = new SkipButtonControlBar({ skip: (segment) => skipToTime({ - v: video, + v: getVideo(), skipTime: segment.segment, skippingSegments: [segment], openNotice: true, @@ -1024,9 +946,9 @@ function setupCategoryPill() { async function sponsorsLookup(keepOldSubmissions = true) { if (lookupWaiting) return; - if (!video || !isVisible(video)) refreshVideoAttachments(); + if (!getVideo() || !isVisible(getVideo())) refreshVideoAttachments(); //there is still no video here - if (!video) { + if (!getVideo()) { lookupWaiting = true; setTimeout(() => { lookupWaiting = false; @@ -1072,7 +994,7 @@ async function sponsorsLookup(keepOldSubmissions = true) { const hashParams = getHashParams(); if (hashParams.requiredSegment) extraRequestData.requiredSegment = hashParams.requiredSegment; - const hashPrefix = (await utils.getHash(sponsorVideoID, 1)).slice(0, 4) as VideoID & HashedValue; + const hashPrefix = (await utils.getHash(getVideoID(), 1)).slice(0, 4) as VideoID & HashedValue; const response = await utils.asyncRequestToServer('GET', "/api/skipSegments/" + hashPrefix, { categories, actionTypes: getEnabledActionTypes(showChapterMessage), @@ -1085,7 +1007,7 @@ async function sponsorsLookup(keepOldSubmissions = true) { if (response?.ok) { let recievedSegments: SponsorTime[] = JSON.parse(response.responseText) - ?.filter((video) => video.videoID === sponsorVideoID) + ?.filter((video) => video.videoID === getVideoID()) ?.map((video) => video.segments)?.[0] ?.map((segment) => ({ ...segment, @@ -1167,7 +1089,7 @@ async function sponsorsLookup(keepOldSubmissions = true) { //update the preview bar //leave the type blank for now until categories are added - if (lastPreviewBarUpdate == sponsorVideoID || (lastPreviewBarUpdate == null && !isNaN(video.duration))) { + if (lastPreviewBarUpdate == getVideoID() || (lastPreviewBarUpdate == null && !isNaN(getVideo().duration))) { //set it now //otherwise the listener can handle it updatePreviewBar(); @@ -1187,7 +1109,7 @@ async function sponsorsLookup(keepOldSubmissions = true) { found: sponsorDataFound, status: lastResponseStatus, sponsorTimes: sponsorTimes, - time: video.currentTime, + time: getVideo().currentTime, onMobileYouTube }); @@ -1198,7 +1120,7 @@ async function sponsorsLookup(keepOldSubmissions = true) { function importExistingChapters(wait: boolean) { if (!existingChaptersImported) { - waitFor(() => video?.duration && getExistingChapters(sponsorVideoID, video.duration), + waitFor(() => getVideo()?.duration && getExistingChapters(getVideoID(), getVideo().duration), wait ? 15000 : 0, 400, (c) => c?.length > 0).then((chapters) => { if (!existingChaptersImported && chapters?.length > 0) { sponsorTimes = (sponsorTimes ?? []).concat(...chapters).sort((a, b) => a.segment[0] - b.segment[0]); @@ -1222,12 +1144,12 @@ function getEnabledActionTypes(forceFullVideo = false): ActionType[] { } async function lockedCategoriesLookup(): Promise { - const hashPrefix = (await utils.getHash(sponsorVideoID, 1)).slice(0, 4); + const hashPrefix = (await utils.getHash(getVideoID(), 1)).slice(0, 4); const response = await utils.asyncRequestToServer("GET", "/api/lockCategories/" + hashPrefix); if (response.ok) { try { - const categoriesResponse = JSON.parse(response.responseText).filter((lockInfo) => lockInfo.videoID === sponsorVideoID)[0]?.categories; + const categoriesResponse = JSON.parse(response.responseText).filter((lockInfo) => lockInfo.videoID === getVideoID())[0]?.categories; if (Array.isArray(categoriesResponse)) { lockedCategories = categoriesResponse; } @@ -1249,7 +1171,7 @@ function retryFetch(errorCode: number): void { const delay = errorCode === 404 ? (30000 + Math.random() * 30000) : (2000 + Math.random() * 10000); retryFetchTimeout = setTimeout(() => { - if (sponsorVideoID && sponsorTimes?.length === 0 + if (getVideoID() && sponsorTimes?.length === 0 || sponsorTimes.every((segment) => segment.source !== SponsorSourceType.Server)) { // sponsorsLookup(); } @@ -1268,7 +1190,7 @@ function startSkipScheduleCheckingForStartSponsors() { let startingSegmentTime = getStartTimeFromUrl(document.URL) || -1; let found = false; for (const time of sponsorTimes) { - if (time.segment[0] <= video.currentTime && time.segment[0] > startingSegmentTime && time.segment[1] > video.currentTime + if (time.segment[0] <= getVideo().currentTime && time.segment[0] > startingSegmentTime && time.segment[1] > getVideo().currentTime && time.actionType !== ActionType.Poi) { startingSegmentTime = time.segment[0]; found = true; @@ -1277,7 +1199,7 @@ function startSkipScheduleCheckingForStartSponsors() { } if (!found) { for (const time of sponsorTimesSubmitting) { - if (time.segment[0] <= video.currentTime && time.segment[0] > startingSegmentTime && time.segment[1] > video.currentTime + if (time.segment[0] <= getVideo().currentTime && time.segment[0] > startingSegmentTime && time.segment[1] > getVideo().currentTime && time.actionType !== ActionType.Poi) { startingSegmentTime = time.segment[0]; found = true; @@ -1288,18 +1210,18 @@ function startSkipScheduleCheckingForStartSponsors() { // For highlight category const poiSegments = sponsorTimes - .filter((time) => time.segment[1] > video.currentTime + .filter((time) => time.segment[1] > getVideo().currentTime && time.actionType === ActionType.Poi && time.hidden === SponsorHideType.Visible) .sort((a, b) => b.segment[0] - a.segment[0]); for (const time of poiSegments) { const skipOption = utils.getCategorySelection(time.category)?.option; if (skipOption !== CategorySkipOption.ShowOverlay) { skipToTime({ - v: video, + v: getVideo(), skipTime: time.segment, skippingSegments: [time], openNotice: true, - unskipTime: video.currentTime + unskipTime: getVideo().currentTime }); if (skipOption === CategorySkipOption.AutoSkip) break; } @@ -1318,82 +1240,6 @@ function startSkipScheduleCheckingForStartSponsors() { } } -function getYouTubeVideoID(document: Document, url?: string): string { - url ||= document.URL; - // pageType shortcut - if (pageType === PageType.Channel) return getYouTubeVideoIDFromDocument(); - // clips should never skip, going from clip to full video has no indications. - if (url.includes("youtube.com/clip/")) return null; - // skip to document and don't hide if on /embed/ - if (url.includes("/embed/") && url.includes("youtube.com")) return getYouTubeVideoIDFromDocument(false, PageType.Embed); - // skip to URL if matches youtube watch or invidious or matches youtube pattern - if ((!url.includes("youtube.com")) || url.includes("/watch") || url.includes("/shorts/") || url.includes("playlist")) return getYouTubeVideoIDFromURL(url); - // skip to document if matches pattern - if (url.includes("/channel/") || url.includes("/user/") || url.includes("/c/")) return getYouTubeVideoIDFromDocument(true, PageType.Channel); - // not sure, try URL then document - return getYouTubeVideoIDFromURL(url) || getYouTubeVideoIDFromDocument(false); -} - -function getYouTubeVideoIDFromDocument(hideIcon = true, pageHint = PageType.Watch): string { - const selector = "a.ytp-title-link[data-sessionlink='feature=player-title']"; - // get ID from document (channel trailer / embedded playlist) - const element = pageHint === PageType.Embed ? document.querySelector(selector) - : video?.parentElement?.parentElement?.querySelector(selector); - const videoURL = element?.getAttribute("href"); - if (videoURL) { - onInvidious = hideIcon; - // if href found, hint was correct - pageType = pageHint; - return getYouTubeVideoIDFromURL(videoURL); - } else { - return null; - } -} - -function getYouTubeVideoIDFromURL(url: string): string { - if(url.startsWith("https://www.youtube.com/tv#/")) url = url.replace("#", ""); - - //Attempt to parse url - let urlObject: URL = null; - try { - urlObject = new URL(url); - } catch (e) { - console.error("[SB] Unable to parse URL: " + url); - return null; - } - - // Check if valid hostname - if (Config.config && Config.config.invidiousInstances.includes(urlObject.host)) { - onInvidious = true; - } else if (urlObject.host === "m.youtube.com") { - onMobileYouTube = true; - } else if (!["m.youtube.com", "www.youtube.com", "www.youtube-nocookie.com", "music.youtube.com"].includes(urlObject.host)) { - if (!Config.config) { - // Call this later, in case this is an Invidious tab - utils.wait(() => Config.config !== null).then(() => videoIDChange(getYouTubeVideoIDFromURL(url))); - } - - return null; - } else { - onInvidious = false; - } - - //Get ID from searchParam - if (urlObject.searchParams.has("v") && ["/watch", "/watch/"].includes(urlObject.pathname) || urlObject.pathname.startsWith("/tv/watch")) { - const id = urlObject.searchParams.get("v"); - return id.length == 11 ? id : null; - } else if (urlObject.pathname.startsWith("/embed/") || urlObject.pathname.startsWith("/shorts/")) { - try { - const id = urlObject.pathname.split("/")[2] - if (id?.length >=11 ) return id.slice(0, 11); - } catch (e) { - console.error("[SB] Video ID not valid for " + url); - return null; - } - } - return null; -} - /** * This function is required on mobile YouTube and will keep getting called whenever the preview bar disapears */ @@ -1406,12 +1252,12 @@ function updatePreviewBarPositionMobile(parent: HTMLElement) { function updatePreviewBar(): void { if (previewBar === null) return; - if (isAdPlaying) { + if (getIsAdPlaying()) { previewBar.clear(); return; } - if (video === null) return; + if (getVideo() === null) return; const hashParams = getHashParams(); const requiredSegment = hashParams?.requiredSegment as SegmentUUID || undefined; @@ -1445,8 +1291,8 @@ function updatePreviewBar(): void { }); }); - previewBar.set(previewBarSegments.filter((segment) => segment.actionType !== ActionType.Full), video?.duration) - if (video) updateActiveSegment(video.currentTime); + previewBar.set(previewBarSegments.filter((segment) => segment.actionType !== ActionType.Full), getVideo()?.duration) + if (getVideo()) updateActiveSegment(getVideo().currentTime); if (Config.config.showTimeWithSkips) { const skippedDuration = utils.getTimestampsDuration(previewBarSegments @@ -1457,41 +1303,13 @@ function updatePreviewBar(): void { } // Update last video id - lastPreviewBarUpdate = sponsorVideoID; + lastPreviewBarUpdate = getVideoID(); } //checks if this channel is whitelisted, should be done only after the channelID has been loaded -async function whitelistCheck() { +async function channelIDChange(channelIDInfo: ChannelIDInfo) { const whitelistedChannels = Config.config.whitelistedChannels; - try { - waitingForChannelID = true; - await utils.wait(() => channelIDInfo.status === ChannelIDStatus.Found, 6000, 20); - - // If found, continue on, it was set by the listener - } catch (e) { - // Try fallback - const channelIDFallback = (document.querySelector("a.ytd-video-owner-renderer") // YouTube - ?? document.querySelector("a.ytp-title-channel-logo") // YouTube Embed - ?? document.querySelector(".channel-profile #channel-name")?.parentElement.parentElement // Invidious - ?? document.querySelector("a.slim-owner-icon-and-title")) // Mobile YouTube - ?.getAttribute("href")?.match(/\/(?:channel|c|user)\/(UC[a-zA-Z0-9_-]{22}|[a-zA-Z0-9_-]+)/)?.[1]; - - if (channelIDFallback) { - channelIDInfo = { - status: ChannelIDStatus.Found, - id: channelIDFallback - }; - } else { - channelIDInfo = { - status: ChannelIDStatus.Failed, - id: null - }; - } - } - - waitingForChannelID = false; - //see if this is a whitelisted channel if (whitelistedChannels != undefined && channelIDInfo.status === ChannelIDStatus.Found && whitelistedChannels.includes(channelIDInfo.id)) { @@ -1663,11 +1481,11 @@ function getStartTimes(sponsorTimes: SponsorTime[], includeIntersectingSegments: * @param time */ function previewTime(time: number, unpause = true) { - video.currentTime = time; + getVideo().currentTime = time; // Unpause the video if needed - if (unpause && video.paused){ - video.play(); + if (unpause && getVideo().paused){ + getVideo().play(); } } @@ -1730,7 +1548,7 @@ function skipToTime({v, skipTime, skippingSegments, openNotice, forceAutoSkip, u if (autoSkip && Config.config.audioNotificationOnSkip) { const beep = new Audio(chrome.runtime.getURL("icons/beep.ogg")); - beep.volume = video.volume * 0.1; + beep.volume = getVideo().volume * 0.1; const oldMetadata = navigator.mediaSession.metadata beep.play(); beep.addEventListener("ended", () => { @@ -1791,27 +1609,27 @@ function createSkipNotice(skippingSegments: SponsorTime[], autoSkip: boolean, un function unskipSponsorTime(segment: SponsorTime, unskipTime: number = null, forceSeek = false) { if (segment.actionType === ActionType.Mute) { - video.muted = false; + getVideo().muted = false; videoMuted = false; } if (forceSeek || segment.actionType === ActionType.Skip) { //add a tiny bit of time to make sure it is not skipped again - video.currentTime = unskipTime ?? segment.segment[0] + 0.001; + getVideo().currentTime = unskipTime ?? segment.segment[0] + 0.001; } } function reskipSponsorTime(segment: SponsorTime, forceSeek = false) { if (segment.actionType === ActionType.Mute && !forceSeek) { - video.muted = true; + getVideo().muted = true; videoMuted = true; } else { - const skippedTime = Math.max(segment.segment[1] - video.currentTime, 0); + const skippedTime = Math.max(segment.segment[1] - getVideo().currentTime, 0); const segmentDuration = segment.segment[1] - segment.segment[0]; const fullSkip = skippedTime / segmentDuration > manualSkipPercentCount; - video.currentTime = segment.segment[1]; + getVideo().currentTime = segment.segment[1]; sendTelemetryAndCount([segment], segment.actionType !== ActionType.Chapter ? skippedTime : 0, fullSkip); startSponsorSchedule(true, segment.segment[1], false); } @@ -1892,7 +1710,7 @@ async function createButtons(): Promise { /** Creates any missing buttons on the player and updates their visiblity. */ async function updateVisibilityOfPlayerControlsButton(): Promise { // Not on a proper video yet - if (!sponsorVideoID || onMobileYouTube) return; + if (!getVideoID() || onMobileYouTube) return; await createButtons(); @@ -1910,7 +1728,7 @@ async function updateVisibilityOfPlayerControlsButton(): Promise { /** Updates the visibility of buttons on the player related to creating segments. */ function updateEditButtonsOnPlayer(): void { // Don't try to update the buttons if we aren't on a YouTube video page - if (!sponsorVideoID || onMobileYouTube) return; + if (!getVideoID() || onMobileYouTube) return; const buttonsEnabled = !(Config.config.hideVideoPlayerControls || onInvidious); @@ -1949,7 +1767,7 @@ function updateEditButtonsOnPlayer(): void { /** * Used for submitting. This will use the HTML displayed number when required as the video's - * current time is out of date while scrubbing or at the end of the video. This is not needed + * current time is out of date while scrubbing or at the end of the getVideo(). This is not needed * for sponsor skipping as the video is not playing during these times. */ function getRealCurrentTime(): number { @@ -1959,9 +1777,9 @@ function getRealCurrentTime(): number { if (playButtonSVGData === replaceSVGData) { // At the end of the video - return video?.duration; + return getVideo()?.duration; } else { - return video.currentTime; + return getVideo().currentTime; } } @@ -1986,7 +1804,7 @@ function startOrEndTimingNewSegment() { } // Save the newly created segment - Config.config.unsubmittedSegments[sponsorVideoID] = sponsorTimesSubmitting; + Config.config.unsubmittedSegments[getVideoID()] = sponsorTimesSubmitting; Config.forceSyncUpdate("unsubmittedSegments"); // Make sure they know if someone has already submitted something it while they were watching @@ -2019,11 +1837,11 @@ function cancelCreatingSegment() { if (isSegmentCreationInProgress()) { if (sponsorTimesSubmitting.length > 1) { // If there's more than one segment: remove last sponsorTimesSubmitting.pop(); - Config.config.unsubmittedSegments[sponsorVideoID] = sponsorTimesSubmitting; + Config.config.unsubmittedSegments[getVideoID()] = sponsorTimesSubmitting; } else { // Otherwise delete the video entry & close submission menu resetSponsorSubmissionNotice(); sponsorTimesSubmitting = []; - delete Config.config.unsubmittedSegments[sponsorVideoID]; + delete Config.config.unsubmittedSegments[getVideoID()]; } Config.forceSyncUpdate("unsubmittedSegments"); } @@ -2033,7 +1851,7 @@ function cancelCreatingSegment() { } function updateSponsorTimesSubmitting(getFromConfig = true) { - const segmentTimes = Config.config.unsubmittedSegments[sponsorVideoID]; + const segmentTimes = Config.config.unsubmittedSegments[getVideoID()]; //see if this data should be saved in the sponsorTimesSubmitting variable if (getFromConfig && segmentTimes != undefined) { @@ -2058,7 +1876,7 @@ function updateSponsorTimesSubmitting(getFromConfig = true) { updatePreviewBar(); // Restart skipping schedule - if (video !== null) startSponsorSchedule(); + if (getVideo() !== null) startSponsorSchedule(); if (submissionNotice !== null) { submissionNotice.update(); @@ -2131,7 +1949,7 @@ function closeInfoMenu() { } function clearSponsorTimes() { - const currentVideoID = sponsorVideoID; + const currentVideoID = getVideoID(); const sponsorTimes = Config.config.unsubmittedSegments[currentVideoID]; @@ -2225,7 +2043,7 @@ async function voteAsync(type: number, UUID: SegmentUUID, category?: Category): } if (!category && !Config.config.isVip) { - utils.addHiddenSegment(sponsorVideoID, segment.UUID, segment.hidden); + utils.addHiddenSegment(getVideoID(), segment.UUID, segment.hidden); } updatePreviewBar(); @@ -2277,7 +2095,7 @@ async function sendSubmitMessage() { // check if all segments are full video const onlyFullVideo = sponsorTimesSubmitting.every((segment) => segment.actionType === ActionType.Full); // Block if submitting on a running livestream or premiere - if (!onlyFullVideo && (isLivePremiere || isVisible(document.querySelector(".ytp-live-badge")))) { + if (!onlyFullVideo && (getIsLivePremiere() || isVisible(document.querySelector(".ytp-live-badge")))) { alert(chrome.i18n.getMessage("liveOrPremiere")); return; } @@ -2288,13 +2106,13 @@ async function sendSubmitMessage() { //check if a sponsor exceeds the duration of the video for (let i = 0; i < sponsorTimesSubmitting.length; i++) { - if (sponsorTimesSubmitting[i].segment[1] > video.duration) { - sponsorTimesSubmitting[i].segment[1] = video.duration; + if (sponsorTimesSubmitting[i].segment[1] > getVideo().duration) { + sponsorTimesSubmitting[i].segment[1] = getVideo().duration; } } //update sponsorTimes - Config.config.unsubmittedSegments[sponsorVideoID] = sponsorTimesSubmitting; + Config.config.unsubmittedSegments[getVideoID()] = sponsorTimesSubmitting; Config.forceSyncUpdate("unsubmittedSegments"); // Check to see if any of the submissions are below the minimum duration set @@ -2311,10 +2129,10 @@ async function sendSubmitMessage() { } const response = await utils.asyncRequestToServer("POST", "/api/skipSegments", { - videoID: sponsorVideoID, + videoID: getVideoID(), userID: Config.config.userID, segments: sponsorTimesSubmitting, - videoDuration: video?.duration, + videoDuration: getVideo()?.duration, userAgent: `${chrome.runtime.id}/v${chrome.runtime.getManifest().version}` }); @@ -2322,7 +2140,7 @@ async function sendSubmitMessage() { stopAnimation(); // Remove segments from storage since they've already been submitted - delete Config.config.unsubmittedSegments[sponsorVideoID]; + delete Config.config.unsubmittedSegments[getVideoID()]; Config.forceSyncUpdate("unsubmittedSegments"); const newSegments = sponsorTimesSubmitting; @@ -2390,40 +2208,6 @@ function getSegmentsMessage(sponsorTimes: SponsorTime[]): string { return sponsorTimesMessage; } -function windowListenerHandler(event: MessageEvent): void { - const data = event.data; - const dataType = data.type; - - if (data.source !== "sponsorblock" || document?.URL?.includes("youtube.com/clip/")) return; - - if (dataType === "navigation" && data.videoID) { - pageType = data.pageType; - - if (data.channelID) { - channelIDInfo = { - id: data.channelID, - status: ChannelIDStatus.Found - }; - - if (!waitingForChannelID) { - whitelistCheck(); - } - } - - videoIDChange(data.videoID); - } else if (dataType === "ad") { - if (isAdPlaying != data.playing) { - isAdPlaying = data.playing - updatePreviewBar(); - updateVisibilityOfPlayerControlsButton(); - } - } else if (dataType === "data" && data.videoID) { - videoIDChange(data.videoID); - - isLivePremiere = data.isLive || data.isPremiere - } -} - function updateActiveSegment(currentTime: number): void { const activeSegments = previewBar?.updateChapterText(sponsorTimes, sponsorTimesSubmitting, currentTime); chrome.runtime.sendMessage({ @@ -2441,65 +2225,41 @@ function nextChapter(): void { const chapters = previewBar.unfilteredChapterGroups?.filter((time) => [ActionType.Chapter, null].includes(time.actionType)); if (!chapters || chapters.length <= 0) return; - lastNextChapterKeybind.time = video.currentTime; + lastNextChapterKeybind.time = getVideo().currentTime; lastNextChapterKeybind.date = Date.now(); - const nextChapter = chapters.findIndex((time) => time.segment[0] > video.currentTime); + const nextChapter = chapters.findIndex((time) => time.segment[0] > getVideo().currentTime); if (nextChapter !== -1) { - video.currentTime = chapters[nextChapter].segment[0]; + getVideo().currentTime = chapters[nextChapter].segment[0]; } else { - video.currentTime = video.duration; + getVideo().currentTime = getVideo().duration; } } function previousChapter(): void { if (Date.now() - lastNextChapterKeybind.date < 3000) { - video.currentTime = lastNextChapterKeybind.time; + getVideo().currentTime = lastNextChapterKeybind.time; lastNextChapterKeybind.date = 0; return; } const chapters = previewBar.unfilteredChapterGroups?.filter((time) => [ActionType.Chapter, null].includes(time.actionType)); if (!chapters || chapters.length <= 0) { - video.currentTime = 0; + getVideo().currentTime = 0; return; } // subtract 5 seconds to allow skipping back to the previous chapter if close to start of // the current one - const nextChapter = chapters.findIndex((time) => time.segment[0] > video.currentTime - Math.min(5, time.segment[1] - time.segment[0])); + const nextChapter = chapters.findIndex((time) => time.segment[0] > getVideo().currentTime - Math.min(5, time.segment[1] - time.segment[0])); const previousChapter = nextChapter !== -1 ? (nextChapter - 1) : (chapters.length - 1); if (previousChapter !== -1) { - video.currentTime = chapters[previousChapter].segment[0]; + getVideo().currentTime = chapters[previousChapter].segment[0]; } else { - video.currentTime = 0; + getVideo().currentTime = 0; } } -function addPageListeners(): void { - const refreshListners = () => { - if (!isVisible(video)) { - refreshVideoAttachments(); - } - }; - - // inject into document - const docScript = document.createElement("script"); - docScript.src = chrome.runtime.getURL("js/document.js"); - // Not injected on invidious - (document.head || document.documentElement)?.appendChild(docScript); - - document.addEventListener("yt-navigate-start", resetValues); - document.addEventListener("yt-navigate-finish", refreshListners); - // piped player init - window.addEventListener("playerInit", () => { - if (!document.querySelector('meta[property="og:title"][content="Piped"]')) return - previewBar = null; // remove old previewbar - createPreviewBar() - }); - window.addEventListener("message", windowListenerHandler); -} - function addHotkeyListener(): void { document.addEventListener("keydown", hotkeyListener); @@ -2584,9 +2344,9 @@ function addCSS() { * Update the isAdPlaying flag and hide preview bar/controls if ad is playing */ function updateAdFlag(): void { - const wasAdPlaying = isAdPlaying; - isAdPlaying = document.getElementsByClassName('ad-showing').length > 0; - if(wasAdPlaying != isAdPlaying) { + const wasAdPlaying = getIsAdPlaying(); + setIsAdPlaying(document.getElementsByClassName('ad-showing').length > 0); + if(wasAdPlaying != getIsAdPlaying()) { updatePreviewBar(); updateVisibilityOfPlayerControlsButton(); } @@ -2616,7 +2376,7 @@ function showTimeWithoutSkips(skippedDuration: number): void { display.appendChild(duration); } - const durationAfterSkips = getFormattedTime(video?.duration - skippedDuration); + const durationAfterSkips = getFormattedTime(getVideo()?.duration - skippedDuration); duration.innerText = (durationAfterSkips == null || skippedDuration <= 0) ? "" : " (" + durationAfterSkips + ")"; } @@ -2649,23 +2409,7 @@ function checkForPreloadedSegment() { } if (pushed) { - Config.config.unsubmittedSegments[sponsorVideoID] = sponsorTimesSubmitting; + Config.config.unsubmittedSegments[getVideoID()] = sponsorTimesSubmitting; Config.forceSyncUpdate("unsubmittedSegments"); } -} - -// Register listener for URL change via Navigation API -const navigationApiAvailable = "navigation" in window; -if (navigationApiAvailable) { - // TODO: Remove type cast once type declarations are updated - (window as unknown as { navigation: EventTarget }).navigation.addEventListener("navigate", (e) => - videoIDChange(getYouTubeVideoID(document, (e as unknown as Record>).destination.url))); -} - -// Record availability of Navigation API -utils.wait(() => Config.local !== null).then(() => { - if (Config.local.navigationApiAvailable !== navigationApiAvailable) { - Config.local.navigationApiAvailable = navigationApiAvailable; - Config.forceLocalUpdate("navigationApiAvailable"); - } -}); +} \ No newline at end of file diff --git a/src/document.ts b/src/document.ts index 2b53b462..ed4c2840 100644 --- a/src/document.ts +++ b/src/document.ts @@ -1,95 +1,3 @@ -/* - Content script are run in an isolated DOM so it is not possible to access some key details that are sanitized when passed cross-dom - This script is used to get the details from the page and make them available for the content script by being injected directly into the page -*/ +import { init } from "@ajayyy/maze-utils/lib/injected/document"; -import { PageType } from "./types"; - -interface StartMessage { - type: "navigation"; - pageType: PageType; - videoID: string | null; -} - -interface FinishMessage extends StartMessage { - channelID: string; - channelTitle: string; -} - -interface AdMessage { - type: "ad"; - playing: boolean; -} - -interface VideoData { - type: "data"; - videoID: string; - isLive: boolean; - isPremiere: boolean; -} - -type WindowMessage = StartMessage | FinishMessage | AdMessage | VideoData; - -// global playerClient - too difficult to type -// eslint-disable-next-line @typescript-eslint/no-explicit-any -let playerClient: any; -let lastVideo = ""; - -const sendMessage = (message: WindowMessage): void => { - window.postMessage({ source: "sponsorblock", ...message }, "/"); -} - -function setupPlayerClient(e: CustomEvent): void { - if (playerClient) return; // early exit if already defined - - playerClient = e.detail; - sendVideoData(); // send playerData after setup - - e.detail.addEventListener('onAdStart', () => sendMessage({ type: "ad", playing: true } as AdMessage)); - e.detail.addEventListener('onAdFinish', () => sendMessage({ type: "ad", playing: false } as AdMessage)); -} - -document.addEventListener("yt-player-updated", setupPlayerClient); -document.addEventListener("yt-navigate-start", navigationStartSend); -document.addEventListener("yt-navigate-finish", navigateFinishSend); - -function navigationParser(event: CustomEvent): StartMessage { - const pageType: PageType = event.detail.pageType; - if (pageType) { - const result: StartMessage = { type: "navigation", pageType, videoID: null }; - if (pageType === "shorts" || pageType === "watch") { - const endpoint = event.detail.endpoint - if (!endpoint) return null; - - result.videoID = (pageType === "shorts" ? endpoint.reelWatchEndpoint : endpoint.watchEndpoint).videoId; - } - - return result; - } else { - return null; - } -} - -function navigationStartSend(event: CustomEvent): void { - const message = navigationParser(event) as StartMessage; - if (message) { - sendMessage(message); - } -} - -function navigateFinishSend(event: CustomEvent): void { - sendVideoData(); // arrived at new video, send video data - const videoDetails = event.detail?.response?.playerResponse?.videoDetails; - if (videoDetails) { - sendMessage({ channelID: videoDetails.channelId, channelTitle: videoDetails.author, ...navigationParser(event) } as FinishMessage); - } -} - -function sendVideoData(): void { - if (!playerClient) return; - const videoData = playerClient.getVideoData(); - if (videoData && videoData.video_id !== lastVideo) { - lastVideo = videoData.video_id; - sendMessage({ type: "data", videoID: videoData.video_id, isLive: videoData.isLive, isPremiere: videoData.isPremiere } as VideoData); - } -} \ No newline at end of file +init(); \ No newline at end of file diff --git a/src/js-components/previewBar.ts b/src/js-components/previewBar.ts index 2d105c2c..30d54d3a 100644 --- a/src/js-components/previewBar.ts +++ b/src/js-components/previewBar.ts @@ -11,8 +11,8 @@ import { ActionType, Category, SegmentContainer, SponsorHideType, SponsorSourceT import { partition } from "../utils/arrayUtils"; import { DEFAULT_CATEGORY, shortCategoryName } from "../utils/categoryUtils"; import { normalizeChapterName } from "../utils/exporter"; -import { findValidElement } from "../utils/pageUtils"; import { getFormattedTimeToSeconds } from "@ajayyy/maze-utils/lib/formating"; +import { findValidElement } from "@ajayyy/maze-utils/lib/dom"; const TOOLTIP_VISIBLE_CLASS = 'sponsorCategoryTooltipVisible'; const MIN_CHAPTER_SIZE = 0.003; diff --git a/src/options.ts b/src/options.ts index e963a683..c6e4aae1 100644 --- a/src/options.ts +++ b/src/options.ts @@ -14,7 +14,7 @@ import UnsubmittedVideos from "./render/UnsubmittedVideos"; import KeybindComponent from "./components/options/KeybindComponent"; import { showDonationLink } from "./utils/configUtils"; import { localizeHtmlPage } from "./utils/pageUtils"; -import { StorageChangesObject } from "./types"; +import { StorageChangesObject } from "@ajayyy/maze-utils/lib/config"; const utils = new Utils(); let embed = false; diff --git a/src/popup.ts b/src/popup.ts index f631d7cd..edd4e9e5 100644 --- a/src/popup.ts +++ b/src/popup.ts @@ -7,7 +7,6 @@ import { SponsorHideType, SponsorSourceType, SponsorTime, - StorageChangesObject, } from "./types"; import { GetChannelIDResponse, @@ -28,6 +27,7 @@ import { exportTimes } from "./utils/exporter"; import GenericNotice from "./render/GenericNotice"; import { noRefreshFetchingChaptersAllowed } from "./utils/licenseKey"; import { getFormattedTime } from "@ajayyy/maze-utils/lib/formating"; +import { StorageChangesObject } from "@ajayyy/maze-utils/lib/config"; const utils = new Utils(); diff --git a/src/types.ts b/src/types.ts index 9be4f9ce..4454d511 100644 --- a/src/types.ts +++ b/src/types.ts @@ -194,8 +194,6 @@ export interface VideoInfo { export type VideoID = string; -export type StorageChangesObject = { [key: string]: chrome.storage.StorageChange }; - export type UnEncodedSegmentTimes = [string, SponsorTime[]][]; export enum ChannelIDStatus { @@ -239,15 +237,6 @@ export type Keybind = { shift?: boolean; } -export enum PageType { - Shorts = "shorts", - Watch = "watch", - Search = "search", - Browse = "browse", - Channel = "channel", - Embed = "embed" -} - export interface ButtonListener { name: string; listener: (e?: React.MouseEvent) => void; diff --git a/src/utils.ts b/src/utils.ts index 8d8b4b16..1cdbf73f 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -2,9 +2,9 @@ import Config, { VideoDownvotes } from "./config"; import { CategorySelection, SponsorTime, FetchResponse, BackgroundScriptContainer, Registration, HashedValue, VideoID, SponsorHideType } from "./types"; import * as CompileConfig from "../config.json"; -import { findValidElement, findValidElementFromSelector } from "./utils/pageUtils"; import { waitFor } from "@ajayyy/maze-utils"; import { isSafari } from "./utils/configUtils"; +import { findValidElementFromSelector } from "@ajayyy/maze-utils/lib/dom"; export default class Utils { @@ -23,11 +23,6 @@ export default class Utils { "shared.css" ]; - /* Used for waitForElement */ - creatingWaitingMutationObserver = false; - waitingMutationObserver: MutationObserver = null; - waitingElements: { selector: string; visibleCheck: boolean; callback: (element: Element) => void }[] = []; - constructor(backgroundScriptContainer: BackgroundScriptContainer = null) { this.backgroundScriptContainer = backgroundScriptContainer; } @@ -36,74 +31,6 @@ export default class Utils { return waitFor(condition, timeout, check); } - /* Uses a mutation observer to wait asynchronously */ - async waitForElement(selector: string, visibleCheck = false): Promise { - return await new Promise((resolve) => { - const initialElement = this.getElement(selector, visibleCheck); - if (initialElement) { - resolve(initialElement); - return; - } - - this.waitingElements.push({ - selector, - visibleCheck, - callback: resolve - }); - - if (!this.creatingWaitingMutationObserver) { - this.creatingWaitingMutationObserver = true; - - if (document.body) { - this.setupWaitingMutationListener(); - } else { - window.addEventListener("DOMContentLoaded", () => { - this.setupWaitingMutationListener(); - }); - } - } - }); - } - - private setupWaitingMutationListener(): void { - if (!this.waitingMutationObserver) { - const checkForObjects = () => { - const foundSelectors = []; - for (const { selector, visibleCheck, callback } of this.waitingElements) { - const element = this.getElement(selector, visibleCheck); - if (element) { - callback(element); - foundSelectors.push(selector); - } - } - - this.waitingElements = this.waitingElements.filter((element) => !foundSelectors.includes(element.selector)); - - if (this.waitingElements.length === 0) { - this.waitingMutationObserver?.disconnect(); - this.waitingMutationObserver = null; - this.creatingWaitingMutationObserver = false; - } - }; - - // Do an initial check over all objects - checkForObjects(); - - if (this.waitingElements.length > 0) { - this.waitingMutationObserver = new MutationObserver(checkForObjects); - - this.waitingMutationObserver.observe(document.body, { - childList: true, - subtree: true - }); - } - } - } - - private getElement(selector: string, visibleCheck: boolean) { - return visibleCheck ? findValidElement(document.querySelectorAll(selector)) : document.querySelector(selector); - } - containsPermission(permissions: chrome.permissions.Permissions): Promise { return new Promise((resolve) => { chrome.permissions.contains(permissions, resolve) diff --git a/src/utils/pageUtils.ts b/src/utils/pageUtils.ts index 5281bf1f..c6b5b92d 100644 --- a/src/utils/pageUtils.ts +++ b/src/utils/pageUtils.ts @@ -28,25 +28,6 @@ export function isVisible(element: HTMLElement): boolean { return element && element.offsetWidth > 0 && element.offsetHeight > 0; } -export function findValidElementFromSelector(selectors: string[]): HTMLElement { - return findValidElementFromGenerator(selectors, (selector) => document.querySelector(selector)); -} - -export function findValidElement(elements: HTMLElement[] | NodeListOf): HTMLElement { - return findValidElementFromGenerator(elements); -} - -function findValidElementFromGenerator(objects: T[] | NodeListOf, generator?: (obj: T) => HTMLElement): HTMLElement { - for (const obj of objects) { - const element = generator ? generator(obj as T) : obj as HTMLElement; - if (element && isVisible(element)) { - return element; - } - } - - return null; -} - export function getHashParams(): Record { const windowHash = window.location.hash.slice(1); if (windowHash) {