diff --git a/src-tauri/src/cmds.rs b/src-tauri/src/cmds.rs index 4026c22..1e24371 100644 --- a/src-tauri/src/cmds.rs +++ b/src-tauri/src/cmds.rs @@ -32,7 +32,7 @@ pub async fn import_profile( with_proxy: bool, profiles_state: State<'_, ProfilesState>, ) -> 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(); 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 /// view the temp profile file by using vscode or other editor #[tauri::command] -pub async fn new_profile( - name: String, - desc: String, +pub async fn create_profile( + item: PrfItem, // partial profiles_state: State<'_, ProfilesState>, ) -> 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(); wrap_err!(profiles.append_item(item)) @@ -73,7 +72,7 @@ pub async fn update_profile( 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(); wrap_err!(profiles.update_item(index.clone(), item))?; diff --git a/src-tauri/src/core/profiles.rs b/src-tauri/src/core/profiles.rs index b12b01f..a80d9ac 100644 --- a/src-tauri/src/core/profiles.rs +++ b/src-tauri/src/core/profiles.rs @@ -75,6 +75,42 @@ impl Default for PrfItem { } impl PrfItem { + /// From partial item + /// must contain `itype` + pub async fn from(item: PrfItem) -> Result { + 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 /// create a new item from name/desc pub fn from_local(name: String, desc: String) -> Result { @@ -91,13 +127,18 @@ impl PrfItem { selected: None, extra: None, updated: Some(help::get_now()), - file_data: Some(tmpl::ITEM_CONFIG.into()), + file_data: Some(tmpl::ITEM_LOCAL.into()), }) } /// ## Remote type /// create a new item from url - pub async fn from_url(url: &str, with_proxy: bool) -> Result { + pub async fn from_url( + url: &str, + name: Option, + desc: Option, + with_proxy: bool, + ) -> Result { let mut builder = reqwest::ClientBuilder::new(); if !with_proxy { @@ -124,14 +165,14 @@ impl PrfItem { let uid = help::get_uid("r"); 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?; Ok(PrfItem { uid: Some(uid), itype: Some("remote".into()), name: Some(name), - desc: None, + desc, file: Some(file), url: Some(url.into()), selected: None, @@ -140,6 +181,46 @@ impl PrfItem { file_data: Some(data), }) } + + /// ## Merge type (enhance) + /// create the enhanced item by using `merge` rule + pub fn from_merge(name: String, desc: String) -> Result { + 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 { + 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()), + }) + } } /// diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 944c24a..d06b944 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -85,9 +85,9 @@ fn main() -> std::io::Result<()> { cmds::get_verge_config, cmds::patch_verge_config, // profile - cmds::new_profile, cmds::view_profile, cmds::patch_profile, + cmds::create_profile, cmds::import_profile, cmds::update_profile, cmds::delete_profile, diff --git a/src-tauri/src/utils/tmpl.rs b/src-tauri/src/utils/tmpl.rs index 2462c2d..979ff86 100644 --- a/src-tauri/src/utils/tmpl.rs +++ b/src-tauri/src/utils/tmpl.rs @@ -32,11 +32,38 @@ system_proxy_bypass: localhost;127.*;10.*;192.168.*; "; /// template for new a profile item -pub const ITEM_CONFIG: &str = "# Profile Template for clash verge\n\n -# proxies defination (optional, the same as clash) -proxies:\n -# proxy-groups (optional, the same as clash) -proxy-groups:\n -# rules (optional, the same as clash) -rules:\n\n +pub const ITEM_LOCAL: &str = "# Profile Template for clash verge + +proxies: + +proxy-groups: + +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; +} "; diff --git a/src/components/profile/profile-edit.tsx b/src/components/profile/profile-edit.tsx index 0b191ad..cda39d2 100644 --- a/src/components/profile/profile-edit.tsx +++ b/src/components/profile/profile-edit.tsx @@ -1,6 +1,6 @@ -import { useEffect, useState } from "react"; -import { useLockFn } from "ahooks"; import { mutate } from "swr"; +import { useEffect } from "react"; +import { useLockFn, useSetState } from "ahooks"; import { Button, Dialog, @@ -22,66 +22,80 @@ interface Props { // edit the profile item const ProfileEdit = (props: Props) => { const { open, itemData, onClose } = props; - - // todo: more type - const [name, setName] = useState(itemData.name); - const [desc, setDesc] = useState(itemData.desc); - const [url, setUrl] = useState(itemData.url); + const [form, setForm] = useSetState({ ...itemData }); useEffect(() => { if (itemData) { - setName(itemData.name); - setDesc(itemData.desc); - setUrl(itemData.url); + setForm({ ...itemData }); } }, [itemData]); const onUpdate = useLockFn(async () => { try { const { uid } = itemData; + const { name, desc, url } = form; await patchProfile(uid, { uid, name, desc, url }); mutate("getProfiles"); onClose(); } 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 ( - Edit Profile - + Edit Profile + + + + setName(e.target.value)} + value={form.name} + onChange={(e) => setForm({ name: e.target.value })} /> setDesc(e.target.value)} + value={form.desc} + onChange={(e) => setForm({ desc: e.target.value })} /> - setUrl(e.target.value)} - /> + {type === "remote" && ( + setForm({ url: e.target.value })} + /> + )} + - diff --git a/src/components/profile/profile-item.tsx b/src/components/profile/profile-item.tsx index 0484b46..238974d 100644 --- a/src/components/profile/profile-item.tsx +++ b/src/components/profile/profile-item.tsx @@ -1,5 +1,7 @@ -import React, { useEffect, useState } from "react"; import dayjs from "dayjs"; +import { useLockFn } from "ahooks"; +import { useSWRConfig } from "swr"; +import { useEffect, useState, MouseEvent } from "react"; import { alpha, Box, @@ -11,8 +13,6 @@ import { MenuItem, Menu, } from "@mui/material"; -import { useLockFn } from "ahooks"; -import { useSWRConfig } from "swr"; import { RefreshRounded } from "@mui/icons-material"; import { CmdType } from "../../services/types"; import { updateProfile, deleteProfile, viewProfile } from "../../services/cmds"; @@ -48,7 +48,7 @@ interface Props { onSelect: (force: boolean) => void; } -const ProfileItem: React.FC = (props) => { +const ProfileItem = (props: Props) => { const { selected, itemData, onSelect } = props; const { mutate } = useSWRConfig(); @@ -118,9 +118,7 @@ const ProfileItem: React.FC = (props) => { } }); - const handleContextMenu = ( - event: React.MouseEvent - ) => { + const handleContextMenu = (event: MouseEvent) => { const { clientX, clientY } = event; setPosition({ top: clientY, left: clientX }); setAnchorEl(event.currentTarget); @@ -180,7 +178,7 @@ const ProfileItem: React.FC = (props) => { return { bgcolor, color, "& h2": { color: h2color } }; }} onClick={() => onSelect(false)} - onContextMenu={handleContextMenu} + onContextMenu={handleContextMenu as any} > void; - onSubmit: (name: string, desc: string) => void; } +// create a new profile +// remote / local file / merge / script const ProfileNew = (props: Props) => { - const { open, onClose, onSubmit } = props; - const [name, setName] = useState(""); - const [desc, setDesc] = useState(""); + const { open, onClose } = props; - const onCreate = () => { - if (!name.trim()) { - Notice.error("`Name` should not be null"); + const { mutate } = useSWRConfig(); + const [form, setForm] = useSetState({ + name: "", + desc: "", + type: "remote", + url: "", + }); + + const onCreate = useLockFn(async () => { + if (!form.type) { + Notice.error("`Type` should not be null"); return; } - onSubmit(name, desc); - }; - useEffect(() => { - if (!open) { - setName(""); - setDesc(""); + try { + await createProfile({ ...form }); + setForm({ name: "", desc: "", type: "remote", url: "" }); + 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 ( - Create Profile - + Create Profile + + setName(e.target.value)} + value={form.name} + onChange={(e) => setForm({ name: e.target.value })} /> + + Type + + + setDesc(e.target.value)} + value={form.desc} + onChange={(e) => setForm({ desc: e.target.value })} /> + + {form.type === "remote" && ( + setForm({ url: e.target.value })} + /> + )} +