diff --git a/src/main/index.ts b/src/main/index.ts index db481f6..5214bf6 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -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 () { diff --git a/src/main/resolve/shortcut.ts b/src/main/resolve/shortcut.ts new file mode 100644 index 0000000..bb0e6a2 --- /dev/null +++ b/src/main/resolve/shortcut.ts @@ -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 { + 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 { + 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 + } + } +} diff --git a/src/main/resolve/tray.ts b/src/main/resolve/tray.ts index 4caf525..64c0f24 100644 --- a/src/main/resolve/tray.ts +++ b/src/main/resolve/tray.ts @@ -22,7 +22,19 @@ export let tray: Tray | null = null const buildContextMenu = async (): Promise => { 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 => { const contextMenu = [ { id: 'show', + accelerator: showWindowShortcut, label: '显示窗口', type: 'normal', click: (): void => { @@ -75,6 +88,7 @@ const buildContextMenu = async (): Promise => { { id: 'rule', label: '规则模式', + accelerator: ruleModeShortcut, type: 'radio', checked: mode === 'rule', click: async (): Promise => { @@ -87,6 +101,7 @@ const buildContextMenu = async (): Promise => { { id: 'global', label: '全局模式', + accelerator: globalModeShortcut, type: 'radio', checked: mode === 'global', click: async (): Promise => { @@ -99,6 +114,7 @@ const buildContextMenu = async (): Promise => { { id: 'direct', label: '直连模式', + accelerator: directModeShortcut, type: 'radio', checked: mode === 'direct', click: async (): Promise => { @@ -112,14 +128,15 @@ const buildContextMenu = async (): Promise => { { type: 'checkbox', label: '系统代理', + accelerator: triggerSysProxyShortcut, checked: sysProxy.enable, click: async (item): Promise => { 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 => { { type: 'checkbox', label: '虚拟网卡', + accelerator: triggerTunShortcut, checked: tun?.enable ?? false, click: async (item): Promise => { const enable = item.checked @@ -181,12 +199,19 @@ const buildContextMenu = async (): Promise => { 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) } diff --git a/src/main/utils/ipc.ts b/src/main/utils/ipc.ts index 609768f..69bbb0f 100644 --- a/src/main/utils/ipc.ts +++ b/src/main/utils/ipc.ts @@ -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( // eslint-disable-next-line @typescript-eslint/no-explicit-any fn: (...args: any[]) => Promise // 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) }) diff --git a/src/renderer/src/components/settings/actions.tsx b/src/renderer/src/components/settings/actions.tsx new file mode 100644 index 0000000..f11c3fd --- /dev/null +++ b/src/renderer/src/components/settings/actions.tsx @@ -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 && ( + setOpenUpdate(false)} + version={newVersion} + changelog={changelog} + /> + )} + + + + + + + + +
v{version}
+
+
+ + ) +} + +export default Actions diff --git a/src/renderer/src/components/settings/general-config.tsx b/src/renderer/src/components/settings/general-config.tsx new file mode 100644 index 0000000..10a98db --- /dev/null +++ b/src/renderer/src/components/settings/general-config.tsx @@ -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 ( + + + { + try { + if (v) { + await enableAutoRun() + } else { + await disableAutoRun() + } + } catch (e) { + alert(e) + } finally { + mutateEnable() + } + }} + /> + + + { + patchAppConfig({ autoCheckUpdate: v }) + }} + /> + + + { + patchAppConfig({ silentStart: v }) + }} + /> + + + + + } + divider + > + + + {platform !== 'linux' && ( + + { + await patchAppConfig({ proxyInTray: v }) + }} + /> + + )} + {platform === 'darwin' && ( + <> + + { + await patchAppConfig({ useDockIcon: v }) + }} + /> + + + { + await patchAppConfig({ showTraffic: v }) + await restartCore() + }} + /> + + + )} + {platform === 'win32' && ( + + + + )} + + { + onThemeChange(key, 'theme') + }} + > + + + + + + + {appTheme !== 'system' && ( + + { + onThemeChange(key, 'color') + }} + > + + + + + + )} + + ) +} + +export default GeneralConfig diff --git a/src/renderer/src/components/settings/mihomo-config.tsx b/src/renderer/src/components/settings/mihomo-config.tsx new file mode 100644 index 0000000..b148b59 --- /dev/null +++ b/src/renderer/src/components/settings/mihomo-config.tsx @@ -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 ( + + + { + setUa(v) + setUaDebounce(v) + }} + > + + + { + setUrl(v) + setUrlDebounce(v) + }} + > + + + { + patchAppConfig({ delayTestTimeout: parseInt(v) }) + }} + /> + + + { + await patchAppConfig({ controlDns: v }) + await patchControledMihomoConfig({}) + }} + /> + + + { + await patchAppConfig({ controlSniff: v }) + await patchControledMihomoConfig({}) + }} + /> + + + { + patchAppConfig({ autoCloseConnection: v }) + }} + /> + + + ) +} + +export default MihomoConfig diff --git a/src/renderer/src/components/settings/shortcut-config.tsx b/src/renderer/src/components/settings/shortcut-config.tsx new file mode 100644 index 0000000..29b83b4 --- /dev/null +++ b/src/renderer/src/components/settings/shortcut-config.tsx @@ -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 ( + + +
+ +
+
+ +
+ +
+
+ +
+ +
+
+ +
+ +
+
+ +
+ +
+
+ +
+ +
+
+ +
+ +
+
+ +
+ +
+
+
+ ) +} + +const ShortcutInput: React.FC<{ + value: string + action: string + patchAppConfig: (value: Partial) => Promise +}> = (props) => { + const { value, action, patchAppConfig } = props + const [inputValue, setInputValue] = useState(value) + + const parseShortcut = ( + event: KeyboardEvent, + setKey: { (value: React.SetStateAction): 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 && ( + + )} + { + parseShortcut(e, setInputValue) + }} + size="sm" + onClear={() => setInputValue('')} + value={inputValue} + className="w-[calc(100%-72px)] pr-0" + /> + + ) +} + +export default ShortcutConfig diff --git a/src/renderer/src/components/settings/webdav-config.tsx b/src/renderer/src/components/settings/webdav-config.tsx new file mode 100644 index 0000000..69f0f1e --- /dev/null +++ b/src/renderer/src/components/settings/webdav-config.tsx @@ -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([]) + 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 => { + setBackuping(true) + try { + await webdavBackup() + new window.Notification('备份成功', { body: '备份文件已上传至WebDav' }) + } catch (e) { + alert(e) + } finally { + setBackuping(false) + } + } + + const handleRestore = async (): Promise => { + try { + setRestoring(true) + const filenames = await listWebdavBackups() + setFilenames(filenames) + setRestoreOpen(true) + } catch (e) { + alert(`获取备份列表失败: ${e}`) + } finally { + setRestoring(false) + } + } + return ( + <> + {restoreOpen && ( + setRestoreOpen(false)} /> + )} + + + { + setWebdav({ ...webdav, webdavUrl: v }) + setWebdavDebounce({ ...webdav, webdavUrl: v }) + }} + /> + + + { + setWebdav({ ...webdav, webdavUsername: v }) + setWebdavDebounce({ ...webdav, webdavUsername: v }) + }} + /> + + + { + setWebdav({ ...webdav, webdavPassword: v }) + setWebdavDebounce({ ...webdav, webdavPassword: v }) + }} + /> + +
+ + +
+
+ + ) +} + +export default WebdavConfig diff --git a/src/renderer/src/pages/settings.tsx b/src/renderer/src/pages/settings.tsx index 1ec6748..e2a3169 100644 --- a/src/renderer/src/pages/settings.tsx +++ b/src/renderer/src/pages/settings.tsx @@ -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([]) - 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 => { - setBackuping(true) - try { - await webdavBackup() - new window.Notification('备份成功', { body: '备份文件已上传至WebDav' }) - } catch (e) { - alert(e) - } finally { - setBackuping(false) - } - } - - const handleRestore = async (): Promise => { - try { - setRestoring(true) - const filenames = await listWebdavBackups() - setRestoring(false) - setFilenames(filenames) - setRestoreOpen(true) - } catch (e) { - alert(`获取备份列表失败: ${e}`) - } - } - return ( - <> - {restoreOpen && ( - setRestoreOpen(false)} /> - )} - {openUpdate && ( - setOpenUpdate(false)} - version={newVersion} - changelog={changelog} - /> - )} - - - - - - } - > - - - { - try { - if (v) { - await enableAutoRun() - } else { - await disableAutoRun() - } - } catch (e) { - alert(e) - } finally { - mutateEnable() - } - }} - /> - - - { - patchAppConfig({ autoCheckUpdate: v }) - }} - /> - - - { - patchAppConfig({ silentStart: v }) - }} - /> - - - - - } - divider + + - - - - - - { - setUa(v) - setUaDebounce(v) - }} - > - - - { - setUrl(v) - setUrlDebounce(v) - }} - > - - - { - patchAppConfig({ delayTestTimeout: parseInt(v) }) - }} - /> - - - { - await patchAppConfig({ controlDns: v }) - await patchControledMihomoConfig({}) - }} - /> - - - { - await patchAppConfig({ controlSniff: v }) - await patchControledMihomoConfig({}) - }} - /> - - - { - patchAppConfig({ autoCloseConnection: v }) - }} - /> - - - - - - - - - - -
v{version}
-
-
-
- + + + + + } + > + + + + + + ) } diff --git a/src/renderer/src/utils/ipc.ts b/src/renderer/src/utils/ipc.ts index f03beea..a86268b 100644 --- a/src/renderer/src/utils/ipc.ts +++ b/src/renderer/src/utils/ipc.ts @@ -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 { + return ipcErrorWrapper( + await window.electron.ipcRenderer.invoke('registerShortcut', oldShortcut, newShortcut, action) + ) +} + export async function copyEnv(): Promise { return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('copyEnv')) } diff --git a/src/shared/types.d.ts b/src/shared/types.d.ts index a9199a6..c9d9aa5 100644 --- a/src/shared/types.d.ts +++ b/src/shared/types.d.ts @@ -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 {