feat: support more options for remote profile

This commit is contained in:
GyDi 2022-03-10 02:03:55 +08:00
parent 04c754c0ac
commit fe1fea671c
No known key found for this signature in database
GPG Key ID: 1C95E0D3467B3084
7 changed files with 135 additions and 28 deletions

View File

@ -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))?;

View File

@ -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();
}

View 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

View File

@ -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) {

View File

@ -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

View File

@ -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) {

View File

@ -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 {