mirror of
https://github.com/clash-verge-rev/clash-verge-rev.git
synced 2024-11-16 11:42:21 +08:00
feat: support more options for remote profile
This commit is contained in:
parent
04c754c0ac
commit
fe1fea671c
|
@ -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))?;
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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 {
|
||||
|
|
Loading…
Reference in New Issue
Block a user