mirror of
https://github.com/ajayyy/SponsorBlock.git
synced 2025-12-18 05:28:32 +03:00
Merge remote-tracking branch 'origin/master' into settings
# Conflicts: # public/options/options.html # src/config.ts # src/content.ts # src/options.ts
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);
|
||||
});
|
||||
|
||||
107
src/components/CategoryPillComponent.tsx
Normal file
107
src/components/CategoryPillComponent.tsx
Normal file
@@ -0,0 +1,107 @@
|
||||
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: Config.config.barTypes["preview-" + this.state.segment?.category]?.color,
|
||||
display: this.state.show ? "flex" : "none",
|
||||
color: this.state.segment?.category === "sponsor" ? "white" : "black",
|
||||
}
|
||||
|
||||
return (
|
||||
<span style={style}
|
||||
className={"sponsorBlockCategoryPill"}
|
||||
title={chrome.i18n.getMessage("categoryPillTitleText")}
|
||||
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>
|
||||
</>
|
||||
)}
|
||||
</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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export default CategoryPillComponent;
|
||||
@@ -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 {
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import * as React from "react";
|
||||
import * as CompileConfig from "../../config.json";
|
||||
import Config from "../config"
|
||||
import { Category, ContentContainer, CategoryActionType, SponsorHideType, SponsorTime, NoticeVisbilityMode, ActionType } from "../types";
|
||||
import { Category, ContentContainer, CategoryActionType, SponsorHideType, SponsorTime, NoticeVisbilityMode, ActionType, SponsorSourceType, SegmentUUID } from "../types";
|
||||
import NoticeComponent from "./NoticeComponent";
|
||||
import NoticeTextSelectionComponent from "./NoticeTextSectionComponent";
|
||||
import Utils from "../utils";
|
||||
@@ -13,15 +13,7 @@ import { keybindToString } from "../utils/configUtils";
|
||||
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 +66,6 @@ class SkipNoticeComponent extends React.Component<SkipNoticeProps, SkipNoticeSta
|
||||
|
||||
amountOfPreviousNotices: number;
|
||||
showInSecondSlot: boolean;
|
||||
audio: HTMLAudioElement;
|
||||
|
||||
idSuffix: string;
|
||||
|
||||
@@ -96,7 +87,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 +146,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 +169,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 +178,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 +208,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 +271,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>
|
||||
@@ -534,10 +512,10 @@ class SkipNoticeComponent extends React.Component<SkipNoticeProps, SkipNoticeSta
|
||||
const sponsorVideoID = this.props.contentContainer().sponsorVideoID;
|
||||
const sponsorTimesSubmitting : SponsorTime = {
|
||||
segment: this.segments[index].segment,
|
||||
UUID: null,
|
||||
UUID: utils.generateUserID() as SegmentUUID,
|
||||
category: this.segments[index].category,
|
||||
actionType: this.segments[index].actionType,
|
||||
source: 2
|
||||
source: SponsorSourceType.Local
|
||||
};
|
||||
|
||||
const segmentTimes = Config.config.segmentTimes.get(sponsorVideoID) || [];
|
||||
@@ -741,16 +719,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, ContentContainer, SponsorTime } from "../types";
|
||||
import { ActionType, Category, CategoryActionType, ContentContainer, SponsorTime } from "../types";
|
||||
import Utils from "../utils";
|
||||
import { getCategoryActionType } from "../utils/categoryUtils";
|
||||
import SubmissionNoticeComponent from "./SubmissionNoticeComponent";
|
||||
@@ -40,6 +40,7 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
|
||||
|
||||
previousSkipType: CategoryActionType;
|
||||
timeBeforeChangingToPOI: number; // Initialized when first selecting POI
|
||||
fullVideoWarningShown = false;
|
||||
|
||||
constructor(props: SponsorTimeEditProps) {
|
||||
super(props);
|
||||
@@ -73,6 +74,8 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
|
||||
this.configUpdateListener = () => this.configUpdate();
|
||||
Config.configListeners.push(this.configUpdate.bind(this));
|
||||
}
|
||||
|
||||
this.checkToShowFullVideoWarning();
|
||||
}
|
||||
|
||||
componentWillUnmount(): void {
|
||||
@@ -82,6 +85,8 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
|
||||
}
|
||||
|
||||
render(): React.ReactElement {
|
||||
this.checkToShowFullVideoWarning();
|
||||
|
||||
const style: React.CSSProperties = {
|
||||
textAlign: "center"
|
||||
};
|
||||
@@ -100,11 +105,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}
|
||||
@@ -155,6 +163,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) +
|
||||
@@ -246,7 +255,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;
|
||||
@@ -254,6 +263,7 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
|
||||
this.setState({sponsorTimeEdits});
|
||||
this.saveEditTimes();
|
||||
}
|
||||
|
||||
changeTimesWhenScrolling(index: number, e: React.WheelEvent, sponsorTime: SponsorTime): void {
|
||||
let step = 0;
|
||||
// shift + ctrl = 1
|
||||
@@ -284,11 +294,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,
|
||||
@@ -296,10 +312,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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -444,6 +477,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()}
|
||||
@@ -123,7 +123,7 @@ class SubmissionNoticeComponent extends React.Component<SubmissionNoticeProps, S
|
||||
const timeRef = React.createRef<SponsorTimeEditComponent>();
|
||||
|
||||
elements.push(
|
||||
<SponsorTimeEditComponent key={i}
|
||||
<SponsorTimeEditComponent key={sponsorTimes[i].UUID}
|
||||
idSuffix={this.state.idSuffix + i}
|
||||
index={i}
|
||||
contentContainer={this.props.contentContainer}
|
||||
|
||||
@@ -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, Keybind } from "./types";
|
||||
import { keybindEquals } from "./utils/configUtils";
|
||||
|
||||
@@ -18,6 +19,7 @@ interface SBConfig {
|
||||
showTimeWithSkips: boolean,
|
||||
disableSkipping: boolean,
|
||||
muteSegments: boolean,
|
||||
fullVideoSegments: boolean,
|
||||
trackViewCount: boolean,
|
||||
trackViewCountInPrivate: boolean,
|
||||
dontShowNotice: boolean,
|
||||
@@ -49,7 +51,7 @@ interface SBConfig {
|
||||
locked: string
|
||||
},
|
||||
scrollToEditTimeUpdate: boolean,
|
||||
fillerUpdate: boolean,
|
||||
categoryPillUpdate: boolean,
|
||||
|
||||
skipKeybind: Keybind,
|
||||
startSponsorKeybind: Keybind,
|
||||
@@ -176,6 +178,7 @@ const Config: SBObject = {
|
||||
showTimeWithSkips: true,
|
||||
disableSkipping: false,
|
||||
muteSegments: true,
|
||||
fullVideoSegments: true,
|
||||
trackViewCount: true,
|
||||
trackViewCountInPrivate: true,
|
||||
dontShowNotice: false,
|
||||
@@ -187,7 +190,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,
|
||||
@@ -202,7 +205,7 @@ const Config: SBObject = {
|
||||
autoHideInfoButton: true,
|
||||
autoSkipOnMusicVideos: false,
|
||||
scrollToEditTimeUpdate: false, // false means the tooltip will be shown
|
||||
fillerUpdate: false,
|
||||
categoryPillUpdate: false,
|
||||
|
||||
/**
|
||||
* Default keybinds should not set "code" as that's gonna be different based on the user's locale. They should also only use EITHER ctrl OR alt modifiers (or none).
|
||||
@@ -402,6 +405,9 @@ function fetchConfig(): Promise<void> {
|
||||
}
|
||||
|
||||
function migrateOldFormats(config: SBConfig) {
|
||||
if (config["fillerUpdate"] !== undefined) {
|
||||
chrome.storage.sync.remove("fillerUpdate");
|
||||
}
|
||||
if (config["highlightCategoryAdded"] !== undefined) {
|
||||
chrome.storage.sync.remove("highlightCategoryAdded");
|
||||
}
|
||||
@@ -465,6 +471,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() {
|
||||
|
||||
219
src/content.ts
219
src/content.ts
@@ -11,14 +11,17 @@ 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, isVisible } from "./utils/pageUtils";
|
||||
import { keybindEquals } from "./utils/configUtils";
|
||||
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);
|
||||
@@ -76,9 +79,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;
|
||||
@@ -87,7 +92,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
|
||||
@@ -141,7 +147,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()
|
||||
@@ -267,11 +273,12 @@ function resetValues() {
|
||||
}
|
||||
|
||||
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;
|
||||
@@ -337,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 {
|
||||
@@ -401,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);
|
||||
@@ -422,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);
|
||||
@@ -533,7 +530,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))) {
|
||||
@@ -564,7 +561,7 @@ function setupVideoMutationListener() {
|
||||
}
|
||||
|
||||
function refreshVideoAttachments() {
|
||||
const newVideo = document.querySelector('video');
|
||||
const newVideo = findValidElement(document.querySelectorAll('video')) as HTMLVideoElement;
|
||||
if (newVideo && newVideo !== video) {
|
||||
video = newVideo;
|
||||
|
||||
@@ -573,12 +570,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) {
|
||||
@@ -661,8 +668,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);
|
||||
@@ -696,7 +711,7 @@ async function sponsorsLookup(id: string, keepOldSubmissions = true) {
|
||||
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.Skip],
|
||||
actionTypes: getEnabledActionTypes(),
|
||||
userAgent: `${chrome.runtime.id}`,
|
||||
...extraRequestData
|
||||
});
|
||||
@@ -777,6 +792,18 @@ async function sponsorsLookup(id: string, keepOldSubmissions = true) {
|
||||
lookupVipInformation(id);
|
||||
}
|
||||
|
||||
function getEnabledActionTypes(): ActionType[] {
|
||||
const actionTypes = [ActionType.Skip];
|
||||
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) {
|
||||
@@ -888,6 +915,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 {
|
||||
@@ -916,8 +948,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
|
||||
@@ -937,7 +991,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
|
||||
@@ -955,7 +1009,7 @@ function getYouTubeVideoID(url: string): string | boolean {
|
||||
console.error("[SB] Video ID not valid for " + url);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -987,6 +1041,7 @@ function updatePreviewBar(): void {
|
||||
segment: segment.segment as [number, number],
|
||||
category: segment.category,
|
||||
unsubmitted: false,
|
||||
actionType: segment.actionType,
|
||||
showLarger: getCategoryActionType(segment.category) === CategoryActionType.POI
|
||||
});
|
||||
});
|
||||
@@ -997,11 +1052,12 @@ function updatePreviewBar(): void {
|
||||
segment: segment.segment as [number, number],
|
||||
category: segment.category,
|
||||
unsubmitted: true,
|
||||
actionType: segment.actionType,
|
||||
showLarger: getCategoryActionType(segment.category) === CategoryActionType.POI
|
||||
});
|
||||
});
|
||||
|
||||
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));
|
||||
@@ -1246,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
|
||||
@@ -1368,7 +1429,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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1452,7 +1513,7 @@ function startOrEndTimingNewSegment() {
|
||||
if (!isSegmentCreationInProgress()) {
|
||||
sponsorTimesSubmitting.push({
|
||||
segment: [roundedTime],
|
||||
UUID: null,
|
||||
UUID: utils.generateUserID() as SegmentUUID,
|
||||
category: Config.config.defaultCategory,
|
||||
actionType: ActionType.Skip,
|
||||
source: SponsorSourceType.Local
|
||||
@@ -1648,17 +1709,41 @@ 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
|
||||
if (sponsorIndex == -1 || sponsorTimes[sponsorIndex].UUID === null) return;
|
||||
if (sponsorIndex == -1 || sponsorTimes[sponsorIndex].source === SponsorSourceType.Local) return;
|
||||
|
||||
// See if the local time saved count and skip count should be saved
|
||||
if (type === 0 && sponsorSkipped[sponsorIndex] || type === 1 && !sponsorSkipped[sponsorIndex]) {
|
||||
@@ -1674,33 +1759,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);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1743,7 +1809,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++) {
|
||||
@@ -1788,6 +1854,7 @@ async function sendSubmitMessage() {
|
||||
if (recievedNewSegments?.length === newSegments.length) {
|
||||
for (let i = 0; i < recievedNewSegments.length; i++) {
|
||||
newSegments[i].UUID = recievedNewSegments[i].UUID;
|
||||
newSegments[i].source = SponsorSourceType.Server;
|
||||
}
|
||||
}
|
||||
} catch(e) {} // eslint-disable-line no-empty
|
||||
@@ -1814,7 +1881,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));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1841,6 +1908,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);
|
||||
document.addEventListener("keyup", (e) => pressedKeys.delete(e.key));
|
||||
|
||||
@@ -6,6 +6,7 @@ https://github.com/videosegments/videosegments/commits/f1e111bdfe231947800c6efdd
|
||||
'use strict';
|
||||
|
||||
import Config from "../config";
|
||||
import { ActionType } from "../types";
|
||||
import Utils from "../utils";
|
||||
const utils = new Utils();
|
||||
|
||||
@@ -15,6 +16,7 @@ export interface PreviewBarSegment {
|
||||
segment: [number, number];
|
||||
category: string;
|
||||
unsubmitted: boolean;
|
||||
actionType: ActionType;
|
||||
showLarger: boolean;
|
||||
}
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import { getSkippingText } from "../utils/categoryUtils";
|
||||
import { keybindToString } from "../utils/configUtils";
|
||||
|
||||
import Utils from "../utils";
|
||||
import { AnimationUtils } from "../utils/animationUtils";
|
||||
const utils = new Utils();
|
||||
|
||||
export interface SkipButtonControlBarProps {
|
||||
@@ -81,9 +82,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;
|
||||
}
|
||||
@@ -105,7 +106,7 @@ export class SkipButtonControlBar {
|
||||
|
||||
this.refreshText();
|
||||
this.textContainer?.classList?.remove("hidden");
|
||||
utils.disableAutoHideAnimation(this.skipIcon);
|
||||
AnimationUtils.disableAutoHideAnimation(this.skipIcon);
|
||||
|
||||
this.startTimer();
|
||||
}
|
||||
@@ -161,7 +162,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;
|
||||
}
|
||||
@@ -3,6 +3,7 @@ import * as ReactDOM from "react-dom";
|
||||
|
||||
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;
|
||||
@@ -342,10 +343,9 @@ function updateDisplayElement(element: HTMLElement) {
|
||||
switch (displayOption) {
|
||||
case "invidiousInstances": {
|
||||
element.innerText = displayText.join(', ');
|
||||
const defaults = Config.defaults[displayOption];
|
||||
let allEquals = displayText.length == defaults.length;
|
||||
for (let i = 0; i < defaults.length && allEquals; i++) {
|
||||
if (displayText[i] != defaults[i])
|
||||
let allEquals = displayText.length == invidiousList.length;
|
||||
for (let i = 0; i < invidiousList.length && allEquals; i++) {
|
||||
if (displayText[i] != invidiousList[i])
|
||||
allEquals = false;
|
||||
}
|
||||
if (!allEquals) {
|
||||
@@ -404,8 +404,8 @@ function invidiousInstanceAddInit(element: HTMLElement, option: string) {
|
||||
|
||||
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;
|
||||
resetButton.classList.add("hidden");
|
||||
}
|
||||
});
|
||||
@@ -504,15 +504,17 @@ function activatePrivateTextChange(element: HTMLElement) {
|
||||
// See if anything extra must be done
|
||||
switch (option) {
|
||||
case "userID":
|
||||
utils.asyncRequestToServer("GET", "/api/userInfo", {
|
||||
userID: Config.config[option],
|
||||
values: ["warnings", "banned"]
|
||||
}).then((result) => {
|
||||
const userInfo = JSON.parse(result.responseText);
|
||||
if (userInfo.warnings > 0 || userInfo.banned) {
|
||||
setButton.classList.add("hidden");
|
||||
}
|
||||
});
|
||||
if (Config.config[option]) {
|
||||
utils.asyncRequestToServer("GET", "/api/userInfo", {
|
||||
userID: Config.config[option],
|
||||
values: ["warnings", "banned"]
|
||||
}).then((result) => {
|
||||
const userInfo = JSON.parse(result.responseText);
|
||||
if (userInfo.warnings > 0 || userInfo.banned) {
|
||||
setButton.classList.add("hidden");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
21
src/popup.ts
21
src/popup.ts
@@ -1,10 +1,12 @@
|
||||
import Config from "./config";
|
||||
|
||||
import Utils from "./utils";
|
||||
import { SponsorTime, SponsorHideType, CategoryActionType } from "./types";
|
||||
import { SponsorTime, SponsorHideType, CategoryActionType, ActionType } from "./types";
|
||||
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 {
|
||||
@@ -405,10 +407,15 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
|
||||
|
||||
const textNode = document.createTextNode(utils.shortCategoryName(segmentTimes[i].category) + 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(segmentTimes[i].category) !== CategoryActionType.POI
|
||||
? " " + chrome.i18n.getMessage("to") + " " + utils.getFormattedTime(segmentTimes[i].segment[1], true)
|
||||
: "");
|
||||
}
|
||||
|
||||
segmentTimeFromToNode.style.margin = "5px";
|
||||
|
||||
sponsorTimeButton.appendChild(categoryColorCircle);
|
||||
@@ -444,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();
|
||||
});
|
||||
|
||||
@@ -554,7 +561,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);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -595,7 +602,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);
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -698,7 +705,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,
|
||||
@@ -743,7 +750,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(() =>
|
||||
// YouTube, Mobile YouTube, Invidious
|
||||
document.querySelector(".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"
|
||||
|
||||
@@ -59,7 +59,8 @@ export enum CategoryActionType {
|
||||
|
||||
export enum ActionType {
|
||||
Skip = "skip",
|
||||
Mute = "mute"
|
||||
Mute = "mute",
|
||||
Full = "full"
|
||||
}
|
||||
|
||||
export const ActionTypes = [ActionType.Skip, ActionType.Mute];
|
||||
|
||||
126
src/utils.ts
126
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,11 +330,15 @@ 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");
|
||||
|
||||
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
|
||||
};
|
||||
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,27 @@ 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;
|
||||
}
|
||||
Reference in New Issue
Block a user