Merge branch 'master' of https://github.com/ajayyy/SponsorBlock into chapters

This commit is contained in:
Ajay
2022-01-16 15:58:09 -05:00
62 changed files with 2954 additions and 653 deletions

View File

@@ -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);
});

View 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;

View File

@@ -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>
);

View File

@@ -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 {

View File

@@ -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: {

View File

@@ -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 {

View File

@@ -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()}

View File

@@ -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() {

View File

@@ -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
});
}
}
}
}
}

View File

@@ -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();
}

View File

@@ -66,3 +66,8 @@ export type MessageResponse =
| IsChannelWhitelistedResponse
| Record<string, never>;
export interface VoteResponse {
successType: number;
statusCode: number;
responseText: string;
}

View File

@@ -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;
}
});
}

View File

@@ -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
View 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
});
}
}
}
}
}

View File

@@ -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[];

View File

@@ -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"

View File

@@ -62,6 +62,7 @@ export enum ActionType {
Skip = "skip",
Mute = "mute",
Chapter = "chapter",
Full = "full"
}
export const ActionTypes = [ActionType.Skip, ActionType.Mute];

View File

@@ -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++;
}
}
}

View 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
};

View File

@@ -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
View 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
View 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;
}
}

View File

@@ -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 {};
}