From 34cfd14e74f10e62c4a42f3ef9bea61bb6fc111b Mon Sep 17 00:00:00 2001 From: Ajay Ramachandran Date: Wed, 1 Sep 2021 16:30:17 -0400 Subject: [PATCH 1/9] Add UI for submitting mute segments and request mute segments --- public/_locales/en/messages.json | 3 ++ public/content.css | 2 +- src/components/SponsorTimeEditComponent.tsx | 41 ++++++++++++++++++--- src/config.ts | 2 + src/content.ts | 5 ++- src/types.ts | 8 ++++ 6 files changed, 53 insertions(+), 8 deletions(-) diff --git a/public/_locales/en/messages.json b/public/_locales/en/messages.json index 2b85bb14..d579ac45 100644 --- a/public/_locales/en/messages.json +++ b/public/_locales/en/messages.json @@ -302,6 +302,9 @@ "skip": { "message": "Skip" }, + "mute": { + "message": "Mute" + }, "skip_category": { "message": "Skip {0}?" }, diff --git a/public/content.css b/public/content.css index b2e4b1e5..d9fc7045 100644 --- a/public/content.css +++ b/public/content.css @@ -467,7 +467,7 @@ input::-webkit-inner-spin-button { text-decoration: underline; } -.sponsorTimeCategories { +.sponsorTimeEditSelector { margin-top: 5px; margin-bottom: 5px; diff --git a/src/components/SponsorTimeEditComponent.tsx b/src/components/SponsorTimeEditComponent.tsx index 8d9f516e..5f7de240 100644 --- a/src/components/SponsorTimeEditComponent.tsx +++ b/src/components/SponsorTimeEditComponent.tsx @@ -1,12 +1,12 @@ import * as React from "react"; - -import Config from "../config"; import * as CompileConfig from "../../config.json"; - +import Config from "../config"; +import { ActionType, ActionTypes, Category, CategoryActionType, ContentContainer, SponsorTime } from "../types"; import Utils from "../utils"; -import { Category, CategoryActionType, ContentContainer, SponsorTime } from "../types"; -import SubmissionNoticeComponent from "./SubmissionNoticeComponent"; import { getCategoryActionType } from "../utils/categoryUtils"; +import SubmissionNoticeComponent from "./SubmissionNoticeComponent"; + + const utils = new Utils(); export interface SponsorTimeEditProps { @@ -32,6 +32,7 @@ class SponsorTimeEditComponent extends React.Component; + actionTypeOptionRef: React.RefObject; configUpdateListener: () => void; @@ -39,6 +40,7 @@ class SponsorTimeEditComponent extends React.Component this.saveEditTimes()}> + {this.getActionTypeOptions()} + + +
{/* Editing Tools */} @@ -269,6 +282,21 @@ class SponsorTimeEditComponent extends React.Component + {chrome.i18n.getMessage(actionType)} + + ); + } + + return elements; + } + setTimeToNow(index: number): void { this.setTimeTo(index, this.props.contentContainer().getRealCurrentTime()); } @@ -331,6 +359,7 @@ class SponsorTimeEditComponent extends React.Component Date: Wed, 1 Sep 2021 19:51:42 -0400 Subject: [PATCH 2/9] Add silent skipping to scheduler --- src/content.ts | 121 +++++++++++++++++++++++++++++++++++-------------- src/types.ts | 4 ++ tsconfig.json | 7 ++- 3 files changed, 96 insertions(+), 36 deletions(-) diff --git a/src/content.ts b/src/content.ts index a2405677..0cb95b37 100644 --- a/src/content.ts +++ b/src/content.ts @@ -1,5 +1,5 @@ import Config from "./config"; -import { SponsorTime, CategorySkipOption, VideoID, SponsorHideType, VideoInfo, StorageChangesObject, CategoryActionType, ChannelIDInfo, ChannelIDStatus, SponsorSourceType, SegmentUUID, Category, SkipToTimeParams, ToggleSkippable, ActionType } from "./types"; +import { SponsorTime, CategorySkipOption, VideoID, SponsorHideType, VideoInfo, StorageChangesObject, CategoryActionType, ChannelIDInfo, ChannelIDStatus, SponsorSourceType, SegmentUUID, Category, SkipToTimeParams, ToggleSkippable, ActionType, ScheduledTime } from "./types"; import { ContentContainer } from "./types"; import Utils from "./utils"; @@ -45,6 +45,7 @@ let sponsorSkipped: boolean[] = []; //the video let video: HTMLVideoElement; +let videoMuted = false; // Has it been attempted to be muted let videoMutationObserver: MutationObserver = null; // List of videos that have had event listeners added to them const videosWithEventListeners: HTMLVideoElement[] = []; @@ -396,7 +397,6 @@ function cancelSponsorSchedule(): void { } /** - * * @param currentTime Optional if you don't want to use the actual current time */ function startSponsorSchedule(includeIntersectingSegments = false, currentTime?: number, includeNonIntersectingSegments = true): void { @@ -412,6 +412,12 @@ function startSponsorSchedule(includeIntersectingSegments = false, currentTime?: } if (!video || video.paused) return; + if (currentTime === undefined || currentTime === null) currentTime = video.currentTime; + + if (videoMuted && !inMuteSegment(currentTime)) { + video.muted = false; + videoMuted = false; + } if (Config.config.disableSkipping || channelWhitelisted || (channelIDInfo.status === ChannelIDStatus.Fetching && Config.config.forceChannelCheck)){ return; @@ -419,14 +425,12 @@ function startSponsorSchedule(includeIntersectingSegments = false, currentTime?: if (incorrectVideoCheck()) return; - if (currentTime === undefined || currentTime === null) currentTime = video.currentTime; - const skipInfo = getNextSkipIndex(currentTime, includeIntersectingSegments, includeNonIntersectingSegments); if (skipInfo.index === -1) return; const currentSkip = skipInfo.array[skipInfo.index]; - const skipTime: number[] = [currentSkip.segment[0], skipInfo.array[skipInfo.endIndex].segment[1]]; + const skipTime: number[] = [currentSkip.scheduledTime, skipInfo.array[skipInfo.endIndex].segment[1]]; const timeUntilSponsor = skipTime[0] - currentTime; const videoID = sponsorVideoID; @@ -461,7 +465,8 @@ function startSponsorSchedule(includeIntersectingSegments = false, currentTime?: openNotice: skipInfo.openNotice }); - if (utils.getCategorySelection(currentSkip.category)?.option === CategorySkipOption.ManualSkip) { + if (utils.getCategorySelection(currentSkip.category)?.option === CategorySkipOption.ManualSkip + || currentSkip.actionType === ActionType.Mute) { forcedSkipTime = skipTime[0] + 0.001; } else { forcedSkipTime = skipTime[1]; @@ -480,12 +485,19 @@ function startSponsorSchedule(includeIntersectingSegments = false, currentTime?: } } +function inMuteSegment(currentTime: number): boolean { + const checkFunction = (segment) => segment.actionType === ActionType.Mute && segment.segment[0] <= currentTime && segment.segment[1] > currentTime; + return sponsorTimes.some(checkFunction) || sponsorTimesSubmitting.some(checkFunction); +} + /** * 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.URL); - if (currentVideoID !== (videoID || sponsorVideoID) || (sponsorTime && (!sponsorTimes || !sponsorTimes.includes(sponsorTime)) && !sponsorTimesSubmitting.includes(sponsorTime))) { + if (currentVideoID !== (videoID || sponsorVideoID) || (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); @@ -946,31 +958,33 @@ async function whitelistCheck() { * Returns info about the next upcoming sponsor skip */ function getNextSkipIndex(currentTime: number, includeIntersectingSegments: boolean, includeNonIntersectingSegments: boolean): - {array: SponsorTime[], index: number, endIndex: number, openNotice: boolean} { + {array: ScheduledTime[], index: number, endIndex: number, openNotice: boolean} { - const sponsorStartTimes = getStartTimes(sponsorTimes, includeIntersectingSegments, includeNonIntersectingSegments); - const sponsorStartTimesAfterCurrentTime = getStartTimes(sponsorTimes, includeIntersectingSegments, includeNonIntersectingSegments, currentTime, true, true); + const { includedTimes: submittedArray, startTimeIndexes: sponsorStartTimes } = + getStartTimes(sponsorTimes, includeIntersectingSegments, includeNonIntersectingSegments); + const { startTimeIndexes: sponsorStartTimesAfterCurrentTime } = getStartTimes(sponsorTimes, includeIntersectingSegments, includeNonIntersectingSegments, currentTime, true, true); const minSponsorTimeIndex = sponsorStartTimes.indexOf(Math.min(...sponsorStartTimesAfterCurrentTime)); - const endTimeIndex = getLatestEndTimeIndex(sponsorTimes, minSponsorTimeIndex); + const endTimeIndex = getLatestEndTimeIndex(submittedArray, minSponsorTimeIndex); - const unsubmittedSponsorStartTimes = getStartTimes(sponsorTimesSubmitting, includeIntersectingSegments, includeNonIntersectingSegments); - const unsubmittedSponsorStartTimesAfterCurrentTime = getStartTimes(sponsorTimesSubmitting, includeIntersectingSegments, includeNonIntersectingSegments, currentTime, false, false); + const { includedTimes: unsubmittedArray, startTimeIndexes: unsubmittedSponsorStartTimes } = + getStartTimes(sponsorTimesSubmitting, includeIntersectingSegments, includeNonIntersectingSegments); + const { startTimeIndexes: unsubmittedSponsorStartTimesAfterCurrentTime } = getStartTimes(sponsorTimesSubmitting, includeIntersectingSegments, includeNonIntersectingSegments, currentTime, false, false); const minUnsubmittedSponsorTimeIndex = unsubmittedSponsorStartTimes.indexOf(Math.min(...unsubmittedSponsorStartTimesAfterCurrentTime)); - const previewEndTimeIndex = getLatestEndTimeIndex(sponsorTimesSubmitting, minUnsubmittedSponsorTimeIndex); + const previewEndTimeIndex = getLatestEndTimeIndex(unsubmittedArray, minUnsubmittedSponsorTimeIndex); if ((minUnsubmittedSponsorTimeIndex === -1 && minSponsorTimeIndex !== -1) || sponsorStartTimes[minSponsorTimeIndex] < unsubmittedSponsorStartTimes[minUnsubmittedSponsorTimeIndex]) { return { - array: sponsorTimes.filter((segment) => getCategoryActionType(segment.category) === CategoryActionType.Skippable), + array: submittedArray, index: minSponsorTimeIndex, endIndex: endTimeIndex, openNotice: true }; } else { return { - array: sponsorTimesSubmitting.filter((segment) => getCategoryActionType(segment.category) === CategoryActionType.Skippable), + array: unsubmittedArray, index: minUnsubmittedSponsorTimeIndex, endIndex: previewEndTimeIndex, openNotice: false @@ -994,7 +1008,10 @@ function getNextSkipIndex(currentTime: number, includeIntersectingSegments: bool function getLatestEndTimeIndex(sponsorTimes: SponsorTime[], index: number, hideHiddenSponsors = true): number { // Only combine segments for AutoSkip if (index == -1 || - !shouldAutoSkip(sponsorTimes[index])) return index; + !shouldAutoSkip(sponsorTimes[index]) + || sponsorTimes[index].actionType !== ActionType.Skip) { + return index; + } // Default to the normal endTime let latestEndTimeIndex = index; @@ -1005,7 +1022,8 @@ function getLatestEndTimeIndex(sponsorTimes: SponsorTime[], index: number, hideH if (currentSegment[0] <= latestEndTime && currentSegment[1] > latestEndTime && (!hideHiddenSponsors || sponsorTimes[i].hidden === SponsorHideType.Visible) - && shouldAutoSkip(sponsorTimes[i])) { + && shouldAutoSkip(sponsorTimes[i]) + && sponsorTimes[i].actionType === ActionType.Skip) { // Overlapping segment latestEndTimeIndex = i; } @@ -1030,24 +1048,43 @@ function getLatestEndTimeIndex(sponsorTimes: SponsorTime[], index: number, hideH * the current time, but end after */ function getStartTimes(sponsorTimes: SponsorTime[], includeIntersectingSegments: boolean, includeNonIntersectingSegments: boolean, - minimum?: number, onlySkippableSponsors = false, hideHiddenSponsors = false): number[] { - if (sponsorTimes === null) return []; + minimum?: number, onlySkippableSponsors = false, hideHiddenSponsors = false): {includedTimes: ScheduledTime[], startTimeIndexes: number[]} { + if (!sponsorTimes) return {includedTimes: [], startTimeIndexes: []}; - const startTimes: number[] = []; + const includedTimes: ScheduledTime[] = []; + const startTimeIndexes: number[] = []; - for (let i = 0; i < sponsorTimes?.length; i++) { + const possibleTimes = sponsorTimes.flatMap((sponsorTime) => { + const results = [{ + ...sponsorTime, + scheduledTime: sponsorTime.segment[0] + }] + + if (sponsorTime.actionType === ActionType.Mute) { + // Schedule at the end time to know when to unmute + results.push({ + ...sponsorTime, + scheduledTime: sponsorTime.segment[1] + }) + } + + return results; + }) + + for (let i = 0; i < possibleTimes.length; i++) { if ((minimum === undefined - || ((includeNonIntersectingSegments && sponsorTimes[i].segment[0] >= minimum) - || (includeIntersectingSegments && sponsorTimes[i].segment[0] < minimum && sponsorTimes[i].segment[1] > minimum))) - && (!onlySkippableSponsors || shouldSkip(sponsorTimes[i])) - && (!hideHiddenSponsors || sponsorTimes[i].hidden === SponsorHideType.Visible) - && getCategoryActionType(sponsorTimes[i].category) === CategoryActionType.Skippable) { + || ((includeNonIntersectingSegments && possibleTimes[i].scheduledTime >= minimum) + || (includeIntersectingSegments && possibleTimes[i].scheduledTime < minimum && possibleTimes[i].segment[1] > minimum))) + && (!onlySkippableSponsors || shouldSkip(possibleTimes[i])) + && (!hideHiddenSponsors || possibleTimes[i].hidden === SponsorHideType.Visible) + && getCategoryActionType(possibleTimes[i].category) === CategoryActionType.Skippable) { - startTimes.push(sponsorTimes[i].segment[0]); + startTimeIndexes.push(possibleTimes[i].scheduledTime); + includedTimes.push(possibleTimes[i]); } } - return startTimes; + return { includedTimes, startTimeIndexes }; } /** @@ -1093,13 +1130,27 @@ function skipToTime({v, skipTime, skippingSegments, openNotice, forceAutoSkip, u if ((autoSkip || sponsorTimesSubmitting.some((time) => time.segment === skippingSegments[0].segment)) && v.currentTime !== skipTime[1]) { - // Fix for looped videos not working when skipping to the end #426 - // for some reason you also can't skip to 1 second before the end - if (v.loop && v.duration > 1 && skipTime[1] >= v.duration - 1) { - v.currentTime = 0; - } else { - v.currentTime = skipTime[1]; + switch(skippingSegments[0].actionType) { + case ActionType.Skip: { + // Fix for looped videos not working when skipping to the end #426 + // for some reason you also can't skip to 1 second before the end + if (v.loop && v.duration > 1 && skipTime[1] >= v.duration - 1) { + v.currentTime = 0; + } else { + v.currentTime = skipTime[1]; + } + + break; + } + case ActionType.Mute: { + if (!v.muted) { + v.muted = true; + videoMuted = true; + } + break; + } } + } if (!autoSkip diff --git a/src/types.ts b/src/types.ts index 5635039e..cb5cc0e2 100644 --- a/src/types.ts +++ b/src/types.ts @@ -82,6 +82,10 @@ export interface SponsorTime { source?: SponsorSourceType; } +export interface ScheduledTime extends SponsorTime { + scheduledTime: number; +} + export interface PreviewBarOption { color: string, opacity: string diff --git a/tsconfig.json b/tsconfig.json index 8fa7472c..55769505 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -8,6 +8,11 @@ "noEmitOnError": false, "typeRoots": [ "node_modules/@types" ], "resolveJsonModule": true, - "jsx": "react" + "jsx": "react", + "lib": [ + "es2019", + "dom", + "dom.iterable" + ] } } \ No newline at end of file From 4092bf9b0546f13be383937fbc052d3b9c1abdca Mon Sep 17 00:00:00 2001 From: Ajay Ramachandran Date: Wed, 1 Sep 2021 20:52:40 -0400 Subject: [PATCH 3/9] Make skip notice buttons work with mute segments --- public/_locales/en/messages.json | 3 + src/components/SkipNoticeComponent.tsx | 102 +++++++++++++++++-------- src/content.ts | 30 +++++--- 3 files changed, 92 insertions(+), 43 deletions(-) diff --git a/public/_locales/en/messages.json b/public/_locales/en/messages.json index d579ac45..043d64e0 100644 --- a/public/_locales/en/messages.json +++ b/public/_locales/en/messages.json @@ -52,6 +52,9 @@ "reskip": { "message": "Reskip" }, + "unmute": { + "message": "Unmute" + }, "paused": { "message": "Paused" }, diff --git a/src/components/SkipNoticeComponent.tsx b/src/components/SkipNoticeComponent.tsx index 0af1efad..b3ca619b 100644 --- a/src/components/SkipNoticeComponent.tsx +++ b/src/components/SkipNoticeComponent.tsx @@ -1,7 +1,7 @@ import * as React from "react"; import * as CompileConfig from "../../config.json"; import Config from "../config" -import { Category, ContentContainer, CategoryActionType, SponsorHideType, SponsorTime, NoticeVisbilityMode } from "../types"; +import { Category, ContentContainer, CategoryActionType, SponsorHideType, SponsorTime, NoticeVisbilityMode, ActionType } from "../types"; import NoticeComponent from "./NoticeComponent"; import NoticeTextSelectionComponent from "./NoticeTextSectionComponent"; @@ -39,8 +39,8 @@ export interface SkipNoticeState { maxCountdownTime?: () => number; countdownText?: string; - unskipText?: string; - unskipCallback?: (index: number) => void; + skipButtonText?: string; + skipButtonCallback?: (index: number) => void; downvoting?: boolean; choosingCategory?: boolean; @@ -110,8 +110,8 @@ class SkipNoticeComponent extends React.Component this.unskip(index), + skipButtonText: this.getUnskipText(), + skipButtonCallback: (index) => this.unskip(index), downvoting: false, choosingCategory: false, @@ -126,7 +126,7 @@ class SkipNoticeComponent extends React.Component this.prepAction(SkipNoticeAction.Unskip)}> - {this.state.unskipText + (this.state.showKeybindHint ? " (" + Config.config.skipKeybind + ")" : "")} + {this.state.skipButtonText + (this.state.showKeybindHint ? " (" + Config.config.skipKeybind + ")" : "")} ); @@ -396,7 +396,7 @@ class SkipNoticeComponent extends React.Component Config.config.skipNoticeDuration, + countdownTime: Config.config.skipNoticeDuration + }; + + // See if the title should be changed + if (!this.autoSkip) { + newState.noticeTitle = chrome.i18n.getMessage("noticeTitle"); + } + + //reset countdown + this.setState(newState, () => { + this.noticeRef.current.resetCountdown(); + }); } /** Sets up notice to be not skipped yet */ @@ -478,36 +500,14 @@ class SkipNoticeComponent extends React.Component this.reskip(index), + skipButtonText: buttonText, + skipButtonCallback: (index) => this.reskip(index), // change max duration to however much of the sponsor is left maxCountdownTime: maxCountdownTime, countdownTime: maxCountdownTime() } as SkipNoticeState; } - reskip(index: number): void { - this.contentContainer().reskipSponsorTime(this.segments[index]); - - const newState: SkipNoticeState = { - unskipText: chrome.i18n.getMessage("unskip"), - unskipCallback: this.unskip.bind(this), - - maxCountdownTime: () => Config.config.skipNoticeDuration, - countdownTime: Config.config.skipNoticeDuration - }; - - // See if the title should be changed - if (!this.autoSkip) { - newState.noticeTitle = chrome.i18n.getMessage("noticeTitle"); - } - - //reset countdown - this.setState(newState, () => { - this.noticeRef.current.resetCountdown(); - }); - } - afterVote(segment: SponsorTime, type: number, category: Category): void { this.addVoteButtonInfo(chrome.i18n.getMessage("voted")); @@ -558,6 +558,42 @@ class SkipNoticeComponent extends React.Component manualSkipPercentCount; - - video.currentTime = segment.segment[1]; - sendTelemetryAndCount([segment], skippedTime, fullSkip); - startSponsorSchedule(true, segment.segment[1], false); + if (segment.actionType === ActionType.Mute) { + video.muted = true; + videoMuted = true; + } else { + const skippedTime = Math.max(segment.segment[1] - video.currentTime, 0); + const segmentDuration = segment.segment[1] - segment.segment[0]; + const fullSkip = skippedTime / segmentDuration > manualSkipPercentCount; + + video.currentTime = segment.segment[1]; + sendTelemetryAndCount([segment], skippedTime, fullSkip); + startSponsorSchedule(true, segment.segment[1], false); + } } function createButton(baseID: string, title: string, callback: () => void, imageName: string, isDraggable = false): HTMLElement { From 60e54ee1294747c3d15a8b17dad0940dbcf1345a Mon Sep 17 00:00:00 2001 From: Ajay Ramachandran Date: Wed, 1 Sep 2021 21:03:32 -0400 Subject: [PATCH 4/9] Hide unmute buttons after segment is finished --- src/components/SkipNoticeComponent.tsx | 16 ++++++++++++++-- src/content.ts | 5 +++++ src/render/SkipNotice.tsx | 4 ++++ 3 files changed, 23 insertions(+), 2 deletions(-) diff --git a/src/components/SkipNoticeComponent.tsx b/src/components/SkipNoticeComponent.tsx index b3ca619b..0900557f 100644 --- a/src/components/SkipNoticeComponent.tsx +++ b/src/components/SkipNoticeComponent.tsx @@ -41,6 +41,7 @@ export interface SkipNoticeState { skipButtonText?: string; skipButtonCallback?: (index: number) => void; + showSkipButton?: boolean; downvoting?: boolean; choosingCategory?: boolean; @@ -112,6 +113,7 @@ class SkipNoticeComponent extends React.Component this.unskip(index), + showSkipButton: true, downvoting: false, choosingCategory: false, @@ -296,9 +298,9 @@ class SkipNoticeComponent extends React.Component 1 + if (this.state.showSkipButton && (this.segments.length > 1 || getCategoryActionType(this.segments[0].category) !== CategoryActionType.POI - || this.props.unskipTime) { + || this.props.unskipTime)) { return (