init
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
node_modules
|
||||
dist
|
||||
Generated
+4172
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,12 @@
|
||||
{
|
||||
"name": "proxy-container",
|
||||
"version": "1.0.0",
|
||||
"description": "Firefox extension settings page only",
|
||||
"scripts": {
|
||||
"dev": "web-ext run --source-dir=src",
|
||||
"build": "web-ext build --source-dir=src --artifacts-dir=dist"
|
||||
},
|
||||
"devDependencies": {
|
||||
"web-ext": "^8.0.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,275 @@
|
||||
const STORAGE_KEY = "containerSettings";
|
||||
|
||||
const IGNORED_URL_PREFIXES = [
|
||||
"about:",
|
||||
"moz-extension:",
|
||||
"chrome:",
|
||||
"file:",
|
||||
"view-source:",
|
||||
"data:",
|
||||
"blob:",
|
||||
"javascript:"
|
||||
];
|
||||
|
||||
const redirectedTabIds = new Set();
|
||||
|
||||
browser.action.onClicked.addListener(() => {
|
||||
browser.runtime.openOptionsPage();
|
||||
});
|
||||
|
||||
async function getContainerSettingsMap() {
|
||||
const result = await browser.storage.local.get(STORAGE_KEY);
|
||||
return result[STORAGE_KEY] || {};
|
||||
}
|
||||
|
||||
function normalizeProxyType(type) {
|
||||
if (type === "socks5") {
|
||||
return "socks";
|
||||
}
|
||||
|
||||
const allowedTypes = ["http", "https", "socks", "socks4"];
|
||||
|
||||
if (allowedTypes.includes(type)) {
|
||||
return type;
|
||||
}
|
||||
|
||||
return "http";
|
||||
}
|
||||
|
||||
function shouldIgnoreUrl(url) {
|
||||
if (!url) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return IGNORED_URL_PREFIXES.some((prefix) => url.startsWith(prefix));
|
||||
}
|
||||
|
||||
function escapeRegex(value) {
|
||||
return value.replace(/[|\\{}()[\]^$+?.*]/g, "\\$&");
|
||||
}
|
||||
|
||||
function globToRegex(glob) {
|
||||
let pattern = String(glob || "").trim();
|
||||
|
||||
if (!pattern) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!pattern.includes("://")) {
|
||||
pattern = `*://${pattern}`;
|
||||
}
|
||||
|
||||
pattern = pattern.replace(
|
||||
/^(\*:\/\/|https?:\/\/)\*\./,
|
||||
"$1{SUBDOMAIN_OR_ROOT}"
|
||||
);
|
||||
|
||||
let regex = escapeRegex(pattern);
|
||||
|
||||
regex = regex
|
||||
.replaceAll("\\*", ".*")
|
||||
.replaceAll("\\?", ".");
|
||||
|
||||
regex = regex.replaceAll(
|
||||
"\\{SUBDOMAIN_OR_ROOT\\}",
|
||||
"([^/]+\\.)?"
|
||||
);
|
||||
|
||||
return new RegExp(`^${regex}(/.*)?$`, "i");
|
||||
}
|
||||
|
||||
function testUrlRule(rule, url) {
|
||||
try {
|
||||
const regex = globToRegex(rule);
|
||||
return regex ? regex.test(url) : false;
|
||||
} catch (error) {
|
||||
console.warn("Invalid URL rule skipped:", rule, error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
async function findContainerForUrl(url) {
|
||||
const settingsMap = await getContainerSettingsMap();
|
||||
|
||||
for (const [cookieStoreId, settings] of Object.entries(settingsMap)) {
|
||||
const urlRules = settings.urlRules || [];
|
||||
|
||||
console.log(urlRules)
|
||||
|
||||
for (const rule of urlRules) {
|
||||
if (testUrlRule(rule, url)) {
|
||||
return cookieStoreId;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
async function redirectTabToContainer(tabId, tab, targetCookieStoreId) {
|
||||
if (!targetCookieStoreId) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!tab || !tab.url) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (tab.cookieStoreId === targetCookieStoreId) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (redirectedTabIds.has(tabId)) {
|
||||
return;
|
||||
}
|
||||
|
||||
redirectedTabIds.add(tabId);
|
||||
|
||||
try {
|
||||
await browser.tabs.create({
|
||||
url: tab.url,
|
||||
cookieStoreId: targetCookieStoreId,
|
||||
active: tab.active,
|
||||
index: tab.index
|
||||
});
|
||||
|
||||
await browser.tabs.remove(tabId);
|
||||
} catch (error) {
|
||||
console.error("Failed to redirect tab to container:", error);
|
||||
} finally {
|
||||
setTimeout(() => {
|
||||
redirectedTabIds.delete(tabId);
|
||||
}, 1500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* URL routing:
|
||||
* пользователь открывает URL -> правило находит контейнер -> вкладка переоткрывается в контейнере.
|
||||
*/
|
||||
browser.tabs.onUpdated.addListener(
|
||||
async (tabId, changeInfo, tab) => {
|
||||
if (!changeInfo.url) {
|
||||
return;
|
||||
}
|
||||
|
||||
const url = changeInfo.url;
|
||||
|
||||
if (shouldIgnoreUrl(url)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const targetCookieStoreId = await findContainerForUrl(url);
|
||||
|
||||
console.log("targetCookieStoreId", url)
|
||||
|
||||
if (!targetCookieStoreId) {
|
||||
return;
|
||||
}
|
||||
|
||||
await redirectTabToContainer(tabId, tab, targetCookieStoreId);
|
||||
} catch (error) {
|
||||
console.error("URL routing failed:", error);
|
||||
}
|
||||
},
|
||||
{
|
||||
properties: ["url"]
|
||||
}
|
||||
);
|
||||
|
||||
/**
|
||||
* Per-container proxy:
|
||||
* запрос из контейнера -> cookieStoreId -> proxy-настройка контейнера.
|
||||
*/
|
||||
browser.proxy.onRequest.addListener(
|
||||
async (requestDetails) => {
|
||||
console.log("proxy")
|
||||
const cookieStoreId = requestDetails.cookieStoreId;
|
||||
|
||||
if (!cookieStoreId || cookieStoreId === "firefox-default") {
|
||||
return { type: "direct" };
|
||||
}
|
||||
|
||||
const settingsMap = await getContainerSettingsMap();
|
||||
const containerSettings = settingsMap[cookieStoreId];
|
||||
|
||||
if (!containerSettings || !containerSettings.proxy) {
|
||||
return { type: "direct" };
|
||||
}
|
||||
|
||||
const proxy = containerSettings.proxy;
|
||||
|
||||
if (!proxy.enabled) {
|
||||
return { type: "direct" };
|
||||
}
|
||||
|
||||
if (!proxy.host || !proxy.port) {
|
||||
return { type: "direct" };
|
||||
}
|
||||
|
||||
const proxyInfo = {
|
||||
type: normalizeProxyType(proxy.type),
|
||||
host: proxy.host,
|
||||
port: Number(proxy.port)
|
||||
};
|
||||
|
||||
|
||||
if (
|
||||
proxy.proxyDNS === true &&
|
||||
(proxyInfo.type === "socks" || proxyInfo.type === "socks4")
|
||||
) {
|
||||
proxyInfo.proxyDNS = true;
|
||||
}
|
||||
|
||||
return proxyInfo;
|
||||
},
|
||||
{
|
||||
urls: ["<all_urls>"]
|
||||
}
|
||||
);
|
||||
|
||||
browser.proxy.onError.addListener((error) => {
|
||||
console.error("Proxy error:", error);
|
||||
});
|
||||
|
||||
/**
|
||||
* Proxy authentication.
|
||||
*/
|
||||
browser.webRequest.onAuthRequired.addListener(
|
||||
async (details) => {
|
||||
if (!details.isProxy) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const cookieStoreId = details.cookieStoreId;
|
||||
|
||||
if (!cookieStoreId) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const settingsMap = await getContainerSettingsMap();
|
||||
const containerSettings = settingsMap[cookieStoreId];
|
||||
|
||||
if (!containerSettings || !containerSettings.proxy) {
|
||||
return {};
|
||||
}
|
||||
|
||||
const proxy = containerSettings.proxy;
|
||||
|
||||
if (!proxy.enabled || !proxy.username) {
|
||||
return {};
|
||||
}
|
||||
|
||||
return {
|
||||
authCredentials: {
|
||||
username: proxy.username,
|
||||
password: proxy.password || ""
|
||||
}
|
||||
};
|
||||
},
|
||||
{
|
||||
urls: ["<all_urls>"]
|
||||
},
|
||||
["blocking"]
|
||||
);
|
||||
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"manifest_version": 3,
|
||||
"name": "Proxy Container",
|
||||
"version": "1.0.0",
|
||||
"description": "Firefox containers with regex routing and per-container proxy",
|
||||
|
||||
"permissions": [
|
||||
"storage",
|
||||
"contextualIdentities",
|
||||
"proxy",
|
||||
"tabs",
|
||||
"cookies",
|
||||
"webRequest",
|
||||
"webRequestBlocking"
|
||||
],
|
||||
|
||||
"host_permissions": [
|
||||
"<all_urls>"
|
||||
],
|
||||
|
||||
"background": {
|
||||
"scripts": [
|
||||
"background.js"
|
||||
]
|
||||
},
|
||||
|
||||
"action": {
|
||||
"default_title": "Открыть настройки Proxy Container"
|
||||
},
|
||||
|
||||
"options_ui": {
|
||||
"page": "options.html",
|
||||
"open_in_tab": true
|
||||
},
|
||||
|
||||
"browser_specific_settings": {
|
||||
"gecko": {
|
||||
"id": "proxy-container@example.local",
|
||||
"strict_min_version": "91.1.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,251 @@
|
||||
<!doctype html>
|
||||
<html lang="ru">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>Proxy Container Settings</title>
|
||||
|
||||
<style>
|
||||
body {
|
||||
max-width: 1100px;
|
||||
margin: 40px auto;
|
||||
padding: 0 20px;
|
||||
font-family: Arial, sans-serif;
|
||||
line-height: 1.5;
|
||||
color: #222;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2,
|
||||
h3 {
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.panel {
|
||||
border: 1px solid #ccc;
|
||||
padding: 16px;
|
||||
margin-bottom: 24px;
|
||||
border-radius: 6px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.container-card {
|
||||
border: 1px solid #ddd;
|
||||
padding: 16px;
|
||||
margin-bottom: 14px;
|
||||
border-radius: 6px;
|
||||
background: #fafafa;
|
||||
}
|
||||
|
||||
.field {
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
font-weight: 700;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
input,
|
||||
select,
|
||||
textarea {
|
||||
width: 100%;
|
||||
box-sizing: border-box;
|
||||
padding: 8px;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
input[type="checkbox"] {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
textarea {
|
||||
min-height: 96px;
|
||||
font-family: Consolas, monospace;
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 8px 12px;
|
||||
cursor: pointer;
|
||||
margin-right: 6px;
|
||||
margin-top: 6px;
|
||||
}
|
||||
|
||||
.container-title {
|
||||
font-weight: 700;
|
||||
font-size: 18px;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.small {
|
||||
font-size: 12px;
|
||||
color: #555;
|
||||
word-break: break-all;
|
||||
margin-bottom: 14px;
|
||||
}
|
||||
|
||||
.danger {
|
||||
color: #a40000;
|
||||
}
|
||||
|
||||
.success {
|
||||
color: #006800;
|
||||
}
|
||||
|
||||
#status {
|
||||
font-weight: 700;
|
||||
margin-bottom: 18px;
|
||||
}
|
||||
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
@media (max-width: 760px) {
|
||||
.grid {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.checkbox-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.muted {
|
||||
color: #666;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
code {
|
||||
background: #eee;
|
||||
padding: 2px 4px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Proxy Container</h1>
|
||||
|
||||
<p id="status"></p>
|
||||
|
||||
<section class="panel">
|
||||
<h2>Создать контейнер</h2>
|
||||
|
||||
<div class="grid">
|
||||
<div class="field">
|
||||
<label for="name">Название контейнера</label>
|
||||
<input id="name" type="text" placeholder="Work" />
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="color">Цвет</label>
|
||||
<select id="color">
|
||||
<option value="blue">blue</option>
|
||||
<option value="turquoise">turquoise</option>
|
||||
<option value="green">green</option>
|
||||
<option value="yellow">yellow</option>
|
||||
<option value="orange">orange</option>
|
||||
<option value="red">red</option>
|
||||
<option value="pink">pink</option>
|
||||
<option value="purple">purple</option>
|
||||
<option value="toolbar">toolbar</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="icon">Иконка</label>
|
||||
<select id="icon">
|
||||
<option value="fingerprint">fingerprint</option>
|
||||
<option value="briefcase">briefcase</option>
|
||||
<option value="cart">cart</option>
|
||||
<option value="circle">circle</option>
|
||||
<option value="dollar">dollar</option>
|
||||
<option value="fence">fence</option>
|
||||
<option value="gift">gift</option>
|
||||
<option value="vacation">vacation</option>
|
||||
<option value="food">food</option>
|
||||
<option value="fruit">fruit</option>
|
||||
<option value="pet">pet</option>
|
||||
<option value="tree">tree</option>
|
||||
<option value="chill">chill</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="urlRules">URL-правила, по одному на строку</label>
|
||||
<textarea id="urlRules" placeholder="*.ru github.com/* https://*.example.com/*"></textarea>
|
||||
<div class="muted">
|
||||
Примеры: <code>*.ru</code>, <code>github.com/*</code>, <code>https://*.example.com/*</code>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3>Proxy</h3>
|
||||
|
||||
<div class="field">
|
||||
<label class="checkbox-label">
|
||||
<input id="proxyEnabled" type="checkbox" />
|
||||
Использовать proxy для этого контейнера
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="grid">
|
||||
<div class="field">
|
||||
<label for="proxyType">Proxy type</label>
|
||||
<select id="proxyType">
|
||||
<option value="http">http</option>
|
||||
<option value="https">https</option>
|
||||
<option value="socks">socks</option>
|
||||
<option value="socks4">socks4</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="proxyPort">Proxy port</label>
|
||||
<input id="proxyPort" type="number" placeholder="8080" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="proxyHost">Proxy host</label>
|
||||
<input id="proxyHost" type="text" placeholder="127.0.0.1" />
|
||||
</div>
|
||||
|
||||
<div class="grid">
|
||||
<div class="field">
|
||||
<label for="proxyUsername">Proxy username</label>
|
||||
<input id="proxyUsername" type="text" placeholder="необязательно" />
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label for="proxyPassword">Proxy password</label>
|
||||
<input id="proxyPassword" type="password" placeholder="необязательно" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="checkbox-label">
|
||||
<input id="proxyDNS" type="checkbox" />
|
||||
Proxy DNS через SOCKS
|
||||
</label>
|
||||
<div class="muted">
|
||||
Имеет смысл только для SOCKS/SOCKS4.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button id="createContainer">Создать контейнер</button>
|
||||
</section>
|
||||
|
||||
<section>
|
||||
<h2>Существующие контейнеры</h2>
|
||||
<div id="containers"></div>
|
||||
</section>
|
||||
|
||||
<script src="options.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
+418
@@ -0,0 +1,418 @@
|
||||
const STORAGE_KEY = "containerSettings";
|
||||
|
||||
const CONTAINER_COLORS = [
|
||||
"blue",
|
||||
"turquoise",
|
||||
"green",
|
||||
"yellow",
|
||||
"orange",
|
||||
"red",
|
||||
"pink",
|
||||
"purple",
|
||||
"toolbar"
|
||||
];
|
||||
|
||||
const CONTAINER_ICONS = [
|
||||
"fingerprint",
|
||||
"briefcase",
|
||||
"cart",
|
||||
"circle",
|
||||
"dollar",
|
||||
"fence",
|
||||
"gift",
|
||||
"vacation",
|
||||
"food",
|
||||
"fruit",
|
||||
"pet",
|
||||
"tree",
|
||||
"chill"
|
||||
];
|
||||
|
||||
const PROXY_TYPES = [
|
||||
"http",
|
||||
"https",
|
||||
"socks",
|
||||
"socks4"
|
||||
];
|
||||
|
||||
const DEFAULT_PROXY = {
|
||||
enabled: false,
|
||||
type: "http",
|
||||
host: "",
|
||||
port: 8080,
|
||||
username: "",
|
||||
password: "",
|
||||
proxyDNS: false
|
||||
};
|
||||
|
||||
const nameInput = document.getElementById("name");
|
||||
const colorInput = document.getElementById("color");
|
||||
const iconInput = document.getElementById("icon");
|
||||
const urlRulesInput = document.getElementById("urlRules");
|
||||
|
||||
const proxyEnabledInput = document.getElementById("proxyEnabled");
|
||||
const proxyTypeInput = document.getElementById("proxyType");
|
||||
const proxyHostInput = document.getElementById("proxyHost");
|
||||
const proxyPortInput = document.getElementById("proxyPort");
|
||||
const proxyUsernameInput = document.getElementById("proxyUsername");
|
||||
const proxyPasswordInput = document.getElementById("proxyPassword");
|
||||
const proxyDNSInput = document.getElementById("proxyDNS");
|
||||
|
||||
const createButton = document.getElementById("createContainer");
|
||||
const containersElement = document.getElementById("containers");
|
||||
const statusElement = document.getElementById("status");
|
||||
|
||||
function setStatus(message, isError = false) {
|
||||
statusElement.textContent = message;
|
||||
statusElement.className = isError ? "danger" : "success";
|
||||
|
||||
if (message) {
|
||||
setTimeout(() => {
|
||||
statusElement.textContent = "";
|
||||
statusElement.className = "";
|
||||
}, 3000);
|
||||
}
|
||||
}
|
||||
|
||||
function escapeHtml(value) {
|
||||
return String(value)
|
||||
.replaceAll("&", "&")
|
||||
.replaceAll("<", "<")
|
||||
.replaceAll(">", ">")
|
||||
.replaceAll('"', """);
|
||||
}
|
||||
|
||||
function parseUrlRules(text) {
|
||||
return text
|
||||
.split("\n")
|
||||
.map((line) => line.trim())
|
||||
.filter(Boolean);
|
||||
}
|
||||
|
||||
function validateUrlRules(rules) {
|
||||
for (const rule of rules) {
|
||||
if (rule.includes(" ")) {
|
||||
throw new Error(`URL-правило содержит пробел: ${rule}`);
|
||||
}
|
||||
|
||||
if (rule.length > 500) {
|
||||
throw new Error(`URL-правило слишком длинное: ${rule}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function validateProxy(proxy) {
|
||||
if (!proxy.enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!PROXY_TYPES.includes(proxy.type)) {
|
||||
throw new Error("Некорректный proxy type");
|
||||
}
|
||||
|
||||
if (!proxy.host) {
|
||||
throw new Error("Proxy host пустой");
|
||||
}
|
||||
|
||||
if (!Number.isInteger(proxy.port) || proxy.port < 1 || proxy.port > 65535) {
|
||||
throw new Error("Proxy port должен быть от 1 до 65535");
|
||||
}
|
||||
}
|
||||
|
||||
async function getSettingsMap() {
|
||||
const result = await browser.storage.local.get(STORAGE_KEY);
|
||||
return result[STORAGE_KEY] || {};
|
||||
}
|
||||
|
||||
async function saveSettingsMap(settingsMap) {
|
||||
await browser.storage.local.set({
|
||||
[STORAGE_KEY]: settingsMap
|
||||
});
|
||||
}
|
||||
|
||||
function readCreateFormProxy() {
|
||||
return {
|
||||
enabled: proxyEnabledInput.checked,
|
||||
type: proxyTypeInput.value,
|
||||
host: proxyHostInput.value.trim(),
|
||||
port: Number(proxyPortInput.value),
|
||||
username: proxyUsernameInput.value.trim(),
|
||||
password: proxyPasswordInput.value,
|
||||
proxyDNS: proxyDNSInput.checked
|
||||
};
|
||||
}
|
||||
|
||||
function clearCreateForm() {
|
||||
nameInput.value = "";
|
||||
colorInput.value = "blue";
|
||||
iconInput.value = "fingerprint";
|
||||
urlRulesInput.value = "";
|
||||
|
||||
proxyEnabledInput.checked = false;
|
||||
proxyTypeInput.value = "http";
|
||||
proxyHostInput.value = "";
|
||||
proxyPortInput.value = "";
|
||||
proxyUsernameInput.value = "";
|
||||
proxyPasswordInput.value = "";
|
||||
proxyDNSInput.checked = false;
|
||||
}
|
||||
|
||||
async function createContainer() {
|
||||
const name = nameInput.value.trim();
|
||||
const color = colorInput.value;
|
||||
const icon = iconInput.value;
|
||||
const urlRules = parseUrlRules(urlRulesInput.value);
|
||||
const proxy = readCreateFormProxy();
|
||||
|
||||
if (!name) {
|
||||
setStatus("Ошибка: название контейнера пустое", true);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
validateUrlRules(urlRules);
|
||||
validateProxy(proxy);
|
||||
|
||||
const container = await browser.contextualIdentities.create({
|
||||
name,
|
||||
color,
|
||||
icon
|
||||
});
|
||||
|
||||
const settingsMap = await getSettingsMap();
|
||||
|
||||
settingsMap[container.cookieStoreId] = {
|
||||
urlRules,
|
||||
proxy
|
||||
};
|
||||
|
||||
await saveSettingsMap(settingsMap);
|
||||
|
||||
clearCreateForm();
|
||||
|
||||
setStatus("Контейнер создан");
|
||||
await renderContainers();
|
||||
} catch (error) {
|
||||
setStatus(error.message, true);
|
||||
}
|
||||
}
|
||||
|
||||
function createOptions(values, selectedValue) {
|
||||
return values
|
||||
.map((value) => {
|
||||
const selected = value === selectedValue ? "selected" : "";
|
||||
return `<option value="${escapeHtml(value)}" ${selected}>${escapeHtml(value)}</option>`;
|
||||
})
|
||||
.join("");
|
||||
}
|
||||
|
||||
function readCardProxy(card) {
|
||||
return {
|
||||
enabled: card.querySelector(".edit-proxy-enabled").checked,
|
||||
type: card.querySelector(".edit-proxy-type").value,
|
||||
host: card.querySelector(".edit-proxy-host").value.trim(),
|
||||
port: Number(card.querySelector(".edit-proxy-port").value),
|
||||
username: card.querySelector(".edit-proxy-username").value.trim(),
|
||||
password: card.querySelector(".edit-proxy-password").value,
|
||||
proxyDNS: card.querySelector(".edit-proxy-dns").checked
|
||||
};
|
||||
}
|
||||
|
||||
async function updateContainer(cookieStoreId) {
|
||||
const card = document.querySelector(`[data-cookie-store-id="${cookieStoreId}"]`);
|
||||
|
||||
if (!card) {
|
||||
setStatus("Ошибка: карточка контейнера не найдена", true);
|
||||
return;
|
||||
}
|
||||
|
||||
const name = card.querySelector(".edit-name").value.trim();
|
||||
const color = card.querySelector(".edit-color").value;
|
||||
const icon = card.querySelector(".edit-icon").value;
|
||||
const urlRules = parseUrlRules(card.querySelector(".edit-url-rules").value);
|
||||
const proxy = readCardProxy(card);
|
||||
|
||||
if (!name) {
|
||||
setStatus("Ошибка: название контейнера пустое", true);
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
validateUrlRules(urlRules);
|
||||
validateProxy(proxy);
|
||||
|
||||
await browser.contextualIdentities.update(cookieStoreId, {
|
||||
name,
|
||||
color,
|
||||
icon
|
||||
});
|
||||
|
||||
const settingsMap = await getSettingsMap();
|
||||
|
||||
settingsMap[cookieStoreId] = {
|
||||
urlRules,
|
||||
proxy
|
||||
};
|
||||
|
||||
await saveSettingsMap(settingsMap);
|
||||
|
||||
setStatus("Контейнер обновлён");
|
||||
await renderContainers();
|
||||
} catch (error) {
|
||||
setStatus(error.message, true);
|
||||
}
|
||||
}
|
||||
|
||||
async function removeContainer(cookieStoreId) {
|
||||
const confirmed = confirm("Удалить контейнер? Это удалит Firefox-контейнер и его настройки.");
|
||||
|
||||
if (!confirmed) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await browser.contextualIdentities.remove(cookieStoreId);
|
||||
|
||||
const settingsMap = await getSettingsMap();
|
||||
delete settingsMap[cookieStoreId];
|
||||
|
||||
await saveSettingsMap(settingsMap);
|
||||
|
||||
setStatus("Контейнер удалён");
|
||||
await renderContainers();
|
||||
} catch (error) {
|
||||
setStatus(error.message, true);
|
||||
}
|
||||
}
|
||||
|
||||
function renderContainerCard(container, storedSettings) {
|
||||
const urlRules = storedSettings.urlRules || [];
|
||||
const proxy = {
|
||||
...DEFAULT_PROXY,
|
||||
...(storedSettings.proxy || {})
|
||||
};
|
||||
|
||||
const card = document.createElement("div");
|
||||
card.className = "container-card";
|
||||
card.dataset.cookieStoreId = container.cookieStoreId;
|
||||
|
||||
card.innerHTML = `
|
||||
<div class="container-title">${escapeHtml(container.name)}</div>
|
||||
<div class="small">cookieStoreId: ${escapeHtml(container.cookieStoreId)}</div>
|
||||
|
||||
<div class="grid">
|
||||
<div class="field">
|
||||
<label>Название</label>
|
||||
<input class="edit-name" type="text" value="${escapeHtml(container.name)}" />
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label>Цвет</label>
|
||||
<select class="edit-color">
|
||||
${createOptions(CONTAINER_COLORS, container.color)}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label>Иконка</label>
|
||||
<select class="edit-icon">
|
||||
${createOptions(CONTAINER_ICONS, container.icon)}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label>URL-правила</label>
|
||||
<textarea class="edit-url-rules">${escapeHtml(urlRules.join("\n"))}</textarea>
|
||||
<div class="muted">
|
||||
Примеры: <code>*.ru</code>, <code>github.com/*</code>, <code>https://*.example.com/*</code>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3>Proxy</h3>
|
||||
|
||||
<div class="field">
|
||||
<label class="checkbox-label">
|
||||
<input class="edit-proxy-enabled" type="checkbox" ${proxy.enabled ? "checked" : ""} />
|
||||
Использовать proxy для этого контейнера
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="grid">
|
||||
<div class="field">
|
||||
<label>Proxy type</label>
|
||||
<select class="edit-proxy-type">
|
||||
${createOptions(PROXY_TYPES, proxy.type)}
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label>Proxy port</label>
|
||||
<input class="edit-proxy-port" type="number" value="${escapeHtml(proxy.port)}" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label>Proxy host</label>
|
||||
<input class="edit-proxy-host" type="text" value="${escapeHtml(proxy.host)}" placeholder="127.0.0.1" />
|
||||
</div>
|
||||
|
||||
<div class="grid">
|
||||
<div class="field">
|
||||
<label>Proxy username</label>
|
||||
<input class="edit-proxy-username" type="text" value="${escapeHtml(proxy.username)}" placeholder="необязательно" />
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label>Proxy password</label>
|
||||
<input class="edit-proxy-password" type="password" value="${escapeHtml(proxy.password)}" placeholder="необязательно" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="field">
|
||||
<label class="checkbox-label">
|
||||
<input class="edit-proxy-dns" type="checkbox" ${proxy.proxyDNS ? "checked" : ""} />
|
||||
Proxy DNS через SOCKS
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<button class="save-container">Сохранить</button>
|
||||
<button class="delete-container">Удалить</button>
|
||||
`;
|
||||
|
||||
card.querySelector(".save-container").addEventListener("click", () => {
|
||||
updateContainer(container.cookieStoreId);
|
||||
});
|
||||
|
||||
card.querySelector(".delete-container").addEventListener("click", () => {
|
||||
removeContainer(container.cookieStoreId);
|
||||
});
|
||||
|
||||
return card;
|
||||
}
|
||||
|
||||
async function renderContainers() {
|
||||
containersElement.innerHTML = "";
|
||||
|
||||
try {
|
||||
const containers = await browser.contextualIdentities.query({});
|
||||
const settingsMap = await getSettingsMap();
|
||||
|
||||
if (!containers.length) {
|
||||
containersElement.textContent = "Контейнеров пока нет.";
|
||||
return;
|
||||
}
|
||||
|
||||
for (const container of containers) {
|
||||
const storedSettings = settingsMap[container.cookieStoreId] || {};
|
||||
const card = renderContainerCard(container, storedSettings);
|
||||
containersElement.appendChild(card);
|
||||
}
|
||||
} catch (error) {
|
||||
setStatus(error.message, true);
|
||||
}
|
||||
}
|
||||
|
||||
createButton.addEventListener("click", createContainer);
|
||||
|
||||
renderContainers();
|
||||
Reference in New Issue
Block a user