diff --git a/resources/icon.ico b/resources/icon.ico index 93b62a3..196c6bc 100644 Binary files a/resources/icon.ico and b/resources/icon.ico differ diff --git a/resources/icon.png b/resources/icon.png index 3219053..7558a9b 100644 Binary files a/resources/icon.png and b/resources/icon.png differ diff --git a/src/main/cmds.ts b/src/main/cmds.ts index a0ec5be..d80d9b5 100644 --- a/src/main/cmds.ts +++ b/src/main/cmds.ts @@ -4,9 +4,11 @@ import { checkAutoRun, disableAutoRun, enableAutoRun } from './autoRun' import { getAppConfig, setAppConfig, + getProfileConfig, getControledMihomoConfig, setControledMihomoConfig } from './config' +import { restartCore } from './manager' export function registerIpcMainHandlers(): void { ipcMain.handle('mihomoVersion', mihomoVersion) @@ -21,4 +23,6 @@ export function registerIpcMainHandlers(): void { ipcMain.handle('setControledMihomoConfig', (_e, config) => { setControledMihomoConfig(config) }) + ipcMain.handle('getProfileConfig', (_e, force) => getProfileConfig(force)) + ipcMain.handle('restartCore', () => restartCore()) } diff --git a/src/main/config.ts b/src/main/config.ts index a715961..01b2e32 100644 --- a/src/main/config.ts +++ b/src/main/config.ts @@ -1,50 +1,80 @@ import yaml from 'yaml' import fs from 'fs' -import { app } from 'electron' -import path from 'path' -import { defaultConfig } from './template' - -const dataDir = app.getPath('userData') -const appConfigPath = path.join(dataDir, 'config.yaml') -const controledMihomoConfigPath = path.join(dataDir, 'mihomo.yaml') +import { + defaultConfig, + defaultControledMihomoConfig, + defaultProfile, + defaultProfileConfig +} from './template' +import { appConfigPath, controledMihomoConfigPath, profileConfigPath, profilePath } from './dirs' export let appConfig: IAppConfig +export let profileConfig: IProfileConfig +export let currentProfile: Partial export let controledMihomoConfig: Partial export function initConfig(): void { - if (!fs.existsSync(dataDir)) { - fs.mkdirSync(dataDir) + if (!fs.existsSync(appConfigPath())) { + fs.writeFileSync(appConfigPath(), yaml.stringify(defaultConfig)) } - if (!fs.existsSync(appConfigPath)) { - fs.writeFileSync(appConfigPath, yaml.stringify(defaultConfig)) + if (!fs.existsSync(profileConfigPath())) { + fs.writeFileSync(profileConfigPath(), yaml.stringify(defaultProfileConfig)) } - if (!fs.existsSync(controledMihomoConfigPath)) { - fs.writeFileSync(controledMihomoConfigPath, yaml.stringify({})) + if (!fs.existsSync(profilePath('default'))) { + fs.writeFileSync(profilePath('default'), yaml.stringify(defaultProfile)) + } + if (!fs.existsSync(controledMihomoConfigPath())) { + fs.writeFileSync(controledMihomoConfigPath(), yaml.stringify(defaultControledMihomoConfig)) } getAppConfig(true) getControledMihomoConfig(true) + getProfileConfig(true) + getCurrentProfile(true) } export function getAppConfig(force = false): IAppConfig { if (force || !appConfig) { - appConfig = yaml.parse(fs.readFileSync(appConfigPath, 'utf-8')) + appConfig = yaml.parse(fs.readFileSync(appConfigPath(), 'utf-8')) } return appConfig } export function setAppConfig(patch: Partial): void { appConfig = Object.assign(appConfig, patch) - fs.writeFileSync(appConfigPath, yaml.stringify(appConfig)) + fs.writeFileSync(appConfigPath(), yaml.stringify(appConfig)) } export function getControledMihomoConfig(force = false): Partial { if (force || !controledMihomoConfig) { - controledMihomoConfig = yaml.parse(fs.readFileSync(controledMihomoConfigPath, 'utf-8')) + controledMihomoConfig = yaml.parse(fs.readFileSync(controledMihomoConfigPath(), 'utf-8')) } return controledMihomoConfig } export function setControledMihomoConfig(patch: Partial): void { controledMihomoConfig = Object.assign(controledMihomoConfig, patch) - fs.writeFileSync(controledMihomoConfigPath, yaml.stringify(controledMihomoConfig)) + fs.writeFileSync(controledMihomoConfigPath(), yaml.stringify(controledMihomoConfig)) +} + +export function getProfileConfig(force = false): IProfileConfig { + if (force || !profileConfig) { + profileConfig = yaml.parse(fs.readFileSync(profileConfigPath(), 'utf-8')) + } + return profileConfig +} + +export function setProfileConfig(patch: Partial): void { + profileConfig = Object.assign(profileConfig, patch) + fs.writeFileSync(profileConfigPath(), yaml.stringify(profileConfig)) +} + +export function getCurrentProfile(force = false): Partial { + if (force || !currentProfile) { + if (profileConfig.current) { + currentProfile = yaml.parse(fs.readFileSync(profilePath(profileConfig.current), 'utf-8')) + } else { + currentProfile = yaml.parse(fs.readFileSync(profilePath('default'), 'utf-8')) + } + } + return currentProfile } diff --git a/src/main/dirs.ts b/src/main/dirs.ts new file mode 100644 index 0000000..5e845a4 --- /dev/null +++ b/src/main/dirs.ts @@ -0,0 +1,59 @@ +import { is } from '@electron-toolkit/utils' +import { app } from 'electron' +import path from 'path' +import fs from 'fs' + +export const dataDir = app.getPath('userData') + +export function initDirs(): void { + if (!fs.existsSync(dataDir)) { + fs.mkdirSync(dataDir) + } + if (!fs.existsSync(profilesDir())) { + fs.mkdirSync(profilesDir()) + } + if (!fs.existsSync(mihomoWorkDir())) { + fs.mkdirSync(mihomoWorkDir()) + } +} + +export function mihomoCoreDir(): string { + if (is.dev) { + return path.join(__dirname, '../../resources/sidecar') + } else { + return path.join(process.resourcesPath, 'sidecar') + } +} + +export function mihomoCorePath(core: string): string { + const isWin = process.platform === 'win32' + return path.join(mihomoCoreDir(), `${core}${isWin ? '.exe' : ''}`) +} + +export function appConfigPath(): string { + return path.join(dataDir, 'config.yaml') +} + +export function controledMihomoConfigPath(): string { + return path.join(dataDir, 'mihomo.yaml') +} + +export function profileConfigPath(): string { + return path.join(dataDir, 'profile.yaml') +} + +export function profilesDir(): string { + return path.join(dataDir, 'profiles') +} + +export function profilePath(id: string): string { + return path.join(profilesDir(), `${id}.yaml`) +} + +export function mihomoWorkDir(): string { + return path.join(dataDir, 'work') +} + +export function mihomoWorkConfigPath(): string { + return path.join(mihomoWorkDir(), 'config.yaml') +} diff --git a/src/main/factory.ts b/src/main/factory.ts new file mode 100644 index 0000000..45db983 --- /dev/null +++ b/src/main/factory.ts @@ -0,0 +1,9 @@ +import { controledMihomoConfig, currentProfile } from './config' +import { mihomoWorkConfigPath } from './dirs' +import yaml from 'yaml' +import fs from 'fs' + +export function generateProfile(): void { + const profile = Object.assign(currentProfile, controledMihomoConfig) + fs.writeFileSync(mihomoWorkConfigPath(), yaml.stringify(profile)) +} diff --git a/src/main/index.ts b/src/main/index.ts index 47749fa..b50d8ea 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -5,13 +5,16 @@ import pngIcon from '../../resources/icon.png?asset' import icoIcon from '../../resources/icon.ico?asset' import { registerIpcMainHandlers } from './cmds' import { initConfig, appConfig } from './config' +import { stopCore, startCore } from './manager' +import { initDirs } from './dirs' let window: BrowserWindow | null = null let tray: Tray | null = null let trayContextMenu: Menu | null = null +initDirs() initConfig() - +startCore() function createWindow(): void { // Create the browser window. window = new BrowserWindow({ @@ -124,5 +127,6 @@ app.on('window-all-closed', () => { }) app.on('before-quit', () => { + stopCore() app.exit() }) diff --git a/src/main/manager.ts b/src/main/manager.ts new file mode 100644 index 0000000..e26ccf0 --- /dev/null +++ b/src/main/manager.ts @@ -0,0 +1,25 @@ +import { execFile, ChildProcess } from 'child_process' +import { mihomoCorePath, mihomoWorkDir } from './dirs' +import { generateProfile } from './factory' +import { appConfig } from './config' + +let child: ChildProcess + +export function startCore(): void { + const corePath = mihomoCorePath(appConfig.core ?? 'mihomo') + generateProfile() + stopCore() + child = execFile(corePath, ['-d', mihomoWorkDir()], (error, stdout) => { + console.log(stdout) + }) +} + +export function stopCore(): void { + if (child) { + child.kill('SIGINT') + } +} + +export function restartCore(): void { + startCore() +} diff --git a/src/main/mihomo-api.ts b/src/main/mihomo-api.ts index e887a5e..45b8d4c 100644 --- a/src/main/mihomo-api.ts +++ b/src/main/mihomo-api.ts @@ -1,4 +1,5 @@ import axios, { AxiosInstance } from 'axios' +import { controledMihomoConfig } from './config' let axiosIns: AxiosInstance = null! @@ -7,8 +8,9 @@ let axiosIns: AxiosInstance = null! export const getAxios = async (force: boolean = false): Promise => { if (axiosIns && !force) return axiosIns - const server = '127.0.0.1:9097' - const secret = '' + let server = controledMihomoConfig['external-controller'] + const secret = controledMihomoConfig.secret ?? '' + if (server?.startsWith(':')) server = `127.0.0.1${server}` axiosIns = axios.create({ baseURL: `http://${server}`, diff --git a/src/main/template.ts b/src/main/template.ts index 7c9ee54..d9f388e 100644 --- a/src/main/template.ts +++ b/src/main/template.ts @@ -1,3 +1,30 @@ export const defaultConfig: IAppConfig = { + core: 'mihomo', silentStart: false } + +export const defaultControledMihomoConfig: Partial = { + 'external-controller': '127.0.0.1:9090', + ipv6: false, + mode: 'rule', + 'mixed-port': 7890, + 'allow-lan': false, + 'log-level': 'info' +} + +export const defaultProfileConfig: IProfileConfig = { + current: 'default', + profiles: [ + { + id: 'default', + type: 'local', + name: '默认' + } + ] +} + +export const defaultProfile: Partial = { + proxies: [], + 'proxy-groups': [], + rules: [] +} diff --git a/src/renderer/src/App.tsx b/src/renderer/src/App.tsx index 4270bbd..346da42 100644 --- a/src/renderer/src/App.tsx +++ b/src/renderer/src/App.tsx @@ -13,6 +13,7 @@ import RuleCard from '@renderer/components/sider/rule-card' import OverrideCard from '@renderer/components/sider/override-card' import ConnCard from '@renderer/components/sider/conn-card' import LogCard from '@renderer/components/sider/log-card' +import MihomoCoreCard from './components/sider/mihomo-core-card.tsx' const App: React.FC = () => { const { setTheme } = useTheme() @@ -67,6 +68,7 @@ const App: React.FC = () => {

配置

+
diff --git a/src/renderer/src/components/sider/conn-card.tsx b/src/renderer/src/components/sider/conn-card.tsx index 0a4cdc9..e018727 100644 --- a/src/renderer/src/components/sider/conn-card.tsx +++ b/src/renderer/src/components/sider/conn-card.tsx @@ -1,4 +1,4 @@ -import { Button, Card, CardBody, CardFooter, Chip } from '@nextui-org/react' +import { Button, Card, CardBody, CardFooter } from '@nextui-org/react' import { IoLink } from 'react-icons/io5' import { useLocation, useNavigate } from 'react-router-dom' @@ -22,9 +22,6 @@ const ConnCard: React.FC = () => { > - - 1103 -
diff --git a/src/renderer/src/components/sider/mihomo-core-card.tsx.tsx b/src/renderer/src/components/sider/mihomo-core-card.tsx.tsx new file mode 100644 index 0000000..5911d6d --- /dev/null +++ b/src/renderer/src/components/sider/mihomo-core-card.tsx.tsx @@ -0,0 +1,72 @@ +import { + Button, + Card, + CardBody, + CardFooter, + Dropdown, + DropdownItem, + DropdownMenu, + DropdownTrigger +} from '@nextui-org/react' +import { useAppConfig } from '@renderer/hooks/use-config' +import { mihomoVersion, restartCore } from '@renderer/utils/ipc' +import { IoMdRefresh } from 'react-icons/io' +import useSWR from 'swr' + +const CoreMap = { + mihomo: 'Mihomo', + 'mihomo-alpha': 'Mihomo Alpha' +} + +const MihomoCoreCard: React.FC = () => { + const { data: version, mutate } = useSWR('mihomoVersion', mihomoVersion) + const { appConfig, patchAppConfig } = useAppConfig() + const { core } = appConfig || {} + + return ( + + +
+

+ {version?.version ?? '-'} +

+ +
+
+ + + + + + { + await patchAppConfig({ core: key as 'mihomo' | 'mihomo-alpha' }) + await restartCore() + await mutate() + }} + > + Mihomo + Mihomo Alpha + + + +
+ ) +} + +export default MihomoCoreCard diff --git a/src/renderer/src/hooks/use-config.tsx b/src/renderer/src/hooks/use-config.tsx index 5ebb4bb..0748162 100644 --- a/src/renderer/src/hooks/use-config.tsx +++ b/src/renderer/src/hooks/use-config.tsx @@ -4,7 +4,7 @@ import { getAppConfig, setAppConfig } from '@renderer/utils/ipc' interface RetuenType { appConfig: IAppConfig | undefined mutateAppConfig: () => void - patchAppConfig: (value: Partial) => void + patchAppConfig: (value: Partial) => Promise } export const useAppConfig = (): RetuenType => { diff --git a/src/renderer/src/utils/ipc.ts b/src/renderer/src/utils/ipc.ts index eeefef5..ef52cd5 100644 --- a/src/renderer/src/utils/ipc.ts +++ b/src/renderer/src/utils/ipc.ts @@ -29,3 +29,7 @@ export async function getControledMihomoConfig(force = false): Promise): Promise { await window.electron.ipcRenderer.invoke('setControledMihomoConfig', patch) } + +export async function restartCore(): Promise { + await window.electron.ipcRenderer.invoke('restartCore') +} diff --git a/src/shared/types.d.ts b/src/shared/types.d.ts index 0345c22..92c6f0e 100644 --- a/src/shared/types.d.ts +++ b/src/shared/types.d.ts @@ -1,4 +1,5 @@ type OutboundMode = 'rule' | 'global' | 'direct' +type LogLevel = 'info' | 'debug' | 'warn' | 'error' | 'silent' interface IMihomoVersion { version: string @@ -6,12 +7,32 @@ interface IMihomoVersion { } interface IAppConfig { + core: 'mihomo' | 'mihomo-alpha' silentStart: boolean } interface IMihomoConfig { + 'external-controller': string + secret?: string + ipv6: boolean mode: OutboundMode 'mixed-port': number + 'allow-lan': boolean + 'log-level': LogLevel 'socks-port'?: number port?: number + proxies?: [] + 'proxy-groups'?: [] + rules?: [] +} + +interface IProfileConfig { + current?: string + profiles?: IProfileItem[] +} + +interface IProfileItem { + id: string + type: 'remote' | 'local' + name: string }