mirror of
https://hubproxy.babadafafafafa.cn/https://github.com/mihomo-party-org/clash-party.git
synced 2026-09-20 08:03:39 +08:00
3
.gitignore
vendored
3
.gitignore
vendored
@@ -11,6 +11,3 @@ out
|
||||
party.md
|
||||
CLAUDE.md
|
||||
tsconfig.node.tsbuildinfo
|
||||
.omc
|
||||
.claude
|
||||
debug
|
||||
|
||||
@@ -4,6 +4,3 @@ pnpm-lock.yaml
|
||||
LICENSE.md
|
||||
tsconfig.json
|
||||
tsconfig.*.json
|
||||
.omc
|
||||
.claude
|
||||
debug
|
||||
|
||||
@@ -39,8 +39,3 @@ export async function patchAppConfig(patch: Partial<IAppConfig>): Promise<void>
|
||||
})
|
||||
await appConfigWriteQueue
|
||||
}
|
||||
|
||||
export async function getAppProxyRules(): Promise<IAppProxyRule[]> {
|
||||
const { appProxyRules = [] } = await getAppConfig()
|
||||
return appProxyRules
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export { getAppConfig, patchAppConfig, getAppProxyRules } from './app'
|
||||
export { getAppConfig, patchAppConfig } from './app'
|
||||
export { getControledMihomoConfig, patchControledMihomoConfig } from './controledMihomo'
|
||||
export {
|
||||
getProfile,
|
||||
|
||||
@@ -73,9 +73,7 @@ export async function generateProfile(): Promise<string | undefined> {
|
||||
const overrideIds = await getOrderedOverrideIds(current)
|
||||
const profileWithNormalOverride = await applyOverrides(baseProfile, overrideIds.normal)
|
||||
const profileWithRuleOverride = await applyRuleOverride(current, profileWithNormalOverride)
|
||||
const { appProxyRules = [] } = await getAppConfig()
|
||||
const profileWithAppProxy = applyAppProxyRules(profileWithRuleOverride, appProxyRules)
|
||||
const currentProfile = await applyOverrides(profileWithAppProxy, overrideIds.smart)
|
||||
const currentProfile = await applyOverrides(profileWithRuleOverride, overrideIds.smart)
|
||||
let controledMihomoConfig = await getControledMihomoConfig()
|
||||
|
||||
// 根据开关状态过滤控制配置
|
||||
@@ -117,18 +115,6 @@ export async function generateProfile(): Promise<string | undefined> {
|
||||
return current
|
||||
}
|
||||
|
||||
function applyAppProxyRules(profile: IMihomoConfig, rules: IAppProxyRule[]): IMihomoConfig {
|
||||
const disabledApps = rules.filter(
|
||||
(r) => !r.enabled && r.processName && !r.processName.includes(',')
|
||||
)
|
||||
if (disabledApps.length === 0) return profile
|
||||
|
||||
const profileRules = (profile.rules || []) as unknown as string[]
|
||||
const processRules = disabledApps.map((r) => `PROCESS-NAME,${r.processName},DIRECT`)
|
||||
profile.rules = [...processRules, ...profileRules] as unknown as []
|
||||
return profile
|
||||
}
|
||||
|
||||
async function applyRuleOverride(
|
||||
current: string | undefined,
|
||||
profile: IMihomoConfig
|
||||
|
||||
@@ -33,7 +33,6 @@ import {
|
||||
patchAppConfig,
|
||||
getControledMihomoConfig,
|
||||
patchControledMihomoConfig,
|
||||
getAppProxyRules,
|
||||
getProfileConfig,
|
||||
getCurrentProfileItem,
|
||||
getProfileItem,
|
||||
@@ -250,7 +249,7 @@ const asyncHandlers: Record<string, AsyncFn> = {
|
||||
patchAppConfig,
|
||||
getControledMihomoConfig,
|
||||
patchControledMihomoConfig,
|
||||
getAppProxyRules,
|
||||
// Profile
|
||||
getProfileConfig,
|
||||
setProfileConfig,
|
||||
getCurrentProfileItem,
|
||||
|
||||
@@ -40,7 +40,6 @@ export const defaultConfig: IAppConfig = {
|
||||
'resource',
|
||||
'override',
|
||||
'connection',
|
||||
'apps',
|
||||
'mihomo',
|
||||
'dns',
|
||||
'sniff',
|
||||
@@ -63,9 +62,7 @@ export const defaultConfig: IAppConfig = {
|
||||
enableTproxyPort: false,
|
||||
testProfileOnStart: true,
|
||||
useHotReloadProfile: false,
|
||||
hotReloadProfileAutoCloseConnection: false,
|
||||
appProxyRules: [],
|
||||
appsCardStatus: 'col-span-1'
|
||||
hotReloadProfileAutoCloseConnection: false
|
||||
}
|
||||
|
||||
export const defaultControledMihomoConfig: Partial<IMihomoConfig> = {
|
||||
|
||||
@@ -33,7 +33,6 @@ const validInvokeChannels = [
|
||||
'patchAppConfig',
|
||||
'getControledMihomoConfig',
|
||||
'patchControledMihomoConfig',
|
||||
'getAppProxyRules',
|
||||
'resetAppConfig',
|
||||
// Profile
|
||||
'getProfileConfig',
|
||||
|
||||
@@ -34,7 +34,6 @@ import { TitleBarOverlayOptions } from 'electron'
|
||||
import SubStoreCard from '@renderer/components/sider/substore-card'
|
||||
import NetworkCard from '@renderer/components/sider/network-card'
|
||||
import UsageCard from '@renderer/components/sider/usage-card'
|
||||
import AppsCard from '@renderer/components/sider/apps-card'
|
||||
import { useTrafficLogger } from '@renderer/hooks/use-traffic-logger'
|
||||
import { createTourDriver, getDriver, startTourIfNeeded } from '@renderer/utils/tour'
|
||||
import 'driver.js/dist/driver.css'
|
||||
@@ -54,7 +53,6 @@ const ALL_SIDER_KEYS = [
|
||||
'resource',
|
||||
'override',
|
||||
'connection',
|
||||
'apps',
|
||||
'mihomo',
|
||||
'dns',
|
||||
'sniff',
|
||||
@@ -88,7 +86,6 @@ const App: React.FC = () => {
|
||||
'resource',
|
||||
'override',
|
||||
'connection',
|
||||
'apps',
|
||||
'mihomo',
|
||||
'dns',
|
||||
'sniff',
|
||||
@@ -191,7 +188,6 @@ const App: React.FC = () => {
|
||||
proxy: 'proxies',
|
||||
mihomo: 'mihomo',
|
||||
connection: 'connections',
|
||||
apps: 'apps',
|
||||
dns: 'dns',
|
||||
sniff: 'sniffer',
|
||||
log: 'logs',
|
||||
@@ -210,7 +206,6 @@ const App: React.FC = () => {
|
||||
proxy: ProxyCard,
|
||||
mihomo: MihomoCoreCard,
|
||||
connection: ConnCard,
|
||||
apps: AppsCard,
|
||||
dns: DNSCard,
|
||||
sniff: SniffCard,
|
||||
log: LogCard,
|
||||
|
||||
@@ -267,7 +267,7 @@ const NetworkTopologyCard: React.FC = () => {
|
||||
}
|
||||
window.electron.ipcRenderer.on('mihomoConnections', handler)
|
||||
return () => {
|
||||
window.electron.ipcRenderer.removeListener('mihomoConnections', handler)
|
||||
window.electron.ipcRenderer.removeAllListeners('mihomoConnections')
|
||||
}
|
||||
}, [isPaused])
|
||||
|
||||
|
||||
@@ -1,106 +0,0 @@
|
||||
import { Button, Card, CardBody, CardFooter, Tooltip } from '@heroui/react'
|
||||
import { MdApps } from 'react-icons/md'
|
||||
import { useLocation, useNavigate } from 'react-router-dom'
|
||||
import { useSortable } from '@dnd-kit/sortable'
|
||||
import { CSS } from '@dnd-kit/utilities'
|
||||
import { useAppConfig } from '@renderer/hooks/use-app-config'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
interface Props {
|
||||
iconOnly?: boolean
|
||||
}
|
||||
const AppsCard: React.FC<Props> = (props) => {
|
||||
const { t } = useTranslation()
|
||||
const { appConfig } = useAppConfig()
|
||||
const { iconOnly } = props
|
||||
const {
|
||||
appsCardStatus = 'col-span-1',
|
||||
disableAnimations = false,
|
||||
appProxyRules = []
|
||||
} = appConfig || {}
|
||||
const bypassedCount = appProxyRules.filter((r) => !r.enabled).length
|
||||
const location = useLocation()
|
||||
const navigate = useNavigate()
|
||||
const match = location.pathname.includes('/apps')
|
||||
const {
|
||||
attributes,
|
||||
listeners,
|
||||
setNodeRef,
|
||||
transform: tf,
|
||||
transition,
|
||||
isDragging
|
||||
} = useSortable({ id: 'apps' })
|
||||
const transform = tf ? { x: tf.x, y: tf.y, scaleX: 1, scaleY: 1 } : null
|
||||
|
||||
if (iconOnly) {
|
||||
return (
|
||||
<div className={`${appsCardStatus} flex justify-center`}>
|
||||
<Tooltip content={t('sider.cards.apps')} placement="right">
|
||||
<Button
|
||||
size="sm"
|
||||
isIconOnly
|
||||
color={match ? 'primary' : 'default'}
|
||||
variant={match ? 'solid' : 'light'}
|
||||
onPress={() => {
|
||||
navigate('/apps')
|
||||
}}
|
||||
>
|
||||
<MdApps className="text-[20px]" />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
position: 'relative',
|
||||
transform: CSS.Transform.toString(transform),
|
||||
transition,
|
||||
zIndex: isDragging ? 'calc(infinity)' : undefined
|
||||
}}
|
||||
className={`${appsCardStatus} apps-card`}
|
||||
>
|
||||
<Card
|
||||
fullWidth
|
||||
ref={setNodeRef}
|
||||
{...attributes}
|
||||
{...listeners}
|
||||
className={`${match ? 'bg-primary' : 'hover:bg-primary/30'} ${disableAnimations ? '' : `motion-reduce:transition-transform-background ${isDragging ? 'scale-[0.95] tap-highlight-transparent' : ''}`}`}
|
||||
>
|
||||
<CardBody className="pb-1 pt-0 px-0">
|
||||
<div className="flex justify-between">
|
||||
<Button
|
||||
isIconOnly
|
||||
className="bg-transparent pointer-events-none"
|
||||
variant="flat"
|
||||
color="default"
|
||||
>
|
||||
<MdApps
|
||||
className={`${match ? 'text-primary-foreground' : 'text-foreground'} text-[24px] font-bold`}
|
||||
/>
|
||||
</Button>
|
||||
{bypassedCount > 0 && (
|
||||
<span
|
||||
className={`text-xs mt-2 mr-2 ${match ? 'text-primary-foreground/70' : 'text-foreground/50'}`}
|
||||
>
|
||||
{bypassedCount} {t('apps.bypassed')}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</CardBody>
|
||||
<CardFooter className="pt-1">
|
||||
<h3
|
||||
className={`text-md font-bold sider-card-title ${match ? 'text-primary-foreground' : 'text-foreground'}`}
|
||||
>
|
||||
{t('sider.cards.apps')}
|
||||
</h3>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default AppsCard
|
||||
@@ -323,14 +323,6 @@
|
||||
"sider.cards.logs": "Logs",
|
||||
"sider.cards.substore": "Sub-Store",
|
||||
"sider.cards.network": "Network Info",
|
||||
"sider.cards.apps": "Apps",
|
||||
"apps.title": "App Proxy",
|
||||
"apps.search": "Search apps",
|
||||
"apps.connections": "connections",
|
||||
"apps.bypassed": "Bypassed",
|
||||
"apps.noApps": "No active connections",
|
||||
"apps.warning.noProcess": "Process detection is off. Enable \"Find Process Mode\" for this feature to work.",
|
||||
"apps.warning.noRuleMode": "Rules are not effective in current outbound mode. Switch to \"Rule\" mode.",
|
||||
"network.title": "Network Info",
|
||||
"network.ipCard.title": "Current IP",
|
||||
"network.ipAddress": "IP Address",
|
||||
|
||||
@@ -298,14 +298,6 @@
|
||||
"sider.cards.logs": "گزارشها",
|
||||
"sider.cards.substore": "ساب استور",
|
||||
"sider.cards.network": "اطلاعات شبکه",
|
||||
"sider.cards.apps": "اپلیکیشنها",
|
||||
"apps.title": "پروکسی برنامه",
|
||||
"apps.search": "جستجوی برنامه",
|
||||
"apps.connections": "اتصال",
|
||||
"apps.bypassed": "دور زده",
|
||||
"apps.noApps": "اتصال فعالی نیست",
|
||||
"apps.warning.noProcess": "تشخیص فرآیند خاموش است.",
|
||||
"apps.warning.noRuleMode": "قوانین در حالت فعلی فعال نیستند.",
|
||||
"network.title": "اطلاعات شبکه",
|
||||
"network.ipCard.title": "IP فعلی",
|
||||
"network.ipAddress": "آدرس IP",
|
||||
|
||||
@@ -300,14 +300,6 @@
|
||||
"sider.cards.logs": "Журналы",
|
||||
"sider.cards.substore": "Sub-Store",
|
||||
"sider.cards.network": "Сетевая информация",
|
||||
"sider.cards.apps": "Приложения",
|
||||
"apps.title": "Прокси приложений",
|
||||
"apps.search": "Поиск приложений",
|
||||
"apps.connections": "соединений",
|
||||
"apps.bypassed": "Обойдён",
|
||||
"apps.noApps": "Нет активных соединений",
|
||||
"apps.warning.noProcess": "Обнаружение процессов отключено.",
|
||||
"apps.warning.noRuleMode": "Правила не действуют в текущем режиме.",
|
||||
"network.title": "Сетевая информация",
|
||||
"network.ipCard.title": "Текущий IP",
|
||||
"network.ipAddress": "IP-адрес",
|
||||
|
||||
@@ -323,14 +323,6 @@
|
||||
"sider.cards.logs": "日志",
|
||||
"sider.cards.substore": "Sub-Store",
|
||||
"sider.cards.network": "网络信息",
|
||||
"sider.cards.apps": "应用",
|
||||
"apps.title": "应用代理",
|
||||
"apps.search": "搜索应用",
|
||||
"apps.connections": "个连接",
|
||||
"apps.bypassed": "已绕过",
|
||||
"apps.noApps": "暂无活跃连接",
|
||||
"apps.warning.noProcess": "进程检测已关闭,请开启\"查找进程模式\"以使用此功能。",
|
||||
"apps.warning.noRuleMode": "当前出站模式下规则不生效,请切换到\"规则\"模式。",
|
||||
"network.title": "网络信息",
|
||||
"network.ipCard.title": "当前 IP",
|
||||
"network.ipAddress": "IP 地址",
|
||||
|
||||
@@ -323,14 +323,6 @@
|
||||
"sider.cards.logs": "日誌",
|
||||
"sider.cards.substore": "Sub-Store",
|
||||
"sider.cards.network": "網路資訊",
|
||||
"sider.cards.apps": "應用",
|
||||
"apps.title": "應用代理",
|
||||
"apps.search": "搜尋應用",
|
||||
"apps.connections": "個連線",
|
||||
"apps.bypassed": "已繞過",
|
||||
"apps.noApps": "暫無活躍連線",
|
||||
"apps.warning.noProcess": "進程檢測已關閉,請開啟「查找進程模式」以使用此功能。",
|
||||
"apps.warning.noRuleMode": "當前出站模式下規則不生效,請切換到「規則」模式。",
|
||||
"network.title": "網路資訊",
|
||||
"network.ipCard.title": "目前 IP",
|
||||
"network.ipAddress": "IP 位址",
|
||||
|
||||
@@ -1,304 +0,0 @@
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Virtuoso } from 'react-virtuoso'
|
||||
import { Avatar, Input, Switch, Card, CardBody } from '@heroui/react'
|
||||
import { MdApps, MdSearch, MdWarning } from 'react-icons/md'
|
||||
import { IoLogoApple } from 'react-icons/io5'
|
||||
import BasePage from '@renderer/components/base/base-page'
|
||||
import { useAppConfig } from '@renderer/hooks/use-app-config'
|
||||
import { useControledMihomoConfig } from '@renderer/hooks/use-controled-mihomo-config'
|
||||
import {
|
||||
patchAppConfig,
|
||||
mihomoHotReloadConfig,
|
||||
getIconDataURL,
|
||||
getAppName
|
||||
} from '@renderer/utils/ipc'
|
||||
import { calcTraffic } from '@renderer/utils/calc'
|
||||
import { getIconFromCache, saveIconToCache } from '@renderer/utils/icon-cache'
|
||||
import { cropAndPadTransparent } from '@renderer/utils/image'
|
||||
import { platform } from '@renderer/utils/init'
|
||||
|
||||
interface AppGroup {
|
||||
processName: string
|
||||
processPath: string
|
||||
connections: IMihomoConnectionDetail[]
|
||||
upload: number
|
||||
download: number
|
||||
uploadSpeed: number
|
||||
downloadSpeed: number
|
||||
}
|
||||
|
||||
const Apps: React.FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const { appConfig } = useAppConfig()
|
||||
const { controledMihomoConfig } = useControledMihomoConfig()
|
||||
const [appGroups, setAppGroups] = useState<AppGroup[]>([])
|
||||
const [proxyRules, setProxyRules] = useState<IAppProxyRule[]>([])
|
||||
const [filter, setFilter] = useState('')
|
||||
const [iconMap, setIconMap] = useState<Record<string, string>>({})
|
||||
const [appNameCache, setAppNameCache] = useState<Record<string, string>>({})
|
||||
const iconProcessingRef = useRef<Set<string>>(new Set())
|
||||
const appNameProcessingRef = useRef<Set<string>>(new Set())
|
||||
|
||||
const findProcessMode = controledMihomoConfig?.['find-process-mode'] || 'strict'
|
||||
const mode = controledMihomoConfig?.mode || 'rule'
|
||||
|
||||
// Load proxy rules from config
|
||||
useEffect(() => {
|
||||
if (appConfig?.appProxyRules) {
|
||||
setProxyRules(appConfig.appProxyRules)
|
||||
}
|
||||
}, [appConfig?.appProxyRules])
|
||||
|
||||
// Listen to mihomoConnections
|
||||
useEffect(() => {
|
||||
const handler = (_e: unknown, ...args: unknown[]): void => {
|
||||
const info = args[0] as IMihomoConnectionsInfo
|
||||
if (!info.connections) return
|
||||
|
||||
const groupMap = new Map<string, AppGroup>()
|
||||
for (const conn of info.connections) {
|
||||
const processName = conn.metadata.process || 'Unknown'
|
||||
const processPath = conn.metadata.processPath || ''
|
||||
const key = processName
|
||||
|
||||
if (!groupMap.has(key)) {
|
||||
groupMap.set(key, {
|
||||
processName,
|
||||
processPath,
|
||||
connections: [],
|
||||
upload: 0,
|
||||
download: 0,
|
||||
uploadSpeed: 0,
|
||||
downloadSpeed: 0
|
||||
})
|
||||
}
|
||||
const group = groupMap.get(key)
|
||||
if (!group) return
|
||||
group.connections.push(conn)
|
||||
group.upload += conn.upload
|
||||
group.download += conn.download
|
||||
group.uploadSpeed += conn.uploadSpeed || 0
|
||||
group.downloadSpeed += conn.downloadSpeed || 0
|
||||
}
|
||||
|
||||
const groups = Array.from(groupMap.values()).sort(
|
||||
(a, b) => b.download + b.upload - (a.download + a.upload)
|
||||
)
|
||||
setAppGroups(groups)
|
||||
}
|
||||
|
||||
window.electron.ipcRenderer.on('mihomoConnections', handler)
|
||||
return () => {
|
||||
window.electron.ipcRenderer.removeListener('mihomoConnections', handler)
|
||||
}
|
||||
}, [])
|
||||
|
||||
// Load icons
|
||||
useEffect(() => {
|
||||
if (findProcessMode === 'off') return
|
||||
const queue: string[] = []
|
||||
for (const group of appGroups) {
|
||||
if (!group.processPath) continue
|
||||
if (iconMap[group.processPath]) continue
|
||||
if (getIconFromCache(group.processPath)) continue
|
||||
if (!queue.includes(group.processPath)) {
|
||||
queue.push(group.processPath)
|
||||
}
|
||||
}
|
||||
if (queue.length === 0) return
|
||||
|
||||
const processQueue = (): void => {
|
||||
const path = queue.shift()
|
||||
if (!path) return
|
||||
if (iconProcessingRef.current.has(path)) {
|
||||
if (queue.length > 0) setTimeout(processQueue, 50)
|
||||
return
|
||||
}
|
||||
iconProcessingRef.current.add(path)
|
||||
getIconDataURL(path)
|
||||
.then((dataUrl) => {
|
||||
if (platform !== 'darwin' && dataUrl) {
|
||||
return cropAndPadTransparent(dataUrl)
|
||||
}
|
||||
return dataUrl
|
||||
})
|
||||
.then((dataUrl) => {
|
||||
if (dataUrl) {
|
||||
saveIconToCache(path, dataUrl)
|
||||
setIconMap((prev) => ({ ...prev, [path]: dataUrl }))
|
||||
}
|
||||
})
|
||||
.catch(() => {})
|
||||
.finally(() => {
|
||||
iconProcessingRef.current.delete(path)
|
||||
if (queue.length > 0) setTimeout(processQueue, 50)
|
||||
})
|
||||
}
|
||||
setTimeout(processQueue, 10)
|
||||
}, [appGroups, iconMap, findProcessMode])
|
||||
|
||||
// Load app names
|
||||
useEffect(() => {
|
||||
if (platform !== 'darwin') return
|
||||
const queue: string[] = []
|
||||
for (const group of appGroups) {
|
||||
if (!group.processPath) continue
|
||||
if (appNameCache[group.processPath]) continue
|
||||
if (!queue.includes(group.processPath)) {
|
||||
queue.push(group.processPath)
|
||||
}
|
||||
}
|
||||
if (queue.length === 0) return
|
||||
|
||||
const processQueue = (): void => {
|
||||
const path = queue.shift()
|
||||
if (!path) return
|
||||
if (appNameProcessingRef.current.has(path)) {
|
||||
if (queue.length > 0) setTimeout(processQueue, 100)
|
||||
return
|
||||
}
|
||||
appNameProcessingRef.current.add(path)
|
||||
getAppName(path)
|
||||
.then((name) => {
|
||||
if (name) {
|
||||
setAppNameCache((prev) => ({ ...prev, [path]: name }))
|
||||
}
|
||||
})
|
||||
.catch(() => {})
|
||||
.finally(() => {
|
||||
appNameProcessingRef.current.delete(path)
|
||||
if (queue.length > 0) setTimeout(processQueue, 100)
|
||||
})
|
||||
}
|
||||
setTimeout(processQueue, 10)
|
||||
}, [appGroups, appNameCache])
|
||||
|
||||
// Toggle app proxy
|
||||
const handleToggle = useCallback(
|
||||
async (processName: string, processPath: string, enabled: boolean) => {
|
||||
let newRules: IAppProxyRule[]
|
||||
const existing = proxyRules.find((r) => r.processName === processName)
|
||||
if (existing) {
|
||||
newRules = proxyRules.map((r) => (r.processName === processName ? { ...r, enabled } : r))
|
||||
} else {
|
||||
newRules = [...proxyRules, { processName, processPath, enabled }]
|
||||
}
|
||||
setProxyRules(newRules)
|
||||
try {
|
||||
await patchAppConfig({ appProxyRules: newRules })
|
||||
await mihomoHotReloadConfig()
|
||||
} catch {
|
||||
setProxyRules(proxyRules)
|
||||
}
|
||||
},
|
||||
[proxyRules]
|
||||
)
|
||||
|
||||
// Filter
|
||||
const filteredGroups = useMemo(() => {
|
||||
if (!filter) return appGroups
|
||||
const lowerFilter = filter.toLowerCase()
|
||||
return appGroups.filter(
|
||||
(g) =>
|
||||
g.processName.toLowerCase().includes(lowerFilter) ||
|
||||
(appNameCache[g.processPath] || '').toLowerCase().includes(lowerFilter)
|
||||
)
|
||||
}, [appGroups, filter, appNameCache])
|
||||
|
||||
const isProcessOff = findProcessMode === 'off'
|
||||
const isNotRuleMode = mode !== 'rule'
|
||||
|
||||
return (
|
||||
<BasePage title={t('apps.title')}>
|
||||
{/* Warning banners */}
|
||||
{isProcessOff && (
|
||||
<div className="mx-2 mt-2 flex items-center gap-2 rounded-lg bg-warning-100 p-3 text-sm text-warning-700 dark:bg-warning-900/30 dark:text-warning-400">
|
||||
<MdWarning className="text-lg flex-shrink-0" />
|
||||
<span>{t('apps.warning.noProcess')}</span>
|
||||
</div>
|
||||
)}
|
||||
{isNotRuleMode && (
|
||||
<div className="mx-2 mt-2 flex items-center gap-2 rounded-lg bg-warning-100 p-3 text-sm text-warning-700 dark:bg-warning-900/30 dark:text-warning-400">
|
||||
<MdWarning className="text-lg flex-shrink-0" />
|
||||
<span>{t('apps.warning.noRuleMode')}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="px-2 pt-2">
|
||||
<Input
|
||||
size="sm"
|
||||
placeholder={t('apps.search')}
|
||||
value={filter}
|
||||
onValueChange={setFilter}
|
||||
startContent={<MdSearch className="text-foreground/50" />}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{filteredGroups.length === 0 ? (
|
||||
<div className="flex flex-col items-center justify-center py-20 text-foreground/50">
|
||||
<MdApps className="text-5xl mb-2" />
|
||||
<span>{t('apps.noApps')}</span>
|
||||
</div>
|
||||
) : (
|
||||
<div className="h-[calc(100vh-180px)]">
|
||||
<Virtuoso
|
||||
data={filteredGroups}
|
||||
itemContent={(_, group) => {
|
||||
const rule = proxyRules.find((r) => r.processName === group.processName)
|
||||
const isEnabled = rule ? rule.enabled : true
|
||||
const isUnknown = group.processName === 'Unknown'
|
||||
const iconUrl =
|
||||
findProcessMode !== 'off' && group.processPath
|
||||
? iconMap[group.processPath] || getIconFromCache(group.processPath) || ''
|
||||
: ''
|
||||
const displayName =
|
||||
group.processPath && appNameCache[group.processPath]
|
||||
? appNameCache[group.processPath]
|
||||
: group.processName
|
||||
|
||||
return (
|
||||
<Card className="m-2" radius="sm">
|
||||
<CardBody className="flex flex-row items-center gap-3 px-3 py-2">
|
||||
<Avatar
|
||||
src={iconUrl}
|
||||
icon={iconUrl ? undefined : <IoLogoApple />}
|
||||
className="flex-shrink-0"
|
||||
size="sm"
|
||||
radius="sm"
|
||||
/>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm font-medium truncate">{displayName}</span>
|
||||
<span className="text-xs text-foreground/50 flex-shrink-0">
|
||||
{group.connections.length} {t('apps.connections')}
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-xs text-foreground/50 flex gap-2">
|
||||
<span>↑ {calcTraffic(group.uploadSpeed)}/s</span>
|
||||
<span>↓ {calcTraffic(group.downloadSpeed)}/s</span>
|
||||
<span>↑ {calcTraffic(group.upload)}</span>
|
||||
<span>↓ {calcTraffic(group.download)}</span>
|
||||
</div>
|
||||
</div>
|
||||
<Switch
|
||||
size="sm"
|
||||
isSelected={isEnabled}
|
||||
isDisabled={isUnknown || isProcessOff}
|
||||
onValueChange={(val) =>
|
||||
handleToggle(group.processName, group.processPath, val)
|
||||
}
|
||||
color={isEnabled ? 'primary' : 'danger'}
|
||||
/>
|
||||
</CardBody>
|
||||
</Card>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</BasePage>
|
||||
)
|
||||
}
|
||||
|
||||
export default Apps
|
||||
@@ -427,7 +427,7 @@ const Connections: React.FC = () => {
|
||||
}
|
||||
|
||||
return (): void => {
|
||||
window.electron.ipcRenderer.removeListener('mihomoConnections', handler)
|
||||
window.electron.ipcRenderer.removeAllListeners('mihomoConnections')
|
||||
}
|
||||
}, [isPaused])
|
||||
const togglePause = useCallback(() => {
|
||||
|
||||
@@ -7,7 +7,6 @@ import Settings from '@renderer/pages/settings'
|
||||
import Profiles from '@renderer/pages/profiles'
|
||||
import Logs from '@renderer/pages/logs'
|
||||
import Connections from '@renderer/pages/connections'
|
||||
import Apps from '@renderer/pages/apps'
|
||||
import Mihomo from '@renderer/pages/mihomo'
|
||||
import Sysproxy from '@renderer/pages/sysproxy'
|
||||
import Tun from '@renderer/pages/tun'
|
||||
@@ -61,10 +60,6 @@ const routes = [
|
||||
path: '/connections',
|
||||
element: <Connections />
|
||||
},
|
||||
{
|
||||
path: '/apps',
|
||||
element: <Apps />
|
||||
},
|
||||
{
|
||||
path: '/override',
|
||||
element: <Override />
|
||||
|
||||
@@ -46,7 +46,6 @@ interface IpcApi {
|
||||
patchAppConfig: (patch: Partial<IAppConfig>) => Promise<void>
|
||||
getControledMihomoConfig: (force?: boolean) => Promise<Partial<IMihomoConfig>>
|
||||
patchControledMihomoConfig: (patch: Partial<IMihomoConfig>) => Promise<void>
|
||||
getAppProxyRules: () => Promise<IAppProxyRule[]>
|
||||
resetAppConfig: () => Promise<void>
|
||||
// Profile
|
||||
getProfileConfig: (force?: boolean) => Promise<IProfileConfig>
|
||||
@@ -204,7 +203,6 @@ export const {
|
||||
patchAppConfig,
|
||||
getControledMihomoConfig,
|
||||
patchControledMihomoConfig,
|
||||
getAppProxyRules,
|
||||
resetAppConfig,
|
||||
// Profile
|
||||
getProfileConfig,
|
||||
|
||||
8
src/shared/types.d.ts
vendored
8
src/shared/types.d.ts
vendored
@@ -236,12 +236,6 @@ interface ISysProxyConfig {
|
||||
pacScript?: string
|
||||
}
|
||||
|
||||
interface IAppProxyRule {
|
||||
processName: string
|
||||
processPath: string
|
||||
enabled: boolean
|
||||
}
|
||||
|
||||
interface IAppConfig {
|
||||
core: 'mihomo' | 'mihomo-alpha' | 'mihomo-smart' | 'mihomo-specific'
|
||||
specificVersion?: string
|
||||
@@ -291,8 +285,6 @@ interface IAppConfig {
|
||||
sysproxyCardStatus?: CardStatus
|
||||
tunCardStatus?: CardStatus
|
||||
usageCardStatus?: CardStatus
|
||||
appsCardStatus?: CardStatus
|
||||
appProxyRules?: IAppProxyRule[]
|
||||
githubToken?: string
|
||||
useSubStore: boolean
|
||||
subStoreHost?: string
|
||||
|
||||
Reference in New Issue
Block a user