diff options
author | Ajay Ramachandran <[email protected]> | 2021-10-15 19:00:38 -0400 |
---|---|---|
committer | GitHub <[email protected]> | 2021-10-15 19:00:38 -0400 |
commit | e9204be96fff35c466a0d0c154d54c025df7ecb7 (patch) | |
tree | e747f550b10463b8b8d1690017f7413da8d0ae37 | |
parent | b8ab05ccad16604bd02c30d0e0ba107bcbe9f18e (diff) | |
parent | 08630616655d91a40b864d9997f0f5975d0e9b6a (diff) | |
download | SponsorBlock-e9204be96fff35c466a0d0c154d54c025df7ecb7.tar.gz SponsorBlock-e9204be96fff35c466a0d0c154d54c025df7ecb7.zip |
Merge pull request #988 from FlorianZahn/copySegment
Copy segments into your unsubmitted and SkipNotice changes
-rw-r--r-- | config.json.example | 12 | ||||
-rw-r--r-- | manifest/manifest.json | 1 | ||||
-rw-r--r-- | public/_locales/en/messages.json | 17 | ||||
-rw-r--r-- | public/content.css | 16 | ||||
-rw-r--r-- | public/icons/thumbs_down_locked.svg | 58 | ||||
-rw-r--r-- | src/components/SkipNoticeComponent.tsx | 354 | ||||
-rw-r--r-- | src/components/SponsorTimeEditComponent.tsx | 11 | ||||
-rw-r--r-- | src/config.ts | 19 | ||||
-rw-r--r-- | src/content.ts | 65 | ||||
-rw-r--r-- | src/popup.ts | 4 | ||||
-rw-r--r-- | src/svg-icons/pencil_svg.tsx | 18 | ||||
-rw-r--r-- | src/svg-icons/thumbs_down_svg.tsx | 23 | ||||
-rw-r--r-- | src/svg-icons/thumbs_up_svg.tsx | 22 | ||||
-rw-r--r-- | src/types.ts | 4 | ||||
-rw-r--r-- | src/utils.ts | 1 |
15 files changed, 518 insertions, 107 deletions
diff --git a/config.json.example b/config.json.example index ffc482a5..f6916065 100644 --- a/config.json.example +++ b/config.json.example @@ -12,5 +12,17 @@ "preview": ["skip"], "music_offtopic": ["skip"], "poi_highlight": ["skip"] + }, + "wikiLinks": { + "sponsor": "https://wiki.sponsor.ajay.app/w/Sponsor", + "selfpromo": "https://wiki.sponsor.ajay.app/w/Unpaid/Self_Promotion", + "interaction": "https://wiki.sponsor.ajay.app/w/Interaction_Reminder_(Subscribe)", + "intro": "https://wiki.sponsor.ajay.app/w/Intermission/Intro_Animation", + "outro": "https://wiki.sponsor.ajay.app/w/Endcards/Credits", + "preview": "https://wiki.sponsor.ajay.app/w/Preview/Recap", + "music_offtopic": "https://wiki.sponsor.ajay.app/w/Music:_Non-Music_Section", + "poi_highlight": "https://wiki.sponsor.ajay.app/w/Highlight", + "guidelines": "https://wiki.sponsor.ajay.app/w/Guidelines", + "mute": "https://wiki.sponsor.ajay.app/w/Mute_Segment" } } diff --git a/manifest/manifest.json b/manifest/manifest.json index c9350235..3c49bebf 100644 --- a/manifest/manifest.json +++ b/manifest/manifest.json @@ -37,6 +37,7 @@ "icons/upvote.png", "icons/downvote.png", "icons/thumbs_down.svg", + "icons/thumbs_down_locked.svg", "icons/thumbs_up.svg", "icons/help.svg", "icons/report.png", diff --git a/public/_locales/en/messages.json b/public/_locales/en/messages.json index 35a27cf2..3d575832 100644 --- a/public/_locales/en/messages.json +++ b/public/_locales/en/messages.json @@ -700,7 +700,7 @@ "message": "Incorrect/Wrong Timing" }, "incorrectCategory": { - "message": "Wrong Category" + "message": "Change Category" }, "nonMusicCategoryOnMusic": { "message": "This video is categorized as music. Are you sure this has a sponsor? If this is actually a \"Non-Music segment\", open up the extension options and enable this category. Then, you can submit this segment as \"Non-Music\" instead of sponsor. Please read the guidelines if you are confused." @@ -811,6 +811,21 @@ "LearnMore": { "message": "Learn More" }, + "CopyDownvoteButtonInfo": { + "message": "Downvotes and creates a local copy for you to resubmit" + }, + "OpenCategoryWikiPage": { + "message": "Open this category's wiki page." + }, + "CopyAndDownvote": { + "message": "Copy and downvote" + }, + "ContinueVoting": { + "message": "Continue Voting" + }, + "ChangeCategoryTooltip": { + "message": "This will instantly apply to your segments" + }, "SponsorTimeEditScrollNewFeature": { "message": "Use your mousewheel while hovering over the edit box to quickly adjust the time. Combinations of the ctrl or shift key can be used to fine tune the changes." } diff --git a/public/content.css b/public/content.css index 2bd1b113..4751d293 100644 --- a/public/content.css +++ b/public/content.css @@ -217,7 +217,7 @@ /* if two are very close to eachother */ .secondSkipNotice { - bottom: 250px; + bottom: 290px; } .noticeLeftIcon { @@ -254,12 +254,16 @@ .sponsorTimesVoteButtonsContainer { float: left; - + vertical-align:middle; padding: 2px 5px; margin-right: 4px; } +.sponsorTimesVoteButtonsContainer div{ + display: inline-block; +} + .sponsorSkipNoticeRightSection { right: 0; position: absolute; @@ -330,7 +334,8 @@ } .voteButton { - height: 17px; + height: 24px; + width: 24px; cursor: pointer; } .voteButton:hover { @@ -556,6 +561,10 @@ input::-webkit-inner-spin-button { border-color: rgba(28, 28, 28, 0.7) transparent transparent transparent; } +.sponsorBlockLockedColor { + color: #ffc83d; +} + .sponsorBlockRectangleTooltip { position: absolute; border-radius: 5px; @@ -565,3 +574,4 @@ input::-webkit-inner-spin-button { white-space: normal; line-height: 1.5em; } + diff --git a/public/icons/thumbs_down_locked.svg b/public/icons/thumbs_down_locked.svg new file mode 100644 index 00000000..57672e2d --- /dev/null +++ b/public/icons/thumbs_down_locked.svg @@ -0,0 +1,58 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<svg + xmlns:dc="http://purl.org/dc/elements/1.1/" + xmlns:cc="http://creativecommons.org/ns#" + xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" + xmlns:svg="http://www.w3.org/2000/svg" + xmlns="http://www.w3.org/2000/svg" + xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd" + xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" + height="24" + viewBox="0 0 24 24" + width="24" + version="1.1" + id="svg6" + sodipodi:docname="thumbs_down.svg" + inkscape:version="0.92.4 (5da689c313, 2019-01-14)"> + <metadata + id="metadata12"> + <rdf:RDF> + <cc:Work + rdf:about=""> + <dc:format>image/svg+xml</dc:format> + <dc:type + rdf:resource="http://purl.org/dc/dcmitype/StillImage" /> + </cc:Work> + </rdf:RDF> + </metadata> + <defs + id="defs10" /> + <sodipodi:namedview + pagecolor="#ffffff" + bordercolor="#666666" + borderopacity="1" + objecttolerance="10" + gridtolerance="10" + guidetolerance="10" + inkscape:pageopacity="0" + inkscape:pageshadow="2" + inkscape:window-width="730" + inkscape:window-height="480" + id="namedview8" + showgrid="false" + inkscape:zoom="9.8333333" + inkscape:cx="12" + inkscape:cy="12" + inkscape:window-x="0" + inkscape:window-y="0" + inkscape:window-maximized="0" + inkscape:current-layer="svg6" /> + <path + d="M0 0h24v24H0z" + fill="none" + id="path2" /> + <path + d="M15 3H6c-.83 0-1.54.5-1.84 1.22l-3.02 7.05c-.09.23-.14.47-.14.73v2c0 1.1.9 2 2 2h6.31l-.95 4.57-.03.32c0 .41.17.79.44 1.06L9.83 23l6.59-6.59c.36-.36.58-.86.58-1.41V5c0-1.1-.9-2-2-2zm4 0v12h4V3h-4z" + id="path4" + style="fill:#ffc83d" /> +</svg> diff --git a/src/components/SkipNoticeComponent.tsx b/src/components/SkipNoticeComponent.tsx index 4a515270..6d3c378b 100644 --- a/src/components/SkipNoticeComponent.tsx +++ b/src/components/SkipNoticeComponent.tsx @@ -4,14 +4,22 @@ 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 Utils from "../utils"; +const utils = new Utils(); import { getCategoryActionType, getSkippingText } from "../utils/categoryUtils"; +import ThumbsUpSvg from "../svg-icons/thumbs_up_svg"; +import ThumbsDownSvg from "../svg-icons/thumbs_down_svg"; +import PencilSvg from "../svg-icons/pencil_svg"; + export enum SkipNoticeAction { None, Upvote, Downvote, CategoryVote, + CopyDownvote, Unskip } @@ -43,7 +51,7 @@ export interface SkipNoticeState { skipButtonCallback?: (index: number) => void; showSkipButton?: boolean; - downvoting?: boolean; + editing?: boolean; choosingCategory?: boolean; thanksForVotingText?: string; //null until the voting buttons should be hidden @@ -52,6 +60,10 @@ export interface SkipNoticeState { showKeybindHint?: boolean; smaller?: boolean; + + voted?: SkipNoticeAction[]; + copied?: SkipNoticeAction[]; + } class SkipNoticeComponent extends React.Component<SkipNoticeProps, SkipNoticeState> { @@ -69,6 +81,10 @@ class SkipNoticeComponent extends React.Component<SkipNoticeProps, SkipNoticeSta noticeRef: React.MutableRefObject<NoticeComponent>; categoryOptionRef: React.RefObject<HTMLSelectElement>; + selectedColor: string; + unselectedColor: string; + lockedColor: string; + // Used to update on config change configListener: () => void; @@ -94,12 +110,16 @@ class SkipNoticeComponent extends React.Component<SkipNoticeProps, SkipNoticeSta this.segments.sort((a, b) => a.segment[0] - b.segment[0]); } - //this is the suffix added at the end of every id + // 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; + this.selectedColor = Config.config.colorPalette.red; + this.unselectedColor = Config.config.colorPalette.white; + this.lockedColor = Config.config.colorPalette.locked; + // Setup state this.state = { noticeTitle, @@ -115,7 +135,7 @@ class SkipNoticeComponent extends React.Component<SkipNoticeProps, SkipNoticeSta skipButtonCallback: (index) => this.unskip(index), showSkipButton: true, - downvoting: false, + editing: false, choosingCategory: false, thanksForVotingText: null, @@ -123,7 +143,11 @@ class SkipNoticeComponent extends React.Component<SkipNoticeProps, SkipNoticeSta showKeybindHint: this.props.showKeybindHint ?? true, - smaller: this.props.smaller ?? false + smaller: this.props.smaller ?? false, + + // Keep track of what segment the user interacted with. + voted: new Array(this.props.segments.length).fill(SkipNoticeAction.None), + copied: new Array(this.props.segments.length).fill(SkipNoticeAction.None), } if (!this.autoSkip) { @@ -186,29 +210,38 @@ class SkipNoticeComponent extends React.Component<SkipNoticeProps, SkipNoticeSta key={0}> {/* Vote Button Container */} - {!this.state.thanksForVotingText ? + {!this.state.thanksForVotingText ? <td id={"sponsorTimesVoteButtonsContainer" + this.idSuffix} className="sponsorTimesVoteButtonsContainer"> {/* Upvote Button */} - <img id={"sponsorTimesDownvoteButtonsContainer" + this.idSuffix} - className="sponsorSkipObject voteButton" - style={{marginRight: "10px"}} - src={chrome.extension.getURL("icons/thumbs_up.svg")} - title={chrome.i18n.getMessage("upvoteButtonInfo")} - onClick={() => this.prepAction(SkipNoticeAction.Upvote)}> - - </img> + <div id={"sponsorTimesDownvoteButtonsContainerUpvote" + this.idSuffix} + className="voteButton" + style={{marginRight: "5px"}} + title={chrome.i18n.getMessage("upvoteButtonInfo")} + onClick={() => this.prepAction(SkipNoticeAction.Upvote)}> + <ThumbsUpSvg fill={(this.state.actionState === SkipNoticeAction.Upvote) ? this.selectedColor : this.unselectedColor} /> + </div> {/* Report Button */} - <img id={"sponsorTimesDownvoteButtonsContainer" + this.idSuffix} - className="sponsorSkipObject voteButton" - src={chrome.extension.getURL("icons/thumbs_down.svg")} - title={chrome.i18n.getMessage("reportButtonInfo")} - onClick={() => this.adjustDownvotingState(true)}> - - </img> - + <div id={"sponsorTimesDownvoteButtonsContainerDownvote" + this.idSuffix} + className="voteButton" + style={{marginRight: "5px", marginLeft: "5px"}} + title={chrome.i18n.getMessage("reportButtonInfo")} + onClick={() => this.prepAction(SkipNoticeAction.Downvote)}> + <ThumbsDownSvg fill={this.downvoteButtonColor(SkipNoticeAction.Downvote)} /> + </div> + + {/* Copy and Downvote Button */} + <div id={"sponsorTimesDownvoteButtonsContainerCopyDownvote" + this.idSuffix} + className="voteButton" + style={{marginLeft: "5px"}} + onClick={() => this.openEditingOptions()}> + <PencilSvg fill={this.state.editing === true + || this.state.actionState === SkipNoticeAction.CopyDownvote + || this.state.choosingCategory === true + ? this.selectedColor : this.unselectedColor} /> + </div> </td> : @@ -216,7 +249,22 @@ class SkipNoticeComponent extends React.Component<SkipNoticeProps, SkipNoticeSta <td id={"sponsorTimesVoteButtonInfoMessage" + this.idSuffix} className="sponsorTimesInfoMessage sponsorTimesVoteButtonMessage" style={{marginRight: "10px"}}> - {this.state.thanksForVotingText} + + {/* Submitted string */} + <span style={{marginRight: "10px"}}> + {this.state.thanksForVotingText} + </span> + + {/* Continue Voting Button */} + <button id={"sponsorTimesContinueVotingContainer" + this.idSuffix} + className="sponsorSkipObject sponsorSkipNoticeButton" + title={"Continue Voting"} + onClick={() => this.setState({ + thanksForVotingText: null, + messages: [] + })}> + {chrome.i18n.getMessage("ContinueVoting")} + </button> </td> } @@ -229,45 +277,46 @@ class SkipNoticeComponent extends React.Component<SkipNoticeProps, SkipNoticeSta key={1}> <button className="sponsorSkipObject sponsorSkipNoticeButton sponsorSkipNoticeRightButton" onClick={this.contentContainer().dontShowNoticeAgain}> - {chrome.i18n.getMessage("Hide")} </button> </td> } </tr>), - /* Downvote Options Row */ - (this.state.downvoting && - <tr id={"sponsorSkipNoticeDownvoteOptionsRow" + this.idSuffix} + /* Edit Segments Row */ + (this.state.editing && !this.state.thanksForVotingText && !(this.state.choosingCategory || this.state.actionState === SkipNoticeAction.CopyDownvote) && + <tr id={"sponsorSkipNoticeEditSegmentsRow" + this.idSuffix} key={2}> - <td id={"sponsorTimesDownvoteOptionsContainer" + this.idSuffix}> + <td id={"sponsorTimesEditSegmentsContainer" + this.idSuffix}> - {/* Normal downvote */} + {/* Copy Segment */} <button className="sponsorSkipObject sponsorSkipNoticeButton" - onClick={() => this.prepAction(SkipNoticeAction.Downvote)}> - {chrome.i18n.getMessage("downvoteDescription")} + title={chrome.i18n.getMessage("CopyDownvoteButtonInfo")} + style={{color: this.downvoteButtonColor(SkipNoticeAction.Downvote)}} + onClick={() => this.prepAction(SkipNoticeAction.CopyDownvote)}> + {chrome.i18n.getMessage("CopyAndDownvote")} </button> {/* Category vote */} <button className="sponsorSkipObject sponsorSkipNoticeButton" - onClick={() => this.openCategoryChooser()}> - + title={chrome.i18n.getMessage("ChangeCategoryTooltip")} + style={{color: (this.state.actionState === SkipNoticeAction.CategoryVote && this.state.editing == true) ? this.selectedColor : this.unselectedColor}} + onClick={() => this.resetStateToStart(SkipNoticeAction.CategoryVote, true, true)}> {chrome.i18n.getMessage("incorrectCategory")} </button> </td> - </tr> ), /* Category Chooser Row */ - (this.state.choosingCategory && + (this.state.choosingCategory && !this.state.thanksForVotingText && <tr id={"sponsorSkipNoticeCategoryChooserRow" + this.idSuffix} key={3}> <td> {/* Category Selector */} <select id={"sponsorTimeCategories" + this.idSuffix} className="sponsorTimeCategories sponsorTimeEditSelector" - defaultValue={this.segments[0].category} //Just default to the first segment, as we don't know which they'll choose + defaultValue={this.segments[0].category} ref={this.categoryOptionRef}> {this.getCategoryOptions()} @@ -281,13 +330,12 @@ class SkipNoticeComponent extends React.Component<SkipNoticeProps, SkipNoticeSta {chrome.i18n.getMessage("submit")} </button> } - </td> </tr> ), /* Segment Chooser Row */ - (this.state.actionState !== SkipNoticeAction.None && + (this.state.actionState !== SkipNoticeAction.None && this.segments.length > 1 && !this.state.thanksForVotingText && <tr id={"sponsorSkipNoticeSubmissionOptionsRow" + this.idSuffix} key={4}> <td id={"sponsorTimesSubmissionOptionsContainer" + this.idSuffix}> @@ -305,10 +353,11 @@ class SkipNoticeComponent extends React.Component<SkipNoticeProps, SkipNoticeSta return ( <span className="sponsorSkipNoticeUnskipSection"> <button id={"sponsorSkipUnskipButton" + this.idSuffix} - className="sponsorSkipObject sponsorSkipNoticeButton" - style={{marginLeft: "4px"}} - onClick={() => this.prepAction(SkipNoticeAction.Unskip)}> - + className="sponsorSkipObject sponsorSkipNoticeButton" + style={{marginLeft: "4px", + color: (this.state.actionState === SkipNoticeAction.Unskip) ? this.selectedColor : this.unselectedColor + }} + onClick={() => this.prepAction(SkipNoticeAction.Unskip)}> {this.state.skipButtonText + (this.state.showKeybindHint ? " (" + Config.config.skipKeybind + ")" : "")} </button> </span> @@ -318,20 +367,40 @@ class SkipNoticeComponent extends React.Component<SkipNoticeProps, SkipNoticeSta getSubmissionChooser(): JSX.Element[] { const elements: JSX.Element[] = []; - for (let i = 0; i < this.segments.length; i++) { elements.push( <button className="sponsorSkipObject sponsorSkipNoticeButton" + style={{opacity: this.getSubmissionChooserOpacity(i), + color: this.getSubmissionChooserColor(i)}} onClick={() => this.performAction(i)} key={"submission" + i + this.segments[i].category + this.idSuffix}> {(i + 1) + ". " + chrome.i18n.getMessage("category_" + this.segments[i].category)} </button> ); } - return elements; } + getSubmissionChooserOpacity(index: number): number { + const isUpvote = this.state.actionState === SkipNoticeAction.Upvote; + const isDownvote = this.state.actionState == SkipNoticeAction.Downvote; + const isCopyDownvote = this.state.actionState == SkipNoticeAction.CopyDownvote; + const shouldBeGray: boolean = (isUpvote && this.state.voted[index] == SkipNoticeAction.Upvote) || + (isDownvote && this.state.voted[index] == SkipNoticeAction.Downvote) || + (isCopyDownvote && this.state.copied[index] == SkipNoticeAction.CopyDownvote); + + return shouldBeGray ? 0.35 : 1; + } + + getSubmissionChooserColor(index: number): string { + const isDownvote = this.state.actionState == SkipNoticeAction.Downvote; + const isCopyDownvote = this.state.actionState == SkipNoticeAction.CopyDownvote; + const shouldWarnUser = Config.config.isVip && (isDownvote || isCopyDownvote) + && this.segments[index].locked === 1; + + return shouldWarnUser ? this.lockedColor : this.unselectedColor; + } + onMouseEnter(): void { if (this.state.smaller) { this.setState({ @@ -340,16 +409,6 @@ class SkipNoticeComponent extends React.Component<SkipNoticeProps, SkipNoticeSta } } - prepAction(action: SkipNoticeAction): void { - if (this.segments.length === 1) { - this.performAction(0, action); - } else { - this.setState({ - actionState: action - }); - } - } - getMessageBoxes(): JSX.Element[] { if (this.state.messages.length === 0) { // Add a spacer if there is no text @@ -365,8 +424,8 @@ class SkipNoticeComponent extends React.Component<SkipNoticeProps, SkipNoticeSta for (let i = 0; i < this.state.messages.length; i++) { elements.push( - <tr> - <td> + <tr key={i + "_messageBox"}> + <td key={i + "_messageBox"}> <NoticeTextSelectionComponent idSuffix={this.idSuffix} text={this.state.messages[i]} onClick={this.state.messageOnClick} @@ -380,6 +439,33 @@ class SkipNoticeComponent extends React.Component<SkipNoticeProps, SkipNoticeSta return elements; } + prepAction(action: SkipNoticeAction): void { + if (this.segments.length === 1) { + this.performAction(0, action); + } else { + switch (action ?? this.state.actionState) { + case SkipNoticeAction.None: + this.resetStateToStart(); + break; + case SkipNoticeAction.Upvote: + this.resetStateToStart(SkipNoticeAction.Upvote); + break; + case SkipNoticeAction.Downvote: + this.resetStateToStart(SkipNoticeAction.Downvote); + break; + case SkipNoticeAction.CategoryVote: + this.resetStateToStart(SkipNoticeAction.CategoryVote, true, true); + break; + case SkipNoticeAction.CopyDownvote: + this.resetStateToStart(SkipNoticeAction.CopyDownvote, true); + break; + case SkipNoticeAction.Unskip: + this.resetStateToStart(SkipNoticeAction.Unskip); + break; + } + } + } + /** * Performs the action from the current state * @@ -388,74 +474,110 @@ class SkipNoticeComponent extends React.Component<SkipNoticeProps, SkipNoticeSta performAction(index: number, action?: SkipNoticeAction): void { switch (action ?? this.state.actionState) { case SkipNoticeAction.None: + this.noAction(index); break; case SkipNoticeAction.Upvote: - this.contentContainer().vote(1, this.segments[index].UUID, undefined, this); + this.upvote(index); break; case SkipNoticeAction.Downvote: - this.contentContainer().vote(0, this.segments[index].UUID, undefined, this); + this.downvote(index); break; case SkipNoticeAction.CategoryVote: - this.contentContainer().vote(undefined, this.segments[index].UUID, this.categoryOptionRef.current.value as Category, this) + this.categoryVote(index); + break; + case SkipNoticeAction.CopyDownvote: + this.copyDownvote(index); break; case SkipNoticeAction.Unskip: - this.state.skipButtonCallback(index); + this.unskipAction(index); + break; + default: + this.resetStateToStart(); break; } + } + + noAction(index: number): void { + const voted = this.state.voted; + voted[index] = SkipNoticeAction.None; this.setState({ - actionState: SkipNoticeAction.None + voted }); } - adjustDownvotingState(value: boolean): void { - if (!value) this.clearConfigListener(); + upvote(index: number): void { + if (this.segments.length === 1) this.resetStateToStart(); + this.contentContainer().vote(1, this.segments[index].UUID, undefined, this); + } - this.setState({ - downvoting: value, - choosingCategory: false - }); + downvote(index: number): void { + if (this.segments.length === 1) this.resetStateToStart(); + + this.contentContainer().vote(0, this.segments[index].UUID, undefined, this); } - clearConfigListener(): void { - if (this.configListener) { - Config.configListeners.splice(Config.configListeners.indexOf(this.configListener), 1); - this.configListener = null; - } + categoryVote(index: number): void { + this.contentContainer().vote(undefined, this.segments[index].UUID, this.categoryOptionRef.current.value as Category, this) } - openCategoryChooser(): void { - // Add as a config listener - this.configListener = () => this.forceUpdate(); - Config.configListeners.push(this.configListener); + 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(); + + this.contentContainer().vote(0, this.segments[index].UUID, undefined, this); + + const copied = this.state.copied; + copied[index] = SkipNoticeAction.CopyDownvote; this.setState({ - choosingCategory: true, - downvoting: false - }, () => { - if (this.segments.length > 1) { - // Use the action selectors as a submit button - this.prepAction(SkipNoticeAction.CategoryVote); - } + copied }); } + unskipAction(index: number): void { + this.state.skipButtonCallback(index); + } + + openEditingOptions(): void { + this.resetStateToStart(undefined, true); + } + getCategoryOptions(): React.ReactElement[] { const elements = []; - const categories = CompileConfig.categoryList.filter((cat => getCategoryActionType(cat as Category) === CategoryActionType.Skippable)); + const categories = (CompileConfig.categoryList.filter((cat => getCategoryActionType(cat as Category) === CategoryActionType.Skippable))) as Category[]; for (const category of categories) { elements.push( <option value={category} - key={category}> + key={category} + className={this.getCategoryNameClass(category)}> {chrome.i18n.getMessage("category_" + category)} </option> ); } - return elements; } + getCategoryNameClass(category: string): string { + return this.props.contentContainer().lockedCategories.includes(category) ? "sponsorBlockLockedColor" : "" + } + unskip(index: number): void { this.contentContainer().unskipSponsorTime(this.segments[index], this.props.unskipTime); @@ -512,21 +634,42 @@ class SkipNoticeComponent extends React.Component<SkipNoticeProps, SkipNoticeSta } afterVote(segment: SponsorTime, type: number, category: Category): void { - this.addVoteButtonInfo(chrome.i18n.getMessage("voted")); + const index = utils.getSponsorIndexFromUUID(this.segments, segment.UUID); + const wikiLinkText = CompileConfig.wikiLinks[segment.category]; + + const voted = this.state.voted; + switch (type) { + case 0: + this.clearConfigListener(); + this.setNoticeInfoMessageWithOnClick(() => window.open(wikiLinkText), chrome.i18n.getMessage("OpenCategoryWikiPage")); - if (type === 0) { - this.setNoticeInfoMessage(chrome.i18n.getMessage("hitGoBack")); - this.adjustDownvotingState(false); + voted[index] = SkipNoticeAction.Downvote; + break; + case 1: + voted[index] = SkipNoticeAction.Upvote; + break; + case 20: + voted[index] = SkipNoticeAction.None; + break; } - + + this.setState({ + voted + }); + + this.addVoteButtonInfo(chrome.i18n.getMessage("voted")); + // Change the sponsor locally if (segment) { if (type === 0) { segment.hidden = SponsorHideType.Downvoted; } else if (category) { - segment.category = category; + segment.category = category; // This is the actual segment on the video page + this.segments[index].category = category; //this is the segment inside the skip notice. + } else if (type === 1) { + segment.hidden = SponsorHideType.Visible; } - + this.contentContainer().updatePreviewBar(); } } @@ -562,6 +705,13 @@ class SkipNoticeComponent extends React.Component<SkipNoticeProps, SkipNoticeSta this.props.closeListener(); } + clearConfigListener(): void { + if (this.configListener) { + Config.configListeners.splice(Config.configListeners.indexOf(this.configListener), 1); + this.configListener = null; + } + } + unmutedListener(): void { if (this.props.segments.length === 1 && this.props.segments[0].actionType === ActionType.Mute @@ -572,6 +722,26 @@ class SkipNoticeComponent extends React.Component<SkipNoticeProps, SkipNoticeSta } } + resetStateToStart(actionState: SkipNoticeAction = SkipNoticeAction.None, editing = false, choosingCategory = false): void { + this.setState({ + actionState: actionState, + editing: editing, + choosingCategory: choosingCategory, + thanksForVotingText: null, + messages: [] + }); + } + + downvoteButtonColor(downvoteType: SkipNoticeAction): string { + // Also used for "Copy and Downvote" + if (this.segments.length > 1) { + return (this.state.actionState === downvoteType) ? this.selectedColor : this.unselectedColor; + } else { + // You dont have segment selectors so the lockbutton needs to be colored and cannot be selected. + return Config.config.isVip && this.segments[0].locked === 1 ? this.lockedColor : this.unselectedColor; + } + } + private getUnskipText(): string { switch (this.props.segments[0].actionType) { case ActionType.Mute: { diff --git a/src/components/SponsorTimeEditComponent.tsx b/src/components/SponsorTimeEditComponent.tsx index d16d4760..371228dd 100644 --- a/src/components/SponsorTimeEditComponent.tsx +++ b/src/components/SponsorTimeEditComponent.tsx @@ -24,6 +24,7 @@ export interface SponsorTimeEditProps { export interface SponsorTimeEditState { editing: boolean; sponsorTimeEdits: [string, string]; + selectedCategory: Category; } const DEFAULT_CATEGORY = "chooseACategory"; @@ -47,7 +48,8 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo this.state = { editing: false, - sponsorTimeEdits: [null, null] + sponsorTimeEdits: [null, null], + selectedCategory: DEFAULT_CATEGORY as Category }; } @@ -306,7 +308,8 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo for (const category of (this.props.categoryList ?? CompileConfig.categoryList)) { elements.push( <option value={category} - key={category}> + key={category} + className={this.getCategoryLockedClass(category)}> {chrome.i18n.getMessage("category_" + category)} </option> ); @@ -315,6 +318,10 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo return elements; } + getCategoryLockedClass(category: string): string { + return this.props.contentContainer().lockedCategories.includes(category) ? "sponsorBlockLockedColor" : ""; + } + categorySelectionChange(event: React.ChangeEvent<HTMLSelectElement>): void { // See if show more categories was pressed if (event.target.value !== DEFAULT_CATEGORY && !Config.config.categorySelections.some((category) => category.name === event.target.value)) { diff --git a/src/config.ts b/src/config.ts index 3e211671..51c1f06e 100644 --- a/src/config.ts +++ b/src/config.ts @@ -3,7 +3,9 @@ import { Category, CategorySelection, CategorySkipOption, NoticeVisbilityMode, P interface SBConfig { userID: string, - /** Contains unsubmitted segments that the user has created. */ + isVip: boolean, + lastIsVipUpdate: number, + /* Contains unsubmitted segments that the user has created. */ segmentTimes: SBMap<string, SponsorTime[]>, defaultCategory: Category, whitelistedChannels: string[], @@ -44,7 +46,12 @@ interface SBConfig { autoHideInfoButton: boolean, autoSkipOnMusicVideos: boolean, highlightCategoryUpdate: boolean, - scrollToEditTimeUpdate: boolean + colorPalette: { + red: string, + white: string, + locked: string + }, + scrollToEditTimeUpdate: boolean, // What categories should be skipped categorySelections: CategorySelection[], @@ -152,6 +159,8 @@ const Config: SBObject = { configListeners: [], defaults: { userID: null, + isVip: false, + lastIsVipUpdate: 0, segmentTimes: new SBMap("segmentTimes"), defaultCategory: "chooseACategory" as Category, whitelistedChannels: [], @@ -199,6 +208,12 @@ const Config: SBObject = { option: CategorySkipOption.AutoSkip }], + colorPalette: { + red: "#780303", + white: "#ffffff", + locked: "#ffc83d" + }, + // Preview bar barTypes: { "preview-chooseACategory": { diff --git a/src/content.ts b/src/content.ts index 4b480ddd..7eeed429 100644 --- a/src/content.ts +++ b/src/content.ts @@ -34,8 +34,10 @@ let lastPOISkip = 0; // JSON video info let videoInfo: VideoInfo = null; -//the channel this video is about +// The channel this video is about let channelIDInfo: ChannelIDInfo; +// Locked Categories in this tab, like: ["sponsor","intro","outro"] +let lockedCategories: Category[] = []; // Skips are scheduled to ensure precision. // Skips are rescheduled every seeking event. @@ -121,7 +123,8 @@ const skipNoticeContentContainer: ContentContainer = () => ({ updateEditButtonsOnPlayer, previewTime, videoInfo, - getRealCurrentTime: getRealCurrentTime + getRealCurrentTime: getRealCurrentTime, + lockedCategories }); // value determining when to count segment as skipped and send telemetry to server (percent based) @@ -231,6 +234,7 @@ function resetValues() { status: ChannelIDStatus.Fetching, id: null }; + lockedCategories = []; //empty the preview bar if (previewBar !== null) { @@ -752,6 +756,55 @@ async function sponsorsLookup(id: string, keepOldSubmissions = true) { sponsorLookupRetries++; } + + lookupVipInformation(id); +} + +function lookupVipInformation(id: string): void { + updateVipInfo().then((isVip) => { + if (isVip) { + lockedCategoriesLookup(id); + } + }); +} + +async function updateVipInfo(): Promise<boolean> { + const currentTime = Date.now(); + const lastUpdate = Config.config.lastIsVipUpdate; + if (currentTime - lastUpdate > 1000 * 60 * 60 * 72) { // 72 hours + Config.config.lastIsVipUpdate = currentTime; + + const response = await utils.asyncRequestToServer("GET", "/api/isUserVIP", { userID: Config.config.userID}); + + if (response.ok) { + let isVip = false; + try { + const vipResponse = JSON.parse(response.responseText)?.vip; + if (typeof(vipResponse) === "boolean") { + isVip = vipResponse; + } + } catch (e) { } //eslint-disable-line no-empty + + Config.config.isVip = isVip; + return isVip; + } + } + + return Config.config.isVip; +} + +async function lockedCategoriesLookup(id: string): Promise<void> { + const hashPrefix = (await utils.getHash(id, 1)).substr(0, 4); + const response = await utils.asyncRequestToServer("GET", "/api/lockCategories/" + hashPrefix); + + if (response.ok) { + try { + const categoriesResponse = JSON.parse(response.responseText).filter((lockInfo) => lockInfo.videoID === id)[0]?.categories; + if (Array.isArray(categoriesResponse)) { + lockedCategories = categoriesResponse; + } + } catch (e) { } //eslint-disable-line no-empty + } } function retryFetch(): void { @@ -1683,8 +1736,12 @@ function resetSponsorSubmissionNotice() { } function submitSponsorTimes() { - if (submissionNotice !== null) return; - + if (submissionNotice !== null){ + submissionNotice.close(); + submissionNotice = null; + return; + } + if (sponsorTimesSubmitting !== undefined && sponsorTimesSubmitting.length > 0) { submissionNotice = new SubmissionNotice(skipNoticeContentContainer, sendSubmitMessage); } diff --git a/src/popup.ts b/src/popup.ts index 680e0779..5a75c8ba 100644 --- a/src/popup.ts +++ b/src/popup.ts @@ -379,8 +379,10 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> { container.removeChild(container.firstChild); } + const isVip = Config.config.isVip; for (let i = 0; i < segmentTimes.length; i++) { const UUID = segmentTimes[i].UUID; + const locked = segmentTimes[i].locked; const sponsorTimeButton = document.createElement("button"); sponsorTimeButton.className = "segmentTimeButton popupElement"; @@ -430,7 +432,7 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> { const downvoteButton = document.createElement("img"); downvoteButton.id = "sponsorTimesDownvoteButtonsContainer" + UUID; downvoteButton.className = "voteButton"; - downvoteButton.src = chrome.runtime.getURL("icons/thumbs_down.svg"); + downvoteButton.src = locked && isVip ? chrome.runtime.getURL("icons/thumbs_down_locked.svg") : chrome.runtime.getURL("icons/thumbs_down.svg"); downvoteButton.addEventListener("click", () => vote(0, UUID)); //uuid button diff --git a/src/svg-icons/pencil_svg.tsx b/src/svg-icons/pencil_svg.tsx new file mode 100644 index 00000000..3ddb81c2 --- /dev/null +++ b/src/svg-icons/pencil_svg.tsx @@ -0,0 +1,18 @@ +import * as React from "react"; + +const pencilSvg = ({ + fill = "#ffffff" + }): JSX.Element => ( + <svg + xmlns="http://www.w3.org/2000/svg" + width="18" + height="18" + viewBox="0 0 24 24" + fill={fill} + > + <path + d="M14.1 7.1l2.9 2.9L6.1 20.7l-3.6.7.7-3.6L14.1 7.1zm0-2.8L1.4 16.9 0 24l7.1-1.4L19.8 9.9l-5.7-5.7zm7.1 4.3L24 5.7 18.3 0l-2.8 2.8 5.7 5.7z"></path> + </svg> + ); + +export default pencilSvg; diff --git a/src/svg-icons/thumbs_down_svg.tsx b/src/svg-icons/thumbs_down_svg.tsx new file mode 100644 index 00000000..ce61db5a --- /dev/null +++ b/src/svg-icons/thumbs_down_svg.tsx @@ -0,0 +1,23 @@ +import * as React from "react"; + +const thumbsDownSvg = ({ + fill = "#ffffff" + }): JSX.Element => ( + <svg + xmlns="http://www.w3.org/2000/svg" + width="18" + height="18" + fill={fill} + viewBox="0 0 24 24" + > + <path + fill="none" + d="M0 0h24v24H0z"> + </path> + <path + d="M15 3H6c-.83 0-1.54.5-1.84 1.22l-3.02 7.05c-.09.23-.14.47-.14.73v2c0 1.1.9 2 2 2h6.31l-.95 4.57-.03.32c0 .41.17.79.44 1.06L9.83 23l6.59-6.59c.36-.36.58-.86.58-1.41V5c0-1.1-.9-2-2-2zm4 0v12h4V3h-4z" + ></path> + </svg> + ); + +export default thumbsDownSvg; diff --git a/src/svg-icons/thumbs_up_svg.tsx b/src/svg-icons/thumbs_up_svg.tsx new file mode 100644 index 00000000..10c95d94 --- /dev/null +++ b/src/svg-icons/thumbs_up_svg.tsx @@ -0,0 +1,22 @@ +import * as React from "react"; + +const thumbsUpSvg = ({ + fill = "#ffffff" + }): JSX.Element => ( + <svg + xmlns="http://www.w3.org/2000/svg" + width="18" + height="18" + fill={fill} + viewBox="0 0 24 24" + > + <path + fill="none" + d="M0 0h24v24H0V0z"></path> + <path + d="M1 21h4V9H1v12zm22-11c0-1.1-.9-2-2-2h-6.31l.95-4.57.03-.32c0-.41-.17-.79-.44-1.06L14.17 1 7.59 7.59C7.22 7.95 7 8.45 7 9v10c0 1.1.9 2 2 2h9c.83 0 1.54-.5 1.84-1.22l3.02-7.05c.09-.23.14-.47.14-.73v-2z" + ></path> + </svg> + ); + +export default thumbsUpSvg; diff --git a/src/types.ts b/src/types.ts index c60e52bb..1caf257c 100644 --- a/src/types.ts +++ b/src/types.ts @@ -20,7 +20,8 @@ export interface ContentContainer { updateEditButtonsOnPlayer: () => void, previewTime: (time: number, unpause?: boolean) => void, videoInfo: VideoInfo, - getRealCurrentTime: () => number + getRealCurrentTime: () => number, + lockedCategories: string[] } } @@ -74,6 +75,7 @@ export enum SponsorSourceType { export interface SponsorTime { segment: [number] | [number, number]; UUID: SegmentUUID; + locked?: number; category: Category; actionType: ActionType; diff --git a/src/utils.ts b/src/utils.ts index 1fe7b286..99d61137 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -539,5 +539,4 @@ export default class Utils { return hashHex; } - } |