added support for special headers and displaying their information on the main page
This commit is contained in:
1
src-tauri/Cargo.lock
generated
1
src-tauri/Cargo.lock
generated
@@ -1125,6 +1125,7 @@ dependencies = [
|
|||||||
"tokio",
|
"tokio",
|
||||||
"tokio-tungstenite 0.27.0",
|
"tokio-tungstenite 0.27.0",
|
||||||
"tungstenite 0.27.0",
|
"tungstenite 0.27.0",
|
||||||
|
"url",
|
||||||
"users",
|
"users",
|
||||||
"warp",
|
"warp",
|
||||||
"winapi",
|
"winapi",
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ identifier = "io.github.clash-verge-rev.clash-verge-rev"
|
|||||||
tauri-build = { version = "2.3.0", features = [] }
|
tauri-build = { version = "2.3.0", features = [] }
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
url = "2.5.4"
|
||||||
os_info = "3.0"
|
os_info = "3.0"
|
||||||
machine-uid = "0.2"
|
machine-uid = "0.2"
|
||||||
warp = "0.3.7"
|
warp = "0.3.7"
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ use reqwest::StatusCode;
|
|||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use serde_yaml::Mapping;
|
use serde_yaml::Mapping;
|
||||||
use std::{fs, time::Duration};
|
use std::{fs, time::Duration};
|
||||||
|
use base64::{engine::general_purpose::STANDARD, Engine as _};
|
||||||
|
use url::Url;
|
||||||
|
|
||||||
use super::Config;
|
use super::Config;
|
||||||
|
|
||||||
@@ -53,6 +55,14 @@ pub struct PrfItem {
|
|||||||
#[serde(skip_serializing_if = "Option::is_none")]
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
pub home: Option<String>,
|
pub home: Option<String>,
|
||||||
|
|
||||||
|
/// profile support url
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub support_url: Option<String>,
|
||||||
|
|
||||||
|
/// profile announce
|
||||||
|
#[serde(skip_serializing_if = "Option::is_none")]
|
||||||
|
pub announce: Option<String>,
|
||||||
|
|
||||||
/// the file data
|
/// the file data
|
||||||
#[serde(skip)]
|
#[serde(skip)]
|
||||||
pub file_data: Option<String>,
|
pub file_data: Option<String>,
|
||||||
@@ -234,6 +244,8 @@ impl PrfItem {
|
|||||||
..PrfOption::default()
|
..PrfOption::default()
|
||||||
}),
|
}),
|
||||||
home: None,
|
home: None,
|
||||||
|
support_url: None,
|
||||||
|
announce: None,
|
||||||
updated: Some(chrono::Local::now().timestamp() as usize),
|
updated: Some(chrono::Local::now().timestamp() as usize),
|
||||||
file_data: Some(file_data.unwrap_or(tmpl::ITEM_LOCAL.into())),
|
file_data: Some(file_data.unwrap_or(tmpl::ITEM_LOCAL.into())),
|
||||||
})
|
})
|
||||||
@@ -297,6 +309,21 @@ impl PrfItem {
|
|||||||
|
|
||||||
let header = resp.headers();
|
let header = resp.headers();
|
||||||
|
|
||||||
|
let mut final_url = url.to_string();
|
||||||
|
|
||||||
|
if let Some(new_domain_value) = header.get("new-sub-domain") {
|
||||||
|
if let Ok(new_domain) = new_domain_value.to_str() {
|
||||||
|
if !new_domain.is_empty() {
|
||||||
|
if let Ok(mut parsed_url) = Url::parse(url) {
|
||||||
|
if parsed_url.set_host(Some(new_domain)).is_ok() {
|
||||||
|
final_url = parsed_url.to_string();
|
||||||
|
log::info!(target: "app", "URL host updated to -> {}", final_url);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// parse the Subscription UserInfo
|
// parse the Subscription UserInfo
|
||||||
let extra = match header.get("Subscription-Userinfo") {
|
let extra = match header.get("Subscription-Userinfo") {
|
||||||
Some(value) => {
|
Some(value) => {
|
||||||
@@ -354,9 +381,45 @@ impl PrfItem {
|
|||||||
None => None,
|
None => None,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let support_url = match header.get("support-url") {
|
||||||
|
Some(value) => {
|
||||||
|
let str_value = value.to_str().unwrap_or("");
|
||||||
|
Some(str_value.to_string())
|
||||||
|
}
|
||||||
|
None => None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let announce = match header.get("announce") {
|
||||||
|
Some(value) => {
|
||||||
|
let str_value = value.to_str().unwrap_or("");
|
||||||
|
if let Some(b64_data) = str_value.strip_prefix("base64:") {
|
||||||
|
STANDARD.decode(b64_data)
|
||||||
|
.ok()
|
||||||
|
.and_then(|bytes| String::from_utf8(bytes).ok())
|
||||||
|
} else {
|
||||||
|
Some(str_value.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let profile_title = match header.get("profile-title") {
|
||||||
|
Some(value) => {
|
||||||
|
let str_value = value.to_str().unwrap_or("");
|
||||||
|
if let Some(b64_data) = str_value.strip_prefix("base64:") {
|
||||||
|
STANDARD.decode(b64_data)
|
||||||
|
.ok()
|
||||||
|
.and_then(|bytes| String::from_utf8(bytes).ok())
|
||||||
|
} else {
|
||||||
|
Some(str_value.to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
None => None,
|
||||||
|
};
|
||||||
|
|
||||||
let uid = help::get_uid("R");
|
let uid = help::get_uid("R");
|
||||||
let file = format!("{uid}.yaml");
|
let file = format!("{uid}.yaml");
|
||||||
let name = name.unwrap_or(filename.unwrap_or("Remote File".into()));
|
let name = name.or(profile_title).unwrap_or(filename.unwrap_or("Remote File".into()));
|
||||||
let data = resp.text_with_charset("utf-8").await?;
|
let data = resp.text_with_charset("utf-8").await?;
|
||||||
|
|
||||||
// process the charset "UTF-8 with BOM"
|
// process the charset "UTF-8 with BOM"
|
||||||
@@ -404,7 +467,7 @@ impl PrfItem {
|
|||||||
name: Some(name),
|
name: Some(name),
|
||||||
desc,
|
desc,
|
||||||
file: Some(file),
|
file: Some(file),
|
||||||
url: Some(url.into()),
|
url: Some(final_url),
|
||||||
selected: None,
|
selected: None,
|
||||||
extra,
|
extra,
|
||||||
option: Some(PrfOption {
|
option: Some(PrfOption {
|
||||||
@@ -417,6 +480,8 @@ impl PrfItem {
|
|||||||
..PrfOption::default()
|
..PrfOption::default()
|
||||||
}),
|
}),
|
||||||
home,
|
home,
|
||||||
|
support_url,
|
||||||
|
announce,
|
||||||
updated: Some(chrono::Local::now().timestamp() as usize),
|
updated: Some(chrono::Local::now().timestamp() as usize),
|
||||||
file_data: Some(data.into()),
|
file_data: Some(data.into()),
|
||||||
})
|
})
|
||||||
@@ -444,6 +509,8 @@ impl PrfItem {
|
|||||||
extra: None,
|
extra: None,
|
||||||
option: None,
|
option: None,
|
||||||
home: None,
|
home: None,
|
||||||
|
support_url: None,
|
||||||
|
announce: None,
|
||||||
updated: Some(chrono::Local::now().timestamp() as usize),
|
updated: Some(chrono::Local::now().timestamp() as usize),
|
||||||
file_data: Some(template),
|
file_data: Some(template),
|
||||||
})
|
})
|
||||||
@@ -466,6 +533,8 @@ impl PrfItem {
|
|||||||
file: Some(file),
|
file: Some(file),
|
||||||
url: None,
|
url: None,
|
||||||
home: None,
|
home: None,
|
||||||
|
support_url: None,
|
||||||
|
announce: None,
|
||||||
selected: None,
|
selected: None,
|
||||||
extra: None,
|
extra: None,
|
||||||
option: None,
|
option: None,
|
||||||
@@ -487,6 +556,8 @@ impl PrfItem {
|
|||||||
file: Some(file),
|
file: Some(file),
|
||||||
url: None,
|
url: None,
|
||||||
home: None,
|
home: None,
|
||||||
|
support_url: None,
|
||||||
|
announce: None,
|
||||||
selected: None,
|
selected: None,
|
||||||
extra: None,
|
extra: None,
|
||||||
option: None,
|
option: None,
|
||||||
@@ -508,6 +579,8 @@ impl PrfItem {
|
|||||||
file: Some(file),
|
file: Some(file),
|
||||||
url: None,
|
url: None,
|
||||||
home: None,
|
home: None,
|
||||||
|
support_url: None,
|
||||||
|
announce: None,
|
||||||
selected: None,
|
selected: None,
|
||||||
extra: None,
|
extra: None,
|
||||||
option: None,
|
option: None,
|
||||||
@@ -529,6 +602,8 @@ impl PrfItem {
|
|||||||
file: Some(file),
|
file: Some(file),
|
||||||
url: None,
|
url: None,
|
||||||
home: None,
|
home: None,
|
||||||
|
support_url: None,
|
||||||
|
announce: None,
|
||||||
selected: None,
|
selected: None,
|
||||||
extra: None,
|
extra: None,
|
||||||
option: None,
|
option: None,
|
||||||
|
|||||||
@@ -220,6 +220,10 @@ impl IProfiles {
|
|||||||
each.extra = item.extra;
|
each.extra = item.extra;
|
||||||
each.updated = item.updated;
|
each.updated = item.updated;
|
||||||
each.home = item.home;
|
each.home = item.home;
|
||||||
|
each.announce = item.announce;
|
||||||
|
each.support_url = item.support_url;
|
||||||
|
each.name = item.name;
|
||||||
|
each.url = item.url;
|
||||||
each.option = PrfOption::merge(each.option.clone(), item.option);
|
each.option = PrfOption::merge(each.option.clone(), item.option);
|
||||||
// save the file data
|
// save the file data
|
||||||
// move the field value after save
|
// move the field value after save
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ import {
|
|||||||
Wrench,
|
Wrench,
|
||||||
AlertTriangle,
|
AlertTriangle,
|
||||||
Loader2,
|
Loader2,
|
||||||
|
Globe,
|
||||||
|
Send,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { useVerge } from "@/hooks/use-verge";
|
import { useVerge } from "@/hooks/use-verge";
|
||||||
import { useSystemState } from "@/hooks/use-system-state";
|
import { useSystemState } from "@/hooks/use-system-state";
|
||||||
@@ -34,6 +36,7 @@ import { Switch } from "@/components/ui/switch";
|
|||||||
import { ProxySelectors } from "@/components/home/proxy-selectors";
|
import { ProxySelectors } from "@/components/home/proxy-selectors";
|
||||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||||
import { closeAllConnections } from "@/services/api";
|
import { closeAllConnections } from "@/services/api";
|
||||||
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "@/components/ui/tooltip";
|
||||||
|
|
||||||
const MinimalHomePage: React.FC = () => {
|
const MinimalHomePage: React.FC = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@@ -50,12 +53,12 @@ const MinimalHomePage: React.FC = () => {
|
|||||||
return items.filter((i: any) => i && allowedTypes.includes(i.type!));
|
return items.filter((i: any) => i && allowedTypes.includes(i.type!));
|
||||||
}, [profiles]);
|
}, [profiles]);
|
||||||
|
|
||||||
const currentProfileName = useMemo(() => {
|
const currentProfile = useMemo(() => {
|
||||||
return (
|
return profileItems.find(p => p.uid === profiles?.current);
|
||||||
profileItems.find((p) => p.uid === profiles?.current)?.name ||
|
|
||||||
profiles?.current
|
|
||||||
);
|
|
||||||
}, [profileItems, profiles?.current]);
|
}, [profileItems, profiles?.current]);
|
||||||
|
console.log(currentProfile);
|
||||||
|
const currentProfileName = currentProfile?.name || profiles?.current;
|
||||||
|
|
||||||
const activateProfile = useCallback(
|
const activateProfile = useCallback(
|
||||||
async (uid: string, notifySuccess: boolean) => {
|
async (uid: string, notifySuccess: boolean) => {
|
||||||
try {
|
try {
|
||||||
@@ -200,6 +203,11 @@ const MinimalHomePage: React.FC = () => {
|
|||||||
|
|
||||||
<div className="flex items-center justify-center flex-grow w-full">
|
<div className="flex items-center justify-center flex-grow w-full">
|
||||||
<div className="flex flex-col items-center gap-8 pt-10">
|
<div className="flex flex-col items-center gap-8 pt-10">
|
||||||
|
{currentProfile?.announce && (
|
||||||
|
<p className="relative -translate-y-15 text-xl font-semibold text-foreground max-w-lg text-center">
|
||||||
|
{currentProfile.announce}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
<h1
|
<h1
|
||||||
className="text-4xl mb-2 font-semibold"
|
className="text-4xl mb-2 font-semibold"
|
||||||
@@ -270,7 +278,29 @@ const MinimalHomePage: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<footer className="flex justify-center p-4 flex-shrink-0">
|
||||||
|
{currentProfile?.support_url && (
|
||||||
|
<div className="flex items-center gap-2 text-sm text-muted-foreground">
|
||||||
|
<span>{t("Support")}:</span>
|
||||||
|
<TooltipProvider>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<a href={currentProfile.support_url} target="_blank" rel="noopener noreferrer" className="transition-colors hover:text-primary">
|
||||||
|
{(currentProfile.support_url.includes('t.me') || currentProfile.support_url.includes('telegram')) ? (
|
||||||
|
<Send className="h-5 w-5" />
|
||||||
|
) : (
|
||||||
|
<Globe className="h-5 w-5" />
|
||||||
|
)}
|
||||||
|
</a>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent>
|
||||||
|
<p>{currentProfile.support_url}</p>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</footer>
|
||||||
<ProfileViewer ref={viewerRef} onChange={() => mutateProfiles()} />
|
<ProfileViewer ref={viewerRef} onChange={() => mutateProfiles()} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
3
src/services/types.d.ts
vendored
3
src/services/types.d.ts
vendored
@@ -181,6 +181,7 @@ interface IClashInfo {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface IProfileItem {
|
interface IProfileItem {
|
||||||
|
currentProfile: any;
|
||||||
uid: string;
|
uid: string;
|
||||||
type?: "local" | "remote" | "merge" | "script";
|
type?: "local" | "remote" | "merge" | "script";
|
||||||
name?: string;
|
name?: string;
|
||||||
@@ -200,6 +201,8 @@ interface IProfileItem {
|
|||||||
};
|
};
|
||||||
option?: IProfileOption;
|
option?: IProfileOption;
|
||||||
home?: string;
|
home?: string;
|
||||||
|
support_url?: string;
|
||||||
|
announce?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IProfileOption {
|
interface IProfileOption {
|
||||||
|
|||||||
Reference in New Issue
Block a user