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