mirror of
https://github.com/younesaassila/ttv-lol-pro.git
synced 2025-04-29 22:14:27 +02:00
798 lines
26 KiB
TypeScript
798 lines
26 KiB
TypeScript
import Bowser from "bowser";
|
|
import browser from "webextension-polyfill";
|
|
import onStartupStoreCleanup from "../background/handlers/onStartupStoreCleanup";
|
|
import $ from "../common/ts/$";
|
|
import { readFile, saveFile } from "../common/ts/file";
|
|
import findChannelFromTwitchTvUrl from "../common/ts/findChannelFromTwitchTvUrl";
|
|
import isChannelWhitelisted from "../common/ts/isChannelWhitelisted";
|
|
import isChromium from "../common/ts/isChromium";
|
|
import isRequestTypeProxied from "../common/ts/isRequestTypeProxied";
|
|
import { getProxyInfoFromUrl } from "../common/ts/proxyInfo";
|
|
import {
|
|
clearProxySettings,
|
|
updateProxySettings,
|
|
} from "../common/ts/proxySettings";
|
|
import sendAdLog from "../common/ts/sendAdLog";
|
|
import store from "../store";
|
|
import getDefaultState from "../store/getDefaultState";
|
|
import type { State } from "../store/types";
|
|
import { KeyOfType, ProxyRequestType } from "../types";
|
|
|
|
//#region Types
|
|
type AllowedResult = [boolean, string?];
|
|
type InsertMode = "append" | "prepend" | "both";
|
|
type StoreStringArrayKey = KeyOfType<typeof store.state, string[]>;
|
|
type ListOptions = {
|
|
getAlreadyExistsAlertMessage(text: string): string;
|
|
getItemPlaceholder(text: string): string;
|
|
getPromptPlaceholder(insertMode: InsertMode): string;
|
|
isAddAllowed(text: string): AllowedResult;
|
|
isEditAllowed(text: string): AllowedResult;
|
|
onChange?(oldText: string | undefined, newText: string): void;
|
|
focusPrompt: boolean;
|
|
hidePromptMarker: boolean;
|
|
insertMode: InsertMode;
|
|
spellcheck: boolean;
|
|
};
|
|
//#endregion
|
|
|
|
//#region HTML Elements
|
|
// Import/Export
|
|
const exportButtonElement = $("#export-button") as HTMLButtonElement;
|
|
const importButtonElement = $("#import-button") as HTMLButtonElement;
|
|
const resetButtonElement = $("#reset-button") as HTMLButtonElement;
|
|
// Passport
|
|
const passportLevelSliderElement = $(
|
|
"#passport-level-slider"
|
|
) as HTMLInputElement;
|
|
const passportLevelWarningElement = $("#passport-level-warning") as HTMLElement;
|
|
const anonymousModeCheckboxElement = $(
|
|
"#anonymous-mode-checkbox"
|
|
) as HTMLInputElement;
|
|
// Proxy usage
|
|
const passportLevelProxyUsageElement = $(
|
|
"#passport-level-proxy-usage"
|
|
) as HTMLDetailsElement;
|
|
const passportLevelProxyUsageSummaryElement = $(
|
|
"#passport-level-proxy-usage-summary"
|
|
) as HTMLElement;
|
|
const passportLevelProxyUsagePassportElement = $(
|
|
"#passport-level-proxy-usage-passport"
|
|
) as HTMLTableCellElement;
|
|
const passportLevelProxyUsageUsherElement = $(
|
|
"#passport-level-proxy-usage-usher"
|
|
) as HTMLTableCellElement;
|
|
const passportLevelProxyUsageVideoWeaverElement = $(
|
|
"#passport-level-proxy-usage-video-weaver"
|
|
) as HTMLTableCellElement;
|
|
const passportLevelProxyUsageGqlElement = $(
|
|
"#passport-level-proxy-usage-gql"
|
|
) as HTMLTableCellElement;
|
|
const passportLevelProxyUsageWwwElement = $(
|
|
"#passport-level-proxy-usage-www"
|
|
) as HTMLTableCellElement;
|
|
// Whitelisted channels
|
|
const whitelistedChannelsListElement = $(
|
|
"#whitelisted-channels-list"
|
|
) as HTMLUListElement;
|
|
const whitelistSubscriptionsCheckboxElement = $(
|
|
"#whitelist-subscriptions-checkbox"
|
|
) as HTMLInputElement;
|
|
// Proxies
|
|
const optimizedProxiesInputElement = $("#optimized") as HTMLInputElement;
|
|
const optimizedProxiesListElement = $(
|
|
"#optimized-proxies-list"
|
|
) as HTMLOListElement;
|
|
const normalProxiesInputElement = $("#normal") as HTMLInputElement;
|
|
const normalProxiesListElement = $("#normal-proxies-list") as HTMLOListElement;
|
|
const otherProtocolsCheckboxElement = $(
|
|
"#other-protocols-checkbox"
|
|
) as HTMLInputElement;
|
|
// Ad log
|
|
const adLogEnabledCheckboxElement = $(
|
|
"#ad-log-enabled-checkbox"
|
|
) as HTMLInputElement;
|
|
const adLogSendButtonElement = $("#ad-log-send-button") as HTMLButtonElement;
|
|
const adLogExportButtonElement = $(
|
|
"#ad-log-export-button"
|
|
) as HTMLButtonElement;
|
|
const adLogClearButtonElement = $("#ad-log-clear-button") as HTMLButtonElement;
|
|
// Troubleshooting
|
|
const viewStatusOfProxiesButtonElement = $(
|
|
"#view-status-of-proxies-button"
|
|
) as HTMLButtonElement;
|
|
const clearSessionStorageButtonElement = $(
|
|
"#clear-session-storage-button"
|
|
) as HTMLButtonElement;
|
|
const unsetPacScriptButtonElement = $(
|
|
"#unset-pac-script-button"
|
|
) as HTMLButtonElement;
|
|
const generateTwitchTabsReportButtonElement = $(
|
|
"#generate-twitch-tabs-report-button"
|
|
) as HTMLButtonElement;
|
|
// Footer
|
|
const versionElement = $("#version") as HTMLParagraphElement;
|
|
// Main
|
|
const mainElement = $("main") as HTMLElement;
|
|
//#endregion
|
|
|
|
const DEFAULT_STATE: Readonly<State> = Object.freeze(getDefaultState());
|
|
const DEFAULT_LIST_OPTIONS: Readonly<ListOptions> = Object.freeze({
|
|
getAlreadyExistsAlertMessage: text => `'${text}' is already in the list`,
|
|
getItemPlaceholder: text => `Leave empty to remove '${text}' from the list`,
|
|
getPromptPlaceholder: () => "Enter text to create a new item…",
|
|
isAddAllowed: () => [true] as AllowedResult,
|
|
isEditAllowed: () => [true] as AllowedResult,
|
|
focusPrompt: false, // Is set to `true` once the user has added an item.
|
|
hidePromptMarker: false,
|
|
insertMode: "append",
|
|
spellcheck: false,
|
|
});
|
|
|
|
if (store.readyState === "complete") main();
|
|
else store.addEventListener("load", main);
|
|
|
|
function main() {
|
|
// Remove elements that are only for Chromium or Firefox.
|
|
document
|
|
.querySelectorAll(isChromium ? ".firefox-only" : ".chromium-only")
|
|
.forEach(element => element.remove());
|
|
// Passport
|
|
passportLevelSliderElement.value = store.state.passportLevel.toString();
|
|
passportLevelSliderElement.addEventListener("input", () => {
|
|
store.state.passportLevel = parseInt(passportLevelSliderElement.value);
|
|
if (isChromium && store.state.chromiumProxyActive) {
|
|
updateProxySettings();
|
|
}
|
|
updateProxyUsage();
|
|
});
|
|
updateProxyUsage();
|
|
anonymousModeCheckboxElement.checked = store.state.anonymousMode;
|
|
anonymousModeCheckboxElement.addEventListener("change", () => {
|
|
store.state.anonymousMode = anonymousModeCheckboxElement.checked;
|
|
});
|
|
// Whitelisted channels
|
|
listInit(whitelistedChannelsListElement, "whitelistedChannels", {
|
|
getAlreadyExistsAlertMessage: channelName =>
|
|
`'${channelName}' is already whitelisted`,
|
|
getPromptPlaceholder: () => "Enter a channel name…",
|
|
isAddAllowed(text) {
|
|
if (!/^[a-z0-9_]+$/i.test(text)) {
|
|
return [false, `'${text}' is not a valid channel name`];
|
|
}
|
|
return [true];
|
|
},
|
|
});
|
|
whitelistSubscriptionsCheckboxElement.checked =
|
|
store.state.whitelistChannelSubscriptions;
|
|
whitelistSubscriptionsCheckboxElement.addEventListener("change", () => {
|
|
const { checked } = whitelistSubscriptionsCheckboxElement;
|
|
store.state.whitelistChannelSubscriptions = checked;
|
|
if (!checked) {
|
|
// Clear active channel subscriptions to free up storage space.
|
|
store.state.activeChannelSubscriptions = [];
|
|
}
|
|
});
|
|
// Proxies
|
|
if (store.state.optimizedProxiesEnabled)
|
|
optimizedProxiesInputElement.checked = true;
|
|
else normalProxiesInputElement.checked = true;
|
|
const onProxyModeChange = () => {
|
|
store.state.optimizedProxiesEnabled = optimizedProxiesInputElement.checked;
|
|
if (isChromium && store.state.chromiumProxyActive) {
|
|
updateProxySettings();
|
|
}
|
|
updateProxyUsage();
|
|
};
|
|
optimizedProxiesInputElement.addEventListener("change", onProxyModeChange);
|
|
normalProxiesInputElement.addEventListener("change", onProxyModeChange);
|
|
loadProxiesLists();
|
|
otherProtocolsCheckboxElement.checked = store.state.allowOtherProxyProtocols;
|
|
otherProtocolsCheckboxElement.addEventListener("change", () => {
|
|
store.state.allowOtherProxyProtocols =
|
|
otherProtocolsCheckboxElement.checked;
|
|
loadProxiesLists();
|
|
});
|
|
// Ad log
|
|
adLogEnabledCheckboxElement.checked = store.state.adLogEnabled;
|
|
adLogEnabledCheckboxElement.addEventListener("change", () => {
|
|
store.state.adLogEnabled = adLogEnabledCheckboxElement.checked;
|
|
});
|
|
// Footer
|
|
versionElement.textContent = `Version ${
|
|
browser.runtime.getManifest().version
|
|
}`;
|
|
// Main
|
|
mainElement.style.display = "block";
|
|
}
|
|
|
|
function updateProxyUsage() {
|
|
const requestParams = {
|
|
isChromium: isChromium,
|
|
optimizedProxiesEnabled: store.state.optimizedProxiesEnabled,
|
|
passportLevel: store.state.passportLevel,
|
|
fullModeEnabled: false,
|
|
isFlagged: false,
|
|
};
|
|
|
|
// Proxy usage label.
|
|
let usageScore = 0;
|
|
// Unoptimized mode penalty.
|
|
if (!store.state.optimizedProxiesEnabled) usageScore += 1;
|
|
// GraphQL integrity penalty and warning.
|
|
if (isRequestTypeProxied(ProxyRequestType.GraphQLIntegrity, requestParams)) {
|
|
usageScore += 1;
|
|
passportLevelWarningElement.style.display = "block";
|
|
} else {
|
|
passportLevelWarningElement.style.display = "none";
|
|
}
|
|
switch (usageScore) {
|
|
case 0:
|
|
passportLevelProxyUsageSummaryElement.textContent = "🙂 Low proxy usage";
|
|
passportLevelProxyUsageElement.dataset.usage = "low";
|
|
break;
|
|
case 1:
|
|
passportLevelProxyUsageSummaryElement.textContent =
|
|
"😐 Medium proxy usage";
|
|
passportLevelProxyUsageElement.dataset.usage = "medium";
|
|
break;
|
|
case 2:
|
|
passportLevelProxyUsageSummaryElement.textContent = "🙁 High proxy usage";
|
|
passportLevelProxyUsageElement.dataset.usage = "high";
|
|
break;
|
|
}
|
|
|
|
// Passport
|
|
if (isRequestTypeProxied(ProxyRequestType.Passport, requestParams)) {
|
|
passportLevelProxyUsagePassportElement.textContent = "All";
|
|
} else {
|
|
passportLevelProxyUsagePassportElement.textContent = "None";
|
|
}
|
|
// Usher
|
|
passportLevelProxyUsageUsherElement.textContent = "All";
|
|
// Video Weaver
|
|
if (isRequestTypeProxied(ProxyRequestType.VideoWeaver, requestParams)) {
|
|
passportLevelProxyUsageVideoWeaverElement.textContent = "All";
|
|
} else {
|
|
passportLevelProxyUsageVideoWeaverElement.textContent = "Few";
|
|
}
|
|
// GraphQL
|
|
if (isRequestTypeProxied(ProxyRequestType.GraphQL, requestParams)) {
|
|
passportLevelProxyUsageGqlElement.textContent = "All";
|
|
} else if (
|
|
isRequestTypeProxied(ProxyRequestType.GraphQLIntegrity, requestParams)
|
|
) {
|
|
passportLevelProxyUsageGqlElement.textContent = "Some";
|
|
} else if (
|
|
isRequestTypeProxied(ProxyRequestType.GraphQLToken, requestParams)
|
|
) {
|
|
passportLevelProxyUsageGqlElement.textContent = "Few";
|
|
} else {
|
|
passportLevelProxyUsageGqlElement.textContent = "None";
|
|
}
|
|
// WWW
|
|
if (isRequestTypeProxied(ProxyRequestType.TwitchWebpage, requestParams)) {
|
|
passportLevelProxyUsageWwwElement.textContent = "All";
|
|
} else {
|
|
passportLevelProxyUsageWwwElement.textContent = "None";
|
|
}
|
|
}
|
|
|
|
function loadProxiesLists() {
|
|
listInit(optimizedProxiesListElement, "optimizedProxies", {
|
|
getPromptPlaceholder: insertMode => {
|
|
if (insertMode == "prepend") return "Enter a proxy URL… (Primary)";
|
|
return "Enter a proxy URL… (Fallback)";
|
|
},
|
|
isAddAllowed: isOptimizedProxyUrlAllowed,
|
|
onChange() {
|
|
if (isChromium && store.state.chromiumProxyActive) {
|
|
updateProxySettings();
|
|
}
|
|
},
|
|
hidePromptMarker: true,
|
|
insertMode: "both",
|
|
});
|
|
listInit(normalProxiesListElement, "normalProxies", {
|
|
getPromptPlaceholder: insertMode => {
|
|
if (insertMode == "prepend") return "Enter a proxy URL… (Primary)";
|
|
return "Enter a proxy URL… (Fallback)";
|
|
},
|
|
isAddAllowed: isNormalProxyUrlAllowed,
|
|
onChange() {
|
|
if (isChromium && store.state.chromiumProxyActive) {
|
|
updateProxySettings();
|
|
}
|
|
},
|
|
hidePromptMarker: true,
|
|
insertMode: "both",
|
|
});
|
|
}
|
|
|
|
function isOptimizedProxyUrlAllowed(url: string): AllowedResult {
|
|
const urlLower = url.toLowerCase();
|
|
|
|
// Allow default proxies.
|
|
if (DEFAULT_STATE.optimizedProxies.includes(urlLower)) {
|
|
return [true];
|
|
}
|
|
|
|
// Forbid v1 proxies.
|
|
const proxiesV1 = [
|
|
// *.ttv.lol
|
|
"api.ttv.lol",
|
|
// *.luminous.dev
|
|
"eu.luminous.dev",
|
|
"eu2.luminous.dev",
|
|
"as.luminous.dev",
|
|
"bg.luminous.dev",
|
|
// *.perfprod.com
|
|
"lb-eu.perfprod.com",
|
|
"lb-eu2.perfprod.com",
|
|
"lb-na.perfprod.com",
|
|
"lb-as.perfprod.com",
|
|
// *.cdn-perfprod.com
|
|
"lb-eu.cdn-perfprod.com",
|
|
"lb-eu2.cdn-perfprod.com",
|
|
"lb-na.cdn-perfprod.com",
|
|
"lb-as.cdn-perfprod.com",
|
|
];
|
|
if (proxiesV1.some(proxy => urlLower.includes(proxy))) {
|
|
return [false, "TTV LOL PRO v1 proxies are not compatible"];
|
|
}
|
|
|
|
if (url.includes("://")) {
|
|
const [protocol] = url.split("://", 1);
|
|
if (!store.state.allowOtherProxyProtocols) {
|
|
return [
|
|
false,
|
|
"Proxy URLs are not allowed to contain a protocol (e.g. 'http://')",
|
|
];
|
|
} else if (!["http", "https", "socks", "socks4"].includes(protocol)) {
|
|
return [false, `'${protocol}' is not a supported protocol`];
|
|
}
|
|
url = url.substring(protocol.length + 3, url.length);
|
|
}
|
|
|
|
if (url.includes("/")) {
|
|
return [false, "Proxy URLs must not contain a path (e.g. '/path')"];
|
|
}
|
|
|
|
try {
|
|
const host = url.substring(url.lastIndexOf("@") + 1, url.length);
|
|
new URL(`http://${host}`); // Throws if the host is invalid.
|
|
return [true];
|
|
} catch {
|
|
return [false, `'${url}' is not a valid proxy URL`];
|
|
}
|
|
}
|
|
|
|
function isNormalProxyUrlAllowed(url: string): AllowedResult {
|
|
const [allowed, error] = isOptimizedProxyUrlAllowed(url);
|
|
if (!allowed) return [false, error];
|
|
|
|
const urlLower = url.toLowerCase();
|
|
|
|
// Allow default proxies.
|
|
if (DEFAULT_STATE.normalProxies.includes(urlLower)) {
|
|
return [true];
|
|
}
|
|
|
|
// Allow donator proxy (password protected).
|
|
const proxyInfo = getProxyInfoFromUrl(urlLower);
|
|
const restrictedProxyHost = "restricted.api.cdn-perfprod.com";
|
|
if (
|
|
proxyInfo.host === restrictedProxyHost ||
|
|
proxyInfo.host.endsWith(`.${restrictedProxyHost}`)
|
|
) {
|
|
return [true];
|
|
}
|
|
|
|
// Forbid other perfprod.com proxies.
|
|
if (
|
|
urlLower.includes(".perfprod.com") ||
|
|
urlLower.includes(".cdn-perfprod.com")
|
|
) {
|
|
return [false, "This proxy is not compatible with 'Proxy all requests'"];
|
|
}
|
|
|
|
// Forbid proxies containing "optimized".
|
|
if (urlLower.includes("optimized")) {
|
|
return [false, "This proxy is not compatible with 'Proxy all requests'"];
|
|
}
|
|
|
|
return [true];
|
|
}
|
|
|
|
/**
|
|
* Initializes a list element.
|
|
* @param listElement
|
|
* @param storeKey
|
|
* @param options
|
|
*/
|
|
function listInit(
|
|
listElement: HTMLOListElement | HTMLUListElement,
|
|
storeKey: StoreStringArrayKey,
|
|
options: Partial<ListOptions> = {}
|
|
) {
|
|
listElement.innerHTML = ""; // Reset list element.
|
|
const listOptions: ListOptions = { ...DEFAULT_LIST_OPTIONS, ...options };
|
|
for (const text of store.state[storeKey]) {
|
|
_listAppend(listElement, storeKey, text, {
|
|
...listOptions,
|
|
insertMode: "append", // Always append when initializing because the array is already in the correct order.
|
|
});
|
|
}
|
|
// Add prompt(s).
|
|
if (options.insertMode === "both") {
|
|
_listPrompt(listElement, storeKey, {
|
|
...listOptions,
|
|
insertMode: "append",
|
|
});
|
|
_listPrompt(listElement, storeKey, {
|
|
...listOptions,
|
|
insertMode: "prepend",
|
|
});
|
|
} else {
|
|
_listPrompt(listElement, storeKey, listOptions);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Appends an item to a list element.
|
|
* @param listElement
|
|
* @param storeKey
|
|
* @param text
|
|
* @param options
|
|
*/
|
|
function _listAppend(
|
|
listElement: HTMLOListElement | HTMLUListElement,
|
|
storeKey: StoreStringArrayKey,
|
|
text: string,
|
|
options: ListOptions
|
|
) {
|
|
const listItem = document.createElement("li");
|
|
const textInput = document.createElement("input");
|
|
textInput.type = "text";
|
|
|
|
const [allowed] = options.isEditAllowed(text);
|
|
if (!allowed) textInput.disabled = true;
|
|
|
|
textInput.placeholder = options.getItemPlaceholder(text);
|
|
textInput.spellcheck = options.spellcheck;
|
|
textInput.value = text;
|
|
|
|
// Highlight text when focused.
|
|
textInput.addEventListener("focus", textInput.select.bind(textInput));
|
|
|
|
// Update store when text is changed.
|
|
textInput.addEventListener("change", e => {
|
|
// Get index of item in array.
|
|
const itemIndex = store.state[storeKey].findIndex(
|
|
item => item.toLowerCase() === text.toLowerCase()
|
|
);
|
|
if (itemIndex === -1)
|
|
return console.error(`Item '${text}' not found in '${storeKey}' array`);
|
|
|
|
const textInput = e.target as HTMLInputElement;
|
|
const oldText = text;
|
|
const newText = textInput.value.trim();
|
|
// Remove item if text is empty.
|
|
if (newText === "") {
|
|
store.state[storeKey].splice(itemIndex, 1);
|
|
listItem.remove();
|
|
if (options.onChange) options.onChange(oldText, newText);
|
|
return;
|
|
}
|
|
// Check if text is valid.
|
|
const [allowed, error] = options.isAddAllowed(newText);
|
|
if (!allowed) {
|
|
alert(error || "You cannot add this item");
|
|
textInput.value = text;
|
|
return;
|
|
}
|
|
// Update item in array.
|
|
store.state[storeKey][itemIndex] = newText;
|
|
textInput.placeholder = options.getItemPlaceholder(newText);
|
|
textInput.value = newText; // Update text in case it was trimmed.
|
|
text = newText; // Update current text variable.
|
|
if (options.onChange) options.onChange(oldText, newText);
|
|
});
|
|
|
|
listItem.append(textInput);
|
|
|
|
if (options.insertMode === "prepend") listElement.prepend(listItem);
|
|
else listElement.append(listItem);
|
|
}
|
|
|
|
/**
|
|
* Creates a prompt (text input) to add new items to a list.
|
|
* @param listElement
|
|
* @param storeKey
|
|
* @param options
|
|
*/
|
|
function _listPrompt(
|
|
listElement: HTMLOListElement | HTMLUListElement,
|
|
storeKey: StoreStringArrayKey,
|
|
options: ListOptions
|
|
) {
|
|
const listItem = document.createElement("li");
|
|
if (options.hidePromptMarker) listItem.classList.add("hide-marker");
|
|
const promptInput = document.createElement("input");
|
|
promptInput.type = "text";
|
|
|
|
promptInput.placeholder = options.getPromptPlaceholder(options.insertMode);
|
|
promptInput.spellcheck = options.spellcheck;
|
|
|
|
// Update store when text is changed.
|
|
promptInput.addEventListener("change", e => {
|
|
const promptInput = e.target as HTMLInputElement;
|
|
const text = promptInput.value.trim();
|
|
// Do nothing if text is empty.
|
|
if (text === "") return;
|
|
// Check if text is valid.
|
|
const [allowed, error] = options.isAddAllowed(text);
|
|
if (!allowed) {
|
|
alert(error || "You cannot add this item");
|
|
promptInput.value = "";
|
|
return;
|
|
}
|
|
// Check if item already exists.
|
|
const alreadyExists = store.state[storeKey].some(
|
|
item => item.toLowerCase() === text.toLowerCase()
|
|
);
|
|
if (alreadyExists) {
|
|
alert(options.getAlreadyExistsAlertMessage(text));
|
|
promptInput.value = "";
|
|
return;
|
|
}
|
|
// Add item to array.
|
|
const newArray = store.state[storeKey];
|
|
if (options.insertMode === "prepend") newArray.unshift(text);
|
|
else newArray.push(text);
|
|
store.state[storeKey] = newArray;
|
|
if (options.onChange) options.onChange(undefined, text);
|
|
|
|
listItem.remove();
|
|
_listAppend(listElement, storeKey, text, options);
|
|
_listPrompt(listElement, storeKey, {
|
|
...options,
|
|
focusPrompt: true,
|
|
});
|
|
});
|
|
|
|
listItem.append(promptInput);
|
|
|
|
if (options.insertMode === "prepend") listElement.prepend(listItem);
|
|
else listElement.append(listItem);
|
|
|
|
if (options.focusPrompt) promptInput.focus();
|
|
}
|
|
|
|
exportButtonElement.addEventListener("click", () => {
|
|
const state: Partial<State> = {
|
|
adLogEnabled: store.state.adLogEnabled,
|
|
anonymousMode: store.state.anonymousMode,
|
|
normalProxies: store.state.normalProxies,
|
|
optimizedProxies: store.state.optimizedProxies,
|
|
optimizedProxiesEnabled: store.state.optimizedProxiesEnabled,
|
|
passportLevel: store.state.passportLevel,
|
|
whitelistChannelSubscriptions: store.state.whitelistChannelSubscriptions,
|
|
whitelistedChannels: store.state.whitelistedChannels,
|
|
};
|
|
saveFile(
|
|
"ttv-lol-pro_backup.json",
|
|
JSON.stringify(state),
|
|
"application/json;charset=utf-8"
|
|
);
|
|
});
|
|
|
|
importButtonElement.addEventListener("click", async () => {
|
|
const DEFAULT_STATE_KEYS = Object.keys(DEFAULT_STATE);
|
|
|
|
try {
|
|
const data = await readFile("application/json;charset=utf-8");
|
|
const state = JSON.parse(data);
|
|
|
|
for (const entry of Object.entries(state)) {
|
|
const key = entry[0] as keyof State;
|
|
const value = entry[1];
|
|
|
|
if (!DEFAULT_STATE_KEYS.includes(key)) {
|
|
console.warn(`Unknown key '${key}' in imported settings`);
|
|
continue;
|
|
}
|
|
let filteredValue = value;
|
|
if (key === "optimizedProxies" && Array.isArray(value)) {
|
|
filteredValue = value.filter(item =>
|
|
item != null ? isOptimizedProxyUrlAllowed(item.toString())[0] : false
|
|
);
|
|
}
|
|
if (key === "normalProxies" && Array.isArray(value)) {
|
|
filteredValue = value.filter(item =>
|
|
item != null ? isNormalProxyUrlAllowed(item.toString())[0] : false
|
|
);
|
|
}
|
|
if (key === "passportLevel") {
|
|
if (typeof value !== "number") {
|
|
filteredValue = DEFAULT_STATE.passportLevel;
|
|
} else {
|
|
filteredValue = Math.min(Math.max(value, 0), 2);
|
|
}
|
|
}
|
|
// @ts-ignore
|
|
store.state[key] = filteredValue;
|
|
}
|
|
window.location.reload(); // Reload page to update UI.
|
|
} catch (error) {
|
|
alert(`An error occurred while importing settings: ${error}`);
|
|
}
|
|
});
|
|
|
|
resetButtonElement.addEventListener("click", () => {
|
|
const confirmation = confirm(
|
|
"Are you sure you want to reset all settings to their default values?"
|
|
);
|
|
if (!confirmation) return;
|
|
store.clear();
|
|
window.location.reload(); // Reload page to update UI.
|
|
});
|
|
|
|
adLogSendButtonElement.addEventListener("click", async () => {
|
|
const success = await sendAdLog();
|
|
if (success === null) {
|
|
return alert("No log entries to send.");
|
|
}
|
|
if (!success) {
|
|
return alert("Failed to send log.");
|
|
}
|
|
alert("Log sent successfully.");
|
|
});
|
|
|
|
adLogExportButtonElement.addEventListener("click", () => {
|
|
saveFile(
|
|
"ttv-lol-pro_ad-log.json",
|
|
JSON.stringify(store.state.adLog),
|
|
"application/json;charset=utf-8"
|
|
);
|
|
});
|
|
|
|
adLogClearButtonElement.addEventListener("click", () => {
|
|
const confirmation = confirm(
|
|
"Are you sure you want to clear the ad log? This cannot be undone."
|
|
);
|
|
if (!confirmation) return;
|
|
store.state.adLog = [];
|
|
});
|
|
|
|
viewStatusOfProxiesButtonElement.addEventListener("click", () => {
|
|
location.href = "https://status.perfprod.com/";
|
|
});
|
|
|
|
clearSessionStorageButtonElement.addEventListener("click", () => {
|
|
onStartupStoreCleanup();
|
|
alert("Session storage cleared successfully.");
|
|
});
|
|
|
|
unsetPacScriptButtonElement.addEventListener("click", () => {
|
|
if (isChromium) {
|
|
clearProxySettings();
|
|
alert("PAC script unset successfully.");
|
|
}
|
|
});
|
|
|
|
generateTwitchTabsReportButtonElement.addEventListener("click", async () => {
|
|
let report = "**Twitch Tabs Report**\n\n";
|
|
|
|
const extensionInfo = await browser.management.getSelf();
|
|
const userAgentParser = Bowser.getParser(window.navigator.userAgent);
|
|
report += `Extension: ${extensionInfo.name} v${extensionInfo.version} (${extensionInfo.installType})\n`;
|
|
report += `Browser: ${userAgentParser.getBrowserName()} ${userAgentParser.getBrowserVersion()} (${userAgentParser.getOSName()} ${userAgentParser.getOSVersion()})\n\n`;
|
|
|
|
const openedTabs = await browser.tabs.query({
|
|
url: ["https://www.twitch.tv/*", "https://m.twitch.tv/*"],
|
|
});
|
|
const detectedTabs = store.state.openedTwitchTabs;
|
|
|
|
// Print all opened tabs.
|
|
report += `Opened Twitch tabs (${openedTabs.length}):\n`;
|
|
for (const tab of openedTabs) {
|
|
report += `- ${tab.url || tab.pendingUrl} (id: ${tab.id}, windowId: ${
|
|
tab.windowId
|
|
})\n`;
|
|
}
|
|
report += "\n";
|
|
|
|
// Whitelisted tabs in `openedTabs`.
|
|
const openedWhitelistedTabs = openedTabs.filter(tab => {
|
|
const url = tab.url || tab.pendingUrl;
|
|
if (!url) return false;
|
|
const channelName = findChannelFromTwitchTvUrl(url);
|
|
const isWhitelisted = channelName
|
|
? isChannelWhitelisted(channelName)
|
|
: false;
|
|
return isWhitelisted;
|
|
});
|
|
report += `Out of the ${openedTabs.length} opened Twitch tabs, ${
|
|
openedWhitelistedTabs.length
|
|
} ${openedWhitelistedTabs.length === 1 ? "is" : "are"} whitelisted:\n`;
|
|
for (const tab of openedWhitelistedTabs) {
|
|
report += `- ${tab.url || tab.pendingUrl} (id: ${tab.id}, windowId: ${
|
|
tab.windowId
|
|
})\n`;
|
|
}
|
|
report += "\n";
|
|
|
|
// Check for missing tabs in `detectedTabs`.
|
|
const missingTabs = openedTabs.filter(
|
|
tab => !detectedTabs.some(extensionTab => extensionTab.id === tab.id)
|
|
);
|
|
if (missingTabs.length > 0) {
|
|
report += `The following Twitch tabs are missing from \`store.state.openedTwitchTabs\`:\n`;
|
|
for (const tab of missingTabs) {
|
|
report += `- ${tab.url || tab.pendingUrl} (id: ${tab.id}, windowId: ${
|
|
tab.windowId
|
|
})\n`;
|
|
}
|
|
report += "\n";
|
|
} else {
|
|
report +=
|
|
"All opened Twitch tabs are present in `store.state.openedTwitchTabs`.\n\n";
|
|
}
|
|
|
|
// Check for extra tabs in `detectedTabs`.
|
|
const extraTabs = detectedTabs.filter(
|
|
extensionTab => !openedTabs.some(tab => tab.id === extensionTab.id)
|
|
);
|
|
if (extraTabs.length > 0) {
|
|
report += `The following Twitch tabs are extra in \`store.state.openedTwitchTabs\`:\n`;
|
|
for (const tab of extraTabs) {
|
|
report += `- ${tab.url || tab.pendingUrl} (id: ${tab.id}, windowId: ${
|
|
tab.windowId
|
|
})\n`;
|
|
}
|
|
report += "\n";
|
|
} else {
|
|
report += "No extra Twitch tabs in `store.state.openedTwitchTabs`.\n\n";
|
|
}
|
|
|
|
// Whitelisted tabs in `detectedTabs`.
|
|
const detectedWhitelistedTabs = detectedTabs.filter(tab => {
|
|
const url = tab.url || tab.pendingUrl;
|
|
if (!url) return false;
|
|
const channelName = findChannelFromTwitchTvUrl(url);
|
|
const isWhitelisted = channelName
|
|
? isChannelWhitelisted(channelName)
|
|
: false;
|
|
return isWhitelisted;
|
|
});
|
|
report += `Out of the ${
|
|
detectedTabs.length
|
|
} Twitch tabs in \`store.state.openedTwitchTabs\`, ${
|
|
detectedWhitelistedTabs.length
|
|
} ${detectedWhitelistedTabs.length === 1 ? "is" : "are"} whitelisted:\n`;
|
|
for (const tab of detectedWhitelistedTabs) {
|
|
report += `- ${tab.url || tab.pendingUrl} (id: ${tab.id}, windowId: ${
|
|
tab.windowId
|
|
})\n`;
|
|
}
|
|
report += "\n";
|
|
|
|
// Should the PAC script be set?
|
|
const allTabsAreWhitelisted =
|
|
openedWhitelistedTabs.length === openedTabs.length;
|
|
const shouldSetPacScript = openedTabs.length > 0 && !allTabsAreWhitelisted;
|
|
report += `Should the PAC script be set? ${
|
|
shouldSetPacScript ? "Yes" : "No"
|
|
}\n`;
|
|
report += `Is the PAC script set? ${
|
|
store.state.chromiumProxyActive ? "Yes" : "No"
|
|
}\n`;
|
|
report += "\n";
|
|
|
|
saveFile("ttv-lol-pro_tabs-report.txt", report, "text/plain;charset=utf-8");
|
|
alert(
|
|
"Report saved successfully. Please send it to the developer if requested."
|
|
);
|
|
});
|