Compare commits

...

2 Commits

21 changed files with 183 additions and 169 deletions

View File

@ -250,7 +250,7 @@ export async function createProfile(item: Partial<IProfileItem>): Promise<IProfi
authToken: item.authToken,
userAgent: item.userAgent,
updated: new Date().getTime(),
updateTimeout: item.updateTimeout || 5
updateTimeout: item.updateTimeout
}
// Local
@ -264,7 +264,10 @@ export async function createProfile(item: Partial<IProfileItem>): Promise<IProfi
const { userAgent, subscriptionTimeout = 30000 } = await getAppConfig()
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'> = {
url: item.url,
@ -454,7 +457,7 @@ export async function convertMrsRuleset(filePath: string, behavior: string): Pro
try {
// 使用 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}"`)
const content = await readFile(tempFilePath, 'utf-8')
await unlink(tempFilePath)

View File

@ -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 }

View File

@ -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)

View File

@ -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> {

View File

@ -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')

View File

@ -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>
)

View File

@ -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>

View File

@ -33,8 +33,7 @@ const EditInfoModal: React.FC<Props> = (props) => {
const { overrideConfig } = useOverrideConfig()
const { items: overrideItems = [] } = overrideConfig || {}
const [values, setValues] = useState({
...item,
updateTimeout: item.updateTimeout ?? 5
...item
})
const inputWidth = 'w-[400px] md:w-[400px] lg:w-[600px] xl:w-[800px]'
const { t } = useTranslation()
@ -43,7 +42,6 @@ const EditInfoModal: React.FC<Props> = (props) => {
try {
const updatedItem = {
...values,
updateTimeout: values.updateTimeout ?? 5,
override: values.override?.filter(
(i) =>
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() ?? ''}
onValueChange={(v) => {
if (v === '') {
setValues({ ...values, updateTimeout: undefined as unknown as number })
setValues({ ...values, updateTimeout: undefined })
return
}
if (/^\d+$/.test(v)) {

View File

@ -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 =

View File

@ -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>

View File

@ -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)

View File

@ -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"
}
}

View File

@ -675,4 +675,4 @@
"settings.githubProxy": "پروکسی دانلود GitHub",
"settings.githubProxy.auto": "خودکار (پروکسی اول)",
"settings.githubProxy.direct": "مستقیم"
}
}

View File

@ -677,4 +677,4 @@
"settings.githubProxy": "Прокси для загрузки GitHub",
"settings.githubProxy.auto": "Авто (сначала прокси)",
"settings.githubProxy.direct": "Прямое подключение"
}
}

View File

@ -711,4 +711,4 @@
"settings.githubProxy": "GitHub 下载代理",
"settings.githubProxy.auto": "自动(优先代理)",
"settings.githubProxy.direct": "直连"
}
}

View File

@ -711,4 +711,4 @@
"settings.githubProxy": "GitHub 下載代理",
"settings.githubProxy.auto": "自動(優先代理)",
"settings.githubProxy.direct": "直連"
}
}

View File

@ -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[]>([])

View File

@ -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 })

View File

@ -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}

View File

@ -401,13 +401,14 @@ const Proxies: React.FC = () => {
groupCounts,
isOpen,
proxyDisplayMode,
t,
searchValue,
delaying,
cols,
allProxies,
virtuosoRef,
t,
mutate,
setIsOpen,
allProxies,
cols,
virtuosoRef,
onGroupDelay
]
)

View File

@ -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