diff --git a/package.json b/package.json index 81394a1..79b142e 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ "electron-updater": "^6.2.1", "framer-motion": "^11.3.19", "next-themes": "^0.3.0", + "pubsub-js": "^1.9.4", "react-icons": "^5.2.1", "react-monaco-editor": "^0.55.0", "react-router-dom": "^6.25.1", @@ -40,6 +41,7 @@ "@electron-toolkit/eslint-config-ts": "^2.0.0", "@electron-toolkit/tsconfig": "^1.0.1", "@types/node": "^22.0.0", + "@types/pubsub-js": "^1.8.6", "@types/react": "^18.3.3", "@types/react-dom": "^18.3.0", "@types/ws": "^8.5.12", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 82828eb..6a9b5dc 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -32,6 +32,9 @@ importers: next-themes: specifier: ^0.3.0 version: 0.3.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1) + pubsub-js: + specifier: ^1.9.4 + version: 1.9.4 react-icons: specifier: ^5.2.1 version: 5.2.1(react@18.3.1) @@ -66,6 +69,9 @@ importers: '@types/node': specifier: ^22.0.0 version: 22.0.0 + '@types/pubsub-js': + specifier: ^1.8.6 + version: 1.8.6 '@types/react': specifier: ^18.3.3 version: 18.3.3 @@ -1795,6 +1801,9 @@ packages: '@types/prop-types@15.7.12': resolution: {integrity: sha512-5zvhXYtRNRluoE/jAp4GVsSduVUzNWKkOZrCDBWYtE7biZywwdC2AcEzg+cSMLFRfVgeAFqpfNabiPjxFddV1Q==} + '@types/pubsub-js@1.8.6': + resolution: {integrity: sha512-Kwug5cwV0paUDm/NfwDx1sp9xI0bGIvmWJjJWCU8NngkCCMt3EIC7oPDvb6fV7BR8kPpFyyBu4D11bda/2MdPA==} + '@types/react-dom@18.3.0': resolution: {integrity: sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg==} @@ -3585,6 +3594,9 @@ packages: proxy-from-env@1.1.0: resolution: {integrity: sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==} + pubsub-js@1.9.4: + resolution: {integrity: sha512-hJYpaDvPH4w8ZX/0Fdf9ma1AwRgU353GfbaVfPjfJQf1KxZ2iHaHl3fAUw1qlJIR5dr4F3RzjGaWohYUEyoh7A==} + pump@3.0.0: resolution: {integrity: sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==} @@ -6848,6 +6860,8 @@ snapshots: '@types/prop-types@15.7.12': {} + '@types/pubsub-js@1.8.6': {} + '@types/react-dom@18.3.0': dependencies: '@types/react': 18.3.3 @@ -8929,6 +8943,8 @@ snapshots: proxy-from-env@1.1.0: {} + pubsub-js@1.9.4: {} + pump@3.0.0: dependencies: end-of-stream: 1.4.4 diff --git a/src/main/core/mihomoApi.ts b/src/main/core/mihomoApi.ts index 65d3641..7270fc1 100644 --- a/src/main/core/mihomoApi.ts +++ b/src/main/core/mihomoApi.ts @@ -1,5 +1,5 @@ import axios, { AxiosInstance } from 'axios' -import { getControledMihomoConfig } from '../config' +import { getAppConfig, getControledMihomoConfig } from '../config' import WebSocket from 'ws' import { window } from '..' @@ -59,6 +59,19 @@ export const mihomoChangeProxy = async (group: string, proxy: string): Promise => { + const appConfig = getAppConfig() + const { delayTestUrl, delayTestTimeout } = appConfig + const instance = await getAxios() + return instance.get(`/proxies/${encodeURIComponent(proxy)}/delay`, { + params: { + url: url || delayTestUrl || 'https://www.gstatic.com/generate_204', + timeout: delayTestTimeout || 5000 + }, + timeout: delayTestTimeout || 5000 + }) +} + export const startMihomoTraffic = (): void => { mihomoTraffic() } diff --git a/src/main/utils/cmds.ts b/src/main/utils/cmds.ts index dfdd9b3..1295151 100644 --- a/src/main/utils/cmds.ts +++ b/src/main/utils/cmds.ts @@ -4,6 +4,7 @@ import { mihomoConfig, mihomoConnections, mihomoProxies, + mihomoProxyDelay, mihomoRules, mihomoVersion, patchMihomoConfig, @@ -33,6 +34,7 @@ export function registerIpcMainHandlers(): void { ipcMain.handle('mihomoRules', mihomoRules) ipcMain.handle('mihomoProxies', () => mihomoProxies()) ipcMain.handle('mihomoChangeProxy', (_e, group, proxy) => mihomoChangeProxy(group, proxy)) + ipcMain.handle('mihomoProxyDelay', (_e, proxy, url) => mihomoProxyDelay(proxy, url)) ipcMain.handle('startMihomoLogs', startMihomoLogs) ipcMain.handle('stopMihomoLogs', () => stopMihomoLogs()) ipcMain.handle('patchMihomoConfig', async (_e, patch) => await patchMihomoConfig(patch)) diff --git a/src/renderer/src/components/proxies/proxy-item.tsx b/src/renderer/src/components/proxies/proxy-item.tsx index 902466c..b3ef264 100644 --- a/src/renderer/src/components/proxies/proxy-item.tsx +++ b/src/renderer/src/components/proxies/proxy-item.tsx @@ -1,14 +1,65 @@ -import { Card, CardBody, Divider } from '@nextui-org/react' -import React from 'react' +import { Button, Card, CardBody, Divider } from '@nextui-org/react' +import { useAppConfig } from '@renderer/hooks/use-config' +import PubSub from 'pubsub-js' +import React, { useEffect, useState } from 'react' interface Props { + onProxyDelay: (proxy: string) => Promise + proxyDisplayMode: 'simple' | 'full' proxy: IMihomoProxy | IMihomoGroup + group: string onSelect: (proxy: string) => void selected: boolean } const ProxyItem: React.FC = (props) => { - const { proxy, selected, onSelect } = props + const { proxyDisplayMode, group, proxy, selected, onSelect, onProxyDelay } = props + const { appConfig } = useAppConfig() + const { delayTestTimeout = 5000 } = appConfig || {} + const [delay, setDelay] = useState(() => { + if (proxy.history.length > 0) { + return proxy.history[0].delay + } + return 0 + }) + const [loading, setLoading] = useState(false) + + function delayColor(delay: number): 'primary' | 'success' | 'warning' | 'danger' { + if (delay < 0) return 'danger' + if (delay === 0) return 'primary' + if (delay < 500) return 'success' + if (delay < delayTestTimeout) return 'warning' + return 'danger' + } + + function delayText(delay: number): string { + if (delay < 0) return 'Error' + if (delay === 0) return 'Delay' + if (delay < delayTestTimeout) return delay.toString() + return 'Timeout' + } + + const onDelay = (): void => { + setLoading(true) + onProxyDelay(proxy.name).then( + (delay) => { + setDelay(delay.delay || delayTestTimeout + 1) + setLoading(false) + }, + () => { + setDelay(-1) + setLoading(false) + } + ) + } + + useEffect(() => { + const token = PubSub.subscribe(`${group}-delay`, onDelay) + + return (): void => { + PubSub.unsubscribe(token) + } + }, []) return ( <> @@ -16,13 +67,26 @@ const ProxyItem: React.FC = (props) => { onPress={() => onSelect(proxy.name)} isPressable fullWidth - className={`my-1 ${selected ? 'bg-primary' : ''}`} + className={`my-1 ${selected ? 'bg-primary/30' : ''}`} radius="sm" >
-
{proxy.name}
-
{proxy.history.length > 0 && proxy.history[0].delay}
+
+
{proxy.name}
+ {proxyDisplayMode === 'full' && ( +
{proxy.type}
+ )} +
+
diff --git a/src/renderer/src/components/proxies/proxy-list.tsx b/src/renderer/src/components/proxies/proxy-list.tsx index 4469c6f..59e97c5 100644 --- a/src/renderer/src/components/proxies/proxy-list.tsx +++ b/src/renderer/src/components/proxies/proxy-list.tsx @@ -3,13 +3,16 @@ import { Virtuoso } from 'react-virtuoso' import ProxyItem from './proxy-item' interface Props { + onProxyDelay: (proxy: string) => Promise onChangeProxy: (proxy: string) => void + proxyDisplayMode: 'simple' | 'full' proxies: (IMihomoProxy | IMihomoGroup)[] + group: string now: string } const ProxyList: React.FC = (props) => { - const { onChangeProxy, proxies, now } = props + const { proxyDisplayMode, onProxyDelay, onChangeProxy, proxies, group, now } = props return ( = (props) => { increaseViewportBy={100} itemContent={(index) => ( )} diff --git a/src/renderer/src/components/sider/proxy-card.tsx b/src/renderer/src/components/sider/proxy-card.tsx index be6a0f7..d4ef4bb 100644 --- a/src/renderer/src/components/sider/proxy-card.tsx +++ b/src/renderer/src/components/sider/proxy-card.tsx @@ -1,11 +1,14 @@ -import { Button, Card, CardBody, CardFooter } from '@nextui-org/react' -import { SiSpeedtest } from 'react-icons/si' +import { Button, Card, CardBody, CardFooter, Chip } from '@nextui-org/react' +import { mihomoProxies } from '@renderer/utils/ipc' +import { SiNginxproxymanager } from 'react-icons/si' import { useLocation, useNavigate } from 'react-router-dom' +import useSWR from 'swr' const ProxyCard: React.FC = () => { const navigate = useNavigate() const location = useLocation() const match = location.pathname.includes('/proxies') + const { data: proxies = { proxies: {} } } = useSWR('mihomoProxies', mihomoProxies) return ( { isPressable onPress={() => navigate('/proxies')} > - -
-

节点名称

- + + {Object.keys(proxies.proxies).length ?? 0} +
- 二级节点 +

代理组

) diff --git a/src/renderer/src/pages/proxies.tsx b/src/renderer/src/pages/proxies.tsx index 5215db5..4549088 100644 --- a/src/renderer/src/pages/proxies.tsx +++ b/src/renderer/src/pages/proxies.tsx @@ -1,12 +1,18 @@ -import { Accordion, AccordionItem, Avatar } from '@nextui-org/react' +import { Accordion, AccordionItem, Avatar, Button } from '@nextui-org/react' import BasePage from '@renderer/components/base/base-page' import ProxyList from '@renderer/components/proxies/proxy-list' -import { mihomoChangeProxy, mihomoProxies } from '@renderer/utils/ipc' +import { useAppConfig } from '@renderer/hooks/use-config' +import { MdOutlineSpeed } from 'react-icons/md' +import { mihomoChangeProxy, mihomoProxies, mihomoProxyDelay } from '@renderer/utils/ipc' +import { CgDetailsLess, CgDetailsMore } from 'react-icons/cg' import { useEffect, useMemo } from 'react' +import PubSub from 'pubsub-js' import useSWR from 'swr' const Proxies: React.FC = () => { const { data: proxies, mutate } = useSWR('mihomoProxies', mihomoProxies) + const { appConfig, patchAppConfig } = useAppConfig() + const { proxyDisplayMode = 'simple' } = appConfig || {} const groups = useMemo(() => { const groups: IMihomoGroup[] = [] @@ -44,16 +50,60 @@ const Proxies: React.FC = () => { }) } + const onProxyDelay = async (proxy: string, url?: string): Promise => { + return await mihomoProxyDelay(proxy, url) + } + useEffect(() => {}, []) return ( - + { + patchAppConfig({ proxyDisplayMode: proxyDisplayMode === 'simple' ? 'full' : 'simple' }) + }} + > + {proxyDisplayMode === 'simple' ? ( + + ) : ( + + )} + + } + > {groups.map((group) => { return ( +
{group.name}
+ + + } + subtitle={ + proxyDisplayMode === 'full' && ( +
+ {group.type} +   + {group.now} +
+ ) + } + classNames={{ title: 'select-none', base: 'px-2', content: 'pt-2', trigger: 'py-2' }} startContent={ group.icon.length > 0 ? ( @@ -61,8 +111,11 @@ const Proxies: React.FC = () => { } > onProxyDelay(proxy, group.testUrl)} onChangeProxy={(proxy) => onChangeProxy(group.name, proxy)} + proxyDisplayMode={proxyDisplayMode} proxies={groupProxies[group.name]} + group={group.name} now={group.now} />
diff --git a/src/renderer/src/pages/settings.tsx b/src/renderer/src/pages/settings.tsx index f306607..af6e15a 100644 --- a/src/renderer/src/pages/settings.tsx +++ b/src/renderer/src/pages/settings.tsx @@ -1,4 +1,4 @@ -import { Button, Switch } from '@nextui-org/react' +import { Button, Input, Switch } from '@nextui-org/react' import BasePage from '@renderer/components/base/base-page' import SettingCard from '@renderer/components/base/base-setting-card' import SettingItem from '@renderer/components/base/base-setting-item' @@ -15,7 +15,7 @@ const Settings: React.FC = () => { }) const { appConfig, patchAppConfig } = useAppConfig() - const { silentStart = false } = appConfig || {} + const { silentStart = false, delayTestUrl, delayTestTimeout } = appConfig || {} return ( { /> + + + { + patchAppConfig({ delayTestUrl: v }) + }} + > + + + { + patchAppConfig({ delayTestTimeout: parseInt(v) }) + }} + > + + ) } diff --git a/src/renderer/src/pages/syspeoxy.tsx b/src/renderer/src/pages/syspeoxy.tsx index 7c217a1..e350c9e 100644 --- a/src/renderer/src/pages/syspeoxy.tsx +++ b/src/renderer/src/pages/syspeoxy.tsx @@ -57,6 +57,7 @@ const Sysproxy: React.FC = () => { size="sm" className="w-[50%]" value={values.host} + spellCheck={false} placeholder="默认127.0.0.1若无特殊需求请勿修改" onValueChange={(v) => { setValues({ ...values, host: v }) diff --git a/src/renderer/src/utils/ipc.ts b/src/renderer/src/utils/ipc.ts index 08e0a76..f9aa14a 100644 --- a/src/renderer/src/utils/ipc.ts +++ b/src/renderer/src/utils/ipc.ts @@ -22,6 +22,10 @@ export async function mihomoChangeProxy(group: string, proxy: string): Promise { + return await window.electron.ipcRenderer.invoke('mihomoProxyDelay', proxy, url) +} + export async function startMihomoLogs(): Promise { return await window.electron.ipcRenderer.invoke('startMihomoLogs') } diff --git a/src/shared/types.d.ts b/src/shared/types.d.ts index d193991..1b597e7 100644 --- a/src/shared/types.d.ts +++ b/src/shared/types.d.ts @@ -77,6 +77,11 @@ interface IMihomoHistory { delay: number } +interface IMihomoDelay { + delay?: number + message?: string +} + interface IMihomoProxy { alive: boolean extra: Record @@ -93,6 +98,7 @@ interface IMihomoGroup { alive: boolean all: string[] extra: Record + testUrl?: string hidden: boolean history: IMihomoHistory[] icon: string @@ -118,9 +124,12 @@ interface ISysProxyConfig { interface IAppConfig { core: 'mihomo' | 'mihomo-alpha' + proxyDisplayMode: 'simple' | 'full' silentStart: boolean sysProxy: ISysProxyConfig userAgent?: string + delayTestUrl?: string + delayTestTimeout?: number } interface IMihomoTunConfig {