diff --git a/Cargo.lock b/Cargo.lock index fb6a89daa..429d95ad1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1170,6 +1170,7 @@ dependencies = [ "tauri-plugin-http", "tauri-plugin-mihomo", "tauri-plugin-notification", + "tauri-plugin-opener", "tauri-plugin-process", "tauri-plugin-shell", "tauri-plugin-updater", @@ -7462,6 +7463,28 @@ dependencies = [ "url", ] +[[package]] +name = "tauri-plugin-opener" +version = "2.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17e1bea14edce6b793a04e2417e3fd924b9bc4faae83cdee7d714156cceeed29" +dependencies = [ + "dunce", + "glob", + "objc2-app-kit", + "objc2-foundation", + "open", + "schemars 0.8.22", + "serde", + "serde_json", + "tauri", + "tauri-plugin", + "thiserror 2.0.20", + "url", + "windows 0.61.3", + "zbus", +] + [[package]] name = "tauri-plugin-process" version = "2.3.1" diff --git a/package.json b/package.json index 712fbf094..e5d3442fe 100644 --- a/package.json +++ b/package.json @@ -56,7 +56,7 @@ "@tauri-apps/plugin-dialog": "^2.7.1", "@tauri-apps/plugin-fs": "^2.5.1", "@tauri-apps/plugin-http": "~2.5.9", - "@tauri-apps/plugin-shell": "2.3.5", + "@tauri-apps/plugin-opener": "2.5.4", "@tauri-apps/plugin-updater": "2.10.1", "ahooks": "^3.9.7", "cidr-block": "^2.3.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 7ca6abd38..65c404546 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -53,9 +53,9 @@ importers: '@tauri-apps/plugin-http': specifier: ~2.5.9 version: 2.5.9 - '@tauri-apps/plugin-shell': - specifier: 2.3.5 - version: 2.3.5 + '@tauri-apps/plugin-opener': + specifier: 2.5.4 + version: 2.5.4 '@tauri-apps/plugin-updater': specifier: 2.10.1 version: 2.10.1 @@ -1874,8 +1874,8 @@ packages: '@tauri-apps/plugin-http@2.5.9': resolution: {integrity: sha512-lCiY0+vs4HvIUSvZrBs8TC3TiCB0MOPRmiUjTq4prW7SlcJE2jdLeT6KBsJrT9Tlplufl7W1pY6SFAO3gCWxDA==} - '@tauri-apps/plugin-shell@2.3.5': - resolution: {integrity: sha512-jewtULhiQ7lI7+owCKAjc8tYLJr92U16bPOeAa472LHJdgaibLP83NcfAF2e+wkEcA53FxKQAZ7byDzs2eeizg==} + '@tauri-apps/plugin-opener@2.5.4': + resolution: {integrity: sha512-1HnPkb+AmgO29HBazm4uPLKB+r7zzcTBW1d0fyYp1uP+jwtpoiNDGKMMzz58SFp49nOIrxdE3aUJtT57lfO9CQ==} '@tauri-apps/plugin-updater@2.10.1': resolution: {integrity: sha512-NFYMg+tWOZPJdzE/PpFj2qfqwAWwNS3kXrb1tm1gnBJ9mYzZ4WDRrwy8udzWoAnfGCHLuePNLY1WVCNHnh3eRA==} @@ -5830,7 +5830,7 @@ snapshots: dependencies: '@tauri-apps/api': 2.11.1 - '@tauri-apps/plugin-shell@2.3.5': + '@tauri-apps/plugin-opener@2.5.4': dependencies: '@tauri-apps/api': 2.11.1 diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 84c45bc07..ba9aaa4d2 100755 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -74,6 +74,7 @@ sysproxy = { git = "https://github.com/clash-verge-rev/sysproxy-rs.git", rev = " ] } network-interface = { version = "2.0.5", features = ["serde"] } tauri-plugin-shell = "2.3.5" +tauri-plugin-opener = "2.5.4" tauri-plugin-dialog = "2.7.2" tauri-plugin-fs = "2.5.1" tauri-plugin-process = "2.3.1" diff --git a/src-tauri/capabilities/migrated.json b/src-tauri/capabilities/migrated.json index 8248660a9..280595c1d 100644 --- a/src-tauri/capabilities/migrated.json +++ b/src-tauri/capabilities/migrated.json @@ -63,11 +63,10 @@ "core:window:allow-set-maximizable", "core:window:allow-set-minimizable", "core:webview:allow-print", - "shell:allow-execute", - "shell:allow-open", - "shell:allow-kill", - "shell:allow-spawn", - "shell:allow-stdin-write", + { + "identifier": "opener:allow-open-url", + "allow": [{ "url": "http://*" }, { "url": "https://*" }] + }, "dialog:allow-open", "global-shortcut:allow-is-registered", "global-shortcut:allow-register", @@ -77,7 +76,6 @@ "process:allow-exit", "clipboard-manager:allow-read-text", "clipboard-manager:allow-write-text", - "shell:default", "dialog:default" ] } diff --git a/src-tauri/src/cmd/app.rs b/src-tauri/src/cmd/app.rs index ed0fe8d62..885759a3c 100644 --- a/src-tauri/src/cmd/app.rs +++ b/src-tauri/src/cmd/app.rs @@ -23,11 +23,6 @@ pub async fn open_logs_dir() -> CmdResult<()> { open::that(log_dir).stringify_err() } -#[tauri::command] -pub fn open_web_url(url: String) -> CmdResult<()> { - open::that(url.as_str()).stringify_err() -} - #[tauri::command] pub fn open_devtools(app_handle: AppHandle) { if let Some(window) = app_handle.get_webview_window("main") { diff --git a/src-tauri/src/config/prfitem.rs b/src-tauri/src/config/prfitem.rs index c677215c5..f7c84de55 100644 --- a/src-tauri/src/config/prfitem.rs +++ b/src-tauri/src/config/prfitem.rs @@ -16,6 +16,17 @@ use tokio::fs; use reqwest_dav::re_exports::url::form_urlencoded; use tauri::Url; +pub(super) fn normalize_profile_home_url(raw: &str) -> Option { + let url = Url::parse(raw.trim()).ok()?; + + if !matches!(url.scheme(), "http" | "https") { + return None; + } + + url.host_str()?; + Some(url.to_string().into()) +} + #[derive(Debug, Clone, Deserialize, Serialize, Default)] pub struct PrfItem { pub uid: Option, @@ -332,13 +343,10 @@ impl PrfItem { }, }; - let home = match header.get("profile-web-page-url") { - Some(value) => { - let str_value = value.to_str().unwrap_or(""); - Some(str_value.into()) - } - None => None, - }; + let home = header + .get("profile-web-page-url") + .and_then(|value| value.to_str().ok()) + .and_then(normalize_profile_home_url); let uid = help::get_uid("R").into(); let file = format!("{uid}.yaml").into(); diff --git a/src-tauri/src/config/profiles.rs b/src-tauri/src/config/profiles.rs index c62661803..6cf5754be 100644 --- a/src-tauri/src/config/profiles.rs +++ b/src-tauri/src/config/profiles.rs @@ -1,6 +1,6 @@ use super::{ PrfOption, - prfitem::{PrfItem, PrfSelected}, + prfitem::{PrfItem, PrfSelected, normalize_profile_home_url}, }; use crate::{ core::{handle, tray::Tray}, @@ -106,6 +106,7 @@ impl IProfiles { match help::read_yaml::(&path).await { Ok(mut profiles) => { let items = profiles.items.get_or_insert_with(Vec::new); + let mut home_changed = false; for item in items.iter_mut() { if item.uid.is_none() { item.uid = Some(help::get_uid("d").into()); @@ -116,6 +117,19 @@ impl IProfiles { .allow_auto_update .get_or_insert(true); } + + if item + .home + .as_deref() + .is_some_and(|home| normalize_profile_home_url(home).is_none()) + { + item.home = None; + home_changed = true; + } + } + + if home_changed && let Err(err) = profiles.save_file().await { + logging!(error, Type::Config, "无法保存已清理的 profiles.yaml: {err}"); } profiles } diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 4043bbcb2..2e50c0508 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -51,6 +51,7 @@ mod app_init { .plugin(tauri_plugin_fs::init()) .plugin(tauri_plugin_dialog::init()) .plugin(tauri_plugin_shell::init()) + .plugin(tauri_plugin_opener::init()) .plugin(tauri_plugin_deep_link::init()) .plugin(tauri_plugin_http::init()) .plugin( @@ -128,7 +129,6 @@ mod app_init { cmd::get_embedded_server_port, cmd::open_app_dir, cmd::open_logs_dir, - cmd::open_web_url, cmd::open_core_dir, cmd::get_portable_flag, cmd::get_network_interfaces, diff --git a/src/components/home/home-profile-card.tsx b/src/components/home/home-profile-card.tsx index 51db674a7..7ac4203da 100644 --- a/src/components/home/home-profile-card.tsx +++ b/src/components/home/home-profile-card.tsx @@ -25,8 +25,9 @@ import { useTranslation } from 'react-i18next' import { useNavigate } from 'react-router' import { useAppRefreshers } from '@/providers/app-data-context' -import { openWebUrl, updateProfile } from '@/services/cmds' +import { updateProfile } from '@/services/cmds' import { showNotice } from '@/services/notice-service' +import { openExternalUrl } from '@/utils/open-external-url' import parseTraffic from '@/utils/parse-traffic' import { EnhancedCard } from './enhanced-card' @@ -49,6 +50,10 @@ const parseExpire = (expire?: number) => { return dayjs(expire * 1000).format('YYYY-MM-DD') } +const openProfileHome = (url: string) => { + void openExternalUrl(url).catch(showNotice.error) +} + // 使用类型定义,而不是导入 interface ProfileExtra { upload: number @@ -115,7 +120,7 @@ const ProfileDetails = ({ {current.home ? ( current.home && openWebUrl(current.home)} + onClick={() => current.home && openProfileHome(current.home)} sx={{ display: 'inline-flex', alignItems: 'center', @@ -318,7 +323,7 @@ export const HomeProfileCard = ({ current.home && openWebUrl(current.home)} + onClick={() => current.home && openProfileHome(current.home)} sx={{ color: 'inherit', textDecoration: 'none', diff --git a/src/components/profile/profile-item.tsx b/src/components/profile/profile-item.tsx index 2954b8d91..3f7bd0c41 100644 --- a/src/components/profile/profile-item.tsx +++ b/src/components/profile/profile-item.tsx @@ -18,7 +18,6 @@ import { MenuItem, Typography, } from '@mui/material' -import { open } from '@tauri-apps/plugin-shell' import { useLockFn } from 'ahooks' import dayjs from 'dayjs' import { @@ -47,6 +46,7 @@ import { showNotice } from '@/services/notice-service' import { useLoadingCache, useSetLoadingCache } from '@/services/states' import type { TranslationKey } from '@/types/generated/i18n-keys' import { debugLog } from '@/utils/debug' +import { openExternalUrl } from '@/utils/open-external-url' import parseTraffic from '@/utils/parse-traffic' import { ProfileBox } from './profile-box' @@ -316,7 +316,8 @@ const ProfileItemBase = (props: ProfileItemProps) => { const onOpenHome = () => { setAnchorEl(null) - open(itemData.home ?? '') + if (!itemData.home) return + void openExternalUrl(itemData.home).catch(showNotice.error) } const onEditInfo = () => { diff --git a/src/components/setting/mods/update-viewer.tsx b/src/components/setting/mods/update-viewer.tsx index a90b5780e..569f53d6f 100644 --- a/src/components/setting/mods/update-viewer.tsx +++ b/src/components/setting/mods/update-viewer.tsx @@ -1,5 +1,4 @@ import { alpha, Box, Button, LinearProgress } from '@mui/material' -import { open as openUrl } from '@tauri-apps/plugin-shell' import type { DownloadEvent } from '@tauri-apps/plugin-updater' import { useLockFn } from 'ahooks' import type { Ref } from 'react' @@ -19,6 +18,7 @@ import { useUpdate } from '@/hooks/use-update' import { restartApp } from '@/services/cmds' import { showNotice } from '@/services/notice-service' import { useSetUpdateState, useUpdateState } from '@/services/states' +import { openExternalUrl } from '@/utils/open-external-url' type MarkdownNode = { type: string @@ -151,6 +151,10 @@ export function UpdateViewer({ ref }: { ref?: Ref }) { const downloadedRef = useRef(0) const totalRef = useRef(0) + const openUrlWithNotice = (url: string) => { + void openExternalUrl(url).catch(showNotice.error) + } + const progress = useMemo(() => { if (total <= 0) return 0 return Math.min((downloaded / total) * 100, 100) @@ -276,7 +280,7 @@ export function UpdateViewer({ ref }: { ref?: Ref }) { size="small" sx={{ whiteSpace: 'nowrap' }} onClick={() => { - openUrl( + openUrlWithNotice( `https://github.com/clash-verge-rev/clash-verge-rev/releases/tag/v${updateInfo?.version}`, ) }} @@ -420,10 +424,25 @@ export function UpdateViewer({ ref }: { ref?: Ref }) { { - const { children } = props + a: ({ href, children, ...props }) => { return ( - + { + event.preventDefault() + if (!href) return + openUrlWithNotice(href) + }} + onAuxClick={(event) => { + event.preventDefault() + if (event.button === 1 && href) { + openUrlWithNotice(href) + } + }} + > {children} ) diff --git a/src/components/setting/mods/web-ui-viewer.tsx b/src/components/setting/mods/web-ui-viewer.tsx index 1920e81b8..0cb24653f 100644 --- a/src/components/setting/mods/web-ui-viewer.tsx +++ b/src/components/setting/mods/web-ui-viewer.tsx @@ -7,8 +7,8 @@ import { useTranslation } from 'react-i18next' import { BaseDialog, BaseEmpty, DialogRef } from '@/components/base' import { useClashInfo } from '@/hooks/use-clash' import { useVerge } from '@/hooks/use-verge' -import { openWebUrl } from '@/services/cmds' import { showNotice } from '@/services/notice-service' +import { openExternalUrl } from '@/utils/open-external-url' import { WebUIItem } from './web-ui-item' @@ -98,7 +98,7 @@ export function WebUIViewer({ ref }: { ref?: Ref }) { ) } - await openWebUrl(url) + await openExternalUrl(url) } catch (e: any) { showNotice.error('settings.modals.webUI.errors.openFailed', e) } diff --git a/src/pages/home.tsx b/src/pages/home.tsx index 15ea40ef4..32e1aafa7 100644 --- a/src/pages/home.tsx +++ b/src/pages/home.tsx @@ -34,7 +34,9 @@ import { HomeProfileCard } from '@/components/home/home-profile-card' import { ProxyTunCard } from '@/components/home/proxy-tun-card' import { useProfiles } from '@/hooks/use-profiles' import { useVerge } from '@/hooks/use-verge' -import { entry_lightweight_mode, openWebUrl } from '@/services/cmds' +import { entry_lightweight_mode } from '@/services/cmds' +import { showNotice } from '@/services/notice-service' +import { openExternalUrl } from '@/utils/open-external-url' const preloadTestCard = () => import('@/components/home/test-card').then((module) => ({ @@ -240,9 +242,11 @@ const HomePage = () => { (verge?.home_cards as HomeCardsSettings | undefined) ?? DEFAULT_HOME_CARDS // 文档链接函数 - const toGithubDoc = useLockFn(() => { - return openWebUrl('https://clash-verge-rev.github.io/index.html') - }) + const toGithubDoc = useLockFn(() => + openExternalUrl('https://clash-verge-rev.github.io/index.html').catch( + showNotice.error, + ), + ) // 新增:打开设置弹窗 const openSettings = useCallback(() => { diff --git a/src/pages/settings.tsx b/src/pages/settings.tsx index 6707b5e35..d00c8629d 100644 --- a/src/pages/settings.tsx +++ b/src/pages/settings.tsx @@ -8,9 +8,9 @@ import SettingClash from '@/components/setting/setting-clash' import SettingSystem from '@/components/setting/setting-system' import SettingVergeAdvanced from '@/components/setting/setting-verge-advanced' import SettingVergeBasic from '@/components/setting/setting-verge-basic' -import { openWebUrl } from '@/services/cmds' import { showNotice } from '@/services/notice-service' import { useThemeMode } from '@/services/states' +import { openExternalUrl } from '@/utils/open-external-url' const SettingPage = () => { const { t } = useTranslation() @@ -19,17 +19,21 @@ const SettingPage = () => { showNotice.error(err) } - const toGithubRepo = useLockFn(() => { - return openWebUrl('https://github.com/clash-verge-rev/clash-verge-rev') - }) + const toGithubRepo = useLockFn(() => + openExternalUrl('https://github.com/clash-verge-rev/clash-verge-rev').catch( + onError, + ), + ) - const toGithubDoc = useLockFn(() => { - return openWebUrl('https://clash-verge-rev.github.io/index.html') - }) + const toGithubDoc = useLockFn(() => + openExternalUrl('https://clash-verge-rev.github.io/index.html').catch( + onError, + ), + ) - const toTelegramChannel = useLockFn(() => { - return openWebUrl('https://t.me/clash_verge_re') - }) + const toTelegramChannel = useLockFn(() => + openExternalUrl('https://t.me/clash_verge_re').catch(onError), + ) const mode = useThemeMode() const isDark = mode === 'light' ? false : true diff --git a/src/services/cmds.ts b/src/services/cmds.ts index 98918e410..4aad73073 100644 --- a/src/services/cmds.ts +++ b/src/services/cmds.ts @@ -241,14 +241,6 @@ export async function openLogsDir() { return invoke('open_logs_dir').catch((err) => showNotice.error(err)) } -export const openWebUrl = async (url: string) => { - try { - await invoke('open_web_url', { url }) - } catch (err: any) { - showNotice.error(err) - } -} - export async function cmdTestDelay(url: string) { return invoke('test_delay', { url }) } diff --git a/src/utils/open-external-url.ts b/src/utils/open-external-url.ts new file mode 100644 index 000000000..da912c62a --- /dev/null +++ b/src/utils/open-external-url.ts @@ -0,0 +1,11 @@ +import { openUrl } from '@tauri-apps/plugin-opener' + +export async function openExternalUrl(raw: string) { + const url = new URL(raw.trim()) + + if (url.protocol !== 'http:' && url.protocol !== 'https:') { + throw new Error('Only HTTP and HTTPS URLs are allowed') + } + + await openUrl(url.toString()) +}