feat: profile item adjust

This commit is contained in:
GyDi 2022-03-05 19:04:20 +08:00
parent 08fa5205b0
commit f44039b628
No known key found for this signature in database
GPG Key ID: 1C95E0D3467B3084
10 changed files with 262 additions and 108 deletions

View File

@ -32,7 +32,7 @@ pub async fn import_profile(
with_proxy: bool, with_proxy: bool,
profiles_state: State<'_, ProfilesState>, profiles_state: State<'_, ProfilesState>,
) -> Result<(), String> { ) -> Result<(), String> {
let item = wrap_err!(PrfItem::from_url(&url, with_proxy).await)?; let item = wrap_err!(PrfItem::from_url(&url, None, None, with_proxy).await)?;
let mut profiles = profiles_state.0.lock().unwrap(); let mut profiles = profiles_state.0.lock().unwrap();
wrap_err!(profiles.append_item(item)) wrap_err!(profiles.append_item(item))
@ -42,12 +42,11 @@ pub async fn import_profile(
/// append a temp profile item file to the `profiles` dir /// append a temp profile item file to the `profiles` dir
/// view the temp profile file by using vscode or other editor /// view the temp profile file by using vscode or other editor
#[tauri::command] #[tauri::command]
pub async fn new_profile( pub async fn create_profile(
name: String, item: PrfItem, // partial
desc: String,
profiles_state: State<'_, ProfilesState>, profiles_state: State<'_, ProfilesState>,
) -> Result<(), String> { ) -> Result<(), String> {
let item = wrap_err!(PrfItem::from_local(name, desc))?; let item = wrap_err!(PrfItem::from(item).await)?;
let mut profiles = profiles_state.0.lock().unwrap(); let mut profiles = profiles_state.0.lock().unwrap();
wrap_err!(profiles.append_item(item)) wrap_err!(profiles.append_item(item))
@ -73,7 +72,7 @@ pub async fn update_profile(
item.url.clone().unwrap() item.url.clone().unwrap()
}; };
let item = wrap_err!(PrfItem::from_url(&url, with_proxy).await)?; let item = wrap_err!(PrfItem::from_url(&url, None, None, with_proxy).await)?;
let mut profiles = profiles_state.0.lock().unwrap(); let mut profiles = profiles_state.0.lock().unwrap();
wrap_err!(profiles.update_item(index.clone(), item))?; wrap_err!(profiles.update_item(index.clone(), item))?;

View File

@ -75,6 +75,42 @@ impl Default for PrfItem {
} }
impl PrfItem { impl PrfItem {
/// From partial item
/// must contain `itype`
pub async fn from(item: PrfItem) -> Result<PrfItem> {
if item.itype.is_none() {
bail!("type should not be null");
}
match item.itype.unwrap().as_str() {
"remote" => {
if item.url.is_none() {
bail!("url should not be null");
}
let url = item.url.as_ref().unwrap().as_str();
let name = item.name;
let desc = item.desc;
PrfItem::from_url(url, name, desc, false).await
}
"local" => {
let name = item.name.unwrap_or("Local File".into());
let desc = item.desc.unwrap_or("".into());
PrfItem::from_local(name, desc)
}
"merge" => {
let name = item.name.unwrap_or("Merge".into());
let desc = item.desc.unwrap_or("".into());
PrfItem::from_merge(name, desc)
}
"script" => {
let name = item.name.unwrap_or("Script".into());
let desc = item.desc.unwrap_or("".into());
PrfItem::from_script(name, desc)
}
typ @ _ => bail!("invalid type \"{typ}\""),
}
}
/// ## Local type /// ## Local type
/// create a new item from name/desc /// create a new item from name/desc
pub fn from_local(name: String, desc: String) -> Result<PrfItem> { pub fn from_local(name: String, desc: String) -> Result<PrfItem> {
@ -91,13 +127,18 @@ impl PrfItem {
selected: None, selected: None,
extra: None, extra: None,
updated: Some(help::get_now()), updated: Some(help::get_now()),
file_data: Some(tmpl::ITEM_CONFIG.into()), file_data: Some(tmpl::ITEM_LOCAL.into()),
}) })
} }
/// ## Remote type /// ## Remote type
/// create a new item from url /// create a new item from url
pub async fn from_url(url: &str, with_proxy: bool) -> Result<PrfItem> { pub async fn from_url(
url: &str,
name: Option<String>,
desc: Option<String>,
with_proxy: bool,
) -> Result<PrfItem> {
let mut builder = reqwest::ClientBuilder::new(); let mut builder = reqwest::ClientBuilder::new();
if !with_proxy { if !with_proxy {
@ -124,14 +165,14 @@ impl PrfItem {
let uid = help::get_uid("r"); let uid = help::get_uid("r");
let file = format!("{uid}.yaml"); let file = format!("{uid}.yaml");
let name = uid.clone(); let name = name.unwrap_or(uid.clone());
let data = resp.text_with_charset("utf-8").await?; let data = resp.text_with_charset("utf-8").await?;
Ok(PrfItem { Ok(PrfItem {
uid: Some(uid), uid: Some(uid),
itype: Some("remote".into()), itype: Some("remote".into()),
name: Some(name), name: Some(name),
desc: None, desc,
file: Some(file), file: Some(file),
url: Some(url.into()), url: Some(url.into()),
selected: None, selected: None,
@ -140,6 +181,46 @@ impl PrfItem {
file_data: Some(data), file_data: Some(data),
}) })
} }
/// ## Merge type (enhance)
/// create the enhanced item by using `merge` rule
pub fn from_merge(name: String, desc: String) -> Result<PrfItem> {
let uid = help::get_uid("m");
let file = format!("{uid}.yaml");
Ok(PrfItem {
uid: Some(uid),
itype: Some("merge".into()),
name: Some(name),
desc: Some(desc),
file: Some(file),
url: None,
selected: None,
extra: None,
updated: Some(help::get_now()),
file_data: Some(tmpl::ITEM_MERGE.into()),
})
}
/// ## Script type (enhance)
/// create the enhanced item by using javascript(browserjs)
pub fn from_script(name: String, desc: String) -> Result<PrfItem> {
let uid = help::get_uid("s");
let file = format!("{uid}.js"); // js ext
Ok(PrfItem {
uid: Some(uid),
itype: Some("script".into()),
name: Some(name),
desc: Some(desc),
file: Some(file),
url: None,
selected: None,
extra: None,
updated: Some(help::get_now()),
file_data: Some(tmpl::ITEM_SCRIPT.into()),
})
}
} }
/// ///

View File

@ -85,9 +85,9 @@ fn main() -> std::io::Result<()> {
cmds::get_verge_config, cmds::get_verge_config,
cmds::patch_verge_config, cmds::patch_verge_config,
// profile // profile
cmds::new_profile,
cmds::view_profile, cmds::view_profile,
cmds::patch_profile, cmds::patch_profile,
cmds::create_profile,
cmds::import_profile, cmds::import_profile,
cmds::update_profile, cmds::update_profile,
cmds::delete_profile, cmds::delete_profile,

View File

@ -32,11 +32,38 @@ system_proxy_bypass: localhost;127.*;10.*;192.168.*;<local>
"; ";
/// template for new a profile item /// template for new a profile item
pub const ITEM_CONFIG: &str = "# Profile Template for clash verge\n\n pub const ITEM_LOCAL: &str = "# Profile Template for clash verge
# proxies defination (optional, the same as clash)
proxies:\n proxies:
# proxy-groups (optional, the same as clash)
proxy-groups:\n proxy-groups:
# rules (optional, the same as clash)
rules:\n\n rules:
";
/// enhanced profile
pub const ITEM_MERGE: &str = "# Merge Template for clash verge
# The `Merge` format used to enhance profile
prepend-rules:
prepend-proxies:
prepend-proxy-groups:
append-rules:
append-proxies:
append-proxy-groups:
";
/// enhanced profile
pub const ITEM_SCRIPT: &str = "// Should define the `main` function
// The argument to this function is the clash config
// or the result of the previous handler
// so you should return the config after processing
function main(params) {
return params;
}
"; ";

View File

@ -1,6 +1,6 @@
import { useEffect, useState } from "react";
import { useLockFn } from "ahooks";
import { mutate } from "swr"; import { mutate } from "swr";
import { useEffect } from "react";
import { useLockFn, useSetState } from "ahooks";
import { import {
Button, Button,
Dialog, Dialog,
@ -22,66 +22,80 @@ interface Props {
// edit the profile item // edit the profile item
const ProfileEdit = (props: Props) => { const ProfileEdit = (props: Props) => {
const { open, itemData, onClose } = props; const { open, itemData, onClose } = props;
const [form, setForm] = useSetState({ ...itemData });
// todo: more type
const [name, setName] = useState(itemData.name);
const [desc, setDesc] = useState(itemData.desc);
const [url, setUrl] = useState(itemData.url);
useEffect(() => { useEffect(() => {
if (itemData) { if (itemData) {
setName(itemData.name); setForm({ ...itemData });
setDesc(itemData.desc);
setUrl(itemData.url);
} }
}, [itemData]); }, [itemData]);
const onUpdate = useLockFn(async () => { const onUpdate = useLockFn(async () => {
try { try {
const { uid } = itemData; const { uid } = itemData;
const { name, desc, url } = form;
await patchProfile(uid, { uid, name, desc, url }); await patchProfile(uid, { uid, name, desc, url });
mutate("getProfiles"); mutate("getProfiles");
onClose(); onClose();
} catch (err: any) { } catch (err: any) {
Notice.error(err?.message || err?.toString()); Notice.error(err?.message || err.toString());
} }
}); });
const textFieldProps = {
fullWidth: true,
size: "small",
margin: "normal",
variant: "outlined",
} as const;
const type =
form.type ?? form.url
? "remote"
: form.file?.endsWith("js")
? "script"
: "local";
return ( return (
<Dialog open={open} onClose={onClose}> <Dialog open={open} onClose={onClose}>
<DialogTitle>Edit Profile</DialogTitle> <DialogTitle sx={{ pb: 0.5 }}>Edit Profile</DialogTitle>
<DialogContent sx={{ width: 360, pb: 0.5 }}>
<DialogContent sx={{ width: 336, pb: 1 }}>
<TextField <TextField
{...textFieldProps}
disabled
label="Type"
value={type}
sx={{ input: { textTransform: "capitalize" } }}
/>
<TextField
{...textFieldProps}
autoFocus autoFocus
fullWidth
label="Name" label="Name"
margin="dense" value={form.name}
variant="outlined" onChange={(e) => setForm({ name: e.target.value })}
value={name}
onChange={(e) => setName(e.target.value)}
/> />
<TextField <TextField
fullWidth {...textFieldProps}
label="Descriptions" label="Descriptions"
margin="normal" value={form.desc}
variant="outlined" onChange={(e) => setForm({ desc: e.target.value })}
value={desc}
onChange={(e) => setDesc(e.target.value)}
/> />
<TextField {type === "remote" && (
fullWidth <TextField
label="Remote URL" {...textFieldProps}
margin="normal" label="Subscription Url"
variant="outlined" value={form.url}
value={url} onChange={(e) => setForm({ url: e.target.value })}
onChange={(e) => setUrl(e.target.value)} />
/> )}
</DialogContent> </DialogContent>
<DialogActions sx={{ px: 2, pb: 2 }}> <DialogActions sx={{ px: 2, pb: 2 }}>
<Button onClick={onClose}>Cancel</Button> <Button onClick={onClose}>Cancel</Button>
<Button onClick={onUpdate} variant="contained"> <Button onClick={onUpdate} variant="contained">
Update Update
</Button> </Button>

View File

@ -1,5 +1,7 @@
import React, { useEffect, useState } from "react";
import dayjs from "dayjs"; import dayjs from "dayjs";
import { useLockFn } from "ahooks";
import { useSWRConfig } from "swr";
import { useEffect, useState, MouseEvent } from "react";
import { import {
alpha, alpha,
Box, Box,
@ -11,8 +13,6 @@ import {
MenuItem, MenuItem,
Menu, Menu,
} from "@mui/material"; } from "@mui/material";
import { useLockFn } from "ahooks";
import { useSWRConfig } from "swr";
import { RefreshRounded } from "@mui/icons-material"; import { RefreshRounded } from "@mui/icons-material";
import { CmdType } from "../../services/types"; import { CmdType } from "../../services/types";
import { updateProfile, deleteProfile, viewProfile } from "../../services/cmds"; import { updateProfile, deleteProfile, viewProfile } from "../../services/cmds";
@ -48,7 +48,7 @@ interface Props {
onSelect: (force: boolean) => void; onSelect: (force: boolean) => void;
} }
const ProfileItem: React.FC<Props> = (props) => { const ProfileItem = (props: Props) => {
const { selected, itemData, onSelect } = props; const { selected, itemData, onSelect } = props;
const { mutate } = useSWRConfig(); const { mutate } = useSWRConfig();
@ -118,9 +118,7 @@ const ProfileItem: React.FC<Props> = (props) => {
} }
}); });
const handleContextMenu = ( const handleContextMenu = (event: MouseEvent<HTMLDivElement, MouseEvent>) => {
event: React.MouseEvent<HTMLDivElement, MouseEvent>
) => {
const { clientX, clientY } = event; const { clientX, clientY } = event;
setPosition({ top: clientY, left: clientX }); setPosition({ top: clientY, left: clientX });
setAnchorEl(event.currentTarget); setAnchorEl(event.currentTarget);
@ -180,7 +178,7 @@ const ProfileItem: React.FC<Props> = (props) => {
return { bgcolor, color, "& h2": { color: h2color } }; return { bgcolor, color, "& h2": { color: h2color } };
}} }}
onClick={() => onSelect(false)} onClick={() => onSelect(false)}
onContextMenu={handleContextMenu} onContextMenu={handleContextMenu as any}
> >
<Box display="flex" justifyContent="space-between"> <Box display="flex" justifyContent="space-between">
<Typography <Typography

View File

@ -1,63 +1,105 @@
import { useEffect, useState } from "react"; import { useSWRConfig } from "swr";
import { useLockFn, useSetState } from "ahooks";
import { import {
Button, Button,
Dialog, Dialog,
DialogActions, DialogActions,
DialogContent, DialogContent,
DialogTitle, DialogTitle,
FormControl,
InputLabel,
MenuItem,
Select,
TextField, TextField,
} from "@mui/material"; } from "@mui/material";
import { createProfile } from "../../services/cmds";
import Notice from "../base/base-notice"; import Notice from "../base/base-notice";
interface Props { interface Props {
open: boolean; open: boolean;
onClose: () => void; onClose: () => void;
onSubmit: (name: string, desc: string) => void;
} }
// create a new profile
// remote / local file / merge / script
const ProfileNew = (props: Props) => { const ProfileNew = (props: Props) => {
const { open, onClose, onSubmit } = props; const { open, onClose } = props;
const [name, setName] = useState("");
const [desc, setDesc] = useState("");
const onCreate = () => { const { mutate } = useSWRConfig();
if (!name.trim()) { const [form, setForm] = useSetState({
Notice.error("`Name` should not be null"); name: "",
desc: "",
type: "remote",
url: "",
});
const onCreate = useLockFn(async () => {
if (!form.type) {
Notice.error("`Type` should not be null");
return; return;
} }
onSubmit(name, desc);
};
useEffect(() => { try {
if (!open) { await createProfile({ ...form });
setName(""); setForm({ name: "", desc: "", type: "remote", url: "" });
setDesc(""); mutate("getProfiles");
onClose();
} catch (err: any) {
Notice.error(err.message || err.toString());
} }
}, [open]); });
const textFieldProps = {
fullWidth: true,
size: "small",
margin: "normal",
variant: "outlined",
} as const;
return ( return (
<Dialog open={open} onClose={onClose}> <Dialog open={open} onClose={onClose}>
<DialogTitle>Create Profile</DialogTitle> <DialogTitle sx={{ pb: 0.5 }}>Create Profile</DialogTitle>
<DialogContent sx={{ width: 320, pb: 0.5 }}>
<DialogContent sx={{ width: 336, pb: 1 }}>
<TextField <TextField
{...textFieldProps}
autoFocus autoFocus
fullWidth
label="Name" label="Name"
margin="dense" value={form.name}
variant="outlined" onChange={(e) => setForm({ name: e.target.value })}
value={name}
onChange={(e) => setName(e.target.value)}
/> />
<FormControl size="small" fullWidth sx={{ mt: 2, mb: 1 }}>
<InputLabel>Type</InputLabel>
<Select
label="Type"
value={form.type}
onChange={(e) => setForm({ type: e.target.value })}
>
<MenuItem value="remote">Remote</MenuItem>
<MenuItem value="local">Local</MenuItem>
<MenuItem value="script">Script</MenuItem>
<MenuItem value="merge">Merge</MenuItem>
</Select>
</FormControl>
<TextField <TextField
fullWidth {...textFieldProps}
label="Descriptions" label="Descriptions"
margin="normal" value={form.desc}
variant="outlined" onChange={(e) => setForm({ desc: e.target.value })}
value={desc}
onChange={(e) => setDesc(e.target.value)}
/> />
{form.type === "remote" && (
<TextField
{...textFieldProps}
label="Subscription Url"
value={form.url}
onChange={(e) => setForm({ url: e.target.value })}
/>
)}
</DialogContent> </DialogContent>
<DialogActions sx={{ px: 2, pb: 2 }}> <DialogActions sx={{ px: 2, pb: 2 }}>
<Button onClick={onClose}>Cancel</Button> <Button onClick={onClose}>Cancel</Button>
<Button onClick={onCreate} variant="contained"> <Button onClick={onCreate} variant="contained">

View File

@ -1,19 +1,18 @@
import useSWR, { useSWRConfig } from "swr"; import useSWR, { useSWRConfig } from "swr";
import { useEffect, useState } from "react"; import { useEffect, useState } from "react";
import { useLockFn } from "ahooks"; import { useLockFn } from "ahooks";
import { Box, Button, Grid, TextField } from "@mui/material"; import { Box, Button, Grid, TextField, Typography } from "@mui/material";
import { import {
getProfiles, getProfiles,
selectProfile, selectProfile,
patchProfile, patchProfile,
importProfile, importProfile,
newProfile,
} from "../services/cmds"; } from "../services/cmds";
import { getProxies, updateProxy } from "../services/api"; import { getProxies, updateProxy } from "../services/api";
import Notice from "../components/base/base-notice"; import Notice from "../components/base/base-notice";
import BasePage from "../components/base/base-page"; import BasePage from "../components/base/base-page";
import ProfileItem from "../components/profile/profile-item";
import ProfileNew from "../components/profile/profile-new"; import ProfileNew from "../components/profile/profile-new";
import ProfileItem from "../components/profile/profile-item";
const ProfilePage = () => { const ProfilePage = () => {
const [url, setUrl] = useState(""); const [url, setUrl] = useState("");
@ -21,6 +20,7 @@ const ProfilePage = () => {
const { mutate } = useSWRConfig(); const { mutate } = useSWRConfig();
const { data: profiles = {} } = useSWR("getProfiles", getProfiles); const { data: profiles = {} } = useSWR("getProfiles", getProfiles);
const [dialogOpen, setDialogOpen] = useState(false);
useEffect(() => { useEffect(() => {
if (profiles.current == null) return; if (profiles.current == null) return;
@ -96,18 +96,7 @@ const ProfilePage = () => {
await selectProfile(current); await selectProfile(current);
mutate("getProfiles", { ...profiles, current: current }, true); mutate("getProfiles", { ...profiles, current: current }, true);
} catch (err: any) { } catch (err: any) {
err && Notice.error(err.toString()); err && Notice.error(err.message || err.toString());
}
});
const [dialogOpen, setDialogOpen] = useState(false);
const onNew = useLockFn(async (name: string, desc: string) => {
try {
await newProfile(name, desc);
setDialogOpen(false);
mutate("getProfiles");
} catch (err: any) {
err && Notice.error(err.toString());
} }
}); });
@ -149,11 +138,13 @@ const ProfilePage = () => {
))} ))}
</Grid> </Grid>
<ProfileNew <ProfileNew open={dialogOpen} onClose={() => setDialogOpen(false)} />
open={dialogOpen}
onClose={() => setDialogOpen(false)} <header data-windrag style={{ marginTop: 20, userSelect: "none" }}>
onSubmit={onNew} <Typography variant="h5" component="h2" data-windrag>
/> Enhanced
</Typography>
</header>
</BasePage> </BasePage>
); );
}; };

View File

@ -9,8 +9,8 @@ export async function syncProfiles() {
return invoke<void>("sync_profiles"); return invoke<void>("sync_profiles");
} }
export async function newProfile(name: string, desc: string) { export async function createProfile(item: Partial<CmdType.ProfileItem>) {
return invoke<void>("new_profile", { name, desc }); return invoke<void>("create_profile", { item });
} }
export async function viewProfile(index: string) { export async function viewProfile(index: string) {

View File

@ -78,6 +78,8 @@ export namespace ApiType {
* Some interface for command * Some interface for command
*/ */
export namespace CmdType { export namespace CmdType {
export type ProfileType = "local" | "remote" | "merge" | "script";
export interface ClashInfo { export interface ClashInfo {
status: string; status: string;
port?: string; port?: string;
@ -87,7 +89,7 @@ export namespace CmdType {
export interface ProfileItem { export interface ProfileItem {
uid: string; uid: string;
type?: string; type?: ProfileType | string;
name?: string; name?: string;
desc?: string; desc?: string;
file?: string; file?: string;