import * as React from "react"; import * as CompileConfig from "../../config.json"; import Config from "../config" import { Category, ContentContainer, CategoryActionType, SponsorHideType, SponsorTime, NoticeVisbilityMode, ActionType } from "../types"; import NoticeComponent from "./NoticeComponent"; import NoticeTextSelectionComponent from "./NoticeTextSectionComponent"; import SubmissionNotice from "../render/SubmissionNotice"; import { getCategoryActionType, getSkippingText } from "../utils/categoryUtils"; export enum SkipNoticeAction { None, Upvote, Downvote, CategoryVote, CopyDownvote, Unskip } export interface SkipNoticeProps { segments: SponsorTime[]; autoSkip: boolean; // Contains functions and variables from the content script needed by the skip notice contentContainer: ContentContainer; closeListener: () => void; showKeybindHint?: boolean; smaller: boolean; unskipTime?: number; } export interface SkipNoticeState { noticeTitle?: string; messages?: string[]; messageOnClick?: (event: React.MouseEvent) => unknown; countdownTime?: number; maxCountdownTime?: () => number; countdownText?: string; skipButtonText?: string; skipButtonCallback?: (index: number) => void; showSkipButton?: boolean; downvoting?: boolean; choosingCategory?: boolean; thanksForVotingText?: string; //null until the voting buttons should be hidden actionState?: SkipNoticeAction; showKeybindHint?: boolean; smaller?: boolean; } class SkipNoticeComponent extends React.Component { segments: SponsorTime[]; autoSkip: boolean; // Contains functions and variables from the content script needed by the skip notice contentContainer: ContentContainer; amountOfPreviousNotices: number; showInSecondSlot: boolean; audio: HTMLAudioElement; idSuffix: string; noticeRef: React.MutableRefObject; categoryOptionRef: React.RefObject; // Used to update on config change configListener: () => void; constructor(props: SkipNoticeProps) { super(props); this.noticeRef = React.createRef(); this.categoryOptionRef = React.createRef(); this.segments = props.segments; this.autoSkip = props.autoSkip; this.contentContainer = props.contentContainer; this.audio = null; const noticeTitle = getSkippingText(this.segments, this.props.autoSkip); const previousSkipNotices = document.getElementsByClassName("sponsorSkipNoticeParent"); this.amountOfPreviousNotices = previousSkipNotices.length; // If there is at least one already in the first slot this.showInSecondSlot = previousSkipNotices.length > 0 && [...previousSkipNotices].some(notice => !notice.classList.contains("secondSkipNotice")); // Sort segments if (this.segments.length > 1) { this.segments.sort((a, b) => a.segment[0] - b.segment[0]); } //this is the suffix added at the end of every id for (const segment of this.segments) { this.idSuffix += segment.UUID; } this.idSuffix += this.amountOfPreviousNotices; // Setup state this.state = { noticeTitle, messages: [], messageOnClick: null, //the countdown until this notice closes maxCountdownTime: () => Config.config.skipNoticeDuration, countdownTime: Config.config.skipNoticeDuration, countdownText: null, skipButtonText: this.getUnskipText(), skipButtonCallback: (index) => this.unskip(index), showSkipButton: true, downvoting: false, choosingCategory: false, thanksForVotingText: null, actionState: SkipNoticeAction.None, showKeybindHint: this.props.showKeybindHint ?? true, smaller: this.props.smaller ?? false } if (!this.autoSkip) { // Assume manual skip is only skipping 1 submission Object.assign(this.state, this.getUnskippedModeInfo(0, this.getSkipText())); } } componentDidMount(): void { if (Config.config.audioNotificationOnSkip && this.audio) { this.audio.volume = this.contentContainer().v.volume * 0.1; if (this.autoSkip) this.audio.play(); } } render(): React.ReactElement { const noticeStyle: React.CSSProperties = { } if (this.contentContainer().onMobileYouTube) { noticeStyle.bottom = "4em"; noticeStyle.transform = "scale(0.8) translate(10%, 10%)"; } // If it started out as smaller, always keep the // skip button there const firstColumn = this.props.smaller ? ( this.getSkipButton() ) : null; return ( = NoticeVisbilityMode.FadedForAll || (Config.config.noticeVisibilityMode >= NoticeVisbilityMode.FadedForAutoSkip && this.autoSkip)} timed={true} maxCountdownTime={this.state.maxCountdownTime} videoSpeed={() => this.contentContainer().v?.playbackRate} style={noticeStyle} ref={this.noticeRef} closeListener={() => this.closeListener()} smaller={this.state.smaller} limitWidth={true} firstColumn={firstColumn} bottomRow={[...this.getMessageBoxes(), ...this.getBottomRow() ]} onMouseEnter={() => this.onMouseEnter() } > {(Config.config.audioNotificationOnSkip) && } ); } getBottomRow(): JSX.Element[] { return [ /* Bottom Row */ ( {/* Vote Button Container */} {!this.state.thanksForVotingText ? {/* Upvote Button */} this.prepAction(SkipNoticeAction.Upvote)}> {/* Report Button */} this.adjustDownvotingState(true)}> {/* Copy and Downvote Button */} this.prepAction(SkipNoticeAction.CopyDownvote)}> : {this.state.thanksForVotingText} } {/* Unskip/Skip Button */} {!this.props.smaller ? this.getSkipButton() : null} {/* Never show button if autoSkip is enabled */} {!this.autoSkip ? "" : } ), /* Downvote Options Row */ (this.state.downvoting && {/* Normal downvote */} {/* Category vote */} ), /* Category Chooser Row */ (this.state.choosingCategory && {/* Category Selector */} {/* Submit Button */} {this.segments.length === 1 && } ), /* Segment Chooser Row */ (this.state.actionState !== SkipNoticeAction.None && {this.getSubmissionChooser()} ) ]; } getSkipButton(): JSX.Element { if (this.state.showSkipButton && (this.segments.length > 1 || getCategoryActionType(this.segments[0].category) !== CategoryActionType.POI || this.props.unskipTime)) { return ( ); } } getSubmissionChooser(): JSX.Element[] { const elements: JSX.Element[] = []; for (let i = 0; i < this.segments.length; i++) { elements.push( ); } return elements; } onMouseEnter(): void { if (this.state.smaller) { this.setState({ smaller: false }); } } getMessageBoxes(): JSX.Element[] { if (this.state.messages.length === 0) { // Add a spacer if there is no text return [ ]; } const elements: JSX.Element[] = []; for (let i = 0; i < this.state.messages.length; i++) { elements.push( ) } return elements; } prepAction(action: SkipNoticeAction): void { const isDownvotingCategory = (SkipNoticeAction.CategoryVote === action); if (this.segments.length === 1) { this.performAction(0, action); } else { this.setState({ actionState: action, downvoting: false, choosingCategory: isDownvotingCategory }); } } /** * Performs the action from the current state * * @param index */ performAction(index: number, action?: SkipNoticeAction): void { switch (action ?? this.state.actionState) { case SkipNoticeAction.None: break; case SkipNoticeAction.Upvote: this.contentContainer().vote(1, this.segments[index].UUID, undefined, this); break; case SkipNoticeAction.Downvote: this.contentContainer().vote(0, this.segments[index].UUID, undefined, this); break; case SkipNoticeAction.CategoryVote: this.contentContainer().vote(undefined, this.segments[index].UUID, this.categoryOptionRef.current.value as Category, this) break; case SkipNoticeAction.CopyDownvote: this.copyDownvote(index); break; case SkipNoticeAction.Unskip: this.state.skipButtonCallback(index); break; } this.setState({ actionState: SkipNoticeAction.None, downvoting: false, choosingCategory: false }); } adjustDownvotingState(value: boolean): void { this.setState({ downvoting: value, choosingCategory: false, actionState: SkipNoticeAction.None }); } openCategoryChooser(): void { this.setState({ choosingCategory: true, downvoting: false }, () => { if (this.segments.length > 1) { // Use the action selectors as a submit button this.prepAction(SkipNoticeAction.CategoryVote); } }); } getCategoryOptions(): React.ReactElement[] { const elements = []; const categories = CompileConfig.categoryList.filter((cat => getCategoryActionType(cat as Category) === CategoryActionType.Skippable)); for (const category of categories) { elements.push( ); } return elements; } unskip(index: number): void { this.contentContainer().unskipSponsorTime(this.segments[index], this.props.unskipTime); this.unskippedMode(index, this.getReskipText()); } reskip(index: number): void { this.contentContainer().reskipSponsorTime(this.segments[index]); const newState: SkipNoticeState = { skipButtonText: this.getUnskipText(), skipButtonCallback: 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(); }); } /** Sets up notice to be not skipped yet */ unskippedMode(index: number, buttonText: string): void { //setup new callback and reset countdown this.setState(this.getUnskippedModeInfo(index, buttonText), () => { this.noticeRef.current.resetCountdown(); }); } getUnskippedModeInfo(index: number, buttonText: string): SkipNoticeState { const changeCountdown = getCategoryActionType(this.segments[index].category) === CategoryActionType.Skippable; const maxCountdownTime = changeCountdown ? () => { const sponsorTime = this.segments[index]; const duration = Math.round((sponsorTime.segment[1] - this.contentContainer().v.currentTime) * (1 / this.contentContainer().v.playbackRate)); return Math.max(duration, Config.config.skipNoticeDuration); } : this.state.maxCountdownTime; return { skipButtonText: buttonText, skipButtonCallback: (index) => this.reskip(index), // change max duration to however much of the sponsor is left maxCountdownTime: maxCountdownTime, countdownTime: maxCountdownTime() } as SkipNoticeState; } afterVote(segment: SponsorTime, type: number, category: Category): void { this.addVoteButtonInfo(chrome.i18n.getMessage("voted")); if (type === 0) { this.setNoticeInfoMessage(chrome.i18n.getMessage("hitGoBack")); this.adjustDownvotingState(false); } // Change the sponsor locally if (segment) { if (type === 0) { segment.hidden = SponsorHideType.Downvoted; } else if (category) { segment.category = category; } this.contentContainer().updatePreviewBar(); } } copyDownvote(index: number): void { const sponsorVideoID = this.props.contentContainer().sponsorVideoID; const sponsorTimesSubmitting : SponsorTime = { segment: this.segments[index].segment, UUID: null, category: this.segments[index].category, actionType: this.segments[index].actionType, source: 2 }; const segmentTimes = Config.config.segmentTimes.get(sponsorVideoID) || []; segmentTimes.push(sponsorTimesSubmitting); Config.config.segmentTimes.set(sponsorVideoID, segmentTimes); this.props.contentContainer().sponsorTimesSubmitting.push(sponsorTimesSubmitting); this.props.contentContainer().updatePreviewBar(); this.props.contentContainer().resetSponsorSubmissionNotice(); this.props.contentContainer().updateEditButtonsOnPlayer(); } setNoticeInfoMessageWithOnClick(onClick: (event: React.MouseEvent) => unknown, ...messages: string[]): void { this.setState({ messages, messageOnClick: (event) => onClick(event) }); } setNoticeInfoMessage(...messages: string[]): void { this.setState({ messages }); } addVoteButtonInfo(message: string): void { this.setState({ thanksForVotingText: message }); } resetVoteButtonInfo(): void { this.setState({ thanksForVotingText: null }); } closeListener(): void { //this.clearConfigListener(); this.props.closeListener(); } unmutedListener(): void { if (this.props.segments.length === 1 && this.props.segments[0].actionType === ActionType.Mute && this.contentContainer().v.currentTime >= this.props.segments[0].segment[1]) { this.setState({ showSkipButton: false }); } } private getUnskipText(): string { switch (this.props.segments[0].actionType) { case ActionType.Mute: { return chrome.i18n.getMessage("unmute"); } case ActionType.Skip: default: { return chrome.i18n.getMessage("unskip"); } } } private getReskipText(): string { switch (this.props.segments[0].actionType) { case ActionType.Mute: { return chrome.i18n.getMessage("mute"); } case ActionType.Skip: default: { return chrome.i18n.getMessage("reskip"); } } } private getSkipText(): string { switch (this.props.segments[0].actionType) { case ActionType.Mute: { return chrome.i18n.getMessage("mute"); } case ActionType.Skip: default: { return chrome.i18n.getMessage("skip"); } } } } export default SkipNoticeComponent;