mirror of
https://gh.catmak.name/https://github.com/mihomo-party-org/mihomo-party
synced 2025-12-27 13:10:30 +08:00
trafficInfo
This commit is contained in:
parent
945ab611fe
commit
27f2f70f1b
@ -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,
|
||||||
|
|||||||
@ -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'))
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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') {
|
||||||
|
|||||||
@ -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}`)
|
||||||
|
|||||||
@ -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 } })
|
||||||
|
|||||||
@ -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()
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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))
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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 {
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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> */}
|
||||||
|
|||||||
@ -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">
|
||||||
|
|||||||
@ -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')}
|
||||||
>
|
>
|
||||||
|
|||||||
@ -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'
|
||||||
|
|||||||
@ -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"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -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')}
|
||||||
>
|
>
|
||||||
|
|||||||
36
src/renderer/src/components/sider/test-card.tsx
Normal file
36
src/renderer/src/components/sider/test-card.tsx
Normal 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
|
||||||
@ -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()
|
||||||
|
|||||||
5
src/renderer/src/pages/tests.tsx
Normal file
5
src/renderer/src/pages/tests.tsx
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
const Tests: React.FC = () => {
|
||||||
|
return <div>Tests</div>
|
||||||
|
}
|
||||||
|
|
||||||
|
export default Tests
|
||||||
@ -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 />
|
||||||
|
|||||||
@ -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
17
src/shared/types.d.ts
vendored
@ -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
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user