mirror of
https://gh.catmak.name/https://github.com/mihomo-party-org/mihomo-party
synced 2026-04-13 08:00:30 +08:00
Compare commits
2 Commits
2698f36868
...
0bedac12ed
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0bedac12ed | ||
|
|
a5c59ce689 |
@ -250,7 +250,7 @@ export async function createProfile(item: Partial<IProfileItem>): Promise<IProfi
|
|||||||
authToken: item.authToken,
|
authToken: item.authToken,
|
||||||
userAgent: item.userAgent,
|
userAgent: item.userAgent,
|
||||||
updated: new Date().getTime(),
|
updated: new Date().getTime(),
|
||||||
updateTimeout: item.updateTimeout || 5
|
updateTimeout: item.updateTimeout
|
||||||
}
|
}
|
||||||
|
|
||||||
// Local
|
// Local
|
||||||
@ -264,7 +264,10 @@ export async function createProfile(item: Partial<IProfileItem>): Promise<IProfi
|
|||||||
|
|
||||||
const { userAgent, subscriptionTimeout = 30000 } = await getAppConfig()
|
const { userAgent, subscriptionTimeout = 30000 } = await getAppConfig()
|
||||||
const { 'mixed-port': mixedPort = 7890 } = await getControledMihomoConfig()
|
const { 'mixed-port': mixedPort = 7890 } = await getControledMihomoConfig()
|
||||||
const userItemTimeoutMs = (newItem.updateTimeout || 5) * 1000
|
const userItemTimeoutMs =
|
||||||
|
typeof newItem.updateTimeout === 'number' && newItem.updateTimeout > 0
|
||||||
|
? newItem.updateTimeout * 1000
|
||||||
|
: subscriptionTimeout
|
||||||
|
|
||||||
const baseOptions: Omit<FetchOptions, 'useProxy' | 'timeout'> = {
|
const baseOptions: Omit<FetchOptions, 'useProxy' | 'timeout'> = {
|
||||||
url: item.url,
|
url: item.url,
|
||||||
@ -454,7 +457,7 @@ export async function convertMrsRuleset(filePath: string, behavior: string): Pro
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
// 使用 mihomo convert-ruleset 命令转换 MRS 文件为 text 格式
|
// 使用 mihomo convert-ruleset 命令转换 MRS 文件为 text 格式
|
||||||
// 命令格式: mihomo convert-ruleset <behavior> <format> <source>
|
// 命令格式:mihomo convert-ruleset <behavior> <format> <source>
|
||||||
await execAsync(`"${corePath}" convert-ruleset ${behavior} mrs "${fullPath}" "${tempFilePath}"`)
|
await execAsync(`"${corePath}" convert-ruleset ${behavior} mrs "${fullPath}" "${tempFilePath}"`)
|
||||||
const content = await readFile(tempFilePath, 'utf-8')
|
const content = await readFile(tempFilePath, 'utf-8')
|
||||||
await unlink(tempFilePath)
|
await unlink(tempFilePath)
|
||||||
|
|||||||
@ -1,30 +1,6 @@
|
|||||||
import { execSync } from 'child_process'
|
import { execSync } from 'child_process'
|
||||||
import { electronApp, optimizer } from '@electron-toolkit/utils'
|
import { electronApp, optimizer } from '@electron-toolkit/utils'
|
||||||
import { app, dialog } from 'electron'
|
import { app, dialog } from 'electron'
|
||||||
|
|
||||||
if (process.platform === 'win32') {
|
|
||||||
try {
|
|
||||||
const stdout = execSync('powershell -NoProfile -Command "$PSVersionTable.PSVersion.Major"', {
|
|
||||||
encoding: 'utf8',
|
|
||||||
timeout: 5000
|
|
||||||
})
|
|
||||||
const major = parseInt(stdout.trim(), 10)
|
|
||||||
if (!isNaN(major) && major < 5) {
|
|
||||||
const isZh = Intl.DateTimeFormat().resolvedOptions().locale?.startsWith('zh')
|
|
||||||
const title = isZh ? '需要更新 PowerShell' : 'PowerShell Update Required'
|
|
||||||
const message = isZh
|
|
||||||
? `检测到您的 PowerShell 版本为 ${major}.x,部分功能需要 PowerShell 5.1 才能正常运行。\\n\\n请访问 Microsoft 官网下载并安装 Windows Management Framework 5.1。`
|
|
||||||
: `Detected PowerShell version ${major}.x. Some features require PowerShell 5.1.\\n\\nPlease install Windows Management Framework 5.1 from the Microsoft website.`
|
|
||||||
execSync(
|
|
||||||
`mshta "javascript:var sh=new ActiveXObject('WScript.Shell');sh.Popup('${message}',0,'${title}',48);close()"`,
|
|
||||||
{ timeout: 60000 }
|
|
||||||
)
|
|
||||||
process.exit(0)
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
}
|
|
||||||
import i18next from 'i18next'
|
import i18next from 'i18next'
|
||||||
import { initI18n } from '../shared/i18n'
|
import { initI18n } from '../shared/i18n'
|
||||||
import { registerIpcMainHandlers } from './utils/ipc'
|
import { registerIpcMainHandlers } from './utils/ipc'
|
||||||
@ -61,6 +37,30 @@ import {
|
|||||||
getSystemLanguage
|
getSystemLanguage
|
||||||
} from './lifecycle'
|
} from './lifecycle'
|
||||||
|
|
||||||
|
if (process.platform === 'win32') {
|
||||||
|
try {
|
||||||
|
const stdout = execSync('powershell -NoProfile -Command "$PSVersionTable.PSVersion.Major"', {
|
||||||
|
encoding: 'utf8',
|
||||||
|
timeout: 5000
|
||||||
|
})
|
||||||
|
const major = parseInt(stdout.trim(), 10)
|
||||||
|
if (!isNaN(major) && major < 5) {
|
||||||
|
const isZh = Intl.DateTimeFormat().resolvedOptions().locale?.startsWith('zh')
|
||||||
|
const title = isZh ? '需要更新 PowerShell' : 'PowerShell Update Required'
|
||||||
|
const message = isZh
|
||||||
|
? `检测到您的 PowerShell 版本为 ${major}.x,部分功能需要 PowerShell 5.1 才能正常运行。\\n\\n请访问 Microsoft 官网下载并安装 Windows Management Framework 5.1。`
|
||||||
|
: `Detected PowerShell version ${major}.x. Some features require PowerShell 5.1.\\n\\nPlease install Windows Management Framework 5.1 from the Microsoft website.`
|
||||||
|
execSync(
|
||||||
|
`mshta "javascript:var sh=new ActiveXObject('WScript.Shell');sh.Popup('${message}',0,'${title}',48);close()"`,
|
||||||
|
{ timeout: 60000 }
|
||||||
|
)
|
||||||
|
process.exit(0)
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const mainLogger = createLogger('Main')
|
const mainLogger = createLogger('Main')
|
||||||
|
|
||||||
export { mainWindow, showMainWindow, triggerMainWindow, closeMainWindow }
|
export { mainWindow, showMainWindow, triggerMainWindow, closeMainWindow }
|
||||||
|
|||||||
@ -117,19 +117,17 @@ export async function downloadAndInstallUpdate(version: string): Promise<void> {
|
|||||||
{ proxy, responseType: 'text' }
|
{ proxy, responseType: 'text' }
|
||||||
)
|
)
|
||||||
const expectedHash = (sha256Res.data as string).trim().split(/\s+/)[0]
|
const expectedHash = (sha256Res.data as string).trim().split(/\s+/)[0]
|
||||||
const res = await tryDownload(
|
const res = await tryDownload(buildDownloadUrls(`${githubBase}${file}`, githubProxy), {
|
||||||
buildDownloadUrls(`${githubBase}${file}`, githubProxy),
|
responseType: 'arraybuffer',
|
||||||
{
|
timeout: 0,
|
||||||
responseType: 'arraybuffer',
|
proxy,
|
||||||
timeout: 0,
|
headers: { 'Content-Type': 'application/octet-stream' },
|
||||||
proxy,
|
onProgress: (loaded: number, total: number) => {
|
||||||
headers: { 'Content-Type': 'application/octet-stream' },
|
mainWindow?.webContents.send('updateDownloadProgress', {
|
||||||
onProgress: (loaded: number, total: number) => {
|
status: 'downloading',
|
||||||
mainWindow?.webContents.send('updateDownloadProgress', {
|
percent: Math.round((loaded / total) * 100)
|
||||||
status: 'downloading',
|
})
|
||||||
percent: Math.round((loaded / total) * 100)
|
}
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
mainWindow?.webContents.send('updateDownloadProgress', { status: 'verifying' })
|
mainWindow?.webContents.send('updateDownloadProgress', { status: 'verifying' })
|
||||||
const fileBuffer = Buffer.from(res.data as ArrayBuffer)
|
const fileBuffer = Buffer.from(res.data as ArrayBuffer)
|
||||||
|
|||||||
@ -104,8 +104,7 @@ export async function webdavBackup(): Promise<boolean> {
|
|||||||
|
|
||||||
if (webdavMaxBackups > 0) {
|
if (webdavMaxBackups > 0) {
|
||||||
try {
|
try {
|
||||||
const files = await client.getDirectoryContents(webdavDir, { glob: '*.zip' })
|
const fileList = await client.getDirectoryContents(webdavDir, { glob: '*.zip' })
|
||||||
const fileList = Array.isArray(files) ? files : files.data
|
|
||||||
|
|
||||||
const currentPlatformFiles = fileList.filter((file) => {
|
const currentPlatformFiles = fileList.filter((file) => {
|
||||||
return file.basename.startsWith(`${process.platform}_`)
|
return file.basename.startsWith(`${process.platform}_`)
|
||||||
@ -147,11 +146,7 @@ export async function webdavRestore(filename: string): Promise<void> {
|
|||||||
export async function listWebdavBackups(): Promise<string[]> {
|
export async function listWebdavBackups(): Promise<string[]> {
|
||||||
const { client, webdavDir } = await getWebDAVClient()
|
const { client, webdavDir } = await getWebDAVClient()
|
||||||
const files = await client.getDirectoryContents(webdavDir, { glob: '*.zip' })
|
const files = await client.getDirectoryContents(webdavDir, { glob: '*.zip' })
|
||||||
if (Array.isArray(files)) {
|
return files.map((file) => file.basename)
|
||||||
return files.map((file) => file.basename)
|
|
||||||
} else {
|
|
||||||
return files.data.map((file) => file.basename)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function webdavDelete(filename: string): Promise<void> {
|
export async function webdavDelete(filename: string): Promise<void> {
|
||||||
|
|||||||
@ -6,10 +6,10 @@ import { join } from 'path'
|
|||||||
import { createGunzip } from 'zlib'
|
import { createGunzip } from 'zlib'
|
||||||
import AdmZip from 'adm-zip'
|
import AdmZip from 'adm-zip'
|
||||||
import { stopCore } from '../core/manager'
|
import { stopCore } from '../core/manager'
|
||||||
|
import { getAppConfig } from '../config'
|
||||||
import { mihomoCoreDir } from './dirs'
|
import { mihomoCoreDir } from './dirs'
|
||||||
import * as chromeRequest from './chromeRequest'
|
import * as chromeRequest from './chromeRequest'
|
||||||
import { createLogger } from './logger'
|
import { createLogger } from './logger'
|
||||||
import { getAppConfig } from '../config'
|
|
||||||
|
|
||||||
const log = createLogger('GitHub')
|
const log = createLogger('GitHub')
|
||||||
|
|
||||||
|
|||||||
@ -5,6 +5,8 @@ import { useTranslation } from 'react-i18next'
|
|||||||
|
|
||||||
const ErrorFallback = ({ error }: FallbackProps): React.ReactElement => {
|
const ErrorFallback = ({ error }: FallbackProps): React.ReactElement => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||||
|
const errorStack = error instanceof Error ? (error.stack ?? '') : ''
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-4">
|
<div className="p-4">
|
||||||
@ -33,17 +35,17 @@ const ErrorFallback = ({ error }: FallbackProps): React.ReactElement => {
|
|||||||
variant="flat"
|
variant="flat"
|
||||||
className="ml-2"
|
className="ml-2"
|
||||||
onPress={() =>
|
onPress={() =>
|
||||||
navigator.clipboard.writeText('```\n' + error.message + '\n' + error.stack + '\n```')
|
navigator.clipboard.writeText('```\n' + errorMessage + '\n' + errorStack + '\n```')
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{t('common.error.copyErrorMessage')}
|
{t('common.error.copyErrorMessage')}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<p className="my-2">{error.message}</p>
|
<p className="my-2">{errorMessage}</p>
|
||||||
|
|
||||||
<details title="Error Stack">
|
<details title="Error Stack">
|
||||||
<summary>Error Stack</summary>
|
<summary>Error Stack</summary>
|
||||||
<pre>{error.stack}</pre>
|
<pre>{errorStack}</pre>
|
||||||
</details>
|
</details>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
|
|||||||
@ -1,8 +1,10 @@
|
|||||||
import React, { useEffect, useRef, useState, useMemo, useCallback } from 'react'
|
import React, { useEffect, useRef, useState, useMemo, useCallback } from 'react'
|
||||||
import * as d3 from 'd3'
|
import * as d3 from 'd3'
|
||||||
import { Button } from '@heroui/react'
|
import { Button } from '@heroui/react'
|
||||||
import { IoPauseOutline, IoPlayOutline, IoGitNetworkOutline } from 'react-icons/io5'
|
|
||||||
import {
|
import {
|
||||||
|
IoPauseOutline,
|
||||||
|
IoPlayOutline,
|
||||||
|
IoGitNetworkOutline,
|
||||||
IoDesktopOutline,
|
IoDesktopOutline,
|
||||||
IoServerOutline,
|
IoServerOutline,
|
||||||
IoFunnelOutline
|
IoFunnelOutline
|
||||||
@ -101,7 +103,8 @@ function buildHierarchy(
|
|||||||
proxies: new Map()
|
proxies: new Map()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
const groupEntry = groupsMap.get(group)!
|
const groupEntry = groupsMap.get(group)
|
||||||
|
if (!groupEntry) continue
|
||||||
groupEntry.data.connections++
|
groupEntry.data.connections++
|
||||||
groupEntry.data.traffic += traffic
|
groupEntry.data.traffic += traffic
|
||||||
|
|
||||||
@ -117,7 +120,8 @@ function buildHierarchy(
|
|||||||
rules: new Map()
|
rules: new Map()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
const proxyEntry = groupEntry.proxies.get(proxy)!
|
const proxyEntry = groupEntry.proxies.get(proxy)
|
||||||
|
if (!proxyEntry) continue
|
||||||
proxyEntry.data.connections++
|
proxyEntry.data.connections++
|
||||||
proxyEntry.data.traffic += traffic
|
proxyEntry.data.traffic += traffic
|
||||||
|
|
||||||
@ -133,7 +137,8 @@ function buildHierarchy(
|
|||||||
clients: new Map()
|
clients: new Map()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
const ruleEntry = proxyEntry.rules.get(fullRule)!
|
const ruleEntry = proxyEntry.rules.get(fullRule)
|
||||||
|
if (!ruleEntry) continue
|
||||||
ruleEntry.data.connections++
|
ruleEntry.data.connections++
|
||||||
ruleEntry.data.traffic += traffic
|
ruleEntry.data.traffic += traffic
|
||||||
|
|
||||||
@ -149,7 +154,8 @@ function buildHierarchy(
|
|||||||
ports: new Map()
|
ports: new Map()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
const clientEntry = ruleEntry.clients.get(clientIP)!
|
const clientEntry = ruleEntry.clients.get(clientIP)
|
||||||
|
if (!clientEntry) continue
|
||||||
clientEntry.data.connections++
|
clientEntry.data.connections++
|
||||||
clientEntry.data.traffic += traffic
|
clientEntry.data.traffic += traffic
|
||||||
|
|
||||||
@ -162,7 +168,8 @@ function buildHierarchy(
|
|||||||
traffic: 0
|
traffic: 0
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
const portNode = clientEntry.ports.get(sourcePort)!
|
const portNode = clientEntry.ports.get(sourcePort)
|
||||||
|
if (!portNode) continue
|
||||||
portNode.connections++
|
portNode.connections++
|
||||||
portNode.traffic += traffic
|
portNode.traffic += traffic
|
||||||
}
|
}
|
||||||
@ -179,13 +186,16 @@ function buildHierarchy(
|
|||||||
}
|
}
|
||||||
|
|
||||||
groupsMap.forEach((groupEntry) => {
|
groupsMap.forEach((groupEntry) => {
|
||||||
const groupNode: TopologyNodeData = { ...groupEntry.data, children: [] }
|
const groupChildren: TopologyNodeData[] = []
|
||||||
|
const groupNode: TopologyNodeData = { ...groupEntry.data, children: groupChildren }
|
||||||
|
|
||||||
groupEntry.proxies.forEach((proxyEntry) => {
|
groupEntry.proxies.forEach((proxyEntry) => {
|
||||||
const proxyNode: TopologyNodeData = { ...proxyEntry.data, children: [] }
|
const proxyChildren: TopologyNodeData[] = []
|
||||||
|
const proxyNode: TopologyNodeData = { ...proxyEntry.data, children: proxyChildren }
|
||||||
|
|
||||||
proxyEntry.rules.forEach((ruleEntry) => {
|
proxyEntry.rules.forEach((ruleEntry) => {
|
||||||
const ruleNode: TopologyNodeData = { ...ruleEntry.data, children: [] }
|
const ruleChildren: TopologyNodeData[] = []
|
||||||
|
const ruleNode: TopologyNodeData = { ...ruleEntry.data, children: ruleChildren }
|
||||||
|
|
||||||
ruleEntry.clients.forEach((clientEntry) => {
|
ruleEntry.clients.forEach((clientEntry) => {
|
||||||
const portChildren = Array.from(clientEntry.ports.values())
|
const portChildren = Array.from(clientEntry.ports.values())
|
||||||
@ -193,21 +203,21 @@ function buildHierarchy(
|
|||||||
const clientNode: TopologyNodeData = isClientCollapsed
|
const clientNode: TopologyNodeData = isClientCollapsed
|
||||||
? { ...clientEntry.data, _children: portChildren, children: undefined, collapsed: true }
|
? { ...clientEntry.data, _children: portChildren, children: undefined, collapsed: true }
|
||||||
: { ...clientEntry.data, children: portChildren, collapsed: false }
|
: { ...clientEntry.data, children: portChildren, collapsed: false }
|
||||||
ruleNode.children!.push(clientNode)
|
ruleChildren.push(clientNode)
|
||||||
})
|
})
|
||||||
|
|
||||||
const isRuleCollapsed = !collapsedNodes.has(`expanded-${ruleEntry.data.id}`)
|
const isRuleCollapsed = !collapsedNodes.has(`expanded-${ruleEntry.data.id}`)
|
||||||
if (isRuleCollapsed && ruleNode.children!.length > 0) {
|
if (isRuleCollapsed && ruleChildren.length > 0) {
|
||||||
ruleNode._children = ruleNode.children
|
ruleNode._children = ruleChildren
|
||||||
ruleNode.children = undefined
|
ruleNode.children = undefined
|
||||||
ruleNode.collapsed = true
|
ruleNode.collapsed = true
|
||||||
} else {
|
} else {
|
||||||
ruleNode.collapsed = false
|
ruleNode.collapsed = false
|
||||||
}
|
}
|
||||||
proxyNode.children!.push(ruleNode)
|
proxyChildren.push(ruleNode)
|
||||||
})
|
})
|
||||||
|
|
||||||
groupNode.children!.push(applyCollapse(proxyNode))
|
groupChildren.push(applyCollapse(proxyNode))
|
||||||
})
|
})
|
||||||
|
|
||||||
rootChildren.push(applyCollapse(groupNode))
|
rootChildren.push(applyCollapse(groupNode))
|
||||||
@ -293,29 +303,26 @@ const NetworkTopologyCard: React.FC = () => {
|
|||||||
|
|
||||||
// Toggle collapse
|
// Toggle collapse
|
||||||
const toggleCollapseRef = useRef<(nodeId: string, isCollapsed: boolean) => void>(() => {})
|
const toggleCollapseRef = useRef<(nodeId: string, isCollapsed: boolean) => void>(() => {})
|
||||||
toggleCollapseRef.current = useCallback(
|
toggleCollapseRef.current = useCallback((nodeId: string, isCurrentlyCollapsed: boolean) => {
|
||||||
(nodeId: string, isCurrentlyCollapsed: boolean) => {
|
const expandedKey = `expanded-${nodeId}`
|
||||||
const expandedKey = `expanded-${nodeId}`
|
setCollapsedNodes((prev) => {
|
||||||
setCollapsedNodes((prev) => {
|
const next = new Set(prev)
|
||||||
const next = new Set(prev)
|
if (isCurrentlyCollapsed) {
|
||||||
if (isCurrentlyCollapsed) {
|
if (nodeId.startsWith('rule-') || nodeId.startsWith('client-')) {
|
||||||
if (nodeId.startsWith('rule-') || nodeId.startsWith('client-')) {
|
next.add(expandedKey)
|
||||||
next.add(expandedKey)
|
|
||||||
} else {
|
|
||||||
next.delete(nodeId)
|
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
if (nodeId.startsWith('rule-') || nodeId.startsWith('client-')) {
|
next.delete(nodeId)
|
||||||
next.delete(expandedKey)
|
|
||||||
} else {
|
|
||||||
next.add(nodeId)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return next
|
} else {
|
||||||
})
|
if (nodeId.startsWith('rule-') || nodeId.startsWith('client-')) {
|
||||||
},
|
next.delete(expandedKey)
|
||||||
[]
|
} else {
|
||||||
)
|
next.add(nodeId)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return next
|
||||||
|
})
|
||||||
|
}, [])
|
||||||
|
|
||||||
// D3 render
|
// D3 render
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@ -357,8 +364,7 @@ const NetworkTopologyCard: React.FC = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const getNodeWidth = (d: d3.HierarchyNode<TopologyNodeData>) =>
|
const getNodeWidth = (d: d3.HierarchyNode<TopologyNodeData>) => nodeWidths.get(d.data.id) ?? 80
|
||||||
nodeWidths.get(d.data.id) ?? 80
|
|
||||||
|
|
||||||
// Max width per depth
|
// Max width per depth
|
||||||
const maxWidthPerLevel = new Map<number, number>()
|
const maxWidthPerLevel = new Map<number, number>()
|
||||||
@ -487,7 +493,7 @@ const NetworkTopologyCard: React.FC = () => {
|
|||||||
.filter((d) =>
|
.filter((d) =>
|
||||||
Boolean(
|
Boolean(
|
||||||
(d.data.children && d.data.children.length > 0) ||
|
(d.data.children && d.data.children.length > 0) ||
|
||||||
(d.data._children && d.data._children.length > 0)
|
(d.data._children && d.data._children.length > 0)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
.append('text')
|
.append('text')
|
||||||
@ -513,8 +519,7 @@ const NetworkTopologyCard: React.FC = () => {
|
|||||||
nodes
|
nodes
|
||||||
.append('title')
|
.append('title')
|
||||||
.text(
|
.text(
|
||||||
(d) =>
|
(d) => `${d.data.name}\n${d.data.connections} connections\n${calcTraffic(d.data.traffic)}`
|
||||||
`${d.data.name}\n${d.data.connections} connections\n${calcTraffic(d.data.traffic)}`
|
|
||||||
)
|
)
|
||||||
}, [hierarchyData, resolvedTheme])
|
}, [hierarchyData, resolvedTheme])
|
||||||
|
|
||||||
@ -555,13 +560,21 @@ const NetworkTopologyCard: React.FC = () => {
|
|||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{/* Stats */}
|
{/* Stats */}
|
||||||
<div className="hidden flex-wrap gap-x-2 text-[12px] text-foreground/50 sm:flex">
|
<div className="hidden flex-wrap gap-x-2 text-[12px] text-foreground/50 sm:flex">
|
||||||
<span>{stats.clientCount} {t('network.topology.clients')}</span>
|
<span>
|
||||||
|
{stats.clientCount} {t('network.topology.clients')}
|
||||||
|
</span>
|
||||||
<span>·</span>
|
<span>·</span>
|
||||||
<span>{stats.ruleCount} {t('network.topology.rules')}</span>
|
<span>
|
||||||
|
{stats.ruleCount} {t('network.topology.rules')}
|
||||||
|
</span>
|
||||||
<span>·</span>
|
<span>·</span>
|
||||||
<span>{stats.groupCount} {t('network.topology.groups')}</span>
|
<span>
|
||||||
|
{stats.groupCount} {t('network.topology.groups')}
|
||||||
|
</span>
|
||||||
<span>·</span>
|
<span>·</span>
|
||||||
<span>{stats.proxyCount} {t('network.topology.nodes')}</span>
|
<span>
|
||||||
|
{stats.proxyCount} {t('network.topology.nodes')}
|
||||||
|
</span>
|
||||||
<span>·</span>
|
<span>·</span>
|
||||||
<span>{calcTraffic(stats.totalTraffic)}</span>
|
<span>{calcTraffic(stats.totalTraffic)}</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@ -33,8 +33,7 @@ const EditInfoModal: React.FC<Props> = (props) => {
|
|||||||
const { overrideConfig } = useOverrideConfig()
|
const { overrideConfig } = useOverrideConfig()
|
||||||
const { items: overrideItems = [] } = overrideConfig || {}
|
const { items: overrideItems = [] } = overrideConfig || {}
|
||||||
const [values, setValues] = useState({
|
const [values, setValues] = useState({
|
||||||
...item,
|
...item
|
||||||
updateTimeout: item.updateTimeout ?? 5
|
|
||||||
})
|
})
|
||||||
const inputWidth = 'w-[400px] md:w-[400px] lg:w-[600px] xl:w-[800px]'
|
const inputWidth = 'w-[400px] md:w-[400px] lg:w-[600px] xl:w-[800px]'
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
@ -43,7 +42,6 @@ const EditInfoModal: React.FC<Props> = (props) => {
|
|||||||
try {
|
try {
|
||||||
const updatedItem = {
|
const updatedItem = {
|
||||||
...values,
|
...values,
|
||||||
updateTimeout: values.updateTimeout ?? 5,
|
|
||||||
override: values.override?.filter(
|
override: values.override?.filter(
|
||||||
(i) =>
|
(i) =>
|
||||||
overrideItems.find((t) => t.id === i) && !overrideItems.find((t) => t.id === i)?.global
|
overrideItems.find((t) => t.id === i) && !overrideItems.find((t) => t.id === i)?.global
|
||||||
@ -215,7 +213,7 @@ const EditInfoModal: React.FC<Props> = (props) => {
|
|||||||
value={values.updateTimeout?.toString() ?? ''}
|
value={values.updateTimeout?.toString() ?? ''}
|
||||||
onValueChange={(v) => {
|
onValueChange={(v) => {
|
||||||
if (v === '') {
|
if (v === '') {
|
||||||
setValues({ ...values, updateTimeout: undefined as unknown as number })
|
setValues({ ...values, updateTimeout: undefined })
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (/^\d+$/.test(v)) {
|
if (/^\d+$/.test(v)) {
|
||||||
|
|||||||
@ -1346,7 +1346,7 @@ const EditRulesModal: React.FC<Props> = (props) => {
|
|||||||
const originalIndex = ruleIndexMap.get(rule) ?? -1
|
const originalIndex = ruleIndexMap.get(rule) ?? -1
|
||||||
return `${originalIndex}-${index}`
|
return `${originalIndex}-${index}`
|
||||||
}}
|
}}
|
||||||
itemContent={(index, rule) => {
|
itemContent={(_index, rule) => {
|
||||||
const originalIndex = ruleIndexMap.get(rule) ?? -1
|
const originalIndex = ruleIndexMap.get(rule) ?? -1
|
||||||
const isDeleted = deletedRules.has(originalIndex)
|
const isDeleted = deletedRules.has(originalIndex)
|
||||||
const isPrependOrAppend =
|
const isPrependOrAppend =
|
||||||
|
|||||||
@ -59,34 +59,34 @@ const RuleItem: React.FC<RuleItemProps> = (props) => {
|
|||||||
</Chip>
|
</Chip>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{extra && (() => {
|
{extra &&
|
||||||
const total = extra.hitCount + extra.missCount
|
(() => {
|
||||||
const rate = total > 0 ? (extra.hitCount / total) * 100 : 0
|
const total = extra.hitCount + extra.missCount
|
||||||
return (
|
const rate = total > 0 ? (extra.hitCount / total) * 100 : 0
|
||||||
<>
|
return (
|
||||||
<div className="flex items-center gap-3 text-xs shrink-0">
|
<>
|
||||||
<span className="text-foreground-500 whitespace-nowrap">
|
<div className="flex items-center gap-3 text-xs shrink-0">
|
||||||
{formatRelativeTime(extra.hitAt || extra.missAt)}
|
<span className="text-foreground-500 whitespace-nowrap">
|
||||||
</span>
|
{formatRelativeTime(extra.hitAt || extra.missAt)}
|
||||||
<span className="text-foreground-600 font-medium whitespace-nowrap">
|
</span>
|
||||||
{extra.hitCount}/{total}
|
<span className="text-foreground-600 font-medium whitespace-nowrap">
|
||||||
</span>
|
{extra.hitCount}/{total}
|
||||||
<Chip size="sm" variant="flat" color="primary" className="text-xs">
|
</span>
|
||||||
{rate.toFixed(1)}%
|
<Chip size="sm" variant="flat" color="primary" className="text-xs">
|
||||||
</Chip>
|
{rate.toFixed(1)}%
|
||||||
</div>
|
</Chip>
|
||||||
<Switch
|
</div>
|
||||||
size="sm"
|
<Switch
|
||||||
isSelected={isEnabled}
|
size="sm"
|
||||||
onValueChange={handleToggle}
|
isSelected={isEnabled}
|
||||||
aria-label="Toggle rule"
|
onValueChange={handleToggle}
|
||||||
/>
|
aria-label="Toggle rule"
|
||||||
</>
|
/>
|
||||||
)
|
</>
|
||||||
})()}
|
)
|
||||||
|
})()}
|
||||||
</div>
|
</div>
|
||||||
</CardBody>
|
</CardBody>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@ -128,30 +128,27 @@ const ConnCard: React.FC<Props> = (props) => {
|
|||||||
const transform = tf ? { x: tf.x, y: tf.y, scaleX: 1, scaleY: 1 } : null
|
const transform = tf ? { x: tf.x, y: tf.y, scaleX: 1, scaleY: 1 } : null
|
||||||
|
|
||||||
// 使用 useCallback 创建稳定的 handler 引用,通过 ref 读取 showTraffic 避免重建
|
// 使用 useCallback 创建稳定的 handler 引用,通过 ref 读取 showTraffic 避免重建
|
||||||
const handleTraffic = useCallback(
|
const handleTraffic = useCallback((_e: unknown, ...args: unknown[]) => {
|
||||||
(_e: unknown, ...args: unknown[]) => {
|
const info = args[0] as IMihomoTrafficInfo
|
||||||
const info = args[0] as IMihomoTrafficInfo
|
setUpload(info.up)
|
||||||
setUpload(info.up)
|
setDownload(info.down)
|
||||||
setDownload(info.down)
|
setSeries((prev) => {
|
||||||
setSeries((prev) => {
|
const data = [...prev]
|
||||||
const data = [...prev]
|
data.shift()
|
||||||
data.shift()
|
data.push(info.up + info.down)
|
||||||
data.push(info.up + info.down)
|
return data
|
||||||
return data
|
})
|
||||||
})
|
if (platform === 'darwin' && showTrafficRef.current) {
|
||||||
if (platform === 'darwin' && showTrafficRef.current) {
|
const up = info.up
|
||||||
const up = info.up
|
const down = info.down
|
||||||
const down = info.down
|
if (up !== currentUploadRef.current || down !== currentDownloadRef.current) {
|
||||||
if (up !== currentUploadRef.current || down !== currentDownloadRef.current) {
|
currentUploadRef.current = up
|
||||||
currentUploadRef.current = up
|
currentDownloadRef.current = down
|
||||||
currentDownloadRef.current = down
|
const png = renderTrafficIcon(up, down)
|
||||||
const png = renderTrafficIcon(up, down)
|
window.electron.ipcRenderer.send('trayIconUpdate', png, true)
|
||||||
window.electron.ipcRenderer.send('trayIconUpdate', png, true)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
[] // eslint-disable-line react-hooks/exhaustive-deps
|
}, [])
|
||||||
)
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
window.electron.ipcRenderer.on('mihomoTraffic', handleTraffic)
|
window.electron.ipcRenderer.on('mihomoTraffic', handleTraffic)
|
||||||
@ -295,7 +292,6 @@ const ConnCard: React.FC<Props> = (props) => {
|
|||||||
|
|
||||||
export default ConnCard
|
export default ConnCard
|
||||||
|
|
||||||
|
|
||||||
const trayIconBase64 = `data:image/png;base64,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`
|
const trayIconBase64 = `data:image/png;base64,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`
|
||||||
|
|
||||||
// 固定宽高,同步渲染避免闪烁
|
// 固定宽高,同步渲染避免闪烁
|
||||||
@ -317,7 +313,10 @@ function renderTrafficIcon(upload: number, download: number): string {
|
|||||||
trafficCanvas.height = ICON_H
|
trafficCanvas.height = ICON_H
|
||||||
trafficCtx = trafficCanvas.getContext('2d')
|
trafficCtx = trafficCanvas.getContext('2d')
|
||||||
}
|
}
|
||||||
const ctx = trafficCtx!
|
if (!trafficCtx) {
|
||||||
|
return trayIconBase64
|
||||||
|
}
|
||||||
|
const ctx = trafficCtx
|
||||||
ctx.clearRect(0, 0, ICON_W, ICON_H)
|
ctx.clearRect(0, 0, ICON_W, ICON_H)
|
||||||
if (trafficIconLoaded) {
|
if (trafficIconLoaded) {
|
||||||
ctx.drawImage(trafficIcon, 0, 0, ICON_H, ICON_H)
|
ctx.drawImage(trafficIcon, 0, 0, ICON_H, ICON_H)
|
||||||
|
|||||||
@ -708,7 +708,6 @@
|
|||||||
"network.topology.waiting": "Waiting for connections...",
|
"network.topology.waiting": "Waiting for connections...",
|
||||||
"network.topology.pause": "Pause",
|
"network.topology.pause": "Pause",
|
||||||
"network.topology.resume": "Resume",
|
"network.topology.resume": "Resume",
|
||||||
"guide.end.description": "Now that you understand the basic usage of the software, import your subscription and start using it. Enjoy!\nYou can also join our official <a href=\"https://t.me/mihomo_party_group\" target=\"_blank\">Telegram group</a> for the latest news.",
|
|
||||||
"settings.githubProxy": "GitHub Download Proxy",
|
"settings.githubProxy": "GitHub Download Proxy",
|
||||||
"settings.githubProxy.auto": "Auto (proxy first)",
|
"settings.githubProxy.auto": "Auto (proxy first)",
|
||||||
"settings.githubProxy.direct": "Direct"
|
"settings.githubProxy.direct": "Direct"
|
||||||
|
|||||||
@ -50,6 +50,7 @@ const Connections: React.FC = () => {
|
|||||||
const { 'find-process-mode': findProcessMode = 'always' } = controledMihomoConfig || {}
|
const { 'find-process-mode': findProcessMode = 'always' } = controledMihomoConfig || {}
|
||||||
const [filter, setFilter] = useState('')
|
const [filter, setFilter] = useState('')
|
||||||
const { appConfig, patchAppConfig } = useAppConfig()
|
const { appConfig, patchAppConfig } = useAppConfig()
|
||||||
|
const appConfigValues: Partial<IAppConfig> = appConfig ?? {}
|
||||||
const {
|
const {
|
||||||
connectionDirection = 'asc',
|
connectionDirection = 'asc',
|
||||||
connectionOrderBy = 'time',
|
connectionOrderBy = 'time',
|
||||||
@ -73,7 +74,7 @@ const Connections: React.FC = () => {
|
|||||||
connectionTableSortDirection,
|
connectionTableSortDirection,
|
||||||
displayIcon = true,
|
displayIcon = true,
|
||||||
displayAppName = true
|
displayAppName = true
|
||||||
} = appConfig || {}
|
} = appConfigValues
|
||||||
const [connectionsInfo, setConnectionsInfo] = useState<IMihomoConnectionsInfo>()
|
const [connectionsInfo, setConnectionsInfo] = useState<IMihomoConnectionsInfo>()
|
||||||
const [allConnections, setAllConnections] = useState<IMihomoConnectionDetail[]>(cachedConnections)
|
const [allConnections, setAllConnections] = useState<IMihomoConnectionDetail[]>(cachedConnections)
|
||||||
const [activeConnections, setActiveConnections] = useState<IMihomoConnectionDetail[]>([])
|
const [activeConnections, setActiveConnections] = useState<IMihomoConnectionDetail[]>([])
|
||||||
|
|||||||
@ -123,10 +123,11 @@ const DNS: React.FC = () => {
|
|||||||
|
|
||||||
const handleSubkeyChange = (type: string, domain: string, value: string, index: number): void => {
|
const handleSubkeyChange = (type: string, domain: string, value: string, index: number): void => {
|
||||||
const list = [...values[type]]
|
const list = [...values[type]]
|
||||||
const parts = value.split(',').map((s: string) => s.trim()).filter(Boolean)
|
const parts = value
|
||||||
const processedValue = type === 'hosts'
|
.split(',')
|
||||||
? parts
|
.map((s: string) => s.trim())
|
||||||
: (parts.length > 1 ? parts : value.trim())
|
.filter(Boolean)
|
||||||
|
const processedValue = type === 'hosts' ? parts : parts.length > 1 ? parts : value.trim()
|
||||||
if (domain || parts.length > 0) list[index] = { domain: domain.trim(), value: processedValue }
|
if (domain || parts.length > 0) list[index] = { domain: domain.trim(), value: processedValue }
|
||||||
else list.splice(index, 1)
|
else list.splice(index, 1)
|
||||||
setValues({ ...values, [type]: list })
|
setValues({ ...values, [type]: list })
|
||||||
|
|||||||
@ -196,10 +196,10 @@ const IPPage: React.FC = () => {
|
|||||||
|
|
||||||
const averageLatency = (() => {
|
const averageLatency = (() => {
|
||||||
const successes = LATENCY_TARGETS.map((t) => latencyResults[t.url]).filter(
|
const successes = LATENCY_TARGETS.map((t) => latencyResults[t.url]).filter(
|
||||||
(r) => r?.status === 'success' && r.latency !== null
|
(r): r is LatencyResult & { latency: number } => r?.status === 'success' && r.latency !== null
|
||||||
)
|
)
|
||||||
if (successes.length === 0) return null
|
if (successes.length === 0) return null
|
||||||
return Math.round(successes.reduce((acc, r) => acc + (r!.latency ?? 0), 0) / successes.length)
|
return Math.round(successes.reduce((acc, r) => acc + r.latency, 0) / successes.length)
|
||||||
})()
|
})()
|
||||||
|
|
||||||
const fetchIP = useCallback(
|
const fetchIP = useCallback(
|
||||||
@ -292,7 +292,9 @@ const IPPage: React.FC = () => {
|
|||||||
<div className="flex flex-col gap-2.5">
|
<div className="flex flex-col gap-2.5">
|
||||||
{/* IP 地址高亮行(负 margin 贴边) */}
|
{/* IP 地址高亮行(负 margin 贴边) */}
|
||||||
<div className="-mx-1 -mt-1 mb-1 flex items-center justify-between gap-3 rounded-lg border border-primary/20 bg-primary/8 px-2.5 py-2">
|
<div className="-mx-1 -mt-1 mb-1 flex items-center justify-between gap-3 rounded-lg border border-primary/20 bg-primary/8 px-2.5 py-2">
|
||||||
<span className="shrink-0 text-[13px] text-foreground/60">{t('network.ipAddress')}</span>
|
<span className="shrink-0 text-[13px] text-foreground/60">
|
||||||
|
{t('network.ipAddress')}
|
||||||
|
</span>
|
||||||
<div className="flex items-center gap-1.5">
|
<div className="flex items-center gap-1.5">
|
||||||
<span className="overflow-hidden text-right font-mono text-[13px] font-semibold text-primary text-ellipsis whitespace-nowrap">
|
<span className="overflow-hidden text-right font-mono text-[13px] font-semibold text-primary text-ellipsis whitespace-nowrap">
|
||||||
{hidden ? '••••••••••••••' : ipInfo.ip}
|
{hidden ? '••••••••••••••' : ipInfo.ip}
|
||||||
|
|||||||
@ -401,13 +401,14 @@ const Proxies: React.FC = () => {
|
|||||||
groupCounts,
|
groupCounts,
|
||||||
isOpen,
|
isOpen,
|
||||||
proxyDisplayMode,
|
proxyDisplayMode,
|
||||||
|
t,
|
||||||
searchValue,
|
searchValue,
|
||||||
delaying,
|
delaying,
|
||||||
cols,
|
mutate,
|
||||||
allProxies,
|
|
||||||
virtuosoRef,
|
|
||||||
t,
|
|
||||||
setIsOpen,
|
setIsOpen,
|
||||||
|
allProxies,
|
||||||
|
cols,
|
||||||
|
virtuosoRef,
|
||||||
onGroupDelay
|
onGroupDelay
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|||||||
2
src/shared/types.d.ts
vendored
2
src/shared/types.d.ts
vendored
@ -258,6 +258,8 @@ interface IAppConfig {
|
|||||||
connectionTableColumnWidths?: Record<string, number>
|
connectionTableColumnWidths?: Record<string, number>
|
||||||
connectionTableSortColumn?: string
|
connectionTableSortColumn?: string
|
||||||
connectionTableSortDirection?: 'asc' | 'desc'
|
connectionTableSortDirection?: 'asc' | 'desc'
|
||||||
|
displayIcon?: boolean
|
||||||
|
displayAppName?: boolean
|
||||||
spinFloatingIcon?: boolean
|
spinFloatingIcon?: boolean
|
||||||
disableTray?: boolean
|
disableTray?: boolean
|
||||||
swapTrayClick?: boolean
|
swapTrayClick?: boolean
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user