Files
SponsorBlock/src/popup.ts

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();