aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorAjay <[email protected]>2022-01-05 20:49:56 -0500
committerAjay <[email protected]>2022-01-05 20:49:56 -0500
commit8e964b40b308c966a53fa3590499827bb1492ab4 (patch)
tree578aff18844a3a067e84336b544ec2799f6f6e35
parenta6a9b7dd8c7a88654909c0ee6a3218738d343504 (diff)
downloadSponsorBlock-8e964b40b308c966a53fa3590499827bb1492ab4.tar.gz
SponsorBlock-8e964b40b308c966a53fa3590499827bb1492ab4.zip
Add vote buttons to pill that open on click
-rw-r--r--public/content.css1
-rw-r--r--src/components/CategoryPillComponent.tsx51
-rw-r--r--src/components/SkipNoticeComponent.tsx25
-rw-r--r--src/content.ts73
-rw-r--r--src/js-components/skipButtonControlBar.ts9
-rw-r--r--src/messageTypes.ts5
-rw-r--r--src/popup.ts10
-rw-r--r--src/render/CategoryPill.tsx29
-rw-r--r--src/utils.ts92
-rw-r--r--src/utils/animationUtils.ts78
-rw-r--r--src/utils/genericUtils.ts26
-rw-r--r--src/utils/noticeUtils.ts21
12 files changed, 249 insertions, 171 deletions
diff --git a/public/content.css b/public/content.css
index aabc6790..a8730b1d 100644
--- a/public/content.css
+++ b/public/content.css
@@ -622,6 +622,7 @@ input::-webkit-inner-spin-button {
cursor: pointer;
font-size: 75%;
height: 100%;
+ align-items: center;
}
.sponsorBlockCategoryPillTitleSection {
diff --git a/src/components/CategoryPillComponent.tsx b/src/components/CategoryPillComponent.tsx
index 1ec12695..7b760b1f 100644
--- a/src/components/CategoryPillComponent.tsx
+++ b/src/components/CategoryPillComponent.tsx
@@ -1,9 +1,16 @@
import * as React from "react";
import Config from "../config";
-import { SponsorTime } from "../types";
+import { Category, SegmentUUID, SponsorTime } from "../types";
-export interface CategoryPillProps {
+import ThumbsUpSvg from "../svg-icons/thumbs_up_svg";
+import ThumbsDownSvg from "../svg-icons/thumbs_down_svg";
+import { downvoteButtonColor, SkipNoticeAction } from "../utils/noticeUtils";
+import { VoteResponse } from "../messageTypes";
+import { AnimationUtils } from "../utils/animationUtils";
+import { GenericUtils } from "../utils/genericUtils";
+export interface CategoryPillProps {
+ vote: (type: number, UUID: SegmentUUID, category?: Category) => Promise<VoteResponse>;
}
export interface CategoryPillState {
@@ -32,7 +39,8 @@ class CategoryPillComponent extends React.Component<CategoryPillProps, CategoryP
return (
<span style={style}
- className={"sponsorBlockCategoryPill"} >
+ className={"sponsorBlockCategoryPill"}
+ onClick={() => this.state.show && this.setState({ open: !this.state.open })}>
<span className="sponsorBlockCategoryPillTitleSection">
<img className="sponsorSkipLogo sponsorSkipObject"
src={chrome.extension.getURL("icons/IconSponsorBlocker256px.png")}>
@@ -41,9 +49,46 @@ class CategoryPillComponent extends React.Component<CategoryPillProps, CategoryP
{chrome.i18n.getMessage("category_" + this.state.segment?.category)}
</span>
</span>
+
+ {this.state.open && (
+ <>
+ {/* Upvote Button */}
+ <div id={"sponsorTimesDownvoteButtonsContainerUpvoteCategoryPill"}
+ className="voteButton"
+ style={{marginLeft: "5px"}}
+ title={chrome.i18n.getMessage("upvoteButtonInfo")}
+ onClick={(event) => this.vote(event, 1)}>
+ <ThumbsUpSvg fill={Config.config.colorPalette.white} />
+ </div>
+
+ {/* Downvote Button */}
+ <div id={"sponsorTimesDownvoteButtonsContainerDownvoteCategoryPill"}
+ className="voteButton"
+ title={chrome.i18n.getMessage("reportButtonInfo")}
+ onClick={(event) => this.vote(event, 0)}>
+ <ThumbsDownSvg fill={downvoteButtonColor(null, null, SkipNoticeAction.Downvote)} />
+ </div>
+ </>
+ )}
</span>
);
}
+
+ private async vote(event: React.MouseEvent, type: number): Promise<void> {
+ event.stopPropagation();
+ if (this.state.segment) {
+ const stopAnimation = AnimationUtils.applyLoadingAnimation(event.currentTarget as HTMLElement, 0.3);
+
+ const response = await this.props.vote(type, this.state.segment.UUID);
+ await stopAnimation();
+
+ if (response.successType == 1 || (response.successType == -1 && response.statusCode == 429)) {
+ this.setState({ open: false });
+ } else if (response.statusCode !== 403) {
+ alert(GenericUtils.getErrorMessage(response.statusCode, response.responseText));
+ }
+ }
+ }
}
export default CategoryPillComponent;
diff --git a/src/components/SkipNoticeComponent.tsx b/src/components/SkipNoticeComponent.tsx
index da771853..49dac9ab 100644
--- a/src/components/SkipNoticeComponent.tsx
+++ b/src/components/SkipNoticeComponent.tsx
@@ -4,7 +4,6 @@ import Config from "../config"
import { Category, ContentContainer, CategoryActionType, SponsorHideType, SponsorTime, NoticeVisbilityMode, ActionType, SponsorSourceType, SegmentUUID } from "../types";
import NoticeComponent from "./NoticeComponent";
import NoticeTextSelectionComponent from "./NoticeTextSectionComponent";
-import SubmissionNotice from "../render/SubmissionNotice";
import Utils from "../utils";
const utils = new Utils();
@@ -13,15 +12,7 @@ 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
-}
+import { downvoteButtonColor, SkipNoticeAction } from "../utils/noticeUtils";
export interface SkipNoticeProps {
segments: SponsorTime[];
@@ -216,7 +207,7 @@ class SkipNoticeComponent extends React.Component<SkipNoticeProps, SkipNoticeSta
style={{marginRight: "5px", marginLeft: "5px"}}
title={chrome.i18n.getMessage("reportButtonInfo")}
onClick={() => this.prepAction(SkipNoticeAction.Downvote)}>
- <ThumbsDownSvg fill={this.downvoteButtonColor(SkipNoticeAction.Downvote)} />
+ <ThumbsDownSvg fill={downvoteButtonColor(this.segments, this.state.actionState, SkipNoticeAction.Downvote)} />
</div>
{/* Copy and Downvote Button */}
@@ -279,7 +270,7 @@ class SkipNoticeComponent extends React.Component<SkipNoticeProps, SkipNoticeSta
{/* Copy Segment */}
<button className="sponsorSkipObject sponsorSkipNoticeButton"
title={chrome.i18n.getMessage("CopyDownvoteButtonInfo")}
- style={{color: this.downvoteButtonColor(SkipNoticeAction.Downvote)}}
+ style={{color: downvoteButtonColor(this.segments, this.state.actionState, SkipNoticeAction.Downvote)}}
onClick={() => this.prepAction(SkipNoticeAction.CopyDownvote)}>
{chrome.i18n.getMessage("CopyAndDownvote")}
</button>
@@ -727,16 +718,6 @@ class SkipNoticeComponent extends React.Component<SkipNoticeProps, SkipNoticeSta
});
}
- 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/content.ts b/src/content.ts
index f9e3172a..07bf607c 100644
--- a/src/content.ts
+++ b/src/content.ts
@@ -11,7 +11,7 @@ import PreviewBar, {PreviewBarSegment} from "./js-components/previewBar";
import SkipNotice from "./render/SkipNotice";
import SkipNoticeComponent from "./components/SkipNoticeComponent";
import SubmissionNotice from "./render/SubmissionNotice";
-import { Message, MessageResponse } from "./messageTypes";
+import { Message, MessageResponse, VoteResponse } from "./messageTypes";
import * as Chat from "./js-components/chat";
import { getCategoryActionType } from "./utils/categoryUtils";
import { SkipButtonControlBar } from "./js-components/skipButtonControlBar";
@@ -19,6 +19,8 @@ import { Tooltip } from "./render/Tooltip";
import { getStartTimeFromUrl } from "./utils/urlParser";
import { getControls } from "./utils/pageUtils";
import { CategoryPill } from "./render/CategoryPill";
+import { AnimationUtils } from "./utils/animationUtils";
+import { GenericUtils } from "./utils/genericUtils";
// Hack to get the CSS loaded on permission-based sites (Invidious)
utils.wait(() => Config.config !== null, 5000, 10).then(addCSS);
@@ -647,7 +649,7 @@ function setupCategoryPill() {
categoryPill = new CategoryPill();
}
- categoryPill.attachToPage(onMobileYouTube, onInvidious);
+ categoryPill.attachToPage(onMobileYouTube, onInvidious, voteAsync);
}
async function sponsorsLookup(id: string, keepOldSubmissions = true) {
@@ -1369,7 +1371,7 @@ async function createButtons(): Promise<void> {
&& playerButtons["info"]?.button && !controlsWithEventListeners.includes(controlsContainer)) {
controlsWithEventListeners.push(controlsContainer);
- utils.setupAutoHideAnimation(playerButtons["info"].button, controlsContainer);
+ AnimationUtils.setupAutoHideAnimation(playerButtons["info"].button, controlsContainer);
}
}
@@ -1649,13 +1651,37 @@ function clearSponsorTimes() {
}
//if skipNotice is null, it will not affect the UI
-function vote(type: number, UUID: SegmentUUID, category?: Category, skipNotice?: SkipNoticeComponent) {
+async function vote(type: number, UUID: SegmentUUID, category?: Category, skipNotice?: SkipNoticeComponent): Promise<void> {
if (skipNotice !== null && skipNotice !== undefined) {
//add loading info
skipNotice.addVoteButtonInfo.bind(skipNotice)(chrome.i18n.getMessage("Loading"))
skipNotice.setNoticeInfoMessage.bind(skipNotice)();
}
+ const response = await voteAsync(type, UUID, category);
+ if (response != undefined) {
+ //see if it was a success or failure
+ if (skipNotice != null) {
+ if (response.successType == 1 || (response.successType == -1 && response.statusCode == 429)) {
+ //success (treat rate limits as a success)
+ skipNotice.afterVote.bind(skipNotice)(utils.getSponsorTimeFromUUID(sponsorTimes, UUID), type, category);
+ } else if (response.successType == -1) {
+ if (response.statusCode === 403 && response.responseText.startsWith("Vote rejected due to a warning from a moderator.")) {
+ skipNotice.setNoticeInfoMessageWithOnClick.bind(skipNotice)(() => {
+ Chat.openWarningChat(response.responseText);
+ skipNotice.closeListener.call(skipNotice);
+ }, chrome.i18n.getMessage("voteRejectedWarning"));
+ } else {
+ skipNotice.setNoticeInfoMessage.bind(skipNotice)(GenericUtils.getErrorMessage(response.statusCode, response.responseText))
+ }
+
+ skipNotice.resetVoteButtonInfo.bind(skipNotice)();
+ }
+ }
+ }
+}
+
+async function voteAsync(type: number, UUID: SegmentUUID, category?: Category): Promise<VoteResponse> {
const sponsorIndex = utils.getSponsorIndexFromUUID(sponsorTimes, UUID);
// Don't vote for preview sponsors
@@ -1675,33 +1701,14 @@ function vote(type: number, UUID: SegmentUUID, category?: Category, skipNotice?:
Config.config.skipCount = Config.config.skipCount + factor;
}
-
- chrome.runtime.sendMessage({
- message: "submitVote",
- type: type,
- UUID: UUID,
- category: category
- }, function(response) {
- if (response != undefined) {
- //see if it was a success or failure
- if (skipNotice != null) {
- if (response.successType == 1 || (response.successType == -1 && response.statusCode == 429)) {
- //success (treat rate limits as a success)
- skipNotice.afterVote.bind(skipNotice)(utils.getSponsorTimeFromUUID(sponsorTimes, UUID), type, category);
- } else if (response.successType == -1) {
- if (response.statusCode === 403 && response.responseText.startsWith("Vote rejected due to a warning from a moderator.")) {
- skipNotice.setNoticeInfoMessageWithOnClick.bind(skipNotice)(() => {
- Chat.openWarningChat(response.responseText);
- skipNotice.closeListener.call(skipNotice);
- }, chrome.i18n.getMessage("voteRejectedWarning"));
- } else {
- skipNotice.setNoticeInfoMessage.bind(skipNotice)(utils.getErrorMessage(response.statusCode, response.responseText))
- }
-
- skipNotice.resetVoteButtonInfo.bind(skipNotice)();
- }
- }
- }
+
+ return new Promise((resolve) => {
+ chrome.runtime.sendMessage({
+ message: "submitVote",
+ type: type,
+ UUID: UUID,
+ category: category
+ }, resolve);
});
}
@@ -1744,7 +1751,7 @@ function submitSponsorTimes() {
async function sendSubmitMessage() {
// Add loading animation
playerButtons.submit.image.src = chrome.extension.getURL("icons/PlayerUploadIconSponsorBlocker.svg");
- const stopAnimation = utils.applyLoadingAnimation(playerButtons.submit.button, 1, () => updateEditButtonsOnPlayer());
+ const stopAnimation = AnimationUtils.applyLoadingAnimation(playerButtons.submit.button, 1, () => updateEditButtonsOnPlayer());
//check if a sponsor exceeds the duration of the video
for (let i = 0; i < sponsorTimesSubmitting.length; i++) {
@@ -1816,7 +1823,7 @@ async function sendSubmitMessage() {
if (response.status === 403 && response.responseText.startsWith("Submission rejected due to a warning from a moderator.")) {
Chat.openWarningChat(response.responseText);
} else {
- alert(utils.getErrorMessage(response.status, response.responseText));
+ alert(GenericUtils.getErrorMessage(response.status, response.responseText));
}
}
}
diff --git a/src/js-components/skipButtonControlBar.ts b/src/js-components/skipButtonControlBar.ts
index 32018307..a27eefd0 100644
--- a/src/js-components/skipButtonControlBar.ts
+++ b/src/js-components/skipButtonControlBar.ts
@@ -3,6 +3,7 @@ import { SponsorTime } from "../types";
import { getSkippingText } from "../utils/categoryUtils";
import Utils from "../utils";
+import { AnimationUtils } from "../utils/animationUtils";
const utils = new Utils();
export interface SkipButtonControlBarProps {
@@ -80,9 +81,9 @@ export class SkipButtonControlBar {
}
if (!this.onMobileYouTube) {
- utils.setupAutoHideAnimation(this.skipIcon, mountingContainer, false, false);
+ AnimationUtils.setupAutoHideAnimation(this.skipIcon, mountingContainer, false, false);
} else {
- const { hide, show } = utils.setupCustomHideAnimation(this.skipIcon, mountingContainer, false, false);
+ const { hide, show } = AnimationUtils.setupCustomHideAnimation(this.skipIcon, mountingContainer, false, false);
this.hideButton = hide;
this.showButton = show;
}
@@ -104,7 +105,7 @@ export class SkipButtonControlBar {
this.refreshText();
this.textContainer?.classList?.remove("hidden");
- utils.disableAutoHideAnimation(this.skipIcon);
+ AnimationUtils.disableAutoHideAnimation(this.skipIcon);
this.startTimer();
}
@@ -160,7 +161,7 @@ export class SkipButtonControlBar {
this.getChapterPrefix()?.classList?.add("hidden");
- utils.enableAutoHideAnimation(this.skipIcon);
+ AnimationUtils.enableAutoHideAnimation(this.skipIcon);
if (this.onMobileYouTube) {
this.hideButton();
}
diff --git a/src/messageTypes.ts b/src/messageTypes.ts
index 4989c741..1b2949ea 100644
--- a/src/messageTypes.ts
+++ b/src/messageTypes.ts
@@ -61,3 +61,8 @@ export type MessageResponse =
| IsChannelWhitelistedResponse
| Record<string, never>;
+export interface VoteResponse {
+ successType: number;
+ statusCode: number;
+ responseText: string;
+} \ No newline at end of file
diff --git a/src/popup.ts b/src/popup.ts
index 8b5366f3..4d1d6743 100644
--- a/src/popup.ts
+++ b/src/popup.ts
@@ -5,6 +5,8 @@ import { SponsorTime, SponsorHideType, CategoryActionType, ActionType } from "./
import { Message, MessageResponse, IsInfoFoundMessageResponse } from "./messageTypes";
import { showDonationLink } from "./utils/configUtils";
import { getCategoryActionType } from "./utils/categoryUtils";
+import { AnimationUtils } from "./utils/animationUtils";
+import { GenericUtils } from "./utils/genericUtils";
const utils = new Utils();
interface MessageListener {
@@ -449,7 +451,7 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
uuidButton.src = chrome.runtime.getURL("icons/clipboard.svg");
uuidButton.addEventListener("click", () => {
navigator.clipboard.writeText(UUID);
- const stopAnimation = utils.applyLoadingAnimation(uuidButton, 0.3);
+ const stopAnimation = AnimationUtils.applyLoadingAnimation(uuidButton, 0.3);
stopAnimation();
});
@@ -555,7 +557,7 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
PageElements.sponsorTimesContributionsContainer.classList.remove("hidden");
} else {
- PageElements.setUsernameStatus.innerText = utils.getErrorMessage(response.status, response.responseText);
+ PageElements.setUsernameStatus.innerText = GenericUtils.getErrorMessage(response.status, response.responseText);
}
});
@@ -596,7 +598,7 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
//success (treat rate limits as a success)
addVoteMessage(chrome.i18n.getMessage("voted"), UUID);
} else if (response.successType == -1) {
- addVoteMessage(utils.getErrorMessage(response.statusCode, response.responseText), UUID);
+ addVoteMessage(GenericUtils.getErrorMessage(response.statusCode, response.responseText), UUID);
}
}
});
@@ -699,7 +701,7 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
}
function refreshSegments() {
- const stopAnimation = utils.applyLoadingAnimation(PageElements.refreshSegmentsButton, 0.3);
+ const stopAnimation = AnimationUtils.applyLoadingAnimation(PageElements.refreshSegmentsButton, 0.3);
messageHandler.query({
active: true,
diff --git a/src/render/CategoryPill.tsx b/src/render/CategoryPill.tsx
index 4681a813..d3530c38 100644
--- a/src/render/CategoryPill.tsx
+++ b/src/render/CategoryPill.tsx
@@ -1,7 +1,8 @@
import * as React from "react";
import * as ReactDOM from "react-dom";
import CategoryPillComponent, { CategoryPillState } from "../components/CategoryPillComponent";
-import { SponsorTime } from "../types";
+import { VoteResponse } from "../messageTypes";
+import { Category, SegmentUUID, SponsorTime } from "../types";
import { GenericUtils } from "../utils/genericUtils";
export class CategoryPill {
@@ -16,7 +17,8 @@ export class CategoryPill {
this.ref = React.createRef();
}
- async attachToPage(onMobileYouTube: boolean, onInvidious: boolean): Promise<void> {
+ async attachToPage(onMobileYouTube: boolean, onInvidious: boolean,
+ vote: (type: number, UUID: SegmentUUID, category?: Category) => Promise<VoteResponse>): Promise<void> {
const referenceNode =
await GenericUtils.wait(() =>
// YouTube, Mobile YouTube, Invidious
@@ -35,7 +37,7 @@ export class CategoryPill {
}
ReactDOM.render(
- <CategoryPillComponent ref={this.ref} />,
+ <CategoryPillComponent ref={this.ref} vote={vote} />,
this.container
);
@@ -49,7 +51,7 @@ export class CategoryPill {
this.mutationObserver.disconnect();
}
- this.mutationObserver = new MutationObserver(() => this.attachToPage(onMobileYouTube, onInvidious));
+ this.mutationObserver = new MutationObserver(() => this.attachToPage(onMobileYouTube, onInvidious, vote));
this.mutationObserver.observe(referenceNode, {
childList: true,
@@ -78,15 +80,18 @@ export class CategoryPill {
}
setSegment(segment: SponsorTime): void {
- const newState = {
- segment,
- show: true
- };
+ if (this.ref.current?.state?.segment !== segment) {
+ const newState = {
+ segment,
+ show: true,
+ open: false
+ };
- if (this.ref.current) {
- this.ref.current?.setState(newState);
- } else {
- this.unsavedState = newState;
+ if (this.ref.current) {
+ this.ref.current?.setState(newState);
+ } else {
+ this.unsavedState = newState;
+ }
}
}
diff --git a/src/utils.ts b/src/utils.ts
index 7cffa45a..760b2d65 100644
--- a/src/utils.ts
+++ b/src/utils.ts
@@ -144,75 +144,6 @@ export default class Utils {
}
/**
- * Starts a spinning animation and returns a function to be called when it should be stopped
- * The callback will be called when the animation is finished
- * It waits until a full rotation is complete
- */
- applyLoadingAnimation(element: HTMLElement, time: number, callback?: () => void): () => void {
- element.style.animation = `rotate ${time}s 0s infinite`;
-
- return () => {
- // Make the animation finite
- element.style.animation = `rotate ${time}s`;
-
- // When the animation is over, hide the button
- const animationEndListener = () => {
- if (callback) callback();
-
- element.style.animation = "none";
-
- element.removeEventListener("animationend", animationEndListener);
- };
-
- element.addEventListener("animationend", animationEndListener);
- }
- }
-
- setupCustomHideAnimation(element: Element, container: Element, enabled = true, rightSlide = true): { hide: () => void, show: () => void } {
- if (enabled) element.classList.add("autoHiding");
- element.classList.add("hidden");
- element.classList.add("animationDone");
- if (!rightSlide) element.classList.add("autoHideLeft");
-
- let mouseEntered = false;
-
- return {
- hide: () => {
- mouseEntered = false;
- if (element.classList.contains("autoHiding")) {
- element.classList.add("hidden");
- }
- },
- show: () => {
- mouseEntered = true;
- element.classList.remove("animationDone");
-
- // Wait for next event loop
- setTimeout(() => {
- if (mouseEntered) element.classList.remove("hidden")
- }, 10);
- }
- };
- }
-
- setupAutoHideAnimation(element: Element, container: Element, enabled = true, rightSlide = true): void {
- const { hide, show } = this.setupCustomHideAnimation(element, container, enabled, rightSlide);
-
- container.addEventListener("mouseleave", () => hide());
- container.addEventListener("mouseenter", () => show());
- }
-
- enableAutoHideAnimation(element: Element): void {
- element.classList.add("autoHiding");
- element.classList.add("hidden");
- }
-
- disableAutoHideAnimation(element: Element): void {
- element.classList.remove("autoHiding");
- element.classList.remove("hidden");
- }
-
- /**
* Merges any overlapping timestamp ranges into single segments and returns them as a new array.
*/
getMergedTimestamps(timestamps: number[][]): [number, number][] {
@@ -344,29 +275,6 @@ export default class Utils {
}
/**
- * Gets the error message in a nice string
- *
- * @param {int} statusCode
- * @returns {string} errorMessage
- */
- getErrorMessage(statusCode: number, responseText: string): string {
- let errorMessage = "";
- const postFix = (responseText ? "\n\n" + responseText : "");
-
- if([400, 429, 409, 502, 503, 0].includes(statusCode)) {
- //treat them the same
- if (statusCode == 503) statusCode = 502;
-
- errorMessage = chrome.i18n.getMessage(statusCode + "") + " " + chrome.i18n.getMessage("errorCode") + statusCode
- + "\n\n" + chrome.i18n.getMessage("statusReminder");
- } else {
- errorMessage = chrome.i18n.getMessage("connectionError") + statusCode;
- }
-
- return errorMessage + postFix;
- }
-
- /**
* Sends a request to a custom server
*
* @param type The request type. "GET", "POST", etc.
diff --git a/src/utils/animationUtils.ts b/src/utils/animationUtils.ts
new file mode 100644
index 00000000..933e6446
--- /dev/null
+++ b/src/utils/animationUtils.ts
@@ -0,0 +1,78 @@
+ /**
+ * Starts a spinning animation and returns a function to be called when it should be stopped
+ * The callback will be called when the animation is finished
+ * It waits until a full rotation is complete
+ */
+function applyLoadingAnimation(element: HTMLElement, time: number, callback?: () => void): () => Promise<void> {
+ element.style.animation = `rotate ${time}s 0s infinite`;
+
+ return async () => new Promise((resolve) => {
+ // Make the animation finite
+ element.style.animation = `rotate ${time}s`;
+
+ // When the animation is over, hide the button
+ const animationEndListener = () => {
+ if (callback) callback();
+
+ element.style.animation = "none";
+
+ element.removeEventListener("animationend", animationEndListener);
+
+ resolve();
+ };
+
+ element.addEventListener("animationend", animationEndListener);
+ });
+}
+
+function setupCustomHideAnimation(element: Element, container: Element, enabled = true, rightSlide = true): { hide: () => void, show: () => void } {
+ if (enabled) element.classList.add("autoHiding");
+ element.classList.add("hidden");
+ element.classList.add("animationDone");
+ if (!rightSlide) element.classList.add("autoHideLeft");
+
+ let mouseEntered = false;
+
+ return {
+ hide: () => {
+ mouseEntered = false;
+ if (element.classList.contains("autoHiding")) {
+ element.classList.add("hidden");
+ }
+ },
+ show: () => {
+ mouseEntered = true;
+ element.classList.remove("animationDone");
+
+ // Wait for next event loop
+ setTimeout(() => {
+ if (mouseEntered) element.classList.remove("hidden")
+ }, 10);
+ }
+ };
+}
+
+function setupAutoHideAnimation(element: Element, container: Element, enabled = true, rightSlide = true): void {
+ const { hide, show } = this.setupCustomHideAnimation(element, container, enabled, rightSlide);
+
+ container.addEventListener("mouseleave", () => hide());
+ container.addEventListener("mouseenter", () => show());
+}
+
+function enableAutoHideAnimation(element: Element): void {
+ element.classList.add("autoHiding");
+ element.classList.add("hidden");
+}
+
+function disableAutoHideAnimation(element: Element): void {
+ element.classList.remove("autoHiding");
+ element.classList.remove("hidden");
+}
+
+export const AnimationUtils = {
+ applyLoadingAnimation,
+ setupAutoHideAnimation,
+ setupCustomHideAnimation,
+ enableAutoHideAnimation,
+ disableAutoHideAnimation
+}; \ No newline at end of file
diff --git a/src/utils/genericUtils.ts b/src/utils/genericUtils.ts
index 32cf83f5..b146e57a 100644
--- a/src/utils/genericUtils.ts
+++ b/src/utils/genericUtils.ts
@@ -21,6 +21,30 @@ async function wait<T>(condition: () => T | false, timeout = 5000, check = 100):
});
}
+/**
+ * Gets the error message in a nice string
+ *
+ * @param {int} statusCode
+ * @returns {string} errorMessage
+ */
+function getErrorMessage(statusCode: number, responseText: string): string {
+ let errorMessage = "";
+ const postFix = (responseText ? "\n\n" + responseText : "");
+
+ if([400, 429, 409, 502, 503, 0].includes(statusCode)) {
+ //treat them the same
+ if (statusCode == 503) statusCode = 502;
+
+ errorMessage = chrome.i18n.getMessage(statusCode + "") + " " + chrome.i18n.getMessage("errorCode") + statusCode
+ + "\n\n" + chrome.i18n.getMessage("statusReminder");
+ } else {
+ errorMessage = chrome.i18n.getMessage("connectionError") + statusCode;
+ }
+
+ return errorMessage + postFix;
+}
+
export const GenericUtils = {
- wait
+ wait,
+ getErrorMessage
} \ No newline at end of file
diff --git a/src/utils/noticeUtils.ts b/src/utils/noticeUtils.ts
new file mode 100644
index 00000000..5d77063b
--- /dev/null
+++ b/src/utils/noticeUtils.ts
@@ -0,0 +1,21 @@
+import Config from "../config";
+import { SponsorTime } from "../types";
+
+export enum SkipNoticeAction {
+ None,
+ Upvote,
+ Downvote,
+ CategoryVote,
+ CopyDownvote,
+ Unskip
+}
+
+export function downvoteButtonColor(segments: SponsorTime[], actionState: SkipNoticeAction, downvoteType: SkipNoticeAction): string {
+ // Also used for "Copy and Downvote"
+ if (segments?.length > 1) {
+ return (actionState === downvoteType) ? Config.config.colorPalette.red : Config.config.colorPalette.white;
+ } else {
+ // You dont have segment selectors so the lockbutton needs to be colored and cannot be selected.
+ return Config.config.isVip && segments[0].locked === 1 ? Config.config.colorPalette.locked : Config.config.colorPalette.white;
+ }
+} \ No newline at end of file