mirror of
https://github.com/ajayyy/SponsorBlock.git
synced 2025-12-06 11:37:02 +03:00
Add vote buttons to pill that open on click
This commit is contained in:
@@ -622,6 +622,7 @@ input::-webkit-inner-spin-button {
|
||||
cursor: pointer;
|
||||
font-size: 75%;
|
||||
height: 100%;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.sponsorBlockCategoryPillTitleSection {
|
||||
|
||||
@@ -1,9 +1,16 @@
|
||||
import * as React from "react";
|
||||
import Config from "../config";
|
||||
import { SponsorTime } from "../types";
|
||||
import { Category, SegmentUUID, SponsorTime } from "../types";
|
||||
|
||||
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;
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -61,3 +61,8 @@ export type MessageResponse =
|
||||
| IsChannelWhitelistedResponse
|
||||
| Record<string, never>;
|
||||
|
||||
export interface VoteResponse {
|
||||
successType: number;
|
||||
statusCode: number;
|
||||
responseText: string;
|
||||
}
|
||||
10
src/popup.ts
10
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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
92
src/utils.ts
92
src/utils.ts
@@ -143,75 +143,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.
|
||||
*/
|
||||
@@ -343,29 +274,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
|
||||
*
|
||||
|
||||
78
src/utils/animationUtils.ts
Normal file
78
src/utils/animationUtils.ts
Normal file
@@ -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
|
||||
};
|
||||
@@ -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
|
||||
}
|
||||
21
src/utils/noticeUtils.ts
Normal file
21
src/utils/noticeUtils.ts
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user