Merge branch 'master' into settings

This commit is contained in:
Ajay Ramachandran
2022-01-21 12:28:47 -05:00
committed by GitHub
41 changed files with 1281 additions and 360 deletions

View File

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

View File

@@ -4,7 +4,7 @@ import Config from "../config"
import * as CompileConfig from "../../config.json";
import { Category, CategorySkipOption } from "../types";
import { getCategoryActionType } from "../utils/categoryUtils";
import { getCategorySuffix } from "../utils/categoryUtils";
export interface CategorySkipOptionsProps {
category: Category;
@@ -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>
);

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -44,4 +44,14 @@ export function getCategoryActionType(category: Category): CategoryActionType {
} else {
return CategoryActionType.Skippable;
}
}
export function getCategorySuffix(category: Category): string {
if (category.startsWith("poi_")) {
return "_POI";
} else if (category === "exclusive_access") {
return "_full";
} else {
return "";
}
}

View File

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