Started conversion to TypeScript.

This commit is contained in:
Ajay Ramachandran
2020-01-28 22:16:48 -05:00
parent 5837205a9a
commit 03836b69f2
52 changed files with 5729 additions and 418 deletions

2
.gitignore vendored
View File

@@ -4,3 +4,5 @@ ignored
node_modules node_modules
web-ext-artifacts web-ext-artifacts
.vscode/ .vscode/
dist/
tmp/

BIN
icon.png

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.4 KiB

8
jest.config.js Normal file
View File

@@ -0,0 +1,8 @@
module.exports = {
"roots": [
"src"
],
"transform": {
"^.+\\.ts$": "ts-jest"
},
};

5237
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -5,12 +5,27 @@
"main": "background.js", "main": "background.js",
"dependencies": {}, "dependencies": {},
"devDependencies": { "devDependencies": {
"web-ext": "^4.0.0" "web-ext": "^4.0.0",
"@types/chrome": "0.0.91",
"@types/firefox-webext-browser": "70.0.1",
"@types/jest": "^24.0.23",
"@types/jquery": "^3.3.31",
"copy-webpack-plugin": "^5.0.5",
"jest": "^24.9.0",
"ts-jest": "^24.2.0",
"rimraf": "^3.0.0",
"ts-loader": "^6.2.1",
"typescript": "~3.7.3",
"webpack": "~4.41.2",
"webpack-cli": "~3.3.10",
"webpack-merge": "~4.2.2"
}, },
"scripts": { "scripts": {
"test": "echo \"Error: no test specified\" && exit 1", "dev": "cd dist && web-ext run --start-url https://chrome.google.com/webstore/detail/ublock-origin/cjpalhdlnbpafiamejdnhcphjbkeiagm",
"dev": "web-ext run --start-url https://chrome.google.com/webstore/detail/ublock-origin/cjpalhdlnbpafiamejdnhcphjbkeiagm", "watch": "webpack --config webpack/webpack.dev.js --watch",
"build": "web-ext build --overwrite-dest -i \"*(package-lock.json|README.md|package.json|config.js.example|firefox_manifest-extra.json|manifest.json.original|ignored|crowdin.yml)\"" "build": "webpack --config webpack/webpack.prod.js",
"clean": "rimraf dist",
"test": "npx jest"
}, },
"repository": { "repository": {
"type": "git", "type": "git",

View File

Before

Width:  |  Height:  |  Size: 551 B

After

Width:  |  Height:  |  Size: 551 B

View File

Before

Width:  |  Height:  |  Size: 6.7 KiB

After

Width:  |  Height:  |  Size: 6.7 KiB

View File

Before

Width:  |  Height:  |  Size: 1.2 KiB

After

Width:  |  Height:  |  Size: 1.2 KiB

View File

Before

Width:  |  Height:  |  Size: 9.1 KiB

After

Width:  |  Height:  |  Size: 9.1 KiB

View File

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

View File

Before

Width:  |  Height:  |  Size: 4.3 KiB

After

Width:  |  Height:  |  Size: 4.3 KiB

View File

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

View File

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

View File

Before

Width:  |  Height:  |  Size: 6.1 KiB

After

Width:  |  Height:  |  Size: 6.1 KiB

View File

Before

Width:  |  Height:  |  Size: 7.1 KiB

After

Width:  |  Height:  |  Size: 7.1 KiB

View File

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 11 KiB

View File

Before

Width:  |  Height:  |  Size: 12 KiB

After

Width:  |  Height:  |  Size: 12 KiB

View File

Before

Width:  |  Height:  |  Size: 3.5 KiB

After

Width:  |  Height:  |  Size: 3.5 KiB

View File

Before

Width:  |  Height:  |  Size: 6.4 KiB

After

Width:  |  Height:  |  Size: 6.4 KiB

View File

Before

Width:  |  Height:  |  Size: 83 KiB

After

Width:  |  Height:  |  Size: 83 KiB

View File

Before

Width:  |  Height:  |  Size: 2.7 KiB

After

Width:  |  Height:  |  Size: 2.7 KiB

View File

Before

Width:  |  Height:  |  Size: 6.4 KiB

After

Width:  |  Height:  |  Size: 6.4 KiB

View File

Before

Width:  |  Height:  |  Size: 3.6 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

View File

@@ -1,82 +1,118 @@
SB = {
interface SBObject {
configListeners: Array<Function>;
defaults: any;
localConfig: any;
config: any;
}
// Allows a SBMap to be conveted into json form
// Currently used for local storage
class SBMap<T, U> extends Map {
toJSON() {
return Array.from(this.entries());
}
}
// TODO: Rename to something more meaningful
var SB: SBObject = {
/** /**
* Callback function when an option is updated * Callback function when an option is updated
*
* @type {CallableFunction}
*/ */
configListeners: [] configListeners: [],
defaults: {
"sponsorTimes": new SBMap(),
"startSponsorKeybind": ";",
"submitKeybind": "'",
"minutesSaved": 0,
"skipCount": 0,
"sponsorTimesContributed": 0,
"disableSkipping": false,
"disableAutoSkip": false,
"trackViewCount": true,
"dontShowNotice": false,
"hideVideoPlayerControls": false,
"hideInfoButtonPlayerControls": false,
"hideDeleteButtonPlayerControls": false,
"hideDiscordLaunches": 0,
"hideDiscordLink": false,
"invidiousInstances": ["invidio.us", "invidiou.sh", "invidious.snopyta.org"],
"invidiousUpdateInfoShowCount": 0,
"autoUpvote": true
},
localConfig: {},
config: {}
}; };
// Function setup // Function setup
// Allows a map to be conveted into json form
// Currently used for local storage
Map.prototype.toJSON = function() {
return Array.from(this.entries());
};
// Proxy Map changes to Map in SB.localConfig // Proxy Map changes to Map in SB.localConfig
// Saves the changes to chrome.storage in json form // Saves the changes to chrome.storage in json form
class MapIO { class MapIO {
id: string;
SBMap: SBMap<String, any>;
constructor(id) { constructor(id) {
// The name of the item in the array // The name of the item in the array
this.id = id; this.id = id;
// A local copy of the map (SB.config.mapname.map) // A local copy of the SBMap (SB.config.SBMapname.SBMap)
this.map = SB.localConfig[this.id]; this.SBMap = SB.localConfig[this.id];
} }
set(key, value) { set(key, value) {
// Proxy to map // Proxy to SBMap
this.map.set(key, value); this.SBMap.set(key, value);
// Store updated map locally // Store updated SBMap locally
chrome.storage.sync.set({ chrome.storage.sync.set({
[this.id]: encodeStoredItem(this.map) [this.id]: encodeStoredItem(this.SBMap)
}); });
return this.map; return this.SBMap;
} }
get(key) { get(key) {
return this.map.get(key); return this.SBMap.get(key);
} }
has(key) { has(key) {
return this.map.has(key); return this.SBMap.has(key);
} }
size() { size() {
return this.map.size; return this.SBMap.size;
} }
delete(key) { delete(key) {
// Proxy to map // Proxy to SBMap
this.map.delete(key); this.SBMap.delete(key);
// Store updated map locally // Store updated SBMap locally
chrome.storage.sync.set({ chrome.storage.sync.set({
[this.id]: encodeStoredItem(this.map) [this.id]: encodeStoredItem(this.SBMap)
}); });
} }
clear() { clear() {
this.map.clear(); this.SBMap.clear();
chrome.storage.sync.set({ chrome.storage.sync.set({
[this.id]: encodeStoredItem(this.map) [this.id]: encodeStoredItem(this.SBMap)
}); });
} }
} }
/** /**
* A Map cannot be stored in the chrome storage. * A SBMap cannot be stored in the chrome storage.
* This data will be encoded into an array instead as specified by the toJSON function. * This data will be encoded into an array instead as specified by the toJSON function.
* *
* @param {*} data * @param {*} data
*/ */
function encodeStoredItem(data) { function encodeStoredItem(data) {
// if data is Map convert to json for storing // if data is SBMap convert to json for storing
if(!(data instanceof Map)) return data; if(!(data instanceof SBMap)) return data;
return JSON.stringify(data); return JSON.stringify(data);
} }
/** /**
* A Map cannot be stored in the chrome storage. * A SBMap cannot be stored in the chrome storage.
* This data will be decoded from the array it is stored in * This data will be decoded from the array it is stored in
* *
* @param {*} data * @param {*} data
@@ -88,7 +124,7 @@ function decodeStoredItem(data) {
let str = JSON.parse(data); let str = JSON.parse(data);
if(!Array.isArray(str)) return data; if(!Array.isArray(str)) return data;
return new Map(str); return new SBMap(str);
} catch(e) { } catch(e) {
// If all else fails, return the data // If all else fails, return the data
@@ -96,7 +132,7 @@ function decodeStoredItem(data) {
} }
} }
function configProxy() { function configProxy(): void {
chrome.storage.onChanged.addListener((changes, namespace) => { chrome.storage.onChanged.addListener((changes, namespace) => {
for (const key in changes) { for (const key in changes) {
SB.localConfig[key] = decodeStoredItem(changes[key].newValue); SB.localConfig[key] = decodeStoredItem(changes[key].newValue);
@@ -107,24 +143,28 @@ function configProxy() {
} }
}); });
var handler = { var handler: ProxyHandler<any> = {
set(obj, prop, value) { set(obj, prop, value) {
SB.localConfig[prop] = value; SB.localConfig[prop] = value;
chrome.storage.sync.set({ chrome.storage.sync.set({
[prop]: encodeStoredItem(value) [prop]: encodeStoredItem(value)
}); });
return true;
}, },
get(obj, prop) { get(obj, prop): any {
let data = SB.localConfig[prop]; let data = SB.localConfig[prop];
if(data instanceof Map) data = new MapIO(prop); if(data instanceof SBMap) data = new MapIO(prop);
return obj[prop] || data; return obj[prop] || data;
}, },
deleteProperty(obj, prop) { deleteProperty(obj, prop) {
chrome.storage.sync.remove(prop); chrome.storage.sync.remove(<string> prop);
return true;
} }
}; };
@@ -158,27 +198,6 @@ async function setupConfig() {
migrateOldFormats(); migrateOldFormats();
} }
SB.defaults = {
"sponsorTimes": new Map(),
"startSponsorKeybind": ";",
"submitKeybind": "'",
"minutesSaved": 0,
"skipCount": 0,
"sponsorTimesContributed": 0,
"disableSkipping": false,
"disableAutoSkip": false,
"trackViewCount": true,
"dontShowNotice": false,
"hideVideoPlayerControls": false,
"hideInfoButtonPlayerControls": false,
"hideDeleteButtonPlayerControls": false,
"hideDiscordLaunches": 0,
"hideDiscordLink": false,
"invidiousInstances": ["invidio.us", "invidiou.sh", "invidious.snopyta.org"],
"invidiousUpdateInfoShowCount": 0,
"autoUpvote": true
}
// Reset config // Reset config
function resetConfig() { function resetConfig() {
SB.config = SB.defaults; SB.config = SB.defaults;
@@ -201,3 +220,5 @@ function addDefaults() {
// Sync config // Sync config
setupConfig(); setupConfig();
export default SB;

3
src/config.js.example Normal file
View File

@@ -0,0 +1,3 @@
//this file is loaded along iwth content.js
//this file sets the server to connect to, and is gitignored
var serverAddress = "https://sponsor.ajay.app";

View File

@@ -1,3 +1,9 @@
import Utils from "./utils";
import SB from "./SB";
import PreviewBar from "./js-components/previewBar";
import SkipNotice from "./js-components/previewBar";
//was sponsor data found when doing SponsorsLookup //was sponsor data found when doing SponsorsLookup
var sponsorDataFound = false; var sponsorDataFound = false;
var previousVideoID = null; var previousVideoID = null;
@@ -40,7 +46,7 @@ var previewBar = null;
var controls = null; var controls = null;
// Direct Links // Direct Links
videoIDChange(getYouTubeVideoID(document.URL)); videoIDChange(Utils.getYouTubeVideoID(document.URL));
//the last time looked at (used to see if this time is in the interval) //the last time looked at (used to see if this time is in the interval)
var lastTime = -1; var lastTime = -1;
@@ -72,7 +78,7 @@ function messageListener(request, sender, sendResponse) {
//messages from popup script //messages from popup script
switch(request.message){ switch(request.message){
case "update": case "update":
videoIDChange(getYouTubeVideoID(document.URL)); videoIDChange(Utils.getYouTubeVideoID(document.URL));
break; break;
case "sponsorStart": case "sponsorStart":
sponsorMessageStarted(sendResponse); sponsorMessageStarted(sendResponse);
@@ -166,7 +172,6 @@ if (!SB.configListeners.includes(contentConfigUpdateListener)) {
//check for hotkey pressed //check for hotkey pressed
document.onkeydown = async function(e){ document.onkeydown = async function(e){
e = e || window.event;
var key = e.key; var key = e.key;
let video = document.getElementById("movie_player"); let video = document.getElementById("movie_player");
@@ -217,13 +222,13 @@ function videoIDChange(id) {
//id is not valid //id is not valid
if (!id) return; if (!id) return;
let channelIDPromise = wait(getChannelID); let channelIDPromise = Utils.wait(getChannelID);
channelIDPromise.then(() => channelIDPromise.isFulfilled = true).catch(() => channelIDPromise.isRejected = true); channelIDPromise.then(() => channelIDPromise.isFulfilled = true).catch(() => channelIDPromise.isRejected = true);
//setup the preview bar //setup the preview bar
if (previewBar == null) { if (previewBar == null) {
//create it //create it
wait(getControls).then(result => { Utils.wait(getControls).then(result => {
const progressElementSelectors = [ const progressElementSelectors = [
// For YouTube // For YouTube
"ytp-progress-bar-container", "ytp-progress-bar-container",
@@ -274,7 +279,7 @@ function videoIDChange(id) {
sponsorTimesSubmitting = []; sponsorTimesSubmitting = [];
//see if the onvideo control image needs to be changed //see if the onvideo control image needs to be changed
wait(getControls).then(result => { Utils.wait(getControls).then(result => {
chrome.runtime.sendMessage({ chrome.runtime.sendMessage({
message: "getSponsorTimes", message: "getSponsorTimes",
videoID: id videoID: id
@@ -299,12 +304,12 @@ function videoIDChange(id) {
}); });
}); });
//see if video controls buttons should be added //see if video controls buttons should be added
if (!onInvidious) { if (!Utils.onInvidious) {
updateVisibilityOfPlayerControlsButton(); updateVisibilityOfPlayerControlsButton();
} }
} }
function sponsorsLookup(id, channelIDPromise) { function sponsorsLookup(id: string, channelIDPromise = null) {
v = document.querySelector('video') // Youtube video player v = document.querySelector('video') // Youtube video player
//there is no video here //there is no video here
if (v == null) { if (v == null) {
@@ -324,7 +329,7 @@ function sponsorsLookup(id, channelIDPromise) {
whitelistCheck(); whitelistCheck();
} else if (channelIDPromise.isRejected) { } else if (channelIDPromise.isRejected) {
//try again //try again
wait(getChannelID).then(whitelistCheck).catch(); Utils.wait(getChannelID).then(whitelistCheck).catch();
} else { } else {
//add it as a then statement //add it as a then statement
channelIDPromise.then(whitelistCheck); channelIDPromise.then(whitelistCheck);
@@ -410,7 +415,7 @@ function updatePreviewBar() {
types.push("previewSponsor"); types.push("previewSponsor");
} }
wait(() => previewBar !== null).then((result) => previewBar.set(allSponsorTimes, types, v.duration)); Utils.wait(() => previewBar !== null).then((result) => previewBar.set(allSponsorTimes, types, v.duration));
//update last video id //update last video id
lastPreviewBarUpdate = sponsorVideoID; lastPreviewBarUpdate = sponsorVideoID;
@@ -423,7 +428,7 @@ function getChannelID() {
channelURLContainer = document.querySelector("#channel-name > #container > #text-container > #text"); channelURLContainer = document.querySelector("#channel-name > #container > #text-container > #text");
if (channelURLContainer !== null) { if (channelURLContainer !== null) {
channelURLContainer = channelURLContainer.firstElementChild; channelURLContainer = channelURLContainer.firstElementChild;
} else if (onInvidious) { } else if (Utils.onInvidious) {
// Unfortunately, the Invidious HTML doesn't have much in the way of element identifiers... // Unfortunately, the Invidious HTML doesn't have much in the way of element identifiers...
channelURLContainer = document.querySelector("body > div > div.pure-u-1.pure-u-md-20-24 div.pure-u-1.pure-u-lg-3-5 > div > a"); channelURLContainer = document.querySelector("body > div > div.pure-u-1.pure-u-md-20-24 div.pure-u-1.pure-u-lg-3-5 > div > a");
} else { } else {
@@ -443,8 +448,8 @@ function getChannelID() {
let titleInfoContainer = document.getElementById("info-contents"); let titleInfoContainer = document.getElementById("info-contents");
let currentTitle = ""; let currentTitle = "";
if (titleInfoContainer != null) { if (titleInfoContainer != null) {
currentTitle = titleInfoContainer.firstElementChild.firstElementChild.querySelector(".title").firstElementChild.innerText; currentTitle = (<HTMLElement> titleInfoContainer.firstElementChild.firstElementChild.querySelector(".title").firstElementChild).innerText;
} else if (onInvidious) { } else if (Utils.onInvidious) {
// Unfortunately, the Invidious HTML doesn't have much in the way of element identifiers... // Unfortunately, the Invidious HTML doesn't have much in the way of element identifiers...
currentTitle = document.querySelector("body > div > div.pure-u-1.pure-u-md-20-24 div.pure-u-1.pure-u-lg-3-5 > div > a > div > span").textContent; currentTitle = document.querySelector("body > div > div.pure-u-1.pure-u-md-20-24 div.pure-u-1.pure-u-lg-3-5 > div > a > div > span").textContent;
} else { } else {
@@ -637,7 +642,7 @@ function getControls() {
//adds all the player controls buttons //adds all the player controls buttons
async function createButtons() { async function createButtons() {
let result = await wait(getControls).catch(); let result = await Utils.wait(getControls).catch();
//set global controls variable //set global controls variable
controls = result; controls = result;
@@ -655,7 +660,7 @@ async function updateVisibilityOfPlayerControlsButton() {
await createButtons(); await createButtons();
if (SB.config.hideVideoPlayerControls || onInvidious) { if (SB.config.hideVideoPlayerControls || Utils.onInvidious) {
document.getElementById("startSponsorButton").style.display = "none"; document.getElementById("startSponsorButton").style.display = "none";
document.getElementById("submitButton").style.display = "none"; document.getElementById("submitButton").style.display = "none";
} else { } else {
@@ -663,13 +668,13 @@ async function updateVisibilityOfPlayerControlsButton() {
} }
//don't show the info button on embeds //don't show the info button on embeds
if (SB.config.hideInfoButtonPlayerControls || document.URL.includes("/embed/") || onInvidious) { if (SB.config.hideInfoButtonPlayerControls || document.URL.includes("/embed/") || Utils.onInvidious) {
document.getElementById("infoButton").style.display = "none"; document.getElementById("infoButton").style.display = "none";
} else { } else {
document.getElementById("infoButton").style.removeProperty("display"); document.getElementById("infoButton").style.removeProperty("display");
} }
if (SB.config.hideDeleteButtonPlayerControls || onInvidious) { if (SB.config.hideDeleteButtonPlayerControls || Utils.onInvidious) {
document.getElementById("deleteButton").style.display = "none"; document.getElementById("deleteButton").style.display = "none";
} }
} }
@@ -718,15 +723,15 @@ async function changeStartSponsorButton(showStartSponsor, uploadButtonVisible) {
if(!sponsorVideoID) return false; if(!sponsorVideoID) return false;
//make sure submit button is loaded //make sure submit button is loaded
await wait(isSubmitButtonLoaded); await Utils.wait(isSubmitButtonLoaded);
//if it isn't visible, there is no data //if it isn't visible, there is no data
let shouldHide = (uploadButtonVisible && !(SB.config.hideDeleteButtonPlayerControls || onInvidious)) ? "unset" : "none" let shouldHide = (uploadButtonVisible && !(SB.config.hideDeleteButtonPlayerControls || Utils.onInvidious)) ? "unset" : "none"
document.getElementById("deleteButton").style.display = shouldHide; document.getElementById("deleteButton").style.display = shouldHide;
if (showStartSponsor) { if (showStartSponsor) {
showingStartSponsor = true; showingStartSponsor = true;
document.getElementById("startSponsorImage").src = chrome.extension.getURL("icons/PlayerStartIconSponsorBlocker256px.png"); (<HTMLImageElement> document.getElementById("startSponsorImage")).src = chrome.extension.getURL("icons/PlayerStartIconSponsorBlocker256px.png");
document.getElementById("startSponsorButton").setAttribute("title", chrome.i18n.getMessage("sponsorStart")); document.getElementById("startSponsorButton").setAttribute("title", chrome.i18n.getMessage("sponsorStart"));
if (document.getElementById("startSponsorImage").style.display != "none" && uploadButtonVisible && !SB.config.hideInfoButtonPlayerControls) { if (document.getElementById("startSponsorImage").style.display != "none" && uploadButtonVisible && !SB.config.hideInfoButtonPlayerControls) {
@@ -737,7 +742,7 @@ async function changeStartSponsorButton(showStartSponsor, uploadButtonVisible) {
} }
} else { } else {
showingStartSponsor = false; showingStartSponsor = false;
document.getElementById("startSponsorImage").src = chrome.extension.getURL("icons/PlayerStopIconSponsorBlocker256px.png"); (<HTMLImageElement> document.getElementById("startSponsorImage")).src = chrome.extension.getURL("icons/PlayerStopIconSponsorBlocker256px.png");
document.getElementById("startSponsorButton").setAttribute("title", chrome.i18n.getMessage("sponsorEND")); document.getElementById("startSponsorButton").setAttribute("title", chrome.i18n.getMessage("sponsorEND"));
//disable submit button //disable submit button
@@ -769,7 +774,7 @@ function openInfoMenu() {
//close button //close button
let closeButton = document.createElement("div"); let closeButton = document.createElement("div");
closeButton.innerText = "Close Popup"; closeButton.innerText = "Close Popup";
closeButton.classList = "smallLink"; closeButton.classList.add("smallLink");
closeButton.setAttribute("align", "center"); closeButton.setAttribute("align", "center");
closeButton.addEventListener("click", closeInfoMenu); closeButton.addEventListener("click", closeInfoMenu);
@@ -791,7 +796,7 @@ function openInfoMenu() {
//make the logo source not 404 //make the logo source not 404
//query selector must be used since getElementByID doesn't work on a node and this isn't added to the document yet //query selector must be used since getElementByID doesn't work on a node and this isn't added to the document yet
let logo = popup.querySelector("#sponsorBlockPopupLogo"); let logo = <HTMLImageElement> popup.querySelector("#sponsorBlockPopupLogo");
logo.src = chrome.extension.getURL("icons/LogoSponsorBlocker256px.png"); logo.src = chrome.extension.getURL("icons/LogoSponsorBlocker256px.png");
//remove the style sheet and font that are not necessary //remove the style sheet and font that are not necessary
@@ -889,7 +894,7 @@ function vote(type, UUID, skipNotice) {
skipNotice.addNoticeInfoMessage.bind(skipNotice)(chrome.i18n.getMessage("voteFail")) skipNotice.addNoticeInfoMessage.bind(skipNotice)(chrome.i18n.getMessage("voteFail"))
skipNotice.resetVoteButtonInfo.bind(skipNotice)(); skipNotice.resetVoteButtonInfo.bind(skipNotice)();
} else if (response.successType == -1) { } else if (response.successType == -1) {
skipNotice.addNoticeInfoMessage.bind(skipNotice)(getErrorMessage(response.statusCode)) skipNotice.addNoticeInfoMessage.bind(skipNotice)(Utils.getErrorMessage(response.statusCode))
skipNotice.resetVoteButtonInfo.bind(skipNotice)(); skipNotice.resetVoteButtonInfo.bind(skipNotice)();
} }
} }
@@ -961,7 +966,7 @@ function submitSponsorTimes() {
//called after all the checks have been made that it's okay to do so //called after all the checks have been made that it's okay to do so
function sendSubmitMessage(){ function sendSubmitMessage(){
//add loading animation //add loading animation
document.getElementById("submitImage").src = chrome.extension.getURL("icons/PlayerUploadIconSponsorBlocker256px.png"); (<HTMLImageElement> document.getElementById("submitImage")).src = chrome.extension.getURL("icons/PlayerUploadIconSponsorBlocker256px.png");
document.getElementById("submitButton").style.animation = "rotate 1s 0s infinite"; document.getElementById("submitButton").style.animation = "rotate 1s 0s infinite";
let currentVideoID = sponsorVideoID; let currentVideoID = sponsorVideoID;
@@ -994,7 +999,7 @@ function sendSubmitMessage(){
sponsorTimes = sponsorTimes.concat(sponsorTimesSubmitting); sponsorTimes = sponsorTimes.concat(sponsorTimesSubmitting);
for (let i = 0; i < sponsorTimesSubmitting.length; i++) { for (let i = 0; i < sponsorTimesSubmitting.length; i++) {
// Add some random IDs // Add some random IDs
UUIDs.push(generateUserID()); UUIDs.push(Utils.generateUserID());
} }
// Empty the submitting times // Empty the submitting times
@@ -1004,9 +1009,9 @@ function sendSubmitMessage(){
} else { } else {
//show that the upload failed //show that the upload failed
document.getElementById("submitButton").style.animation = "unset"; document.getElementById("submitButton").style.animation = "unset";
document.getElementById("submitImage").src = chrome.extension.getURL("icons/PlayerUploadFailedIconSponsorBlocker256px.png"); (<HTMLImageElement> document.getElementById("submitImage")).src = chrome.extension.getURL("icons/PlayerUploadFailedIconSponsorBlocker256px.png");
alert(getErrorMessage(response.statusCode)); alert(Utils.getErrorMessage(response.statusCode));
} }
} }
}); });
@@ -1037,23 +1042,25 @@ function getSponsorTimesMessage(sponsorTimes) {
//converts time in seconds to minutes:seconds //converts time in seconds to minutes:seconds
function getFormattedTime(seconds) { function getFormattedTime(seconds) {
let minutes = Math.floor(seconds / 60); let minutes = Math.floor(seconds / 60);
let secondsDisplay = Math.round(seconds - minutes * 60); let secondsNum: number = Math.round(seconds - minutes * 60);
if (secondsDisplay < 10) { let secondsDisplay: string = String(secondsNum);
if (secondsNum < 10) {
//add a zero //add a zero
secondsDisplay = "0" + secondsDisplay; secondsDisplay = "0" + secondsNum;
} }
let formatted = minutes+ ":" + secondsDisplay; let formatted = minutes + ":" + secondsDisplay;
return formatted; return formatted;
} }
function sendRequestToServer(type, address, callback) { function sendRequestToServer(type: string, address: string, callback = null) {
let xmlhttp = new XMLHttpRequest(); let xmlhttp = new XMLHttpRequest();
xmlhttp.open(type, serverAddress + address, true); xmlhttp.open(type, serverAddress + address, true);
if (callback != undefined) { if (callback !== null) {
xmlhttp.onreadystatechange = function () { xmlhttp.onreadystatechange = function () {
callback(xmlhttp, false); callback(xmlhttp, false);
}; };

View File

@@ -21,11 +21,13 @@ let barTypes = {
}; };
class PreviewBar { class PreviewBar {
container: HTMLUListElement;
parent: any;
constructor(parent) { constructor(parent) {
this.container = document.createElement('ul'); this.container = document.createElement('ul');
this.container.id = 'previewbar'; this.container.id = 'previewbar';
this.parent = parent; this.parent = parent;
this.bars = []
this.updatePosition(); this.updatePosition();
} }
@@ -39,7 +41,7 @@ class PreviewBar {
} }
updateColor(segment, color, opacity) { updateColor(segment, color, opacity) {
let bars = document.querySelectorAll('[data-vs-segment-type=' + segment + ']'); let bars = <NodeListOf<HTMLElement>> document.querySelectorAll('[data-vs-segment-type=' + segment + ']');
for (let bar of bars) { for (let bar of bars) {
bar.style.backgroundColor = color; bar.style.backgroundColor = color;
bar.style.opacity = opacity; bar.style.opacity = opacity;
@@ -73,8 +75,7 @@ class PreviewBar {
bar.style.left = (timestamps[i][0] / duration * 100) + "%"; bar.style.left = (timestamps[i][0] / duration * 100) + "%";
bar.style.position = "absolute" bar.style.position = "absolute"
this.container.insertAdjacentElement('beforeEnd', bar); this.container.insertAdjacentElement("beforeEnd", bar);
this.bars[i] = bar;
} }
} }
@@ -90,3 +91,5 @@ class PreviewBar {
this.container = undefined; this.container = undefined;
} }
} }
export default PreviewBar;

View File

@@ -4,13 +4,16 @@
* The notice that tells the user that a sponsor was just skipped * The notice that tells the user that a sponsor was just skipped
*/ */
class SkipNotice { class SkipNotice {
/** parent: HTMLElement;
* @param {HTMLElement} parent UUID: string;
* @param {String} UUID manualSkip: boolean;
* @param {String} noticeTitle maxCountdownTime: () => number;
* @param {boolean} manualSkip countdownTime: any;
*/ countdownInterval: number;
constructor(parent, UUID, manualSkip = false) { unskipCallback: any;
idSuffix: any;
constructor(parent: HTMLElement, UUID: string, manualSkip: boolean = false) {
this.parent = parent; this.parent = parent;
this.UUID = UUID; this.UUID = UUID;
this.manualSkip = manualSkip; this.manualSkip = manualSkip;
@@ -48,7 +51,7 @@ class SkipNotice {
noticeElement.id = "sponsorSkipNotice" + this.idSuffix; noticeElement.id = "sponsorSkipNotice" + this.idSuffix;
noticeElement.classList.add("sponsorSkipObject"); noticeElement.classList.add("sponsorSkipObject");
noticeElement.classList.add("sponsorSkipNotice"); noticeElement.classList.add("sponsorSkipNotice");
noticeElement.style.zIndex = 50 + amountOfPreviousNotices; noticeElement.style.zIndex = String(50 + amountOfPreviousNotices);
//add mouse enter and leave listeners //add mouse enter and leave listeners
noticeElement.addEventListener("mouseenter", this.pauseCountdown.bind(this)); noticeElement.addEventListener("mouseenter", this.pauseCountdown.bind(this));
@@ -170,12 +173,12 @@ class SkipNotice {
if (referenceNode == null) { if (referenceNode == null) {
//for embeds //for embeds
let player = document.getElementById("player"); let player = document.getElementById("player");
referenceNode = player.firstChild; referenceNode = <HTMLElement> player.firstChild;
let index = 1; let index = 1;
//find the child that is the video player (sometimes it is not the first) //find the child that is the video player (sometimes it is not the first)
while (!referenceNode.classList.contains("html5-video-player") || !referenceNode.classList.contains("ytp-embed")) { while (!referenceNode.classList.contains("html5-video-player") || !referenceNode.classList.contains("ytp-embed")) {
referenceNode = player.children[index]; referenceNode = <HTMLElement> player.children[index];
index++; index++;
} }
@@ -336,7 +339,7 @@ class SkipNotice {
noticeElement.innerText = title; noticeElement.innerText = title;
} }
addNoticeInfoMessage(message, message2) { addNoticeInfoMessage(message: string, message2: string = "") {
let previousInfoMessage = document.getElementById("sponsorTimesInfoMessage" + this.idSuffix); let previousInfoMessage = document.getElementById("sponsorTimesInfoMessage" + this.idSuffix);
if (previousInfoMessage != null) { if (previousInfoMessage != null) {
//remove it //remove it
@@ -426,3 +429,5 @@ class SkipNotice {
} }
} }
export default SkipNotice;

279
src/utils.ts Normal file
View File

@@ -0,0 +1,279 @@
import SB from "./SB";
class Utils {
static isBackgroundScript = false;
static onInvidious = false;
// Function that can be used to wait for a condition before returning
static async wait(condition, timeout = 5000, check = 100) {
return await new Promise((resolve, reject) => {
setTimeout(() => reject("TIMEOUT"), timeout);
let intervalCheck = () => {
let result = condition();
if (result !== false) {
resolve(result);
clearInterval(interval);
};
};
let interval = setInterval(intervalCheck, check);
//run the check once first, this speeds it up a lot
intervalCheck();
});
}
static getYouTubeVideoID(url: string) {
// For YouTube TV support
if(url.startsWith("https://www.youtube.com/tv#/")) url = url.replace("#", "");
//Attempt to parse url
let urlObject = null;
try {
urlObject = new URL(url);
} catch (e) {
console.error("[SB] Unable to parse URL: " + url);
return false;
}
//Check if valid hostname
if (SB.config && SB.config.invidiousInstances.includes(urlObject.host)) {
onInvidious = true;
} else if (!["www.youtube.com", "www.youtube-nocookie.com"].includes(urlObject.host)) {
if (!SB.config) {
// Call this later, in case this is an Invidious tab
this.wait(() => SB.config !== undefined).then(() => this.videoIDChange(this.getYouTubeVideoID(url)));
}
return false
}
//Get ID from searchParam
if (urlObject.searchParams.has("v") && ["/watch", "/watch/"].includes(urlObject.pathname) || urlObject.pathname.startsWith("/tv/watch")) {
let id = urlObject.searchParams.get("v");
return id.length == 11 ? id : false;
} else if (urlObject.pathname.startsWith("/embed/")) {
try {
return urlObject.pathname.substr(7, 11);
} catch (e) {
console.error("[SB] Video ID not valid for " + url);
return false;
}
}
return false;
}
/**
* Asks for the optional permissions required for all extra sites.
* It also starts the content script registrations.
*
* For now, it is just SB.config.invidiousInstances.
*
* @param {CallableFunction} callback
*/
static setupExtraSitePermissions(callback) {
// Request permission
let permissions = ["declarativeContent"];
if (this.isFirefox()) permissions = [];
chrome.permissions.request({
origins: this.getInvidiousInstancesRegex(),
permissions: permissions
}, async function (granted) {
if (granted) {
this.setupExtraSiteContentScripts();
} else {
this.removeExtraSiteRegistration();
}
callback(granted);
});
}
/**
* Registers the content scripts for the extra sites.
* Will use a different method depending on the browser.
* This is called by setupExtraSitePermissions().
*
* For now, it is just SB.config.invidiousInstances.
*/
static setupExtraSiteContentScripts() {
let js = [
"config.js",
"SB.js",
"utils/previewBar.js",
"utils/skipNotice.js",
"utils.js",
"content.js",
"popup.js"
];
let css = [
"content.css",
"./libs/Source+Sans+Pro.css",
"popup.css"
];
if (this.isFirefox()) {
let firefoxJS = [];
for (const file of js) {
firefoxJS.push({file});
}
let firefoxCSS = [];
for (const file of css) {
firefoxCSS.push({file});
}
let registration = {
message: "registerContentScript",
id: "invidious",
allFrames: true,
js: firefoxJS,
css: firefoxCSS,
matches: this.getInvidiousInstancesRegex()
};
if (isBackgroundScript) {
registerFirefoxContentScript(registration);
} else {
chrome.runtime.sendMessage(registration);
}
} else {
chrome.declarativeContent.onPageChanged.removeRules(["invidious"], function() {
let conditions = [];
for (const regex of this.getInvidiousInstancesRegex()) {
conditions.push(new chrome.declarativeContent.PageStateMatcher({
pageUrl: { urlMatches: regex }
}));
}
// Add page rule
let rule = {
id: "invidious",
conditions,
// This API is experimental and not visible by the TypeScript compiler
actions: [new (<any> chrome.declarativeContent).RequestContentScript({
allFrames: true,
js,
css
})]
};
chrome.declarativeContent.onPageChanged.addRules([rule]);
});
}
}
/**
* Removes the permission and content script registration.
*/
static removeExtraSiteRegistration() {
if (this.isFirefox()) {
let id = "invidious";
if (isBackgroundScript) {
if (contentScriptRegistrations[id]) {
contentScriptRegistrations[id].unregister();
delete contentScriptRegistrations[id];
}
} else {
chrome.runtime.sendMessage({
message: "unregisterContentScript",
id: id
});
}
} else {
chrome.declarativeContent.onPageChanged.removeRules(["invidious"]);
}
chrome.permissions.remove({
origins: this.getInvidiousInstancesRegex()
});
}
static localizeHtmlPage() {
//Localize by replacing __MSG_***__ meta tags
var objects = document.getElementsByClassName("sponsorBlockPageBody")[0].children;
for (var j = 0; j < objects.length; j++) {
var obj = objects[j];
let localizedMessage = this.getLocalizedMessage(obj.innerHTML.toString());
if (localizedMessage) obj.innerHTML = localizedMessage;
}
}
static getLocalizedMessage(text) {
var valNewH = text.replace(/__MSG_(\w+)__/g, function(match, v1) {
return v1 ? chrome.i18n.getMessage(v1) : "";
});
if(valNewH != text) {
return valNewH;
} else {
return false;
}
}
/**
* @returns {String[]} Invidious Instances in regex form
*/
static getInvidiousInstancesRegex() {
var invidiousInstancesRegex = [];
for (const url of SB.config.invidiousInstances) {
invidiousInstancesRegex.push("https://*." + url + "/*");
invidiousInstancesRegex.push("http://*." + url + "/*");
}
return invidiousInstancesRegex;
}
static generateUserID(length = 36) {
let charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
let result = "";
if (window.crypto && window.crypto.getRandomValues) {
let 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;
}
}
/**
* Gets the error message in a nice string
*
* @param {int} statusCode
* @returns {string} errorMessage
*/
static getErrorMessage(statusCode) {
let errorMessage = "";
if([400, 429, 409, 502, 0].includes(statusCode)) {
//treat them the same
if (statusCode == 503) statusCode = 502;
errorMessage = chrome.i18n.getMessage(statusCode + "") + " " + chrome.i18n.getMessage("errorCode") + statusCode
+ "\n\n" + chrome.i18n.getMessage("statusReminder");
} else {
errorMessage = chrome.i18n.getMessage("connectionError") + statusCode;
}
return errorMessage;
}
/**
* Is this Firefox (web-extensions)
*/
static isFirefox() {
return typeof(browser) !== "undefined";
}
}
export default Utils;

12
tsconfig.json Normal file
View File

@@ -0,0 +1,12 @@
{
"compilerOptions": {
"module": "commonjs",
"target": "es6",
"noImplicitAny": false,
"sourceMap": false,
"rootDir": "src",
"outDir": "dist/js",
"noEmitOnError": true,
"typeRoots": [ "node_modules/@types" ]
}
}

271
utils.js
View File

@@ -1,271 +0,0 @@
var isBackgroundScript = false;
var onInvidious = false;
// Function that can be used to wait for a condition before returning
async function wait(condition, timeout = 5000, check = 100) {
return await new Promise((resolve, reject) => {
setTimeout(() => reject("TIMEOUT"), timeout);
let intervalCheck = () => {
let result = condition();
if (result !== false) {
resolve(result);
clearInterval(interval);
};
};
let interval = setInterval(intervalCheck, check);
//run the check once first, this speeds it up a lot
intervalCheck();
});
}
function getYouTubeVideoID(url) {
// For YouTube TV support
if(url.startsWith("https://www.youtube.com/tv#/")) url = url.replace("#", "");
//Attempt to parse url
let urlObject = null;
try {
urlObject = new URL(url);
} catch (e) {
console.error("[SB] Unable to parse URL: " + url);
return false;
}
//Check if valid hostname
if (SB.config && SB.config.invidiousInstances.includes(urlObject.host)) {
onInvidious = true;
} else if (!["www.youtube.com", "www.youtube-nocookie.com"].includes(urlObject.host)) {
if (!SB.config) {
// Call this later, in case this is an Invidious tab
wait(() => SB.config !== undefined).then(() => videoIDChange(getYouTubeVideoID(url)));
}
return false
}
//Get ID from searchParam
if (urlObject.searchParams.has("v") && ["/watch", "/watch/"].includes(urlObject.pathname) || urlObject.pathname.startsWith("/tv/watch")) {
id = urlObject.searchParams.get("v");
return id.length == 11 ? id : false;
} else if (urlObject.pathname.startsWith("/embed/")) {
try {
return urlObject.pathname.substr(7, 11);
} catch (e) {
console.error("[SB] Video ID not valid for " + url);
return false;
}
}
return false;
}
/**
* Asks for the optional permissions required for all extra sites.
* It also starts the content script registrations.
*
* For now, it is just SB.config.invidiousInstances.
*
* @param {CallableFunction} callback
*/
function setupExtraSitePermissions(callback) {
// Request permission
let permissions = ["declarativeContent"];
if (isFirefox()) permissions = [];
chrome.permissions.request({
origins: getInvidiousInstancesRegex(),
permissions: permissions
}, async function (granted) {
if (granted) {
setupExtraSiteContentScripts();
} else {
removeExtraSiteRegistration();
}
callback(granted);
});
}
/**
* Registers the content scripts for the extra sites.
* Will use a different method depending on the browser.
* This is called by setupExtraSitePermissions().
*
* For now, it is just SB.config.invidiousInstances.
*/
function setupExtraSiteContentScripts() {
let js = [
"config.js",
"SB.js",
"utils/previewBar.js",
"utils/skipNotice.js",
"utils.js",
"content.js",
"popup.js"
];
let css = [
"content.css",
"./libs/Source+Sans+Pro.css",
"popup.css"
];
if (isFirefox()) {
let firefoxJS = [];
for (const file of js) {
firefoxJS.push({file});
}
let firefoxCSS = [];
for (const file of css) {
firefoxCSS.push({file});
}
let registration = {
message: "registerContentScript",
id: "invidious",
allFrames: true,
js: firefoxJS,
css: firefoxCSS,
matches: getInvidiousInstancesRegex()
};
if (isBackgroundScript) {
registerFirefoxContentScript(registration);
} else {
chrome.runtime.sendMessage(registration);
}
} else {
chrome.declarativeContent.onPageChanged.removeRules(["invidious"], function() {
let conditions = [];
for (const regex of getInvidiousInstancesRegex()) {
conditions.push(new chrome.declarativeContent.PageStateMatcher({
pageUrl: { urlMatches: regex }
}));
}
// Add page rule
let rule = {
id: "invidious",
conditions,
actions: [new chrome.declarativeContent.RequestContentScript({
allFrames: true,
js,
css
})]
};
chrome.declarativeContent.onPageChanged.addRules([rule]);
});
}
}
/**
* Removes the permission and content script registration.
*/
function removeExtraSiteRegistration() {
if (isFirefox()) {
let id = "invidious";
if (isBackgroundScript) {
if (contentScriptRegistrations[id]) {
contentScriptRegistrations[id].unregister();
delete contentScriptRegistrations[id];
}
} else {
chrome.runtime.sendMessage({
message: "unregisterContentScript",
id: id
});
}
} else {
chrome.declarativeContent.onPageChanged.removeRules(["invidious"]);
}
chrome.permissions.remove({
origins: getInvidiousInstancesRegex()
});
}
function localizeHtmlPage() {
//Localize by replacing __MSG_***__ meta tags
var objects = document.getElementsByClassName("sponsorBlockPageBody")[0].children;
for (var j = 0; j < objects.length; j++) {
var obj = objects[j];
let localizedMessage = getLocalizedMessage(obj.innerHTML.toString());
if (localizedMessage) obj.innerHTML = localizedMessage;
}
}
function getLocalizedMessage(text) {
var valNewH = text.replace(/__MSG_(\w+)__/g, function(match, v1) {
return v1 ? chrome.i18n.getMessage(v1) : "";
});
if(valNewH != text) {
return valNewH;
} else {
return false;
}
}
/**
* @returns {String[]} Invidious Instances in regex form
*/
function getInvidiousInstancesRegex() {
var invidiousInstancesRegex = [];
for (const url of SB.config.invidiousInstances) {
invidiousInstancesRegex.push("https://*." + url + "/*");
invidiousInstancesRegex.push("http://*." + url + "/*");
}
return invidiousInstancesRegex;
}
function generateUserID(length = 36) {
let charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
let result = "";
if (window.crypto && window.crypto.getRandomValues) {
values = new Uint32Array(length);
window.crypto.getRandomValues(values);
for (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;
}
}
/**
* Gets the error message in a nice string
*
* @param {int} statusCode
* @returns {string} errorMessage
*/
function getErrorMessage(statusCode) {
let errorMessage = "";
if([400, 429, 409, 502, 0].includes(statusCode)) {
//treat them the same
if (statusCode == 503) statusCode = 502;
errorMessage = chrome.i18n.getMessage(statusCode + "") + " " + chrome.i18n.getMessage("errorCode") + statusCode
+ "\n\n" + chrome.i18n.getMessage("statusReminder");
} else {
errorMessage = chrome.i18n.getMessage("connectionError") + statusCode;
}
return errorMessage;
}
/**
* Is this Firefox (web-extensions)
*/
function isFirefox() {
return typeof(browser) !== "undefined";
}

43
webpack/webpack.common.js Normal file
View File

@@ -0,0 +1,43 @@
const webpack = require("webpack");
const path = require('path');
const CopyPlugin = require('copy-webpack-plugin');
const srcDir = '../src/';
module.exports = {
entry: {
popup: path.join(__dirname, srcDir + 'popup.ts'),
background: path.join(__dirname, srcDir + 'background.ts'),
content_script: path.join(__dirname, srcDir + 'content_script.ts')
},
output: {
path: path.join(__dirname, '../dist/js'),
filename: '[name].js'
},
optimization: {
splitChunks: {
name: 'vendor',
chunks: "initial"
}
},
module: {
rules: [
{
test: /\.tsx?$/,
use: 'ts-loader',
exclude: /node_modules/
}
]
},
resolve: {
extensions: ['.ts', '.tsx', '.js']
},
plugins: [
// exclude locale files in moment
new webpack.IgnorePlugin(/^\.\/locale$/, /moment$/),
new CopyPlugin([
{ from: '.', to: '../' }
],
{context: 'public' }
),
]
};

7
webpack/webpack.dev.js Normal file
View File

@@ -0,0 +1,7 @@
const merge = require('webpack-merge');
const common = require('./webpack.common.js');
module.exports = merge(common, {
devtool: 'inline-source-map',
mode: 'development'
});

6
webpack/webpack.prod.js Normal file
View File

@@ -0,0 +1,6 @@
const merge = require('webpack-merge');
const common = require('./webpack.common.js');
module.exports = merge(common, {
mode: 'production'
});