mirror of
https://github.com/yaklang/yaklang-chrome-extension.git
synced 2026-09-21 19:10:41 +08:00
init wxt framework
This commit is contained in:
@@ -0,0 +1,578 @@
|
||||
---
|
||||
description: 使用 WXT、React 和 TypeScript 构建代理管理扩展的指南
|
||||
globs:
|
||||
alwaysApply: false
|
||||
---
|
||||
---
|
||||
description: 使用 WXT、React 和 TypeScript 构建代理管理扩展的指南
|
||||
globs: "**/*.ts, **/*.tsx, **/*.js, **/*.jsx"
|
||||
---
|
||||
|
||||
## 概览
|
||||
|
||||
](https://wxt.dev/) 是一个为浏览器扩展开发提供现代开发体验的框架。本指南将帮助您使用 WXT、React 和 TypeScript 重构类似 SwitchyOmega 的代理管理扩展。
|
||||
|
||||
## 项目结构
|
||||
|
||||
推荐使用以下项目结构:
|
||||
```
|
||||
.
|
||||
├── .output/
|
||||
├── .wxt/
|
||||
├── modules/
|
||||
├── public/ # 包含要复制到输出文件夹的任何文件,而无需WXT处理
|
||||
├── ord/ # 需要重构的旧代码
|
||||
├── src/
|
||||
│ │ ├── assets/
|
||||
│ │ ├── components/
|
||||
│ │ ├── composables/
|
||||
│ │ ├── entrypoints/ # 包含所有被捆绑到扩展名的入口点
|
||||
│ │ ├── hooks/ # 默认自动导入,包含项目用于 React 和 Solid 的钩子的源代码
|
||||
│ │ ├── utils/
|
||||
├── .env
|
||||
├── .env.publish
|
||||
├── app.config.ts
|
||||
├── package.json
|
||||
├── tsconfig.json
|
||||
├── web-ext.config.ts # 配置浏览器启动
|
||||
├── wxt.config.ts # WXT项目的主要配置文件
|
||||
|
||||
```
|
||||
|
||||
|
||||
Different browsers provide different global variables for accessing the extension APIs (chrome provides chrome, firefox provides browser, etc).
|
||||
|
||||
WXT merges these two into a unified API accessed through the browser variable.
|
||||
|
||||
```
|
||||
import { browser } from 'wxt/browser';
|
||||
|
||||
browser.action.onClicked.addListener(() => {
|
||||
// ...
|
||||
});
|
||||
```
|
||||
TIP
|
||||
|
||||
With auto-imports enabled, you don't even need to import this variable from wxt/browser!
|
||||
|
||||
The browser variable WXT provides is a simple export of the browser or chrome globals provided by the browser at runtime:
|
||||
|
||||
|
||||
export const browser = globalThis.browser?.runtime?.id
|
||||
? globalThis.browser
|
||||
: globalThis.chrome;
|
||||
This means you can use the promise-style API for both MV2 and MV3, and it will work across all browsers (Chromium, Firefox, Safari, etc).
|
||||
|
||||
Accessing Types
|
||||
All types can be accessed via WXT's Browser namespace:
|
||||
|
||||
```
|
||||
import { type Browser } from 'wxt/browser';
|
||||
|
||||
function handleMessage(message: any, sender: Browser.runtime.MessageSender) {
|
||||
// ...
|
||||
}
|
||||
```
|
||||
|
||||
## 入口点设置
|
||||
|
||||
### 后台脚本
|
||||
|
||||
```typescript
|
||||
// entrypoints/background/index.ts
|
||||
import { defineBackground } from 'wxt/background';
|
||||
import { setupProxyManagement } from './proxy';
|
||||
|
||||
export default defineBackground({
|
||||
// 设置清单选项
|
||||
type: 'module',
|
||||
|
||||
main() {
|
||||
// 初始化代理管理
|
||||
setupProxyManagement();
|
||||
|
||||
// 监听消息
|
||||
browser.runtime.onMessage.addListener((message, sender) => {
|
||||
if (message.type === 'SWITCH_PROXY') {
|
||||
return handleProxySwitch(message.proxyId);
|
||||
}
|
||||
});
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
### 弹出窗口
|
||||
|
||||
```html
|
||||
<!-- entrypoints/popup/index.html -->
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>代理切换器</title>
|
||||
<meta
|
||||
name="manifest.default_icon"
|
||||
content="{
|
||||
16: '/icon-16.png',
|
||||
48: '/icon-48.png'
|
||||
}"
|
||||
/>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="./index.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
```tsx
|
||||
// entrypoints/popup/index.tsx
|
||||
import React from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import App from './App';
|
||||
|
||||
const root = createRoot(document.getElementById('app')!);
|
||||
root.render(<App />);
|
||||
```
|
||||
|
||||
```tsx
|
||||
// entrypoints/popup/App.tsx
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import ProxySelector from '../../components/ProxySelector';
|
||||
import { getProxyList, getCurrentProxy } from '../../utils/proxy';
|
||||
import type { Proxy } from '../../types';
|
||||
|
||||
const App: React.FC = () => {
|
||||
const [proxies, setProxies] = useState<Proxy[]>([]);
|
||||
const [currentProxy, setCurrentProxy] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const loadData = async () => {
|
||||
const proxyList = await getProxyList();
|
||||
const current = await getCurrentProxy();
|
||||
setProxies(proxyList);
|
||||
setCurrentProxy(current);
|
||||
};
|
||||
|
||||
loadData();
|
||||
}, []);
|
||||
|
||||
const handleProxyChange = async (proxyId: string) => {
|
||||
await browser.runtime.sendMessage({ type: 'SWITCH_PROXY', proxyId });
|
||||
setCurrentProxy(proxyId);
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="popup">
|
||||
<h1>代理切换器</h1>
|
||||
<ProxySelector
|
||||
proxies={proxies}
|
||||
currentProxy={currentProxy}
|
||||
onChange={handleProxyChange}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default App;
|
||||
```
|
||||
|
||||
### 选项页面
|
||||
|
||||
```html
|
||||
<!-- entrypoints/options/index.html -->
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>代理切换器设置</title>
|
||||
<meta name="manifest.open_in_tab" content="true" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="./index.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
```tsx
|
||||
// entrypoints/options/App.tsx
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { getProxyList, saveProxy, deleteProxy } from '../../utils/proxy';
|
||||
import type { Proxy } from '../../types';
|
||||
|
||||
const App: React.FC = () => {
|
||||
const [proxies, setProxies] = useState<Proxy[]>([]);
|
||||
const [newProxy, setNewProxy] = useState<Partial<Proxy>>({
|
||||
name: '',
|
||||
host: '',
|
||||
port: '',
|
||||
protocol: 'http'
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
loadProxies();
|
||||
}, []);
|
||||
|
||||
const loadProxies = async () => {
|
||||
const list = await getProxyList();
|
||||
setProxies(list);
|
||||
};
|
||||
|
||||
const handleSaveProxy = async () => {
|
||||
if (!newProxy.name || !newProxy.host || !newProxy.port) return;
|
||||
|
||||
await saveProxy(newProxy as Proxy);
|
||||
loadProxies();
|
||||
setNewProxy({
|
||||
name: '',
|
||||
host: '',
|
||||
port: '',
|
||||
protocol: 'http'
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="options">
|
||||
<h1>代理管理器设置</h1>
|
||||
|
||||
<div className="proxy-list">
|
||||
{proxies.map(proxy => (
|
||||
<div key={proxy.id} className="proxy-item">
|
||||
<span>{proxy.name} ({proxy.protocol}://{proxy.host}:{proxy.port})</span>
|
||||
<button onClick={() => deleteProxy(proxy.id)}>删除</button>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="add-proxy">
|
||||
<h2>添加新代理</h2>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="名称"
|
||||
value={newProxy.name}
|
||||
onChange={e => setNewProxy({...newProxy, name: e.target.value})}
|
||||
/>
|
||||
<select
|
||||
value={newProxy.protocol}
|
||||
onChange={e => setNewProxy({...newProxy, protocol: e.target.value})}
|
||||
>
|
||||
<option value="http">HTTP</option>
|
||||
<option value="https">HTTPS</option>
|
||||
<option value="socks4">SOCKS4</option>
|
||||
<option value="socks5">SOCKS5</option>
|
||||
</select>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="主机"
|
||||
value={newProxy.host}
|
||||
onChange={e => setNewProxy({...newProxy, host: e.target.value})}
|
||||
/>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="端口"
|
||||
value={newProxy.port}
|
||||
onChange={e => setNewProxy({...newProxy, port: e.target.value})}
|
||||
/>
|
||||
<button onClick={handleSaveProxy}>保存</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default App;
|
||||
```
|
||||
|
||||
### 内容脚本
|
||||
|
||||
```typescript
|
||||
// entrypoints/content.ts
|
||||
import { defineContentScript } from 'wxt/content-script';
|
||||
|
||||
export default defineContentScript({
|
||||
matches: ['<all_urls>'],
|
||||
|
||||
main() {
|
||||
// 在页面中执行的内容脚本逻辑
|
||||
console.log('代理切换器内容脚本已加载');
|
||||
|
||||
// 根据需要与后台脚本通信
|
||||
browser.runtime.sendMessage({ type: 'CONTENT_SCRIPT_LOADED' });
|
||||
},
|
||||
});
|
||||
```
|
||||
|
||||
## 最佳实践
|
||||
|
||||
1. **使用 WXT 存储模块**: 利用 `@wxt-dev/storage` 管理扩展数据。
|
||||
|
||||
```typescript
|
||||
// 安装: npm install @wxt-dev/storage
|
||||
|
||||
// utils/storage.ts
|
||||
import { createStorage } from '@wxt-dev/storage';
|
||||
|
||||
export const storage = createStorage({
|
||||
proxies: {
|
||||
defaultValue: [],
|
||||
schema: z.array(z.object({
|
||||
id: z.string(),
|
||||
name: z.string(),
|
||||
protocol: z.enum(['http', 'https', 'socks4', 'socks5']),
|
||||
host: z.string(),
|
||||
port: z.string()
|
||||
}))
|
||||
},
|
||||
currentProxyId: {
|
||||
defaultValue: null,
|
||||
schema: z.string().nullable()
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
2. **组件化开发**: 创建可重用的React组件。
|
||||
|
||||
```tsx
|
||||
// components/ProxySelector.tsx
|
||||
import React from 'react';
|
||||
import type { Proxy } from '../types';
|
||||
|
||||
interface ProxySelectorProps {
|
||||
proxies: Proxy[];
|
||||
currentProxy: string | null;
|
||||
onChange: (proxyId: string) => void;
|
||||
}
|
||||
|
||||
const ProxySelector: React.FC<ProxySelectorProps> = ({ proxies, currentProxy, onChange }) => {
|
||||
return (
|
||||
<div className="proxy-selector">
|
||||
{proxies.map(proxy => (
|
||||
<div
|
||||
key={proxy.id}
|
||||
className={`proxy-item ${currentProxy === proxy.id ? 'active' : ''}`}
|
||||
onClick={() => onChange(proxy.id)}
|
||||
>
|
||||
{proxy.name}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProxySelector;
|
||||
```
|
||||
|
||||
3. **类型安全**: 为所有对象定义TypeScript接口。
|
||||
|
||||
```typescript
|
||||
// types/index.ts
|
||||
export interface Proxy {
|
||||
id: string;
|
||||
name: string;
|
||||
protocol: 'http' | 'https' | 'socks4' | 'socks5';
|
||||
host: string;
|
||||
port: string;
|
||||
username?: string;
|
||||
password?: string;
|
||||
}
|
||||
|
||||
export interface ProxyRule {
|
||||
id: string;
|
||||
name: string;
|
||||
pattern: string;
|
||||
proxyId: string;
|
||||
}
|
||||
```
|
||||
|
||||
4. **使用环境变量**: 为不同环境配置不同的设置。
|
||||
|
||||
```typescript
|
||||
// wxt.config.ts
|
||||
import { defineConfig } from 'wxt';
|
||||
|
||||
export default defineConfig({
|
||||
manifest: {
|
||||
name: process.env.NODE_ENV === 'development' ? '[DEV] 代理切换器' : '代理切换器',
|
||||
version: '1.0.0',
|
||||
description: '一个强大的浏览器代理管理扩展',
|
||||
},
|
||||
// 其他配置...
|
||||
});
|
||||
```
|
||||
|
||||
5. **消息通信**: 使用结构化消息系统。
|
||||
|
||||
```typescript
|
||||
// utils/messaging.ts
|
||||
export type MessageType =
|
||||
| { type: 'SWITCH_PROXY'; proxyId: string }
|
||||
| { type: 'GET_CURRENT_PROXY' }
|
||||
| { type: 'PROXY_CHANGED'; proxyId: string };
|
||||
|
||||
export function sendMessage<T extends MessageType>(message: T): Promise<any> {
|
||||
return browser.runtime.sendMessage(message);
|
||||
}
|
||||
```
|
||||
|
||||
6. **图标状态管理**: 根据当前代理状态更新扩展图标。
|
||||
|
||||
```typescript
|
||||
// background/proxy.ts
|
||||
function updateExtensionIcon(proxyId: string | null) {
|
||||
const iconPath = proxyId
|
||||
? '/icons/proxy-active.png'
|
||||
: '/icons/proxy-inactive.png';
|
||||
|
||||
browser.action.setIcon({ path: iconPath });
|
||||
}
|
||||
```
|
||||
|
||||
7. **错误处理**: 实现良好的错误捕获和报告。
|
||||
|
||||
```typescript
|
||||
// utils/error.ts
|
||||
export async function executeWithErrorHandling<T>(
|
||||
fn: () => Promise<T>,
|
||||
errorMessage = '执行操作时出错'
|
||||
): Promise<T | null> {
|
||||
try {
|
||||
return await fn();
|
||||
} catch (error) {
|
||||
console.error(`${errorMessage}:`, error);
|
||||
browser.notifications.create({
|
||||
type: 'basic',
|
||||
iconUrl: '/icon-48.png',
|
||||
title: '代理切换器错误',
|
||||
message: errorMessage
|
||||
});
|
||||
return null;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
8. **使用现代钩子**: 为React组件编写自定义钩子。
|
||||
|
||||
```typescript
|
||||
// hooks/useProxies.ts
|
||||
import { useState, useEffect } from 'react';
|
||||
import { storage } from '../utils/storage';
|
||||
import type { Proxy } from '../types';
|
||||
|
||||
export function useProxies() {
|
||||
const [proxies, setProxies] = useState<Proxy[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
const load = async () => {
|
||||
const data = await storage.proxies.get();
|
||||
setProxies(data);
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
load();
|
||||
|
||||
return storage.proxies.subscribe(newProxies => {
|
||||
setProxies(newProxies);
|
||||
});
|
||||
}, []);
|
||||
|
||||
return { proxies, loading };
|
||||
}
|
||||
```
|
||||
|
||||
## 版本兼容性
|
||||
|
||||
本指南适用于:
|
||||
- WXT v0.20.0 及以上
|
||||
- React 18+
|
||||
- TypeScript 5.0+
|
||||
|
||||
## 扩展功能实现
|
||||
|
||||
### 代理管理功能
|
||||
|
||||
```typescript
|
||||
// utils/proxy.ts
|
||||
import { storage } from './storage';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import type { Proxy } from '../types';
|
||||
|
||||
export async function getProxyList(): Promise<Proxy[]> {
|
||||
return await storage.proxies.get();
|
||||
}
|
||||
|
||||
export async function getCurrentProxy(): Promise<string | null> {
|
||||
return await storage.currentProxyId.get();
|
||||
}
|
||||
|
||||
export async function switchProxy(proxyId: string | null): Promise<void> {
|
||||
// 更新存储
|
||||
await storage.currentProxyId.set(proxyId);
|
||||
|
||||
if (!proxyId) {
|
||||
// 清除代理
|
||||
await browser.proxy.settings.clear({});
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取代理详情
|
||||
const proxies = await storage.proxies.get();
|
||||
const proxy = proxies.find(p => p.id === proxyId);
|
||||
|
||||
if (!proxy) return;
|
||||
|
||||
// 设置代理
|
||||
await browser.proxy.settings.set({
|
||||
value: {
|
||||
mode: 'fixed_servers',
|
||||
rules: {
|
||||
proxyForHttp: {
|
||||
scheme: proxy.protocol,
|
||||
host: proxy.host,
|
||||
port: parseInt(proxy.port)
|
||||
},
|
||||
proxyForHttps: {
|
||||
scheme: proxy.protocol,
|
||||
host: proxy.host,
|
||||
port: parseInt(proxy.port)
|
||||
}
|
||||
}
|
||||
},
|
||||
scope: 'regular'
|
||||
});
|
||||
}
|
||||
|
||||
export async function saveProxy(proxy: Omit<Proxy, 'id'>): Promise<Proxy> {
|
||||
const newProxy: Proxy = {
|
||||
...proxy,
|
||||
id: uuidv4()
|
||||
};
|
||||
|
||||
const proxies = await storage.proxies.get();
|
||||
await storage.proxies.set([...proxies, newProxy]);
|
||||
|
||||
return newProxy;
|
||||
}
|
||||
|
||||
export async function deleteProxy(proxyId: string): Promise<void> {
|
||||
const proxies = await storage.proxies.get();
|
||||
await storage.proxies.set(proxies.filter(p => p.id !== proxyId));
|
||||
|
||||
// 如果删除的是当前使用的代理,清除当前代理
|
||||
const currentProxyId = await storage.currentProxyId.get();
|
||||
if (currentProxyId === proxyId) {
|
||||
await storage.currentProxyId.set(null);
|
||||
await browser.proxy.settings.clear({});
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 相关资源
|
||||
|
||||
- [WXT 官方文档](mdc:https:/wxt.dev)
|
||||
- [WXT GitHub 仓库](mdc:https:/github.com/wxt-dev/wxt)
|
||||
- [Chrome 扩展 API 文档](mdc:https:/developer.chrome.com/docs/extensions/reference)
|
||||
- [React 文档](mdc:https:/reactjs.org)
|
||||
- [TypeScript 文档](mdc:https:/www.typescriptlang.org)
|
||||
+24
-20
@@ -3,28 +3,32 @@ build.crx
|
||||
build.zip
|
||||
build.pem
|
||||
.idea/
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# production
|
||||
/build
|
||||
/2.5.21_0
|
||||
# misc
|
||||
.DS_Store
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
# Logs
|
||||
logs
|
||||
*.log
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
pnpm-debug.log*
|
||||
lerna-debug.log*
|
||||
|
||||
node_modules
|
||||
.output
|
||||
stats.html
|
||||
stats-*.json
|
||||
.wxt
|
||||
web-ext.config.ts
|
||||
|
||||
# Editor directories and files
|
||||
.vscode/*
|
||||
!.vscode/extensions.json
|
||||
.idea
|
||||
.DS_Store
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
||||
*.sw?
|
||||
|
||||
|
||||
ord/
|
||||
@@ -0,0 +1,3 @@
|
||||
# WXT + React
|
||||
|
||||
This template should help get you started developing with React in WXT.
|
||||
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"name": "yakit-chrome-client",
|
||||
"description": "Yakit Browser Extension",
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "wxt",
|
||||
"dev:firefox": "wxt -b firefox",
|
||||
"build": "wxt build",
|
||||
"build:firefox": "wxt build -b firefox",
|
||||
"zip": "wxt zip",
|
||||
"zip:firefox": "wxt zip -b firefox",
|
||||
"compile": "tsc --noEmit",
|
||||
"postinstall": "wxt prepare"
|
||||
},
|
||||
"dependencies": {
|
||||
"@ant-design/icons": "^6.0.0",
|
||||
"antd": "^5.24.6",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0",
|
||||
"uuid": "^11.1.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^19.1.0",
|
||||
"@types/react-dom": "^19.1.2",
|
||||
"@types/uuid": "^10.0.0",
|
||||
"@wxt-dev/module-react": "^1.1.3",
|
||||
"typescript": "^5.8.3",
|
||||
"wxt": "^0.20.0"
|
||||
}
|
||||
}
|
||||
Generated
+5408
File diff suppressed because it is too large
Load Diff
Binary file not shown.
|
After Width: | Height: | Size: 6.5 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.9 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 3.0 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 50 KiB |
File diff suppressed because one or more lines are too long
|
After Width: | Height: | Size: 36 KiB |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>
|
||||
|
After Width: | Height: | Size: 4.0 KiB |
@@ -0,0 +1,235 @@
|
||||
.proxy-container {
|
||||
min-width: 200px;
|
||||
background: white;
|
||||
}
|
||||
|
||||
.proxy-menu {
|
||||
border: none !important;
|
||||
box-shadow: none !important;
|
||||
}
|
||||
|
||||
.menu-item {
|
||||
height: 40px !important;
|
||||
line-height: 40px !important;
|
||||
margin: 0 !important;
|
||||
padding: 0 16px !important;
|
||||
}
|
||||
|
||||
.menu-item .anticon {
|
||||
font-size: 16px;
|
||||
color: var(--yakit-primary);
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.menu-item-label {
|
||||
font-size: 14px;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.menu-item:hover {
|
||||
background-color: var(--yakit-primary-5) !important;
|
||||
}
|
||||
|
||||
.menu-item:hover .anticon,
|
||||
.menu-item:hover .menu-item-label {
|
||||
color: var(--yakit-primary) !important;
|
||||
}
|
||||
|
||||
/* 选中状态 */
|
||||
.menu-item.ant-menu-item-selected {
|
||||
background-color: var(--yakit-primary) !important;
|
||||
}
|
||||
|
||||
.menu-item.ant-menu-item-selected .anticon,
|
||||
.menu-item.ant-menu-item-selected .menu-item-label {
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
.menu-item.ant-menu-item-selected:hover {
|
||||
background-color: var(--yakit-primary-hover) !important;
|
||||
}
|
||||
|
||||
/* 分隔线 */
|
||||
.ant-menu-item-divider {
|
||||
margin: 4px 0 !important;
|
||||
border-color: #EAECF3 !important;
|
||||
}
|
||||
|
||||
/* 设置选项 */
|
||||
.menu-item-setting {
|
||||
border-top: 1px solid #EAECF3;
|
||||
margin-top: 4px !important;
|
||||
}
|
||||
|
||||
.menu-item-setting .anticon {
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.menu-item-setting:hover {
|
||||
background-color: var(--yakit-primary-5) !important;
|
||||
}
|
||||
|
||||
.menu-item-setting:hover .anticon,
|
||||
.menu-item-setting:hover .menu-item-label {
|
||||
color: var(--yakit-primary) !important;
|
||||
}
|
||||
|
||||
/* 调整图标大小和对齐 */
|
||||
.anticon {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
/* 添加以下样式来确保下拉菜单显示在正确的位置 */
|
||||
.ant-dropdown {
|
||||
position: absolute !important;
|
||||
top: 100% !important;
|
||||
left: 0 !important;
|
||||
width: 100% !important;
|
||||
min-width: 200px !important;
|
||||
}
|
||||
|
||||
.dropdown-content {
|
||||
background: white;
|
||||
border-radius: 4px;
|
||||
box-shadow: 0 3px 6px -4px rgba(0,0,0,0.12),
|
||||
0 6px 16px 0 rgba(0,0,0,0.08),
|
||||
0 9px 28px 8px rgba(0,0,0,0.05);
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
/* 确保容器不会限制弹出层 */
|
||||
.proxy-container {
|
||||
min-width: 200px;
|
||||
background: white;
|
||||
border-radius: 4px;
|
||||
position: static;
|
||||
}
|
||||
|
||||
/* 添加这个样式来确保下拉菜单显示在正确的位置 */
|
||||
body {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.ant-menu {
|
||||
border: none !important;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.15) !important;
|
||||
padding: 4px 0 !important;
|
||||
width: 180px !important;
|
||||
background: white !important;
|
||||
}
|
||||
|
||||
.ant-menu-item {
|
||||
height: 28px !important;
|
||||
line-height: 28px !important;
|
||||
margin: 0 !important;
|
||||
padding: 0 16px !important;
|
||||
}
|
||||
|
||||
.ant-menu-item:hover {
|
||||
background-color: #f5f5f5 !important;
|
||||
}
|
||||
|
||||
.menu-icon {
|
||||
margin-right: 8px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.menu-item-selected {
|
||||
background-color: #e6f7ff !important;
|
||||
}
|
||||
|
||||
.ant-menu-item-divider {
|
||||
margin: 4px 0 !important;
|
||||
height: 1px !important;
|
||||
background-color: #f0f0f0 !important;
|
||||
}
|
||||
|
||||
.ant-menu-item:last-child {
|
||||
margin-top: 4px !important;
|
||||
border-top: 1px solid #f0f0f0;
|
||||
}
|
||||
|
||||
/* 移除多余的样式 */
|
||||
.ant-menu-root {
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
/* 调整整体容器大小 */
|
||||
.ant-menu-root {
|
||||
width: 180px !important;
|
||||
min-height: auto !important;
|
||||
}
|
||||
|
||||
/* 添加代理按钮样式 */
|
||||
.menu-item-add {
|
||||
color: #666 !important;
|
||||
}
|
||||
|
||||
.menu-item-add:hover {
|
||||
background-color: #f5f5f5 !important;
|
||||
color: var(--yakit-primary) !important;
|
||||
}
|
||||
|
||||
.menu-item-add .anticon {
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.menu-item-add:hover .anticon {
|
||||
color: var(--yakit-primary);
|
||||
}
|
||||
|
||||
.menu-loading {
|
||||
pointer-events: none;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.menu-item-loading {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.menu-item-selected {
|
||||
transition: all 0.3s ease;
|
||||
background-color: var(--yakit-primary-5) !important;
|
||||
}
|
||||
|
||||
/* 添加过渡效果 */
|
||||
.ant-menu-item {
|
||||
transition: all 0.3s ease !important;
|
||||
}
|
||||
|
||||
.ant-menu-item .menu-icon {
|
||||
transition: color 0.3s ease;
|
||||
}
|
||||
|
||||
.proxy-switch-container {
|
||||
position: relative;
|
||||
width: 180px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.panel-watermark {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
opacity: 0.03;
|
||||
pointer-events: none;
|
||||
object-fit: contain;
|
||||
object-position: right bottom;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
/* 确保菜单项在水印上层 */
|
||||
.ant-menu-item {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
background: transparent !important;
|
||||
}
|
||||
|
||||
/* 确保分割线在水印上层 */
|
||||
.ant-menu-item-divider {
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
@@ -0,0 +1,254 @@
|
||||
import React, {useEffect, useState} from 'react';
|
||||
import {Menu} from 'antd';
|
||||
import {DisconnectOutlined, SettingOutlined, PlusOutlined} from '@ant-design/icons';
|
||||
import {browser,} from 'wxt/browser';
|
||||
import type {MenuProps} from 'antd';
|
||||
import type {ProxyConfig} from '@/types/proxy';
|
||||
import {ContentActionType, ProxyActionType} from '@/types/action';
|
||||
|
||||
import './index.css';
|
||||
|
||||
// YAK 图标 URL
|
||||
const YAK_ICON_URL = browser.runtime.getURL('/yak.svg');
|
||||
|
||||
// 固定的代理模式
|
||||
const FIXED_MODES = [
|
||||
{
|
||||
key: 'direct',
|
||||
name: '[直接连接]',
|
||||
icon: <DisconnectOutlined/>,
|
||||
color: '#666',
|
||||
config: {
|
||||
id: 'direct',
|
||||
name: '[直接连接]',
|
||||
proxyType: 'direct',
|
||||
enabled: false
|
||||
}
|
||||
},
|
||||
{
|
||||
key: 'system',
|
||||
name: '[系统代理]',
|
||||
icon: <SettingOutlined/>,
|
||||
color: '#666',
|
||||
config: {
|
||||
id: 'system',
|
||||
name: '[系统代理]',
|
||||
proxyType: 'system',
|
||||
enabled: false
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
interface CustomProxy {
|
||||
key: string;
|
||||
name: string;
|
||||
color: string;
|
||||
config: ProxyConfig;
|
||||
enabled?: boolean;
|
||||
}
|
||||
|
||||
export const ProxySwitch: React.FC = () => {
|
||||
const [initialized, setInitialized] = useState<boolean>(false);
|
||||
const [currentMode, setCurrentMode] = useState<string>('');
|
||||
const [customProxies, setCustomProxies] = useState<CustomProxy[]>([]);
|
||||
const [isLoading, setIsLoading] = useState<boolean>(false);
|
||||
|
||||
// 监听存储变化
|
||||
useEffect(() => {
|
||||
const handleMessage = (message: any) => {
|
||||
if (message.action === ContentActionType.PROXY_CONFIGS_UPDATED && message.source !== 'proxy_switch') {
|
||||
loadCustomProxies();
|
||||
}
|
||||
};
|
||||
|
||||
browser.runtime.onMessage.addListener(handleMessage);
|
||||
return () => {
|
||||
browser.runtime.onMessage.removeListener(handleMessage);
|
||||
};
|
||||
}, []);
|
||||
|
||||
// 初始化
|
||||
useEffect(() => {
|
||||
const init = async () => {
|
||||
await loadProxyStatus();
|
||||
await loadCustomProxies();
|
||||
setInitialized(true);
|
||||
};
|
||||
init();
|
||||
}, []);
|
||||
|
||||
// 获取当前代理状态
|
||||
const loadProxyStatus = async () => {
|
||||
try {
|
||||
const response = await browser.runtime.sendMessage({
|
||||
action: ProxyActionType.GET_PROXY_STATUS
|
||||
});
|
||||
|
||||
if (!response) {
|
||||
console.log('No response from background script');
|
||||
return;
|
||||
}
|
||||
|
||||
if (response.success) {
|
||||
const activeMode = response.data.mode;
|
||||
setCurrentMode(activeMode);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading proxy status:', error);
|
||||
setCurrentMode('direct');
|
||||
}
|
||||
};
|
||||
|
||||
// 加载自定义代理配置
|
||||
const loadCustomProxies = async () => {
|
||||
try {
|
||||
const DB_NAME = 'yaklang_extension';
|
||||
const STORE_NAME = 'proxy_configs';
|
||||
|
||||
// 打开数据库
|
||||
const db = await new Promise<IDBDatabase>((resolve, reject) => {
|
||||
const request = indexedDB.open(DB_NAME, 1);
|
||||
request.onerror = () => reject(request.error);
|
||||
request.onsuccess = () => resolve(request.result);
|
||||
});
|
||||
|
||||
// 从数据库读取代理配置
|
||||
const configs = await new Promise<ProxyConfig[]>((resolve, reject) => {
|
||||
try {
|
||||
const transaction = db.transaction([STORE_NAME], 'readonly');
|
||||
const store = transaction.objectStore(STORE_NAME);
|
||||
const request = store.getAll();
|
||||
|
||||
request.onerror = () => reject(request.error);
|
||||
request.onsuccess = () => resolve(request.result || []);
|
||||
} catch (error) {
|
||||
reject(error);
|
||||
}
|
||||
});
|
||||
|
||||
// 处理代理配置
|
||||
const proxies = configs
|
||||
.filter((proxy: ProxyConfig) => !FIXED_MODES.some(mode => mode.key === proxy.id))
|
||||
.map((proxy: ProxyConfig): CustomProxy => ({
|
||||
key: proxy.id,
|
||||
name: proxy.name,
|
||||
color: '#1890ff',
|
||||
config: proxy,
|
||||
enabled: proxy.enabled
|
||||
}));
|
||||
setCustomProxies(proxies);
|
||||
|
||||
const enabledProxy = configs.find((proxy: ProxyConfig) => proxy.enabled);
|
||||
if (enabledProxy) {
|
||||
setCurrentMode(enabledProxy.id);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error loading custom proxies:', error);
|
||||
setCustomProxies([]);
|
||||
}
|
||||
};
|
||||
|
||||
// 处理代理模式变更
|
||||
const handleModeChange = async (mode: string) => {
|
||||
if (mode === 'setting') {
|
||||
// 打开设置页面
|
||||
await browser.runtime.openOptionsPage?.();
|
||||
return;
|
||||
}
|
||||
|
||||
if (mode === 'add') {
|
||||
// 打开添加代理表单
|
||||
try {
|
||||
const [activeTab] = await browser.tabs.query({
|
||||
active: true,
|
||||
currentWindow: true
|
||||
});
|
||||
|
||||
const optionsUrl = browser.runtime.getURL('/options.html');
|
||||
|
||||
if (activeTab?.url === optionsUrl) {
|
||||
browser.tabs.sendMessage(activeTab.id!, {
|
||||
action: ContentActionType.TRIGGER_ADD_PROXY
|
||||
});
|
||||
} else {
|
||||
await browser.tabs.create({
|
||||
url: optionsUrl
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to get current tab:', error);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setIsLoading(true);
|
||||
|
||||
// 发送切换代理请求
|
||||
const response = await browser.runtime.sendMessage({
|
||||
action: ProxyActionType.SWITCH_PROXY,
|
||||
mode
|
||||
});
|
||||
|
||||
if (response && response.success) {
|
||||
setCurrentMode(mode);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error switching to proxy mode ${mode}:`, error);
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
// 构建菜单项
|
||||
const buildMenuItems = () => {
|
||||
const items: MenuProps['items'] = [
|
||||
...FIXED_MODES.map(mode => ({
|
||||
key: mode.key,
|
||||
label: mode.name,
|
||||
icon: mode.icon,
|
||||
})),
|
||||
{type: 'divider'}
|
||||
];
|
||||
|
||||
// 添加自定义代理
|
||||
if (customProxies.length > 0) {
|
||||
items.push(
|
||||
...customProxies.map(proxy => ({
|
||||
key: proxy.key,
|
||||
label: proxy.name,
|
||||
icon: <img src={YAK_ICON_URL} alt="YAK" style={{width: 16, height: 16}}/>,
|
||||
}))
|
||||
);
|
||||
items.push({type: 'divider'});
|
||||
}
|
||||
|
||||
// 添加设置选项
|
||||
items.push({
|
||||
key: 'setting',
|
||||
label: '代理设置',
|
||||
icon: <SettingOutlined/>,
|
||||
});
|
||||
|
||||
// 添加新建代理选项
|
||||
items.push({
|
||||
key: 'add',
|
||||
label: '添加代理',
|
||||
icon: <PlusOutlined/>,
|
||||
});
|
||||
|
||||
return items;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="proxy-switch-container">
|
||||
<Menu
|
||||
className="proxy-menu"
|
||||
selectedKeys={[currentMode]}
|
||||
items={buildMenuItems()}
|
||||
onClick={({key}) => handleModeChange(key)}
|
||||
/>
|
||||
{isLoading && <div className="loading-overlay">切换中...</div>}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -0,0 +1,36 @@
|
||||
import { browser, type Browser } from 'wxt/browser';
|
||||
import {ContentActionType, ProxyActionType} from '@/types/action';
|
||||
import { getCurrentProxyMode, switchProxyMode } from '@/utils/proxy';
|
||||
|
||||
export default defineBackground({
|
||||
type: 'module',
|
||||
|
||||
main() {
|
||||
// 初始化代理状态监听
|
||||
browser.runtime.onMessage.addListener((message: any, sender: Browser.runtime.MessageSender, sendResponse: (response?: any) => void) => {
|
||||
if (message.action === ProxyActionType.GET_PROXY_STATUS) {
|
||||
// 获取当前代理状态
|
||||
getCurrentProxyMode().then(mode => {
|
||||
sendResponse({ success: true, data: { mode } });
|
||||
});
|
||||
return true;
|
||||
} else if (message.action === ProxyActionType.SWITCH_PROXY) {
|
||||
// 切换代理
|
||||
switchProxyMode(message.mode).then(success => {
|
||||
sendResponse({ success });
|
||||
|
||||
// 如果切换成功,广播代理状态更改消息
|
||||
if (success) {
|
||||
browser.runtime.sendMessage({
|
||||
action: ContentActionType.PROXY_CONFIGS_UPDATED,
|
||||
source: 'background'
|
||||
});
|
||||
}
|
||||
});
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
||||
console.log('代理管理后台服务已启动');
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,6 @@
|
||||
export default defineContentScript({
|
||||
matches: ['*://*.google.com/*'],
|
||||
main() {
|
||||
console.log('Hello content.');
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,39 @@
|
||||
.options-layout {
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
.options-header {
|
||||
background-color: #F28B44;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 0 24px;
|
||||
}
|
||||
|
||||
.options-content {
|
||||
padding: 24px;
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
background-color: #f5f5f5;
|
||||
}
|
||||
|
||||
.proxy-list-card {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.proxy-list-card .ant-list-item {
|
||||
padding: 12px 24px;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.proxy-list-card .ant-list-item:hover {
|
||||
background-color: rgba(242, 139, 68, 0.05);
|
||||
}
|
||||
|
||||
.add-proxy-card {
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
|
||||
.ant-space {
|
||||
width: 100%;
|
||||
}
|
||||
@@ -0,0 +1,242 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { ConfigProvider, Layout, Typography, List, Button, Form, Input, Select, Space, Card, Divider } from 'antd';
|
||||
import { PlusOutlined, DeleteOutlined } from '@ant-design/icons';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { browser } from 'wxt/browser';
|
||||
import type { ProxyConfig } from '../../types/proxy';
|
||||
import { getAllProxyConfigs, saveProxyConfig, deleteProxyConfig, enableProxyConfig } from '../../utils/storage';
|
||||
import {ContentActionType, ProxyActionType} from '../../types/action';
|
||||
import './App.css';
|
||||
|
||||
const { Header, Content } = Layout;
|
||||
const { Title } = Typography;
|
||||
const { Option } = Select;
|
||||
|
||||
export default function App() {
|
||||
const [proxies, setProxies] = useState<ProxyConfig[]>([]);
|
||||
const [form] = Form.useForm();
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
loadProxies();
|
||||
|
||||
// 监听添加代理请求
|
||||
const handleMessage = (message: any) => {
|
||||
if (message.action === ContentActionType.TRIGGER_ADD_PROXY) {
|
||||
document.getElementById('add-proxy-form')?.scrollIntoView({ behavior: 'smooth' });
|
||||
}
|
||||
};
|
||||
|
||||
browser.runtime.onMessage.addListener(handleMessage);
|
||||
return () => {
|
||||
browser.runtime.onMessage.removeListener(handleMessage);
|
||||
};
|
||||
}, []);
|
||||
|
||||
const loadProxies = async () => {
|
||||
try {
|
||||
const configs = await getAllProxyConfigs();
|
||||
setProxies(configs.filter(config => config.proxyType !== 'direct' && config.proxyType !== 'system'));
|
||||
} catch (error) {
|
||||
console.error('Error loading proxies:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSave = async (values: any) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
|
||||
const newProxy: ProxyConfig = {
|
||||
id: uuidv4(),
|
||||
name: values.name,
|
||||
proxyType: values.proxyType,
|
||||
host: values.host,
|
||||
port: Number(values.port),
|
||||
enabled: false
|
||||
};
|
||||
|
||||
// if (values.username) newProxy.username = values.username;
|
||||
// if (values.password) newProxy.password = values.password;
|
||||
|
||||
await saveProxyConfig(newProxy);
|
||||
|
||||
// 重新加载代理列表
|
||||
await loadProxies();
|
||||
|
||||
// 重置表单
|
||||
form.resetFields();
|
||||
|
||||
// 通知后台脚本
|
||||
browser.runtime.sendMessage({
|
||||
action: ContentActionType.PROXY_CONFIGS_UPDATED,
|
||||
source: 'options'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error saving proxy:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async (id: string) => {
|
||||
try {
|
||||
await deleteProxyConfig(id);
|
||||
await loadProxies();
|
||||
|
||||
// 通知后台脚本
|
||||
browser.runtime.sendMessage({
|
||||
action: ContentActionType.PROXY_CONFIGS_UPDATED,
|
||||
source: 'options'
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(`Error deleting proxy ${id}:`, error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleActivate = async (id: string) => {
|
||||
try {
|
||||
// 启用代理
|
||||
await enableProxyConfig(id);
|
||||
|
||||
// 发送切换代理请求
|
||||
await browser.runtime.sendMessage({
|
||||
action: ProxyActionType.SWITCH_PROXY,
|
||||
mode: id
|
||||
});
|
||||
|
||||
// 重新加载代理列表
|
||||
await loadProxies();
|
||||
} catch (error) {
|
||||
console.error(`Error activating proxy ${id}:`, error);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<ConfigProvider
|
||||
theme={{
|
||||
token: {
|
||||
colorPrimary: "#F28B44",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Layout className="options-layout">
|
||||
<Header className="options-header">
|
||||
<Title level={3} style={{ color: 'white', margin: 0 }}>Yaklang 代理管理设置</Title>
|
||||
</Header>
|
||||
<Content className="options-content">
|
||||
<Card className="proxy-list-card" title="已保存的代理">
|
||||
<List
|
||||
dataSource={proxies}
|
||||
renderItem={(proxy) => (
|
||||
<List.Item
|
||||
key={proxy.id}
|
||||
actions={[
|
||||
<Button
|
||||
type="primary"
|
||||
onClick={() => handleActivate(proxy.id)}
|
||||
disabled={proxy.enabled}
|
||||
>
|
||||
{proxy.enabled ? '已启用' : '启用'}
|
||||
</Button>,
|
||||
<Button
|
||||
danger
|
||||
icon={<DeleteOutlined />}
|
||||
onClick={() => handleDelete(proxy.id)}
|
||||
/>
|
||||
]}
|
||||
>
|
||||
<List.Item.Meta
|
||||
title={proxy.name}
|
||||
description={`${proxy.proxyType}://${proxy.host}:${proxy.port}`}
|
||||
/>
|
||||
</List.Item>
|
||||
)}
|
||||
/>
|
||||
</Card>
|
||||
|
||||
<Divider />
|
||||
|
||||
<Card id="add-proxy-form" className="add-proxy-card" title="添加新代理">
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
onFinish={handleSave}
|
||||
>
|
||||
<Form.Item
|
||||
name="name"
|
||||
label="代理名称"
|
||||
rules={[{ required: true, message: '请输入代理名称' }]}
|
||||
>
|
||||
<Input placeholder="例如: 公司内网代理" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="proxyType"
|
||||
label="代理类型"
|
||||
initialValue="http"
|
||||
rules={[{ required: true, message: '请选择代理类型' }]}
|
||||
>
|
||||
<Select>
|
||||
<Option value="http">HTTP</Option>
|
||||
<Option value="https">HTTPS</Option>
|
||||
<Option value="socks4">SOCKS4</Option>
|
||||
<Option value="socks5">SOCKS5</Option>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
|
||||
<Space style={{ display: 'flex' }}>
|
||||
<Form.Item
|
||||
name="host"
|
||||
label="主机地址"
|
||||
rules={[{ required: true, message: '请输入主机地址' }]}
|
||||
style={{ flex: 3 }}
|
||||
>
|
||||
<Input placeholder="例如: proxy.example.com 或 192.168.1.100" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="port"
|
||||
label="端口"
|
||||
rules={[{ required: true, message: '请输入端口' }]}
|
||||
style={{ flex: 1 }}
|
||||
>
|
||||
<Input placeholder="例如: 8080" />
|
||||
</Form.Item>
|
||||
</Space>
|
||||
|
||||
<Space style={{ display: 'flex' }}>
|
||||
<Form.Item
|
||||
name="username"
|
||||
label="用户名 (可选)"
|
||||
style={{ flex: 1 }}
|
||||
>
|
||||
<Input placeholder="认证用户名" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
name="password"
|
||||
label="密码 (可选)"
|
||||
style={{ flex: 1 }}
|
||||
>
|
||||
<Input.Password placeholder="认证密码" />
|
||||
</Form.Item>
|
||||
</Space>
|
||||
|
||||
<Form.Item>
|
||||
<Button
|
||||
type="primary"
|
||||
htmlType="submit"
|
||||
icon={<PlusOutlined />}
|
||||
loading={loading}
|
||||
block
|
||||
>
|
||||
添加代理
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Card>
|
||||
</Content>
|
||||
</Layout>
|
||||
</ConfigProvider>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Yaklang 代理管理设置</title>
|
||||
<meta name="manifest.open_in_tab" content="true" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script type="module" src="./main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,7 @@
|
||||
import React from 'react';
|
||||
import { createRoot } from 'react-dom/client';
|
||||
import App from './App';
|
||||
import './style.css';
|
||||
|
||||
const root = createRoot(document.getElementById('app')!);
|
||||
root.render(<App />);
|
||||
@@ -0,0 +1,9 @@
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
@@ -0,0 +1,73 @@
|
||||
#root {
|
||||
max-width: 1280px;
|
||||
margin: 0 auto;
|
||||
padding: 2rem;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.logo {
|
||||
height: 6em;
|
||||
padding: 1.5em;
|
||||
will-change: filter;
|
||||
transition: filter 300ms;
|
||||
}
|
||||
.logo:hover {
|
||||
filter: drop-shadow(0 0 2em #54bc4ae0);
|
||||
}
|
||||
.logo.react:hover {
|
||||
filter: drop-shadow(0 0 2em #61dafbaa);
|
||||
}
|
||||
|
||||
@keyframes logo-spin {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
a:nth-of-type(2) .logo {
|
||||
animation: logo-spin infinite 20s linear;
|
||||
}
|
||||
}
|
||||
|
||||
.card {
|
||||
padding: 2em;
|
||||
}
|
||||
|
||||
.read-the-docs {
|
||||
color: #888;
|
||||
}
|
||||
|
||||
.popup-container {
|
||||
width: 320px;
|
||||
min-height: 400px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif;
|
||||
}
|
||||
|
||||
.popup-header {
|
||||
background-color: #F28B44;
|
||||
color: white;
|
||||
padding: 12px 16px;
|
||||
text-align: center;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.popup-header h1 {
|
||||
margin: 0;
|
||||
font-size: 18px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.popup-content {
|
||||
flex: 1;
|
||||
padding: 0;
|
||||
background-color: #fff;
|
||||
overflow: auto;
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import React from 'react';
|
||||
import { ConfigProvider } from 'antd';
|
||||
import { ProxySwitch } from '../../components/ProxySwitch';
|
||||
import './App.css';
|
||||
|
||||
export default function App() {
|
||||
return (
|
||||
<ConfigProvider
|
||||
theme={{
|
||||
token: {
|
||||
colorPrimary: "#F28B44",
|
||||
},
|
||||
}}
|
||||
>
|
||||
<div className="popup-container">
|
||||
<header className="popup-header">
|
||||
<h1>Yaklang 代理管理</h1>
|
||||
</header>
|
||||
<main className="popup-content">
|
||||
<ProxySwitch />
|
||||
</main>
|
||||
</div>
|
||||
</ConfigProvider>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Default Popup Title</title>
|
||||
<meta name="manifest.type" content="browser_action" />
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="./main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,10 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import App from './App.tsx';
|
||||
import './style.css';
|
||||
|
||||
ReactDOM.createRoot(document.getElementById('root')!).render(
|
||||
<React.StrictMode>
|
||||
<App />
|
||||
</React.StrictMode>,
|
||||
);
|
||||
@@ -0,0 +1,69 @@
|
||||
:root {
|
||||
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
|
||||
line-height: 1.5;
|
||||
font-weight: 400;
|
||||
|
||||
color-scheme: light dark;
|
||||
color: rgba(255, 255, 255, 0.87);
|
||||
background-color: #242424;
|
||||
|
||||
font-synthesis: none;
|
||||
text-rendering: optimizeLegibility;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
}
|
||||
|
||||
a {
|
||||
font-weight: 500;
|
||||
color: #646cff;
|
||||
text-decoration: inherit;
|
||||
}
|
||||
a:hover {
|
||||
color: #535bf2;
|
||||
}
|
||||
|
||||
body {
|
||||
margin: 0;
|
||||
display: flex;
|
||||
place-items: center;
|
||||
min-width: 320px;
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
h1 {
|
||||
font-size: 3.2em;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
button {
|
||||
border-radius: 8px;
|
||||
border: 1px solid transparent;
|
||||
padding: 0.6em 1.2em;
|
||||
font-size: 1em;
|
||||
font-weight: 500;
|
||||
font-family: inherit;
|
||||
background-color: #1a1a1a;
|
||||
cursor: pointer;
|
||||
transition: border-color 0.25s;
|
||||
}
|
||||
button:hover {
|
||||
border-color: #646cff;
|
||||
}
|
||||
button:focus,
|
||||
button:focus-visible {
|
||||
outline: 4px auto -webkit-focus-ring-color;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
:root {
|
||||
color: #213547;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
a:hover {
|
||||
color: #747bff;
|
||||
}
|
||||
button {
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,303 @@
|
||||
/* Base styles for the proxy panel */
|
||||
.yak-proxy-root * {
|
||||
all: initial;
|
||||
box-sizing: border-box;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||
line-height: normal;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: none;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.floating-panel {
|
||||
position: fixed;
|
||||
top: 30%;
|
||||
right: 0;
|
||||
transform: translateY(-30%);
|
||||
background: white;
|
||||
z-index: 2147483647;
|
||||
width: 50px;
|
||||
height: 40px;
|
||||
overflow: hidden;
|
||||
transition: width 0.2s cubic-bezier(0.4, 0, 0.2, 1),
|
||||
height 0.2s cubic-bezier(0.4, 0, 0.2, 1),
|
||||
background-color 0.2s ease;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* Non-expanded state */
|
||||
.floating-panel:not(.expanded):not(.dragging) {
|
||||
border-radius: 50px 0 0 50px;
|
||||
box-shadow: -4px 0 20px rgba(0,0,0,0.15);
|
||||
border: 1px solid #eee;
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
/* Dragging state */
|
||||
.floating-panel.dragging {
|
||||
cursor: grabbing;
|
||||
user-select: none;
|
||||
opacity: 0.95;
|
||||
transition: none;
|
||||
}
|
||||
|
||||
/* Hover state */
|
||||
.floating-panel:not(.expanded):hover {
|
||||
width: 120px;
|
||||
background: #fff7e6;
|
||||
border-color: #ffd591;
|
||||
}
|
||||
|
||||
/* Expanded state */
|
||||
.floating-panel.expanded {
|
||||
width: 180px;
|
||||
height: auto;
|
||||
max-height: 400px;
|
||||
border-radius: 8px 0 0 8px;
|
||||
box-shadow: -2px 0 10px rgba(0,0,0,0.1);
|
||||
border: 1px solid #eee;
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
/* Panel header */
|
||||
.panel-header {
|
||||
height: 40px;
|
||||
min-height: 40px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 8px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
/* Header in expanded state */
|
||||
.floating-panel.expanded .panel-header {
|
||||
background: #f8f9fa;
|
||||
border-bottom: 1px solid #eee;
|
||||
}
|
||||
|
||||
.header-content {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Yak icon */
|
||||
.yak-icon {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
min-width: 36px;
|
||||
object-fit: contain;
|
||||
filter: drop-shadow(0 2px 4px rgba(0,0,0,0.1));
|
||||
transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
|
||||
}
|
||||
|
||||
.floating-panel.expanded .yak-icon {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
min-width: 24px;
|
||||
}
|
||||
|
||||
/* Active proxy info */
|
||||
.active-proxy-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-left: 8px;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
color: #ff6b00;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.active-proxy-info span:first-child {
|
||||
margin-right: 6px;
|
||||
}
|
||||
|
||||
.active-proxy-info span:nth-child(2) {
|
||||
color: #333;
|
||||
}
|
||||
|
||||
/* Panel content */
|
||||
.panel-content {
|
||||
display: none;
|
||||
background: white;
|
||||
overflow-y: auto;
|
||||
max-height: 360px;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
|
||||
.floating-panel.expanded .panel-content {
|
||||
display: block;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Scrollbar styles */
|
||||
.panel-content::-webkit-scrollbar {
|
||||
width: 4px;
|
||||
}
|
||||
|
||||
.panel-content::-webkit-scrollbar-track {
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
.panel-content::-webkit-scrollbar-thumb {
|
||||
background: #ddd;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.panel-content::-webkit-scrollbar-thumb:hover {
|
||||
background: #ccc;
|
||||
}
|
||||
|
||||
/* Proxy item */
|
||||
.proxy-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 8px 12px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
white-space: nowrap;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.proxy-item:hover {
|
||||
background: #fff7e6;
|
||||
}
|
||||
|
||||
.proxy-item.active {
|
||||
background: #fff7e6;
|
||||
color: #ff6b00;
|
||||
}
|
||||
|
||||
.proxy-item.active span {
|
||||
color: #ff6b00;
|
||||
}
|
||||
|
||||
.proxy-item span:first-child {
|
||||
margin-right: 8px;
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.proxy-item span {
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.proxy-status {
|
||||
position: absolute;
|
||||
right: 12px;
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background: #52c41a;
|
||||
box-shadow: 0 0 4px rgba(82,196,26,0.3);
|
||||
}
|
||||
|
||||
.proxy-item.active .proxy-status {
|
||||
background: #ff6b00;
|
||||
box-shadow: 0 0 4px rgba(255,107,0,0.3);
|
||||
}
|
||||
|
||||
/* Divider */
|
||||
.divider {
|
||||
height: 1px;
|
||||
background: #f0f0f0;
|
||||
margin: 4px 0;
|
||||
}
|
||||
|
||||
/* Action buttons */
|
||||
.action-button {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 8px 12px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
white-space: nowrap;
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.action-button:hover {
|
||||
background: #fff7e6;
|
||||
color: #ff6b00;
|
||||
}
|
||||
|
||||
.action-button:hover span {
|
||||
color: #ff6b00;
|
||||
}
|
||||
|
||||
.action-button span:first-child {
|
||||
margin-right: 8px;
|
||||
}
|
||||
|
||||
.action-button span {
|
||||
color: #666;
|
||||
}
|
||||
|
||||
/* Tab container */
|
||||
.tabs-container {
|
||||
display: flex;
|
||||
height: 100%;
|
||||
min-height: 200px;
|
||||
}
|
||||
|
||||
.tab-list {
|
||||
width: 40px;
|
||||
background: #f8f9fa;
|
||||
border-right: 1px solid #eee;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding-top: 8px;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
align-self: flex-start;
|
||||
height: 100%;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.tab-button {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 4px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
background: transparent;
|
||||
border: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.tab-button:hover {
|
||||
background: #fff7e6;
|
||||
}
|
||||
|
||||
.tab-button.active {
|
||||
background: #fff7e6;
|
||||
color: #ff6b00;
|
||||
}
|
||||
|
||||
.tab-content {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-width: 0;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.tab-panel {
|
||||
display: none;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.tab-panel.active {
|
||||
display: flex;
|
||||
}
|
||||
@@ -0,0 +1,356 @@
|
||||
import React, {useState, useEffect, useRef} from 'react';
|
||||
import {browser} from 'wxt/browser';
|
||||
import type {ProxyConfig} from '@/types/proxy.ts';
|
||||
|
||||
// Constants - using string literal instead of getURL since it will be replaced at build time
|
||||
const YAK_ICON_URL = browser.runtime.getURL("/yak.svg");
|
||||
|
||||
// Action types from the application
|
||||
const ProxyActionType = {
|
||||
SET_PROXY_CONFIG: "SET_PROXY_CONFIG",
|
||||
CLEAR_PROXY_CONFIG: "CLEAR_PROXY_CONFIG",
|
||||
GET_PROXY_STATUS: "GET_PROXY_STATUS",
|
||||
GET_PROXY_CONFIGS: "GET_PROXY_CONFIGS",
|
||||
UPDATE_PROXY_CONFIG: "UPDATE_PROXY_CONFIG",
|
||||
};
|
||||
|
||||
// Export anonymous component directly as default export
|
||||
const App: React.FC = () => {
|
||||
// State
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
const [isDragging, setIsDragging] = useState(false);
|
||||
const [activeTab, setActiveTab] = useState('proxy');
|
||||
const [proxyStatus, setProxyStatus] = useState({
|
||||
enable: false,
|
||||
proxy: '',
|
||||
currentMode: 'direct'
|
||||
});
|
||||
const [proxyConfigs, setProxyConfigs] = useState<ProxyConfig[]>([]);
|
||||
|
||||
// Refs
|
||||
const panelRef = useRef<HTMLDivElement>(null);
|
||||
const dragStartRef = useRef({y: 0, top: 0});
|
||||
const timeoutRef = useRef<number | null>(null);
|
||||
|
||||
// Setup message listener for updates
|
||||
useEffect(() => {
|
||||
const messageListener = async (message: any) => {
|
||||
if (message.action === "PROXY_STATUS_CHANGED" || message.action === "PROXY_CONFIGS_UPDATED") {
|
||||
await fetchProxyStatus();
|
||||
await fetchProxyConfigs();
|
||||
}
|
||||
};
|
||||
|
||||
browser.runtime.onMessage.addListener(messageListener);
|
||||
|
||||
// Initial data fetch
|
||||
fetchProxyStatus();
|
||||
fetchProxyConfigs();
|
||||
|
||||
// Position from localStorage if available
|
||||
const savedPosition = localStorage.getItem("yakitProxyPanelPosition");
|
||||
if (savedPosition && panelRef.current) {
|
||||
const top = (parseFloat(savedPosition) / 100) * window.innerHeight;
|
||||
panelRef.current.style.top = `${top}px`;
|
||||
panelRef.current.style.transform = 'translateY(0)';
|
||||
}
|
||||
|
||||
return () => {
|
||||
browser.runtime.onMessage.removeListener(messageListener);
|
||||
if (timeoutRef.current !== null) {
|
||||
clearTimeout(timeoutRef.current);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
// Fetch current proxy status
|
||||
const fetchProxyStatus = async () => {
|
||||
try {
|
||||
const response = await sendMessageWithRetry({
|
||||
action: ProxyActionType.GET_PROXY_STATUS,
|
||||
});
|
||||
|
||||
if (response && response.success) {
|
||||
const status = response.data;
|
||||
setProxyStatus({
|
||||
enable: status.enabled,
|
||||
proxy: status.mode === "system" ? "system" : "",
|
||||
currentMode: status.mode || "direct",
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching proxy status:", error);
|
||||
}
|
||||
};
|
||||
|
||||
// Fetch proxy configurations
|
||||
const fetchProxyConfigs = async () => {
|
||||
try {
|
||||
const response = await sendMessageWithRetry({
|
||||
action: ProxyActionType.GET_PROXY_CONFIGS,
|
||||
});
|
||||
|
||||
if (response && response.success) {
|
||||
setProxyConfigs(response.data || []);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error fetching proxy configs:", error);
|
||||
}
|
||||
};
|
||||
|
||||
// Send message with retry logic
|
||||
const sendMessageWithRetry = async (message: any, maxRetries = 3) => {
|
||||
for (let i = 0; i < maxRetries; i++) {
|
||||
try {
|
||||
return await browser.runtime.sendMessage(message);
|
||||
} catch (error) {
|
||||
console.warn(`Attempt ${i + 1} failed:`, error);
|
||||
if (i === maxRetries - 1) {
|
||||
throw error;
|
||||
}
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Handle switching to a different proxy
|
||||
const handleProxySwitch = async (config: ProxyConfig) => {
|
||||
try {
|
||||
await sendMessageWithRetry({
|
||||
action: ProxyActionType.SET_PROXY_CONFIG,
|
||||
config,
|
||||
});
|
||||
|
||||
// Update the UI
|
||||
await fetchProxyStatus();
|
||||
} catch (error) {
|
||||
console.error("Error switching proxy:", error);
|
||||
}
|
||||
};
|
||||
|
||||
// Open options page
|
||||
const openOptionsPage = async (triggerAdd = false) => {
|
||||
try {
|
||||
await sendMessageWithRetry({
|
||||
action: "OPEN_OPTIONS_PAGE",
|
||||
triggerAdd,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Error opening options page:", error);
|
||||
}
|
||||
};
|
||||
|
||||
// Handle dragging functionality
|
||||
const handleMouseDown = (e: React.MouseEvent<HTMLDivElement>) => {
|
||||
if (expanded) {
|
||||
setExpanded(false);
|
||||
return;
|
||||
}
|
||||
|
||||
if (e.button !== 0) return; // Only left mouse button
|
||||
|
||||
setIsDragging(true);
|
||||
|
||||
const rect = panelRef.current?.getBoundingClientRect();
|
||||
if (rect) {
|
||||
dragStartRef.current = {
|
||||
y: e.clientY,
|
||||
top: rect.top,
|
||||
};
|
||||
}
|
||||
|
||||
e.preventDefault();
|
||||
};
|
||||
|
||||
const handleMouseMove = (e: React.MouseEvent<HTMLDivElement>) => {
|
||||
if (!isDragging) return;
|
||||
|
||||
const deltaY = e.clientY - dragStartRef.current.y;
|
||||
const newTop = dragStartRef.current.top + deltaY;
|
||||
|
||||
// Limit drag range to viewport
|
||||
const maxTop = window.innerHeight - (panelRef.current?.offsetHeight || 0);
|
||||
const boundedTop = Math.max(0, Math.min(newTop, maxTop));
|
||||
|
||||
if (panelRef.current) {
|
||||
panelRef.current.style.top = `${boundedTop}px`;
|
||||
panelRef.current.style.transform = 'translateY(0)';
|
||||
}
|
||||
};
|
||||
|
||||
const handleMouseUp = () => {
|
||||
if (!isDragging) return;
|
||||
|
||||
setIsDragging(false);
|
||||
|
||||
// Save position
|
||||
if (panelRef.current) {
|
||||
const top = panelRef.current.getBoundingClientRect().top;
|
||||
const percentage = (top / window.innerHeight) * 100;
|
||||
localStorage.setItem("yakitProxyPanelPosition", percentage.toString());
|
||||
}
|
||||
};
|
||||
|
||||
// Handle mouse enter to clear any auto-collapse timeouts
|
||||
const handleMouseEnter = () => {
|
||||
if (timeoutRef.current !== null) {
|
||||
clearTimeout(timeoutRef.current);
|
||||
timeoutRef.current = null;
|
||||
}
|
||||
};
|
||||
|
||||
// Handle mouse leave to auto-collapse the panel
|
||||
const handleMouseLeave = () => {
|
||||
if (expanded) {
|
||||
timeoutRef.current = window.setTimeout(() => {
|
||||
setExpanded(false);
|
||||
timeoutRef.current = null;
|
||||
}, 300);
|
||||
}
|
||||
};
|
||||
|
||||
// Get active proxy name and icon
|
||||
let proxyIcon = "🟢";
|
||||
let proxyName = "直接连接";
|
||||
|
||||
if (proxyStatus.currentMode === "system") {
|
||||
proxyIcon = "⚙️";
|
||||
proxyName = "系统代理";
|
||||
} else if (proxyStatus.currentMode === "fixed_servers") {
|
||||
const activeConfig = proxyConfigs.find(c => c.enabled);
|
||||
if (activeConfig) {
|
||||
proxyIcon = activeConfig.proxyType === "pac_script" ? "📜" : "🌐";
|
||||
proxyName = activeConfig.name || "未命名代理";
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={panelRef}
|
||||
className={`floating-panel ${expanded ? 'expanded' : ''} ${isDragging ? 'dragging' : ''}`}
|
||||
data-active-tab={activeTab}
|
||||
onMouseMove={handleMouseMove}
|
||||
onMouseUp={handleMouseUp}
|
||||
onMouseLeave={handleMouseLeave}
|
||||
onMouseEnter={handleMouseEnter}
|
||||
>
|
||||
<div
|
||||
className="panel-header"
|
||||
onMouseDown={handleMouseDown}
|
||||
onClick={() => !isDragging && setExpanded(!expanded)}
|
||||
>
|
||||
<div className="header-content">
|
||||
<img src={YAK_ICON_URL} className="yak-icon" alt="Yak"/>
|
||||
<div className="active-proxy-info">
|
||||
<span>{proxyIcon}</span>
|
||||
<span>{proxyName}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{expanded && (
|
||||
<div className="panel-content">
|
||||
<div className="tabs-container">
|
||||
<div className="tab-list">
|
||||
<button
|
||||
className={`tab-button ${activeTab === 'proxy' ? 'active' : ''}`}
|
||||
onClick={() => setActiveTab('proxy')}
|
||||
title="代理设置"
|
||||
>
|
||||
🌐
|
||||
</button>
|
||||
<button
|
||||
className={`tab-button ${activeTab === 'links' ? 'active' : ''}`}
|
||||
onClick={() => setActiveTab('links')}
|
||||
title="页面链接"
|
||||
>
|
||||
🔗
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="tab-content">
|
||||
<div className={`tab-panel ${activeTab === 'proxy' ? 'active' : ''}`} data-panel="proxy">
|
||||
<div
|
||||
className={`proxy-item ${proxyStatus.currentMode === 'direct' ? 'active' : ''}`}
|
||||
onClick={() => handleProxySwitch({
|
||||
id: 'direct',
|
||||
name: '[直接连接]',
|
||||
proxyType: 'direct',
|
||||
enabled: false
|
||||
})}
|
||||
title="直接连接"
|
||||
>
|
||||
<span>🟢</span>
|
||||
<span>直接连接</span>
|
||||
{proxyStatus.currentMode === 'direct' && <div className="proxy-status"></div>}
|
||||
</div>
|
||||
|
||||
<div
|
||||
className={`proxy-item ${proxyStatus.currentMode === 'system' ? 'active' : ''}`}
|
||||
onClick={() => handleProxySwitch({
|
||||
id: 'system',
|
||||
name: '[系统代理]',
|
||||
proxyType: 'system',
|
||||
enabled: true
|
||||
})}
|
||||
title="系统代理"
|
||||
>
|
||||
<span>⚙️</span>
|
||||
<span>系统代理</span>
|
||||
{proxyStatus.currentMode === 'system' && <div className="proxy-status"></div>}
|
||||
</div>
|
||||
|
||||
<div className="divider"></div>
|
||||
|
||||
{proxyConfigs.map(config => {
|
||||
if (config.proxyType !== 'direct' && config.proxyType !== 'system') {
|
||||
const isActive = proxyStatus.currentMode === 'fixed_servers' && config.enabled;
|
||||
const proxyIcon = config.proxyType === 'pac_script' ? '📜' : '🌐';
|
||||
const tooltipText = config.proxyType === 'pac_script'
|
||||
? 'PAC Script'
|
||||
: `${config.scheme ? `${config.scheme.toUpperCase()} ` : ''}${config.host}:${config.port}`;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={config.id}
|
||||
className={`proxy-item ${isActive ? 'active' : ''}`}
|
||||
onClick={() => handleProxySwitch({...config, enabled: true})}
|
||||
title={tooltipText}
|
||||
>
|
||||
<span>{proxyIcon}</span>
|
||||
<span>{config.name || '未命名代理'}</span>
|
||||
{isActive && <div className="proxy-status"></div>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return null;
|
||||
})}
|
||||
|
||||
<div className="divider"></div>
|
||||
|
||||
<div className="action-button" onClick={() => openOptionsPage(true)}>
|
||||
<span>➕</span>
|
||||
<span>添加代理</span>
|
||||
</div>
|
||||
|
||||
<div className="action-button" onClick={() => openOptionsPage(false)}>
|
||||
<span>⚙️</span>
|
||||
<span>设置</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className={`tab-panel ${activeTab === 'links' ? 'active' : ''}`} data-panel="links">
|
||||
{/* Links panel content will be added in the future */}
|
||||
<div className="links-placeholder" style={{padding: '16px', textAlign: 'center'}}>
|
||||
<p>链接面板功能将在未来版本中实现</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default App;
|
||||
@@ -0,0 +1,39 @@
|
||||
import './App.css';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import App from './App.tsx';
|
||||
|
||||
export default defineContentScript({
|
||||
matches: ['<all_urls>'],
|
||||
cssInjectionMode: 'ui',
|
||||
|
||||
|
||||
async main(ctx) {
|
||||
console.log("Proxy content script starting...");
|
||||
|
||||
// Define your UI with shadow root for isolation
|
||||
const ui = await createShadowRootUi(ctx, {
|
||||
name: 'yakit-proxy-panel',
|
||||
position: 'inline',
|
||||
anchor: 'body',
|
||||
onMount: (container) => {
|
||||
// Create a wrapper div for the React app
|
||||
const app = document.createElement('div');
|
||||
app.id = 'yakit-proxy-root';
|
||||
app.className = 'yak-proxy-root';
|
||||
container.append(app);
|
||||
|
||||
// Create a root on the UI container and render a component
|
||||
const root = ReactDOM.createRoot(app);
|
||||
root.render(<App />);
|
||||
return root;
|
||||
},
|
||||
onRemove: (root) => {
|
||||
// Unmount the root when the UI is removed
|
||||
root?.unmount();
|
||||
},
|
||||
});
|
||||
|
||||
// Mount the UI
|
||||
ui.mount();
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,19 @@
|
||||
export const ProxyActionType = {
|
||||
SET_PROXY_CONFIG: "SET_PROXY_CONFIG",
|
||||
CLEAR_PROXY_CONFIG: "CLEAR_PROXY_CONFIG",
|
||||
GET_PROXY_STATUS: "GET_PROXY_STATUS",
|
||||
CLEAR_PROXY_LOGS: "CLEAR_PROXY_LOGS",
|
||||
GET_PROXY_LOGS: "GET_PROXY_LOGS",
|
||||
GET_PROXY_CONFIGS: "GET_PROXY_CONFIGS",
|
||||
ADD_PROXY_CONFIG: "ADD_PROXY_CONFIG",
|
||||
UPDATE_PROXY_CONFIG: "UPDATE_PROXY_CONFIG",
|
||||
SWITCH_PROXY: "SWITCH_PROXY",
|
||||
} as const;
|
||||
|
||||
export const ContentActionType = {
|
||||
PROXY_CONFIGS_UPDATED: "PROXY_CONFIGS_UPDATED",
|
||||
PROXY_STATUS_CHANGED: "PROXY_STATUS_CHANGED",
|
||||
TRIGGER_ADD_PROXY: "TRIGGER_ADD_PROXY",
|
||||
}
|
||||
|
||||
export type ProxyActionType = typeof ProxyActionType[keyof typeof ProxyActionType];
|
||||
@@ -0,0 +1,45 @@
|
||||
export interface PacScript {
|
||||
data?: string;
|
||||
url?: string;
|
||||
mandatory?: boolean;
|
||||
}
|
||||
|
||||
export interface ProxyConfig {
|
||||
id: string;
|
||||
name: string;
|
||||
enabled: boolean;
|
||||
proxyType: "direct" | "system" | "fixed_servers" | "pac_script" | "auto_detect";
|
||||
mode?: string;
|
||||
host?: string;
|
||||
port?: number;
|
||||
scheme?: "http" | "https" | "socks4" | "socks5";
|
||||
pacScript?: PacScript;
|
||||
bypassList?: string[];
|
||||
matchList?: string[];
|
||||
}
|
||||
|
||||
export interface ProxyLog {
|
||||
id: string;
|
||||
timestamp: number;
|
||||
url: string;
|
||||
proxyId: string;
|
||||
proxyName: string;
|
||||
status: 'success' | 'error';
|
||||
errorMessage?: string;
|
||||
method?: string;
|
||||
requestHeaders?: Record<string, string>;
|
||||
requestBody?: string;
|
||||
responseHeaders?: Record<string, string>;
|
||||
responseBody?: string;
|
||||
timing?: {
|
||||
startTime: number;
|
||||
endTime: number;
|
||||
duration: number;
|
||||
};
|
||||
protocol?: string;
|
||||
ip?: string;
|
||||
fromCache?: boolean;
|
||||
host?: string;
|
||||
port?: number;
|
||||
resourceType?: 'xhr' | 'fetch' | 'script' | 'stylesheet' | 'image' | 'other';
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
import { browser } from 'wxt/browser';
|
||||
import type { ProxyConfig } from '../types/proxy';
|
||||
import { getAllProxyConfigs, getProxyConfig, enableProxyConfig, disableAllProxies } from './storage';
|
||||
|
||||
/**
|
||||
* 获取当前激活的代理模式
|
||||
* @returns 返回当前的代理模式(direct, system, 或代理ID)
|
||||
*/
|
||||
export async function getCurrentProxyMode(): Promise<string> {
|
||||
try {
|
||||
const configs = await getAllProxyConfigs();
|
||||
const enabledProxy = configs.find(config => config.enabled);
|
||||
|
||||
if (enabledProxy) {
|
||||
return enabledProxy.id;
|
||||
}
|
||||
|
||||
// 如果没有启用的代理,返回直接连接模式
|
||||
return 'direct';
|
||||
} catch (error) {
|
||||
console.error('Error getting current proxy mode:', error);
|
||||
return 'direct';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换到指定的代理模式
|
||||
* @param mode 代理模式(ID, direct, 或 system)
|
||||
* @returns 是否成功切换
|
||||
*/
|
||||
export async function switchProxyMode(mode: string): Promise<boolean> {
|
||||
try {
|
||||
if (mode === 'direct') {
|
||||
// 清除所有代理设置
|
||||
await disableAllProxies();
|
||||
await browser.proxy.settings.clear({});
|
||||
return true;
|
||||
}
|
||||
|
||||
if (mode === 'system') {
|
||||
// 使用系统代理
|
||||
await disableAllProxies();
|
||||
await browser.proxy.settings.set({
|
||||
value: { mode: 'system' },
|
||||
scope: 'regular'
|
||||
});
|
||||
return true;
|
||||
}
|
||||
|
||||
// 使用自定义代理
|
||||
const config = await getProxyConfig(mode);
|
||||
if (!config) {
|
||||
console.error(`Proxy config with ID ${mode} not found`);
|
||||
return false;
|
||||
}
|
||||
|
||||
// 启用选定的代理配置
|
||||
await enableProxyConfig(config.id);
|
||||
|
||||
// 设置代理
|
||||
if (config.proxyType === 'direct') {
|
||||
await browser.proxy.settings.clear({});
|
||||
} else if (config.proxyType === 'system') {
|
||||
await browser.proxy.settings.set({
|
||||
value: { mode: 'system' },
|
||||
scope: 'regular'
|
||||
});
|
||||
} else {
|
||||
// 构建代理配置
|
||||
const proxyConfig = {
|
||||
mode: 'fixed_servers',
|
||||
rules: {}
|
||||
};
|
||||
|
||||
// 添加代理规则
|
||||
const proxyRule = {
|
||||
scheme: config.proxyType,
|
||||
host: config.host || '',
|
||||
port: config.port || 80
|
||||
};
|
||||
|
||||
// 添加认证信息
|
||||
// if (config.username && config.password) {
|
||||
// proxyRule.username = config.username;
|
||||
// proxyRule.password = config.password;
|
||||
// }
|
||||
|
||||
// 设置代理规则
|
||||
proxyConfig.rules = {
|
||||
singleProxy: proxyRule,
|
||||
bypassList: ['localhost', '127.0.0.1']
|
||||
};
|
||||
|
||||
// 应用代理设置
|
||||
await browser.proxy.settings.set({
|
||||
value: proxyConfig,
|
||||
scope: 'regular'
|
||||
});
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error(`Error switching to proxy mode ${mode}:`, error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
import type { ProxyConfig } from '../types/proxy';
|
||||
|
||||
// 数据库名称和存储名称
|
||||
const DB_NAME = 'yaklang_extension';
|
||||
const PROXY_STORE_NAME = 'proxy_configs';
|
||||
|
||||
// 打开数据库
|
||||
async function openDB(): Promise<IDBDatabase> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const request = indexedDB.open(DB_NAME, 1);
|
||||
|
||||
request.onerror = () => reject(request.error);
|
||||
|
||||
request.onupgradeneeded = (event) => {
|
||||
const db = (event.target as IDBOpenDBRequest).result;
|
||||
|
||||
// 如果存储不存在,创建它
|
||||
if (!db.objectStoreNames.contains(PROXY_STORE_NAME)) {
|
||||
db.createObjectStore(PROXY_STORE_NAME, { keyPath: 'id' });
|
||||
}
|
||||
};
|
||||
|
||||
request.onsuccess = () => resolve(request.result);
|
||||
});
|
||||
}
|
||||
|
||||
// 获取所有代理配置
|
||||
export async function getAllProxyConfigs(): Promise<ProxyConfig[]> {
|
||||
try {
|
||||
const db = await openDB();
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = db.transaction([PROXY_STORE_NAME], 'readonly');
|
||||
const store = transaction.objectStore(PROXY_STORE_NAME);
|
||||
const request = store.getAll();
|
||||
|
||||
request.onerror = () => reject(request.error);
|
||||
request.onsuccess = () => resolve(request.result || []);
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error getting proxy configs:', error);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
// 获取单个代理配置
|
||||
export async function getProxyConfig(id: string): Promise<ProxyConfig | null> {
|
||||
try {
|
||||
const db = await openDB();
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = db.transaction([PROXY_STORE_NAME], 'readonly');
|
||||
const store = transaction.objectStore(PROXY_STORE_NAME);
|
||||
const request = store.get(id);
|
||||
|
||||
request.onerror = () => reject(request.error);
|
||||
request.onsuccess = () => resolve(request.result || null);
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(`Error getting proxy config ${id}:`, error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
// 保存代理配置
|
||||
export async function saveProxyConfig(config: ProxyConfig): Promise<boolean> {
|
||||
try {
|
||||
const db = await openDB();
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = db.transaction([PROXY_STORE_NAME], 'readwrite');
|
||||
const store = transaction.objectStore(PROXY_STORE_NAME);
|
||||
const request = store.put(config);
|
||||
|
||||
request.onerror = () => reject(request.error);
|
||||
request.onsuccess = () => resolve(true);
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error saving proxy config:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 删除代理配置
|
||||
export async function deleteProxyConfig(id: string): Promise<boolean> {
|
||||
try {
|
||||
const db = await openDB();
|
||||
return new Promise((resolve, reject) => {
|
||||
const transaction = db.transaction([PROXY_STORE_NAME], 'readwrite');
|
||||
const store = transaction.objectStore(PROXY_STORE_NAME);
|
||||
const request = store.delete(id);
|
||||
|
||||
request.onerror = () => reject(request.error);
|
||||
request.onsuccess = () => resolve(true);
|
||||
});
|
||||
} catch (error) {
|
||||
console.error(`Error deleting proxy config ${id}:`, error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 启用指定的代理,禁用其他
|
||||
export async function enableProxyConfig(id: string): Promise<boolean> {
|
||||
try {
|
||||
const configs = await getAllProxyConfigs();
|
||||
|
||||
for (const config of configs) {
|
||||
const updated = { ...config, enabled: config.id === id };
|
||||
await saveProxyConfig(updated);
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error(`Error enabling proxy config ${id}:`, error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// 禁用所有代理
|
||||
export async function disableAllProxies(): Promise<boolean> {
|
||||
try {
|
||||
const configs = await getAllProxyConfigs();
|
||||
|
||||
for (const config of configs) {
|
||||
if (config.enabled) {
|
||||
const updated = { ...config, enabled: false };
|
||||
await saveProxyConfig(updated);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
} catch (error) {
|
||||
console.error('Error disabling all proxies:', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"extends": "./.wxt/tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"allowImportingTsExtensions": true,
|
||||
"jsx": "react-jsx"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import { defineConfig } from 'wxt';
|
||||
|
||||
// See https://wxt.dev/api/config.html
|
||||
export default defineConfig({
|
||||
srcDir: 'src',
|
||||
modules: ['@wxt-dev/module-react'],
|
||||
manifest: {
|
||||
name: 'Yaklang 代理管理',
|
||||
description: '一个用于快速切换浏览器代理设置的扩展',
|
||||
version: '0.1.0',
|
||||
permissions: [
|
||||
'proxy',
|
||||
'storage',
|
||||
'tabs'
|
||||
],
|
||||
host_permissions: [
|
||||
'<all_urls>'
|
||||
],
|
||||
web_accessible_resources: [
|
||||
{
|
||||
resources: ['yak.svg'],
|
||||
matches: ['<all_urls>']
|
||||
}
|
||||
],
|
||||
icons: {
|
||||
"16": "icon/icon16.png",
|
||||
"48": "icon/icon48.png",
|
||||
"128": "icon/icon128.png"
|
||||
},
|
||||
}
|
||||
});
|
||||
Reference in New Issue
Block a user