mirror of
https://github.com/yaklang/yaklang-chrome-extension.git
synced 2026-09-22 03:10:43 +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.zip
|
||||||
build.pem
|
build.pem
|
||||||
.idea/
|
.idea/
|
||||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
# Logs
|
||||||
|
logs
|
||||||
# dependencies
|
*.log
|
||||||
/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
|
|
||||||
|
|
||||||
npm-debug.log*
|
npm-debug.log*
|
||||||
yarn-debug.log*
|
yarn-debug.log*
|
||||||
yarn-error.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/
|
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