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 { createTray } from './resolve/tray'
|
||||||
import { init } from './utils/init'
|
import { init } from './utils/init'
|
||||||
import { join } from 'path'
|
import { join } from 'path'
|
||||||
|
import { initShortcut } from './resolve/shortcut'
|
||||||
|
|
||||||
export let mainWindow: BrowserWindow | null = null
|
export let mainWindow: BrowserWindow | null = null
|
||||||
|
|
||||||
@ -69,6 +70,7 @@ app.whenReady().then(async () => {
|
|||||||
optimizer.watchWindowShortcuts(window)
|
optimizer.watchWindowShortcuts(window)
|
||||||
})
|
})
|
||||||
registerIpcMainHandlers()
|
registerIpcMainHandlers()
|
||||||
|
await initShortcut()
|
||||||
createWindow()
|
createWindow()
|
||||||
await createTray()
|
await createTray()
|
||||||
app.on('activate', function () {
|
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 buildContextMenu = async (): Promise<Menu> => {
|
||||||
const { mode, tun } = await getControledMihomoConfig()
|
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[] = []
|
let groupsMenu: Electron.MenuItemConstructorOptions[] = []
|
||||||
if (proxyInTray && process.platform !== 'linux') {
|
if (proxyInTray && process.platform !== 'linux') {
|
||||||
try {
|
try {
|
||||||
@ -66,6 +78,7 @@ const buildContextMenu = async (): Promise<Menu> => {
|
|||||||
const contextMenu = [
|
const contextMenu = [
|
||||||
{
|
{
|
||||||
id: 'show',
|
id: 'show',
|
||||||
|
accelerator: showWindowShortcut,
|
||||||
label: '显示窗口',
|
label: '显示窗口',
|
||||||
type: 'normal',
|
type: 'normal',
|
||||||
click: (): void => {
|
click: (): void => {
|
||||||
@ -75,6 +88,7 @@ const buildContextMenu = async (): Promise<Menu> => {
|
|||||||
{
|
{
|
||||||
id: 'rule',
|
id: 'rule',
|
||||||
label: '规则模式',
|
label: '规则模式',
|
||||||
|
accelerator: ruleModeShortcut,
|
||||||
type: 'radio',
|
type: 'radio',
|
||||||
checked: mode === 'rule',
|
checked: mode === 'rule',
|
||||||
click: async (): Promise<void> => {
|
click: async (): Promise<void> => {
|
||||||
@ -87,6 +101,7 @@ const buildContextMenu = async (): Promise<Menu> => {
|
|||||||
{
|
{
|
||||||
id: 'global',
|
id: 'global',
|
||||||
label: '全局模式',
|
label: '全局模式',
|
||||||
|
accelerator: globalModeShortcut,
|
||||||
type: 'radio',
|
type: 'radio',
|
||||||
checked: mode === 'global',
|
checked: mode === 'global',
|
||||||
click: async (): Promise<void> => {
|
click: async (): Promise<void> => {
|
||||||
@ -99,6 +114,7 @@ const buildContextMenu = async (): Promise<Menu> => {
|
|||||||
{
|
{
|
||||||
id: 'direct',
|
id: 'direct',
|
||||||
label: '直连模式',
|
label: '直连模式',
|
||||||
|
accelerator: directModeShortcut,
|
||||||
type: 'radio',
|
type: 'radio',
|
||||||
checked: mode === 'direct',
|
checked: mode === 'direct',
|
||||||
click: async (): Promise<void> => {
|
click: async (): Promise<void> => {
|
||||||
@ -112,14 +128,15 @@ const buildContextMenu = async (): Promise<Menu> => {
|
|||||||
{
|
{
|
||||||
type: 'checkbox',
|
type: 'checkbox',
|
||||||
label: '系统代理',
|
label: '系统代理',
|
||||||
|
accelerator: triggerSysProxyShortcut,
|
||||||
checked: sysProxy.enable,
|
checked: sysProxy.enable,
|
||||||
click: async (item): Promise<void> => {
|
click: async (item): Promise<void> => {
|
||||||
const enable = item.checked
|
const enable = item.checked
|
||||||
try {
|
try {
|
||||||
|
await triggerSysProxy(enable)
|
||||||
await patchAppConfig({ sysProxy: { enable } })
|
await patchAppConfig({ sysProxy: { enable } })
|
||||||
triggerSysProxy(enable)
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
await patchAppConfig({ sysProxy: { enable: !enable } })
|
// ignore
|
||||||
} finally {
|
} finally {
|
||||||
mainWindow?.webContents.send('appConfigUpdated')
|
mainWindow?.webContents.send('appConfigUpdated')
|
||||||
ipcMain.emit('updateTrayMenu')
|
ipcMain.emit('updateTrayMenu')
|
||||||
@ -129,6 +146,7 @@ const buildContextMenu = async (): Promise<Menu> => {
|
|||||||
{
|
{
|
||||||
type: 'checkbox',
|
type: 'checkbox',
|
||||||
label: '虚拟网卡',
|
label: '虚拟网卡',
|
||||||
|
accelerator: triggerTunShortcut,
|
||||||
checked: tun?.enable ?? false,
|
checked: tun?.enable ?? false,
|
||||||
click: async (item): Promise<void> => {
|
click: async (item): Promise<void> => {
|
||||||
const enable = item.checked
|
const enable = item.checked
|
||||||
@ -181,12 +199,19 @@ const buildContextMenu = async (): Promise<Menu> => {
|
|||||||
id: 'restart',
|
id: 'restart',
|
||||||
label: '重启应用',
|
label: '重启应用',
|
||||||
type: 'normal',
|
type: 'normal',
|
||||||
|
accelerator: restartAppShortcut,
|
||||||
click: (): void => {
|
click: (): void => {
|
||||||
app.relaunch()
|
app.relaunch()
|
||||||
app.quit()
|
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[]
|
] as Electron.MenuItemConstructorOptions[]
|
||||||
return Menu.buildFromTemplate(contextMenu)
|
return Menu.buildFromTemplate(contextMenu)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -54,6 +54,7 @@ import { isPortable, setPortable } from './dirs'
|
|||||||
import { listWebdavBackups, webdavBackup, webdavRestore } from '../resolve/backup'
|
import { listWebdavBackups, webdavBackup, webdavRestore } from '../resolve/backup'
|
||||||
import { getInterfaces } from '../sys/interface'
|
import { getInterfaces } from '../sys/interface'
|
||||||
import { copyEnv } from '../resolve/tray'
|
import { copyEnv } from '../resolve/tray'
|
||||||
|
import { registerShortcut } from '../resolve/shortcut'
|
||||||
|
|
||||||
function ipcErrorWrapper<T>( // eslint-disable-next-line @typescript-eslint/no-explicit-any
|
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
|
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('webdavBackup', ipcErrorWrapper(webdavBackup))
|
||||||
ipcMain.handle('webdavRestore', (_e, filename) => ipcErrorWrapper(webdavRestore)(filename))
|
ipcMain.handle('webdavRestore', (_e, filename) => ipcErrorWrapper(webdavRestore)(filename))
|
||||||
ipcMain.handle('listWebdavBackups', ipcErrorWrapper(listWebdavBackups))
|
ipcMain.handle('listWebdavBackups', ipcErrorWrapper(listWebdavBackups))
|
||||||
|
ipcMain.handle('registerShortcut', (_e, oldShortcut, newShortcut, action) =>
|
||||||
|
ipcErrorWrapper(registerShortcut)(oldShortcut, newShortcut, action)
|
||||||
|
)
|
||||||
ipcMain.handle('setNativeTheme', (_e, theme) => {
|
ipcMain.handle('setNativeTheme', (_e, theme) => {
|
||||||
setNativeTheme(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,145 +1,15 @@
|
|||||||
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 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 { CgWebsite } from 'react-icons/cg'
|
||||||
import { IoLogoGithub } from 'react-icons/io5'
|
import { IoLogoGithub } from 'react-icons/io5'
|
||||||
import { platform, version } from '@renderer/utils/init'
|
import WebdavConfig from '@renderer/components/settings/webdav-config'
|
||||||
import useSWR from 'swr'
|
import GeneralConfig from '@renderer/components/settings/general-config'
|
||||||
import { Key, useState } from 'react'
|
import MihomoConfig from '@renderer/components/settings/mihomo-config'
|
||||||
import debounce from '@renderer/utils/debounce'
|
import Actions from '@renderer/components/settings/actions'
|
||||||
import { useTheme } from 'next-themes'
|
import ShortcutConfig from '@renderer/components/settings/shortcut-config'
|
||||||
import WebdavRestoreModal from '@renderer/components/settings/webdav-restore-modal'
|
|
||||||
|
|
||||||
const Settings: React.FC = () => {
|
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 (
|
return (
|
||||||
<>
|
|
||||||
{restoreOpen && (
|
|
||||||
<WebdavRestoreModal filenames={filenames} onClose={() => setRestoreOpen(false)} />
|
|
||||||
)}
|
|
||||||
{openUpdate && (
|
|
||||||
<UpdaterModal
|
|
||||||
onClose={() => setOpenUpdate(false)}
|
|
||||||
version={newVersion}
|
|
||||||
changelog={changelog}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<BasePage
|
<BasePage
|
||||||
title="应用设置"
|
title="应用设置"
|
||||||
header={
|
header={
|
||||||
@ -168,317 +38,12 @@ const Settings: React.FC = () => {
|
|||||||
</>
|
</>
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<SettingCard>
|
<GeneralConfig />
|
||||||
<SettingItem title="开机自启" divider>
|
<WebdavConfig />
|
||||||
<Switch
|
<MihomoConfig />
|
||||||
size="sm"
|
<ShortcutConfig />
|
||||||
isSelected={enable}
|
<Actions />
|
||||||
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>
|
|
||||||
<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>
|
</BasePage>
|
||||||
</>
|
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -287,6 +287,16 @@ export async function setNativeTheme(theme: 'system' | 'light' | 'dark'): Promis
|
|||||||
return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('setNativeTheme', theme))
|
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> {
|
export async function copyEnv(): Promise<void> {
|
||||||
return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('copyEnv'))
|
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
|
webdavPassword?: string
|
||||||
useNameserverPolicy: boolean
|
useNameserverPolicy: boolean
|
||||||
nameserverPolicy: { [key: string]: string | string[] }
|
nameserverPolicy: { [key: string]: string | string[] }
|
||||||
|
showWindowShortcut?: string
|
||||||
|
triggerSysProxyShortcut?: string
|
||||||
|
triggerTunShortcut?: string
|
||||||
|
ruleModeShortcut?: string
|
||||||
|
globalModeShortcut?: string
|
||||||
|
directModeShortcut?: string
|
||||||
|
restartAppShortcut?: string
|
||||||
|
quitAppShortcut?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
interface IMihomoTunConfig {
|
interface IMihomoTunConfig {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user