mirror of
https://github.com/ajayyy/SponsorBlock.git
synced 2025-12-07 12:07:11 +03:00
1133 lines
44 KiB
TypeScript
1133 lines
44 KiB
TypeScript
import Config from "./config";
|
|
|
|
import Utils from "./utils";
|
|
import {
|
|
ActionType,
|
|
SegmentUUID,
|
|
SponsorHideType,
|
|
SponsorSourceType,
|
|
SponsorTime,
|
|
StorageChangesObject,
|
|
} from "./types";
|
|
import {
|
|
GetChannelIDResponse,
|
|
IsChannelWhitelistedResponse,
|
|
IsInfoFoundMessageResponse,
|
|
Message,
|
|
MessageResponse,
|
|
PopupMessage,
|
|
SponsorStartResponse,
|
|
VoteResponse,
|
|
} from "./messageTypes";
|
|
import { showDonationLink } from "./utils/configUtils";
|
|
import { AnimationUtils } from "./utils/animationUtils";
|
|
import { GenericUtils } from "./utils/genericUtils";
|
|
import { shortCategoryName } from "./utils/categoryUtils";
|
|
import { localizeHtmlPage } from "./utils/pageUtils";
|
|
import { exportTimes } from "./utils/exporter";
|
|
import GenericNotice from "./render/GenericNotice";
|
|
import { noRefreshFetchingChaptersAllowed } from "./utils/licenseKey";
|
|
import { getFormattedTime } from "@ajayyy/maze-utils/lib/formating";
|
|
|
|
const utils = new Utils();
|
|
|
|
interface MessageListener {
|
|
(request: Message, sender: unknown, sendResponse: (response: MessageResponse) => void): void;
|
|
}
|
|
|
|
class MessageHandler {
|
|
messageListener: MessageListener;
|
|
|
|
constructor(messageListener?: MessageListener) {
|
|
this.messageListener = messageListener;
|
|
}
|
|
|
|
sendMessage(id: number, request: Message, callback?) {
|
|
if (this.messageListener) {
|
|
this.messageListener(request, null, callback);
|
|
} else if (chrome.tabs) {
|
|
chrome.tabs.sendMessage(id, request, callback);
|
|
} else {
|
|
chrome.runtime.sendMessage({ message: "tabs", data: request }, callback);
|
|
}
|
|
}
|
|
|
|
query(config, callback) {
|
|
if (this.messageListener || !chrome.tabs) {
|
|
// Send back dummy info
|
|
callback([{
|
|
url: document.URL,
|
|
id: -1
|
|
}]);
|
|
} else {
|
|
chrome.tabs.query(config, callback);
|
|
}
|
|
|
|
}
|
|
}
|
|
|
|
// To prevent clickjacking
|
|
let allowPopup = window === window.top;
|
|
window.addEventListener("message", async (e): Promise<void> => {
|
|
if (e.source !== window.parent) return;
|
|
if (e.origin.endsWith('.youtube.com')) {
|
|
allowPopup = true;
|
|
}
|
|
});
|
|
|
|
//make this a function to allow this to run on the content page
|
|
async function runThePopup(messageListener?: MessageListener): Promise<void> {
|
|
const messageHandler = new MessageHandler(messageListener);
|
|
localizeHtmlPage();
|
|
|
|
type InputPageElements = {
|
|
whitelistToggle?: HTMLInputElement;
|
|
toggleSwitch?: HTMLInputElement;
|
|
usernameInput?: HTMLInputElement;
|
|
};
|
|
type PageElements = { [key: string]: HTMLElement } & InputPageElements
|
|
|
|
let stopLoadingAnimation = null;
|
|
|
|
//the start and end time pairs (2d)
|
|
let sponsorTimes: SponsorTime[] = [];
|
|
let downloadedTimes: SponsorTime[] = [];
|
|
|
|
//current video ID of this tab
|
|
let currentVideoID = null;
|
|
|
|
enum SegmentTab {
|
|
Segments,
|
|
Chapters
|
|
}
|
|
let segmentTab = SegmentTab.Segments;
|
|
let port: chrome.runtime.Port = null;
|
|
|
|
//saves which detail elemts are opened, by saving the uuids
|
|
const openedUUIDs: SegmentUUID[] = [];
|
|
|
|
const PageElements: PageElements = {};
|
|
|
|
[
|
|
"sponsorBlockPopupBody",
|
|
"sponsorblockPopup",
|
|
"sponsorStart",
|
|
// Top toggles
|
|
"whitelistChannel",
|
|
"unwhitelistChannel",
|
|
"whitelistToggle",
|
|
"whitelistForceCheck",
|
|
"disableSkipping",
|
|
"enableSkipping",
|
|
"toggleSwitch",
|
|
// Options
|
|
"showNoticeAgain",
|
|
"optionsButton",
|
|
"helpButton",
|
|
// More controls
|
|
"submitTimes",
|
|
"sponsorTimesContributionsContainer",
|
|
"sponsorTimesContributionsDisplay",
|
|
"sponsorTimesViewsContainer",
|
|
"sponsorTimesViewsDisplay",
|
|
"sponsorTimesViewsDisplayEndWord",
|
|
"sponsorTimesOthersTimeSavedDisplay",
|
|
"sponsorTimesOthersTimeSavedEndWord",
|
|
"sponsorTimesSkipsDoneContainer",
|
|
"sponsorTimesSkipsDoneDisplay",
|
|
"sponsorTimesSkipsDoneEndWord",
|
|
"sponsorTimeSavedDisplay",
|
|
"sponsorTimeSavedEndWord",
|
|
// Username
|
|
"setUsernameContainer",
|
|
"setUsernameButton",
|
|
"setUsernameStatus",
|
|
"setUsernameStatus",
|
|
"setUsername",
|
|
"usernameInput",
|
|
"usernameValue",
|
|
"submitUsername",
|
|
"sbPopupIconCopyUserID",
|
|
// More
|
|
"submissionHint",
|
|
"mainControls",
|
|
"loadingIndicator",
|
|
"videoFound",
|
|
"sponsorMessageTimes",
|
|
//"downloadedSponsorMessageTimes",
|
|
"refreshSegmentsButton",
|
|
"whitelistButton",
|
|
"sbDonate",
|
|
"issueReporterTabs",
|
|
"issueReporterTabSegments",
|
|
"issueReporterTabChapters",
|
|
"sponsorTimesDonateContainer",
|
|
"sbConsiderDonateLink",
|
|
"sbCloseDonate",
|
|
"sbBetaServerWarning",
|
|
"sbCloseButton",
|
|
"issueReporterImportExport",
|
|
"importSegmentsButton",
|
|
"exportSegmentsButton",
|
|
"importSegmentsMenu",
|
|
"importSegmentsText",
|
|
"importSegmentsSubmit"
|
|
|
|
].forEach(id => PageElements[id] = document.getElementById(id));
|
|
|
|
getSegmentsFromContentScript(false);
|
|
await utils.wait(() => Config.config !== null && allowPopup, 5000, 5);
|
|
PageElements.sponsorBlockPopupBody.style.removeProperty("visibility");
|
|
if (!Config.configSyncListeners.includes(contentConfigUpdateListener)) {
|
|
Config.configSyncListeners.push(contentConfigUpdateListener);
|
|
}
|
|
|
|
PageElements.sbCloseButton.addEventListener("click", () => {
|
|
sendTabMessage({
|
|
message: "closePopup"
|
|
});
|
|
});
|
|
|
|
if (window !== window.top) {
|
|
PageElements.sbCloseButton.classList.remove("hidden");
|
|
PageElements.sponsorBlockPopupBody.classList.add("is-embedded");
|
|
}
|
|
|
|
// Hide donate button if wanted (Safari, or user choice)
|
|
if (!showDonationLink()) {
|
|
PageElements.sbDonate.style.display = "none";
|
|
}
|
|
PageElements.sbDonate.addEventListener("click", () => Config.config.donateClicked = Config.config.donateClicked + 1);
|
|
|
|
if (Config.config.testingServer) {
|
|
PageElements.sbBetaServerWarning.classList.remove("hidden");
|
|
PageElements.sbBetaServerWarning.addEventListener("click", function () {
|
|
openOptionsAt("advanced");
|
|
});
|
|
}
|
|
|
|
PageElements.exportSegmentsButton.addEventListener("click", exportSegments);
|
|
PageElements.importSegmentsButton.addEventListener("click",
|
|
() => PageElements.importSegmentsMenu.classList.toggle("hidden"));
|
|
PageElements.importSegmentsSubmit.addEventListener("click", importSegments);
|
|
|
|
PageElements.sponsorStart.addEventListener("click", sendSponsorStartMessage);
|
|
PageElements.whitelistToggle.addEventListener("change", function () {
|
|
if (this.checked) {
|
|
whitelistChannel();
|
|
} else {
|
|
unwhitelistChannel();
|
|
}
|
|
});
|
|
PageElements.whitelistForceCheck.addEventListener("click", () => {openOptionsAt("behavior")});
|
|
PageElements.toggleSwitch.addEventListener("change", function () {
|
|
toggleSkipping(!this.checked);
|
|
});
|
|
PageElements.submitTimes.addEventListener("click", submitTimes);
|
|
PageElements.showNoticeAgain.addEventListener("click", showNoticeAgain);
|
|
PageElements.setUsernameButton.addEventListener("click", setUsernameButton);
|
|
PageElements.usernameValue.addEventListener("click", setUsernameButton);
|
|
PageElements.submitUsername.addEventListener("click", submitUsername);
|
|
PageElements.optionsButton.addEventListener("click", openOptions);
|
|
PageElements.helpButton.addEventListener("click", openHelp);
|
|
PageElements.refreshSegmentsButton.addEventListener("click", refreshSegments);
|
|
PageElements.sbPopupIconCopyUserID.addEventListener("click", async () => copyToClipboard(await utils.getHash(Config.config.userID)));
|
|
|
|
// Forward click events
|
|
if (window !== window.top) {
|
|
document.addEventListener("keydown", (e) => {
|
|
const target = e.target as HTMLElement;
|
|
if (target.tagName === "INPUT"
|
|
|| target.tagName === "TEXTAREA"
|
|
|| e.key === "ArrowUp"
|
|
|| e.key === "ArrowDown") {
|
|
return;
|
|
}
|
|
|
|
if (e.key === " ") {
|
|
// No scrolling
|
|
e.preventDefault();
|
|
}
|
|
|
|
sendTabMessage({
|
|
message: "keydown",
|
|
key: e.key,
|
|
keyCode: e.keyCode,
|
|
code: e.code,
|
|
which: e.which,
|
|
shiftKey: e.shiftKey,
|
|
ctrlKey: e.ctrlKey,
|
|
altKey: e.altKey,
|
|
metaKey: e.metaKey
|
|
});
|
|
});
|
|
}
|
|
|
|
setupComPort();
|
|
|
|
//show proper disable skipping button
|
|
const disableSkipping = Config.config.disableSkipping;
|
|
if (disableSkipping != undefined && disableSkipping) {
|
|
PageElements.disableSkipping.style.display = "none";
|
|
PageElements.enableSkipping.style.display = "unset";
|
|
PageElements.toggleSwitch.checked = false;
|
|
}
|
|
|
|
//if the don't show notice again variable is true, an option to
|
|
// disable should be available
|
|
const dontShowNotice = Config.config.dontShowNotice;
|
|
if (dontShowNotice != undefined && dontShowNotice) {
|
|
PageElements.showNoticeAgain.style.display = "unset";
|
|
}
|
|
|
|
const values = ["userName", "viewCount", "minutesSaved", "vip", "permissions"];
|
|
if (!Config.config.payments.freeAccess && !noRefreshFetchingChaptersAllowed()) values.push("freeChaptersAccess");
|
|
|
|
utils.asyncRequestToServer("GET", "/api/userInfo", {
|
|
publicUserID: await utils.getHash(Config.config.userID),
|
|
values
|
|
}).then((res) => {
|
|
if (res.status === 200) {
|
|
const userInfo = JSON.parse(res.responseText);
|
|
PageElements.usernameValue.innerText = userInfo.userName;
|
|
|
|
const viewCount = userInfo.viewCount;
|
|
if (viewCount != 0) {
|
|
if (viewCount > 1) {
|
|
PageElements.sponsorTimesViewsDisplayEndWord.innerText = chrome.i18n.getMessage("Segments");
|
|
} else {
|
|
PageElements.sponsorTimesViewsDisplayEndWord.innerText = chrome.i18n.getMessage("Segment");
|
|
}
|
|
PageElements.sponsorTimesViewsDisplay.innerText = viewCount.toLocaleString();
|
|
PageElements.sponsorTimesViewsContainer.style.display = "block";
|
|
}
|
|
|
|
showDonateWidget(viewCount);
|
|
|
|
const minutesSaved = userInfo.minutesSaved;
|
|
if (minutesSaved != 0) {
|
|
if (minutesSaved != 1) {
|
|
PageElements.sponsorTimesOthersTimeSavedEndWord.innerText = chrome.i18n.getMessage("minsLower");
|
|
} else {
|
|
PageElements.sponsorTimesOthersTimeSavedEndWord.innerText = chrome.i18n.getMessage("minLower");
|
|
}
|
|
PageElements.sponsorTimesOthersTimeSavedDisplay.innerText = getFormattedHours(minutesSaved);
|
|
}
|
|
|
|
Config.config.isVip = userInfo.vip;
|
|
Config.config.permissions = userInfo.permissions;
|
|
|
|
if (userInfo.freeChaptersAccess) {
|
|
Config.config.payments.chaptersAllowed = userInfo.freeChaptersAccess;
|
|
Config.config.payments.freeAccess = userInfo.freeChaptersAccess;
|
|
Config.config.payments.lastCheck = Date.now();
|
|
Config.forceSyncUpdate("payments");
|
|
}
|
|
}
|
|
});
|
|
|
|
//get the amount of times this user has contributed and display it to thank them
|
|
if (Config.config.sponsorTimesContributed != undefined) {
|
|
PageElements.sponsorTimesContributionsDisplay.innerText = Config.config.sponsorTimesContributed.toLocaleString();
|
|
PageElements.sponsorTimesContributionsContainer.classList.remove("hidden");
|
|
}
|
|
|
|
//get the amount of times this user has skipped a sponsor
|
|
if (Config.config.skipCount != undefined) {
|
|
if (Config.config.skipCount != 1) {
|
|
PageElements.sponsorTimesSkipsDoneEndWord.innerText = chrome.i18n.getMessage("Segments");
|
|
} else {
|
|
PageElements.sponsorTimesSkipsDoneEndWord.innerText = chrome.i18n.getMessage("Segment");
|
|
}
|
|
|
|
PageElements.sponsorTimesSkipsDoneDisplay.innerText = Config.config.skipCount.toLocaleString();
|
|
PageElements.sponsorTimesSkipsDoneContainer.style.display = "block";
|
|
}
|
|
|
|
//get the amount of time this user has saved.
|
|
if (Config.config.minutesSaved != undefined) {
|
|
if (Config.config.minutesSaved != 1) {
|
|
PageElements.sponsorTimeSavedEndWord.innerText = chrome.i18n.getMessage("minsLower");
|
|
} else {
|
|
PageElements.sponsorTimeSavedEndWord.innerText = chrome.i18n.getMessage("minLower");
|
|
}
|
|
|
|
PageElements.sponsorTimeSavedDisplay.innerText = getFormattedHours(Config.config.minutesSaved);
|
|
}
|
|
|
|
// Must be delayed so it only happens once loaded
|
|
setTimeout(() => PageElements.sponsorblockPopup.classList.remove("preload"), 250);
|
|
|
|
PageElements.issueReporterTabSegments.addEventListener("click", () => {
|
|
PageElements.issueReporterTabSegments.classList.add("sbSelected");
|
|
PageElements.issueReporterTabChapters.classList.remove("sbSelected");
|
|
|
|
segmentTab = SegmentTab.Segments;
|
|
getSegmentsFromContentScript(true);
|
|
});
|
|
|
|
PageElements.issueReporterTabChapters.addEventListener("click", () => {
|
|
PageElements.issueReporterTabSegments.classList.remove("sbSelected");
|
|
PageElements.issueReporterTabChapters.classList.add("sbSelected");
|
|
|
|
segmentTab = SegmentTab.Chapters;
|
|
getSegmentsFromContentScript(true);
|
|
});
|
|
|
|
function showDonateWidget(viewCount: number) {
|
|
if (Config.config.showDonationLink && Config.config.donateClicked <= 0 && Config.config.showPopupDonationCount < 5
|
|
&& viewCount < 50000 && !Config.config.isVip && Config.config.skipCount > 10) {
|
|
PageElements.sponsorTimesDonateContainer.style.display = "flex";
|
|
PageElements.sbConsiderDonateLink.addEventListener("click", () => {
|
|
Config.config.donateClicked = Config.config.donateClicked + 1;
|
|
});
|
|
|
|
PageElements.sbCloseDonate.addEventListener("click", () => {
|
|
PageElements.sponsorTimesDonateContainer.style.display = "none";
|
|
Config.config.showPopupDonationCount = 100;
|
|
});
|
|
|
|
Config.config.showPopupDonationCount = Config.config.showPopupDonationCount + 1;
|
|
}
|
|
}
|
|
|
|
function onTabs(tabs, updating: boolean): void {
|
|
messageHandler.sendMessage(tabs[0].id, { message: 'getVideoID' }, function (result) {
|
|
if (result !== undefined && result.videoID) {
|
|
currentVideoID = result.videoID;
|
|
|
|
loadTabData(tabs, updating);
|
|
} else if (result === undefined && chrome.runtime.lastError) {
|
|
//this isn't a YouTube video then, or at least the content script is not loaded
|
|
displayNoVideo();
|
|
}
|
|
});
|
|
}
|
|
|
|
async function loadTabData(tabs, updating: boolean): Promise<void> {
|
|
if (!currentVideoID) {
|
|
//this isn't a YouTube video then
|
|
displayNoVideo();
|
|
return;
|
|
}
|
|
|
|
await utils.wait(() => Config.config !== null, 5000, 10);
|
|
sponsorTimes = Config.config.unsubmittedSegments[currentVideoID] ?? [];
|
|
updateSegmentEditingUI();
|
|
|
|
messageHandler.sendMessage(
|
|
tabs[0].id,
|
|
{ message: 'isInfoFound', updating },
|
|
infoFound
|
|
);
|
|
}
|
|
|
|
function getSegmentsFromContentScript(updating: boolean): void {
|
|
messageHandler.query({
|
|
active: true,
|
|
currentWindow: true
|
|
}, (tabs) => onTabs(tabs, updating));
|
|
}
|
|
|
|
async function infoFound(request: IsInfoFoundMessageResponse) {
|
|
// End any loading animation
|
|
if (stopLoadingAnimation != null) {
|
|
stopLoadingAnimation();
|
|
stopLoadingAnimation = null;
|
|
}
|
|
|
|
if (chrome.runtime.lastError) {
|
|
//This page doesn't have the injected content script, or at least not yet
|
|
displayNoVideo();
|
|
return;
|
|
}
|
|
|
|
//if request is undefined, then the page currently being browsed is not YouTube
|
|
if (request != undefined) {
|
|
//remove loading text
|
|
PageElements.mainControls.style.display = "block";
|
|
if (request.onMobileYouTube) PageElements.mainControls.classList.add("hidden");
|
|
PageElements.whitelistButton.classList.remove("hidden");
|
|
PageElements.loadingIndicator.style.display = "none";
|
|
|
|
downloadedTimes = request.sponsorTimes ?? [];
|
|
displayDownloadedSponsorTimes(downloadedTimes, request.time);
|
|
if (request.found) {
|
|
PageElements.videoFound.innerHTML = chrome.i18n.getMessage("sponsorFound");
|
|
PageElements.issueReporterImportExport.classList.remove("hidden");
|
|
} else if (request.status == 404 || request.status == 200) {
|
|
PageElements.videoFound.innerHTML = chrome.i18n.getMessage("sponsor404");
|
|
PageElements.issueReporterImportExport.classList.remove("hidden");
|
|
} else {
|
|
if (request.status) {
|
|
PageElements.videoFound.innerHTML = chrome.i18n.getMessage("connectionError") + request.status;
|
|
} else {
|
|
PageElements.videoFound.innerHTML = chrome.i18n.getMessage("segmentsStillLoading");
|
|
}
|
|
|
|
PageElements.issueReporterImportExport.classList.remove("hidden");
|
|
}
|
|
}
|
|
|
|
//see if whitelist button should be swapped
|
|
const response = await sendTabMessageAsync({ message: 'isChannelWhitelisted' }) as IsChannelWhitelistedResponse;
|
|
if (response.value) {
|
|
PageElements.whitelistChannel.style.display = "none";
|
|
PageElements.unwhitelistChannel.style.display = "unset";
|
|
PageElements.whitelistToggle.checked = true;
|
|
document.querySelectorAll('.SBWhitelistIcon')[0].classList.add("rotated");
|
|
}
|
|
}
|
|
|
|
async function sendSponsorStartMessage() {
|
|
//the content script will get the message if a YouTube page is open
|
|
const response = await sendTabMessageAsync({ from: 'popup', message: 'sponsorStart' }) as SponsorStartResponse;
|
|
startSponsorCallback(response);
|
|
|
|
// Perform a second update after the config changes take effect as a workaround for a race condition
|
|
const removeListener = (listener: typeof lateUpdate) => {
|
|
const index = Config.configSyncListeners.indexOf(listener);
|
|
if (index !== -1) Config.configSyncListeners.splice(index, 1);
|
|
};
|
|
|
|
const lateUpdate = () => {
|
|
startSponsorCallback(response);
|
|
removeListener(lateUpdate);
|
|
};
|
|
|
|
Config.configSyncListeners.push(lateUpdate);
|
|
|
|
// Remove the listener after 200ms in case the changes were propagated by the time we got the response
|
|
setTimeout(() => removeListener(lateUpdate), 200);
|
|
}
|
|
|
|
function startSponsorCallback(response: SponsorStartResponse) {
|
|
// Only update the segments after a segment was created
|
|
if (!response.creatingSegment) {
|
|
sponsorTimes = Config.config.unsubmittedSegments[currentVideoID] || [];
|
|
}
|
|
|
|
// Update the UI
|
|
updateSegmentEditingUI();
|
|
}
|
|
|
|
//display the video times from the array at the top, in a different section
|
|
function displayDownloadedSponsorTimes(sponsorTimes: SponsorTime[], time: number) {
|
|
let currentSegmentTab = segmentTab;
|
|
if (!sponsorTimes.some((segment) => segment.actionType === ActionType.Chapter && segment.source !== SponsorSourceType.YouTube)) {
|
|
PageElements.issueReporterTabs.classList.add("hidden");
|
|
currentSegmentTab = SegmentTab.Segments;
|
|
} else {
|
|
if (currentSegmentTab === SegmentTab.Segments
|
|
&& sponsorTimes.every((segment) => segment.actionType === ActionType.Chapter)) {
|
|
PageElements.issueReporterTabs.classList.add("hidden");
|
|
currentSegmentTab = SegmentTab.Chapters;
|
|
} else {
|
|
PageElements.issueReporterTabs.classList.remove("hidden");
|
|
}
|
|
}
|
|
|
|
// Sort list by start time
|
|
const downloadedTimes = sponsorTimes
|
|
.filter((segment) => {
|
|
if (currentSegmentTab === SegmentTab.Segments) {
|
|
return segment.actionType !== ActionType.Chapter;
|
|
} else if (currentSegmentTab === SegmentTab.Chapters) {
|
|
return segment.actionType === ActionType.Chapter
|
|
&& segment.source !== SponsorSourceType.YouTube;
|
|
} else {
|
|
return true;
|
|
}
|
|
})
|
|
.sort((a, b) => a.segment[1] - b.segment[1])
|
|
.sort((a, b) => a.segment[0] - b.segment[0]);
|
|
|
|
//add them as buttons to the issue reporting container
|
|
const container = document.getElementById("issueReporterTimeButtons");
|
|
while (container.firstChild) {
|
|
container.removeChild(container.firstChild);
|
|
}
|
|
|
|
if (downloadedTimes.length > 0) {
|
|
PageElements.exportSegmentsButton.classList.remove("hidden");
|
|
} else {
|
|
PageElements.exportSegmentsButton.classList.add("hidden");
|
|
}
|
|
|
|
const isVip = Config.config.isVip;
|
|
for (let i = 0; i < downloadedTimes.length; i++) {
|
|
const UUID = downloadedTimes[i].UUID;
|
|
const locked = downloadedTimes[i].locked;
|
|
const category = downloadedTimes[i].category;
|
|
const actionType = downloadedTimes[i].actionType;
|
|
|
|
const segmentSummary = document.createElement("summary");
|
|
segmentSummary.classList.add("segmentSummary");
|
|
if (time >= downloadedTimes[i].segment[0]) {
|
|
if (time < downloadedTimes[i].segment[1]) {
|
|
segmentSummary.classList.add("segmentActive");
|
|
} else {
|
|
segmentSummary.classList.add("segmentPassed");
|
|
}
|
|
}
|
|
|
|
const categoryColorCircle = document.createElement("span");
|
|
categoryColorCircle.id = "sponsorTimesCategoryColorCircle" + UUID;
|
|
categoryColorCircle.style.backgroundColor = Config.config.barTypes[category]?.color;
|
|
categoryColorCircle.classList.add("dot");
|
|
categoryColorCircle.classList.add("sponsorTimesCategoryColorCircle");
|
|
|
|
let extraInfo = "";
|
|
if (downloadedTimes[i].hidden === SponsorHideType.Downvoted) {
|
|
//this one is downvoted
|
|
extraInfo = " (" + chrome.i18n.getMessage("hiddenDueToDownvote") + ")";
|
|
} else if (downloadedTimes[i].hidden === SponsorHideType.MinimumDuration) {
|
|
//this one is too short
|
|
extraInfo = " (" + chrome.i18n.getMessage("hiddenDueToDuration") + ")";
|
|
} else if (downloadedTimes[i].hidden === SponsorHideType.Hidden) {
|
|
extraInfo = " (" + chrome.i18n.getMessage("manuallyHidden") + ")";
|
|
}
|
|
|
|
const name = downloadedTimes[i].description || shortCategoryName(category);
|
|
const textNode = document.createTextNode(name + extraInfo);
|
|
const segmentTimeFromToNode = document.createElement("div");
|
|
if (downloadedTimes[i].actionType === ActionType.Full) {
|
|
segmentTimeFromToNode.innerText = chrome.i18n.getMessage("full");
|
|
} else {
|
|
segmentTimeFromToNode.innerText = getFormattedTime(downloadedTimes[i].segment[0], true) +
|
|
(actionType !== ActionType.Poi
|
|
? " " + chrome.i18n.getMessage("to") + " " + getFormattedTime(downloadedTimes[i].segment[1], true)
|
|
: "");
|
|
}
|
|
|
|
segmentTimeFromToNode.style.margin = "5px";
|
|
|
|
// for inline-styling purposes
|
|
const labelContainer = document.createElement("div");
|
|
if (actionType !== ActionType.Chapter) labelContainer.appendChild(categoryColorCircle);
|
|
|
|
const span = document.createElement('span');
|
|
span.className = "summaryLabel";
|
|
span.appendChild(textNode);
|
|
labelContainer.appendChild(span);
|
|
|
|
segmentSummary.appendChild(labelContainer);
|
|
segmentSummary.appendChild(segmentTimeFromToNode);
|
|
|
|
const votingButtons = document.createElement("details");
|
|
votingButtons.classList.add("votingButtons");
|
|
votingButtons.id = "votingButtons" + UUID;
|
|
votingButtons.setAttribute("data-uuid", UUID);
|
|
votingButtons.addEventListener("toggle", () => {
|
|
if (votingButtons.open) {
|
|
openedUUIDs.push(UUID);
|
|
} else {
|
|
const index = openedUUIDs.indexOf(UUID);
|
|
if (index !== -1) {
|
|
openedUUIDs.splice(openedUUIDs.indexOf(UUID), 1);
|
|
}
|
|
}
|
|
});
|
|
votingButtons.open = openedUUIDs.some((u) => u === UUID);
|
|
|
|
//thumbs up and down buttons
|
|
const voteButtonsContainer = document.createElement("div");
|
|
voteButtonsContainer.id = "sponsorTimesVoteButtonsContainer" + UUID;
|
|
voteButtonsContainer.classList.add("sbVoteButtonsContainer");
|
|
|
|
const upvoteButton = document.createElement("img");
|
|
upvoteButton.id = "sponsorTimesUpvoteButtonsContainer" + UUID;
|
|
upvoteButton.className = "voteButton";
|
|
upvoteButton.title = chrome.i18n.getMessage("upvote");
|
|
upvoteButton.src = chrome.runtime.getURL("icons/thumbs_up.svg");
|
|
upvoteButton.addEventListener("click", () => vote(1, UUID));
|
|
|
|
const downvoteButton = document.createElement("img");
|
|
downvoteButton.id = "sponsorTimesDownvoteButtonsContainer" + UUID;
|
|
downvoteButton.className = "voteButton";
|
|
downvoteButton.title = chrome.i18n.getMessage("downvote");
|
|
downvoteButton.src = locked && isVip ? chrome.runtime.getURL("icons/thumbs_down_locked.svg") : chrome.runtime.getURL("icons/thumbs_down.svg");
|
|
downvoteButton.addEventListener("click", () => vote(0, UUID));
|
|
|
|
const uuidButton = document.createElement("img");
|
|
uuidButton.id = "sponsorTimesCopyUUIDButtonContainer" + UUID;
|
|
uuidButton.className = "voteButton";
|
|
uuidButton.src = chrome.runtime.getURL("icons/clipboard.svg");
|
|
uuidButton.title = chrome.i18n.getMessage("copySegmentID");
|
|
uuidButton.addEventListener("click", () => {
|
|
copyToClipboard(UUID);
|
|
const stopAnimation = AnimationUtils.applyLoadingAnimation(uuidButton, 0.3);
|
|
stopAnimation();
|
|
});
|
|
|
|
const hideButton = document.createElement("img");
|
|
hideButton.id = "sponsorTimesCopyUUIDButtonContainer" + UUID;
|
|
hideButton.className = "voteButton";
|
|
hideButton.title = chrome.i18n.getMessage("hideSegment");
|
|
if (downloadedTimes[i].hidden === SponsorHideType.Hidden) {
|
|
hideButton.src = chrome.runtime.getURL("icons/not_visible.svg");
|
|
} else {
|
|
hideButton.src = chrome.runtime.getURL("icons/visible.svg");
|
|
}
|
|
hideButton.addEventListener("click", () => {
|
|
const stopAnimation = AnimationUtils.applyLoadingAnimation(hideButton, 0.4);
|
|
stopAnimation();
|
|
|
|
if (downloadedTimes[i].hidden === SponsorHideType.Hidden) {
|
|
hideButton.src = chrome.runtime.getURL("icons/visible.svg");
|
|
downloadedTimes[i].hidden = SponsorHideType.Visible;
|
|
} else {
|
|
hideButton.src = chrome.runtime.getURL("icons/not_visible.svg");
|
|
downloadedTimes[i].hidden = SponsorHideType.Hidden;
|
|
}
|
|
|
|
sendTabMessage({
|
|
message: "hideSegment",
|
|
type: downloadedTimes[i].hidden,
|
|
UUID: UUID
|
|
})
|
|
});
|
|
|
|
const skipButton = document.createElement("img");
|
|
skipButton.id = "sponsorTimesSkipButtonContainer" + UUID;
|
|
skipButton.className = "voteButton";
|
|
skipButton.src = chrome.runtime.getURL("icons/skip.svg");
|
|
skipButton.title = actionType === ActionType.Chapter ? chrome.i18n.getMessage("playChapter")
|
|
: chrome.i18n.getMessage("skipSegment");
|
|
skipButton.addEventListener("click", () => skipSegment(actionType, UUID, skipButton));
|
|
votingButtons.addEventListener("dblclick", () => skipSegment(actionType, UUID));
|
|
|
|
//add thumbs up, thumbs down and uuid copy buttons to the container
|
|
voteButtonsContainer.appendChild(upvoteButton);
|
|
voteButtonsContainer.appendChild(downvoteButton);
|
|
voteButtonsContainer.appendChild(uuidButton);
|
|
if (downloadedTimes[i].actionType === ActionType.Skip || downloadedTimes[i].actionType === ActionType.Mute
|
|
|| downloadedTimes[i].actionType === ActionType.Poi
|
|
&& [SponsorHideType.Visible, SponsorHideType.Hidden].includes(downloadedTimes[i].hidden)) {
|
|
voteButtonsContainer.appendChild(hideButton);
|
|
}
|
|
if (downloadedTimes[i].actionType !== ActionType.Full) {
|
|
voteButtonsContainer.appendChild(skipButton);
|
|
}
|
|
|
|
// Will contain request status
|
|
const voteStatusContainer = document.createElement("div");
|
|
voteStatusContainer.id = "sponsorTimesVoteStatusContainer" + UUID;
|
|
voteStatusContainer.classList.add("sponsorTimesVoteStatusContainer");
|
|
voteStatusContainer.style.display = "none";
|
|
|
|
const thanksForVotingText = document.createElement("div");
|
|
thanksForVotingText.id = "sponsorTimesThanksForVotingText" + UUID;
|
|
thanksForVotingText.classList.add("sponsorTimesThanksForVotingText");
|
|
voteStatusContainer.appendChild(thanksForVotingText);
|
|
|
|
votingButtons.append(segmentSummary);
|
|
votingButtons.append(voteButtonsContainer);
|
|
votingButtons.append(voteStatusContainer);
|
|
|
|
container.appendChild(votingButtons);
|
|
}
|
|
}
|
|
|
|
function submitTimes() {
|
|
if (sponsorTimes.length > 0) {
|
|
sendTabMessage({ message: 'submitTimes' })
|
|
}
|
|
}
|
|
|
|
function showNoticeAgain() {
|
|
Config.config.dontShowNotice = false;
|
|
|
|
PageElements.showNoticeAgain.style.display = "none";
|
|
}
|
|
|
|
function isCreatingSegment(): boolean {
|
|
const segments = Config.config.unsubmittedSegments[currentVideoID];
|
|
if (!segments) return false;
|
|
const lastSegment = segments[segments.length - 1];
|
|
return lastSegment && lastSegment?.segment?.length !== 2;
|
|
}
|
|
|
|
/** Updates any UI related to segment editing and submission according to the current state. */
|
|
function updateSegmentEditingUI() {
|
|
PageElements.sponsorStart.innerText = chrome.i18n.getMessage(isCreatingSegment() ? "sponsorEnd" : "sponsorStart");
|
|
|
|
PageElements.submitTimes.style.display = sponsorTimes && sponsorTimes.length > 0 ? "unset" : "none";
|
|
PageElements.submissionHint.style.display = sponsorTimes && sponsorTimes.length > 0 ? "unset" : "none";
|
|
}
|
|
|
|
//make the options div visible
|
|
function openOptions() {
|
|
chrome.runtime.sendMessage({ "message": "openConfig" });
|
|
}
|
|
|
|
function openOptionsAt(location) {
|
|
chrome.runtime.sendMessage({ "message": "openConfig", "hash": location });
|
|
}
|
|
|
|
function openHelp() {
|
|
chrome.runtime.sendMessage({ "message": "openHelp" });
|
|
}
|
|
|
|
function sendTabMessage(data: Message, callback?) {
|
|
messageHandler.query({
|
|
active: true,
|
|
currentWindow: true
|
|
}, tabs => {
|
|
messageHandler.sendMessage(
|
|
tabs[0].id,
|
|
data,
|
|
callback
|
|
);
|
|
}
|
|
);
|
|
}
|
|
|
|
function sendTabMessageAsync(data: Message): Promise<unknown> {
|
|
return new Promise((resolve) => sendTabMessage(data, (response) => resolve(response)))
|
|
}
|
|
|
|
//make the options username setting option visible
|
|
function setUsernameButton() {
|
|
PageElements.usernameInput.value = PageElements.usernameValue.innerText;
|
|
|
|
PageElements.submitUsername.style.display = "unset";
|
|
PageElements.usernameInput.style.display = "unset";
|
|
|
|
PageElements.setUsernameContainer.style.display = "none";
|
|
PageElements.setUsername.style.display = "flex";
|
|
PageElements.setUsername.classList.add("SBExpanded");
|
|
|
|
PageElements.setUsernameStatus.style.display = "none";
|
|
|
|
PageElements.sponsorTimesContributionsContainer.classList.add("hidden");
|
|
}
|
|
|
|
//submit the new username
|
|
function submitUsername() {
|
|
//add loading indicator
|
|
PageElements.setUsernameStatus.style.display = "unset";
|
|
PageElements.setUsernameStatus.innerText = chrome.i18n.getMessage("Loading");
|
|
|
|
utils.sendRequestToServer("POST", "/api/setUsername?userID=" + Config.config.userID + "&username=" + PageElements.usernameInput.value, function (response) {
|
|
if (response.status == 200) {
|
|
//submitted
|
|
PageElements.submitUsername.style.display = "none";
|
|
PageElements.usernameInput.style.display = "none";
|
|
|
|
PageElements.setUsernameContainer.style.removeProperty("display");
|
|
PageElements.setUsername.classList.remove("SBExpanded");
|
|
PageElements.usernameValue.innerText = PageElements.usernameInput.value;
|
|
|
|
PageElements.setUsernameStatus.style.display = "none";
|
|
|
|
PageElements.sponsorTimesContributionsContainer.classList.remove("hidden");
|
|
} else {
|
|
PageElements.setUsernameStatus.innerText = GenericUtils.getErrorMessage(response.status, response.responseText);
|
|
}
|
|
});
|
|
|
|
|
|
PageElements.setUsernameContainer.style.display = "none";
|
|
PageElements.setUsername.style.display = "unset";
|
|
}
|
|
|
|
//this is not a YouTube video page
|
|
function displayNoVideo() {
|
|
document.getElementById("loadingIndicator").innerText = chrome.i18n.getMessage("noVideoID");
|
|
|
|
PageElements.issueReporterTabs.classList.add("hidden");
|
|
}
|
|
|
|
function addVoteMessage(message, UUID) {
|
|
const voteButtonsContainer = document.getElementById("sponsorTimesVoteButtonsContainer" + UUID);
|
|
voteButtonsContainer.style.display = "none";
|
|
|
|
const voteStatusContainer = document.getElementById("sponsorTimesVoteStatusContainer" + UUID);
|
|
voteStatusContainer.style.removeProperty("display");
|
|
|
|
const thanksForVotingText = document.getElementById("sponsorTimesThanksForVotingText" + UUID);
|
|
thanksForVotingText.innerText = message;
|
|
}
|
|
|
|
function removeVoteMessage(UUID) {
|
|
const voteButtonsContainer = document.getElementById("sponsorTimesVoteButtonsContainer" + UUID);
|
|
voteButtonsContainer.style.display = "block";
|
|
|
|
const voteStatusContainer = document.getElementById("sponsorTimesVoteStatusContainer" + UUID);
|
|
voteStatusContainer.style.display = "none";
|
|
|
|
const thanksForVotingText = document.getElementById("sponsorTimesThanksForVotingText" + UUID);
|
|
thanksForVotingText.removeAttribute("innerText");
|
|
}
|
|
|
|
async function vote(type, UUID) {
|
|
//add loading info
|
|
addVoteMessage(chrome.i18n.getMessage("Loading"), UUID);
|
|
const response = await sendTabMessageAsync({
|
|
message: "submitVote",
|
|
type: type,
|
|
UUID: UUID
|
|
}) as VoteResponse;
|
|
|
|
if (response != undefined) {
|
|
//see if it was a success or failure
|
|
if (response.successType == 1 || (response.successType == -1 && response.statusCode == 429)) {
|
|
//success (treat rate limits as a success)
|
|
addVoteMessage(chrome.i18n.getMessage("voted"), UUID);
|
|
} else if (response.successType == -1) {
|
|
addVoteMessage(GenericUtils.getErrorMessage(response.statusCode, response.responseText), UUID);
|
|
}
|
|
setTimeout(() => removeVoteMessage(UUID), 1500);
|
|
}
|
|
}
|
|
|
|
async function whitelistChannel() {
|
|
//get the channel url
|
|
const response = await sendTabMessageAsync({ message: 'getChannelID' }) as GetChannelIDResponse;
|
|
if (!response.channelID) {
|
|
alert(chrome.i18n.getMessage("channelDataNotFound") + " https://github.com/ajayyy/SponsorBlock/issues/753");
|
|
return;
|
|
}
|
|
|
|
//get whitelisted channels
|
|
let whitelistedChannels = Config.config.whitelistedChannels;
|
|
if (whitelistedChannels == undefined) {
|
|
whitelistedChannels = [];
|
|
}
|
|
|
|
//add on this channel
|
|
whitelistedChannels.push(response.channelID);
|
|
|
|
//change button
|
|
PageElements.whitelistChannel.style.display = "none";
|
|
PageElements.unwhitelistChannel.style.display = "unset";
|
|
document.querySelectorAll('.SBWhitelistIcon')[0].classList.add("rotated");
|
|
|
|
//show 'consider force channel check' alert
|
|
if (!Config.config.forceChannelCheck) PageElements.whitelistForceCheck.classList.remove("hidden");
|
|
|
|
//save this
|
|
Config.config.whitelistedChannels = whitelistedChannels;
|
|
|
|
//send a message to the client
|
|
sendTabMessage({
|
|
message: 'whitelistChange',
|
|
value: true
|
|
});
|
|
}
|
|
|
|
async function unwhitelistChannel() {
|
|
//get the channel url
|
|
const response = await sendTabMessageAsync({ message: 'getChannelID' }) as GetChannelIDResponse;
|
|
|
|
//get whitelisted channels
|
|
let whitelistedChannels = Config.config.whitelistedChannels;
|
|
if (whitelistedChannels == undefined) {
|
|
whitelistedChannels = [];
|
|
}
|
|
|
|
//remove this channel
|
|
const index = whitelistedChannels.indexOf(response.channelID);
|
|
whitelistedChannels.splice(index, 1);
|
|
|
|
//change button
|
|
PageElements.whitelistChannel.style.display = "unset";
|
|
PageElements.unwhitelistChannel.style.display = "none";
|
|
document.querySelectorAll('.SBWhitelistIcon')[0].classList.remove("rotated");
|
|
|
|
//hide 'consider force channel check' alert
|
|
PageElements.whitelistForceCheck.classList.add("hidden");
|
|
|
|
//save this
|
|
Config.config.whitelistedChannels = whitelistedChannels;
|
|
|
|
//send a message to the client
|
|
sendTabMessage({
|
|
message: 'whitelistChange',
|
|
value: false
|
|
});
|
|
}
|
|
|
|
function startLoadingAnimation() {
|
|
stopLoadingAnimation = AnimationUtils.applyLoadingAnimation(PageElements.refreshSegmentsButton, 0.3);
|
|
}
|
|
|
|
function refreshSegments() {
|
|
startLoadingAnimation();
|
|
sendTabMessage({ message: 'refreshSegments' });
|
|
}
|
|
|
|
function skipSegment(actionType: ActionType, UUID: SegmentUUID, element?: HTMLElement): void {
|
|
if (actionType === ActionType.Chapter) {
|
|
sendTabMessage({
|
|
message: "unskip",
|
|
UUID: UUID
|
|
});
|
|
} else {
|
|
sendTabMessage({
|
|
message: "reskip",
|
|
UUID: UUID
|
|
});
|
|
}
|
|
|
|
if (element) {
|
|
const stopAnimation = AnimationUtils.applyLoadingAnimation(element, 0.3);
|
|
stopAnimation();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Should skipping be disabled (visuals stay)
|
|
*/
|
|
function toggleSkipping(disabled) {
|
|
Config.config.disableSkipping = disabled;
|
|
|
|
let hiddenButton = PageElements.disableSkipping;
|
|
let shownButton = PageElements.enableSkipping;
|
|
|
|
if (!disabled) {
|
|
hiddenButton = PageElements.enableSkipping;
|
|
shownButton = PageElements.disableSkipping;
|
|
}
|
|
|
|
shownButton.style.display = "unset";
|
|
hiddenButton.style.display = "none";
|
|
}
|
|
|
|
function copyToClipboard(text: string): void {
|
|
if (window === window.top) {
|
|
window.navigator.clipboard.writeText(text);
|
|
} else {
|
|
sendTabMessage({
|
|
message: "copyToClipboard",
|
|
text
|
|
});
|
|
}
|
|
}
|
|
|
|
async function importSegments() {
|
|
const text = (PageElements.importSegmentsText as HTMLInputElement).value;
|
|
|
|
sendTabMessage({
|
|
message: "importSegments",
|
|
data: text
|
|
});
|
|
|
|
PageElements.importSegmentsMenu.classList.add("hidden");
|
|
}
|
|
|
|
function exportSegments() {
|
|
copyToClipboard(exportTimes(downloadedTimes));
|
|
|
|
const stopAnimation = AnimationUtils.applyLoadingAnimation(PageElements.exportSegmentsButton, 0.3);
|
|
stopAnimation();
|
|
new GenericNotice(null, "exportCopied", {
|
|
title: chrome.i18n.getMessage(`CopiedExclamation`),
|
|
timed: true,
|
|
maxCountdownTime: () => 0.6,
|
|
referenceNode: PageElements.exportSegmentsButton.parentElement,
|
|
dontPauseCountdown: true,
|
|
style: {
|
|
top: 0,
|
|
bottom: 0,
|
|
minWidth: 0,
|
|
right: "30px",
|
|
margin: "auto",
|
|
height: "max-content"
|
|
},
|
|
hideLogo: true,
|
|
hideRightInfo: true
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Converts time in minutes to 2d 5h 25.1
|
|
* If less than 1 hour, just returns minutes
|
|
*
|
|
* @param {float} minutes
|
|
* @returns {string}
|
|
*/
|
|
function getFormattedHours(minutes) {
|
|
minutes = Math.round(minutes * 10) / 10;
|
|
const days = Math.floor(minutes / 1440);
|
|
const hours = Math.floor(minutes / 60) % 24;
|
|
return (days > 0 ? days + chrome.i18n.getMessage("dayAbbreviation") + " " : "") + (hours > 0 ? hours + chrome.i18n.getMessage("hourAbbreviation") + " " : "") + (minutes % 60).toFixed(1);
|
|
}
|
|
|
|
function contentConfigUpdateListener(changes: StorageChangesObject) {
|
|
for (const key in changes) {
|
|
switch(key) {
|
|
case "unsubmittedSegments":
|
|
sponsorTimes = Config.config.unsubmittedSegments[currentVideoID] ?? [];
|
|
updateSegmentEditingUI();
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
function setupComPort(): void {
|
|
port = chrome.runtime.connect({ name: "popup" });
|
|
port.onDisconnect.addListener(() => setupComPort());
|
|
port.onMessage.addListener((msg) => onMessage(msg));
|
|
}
|
|
|
|
function updateCurrentTime(currentTime: number) {
|
|
// Create a map of segment UUID -> segment object for easy access
|
|
const segmentMap: Record<string, SponsorTime> = {};
|
|
for (const segment of downloadedTimes)
|
|
segmentMap[segment.UUID] = segment
|
|
|
|
// Iterate over segment elements and update their classes
|
|
const segmentList = document.getElementById("issueReporterTimeButtons");
|
|
for (const segmentElement of segmentList.children) {
|
|
const UUID = segmentElement.getAttribute("data-uuid");
|
|
if (UUID == null || segmentMap[UUID] == undefined) continue;
|
|
|
|
const summaryElement = segmentElement.querySelector("summary")
|
|
if (summaryElement == null) continue;
|
|
|
|
const segment = segmentMap[UUID]
|
|
summaryElement.classList.remove("segmentActive", "segmentPassed")
|
|
if (currentTime >= segment.segment[0]) {
|
|
if (currentTime < segment.segment[1]) {
|
|
summaryElement.classList.add("segmentActive");
|
|
} else {
|
|
summaryElement.classList.add("segmentPassed");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function onMessage(msg: PopupMessage) {
|
|
switch (msg.message) {
|
|
case "time":
|
|
updateCurrentTime(msg.time);
|
|
break;
|
|
case "infoUpdated":
|
|
infoFound(msg);
|
|
break;
|
|
case "videoChanged":
|
|
currentVideoID = msg.videoID
|
|
sponsorTimes = Config.config.unsubmittedSegments[currentVideoID] ?? [];
|
|
updateSegmentEditingUI();
|
|
|
|
if (msg.whitelisted) {
|
|
PageElements.whitelistChannel.style.display = "none";
|
|
PageElements.unwhitelistChannel.style.display = "unset";
|
|
PageElements.whitelistToggle.checked = true;
|
|
document.querySelectorAll('.SBWhitelistIcon')[0].classList.add("rotated");
|
|
}
|
|
|
|
// Clear segments list & start loading animation
|
|
// We'll get a ping once they're loaded
|
|
startLoadingAnimation();
|
|
PageElements.videoFound.innerHTML = chrome.i18n.getMessage("Loading");
|
|
displayDownloadedSponsorTimes([], 0);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
runThePopup();
|