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:
Áron Hegymegi-Kiss
2022-01-08 19:30:49 +01:00
55 changed files with 2077 additions and 374 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,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;

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

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

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

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,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");

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

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