Add skip keybind

https://github.com/ajayyy/SponsorBlock/issues/299
This commit is contained in:
Ajay Ramachandran
2021-01-17 14:25:45 -05:00
parent ff0dc6e570
commit e269b1aec6
8 changed files with 123 additions and 51 deletions

View File

@@ -232,6 +232,9 @@
"message": "If you still don't like it, hit the never show button.", "message": "If you still don't like it, hit the never show button.",
"description": "The second line of the message displayed after the notice was upgraded." "description": "The second line of the message displayed after the notice was upgraded."
}, },
"setSkipShortcut": {
"message": "Set key for skipping a segment"
},
"setStartSponsorShortcut": { "setStartSponsorShortcut": {
"message": "Set key for start segment keybind" "message": "Set key for start segment keybind"
}, },

View File

@@ -84,6 +84,27 @@
<br/> <br/>
<br/> <br/>
<div option-type="keybind-change" sync-option="skipKeybind">
<div class="option-button trigger-button">
__MSG_setSkipShortcut__
</div>
<div class="option-hidden-section hidden">
<br/>
<div class="medium-description keybind-status">
__MSG_keybindDescription__
</div>
<span class="medium-description bold keybind-status-key">
</span>
</div>
</div>
<br/>
<br/>
<div option-type="keybind-change" sync-option="startSponsorKeybind"> <div option-type="keybind-change" sync-option="startSponsorKeybind">
<div class="option-button trigger-button"> <div class="option-button trigger-button">
__MSG_setStartSponsorShortcut__ __MSG_setStartSponsorShortcut__

View File

@@ -178,11 +178,18 @@ class NoticeComponent extends React.Component<NoticeProps, NoticeState> {
}) })
} }
removeFadeAnimation(): void {
//remove the fade out class if it exists
const notice = document.getElementById("sponsorSkipNotice" + this.idSuffix);
notice.classList.remove("sponsorSkipNoticeFadeOut");
notice.style.animation = "none";
}
pauseCountdown(): void { pauseCountdown(): void {
if (!this.props.timed) return; if (!this.props.timed) return;
//remove setInterval //remove setInterval
clearInterval(this.countdownInterval); if (this.countdownInterval) clearInterval(this.countdownInterval);
this.countdownInterval = null; this.countdownInterval = null;
//reset countdown and inform the user //reset countdown and inform the user
@@ -191,10 +198,7 @@ class NoticeComponent extends React.Component<NoticeProps, NoticeState> {
countdownText: this.state.countdownManuallyPaused ? chrome.i18n.getMessage("manualPaused") : chrome.i18n.getMessage("paused") countdownText: this.state.countdownManuallyPaused ? chrome.i18n.getMessage("manualPaused") : chrome.i18n.getMessage("paused")
}); });
//remove the fade out class if it exists this.removeFadeAnimation();
const notice = document.getElementById("sponsorSkipNotice" + this.idSuffix);
notice.classList.remove("sponsorSkipNoticeFadeOut");
notice.style.animation = "none";
} }
startCountdown(): void { startCountdown(): void {
@@ -208,16 +212,25 @@ class NoticeComponent extends React.Component<NoticeProps, NoticeState> {
countdownText: null countdownText: null
}); });
this.setupInterval();
}
setupInterval(): void {
if (this.countdownInterval) clearInterval(this.countdownInterval);
this.countdownInterval = setInterval(this.countdown.bind(this), 1000); this.countdownInterval = setInterval(this.countdown.bind(this), 1000);
} }
resetCountdown(): void { resetCountdown(): void {
if (!this.props.timed) return; if (!this.props.timed) return;
this.setupInterval();
this.setState({ this.setState({
countdownTime: this.state.maxCountdownTime(), countdownTime: this.state.maxCountdownTime(),
countdownText: null countdownText: null
}); });
this.removeFadeAnimation();
} }
/** /**

View File

@@ -5,7 +5,7 @@ import { ContentContainer, SponsorHideType, SponsorTime } from "../types";
import NoticeComponent from "./NoticeComponent"; import NoticeComponent from "./NoticeComponent";
import NoticeTextSelectionComponent from "./NoticeTextSectionComponent"; import NoticeTextSelectionComponent from "./NoticeTextSectionComponent";
enum SkipNoticeAction { export enum SkipNoticeAction {
None, None,
Upvote, Upvote,
Downvote, Downvote,
@@ -24,23 +24,23 @@ export interface SkipNoticeProps {
} }
export interface SkipNoticeState { export interface SkipNoticeState {
noticeTitle: string; noticeTitle?: string;
messages: string[]; messages?: string[];
messageOnClick: (event: React.MouseEvent) => unknown; messageOnClick?: (event: React.MouseEvent) => unknown;
countdownTime: number; countdownTime?: number;
maxCountdownTime: () => number; maxCountdownTime?: () => number;
countdownText: string; countdownText?: string;
unskipText: string; unskipText?: string;
unskipCallback: (index: number) => void; unskipCallback?: (index: number) => void;
downvoting: boolean; downvoting?: boolean;
choosingCategory: boolean; choosingCategory?: boolean;
thanksForVotingText: string; //null until the voting buttons should be hidden thanksForVotingText?: string; //null until the voting buttons should be hidden
actionState: SkipNoticeAction; actionState?: SkipNoticeAction;
} }
class SkipNoticeComponent extends React.Component<SkipNoticeProps, SkipNoticeState> { class SkipNoticeComponent extends React.Component<SkipNoticeProps, SkipNoticeState> {
@@ -196,7 +196,7 @@ class SkipNoticeComponent extends React.Component<SkipNoticeProps, SkipNoticeSta
style={{marginLeft: "4px"}} style={{marginLeft: "4px"}}
onClick={() => this.prepAction(SkipNoticeAction.Unskip)}> onClick={() => this.prepAction(SkipNoticeAction.Unskip)}>
{this.state.unskipText} {this.state.unskipText + " (" + Config.config.skipKeybind + ")"}
</button> </button>
</td> </td>
@@ -456,21 +456,23 @@ class SkipNoticeComponent extends React.Component<SkipNoticeProps, SkipNoticeSta
reskip(index: number): void { reskip(index: number): void {
this.contentContainer().reskipSponsorTime(this.segments[index]); this.contentContainer().reskipSponsorTime(this.segments[index]);
//reset countdown const newState: SkipNoticeState = {
this.setState({
unskipText: chrome.i18n.getMessage("unskip"), unskipText: chrome.i18n.getMessage("unskip"),
unskipCallback: this.unskip.bind(this), unskipCallback: this.unskip.bind(this),
maxCountdownTime: () => 4, maxCountdownTime: () => 4,
countdownTime: 4 countdownTime: 4
}); };
// See if the title should be changed // See if the title should be changed
if (!this.autoSkip) { if (!this.autoSkip) {
this.setState({ newState.noticeTitle = chrome.i18n.getMessage("noticeTitle");
noticeTitle: chrome.i18n.getMessage("noticeTitle")
});
} }
//reset countdown
this.setState(newState, () => {
this.noticeRef.current.resetCountdown();
});
} }
afterVote(segment: SponsorTime, type: number, category: string): void { afterVote(segment: SponsorTime, type: number, category: string): void {

View File

@@ -10,6 +10,7 @@ interface SBConfig {
defaultCategory: string, defaultCategory: string,
whitelistedChannels: string[], whitelistedChannels: string[],
forceChannelCheck: boolean, forceChannelCheck: boolean,
skipKeybind: string,
startSponsorKeybind: string, startSponsorKeybind: string,
submitKeybind: string, submitKeybind: string,
minutesSaved: number, minutesSaved: number,
@@ -142,6 +143,7 @@ const Config: SBObject = {
defaultCategory: "chooseACategory", defaultCategory: "chooseACategory",
whitelistedChannels: [], whitelistedChannels: [],
forceChannelCheck: false, forceChannelCheck: false,
skipKeybind: "Enter",
startSponsorKeybind: ";", startSponsorKeybind: ";",
submitKeybind: "'", submitKeybind: "'",
minutesSaved: 0, minutesSaved: 0,

View File

@@ -23,6 +23,8 @@ let sponsorDataFound = false;
let sponsorTimes: SponsorTime[] = null; let sponsorTimes: SponsorTime[] = null;
//what video id are these sponsors for //what video id are these sponsors for
let sponsorVideoID: VideoID = null; let sponsorVideoID: VideoID = null;
// List of open skip notices
const skipNotices: SkipNotice[] = [];
// JSON video info // JSON video info
let videoInfo: VideoInfo = null; let videoInfo: VideoInfo = null;
@@ -35,11 +37,13 @@ let channelID: string;
let currentSkipSchedule: NodeJS.Timeout = null; let currentSkipSchedule: NodeJS.Timeout = null;
let seekListenerSetUp = false let seekListenerSetUp = false
/** @type {Array[boolean]} Has the sponsor been skipped */ /** Has the sponsor been skipped */
let sponsorSkipped: boolean[] = []; let sponsorSkipped: boolean[] = [];
//the video //the video
let video: HTMLVideoElement; let video: HTMLVideoElement;
// List of videos that have had event listeners added to them
const videoRootsWithEventListeners: HTMLDivElement[] = [];
let onInvidious; let onInvidious;
let onMobileYouTube; let onMobileYouTube;
@@ -99,6 +103,7 @@ const skipNoticeContentContainer: ContentContainer = () => ({
unskipSponsorTime, unskipSponsorTime,
sponsorTimes, sponsorTimes,
sponsorTimesSubmitting, sponsorTimesSubmitting,
skipNotices,
v: video, v: video,
sponsorVideoID, sponsorVideoID,
reskipSponsorTime, reskipSponsorTime,
@@ -197,28 +202,6 @@ if (!Config.configListeners.includes(contentConfigUpdateListener)) {
Config.configListeners.push(contentConfigUpdateListener); Config.configListeners.push(contentConfigUpdateListener);
} }
//check for hotkey pressed
document.onkeydown = function(e: KeyboardEvent){
const key = e.key;
const video = document.getElementById("movie_player");
const startSponsorKey = Config.config.startSponsorKeybind;
const submitKey = Config.config.submitKeybind;
//is the video in focus, otherwise they could be typing a comment
if (document.activeElement === video) {
if(key == startSponsorKey){
//semicolon
startSponsorClicked();
} else if (key == submitKey) {
//single quote
submitSponsorTimes();
}
}
}
function resetValues() { function resetValues() {
lastCheckTime = 0; lastCheckTime = 0;
lastCheckVideoTime = -1; lastCheckVideoTime = -1;
@@ -512,6 +495,8 @@ async function sponsorsLookup(id: string) {
return; return;
} }
addHotkeyListener();
if (!durationListenerSetUp) { if (!durationListenerSetUp) {
durationListenerSetUp = true; durationListenerSetUp = true;
@@ -996,7 +981,7 @@ function skipToTime(v: HTMLVideoElement, skipTime: number[], skippingSegments: S
if (openNotice) { if (openNotice) {
//send out the message saying that a sponsor message was skipped //send out the message saying that a sponsor message was skipped
if (!Config.config.dontShowNotice || !autoSkip) { if (!Config.config.dontShowNotice || !autoSkip) {
new SkipNotice(skippingSegments, autoSkip, skipNoticeContentContainer); skipNotices.push(new SkipNotice(skippingSegments, autoSkip, skipNoticeContentContainer));
} }
} }
@@ -1542,6 +1527,41 @@ function getSegmentsMessage(sponsorTimes: SponsorTime[]): string {
return sponsorTimesMessage; return sponsorTimesMessage;
} }
function addHotkeyListener(): boolean {
const videoRoot = document.getElementById("movie_player") as HTMLDivElement;
if (!videoRootsWithEventListeners.includes(videoRoot)) {
videoRoot.addEventListener("keydown", hotkeyListener);
videoRootsWithEventListeners.push(videoRoot);
return true;
}
return false;
}
function hotkeyListener(e: KeyboardEvent): void {
const key = e.key;
const skipKey = Config.config.skipKeybind;
const startSponsorKey = Config.config.startSponsorKeybind;
const submitKey = Config.config.submitKeybind;
switch (key) {
case skipKey:
if (skipNotices.length > 0) {
const latestSkipNotice = skipNotices[skipNotices.length - 1];
latestSkipNotice.toggleSkip.call(latestSkipNotice);
}
break;
case startSponsorKey:
startSponsorClicked();
break;
case submitKey:
submitSponsorTimes();
break;
}
}
/** /**
* Is this an unlisted YouTube video. * Is this an unlisted YouTube video.
* Assumes that the the privacy info is available. * Assumes that the the privacy info is available.

View File

@@ -1,7 +1,7 @@
import * as React from "react"; import * as React from "react";
import * as ReactDOM from "react-dom"; import * as ReactDOM from "react-dom";
import SkipNoticeComponent from "../components/SkipNoticeComponent"; import SkipNoticeComponent, { SkipNoticeAction } from "../components/SkipNoticeComponent";
import { SponsorTime, ContentContainer } from "../types"; import { SponsorTime, ContentContainer } from "../types";
class SkipNotice { class SkipNotice {
@@ -15,6 +15,8 @@ class SkipNotice {
skipNoticeRef: React.MutableRefObject<SkipNoticeComponent>; skipNoticeRef: React.MutableRefObject<SkipNoticeComponent>;
constructor(segments: SponsorTime[], autoSkip = false, contentContainer: ContentContainer) { constructor(segments: SponsorTime[], autoSkip = false, contentContainer: ContentContainer) {
this.skipNoticeRef = React.createRef();
this.segments = segments; this.segments = segments;
this.autoSkip = autoSkip; this.autoSkip = autoSkip;
this.contentContainer = contentContainer; this.contentContainer = contentContainer;
@@ -67,6 +69,13 @@ class SkipNotice {
ReactDOM.unmountComponentAtNode(this.noticeElement); ReactDOM.unmountComponentAtNode(this.noticeElement);
this.noticeElement.remove(); this.noticeElement.remove();
const skipNotices = this.contentContainer().skipNotices;
skipNotices.splice(skipNotices.indexOf(this), 1);
}
toggleSkip(): void {
this.skipNoticeRef.current.prepAction(SkipNoticeAction.Unskip);
} }
} }

View File

@@ -1,5 +1,6 @@
import SubmissionNotice from "./render/SubmissionNotice"; import SubmissionNotice from "./render/SubmissionNotice";
import SkipNoticeComponent from "./components/SkipNoticeComponent"; import SkipNoticeComponent from "./components/SkipNoticeComponent";
import SkipNotice from "./render/SkipNotice";
export interface ContentContainer { export interface ContentContainer {
(): { (): {
@@ -8,6 +9,7 @@ export interface ContentContainer {
unskipSponsorTime: (segment: SponsorTime) => void, unskipSponsorTime: (segment: SponsorTime) => void,
sponsorTimes: SponsorTime[], sponsorTimes: SponsorTime[],
sponsorTimesSubmitting: SponsorTime[], sponsorTimesSubmitting: SponsorTime[],
skipNotices: SkipNotice[],
v: HTMLVideoElement, v: HTMLVideoElement,
sponsorVideoID, sponsorVideoID,
reskipSponsorTime: (segment: SponsorTime) => void, reskipSponsorTime: (segment: SponsorTime) => void,