Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9b9cc90414 | ||
|
|
c1eb539a5c | ||
|
|
e1793f57ef | ||
|
|
dbd09a8743 | ||
|
|
0d189ca617 | ||
|
|
dc9bcc40ee | ||
|
|
4991f7ff39 | ||
|
|
a393b8b122 | ||
|
|
c73b354386 | ||
|
|
392ecee3ff | ||
|
|
bae721c49e | ||
|
|
4e806e21a6 | ||
|
|
ec0fdf83b2 | ||
|
|
cb94d8414f | ||
|
|
8890051c17 | ||
|
|
cf00c9476f |
8
.github/workflows/ci.yml
vendored
8
.github/workflows/ci.yml
vendored
@@ -53,9 +53,9 @@ jobs:
|
||||
yarn run check
|
||||
|
||||
- name: Tauri build
|
||||
uses: tauri-apps/tauri-action@b9ce5d7dc68082d21d30a60103b0ab8c5ddae3a1
|
||||
uses: tauri-apps/tauri-action@0e558392ccadcb49bcc89e7df15a400e8f0c954d
|
||||
# enable cache even though failed
|
||||
continue-on-error: true
|
||||
# continue-on-error: true
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }}
|
||||
@@ -67,6 +67,10 @@ jobs:
|
||||
releaseDraft: false
|
||||
prerelease: true
|
||||
|
||||
- name: Green zip bundle
|
||||
run: |
|
||||
yarn run green
|
||||
|
||||
release-update:
|
||||
needs: release
|
||||
runs-on: macos-11
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "clash-verge",
|
||||
"version": "0.0.22",
|
||||
"version": "0.0.23",
|
||||
"license": "GPL-3.0",
|
||||
"scripts": {
|
||||
"dev": "tauri dev",
|
||||
@@ -10,6 +10,7 @@
|
||||
"web:build": "tsc && vite build",
|
||||
"web:serve": "vite preview",
|
||||
"check": "node scripts/check.mjs",
|
||||
"green": "node scripts/green.mjs",
|
||||
"publish": "node scripts/publish.mjs",
|
||||
"release": "node scripts/release.mjs",
|
||||
"prepare": "husky install"
|
||||
@@ -23,8 +24,10 @@
|
||||
"ahooks": "^3.1.13",
|
||||
"axios": "^0.26.0",
|
||||
"dayjs": "^1.10.8",
|
||||
"i18next": "^21.6.14",
|
||||
"react": "^17.0.2",
|
||||
"react-dom": "^17.0.2",
|
||||
"react-i18next": "^11.15.6",
|
||||
"react-router-dom": "^6.2.2",
|
||||
"react-virtuoso": "^2.7.0",
|
||||
"recoil": "^0.6.1",
|
||||
|
||||
56
scripts/green.mjs
Normal file
56
scripts/green.mjs
Normal file
@@ -0,0 +1,56 @@
|
||||
import fs from "fs-extra";
|
||||
import path from "path";
|
||||
import AdmZip from "adm-zip";
|
||||
import { createRequire } from "module";
|
||||
import { getOctokit, context } from "@actions/github";
|
||||
|
||||
/// Script for ci
|
||||
/// 打包绿色版/便携版 (only Windows)
|
||||
async function resolveGreen() {
|
||||
if (process.platform !== "win32") return;
|
||||
|
||||
const releaseDir = "./src-tauri/target/release";
|
||||
|
||||
if (!(await fs.pathExists(releaseDir))) {
|
||||
throw new Error("could not found the release dir");
|
||||
}
|
||||
|
||||
const zip = new AdmZip();
|
||||
|
||||
zip.addLocalFile(path.join(releaseDir, "Clash Verge.exe"));
|
||||
zip.addLocalFile(path.join(releaseDir, "clash.exe"));
|
||||
zip.addLocalFolder(path.join(releaseDir, "resources"), "resources");
|
||||
|
||||
const require = createRequire(import.meta.url);
|
||||
const packageJson = require("../package.json");
|
||||
const { version } = packageJson;
|
||||
|
||||
const zipFile = `Clash.Verge_${version}_x64_green.zip`;
|
||||
zip.writeZip(zipFile);
|
||||
|
||||
console.log("[INFO]: create green zip successfully");
|
||||
|
||||
// push release assets
|
||||
if (process.env.GITHUB_TOKEN === undefined) {
|
||||
throw new Error("GITHUB_TOKEN is required");
|
||||
}
|
||||
|
||||
const options = { owner: context.repo.owner, repo: context.repo.repo };
|
||||
const github = getOctokit(process.env.GITHUB_TOKEN);
|
||||
|
||||
const { data: release } = await github.rest.repos.getReleaseByTag({
|
||||
...options,
|
||||
tag: `v${version}`,
|
||||
});
|
||||
|
||||
console.log(release.name);
|
||||
|
||||
await github.rest.repos.uploadReleaseAsset({
|
||||
...options,
|
||||
release_id: release.id,
|
||||
name: zipFile,
|
||||
data: zip.toBuffer(),
|
||||
});
|
||||
}
|
||||
|
||||
resolveGreen().catch(console.error);
|
||||
@@ -59,19 +59,27 @@ pub async fn update_profile(
|
||||
clash_state: State<'_, ClashState>,
|
||||
profiles_state: State<'_, ProfilesState>,
|
||||
) -> Result<(), String> {
|
||||
let url = {
|
||||
let (url, opt) = {
|
||||
// must release the lock here
|
||||
let profiles = profiles_state.0.lock().unwrap();
|
||||
let item = wrap_err!(profiles.get_item(&index))?;
|
||||
|
||||
// check the profile type
|
||||
if let Some(typ) = item.itype.as_ref() {
|
||||
if *typ != "remote" {
|
||||
ret_err!(format!("could not update the `{typ}` profile"));
|
||||
}
|
||||
}
|
||||
|
||||
if item.url.is_none() {
|
||||
ret_err!("failed to get the item url");
|
||||
}
|
||||
|
||||
item.url.clone().unwrap()
|
||||
(item.url.clone().unwrap(), item.option.clone())
|
||||
};
|
||||
|
||||
let item = wrap_err!(PrfItem::from_url(&url, None, None, option).await)?;
|
||||
let fetch_opt = PrfOption::merge(opt, option);
|
||||
let item = wrap_err!(PrfItem::from_url(&url, None, None, fetch_opt).await)?;
|
||||
|
||||
let mut profiles = profiles_state.0.lock().unwrap();
|
||||
wrap_err!(profiles.update_item(index.clone(), item))?;
|
||||
@@ -313,27 +321,54 @@ pub fn open_logs_dir() -> Result<(), String> {
|
||||
open_path_cmd(log_dir, "failed to open logs dir")
|
||||
}
|
||||
|
||||
/// get open/explorer command
|
||||
fn open_path_cmd(dir: PathBuf, err_str: &str) -> Result<(), String> {
|
||||
/// use the os default open command to open file or dir
|
||||
fn open_path_cmd(path: PathBuf, err_str: &str) -> Result<(), String> {
|
||||
let result;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
use std::os::windows::process::CommandExt;
|
||||
|
||||
if let Err(err) = Command::new("explorer")
|
||||
result = Command::new("explorer")
|
||||
.creation_flags(0x08000000)
|
||||
.arg(dir)
|
||||
.spawn()
|
||||
{
|
||||
log::error!("{err}");
|
||||
.arg(&path)
|
||||
.spawn();
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
result = Command::new("open").arg(&path).spawn();
|
||||
}
|
||||
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
result = Command::new("xdg-open").arg(&path).spawn();
|
||||
}
|
||||
|
||||
match result {
|
||||
Ok(child) => match child.wait_with_output() {
|
||||
Ok(out) => {
|
||||
if let Some(code) = out.status.code() {
|
||||
if code != 0 {
|
||||
log::error!(
|
||||
"failed to open path {:?} for {} (code {code})",
|
||||
&path,
|
||||
String::from_utf8_lossy(&out.stderr),
|
||||
);
|
||||
return Err(err_str.into());
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
log::error!("failed to open path {:?} for {err}", &path);
|
||||
return Err(err_str.into());
|
||||
}
|
||||
},
|
||||
Err(err) => {
|
||||
log::error!("failed to open path {:?} for {err}", &path);
|
||||
return Err(err_str.into());
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
if let Err(err) = Command::new("open").arg(dir).spawn() {
|
||||
log::error!("{err}");
|
||||
return Err(err_str.into());
|
||||
}
|
||||
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
@@ -95,7 +95,18 @@ impl Clash {
|
||||
|
||||
let server = match clash_config.get(&key_server) {
|
||||
Some(value) => match value {
|
||||
Value::String(val_str) => Some(val_str.clone()),
|
||||
Value::String(val_str) => {
|
||||
// `external-controller` could be
|
||||
// "127.0.0.1:9090" or ":9090"
|
||||
// Todo: maybe it could support single port
|
||||
let server = val_str.clone();
|
||||
let server = match server.starts_with(":") {
|
||||
true => format!("127.0.0.1{server}"),
|
||||
false => server,
|
||||
};
|
||||
|
||||
Some(server)
|
||||
}
|
||||
_ => None,
|
||||
},
|
||||
_ => None,
|
||||
@@ -253,19 +264,13 @@ impl Clash {
|
||||
/// activate the profile
|
||||
/// generate a new profile to the temp_dir
|
||||
/// then put the path to the clash core
|
||||
fn _activate(info: ClashInfo, config: Mapping) -> Result<()> {
|
||||
fn _activate(info: ClashInfo, config: Mapping, window: Option<Window>) -> Result<()> {
|
||||
let temp_path = dirs::profiles_temp_path();
|
||||
config::save_yaml(temp_path.clone(), &config, Some("# Clash Verge Temp File"))?;
|
||||
|
||||
tauri::async_runtime::spawn(async move {
|
||||
// `external-controller` could be
|
||||
// "127.0.0.1:9090" or ":9090"
|
||||
// Todo: maybe it could support single port
|
||||
let server = info.server.unwrap();
|
||||
let server = match server.starts_with(":") {
|
||||
true => format!("http://127.0.0.1{server}/configs"),
|
||||
false => format!("http://{server}/configs"),
|
||||
};
|
||||
let server = format!("http://{server}/configs");
|
||||
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.insert("Content-Type", "application/json".parse().unwrap());
|
||||
@@ -289,6 +294,12 @@ impl Clash {
|
||||
if resp.status() != 204 {
|
||||
log::error!("failed to activate clash for status \"{}\"", resp.status());
|
||||
}
|
||||
|
||||
// emit the window to update something
|
||||
if let Some(window) = window {
|
||||
window.emit("verge://refresh-clash-config", "yes").unwrap();
|
||||
}
|
||||
|
||||
// do not retry
|
||||
break;
|
||||
}
|
||||
@@ -320,6 +331,7 @@ impl Clash {
|
||||
|
||||
// generate the payload
|
||||
let payload = profiles.gen_enhanced(event_name.clone())?;
|
||||
let window = self.window.clone();
|
||||
|
||||
win.once(&event_name, move |event| {
|
||||
if let Some(result) = event.payload() {
|
||||
@@ -328,7 +340,7 @@ impl Clash {
|
||||
if let Some(data) = result.data {
|
||||
// all of these can not be revised by script
|
||||
// http/https/socks port should be under control
|
||||
let not_allow: Vec<Value> = vec![
|
||||
let not_allow = vec![
|
||||
"port",
|
||||
"socks-port",
|
||||
"mixed-port",
|
||||
@@ -337,23 +349,29 @@ impl Clash {
|
||||
"external-controller",
|
||||
"secret",
|
||||
"log-level",
|
||||
]
|
||||
.iter()
|
||||
.map(|&i| Value::from(i))
|
||||
.collect();
|
||||
];
|
||||
|
||||
for (key, value) in data.into_iter() {
|
||||
if not_allow.iter().find(|&i| i == &key).is_none() {
|
||||
config.insert(key, value);
|
||||
}
|
||||
key.as_str().map(|key_str| {
|
||||
// change to lowercase
|
||||
let mut key_str = String::from(key_str);
|
||||
key_str.make_ascii_lowercase();
|
||||
|
||||
// filter
|
||||
if !not_allow.contains(&&*key_str) {
|
||||
config.insert(Value::String(key_str), value);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Self::_activate(info, config).unwrap();
|
||||
log::info!("profile enhanced status {}", result.status);
|
||||
|
||||
Self::_activate(info, config, window).unwrap();
|
||||
}
|
||||
|
||||
log::info!("profile enhanced status {}", result.status);
|
||||
|
||||
result.error.map(|error| log::error!("{error}"));
|
||||
if let Some(error) = result.error {
|
||||
log::error!("{error}");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -379,7 +397,7 @@ impl Clash {
|
||||
config.insert(key, value);
|
||||
}
|
||||
|
||||
Self::_activate(info, config)?;
|
||||
Self::_activate(info, config, self.window.clone())?;
|
||||
self.activate_enhanced(profiles, delay)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,6 +73,31 @@ pub struct PrfOption {
|
||||
pub with_proxy: Option<bool>,
|
||||
}
|
||||
|
||||
impl PrfOption {
|
||||
pub fn merge(one: Option<Self>, other: Option<Self>) -> Option<Self> {
|
||||
if one.is_some() && other.is_some() {
|
||||
let mut one = one.unwrap();
|
||||
let other = other.unwrap();
|
||||
|
||||
if let Some(val) = other.user_agent {
|
||||
one.user_agent = Some(val);
|
||||
}
|
||||
|
||||
if let Some(val) = other.with_proxy {
|
||||
one.with_proxy = Some(val);
|
||||
}
|
||||
|
||||
return Some(one);
|
||||
}
|
||||
|
||||
if one.is_none() {
|
||||
return other;
|
||||
}
|
||||
|
||||
return one;
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for PrfItem {
|
||||
fn default() -> Self {
|
||||
PrfItem {
|
||||
@@ -414,9 +439,7 @@ impl Profiles {
|
||||
patch!(each, item, selected);
|
||||
patch!(each, item, extra);
|
||||
patch!(each, item, updated);
|
||||
|
||||
// can be removed
|
||||
each.option = item.option;
|
||||
patch!(each, item, option);
|
||||
|
||||
self.items = Some(items);
|
||||
return self.save_file();
|
||||
@@ -543,13 +566,17 @@ impl Profiles {
|
||||
"rules",
|
||||
];
|
||||
|
||||
valid_keys.iter().for_each(|key| {
|
||||
let key = Value::String(key.to_string());
|
||||
if def_config.contains_key(&key) {
|
||||
let value = def_config[&key].clone();
|
||||
new_config.insert(key, value);
|
||||
}
|
||||
});
|
||||
for (key, value) in def_config.into_iter() {
|
||||
key.as_str().map(|key_str| {
|
||||
// change to lowercase
|
||||
let mut key_str = String::from(key_str);
|
||||
key_str.make_ascii_lowercase();
|
||||
|
||||
if valid_keys.contains(&&*key_str) {
|
||||
new_config.insert(Value::String(key_str), value);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return Ok(new_config);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::log_if_err;
|
||||
use crate::{
|
||||
core::Clash,
|
||||
log_if_err,
|
||||
utils::{config, dirs, sysopt::SysProxyConfig},
|
||||
};
|
||||
use anyhow::{bail, Result};
|
||||
@@ -12,6 +12,9 @@ use tauri::{async_runtime::Mutex, utils::platform::current_exe};
|
||||
/// ### `verge.yaml` schema
|
||||
#[derive(Default, Debug, Clone, Deserialize, Serialize)]
|
||||
pub struct VergeConfig {
|
||||
// i18n
|
||||
pub language: Option<String>,
|
||||
|
||||
/// `light` or `dark`
|
||||
pub theme_mode: Option<String>,
|
||||
|
||||
@@ -188,6 +191,9 @@ impl Verge {
|
||||
/// so call the save_file at the end is savely
|
||||
pub fn patch_config(&mut self, patch: VergeConfig) -> Result<()> {
|
||||
// only change it
|
||||
if patch.language.is_some() {
|
||||
self.config.language = patch.language;
|
||||
}
|
||||
if patch.theme_mode.is_some() {
|
||||
self.config.theme_mode = patch.theme_mode;
|
||||
}
|
||||
|
||||
@@ -45,13 +45,8 @@ fn main() -> std::io::Result<()> {
|
||||
let profiles_state = app_handle.state::<states::ProfilesState>();
|
||||
let mut clash = clash_state.0.lock().unwrap();
|
||||
let mut profiles = profiles_state.0.lock().unwrap();
|
||||
match clash.restart_sidecar(&mut profiles) {
|
||||
Ok(_) => {
|
||||
let window = app_handle.get_window("main").unwrap();
|
||||
window.emit("restart_clash", "yes").unwrap();
|
||||
}
|
||||
Err(err) => log::error!("{}", err),
|
||||
}
|
||||
|
||||
crate::log_if_err!(clash.restart_sidecar(&mut profiles));
|
||||
}
|
||||
"quit" => {
|
||||
resolve::resolve_reset(app_handle);
|
||||
|
||||
@@ -68,7 +68,7 @@ macro_rules! wrap_err {
|
||||
/// return the string literal error
|
||||
#[macro_export]
|
||||
macro_rules! ret_err {
|
||||
($str: literal) => {
|
||||
($str: expr) => {
|
||||
return Err($str.into())
|
||||
};
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ items: ~
|
||||
/// template for `verge.yaml`
|
||||
pub const VERGE_CONFIG: &[u8] = b"# Defaulf Config For Clash Verge
|
||||
|
||||
language: en
|
||||
theme_mode: light
|
||||
theme_blur: false
|
||||
traffic_graph: true
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"package": {
|
||||
"productName": "Clash Verge",
|
||||
"version": "0.0.22"
|
||||
"version": "0.0.23"
|
||||
},
|
||||
"build": {
|
||||
"distDir": "../dist",
|
||||
|
||||
@@ -12,6 +12,7 @@ import useLogSetup from "./use-log-setup";
|
||||
import useTrafficGraph from "./use-traffic-graph";
|
||||
import parseTraffic from "../../utils/parse-traffic";
|
||||
|
||||
// setup the traffic
|
||||
const LayoutTraffic = () => {
|
||||
const portValue = useRecoilValue(atomClashPort);
|
||||
const [traffic, setTraffic] = useState({ up: 0, down: 0 });
|
||||
@@ -26,12 +27,14 @@ const LayoutTraffic = () => {
|
||||
useLogSetup();
|
||||
|
||||
useEffect(() => {
|
||||
let unlisten: () => void = null!;
|
||||
|
||||
// should reconnect the traffic ws
|
||||
listen("restart_clash", () => setRefresh({})).then((fn) => (unlisten = fn));
|
||||
const unlisten = listen("verge://refresh-clash-config", () =>
|
||||
setRefresh({})
|
||||
);
|
||||
|
||||
return () => unlisten?.();
|
||||
return () => {
|
||||
unlisten.then((fn) => fn());
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import dayjs from "dayjs";
|
||||
import { useEffect } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useSetRecoilState } from "recoil";
|
||||
import { listen } from "@tauri-apps/api/event";
|
||||
import { ApiType } from "../../services/types";
|
||||
@@ -10,11 +10,11 @@ const MAX_LOG_NUM = 1000;
|
||||
|
||||
// setup the log websocket
|
||||
export default function useLogSetup() {
|
||||
const [refresh, setRefresh] = useState({});
|
||||
const setLogData = useSetRecoilState(atomLogData);
|
||||
|
||||
useEffect(() => {
|
||||
let ws: WebSocket = null!;
|
||||
let unlisten: () => void = null!;
|
||||
|
||||
const handler = (event: MessageEvent<any>) => {
|
||||
const data = JSON.parse(event.data) as ApiType.LogItem;
|
||||
@@ -25,25 +25,19 @@ export default function useLogSetup() {
|
||||
});
|
||||
};
|
||||
|
||||
(async () => {
|
||||
const { server = "", secret = "" } = await getInfomation();
|
||||
|
||||
getInfomation().then((info) => {
|
||||
const { server = "", secret = "" } = info;
|
||||
ws = new WebSocket(`ws://${server}/logs?token=${secret}`);
|
||||
ws.addEventListener("message", handler);
|
||||
});
|
||||
|
||||
// reconnect the websocket
|
||||
unlisten = await listen("restart_clash", async () => {
|
||||
const { server = "", secret = "" } = await getInfomation();
|
||||
|
||||
ws?.close();
|
||||
ws = new WebSocket(`ws://${server}/logs?token=${secret}`);
|
||||
ws.addEventListener("message", handler);
|
||||
});
|
||||
})();
|
||||
const unlisten = listen("verge://refresh-clash-config", () =>
|
||||
setRefresh({})
|
||||
);
|
||||
|
||||
return () => {
|
||||
ws?.close();
|
||||
unlisten?.();
|
||||
unlisten?.then((fn) => fn());
|
||||
};
|
||||
}, []);
|
||||
}, [refresh]);
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ const ProfileEdit = (props: Props) => {
|
||||
try {
|
||||
const { uid } = itemData;
|
||||
const { name, desc, url } = form;
|
||||
const option_ = showOpt ? option : undefined;
|
||||
const option_ = itemData.type === "remote" ? option : undefined;
|
||||
|
||||
if (itemData.type === "remote" && !url) {
|
||||
throw new Error("Remote URL should not be null");
|
||||
|
||||
@@ -54,7 +54,7 @@ const ProfileNew = (props: Props) => {
|
||||
throw new Error("The URL should not be null");
|
||||
}
|
||||
|
||||
const option_ = showOpt ? option : undefined;
|
||||
const option_ = form.type === "remote" ? option : undefined;
|
||||
await createProfile({ ...form, name, option: option_ });
|
||||
setForm({ type: "remote", name: "", desc: "", url: "" });
|
||||
setOption({ user_agent: "" });
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import useSWR, { useSWRConfig } from "swr";
|
||||
import { useSetRecoilState } from "recoil";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
ListItemText,
|
||||
TextField,
|
||||
@@ -21,15 +22,16 @@ interface Props {
|
||||
}
|
||||
|
||||
const SettingClash = ({ onError }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const { mutate } = useSWRConfig();
|
||||
const { data: clashConfig } = useSWR("getClashConfig", getClashConfig);
|
||||
const { data: versionData } = useSWR("getVersion", getVersion);
|
||||
|
||||
const {
|
||||
ipv6 = false,
|
||||
"allow-lan": allowLan = false,
|
||||
"log-level": logLevel = "silent",
|
||||
"mixed-port": mixedPort = 0,
|
||||
ipv6,
|
||||
"allow-lan": allowLan,
|
||||
"log-level": logLevel,
|
||||
"mixed-port": mixedPort,
|
||||
} = clashConfig ?? {};
|
||||
|
||||
const setGlobalClashPort = useSetRecoilState(atomClashPort);
|
||||
@@ -64,11 +66,11 @@ const SettingClash = ({ onError }: Props) => {
|
||||
: versionData?.version || "-";
|
||||
|
||||
return (
|
||||
<SettingList title="Clash Setting">
|
||||
<SettingList title={t("Clash Setting")}>
|
||||
<SettingItem>
|
||||
<ListItemText primary="Allow Lan" />
|
||||
<ListItemText primary={t("Allow Lan")} />
|
||||
<GuardState
|
||||
value={allowLan}
|
||||
value={allowLan ?? false}
|
||||
valueProps="checked"
|
||||
onCatch={onError}
|
||||
onFormat={onSwitchFormat}
|
||||
@@ -80,9 +82,9 @@ const SettingClash = ({ onError }: Props) => {
|
||||
</SettingItem>
|
||||
|
||||
<SettingItem>
|
||||
<ListItemText primary="IPv6" />
|
||||
<ListItemText primary={t("IPv6")} />
|
||||
<GuardState
|
||||
value={ipv6}
|
||||
value={ipv6 ?? false}
|
||||
valueProps="checked"
|
||||
onCatch={onError}
|
||||
onFormat={onSwitchFormat}
|
||||
@@ -94,9 +96,9 @@ const SettingClash = ({ onError }: Props) => {
|
||||
</SettingItem>
|
||||
|
||||
<SettingItem>
|
||||
<ListItemText primary="Log Level" />
|
||||
<ListItemText primary={t("Log Level")} />
|
||||
<GuardState
|
||||
value={logLevel}
|
||||
value={logLevel ?? "info"}
|
||||
onCatch={onError}
|
||||
onFormat={(e: any) => e.target.value}
|
||||
onChange={(e) => onChangeData({ "log-level": e })}
|
||||
@@ -113,9 +115,9 @@ const SettingClash = ({ onError }: Props) => {
|
||||
</SettingItem>
|
||||
|
||||
<SettingItem>
|
||||
<ListItemText primary="Mixed Port" />
|
||||
<ListItemText primary={t("Mixed Port")} />
|
||||
<GuardState
|
||||
value={mixedPort!}
|
||||
value={mixedPort ?? 0}
|
||||
onCatch={onError}
|
||||
onFormat={(e: any) => +e.target.value?.replace(/\D+/, "")}
|
||||
onChange={(e) => onChangeData({ "mixed-port": e })}
|
||||
@@ -127,7 +129,7 @@ const SettingClash = ({ onError }: Props) => {
|
||||
</SettingItem>
|
||||
|
||||
<SettingItem>
|
||||
<ListItemText primary="Clash core" />
|
||||
<ListItemText primary={t("Clash core")} />
|
||||
<Typography sx={{ py: 1 }}>{clashVer}</Typography>
|
||||
</SettingItem>
|
||||
</SettingList>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import useSWR, { useSWRConfig } from "swr";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Box, ListItemText, Switch, TextField } from "@mui/material";
|
||||
import { getVergeConfig, patchVergeConfig } from "../../services/cmds";
|
||||
import { SettingList, SettingItem } from "./setting";
|
||||
@@ -11,15 +12,16 @@ interface Props {
|
||||
}
|
||||
|
||||
const SettingSystem = ({ onError }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const { mutate } = useSWRConfig();
|
||||
const { data: vergeConfig } = useSWR("getVergeConfig", getVergeConfig);
|
||||
|
||||
const {
|
||||
enable_tun_mode = false,
|
||||
enable_auto_launch = false,
|
||||
enable_system_proxy = false,
|
||||
system_proxy_bypass = "",
|
||||
enable_proxy_guard = false,
|
||||
enable_tun_mode,
|
||||
enable_auto_launch,
|
||||
enable_system_proxy,
|
||||
system_proxy_bypass,
|
||||
enable_proxy_guard,
|
||||
} = vergeConfig ?? {};
|
||||
|
||||
const onSwitchFormat = (_e: any, value: boolean) => value;
|
||||
@@ -28,11 +30,11 @@ const SettingSystem = ({ onError }: Props) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<SettingList title="System Setting">
|
||||
<SettingList title={t("System Setting")}>
|
||||
<SettingItem>
|
||||
<ListItemText primary="Tun Mode" />
|
||||
<ListItemText primary={t("Tun Mode")} />
|
||||
<GuardState
|
||||
value={enable_tun_mode}
|
||||
value={enable_tun_mode ?? false}
|
||||
valueProps="checked"
|
||||
onCatch={onError}
|
||||
onFormat={onSwitchFormat}
|
||||
@@ -44,9 +46,9 @@ const SettingSystem = ({ onError }: Props) => {
|
||||
</SettingItem>
|
||||
|
||||
<SettingItem>
|
||||
<ListItemText primary="Auto Launch" />
|
||||
<ListItemText primary={t("Auto Launch")} />
|
||||
<GuardState
|
||||
value={enable_auto_launch}
|
||||
value={enable_auto_launch ?? false}
|
||||
valueProps="checked"
|
||||
onCatch={onError}
|
||||
onFormat={onSwitchFormat}
|
||||
@@ -61,13 +63,13 @@ const SettingSystem = ({ onError }: Props) => {
|
||||
<ListItemText
|
||||
primary={
|
||||
<Box sx={{ display: "flex", alignItems: "center" }}>
|
||||
System Proxy
|
||||
{t("System Proxy")}
|
||||
<SysproxyTooltip />
|
||||
</Box>
|
||||
}
|
||||
/>
|
||||
<GuardState
|
||||
value={enable_system_proxy}
|
||||
value={enable_system_proxy ?? false}
|
||||
valueProps="checked"
|
||||
onCatch={onError}
|
||||
onFormat={onSwitchFormat}
|
||||
@@ -83,9 +85,9 @@ const SettingSystem = ({ onError }: Props) => {
|
||||
|
||||
{enable_system_proxy && (
|
||||
<SettingItem>
|
||||
<ListItemText primary="Proxy Guard" />
|
||||
<ListItemText primary={t("Proxy Guard")} />
|
||||
<GuardState
|
||||
value={enable_proxy_guard}
|
||||
value={enable_proxy_guard ?? false}
|
||||
valueProps="checked"
|
||||
onCatch={onError}
|
||||
onFormat={onSwitchFormat}
|
||||
@@ -99,7 +101,7 @@ const SettingSystem = ({ onError }: Props) => {
|
||||
|
||||
{enable_system_proxy && (
|
||||
<SettingItem>
|
||||
<ListItemText primary="Proxy Bypass" />
|
||||
<ListItemText primary={t("Proxy Bypass")} />
|
||||
<GuardState
|
||||
value={system_proxy_bypass ?? ""}
|
||||
onCatch={onError}
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
import useSWR, { useSWRConfig } from "swr";
|
||||
import { IconButton, ListItemText, Switch, Typography } from "@mui/material";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
IconButton,
|
||||
ListItemText,
|
||||
MenuItem,
|
||||
Select,
|
||||
Switch,
|
||||
Typography,
|
||||
} from "@mui/material";
|
||||
import {
|
||||
getVergeConfig,
|
||||
openAppDir,
|
||||
@@ -18,11 +26,11 @@ interface Props {
|
||||
}
|
||||
|
||||
const SettingVerge = ({ onError }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const { mutate } = useSWRConfig();
|
||||
const { data: vergeConfig } = useSWR("getVergeConfig", getVergeConfig);
|
||||
|
||||
const { theme_mode = "light", theme_blur = false, traffic_graph } =
|
||||
vergeConfig ?? {};
|
||||
const { theme_mode, theme_blur, traffic_graph, language } = vergeConfig ?? {};
|
||||
|
||||
const onSwitchFormat = (_e: any, value: boolean) => value;
|
||||
const onChangeData = (patch: Partial<CmdType.VergeConfig>) => {
|
||||
@@ -30,9 +38,9 @@ const SettingVerge = ({ onError }: Props) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<SettingList title="Verge Setting">
|
||||
<SettingList title={t("Verge Setting")}>
|
||||
<SettingItem>
|
||||
<ListItemText primary="Theme Mode" />
|
||||
<ListItemText primary={t("Theme Mode")} />
|
||||
<GuardState
|
||||
value={theme_mode === "dark"}
|
||||
valueProps="checked"
|
||||
@@ -48,9 +56,9 @@ const SettingVerge = ({ onError }: Props) => {
|
||||
</SettingItem>
|
||||
|
||||
<SettingItem>
|
||||
<ListItemText primary="Theme Blur" />
|
||||
<ListItemText primary={t("Theme Blur")} />
|
||||
<GuardState
|
||||
value={theme_blur}
|
||||
value={theme_blur ?? false}
|
||||
valueProps="checked"
|
||||
onCatch={onError}
|
||||
onFormat={onSwitchFormat}
|
||||
@@ -62,7 +70,7 @@ const SettingVerge = ({ onError }: Props) => {
|
||||
</SettingItem>
|
||||
|
||||
<SettingItem>
|
||||
<ListItemText primary="Traffic Graph" />
|
||||
<ListItemText primary={t("Traffic Graph")} />
|
||||
<GuardState
|
||||
value={traffic_graph ?? true}
|
||||
valueProps="checked"
|
||||
@@ -76,21 +84,37 @@ const SettingVerge = ({ onError }: Props) => {
|
||||
</SettingItem>
|
||||
|
||||
<SettingItem>
|
||||
<ListItemText primary="Open App Dir" />
|
||||
<ListItemText primary={t("Language")} />
|
||||
<GuardState
|
||||
value={language ?? "en"}
|
||||
onCatch={onError}
|
||||
onFormat={(e: any) => e.target.value}
|
||||
onChange={(e) => onChangeData({ language: e })}
|
||||
onGuard={(e) => patchVergeConfig({ language: e })}
|
||||
>
|
||||
<Select size="small" sx={{ width: 100 }}>
|
||||
<MenuItem value="zh">中文</MenuItem>
|
||||
<MenuItem value="en">English</MenuItem>
|
||||
</Select>
|
||||
</GuardState>
|
||||
</SettingItem>
|
||||
|
||||
<SettingItem>
|
||||
<ListItemText primary={t("Open App Dir")} />
|
||||
<IconButton color="inherit" size="small" onClick={openAppDir}>
|
||||
<ArrowForward />
|
||||
</IconButton>
|
||||
</SettingItem>
|
||||
|
||||
<SettingItem>
|
||||
<ListItemText primary="Open Logs Dir" />
|
||||
<ListItemText primary={t("Open Logs Dir")} />
|
||||
<IconButton color="inherit" size="small" onClick={openLogsDir}>
|
||||
<ArrowForward />
|
||||
</IconButton>
|
||||
</SettingItem>
|
||||
|
||||
<SettingItem>
|
||||
<ListItemText primary="Version" />
|
||||
<ListItemText primary={t("Version")} />
|
||||
<Typography sx={{ py: "6px" }}>v{version}</Typography>
|
||||
</SettingItem>
|
||||
</SettingList>
|
||||
|
||||
42
src/locales/en.json
Normal file
42
src/locales/en.json
Normal file
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"Label-Proxies": "Proxies",
|
||||
"Label-Profiles": "Profiles",
|
||||
"Label-Connections": "Connections",
|
||||
"Label-Logs": "Logs",
|
||||
"Label-Settings": "Settings",
|
||||
|
||||
"Connections": "Connections",
|
||||
"Logs": "Logs",
|
||||
"Clear": "Clear",
|
||||
"Proxies": "Proxies",
|
||||
"Proxy Groups": "Proxy Groups",
|
||||
"rule": "rule",
|
||||
"global": "global",
|
||||
"direct": "direct",
|
||||
"Profiles": "Profiles",
|
||||
"Profile URL": "Profile URL",
|
||||
"Import": "Import",
|
||||
"New": "New",
|
||||
|
||||
"Settings": "Settings",
|
||||
"Clash Setting": "Clash Setting",
|
||||
"System Setting": "System Setting",
|
||||
"Verge Setting": "Verge Setting",
|
||||
"Allow Lan": "Allow Lan",
|
||||
"IPv6": "IPv6",
|
||||
"Log Level": "Log Level",
|
||||
"Mixed Port": "Mixed Port",
|
||||
"Clash core": "Clash core",
|
||||
"Tun Mode": "Tun Mode",
|
||||
"Auto Launch": "Auto Launch",
|
||||
"System Proxy": "System Proxy",
|
||||
"Proxy Guard": "Proxy Guard",
|
||||
"Proxy Bypass": "Proxy Bypass",
|
||||
"Theme Mode": "Theme Mode",
|
||||
"Theme Blur": "Theme Blur",
|
||||
"Traffic Graph": "Traffic Graph",
|
||||
"Language": "Language",
|
||||
"Open App Dir": "Open App Dir",
|
||||
"Open Logs Dir": "Open Logs Dir",
|
||||
"Version": "Version"
|
||||
}
|
||||
42
src/locales/zh.json
Normal file
42
src/locales/zh.json
Normal file
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"Label-Proxies": "代 理",
|
||||
"Label-Profiles": "配 置",
|
||||
"Label-Connections": "连 接",
|
||||
"Label-Logs": "日 志",
|
||||
"Label-Settings": "设 置",
|
||||
|
||||
"Connections": "连接",
|
||||
"Logs": "日志",
|
||||
"Clear": "清除",
|
||||
"Proxies": "代理",
|
||||
"Proxy Groups": "代理组",
|
||||
"rule": "规则",
|
||||
"global": "全局",
|
||||
"direct": "直连",
|
||||
"Profiles": "配置",
|
||||
"Profile URL": "配置文件链接",
|
||||
"Import": "导入",
|
||||
"New": "新建",
|
||||
|
||||
"Settings": "设置",
|
||||
"Clash Setting": "Clash 设置",
|
||||
"System Setting": "系统设置",
|
||||
"Verge Setting": "Verge 设置",
|
||||
"Allow Lan": "局域网连接",
|
||||
"IPv6": "IPv6",
|
||||
"Log Level": "日志等级",
|
||||
"Mixed Port": "端口设置",
|
||||
"Clash core": "Clash 内核",
|
||||
"Tun Mode": "Tun 模式",
|
||||
"Auto Launch": "开机自启",
|
||||
"System Proxy": "系统代理",
|
||||
"Proxy Guard": "系统代理守卫",
|
||||
"Proxy Bypass": "Proxy Bypass",
|
||||
"Theme Mode": "暗夜模式",
|
||||
"Theme Blur": "背景模糊",
|
||||
"Traffic Graph": "流量图显",
|
||||
"Language": "语言设置",
|
||||
"Open App Dir": "应用目录",
|
||||
"Open Logs Dir": "日志目录",
|
||||
"Version": "版本"
|
||||
}
|
||||
@@ -7,6 +7,7 @@ import { RecoilRoot } from "recoil";
|
||||
import { BrowserRouter } from "react-router-dom";
|
||||
import Layout from "./pages/_layout";
|
||||
import enhance from "./services/enhance";
|
||||
import "./services/i18n";
|
||||
|
||||
enhance.setup();
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import i18next from "i18next";
|
||||
import useSWR, { SWRConfig, useSWRConfig } from "swr";
|
||||
import { useEffect, useMemo } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Route, Routes } from "react-router-dom";
|
||||
import { alpha, createTheme, List, Paper, ThemeProvider } from "@mui/material";
|
||||
import { listen } from "@tauri-apps/api/event";
|
||||
@@ -16,6 +18,7 @@ import UpdateButton from "../components/layout/update-button";
|
||||
const isMacos = navigator.userAgent.includes("Mac OS X");
|
||||
|
||||
const Layout = () => {
|
||||
const { t } = useTranslation();
|
||||
const { mutate } = useSWRConfig();
|
||||
const { data } = useSWR("getVergeConfig", getVergeConfig);
|
||||
|
||||
@@ -27,16 +30,20 @@ const Layout = () => {
|
||||
if (e.key === "Escape") appWindow.hide();
|
||||
});
|
||||
|
||||
listen("restart_clash", async () => {
|
||||
listen("verge://refresh-clash-config", async () => {
|
||||
// the clash info may be updated
|
||||
await getAxios(true);
|
||||
// make sure that the clash is ok
|
||||
setTimeout(() => mutate("getProxies"), 1000);
|
||||
setTimeout(() => mutate("getProxies"), 2000);
|
||||
mutate("getProxies");
|
||||
mutate("getClashConfig");
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (data?.language) {
|
||||
i18next.changeLanguage(data.language);
|
||||
}
|
||||
}, [data?.language]);
|
||||
|
||||
const theme = useMemo(() => {
|
||||
// const background = mode === "light" ? "#f5f5f5" : "#000";
|
||||
const selectColor = mode === "light" ? "#f5f5f5" : "#d5d5d5";
|
||||
@@ -87,7 +94,7 @@ const Layout = () => {
|
||||
<List className="the-menu" data-windrag>
|
||||
{routers.map((router) => (
|
||||
<LayoutItem key={router.label} to={router.link}>
|
||||
{router.label}
|
||||
{t(router.label)}
|
||||
</LayoutItem>
|
||||
))}
|
||||
</List>
|
||||
|
||||
@@ -6,27 +6,27 @@ import ConnectionsPage from "./connections";
|
||||
|
||||
export const routers = [
|
||||
{
|
||||
label: "Proxies",
|
||||
label: "Label-Proxies",
|
||||
link: "/",
|
||||
ele: ProxiesPage,
|
||||
},
|
||||
{
|
||||
label: "Profiles",
|
||||
label: "Label-Profiles",
|
||||
link: "/profile",
|
||||
ele: ProfilesPage,
|
||||
},
|
||||
{
|
||||
label: "Connections",
|
||||
label: "Label-Connections",
|
||||
link: "/connections",
|
||||
ele: ConnectionsPage,
|
||||
},
|
||||
{
|
||||
label: "Logs",
|
||||
label: "Label-Logs",
|
||||
link: "/logs",
|
||||
ele: LogsPage,
|
||||
},
|
||||
{
|
||||
label: "Settings",
|
||||
label: "Label-Settings",
|
||||
link: "/settings",
|
||||
ele: SettingsPage,
|
||||
},
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { useEffect, useState } from "react";
|
||||
import { Paper } from "@mui/material";
|
||||
import { Virtuoso } from "react-virtuoso";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { ApiType } from "../services/types";
|
||||
import { getInfomation } from "../services/api";
|
||||
import BasePage from "../components/base/base-page";
|
||||
@@ -8,6 +9,8 @@ import ConnectionItem from "../components/connection/connection-item";
|
||||
|
||||
const ConnectionsPage = () => {
|
||||
const initConn = { uploadTotal: 0, downloadTotal: 0, connections: [] };
|
||||
|
||||
const { t } = useTranslation();
|
||||
const [conn, setConn] = useState<ApiType.Connections>(initConn);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -27,7 +30,7 @@ const ConnectionsPage = () => {
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<BasePage title="Connections" contentStyle={{ height: "100%" }}>
|
||||
<BasePage title={t("Connections")} contentStyle={{ height: "100%" }}>
|
||||
<Paper sx={{ boxShadow: 2, height: "100%" }}>
|
||||
<Virtuoso
|
||||
data={conn.connections}
|
||||
|
||||
@@ -1,16 +1,18 @@
|
||||
import { useRecoilState } from "recoil";
|
||||
import { Button, Paper } from "@mui/material";
|
||||
import { Virtuoso } from "react-virtuoso";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { atomLogData } from "../services/states";
|
||||
import BasePage from "../components/base/base-page";
|
||||
import LogItem from "../components/log/log-item";
|
||||
|
||||
const LogPage = () => {
|
||||
const { t } = useTranslation();
|
||||
const [logData, setLogData] = useRecoilState(atomLogData);
|
||||
|
||||
return (
|
||||
<BasePage
|
||||
title="Logs"
|
||||
title={t("Logs")}
|
||||
contentStyle={{ height: "100%" }}
|
||||
header={
|
||||
<Button
|
||||
@@ -19,7 +21,7 @@ const LogPage = () => {
|
||||
variant="contained"
|
||||
onClick={() => setLogData([])}
|
||||
>
|
||||
Clear
|
||||
{t("Clear")}
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
|
||||
@@ -2,6 +2,7 @@ import useSWR, { useSWRConfig } from "swr";
|
||||
import { useLockFn } from "ahooks";
|
||||
import { useEffect, useMemo, useState } from "react";
|
||||
import { Box, Button, Grid, TextField } from "@mui/material";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import {
|
||||
getProfiles,
|
||||
patchProfile,
|
||||
@@ -19,6 +20,7 @@ import ProfileItem from "../components/profile/profile-item";
|
||||
import ProfileMore from "../components/profile/profile-more";
|
||||
|
||||
const ProfilePage = () => {
|
||||
const { t } = useTranslation();
|
||||
const { mutate } = useSWRConfig();
|
||||
|
||||
const [url, setUrl] = useState("");
|
||||
@@ -175,12 +177,12 @@ const ProfilePage = () => {
|
||||
});
|
||||
|
||||
return (
|
||||
<BasePage title="Profiles">
|
||||
<BasePage title={t("Profiles")}>
|
||||
<Box sx={{ display: "flex", mb: 2.5 }}>
|
||||
<TextField
|
||||
id="clas_verge_profile_url"
|
||||
name="profile_url"
|
||||
label="Profile URL"
|
||||
label={t("Profile URL")}
|
||||
size="small"
|
||||
fullWidth
|
||||
value={url}
|
||||
@@ -193,10 +195,10 @@ const ProfilePage = () => {
|
||||
onClick={onImport}
|
||||
sx={{ mr: 1 }}
|
||||
>
|
||||
Import
|
||||
{t("Import")}
|
||||
</Button>
|
||||
<Button variant="contained" onClick={() => setDialogOpen(true)}>
|
||||
New
|
||||
{t("New")}
|
||||
</Button>
|
||||
</Box>
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import useSWR, { useSWRConfig } from "swr";
|
||||
import { useEffect } from "react";
|
||||
import { useLockFn } from "ahooks";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { Button, ButtonGroup, List, Paper } from "@mui/material";
|
||||
import { getClashConfig, updateConfigs } from "../services/api";
|
||||
import { patchClashConfig } from "../services/cmds";
|
||||
@@ -10,6 +11,7 @@ import ProxyGroup from "../components/proxy/proxy-group";
|
||||
import ProxyGlobal from "../components/proxy/proxy-global";
|
||||
|
||||
const ProxyPage = () => {
|
||||
const { t } = useTranslation();
|
||||
const { mutate } = useSWRConfig();
|
||||
const { data: proxiesData } = useSWR("getProxies", getProxies);
|
||||
const { data: clashConfig } = useSWR("getClashConfig", getClashConfig);
|
||||
@@ -45,7 +47,7 @@ const ProxyPage = () => {
|
||||
return (
|
||||
<BasePage
|
||||
contentStyle={pageStyle}
|
||||
title={showGroup ? "Proxy Groups" : "Proxies"}
|
||||
title={showGroup ? t("Proxy Groups") : t("Proxies")}
|
||||
header={
|
||||
<ButtonGroup size="small">
|
||||
{modeList.map((mode) => (
|
||||
@@ -55,7 +57,7 @@ const ProxyPage = () => {
|
||||
onClick={() => onChangeMode(mode)}
|
||||
sx={{ textTransform: "capitalize" }}
|
||||
>
|
||||
{mode}
|
||||
{t(mode)}
|
||||
</Button>
|
||||
))}
|
||||
</ButtonGroup>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Paper } from "@mui/material";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import Notice from "../components/base/base-notice";
|
||||
import BasePage from "../components/base/base-page";
|
||||
import SettingVerge from "../components/setting/setting-verge";
|
||||
@@ -6,12 +7,14 @@ import SettingClash from "../components/setting/setting-clash";
|
||||
import SettingSystem from "../components/setting/setting-system";
|
||||
|
||||
const SettingPage = () => {
|
||||
const onError = (error: any) => {
|
||||
error && Notice.error(error.toString());
|
||||
const { t } = useTranslation();
|
||||
|
||||
const onError = (err: any) => {
|
||||
Notice.error(err?.message || err.toString());
|
||||
};
|
||||
|
||||
return (
|
||||
<BasePage title="Settings">
|
||||
<BasePage title={t("Settings")}>
|
||||
<Paper sx={{ borderRadius: 1, boxShadow: 2, mb: 3 }}>
|
||||
<SettingClash onError={onError} />
|
||||
</Paper>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { invoke } from "@tauri-apps/api/tauri";
|
||||
import { ApiType, CmdType } from "./types";
|
||||
import Notice from "../components/base/base-notice";
|
||||
|
||||
export async function getProfiles() {
|
||||
return invoke<CmdType.ProfilesConfig>("get_profiles");
|
||||
@@ -83,9 +84,13 @@ export async function killSidecars() {
|
||||
}
|
||||
|
||||
export async function openAppDir() {
|
||||
return invoke<void>("open_app_dir");
|
||||
return invoke<void>("open_app_dir").catch((err) =>
|
||||
Notice.error(err?.message || err.toString(), 1500)
|
||||
);
|
||||
}
|
||||
|
||||
export async function openLogsDir() {
|
||||
return invoke<void>("open_logs_dir");
|
||||
return invoke<void>("open_logs_dir").catch((err) =>
|
||||
Notice.error(err?.message || err.toString(), 1500)
|
||||
);
|
||||
}
|
||||
|
||||
17
src/services/i18n.ts
Normal file
17
src/services/i18n.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import i18n from "i18next";
|
||||
import { initReactI18next } from "react-i18next";
|
||||
import en from "../locales/en.json";
|
||||
import zh from "../locales/zh.json";
|
||||
|
||||
const resources = {
|
||||
en: { translation: en },
|
||||
zh: { translation: zh },
|
||||
};
|
||||
|
||||
i18n.use(initReactI18next).init({
|
||||
resources,
|
||||
lng: "en",
|
||||
interpolation: {
|
||||
escapeValue: false,
|
||||
},
|
||||
});
|
||||
@@ -120,6 +120,7 @@ export namespace CmdType {
|
||||
}
|
||||
|
||||
export interface VergeConfig {
|
||||
language?: string;
|
||||
theme_mode?: "light" | "dark";
|
||||
theme_blur?: boolean;
|
||||
traffic_graph?: boolean;
|
||||
|
||||
35
yarn.lock
35
yarn.lock
@@ -227,7 +227,7 @@
|
||||
"@babel/plugin-syntax-jsx" "^7.16.7"
|
||||
"@babel/types" "^7.17.0"
|
||||
|
||||
"@babel/runtime@^7.13.10", "@babel/runtime@^7.17.2", "@babel/runtime@^7.5.5", "@babel/runtime@^7.7.2", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.7":
|
||||
"@babel/runtime@^7.13.10", "@babel/runtime@^7.14.5", "@babel/runtime@^7.17.2", "@babel/runtime@^7.5.5", "@babel/runtime@^7.7.2", "@babel/runtime@^7.7.6", "@babel/runtime@^7.8.7":
|
||||
version "7.17.2"
|
||||
resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.17.2.tgz#66f68591605e59da47523c631416b18508779941"
|
||||
integrity sha512-hzeyJyMA1YGdJTuWU0e/j4wKXrU4OMFvY2MSlaI9B7VQb0r5cxTE3EAIS2Q7Tn2RIcDkRvTA/v2JsAEhxe99uw==
|
||||
@@ -1291,6 +1291,18 @@ hoist-non-react-statics@^3.3.1, hoist-non-react-statics@^3.3.2:
|
||||
dependencies:
|
||||
react-is "^16.7.0"
|
||||
|
||||
html-escaper@^2.0.2:
|
||||
version "2.0.2"
|
||||
resolved "https://registry.yarnpkg.com/html-escaper/-/html-escaper-2.0.2.tgz#dfd60027da36a36dfcbe236262c00a5822681453"
|
||||
integrity sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==
|
||||
|
||||
html-parse-stringify@^3.0.1:
|
||||
version "3.0.1"
|
||||
resolved "https://registry.yarnpkg.com/html-parse-stringify/-/html-parse-stringify-3.0.1.tgz#dfc1017347ce9f77c8141a507f233040c59c55d2"
|
||||
integrity sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==
|
||||
dependencies:
|
||||
void-elements "3.1.0"
|
||||
|
||||
human-signals@^1.1.1:
|
||||
version "1.1.1"
|
||||
resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-1.1.1.tgz#c5b1cd14f50aeae09ab6c59fe63ba3395fe4dfa3"
|
||||
@@ -1301,6 +1313,13 @@ husky@^7.0.0:
|
||||
resolved "https://registry.yarnpkg.com/husky/-/husky-7.0.4.tgz#242048245dc49c8fb1bf0cc7cfb98dd722531535"
|
||||
integrity sha512-vbaCKN2QLtP/vD4yvs6iz6hBEo6wkSzs8HpRah1Z6aGmF2KW5PdYuAd7uX5a+OyBZHBhd+TFLqgjUgytQr4RvQ==
|
||||
|
||||
i18next@^21.6.14:
|
||||
version "21.6.14"
|
||||
resolved "https://registry.yarnpkg.com/i18next/-/i18next-21.6.14.tgz#2bc199fba7f4da44b5952d7df0a3814a6e5c3943"
|
||||
integrity sha512-XL6WyD+xlwQwbieXRlXhKWoLb/rkch50/rA+vl6untHnJ+aYnkQ0YDZciTWE78PPhOpbi2gR0LTJCJpiAhA+uQ==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.17.2"
|
||||
|
||||
ignore@^5.1.4:
|
||||
version "5.2.0"
|
||||
resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.2.0.tgz#6d3bac8fa7fe0d45d9f9be7bac2fc279577e345a"
|
||||
@@ -1653,6 +1672,15 @@ react-dom@^17.0.2:
|
||||
object-assign "^4.1.1"
|
||||
scheduler "^0.20.2"
|
||||
|
||||
react-i18next@^11.15.6:
|
||||
version "11.15.6"
|
||||
resolved "https://registry.yarnpkg.com/react-i18next/-/react-i18next-11.15.6.tgz#693430fbee5ac7d0774bd88683575d62adb24afb"
|
||||
integrity sha512-OUWcFdNgIA9swVx3JGIreuquglAinpRwB/HYrCprTN+s9BQDt9LSiY7x5DGc2JzVpwqtpoTV7oRUTOxEPNyUPw==
|
||||
dependencies:
|
||||
"@babel/runtime" "^7.14.5"
|
||||
html-escaper "^2.0.2"
|
||||
html-parse-stringify "^3.0.1"
|
||||
|
||||
react-is@^16.13.1, react-is@^16.7.0:
|
||||
version "16.13.1"
|
||||
resolved "https://registry.yarnpkg.com/react-is/-/react-is-16.13.1.tgz#789729a4dc36de2999dc156dd6c1d9c18cea56a4"
|
||||
@@ -1901,6 +1929,11 @@ vite@^2.8.6:
|
||||
optionalDependencies:
|
||||
fsevents "~2.3.2"
|
||||
|
||||
void-elements@3.1.0:
|
||||
version "3.1.0"
|
||||
resolved "https://registry.yarnpkg.com/void-elements/-/void-elements-3.1.0.tgz#614f7fbf8d801f0bb5f0661f5b2f5785750e4f09"
|
||||
integrity sha1-YU9/v42AHwu18GYfWy9XhXUOTwk=
|
||||
|
||||
web-streams-polyfill@^3.0.3:
|
||||
version "3.2.0"
|
||||
resolved "https://registry.yarnpkg.com/web-streams-polyfill/-/web-streams-polyfill-3.2.0.tgz#a6b74026b38e4885869fb5c589e90b95ccfc7965"
|
||||
|
||||
Reference in New Issue
Block a user