diff options
author | Ajay <[email protected]> | 2022-01-05 20:49:56 -0500 |
---|---|---|
committer | Ajay <[email protected]> | 2022-01-05 20:49:56 -0500 |
commit | 8e964b40b308c966a53fa3590499827bb1492ab4 (patch) | |
tree | 578aff18844a3a067e84336b544ec2799f6f6e35 | |
parent | a6a9b7dd8c7a88654909c0ee6a3218738d343504 (diff) | |
download | SponsorBlock-8e964b40b308c966a53fa3590499827bb1492ab4.tar.gz SponsorBlock-8e964b40b308c966a53fa3590499827bb1492ab4.zip |
Add vote buttons to pill that open on click
-rw-r--r-- | public/content.css | 1 | ||||
-rw-r--r-- | src/components/CategoryPillComponent.tsx | 51 | ||||
-rw-r--r-- | src/components/SkipNoticeComponent.tsx | 25 | ||||
-rw-r--r-- | src/content.ts | 73 | ||||
-rw-r--r-- | src/js-components/skipButtonControlBar.ts | 9 | ||||
-rw-r--r-- | src/messageTypes.ts | 5 | ||||
-rw-r--r-- | src/popup.ts | 10 | ||||
-rw-r--r-- | src/render/CategoryPill.tsx | 29 | ||||
-rw-r--r-- | src/utils.ts | 92 | ||||
-rw-r--r-- | src/utils/animationUtils.ts | 78 | ||||
-rw-r--r-- | src/utils/genericUtils.ts | 26 | ||||
-rw-r--r-- | src/utils/noticeUtils.ts | 21 |
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 |