diff --git a/src-tauri/src/cmds.rs b/src-tauri/src/cmds.rs index ab142343a138c2f7f0eb31ca67f549569654d3b5..226b5579edccce90041edf2b1f18d0e5477189d1 100644 --- a/src-tauri/src/cmds.rs +++ b/src-tauri/src/cmds.rs @@ -1,5 +1,5 @@ use crate::{ - core::{ClashInfo, PrfItem, Profiles, VergeConfig}, + core::{ClashInfo, PrfItem, PrfOption, Profiles, VergeConfig}, states::{ClashState, ProfilesState, VergeState}, utils::{dirs, sysopt::SysProxyConfig}, }; @@ -28,10 +28,10 @@ pub fn sync_profiles(profiles_state: State<'_, ProfilesState>) -> Result<(), Str #[tauri::command] pub async fn import_profile( url: String, - with_proxy: bool, + option: Option<PrfOption>, profiles_state: State<'_, ProfilesState>, ) -> Result<(), String> { - let item = wrap_err!(PrfItem::from_url(&url, None, None, with_proxy).await)?; + let item = wrap_err!(PrfItem::from_url(&url, None, None, option).await)?; let mut profiles = profiles_state.0.lock().unwrap(); wrap_err!(profiles.append_item(item)) @@ -55,7 +55,7 @@ pub async fn create_profile( #[tauri::command] pub async fn update_profile( index: String, - with_proxy: bool, + option: Option<PrfOption>, clash_state: State<'_, ClashState>, profiles_state: State<'_, ProfilesState>, ) -> Result<(), String> { @@ -71,7 +71,7 @@ pub async fn update_profile( item.url.clone().unwrap() }; - let item = wrap_err!(PrfItem::from_url(&url, None, None, with_proxy).await)?; + let item = wrap_err!(PrfItem::from_url(&url, None, None, option).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 d82d48d87f3e5a9818329125b4f4bb58925a62c3..6b7bdd51da87046d3887eb531af57dc225b1e590 100644 --- a/src-tauri/src/core/profiles.rs +++ b/src-tauri/src/core/profiles.rs @@ -31,13 +31,17 @@ pub struct PrfItem { #[serde(skip_serializing_if = "Option::is_none")] pub selected: Option<Vec<PrfSelected>>, - /// user info + /// subscription user info #[serde(skip_serializing_if = "Option::is_none")] pub extra: Option<PrfExtra>, /// updated time pub updated: Option<usize>, + /// some options of the item + #[serde(skip_serializing_if = "Option::is_none")] + pub option: Option<PrfOption>, + /// the file data #[serde(skip)] pub file_data: Option<String>, @@ -57,6 +61,18 @@ pub struct PrfExtra { pub expire: usize, } +#[derive(Default, Debug, Clone, Deserialize, Serialize)] +pub struct PrfOption { + /// for `remote` profile's http request + /// see issue #13 + #[serde(skip_serializing_if = "Option::is_none")] + pub user_agent: Option<String>, + + /// for `remote` profile + #[serde(skip_serializing_if = "Option::is_none")] + pub with_proxy: Option<bool>, +} + impl Default for PrfItem { fn default() -> Self { PrfItem { @@ -69,6 +85,7 @@ impl Default for PrfItem { selected: None, extra: None, updated: None, + option: None, file_data: None, } } @@ -90,7 +107,7 @@ impl PrfItem { 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 + PrfItem::from_url(url, name, desc, item.option).await } "local" => { let name = item.name.unwrap_or("Local File".into()); @@ -126,6 +143,7 @@ impl PrfItem { url: None, selected: None, extra: None, + option: None, updated: Some(help::get_now()), file_data: Some(tmpl::ITEM_LOCAL.into()), }) @@ -137,13 +155,25 @@ impl PrfItem { url: &str, name: Option<String>, desc: Option<String>, - with_proxy: bool, + option: Option<PrfOption>, ) -> Result<PrfItem> { + let with_proxy = match option.as_ref() { + Some(opt) => opt.with_proxy.unwrap_or(false), + None => false, + }; + let user_agent = match option.as_ref() { + Some(opt) => opt.user_agent.clone(), + None => None, + }; + let mut builder = reqwest::ClientBuilder::new(); if !with_proxy { builder = builder.no_proxy(); } + if let Some(user_agent) = user_agent { + builder = builder.user_agent(user_agent); + } let resp = builder.build()?.get(url).send().await?; let header = resp.headers(); @@ -177,6 +207,7 @@ impl PrfItem { url: Some(url.into()), selected: None, extra, + option, updated: Some(help::get_now()), file_data: Some(data), }) @@ -197,6 +228,7 @@ impl PrfItem { url: None, selected: None, extra: None, + option: None, updated: Some(help::get_now()), file_data: Some(tmpl::ITEM_MERGE.into()), }) @@ -217,6 +249,7 @@ impl PrfItem { url: None, selected: None, extra: None, + option: None, updated: Some(help::get_now()), file_data: Some(tmpl::ITEM_SCRIPT.into()), }) @@ -382,6 +415,9 @@ impl Profiles { patch!(each, item, extra); patch!(each, item, updated); + // can be removed + each.option = item.option; + self.items = Some(items); return self.save_file(); } diff --git a/src/components/profile/profile-edit.tsx b/src/components/profile/profile-edit.tsx index 72693d8b874e464df0608cff0ff5777c2b9117cf..a739ebbf863585dc43b6ff0eaef5be03fad5d014 100644 --- a/src/components/profile/profile-edit.tsx +++ b/src/components/profile/profile-edit.tsx @@ -1,5 +1,5 @@ import { mutate } from "swr"; -import { useEffect } from "react"; +import { useEffect, useState } from "react"; import { useLockFn, useSetState } from "ahooks"; import { Button, @@ -7,8 +7,10 @@ import { DialogActions, DialogContent, DialogTitle, + IconButton, TextField, } from "@mui/material"; +import { Settings } from "@mui/icons-material"; import { CmdType } from "../../services/types"; import { patchProfile } from "../../services/cmds"; import Notice from "../base/base-notice"; @@ -20,13 +22,18 @@ interface Props { } // edit the profile item +// remote / local file / merge / script const ProfileEdit = (props: Props) => { const { open, itemData, onClose } = props; const [form, setForm] = useSetState({ ...itemData }); + const [option, setOption] = useSetState(itemData.option ?? {}); + const [showOpt, setShowOpt] = useState(!!itemData.option); useEffect(() => { if (itemData) { setForm({ ...itemData }); + setOption(itemData.option ?? {}); + setShowOpt(!!itemData.option?.user_agent); } }, [itemData]); @@ -34,12 +41,14 @@ const ProfileEdit = (props: Props) => { try { const { uid } = itemData; const { name, desc, url } = form; + const option_ = showOpt ? option : undefined; if (itemData.type === "remote" && !url) { throw new Error("Remote URL should not be null"); } - await patchProfile(uid, { uid, name, desc, url }); + await patchProfile(uid, { uid, name, desc, url, option: option_ }); + setShowOpt(false); mutate("getProfiles"); onClose(); } catch (err: any) { @@ -94,9 +103,28 @@ const ProfileEdit = (props: Props) => { onChange={(e) => setForm({ url: e.target.value })} /> )} + + {showOpt && ( + <TextField + {...textFieldProps} + label="User Agent" + value={option.user_agent} + onChange={(e) => setOption({ user_agent: e.target.value })} + /> + )} </DialogContent> - <DialogActions sx={{ px: 2, pb: 2 }}> + <DialogActions sx={{ px: 2, pb: 2, position: "relative" }}> + {form.type === "remote" && ( + <IconButton + size="small" + sx={{ position: "absolute", left: 18 }} + onClick={() => setShowOpt((o) => !o)} + > + <Settings /> + </IconButton> + )} + <Button onClick={onClose}>Cancel</Button> <Button onClick={onUpdate} variant="contained"> Update diff --git a/src/components/profile/profile-item.tsx b/src/components/profile/profile-item.tsx index ebcf4ecf6ab009349865a4da912a54935fbf409a..b746ee4825d1c829dfa613ea665f4623ead2059f 100644 --- a/src/components/profile/profile-item.tsx +++ b/src/components/profile/profile-item.tsx @@ -98,7 +98,7 @@ const ProfileItem = (props: Props) => { if (loading) return; setLoading(true); try { - await updateProfile(itemData.uid, withProxy); + await updateProfile(itemData.uid, { with_proxy: withProxy }); setLoading(false); mutate("getProfiles"); } catch (err: any) { diff --git a/src/components/profile/profile-new.tsx b/src/components/profile/profile-new.tsx index f58e770256b38152e1cece0319d7e4a2f9445f9d..efebd5ffde5dadd259c7567086de1339d0beecbf 100644 --- a/src/components/profile/profile-new.tsx +++ b/src/components/profile/profile-new.tsx @@ -1,3 +1,4 @@ +import { useState } from "react"; import { useSWRConfig } from "swr"; import { useLockFn, useSetState } from "ahooks"; import { @@ -7,11 +8,13 @@ import { DialogContent, DialogTitle, FormControl, + IconButton, InputLabel, MenuItem, Select, TextField, } from "@mui/material"; +import { Settings } from "@mui/icons-material"; import { createProfile } from "../../services/cmds"; import Notice from "../base/base-notice"; @@ -27,12 +30,17 @@ const ProfileNew = (props: Props) => { const { mutate } = useSWRConfig(); const [form, setForm] = useSetState({ + type: "remote", name: "", desc: "", - type: "remote", url: "", }); + const [showOpt, setShowOpt] = useState(false); + const [option, setOption] = useSetState({ + user_agent: "", + }); // able to add more option + const onCreate = useLockFn(async () => { if (!form.type) { Notice.error("`Type` should not be null"); @@ -43,11 +51,15 @@ const ProfileNew = (props: Props) => { const name = form.name || `${form.type} file`; if (form.type === "remote" && !form.url) { - throw new Error("Remote URL should not be null"); + throw new Error("The URL should not be null"); } - await createProfile({ ...form, name }); - setForm({ name: "", desc: "", type: "remote", url: "" }); + const option_ = showOpt ? option : undefined; + await createProfile({ ...form, name, option: option_ }); + setForm({ type: "remote", name: "", desc: "", url: "" }); + setOption({ user_agent: "" }); + setShowOpt(false); + mutate("getProfiles"); onClose(); } catch (err: any) { @@ -67,17 +79,10 @@ const ProfileNew = (props: Props) => { <DialogTitle sx={{ pb: 0.5 }}>Create Profile</DialogTitle> <DialogContent sx={{ width: 336, pb: 1 }}> - <TextField - {...textFieldProps} - autoFocus - label="Name" - value={form.name} - onChange={(e) => setForm({ name: e.target.value })} - /> - <FormControl size="small" fullWidth sx={{ mt: 2, mb: 1 }}> <InputLabel>Type</InputLabel> <Select + autoFocus label="Type" value={form.type} onChange={(e) => setForm({ type: e.target.value })} @@ -89,6 +94,13 @@ const ProfileNew = (props: Props) => { </Select> </FormControl> + <TextField + {...textFieldProps} + label="Name" + value={form.name} + onChange={(e) => setForm({ name: e.target.value })} + /> + <TextField {...textFieldProps} label="Descriptions" @@ -104,9 +116,28 @@ const ProfileNew = (props: Props) => { onChange={(e) => setForm({ url: e.target.value })} /> )} + + {showOpt && ( + <TextField + {...textFieldProps} + label="User Agent" + value={option.user_agent} + onChange={(e) => setOption({ user_agent: e.target.value })} + /> + )} </DialogContent> - <DialogActions sx={{ px: 2, pb: 2 }}> + <DialogActions sx={{ px: 2, pb: 2, position: "relative" }}> + {form.type === "remote" && ( + <IconButton + size="small" + sx={{ position: "absolute", left: 18 }} + onClick={() => setShowOpt((o) => !o)} + > + <Settings /> + </IconButton> + )} + <Button onClick={onClose}>Cancel</Button> <Button onClick={onCreate} variant="contained"> Create diff --git a/src/services/cmds.ts b/src/services/cmds.ts index f01f1531a2014d4f3dde50071d06126a2cc79902..a3b59f5b25406648eeca4660394810533f0345fe 100644 --- a/src/services/cmds.ts +++ b/src/services/cmds.ts @@ -22,11 +22,17 @@ export async function viewProfile(index: string) { } export async function importProfile(url: string) { - return invoke<void>("import_profile", { url, withProxy: true }); + return invoke<void>("import_profile", { + url, + option: { with_proxy: true }, + }); } -export async function updateProfile(index: string, withProxy: boolean) { - return invoke<void>("update_profile", { index, withProxy }); +export async function updateProfile( + index: string, + option?: CmdType.ProfileOption +) { + return invoke<void>("update_profile", { index, option }); } export async function deleteProfile(index: string) { diff --git a/src/services/types.ts b/src/services/types.ts index 1d04a632de059d7ae6938580adc9992ef261f645..5642cb45ea0ef5158c8c21fac7755239ffa3e0ff 100644 --- a/src/services/types.ts +++ b/src/services/types.ts @@ -105,6 +105,12 @@ export namespace CmdType { total: number; expire: number; }; + option?: ProfileOption; + } + + export interface ProfileOption { + user_agent?: string; + with_proxy?: boolean; } export interface ProfilesConfig {