mirror of
https://github.com/ajayyy/SponsorBlock.git
synced 2026-02-02 07:40:47 +03:00
Merge branch 'master' of https://github.com/ajayyy/SponsorBlock into chapters
This commit is contained in:
@@ -80,6 +80,9 @@ chrome.runtime.onInstalled.addListener(function () {
|
||||
const newUserID = utils.generateUserID();
|
||||
//save this UUID
|
||||
Config.config.userID = newUserID;
|
||||
|
||||
// Don't show update notification
|
||||
Config.config.categoryPillUpdate = true;
|
||||
}
|
||||
}, 1500);
|
||||
});
|
||||
|
||||
125
src/components/CategoryPillComponent.tsx
Normal file
125
src/components/CategoryPillComponent.tsx
Normal file
@@ -0,0 +1,125 @@
|
||||
import * as React from "react";
|
||||
import Config from "../config";
|
||||
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 {
|
||||
segment?: SponsorTime;
|
||||
show: boolean;
|
||||
open?: boolean;
|
||||
}
|
||||
|
||||
class CategoryPillComponent extends React.Component<CategoryPillProps, CategoryPillState> {
|
||||
|
||||
constructor(props: CategoryPillProps) {
|
||||
super(props);
|
||||
|
||||
this.state = {
|
||||
segment: null,
|
||||
show: false,
|
||||
open: false
|
||||
};
|
||||
}
|
||||
|
||||
render(): React.ReactElement {
|
||||
const style: React.CSSProperties = {
|
||||
backgroundColor: this.getColor(),
|
||||
display: this.state.show ? "flex" : "none",
|
||||
color: this.state.segment?.category === "sponsor"
|
||||
|| this.state.segment?.category === "exclusive_access" ? "white" : "black",
|
||||
}
|
||||
|
||||
return (
|
||||
<span style={style}
|
||||
className={"sponsorBlockCategoryPill"}
|
||||
title={this.getTitleText()}
|
||||
onClick={(e) => this.toggleOpen(e)}>
|
||||
<span className="sponsorBlockCategoryPillTitleSection">
|
||||
<img className="sponsorSkipLogo sponsorSkipObject"
|
||||
src={chrome.extension.getURL("icons/IconSponsorBlocker256px.png")}>
|
||||
</img>
|
||||
<span className="sponsorBlockCategoryPillTitle">
|
||||
{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={(e) => this.vote(e, 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>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Close Button */}
|
||||
<img src={chrome.extension.getURL("icons/close.png")}
|
||||
className="categoryPillClose"
|
||||
onClick={() => this.setState({ show: false })}>
|
||||
</img>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
private toggleOpen(event: React.MouseEvent): void {
|
||||
event.stopPropagation();
|
||||
|
||||
if (this.state.show) {
|
||||
this.setState({ open: !this.state.open });
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
show: type === 1
|
||||
});
|
||||
} else if (response.statusCode !== 403) {
|
||||
alert(GenericUtils.getErrorMessage(response.statusCode, response.responseText));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private getColor(): string {
|
||||
const configObject = Config.config.barTypes["preview-" + this.state.segment?.category]
|
||||
|| Config.config.barTypes[this.state.segment?.category];
|
||||
return configObject?.color;
|
||||
}
|
||||
|
||||
getTitleText(): string {
|
||||
const shortDescription = chrome.i18n.getMessage(`category_${this.state.segment?.category}_pill`);
|
||||
return (shortDescription ? shortDescription + ". ": "") + chrome.i18n.getMessage("categoryPillTitleText");
|
||||
}
|
||||
}
|
||||
|
||||
export default CategoryPillComponent;
|
||||
@@ -4,7 +4,7 @@ import Config from "../config"
|
||||
import * as CompileConfig from "../../config.json";
|
||||
import { Category, CategorySkipOption } from "../types";
|
||||
|
||||
import { getCategoryActionType } from "../utils/categoryUtils";
|
||||
import { getCategorySuffix } from "../utils/categoryUtils";
|
||||
|
||||
export interface CategorySkipOptionsProps {
|
||||
category: Category;
|
||||
@@ -80,7 +80,7 @@ class CategorySkipOptionsComponent extends React.Component<CategorySkipOptionsPr
|
||||
</td>
|
||||
}
|
||||
|
||||
{this.props.category !== "chapter" &&
|
||||
{!["chapter", "exclusive_access"].includes(this.props.category) &&
|
||||
<td id={this.props.category + "PreviewColorOption"}
|
||||
className="previewColorOption">
|
||||
<input
|
||||
@@ -160,11 +160,12 @@ class CategorySkipOptionsComponent extends React.Component<CategorySkipOptionsPr
|
||||
|
||||
let optionNames = ["disable", "showOverlay", "manualSkip", "autoSkip"];
|
||||
if (this.props.category === "chapter") optionNames = ["disable", "showOverlay"]
|
||||
else if (this.props.category === "exclusive_access") optionNames = ["disable", "showOverlay"];
|
||||
|
||||
for (const optionName of optionNames) {
|
||||
elements.push(
|
||||
<option key={optionName} value={optionName}>
|
||||
{chrome.i18n.getMessage(optionName !== "disable" ? optionName + getCategoryActionType(this.props.category)
|
||||
{chrome.i18n.getMessage(optionName !== "disable" ? optionName + getCategorySuffix(this.props.category)
|
||||
: optionName)}
|
||||
</option>
|
||||
);
|
||||
|
||||
@@ -16,8 +16,6 @@ export interface NoticeProps {
|
||||
timed?: boolean,
|
||||
idSuffix?: string,
|
||||
|
||||
videoSpeed?: () => number,
|
||||
|
||||
fadeIn?: boolean,
|
||||
startFaded?: boolean,
|
||||
firstColumn?: React.ReactElement,
|
||||
@@ -51,7 +49,6 @@ export interface NoticeState {
|
||||
|
||||
class NoticeComponent extends React.Component<NoticeProps, NoticeState> {
|
||||
countdownInterval: NodeJS.Timeout;
|
||||
intervalVideoSpeed: number;
|
||||
|
||||
idSuffix: string;
|
||||
|
||||
@@ -259,10 +256,6 @@ class NoticeComponent extends React.Component<NoticeProps, NoticeState> {
|
||||
|
||||
const countdownTime = Math.min(this.state.countdownTime - 1, this.state.maxCountdownTime());
|
||||
|
||||
if (this.props.videoSpeed && this.intervalVideoSpeed != this.props.videoSpeed()) {
|
||||
this.setupInterval();
|
||||
}
|
||||
|
||||
if (countdownTime <= 0) {
|
||||
//remove this from setInterval
|
||||
clearInterval(this.countdownInterval);
|
||||
@@ -325,10 +318,7 @@ class NoticeComponent extends React.Component<NoticeProps, NoticeState> {
|
||||
setupInterval(): void {
|
||||
if (this.countdownInterval) clearInterval(this.countdownInterval);
|
||||
|
||||
const intervalDuration = this.props.videoSpeed ? 1000 / this.props.videoSpeed() : 1000;
|
||||
this.countdownInterval = setInterval(this.countdown.bind(this), intervalDuration);
|
||||
|
||||
if (this.props.videoSpeed) this.intervalVideoSpeed = this.props.videoSpeed();
|
||||
this.countdownInterval = setInterval(this.countdown.bind(this), 1000);
|
||||
}
|
||||
|
||||
resetCountdown(): void {
|
||||
|
||||
@@ -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[];
|
||||
@@ -74,7 +65,6 @@ class SkipNoticeComponent extends React.Component<SkipNoticeProps, SkipNoticeSta
|
||||
|
||||
amountOfPreviousNotices: number;
|
||||
showInSecondSlot: boolean;
|
||||
audio: HTMLAudioElement;
|
||||
|
||||
idSuffix: string;
|
||||
|
||||
@@ -96,7 +86,6 @@ class SkipNoticeComponent extends React.Component<SkipNoticeProps, SkipNoticeSta
|
||||
this.segments = props.segments;
|
||||
this.autoSkip = props.autoSkip;
|
||||
this.contentContainer = props.contentContainer;
|
||||
this.audio = null;
|
||||
|
||||
const noticeTitle = getSkippingText(this.segments, this.props.autoSkip);
|
||||
|
||||
@@ -156,13 +145,6 @@ class SkipNoticeComponent extends React.Component<SkipNoticeProps, SkipNoticeSta
|
||||
}
|
||||
}
|
||||
|
||||
componentDidMount(): void {
|
||||
if (Config.config.audioNotificationOnSkip && this.audio) {
|
||||
this.audio.volume = this.contentContainer().v.volume * 0.1;
|
||||
if (this.autoSkip) this.audio.play();
|
||||
}
|
||||
}
|
||||
|
||||
render(): React.ReactElement {
|
||||
const noticeStyle: React.CSSProperties = { }
|
||||
if (this.contentContainer().onMobileYouTube) {
|
||||
@@ -186,7 +168,6 @@ class SkipNoticeComponent extends React.Component<SkipNoticeProps, SkipNoticeSta
|
||||
|| (Config.config.noticeVisibilityMode >= NoticeVisbilityMode.FadedForAutoSkip && this.autoSkip)}
|
||||
timed={true}
|
||||
maxCountdownTime={this.state.maxCountdownTime}
|
||||
videoSpeed={() => this.contentContainer().v?.playbackRate}
|
||||
style={noticeStyle}
|
||||
biggerCloseButton={this.contentContainer().onMobileYouTube}
|
||||
ref={this.noticeRef}
|
||||
@@ -196,10 +177,6 @@ class SkipNoticeComponent extends React.Component<SkipNoticeProps, SkipNoticeSta
|
||||
firstColumn={firstColumn}
|
||||
bottomRow={[...this.getMessageBoxes(), ...this.getBottomRow() ]}
|
||||
onMouseEnter={() => this.onMouseEnter() } >
|
||||
|
||||
{(Config.config.audioNotificationOnSkip) && <audio ref={(source) => { this.audio = source; }}>
|
||||
<source src={chrome.extension.getURL("icons/beep.ogg")} type="audio/ogg"></source>
|
||||
</audio>}
|
||||
</NoticeComponent>
|
||||
);
|
||||
}
|
||||
@@ -230,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 */}
|
||||
@@ -293,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>
|
||||
@@ -741,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: {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as React from "react";
|
||||
import * as CompileConfig from "../../config.json";
|
||||
import Config from "../config";
|
||||
import { ActionType, ActionTypes, Category, CategoryActionType, ChannelIDStatus, ContentContainer, SponsorTime } from "../types";
|
||||
import { ActionType, Category, CategoryActionType, ChannelIDStatus, ContentContainer, SponsorTime } from "../types";
|
||||
import Utils from "../utils";
|
||||
import { getCategoryActionType } from "../utils/categoryUtils";
|
||||
import SubmissionNoticeComponent from "./SubmissionNoticeComponent";
|
||||
@@ -45,6 +45,7 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
|
||||
|
||||
previousSkipType: CategoryActionType;
|
||||
timeBeforeChangingToPOI: number; // Initialized when first selecting POI
|
||||
fullVideoWarningShown = false;
|
||||
|
||||
// For description auto-complete
|
||||
fetchingSuggestions: boolean;
|
||||
@@ -86,6 +87,8 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
|
||||
this.configUpdateListener = () => this.configUpdate();
|
||||
Config.configListeners.push(this.configUpdate.bind(this));
|
||||
}
|
||||
|
||||
this.checkToShowFullVideoWarning();
|
||||
}
|
||||
|
||||
componentWillUnmount(): void {
|
||||
@@ -95,6 +98,8 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
|
||||
}
|
||||
|
||||
render(): React.ReactElement {
|
||||
this.checkToShowFullVideoWarning();
|
||||
|
||||
const style: React.CSSProperties = {
|
||||
textAlign: "center"
|
||||
};
|
||||
@@ -113,11 +118,14 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
|
||||
};
|
||||
// Create time display
|
||||
let timeDisplay: JSX.Element;
|
||||
const timeDisplayStyle: React.CSSProperties = {};
|
||||
const sponsorTime = this.props.contentContainer().sponsorTimesSubmitting[this.props.index];
|
||||
const segment = sponsorTime.segment;
|
||||
if (sponsorTime?.actionType === ActionType.Full) timeDisplayStyle.display = "none";
|
||||
if (this.state.editing) {
|
||||
timeDisplay = (
|
||||
<div id={"sponsorTimesContainer" + this.idSuffix}
|
||||
style={timeDisplayStyle}
|
||||
className="sponsorTimeDisplay">
|
||||
|
||||
<span id={"nowButton0" + this.idSuffix}
|
||||
@@ -168,6 +176,7 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
|
||||
timeDisplay = (
|
||||
|
||||
<div id={"sponsorTimesContainer" + this.idSuffix}
|
||||
style={timeDisplayStyle}
|
||||
className="sponsorTimeDisplay"
|
||||
onClick={this.toggleEditTime.bind(this)}>
|
||||
{utils.getFormattedTime(segment[0], true) +
|
||||
@@ -193,7 +202,8 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
|
||||
</select>
|
||||
|
||||
{/* open in new tab */}
|
||||
<a href="https://wiki.sponsor.ajay.app/index.php/Segment_Categories"
|
||||
<a href={CompileConfig.wikiLinks[sponsorTime.category]
|
||||
|| "https://wiki.sponsor.ajay.app/index.php/Segment_Categories"}
|
||||
target="_blank" rel="noreferrer">
|
||||
<img id={"sponsorTimeCategoriesHelpButton" + this.idSuffix}
|
||||
className="helpButton"
|
||||
@@ -203,7 +213,9 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
|
||||
</div>
|
||||
|
||||
{/* Action Type */}
|
||||
{CompileConfig.categorySupport[sponsorTime.category]?.length > 1 ? (
|
||||
{CompileConfig.categorySupport[sponsorTime.category] &&
|
||||
(CompileConfig.categorySupport[sponsorTime.category]?.length > 1
|
||||
|| CompileConfig.categorySupport[sponsorTime.category]?.[0] !== "skip") ? (
|
||||
<div style={{position: "relative"}}>
|
||||
<select id={"sponsorTimeActionTypes" + this.idSuffix}
|
||||
className="sponsorTimeEditSelector sponsorTimeActionTypes"
|
||||
@@ -280,7 +292,7 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
|
||||
const before = utils.getFormattedTimeToSeconds(sponsorTimeEdits[index]);
|
||||
const after = utils.getFormattedTimeToSeconds(targetValue);
|
||||
const difference = Math.abs(before - after);
|
||||
if (0 < difference && difference < 0.5) this.showToolTip();
|
||||
if (0 < difference && difference < 0.5) this.showScrollToEditToolTip();
|
||||
|
||||
sponsorTimeEdits[index] = targetValue;
|
||||
if (index === 0 && getCategoryActionType(sponsorTime.category) === CategoryActionType.POI) sponsorTimeEdits[1] = targetValue;
|
||||
@@ -319,11 +331,17 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
|
||||
}
|
||||
}
|
||||
|
||||
showToolTip(): void {
|
||||
showScrollToEditToolTip(): void {
|
||||
if (!Config.config.scrollToEditTimeUpdate && document.getElementById("sponsorRectangleTooltip" + "sponsorTimesContainer" + this.idSuffix) === null) {
|
||||
const element = document.getElementById("sponsorTimesContainer" + this.idSuffix);
|
||||
this.showToolTip(chrome.i18n.getMessage("SponsorTimeEditScrollNewFeature"), () => { Config.config.scrollToEditTimeUpdate = true });
|
||||
}
|
||||
}
|
||||
|
||||
showToolTip(text: string, buttonFunction?: () => void): boolean {
|
||||
const element = document.getElementById("sponsorTimesContainer" + this.idSuffix);
|
||||
if (element) {
|
||||
new RectangleTooltip({
|
||||
text: chrome.i18n.getMessage("SponsorTimeEditScrollNewFeature"),
|
||||
text,
|
||||
referenceNode: element.parentElement,
|
||||
prependElement: element,
|
||||
timeout: 15,
|
||||
@@ -331,10 +349,27 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
|
||||
leftOffset: -318 + "px",
|
||||
backgroundColor: "rgba(28, 28, 28, 1.0)",
|
||||
htmlId: "sponsorTimesContainer" + this.idSuffix,
|
||||
buttonFunction: () => { Config.config.scrollToEditTimeUpdate = true },
|
||||
buttonFunction,
|
||||
fontSize: "14px",
|
||||
maxHeight: "200px"
|
||||
});
|
||||
|
||||
return true;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
checkToShowFullVideoWarning(): void {
|
||||
const sponsorTime = this.props.contentContainer().sponsorTimesSubmitting[this.props.index];
|
||||
const segmentDuration = sponsorTime.segment[1] - sponsorTime.segment[0];
|
||||
const videoPercentage = segmentDuration / this.props.contentContainer().v.duration;
|
||||
|
||||
if (videoPercentage > 0.6 && !this.fullVideoWarningShown
|
||||
&& (sponsorTime.category === "sponsor" || sponsorTime.category === "selfpromo" || sponsorTime.category === "chooseACategory")) {
|
||||
if (this.showToolTip(chrome.i18n.getMessage("fullVideoTooltipWarning"))) {
|
||||
this.fullVideoWarningShown = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -480,7 +515,6 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
|
||||
const actionType = inputActionType && CompileConfig.categorySupport[category]?.includes(inputActionType) ? inputActionType as ActionType
|
||||
: CompileConfig.categorySupport[category]?.[0] ?? ActionType.Skip;
|
||||
sponsorTimesSubmitting[this.props.index].actionType = actionType;
|
||||
|
||||
|
||||
const description = actionType === ActionType.Chapter ? this.descriptionOptionRef?.current?.value : "";
|
||||
sponsorTimesSubmitting[this.props.index].description = description;
|
||||
@@ -488,6 +522,12 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
|
||||
Config.config.segmentTimes.set(this.props.contentContainer().sponsorVideoID, sponsorTimesSubmitting);
|
||||
|
||||
this.props.contentContainer().updatePreviewBar();
|
||||
|
||||
if (sponsorTimesSubmitting[this.props.index].actionType === ActionType.Full
|
||||
&& (sponsorTimesSubmitting[this.props.index].segment[0] !== 0 || sponsorTimesSubmitting[this.props.index].segment[1] !== 0)) {
|
||||
this.setTimeTo(0, 0);
|
||||
this.setTimeTo(1, 0);
|
||||
}
|
||||
}
|
||||
|
||||
previewTime(ctrlPressed = false, shiftPressed = false): void {
|
||||
|
||||
@@ -73,7 +73,7 @@ class SubmissionNoticeComponent extends React.Component<SubmissionNoticeProps, S
|
||||
idSuffix={this.state.idSuffix}
|
||||
ref={this.noticeRef}
|
||||
closeListener={this.cancel.bind(this)}
|
||||
zIndex={50000}>
|
||||
zIndex={5000}>
|
||||
|
||||
{/* Text Boxes */}
|
||||
{this.getMessageBoxes()}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import * as CompileConfig from "../config.json";
|
||||
import * as invidiousList from "../ci/invidiouslist.json";
|
||||
import { Category, CategorySelection, CategorySkipOption, NoticeVisbilityMode, PreviewBarOption, SponsorTime, StorageChangesObject, UnEncodedSegmentTimes as UnencodedSegmentTimes } from "./types";
|
||||
|
||||
interface SBConfig {
|
||||
@@ -20,6 +21,7 @@ interface SBConfig {
|
||||
showTimeWithSkips: boolean,
|
||||
disableSkipping: boolean,
|
||||
muteSegments: boolean,
|
||||
fullVideoSegments: boolean,
|
||||
trackViewCount: boolean,
|
||||
trackViewCountInPrivate: boolean,
|
||||
dontShowNotice: boolean,
|
||||
@@ -51,7 +53,7 @@ interface SBConfig {
|
||||
locked: string
|
||||
},
|
||||
scrollToEditTimeUpdate: boolean,
|
||||
fillerUpdate: boolean,
|
||||
categoryPillUpdate: boolean,
|
||||
|
||||
// What categories should be skipped
|
||||
categorySelections: CategorySelection[],
|
||||
@@ -63,6 +65,7 @@ interface SBConfig {
|
||||
"preview-sponsor": PreviewBarOption,
|
||||
"selfpromo": PreviewBarOption,
|
||||
"preview-selfpromo": PreviewBarOption,
|
||||
"exclusive_access": PreviewBarOption,
|
||||
"interaction": PreviewBarOption,
|
||||
"preview-interaction": PreviewBarOption,
|
||||
"intro": PreviewBarOption,
|
||||
@@ -177,6 +180,7 @@ const Config: SBObject = {
|
||||
showTimeWithSkips: true,
|
||||
disableSkipping: false,
|
||||
muteSegments: true,
|
||||
fullVideoSegments: true,
|
||||
trackViewCount: true,
|
||||
trackViewCountInPrivate: true,
|
||||
dontShowNotice: false,
|
||||
@@ -188,7 +192,7 @@ const Config: SBObject = {
|
||||
hideSkipButtonPlayerControls: false,
|
||||
hideDiscordLaunches: 0,
|
||||
hideDiscordLink: false,
|
||||
invidiousInstances: ["invidious.snopyta.org"],
|
||||
invidiousInstances: ["invidious.snopyta.org"], // leave as default
|
||||
supportInvidious: false,
|
||||
serverAddress: CompileConfig.serverAddress,
|
||||
minDuration: 0,
|
||||
@@ -203,7 +207,7 @@ const Config: SBObject = {
|
||||
autoHideInfoButton: true,
|
||||
autoSkipOnMusicVideos: false,
|
||||
scrollToEditTimeUpdate: false, // false means the tooltip will be shown
|
||||
fillerUpdate: false,
|
||||
categoryPillUpdate: false,
|
||||
|
||||
categorySelections: [{
|
||||
name: "sponsor" as Category,
|
||||
@@ -211,6 +215,9 @@ const Config: SBObject = {
|
||||
}, {
|
||||
name: "poi_highlight" as Category,
|
||||
option: CategorySkipOption.ManualSkip
|
||||
}, {
|
||||
name: "exclusive_access" as Category,
|
||||
option: CategorySkipOption.ShowOverlay
|
||||
}],
|
||||
|
||||
colorPalette: {
|
||||
@@ -241,6 +248,10 @@ const Config: SBObject = {
|
||||
color: "#bfbf35",
|
||||
opacity: "0.7"
|
||||
},
|
||||
"exclusive_access": {
|
||||
color: "#008a5c",
|
||||
opacity: "0.7"
|
||||
},
|
||||
"interaction": {
|
||||
color: "#cc00ff",
|
||||
opacity: "0.7"
|
||||
@@ -392,6 +403,20 @@ function fetchConfig(): Promise<void> {
|
||||
}
|
||||
|
||||
function migrateOldFormats(config: SBConfig) {
|
||||
if (!config["exclusive_accessCategoryAdded"] && !config.categorySelections.some((s) => s.name === "exclusive_access")) {
|
||||
config["exclusive_accessCategoryAdded"] = true;
|
||||
|
||||
config.categorySelections.push({
|
||||
name: "exclusive_access" as Category,
|
||||
option: CategorySkipOption.ShowOverlay
|
||||
});
|
||||
|
||||
config.categorySelections = config.categorySelections;
|
||||
}
|
||||
|
||||
if (config["fillerUpdate"] !== undefined) {
|
||||
chrome.storage.sync.remove("fillerUpdate");
|
||||
}
|
||||
if (config["highlightCategoryAdded"] !== undefined) {
|
||||
chrome.storage.sync.remove("highlightCategoryAdded");
|
||||
}
|
||||
@@ -432,6 +457,11 @@ function migrateOldFormats(config: SBConfig) {
|
||||
if (config["previousVideoID"] !== undefined) {
|
||||
chrome.storage.sync.remove("previousVideoID");
|
||||
}
|
||||
|
||||
// populate invidiousInstances with new instances if 3p support is **DISABLED**
|
||||
if (!config["supportInvidious"] && config["invidiousInstances"].length !== invidiousList.length) {
|
||||
config["invidiousInstances"] = invidiousList;
|
||||
}
|
||||
}
|
||||
|
||||
async function setupConfig() {
|
||||
|
||||
265
src/content.ts
265
src/content.ts
@@ -11,13 +11,15 @@ 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";
|
||||
import { Tooltip } from "./render/Tooltip";
|
||||
import { getStartTimeFromUrl } from "./utils/urlParser";
|
||||
import { getControls } from "./utils/pageUtils";
|
||||
import { findValidElement, getControls, getHashParams, isVisible } 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);
|
||||
@@ -75,9 +77,11 @@ let lastCheckVideoTime = -1;
|
||||
//is this channel whitelised from getting sponsors skipped
|
||||
let channelWhitelisted = false;
|
||||
|
||||
// create preview bar
|
||||
let previewBar: PreviewBar = null;
|
||||
// Skip to highlight button
|
||||
let skipButtonControlBar: SkipButtonControlBar = null;
|
||||
// For full video sponsors/selfpromo
|
||||
let categoryPill: CategoryPill = null;
|
||||
|
||||
/** Element containing the player controls on the YouTube player. */
|
||||
let controls: HTMLElement | null = null;
|
||||
@@ -86,7 +90,8 @@ let controls: HTMLElement | null = null;
|
||||
const playerButtons: Record<string, {button: HTMLButtonElement, image: HTMLImageElement, setupListener: boolean}> = {};
|
||||
|
||||
// Direct Links after the config is loaded
|
||||
utils.wait(() => Config.config !== null, 1000, 1).then(() => videoIDChange(getYouTubeVideoID(document.URL)));
|
||||
utils.wait(() => Config.config !== null, 1000, 1).then(() => videoIDChange(getYouTubeVideoID(document)));
|
||||
addPageListeners();
|
||||
addHotkeyListener();
|
||||
|
||||
//the amount of times the sponsor lookup has retried
|
||||
@@ -138,7 +143,7 @@ function messageListener(request: Message, sender: unknown, sendResponse: (respo
|
||||
//messages from popup script
|
||||
switch(request.message){
|
||||
case "update":
|
||||
videoIDChange(getYouTubeVideoID(document.URL));
|
||||
videoIDChange(getYouTubeVideoID(document));
|
||||
break;
|
||||
case "sponsorStart":
|
||||
startOrEndTimingNewSegment()
|
||||
@@ -264,15 +269,16 @@ function resetValues() {
|
||||
isAdPlaying = false;
|
||||
|
||||
for (let i = 0; i < skipNotices.length; i++) {
|
||||
skipNotices.pop().close();
|
||||
skipNotices.pop()?.close();
|
||||
}
|
||||
|
||||
skipButtonControlBar?.disable();
|
||||
categoryPill?.setVisibility(false);
|
||||
}
|
||||
|
||||
async function videoIDChange(id) {
|
||||
//if the id has not changed return
|
||||
if (sponsorVideoID === id) return;
|
||||
//if the id has not changed return unless the video element has changed
|
||||
if (sponsorVideoID === id && isVisible(video)) return;
|
||||
|
||||
//set the global videoID
|
||||
sponsorVideoID = id;
|
||||
@@ -338,26 +344,6 @@ async function videoIDChange(id) {
|
||||
// Clear unsubmitted segments from the previous video
|
||||
sponsorTimesSubmitting = [];
|
||||
updateSponsorTimesSubmitting();
|
||||
|
||||
// Filler update
|
||||
if (!Config.config.fillerUpdate) {
|
||||
Config.config.fillerUpdate = true;
|
||||
|
||||
utils.wait(getControls).then(() => {
|
||||
const playButton = document.querySelector(".ytp-play-button") as HTMLElement;
|
||||
const allCategories = ["sponsor", "intro", "outro", "selfpromo", "interaction"];
|
||||
if (playButton && allCategories.every((name) => Config.config.categorySelections.some((selection) => selection.name === name))
|
||||
&& utils.getCategorySelection("filler") === undefined) {
|
||||
new Tooltip({
|
||||
text: chrome.i18n.getMessage("fillerNewFeature"),
|
||||
link: "https://wiki.sponsor.ajay.app/w/Filler_Tangent",
|
||||
referenceNode: playButton.parentElement,
|
||||
prependElement: playButton,
|
||||
timeout: 10
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function handleMobileControlsMutations(): void {
|
||||
@@ -402,7 +388,7 @@ function createPreviewBar(): void {
|
||||
];
|
||||
|
||||
for (const selector of progressElementSelectors) {
|
||||
const el = document.querySelector<HTMLElement>(selector);
|
||||
const el = findValidElement(document.querySelectorAll(selector));
|
||||
|
||||
if (el) {
|
||||
previewBar = new PreviewBar(el, onMobileYouTube, onInvidious);
|
||||
@@ -423,6 +409,16 @@ function durationChangeListener(): void {
|
||||
updatePreviewBar();
|
||||
}
|
||||
|
||||
/**
|
||||
* Triggered once the video is ready.
|
||||
* This is mainly to attach to embedded players who don't have a video element visible.
|
||||
*/
|
||||
function videoOnReadyListener(): void {
|
||||
createPreviewBar();
|
||||
updatePreviewBar();
|
||||
createButtons();
|
||||
}
|
||||
|
||||
function cancelSponsorSchedule(): void {
|
||||
if (currentSkipSchedule !== null) {
|
||||
clearTimeout(currentSkipSchedule);
|
||||
@@ -535,7 +531,7 @@ function inMuteSegment(currentTime: number): boolean {
|
||||
* This makes sure the videoID is still correct and if the sponsorTime is included
|
||||
*/
|
||||
function incorrectVideoCheck(videoID?: string, sponsorTime?: SponsorTime): boolean {
|
||||
const currentVideoID = getYouTubeVideoID(document.URL);
|
||||
const currentVideoID = getYouTubeVideoID(document);
|
||||
if (currentVideoID !== (videoID || sponsorVideoID) || (sponsorTime
|
||||
&& (!sponsorTimes || !sponsorTimes?.some((time) => time.segment === sponsorTime.segment))
|
||||
&& !sponsorTimesSubmitting.some((time) => time.segment === sponsorTime.segment))) {
|
||||
@@ -566,7 +562,7 @@ function setupVideoMutationListener() {
|
||||
}
|
||||
|
||||
function refreshVideoAttachments() {
|
||||
const newVideo = document.querySelector('video');
|
||||
const newVideo = findValidElement(document.querySelectorAll('video')) as HTMLVideoElement;
|
||||
if (newVideo && newVideo !== video) {
|
||||
video = newVideo;
|
||||
|
||||
@@ -575,12 +571,22 @@ function refreshVideoAttachments() {
|
||||
|
||||
setupVideoListeners();
|
||||
setupSkipButtonControlBar();
|
||||
setupCategoryPill();
|
||||
}
|
||||
|
||||
// Create a new bar in the new video element
|
||||
if (previewBar && !utils.findReferenceNode()?.contains(previewBar.container)) {
|
||||
previewBar.remove();
|
||||
previewBar = null;
|
||||
|
||||
createPreviewBar();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function setupVideoListeners() {
|
||||
//wait until it is loaded
|
||||
video.addEventListener('loadstart', videoOnReadyListener)
|
||||
video.addEventListener('durationchange', durationChangeListener);
|
||||
|
||||
if (!Config.config.disableSkipping) {
|
||||
@@ -665,8 +671,16 @@ function setupSkipButtonControlBar() {
|
||||
skipButtonControlBar.attachToPage();
|
||||
}
|
||||
|
||||
function setupCategoryPill() {
|
||||
if (!categoryPill) {
|
||||
categoryPill = new CategoryPill();
|
||||
}
|
||||
|
||||
categoryPill.attachToPage(onMobileYouTube, onInvidious, voteAsync);
|
||||
}
|
||||
|
||||
async function sponsorsLookup(id: string, keepOldSubmissions = true) {
|
||||
if (!video) refreshVideoAttachments();
|
||||
if (!video || !isVisible(video)) refreshVideoAttachments();
|
||||
//there is still no video here
|
||||
if (!video) {
|
||||
setTimeout(() => sponsorsLookup(id), 100);
|
||||
@@ -685,22 +699,14 @@ async function sponsorsLookup(id: string, keepOldSubmissions = true) {
|
||||
}
|
||||
|
||||
const extraRequestData: Record<string, unknown> = {};
|
||||
const windowHash = window.location.hash.substr(1);
|
||||
if (windowHash) {
|
||||
const params: Record<string, unknown> = windowHash.split('&').reduce((acc, param) => {
|
||||
const [key, value] = param.split('=');
|
||||
acc[key] = value;
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
if (params.requiredSegment) extraRequestData.requiredSegment = params.requiredSegment;
|
||||
}
|
||||
const hashParams = getHashParams();
|
||||
if (hashParams.requiredSegment) extraRequestData.requiredSegment = hashParams.requiredSegment;
|
||||
|
||||
// Check for hashPrefix setting
|
||||
const hashPrefix = (await utils.getHash(id, 1)).substr(0, 4);
|
||||
const response = await utils.asyncRequestToServer('GET', "/api/skipSegments/" + hashPrefix, {
|
||||
categories,
|
||||
actionTypes: Config.config.muteSegments ? [ActionType.Skip, ActionType.Mute, ActionType.Chapter] : [ActionType.Skip, ActionType.Chapter],
|
||||
actionTypes: getEnabledActionTypes(),
|
||||
userAgent: `${chrome.runtime.id}`,
|
||||
...extraRequestData
|
||||
});
|
||||
@@ -785,6 +791,18 @@ async function sponsorsLookup(id: string, keepOldSubmissions = true) {
|
||||
lookupVipInformation(id);
|
||||
}
|
||||
|
||||
function getEnabledActionTypes(): ActionType[] {
|
||||
const actionTypes = [ActionType.Skip, ActionType.Chapter];
|
||||
if (Config.config.muteSegments) {
|
||||
actionTypes.push(ActionType.Mute);
|
||||
}
|
||||
if (Config.config.fullVideoSegments) {
|
||||
actionTypes.push(ActionType.Full);
|
||||
}
|
||||
|
||||
return actionTypes;
|
||||
}
|
||||
|
||||
function lookupVipInformation(id: string): void {
|
||||
updateVipInfo().then((isVip) => {
|
||||
if (isVip) {
|
||||
@@ -896,6 +914,11 @@ function startSkipScheduleCheckingForStartSponsors() {
|
||||
}
|
||||
}
|
||||
|
||||
const fullVideoSegment = sponsorTimes.filter((time) => time.actionType === ActionType.Full)[0];
|
||||
if (fullVideoSegment) {
|
||||
categoryPill?.setSegment(fullVideoSegment);
|
||||
}
|
||||
|
||||
if (startingSegmentTime !== -1) {
|
||||
startSponsorSchedule(undefined, startingSegmentTime);
|
||||
} else {
|
||||
@@ -924,8 +947,30 @@ async function getVideoInfo(): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
function getYouTubeVideoID(url: string): string | boolean {
|
||||
// For YouTube TV support
|
||||
function getYouTubeVideoID(document: Document): string | boolean {
|
||||
const url = document.URL;
|
||||
// skip to URL if matches youtube watch or invidious or matches youtube pattern
|
||||
if ((!url.includes("youtube.com")) || url.includes("/watch") || url.includes("/shorts/") || url.includes("playlist")) return getYouTubeVideoIDFromURL(url);
|
||||
// skip to document and don't hide if on /embed/
|
||||
if (url.includes("/embed/")) return getYouTubeVideoIDFromDocument(document, false);
|
||||
// skip to document if matches pattern
|
||||
if (url.includes("/channel/") || url.includes("/user/") || url.includes("/c/")) return getYouTubeVideoIDFromDocument(document);
|
||||
// not sure, try URL then document
|
||||
return getYouTubeVideoIDFromURL(url) || getYouTubeVideoIDFromDocument(document);
|
||||
}
|
||||
|
||||
function getYouTubeVideoIDFromDocument(document: Document, hideIcon = true): string | boolean {
|
||||
// get ID from document (channel trailer / embedded playlist)
|
||||
const videoURL = document.querySelector("[data-sessionlink='feature=player-title']")?.getAttribute("href");
|
||||
if (videoURL) {
|
||||
onInvidious = hideIcon;
|
||||
return getYouTubeVideoIDFromURL(videoURL);
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
function getYouTubeVideoIDFromURL(url: string): string | boolean {
|
||||
if(url.startsWith("https://www.youtube.com/tv#/")) url = url.replace("#", "");
|
||||
|
||||
//Attempt to parse url
|
||||
@@ -945,7 +990,7 @@ function getYouTubeVideoID(url: string): string | boolean {
|
||||
} else if (!["m.youtube.com", "www.youtube.com", "www.youtube-nocookie.com", "music.youtube.com"].includes(urlObject.host)) {
|
||||
if (!Config.config) {
|
||||
// Call this later, in case this is an Invidious tab
|
||||
utils.wait(() => Config.config !== null).then(() => videoIDChange(getYouTubeVideoID(url)));
|
||||
utils.wait(() => Config.config !== null).then(() => videoIDChange(getYouTubeVideoIDFromURL(url)));
|
||||
}
|
||||
|
||||
return false
|
||||
@@ -963,7 +1008,7 @@ function getYouTubeVideoID(url: string): string | boolean {
|
||||
console.error("[SB] Video ID not valid for " + url);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1013,7 +1058,7 @@ function updatePreviewBar(): void {
|
||||
});
|
||||
});
|
||||
|
||||
previewBar.set(previewBarSegments, video?.duration)
|
||||
previewBar.set(previewBarSegments.filter((segment) => segment.actionType !== ActionType.Full), video?.duration)
|
||||
|
||||
if (Config.config.showTimeWithSkips) {
|
||||
const skippedDuration = utils.getTimestampsDuration(previewBarSegments.map(({segment}) => segment));
|
||||
@@ -1257,7 +1302,12 @@ function skipToTime({v, skipTime, skippingSegments, openNotice, forceAutoSkip, u
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
if (autoSkip && Config.config.audioNotificationOnSkip) {
|
||||
const beep = new Audio(chrome.runtime.getURL("icons/beep.ogg"));
|
||||
beep.volume = video.volume * 0.1;
|
||||
beep.play();
|
||||
}
|
||||
|
||||
if (!autoSkip
|
||||
@@ -1357,8 +1407,9 @@ function shouldAutoSkip(segment: SponsorTime): boolean {
|
||||
}
|
||||
|
||||
function shouldSkip(segment: SponsorTime): boolean {
|
||||
return utils.getCategorySelection(segment.category)?.option !== CategorySkipOption.ShowOverlay ||
|
||||
(Config.config.autoSkipOnMusicVideos && sponsorTimes?.some((s) => s.category === "music_offtopic"));
|
||||
return (segment.actionType !== ActionType.Full
|
||||
&& utils.getCategorySelection(segment.category)?.option !== CategorySkipOption.ShowOverlay)
|
||||
|| (Config.config.autoSkipOnMusicVideos && sponsorTimes?.some((s) => s.category === "music_offtopic"));
|
||||
}
|
||||
|
||||
/** Creates any missing buttons on the YouTube player if possible. */
|
||||
@@ -1379,7 +1430,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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1537,6 +1588,8 @@ function updateSponsorTimesSubmitting(getFromConfig = true) {
|
||||
if (submissionNotice !== null) {
|
||||
submissionNotice.update();
|
||||
}
|
||||
|
||||
checkForPreloadedSegment();
|
||||
}
|
||||
|
||||
function openInfoMenu() {
|
||||
@@ -1660,13 +1713,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
|
||||
@@ -1686,33 +1763,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);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1753,9 +1811,15 @@ function submitSponsorTimes() {
|
||||
//send the message to the background js
|
||||
//called after all the checks have been made that it's okay to do so
|
||||
async function sendSubmitMessage() {
|
||||
// Block if submitting on a running livestream or premiere
|
||||
if (isVisible(document.querySelector(".ytp-live-badge"))) {
|
||||
alert(chrome.i18n.getMessage("liveOrPremiere"));
|
||||
return;
|
||||
}
|
||||
|
||||
// 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++) {
|
||||
@@ -1770,8 +1834,8 @@ async function sendSubmitMessage() {
|
||||
// Check to see if any of the submissions are below the minimum duration set
|
||||
if (Config.config.minDuration > 0) {
|
||||
for (let i = 0; i < sponsorTimesSubmitting.length; i++) {
|
||||
if (sponsorTimesSubmitting[i].segment[1] - sponsorTimesSubmitting[i].segment[0] < Config.config.minDuration
|
||||
&& getCategoryActionType(sponsorTimesSubmitting[i].category) !== CategoryActionType.POI) {
|
||||
const duration = sponsorTimesSubmitting[i].segment[1] - sponsorTimesSubmitting[i].segment[0];
|
||||
if (duration > 0 && duration < Config.config.minDuration) {
|
||||
const confirmShort = chrome.i18n.getMessage("shortCheck") + "\n\n" +
|
||||
getSegmentsMessage(sponsorTimesSubmitting);
|
||||
|
||||
@@ -1827,7 +1891,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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1854,6 +1918,16 @@ function getSegmentsMessage(sponsorTimes: SponsorTime[]): string {
|
||||
return sponsorTimesMessage;
|
||||
}
|
||||
|
||||
function addPageListeners(): void {
|
||||
const refreshListners = () => {
|
||||
if (!isVisible(video)) {
|
||||
refreshVideoAttachments();
|
||||
}
|
||||
};
|
||||
|
||||
document.addEventListener("yt-navigate-finish", refreshListners);
|
||||
}
|
||||
|
||||
function addHotkeyListener(): void {
|
||||
document.addEventListener("keydown", hotkeyListener);
|
||||
}
|
||||
@@ -1963,3 +2037,24 @@ function showTimeWithoutSkips(skippedDuration: number): void {
|
||||
|
||||
duration.innerText = (durationAfterSkips == null || skippedDuration <= 0) ? "" : " (" + durationAfterSkips + ")";
|
||||
}
|
||||
|
||||
function checkForPreloadedSegment() {
|
||||
const hashParams = getHashParams();
|
||||
|
||||
const segments = hashParams.segments;
|
||||
if (Array.isArray(segments)) {
|
||||
for (const segment of segments) {
|
||||
if (Array.isArray(segment.segment)) {
|
||||
if (!sponsorTimesSubmitting.some((s) => s.segment[0] === segment.segment[0] && s.segment[1] === s.segment[1])) {
|
||||
sponsorTimesSubmitting.push({
|
||||
segment: segment.segment,
|
||||
UUID: utils.generateUserID() as SegmentUUID,
|
||||
category: segment.category ? segment.category : Config.config.defaultCategory,
|
||||
actionType: segment.actionType ? segment.actionType : ActionType.Skip,
|
||||
source: SponsorSourceType.Local
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -66,3 +66,8 @@ export type MessageResponse =
|
||||
| IsChannelWhitelistedResponse
|
||||
| Record<string, never>;
|
||||
|
||||
export interface VoteResponse {
|
||||
successType: number;
|
||||
statusCode: number;
|
||||
responseText: string;
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import Config from "./config";
|
||||
import * as CompileConfig from "../config.json";
|
||||
import * as invidiousList from "../ci/invidiouslist.json";
|
||||
|
||||
// Make the config public for debugging purposes
|
||||
window.SB = Config;
|
||||
@@ -297,8 +298,8 @@ function invidiousInstanceAddInit(element: HTMLElement, option: string) {
|
||||
const resetButton = element.querySelector(".invidious-instance-reset");
|
||||
resetButton.addEventListener("click", function() {
|
||||
if (confirm(chrome.i18n.getMessage("resetInvidiousInstanceAlert"))) {
|
||||
// Set to a clone of the default
|
||||
Config.config[option] = Config.defaults[option].slice(0);
|
||||
// Set to CI populated list
|
||||
Config.config[option] = invidiousList;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
21
src/popup.ts
21
src/popup.ts
@@ -5,6 +5,8 @@ import { SponsorTime, SponsorHideType, CategoryActionType, ActionType, SegmentUU
|
||||
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 {
|
||||
@@ -450,10 +452,15 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
|
||||
const name = segmentTimes[i].description || utils.shortCategoryName(category);
|
||||
const textNode = document.createTextNode(name + extraInfo);
|
||||
const segmentTimeFromToNode = document.createElement("div");
|
||||
segmentTimeFromToNode.innerText = utils.getFormattedTime(segmentTimes[i].segment[0], true) +
|
||||
if (segmentTimes[i].actionType === ActionType.Full) {
|
||||
segmentTimeFromToNode.innerText = chrome.i18n.getMessage("full");
|
||||
} else {
|
||||
segmentTimeFromToNode.innerText = utils.getFormattedTime(segmentTimes[i].segment[0], true) +
|
||||
(getCategoryActionType(category) !== CategoryActionType.POI
|
||||
? " " + chrome.i18n.getMessage("to") + " " + utils.getFormattedTime(segmentTimes[i].segment[1], true)
|
||||
: "");
|
||||
}
|
||||
|
||||
segmentTimeFromToNode.style.margin = "5px";
|
||||
|
||||
if (actionType !== ActionType.Chapter) sponsorTimeButton.appendChild(categoryColorCircle);
|
||||
@@ -488,7 +495,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();
|
||||
});
|
||||
|
||||
@@ -602,7 +609,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);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -645,7 +652,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);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -748,7 +755,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,
|
||||
@@ -780,7 +787,7 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
|
||||
}
|
||||
|
||||
if (element) {
|
||||
const stopAnimation = utils.applyLoadingAnimation(element, 0.3);
|
||||
const stopAnimation = AnimationUtils.applyLoadingAnimation(element, 0.3);
|
||||
stopAnimation();
|
||||
}
|
||||
}
|
||||
@@ -824,7 +831,7 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
|
||||
*/
|
||||
function getFormattedHours(minutes) {
|
||||
minutes = Math.round(minutes * 10) / 10;
|
||||
const days = Math.floor(minutes / 3600);
|
||||
const days = Math.floor(minutes / 1440);
|
||||
const hours = Math.floor(minutes / 60) % 24;
|
||||
return (days > 0 ? days + chrome.i18n.getMessage("dayAbbreviation") + " " : "") + (hours > 0 ? hours + chrome.i18n.getMessage("hourAbbreviation") + " " : "") + (minutes % 60).toFixed(1);
|
||||
}
|
||||
|
||||
116
src/render/CategoryPill.tsx
Normal file
116
src/render/CategoryPill.tsx
Normal file
@@ -0,0 +1,116 @@
|
||||
import * as React from "react";
|
||||
import * as ReactDOM from "react-dom";
|
||||
import CategoryPillComponent, { CategoryPillState } from "../components/CategoryPillComponent";
|
||||
import Config from "../config";
|
||||
import { VoteResponse } from "../messageTypes";
|
||||
import { Category, SegmentUUID, SponsorTime } from "../types";
|
||||
import { GenericUtils } from "../utils/genericUtils";
|
||||
import { Tooltip } from "./Tooltip";
|
||||
|
||||
export class CategoryPill {
|
||||
container: HTMLElement;
|
||||
ref: React.RefObject<CategoryPillComponent>;
|
||||
|
||||
unsavedState: CategoryPillState;
|
||||
|
||||
mutationObserver?: MutationObserver;
|
||||
|
||||
constructor() {
|
||||
this.ref = React.createRef();
|
||||
}
|
||||
|
||||
async attachToPage(onMobileYouTube: boolean, onInvidious: boolean,
|
||||
vote: (type: number, UUID: SegmentUUID, category?: Category) => Promise<VoteResponse>): Promise<void> {
|
||||
const referenceNode =
|
||||
await GenericUtils.wait(() =>
|
||||
// New YouTube Title, YouTube, Mobile YouTube, Invidious
|
||||
document.querySelector("#title h1, .ytd-video-primary-info-renderer.title, .slim-video-information-title, #player-container + .h-box > h1") as HTMLElement);
|
||||
|
||||
if (referenceNode && !referenceNode.contains(this.container)) {
|
||||
this.container = document.createElement('span');
|
||||
this.container.id = "categoryPill";
|
||||
this.container.style.display = "relative";
|
||||
|
||||
referenceNode.prepend(this.container);
|
||||
referenceNode.style.display = "flex";
|
||||
|
||||
if (this.ref.current) {
|
||||
this.unsavedState = this.ref.current.state;
|
||||
}
|
||||
|
||||
ReactDOM.render(
|
||||
<CategoryPillComponent ref={this.ref} vote={vote} />,
|
||||
this.container
|
||||
);
|
||||
|
||||
if (this.unsavedState) {
|
||||
this.ref.current?.setState(this.unsavedState);
|
||||
this.unsavedState = null;
|
||||
}
|
||||
|
||||
if (onMobileYouTube) {
|
||||
if (this.mutationObserver) {
|
||||
this.mutationObserver.disconnect();
|
||||
}
|
||||
|
||||
this.mutationObserver = new MutationObserver(() => this.attachToPage(onMobileYouTube, onInvidious, vote));
|
||||
|
||||
this.mutationObserver.observe(referenceNode, {
|
||||
childList: true,
|
||||
subtree: true
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
close(): void {
|
||||
ReactDOM.unmountComponentAtNode(this.container);
|
||||
this.container.remove();
|
||||
}
|
||||
|
||||
setVisibility(show: boolean): void {
|
||||
const newState = {
|
||||
show,
|
||||
open: show ? this.ref.current?.state.open : false
|
||||
};
|
||||
|
||||
if (this.ref.current) {
|
||||
this.ref.current?.setState(newState);
|
||||
} else {
|
||||
this.unsavedState = newState;
|
||||
}
|
||||
}
|
||||
|
||||
async setSegment(segment: SponsorTime): Promise<void> {
|
||||
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 (!Config.config.categoryPillUpdate) {
|
||||
Config.config.categoryPillUpdate = true;
|
||||
|
||||
const watchDiv = await GenericUtils.wait(() => document.querySelector("#info.ytd-watch-flexy") as HTMLElement);
|
||||
if (watchDiv) {
|
||||
new Tooltip({
|
||||
text: chrome.i18n.getMessage("categoryPillNewFeature"),
|
||||
link: "https://blog.ajay.app/full-video-sponsorblock",
|
||||
referenceNode: watchDiv,
|
||||
prependElement: watchDiv.firstChild as HTMLElement,
|
||||
bottomOffset: "-10px",
|
||||
opacity: 0.95,
|
||||
timeout: 50000
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4,9 +4,10 @@ import * as ReactDOM from "react-dom";
|
||||
import Utils from "../utils";
|
||||
const utils = new Utils();
|
||||
|
||||
import SkipNoticeComponent, { SkipNoticeAction } from "../components/SkipNoticeComponent";
|
||||
import SkipNoticeComponent from "../components/SkipNoticeComponent";
|
||||
import { SponsorTime, ContentContainer, NoticeVisbilityMode } from "../types";
|
||||
import Config from "../config";
|
||||
import { SkipNoticeAction } from "../utils/noticeUtils";
|
||||
|
||||
class SkipNotice {
|
||||
segments: SponsorTime[];
|
||||
|
||||
@@ -8,6 +8,7 @@ export interface TooltipProps {
|
||||
prependElement?: HTMLElement, // Element to append before
|
||||
bottomOffset?: string
|
||||
timeout?: number;
|
||||
opacity?: number;
|
||||
}
|
||||
|
||||
export class Tooltip {
|
||||
@@ -18,11 +19,12 @@ export class Tooltip {
|
||||
|
||||
constructor(props: TooltipProps) {
|
||||
props.bottomOffset ??= "70px";
|
||||
props.opacity ??= 0.7;
|
||||
this.text = props.text;
|
||||
|
||||
this.container = document.createElement('div');
|
||||
this.container.id = "sponsorTooltip" + props.text;
|
||||
this.container.style.display = "relative";
|
||||
this.container.style.position = "relative";
|
||||
|
||||
if (props.prependElement) {
|
||||
props.referenceNode.insertBefore(this.container, props.prependElement);
|
||||
@@ -34,8 +36,10 @@ export class Tooltip {
|
||||
this.timer = setTimeout(() => this.close(), props.timeout * 1000);
|
||||
}
|
||||
|
||||
const backgroundColor = `rgba(28, 28, 28, ${props.opacity})`;
|
||||
|
||||
ReactDOM.render(
|
||||
<div style={{bottom: props.bottomOffset}}
|
||||
<div style={{bottom: props.bottomOffset, backgroundColor}}
|
||||
className="sponsorBlockTooltip" >
|
||||
<div>
|
||||
<img className="sponsorSkipLogo sponsorSkipObject"
|
||||
|
||||
@@ -62,6 +62,7 @@ export enum ActionType {
|
||||
Skip = "skip",
|
||||
Mute = "mute",
|
||||
Chapter = "chapter",
|
||||
Full = "full"
|
||||
}
|
||||
|
||||
export const ActionTypes = [ActionType.Skip, ActionType.Mute];
|
||||
|
||||
138
src/utils.ts
138
src/utils.ts
@@ -2,6 +2,8 @@ import Config from "./config";
|
||||
import { CategorySelection, SponsorTime, FetchResponse, BackgroundScriptContainer, Registration } from "./types";
|
||||
|
||||
import * as CompileConfig from "../config.json";
|
||||
import { findValidElementFromSelector } from "./utils/pageUtils";
|
||||
import { GenericUtils } from "./utils/genericUtils";
|
||||
|
||||
export default class Utils {
|
||||
|
||||
@@ -23,24 +25,8 @@ export default class Utils {
|
||||
this.backgroundScriptContainer = backgroundScriptContainer;
|
||||
}
|
||||
|
||||
/** Function that can be used to wait for a condition before returning. */
|
||||
async wait<T>(condition: () => T | false, timeout = 5000, check = 100): Promise<T> {
|
||||
return await new Promise((resolve, reject) => {
|
||||
setTimeout(() => reject("TIMEOUT"), timeout);
|
||||
|
||||
const intervalCheck = () => {
|
||||
const result = condition();
|
||||
if (result !== false) {
|
||||
resolve(result);
|
||||
clearInterval(interval);
|
||||
}
|
||||
};
|
||||
|
||||
const interval = setInterval(intervalCheck, check);
|
||||
|
||||
//run the check once first, this speeds it up a lot
|
||||
intervalCheck();
|
||||
});
|
||||
return GenericUtils.wait(condition, timeout, check);
|
||||
}
|
||||
|
||||
containsPermission(permissions: chrome.permissions.Permissions): Promise<boolean> {
|
||||
@@ -158,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.
|
||||
*/
|
||||
@@ -358,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
|
||||
*
|
||||
@@ -436,22 +330,28 @@ export default class Utils {
|
||||
}
|
||||
|
||||
findReferenceNode(): HTMLElement {
|
||||
let referenceNode = document.getElementById("player-container-id")
|
||||
?? document.getElementById("movie_player")
|
||||
?? document.querySelector("#main-panel.ytmusic-player-page") // YouTube music
|
||||
?? document.querySelector("#player-container .video-js") // Invidious
|
||||
?? document.querySelector(".main-video-section > .video-container"); // Cloudtube
|
||||
const selectors = [
|
||||
"#player-container-id",
|
||||
"#movie_player",
|
||||
"#c4-player", // Channel Trailer
|
||||
"#main-panel.ytmusic-player-page", // YouTube music
|
||||
"#player-container .video-js", // Invidious
|
||||
".main-video-section > .video-container" // Cloudtube
|
||||
]
|
||||
let referenceNode = findValidElementFromSelector(selectors)
|
||||
if (referenceNode == null) {
|
||||
//for embeds
|
||||
const player = document.getElementById("player");
|
||||
referenceNode = player.firstChild as HTMLElement;
|
||||
let index = 1;
|
||||
if (referenceNode) {
|
||||
let index = 1;
|
||||
|
||||
//find the child that is the video player (sometimes it is not the first)
|
||||
while (index < player.children.length && (!referenceNode.classList.contains("html5-video-player") || !referenceNode.classList.contains("ytp-embed"))) {
|
||||
referenceNode = player.children[index] as HTMLElement;
|
||||
//find the child that is the video player (sometimes it is not the first)
|
||||
while (index < player.children.length && (!referenceNode.classList.contains("html5-video-player") || !referenceNode.classList.contains("ytp-embed"))) {
|
||||
referenceNode = player.children[index] as HTMLElement;
|
||||
|
||||
index++;
|
||||
index++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
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
|
||||
};
|
||||
@@ -44,4 +44,14 @@ export function getCategoryActionType(category: Category): CategoryActionType {
|
||||
} else {
|
||||
return CategoryActionType.Skippable;
|
||||
}
|
||||
}
|
||||
|
||||
export function getCategorySuffix(category: Category): string {
|
||||
if (category.startsWith("poi_")) {
|
||||
return "_POI";
|
||||
} else if (category === "exclusive_access") {
|
||||
return "_full";
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
50
src/utils/genericUtils.ts
Normal file
50
src/utils/genericUtils.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
/** Function that can be used to wait for a condition before returning. */
|
||||
async function wait<T>(condition: () => T | false, timeout = 5000, check = 100): Promise<T> {
|
||||
return await new Promise((resolve, reject) => {
|
||||
setTimeout(() => {
|
||||
clearInterval(interval);
|
||||
reject("TIMEOUT");
|
||||
}, timeout);
|
||||
|
||||
const intervalCheck = () => {
|
||||
const result = condition();
|
||||
if (result) {
|
||||
resolve(result);
|
||||
clearInterval(interval);
|
||||
}
|
||||
};
|
||||
|
||||
const interval = setInterval(intervalCheck, check);
|
||||
|
||||
//run the check once first, this speeds it up a lot
|
||||
intervalCheck();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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,
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -17,4 +17,48 @@ export function getControls(): HTMLElement | false {
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
export function isVisible(element: HTMLElement): boolean {
|
||||
return element && element.offsetWidth > 0 && element.offsetHeight > 0;
|
||||
}
|
||||
|
||||
export function findValidElementFromSelector(selectors: string[]): HTMLElement {
|
||||
return findValidElementFromGenerator(selectors, (selector) => document.querySelector(selector));
|
||||
}
|
||||
|
||||
export function findValidElement(elements: HTMLElement[] | NodeListOf<HTMLElement>): HTMLElement {
|
||||
return findValidElementFromGenerator(elements);
|
||||
}
|
||||
|
||||
function findValidElementFromGenerator<T>(objects: T[] | NodeListOf<HTMLElement>, generator?: (obj: T) => HTMLElement): HTMLElement {
|
||||
for (const obj of objects) {
|
||||
const element = generator ? generator(obj as T) : obj as HTMLElement;
|
||||
if (element && isVisible(element)) {
|
||||
return element;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export function getHashParams(): Record<string, unknown> {
|
||||
const windowHash = window.location.hash.substr(1);
|
||||
if (windowHash) {
|
||||
const params: Record<string, unknown> = windowHash.split('&').reduce((acc, param) => {
|
||||
const [key, value] = param.split('=');
|
||||
const decoded = decodeURIComponent(value);
|
||||
try {
|
||||
acc[key] = decoded?.match(/{|\[/) ? JSON.parse(decoded) : value;
|
||||
} catch (e) {
|
||||
console.error(`Failed to parse hash parameter ${key}: ${value}`);
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
return params;
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
Reference in New Issue
Block a user