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
|
party.md
|
||||||
CLAUDE.md
|
CLAUDE.md
|
||||||
tsconfig.node.tsbuildinfo
|
tsconfig.node.tsbuildinfo
|
||||||
.omc
|
|
||||||
.claude
|
|
||||||
debug
|
|
||||||
|
|||||||
@@ -4,6 +4,3 @@ pnpm-lock.yaml
|
|||||||
LICENSE.md
|
LICENSE.md
|
||||||
tsconfig.json
|
tsconfig.json
|
||||||
tsconfig.*.json
|
tsconfig.*.json
|
||||||
.omc
|
|
||||||
.claude
|
|
||||||
debug
|
|
||||||
|
|||||||
@@ -39,8 +39,3 @@ export async function patchAppConfig(patch: Partial<IAppConfig>): Promise<void>
|
|||||||
})
|
})
|
||||||
await appConfigWriteQueue
|
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 { getControledMihomoConfig, patchControledMihomoConfig } from './controledMihomo'
|
||||||
export {
|
export {
|
||||||
getProfile,
|
getProfile,
|
||||||
|
|||||||
@@ -73,9 +73,7 @@ export async function generateProfile(): Promise<string | undefined> {
|
|||||||
const overrideIds = await getOrderedOverrideIds(current)
|
const overrideIds = await getOrderedOverrideIds(current)
|
||||||
const profileWithNormalOverride = await applyOverrides(baseProfile, overrideIds.normal)
|
const profileWithNormalOverride = await applyOverrides(baseProfile, overrideIds.normal)
|
||||||
const profileWithRuleOverride = await applyRuleOverride(current, profileWithNormalOverride)
|
const profileWithRuleOverride = await applyRuleOverride(current, profileWithNormalOverride)
|
||||||
const { appProxyRules = [] } = await getAppConfig()
|
const currentProfile = await applyOverrides(profileWithRuleOverride, overrideIds.smart)
|
||||||
const profileWithAppProxy = applyAppProxyRules(profileWithRuleOverride, appProxyRules)
|
|
||||||
const currentProfile = await applyOverrides(profileWithAppProxy, overrideIds.smart)
|
|
||||||
let controledMihomoConfig = await getControledMihomoConfig()
|
let controledMihomoConfig = await getControledMihomoConfig()
|
||||||
|
|
||||||
// 根据开关状态过滤控制配置
|
// 根据开关状态过滤控制配置
|
||||||
@@ -117,18 +115,6 @@ export async function generateProfile(): Promise<string | undefined> {
|
|||||||
return current
|
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(
|
async function applyRuleOverride(
|
||||||
current: string | undefined,
|
current: string | undefined,
|
||||||
profile: IMihomoConfig
|
profile: IMihomoConfig
|
||||||
|
|||||||
@@ -33,7 +33,6 @@ import {
|
|||||||
patchAppConfig,
|
patchAppConfig,
|
||||||
getControledMihomoConfig,
|
getControledMihomoConfig,
|
||||||
patchControledMihomoConfig,
|
patchControledMihomoConfig,
|
||||||
getAppProxyRules,
|
|
||||||
getProfileConfig,
|
getProfileConfig,
|
||||||
getCurrentProfileItem,
|
getCurrentProfileItem,
|
||||||
getProfileItem,
|
getProfileItem,
|
||||||
@@ -250,7 +249,7 @@ const asyncHandlers: Record<string, AsyncFn> = {
|
|||||||
patchAppConfig,
|
patchAppConfig,
|
||||||
getControledMihomoConfig,
|
getControledMihomoConfig,
|
||||||
patchControledMihomoConfig,
|
patchControledMihomoConfig,
|
||||||
getAppProxyRules,
|
// Profile
|
||||||
getProfileConfig,
|
getProfileConfig,
|
||||||
setProfileConfig,
|
setProfileConfig,
|
||||||
getCurrentProfileItem,
|
getCurrentProfileItem,
|
||||||
|
|||||||
@@ -40,7 +40,6 @@ export const defaultConfig: IAppConfig = {
|
|||||||
'resource',
|
'resource',
|
||||||
'override',
|
'override',
|
||||||
'connection',
|
'connection',
|
||||||
'apps',
|
|
||||||
'mihomo',
|
'mihomo',
|
||||||
'dns',
|
'dns',
|
||||||
'sniff',
|
'sniff',
|
||||||
@@ -63,9 +62,7 @@ export const defaultConfig: IAppConfig = {
|
|||||||
enableTproxyPort: false,
|
enableTproxyPort: false,
|
||||||
testProfileOnStart: true,
|
testProfileOnStart: true,
|
||||||
useHotReloadProfile: false,
|
useHotReloadProfile: false,
|
||||||
hotReloadProfileAutoCloseConnection: false,
|
hotReloadProfileAutoCloseConnection: false
|
||||||
appProxyRules: [],
|
|
||||||
appsCardStatus: 'col-span-1'
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export const defaultControledMihomoConfig: Partial<IMihomoConfig> = {
|
export const defaultControledMihomoConfig: Partial<IMihomoConfig> = {
|
||||||
|
|||||||
@@ -33,7 +33,6 @@ const validInvokeChannels = [
|
|||||||
'patchAppConfig',
|
'patchAppConfig',
|
||||||
'getControledMihomoConfig',
|
'getControledMihomoConfig',
|
||||||
'patchControledMihomoConfig',
|
'patchControledMihomoConfig',
|
||||||
'getAppProxyRules',
|
|
||||||
'resetAppConfig',
|
'resetAppConfig',
|
||||||
// Profile
|
// Profile
|
||||||
'getProfileConfig',
|
'getProfileConfig',
|
||||||
|
|||||||
@@ -34,7 +34,6 @@ import { TitleBarOverlayOptions } from 'electron'
|
|||||||
import SubStoreCard from '@renderer/components/sider/substore-card'
|
import SubStoreCard from '@renderer/components/sider/substore-card'
|
||||||
import NetworkCard from '@renderer/components/sider/network-card'
|
import NetworkCard from '@renderer/components/sider/network-card'
|
||||||
import UsageCard from '@renderer/components/sider/usage-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 { useTrafficLogger } from '@renderer/hooks/use-traffic-logger'
|
||||||
import { createTourDriver, getDriver, startTourIfNeeded } from '@renderer/utils/tour'
|
import { createTourDriver, getDriver, startTourIfNeeded } from '@renderer/utils/tour'
|
||||||
import 'driver.js/dist/driver.css'
|
import 'driver.js/dist/driver.css'
|
||||||
@@ -54,7 +53,6 @@ const ALL_SIDER_KEYS = [
|
|||||||
'resource',
|
'resource',
|
||||||
'override',
|
'override',
|
||||||
'connection',
|
'connection',
|
||||||
'apps',
|
|
||||||
'mihomo',
|
'mihomo',
|
||||||
'dns',
|
'dns',
|
||||||
'sniff',
|
'sniff',
|
||||||
@@ -88,7 +86,6 @@ const App: React.FC = () => {
|
|||||||
'resource',
|
'resource',
|
||||||
'override',
|
'override',
|
||||||
'connection',
|
'connection',
|
||||||
'apps',
|
|
||||||
'mihomo',
|
'mihomo',
|
||||||
'dns',
|
'dns',
|
||||||
'sniff',
|
'sniff',
|
||||||
@@ -191,7 +188,6 @@ const App: React.FC = () => {
|
|||||||
proxy: 'proxies',
|
proxy: 'proxies',
|
||||||
mihomo: 'mihomo',
|
mihomo: 'mihomo',
|
||||||
connection: 'connections',
|
connection: 'connections',
|
||||||
apps: 'apps',
|
|
||||||
dns: 'dns',
|
dns: 'dns',
|
||||||
sniff: 'sniffer',
|
sniff: 'sniffer',
|
||||||
log: 'logs',
|
log: 'logs',
|
||||||
@@ -210,7 +206,6 @@ const App: React.FC = () => {
|
|||||||
proxy: ProxyCard,
|
proxy: ProxyCard,
|
||||||
mihomo: MihomoCoreCard,
|
mihomo: MihomoCoreCard,
|
||||||
connection: ConnCard,
|
connection: ConnCard,
|
||||||
apps: AppsCard,
|
|
||||||
dns: DNSCard,
|
dns: DNSCard,
|
||||||
sniff: SniffCard,
|
sniff: SniffCard,
|
||||||
log: LogCard,
|
log: LogCard,
|
||||||
|
|||||||
@@ -267,7 +267,7 @@ const NetworkTopologyCard: React.FC = () => {
|
|||||||
}
|
}
|
||||||
window.electron.ipcRenderer.on('mihomoConnections', handler)
|
window.electron.ipcRenderer.on('mihomoConnections', handler)
|
||||||
return () => {
|
return () => {
|
||||||
window.electron.ipcRenderer.removeListener('mihomoConnections', handler)
|
window.electron.ipcRenderer.removeAllListeners('mihomoConnections')
|
||||||
}
|
}
|
||||||
}, [isPaused])
|
}, [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.logs": "Logs",
|
||||||
"sider.cards.substore": "Sub-Store",
|
"sider.cards.substore": "Sub-Store",
|
||||||
"sider.cards.network": "Network Info",
|
"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.title": "Network Info",
|
||||||
"network.ipCard.title": "Current IP",
|
"network.ipCard.title": "Current IP",
|
||||||
"network.ipAddress": "IP Address",
|
"network.ipAddress": "IP Address",
|
||||||
|
|||||||
@@ -298,14 +298,6 @@
|
|||||||
"sider.cards.logs": "گزارشها",
|
"sider.cards.logs": "گزارشها",
|
||||||
"sider.cards.substore": "ساب استور",
|
"sider.cards.substore": "ساب استور",
|
||||||
"sider.cards.network": "اطلاعات شبکه",
|
"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.title": "اطلاعات شبکه",
|
||||||
"network.ipCard.title": "IP فعلی",
|
"network.ipCard.title": "IP فعلی",
|
||||||
"network.ipAddress": "آدرس IP",
|
"network.ipAddress": "آدرس IP",
|
||||||
|
|||||||
@@ -300,14 +300,6 @@
|
|||||||
"sider.cards.logs": "Журналы",
|
"sider.cards.logs": "Журналы",
|
||||||
"sider.cards.substore": "Sub-Store",
|
"sider.cards.substore": "Sub-Store",
|
||||||
"sider.cards.network": "Сетевая информация",
|
"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.title": "Сетевая информация",
|
||||||
"network.ipCard.title": "Текущий IP",
|
"network.ipCard.title": "Текущий IP",
|
||||||
"network.ipAddress": "IP-адрес",
|
"network.ipAddress": "IP-адрес",
|
||||||
|
|||||||
@@ -323,14 +323,6 @@
|
|||||||
"sider.cards.logs": "日志",
|
"sider.cards.logs": "日志",
|
||||||
"sider.cards.substore": "Sub-Store",
|
"sider.cards.substore": "Sub-Store",
|
||||||
"sider.cards.network": "网络信息",
|
"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.title": "网络信息",
|
||||||
"network.ipCard.title": "当前 IP",
|
"network.ipCard.title": "当前 IP",
|
||||||
"network.ipAddress": "IP 地址",
|
"network.ipAddress": "IP 地址",
|
||||||
|
|||||||
@@ -323,14 +323,6 @@
|
|||||||
"sider.cards.logs": "日誌",
|
"sider.cards.logs": "日誌",
|
||||||
"sider.cards.substore": "Sub-Store",
|
"sider.cards.substore": "Sub-Store",
|
||||||
"sider.cards.network": "網路資訊",
|
"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.title": "網路資訊",
|
||||||
"network.ipCard.title": "目前 IP",
|
"network.ipCard.title": "目前 IP",
|
||||||
"network.ipAddress": "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 => {
|
return (): void => {
|
||||||
window.electron.ipcRenderer.removeListener('mihomoConnections', handler)
|
window.electron.ipcRenderer.removeAllListeners('mihomoConnections')
|
||||||
}
|
}
|
||||||
}, [isPaused])
|
}, [isPaused])
|
||||||
const togglePause = useCallback(() => {
|
const togglePause = useCallback(() => {
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import Settings from '@renderer/pages/settings'
|
|||||||
import Profiles from '@renderer/pages/profiles'
|
import Profiles from '@renderer/pages/profiles'
|
||||||
import Logs from '@renderer/pages/logs'
|
import Logs from '@renderer/pages/logs'
|
||||||
import Connections from '@renderer/pages/connections'
|
import Connections from '@renderer/pages/connections'
|
||||||
import Apps from '@renderer/pages/apps'
|
|
||||||
import Mihomo from '@renderer/pages/mihomo'
|
import Mihomo from '@renderer/pages/mihomo'
|
||||||
import Sysproxy from '@renderer/pages/sysproxy'
|
import Sysproxy from '@renderer/pages/sysproxy'
|
||||||
import Tun from '@renderer/pages/tun'
|
import Tun from '@renderer/pages/tun'
|
||||||
@@ -61,10 +60,6 @@ const routes = [
|
|||||||
path: '/connections',
|
path: '/connections',
|
||||||
element: <Connections />
|
element: <Connections />
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: '/apps',
|
|
||||||
element: <Apps />
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: '/override',
|
path: '/override',
|
||||||
element: <Override />
|
element: <Override />
|
||||||
|
|||||||
@@ -46,7 +46,6 @@ interface IpcApi {
|
|||||||
patchAppConfig: (patch: Partial<IAppConfig>) => Promise<void>
|
patchAppConfig: (patch: Partial<IAppConfig>) => Promise<void>
|
||||||
getControledMihomoConfig: (force?: boolean) => Promise<Partial<IMihomoConfig>>
|
getControledMihomoConfig: (force?: boolean) => Promise<Partial<IMihomoConfig>>
|
||||||
patchControledMihomoConfig: (patch: Partial<IMihomoConfig>) => Promise<void>
|
patchControledMihomoConfig: (patch: Partial<IMihomoConfig>) => Promise<void>
|
||||||
getAppProxyRules: () => Promise<IAppProxyRule[]>
|
|
||||||
resetAppConfig: () => Promise<void>
|
resetAppConfig: () => Promise<void>
|
||||||
// Profile
|
// Profile
|
||||||
getProfileConfig: (force?: boolean) => Promise<IProfileConfig>
|
getProfileConfig: (force?: boolean) => Promise<IProfileConfig>
|
||||||
@@ -204,7 +203,6 @@ export const {
|
|||||||
patchAppConfig,
|
patchAppConfig,
|
||||||
getControledMihomoConfig,
|
getControledMihomoConfig,
|
||||||
patchControledMihomoConfig,
|
patchControledMihomoConfig,
|
||||||
getAppProxyRules,
|
|
||||||
resetAppConfig,
|
resetAppConfig,
|
||||||
// Profile
|
// Profile
|
||||||
getProfileConfig,
|
getProfileConfig,
|
||||||
|
|||||||
8
src/shared/types.d.ts
vendored
8
src/shared/types.d.ts
vendored
@@ -236,12 +236,6 @@ interface ISysProxyConfig {
|
|||||||
pacScript?: string
|
pacScript?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IAppProxyRule {
|
|
||||||
processName: string
|
|
||||||
processPath: string
|
|
||||||
enabled: boolean
|
|
||||||
}
|
|
||||||
|
|
||||||
interface IAppConfig {
|
interface IAppConfig {
|
||||||
core: 'mihomo' | 'mihomo-alpha' | 'mihomo-smart' | 'mihomo-specific'
|
core: 'mihomo' | 'mihomo-alpha' | 'mihomo-smart' | 'mihomo-specific'
|
||||||
specificVersion?: string
|
specificVersion?: string
|
||||||
@@ -291,8 +285,6 @@ interface IAppConfig {
|
|||||||
sysproxyCardStatus?: CardStatus
|
sysproxyCardStatus?: CardStatus
|
||||||
tunCardStatus?: CardStatus
|
tunCardStatus?: CardStatus
|
||||||
usageCardStatus?: CardStatus
|
usageCardStatus?: CardStatus
|
||||||
appsCardStatus?: CardStatus
|
|
||||||
appProxyRules?: IAppProxyRule[]
|
|
||||||
githubToken?: string
|
githubToken?: string
|
||||||
useSubStore: boolean
|
useSubStore: boolean
|
||||||
subStoreHost?: string
|
subStoreHost?: string
|
||||||
|
|||||||
Reference in New Issue
Block a user