first commit
This commit is contained in:
23
README.md
Normal file
23
README.md
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
# Proxy Switcher(Chrome 扩展)
|
||||||
|
|
||||||
|
一键切换 Chrome 代理:直连 / System / HTTP / SOCKS5 / PAC,支持弹窗选择与 PAC URL 保存。
|
||||||
|
|
||||||
|
## 使用方法
|
||||||
|
|
||||||
|
1. 打开 Chrome `chrome://extensions/`
|
||||||
|
2. 右上角打开“开发者模式”
|
||||||
|
3. 点击“加载已解压的扩展程序”,选择本目录 `2025-08-12_代理切换-浏览器扩展`
|
||||||
|
4. 点击工具栏图标,在弹窗中选择代理配置并点击“应用”
|
||||||
|
5. 选择 PAC 模式时,可在输入框中填写 PAC URL 后点击“保存”
|
||||||
|
6. 选择 自定义代理,可输入协议/地址/端口与绕过列表(逗号分隔),可保存或直接“应用”
|
||||||
|
|
||||||
|
## 权限说明
|
||||||
|
|
||||||
|
- `proxy`: 设置浏览器代理
|
||||||
|
- `storage`: 保存配置与当前选择
|
||||||
|
|
||||||
|
## 注意
|
||||||
|
|
||||||
|
- 如果系统或安全策略禁用了扩展修改代理,可能无法生效
|
||||||
|
- PAC URL 需可访问且返回有效 PAC 脚本
|
||||||
|
- 若切换到“系统代理/自动检测”仍无效,可尝试先切到“直连”,再切回“系统/自动”,或重载扩展。代码已在切换时先清理旧规则再设置以减少残留影响。
|
||||||
276
background.js
Normal file
276
background.js
Normal file
@ -0,0 +1,276 @@
|
|||||||
|
const STORAGE_KEYS = {
|
||||||
|
ACTIVE_PROFILE_ID: "activeProfileId",
|
||||||
|
PROFILES: "profiles",
|
||||||
|
};
|
||||||
|
|
||||||
|
const DEFAULT_PROFILES = [
|
||||||
|
{ id: "direct", name: "直连(关闭代理)", type: "direct" },
|
||||||
|
{ id: "system", name: "跟随系统代理", type: "system" },
|
||||||
|
{ id: "auto", name: "自动检测(WPAD)", type: "auto_detect" },
|
||||||
|
{
|
||||||
|
id: "local-http",
|
||||||
|
name: "HTTP 127.0.0.1:7897",
|
||||||
|
type: "fixed",
|
||||||
|
scheme: "http",
|
||||||
|
host: "127.0.0.1",
|
||||||
|
port: 7897,
|
||||||
|
bypassList: ["localhost", "127.0.0.1"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "local-socks5",
|
||||||
|
name: "SOCKS5 127.0.0.1:7897",
|
||||||
|
type: "fixed",
|
||||||
|
scheme: "socks5",
|
||||||
|
host: "127.0.0.1",
|
||||||
|
port: 7897,
|
||||||
|
bypassList: ["localhost", "127.0.0.1"],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "pac",
|
||||||
|
name: "PAC(自定义 URL)",
|
||||||
|
type: "pac",
|
||||||
|
pacUrl: "",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "custom",
|
||||||
|
name: "自定义代理",
|
||||||
|
type: "fixed",
|
||||||
|
scheme: "http",
|
||||||
|
host: "",
|
||||||
|
port: 0,
|
||||||
|
bypassList: ["localhost", "127.0.0.1"],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
// 动态图标(仅使用 PNG)
|
||||||
|
// 只支持 PNG,去掉 SVG 支持与栅格化逻辑
|
||||||
|
const ICON_FALLBACK_PNG = {
|
||||||
|
open: { 16: "icons/open-16.png", 32: "icons/open-32.png" },
|
||||||
|
close: { 16: "icons/close-16.png", 32: "icons/close-32.png" },
|
||||||
|
};
|
||||||
|
|
||||||
|
chrome.runtime.onInstalled.addListener(async () => {
|
||||||
|
const { profiles, activeProfileId } = await chrome.storage.local.get([
|
||||||
|
STORAGE_KEYS.PROFILES,
|
||||||
|
STORAGE_KEYS.ACTIVE_PROFILE_ID,
|
||||||
|
]);
|
||||||
|
const ensured = ensureProfiles(profiles);
|
||||||
|
await chrome.storage.local.set({ [STORAGE_KEYS.PROFILES]: ensured });
|
||||||
|
if (!activeProfileId) {
|
||||||
|
await chrome.storage.local.set({
|
||||||
|
[STORAGE_KEYS.ACTIVE_PROFILE_ID]: "direct",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
await applyActiveProfile();
|
||||||
|
});
|
||||||
|
|
||||||
|
chrome.runtime.onStartup.addListener(async () => {
|
||||||
|
const { profiles } = await chrome.storage.local.get(STORAGE_KEYS.PROFILES);
|
||||||
|
const ensured = ensureProfiles(profiles);
|
||||||
|
await chrome.storage.local.set({ [STORAGE_KEYS.PROFILES]: ensured });
|
||||||
|
await applyActiveProfile();
|
||||||
|
});
|
||||||
|
|
||||||
|
chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
|
||||||
|
(async () => {
|
||||||
|
if (message?.type === "applyProfileById") {
|
||||||
|
await chrome.storage.local.set({
|
||||||
|
[STORAGE_KEYS.ACTIVE_PROFILE_ID]: message.profileId,
|
||||||
|
});
|
||||||
|
await applyActiveProfile();
|
||||||
|
sendResponse({ ok: true });
|
||||||
|
} else if (message?.type === "updateCustomProxy") {
|
||||||
|
const { scheme, host, port, bypassList } = message;
|
||||||
|
const { profiles } = await chrome.storage.local.get(
|
||||||
|
STORAGE_KEYS.PROFILES
|
||||||
|
);
|
||||||
|
const updated = (profiles || []).map((p) =>
|
||||||
|
p.id === "custom"
|
||||||
|
? {
|
||||||
|
...p,
|
||||||
|
type: "fixed",
|
||||||
|
scheme: scheme || "http",
|
||||||
|
host: host || "",
|
||||||
|
port: Number(port) || 0,
|
||||||
|
bypassList: Array.isArray(bypassList)
|
||||||
|
? bypassList
|
||||||
|
: typeof bypassList === "string" && bypassList.trim()
|
||||||
|
? bypassList
|
||||||
|
.split(",")
|
||||||
|
.map((s) => s.trim())
|
||||||
|
.filter(Boolean)
|
||||||
|
: p.bypassList || [],
|
||||||
|
}
|
||||||
|
: p
|
||||||
|
);
|
||||||
|
await chrome.storage.local.set({ [STORAGE_KEYS.PROFILES]: updated });
|
||||||
|
const { activeProfileId } = await chrome.storage.local.get(
|
||||||
|
STORAGE_KEYS.ACTIVE_PROFILE_ID
|
||||||
|
);
|
||||||
|
if (activeProfileId === "custom") {
|
||||||
|
await applyActiveProfile();
|
||||||
|
}
|
||||||
|
sendResponse({ ok: true });
|
||||||
|
} else if (message?.type === "updatePacUrl") {
|
||||||
|
const { profiles } = await chrome.storage.local.get(
|
||||||
|
STORAGE_KEYS.PROFILES
|
||||||
|
);
|
||||||
|
const updated = (profiles || []).map((p) =>
|
||||||
|
p.id === "pac" ? { ...p, pacUrl: message.pacUrl || "" } : p
|
||||||
|
);
|
||||||
|
await chrome.storage.local.set({ [STORAGE_KEYS.PROFILES]: updated });
|
||||||
|
const { activeProfileId } = await chrome.storage.local.get(
|
||||||
|
STORAGE_KEYS.ACTIVE_PROFILE_ID
|
||||||
|
);
|
||||||
|
if (activeProfileId === "pac") {
|
||||||
|
await applyActiveProfile();
|
||||||
|
}
|
||||||
|
sendResponse({ ok: true });
|
||||||
|
} else if (message?.type === "getState") {
|
||||||
|
const { profiles, activeProfileId } = await chrome.storage.local.get([
|
||||||
|
STORAGE_KEYS.PROFILES,
|
||||||
|
STORAGE_KEYS.ACTIVE_PROFILE_ID,
|
||||||
|
]);
|
||||||
|
const ensured = ensureProfiles(profiles);
|
||||||
|
if (ensured !== profiles) {
|
||||||
|
await chrome.storage.local.set({ [STORAGE_KEYS.PROFILES]: ensured });
|
||||||
|
}
|
||||||
|
sendResponse({ ok: true, profiles: ensured, activeProfileId });
|
||||||
|
} else if (message?.type === "resetProfiles") {
|
||||||
|
await chrome.storage.local.set({
|
||||||
|
[STORAGE_KEYS.PROFILES]: DEFAULT_PROFILES,
|
||||||
|
[STORAGE_KEYS.ACTIVE_PROFILE_ID]: "direct",
|
||||||
|
});
|
||||||
|
await applyActiveProfile();
|
||||||
|
sendResponse({ ok: true });
|
||||||
|
}
|
||||||
|
})();
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
|
||||||
|
async function applyActiveProfile() {
|
||||||
|
const { profiles, activeProfileId } = await chrome.storage.local.get([
|
||||||
|
STORAGE_KEYS.PROFILES,
|
||||||
|
STORAGE_KEYS.ACTIVE_PROFILE_ID,
|
||||||
|
]);
|
||||||
|
const profile =
|
||||||
|
(profiles || DEFAULT_PROFILES).find((p) => p.id === activeProfileId) ||
|
||||||
|
DEFAULT_PROFILES[0];
|
||||||
|
const config = buildProxyConfig(profile);
|
||||||
|
// 为避免残留规则影响,先清理再设置
|
||||||
|
await new Promise((resolve) =>
|
||||||
|
chrome.proxy.settings.clear({ scope: "regular" }, resolve)
|
||||||
|
);
|
||||||
|
await new Promise((resolve) => {
|
||||||
|
chrome.proxy.settings.set({ value: config, scope: "regular" }, () => {
|
||||||
|
if (chrome.runtime.lastError) {
|
||||||
|
console.warn("proxy set error:", chrome.runtime.lastError);
|
||||||
|
}
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
await updateBadge(profile);
|
||||||
|
await updateIcon(profile);
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildProxyConfig(profile) {
|
||||||
|
if (profile.type === "direct") {
|
||||||
|
return { mode: "direct" };
|
||||||
|
}
|
||||||
|
if (profile.type === "system") {
|
||||||
|
return { mode: "system" };
|
||||||
|
}
|
||||||
|
if (profile.type === "auto_detect") {
|
||||||
|
return { mode: "auto_detect" };
|
||||||
|
}
|
||||||
|
if (profile.type === "pac") {
|
||||||
|
const url = (profile.pacUrl || "").trim();
|
||||||
|
if (url) {
|
||||||
|
return { mode: "pac_script", pacScript: { url } };
|
||||||
|
}
|
||||||
|
return { mode: "direct" };
|
||||||
|
}
|
||||||
|
if (profile.type === "fixed") {
|
||||||
|
const singleProxy = {
|
||||||
|
scheme: profile.scheme || "http",
|
||||||
|
host: profile.host,
|
||||||
|
port: Number(profile.port) || 0,
|
||||||
|
};
|
||||||
|
if (!singleProxy.host || !singleProxy.port) {
|
||||||
|
// 避免设置非法 fixed 导致联网异常
|
||||||
|
return { mode: "direct" };
|
||||||
|
}
|
||||||
|
const bypassList = Array.isArray(profile.bypassList)
|
||||||
|
? profile.bypassList
|
||||||
|
: [];
|
||||||
|
return {
|
||||||
|
mode: "fixed_servers",
|
||||||
|
rules: { singleProxy, bypassList },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return { mode: "direct" };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateBadge(profile) {
|
||||||
|
// 仅显示图标,不显示任何徽章文字
|
||||||
|
if (chrome.action?.setBadgeText) {
|
||||||
|
await chrome.action.setBadgeText({ text: "" });
|
||||||
|
}
|
||||||
|
if (chrome.action?.setBadgeBackgroundColor) {
|
||||||
|
await chrome.action.setBadgeBackgroundColor({ color: "#00000000" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function updateIcon(profile) {
|
||||||
|
if (!chrome.action?.setIcon) return;
|
||||||
|
const isProxyOn = profile.type !== "direct";
|
||||||
|
const path = isProxyOn ? ICON_FALLBACK_PNG.open : ICON_FALLBACK_PNG.close;
|
||||||
|
try {
|
||||||
|
await chrome.action.setIcon({ path });
|
||||||
|
} catch (e) {
|
||||||
|
console.warn("setIcon failed", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 已移除 SVG 栅格化与程序化绘制,专用 PNG 路径作为图标资源
|
||||||
|
|
||||||
|
function ensureProfiles(existing) {
|
||||||
|
const current = Array.isArray(existing) ? [...existing] : [];
|
||||||
|
const byId = new Map(current.map((p) => [p.id, p]));
|
||||||
|
const need = [
|
||||||
|
"direct",
|
||||||
|
"system",
|
||||||
|
"auto",
|
||||||
|
"local-http",
|
||||||
|
"local-socks5",
|
||||||
|
"pac",
|
||||||
|
"custom",
|
||||||
|
];
|
||||||
|
let changed = false;
|
||||||
|
for (const id of need) {
|
||||||
|
if (!byId.has(id)) {
|
||||||
|
const def = DEFAULT_PROFILES.find((p) => p.id === id);
|
||||||
|
if (def) {
|
||||||
|
current.push(def);
|
||||||
|
byId.set(id, def);
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 修补关键字段
|
||||||
|
const custom = byId.get("custom");
|
||||||
|
if (custom) {
|
||||||
|
if (!custom.scheme) (custom.scheme = "http"), (changed = true);
|
||||||
|
if (typeof custom.port !== "number")
|
||||||
|
(custom.port = Number(custom.port) || 0), (changed = true);
|
||||||
|
if (!Array.isArray(custom.bypassList))
|
||||||
|
(custom.bypassList = ["localhost", "127.0.0.1"]), (changed = true);
|
||||||
|
custom.type = "fixed";
|
||||||
|
}
|
||||||
|
const auto = byId.get("auto");
|
||||||
|
if (auto && auto.type !== "auto_detect") {
|
||||||
|
auto.type = "auto_detect";
|
||||||
|
changed = true;
|
||||||
|
}
|
||||||
|
return changed ? current : current;
|
||||||
|
}
|
||||||
BIN
icons/close-16.png
Normal file
BIN
icons/close-16.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 731 B |
BIN
icons/close-32.png
Normal file
BIN
icons/close-32.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.5 KiB |
51
icons/close.svg
Normal file
51
icons/close.svg
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
<?xml version="1.0" encoding="iso-8859-1"?>
|
||||||
|
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||||
|
<svg height="800px" width="800px" version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||||
|
viewBox="0 0 300.725 300.725" xml:space="preserve">
|
||||||
|
<g>
|
||||||
|
<g id="close-hang-sign-light-shop_1_">
|
||||||
|
<path style="fill:#F78B28;" d="M150.363,176.206c-5.183,0-9.398,4.215-9.398,9.398v28.193c0,5.183,4.215,9.398,9.398,9.398
|
||||||
|
s9.398-4.215,9.398-9.398v-28.193C159.76,180.421,155.546,176.206,150.363,176.206z"/>
|
||||||
|
<path style="fill:#F78B28;" d="M281.93,143.314H28.193c-7.772,0-14.097,6.325-14.097,14.096v84.579
|
||||||
|
c0,7.772,6.325,14.097,14.097,14.097H281.93c7.772,0,14.096-6.325,14.096-14.097V157.41
|
||||||
|
C296.027,149.639,289.702,143.314,281.93,143.314z M84.579,185.604h-9.398v-2.349c0-3.886-3.162-7.048-7.048-7.048
|
||||||
|
c-3.886,0-7.048,3.162-7.048,7.048v32.892c0,3.886,3.162,7.048,7.048,7.048s7.048-3.162,7.048-7.048v-2.349h9.398v2.349
|
||||||
|
c0,9.069-7.377,16.446-16.446,16.446s-16.446-7.377-16.446-16.446v-32.892c0-9.069,7.377-16.446,16.446-16.446
|
||||||
|
s16.446,7.377,16.446,16.446V185.604z M126.869,232.592H98.676c-2.594,0-4.699-2.105-4.699-4.699v-61.085h9.398v56.386h23.494
|
||||||
|
V232.592z M169.158,213.797c0,10.366-8.43,18.795-18.795,18.795s-18.795-8.43-18.795-18.795v-28.193
|
||||||
|
c0-10.366,8.43-18.795,18.795-18.795c10.366,0,18.795,8.43,18.795,18.795V213.797z M211.448,185.604h-9.398v-2.349
|
||||||
|
c0-3.886-3.162-7.048-7.048-7.048s-7.048,3.162-7.048,7.048v4.699c0,3.886,3.162,7.048,7.048,7.048
|
||||||
|
c9.069,0,16.446,7.377,16.446,16.446v4.699c0,9.069-7.377,16.446-16.446,16.446s-16.446-7.377-16.446-16.446v-2.349h9.398v2.349
|
||||||
|
c0,3.886,3.162,7.048,7.048,7.048s7.048-3.162,7.048-7.048v-4.699c0-3.886-3.162-7.048-7.048-7.048
|
||||||
|
c-9.069,0-16.446-7.377-16.446-16.446v-4.699c0-9.069,7.377-16.446,16.446-16.446s16.446,7.377,16.446,16.446V185.604z
|
||||||
|
M253.737,176.206h-23.494v18.795h18.795v9.398h-18.795v18.795h23.494v9.398h-28.193c-2.594,0-4.699-2.105-4.699-4.699v-23.494
|
||||||
|
v-9.398v-23.494c0-2.594,2.105-4.699,4.699-4.699h28.193V176.206z"/>
|
||||||
|
<circle style="fill:#FFFFFF;" cx="155.062" cy="58.735" r="4.699"/>
|
||||||
|
<path d="M190.303,162.11c-9.069,0-16.446,7.377-16.446,16.446v4.699c0,9.069,7.377,16.446,16.446,16.446
|
||||||
|
c3.886,0,7.048,3.162,7.048,7.048v4.699c0,3.886-3.162,7.048-7.048,7.048s-7.048-3.162-7.048-7.048V209.1h-9.398v2.349
|
||||||
|
c0,9.069,7.377,16.446,16.446,16.446s16.446-7.377,16.446-16.446v-4.699c0-9.069-7.377-16.446-16.446-16.446
|
||||||
|
c-3.886,0-7.048-3.162-7.048-7.048v-4.699c0-3.886,3.162-7.048,7.048-7.048s7.048,3.162,7.048,7.048v2.349h9.398v-2.349
|
||||||
|
C206.749,169.487,199.372,162.11,190.303,162.11z"/>
|
||||||
|
<path d="M216.146,166.809v23.494v9.398v23.494c0,2.594,2.105,4.699,4.699,4.699h28.193v-9.398h-23.494V199.7h18.795v-9.398
|
||||||
|
h-18.795v-18.795h23.494v-9.398h-28.193C218.251,162.11,216.146,164.215,216.146,166.809z"/>
|
||||||
|
<path d="M277.231,129.218h-31.327l-86.233-64.675c2.913-2.584,4.788-6.315,4.788-10.507c0-7.772-6.325-14.097-14.097-14.097
|
||||||
|
s-14.096,6.325-14.096,14.097c0,4.191,1.875,7.922,4.788,10.507l-86.233,64.675H23.494C10.539,129.218,0,139.757,0,152.712v84.579
|
||||||
|
c0,12.955,10.539,23.494,23.494,23.494h253.737c12.955,0,23.494-10.539,23.494-23.494v-84.579
|
||||||
|
C300.725,139.757,290.186,129.218,277.231,129.218z M150.363,49.338c2.589,0,4.699,2.11,4.699,4.699
|
||||||
|
c0,2.589-2.11,4.699-4.699,4.699s-4.699-2.11-4.699-4.699C145.664,51.448,147.774,49.338,150.363,49.338z M150.363,69.308
|
||||||
|
l79.88,59.91H70.483L150.363,69.308z M291.328,237.291c0,7.772-6.325,14.097-14.096,14.097H23.494
|
||||||
|
c-7.772,0-14.097-6.325-14.097-14.097v-84.579c0-7.772,6.325-14.097,14.097-14.097h253.737c7.772,0,14.096,6.325,14.096,14.097
|
||||||
|
v84.579H291.328z"/>
|
||||||
|
<path d="M63.434,162.11c-9.069,0-16.446,7.377-16.446,16.446v32.892c0,9.069,7.377,16.446,16.446,16.446
|
||||||
|
s16.446-7.377,16.446-16.446v-2.349h-9.398v2.349c0,3.886-3.162,7.048-7.048,7.048s-7.048-3.162-7.048-7.048v-32.892
|
||||||
|
c0-3.886,3.162-7.048,7.048-7.048s7.048,3.162,7.048,7.048v2.349h9.398v-2.349C79.88,169.487,72.503,162.11,63.434,162.11z"/>
|
||||||
|
<path d="M98.676,162.11h-9.398v61.085c0,2.594,2.105,4.699,4.699,4.699h28.193v-9.398H98.676V162.11z"/>
|
||||||
|
<path d="M145.664,162.11c-10.366,0-18.795,8.43-18.795,18.795v28.193c0,10.366,8.43,18.795,18.795,18.795
|
||||||
|
c10.366,0,18.795-8.43,18.795-18.795v-28.193C164.459,170.54,156.03,162.11,145.664,162.11z M155.062,209.098
|
||||||
|
c0,5.183-4.215,9.398-9.398,9.398s-9.398-4.215-9.398-9.398v-28.193c0-5.183,4.215-9.398,9.398-9.398s9.398,4.215,9.398,9.398
|
||||||
|
V209.098z"/>
|
||||||
|
</g>
|
||||||
|
<g id="Layer_1_5_">
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 4.4 KiB |
BIN
icons/open-16.png
Normal file
BIN
icons/open-16.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 842 B |
BIN
icons/open-32.png
Normal file
BIN
icons/open-32.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.9 KiB |
18
icons/open.svg
Normal file
18
icons/open.svg
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
|
||||||
|
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||||
|
<svg width="800px" height="800px" viewBox="0 -6.33 90.755 90.755" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g id="open_label_door_hanger" data-name="open label door hanger" transform="translate(-458.165 -809.902)">
|
||||||
|
<path id="Path_195" data-name="Path 195" d="M519.038,820.274a7.714,7.714,0,1,1-4.582-9.9A7.718,7.718,0,0,1,519.038,820.274Z" fill="#27b7ff"/>
|
||||||
|
<g id="Group_78" data-name="Group 78">
|
||||||
|
<path id="Path_196" data-name="Path 196" d="M546.082,880.081a9.438,9.438,0,0,1-10.8,7.79l-69.21-11.225a9.446,9.446,0,0,1-7.787-10.8L461,849.107a9.438,9.438,0,0,1,10.8-7.79l69.213,11.229a9.443,9.443,0,0,1,7.787,10.8Z" fill="#a4d322"/>
|
||||||
|
</g>
|
||||||
|
<g id="Group_79" data-name="Group 79">
|
||||||
|
<path id="Path_197" data-name="Path 197" d="M488.881,862.22a7.025,7.025,0,0,1-2.645,4.77,7.823,7.823,0,0,1-10.088-1.638,8.06,8.06,0,0,1,1.644-10.147,7.857,7.857,0,0,1,10.083,1.636A7.015,7.015,0,0,1,488.881,862.22Zm-5.1,2.635a4,4,0,0,0,.971-1.313,6.922,6.922,0,0,0,.557-1.906,6.7,6.7,0,0,0,.055-2.089,3.965,3.965,0,0,0-.5-1.474,2.736,2.736,0,0,0-.925-.955,3.537,3.537,0,0,0-1.2-.444,3.423,3.423,0,0,0-1.267.033,3.009,3.009,0,0,0-1.19.6,3.9,3.9,0,0,0-.95,1.272,6.757,6.757,0,0,0-.615,1.995,6.655,6.655,0,0,0-.058,2.071,4.145,4.145,0,0,0,.5,1.488,2.813,2.813,0,0,0,.923.942,3.142,3.142,0,0,0,1.229.462,3.2,3.2,0,0,0,1.3-.054A2.814,2.814,0,0,0,483.785,864.855Z" fill="#ffffff"/>
|
||||||
|
<path id="Path_198" data-name="Path 198" d="M502.913,861.969a4.98,4.98,0,0,1-.6,1.7,3.95,3.95,0,0,1-1.123,1.292,5.489,5.489,0,0,1-1.975.9,6.505,6.505,0,0,1-2.5.016l-2.138-.353-.708,4.348-3.438-.556,2.165-13.337,5.656.919a8.923,8.923,0,0,1,2.111.566,5.212,5.212,0,0,1,1.438.909,3.585,3.585,0,0,1,1.009,1.548A4.335,4.335,0,0,1,502.913,861.969Zm-3.564-.5a1.6,1.6,0,0,0-.154-1.017,1.71,1.71,0,0,0-.613-.686,3.2,3.2,0,0,0-1.01-.4c-.34-.069-.789-.151-1.354-.244l-.6-.1-.646,3.985.984.16a8.058,8.058,0,0,0,1.472.123,1.985,1.985,0,0,0,1.017-.27,1.584,1.584,0,0,0,.588-.606A2.8,2.8,0,0,0,499.349,861.471Z" fill="#ffffff"/>
|
||||||
|
<path id="Path_199" data-name="Path 199" d="M513.514,873.054l-9.646-1.566,2.163-13.329,9.643,1.563-.417,2.574-6.22-1-.375,2.3,5.775.937-.421,2.579-5.773-.935-.532,3.287,6.222,1.013Z" fill="#ffffff"/>
|
||||||
|
<path id="Path_200" data-name="Path 200" d="M528.519,875.486l-3.313-.536-4.173-10.067-1.489,9.146-3.146-.508,2.16-13.337,4.107.673,3.628,8.428,1.239-7.644,3.152.516Z" fill="#ffffff"/>
|
||||||
|
</g>
|
||||||
|
<path id="Path_201" data-name="Path 201" d="M517.746,822.528a7.749,7.749,0,0,1-4.96,2.744L526.558,850.2l7.121,1.16Zm-13.272-2.47-26.56,22.254,7.4,1.2L508.12,824.4A7.762,7.762,0,0,1,504.474,820.058Z" fill="#f4f4f4"/>
|
||||||
|
<path id="Path_202" data-name="Path 202" d="M477.914,842.312l7.4,1.2,3.768-3.16-7.4-1.2Zm53.478,4.9-7.121-1.152,2.287,4.137,7.121,1.16Z" fill="#c9d1d1"/>
|
||||||
|
</g>
|
||||||
|
After Width: | Height: | Size: 2.9 KiB |
14
manifest.json
Normal file
14
manifest.json
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
{
|
||||||
|
"manifest_version": 3,
|
||||||
|
"name": "Proxy Switcher",
|
||||||
|
"version": "1.0.1",
|
||||||
|
"description": "一键切换 Chrome 代理",
|
||||||
|
"permissions": ["proxy", "storage"],
|
||||||
|
"background": {
|
||||||
|
"service_worker": "background.js"
|
||||||
|
},
|
||||||
|
"action": {
|
||||||
|
"default_popup": "popup.html",
|
||||||
|
"default_title": "Proxy Switcher"
|
||||||
|
}
|
||||||
|
}
|
||||||
158
popup.css
Normal file
158
popup.css
Normal file
@ -0,0 +1,158 @@
|
|||||||
|
:root {
|
||||||
|
--bg: #ffffff;
|
||||||
|
--card: #f8fafc;
|
||||||
|
--text: #0f172a;
|
||||||
|
--muted: #64748b;
|
||||||
|
--line: #e2e8f0;
|
||||||
|
--accent: #6366f1;
|
||||||
|
--accent-2: #3b82f6;
|
||||||
|
--focus: rgba(99, 102, 241, 0.35);
|
||||||
|
--shadow: 0 6px 20px rgba(2, 6, 23, 0.06), 0 1px 3px rgba(2, 6, 23, 0.06);
|
||||||
|
--radius: 12px;
|
||||||
|
--radius-sm: 10px;
|
||||||
|
--ring: 0 0 0 3px var(--focus);
|
||||||
|
}
|
||||||
|
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
background: var(--bg);
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
font: 13px/1.5 system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial,
|
||||||
|
"PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
.popup {
|
||||||
|
width: 340px;
|
||||||
|
max-width: 380px;
|
||||||
|
min-width: 320px;
|
||||||
|
padding: 14px 14px 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
.title {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
.title h1 {
|
||||||
|
font-size: 14px;
|
||||||
|
margin: 0;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
.logo {
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: linear-gradient(135deg, var(--accent), var(--accent-2));
|
||||||
|
box-shadow: 0 2px 8px rgba(99, 102, 241, 0.35);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 移除未使用的标签页样式 */
|
||||||
|
|
||||||
|
.main {
|
||||||
|
display: grid;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field {
|
||||||
|
display: grid;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
.field-label {
|
||||||
|
font-size: 12px;
|
||||||
|
color: var(--muted);
|
||||||
|
}
|
||||||
|
.hint {
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
select,
|
||||||
|
input[type="text"],
|
||||||
|
input[type="number"] {
|
||||||
|
padding: 6px 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
transition: border-color 0.2s ease, box-shadow 0.2s ease, background 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-btn {
|
||||||
|
border: 0;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--accent);
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 6px 8px;
|
||||||
|
border-radius: 8px;
|
||||||
|
}
|
||||||
|
.text-btn:hover {
|
||||||
|
background: rgba(99, 102, 241, 0.1);
|
||||||
|
}
|
||||||
|
.text-btn:focus-visible {
|
||||||
|
outline: none;
|
||||||
|
box-shadow: var(--ring);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 自定义下拉包装 */
|
||||||
|
.select {
|
||||||
|
position: relative;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
background: #fff;
|
||||||
|
border: 1px solid var(--line);
|
||||||
|
border-radius: var(--radius-sm);
|
||||||
|
padding: 0;
|
||||||
|
transition: border-color 0.2s ease, box-shadow 0.2s ease, background 0.2s ease;
|
||||||
|
}
|
||||||
|
.select:focus-within {
|
||||||
|
border-color: var(--accent);
|
||||||
|
box-shadow: var(--ring);
|
||||||
|
}
|
||||||
|
.select select {
|
||||||
|
appearance: none;
|
||||||
|
-webkit-appearance: none;
|
||||||
|
-moz-appearance: none;
|
||||||
|
border: 0;
|
||||||
|
outline: none;
|
||||||
|
padding: 6px 26px 6px 8px;
|
||||||
|
background: transparent;
|
||||||
|
font-size: 12px;
|
||||||
|
}
|
||||||
|
.select .chevron {
|
||||||
|
position: absolute;
|
||||||
|
right: 8px;
|
||||||
|
color: var(--muted);
|
||||||
|
pointer-events: none;
|
||||||
|
transition: transform 0.2s ease, color 0.2s ease;
|
||||||
|
}
|
||||||
|
.select.open .chevron {
|
||||||
|
color: var(--accent);
|
||||||
|
transform: translateY(1px) rotate(180deg);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 移除未使用的 Quick Save 样式 */
|
||||||
|
/* 移除未使用的按钮/状态样式 */
|
||||||
|
|
||||||
|
/* 移除未使用的 Toast 样式 */
|
||||||
|
|
||||||
|
/* 移除未使用的动画 */
|
||||||
125
popup.html
Normal file
125
popup.html
Normal file
@ -0,0 +1,125 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="zh">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title>Proxy Switcher</title>
|
||||||
|
<link rel="stylesheet" href="popup.css" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="popup">
|
||||||
|
<header class="header">
|
||||||
|
<div class="title">
|
||||||
|
<span class="logo" aria-hidden="true"></span>
|
||||||
|
<h1>Proxy Switcher</h1>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
<main class="main">
|
||||||
|
<section class="panel" role="region" aria-label="代理设置">
|
||||||
|
<label class="field">
|
||||||
|
<span class="field-label">代理配置</span>
|
||||||
|
<div class="row">
|
||||||
|
<div class="select">
|
||||||
|
<select id="profileSelect"></select>
|
||||||
|
|
||||||
|
<svg
|
||||||
|
class="chevron"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
width="14"
|
||||||
|
height="14"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M6 9l6 6 6-6"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
fill="none"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
<button id="applyBtn" class="text-btn">应用</button>
|
||||||
|
</div>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<!-- <div class="row">
|
||||||
|
<button id="resetBtn" class="text-btn" title="恢复默认配置">
|
||||||
|
重置配置
|
||||||
|
</button>
|
||||||
|
</div> -->
|
||||||
|
|
||||||
|
<div id="pacBlock" style="display: none">
|
||||||
|
<div class="row">
|
||||||
|
<input
|
||||||
|
id="pacUrl"
|
||||||
|
type="text"
|
||||||
|
placeholder="PAC URL,如 https://example.com/proxy.pac"
|
||||||
|
/>
|
||||||
|
<button id="savePac" class="text-btn">保存</button>
|
||||||
|
</div>
|
||||||
|
<div class="hint">当选择 PAC 模式时,可填写 PAC 文件地址。</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="customBlock" style="display: none">
|
||||||
|
<div class="row">
|
||||||
|
<label class="field-label" style="min-width: 40px">协议</label>
|
||||||
|
<div class="select">
|
||||||
|
<select id="customScheme">
|
||||||
|
<option value="http">HTTP</option>
|
||||||
|
<option value="https">HTTPS</option>
|
||||||
|
<option value="socks5">SOCKS5</option>
|
||||||
|
<option value="socks4">SOCKS4</option>
|
||||||
|
</select>
|
||||||
|
<svg
|
||||||
|
class="chevron"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
width="14"
|
||||||
|
height="14"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M6 9l6 6 6-6"
|
||||||
|
stroke="currentColor"
|
||||||
|
stroke-width="2"
|
||||||
|
fill="none"
|
||||||
|
stroke-linecap="round"
|
||||||
|
stroke-linejoin="round"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<input
|
||||||
|
id="customHost"
|
||||||
|
type="text"
|
||||||
|
placeholder="代理地址,如 127.0.0.1"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
id="customPort"
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
max="65535"
|
||||||
|
placeholder="端口,如 7890"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<input
|
||||||
|
id="customBypass"
|
||||||
|
type="text"
|
||||||
|
placeholder="不走代理(逗号分隔),如 localhost, 127.0.0.1"
|
||||||
|
/>
|
||||||
|
<button id="saveCustom" class="text-btn">保存</button>
|
||||||
|
</div>
|
||||||
|
<!-- <div class="hint">
|
||||||
|
“自定义代理”会保存到本地,点击“应用”即可立即启用。
|
||||||
|
</div> -->
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="popup.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
123
popup.js
Normal file
123
popup.js
Normal file
@ -0,0 +1,123 @@
|
|||||||
|
// ========== 代理切换逻辑 ==========
|
||||||
|
const els = {
|
||||||
|
profileSelect: document.getElementById("profileSelect"),
|
||||||
|
applyBtn: document.getElementById("applyBtn"),
|
||||||
|
pacBlock: document.getElementById("pacBlock"),
|
||||||
|
pacUrl: document.getElementById("pacUrl"),
|
||||||
|
savePac: document.getElementById("savePac"),
|
||||||
|
customBlock: document.getElementById("customBlock"),
|
||||||
|
customScheme: document.getElementById("customScheme"),
|
||||||
|
customHost: document.getElementById("customHost"),
|
||||||
|
customPort: document.getElementById("customPort"),
|
||||||
|
customBypass: document.getElementById("customBypass"),
|
||||||
|
saveCustom: document.getElementById("saveCustom"),
|
||||||
|
// resetBtn: document.getElementById("resetBtn"),
|
||||||
|
};
|
||||||
|
|
||||||
|
// 下拉箭头展开/收起状态同步
|
||||||
|
function bindSelectOpenState(selectEl) {
|
||||||
|
if (!selectEl) return;
|
||||||
|
const wrapper = selectEl.closest(".select");
|
||||||
|
if (!wrapper) return;
|
||||||
|
// 点击时认为打开
|
||||||
|
selectEl.addEventListener("mousedown", () => wrapper.classList.add("open"));
|
||||||
|
// 获得焦点也可视为打开
|
||||||
|
selectEl.addEventListener("focus", () => wrapper.classList.add("open"));
|
||||||
|
// 失焦或变更后关闭
|
||||||
|
selectEl.addEventListener("blur", () => wrapper.classList.remove("open"));
|
||||||
|
selectEl.addEventListener("change", () => wrapper.classList.remove("open"));
|
||||||
|
}
|
||||||
|
|
||||||
|
bindSelectOpenState(els.profileSelect);
|
||||||
|
bindSelectOpenState(els.customScheme);
|
||||||
|
|
||||||
|
init();
|
||||||
|
|
||||||
|
async function init() {
|
||||||
|
const state = await sendMessage({ type: "getState" });
|
||||||
|
const profiles = state.profiles || [];
|
||||||
|
const activeProfileId = state.activeProfileId || "direct";
|
||||||
|
|
||||||
|
// 填充下拉
|
||||||
|
els.profileSelect.innerHTML = "";
|
||||||
|
for (const p of profiles) {
|
||||||
|
const opt = document.createElement("option");
|
||||||
|
opt.value = p.id;
|
||||||
|
opt.textContent = p.name;
|
||||||
|
if (p.id === activeProfileId) opt.selected = true;
|
||||||
|
els.profileSelect.appendChild(opt);
|
||||||
|
}
|
||||||
|
|
||||||
|
toggleBlocks();
|
||||||
|
|
||||||
|
// 回填 PAC、自定义
|
||||||
|
const pacProfile = profiles.find((p) => p.id === "pac");
|
||||||
|
if (els.pacUrl) els.pacUrl.value = pacProfile?.pacUrl || "";
|
||||||
|
|
||||||
|
const customProfile = profiles.find((p) => p.id === "custom");
|
||||||
|
if (customProfile) {
|
||||||
|
if (els.customScheme)
|
||||||
|
els.customScheme.value = customProfile.scheme || "http";
|
||||||
|
if (els.customHost) els.customHost.value = customProfile.host || "";
|
||||||
|
if (els.customPort) els.customPort.value = customProfile.port || "";
|
||||||
|
if (els.customBypass)
|
||||||
|
els.customBypass.value = (customProfile.bypassList || []).join(", ");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
els.profileSelect?.addEventListener("change", toggleBlocks);
|
||||||
|
|
||||||
|
function toggleBlocks() {
|
||||||
|
const mode = els.profileSelect?.value;
|
||||||
|
const isPac = mode === "pac";
|
||||||
|
const isCustom = mode === "custom";
|
||||||
|
if (els.pacBlock) els.pacBlock.style.display = isPac ? "block" : "none";
|
||||||
|
if (els.customBlock)
|
||||||
|
els.customBlock.style.display = isCustom ? "block" : "none";
|
||||||
|
}
|
||||||
|
|
||||||
|
els.applyBtn?.addEventListener("click", async () => {
|
||||||
|
const profileId = els.profileSelect.value;
|
||||||
|
if (profileId === "custom") {
|
||||||
|
await sendMessage({
|
||||||
|
type: "updateCustomProxy",
|
||||||
|
scheme: els.customScheme?.value,
|
||||||
|
host: els.customHost?.value.trim(),
|
||||||
|
port: Number(els.customPort?.value) || 0,
|
||||||
|
bypassList: (els.customBypass?.value || "")
|
||||||
|
.split(",")
|
||||||
|
.map((s) => s.trim())
|
||||||
|
.filter(Boolean),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
await sendMessage({ type: "applyProfileById", profileId });
|
||||||
|
window.close();
|
||||||
|
});
|
||||||
|
|
||||||
|
els.savePac?.addEventListener("click", async () => {
|
||||||
|
const pacUrl = (els.pacUrl?.value || "").trim();
|
||||||
|
await sendMessage({ type: "updatePacUrl", pacUrl });
|
||||||
|
});
|
||||||
|
|
||||||
|
els.saveCustom?.addEventListener("click", async () => {
|
||||||
|
await sendMessage({
|
||||||
|
type: "updateCustomProxy",
|
||||||
|
scheme: els.customScheme?.value,
|
||||||
|
host: els.customHost?.value.trim(),
|
||||||
|
port: Number(els.customPort?.value) || 0,
|
||||||
|
bypassList: (els.customBypass?.value || "")
|
||||||
|
.split(",")
|
||||||
|
.map((s) => s.trim())
|
||||||
|
.filter(Boolean),
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// 重置按钮已移除,如需恢复可取消上方注释并启用以下代码
|
||||||
|
// els.resetBtn?.addEventListener("click", async () => {
|
||||||
|
// await sendMessage({ type: "resetProfiles" });
|
||||||
|
// await init();
|
||||||
|
// });
|
||||||
|
|
||||||
|
function sendMessage(msg) {
|
||||||
|
return new Promise((resolve) => chrome.runtime.sendMessage(msg, resolve));
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user