mirror of
https://github.com/ajayyy/SponsorBlock.git
synced 2026-02-01 15:21:12 +03:00
@@ -1,3 +1,5 @@
|
||||
import * as CompileConfig from "../config.json";
|
||||
|
||||
interface SBConfig {
|
||||
userID: string,
|
||||
sponsorTimes: SBMap<string, any>,
|
||||
@@ -14,12 +16,16 @@ interface SBConfig {
|
||||
hideVideoPlayerControls: boolean,
|
||||
hideInfoButtonPlayerControls: boolean,
|
||||
hideDeleteButtonPlayerControls: boolean,
|
||||
hideUploadButtonPlayerControls: boolean,
|
||||
hideDiscordLaunches: number,
|
||||
hideDiscordLink: boolean,
|
||||
invidiousInstances: string[],
|
||||
invidiousUpdateInfoShowCount: number,
|
||||
autoUpvote: boolean,
|
||||
supportInvidious: false
|
||||
supportInvidious: boolean,
|
||||
serverAddress: string,
|
||||
minDuration: number,
|
||||
checkForUnlistedVideos: boolean
|
||||
}
|
||||
|
||||
interface SBObject {
|
||||
@@ -42,7 +48,7 @@ class SBMap<T, U> extends Map {
|
||||
// Import all entries if they were given
|
||||
if (entries !== undefined) {
|
||||
for (const item of entries) {
|
||||
this.set(item[0], item[1])
|
||||
super.set(item[0], item[1])
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -105,12 +111,16 @@ var Config: SBObject = {
|
||||
hideVideoPlayerControls: false,
|
||||
hideInfoButtonPlayerControls: false,
|
||||
hideDeleteButtonPlayerControls: false,
|
||||
hideUploadButtonPlayerControls: false,
|
||||
hideDiscordLaunches: 0,
|
||||
hideDiscordLink: false,
|
||||
invidiousInstances: ["invidio.us", "invidiou.sh", "invidious.snopyta.org"],
|
||||
invidiousUpdateInfoShowCount: 0,
|
||||
autoUpvote: true,
|
||||
supportInvidious: false
|
||||
supportInvidious: false,
|
||||
serverAddress: CompileConfig.serverAddress,
|
||||
minDuration: 0,
|
||||
checkForUnlistedVideos: false
|
||||
},
|
||||
localConfig: null,
|
||||
config: null
|
||||
@@ -236,7 +246,7 @@ function resetConfig() {
|
||||
};
|
||||
|
||||
function convertJSON() {
|
||||
Object.keys(Config.defaults).forEach(key => {
|
||||
Object.keys(Config.localConfig).forEach(key => {
|
||||
Config.localConfig[key] = decodeStoredItem(key, Config.localConfig[key]);
|
||||
});
|
||||
}
|
||||
|
||||
151
src/content.ts
151
src/content.ts
@@ -8,12 +8,15 @@ import runThePopup from "./popup";
|
||||
import PreviewBar from "./js-components/previewBar";
|
||||
import SkipNotice from "./js-components/skipNotice";
|
||||
|
||||
// Hack to get the CSS loaded on permission-based sites (Invidious)
|
||||
utils.wait(() => Config.config !== null, 5000, 10).then(addCSS);
|
||||
|
||||
//was sponsor data found when doing SponsorsLookup
|
||||
var sponsorDataFound = false;
|
||||
var previousVideoID = null;
|
||||
//the actual sponsorTimes if loaded and UUIDs associated with them
|
||||
var sponsorTimes = null;
|
||||
var UUIDs = null;
|
||||
var UUIDs = [];
|
||||
//what video id are these sponsors for
|
||||
var sponsorVideoID = null;
|
||||
|
||||
@@ -215,7 +218,7 @@ function resetValues() {
|
||||
|
||||
//reset sponsor times
|
||||
sponsorTimes = null;
|
||||
UUIDs = null;
|
||||
UUIDs = [];
|
||||
sponsorLookupRetries = 0;
|
||||
|
||||
//empty the preview bar
|
||||
@@ -227,7 +230,7 @@ function resetValues() {
|
||||
sponsorDataFound = false;
|
||||
}
|
||||
|
||||
function videoIDChange(id) {
|
||||
async function videoIDChange(id) {
|
||||
//if the id has not changed return
|
||||
if (sponsorVideoID === id) return;
|
||||
|
||||
@@ -239,6 +242,19 @@ function videoIDChange(id) {
|
||||
//id is not valid
|
||||
if (!id) return;
|
||||
|
||||
// Wait for options to be ready
|
||||
await utils.wait(() => Config.config !== null, 5000, 1);
|
||||
|
||||
// If enabled, it will check if this video is private or unlisted and double check with the user if the sponsors should be looked up
|
||||
if (Config.config.checkForUnlistedVideos) {
|
||||
await utils.wait(isPrivacyInfoAvailable);
|
||||
|
||||
if (isUnlisted()) {
|
||||
let shouldContinue = confirm(chrome.i18n.getMessage("confirmPrivacy"));
|
||||
if(!shouldContinue) return;
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: Use a better method here than using type any
|
||||
// This is done to be able to do channelIDPromise.isFulfilled and channelIDPromise.isRejected
|
||||
let channelIDPromise: any = utils.wait(getChannelID);
|
||||
@@ -363,8 +379,36 @@ function sponsorsLookup(id: string, channelIDPromise?) {
|
||||
if (xmlhttp.readyState == 4 && xmlhttp.status == 200) {
|
||||
sponsorDataFound = true;
|
||||
|
||||
sponsorTimes = JSON.parse(xmlhttp.responseText).sponsorTimes;
|
||||
UUIDs = JSON.parse(xmlhttp.responseText).UUIDs;
|
||||
let recievedSponsorTimes = JSON.parse(xmlhttp.responseText).sponsorTimes;
|
||||
let recievedUUIDs = JSON.parse(xmlhttp.responseText).UUIDs;
|
||||
|
||||
// Check if any old submissions should be kept
|
||||
for (let i = 0; i < UUIDs.length; i++) {
|
||||
if (UUIDs[i] === null) {
|
||||
// This is a user submission, keep it
|
||||
recievedSponsorTimes.push(sponsorTimes[i]);
|
||||
recievedUUIDs.push(UUIDs[i]);
|
||||
}
|
||||
}
|
||||
|
||||
sponsorTimes = recievedSponsorTimes;
|
||||
UUIDs = recievedUUIDs;
|
||||
|
||||
// Remove all submissions smaller than the minimum duration
|
||||
if (Config.config.minDuration !== 0) {
|
||||
let smallSponsors = [];
|
||||
let smallUUIDs = [];
|
||||
|
||||
for (let i = 0; i < sponsorTimes.length; i++) {
|
||||
if (sponsorTimes[i][1] - sponsorTimes[i][0] >= Config.config.minDuration) {
|
||||
smallSponsors.push(sponsorTimes[i]);
|
||||
smallUUIDs.push(UUIDs[i]);
|
||||
}
|
||||
}
|
||||
|
||||
sponsorTimes = smallSponsors;
|
||||
UUIDs = smallUUIDs;
|
||||
}
|
||||
|
||||
// Reset skip save
|
||||
sponsorSkipped = [];
|
||||
@@ -613,7 +657,7 @@ function skipToTime(v, index, sponsorTimes, openNotice) {
|
||||
}
|
||||
|
||||
lastSponsorTimeSkipped = sponsorTimes[index][0];
|
||||
|
||||
|
||||
let currentUUID = UUIDs[index];
|
||||
lastSponsorTimeSkippedUUID = currentUUID;
|
||||
|
||||
@@ -627,17 +671,18 @@ function skipToTime(v, index, sponsorTimes, openNotice) {
|
||||
vote(1, currentUUID, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//send telemetry that a this sponsor was skipped
|
||||
if (Config.config.trackViewCount && !sponsorSkipped[index]) {
|
||||
utils.sendRequestToServer("POST", "/api/viewedVideoSponsorTime?UUID=" + currentUUID);
|
||||
//send telemetry that a this sponsor was skipped
|
||||
if (Config.config.trackViewCount && !sponsorSkipped[index]) {
|
||||
utils.sendRequestToServer("POST", "/api/viewedVideoSponsorTime?UUID=" + currentUUID);
|
||||
|
||||
if (!Config.config.disableAutoSkip) {
|
||||
// Count this as a skip
|
||||
Config.config.minutesSaved = Config.config.minutesSaved + (sponsorTimes[index][1] - sponsorTimes[index][0]) / 60;
|
||||
Config.config.skipCount = Config.config.skipCount + 1;
|
||||
sponsorSkipped[index] = true;
|
||||
if (!Config.config.disableAutoSkip) {
|
||||
// Count this as a skip
|
||||
Config.config.minutesSaved = Config.config.minutesSaved + (sponsorTimes[index][1] - sponsorTimes[index][0]) / 60;
|
||||
Config.config.skipCount = Config.config.skipCount + 1;
|
||||
|
||||
sponsorSkipped[index] = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -787,7 +832,7 @@ async function changeStartSponsorButton(showStartSponsor, uploadButtonVisible) {
|
||||
(<HTMLImageElement> document.getElementById("startSponsorImage")).src = chrome.extension.getURL("icons/PlayerStartIconSponsorBlocker256px.png");
|
||||
document.getElementById("startSponsorButton").setAttribute("title", chrome.i18n.getMessage("sponsorStart"));
|
||||
|
||||
if (document.getElementById("startSponsorImage").style.display != "none" && uploadButtonVisible && !Config.config.hideInfoButtonPlayerControls) {
|
||||
if (document.getElementById("startSponsorImage").style.display != "none" && uploadButtonVisible && !Config.config.hideUploadButtonPlayerControls) {
|
||||
document.getElementById("submitButton").style.display = "unset";
|
||||
} else if (!uploadButtonVisible) {
|
||||
//disable submit button
|
||||
@@ -921,12 +966,10 @@ function vote(type, UUID, skipNotice) {
|
||||
sponsorSkipped[sponsorIndex] = false;
|
||||
}
|
||||
|
||||
// Count this as a skip
|
||||
Config.config.minutesSaved = Config.config.minutesSaved + factor * (sponsorTimes[sponsorIndex][1] - sponsorTimes[sponsorIndex][0]) / 60;
|
||||
|
||||
Config.config.skipCount = 0;
|
||||
|
||||
Config.config.skipCount = Config.config.skipCount + factor * 1;
|
||||
// Count this as a skip
|
||||
Config.config.minutesSaved = Config.config.minutesSaved + factor * (sponsorTimes[sponsorIndex][1] - sponsorTimes[sponsorIndex][0]) / 60;
|
||||
|
||||
Config.config.skipCount = Config.config.skipCount + factor;
|
||||
}
|
||||
|
||||
chrome.runtime.sendMessage({
|
||||
@@ -1006,6 +1049,17 @@ function submitSponsorTimes() {
|
||||
//update sponsorTimesSubmitting
|
||||
sponsorTimesSubmitting = sponsorTimes;
|
||||
|
||||
// Check to see if any of the submissions are below the minimum duration set
|
||||
if (Config.config.minDuration > 0) {
|
||||
for (let i = 0; i < sponsorTimes.length; i++) {
|
||||
if (sponsorTimes[i][1] - sponsorTimes[i][0] < Config.config.minDuration) {
|
||||
let confirmShort = chrome.i18n.getMessage("shortCheck") + "\n\n" + getSponsorTimesMessage(sponsorTimes);
|
||||
|
||||
if(!confirm(confirmShort)) return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let confirmMessage = chrome.i18n.getMessage("submitCheck") + "\n\n" + getSponsorTimesMessage(sponsorTimes)
|
||||
+ "\n\n" + chrome.i18n.getMessage("confirmMSG") + "\n\n" + chrome.i18n.getMessage("guildlinesSummary");
|
||||
if(!confirm(confirmMessage)) return;
|
||||
@@ -1049,10 +1103,12 @@ function sendSubmitMessage(){
|
||||
Config.config.sponsorTimes.delete(currentVideoID);
|
||||
|
||||
//add submissions to current sponsors list
|
||||
if (sponsorTimes === null) sponsorTimes = [];
|
||||
|
||||
sponsorTimes = sponsorTimes.concat(sponsorTimesSubmitting);
|
||||
for (let i = 0; i < sponsorTimesSubmitting.length; i++) {
|
||||
// Add some random IDs
|
||||
UUIDs.push(utils.generateUserID());
|
||||
// Add placeholder IDs
|
||||
UUIDs.push(null);
|
||||
}
|
||||
|
||||
// Empty the submitting times
|
||||
@@ -1092,6 +1148,53 @@ function getSponsorTimesMessage(sponsorTimes) {
|
||||
return sponsorTimesMessage;
|
||||
}
|
||||
|
||||
// Privacy utils
|
||||
function isPrivacyInfoAvailable(): boolean {
|
||||
if(document.location.pathname.startsWith("/embed/")) return true;
|
||||
return document.getElementsByClassName("style-scope ytd-badge-supported-renderer").length >= 2;
|
||||
}
|
||||
|
||||
/**
|
||||
* What privacy level is this YouTube video?
|
||||
*/
|
||||
function getPrivacy(): string {
|
||||
if(document.location.pathname.startsWith("/embed/")) return "Public";
|
||||
|
||||
let privacyElement = <HTMLElement> document.getElementsByClassName("style-scope ytd-badge-supported-renderer")[2];
|
||||
return privacyElement.innerText;
|
||||
}
|
||||
|
||||
/**
|
||||
* Is this an unlisted YouTube video.
|
||||
* Assumes that the the privacy info is available.
|
||||
*/
|
||||
function isUnlisted(): boolean {
|
||||
let privacyElement = <HTMLElement> document.getElementsByClassName("style-scope ytd-badge-supported-renderer")[2];
|
||||
|
||||
return privacyElement.innerText.toLocaleLowerCase() === "unlisted";
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds the CSS to the page if needed. Required on optional sites with Chrome.
|
||||
*/
|
||||
function addCSS() {
|
||||
if (!utils.isFirefox() && Config.config.invidiousInstances.includes(new URL(document.URL).host)) {
|
||||
window.addEventListener("DOMContentLoaded", () => {
|
||||
let head = document.getElementsByTagName("head")[0];
|
||||
|
||||
for (const file of utils.css) {
|
||||
let fileref = document.createElement("link");
|
||||
|
||||
fileref.rel = "stylesheet";
|
||||
fileref.type = "text/css";
|
||||
fileref.href = chrome.extension.getURL(file);
|
||||
|
||||
head.appendChild(fileref);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
//converts time in seconds to minutes:seconds
|
||||
function getFormattedTime(seconds) {
|
||||
let minutes = Math.floor(seconds / 60);
|
||||
|
||||
100
src/options.ts
100
src/options.ts
@@ -50,19 +50,66 @@ async function init() {
|
||||
switch (option) {
|
||||
case "supportInvidious":
|
||||
invidiousOnClick(checkbox, option);
|
||||
break;
|
||||
case "disableAutoSkip":
|
||||
if (!checkbox.checked) {
|
||||
// Enable the notice
|
||||
Config.config["dontShowNotice"] = false;
|
||||
|
||||
let showNoticeSwitch = <HTMLInputElement> document.querySelector("[sync-option='dontShowNotice'] > label > label > input");
|
||||
showNoticeSwitch.checked = true;
|
||||
}
|
||||
|
||||
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");
|
||||
let textChangeInput = <HTMLInputElement> optionsElements[i].querySelector(".option-text-box");
|
||||
|
||||
let textChangeSetButton = <HTMLElement> optionsElements[i].querySelector(".text-change-set");
|
||||
|
||||
textChangeInput.value = Config.config[textChangeOption];
|
||||
|
||||
textChangeSetButton.addEventListener("click", () => {
|
||||
// See if anything extra must be done
|
||||
switch (textChangeOption) {
|
||||
case "serverAddress":
|
||||
let result = validateServerAddress(textChangeInput.value);
|
||||
|
||||
if (result !== null) {
|
||||
textChangeInput.value = result;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
Config.config[textChangeOption] = textChangeInput.value;
|
||||
});
|
||||
|
||||
// Reset to the default if needed
|
||||
let textChangeResetButton = <HTMLElement> optionsElements[i].querySelector(".text-change-reset");
|
||||
textChangeResetButton.addEventListener("click", () => {
|
||||
if (!confirm(chrome.i18n.getMessage("areYouSureReset"))) return;
|
||||
|
||||
Config.config[textChangeOption] = Config.defaults[textChangeOption];
|
||||
|
||||
textChangeInput.value = Config.config[textChangeOption];
|
||||
});
|
||||
|
||||
break;
|
||||
case "private-text-change":
|
||||
let button = optionsElements[i].querySelector(".trigger-button");
|
||||
button.addEventListener("click", () => activatePrivateTextChange(<HTMLElement> optionsElements[i]));
|
||||
|
||||
let privateTextChangeOption = optionsElements[i].getAttribute("sync-option");
|
||||
// See if anything extra must be done
|
||||
switch (textChangeOption) {
|
||||
switch (privateTextChangeOption) {
|
||||
case "invidiousInstances":
|
||||
invidiousInstanceAddInit(<HTMLElement> optionsElements[i], textChangeOption);
|
||||
invidiousInstanceAddInit(<HTMLElement> optionsElements[i], privateTextChangeOption);
|
||||
}
|
||||
|
||||
break;
|
||||
@@ -73,6 +120,24 @@ async function init() {
|
||||
break;
|
||||
case "display":
|
||||
updateDisplayElement(<HTMLElement> optionsElements[i])
|
||||
|
||||
break;
|
||||
case "number-change":
|
||||
let numberChangeOption = optionsElements[i].getAttribute("sync-option");
|
||||
let configValue = Config.config[numberChangeOption];
|
||||
let numberInput = optionsElements[i].querySelector("input");
|
||||
|
||||
if (isNaN(configValue) || configValue < 0) {
|
||||
numberInput.value = Config.defaults[numberChangeOption];
|
||||
} else {
|
||||
numberInput.value = configValue;
|
||||
}
|
||||
|
||||
numberInput.addEventListener("input", () => {
|
||||
Config.config[numberChangeOption] = numberInput.value;
|
||||
});
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -265,7 +330,7 @@ function keybindKeyPressed(element: HTMLElement, e: KeyboardEvent) {
|
||||
*
|
||||
* @param element
|
||||
*/
|
||||
function activateTextChange(element: HTMLElement) {
|
||||
function activatePrivateTextChange(element: HTMLElement) {
|
||||
let button = element.querySelector(".trigger-button");
|
||||
if (button.classList.contains("disabled")) return;
|
||||
|
||||
@@ -293,4 +358,27 @@ function activateTextChange(element: HTMLElement) {
|
||||
});
|
||||
|
||||
element.querySelector(".option-hidden-section").classList.remove("hidden");
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the value used for the database server address.
|
||||
* Returns null and alerts the user if there is an issue.
|
||||
*
|
||||
* @param input Input server address
|
||||
*/
|
||||
function validateServerAddress(input: string): string {
|
||||
input = input.trim();
|
||||
|
||||
// Trim the trailing slashes
|
||||
input = input.replace(/\/+$/, "");
|
||||
|
||||
// If it isn't HTTP protocol
|
||||
if ((!input.startsWith("https://") && !input.startsWith("http://"))) {
|
||||
|
||||
alert(chrome.i18n.getMessage("customAddressError"));
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
return input;
|
||||
}
|
||||
32
src/utils.ts
32
src/utils.ts
@@ -1,4 +1,3 @@
|
||||
import * as CompileConfig from "../config.json";
|
||||
import Config from "./config";
|
||||
|
||||
class Utils {
|
||||
@@ -6,6 +5,17 @@ class Utils {
|
||||
// Contains functions needed from the background script
|
||||
backgroundScriptContainer: any = null;
|
||||
|
||||
// Used to add content scripts and CSS required
|
||||
js = [
|
||||
"./js/vendor.js",
|
||||
"./js/content.js"
|
||||
];
|
||||
css = [
|
||||
"content.css",
|
||||
"./libs/Source+Sans+Pro.css",
|
||||
"popup.css"
|
||||
];
|
||||
|
||||
constructor(backgroundScriptContainer?: any) {
|
||||
this.backgroundScriptContainer = backgroundScriptContainer;
|
||||
}
|
||||
@@ -67,25 +77,15 @@ class Utils {
|
||||
* 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) {
|
||||
for (const file of this.js) {
|
||||
firefoxJS.push({file});
|
||||
}
|
||||
let firefoxCSS = [];
|
||||
for (const file of css) {
|
||||
for (const file of this.css) {
|
||||
firefoxCSS.push({file});
|
||||
}
|
||||
|
||||
@@ -119,8 +119,8 @@ class Utils {
|
||||
// This API is experimental and not visible by the TypeScript compiler
|
||||
actions: [new (<any> chrome.declarativeContent).RequestContentScript({
|
||||
allFrames: true,
|
||||
js,
|
||||
css
|
||||
js: self.js,
|
||||
css: self.css
|
||||
})]
|
||||
};
|
||||
|
||||
@@ -240,7 +240,7 @@ class Utils {
|
||||
sendRequestToServer(type: string, address: string, callback?: (xmlhttp: XMLHttpRequest, err: boolean) => any) {
|
||||
let xmlhttp = new XMLHttpRequest();
|
||||
|
||||
xmlhttp.open(type, CompileConfig.serverAddress + address, true);
|
||||
xmlhttp.open(type, Config.config.serverAddress + address, true);
|
||||
|
||||
if (callback != undefined) {
|
||||
xmlhttp.onreadystatechange = function () {
|
||||
|
||||
Reference in New Issue
Block a user