mirror of
https://gh.catmak.name/https://github.com/mihomo-party-org/mihomo-party
synced 2026-04-12 23:50:31 +08:00
fix: resolve type safety regressions across renderer and config
This commit is contained in:
parent
2698f36868
commit
a5c59ce689
@ -1,30 +1,6 @@
|
||||
import { execSync } from 'child_process'
|
||||
import { electronApp, optimizer } from '@electron-toolkit/utils'
|
||||
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 { initI18n } from '../shared/i18n'
|
||||
import { registerIpcMainHandlers } from './utils/ipc'
|
||||
@ -61,6 +37,30 @@ import {
|
||||
getSystemLanguage
|
||||
} 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')
|
||||
|
||||
export { mainWindow, showMainWindow, triggerMainWindow, closeMainWindow }
|
||||
|
||||
@ -117,19 +117,17 @@ export async function downloadAndInstallUpdate(version: string): Promise<void> {
|
||||
{ proxy, responseType: 'text' }
|
||||
)
|
||||
const expectedHash = (sha256Res.data as string).trim().split(/\s+/)[0]
|
||||
const res = await tryDownload(
|
||||
buildDownloadUrls(`${githubBase}${file}`, githubProxy),
|
||||
{
|
||||
responseType: 'arraybuffer',
|
||||
timeout: 0,
|
||||
proxy,
|
||||
headers: { 'Content-Type': 'application/octet-stream' },
|
||||
onProgress: (loaded: number, total: number) => {
|
||||
mainWindow?.webContents.send('updateDownloadProgress', {
|
||||
status: 'downloading',
|
||||
percent: Math.round((loaded / total) * 100)
|
||||
})
|
||||
}
|
||||
const res = await tryDownload(buildDownloadUrls(`${githubBase}${file}`, githubProxy), {
|
||||
responseType: 'arraybuffer',
|
||||
timeout: 0,
|
||||
proxy,
|
||||
headers: { 'Content-Type': 'application/octet-stream' },
|
||||
onProgress: (loaded: number, total: number) => {
|
||||
mainWindow?.webContents.send('updateDownloadProgress', {
|
||||
status: 'downloading',
|
||||
percent: Math.round((loaded / total) * 100)
|
||||
})
|
||||
}
|
||||
})
|
||||
mainWindow?.webContents.send('updateDownloadProgress', { status: 'verifying' })
|
||||
const fileBuffer = Buffer.from(res.data as ArrayBuffer)
|
||||
|
||||
@ -104,8 +104,7 @@ export async function webdavBackup(): Promise<boolean> {
|
||||
|
||||
if (webdavMaxBackups > 0) {
|
||||
try {
|
||||
const files = await client.getDirectoryContents(webdavDir, { glob: '*.zip' })
|
||||
const fileList = Array.isArray(files) ? files : files.data
|
||||
const fileList = await client.getDirectoryContents(webdavDir, { glob: '*.zip' })
|
||||
|
||||
const currentPlatformFiles = fileList.filter((file) => {
|
||||
return file.basename.startsWith(`${process.platform}_`)
|
||||
@ -147,11 +146,7 @@ export async function webdavRestore(filename: string): Promise<void> {
|
||||
export async function listWebdavBackups(): Promise<string[]> {
|
||||
const { client, webdavDir } = await getWebDAVClient()
|
||||
const files = await client.getDirectoryContents(webdavDir, { glob: '*.zip' })
|
||||
if (Array.isArray(files)) {
|
||||
return files.map((file) => file.basename)
|
||||
} else {
|
||||
return files.data.map((file) => file.basename)
|
||||
}
|
||||
return files.map((file) => file.basename)
|
||||
}
|
||||
|
||||
export async function webdavDelete(filename: string): Promise<void> {
|
||||
|
||||
@ -6,10 +6,10 @@ import { join } from 'path'
|
||||
import { createGunzip } from 'zlib'
|
||||
import AdmZip from 'adm-zip'
|
||||
import { stopCore } from '../core/manager'
|
||||
import { getAppConfig } from '../config'
|
||||
import { mihomoCoreDir } from './dirs'
|
||||
import * as chromeRequest from './chromeRequest'
|
||||
import { createLogger } from './logger'
|
||||
import { getAppConfig } from '../config'
|
||||
|
||||
const log = createLogger('GitHub')
|
||||
|
||||
|
||||
@ -5,6 +5,8 @@ import { useTranslation } from 'react-i18next'
|
||||
|
||||
const ErrorFallback = ({ error }: FallbackProps): React.ReactElement => {
|
||||
const { t } = useTranslation()
|
||||
const errorMessage = error instanceof Error ? error.message : String(error)
|
||||
const errorStack = error instanceof Error ? (error.stack ?? '') : ''
|
||||
|
||||
return (
|
||||
<div className="p-4">
|
||||
@ -33,17 +35,17 @@ const ErrorFallback = ({ error }: FallbackProps): React.ReactElement => {
|
||||
variant="flat"
|
||||
className="ml-2"
|
||||
onPress={() =>
|
||||
navigator.clipboard.writeText('```\n' + error.message + '\n' + error.stack + '\n```')
|
||||
navigator.clipboard.writeText('```\n' + errorMessage + '\n' + errorStack + '\n```')
|
||||
}
|
||||
>
|
||||
{t('common.error.copyErrorMessage')}
|
||||
</Button>
|
||||
|
||||
<p className="my-2">{error.message}</p>
|
||||
<p className="my-2">{errorMessage}</p>
|
||||
|
||||
<details title="Error Stack">
|
||||
<summary>Error Stack</summary>
|
||||
<pre>{error.stack}</pre>
|
||||
<pre>{errorStack}</pre>
|
||||
</details>
|
||||
</div>
|
||||
)
|
||||
|
||||
@ -1,8 +1,10 @@
|
||||
import React, { useEffect, useRef, useState, useMemo, useCallback } from 'react'
|
||||
import * as d3 from 'd3'
|
||||
import { Button } from '@heroui/react'
|
||||
import { IoPauseOutline, IoPlayOutline, IoGitNetworkOutline } from 'react-icons/io5'
|
||||
import {
|
||||
IoPauseOutline,
|
||||
IoPlayOutline,
|
||||
IoGitNetworkOutline,
|
||||
IoDesktopOutline,
|
||||
IoServerOutline,
|
||||
IoFunnelOutline
|
||||
@ -101,7 +103,8 @@ function buildHierarchy(
|
||||
proxies: new Map()
|
||||
})
|
||||
}
|
||||
const groupEntry = groupsMap.get(group)!
|
||||
const groupEntry = groupsMap.get(group)
|
||||
if (!groupEntry) continue
|
||||
groupEntry.data.connections++
|
||||
groupEntry.data.traffic += traffic
|
||||
|
||||
@ -117,7 +120,8 @@ function buildHierarchy(
|
||||
rules: new Map()
|
||||
})
|
||||
}
|
||||
const proxyEntry = groupEntry.proxies.get(proxy)!
|
||||
const proxyEntry = groupEntry.proxies.get(proxy)
|
||||
if (!proxyEntry) continue
|
||||
proxyEntry.data.connections++
|
||||
proxyEntry.data.traffic += traffic
|
||||
|
||||
@ -133,7 +137,8 @@ function buildHierarchy(
|
||||
clients: new Map()
|
||||
})
|
||||
}
|
||||
const ruleEntry = proxyEntry.rules.get(fullRule)!
|
||||
const ruleEntry = proxyEntry.rules.get(fullRule)
|
||||
if (!ruleEntry) continue
|
||||
ruleEntry.data.connections++
|
||||
ruleEntry.data.traffic += traffic
|
||||
|
||||
@ -149,7 +154,8 @@ function buildHierarchy(
|
||||
ports: new Map()
|
||||
})
|
||||
}
|
||||
const clientEntry = ruleEntry.clients.get(clientIP)!
|
||||
const clientEntry = ruleEntry.clients.get(clientIP)
|
||||
if (!clientEntry) continue
|
||||
clientEntry.data.connections++
|
||||
clientEntry.data.traffic += traffic
|
||||
|
||||
@ -162,7 +168,8 @@ function buildHierarchy(
|
||||
traffic: 0
|
||||
})
|
||||
}
|
||||
const portNode = clientEntry.ports.get(sourcePort)!
|
||||
const portNode = clientEntry.ports.get(sourcePort)
|
||||
if (!portNode) continue
|
||||
portNode.connections++
|
||||
portNode.traffic += traffic
|
||||
}
|
||||
@ -179,13 +186,16 @@ function buildHierarchy(
|
||||
}
|
||||
|
||||
groupsMap.forEach((groupEntry) => {
|
||||
const groupNode: TopologyNodeData = { ...groupEntry.data, children: [] }
|
||||
const groupChildren: TopologyNodeData[] = []
|
||||
const groupNode: TopologyNodeData = { ...groupEntry.data, children: groupChildren }
|
||||
|
||||
groupEntry.proxies.forEach((proxyEntry) => {
|
||||
const proxyNode: TopologyNodeData = { ...proxyEntry.data, children: [] }
|
||||
const proxyChildren: TopologyNodeData[] = []
|
||||
const proxyNode: TopologyNodeData = { ...proxyEntry.data, children: proxyChildren }
|
||||
|
||||
proxyEntry.rules.forEach((ruleEntry) => {
|
||||
const ruleNode: TopologyNodeData = { ...ruleEntry.data, children: [] }
|
||||
const ruleChildren: TopologyNodeData[] = []
|
||||
const ruleNode: TopologyNodeData = { ...ruleEntry.data, children: ruleChildren }
|
||||
|
||||
ruleEntry.clients.forEach((clientEntry) => {
|
||||
const portChildren = Array.from(clientEntry.ports.values())
|
||||
@ -193,21 +203,21 @@ function buildHierarchy(
|
||||
const clientNode: TopologyNodeData = isClientCollapsed
|
||||
? { ...clientEntry.data, _children: portChildren, children: undefined, collapsed: true }
|
||||
: { ...clientEntry.data, children: portChildren, collapsed: false }
|
||||
ruleNode.children!.push(clientNode)
|
||||
ruleChildren.push(clientNode)
|
||||
})
|
||||
|
||||
const isRuleCollapsed = !collapsedNodes.has(`expanded-${ruleEntry.data.id}`)
|
||||
if (isRuleCollapsed && ruleNode.children!.length > 0) {
|
||||
ruleNode._children = ruleNode.children
|
||||
if (isRuleCollapsed && ruleChildren.length > 0) {
|
||||
ruleNode._children = ruleChildren
|
||||
ruleNode.children = undefined
|
||||
ruleNode.collapsed = true
|
||||
} else {
|
||||
ruleNode.collapsed = false
|
||||
}
|
||||
proxyNode.children!.push(ruleNode)
|
||||
proxyChildren.push(ruleNode)
|
||||
})
|
||||
|
||||
groupNode.children!.push(applyCollapse(proxyNode))
|
||||
groupChildren.push(applyCollapse(proxyNode))
|
||||
})
|
||||
|
||||
rootChildren.push(applyCollapse(groupNode))
|
||||
@ -293,29 +303,26 @@ const NetworkTopologyCard: React.FC = () => {
|
||||
|
||||
// Toggle collapse
|
||||
const toggleCollapseRef = useRef<(nodeId: string, isCollapsed: boolean) => void>(() => {})
|
||||
toggleCollapseRef.current = useCallback(
|
||||
(nodeId: string, isCurrentlyCollapsed: boolean) => {
|
||||
const expandedKey = `expanded-${nodeId}`
|
||||
setCollapsedNodes((prev) => {
|
||||
const next = new Set(prev)
|
||||
if (isCurrentlyCollapsed) {
|
||||
if (nodeId.startsWith('rule-') || nodeId.startsWith('client-')) {
|
||||
next.add(expandedKey)
|
||||
} else {
|
||||
next.delete(nodeId)
|
||||
}
|
||||
toggleCollapseRef.current = useCallback((nodeId: string, isCurrentlyCollapsed: boolean) => {
|
||||
const expandedKey = `expanded-${nodeId}`
|
||||
setCollapsedNodes((prev) => {
|
||||
const next = new Set(prev)
|
||||
if (isCurrentlyCollapsed) {
|
||||
if (nodeId.startsWith('rule-') || nodeId.startsWith('client-')) {
|
||||
next.add(expandedKey)
|
||||
} else {
|
||||
if (nodeId.startsWith('rule-') || nodeId.startsWith('client-')) {
|
||||
next.delete(expandedKey)
|
||||
} else {
|
||||
next.add(nodeId)
|
||||
}
|
||||
next.delete(nodeId)
|
||||
}
|
||||
return next
|
||||
})
|
||||
},
|
||||
[]
|
||||
)
|
||||
} else {
|
||||
if (nodeId.startsWith('rule-') || nodeId.startsWith('client-')) {
|
||||
next.delete(expandedKey)
|
||||
} else {
|
||||
next.add(nodeId)
|
||||
}
|
||||
}
|
||||
return next
|
||||
})
|
||||
}, [])
|
||||
|
||||
// D3 render
|
||||
useEffect(() => {
|
||||
@ -357,8 +364,7 @@ const NetworkTopologyCard: React.FC = () => {
|
||||
}
|
||||
}
|
||||
|
||||
const getNodeWidth = (d: d3.HierarchyNode<TopologyNodeData>) =>
|
||||
nodeWidths.get(d.data.id) ?? 80
|
||||
const getNodeWidth = (d: d3.HierarchyNode<TopologyNodeData>) => nodeWidths.get(d.data.id) ?? 80
|
||||
|
||||
// Max width per depth
|
||||
const maxWidthPerLevel = new Map<number, number>()
|
||||
@ -487,7 +493,7 @@ const NetworkTopologyCard: React.FC = () => {
|
||||
.filter((d) =>
|
||||
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)
|
||||
)
|
||||
)
|
||||
.append('text')
|
||||
@ -513,8 +519,7 @@ const NetworkTopologyCard: React.FC = () => {
|
||||
nodes
|
||||
.append('title')
|
||||
.text(
|
||||
(d) =>
|
||||
`${d.data.name}\n${d.data.connections} connections\n${calcTraffic(d.data.traffic)}`
|
||||
(d) => `${d.data.name}\n${d.data.connections} connections\n${calcTraffic(d.data.traffic)}`
|
||||
)
|
||||
}, [hierarchyData, resolvedTheme])
|
||||
|
||||
@ -555,13 +560,21 @@ const NetworkTopologyCard: React.FC = () => {
|
||||
<div className="flex items-center gap-2">
|
||||
{/* Stats */}
|
||||
<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>{stats.ruleCount} {t('network.topology.rules')}</span>
|
||||
<span>
|
||||
{stats.ruleCount} {t('network.topology.rules')}
|
||||
</span>
|
||||
<span>·</span>
|
||||
<span>{stats.groupCount} {t('network.topology.groups')}</span>
|
||||
<span>
|
||||
{stats.groupCount} {t('network.topology.groups')}
|
||||
</span>
|
||||
<span>·</span>
|
||||
<span>{stats.proxyCount} {t('network.topology.nodes')}</span>
|
||||
<span>
|
||||
{stats.proxyCount} {t('network.topology.nodes')}
|
||||
</span>
|
||||
<span>·</span>
|
||||
<span>{calcTraffic(stats.totalTraffic)}</span>
|
||||
</div>
|
||||
|
||||
@ -1346,7 +1346,7 @@ const EditRulesModal: React.FC<Props> = (props) => {
|
||||
const originalIndex = ruleIndexMap.get(rule) ?? -1
|
||||
return `${originalIndex}-${index}`
|
||||
}}
|
||||
itemContent={(index, rule) => {
|
||||
itemContent={(_index, rule) => {
|
||||
const originalIndex = ruleIndexMap.get(rule) ?? -1
|
||||
const isDeleted = deletedRules.has(originalIndex)
|
||||
const isPrependOrAppend =
|
||||
|
||||
@ -59,34 +59,34 @@ const RuleItem: React.FC<RuleItemProps> = (props) => {
|
||||
</Chip>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
{extra && (() => {
|
||||
const total = extra.hitCount + extra.missCount
|
||||
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">
|
||||
{formatRelativeTime(extra.hitAt || extra.missAt)}
|
||||
</span>
|
||||
<span className="text-foreground-600 font-medium whitespace-nowrap">
|
||||
{extra.hitCount}/{total}
|
||||
</span>
|
||||
<Chip size="sm" variant="flat" color="primary" className="text-xs">
|
||||
{rate.toFixed(1)}%
|
||||
</Chip>
|
||||
</div>
|
||||
<Switch
|
||||
size="sm"
|
||||
isSelected={isEnabled}
|
||||
onValueChange={handleToggle}
|
||||
aria-label="Toggle rule"
|
||||
/>
|
||||
</>
|
||||
)
|
||||
})()}
|
||||
{extra &&
|
||||
(() => {
|
||||
const total = extra.hitCount + extra.missCount
|
||||
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">
|
||||
{formatRelativeTime(extra.hitAt || extra.missAt)}
|
||||
</span>
|
||||
<span className="text-foreground-600 font-medium whitespace-nowrap">
|
||||
{extra.hitCount}/{total}
|
||||
</span>
|
||||
<Chip size="sm" variant="flat" color="primary" className="text-xs">
|
||||
{rate.toFixed(1)}%
|
||||
</Chip>
|
||||
</div>
|
||||
<Switch
|
||||
size="sm"
|
||||
isSelected={isEnabled}
|
||||
onValueChange={handleToggle}
|
||||
aria-label="Toggle rule"
|
||||
/>
|
||||
</>
|
||||
)
|
||||
})()}
|
||||
</div>
|
||||
</CardBody>
|
||||
</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
|
||||
|
||||
// 使用 useCallback 创建稳定的 handler 引用,通过 ref 读取 showTraffic 避免重建
|
||||
const handleTraffic = useCallback(
|
||||
(_e: unknown, ...args: unknown[]) => {
|
||||
const info = args[0] as IMihomoTrafficInfo
|
||||
setUpload(info.up)
|
||||
setDownload(info.down)
|
||||
setSeries((prev) => {
|
||||
const data = [...prev]
|
||||
data.shift()
|
||||
data.push(info.up + info.down)
|
||||
return data
|
||||
})
|
||||
if (platform === 'darwin' && showTrafficRef.current) {
|
||||
const up = info.up
|
||||
const down = info.down
|
||||
if (up !== currentUploadRef.current || down !== currentDownloadRef.current) {
|
||||
currentUploadRef.current = up
|
||||
currentDownloadRef.current = down
|
||||
const png = renderTrafficIcon(up, down)
|
||||
window.electron.ipcRenderer.send('trayIconUpdate', png, true)
|
||||
}
|
||||
const handleTraffic = useCallback((_e: unknown, ...args: unknown[]) => {
|
||||
const info = args[0] as IMihomoTrafficInfo
|
||||
setUpload(info.up)
|
||||
setDownload(info.down)
|
||||
setSeries((prev) => {
|
||||
const data = [...prev]
|
||||
data.shift()
|
||||
data.push(info.up + info.down)
|
||||
return data
|
||||
})
|
||||
if (platform === 'darwin' && showTrafficRef.current) {
|
||||
const up = info.up
|
||||
const down = info.down
|
||||
if (up !== currentUploadRef.current || down !== currentDownloadRef.current) {
|
||||
currentUploadRef.current = up
|
||||
currentDownloadRef.current = down
|
||||
const png = renderTrafficIcon(up, down)
|
||||
window.electron.ipcRenderer.send('trayIconUpdate', png, true)
|
||||
}
|
||||
},
|
||||
[] // eslint-disable-line react-hooks/exhaustive-deps
|
||||
)
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
window.electron.ipcRenderer.on('mihomoTraffic', handleTraffic)
|
||||
@ -295,7 +292,6 @@ const ConnCard: React.FC<Props> = (props) => {
|
||||
|
||||
export default ConnCard
|
||||
|
||||
|
||||
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
|
||||
trafficCtx = trafficCanvas.getContext('2d')
|
||||
}
|
||||
const ctx = trafficCtx!
|
||||
if (!trafficCtx) {
|
||||
return trayIconBase64
|
||||
}
|
||||
const ctx = trafficCtx
|
||||
ctx.clearRect(0, 0, ICON_W, ICON_H)
|
||||
if (trafficIconLoaded) {
|
||||
ctx.drawImage(trafficIcon, 0, 0, ICON_H, ICON_H)
|
||||
|
||||
@ -708,8 +708,7 @@
|
||||
"network.topology.waiting": "Waiting for connections...",
|
||||
"network.topology.pause": "Pause",
|
||||
"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.auto": "Auto (proxy first)",
|
||||
"settings.githubProxy.direct": "Direct"
|
||||
}
|
||||
}
|
||||
|
||||
@ -675,4 +675,4 @@
|
||||
"settings.githubProxy": "پروکسی دانلود GitHub",
|
||||
"settings.githubProxy.auto": "خودکار (پروکسی اول)",
|
||||
"settings.githubProxy.direct": "مستقیم"
|
||||
}
|
||||
}
|
||||
|
||||
@ -677,4 +677,4 @@
|
||||
"settings.githubProxy": "Прокси для загрузки GitHub",
|
||||
"settings.githubProxy.auto": "Авто (сначала прокси)",
|
||||
"settings.githubProxy.direct": "Прямое подключение"
|
||||
}
|
||||
}
|
||||
|
||||
@ -711,4 +711,4 @@
|
||||
"settings.githubProxy": "GitHub 下载代理",
|
||||
"settings.githubProxy.auto": "自动(优先代理)",
|
||||
"settings.githubProxy.direct": "直连"
|
||||
}
|
||||
}
|
||||
|
||||
@ -711,4 +711,4 @@
|
||||
"settings.githubProxy": "GitHub 下載代理",
|
||||
"settings.githubProxy.auto": "自動(優先代理)",
|
||||
"settings.githubProxy.direct": "直連"
|
||||
}
|
||||
}
|
||||
|
||||
@ -50,6 +50,7 @@ const Connections: React.FC = () => {
|
||||
const { 'find-process-mode': findProcessMode = 'always' } = controledMihomoConfig || {}
|
||||
const [filter, setFilter] = useState('')
|
||||
const { appConfig, patchAppConfig } = useAppConfig()
|
||||
const appConfigValues: Partial<IAppConfig> = appConfig ?? {}
|
||||
const {
|
||||
connectionDirection = 'asc',
|
||||
connectionOrderBy = 'time',
|
||||
@ -73,7 +74,7 @@ const Connections: React.FC = () => {
|
||||
connectionTableSortDirection,
|
||||
displayIcon = true,
|
||||
displayAppName = true
|
||||
} = appConfig || {}
|
||||
} = appConfigValues
|
||||
const [connectionsInfo, setConnectionsInfo] = useState<IMihomoConnectionsInfo>()
|
||||
const [allConnections, setAllConnections] = useState<IMihomoConnectionDetail[]>(cachedConnections)
|
||||
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 list = [...values[type]]
|
||||
const parts = value.split(',').map((s: string) => s.trim()).filter(Boolean)
|
||||
const processedValue = type === 'hosts'
|
||||
? parts
|
||||
: (parts.length > 1 ? parts : value.trim())
|
||||
const parts = value
|
||||
.split(',')
|
||||
.map((s: string) => s.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 }
|
||||
else list.splice(index, 1)
|
||||
setValues({ ...values, [type]: list })
|
||||
|
||||
@ -196,10 +196,10 @@ const IPPage: React.FC = () => {
|
||||
|
||||
const averageLatency = (() => {
|
||||
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
|
||||
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(
|
||||
@ -292,7 +292,9 @@ const IPPage: React.FC = () => {
|
||||
<div className="flex flex-col gap-2.5">
|
||||
{/* 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">
|
||||
<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">
|
||||
<span className="overflow-hidden text-right font-mono text-[13px] font-semibold text-primary text-ellipsis whitespace-nowrap">
|
||||
{hidden ? '••••••••••••••' : ipInfo.ip}
|
||||
|
||||
@ -401,13 +401,14 @@ const Proxies: React.FC = () => {
|
||||
groupCounts,
|
||||
isOpen,
|
||||
proxyDisplayMode,
|
||||
t,
|
||||
searchValue,
|
||||
delaying,
|
||||
cols,
|
||||
allProxies,
|
||||
virtuosoRef,
|
||||
t,
|
||||
mutate,
|
||||
setIsOpen,
|
||||
allProxies,
|
||||
cols,
|
||||
virtuosoRef,
|
||||
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>
|
||||
connectionTableSortColumn?: string
|
||||
connectionTableSortDirection?: 'asc' | 'desc'
|
||||
displayIcon?: boolean
|
||||
displayAppName?: boolean
|
||||
spinFloatingIcon?: boolean
|
||||
disableTray?: boolean
|
||||
swapTrayClick?: boolean
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user