trafficInfo

This commit is contained in:
pompurin404 2024-08-02 11:25:43 +08:00
parent 945ab611fe
commit 27f2f70f1b
No known key found for this signature in database
22 changed files with 195 additions and 122 deletions

View File

@ -1,12 +1,6 @@
export { appConfig, getAppConfig, setAppConfig } from './app' export { getAppConfig, setAppConfig } from './app'
export { getControledMihomoConfig, setControledMihomoConfig } from './controledMihomo'
export { export {
controledMihomoConfig,
getControledMihomoConfig,
setControledMihomoConfig
} from './controledMihomo'
export {
profileConfig,
currentProfile,
getCurrentProfile, getCurrentProfile,
getCurrentProfileItem, getCurrentProfileItem,
getProfileItem, getProfileItem,

View File

@ -1,12 +1,14 @@
import { controledMihomoConfig } from './controledMihomo' import { getControledMihomoConfig } from './controledMihomo'
import { profileConfigPath, profilePath } from '../utils/dirs' import { profileConfigPath, profilePath } from '../utils/dirs'
import { app } from 'electron' import { restartCore } from '../core/manager'
import { getAppConfig } from './app'
import { window } from '..'
import axios from 'axios' import axios from 'axios'
import yaml from 'yaml' import yaml from 'yaml'
import fs from 'fs' import fs from 'fs'
export let profileConfig: IProfileConfig // profile.yaml let profileConfig: IProfileConfig // profile.yaml
export let currentProfile: Partial<IMihomoConfig> // profiles/xxx.yaml let currentProfile: Partial<IMihomoConfig> // profiles/xxx.yaml
export function getProfileConfig(force = false): IProfileConfig { export function getProfileConfig(force = false): IProfileConfig {
if (force || !profileConfig) { if (force || !profileConfig) {
@ -16,19 +18,25 @@ export function getProfileConfig(force = false): IProfileConfig {
} }
export function getProfileItem(id: string | undefined): IProfileItem { export function getProfileItem(id: string | undefined): IProfileItem {
const items = profileConfig.items const items = getProfileConfig().items
return items?.find((item) => item.id === id) || { id: 'default', type: 'local', name: '空白订阅' } return items?.find((item) => item.id === id) || { id: 'default', type: 'local', name: '空白订阅' }
} }
export async function addProfileItem(item: Partial<IProfileItem>): Promise<void> { export async function addProfileItem(item: Partial<IProfileItem>): Promise<void> {
const newItem = await createProfile(item) const newItem = await createProfile(item)
profileConfig.items = getProfileConfig().items.filter((item) => item.id !== newItem.id)
profileConfig.items.push(newItem) profileConfig.items.push(newItem)
console.log(!profileConfig.current) let changeProfile = false
if (!profileConfig.current) { if (!getProfileConfig().current) {
profileConfig.current = newItem.id profileConfig.current = newItem.id
changeProfile = true
} }
console.log(profileConfig.current)
fs.writeFileSync(profileConfigPath(), yaml.stringify(profileConfig)) fs.writeFileSync(profileConfigPath(), yaml.stringify(profileConfig))
window?.webContents.send('profileConfigUpdated')
if (changeProfile) {
getCurrentProfile(true)
restartCore()
}
} }
export function removeProfileItem(id: string): void { export function removeProfileItem(id: string): void {
@ -37,10 +45,33 @@ export function removeProfileItem(id: string): void {
profileConfig.current = profileConfig.items[0]?.id profileConfig.current = profileConfig.items[0]?.id
} }
fs.writeFileSync(profileConfigPath(), yaml.stringify(profileConfig)) fs.writeFileSync(profileConfigPath(), yaml.stringify(profileConfig))
window?.webContents.send('profileConfigUpdated')
} }
export function getCurrentProfileItem(): IProfileItem { export function getCurrentProfileItem(): IProfileItem {
return getProfileItem(profileConfig.current) return getProfileItem(getProfileConfig().current)
}
// attachment;filename=xxx.yaml; filename*=UTF-8''%xx%xx%xx
function parseFilename(str: string): string {
if (str.includes("filename*=UTF-8''")) {
const filename = decodeURIComponent(str.split("filename*=UTF-8''")[1])
return filename
} else {
const filename = str.split('filename=')[1]
return filename
}
}
// subscription-userinfo: upload=1234; download=2234; total=1024000; expire=2218532293
function parseSubinfo(str: string): ISubscriptionUserInfo {
const parts = str.split('; ')
const obj = {} as ISubscriptionUserInfo
parts.forEach((part) => {
const [key, value] = part.split('=')
obj[key] = parseInt(value)
})
return obj
} }
export async function createProfile(item: Partial<IProfileItem>): Promise<IProfileItem> { export async function createProfile(item: Partial<IProfileItem>): Promise<IProfileItem> {
@ -50,6 +81,7 @@ export async function createProfile(item: Partial<IProfileItem>): Promise<IProfi
name: item.name || 'Local File', name: item.name || 'Local File',
type: item.type || 'local', type: item.type || 'local',
url: item.url, url: item.url,
interval: item.interval || 0,
updated: new Date().getTime() updated: new Date().getTime()
} as IProfileItem } as IProfileItem
switch (newItem.type) { switch (newItem.type) {
@ -58,32 +90,31 @@ export async function createProfile(item: Partial<IProfileItem>): Promise<IProfi
throw new Error('URL is required for remote profile') throw new Error('URL is required for remote profile')
} }
try { try {
const ua = getAppConfig().userAgent || 'clash-meta'
const res = await axios.get(item.url, { const res = await axios.get(item.url, {
proxy: { proxy: {
protocol: 'http', protocol: 'http',
host: '127.0.0.1', host: '127.0.0.1',
port: controledMihomoConfig['mixed-port'] || 7890 port: getControledMihomoConfig()['mixed-port'] || 7890
}, },
headers: { headers: {
'User-Agent': `Mihomo.Party.${app.getVersion()}` 'User-Agent': ua
}, },
responseType: 'text' responseType: 'text'
}) })
const data = res.data const data = res.data
const headers = res.headers const headers = res.headers
if (headers['content-disposition']) { if (headers['content-disposition']) {
newItem.name = headers['content-disposition'].split('filename=')[1] newItem.name = parseFilename(headers['content-disposition'])
}
if (headers['profile-web-page-url']) {
newItem.home = headers['profile-web-page-url']
}
if (headers['profile-update-interval']) {
newItem.interval = parseInt(headers['profile-update-interval']) * 60
} }
if (headers['subscription-userinfo']) { if (headers['subscription-userinfo']) {
const extra = headers['subscription-userinfo'] newItem.extra = parseSubinfo(headers['subscription-userinfo'])
.split(';')
.map((item: string) => item.split('=')[1].trim())
newItem.extra = {
upload: parseInt(extra[0]),
download: parseInt(extra[1]),
total: parseInt(extra[2]),
expire: parseInt(extra[3])
}
} }
fs.writeFileSync(profilePath(id), data, 'utf-8') fs.writeFileSync(profilePath(id), data, 'utf-8')
} catch (e) { } catch (e) {
@ -106,8 +137,9 @@ export async function createProfile(item: Partial<IProfileItem>): Promise<IProfi
export function getCurrentProfile(force = false): Partial<IMihomoConfig> { export function getCurrentProfile(force = false): Partial<IMihomoConfig> {
if (force || !currentProfile) { if (force || !currentProfile) {
if (profileConfig.current) { const current = getProfileConfig().current
currentProfile = yaml.parse(fs.readFileSync(profilePath(profileConfig.current), 'utf-8')) if (current) {
currentProfile = yaml.parse(fs.readFileSync(profilePath(current), 'utf-8'))
} else { } else {
currentProfile = yaml.parse(fs.readFileSync(profilePath('default'), 'utf-8')) currentProfile = yaml.parse(fs.readFileSync(profilePath('default'), 'utf-8'))
} }

View File

@ -1,12 +1,12 @@
import { ChildProcess, execSync, spawn } from 'child_process' import { ChildProcess, execSync, spawn } from 'child_process'
import { logPath, mihomoCorePath, mihomoWorkDir } from '../utils/dirs' import { logPath, mihomoCorePath, mihomoWorkDir } from '../utils/dirs'
import { generateProfile } from '../resolve/factory' import { generateProfile } from '../resolve/factory'
import { appConfig } from '../config' import { getAppConfig } from '../config'
import fs from 'fs' import fs from 'fs'
let child: ChildProcess let child: ChildProcess
export async function startCore(): Promise<void> { export async function startCore(): Promise<void> {
const corePath = mihomoCorePath(appConfig.core ?? 'mihomo') const corePath = mihomoCorePath(getAppConfig().core ?? 'mihomo')
generateProfile() generateProfile()
stopCore() stopCore()
if (process.platform !== 'win32') { if (process.platform !== 'win32') {

View File

@ -1,5 +1,5 @@
import axios, { AxiosInstance } from 'axios' import axios, { AxiosInstance } from 'axios'
import { controledMihomoConfig } from '../config' import { getControledMihomoConfig } from '../config'
import WebSocket from 'ws' import WebSocket from 'ws'
import { window } from '..' import { window } from '..'
@ -9,8 +9,8 @@ let mihomoTrafficWs: WebSocket = null!
export const getAxios = async (force: boolean = false): Promise<AxiosInstance> => { export const getAxios = async (force: boolean = false): Promise<AxiosInstance> => {
if (axiosIns && !force) return axiosIns if (axiosIns && !force) return axiosIns
let server = controledMihomoConfig['external-controller'] let server = getControledMihomoConfig()['external-controller']
const secret = controledMihomoConfig.secret ?? '' const secret = getControledMihomoConfig().secret ?? ''
if (server?.startsWith(':')) server = `127.0.0.1${server}` if (server?.startsWith(':')) server = `127.0.0.1${server}`
axiosIns = axios.create({ axiosIns = axios.create({
@ -49,8 +49,8 @@ export const mihomoRules = async (): Promise<IMihomoRulesInfo> => {
} }
export const mihomoTraffic = (): void => { export const mihomoTraffic = (): void => {
let server = controledMihomoConfig['external-controller'] let server = getControledMihomoConfig()['external-controller']
const secret = controledMihomoConfig.secret ?? '' const secret = getControledMihomoConfig().secret ?? ''
if (server?.startsWith(':')) server = `127.0.0.1${server}` if (server?.startsWith(':')) server = `127.0.0.1${server}`
mihomoTrafficWs = new WebSocket(`ws://${server}/traffic?secret=${secret}`) mihomoTrafficWs = new WebSocket(`ws://${server}/traffic?secret=${secret}`)

View File

@ -1,4 +1,9 @@
import { appConfig, controledMihomoConfig, setAppConfig, setControledMihomoConfig } from '../config' import {
getAppConfig,
getControledMihomoConfig,
setAppConfig,
setControledMihomoConfig
} from '../config'
import icoIcon from '../../../resources/icon.ico?asset' import icoIcon from '../../../resources/icon.ico?asset'
import pngIcon from '../../../resources/icon.png?asset' import pngIcon from '../../../resources/icon.png?asset'
import { patchMihomoConfig } from './mihomoApi' import { patchMihomoConfig } from './mihomoApi'
@ -24,7 +29,7 @@ const buildContextMenu = (): Menu => {
id: 'rule', id: 'rule',
label: '规则模式', label: '规则模式',
type: 'radio', type: 'radio',
checked: controledMihomoConfig.mode === 'rule', checked: getControledMihomoConfig().mode === 'rule',
click: (): void => { click: (): void => {
setControledMihomoConfig({ mode: 'rule' }) setControledMihomoConfig({ mode: 'rule' })
patchMihomoConfig({ mode: 'rule' }) patchMihomoConfig({ mode: 'rule' })
@ -36,7 +41,7 @@ const buildContextMenu = (): Menu => {
id: 'global', id: 'global',
label: '全局模式', label: '全局模式',
type: 'radio', type: 'radio',
checked: controledMihomoConfig.mode === 'global', checked: getControledMihomoConfig().mode === 'global',
click: (): void => { click: (): void => {
setControledMihomoConfig({ mode: 'global' }) setControledMihomoConfig({ mode: 'global' })
patchMihomoConfig({ mode: 'global' }) patchMihomoConfig({ mode: 'global' })
@ -48,7 +53,7 @@ const buildContextMenu = (): Menu => {
id: 'direct', id: 'direct',
label: '直连模式', label: '直连模式',
type: 'radio', type: 'radio',
checked: controledMihomoConfig.mode === 'direct', checked: getControledMihomoConfig().mode === 'direct',
click: (): void => { click: (): void => {
setControledMihomoConfig({ mode: 'direct' }) setControledMihomoConfig({ mode: 'direct' })
patchMihomoConfig({ mode: 'direct' }) patchMihomoConfig({ mode: 'direct' })
@ -60,7 +65,7 @@ const buildContextMenu = (): Menu => {
{ {
type: 'checkbox', type: 'checkbox',
label: '系统代理', label: '系统代理',
checked: appConfig.sysProxy?.enable ?? false, checked: getAppConfig().sysProxy?.enable ?? false,
click: (item): void => { click: (item): void => {
const enable = item.checked const enable = item.checked
setAppConfig({ sysProxy: { enable } }) setAppConfig({ sysProxy: { enable } })
@ -72,7 +77,7 @@ const buildContextMenu = (): Menu => {
{ {
type: 'checkbox', type: 'checkbox',
label: '虚拟网卡', label: '虚拟网卡',
checked: controledMihomoConfig.tun?.enable ?? false, checked: getControledMihomoConfig().tun?.enable ?? false,
click: (item): void => { click: (item): void => {
const enable = item.checked const enable = item.checked
setControledMihomoConfig({ tun: { enable } }) setControledMihomoConfig({ tun: { enable } })

View File

@ -6,7 +6,7 @@ import icon from '../../resources/icon.png?asset'
import { mihomoTraffic } from './core/mihomoApi' import { mihomoTraffic } from './core/mihomoApi'
import { createTray } from './core/tray' import { createTray } from './core/tray'
import { init } from './resolve/init' import { init } from './resolve/init'
import { appConfig } from './config' import { getAppConfig } from './config'
import { join } from 'path' import { join } from 'path'
export let window: BrowserWindow | null = null export let window: BrowserWindow | null = null
@ -80,7 +80,7 @@ function createWindow(): void {
}) })
window.on('ready-to-show', () => { window.on('ready-to-show', () => {
if (!appConfig.silentStart) { if (!getAppConfig().silentStart) {
window?.show() window?.show()
window?.focusOnWebView() window?.focusOnWebView()
} }

View File

@ -1,9 +1,9 @@
import { controledMihomoConfig, currentProfile } from '../config' import { getControledMihomoConfig, getCurrentProfile } from '../config'
import { mihomoWorkConfigPath } from '../utils/dirs' import { mihomoWorkConfigPath } from '../utils/dirs'
import yaml from 'yaml' import yaml from 'yaml'
import fs from 'fs' import fs from 'fs'
export function generateProfile(): void { export function generateProfile(): void {
const profile = Object.assign(currentProfile, controledMihomoConfig) const profile = Object.assign(getCurrentProfile(), getControledMihomoConfig())
fs.writeFileSync(mihomoWorkConfigPath(), yaml.stringify(profile)) fs.writeFileSync(mihomoWorkConfigPath(), yaml.stringify(profile))
} }

View File

@ -9,12 +9,6 @@ import {
profilesDir, profilesDir,
resourcesFilesDir resourcesFilesDir
} from '../utils/dirs' } from '../utils/dirs'
import {
getAppConfig,
getControledMihomoConfig,
getCurrentProfile,
getProfileConfig
} from '../config'
import { import {
defaultConfig, defaultConfig,
defaultControledMihomoConfig, defaultControledMihomoConfig,
@ -53,16 +47,12 @@ function initConfig(): void {
if (!fs.existsSync(controledMihomoConfigPath())) { if (!fs.existsSync(controledMihomoConfigPath())) {
fs.writeFileSync(controledMihomoConfigPath(), yaml.stringify(defaultControledMihomoConfig)) fs.writeFileSync(controledMihomoConfigPath(), yaml.stringify(defaultControledMihomoConfig))
} }
getAppConfig(true)
getControledMihomoConfig(true)
getProfileConfig(true)
getCurrentProfile(true)
} }
function initFiles(): void { function initFiles(): void {
const fileList = ['Country.mmdb', 'geoip.dat', 'geosite.dat'] const fileList = ['Country.mmdb', 'geoip.dat', 'geosite.dat']
for (const file of fileList) { for (const file of fileList) {
const targetPath = path.join(profilesDir(), file) const targetPath = path.join(mihomoWorkDir(), file)
const sourcePath = path.join(resourcesFilesDir(), file) const sourcePath = path.join(resourcesFilesDir(), file)
if (!fs.existsSync(targetPath) && fs.existsSync(sourcePath)) { if (!fs.existsSync(targetPath) && fs.existsSync(sourcePath)) {
fs.copyFileSync(sourcePath, targetPath) fs.copyFileSync(sourcePath, targetPath)

View File

@ -1,4 +1,4 @@
import { controledMihomoConfig } from '../config' import { getControledMihomoConfig } from '../config'
export function triggerSysProxy(enable: boolean): void { export function triggerSysProxy(enable: boolean): void {
if (enable) { if (enable) {
@ -9,7 +9,7 @@ export function triggerSysProxy(enable: boolean): void {
} }
export function enableSysProxy(): void { export function enableSysProxy(): void {
console.log('enableSysProxy', controledMihomoConfig['mixed-port']) console.log('enableSysProxy', getControledMihomoConfig()['mixed-port'])
} }
export function disableSysProxy(): void { export function disableSysProxy(): void {

View File

@ -25,7 +25,7 @@ export function registerIpcMainHandlers(): void {
ipcMain.handle('mihomoVersion', mihomoVersion) ipcMain.handle('mihomoVersion', mihomoVersion)
ipcMain.handle('mihomoConfig', mihomoConfig) ipcMain.handle('mihomoConfig', mihomoConfig)
ipcMain.handle('mihomoConnections', mihomoConnections) ipcMain.handle('mihomoConnections', mihomoConnections)
ipcMain.handle('mihomeRules', mihomoRules) ipcMain.handle('mihomoRules', mihomoRules)
ipcMain.handle('patchMihomoConfig', async (_e, patch) => await patchMihomoConfig(patch)) ipcMain.handle('patchMihomoConfig', async (_e, patch) => await patchMihomoConfig(patch))
ipcMain.handle('checkAutoRun', checkAutoRun) ipcMain.handle('checkAutoRun', checkAutoRun)
ipcMain.handle('enableAutoRun', enableAutoRun) ipcMain.handle('enableAutoRun', enableAutoRun)

View File

@ -14,6 +14,7 @@ import OverrideCard from '@renderer/components/sider/override-card'
import ConnCard from '@renderer/components/sider/conn-card' import ConnCard from '@renderer/components/sider/conn-card'
import LogCard from '@renderer/components/sider/log-card' import LogCard from '@renderer/components/sider/log-card'
import MihomoCoreCard from './components/sider/mihomo-core-card.tsx' import MihomoCoreCard from './components/sider/mihomo-core-card.tsx'
import TestCard from './components/sider/test-card.js'
const App: React.FC = () => { const App: React.FC = () => {
const { setTheme } = useTheme() const { setTheme } = useTheme()
@ -74,14 +75,15 @@ const App: React.FC = () => {
<ProfileCard /> <ProfileCard />
<ProxyCard /> <ProxyCard />
<MihomoCoreCard /> <MihomoCoreCard />
<ConnCard />
</div> </div>
<div className="flex justify-between mx-2"> <div className="flex justify-between mx-2">
<ConnCard />
<LogCard /> <LogCard />
<RuleCard />
</div> </div>
<div className="flex justify-between mx-2"> <div className="flex justify-between mx-2">
<RuleCard /> <TestCard />
<OverrideCard /> <OverrideCard />
</div> </div>
{/* </div> */} {/* </div> */}

View File

@ -1,22 +1,22 @@
import { Button, Card, CardBody, CardFooter, Chip } from '@nextui-org/react' import { Button, Card, CardBody, CardFooter } from '@nextui-org/react'
import { FaCircleArrowDown, FaCircleArrowUp } from 'react-icons/fa6'
import { useLocation, useNavigate } from 'react-router-dom' import { useLocation, useNavigate } from 'react-router-dom'
import { calcTraffic } from '@renderer/utils/calc'
import { useEffect, useState } from 'react'
import { IoLink } from 'react-icons/io5' import { IoLink } from 'react-icons/io5'
import { useEffect } from 'react'
import useSWR from 'swr'
import { mihomoConnections } from '@renderer/utils/ipc'
const ConnCard: React.FC = () => { const ConnCard: React.FC = () => {
const navigate = useNavigate() const navigate = useNavigate()
const location = useLocation() const location = useLocation()
const match = location.pathname.includes('/connections') const match = location.pathname.includes('/connections')
const { data: connections } = useSWR<IMihomoConnectionsInfo>('/connections', mihomoConnections, { const [upload, setUpload] = useState(0)
refreshInterval: 5000 const [download, setDownload] = useState(0)
})
useEffect(() => { useEffect(() => {
window.electron.ipcRenderer.on('mihomoTraffic', (_e, info: IMihomoTrafficInfo) => { window.electron.ipcRenderer.on('mihomoTraffic', (_e, info: IMihomoTrafficInfo) => {
console.log(info) setUpload(info.up)
setDownload(info.down)
}) })
return (): void => { return (): void => {
window.electron.ipcRenderer.removeAllListeners('mihomoTraffic') window.electron.ipcRenderer.removeAllListeners('mihomoTraffic')
@ -25,7 +25,8 @@ const ConnCard: React.FC = () => {
return ( return (
<Card <Card
className={`w-[50%] mr-1 mb-2 ${match ? 'bg-primary' : ''}`} fullWidth
className={`mb-2 ${match ? 'bg-primary' : ''}`}
isPressable isPressable
onPress={() => navigate('/connections')} onPress={() => navigate('/connections')}
> >
@ -39,24 +40,16 @@ const ConnCard: React.FC = () => {
> >
<IoLink color="default" className="text-[20px]" /> <IoLink color="default" className="text-[20px]" />
</Button> </Button>
<Chip <div className="p-2 w-full">
classNames={ <div className="flex justify-between">
match <div className="w-full text-right mr-2">{calcTraffic(upload)}/s</div>
? { <FaCircleArrowUp className="h-[24px] leading-[24px]" />
base: 'border-foreground', </div>
content: 'text-foreground' <div className="flex justify-between">
} <div className="w-full text-right mr-2">{calcTraffic(download)}/s</div>
: { <FaCircleArrowDown className="h-[24px] leading-[24px]" />
base: 'border-primary', </div>
content: 'text-primary' </div>
}
}
size="sm"
variant="bordered"
className="mr-3 mt-2"
>
{connections?.connections?.length ?? 0}
</Chip>
</div> </div>
</CardBody> </CardBody>
<CardFooter className="pt-1"> <CardFooter className="pt-1">

View File

@ -8,7 +8,7 @@ const LogCard: React.FC = () => {
const match = location.pathname.includes('/logs') const match = location.pathname.includes('/logs')
return ( return (
<Card <Card
className={`w-[50%] ml-1 mb-2 ${match ? 'bg-primary' : ''}`} className={`w-[50%] mr-1 mb-2 ${match ? 'bg-primary' : ''}`}
isPressable isPressable
onPress={() => navigate('/logs')} onPress={() => navigate('/logs')}
> >

View File

@ -1,4 +1,4 @@
import { Button, Card, CardBody, CardFooter, cn } from '@nextui-org/react' import { Button, Card, CardBody, CardFooter } from '@nextui-org/react'
import React, { useState } from 'react' import React, { useState } from 'react'
import { MdFormatOverline } from 'react-icons/md' import { MdFormatOverline } from 'react-icons/md'
import { useLocation, useNavigate } from 'react-router-dom' import { useLocation, useNavigate } from 'react-router-dom'

View File

@ -1,29 +1,25 @@
import { Button, Card, CardBody, CardFooter, Progress } from '@nextui-org/react' import { Button, Card, CardBody, CardFooter, Progress } from '@nextui-org/react'
import { getCurrentProfileItem } from '@renderer/utils/ipc' import { useProfileConfig } from '@renderer/hooks/use-profile'
import { useEffect } from 'react'
import { IoMdRefresh } from 'react-icons/io'
import { useLocation, useNavigate } from 'react-router-dom' import { useLocation, useNavigate } from 'react-router-dom'
import { calcTraffic } from '@renderer/utils/calc' import { calcTraffic } from '@renderer/utils/calc'
import useSWR from 'swr' import { IoMdRefresh } from 'react-icons/io'
const ProfileCard: React.FC = () => { const ProfileCard: React.FC = () => {
const navigate = useNavigate() const navigate = useNavigate()
const location = useLocation() const location = useLocation()
const match = location.pathname.includes('/profiles') const match = location.pathname.includes('/profiles')
const { data: info, mutate } = useSWR('getCurrentProfileItem', getCurrentProfileItem) const { profileConfig } = useProfileConfig()
const { current, items } = profileConfig ?? {}
const info = items?.find((item) => item.id === current) ?? {
id: 'default',
type: 'local',
name: '空白订阅'
}
const extra = info?.extra const extra = info?.extra
const usage = (extra?.upload ?? 0) + (extra?.download ?? 0) const usage = (extra?.upload ?? 0) + (extra?.download ?? 0)
const total = extra?.total ?? 0 const total = extra?.total ?? 0
useEffect(() => {
window.electron.ipcRenderer.on('profileConfigUpdated', () => {
mutate()
})
return (): void => {
window.electron.ipcRenderer.removeAllListeners('profileConfigUpdated')
}
})
return ( return (
<Card <Card
fullWidth fullWidth
@ -43,8 +39,8 @@ const ProfileCard: React.FC = () => {
</CardBody> </CardBody>
<CardFooter className="pt-1"> <CardFooter className="pt-1">
<Progress <Progress
classNames={{ indicator: 'bg-foreground' }} classNames={{ indicator: 'bg-foreground', label: 'select-none' }}
label={`${calcTraffic(usage)}/${calcTraffic(total)}`} label={extra ? `${calcTraffic(usage)}/${calcTraffic(total)}` : undefined}
value={calcPercent(extra?.upload, extra?.download, extra?.total)} value={calcPercent(extra?.upload, extra?.download, extra?.total)}
className="max-w-md" className="max-w-md"
/> />

View File

@ -8,13 +8,13 @@ const RuleCard: React.FC = () => {
const navigate = useNavigate() const navigate = useNavigate()
const location = useLocation() const location = useLocation()
const match = location.pathname.includes('/rules') const match = location.pathname.includes('/rules')
const { data: rules } = useSWR<IMihomoRulesInfo>('/connections', mihomoRules, { const { data: rules } = useSWR<IMihomoRulesInfo>('/rules', mihomoRules, {
refreshInterval: 5000 refreshInterval: 5000
}) })
return ( return (
<Card <Card
className={`w-[50%] mr-1 mb-2 ${match ? 'bg-primary' : ''}`} className={`w-[50%] ml-1 mb-2 ${match ? 'bg-primary' : ''}`}
isPressable isPressable
onPress={() => navigate('/rules')} onPress={() => navigate('/rules')}
> >

View File

@ -0,0 +1,36 @@
import { Button, Card, CardBody, CardFooter } from '@nextui-org/react'
import React from 'react'
import { TbWorldCheck } from 'react-icons/tb'
import { useLocation, useNavigate } from 'react-router-dom'
const TestCard: React.FC = () => {
const navigate = useNavigate()
const location = useLocation()
const match = location.pathname.includes('/tests')
return (
<Card
className={`w-[50%] mr-1 mb-2 ${match ? 'bg-primary' : ''}`}
isPressable
onPress={() => navigate('/tests')}
>
<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"
>
<TbWorldCheck color="default" className="text-[20px]" />
</Button>
</div>
</CardBody>
<CardFooter className="pt-1">
<h3 className="select-none text-md font-bold"></h3>
</CardFooter>
</Card>
)
}
export default TestCard

View File

@ -21,15 +21,12 @@ export const useProfileConfig = (): RetuenType => {
const addProfileItem = async (item: Partial<IProfileItem>): Promise<void> => { const addProfileItem = async (item: Partial<IProfileItem>): Promise<void> => {
await add(item) await add(item)
mutateProfileConfig() mutateProfileConfig()
window.electron.ipcRenderer.send('profileConfigUpdated')
} }
const removeProfileItem = async (id: string): Promise<void> => { const removeProfileItem = async (id: string): Promise<void> => {
await remove(id) await remove(id)
mutateProfileConfig() mutateProfileConfig()
window.electron.ipcRenderer.send('profileConfigUpdated')
} }
useEffect(() => { useEffect(() => {
window.electron.ipcRenderer.on('profileConfigUpdated', () => { window.electron.ipcRenderer.on('profileConfigUpdated', () => {
mutateProfileConfig() mutateProfileConfig()

View File

@ -0,0 +1,5 @@
const Tests: React.FC = () => {
return <div>Tests</div>
}
export default Tests

View File

@ -9,6 +9,7 @@ import Connections from '@renderer/pages/connections'
import Mihomo from '@renderer/pages/mihomo' import Mihomo from '@renderer/pages/mihomo'
import Sysproxy from '@renderer/pages/syspeoxy' import Sysproxy from '@renderer/pages/syspeoxy'
import Tun from '@renderer/pages/tun' import Tun from '@renderer/pages/tun'
import Tests from '@renderer/pages/tests'
const routes = [ const routes = [
{ {
@ -31,6 +32,10 @@ const routes = [
path: '/rules', path: '/rules',
element: <Rules /> element: <Rules />
}, },
{
path: '/tests',
element: <Tests />
},
{ {
path: '/logs', path: '/logs',
element: <Logs /> element: <Logs />

View File

@ -1,6 +1,19 @@
export function calcTraffic(bit: number): string { export function calcTraffic(bit: number): string {
if (bit < 1024) return `${bit} B` if (bit < 1024) return `${bit} B`
if (bit < 1024 * 1024) return `${(bit / 1024).toFixed(2)} KB` bit /= 1024
if (bit < 1024 * 1024 * 1024) return `${(bit / 1024 / 1024).toFixed(2)} MB` if (bit < 1024) return `${bit.toFixed(2)} KB`
return `${(bit / 1024 / 1024 / 1024).toFixed(2)} GB` bit /= 1024
if (bit < 1024) return `${bit.toFixed(2)} MB`
bit /= 1024
if (bit < 1024) return `${bit.toFixed(2)} GB`
bit /= 1024
if (bit < 1024) return `${bit.toFixed(2)} TB`
bit /= 1024
if (bit < 1024) return `${bit.toFixed(2)} PB`
bit /= 1024
if (bit < 1024) return `${bit.toFixed(2)} EB`
bit /= 1024
if (bit < 1024) return `${bit.toFixed(2)} ZB`
bit /= 1024
return `${bit.toFixed(2)} YB`
} }

17
src/shared/types.d.ts vendored
View File

@ -74,6 +74,7 @@ interface IAppConfig {
core: 'mihomo' | 'mihomo-alpha' core: 'mihomo' | 'mihomo-alpha'
silentStart: boolean silentStart: boolean
sysProxy: ISysProxyConfig sysProxy: ISysProxyConfig
userAgent?: string
} }
interface IMihomoTunConfig { interface IMihomoTunConfig {
@ -127,17 +128,21 @@ interface IProfileConfig {
items: IProfileItem[] items: IProfileItem[]
} }
interface ISubscriptionUserInfo {
upload: number
download: number
total: number
expire: number
}
interface IProfileItem { interface IProfileItem {
id: string id: string
type: 'remote' | 'local' type: 'remote' | 'local'
name: string name: string
url?: string // remote url?: string // remote
file?: string // local file?: string // local
interval?: number
home?: string
updated?: number updated?: number
extra?: { extra?: ISubscriptionUserInfo
upload: number
download: number
total: number
expire: number
}
} }