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