mirror of
https://github.com/ajayyy/SponsorBlock.git
synced 2025-12-25 00:48:26 +03:00
Merge branch 'master' into settings
This commit is contained in:
@@ -33,15 +33,16 @@ class CategoryPillComponent extends React.Component<CategoryPillProps, CategoryP
|
||||
|
||||
render(): React.ReactElement {
|
||||
const style: React.CSSProperties = {
|
||||
backgroundColor: Config.config.barTypes["preview-" + this.state.segment?.category]?.color,
|
||||
backgroundColor: this.getColor(),
|
||||
display: this.state.show ? "flex" : "none",
|
||||
color: this.state.segment?.category === "sponsor" ? "white" : "black",
|
||||
color: this.state.segment?.category === "sponsor"
|
||||
|| this.state.segment?.category === "exclusive_access" ? "white" : "black",
|
||||
}
|
||||
|
||||
return (
|
||||
<span style={style}
|
||||
className={"sponsorBlockCategoryPill"}
|
||||
title={chrome.i18n.getMessage("categoryPillTitleText")}
|
||||
title={this.getTitleText()}
|
||||
onClick={(e) => this.toggleOpen(e)}>
|
||||
<span className="sponsorBlockCategoryPillTitleSection">
|
||||
<img className="sponsorSkipLogo sponsorSkipObject"
|
||||
@@ -72,6 +73,12 @@ class CategoryPillComponent extends React.Component<CategoryPillProps, CategoryP
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* Close Button */}
|
||||
<img src={chrome.extension.getURL("icons/close.png")}
|
||||
className="categoryPillClose"
|
||||
onClick={() => this.setState({ show: false })}>
|
||||
</img>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
@@ -102,6 +109,17 @@ class CategoryPillComponent extends React.Component<CategoryPillProps, CategoryP
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private getColor(): string {
|
||||
const configObject = Config.config.barTypes["preview-" + this.state.segment?.category]
|
||||
|| Config.config.barTypes[this.state.segment?.category];
|
||||
return configObject?.color;
|
||||
}
|
||||
|
||||
getTitleText(): string {
|
||||
const shortDescription = chrome.i18n.getMessage(`category_${this.state.segment?.category}_pill`);
|
||||
return (shortDescription ? shortDescription + ". ": "") + chrome.i18n.getMessage("categoryPillTitleText");
|
||||
}
|
||||
}
|
||||
|
||||
export default CategoryPillComponent;
|
||||
|
||||
@@ -4,7 +4,7 @@ import Config from "../config"
|
||||
import * as CompileConfig from "../../config.json";
|
||||
import { Category, CategorySkipOption } from "../types";
|
||||
|
||||
import { getCategoryActionType } from "../utils/categoryUtils";
|
||||
import { getCategorySuffix } from "../utils/categoryUtils";
|
||||
|
||||
export interface CategorySkipOptionsProps {
|
||||
category: Category;
|
||||
@@ -78,14 +78,16 @@ class CategorySkipOptionsComponent extends React.Component<CategorySkipOptionsPr
|
||||
value={this.state.color} />
|
||||
</td>
|
||||
|
||||
<td id={this.props.category + "PreviewColorOption"}
|
||||
className="previewColorOption">
|
||||
<input
|
||||
className="categoryColorTextBox option-text-box"
|
||||
type="color"
|
||||
onChange={(event) => this.setColorState(event, true)}
|
||||
value={this.state.previewColor} />
|
||||
</td>
|
||||
{this.props.category !== "exclusive_access" &&
|
||||
<td id={this.props.category + "PreviewColorOption"}
|
||||
className="previewColorOption">
|
||||
<input
|
||||
className="categoryColorTextBox option-text-box"
|
||||
type="color"
|
||||
onChange={(event) => this.setColorState(event, true)}
|
||||
value={this.state.previewColor} />
|
||||
</td>
|
||||
}
|
||||
|
||||
</tr>
|
||||
|
||||
@@ -154,12 +156,13 @@ class CategorySkipOptionsComponent extends React.Component<CategorySkipOptionsPr
|
||||
getCategorySkipOptions(): JSX.Element[] {
|
||||
const elements: JSX.Element[] = [];
|
||||
|
||||
const optionNames = ["disable", "showOverlay", "manualSkip", "autoSkip"];
|
||||
let optionNames = ["disable", "showOverlay", "manualSkip", "autoSkip"];
|
||||
if (this.props.category === "exclusive_access") optionNames = ["disable", "showOverlay"];
|
||||
|
||||
for (const optionName of optionNames) {
|
||||
elements.push(
|
||||
<option key={optionName} value={optionName}>
|
||||
{chrome.i18n.getMessage(optionName !== "disable" ? optionName + getCategoryActionType(this.props.category)
|
||||
{chrome.i18n.getMessage(optionName !== "disable" ? optionName + getCategorySuffix(this.props.category)
|
||||
: optionName)}
|
||||
</option>
|
||||
);
|
||||
|
||||
@@ -189,7 +189,8 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
|
||||
</select>
|
||||
|
||||
{/* open in new tab */}
|
||||
<a href="https://wiki.sponsor.ajay.app/index.php/Segment_Categories"
|
||||
<a href={CompileConfig.wikiLinks[sponsorTime.category]
|
||||
|| "https://wiki.sponsor.ajay.app/index.php/Segment_Categories"}
|
||||
target="_blank" rel="noreferrer">
|
||||
<img id={"sponsorTimeCategoriesHelpButton" + this.idSuffix}
|
||||
className="helpButton"
|
||||
@@ -199,7 +200,9 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
|
||||
</div>
|
||||
|
||||
{/* Action Type */}
|
||||
{CompileConfig.categorySupport[sponsorTime.category]?.length > 1 ? (
|
||||
{CompileConfig.categorySupport[sponsorTime.category] &&
|
||||
(CompileConfig.categorySupport[sponsorTime.category]?.length > 1
|
||||
|| CompileConfig.categorySupport[sponsorTime.category]?.[0] !== "skip") ? (
|
||||
<div style={{position: "relative"}}>
|
||||
<select id={"sponsorTimeActionTypes" + this.idSuffix}
|
||||
className="sponsorTimeEditSelector sponsorTimeActionTypes"
|
||||
@@ -470,9 +473,13 @@ class SponsorTimeEditComponent extends React.Component<SponsorTimeEditProps, Spo
|
||||
}
|
||||
}
|
||||
|
||||
sponsorTimesSubmitting[this.props.index].category = this.categoryOptionRef.current.value as Category;
|
||||
sponsorTimesSubmitting[this.props.index].actionType =
|
||||
this.actionTypeOptionRef?.current ? this.actionTypeOptionRef.current.value as ActionType : ActionType.Skip;
|
||||
const category = this.categoryOptionRef.current.value as Category
|
||||
sponsorTimesSubmitting[this.props.index].category = category;
|
||||
|
||||
const inputActionType = this.actionTypeOptionRef?.current?.value as ActionType;
|
||||
const actionType = inputActionType && CompileConfig.categorySupport[category]?.includes(inputActionType) ? inputActionType as ActionType
|
||||
: CompileConfig.categorySupport[category]?.[0] ?? ActionType.Skip;
|
||||
sponsorTimesSubmitting[this.props.index].actionType = actionType;
|
||||
|
||||
Config.config.segmentTimes.set(this.props.contentContainer().sponsorVideoID, sponsorTimesSubmitting);
|
||||
|
||||
|
||||
@@ -67,6 +67,7 @@ interface SBConfig {
|
||||
"preview-sponsor": PreviewBarOption,
|
||||
"selfpromo": PreviewBarOption,
|
||||
"preview-selfpromo": PreviewBarOption,
|
||||
"exclusive_access": PreviewBarOption,
|
||||
"interaction": PreviewBarOption,
|
||||
"preview-interaction": PreviewBarOption,
|
||||
"intro": PreviewBarOption,
|
||||
@@ -224,6 +225,9 @@ const Config: SBObject = {
|
||||
}, {
|
||||
name: "poi_highlight" as Category,
|
||||
option: CategorySkipOption.ManualSkip
|
||||
}, {
|
||||
name: "exclusive_access" as Category,
|
||||
option: CategorySkipOption.ShowOverlay
|
||||
}],
|
||||
|
||||
colorPalette: {
|
||||
@@ -254,6 +258,10 @@ const Config: SBObject = {
|
||||
color: "#bfbf35",
|
||||
opacity: "0.7"
|
||||
},
|
||||
"exclusive_access": {
|
||||
color: "#008a5c",
|
||||
opacity: "0.7"
|
||||
},
|
||||
"interaction": {
|
||||
color: "#cc00ff",
|
||||
opacity: "0.7"
|
||||
@@ -405,6 +413,17 @@ function fetchConfig(): Promise<void> {
|
||||
}
|
||||
|
||||
function migrateOldFormats(config: SBConfig) {
|
||||
if (!config["exclusive_accessCategoryAdded"] && !config.categorySelections.some((s) => s.name === "exclusive_access")) {
|
||||
config["exclusive_accessCategoryAdded"] = true;
|
||||
|
||||
config.categorySelections.push({
|
||||
name: "exclusive_access" as Category,
|
||||
option: CategorySkipOption.ShowOverlay
|
||||
});
|
||||
|
||||
config.categorySelections = config.categorySelections;
|
||||
}
|
||||
|
||||
if (config["fillerUpdate"] !== undefined) {
|
||||
chrome.storage.sync.remove("fillerUpdate");
|
||||
}
|
||||
|
||||
@@ -15,9 +15,8 @@ import { Message, MessageResponse, VoteResponse } from "./messageTypes";
|
||||
import * as Chat from "./js-components/chat";
|
||||
import { getCategoryActionType } from "./utils/categoryUtils";
|
||||
import { SkipButtonControlBar } from "./js-components/skipButtonControlBar";
|
||||
import { Tooltip } from "./render/Tooltip";
|
||||
import { getStartTimeFromUrl } from "./utils/urlParser";
|
||||
import { findValidElement, getControls, isVisible } from "./utils/pageUtils";
|
||||
import { findValidElement, getControls, getHashParams, isVisible } from "./utils/pageUtils";
|
||||
import { keybindEquals } from "./utils/configUtils";
|
||||
import { CategoryPill } from "./render/CategoryPill";
|
||||
import { AnimationUtils } from "./utils/animationUtils";
|
||||
@@ -93,6 +92,8 @@ const playerButtons: Record<string, {button: HTMLButtonElement, image: HTMLImage
|
||||
|
||||
// Direct Links after the config is loaded
|
||||
utils.wait(() => Config.config !== null, 1000, 1).then(() => videoIDChange(getYouTubeVideoID(document)));
|
||||
// wait for hover preview to appear, and refresh attachments if ever found
|
||||
window.addEventListener("DOMContentLoaded", () => utils.waitForElement(".ytp-inline-preview-ui").then(() => refreshVideoAttachments()));
|
||||
addPageListeners();
|
||||
addHotkeyListener();
|
||||
|
||||
@@ -269,7 +270,7 @@ function resetValues() {
|
||||
isAdPlaying = false;
|
||||
|
||||
for (let i = 0; i < skipNotices.length; i++) {
|
||||
skipNotices.pop().close();
|
||||
skipNotices.pop()?.close();
|
||||
}
|
||||
|
||||
skipButtonControlBar?.disable();
|
||||
@@ -686,9 +687,6 @@ async function sponsorsLookup(id: string, keepOldSubmissions = true) {
|
||||
|
||||
setupVideoMutationListener();
|
||||
|
||||
//check database for sponsor times
|
||||
//made true once a setTimeout has been created to try again after a server error
|
||||
let recheckStarted = false;
|
||||
// Create categories list
|
||||
const categories: string[] = [];
|
||||
for (const categorySelection of Config.config.categorySelections) {
|
||||
@@ -696,16 +694,8 @@ async function sponsorsLookup(id: string, keepOldSubmissions = true) {
|
||||
}
|
||||
|
||||
const extraRequestData: Record<string, unknown> = {};
|
||||
const windowHash = window.location.hash.substr(1);
|
||||
if (windowHash) {
|
||||
const params: Record<string, unknown> = windowHash.split('&').reduce((acc, param) => {
|
||||
const [key, value] = param.split('=');
|
||||
acc[key] = value;
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
if (params.requiredSegment) extraRequestData.requiredSegment = params.requiredSegment;
|
||||
}
|
||||
const hashParams = getHashParams();
|
||||
if (hashParams.requiredSegment) extraRequestData.requiredSegment = hashParams.requiredSegment;
|
||||
|
||||
// Check for hashPrefix setting
|
||||
const hashPrefix = (await utils.getHash(id, 1)).substr(0, 4);
|
||||
@@ -775,18 +765,6 @@ async function sponsorsLookup(id: string, keepOldSubmissions = true) {
|
||||
sponsorLookupRetries = 0;
|
||||
} else if (response?.status === 404) {
|
||||
retryFetch();
|
||||
} else if (sponsorLookupRetries < 15 && !recheckStarted) {
|
||||
recheckStarted = true;
|
||||
|
||||
//TODO lower when server becomes better (back to 1 second)
|
||||
//some error occurred, try again in a second
|
||||
setTimeout(() => {
|
||||
if (sponsorVideoID && sponsorTimes?.length === 0) {
|
||||
sponsorsLookup(sponsorVideoID);
|
||||
}
|
||||
}, 5000 + Math.random() * 15000 + 5000 * sponsorLookupRetries);
|
||||
|
||||
sponsorLookupRetries++;
|
||||
}
|
||||
|
||||
lookupVipInformation(id);
|
||||
@@ -957,7 +935,7 @@ function getYouTubeVideoID(document: Document): string | boolean {
|
||||
// skip to document if matches pattern
|
||||
if (url.includes("/channel/") || url.includes("/user/") || url.includes("/c/")) return getYouTubeVideoIDFromDocument(document);
|
||||
// not sure, try URL then document
|
||||
return getYouTubeVideoIDFromURL(url) || getYouTubeVideoIDFromDocument(document);
|
||||
return getYouTubeVideoIDFromURL(url) || getYouTubeVideoIDFromDocument(document, false);
|
||||
}
|
||||
|
||||
function getYouTubeVideoIDFromDocument(document: Document, hideIcon = true): string | boolean {
|
||||
@@ -1407,8 +1385,9 @@ function shouldAutoSkip(segment: SponsorTime): boolean {
|
||||
}
|
||||
|
||||
function shouldSkip(segment: SponsorTime): boolean {
|
||||
return utils.getCategorySelection(segment.category)?.option !== CategorySkipOption.ShowOverlay ||
|
||||
(Config.config.autoSkipOnMusicVideos && sponsorTimes?.some((s) => s.category === "music_offtopic"));
|
||||
return (segment.actionType !== ActionType.Full
|
||||
&& utils.getCategorySelection(segment.category)?.option !== CategorySkipOption.ShowOverlay)
|
||||
|| (Config.config.autoSkipOnMusicVideos && sponsorTimes?.some((s) => s.category === "music_offtopic"));
|
||||
}
|
||||
|
||||
/** Creates any missing buttons on the YouTube player if possible. */
|
||||
@@ -1586,6 +1565,8 @@ function updateSponsorTimesSubmitting(getFromConfig = true) {
|
||||
if (submissionNotice !== null) {
|
||||
submissionNotice.update();
|
||||
}
|
||||
|
||||
checkForPreloadedSegment();
|
||||
}
|
||||
|
||||
function openInfoMenu() {
|
||||
@@ -1807,6 +1788,12 @@ function submitSponsorTimes() {
|
||||
//send the message to the background js
|
||||
//called after all the checks have been made that it's okay to do so
|
||||
async function sendSubmitMessage() {
|
||||
// Block if submitting on a running livestream or premiere
|
||||
if (isVisible(document.querySelector(".ytp-live-badge"))) {
|
||||
alert(chrome.i18n.getMessage("liveOrPremiere"));
|
||||
return;
|
||||
}
|
||||
|
||||
// Add loading animation
|
||||
playerButtons.submit.image.src = chrome.extension.getURL("icons/PlayerUploadIconSponsorBlocker.svg");
|
||||
const stopAnimation = AnimationUtils.applyLoadingAnimation(playerButtons.submit.button, 1, () => updateEditButtonsOnPlayer());
|
||||
@@ -1824,8 +1811,8 @@ async function sendSubmitMessage() {
|
||||
// Check to see if any of the submissions are below the minimum duration set
|
||||
if (Config.config.minDuration > 0) {
|
||||
for (let i = 0; i < sponsorTimesSubmitting.length; i++) {
|
||||
if (sponsorTimesSubmitting[i].segment[1] - sponsorTimesSubmitting[i].segment[0] < Config.config.minDuration
|
||||
&& getCategoryActionType(sponsorTimesSubmitting[i].category) !== CategoryActionType.POI) {
|
||||
const duration = sponsorTimesSubmitting[i].segment[1] - sponsorTimesSubmitting[i].segment[0];
|
||||
if (duration > 0 && duration < Config.config.minDuration) {
|
||||
const confirmShort = chrome.i18n.getMessage("shortCheck") + "\n\n" +
|
||||
getSegmentsMessage(sponsorTimesSubmitting);
|
||||
|
||||
@@ -2043,3 +2030,24 @@ function showTimeWithoutSkips(skippedDuration: number): void {
|
||||
|
||||
duration.innerText = (durationAfterSkips == null || skippedDuration <= 0) ? "" : " (" + durationAfterSkips + ")";
|
||||
}
|
||||
|
||||
function checkForPreloadedSegment() {
|
||||
const hashParams = getHashParams();
|
||||
|
||||
const segments = hashParams.segments;
|
||||
if (Array.isArray(segments)) {
|
||||
for (const segment of segments) {
|
||||
if (Array.isArray(segment.segment)) {
|
||||
if (!sponsorTimesSubmitting.some((s) => s.segment[0] === segment.segment[0] && s.segment[1] === s.segment[1])) {
|
||||
sponsorTimesSubmitting.push({
|
||||
segment: segment.segment,
|
||||
UUID: utils.generateUserID() as SegmentUUID,
|
||||
category: segment.category ? segment.category : Config.config.defaultCategory,
|
||||
actionType: segment.actionType ? segment.actionType : ActionType.Skip,
|
||||
source: SponsorSourceType.Local
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -23,8 +23,8 @@ export class CategoryPill {
|
||||
vote: (type: number, UUID: SegmentUUID, category?: Category) => Promise<VoteResponse>): Promise<void> {
|
||||
const referenceNode =
|
||||
await GenericUtils.wait(() =>
|
||||
// YouTube, Mobile YouTube, Invidious
|
||||
document.querySelector(".ytd-video-primary-info-renderer.title, .slim-video-information-title, #player-container + .h-box > h1") as HTMLElement);
|
||||
// New YouTube Title, YouTube, Mobile YouTube, Invidious
|
||||
document.querySelector("#title h1, .ytd-video-primary-info-renderer.title, .slim-video-information-title, #player-container + .h-box > h1") as HTMLElement);
|
||||
|
||||
if (referenceNode && !referenceNode.contains(this.container)) {
|
||||
this.container = document.createElement('span');
|
||||
|
||||
53
src/utils.ts
53
src/utils.ts
@@ -21,6 +21,10 @@ export default class Utils {
|
||||
"popup.css"
|
||||
];
|
||||
|
||||
/* Used for waitForElement */
|
||||
waitingMutationObserver:MutationObserver = null;
|
||||
waitingElements: { selector: string, callback: (element: Element) => void }[] = [];
|
||||
|
||||
constructor(backgroundScriptContainer: BackgroundScriptContainer = null) {
|
||||
this.backgroundScriptContainer = backgroundScriptContainer;
|
||||
}
|
||||
@@ -29,6 +33,41 @@ export default class Utils {
|
||||
return GenericUtils.wait(condition, timeout, check);
|
||||
}
|
||||
|
||||
/* Uses a mutation observer to wait asynchronously */
|
||||
async waitForElement(selector: string): Promise<Element> {
|
||||
return await new Promise((resolve) => {
|
||||
this.waitingElements.push({
|
||||
selector,
|
||||
callback: resolve
|
||||
});
|
||||
|
||||
if (!this.waitingMutationObserver) {
|
||||
this.waitingMutationObserver = new MutationObserver(() => {
|
||||
const foundSelectors = [];
|
||||
for (const { selector, callback } of this.waitingElements) {
|
||||
const element = document.querySelector(selector);
|
||||
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.waitingMutationObserver.observe(document.body, {
|
||||
childList: true,
|
||||
subtree: true
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
containsPermission(permissions: chrome.permissions.Permissions): Promise<boolean> {
|
||||
return new Promise((resolve) => {
|
||||
chrome.permissions.contains(permissions, resolve)
|
||||
@@ -331,9 +370,9 @@ export default class Utils {
|
||||
|
||||
findReferenceNode(): HTMLElement {
|
||||
const selectors = [
|
||||
"#player-container-id",
|
||||
"#movie_player",
|
||||
"#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
|
||||
@@ -343,13 +382,15 @@ export default class Utils {
|
||||
//for embeds
|
||||
const player = document.getElementById("player");
|
||||
referenceNode = player.firstChild as HTMLElement;
|
||||
let index = 1;
|
||||
if (referenceNode) {
|
||||
let index = 1;
|
||||
|
||||
//find the child that is the video player (sometimes it is not the first)
|
||||
while (index < player.children.length && (!referenceNode.classList.contains("html5-video-player") || !referenceNode.classList.contains("ytp-embed"))) {
|
||||
referenceNode = player.children[index] as HTMLElement;
|
||||
//find the child that is the video player (sometimes it is not the first)
|
||||
while (index < player.children.length && (!referenceNode.classList?.contains("html5-video-player") || !referenceNode.classList?.contains("ytp-embed"))) {
|
||||
referenceNode = player.children[index] as HTMLElement;
|
||||
|
||||
index++;
|
||||
index++;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -44,4 +44,14 @@ export function getCategoryActionType(category: Category): CategoryActionType {
|
||||
} else {
|
||||
return CategoryActionType.Skippable;
|
||||
}
|
||||
}
|
||||
|
||||
export function getCategorySuffix(category: Category): string {
|
||||
if (category.startsWith("poi_")) {
|
||||
return "_POI";
|
||||
} else if (category === "exclusive_access") {
|
||||
return "_full";
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
}
|
||||
@@ -40,4 +40,25 @@ function findValidElementFromGenerator<T>(objects: T[] | NodeListOf<HTMLElement>
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export function getHashParams(): Record<string, unknown> {
|
||||
const windowHash = window.location.hash.substr(1);
|
||||
if (windowHash) {
|
||||
const params: Record<string, unknown> = windowHash.split('&').reduce((acc, param) => {
|
||||
const [key, value] = param.split('=');
|
||||
const decoded = decodeURIComponent(value);
|
||||
try {
|
||||
acc[key] = decoded?.match(/{|\[/) ? JSON.parse(decoded) : value;
|
||||
} catch (e) {
|
||||
console.error(`Failed to parse hash parameter ${key}: ${value}`);
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
return params;
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
Reference in New Issue
Block a user