Merge branch 'master' of https://github.com/ajayyy/SponsorBlock into min-duration

# Conflicts:
#	SB.js
#	src/options.ts
This commit is contained in:
Ajay Ramachandran
2020-02-08 19:09:17 -05:00
62 changed files with 6723 additions and 982 deletions

240
src/background.ts Normal file
View File

@@ -0,0 +1,240 @@
import * as Types from "./types";
import Config from "./config";
import Utils from "./utils";
var utils = new Utils({
registerFirefoxContentScript,
unregisterFirefoxContentScript
});
// Used only on Firefox, which does not support non persistent background pages.
var contentScriptRegistrations = {};
// Register content script if needed
if (utils.isFirefox()) {
utils.wait(() => Config.config !== null).then(function() {
if (Config.config.supportInvidious) utils.setupExtraSiteContentScripts();
});
}
chrome.tabs.onUpdated.addListener(function(tabId) {
chrome.tabs.sendMessage(tabId, {
message: 'update',
}, () => void chrome.runtime.lastError ); // Suppress error on Firefox
});
chrome.runtime.onMessage.addListener(function (request, sender, callback) {
switch(request.message) {
case "openConfig":
chrome.runtime.openOptionsPage();
return
case "submitTimes":
submitTimes(request.videoID, callback);
//this allows the callback to be called later by the submitTimes function
return true;
case "addSponsorTime":
addSponsorTime(request.time, request.videoID, callback);
//this allows the callback to be called later
return true;
case "getSponsorTimes":
getSponsorTimes(request.videoID, function(sponsorTimes) {
callback({
sponsorTimes
});
});
//this allows the callback to be called later
return true;
case "submitVote":
submitVote(request.type, request.UUID, callback);
//this allows the callback to be called later
return true;
case "alertPrevious":
chrome.notifications.create("stillThere" + Math.random(), {
type: "basic",
title: chrome.i18n.getMessage("wantToSubmit") + " " + request.previousVideoID + "?",
message: chrome.i18n.getMessage("leftTimes"),
iconUrl: "./icons/LogoSponsorBlocker256px.png"
});
case "registerContentScript":
registerFirefoxContentScript(request);
return false;
case "unregisterContentScript":
unregisterFirefoxContentScript(request.id)
return false;
}
});
//add help page on install
chrome.runtime.onInstalled.addListener(function (object) {
// This let's the config sync to run fully before checking.
// This is required on Firefox
setTimeout(function() {
const userID = Config.config.userID;
// If there is no userID, then it is the first install.
if (!userID){
//open up the install page
chrome.tabs.create({url: chrome.extension.getURL("/help/index_en.html")});
//generate a userID
const newUserID = utils.generateUserID();
//save this UUID
Config.config.userID = newUserID;
//TODO: Remove when invidious support is old
// Don't show this to new users
Config.config.invidiousUpdateInfoShowCount = 6;
}
}, 1500);
});
/**
* Only works on Firefox.
* Firefox requires that it be applied after every extension restart.
*
* @param {JSON} options
*/
function registerFirefoxContentScript(options) {
let oldRegistration = contentScriptRegistrations[options.id];
if (oldRegistration) oldRegistration.unregister();
browser.contentScripts.register({
allFrames: options.allFrames,
js: options.js,
css: options.css,
matches: options.matches
}).then((registration) => void (contentScriptRegistrations[options.id] = registration));
}
/**
* Only works on Firefox.
* Firefox requires that this is handled by the background script
*
*/
function unregisterFirefoxContentScript(id: string) {
contentScriptRegistrations[id].unregister();
delete contentScriptRegistrations[id];
}
//gets the sponsor times from memory
function getSponsorTimes(videoID, callback) {
let sponsorTimes = [];
let sponsorTimesStorage = Config.config.sponsorTimes.get(videoID);
if (sponsorTimesStorage != undefined && sponsorTimesStorage.length > 0) {
sponsorTimes = sponsorTimesStorage;
}
callback(sponsorTimes);
}
function addSponsorTime(time, videoID, callback) {
getSponsorTimes(videoID, function(sponsorTimes) {
//add to sponsorTimes
if (sponsorTimes.length > 0 && sponsorTimes[sponsorTimes.length - 1].length < 2) {
//it is an end time
sponsorTimes[sponsorTimes.length - 1][1] = time;
} else {
//it is a start time
let sponsorTimesIndex = sponsorTimes.length;
sponsorTimes[sponsorTimesIndex] = [];
sponsorTimes[sponsorTimesIndex][0] = time;
}
//save this info
Config.config.sponsorTimes.set(videoID, sponsorTimes);
callback();
});
}
function submitVote(type, UUID, callback) {
let userID = Config.config.userID;
if (userID == undefined || userID === "undefined") {
//generate one
userID = utils.generateUserID();
Config.config.userID = userID;
}
//publish this vote
utils.sendRequestToServer("POST", "/api/voteOnSponsorTime?UUID=" + UUID + "&userID=" + userID + "&type=" + type, function(xmlhttp, error) {
if (xmlhttp.readyState == 4 && xmlhttp.status == 200) {
callback({
successType: 1
});
} else if (xmlhttp.readyState == 4 && xmlhttp.status == 405) {
//duplicate vote
callback({
successType: 0,
statusCode: xmlhttp.status
});
} else if (error) {
//error while connect
callback({
successType: -1,
statusCode: xmlhttp.status
});
}
});
}
async function submitTimes(videoID, callback) {
//get the video times from storage
let sponsorTimes = Config.config.sponsorTimes.get(videoID);
let userID = Config.config.userID;
if (sponsorTimes != undefined && sponsorTimes.length > 0) {
let durationResult = <Types.videoDurationResponse> await new Promise((resolve, reject) => {
chrome.tabs.query({
active: true,
currentWindow: true
}, function(tabs) {
chrome.tabs.sendMessage(tabs[0].id, {
message: "getVideoDuration"
}, (response) => resolve(response));
});
});
//check if a sponsor exceeds the duration of the video
for (let i = 0; i < sponsorTimes.length; i++) {
if (sponsorTimes[i][1] > durationResult.duration) {
sponsorTimes[i][1] = durationResult.duration;
}
}
//submit these times
for (let i = 0; i < sponsorTimes.length; i++) {
//to prevent it from happeneing twice
let increasedContributionAmount = false;
//submit the sponsorTime
utils.sendRequestToServer("GET", "/api/postVideoSponsorTimes?videoID=" + videoID + "&startTime=" + sponsorTimes[i][0] + "&endTime=" + sponsorTimes[i][1]
+ "&userID=" + userID, function(xmlhttp, error) {
if (xmlhttp.readyState == 4 && !error) {
callback({
statusCode: xmlhttp.status
});
if (xmlhttp.status == 200) {
//save the amount contributed
if (!increasedContributionAmount) {
increasedContributionAmount = true;
Config.config.sponsorTimesContributed = Config.config.sponsorTimesContributed + sponsorTimes.length;
}
} else if (error) {
callback({
statusCode: -1
});
}
}
});
}
}
}

243
src/config.ts Normal file
View File

@@ -0,0 +1,243 @@
interface SBConfig {
userID: string,
sponsorTimes: SBMap<string, any>,
whitelistedChannels: Array<any>,
startSponsorKeybind: string,
submitKeybind: string,
minutesSaved: number,
skipCount: number,
sponsorTimesContributed: number,
disableSkipping: boolean,
disableAutoSkip: boolean,
trackViewCount: boolean,
dontShowNotice: boolean,
hideVideoPlayerControls: boolean,
hideInfoButtonPlayerControls: boolean,
hideDeleteButtonPlayerControls: boolean,
hideDiscordLaunches: number,
hideDiscordLink: boolean,
invidiousInstances: string[],
invidiousUpdateInfoShowCount: number,
autoUpvote: boolean,
supportInvidious: false
}
interface SBObject {
configListeners: Array<Function>;
defaults: SBConfig;
localConfig: SBConfig;
config: SBConfig;
}
// Allows a SBMap to be conveted into json form
// Currently used for local storage
class SBMap<T, U> extends Map {
id: string;
constructor(id: string, entries?: [T, U][]) {
super();
this.id = id;
// Import all entries if they were given
if (entries !== undefined) {
for (const item of entries) {
this.set(item[0], item[1])
}
}
}
set(key, value) {
const result = super.set(key, value);
// Store updated SBMap locally
chrome.storage.sync.set({
[this.id]: encodeStoredItem(this)
});
return result;
}
delete(key) {
const result = super.delete(key);
// Store updated SBMap locally
chrome.storage.sync.set({
[this.id]: encodeStoredItem(this)
});
return result;
}
clear() {
const result = super.clear();
chrome.storage.sync.set({
[this.id]: encodeStoredItem(this)
});
return result;
}
toJSON() {
return Array.from(this.entries());
}
}
var Config: SBObject = {
/**
* Callback function when an option is updated
*/
configListeners: [],
defaults: {
userID: null,
sponsorTimes: new SBMap("sponsorTimes"),
whitelistedChannels: [],
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,
supportInvidious: false
},
localConfig: null,
config: null
};
// Function setup
/**
* A SBMap cannot be stored in the chrome storage.
* This data will be encoded into an array instead as specified by the toJSON function.
*
* @param data
*/
function encodeStoredItem(data) {
// if data is SBMap convert to json for storing
if(!(data instanceof SBMap)) return data;
return JSON.stringify(data);
}
/**
* An SBMap cannot be stored in the chrome storage.
* This data will be decoded from the array it is stored in
*
* @param {*} data
*/
function decodeStoredItem(id: string, data) {
if(typeof data !== "string") return data;
try {
let str = JSON.parse(data);
if(!Array.isArray(str)) return data;
return new SBMap(id, str);
} catch(e) {
// If all else fails, return the data
return data;
}
}
function configProxy(): any {
chrome.storage.onChanged.addListener((changes, namespace) => {
for (const key in changes) {
Config.localConfig[key] = decodeStoredItem(key, changes[key].newValue);
}
for (const callback of Config.configListeners) {
callback(changes);
}
});
var handler: ProxyHandler<any> = {
set(obj, prop, value) {
Config.localConfig[prop] = value;
chrome.storage.sync.set({
[prop]: encodeStoredItem(value)
});
return true;
},
get(obj, prop): any {
let data = Config.localConfig[prop];
return obj[prop] || data;
},
deleteProperty(obj, prop) {
chrome.storage.sync.remove(<string> prop);
return true;
}
};
return new Proxy({handler}, handler);
}
function fetchConfig() {
return new Promise((resolve, reject) => {
chrome.storage.sync.get(null, function(items) {
Config.localConfig = <SBConfig> <unknown> items; // Data is ready
resolve();
});
});
}
function migrateOldFormats() { // Convert sponsorTimes format
for (const key in Config.localConfig) {
if (key.startsWith("sponsorTimes") && key !== "sponsorTimes" && key !== "sponsorTimesContributed") {
Config.config.sponsorTimes.set(key.substr(12), Config.config[key]);
delete Config.config[key];
}
}
}
async function setupConfig() {
await fetchConfig();
addDefaults();
convertJSON();
Config.config = configProxy();
migrateOldFormats();
}
// Reset config
function resetConfig() {
Config.config = Config.defaults;
};
function convertJSON() {
Object.keys(Config.defaults).forEach(key => {
Config.localConfig[key] = decodeStoredItem(key, Config.localConfig[key]);
});
}
// Add defaults
function addDefaults() {
for (const key in Config.defaults) {
if(!Config.localConfig.hasOwnProperty(key)) {
Config.localConfig[key] = Config.defaults[key];
}
}
};
// Sync config
setupConfig();
export default Config;

1143
src/content.ts Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,95 @@
/*
This is based on code from VideoSegments.
https://github.com/videosegments/videosegments/commits/f1e111bdfe231947800c6efdd51f62a4e7fef4d4/segmentsbar/segmentsbar.js
*/
'use strict';
let barTypes = {
"undefined": {
color: "#00d400",
opacity: "0.5"
},
"sponsor": {
color: "#00d400",
opacity: "0.5"
},
"previewSponsor": {
color: "#0000d4",
opacity: "0.5"
}
};
class PreviewBar {
container: HTMLUListElement;
parent: any;
constructor(parent) {
this.container = document.createElement('ul');
this.container.id = 'previewbar';
this.parent = parent;
this.updatePosition();
}
updatePosition() {
//below the seek bar
// this.parent.insertAdjacentElement("afterEnd", this.container);
//on the seek bar
this.parent.insertAdjacentElement("afterBegin", this.container);
}
updateColor(segment, color, opacity) {
let bars = <NodeListOf<HTMLElement>> document.querySelectorAll('[data-vs-segment-type=' + segment + ']');
for (let bar of bars) {
bar.style.backgroundColor = color;
bar.style.opacity = opacity;
}
}
set(timestamps, types, duration) {
while (this.container.firstChild) {
this.container.removeChild(this.container.firstChild);
}
if (!timestamps || !types) {
return;
}
// to avoid rounding error resulting in width more than 100%
duration = Math.floor(duration * 100) / 100;
let width;
for (let i = 0; i < timestamps.length; i++) {
if (types[i] == null) continue;
width = (timestamps[i][1] - timestamps[i][0]) / duration * 100;
width = Math.floor(width * 100) / 100;
let bar = this.createBar();
bar.setAttribute('data-vs-segment-type', types[i]);
bar.style.backgroundColor = barTypes[types[i]].color;
bar.style.opacity = barTypes[types[i]].opacity;
bar.style.width = width + '%';
bar.style.left = (timestamps[i][0] / duration * 100) + "%";
bar.style.position = "absolute"
this.container.insertAdjacentElement("beforeend", bar);
}
}
createBar() {
let bar = document.createElement('li');
bar.classList.add('previewbar');
bar.innerHTML = '&nbsp;';
return bar;
}
remove() {
this.container.remove();
this.container = undefined;
}
}
export default PreviewBar;

View File

@@ -0,0 +1,437 @@
'use strict';
/**
* The notice that tells the user that a sponsor was just skipped
*/
class SkipNotice {
parent: HTMLElement;
UUID: string;
manualSkip: boolean;
// Contains functions and variables from the content script needed by the skip notice
contentContainer: () => any;
maxCountdownTime: () => number;
countdownTime: any;
countdownInterval: NodeJS.Timeout;
unskipCallback: any;
idSuffix: any;
constructor(parent: HTMLElement, UUID: string, manualSkip: boolean = false, contentContainer) {
this.parent = parent;
this.UUID = UUID;
this.manualSkip = manualSkip;
this.contentContainer = contentContainer;
let noticeTitle = chrome.i18n.getMessage("noticeTitle");
if (manualSkip) {
noticeTitle = chrome.i18n.getMessage("noticeTitleNotSkipped");
}
this.maxCountdownTime = () => 4;
//the countdown until this notice closes
this.countdownTime = this.maxCountdownTime();
//the id for the setInterval running the countdown
this.countdownInterval = null;
//the unskip button's callback
this.unskipCallback = this.unskip.bind(this);
//add notice
let amountOfPreviousNotices = document.getElementsByClassName("sponsorSkipNotice").length;
//this is the suffix added at the end of every id
this.idSuffix = this.UUID + amountOfPreviousNotices;
if (amountOfPreviousNotices > 0) {
//already exists
let previousNotice = document.getElementsByClassName("sponsorSkipNotice")[0];
previousNotice.classList.add("secondSkipNotice")
}
let noticeElement = document.createElement("div");
//what sponsor time this is about
noticeElement.id = "sponsorSkipNotice" + this.idSuffix;
noticeElement.classList.add("sponsorSkipObject");
noticeElement.classList.add("sponsorSkipNotice");
noticeElement.style.zIndex = String(50 + amountOfPreviousNotices);
//add mouse enter and leave listeners
noticeElement.addEventListener("mouseenter", this.pauseCountdown.bind(this));
noticeElement.addEventListener("mouseleave", this.startCountdown.bind(this));
//the row that will contain the info
let firstRow = document.createElement("tr");
firstRow.id = "sponsorSkipNoticeFirstRow" + this.idSuffix;
let logoColumn = document.createElement("td");
let logoElement = document.createElement("img");
logoElement.id = "sponsorSkipLogo" + this.idSuffix;
logoElement.className = "sponsorSkipLogo sponsorSkipObject";
logoElement.src = chrome.extension.getURL("icons/IconSponsorBlocker256px.png");
let noticeMessage = document.createElement("span");
noticeMessage.id = "sponsorSkipMessage" + this.idSuffix;
noticeMessage.classList.add("sponsorSkipMessage");
noticeMessage.classList.add("sponsorSkipObject");
noticeMessage.innerText = noticeTitle;
//create the first column
logoColumn.appendChild(logoElement);
logoColumn.appendChild(noticeMessage);
//add the x button
let closeButtonContainer = document.createElement("td");
closeButtonContainer.className = "sponsorSkipNoticeRightSection";
closeButtonContainer.style.top = "11px";
let timeLeft = document.createElement("span");
timeLeft.id = "sponsorSkipNoticeTimeLeft" + this.idSuffix;
timeLeft.innerText = this.countdownTime + "s";
timeLeft.className = "sponsorSkipObject sponsorSkipNoticeTimeLeft";
let hideButton = document.createElement("img");
hideButton.src = chrome.extension.getURL("icons/close.png");
hideButton.className = "sponsorSkipObject sponsorSkipNoticeButton sponsorSkipNoticeCloseButton sponsorSkipNoticeRightButton";
hideButton.addEventListener("click", this.close.bind(this));
closeButtonContainer.appendChild(timeLeft);
closeButtonContainer.appendChild(hideButton);
//add all objects to first row
firstRow.appendChild(logoColumn);
firstRow.appendChild(closeButtonContainer);
let spacer = document.createElement("hr");
spacer.id = "sponsorSkipNoticeSpacer" + this.idSuffix;
spacer.className = "sponsorBlockSpacer";
//the row that will contain the buttons
let secondRow = document.createElement("tr");
secondRow.id = "sponsorSkipNoticeSecondRow" + this.idSuffix;
//thumbs up and down buttons
let voteButtonsContainer = document.createElement("td");
voteButtonsContainer.id = "sponsorTimesVoteButtonsContainer" + this.idSuffix;
voteButtonsContainer.className = "sponsorTimesVoteButtonsContainer"
let reportText = document.createElement("span");
reportText.id = "sponsorTimesReportText" + this.idSuffix;
reportText.className = "sponsorTimesInfoMessage sponsorTimesVoteButtonMessage";
reportText.innerText = chrome.i18n.getMessage("reportButtonTitle");
reportText.style.marginRight = "5px";
reportText.setAttribute("title", chrome.i18n.getMessage("reportButtonInfo"));
let downvoteButton = document.createElement("img");
downvoteButton.id = "sponsorTimesDownvoteButtonsContainer" + this.idSuffix;
downvoteButton.className = "sponsorSkipObject voteButton";
downvoteButton.src = chrome.extension.getURL("icons/report.png");
downvoteButton.addEventListener("click", () => this.contentContainer().vote(0, this.UUID, this));
downvoteButton.setAttribute("title", chrome.i18n.getMessage("reportButtonInfo"));
//add downvote and report text to container
voteButtonsContainer.appendChild(reportText);
voteButtonsContainer.appendChild(downvoteButton);
//add unskip button
let unskipContainer = document.createElement("td");
unskipContainer.className = "sponsorSkipNoticeUnskipSection";
let unskipButton = document.createElement("button");
unskipButton.id = "sponsorSkipUnskipButton" + this.idSuffix;
unskipButton.innerText = chrome.i18n.getMessage("unskip");
unskipButton.className = "sponsorSkipObject sponsorSkipNoticeButton";
unskipButton.addEventListener("click", this.unskipCallback);
unskipButton.style.marginLeft = "4px";
unskipContainer.appendChild(unskipButton);
//add don't show again button
let dontshowContainer = document.createElement("td");
dontshowContainer.className = "sponsorSkipNoticeRightSection";
let dontShowAgainButton = document.createElement("button");
dontShowAgainButton.innerText = chrome.i18n.getMessage("Hide");
dontShowAgainButton.className = "sponsorSkipObject sponsorSkipNoticeButton sponsorSkipNoticeRightButton";
dontShowAgainButton.addEventListener("click", this.contentContainer().dontShowNoticeAgain);
// Don't let them hide it if manually skipping
if (!this.manualSkip) {
dontshowContainer.appendChild(dontShowAgainButton);
}
//add to row
secondRow.appendChild(voteButtonsContainer);
secondRow.appendChild(unskipContainer);
secondRow.appendChild(dontshowContainer);
noticeElement.appendChild(firstRow);
noticeElement.appendChild(spacer);
noticeElement.appendChild(secondRow);
//get reference node
let referenceNode = document.getElementById("movie_player") || document.querySelector("#player-container .video-js");
if (referenceNode == null) {
//for embeds
let player = document.getElementById("player");
referenceNode = <HTMLElement> player.firstChild;
let index = 1;
//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")) {
referenceNode = <HTMLElement> player.children[index];
index++;
}
}
referenceNode.prepend(noticeElement);
if (manualSkip) {
this.unskippedMode(chrome.i18n.getMessage("skip"));
}
this.startCountdown();
}
//called every second to lower the countdown before hiding the notice
countdown() {
this.countdownTime--;
if (this.countdownTime <= 0) {
//remove this from setInterval
clearInterval(this.countdownInterval);
//time to close this notice
this.close();
return;
}
if (this.countdownTime == 3) {
//start fade out animation
let notice = document.getElementById("sponsorSkipNotice" + this.idSuffix);
notice.style.removeProperty("animation");
notice.classList.add("sponsorSkipNoticeFadeOut");
}
this.updateTimerDisplay();
}
pauseCountdown() {
//remove setInterval
clearInterval(this.countdownInterval);
this.countdownInterval = null;
//reset countdown
this.countdownTime = this.maxCountdownTime();
//inform the user
let timeLeft = document.getElementById("sponsorSkipNoticeTimeLeft" + this.idSuffix);
timeLeft.innerText = chrome.i18n.getMessage("paused");
//remove the fade out class if it exists
let notice = document.getElementById("sponsorSkipNotice" + this.idSuffix);
notice.classList.remove("sponsorSkipNoticeFadeOut");
notice.style.animation = "none";
}
startCountdown() {
//if it has already started, don't start it again
if (this.countdownInterval !== null) return;
this.countdownInterval = setInterval(this.countdown.bind(this), 1000);
this.updateTimerDisplay();
}
updateTimerDisplay() {
//update the timer display
let timeLeft = document.getElementById("sponsorSkipNoticeTimeLeft" + this.idSuffix);
timeLeft.innerText = this.countdownTime + "s";
}
unskip() {
this.contentContainer().unskipSponsorTime(this.UUID);
this.unskippedMode(chrome.i18n.getMessage("reskip"));
}
/** Sets up notice to be not skipped yet */
unskippedMode(buttonText) {
//change unskip button to a reskip button
let unskipButton = this.changeUnskipButton(buttonText);
//setup new callback
this.unskipCallback = this.reskip.bind(this);
unskipButton.addEventListener("click", this.unskipCallback);
//change max duration to however much of the sponsor is left
this.maxCountdownTime = function() {
let sponsorTime = this.contentContainer().sponsorTimes[this.contentContainer().UUIDs.indexOf(this.UUID)];
let duration = Math.round(sponsorTime[1] - this.contentContainer().v.currentTime);
return Math.max(duration, 4);
};
this.countdownTime = this.maxCountdownTime();
this.updateTimerDisplay();
}
reskip() {
this.contentContainer().reskipSponsorTime(this.UUID);
//change reskip button to a unskip button
let unskipButton = this.changeUnskipButton(chrome.i18n.getMessage("unskip"));
//setup new callback
this.unskipCallback = this.unskip.bind(this);
unskipButton.addEventListener("click", this.unskipCallback);
//reset duration
this.maxCountdownTime = () => 4;
this.countdownTime = this.maxCountdownTime();
this.updateTimerDisplay();
// See if the title should be changed
if (this.manualSkip) {
this.changeNoticeTitle(chrome.i18n.getMessage("noticeTitle"));
this.contentContainer().vote(1, this.UUID, this);
}
}
/**
* Changes the text on the reskip button
*
* @param {string} text
* @returns {HTMLElement} unskipButton
*/
changeUnskipButton(text) {
let unskipButton = document.getElementById("sponsorSkipUnskipButton" + this.idSuffix);
unskipButton.innerText = text;
unskipButton.removeEventListener("click", this.unskipCallback);
return unskipButton;
}
afterDownvote() {
this.addVoteButtonInfo(chrome.i18n.getMessage("voted"));
this.addNoticeInfoMessage(chrome.i18n.getMessage("hitGoBack"));
//remove this sponsor from the sponsors looked up
//find which one it is
for (let i = 0; i < this.contentContainer().sponsorTimes.length; i++) {
if (this.contentContainer().UUIDs[i] == this.UUID) {
//this one is the one to hide
//add this as a hidden sponsorTime
this.contentContainer().hiddenSponsorTimes.push(i);
this.contentContainer().updatePreviewBar();
break;
}
}
}
changeNoticeTitle(title) {
let noticeElement = document.getElementById("sponsorSkipMessage" + this.idSuffix);
noticeElement.innerText = title;
}
addNoticeInfoMessage(message: string, message2: string = "") {
let previousInfoMessage = document.getElementById("sponsorTimesInfoMessage" + this.idSuffix);
if (previousInfoMessage != null) {
//remove it
document.getElementById("sponsorSkipNotice" + this.idSuffix).removeChild(previousInfoMessage);
}
let previousInfoMessage2 = document.getElementById("sponsorTimesInfoMessage" + this.idSuffix + "2");
if (previousInfoMessage2 != null) {
//remove it
document.getElementById("sponsorSkipNotice" + this.idSuffix).removeChild(previousInfoMessage2);
}
//add info
let thanksForVotingText = document.createElement("p");
thanksForVotingText.id = "sponsorTimesInfoMessage" + this.idSuffix;
thanksForVotingText.className = "sponsorTimesInfoMessage";
thanksForVotingText.innerText = message;
//add element to div
document.getElementById("sponsorSkipNotice" + this.idSuffix).insertBefore(thanksForVotingText, document.getElementById("sponsorSkipNoticeSpacer" + this.idSuffix));
if (message2 !== undefined) {
let thanksForVotingText2 = document.createElement("p");
thanksForVotingText2.id = "sponsorTimesInfoMessage" + this.idSuffix + "2";
thanksForVotingText2.className = "sponsorTimesInfoMessage";
thanksForVotingText2.innerText = message2;
//add element to div
document.getElementById("sponsorSkipNotice" + this.idSuffix).insertBefore(thanksForVotingText2, document.getElementById("sponsorSkipNoticeSpacer" + this.idSuffix));
}
}
resetNoticeInfoMessage() {
let previousInfoMessage = document.getElementById("sponsorTimesInfoMessage" + this.idSuffix);
if (previousInfoMessage != null) {
//remove it
document.getElementById("sponsorSkipNotice" + this.idSuffix).removeChild(previousInfoMessage);
}
}
addVoteButtonInfo(message) {
this.resetVoteButtonInfo();
//hide report button and text for it
let downvoteButton = document.getElementById("sponsorTimesDownvoteButtonsContainer" + this.idSuffix);
if (downvoteButton != null) {
downvoteButton.style.display = "none";
}
let downvoteButtonText = document.getElementById("sponsorTimesReportText" + this.idSuffix);
if (downvoteButtonText != null) {
downvoteButtonText.style.display = "none";
}
//add info
let thanksForVotingText = document.createElement("td");
thanksForVotingText.id = "sponsorTimesVoteButtonInfoMessage" + this.idSuffix;
thanksForVotingText.className = "sponsorTimesInfoMessage sponsorTimesVoteButtonMessage";
thanksForVotingText.innerText = message;
//add element to div
document.getElementById("sponsorSkipNoticeSecondRow" + this.idSuffix).prepend(thanksForVotingText);
}
resetVoteButtonInfo() {
let previousInfoMessage = document.getElementById("sponsorTimesVoteButtonInfoMessage" + this.idSuffix);
if (previousInfoMessage != null) {
//remove it
document.getElementById("sponsorSkipNoticeSecondRow" + this.idSuffix).removeChild(previousInfoMessage);
}
//show button again
document.getElementById("sponsorTimesDownvoteButtonsContainer" + this.idSuffix).style.removeProperty("display");
}
//close this notice
close() {
//reset message
this.resetNoticeInfoMessage();
let notice = document.getElementById("sponsorSkipNotice" + this.idSuffix);
if (notice != null) {
notice.remove();
}
//remove setInterval
if (this.countdownInterval !== null) clearInterval(this.countdownInterval);
}
}
export default SkipNotice;

314
src/options.ts Normal file
View File

@@ -0,0 +1,314 @@
import Config from "./config";
import Utils from "./utils";
var utils = new Utils();
window.addEventListener('DOMContentLoaded', init);
async function init() {
utils.localizeHtmlPage();
if (!Config.configListeners.includes(optionsConfigUpdateListener)) {
Config.configListeners.push(optionsConfigUpdateListener);
}
await utils.wait(() => Config.config !== null);
// Set all of the toggle options to the correct option
let optionsContainer = document.getElementById("options");
let optionsElements = optionsContainer.querySelectorAll("*");
for (let i = 0; i < optionsElements.length; i++) {
switch (optionsElements[i].getAttribute("option-type")) {
case "toggle":
let option = optionsElements[i].getAttribute("sync-option");
let optionResult = Config.config[option];
let checkbox = optionsElements[i].querySelector("input");
let reverse = optionsElements[i].getAttribute("toggle-type") === "reverse";
if (optionResult != undefined) {
checkbox.checked = optionResult;
if (reverse) {
optionsElements[i].querySelector("input").checked = !optionResult;
}
}
// See if anything extra should be run first time
switch (option) {
case "supportInvidious":
invidiousInit(checkbox, option);
break;
}
// Add click listener
checkbox.addEventListener("click", () => {
Config.config[option] = reverse ? !checkbox.checked : checkbox.checked;
// See if anything extra must be run
switch (option) {
case "supportInvidious":
invidiousOnClick(checkbox, option);
break;
}
});
break;
case "text-change":
let button = optionsElements[i].querySelector(".trigger-button");
button.addEventListener("click", () => activateTextChange(<HTMLElement> optionsElements[i]));
let textChangeOption = optionsElements[i].getAttribute("sync-option");
// See if anything extra must be done
switch (textChangeOption) {
case "invidiousInstances":
invidiousInstanceAddInit(<HTMLElement> optionsElements[i], textChangeOption);
}
break;
case "keybind-change":
let keybindButton = optionsElements[i].querySelector(".trigger-button");
keybindButton.addEventListener("click", () => activateKeybindChange(<HTMLElement> optionsElements[i]));
break;
case "display":
updateDisplayElement(<HTMLElement> optionsElements[i])
break;
case "number-change":
let numberChangeOption = optionsElements[i].getAttribute("sync-option");
let configValue = SB.config[numberChangeOption];
let numberInput = optionsElements[i].querySelector("input");
if (isNaN(configValue) || configValue < 0) {
numberInput.value = SB.defaults[numberChangeOption];
} else {
numberInput.value = configValue;
}
numberInput.addEventListener("input", () => {
SB.config[numberChangeOption] = numberInput.value;
});
break;
}
}
optionsContainer.classList.remove("hidden");
optionsContainer.classList.add("animated");
}
/**
* Called when the config is updated
*
* @param {String} element
*/
function optionsConfigUpdateListener(changes) {
let optionsContainer = document.getElementById("options");
let optionsElements = optionsContainer.querySelectorAll("*");
for (let i = 0; i < optionsElements.length; i++) {
switch (optionsElements[i].getAttribute("option-type")) {
case "display":
updateDisplayElement(<HTMLElement> optionsElements[i])
}
}
}
/**
* Will set display elements to the proper text
*
* @param element
*/
function updateDisplayElement(element: HTMLElement) {
let displayOption = element.getAttribute("sync-option")
let displayText = Config.config[displayOption];
element.innerText = displayText;
// See if anything extra must be run
switch (displayOption) {
case "invidiousInstances":
element.innerText = displayText.join(', ');
break;
}
}
/**
* Initializes the option to add Invidious instances
*
* @param element
* @param option
*/
function invidiousInstanceAddInit(element: HTMLElement, option: string) {
let textBox = <HTMLInputElement> element.querySelector(".option-text-box");
let button = element.querySelector(".trigger-button");
let setButton = element.querySelector(".text-change-set");
setButton.addEventListener("click", async function(e) {
if (textBox.value == "" || textBox.value.includes("/") || textBox.value.includes("http") || textBox.value.includes(":")) {
alert(chrome.i18n.getMessage("addInvidiousInstanceError"));
} else {
// Add this
let instanceList = Config.config[option];
if (!instanceList) instanceList = [];
instanceList.push(textBox.value);
Config.config[option] = instanceList;
let checkbox = <HTMLInputElement> document.querySelector("#support-invidious input");
checkbox.checked = true;
invidiousOnClick(checkbox, "supportInvidious");
textBox.value = "";
// Hide this section again
element.querySelector(".option-hidden-section").classList.add("hidden");
button.classList.remove("disabled");
}
});
let resetButton = element.querySelector(".invidious-instance-reset");
resetButton.addEventListener("click", function(e) {
if (confirm(chrome.i18n.getMessage("resetInvidiousInstanceAlert"))) {
// Set to a clone of the default
Config.config[option] = Config.defaults[option].slice(0);
}
});
}
/**
* Run when the invidious button is being initialized
*
* @param checkbox
* @param option
*/
function invidiousInit(checkbox: HTMLInputElement, option: string) {
let permissions = ["declarativeContent"];
if (utils.isFirefox()) permissions = [];
chrome.permissions.contains({
origins: utils.getInvidiousInstancesRegex(),
permissions: permissions
}, function (result) {
if (result != checkbox.checked) {
Config.config[option] = result;
checkbox.checked = result;
}
});
}
/**
* Run whenever the invidious checkbox is clicked
*
* @param checkbox
* @param option
*/
function invidiousOnClick(checkbox: HTMLInputElement, option: string) {
if (checkbox.checked) {
utils.setupExtraSitePermissions(function (granted) {
if (!granted) {
Config.config[option] = false;
checkbox.checked = false;
}
});
} else {
utils.removeExtraSiteRegistration();
}
}
/**
* Will trigger the container to ask the user for a keybind.
*
* @param element
*/
function activateKeybindChange(element: HTMLElement) {
let button = element.querySelector(".trigger-button");
if (button.classList.contains("disabled")) return;
button.classList.add("disabled");
let option = element.getAttribute("sync-option");
let currentlySet = Config.config[option] !== null ? chrome.i18n.getMessage("keybindCurrentlySet") : "";
let status = <HTMLElement> element.querySelector(".option-hidden-section > .keybind-status");
status.innerText = chrome.i18n.getMessage("keybindDescription") + currentlySet;
if (Config.config[option] !== null) {
let statusKey = <HTMLElement> element.querySelector(".option-hidden-section > .keybind-status-key");
statusKey.innerText = Config.config[option];
}
element.querySelector(".option-hidden-section").classList.remove("hidden");
document.addEventListener("keydown", (e) => keybindKeyPressed(element, e), {once: true});
}
/**
* Called when a key is pressed in an activiated keybind change option.
*
* @param element
* @param e
*/
function keybindKeyPressed(element: HTMLElement, e: KeyboardEvent) {
var key = e.key;
let button = element.querySelector(".trigger-button");
// cancel setting a keybind
if (key === "Escape") {
element.querySelector(".option-hidden-section").classList.add("hidden");
button.classList.remove("disabled");
return;
}
let option = element.getAttribute("sync-option");
Config.config[option] = key;
let status = <HTMLElement> element.querySelector(".option-hidden-section > .keybind-status");
status.innerText = chrome.i18n.getMessage("keybindDescriptionComplete");
let statusKey = <HTMLElement> element.querySelector(".option-hidden-section > .keybind-status-key");
statusKey.innerText = key;
button.classList.remove("disabled");
}
/**
* Will trigger the textbox to appear to be able to change an option's text.
*
* @param element
*/
function activateTextChange(element: HTMLElement) {
let button = element.querySelector(".trigger-button");
if (button.classList.contains("disabled")) return;
button.classList.add("disabled");
let textBox = <HTMLInputElement> element.querySelector(".option-text-box");
let option = element.getAttribute("sync-option");
// See if anything extra must be done
switch (option) {
case "invidiousInstances":
element.querySelector(".option-hidden-section").classList.remove("hidden");
return;
}
textBox.value = Config.config[option];
let setButton = element.querySelector(".text-change-set");
setButton.addEventListener("click", () => {
let confirmMessage = element.getAttribute("confirm-message");
if (confirmMessage === null || confirm(chrome.i18n.getMessage(confirmMessage))) {
Config.config[option] = textBox.value;
}
});
element.querySelector(".option-hidden-section").classList.remove("hidden");
}

1130
src/popup.ts Normal file

File diff suppressed because it is too large Load Diff

7
src/types.ts Normal file
View File

@@ -0,0 +1,7 @@
interface videoDurationResponse {
duration: number;
}
export {
videoDurationResponse
};

267
src/utils.ts Normal file
View File

@@ -0,0 +1,267 @@
import * as CompileConfig from "../config.json";
import Config from "./config";
class Utils {
// Contains functions needed from the background script
backgroundScriptContainer: any = null;
constructor(backgroundScriptContainer?: any) {
this.backgroundScriptContainer = backgroundScriptContainer;
}
// Function that can be used to wait for a condition before returning
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();
});
}
/**
* 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
*/
setupExtraSitePermissions(callback) {
// Request permission
let permissions = ["declarativeContent"];
if (this.isFirefox()) permissions = [];
let self = this;
chrome.permissions.request({
origins: this.getInvidiousInstancesRegex(),
permissions: permissions
}, async function (granted) {
if (granted) {
self.setupExtraSiteContentScripts();
} else {
self.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.
*/
setupExtraSiteContentScripts() {
let js = [
"./js/vendor.js",
"./js/content.js"
];
let css = [
"content.css",
"./libs/Source+Sans+Pro.css",
"popup.css"
];
let self = this;
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 (this.backgroundScriptContainer) {
this.backgroundScriptContainer.registerFirefoxContentScript(registration);
} else {
chrome.runtime.sendMessage(registration);
}
} else {
chrome.declarativeContent.onPageChanged.removeRules(["invidious"], function() {
let conditions = [];
for (const regex of self.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.
*/
removeExtraSiteRegistration() {
if (this.isFirefox()) {
let id = "invidious";
if (this.backgroundScriptContainer) {
this.backgroundScriptContainer.unregisterFirefoxContentScript(id);
} else {
chrome.runtime.sendMessage({
message: "unregisterContentScript",
id: id
});
}
} else if (chrome.declarativeContent) {
// Only if we have permission
chrome.declarativeContent.onPageChanged.removeRules(["invidious"]);
}
chrome.permissions.remove({
origins: this.getInvidiousInstancesRegex()
});
}
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;
}
}
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
*/
getInvidiousInstancesRegex() {
var invidiousInstancesRegex = [];
for (const url of Config.config.invidiousInstances) {
invidiousInstancesRegex.push("https://*." + url + "/*");
invidiousInstancesRegex.push("http://*." + url + "/*");
}
return invidiousInstancesRegex;
}
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
*/
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;
}
/**
* Sends a request to the SponsorBlock server with address added as a query
*
* @param type The request type. "GET", "POST", etc.
* @param address The address to add to the SponsorBlock server address
* @param callback
*/
sendRequestToServer(type: string, address: string, callback?: (xmlhttp: XMLHttpRequest, err: boolean) => any) {
let xmlhttp = new XMLHttpRequest();
xmlhttp.open(type, CompileConfig.serverAddress + address, true);
if (callback != undefined) {
xmlhttp.onreadystatechange = function () {
callback(xmlhttp, false);
};
xmlhttp.onerror = function(ev) {
callback(xmlhttp, true);
};
}
//submit this request
xmlhttp.send();
}
/**
* Is this Firefox (web-extensions)
*/
isFirefox() {
return typeof(browser) !== "undefined";
}
}
export default Utils;