Merge master

This commit is contained in:
Max Baumann
2020-12-18 18:35:26 +01:00
49 changed files with 329 additions and 942 deletions

View File

@@ -2,8 +2,10 @@ import * as CompileConfig from "../config.json";
import Config from "./config";
import { Registration } from "./types";
// Make the config public for debugging purposes
(<any> window).SB = Config;
window.SB = Config;
import Utils from "./utils";
const utils = new Utils({
@@ -33,7 +35,7 @@ chrome.runtime.onMessage.addListener(function (request, sender, callback) {
chrome.runtime.openOptionsPage();
return;
case "openHelp":
window.open(chrome.runtime.getURL('help/index_en.html'));
chrome.tabs.create({url: chrome.runtime.getURL('help/index_en.html')});
return;
case "sendRequest":
sendRequestToCustomServer(request.type, request.url, request.data).then(async (response) => {
@@ -70,7 +72,7 @@ chrome.runtime.onMessage.addListener(function (request, sender, callback) {
});
//add help page on install
chrome.runtime.onInstalled.addListener(function (object) {
chrome.runtime.onInstalled.addListener(function () {
// This let's the config sync to run fully before checking.
// This is required on Firefox
setTimeout(function() {

View File

@@ -1,6 +1,5 @@
import * as React from "react";
import Config from "../config"
import * as CompileConfig from "../../config.json";
import CategorySkipOptionsComponent from "./CategorySkipOptionsComponent";

View File

@@ -2,9 +2,6 @@ import * as React from "react";
import Config from "../config"
import { CategorySkipOption } from "../types";
import Utils from "../utils";
const utils = new Utils();
export interface CategorySkipOptionsProps {
category: string;

View File

@@ -28,7 +28,7 @@ export interface NoticeState {
class NoticeComponent extends React.Component<NoticeProps, NoticeState> {
countdownInterval: NodeJS.Timeout;
idSuffix: any;
idSuffix: string;
amountOfPreviousNotices: number;

View File

@@ -3,7 +3,7 @@ import * as React from "react";
export interface NoticeTextSelectionProps {
text: string,
idSuffix: string,
onClick?: (event: React.MouseEvent) => any
onClick?: (event: React.MouseEvent) => unknown
}
export interface NoticeTextSelectionState {

View File

@@ -2,10 +2,6 @@ import * as React from "react";
import * as CompileConfig from "../../config.json";
import Config from "../config"
import { ContentContainer, SponsorHideType, SponsorTime } from "../types";
import Utils from "../utils";
const utils = new Utils();
import NoticeComponent from "./NoticeComponent";
import NoticeTextSelectionComponent from "./NoticeTextSectionComponent";
@@ -42,7 +38,7 @@ export interface SkipNoticeState {
downvoting: boolean;
choosingCategory: boolean;
thanksForVotingText: boolean; //null until the voting buttons should be hidden
thanksForVotingText: string; //null until the voting buttons should be hidden
actionState: SkipNoticeAction;
}
@@ -447,7 +443,7 @@ class SkipNoticeComponent extends React.Component<SkipNoticeProps, SkipNoticeSta
});
}
getUnskippedModeInfo(index: number, buttonText: string) {
getUnskippedModeInfo(index: number, buttonText: string): SkipNoticeState {
const self = this;
const maxCountdownTime = function() {
const sponsorTime = self.segments[index];
@@ -458,14 +454,11 @@ class SkipNoticeComponent extends React.Component<SkipNoticeProps, SkipNoticeSta
return {
unskipText: buttonText,
unskipCallback: (index) => this.reskip(index),
//change max duration to however much of the sponsor is left
// change max duration to however much of the sponsor is left
maxCountdownTime: maxCountdownTime,
countdownTime: maxCountdownTime()
}
} as SkipNoticeState;
}
reskip(index: number): void {
@@ -508,7 +501,7 @@ class SkipNoticeComponent extends React.Component<SkipNoticeProps, SkipNoticeSta
}
}
setNoticeInfoMessageWithOnClick(onClick: (event: React.MouseEvent) => any, ...messages: string[]): void {
setNoticeInfoMessageWithOnClick(onClick: (event: React.MouseEvent) => unknown, ...messages: string[]): void {
this.setState({
messages,
messageOnClick: (event) => onClick(event)
@@ -521,7 +514,7 @@ class SkipNoticeComponent extends React.Component<SkipNoticeProps, SkipNoticeSta
});
}
addVoteButtonInfo(message): void {
addVoteButtonInfo(message: string): void {
this.setState({
thanksForVotingText: message
});

View File

@@ -1,5 +1,5 @@
import * as CompileConfig from "../config.json";
import { CategorySelection, CategorySkipOption, PreviewBarOption, SponsorTime } from "./types";
import { CategorySelection, CategorySkipOption, PreviewBarOption, SponsorTime, StorageChangesObject } from "./types";
import Utils from "./utils";
const utils = new Utils();
@@ -58,8 +58,8 @@ interface SBConfig {
}
}
interface SBObject {
configListeners: Array<Function>;
export interface SBObject {
configListeners: Array<(changes: StorageChangesObject) => unknown>;
defaults: SBConfig;
localConfig: SBConfig;
config: SBConfig;
@@ -275,8 +275,8 @@ function decodeStoredItem<T>(id: string, data: T): T | SBMap<string, SponsorTime
return data;
}
function configProxy(): any {
chrome.storage.onChanged.addListener((changes, namespace) => {
function configProxy(): SBConfig {
chrome.storage.onChanged.addListener((changes: {[key: string]: chrome.storage.StorageChange}) => {
for (const key in changes) {
Config.localConfig[key] = decodeStoredItem(key, changes[key].newValue);
}
@@ -286,8 +286,8 @@ function configProxy(): any {
}
});
const handler: ProxyHandler<any> = {
set(obj, prop, value) {
const handler: ProxyHandler<SBConfig> = {
set<K extends keyof SBConfig>(obj: SBConfig, prop: K, value: SBConfig[K]) {
Config.localConfig[prop] = value;
chrome.storage.sync.set({
@@ -297,13 +297,13 @@ function configProxy(): any {
return true;
},
get(obj, prop): any {
get<K extends keyof SBConfig>(obj: SBConfig, prop: K): SBConfig[K] {
const data = Config.localConfig[prop];
return obj[prop] || data;
},
deleteProperty(obj, prop) {
deleteProperty(obj: SBConfig, prop: keyof SBConfig) {
chrome.storage.sync.remove(<string> prop);
return true;
@@ -311,11 +311,11 @@ function configProxy(): any {
};
return new Proxy({handler}, handler);
return new Proxy<SBConfig>({handler} as unknown as SBConfig, handler);
}
function fetchConfig(): Promise<void> {
return new Promise((resolve, reject) => {
return new Promise((resolve) => {
chrome.storage.sync.get(null, function(items) {
Config.localConfig = <SBConfig> <unknown> items; // Data is ready
resolve();
@@ -439,11 +439,6 @@ async function setupConfig() {
Config.config = config;
}
// Reset config
function resetConfig() {
Config.config = Config.defaults;
}
function convertJSON(): void {
Object.keys(Config.localConfig).forEach(key => {
Config.localConfig[key] = decodeStoredItem(key, Config.localConfig[key]);

View File

@@ -1,6 +1,6 @@
import Config from "./config";
import { SponsorTime, CategorySkipOption, CategorySelection, VideoID, SponsorHideType, FetchResponse } from "./types";
import { SponsorTime, CategorySkipOption, VideoID, SponsorHideType, FetchResponse, VideoInfo, StorageChangesObject } from "./types";
import { ContentContainer } from "./types";
import Utils from "./utils";
@@ -26,7 +26,7 @@ let sponsorTimes: SponsorTime[] = null;
let sponsorVideoID: VideoID = null;
// JSON video info
let videoInfo: any = null;
let videoInfo: VideoInfo = null;
//the channel this video is about
let channelID: string;
@@ -54,6 +54,9 @@ let durationListenerSetUp = false;
// Is the video currently being switched
let switchingVideos = null;
// Made true every videoID change
let firstEvent = false;
// Used by the play and playing listeners to make sure two aren't
// called at the same time
let lastCheckTime = 0;
@@ -179,7 +182,7 @@ function messageListener(request: Message, sender: unknown, sendResponse: (respo
*
* @param {String} changes
*/
function contentConfigUpdateListener(changes) {
function contentConfigUpdateListener(changes: StorageChangesObject) {
for (const key in changes) {
switch(key) {
case "hideVideoPlayerControls":
@@ -244,6 +247,8 @@ function resetValues() {
switchingVideos = true;
}
firstEvent = true;
// Reset advert playing flag
isAdPlaying = false;
}
@@ -363,7 +368,7 @@ function handleMobileControlsMutations(): void {
if (previewBar !== null) {
if (document.body.contains(previewBar.container)) {
updatePreviewBarPositionMobile(document.getElementsByClassName(mobileYouTubeSelector)[0]);
updatePreviewBarPositionMobile(document.getElementsByClassName(mobileYouTubeSelector)[0] as HTMLElement);
return;
} else {
@@ -397,7 +402,7 @@ function createPreviewBar(): void {
const el = document.querySelectorAll(selector);
if (el && el.length && el[0]) {
previewBar = new PreviewBar(el[0], onMobileYouTube, onInvidious);
previewBar = new PreviewBar(el[0] as HTMLElement, onMobileYouTube, onInvidious);
updatePreviewBar();
@@ -410,7 +415,7 @@ function createPreviewBar(): void {
* Triggered every time the video duration changes.
* This happens when the resolution changes or at random time to clear memory.
*/
function durationChangeListener() {
function durationChangeListener(): void {
updateAdFlag();
updatePreviewBar();
}
@@ -545,6 +550,12 @@ async function sponsorsLookup(id: string) {
video.addEventListener('play', () => {
switchingVideos = false;
// If it is not the first event, then the only way to get to 0 is if there is a seek event
// This check makes sure that changing the video resolution doesn't cause the extension to think it
// gone back to the begining
if (!firstEvent && video.currentTime === 0) return;
firstEvent = false;
// Check if an ad is playing
updateAdFlag();
@@ -578,6 +589,8 @@ async function sponsorsLookup(id: string) {
}
});
video.addEventListener('ratechange', () => startSponsorSchedule());
// Used by videospeed extension (https://github.com/igrigorik/videospeed/pull/740)
video.addEventListener('videoSpeed_ratechange', () => startSponsorSchedule());
video.addEventListener('pause', () => {
// Reset lastCheckVideoTime
lastCheckVideoTime = -1;
@@ -740,7 +753,7 @@ function startSkipScheduleCheckingForStartSponsors() {
* Get the video info for the current tab from YouTube
*/
function getVideoInfo() {
sendRequestToCustomServer('GET', "https://www.youtube.com/get_video_info?video_id=" + sponsorVideoID, function(xmlhttp, error) {
sendRequestToCustomServer('GET', "https://www.youtube.com/get_video_info?video_id=" + sponsorVideoID, function(xmlhttp) {
if (xmlhttp.readyState == 4 && xmlhttp.status == 200) {
const decodedData = decodeURIComponent(xmlhttp.responseText).match(/player_response=([^&]*)/)[1];
if (!decodedData) {
@@ -798,13 +811,13 @@ function getYouTubeVideoID(url: string) {
/**
* This function is required on mobile YouTube and will keep getting called whenever the preview bar disapears
*/
function updatePreviewBarPositionMobile(parent: Element) {
function updatePreviewBarPositionMobile(parent: HTMLElement) {
if (document.getElementById("previewbar") === null) {
previewBar.updatePosition(parent);
}
}
function updatePreviewBar() {
function updatePreviewBar(): void {
if(isAdPlaying) {
previewBar.set([], [], 0);
return;
@@ -1051,7 +1064,7 @@ function createButton(baseID, title, callback, imageName, isDraggable=false): bo
newButton.classList.add("playerButton");
newButton.classList.add("ytp-button");
newButton.setAttribute("title", chrome.i18n.getMessage(title));
newButton.addEventListener("click", (event: Event) => {
newButton.addEventListener("click", () => {
callback();
});
@@ -1435,8 +1448,6 @@ function submitSponsorTimes() {
//it can't update to this info yet
closeInfoMenu();
const currentVideoID = sponsorVideoID;
if (sponsorTimesSubmitting !== undefined && sponsorTimesSubmitting.length > 0) {
submissionNotice = new SubmissionNotice(skipNoticeContentContainer, sendSubmitMessage);
}
@@ -1583,7 +1594,7 @@ function sendRequestToCustomServer(type, fullAddress, callback) {
callback(xmlhttp, false);
};
xmlhttp.onerror = function(ev) {
xmlhttp.onerror = function() {
callback(xmlhttp, true);
};
}
@@ -1595,7 +1606,7 @@ function sendRequestToCustomServer(type, fullAddress, callback) {
/**
* Update the isAdPlaying flag and hide preview bar/controls if ad is playing
*/
function updateAdFlag() {
function updateAdFlag(): void {
const wasAdPlaying = isAdPlaying;
isAdPlaying = document.getElementsByClassName('ad-showing').length > 0;

19
src/globals.d.ts vendored Normal file
View File

@@ -0,0 +1,19 @@
import { SBObject } from "./config";
declare global {
interface Window { SB: SBObject; }
// Remove this once the API becomes stable and types are shipped in @types/chrome
namespace chrome {
namespace declarativeContent {
export interface RequestContentScriptOptions {
allFrames?: boolean;
css?: string[];
instanceType?: "declarativeContent.RequestContentScript";
js?: string[];
matchAboutBlanck?: boolean;
}
export class RequestContentScript {
constructor(options: RequestContentScriptOptions);
}
}
}
}

View File

@@ -11,14 +11,14 @@ const utils = new Utils();
class PreviewBar {
container: HTMLUListElement;
parent: any;
parent: HTMLElement;
onMobileYouTube: boolean;
onInvidious: boolean;
timestamps: number[][];
types: string[];
constructor(parent: any, onMobileYouTube: boolean, onInvidious: boolean) {
constructor(parent: HTMLElement, onMobileYouTube: boolean, onInvidious: boolean) {
this.container = document.createElement('ul');
this.container.id = 'previewbar';
this.parent = parent;
@@ -47,16 +47,16 @@ class PreviewBar {
let mouseOnSeekBar = false;
seekBar.addEventListener("mouseenter", (event) => {
seekBar.addEventListener("mouseenter", () => {
mouseOnSeekBar = true;
});
seekBar.addEventListener("mouseleave", (event) => {
seekBar.addEventListener("mouseleave", () => {
mouseOnSeekBar = false;
categoryTooltip.classList.add("sbHidden");
});
const observer = new MutationObserver((mutations, observer) => {
const observer = new MutationObserver((mutations) => {
if (!mouseOnSeekBar) return;
// See if mutation observed is only this ID (if so, ignore)
@@ -112,7 +112,7 @@ class PreviewBar {
});
}
updatePosition(parent: any): void {
updatePosition(parent: HTMLElement): void {
//below the seek bar
// this.parent.insertAdjacentElement("afterEnd", this.container);
@@ -126,7 +126,7 @@ class PreviewBar {
}
//on the seek bar
this.parent.insertAdjacentElement("afterBegin", this.container);
this.parent.insertAdjacentElement("afterbegin", this.container);
}
updateColor(segment: string, color: string, opacity: string): void {

View File

@@ -2,7 +2,7 @@ import Config from "./config";
import * as CompileConfig from "../config.json";
// Make the config public for debugging purposes
(<any> window).SB = Config;
window.SB = Config;
import Utils from "./utils";
import CategoryChooser from "./render/CategoryChooser";
@@ -107,7 +107,7 @@ async function init() {
// Permission needed on Firefox
if (utils.isFirefox()) {
const permissionSuccess = await new Promise((resolve, reject) => {
const permissionSuccess = await new Promise((resolve) => {
chrome.permissions.request({
origins: [textChangeInput.value + "/"],
permissions: []
@@ -202,7 +202,7 @@ async function init() {
*
* @param {String} element
*/
function optionsConfigUpdateListener(changes) {
function optionsConfigUpdateListener() {
const optionsContainer = document.getElementById("options");
const optionsElements = optionsContainer.querySelectorAll("*");
@@ -243,7 +243,7 @@ function invidiousInstanceAddInit(element: HTMLElement, option: string) {
const button = element.querySelector(".trigger-button");
const setButton = element.querySelector(".text-change-set");
setButton.addEventListener("click", async function(e) {
setButton.addEventListener("click", async function() {
if (textBox.value == "" || textBox.value.includes("/") || textBox.value.includes("http")) {
alert(chrome.i18n.getMessage("addInvidiousInstanceError"));
} else {
@@ -269,7 +269,7 @@ function invidiousInstanceAddInit(element: HTMLElement, option: string) {
});
const resetButton = element.querySelector(".invidious-instance-reset");
resetButton.addEventListener("click", function(e) {
resetButton.addEventListener("click", function() {
if (confirm(chrome.i18n.getMessage("resetInvidiousInstanceAlert"))) {
// Set to a clone of the default
Config.config[option] = Config.defaults[option].slice(0);
@@ -536,7 +536,7 @@ function copyDebugOutputToClipboard() {
.then(() => {
alert(chrome.i18n.getMessage("copyDebugInformationComplete"));
})
.catch((err) => {
.catch(() => {
alert(chrome.i18n.getMessage("copyDebugInformationFailed"));
});
}
}

View File

@@ -38,6 +38,8 @@ class MessageHandler {
}
}
//make this a function to allow this to run on the content page
async function runThePopup(messageListener?: MessageListener): Promise<void> {
const messageHandler = new MessageHandler(messageListener);
@@ -46,7 +48,14 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
await utils.wait(() => Config.config !== null);
const PageElements: any = {};
type InputPageElements = {
whitelistToggle?: HTMLInputElement,
toggleSwitch?: HTMLInputElement,
usernameInput?: HTMLInputElement,
};
type PageElements = { [key: string]: HTMLElement } & InputPageElements
const PageElements: PageElements = {};
[
"sponsorblockPopup",
@@ -376,7 +385,7 @@ async function runThePopup(messageListener?: MessageListener): Promise<void> {
const sponsorTimeButton = document.createElement("button");
sponsorTimeButton.className = "segmentTimeButton popupElement";
const prefix = chrome.i18n.getMessage("category_" + segmentTimes[i].category) + ": ";
const prefix = utils.shortCategoryName(segmentTimes[i].category) + ": ";
let extraInfo = "";
if (segmentTimes[i].hidden === SponsorHideType.Downvoted) {

View File

@@ -78,8 +78,86 @@ interface BackgroundScriptContainer {
unregisterFirefoxContentScript: (id: string) => void
}
interface VideoInfo {
responseContext: {
serviceTrackingParams: Array<{service: string, params: Array<{key: string, value: string}>}>,
webResponseContextExtensionData: {
hasDecorated: boolean
}
},
playabilityStatus: {
status: string,
playableInEmbed: boolean,
miniplayer: {
miniplayerRenderer: {
playbackMode: string
}
}
};
streamingData: unknown;
playbackTracking: unknown;
videoDetails: {
videoId: string,
title: string,
lengthSeconds: string,
keywords: string[],
channelId: string,
isOwnerViewing: boolean,
shortDescription: string,
isCrawlable: boolean,
thumbnail: {
thumbnails: Array<{url: string, width: number, height: number}>
},
averageRating: number,
allowRatings: boolean,
viewCount: string,
author: string,
isPrivate: boolean,
isUnpluggedCorpus: boolean,
isLiveContent: boolean,
};
playerConfig: unknown;
storyboards: unknown;
microformat: {
playerMicroformatRenderer: {
thumbnail: {
thumbnails: Array<{url: string, width: number, height: number}>
},
embed: {
iframeUrl: string,
flashUrl: string,
width: number,
height: number,
flashSecureUrl: string,
},
title: {
simpleText: string,
},
description: {
simpleText: string,
},
lengthSeconds: string,
ownerProfileUrl: string,
externalChannelId: string,
availableCountries: string[],
isUnlisted: boolean,
hasYpcMetadata: boolean,
viewCount: string,
category: string,
publishDate: string,
ownerChannelName: string,
uploadDate: string,
}
};
trackingParams: string;
attestation: unknown;
messages: unknown;
}
type VideoID = string;
type StorageChangesObject = { [key: string]: chrome.storage.StorageChange };
export {
FetchResponse,
VideoDurationResponse,
@@ -91,5 +169,7 @@ export {
SponsorHideType,
PreviewBarOption,
Registration,
BackgroundScriptContainer
BackgroundScriptContainer,
VideoInfo,
StorageChangesObject,
};

View File

@@ -119,8 +119,7 @@ class Utils {
const rule = {
id: "invidious",
conditions,
// This API is experimental and not visible by the TypeScript compiler
actions: [new (<any> chrome.declarativeContent).RequestContentScript({
actions: [new chrome.declarativeContent.RequestContentScript({
allFrames: true,
js: self.js,
css: self.css
@@ -206,7 +205,7 @@ class Utils {
getLocalizedMessage(text: string): string | false {
const valNewH = text.replace(/__MSG_(\w+)__/g, function(match, v1) {
return v1 ? chrome.i18n.getMessage(v1) : "";
return v1 ? chrome.i18n.getMessage(v1).replace("\n", "<br/>") : "";
});
if(valNewH != text) {