mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2026-01-28 16:30:52 +08:00
Add cors (#3909)
* add external `cors` control panel
* optimize format
* fix-script.rs
* fix-service.rs
* fix-rs
async_proxy_query.rs
event_driven_proxy.rs
service_ipc.rs
service.rs
sysopt.rs
* lower the prettier version number to 3.5.3
* Revert "lower the prettier version number to 3.5.3"
This reverts commit 0f1c3dfa8a.
* fix: prttier erros
* add developer environment detection and controlled the display of development environment URL
* submit required
* fix-external-controller-cors
* use the custom component ToggleButton to ensure a uniform button style
* fix-tsx
hotkey-viewer.tsx
external-controller-cors.tsx
* fix-bug_report.yml
* remove the annoying title
* fix-write overload problem
* Individual button settings
* fix-setting-clash.tsx
---------
Co-authored-by: Tunglies <77394545+Tunglies@users.noreply.github.com>
Co-authored-by: Tunglies <tunglies.dev@outlook.com>
This commit is contained in:
@@ -15,6 +15,7 @@
|
||||
- `sidecar` 模式下清理多余的内核进程,防止运行出现异常
|
||||
- 新 macOS 下 TUN 和系统代理模式托盘图标(暂测)
|
||||
- 快捷键事件通过系统通知
|
||||
- 添加外部 `cors` 控制面板
|
||||
|
||||
### 🚀 优化改进
|
||||
|
||||
|
||||
@@ -97,25 +97,6 @@ impl IClashTemp {
|
||||
config.insert("port".into(), port.into());
|
||||
config.insert("external-controller".into(), ctrl.into());
|
||||
|
||||
// 强制覆盖 external-controller-cors 字段,允许本地和 tauri 前端
|
||||
let mut cors_map = Mapping::new();
|
||||
cors_map.insert("allow-private-network".into(), true.into());
|
||||
cors_map.insert(
|
||||
"allow-origins".into(),
|
||||
vec![
|
||||
"tauri://localhost",
|
||||
"http://tauri.localhost",
|
||||
// Only enable this in dev mode
|
||||
#[cfg(feature = "verge-dev")]
|
||||
"http://localhost:3000",
|
||||
"https://yacd.metacubex.one",
|
||||
"https://metacubex.github.io",
|
||||
"https://board.zash.run.place",
|
||||
]
|
||||
.into(),
|
||||
);
|
||||
config.insert("external-controller-cors".into(), cors_map.into());
|
||||
|
||||
config
|
||||
}
|
||||
|
||||
|
||||
@@ -123,7 +123,7 @@ impl AsyncProxyQuery {
|
||||
.position(|&x| x == 0)
|
||||
.unwrap_or(url_buffer.len());
|
||||
pac_url = String::from_utf16_lossy(&url_buffer[..end_pos]);
|
||||
log::debug!(target: "app", "从注册表读取到PAC URL: {}", pac_url);
|
||||
log::debug!(target: "app", "从注册表读取到PAC URL: {pac_url}");
|
||||
}
|
||||
|
||||
// 2. 检查自动检测设置是否启用
|
||||
@@ -148,7 +148,7 @@ impl AsyncProxyQuery {
|
||||
|| (detect_query_result == 0 && detect_value_type == REG_DWORD && auto_detect != 0);
|
||||
|
||||
if pac_enabled {
|
||||
log::debug!(target: "app", "PAC配置启用: URL={}, AutoDetect={}", pac_url, auto_detect);
|
||||
log::debug!(target: "app", "PAC配置启用: URL={pac_url}, AutoDetect={auto_detect}");
|
||||
|
||||
if pac_url.is_empty() && auto_detect != 0 {
|
||||
pac_url = "auto-detect".to_string();
|
||||
@@ -361,7 +361,7 @@ impl AsyncProxyQuery {
|
||||
(proxy_server, 8080)
|
||||
};
|
||||
|
||||
log::debug!(target: "app", "从注册表读取到代理设置: {}:{}, bypass: {}", host, port, bypass_list);
|
||||
log::debug!(target: "app", "从注册表读取到代理设置: {host}:{port}, bypass: {bypass_list}");
|
||||
|
||||
Ok(AsyncSysproxy {
|
||||
enable: true,
|
||||
|
||||
@@ -534,7 +534,7 @@ impl EventDrivenProxyManager {
|
||||
let binary_path = match dirs::service_path() {
|
||||
Ok(path) => path,
|
||||
Err(e) => {
|
||||
log::error!(target: "app", "获取服务路径失败: {}", e);
|
||||
log::error!(target: "app", "获取服务路径失败: {e}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
@@ -554,17 +554,17 @@ impl EventDrivenProxyManager {
|
||||
match output {
|
||||
Ok(output) => {
|
||||
if !output.status.success() {
|
||||
log::error!(target: "app", "执行sysproxy命令失败: {:?}", args);
|
||||
log::error!(target: "app", "执行sysproxy命令失败: {args:?}");
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
if !stderr.is_empty() {
|
||||
log::error!(target: "app", "sysproxy错误输出: {}", stderr);
|
||||
log::error!(target: "app", "sysproxy错误输出: {stderr}");
|
||||
}
|
||||
} else {
|
||||
log::debug!(target: "app", "成功执行sysproxy命令: {:?}", args);
|
||||
log::debug!(target: "app", "成功执行sysproxy命令: {args:?}");
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!(target: "app", "执行sysproxy命令出错: {}", e);
|
||||
log::error!(target: "app", "执行sysproxy命令出错: {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -218,7 +218,7 @@ pub async fn reinstall_service() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
Err(err) => {
|
||||
let error = format!("failed to install service: {}", err);
|
||||
let error = format!("failed to install service: {err}");
|
||||
service_state.last_error = Some(error.clone());
|
||||
service_state.prefer_sidecar = true;
|
||||
service_state.save()?;
|
||||
@@ -346,7 +346,7 @@ pub async fn reinstall_service() -> Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
Err(err) => {
|
||||
let error = format!("failed to install service: {}", err);
|
||||
let error = format!("failed to install service: {err}");
|
||||
service_state.last_error = Some(error.clone());
|
||||
service_state.prefer_sidecar = true;
|
||||
service_state.save()?;
|
||||
|
||||
@@ -131,7 +131,7 @@ pub async fn send_ipc_request(
|
||||
|
||||
logging!(info, Type::Service, true, "正在连接服务 (Windows)...");
|
||||
|
||||
let command_type = format!("{:?}", command);
|
||||
let command_type = format!("{command:?}");
|
||||
|
||||
let request = match create_signed_request(command, payload) {
|
||||
Ok(req) => req,
|
||||
|
||||
@@ -153,7 +153,7 @@ impl Sysopt {
|
||||
|
||||
let shell = app_handle.shell();
|
||||
let output = if pac_enable {
|
||||
let address = format!("http://{}:{}/commands/pac", proxy_host, pac_port);
|
||||
let address = format!("http://{proxy_host}:{pac_port}/commands/pac");
|
||||
let output = shell
|
||||
.command(sysproxy_exe.as_path().to_str().unwrap())
|
||||
.args(["pac", address.as_str()])
|
||||
@@ -162,7 +162,7 @@ impl Sysopt {
|
||||
.unwrap();
|
||||
output
|
||||
} else {
|
||||
let address = format!("{}:{}", proxy_host, port);
|
||||
let address = format!("{proxy_host}:{port}");
|
||||
let bypass = get_bypass();
|
||||
let output = shell
|
||||
.command(sysproxy_exe.as_path().to_str().unwrap())
|
||||
@@ -248,14 +248,14 @@ impl Sysopt {
|
||||
{
|
||||
if is_enable {
|
||||
if let Err(e) = startup_shortcut::create_shortcut() {
|
||||
log::error!(target: "app", "创建启动快捷方式失败: {}", e);
|
||||
log::error!(target: "app", "创建启动快捷方式失败: {e}");
|
||||
// 如果快捷方式创建失败,回退到原来的方法
|
||||
self.try_original_autostart_method(is_enable);
|
||||
} else {
|
||||
return Ok(());
|
||||
}
|
||||
} else if let Err(e) = startup_shortcut::remove_shortcut() {
|
||||
log::error!(target: "app", "删除启动快捷方式失败: {}", e);
|
||||
log::error!(target: "app", "删除启动快捷方式失败: {e}");
|
||||
self.try_original_autostart_method(is_enable);
|
||||
} else {
|
||||
return Ok(());
|
||||
@@ -290,11 +290,11 @@ impl Sysopt {
|
||||
{
|
||||
match startup_shortcut::is_shortcut_enabled() {
|
||||
Ok(enabled) => {
|
||||
log::info!(target: "app", "快捷方式自启动状态: {}", enabled);
|
||||
log::info!(target: "app", "快捷方式自启动状态: {enabled}");
|
||||
return Ok(enabled);
|
||||
}
|
||||
Err(e) => {
|
||||
log::error!(target: "app", "检查快捷方式失败,尝试原来的方法: {}", e);
|
||||
log::error!(target: "app", "检查快捷方式失败,尝试原来的方法: {e}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -141,8 +141,8 @@ fn test_script() {
|
||||
fn test_escape_unescape() {
|
||||
let test_string = r#"Hello "World"!\nThis is a test with \u00A9 copyright symbol."#;
|
||||
let escaped = escape_js_string_for_single_quote(test_string);
|
||||
println!("Original: {}", test_string);
|
||||
println!("Escaped: {}", escaped);
|
||||
println!("Original: {test_string}");
|
||||
println!("Escaped: {escaped}");
|
||||
|
||||
let json_str = r#"{"key":"value","nested":{"key":"value"}}"#;
|
||||
let parsed = parse_json_safely(json_str).unwrap();
|
||||
|
||||
354
src/components/setting/mods/external-controller-cors.tsx
Normal file
354
src/components/setting/mods/external-controller-cors.tsx
Normal file
@@ -0,0 +1,354 @@
|
||||
import { BaseDialog } from "@/components/base";
|
||||
import { useClash } from "@/hooks/use-clash";
|
||||
import { showNotice } from "@/services/noticeService";
|
||||
import { Delete as DeleteIcon } from "@mui/icons-material";
|
||||
import {
|
||||
Box,
|
||||
Button,
|
||||
Divider,
|
||||
List,
|
||||
ListItem,
|
||||
styled,
|
||||
TextField,
|
||||
} from "@mui/material";
|
||||
import { useLockFn, useRequest } from "ahooks";
|
||||
import { forwardRef, useImperativeHandle, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
// 自定义开关按钮样式
|
||||
const ToggleButton = styled("label")`
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 48px;
|
||||
height: 24px;
|
||||
|
||||
input {
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.slider {
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: #e0e0e0;
|
||||
transition: 0.4s;
|
||||
border-radius: 34px;
|
||||
|
||||
&:before {
|
||||
position: absolute;
|
||||
content: "";
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
left: 4px;
|
||||
bottom: 4px;
|
||||
background-color: white;
|
||||
transition: 0.4s;
|
||||
border-radius: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
input:checked + .slider {
|
||||
background-color: #2196f3;
|
||||
}
|
||||
|
||||
input:focus + .slider {
|
||||
box-shadow: 0 0 1px #2196f3;
|
||||
}
|
||||
|
||||
input:checked + .slider:before {
|
||||
transform: translateX(24px);
|
||||
}
|
||||
`;
|
||||
|
||||
// 定义开发环境的URL列表
|
||||
// 这些URL在开发模式下会被自动包含在允许的来源中
|
||||
// 在生产环境中,这些URL会被过滤掉
|
||||
// 这样可以确保在生产环境中不会意外暴露开发环境的URL
|
||||
const DEV_URLS = [
|
||||
"tauri://localhost",
|
||||
"http://tauri.localhost",
|
||||
"http://localhost:3000",
|
||||
];
|
||||
|
||||
// 判断是否处于开发模式
|
||||
const isDevMode = import.meta.env.MODE === "development";
|
||||
|
||||
// 过滤开发环境URL
|
||||
const filterDevOrigins = (origins: string[]) => {
|
||||
if (isDevMode) {
|
||||
return origins;
|
||||
}
|
||||
return origins.filter((origin: string) => !DEV_URLS.includes(origin.trim()));
|
||||
};
|
||||
|
||||
// 获取完整的源列表,包括开发URL
|
||||
const getFullOrigins = (origins: string[]) => {
|
||||
if (!isDevMode) {
|
||||
return origins;
|
||||
}
|
||||
|
||||
// 合并现有源和开发URL,并去重
|
||||
const allOrigins = [...origins, ...DEV_URLS];
|
||||
const uniqueOrigins = [...new Set(allOrigins)];
|
||||
return uniqueOrigins;
|
||||
};
|
||||
|
||||
// 统一使用的按钮样式
|
||||
const buttonStyle = {
|
||||
borderRadius: "8px",
|
||||
textTransform: "none",
|
||||
boxShadow: "0 2px 4px rgba(0,0,0,0.1)",
|
||||
transition: "all 0.3s ease",
|
||||
"&:hover": {
|
||||
boxShadow: "0 4px 8px rgba(0,0,0,0.15)",
|
||||
transform: "translateY(-1px)",
|
||||
},
|
||||
"&:active": {
|
||||
transform: "translateY(0)",
|
||||
},
|
||||
};
|
||||
|
||||
// 保存按钮样式
|
||||
const saveButtonStyle = {
|
||||
...buttonStyle,
|
||||
backgroundColor: "#165DFF",
|
||||
color: "white",
|
||||
"&:hover": {
|
||||
backgroundColor: "#0E42D2",
|
||||
},
|
||||
};
|
||||
|
||||
// 添加按钮样式
|
||||
const addButtonStyle = {
|
||||
...buttonStyle,
|
||||
backgroundColor: "#4CAF50",
|
||||
color: "white",
|
||||
"&:hover": {
|
||||
backgroundColor: "#388E3C",
|
||||
},
|
||||
};
|
||||
|
||||
// 删除按钮样式
|
||||
const deleteButtonStyle = {
|
||||
...buttonStyle,
|
||||
backgroundColor: "#FF5252",
|
||||
color: "white",
|
||||
"&:hover": {
|
||||
backgroundColor: "#D32F2F",
|
||||
},
|
||||
};
|
||||
|
||||
interface ClashHeaderConfigingRef {
|
||||
open: () => void;
|
||||
close: () => void;
|
||||
}
|
||||
|
||||
export const HeaderConfiguration = forwardRef<ClashHeaderConfigingRef>(
|
||||
(props, ref) => {
|
||||
const { t } = useTranslation();
|
||||
const { clash, mutateClash, patchClash } = useClash();
|
||||
const [open, setOpen] = useState(false);
|
||||
|
||||
// CORS配置状态管理
|
||||
const [corsConfig, setCorsConfig] = useState<{
|
||||
allowPrivateNetwork: boolean;
|
||||
allowOrigins: string[];
|
||||
}>(() => {
|
||||
const cors = clash?.["external-controller-cors"];
|
||||
const origins = cors?.["allow-origins"] ?? ["*"];
|
||||
return {
|
||||
allowPrivateNetwork: cors?.["allow-private-network"] ?? true,
|
||||
allowOrigins: filterDevOrigins(origins),
|
||||
};
|
||||
});
|
||||
|
||||
// 处理CORS配置变更
|
||||
const handleCorsConfigChange = (
|
||||
key: "allowPrivateNetwork" | "allowOrigins",
|
||||
value: boolean | string[],
|
||||
) => {
|
||||
setCorsConfig((prev) => ({
|
||||
...prev,
|
||||
[key]: value,
|
||||
}));
|
||||
};
|
||||
|
||||
// 添加新的允许来源
|
||||
const handleAddOrigin = () => {
|
||||
handleCorsConfigChange("allowOrigins", [...corsConfig.allowOrigins, ""]);
|
||||
};
|
||||
|
||||
// 更新允许来源列表中的某一项
|
||||
const handleUpdateOrigin = (index: number, value: string) => {
|
||||
const newOrigins = [...corsConfig.allowOrigins];
|
||||
newOrigins[index] = value;
|
||||
handleCorsConfigChange("allowOrigins", newOrigins);
|
||||
};
|
||||
|
||||
// 删除允许来源列表中的某一项
|
||||
const handleDeleteOrigin = (index: number) => {
|
||||
const newOrigins = [...corsConfig.allowOrigins];
|
||||
newOrigins.splice(index, 1);
|
||||
handleCorsConfigChange("allowOrigins", newOrigins);
|
||||
};
|
||||
|
||||
// 保存配置请求
|
||||
const { loading, run: saveConfig } = useRequest(
|
||||
async () => {
|
||||
// 保存时使用完整的源列表(包括开发URL)
|
||||
const fullOrigins = getFullOrigins(corsConfig.allowOrigins);
|
||||
|
||||
await patchClash({
|
||||
"external-controller-cors": {
|
||||
"allow-private-network": corsConfig.allowPrivateNetwork,
|
||||
"allow-origins": fullOrigins.filter(
|
||||
(origin: string) => origin.trim() !== "",
|
||||
),
|
||||
},
|
||||
});
|
||||
await mutateClash();
|
||||
},
|
||||
{
|
||||
manual: true,
|
||||
onSuccess: () => {
|
||||
setOpen(false);
|
||||
showNotice("success", t("Configuration saved successfully"));
|
||||
},
|
||||
onError: () => {
|
||||
showNotice("error", t("Failed to save configuration"));
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
open: () => {
|
||||
const cors = clash?.["external-controller-cors"];
|
||||
const origins = cors?.["allow-origins"] ?? ["*"];
|
||||
setCorsConfig({
|
||||
allowPrivateNetwork: cors?.["allow-private-network"] ?? true,
|
||||
allowOrigins: filterDevOrigins(origins),
|
||||
});
|
||||
setOpen(true);
|
||||
},
|
||||
close: () => setOpen(false),
|
||||
}));
|
||||
|
||||
const handleSave = useLockFn(async () => {
|
||||
await saveConfig();
|
||||
});
|
||||
|
||||
return (
|
||||
<BaseDialog
|
||||
open={open}
|
||||
title={t("External Cors Configuration")}
|
||||
contentSx={{ width: 500 }}
|
||||
okBtn={loading ? t("Saving...") : t("Save")}
|
||||
cancelBtn={t("Cancel")}
|
||||
onClose={() => setOpen(false)}
|
||||
onCancel={() => setOpen(false)}
|
||||
onOk={handleSave}
|
||||
>
|
||||
<List sx={{ width: "90%", padding: 2 }}>
|
||||
<ListItem sx={{ padding: "8px 0" }}>
|
||||
<Box
|
||||
display="flex"
|
||||
justifyContent="space-between"
|
||||
alignItems="center"
|
||||
width="100%"
|
||||
>
|
||||
<span style={{ fontWeight: "normal" }}>
|
||||
{t("Allow private network access")}
|
||||
</span>
|
||||
<ToggleButton>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={corsConfig.allowPrivateNetwork}
|
||||
onChange={(e) =>
|
||||
handleCorsConfigChange(
|
||||
"allowPrivateNetwork",
|
||||
e.target.checked,
|
||||
)
|
||||
}
|
||||
id="private-network-toggle"
|
||||
/>
|
||||
<span className="slider"></span>
|
||||
</ToggleButton>
|
||||
</Box>
|
||||
</ListItem>
|
||||
|
||||
<Divider sx={{ my: 2 }} />
|
||||
|
||||
<ListItem sx={{ padding: "8px 0" }}>
|
||||
<div style={{ width: "100%" }}>
|
||||
<div style={{ marginBottom: 8, fontWeight: "bold" }}>
|
||||
{t("Allowed Origins")}
|
||||
</div>
|
||||
{corsConfig.allowOrigins.map((origin, index) => (
|
||||
<div
|
||||
key={index}
|
||||
style={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
marginBottom: 8,
|
||||
}}
|
||||
>
|
||||
<TextField
|
||||
fullWidth
|
||||
size="small"
|
||||
sx={{ fontSize: 14, marginRight: 2 }}
|
||||
value={origin}
|
||||
onChange={(e) => handleUpdateOrigin(index, e.target.value)}
|
||||
placeholder={t("Please enter a valid url")}
|
||||
inputProps={{ style: { fontSize: 14 } }}
|
||||
/>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="error"
|
||||
size="small"
|
||||
onClick={() => handleDeleteOrigin(index)}
|
||||
disabled={corsConfig.allowOrigins.length <= 1}
|
||||
sx={deleteButtonStyle}
|
||||
>
|
||||
<DeleteIcon fontSize="small" />
|
||||
</Button>
|
||||
</div>
|
||||
))}
|
||||
<Button
|
||||
variant="contained"
|
||||
size="small"
|
||||
onClick={handleAddOrigin}
|
||||
sx={addButtonStyle}
|
||||
>
|
||||
{t("Add")}
|
||||
</Button>
|
||||
|
||||
{isDevMode && (
|
||||
<div
|
||||
style={{
|
||||
marginTop: 12,
|
||||
padding: 8,
|
||||
backgroundColor: "#f5f5f5",
|
||||
borderRadius: 4,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
style={{ color: "#666", fontSize: 12, fontStyle: "italic" }}
|
||||
>
|
||||
{t(
|
||||
"Development mode: Automatically includes Tauri and localhost origins",
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</ListItem>
|
||||
</List>
|
||||
</BaseDialog>
|
||||
);
|
||||
},
|
||||
);
|
||||
@@ -1,12 +1,62 @@
|
||||
import { forwardRef, useImperativeHandle, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { useLockFn } from "ahooks";
|
||||
import { styled, Typography, Switch } from "@mui/material";
|
||||
import { styled, Typography } from "@mui/material";
|
||||
import { useVerge } from "@/hooks/use-verge";
|
||||
import { BaseDialog, DialogRef } from "@/components/base";
|
||||
import { HotkeyInput } from "./hotkey-input";
|
||||
import { showNotice } from "@/services/noticeService";
|
||||
|
||||
// 修复后的自定义开关组件
|
||||
const ToggleButton = styled("label")`
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
width: 48px;
|
||||
height: 24px;
|
||||
|
||||
input {
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.slider {
|
||||
position: absolute;
|
||||
cursor: pointer;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: #e0e0e0;
|
||||
transition: 0.4s;
|
||||
border-radius: 34px;
|
||||
|
||||
&:before {
|
||||
position: absolute;
|
||||
content: "";
|
||||
height: 16px;
|
||||
width: 16px;
|
||||
left: 4px;
|
||||
bottom: 4px;
|
||||
background-color: white;
|
||||
transition: 0.4s;
|
||||
border-radius: 50%;
|
||||
}
|
||||
}
|
||||
|
||||
input:checked + .slider {
|
||||
background-color: #2196f3;
|
||||
}
|
||||
|
||||
input:focus + .slider {
|
||||
box-shadow: 0 0 1px #2196f3;
|
||||
}
|
||||
|
||||
input:checked + .slider:before {
|
||||
transform: translateX(24px);
|
||||
}
|
||||
`;
|
||||
|
||||
const ItemWrapper = styled("div")`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -97,11 +147,15 @@ export const HotkeyViewer = forwardRef<DialogRef>((props, ref) => {
|
||||
>
|
||||
<ItemWrapper style={{ marginBottom: 16 }}>
|
||||
<Typography>{t("Enable Global Hotkey")}</Typography>
|
||||
<Switch
|
||||
edge="end"
|
||||
checked={enableGlobalHotkey}
|
||||
onChange={(e) => setEnableHotkey(e.target.checked)}
|
||||
/>
|
||||
<ToggleButton>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={enableGlobalHotkey}
|
||||
onChange={(e) => setEnableHotkey(e.target.checked)}
|
||||
id="global-hotkey-toggle"
|
||||
/>
|
||||
<span className="slider"></span>
|
||||
</ToggleButton>
|
||||
</ItemWrapper>
|
||||
|
||||
{HOTKEY_FUNC.map((func) => (
|
||||
|
||||
@@ -21,6 +21,7 @@ import { GuardState } from "./mods/guard-state";
|
||||
import { NetworkInterfaceViewer } from "./mods/network-interface-viewer";
|
||||
import { SettingItem, SettingList } from "./mods/setting-comp";
|
||||
import { WebUIViewer } from "./mods/web-ui-viewer";
|
||||
import { HeaderConfiguration } from "./mods/external-controller-cors";
|
||||
|
||||
const isWIN = getSystem() === "windows";
|
||||
|
||||
@@ -57,6 +58,7 @@ const SettingClash = ({ onError }: Props) => {
|
||||
const coreRef = useRef<DialogRef>(null);
|
||||
const networkRef = useRef<DialogRef>(null);
|
||||
const dnsRef = useRef<DialogRef>(null);
|
||||
const corsRef = useRef<DialogRef>(null);
|
||||
|
||||
const onSwitchFormat = (_e: any, value: boolean) => value;
|
||||
const onChangeData = (patch: Partial<IConfigData>) => {
|
||||
@@ -101,6 +103,7 @@ const SettingClash = ({ onError }: Props) => {
|
||||
<ClashCoreViewer ref={coreRef} />
|
||||
<NetworkInterfaceViewer ref={networkRef} />
|
||||
<DnsViewer ref={dnsRef} />
|
||||
<HeaderConfiguration ref={corsRef} />
|
||||
|
||||
<SettingItem
|
||||
label={t("Allow Lan")}
|
||||
@@ -215,18 +218,20 @@ const SettingClash = ({ onError }: Props) => {
|
||||
</SettingItem>
|
||||
|
||||
<SettingItem
|
||||
onClick={() => ctrlRef.current?.open()}
|
||||
label={
|
||||
<>
|
||||
{t("External")}
|
||||
<TooltipIcon
|
||||
title={t(
|
||||
"Enable one-click random API port and key. Click to randomize the port and key",
|
||||
)}
|
||||
sx={{ opacity: "0.7" }}
|
||||
/>
|
||||
</>
|
||||
label={<>{t("External")}</>}
|
||||
extra={
|
||||
<TooltipIcon
|
||||
title={t("External Cors Settings")}
|
||||
icon={SettingsRounded}
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
corsRef.current?.open();
|
||||
}}
|
||||
/>
|
||||
}
|
||||
onClick={() => {
|
||||
ctrlRef.current?.open();
|
||||
}}
|
||||
/>
|
||||
|
||||
<SettingItem onClick={() => webRef.current?.open()} label={t("Web UI")} />
|
||||
|
||||
@@ -639,5 +639,13 @@
|
||||
"AppHiddenTitle": "APP Hidden",
|
||||
"AppHiddenBody": "APP window hidden by hotkey",
|
||||
"Invalid Profile URL": "Invalid profile URL. Please enter a URL starting with http:// or https://",
|
||||
"Saved Successfully": "Saved successfully"
|
||||
"Saved Successfully": "Saved successfully",
|
||||
"External Cors": "External Cors",
|
||||
"Enable one-click CORS for external API. Click to toggle CORS": "Enable one-click CORS for external API. Click to toggle CORS",
|
||||
"External Cors Configuration": "External Cors Configuration",
|
||||
"Allow private network access": "Allow private network access",
|
||||
"Allowed Origins": "Allowed Origins",
|
||||
"Please enter a valid url": "Please enter a valid url",
|
||||
"Add": "Add",
|
||||
"Development mode: Automatically includes Tauri and localhost origins": "Development mode: Automatically includes Tauri and localhost origins"
|
||||
}
|
||||
|
||||
@@ -639,5 +639,13 @@
|
||||
"AppHiddenTitle": "应用隐藏",
|
||||
"AppHiddenBody": "已通过快捷键隐藏应用窗口",
|
||||
"Invalid Profile URL": "无效的订阅链接,请输入以 http:// 或 https:// 开头的地址",
|
||||
"Saved Successfully": "保存成功"
|
||||
"Saved Successfully": "保存成功",
|
||||
"External Cors": "外部控制跨域",
|
||||
"Enable one-click CORS for external API. Click to toggle CORS": "设置内核跨域访问,点击切换 CORS是否启用",
|
||||
"External Cors Configuration": "外部控制跨域配置",
|
||||
"Allow private network access": "允许专用网络访问",
|
||||
"Allowed Origins": "允许的来源",
|
||||
"Please enter a valid url": "请输入有效的网址",
|
||||
"Add": "添加",
|
||||
"Development mode: Automatically includes Tauri and localhost origins": "开发模式:自动包含 Tauri 和 localhost 来源"
|
||||
}
|
||||
|
||||
4
src/services/types.d.ts
vendored
4
src/services/types.d.ts
vendored
@@ -31,6 +31,10 @@ interface IConfigData {
|
||||
"socks-port": number;
|
||||
"tproxy-port": number;
|
||||
"external-controller": string;
|
||||
"external-controller-cors": {
|
||||
"allow-private-network": boolean;
|
||||
"allow-origins": string[];
|
||||
};
|
||||
secret: string;
|
||||
"unified-delay": boolean;
|
||||
tun: {
|
||||
|
||||
Reference in New Issue
Block a user