mirror of
https://github.com/ajayyy/SponsorBlock.git
synced 2025-12-19 05:58:37 +03:00
Merge pull request #181 from afrmtbl/add-invidious
Initial Invidious support
This commit is contained in:
3
SB.js
3
SB.js
@@ -159,7 +159,8 @@ SB.defaults = {
|
|||||||
"hideInfoButtonPlayerControls": false,
|
"hideInfoButtonPlayerControls": false,
|
||||||
"hideDeleteButtonPlayerControls": false,
|
"hideDeleteButtonPlayerControls": false,
|
||||||
"hideDiscordLaunches": 0,
|
"hideDiscordLaunches": 0,
|
||||||
"hideDiscordLink": false
|
"hideDiscordLink": false,
|
||||||
|
"invidiousInstances": ["invidio.us", "invidiou.sh"]
|
||||||
}
|
}
|
||||||
|
|
||||||
// Reset config
|
// Reset config
|
||||||
|
|||||||
@@ -344,5 +344,35 @@
|
|||||||
},
|
},
|
||||||
"keybindCurrentlySet": {
|
"keybindCurrentlySet": {
|
||||||
"message": ". It is currently set to:"
|
"message": ". It is currently set to:"
|
||||||
|
},
|
||||||
|
"supportInvidious": {
|
||||||
|
"message": "Support Invidious"
|
||||||
|
},
|
||||||
|
"supportInvidiousDescription": {
|
||||||
|
"message": "Invidious (invidio.us) is a third party YouTube client. To enable support, you must accept the extra permissions. This does NOT work in incongnito on chrome and other chromium variants."
|
||||||
|
},
|
||||||
|
"optionsInfo": {
|
||||||
|
"message": "Enable Invidious support, disable autoskip, hide buttons and more."
|
||||||
|
},
|
||||||
|
"addInvidiousInstance": {
|
||||||
|
"message": "Add Invidious Instance"
|
||||||
|
},
|
||||||
|
"addInvidiousInstanceDescription": {
|
||||||
|
"message": "Add a custom instance of Invidious. This must be formatted with JUST the domain. Example: invidious.ajay.app"
|
||||||
|
},
|
||||||
|
"add": {
|
||||||
|
"message": "Add"
|
||||||
|
},
|
||||||
|
"addInvidiousInstanceError": {
|
||||||
|
"message": "This is an invalid domain. This should JUST include the domain part. Example: invidious.ajay.app"
|
||||||
|
},
|
||||||
|
"resetInvidiousInstance": {
|
||||||
|
"message": "Reset Invidious Instance List"
|
||||||
|
},
|
||||||
|
"resetInvidiousInstanceAlert": {
|
||||||
|
"message": "You are about to reset the Invidious instance list"
|
||||||
|
},
|
||||||
|
"currentInstances": {
|
||||||
|
"message": "Current Instances:"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +1,15 @@
|
|||||||
|
isBackgroundScript = true;
|
||||||
|
|
||||||
|
// Used only on Firefox, which does not support non persistent background pages.
|
||||||
|
var contentScriptRegistrations = {};
|
||||||
|
|
||||||
|
// Register content script if needed
|
||||||
|
if (isFirefox()) {
|
||||||
|
wait(() => SB.config !== undefined).then(function() {
|
||||||
|
if (SB.config.supportInvidious) setupExtraSiteContentScripts();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
chrome.tabs.onUpdated.addListener(function(tabId) {
|
chrome.tabs.onUpdated.addListener(function(tabId) {
|
||||||
chrome.tabs.sendMessage(tabId, {
|
chrome.tabs.sendMessage(tabId, {
|
||||||
message: 'update',
|
message: 'update',
|
||||||
@@ -8,38 +20,46 @@ chrome.runtime.onMessage.addListener(async function (request, sender, callback)
|
|||||||
await wait(() => SB.config !== undefined);
|
await wait(() => SB.config !== undefined);
|
||||||
|
|
||||||
switch(request.message) {
|
switch(request.message) {
|
||||||
case "submitTimes":
|
case "submitTimes":
|
||||||
submitTimes(request.videoID, callback);
|
submitTimes(request.videoID, callback);
|
||||||
|
|
||||||
//this allows the callback to be called later by the submitTimes function
|
//this allows the callback to be called later by the submitTimes function
|
||||||
return true;
|
return true;
|
||||||
case "addSponsorTime":
|
case "addSponsorTime":
|
||||||
addSponsorTime(request.time, request.videoID, callback);
|
addSponsorTime(request.time, request.videoID, callback);
|
||||||
|
|
||||||
//this allows the callback to be called later
|
//this allows the callback to be called later
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
case "getSponsorTimes":
|
case "getSponsorTimes":
|
||||||
getSponsorTimes(request.videoID, function(sponsorTimes) {
|
getSponsorTimes(request.videoID, function(sponsorTimes) {
|
||||||
callback({
|
callback({
|
||||||
sponsorTimes: sponsorTimes
|
sponsorTimes: sponsorTimes
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
//this allows the callback to be called later
|
//this allows the callback to be called later
|
||||||
return true;
|
return true;
|
||||||
case "submitVote":
|
case "submitVote":
|
||||||
submitVote(request.type, request.UUID, callback);
|
submitVote(request.type, request.UUID, callback);
|
||||||
|
|
||||||
//this allows the callback to be called later
|
//this allows the callback to be called later
|
||||||
return true;
|
return true;
|
||||||
case "alertPrevious":
|
case "alertPrevious":
|
||||||
chrome.notifications.create("stillThere" + Math.random(), {
|
chrome.notifications.create("stillThere" + Math.random(), {
|
||||||
type: "basic",
|
type: "basic",
|
||||||
title: chrome.i18n.getMessage("wantToSubmit") + " " + request.previousVideoID + "?",
|
title: chrome.i18n.getMessage("wantToSubmit") + " " + request.previousVideoID + "?",
|
||||||
message: chrome.i18n.getMessage("leftTimes"),
|
message: chrome.i18n.getMessage("leftTimes"),
|
||||||
iconUrl: "./icons/LogoSponsorBlocker256px.png"
|
iconUrl: "./icons/LogoSponsorBlocker256px.png"
|
||||||
});
|
});
|
||||||
|
case "registerContentScript":
|
||||||
|
registerFirefoxContentScript(request);
|
||||||
|
return false;
|
||||||
|
case "unregisterContentScript":
|
||||||
|
contentScriptRegistrations[request.id].unregister();
|
||||||
|
delete contentScriptRegistrations[request.id];
|
||||||
|
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -61,6 +81,24 @@ chrome.runtime.onInstalled.addListener(function (object) {
|
|||||||
}, 1500);
|
}, 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(() => void (contentScriptRegistrations[options.id] = registration));
|
||||||
|
}
|
||||||
|
|
||||||
//gets the sponsor times from memory
|
//gets the sponsor times from memory
|
||||||
function getSponsorTimes(videoID, callback) {
|
function getSponsorTimes(videoID, callback) {
|
||||||
let sponsorTimes = [];
|
let sponsorTimes = [];
|
||||||
|
|||||||
173
content.js
173
content.js
@@ -69,78 +69,78 @@ var popupInitialised = false;
|
|||||||
chrome.runtime.onMessage.addListener(messageListener);
|
chrome.runtime.onMessage.addListener(messageListener);
|
||||||
|
|
||||||
function messageListener(request, sender, sendResponse) {
|
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(getYouTubeVideoID(document.URL));
|
||||||
break;
|
break;
|
||||||
case "sponsorStart":
|
case "sponsorStart":
|
||||||
sponsorMessageStarted(sendResponse);
|
sponsorMessageStarted(sendResponse);
|
||||||
|
|
||||||
break;
|
break;
|
||||||
case "sponsorDataChanged":
|
case "sponsorDataChanged":
|
||||||
updateSponsorTimesSubmitting();
|
updateSponsorTimesSubmitting();
|
||||||
|
|
||||||
break;
|
break;
|
||||||
case "isInfoFound":
|
case "isInfoFound":
|
||||||
//send the sponsor times along with if it's found
|
//send the sponsor times along with if it's found
|
||||||
sendResponse({
|
sendResponse({
|
||||||
found: sponsorDataFound,
|
found: sponsorDataFound,
|
||||||
sponsorTimes: sponsorTimes,
|
sponsorTimes: sponsorTimes,
|
||||||
hiddenSponsorTimes: hiddenSponsorTimes,
|
hiddenSponsorTimes: hiddenSponsorTimes,
|
||||||
UUIDs: UUIDs
|
UUIDs: UUIDs
|
||||||
});
|
});
|
||||||
|
|
||||||
if (popupInitialised && document.getElementById("sponsorBlockPopupContainer") != null) {
|
if (popupInitialised && document.getElementById("sponsorBlockPopupContainer") != null) {
|
||||||
//the popup should be closed now that another is opening
|
//the popup should be closed now that another is opening
|
||||||
closeInfoMenu();
|
closeInfoMenu();
|
||||||
}
|
}
|
||||||
|
|
||||||
popupInitialised = true;
|
popupInitialised = true;
|
||||||
break;
|
break;
|
||||||
case "getVideoID":
|
case "getVideoID":
|
||||||
sendResponse({
|
sendResponse({
|
||||||
videoID: sponsorVideoID
|
videoID: sponsorVideoID
|
||||||
});
|
});
|
||||||
|
|
||||||
break;
|
break;
|
||||||
case "getVideoDuration":
|
case "getVideoDuration":
|
||||||
sendResponse({
|
sendResponse({
|
||||||
duration: v.duration
|
duration: v.duration
|
||||||
});
|
});
|
||||||
|
|
||||||
break;
|
break;
|
||||||
case "skipToTime":
|
case "skipToTime":
|
||||||
v.currentTime = request.time;
|
v.currentTime = request.time;
|
||||||
return
|
return
|
||||||
case "getCurrentTime":
|
case "getCurrentTime":
|
||||||
sendResponse({
|
sendResponse({
|
||||||
currentTime: v.currentTime
|
currentTime: v.currentTime
|
||||||
});
|
});
|
||||||
|
|
||||||
break;
|
break;
|
||||||
case "getChannelURL":
|
case "getChannelURL":
|
||||||
sendResponse({
|
sendResponse({
|
||||||
channelURL: channelURL
|
channelURL: channelURL
|
||||||
});
|
});
|
||||||
|
|
||||||
break;
|
break;
|
||||||
case "isChannelWhitelisted":
|
case "isChannelWhitelisted":
|
||||||
sendResponse({
|
sendResponse({
|
||||||
value: channelWhitelisted
|
value: channelWhitelisted
|
||||||
});
|
});
|
||||||
|
|
||||||
break;
|
break;
|
||||||
case "whitelistChange":
|
case "whitelistChange":
|
||||||
channelWhitelisted = request.value;
|
channelWhitelisted = request.value;
|
||||||
sponsorsLookup(sponsorVideoID);
|
sponsorsLookup(sponsorVideoID);
|
||||||
|
|
||||||
break;
|
break;
|
||||||
case "changeStartSponsorButton":
|
case "changeStartSponsorButton":
|
||||||
changeStartSponsorButton(request.showStartSponsor, request.uploadButtonVisible);
|
changeStartSponsorButton(request.showStartSponsor, request.uploadButtonVisible);
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -148,7 +148,7 @@ function messageListener(request, sender, sendResponse) {
|
|||||||
*
|
*
|
||||||
* @param {String} changes
|
* @param {String} changes
|
||||||
*/
|
*/
|
||||||
function configUpdateListener(changes) {
|
function contentConfigUpdateListener(changes) {
|
||||||
for (const key in changes) {
|
for (const key in changes) {
|
||||||
switch(key) {
|
switch(key) {
|
||||||
case "hideVideoPlayerControls":
|
case "hideVideoPlayerControls":
|
||||||
@@ -160,8 +160,8 @@ function configUpdateListener(changes) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!SB.configListeners.includes(configUpdateListener)) {
|
if (!SB.configListeners.includes(contentConfigUpdateListener)) {
|
||||||
SB.configListeners.push(configUpdateListener);
|
SB.configListeners.push(contentConfigUpdateListener);
|
||||||
}
|
}
|
||||||
|
|
||||||
//check for hotkey pressed
|
//check for hotkey pressed
|
||||||
@@ -224,8 +224,22 @@ function videoIDChange(id) {
|
|||||||
if (previewBar == null) {
|
if (previewBar == null) {
|
||||||
//create it
|
//create it
|
||||||
wait(getControls).then(result => {
|
wait(getControls).then(result => {
|
||||||
let progressBar = document.getElementsByClassName("ytp-progress-bar-container")[0] || document.getElementsByClassName("no-model cue-range-markers")[0];
|
const progressElementSelectors = [
|
||||||
previewBar = new PreviewBar(progressBar);
|
// For YouTube
|
||||||
|
"ytp-progress-bar-container",
|
||||||
|
"no-model cue-range-markers",
|
||||||
|
// For Invidious/VideoJS
|
||||||
|
"vjs-progress-holder"
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const selector of progressElementSelectors) {
|
||||||
|
const el = document.getElementsByClassName(selector);
|
||||||
|
|
||||||
|
if (el && el.length && el[0]) {
|
||||||
|
previewBar = new PreviewBar(el[0]);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -284,8 +298,10 @@ function videoIDChange(id) {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
updateVisibilityOfPlayerControlsButton();
|
//see if video controls buttons should be added
|
||||||
updateVisibilityOfPlayerControlsButton(false);
|
if (!onInvidious) {
|
||||||
|
updateVisibilityOfPlayerControlsButton();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function sponsorsLookup(id, channelIDPromise) {
|
function sponsorsLookup(id, channelIDPromise) {
|
||||||
@@ -407,6 +423,9 @@ 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) {
|
||||||
|
// 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");
|
||||||
} else {
|
} else {
|
||||||
//old YouTube theme
|
//old YouTube theme
|
||||||
let channelContainers = document.getElementsByClassName("yt-user-info");
|
let channelContainers = document.getElementsByClassName("yt-user-info");
|
||||||
@@ -425,6 +444,9 @@ function getChannelID() {
|
|||||||
let currentTitle = "";
|
let currentTitle = "";
|
||||||
if (titleInfoContainer != null) {
|
if (titleInfoContainer != null) {
|
||||||
currentTitle = titleInfoContainer.firstElementChild.firstElementChild.querySelector(".title").firstElementChild.innerText;
|
currentTitle = titleInfoContainer.firstElementChild.firstElementChild.querySelector(".title").firstElementChild.innerText;
|
||||||
|
} else if (onInvidious) {
|
||||||
|
// 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;
|
||||||
} else {
|
} else {
|
||||||
//old YouTube theme
|
//old YouTube theme
|
||||||
currentTitle = document.getElementById("eow-title").innerText;
|
currentTitle = document.getElementById("eow-title").innerText;
|
||||||
@@ -595,7 +617,14 @@ function createButton(baseID, title, callback, imageName, isDraggable=false) {
|
|||||||
|
|
||||||
function getControls() {
|
function getControls() {
|
||||||
let controls = document.getElementsByClassName("ytp-right-controls");
|
let controls = document.getElementsByClassName("ytp-right-controls");
|
||||||
return (!controls || controls.length === 0) ? false : controls[controls.length - 1]
|
|
||||||
|
if (!controls || controls.length === 0) {
|
||||||
|
// The invidious video element's controls element
|
||||||
|
controls = document.getElementsByClassName("vjs-control-bar");
|
||||||
|
return (!controls || controls.length === 0) ? false : controls[controls.length - 1];
|
||||||
|
} else {
|
||||||
|
return controls[controls.length - 1];
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
//adds all the player controls buttons
|
//adds all the player controls buttons
|
||||||
@@ -618,7 +647,7 @@ async function updateVisibilityOfPlayerControlsButton() {
|
|||||||
|
|
||||||
await createButtons();
|
await createButtons();
|
||||||
|
|
||||||
if (SB.config.hideVideoPlayerControls) {
|
if (SB.config.hideVideoPlayerControls || 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 {
|
||||||
@@ -626,13 +655,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/")) {
|
if (SB.config.hideInfoButtonPlayerControls || document.URL.includes("/embed/") || 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) {
|
if (SB.config.hideDeleteButtonPlayerControls || onInvidious) {
|
||||||
document.getElementById("deleteButton").style.display = "none";
|
document.getElementById("deleteButton").style.display = "none";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
"all_frames": true,
|
"all_frames": true,
|
||||||
"js": [
|
"js": [
|
||||||
"config.js",
|
"config.js",
|
||||||
"SB.js",
|
"SB.js",
|
||||||
"utils/previewBar.js",
|
"utils/previewBar.js",
|
||||||
"utils/skipNotice.js",
|
"utils/skipNotice.js",
|
||||||
"utils.js",
|
"utils.js",
|
||||||
@@ -48,15 +48,19 @@
|
|||||||
"notifications",
|
"notifications",
|
||||||
"https://sponsor.ajay.app/*"
|
"https://sponsor.ajay.app/*"
|
||||||
],
|
],
|
||||||
|
"optional_permissions": [
|
||||||
|
"*://*/*",
|
||||||
|
"declarativeContent"
|
||||||
|
],
|
||||||
"browser_action": {
|
"browser_action": {
|
||||||
"default_title": "__MSG_Name__",
|
"default_title": "__MSG_Name__",
|
||||||
"default_popup": "popup.html"
|
"default_popup": "popup.html"
|
||||||
},
|
},
|
||||||
"background": {
|
"background": {
|
||||||
"scripts":[
|
"scripts":[
|
||||||
|
"config.js",
|
||||||
"SB.js",
|
"SB.js",
|
||||||
"utils.js",
|
"utils.js",
|
||||||
"config.js",
|
|
||||||
"background.js"
|
"background.js"
|
||||||
],
|
],
|
||||||
"persistent": false
|
"persistent": false
|
||||||
@@ -68,6 +72,9 @@
|
|||||||
"128": "icons/LogoSponsorBlocker128px.png",
|
"128": "icons/LogoSponsorBlocker128px.png",
|
||||||
"256": "icons/LogoSponsorBlocker256px.png"
|
"256": "icons/LogoSponsorBlocker256px.png"
|
||||||
},
|
},
|
||||||
"options_page": "options/options.html",
|
"options_ui": {
|
||||||
|
"page": "options/options.html",
|
||||||
|
"open_in_tab": true
|
||||||
|
},
|
||||||
"manifest_version": 2
|
"manifest_version": 2
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,12 +7,16 @@ body {
|
|||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.inline {
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
.bold {
|
.bold {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
}
|
}
|
||||||
|
|
||||||
.hidden {
|
.hidden {
|
||||||
display: none;
|
display: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.keybind-status {
|
.keybind-status {
|
||||||
@@ -42,7 +46,7 @@ body {
|
|||||||
border-radius: 5px;
|
border-radius: 5px;
|
||||||
font-size: 14px;
|
font-size: 14px;
|
||||||
|
|
||||||
width: fit-content;
|
width: max-content;
|
||||||
}
|
}
|
||||||
|
|
||||||
.option-button:hover {
|
.option-button:hover {
|
||||||
|
|||||||
@@ -21,7 +21,59 @@
|
|||||||
<h1>__MSG_Options__</h1>
|
<h1>__MSG_Options__</h1>
|
||||||
|
|
||||||
<div id="options" class="hidden">
|
<div id="options" class="hidden">
|
||||||
|
|
||||||
|
<div id="support-invidious" option-type="toggle" sync-option="supportInvidious">
|
||||||
|
<label class="switch-container" label-name="__MSG_supportInvidious__">
|
||||||
|
<label class="switch">
|
||||||
|
<input type="checkbox">
|
||||||
|
<span class="slider round"></span>
|
||||||
|
</label>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<br/>
|
||||||
|
<br/>
|
||||||
|
|
||||||
|
<div class="small-description">__MSG_supportInvidiousDescription__</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<br/>
|
||||||
|
<br/>
|
||||||
|
|
||||||
|
<div option-type="text-change" sync-option="invidiousInstances">
|
||||||
|
<div class="option-button trigger-button">
|
||||||
|
__MSG_addInvidiousInstance__
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<br/>
|
||||||
|
|
||||||
|
<div class="small-description">__MSG_addInvidiousInstanceDescription__</div>
|
||||||
|
|
||||||
|
<div class="option-hidden-section hidden">
|
||||||
|
<br/>
|
||||||
|
|
||||||
|
<input class="option-text-box" type="text">
|
||||||
|
|
||||||
|
<br/>
|
||||||
|
<br/>
|
||||||
|
|
||||||
|
<div class="option-button text-change-set inline">
|
||||||
|
__MSG_add__
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="option-button invidious-instance-reset inline">
|
||||||
|
__MSG_resetInvidiousInstance__
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<br/>
|
||||||
|
<br/>
|
||||||
|
|
||||||
|
<span class="small-description">__MSG_currentInstances__</span>
|
||||||
|
<span class="small-description" option-type="display" sync-option="invidiousInstances"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<br/>
|
||||||
|
<br/>
|
||||||
|
|
||||||
<div option-type="toggle" toggle-type="reverse" sync-option="disableAutoSkip">
|
<div option-type="toggle" toggle-type="reverse" sync-option="disableAutoSkip">
|
||||||
<label class="switch-container" label-name="__MSG_autoSkip__">
|
<label class="switch-container" label-name="__MSG_autoSkip__">
|
||||||
|
|||||||
@@ -3,17 +3,20 @@ window.addEventListener('DOMContentLoaded', init);
|
|||||||
async function init() {
|
async function init() {
|
||||||
localizeHtmlPage();
|
localizeHtmlPage();
|
||||||
|
|
||||||
|
if (!SB.configListeners.includes(optionsConfigUpdateListener)) {
|
||||||
|
SB.configListeners.push(optionsConfigUpdateListener);
|
||||||
|
}
|
||||||
|
|
||||||
await wait(() => SB.config !== undefined);
|
await wait(() => SB.config !== undefined);
|
||||||
|
|
||||||
// Set all of the toggle options to the correct option
|
// Set all of the toggle options to the correct option
|
||||||
let optionsContainer = document.getElementById("options");
|
let optionsContainer = document.getElementById("options");
|
||||||
let optionsElements = optionsContainer.children;
|
let optionsElements = optionsContainer.querySelectorAll("*");
|
||||||
|
|
||||||
for (let i = 0; i < optionsElements.length; i++) {
|
for (let i = 0; i < optionsElements.length; i++) {
|
||||||
switch (optionsElements[i].getAttribute("option-type")) {
|
switch (optionsElements[i].getAttribute("option-type")) {
|
||||||
case "toggle":
|
case "toggle":
|
||||||
let option = optionsElements[i].getAttribute("sync-option");
|
let option = optionsElements[i].getAttribute("sync-option");
|
||||||
|
|
||||||
let optionResult = SB.config[option];
|
let optionResult = SB.config[option];
|
||||||
|
|
||||||
let checkbox = optionsElements[i].querySelector("input");
|
let checkbox = optionsElements[i].querySelector("input");
|
||||||
@@ -27,20 +30,44 @@ async function init() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
checkbox.addEventListener("click", () =>{
|
// See if anything extra should be run first time
|
||||||
|
switch (option) {
|
||||||
|
case "supportInvidious":
|
||||||
|
invidiousInit(checkbox, option);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add click listener
|
||||||
|
checkbox.addEventListener("click", () => {
|
||||||
SB.config[option] = reverse ? !checkbox.checked : checkbox.checked;
|
SB.config[option] = reverse ? !checkbox.checked : checkbox.checked;
|
||||||
|
|
||||||
|
// See if anything extra must be run
|
||||||
|
switch (option) {
|
||||||
|
case "supportInvidious":
|
||||||
|
invidiousOnClick(checkbox, option);
|
||||||
|
break;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
case "text-change":
|
case "text-change":
|
||||||
let button = optionsElements[i].querySelector(".trigger-button");
|
let button = optionsElements[i].querySelector(".trigger-button");
|
||||||
button.addEventListener("click", () => activateTextChange(optionsElements[i]));
|
button.addEventListener("click", () => activateTextChange(optionsElements[i]));
|
||||||
|
|
||||||
|
let textChangeOption = optionsElements[i].getAttribute("sync-option");
|
||||||
|
// See if anything extra must be done
|
||||||
|
switch (textChangeOption) {
|
||||||
|
case "invidiousInstances":
|
||||||
|
invidiousInstanceAddInit(optionsElements[i], textChangeOption);
|
||||||
|
}
|
||||||
|
|
||||||
break;
|
break;
|
||||||
case "keybind-change":
|
case "keybind-change":
|
||||||
let keybindButton = optionsElements[i].querySelector(".trigger-button");
|
let keybindButton = optionsElements[i].querySelector(".trigger-button");
|
||||||
keybindButton.addEventListener("click", () => activateKeybindChange(optionsElements[i]));
|
keybindButton.addEventListener("click", () => activateKeybindChange(optionsElements[i]));
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
case "display":
|
||||||
|
updateDisplayElement(optionsElements[i])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,6 +75,127 @@ async function init() {
|
|||||||
optionsContainer.classList.add("animated");
|
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(optionsElements[i])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Will set display elements to the proper text
|
||||||
|
*
|
||||||
|
* @param {HTMLElement} element
|
||||||
|
*/
|
||||||
|
function updateDisplayElement(element) {
|
||||||
|
let displayOption = element.getAttribute("sync-option")
|
||||||
|
let displayText = SB.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 {HTMLElement} element
|
||||||
|
* @param {String} option
|
||||||
|
*/
|
||||||
|
function invidiousInstanceAddInit(element, option) {
|
||||||
|
let textBox = 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 = SB.config[option];
|
||||||
|
if (!instanceList) instanceList = [];
|
||||||
|
|
||||||
|
instanceList.push(textBox.value);
|
||||||
|
|
||||||
|
SB.config[option] = instanceList;
|
||||||
|
|
||||||
|
let checkbox = 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
|
||||||
|
SB.config[option] = SB.defaults[option].slice(0);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run when the invidious button is being initialized
|
||||||
|
*
|
||||||
|
* @param {HTMLElement} checkbox
|
||||||
|
* @param {string} option
|
||||||
|
*/
|
||||||
|
function invidiousInit(checkbox, option) {
|
||||||
|
let permissions = ["declarativeContent"];
|
||||||
|
if (isFirefox()) permissions = [];
|
||||||
|
|
||||||
|
chrome.permissions.contains({
|
||||||
|
origins: getInvidiousInstancesRegex(),
|
||||||
|
permissions: permissions
|
||||||
|
}, function (result) {
|
||||||
|
if (result != checkbox.checked) {
|
||||||
|
SB.config[option] = result;
|
||||||
|
|
||||||
|
checkbox.checked = result;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Run whenever the invidious checkbox is clicked
|
||||||
|
*
|
||||||
|
* @param {HTMLElement} checkbox
|
||||||
|
* @param {string} option
|
||||||
|
*/
|
||||||
|
function invidiousOnClick(checkbox, option) {
|
||||||
|
if (checkbox.checked) {
|
||||||
|
setupExtraSitePermissions(function (granted) {
|
||||||
|
if (!granted) {
|
||||||
|
SB.config[option] = false;
|
||||||
|
checkbox.checked = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
removeExtraSiteRegistration();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Will trigger the container to ask the user for a keybind.
|
* Will trigger the container to ask the user for a keybind.
|
||||||
*
|
*
|
||||||
@@ -114,9 +262,16 @@ function activateTextChange(element) {
|
|||||||
|
|
||||||
let textBox = element.querySelector(".option-text-box");
|
let textBox = element.querySelector(".option-text-box");
|
||||||
let option = element.getAttribute("sync-option");
|
let option = element.getAttribute("sync-option");
|
||||||
|
|
||||||
textBox.value = SB.config[option];
|
|
||||||
|
|
||||||
|
// See if anything extra must be done
|
||||||
|
switch (option) {
|
||||||
|
case "invidiousInstances":
|
||||||
|
element.querySelector(".option-hidden-section").classList.remove("hidden");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
textBox.value = SB.config[option];
|
||||||
|
|
||||||
let setButton = element.querySelector(".text-change-set");
|
let setButton = element.querySelector(".text-change-set");
|
||||||
setButton.addEventListener("click", () => {
|
setButton.addEventListener("click", () => {
|
||||||
let confirmMessage = element.getAttribute("confirm-message");
|
let confirmMessage = element.getAttribute("confirm-message");
|
||||||
@@ -127,4 +282,4 @@ function activateTextChange(element) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
element.querySelector(".option-hidden-section").classList.remove("hidden");
|
element.querySelector(".option-hidden-section").classList.remove("hidden");
|
||||||
}
|
}
|
||||||
@@ -196,6 +196,11 @@
|
|||||||
<button id="optionsButton" class="dangerButton popupElement">__MSG_Options__</button>
|
<button id="optionsButton" class="dangerButton popupElement">__MSG_Options__</button>
|
||||||
|
|
||||||
<br/>
|
<br/>
|
||||||
|
|
||||||
|
<sub class="popupElement">
|
||||||
|
__MSG_optionsInfo__
|
||||||
|
</sub>
|
||||||
|
|
||||||
<br/>
|
<br/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
1
popup.js
1
popup.js
@@ -231,7 +231,6 @@ async function runThePopup() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
//load video times for this video
|
//load video times for this video
|
||||||
console.log( SB.config.sponsorTimes.set)
|
|
||||||
setTimeout(()=> console.log( SB.config.sponsorTimes.set), 200 )
|
setTimeout(()=> console.log( SB.config.sponsorTimes.set), 200 )
|
||||||
let sponsorTimesStorage = SB.config.sponsorTimes.get(currentVideoID);
|
let sponsorTimesStorage = SB.config.sponsorTimes.get(currentVideoID);
|
||||||
if (sponsorTimesStorage != undefined && sponsorTimesStorage.length > 0) {
|
if (sponsorTimesStorage != undefined && sponsorTimesStorage.length > 0) {
|
||||||
|
|||||||
162
utils.js
162
utils.js
@@ -1,3 +1,6 @@
|
|||||||
|
var isBackgroundScript = false;
|
||||||
|
var onInvidious = false;
|
||||||
|
|
||||||
// Function that can be used to wait for a condition before returning
|
// Function that can be used to wait for a condition before returning
|
||||||
async function wait(condition, timeout = 5000, check = 100) {
|
async function wait(condition, timeout = 5000, check = 100) {
|
||||||
return await new Promise((resolve, reject) => {
|
return await new Promise((resolve, reject) => {
|
||||||
@@ -20,7 +23,7 @@ async function wait(condition, timeout = 5000, check = 100) {
|
|||||||
|
|
||||||
function getYouTubeVideoID(url) {
|
function getYouTubeVideoID(url) {
|
||||||
// For YouTube TV support
|
// For YouTube TV support
|
||||||
if(document.URL.startsWith("https://www.youtube.com/tv#/")) url = url.replace("#", "");
|
if(url.startsWith("https://www.youtube.com/tv#/")) url = url.replace("#", "");
|
||||||
|
|
||||||
//Attempt to parse url
|
//Attempt to parse url
|
||||||
let urlObject = null;
|
let urlObject = null;
|
||||||
@@ -32,7 +35,16 @@ function getYouTubeVideoID(url) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
//Check if valid hostname
|
//Check if valid hostname
|
||||||
if(!["www.youtube.com","www.youtube-nocookie.com"].includes(urlObject.host)) return false;
|
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
|
//Get ID from searchParam
|
||||||
if (urlObject.searchParams.has("v") && ["/watch", "/watch/"].includes(urlObject.pathname) || urlObject.pathname.startsWith("/tv/watch")) {
|
if (urlObject.searchParams.has("v") && ["/watch", "/watch/"].includes(urlObject.pathname) || urlObject.pathname.startsWith("/tv/watch")) {
|
||||||
@@ -49,6 +61,132 @@ function getYouTubeVideoID(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() {
|
function localizeHtmlPage() {
|
||||||
//Localize by replacing __MSG_***__ meta tags
|
//Localize by replacing __MSG_***__ meta tags
|
||||||
var objects = document.getElementsByClassName("sponsorBlockPageBody")[0].children;
|
var objects = document.getElementsByClassName("sponsorBlockPageBody")[0].children;
|
||||||
@@ -72,6 +210,19 @@ function getLocalizedMessage(text) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @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) {
|
function generateUserID(length = 36) {
|
||||||
let charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
let charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||||
let result = "";
|
let result = "";
|
||||||
@@ -111,3 +262,10 @@ function getErrorMessage(statusCode) {
|
|||||||
|
|
||||||
return errorMessage;
|
return errorMessage;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Is this Firefox (web-extensions)
|
||||||
|
*/
|
||||||
|
function isFirefox() {
|
||||||
|
return typeof(browser) !== "undefined";
|
||||||
|
}
|
||||||
|
|||||||
@@ -166,7 +166,7 @@ class SkipNotice {
|
|||||||
noticeElement.appendChild(secondRow);
|
noticeElement.appendChild(secondRow);
|
||||||
|
|
||||||
//get reference node
|
//get reference node
|
||||||
let referenceNode = document.getElementById("movie_player");
|
let referenceNode = document.getElementById("movie_player") || document.querySelector("#player-container .video-js");
|
||||||
if (referenceNode == null) {
|
if (referenceNode == null) {
|
||||||
//for embeds
|
//for embeds
|
||||||
let player = document.getElementById("player");
|
let player = document.getElementById("player");
|
||||||
@@ -425,4 +425,4 @@ class SkipNotice {
|
|||||||
if (this.countdownInterval != -1) clearInterval(this.countdownInterval);
|
if (this.countdownInterval != -1) clearInterval(this.countdownInterval);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user