Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
451afdb660 | ||
|
|
d298bda92c | ||
|
|
fd99ba6255 | ||
|
|
6ade0b2b1a | ||
|
|
9902003da9 | ||
|
|
0ff2fcac11 |
12
README.md
12
README.md
@@ -13,6 +13,7 @@ A <a href="https://github.com/Dreamacro/clash">Clash</a> GUI based on <a href="h
|
|||||||
|
|
||||||
- Full `clash` config supported, Partial `clash premium` config supported.
|
- Full `clash` config supported, Partial `clash premium` config supported.
|
||||||
- Profiles management and enhancement (by yaml and Javascript). [Doc](https://github.com/zzzgydi/clash-verge/issues/12)
|
- Profiles management and enhancement (by yaml and Javascript). [Doc](https://github.com/zzzgydi/clash-verge/issues/12)
|
||||||
|
- Simple UI and supports custom theme color.
|
||||||
- System proxy setting and guard.
|
- System proxy setting and guard.
|
||||||
|
|
||||||
## Install
|
## Install
|
||||||
@@ -64,6 +65,17 @@ yarn build
|
|||||||
<img src="./docs/demo6.png" alt="demo6" width="32%" />
|
<img src="./docs/demo6.png" alt="demo6" width="32%" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
### Custom Theme
|
||||||
|
|
||||||
|
<div align="center">
|
||||||
|
<img src="./docs/color1.png" alt="demo1" width="16%" />
|
||||||
|
<img src="./docs/color2.png" alt="demo2" width="16%" />
|
||||||
|
<img src="./docs/color3.png" alt="demo3" width="16%" />
|
||||||
|
<img src="./docs/color4.png" alt="demo4" width="16%" />
|
||||||
|
<img src="./docs/color5.png" alt="demo5" width="16%" />
|
||||||
|
<img src="./docs/color6.png" alt="demo6" width="16%" />
|
||||||
|
</div>
|
||||||
|
|
||||||
## Disclaimer
|
## Disclaimer
|
||||||
|
|
||||||
This is a learning project for Rust practice.
|
This is a learning project for Rust practice.
|
||||||
|
|||||||
13
UPDATELOG.md
13
UPDATELOG.md
@@ -1,3 +1,16 @@
|
|||||||
|
## v0.0.28
|
||||||
|
|
||||||
|
### Features
|
||||||
|
|
||||||
|
- enable to use clash config fields (UI)
|
||||||
|
|
||||||
|
### Bug Fixes
|
||||||
|
|
||||||
|
- remove the character
|
||||||
|
- fix some icon color
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## v0.0.27
|
## v0.0.27
|
||||||
|
|
||||||
### Features
|
### Features
|
||||||
|
|||||||
BIN
docs/color1.png
Normal file
BIN
docs/color1.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 29 KiB |
BIN
docs/color2.png
Normal file
BIN
docs/color2.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 30 KiB |
BIN
docs/color3.png
Normal file
BIN
docs/color3.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 29 KiB |
BIN
docs/color4.png
Normal file
BIN
docs/color4.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 29 KiB |
BIN
docs/color5.png
Normal file
BIN
docs/color5.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 32 KiB |
BIN
docs/color6.png
Normal file
BIN
docs/color6.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 31 KiB |
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "clash-verge",
|
"name": "clash-verge",
|
||||||
"version": "0.0.27",
|
"version": "0.0.28",
|
||||||
"license": "GPL-3.0",
|
"license": "GPL-3.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "tauri dev",
|
"dev": "tauri dev",
|
||||||
|
|||||||
@@ -125,6 +125,23 @@ pub fn change_profile_chain(
|
|||||||
wrap_err!(clash.activate_enhanced(&profiles, false, false))
|
wrap_err!(clash.activate_enhanced(&profiles, false, false))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// change the profile valid fields
|
||||||
|
#[tauri::command]
|
||||||
|
pub fn change_profile_valid(
|
||||||
|
valid: Option<Vec<String>>,
|
||||||
|
app_handle: tauri::AppHandle,
|
||||||
|
clash_state: State<'_, ClashState>,
|
||||||
|
profiles_state: State<'_, ProfilesState>,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
let mut clash = clash_state.0.lock().unwrap();
|
||||||
|
let mut profiles = profiles_state.0.lock().unwrap();
|
||||||
|
|
||||||
|
profiles.put_valid(valid);
|
||||||
|
clash.set_window(app_handle.get_window("main"));
|
||||||
|
|
||||||
|
wrap_err!(clash.activate_enhanced(&profiles, false, false))
|
||||||
|
}
|
||||||
|
|
||||||
/// manually exec enhanced profile
|
/// manually exec enhanced profile
|
||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
pub fn enhance_profiles(
|
pub fn enhance_profiles(
|
||||||
|
|||||||
@@ -316,6 +316,9 @@ pub struct Profiles {
|
|||||||
/// same as PrfConfig.chain
|
/// same as PrfConfig.chain
|
||||||
chain: Option<Vec<String>>,
|
chain: Option<Vec<String>>,
|
||||||
|
|
||||||
|
/// record valid fields for clash
|
||||||
|
valid: Option<Vec<String>>,
|
||||||
|
|
||||||
/// profile list
|
/// profile list
|
||||||
items: Option<Vec<PrfItem>>,
|
items: Option<Vec<PrfItem>>,
|
||||||
}
|
}
|
||||||
@@ -399,6 +402,11 @@ impl Profiles {
|
|||||||
self.chain = chain;
|
self.chain = chain;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// just change the `field`
|
||||||
|
pub fn put_valid(&mut self, valid: Option<Vec<String>>) {
|
||||||
|
self.valid = valid;
|
||||||
|
}
|
||||||
|
|
||||||
/// find the item by the uid
|
/// find the item by the uid
|
||||||
pub fn get_item(&self, uid: &String) -> Result<&PrfItem> {
|
pub fn get_item(&self, uid: &String) -> Result<&PrfItem> {
|
||||||
if self.items.is_some() {
|
if self.items.is_some() {
|
||||||
@@ -599,9 +607,12 @@ impl Profiles {
|
|||||||
None => vec![],
|
None => vec![],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let valid = self.valid.clone().unwrap_or(vec![]);
|
||||||
|
|
||||||
Ok(PrfEnhanced {
|
Ok(PrfEnhanced {
|
||||||
current,
|
current,
|
||||||
chain,
|
chain,
|
||||||
|
valid,
|
||||||
callback,
|
callback,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -613,6 +624,8 @@ pub struct PrfEnhanced {
|
|||||||
|
|
||||||
pub chain: Vec<PrfData>,
|
pub chain: Vec<PrfData>,
|
||||||
|
|
||||||
|
pub valid: Vec<String>,
|
||||||
|
|
||||||
pub callback: String,
|
pub callback: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -119,6 +119,7 @@ fn main() -> std::io::Result<()> {
|
|||||||
cmds::sync_profiles,
|
cmds::sync_profiles,
|
||||||
cmds::enhance_profiles,
|
cmds::enhance_profiles,
|
||||||
cmds::change_profile_chain,
|
cmds::change_profile_chain,
|
||||||
|
cmds::change_profile_valid,
|
||||||
cmds::read_profile_file,
|
cmds::read_profile_file,
|
||||||
cmds::save_profile_file
|
cmds::save_profile_file
|
||||||
]);
|
]);
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"package": {
|
"package": {
|
||||||
"productName": "Clash Verge",
|
"productName": "Clash Verge",
|
||||||
"version": "0.0.27"
|
"version": "0.0.28"
|
||||||
},
|
},
|
||||||
"build": {
|
"build": {
|
||||||
"distDir": "../dist",
|
"distDir": "../dist",
|
||||||
|
|||||||
@@ -1,26 +1,49 @@
|
|||||||
import useSWR from "swr";
|
import useSWR from "swr";
|
||||||
|
import { useState } from "react";
|
||||||
import { useLockFn } from "ahooks";
|
import { useLockFn } from "ahooks";
|
||||||
import { Box, Grid, IconButton, Stack } from "@mui/material";
|
import {
|
||||||
import { RestartAltRounded } from "@mui/icons-material";
|
Box,
|
||||||
|
Divider,
|
||||||
|
Grid,
|
||||||
|
IconButton,
|
||||||
|
ListItemIcon,
|
||||||
|
ListItemText,
|
||||||
|
Menu,
|
||||||
|
MenuItem,
|
||||||
|
Stack,
|
||||||
|
} from "@mui/material";
|
||||||
|
import {
|
||||||
|
AddchartRounded,
|
||||||
|
CheckRounded,
|
||||||
|
MenuRounded,
|
||||||
|
RestartAltRounded,
|
||||||
|
} from "@mui/icons-material";
|
||||||
import {
|
import {
|
||||||
getProfiles,
|
getProfiles,
|
||||||
deleteProfile,
|
deleteProfile,
|
||||||
enhanceProfiles,
|
enhanceProfiles,
|
||||||
changeProfileChain,
|
changeProfileChain,
|
||||||
|
changeProfileValid,
|
||||||
} from "../../services/cmds";
|
} from "../../services/cmds";
|
||||||
import { CmdType } from "../../services/types";
|
import { CmdType } from "../../services/types";
|
||||||
import Notice from "../base/base-notice";
|
import getSystem from "../../utils/get-system";
|
||||||
import ProfileMore from "./profile-more";
|
import ProfileMore from "./profile-more";
|
||||||
|
import Notice from "../base/base-notice";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
items: CmdType.ProfileItem[];
|
items: CmdType.ProfileItem[];
|
||||||
chain: string[];
|
chain: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const OS = getSystem();
|
||||||
|
|
||||||
const EnhancedMode = (props: Props) => {
|
const EnhancedMode = (props: Props) => {
|
||||||
const { items, chain } = props;
|
const { items, chain } = props;
|
||||||
|
|
||||||
const { mutate } = useSWR("getProfiles", getProfiles);
|
const { data, mutate } = useSWR("getProfiles", getProfiles);
|
||||||
|
const valid = data?.valid || [];
|
||||||
|
|
||||||
|
const [anchorEl, setAnchorEl] = useState<any>(null);
|
||||||
|
|
||||||
// handler
|
// handler
|
||||||
const onEnhance = useLockFn(async () => {
|
const onEnhance = useLockFn(async () => {
|
||||||
@@ -74,6 +97,19 @@ const EnhancedMode = (props: Props) => {
|
|||||||
mutate((conf = {}) => ({ ...conf, chain: newChain }), true);
|
mutate((conf = {}) => ({ ...conf, chain: newChain }), true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// update valid list
|
||||||
|
const onToggleValid = useLockFn(async (key: string) => {
|
||||||
|
try {
|
||||||
|
const newValid = valid.includes(key)
|
||||||
|
? valid.filter((i) => i !== key)
|
||||||
|
: valid.concat(key);
|
||||||
|
await changeProfileValid(newValid);
|
||||||
|
mutate();
|
||||||
|
} catch (err: any) {
|
||||||
|
Notice.error(err.message || err.toString());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box sx={{ mt: 4 }}>
|
<Box sx={{ mt: 4 }}>
|
||||||
<Stack
|
<Stack
|
||||||
@@ -92,9 +128,74 @@ const EnhancedMode = (props: Props) => {
|
|||||||
<RestartAltRounded />
|
<RestartAltRounded />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
|
|
||||||
{/* <IconButton size="small" color="inherit">
|
<IconButton
|
||||||
|
size="small"
|
||||||
|
color="inherit"
|
||||||
|
id="profile-use-button"
|
||||||
|
title="enable clash fields"
|
||||||
|
aria-controls={!!anchorEl ? "profile-use-menu" : undefined}
|
||||||
|
aria-haspopup="true"
|
||||||
|
aria-expanded={!!anchorEl ? "true" : undefined}
|
||||||
|
onClick={(e) => setAnchorEl(e.currentTarget)}
|
||||||
|
>
|
||||||
<MenuRounded />
|
<MenuRounded />
|
||||||
</IconButton> */}
|
</IconButton>
|
||||||
|
|
||||||
|
<Menu
|
||||||
|
id="profile-use-menu"
|
||||||
|
open={!!anchorEl}
|
||||||
|
anchorEl={anchorEl}
|
||||||
|
onClose={() => setAnchorEl(null)}
|
||||||
|
transitionDuration={225}
|
||||||
|
TransitionProps={
|
||||||
|
OS === "macos" ? { style: { transitionDuration: "225ms" } } : {}
|
||||||
|
}
|
||||||
|
MenuListProps={{
|
||||||
|
dense: true,
|
||||||
|
"aria-labelledby": "profile-use-button",
|
||||||
|
}}
|
||||||
|
onContextMenu={(e) => {
|
||||||
|
setAnchorEl(null);
|
||||||
|
e.preventDefault();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<MenuItem>
|
||||||
|
<ListItemIcon color="inherit">
|
||||||
|
<AddchartRounded />
|
||||||
|
</ListItemIcon>
|
||||||
|
Use Clash Fields
|
||||||
|
</MenuItem>
|
||||||
|
|
||||||
|
<Divider />
|
||||||
|
|
||||||
|
{[
|
||||||
|
"tun",
|
||||||
|
"dns",
|
||||||
|
"hosts",
|
||||||
|
"script",
|
||||||
|
"profile",
|
||||||
|
"payload",
|
||||||
|
"interface-name",
|
||||||
|
"routing-mark",
|
||||||
|
].map((key) => {
|
||||||
|
const has = valid.includes(key);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MenuItem
|
||||||
|
key={key}
|
||||||
|
sx={{ width: 180 }}
|
||||||
|
onClick={() => onToggleValid(key)}
|
||||||
|
>
|
||||||
|
{has && (
|
||||||
|
<ListItemIcon color="inherit">
|
||||||
|
<CheckRounded />
|
||||||
|
</ListItemIcon>
|
||||||
|
)}
|
||||||
|
<ListItemText inset={!has}>{key}</ListItemText>
|
||||||
|
</MenuItem>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</Menu>
|
||||||
</Stack>
|
</Stack>
|
||||||
|
|
||||||
<Grid container spacing={2}>
|
<Grid container spacing={2}>
|
||||||
|
|||||||
@@ -124,18 +124,25 @@ const ProxyGlobal = (props: Props) => {
|
|||||||
<IconButton
|
<IconButton
|
||||||
size="small"
|
size="small"
|
||||||
title="location"
|
title="location"
|
||||||
|
color="inherit"
|
||||||
onClick={() => onLocation(true)}
|
onClick={() => onLocation(true)}
|
||||||
>
|
>
|
||||||
<MyLocationRounded />
|
<MyLocationRounded />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
|
|
||||||
<IconButton size="small" title="delay check" onClick={onCheckAll}>
|
<IconButton
|
||||||
|
size="small"
|
||||||
|
title="delay check"
|
||||||
|
color="inherit"
|
||||||
|
onClick={onCheckAll}
|
||||||
|
>
|
||||||
<NetworkCheckRounded />
|
<NetworkCheckRounded />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
|
|
||||||
<IconButton
|
<IconButton
|
||||||
size="small"
|
size="small"
|
||||||
title="proxy detail"
|
title="proxy detail"
|
||||||
|
color="inherit"
|
||||||
onClick={() => setShowType(!showType)}
|
onClick={() => setShowType(!showType)}
|
||||||
>
|
>
|
||||||
{showType ? <VisibilityRounded /> : <VisibilityOffRounded />}
|
{showType ? <VisibilityRounded /> : <VisibilityOffRounded />}
|
||||||
@@ -144,6 +151,7 @@ const ProxyGlobal = (props: Props) => {
|
|||||||
<IconButton
|
<IconButton
|
||||||
size="small"
|
size="small"
|
||||||
title="filter"
|
title="filter"
|
||||||
|
color="inherit"
|
||||||
onClick={() => setShowFilter(!showFilter)}
|
onClick={() => setShowFilter(!showFilter)}
|
||||||
>
|
>
|
||||||
{showFilter ? <FilterAltRounded /> : <FilterAltOffRounded />}
|
{showFilter ? <FilterAltRounded /> : <FilterAltOffRounded />}
|
||||||
|
|||||||
@@ -149,18 +149,25 @@ const ProxyGroup = ({ group }: Props) => {
|
|||||||
<IconButton
|
<IconButton
|
||||||
size="small"
|
size="small"
|
||||||
title="location"
|
title="location"
|
||||||
|
color="inherit"
|
||||||
onClick={() => onLocation(true)}
|
onClick={() => onLocation(true)}
|
||||||
>
|
>
|
||||||
<MyLocationRounded />
|
<MyLocationRounded />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
|
|
||||||
<IconButton size="small" title="delay check" onClick={onCheckAll}>
|
<IconButton
|
||||||
|
size="small"
|
||||||
|
title="delay check"
|
||||||
|
color="inherit"
|
||||||
|
onClick={onCheckAll}
|
||||||
|
>
|
||||||
<NetworkCheckRounded />
|
<NetworkCheckRounded />
|
||||||
</IconButton>
|
</IconButton>
|
||||||
|
|
||||||
<IconButton
|
<IconButton
|
||||||
size="small"
|
size="small"
|
||||||
title="proxy detail"
|
title="proxy detail"
|
||||||
|
color="inherit"
|
||||||
onClick={() => setShowType(!showType)}
|
onClick={() => setShowType(!showType)}
|
||||||
>
|
>
|
||||||
{showType ? <VisibilityRounded /> : <VisibilityOffRounded />}
|
{showType ? <VisibilityRounded /> : <VisibilityOffRounded />}
|
||||||
@@ -169,6 +176,7 @@ const ProxyGroup = ({ group }: Props) => {
|
|||||||
<IconButton
|
<IconButton
|
||||||
size="small"
|
size="small"
|
||||||
title="filter"
|
title="filter"
|
||||||
|
color="inherit"
|
||||||
onClick={() => setShowFilter(!showFilter)}
|
onClick={() => setShowFilter(!showFilter)}
|
||||||
>
|
>
|
||||||
{showFilter ? <FilterAltRounded /> : <FilterAltOffRounded />}
|
{showFilter ? <FilterAltRounded /> : <FilterAltOffRounded />}
|
||||||
|
|||||||
@@ -165,7 +165,7 @@ const ProfilePage = () => {
|
|||||||
))}
|
))}
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
{enhanceItems.length && (
|
{enhanceItems.length > 0 && (
|
||||||
<EnhancedMode items={enhanceItems} chain={profiles.chain || []} />
|
<EnhancedMode items={enhanceItems} chain={profiles.chain || []} />
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
@@ -66,6 +66,10 @@ export async function changeProfileChain(chain?: string[]) {
|
|||||||
return invoke<void>("change_profile_chain", { chain });
|
return invoke<void>("change_profile_chain", { chain });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function changeProfileValid(valid?: string[]) {
|
||||||
|
return invoke<void>("change_profile_valid", { valid });
|
||||||
|
}
|
||||||
|
|
||||||
export async function getClashInfo() {
|
export async function getClashInfo() {
|
||||||
return invoke<CmdType.ClashInfo | null>("get_clash_info");
|
return invoke<CmdType.ClashInfo | null>("get_clash_info");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -145,11 +145,12 @@ class Enhance {
|
|||||||
// enhanced mode runner
|
// enhanced mode runner
|
||||||
private async runner(payload: CmdType.EnhancedPayload) {
|
private async runner(payload: CmdType.EnhancedPayload) {
|
||||||
const chain = payload.chain || [];
|
const chain = payload.chain || [];
|
||||||
|
const valid = payload.valid || [];
|
||||||
|
|
||||||
if (!Array.isArray(chain)) throw new Error("unhandle error");
|
if (!Array.isArray(chain)) throw new Error("unhandle error");
|
||||||
|
|
||||||
let pdata = payload.current || {};
|
let pdata = payload.current || {};
|
||||||
let useList = [] as string[];
|
let useList = valid;
|
||||||
|
|
||||||
for (const each of chain) {
|
for (const each of chain) {
|
||||||
const { uid, type = "" } = each.item;
|
const { uid, type = "" } = each.item;
|
||||||
|
|||||||
@@ -116,6 +116,7 @@ export namespace CmdType {
|
|||||||
export interface ProfilesConfig {
|
export interface ProfilesConfig {
|
||||||
current?: string;
|
current?: string;
|
||||||
chain?: string[];
|
chain?: string[];
|
||||||
|
valid?: string[];
|
||||||
items?: ProfileItem[];
|
items?: ProfileItem[];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -191,6 +192,7 @@ export namespace CmdType {
|
|||||||
|
|
||||||
export interface EnhancedPayload {
|
export interface EnhancedPayload {
|
||||||
chain: ChainItem[];
|
chain: ChainItem[];
|
||||||
|
valid: string[];
|
||||||
current: ProfileData;
|
current: ProfileData;
|
||||||
callback: string;
|
callback: string;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user