Merge branch 'master' into EthanBnntt-patch-1

This commit is contained in:
Ajay Ramachandran
2023-09-05 01:28:15 -04:00
committed by GitHub
147 changed files with 6012 additions and 45040 deletions

View File

@@ -2,13 +2,21 @@ import * as CompileConfig from "../config.json";
import Config from "./config";
import { Registration } from "./types";
import "content-scripts-register-polyfill";
import { sendRealRequestToCustomServer, setupBackgroundRequestProxy } from "../maze-utils/src/background-request-proxy";
import { setupTabUpdates } from "../maze-utils/src/tab-updates";
import { generateUserID } from "../maze-utils/src/setup";
// Make the config public for debugging purposes
window.SB = Config;
import Utils from "./utils";
import { GenericUtils } from "./utils/genericUtils";
import { getExtensionIdsToImportFrom } from "./utils/crossExtension";
import { isFirefoxOrSafari } from "../maze-utils/src";
import { injectUpdatedScripts } from "../maze-utils/src/cleanup";
import { logWarn } from "./utils/logger";
import { chromeP } from "../maze-utils/src/browserApi";
const utils = new Utils({
registerFirefoxContentScript,
unregisterFirefoxContentScript
@@ -20,65 +28,24 @@ const popupPort: Record<string, chrome.runtime.Port> = {};
const contentScriptRegistrations = {};
// Register content script if needed
if (utils.isFirefox()) {
utils.wait(() => Config.config !== null).then(function() {
if (Config.config.supportInvidious) utils.setupExtraSiteContentScripts();
});
}
function onTabUpdatedListener(tabId: number) {
chrome.tabs.sendMessage(tabId, {
message: 'update',
}, () => void chrome.runtime.lastError ); // Suppress error on Firefox
}
function onNavigationApiAvailableChange(changes: {[key: string]: chrome.storage.StorageChange}) {
if (changes.navigationApiAvailable) {
if (changes.navigationApiAvailable.newValue) {
chrome.tabs.onUpdated.removeListener(onTabUpdatedListener);
} else {
chrome.tabs.onUpdated.addListener(onTabUpdatedListener);
}
}
}
// If Navigation API is not supported, then background has to inform content script about video change.
// This happens on Safari, Firefox, and Chromium 101 (inclusive) and below.
chrome.tabs.onUpdated.addListener(onTabUpdatedListener);
utils.wait(() => Config.local !== null).then(() => {
if (Config.local.navigationApiAvailable) {
chrome.tabs.onUpdated.removeListener(onTabUpdatedListener);
}
utils.wait(() => Config.isReady()).then(function() {
if (Config.config.supportInvidious) utils.setupExtraSiteContentScripts();
});
if (!Config.configSyncListeners.includes(onNavigationApiAvailableChange)) {
Config.configSyncListeners.push(onNavigationApiAvailableChange);
}
setupBackgroundRequestProxy();
setupTabUpdates(Config);
chrome.runtime.onMessage.addListener(function (request, sender, callback) {
switch(request.message) {
case "openConfig":
chrome.tabs.create({url: chrome.runtime.getURL('options/options.html' + (request.hash ? '#' + request.hash : ''))});
return;
return false;
case "openHelp":
chrome.tabs.create({url: chrome.runtime.getURL('help/index.html')});
return;
case "openUpsell":
chrome.tabs.create({url: chrome.runtime.getURL('upsell/index.html')});
return;
return false;
case "openPage":
chrome.tabs.create({url: chrome.runtime.getURL(request.url)});
return;
case "sendRequest":
sendRequestToCustomServer(request.type, request.url, request.data).then(async (response) => {
callback({
responseText: await response.text(),
status: response.status,
ok: response.ok
});
});
return true;
return false;
case "submitVote":
submitVote(request.type, request.UUID, request.category).then(callback);
@@ -109,12 +76,32 @@ chrome.runtime.onMessage.addListener(function (request, sender, callback) {
case "infoUpdated":
case "videoChanged":
if (sender.tab) {
popupPort[sender.tab.id]?.postMessage(request);
try {
popupPort[sender.tab.id]?.postMessage(request);
} catch (e) {
// This can happen if the popup is closed
}
}
return false;
default:
return false;
}
});
chrome.runtime.onMessageExternal.addListener((request, sender, callback) => {
if (getExtensionIdsToImportFrom().includes(sender.id)) {
if (request.message === "requestConfig") {
callback({
userID: Config.config.userID,
allowExpirements: Config.config.allowExpirements,
showDonationLink: Config.config.showDonationLink,
showUpsells: Config.config.showUpsells,
darkMode: Config.config.darkMode,
})
}
}
});
chrome.runtime.onConnect.addListener((port) => {
if (port.name === "popup") {
chrome.tabs.query({
@@ -139,7 +126,7 @@ chrome.runtime.onInstalled.addListener(function () {
chrome.tabs.create({url: chrome.extension.getURL("/help/index.html")});
//generate a userID
const newUserID = GenericUtils.generateUserID();
const newUserID = generateUserID();
//save this UUID
Config.config.userID = newUserID;
@@ -153,6 +140,11 @@ chrome.runtime.onInstalled.addListener(function () {
}
}
}, 1500);
// Only do this once the old version understands how to clean itself up
if (!isFirefoxOrSafari() && chrome.runtime.getManifest().version !== "5.4.13") {
injectUpdatedScripts().catch(logWarn);
}
});
/**
@@ -161,26 +153,61 @@ chrome.runtime.onInstalled.addListener(function () {
*
* @param {JSON} options
*/
function registerFirefoxContentScript(options: Registration) {
const oldRegistration = contentScriptRegistrations[options.id];
if (oldRegistration) oldRegistration.unregister();
async function registerFirefoxContentScript(options: Registration) {
if ("scripting" in chrome && "getRegisteredContentScripts" in chrome.scripting) {
const existingRegistrations = await chromeP.scripting.getRegisteredContentScripts({
ids: [options.id]
}).catch(() => []);
if (existingRegistrations.length > 0
&& existingRegistrations[0].matches.every((match) => options.matches.includes(match))) {
// No need to register another script, already registered
return;
}
}
await unregisterFirefoxContentScript(options.id);
if ("scripting" in chrome && "getRegisteredContentScripts" in chrome.scripting) {
await chromeP.scripting.registerContentScripts([{
id: options.id,
runAt: "document_start",
matches: options.matches,
allFrames: options.allFrames,
js: options.js,
css: options.css,
persistAcrossSessions: true,
}]);
} else {
chrome.contentScripts.register({
allFrames: options.allFrames,
js: options.js?.map?.(file => ({file})),
css: options.css?.map?.(file => ({file})),
matches: options.matches
}).then((registration) => void (contentScriptRegistrations[options.id] = registration));
}
browser.contentScripts.register({
allFrames: options.allFrames,
js: options.js,
css: options.css,
matches: options.matches
}).then((registration) => void (contentScriptRegistrations[options.id] = registration));
}
/**
* Only works on Firefox.
* Firefox requires that this is handled by the background script
*
*/
function unregisterFirefoxContentScript(id: string) {
contentScriptRegistrations[id].unregister();
delete contentScriptRegistrations[id];
async function unregisterFirefoxContentScript(id: string) {
if ("scripting" in chrome && "getRegisteredContentScripts" in chrome.scripting) {
try {
await chromeP.scripting.unregisterContentScripts({
ids: [id]
});
} catch (e) {
// Not registered yet
}
} else {
if (contentScriptRegistrations[id]) {
contentScriptRegistrations[id].unregister();
delete contentScriptRegistrations[id];
}
}
}
async function submitVote(type: number, UUID: string, category: string) {
@@ -188,7 +215,7 @@ async function submitVote(type: number, UUID: string, category: string) {
if (userID == undefined || userID === "undefined") {
//generate one
userID = GenericUtils.generateUserID();
userID = generateUserID();
Config.config.userID = userID;
}
@@ -219,35 +246,9 @@ async function submitVote(type: number, UUID: string, category: string) {
}
}
async function asyncRequestToServer(type: string, address: string, data = {}) {
const serverAddress = Config.config.testingServer ? CompileConfig.testingServerAddress : Config.config.serverAddress;
return await (sendRequestToCustomServer(type, serverAddress + address, data));
}
/**
* Sends a request to the specified url
*
* @param type The request type "GET", "POST", etc.
* @param address The address to add to the SponsorBlock server address
* @param callback
*/
async function sendRequestToCustomServer(type: string, url: string, data = {}) {
// If GET, convert JSON to parameters
if (type.toLowerCase() === "get") {
url = GenericUtils.objectToURI(url, data, true);
data = null;
}
const response = await fetch(url, {
method: type,
headers: {
'Content-Type': 'application/json'
},
redirect: 'follow',
body: data ? JSON.stringify(data) : null
});
return response;
}
return await (sendRealRequestToCustomServer(type, serverAddress + address, data));
}

View File

@@ -7,11 +7,13 @@ 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";
import { Tooltip } from "../render/Tooltip";
import { getErrorMessage } from "../../maze-utils/src/formating";
export interface CategoryPillProps {
vote: (type: number, UUID: SegmentUUID, category?: Category) => Promise<VoteResponse>;
showTextByDefault: boolean;
showTooltipOnClick: boolean;
}
export interface CategoryPillState {
@@ -43,18 +45,23 @@ class CategoryPillComponent extends React.Component<CategoryPillProps, CategoryP
return (
<span style={style}
className={"sponsorBlockCategoryPill"}
className={"sponsorBlockCategoryPill" + (!this.props.showTextByDefault ? " sbPillNoText" : "")}
aria-label={this.getTitleText()}
onClick={(e) => this.toggleOpen(e)}
onMouseEnter={() => this.openTooltip()}
onMouseLeave={() => this.closeTooltip()}>
<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>
{
(this.props.showTextByDefault || this.state.open) &&
<span className="sponsorBlockCategoryPillTitle">
{chrome.i18n.getMessage("category_" + this.state.segment?.category)}
</span>
}
</span>
{this.state.open && (
@@ -81,7 +88,10 @@ class CategoryPillComponent extends React.Component<CategoryPillProps, CategoryP
{/* Close Button */}
<img src={chrome.extension.getURL("icons/close.png")}
className="categoryPillClose"
onClick={() => this.setState({ show: false })}>
onClick={() => {
this.setState({ show: false });
this.closeTooltip();
}}>
</img>
</span>
);
@@ -91,6 +101,14 @@ class CategoryPillComponent extends React.Component<CategoryPillProps, CategoryP
event.stopPropagation();
if (this.state.show) {
if (this.props.showTooltipOnClick) {
if (this.state.open) {
this.closeTooltip();
} else {
this.openTooltip();
}
}
this.setState({ open: !this.state.open });
}
}
@@ -104,58 +122,52 @@ class CategoryPillComponent extends React.Component<CategoryPillProps, CategoryP
await stopAnimation();
if (response.successType == 1 || (response.successType == -1 && response.statusCode == 429)) {
this.setState({
open: false,
this.setState({
open: false,
show: type === 1
});
this.closeTooltip();
} else if (response.statusCode !== 403) {
alert(GenericUtils.getErrorMessage(response.statusCode, response.responseText));
alert(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;
// Handled by setCategoryColorCSSVariables() of content.ts
const category = this.state.segment?.category;
return category == null ? null : `var(--sb-category-preview-${category}, var(--sb-category-${category}))`;
}
private getTextColor(): string {
const color = this.getColor();
if (!color) return null;
const existingCalculatedColor = Config.config.categoryPillColors[this.state.segment?.category];
if (existingCalculatedColor && existingCalculatedColor.lastColor === color) {
return existingCalculatedColor.textColor;
} else {
const luminance = GenericUtils.getLuminance(color);
const textColor = luminance > 128 ? "black" : "white";
Config.config.categoryPillColors[this.state.segment?.category] = {
lastColor: color,
textColor
};
return textColor;
}
// Handled by setCategoryColorCSSVariables() of content.ts
const category = this.state.segment?.category;
return category == null ? null : `var(--sb-category-text-preview-${category}, var(--sb-category-text-${category}))`;
}
private openTooltip(): void {
const tooltipMount = document.querySelector("ytd-video-primary-info-renderer > #container") as HTMLElement;
if (this.tooltip) {
this.tooltip.close();
}
const tooltipMount = document.querySelector("#above-the-fold, ytm-slim-owner-renderer") as HTMLElement;
if (tooltipMount) {
this.tooltip = new Tooltip({
text: this.getTitleText(),
referenceNode: tooltipMount,
bottomOffset: "70px",
bottomOffset: "0px",
opacity: 0.95,
displayTriangle: false,
showLogo: false,
showGotIt: false
showGotIt: false,
prependElement: tooltipMount.firstElementChild as HTMLElement
});
}
}
private closeTooltip(): void {
this.tooltip?.close();
this.tooltip?.close?.();
this.tooltip = null;
}

View File

@@ -1,14 +1,14 @@
import * as React from "react";
import Config from "../config";
import { Category, SegmentUUID, SponsorTime } from "../types";
import { ActionType, 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";
import { Tooltip } from "../render/Tooltip";
import { getErrorMessage } from "../../maze-utils/src/formating";
export interface ChapterVoteProps {
vote: (type: number, UUID: SegmentUUID, category?: Category) => Promise<VoteResponse>;
@@ -65,35 +65,39 @@ class ChapterVoteComponent extends React.Component<ChapterVoteProps, ChapterVote
this.tooltip.close();
this.tooltip = null;
} else {
const referenceNode = chapterNode?.parentElement?.parentElement;
if (referenceNode) {
const outerBounding = referenceNode.getBoundingClientRect();
const buttonBounding = (e.target as HTMLElement)?.parentElement?.getBoundingClientRect();
this.tooltip = new Tooltip({
referenceNode: chapterNode?.parentElement?.parentElement,
prependElement: chapterNode?.parentElement,
showLogo: false,
showGotIt: false,
bottomOffset: `${outerBounding.height + 25}px`,
leftOffset: `${buttonBounding.x - outerBounding.x}px`,
extraClass: "centeredSBTriangle",
buttons: [
{
name: chrome.i18n.getMessage("incorrectVote"),
listener: (event) => this.vote(event, 0, e.target as HTMLElement).then(() => {
this.tooltip?.close();
this.tooltip = null;
})
}, {
name: chrome.i18n.getMessage("harmfulVote"),
listener: (event) => this.vote(event, 30, e.target as HTMLElement).then(() => {
this.tooltip?.close();
this.tooltip = null;
})
}
]
});
if (this.state.segment?.actionType === ActionType.Chapter) {
const referenceNode = chapterNode?.parentElement?.parentElement;
if (referenceNode) {
const outerBounding = referenceNode.getBoundingClientRect();
const buttonBounding = (e.target as HTMLElement)?.parentElement?.getBoundingClientRect();
this.tooltip = new Tooltip({
referenceNode: chapterNode?.parentElement?.parentElement,
prependElement: chapterNode?.parentElement,
showLogo: false,
showGotIt: false,
bottomOffset: `${outerBounding.height + 25}px`,
leftOffset: `${buttonBounding.x - outerBounding.x}px`,
extraClass: "centeredSBTriangle",
buttons: [
{
name: chrome.i18n.getMessage("incorrectVote"),
listener: (event) => this.vote(event, 0, e.target as HTMLElement).then(() => {
this.tooltip?.close();
this.tooltip = null;
})
}, {
name: chrome.i18n.getMessage("harmfulVote"),
listener: (event) => this.vote(event, 30, e.target as HTMLElement).then(() => {
this.tooltip?.close();
this.tooltip = null;
})
}
]
});
}
} else {
this.vote(e, 0, e.target as HTMLElement)
}
}
}}>
@@ -120,7 +124,7 @@ class ChapterVoteComponent extends React.Component<ChapterVoteProps, ChapterVote
show: type === 1
});
} else if (response.statusCode !== 403) {
alert(GenericUtils.getErrorMessage(response.statusCode, response.responseText));
alert(getErrorMessage(response.statusCode, response.responseText));
}
}
}

View File

@@ -1,5 +1,6 @@
import * as React from "react";
import Config from "../config";
import SbSvg from "../svg-icons/sb_svg";
enum CountdownMode {
Timer,
@@ -28,6 +29,7 @@ export interface NoticeProps {
extraClass?: string;
hideLogo?: boolean;
hideRightInfo?: boolean;
logoFill?: string;
// Callback for when this is closed
closeListener: () => void;
@@ -122,10 +124,10 @@ class NoticeComponent extends React.Component<NoticeProps, NoticeState> {
<td className="noticeLeftIcon">
{/* Logo */}
{!this.props.hideLogo &&
<img id={"sponsorSkipLogo" + this.idSuffix}
className="sponsorSkipLogo sponsorSkipObject"
src={chrome.extension.getURL("icons/IconSponsorBlocker256px.png")}>
</img>
<SbSvg
id={"sponsorSkipLogo" + this.idSuffix}
fill={this.props.logoFill}
className="sponsorSkipLogo sponsorSkipObject"/>
}
<span id={"sponsorSkipMessage" + this.idSuffix}
@@ -195,7 +197,7 @@ class NoticeComponent extends React.Component<NoticeProps, NoticeState> {
id={"skipNoticeTimerText" + this.idSuffix}
key="skipNoticeTimerText"
className={this.state.countdownMode !== CountdownMode.Timer ? "hidden" : ""} >
{this.state.countdownTime + "s"}
{chrome.i18n.getMessage("NoticeTimeAfterSkip").replace("{seconds}", this.state.countdownTime.toString())}
</span>
),(
<img

View File

@@ -7,13 +7,13 @@ import NoticeTextSelectionComponent from "./NoticeTextSectionComponent";
import Utils from "../utils";
const utils = new Utils();
import { getSkippingText } from "../utils/categoryUtils";
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";
import { downvoteButtonColor, SkipNoticeAction } from "../utils/noticeUtils";
import { GenericUtils } from "../utils/genericUtils";
import { generateUserID } from "../../maze-utils/src/setup";
import { keybindToString } from "../../maze-utils/src/config";
enum SkipButtonState {
Undo, // Unskip
@@ -177,7 +177,8 @@ class SkipNoticeComponent extends React.Component<SkipNoticeProps, SkipNoticeSta
) : null;
return (
<NoticeComponent noticeTitle={this.state.noticeTitle}
<NoticeComponent
noticeTitle={this.state.noticeTitle}
amountOfPreviousNotices={this.amountOfPreviousNotices}
showInSecondSlot={this.showInSecondSlot}
idSuffix={this.idSuffix}
@@ -191,6 +192,7 @@ class SkipNoticeComponent extends React.Component<SkipNoticeProps, SkipNoticeSta
ref={this.noticeRef}
closeListener={() => this.closeListener()}
smaller={this.state.smaller}
logoFill={Config.config.barTypes[this.segments[0].category].color}
limitWidth={true}
firstColumn={firstColumn}
bottomRow={[...this.getMessageBoxes(), ...this.getBottomRow() ]}
@@ -373,6 +375,7 @@ class SkipNoticeComponent extends React.Component<SkipNoticeProps, SkipNoticeSta
</span>
);
}
return null;
}
getSubmissionChooser(): JSX.Element[] {
@@ -541,7 +544,7 @@ class SkipNoticeComponent extends React.Component<SkipNoticeProps, SkipNoticeSta
const sponsorVideoID = this.props.contentContainer().sponsorVideoID;
const sponsorTimesSubmitting : SponsorTime = {
segment: this.segments[index].segment,
UUID: GenericUtils.generateUserID() as SegmentUUID,
UUID: generateUserID() as SegmentUUID,
category: this.segments[index].category,
actionType: this.segments[index].actionType,
source: SponsorSourceType.Local

View File

@@ -6,10 +6,8 @@ import Utils from "../utils";
import SubmissionNoticeComponent from "./SubmissionNoticeComponent";
import { RectangleTooltip } from "../render/RectangleTooltip";
import SelectorComponent, { SelectorOption } from "./SelectorComponent";
import { GenericUtils } from "../utils/genericUtils";
import { noRefreshFetchingChaptersAllowed } from "../utils/licenseKey";
import { DEFAULT_CATEGORY } from "../utils/categoryUtils";
import { getFormattedTime, getFormattedTimeToSeconds } from "../../maze-utils/src/formating";
const utils = new Utils();
@@ -140,6 +138,8 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
type="text"
style={{color: "inherit", backgroundColor: "inherit"}}
value={this.state.sponsorTimeEdits[0]}
onKeyDown={(e) => e.stopPropagation()}
onKeyUp={(e) => e.stopPropagation()}
onChange={(e) => this.handleOnChange(0, e, sponsorTime, e.target.value)}
onWheel={(e) => this.changeTimesWhenScrolling(0, e, sponsorTime)}>
</input>
@@ -181,9 +181,9 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
style={timeDisplayStyle}
className="sponsorTimeDisplay"
onClick={this.toggleEditTime.bind(this)}>
{GenericUtils.getFormattedTime(segment[0], true) +
{getFormattedTime(segment[0], true) +
((!isNaN(segment[1]) && sponsorTime.actionType !== ActionType.Poi)
? " " + chrome.i18n.getMessage("to") + " " + GenericUtils.getFormattedTime(segment[1], true) : "")}
? " " + chrome.i18n.getMessage("to") + " " + getFormattedTime(segment[1], true) : "")}
</div>
);
}
@@ -308,8 +308,8 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
const sponsorTimeEdits = this.state.sponsorTimeEdits;
// check if change is small engough to show tooltip
const before = GenericUtils.getFormattedTimeToSeconds(sponsorTimeEdits[index]);
const after = GenericUtils.getFormattedTimeToSeconds(targetValue);
const before = getFormattedTimeToSeconds(sponsorTimeEdits[index]);
const after = getFormattedTimeToSeconds(targetValue);
const difference = Math.abs(before - after);
if (0 < difference && difference < 0.5) this.showScrollToEditToolTip();
@@ -320,6 +320,7 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
}
changeTimesWhenScrolling(index: number, e: React.WheelEvent, sponsorTime: SponsorTime): void {
if (!Config.config.allowScrollingToEdit) return;
let step = 0;
// shift + ctrl = 1
// ctrl = 0.1
@@ -332,7 +333,7 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
}
const sponsorTimeEdits = this.state.sponsorTimeEdits;
let timeAsNumber = GenericUtils.getFormattedTimeToSeconds(this.state.sponsorTimeEdits[index]);
let timeAsNumber = getFormattedTimeToSeconds(this.state.sponsorTimeEdits[index]);
if (timeAsNumber !== null && e.deltaY != 0) {
if (e.deltaY < 0) {
timeAsNumber += step;
@@ -342,7 +343,7 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
timeAsNumber = 0;
}
sponsorTimeEdits[index] = GenericUtils.getFormattedTime(timeAsNumber, true);
sponsorTimeEdits[index] = getFormattedTime(timeAsNumber, true);
if (sponsorTime.actionType === ActionType.Poi) sponsorTimeEdits[1] = sponsorTimeEdits[0];
this.setState({sponsorTimeEdits});
@@ -420,7 +421,7 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
// If permission not loaded, treat it like we have permission except chapter
const defaultBlockCategories = ["chapter"];
const permission = (Config.config.showCategoryWithoutPermission
|| Config.config.permissions[category as Category]) && (category !== "chapter" || noRefreshFetchingChaptersAllowed());
|| Config.config.permissions[category as Category]);
if ((defaultBlockCategories.includes(category)
|| (permission !== undefined && !Config.config.showCategoryWithoutPermission)) && !permission) continue;
@@ -574,8 +575,8 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
/** Returns an array in the sponsorTimeEdits form (formatted time string) from a normal seconds sponsor time */
getFormattedSponsorTimesEdits(sponsorTime: SponsorTime): [string, string] {
return [GenericUtils.getFormattedTime(sponsorTime.segment[0], true),
GenericUtils.getFormattedTime(sponsorTime.segment[1], true)];
return [getFormattedTime(sponsorTime.segment[0], true),
getFormattedTime(sponsorTime.segment[1], true)];
}
saveEditTimes(): void {
@@ -583,8 +584,8 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
const category = this.categoryOptionRef.current.value as Category
if (this.state.editing) {
const startTime = GenericUtils.getFormattedTimeToSeconds(this.state.sponsorTimeEdits[0]);
const endTime = GenericUtils.getFormattedTimeToSeconds(this.state.sponsorTimeEdits[1]);
const startTime = getFormattedTimeToSeconds(this.state.sponsorTimeEdits[0]);
const endTime = getFormattedTimeToSeconds(this.state.sponsorTimeEdits[1]);
// Change segment time only if the format was correct
if (startTime !== null && endTime !== null) {
@@ -595,7 +596,7 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
this.props.contentContainer().updateEditButtonsOnPlayer();
}
}
} else if (this.state.sponsorTimeEdits[1] === null && category === "outro") {
} else if (this.state.sponsorTimeEdits[1] === null && category === "outro" && !sponsorTimesSubmitting[this.props.index].segment[1]) {
sponsorTimesSubmitting[this.props.index].segment[1] = this.props.contentContainer().v.duration;
this.props.contentContainer().updateEditButtonsOnPlayer();
}
@@ -687,13 +688,13 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
descriptionUpdate(description: string): void {
this.setState({
description
}, () => {
this.saveEditTimes();
});
if (!this.fetchingSuggestions) {
this.fetchSuggestions(description);
}
this.saveEditTimes();
}
async fetchSuggestions(description: string): Promise<void> {

View File

@@ -1,35 +0,0 @@
import * as React from "react";
export interface TooltipProps {
text: string;
show: boolean;
}
export interface TooltipState {
}
class TooltipComponent extends React.Component<TooltipProps, TooltipState> {
constructor(props: TooltipProps) {
super(props);
}
render(): React.ReactElement {
const style: React.CSSProperties = {
display: this.props.show ? "flex" : "none",
position: "absolute",
}
return (
<span style={style}
className={"sponsorBlockTooltip"} >
<span className="sponsorBlockTooltipText">
{this.props.text}
</span>
</span>
);
}
}
export default TooltipComponent;

View File

@@ -6,8 +6,6 @@ import { Category, CategorySkipOption } from "../../types";
import { getCategorySuffix } from "../../utils/categoryUtils";
import ToggleOptionComponent from "./ToggleOptionComponent";
import { fetchingChaptersAllowed } from "../../utils/licenseKey";
import LockSvg from "../../svg-icons/lock_svg";
export interface CategorySkipOptionsProps {
category: Category;
@@ -19,7 +17,6 @@ export interface CategorySkipOptionsProps {
export interface CategorySkipOptionsState {
color: string;
previewColor: string;
hideChapter: boolean;
}
export interface ToggleOption {
@@ -37,29 +34,11 @@ class CategorySkipOptionsComponent extends React.Component<CategorySkipOptionsPr
// Setup state
this.state = {
color: props.defaultColor || Config.config.barTypes[this.props.category]?.color,
previewColor: props.defaultPreviewColor || Config.config.barTypes["preview-" + this.props.category]?.color,
hideChapter: true
previewColor: props.defaultPreviewColor || Config.config.barTypes["preview-" + this.props.category]?.color
};
fetchingChaptersAllowed().then((allowed) => {
this.setState({
hideChapter: !allowed
});
});
}
render(): React.ReactElement {
if (this.state.hideChapter) {
// Ensure force update refreshes this
fetchingChaptersAllowed().then((allowed) => {
if (allowed) {
this.setState({
hideChapter: !allowed
});
}
});
}
let defaultOption = "disable";
// Set the default opton properly
for (const categorySelection of Config.config.categorySelections) {
@@ -80,20 +59,10 @@ class CategorySkipOptionsComponent extends React.Component<CategorySkipOptionsPr
}
}
let extraClasses = "";
const disabled = this.props.category === "chapter" && this.state.hideChapter;
if (disabled) {
extraClasses += " disabled";
if (!Config.config.showUpsells) {
return <></>;
}
}
return (
<>
<tr id={this.props.category + "OptionsRow"}
className={`categoryTableElement${extraClasses}`} >
className={`categoryTableElement`} >
<td id={this.props.category + "OptionName"}
className="categoryTableLabel">
{chrome.i18n.getMessage("category_" + this.props.category)}
@@ -104,14 +73,9 @@ class CategorySkipOptionsComponent extends React.Component<CategorySkipOptionsPr
<select
className="optionsSelector"
defaultValue={defaultOption}
disabled={disabled}
onChange={this.skipOptionSelected.bind(this)}>
{this.getCategorySkipOptions()}
</select>
{disabled &&
<LockSvg className="upsellButton" onClick={() => chrome.tabs.create({url: chrome.runtime.getURL('upsell/index.html')})}/>
}
</td>
{this.props.category !== "chapter" &&
@@ -120,7 +84,6 @@ class CategorySkipOptionsComponent extends React.Component<CategorySkipOptionsPr
<input
className="categoryColorTextBox option-text-box"
type="color"
disabled={disabled}
onChange={(event) => this.setColorState(event, false)}
value={this.state.color} />
</td>
@@ -140,7 +103,7 @@ class CategorySkipOptionsComponent extends React.Component<CategorySkipOptionsPr
</tr>
<tr id={this.props.category + "DescriptionRow"}
className={`small-description categoryTableDescription${extraClasses}`}>
className={`small-description categoryTableDescription`}>
<td
colSpan={2}>
{chrome.i18n.getMessage("category_" + this.props.category + "_description")}
@@ -151,7 +114,7 @@ class CategorySkipOptionsComponent extends React.Component<CategorySkipOptionsPr
</td>
</tr>
{this.getExtraOptionComponents(this.props.category, extraClasses, disabled)}
{this.getExtraOptionComponents(this.props.category)}
</>
);
@@ -176,6 +139,12 @@ class CategorySkipOptionsComponent extends React.Component<CategorySkipOptionsPr
case "autoSkip":
option = CategorySkipOption.AutoSkip;
if (this.props.category === "filler" && !Config.config.isVip) {
if (!confirm(chrome.i18n.getMessage("FillerWarning"))) {
event.target.value = "disable";
}
}
break;
}
@@ -235,16 +204,15 @@ class CategorySkipOptionsComponent extends React.Component<CategorySkipOptionsPr
}, 50);
}
getExtraOptionComponents(category: string, extraClasses: string, disabled: boolean): JSX.Element[] {
getExtraOptionComponents(category: string): JSX.Element[] {
const result = [];
for (const option of this.getExtraOptions(category)) {
result.push(
<tr key={option.configKey} className={extraClasses}>
<tr key={option.configKey}>
<td id={`${category}_${option.configKey}`} className="categoryExtraOptions">
<ToggleOptionComponent
configKey={option.configKey}
label={option.label}
disabled={!option.dontDisable && disabled}
style={{width: "inherit"}}
/>
</td>

View File

@@ -1,9 +1,8 @@
import * as React from "react";
import { createRoot, Root } from 'react-dom/client';
import Config from "../../config";
import { Keybind } from "../../types";
import KeybindDialogComponent from "./KeybindDialogComponent";
import { keybindEquals, keybindToString, formatKey } from "../../utils/configUtils";
import { formatKey, Keybind, keybindEquals, keybindToString } from "../../../maze-utils/src/config";
export interface KeybindProps {
option: string;

View File

@@ -1,8 +1,7 @@
import * as React from "react";
import { ChangeEvent } from "react";
import Config from "../../config";
import { Keybind } from "../../types";
import { keybindEquals, formatKey } from "../../utils/configUtils";
import { Keybind, formatKey, keybindEquals } from "../../../maze-utils/src/config";
export interface KeybindDialogProps {
option: string;
@@ -124,7 +123,7 @@ class KeybindDialogComponent extends React.Component<KeybindDialogProps, Keybind
let youtubeShortcuts: Keybind[];
if (/[a-zA-Z0-9,.+\-\][:]/.test(this.state.key.key)) {
youtubeShortcuts = [{key: "k"}, {key: "j"}, {key: "l"}, {key: "p", shift: true}, {key: "n", shift: true}, {key: ","}, {key: "."}, {key: ",", shift: true}, {key: ".", shift: true},
{key: "ArrowRight"}, {key: "ArrowLeft"}, {key: "ArrowUp"}, {key: "ArrowDown"}, {key: "ArrowRight", ctrl: true}, {key: "ArrowLeft", ctrl: true}, {key: "c"}, {key: "o"},
{key: "ArrowRight"}, {key: "ArrowLeft"}, {key: "ArrowUp"}, {key: "ArrowDown"}, {key: "c"}, {key: "o"},
{key: "w"}, {key: "+"}, {key: "-"}, {key: "f"}, {key: "t"}, {key: "i"}, {key: "m"}, {key: "a"}, {key: "s"}, {key: "d"}, {key: "Home"}, {key: "End"},
{key: "0"}, {key: "1"}, {key: "2"}, {key: "3"}, {key: "4"}, {key: "5"}, {key: "6"}, {key: "7"}, {key: "8"}, {key: "9"}, {key: "]"}, {key: "["}];
} else {

View File

@@ -1,7 +1,8 @@
import * as CompileConfig from "../config.json";
import * as invidiousList from "../ci/invidiouslist.json";
import { Category, CategorySelection, CategorySkipOption, NoticeVisbilityMode, PreviewBarOption, SponsorTime, StorageChangesObject, Keybind, HashedValue, VideoID, SponsorHideType } from "./types";
import { keybindEquals } from "./utils/configUtils";
import { Category, CategorySelection, CategorySkipOption, NoticeVisbilityMode, PreviewBarOption, SponsorTime, VideoID, SponsorHideType } from "./types";
import { Keybind, ProtoConfig, keybindEquals } from "../maze-utils/src/config";
import { HashedValue } from "../maze-utils/src/hash";
export interface Permission {
canSubmit: boolean;
@@ -25,6 +26,7 @@ interface SBConfig {
disableSkipping: boolean;
muteSegments: boolean;
fullVideoSegments: boolean;
fullVideoLabelsOnThumbnails: boolean;
manualSkipOnFullVideo: boolean;
trackViewCount: boolean;
trackViewCountInPrivate: boolean;
@@ -52,6 +54,7 @@ interface SBConfig {
showDonationLink: boolean;
showPopupDonationCount: number;
showUpsells: boolean;
showNewFeaturePopups: boolean;
donateClicked: number;
autoHideInfoButton: boolean;
autoSkipOnMusicVideos: boolean;
@@ -68,6 +71,11 @@ interface SBConfig {
showCategoryWithoutPermission: boolean;
showSegmentNameInChapterBar: boolean;
useVirtualTime: boolean;
showSegmentFailedToFetchWarning: boolean;
allowScrollingToEdit: boolean;
deArrowInstalled: boolean;
showDeArrowPromotion: boolean;
showZoomToFillError2: boolean;
// Used to cache calculated text color info
categoryPillColors: {
@@ -119,7 +127,7 @@ interface SBConfig {
};
}
export type VideoDownvotes = { segments: { uuid: HashedValue; hidden: SponsorHideType }[] ; lastAccess: number };
export type VideoDownvotes = { segments: { uuid: HashedValue; hidden: SponsorHideType }[]; lastAccess: number };
interface SBStorage {
/* VideoID prefixes to UUID prefixes */
@@ -127,337 +135,36 @@ interface SBStorage {
navigationApiAvailable: boolean;
}
export interface SBObject {
configLocalListeners: Array<(changes: StorageChangesObject) => unknown>;
configSyncListeners: Array<(changes: StorageChangesObject) => unknown>;
syncDefaults: SBConfig;
localDefaults: SBStorage;
cachedSyncConfig: SBConfig;
cachedLocalStorage: SBStorage;
config: SBConfig;
local: SBStorage;
forceSyncUpdate(prop: string): void;
forceLocalUpdate(prop: string): void;
resetToDefault(): void;
}
const Config: SBObject = {
/**
* Callback function when an option is updated
*/
configLocalListeners: [],
configSyncListeners: [],
syncDefaults: {
userID: null,
isVip: false,
permissions: {},
unsubmittedSegments: {},
defaultCategory: "chooseACategory" as Category,
renderSegmentsAsChapters: false,
whitelistedChannels: [],
forceChannelCheck: false,
minutesSaved: 0,
skipCount: 0,
sponsorTimesContributed: 0,
submissionCountSinceCategories: 0,
showTimeWithSkips: true,
disableSkipping: false,
muteSegments: true,
fullVideoSegments: true,
manualSkipOnFullVideo: false,
trackViewCount: true,
trackViewCountInPrivate: true,
trackDownvotes: true,
dontShowNotice: false,
noticeVisibilityMode: NoticeVisbilityMode.FadedForAutoSkip,
hideVideoPlayerControls: false,
hideInfoButtonPlayerControls: false,
hideDeleteButtonPlayerControls: false,
hideUploadButtonPlayerControls: false,
hideSkipButtonPlayerControls: false,
hideDiscordLaunches: 0,
hideDiscordLink: false,
invidiousInstances: ["invidious.snopyta.org"], // leave as default
supportInvidious: false,
serverAddress: CompileConfig.serverAddress,
minDuration: 0,
skipNoticeDuration: 4,
audioNotificationOnSkip: false,
checkForUnlistedVideos: false,
testingServer: false,
refetchWhenNotFound: true,
ytInfoPermissionGranted: false,
allowExpirements: true,
showDonationLink: true,
showPopupDonationCount: 0,
showUpsells: true,
donateClicked: 0,
autoHideInfoButton: true,
autoSkipOnMusicVideos: false,
scrollToEditTimeUpdate: false, // false means the tooltip will be shown
categoryPillUpdate: false,
showChapterInfoMessage: true,
darkMode: true,
showCategoryGuidelines: true,
showCategoryWithoutPermission: false,
showSegmentNameInChapterBar: true,
useVirtualTime: true,
categoryPillColors: {},
/**
* 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).
* Using ctrl+alt, or shift may produce a different character that we will not be able to recognize in different locales.
* The exception for shift is letters, where it only capitalizes. So shift+A is fine, but shift+1 isn't.
* Don't forget to add the new keybind to the checks in "KeybindDialogComponent.isKeybindAvailable()" and in "migrateOldFormats()"!
* TODO: Find a way to skip having to update these checks. Maybe storing keybinds in a Map?
*/
skipKeybind: {key: "Enter"},
startSponsorKeybind: {key: ";"},
submitKeybind: {key: "'"},
nextChapterKeybind: {key: "]", ctrl: true},
previousChapterKeybind: {key: "[", ctrl: true},
categorySelections: [{
name: "sponsor" as Category,
option: CategorySkipOption.AutoSkip
}, {
name: "poi_highlight" as Category,
option: CategorySkipOption.ManualSkip
}, {
name: "exclusive_access" as Category,
option: CategorySkipOption.ShowOverlay
}],
payments: {
licenseKey: null,
lastCheck: 0,
lastFreeCheck: 0,
freeAccess: false,
chaptersAllowed: false
},
colorPalette: {
red: "#780303",
white: "#ffffff",
locked: "#ffc83d"
},
// Preview bar
barTypes: {
"preview-chooseACategory": {
color: "#ffffff",
opacity: "0.7"
},
"sponsor": {
color: "#00d400",
opacity: "0.7"
},
"preview-sponsor": {
color: "#007800",
opacity: "0.7"
},
"selfpromo": {
color: "#ffff00",
opacity: "0.7"
},
"preview-selfpromo": {
color: "#bfbf35",
opacity: "0.7"
},
"exclusive_access": {
color: "#008a5c",
opacity: "0.7"
},
"interaction": {
color: "#cc00ff",
opacity: "0.7"
},
"preview-interaction": {
color: "#6c0087",
opacity: "0.7"
},
"intro": {
color: "#00ffff",
opacity: "0.7"
},
"preview-intro": {
color: "#008080",
opacity: "0.7"
},
"outro": {
color: "#0202ed",
opacity: "0.7"
},
"preview-outro": {
color: "#000070",
opacity: "0.7"
},
"preview": {
color: "#008fd6",
opacity: "0.7"
},
"preview-preview": {
color: "#005799",
opacity: "0.7"
},
"music_offtopic": {
color: "#ff9900",
opacity: "0.7"
},
"preview-music_offtopic": {
color: "#a6634a",
opacity: "0.7"
},
"poi_highlight": {
color: "#ff1684",
opacity: "0.7"
},
"preview-poi_highlight": {
color: "#9b044c",
opacity: "0.7"
},
"filler": {
color: "#7300FF",
opacity: "0.9"
},
"preview-filler": {
color: "#2E0066",
opacity: "0.7"
}
}
},
localDefaults: {
downvotedSegments: {},
navigationApiAvailable: null
},
cachedSyncConfig: null,
cachedLocalStorage: null,
config: null,
local: null,
forceSyncUpdate,
forceLocalUpdate,
resetToDefault
};
// Function setup
function configProxy(): { sync: SBConfig; local: SBStorage } {
chrome.storage.onChanged.addListener((changes: {[key: string]: chrome.storage.StorageChange}, areaName) => {
if (areaName === "sync") {
for (const key in changes) {
Config.cachedSyncConfig[key] = changes[key].newValue;
}
for (const callback of Config.configSyncListeners) {
callback(changes);
}
} else if (areaName === "local") {
for (const key in changes) {
Config.cachedLocalStorage[key] = changes[key].newValue;
}
for (const callback of Config.configLocalListeners) {
callback(changes);
}
}
});
const syncHandler: ProxyHandler<SBConfig> = {
set<K extends keyof SBConfig>(obj: SBConfig, prop: K, value: SBConfig[K]) {
Config.cachedSyncConfig[prop] = value;
chrome.storage.sync.set({
[prop]: value
});
return true;
},
get<K extends keyof SBConfig>(obj: SBConfig, prop: K): SBConfig[K] {
const data = Config.cachedSyncConfig[prop];
return obj[prop] || data;
},
deleteProperty(obj: SBConfig, prop: keyof SBConfig) {
chrome.storage.sync.remove(<string> prop);
return true;
}
};
const localHandler: ProxyHandler<SBStorage> = {
set<K extends keyof SBStorage>(obj: SBStorage, prop: K, value: SBStorage[K]) {
Config.cachedLocalStorage[prop] = value;
chrome.storage.local.set({
[prop]: value
});
return true;
},
get<K extends keyof SBStorage>(obj: SBStorage, prop: K): SBStorage[K] {
const data = Config.cachedLocalStorage[prop];
return obj[prop] || data;
},
deleteProperty(obj: SBStorage, prop: keyof SBStorage) {
chrome.storage.local.remove(<string> prop);
return true;
}
};
return {
sync: new Proxy<SBConfig>({ handler: syncHandler } as unknown as SBConfig, syncHandler),
local: new Proxy<SBStorage>({ handler: localHandler } as unknown as SBStorage, localHandler)
};
}
function forceSyncUpdate(prop: string): void {
const value = Config.cachedSyncConfig[prop];
if (prop === "unsubmittedSegments") {
// Early to be safe
if (JSON.stringify(value).length + prop.length > 8000) {
for (const key in value) {
if (!value[key] || value[key].length <= 0) {
delete value[key];
}
}
}
class ConfigClass extends ProtoConfig<SBConfig, SBStorage> {
resetToDefault() {
chrome.storage.sync.set({
...this.syncDefaults,
userID: this.config.userID,
minutesSaved: this.config.minutesSaved,
skipCount: this.config.skipCount,
sponsorTimesContributed: this.config.sponsorTimesContributed
});
}
chrome.storage.sync.set({
[prop]: value
});
}
function forceLocalUpdate(prop: string): void {
chrome.storage.local.set({
[prop]: Config.cachedLocalStorage[prop]
});
}
async function fetchConfig(): Promise<void> {
await Promise.all([new Promise<void>((resolve) => {
chrome.storage.sync.get(null, function(items) {
Config.cachedSyncConfig = <SBConfig> <unknown> items;
resolve();
});
}), new Promise<void>((resolve) => {
chrome.storage.local.get(null, function(items) {
Config.cachedLocalStorage = <SBStorage> <unknown> items;
resolve();
});
})]);
}
function migrateOldSyncFormats(config: SBConfig) {
if (config["showZoomToFillError"]) {
chrome.storage.sync.remove("showZoomToFillError");
}
if (!config["chapterCategoryAdded"]) {
config["chapterCategoryAdded"] = true;
if (!config.categorySelections.some((s) => s.name === "chapter")) {
config.categorySelections.push({
name: "chapter" as Category,
option: CategorySkipOption.ShowOverlay
});
config.categorySelections = config.categorySelections;
}
}
if (config["segmentTimes"]) {
const unsubmittedSegments = {};
for (const item of config["segmentTimes"]) {
@@ -467,15 +174,8 @@ function migrateOldSyncFormats(config: SBConfig) {
chrome.storage.sync.remove("segmentTimes", () => config.unsubmittedSegments = unsubmittedSegments);
}
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["exclusive_accessCategoryAdded"] !== undefined) {
chrome.storage.sync.remove("exclusive_accessCategoryAdded");
}
if (config["fillerUpdate"] !== undefined) {
@@ -496,7 +196,7 @@ function migrateOldSyncFormats(config: SBConfig) {
config["autoSkipOnMusicVideosUpdate"] = true;
for (const selection of config.categorySelections) {
if (selection.name === "music_offtopic"
&& selection.option === CategorySkipOption.AutoSkip) {
&& selection.option === CategorySkipOption.AutoSkip) {
config.autoSkipOnMusicVideos = true;
break;
@@ -515,20 +215,20 @@ function migrateOldSyncFormats(config: SBConfig) {
}
if (typeof config["skipKeybind"] == "string") {
config["skipKeybind"] = {key: config["skipKeybind"]};
config["skipKeybind"] = { key: config["skipKeybind"] };
}
if (typeof config["startSponsorKeybind"] == "string") {
config["startSponsorKeybind"] = {key: config["startSponsorKeybind"]};
config["startSponsorKeybind"] = { key: config["startSponsorKeybind"] };
}
if (typeof config["submitKeybind"] == "string") {
config["submitKeybind"] = {key: config["submitKeybind"]};
config["submitKeybind"] = { key: config["submitKeybind"] };
}
// Unbind key if it matches a previous one set by the user (should be ordered oldest to newest)
const keybinds = ["skipKeybind", "startSponsorKeybind", "submitKeybind"];
for (let i = keybinds.length-1; i >= 0; i--) {
for (let i = keybinds.length - 1; i >= 0; i--) {
for (let j = 0; j < keybinds.length; j++) {
if (i == j)
continue;
@@ -546,59 +246,210 @@ function migrateOldSyncFormats(config: SBConfig) {
}
// populate invidiousInstances with new instances if 3p support is **DISABLED**
if (!config["supportInvidious"] && config["invidiousInstances"].length !== invidiousList.length) {
config["invidiousInstances"] = invidiousList;
if (!config["supportInvidious"] && config["invidiousInstances"].length < invidiousList.length) {
config["invidiousInstances"] = [...new Set([...invidiousList, ...config["invidiousInstances"]])];
}
if (config["lastIsVipUpdate"]) {
chrome.storage.sync.remove("lastIsVipUpdate");
}
}
async function setupConfig() {
if (typeof(chrome) === "undefined") return;
const syncDefaults = {
userID: null,
isVip: false,
permissions: {},
unsubmittedSegments: {},
defaultCategory: "chooseACategory" as Category,
renderSegmentsAsChapters: false,
whitelistedChannels: [],
forceChannelCheck: false,
minutesSaved: 0,
skipCount: 0,
sponsorTimesContributed: 0,
submissionCountSinceCategories: 0,
showTimeWithSkips: true,
disableSkipping: false,
muteSegments: true,
fullVideoSegments: true,
fullVideoLabelsOnThumbnails: true,
manualSkipOnFullVideo: false,
trackViewCount: true,
trackViewCountInPrivate: true,
trackDownvotes: true,
dontShowNotice: false,
noticeVisibilityMode: NoticeVisbilityMode.FadedForAutoSkip,
hideVideoPlayerControls: false,
hideInfoButtonPlayerControls: false,
hideDeleteButtonPlayerControls: false,
hideUploadButtonPlayerControls: false,
hideSkipButtonPlayerControls: false,
hideDiscordLaunches: 0,
hideDiscordLink: false,
invidiousInstances: ["invidious.snopyta.org"], // leave as default
supportInvidious: false,
serverAddress: CompileConfig.serverAddress,
minDuration: 0,
skipNoticeDuration: 4,
audioNotificationOnSkip: false,
checkForUnlistedVideos: false,
testingServer: false,
refetchWhenNotFound: true,
ytInfoPermissionGranted: false,
allowExpirements: true,
showDonationLink: true,
showPopupDonationCount: 0,
showUpsells: true,
showNewFeaturePopups: true,
donateClicked: 0,
autoHideInfoButton: true,
autoSkipOnMusicVideos: false,
scrollToEditTimeUpdate: false, // false means the tooltip will be shown
categoryPillUpdate: false,
showChapterInfoMessage: true,
darkMode: true,
showCategoryGuidelines: true,
showCategoryWithoutPermission: false,
showSegmentNameInChapterBar: true,
useVirtualTime: true,
showSegmentFailedToFetchWarning: true,
allowScrollingToEdit: true,
deArrowInstalled: false,
showDeArrowPromotion: true,
showZoomToFillError2: true,
await fetchConfig();
addDefaults();
const config = configProxy();
migrateOldSyncFormats(config.sync);
categoryPillColors: {},
Config.config = config.sync;
Config.local = config.local;
}
/**
* 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).
* Using ctrl+alt, or shift may produce a different character that we will not be able to recognize in different locales.
* The exception for shift is letters, where it only capitalizes. So shift+A is fine, but shift+1 isn't.
* Don't forget to add the new keybind to the checks in "KeybindDialogComponent.isKeybindAvailable()" and in "migrateOldFormats()"!
* TODO: Find a way to skip having to update these checks. Maybe storing keybinds in a Map?
*/
skipKeybind: { key: "Enter" },
startSponsorKeybind: { key: ";" },
submitKeybind: { key: "'" },
nextChapterKeybind: { key: "ArrowRight", ctrl: true },
previousChapterKeybind: { key: "ArrowLeft", ctrl: true },
// Add defaults
function addDefaults() {
for (const key in Config.syncDefaults) {
if(!Object.prototype.hasOwnProperty.call(Config.cachedSyncConfig, key)) {
Config.cachedSyncConfig[key] = Config.syncDefaults[key];
} else if (key === "barTypes") {
for (const key2 in Config.syncDefaults[key]) {
if(!Object.prototype.hasOwnProperty.call(Config.cachedSyncConfig[key], key2)) {
Config.cachedSyncConfig[key][key2] = Config.syncDefaults[key][key2];
}
}
categorySelections: [{
name: "sponsor" as Category,
option: CategorySkipOption.AutoSkip
}, {
name: "poi_highlight" as Category,
option: CategorySkipOption.ManualSkip
}, {
name: "exclusive_access" as Category,
option: CategorySkipOption.ShowOverlay
}, {
name: "chapter" as Category,
option: CategorySkipOption.ShowOverlay
}],
payments: {
licenseKey: null,
lastCheck: 0,
lastFreeCheck: 0,
freeAccess: false,
chaptersAllowed: false
},
colorPalette: {
red: "#780303",
white: "#ffffff",
locked: "#ffc83d"
},
// Preview bar
barTypes: {
"preview-chooseACategory": {
color: "#ffffff",
opacity: "0.7"
},
"sponsor": {
color: "#00d400",
opacity: "0.7"
},
"preview-sponsor": {
color: "#007800",
opacity: "0.7"
},
"selfpromo": {
color: "#ffff00",
opacity: "0.7"
},
"preview-selfpromo": {
color: "#bfbf35",
opacity: "0.7"
},
"exclusive_access": {
color: "#008a5c",
opacity: "0.7"
},
"interaction": {
color: "#cc00ff",
opacity: "0.7"
},
"preview-interaction": {
color: "#6c0087",
opacity: "0.7"
},
"intro": {
color: "#00ffff",
opacity: "0.7"
},
"preview-intro": {
color: "#008080",
opacity: "0.7"
},
"outro": {
color: "#0202ed",
opacity: "0.7"
},
"preview-outro": {
color: "#000070",
opacity: "0.7"
},
"preview": {
color: "#008fd6",
opacity: "0.7"
},
"preview-preview": {
color: "#005799",
opacity: "0.7"
},
"music_offtopic": {
color: "#ff9900",
opacity: "0.7"
},
"preview-music_offtopic": {
color: "#a6634a",
opacity: "0.7"
},
"poi_highlight": {
color: "#ff1684",
opacity: "0.7"
},
"preview-poi_highlight": {
color: "#9b044c",
opacity: "0.7"
},
"filler": {
color: "#7300FF",
opacity: "0.9"
},
"preview-filler": {
color: "#2E0066",
opacity: "0.7"
}
}
};
for (const key in Config.localDefaults) {
if(!Object.prototype.hasOwnProperty.call(Config.cachedLocalStorage, key)) {
Config.cachedLocalStorage[key] = Config.localDefaults[key];
}
}
}
const localDefaults = {
downvotedSegments: {},
navigationApiAvailable: null
};
function resetToDefault() {
chrome.storage.sync.set({
...Config.syncDefaults,
userID: Config.config.userID,
minutesSaved: Config.config.minutesSaved,
skipCount: Config.config.skipCount,
sponsorTimesContributed: Config.config.sponsorTimesContributed
});
}
// Sync config
setupConfig();
export default Config;
const Config = new ConfigClass(syncDefaults, localDefaults, migrateOldSyncFormats);
export default Config;

File diff suppressed because it is too large Load Diff

View File

@@ -1,93 +1,3 @@
/*
Content script are run in an isolated DOM so it is not possible to access some key details that are sanitized when passed cross-dom
This script is used to get the details from the page and make them available for the content script by being injected directly into the page
*/
import { init } from "../maze-utils/src/injected/document";
import { PageType } from "./types";
interface StartMessage {
type: "navigation";
pageType: PageType;
videoID: string | null;
}
interface FinishMessage extends StartMessage {
channelID: string;
channelTitle: string;
}
interface AdMessage {
type: "ad";
playing: boolean;
}
interface VideoData {
type: "data";
videoID: string;
isLive: boolean;
isPremiere: boolean;
}
type WindowMessage = StartMessage | FinishMessage | AdMessage | VideoData;
// global playerClient - too difficult to type
// eslint-disable-next-line @typescript-eslint/no-explicit-any
let playerClient: any;
const sendMessage = (message: WindowMessage): void => {
window.postMessage({ source: "sponsorblock", ...message }, "/");
}
function setupPlayerClient(e: CustomEvent): void {
if (playerClient) return; // early exit if already defined
playerClient = e.detail;
sendVideoData(); // send playerData after setup
e.detail.addEventListener('onAdStart', () => sendMessage({ type: "ad", playing: true } as AdMessage));
e.detail.addEventListener('onAdFinish', () => sendMessage({ type: "ad", playing: false } as AdMessage));
}
document.addEventListener("yt-player-updated", setupPlayerClient);
document.addEventListener("yt-navigate-start", navigationStartSend);
document.addEventListener("yt-navigate-finish", navigateFinishSend);
function navigationParser(event: CustomEvent): StartMessage {
const pageType: PageType = event.detail.pageType;
if (pageType) {
const result: StartMessage = { type: "navigation", pageType, videoID: null };
if (pageType === "shorts" || pageType === "watch") {
const endpoint = event.detail.endpoint
if (!endpoint) return null;
result.videoID = (pageType === "shorts" ? endpoint.reelWatchEndpoint : endpoint.watchEndpoint).videoId;
}
return result;
} else {
return null;
}
}
function navigationStartSend(event: CustomEvent): void {
const message = navigationParser(event) as StartMessage;
if (message) {
sendMessage(message);
}
}
function navigateFinishSend(event: CustomEvent): void {
sendVideoData(); // arrived at new video, send video data
const videoDetails = event.detail?.response?.playerResponse?.videoDetails;
if (videoDetails) {
sendMessage({ channelID: videoDetails.channelId, channelTitle: videoDetails.author, ...navigationParser(event) } as FinishMessage);
}
}
function sendVideoData(): void {
if (!playerClient) return;
const videoData = playerClient.getVideoData();
if (videoData) {
sendMessage({ type: "data", videoID: videoData.video_id, isLive: videoData.isLive, isPremiere: videoData.isPremiere } as VideoData);
}
}
init();

View File

@@ -1,15 +1,19 @@
import { localizeHtmlPage } from "../maze-utils/src/setup";
import Config from "./config";
import { showDonationLink } from "./utils/configUtils";
import { localizeHtmlPage } from "./utils/pageUtils";
import { GenericUtils } from "./utils/genericUtils";
import { waitFor } from "../maze-utils/src";
window.addEventListener('DOMContentLoaded', init);
if (document.readyState === "complete") {
init();
} else {
document.addEventListener("DOMContentLoaded", init);
}
async function init() {
localizeHtmlPage();
await GenericUtils.wait(() => Config.config !== null);
await waitFor(() => Config.config !== null);
if (!Config.config.darkMode) {
document.documentElement.setAttribute("data-theme", "light");

View File

@@ -10,8 +10,10 @@ import { ChapterVote } from "../render/ChapterVote";
import { ActionType, Category, SegmentContainer, SponsorHideType, SponsorSourceType, SponsorTime } from "../types";
import { partition } from "../utils/arrayUtils";
import { DEFAULT_CATEGORY, shortCategoryName } from "../utils/categoryUtils";
import { GenericUtils } from "../utils/genericUtils";
import { findValidElement } from "../utils/pageUtils";
import { normalizeChapterName } from "../utils/exporter";
import { getFormattedTimeToSeconds } from "../../maze-utils/src/formating";
import { findValidElement } from "../../maze-utils/src/dom";
import { addCleanupListener } from "../../maze-utils/src/cleanup";
const TOOLTIP_VISIBLE_CLASS = 'sponsorCategoryTooltipVisible';
const MIN_CHAPTER_SIZE = 0.003;
@@ -37,6 +39,10 @@ class PreviewBar {
categoryTooltip?: HTMLDivElement;
categoryTooltipContainer?: HTMLElement;
chapterTooltip?: HTMLDivElement;
lastSmallestSegment: Record<string, {
index: number;
segment: PreviewBarSegment;
}> = {};
parent: HTMLElement;
onMobileYouTube: boolean;
@@ -57,6 +63,7 @@ class PreviewBar {
originalChapterBar: HTMLElement;
originalChapterBarBlocks: NodeListOf<HTMLElement>;
chapterMargin: number;
lastRenderedSegments: PreviewBarSegment[];
unfilteredChapterGroups: ChapterGroup[];
chapterGroups: ChapterGroup[];
@@ -91,7 +98,9 @@ class PreviewBar {
this.chapterTooltip = document.createElement("div");
this.chapterTooltip.className = "ytp-tooltip-title sponsorCategoryTooltip";
const tooltipTextWrapper = document.querySelector(".ytp-tooltip-text-wrapper");
// global chaper tooltip or duration tooltip
const tooltipTextWrapper = document.querySelector(".ytp-tooltip-text-wrapper") ?? document.querySelector("#progress-bar-container.ytk-player > #hover-time-info");
const originalTooltip = tooltipTextWrapper.querySelector(".ytp-tooltip-title:not(.sponsorCategoryTooltip)") as HTMLElement;
if (!tooltipTextWrapper || !tooltipTextWrapper.parentElement) return;
// Grab the tooltip from the text wrapper as the tooltip doesn't have its classes on init
@@ -118,8 +127,8 @@ class PreviewBar {
const observer = new MutationObserver((mutations) => {
if (!mouseOnSeekBar || !this.categoryTooltip || !this.categoryTooltipContainer) return;
// If the mutation observed is only for our tooltip text, ignore
if (mutations.some((mutation) => (mutation.target as HTMLElement).classList.contains("sponsorCategoryTooltip"))) {
// Only care about mutations to time tooltip
if (!mutations.some((mutation) => (mutation.target as HTMLElement).classList.contains("ytp-tooltip-text"))) {
return;
}
@@ -133,19 +142,23 @@ class PreviewBar {
const tooltipText = tooltipTextElement.textContent;
if (tooltipText === null || tooltipText.length === 0) continue;
timeInSeconds = GenericUtils.getFormattedTimeToSeconds(tooltipText);
timeInSeconds = getFormattedTimeToSeconds(tooltipText);
if (timeInSeconds !== null) break;
}
if (timeInSeconds === null) return;
if (timeInSeconds === null) {
originalTooltip.style.removeProperty("display");
return;
}
// Find the segment at that location, using the shortest if multiple found
const [normalSegments, chapterSegments] =
partition(this.segments.filter((s) => s.source !== SponsorSourceType.YouTube),
const [normalSegments, chapterSegments] =
partition(this.segments.filter((s) => s.source !== SponsorSourceType.YouTube),
(segment) => segment.actionType !== ActionType.Chapter);
let mainSegment = this.getSmallestSegment(timeInSeconds, normalSegments);
let secondarySegment = this.getSmallestSegment(timeInSeconds, chapterSegments);
let mainSegment = this.getSmallestSegment(timeInSeconds, normalSegments, "normal");
let secondarySegment = this.getSmallestSegment(timeInSeconds, chapterSegments, "chapter");
if (mainSegment === null && secondarySegment !== null) {
mainSegment = secondarySegment;
secondarySegment = this.getSmallestSegment(timeInSeconds, chapterSegments.filter((s) => s !== secondarySegment));
@@ -153,6 +166,7 @@ class PreviewBar {
if (mainSegment === null && secondarySegment === null) {
this.categoryTooltipContainer.classList.remove(TOOLTIP_VISIBLE_CLASS);
originalTooltip.style.removeProperty("display");
} else {
this.categoryTooltipContainer.classList.add(TOOLTIP_VISIBLE_CLASS);
if (mainSegment !== null && secondarySegment !== null) {
@@ -164,6 +178,14 @@ class PreviewBar {
this.setTooltipTitle(mainSegment, this.categoryTooltip);
this.setTooltipTitle(secondarySegment, this.chapterTooltip);
if (normalizeChapterName(originalTooltip.textContent) === normalizeChapterName(this.categoryTooltip.textContent)
|| normalizeChapterName(originalTooltip.textContent) === normalizeChapterName(this.chapterTooltip.textContent)) {
if (originalTooltip.style.display !== "none") originalTooltip.style.display = "none";
noYoutubeChapters = true;
} else if (originalTooltip.style.display === "none") {
originalTooltip.style.removeProperty("display");
}
// Used to prevent overlapping
this.categoryTooltip.classList.toggle("ytp-tooltip-text-no-title", noYoutubeChapters);
this.chapterTooltip.classList.toggle("ytp-tooltip-text-no-title", noYoutubeChapters);
@@ -171,6 +193,8 @@ class PreviewBar {
// To prevent offset issue
this.categoryTooltip.style.right = titleTooltip.style.right;
this.chapterTooltip.style.right = titleTooltip.style.right;
this.categoryTooltip.style.textAlign = titleTooltip.style.textAlign;
this.chapterTooltip.style.textAlign = titleTooltip.style.textAlign;
}
});
@@ -178,6 +202,10 @@ class PreviewBar {
childList: true,
subtree: true,
});
addCleanupListener(() => {
observer.disconnect();
});
}
private setTooltipTitle(segment: PreviewBarSegment, tooltip: HTMLElement): void {
@@ -270,7 +298,7 @@ class PreviewBar {
if (this.originalChapterBar) {
this.originalChapterBarBlocks = this.originalChapterBar.querySelectorAll(":scope > div") as NodeListOf<HTMLElement>
this.existingChapters = this.segments.filter((s) => s.source === SponsorSourceType.YouTube).sort((a, b) => a.segment[0] - b.segment[0]);
if (this.existingChapters?.length > 0) {
const margin = parseFloat(this.originalChapterBarBlocks?.[0]?.style?.marginRight?.replace("px", ""));
if (margin) this.chapterMargin = margin;
@@ -292,7 +320,7 @@ class PreviewBar {
if (chapterChevron) {
if (this.segments.some((segment) => segment.source === SponsorSourceType.YouTube)) {
chapterChevron.style.removeProperty("display");
} else {
} else if (this.segments) {
chapterChevron.style.display = "none";
}
}
@@ -309,18 +337,22 @@ class PreviewBar {
const fullCategoryName = (unsubmitted ? 'preview-' : '') + category;
bar.setAttribute('sponsorblock-category', fullCategoryName);
bar.style.backgroundColor = Config.config.barTypes[fullCategoryName]?.color;
// Handled by setCategoryColorCSSVariables() of content.ts
bar.style.backgroundColor = `var(--sb-category-${fullCategoryName})`;
if (!this.onMobileYouTube) bar.style.opacity = Config.config.barTypes[fullCategoryName]?.opacity;
bar.style.position = "absolute";
const duration = Math.min(segment[1], this.videoDuration) - segment[0];
const startTime = segment[1] ? Math.min(this.videoDuration, segment[0]) : segment[0];
const endTime = Math.min(this.videoDuration, segment[1]);
bar.style.left = this.timeToPercentage(startTime);
if (duration > 0) {
bar.style.width = `calc(${this.intervalToPercentage(segment[0], segment[1])}${
this.chapterFilter(barSegment) && segment[1] < this.videoDuration ? ` - ${this.chapterMargin}px` : ''})`;
bar.style.right = this.timeToPercentage(this.videoDuration - endTime);
}
if (this.chapterFilter(barSegment) && segment[1] < this.videoDuration) {
bar.style.marginRight = `${this.chapterMargin}px`;
}
const time = segment[1] ? Math.min(this.videoDuration, segment[0]) : segment[0];
bar.style.left = this.timeToPercentage(time);
return bar;
}
@@ -335,12 +367,17 @@ class PreviewBar {
return;
}
// Merge overlapping chapters
this.unfilteredChapterGroups = this.createChapterRenderGroups(segments);
const remakingBar = segments !== this.lastRenderedSegments;
if (remakingBar) {
this.lastRenderedSegments = segments;
if (segments.every((segments) => segments.source === SponsorSourceType.YouTube)
|| (!Config.config.renderSegmentsAsChapters
&& segments.every((segment) => segment.actionType !== ActionType.Chapter
// Merge overlapping chapters
this.unfilteredChapterGroups = this.createChapterRenderGroups(segments);
}
if (segments.every((segments) => segments.source === SponsorSourceType.YouTube)
|| (!Config.config.renderSegmentsAsChapters
&& segments.every((segment) => segment.actionType !== ActionType.Chapter
|| segment.source === SponsorSourceType.YouTube))) {
if (this.customChaptersBar) this.customChaptersBar.style.display = "none";
this.originalChapterBar.style.removeProperty("display");
@@ -423,7 +460,9 @@ class PreviewBar {
}
}
this.updateChapterAllMutation(this.originalChapterBar, this.progressBar, true);
if (remakingBar) {
this.updateChapterAllMutation(this.originalChapterBar, this.progressBar, true);
}
}
createChapterRenderGroups(segments: PreviewBarSegment[]): ChapterGroup[] {
@@ -433,7 +472,7 @@ class PreviewBar {
const latestChapter = result[result.length - 1];
if (latestChapter && latestChapter.segment[1] > segment.segment[0]) {
const segmentDuration = segment.segment[1] - segment.segment[0];
if (segment.segment[0] < latestChapter.segment[0]
if (segment.segment[0] < latestChapter.segment[0]
|| segmentDuration < latestChapter.originalDuration) {
// Remove latest if it starts too late
let latestValidChapter = latestChapter;
@@ -581,6 +620,7 @@ class PreviewBar {
for (const mutation of mutations) {
if (mutation.type === "childList") {
this.update();
break;
}
}
@@ -591,6 +631,11 @@ class PreviewBar {
childListObserver.observe(this.originalChapterBar, {
childList: true
});
addCleanupListener(() => {
attributeObserver.disconnect();
childListObserver.disconnect();
});
}
private updateChapterAllMutation(originalChapterBar: HTMLElement, progressBar: HTMLElement, firstUpdate = false): void {
@@ -634,7 +679,7 @@ class PreviewBar {
if (changedData.scale !== null) {
const transformScale = (changedData.scale) / progressBar.clientWidth;
customChangedElement.style.transform =
customChangedElement.style.transform =
`scaleX(${Math.max(0, Math.min(1 - calculatedLeft, (transformScale - cursor) / fullSectionWidth - calculatedLeft))}`;
if (firstUpdate) {
customChangedElement.style.transition = "none";
@@ -651,7 +696,7 @@ class PreviewBar {
cursor += sectionWidthDecimal;
}
if (sections.length !== 0 && sections.length !== this.existingChapters?.length
if (sections.length !== 0 && sections.length !== this.existingChapters?.length
&& Date.now() - this.lastChapterUpdate > 3000) {
this.lastChapterUpdate = Date.now();
this.updateExistingChapters();
@@ -659,7 +704,7 @@ class PreviewBar {
}
}
private findLeftAndScale(selector: string, currentElement: HTMLElement, progressBar: HTMLElement):
private findLeftAndScale(selector: string, currentElement: HTMLElement, progressBar: HTMLElement):
{ left: number; scale: number } {
const sections = currentElement.parentElement.parentElement.parentElement.children;
let currentWidth = 0;
@@ -676,8 +721,8 @@ class PreviewBar {
for (let i = 0; i < sections.length; i++) {
const section = sections[i] as HTMLElement;
const checkElement = section.querySelector(selector) as HTMLElement;
const currentSectionWidthNoMargin = this.getPartialChapterSectionStyle(section, "width") || progressBar.clientWidth;
const currentSectionWidth = currentSectionWidthNoMargin
const currentSectionWidthNoMargin = this.getPartialChapterSectionStyle(section, "width") ?? progressBar.clientWidth;
const currentSectionWidth = currentSectionWidthNoMargin
+ this.getPartialChapterSectionStyle(section, "marginRight");
// First check for left
@@ -720,37 +765,40 @@ class PreviewBar {
currentWidth += lastWidth;
}
return {
left: left + leftPosition,
return {
left: left + leftPosition,
scale: scale !== null ? scale * scaleWidth + scalePosition : null
};
}
private getPartialChapterSectionStyle(element: HTMLElement, param: string): number {
const data = element.style[param];
if (data?.includes("100%")) {
return 0;
if (data?.includes("%")) {
return this.customChaptersBar.clientWidth * (parseFloat(data.replace("%", "")) / 100);
} else {
return parseInt(element.style[param].match(/\d+/g)?.[0]) || 0;
}
}
updateChapterText(segments: SponsorTime[], submittingSegments: SponsorTime[], currentTime: number): SponsorTime[] {
if (!Config.config.showSegmentNameInChapterBar
if (!Config.config.showSegmentNameInChapterBar
|| ((!segments || segments.length <= 0) && submittingSegments?.length <= 0)) {
const chaptersContainer = this.getChaptersContainer();
const chapterButton = this.getChapterButton(chaptersContainer);
if (chapterButton.classList.contains("ytp-chapter-container-disabled")) {
chaptersContainer.style.display = "none";
if (chaptersContainer) {
chaptersContainer.querySelector(".sponsorChapterText")?.remove();
const chapterTitle = chaptersContainer.querySelector(".ytp-chapter-title-content") as HTMLDivElement;
chapterTitle.style.removeProperty("display");
chaptersContainer.classList.remove("sponsorblock-chapter-visible");
}
return;
return [];
}
segments ??= [];
if (submittingSegments?.length > 0) segments = segments.concat(submittingSegments);
const activeSegments = segments.filter((segment) => {
return segment.hidden === SponsorHideType.Visible
return segment.hidden === SponsorHideType.Visible
&& segment.segment[0] <= currentTime && segment.segment[1] > currentTime
&& segment.category !== DEFAULT_CATEGORY;
});
@@ -767,7 +815,7 @@ class PreviewBar {
if (chaptersContainer) {
if (segments.length > 0) {
chaptersContainer.style.removeProperty("display");
chaptersContainer.classList.add("sponsorblock-chapter-visible");
const chosenSegment = segments.sort((a, b) => {
if (a.actionType === ActionType.Chapter && b.actionType !== ActionType.Chapter) {
@@ -784,7 +832,16 @@ class PreviewBar {
chapterButton.disabled = false;
const chapterTitle = chaptersContainer.querySelector(".ytp-chapter-title-content") as HTMLDivElement;
chapterTitle.innerText = chosenSegment.description || shortCategoryName(chosenSegment.category);
chapterTitle.style.display = "none";
const chapterCustomText = (chapterTitle.parentElement.querySelector(".sponsorChapterText") || (() => {
const elem = document.createElement("div");
chapterTitle.parentElement.insertBefore(elem, chapterTitle);
elem.classList.add("sponsorChapterText");
return elem;
})()) as HTMLDivElement;
chapterCustomText.innerText = chosenSegment.description || shortCategoryName(chosenSegment.category);
if (chosenSegment.actionType !== ActionType.Chapter) {
chapterTitle.classList.add("sponsorBlock-segment-title");
} else {
@@ -798,7 +855,7 @@ class PreviewBar {
if (oldVoteContainers.length > 0) {
oldVoteContainers.forEach((oldVoteContainer) => oldVoteContainer.remove());
}
chapterButton.insertBefore(chapterVoteContainer, this.getChapterChevron());
}
@@ -808,7 +865,12 @@ class PreviewBar {
this.chapterVote.setVisibility(false);
}
} else {
chaptersContainer.style.display = "none";
chaptersContainer.querySelector(".sponsorChapterText")?.remove();
const chapterTitle = chaptersContainer.querySelector(".ytp-chapter-title-content") as HTMLDivElement;
chapterTitle.style.removeProperty("display");
chaptersContainer.classList.remove("sponsorblock-chapter-visible");
this.chapterVote.setVisibility(false);
}
}
@@ -820,7 +882,7 @@ class PreviewBar {
private getChapterButton(chaptersContainer: HTMLElement): HTMLButtonElement {
return (chaptersContainer ?? this.getChaptersContainer())
.querySelector("button.ytp-chapter-title") as HTMLButtonElement;
?.querySelector("button.ytp-chapter-title") as HTMLButtonElement;
}
remove(): void {
@@ -868,7 +930,7 @@ class PreviewBar {
for (let i = 0; i < this.originalChapterBarBlocks.length; i++) {
const chapterElement = this.originalChapterBarBlocks[i];
const widthPixels = parseFloat(chapterElement.style.width.replace("px", ""));
if (time >= this.existingChapters[i].segment[1]) {
const marginPixels = chapterElement.style.marginRight ? parseFloat(chapterElement.style.marginRight.replace("px", "")) : 0;
pixelOffset += widthPixels + marginPixels;
@@ -883,8 +945,8 @@ class PreviewBar {
if (latestChapter) {
const latestWidth = parseFloat(this.originalChapterBarBlocks[lastCheckedChapter + 1].style.width.replace("px", ""));
const latestChapterDuration = latestChapter.segment[1] - latestChapter.segment[0];
const percentageInCurrentChapter = (time - latestChapter.segment[0]) / latestChapterDuration;
const percentageInCurrentChapter = (time - latestChapter.segment[0]) / latestChapterDuration;
const sizeOfCurrentChapter = latestWidth / totalPixels;
return Math.min(1, ((pixelOffset / totalPixels) + (percentageInCurrentChapter * sizeOfCurrentChapter)));
}
@@ -900,11 +962,18 @@ class PreviewBar {
return this.videoDuration * (showLarger ? 0.006 : 0.003);
}
private getSmallestSegment(timeInSeconds: number, segments: PreviewBarSegment[]): PreviewBarSegment | null {
// Name parameter used for cache
private getSmallestSegment(timeInSeconds: number, segments: PreviewBarSegment[], name?: string): PreviewBarSegment | null {
const proposedIndex = name ? this.lastSmallestSegment[name]?.index : null;
const startSearchIndex = proposedIndex && segments[proposedIndex] === this.lastSmallestSegment[name].segment ? proposedIndex : 0;
const direction = startSearchIndex > 0 && timeInSeconds < this.lastSmallestSegment[name].segment.segment[0] ? -1 : 1;
let segment: PreviewBarSegment | null = null;
let index = -1;
let currentSegmentLength = Infinity;
for (const seg of segments) { //
for (let i = startSearchIndex; i < segments.length && i >= 0; i += direction) {
const seg = segments[i];
const segmentLength = seg.segment[1] - seg.segment[0];
const minSize = this.getMinimumSize(seg.showLarger);
@@ -914,8 +983,20 @@ class PreviewBar {
if (segmentLength < currentSegmentLength) {
currentSegmentLength = segmentLength;
segment = seg;
index = i;
}
}
if (direction === 1 && seg.segment[0] > timeInSeconds) {
break;
}
}
if (segment) {
this.lastSmallestSegment[name] = {
index: index,
segment: segment
};
}
return segment;

View File

@@ -1,8 +1,8 @@
import Config from "../config";
import { SponsorTime } from "../types";
import { getSkippingText } from "../utils/categoryUtils";
import { keybindToString } from "../utils/configUtils";
import { AnimationUtils } from "../utils/animationUtils";
import { keybindToString } from "../../maze-utils/src/config";
export interface SkipButtonControlBarProps {
skip: (segment: SponsorTime) => void;
@@ -102,6 +102,7 @@ export class SkipButtonControlBar {
this.enabled = true;
this.refreshText();
this.container?.classList?.remove("textDisabled");
this.textContainer?.classList?.remove("hidden");
AnimationUtils.disableAutoHideAnimation(this.skipIcon);
@@ -134,7 +135,6 @@ export class SkipButtonControlBar {
disable(): void {
this.container.classList.add("hidden");
this.textContainer?.classList?.remove("hidden");
this.chapterText?.classList?.remove("hidden");
this.getChapterPrefix()?.classList?.remove("hidden");
@@ -142,6 +142,10 @@ export class SkipButtonControlBar {
this.enabled = false;
}
isEnabled(): boolean {
return this.enabled;
}
toggleSkip(): void {
this.skip(this.segment);
this.disableText();

View File

@@ -13,15 +13,22 @@ import CategoryChooser from "./render/CategoryChooser";
import UnsubmittedVideos from "./render/UnsubmittedVideos";
import KeybindComponent from "./components/options/KeybindComponent";
import { showDonationLink } from "./utils/configUtils";
import { localizeHtmlPage } from "./utils/pageUtils";
import { StorageChangesObject } from "./types";
import { localizeHtmlPage } from "../maze-utils/src/setup";
import { StorageChangesObject } from "../maze-utils/src/config";
import { getHash } from "../maze-utils/src/hash";
import { isFirefoxOrSafari } from "../maze-utils/src";
import { isDeArrowInstalled } from "./utils/crossExtension";
const utils = new Utils();
let embed = false;
const categoryChoosers: CategoryChooser[] = [];
const unsubmittedVideos: UnsubmittedVideos[] = [];
window.addEventListener('DOMContentLoaded', init);
if (document.readyState === "complete") {
init();
} else {
document.addEventListener("DOMContentLoaded", init);
}
async function init() {
localizeHtmlPage();
@@ -66,6 +73,18 @@ async function init() {
donate.classList.add("hidden");
}
// DeArrow promotion
if (Config.config.showNewFeaturePopups && Config.config.showUpsells) {
isDeArrowInstalled().then((installed) => {
if (!installed) {
const deArrowPromotion = document.getElementById("deArrowPromotion");
deArrowPromotion.classList.remove("hidden");
deArrowPromotion.addEventListener("click", () => Config.config.showDeArrowPromotion = false);
}
});
}
// Set all of the toggle options to the correct option
const optionsContainer = document.getElementById("options");
const optionsElements = optionsContainer.querySelectorAll("*");
@@ -185,7 +204,7 @@ async function init() {
}
// Permission needed on Firefox
if (utils.isFirefox()) {
if (isFirefoxOrSafari()) {
const permissionSuccess = await new Promise((resolve) => {
chrome.permissions.request({
origins: [textChangeInput.value + "/"],
@@ -430,7 +449,12 @@ function invidiousInstanceAddInit(element: HTMLElement, option: string) {
let instanceList = Config.config[option];
if (!instanceList) instanceList = [];
instanceList.push(textBox.value);
let domain = textBox.value.trim().toLowerCase();
if (domain.includes(":")) {
domain = domain.split(":")[0];
}
instanceList.push(domain);
Config.config[option] = instanceList;
@@ -532,7 +556,7 @@ function activatePrivateTextChange(element: HTMLElement) {
case "userID":
if (Config.config[option]) {
utils.asyncRequestToServer("GET", "/api/userInfo", {
userID: Config.config[option],
publicUserID: getHash(Config.config[option]),
values: ["warnings", "banned"]
}).then((result) => {
const userInfo = JSON.parse(result.responseText);
@@ -672,4 +696,4 @@ function copyDebugOutputToClipboard() {
function isIncognitoAllowed(): Promise<boolean> {
return new Promise((resolve) => chrome.extension.isAllowedIncognitoAccess(resolve));
}
}

View File

@@ -1,13 +1,17 @@
import Config from "./config";
import Utils from "./utils";
import { localizeHtmlPage } from "./utils/pageUtils";
import { localizeHtmlPage } from "../maze-utils/src/setup";
const utils = new Utils();
// This is needed, if Config is not imported before Utils, things break.
// Probably due to cyclic dependencies
Config.config;
window.addEventListener('DOMContentLoaded', init);
if (document.readyState === "complete") {
init();
} else {
document.addEventListener("DOMContentLoaded", init);
}
async function init() {
localizeHtmlPage();

View File

@@ -7,7 +7,6 @@ import {
SponsorHideType,
SponsorSourceType,
SponsorTime,
StorageChangesObject,
} from "./types";
import {
GetChannelIDResponse,
@@ -21,12 +20,13 @@ import {
} from "./messageTypes";
import { showDonationLink } from "./utils/configUtils";
import { AnimationUtils } from "./utils/animationUtils";
import { GenericUtils } from "./utils/genericUtils";
import { shortCategoryName } from "./utils/categoryUtils";
import { localizeHtmlPage } from "./utils/pageUtils";
import { localizeHtmlPage } from "../maze-utils/src/setup";
import { exportTimes } from "./utils/exporter";
import GenericNotice from "./render/GenericNotice";
import { noRefreshFetchingChaptersAllowed } from "./utils/licenseKey";
import { getErrorMessage, getFormattedTime } from "../maze-utils/src/formating";
import { StorageChangesObject } from "../maze-utils/src/config";
import { getHash } from "../maze-utils/src/hash";
const utils = new Utils();
@@ -67,9 +67,11 @@ class MessageHandler {
// To prevent clickjacking
let allowPopup = window === window.top;
window.addEventListener("message", async (e) => {
window.addEventListener("message", async (e): Promise<void> => {
if (e.source !== window.parent) return;
if (e.origin.endsWith('.youtube.com')) return allowPopup = true;
if (e.origin.endsWith('.youtube.com')) {
allowPopup = true;
}
});
//make this a function to allow this to run on the content page
@@ -228,7 +230,7 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
PageElements.optionsButton.addEventListener("click", openOptions);
PageElements.helpButton.addEventListener("click", openHelp);
PageElements.refreshSegmentsButton.addEventListener("click", refreshSegments);
PageElements.sbPopupIconCopyUserID.addEventListener("click", async () => copyToClipboard(await utils.getHash(Config.config.userID)));
PageElements.sbPopupIconCopyUserID.addEventListener("click", async () => copyToClipboard(await getHash(Config.config.userID)));
// Forward click events
if (window !== window.top) {
@@ -278,10 +280,9 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
}
const values = ["userName", "viewCount", "minutesSaved", "vip", "permissions"];
if (!Config.config.payments.freeAccess && !noRefreshFetchingChaptersAllowed()) values.push("freeChaptersAccess");
utils.asyncRequestToServer("GET", "/api/userInfo", {
userID: Config.config.userID,
publicUserID: await getHash(Config.config.userID),
values
}).then((res) => {
if (res.status === 200) {
@@ -313,13 +314,6 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
Config.config.isVip = userInfo.vip;
Config.config.permissions = userInfo.permissions;
if (userInfo.freeChaptersAccess) {
Config.config.payments.chaptersAllowed = userInfo.freeChaptersAccess;
Config.config.payments.freeAccess = userInfo.freeChaptersAccess;
Config.config.payments.lastCheck = Date.now();
Config.forceSyncUpdate("payments");
}
}
});
@@ -456,8 +450,13 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
PageElements.videoFound.innerHTML = chrome.i18n.getMessage("sponsor404");
PageElements.issueReporterImportExport.classList.remove("hidden");
} else {
PageElements.videoFound.innerHTML = chrome.i18n.getMessage("connectionError") + request.status;
PageElements.issueReporterImportExport.classList.add("hidden");
if (request.status) {
PageElements.videoFound.innerHTML = chrome.i18n.getMessage("connectionError") + request.status;
} else {
PageElements.videoFound.innerHTML = chrome.i18n.getMessage("segmentsStillLoading");
}
PageElements.issueReporterImportExport.classList.remove("hidden");
}
}
@@ -586,9 +585,9 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
if (downloadedTimes[i].actionType === ActionType.Full) {
segmentTimeFromToNode.innerText = chrome.i18n.getMessage("full");
} else {
segmentTimeFromToNode.innerText = GenericUtils.getFormattedTime(downloadedTimes[i].segment[0], true) +
segmentTimeFromToNode.innerText = getFormattedTime(downloadedTimes[i].segment[0], true) +
(actionType !== ActionType.Poi
? " " + chrome.i18n.getMessage("to") + " " + GenericUtils.getFormattedTime(downloadedTimes[i].segment[1], true)
? " " + chrome.i18n.getMessage("to") + " " + getFormattedTime(downloadedTimes[i].segment[1], true)
: "");
}
@@ -609,6 +608,7 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
const votingButtons = document.createElement("details");
votingButtons.classList.add("votingButtons");
votingButtons.id = "votingButtons" + UUID;
votingButtons.setAttribute("data-uuid", UUID);
votingButtons.addEventListener("toggle", () => {
if (votingButtons.open) {
openedUUIDs.push(UUID);
@@ -693,6 +693,7 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
voteButtonsContainer.appendChild(downvoteButton);
voteButtonsContainer.appendChild(uuidButton);
if (downloadedTimes[i].actionType === ActionType.Skip || downloadedTimes[i].actionType === ActionType.Mute
|| downloadedTimes[i].actionType === ActionType.Poi
&& [SponsorHideType.Visible, SponsorHideType.Hidden].includes(downloadedTimes[i].hidden)) {
voteButtonsContainer.appendChild(hideButton);
}
@@ -813,7 +814,7 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
PageElements.sponsorTimesContributionsContainer.classList.remove("hidden");
} else {
PageElements.setUsernameStatus.innerText = GenericUtils.getErrorMessage(response.status, response.responseText);
PageElements.setUsernameStatus.innerText = getErrorMessage(response.status, response.responseText);
}
});
@@ -866,7 +867,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(GenericUtils.getErrorMessage(response.statusCode, response.responseText), UUID);
addVoteMessage(getErrorMessage(response.statusCode, response.responseText), UUID);
}
setTimeout(() => removeVoteMessage(UUID), 1500);
}
@@ -1062,10 +1063,37 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
port.onMessage.addListener((msg) => onMessage(msg));
}
function updateCurrentTime(currentTime: number) {
// Create a map of segment UUID -> segment object for easy access
const segmentMap: Record<string, SponsorTime> = {};
for (const segment of downloadedTimes)
segmentMap[segment.UUID] = segment
// Iterate over segment elements and update their classes
const segmentList = document.getElementById("issueReporterTimeButtons");
for (const segmentElement of segmentList.children) {
const UUID = segmentElement.getAttribute("data-uuid");
if (UUID == null || segmentMap[UUID] == undefined) continue;
const summaryElement = segmentElement.querySelector("summary")
if (summaryElement == null) continue;
const segment = segmentMap[UUID]
summaryElement.classList.remove("segmentActive", "segmentPassed")
if (currentTime >= segment.segment[0]) {
if (currentTime < segment.segment[1]) {
summaryElement.classList.add("segmentActive");
} else {
summaryElement.classList.add("segmentPassed");
}
}
}
}
function onMessage(msg: PopupMessage) {
switch (msg.message) {
case "time":
displayDownloadedSponsorTimes(downloadedTimes, msg.time);
updateCurrentTime(msg.time);
break;
case "infoUpdated":
infoFound(msg);

View File

@@ -4,61 +4,94 @@ import CategoryPillComponent, { CategoryPillState } from "../components/Category
import Config from "../config";
import { VoteResponse } from "../messageTypes";
import { Category, SegmentUUID, SponsorTime } from "../types";
import { GenericUtils } from "../utils/genericUtils";
import { Tooltip } from "./Tooltip";
import { waitFor } from "../../maze-utils/src";
import { getYouTubeTitleNode } from "../../maze-utils/src/elements";
import { addCleanupListener } from "../../maze-utils/src/cleanup";
const id = "categoryPill";
export class CategoryPill {
container: HTMLElement;
ref: React.RefObject<CategoryPillComponent>;
root: Root;
unsavedState: CategoryPillState;
lastState: CategoryPillState;
mutationObserver?: MutationObserver;
onMobileYouTube: boolean;
onInvidious: boolean;
vote: (type: number, UUID: SegmentUUID, category?: Category) => Promise<VoteResponse>;
constructor() {
this.ref = React.createRef();
addCleanupListener(() => {
if (this.mutationObserver) {
this.mutationObserver.disconnect();
}
});
}
async attachToPage(onMobileYouTube: boolean, onInvidious: boolean,
vote: (type: number, UUID: SegmentUUID, category?: Category) => Promise<VoteResponse>): Promise<void> {
this.onMobileYouTube = onMobileYouTube;
this.onInvidious = onInvidious;
this.vote = vote;
this.attachToPageInternal();
}
private async attachToPageInternal(): 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);
await waitFor(() => getYouTubeTitleNode());
if (referenceNode && !referenceNode.contains(this.container)) {
this.container = document.createElement('span');
this.container.id = "categoryPill";
this.container.style.display = "relative";
if (!this.container) {
this.container = document.createElement('span');
this.container.id = id;
this.container.style.display = "relative";
referenceNode.prepend(this.container);
referenceNode.style.display = "flex";
this.root = createRoot(this.container);
this.ref = React.createRef();
this.root.render(<CategoryPillComponent
ref={this.ref}
vote={this.vote}
showTextByDefault={!this.onMobileYouTube}
showTooltipOnClick={this.onMobileYouTube} />);
if (this.ref.current) {
this.unsavedState = this.ref.current.state;
}
this.root = createRoot(this.container);
this.root.render(<CategoryPillComponent ref={this.ref} vote={vote} />);
if (this.unsavedState) {
this.ref.current?.setState(this.unsavedState);
this.unsavedState = null;
}
if (onMobileYouTube) {
if (this.mutationObserver) {
this.mutationObserver.disconnect();
if (this.onMobileYouTube) {
if (this.mutationObserver) {
this.mutationObserver.disconnect();
}
this.mutationObserver = new MutationObserver((changes) => {
if (changes.some((change) => change.removedNodes.length > 0)) {
this.attachToPageInternal();
}
});
this.mutationObserver.observe(referenceNode, {
childList: true,
subtree: true
});
}
this.mutationObserver = new MutationObserver(() => this.attachToPage(onMobileYouTube, onInvidious, vote));
}
this.mutationObserver.observe(referenceNode, {
childList: true,
subtree: true
if (this.lastState) {
waitFor(() => this.ref.current).then(() => {
this.ref.current?.setState(this.lastState);
});
}
// Use a parent because YouTube does weird things to the top level object
// react would have to rerender if container was the top level
const parent = document.createElement("span");
parent.id = "categoryPillParent";
parent.appendChild(this.container);
referenceNode.prepend(parent);
referenceNode.style.display = "flex";
}
}
@@ -73,11 +106,8 @@ export class CategoryPill {
open: show ? this.ref.current?.state.open : false
};
if (this.ref.current) {
this.ref.current?.setState(newState);
} else {
this.unsavedState = newState;
}
this.ref.current?.setState(newState);
this.lastState = newState;
}
async setSegment(segment: SponsorTime): Promise<void> {
@@ -88,16 +118,13 @@ export class CategoryPill {
open: false
};
if (this.ref.current) {
this.ref.current?.setState(newState);
} else {
this.unsavedState = newState;
}
this.ref.current?.setState(newState);
this.lastState = newState;
if (!Config.config.categoryPillUpdate) {
Config.config.categoryPillUpdate = true;
const watchDiv = await GenericUtils.wait(() => document.querySelector("#info.ytd-watch-flexy") as HTMLElement);
const watchDiv = await waitFor(() => document.querySelector("#info.ytd-watch-flexy") as HTMLElement);
if (watchDiv) {
new Tooltip({
text: chrome.i18n.getMessage("categoryPillNewFeature"),
@@ -111,5 +138,9 @@ export class CategoryPill {
}
}
}
if (this.onMobileYouTube && !document.contains(this.container)) {
this.attachToPageInternal();
}
}
}

View File

@@ -5,8 +5,9 @@ import NoticeComponent from "../components/NoticeComponent";
import Utils from "../utils";
const utils = new Utils();
import { ButtonListener, ContentContainer } from "../types";
import { ContentContainer } from "../types";
import NoticeTextSelectionComponent from "../components/NoticeTextSectionComponent";
import { ButtonListener } from "../../maze-utils/src/components/component-types";
export interface TextBox {
icon: string;
@@ -46,6 +47,7 @@ export default class GenericNotice {
const referenceNode = options.referenceNode ?? utils.findReferenceNode();
this.noticeElement = document.createElement("div");
this.noticeElement.className = "sponsorSkipNoticeContainer";
this.noticeElement.id = "sponsorSkipNoticeContainer" + idSuffix;
referenceNode.prepend(this.noticeElement);

View File

@@ -38,6 +38,7 @@ class SkipNotice {
idSuffix += amountOfPreviousNotices;
this.noticeElement = document.createElement("div");
this.noticeElement.className = "sponsorSkipNoticeContainer";
this.noticeElement.id = "sponsorSkipNoticeContainer" + idSuffix;
referenceNode.prepend(this.noticeElement);

View File

@@ -1,129 +1,7 @@
import * as React from "react";
import { createRoot, Root } from 'react-dom/client';
import { ButtonListener } from "../types";
import { GenericTooltip, TooltipProps } from "../../maze-utils/src/components/Tooltip";
export interface TooltipProps {
text?: string;
link?: string;
linkOnClick?: () => void;
referenceNode: HTMLElement;
prependElement?: HTMLElement; // Element to append before
bottomOffset?: string;
leftOffset?: string;
rightOffset?: string;
timeout?: number;
opacity?: number;
displayTriangle?: boolean;
extraClass?: string;
showLogo?: boolean;
showGotIt?: boolean;
positionRealtive?: boolean;
buttons?: ButtonListener[];
}
export class Tooltip {
text?: string;
container: HTMLDivElement;
timer: NodeJS.Timeout;
root: Root;
export class Tooltip extends GenericTooltip {
constructor(props: TooltipProps) {
props.bottomOffset ??= "70px";
props.leftOffset ??= "inherit";
props.rightOffset ??= "inherit";
props.opacity ??= 0.7;
props.displayTriangle ??= true;
props.extraClass ??= "";
props.showLogo ??= true;
props.showGotIt ??= true;
props.positionRealtive ??= true;
this.text = props.text;
this.container = document.createElement('div');
this.container.id = "sponsorTooltip" + props.text;
if (props.positionRealtive) this.container.style.position = "relative";
if (props.prependElement) {
props.referenceNode.insertBefore(this.container, props.prependElement);
} else {
props.referenceNode.appendChild(this.container);
}
if (props.timeout) {
this.timer = setTimeout(() => this.close(), props.timeout * 1000);
}
const backgroundColor = `rgba(28, 28, 28, ${props.opacity})`;
this.root = createRoot(this.container);
this.root.render(
<div style={{bottom: props.bottomOffset, left: props.leftOffset, right: props.rightOffset, backgroundColor}}
className={"sponsorBlockTooltip" + (props.displayTriangle ? " sbTriangle" : "") + ` ${props.extraClass}`}>
<div>
{props.showLogo ?
<img className="sponsorSkipLogo sponsorSkipObject"
src={chrome.extension.getURL("icons/IconSponsorBlocker256px.png")}>
</img>
: null}
{this.text ?
<span className="sponsorSkipObject">
{this.text + (props.link ? ". " : "")}
{props.link ?
<a style={{textDecoration: "underline"}}
target="_blank"
rel="noopener noreferrer"
href={props.link}>
{chrome.i18n.getMessage("LearnMore")}
</a>
: (props.linkOnClick ?
<a style={{textDecoration: "underline", marginLeft: "5px", cursor: "pointer"}}
onClick={props.linkOnClick}>
{chrome.i18n.getMessage("LearnMore")}
</a>
: null)}
</span>
: null}
{this.getButtons(props.buttons)}
</div>
{props.showGotIt ?
<button className="sponsorSkipObject sponsorSkipNoticeButton"
style ={{float: "right" }}
onClick={() => this.close()}>
{chrome.i18n.getMessage("GotIt")}
</button>
: null}
</div>
)
}
getButtons(buttons?: ButtonListener[]): JSX.Element[] {
if (buttons) {
const result: JSX.Element[] = [];
for (const button of buttons) {
result.push(
<button className="sponsorSkipObject sponsorSkipNoticeButton sponsorSkipNoticeRightButton"
key={button.name}
onClick={(e) => button.listener(e)}>
{button.name}
</button>
)
}
return result;
} else {
return null;
}
}
close(): void {
this.root.unmount();
this.container.remove();
if (this.timer) clearTimeout(this.timer);
super(props, "icons/IconSponsorBlocker256px.png")
}
}

55
src/svg-icons/sb_svg.tsx Normal file
View File

@@ -0,0 +1,55 @@
import * as React from "react";
export interface SbIconProps {
id?: string;
fill?: string;
className?: string;
width?: string;
height?: string;
onClick?: () => void;
}
export default function SbSvg({
id = "",
fill = "#ff0000",
className = "",
onClick
}: SbIconProps): JSX.Element {
return (
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 565.15 568"
id={id}
className={className}
onClick={() => onClick?.() } >
<g
id="Layer_2"
data-name="Layer 2">
<g
id="Layer_1-2"
data-name="Layer 1"
style={{
fill
}}>
<path
d="M282.58,568a65,65,0,0,1-34.14-9.66C95.41,463.94,2.54,300.46,0,121A64.91,64.91,0,0,1,34,62.91a522.56,522.56,0,0,1,497.16,0,64.91,64.91,0,0,1,34,58.12c-2.53,179.43-95.4,342.91-248.42,437.3A65,65,0,0,1,282.58,568Zm0-548.31A502.24,502.24,0,0,0,43.4,80.22a45.27,45.27,0,0,0-23.7,40.53c2.44,172.67,91.81,330,239.07,420.83a46.19,46.19,0,0,0,47.61,0C453.64,450.73,543,293.42,545.45,120.75a45.26,45.26,0,0,0-23.7-40.54A502.26,502.26,0,0,0,282.58,19.69Z"
id="path8"
style={{
fill
}} />
<path
style={{
fill
}}
d="M 284.70508 42.693359 A 479.9 479.9 0 0 0 54.369141 100.41992 A 22.53 22.53 0 0 0 42.669922 120.41992 C 45.069922 290.25992 135.67008 438.63977 270.83008 522.00977 A 22.48 22.48 0 0 0 294.32031 522.00977 C 429.48031 438.63977 520.08047 290.25992 522.48047 120.41992 A 22.53 22.53 0 0 0 510.7793 100.41992 A 479.9 479.9 0 0 0 284.70508 42.693359 z M 220.41016 145.74023 L 411.2793 255.93945 L 220.41016 366.14062 L 220.41016 145.74023 z "
id="path10" />
</g>
</g>
<polygon style={{
fill: "#fff"
}}
points="411.28 255.94 220.41 145.74 220.41 366.14 411.28 255.94"
/>
</svg>
);
}

View File

@@ -26,19 +26,12 @@ export interface ContentContainer {
};
}
export interface FetchResponse {
responseText: string;
status: number;
ok: boolean;
}
export type HashedValue = string & { __hashBrand: unknown };
export interface VideoDurationResponse {
duration: number;
}
export enum CategorySkipOption {
Disabled = -1,
ShowOverlay,
ManualSkip,
AutoSkip
@@ -106,8 +99,8 @@ export interface Registration {
message: string;
id: string;
allFrames: boolean;
js: browser.extensionTypes.ExtensionFileOrCode[];
css: browser.extensionTypes.ExtensionFileOrCode[];
js: string[];
css: string[];
matches: string[];
}
@@ -194,8 +187,6 @@ export interface VideoInfo {
export type VideoID = string;
export type StorageChangesObject = { [key: string]: chrome.storage.StorageChange };
export type UnEncodedSegmentTimes = [string, SponsorTime[]][];
export enum ChannelIDStatus {
@@ -229,26 +220,4 @@ export enum NoticeVisbilityMode {
MiniForAll = 2,
FadedForAutoSkip = 3,
FadedForAll = 4
}
export type Keybind = {
key: string;
code?: string;
ctrl?: boolean;
alt?: boolean;
shift?: boolean;
}
export enum PageType {
Shorts = "shorts",
Watch = "watch",
Search = "search",
Browse = "browse",
Channel = "channel",
Embed = "embed"
}
export interface ButtonListener {
name: string;
listener: (e?: React.MouseEvent<HTMLButtonElement, MouseEvent>) => void;
}

View File

@@ -1,83 +0,0 @@
import Config from "./config";
import { checkLicenseKey } from "./utils/licenseKey";
import { localizeHtmlPage } from "./utils/pageUtils";
import * as countries from "../public/res/countries.json";
import Utils from "./utils";
import { Category, CategorySkipOption } from "./types";
// This is needed, if Config is not imported before Utils, things break.
// Probably due to cyclic dependencies
Config.config;
const utils = new Utils();
window.addEventListener('DOMContentLoaded', init);
async function init() {
localizeHtmlPage();
const cantAfford = document.getElementById("cantAfford");
const cantAffordTexts = chrome.i18n.getMessage("cantAfford").split(/{|}/);
cantAfford.appendChild(document.createTextNode(cantAffordTexts[0]));
const discountButton = document.createElement("span");
discountButton.id = "discountButton";
discountButton.innerText = cantAffordTexts[1];
cantAfford.appendChild(discountButton);
cantAfford.appendChild(document.createTextNode(cantAffordTexts[2]));
const redeemButton = document.getElementById("redeemButton") as HTMLInputElement;
const redeemInput = document.getElementById("redeemCodeInput") as HTMLInputElement;
redeemButton.addEventListener("click", async () => {
const licenseKey = redeemInput.value;
if (await checkLicenseKey(licenseKey)) {
Config.config.payments.licenseKey = licenseKey;
Config.forceSyncUpdate("payments");
if (!utils.getCategorySelection("chapter")) {
Config.config.categorySelections.push({
name: "chapter" as Category,
option: CategorySkipOption.ShowOverlay
});
}
alert(chrome.i18n.getMessage("redeemSuccess"));
} else {
alert(chrome.i18n.getMessage("redeemFailed"));
}
});
discountButton.addEventListener("click", async () => {
const subsidizedSection = document.getElementById("subsidizedPrice");
subsidizedSection.classList.remove("hidden");
const oldSelector = document.getElementById("countrySelector");
if (oldSelector) oldSelector.remove();
const countrySelector = document.createElement("select");
countrySelector.id = "countrySelector";
countrySelector.className = "optionsSelector";
const defaultOption = document.createElement("option");
defaultOption.innerText = chrome.i18n.getMessage("chooseACountry");
countrySelector.appendChild(defaultOption);
for (const country of Object.keys(countries)) {
const option = document.createElement("option");
option.value = country;
option.innerText = country;
countrySelector.appendChild(option);
}
countrySelector.addEventListener("change", () => {
if (countries[countrySelector.value]?.allowed) {
document.getElementById("subsidizedLink").classList.remove("hidden");
document.getElementById("noSubsidizedLink").classList.add("hidden");
} else {
document.getElementById("subsidizedLink").classList.add("hidden");
document.getElementById("noSubsidizedLink").classList.remove("hidden");
}
});
subsidizedSection.appendChild(countrySelector);
});
}

View File

@@ -1,9 +1,12 @@
import Config, { VideoDownvotes } from "./config";
import { CategorySelection, SponsorTime, FetchResponse, BackgroundScriptContainer, Registration, HashedValue, VideoID, SponsorHideType } from "./types";
import { CategorySelection, SponsorTime, BackgroundScriptContainer, Registration, VideoID, SponsorHideType, CategorySkipOption } from "./types";
import { getHash, HashedValue } from "../maze-utils/src/hash";
import * as CompileConfig from "../config.json";
import { findValidElement, findValidElementFromSelector } from "./utils/pageUtils";
import { GenericUtils } from "./utils/genericUtils";
import { isFirefoxOrSafari, waitFor } from "../maze-utils/src";
import { findValidElementFromSelector } from "../maze-utils/src/dom";
import { FetchResponse, sendRequestToCustomServer } from "../maze-utils/src/background-request-proxy"
import { isSafari } from "../maze-utils/src/config";
export default class Utils {
@@ -12,94 +15,21 @@ export default class Utils {
// Used to add content scripts and CSS required
js = [
"./js/vendor.js",
"./js/content.js"
];
css = [
"content.css",
"./libs/Source+Sans+Pro.css",
"popup.css"
"popup.css",
"shared.css"
];
/* Used for waitForElement */
creatingWaitingMutationObserver = false;
waitingMutationObserver: MutationObserver = null;
waitingElements: { selector: string; visibleCheck: boolean; callback: (element: Element) => void }[] = [];
constructor(backgroundScriptContainer: BackgroundScriptContainer = null) {
this.backgroundScriptContainer = backgroundScriptContainer;
}
async wait<T>(condition: () => T, timeout = 5000, check = 100): Promise<T> {
return GenericUtils.wait(condition, timeout, check);
}
/* Uses a mutation observer to wait asynchronously */
async waitForElement(selector: string, visibleCheck = false): Promise<Element> {
return await new Promise((resolve) => {
const initialElement = this.getElement(selector, visibleCheck);
if (initialElement) {
resolve(initialElement);
return;
}
this.waitingElements.push({
selector,
visibleCheck,
callback: resolve
});
if (!this.creatingWaitingMutationObserver) {
this.creatingWaitingMutationObserver = true;
if (document.body) {
this.setupWaitingMutationListener();
} else {
window.addEventListener("DOMContentLoaded", () => {
this.setupWaitingMutationListener();
});
}
}
});
}
private setupWaitingMutationListener(): void {
if (!this.waitingMutationObserver) {
const checkForObjects = () => {
const foundSelectors = [];
for (const { selector, visibleCheck, callback } of this.waitingElements) {
const element = this.getElement(selector, visibleCheck);
if (element) {
callback(element);
foundSelectors.push(selector);
}
}
this.waitingElements = this.waitingElements.filter((element) => !foundSelectors.includes(element.selector));
if (this.waitingElements.length === 0) {
this.waitingMutationObserver?.disconnect();
this.waitingMutationObserver = null;
this.creatingWaitingMutationObserver = false;
}
};
// Do an initial check over all objects
checkForObjects();
if (this.waitingElements.length > 0) {
this.waitingMutationObserver = new MutationObserver(checkForObjects);
this.waitingMutationObserver.observe(document.body, {
childList: true,
subtree: true
});
}
}
}
private getElement(selector: string, visibleCheck: boolean) {
return visibleCheck ? findValidElement(document.querySelectorAll(selector)) : document.querySelector(selector);
return waitFor(condition, timeout, check);
}
containsPermission(permissions: chrome.permissions.Permissions): Promise<boolean> {
@@ -117,9 +47,13 @@ export default class Utils {
* @param {CallableFunction} callback
*/
setupExtraSitePermissions(callback: (granted: boolean) => void): void {
// Request permission
let permissions = ["declarativeContent"];
if (this.isFirefox()) permissions = [];
const permissions = [];
if (!isFirefoxOrSafari()) {
permissions.push("declarativeContent");
}
if (!isFirefoxOrSafari() || isSafari()) {
permissions.push("webNavigation");
}
chrome.permissions.request({
origins: this.getPermissionRegex(),
@@ -143,52 +77,19 @@ export default class Utils {
* For now, it is just SB.config.invidiousInstances.
*/
setupExtraSiteContentScripts(): void {
if (this.isFirefox()) {
const firefoxJS = [];
for (const file of this.js) {
firefoxJS.push({file});
}
const firefoxCSS = [];
for (const file of this.css) {
firefoxCSS.push({file});
}
const registration: Registration = {
message: "registerContentScript",
id: "invidious",
allFrames: true,
js: this.js,
css: this.css,
matches: this.getPermissionRegex()
};
const registration: Registration = {
message: "registerContentScript",
id: "invidious",
allFrames: true,
js: firefoxJS,
css: firefoxCSS,
matches: this.getPermissionRegex()
};
if (this.backgroundScriptContainer) {
this.backgroundScriptContainer.registerFirefoxContentScript(registration);
} else {
chrome.runtime.sendMessage(registration);
}
if (this.backgroundScriptContainer) {
this.backgroundScriptContainer.registerFirefoxContentScript(registration);
} else {
chrome.declarativeContent.onPageChanged.removeRules(["invidious"], () => {
const conditions = [];
for (const regex of this.getPermissionRegex()) {
conditions.push(new chrome.declarativeContent.PageStateMatcher({
pageUrl: { urlMatches: regex }
}));
}
// Add page rule
const rule = {
id: "invidious",
conditions,
actions: [new chrome.declarativeContent.RequestContentScript({
allFrames: true,
js: this.js,
css: this.css
})]
};
chrome.declarativeContent.onPageChanged.addRules([rule]);
});
chrome.runtime.sendMessage(registration);
}
}
@@ -196,18 +97,18 @@ export default class Utils {
* Removes the permission and content script registration.
*/
removeExtraSiteRegistration(): void {
if (this.isFirefox()) {
const id = "invidious";
const id = "invidious";
if (this.backgroundScriptContainer) {
this.backgroundScriptContainer.unregisterFirefoxContentScript(id);
} else {
chrome.runtime.sendMessage({
message: "unregisterContentScript",
id: id
});
}
} else if (chrome.declarativeContent) {
if (this.backgroundScriptContainer) {
this.backgroundScriptContainer.unregisterFirefoxContentScript(id);
} else {
chrome.runtime.sendMessage({
message: "unregisterContentScript",
id: id
});
}
if (!isFirefoxOrSafari() && chrome.declarativeContent) {
// Only if we have permission
chrome.declarativeContent.onPageChanged.removeRules(["invidious"]);
}
@@ -237,7 +138,7 @@ export default class Utils {
containsInvidiousPermission(): Promise<boolean> {
return new Promise((resolve) => {
let permissions = ["declarativeContent"];
if (this.isFirefox()) permissions = [];
if (isFirefoxOrSafari()) permissions = [];
chrome.permissions.contains({
origins: this.getPermissionRegex(),
@@ -319,6 +220,7 @@ export default class Utils {
return selection;
}
}
return { name: category, option: CategorySkipOption.Disabled} as CategorySelection;
}
/**
@@ -345,18 +247,8 @@ export default class Utils {
* @param address The address to add to the SponsorBlock server address
* @param callback
*/
async asyncRequestToCustomServer(type: string, url: string, data = {}): Promise<FetchResponse> {
return new Promise((resolve) => {
// Ask the background script to do the work
chrome.runtime.sendMessage({
message: "sendRequest",
type,
url,
data
}, (response) => {
resolve(response);
});
});
asyncRequestToCustomServer(type: string, url: string, data = {}): Promise<FetchResponse> {
return sendRequestToCustomServer(type, url, data);
}
/**
@@ -396,18 +288,21 @@ export default class Utils {
const selectors = [
"#player-container-id", // Mobile YouTube
"#movie_player",
".html5-video-player", // May 2023 Card-Based YouTube Layout
"#c4-player", // Channel Trailer
"#player-container", // Preview on hover
"#main-panel.ytmusic-player-page", // YouTube music
"#player-container .video-js", // Invidious
".main-video-section > .video-container" // Cloudtube
".main-video-section > .video-container", // Cloudtube
".shaka-video-container", // Piped
"#player-container.ytk-player", // YT Kids
];
let referenceNode = findValidElementFromSelector(selectors)
if (referenceNode == null) {
//for embeds
const player = document.getElementById("player");
referenceNode = player.firstChild as HTMLElement;
referenceNode = player?.firstChild as HTMLElement;
if (referenceNode) {
let index = 1;
@@ -431,32 +326,11 @@ export default class Utils {
return Boolean(num.match(/^[0-9a-f]+$/i));
}
/**
* Is this Firefox (web-extensions)
*/
isFirefox(): boolean {
return typeof(browser) !== "undefined";
}
async getHash<T extends string>(value: T, times = 5000): Promise<T & HashedValue> {
if (times <= 0) return "" as T & HashedValue;
let hashHex: string = value;
for (let i = 0; i < times; i++) {
const hashBuffer = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(hashHex).buffer);
const hashArray = Array.from(new Uint8Array(hashBuffer));
hashHex = hashArray.map(b => b.toString(16).padStart(2, '0')).join('');
}
return hashHex as T & HashedValue;
}
async addHiddenSegment(videoID: VideoID, segmentUUID: string, hidden: SponsorHideType) {
if (chrome.extension.inIncognitoContext || !Config.config.trackDownvotes) return;
const hashedVideoID = (await this.getHash(videoID, 1)).slice(0, 4) as VideoID & HashedValue;
const UUIDHash = await this.getHash(segmentUUID, 1);
const hashedVideoID = (await getHash(videoID, 1)).slice(0, 4) as VideoID & HashedValue;
const UUIDHash = await getHash(segmentUUID, 1);
const allDownvotes = Config.local.downvotedSegments;
const currentVideoData = allDownvotes[hashedVideoID] || { segments: [], lastAccess: 0 };
@@ -464,7 +338,11 @@ export default class Utils {
currentVideoData.lastAccess = Date.now();
const existingData = currentVideoData.segments.find((segment) => segment.uuid === UUIDHash);
if (hidden === SponsorHideType.Visible) {
delete allDownvotes[hashedVideoID];
currentVideoData.segments.splice(currentVideoData.segments.indexOf(existingData), 1);
if (currentVideoData.segments.length === 0) {
delete allDownvotes[hashedVideoID];
}
} else {
if (existingData) {
existingData.hidden = hidden;

View File

@@ -0,0 +1,15 @@
import Config from "../config";
export function runCompatibilityChecks() {
if (Config.config.showZoomToFillError2 && document.URL.includes("watch?v=")) {
setTimeout(() => {
const zoomToFill = document.querySelector(".zoomtofillBtn");
if (zoomToFill) {
alert(chrome.i18n.getMessage("zoomToFillUnsupported"));
}
Config.config.showZoomToFillError2 = false;
}, 10000);
}
}

View File

@@ -1,48 +1,5 @@
import Config from "../config";
import { Keybind } from "../types";
export function showDonationLink(): boolean {
return navigator.vendor !== "Apple Computer, Inc." && Config.config.showDonationLink;
}
export function isSafari(): boolean {
return navigator.vendor === "Apple Computer, Inc.";
}
export function keybindEquals(first: Keybind, second: Keybind): boolean {
if (first == null || second == null ||
Boolean(first.alt) != Boolean(second.alt) || Boolean(first.ctrl) != Boolean(second.ctrl) || Boolean(first.shift) != Boolean(second.shift) ||
first.key == null && first.code == null || second.key == null && second.code == null)
return false;
if (first.code != null && second.code != null)
return first.code === second.code;
if (first.key != null && second.key != null)
return first.key.toUpperCase() === second.key.toUpperCase();
return false;
}
export function formatKey(key: string): string {
if (key == null)
return "";
else if (key == " ")
return "Space";
else if (key.length == 1)
return key.toUpperCase();
else
return key;
}
export function keybindToString(keybind: Keybind): string {
if (keybind == null || keybind.key == null)
return "";
let ret = "";
if (keybind.ctrl)
ret += "Ctrl+";
if (keybind.alt)
ret += "Alt+";
if (keybind.shift)
ret += "Shift+";
return ret += formatKey(keybind.key);
}

View File

@@ -148,5 +148,13 @@ export function getGuidelineInfo(category: Category): TextBox[] {
icon: "icons/check-smaller.svg",
text: chrome.i18n.getMessage(`category_${category}_guideline3`)
}];
default:
return [{
icon: "icons/segway.png",
text: chrome.i18n.getMessage(`generic_guideline1`)
}, {
icon: "icons/right-arrow.svg",
text: chrome.i18n.getMessage(`generic_guideline2`)
}];
}
}

View File

@@ -0,0 +1,44 @@
import * as CompileConfig from "../../config.json";
import Config from "../config";
import { isSafari } from "../../maze-utils/src/config";
import { isFirefoxOrSafari } from "../../maze-utils/src";
export function isDeArrowInstalled(): Promise<boolean> {
if (Config.config.deArrowInstalled) {
return Promise.resolve(true);
} else {
return new Promise((resolve) => {
const extensionIds = getExtensionIdsToImportFrom();
let count = 0;
for (const id of extensionIds) {
chrome.runtime.sendMessage(id, { message: "isInstalled" }, (response) => {
if (chrome.runtime.lastError) {
count++;
if (count === extensionIds.length) {
resolve(false);
}
return;
}
resolve(response);
if (response) {
Config.config.deArrowInstalled = true;
}
});
}
});
}
}
export function getExtensionIdsToImportFrom(): string[] {
if (isSafari()) {
return CompileConfig.extensionImportList.safari;
} else if (isFirefoxOrSafari()) {
return CompileConfig.extensionImportList.firefox;
} else {
return CompileConfig.extensionImportList.chromium;
}
}

View File

@@ -1,14 +1,15 @@
import { ActionType, Category, SegmentUUID, SponsorSourceType, SponsorTime } from "../types";
import { shortCategoryName } from "./categoryUtils";
import { GenericUtils } from "./genericUtils";
import * as CompileConfig from "../../config.json";
import { getFormattedTime, getFormattedTimeToSeconds } from "../../maze-utils/src/formating";
import { generateUserID } from "../../maze-utils/src/setup";
const inTest = typeof chrome === "undefined";
const chapterNames = CompileConfig.categoryList.filter((code) => code !== "chapter")
.map((code) => ({
code,
name: !inTest ? chrome.i18n.getMessage("category_" + code) : code
names: !inTest ? [chrome.i18n.getMessage("category_" + code), shortCategoryName(code)] : [code]
}));
export function exportTimes(segments: SponsorTime[]): string {
@@ -26,9 +27,9 @@ export function exportTimes(segments: SponsorTime[]): string {
function exportTime(segment: SponsorTime): string {
const name = segment.description || shortCategoryName(segment.category);
return `${GenericUtils.getFormattedTime(segment.segment[0], true)}${
return `${getFormattedTime(segment.segment[0], true)}${
segment.segment[1] && segment.segment[0] !== segment.segment[1]
? ` - ${GenericUtils.getFormattedTime(segment.segment[1], true)}` : ""} ${name}`;
? ` - ${getFormattedTime(segment.segment[1], true)}` : ""} ${name}`;
}
export function importTimes(data: string, videoDuration: number): SponsorTime[] {
@@ -37,25 +38,31 @@ export function importTimes(data: string, videoDuration: number): SponsorTime[]
for (const line of lines) {
const match = line.match(/(?:((?:\d+:)?\d+:\d+)+(?:\.\d+)?)|(?:\d+(?=s| second))/g);
if (match) {
const startTime = GenericUtils.getFormattedTimeToSeconds(match[0]);
const startTime = getFormattedTimeToSeconds(match[0]);
if (startTime !== null) {
const specialCharsMatcher = /^(?:\s+seconds?)?[-:()\s]*|(?:\s+at)?[-:()\s]+$/g
const titleLeft = line.split(match[0])[0].replace(specialCharsMatcher, "");
// Remove "seconds", "at", special characters, and ")" if there was a "("
const specialCharMatchers = [{
matcher: /^(?:\s+seconds?)?[-:()\s]*|(?:\s+at)?[-:(\s]+$/g
}, {
matcher: /[-:()\s]*$/g,
condition: (value) => !!value.match(/^\s*\(/)
}];
const titleLeft = removeIf(line.split(match[0])[0], specialCharMatchers);
let titleRight = null;
const split2 = line.split(match[1] || match[0]);
titleRight = split2[split2.length - 1].replace(specialCharsMatcher, "");
titleRight = removeIf(split2[split2.length - 1], specialCharMatchers)
const title = titleLeft?.length > titleRight?.length ? titleLeft : titleRight;
if (title) {
const determinedCategory = chapterNames.find(c => c.name === title)?.code as Category;
const determinedCategory = chapterNames.find(c => c.names.includes(title))?.code as Category;
const segment: SponsorTime = {
segment: [startTime, GenericUtils.getFormattedTimeToSeconds(match[1])],
segment: [startTime, getFormattedTimeToSeconds(match[1])],
category: determinedCategory ?? ("chapter" as Category),
actionType: determinedCategory ? ActionType.Skip : ActionType.Chapter,
description: title,
source: SponsorSourceType.Local,
UUID: GenericUtils.generateUserID() as SegmentUUID
UUID: generateUserID() as SegmentUUID
};
if (result.length > 0 && result[result.length - 1].segment[1] === null) {
@@ -75,6 +82,17 @@ export function importTimes(data: string, videoDuration: number): SponsorTime[]
return result;
}
function removeIf(value: string, matchers: Array<{ matcher: RegExp; condition?: (value: string) => boolean }>): string {
let result = value;
for (const matcher of matchers) {
if (!matcher.condition || matcher.condition(value)) {
result = result.replace(matcher.matcher, "");
}
}
return result;
}
export function exportTimesAsHashParam(segments: SponsorTime[]): string {
const hashparamSegments = segments.map(segment => ({
actionType: segment.actionType,
@@ -85,3 +103,8 @@ export function exportTimesAsHashParam(segments: SponsorTime[]): string {
return `#segments=${JSON.stringify(hashparamSegments)}`;
}
export function normalizeChapterName(description: string): string {
return description.toLowerCase().replace(/\.|:|-/g, "").replace(/\s+/g, " ");
}

View File

@@ -1,91 +1,3 @@
/** Function that can be used to wait for a condition before returning. */
async function wait<T>(condition: () => T, timeout = 5000, check = 100, predicate?: (obj: T) => boolean): Promise<T> {
return await new Promise((resolve, reject) => {
setTimeout(() => {
clearInterval(interval);
reject("TIMEOUT");
}, timeout);
const intervalCheck = () => {
const result = condition();
if (predicate ? predicate(result) : result) {
resolve(result);
clearInterval(interval);
}
};
const interval = setInterval(intervalCheck, check);
//run the check once first, this speeds it up a lot
intervalCheck();
});
}
function getFormattedTimeToSeconds(formatted: string): number | null {
const fragments = /^(?:(?:(\d+):)?(\d+):)?(\d*(?:[.,]\d+)?)$/.exec(formatted);
if (fragments === null) {
return null;
}
const hours = fragments[1] ? parseInt(fragments[1]) : 0;
const minutes = fragments[2] ? parseInt(fragments[2] || '0') : 0;
const seconds = fragments[3] ? parseFloat(fragments[3].replace(',', '.')) : 0;
return hours * 3600 + minutes * 60 + seconds;
}
function getFormattedTime(seconds: number, precise?: boolean): string {
seconds = Math.max(seconds, 0);
const hours = Math.floor(seconds / 60 / 60);
const minutes = Math.floor(seconds / 60) % 60;
let minutesDisplay = String(minutes);
let secondsNum = seconds % 60;
if (!precise) {
secondsNum = Math.floor(secondsNum);
}
let secondsDisplay = String(precise ? secondsNum.toFixed(3) : secondsNum);
if (secondsNum < 10) {
//add a zero
secondsDisplay = "0" + secondsDisplay;
}
if (hours && minutes < 10) {
//add a zero
minutesDisplay = "0" + minutesDisplay;
}
if (isNaN(hours) || isNaN(minutes)) {
return null;
}
const formatted = (hours ? hours + ":" : "") + minutesDisplay + ":" + secondsDisplay;
return formatted;
}
/**
* Gets the error message in a nice string
*
* @param {int} statusCode
* @returns {string} errorMessage
*/
function getErrorMessage(statusCode: number, responseText: string): string {
const postFix = ((responseText && !(responseText.includes(`cf-wrapper`) || responseText.includes("<!DOCTYPE html>"))) ? "\n\n" + responseText : "");
// display response body for 4xx
if([400, 429, 409, 0].includes(statusCode)) {
return chrome.i18n.getMessage(statusCode + "") + " " + chrome.i18n.getMessage("errorCode") + statusCode + postFix;
} else if (statusCode >= 500 && statusCode <= 599) {
// 503 == 502
if (statusCode == 503) statusCode = 502;
return chrome.i18n.getMessage(statusCode + "") + " " + chrome.i18n.getMessage("errorCode") + statusCode
+ "\n\n" + chrome.i18n.getMessage("statusReminder");
} else {
return chrome.i18n.getMessage("connectionError") + statusCode + postFix;
}
}
/* Gets percieved luminance of a color */
function getLuminance(color: string): number {
const {r, g, b} = hexToRgb(color);
@@ -113,44 +25,7 @@ function indexesOf<T>(array: T[], value: T): number[] {
return array.map((v, i) => v === value ? i : -1).filter(i => i !== -1);
}
function objectToURI<T>(url: string, data: T, includeQuestionMark: boolean): string {
let counter = 0;
for (const key in data) {
const seperator = (url.includes("?") || counter > 0) ? "&" : (includeQuestionMark ? "?" : "");
const value = (typeof(data[key]) === "string") ? data[key] as unknown as string : JSON.stringify(data[key]);
url += seperator + encodeURIComponent(key) + "=" + encodeURIComponent(value);
counter++;
}
return url;
}
function generateUserID(length = 36): string {
const charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
let result = "";
if (window.crypto && window.crypto.getRandomValues) {
const values = new Uint32Array(length);
window.crypto.getRandomValues(values);
for (let i = 0; i < length; i++) {
result += charset[values[i] % charset.length];
}
return result;
} else {
for (let i = 0; i < length; i++) {
result += charset[Math.floor(Math.random() * charset.length)];
}
return result;
}
}
export const GenericUtils = {
wait,
getFormattedTime,
getFormattedTimeToSeconds,
getErrorMessage,
getLuminance,
generateUserID,
indexesOf,
objectToURI
indexesOf
}

View File

@@ -1,77 +0,0 @@
import Config from "../config";
import Utils from "../utils";
import * as CompileConfig from "../../config.json";
const utils = new Utils();
export async function checkLicenseKey(licenseKey: string): Promise<boolean> {
const result = await utils.asyncRequestToServer("GET", "/api/verifyToken", {
licenseKey
});
try {
if (result.ok && JSON.parse(result.responseText).allowed) {
Config.config.payments.chaptersAllowed = true;
Config.config.showChapterInfoMessage = false;
Config.config.payments.lastCheck = Date.now();
Config.forceSyncUpdate("payments");
return true;
}
} catch (e) { } //eslint-disable-line no-empty
return false
}
/**
* The other one also tried refreshing, so returns a promise
*/
export function noRefreshFetchingChaptersAllowed(): boolean {
return Config.config.payments.chaptersAllowed || CompileConfig["freeChapterAccess"];
}
export async function fetchingChaptersAllowed(): Promise<boolean> {
if (Config.config.payments.freeAccess || CompileConfig["freeChapterAccess"]) {
return true;
}
//more than 14 days
if (Config.config.payments.licenseKey && Date.now() - Config.config.payments.lastCheck > 14 * 24 * 60 * 60 * 1000) {
const licensePromise = checkLicenseKey(Config.config.payments.licenseKey);
if (!Config.config.payments.chaptersAllowed) {
return licensePromise;
}
}
if (Config.config.payments.chaptersAllowed) return true;
if (Config.config.payments.lastCheck === 0 && Date.now() - Config.config.payments.lastFreeCheck > 2 * 24 * 60 * 60 * 1000) {
Config.config.payments.lastFreeCheck = Date.now();
Config.forceSyncUpdate("payments");
// Check for free access if no license key, and it is the first time
const result = await utils.asyncRequestToServer("GET", "/api/userInfo", {
value: "freeChaptersAccess",
userID: Config.config.userID
});
try {
if (result.ok) {
const userInfo = JSON.parse(result.responseText);
Config.config.payments.lastCheck = Date.now();
if (userInfo.freeChaptersAccess) {
Config.config.payments.freeAccess = true;
Config.config.payments.chaptersAllowed = true;
Config.config.showChapterInfoMessage = false;
Config.forceSyncUpdate("payments");
return true;
}
}
} catch (e) { } //eslint-disable-line no-empty
}
return false;
}

8
src/utils/pageCleaner.ts Normal file
View File

@@ -0,0 +1,8 @@
export function cleanPage() {
// For live-updates
if (document.readyState === "complete") {
for (const element of document.querySelectorAll("#categoryPillParent, .playerButton, .sponsorThumbnailLabel, #submissionNoticeContainer, .sponsorSkipNoticeContainer, #sponsorBlockPopupContainer, .skipButtonControlBarContainer, #previewbar, .sponsorBlockChapterBar")) {
element.remove();
}
}
}

View File

@@ -1,5 +1,5 @@
import { ActionType, Category, SponsorSourceType, SponsorTime, VideoID } from "../types";
import { GenericUtils } from "./genericUtils";
import { getFormattedTimeToSeconds } from "../../maze-utils/src/formating";
export function getControls(): HTMLElement {
const controlsSelectors = [
@@ -9,12 +9,14 @@ export function getControls(): HTMLElement {
".player-controls-top",
// Invidious/videojs video element's controls element
".vjs-control-bar",
// Piped shaka player
".shaka-bottom-controls"
];
for (const controlsSelector of controlsSelectors) {
const controls = document.querySelectorAll(controlsSelector);
const controls = Array.from(document.querySelectorAll(controlsSelector)).filter(el => !isInPreviewPlayer(el));
if (controls && controls.length > 0) {
if (controls.length > 0) {
return <HTMLElement> controls[controls.length - 1];
}
}
@@ -22,29 +24,14 @@ export function getControls(): HTMLElement {
return null;
}
export function isInPreviewPlayer(element: Element): boolean {
return !!element.closest("#inline-preview-player");
}
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.slice(1);
if (windowHash) {
@@ -68,24 +55,26 @@ export function getHashParams(): Record<string, unknown> {
export function getExistingChapters(currentVideoID: VideoID, duration: number): SponsorTime[] {
const chaptersBox = document.querySelector("ytd-macro-markers-list-renderer");
const title = document.querySelector("[target-id=engagement-panel-macro-markers-auto-chapters] #title-text");
if (title?.textContent?.includes("Key moment")) return [];
const chapters: SponsorTime[] = [];
// .ytp-timed-markers-container indicates that key-moments are present, which should not be divided
if (chaptersBox && !(document.querySelector(".ytp-timed-markers-container")?.childElementCount > 0)) {
if (chaptersBox) {
let lastSegment: SponsorTime = null;
const links = chaptersBox.querySelectorAll("ytd-macro-markers-list-item-renderer > a");
for (const link of links) {
const timeElement = link.querySelector("#time") as HTMLElement;
const description = link.querySelector("#details h4") as HTMLElement;
if (timeElement && description?.innerText?.length > 0 && link.getAttribute("href")?.includes(currentVideoID)) {
const time = GenericUtils.getFormattedTimeToSeconds(timeElement.innerText);
const time = getFormattedTimeToSeconds(timeElement.innerText.replace(/\./g, ":"));
if (time === null) return [];
if (lastSegment) {
lastSegment.segment[1] = time;
chapters.push(lastSegment);
}
lastSegment = {
segment: [time, null],
category: "chapter" as Category,
@@ -106,25 +95,6 @@ export function getExistingChapters(currentVideoID: VideoID, duration: number):
return chapters;
}
export function localizeHtmlPage(): void {
//Localize by replacing __MSG_***__ meta tags
const localizedTitle = getLocalizedMessage(document.title);
if (localizedTitle) document.title = localizedTitle;
const body = document.querySelector(".sponsorBlockPageBody");
const localizedMessage = getLocalizedMessage(body.innerHTML.toString());
if (localizedMessage) body.innerHTML = localizedMessage;
}
export function getLocalizedMessage(text: string): string | false {
const valNewH = text.replace(/__MSG_(\w+)__/g, function(match, v1) {
return v1 ? chrome.i18n.getMessage(v1).replace(/</g, "&#60;")
.replace(/"/g, "&quot;").replace(/\n/g, "<br/>") : "";
});
if (valNewH != text) {
return valNewH;
} else {
return false;
}
export function isPlayingPlaylist() {
return !!document.URL.includes("&list=");
}

115
src/utils/thumbnails.ts Normal file
View File

@@ -0,0 +1,115 @@
import { isOnInvidious, parseYouTubeVideoIDFromURL } from "../../maze-utils/src/video";
import Config from "../config";
import { getVideoLabel } from "./videoLabels";
import { setThumbnailListener } from "../../maze-utils/src/thumbnailManagement";
export async function labelThumbnails(thumbnails: HTMLImageElement[]): Promise<void> {
await Promise.all(thumbnails.map((t) => labelThumbnail(t)));
}
export async function labelThumbnail(thumbnail: HTMLImageElement): Promise<HTMLElement | null> {
if (!Config.config?.fullVideoSegments || !Config.config?.fullVideoLabelsOnThumbnails) {
hideThumbnailLabel(thumbnail);
return null;
}
const link = (isOnInvidious() ? thumbnail.parentElement : thumbnail.querySelector("#thumbnail")) as HTMLAnchorElement
if (!link || link.nodeName !== "A" || !link.href) return null; // no link found
const videoID = parseYouTubeVideoIDFromURL(link.href)?.videoID;
if (!videoID) {
hideThumbnailLabel(thumbnail);
return null;
}
const category = await getVideoLabel(videoID);
if (!category) {
hideThumbnailLabel(thumbnail);
return null;
}
const { overlay, text } = createOrGetThumbnail(thumbnail);
overlay.style.setProperty('--category-color', `var(--sb-category-preview-${category}, var(--sb-category-${category}))`);
overlay.style.setProperty('--category-text-color', `var(--sb-category-text-preview-${category}, var(--sb-category-text-${category}))`);
text.innerText = chrome.i18n.getMessage(`category_${category}`);
overlay.classList.add("sponsorThumbnailLabelVisible");
return overlay;
}
function getOldThumbnailLabel(thumbnail: HTMLImageElement): HTMLElement | null {
return thumbnail.querySelector(".sponsorThumbnailLabel") as HTMLElement | null;
}
function hideThumbnailLabel(thumbnail: HTMLImageElement): void {
const oldLabel = getOldThumbnailLabel(thumbnail);
if (oldLabel) {
oldLabel.classList.remove("sponsorThumbnailLabelVisible");
}
}
function createOrGetThumbnail(thumbnail: HTMLImageElement): { overlay: HTMLElement; text: HTMLElement } {
const oldElement = getOldThumbnailLabel(thumbnail);
if (oldElement) {
return {
overlay: oldElement as HTMLElement,
text: oldElement.querySelector("span") as HTMLElement
};
}
const overlay = document.createElement("div") as HTMLElement;
overlay.classList.add("sponsorThumbnailLabel");
// Disable hover autoplay
overlay.addEventListener("pointerenter", (e) => {
e.stopPropagation();
thumbnail.dispatchEvent(new PointerEvent("pointerleave", { bubbles: true }));
});
overlay.addEventListener("pointerleave", (e) => {
e.stopPropagation();
thumbnail.dispatchEvent(new PointerEvent("pointerenter", { bubbles: true }));
});
const icon = createSBIconElement();
const text = document.createElement("span");
overlay.appendChild(icon);
overlay.appendChild(text);
thumbnail.appendChild(overlay);
return {
overlay,
text
};
}
function createSBIconElement(): SVGSVGElement {
const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
svg.setAttribute("viewBox", "0 0 565.15 568");
const use = document.createElementNS("http://www.w3.org/2000/svg", "use");
use.setAttribute("href", "#SponsorBlockIcon");
svg.appendChild(use);
return svg;
}
// Inserts the icon svg definition, so it can be used elsewhere
function insertSBIconDefinition() {
const container = document.createElement("span");
// svg from /public/icons/PlayerStartIconSponsorBlocker.svg, with useless stuff removed
container.innerHTML = `
<svg viewBox="0 0 565.15 568" style="display: none">
<defs>
<g id="SponsorBlockIcon">
<path d="M282.58,568a65,65,0,0,1-34.14-9.66C95.41,463.94,2.54,300.46,0,121A64.91,64.91,0,0,1,34,62.91a522.56,522.56,0,0,1,497.16,0,64.91,64.91,0,0,1,34,58.12c-2.53,179.43-95.4,342.91-248.42,437.3A65,65,0,0,1,282.58,568Zm0-548.31A502.24,502.24,0,0,0,43.4,80.22a45.27,45.27,0,0,0-23.7,40.53c2.44,172.67,91.81,330,239.07,420.83a46.19,46.19,0,0,0,47.61,0C453.64,450.73,543,293.42,545.45,120.75a45.26,45.26,0,0,0-23.7-40.54A502.26,502.26,0,0,0,282.58,19.69Z"/>
<path d="M 284.70508 42.693359 A 479.9 479.9 0 0 0 54.369141 100.41992 A 22.53 22.53 0 0 0 42.669922 120.41992 C 45.069922 290.25992 135.67008 438.63977 270.83008 522.00977 A 22.48 22.48 0 0 0 294.32031 522.00977 C 429.48031 438.63977 520.08047 290.25992 522.48047 120.41992 A 22.53 22.53 0 0 0 510.7793 100.41992 A 479.9 479.9 0 0 0 284.70508 42.693359 z M 220.41016 145.74023 L 411.2793 255.93945 L 220.41016 366.14062 L 220.41016 145.74023 z "/>
</g>
</defs>
</svg>`;
document.body.appendChild(container.children[0]);
}
export function setupThumbnailListener(): void {
setThumbnailListener(labelThumbnails, () => {
insertSBIconDefinition();
}, () => Config.isReady());
}

View File

@@ -22,5 +22,7 @@ export function urlTimeToSeconds(time: string): number {
return hours * 3600 + minutes * 60 + seconds;
} else if (/\d+/.test(time)) {
return parseInt(time, 10);
} else {
return 0;
}
}

70
src/utils/videoLabels.ts Normal file
View File

@@ -0,0 +1,70 @@
import { Category, CategorySkipOption, VideoID } from "../types";
import { getHash } from "../../maze-utils/src/hash";
import Utils from "../utils";
import { logWarn } from "./logger";
const utils = new Utils();
export interface LabelCacheEntry {
timestamp: number;
videos: Record<VideoID, Category>;
}
const labelCache: Record<string, LabelCacheEntry> = {};
const cacheLimit = 1000;
async function getLabelHashBlock(hashPrefix: string): Promise<LabelCacheEntry | null> {
// Check cache
const cachedEntry = labelCache[hashPrefix];
if (cachedEntry) {
return cachedEntry;
}
const response = await utils.asyncRequestToServer("GET", `/api/videoLabels/${hashPrefix}`);
if (response.status !== 200) {
// No video labels or server down
labelCache[hashPrefix] = {
timestamp: Date.now(),
videos: {},
};
return null;
}
try {
const data = JSON.parse(response.responseText);
const newEntry: LabelCacheEntry = {
timestamp: Date.now(),
videos: Object.fromEntries(data.map(video => [video.videoID, video.segments[0].category])),
};
labelCache[hashPrefix] = newEntry;
if (Object.keys(labelCache).length > cacheLimit) {
// Remove oldest entry
const oldestEntry = Object.entries(labelCache).reduce((a, b) => a[1].timestamp < b[1].timestamp ? a : b);
delete labelCache[oldestEntry[0]];
}
return newEntry;
} catch (e) {
logWarn(`Error parsing video labels: ${e}`);
return null;
}
}
export async function getVideoLabel(videoID: VideoID): Promise<Category | null> {
const prefix = (await getHash(videoID, 1)).slice(0, 3);
const result = await getLabelHashBlock(prefix);
if (result) {
const category = result.videos[videoID];
if (category && utils.getCategorySelection(category).option !== CategorySkipOption.Disabled) {
return category;
} else {
return null;
}
}
return null;
}

View File

@@ -1,8 +1,9 @@
import { objectToURI } from "../../maze-utils/src";
import { getHash } from "../../maze-utils/src/hash";
import Config from "../config";
import GenericNotice, { NoticeOptions } from "../render/GenericNotice";
import { ContentContainer } from "../types";
import Utils from "../utils";
import { GenericUtils } from "./genericUtils";
const utils = new Utils();
export interface ChatConfig {
@@ -13,7 +14,7 @@ export interface ChatConfig {
export async function openWarningDialog(contentContainer: ContentContainer): Promise<void> {
const userInfo = await utils.asyncRequestToServer("GET", "/api/userInfo", {
userID: Config.config.userID,
publicUserID: await getHash(Config.config.userID),
values: ["warningReason"]
});
@@ -21,7 +22,7 @@ export async function openWarningDialog(contentContainer: ContentContainer): Pro
const warningReason = JSON.parse(userInfo.responseText)?.warningReason;
const userNameData = await utils.asyncRequestToServer("GET", "/api/getUsername?userID=" + Config.config.userID);
const userName = userNameData.ok ? JSON.parse(userNameData.responseText).userName : "";
const publicUserID = await utils.getHash(Config.config.userID);
const publicUserID = await getHash(Config.config.userID);
let notice: GenericNotice = null;
const options: NoticeOptions = {
@@ -62,5 +63,5 @@ export async function openWarningDialog(contentContainer: ContentContainer): Pro
}
export function openChat(config: ChatConfig): void {
window.open("https://chat.sponsor.ajay.app/#" + GenericUtils.objectToURI("", config, false));
}
window.open("https://chat.sponsor.ajay.app/#" + objectToURI("", config, false));
}