init wxt framework

This commit is contained in:
go0p
2025-04-14 17:10:10 +08:00
parent 273ea4878f
commit dde16515ec
34 changed files with 8126 additions and 20 deletions
+36
View File
@@ -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('代理管理后台服务已启动');
},
});
+6
View File
@@ -0,0 +1,6 @@
export default defineContentScript({
matches: ['*://*.google.com/*'],
main() {
console.log('Hello content.');
},
});
+39
View File
@@ -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%;
}
+242
View File
@@ -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>
);
}
+13
View File
@@ -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>
+7
View File
@@ -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 />);
+9
View File
@@ -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;
}
+73
View File
@@ -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;
}
+25
View File
@@ -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>
);
}
+13
View File
@@ -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>
+10
View File
@@ -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>,
);
+69
View File
@@ -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;
}
}
+303
View File
@@ -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;
}
+356
View File
@@ -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;
+39
View File
@@ -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();
},
});