mirror of
https://gh.catmak.name/https://github.com/mihomo-party-org/mihomo-party
synced 2025-12-27 21:20:29 +08:00
support global shortcut
This commit is contained in:
parent
1b8205cb18
commit
c2b839b7b9
@ -10,6 +10,7 @@ import icon from '../../resources/icon.png?asset'
|
||||
import { createTray } from './resolve/tray'
|
||||
import { init } from './utils/init'
|
||||
import { join } from 'path'
|
||||
import { initShortcut } from './resolve/shortcut'
|
||||
|
||||
export let mainWindow: BrowserWindow | null = null
|
||||
|
||||
@ -69,6 +70,7 @@ app.whenReady().then(async () => {
|
||||
optimizer.watchWindowShortcuts(window)
|
||||
})
|
||||
registerIpcMainHandlers()
|
||||
await initShortcut()
|
||||
createWindow()
|
||||
await createTray()
|
||||
app.on('activate', function () {
|
||||
|
||||
165
src/main/resolve/shortcut.ts
Normal file
165
src/main/resolve/shortcut.ts
Normal file
@ -0,0 +1,165 @@
|
||||
import { app, globalShortcut, ipcMain } from 'electron'
|
||||
import { mainWindow, showMainWindow } from '..'
|
||||
import {
|
||||
getAppConfig,
|
||||
getControledMihomoConfig,
|
||||
patchAppConfig,
|
||||
patchControledMihomoConfig
|
||||
} from '../config'
|
||||
import { triggerSysProxy } from '../sys/sysproxy'
|
||||
import { patchMihomoConfig } from '../core/mihomoApi'
|
||||
|
||||
export async function registerShortcut(
|
||||
oldShortcut: string,
|
||||
newShortcut: string,
|
||||
action: string
|
||||
): Promise<boolean> {
|
||||
if (oldShortcut !== '') {
|
||||
globalShortcut.unregister(oldShortcut)
|
||||
}
|
||||
if (newShortcut === '') {
|
||||
return true
|
||||
}
|
||||
switch (action) {
|
||||
case 'showWindowShortcut': {
|
||||
return globalShortcut.register(newShortcut, () => {
|
||||
if (mainWindow?.isVisible()) {
|
||||
mainWindow?.close()
|
||||
} else {
|
||||
showMainWindow()
|
||||
}
|
||||
})
|
||||
}
|
||||
case 'triggerSysProxyShortcut': {
|
||||
return globalShortcut.register(newShortcut, async () => {
|
||||
const {
|
||||
sysProxy: { enable }
|
||||
} = await getAppConfig()
|
||||
try {
|
||||
await triggerSysProxy(!enable)
|
||||
await patchAppConfig({ sysProxy: { enable: !enable } })
|
||||
} catch {
|
||||
// ignore
|
||||
} finally {
|
||||
mainWindow?.webContents.send('appConfigUpdated')
|
||||
ipcMain.emit('updateTrayMenu')
|
||||
}
|
||||
})
|
||||
}
|
||||
case 'triggerTunShortcut': {
|
||||
return globalShortcut.register(newShortcut, async () => {
|
||||
const { tun } = await getControledMihomoConfig()
|
||||
const enable = tun?.enable ?? false
|
||||
await patchControledMihomoConfig({ tun: { enable: !enable } })
|
||||
await patchMihomoConfig({ tun: { enable: !enable } })
|
||||
mainWindow?.webContents.send('controledMihomoConfigUpdated')
|
||||
ipcMain.emit('updateTrayMenu')
|
||||
})
|
||||
}
|
||||
case 'ruleModeShortcut': {
|
||||
return globalShortcut.register(newShortcut, async () => {
|
||||
await patchControledMihomoConfig({ mode: 'rule' })
|
||||
await patchMihomoConfig({ mode: 'rule' })
|
||||
mainWindow?.webContents.send('controledMihomoConfigUpdated')
|
||||
ipcMain.emit('updateTrayMenu')
|
||||
})
|
||||
}
|
||||
case 'globalModeShortcut': {
|
||||
return globalShortcut.register(newShortcut, async () => {
|
||||
await patchControledMihomoConfig({ mode: 'global' })
|
||||
await patchMihomoConfig({ mode: 'global' })
|
||||
mainWindow?.webContents.send('controledMihomoConfigUpdated')
|
||||
ipcMain.emit('updateTrayMenu')
|
||||
})
|
||||
}
|
||||
case 'directModeShortcut': {
|
||||
return globalShortcut.register(newShortcut, async () => {
|
||||
await patchControledMihomoConfig({ mode: 'direct' })
|
||||
await patchMihomoConfig({ mode: 'direct' })
|
||||
mainWindow?.webContents.send('controledMihomoConfigUpdated')
|
||||
ipcMain.emit('updateTrayMenu')
|
||||
})
|
||||
}
|
||||
case 'restartAppShortcut': {
|
||||
return globalShortcut.register(newShortcut, () => {
|
||||
app.relaunch()
|
||||
app.quit()
|
||||
})
|
||||
}
|
||||
case 'quitAppShortcut': {
|
||||
return globalShortcut.register(newShortcut, () => {
|
||||
app.quit()
|
||||
})
|
||||
}
|
||||
}
|
||||
throw new Error('Unknown action')
|
||||
}
|
||||
|
||||
export async function initShortcut(): Promise<void> {
|
||||
const {
|
||||
showWindowShortcut,
|
||||
triggerSysProxyShortcut,
|
||||
triggerTunShortcut,
|
||||
ruleModeShortcut,
|
||||
globalModeShortcut,
|
||||
directModeShortcut,
|
||||
restartAppShortcut,
|
||||
quitAppShortcut
|
||||
} = await getAppConfig()
|
||||
if (showWindowShortcut) {
|
||||
try {
|
||||
await registerShortcut('', showWindowShortcut, 'showWindowShortcut')
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
if (triggerSysProxyShortcut) {
|
||||
try {
|
||||
await registerShortcut('', triggerSysProxyShortcut, 'triggerSysProxyShortcut')
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
if (triggerTunShortcut) {
|
||||
try {
|
||||
await registerShortcut('', triggerTunShortcut, 'triggerTunShortcut')
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
if (ruleModeShortcut) {
|
||||
try {
|
||||
await registerShortcut('', ruleModeShortcut, 'ruleModeShortcut')
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
if (globalModeShortcut) {
|
||||
try {
|
||||
await registerShortcut('', globalModeShortcut, 'globalModeShortcut')
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
if (directModeShortcut) {
|
||||
try {
|
||||
await registerShortcut('', directModeShortcut, 'directModeShortcut')
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
if (restartAppShortcut) {
|
||||
try {
|
||||
await registerShortcut('', restartAppShortcut, 'restartAppShortcut')
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
if (quitAppShortcut) {
|
||||
try {
|
||||
await registerShortcut('', quitAppShortcut, 'quitAppShortcut')
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -22,7 +22,19 @@ export let tray: Tray | null = null
|
||||
|
||||
const buildContextMenu = async (): Promise<Menu> => {
|
||||
const { mode, tun } = await getControledMihomoConfig()
|
||||
const { sysProxy, autoCloseConnection, proxyInTray = true } = await getAppConfig()
|
||||
const {
|
||||
sysProxy,
|
||||
autoCloseConnection,
|
||||
proxyInTray = true,
|
||||
triggerSysProxyShortcut = '',
|
||||
showWindowShortcut = '',
|
||||
triggerTunShortcut = '',
|
||||
ruleModeShortcut = '',
|
||||
globalModeShortcut = '',
|
||||
directModeShortcut = '',
|
||||
restartAppShortcut = '',
|
||||
quitAppShortcut = ''
|
||||
} = await getAppConfig()
|
||||
let groupsMenu: Electron.MenuItemConstructorOptions[] = []
|
||||
if (proxyInTray && process.platform !== 'linux') {
|
||||
try {
|
||||
@ -66,6 +78,7 @@ const buildContextMenu = async (): Promise<Menu> => {
|
||||
const contextMenu = [
|
||||
{
|
||||
id: 'show',
|
||||
accelerator: showWindowShortcut,
|
||||
label: '显示窗口',
|
||||
type: 'normal',
|
||||
click: (): void => {
|
||||
@ -75,6 +88,7 @@ const buildContextMenu = async (): Promise<Menu> => {
|
||||
{
|
||||
id: 'rule',
|
||||
label: '规则模式',
|
||||
accelerator: ruleModeShortcut,
|
||||
type: 'radio',
|
||||
checked: mode === 'rule',
|
||||
click: async (): Promise<void> => {
|
||||
@ -87,6 +101,7 @@ const buildContextMenu = async (): Promise<Menu> => {
|
||||
{
|
||||
id: 'global',
|
||||
label: '全局模式',
|
||||
accelerator: globalModeShortcut,
|
||||
type: 'radio',
|
||||
checked: mode === 'global',
|
||||
click: async (): Promise<void> => {
|
||||
@ -99,6 +114,7 @@ const buildContextMenu = async (): Promise<Menu> => {
|
||||
{
|
||||
id: 'direct',
|
||||
label: '直连模式',
|
||||
accelerator: directModeShortcut,
|
||||
type: 'radio',
|
||||
checked: mode === 'direct',
|
||||
click: async (): Promise<void> => {
|
||||
@ -112,14 +128,15 @@ const buildContextMenu = async (): Promise<Menu> => {
|
||||
{
|
||||
type: 'checkbox',
|
||||
label: '系统代理',
|
||||
accelerator: triggerSysProxyShortcut,
|
||||
checked: sysProxy.enable,
|
||||
click: async (item): Promise<void> => {
|
||||
const enable = item.checked
|
||||
try {
|
||||
await triggerSysProxy(enable)
|
||||
await patchAppConfig({ sysProxy: { enable } })
|
||||
triggerSysProxy(enable)
|
||||
} catch (e) {
|
||||
await patchAppConfig({ sysProxy: { enable: !enable } })
|
||||
// ignore
|
||||
} finally {
|
||||
mainWindow?.webContents.send('appConfigUpdated')
|
||||
ipcMain.emit('updateTrayMenu')
|
||||
@ -129,6 +146,7 @@ const buildContextMenu = async (): Promise<Menu> => {
|
||||
{
|
||||
type: 'checkbox',
|
||||
label: '虚拟网卡',
|
||||
accelerator: triggerTunShortcut,
|
||||
checked: tun?.enable ?? false,
|
||||
click: async (item): Promise<void> => {
|
||||
const enable = item.checked
|
||||
@ -181,12 +199,19 @@ const buildContextMenu = async (): Promise<Menu> => {
|
||||
id: 'restart',
|
||||
label: '重启应用',
|
||||
type: 'normal',
|
||||
accelerator: restartAppShortcut,
|
||||
click: (): void => {
|
||||
app.relaunch()
|
||||
app.quit()
|
||||
}
|
||||
},
|
||||
{ id: 'quit', label: '退出应用', type: 'normal', click: (): void => app.quit() }
|
||||
{
|
||||
id: 'quit',
|
||||
label: '退出应用',
|
||||
type: 'normal',
|
||||
accelerator: quitAppShortcut,
|
||||
click: (): void => app.quit()
|
||||
}
|
||||
] as Electron.MenuItemConstructorOptions[]
|
||||
return Menu.buildFromTemplate(contextMenu)
|
||||
}
|
||||
|
||||
@ -54,6 +54,7 @@ import { isPortable, setPortable } from './dirs'
|
||||
import { listWebdavBackups, webdavBackup, webdavRestore } from '../resolve/backup'
|
||||
import { getInterfaces } from '../sys/interface'
|
||||
import { copyEnv } from '../resolve/tray'
|
||||
import { registerShortcut } from '../resolve/shortcut'
|
||||
|
||||
function ipcErrorWrapper<T>( // eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
fn: (...args: any[]) => Promise<T> // eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
@ -149,6 +150,9 @@ export function registerIpcMainHandlers(): void {
|
||||
ipcMain.handle('webdavBackup', ipcErrorWrapper(webdavBackup))
|
||||
ipcMain.handle('webdavRestore', (_e, filename) => ipcErrorWrapper(webdavRestore)(filename))
|
||||
ipcMain.handle('listWebdavBackups', ipcErrorWrapper(listWebdavBackups))
|
||||
ipcMain.handle('registerShortcut', (_e, oldShortcut, newShortcut, action) =>
|
||||
ipcErrorWrapper(registerShortcut)(oldShortcut, newShortcut, action)
|
||||
)
|
||||
ipcMain.handle('setNativeTheme', (_e, theme) => {
|
||||
setNativeTheme(theme)
|
||||
})
|
||||
|
||||
63
src/renderer/src/components/settings/actions.tsx
Normal file
63
src/renderer/src/components/settings/actions.tsx
Normal file
@ -0,0 +1,63 @@
|
||||
import { Button } from '@nextui-org/react'
|
||||
import SettingCard from '../base/base-setting-card'
|
||||
import SettingItem from '../base/base-setting-item'
|
||||
import { checkUpdate, quitApp } from '@renderer/utils/ipc'
|
||||
import { useState } from 'react'
|
||||
import UpdaterModal from '../updater/updater-modal'
|
||||
import { version } from '@renderer/utils/init'
|
||||
|
||||
const Actions: React.FC = () => {
|
||||
const [newVersion, setNewVersion] = useState('')
|
||||
const [changelog, setChangelog] = useState('')
|
||||
const [openUpdate, setOpenUpdate] = useState(false)
|
||||
const [checkingUpdate, setCheckingUpdate] = useState(false)
|
||||
|
||||
return (
|
||||
<>
|
||||
{openUpdate && (
|
||||
<UpdaterModal
|
||||
onClose={() => setOpenUpdate(false)}
|
||||
version={newVersion}
|
||||
changelog={changelog}
|
||||
/>
|
||||
)}
|
||||
<SettingCard>
|
||||
<SettingItem title="检查更新" divider>
|
||||
<Button
|
||||
size="sm"
|
||||
isLoading={checkingUpdate}
|
||||
onPress={async () => {
|
||||
try {
|
||||
setCheckingUpdate(true)
|
||||
const version = await checkUpdate()
|
||||
if (version) {
|
||||
setNewVersion(version.version)
|
||||
setChangelog(version.changelog)
|
||||
setOpenUpdate(true)
|
||||
} else {
|
||||
new window.Notification('当前已是最新版本', { body: '无需更新' })
|
||||
}
|
||||
} catch (e) {
|
||||
alert(e)
|
||||
} finally {
|
||||
setCheckingUpdate(false)
|
||||
}
|
||||
}}
|
||||
>
|
||||
检查更新
|
||||
</Button>
|
||||
</SettingItem>
|
||||
<SettingItem title="退出应用" divider>
|
||||
<Button size="sm" onPress={quitApp}>
|
||||
退出应用
|
||||
</Button>
|
||||
</SettingItem>
|
||||
<SettingItem title="应用版本">
|
||||
<div>v{version}</div>
|
||||
</SettingItem>
|
||||
</SettingCard>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default Actions
|
||||
222
src/renderer/src/components/settings/general-config.tsx
Normal file
222
src/renderer/src/components/settings/general-config.tsx
Normal file
@ -0,0 +1,222 @@
|
||||
import React, { Key } from 'react'
|
||||
import SettingCard from '../base/base-setting-card'
|
||||
import SettingItem from '../base/base-setting-item'
|
||||
import { Button, Select, SelectItem, Switch, Tab, Tabs } from '@nextui-org/react'
|
||||
import { BiCopy } from 'react-icons/bi'
|
||||
import useSWR from 'swr'
|
||||
import {
|
||||
checkAutoRun,
|
||||
copyEnv,
|
||||
disableAutoRun,
|
||||
enableAutoRun,
|
||||
isPortable,
|
||||
restartCore,
|
||||
setNativeTheme,
|
||||
setPortable
|
||||
} from '@renderer/utils/ipc'
|
||||
import { useAppConfig } from '@renderer/hooks/use-app-config'
|
||||
import { platform } from '@renderer/utils/init'
|
||||
import { useTheme } from 'next-themes'
|
||||
|
||||
const GeneralConfig: React.FC = () => {
|
||||
const { data: enable, mutate: mutateEnable } = useSWR('checkAutoRun', checkAutoRun)
|
||||
const { data: portable, mutate: mutatePortable } = useSWR('isPortable', isPortable)
|
||||
const { appConfig, patchAppConfig } = useAppConfig()
|
||||
const { setTheme } = useTheme()
|
||||
const {
|
||||
silentStart = false,
|
||||
useDockIcon = true,
|
||||
showTraffic = true,
|
||||
proxyInTray = true,
|
||||
envType = platform === 'win32' ? 'powershell' : 'bash',
|
||||
autoCheckUpdate,
|
||||
appTheme = 'system'
|
||||
} = appConfig || {}
|
||||
|
||||
const onThemeChange = (key: Key, type: 'theme' | 'color'): void => {
|
||||
const [theme, color] = appTheme.split('-')
|
||||
|
||||
if (type === 'theme') {
|
||||
let themeStr = key.toString()
|
||||
if (key !== 'system') {
|
||||
if (color) {
|
||||
themeStr += `-${color}`
|
||||
}
|
||||
}
|
||||
if (themeStr.includes('light')) {
|
||||
setNativeTheme('light')
|
||||
} else if (themeStr === 'system') {
|
||||
setNativeTheme('system')
|
||||
} else {
|
||||
setNativeTheme('dark')
|
||||
}
|
||||
setTheme(themeStr)
|
||||
patchAppConfig({ appTheme: themeStr as AppTheme })
|
||||
} else {
|
||||
let themeStr = theme
|
||||
if (theme !== 'system') {
|
||||
if (key !== 'blue') {
|
||||
themeStr += `-${key}`
|
||||
}
|
||||
setTheme(themeStr)
|
||||
patchAppConfig({ appTheme: themeStr as AppTheme })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<SettingCard>
|
||||
<SettingItem title="开机自启" divider>
|
||||
<Switch
|
||||
size="sm"
|
||||
isSelected={enable}
|
||||
onValueChange={async (v) => {
|
||||
try {
|
||||
if (v) {
|
||||
await enableAutoRun()
|
||||
} else {
|
||||
await disableAutoRun()
|
||||
}
|
||||
} catch (e) {
|
||||
alert(e)
|
||||
} finally {
|
||||
mutateEnable()
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</SettingItem>
|
||||
<SettingItem title="自动检查更新" divider>
|
||||
<Switch
|
||||
size="sm"
|
||||
isSelected={autoCheckUpdate}
|
||||
onValueChange={(v) => {
|
||||
patchAppConfig({ autoCheckUpdate: v })
|
||||
}}
|
||||
/>
|
||||
</SettingItem>
|
||||
<SettingItem title="静默启动" divider>
|
||||
<Switch
|
||||
size="sm"
|
||||
isSelected={silentStart}
|
||||
onValueChange={(v) => {
|
||||
patchAppConfig({ silentStart: v })
|
||||
}}
|
||||
/>
|
||||
</SettingItem>
|
||||
<SettingItem
|
||||
title="复制环境变量类型"
|
||||
actions={
|
||||
<Button isIconOnly size="sm" className="ml-2" variant="light" onPress={copyEnv}>
|
||||
<BiCopy className="text-lg" />
|
||||
</Button>
|
||||
}
|
||||
divider
|
||||
>
|
||||
<Select
|
||||
className="w-[150px]"
|
||||
size="sm"
|
||||
selectedKeys={new Set([envType])}
|
||||
onSelectionChange={async (v) => {
|
||||
try {
|
||||
await patchAppConfig({ envType: v.currentKey as 'bash' | 'cmd' | 'powershell' })
|
||||
} catch (e) {
|
||||
alert(e)
|
||||
}
|
||||
}}
|
||||
>
|
||||
<SelectItem key="bash">Bash</SelectItem>
|
||||
<SelectItem key="cmd">CMD</SelectItem>
|
||||
<SelectItem key="powershell">PowerShell</SelectItem>
|
||||
</Select>
|
||||
</SettingItem>
|
||||
{platform !== 'linux' && (
|
||||
<SettingItem title="托盘菜单显示节点信息" divider>
|
||||
<Switch
|
||||
size="sm"
|
||||
isSelected={proxyInTray}
|
||||
onValueChange={async (v) => {
|
||||
await patchAppConfig({ proxyInTray: v })
|
||||
}}
|
||||
/>
|
||||
</SettingItem>
|
||||
)}
|
||||
{platform === 'darwin' && (
|
||||
<>
|
||||
<SettingItem title="显示Dock图标" divider>
|
||||
<Switch
|
||||
size="sm"
|
||||
isSelected={useDockIcon}
|
||||
onValueChange={async (v) => {
|
||||
await patchAppConfig({ useDockIcon: v })
|
||||
}}
|
||||
/>
|
||||
</SettingItem>
|
||||
<SettingItem title="显示网速信息" divider>
|
||||
<Switch
|
||||
size="sm"
|
||||
isSelected={showTraffic}
|
||||
onValueChange={async (v) => {
|
||||
await patchAppConfig({ showTraffic: v })
|
||||
await restartCore()
|
||||
}}
|
||||
/>
|
||||
</SettingItem>
|
||||
</>
|
||||
)}
|
||||
{platform === 'win32' && (
|
||||
<SettingItem title="数据存储路径" divider>
|
||||
<Select
|
||||
className="w-[150px]"
|
||||
size="sm"
|
||||
selectedKeys={new Set([portable ? 'portable' : 'data'])}
|
||||
onSelectionChange={async (v) => {
|
||||
try {
|
||||
await setPortable(v.currentKey === 'portable')
|
||||
} catch (e) {
|
||||
alert(e)
|
||||
} finally {
|
||||
mutatePortable()
|
||||
}
|
||||
}}
|
||||
>
|
||||
<SelectItem key="data">AppData</SelectItem>
|
||||
<SelectItem key="portable">安装目录</SelectItem>
|
||||
</Select>
|
||||
</SettingItem>
|
||||
)}
|
||||
<SettingItem title="背景色" divider={appTheme !== 'system'}>
|
||||
<Tabs
|
||||
size="sm"
|
||||
color="primary"
|
||||
selectedKey={appTheme.split('-')[0]}
|
||||
onSelectionChange={(key) => {
|
||||
onThemeChange(key, 'theme')
|
||||
}}
|
||||
>
|
||||
<Tab key="system" title="自动" />
|
||||
<Tab key="dark" title="深色" />
|
||||
<Tab key="gray" title="灰色" />
|
||||
<Tab key="light" title="浅色" />
|
||||
</Tabs>
|
||||
</SettingItem>
|
||||
{appTheme !== 'system' && (
|
||||
<SettingItem title="主题色">
|
||||
<Tabs
|
||||
size="sm"
|
||||
color="primary"
|
||||
selectedKey={appTheme.split('-')[1] || 'blue'}
|
||||
onSelectionChange={(key) => {
|
||||
onThemeChange(key, 'color')
|
||||
}}
|
||||
>
|
||||
<Tab key="blue" title="蓝色" />
|
||||
<Tab key="pink" title="粉色" />
|
||||
<Tab key="green" title="绿色" />
|
||||
</Tabs>
|
||||
</SettingItem>
|
||||
)}
|
||||
</SettingCard>
|
||||
)
|
||||
}
|
||||
|
||||
export default GeneralConfig
|
||||
98
src/renderer/src/components/settings/mihomo-config.tsx
Normal file
98
src/renderer/src/components/settings/mihomo-config.tsx
Normal file
@ -0,0 +1,98 @@
|
||||
import React, { useState } from 'react'
|
||||
import SettingCard from '../base/base-setting-card'
|
||||
import SettingItem from '../base/base-setting-item'
|
||||
import { Input, Switch } from '@nextui-org/react'
|
||||
import { useAppConfig } from '@renderer/hooks/use-app-config'
|
||||
import debounce from '@renderer/utils/debounce'
|
||||
import { patchControledMihomoConfig } from '@renderer/utils/ipc'
|
||||
|
||||
const MihomoConfig: React.FC = () => {
|
||||
const { appConfig, patchAppConfig } = useAppConfig()
|
||||
const {
|
||||
controlDns = true,
|
||||
controlSniff = true,
|
||||
delayTestTimeout,
|
||||
autoCloseConnection = true,
|
||||
delayTestUrl,
|
||||
userAgent
|
||||
} = appConfig || {}
|
||||
const [url, setUrl] = useState(delayTestUrl)
|
||||
const setUrlDebounce = debounce((v: string) => {
|
||||
patchAppConfig({ delayTestUrl: v })
|
||||
}, 500)
|
||||
const [ua, setUa] = useState(userAgent)
|
||||
const setUaDebounce = debounce((v: string) => {
|
||||
patchAppConfig({ userAgent: v })
|
||||
}, 500)
|
||||
return (
|
||||
<SettingCard>
|
||||
<SettingItem title="订阅拉取 UA" divider>
|
||||
<Input
|
||||
size="sm"
|
||||
className="w-[60%]"
|
||||
value={ua}
|
||||
placeholder="默认 clash-meta"
|
||||
onValueChange={(v) => {
|
||||
setUa(v)
|
||||
setUaDebounce(v)
|
||||
}}
|
||||
></Input>
|
||||
</SettingItem>
|
||||
<SettingItem title="延迟测试地址" divider>
|
||||
<Input
|
||||
size="sm"
|
||||
className="w-[60%]"
|
||||
value={url}
|
||||
placeholder="默认https://www.gstatic.com/generate_204"
|
||||
onValueChange={(v) => {
|
||||
setUrl(v)
|
||||
setUrlDebounce(v)
|
||||
}}
|
||||
></Input>
|
||||
</SettingItem>
|
||||
<SettingItem title="延迟测试超时时间" divider>
|
||||
<Input
|
||||
type="number"
|
||||
size="sm"
|
||||
className="w-[60%]"
|
||||
value={delayTestTimeout?.toString()}
|
||||
placeholder="默认5000"
|
||||
onValueChange={(v) => {
|
||||
patchAppConfig({ delayTestTimeout: parseInt(v) })
|
||||
}}
|
||||
/>
|
||||
</SettingItem>
|
||||
<SettingItem title="接管DNS设置" divider>
|
||||
<Switch
|
||||
size="sm"
|
||||
isSelected={controlDns}
|
||||
onValueChange={async (v) => {
|
||||
await patchAppConfig({ controlDns: v })
|
||||
await patchControledMihomoConfig({})
|
||||
}}
|
||||
/>
|
||||
</SettingItem>
|
||||
<SettingItem title="接管域名嗅探设置" divider>
|
||||
<Switch
|
||||
size="sm"
|
||||
isSelected={controlSniff}
|
||||
onValueChange={async (v) => {
|
||||
await patchAppConfig({ controlSniff: v })
|
||||
await patchControledMihomoConfig({})
|
||||
}}
|
||||
/>
|
||||
</SettingItem>
|
||||
<SettingItem title="自动断开连接">
|
||||
<Switch
|
||||
size="sm"
|
||||
isSelected={autoCloseConnection}
|
||||
onValueChange={(v) => {
|
||||
patchAppConfig({ autoCloseConnection: v })
|
||||
}}
|
||||
/>
|
||||
</SettingItem>
|
||||
</SettingCard>
|
||||
)
|
||||
}
|
||||
|
||||
export default MihomoConfig
|
||||
227
src/renderer/src/components/settings/shortcut-config.tsx
Normal file
227
src/renderer/src/components/settings/shortcut-config.tsx
Normal file
@ -0,0 +1,227 @@
|
||||
import { Button, Input } from '@nextui-org/react'
|
||||
import SettingCard from '../base/base-setting-card'
|
||||
import SettingItem from '../base/base-setting-item'
|
||||
import { useAppConfig } from '@renderer/hooks/use-app-config'
|
||||
import React, { KeyboardEvent, useState } from 'react'
|
||||
import { platform } from '@renderer/utils/init'
|
||||
import { registerShortcut } from '@renderer/utils/ipc'
|
||||
|
||||
const keyMap = {
|
||||
Backquote: '`',
|
||||
Backslash: '\\',
|
||||
BracketLeft: '[',
|
||||
BracketRight: ']',
|
||||
Comma: ',',
|
||||
Equal: '=',
|
||||
Minus: '-',
|
||||
Plus: 'PLUS',
|
||||
Period: '.',
|
||||
Quote: "'",
|
||||
Semicolon: ';',
|
||||
Slash: '/',
|
||||
Backspace: 'Backspace',
|
||||
CapsLock: 'Capslock',
|
||||
ContextMenu: 'Contextmenu',
|
||||
Space: 'Space',
|
||||
Tab: 'Tab',
|
||||
Convert: 'Convert',
|
||||
Delete: 'Delete',
|
||||
End: 'End',
|
||||
Help: 'Help',
|
||||
Home: 'Home',
|
||||
PageDown: 'Pagedown',
|
||||
PageUp: 'Pageup',
|
||||
Escape: 'Esc',
|
||||
PrintScreen: 'Printscreen',
|
||||
ScrollLock: 'Scrolllock',
|
||||
Pause: 'Pause',
|
||||
Insert: 'Insert',
|
||||
Suspend: 'Suspend'
|
||||
}
|
||||
|
||||
const ShortcutConfig: React.FC = () => {
|
||||
const { appConfig, patchAppConfig } = useAppConfig()
|
||||
const {
|
||||
showWindowShortcut = '',
|
||||
triggerSysProxyShortcut = '',
|
||||
triggerTunShortcut = '',
|
||||
ruleModeShortcut = '',
|
||||
globalModeShortcut = '',
|
||||
directModeShortcut = '',
|
||||
restartAppShortcut = '',
|
||||
quitAppShortcut = ''
|
||||
} = appConfig || {}
|
||||
|
||||
return (
|
||||
<SettingCard>
|
||||
<SettingItem title="打开/关闭窗口" divider>
|
||||
<div className="flex justify-end w-[60%]">
|
||||
<ShortcutInput
|
||||
value={showWindowShortcut}
|
||||
patchAppConfig={patchAppConfig}
|
||||
action="showWindowShortcut"
|
||||
/>
|
||||
</div>
|
||||
</SettingItem>
|
||||
<SettingItem title="打开/关闭系统代理" divider>
|
||||
<div className="flex justify-end w-[60%]">
|
||||
<ShortcutInput
|
||||
value={triggerSysProxyShortcut}
|
||||
patchAppConfig={patchAppConfig}
|
||||
action="triggerSysProxyShortcut"
|
||||
/>
|
||||
</div>
|
||||
</SettingItem>
|
||||
<SettingItem title="打开/关闭虚拟网卡" divider>
|
||||
<div className="flex justify-end w-[60%]">
|
||||
<ShortcutInput
|
||||
value={triggerTunShortcut}
|
||||
patchAppConfig={patchAppConfig}
|
||||
action="triggerTunShortcut"
|
||||
/>
|
||||
</div>
|
||||
</SettingItem>
|
||||
<SettingItem title="切换规则模式" divider>
|
||||
<div className="flex justify-end w-[60%]">
|
||||
<ShortcutInput
|
||||
value={ruleModeShortcut}
|
||||
patchAppConfig={patchAppConfig}
|
||||
action="ruleModeShortcut"
|
||||
/>
|
||||
</div>
|
||||
</SettingItem>
|
||||
<SettingItem title="切换全局模式" divider>
|
||||
<div className="flex justify-end w-[60%]">
|
||||
<ShortcutInput
|
||||
value={globalModeShortcut}
|
||||
patchAppConfig={patchAppConfig}
|
||||
action="globalModeShortcut"
|
||||
/>
|
||||
</div>
|
||||
</SettingItem>
|
||||
<SettingItem title="切换直连模式" divider>
|
||||
<div className="flex justify-end w-[60%]">
|
||||
<ShortcutInput
|
||||
value={directModeShortcut}
|
||||
patchAppConfig={patchAppConfig}
|
||||
action="directModeShortcut"
|
||||
/>
|
||||
</div>
|
||||
</SettingItem>
|
||||
<SettingItem title="重启应用" divider>
|
||||
<div className="flex justify-end w-[60%]">
|
||||
<ShortcutInput
|
||||
value={restartAppShortcut}
|
||||
patchAppConfig={patchAppConfig}
|
||||
action="restartAppShortcut"
|
||||
/>
|
||||
</div>
|
||||
</SettingItem>
|
||||
<SettingItem title="退出应用">
|
||||
<div className="flex justify-end w-[60%]">
|
||||
<ShortcutInput
|
||||
value={quitAppShortcut}
|
||||
patchAppConfig={patchAppConfig}
|
||||
action="quitAppShortcut"
|
||||
/>
|
||||
</div>
|
||||
</SettingItem>
|
||||
</SettingCard>
|
||||
)
|
||||
}
|
||||
|
||||
const ShortcutInput: React.FC<{
|
||||
value: string
|
||||
action: string
|
||||
patchAppConfig: (value: Partial<IAppConfig>) => Promise<void>
|
||||
}> = (props) => {
|
||||
const { value, action, patchAppConfig } = props
|
||||
const [inputValue, setInputValue] = useState(value)
|
||||
|
||||
const parseShortcut = (
|
||||
event: KeyboardEvent,
|
||||
setKey: { (value: React.SetStateAction<string>): void; (arg0: string): void }
|
||||
): void => {
|
||||
event.preventDefault()
|
||||
let code = event.code
|
||||
const key = event.key
|
||||
if (code === 'Backspace') {
|
||||
setKey('')
|
||||
} else {
|
||||
let newValue = ''
|
||||
if (event.ctrlKey) {
|
||||
newValue = 'Ctrl'
|
||||
}
|
||||
if (event.shiftKey) {
|
||||
newValue = `${newValue}${newValue.length > 0 ? '+' : ''}Shift`
|
||||
}
|
||||
if (event.metaKey) {
|
||||
newValue = `${newValue}${newValue.length > 0 ? '+' : ''}${platform === 'darwin' ? 'Command' : 'Super'}`
|
||||
}
|
||||
if (event.altKey) {
|
||||
newValue = `${newValue}${newValue.length > 0 ? '+' : ''}Alt`
|
||||
}
|
||||
if (code.startsWith('Key')) {
|
||||
code = code.substring(3)
|
||||
} else if (code.startsWith('Digit')) {
|
||||
code = code.substring(5)
|
||||
} else if (code.startsWith('Arrow')) {
|
||||
code = code.substring(5)
|
||||
} else if (key.startsWith('Arrow')) {
|
||||
code = key.substring(5)
|
||||
} else if (code.startsWith('Intl')) {
|
||||
code = code.substring(4)
|
||||
} else if (code.startsWith('Numpad')) {
|
||||
if (key.length === 1) {
|
||||
code = 'Num' + code.substring(6)
|
||||
} else {
|
||||
code = key
|
||||
}
|
||||
} else if (/F\d+/.test(code)) {
|
||||
// f1-f12
|
||||
} else if (keyMap[code] !== undefined) {
|
||||
code = keyMap[code]
|
||||
} else {
|
||||
code = ''
|
||||
}
|
||||
setKey(`${newValue}${newValue.length > 0 && code.length > 0 ? '+' : ''}${code}`)
|
||||
}
|
||||
}
|
||||
return (
|
||||
<>
|
||||
{inputValue !== value && (
|
||||
<Button
|
||||
color="primary"
|
||||
className="mr-2"
|
||||
size="sm"
|
||||
onPress={async () => {
|
||||
try {
|
||||
if (await registerShortcut(value, inputValue, action)) {
|
||||
await patchAppConfig({ [action]: inputValue })
|
||||
window.electron.ipcRenderer.send('updateTrayMenu')
|
||||
} else {
|
||||
alert('快捷键注册失败')
|
||||
}
|
||||
} catch (e) {
|
||||
alert(`快捷键注册失败: ${e}`)
|
||||
}
|
||||
}}
|
||||
>
|
||||
确认
|
||||
</Button>
|
||||
)}
|
||||
<Input
|
||||
placeholder="点击输入快捷键"
|
||||
onKeyDown={(e: KeyboardEvent): void => {
|
||||
parseShortcut(e, setInputValue)
|
||||
}}
|
||||
size="sm"
|
||||
onClear={() => setInputValue('')}
|
||||
value={inputValue}
|
||||
className="w-[calc(100%-72px)] pr-0"
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default ShortcutConfig
|
||||
105
src/renderer/src/components/settings/webdav-config.tsx
Normal file
105
src/renderer/src/components/settings/webdav-config.tsx
Normal file
@ -0,0 +1,105 @@
|
||||
import React, { useState } from 'react'
|
||||
import SettingCard from '../base/base-setting-card'
|
||||
import SettingItem from '../base/base-setting-item'
|
||||
import { Button, Input } from '@nextui-org/react'
|
||||
import { listWebdavBackups, webdavBackup } from '@renderer/utils/ipc'
|
||||
import WebdavRestoreModal from './webdav-restore-modal'
|
||||
import debounce from '@renderer/utils/debounce'
|
||||
import { useAppConfig } from '@renderer/hooks/use-app-config'
|
||||
|
||||
const WebdavConfig: React.FC = () => {
|
||||
const { appConfig, patchAppConfig } = useAppConfig()
|
||||
const { webdavUrl, webdavUsername, webdavPassword } = appConfig || {}
|
||||
const [backuping, setBackuping] = useState(false)
|
||||
const [restoring, setRestoring] = useState(false)
|
||||
const [filenames, setFilenames] = useState<string[]>([])
|
||||
const [restoreOpen, setRestoreOpen] = useState(false)
|
||||
|
||||
const [webdav, setWebdav] = useState({ webdavUrl, webdavUsername, webdavPassword })
|
||||
const setWebdavDebounce = debounce(({ webdavUrl, webdavUsername, webdavPassword }) => {
|
||||
patchAppConfig({ webdavUrl, webdavUsername, webdavPassword })
|
||||
}, 500)
|
||||
const handleBackup = async (): Promise<void> => {
|
||||
setBackuping(true)
|
||||
try {
|
||||
await webdavBackup()
|
||||
new window.Notification('备份成功', { body: '备份文件已上传至WebDav' })
|
||||
} catch (e) {
|
||||
alert(e)
|
||||
} finally {
|
||||
setBackuping(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleRestore = async (): Promise<void> => {
|
||||
try {
|
||||
setRestoring(true)
|
||||
const filenames = await listWebdavBackups()
|
||||
setFilenames(filenames)
|
||||
setRestoreOpen(true)
|
||||
} catch (e) {
|
||||
alert(`获取备份列表失败: ${e}`)
|
||||
} finally {
|
||||
setRestoring(false)
|
||||
}
|
||||
}
|
||||
return (
|
||||
<>
|
||||
{restoreOpen && (
|
||||
<WebdavRestoreModal filenames={filenames} onClose={() => setRestoreOpen(false)} />
|
||||
)}
|
||||
<SettingCard>
|
||||
<SettingItem title="WebDav地址" divider>
|
||||
<Input
|
||||
size="sm"
|
||||
className="w-[60%]"
|
||||
value={webdav.webdavUrl}
|
||||
onValueChange={(v) => {
|
||||
setWebdav({ ...webdav, webdavUrl: v })
|
||||
setWebdavDebounce({ ...webdav, webdavUrl: v })
|
||||
}}
|
||||
/>
|
||||
</SettingItem>
|
||||
<SettingItem title="WebDav用户名" divider>
|
||||
<Input
|
||||
size="sm"
|
||||
className="w-[60%]"
|
||||
value={webdav.webdavUsername}
|
||||
onValueChange={(v) => {
|
||||
setWebdav({ ...webdav, webdavUsername: v })
|
||||
setWebdavDebounce({ ...webdav, webdavUsername: v })
|
||||
}}
|
||||
/>
|
||||
</SettingItem>
|
||||
<SettingItem title="WebDav密码" divider>
|
||||
<Input
|
||||
size="sm"
|
||||
className="w-[60%]"
|
||||
type="password"
|
||||
value={webdav.webdavPassword}
|
||||
onValueChange={(v) => {
|
||||
setWebdav({ ...webdav, webdavPassword: v })
|
||||
setWebdavDebounce({ ...webdav, webdavPassword: v })
|
||||
}}
|
||||
/>
|
||||
</SettingItem>
|
||||
<div className="flex justify0between">
|
||||
<Button isLoading={backuping} fullWidth size="sm" className="mr-1" onPress={handleBackup}>
|
||||
备份
|
||||
</Button>
|
||||
<Button
|
||||
isLoading={restoring}
|
||||
fullWidth
|
||||
size="sm"
|
||||
className="ml-1"
|
||||
onPress={handleRestore}
|
||||
>
|
||||
恢复
|
||||
</Button>
|
||||
</div>
|
||||
</SettingCard>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default WebdavConfig
|
||||
@ -1,484 +1,49 @@
|
||||
import { Button, Input, Select, SelectItem, Switch, Tab, Tabs } from '@nextui-org/react'
|
||||
import { Button } from '@nextui-org/react'
|
||||
import BasePage from '@renderer/components/base/base-page'
|
||||
import SettingCard from '@renderer/components/base/base-setting-card'
|
||||
import SettingItem from '@renderer/components/base/base-setting-item'
|
||||
import { useAppConfig } from '@renderer/hooks/use-app-config'
|
||||
import UpdaterModal from '@renderer/components/updater/updater-modal'
|
||||
import {
|
||||
checkAutoRun,
|
||||
enableAutoRun,
|
||||
disableAutoRun,
|
||||
quitApp,
|
||||
checkUpdate,
|
||||
patchControledMihomoConfig,
|
||||
isPortable,
|
||||
setPortable,
|
||||
restartCore,
|
||||
webdavBackup,
|
||||
listWebdavBackups,
|
||||
copyEnv,
|
||||
setNativeTheme
|
||||
} from '@renderer/utils/ipc'
|
||||
import { BiCopy } from 'react-icons/bi'
|
||||
import { CgWebsite } from 'react-icons/cg'
|
||||
import { IoLogoGithub } from 'react-icons/io5'
|
||||
import { platform, version } from '@renderer/utils/init'
|
||||
import useSWR from 'swr'
|
||||
import { Key, useState } from 'react'
|
||||
import debounce from '@renderer/utils/debounce'
|
||||
import { useTheme } from 'next-themes'
|
||||
import WebdavRestoreModal from '@renderer/components/settings/webdav-restore-modal'
|
||||
import WebdavConfig from '@renderer/components/settings/webdav-config'
|
||||
import GeneralConfig from '@renderer/components/settings/general-config'
|
||||
import MihomoConfig from '@renderer/components/settings/mihomo-config'
|
||||
import Actions from '@renderer/components/settings/actions'
|
||||
import ShortcutConfig from '@renderer/components/settings/shortcut-config'
|
||||
|
||||
const Settings: React.FC = () => {
|
||||
const { setTheme } = useTheme()
|
||||
const { data: enable, mutate: mutateEnable } = useSWR('checkAutoRun', checkAutoRun)
|
||||
const { data: portable, mutate: mutatePortable } = useSWR('isPortable', isPortable)
|
||||
const { appConfig, patchAppConfig } = useAppConfig()
|
||||
const {
|
||||
silentStart = false,
|
||||
controlDns = true,
|
||||
controlSniff = true,
|
||||
useDockIcon = true,
|
||||
showTraffic = true,
|
||||
proxyInTray = true,
|
||||
envType = platform === 'win32' ? 'powershell' : 'bash',
|
||||
delayTestUrl,
|
||||
delayTestTimeout,
|
||||
autoCheckUpdate,
|
||||
userAgent,
|
||||
autoCloseConnection = true,
|
||||
appTheme = 'system',
|
||||
webdavUrl,
|
||||
webdavUsername,
|
||||
webdavPassword
|
||||
} = appConfig || {}
|
||||
const [newVersion, setNewVersion] = useState('')
|
||||
const [changelog, setChangelog] = useState('')
|
||||
const [openUpdate, setOpenUpdate] = useState(false)
|
||||
const [checkingUpdate, setCheckingUpdate] = useState(false)
|
||||
const [backuping, setBackuping] = useState(false)
|
||||
const [restoring, setRestoring] = useState(false)
|
||||
const [filenames, setFilenames] = useState<string[]>([])
|
||||
const [restoreOpen, setRestoreOpen] = useState(false)
|
||||
const [url, setUrl] = useState(delayTestUrl)
|
||||
const setUrlDebounce = debounce((v: string) => {
|
||||
patchAppConfig({ delayTestUrl: v })
|
||||
}, 500)
|
||||
const [ua, setUa] = useState(userAgent)
|
||||
const setUaDebounce = debounce((v: string) => {
|
||||
patchAppConfig({ userAgent: v })
|
||||
}, 500)
|
||||
const [webdav, setWebdav] = useState({ webdavUrl, webdavUsername, webdavPassword })
|
||||
const setWebdavDebounce = debounce(({ webdavUrl, webdavUsername, webdavPassword }) => {
|
||||
patchAppConfig({ webdavUrl, webdavUsername, webdavPassword })
|
||||
}, 500)
|
||||
const onThemeChange = (key: Key, type: 'theme' | 'color'): void => {
|
||||
const [theme, color] = appTheme.split('-')
|
||||
|
||||
if (type === 'theme') {
|
||||
let themeStr = key.toString()
|
||||
if (key !== 'system') {
|
||||
if (color) {
|
||||
themeStr += `-${color}`
|
||||
}
|
||||
}
|
||||
if (themeStr.includes('light')) {
|
||||
setNativeTheme('light')
|
||||
} else if (themeStr === 'system') {
|
||||
setNativeTheme('system')
|
||||
} else {
|
||||
setNativeTheme('dark')
|
||||
}
|
||||
setTheme(themeStr)
|
||||
patchAppConfig({ appTheme: themeStr as AppTheme })
|
||||
} else {
|
||||
let themeStr = theme
|
||||
if (theme !== 'system') {
|
||||
if (key !== 'blue') {
|
||||
themeStr += `-${key}`
|
||||
}
|
||||
setTheme(themeStr)
|
||||
patchAppConfig({ appTheme: themeStr as AppTheme })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const handleBackup = async (): Promise<void> => {
|
||||
setBackuping(true)
|
||||
try {
|
||||
await webdavBackup()
|
||||
new window.Notification('备份成功', { body: '备份文件已上传至WebDav' })
|
||||
} catch (e) {
|
||||
alert(e)
|
||||
} finally {
|
||||
setBackuping(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleRestore = async (): Promise<void> => {
|
||||
try {
|
||||
setRestoring(true)
|
||||
const filenames = await listWebdavBackups()
|
||||
setRestoring(false)
|
||||
setFilenames(filenames)
|
||||
setRestoreOpen(true)
|
||||
} catch (e) {
|
||||
alert(`获取备份列表失败: ${e}`)
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{restoreOpen && (
|
||||
<WebdavRestoreModal filenames={filenames} onClose={() => setRestoreOpen(false)} />
|
||||
)}
|
||||
{openUpdate && (
|
||||
<UpdaterModal
|
||||
onClose={() => setOpenUpdate(false)}
|
||||
version={newVersion}
|
||||
changelog={changelog}
|
||||
/>
|
||||
)}
|
||||
|
||||
<BasePage
|
||||
title="应用设置"
|
||||
header={
|
||||
<>
|
||||
<Button
|
||||
isIconOnly
|
||||
size="sm"
|
||||
title="官方文档"
|
||||
className="mr-2"
|
||||
onPress={() => {
|
||||
window.open('https://mihomo.party')
|
||||
}}
|
||||
>
|
||||
<CgWebsite className="text-lg" />
|
||||
</Button>
|
||||
<Button
|
||||
isIconOnly
|
||||
size="sm"
|
||||
title="GitHub仓库"
|
||||
onPress={() => {
|
||||
window.open('https://github.com/pompurin404/mihomo-party')
|
||||
}}
|
||||
>
|
||||
<IoLogoGithub className="text-lg" />
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<SettingCard>
|
||||
<SettingItem title="开机自启" divider>
|
||||
<Switch
|
||||
size="sm"
|
||||
isSelected={enable}
|
||||
onValueChange={async (v) => {
|
||||
try {
|
||||
if (v) {
|
||||
await enableAutoRun()
|
||||
} else {
|
||||
await disableAutoRun()
|
||||
}
|
||||
} catch (e) {
|
||||
alert(e)
|
||||
} finally {
|
||||
mutateEnable()
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</SettingItem>
|
||||
<SettingItem title="自动检查更新" divider>
|
||||
<Switch
|
||||
size="sm"
|
||||
isSelected={autoCheckUpdate}
|
||||
onValueChange={(v) => {
|
||||
patchAppConfig({ autoCheckUpdate: v })
|
||||
}}
|
||||
/>
|
||||
</SettingItem>
|
||||
<SettingItem title="静默启动" divider>
|
||||
<Switch
|
||||
size="sm"
|
||||
isSelected={silentStart}
|
||||
onValueChange={(v) => {
|
||||
patchAppConfig({ silentStart: v })
|
||||
}}
|
||||
/>
|
||||
</SettingItem>
|
||||
<SettingItem
|
||||
title="复制环境变量类型"
|
||||
actions={
|
||||
<Button isIconOnly size="sm" className="ml-2" variant="light" onPress={copyEnv}>
|
||||
<BiCopy className="text-lg" />
|
||||
</Button>
|
||||
}
|
||||
divider
|
||||
<BasePage
|
||||
title="应用设置"
|
||||
header={
|
||||
<>
|
||||
<Button
|
||||
isIconOnly
|
||||
size="sm"
|
||||
title="官方文档"
|
||||
className="mr-2"
|
||||
onPress={() => {
|
||||
window.open('https://mihomo.party')
|
||||
}}
|
||||
>
|
||||
<Select
|
||||
className="w-[150px]"
|
||||
size="sm"
|
||||
selectedKeys={new Set([envType])}
|
||||
onSelectionChange={async (v) => {
|
||||
try {
|
||||
await patchAppConfig({ envType: v.currentKey as 'bash' | 'cmd' | 'powershell' })
|
||||
} catch (e) {
|
||||
alert(e)
|
||||
}
|
||||
}}
|
||||
>
|
||||
<SelectItem key="bash">Bash</SelectItem>
|
||||
<SelectItem key="cmd">CMD</SelectItem>
|
||||
<SelectItem key="powershell">PowerShell</SelectItem>
|
||||
</Select>
|
||||
</SettingItem>
|
||||
{platform !== 'linux' && (
|
||||
<SettingItem title="托盘菜单显示节点信息" divider>
|
||||
<Switch
|
||||
size="sm"
|
||||
isSelected={proxyInTray}
|
||||
onValueChange={async (v) => {
|
||||
await patchAppConfig({ proxyInTray: v })
|
||||
}}
|
||||
/>
|
||||
</SettingItem>
|
||||
)}
|
||||
{platform === 'darwin' && (
|
||||
<>
|
||||
<SettingItem title="显示Dock图标" divider>
|
||||
<Switch
|
||||
size="sm"
|
||||
isSelected={useDockIcon}
|
||||
onValueChange={async (v) => {
|
||||
await patchAppConfig({ useDockIcon: v })
|
||||
}}
|
||||
/>
|
||||
</SettingItem>
|
||||
<SettingItem title="显示网速信息" divider>
|
||||
<Switch
|
||||
size="sm"
|
||||
isSelected={showTraffic}
|
||||
onValueChange={async (v) => {
|
||||
await patchAppConfig({ showTraffic: v })
|
||||
await restartCore()
|
||||
}}
|
||||
/>
|
||||
</SettingItem>
|
||||
</>
|
||||
)}
|
||||
{platform === 'win32' && (
|
||||
<SettingItem title="数据存储路径" divider>
|
||||
<Select
|
||||
className="w-[150px]"
|
||||
size="sm"
|
||||
selectedKeys={new Set([portable ? 'portable' : 'data'])}
|
||||
onSelectionChange={async (v) => {
|
||||
try {
|
||||
await setPortable(v.currentKey === 'portable')
|
||||
} catch (e) {
|
||||
alert(e)
|
||||
} finally {
|
||||
mutatePortable()
|
||||
}
|
||||
}}
|
||||
>
|
||||
<SelectItem key="data">AppData</SelectItem>
|
||||
<SelectItem key="portable">安装目录</SelectItem>
|
||||
</Select>
|
||||
</SettingItem>
|
||||
)}
|
||||
<SettingItem title="背景色" divider={appTheme !== 'system'}>
|
||||
<Tabs
|
||||
size="sm"
|
||||
color="primary"
|
||||
selectedKey={appTheme.split('-')[0]}
|
||||
onSelectionChange={(key) => {
|
||||
onThemeChange(key, 'theme')
|
||||
}}
|
||||
>
|
||||
<Tab key="system" title="自动" />
|
||||
<Tab key="dark" title="深色" />
|
||||
<Tab key="gray" title="灰色" />
|
||||
<Tab key="light" title="浅色" />
|
||||
</Tabs>
|
||||
</SettingItem>
|
||||
{appTheme !== 'system' && (
|
||||
<SettingItem title="主题色">
|
||||
<Tabs
|
||||
size="sm"
|
||||
color="primary"
|
||||
selectedKey={appTheme.split('-')[1] || 'blue'}
|
||||
onSelectionChange={(key) => {
|
||||
onThemeChange(key, 'color')
|
||||
}}
|
||||
>
|
||||
<Tab key="blue" title="蓝色" />
|
||||
<Tab key="pink" title="粉色" />
|
||||
<Tab key="green" title="绿色" />
|
||||
</Tabs>
|
||||
</SettingItem>
|
||||
)}
|
||||
</SettingCard>
|
||||
<SettingCard>
|
||||
<SettingItem title="WebDav地址" divider>
|
||||
<Input
|
||||
size="sm"
|
||||
className="w-[60%]"
|
||||
value={webdav.webdavUrl}
|
||||
onValueChange={(v) => {
|
||||
setWebdav({ ...webdav, webdavUrl: v })
|
||||
setWebdavDebounce({ ...webdav, webdavUrl: v })
|
||||
}}
|
||||
/>
|
||||
</SettingItem>
|
||||
<SettingItem title="WebDav用户名" divider>
|
||||
<Input
|
||||
size="sm"
|
||||
className="w-[60%]"
|
||||
value={webdav.webdavUsername}
|
||||
onValueChange={(v) => {
|
||||
setWebdav({ ...webdav, webdavUsername: v })
|
||||
setWebdavDebounce({ ...webdav, webdavUsername: v })
|
||||
}}
|
||||
/>
|
||||
</SettingItem>
|
||||
<SettingItem title="WebDav密码" divider>
|
||||
<Input
|
||||
size="sm"
|
||||
className="w-[60%]"
|
||||
type="password"
|
||||
value={webdav.webdavPassword}
|
||||
onValueChange={(v) => {
|
||||
setWebdav({ ...webdav, webdavPassword: v })
|
||||
setWebdavDebounce({ ...webdav, webdavPassword: v })
|
||||
}}
|
||||
/>
|
||||
</SettingItem>
|
||||
<div className="flex justify0between">
|
||||
<Button
|
||||
isLoading={backuping}
|
||||
fullWidth
|
||||
size="sm"
|
||||
className="mr-1"
|
||||
onPress={handleBackup}
|
||||
>
|
||||
备份
|
||||
</Button>
|
||||
<Button
|
||||
isLoading={restoring}
|
||||
fullWidth
|
||||
size="sm"
|
||||
className="ml-1"
|
||||
onPress={handleRestore}
|
||||
>
|
||||
恢复
|
||||
</Button>
|
||||
</div>
|
||||
</SettingCard>
|
||||
<SettingCard>
|
||||
<SettingItem title="订阅拉取 UA" divider>
|
||||
<Input
|
||||
size="sm"
|
||||
className="w-[60%]"
|
||||
value={ua}
|
||||
placeholder="默认 clash-meta"
|
||||
onValueChange={(v) => {
|
||||
setUa(v)
|
||||
setUaDebounce(v)
|
||||
}}
|
||||
></Input>
|
||||
</SettingItem>
|
||||
<SettingItem title="延迟测试地址" divider>
|
||||
<Input
|
||||
size="sm"
|
||||
className="w-[60%]"
|
||||
value={url}
|
||||
placeholder="默认https://www.gstatic.com/generate_204"
|
||||
onValueChange={(v) => {
|
||||
setUrl(v)
|
||||
setUrlDebounce(v)
|
||||
}}
|
||||
></Input>
|
||||
</SettingItem>
|
||||
<SettingItem title="延迟测试超时时间" divider>
|
||||
<Input
|
||||
type="number"
|
||||
size="sm"
|
||||
className="w-[60%]"
|
||||
value={delayTestTimeout?.toString()}
|
||||
placeholder="默认5000"
|
||||
onValueChange={(v) => {
|
||||
patchAppConfig({ delayTestTimeout: parseInt(v) })
|
||||
}}
|
||||
/>
|
||||
</SettingItem>
|
||||
<SettingItem title="接管DNS设置" divider>
|
||||
<Switch
|
||||
size="sm"
|
||||
isSelected={controlDns}
|
||||
onValueChange={async (v) => {
|
||||
await patchAppConfig({ controlDns: v })
|
||||
await patchControledMihomoConfig({})
|
||||
}}
|
||||
/>
|
||||
</SettingItem>
|
||||
<SettingItem title="接管域名嗅探设置" divider>
|
||||
<Switch
|
||||
size="sm"
|
||||
isSelected={controlSniff}
|
||||
onValueChange={async (v) => {
|
||||
await patchAppConfig({ controlSniff: v })
|
||||
await patchControledMihomoConfig({})
|
||||
}}
|
||||
/>
|
||||
</SettingItem>
|
||||
<SettingItem title="自动断开连接">
|
||||
<Switch
|
||||
size="sm"
|
||||
isSelected={autoCloseConnection}
|
||||
onValueChange={(v) => {
|
||||
patchAppConfig({ autoCloseConnection: v })
|
||||
}}
|
||||
/>
|
||||
</SettingItem>
|
||||
</SettingCard>
|
||||
<SettingCard>
|
||||
<SettingItem title="检查更新" divider>
|
||||
<Button
|
||||
size="sm"
|
||||
isLoading={checkingUpdate}
|
||||
onPress={async () => {
|
||||
try {
|
||||
setCheckingUpdate(true)
|
||||
const version = await checkUpdate()
|
||||
if (version) {
|
||||
setNewVersion(version.version)
|
||||
setChangelog(version.changelog)
|
||||
setOpenUpdate(true)
|
||||
} else {
|
||||
new window.Notification('当前已是最新版本', { body: '无需更新' })
|
||||
}
|
||||
} catch (e) {
|
||||
alert(e)
|
||||
} finally {
|
||||
setCheckingUpdate(false)
|
||||
}
|
||||
}}
|
||||
>
|
||||
检查更新
|
||||
</Button>
|
||||
</SettingItem>
|
||||
<SettingItem title="退出应用" divider>
|
||||
<Button size="sm" onPress={quitApp}>
|
||||
退出应用
|
||||
</Button>
|
||||
</SettingItem>
|
||||
<SettingItem title="应用版本">
|
||||
<div>v{version}</div>
|
||||
</SettingItem>
|
||||
</SettingCard>
|
||||
</BasePage>
|
||||
</>
|
||||
<CgWebsite className="text-lg" />
|
||||
</Button>
|
||||
<Button
|
||||
isIconOnly
|
||||
size="sm"
|
||||
title="GitHub仓库"
|
||||
onPress={() => {
|
||||
window.open('https://github.com/pompurin404/mihomo-party')
|
||||
}}
|
||||
>
|
||||
<IoLogoGithub className="text-lg" />
|
||||
</Button>
|
||||
</>
|
||||
}
|
||||
>
|
||||
<GeneralConfig />
|
||||
<WebdavConfig />
|
||||
<MihomoConfig />
|
||||
<ShortcutConfig />
|
||||
<Actions />
|
||||
</BasePage>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -287,6 +287,16 @@ export async function setNativeTheme(theme: 'system' | 'light' | 'dark'): Promis
|
||||
return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('setNativeTheme', theme))
|
||||
}
|
||||
|
||||
export async function registerShortcut(
|
||||
oldShortcut: string,
|
||||
newShortcut: string,
|
||||
action: string
|
||||
): Promise<boolean> {
|
||||
return ipcErrorWrapper(
|
||||
await window.electron.ipcRenderer.invoke('registerShortcut', oldShortcut, newShortcut, action)
|
||||
)
|
||||
}
|
||||
|
||||
export async function copyEnv(): Promise<void> {
|
||||
return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('copyEnv'))
|
||||
}
|
||||
|
||||
8
src/shared/types.d.ts
vendored
8
src/shared/types.d.ts
vendored
@ -235,6 +235,14 @@ interface IAppConfig {
|
||||
webdavPassword?: string
|
||||
useNameserverPolicy: boolean
|
||||
nameserverPolicy: { [key: string]: string | string[] }
|
||||
showWindowShortcut?: string
|
||||
triggerSysProxyShortcut?: string
|
||||
triggerTunShortcut?: string
|
||||
ruleModeShortcut?: string
|
||||
globalModeShortcut?: string
|
||||
directModeShortcut?: string
|
||||
restartAppShortcut?: string
|
||||
quitAppShortcut?: string
|
||||
}
|
||||
|
||||
interface IMihomoTunConfig {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user