mirror of
https://gh.catmak.name/https://github.com/mihomo-party-org/mihomo-party
synced 2025-12-26 20:50:30 +08:00
feat: add i18n support with English translations
This commit is contained in:
parent
fa3c412146
commit
8210b477ab
@ -35,7 +35,9 @@
|
||||
"crypto-js": "^4.2.0",
|
||||
"dayjs": "^1.11.13",
|
||||
"express": "^5.0.1",
|
||||
"i18next": "^24.2.2",
|
||||
"iconv-lite": "^0.6.3",
|
||||
"react-i18next": "^15.4.0",
|
||||
"webdav": "^5.7.1",
|
||||
"ws": "^8.18.0",
|
||||
"yaml": "^2.6.0"
|
||||
|
||||
55
pnpm-lock.yaml
generated
55
pnpm-lock.yaml
generated
@ -44,9 +44,15 @@ importers:
|
||||
express:
|
||||
specifier: ^5.0.1
|
||||
version: 5.0.1
|
||||
i18next:
|
||||
specifier: ^24.2.2
|
||||
version: 24.2.2(typescript@5.7.3)
|
||||
iconv-lite:
|
||||
specifier: ^0.6.3
|
||||
version: 0.6.3
|
||||
react-i18next:
|
||||
specifier: ^15.4.0
|
||||
version: 15.4.0(i18next@24.2.2(typescript@5.7.3))(react-dom@19.0.0(react@19.0.0))(react@19.0.0)
|
||||
webdav:
|
||||
specifier: ^5.7.1
|
||||
version: 5.7.1
|
||||
@ -3681,6 +3687,9 @@ packages:
|
||||
hot-patcher@2.0.1:
|
||||
resolution: {integrity: sha512-ECg1JFG0YzehicQaogenlcs2qg6WsXQsxtnbr1i696u5tLUjtJdQAh0u2g0Q5YV45f263Ta1GnUJsc8WIfJf4Q==}
|
||||
|
||||
html-parse-stringify@3.0.1:
|
||||
resolution: {integrity: sha512-KknJ50kTInJ7qIScF3jeaFRpMpE8/lfiTdzf/twXyPBLAGrLRTmkz3AdTnKeh40X8k9L2fdYwEp/42WGXIRGcg==}
|
||||
|
||||
html-url-attributes@3.0.1:
|
||||
resolution: {integrity: sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==}
|
||||
|
||||
@ -3706,6 +3715,14 @@ packages:
|
||||
humanize-ms@1.2.1:
|
||||
resolution: {integrity: sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==}
|
||||
|
||||
i18next@24.2.2:
|
||||
resolution: {integrity: sha512-NE6i86lBCKRYZa5TaUDkU5S4HFgLIEJRLr3Whf2psgaxBleQ2LC1YW1Vc+SCgkAW7VEzndT6al6+CzegSUHcTQ==}
|
||||
peerDependencies:
|
||||
typescript: ^5
|
||||
peerDependenciesMeta:
|
||||
typescript:
|
||||
optional: true
|
||||
|
||||
iconv-corefoundation@1.1.7:
|
||||
resolution: {integrity: sha512-T10qvkw0zz4wnm560lOEg0PovVqUXuOFhhHAkixw8/sycy7TJt7v/RrkEKEQnAw2viPSJu6iAkErxnzR0g8PpQ==}
|
||||
engines: {node: ^8.11.2 || >=10}
|
||||
@ -4711,6 +4728,19 @@ packages:
|
||||
peerDependencies:
|
||||
react: '>=16.13.1'
|
||||
|
||||
react-i18next@15.4.0:
|
||||
resolution: {integrity: sha512-Py6UkX3zV08RTvL6ZANRoBh9sL/ne6rQq79XlkHEdd82cZr2H9usbWpUNVadJntIZP2pu3M2rL1CN+5rQYfYFw==}
|
||||
peerDependencies:
|
||||
i18next: '>= 23.2.3'
|
||||
react: '>= 16.8.0'
|
||||
react-dom: '*'
|
||||
react-native: '*'
|
||||
peerDependenciesMeta:
|
||||
react-dom:
|
||||
optional: true
|
||||
react-native:
|
||||
optional: true
|
||||
|
||||
react-icons@5.4.0:
|
||||
resolution: {integrity: sha512-7eltJxgVt7X64oHh6wSWNwwbKTCtMfK35hcjvJS0yxEAhPM8oUKdS3+kqaW1vicIltw+kR2unHaa12S9pPALoQ==}
|
||||
peerDependencies:
|
||||
@ -5457,6 +5487,10 @@ packages:
|
||||
yaml:
|
||||
optional: true
|
||||
|
||||
void-elements@3.1.0:
|
||||
resolution: {integrity: sha512-Dhxzh5HZuiHQhbvTW9AMetFfBHDMYpo23Uo9btPXgdYP+3T5S+p+jgNy7spra+veYhBP2dCSgxR/i2Y02h5/6w==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
vscode-jsonrpc@8.2.0:
|
||||
resolution: {integrity: sha512-C+r0eKJUIfiDIfwJhria30+TYWPtuHJXHtI7J0YlOmKAo7ogxP20T0zxB7HZQIFhIyvoBPwWskjxrvAtfjyZfA==}
|
||||
engines: {node: '>=14.0.0'}
|
||||
@ -10179,6 +10213,10 @@ snapshots:
|
||||
|
||||
hot-patcher@2.0.1: {}
|
||||
|
||||
html-parse-stringify@3.0.1:
|
||||
dependencies:
|
||||
void-elements: 3.1.0
|
||||
|
||||
html-url-attributes@3.0.1: {}
|
||||
|
||||
http-cache-semantics@4.1.1: {}
|
||||
@ -10215,6 +10253,12 @@ snapshots:
|
||||
dependencies:
|
||||
ms: 2.1.3
|
||||
|
||||
i18next@24.2.2(typescript@5.7.3):
|
||||
dependencies:
|
||||
'@babel/runtime': 7.26.7
|
||||
optionalDependencies:
|
||||
typescript: 5.7.3
|
||||
|
||||
iconv-corefoundation@1.1.7:
|
||||
dependencies:
|
||||
cli-truncate: 2.1.0
|
||||
@ -11303,6 +11347,15 @@ snapshots:
|
||||
'@babel/runtime': 7.26.7
|
||||
react: 19.0.0
|
||||
|
||||
react-i18next@15.4.0(i18next@24.2.2(typescript@5.7.3))(react-dom@19.0.0(react@19.0.0))(react@19.0.0):
|
||||
dependencies:
|
||||
'@babel/runtime': 7.26.7
|
||||
html-parse-stringify: 3.0.1
|
||||
i18next: 24.2.2(typescript@5.7.3)
|
||||
react: 19.0.0
|
||||
optionalDependencies:
|
||||
react-dom: 19.0.0(react@19.0.0)
|
||||
|
||||
react-icons@5.4.0(react@19.0.0):
|
||||
dependencies:
|
||||
react: 19.0.0
|
||||
@ -12224,6 +12277,8 @@ snapshots:
|
||||
tsx: 4.19.2
|
||||
yaml: 2.7.0
|
||||
|
||||
void-elements@3.1.0: {}
|
||||
|
||||
vscode-jsonrpc@8.2.0: {}
|
||||
|
||||
vscode-languageserver-protocol@3.17.5:
|
||||
|
||||
@ -19,6 +19,8 @@ import path from 'path'
|
||||
import { startMonitor } from './resolve/trafficMonitor'
|
||||
import { showFloatingWindow } from './resolve/floatingWindow'
|
||||
import iconv from 'iconv-lite'
|
||||
import { initI18n } from '../shared/i18n'
|
||||
import i18next from 'i18next'
|
||||
|
||||
let quitTimeout: NodeJS.Timeout | null = null
|
||||
export let mainWindow: BrowserWindow | null = null
|
||||
@ -48,8 +50,8 @@ if (process.platform === 'win32' && !is.dev && !process.argv.includes('noadmin')
|
||||
// ignore
|
||||
}
|
||||
dialog.showErrorBox(
|
||||
'首次启动请以管理员权限运行',
|
||||
`首次启动请以管理员权限运行\n${createErrorStr}\n${eStr}`
|
||||
i18next.t('main.error.adminRequired'),
|
||||
`${i18next.t('main.error.adminRequired')}\n${createErrorStr}\n${eStr}`
|
||||
)
|
||||
} finally {
|
||||
app.exit()
|
||||
@ -122,9 +124,11 @@ app.whenReady().then(async () => {
|
||||
// Set app user model id for windows
|
||||
electronApp.setAppUserModelId('party.mihomo.app')
|
||||
try {
|
||||
const appConfig = await getAppConfig()
|
||||
await initI18n({ lng: appConfig.language })
|
||||
await initPromise
|
||||
} catch (e) {
|
||||
dialog.showErrorBox('应用初始化失败', `${e}`)
|
||||
dialog.showErrorBox(i18next.t('main.error.initFailed'), `${e}`)
|
||||
app.quit()
|
||||
}
|
||||
try {
|
||||
@ -133,7 +137,7 @@ app.whenReady().then(async () => {
|
||||
await initProfileUpdater()
|
||||
})
|
||||
} catch (e) {
|
||||
dialog.showErrorBox('内核启动出错', `${e}`)
|
||||
dialog.showErrorBox(i18next.t('main.error.coreStartFailed'), `${e}`)
|
||||
}
|
||||
try {
|
||||
await startMonitor()
|
||||
@ -174,7 +178,7 @@ async function handleDeepLink(url: string): Promise<void> {
|
||||
const profileUrl = urlObj.searchParams.get('url')
|
||||
const profileName = urlObj.searchParams.get('name')
|
||||
if (!profileUrl) {
|
||||
throw new Error('缺少参数 url')
|
||||
throw new Error(i18next.t('main.error.urlParamMissing'))
|
||||
}
|
||||
await addProfileItem({
|
||||
type: 'remote',
|
||||
@ -182,10 +186,10 @@ async function handleDeepLink(url: string): Promise<void> {
|
||||
url: profileUrl
|
||||
})
|
||||
mainWindow?.webContents.send('profileConfigUpdated')
|
||||
new Notification({ title: '订阅导入成功' }).show()
|
||||
new Notification({ title: i18next.t('main.notification.importSuccess') }).show()
|
||||
break
|
||||
} catch (e) {
|
||||
dialog.showErrorBox('订阅导入失败', `${url}\n${e}`)
|
||||
dialog.showErrorBox(i18next.t('main.error.importFailed'), `${url}\n${e}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -21,10 +21,16 @@ import { dataDir, logDir, mihomoCoreDir, mihomoWorkDir } from '../utils/dirs'
|
||||
import { triggerSysProxy } from '../sys/sysproxy'
|
||||
import { quitWithoutCore, restartCore } from '../core/manager'
|
||||
import { floatingWindow, triggerFloatingWindow } from './floatingWindow'
|
||||
import { t } from 'i18next'
|
||||
|
||||
export let tray: Tray | null = null
|
||||
|
||||
export const buildContextMenu = async (): Promise<Menu> => {
|
||||
// 添加调试日志
|
||||
console.log('Current translation for tray.showWindow:', t('tray.showWindow'))
|
||||
console.log('Current translation for tray.hideFloatingWindow:', t('tray.hideFloatingWindow'))
|
||||
console.log('Current translation for tray.showFloatingWindow:', t('tray.showFloatingWindow'))
|
||||
|
||||
const { mode, tun } = await getControledMihomoConfig()
|
||||
const {
|
||||
sysProxy,
|
||||
@ -86,7 +92,7 @@ export const buildContextMenu = async (): Promise<Menu> => {
|
||||
{
|
||||
id: 'show',
|
||||
accelerator: showWindowShortcut,
|
||||
label: '显示窗口',
|
||||
label: t('tray.showWindow'),
|
||||
type: 'normal',
|
||||
click: (): void => {
|
||||
showMainWindow()
|
||||
@ -95,7 +101,7 @@ export const buildContextMenu = async (): Promise<Menu> => {
|
||||
{
|
||||
id: 'show-floating',
|
||||
accelerator: showFloatingWindowShortcut,
|
||||
label: floatingWindow?.isVisible() ? '关闭悬浮窗' : '显示悬浮窗',
|
||||
label: floatingWindow?.isVisible() ? t('tray.hideFloatingWindow') : t('tray.showFloatingWindow'),
|
||||
type: 'normal',
|
||||
click: async (): Promise<void> => {
|
||||
await triggerFloatingWindow()
|
||||
@ -103,7 +109,7 @@ export const buildContextMenu = async (): Promise<Menu> => {
|
||||
},
|
||||
{
|
||||
id: 'rule',
|
||||
label: '规则模式',
|
||||
label: t('tray.ruleMode'),
|
||||
accelerator: ruleModeShortcut,
|
||||
type: 'radio',
|
||||
checked: mode === 'rule',
|
||||
@ -117,7 +123,7 @@ export const buildContextMenu = async (): Promise<Menu> => {
|
||||
},
|
||||
{
|
||||
id: 'global',
|
||||
label: '全局模式',
|
||||
label: t('tray.globalMode'),
|
||||
accelerator: globalModeShortcut,
|
||||
type: 'radio',
|
||||
checked: mode === 'global',
|
||||
@ -131,7 +137,7 @@ export const buildContextMenu = async (): Promise<Menu> => {
|
||||
},
|
||||
{
|
||||
id: 'direct',
|
||||
label: '直连模式',
|
||||
label: t('tray.directMode'),
|
||||
accelerator: directModeShortcut,
|
||||
type: 'radio',
|
||||
checked: mode === 'direct',
|
||||
@ -146,7 +152,7 @@ export const buildContextMenu = async (): Promise<Menu> => {
|
||||
{ type: 'separator' },
|
||||
{
|
||||
type: 'checkbox',
|
||||
label: '系统代理',
|
||||
label: t('tray.systemProxy'),
|
||||
accelerator: triggerSysProxyShortcut,
|
||||
checked: sysProxy.enable,
|
||||
click: async (item): Promise<void> => {
|
||||
@ -165,7 +171,7 @@ export const buildContextMenu = async (): Promise<Menu> => {
|
||||
},
|
||||
{
|
||||
type: 'checkbox',
|
||||
label: '虚拟网卡',
|
||||
label: t('tray.tun'),
|
||||
accelerator: triggerTunShortcut,
|
||||
checked: tun?.enable ?? false,
|
||||
click: async (item): Promise<void> => {
|
||||
@ -190,7 +196,7 @@ export const buildContextMenu = async (): Promise<Menu> => {
|
||||
{ type: 'separator' },
|
||||
{
|
||||
type: 'submenu',
|
||||
label: '订阅配置',
|
||||
label: t('tray.profiles'),
|
||||
submenu: items.map((item) => {
|
||||
return {
|
||||
type: 'radio',
|
||||
@ -208,26 +214,26 @@ export const buildContextMenu = async (): Promise<Menu> => {
|
||||
{ type: 'separator' },
|
||||
{
|
||||
type: 'submenu',
|
||||
label: '打开目录',
|
||||
label: t('tray.openDirectories.title'),
|
||||
submenu: [
|
||||
{
|
||||
type: 'normal',
|
||||
label: '应用目录',
|
||||
label: t('tray.openDirectories.appDir'),
|
||||
click: (): Promise<string> => shell.openPath(dataDir())
|
||||
},
|
||||
{
|
||||
type: 'normal',
|
||||
label: '工作目录',
|
||||
label: t('tray.openDirectories.workDir'),
|
||||
click: (): Promise<string> => shell.openPath(mihomoWorkDir())
|
||||
},
|
||||
{
|
||||
type: 'normal',
|
||||
label: '内核目录',
|
||||
label: t('tray.openDirectories.coreDir'),
|
||||
click: (): Promise<string> => shell.openPath(mihomoCoreDir())
|
||||
},
|
||||
{
|
||||
type: 'normal',
|
||||
label: '日志目录',
|
||||
label: t('tray.openDirectories.logDir'),
|
||||
click: (): Promise<string> => shell.openPath(logDir())
|
||||
}
|
||||
]
|
||||
@ -235,7 +241,7 @@ export const buildContextMenu = async (): Promise<Menu> => {
|
||||
envType.length > 1
|
||||
? {
|
||||
type: 'submenu',
|
||||
label: '复制环境变量',
|
||||
label: t('tray.copyEnv'),
|
||||
submenu: envType.map((type) => {
|
||||
return {
|
||||
id: type,
|
||||
@ -249,7 +255,7 @@ export const buildContextMenu = async (): Promise<Menu> => {
|
||||
}
|
||||
: {
|
||||
id: 'copyenv',
|
||||
label: '复制环境变量',
|
||||
label: t('tray.copyEnv'),
|
||||
type: 'normal',
|
||||
click: async (): Promise<void> => {
|
||||
await copyEnv(envType[0])
|
||||
@ -258,14 +264,14 @@ export const buildContextMenu = async (): Promise<Menu> => {
|
||||
{ type: 'separator' },
|
||||
{
|
||||
id: 'quitWithoutCore',
|
||||
label: '轻量模式',
|
||||
label: t('actions.lightMode.button'),
|
||||
type: 'normal',
|
||||
accelerator: quitWithoutCoreShortcut,
|
||||
click: quitWithoutCore
|
||||
},
|
||||
{
|
||||
id: 'restart',
|
||||
label: '重启应用',
|
||||
label: t('actions.restartApp'),
|
||||
type: 'normal',
|
||||
accelerator: restartAppShortcut,
|
||||
click: (): void => {
|
||||
@ -275,7 +281,7 @@ export const buildContextMenu = async (): Promise<Menu> => {
|
||||
},
|
||||
{
|
||||
id: 'quit',
|
||||
label: '退出应用',
|
||||
label: t('actions.quit.button'),
|
||||
type: 'normal',
|
||||
accelerator: 'CommandOrControl+Q',
|
||||
click: (): void => app.quit()
|
||||
|
||||
@ -87,6 +87,7 @@ import { getGistUrl } from '../resolve/gistApi'
|
||||
import { getImageDataURL } from './image'
|
||||
import { startMonitor } from '../resolve/trafficMonitor'
|
||||
import { closeFloatingWindow, showContextMenu, showFloatingWindow } from '../resolve/floatingWindow'
|
||||
import i18next from 'i18next'
|
||||
|
||||
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
|
||||
@ -254,4 +255,11 @@ export function registerIpcMainHandlers(): void {
|
||||
})
|
||||
ipcMain.handle('quitWithoutCore', ipcErrorWrapper(quitWithoutCore))
|
||||
ipcMain.handle('quitApp', () => app.quit())
|
||||
|
||||
// Add language change handler
|
||||
ipcMain.handle('changeLanguage', async (_e, lng) => {
|
||||
await i18next.changeLanguage(lng)
|
||||
// 触发托盘菜单更新
|
||||
ipcMain.emit('updateTrayMenu')
|
||||
})
|
||||
}
|
||||
|
||||
@ -35,10 +35,17 @@ import SubStoreCard from '@renderer/components/sider/substore-card'
|
||||
import MihomoIcon from './components/base/mihomo-icon'
|
||||
import { driver } from 'driver.js'
|
||||
import 'driver.js/dist/driver.css'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
let navigate: NavigateFunction
|
||||
let driverInstance: ReturnType<typeof driver> | null = null
|
||||
|
||||
export function getDriver(): ReturnType<typeof driver> | null {
|
||||
return driverInstance
|
||||
}
|
||||
|
||||
const App: React.FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const { appConfig, patchAppConfig } = useAppConfig()
|
||||
const {
|
||||
appTheme = 'system',
|
||||
@ -96,12 +103,188 @@ const App: React.FC = () => {
|
||||
}, [siderWidthValue, resizing])
|
||||
|
||||
useEffect(() => {
|
||||
driverInstance = driver({
|
||||
showProgress: true,
|
||||
nextBtnText: t('common.next'),
|
||||
prevBtnText: t('common.prev'),
|
||||
doneBtnText: t('common.done'),
|
||||
progressText: '{{current}} / {{total}}',
|
||||
overlayOpacity: 0.9,
|
||||
steps: [
|
||||
{
|
||||
element: 'none',
|
||||
popover: {
|
||||
title: t('guide.welcome.title'),
|
||||
description: t('guide.welcome.description'),
|
||||
side: 'over',
|
||||
align: 'center'
|
||||
}
|
||||
},
|
||||
{
|
||||
element: '.side',
|
||||
popover: {
|
||||
title: t('guide.sider.title'),
|
||||
description: t('guide.sider.description'),
|
||||
side: 'right',
|
||||
align: 'center'
|
||||
}
|
||||
},
|
||||
{
|
||||
element: '.sysproxy-card',
|
||||
popover: {
|
||||
title: t('guide.card.title'),
|
||||
description: t('guide.card.description'),
|
||||
side: 'right',
|
||||
align: 'start'
|
||||
}
|
||||
},
|
||||
{
|
||||
element: '.main',
|
||||
popover: {
|
||||
title: t('guide.main.title'),
|
||||
description: t('guide.main.description'),
|
||||
side: 'left',
|
||||
align: 'center'
|
||||
}
|
||||
},
|
||||
{
|
||||
element: '.profile-card',
|
||||
popover: {
|
||||
title: t('guide.profile.title'),
|
||||
description: t('guide.profile.description'),
|
||||
side: 'right',
|
||||
align: 'start',
|
||||
onNextClick: async (): Promise<void> => {
|
||||
navigate('/profiles')
|
||||
setTimeout(() => {
|
||||
driverInstance?.moveNext()
|
||||
}, 0)
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
element: '.profiles-sticky',
|
||||
popover: {
|
||||
title: t('guide.import.title'),
|
||||
description: t('guide.import.description'),
|
||||
side: 'bottom',
|
||||
align: 'start'
|
||||
}
|
||||
},
|
||||
{
|
||||
element: '.substore-import',
|
||||
popover: {
|
||||
title: t('guide.substore.title'),
|
||||
description: t('guide.substore.description'),
|
||||
side: 'bottom',
|
||||
align: 'start'
|
||||
}
|
||||
},
|
||||
{
|
||||
element: '.new-profile',
|
||||
popover: {
|
||||
title: t('guide.localProfile.title'),
|
||||
description: t('guide.localProfile.description'),
|
||||
side: 'bottom',
|
||||
align: 'start'
|
||||
}
|
||||
},
|
||||
{
|
||||
element: '.sysproxy-card',
|
||||
popover: {
|
||||
title: t('guide.sysproxy.title'),
|
||||
description: t('guide.sysproxy.description'),
|
||||
side: 'right',
|
||||
align: 'start',
|
||||
onNextClick: async (): Promise<void> => {
|
||||
navigate('/sysproxy')
|
||||
setTimeout(() => {
|
||||
driverInstance?.moveNext()
|
||||
}, 0)
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
element: '.sysproxy-settings',
|
||||
popover: {
|
||||
title: t('guide.sysproxySetting.title'),
|
||||
description: t('guide.sysproxySetting.description'),
|
||||
side: 'top',
|
||||
align: 'start'
|
||||
}
|
||||
},
|
||||
{
|
||||
element: '.tun-card',
|
||||
popover: {
|
||||
title: t('guide.tun.title'),
|
||||
description: t('guide.tun.description'),
|
||||
side: 'right',
|
||||
align: 'start',
|
||||
onNextClick: async (): Promise<void> => {
|
||||
navigate('/tun')
|
||||
setTimeout(() => {
|
||||
driverInstance?.moveNext()
|
||||
}, 0)
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
element: '.tun-settings',
|
||||
popover: {
|
||||
title: t('guide.tunSetting.title'),
|
||||
description: t('guide.tunSetting.description'),
|
||||
side: 'bottom',
|
||||
align: 'start'
|
||||
}
|
||||
},
|
||||
{
|
||||
element: '.override-card',
|
||||
popover: {
|
||||
title: t('guide.override.title'),
|
||||
description: t('guide.override.description'),
|
||||
side: 'right',
|
||||
align: 'center'
|
||||
}
|
||||
},
|
||||
{
|
||||
element: '.dns-card',
|
||||
popover: {
|
||||
title: t('guide.dns.title'),
|
||||
description: t('guide.dns.description'),
|
||||
side: 'right',
|
||||
align: 'center',
|
||||
onNextClick: async (): Promise<void> => {
|
||||
navigate('/profiles')
|
||||
setTimeout(() => {
|
||||
driverInstance?.moveNext()
|
||||
}, 0)
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
element: 'none',
|
||||
popover: {
|
||||
title: t('guide.end.title'),
|
||||
description: t('guide.end.description'),
|
||||
side: 'top',
|
||||
align: 'center',
|
||||
onNextClick: async (): Promise<void> => {
|
||||
navigate('/profiles')
|
||||
setTimeout(() => {
|
||||
driverInstance?.destroy()
|
||||
}, 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
const tourShown = window.localStorage.getItem('tourShown')
|
||||
if (!tourShown) {
|
||||
window.localStorage.setItem('tourShown', 'true')
|
||||
firstDriver.drive()
|
||||
driverInstance.drive()
|
||||
}
|
||||
}, [])
|
||||
}, [t])
|
||||
|
||||
useEffect(() => {
|
||||
setNativeTheme(appTheme)
|
||||
@ -295,191 +478,3 @@ const App: React.FC = () => {
|
||||
}
|
||||
|
||||
export default App
|
||||
|
||||
export const firstDriver = driver({
|
||||
showProgress: true,
|
||||
nextBtnText: '下一步',
|
||||
prevBtnText: '上一步',
|
||||
doneBtnText: '完成',
|
||||
progressText: '{{current}} / {{total}}',
|
||||
overlayOpacity: 0.9,
|
||||
steps: [
|
||||
{
|
||||
element: 'none',
|
||||
popover: {
|
||||
title: '欢迎使用 Mihomo Party',
|
||||
description:
|
||||
'这是一份交互式使用教程,如果您已经完全熟悉本软件的操作,可以直接点击右上角关闭按钮,后续您可以随时从设置中打开本教程',
|
||||
side: 'over',
|
||||
align: 'center'
|
||||
}
|
||||
},
|
||||
{
|
||||
element: '.side',
|
||||
popover: {
|
||||
title: '导航栏',
|
||||
description:
|
||||
'左侧是应用的导航栏,兼顾仪表盘功能,在这里可以切换不同页面,也可以概览常用的状态信息',
|
||||
side: 'right',
|
||||
align: 'center'
|
||||
}
|
||||
},
|
||||
{
|
||||
element: '.sysproxy-card',
|
||||
popover: {
|
||||
title: '卡片',
|
||||
description: '点击导航栏卡片可以跳转到对应页面,拖动导航栏卡片可以自由排列卡片顺序',
|
||||
side: 'right',
|
||||
align: 'start'
|
||||
}
|
||||
},
|
||||
{
|
||||
element: '.main',
|
||||
popover: {
|
||||
title: '主要区域',
|
||||
description: '右侧是应用的主要区域,展示了导航栏所选页面的内容',
|
||||
side: 'left',
|
||||
align: 'center'
|
||||
}
|
||||
},
|
||||
{
|
||||
element: '.profile-card',
|
||||
popover: {
|
||||
title: '订阅管理',
|
||||
description:
|
||||
'订阅管理卡片展示当前运行的订阅配置信息,点击进入订阅管理页面可以在这里管理订阅配置',
|
||||
side: 'right',
|
||||
align: 'start',
|
||||
onNextClick: async (): Promise<void> => {
|
||||
navigate('/profiles')
|
||||
setTimeout(() => {
|
||||
firstDriver.moveNext()
|
||||
}, 0)
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
element: '.profiles-sticky',
|
||||
popover: {
|
||||
title: '订阅导入',
|
||||
description:
|
||||
'Mihomo Party 支持多种订阅导入方式,在此输入订阅链接,点击导入即可导入您的订阅配置,如果您的订阅需要代理才能更新,请勾选“代理”再点击导入,当然这需要已经有一个可以正常使用的订阅才可以',
|
||||
side: 'bottom',
|
||||
align: 'start'
|
||||
}
|
||||
},
|
||||
{
|
||||
element: '.substore-import',
|
||||
popover: {
|
||||
title: 'Sub-Store',
|
||||
description:
|
||||
'Mihomo Party 深度集成了 Sub-Store,您可以点击该按钮进入 Sub-Store 或直接导入您通过 Sub-Store 管理的订阅,Mihomo Party 默认使用内置的 Sub-Store 后端,如果您有自建的 Sub-Store 后端,可以在设置页面中配置,如果您不使用 Sub-Store 也可以在设置页面中关闭',
|
||||
side: 'bottom',
|
||||
align: 'start'
|
||||
}
|
||||
},
|
||||
{
|
||||
element: '.new-profile',
|
||||
popover: {
|
||||
title: '本地订阅',
|
||||
description: '点击“+”可以选择本地文件进行导入或者直接新建空白配置进行编辑',
|
||||
side: 'bottom',
|
||||
align: 'start'
|
||||
}
|
||||
},
|
||||
{
|
||||
element: '.sysproxy-card',
|
||||
popover: {
|
||||
title: '系统代理',
|
||||
description:
|
||||
'导入订阅之后,内核已经开始运行并监听指定端口,此时您已经可以通过指定代理端口来使用代理了,如果您要使大部分应用自动使用该端口的代理,您还需要打开系统代理开关',
|
||||
side: 'right',
|
||||
align: 'start',
|
||||
onNextClick: async (): Promise<void> => {
|
||||
navigate('/sysproxy')
|
||||
setTimeout(() => {
|
||||
firstDriver.moveNext()
|
||||
}, 0)
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
element: '.sysproxy-settings',
|
||||
popover: {
|
||||
title: '系统代理设置',
|
||||
description:
|
||||
'在此您可以进行系统代理相关设置,选择代理模式,如果某些 Windows 应用不遵循系统代理,还可以使用“UWP 工具”解除本地回环限制,对于“手动代理模式”和“PAC 代理模式”的区别,请自行百度',
|
||||
side: 'top',
|
||||
align: 'start'
|
||||
}
|
||||
},
|
||||
{
|
||||
element: '.tun-card',
|
||||
popover: {
|
||||
title: '虚拟网卡',
|
||||
description:
|
||||
'虚拟网卡,即同类软件中常见的“Tun 模式”,对于某些不遵循系统代理的应用,您可以打开虚拟网卡以让内核接管所有流量',
|
||||
side: 'right',
|
||||
align: 'start',
|
||||
onNextClick: async (): Promise<void> => {
|
||||
navigate('/tun')
|
||||
setTimeout(() => {
|
||||
firstDriver.moveNext()
|
||||
}, 0)
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
element: '.tun-settings',
|
||||
popover: {
|
||||
title: '虚拟网卡设置',
|
||||
description:
|
||||
'这里可以更改虚拟网卡相关设置,Mihomo Party 理论上已经完全解决权限问题,如果您的虚拟网卡仍然不可用,可以尝试重设防火墙(Windows)或手动授权内核(MacOS/Linux)后重启内核',
|
||||
side: 'bottom',
|
||||
align: 'start'
|
||||
}
|
||||
},
|
||||
{
|
||||
element: '.override-card',
|
||||
popover: {
|
||||
title: '覆写',
|
||||
description:
|
||||
'Mihomo Party 提供强大的覆写功能,可以对您导入的订阅配置进行个性化修改,如添加规则、自定义代理组等,您可以直接导入别人写好的覆写文件,也可以自己动手编写,<b>编辑好覆写文件一定要记得在需要覆写的订阅上启用</b>,覆写文件的语法请参考 <a href="https://mihomo.party/docs/guide/override" target="_blank">官方文档</a>',
|
||||
side: 'right',
|
||||
align: 'center'
|
||||
}
|
||||
},
|
||||
{
|
||||
element: '.dns-card',
|
||||
popover: {
|
||||
title: 'DNS',
|
||||
description:
|
||||
'软件默认接管了内核的 DNS 设置,如果您需要使用订阅配置中的 DNS 设置,可以到应用设置中关闭“接管 DNS 设置”,域名嗅探同理',
|
||||
side: 'right',
|
||||
align: 'center',
|
||||
onNextClick: async (): Promise<void> => {
|
||||
navigate('/profiles')
|
||||
setTimeout(() => {
|
||||
firstDriver.moveNext()
|
||||
}, 0)
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
element: 'none',
|
||||
popover: {
|
||||
title: '教程结束',
|
||||
description:
|
||||
'现在您已经了解了软件的基本用法,导入您的订阅开始使用吧,祝您使用愉快!\n您还可以加入我们的官方 <a href="https://t.me/mihomo_party_group" target="_blank">Telegram 群组</a> 获取最新资讯',
|
||||
side: 'top',
|
||||
align: 'center',
|
||||
onNextClick: async (): Promise<void> => {
|
||||
navigate('/profiles')
|
||||
setTimeout(() => {
|
||||
firstDriver.destroy()
|
||||
}, 0)
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
@ -1,12 +1,15 @@
|
||||
import { Button } from '@nextui-org/react'
|
||||
import { ReactNode } from 'react'
|
||||
import { ErrorBoundary, FallbackProps } from 'react-error-boundary'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
const ErrorFallback = ({ error }: FallbackProps): JSX.Element => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<div className="p-4">
|
||||
<h2 className="my-2 text-lg font-bold">
|
||||
{'应用崩溃了 :( 请将以下信息提交给开发者以排查错误'}
|
||||
{t('common.error.appCrash')}
|
||||
</h2>
|
||||
|
||||
<Button
|
||||
@ -35,7 +38,7 @@ const ErrorFallback = ({ error }: FallbackProps): JSX.Element => {
|
||||
navigator.clipboard.writeText('```\n' + error.message + '\n' + error.stack + '\n```')
|
||||
}
|
||||
>
|
||||
复制报错信息
|
||||
{t('common.error.copyErrorMessage')}
|
||||
</Button>
|
||||
|
||||
<p className="my-2">{error.message}</p>
|
||||
|
||||
@ -4,6 +4,8 @@ import { platform } from '@renderer/utils/init'
|
||||
import { isAlwaysOnTop, setAlwaysOnTop } from '@renderer/utils/ipc'
|
||||
import React, { forwardRef, useEffect, useImperativeHandle, useRef, useState } from 'react'
|
||||
import { RiPushpin2Fill, RiPushpin2Line } from 'react-icons/ri'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
interface Props {
|
||||
title?: React.ReactNode
|
||||
header?: React.ReactNode
|
||||
@ -13,6 +15,7 @@ interface Props {
|
||||
let saveOnTop = false
|
||||
|
||||
const BasePage = forwardRef<HTMLDivElement, Props>((props, ref) => {
|
||||
const { t } = useTranslation()
|
||||
const { appConfig } = useAppConfig()
|
||||
const { useWindowFrame = false } = appConfig || {}
|
||||
const [overlayWidth, setOverlayWidth] = React.useState(0)
|
||||
@ -51,7 +54,7 @@ const BasePage = forwardRef<HTMLDivElement, Props>((props, ref) => {
|
||||
size="sm"
|
||||
className="app-nodrag"
|
||||
isIconOnly
|
||||
title="窗口置顶"
|
||||
title={t('common.pinWindow')}
|
||||
variant="light"
|
||||
color={onTop ? 'primary' : 'default'}
|
||||
onPress={async () => {
|
||||
|
||||
@ -8,6 +8,7 @@ import {
|
||||
Input
|
||||
} from '@nextui-org/react'
|
||||
import React, { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
interface Props {
|
||||
onCancel: () => void
|
||||
@ -15,22 +16,23 @@ interface Props {
|
||||
}
|
||||
|
||||
const BasePasswordModal: React.FC<Props> = (props) => {
|
||||
const { t } = useTranslation()
|
||||
const { onCancel, onConfirm } = props
|
||||
const [password, setPassword] = useState('')
|
||||
|
||||
return (
|
||||
<Modal backdrop="blur" classNames={{ backdrop: 'top-[48px]' }} hideCloseButton isOpen={true}>
|
||||
<ModalContent>
|
||||
<ModalHeader className="flex app-drag">请输入root密码</ModalHeader>
|
||||
<ModalHeader className="flex app-drag">{t('common.enterRootPassword')}</ModalHeader>
|
||||
<ModalBody>
|
||||
<Input fullWidth type="password" value={password} onValueChange={setPassword} />
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button size="sm" variant="light" onPress={onCancel}>
|
||||
取消
|
||||
{t('common.cancel')}
|
||||
</Button>
|
||||
<Button size="sm" color="primary" onPress={() => onConfirm(password)}>
|
||||
确认
|
||||
{t('common.confirm')}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
|
||||
@ -13,8 +13,9 @@ import {
|
||||
import React from 'react'
|
||||
import SettingItem from '../base/base-setting-item'
|
||||
import { calcTraffic } from '@renderer/utils/calc'
|
||||
import dayjs from 'dayjs'
|
||||
import dayjs from '@renderer/utils/dayjs'
|
||||
import { BiCopy } from 'react-icons/bi'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
interface Props {
|
||||
connection: IMihomoConnectionDetail
|
||||
@ -27,6 +28,7 @@ const CopyableSettingItem: React.FC<{
|
||||
displayName?: string
|
||||
prefix?: string[]
|
||||
}> = ({ title, value, displayName, prefix = [] }) => {
|
||||
const { t } = useTranslation()
|
||||
const getSubDomains = (domain: string): string[] =>
|
||||
domain.split('.').length <= 2
|
||||
? [domain]
|
||||
@ -93,7 +95,7 @@ const CopyableSettingItem: React.FC<{
|
||||
actions={
|
||||
<Dropdown>
|
||||
<DropdownTrigger>
|
||||
<Button title="复制规则" isIconOnly size="sm" variant="light">
|
||||
<Button title={t('connections.detail.copyRule')} isIconOnly size="sm" variant="light">
|
||||
<BiCopy className="text-lg" />
|
||||
</Button>
|
||||
</DropdownTrigger>
|
||||
@ -120,6 +122,8 @@ const CopyableSettingItem: React.FC<{
|
||||
|
||||
const ConnectionDetailModal: React.FC<Props> = (props) => {
|
||||
const { connection, onClose } = props
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<Modal
|
||||
backdrop="blur"
|
||||
@ -131,41 +135,41 @@ const ConnectionDetailModal: React.FC<Props> = (props) => {
|
||||
scrollBehavior="inside"
|
||||
>
|
||||
<ModalContent className="flag-emoji break-all">
|
||||
<ModalHeader className="flex app-drag">连接详情</ModalHeader>
|
||||
<ModalHeader className="flex app-drag">{t('connections.detail.title')}</ModalHeader>
|
||||
<ModalBody>
|
||||
<SettingItem title="连接建立时间">{dayjs(connection.start).fromNow()}</SettingItem>
|
||||
<SettingItem title="规则">
|
||||
<SettingItem title={t('connections.detail.establishTime')}>{dayjs(connection.start).fromNow()}</SettingItem>
|
||||
<SettingItem title={t('connections.detail.rule')}>
|
||||
{connection.rule}
|
||||
{connection.rulePayload ? `(${connection.rulePayload})` : ''}
|
||||
</SettingItem>
|
||||
<SettingItem title="代理链">{[...connection.chains].reverse().join('>>')}</SettingItem>
|
||||
<SettingItem title="上传速度">{calcTraffic(connection.uploadSpeed || 0)}/s</SettingItem>
|
||||
<SettingItem title="下载速度">{calcTraffic(connection.downloadSpeed || 0)}/s</SettingItem>
|
||||
<SettingItem title="上传量">{calcTraffic(connection.upload)}</SettingItem>
|
||||
<SettingItem title="下载量">{calcTraffic(connection.download)}</SettingItem>
|
||||
<SettingItem title={t('connections.detail.proxyChain')}>{[...connection.chains].reverse().join('>>')}</SettingItem>
|
||||
<SettingItem title={t('connections.uploadSpeed')}>{calcTraffic(connection.uploadSpeed || 0)}/s</SettingItem>
|
||||
<SettingItem title={t('connections.downloadSpeed')}>{calcTraffic(connection.downloadSpeed || 0)}/s</SettingItem>
|
||||
<SettingItem title={t('connections.uploadAmount')}>{calcTraffic(connection.upload)}</SettingItem>
|
||||
<SettingItem title={t('connections.downloadAmount')}>{calcTraffic(connection.download)}</SettingItem>
|
||||
<CopyableSettingItem
|
||||
title="连接类型"
|
||||
title={t('connections.detail.connectionType')}
|
||||
value={[connection.metadata.type, connection.metadata.network]}
|
||||
displayName={`${connection.metadata.type}(${connection.metadata.network})`}
|
||||
prefix={['IN-TYPE', 'NETWORK']}
|
||||
/>
|
||||
{connection.metadata.host && (
|
||||
<CopyableSettingItem
|
||||
title="主机"
|
||||
title={t('connections.detail.host')}
|
||||
value={connection.metadata.host}
|
||||
prefix={['DOMAIN', 'DOMAIN-SUFFIX']}
|
||||
/>
|
||||
)}
|
||||
{connection.metadata.sniffHost && (
|
||||
<CopyableSettingItem
|
||||
title="嗅探主机"
|
||||
title={t('connections.detail.sniffHost')}
|
||||
value={connection.metadata.sniffHost}
|
||||
prefix={['DOMAIN', 'DOMAIN-SUFFIX']}
|
||||
/>
|
||||
)}
|
||||
{connection.metadata.process && (
|
||||
<CopyableSettingItem
|
||||
title="进程名"
|
||||
title={t('connections.detail.processName')}
|
||||
value={[
|
||||
connection.metadata.process,
|
||||
...(connection.metadata.uid ? [connection.metadata.uid.toString()] : [])
|
||||
@ -176,35 +180,35 @@ const ConnectionDetailModal: React.FC<Props> = (props) => {
|
||||
)}
|
||||
{connection.metadata.processPath && (
|
||||
<CopyableSettingItem
|
||||
title="进程路径"
|
||||
title={t('connections.detail.processPath')}
|
||||
value={connection.metadata.processPath}
|
||||
prefix={['PROCESS-PATH']}
|
||||
/>
|
||||
)}
|
||||
{connection.metadata.sourceIP && (
|
||||
<CopyableSettingItem
|
||||
title="来源IP"
|
||||
title={t('connections.detail.sourceIP')}
|
||||
value={connection.metadata.sourceIP}
|
||||
prefix={['SRC-IP-CIDR']}
|
||||
/>
|
||||
)}
|
||||
{connection.metadata.sourceGeoIP && connection.metadata.sourceGeoIP.length > 0 && (
|
||||
<CopyableSettingItem
|
||||
title="来源GeoIP"
|
||||
title={t('connections.detail.sourceGeoIP')}
|
||||
value={connection.metadata.sourceGeoIP}
|
||||
prefix={['SRC-GEOIP']}
|
||||
/>
|
||||
)}
|
||||
{connection.metadata.sourceIPASN && (
|
||||
<CopyableSettingItem
|
||||
title="来源ASN"
|
||||
title={t('connections.detail.sourceASN')}
|
||||
value={connection.metadata.sourceIPASN}
|
||||
prefix={['SRC-IP-ASN']}
|
||||
/>
|
||||
)}
|
||||
{connection.metadata.destinationIP && (
|
||||
<CopyableSettingItem
|
||||
title="目标IP"
|
||||
title={t('connections.detail.destinationIP')}
|
||||
value={connection.metadata.destinationIP}
|
||||
prefix={['IP-CIDR']}
|
||||
/>
|
||||
@ -212,83 +216,83 @@ const ConnectionDetailModal: React.FC<Props> = (props) => {
|
||||
{connection.metadata.destinationGeoIP &&
|
||||
connection.metadata.destinationGeoIP.length > 0 && (
|
||||
<CopyableSettingItem
|
||||
title="目标GeoIP"
|
||||
title={t('connections.detail.destinationGeoIP')}
|
||||
value={connection.metadata.destinationGeoIP}
|
||||
prefix={['GEOIP']}
|
||||
/>
|
||||
)}
|
||||
{connection.metadata.destinationIPASN && (
|
||||
<CopyableSettingItem
|
||||
title="目标ASN"
|
||||
title={t('connections.detail.destinationASN')}
|
||||
value={connection.metadata.destinationIPASN}
|
||||
prefix={['IP-ASN']}
|
||||
/>
|
||||
)}
|
||||
{connection.metadata.sourcePort && (
|
||||
<CopyableSettingItem
|
||||
title="来源端口"
|
||||
title={t('connections.detail.sourcePort')}
|
||||
value={connection.metadata.sourcePort}
|
||||
prefix={['SRC-PORT']}
|
||||
/>
|
||||
)}
|
||||
{connection.metadata.destinationPort && (
|
||||
<CopyableSettingItem
|
||||
title="目标端口"
|
||||
title={t('connections.detail.destinationPort')}
|
||||
value={connection.metadata.destinationPort}
|
||||
prefix={['DST-PORT']}
|
||||
/>
|
||||
)}
|
||||
{connection.metadata.inboundIP && (
|
||||
<CopyableSettingItem
|
||||
title="入站IP"
|
||||
title={t('connections.detail.inboundIP')}
|
||||
value={connection.metadata.inboundIP}
|
||||
prefix={['SRC-IP-CIDR']}
|
||||
/>
|
||||
)}
|
||||
{connection.metadata.inboundPort && (
|
||||
<CopyableSettingItem
|
||||
title="入站端口"
|
||||
title={t('connections.detail.inboundPort')}
|
||||
value={connection.metadata.inboundPort}
|
||||
prefix={['SRC-PORT']}
|
||||
prefix={['IN-PORT']}
|
||||
/>
|
||||
)}
|
||||
{connection.metadata.inboundName && (
|
||||
<CopyableSettingItem
|
||||
title="入站名称"
|
||||
title={t('connections.detail.inboundName')}
|
||||
value={connection.metadata.inboundName}
|
||||
prefix={['IN-NAME']}
|
||||
/>
|
||||
)}
|
||||
{connection.metadata.inboundUser && (
|
||||
<CopyableSettingItem
|
||||
title="入站用户"
|
||||
title={t('connections.detail.inboundUser')}
|
||||
value={connection.metadata.inboundUser}
|
||||
prefix={['IN-USER']}
|
||||
/>
|
||||
)}
|
||||
|
||||
<CopyableSettingItem
|
||||
title="DSCP"
|
||||
title={t('connections.detail.dscp')}
|
||||
value={connection.metadata.dscp.toString()}
|
||||
prefix={['DSCP']}
|
||||
/>
|
||||
|
||||
{connection.metadata.remoteDestination && (
|
||||
<SettingItem title="远程目标">{connection.metadata.remoteDestination}</SettingItem>
|
||||
<SettingItem title={t('connections.detail.remoteDestination')}>{connection.metadata.remoteDestination}</SettingItem>
|
||||
)}
|
||||
{connection.metadata.dnsMode && (
|
||||
<SettingItem title="DNS模式">{connection.metadata.dnsMode}</SettingItem>
|
||||
<SettingItem title={t('connections.detail.dnsMode')}>{connection.metadata.dnsMode}</SettingItem>
|
||||
)}
|
||||
{connection.metadata.specialProxy && (
|
||||
<SettingItem title="特殊代理">{connection.metadata.specialProxy}</SettingItem>
|
||||
<SettingItem title={t('connections.detail.specialProxy')}>{connection.metadata.specialProxy}</SettingItem>
|
||||
)}
|
||||
{connection.metadata.specialRules && (
|
||||
<SettingItem title="特殊规则">{connection.metadata.specialRules}</SettingItem>
|
||||
<SettingItem title={t('connections.detail.specialRules')}>{connection.metadata.specialRules}</SettingItem>
|
||||
)}
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button size="sm" variant="light" onPress={onClose}>
|
||||
关闭
|
||||
{t('connections.detail.close')}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { Button, Card, CardFooter, CardHeader, Chip } from '@nextui-org/react'
|
||||
import { calcTraffic } from '@renderer/utils/calc'
|
||||
import dayjs from 'dayjs'
|
||||
import dayjs from '@renderer/utils/dayjs'
|
||||
import React, { useEffect } from 'react'
|
||||
import { CgClose, CgTrash } from 'react-icons/cg'
|
||||
|
||||
|
||||
@ -9,11 +9,15 @@ import {
|
||||
} from '@nextui-org/react'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { getInterfaces } from '@renderer/utils/ipc'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
interface Props {
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
const InterfaceModal: React.FC<Props> = (props) => {
|
||||
const { onClose } = props
|
||||
const { t } = useTranslation()
|
||||
const [info, setInfo] = useState<Record<string, NetworkInterfaceInfo[]>>({})
|
||||
const getInfo = async (): Promise<void> => {
|
||||
setInfo(await getInterfaces())
|
||||
@ -33,7 +37,7 @@ const InterfaceModal: React.FC<Props> = (props) => {
|
||||
scrollBehavior="inside"
|
||||
>
|
||||
<ModalContent>
|
||||
<ModalHeader className="flex app-drag">网络信息</ModalHeader>
|
||||
<ModalHeader className="flex app-drag">{t('mihomo.interface.title')}</ModalHeader>
|
||||
<ModalBody>
|
||||
{Object.entries(info).map(([key, value]) => {
|
||||
return (
|
||||
@ -57,7 +61,7 @@ const InterfaceModal: React.FC<Props> = (props) => {
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button size="sm" variant="light" onPress={onClose}>
|
||||
关闭
|
||||
{t('common.close')}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
|
||||
@ -2,6 +2,8 @@ import { Modal, ModalContent, ModalHeader, ModalBody, ModalFooter, Button } from
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { BaseEditor } from '../base/base-editor'
|
||||
import { getOverride, restartCore, setOverride } from '@renderer/utils/ipc'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
interface Props {
|
||||
id: string
|
||||
language: 'javascript' | 'yaml'
|
||||
@ -10,6 +12,7 @@ interface Props {
|
||||
const EditFileModal: React.FC<Props> = (props) => {
|
||||
const { id, language, onClose } = props
|
||||
const [currData, setCurrData] = useState('')
|
||||
const { t } = useTranslation()
|
||||
|
||||
const getContent = async (): Promise<void> => {
|
||||
setCurrData(await getOverride(id, language === 'javascript' ? 'js' : 'yaml'))
|
||||
@ -31,7 +34,9 @@ const EditFileModal: React.FC<Props> = (props) => {
|
||||
>
|
||||
<ModalContent className="h-full w-[calc(100%-100px)]">
|
||||
<ModalHeader className="flex pb-0 app-drag">
|
||||
编辑覆写{language === 'javascript' ? '脚本' : '配置'}
|
||||
{t('override.editFile.title', {
|
||||
type: language === 'javascript' ? t('override.editFile.script') : t('override.editFile.config')
|
||||
})}
|
||||
</ModalHeader>
|
||||
<ModalBody className="h-full">
|
||||
<BaseEditor
|
||||
@ -42,7 +47,7 @@ const EditFileModal: React.FC<Props> = (props) => {
|
||||
</ModalBody>
|
||||
<ModalFooter className="pt-0">
|
||||
<Button size="sm" variant="light" onPress={onClose}>
|
||||
取消
|
||||
{t('common.cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
@ -57,7 +62,7 @@ const EditFileModal: React.FC<Props> = (props) => {
|
||||
}
|
||||
}}
|
||||
>
|
||||
确认
|
||||
{t('common.confirm')}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
|
||||
@ -11,6 +11,8 @@ import {
|
||||
import React, { useState } from 'react'
|
||||
import SettingItem from '../base/base-setting-item'
|
||||
import { restartCore } from '@renderer/utils/ipc'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
interface Props {
|
||||
item: IOverrideItem
|
||||
updateOverrideItem: (item: IOverrideItem) => Promise<void>
|
||||
@ -19,6 +21,7 @@ interface Props {
|
||||
const EditInfoModal: React.FC<Props> = (props) => {
|
||||
const { item, updateOverrideItem, onClose } = props
|
||||
const [values, setValues] = useState(item)
|
||||
const { t } = useTranslation()
|
||||
|
||||
const onSave = async (): Promise<void> => {
|
||||
await updateOverrideItem(values)
|
||||
@ -36,9 +39,9 @@ const EditInfoModal: React.FC<Props> = (props) => {
|
||||
scrollBehavior="inside"
|
||||
>
|
||||
<ModalContent>
|
||||
<ModalHeader className="flex app-drag">编辑信息</ModalHeader>
|
||||
<ModalHeader className="flex app-drag">{t('override.editInfo.title')}</ModalHeader>
|
||||
<ModalBody>
|
||||
<SettingItem title="名称">
|
||||
<SettingItem title={t('override.editInfo.name')}>
|
||||
<Input
|
||||
size="sm"
|
||||
className="w-[200px]"
|
||||
@ -49,7 +52,7 @@ const EditInfoModal: React.FC<Props> = (props) => {
|
||||
/>
|
||||
</SettingItem>
|
||||
{values.type === 'remote' && (
|
||||
<SettingItem title="地址">
|
||||
<SettingItem title={t('override.editInfo.url')}>
|
||||
<Input
|
||||
size="sm"
|
||||
className="w-[200px]"
|
||||
@ -60,7 +63,7 @@ const EditInfoModal: React.FC<Props> = (props) => {
|
||||
/>
|
||||
</SettingItem>
|
||||
)}
|
||||
<SettingItem title="全局启用">
|
||||
<SettingItem title={t('override.editInfo.global')}>
|
||||
<Switch
|
||||
size="sm"
|
||||
isSelected={values.global}
|
||||
@ -72,10 +75,10 @@ const EditInfoModal: React.FC<Props> = (props) => {
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button size="sm" variant="light" onPress={onClose}>
|
||||
取消
|
||||
{t('common.cancel')}
|
||||
</Button>
|
||||
<Button size="sm" color="primary" onPress={onSave}>
|
||||
保存
|
||||
{t('common.save')}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
|
||||
@ -9,6 +9,8 @@ import {
|
||||
} from '@nextui-org/react'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { getOverride } from '@renderer/utils/ipc'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
interface Props {
|
||||
id: string
|
||||
onClose: () => void
|
||||
@ -16,6 +18,7 @@ interface Props {
|
||||
const ExecLogModal: React.FC<Props> = (props) => {
|
||||
const { id, onClose } = props
|
||||
const [logs, setLogs] = useState<string[]>([])
|
||||
const { t } = useTranslation()
|
||||
|
||||
const getLog = async (): Promise<void> => {
|
||||
setLogs((await getOverride(id, 'log')).split('\n').filter(Boolean))
|
||||
@ -35,7 +38,7 @@ const ExecLogModal: React.FC<Props> = (props) => {
|
||||
scrollBehavior="inside"
|
||||
>
|
||||
<ModalContent>
|
||||
<ModalHeader className="flex app-drag">执行日志</ModalHeader>
|
||||
<ModalHeader className="flex app-drag">{t('override.execLog.title')}</ModalHeader>
|
||||
<ModalBody>
|
||||
{logs.map((log) => {
|
||||
return (
|
||||
@ -48,7 +51,7 @@ const ExecLogModal: React.FC<Props> = (props) => {
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button size="sm" variant="light" onPress={onClose}>
|
||||
关闭
|
||||
{t('override.execLog.close')}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
|
||||
@ -9,7 +9,7 @@ import {
|
||||
DropdownTrigger
|
||||
} from '@nextui-org/react'
|
||||
import { IoMdMore, IoMdRefresh } from 'react-icons/io'
|
||||
import dayjs from 'dayjs'
|
||||
import dayjs from '@renderer/utils/dayjs'
|
||||
import React, { Key, useEffect, useMemo, useState } from 'react'
|
||||
import EditFileModal from './edit-file-modal'
|
||||
import EditInfoModal from './edit-info-modal'
|
||||
@ -17,6 +17,7 @@ import { useSortable } from '@dnd-kit/sortable'
|
||||
import { CSS } from '@dnd-kit/utilities'
|
||||
import ExecLogModal from './exec-log-modal'
|
||||
import { openFile, restartCore } from '@renderer/utils/ipc'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
interface Props {
|
||||
info: IOverrideItem
|
||||
@ -35,6 +36,7 @@ interface MenuItem {
|
||||
}
|
||||
|
||||
const OverrideItem: React.FC<Props> = (props) => {
|
||||
const { t } = useTranslation()
|
||||
const { info, addOverrideItem, removeOverrideItem, mutateOverrideConfig, updateOverrideItem } =
|
||||
props
|
||||
const [updating, setUpdating] = useState(false)
|
||||
@ -57,35 +59,35 @@ const OverrideItem: React.FC<Props> = (props) => {
|
||||
const list = [
|
||||
{
|
||||
key: 'edit-info',
|
||||
label: '编辑信息',
|
||||
label: t('override.menuItems.editInfo'),
|
||||
showDivider: false,
|
||||
color: 'default',
|
||||
className: ''
|
||||
} as MenuItem,
|
||||
{
|
||||
key: 'edit-file',
|
||||
label: '编辑文件',
|
||||
label: t('override.menuItems.editFile'),
|
||||
showDivider: false,
|
||||
color: 'default',
|
||||
className: ''
|
||||
} as MenuItem,
|
||||
{
|
||||
key: 'open-file',
|
||||
label: '打开文件',
|
||||
label: t('override.menuItems.openFile'),
|
||||
showDivider: false,
|
||||
color: 'default',
|
||||
className: ''
|
||||
} as MenuItem,
|
||||
{
|
||||
key: 'exec-log',
|
||||
label: '执行日志',
|
||||
label: t('override.menuItems.execLog'),
|
||||
showDivider: true,
|
||||
color: 'default',
|
||||
className: ''
|
||||
} as MenuItem,
|
||||
{
|
||||
key: 'delete',
|
||||
label: '删除',
|
||||
label: t('override.menuItems.delete'),
|
||||
showDivider: false,
|
||||
color: 'danger',
|
||||
className: 'text-danger'
|
||||
@ -95,7 +97,7 @@ const OverrideItem: React.FC<Props> = (props) => {
|
||||
list.splice(3, 1)
|
||||
}
|
||||
return list
|
||||
}, [info])
|
||||
}, [info, t])
|
||||
const onMenuAction = (key: Key): void => {
|
||||
switch (key) {
|
||||
case 'edit-info': {
|
||||
@ -228,7 +230,7 @@ const OverrideItem: React.FC<Props> = (props) => {
|
||||
<div className={`mt-2 flex justify-start`}>
|
||||
{info.global && (
|
||||
<Chip size="sm" variant="dot" color="primary" className="mr-2">
|
||||
全局
|
||||
{t('override.labels.global')}
|
||||
</Chip>
|
||||
)}
|
||||
<Chip size="sm" variant="bordered">
|
||||
|
||||
@ -3,14 +3,18 @@ import React, { useEffect, useState } from 'react'
|
||||
import { BaseEditor } from '../base/base-editor'
|
||||
import { getProfileStr, setProfileStr } from '@renderer/utils/ipc'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
interface Props {
|
||||
id: string
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
const EditFileModal: React.FC<Props> = (props) => {
|
||||
const { id, onClose } = props
|
||||
const [currData, setCurrData] = useState('')
|
||||
const navigate = useNavigate()
|
||||
const { t } = useTranslation()
|
||||
|
||||
const getContent = async (): Promise<void> => {
|
||||
setCurrData(await getProfileStr(id))
|
||||
@ -33,9 +37,9 @@ const EditFileModal: React.FC<Props> = (props) => {
|
||||
<ModalContent className="h-full w-[calc(100%-100px)]">
|
||||
<ModalHeader className="flex pb-0 app-drag">
|
||||
<div className="flex justify-start">
|
||||
<div className="flex items-center">编辑订阅</div>
|
||||
<div className="flex items-center">{t('profiles.editFile.title')}</div>
|
||||
<small className="ml-2 text-foreground-500">
|
||||
注意:此处编辑配置更新订阅后会还原,如需要自定义配置请使用
|
||||
{t('profiles.editFile.notice')}
|
||||
<Button
|
||||
size="sm"
|
||||
color="primary"
|
||||
@ -45,9 +49,9 @@ const EditFileModal: React.FC<Props> = (props) => {
|
||||
navigate('/override')
|
||||
}}
|
||||
>
|
||||
覆写
|
||||
{t('profiles.editFile.override')}
|
||||
</Button>
|
||||
功能
|
||||
{t('profiles.editFile.feature')}
|
||||
</small>
|
||||
</div>
|
||||
</ModalHeader>
|
||||
@ -56,7 +60,7 @@ const EditFileModal: React.FC<Props> = (props) => {
|
||||
</ModalBody>
|
||||
<ModalFooter className="pt-0">
|
||||
<Button size="sm" variant="light" onPress={onClose}>
|
||||
取消
|
||||
{t('common.cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
@ -66,7 +70,7 @@ const EditFileModal: React.FC<Props> = (props) => {
|
||||
onClose()
|
||||
}}
|
||||
>
|
||||
确认
|
||||
{t('common.confirm')}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
|
||||
@ -19,6 +19,7 @@ import { useOverrideConfig } from '@renderer/hooks/use-override-config'
|
||||
import { restartCore } from '@renderer/utils/ipc'
|
||||
import { MdDeleteForever } from 'react-icons/md'
|
||||
import { FaPlus } from 'react-icons/fa6'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
interface Props {
|
||||
item: IProfileItem
|
||||
@ -31,6 +32,7 @@ const EditInfoModal: React.FC<Props> = (props) => {
|
||||
const { items: overrideItems = [] } = overrideConfig || {}
|
||||
const [values, setValues] = useState(item)
|
||||
const inputWidth = 'w-[400px] md:w-[400px] lg:w-[600px] xl:w-[800px]'
|
||||
const { t } = useTranslation()
|
||||
|
||||
const onSave = async (): Promise<void> => {
|
||||
try {
|
||||
@ -62,9 +64,9 @@ const EditInfoModal: React.FC<Props> = (props) => {
|
||||
scrollBehavior="inside"
|
||||
>
|
||||
<ModalContent>
|
||||
<ModalHeader className="flex app-drag">编辑信息</ModalHeader>
|
||||
<ModalHeader className="flex app-drag">{t('profiles.editInfo.title')}</ModalHeader>
|
||||
<ModalBody>
|
||||
<SettingItem title="名称">
|
||||
<SettingItem title={t('profiles.editInfo.name')}>
|
||||
<Input
|
||||
size="sm"
|
||||
className={cn(inputWidth)}
|
||||
@ -76,7 +78,7 @@ const EditInfoModal: React.FC<Props> = (props) => {
|
||||
</SettingItem>
|
||||
{values.type === 'remote' && (
|
||||
<>
|
||||
<SettingItem title="订阅地址">
|
||||
<SettingItem title={t('profiles.editInfo.url')}>
|
||||
<Input
|
||||
size="sm"
|
||||
className={cn(inputWidth)}
|
||||
@ -86,7 +88,7 @@ const EditInfoModal: React.FC<Props> = (props) => {
|
||||
}}
|
||||
/>
|
||||
</SettingItem>
|
||||
<SettingItem title="使用代理更新">
|
||||
<SettingItem title={t('profiles.editInfo.useProxy')}>
|
||||
<Switch
|
||||
size="sm"
|
||||
isSelected={values.useProxy ?? false}
|
||||
@ -95,7 +97,7 @@ const EditInfoModal: React.FC<Props> = (props) => {
|
||||
}}
|
||||
/>
|
||||
</SettingItem>
|
||||
<SettingItem title="更新间隔(分钟)">
|
||||
<SettingItem title={t('profiles.editInfo.interval')}>
|
||||
<Input
|
||||
size="sm"
|
||||
type="number"
|
||||
@ -108,7 +110,7 @@ const EditInfoModal: React.FC<Props> = (props) => {
|
||||
</SettingItem>
|
||||
</>
|
||||
)}
|
||||
<SettingItem title="覆写">
|
||||
<SettingItem title={t('profiles.editInfo.override.title')}>
|
||||
<div>
|
||||
{overrideItems
|
||||
.filter((i) => i.global)
|
||||
@ -116,7 +118,7 @@ const EditInfoModal: React.FC<Props> = (props) => {
|
||||
return (
|
||||
<div className="flex mb-2" key={i.id}>
|
||||
<Button disabled fullWidth variant="flat" size="sm">
|
||||
{i.name} (全局)
|
||||
{i.name} ({t('profiles.editInfo.override.global')})
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
@ -153,7 +155,7 @@ const EditInfoModal: React.FC<Props> = (props) => {
|
||||
</Button>
|
||||
</DropdownTrigger>
|
||||
<DropdownMenu
|
||||
emptyContent="没有可用的覆写"
|
||||
emptyContent={t('profiles.editInfo.override.noAvailable')}
|
||||
onAction={(key) => {
|
||||
setValues({
|
||||
...values,
|
||||
@ -173,10 +175,10 @@ const EditInfoModal: React.FC<Props> = (props) => {
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button size="sm" variant="light" onPress={onClose}>
|
||||
取消
|
||||
{t('common.cancel')}
|
||||
</Button>
|
||||
<Button size="sm" color="primary" onPress={onSave}>
|
||||
保存
|
||||
{t('common.save')}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
|
||||
@ -13,7 +13,7 @@ import {
|
||||
} from '@nextui-org/react'
|
||||
import { calcPercent, calcTraffic } from '@renderer/utils/calc'
|
||||
import { IoMdMore, IoMdRefresh } from 'react-icons/io'
|
||||
import dayjs from 'dayjs'
|
||||
import dayjs from '@renderer/utils/dayjs'
|
||||
import React, { Key, useEffect, useMemo, useState } from 'react'
|
||||
import EditFileModal from './edit-file-modal'
|
||||
import EditInfoModal from './edit-info-modal'
|
||||
@ -21,6 +21,7 @@ import { useSortable } from '@dnd-kit/sortable'
|
||||
import { CSS } from '@dnd-kit/utilities'
|
||||
import { openFile } from '@renderer/utils/ipc'
|
||||
import { useAppConfig } from '@renderer/hooks/use-app-config'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
interface Props {
|
||||
info: IProfileItem
|
||||
@ -40,6 +41,7 @@ interface MenuItem {
|
||||
className: string
|
||||
}
|
||||
const ProfileItem: React.FC<Props> = (props) => {
|
||||
const { t } = useTranslation()
|
||||
const {
|
||||
info,
|
||||
addProfileItem,
|
||||
@ -75,28 +77,28 @@ const ProfileItem: React.FC<Props> = (props) => {
|
||||
const list = [
|
||||
{
|
||||
key: 'edit-info',
|
||||
label: '编辑信息',
|
||||
label: t('profiles.editInfo.title'),
|
||||
showDivider: false,
|
||||
color: 'default',
|
||||
className: ''
|
||||
} as MenuItem,
|
||||
{
|
||||
key: 'edit-file',
|
||||
label: '编辑文件',
|
||||
label: t('profiles.editFile.title'),
|
||||
showDivider: false,
|
||||
color: 'default',
|
||||
className: ''
|
||||
} as MenuItem,
|
||||
{
|
||||
key: 'open-file',
|
||||
label: '打开文件',
|
||||
label: t('profiles.openFile'),
|
||||
showDivider: true,
|
||||
color: 'default',
|
||||
className: ''
|
||||
} as MenuItem,
|
||||
{
|
||||
key: 'delete',
|
||||
label: '删除',
|
||||
label: t('common.delete'),
|
||||
showDivider: false,
|
||||
color: 'danger',
|
||||
className: 'text-danger'
|
||||
@ -105,14 +107,14 @@ const ProfileItem: React.FC<Props> = (props) => {
|
||||
if (info.home) {
|
||||
list.unshift({
|
||||
key: 'home',
|
||||
label: '主页',
|
||||
label: t('profiles.home'),
|
||||
showDivider: false,
|
||||
color: 'default',
|
||||
className: ''
|
||||
} as MenuItem)
|
||||
}
|
||||
return list
|
||||
}, [info])
|
||||
}, [info, t])
|
||||
|
||||
const onMenuAction = async (key: Key): Promise<void> => {
|
||||
switch (key) {
|
||||
@ -282,7 +284,7 @@ const ProfileItem: React.FC<Props> = (props) => {
|
||||
variant="bordered"
|
||||
className={`${isCurrent ? 'text-primary-foreground border-primary-foreground' : 'border-primary text-primary'}`}
|
||||
>
|
||||
远程
|
||||
{t('profiles.remote')}
|
||||
</Chip>
|
||||
<small>{dayjs(info.updated).fromNow()}</small>
|
||||
</div>
|
||||
@ -296,14 +298,14 @@ const ProfileItem: React.FC<Props> = (props) => {
|
||||
variant="bordered"
|
||||
className={`${isCurrent ? 'text-primary-foreground border-primary-foreground' : 'border-primary text-primary'}`}
|
||||
>
|
||||
本地
|
||||
{t('profiles.local')}
|
||||
</Chip>
|
||||
</div>
|
||||
)}
|
||||
{extra && (
|
||||
<Progress
|
||||
className="w-full"
|
||||
aria-label="流量使用进度"
|
||||
aria-label={t('profiles.trafficUsage')}
|
||||
classNames={{
|
||||
indicator: isCurrent ? 'bg-primary-foreground' : 'bg-foreground'
|
||||
}}
|
||||
|
||||
@ -2,6 +2,7 @@ import { Button, Card, CardBody } from '@nextui-org/react'
|
||||
import { mihomoUnfixedProxy } from '@renderer/utils/ipc'
|
||||
import React, { useMemo, useState } from 'react'
|
||||
import { FaMapPin } from 'react-icons/fa6'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
interface Props {
|
||||
mutateProxies: () => void
|
||||
@ -14,6 +15,7 @@ interface Props {
|
||||
}
|
||||
|
||||
const ProxyItem: React.FC<Props> = (props) => {
|
||||
const { t } = useTranslation()
|
||||
const { mutateProxies, proxyDisplayMode, group, proxy, selected, onSelect, onProxyDelay } = props
|
||||
|
||||
const delay = useMemo(() => {
|
||||
@ -32,8 +34,8 @@ const ProxyItem: React.FC<Props> = (props) => {
|
||||
}
|
||||
|
||||
function delayText(delay: number): string {
|
||||
if (delay === -1) return '测试'
|
||||
if (delay === 0) return '超时'
|
||||
if (delay === -1) return t('proxies.delay.test')
|
||||
if (delay === 0) return t('proxies.delay.timeout')
|
||||
return delay.toString()
|
||||
}
|
||||
|
||||
@ -74,7 +76,7 @@ const ProxyItem: React.FC<Props> = (props) => {
|
||||
{fixed && (
|
||||
<Button
|
||||
isIconOnly
|
||||
title="取消固定"
|
||||
title={t('proxies.unpin')}
|
||||
color="danger"
|
||||
onPress={async () => {
|
||||
await mihomoUnfixedProxy(group.name)
|
||||
@ -124,7 +126,7 @@ const ProxyItem: React.FC<Props> = (props) => {
|
||||
{fixed && (
|
||||
<Button
|
||||
isIconOnly
|
||||
title="取消固定"
|
||||
title={t('proxies.unpin')}
|
||||
color="danger"
|
||||
onPress={async () => {
|
||||
await mihomoUnfixedProxy(group.name)
|
||||
|
||||
@ -5,8 +5,10 @@ import { useControledMihomoConfig } from '@renderer/hooks/use-controled-mihomo-c
|
||||
import { mihomoUpgradeGeo } from '@renderer/utils/ipc'
|
||||
import { useState } from 'react'
|
||||
import { IoMdRefresh } from 'react-icons/io'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
const GeoData: React.FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const { controledMihomoConfig, patchControledMihomoConfig } = useControledMihomoConfig()
|
||||
const {
|
||||
'geox-url': geoxUrl = {
|
||||
@ -27,7 +29,7 @@ const GeoData: React.FC = () => {
|
||||
|
||||
return (
|
||||
<SettingCard>
|
||||
<SettingItem title="GeoIP 数据库" divider>
|
||||
<SettingItem title={t('resources.geoData.geoip')} divider>
|
||||
<div className="flex w-[70%]">
|
||||
{geoipInput !== geoxUrl.geoip && (
|
||||
<Button
|
||||
@ -38,13 +40,13 @@ const GeoData: React.FC = () => {
|
||||
patchControledMihomoConfig({ 'geox-url': { ...geoxUrl, geoip: geoipInput } })
|
||||
}}
|
||||
>
|
||||
确认
|
||||
{t('common.confirm')}
|
||||
</Button>
|
||||
)}
|
||||
<Input size="sm" value={geoipInput} onValueChange={setGeoIpInput} />
|
||||
</div>
|
||||
</SettingItem>
|
||||
<SettingItem title="GeoSite 数据库" divider>
|
||||
<SettingItem title={t('resources.geoData.geosite')} divider>
|
||||
<div className="flex w-[70%]">
|
||||
{geositeInput !== geoxUrl.geosite && (
|
||||
<Button
|
||||
@ -55,13 +57,13 @@ const GeoData: React.FC = () => {
|
||||
patchControledMihomoConfig({ 'geox-url': { ...geoxUrl, geosite: geositeInput } })
|
||||
}}
|
||||
>
|
||||
确认
|
||||
{t('common.confirm')}
|
||||
</Button>
|
||||
)}
|
||||
<Input size="sm" value={geositeInput} onValueChange={setGeositeInput} />
|
||||
</div>
|
||||
</SettingItem>
|
||||
<SettingItem title="MMDB 数据库" divider>
|
||||
<SettingItem title={t('resources.geoData.mmdb')} divider>
|
||||
<div className="flex w-[70%]">
|
||||
{mmdbInput !== geoxUrl.mmdb && (
|
||||
<Button
|
||||
@ -72,13 +74,13 @@ const GeoData: React.FC = () => {
|
||||
patchControledMihomoConfig({ 'geox-url': { ...geoxUrl, mmdb: mmdbInput } })
|
||||
}}
|
||||
>
|
||||
确认
|
||||
{t('common.confirm')}
|
||||
</Button>
|
||||
)}
|
||||
<Input size="sm" value={mmdbInput} onValueChange={setMmdbInput} />
|
||||
</div>
|
||||
</SettingItem>
|
||||
<SettingItem title="ASN 数据库" divider>
|
||||
<SettingItem title={t('resources.geoData.asn')} divider>
|
||||
<div className="flex w-[70%]">
|
||||
{asnInput !== geoxUrl.asn && (
|
||||
<Button
|
||||
@ -89,13 +91,13 @@ const GeoData: React.FC = () => {
|
||||
patchControledMihomoConfig({ 'geox-url': { ...geoxUrl, asn: asnInput } })
|
||||
}}
|
||||
>
|
||||
确认
|
||||
{t('common.confirm')}
|
||||
</Button>
|
||||
)}
|
||||
<Input size="sm" value={asnInput} onValueChange={setAsnInput} />
|
||||
</div>
|
||||
</SettingItem>
|
||||
<SettingItem title="GeoIP 数据模式" divider>
|
||||
<SettingItem title={t('resources.geoData.mode')} divider>
|
||||
<Tabs
|
||||
size="sm"
|
||||
color="primary"
|
||||
@ -109,7 +111,7 @@ const GeoData: React.FC = () => {
|
||||
</Tabs>
|
||||
</SettingItem>
|
||||
<SettingItem
|
||||
title="自动更新 Geo 数据库"
|
||||
title={t('resources.geoData.autoUpdate')}
|
||||
actions={
|
||||
<Button
|
||||
size="sm"
|
||||
@ -119,7 +121,7 @@ const GeoData: React.FC = () => {
|
||||
setUpdating(true)
|
||||
try {
|
||||
await mihomoUpgradeGeo()
|
||||
new Notification('Geo 数据库更新成功')
|
||||
new Notification(t('resources.geoData.updateSuccess'))
|
||||
} catch (e) {
|
||||
alert(e)
|
||||
} finally {
|
||||
@ -141,7 +143,7 @@ const GeoData: React.FC = () => {
|
||||
/>
|
||||
</SettingItem>
|
||||
{geoAutoUpdate && (
|
||||
<SettingItem title="更新间隔(小时)">
|
||||
<SettingItem title={t('resources.geoData.updateInterval')}>
|
||||
<Input
|
||||
size="sm"
|
||||
type="number"
|
||||
|
||||
@ -12,7 +12,7 @@ import { Button, Chip } from '@nextui-org/react'
|
||||
import { IoMdRefresh } from 'react-icons/io'
|
||||
import { CgLoadbarDoc } from 'react-icons/cg'
|
||||
import { MdEditDocument } from 'react-icons/md'
|
||||
import dayjs from 'dayjs'
|
||||
import dayjs from '@renderer/utils/dayjs'
|
||||
import { calcTraffic } from '@renderer/utils/calc'
|
||||
import { getHash } from '@renderer/utils/hash'
|
||||
|
||||
|
||||
@ -13,7 +13,7 @@ import { Button, Chip } from '@nextui-org/react'
|
||||
import { IoMdRefresh } from 'react-icons/io'
|
||||
import { CgLoadbarDoc } from 'react-icons/cg'
|
||||
import { MdEditDocument } from 'react-icons/md'
|
||||
import dayjs from 'dayjs'
|
||||
import dayjs from '@renderer/utils/dayjs'
|
||||
|
||||
const RuleProvider: React.FC = () => {
|
||||
const [showDetails, setShowDetails] = useState({
|
||||
|
||||
@ -12,9 +12,11 @@ import { useState } from 'react'
|
||||
import UpdaterModal from '../updater/updater-modal'
|
||||
import { version } from '@renderer/utils/init'
|
||||
import { IoIosHelpCircle } from 'react-icons/io'
|
||||
import { firstDriver } from '@renderer/App'
|
||||
import { getDriver } from '@renderer/App'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
const Actions: React.FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const [newVersion, setNewVersion] = useState('')
|
||||
const [changelog, setChangelog] = useState('')
|
||||
const [openUpdate, setOpenUpdate] = useState(false)
|
||||
@ -30,12 +32,12 @@ const Actions: React.FC = () => {
|
||||
/>
|
||||
)}
|
||||
<SettingCard>
|
||||
<SettingItem title="打开引导页面" divider>
|
||||
<Button size="sm" onPress={() => firstDriver.drive()}>
|
||||
打开引导页面
|
||||
<SettingItem title={t('actions.guide.title')} divider>
|
||||
<Button size="sm" onPress={() => getDriver()?.drive()}>
|
||||
{t('actions.guide.button')}
|
||||
</Button>
|
||||
</SettingItem>
|
||||
<SettingItem title="检查更新" divider>
|
||||
<SettingItem title={t('actions.update.title')} divider>
|
||||
<Button
|
||||
size="sm"
|
||||
isLoading={checkingUpdate}
|
||||
@ -48,7 +50,9 @@ const Actions: React.FC = () => {
|
||||
setChangelog(version.changelog)
|
||||
setOpenUpdate(true)
|
||||
} else {
|
||||
new window.Notification('当前已是最新版本', { body: '无需更新' })
|
||||
new window.Notification(t('actions.update.upToDate.title'), {
|
||||
body: t('actions.update.upToDate.body')
|
||||
})
|
||||
}
|
||||
} catch (e) {
|
||||
alert(e)
|
||||
@ -57,13 +61,13 @@ const Actions: React.FC = () => {
|
||||
}
|
||||
}}
|
||||
>
|
||||
检查更新
|
||||
{t('actions.update.button')}
|
||||
</Button>
|
||||
</SettingItem>
|
||||
<SettingItem
|
||||
title="重置软件"
|
||||
title={t('actions.reset.title')}
|
||||
actions={
|
||||
<Tooltip content="删除所有配置,将软件恢复初始状态">
|
||||
<Tooltip content={t('actions.reset.tooltip')}>
|
||||
<Button isIconOnly size="sm" variant="light">
|
||||
<IoIosHelpCircle className="text-lg" />
|
||||
</Button>
|
||||
@ -72,13 +76,13 @@ const Actions: React.FC = () => {
|
||||
divider
|
||||
>
|
||||
<Button size="sm" onPress={resetAppConfig}>
|
||||
重置软件
|
||||
{t('actions.reset.button')}
|
||||
</Button>
|
||||
</SettingItem>
|
||||
<SettingItem
|
||||
title="创建堆快照"
|
||||
title={t('actions.heapSnapshot.title')}
|
||||
actions={
|
||||
<Tooltip content="创建主进程堆快照,用于排查内存问题">
|
||||
<Tooltip content={t('actions.heapSnapshot.tooltip')}>
|
||||
<Button isIconOnly size="sm" variant="light">
|
||||
<IoIosHelpCircle className="text-lg" />
|
||||
</Button>
|
||||
@ -87,13 +91,13 @@ const Actions: React.FC = () => {
|
||||
divider
|
||||
>
|
||||
<Button size="sm" onPress={createHeapSnapshot}>
|
||||
创建堆快照
|
||||
{t('actions.heapSnapshot.button')}
|
||||
</Button>
|
||||
</SettingItem>
|
||||
<SettingItem
|
||||
title="轻量模式"
|
||||
title={t('actions.lightMode.title')}
|
||||
actions={
|
||||
<Tooltip content="完全退出软件,只保留内核进程">
|
||||
<Tooltip content={t('actions.lightMode.tooltip')}>
|
||||
<Button isIconOnly size="sm" variant="light">
|
||||
<IoIosHelpCircle className="text-lg" />
|
||||
</Button>
|
||||
@ -102,15 +106,15 @@ const Actions: React.FC = () => {
|
||||
divider
|
||||
>
|
||||
<Button size="sm" onPress={quitWithoutCore}>
|
||||
轻量模式
|
||||
{t('actions.lightMode.button')}
|
||||
</Button>
|
||||
</SettingItem>
|
||||
<SettingItem title="退出应用" divider>
|
||||
<SettingItem title={t('actions.quit.title')} divider>
|
||||
<Button size="sm" onPress={quitApp}>
|
||||
退出应用
|
||||
{t('actions.quit.button')}
|
||||
</Button>
|
||||
</SettingItem>
|
||||
<SettingItem title="应用版本">
|
||||
<SettingItem title={t('actions.version.title')}>
|
||||
<div>v{version}</div>
|
||||
</SettingItem>
|
||||
</SettingCard>
|
||||
|
||||
@ -2,12 +2,16 @@ import { Modal, ModalContent, ModalHeader, ModalBody, ModalFooter, Button } from
|
||||
import { BaseEditor } from '@renderer/components/base/base-editor'
|
||||
import { readTheme } from '@renderer/utils/ipc'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
interface Props {
|
||||
theme: string
|
||||
onCancel: () => void
|
||||
onConfirm: (script: string) => void
|
||||
}
|
||||
|
||||
const CSSEditorModal: React.FC<Props> = (props) => {
|
||||
const { t } = useTranslation()
|
||||
const { theme, onCancel, onConfirm } = props
|
||||
const [currData, setCurrData] = useState('')
|
||||
|
||||
@ -30,7 +34,7 @@ const CSSEditorModal: React.FC<Props> = (props) => {
|
||||
scrollBehavior="inside"
|
||||
>
|
||||
<ModalContent className="h-full w-[calc(100%-100px)]">
|
||||
<ModalHeader className="flex pb-0 app-drag">编辑主题</ModalHeader>
|
||||
<ModalHeader className="flex pb-0 app-drag">{t('theme.editor.title')}</ModalHeader>
|
||||
<ModalBody className="h-full">
|
||||
<BaseEditor
|
||||
language="css"
|
||||
@ -40,10 +44,10 @@ const CSSEditorModal: React.FC<Props> = (props) => {
|
||||
</ModalBody>
|
||||
<ModalFooter className="pt-0">
|
||||
<Button size="sm" variant="light" onPress={onCancel}>
|
||||
取消
|
||||
{t('common.cancel')}
|
||||
</Button>
|
||||
<Button size="sm" color="primary" onPress={() => onConfirm(currData)}>
|
||||
确认
|
||||
{t('common.confirm')}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
|
||||
@ -29,8 +29,10 @@ import { useTheme } from 'next-themes'
|
||||
import { IoIosHelpCircle, IoMdCloudDownload } from 'react-icons/io'
|
||||
import { MdEditDocument } from 'react-icons/md'
|
||||
import CSSEditorModal from './css-editor-modal'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
const GeneralConfig: React.FC = () => {
|
||||
const { t, i18n } = useTranslation()
|
||||
const { data: enable, mutate: mutateEnable } = useSWR('checkAutoRun', checkAutoRun)
|
||||
const { appConfig, patchAppConfig } = useAppConfig()
|
||||
const [customThemes, setCustomThemes] = useState<{ key: string; label: string }[]>()
|
||||
@ -52,7 +54,8 @@ const GeneralConfig: React.FC = () => {
|
||||
customTheme = 'default.css',
|
||||
envType = [platform === 'win32' ? 'powershell' : 'bash'],
|
||||
autoCheckUpdate,
|
||||
appTheme = 'system'
|
||||
appTheme = 'system',
|
||||
language = 'zh-CN'
|
||||
} = appConfig || {}
|
||||
|
||||
useEffect(() => {
|
||||
@ -75,7 +78,24 @@ const GeneralConfig: React.FC = () => {
|
||||
/>
|
||||
)}
|
||||
<SettingCard>
|
||||
<SettingItem title="开机自启" divider>
|
||||
<SettingItem title={t('settings.language')} divider>
|
||||
<Select
|
||||
classNames={{ trigger: 'data-[hover=true]:bg-default-200' }}
|
||||
className="w-[150px]"
|
||||
size="sm"
|
||||
selectedKeys={[language]}
|
||||
aria-label={t('settings.language')}
|
||||
onSelectionChange={async (v) => {
|
||||
const newLang = Array.from(v)[0] as 'zh-CN' | 'en-US'
|
||||
await patchAppConfig({ language: newLang })
|
||||
i18n.changeLanguage(newLang)
|
||||
}}
|
||||
>
|
||||
<SelectItem key="zh-CN">中文简体</SelectItem>
|
||||
<SelectItem key="en-US">English</SelectItem>
|
||||
</Select>
|
||||
</SettingItem>
|
||||
<SettingItem title={t('settings.autoStart')} divider>
|
||||
<Switch
|
||||
size="sm"
|
||||
isSelected={enable}
|
||||
@ -94,7 +114,7 @@ const GeneralConfig: React.FC = () => {
|
||||
}}
|
||||
/>
|
||||
</SettingItem>
|
||||
<SettingItem title="自动检查更新" divider>
|
||||
<SettingItem title={t('settings.autoCheckUpdate')} divider>
|
||||
<Switch
|
||||
size="sm"
|
||||
isSelected={autoCheckUpdate}
|
||||
@ -103,7 +123,7 @@ const GeneralConfig: React.FC = () => {
|
||||
}}
|
||||
/>
|
||||
</SettingItem>
|
||||
<SettingItem title="静默启动" divider>
|
||||
<SettingItem title={t('settings.silentStart')} divider>
|
||||
<Switch
|
||||
size="sm"
|
||||
isSelected={silentStart}
|
||||
@ -113,9 +133,9 @@ const GeneralConfig: React.FC = () => {
|
||||
/>
|
||||
</SettingItem>
|
||||
<SettingItem
|
||||
title="自动开启轻量模式"
|
||||
title={t('settings.autoQuitWithoutCore')}
|
||||
actions={
|
||||
<Tooltip content="关闭窗口指定时间后自动进入轻量模式">
|
||||
<Tooltip content={t('settings.autoQuitWithoutCoreTooltip')}>
|
||||
<Button isIconOnly size="sm" variant="light">
|
||||
<IoIosHelpCircle className="text-lg" />
|
||||
</Button>
|
||||
@ -132,12 +152,12 @@ const GeneralConfig: React.FC = () => {
|
||||
/>
|
||||
</SettingItem>
|
||||
{autoQuitWithoutCore && (
|
||||
<SettingItem title="自动开启轻量模式延时" divider>
|
||||
<SettingItem title={t('settings.autoQuitWithoutCoreDelay')} divider>
|
||||
<Input
|
||||
size="sm"
|
||||
className="w-[100px]"
|
||||
type="number"
|
||||
endContent="秒"
|
||||
endContent={t('common.seconds')}
|
||||
value={autoQuitWithoutCoreDelay.toString()}
|
||||
onValueChange={async (v: string) => {
|
||||
let num = parseInt(v)
|
||||
@ -149,7 +169,7 @@ const GeneralConfig: React.FC = () => {
|
||||
</SettingItem>
|
||||
)}
|
||||
<SettingItem
|
||||
title="复制环境变量类型"
|
||||
title={t('settings.envType')}
|
||||
actions={envType.map((type) => (
|
||||
<Button
|
||||
key={type}
|
||||
@ -170,7 +190,7 @@ const GeneralConfig: React.FC = () => {
|
||||
size="sm"
|
||||
selectionMode="multiple"
|
||||
selectedKeys={new Set(envType)}
|
||||
aria-label="选择环境变量类型"
|
||||
aria-label={t('settings.envType')}
|
||||
onSelectionChange={async (v) => {
|
||||
try {
|
||||
await patchAppConfig({
|
||||
@ -186,7 +206,7 @@ const GeneralConfig: React.FC = () => {
|
||||
<SelectItem key="powershell">PowerShell</SelectItem>
|
||||
</Select>
|
||||
</SettingItem>
|
||||
<SettingItem title="显示悬浮窗" divider>
|
||||
<SettingItem title={t('settings.showFloatingWindow')} divider>
|
||||
<Switch
|
||||
size="sm"
|
||||
isSelected={showFloating}
|
||||
@ -203,7 +223,7 @@ const GeneralConfig: React.FC = () => {
|
||||
|
||||
{showFloating && (
|
||||
<>
|
||||
<SettingItem title="根据网速旋转悬浮窗图标" divider>
|
||||
<SettingItem title={t('settings.spinFloatingIcon')} divider>
|
||||
<Switch
|
||||
size="sm"
|
||||
isSelected={spinFloatingIcon}
|
||||
@ -213,7 +233,7 @@ const GeneralConfig: React.FC = () => {
|
||||
}}
|
||||
/>
|
||||
</SettingItem>
|
||||
<SettingItem title="禁用托盘图标" divider>
|
||||
<SettingItem title={t('settings.disableTray')} divider>
|
||||
<Switch
|
||||
size="sm"
|
||||
isSelected={disableTray}
|
||||
@ -231,7 +251,7 @@ const GeneralConfig: React.FC = () => {
|
||||
)}
|
||||
{platform !== 'linux' && (
|
||||
<>
|
||||
<SettingItem title="托盘菜单显示节点信息" divider>
|
||||
<SettingItem title={t('settings.proxyInTray')} divider>
|
||||
<Switch
|
||||
size="sm"
|
||||
isSelected={proxyInTray}
|
||||
@ -241,7 +261,9 @@ const GeneralConfig: React.FC = () => {
|
||||
/>
|
||||
</SettingItem>
|
||||
<SettingItem
|
||||
title={`${platform === 'win32' ? '任务栏' : '状态栏'}显示网速信息`}
|
||||
title={t('settings.showTraffic', {
|
||||
context: platform === 'win32' ? 'windows' : 'mac'
|
||||
})}
|
||||
divider
|
||||
>
|
||||
<Switch
|
||||
@ -257,7 +279,7 @@ const GeneralConfig: React.FC = () => {
|
||||
)}
|
||||
{platform === 'darwin' && (
|
||||
<>
|
||||
<SettingItem title="显示 Dock 图标" divider>
|
||||
<SettingItem title={t('settings.showDockIcon')} divider>
|
||||
<Switch
|
||||
size="sm"
|
||||
isSelected={useDockIcon}
|
||||
@ -269,7 +291,7 @@ const GeneralConfig: React.FC = () => {
|
||||
</>
|
||||
)}
|
||||
|
||||
<SettingItem title="使用系统标题栏" divider>
|
||||
<SettingItem title={t('settings.useWindowFrame')} divider>
|
||||
<Switch
|
||||
size="sm"
|
||||
isSelected={useWindowFrame}
|
||||
@ -287,7 +309,7 @@ const GeneralConfig: React.FC = () => {
|
||||
}, 1000)}
|
||||
/>
|
||||
</SettingItem>
|
||||
<SettingItem title="背景色" divider>
|
||||
<SettingItem title={t('settings.backgroundColor')} divider>
|
||||
<Tabs
|
||||
size="sm"
|
||||
color="primary"
|
||||
@ -297,20 +319,20 @@ const GeneralConfig: React.FC = () => {
|
||||
patchAppConfig({ appTheme: key as AppTheme })
|
||||
}}
|
||||
>
|
||||
<Tab key="system" title="自动" />
|
||||
<Tab key="dark" title="深色" />
|
||||
<Tab key="light" title="浅色" />
|
||||
<Tab key="system" title={t('settings.backgroundAuto')} />
|
||||
<Tab key="dark" title={t('settings.backgroundDark')} />
|
||||
<Tab key="light" title={t('settings.backgroundLight')} />
|
||||
</Tabs>
|
||||
</SettingItem>
|
||||
<SettingItem
|
||||
title="主题"
|
||||
title={t('settings.theme')}
|
||||
actions={
|
||||
<>
|
||||
<Button
|
||||
size="sm"
|
||||
isLoading={fetching}
|
||||
isIconOnly
|
||||
title="拉取主题"
|
||||
title={t('settings.fetchTheme')}
|
||||
variant="light"
|
||||
onPress={async () => {
|
||||
setFetching(true)
|
||||
@ -329,7 +351,7 @@ const GeneralConfig: React.FC = () => {
|
||||
<Button
|
||||
size="sm"
|
||||
isIconOnly
|
||||
title="导入主题"
|
||||
title={t('settings.importTheme')}
|
||||
variant="light"
|
||||
onPress={async () => {
|
||||
const files = await getFilePath(['css'])
|
||||
@ -347,7 +369,7 @@ const GeneralConfig: React.FC = () => {
|
||||
<Button
|
||||
size="sm"
|
||||
isIconOnly
|
||||
title="编辑主题"
|
||||
title={t('settings.editTheme')}
|
||||
variant="light"
|
||||
onPress={async () => {
|
||||
setOpenCSSEditor(true)
|
||||
@ -364,7 +386,7 @@ const GeneralConfig: React.FC = () => {
|
||||
className="w-[60%]"
|
||||
size="sm"
|
||||
selectedKeys={new Set([customTheme])}
|
||||
aria-label="选择主题"
|
||||
aria-label={t('settings.selectTheme')}
|
||||
onSelectionChange={async (v) => {
|
||||
try {
|
||||
await patchAppConfig({ customTheme: v.currentKey as string })
|
||||
|
||||
@ -9,8 +9,10 @@ import { MdDeleteForever } from 'react-icons/md'
|
||||
import { BiCopy } from 'react-icons/bi'
|
||||
import { IoIosHelpCircle } from 'react-icons/io'
|
||||
import { platform } from '@renderer/utils/init'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
const MihomoConfig: React.FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const { appConfig, patchAppConfig } = useAppConfig()
|
||||
const {
|
||||
diffWorkDir = false,
|
||||
@ -37,59 +39,59 @@ const MihomoConfig: React.FC = () => {
|
||||
}, 500)
|
||||
return (
|
||||
<SettingCard>
|
||||
<SettingItem title="订阅拉取 UA" divider>
|
||||
<SettingItem title={t('mihomo.userAgent')} divider>
|
||||
<Input
|
||||
size="sm"
|
||||
className="w-[60%]"
|
||||
value={ua}
|
||||
placeholder="默认 clash.meta"
|
||||
placeholder={t('mihomo.userAgentPlaceholder')}
|
||||
onValueChange={(v) => {
|
||||
setUa(v)
|
||||
setUaDebounce(v)
|
||||
}}
|
||||
></Input>
|
||||
</SettingItem>
|
||||
<SettingItem title="延迟测试地址" divider>
|
||||
<SettingItem title={t('mihomo.delayTest.url')} divider>
|
||||
<Input
|
||||
size="sm"
|
||||
className="w-[60%]"
|
||||
value={url}
|
||||
placeholder="默认 https://www.gstatic.com/generate_204"
|
||||
placeholder={t('mihomo.delayTest.urlPlaceholder')}
|
||||
onValueChange={(v) => {
|
||||
setUrl(v)
|
||||
setUrlDebounce(v)
|
||||
}}
|
||||
></Input>
|
||||
</SettingItem>
|
||||
<SettingItem title="延迟测试并发数量" divider>
|
||||
<SettingItem title={t('mihomo.delayTest.concurrency')} divider>
|
||||
<Input
|
||||
type="number"
|
||||
size="sm"
|
||||
className="w-[60%]"
|
||||
value={delayTestConcurrency?.toString()}
|
||||
placeholder="默认 50"
|
||||
placeholder={t('mihomo.delayTest.concurrencyPlaceholder')}
|
||||
onValueChange={(v) => {
|
||||
patchAppConfig({ delayTestConcurrency: parseInt(v) })
|
||||
}}
|
||||
/>
|
||||
</SettingItem>
|
||||
<SettingItem title="延迟测试超时时间" divider>
|
||||
<SettingItem title={t('mihomo.delayTest.timeout')} divider>
|
||||
<Input
|
||||
type="number"
|
||||
size="sm"
|
||||
className="w-[60%]"
|
||||
value={delayTestTimeout?.toString()}
|
||||
placeholder="默认 5000"
|
||||
placeholder={t('mihomo.delayTest.timeoutPlaceholder')}
|
||||
onValueChange={(v) => {
|
||||
patchAppConfig({ delayTestTimeout: parseInt(v) })
|
||||
}}
|
||||
/>
|
||||
</SettingItem>
|
||||
<SettingItem
|
||||
title="同步运行时配置到 Gist"
|
||||
title={t('mihomo.gist.title')}
|
||||
actions={
|
||||
<Button
|
||||
title="复制 Gist URL"
|
||||
title={t('mihomo.gist.copyUrl')}
|
||||
isIconOnly
|
||||
size="sm"
|
||||
variant="light"
|
||||
@ -114,32 +116,32 @@ const MihomoConfig: React.FC = () => {
|
||||
size="sm"
|
||||
className="w-[60%]"
|
||||
value={githubToken}
|
||||
placeholder="GitHub Token"
|
||||
placeholder={t('mihomo.gist.token')}
|
||||
onValueChange={(v) => {
|
||||
patchAppConfig({ githubToken: v })
|
||||
}}
|
||||
/>
|
||||
</SettingItem>
|
||||
<SettingItem title="代理节点展示列数" divider>
|
||||
<SettingItem title={t('mihomo.proxyColumns.title')} divider>
|
||||
<Select
|
||||
classNames={{ trigger: 'data-[hover=true]:bg-default-200' }}
|
||||
className="w-[150px]"
|
||||
size="sm"
|
||||
selectedKeys={new Set([proxyCols])}
|
||||
aria-label="选择代理节点展示列数"
|
||||
aria-label={t('mihomo.proxyColumns.title')}
|
||||
onSelectionChange={async (v) => {
|
||||
await patchAppConfig({ proxyCols: v.currentKey as 'auto' | '1' | '2' | '3' | '4' })
|
||||
}}
|
||||
>
|
||||
<SelectItem key="auto">自动</SelectItem>
|
||||
<SelectItem key="1">一列</SelectItem>
|
||||
<SelectItem key="2">两列</SelectItem>
|
||||
<SelectItem key="3">三列</SelectItem>
|
||||
<SelectItem key="4">四列</SelectItem>
|
||||
<SelectItem key="auto">{t('mihomo.proxyColumns.auto')}</SelectItem>
|
||||
<SelectItem key="1">{t('mihomo.proxyColumns.one')}</SelectItem>
|
||||
<SelectItem key="2">{t('mihomo.proxyColumns.two')}</SelectItem>
|
||||
<SelectItem key="3">{t('mihomo.proxyColumns.three')}</SelectItem>
|
||||
<SelectItem key="4">{t('mihomo.proxyColumns.four')}</SelectItem>
|
||||
</Select>
|
||||
</SettingItem>
|
||||
{platform === 'win32' && (
|
||||
<SettingItem title="内核进程优先级" divider>
|
||||
<SettingItem title={t('mihomo.cpuPriority.title')} divider>
|
||||
<Select
|
||||
classNames={{ trigger: 'data-[hover=true]:bg-default-200' }}
|
||||
className="w-[150px]"
|
||||
@ -156,19 +158,19 @@ const MihomoConfig: React.FC = () => {
|
||||
}
|
||||
}}
|
||||
>
|
||||
<SelectItem key="PRIORITY_HIGHEST">实时</SelectItem>
|
||||
<SelectItem key="PRIORITY_HIGH">高</SelectItem>
|
||||
<SelectItem key="PRIORITY_ABOVE_NORMAL">高于正常</SelectItem>
|
||||
<SelectItem key="PRIORITY_NORMAL">正常</SelectItem>
|
||||
<SelectItem key="PRIORITY_BELOW_NORMAL">低于正常</SelectItem>
|
||||
<SelectItem key="PRIORITY_LOW">低</SelectItem>
|
||||
<SelectItem key="PRIORITY_HIGHEST">{t('mihomo.cpuPriority.realtime')}</SelectItem>
|
||||
<SelectItem key="PRIORITY_HIGH">{t('mihomo.cpuPriority.high')}</SelectItem>
|
||||
<SelectItem key="PRIORITY_ABOVE_NORMAL">{t('mihomo.cpuPriority.aboveNormal')}</SelectItem>
|
||||
<SelectItem key="PRIORITY_NORMAL">{t('mihomo.cpuPriority.normal')}</SelectItem>
|
||||
<SelectItem key="PRIORITY_BELOW_NORMAL">{t('mihomo.cpuPriority.belowNormal')}</SelectItem>
|
||||
<SelectItem key="PRIORITY_LOW">{t('mihomo.cpuPriority.low')}</SelectItem>
|
||||
</Select>
|
||||
</SettingItem>
|
||||
)}
|
||||
<SettingItem
|
||||
title="为不同订阅分别指定工作目录"
|
||||
title={t('mihomo.workDir.title')}
|
||||
actions={
|
||||
<Tooltip content="开启后可以避免不同订阅中存在相同代理组名时无法分别保存选择的节点">
|
||||
<Tooltip content={t('mihomo.workDir.tooltip')}>
|
||||
<Button isIconOnly size="sm" variant="light">
|
||||
<IoIosHelpCircle className="text-lg" />
|
||||
</Button>
|
||||
@ -189,7 +191,7 @@ const MihomoConfig: React.FC = () => {
|
||||
}}
|
||||
/>
|
||||
</SettingItem>
|
||||
<SettingItem title="接管 DNS 设置" divider>
|
||||
<SettingItem title={t('mihomo.controlDns')} divider>
|
||||
<Switch
|
||||
size="sm"
|
||||
isSelected={controlDns}
|
||||
@ -204,7 +206,7 @@ const MihomoConfig: React.FC = () => {
|
||||
}}
|
||||
/>
|
||||
</SettingItem>
|
||||
<SettingItem title="接管域名嗅探设置" divider>
|
||||
<SettingItem title={t('mihomo.controlSniff')} divider>
|
||||
<Switch
|
||||
size="sm"
|
||||
isSelected={controlSniff}
|
||||
@ -219,7 +221,7 @@ const MihomoConfig: React.FC = () => {
|
||||
}}
|
||||
/>
|
||||
</SettingItem>
|
||||
<SettingItem title="自动断开连接" divider>
|
||||
<SettingItem title={t('mihomo.autoCloseConnection')} divider>
|
||||
<Switch
|
||||
size="sm"
|
||||
isSelected={autoCloseConnection}
|
||||
@ -228,7 +230,7 @@ const MihomoConfig: React.FC = () => {
|
||||
}}
|
||||
/>
|
||||
</SettingItem>
|
||||
<SettingItem title="在特定的 WiFi SSID 下直连">
|
||||
<SettingItem title={t('mihomo.pauseSSID.title')}>
|
||||
{pauseSSIDInput.join('') !== pauseSSID.join('') && (
|
||||
<Button
|
||||
size="sm"
|
||||
@ -237,7 +239,7 @@ const MihomoConfig: React.FC = () => {
|
||||
patchAppConfig({ pauseSSID: pauseSSIDInput })
|
||||
}}
|
||||
>
|
||||
确认
|
||||
{t('common.confirm')}
|
||||
</Button>
|
||||
)}
|
||||
</SettingItem>
|
||||
|
||||
@ -5,6 +5,7 @@ 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'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
const keyMap = {
|
||||
Backquote: '`',
|
||||
@ -40,6 +41,7 @@ const keyMap = {
|
||||
}
|
||||
|
||||
const ShortcutConfig: React.FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const { appConfig, patchAppConfig } = useAppConfig()
|
||||
const {
|
||||
showWindowShortcut = '',
|
||||
@ -54,8 +56,8 @@ const ShortcutConfig: React.FC = () => {
|
||||
} = appConfig || {}
|
||||
|
||||
return (
|
||||
<SettingCard title="快捷键设置">
|
||||
<SettingItem title="打开/关闭窗口" divider>
|
||||
<SettingCard title={t('shortcuts.title')}>
|
||||
<SettingItem title={t('shortcuts.toggleWindow')} divider>
|
||||
<div className="flex justify-end w-[60%]">
|
||||
<ShortcutInput
|
||||
value={showWindowShortcut}
|
||||
@ -64,7 +66,7 @@ const ShortcutConfig: React.FC = () => {
|
||||
/>
|
||||
</div>
|
||||
</SettingItem>
|
||||
<SettingItem title="打开/关闭悬浮窗" divider>
|
||||
<SettingItem title={t('shortcuts.toggleFloatingWindow')} divider>
|
||||
<div className="flex justify-end w-[60%]">
|
||||
<ShortcutInput
|
||||
value={showFloatingWindowShortcut}
|
||||
@ -73,7 +75,7 @@ const ShortcutConfig: React.FC = () => {
|
||||
/>
|
||||
</div>
|
||||
</SettingItem>
|
||||
<SettingItem title="打开/关闭系统代理" divider>
|
||||
<SettingItem title={t('shortcuts.toggleSystemProxy')} divider>
|
||||
<div className="flex justify-end w-[60%]">
|
||||
<ShortcutInput
|
||||
value={triggerSysProxyShortcut}
|
||||
@ -82,7 +84,7 @@ const ShortcutConfig: React.FC = () => {
|
||||
/>
|
||||
</div>
|
||||
</SettingItem>
|
||||
<SettingItem title="打开/关闭虚拟网卡" divider>
|
||||
<SettingItem title={t('shortcuts.toggleTun')} divider>
|
||||
<div className="flex justify-end w-[60%]">
|
||||
<ShortcutInput
|
||||
value={triggerTunShortcut}
|
||||
@ -91,7 +93,7 @@ const ShortcutConfig: React.FC = () => {
|
||||
/>
|
||||
</div>
|
||||
</SettingItem>
|
||||
<SettingItem title="切换规则模式" divider>
|
||||
<SettingItem title={t('shortcuts.toggleRuleMode')} divider>
|
||||
<div className="flex justify-end w-[60%]">
|
||||
<ShortcutInput
|
||||
value={ruleModeShortcut}
|
||||
@ -100,7 +102,7 @@ const ShortcutConfig: React.FC = () => {
|
||||
/>
|
||||
</div>
|
||||
</SettingItem>
|
||||
<SettingItem title="切换全局模式" divider>
|
||||
<SettingItem title={t('shortcuts.toggleGlobalMode')} divider>
|
||||
<div className="flex justify-end w-[60%]">
|
||||
<ShortcutInput
|
||||
value={globalModeShortcut}
|
||||
@ -109,7 +111,7 @@ const ShortcutConfig: React.FC = () => {
|
||||
/>
|
||||
</div>
|
||||
</SettingItem>
|
||||
<SettingItem title="切换直连模式" divider>
|
||||
<SettingItem title={t('shortcuts.toggleDirectMode')} divider>
|
||||
<div className="flex justify-end w-[60%]">
|
||||
<ShortcutInput
|
||||
value={directModeShortcut}
|
||||
@ -118,7 +120,7 @@ const ShortcutConfig: React.FC = () => {
|
||||
/>
|
||||
</div>
|
||||
</SettingItem>
|
||||
<SettingItem title="轻量模式" divider>
|
||||
<SettingItem title={t('shortcuts.toggleLightMode')} divider>
|
||||
<div className="flex justify-end w-[60%]">
|
||||
<ShortcutInput
|
||||
value={quitWithoutCoreShortcut}
|
||||
@ -127,7 +129,7 @@ const ShortcutConfig: React.FC = () => {
|
||||
/>
|
||||
</div>
|
||||
</SettingItem>
|
||||
<SettingItem title="重启应用">
|
||||
<SettingItem title={t('shortcuts.restartApp')}>
|
||||
<div className="flex justify-end w-[60%]">
|
||||
<ShortcutInput
|
||||
value={restartAppShortcut}
|
||||
@ -145,6 +147,7 @@ const ShortcutInput: React.FC<{
|
||||
action: string
|
||||
patchAppConfig: (value: Partial<IAppConfig>) => Promise<void>
|
||||
}> = (props) => {
|
||||
const { t } = useTranslation()
|
||||
const { value, action, patchAppConfig } = props
|
||||
const [inputValue, setInputValue] = useState(value)
|
||||
|
||||
@ -210,18 +213,18 @@ const ShortcutInput: React.FC<{
|
||||
await patchAppConfig({ [action]: inputValue })
|
||||
window.electron.ipcRenderer.send('updateTrayMenu')
|
||||
} else {
|
||||
alert('快捷键注册失败')
|
||||
alert(t('common.error.shortcutRegistrationFailed'))
|
||||
}
|
||||
} catch (e) {
|
||||
alert(`快捷键注册失败: ${e}`)
|
||||
alert(t('common.error.shortcutRegistrationFailedWithError', { error: e }))
|
||||
}
|
||||
}}
|
||||
>
|
||||
确认
|
||||
{t('common.confirm')}
|
||||
</Button>
|
||||
)}
|
||||
<Input
|
||||
placeholder="点击输入快捷键"
|
||||
placeholder={t('shortcuts.input.placeholder')}
|
||||
onKeyDown={(e: KeyboardEvent): void => {
|
||||
parseShortcut(e, setInputValue)
|
||||
}}
|
||||
|
||||
@ -1,76 +1,73 @@
|
||||
import React from 'react'
|
||||
import SettingCard from '../base/base-setting-card'
|
||||
import SettingItem from '../base/base-setting-item'
|
||||
import { RadioGroup, Radio } from '@nextui-org/react'
|
||||
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'
|
||||
const titleMap = {
|
||||
sysproxyCardStatus: '系统代理',
|
||||
tunCardStatus: '虚拟网卡',
|
||||
profileCardStatus: '订阅管理',
|
||||
proxyCardStatus: '代理组',
|
||||
ruleCardStatus: '规则',
|
||||
resourceCardStatus: '外部资源',
|
||||
overrideCardStatus: '覆写',
|
||||
connectionCardStatus: '连接',
|
||||
mihomoCoreCardStatus: '内核',
|
||||
dnsCardStatus: 'DNS',
|
||||
sniffCardStatus: '域名嗅探',
|
||||
logCardStatus: '日志',
|
||||
substoreCardStatus: 'Sub-Store'
|
||||
import { Radio, RadioGroup } from '@nextui-org/react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import type { FC } from 'react'
|
||||
|
||||
const titleMap: Record<string, string> = {
|
||||
sysproxyCardStatus: 'sider.cards.systemProxy',
|
||||
tunCardStatus: 'sider.cards.tun',
|
||||
profileCardStatus: 'sider.cards.profiles',
|
||||
proxyCardStatus: 'sider.cards.proxies',
|
||||
ruleCardStatus: 'sider.cards.rules',
|
||||
resourceCardStatus: 'sider.cards.resources',
|
||||
overrideCardStatus: 'sider.cards.override',
|
||||
connectionCardStatus: 'sider.cards.connections',
|
||||
mihomoCoreCardStatus: 'sider.cards.core',
|
||||
dnsCardStatus: 'sider.cards.dns',
|
||||
sniffCardStatus: 'sider.cards.sniff',
|
||||
logCardStatus: 'sider.cards.logs',
|
||||
substoreCardStatus: 'sider.cards.substore'
|
||||
}
|
||||
const SiderConfig: React.FC = () => {
|
||||
|
||||
const sizeMap: Record<string, string> = {
|
||||
'col-span-2': 'sider.size.large',
|
||||
'col-span-1': 'sider.size.small',
|
||||
hidden: 'sider.size.hidden'
|
||||
}
|
||||
|
||||
const SiderConfig: FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const { appConfig, patchAppConfig } = useAppConfig()
|
||||
const {
|
||||
sysproxyCardStatus = 'col-span-1',
|
||||
tunCardStatus = 'col-span-1',
|
||||
profileCardStatus = 'col-span-2',
|
||||
proxyCardStatus = 'col-span-1',
|
||||
ruleCardStatus = 'col-span-1',
|
||||
resourceCardStatus = 'col-span-1',
|
||||
overrideCardStatus = 'col-span-1',
|
||||
connectionCardStatus = 'col-span-2',
|
||||
mihomoCoreCardStatus = 'col-span-2',
|
||||
dnsCardStatus = 'col-span-1',
|
||||
sniffCardStatus = 'col-span-1',
|
||||
logCardStatus = 'col-span-1',
|
||||
substoreCardStatus = 'col-span-1'
|
||||
} = appConfig || {}
|
||||
|
||||
const cardStatus = {
|
||||
sysproxyCardStatus,
|
||||
tunCardStatus,
|
||||
profileCardStatus,
|
||||
proxyCardStatus,
|
||||
ruleCardStatus,
|
||||
resourceCardStatus,
|
||||
overrideCardStatus,
|
||||
connectionCardStatus,
|
||||
mihomoCoreCardStatus,
|
||||
dnsCardStatus,
|
||||
sniffCardStatus,
|
||||
logCardStatus,
|
||||
substoreCardStatus
|
||||
sysproxyCardStatus: appConfig?.sysproxyCardStatus || 'col-span-1',
|
||||
tunCardStatus: appConfig?.tunCardStatus || 'col-span-1',
|
||||
profileCardStatus: appConfig?.profileCardStatus || 'col-span-2',
|
||||
proxyCardStatus: appConfig?.proxyCardStatus || 'col-span-1',
|
||||
ruleCardStatus: appConfig?.ruleCardStatus || 'col-span-1',
|
||||
resourceCardStatus: appConfig?.resourceCardStatus || 'col-span-1',
|
||||
overrideCardStatus: appConfig?.overrideCardStatus || 'col-span-1',
|
||||
connectionCardStatus: appConfig?.connectionCardStatus || 'col-span-2',
|
||||
mihomoCoreCardStatus: appConfig?.mihomoCoreCardStatus || 'col-span-2',
|
||||
dnsCardStatus: appConfig?.dnsCardStatus || 'col-span-1',
|
||||
sniffCardStatus: appConfig?.sniffCardStatus || 'col-span-1',
|
||||
logCardStatus: appConfig?.logCardStatus || 'col-span-1',
|
||||
substoreCardStatus: appConfig?.substoreCardStatus || 'col-span-1'
|
||||
}
|
||||
|
||||
return (
|
||||
<SettingCard title="侧边栏设置">
|
||||
{Object.keys(cardStatus).map((key, index, array) => {
|
||||
return (
|
||||
<SettingItem title={titleMap[key]} key={key} divider={index !== array.length - 1}>
|
||||
<RadioGroup
|
||||
orientation="horizontal"
|
||||
value={cardStatus[key]}
|
||||
onValueChange={(v) => {
|
||||
patchAppConfig({ [key]: v as CardStatus })
|
||||
}}
|
||||
>
|
||||
<Radio value="col-span-2">大</Radio>
|
||||
<Radio value="col-span-1">小</Radio>
|
||||
<Radio value="hidden">隐藏</Radio>
|
||||
</RadioGroup>
|
||||
</SettingItem>
|
||||
)
|
||||
})}
|
||||
<SettingCard title={t('sider.title')}>
|
||||
{Object.entries(cardStatus).map(([key, value]) => (
|
||||
<SettingItem key={key} title={t(titleMap[key])}>
|
||||
<RadioGroup
|
||||
orientation="horizontal"
|
||||
value={value}
|
||||
onValueChange={(v: string) => {
|
||||
if (v === 'col-span-1' || v === 'col-span-2' || v === 'hidden') {
|
||||
patchAppConfig({ [key]: v })
|
||||
}
|
||||
}}
|
||||
>
|
||||
{Object.entries(sizeMap).map(([size, label]) => (
|
||||
<Radio key={size} value={size}>
|
||||
{t(label)}
|
||||
</Radio>
|
||||
))}
|
||||
</RadioGroup>
|
||||
</SettingItem>
|
||||
))}
|
||||
</SettingCard>
|
||||
)
|
||||
}
|
||||
|
||||
@ -11,8 +11,10 @@ import {
|
||||
import { useAppConfig } from '@renderer/hooks/use-app-config'
|
||||
import debounce from '@renderer/utils/debounce'
|
||||
import { isValidCron } from 'cron-validator'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
const SubStoreConfig: React.FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const { appConfig, patchAppConfig } = useAppConfig()
|
||||
const {
|
||||
useSubStore = true,
|
||||
@ -37,8 +39,8 @@ const SubStoreConfig: React.FC = () => {
|
||||
const [subStoreBackendUploadCronValue, setSubStoreBackendUploadCronValue] =
|
||||
useState(subStoreBackendUploadCron)
|
||||
return (
|
||||
<SettingCard title="Sub-Store 设置">
|
||||
<SettingItem title="启用 Sub-Store" divider={useSubStore}>
|
||||
<SettingCard title={t('substore.title')}>
|
||||
<SettingItem title={t('substore.enable')} divider={useSubStore}>
|
||||
<Switch
|
||||
size="sm"
|
||||
isSelected={useSubStore}
|
||||
@ -60,7 +62,7 @@ const SubStoreConfig: React.FC = () => {
|
||||
</SettingItem>
|
||||
{useSubStore && (
|
||||
<>
|
||||
<SettingItem title="允许局域网连接" divider>
|
||||
<SettingItem title={t('substore.allowLan')} divider>
|
||||
<Switch
|
||||
size="sm"
|
||||
isSelected={subStoreHost === '0.0.0.0'}
|
||||
@ -79,7 +81,7 @@ const SubStoreConfig: React.FC = () => {
|
||||
}}
|
||||
/>
|
||||
</SettingItem>
|
||||
<SettingItem title="使用自建 Sub-Store 后端" divider>
|
||||
<SettingItem title={t('substore.useCustomBackend')} divider>
|
||||
<Switch
|
||||
size="sm"
|
||||
isSelected={useCustomSubStore}
|
||||
@ -98,12 +100,12 @@ const SubStoreConfig: React.FC = () => {
|
||||
/>
|
||||
</SettingItem>
|
||||
{useCustomSubStore ? (
|
||||
<SettingItem title="自建 Sub-Store 后端地址">
|
||||
<SettingItem title={t('substore.customBackendUrl.title')}>
|
||||
<Input
|
||||
size="sm"
|
||||
className="w-[60%]"
|
||||
value={customSubStoreUrlValue}
|
||||
placeholder="必须包含协议头"
|
||||
placeholder={t('substore.customBackendUrl.placeholder')}
|
||||
onValueChange={(v: string) => {
|
||||
setCustomSubStoreUrlValue(v)
|
||||
setCustomSubStoreUrl(v)
|
||||
@ -112,7 +114,7 @@ const SubStoreConfig: React.FC = () => {
|
||||
</SettingItem>
|
||||
) : (
|
||||
<>
|
||||
<SettingItem title="为 Sub-Store 内所有请求启用代理" divider>
|
||||
<SettingItem title={t('substore.useProxy')} divider>
|
||||
<Switch
|
||||
size="sm"
|
||||
isSelected={useProxyInSubStore}
|
||||
@ -126,7 +128,7 @@ const SubStoreConfig: React.FC = () => {
|
||||
}}
|
||||
/>
|
||||
</SettingItem>
|
||||
<SettingItem title="定时同步订阅/文件" divider>
|
||||
<SettingItem title={t('substore.sync.title')} divider>
|
||||
<div className="flex w-[60%] gap-2">
|
||||
{subStoreBackendSyncCronValue !== subStoreBackendSyncCron && (
|
||||
<Button
|
||||
@ -140,26 +142,26 @@ const SubStoreConfig: React.FC = () => {
|
||||
await patchAppConfig({
|
||||
subStoreBackendSyncCron: subStoreBackendSyncCronValue
|
||||
})
|
||||
new Notification('重启应用生效')
|
||||
new Notification(t('common.notification.restartRequired'))
|
||||
} else {
|
||||
alert('Cron 表达式无效')
|
||||
alert(t('common.error.invalidCron'))
|
||||
}
|
||||
}}
|
||||
>
|
||||
确认
|
||||
{t('common.confirm')}
|
||||
</Button>
|
||||
)}
|
||||
<Input
|
||||
size="sm"
|
||||
value={subStoreBackendSyncCronValue}
|
||||
placeholder="Cron 表达式"
|
||||
placeholder={t('substore.sync.placeholder')}
|
||||
onValueChange={(v: string) => {
|
||||
setSubStoreBackendSyncCronValue(v)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</SettingItem>
|
||||
<SettingItem title="定时恢复配置" divider>
|
||||
<SettingItem title={t('substore.restore.title')} divider>
|
||||
<div className="flex w-[60%] gap-2">
|
||||
{subStoreBackendDownloadCronValue !== subStoreBackendDownloadCron && (
|
||||
<Button
|
||||
@ -173,26 +175,26 @@ const SubStoreConfig: React.FC = () => {
|
||||
await patchAppConfig({
|
||||
subStoreBackendDownloadCron: subStoreBackendDownloadCronValue
|
||||
})
|
||||
new Notification('重启应用生效')
|
||||
new Notification(t('common.notification.restartRequired'))
|
||||
} else {
|
||||
alert('Cron 表达式无效')
|
||||
alert(t('common.error.invalidCron'))
|
||||
}
|
||||
}}
|
||||
>
|
||||
确认
|
||||
{t('common.confirm')}
|
||||
</Button>
|
||||
)}
|
||||
<Input
|
||||
size="sm"
|
||||
value={subStoreBackendDownloadCronValue}
|
||||
placeholder="Cron 表达式"
|
||||
placeholder={t('substore.restore.placeholder')}
|
||||
onValueChange={(v: string) => {
|
||||
setSubStoreBackendDownloadCronValue(v)
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</SettingItem>
|
||||
<SettingItem title="定时备份配置">
|
||||
<SettingItem title={t('substore.backup.title')}>
|
||||
<div className="flex w-[60%] gap-2">
|
||||
{subStoreBackendUploadCronValue !== subStoreBackendUploadCron && (
|
||||
<Button
|
||||
@ -206,19 +208,19 @@ const SubStoreConfig: React.FC = () => {
|
||||
await patchAppConfig({
|
||||
subStoreBackendUploadCron: subStoreBackendUploadCronValue
|
||||
})
|
||||
new Notification('重启应用生效')
|
||||
new Notification(t('common.notification.restartRequired'))
|
||||
} else {
|
||||
alert('Cron 表达式无效')
|
||||
alert(t('common.error.invalidCron'))
|
||||
}
|
||||
}}
|
||||
>
|
||||
确认
|
||||
{t('common.confirm')}
|
||||
</Button>
|
||||
)}
|
||||
<Input
|
||||
size="sm"
|
||||
value={subStoreBackendUploadCronValue}
|
||||
placeholder="Cron 表达式"
|
||||
placeholder={t('substore.backup.placeholder')}
|
||||
onValueChange={(v: string) => {
|
||||
setSubStoreBackendUploadCronValue(v)
|
||||
}}
|
||||
|
||||
@ -6,8 +6,10 @@ 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'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
const WebdavConfig: React.FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const { appConfig, patchAppConfig } = useAppConfig()
|
||||
const { webdavUrl, webdavUsername, webdavPassword, webdavDir = 'mihomo-party' } = appConfig || {}
|
||||
const [backuping, setBackuping] = useState(false)
|
||||
@ -23,7 +25,9 @@ const WebdavConfig: React.FC = () => {
|
||||
setBackuping(true)
|
||||
try {
|
||||
await webdavBackup()
|
||||
new window.Notification('备份成功', { body: '备份文件已上传至 WebDAV' })
|
||||
new window.Notification(t('webdav.notification.backupSuccess.title'), {
|
||||
body: t('webdav.notification.backupSuccess.body')
|
||||
})
|
||||
} catch (e) {
|
||||
alert(e)
|
||||
} finally {
|
||||
@ -38,7 +42,7 @@ const WebdavConfig: React.FC = () => {
|
||||
setFilenames(filenames)
|
||||
setRestoreOpen(true)
|
||||
} catch (e) {
|
||||
alert(`获取备份列表失败: ${e}`)
|
||||
alert(t('common.error.getBackupListFailed', { error: e }))
|
||||
} finally {
|
||||
setRestoring(false)
|
||||
}
|
||||
@ -48,8 +52,8 @@ const WebdavConfig: React.FC = () => {
|
||||
{restoreOpen && (
|
||||
<WebdavRestoreModal filenames={filenames} onClose={() => setRestoreOpen(false)} />
|
||||
)}
|
||||
<SettingCard title="WebDAV 备份">
|
||||
<SettingItem title="WebDAV 地址" divider>
|
||||
<SettingCard title={t('webdav.title')}>
|
||||
<SettingItem title={t('webdav.url')} divider>
|
||||
<Input
|
||||
size="sm"
|
||||
className="w-[60%]"
|
||||
@ -60,7 +64,7 @@ const WebdavConfig: React.FC = () => {
|
||||
}}
|
||||
/>
|
||||
</SettingItem>
|
||||
<SettingItem title="WebDAV 备份目录" divider>
|
||||
<SettingItem title={t('webdav.dir')} divider>
|
||||
<Input
|
||||
size="sm"
|
||||
className="w-[60%]"
|
||||
@ -71,7 +75,7 @@ const WebdavConfig: React.FC = () => {
|
||||
}}
|
||||
/>
|
||||
</SettingItem>
|
||||
<SettingItem title="WebDAV 用户名" divider>
|
||||
<SettingItem title={t('webdav.username')} divider>
|
||||
<Input
|
||||
size="sm"
|
||||
className="w-[60%]"
|
||||
@ -82,7 +86,7 @@ const WebdavConfig: React.FC = () => {
|
||||
}}
|
||||
/>
|
||||
</SettingItem>
|
||||
<SettingItem title="WebDAV 密码" divider>
|
||||
<SettingItem title={t('webdav.password')} divider>
|
||||
<Input
|
||||
size="sm"
|
||||
className="w-[60%]"
|
||||
@ -96,7 +100,7 @@ const WebdavConfig: React.FC = () => {
|
||||
</SettingItem>
|
||||
<div className="flex justify0between">
|
||||
<Button isLoading={backuping} fullWidth size="sm" className="mr-1" onPress={handleBackup}>
|
||||
备份
|
||||
{t('webdav.backup')}
|
||||
</Button>
|
||||
<Button
|
||||
isLoading={restoring}
|
||||
@ -105,7 +109,7 @@ const WebdavConfig: React.FC = () => {
|
||||
className="ml-1"
|
||||
onPress={handleRestore}
|
||||
>
|
||||
恢复
|
||||
{t('webdav.restore')}
|
||||
</Button>
|
||||
</div>
|
||||
</SettingCard>
|
||||
|
||||
@ -2,11 +2,15 @@ import { Modal, ModalContent, ModalHeader, ModalBody, ModalFooter, Button } from
|
||||
import { relaunchApp, webdavDelete, webdavRestore } from '@renderer/utils/ipc'
|
||||
import React, { useState } from 'react'
|
||||
import { MdDeleteForever } from 'react-icons/md'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
interface Props {
|
||||
filenames: string[]
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
const WebdavRestoreModal: React.FC<Props> = (props) => {
|
||||
const { t } = useTranslation()
|
||||
const { filenames: names, onClose } = props
|
||||
const [filenames, setFilenames] = useState<string[]>(names)
|
||||
const [restoring, setRestoring] = useState(false)
|
||||
@ -21,10 +25,10 @@ const WebdavRestoreModal: React.FC<Props> = (props) => {
|
||||
scrollBehavior="inside"
|
||||
>
|
||||
<ModalContent>
|
||||
<ModalHeader className="flex app-drag">恢复备份</ModalHeader>
|
||||
<ModalHeader className="flex app-drag">{t('webdav.restore.title')}</ModalHeader>
|
||||
<ModalBody>
|
||||
{filenames.length === 0 ? (
|
||||
<div className="flex justify-center">还没有备份</div>
|
||||
<div className="flex justify-center">{t('webdav.restore.noBackups')}</div>
|
||||
) : (
|
||||
filenames.map((filename) => (
|
||||
<div className="flex" key={filename}>
|
||||
@ -39,7 +43,7 @@ const WebdavRestoreModal: React.FC<Props> = (props) => {
|
||||
await webdavRestore(filename)
|
||||
await relaunchApp()
|
||||
} catch (e) {
|
||||
alert(`恢复失败: ${e}`)
|
||||
alert(t('common.error.restoreFailed', { error: e }))
|
||||
} finally {
|
||||
setRestoring(false)
|
||||
}
|
||||
@ -57,7 +61,7 @@ const WebdavRestoreModal: React.FC<Props> = (props) => {
|
||||
await webdavDelete(filename)
|
||||
setFilenames(filenames.filter((name) => name !== filename))
|
||||
} catch (e) {
|
||||
alert(`删除失败: ${e}`)
|
||||
alert(t('common.error.deleteFailed', { error: e }))
|
||||
}
|
||||
}}
|
||||
>
|
||||
@ -69,7 +73,7 @@ const WebdavRestoreModal: React.FC<Props> = (props) => {
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button size="sm" variant="light" onPress={onClose}>
|
||||
关闭
|
||||
{t('common.close')}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
|
||||
@ -2,10 +2,13 @@ import { Modal, ModalContent, ModalHeader, ModalBody, ModalFooter, Button } from
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { BaseEditor } from '../base/base-editor'
|
||||
import { getRuntimeConfigStr } from '@renderer/utils/ipc'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
interface Props {
|
||||
onClose: () => void
|
||||
}
|
||||
const ConfigViewer: React.FC<Props> = (props) => {
|
||||
const { t } = useTranslation()
|
||||
const { onClose } = props
|
||||
const [currData, setCurrData] = useState('')
|
||||
|
||||
@ -28,13 +31,13 @@ const ConfigViewer: React.FC<Props> = (props) => {
|
||||
scrollBehavior="inside"
|
||||
>
|
||||
<ModalContent className="h-full w-[calc(100%-100px)]">
|
||||
<ModalHeader className="flex pb-0 app-drag">当前运行时配置</ModalHeader>
|
||||
<ModalHeader className="flex pb-0 app-drag">{t('sider.cards.config')}</ModalHeader>
|
||||
<ModalBody className="h-full">
|
||||
<BaseEditor language="yaml" value={currData} readOnly={true} />
|
||||
</ModalBody>
|
||||
<ModalFooter className="pt-0">
|
||||
<Button size="sm" variant="light" onPress={onClose}>
|
||||
关闭
|
||||
{t('common.close')}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
|
||||
@ -10,6 +10,7 @@ import { useTheme } from 'next-themes'
|
||||
import { useAppConfig } from '@renderer/hooks/use-app-config'
|
||||
import { platform } from '@renderer/utils/init'
|
||||
import { Area, AreaChart, ResponsiveContainer } from 'recharts'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
let currentUpload: number | undefined = undefined
|
||||
let currentDownload: number | undefined = undefined
|
||||
@ -27,6 +28,7 @@ const ConnCard: React.FC<Props> = (props) => {
|
||||
const location = useLocation()
|
||||
const navigate = useNavigate()
|
||||
const match = location.pathname.includes('/connections')
|
||||
const { t } = useTranslation()
|
||||
|
||||
const [upload, setUpload] = useState(0)
|
||||
const [download, setDownload] = useState(0)
|
||||
@ -95,7 +97,7 @@ const ConnCard: React.FC<Props> = (props) => {
|
||||
if (iconOnly) {
|
||||
return (
|
||||
<div className={`${connectionCardStatus} flex justify-center`}>
|
||||
<Tooltip content="连接" placement="right">
|
||||
<Tooltip content={t('sider.cards.connections')} placement="right">
|
||||
<Button
|
||||
size="sm"
|
||||
isIconOnly
|
||||
@ -162,7 +164,7 @@ const ConnCard: React.FC<Props> = (props) => {
|
||||
<h3
|
||||
className={`text-md font-bold ${match ? 'text-primary-foreground' : 'text-foreground'}`}
|
||||
>
|
||||
连接
|
||||
{t('sider.cards.connections')}
|
||||
</h3>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
@ -218,7 +220,7 @@ const ConnCard: React.FC<Props> = (props) => {
|
||||
<h3
|
||||
className={`text-md font-bold ${match ? 'text-primary-foreground' : 'text-foreground'}`}
|
||||
>
|
||||
连接
|
||||
{t('sider.cards.connections')}
|
||||
</h3>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
|
||||
@ -8,11 +8,13 @@ import { useSortable } from '@dnd-kit/sortable'
|
||||
import { CSS } from '@dnd-kit/utilities'
|
||||
import { useAppConfig } from '@renderer/hooks/use-app-config'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
interface Props {
|
||||
iconOnly?: boolean
|
||||
}
|
||||
const DNSCard: React.FC<Props> = (props) => {
|
||||
const { t } = useTranslation()
|
||||
const { appConfig } = useAppConfig()
|
||||
const { iconOnly } = props
|
||||
const { dnsCardStatus = 'col-span-1', controlDns = true } = appConfig || {}
|
||||
@ -41,7 +43,7 @@ const DNSCard: React.FC<Props> = (props) => {
|
||||
if (iconOnly) {
|
||||
return (
|
||||
<div className={`${dnsCardStatus} ${!controlDns ? 'hidden' : ''} flex justify-center`}>
|
||||
<Tooltip content="DNS" placement="right">
|
||||
<Tooltip content={t('sider.cards.dns')} placement="right">
|
||||
<Button
|
||||
size="sm"
|
||||
isIconOnly
|
||||
@ -99,7 +101,7 @@ const DNSCard: React.FC<Props> = (props) => {
|
||||
<h3
|
||||
className={`text-md font-bold ${match ? 'text-primary-foreground' : 'text-foreground'}`}
|
||||
>
|
||||
DNS
|
||||
{t('sider.cards.dns')}
|
||||
</h3>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
|
||||
@ -5,12 +5,14 @@ import { useSortable } from '@dnd-kit/sortable'
|
||||
import { CSS } from '@dnd-kit/utilities'
|
||||
import { useAppConfig } from '@renderer/hooks/use-app-config'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
interface Props {
|
||||
iconOnly?: boolean
|
||||
}
|
||||
|
||||
const LogCard: React.FC<Props> = (props) => {
|
||||
const { t } = useTranslation()
|
||||
const { appConfig } = useAppConfig()
|
||||
const { iconOnly } = props
|
||||
const { logCardStatus = 'col-span-1' } = appConfig || {}
|
||||
@ -32,7 +34,7 @@ const LogCard: React.FC<Props> = (props) => {
|
||||
if (iconOnly) {
|
||||
return (
|
||||
<div className={`${logCardStatus} flex justify-center`}>
|
||||
<Tooltip content="日志" placement="right">
|
||||
<Tooltip content={t('sider.cards.logs')} placement="right">
|
||||
<Button
|
||||
size="sm"
|
||||
isIconOnly
|
||||
@ -84,7 +86,7 @@ const LogCard: React.FC<Props> = (props) => {
|
||||
<h3
|
||||
className={`text-md font-bold ${match ? 'text-primary-foreground' : 'text-foreground'}`}
|
||||
>
|
||||
日志
|
||||
{t('sider.cards.logs')}
|
||||
</h3>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
|
||||
@ -10,6 +10,7 @@ import PubSub from 'pubsub-js'
|
||||
import useSWR from 'swr'
|
||||
import { useAppConfig } from '@renderer/hooks/use-app-config'
|
||||
import { LuCpu } from 'react-icons/lu'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
interface Props {
|
||||
iconOnly?: boolean
|
||||
@ -35,6 +36,7 @@ const MihomoCoreCard: React.FC<Props> = (props) => {
|
||||
})
|
||||
const transform = tf ? { x: tf.x, y: tf.y, scaleX: 1, scaleY: 1 } : null
|
||||
const [mem, setMem] = useState(0)
|
||||
const { t } = useTranslation()
|
||||
|
||||
useEffect(() => {
|
||||
const token = PubSub.subscribe('mihomo-core-changed', () => {
|
||||
@ -52,7 +54,7 @@ const MihomoCoreCard: React.FC<Props> = (props) => {
|
||||
if (iconOnly) {
|
||||
return (
|
||||
<div className={`${mihomoCoreCardStatus} flex justify-center`}>
|
||||
<Tooltip content="内核设置" placement="right">
|
||||
<Tooltip content={t('sider.cards.core')} placement="right">
|
||||
<Button
|
||||
size="sm"
|
||||
isIconOnly
|
||||
@ -125,7 +127,7 @@ const MihomoCoreCard: React.FC<Props> = (props) => {
|
||||
<div
|
||||
className={`flex justify-between w-full text-md font-bold ${match ? 'text-primary-foreground' : 'text-foreground'}`}
|
||||
>
|
||||
<h4>内核设置</h4>
|
||||
<h4>{t('sider.cards.core')}</h4>
|
||||
<h4>{calcTraffic(mem)}</h4>
|
||||
</div>
|
||||
</CardFooter>
|
||||
@ -157,7 +159,7 @@ const MihomoCoreCard: React.FC<Props> = (props) => {
|
||||
<h3
|
||||
className={`text-md font-bold ${match ? 'text-primary-foreground' : 'text-foreground'}`}
|
||||
>
|
||||
内核设置
|
||||
{t('sider.cards.core')}
|
||||
</h3>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
|
||||
@ -4,8 +4,10 @@ import { useControledMihomoConfig } from '@renderer/hooks/use-controled-mihomo-c
|
||||
import { useGroups } from '@renderer/hooks/use-groups'
|
||||
import { mihomoCloseAllConnections, patchMihomoConfig } from '@renderer/utils/ipc'
|
||||
import { Key } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
const OutboundModeSwitcher: React.FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const { controledMihomoConfig, patchControledMihomoConfig } = useControledMihomoConfig()
|
||||
const { mutate: mutateGroups } = useGroups()
|
||||
const { appConfig } = useAppConfig()
|
||||
@ -32,9 +34,9 @@ const OutboundModeSwitcher: React.FC = () => {
|
||||
}}
|
||||
onSelectionChange={(key: Key) => onChangeMode(key as OutboundMode)}
|
||||
>
|
||||
<Tab className={`${mode === 'rule' ? 'font-bold' : ''}`} key="rule" title="规则" />
|
||||
<Tab className={`${mode === 'global' ? 'font-bold' : ''}`} key="global" title="全局" />
|
||||
<Tab className={`${mode === 'direct' ? 'font-bold' : ''}`} key="direct" title="直连" />
|
||||
<Tab className={`${mode === 'rule' ? 'font-bold' : ''}`} key="rule" title={t('sider.cards.outbound.rule')} />
|
||||
<Tab className={`${mode === 'global' ? 'font-bold' : ''}`} key="global" title={t('sider.cards.outbound.global')} />
|
||||
<Tab className={`${mode === 'direct' ? 'font-bold' : ''}`} key="direct" title={t('sider.cards.outbound.direct')} />
|
||||
</Tabs>
|
||||
)
|
||||
}
|
||||
|
||||
@ -5,12 +5,14 @@ import { useLocation, useNavigate } from 'react-router-dom'
|
||||
import { useSortable } from '@dnd-kit/sortable'
|
||||
import { CSS } from '@dnd-kit/utilities'
|
||||
import { useAppConfig } from '@renderer/hooks/use-app-config'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
interface Props {
|
||||
iconOnly?: boolean
|
||||
}
|
||||
|
||||
const OverrideCard: React.FC<Props> = (props) => {
|
||||
const { t } = useTranslation()
|
||||
const { appConfig } = useAppConfig()
|
||||
const { iconOnly } = props
|
||||
const { overrideCardStatus = 'col-span-1' } = appConfig || {}
|
||||
@ -31,7 +33,7 @@ const OverrideCard: React.FC<Props> = (props) => {
|
||||
if (iconOnly) {
|
||||
return (
|
||||
<div className={`${overrideCardStatus} flex justify-center`}>
|
||||
<Tooltip content="覆写" placement="right">
|
||||
<Tooltip content={t('sider.cards.override')} placement="right">
|
||||
<Button
|
||||
size="sm"
|
||||
isIconOnly
|
||||
@ -83,7 +85,7 @@ const OverrideCard: React.FC<Props> = (props) => {
|
||||
<h3
|
||||
className={`text-md font-bold ${match ? 'text-primary-foreground' : 'text-foreground'}`}
|
||||
>
|
||||
覆写
|
||||
{t('sider.cards.override')}
|
||||
</h3>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
|
||||
@ -8,11 +8,12 @@ import relativeTime from 'dayjs/plugin/relativeTime'
|
||||
import { useSortable } from '@dnd-kit/sortable'
|
||||
import { CSS } from '@dnd-kit/utilities'
|
||||
import 'dayjs/locale/zh-cn'
|
||||
import dayjs from 'dayjs'
|
||||
import dayjs from '@renderer/utils/dayjs'
|
||||
import React, { useState } from 'react'
|
||||
import ConfigViewer from './config-viewer'
|
||||
import { useAppConfig } from '@renderer/hooks/use-app-config'
|
||||
import { TiFolder } from 'react-icons/ti'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
dayjs.extend(relativeTime)
|
||||
dayjs.locale('zh-cn')
|
||||
@ -22,6 +23,7 @@ interface Props {
|
||||
}
|
||||
|
||||
const ProfileCard: React.FC<Props> = (props) => {
|
||||
const { t } = useTranslation()
|
||||
const { appConfig, patchAppConfig } = useAppConfig()
|
||||
const { iconOnly } = props
|
||||
const { profileCardStatus = 'col-span-2', profileDisplayDate = 'expire' } = appConfig || {}
|
||||
@ -46,7 +48,7 @@ const ProfileCard: React.FC<Props> = (props) => {
|
||||
const info = items?.find((item) => item.id === current) ?? {
|
||||
id: 'default',
|
||||
type: 'local',
|
||||
name: '空白订阅'
|
||||
name: t('sider.cards.emptyProfile')
|
||||
}
|
||||
|
||||
const extra = info?.extra
|
||||
@ -56,7 +58,7 @@ const ProfileCard: React.FC<Props> = (props) => {
|
||||
if (iconOnly) {
|
||||
return (
|
||||
<div className={`${profileCardStatus} flex justify-center`}>
|
||||
<Tooltip content="订阅管理" placement="right">
|
||||
<Tooltip content={t('sider.cards.profiles')} placement="right">
|
||||
<Button
|
||||
size="sm"
|
||||
isIconOnly
|
||||
@ -109,7 +111,7 @@ const ProfileCard: React.FC<Props> = (props) => {
|
||||
<Button
|
||||
isIconOnly
|
||||
size="sm"
|
||||
title="查看当前运行时配置"
|
||||
title={t('sider.cards.viewRuntimeConfig')}
|
||||
variant="light"
|
||||
color="default"
|
||||
onPress={() => {
|
||||
@ -156,7 +158,7 @@ const ProfileCard: React.FC<Props> = (props) => {
|
||||
await patchAppConfig({ profileDisplayDate: 'update' })
|
||||
}}
|
||||
>
|
||||
{extra.expire ? dayjs.unix(extra.expire).format('YYYY-MM-DD') : '长期有效'}
|
||||
{extra.expire ? dayjs.unix(extra.expire).format('YYYY-MM-DD') : t('sider.cards.neverExpire')}
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
@ -183,7 +185,7 @@ const ProfileCard: React.FC<Props> = (props) => {
|
||||
variant="bordered"
|
||||
className={`${match ? 'text-primary-foreground border-primary-foreground' : 'border-primary text-primary'}`}
|
||||
>
|
||||
远程
|
||||
{t('sider.cards.remote')}
|
||||
</Chip>
|
||||
<small>{dayjs(info.updated).fromNow()}</small>
|
||||
</div>
|
||||
@ -197,14 +199,14 @@ const ProfileCard: React.FC<Props> = (props) => {
|
||||
variant="bordered"
|
||||
className={`${match ? 'text-primary-foreground border-primary-foreground' : 'border-primary text-primary'}`}
|
||||
>
|
||||
本地
|
||||
{t('sider.cards.local')}
|
||||
</Chip>
|
||||
</div>
|
||||
)}
|
||||
{extra && (
|
||||
<Progress
|
||||
className="w-full"
|
||||
aria-label="流量使用进度"
|
||||
aria-label={t('sider.cards.trafficUsage')}
|
||||
classNames={{ indicator: match ? 'bg-primary-foreground' : 'bg-foreground' }}
|
||||
value={calcPercent(extra?.upload, extra?.download, extra?.total)}
|
||||
/>
|
||||
@ -238,7 +240,7 @@ const ProfileCard: React.FC<Props> = (props) => {
|
||||
<h3
|
||||
className={`text-md font-bold ${match ? 'text-primary-foreground' : 'text-foreground'}`}
|
||||
>
|
||||
订阅管理
|
||||
{t('sider.cards.profiles')}
|
||||
</h3>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
|
||||
@ -6,12 +6,14 @@ import { useLocation, useNavigate } from 'react-router-dom'
|
||||
import { useGroups } from '@renderer/hooks/use-groups'
|
||||
import { useAppConfig } from '@renderer/hooks/use-app-config'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
interface Props {
|
||||
iconOnly?: boolean
|
||||
}
|
||||
|
||||
const ProxyCard: React.FC<Props> = (props) => {
|
||||
const { t } = useTranslation()
|
||||
const { appConfig } = useAppConfig()
|
||||
const { iconOnly } = props
|
||||
const { proxyCardStatus = 'col-span-1' } = appConfig || {}
|
||||
@ -34,7 +36,7 @@ const ProxyCard: React.FC<Props> = (props) => {
|
||||
if (iconOnly) {
|
||||
return (
|
||||
<div className={`${proxyCardStatus} flex justify-center`}>
|
||||
<Tooltip content="代理组" placement="right">
|
||||
<Tooltip content={t('proxies.card.title')} placement="right">
|
||||
<Button
|
||||
size="sm"
|
||||
isIconOnly
|
||||
@ -103,7 +105,7 @@ const ProxyCard: React.FC<Props> = (props) => {
|
||||
<h3
|
||||
className={`text-md font-bold ${match ? 'text-primary-foreground' : 'text-foreground'}`}
|
||||
>
|
||||
代理组
|
||||
{t('proxies.card.title')}
|
||||
</h3>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
|
||||
@ -5,12 +5,14 @@ import { useSortable } from '@dnd-kit/sortable'
|
||||
import { CSS } from '@dnd-kit/utilities'
|
||||
import { IoLayersOutline } from 'react-icons/io5'
|
||||
import { useAppConfig } from '@renderer/hooks/use-app-config'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
interface Props {
|
||||
iconOnly?: boolean
|
||||
}
|
||||
|
||||
const ResourceCard: React.FC<Props> = (props) => {
|
||||
const { t } = useTranslation()
|
||||
const { appConfig } = useAppConfig()
|
||||
const { iconOnly } = props
|
||||
const { resourceCardStatus = 'col-span-1' } = appConfig || {}
|
||||
@ -32,7 +34,7 @@ const ResourceCard: React.FC<Props> = (props) => {
|
||||
if (iconOnly) {
|
||||
return (
|
||||
<div className={`${resourceCardStatus} flex justify-center`}>
|
||||
<Tooltip content="外部资源" placement="right">
|
||||
<Tooltip content={t('sider.cards.resources')} placement="right">
|
||||
<Button
|
||||
size="sm"
|
||||
isIconOnly
|
||||
@ -84,7 +86,7 @@ const ResourceCard: React.FC<Props> = (props) => {
|
||||
<h3
|
||||
className={`text-md font-bold ${match ? 'text-primary-foreground' : 'text-foreground'}`}
|
||||
>
|
||||
外部资源
|
||||
{t('sider.cards.resources')}
|
||||
</h3>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
|
||||
@ -6,12 +6,14 @@ import { CSS } from '@dnd-kit/utilities'
|
||||
import { useRules } from '@renderer/hooks/use-rules'
|
||||
import { useAppConfig } from '@renderer/hooks/use-app-config'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
interface Props {
|
||||
iconOnly?: boolean
|
||||
}
|
||||
|
||||
const RuleCard: React.FC<Props> = (props) => {
|
||||
const { t } = useTranslation()
|
||||
const { appConfig } = useAppConfig()
|
||||
const { iconOnly } = props
|
||||
const { ruleCardStatus = 'col-span-1' } = appConfig || {}
|
||||
@ -34,7 +36,7 @@ const RuleCard: React.FC<Props> = (props) => {
|
||||
if (iconOnly) {
|
||||
return (
|
||||
<div className={`${ruleCardStatus} flex justify-center`}>
|
||||
<Tooltip content="规则" placement="right">
|
||||
<Tooltip content={t('sider.cards.rules')} placement="right">
|
||||
<Button
|
||||
size="sm"
|
||||
isIconOnly
|
||||
@ -104,7 +106,7 @@ const RuleCard: React.FC<Props> = (props) => {
|
||||
<h3
|
||||
className={`text-md font-bold ${match ? 'text-primary-foreground' : 'text-foreground'}`}
|
||||
>
|
||||
规则
|
||||
{t('sider.cards.rules')}
|
||||
</h3>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
|
||||
@ -8,11 +8,13 @@ import { useSortable } from '@dnd-kit/sortable'
|
||||
import { CSS } from '@dnd-kit/utilities'
|
||||
import { useAppConfig } from '@renderer/hooks/use-app-config'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
interface Props {
|
||||
iconOnly?: boolean
|
||||
}
|
||||
const SniffCard: React.FC<Props> = (props) => {
|
||||
const { t } = useTranslation()
|
||||
const { appConfig } = useAppConfig()
|
||||
const { iconOnly } = props
|
||||
const { sniffCardStatus = 'col-span-1', controlSniff = true } = appConfig || {}
|
||||
@ -41,7 +43,7 @@ const SniffCard: React.FC<Props> = (props) => {
|
||||
if (iconOnly) {
|
||||
return (
|
||||
<div className={`${sniffCardStatus} ${!controlSniff ? 'hidden' : ''} flex justify-center`}>
|
||||
<Tooltip content="域名嗅探" placement="right">
|
||||
<Tooltip content={t('sider.cards.sniff')} placement="right">
|
||||
<Button
|
||||
size="sm"
|
||||
isIconOnly
|
||||
@ -99,7 +101,7 @@ const SniffCard: React.FC<Props> = (props) => {
|
||||
<h3
|
||||
className={`text-md font-bold ${match ? 'text-primary-foreground' : 'text-foreground'}`}
|
||||
>
|
||||
域名嗅探
|
||||
{t('sider.cards.sniff')}
|
||||
</h3>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
|
||||
@ -5,12 +5,14 @@ import { CSS } from '@dnd-kit/utilities'
|
||||
import SubStoreIcon from '../base/substore-icon'
|
||||
import { useAppConfig } from '@renderer/hooks/use-app-config'
|
||||
import React from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
interface Props {
|
||||
iconOnly?: boolean
|
||||
}
|
||||
|
||||
const SubStoreCard: React.FC<Props> = (props) => {
|
||||
const { t } = useTranslation()
|
||||
const { appConfig } = useAppConfig()
|
||||
const { iconOnly } = props
|
||||
const { substoreCardStatus = 'col-span-1', useSubStore = true } = appConfig || {}
|
||||
@ -32,7 +34,7 @@ const SubStoreCard: React.FC<Props> = (props) => {
|
||||
if (iconOnly) {
|
||||
return (
|
||||
<div className={`${substoreCardStatus} ${!useSubStore ? 'hidden' : ''} flex justify-center`}>
|
||||
<Tooltip content="Sub-Store" placement="right">
|
||||
<Tooltip content={t('sider.cards.substore')} placement="right">
|
||||
<Button
|
||||
size="sm"
|
||||
isIconOnly
|
||||
@ -84,7 +86,7 @@ const SubStoreCard: React.FC<Props> = (props) => {
|
||||
<h3
|
||||
className={`text-md font-bold ${match ? 'text-primary-foreground' : 'text-foreground'}`}
|
||||
>
|
||||
Sub-Store
|
||||
{t('sider.cards.substore')}
|
||||
</h3>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
|
||||
@ -7,12 +7,14 @@ import { AiOutlineGlobal } from 'react-icons/ai'
|
||||
import React from 'react'
|
||||
import { useSortable } from '@dnd-kit/sortable'
|
||||
import { CSS } from '@dnd-kit/utilities'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
interface Props {
|
||||
iconOnly?: boolean
|
||||
}
|
||||
|
||||
const SysproxySwitcher: React.FC<Props> = (props) => {
|
||||
const { t } = useTranslation()
|
||||
const { iconOnly } = props
|
||||
const location = useLocation()
|
||||
const navigate = useNavigate()
|
||||
@ -45,7 +47,7 @@ const SysproxySwitcher: React.FC<Props> = (props) => {
|
||||
if (iconOnly) {
|
||||
return (
|
||||
<div className={`${sysproxyCardStatus} flex justify-center`}>
|
||||
<Tooltip content="系统代理" placement="right">
|
||||
<Tooltip content={t('sider.cards.systemProxy')} placement="right">
|
||||
<Button
|
||||
size="sm"
|
||||
isIconOnly
|
||||
@ -102,7 +104,7 @@ const SysproxySwitcher: React.FC<Props> = (props) => {
|
||||
<h3
|
||||
className={`text-md font-bold ${match ? 'text-primary-foreground' : 'text-foreground'}`}
|
||||
>
|
||||
系统代理
|
||||
{t('sider.cards.systemProxy')}
|
||||
</h3>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
|
||||
@ -8,12 +8,14 @@ import { useSortable } from '@dnd-kit/sortable'
|
||||
import { CSS } from '@dnd-kit/utilities'
|
||||
import React from 'react'
|
||||
import { useAppConfig } from '@renderer/hooks/use-app-config'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
interface Props {
|
||||
iconOnly?: boolean
|
||||
}
|
||||
|
||||
const TunSwitcher: React.FC<Props> = (props) => {
|
||||
const { t } = useTranslation()
|
||||
const { iconOnly } = props
|
||||
const location = useLocation()
|
||||
const navigate = useNavigate()
|
||||
@ -48,7 +50,7 @@ const TunSwitcher: React.FC<Props> = (props) => {
|
||||
if (iconOnly) {
|
||||
return (
|
||||
<div className={`${tunCardStatus} flex justify-center`}>
|
||||
<Tooltip content="虚拟网卡" placement="right">
|
||||
<Tooltip content={t('sider.cards.tun')} placement="right">
|
||||
<Button
|
||||
size="sm"
|
||||
isIconOnly
|
||||
@ -105,7 +107,7 @@ const TunSwitcher: React.FC<Props> = (props) => {
|
||||
<h3
|
||||
className={`text-md font-bold ${match ? 'text-primary-foreground' : 'text-foreground'}`}
|
||||
>
|
||||
虚拟网卡
|
||||
{t('sider.cards.tun')}
|
||||
</h3>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
|
||||
@ -1,14 +1,18 @@
|
||||
import { Modal, ModalContent, ModalHeader, ModalBody, ModalFooter, Button } from '@nextui-org/react'
|
||||
import { BaseEditor } from '@renderer/components/base/base-editor'
|
||||
import React, { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
interface Props {
|
||||
script: string
|
||||
onCancel: () => void
|
||||
onConfirm: (script: string) => void
|
||||
}
|
||||
|
||||
const PacEditorModal: React.FC<Props> = (props) => {
|
||||
const { script, onCancel, onConfirm } = props
|
||||
const [currData, setCurrData] = useState(script)
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<Modal
|
||||
@ -21,7 +25,7 @@ const PacEditorModal: React.FC<Props> = (props) => {
|
||||
scrollBehavior="inside"
|
||||
>
|
||||
<ModalContent className="h-full w-[calc(100%-100px)]">
|
||||
<ModalHeader className="flex pb-0 app-drag">编辑PAC脚本</ModalHeader>
|
||||
<ModalHeader className="flex pb-0 app-drag">{t('sysproxy.pacEditor.title')}</ModalHeader>
|
||||
<ModalBody className="h-full">
|
||||
<BaseEditor
|
||||
language="javascript"
|
||||
@ -31,10 +35,10 @@ const PacEditorModal: React.FC<Props> = (props) => {
|
||||
</ModalBody>
|
||||
<ModalFooter className="pt-0">
|
||||
<Button size="sm" variant="light" onPress={onCancel}>
|
||||
取消
|
||||
{t('common.cancel')}
|
||||
</Button>
|
||||
<Button size="sm" color="primary" onPress={() => onConfirm(currData)}>
|
||||
确认
|
||||
{t('common.confirm')}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
|
||||
@ -10,15 +10,19 @@ import {
|
||||
import ReactMarkdown from 'react-markdown'
|
||||
import React, { useState } from 'react'
|
||||
import { downloadAndInstallUpdate } from '@renderer/utils/ipc'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
interface Props {
|
||||
version: string
|
||||
changelog: string
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
const UpdaterModal: React.FC<Props> = (props) => {
|
||||
const { version, changelog, onClose } = props
|
||||
const [downloading, setDownloading] = useState(false)
|
||||
const { t } = useTranslation()
|
||||
|
||||
const onUpdate = async (): Promise<void> => {
|
||||
try {
|
||||
await downloadAndInstallUpdate(version)
|
||||
@ -38,7 +42,7 @@ const UpdaterModal: React.FC<Props> = (props) => {
|
||||
>
|
||||
<ModalContent className="h-full w-[calc(100%-100px)]">
|
||||
<ModalHeader className="flex justify-between app-drag">
|
||||
<div>v{version} 版本就绪</div>
|
||||
<div>{t('common.updater.versionReady', { version })}</div>
|
||||
<Button
|
||||
color="primary"
|
||||
size="sm"
|
||||
@ -47,7 +51,7 @@ const UpdaterModal: React.FC<Props> = (props) => {
|
||||
open(`https://github.com/mihomo-party-org/mihomo-party/releases/tag/v${version}`)
|
||||
}}
|
||||
>
|
||||
前往下载
|
||||
{t('common.updater.goToDownload')}
|
||||
</Button>
|
||||
</ModalHeader>
|
||||
<ModalBody className="h-full">
|
||||
@ -65,7 +69,7 @@ const UpdaterModal: React.FC<Props> = (props) => {
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button size="sm" variant="light" onPress={onClose}>
|
||||
取消
|
||||
{t('common.cancel')}
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
@ -83,7 +87,7 @@ const UpdaterModal: React.FC<Props> = (props) => {
|
||||
}
|
||||
}}
|
||||
>
|
||||
更新
|
||||
{t('common.updater.update')}
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
|
||||
18
src/renderer/src/i18n.ts
Normal file
18
src/renderer/src/i18n.ts
Normal file
@ -0,0 +1,18 @@
|
||||
import { initReactI18next } from 'react-i18next'
|
||||
import i18n, { initI18n } from '../../shared/i18n'
|
||||
import { getAppConfig } from './utils/ipc'
|
||||
|
||||
// 初始化 React i18next
|
||||
i18n.use(initReactI18next)
|
||||
|
||||
// 从配置中读取语言设置并初始化
|
||||
getAppConfig().then((config) => {
|
||||
initI18n({ lng: config.language })
|
||||
})
|
||||
|
||||
// 通知主进程语言变更
|
||||
i18n.on('languageChanged', (lng) => {
|
||||
window.electron.ipcRenderer.invoke('changeLanguage', lng)
|
||||
})
|
||||
|
||||
export default i18n
|
||||
723
src/renderer/src/locales/en-US.json
Normal file
723
src/renderer/src/locales/en-US.json
Normal file
@ -0,0 +1,723 @@
|
||||
{
|
||||
"common": {
|
||||
"settings": "Settings",
|
||||
"profiles": "Profiles",
|
||||
"proxies": "Proxies",
|
||||
"connections": "Connections",
|
||||
"dns": "DNS",
|
||||
"tun": "TUN",
|
||||
"save": "Save",
|
||||
"cancel": "Cancel",
|
||||
"edit": "Edit",
|
||||
"delete": "Delete",
|
||||
"seconds": "seconds",
|
||||
"confirm": "Confirm",
|
||||
"auto": "Auto",
|
||||
"default": "Default",
|
||||
"close": "Close",
|
||||
"pinWindow": "Pin Window",
|
||||
"enterRootPassword": "Please enter root password",
|
||||
"next": "Next",
|
||||
"prev": "Previous",
|
||||
"done": "Done",
|
||||
"notification": {
|
||||
"restartRequired": "Restart required for changes to take effect"
|
||||
},
|
||||
"error": {
|
||||
"appCrash": "Application crashed :( Please submit the following information to the developer to troubleshoot",
|
||||
"copyErrorMessage": "Copy Error Message",
|
||||
"invalidCron": "Invalid Cron expression",
|
||||
"getBackupListFailed": "Failed to get backup list: {{error}}",
|
||||
"restoreFailed": "Failed to restore: {{error}}",
|
||||
"deleteFailed": "Failed to delete: {{error}}",
|
||||
"shortcutRegistrationFailed": "Failed to register shortcut",
|
||||
"shortcutRegistrationFailedWithError": "Failed to register shortcut: {{error}}"
|
||||
},
|
||||
"updater": {
|
||||
"versionReady": "v{{version}} Version Ready",
|
||||
"goToDownload": "Go to Download",
|
||||
"update": "Update"
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"general": "General Settings",
|
||||
"mihomo": "Mihomo Settings",
|
||||
"language": "Language",
|
||||
"theme": "Theme",
|
||||
"darkMode": "Dark Mode",
|
||||
"lightMode": "Light Mode",
|
||||
"autoStart": "Auto Start",
|
||||
"autoCheckUpdate": "Auto Check Update",
|
||||
"silentStart": "Silent Start",
|
||||
"autoQuitWithoutCore": "Auto Enable Light Mode",
|
||||
"autoQuitWithoutCoreTooltip": "Automatically enter light mode after closing window for specified time",
|
||||
"autoQuitWithoutCoreDelay": "Light Mode Auto Enable Delay",
|
||||
"envType": "Environment Variable Type",
|
||||
"showFloatingWindow": "Show Floating Window",
|
||||
"spinFloatingIcon": "Spin Floating Icon Based on Network Speed",
|
||||
"disableTray": "Disable Tray Icon",
|
||||
"proxyInTray": "Show Proxy Info in Tray Menu",
|
||||
"showTraffic_windows": "Show Network Speed in Taskbar",
|
||||
"showTraffic_mac": "Show Network Speed in Status Bar",
|
||||
"showDockIcon": "Show Dock Icon",
|
||||
"useWindowFrame": "Use System Title Bar",
|
||||
"backgroundColor": "Background Color",
|
||||
"backgroundAuto": "Auto",
|
||||
"backgroundDark": "Dark",
|
||||
"backgroundLight": "Light",
|
||||
"fetchTheme": "Fetch Theme",
|
||||
"importTheme": "Import Theme",
|
||||
"editTheme": "Edit Theme",
|
||||
"selectTheme": "Select Theme",
|
||||
"links": {
|
||||
"docs": "Documentation",
|
||||
"github": "GitHub Repository",
|
||||
"telegram": "Telegram Group"
|
||||
},
|
||||
"title": "Application Settings"
|
||||
},
|
||||
"mihomo": {
|
||||
"userAgent": "Subscription User Agent",
|
||||
"userAgentPlaceholder": "Default: clash.meta",
|
||||
"delayTest": {
|
||||
"url": "Delay Test URL",
|
||||
"urlPlaceholder": "Default: https://www.gstatic.com/generate_204",
|
||||
"concurrency": "Delay Test Concurrency",
|
||||
"concurrencyPlaceholder": "Default: 50",
|
||||
"timeout": "Delay Test Timeout",
|
||||
"timeoutPlaceholder": "Default: 5000"
|
||||
},
|
||||
"gist": {
|
||||
"title": "Sync Runtime Config to Gist",
|
||||
"copyUrl": "Copy Gist URL",
|
||||
"token": "GitHub Token"
|
||||
},
|
||||
"proxyColumns": {
|
||||
"title": "Proxy Display Columns",
|
||||
"auto": "Auto",
|
||||
"one": "One Column",
|
||||
"two": "Two Columns",
|
||||
"three": "Three Columns",
|
||||
"four": "Four Columns"
|
||||
},
|
||||
"cpuPriority": {
|
||||
"title": "Core Process Priority",
|
||||
"realtime": "Realtime",
|
||||
"high": "High",
|
||||
"aboveNormal": "Above Normal",
|
||||
"normal": "Normal",
|
||||
"belowNormal": "Below Normal",
|
||||
"low": "Low"
|
||||
},
|
||||
"workDir": {
|
||||
"title": "Separate Work Directory for Different Subscriptions",
|
||||
"tooltip": "Enable to avoid conflicts when different subscriptions have proxy groups with the same name"
|
||||
},
|
||||
"controlDns": "Control DNS Settings",
|
||||
"controlSniff": "Control Domain Sniffing",
|
||||
"autoCloseConnection": "Auto Close Connection",
|
||||
"pauseSSID": {
|
||||
"title": "Direct Connection for Specific WiFi SSIDs",
|
||||
"placeholder": "Enter SSID"
|
||||
},
|
||||
"title": "Core Settings",
|
||||
"restart": "Restart Core",
|
||||
"memory": "Memory Usage",
|
||||
"coreVersion": "Core Version",
|
||||
"upgradeCore": "Upgrade Core",
|
||||
"CoreAuthLost": "Core Authorization Lost",
|
||||
"coreUpgradeSuccess": "Core upgrade successful. If you want to use virtual network card (Tun), please go to the virtual network card page to manually authorize the Core again",
|
||||
"alreadyLatestVersion": "Already Latest Version",
|
||||
"selectCoreVersion": "Select Core Version",
|
||||
"stableVersion": "Stable Version",
|
||||
"alphaVersion": "Alpha Version",
|
||||
"mixedPort": "Mixed Port",
|
||||
"confirm": "Confirm",
|
||||
"socksPort": "Socks Port",
|
||||
"httpPort": "Http Port",
|
||||
"redirPort": "Redir Port",
|
||||
"externalController": "External Controller Address",
|
||||
"externalControllerSecret": "External Controller Secret",
|
||||
"ipv6": "IPv6",
|
||||
"allowLanConnection": "Allow LAN Connection",
|
||||
"allowedIpSegments": "Allowed IP Segments",
|
||||
"disallowedIpSegments": "Disallowed IP Segments",
|
||||
"userVerification": "User Verification",
|
||||
"skipAuthPrefixes": "Skip Auth IP Segments",
|
||||
"useRttDelayTest": "Use RTT Delay Test",
|
||||
"tcpConcurrent": "TCP Concurrent",
|
||||
"storeSelectedNode": "Store Selected Node",
|
||||
"storeFakeIp": "Store Fake IP",
|
||||
"logRetentionDays": "Log Retention Days",
|
||||
"logLevel": "Log Level",
|
||||
"selectLogLevel": "Select Log Level",
|
||||
"silent": "Silent",
|
||||
"error": "Error",
|
||||
"warning": "Warning",
|
||||
"info": "Info",
|
||||
"debug": "Debug",
|
||||
"findProcess": "Find Process",
|
||||
"selectFindProcessMode": "Select Process Find Mode",
|
||||
"strict": "Auto",
|
||||
"off": "Off",
|
||||
"always": "Always",
|
||||
"username": {
|
||||
"placeholder": "Username"
|
||||
},
|
||||
"password": {
|
||||
"placeholder": "Password"
|
||||
},
|
||||
"ipSegment": {
|
||||
"placeholder": "IP Segment"
|
||||
},
|
||||
"interface": {
|
||||
"title": "Network Information"
|
||||
}
|
||||
},
|
||||
"substore": {
|
||||
"title": "Sub-Store",
|
||||
"openInBrowser": "Open in Browser",
|
||||
"enable": "Enable Sub-Store",
|
||||
"allowLan": "Allow LAN Access",
|
||||
"useCustomBackend": "Use Custom Sub-Store Backend",
|
||||
"customBackendUrl": {
|
||||
"title": "Custom Sub-Store Backend URL",
|
||||
"placeholder": "Must include protocol"
|
||||
},
|
||||
"useProxy": "Enable Proxy for All Sub-Store Requests",
|
||||
"sync": {
|
||||
"title": "Schedule Subscription/File Sync",
|
||||
"placeholder": "Cron expression"
|
||||
},
|
||||
"restore": {
|
||||
"title": "Schedule Config Restore",
|
||||
"placeholder": "Cron expression"
|
||||
},
|
||||
"backup": {
|
||||
"title": "Schedule Config Backup",
|
||||
"placeholder": "Cron expression"
|
||||
}
|
||||
},
|
||||
"webdav": {
|
||||
"title": "WebDAV Backup",
|
||||
"url": "WebDAV URL",
|
||||
"dir": "WebDAV Backup Directory",
|
||||
"username": "WebDAV Username",
|
||||
"password": "WebDAV Password",
|
||||
"backup": "Backup",
|
||||
"restore": {
|
||||
"title": "Restore Backup",
|
||||
"noBackups": "No backups available"
|
||||
},
|
||||
"notification": {
|
||||
"backupSuccess": {
|
||||
"title": "Backup Successful",
|
||||
"body": "Backup file has been uploaded to WebDAV"
|
||||
}
|
||||
}
|
||||
},
|
||||
"shortcuts": {
|
||||
"title": "Keyboard Shortcuts",
|
||||
"toggleWindow": "Toggle Window",
|
||||
"toggleFloatingWindow": "Toggle Floating Window",
|
||||
"toggleSystemProxy": "Toggle System Proxy",
|
||||
"toggleTun": "Toggle TUN",
|
||||
"toggleRuleMode": "Toggle Rule Mode",
|
||||
"toggleGlobalMode": "Toggle Global Mode",
|
||||
"toggleDirectMode": "Toggle Direct Mode",
|
||||
"toggleLightMode": "Toggle Light Mode",
|
||||
"restartApp": "Restart App",
|
||||
"input": {
|
||||
"placeholder": "Click to input shortcut"
|
||||
}
|
||||
},
|
||||
"sider": {
|
||||
"title": "Sidebar Settings",
|
||||
"cards": {
|
||||
"systemProxy": "Sys Proxy",
|
||||
"tun": "TUN",
|
||||
"profiles": "Profiles",
|
||||
"proxies": "Proxy Groups",
|
||||
"rules": "Rules",
|
||||
"resources": "Ext. Res.",
|
||||
"override": "Override",
|
||||
"connections": "Connections",
|
||||
"core": "Core Settings",
|
||||
"dns": "DNS",
|
||||
"sniff": "Sniffing",
|
||||
"logs": "Logs",
|
||||
"substore": "Sub-Store",
|
||||
"config": "Runtime Config",
|
||||
"emptyProfile": "Empty Profile",
|
||||
"viewRuntimeConfig": "View Runtime Config",
|
||||
"remote": "Remote",
|
||||
"local": "Local",
|
||||
"trafficUsage": "Traffic Usage Progress",
|
||||
"neverExpire": "Never Expire",
|
||||
"outbound": {
|
||||
"title": "Outbound Mode",
|
||||
"rule": "Rule",
|
||||
"global": "Global",
|
||||
"direct": "Direct"
|
||||
}
|
||||
},
|
||||
"size": {
|
||||
"large": "Large",
|
||||
"small": "Small",
|
||||
"hidden": "Hidden"
|
||||
}
|
||||
},
|
||||
"actions": {
|
||||
"guide": {
|
||||
"title": "Open Guide",
|
||||
"button": "Open Guide"
|
||||
},
|
||||
"update": {
|
||||
"title": "Check for Updates",
|
||||
"button": "Check for Updates",
|
||||
"upToDate": {
|
||||
"title": "Up to Date",
|
||||
"body": "No updates available"
|
||||
}
|
||||
},
|
||||
"reset": {
|
||||
"title": "Reset App",
|
||||
"button": "Reset App",
|
||||
"tooltip": "Delete all configurations and restore the app to its initial state"
|
||||
},
|
||||
"heapSnapshot": {
|
||||
"title": "Create Heap Snapshot",
|
||||
"button": "Create Heap Snapshot",
|
||||
"tooltip": "Create a heap snapshot of the main process for memory issue debugging"
|
||||
},
|
||||
"lightMode": {
|
||||
"title": "Light Mode",
|
||||
"button": "Light Mode",
|
||||
"tooltip": "Completely exit the app while keeping only the core process"
|
||||
},
|
||||
"restartApp": "Restart App",
|
||||
"quit": {
|
||||
"title": "Quit App",
|
||||
"button": "Quit App"
|
||||
},
|
||||
"version": {
|
||||
"title": "App Version"
|
||||
}
|
||||
},
|
||||
"theme": {
|
||||
"editor": {
|
||||
"title": "Edit Theme"
|
||||
}
|
||||
},
|
||||
"proxies": {
|
||||
"card": {
|
||||
"title": "ProxyGrp"
|
||||
},
|
||||
"delay": {
|
||||
"test": "Test",
|
||||
"timeout": "Timeout"
|
||||
},
|
||||
"unpin": "Unpin",
|
||||
"order": {
|
||||
"default": "Default",
|
||||
"delay": "Delay",
|
||||
"name": "Name"
|
||||
},
|
||||
"mode": {
|
||||
"full": "Detailed Info",
|
||||
"simple": "Simple Info",
|
||||
"direct": "Direct Mode"
|
||||
},
|
||||
"search": {
|
||||
"placeholder": "Search Proxies"
|
||||
},
|
||||
"locate": "Locate Current Proxy"
|
||||
},
|
||||
"sniffer": {
|
||||
"title": "Domain Sniffing Settings",
|
||||
"parsePureIP": "Sniff Unmapped IP Addresses",
|
||||
"forceDNSMapping": "Sniff Real IP Mappings",
|
||||
"overrideDestination": "Override Connection Address",
|
||||
"sniff": {
|
||||
"title": "HTTP Port Sniffing",
|
||||
"tls": "TLS Port Sniffing",
|
||||
"quic": "QUIC Port Sniffing",
|
||||
"ports": {
|
||||
"placeholder": "Port numbers, separated by commas"
|
||||
}
|
||||
},
|
||||
"skipDomain": {
|
||||
"title": "Skip Domain Sniffing",
|
||||
"placeholder": "Example: +.push.apple.com"
|
||||
},
|
||||
"forceDomain": {
|
||||
"title": "Force Domain Sniffing",
|
||||
"placeholder": "Example: v2ex.com"
|
||||
},
|
||||
"skipDstAddress": {
|
||||
"title": "Skip Destination Address Sniffing",
|
||||
"placeholder": "Example: 1.1.1.1/32"
|
||||
},
|
||||
"skipSrcAddress": {
|
||||
"title": "Skip Source Address Sniffing",
|
||||
"placeholder": "Example: 192.168.1.1/24"
|
||||
}
|
||||
},
|
||||
"sysproxy": {
|
||||
"title": "System Proxy",
|
||||
"host": {
|
||||
"title": "Proxy Host",
|
||||
"placeholder": "Default 127.0.0.1, do not modify unless necessary"
|
||||
},
|
||||
"mode": {
|
||||
"title": "Proxy Mode",
|
||||
"manual": "Manual",
|
||||
"pac": "PAC"
|
||||
},
|
||||
"uwp": {
|
||||
"title": "UWP Tool",
|
||||
"open": "Open UWP Tool"
|
||||
},
|
||||
"pac": {
|
||||
"edit": "Edit PAC Script"
|
||||
},
|
||||
"bypass": {
|
||||
"title": "Proxy Bypass",
|
||||
"addDefault": "Add Default Bypass",
|
||||
"placeholder": "Example: *.baidu.com"
|
||||
}
|
||||
},
|
||||
"tun": {
|
||||
"title": "TUN",
|
||||
"firewall": {
|
||||
"title": "Reset Firewall",
|
||||
"reset": "Reset Firewall"
|
||||
},
|
||||
"core": {
|
||||
"title": "Manual Authorization",
|
||||
"auth": "Authorize Core"
|
||||
},
|
||||
"dns": {
|
||||
"autoSet": "Auto Set System DNS"
|
||||
},
|
||||
"stack": {
|
||||
"title": "Tun Mode Stack"
|
||||
},
|
||||
"device": {
|
||||
"title": "Tun Device Name"
|
||||
},
|
||||
"strictRoute": "Strict Route",
|
||||
"autoRoute": "Auto Set Global Route",
|
||||
"autoRedirect": "Auto Set TCP Redirect",
|
||||
"autoDetectInterface": "Auto Detect Interface",
|
||||
"dnsHijack": "DNS Hijack",
|
||||
"excludeAddress": {
|
||||
"title": "Exclude Custom Networks",
|
||||
"placeholder": "Example: 172.20.0.0/16"
|
||||
},
|
||||
"notifications": {
|
||||
"coreAuthSuccess": "Core Authorization Successful",
|
||||
"firewallResetSuccess": "Firewall Reset Successful"
|
||||
}
|
||||
},
|
||||
"dns": {
|
||||
"title": "DNS Settings",
|
||||
"enhancedMode": {
|
||||
"title": "Domain Mapping Mode",
|
||||
"fakeIp": "Fake IP",
|
||||
"redirHost": "Real IP",
|
||||
"normal": "No Mapping"
|
||||
},
|
||||
"fakeIp": {
|
||||
"range": "Response Range",
|
||||
"rangePlaceholder": "Example: 198.18.0.1/16",
|
||||
"filter": "Real IP Response",
|
||||
"filterPlaceholder": "Example: +.lan"
|
||||
},
|
||||
"respectRules": "Respect Rules",
|
||||
"defaultNameserver": "DNS Server Domain Resolution",
|
||||
"defaultNameserverPlaceholder": "Example: 223.5.5.5, IP only",
|
||||
"proxyServerNameserver": "Proxy Server Domain Resolution",
|
||||
"proxyServerNameserverPlaceholder": "Example: tls://dns.alidns.com",
|
||||
"nameserver": "Default Resolution Server",
|
||||
"nameserverPlaceholder": "Example: tls://dns.alidns.com",
|
||||
"directNameserver": "Direct Resolution Server",
|
||||
"directNameserverPlaceholder": "Example: tls://dns.alidns.com",
|
||||
"nameserverPolicy": {
|
||||
"title": "Override DNS Policy",
|
||||
"list": "DNS Policy List",
|
||||
"domainPlaceholder": "Domain",
|
||||
"serverPlaceholder": "DNS Server"
|
||||
},
|
||||
"systemHosts": {
|
||||
"title": "Use System Hosts"
|
||||
},
|
||||
"customHosts": {
|
||||
"title": "Custom Hosts",
|
||||
"list": "Hosts List",
|
||||
"domainPlaceholder": "Domain",
|
||||
"valuePlaceholder": "Domain or IP"
|
||||
}
|
||||
},
|
||||
"profiles": {
|
||||
"title": "Profile Management",
|
||||
"updateAll": "Update All Profiles",
|
||||
"useProxy": "Proxy",
|
||||
"import": "Import",
|
||||
"open": "Open",
|
||||
"new": "New",
|
||||
"newProfile": "New Profile",
|
||||
"substore": {
|
||||
"visit": "Visit Sub-Store"
|
||||
},
|
||||
"error": {
|
||||
"unsupportedFileType": "Unsupported file type"
|
||||
},
|
||||
"emptyProfile": "Empty Profile",
|
||||
"viewRuntimeConfig": "View Current Runtime Config",
|
||||
"neverExpire": "Never Expire",
|
||||
"remote": "Remote",
|
||||
"local": "Local",
|
||||
"trafficUsage": "Traffic Usage Progress",
|
||||
"editInfo": {
|
||||
"title": "Edit Information",
|
||||
"name": "Name",
|
||||
"url": "Subscription URL",
|
||||
"useProxy": "Use Proxy to Update",
|
||||
"interval": "Upd. Interval (min)",
|
||||
"override": {
|
||||
"title": "Override",
|
||||
"global": "Global",
|
||||
"noAvailable": "No available overrides",
|
||||
"add": "Add Override"
|
||||
}
|
||||
},
|
||||
"editFile": {
|
||||
"title": "Edit Profile",
|
||||
"notice": "Note: Changes made here will be reset after profile update. For custom configurations, please use",
|
||||
"override": "Override",
|
||||
"feature": "feature"
|
||||
},
|
||||
"openFile": "Open File",
|
||||
"home": "Home",
|
||||
"traffic": {
|
||||
"usage": "{{used}}/{{total}}",
|
||||
"unlimited": "Unlimited",
|
||||
"expired": "Expired",
|
||||
"remainingDays": "{{days}} days",
|
||||
"lastUpdate": "Last updated: {{time}}"
|
||||
}
|
||||
},
|
||||
"outbound": {
|
||||
"title": "Outbound Mode",
|
||||
"modes": {
|
||||
"rule": "Rule",
|
||||
"global": "Global",
|
||||
"direct": "Direct"
|
||||
}
|
||||
},
|
||||
"rules": {
|
||||
"title": "Rules",
|
||||
"filter": "Filter Rules"
|
||||
},
|
||||
"override": {
|
||||
"title": "Override",
|
||||
"import": "Import",
|
||||
"docs": "Documentation",
|
||||
"repository": "Override Repository",
|
||||
"unsupportedFileType": "Unsupported file type",
|
||||
"actions": {
|
||||
"open": "Open",
|
||||
"newYaml": "New YAML",
|
||||
"newJs": "New JavaScript"
|
||||
},
|
||||
"defaultContent": {
|
||||
"yaml": "# https://mihomo.party/docs/guide/override/yaml",
|
||||
"js": "// https://mihomo.party/docs/guide/override/javascript\nfunction main(config) {\n return config\n}"
|
||||
},
|
||||
"newFile": {
|
||||
"yaml": "New YAML",
|
||||
"js": "New JS"
|
||||
},
|
||||
"editInfo": {
|
||||
"title": "Edit Information",
|
||||
"name": "Name",
|
||||
"url": "URL",
|
||||
"global": "Global Enable"
|
||||
},
|
||||
"editFile": {
|
||||
"title": "Edit Override {{type}}",
|
||||
"script": "Script",
|
||||
"config": "Config"
|
||||
},
|
||||
"execLog": {
|
||||
"title": "Execution Log",
|
||||
"close": "Close"
|
||||
},
|
||||
"menuItems": {
|
||||
"editInfo": "Edit Information",
|
||||
"editFile": "Edit File",
|
||||
"openFile": "Open File",
|
||||
"execLog": "Execution Log",
|
||||
"delete": "Delete"
|
||||
},
|
||||
"labels": {
|
||||
"global": "Global"
|
||||
}
|
||||
},
|
||||
"connections": {
|
||||
"title": "Connections",
|
||||
"upload": "Upload",
|
||||
"download": "Download",
|
||||
"closeAll": "Close All Connections",
|
||||
"active": "Active",
|
||||
"closed": "Closed",
|
||||
"filter": "Filter",
|
||||
"orderBy": "Order By",
|
||||
"uploadAmount": "Upload Amount",
|
||||
"downloadAmount": "Download Amount",
|
||||
"uploadSpeed": "Upload Speed",
|
||||
"downloadSpeed": "Download Speed",
|
||||
"detail": {
|
||||
"title": "Connection Details",
|
||||
"establishTime": "Establish Time",
|
||||
"rule": "Rule",
|
||||
"proxyChain": "Proxy Chain",
|
||||
"connectionType": "Connection Type",
|
||||
"host": "Host",
|
||||
"sniffHost": "Sniff Host",
|
||||
"processName": "Process Name",
|
||||
"processPath": "Process Path",
|
||||
"sourceIP": "Source IP",
|
||||
"sourceGeoIP": "Source GeoIP",
|
||||
"sourceASN": "Source ASN",
|
||||
"destinationIP": "Destination IP",
|
||||
"destinationGeoIP": "Destination GeoIP",
|
||||
"destinationASN": "Destination ASN",
|
||||
"sourcePort": "Source Port",
|
||||
"destinationPort": "Destination Port",
|
||||
"inboundIP": "Inbound IP",
|
||||
"inboundPort": "Inbound Port",
|
||||
"copyRule": "Copy Rule",
|
||||
"inboundName": "Inbound Name",
|
||||
"inboundUser": "Inbound User",
|
||||
"dscp": "DSCP",
|
||||
"remoteDestination": "Remote Destination",
|
||||
"dnsMode": "DNS Mode",
|
||||
"specialProxy": "Special Proxy",
|
||||
"specialRules": "Special Rules",
|
||||
"close": "Close"
|
||||
}
|
||||
},
|
||||
"resources": {
|
||||
"geoData": {
|
||||
"geoip": "GeoIP Database",
|
||||
"geosite": "GeoSite Database",
|
||||
"mmdb": "MMDB Database",
|
||||
"asn": "ASN Database",
|
||||
"mode": "GeoData Mode",
|
||||
"autoUpdate": "Auto Update",
|
||||
"updateInterval": "Update Interval (hours)",
|
||||
"updateSuccess": "GeoData Update Successful"
|
||||
}
|
||||
},
|
||||
"logs": {
|
||||
"title": "Real-time Logs",
|
||||
"filter": "Filter logs",
|
||||
"clear": "Clear logs",
|
||||
"autoScroll": "Auto scroll"
|
||||
},
|
||||
"tray": {
|
||||
"showWindow": "Show Window",
|
||||
"showFloatingWindow": "Show Floating Window",
|
||||
"hideFloatingWindow": "Hide Floating Window",
|
||||
"ruleMode": "Rule Mode",
|
||||
"globalMode": "Global Mode",
|
||||
"directMode": "Direct Mode",
|
||||
"systemProxy": "System Proxy",
|
||||
"tun": "TUN",
|
||||
"profiles": "Profiles",
|
||||
"openDirectories": {
|
||||
"title": "Open Directories",
|
||||
"appDir": "App Directory",
|
||||
"workDir": "Work Directory",
|
||||
"coreDir": "Core Directory",
|
||||
"logDir": "Log Directory"
|
||||
},
|
||||
"copyEnv": "Copy Environment Variables"
|
||||
},
|
||||
"guide": {
|
||||
"welcome": {
|
||||
"title": "Welcome to Mihomo Party",
|
||||
"description": "This is an interactive tutorial. If you are already familiar with the software, you can click the close button in the top right corner. You can always open this tutorial again from the settings."
|
||||
},
|
||||
"sider": {
|
||||
"title": "Navigation Bar",
|
||||
"description": "The left side is the application's navigation bar, which also serves as a dashboard. Here you can switch between different pages and get an overview of commonly used status information."
|
||||
},
|
||||
"card": {
|
||||
"title": "Cards",
|
||||
"description": "Click on the navigation bar cards to jump to the corresponding page. You can also drag and drop the cards to arrange them freely."
|
||||
},
|
||||
"main": {
|
||||
"title": "Main Area",
|
||||
"description": "The right side is the main area of the application, displaying the content of the selected page from the navigation bar."
|
||||
},
|
||||
"profile": {
|
||||
"title": "Profile Management",
|
||||
"description": "The profile management card shows information about the currently running subscription configuration. Click to enter the profile management page where you can manage your subscription configurations."
|
||||
},
|
||||
"import": {
|
||||
"title": "Import Subscription",
|
||||
"description": "Mihomo Party supports various subscription import methods. Enter your subscription link here and click import to import your subscription configuration. If your subscription requires a proxy to update, check the 'Proxy' option before importing (this requires having a working subscription first)."
|
||||
},
|
||||
"substore": {
|
||||
"title": "Sub-Store",
|
||||
"description": "Mihomo Party has deep integration with Sub-Store. You can click this button to enter Sub-Store or directly import your subscriptions managed through Sub-Store. Mihomo Party uses its built-in Sub-Store backend by default. If you have your own Sub-Store backend, you can configure it in the settings page. If you don't use Sub-Store, you can also disable it in the settings."
|
||||
},
|
||||
"localProfile": {
|
||||
"title": "Local Profile",
|
||||
"description": "Click '+' to import a local file or create a new empty configuration for editing."
|
||||
},
|
||||
"sysproxy": {
|
||||
"title": "System Proxy",
|
||||
"description": "After importing a subscription, the core is running and listening on the specified port. You can now use the proxy through the specified port. If you want most applications to automatically use this proxy port, you need to turn on the system proxy switch."
|
||||
},
|
||||
"sysproxySetting": {
|
||||
"title": "System Proxy Settings",
|
||||
"description": "Here you can configure system proxy-related settings and choose the proxy mode. For Windows applications that don't follow system proxy, you can use the 'UWP Tool' to remove local loopback restrictions. For the difference between 'Manual Proxy Mode' and 'PAC Proxy Mode', please search online."
|
||||
},
|
||||
"tun": {
|
||||
"title": "Virtual Network Card",
|
||||
"description": "Virtual network card, commonly known as 'Tun Mode' in similar software, allows you to let the core take control of all traffic for applications that don't follow system proxy settings."
|
||||
},
|
||||
"tunSetting": {
|
||||
"title": "Virtual Network Card Settings",
|
||||
"description": "Here you can modify virtual network card settings. Mihomo Party has theoretically solved all permission issues. If your virtual network card is still not working, try resetting the firewall (Windows) or manually authorizing the core (MacOS/Linux) and then restart the core."
|
||||
},
|
||||
"override": {
|
||||
"title": "Override",
|
||||
"description": "Mihomo Party provides powerful override functionality to customize your imported subscription configurations, such as adding rules and customizing proxy groups. You can directly import override files written by others or write your own. <b>Remember to enable the override file on the subscription you want to override</b>. For override file syntax, please refer to the <a href=\"https://mihomo.party/docs/guide/override\" target=\"_blank\">official documentation</a>."
|
||||
},
|
||||
"dns": {
|
||||
"title": "DNS",
|
||||
"description": "The software takes control of the core's DNS settings by default. If you need to use the DNS settings from your subscription configuration, you can disable 'Control DNS Settings' in the application settings. The same applies to domain sniffing."
|
||||
},
|
||||
"end": {
|
||||
"title": "Tutorial Complete",
|
||||
"description": "Now that you understand the basic usage of the software, import your subscription and start using it. Enjoy!\nYou can also join our official <a href=\"https://t.me/mihomo_party_group\" target=\"_blank\">Telegram group</a> for the latest news."
|
||||
}
|
||||
},
|
||||
"main": {
|
||||
"error": {
|
||||
"adminRequired": "Please run with administrator privileges for first launch",
|
||||
"initFailed": "Application initialization failed",
|
||||
"coreStartFailed": "Core startup error",
|
||||
"importFailed": "Subscription import failed",
|
||||
"urlParamMissing": "Missing parameter: url"
|
||||
},
|
||||
"notification": {
|
||||
"importSuccess": "Subscription imported successfully"
|
||||
}
|
||||
}
|
||||
}
|
||||
723
src/renderer/src/locales/zh-CN.json
Normal file
723
src/renderer/src/locales/zh-CN.json
Normal file
@ -0,0 +1,723 @@
|
||||
{
|
||||
"common": {
|
||||
"settings": "设置",
|
||||
"profiles": "配置",
|
||||
"proxies": "代理",
|
||||
"connections": "连接",
|
||||
"dns": "DNS",
|
||||
"tun": "TUN",
|
||||
"save": "保存",
|
||||
"cancel": "取消",
|
||||
"edit": "编辑",
|
||||
"delete": "删除",
|
||||
"seconds": "秒",
|
||||
"confirm": "确认",
|
||||
"auto": "自动",
|
||||
"default": "默认",
|
||||
"close": "关闭",
|
||||
"pinWindow": "窗口置顶",
|
||||
"enterRootPassword": "请输入root密码",
|
||||
"next": "下一步",
|
||||
"prev": "上一步",
|
||||
"done": "完成",
|
||||
"notification": {
|
||||
"restartRequired": "需要重启应用以使更改生效"
|
||||
},
|
||||
"error": {
|
||||
"appCrash": "应用崩溃了 :( 请将以下信息提交给开发者以排查错误",
|
||||
"copyErrorMessage": "复制报错信息",
|
||||
"invalidCron": "无效的 Cron 表达式",
|
||||
"getBackupListFailed": "获取备份列表失败:{{error}}",
|
||||
"restoreFailed": "恢复失败:{{error}}",
|
||||
"deleteFailed": "删除失败:{{error}}",
|
||||
"shortcutRegistrationFailed": "快捷键注册失败",
|
||||
"shortcutRegistrationFailedWithError": "快捷键注册失败:{{error}}"
|
||||
},
|
||||
"updater": {
|
||||
"versionReady": "v{{version}} 版本就绪",
|
||||
"goToDownload": "前往下载",
|
||||
"update": "更新"
|
||||
}
|
||||
},
|
||||
"settings": {
|
||||
"general": "通用设置",
|
||||
"mihomo": "Mihomo 设置",
|
||||
"language": "语言",
|
||||
"theme": "主题",
|
||||
"darkMode": "深色模式",
|
||||
"lightMode": "浅色模式",
|
||||
"autoStart": "开机自启",
|
||||
"autoCheckUpdate": "自动检查更新",
|
||||
"silentStart": "静默启动",
|
||||
"autoQuitWithoutCore": "自动进入轻量模式",
|
||||
"autoQuitWithoutCoreTooltip": "关闭窗口后指定时间自动进入轻量模式",
|
||||
"autoQuitWithoutCoreDelay": "轻量模式自动启用延迟",
|
||||
"envType": "环境变量类型",
|
||||
"showFloatingWindow": "显示悬浮窗",
|
||||
"spinFloatingIcon": "根据网速旋转悬浮窗图标",
|
||||
"disableTray": "禁用托盘图标",
|
||||
"proxyInTray": "在托盘菜单显示代理信息",
|
||||
"showTraffic_windows": "在任务栏显示网速",
|
||||
"showTraffic_mac": "在状态栏显示网速",
|
||||
"showDockIcon": "显示 Dock 图标",
|
||||
"useWindowFrame": "使用系统标题栏",
|
||||
"backgroundColor": "背景颜色",
|
||||
"backgroundAuto": "自动",
|
||||
"backgroundDark": "深色",
|
||||
"backgroundLight": "浅色",
|
||||
"fetchTheme": "获取主题",
|
||||
"importTheme": "导入主题",
|
||||
"editTheme": "编辑主题",
|
||||
"selectTheme": "选择主题",
|
||||
"links": {
|
||||
"docs": "官方文档",
|
||||
"github": "GitHub 仓库",
|
||||
"telegram": "Telegram 群组"
|
||||
},
|
||||
"title": "应用设置"
|
||||
},
|
||||
"mihomo": {
|
||||
"title": "内核设置",
|
||||
"restart": "重启内核",
|
||||
"memory": "内存使用",
|
||||
"userAgent": "订阅 User Agent",
|
||||
"userAgentPlaceholder": "默认:clash.meta",
|
||||
"delayTest": {
|
||||
"url": "延迟测试 URL",
|
||||
"urlPlaceholder": "默认:https://www.gstatic.com/generate_204",
|
||||
"concurrency": "延迟测试并发数",
|
||||
"concurrencyPlaceholder": "默认:50",
|
||||
"timeout": "延迟测试超时",
|
||||
"timeoutPlaceholder": "默认:5000"
|
||||
},
|
||||
"gist": {
|
||||
"title": "同步运行时配置到 Gist",
|
||||
"copyUrl": "复制 Gist URL",
|
||||
"token": "GitHub Token"
|
||||
},
|
||||
"proxyColumns": {
|
||||
"title": "代理显示列数",
|
||||
"auto": "自动",
|
||||
"one": "一列",
|
||||
"two": "两列",
|
||||
"three": "三列",
|
||||
"four": "四列"
|
||||
},
|
||||
"cpuPriority": {
|
||||
"title": "核心进程优先级",
|
||||
"realtime": "实时",
|
||||
"high": "高",
|
||||
"aboveNormal": "高于正常",
|
||||
"normal": "正常",
|
||||
"belowNormal": "低于正常",
|
||||
"low": "低"
|
||||
},
|
||||
"workDir": {
|
||||
"title": "不同订阅使用独立工作目录",
|
||||
"tooltip": "启用后可避免不同订阅中存在相同名称的代理组时发生冲突"
|
||||
},
|
||||
"controlDns": "控制 DNS 设置",
|
||||
"controlSniff": "控制域名嗅探",
|
||||
"autoCloseConnection": "自动关闭连接",
|
||||
"pauseSSID": {
|
||||
"title": "指定 WiFi SSID 直连",
|
||||
"placeholder": "输入 SSID"
|
||||
},
|
||||
"coreVersion": "内核版本",
|
||||
"upgradeCore": "升级内核",
|
||||
"coreAuthLost": "内核权限丢失",
|
||||
"coreUpgradeSuccess": "内核升级成功,若要使用虚拟网卡(Tun),请到虚拟网卡页面重新手动授权内核",
|
||||
"alreadyLatestVersion": "已经是最新版本",
|
||||
"selectCoreVersion": "选择内核版本",
|
||||
"stableVersion": "稳定版",
|
||||
"alphaVersion": "预览版",
|
||||
"mixedPort": "混合端口",
|
||||
"confirm": "确认",
|
||||
"socksPort": "Socks 端口",
|
||||
"httpPort": "Http 端口",
|
||||
"redirPort": "Redir 端口",
|
||||
"externalController": "外部控制地址",
|
||||
"externalControllerSecret": "外部控制访问密钥",
|
||||
"ipv6": "IPv6",
|
||||
"allowLanConnection": "允许局域网连接",
|
||||
"allowedIpSegments": "允许连接的 IP 段",
|
||||
"disallowedIpSegments": "禁止连接的 IP 段",
|
||||
"userVerification": "用户验证",
|
||||
"skipAuthPrefixes": "允许跳过验证的 IP 段",
|
||||
"useRttDelayTest": "使用 RTT 延迟测试",
|
||||
"tcpConcurrent": "TCP 并发",
|
||||
"storeSelectedNode": "存储选择节点",
|
||||
"storeFakeIp": "存储 FakeIP",
|
||||
"logRetentionDays": "日志保留天数",
|
||||
"logLevel": "日志等级",
|
||||
"selectLogLevel": "选择日志等级",
|
||||
"silent": "静默",
|
||||
"error": "错误",
|
||||
"warning": "警告",
|
||||
"info": "信息",
|
||||
"debug": "调试",
|
||||
"findProcess": "查找进程",
|
||||
"selectFindProcessMode": "选择进程查找模式",
|
||||
"strict": "自动",
|
||||
"off": "关闭",
|
||||
"always": "开启",
|
||||
"username": {
|
||||
"placeholder": "用户名"
|
||||
},
|
||||
"password": {
|
||||
"placeholder": "密码"
|
||||
},
|
||||
"ipSegment": {
|
||||
"placeholder": "IP 段"
|
||||
},
|
||||
"interface": {
|
||||
"title": "网络信息"
|
||||
}
|
||||
},
|
||||
"substore": {
|
||||
"title": "Sub-Store",
|
||||
"openInBrowser": "在浏览器中打开",
|
||||
"enable": "启用 Sub-Store",
|
||||
"allowLan": "允许局域网访问",
|
||||
"useCustomBackend": "使用自定义 Sub-Store 后端",
|
||||
"customBackendUrl": {
|
||||
"title": "自定义 Sub-Store 后端 URL",
|
||||
"placeholder": "必须包含协议"
|
||||
},
|
||||
"useProxy": "为所有 Sub-Store 请求启用代理",
|
||||
"sync": {
|
||||
"title": "定时同步订阅/文件",
|
||||
"placeholder": "Cron 表达式"
|
||||
},
|
||||
"restore": {
|
||||
"title": "定时恢复配置",
|
||||
"placeholder": "Cron 表达式"
|
||||
},
|
||||
"backup": {
|
||||
"title": "定时备份配置",
|
||||
"placeholder": "Cron 表达式"
|
||||
}
|
||||
},
|
||||
"webdav": {
|
||||
"title": "WebDAV 备份",
|
||||
"url": "WebDAV URL",
|
||||
"dir": "WebDAV 备份目录",
|
||||
"username": "WebDAV 用户名",
|
||||
"password": "WebDAV 密码",
|
||||
"backup": "备份",
|
||||
"restore": {
|
||||
"title": "恢复备份",
|
||||
"noBackups": "还没有备份"
|
||||
},
|
||||
"notification": {
|
||||
"backupSuccess": {
|
||||
"title": "备份成功",
|
||||
"body": "备份文件已上传到 WebDAV"
|
||||
}
|
||||
}
|
||||
},
|
||||
"shortcuts": {
|
||||
"title": "快捷键设置",
|
||||
"toggleWindow": "打开/关闭窗口",
|
||||
"toggleFloatingWindow": "打开/关闭悬浮窗",
|
||||
"toggleSystemProxy": "打开/关闭系统代理",
|
||||
"toggleTun": "打开/关闭 TUN",
|
||||
"toggleRuleMode": "切换规则模式",
|
||||
"toggleGlobalMode": "切换全局模式",
|
||||
"toggleDirectMode": "切换直连模式",
|
||||
"toggleLightMode": "切换轻量模式",
|
||||
"restartApp": "重启应用",
|
||||
"input": {
|
||||
"placeholder": "点击输入快捷键"
|
||||
}
|
||||
},
|
||||
"sider": {
|
||||
"title": "侧边栏设置",
|
||||
"cards": {
|
||||
"systemProxy": "系统代理",
|
||||
"tun": "虚拟网卡",
|
||||
"profiles": "订阅管理",
|
||||
"proxies": "代理组",
|
||||
"rules": "规则",
|
||||
"resources": "外部资源",
|
||||
"override": "覆写",
|
||||
"connections": "连接",
|
||||
"core": "内核设置",
|
||||
"dns": "DNS",
|
||||
"sniff": "域名嗅探",
|
||||
"logs": "日志",
|
||||
"substore": "Sub-Store",
|
||||
"config": "运行时配置",
|
||||
"emptyProfile": "空白配置",
|
||||
"viewRuntimeConfig": "查看运行时配置",
|
||||
"remote": "远程",
|
||||
"local": "本地",
|
||||
"trafficUsage": "流量使用进度",
|
||||
"neverExpire": "长期有效",
|
||||
"outbound": {
|
||||
"title": "出站模式",
|
||||
"rule": "规则",
|
||||
"global": "全局",
|
||||
"direct": "直连"
|
||||
}
|
||||
},
|
||||
"size": {
|
||||
"large": "大",
|
||||
"small": "小",
|
||||
"hidden": "隐藏"
|
||||
}
|
||||
},
|
||||
"actions": {
|
||||
"guide": {
|
||||
"title": "打开引导页面",
|
||||
"button": "打开引导页面"
|
||||
},
|
||||
"update": {
|
||||
"title": "检查更新",
|
||||
"button": "检查更新",
|
||||
"upToDate": {
|
||||
"title": "当前已是最新版本",
|
||||
"body": "无需更新"
|
||||
}
|
||||
},
|
||||
"reset": {
|
||||
"title": "重置软件",
|
||||
"button": "重置软件",
|
||||
"tooltip": "删除所有配置,将软件恢复初始状态"
|
||||
},
|
||||
"heapSnapshot": {
|
||||
"title": "创建堆快照",
|
||||
"button": "创建堆快照",
|
||||
"tooltip": "创建主进程堆快照,用于排查内存问题"
|
||||
},
|
||||
"lightMode": {
|
||||
"title": "轻量模式",
|
||||
"button": "轻量模式",
|
||||
"tooltip": "完全退出软件,只保留内核进程"
|
||||
},
|
||||
"restartApp": "重启应用",
|
||||
"quit": {
|
||||
"title": "退出应用",
|
||||
"button": "退出应用"
|
||||
},
|
||||
"version": {
|
||||
"title": "应用版本"
|
||||
}
|
||||
},
|
||||
"theme": {
|
||||
"editor": {
|
||||
"title": "编辑主题"
|
||||
}
|
||||
},
|
||||
"proxies": {
|
||||
"card": {
|
||||
"title": "代理组"
|
||||
},
|
||||
"delay": {
|
||||
"test": "测试",
|
||||
"timeout": "超时"
|
||||
},
|
||||
"unpin": "取消固定",
|
||||
"order": {
|
||||
"default": "默认",
|
||||
"delay": "延迟",
|
||||
"name": "名称"
|
||||
},
|
||||
"mode": {
|
||||
"full": "详细信息",
|
||||
"simple": "简洁信息",
|
||||
"direct": "直连模式"
|
||||
},
|
||||
"search": {
|
||||
"placeholder": "搜索节点"
|
||||
},
|
||||
"locate": "定位到当前节点"
|
||||
},
|
||||
"sniffer": {
|
||||
"title": "域名嗅探设置",
|
||||
"parsePureIP": "对未映射 IP 地址嗅探",
|
||||
"forceDNSMapping": "对真实 IP 映射嗅探",
|
||||
"overrideDestination": "覆盖连接地址",
|
||||
"sniff": {
|
||||
"title": "HTTP 端口嗅探",
|
||||
"tls": "TLS 端口嗅探",
|
||||
"quic": "QUIC 端口嗅探",
|
||||
"ports": {
|
||||
"placeholder": "端口号,使用英文逗号分割"
|
||||
}
|
||||
},
|
||||
"skipDomain": {
|
||||
"title": "跳过域名嗅探",
|
||||
"placeholder": "例:+.push.apple.com"
|
||||
},
|
||||
"forceDomain": {
|
||||
"title": "强制域名嗅探",
|
||||
"placeholder": "例:v2ex.com"
|
||||
},
|
||||
"skipDstAddress": {
|
||||
"title": "跳过目标地址嗅探",
|
||||
"placeholder": "例:1.1.1.1/32"
|
||||
},
|
||||
"skipSrcAddress": {
|
||||
"title": "跳过来源地址嗅探",
|
||||
"placeholder": "例:192.168.1.1/24"
|
||||
}
|
||||
},
|
||||
"sysproxy": {
|
||||
"title": "系统代理",
|
||||
"host": {
|
||||
"title": "代理主机",
|
||||
"placeholder": "默认 127.0.0.1 若无特殊需求请勿修改"
|
||||
},
|
||||
"mode": {
|
||||
"title": "代理模式",
|
||||
"manual": "手动",
|
||||
"pac": "PAC"
|
||||
},
|
||||
"uwp": {
|
||||
"title": "UWP 工具",
|
||||
"open": "打开 UWP 工具"
|
||||
},
|
||||
"pac": {
|
||||
"edit": "编辑 PAC 脚本"
|
||||
},
|
||||
"bypass": {
|
||||
"title": "代理绕过",
|
||||
"addDefault": "添加默认代理绕过",
|
||||
"placeholder": "例: *.baidu.com"
|
||||
}
|
||||
},
|
||||
"tun": {
|
||||
"title": "虚拟网卡",
|
||||
"firewall": {
|
||||
"title": "重设防火墙",
|
||||
"reset": "重设防火墙"
|
||||
},
|
||||
"core": {
|
||||
"title": "手动授权内核",
|
||||
"auth": "手动授权内核"
|
||||
},
|
||||
"dns": {
|
||||
"autoSet": "自动设置系统DNS"
|
||||
},
|
||||
"stack": {
|
||||
"title": "Tun 模式堆栈"
|
||||
},
|
||||
"device": {
|
||||
"title": "Tun 网卡名称"
|
||||
},
|
||||
"strictRoute": "严格路由",
|
||||
"autoRoute": "自动设置全局路由",
|
||||
"autoRedirect": "自动设置TCP重定向",
|
||||
"autoDetectInterface": "自动选择流量出口接口",
|
||||
"dnsHijack": "DNS 劫持",
|
||||
"excludeAddress": {
|
||||
"title": "排除自定义网段",
|
||||
"placeholder": "例: 172.20.0.0/16"
|
||||
},
|
||||
"notifications": {
|
||||
"coreAuthSuccess": "内核授权成功",
|
||||
"firewallResetSuccess": "防火墙重设成功"
|
||||
}
|
||||
},
|
||||
"dns": {
|
||||
"title": "DNS 设置",
|
||||
"enhancedMode": {
|
||||
"title": "域名映射模式",
|
||||
"fakeIp": "虚假 IP",
|
||||
"redirHost": "真实 IP",
|
||||
"normal": "取消映射"
|
||||
},
|
||||
"fakeIp": {
|
||||
"range": "回应范围",
|
||||
"rangePlaceholder": "例:198.18.0.1/16",
|
||||
"filter": "真实 IP 回应",
|
||||
"filterPlaceholder": "例:+.lan"
|
||||
},
|
||||
"respectRules": "遵守规则",
|
||||
"defaultNameserver": "DNS 服务器域名解析",
|
||||
"defaultNameserverPlaceholder": "例:223.5.5.5,仅支持 IP",
|
||||
"proxyServerNameserver": "代理服务器域名解析",
|
||||
"proxyServerNameserverPlaceholder": "例:tls://dns.alidns.com",
|
||||
"nameserver": "默认解析服务器",
|
||||
"nameserverPlaceholder": "例:tls://dns.alidns.com",
|
||||
"directNameserver": "直连解析服务器",
|
||||
"directNameserverPlaceholder": "例:tls://dns.alidns.com",
|
||||
"nameserverPolicy": {
|
||||
"title": "覆盖 DNS 策略",
|
||||
"list": "DNS 策略列表",
|
||||
"domainPlaceholder": "域名",
|
||||
"serverPlaceholder": "DNS 服务器"
|
||||
},
|
||||
"systemHosts": {
|
||||
"title": "使用系统 Hosts"
|
||||
},
|
||||
"customHosts": {
|
||||
"title": "自定义 Hosts",
|
||||
"list": "Hosts 列表",
|
||||
"domainPlaceholder": "域名",
|
||||
"valuePlaceholder": "域名或 IP"
|
||||
}
|
||||
},
|
||||
"profiles": {
|
||||
"title": "订阅管理",
|
||||
"updateAll": "更新全部订阅",
|
||||
"useProxy": "代理",
|
||||
"import": "导入",
|
||||
"open": "打开",
|
||||
"new": "新建",
|
||||
"newProfile": "新建订阅",
|
||||
"substore": {
|
||||
"visit": "访问 Sub-Store"
|
||||
},
|
||||
"error": {
|
||||
"unsupportedFileType": "不支持的文件类型"
|
||||
},
|
||||
"emptyProfile": "空白订阅",
|
||||
"viewRuntimeConfig": "查看当前运行时配置",
|
||||
"neverExpire": "长期有效",
|
||||
"remote": "远程",
|
||||
"local": "本地",
|
||||
"trafficUsage": "流量使用进度",
|
||||
"traffic": {
|
||||
"usage": "{{used}}/{{total}}",
|
||||
"unlimited": "无限制",
|
||||
"expired": "已过期",
|
||||
"remainingDays": "剩余 {{days}} 天",
|
||||
"lastUpdate": "最后更新:{{time}}"
|
||||
},
|
||||
"editInfo": {
|
||||
"title": "编辑信息",
|
||||
"name": "名称",
|
||||
"url": "订阅地址",
|
||||
"useProxy": "使用代理更新",
|
||||
"interval": "更新间隔(分钟)",
|
||||
"override": {
|
||||
"title": "覆写",
|
||||
"global": "全局",
|
||||
"noAvailable": "没有可用的覆写",
|
||||
"add": "添加覆写"
|
||||
}
|
||||
},
|
||||
"editFile": {
|
||||
"title": "编辑订阅",
|
||||
"notice": "注意:此处编辑配置更新订阅后会还原,如需要自定义配置请使用",
|
||||
"override": "覆写",
|
||||
"feature": "功能"
|
||||
},
|
||||
"openFile": "打开文件",
|
||||
"home": "主页"
|
||||
},
|
||||
"outbound": {
|
||||
"title": "出站模式",
|
||||
"modes": {
|
||||
"rule": "规则",
|
||||
"global": "全局",
|
||||
"direct": "直连"
|
||||
}
|
||||
},
|
||||
"rules": {
|
||||
"title": "分流规则",
|
||||
"filter": "筛选过滤"
|
||||
},
|
||||
"override": {
|
||||
"title": "覆写",
|
||||
"import": "导入",
|
||||
"docs": "使用文档",
|
||||
"repository": "常用覆写仓库",
|
||||
"unsupportedFileType": "不支持的文件类型",
|
||||
"actions": {
|
||||
"open": "打开",
|
||||
"newYaml": "新建 YAML",
|
||||
"newJs": "新建 JavaScript"
|
||||
},
|
||||
"defaultContent": {
|
||||
"yaml": "# https://mihomo.party/docs/guide/override/yaml",
|
||||
"js": "// https://mihomo.party/docs/guide/override/javascript\nfunction main(config) {\n return config\n}"
|
||||
},
|
||||
"newFile": {
|
||||
"yaml": "新建YAML",
|
||||
"js": "新建JS"
|
||||
},
|
||||
"editInfo": {
|
||||
"title": "编辑信息",
|
||||
"name": "名称",
|
||||
"url": "地址",
|
||||
"global": "全局启用"
|
||||
},
|
||||
"editFile": {
|
||||
"title": "编辑覆写{{type}}",
|
||||
"script": "脚本",
|
||||
"config": "配置"
|
||||
},
|
||||
"execLog": {
|
||||
"title": "执行日志",
|
||||
"close": "关闭"
|
||||
},
|
||||
"menuItems": {
|
||||
"editInfo": "编辑信息",
|
||||
"editFile": "编辑文件",
|
||||
"openFile": "打开文件",
|
||||
"execLog": "执行日志",
|
||||
"delete": "删除"
|
||||
},
|
||||
"labels": {
|
||||
"global": "全局"
|
||||
}
|
||||
},
|
||||
"connections": {
|
||||
"title": "连接",
|
||||
"upload": "上传",
|
||||
"download": "下载",
|
||||
"closeAll": "关闭全部连接",
|
||||
"active": "活动中",
|
||||
"closed": "已关闭",
|
||||
"filter": "筛选过滤",
|
||||
"orderBy": "连接排序方式",
|
||||
"uploadAmount": "上传量",
|
||||
"downloadAmount": "下载量",
|
||||
"uploadSpeed": "上传速度",
|
||||
"downloadSpeed": "下载速度",
|
||||
"detail": {
|
||||
"title": "连接详情",
|
||||
"establishTime": "连接建立时间",
|
||||
"rule": "规则",
|
||||
"proxyChain": "代理链",
|
||||
"connectionType": "连接类型",
|
||||
"host": "主机",
|
||||
"sniffHost": "嗅探主机",
|
||||
"processName": "进程名",
|
||||
"processPath": "进程路径",
|
||||
"sourceIP": "来源IP",
|
||||
"sourceGeoIP": "来源GeoIP",
|
||||
"sourceASN": "来源ASN",
|
||||
"destinationIP": "目标IP",
|
||||
"destinationGeoIP": "目标GeoIP",
|
||||
"destinationASN": "目标ASN",
|
||||
"sourcePort": "来源端口",
|
||||
"destinationPort": "目标端口",
|
||||
"inboundIP": "入站IP",
|
||||
"inboundPort": "入站端口",
|
||||
"copyRule": "复制规则",
|
||||
"inboundName": "入站名称",
|
||||
"inboundUser": "入站用户",
|
||||
"dscp": "DSCP",
|
||||
"remoteDestination": "远程目标",
|
||||
"dnsMode": "DNS模式",
|
||||
"specialProxy": "特殊代理",
|
||||
"specialRules": "特殊规则",
|
||||
"close": "关闭"
|
||||
}
|
||||
},
|
||||
"resources": {
|
||||
"geoData": {
|
||||
"geoip": "GeoIP 数据库",
|
||||
"geosite": "GeoSite 数据库",
|
||||
"mmdb": "MMDB 数据库",
|
||||
"asn": "ASN 数据库",
|
||||
"mode": "GeoData 数据模式",
|
||||
"autoUpdate": "自动更新",
|
||||
"updateInterval": "更新间隔(小时)",
|
||||
"updateSuccess": "GeoData 更新成功"
|
||||
}
|
||||
},
|
||||
"logs": {
|
||||
"title": "实时日志",
|
||||
"filter": "筛选过滤",
|
||||
"clear": "清空日志",
|
||||
"autoScroll": "自动滚动"
|
||||
},
|
||||
"tray": {
|
||||
"showWindow": "显示窗口",
|
||||
"showFloatingWindow": "显示悬浮窗",
|
||||
"hideFloatingWindow": "关闭悬浮窗",
|
||||
"ruleMode": "规则模式",
|
||||
"globalMode": "全局模式",
|
||||
"directMode": "直连模式",
|
||||
"systemProxy": "系统代理",
|
||||
"tun": "虚拟网卡",
|
||||
"profiles": "订阅配置",
|
||||
"openDirectories": {
|
||||
"title": "打开目录",
|
||||
"appDir": "应用目录",
|
||||
"workDir": "工作目录",
|
||||
"coreDir": "内核目录",
|
||||
"logDir": "日志目录"
|
||||
},
|
||||
"copyEnv": "复制环境变量"
|
||||
},
|
||||
"guide": {
|
||||
"welcome": {
|
||||
"title": "欢迎使用 Mihomo Party",
|
||||
"description": "这是一份交互式使用教程,如果您已经完全熟悉本软件的操作,可以直接点击右上角关闭按钮,后续您可以随时从设置中打开本教程"
|
||||
},
|
||||
"sider": {
|
||||
"title": "导航栏",
|
||||
"description": "左侧是应用的导航栏,兼顾仪表盘功能,在这里可以切换不同页面,也可以概览常用的状态信息"
|
||||
},
|
||||
"card": {
|
||||
"title": "卡片",
|
||||
"description": "点击导航栏卡片可以跳转到对应页面,拖动导航栏卡片可以自由排列卡片顺序"
|
||||
},
|
||||
"main": {
|
||||
"title": "主要区域",
|
||||
"description": "右侧是应用的主要区域,展示了导航栏所选页面的内容"
|
||||
},
|
||||
"profile": {
|
||||
"title": "订阅管理",
|
||||
"description": "订阅管理卡片展示当前运行的订阅配置信息,点击进入订阅管理页面可以在这里管理订阅配置"
|
||||
},
|
||||
"import": {
|
||||
"title": "订阅导入",
|
||||
"description": "Mihomo Party 支持多种订阅导入方式,在此输入订阅链接,点击导入即可导入您的订阅配置,如果您的订阅需要代理才能更新,请勾选\"代理\"再点击导入,当然这需要已经有一个可以正常使用的订阅才可以"
|
||||
},
|
||||
"substore": {
|
||||
"title": "Sub-Store",
|
||||
"description": "Mihomo Party 深度集成了 Sub-Store,您可以点击该按钮进入 Sub-Store 或直接导入您通过 Sub-Store 管理的订阅,Mihomo Party 默认使用内置的 Sub-Store 后端,如果您有自建的 Sub-Store 后端,可以在设置页面中配置,如果您不使用 Sub-Store 也可以在设置页面中关闭"
|
||||
},
|
||||
"localProfile": {
|
||||
"title": "本地订阅",
|
||||
"description": "点击\"+\"可以选择本地文件进行导入或者直接新建空白配置进行编辑"
|
||||
},
|
||||
"sysproxy": {
|
||||
"title": "系统代理",
|
||||
"description": "导入订阅之后,内核已经开始运行并监听指定端口,此时您已经可以通过指定代理端口来使用代理了,如果您要使大部分应用自动使用该端口的代理,您还需要打开系统代理开关"
|
||||
},
|
||||
"sysproxySetting": {
|
||||
"title": "系统代理设置",
|
||||
"description": "在此您可以进行系统代理相关设置,选择代理模式,如果某些 Windows 应用不遵循系统代理,还可以使用\"UWP 工具\"解除本地回环限制,对于\"手动代理模式\"和\"PAC 代理模式\"的区别,请自行百度"
|
||||
},
|
||||
"tun": {
|
||||
"title": "虚拟网卡",
|
||||
"description": "虚拟网卡,即同类软件中常见的\"Tun 模式\",对于某些不遵循系统代理的应用,您可以打开虚拟网卡以让内核接管所有流量"
|
||||
},
|
||||
"tunSetting": {
|
||||
"title": "虚拟网卡设置",
|
||||
"description": "这里可以更改虚拟网卡相关设置,Mihomo Party 理论上已经完全解决权限问题,如果您的虚拟网卡仍然不可用,可以尝试重设防火墙(Windows)或手动授权内核(MacOS/Linux)后重启内核"
|
||||
},
|
||||
"override": {
|
||||
"title": "覆写",
|
||||
"description": "Mihomo Party 提供强大的覆写功能,可以对您导入的订阅配置进行个性化修改,如添加规则、自定义代理组等,您可以直接导入别人写好的覆写文件,也可以自己动手编写,<b>编辑好覆写文件一定要记得在需要覆写的订阅上启用</b>,覆写文件的语法请参考 <a href=\"https://mihomo.party/docs/guide/override\" target=\"_blank\">官方文档</a>"
|
||||
},
|
||||
"dns": {
|
||||
"title": "DNS",
|
||||
"description": "软件默认接管了内核的 DNS 设置,如果您需要使用订阅配置中的 DNS 设置,可以到应用设置中关闭\"接管 DNS 设置\",域名嗅探同理"
|
||||
},
|
||||
"end": {
|
||||
"title": "教程结束",
|
||||
"description": "现在您已经了解了软件的基本用法,导入您的订阅开始使用吧,祝您使用愉快!\n您还可以加入我们的官方 <a href=\"https://t.me/mihomo_party_group\" target=\"_blank\">Telegram 群组</a> 获取最新资讯"
|
||||
}
|
||||
},
|
||||
"main": {
|
||||
"error": {
|
||||
"adminRequired": "首次启动请以管理员权限运行",
|
||||
"initFailed": "应用初始化失败",
|
||||
"coreStartFailed": "内核启动出错",
|
||||
"importFailed": "订阅导入失败",
|
||||
"urlParamMissing": "缺少参数 url"
|
||||
},
|
||||
"notification": {
|
||||
"importSuccess": "订阅导入成功"
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -14,6 +14,7 @@ import { OverrideConfigProvider } from './hooks/use-override-config'
|
||||
import { ProfileConfigProvider } from './hooks/use-profile-config'
|
||||
import { RulesProvider } from './hooks/use-rules'
|
||||
import { GroupsProvider } from './hooks/use-groups'
|
||||
import './i18n'
|
||||
|
||||
let F12Count = 0
|
||||
|
||||
|
||||
@ -5,17 +5,19 @@ import { Badge, Button, Divider, Input, Select, SelectItem, Tab, Tabs } from '@n
|
||||
import { calcTraffic } from '@renderer/utils/calc'
|
||||
import ConnectionItem from '@renderer/components/connections/connection-item'
|
||||
import { Virtuoso } from 'react-virtuoso'
|
||||
import dayjs from 'dayjs'
|
||||
import dayjs from '@renderer/utils/dayjs'
|
||||
import ConnectionDetailModal from '@renderer/components/connections/connection-detail-modal'
|
||||
import { CgClose, CgTrash } from 'react-icons/cg'
|
||||
import { useAppConfig } from '@renderer/hooks/use-app-config'
|
||||
import { HiSortAscending, HiSortDescending } from 'react-icons/hi'
|
||||
import { includesIgnoreCase } from '@renderer/utils/includes'
|
||||
import { differenceWith, unionWith } from 'lodash'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
let cachedConnections: IMihomoConnectionDetail[] = []
|
||||
|
||||
const Connections: React.FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const [filter, setFilter] = useState('')
|
||||
const { appConfig, patchAppConfig } = useAppConfig()
|
||||
const { connectionDirection = 'asc', connectionOrderBy = 'time' } = appConfig || {}
|
||||
@ -133,7 +135,7 @@ const Connections: React.FC = () => {
|
||||
|
||||
return (
|
||||
<BasePage
|
||||
title="连接"
|
||||
title={t('connections.title')}
|
||||
header={
|
||||
<div className="flex">
|
||||
<div className="flex items-center">
|
||||
@ -153,7 +155,7 @@ const Connections: React.FC = () => {
|
||||
>
|
||||
<Button
|
||||
className="app-nodrag ml-1"
|
||||
title="关闭全部连接"
|
||||
title={t('connections.closeAll')}
|
||||
isIconOnly
|
||||
size="sm"
|
||||
variant="light"
|
||||
@ -199,7 +201,7 @@ const Connections: React.FC = () => {
|
||||
content={activeConnections.length}
|
||||
showOutline={false}
|
||||
>
|
||||
<span className="p-1">活动中</span>
|
||||
<span className="p-1">{t('connections.active')}</span>
|
||||
</Badge>
|
||||
}
|
||||
/>
|
||||
@ -214,7 +216,7 @@ const Connections: React.FC = () => {
|
||||
content={closedConnections.length}
|
||||
showOutline={false}
|
||||
>
|
||||
<span className="p-1">已关闭</span>
|
||||
<span className="p-1">{t('connections.closed')}</span>
|
||||
</Badge>
|
||||
}
|
||||
/>
|
||||
@ -223,7 +225,7 @@ const Connections: React.FC = () => {
|
||||
variant="flat"
|
||||
size="sm"
|
||||
value={filter}
|
||||
placeholder="筛选过滤"
|
||||
placeholder={t('connections.filter')}
|
||||
isClearable
|
||||
onValueChange={setFilter}
|
||||
/>
|
||||
@ -232,7 +234,7 @@ const Connections: React.FC = () => {
|
||||
classNames={{ trigger: 'data-[hover=true]:bg-default-200' }}
|
||||
size="sm"
|
||||
className="w-[180px] min-w-[120px]"
|
||||
aria-label="连接排序方式"
|
||||
aria-label={t('connections.orderBy')}
|
||||
selectedKeys={new Set([connectionOrderBy])}
|
||||
onSelectionChange={async (v) => {
|
||||
await patchAppConfig({
|
||||
@ -245,11 +247,10 @@ const Connections: React.FC = () => {
|
||||
})
|
||||
}}
|
||||
>
|
||||
<SelectItem key="upload">上传量</SelectItem>
|
||||
<SelectItem key="download">下载量</SelectItem>
|
||||
<SelectItem key="uploadSpeed">上传速度</SelectItem>
|
||||
<SelectItem key="downloadSpeed">下载速度</SelectItem>
|
||||
<SelectItem key="time">时间</SelectItem>
|
||||
<SelectItem key="upload">{t('connections.uploadAmount')}</SelectItem>
|
||||
<SelectItem key="download">{t('connections.downloadAmount')}</SelectItem>
|
||||
<SelectItem key="uploadSpeed">{t('connections.uploadSpeed')}</SelectItem>
|
||||
<SelectItem key="downloadSpeed">{t('connections.downloadSpeed')}</SelectItem>
|
||||
</Select>
|
||||
<Button
|
||||
size="sm"
|
||||
|
||||
@ -7,8 +7,10 @@ import { useControledMihomoConfig } from '@renderer/hooks/use-controled-mihomo-c
|
||||
import { useAppConfig } from '@renderer/hooks/use-app-config'
|
||||
import { restartCore } from '@renderer/utils/ipc'
|
||||
import React, { Key, ReactNode, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
const DNS: React.FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const { controledMihomoConfig, patchControledMihomoConfig } = useControledMihomoConfig()
|
||||
const { appConfig, patchAppConfig } = useAppConfig()
|
||||
const { nameserverPolicy, useNameserverPolicy } = appConfig || {}
|
||||
@ -132,7 +134,7 @@ const DNS: React.FC = () => {
|
||||
|
||||
return (
|
||||
<BasePage
|
||||
title="DNS 设置"
|
||||
title={t('dns.title')}
|
||||
header={
|
||||
changed && (
|
||||
<Button
|
||||
@ -169,39 +171,40 @@ const DNS: React.FC = () => {
|
||||
})
|
||||
}}
|
||||
>
|
||||
保存
|
||||
{t('common.save')}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
>
|
||||
<SettingCard>
|
||||
<SettingItem title="域名映射模式" divider>
|
||||
<SettingItem title={t('dns.enhancedMode.title')} divider>
|
||||
<Tabs
|
||||
size="sm"
|
||||
color="primary"
|
||||
selectedKey={values.enhancedMode}
|
||||
onSelectionChange={(key: Key) => setValues({ ...values, enhancedMode: key as DnsMode })}
|
||||
>
|
||||
<Tab key="fake-ip" title="虚假 IP" />
|
||||
<Tab key="redir-host" title="真实 IP" />
|
||||
<Tab key="normal" title="取消映射" />
|
||||
<Tab key="fake-ip" title={t('dns.enhancedMode.fakeIp')} />
|
||||
<Tab key="redir-host" title={t('dns.enhancedMode.redirHost')} />
|
||||
<Tab key="normal" title={t('dns.enhancedMode.normal')} />
|
||||
</Tabs>
|
||||
</SettingItem>
|
||||
{values.enhancedMode === 'fake-ip' ? (
|
||||
<>
|
||||
<SettingItem title="回应范围" divider>
|
||||
<SettingItem title={t('dns.fakeIp.range')} divider>
|
||||
<Input
|
||||
size="sm"
|
||||
className="w-[50%]"
|
||||
value={values.fakeIPRange}
|
||||
placeholder={t('dns.fakeIp.rangePlaceholder')}
|
||||
onValueChange={(v) => {
|
||||
setValues({ ...values, fakeIPRange: v })
|
||||
}}
|
||||
/>
|
||||
</SettingItem>
|
||||
<div className="flex flex-col items-stretch">
|
||||
<h3>真实 IP 回应</h3>
|
||||
{renderListInputs('fakeIPFilter', '例:+.lan')}
|
||||
<h3>{t('dns.fakeIp.filter')}</h3>
|
||||
{renderListInputs('fakeIPFilter', t('dns.fakeIp.filterPlaceholder'))}
|
||||
</div>
|
||||
<Divider className="my-2" />
|
||||
</>
|
||||
@ -215,7 +218,7 @@ const DNS: React.FC = () => {
|
||||
}}
|
||||
/>
|
||||
</SettingItem>
|
||||
<SettingItem title="遵守规则" divider>
|
||||
<SettingItem title={t('dns.respectRules')} divider>
|
||||
<Switch
|
||||
size="sm"
|
||||
isSelected={values.respectRules}
|
||||
@ -226,26 +229,26 @@ const DNS: React.FC = () => {
|
||||
</SettingItem>
|
||||
|
||||
<div className="flex flex-col items-stretch">
|
||||
<h3>DNS 服务器域名解析</h3>
|
||||
{renderListInputs('defaultNameserver', '例:223.5.5.5,仅支持 IP')}
|
||||
<h3>{t('dns.defaultNameserver')}</h3>
|
||||
{renderListInputs('defaultNameserver', t('dns.defaultNameserverPlaceholder'))}
|
||||
</div>
|
||||
<Divider className="my-2" />
|
||||
<div className="flex flex-col items-stretch">
|
||||
<h3>代理服务器域名解析</h3>
|
||||
{renderListInputs('proxyServerNameserver', '例:tls://dns.alidns.com')}
|
||||
<h3>{t('dns.proxyServerNameserver')}</h3>
|
||||
{renderListInputs('proxyServerNameserver', t('dns.proxyServerNameserverPlaceholder'))}
|
||||
</div>
|
||||
<Divider className="my-2" />
|
||||
<div className="flex flex-col items-stretch">
|
||||
<h3>默认解析服务器</h3>
|
||||
{renderListInputs('nameserver', '例:tls://dns.alidns.com')}
|
||||
<h3>{t('dns.nameserver')}</h3>
|
||||
{renderListInputs('nameserver', t('dns.nameserverPlaceholder'))}
|
||||
</div>
|
||||
<Divider className="my-2" />
|
||||
<div className="flex flex-col items-stretch">
|
||||
<h3>直连解析服务器</h3>
|
||||
{renderListInputs('directNameserver', '例:tls://dns.alidns.com')}
|
||||
<h3>{t('dns.directNameserver')}</h3>
|
||||
{renderListInputs('directNameserver', t('dns.directNameserverPlaceholder'))}
|
||||
</div>
|
||||
<Divider className="my-2" />
|
||||
<SettingItem title="覆盖DNS策略" divider>
|
||||
<SettingItem title={t('dns.nameserverPolicy.title')} divider>
|
||||
<Switch
|
||||
size="sm"
|
||||
isSelected={values.useNameserverPolicy}
|
||||
@ -257,7 +260,7 @@ const DNS: React.FC = () => {
|
||||
{values.useNameserverPolicy && (
|
||||
<div className="flex flex-col items-stretch">
|
||||
<div className="flex flex-col items-stretch">
|
||||
<h3 className="mb-2"></h3>
|
||||
<h3 className="mb-2">{t('dns.nameserverPolicy.list')}</h3>
|
||||
{[...values.nameserverPolicy, { domain: '', value: '' }].map(
|
||||
({ domain, value }, index) => (
|
||||
<div key={index} className="flex mb-2">
|
||||
@ -265,7 +268,7 @@ const DNS: React.FC = () => {
|
||||
<Input
|
||||
size="sm"
|
||||
fullWidth
|
||||
placeholder="域名"
|
||||
placeholder={t('dns.nameserverPolicy.domainPlaceholder')}
|
||||
value={domain}
|
||||
onValueChange={(v) =>
|
||||
handleSubkeyChange(
|
||||
@ -282,7 +285,7 @@ const DNS: React.FC = () => {
|
||||
<Input
|
||||
size="sm"
|
||||
fullWidth
|
||||
placeholder="DNS 服务器"
|
||||
placeholder={t('dns.nameserverPolicy.serverPlaceholder')}
|
||||
value={Array.isArray(value) ? value.join(',') : value}
|
||||
onValueChange={(v) =>
|
||||
handleSubkeyChange('nameserverPolicy', domain, v, index)
|
||||
@ -306,7 +309,7 @@ const DNS: React.FC = () => {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<SettingItem title="使用系统 Hosts" divider>
|
||||
<SettingItem title={t('dns.systemHosts.title')} divider>
|
||||
<Switch
|
||||
size="sm"
|
||||
isSelected={values.useSystemHosts}
|
||||
@ -315,7 +318,7 @@ const DNS: React.FC = () => {
|
||||
}}
|
||||
/>
|
||||
</SettingItem>
|
||||
<SettingItem title="自定义 Hosts">
|
||||
<SettingItem title={t('dns.customHosts.title')}>
|
||||
<Switch
|
||||
size="sm"
|
||||
isSelected={values.useHosts}
|
||||
@ -326,14 +329,14 @@ const DNS: React.FC = () => {
|
||||
</SettingItem>
|
||||
{values.useHosts && (
|
||||
<div className="flex flex-col items-stretch">
|
||||
<h3 className="mb-2"></h3>
|
||||
<h3 className="mb-2">{t('dns.customHosts.list')}</h3>
|
||||
{[...values.hosts, { domain: '', value: '' }].map(({ domain, value }, index) => (
|
||||
<div key={index} className="flex mb-2">
|
||||
<div className="flex-[4]">
|
||||
<Input
|
||||
size="sm"
|
||||
fullWidth
|
||||
placeholder="域名"
|
||||
placeholder={t('dns.customHosts.domainPlaceholder')}
|
||||
value={domain}
|
||||
onValueChange={(v) =>
|
||||
handleSubkeyChange(
|
||||
@ -350,7 +353,7 @@ const DNS: React.FC = () => {
|
||||
<Input
|
||||
size="sm"
|
||||
fullWidth
|
||||
placeholder="域名或 IP"
|
||||
placeholder={t('dns.customHosts.valuePlaceholder')}
|
||||
value={Array.isArray(value) ? value.join(',') : value}
|
||||
onValueChange={(v) => handleSubkeyChange('hosts', domain, v, index)}
|
||||
/>
|
||||
|
||||
@ -5,6 +5,7 @@ import { Button, Divider, Input } from '@nextui-org/react'
|
||||
import { Virtuoso, VirtuosoHandle } from 'react-virtuoso'
|
||||
import { IoLocationSharp } from 'react-icons/io5'
|
||||
import { CgTrash } from 'react-icons/cg'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
import { includesIgnoreCase } from '@renderer/utils/includes'
|
||||
|
||||
@ -35,6 +36,7 @@ window.electron.ipcRenderer.on('mihomoLogs', (_e, log: IMihomoLogInfo) => {
|
||||
})
|
||||
|
||||
const Logs: React.FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const [logs, setLogs] = useState<IMihomoLogInfo[]>(cachedLogs.log)
|
||||
const [filter, setFilter] = useState('')
|
||||
const [trace, setTrace] = useState(true)
|
||||
@ -68,13 +70,13 @@ const Logs: React.FC = () => {
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<BasePage title="实时日志">
|
||||
<BasePage title={t('logs.title')}>
|
||||
<div className="sticky top-0 z-40">
|
||||
<div className="w-full flex p-2">
|
||||
<Input
|
||||
size="sm"
|
||||
value={filter}
|
||||
placeholder="筛选过滤"
|
||||
placeholder={t('logs.filter')}
|
||||
isClearable
|
||||
onValueChange={setFilter}
|
||||
/>
|
||||
@ -84,6 +86,7 @@ const Logs: React.FC = () => {
|
||||
className="ml-2"
|
||||
color={trace ? 'primary' : 'default'}
|
||||
variant={trace ? 'solid' : 'bordered'}
|
||||
title={t('logs.autoScroll')}
|
||||
onPress={() => {
|
||||
setTrace((prev) => !prev)
|
||||
}}
|
||||
@ -93,7 +96,7 @@ const Logs: React.FC = () => {
|
||||
<Button
|
||||
size="sm"
|
||||
isIconOnly
|
||||
title="清空日志"
|
||||
title={t('logs.clear')}
|
||||
className="ml-2"
|
||||
variant="light"
|
||||
color="danger"
|
||||
|
||||
@ -17,13 +17,15 @@ import {
|
||||
import React, { useState } from 'react'
|
||||
import InterfaceModal from '@renderer/components/mihomo/interface-modal'
|
||||
import { MdDeleteForever } from 'react-icons/md'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
const CoreMap = {
|
||||
mihomo: '稳定版',
|
||||
'mihomo-alpha': '预览版'
|
||||
mihomo: 'mihomo.stableVersion',
|
||||
'mihomo-alpha': 'mihomo.alphaVersion'
|
||||
}
|
||||
|
||||
const Mihomo: React.FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const { appConfig, patchAppConfig } = useAppConfig()
|
||||
const { core = 'mihomo', maxLogDays = 7, sysProxy } = appConfig || {}
|
||||
const { controledMihomoConfig, patchControledMihomoConfig } = useControledMihomoConfig()
|
||||
@ -71,15 +73,15 @@ const Mihomo: React.FC = () => {
|
||||
return (
|
||||
<>
|
||||
{lanOpen && <InterfaceModal onClose={() => setLanOpen(false)} />}
|
||||
<BasePage title="内核设置">
|
||||
<BasePage title={t('mihomo.title')}>
|
||||
<SettingCard>
|
||||
<SettingItem
|
||||
title="内核版本"
|
||||
title={t('mihomo.coreVersion')}
|
||||
actions={
|
||||
<Button
|
||||
size="sm"
|
||||
isIconOnly
|
||||
title="升级内核"
|
||||
title={t('mihomo.upgradeCore')}
|
||||
variant="light"
|
||||
isLoading={upgrading}
|
||||
onPress={async () => {
|
||||
@ -90,13 +92,13 @@ const Mihomo: React.FC = () => {
|
||||
PubSub.publish('mihomo-core-changed')
|
||||
}, 2000)
|
||||
if (platform !== 'win32') {
|
||||
new Notification('内核权限丢失', {
|
||||
body: '内核升级成功,若要使用虚拟网卡(Tun),请到虚拟网卡页面重新手动授权内核'
|
||||
new Notification(t('mihomo.coreAuthLost'), {
|
||||
body: t('mihomo.coreUpgradeSuccess')
|
||||
})
|
||||
}
|
||||
} catch (e) {
|
||||
if (typeof e === 'string' && e.includes('already using latest version')) {
|
||||
new Notification('已经是最新版本')
|
||||
new Notification(t('mihomo.alreadyLatestVersion'))
|
||||
} else {
|
||||
alert(e)
|
||||
}
|
||||
@ -114,7 +116,7 @@ const Mihomo: React.FC = () => {
|
||||
classNames={{ trigger: 'data-[hover=true]:bg-default-200' }}
|
||||
className="w-[100px]"
|
||||
size="sm"
|
||||
aria-label="选择内核版本"
|
||||
aria-label={t('mihomo.selectCoreVersion')}
|
||||
selectedKeys={new Set([core])}
|
||||
onSelectionChange={async (v) => {
|
||||
try {
|
||||
@ -127,11 +129,11 @@ const Mihomo: React.FC = () => {
|
||||
}
|
||||
}}
|
||||
>
|
||||
<SelectItem key="mihomo">{CoreMap['mihomo']}</SelectItem>
|
||||
<SelectItem key="mihomo-alpha">{CoreMap['mihomo-alpha']}</SelectItem>
|
||||
<SelectItem key="mihomo">{t(CoreMap['mihomo'])}</SelectItem>
|
||||
<SelectItem key="mihomo-alpha">{t(CoreMap['mihomo-alpha'])}</SelectItem>
|
||||
</Select>
|
||||
</SettingItem>
|
||||
<SettingItem title="混合端口" divider>
|
||||
<SettingItem title={t('mihomo.mixedPort')} divider>
|
||||
<div className="flex">
|
||||
{mixedPortInput !== mixedPort && (
|
||||
<Button
|
||||
@ -146,7 +148,7 @@ const Mihomo: React.FC = () => {
|
||||
}
|
||||
}}
|
||||
>
|
||||
确认
|
||||
{t('mihomo.confirm')}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
@ -163,7 +165,7 @@ const Mihomo: React.FC = () => {
|
||||
/>
|
||||
</div>
|
||||
</SettingItem>
|
||||
<SettingItem title="Socks 端口" divider>
|
||||
<SettingItem title={t('mihomo.socksPort')} divider>
|
||||
<div className="flex">
|
||||
{socksPortInput !== socksPort && (
|
||||
<Button
|
||||
@ -174,7 +176,7 @@ const Mihomo: React.FC = () => {
|
||||
onChangeNeedRestart({ 'socks-port': socksPortInput })
|
||||
}}
|
||||
>
|
||||
确认
|
||||
{t('mihomo.confirm')}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
@ -191,7 +193,7 @@ const Mihomo: React.FC = () => {
|
||||
/>
|
||||
</div>
|
||||
</SettingItem>
|
||||
<SettingItem title="Http 端口" divider>
|
||||
<SettingItem title={t('mihomo.httpPort')} divider>
|
||||
<div className="flex">
|
||||
{httpPortInput !== httpPort && (
|
||||
<Button
|
||||
@ -202,7 +204,7 @@ const Mihomo: React.FC = () => {
|
||||
onChangeNeedRestart({ port: httpPortInput })
|
||||
}}
|
||||
>
|
||||
确认
|
||||
{t('mihomo.confirm')}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
@ -220,7 +222,7 @@ const Mihomo: React.FC = () => {
|
||||
</div>
|
||||
</SettingItem>
|
||||
{platform !== 'win32' && (
|
||||
<SettingItem title="Redir 端口" divider>
|
||||
<SettingItem title={t('mihomo.redirPort')} divider>
|
||||
<div className="flex">
|
||||
{redirPortInput !== redirPort && (
|
||||
<Button
|
||||
@ -231,7 +233,7 @@ const Mihomo: React.FC = () => {
|
||||
onChangeNeedRestart({ 'redir-port': redirPortInput })
|
||||
}}
|
||||
>
|
||||
确认
|
||||
{t('mihomo.confirm')}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
@ -261,7 +263,7 @@ const Mihomo: React.FC = () => {
|
||||
onChangeNeedRestart({ 'tproxy-port': tproxyPortInput })
|
||||
}}
|
||||
>
|
||||
确认
|
||||
{t('mihomo.confirm')}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
@ -279,7 +281,7 @@ const Mihomo: React.FC = () => {
|
||||
</div>
|
||||
</SettingItem>
|
||||
)}
|
||||
<SettingItem title="外部控制地址" divider>
|
||||
<SettingItem title={t('mihomo.externalController')} divider>
|
||||
<div className="flex">
|
||||
{externalControllerInput !== externalController && (
|
||||
<Button
|
||||
@ -292,7 +294,7 @@ const Mihomo: React.FC = () => {
|
||||
})
|
||||
}}
|
||||
>
|
||||
确认
|
||||
{t('mihomo.confirm')}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
@ -306,7 +308,7 @@ const Mihomo: React.FC = () => {
|
||||
/>
|
||||
</div>
|
||||
</SettingItem>
|
||||
<SettingItem title="外部控制访问密钥" divider>
|
||||
<SettingItem title={t('mihomo.externalControllerSecret')} divider>
|
||||
<div className="flex">
|
||||
{secretInput !== secret && (
|
||||
<Button
|
||||
@ -317,7 +319,7 @@ const Mihomo: React.FC = () => {
|
||||
onChangeNeedRestart({ secret: secretInput })
|
||||
}}
|
||||
>
|
||||
确认
|
||||
{t('mihomo.confirm')}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
@ -332,7 +334,7 @@ const Mihomo: React.FC = () => {
|
||||
/>
|
||||
</div>
|
||||
</SettingItem>
|
||||
<SettingItem title="IPv6" divider>
|
||||
<SettingItem title={t('mihomo.ipv6')} divider>
|
||||
<Switch
|
||||
size="sm"
|
||||
isSelected={ipv6}
|
||||
@ -342,7 +344,7 @@ const Mihomo: React.FC = () => {
|
||||
/>
|
||||
</SettingItem>
|
||||
<SettingItem
|
||||
title="允许局域网连接"
|
||||
title={t('mihomo.allowLanConnection')}
|
||||
actions={
|
||||
<Button
|
||||
size="sm"
|
||||
@ -367,7 +369,7 @@ const Mihomo: React.FC = () => {
|
||||
</SettingItem>
|
||||
{allowLan && (
|
||||
<>
|
||||
<SettingItem title="允许连接的 IP 段">
|
||||
<SettingItem title={t('mihomo.allowedIpSegments')}>
|
||||
{lanAllowedIpsInput.join('') !== lanAllowedIps.join('') && (
|
||||
<Button
|
||||
size="sm"
|
||||
@ -376,7 +378,7 @@ const Mihomo: React.FC = () => {
|
||||
onChangeNeedRestart({ 'lan-allowed-ips': lanAllowedIpsInput })
|
||||
}}
|
||||
>
|
||||
确认
|
||||
{t('mihomo.confirm')}
|
||||
</Button>
|
||||
)}
|
||||
</SettingItem>
|
||||
@ -417,7 +419,7 @@ const Mihomo: React.FC = () => {
|
||||
})}
|
||||
</div>
|
||||
<Divider className="mb-2" />
|
||||
<SettingItem title="禁止连接的 IP 段">
|
||||
<SettingItem title={t('mihomo.disallowedIpSegments')}>
|
||||
{lanDisallowedIpsInput.join('') !== lanDisallowedIps.join('') && (
|
||||
<Button
|
||||
size="sm"
|
||||
@ -426,7 +428,7 @@ const Mihomo: React.FC = () => {
|
||||
onChangeNeedRestart({ 'lan-disallowed-ips': lanDisallowedIpsInput })
|
||||
}}
|
||||
>
|
||||
确认
|
||||
{t('mihomo.confirm')}
|
||||
</Button>
|
||||
)}
|
||||
</SettingItem>
|
||||
@ -471,7 +473,7 @@ const Mihomo: React.FC = () => {
|
||||
<Divider className="mb-2" />
|
||||
</>
|
||||
)}
|
||||
<SettingItem title="用户验证">
|
||||
<SettingItem title={t('mihomo.userVerification')}>
|
||||
{authenticationInput.join('') !== authentication.join('') && (
|
||||
<Button
|
||||
size="sm"
|
||||
@ -480,7 +482,7 @@ const Mihomo: React.FC = () => {
|
||||
onChangeNeedRestart({ authentication: authenticationInput })
|
||||
}}
|
||||
>
|
||||
确认
|
||||
{t('mihomo.confirm')}
|
||||
</Button>
|
||||
)}
|
||||
</SettingItem>
|
||||
@ -493,7 +495,7 @@ const Mihomo: React.FC = () => {
|
||||
<Input
|
||||
size="sm"
|
||||
fullWidth
|
||||
placeholder="用户名"
|
||||
placeholder={t('mihomo.username.placeholder')}
|
||||
value={user || ''}
|
||||
onValueChange={(v) => {
|
||||
if (index === authenticationInput.length) {
|
||||
@ -513,7 +515,7 @@ const Mihomo: React.FC = () => {
|
||||
<Input
|
||||
size="sm"
|
||||
fullWidth
|
||||
placeholder="密码"
|
||||
placeholder={t('mihomo.password.placeholder')}
|
||||
value={pass || ''}
|
||||
onValueChange={(v) => {
|
||||
if (index === authenticationInput.length) {
|
||||
@ -546,7 +548,7 @@ const Mihomo: React.FC = () => {
|
||||
})}
|
||||
</div>
|
||||
<Divider className="mb-2" />
|
||||
<SettingItem title="允许跳过验证的 IP 段">
|
||||
<SettingItem title={t('mihomo.skipAuthPrefixes')}>
|
||||
{skipAuthPrefixesInput.join('') !== skipAuthPrefixes.join('') && (
|
||||
<Button
|
||||
size="sm"
|
||||
@ -555,7 +557,7 @@ const Mihomo: React.FC = () => {
|
||||
onChangeNeedRestart({ 'skip-auth-prefixes': skipAuthPrefixesInput })
|
||||
}}
|
||||
>
|
||||
确认
|
||||
{t('mihomo.confirm')}
|
||||
</Button>
|
||||
)}
|
||||
</SettingItem>
|
||||
@ -567,7 +569,7 @@ const Mihomo: React.FC = () => {
|
||||
disabled={index === 0}
|
||||
size="sm"
|
||||
fullWidth
|
||||
placeholder="IP 段"
|
||||
placeholder={t('mihomo.ipSegment.placeholder')}
|
||||
value={ipcidr || ''}
|
||||
onValueChange={(v) => {
|
||||
if (index === skipAuthPrefixesInput.length) {
|
||||
@ -599,7 +601,7 @@ const Mihomo: React.FC = () => {
|
||||
})}
|
||||
</div>
|
||||
<Divider className="mb-2" />
|
||||
<SettingItem title="使用 RTT 延迟测试" divider>
|
||||
<SettingItem title={t('mihomo.useRttDelayTest')} divider>
|
||||
<Switch
|
||||
size="sm"
|
||||
isSelected={unifiedDelay}
|
||||
@ -608,7 +610,7 @@ const Mihomo: React.FC = () => {
|
||||
}}
|
||||
/>
|
||||
</SettingItem>
|
||||
<SettingItem title="TCP 并发" divider>
|
||||
<SettingItem title={t('mihomo.tcpConcurrent')} divider>
|
||||
<Switch
|
||||
size="sm"
|
||||
isSelected={tcpConcurrent}
|
||||
@ -617,7 +619,7 @@ const Mihomo: React.FC = () => {
|
||||
}}
|
||||
/>
|
||||
</SettingItem>
|
||||
<SettingItem title="存储选择节点" divider>
|
||||
<SettingItem title={t('mihomo.storeSelectedNode')} divider>
|
||||
<Switch
|
||||
size="sm"
|
||||
isSelected={storeSelected}
|
||||
@ -626,7 +628,7 @@ const Mihomo: React.FC = () => {
|
||||
}}
|
||||
/>
|
||||
</SettingItem>
|
||||
<SettingItem title="存储 FakeIP" divider>
|
||||
<SettingItem title={t('mihomo.storeFakeIp')} divider>
|
||||
<Switch
|
||||
size="sm"
|
||||
isSelected={storeFakeIp}
|
||||
@ -635,7 +637,7 @@ const Mihomo: React.FC = () => {
|
||||
}}
|
||||
/>
|
||||
</SettingItem>
|
||||
<SettingItem title="日志保留天数" divider>
|
||||
<SettingItem title={t('mihomo.logRetentionDays')} divider>
|
||||
<Input
|
||||
size="sm"
|
||||
type="number"
|
||||
@ -646,38 +648,38 @@ const Mihomo: React.FC = () => {
|
||||
}}
|
||||
/>
|
||||
</SettingItem>
|
||||
<SettingItem title="日志等级" divider>
|
||||
<SettingItem title={t('mihomo.logLevel')} divider>
|
||||
<Select
|
||||
classNames={{ trigger: 'data-[hover=true]:bg-default-200' }}
|
||||
className="w-[100px]"
|
||||
size="sm"
|
||||
aria-label="选择日志等级"
|
||||
aria-label={t('mihomo.selectLogLevel')}
|
||||
selectedKeys={new Set([logLevel])}
|
||||
onSelectionChange={(v) => {
|
||||
onChangeNeedRestart({ 'log-level': v.currentKey as LogLevel })
|
||||
}}
|
||||
>
|
||||
<SelectItem key="silent">静默</SelectItem>
|
||||
<SelectItem key="error">错误</SelectItem>
|
||||
<SelectItem key="warning">警告</SelectItem>
|
||||
<SelectItem key="info">信息</SelectItem>
|
||||
<SelectItem key="debug">调试</SelectItem>
|
||||
<SelectItem key="silent">{t('mihomo.silent')}</SelectItem>
|
||||
<SelectItem key="error">{t('mihomo.error')}</SelectItem>
|
||||
<SelectItem key="warning">{t('mihomo.warning')}</SelectItem>
|
||||
<SelectItem key="info">{t('mihomo.info')}</SelectItem>
|
||||
<SelectItem key="debug">{t('mihomo.debug')}</SelectItem>
|
||||
</Select>
|
||||
</SettingItem>
|
||||
<SettingItem title="查找进程">
|
||||
<SettingItem title={t('mihomo.findProcess')} divider>
|
||||
<Select
|
||||
classNames={{ trigger: 'data-[hover=true]:bg-default-200' }}
|
||||
className="w-[100px]"
|
||||
size="sm"
|
||||
aria-label="选择进程查找模式"
|
||||
aria-label={t('mihomo.selectFindProcessMode')}
|
||||
selectedKeys={new Set([findProcessMode])}
|
||||
onSelectionChange={(v) => {
|
||||
onChangeNeedRestart({ 'find-process-mode': v.currentKey as FindProcessMode })
|
||||
}}
|
||||
>
|
||||
<SelectItem key="strict">自动</SelectItem>
|
||||
<SelectItem key="off">关闭</SelectItem>
|
||||
<SelectItem key="always">开启</SelectItem>
|
||||
<SelectItem key="strict">{t('mihomo.strict')}</SelectItem>
|
||||
<SelectItem key="off">{t('mihomo.off')}</SelectItem>
|
||||
<SelectItem key="always">{t('mihomo.always')}</SelectItem>
|
||||
</Select>
|
||||
</SettingItem>
|
||||
</SettingCard>
|
||||
|
||||
@ -25,8 +25,10 @@ import OverrideItem from '@renderer/components/override/override-item'
|
||||
import { FaPlus } from 'react-icons/fa6'
|
||||
import { HiOutlineDocumentText } from 'react-icons/hi'
|
||||
import { RiArchiveLine } from 'react-icons/ri'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
const Override: React.FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const {
|
||||
overrideConfig,
|
||||
setOverrideConfig,
|
||||
@ -102,7 +104,7 @@ const Override: React.FC = () => {
|
||||
setFileOver(false)
|
||||
}
|
||||
} else {
|
||||
alert('不支持的文件类型')
|
||||
alert(t('override.unsupportedFileType'))
|
||||
}
|
||||
}
|
||||
setFileOver(false)
|
||||
@ -121,13 +123,13 @@ const Override: React.FC = () => {
|
||||
return (
|
||||
<BasePage
|
||||
ref={pageRef}
|
||||
title="覆写"
|
||||
title={t('override.title')}
|
||||
header={
|
||||
<>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="light"
|
||||
title="使用文档"
|
||||
title={t('override.docs')}
|
||||
isIconOnly
|
||||
className="app-nodrag"
|
||||
onPress={() => {
|
||||
@ -138,7 +140,7 @@ const Override: React.FC = () => {
|
||||
</Button>
|
||||
<Button
|
||||
className="app-nodrag"
|
||||
title="常用覆写仓库"
|
||||
title={t('override.repository')}
|
||||
isIconOnly
|
||||
variant="light"
|
||||
size="sm"
|
||||
@ -180,7 +182,7 @@ const Override: React.FC = () => {
|
||||
isLoading={importing}
|
||||
onPress={handleImport}
|
||||
>
|
||||
导入
|
||||
{t('override.import')}
|
||||
</Button>
|
||||
<Dropdown>
|
||||
<DropdownTrigger>
|
||||
@ -208,24 +210,24 @@ const Override: React.FC = () => {
|
||||
}
|
||||
} else if (key === 'new-yaml') {
|
||||
await addOverrideItem({
|
||||
name: '新建YAML',
|
||||
name: t('override.newFile.yaml'),
|
||||
type: 'local',
|
||||
file: '# https://mihomo.party/docs/guide/override/yaml',
|
||||
file: t('override.defaultContent.yaml'),
|
||||
ext: 'yaml'
|
||||
})
|
||||
} else if (key === 'new-js') {
|
||||
await addOverrideItem({
|
||||
name: '新建JS',
|
||||
name: t('override.newFile.js'),
|
||||
type: 'local',
|
||||
file: '// https://mihomo.party/docs/guide/override/javascript\nfunction main(config) {\n return config\n}',
|
||||
file: t('override.defaultContent.js'),
|
||||
ext: 'js'
|
||||
})
|
||||
}
|
||||
}}
|
||||
>
|
||||
<DropdownItem key="open">打开</DropdownItem>
|
||||
<DropdownItem key="new-yaml">新建 YAML</DropdownItem>
|
||||
<DropdownItem key="new-js">新建 JavaScript</DropdownItem>
|
||||
<DropdownItem key="open">{t('override.actions.open')}</DropdownItem>
|
||||
<DropdownItem key="new-yaml">{t('override.actions.newYaml')}</DropdownItem>
|
||||
<DropdownItem key="new-js">{t('override.actions.newJs')}</DropdownItem>
|
||||
</DropdownMenu>
|
||||
</Dropdown>
|
||||
</div>
|
||||
|
||||
@ -31,8 +31,10 @@ import { IoMdRefresh } from 'react-icons/io'
|
||||
import SubStoreIcon from '@renderer/components/base/substore-icon'
|
||||
import useSWR from 'swr'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
const Profiles: React.FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const {
|
||||
profileConfig,
|
||||
setProfileConfig,
|
||||
@ -67,7 +69,7 @@ const Profiles: React.FC = () => {
|
||||
const items: { icon?: ReactNode; key: string; children: ReactNode; divider: boolean }[] = [
|
||||
{
|
||||
key: 'open-substore',
|
||||
children: '访问 Sub-Store',
|
||||
children: t('profiles.substore.visit'),
|
||||
icon: <SubStoreIcon className="text-lg" />,
|
||||
divider:
|
||||
(Boolean(subs) && subs.length > 0) || (Boolean(collections) && collections.length > 0)
|
||||
@ -177,7 +179,7 @@ const Profiles: React.FC = () => {
|
||||
alert(e)
|
||||
}
|
||||
} else {
|
||||
alert('不支持的文件类型')
|
||||
alert(t('profiles.error.unsupportedFileType'))
|
||||
}
|
||||
}
|
||||
setFileOver(false)
|
||||
@ -196,11 +198,11 @@ const Profiles: React.FC = () => {
|
||||
return (
|
||||
<BasePage
|
||||
ref={pageRef}
|
||||
title="订阅管理"
|
||||
title={t('profiles.title')}
|
||||
header={
|
||||
<Button
|
||||
size="sm"
|
||||
title="更新全部订阅"
|
||||
title={t('profiles.updateAll')}
|
||||
className="app-nodrag"
|
||||
variant="light"
|
||||
isIconOnly
|
||||
@ -248,7 +250,7 @@ const Profiles: React.FC = () => {
|
||||
checked={useProxy}
|
||||
onValueChange={setUseProxy}
|
||||
>
|
||||
代理
|
||||
{t('profiles.useProxy')}
|
||||
</Checkbox>
|
||||
</>
|
||||
}
|
||||
@ -262,7 +264,7 @@ const Profiles: React.FC = () => {
|
||||
isLoading={importing}
|
||||
onPress={handleImport}
|
||||
>
|
||||
导入
|
||||
{t('profiles.import')}
|
||||
</Button>
|
||||
{useSubStore && (
|
||||
<Dropdown
|
||||
@ -361,15 +363,15 @@ const Profiles: React.FC = () => {
|
||||
}
|
||||
} else if (key === 'new') {
|
||||
await addProfileItem({
|
||||
name: '新建订阅',
|
||||
name: t('profiles.newProfile'),
|
||||
type: 'local',
|
||||
file: 'proxies: []\nproxy-groups: []\nrules: []'
|
||||
})
|
||||
}
|
||||
}}
|
||||
>
|
||||
<DropdownItem key="open">打开</DropdownItem>
|
||||
<DropdownItem key="new">新建</DropdownItem>
|
||||
<DropdownItem key="open">{t('profiles.open')}</DropdownItem>
|
||||
<DropdownItem key="new">{t('profiles.new')}</DropdownItem>
|
||||
</DropdownMenu>
|
||||
</Dropdown>
|
||||
</div>
|
||||
|
||||
@ -20,6 +20,7 @@ import { useGroups } from '@renderer/hooks/use-groups'
|
||||
import CollapseInput from '@renderer/components/base/collapse-input'
|
||||
import { includesIgnoreCase } from '@renderer/utils/includes'
|
||||
import { useControledMihomoConfig } from '@renderer/hooks/use-controled-mihomo-config'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
const SCROLL_POSITION_KEY = 'proxy_scroll_position'
|
||||
const GROUP_EXPAND_STATE_KEY = 'proxy_group_expand_state'
|
||||
@ -112,6 +113,7 @@ const useProxyState = (groups: IMihomoMixedGroup[]) => {
|
||||
}
|
||||
|
||||
const Proxies: React.FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const { controledMihomoConfig } = useControledMihomoConfig()
|
||||
const { mode = 'rule' } = controledMihomoConfig || {}
|
||||
const { groups = [], mutate } = useGroups()
|
||||
@ -250,7 +252,7 @@ const Proxies: React.FC = () => {
|
||||
|
||||
return (
|
||||
<BasePage
|
||||
title="代理组"
|
||||
title={t('proxies.card.title')}
|
||||
header={
|
||||
<>
|
||||
<Button
|
||||
@ -270,11 +272,11 @@ const Proxies: React.FC = () => {
|
||||
}}
|
||||
>
|
||||
{proxyDisplayOrder === 'default' ? (
|
||||
<TbCircleLetterD className="text-lg" title="默认" />
|
||||
<TbCircleLetterD className="text-lg" title={t('proxies.order.default')} />
|
||||
) : proxyDisplayOrder === 'delay' ? (
|
||||
<MdOutlineSpeed className="text-lg" title="延迟" />
|
||||
<MdOutlineSpeed className="text-lg" title={t('proxies.order.delay')} />
|
||||
) : (
|
||||
<RxLetterCaseCapitalize className="text-lg" title="名称" />
|
||||
<RxLetterCaseCapitalize className="text-lg" title={t('proxies.order.name')} />
|
||||
)}
|
||||
</Button>
|
||||
<Button
|
||||
@ -289,9 +291,9 @@ const Proxies: React.FC = () => {
|
||||
}}
|
||||
>
|
||||
{proxyDisplayMode === 'full' ? (
|
||||
<CgDetailsMore className="text-lg" title="详细信息" />
|
||||
<CgDetailsMore className="text-lg" title={t('proxies.mode.full')} />
|
||||
) : (
|
||||
<CgDetailsLess className="text-lg" title="简洁信息" />
|
||||
<CgDetailsLess className="text-lg" title={t('proxies.mode.simple')} />
|
||||
)}
|
||||
</Button>
|
||||
</>
|
||||
@ -301,7 +303,7 @@ const Proxies: React.FC = () => {
|
||||
<div className="h-full w-full flex justify-center items-center">
|
||||
<div className="flex flex-col items-center">
|
||||
<MdDoubleArrow className="text-foreground-500 text-[100px]" />
|
||||
<h2 className="text-foreground-500 text-[20px]">直连模式</h2>
|
||||
<h2 className="text-foreground-500 text-[20px]">{t('proxies.mode.direct')}</h2>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
@ -383,7 +385,7 @@ const Proxies: React.FC = () => {
|
||||
</Chip>
|
||||
)}
|
||||
<CollapseInput
|
||||
title="搜索节点"
|
||||
title={t('proxies.search.placeholder')}
|
||||
value={searchValue[index]}
|
||||
onValueChange={(v) => {
|
||||
setSearchValue((prev) => {
|
||||
@ -394,7 +396,7 @@ const Proxies: React.FC = () => {
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
title="定位到当前节点"
|
||||
title={t('proxies.locate')}
|
||||
variant="light"
|
||||
size="sm"
|
||||
isIconOnly
|
||||
@ -424,7 +426,7 @@ const Proxies: React.FC = () => {
|
||||
<FaLocationCrosshairs className="text-lg text-foreground-500" />
|
||||
</Button>
|
||||
<Button
|
||||
title="延迟测试"
|
||||
title={t('proxies.delay.test')}
|
||||
variant="light"
|
||||
isLoading={delaying[index]}
|
||||
size="sm"
|
||||
|
||||
@ -2,9 +2,13 @@ import BasePage from '@renderer/components/base/base-page'
|
||||
import GeoData from '@renderer/components/resources/geo-data'
|
||||
import ProxyProvider from '@renderer/components/resources/proxy-provider'
|
||||
import RuleProvider from '@renderer/components/resources/rule-provider'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
const Resources: React.FC = () => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<BasePage title="外部资源">
|
||||
<BasePage title={t('sider.cards.resources')}>
|
||||
<GeoData />
|
||||
<ProxyProvider />
|
||||
<RuleProvider />
|
||||
|
||||
@ -5,10 +5,12 @@ import { useMemo, useState } from 'react'
|
||||
import { Divider, Input } from '@nextui-org/react'
|
||||
import { useRules } from '@renderer/hooks/use-rules'
|
||||
import { includesIgnoreCase } from '@renderer/utils/includes'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
const Rules: React.FC = () => {
|
||||
const { rules } = useRules()
|
||||
const [filter, setFilter] = useState('')
|
||||
const { t } = useTranslation()
|
||||
|
||||
const filteredRules = useMemo(() => {
|
||||
if (!rules) return []
|
||||
@ -23,13 +25,13 @@ const Rules: React.FC = () => {
|
||||
}, [rules, filter])
|
||||
|
||||
return (
|
||||
<BasePage title="分流规则">
|
||||
<BasePage title={t('rules.title')}>
|
||||
<div className="sticky top-0 z-40">
|
||||
<div className="flex p-2">
|
||||
<Input
|
||||
size="sm"
|
||||
value={filter}
|
||||
placeholder="筛选过滤"
|
||||
placeholder={t('rules.filter')}
|
||||
isClearable
|
||||
onValueChange={setFilter}
|
||||
/>
|
||||
|
||||
@ -10,18 +10,21 @@ import ShortcutConfig from '@renderer/components/settings/shortcut-config'
|
||||
import { FaTelegramPlane } from 'react-icons/fa'
|
||||
import SiderConfig from '@renderer/components/settings/sider-config'
|
||||
import SubStoreConfig from '@renderer/components/settings/substore-config'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
const Settings: React.FC = () => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
return (
|
||||
<BasePage
|
||||
title="应用设置"
|
||||
title={t('settings.title')}
|
||||
header={
|
||||
<>
|
||||
<Button
|
||||
isIconOnly
|
||||
size="sm"
|
||||
variant="light"
|
||||
title="官方文档"
|
||||
title={t('settings.links.docs')}
|
||||
className="app-nodrag"
|
||||
onPress={() => {
|
||||
window.open('https://mihomo.party')
|
||||
@ -34,7 +37,7 @@ const Settings: React.FC = () => {
|
||||
size="sm"
|
||||
variant="light"
|
||||
className="app-nodrag"
|
||||
title="GitHub 仓库"
|
||||
title={t('settings.links.github')}
|
||||
onPress={() => {
|
||||
window.open('https://github.com/mihomo-party-org/mihomo-party')
|
||||
}}
|
||||
@ -46,7 +49,7 @@ const Settings: React.FC = () => {
|
||||
size="sm"
|
||||
variant="light"
|
||||
className="app-nodrag"
|
||||
title="Telegram 群组"
|
||||
title={t('settings.links.telegram')}
|
||||
onPress={() => {
|
||||
window.open('https://t.me/mihomo_party_group')
|
||||
}}
|
||||
|
||||
@ -6,8 +6,10 @@ import { useControledMihomoConfig } from '@renderer/hooks/use-controled-mihomo-c
|
||||
import { restartCore } from '@renderer/utils/ipc'
|
||||
import React, { ReactNode, useState } from 'react'
|
||||
import { MdDeleteForever } from 'react-icons/md'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
const Sniffer: React.FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const { controledMihomoConfig, patchControledMihomoConfig } = useControledMihomoConfig()
|
||||
const { sniffer } = controledMihomoConfig || {}
|
||||
const {
|
||||
@ -118,7 +120,7 @@ const Sniffer: React.FC = () => {
|
||||
|
||||
return (
|
||||
<BasePage
|
||||
title="域名嗅探设置"
|
||||
title={t('sniffer.title')}
|
||||
header={
|
||||
changed && (
|
||||
<Button
|
||||
@ -138,13 +140,13 @@ const Sniffer: React.FC = () => {
|
||||
})
|
||||
}
|
||||
>
|
||||
保存
|
||||
{t('common.save')}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
>
|
||||
<SettingCard>
|
||||
<SettingItem title="覆盖连接地址" divider>
|
||||
<SettingItem title={t('sniffer.overrideDestination')} divider>
|
||||
<Switch
|
||||
size="sm"
|
||||
isSelected={values.overrideDestination}
|
||||
@ -164,7 +166,7 @@ const Sniffer: React.FC = () => {
|
||||
}}
|
||||
/>
|
||||
</SettingItem>
|
||||
<SettingItem title="对真实 IP 映射嗅探" divider>
|
||||
<SettingItem title={t('sniffer.forceDNSMapping')} divider>
|
||||
<Switch
|
||||
size="sm"
|
||||
isSelected={values.forceDNSMapping}
|
||||
@ -173,7 +175,7 @@ const Sniffer: React.FC = () => {
|
||||
}}
|
||||
/>
|
||||
</SettingItem>
|
||||
<SettingItem title="对未映射 IP 地址嗅探" divider>
|
||||
<SettingItem title={t('sniffer.parsePureIP')} divider>
|
||||
<Switch
|
||||
size="sm"
|
||||
isSelected={values.parsePureIP}
|
||||
@ -182,51 +184,51 @@ const Sniffer: React.FC = () => {
|
||||
}}
|
||||
/>
|
||||
</SettingItem>
|
||||
<SettingItem title="HTTP 端口嗅探" divider>
|
||||
<SettingItem title={t('sniffer.sniff.title')} divider>
|
||||
<Input
|
||||
size="sm"
|
||||
className="w-[50%]"
|
||||
placeholder="端口号,使用英文逗号分割"
|
||||
placeholder={t('sniffer.sniff.ports.placeholder')}
|
||||
value={values.sniff.HTTP?.ports.join(',')}
|
||||
onValueChange={(v) => handleSniffPortChange('HTTP', v)}
|
||||
/>
|
||||
</SettingItem>
|
||||
<SettingItem title="TLS 端口嗅探" divider>
|
||||
<SettingItem title={t('sniffer.sniff.tls')} divider>
|
||||
<Input
|
||||
size="sm"
|
||||
className="w-[50%]"
|
||||
placeholder="端口号,使用英文逗号分割"
|
||||
placeholder={t('sniffer.sniff.ports.placeholder')}
|
||||
value={values.sniff.TLS?.ports.join(',')}
|
||||
onValueChange={(v) => handleSniffPortChange('TLS', v)}
|
||||
/>
|
||||
</SettingItem>
|
||||
<SettingItem title="QUIC 端口嗅探" divider>
|
||||
<SettingItem title={t('sniffer.sniff.quic')} divider>
|
||||
<Input
|
||||
size="sm"
|
||||
className="w-[50%]"
|
||||
placeholder="端口号,使用英文逗号分割"
|
||||
placeholder={t('sniffer.sniff.ports.placeholder')}
|
||||
value={values.sniff.QUIC?.ports.join(',')}
|
||||
onValueChange={(v) => handleSniffPortChange('QUIC', v)}
|
||||
/>
|
||||
</SettingItem>
|
||||
<div className="flex flex-col items-stretch">
|
||||
<h3>跳过域名嗅探</h3>
|
||||
{renderListInputs('skipDomain', '例:+.push.apple.com')}
|
||||
<h3>{t('sniffer.skipDomain.title')}</h3>
|
||||
{renderListInputs('skipDomain', t('sniffer.skipDomain.placeholder'))}
|
||||
</div>
|
||||
<Divider className="my-2" />
|
||||
<div className="flex flex-col items-stretch">
|
||||
<h3 className="mb-2">强制域名嗅探</h3>
|
||||
{renderListInputs('forceDomain', '例:v2ex.com')}
|
||||
<h3 className="mb-2">{t('sniffer.forceDomain.title')}</h3>
|
||||
{renderListInputs('forceDomain', t('sniffer.forceDomain.placeholder'))}
|
||||
</div>
|
||||
<Divider className="my-2" />
|
||||
<div className="flex flex-col items-stretch">
|
||||
<h3 className="mb-2">跳过目标地址嗅探</h3>
|
||||
{renderListInputs('skipDstAddress', '例:1.1.1.1/32')}
|
||||
<h3 className="mb-2">{t('sniffer.skipDstAddress.title')}</h3>
|
||||
{renderListInputs('skipDstAddress', t('sniffer.skipDstAddress.placeholder'))}
|
||||
</div>
|
||||
<Divider className="my-2" />
|
||||
<div className="flex flex-col items-stretch">
|
||||
<h3 className="mb-2">跳过来源地址嗅探</h3>
|
||||
{renderListInputs('skipSrcAddress', '例:192.168.1.1/24')}
|
||||
<h3 className="mb-2">{t('sniffer.skipSrcAddress.title')}</h3>
|
||||
{renderListInputs('skipSrcAddress', t('sniffer.skipSrcAddress.placeholder'))}
|
||||
</div>
|
||||
</SettingCard>
|
||||
</BasePage>
|
||||
|
||||
@ -4,8 +4,10 @@ import { useAppConfig } from '@renderer/hooks/use-app-config'
|
||||
import { subStoreFrontendPort, subStorePort } from '@renderer/utils/ipc'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { HiExternalLink } from 'react-icons/hi'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
const SubStore: React.FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const { appConfig } = useAppConfig()
|
||||
const { useCustomSubStore, customSubStoreUrl } = appConfig || {}
|
||||
const [backendPort, setBackendPort] = useState<number | undefined>()
|
||||
@ -23,10 +25,10 @@ const SubStore: React.FC = () => {
|
||||
return (
|
||||
<>
|
||||
<BasePage
|
||||
title="Sub-Store"
|
||||
title={t('substore.title')}
|
||||
header={
|
||||
<Button
|
||||
title="在浏览器中打开"
|
||||
title={t('substore.openInBrowser')}
|
||||
isIconOnly
|
||||
size="sm"
|
||||
className="app-nodrag"
|
||||
|
||||
@ -9,6 +9,7 @@ import { openUWPTool, triggerSysProxy } from '@renderer/utils/ipc'
|
||||
import { Key, useState } from 'react'
|
||||
import React from 'react'
|
||||
import { MdDeleteForever } from 'react-icons/md'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
const defaultBypass: string[] =
|
||||
platform === 'linux'
|
||||
@ -55,6 +56,7 @@ function FindProxyForURL(url, host) {
|
||||
`
|
||||
|
||||
const Sysproxy: React.FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const { appConfig, patchAppConfig } = useAppConfig()
|
||||
const { sysProxy } = appConfig || ({ sysProxy: { enable: false } } as IAppConfig)
|
||||
const [changed, setChanged] = useState(false)
|
||||
@ -104,11 +106,11 @@ const Sysproxy: React.FC = () => {
|
||||
|
||||
return (
|
||||
<BasePage
|
||||
title="系统代理设置"
|
||||
title={t('sysproxy.title')}
|
||||
header={
|
||||
changed && (
|
||||
<Button color="primary" className="app-nodrag" size="sm" onPress={onSave}>
|
||||
保存
|
||||
{t('common.save')}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
@ -124,68 +126,68 @@ const Sysproxy: React.FC = () => {
|
||||
/>
|
||||
)}
|
||||
<SettingCard className="sysproxy-settings">
|
||||
<SettingItem title="代理主机" divider>
|
||||
<SettingItem title={t('sysproxy.host.title')} divider>
|
||||
<Input
|
||||
size="sm"
|
||||
className="w-[50%]"
|
||||
value={values.host}
|
||||
placeholder="默认 127.0.0.1 若无特殊需求请勿修改"
|
||||
placeholder={t('sysproxy.host.placeholder')}
|
||||
onValueChange={(v) => {
|
||||
setValues({ ...values, host: v })
|
||||
}}
|
||||
/>
|
||||
</SettingItem>
|
||||
<SettingItem title="代理模式" divider>
|
||||
<SettingItem title={t('sysproxy.mode.title')} divider>
|
||||
<Tabs
|
||||
size="sm"
|
||||
color="primary"
|
||||
selectedKey={values.mode}
|
||||
onSelectionChange={(key: Key) => setValues({ ...values, mode: key as SysProxyMode })}
|
||||
>
|
||||
<Tab key="manual" title="手动" />
|
||||
<Tab key="auto" title="PAC" />
|
||||
<Tab key="manual" title={t('sysproxy.mode.manual')} />
|
||||
<Tab key="auto" title={t('sysproxy.mode.pac')} />
|
||||
</Tabs>
|
||||
</SettingItem>
|
||||
{platform === 'win32' && (
|
||||
<SettingItem title="UWP 工具" divider>
|
||||
<SettingItem title={t('sysproxy.uwp.title')} divider>
|
||||
<Button
|
||||
size="sm"
|
||||
onPress={async () => {
|
||||
await openUWPTool()
|
||||
}}
|
||||
>
|
||||
打开 UWP 工具
|
||||
{t('sysproxy.uwp.open')}
|
||||
</Button>
|
||||
</SettingItem>
|
||||
)}
|
||||
|
||||
{values.mode === 'auto' && (
|
||||
<SettingItem title="代理模式">
|
||||
<SettingItem title={t('sysproxy.mode.title')}>
|
||||
<Button size="sm" onPress={() => setOpenPacEditor(true)} variant="bordered">
|
||||
编辑 PAC 脚本
|
||||
{t('sysproxy.pac.edit')}
|
||||
</Button>
|
||||
</SettingItem>
|
||||
)}
|
||||
{values.mode === 'manual' && (
|
||||
<>
|
||||
<SettingItem title="添加默认代理绕过" divider>
|
||||
<SettingItem title={t('sysproxy.bypass.addDefault')} divider>
|
||||
<Button
|
||||
size="sm"
|
||||
onPress={() => {
|
||||
setValues({ ...values, bypass: defaultBypass.concat(values.bypass) })
|
||||
}}
|
||||
>
|
||||
添加默认代理绕过
|
||||
{t('sysproxy.bypass.addDefault')}
|
||||
</Button>
|
||||
</SettingItem>
|
||||
<div className="flex flex-col items-stretch">
|
||||
<h3 className="mb-2">代理绕过</h3>
|
||||
<h3 className="mb-2">{t('sysproxy.bypass.title')}</h3>
|
||||
{[...values.bypass, ''].map((domain, index) => (
|
||||
<div key={index} className="mb-2 flex">
|
||||
<Input
|
||||
fullWidth
|
||||
size="sm"
|
||||
placeholder="例: *.baidu.com"
|
||||
placeholder={t('sysproxy.bypass.placeholder')}
|
||||
value={domain}
|
||||
onValueChange={(v) => handleBypassChange(v, index)}
|
||||
/>
|
||||
|
||||
@ -9,8 +9,10 @@ import React, { Key, useState } from 'react'
|
||||
import BasePasswordModal from '@renderer/components/base/base-password-modal'
|
||||
import { useAppConfig } from '@renderer/hooks/use-app-config'
|
||||
import { MdDeleteForever } from 'react-icons/md'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
const Tun: React.FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const { controledMihomoConfig, patchControledMihomoConfig } = useControledMihomoConfig()
|
||||
const { appConfig, patchAppConfig } = useAppConfig()
|
||||
const { autoSetDNS = true } = appConfig || {}
|
||||
@ -75,7 +77,7 @@ const Tun: React.FC = () => {
|
||||
onConfirm={async (password: string) => {
|
||||
try {
|
||||
await manualGrantCorePermition(password)
|
||||
new Notification('内核授权成功')
|
||||
new Notification(t('tun.notifications.coreAuthSuccess'))
|
||||
await restartCore()
|
||||
setOpenPasswordModal(false)
|
||||
} catch (e) {
|
||||
@ -85,7 +87,7 @@ const Tun: React.FC = () => {
|
||||
/>
|
||||
)}
|
||||
<BasePage
|
||||
title="虚拟网卡设置"
|
||||
title={t('tun.title')}
|
||||
header={
|
||||
changed && (
|
||||
<Button
|
||||
@ -108,14 +110,14 @@ const Tun: React.FC = () => {
|
||||
})
|
||||
}
|
||||
>
|
||||
保存
|
||||
{t('common.save')}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
>
|
||||
<SettingCard className="tun-settings">
|
||||
{platform === 'win32' && (
|
||||
<SettingItem title="重设防火墙" divider>
|
||||
<SettingItem title={t('tun.firewall.title')} divider>
|
||||
<Button
|
||||
size="sm"
|
||||
color="primary"
|
||||
@ -124,7 +126,7 @@ const Tun: React.FC = () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
await setupFirewall()
|
||||
new Notification('防火墙重设成功')
|
||||
new Notification(t('tun.notifications.firewallResetSuccess'))
|
||||
await restartCore()
|
||||
} catch (e) {
|
||||
alert(e)
|
||||
@ -133,12 +135,12 @@ const Tun: React.FC = () => {
|
||||
}
|
||||
}}
|
||||
>
|
||||
重设防火墙
|
||||
{t('tun.firewall.reset')}
|
||||
</Button>
|
||||
</SettingItem>
|
||||
)}
|
||||
{platform !== 'win32' && (
|
||||
<SettingItem title="手动授权内核" divider>
|
||||
<SettingItem title={t('tun.core.title')} divider>
|
||||
<Button
|
||||
size="sm"
|
||||
color="primary"
|
||||
@ -146,7 +148,7 @@ const Tun: React.FC = () => {
|
||||
if (platform === 'darwin') {
|
||||
try {
|
||||
await manualGrantCorePermition()
|
||||
new Notification('内核授权成功')
|
||||
new Notification(t('tun.notifications.coreAuthSuccess'))
|
||||
await restartCore()
|
||||
} catch (e) {
|
||||
alert(e)
|
||||
@ -156,12 +158,12 @@ const Tun: React.FC = () => {
|
||||
}
|
||||
}}
|
||||
>
|
||||
手动授权内核
|
||||
{t('tun.core.auth')}
|
||||
</Button>
|
||||
</SettingItem>
|
||||
)}
|
||||
{platform === 'darwin' && (
|
||||
<SettingItem title="自动设置系统DNS" divider>
|
||||
<SettingItem title={t('tun.dns.autoSet')} divider>
|
||||
<Switch
|
||||
size="sm"
|
||||
isSelected={autoSetDNS}
|
||||
@ -172,7 +174,7 @@ const Tun: React.FC = () => {
|
||||
</SettingItem>
|
||||
)}
|
||||
|
||||
<SettingItem title="Tun 模式堆栈" divider>
|
||||
<SettingItem title={t('tun.stack.title')} divider>
|
||||
<Tabs
|
||||
size="sm"
|
||||
color="primary"
|
||||
@ -185,7 +187,7 @@ const Tun: React.FC = () => {
|
||||
</Tabs>
|
||||
</SettingItem>
|
||||
{platform !== 'darwin' && (
|
||||
<SettingItem title="Tun 网卡名称" divider>
|
||||
<SettingItem title={t('tun.device.title')} divider>
|
||||
<Input
|
||||
size="sm"
|
||||
className="w-[100px]"
|
||||
@ -197,7 +199,7 @@ const Tun: React.FC = () => {
|
||||
</SettingItem>
|
||||
)}
|
||||
|
||||
<SettingItem title="严格路由" divider>
|
||||
<SettingItem title={t('tun.strictRoute')} divider>
|
||||
<Switch
|
||||
size="sm"
|
||||
isSelected={values.strictRoute}
|
||||
@ -206,7 +208,7 @@ const Tun: React.FC = () => {
|
||||
}}
|
||||
/>
|
||||
</SettingItem>
|
||||
<SettingItem title="自动设置全局路由" divider>
|
||||
<SettingItem title={t('tun.autoRoute')} divider>
|
||||
<Switch
|
||||
size="sm"
|
||||
isSelected={values.autoRoute}
|
||||
@ -216,7 +218,7 @@ const Tun: React.FC = () => {
|
||||
/>
|
||||
</SettingItem>
|
||||
{platform === 'linux' && (
|
||||
<SettingItem title="自动设置TCP重定向" divider>
|
||||
<SettingItem title={t('tun.autoRedirect')} divider>
|
||||
<Switch
|
||||
size="sm"
|
||||
isSelected={values.autoRedirect}
|
||||
@ -226,7 +228,7 @@ const Tun: React.FC = () => {
|
||||
/>
|
||||
</SettingItem>
|
||||
)}
|
||||
<SettingItem title="自动选择流量出口接口" divider>
|
||||
<SettingItem title={t('tun.autoDetectInterface')} divider>
|
||||
<Switch
|
||||
size="sm"
|
||||
isSelected={values.autoDetectInterface}
|
||||
@ -246,7 +248,7 @@ const Tun: React.FC = () => {
|
||||
}}
|
||||
/>
|
||||
</SettingItem>
|
||||
<SettingItem title="DNS 劫持" divider>
|
||||
<SettingItem title={t('tun.dnsHijack')} divider>
|
||||
<Input
|
||||
size="sm"
|
||||
className="w-[50%]"
|
||||
@ -258,13 +260,13 @@ const Tun: React.FC = () => {
|
||||
/>
|
||||
</SettingItem>
|
||||
<div className="flex flex-col items-stretch">
|
||||
<h3 className="mb-2">排除自定义网段</h3>
|
||||
<h3 className="mb-2">{t('tun.excludeAddress.title')}</h3>
|
||||
{[...values.routeExcludeAddress, ''].map((address, index) => (
|
||||
<div key={index} className="mb-2 flex">
|
||||
<Input
|
||||
fullWidth
|
||||
size="sm"
|
||||
placeholder="例: 172.20.0.0/16"
|
||||
placeholder={t('tun.excludeAddress.placeholder')}
|
||||
value={address}
|
||||
onValueChange={(v) => handleExcludeAddressChange(v, index)}
|
||||
/>
|
||||
|
||||
22
src/renderer/src/utils/dayjs.ts
Normal file
22
src/renderer/src/utils/dayjs.ts
Normal file
@ -0,0 +1,22 @@
|
||||
import dayjs from 'dayjs'
|
||||
import 'dayjs/locale/zh-cn'
|
||||
import 'dayjs/locale/en'
|
||||
import relativeTime from 'dayjs/plugin/relativeTime'
|
||||
import i18n from '@renderer/i18n'
|
||||
|
||||
// 加载相对时间插件
|
||||
dayjs.extend(relativeTime)
|
||||
|
||||
// 根据当前语言设置 dayjs 语言
|
||||
const updateDayjsLocale = (): void => {
|
||||
const currentLanguage = i18n.language
|
||||
dayjs.locale(currentLanguage === 'zh-CN' ? 'zh-cn' : 'en')
|
||||
}
|
||||
|
||||
// 初始设置语言
|
||||
updateDayjsLocale()
|
||||
|
||||
// 监听语言变化
|
||||
i18n.on('languageChanged', updateDayjsLocale)
|
||||
|
||||
export default dayjs
|
||||
31
src/shared/i18n.ts
Normal file
31
src/shared/i18n.ts
Normal file
@ -0,0 +1,31 @@
|
||||
import i18next from 'i18next'
|
||||
import enUS from '../renderer/src/locales/en-US.json'
|
||||
import zhCN from '../renderer/src/locales/zh-CN.json'
|
||||
|
||||
export const resources = {
|
||||
'en-US': {
|
||||
translation: enUS
|
||||
},
|
||||
'zh-CN': {
|
||||
translation: zhCN
|
||||
}
|
||||
}
|
||||
|
||||
export const defaultConfig = {
|
||||
resources,
|
||||
lng: 'zh-CN',
|
||||
fallbackLng: 'en-US',
|
||||
interpolation: {
|
||||
escapeValue: false
|
||||
}
|
||||
}
|
||||
|
||||
export const initI18n = async (options = {}): Promise<typeof i18next> => {
|
||||
await i18next.init({
|
||||
...defaultConfig,
|
||||
...options
|
||||
})
|
||||
return i18next
|
||||
}
|
||||
|
||||
export default i18next
|
||||
1
src/shared/types.d.ts
vendored
1
src/shared/types.d.ts
vendored
@ -291,6 +291,7 @@ interface IAppConfig {
|
||||
directModeShortcut?: string
|
||||
restartAppShortcut?: string
|
||||
quitWithoutCoreShortcut?: string
|
||||
language?: 'zh-CN' | 'en-US'
|
||||
}
|
||||
|
||||
interface IMihomoTunConfig {
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{
|
||||
"extends": "@electron-toolkit/tsconfig/tsconfig.node.json",
|
||||
"include": ["electron.vite.config.*", "src/main/**/*", "src/preload/**/*", "src/shared/**/*.d.ts"],
|
||||
"include": ["electron.vite.config.*", "src/main/**/*", "src/preload/**/*", "src/shared/**/*"],
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"types": ["electron-vite/node"]
|
||||
|
||||
@ -1,11 +1,12 @@
|
||||
{
|
||||
"extends": "@electron-toolkit/tsconfig/tsconfig.web.json",
|
||||
"include": [
|
||||
"src/renderer/**/*",
|
||||
"src/shared/**/*",
|
||||
"src/renderer/src/utils/env.d.ts",
|
||||
"src/renderer/src/**/*",
|
||||
"src/renderer/src/**/*.tsx",
|
||||
"src/preload/*.d.ts",
|
||||
"src/shared/*.d.ts"
|
||||
"src/preload/*.d.ts"
|
||||
],
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user