diff options
author | Ajay <[email protected]> | 2022-08-19 23:16:45 -0400 |
---|---|---|
committer | Ajay <[email protected]> | 2022-08-19 23:16:45 -0400 |
commit | 42d76cf2579968116473cdc24b70d18f84277b66 (patch) | |
tree | 503e347a52680399d8363472ec5414457a2ec38b /src | |
parent | d06b7411dc828b1afcc100b439ff483106c2ab74 (diff) | |
parent | 780ea4a9d0b12762d10e787786816e3e8a6f9b26 (diff) | |
download | SponsorBlock-42d76cf2579968116473cdc24b70d18f84277b66.tar.gz SponsorBlock-42d76cf2579968116473cdc24b70d18f84277b66.zip |
Merge branch 'master' of https://github.com/ajayyy/SponsorBlock into chapters
Diffstat (limited to 'src')
-rw-r--r-- | src/background.ts | 10 | ||||
-rw-r--r-- | src/components/NoticeTextSectionComponent.tsx | 21 | ||||
-rw-r--r-- | src/components/SponsorTimeEditComponent.tsx | 14 | ||||
-rw-r--r-- | src/components/options/CategorySkipOptionsComponent.tsx | 37 | ||||
-rw-r--r-- | src/content.ts | 219 | ||||
-rw-r--r-- | src/js-components/chat.ts | 47 | ||||
-rw-r--r-- | src/messageTypes.ts | 3 | ||||
-rw-r--r-- | src/options.ts | 33 | ||||
-rw-r--r-- | src/permissions.ts | 20 | ||||
-rw-r--r-- | src/popup.ts | 6 | ||||
-rw-r--r-- | src/render/GenericNotice.tsx | 10 | ||||
-rw-r--r-- | src/utils.ts | 123 | ||||
-rw-r--r-- | src/utils/genericUtils.ts | 25 | ||||
-rw-r--r-- | src/utils/warnings.ts | 66 |
14 files changed, 397 insertions, 237 deletions
diff --git a/src/background.ts b/src/background.ts index 15b4c15d..80f0cd8d 100644 --- a/src/background.ts +++ b/src/background.ts @@ -125,7 +125,7 @@ chrome.runtime.onConnect.addListener((port) => { chrome.runtime.onInstalled.addListener(function () { // This let's the config sync to run fully before checking. // This is required on Firefox - setTimeout(function() { + setTimeout(async () => { const userID = Config.config.userID; // If there is no userID, then it is the first install. @@ -141,6 +141,12 @@ chrome.runtime.onInstalled.addListener(function () { // Don't show update notification Config.config.categoryPillUpdate = true; } + + if (Config.config.supportInvidious) { + if (!(await utils.containsInvidiousPermission())) { + chrome.tabs.create({url: chrome.extension.getURL("/permissions/index.html")}); + } + } }, 1500); }); @@ -224,7 +230,7 @@ async function asyncRequestToServer(type: string, address: string, data = {}) { async function sendRequestToCustomServer(type: string, url: string, data = {}) { // If GET, convert JSON to parameters if (type.toLowerCase() === "get") { - url = utils.objectToURI(url, data, true); + url = GenericUtils.objectToURI(url, data, true); data = null; } diff --git a/src/components/NoticeTextSectionComponent.tsx b/src/components/NoticeTextSectionComponent.tsx index 71fcb263..122d0fde 100644 --- a/src/components/NoticeTextSectionComponent.tsx +++ b/src/components/NoticeTextSectionComponent.tsx @@ -36,12 +36,31 @@ class NoticeTextSelectionComponent extends React.Component<NoticeTextSelectionPr : null} <span> - {this.props.text} + {this.getTextElements(this.props.text)} </span> </td> </tr> ); } + + private getTextElements(text: string): Array<string | React.ReactElement> { + const elements: Array<string | React.ReactElement> = []; + const textParts = text.split(/(?=\s+)/); + for (const textPart of textParts) { + if (textPart.match(/^\s*http/)) { + elements.push( + <a href={textPart} target="_blank" rel="noreferrer"> + {textPart} + </a> + ); + } else { + elements.push(textPart); + } + + } + + return elements; + } } export default NoticeTextSelectionComponent;
\ No newline at end of file diff --git a/src/components/SponsorTimeEditComponent.tsx b/src/components/SponsorTimeEditComponent.tsx index 17014882..1515644a 100644 --- a/src/components/SponsorTimeEditComponent.tsx +++ b/src/components/SponsorTimeEditComponent.tsx @@ -115,14 +115,6 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo style.marginTop = "15px"; } - // This method is required to get !important - // https://stackoverflow.com/a/45669262/1985387 - const oldYouTubeDarkStyles = (node) => { - if (node) { - node.style.setProperty("color", "black", "important"); - node.style.setProperty("text-shadow", "none", "important"); - } - }; // Create time display let timeDisplay: JSX.Element; const timeDisplayStyle: React.CSSProperties = {}; @@ -142,8 +134,8 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo </span> <input id={"submittingTime0" + this.idSuffix} className="sponsorTimeEdit sponsorTimeEditInput" - ref={oldYouTubeDarkStyles} type="text" + style={{color: "inherit", backgroundColor: "inherit"}} value={this.state.sponsorTimeEdits[0]} onChange={(e) => this.handleOnChange(0, e, sponsorTime, e.target.value)} onWheel={(e) => this.changeTimesWhenScrolling(0, e, sponsorTime)}> @@ -157,8 +149,8 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo <input id={"submittingTime1" + this.idSuffix} className="sponsorTimeEdit sponsorTimeEditInput" - ref={oldYouTubeDarkStyles} type="text" + style={{color: "inherit", backgroundColor: "inherit"}} value={this.state.sponsorTimeEdits[1]} onChange={(e) => this.handleOnChange(1, e, sponsorTime, e.target.value)} onWheel={(e) => this.changeTimesWhenScrolling(1, e, sponsorTime)}> @@ -204,6 +196,7 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo className="sponsorTimeEditSelector sponsorTimeCategories" defaultValue={sponsorTime.category} ref={this.categoryOptionRef} + style={{color: "inherit", backgroundColor: "inherit"}} onChange={(event) => this.categorySelectionChange(event)}> {this.getCategoryOptions()} </select> @@ -227,6 +220,7 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo <select id={"sponsorTimeActionTypes" + this.idSuffix} className="sponsorTimeEditSelector sponsorTimeActionTypes" defaultValue={sponsorTime.actionType} + style={{color: "inherit", backgroundColor: "inherit"}} ref={this.actionTypeOptionRef} onChange={(e) => this.actionTypeSelectionChange(e)}> {this.getActionTypeOptions(sponsorTime)} diff --git a/src/components/options/CategorySkipOptionsComponent.tsx b/src/components/options/CategorySkipOptionsComponent.tsx index 32083ccb..b298347e 100644 --- a/src/components/options/CategorySkipOptionsComponent.tsx +++ b/src/components/options/CategorySkipOptionsComponent.tsx @@ -116,10 +116,10 @@ class CategorySkipOptionsComponent extends React.Component<CategorySkipOptionsPr skipOptionSelected(event: React.ChangeEvent<HTMLSelectElement>): void { let option: CategorySkipOption; - this.removeCurrentCategorySelection(); - switch (event.target.value) { - case "disable": + case "disable": + Config.config.categorySelections = Config.config.categorySelections.filter( + categorySelection => categorySelection.name !== this.props.category); return; case "showOverlay": option = CategorySkipOption.ShowOverlay; @@ -135,28 +135,17 @@ class CategorySkipOptionsComponent extends React.Component<CategorySkipOptionsPr break; } - Config.config.categorySelections.push({ - name: this.props.category, - option: option - }); - - // Forces the Proxy to send this to the chrome storage API - Config.config.categorySelections = Config.config.categorySelections; - } - - /** Removes this category from the config list of category selections */ - removeCurrentCategorySelection(): void { - // Remove it if it exists - for (let i = 0; i < Config.config.categorySelections.length; i++) { - if (Config.config.categorySelections[i].name === this.props.category) { - Config.config.categorySelections.splice(i, 1); - - // Forces the Proxy to send this to the chrome storage API - Config.config.categorySelections = Config.config.categorySelections; - - break; - } + const existingSelection = Config.config.categorySelections.find(selection => selection.name === this.props.category); + if (existingSelection) { + existingSelection.option = option; + } else { + Config.config.categorySelections.push({ + name: this.props.category, + option: option + }); } + + Config.forceSyncUpdate("categorySelections"); } getCategorySkipOptions(): JSX.Element[] { diff --git a/src/content.ts b/src/content.ts index f30d5760..fc611a89 100644 --- a/src/content.ts +++ b/src/content.ts @@ -10,7 +10,6 @@ import SkipNotice from "./render/SkipNotice"; import SkipNoticeComponent from "./components/SkipNoticeComponent"; import SubmissionNotice from "./render/SubmissionNotice"; import { Message, MessageResponse, VoteResponse } from "./messageTypes"; -import * as Chat from "./js-components/chat"; import { SkipButtonControlBar } from "./js-components/skipButtonControlBar"; import { getStartTimeFromUrl } from "./utils/urlParser"; import { findValidElement, getControls, getExistingChapters, getHashParams, isVisible } from "./utils/pageUtils"; @@ -20,6 +19,7 @@ import { AnimationUtils } from "./utils/animationUtils"; import { GenericUtils } from "./utils/genericUtils"; import { logDebug } from "./utils/logger"; import { importTimes } from "./utils/exporter"; +import { openWarningDialog } from "./utils/warnings"; // Hack to get the CSS loaded on permission-based sites (Invidious) utils.wait(() => Config.config !== null, 5000, 10).then(addCSS); @@ -62,13 +62,14 @@ let sponsorSkipped: boolean[] = []; 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[] = [] // This misleading variable name will be fixed soon -let onInvidious; -let onMobileYouTube; +let onInvidious: boolean; +let onMobileYouTube: boolean; //the video id of the last preview bar update let lastPreviewBarUpdate; @@ -76,9 +77,6 @@ let lastPreviewBarUpdate; // Is the video currently being switched let switchingVideos = null; -// Made true every videoID change -let firstEvent = false; - // Used by the play and playing listeners to make sure two aren't // called at the same time let lastCheckTime = 0; @@ -102,7 +100,8 @@ const playerButtons: Record<string, {button: HTMLButtonElement, image: HTMLImage // 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 -window.addEventListener("DOMContentLoaded", () => utils.waitForElement(".ytp-inline-preview-ui").then(() => refreshVideoAttachments())); +utils.waitForElement(".ytp-inline-preview-ui").then(() => refreshVideoAttachments()) +utils.waitForElement("a.ytp-title-link[data-sessionlink='feature=player-title']").then(() => videoIDChange(getYouTubeVideoID(document)).then()) addPageListeners(); addHotkeyListener(); @@ -119,6 +118,9 @@ 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; + // Contains all of the functions and variables needed by the skip notice const skipNoticeContentContainer: ContentContainer = () => ({ vote, @@ -166,6 +168,7 @@ function messageListener(request: Message, sender: unknown, sendResponse: (respo //send the sponsor times along with if it's found sendResponse({ found: sponsorDataFound, + status: lastResponseStatus, sponsorTimes: sponsorTimes, time: video.currentTime, onMobileYouTube @@ -206,8 +209,12 @@ function messageListener(request: Message, sender: unknown, sendResponse: (respo submitSponsorTimes(); break; case "refreshSegments": + // update video on refresh if videoID invalid + if (!sponsorVideoID) videoIDChange(getYouTubeVideoID(document)); + // fetch segments sponsorsLookup(false).then(() => sendResponse({ found: sponsorDataFound, + status: lastResponseStatus, sponsorTimes: sponsorTimes, time: video.currentTime, onMobileYouTube @@ -301,6 +308,7 @@ if (!Config.configSyncListeners.includes(contentConfigUpdateListener)) { function resetValues() { lastCheckTime = 0; lastCheckVideoTime = -1; + retryCount = 0; sponsorTimes = []; existingChaptersImported = false; @@ -330,8 +338,6 @@ function resetValues() { logDebug("Setting switching videos to true (reset data)"); } - firstEvent = true; - // Reset advert playing flag isAdPlaying = false; @@ -343,7 +349,7 @@ function resetValues() { categoryPill?.setVisibility(false); } -async function videoIDChange(id) { +async function videoIDChange(id): Promise<void> { //if the id has not changed return unless the video element has changed if (sponsorVideoID === id && (isVisible(video) || !video)) return; @@ -447,7 +453,7 @@ function createPreviewBar(): void { isVisibleCheck: true }, { // For new mobile YouTube (#1287) - selector: ".ytm-progress-bar", + selector: ".progress-bar-line", isVisibleCheck: true }, { // For Desktop YouTube @@ -527,6 +533,13 @@ function startSponsorSchedule(includeIntersectingSegments = false, currentTime?: return; } + // ensure we are on the correct video + const newVideoID = getYouTubeVideoID(document); + if (newVideoID !== sponsorVideoID) { + videoIDChange(newVideoID); + return; + } + logDebug(`Considering to start skipping: ${!video}, ${video?.paused}`); if (!video) return; if (currentTime === undefined || currentTime === null) { @@ -537,7 +550,16 @@ function startSponsorSchedule(includeIntersectingSegments = false, currentTime?: updateActiveSegment(currentTime); if (video.paused) return; - if (videoMuted && !inMuteSegment(currentTime)) { + 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 skipBuffer = 0.003; + + if (videoMuted && !inMuteSegment(currentTime, skipInfo.index !== -1 + && timeUntilSponsor < skipBuffer && shouldAutoSkip(currentSkip))) { video.muted = false; videoMuted = false; @@ -547,22 +569,15 @@ 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)){ return; } if (incorrectVideoCheck()) return; - const skipInfo = getNextSkipIndex(currentTime, includeIntersectingSegments, includeNonIntersectingSegments); - - logDebug(`Ready to start skipping: ${skipInfo.index} at ${currentTime}`); - if (skipInfo.index === -1) return; - - 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; - // Find all indexes in between the start and end let skippingSegments = [skipInfo.array[skipInfo.index]]; if (skipInfo.index !== skipInfo.endIndex) { @@ -576,7 +591,11 @@ function startSponsorSchedule(includeIntersectingSegments = false, currentTime?: } } - const skipBuffer = 0.003; + logDebug(`Next step in starting skipping: ${!shouldSkip(currentSkip)}, ${!sponsorTimesSubmitting?.some((segment) => segment.segment === currentSkip.segment)}`); + + // Don't skip if this category should not be skipped + if (!shouldSkip(currentSkip) && !sponsorTimesSubmitting?.some((segment) => segment.segment === currentSkip.segment)) return; + const skippingFunction = (forceVideoTime?: number) => { let forcedSkipTime: number = null; let forcedIncludeIntersectingSegments = false; @@ -593,6 +612,19 @@ function startSponsorSchedule(includeIntersectingSegments = false, currentTime?: skippingSegments, openNotice: skipInfo.openNotice }); + + // These are segments that start at the exact same time but need seperate notices + for (const extra of skipInfo.extraIndexes) { + const extraSkip = skipInfo.array[extra]; + if (shouldSkip(extraSkip)) { + skipToTime({ + v: video, + skipTime: [extraSkip.scheduledTime, extraSkip.segment[1]], + skippingSegments: [extraSkip], + openNotice: skipInfo.openNotice + }); + } + } if (utils.getCategorySelection(currentSkip.category)?.option === CategorySkipOption.ManualSkip || currentSkip.actionType === ActionType.Mute) { @@ -649,15 +681,17 @@ function getVirtualTime(): number { (performance.now() - lastKnownVideoTime.preciseTime) / 1000 + lastKnownVideoTime.videoTime : null); if ((lastTimeFromWaitingEvent || !utils.isFirefox()) - && !isSafari() && virtualTime && Math.abs(virtualTime - video.currentTime) < 0.6) { + && !isSafari() && virtualTime && Math.abs(virtualTime - video.currentTime) < 0.6 && video.currentTime !== 0) { return virtualTime; } else { return video.currentTime; } } -function inMuteSegment(currentTime: number): boolean { - const checkFunction = (segment) => segment.actionType === ActionType.Mute && segment.segment[0] <= currentTime && segment.segment[1] > currentTime; +function inMuteSegment(currentTime: number, includeOverlap: boolean): boolean { + const checkFunction = (segment) => segment.actionType === ActionType.Mute + && segment.segment[0] <= currentTime + && (segment.segment[1] > currentTime || (includeOverlap && segment.segment[1] + 0.02 > currentTime)); return sponsorTimes?.some(checkFunction) || sponsorTimesSubmitting.some(checkFunction); } @@ -695,27 +729,30 @@ function setupVideoMutationListener() { }); } -function refreshVideoAttachments() { - const newVideo = findValidElement(document.querySelectorAll('video')) as HTMLVideoElement; - if (newVideo && newVideo !== video) { - video = newVideo; +async function refreshVideoAttachments(): Promise<void> { + if (waitingForNewVideo) return; - if (!videosWithEventListeners.includes(video)) { - videosWithEventListeners.push(video); + waitingForNewVideo = true; + const newVideo = await utils.waitForElement("video", true) as HTMLVideoElement; + waitingForNewVideo = false; - setupVideoListeners(); - setupSkipButtonControlBar(); - setupCategoryPill(); - } + video = newVideo; + if (!videosWithEventListeners.includes(video)) { + videosWithEventListeners.push(video); - // Create a new bar in the new video element - if (previewBar && !utils.findReferenceNode()?.contains(previewBar.container)) { - previewBar.remove(); - previewBar = null; + setupVideoListeners(); + setupSkipButtonControlBar(); + setupCategoryPill(); + } - createPreviewBar(); - } + if (previewBar && !utils.findReferenceNode()?.contains(previewBar.container)) { + previewBar.remove(); + previewBar = null; + + createPreviewBar(); } + + videoIDChange(getYouTubeVideoID(document)); } function setupVideoListeners() { @@ -727,17 +764,18 @@ function setupVideoListeners() { switchingVideos = false; let startedWaiting = false; + let lastPausedAtZero = true; video.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 (!firstEvent && video.currentTime === 0) return; - firstEvent = false; + if (video.readyState <= HTMLMediaElement.HAVE_CURRENT_DATA + && video.currentTime === 0) return; updateVirtualTime(); - if (switchingVideos) { + if (switchingVideos || lastPausedAtZero) { switchingVideos = false; logDebug("Setting switching videos to false"); @@ -745,6 +783,8 @@ function setupVideoListeners() { if (sponsorTimes) startSkipScheduleCheckingForStartSponsors(); } + lastPausedAtZero = false; + // Check if an ad is playing updateAdFlag(); @@ -760,6 +800,7 @@ function setupVideoListeners() { }); video.addEventListener('playing', () => { updateVirtualTime(); + lastPausedAtZero = false; if (startedWaiting) { startedWaiting = false; @@ -767,6 +808,14 @@ function setupVideoListeners() { || (lastCheckVideoTime !== video.currentTime && Date.now() - lastCheckTime > 2000)}`); } + if (switchingVideos) { + switchingVideos = false; + logDebug("Setting switching videos to false"); + + // If already segments loaded before video, retry to skip starting segments + if (sponsorTimes) startSkipScheduleCheckingForStartSponsors(); + } + // 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)) { @@ -788,6 +837,10 @@ function setupVideoListeners() { startSponsorSchedule(); } else { updateActiveSegment(video.currentTime); + + if (video.currentTime === 0) { + lastPausedAtZero = true; + } } }); video.addEventListener('ratechange', () => startSponsorSchedule()); @@ -867,7 +920,6 @@ async function sponsorsLookup(keepOldSubmissions = true) { const hashParams = getHashParams(); if (hashParams.requiredSegment) extraRequestData.requiredSegment = hashParams.requiredSegment; - // Check for hashPrefix setting const hashPrefix = (await utils.getHash(sponsorVideoID, 1)).slice(0, 4) as VideoID & HashedValue; const response = await utils.asyncRequestToServer('GET', "/api/skipSegments/" + hashPrefix, { categories, @@ -876,6 +928,9 @@ async function sponsorsLookup(keepOldSubmissions = true) { ...extraRequestData }); + // store last response status + lastResponseStatus = response?.status; + if (response?.ok) { const recievedSegments: SponsorTime[] = JSON.parse(response.responseText) ?.filter((video) => video.videoID === sponsorVideoID) @@ -887,7 +942,7 @@ async function sponsorsLookup(keepOldSubmissions = true) { ?.sort((a, b) => a.segment[0] - b.segment[0]); if (!recievedSegments || !recievedSegments.length) { // return if no video found - retryFetch(); + retryFetch(404); return; } @@ -949,8 +1004,8 @@ async function sponsorsLookup(keepOldSubmissions = true) { //otherwise the listener can handle it updatePreviewBar(); } - } else if (response?.status === 404) { - retryFetch(); + } else { + retryFetch(lastResponseStatus); } importExistingChapters(true); @@ -999,17 +1054,24 @@ async function lockedCategoriesLookup(): Promise<void> { } } -function retryFetch(): void { +function retryFetch(errorCode: number): void { if (!Config.config.refetchWhenNotFound) return; - sponsorDataFound = false; + if (errorCode !== 404 && retryCount > 1) { + // Too many errors (50x), give up + return; + } + + retryCount++; + + const delay = errorCode === 404 ? (10000 + Math.random() * 30000) : (2000 + Math.random() * 10000); setTimeout(() => { if (sponsorVideoID && sponsorTimes?.length === 0 || sponsorTimes.every((segment) => segment.source !== SponsorSourceType.Server)) { sponsorsLookup(); } - }, 10000 + Math.random() * 30000); + }, delay); } /** @@ -1073,28 +1135,29 @@ function startSkipScheduleCheckingForStartSponsors() { } } -function getYouTubeVideoID(document: Document): string | boolean { - const url = document.URL; +function getYouTubeVideoID(document: Document, url?: string): string | boolean { + url ||= document.URL; // clips should never skip, going from clip to full video has no indications. if (url.includes("youtube.com/clip/")) return false; // skip to document and don't hide if on /embed/ - if (url.includes("/embed/") && url.includes("youtube.com")) return getYouTubeVideoIDFromDocument(document, false); + if (url.includes("/embed/") && url.includes("youtube.com")) return getYouTubeVideoIDFromDocument(false); // 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(document); + if (url.includes("/channel/") || url.includes("/user/") || url.includes("/c/")) return getYouTubeVideoIDFromDocument(); // not sure, try URL then document - return getYouTubeVideoIDFromURL(url) || getYouTubeVideoIDFromDocument(document, false); + return getYouTubeVideoIDFromURL(url) || getYouTubeVideoIDFromDocument(false); } -function getYouTubeVideoIDFromDocument(document: Document, hideIcon = true): string | boolean { +function getYouTubeVideoIDFromDocument(hideIcon = true): string | boolean { // get ID from document (channel trailer / embedded playlist) - const videoURL = document.querySelector("[data-sessionlink='feature=player-title']")?.getAttribute("href"); + const element = video?.parentElement?.parentElement?.querySelector("a.ytp-title-link[data-sessionlink='feature=player-title']"); + const videoURL = element?.getAttribute("href"); if (videoURL) { onInvidious = hideIcon; return getYouTubeVideoIDFromURL(videoURL); } else { - return false + return false; } } @@ -1241,13 +1304,33 @@ async function whitelistCheck() { * Returns info about the next upcoming sponsor skip */ function getNextSkipIndex(currentTime: number, includeIntersectingSegments: boolean, includeNonIntersectingSegments: boolean): - {array: ScheduledTime[], index: number, endIndex: number, openNotice: boolean} { + {array: ScheduledTime[], index: number, endIndex: number, extraIndexes: number[], openNotice: boolean} { + + const autoSkipSorter = (segment: ScheduledTime) => { + const skipOption = utils.getCategorySelection(segment.category)?.option; + if ((skipOption === CategorySkipOption.AutoSkip || shouldAutoSkip(segment)) + && segment.actionType === ActionType.Skip) { + return 0; + } else if (skipOption !== CategorySkipOption.ShowOverlay) { + return 1; + } else { + return 2; + } + } const { includedTimes: submittedArray, scheduledTimes: sponsorStartTimes } = getStartTimes(sponsorTimes, includeIntersectingSegments, includeNonIntersectingSegments); const { scheduledTimes: sponsorStartTimesAfterCurrentTime } = getStartTimes(sponsorTimes, includeIntersectingSegments, includeNonIntersectingSegments, currentTime, true); - const minSponsorTimeIndex = sponsorStartTimes.indexOf(Math.min(...sponsorStartTimesAfterCurrentTime)); + // This is an array in-case multiple segments have the exact same start time + const minSponsorTimeIndexes = GenericUtils.indexesOf(sponsorStartTimes, Math.min(...sponsorStartTimesAfterCurrentTime)); + // Find auto skipping segments if possible, sort by duration otherwise + const minSponsorTimeIndex = minSponsorTimeIndexes.sort( + (a, b) => ((autoSkipSorter(submittedArray[a]) - autoSkipSorter(submittedArray[b])) + || (submittedArray[a].segment[1] - submittedArray[a].segment[0]) - (submittedArray[b].segment[1] - submittedArray[b].segment[0])))[0] ?? -1; + // Store extra indexes for the non-auto skipping segments if others occur at the exact same start time + const extraIndexes = minSponsorTimeIndexes.filter((i) => i !== minSponsorTimeIndex && autoSkipSorter(submittedArray[i]) !== 0); + const endTimeIndex = getLatestEndTimeIndex(submittedArray, minSponsorTimeIndex); const { includedTimes: unsubmittedArray, scheduledTimes: unsubmittedSponsorStartTimes } = @@ -1263,6 +1346,7 @@ function getNextSkipIndex(currentTime: number, includeIntersectingSegments: bool array: submittedArray, index: minSponsorTimeIndex, endIndex: endTimeIndex, + extraIndexes, // Segments at same time that need seperate notices openNotice: true }; } else { @@ -1270,6 +1354,7 @@ function getNextSkipIndex(currentTime: number, includeIntersectingSegments: bool array: unsubmittedArray, index: minUnsubmittedSponsorTimeIndex, endIndex: previewEndTimeIndex, + extraIndexes: [], // No manual things for unsubmitted openNotice: false }; } @@ -1855,10 +1940,7 @@ async function vote(type: number, UUID: SegmentUUID, category?: Category, skipNo skipNotice.afterVote.bind(skipNotice)(utils.getSponsorTimeFromUUID(sponsorTimes, UUID), type, category); } else if (response.successType == -1) { if (response.statusCode === 403 && response.responseText.startsWith("Vote rejected due to a warning from a moderator.")) { - skipNotice.setNoticeInfoMessageWithOnClick.bind(skipNotice)(() => { - Chat.openWarningChat(response.responseText); - skipNotice.closeListener.call(skipNotice); - }, chrome.i18n.getMessage("voteRejectedWarning")); + openWarningDialog(skipNoticeContentContainer); } else { skipNotice.setNoticeInfoMessage.bind(skipNotice)(GenericUtils.getErrorMessage(response.statusCode, response.responseText)) } @@ -2046,7 +2128,7 @@ async function sendSubmitMessage() { playerButtons.submit.image.src = chrome.extension.getURL("icons/PlayerUploadFailedIconSponsorBlocker.svg"); if (response.status === 403 && response.responseText.startsWith("Submission rejected due to a warning from a moderator.")) { - Chat.openWarningChat(response.responseText); + openWarningDialog(skipNoticeContentContainer); } else { alert(GenericUtils.getErrorMessage(response.status, response.responseText)); } @@ -2272,7 +2354,8 @@ function checkForPreloadedSegment() { 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", () => videoIDChange(getYouTubeVideoID(document))); + (window as unknown as { navigation: EventTarget }).navigation.addEventListener("navigate", (e) => + videoIDChange(getYouTubeVideoID(document, (e as unknown as Record<string, Record<string, string>>).destination.url))); } // Record availability of Navigation API diff --git a/src/js-components/chat.ts b/src/js-components/chat.ts deleted file mode 100644 index 9ff30af7..00000000 --- a/src/js-components/chat.ts +++ /dev/null @@ -1,47 +0,0 @@ -import Config from "../config"; -import Utils from "../utils"; -const utils = new Utils(); - -export interface ChatConfig { - displayName: string, - composerInitialValue?: string, - customDescription?: string -} - -export function openChat(config: ChatConfig): void { - const chat = document.createElement("div"); - chat.classList.add("sbChatNotice"); - chat.style.zIndex = "2000"; - - const iframe= document.createElement("iframe"); - iframe.src = "https://chat.sponsor.ajay.app/#" + utils.objectToURI("", config, false); - chat.appendChild(iframe); - - const closeButton = document.createElement("img"); - closeButton.classList.add("sbChatClose"); - closeButton.src = chrome.extension.getURL("icons/close.png"); - closeButton.addEventListener("click", () => { - chat.remove(); - closeButton.remove(); - }); - chat.appendChild(closeButton); - - const referenceNode = utils.findReferenceNode(); - referenceNode.prepend(chat); -} - -export async function openWarningChat(warningMessage: string): Promise<void> { - const warningReasonMatch = warningMessage.match(/Warning reason: '(.+)'/); - alert(chrome.i18n.getMessage("warningChatInfo") + `\n\n${warningReasonMatch ? ` Warning reason: ${warningReasonMatch[1]}` : ``}`); - - const userNameData = await utils.asyncRequestToServer("GET", "/api/getUsername?userID=" + Config.config.userID); - const userName = userNameData.ok ? JSON.parse(userNameData.responseText).userName : ""; - const publicUserID = await utils.getHash(Config.config.userID); - - openChat({ - displayName: `${userName ? userName : ``}${userName !== publicUserID ? ` | ${publicUserID}` : ``}`, - composerInitialValue: `I got a warning and confirm I [REMOVE THIS CAPITAL TEXT TO CONFIRM] reread the guidelines.` + - warningReasonMatch ? ` Warning reason: ${warningReasonMatch[1]}` : ``, - customDescription: chrome.i18n.getMessage("warningChatInfo") - }); -}
\ No newline at end of file diff --git a/src/messageTypes.ts b/src/messageTypes.ts index cdc49011..c48aba34 100644 --- a/src/messageTypes.ts +++ b/src/messageTypes.ts @@ -73,6 +73,7 @@ export type Message = BaseMessage & (DefaultMessage | BoolValueMessage | IsInfoF export interface IsInfoFoundMessageResponse { found: boolean; + status: number; sponsorTimes: SponsorTime[]; time: number; onMobileYouTube: boolean; @@ -100,7 +101,7 @@ export type MessageResponse = | GetChannelIDResponse | SponsorStartResponse | IsChannelWhitelistedResponse - | Record<string, never> + | Record<string, never> // empty object response {} | VoteResponse | ImportSegmentsResponse; diff --git a/src/options.ts b/src/options.ts index dc3314cf..1a522c9b 100644 --- a/src/options.ts +++ b/src/options.ts @@ -452,13 +452,7 @@ function invidiousInstanceAddInit(element: HTMLElement, option: string) { * @param option */ function invidiousInit(checkbox: HTMLInputElement, option: string) { - let permissions = ["declarativeContent"]; - if (utils.isFirefox()) permissions = []; - - chrome.permissions.contains({ - origins: utils.getPermissionRegex(), - permissions: permissions - }, function (result) { + utils.containsInvidiousPermission().then((result) => { if (result != checkbox.checked) { Config.config[option] = result; @@ -474,22 +468,8 @@ function invidiousInit(checkbox: HTMLInputElement, option: string) { * @param option */ async function invidiousOnClick(checkbox: HTMLInputElement, option: string): Promise<void> { - return new Promise((resolve) => { - if (checkbox.checked) { - utils.setupExtraSitePermissions(function (granted) { - if (!granted) { - Config.config[option] = false; - checkbox.checked = false; - } else { - checkbox.checked = true; - } - - resolve(); - }); - } else { - utils.removeExtraSiteRegistration(); - } - }); + const enabled = await utils.applyInvidiousPermissions(checkbox.checked, option); + checkbox.checked = enabled; } /** @@ -598,8 +578,9 @@ async function setTextOption(option: string, element: HTMLElement, value: string function downloadConfig() { const file = document.createElement("a"); const jsonData = JSON.parse(JSON.stringify(Config.cachedSyncConfig)); - file.setAttribute("href", "data:text/json;charset=utf-8," + encodeURIComponent(JSON.stringify(jsonData))); - file.setAttribute("download", "SponsorBlockConfig.json"); + const dateTimeString = new Date().toJSON().replace("T", "_").replace(/:/g, ".").replace(/.\d+Z/g, "") + file.setAttribute("href", `data:text/json;charset=utf-8,${encodeURIComponent(JSON.stringify(jsonData))}`); + file.setAttribute("download", `SponsorBlockConfig_${dateTimeString}.json`); document.body.append(file); file.click(); file.remove(); @@ -673,4 +654,4 @@ function copyDebugOutputToClipboard() { function isIncognitoAllowed(): Promise<boolean> { return new Promise((resolve) => chrome.extension.isAllowedIncognitoAccess(resolve)); -}
\ No newline at end of file +} diff --git a/src/permissions.ts b/src/permissions.ts index 05da496e..1e2c1119 100644 --- a/src/permissions.ts +++ b/src/permissions.ts @@ -12,25 +12,17 @@ window.addEventListener('DOMContentLoaded', init); async function init() { localizeHtmlPage(); - const domains = document.location.hash.replace("#", "").split(","); - const acceptButton = document.getElementById("acceptPermissionButton"); acceptButton.addEventListener("click", () => { - chrome.permissions.request({ - origins: utils.getPermissionRegex(domains), - permissions: [] - }, (granted) => { - if (granted) { - alert(chrome.i18n.getMessage("permissionRequestSuccess")); + utils.applyInvidiousPermissions(Config.config.supportInvidious).then((enabled) => { + Config.config.supportInvidious = enabled; - Config.config.ytInfoPermissionGranted = true; - - chrome.tabs.getCurrent((tab) => { - chrome.tabs.remove(tab.id); - }); + if (enabled) { + alert(chrome.i18n.getMessage("permissionRequestSuccess")); + window.close(); } else { alert(chrome.i18n.getMessage("permissionRequestFailed")); } - }); + }) }); }
\ No newline at end of file diff --git a/src/popup.ts b/src/popup.ts index 061634c2..9d60b058 100644 --- a/src/popup.ts +++ b/src/popup.ts @@ -415,8 +415,10 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> { if (request.sponsorTimes) { displayDownloadedSponsorTimes(request.sponsorTimes, request.time); } - } else { + } else if (request.status == 404 || request.status == 200) { PageElements.videoFound.innerHTML = chrome.i18n.getMessage("sponsor404"); + } else { + PageElements.videoFound.innerHTML = chrome.i18n.getMessage("connectionError") + request.status; } } @@ -664,7 +666,7 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> { voteButtonsContainer.appendChild(upvoteButton); voteButtonsContainer.appendChild(downvoteButton); voteButtonsContainer.appendChild(uuidButton); - if (downloadedTimes[i].actionType === ActionType.Skip + if (downloadedTimes[i].actionType === ActionType.Skip || downloadedTimes[i].actionType === ActionType.Mute && [SponsorHideType.Visible, SponsorHideType.Hidden].includes(downloadedTimes[i].hidden)) { voteButtonsContainer.appendChild(hideButton); } diff --git a/src/render/GenericNotice.tsx b/src/render/GenericNotice.tsx index 32f0ad50..639edb86 100644 --- a/src/render/GenericNotice.tsx +++ b/src/render/GenericNotice.tsx @@ -73,7 +73,13 @@ export default class GenericNotice { hideRightInfo={options.hideRightInfo} closeListener={() => this.close()} > - {this.getMessageBox(this.idSuffix, options.textBoxes)} + <tr id={"sponsorSkipNoticeMiddleRow" + this.idSuffix} + className="sponsorTimeMessagesRow" + style={{maxHeight: (this.contentContainer().v.offsetHeight - 200) + "px"}}> + <td style={{width: "100%"}}> + {this.getMessageBoxes(this.idSuffix, options.textBoxes)} + </td> + </tr> <tr id={"sponsorSkipNoticeSpacer" + this.idSuffix} className="sponsorBlockSpacer"> @@ -90,7 +96,7 @@ export default class GenericNotice { ); } - getMessageBox(idSuffix: string, textBoxes: TextBox[]): JSX.Element[] { + getMessageBoxes(idSuffix: string, textBoxes: TextBox[]): JSX.Element[] { if (textBoxes) { const result = []; for (let i = 0; i < textBoxes.length; i++) { diff --git a/src/utils.ts b/src/utils.ts index d7e6fcf8..081e014e 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -2,7 +2,7 @@ import Config, { VideoDownvotes } from "./config"; import { CategorySelection, SponsorTime, FetchResponse, BackgroundScriptContainer, Registration, HashedValue, VideoID, SponsorHideType } from "./types"; import * as CompileConfig from "../config.json"; -import { findValidElementFromSelector } from "./utils/pageUtils"; +import { findValidElement, findValidElementFromSelector } from "./utils/pageUtils"; import { GenericUtils } from "./utils/genericUtils"; export default class Utils { @@ -22,8 +22,9 @@ export default class Utils { ]; /* Used for waitForElement */ - waitingMutationObserver:MutationObserver = null; - waitingElements: { selector: string, callback: (element: Element) => void }[] = []; + creatingWaitingMutationObserver = false; + waitingMutationObserver: MutationObserver = null; + waitingElements: { selector: string, visibleCheck: boolean, callback: (element: Element) => void }[] = []; constructor(backgroundScriptContainer: BackgroundScriptContainer = null) { this.backgroundScriptContainer = backgroundScriptContainer; @@ -34,40 +35,66 @@ export default class Utils { } /* Uses a mutation observer to wait asynchronously */ - async waitForElement(selector: string): Promise<Element> { + async waitForElement(selector: string, visibleCheck = false): Promise<Element> { 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.waitingMutationObserver) { - this.waitingMutationObserver = new MutationObserver(() => { - const foundSelectors = []; - for (const { selector, callback } of this.waitingElements) { - const element = document.querySelector(selector); - 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; - } - }); + if (!this.creatingWaitingMutationObserver) { + this.creatingWaitingMutationObserver = true; - this.waitingMutationObserver.observe(document.body, { - childList: true, - subtree: true - }); + if (document.body) { + this.setupWaitingMutationListener(); + } else { + window.addEventListener("DOMContentLoaded", () => { + this.setupWaitingMutationListener(); + }); + } } }); } + private setupWaitingMutationListener(): void { + if (!this.waitingMutationObserver) { + this.waitingMutationObserver = new MutationObserver(() => { + 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; + } + }); + + 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<boolean> { return new Promise((resolve) => { chrome.permissions.contains(permissions, resolve) @@ -183,6 +210,37 @@ export default class Utils { }); } + applyInvidiousPermissions(enable: boolean, option = "supportInvidious"): Promise<boolean> { + return new Promise((resolve) => { + if (enable) { + this.setupExtraSitePermissions((granted) => { + if (!granted) { + Config.config[option] = false; + } + + resolve(granted); + }); + } else { + this.removeExtraSiteRegistration(); + resolve(false); + } + }); + } + + containsInvidiousPermission(): Promise<boolean> { + return new Promise((resolve) => { + let permissions = ["declarativeContent"]; + if (this.isFirefox()) permissions = []; + + chrome.permissions.contains({ + origins: this.getPermissionRegex(), + permissions: permissions + }, function (result) { + resolve(result); + }); + }) + } + /** * Merges any overlapping timestamp ranges into single segments and returns them as a new array. */ @@ -358,19 +416,6 @@ export default class Utils { return referenceNode; } - objectToURI<T>(url: string, data: T, includeQuestionMark: boolean): string { - let counter = 0; - for (const key in data) { - const seperator = (url.includes("?") || counter > 0) ? "&" : (includeQuestionMark ? "?" : ""); - const value = (typeof(data[key]) === "string") ? data[key] as unknown as string : JSON.stringify(data[key]); - url += seperator + encodeURIComponent(key) + "=" + encodeURIComponent(value); - - counter++; - } - - return url; - } - isContentScript(): boolean { return window.location.protocol === "http:" || window.location.protocol === "https:"; } diff --git a/src/utils/genericUtils.ts b/src/utils/genericUtils.ts index 90a6b5e2..0f8555d0 100644 --- a/src/utils/genericUtils.ts +++ b/src/utils/genericUtils.ts @@ -108,6 +108,27 @@ function hexToRgb(hex: string): {r: number, g: number, b: number} { } : null; } +/** + * List of all indexes that have the specified value + * https://stackoverflow.com/a/54954694/1985387 + */ +function indexesOf<T>(array: T[], value: T): number[] { + return array.map((v, i) => v === value ? i : -1).filter(i => i !== -1); +} + +function objectToURI<T>(url: string, data: T, includeQuestionMark: boolean): string { + let counter = 0; + for (const key in data) { + const seperator = (url.includes("?") || counter > 0) ? "&" : (includeQuestionMark ? "?" : ""); + const value = (typeof(data[key]) === "string") ? data[key] as unknown as string : JSON.stringify(data[key]); + url += seperator + encodeURIComponent(key) + "=" + encodeURIComponent(value); + + counter++; + } + + return url; +} + function generateUserID(length = 36): string { const charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"; let result = ""; @@ -132,5 +153,7 @@ export const GenericUtils = { getFormattedTimeToSeconds, getErrorMessage, getLuminance, - generateUserID + generateUserID, + indexesOf, + objectToURI }
\ No newline at end of file diff --git a/src/utils/warnings.ts b/src/utils/warnings.ts new file mode 100644 index 00000000..09423e03 --- /dev/null +++ b/src/utils/warnings.ts @@ -0,0 +1,66 @@ +import Config from "../config"; +import GenericNotice, { NoticeOptions } from "../render/GenericNotice"; +import { ContentContainer } from "../types"; +import Utils from "../utils"; +import { GenericUtils } from "./genericUtils"; +const utils = new Utils(); + +export interface ChatConfig { + displayName: string, + composerInitialValue?: string, + customDescription?: string +} + +export async function openWarningDialog(contentContainer: ContentContainer): Promise<void> { + const userInfo = await utils.asyncRequestToServer("GET", "/api/userInfo", { + userID: Config.config.userID, + values: ["warningReason"] + }); + + if (userInfo.ok) { + const warningReason = JSON.parse(userInfo.responseText)?.warningReason; + const userNameData = await utils.asyncRequestToServer("GET", "/api/getUsername?userID=" + Config.config.userID); + const userName = userNameData.ok ? JSON.parse(userNameData.responseText).userName : ""; + const publicUserID = await utils.getHash(Config.config.userID); + + let notice: GenericNotice = null; + const options: NoticeOptions = { + title: chrome.i18n.getMessage("warningTitle"), + textBoxes: [{ + text: chrome.i18n.getMessage("warningChatInfo"), + icon: null + }, ...warningReason.split("\n").map((reason) => ({ + text: reason, + icon: null + }))], + buttons: [{ + name: chrome.i18n.getMessage("questionButton"), + listener: () => openChat({ + displayName: `${userName ? userName : ``}${userName !== publicUserID ? ` | ${publicUserID}` : ``}` + }) + }, + { + name: chrome.i18n.getMessage("warningConfirmButton"), + listener: async () => { + const result = await utils.asyncRequestToServer("POST", "/api/warnUser", { + userID: Config.config.userID, + enabled: false + }); + + if (result.ok) { + notice?.close(); + } else { + alert(`${chrome.i18n.getMessage("warningError")} ${result.status}`); + } + } + }], + timed: false + }; + + notice = new GenericNotice(contentContainer, "warningNotice", options); + } +} + +export function openChat(config: ChatConfig): void { + window.open("https://chat.sponsor.ajay.app/#" + GenericUtils.objectToURI("", config, false)); +}
\ No newline at end of file |