mirror of
https://github.com/yaklang/yaklang-chrome-extension.git
synced 2026-09-25 12:41:53 +08:00
init wxt framework
This commit is contained in:
@@ -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();
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user