aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorAjay Ramachandran <[email protected]>2021-10-15 19:00:38 -0400
committerGitHub <[email protected]>2021-10-15 19:00:38 -0400
commite9204be96fff35c466a0d0c154d54c025df7ecb7 (patch)
treee747f550b10463b8b8d1690017f7413da8d0ae37
parentb8ab05ccad16604bd02c30d0e0ba107bcbe9f18e (diff)
parent08630616655d91a40b864d9997f0f5975d0e9b6a (diff)
downloadSponsorBlock-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.example12
-rw-r--r--manifest/manifest.json1
-rw-r--r--public/_locales/en/messages.json17
-rw-r--r--public/content.css16
-rw-r--r--public/icons/thumbs_down_locked.svg58
-rw-r--r--src/components/SkipNoticeComponent.tsx354
-rw-r--r--src/components/SponsorTimeEditComponent.tsx11
-rw-r--r--src/config.ts19
-rw-r--r--src/content.ts65
-rw-r--r--src/popup.ts4
-rw-r--r--src/svg-icons/pencil_svg.tsx18
-rw-r--r--src/svg-icons/thumbs_down_svg.tsx23
-rw-r--r--src/svg-icons/thumbs_up_svg.tsx22
-rw-r--r--src/types.ts4
-rw-r--r--src/utils.ts1
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;
}
-
}