mirror of
https://gh.catmak.name/https://github.com/mihomo-party-org/mihomo-party
synced 2026-02-10 19:50:28 +08:00
Compare commits
9 Commits
3f1d1f84a1
...
923bd8d7ee
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
923bd8d7ee | ||
|
|
2c639d5bff | ||
|
|
8384953fb7 | ||
|
|
85f430f188 | ||
|
|
7634177c5c | ||
|
|
3097019e9e | ||
|
|
9e5d11c3c8 | ||
|
|
676743d1b0 | ||
|
|
c1f7a862aa |
@ -66,9 +66,9 @@
|
||||
"cron-validator": "^1.4.0",
|
||||
"dayjs": "^1.11.19",
|
||||
"driver.js": "^1.3.6",
|
||||
"electron": "^37.10.0",
|
||||
"electron": "37.10.0",
|
||||
"electron-builder": "26.0.12",
|
||||
"electron-vite": "^4.0.1",
|
||||
"electron-vite": "4.0.1",
|
||||
"electron-window-state": "^5.0.3",
|
||||
"eslint": "9.39.2",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
|
||||
637
pnpm-lock.yaml
generated
637
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -6,8 +6,12 @@ import { getAppConfig } from './app'
|
||||
import { defaultControledMihomoConfig } from '../utils/template'
|
||||
import { deepMerge } from '../utils/merge'
|
||||
import { existsSync } from 'fs'
|
||||
import { createLogger } from '../utils/logger'
|
||||
|
||||
const controledMihomoLogger = createLogger('ControledMihomo')
|
||||
|
||||
let controledMihomoConfig: Partial<IMihomoConfig> // mihomo.yaml
|
||||
let controledMihomoWriteQueue: Promise<void> = Promise.resolve()
|
||||
|
||||
export async function getControledMihomoConfig(force = false): Promise<Partial<IMihomoConfig>> {
|
||||
if (force || !controledMihomoConfig) {
|
||||
@ -23,7 +27,7 @@ export async function getControledMihomoConfig(force = false): Promise<Partial<I
|
||||
'utf-8'
|
||||
)
|
||||
} catch (error) {
|
||||
console.error('Failed to create mihomo.yaml file:', error)
|
||||
controledMihomoLogger.error('Failed to create mihomo.yaml file', error)
|
||||
}
|
||||
}
|
||||
|
||||
@ -36,6 +40,7 @@ export async function getControledMihomoConfig(force = false): Promise<Partial<I
|
||||
}
|
||||
|
||||
export async function patchControledMihomoConfig(patch: Partial<IMihomoConfig>): Promise<void> {
|
||||
controledMihomoWriteQueue = controledMihomoWriteQueue.then(async () => {
|
||||
const { controlDns = true, controlSniff = true } = await getAppConfig()
|
||||
|
||||
if (patch.hosts) {
|
||||
@ -61,4 +66,6 @@ export async function patchControledMihomoConfig(patch: Partial<IMihomoConfig>):
|
||||
|
||||
await generateProfile()
|
||||
await writeFile(controledMihomoConfigPath(), stringify(controledMihomoConfig), 'utf-8')
|
||||
})
|
||||
await controledMihomoWriteQueue
|
||||
}
|
||||
|
||||
@ -54,9 +54,12 @@ export async function addOverrideItem(item: Partial<IOverrideItem>): Promise<voi
|
||||
export async function removeOverrideItem(id: string): Promise<void> {
|
||||
const config = await getOverrideConfig()
|
||||
const item = await getOverrideItem(id)
|
||||
config.items = config.items?.filter((item) => item.id !== id)
|
||||
if (!item) return
|
||||
config.items = config.items?.filter((i) => i.id !== id)
|
||||
await setOverrideConfig(config)
|
||||
await rm(overridePath(id, item?.ext || 'js'))
|
||||
if (existsSync(overridePath(id, item.ext))) {
|
||||
await rm(overridePath(id, item.ext))
|
||||
}
|
||||
}
|
||||
|
||||
export async function createOverride(item: Partial<IOverrideItem>): Promise<IOverrideItem> {
|
||||
|
||||
@ -14,10 +14,13 @@ import { app } from 'electron'
|
||||
import { mihomoUpgradeConfig } from '../core/mihomoApi'
|
||||
|
||||
import i18next from 'i18next'
|
||||
import { createLogger } from '../utils/logger'
|
||||
|
||||
const profileLogger = createLogger('Profile')
|
||||
|
||||
let profileConfig: IProfileConfig
|
||||
let profileConfigWriteQueue: Promise<void> = Promise.resolve()
|
||||
let targetProfileId: string | null = null
|
||||
let changeProfileQueue: Promise<void> = Promise.resolve()
|
||||
|
||||
export async function getProfileConfig(force = false): Promise<IProfileConfig> {
|
||||
if (force || !profileConfig) {
|
||||
@ -60,37 +63,27 @@ export async function getProfileItem(id: string | undefined): Promise<IProfileIt
|
||||
}
|
||||
|
||||
export async function changeCurrentProfile(id: string): Promise<void> {
|
||||
// 使用队列确保 profile 切换串行执行,避免竞态条件
|
||||
changeProfileQueue = changeProfileQueue.then(async () => {
|
||||
const { current } = await getProfileConfig()
|
||||
|
||||
if (current === id && targetProfileId !== id) {
|
||||
return
|
||||
}
|
||||
|
||||
targetProfileId = id
|
||||
if (current === id) return
|
||||
|
||||
try {
|
||||
await updateProfileConfig((config) => {
|
||||
config.current = id
|
||||
return config
|
||||
})
|
||||
|
||||
if (targetProfileId !== id) {
|
||||
return
|
||||
}
|
||||
await restartCore()
|
||||
if (targetProfileId === id) {
|
||||
targetProfileId = null
|
||||
}
|
||||
} catch (e) {
|
||||
if (targetProfileId === id) {
|
||||
// 回滚配置
|
||||
await updateProfileConfig((config) => {
|
||||
config.current = current
|
||||
return config
|
||||
})
|
||||
targetProfileId = null
|
||||
throw e
|
||||
}
|
||||
}
|
||||
})
|
||||
await changeProfileQueue
|
||||
}
|
||||
|
||||
export async function updateProfileItem(item: IProfileItem): Promise<void> {
|
||||
@ -320,16 +313,16 @@ export async function setProfileStr(id: string, content: string): Promise<void>
|
||||
const { generateProfile } = await import('../core/factory')
|
||||
await generateProfile()
|
||||
await mihomoUpgradeConfig()
|
||||
console.log('[Profile] Config reloaded successfully using mihomoUpgradeConfig')
|
||||
profileLogger.info('Config reloaded successfully using mihomoUpgradeConfig')
|
||||
} catch (error) {
|
||||
console.error('[Profile] Failed to reload config with mihomoUpgradeConfig:', error)
|
||||
profileLogger.error('Failed to reload config with mihomoUpgradeConfig', error)
|
||||
try {
|
||||
console.log('[Profile] Falling back to restart core')
|
||||
profileLogger.info('Falling back to restart core')
|
||||
const { restartCore } = await import('../core/manager')
|
||||
await restartCore()
|
||||
console.log('[Profile] Core restarted successfully')
|
||||
profileLogger.info('Core restarted successfully')
|
||||
} catch (restartError) {
|
||||
console.error('[Profile] Failed to restart core:', restartError)
|
||||
profileLogger.error('Failed to restart core', restartError)
|
||||
throw restartError
|
||||
}
|
||||
}
|
||||
@ -365,12 +358,16 @@ export async function getProfile(id: string | undefined): Promise<IMihomoConfig>
|
||||
// attachment;filename=xxx.yaml; filename*=UTF-8''%xx%xx%xx
|
||||
function parseFilename(str: string): string {
|
||||
if (str.match(/filename\*=.*''/)) {
|
||||
const filename = decodeURIComponent(str.split(/filename\*=.*''/)[1])
|
||||
return filename
|
||||
} else {
|
||||
const filename = str.split('filename=')[1]
|
||||
return filename
|
||||
const parts = str.split(/filename\*=.*''/)
|
||||
if (parts[1]) {
|
||||
return decodeURIComponent(parts[1])
|
||||
}
|
||||
}
|
||||
const parts = str.split('filename=')
|
||||
if (parts[1]) {
|
||||
return parts[1].replace(/^["']|["']$/g, '')
|
||||
}
|
||||
return 'Remote File'
|
||||
}
|
||||
|
||||
// subscription-userinfo: upload=1234; download=2234; total=1024000; expire=2218532293
|
||||
|
||||
@ -21,9 +21,12 @@ import { deepMerge } from '../utils/merge'
|
||||
import vm from 'vm'
|
||||
import { existsSync, writeFileSync } from 'fs'
|
||||
import path from 'path'
|
||||
import { createLogger } from '../utils/logger'
|
||||
|
||||
let runtimeConfigStr: string
|
||||
let runtimeConfig: IMihomoConfig
|
||||
const factoryLogger = createLogger('Factory')
|
||||
|
||||
let runtimeConfigStr: string = ''
|
||||
let runtimeConfig: IMihomoConfig = {} as IMihomoConfig
|
||||
|
||||
// 辅助函数:处理带偏移量的规则
|
||||
function processRulesWithOffset(ruleStrings: string[], currentRules: string[], isAppend = false) {
|
||||
@ -132,7 +135,7 @@ export async function generateProfile(): Promise<string | undefined> {
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('读取或应用规则文件时出错:', error)
|
||||
factoryLogger.error('Failed to read or apply rule file', error)
|
||||
}
|
||||
|
||||
const profile = deepMerge(currentProfile, controledMihomoConfig)
|
||||
|
||||
@ -33,7 +33,7 @@ import {
|
||||
import chokidar from 'chokidar'
|
||||
import { readFile, rm, writeFile } from 'fs/promises'
|
||||
import { promisify } from 'util'
|
||||
import { mainWindow } from '..'
|
||||
import { mainWindow } from '../window'
|
||||
import path from 'path'
|
||||
import os from 'os'
|
||||
import { createWriteStream, existsSync } from 'fs'
|
||||
@ -977,7 +977,10 @@ async function getOriginDNS(): Promise<void> {
|
||||
async function setDNS(dns: string): Promise<void> {
|
||||
const service = await getDefaultService()
|
||||
const execPromise = promisify(exec)
|
||||
await execPromise(`networksetup -setdnsservers "${service}" ${dns}`)
|
||||
// networksetup 需要 root 权限,通过 osascript 请求管理员权限执行
|
||||
const shell = `networksetup -setdnsservers "${service}" ${dns}`
|
||||
const command = `do shell script "${shell}" with administrator privileges`
|
||||
await execPromise(`osascript -e '${command}'`)
|
||||
}
|
||||
|
||||
async function setPublicDNS(): Promise<void> {
|
||||
|
||||
@ -1,12 +1,15 @@
|
||||
import axios, { AxiosInstance } from 'axios'
|
||||
import { getAppConfig, getControledMihomoConfig } from '../config'
|
||||
import { mainWindow } from '..'
|
||||
import { mainWindow } from '../window'
|
||||
import WebSocket from 'ws'
|
||||
import { tray } from '../resolve/tray'
|
||||
import { calcTraffic } from '../utils/calc'
|
||||
import { getRuntimeConfig } from './factory'
|
||||
import { floatingWindow } from '../resolve/floatingWindow'
|
||||
import { getMihomoIpcPath } from './manager'
|
||||
import { createLogger } from '../utils/logger'
|
||||
|
||||
const mihomoApiLogger = createLogger('MihomoApi')
|
||||
|
||||
let axiosIns: AxiosInstance = null!
|
||||
let currentIpcPath: string = ''
|
||||
@ -30,7 +33,7 @@ export const getAxios = async (force: boolean = false): Promise<AxiosInstance> =
|
||||
}
|
||||
|
||||
currentIpcPath = dynamicIpcPath
|
||||
console.log(`[mihomoApi] Creating axios instance with path: ${dynamicIpcPath}`)
|
||||
mihomoApiLogger.info(`Creating axios instance with path: ${dynamicIpcPath}`)
|
||||
|
||||
axiosIns = axios.create({
|
||||
baseURL: `http://localhost`,
|
||||
@ -44,9 +47,9 @@ export const getAxios = async (force: boolean = false): Promise<AxiosInstance> =
|
||||
},
|
||||
(error) => {
|
||||
if (error.code === 'ENOENT') {
|
||||
console.debug(`[mihomoApi] Pipe not ready: ${error.config?.socketPath}`)
|
||||
mihomoApiLogger.debug(`Pipe not ready: ${error.config?.socketPath}`)
|
||||
} else {
|
||||
console.error(`[mihomoApi] Axios error with path ${dynamicIpcPath}:`, error.message)
|
||||
mihomoApiLogger.error(`Axios error with path ${dynamicIpcPath}: ${error.message}`)
|
||||
}
|
||||
|
||||
if (error.response && error.response.data) {
|
||||
@ -191,28 +194,28 @@ export const mihomoUpgradeUI = async (): Promise<void> => {
|
||||
}
|
||||
|
||||
export const mihomoUpgradeConfig = async (): Promise<void> => {
|
||||
console.log('[mihomoApi] mihomoUpgradeConfig called')
|
||||
mihomoApiLogger.info('mihomoUpgradeConfig called')
|
||||
|
||||
try {
|
||||
const instance = await getAxios()
|
||||
console.log('[mihomoApi] axios instance obtained')
|
||||
mihomoApiLogger.info('axios instance obtained')
|
||||
const { diffWorkDir = false } = await getAppConfig()
|
||||
const { current } = await import('../config').then((mod) => mod.getProfileConfig(true))
|
||||
const { mihomoWorkConfigPath } = await import('../utils/dirs')
|
||||
const configPath = diffWorkDir ? mihomoWorkConfigPath(current) : mihomoWorkConfigPath('work')
|
||||
console.log('[mihomoApi] config path:', configPath)
|
||||
mihomoApiLogger.info(`config path: ${configPath}`)
|
||||
const { existsSync } = await import('fs')
|
||||
if (!existsSync(configPath)) {
|
||||
console.log('[mihomoApi] config file does not exist, generating...')
|
||||
mihomoApiLogger.info('config file does not exist, generating...')
|
||||
const { generateProfile } = await import('./factory')
|
||||
await generateProfile()
|
||||
}
|
||||
const response = await instance.put('/configs?force=true', {
|
||||
path: configPath
|
||||
})
|
||||
console.log('[mihomoApi] config upgrade request completed', response?.status || 'no status')
|
||||
mihomoApiLogger.info(`config upgrade request completed ${response?.status || 'no status'}`)
|
||||
} catch (error) {
|
||||
console.error('[mihomoApi] Failed to upgrade config:', error)
|
||||
mihomoApiLogger.error('Failed to upgrade config', error)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
@ -255,7 +258,7 @@ const mihomoTraffic = async (): Promise<void> => {
|
||||
const dynamicIpcPath = getMihomoIpcPath()
|
||||
const wsUrl = `ws+unix:${dynamicIpcPath}:/traffic`
|
||||
|
||||
console.log(`[mihomoApi] Creating traffic WebSocket with URL: ${wsUrl}`)
|
||||
mihomoApiLogger.info(`Creating traffic WebSocket with URL: ${wsUrl}`)
|
||||
mihomoTrafficWs = new WebSocket(wsUrl)
|
||||
|
||||
mihomoTrafficWs.onmessage = async (e): Promise<void> => {
|
||||
@ -286,7 +289,7 @@ const mihomoTraffic = async (): Promise<void> => {
|
||||
}
|
||||
|
||||
mihomoTrafficWs.onerror = (error): void => {
|
||||
console.error(`[mihomoApi] Traffic WebSocket error:`, error)
|
||||
mihomoApiLogger.error('Traffic WebSocket error', error)
|
||||
if (mihomoTrafficWs) {
|
||||
mihomoTrafficWs.close()
|
||||
mihomoTrafficWs = null
|
||||
|
||||
@ -5,13 +5,17 @@ import { getAppConfig } from '../config'
|
||||
export async function subStoreSubs(): Promise<ISubStoreSub[]> {
|
||||
const { useCustomSubStore = false, customSubStoreUrl = '' } = await getAppConfig()
|
||||
const baseUrl = useCustomSubStore ? customSubStoreUrl : `http://127.0.0.1:${subStorePort}`
|
||||
const res = await chromeRequest.get<{ data: ISubStoreSub[] }>(`${baseUrl}/api/subs`, { responseType: 'json' })
|
||||
const res = await chromeRequest.get<{ data: ISubStoreSub[] }>(`${baseUrl}/api/subs`, {
|
||||
responseType: 'json'
|
||||
})
|
||||
return res.data.data
|
||||
}
|
||||
|
||||
export async function subStoreCollections(): Promise<ISubStoreSub[]> {
|
||||
const { useCustomSubStore = false, customSubStoreUrl = '' } = await getAppConfig()
|
||||
const baseUrl = useCustomSubStore ? customSubStoreUrl : `http://127.0.0.1:${subStorePort}`
|
||||
const res = await chromeRequest.get<{ data: ISubStoreSub[] }>(`${baseUrl}/api/collections`, { responseType: 'json' })
|
||||
const res = await chromeRequest.get<{ data: ISubStoreSub[] }>(`${baseUrl}/api/collections`, {
|
||||
responseType: 'json'
|
||||
})
|
||||
return res.data.data
|
||||
}
|
||||
|
||||
32
src/main/deeplink.ts
Normal file
32
src/main/deeplink.ts
Normal file
@ -0,0 +1,32 @@
|
||||
import { Notification } from 'electron'
|
||||
import i18next from 'i18next'
|
||||
import { addProfileItem } from './config'
|
||||
import { mainWindow } from './window'
|
||||
import { safeShowErrorBox } from './utils/init'
|
||||
|
||||
export async function handleDeepLink(url: string): Promise<void> {
|
||||
if (!url.startsWith('clash://') && !url.startsWith('mihomo://')) return
|
||||
|
||||
const urlObj = new URL(url)
|
||||
switch (urlObj.host) {
|
||||
case 'install-config': {
|
||||
try {
|
||||
const profileUrl = urlObj.searchParams.get('url')
|
||||
const profileName = urlObj.searchParams.get('name')
|
||||
if (!profileUrl) {
|
||||
throw new Error(i18next.t('profiles.error.urlParamMissing'))
|
||||
}
|
||||
await addProfileItem({
|
||||
type: 'remote',
|
||||
name: profileName ?? undefined,
|
||||
url: profileUrl
|
||||
})
|
||||
mainWindow?.webContents.send('profileConfigUpdated')
|
||||
new Notification({ title: i18next.t('profiles.notification.importSuccess') }).show()
|
||||
} catch (e) {
|
||||
safeShowErrorBox('profiles.error.importFailed', `${url}\n${e}`)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,64 +1,46 @@
|
||||
import { electronApp, optimizer, is } from '@electron-toolkit/utils'
|
||||
import { electronApp, optimizer } from '@electron-toolkit/utils'
|
||||
import { app, dialog } from 'electron'
|
||||
import { registerIpcMainHandlers } from './utils/ipc'
|
||||
import windowStateKeeper from 'electron-window-state'
|
||||
import { app, shell, BrowserWindow, Menu, dialog, Notification, powerMonitor } from 'electron'
|
||||
import { addProfileItem, getAppConfig, patchAppConfig } from './config'
|
||||
import { getAppConfig, patchAppConfig } from './config'
|
||||
import {
|
||||
quitWithoutCore,
|
||||
startCore,
|
||||
stopCore,
|
||||
checkAdminRestartForTun,
|
||||
checkHighPrivilegeCore,
|
||||
restartAsAdmin,
|
||||
initAdminStatus
|
||||
initAdminStatus,
|
||||
checkAdminPrivileges
|
||||
} from './core/manager'
|
||||
import { triggerSysProxy } from './sys/sysproxy'
|
||||
import icon from '../../resources/icon.png?asset'
|
||||
import { createTray, hideDockIcon, showDockIcon } from './resolve/tray'
|
||||
import { createTray } from './resolve/tray'
|
||||
import { init, initBasic, safeShowErrorBox } from './utils/init'
|
||||
import { join } from 'path'
|
||||
import { initShortcut } from './resolve/shortcut'
|
||||
import { spawn, exec } from 'child_process'
|
||||
import { promisify } from 'util'
|
||||
import { stat } from 'fs/promises'
|
||||
import { initProfileUpdater } from './core/profileUpdater'
|
||||
import { existsSync } from 'fs'
|
||||
import { exePath } from './utils/dirs'
|
||||
import { startMonitor } from './resolve/trafficMonitor'
|
||||
import { showFloatingWindow } from './resolve/floatingWindow'
|
||||
import { initI18n } from '../shared/i18n'
|
||||
import i18next from 'i18next'
|
||||
import { logger } from './utils/logger'
|
||||
import { createLogger } from './utils/logger'
|
||||
import { initWebdavBackupScheduler } from './resolve/backup'
|
||||
|
||||
async function fixUserDataPermissions(): Promise<void> {
|
||||
if (process.platform !== 'darwin') return
|
||||
const mainLogger = createLogger('Main')
|
||||
import {
|
||||
createWindow,
|
||||
mainWindow,
|
||||
showMainWindow,
|
||||
triggerMainWindow,
|
||||
closeMainWindow
|
||||
} from './window'
|
||||
import { handleDeepLink } from './deeplink'
|
||||
import {
|
||||
fixUserDataPermissions,
|
||||
setupPlatformSpecifics,
|
||||
setupAppLifecycle,
|
||||
getSystemLanguage
|
||||
} from './lifecycle'
|
||||
|
||||
const userDataPath = app.getPath('userData')
|
||||
if (!existsSync(userDataPath)) return
|
||||
|
||||
try {
|
||||
const stats = await stat(userDataPath)
|
||||
const currentUid = process.getuid?.() || 0
|
||||
|
||||
if (stats.uid === 0 && currentUid !== 0) {
|
||||
const execPromise = promisify(exec)
|
||||
const username = process.env.USER || process.env.LOGNAME
|
||||
if (username) {
|
||||
await execPromise(`chown -R "${username}:staff" "${userDataPath}"`)
|
||||
await execPromise(`chmod -R u+rwX "${userDataPath}"`)
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
let quitTimeout: NodeJS.Timeout | null = null
|
||||
export let mainWindow: BrowserWindow | null = null
|
||||
export { mainWindow, showMainWindow, triggerMainWindow, closeMainWindow }
|
||||
|
||||
const gotTheLock = app.requestSingleInstanceLock()
|
||||
|
||||
if (!gotTheLock) {
|
||||
app.quit()
|
||||
}
|
||||
@ -72,53 +54,22 @@ initApp().catch((e) => {
|
||||
app.quit()
|
||||
})
|
||||
|
||||
export function customRelaunch(): void {
|
||||
const script = `while kill -0 ${process.pid} 2>/dev/null; do
|
||||
sleep 0.1
|
||||
done
|
||||
${process.argv.join(' ')} & disown
|
||||
exit
|
||||
`
|
||||
spawn('sh', ['-c', `"${script}"`], {
|
||||
shell: true,
|
||||
detached: true,
|
||||
stdio: 'ignore'
|
||||
})
|
||||
}
|
||||
setupPlatformSpecifics()
|
||||
|
||||
if (process.platform === 'linux') {
|
||||
app.relaunch = customRelaunch
|
||||
}
|
||||
|
||||
if (process.platform === 'win32' && !exePath().startsWith('C')) {
|
||||
// https://github.com/electron/electron/issues/43278
|
||||
// https://github.com/electron/electron/issues/36698
|
||||
app.commandLine.appendSwitch('in-process-gpu')
|
||||
}
|
||||
|
||||
// 运行内核检测
|
||||
async function checkHighPrivilegeCoreEarly(): Promise<void> {
|
||||
if (process.platform !== 'win32') {
|
||||
return
|
||||
}
|
||||
if (process.platform !== 'win32') return
|
||||
|
||||
try {
|
||||
await initBasic()
|
||||
|
||||
const { checkAdminPrivileges } = await import('./core/manager')
|
||||
const isCurrentAppAdmin = await checkAdminPrivileges()
|
||||
|
||||
if (isCurrentAppAdmin) {
|
||||
console.log('Current app is running as administrator, skipping privilege check')
|
||||
return
|
||||
}
|
||||
if (isCurrentAppAdmin) return
|
||||
|
||||
const hasHighPrivilegeCore = await checkHighPrivilegeCore()
|
||||
if (hasHighPrivilegeCore) {
|
||||
if (!hasHighPrivilegeCore) return
|
||||
|
||||
try {
|
||||
const appConfig = await getAppConfig()
|
||||
const language =
|
||||
appConfig.language || (app.getLocale().startsWith('zh') ? 'zh-CN' : 'en-US')
|
||||
const language = appConfig.language || (app.getLocale().startsWith('zh') ? 'zh-CN' : 'en-US')
|
||||
await initI18n({ lng: language })
|
||||
} catch {
|
||||
await initI18n({ lng: 'zh-CN' })
|
||||
@ -135,7 +86,6 @@ async function checkHighPrivilegeCoreEarly(): Promise<void> {
|
||||
|
||||
if (choice === 0) {
|
||||
try {
|
||||
// Windows 平台重启应用获取管理员权限
|
||||
await restartAsAdmin(false)
|
||||
process.exit(0)
|
||||
} catch (error) {
|
||||
@ -145,12 +95,26 @@ async function checkHighPrivilegeCoreEarly(): Promise<void> {
|
||||
} else {
|
||||
process.exit(0)
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to check high privilege core:', e)
|
||||
mainLogger.error('Failed to check high privilege core', e)
|
||||
}
|
||||
}
|
||||
|
||||
async function initHardwareAcceleration(): Promise<void> {
|
||||
try {
|
||||
await initBasic()
|
||||
const { disableHardwareAcceleration = false } = await getAppConfig()
|
||||
if (disableHardwareAcceleration) {
|
||||
app.disableHardwareAcceleration()
|
||||
}
|
||||
} catch (e) {
|
||||
mainLogger.warn('Failed to read hardware acceleration config', e)
|
||||
}
|
||||
}
|
||||
|
||||
initHardwareAcceleration()
|
||||
setupAppLifecycle()
|
||||
|
||||
app.on('second-instance', async (_event, commandline) => {
|
||||
showMainWindow()
|
||||
const url = commandline.pop()
|
||||
@ -164,58 +128,16 @@ app.on('open-url', async (_event, url) => {
|
||||
await handleDeepLink(url)
|
||||
})
|
||||
|
||||
app.on('before-quit', async (e) => {
|
||||
e.preventDefault()
|
||||
triggerSysProxy(false)
|
||||
await stopCore()
|
||||
app.exit()
|
||||
})
|
||||
|
||||
powerMonitor.on('shutdown', async () => {
|
||||
triggerSysProxy(false)
|
||||
await stopCore()
|
||||
app.exit()
|
||||
})
|
||||
|
||||
// 获取系统语言
|
||||
function getSystemLanguage(): 'zh-CN' | 'en-US' {
|
||||
const locale = app.getLocale()
|
||||
return locale.startsWith('zh') ? 'zh-CN' : 'en-US'
|
||||
}
|
||||
|
||||
// 硬件加速设置
|
||||
async function initHardwareAcceleration(): Promise<void> {
|
||||
try {
|
||||
await initBasic()
|
||||
const { disableHardwareAcceleration = false } = await getAppConfig()
|
||||
if (disableHardwareAcceleration) {
|
||||
app.disableHardwareAcceleration()
|
||||
}
|
||||
} catch (e) {
|
||||
console.warn('Failed to read hardware acceleration config:', e)
|
||||
}
|
||||
}
|
||||
|
||||
initHardwareAcceleration()
|
||||
|
||||
// This method will be called when Electron has finished
|
||||
// initialization and is ready to create browser windows.
|
||||
// Some APIs can only be used after this event occurs.
|
||||
app.whenReady().then(async () => {
|
||||
// Set app user model id for windows
|
||||
electronApp.setAppUserModelId('party.mihomo.app')
|
||||
|
||||
await initBasic()
|
||||
|
||||
await checkHighPrivilegeCoreEarly()
|
||||
|
||||
await initAdminStatus()
|
||||
|
||||
try {
|
||||
await init()
|
||||
|
||||
const appConfig = await getAppConfig()
|
||||
// 如果配置中没有语言设置,则使用系统语言
|
||||
if (!appConfig.language) {
|
||||
const systemLanguage = getSystemLanguage()
|
||||
await patchAppConfig({ language: systemLanguage })
|
||||
@ -231,28 +153,27 @@ app.whenReady().then(async () => {
|
||||
const [startPromise] = await startCore()
|
||||
startPromise.then(async () => {
|
||||
await initProfileUpdater()
|
||||
await initWebdavBackupScheduler() // 初始化 WebDAV 定时备份任务
|
||||
// 上次是否为了开启 TUN 而重启
|
||||
await initWebdavBackupScheduler()
|
||||
await checkAdminRestartForTun()
|
||||
})
|
||||
} catch (e) {
|
||||
safeShowErrorBox('mihomo.error.coreStartFailed', `${e}`)
|
||||
}
|
||||
|
||||
try {
|
||||
await startMonitor()
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
|
||||
// Default open or close DevTools by F12 in development
|
||||
// and ignore CommandOrControl + R in production.
|
||||
// see https://github.com/alex8088/electron-toolkit/tree/master/packages/utils
|
||||
app.on('browser-window-created', (_, window) => {
|
||||
optimizer.watchWindowShortcuts(window)
|
||||
})
|
||||
|
||||
const { showFloatingWindow: showFloating = false, disableTray = false } = await getAppConfig()
|
||||
registerIpcMainHandlers()
|
||||
await createWindow()
|
||||
|
||||
if (showFloating) {
|
||||
try {
|
||||
await showFloatingWindow()
|
||||
@ -260,192 +181,14 @@ app.whenReady().then(async () => {
|
||||
await logger.error('Failed to create floating window on startup', error)
|
||||
}
|
||||
}
|
||||
|
||||
if (!disableTray) {
|
||||
await createTray()
|
||||
}
|
||||
|
||||
await initShortcut()
|
||||
app.on('activate', function () {
|
||||
// On macOS it's common to re-create a window in the app when the
|
||||
// dock icon is clicked and there are no other windows open.
|
||||
|
||||
app.on('activate', () => {
|
||||
showMainWindow()
|
||||
})
|
||||
})
|
||||
|
||||
async function handleDeepLink(url: string): Promise<void> {
|
||||
if (!url.startsWith('clash://') && !url.startsWith('mihomo://')) return
|
||||
|
||||
const urlObj = new URL(url)
|
||||
switch (urlObj.host) {
|
||||
case 'install-config': {
|
||||
try {
|
||||
const profileUrl = urlObj.searchParams.get('url')
|
||||
const profileName = urlObj.searchParams.get('name')
|
||||
if (!profileUrl) {
|
||||
throw new Error(i18next.t('profiles.error.urlParamMissing'))
|
||||
}
|
||||
await addProfileItem({
|
||||
type: 'remote',
|
||||
name: profileName ?? undefined,
|
||||
url: profileUrl
|
||||
})
|
||||
mainWindow?.webContents.send('profileConfigUpdated')
|
||||
new Notification({ title: i18next.t('profiles.notification.importSuccess') }).show()
|
||||
break
|
||||
} catch (e) {
|
||||
safeShowErrorBox('profiles.error.importFailed', `${url}\n${e}`)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function createWindow(): Promise<void> {
|
||||
const { useWindowFrame = false } = await getAppConfig()
|
||||
const mainWindowState = windowStateKeeper({
|
||||
defaultWidth: 800,
|
||||
defaultHeight: 600,
|
||||
file: 'window-state.json'
|
||||
})
|
||||
// https://github.com/electron/electron/issues/16521#issuecomment-582955104
|
||||
Menu.setApplicationMenu(null)
|
||||
mainWindow = new BrowserWindow({
|
||||
minWidth: 800,
|
||||
minHeight: 600,
|
||||
width: mainWindowState.width,
|
||||
height: mainWindowState.height,
|
||||
x: mainWindowState.x,
|
||||
y: mainWindowState.y,
|
||||
show: false,
|
||||
frame: useWindowFrame,
|
||||
fullscreenable: false,
|
||||
titleBarStyle: useWindowFrame ? 'default' : 'hidden',
|
||||
titleBarOverlay: useWindowFrame
|
||||
? false
|
||||
: {
|
||||
height: 49
|
||||
},
|
||||
autoHideMenuBar: true,
|
||||
...(process.platform === 'linux' ? { icon: icon } : {}),
|
||||
webPreferences: {
|
||||
preload: join(__dirname, '../preload/index.js'),
|
||||
spellcheck: false,
|
||||
sandbox: false,
|
||||
devTools: true
|
||||
}
|
||||
})
|
||||
mainWindowState.manage(mainWindow)
|
||||
mainWindow.on('ready-to-show', async () => {
|
||||
const {
|
||||
silentStart = false,
|
||||
autoQuitWithoutCore = false,
|
||||
autoQuitWithoutCoreDelay = 60
|
||||
} = await getAppConfig()
|
||||
if (autoQuitWithoutCore && !mainWindow?.isVisible()) {
|
||||
if (quitTimeout) {
|
||||
clearTimeout(quitTimeout)
|
||||
}
|
||||
quitTimeout = setTimeout(async () => {
|
||||
await quitWithoutCore()
|
||||
}, autoQuitWithoutCoreDelay * 1000)
|
||||
}
|
||||
if (!silentStart) {
|
||||
if (quitTimeout) {
|
||||
clearTimeout(quitTimeout)
|
||||
}
|
||||
mainWindow?.show()
|
||||
mainWindow?.focusOnWebView()
|
||||
}
|
||||
})
|
||||
mainWindow.webContents.on('did-fail-load', () => {
|
||||
mainWindow?.webContents.reload()
|
||||
})
|
||||
|
||||
mainWindow.on('show', () => {
|
||||
showDockIcon()
|
||||
})
|
||||
|
||||
mainWindow.on('close', async (event) => {
|
||||
event.preventDefault()
|
||||
mainWindow?.hide()
|
||||
const {
|
||||
autoQuitWithoutCore = false,
|
||||
autoQuitWithoutCoreDelay = 60,
|
||||
useDockIcon = true
|
||||
} = await getAppConfig()
|
||||
if (!useDockIcon) {
|
||||
hideDockIcon()
|
||||
}
|
||||
if (autoQuitWithoutCore) {
|
||||
if (quitTimeout) {
|
||||
clearTimeout(quitTimeout)
|
||||
}
|
||||
quitTimeout = setTimeout(async () => {
|
||||
await quitWithoutCore()
|
||||
}, autoQuitWithoutCoreDelay * 1000)
|
||||
}
|
||||
})
|
||||
|
||||
mainWindow.on('resized', () => {
|
||||
if (mainWindow) mainWindowState.saveState(mainWindow)
|
||||
})
|
||||
|
||||
mainWindow.on('move', () => {
|
||||
if (mainWindow) mainWindowState.saveState(mainWindow)
|
||||
})
|
||||
|
||||
mainWindow.on('session-end', async () => {
|
||||
triggerSysProxy(false)
|
||||
await stopCore()
|
||||
})
|
||||
|
||||
mainWindow.webContents.setWindowOpenHandler((details) => {
|
||||
shell.openExternal(details.url)
|
||||
return { action: 'deny' }
|
||||
})
|
||||
|
||||
// 在开发模式下自动打开 DevTools
|
||||
if (is.dev) {
|
||||
mainWindow.webContents.openDevTools()
|
||||
}
|
||||
|
||||
// HMR for renderer base on electron-vite cli.
|
||||
// Load the remote URL for development or the local html file for production.
|
||||
if (is.dev && process.env['ELECTRON_RENDERER_URL']) {
|
||||
mainWindow.loadURL(process.env['ELECTRON_RENDERER_URL'])
|
||||
} else {
|
||||
mainWindow.loadFile(join(__dirname, '../renderer/index.html'))
|
||||
}
|
||||
}
|
||||
|
||||
export function triggerMainWindow(force?: boolean): void {
|
||||
if (mainWindow) {
|
||||
getAppConfig()
|
||||
.then(({ triggerMainWindowBehavior = 'toggle' }) => {
|
||||
if (force === true || triggerMainWindowBehavior === 'toggle') {
|
||||
if (mainWindow?.isVisible()) {
|
||||
closeMainWindow()
|
||||
} else {
|
||||
showMainWindow()
|
||||
}
|
||||
} else {
|
||||
showMainWindow()
|
||||
}
|
||||
})
|
||||
.catch(showMainWindow)
|
||||
}
|
||||
}
|
||||
|
||||
export function showMainWindow(): void {
|
||||
if (mainWindow) {
|
||||
if (quitTimeout) {
|
||||
clearTimeout(quitTimeout)
|
||||
}
|
||||
mainWindow.show()
|
||||
mainWindow.focusOnWebView()
|
||||
}
|
||||
}
|
||||
|
||||
export function closeMainWindow(): void {
|
||||
if (mainWindow) {
|
||||
mainWindow.close()
|
||||
}
|
||||
}
|
||||
|
||||
74
src/main/lifecycle.ts
Normal file
74
src/main/lifecycle.ts
Normal file
@ -0,0 +1,74 @@
|
||||
import { app, powerMonitor } from 'electron'
|
||||
import { spawn, exec } from 'child_process'
|
||||
import { promisify } from 'util'
|
||||
import { stat } from 'fs/promises'
|
||||
import { existsSync } from 'fs'
|
||||
import { stopCore } from './core/manager'
|
||||
import { triggerSysProxy } from './sys/sysproxy'
|
||||
import { exePath } from './utils/dirs'
|
||||
|
||||
export function customRelaunch(): void {
|
||||
const script = `while kill -0 ${process.pid} 2>/dev/null; do
|
||||
sleep 0.1
|
||||
done
|
||||
${process.argv.join(' ')} & disown
|
||||
exit
|
||||
`
|
||||
spawn('sh', ['-c', script], {
|
||||
detached: true,
|
||||
stdio: 'ignore'
|
||||
})
|
||||
}
|
||||
|
||||
export async function fixUserDataPermissions(): Promise<void> {
|
||||
if (process.platform !== 'darwin') return
|
||||
|
||||
const userDataPath = app.getPath('userData')
|
||||
if (!existsSync(userDataPath)) return
|
||||
|
||||
try {
|
||||
const stats = await stat(userDataPath)
|
||||
const currentUid = process.getuid?.() || 0
|
||||
|
||||
if (stats.uid === 0 && currentUid !== 0) {
|
||||
const execPromise = promisify(exec)
|
||||
const username = process.env.USER || process.env.LOGNAME
|
||||
if (username) {
|
||||
await execPromise(`chown -R "${username}:staff" "${userDataPath}"`)
|
||||
await execPromise(`chmod -R u+rwX "${userDataPath}"`)
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// ignore
|
||||
}
|
||||
}
|
||||
|
||||
export function setupPlatformSpecifics(): void {
|
||||
if (process.platform === 'linux') {
|
||||
app.relaunch = customRelaunch
|
||||
}
|
||||
|
||||
if (process.platform === 'win32' && !exePath().startsWith('C')) {
|
||||
app.commandLine.appendSwitch('in-process-gpu')
|
||||
}
|
||||
}
|
||||
|
||||
export function setupAppLifecycle(): void {
|
||||
app.on('before-quit', async (e) => {
|
||||
e.preventDefault()
|
||||
await triggerSysProxy(false)
|
||||
await stopCore()
|
||||
app.exit()
|
||||
})
|
||||
|
||||
powerMonitor.on('shutdown', async () => {
|
||||
triggerSysProxy(false)
|
||||
await stopCore()
|
||||
app.exit()
|
||||
})
|
||||
}
|
||||
|
||||
export function getSystemLanguage(): 'zh-CN' | 'en-US' {
|
||||
const locale = app.getLocale()
|
||||
return locale.startsWith('zh') ? 'zh-CN' : 'en-US'
|
||||
}
|
||||
@ -1,5 +1,5 @@
|
||||
import { app, globalShortcut, ipcMain, Notification } from 'electron'
|
||||
import { mainWindow, triggerMainWindow } from '..'
|
||||
import { mainWindow, triggerMainWindow } from '../window'
|
||||
import {
|
||||
getAppConfig,
|
||||
getControledMihomoConfig,
|
||||
|
||||
@ -5,7 +5,7 @@ import * as chromeRequest from '../utils/chromeRequest'
|
||||
import AdmZip from 'adm-zip'
|
||||
import { getControledMihomoConfig } from '../config'
|
||||
import { existsSync } from 'fs'
|
||||
import { mainWindow } from '..'
|
||||
import { mainWindow } from '../window'
|
||||
import { floatingWindow } from './floatingWindow'
|
||||
import { t } from 'i18next'
|
||||
|
||||
|
||||
@ -23,7 +23,7 @@ import {
|
||||
getTrayIconStatus,
|
||||
calculateTrayIconStatus
|
||||
} from '../core/mihomoApi'
|
||||
import { mainWindow, showMainWindow, triggerMainWindow } from '..'
|
||||
import { mainWindow, showMainWindow, triggerMainWindow } from '../window'
|
||||
import { app, clipboard, ipcMain, Menu, nativeImage, shell, Tray } from 'electron'
|
||||
import { dataDir, logDir, mihomoCoreDir, mihomoWorkDir } from '../utils/dirs'
|
||||
import { triggerSysProxy } from '../sys/sysproxy'
|
||||
@ -540,8 +540,8 @@ export function updateTrayIconImmediate(sysProxyEnabled: boolean, tunEnabled: bo
|
||||
} else if (process.platform === 'linux') {
|
||||
tray.setImage(iconPath)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('更新托盘图标失败:', error)
|
||||
} catch {
|
||||
// Failed to update tray icon
|
||||
}
|
||||
})
|
||||
}
|
||||
@ -563,7 +563,7 @@ export async function updateTrayIcon(): Promise<void> {
|
||||
} else if (process.platform === 'linux') {
|
||||
tray.setImage(iconPath)
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('更新托盘图标失败:', error)
|
||||
} catch {
|
||||
// Failed to update tray icon
|
||||
}
|
||||
}
|
||||
|
||||
@ -2,7 +2,7 @@ import { exec } from 'child_process'
|
||||
import { promisify } from 'util'
|
||||
import { getAppConfig, patchControledMihomoConfig } from '../config'
|
||||
import { patchMihomoConfig } from '../core/mihomoApi'
|
||||
import { mainWindow } from '..'
|
||||
import { mainWindow } from '../window'
|
||||
import { ipcMain, net } from 'electron'
|
||||
import { getDefaultDevice } from '../core/manager'
|
||||
|
||||
|
||||
@ -10,14 +10,15 @@ import axios from 'axios'
|
||||
import fs from 'fs'
|
||||
import { proxyLogger } from '../utils/logger'
|
||||
|
||||
let defaultBypass: string[]
|
||||
let triggerSysProxyTimer: NodeJS.Timeout | null = null
|
||||
const helperSocketPath = '/tmp/mihomo-party-helper.sock'
|
||||
|
||||
if (process.platform === 'linux')
|
||||
defaultBypass = ['localhost', '127.0.0.1', '192.168.0.0/16', '10.0.0.0/8', '172.16.0.0/12', '::1']
|
||||
if (process.platform === 'darwin')
|
||||
defaultBypass = [
|
||||
const defaultBypass: string[] = (() => {
|
||||
switch (process.platform) {
|
||||
case 'linux':
|
||||
return ['localhost', '127.0.0.1', '192.168.0.0/16', '10.0.0.0/8', '172.16.0.0/12', '::1']
|
||||
case 'darwin':
|
||||
return [
|
||||
'127.0.0.1',
|
||||
'192.168.0.0/16',
|
||||
'10.0.0.0/8',
|
||||
@ -27,8 +28,8 @@ if (process.platform === 'darwin')
|
||||
'*.crashlytics.com',
|
||||
'<local>'
|
||||
]
|
||||
if (process.platform === 'win32')
|
||||
defaultBypass = [
|
||||
case 'win32':
|
||||
return [
|
||||
'localhost',
|
||||
'127.*',
|
||||
'192.168.*',
|
||||
@ -51,6 +52,10 @@ if (process.platform === 'win32')
|
||||
'172.31.*',
|
||||
'<local>'
|
||||
]
|
||||
default:
|
||||
return ['localhost', '127.0.0.1', '192.168.0.0/16', '10.0.0.0/8', '172.16.0.0/12', '::1']
|
||||
}
|
||||
})()
|
||||
|
||||
export async function triggerSysProxy(enable: boolean): Promise<void> {
|
||||
if (net.isOnline()) {
|
||||
|
||||
@ -9,6 +9,9 @@ import { join } from 'path'
|
||||
import { existsSync, rmSync } from 'fs'
|
||||
import { createGunzip } from 'zlib'
|
||||
import { stopCore } from '../core/manager'
|
||||
import { createLogger } from './logger'
|
||||
|
||||
const log = createLogger('GitHub')
|
||||
|
||||
export interface GitHubTag {
|
||||
name: string
|
||||
@ -60,13 +63,13 @@ export async function getGitHubTags(
|
||||
const cache = versionCache.get(cacheKey)!
|
||||
// 检查缓存是否过期
|
||||
if (Date.now() - cache.timestamp < CACHE_EXPIRY) {
|
||||
console.log(`[GitHub] Returning cached tags for ${owner}/${repo}`)
|
||||
log.debug(`Returning cached tags for ${owner}/${repo}`)
|
||||
return cache.data
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
console.log(`[GitHub] Fetching tags for ${owner}/${repo}`)
|
||||
log.debug(`Fetching tags for ${owner}/${repo}`)
|
||||
const response = await chromeRequest.get<GitHubTag[]>(
|
||||
`${GITHUB_API_CONFIG.BASE_URL}/repos/${owner}/${repo}/tags?per_page=${GITHUB_API_CONFIG.TAGS_PER_PAGE}`,
|
||||
{
|
||||
@ -85,10 +88,10 @@ export async function getGitHubTags(
|
||||
timestamp: Date.now()
|
||||
})
|
||||
|
||||
console.log(`[GitHub] Successfully fetched ${response.data.length} tags for ${owner}/${repo}`)
|
||||
log.debug(`Successfully fetched ${response.data.length} tags for ${owner}/${repo}`)
|
||||
return response.data
|
||||
} catch (error) {
|
||||
console.error(`[GitHub] Failed to fetch tags for ${owner}/${repo}:`, error)
|
||||
log.error(`Failed to fetch tags for ${owner}/${repo}`, error)
|
||||
if (error instanceof Error) {
|
||||
throw new Error(`GitHub API error: ${error.message}`)
|
||||
}
|
||||
@ -105,7 +108,7 @@ export function clearVersionCache(owner: string, repo: string): void {
|
||||
const cacheKey = `${owner}/${repo}`
|
||||
const hasCache = versionCache.has(cacheKey)
|
||||
versionCache.delete(cacheKey)
|
||||
console.log(`[GitHub] Cache ${hasCache ? 'cleared' : 'not found'} for ${owner}/${repo}`)
|
||||
log.debug(`Cache ${hasCache ? 'cleared' : 'not found'} for ${owner}/${repo}`)
|
||||
}
|
||||
|
||||
/**
|
||||
@ -115,16 +118,16 @@ export function clearVersionCache(owner: string, repo: string): void {
|
||||
*/
|
||||
async function downloadGitHubAsset(url: string, outputPath: string): Promise<void> {
|
||||
try {
|
||||
console.log(`[GitHub] Downloading asset from ${url}`)
|
||||
log.debug(`Downloading asset from ${url}`)
|
||||
const response = await chromeRequest.get(url, {
|
||||
responseType: 'arraybuffer',
|
||||
timeout: 30000
|
||||
})
|
||||
|
||||
await writeFile(outputPath, Buffer.from(response.data as Buffer))
|
||||
console.log(`[GitHub] Successfully downloaded asset to ${outputPath}`)
|
||||
log.debug(`Successfully downloaded asset to ${outputPath}`)
|
||||
} catch (error) {
|
||||
console.error(`[GitHub] Failed to download asset from ${url}:`, error)
|
||||
log.error(`Failed to download asset from ${url}`, error)
|
||||
if (error instanceof Error) {
|
||||
throw new Error(`Download error: ${error.message}`)
|
||||
}
|
||||
@ -138,7 +141,7 @@ async function downloadGitHubAsset(url: string, outputPath: string): Promise<voi
|
||||
*/
|
||||
export async function installMihomoCore(version: string): Promise<void> {
|
||||
try {
|
||||
console.log(`[GitHub] Installing mihomo core version ${version}`)
|
||||
log.info(`Installing mihomo core version ${version}`)
|
||||
|
||||
const plat = platform()
|
||||
const arch = process.arch
|
||||
@ -163,7 +166,7 @@ export async function installMihomoCore(version: string): Promise<void> {
|
||||
|
||||
// 如果目标文件已存在,先停止核心
|
||||
if (existsSync(targetPath)) {
|
||||
console.log('[GitHub] Stopping core before extracting new core file')
|
||||
log.debug('Stopping core before extracting new core file')
|
||||
// 先停止核心
|
||||
await stopCore(true)
|
||||
}
|
||||
@ -173,26 +176,26 @@ export async function installMihomoCore(version: string): Promise<void> {
|
||||
|
||||
// 解压文件
|
||||
if (urlExt === 'zip') {
|
||||
console.log(`[GitHub] Extracting ZIP file ${tempZip}`)
|
||||
log.debug(`Extracting ZIP file ${tempZip}`)
|
||||
const zip = new AdmZip(tempZip)
|
||||
const entries = zip.getEntries()
|
||||
const entry = entries.find((e) => e.entryName.includes(exeFile))
|
||||
|
||||
if (entry) {
|
||||
zip.extractEntryTo(entry, coreDir, false, true, false, targetFile)
|
||||
console.log(`[GitHub] Successfully extracted ${exeFile} to ${targetPath}`)
|
||||
log.debug(`Successfully extracted ${exeFile} to ${targetPath}`)
|
||||
} else {
|
||||
throw new Error(`Executable file not found in zip: ${exeFile}`)
|
||||
}
|
||||
} else {
|
||||
// 处理.gz 文件
|
||||
console.log(`[GitHub] Extracting GZ file ${tempZip}`)
|
||||
log.debug(`Extracting GZ file ${tempZip}`)
|
||||
const readStream = createReadStream(tempZip)
|
||||
const writeStream = createWriteStream(targetPath)
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const onError = (error: Error) => {
|
||||
console.error('[GitHub] Gzip decompression failed:', error.message)
|
||||
log.error('Gzip decompression failed', error)
|
||||
reject(new Error(`Gzip decompression failed: ${error.message}`))
|
||||
}
|
||||
|
||||
@ -200,12 +203,12 @@ export async function installMihomoCore(version: string): Promise<void> {
|
||||
.pipe(createGunzip().on('error', onError))
|
||||
.pipe(writeStream)
|
||||
.on('finish', () => {
|
||||
console.log('[GitHub] Gunzip finished')
|
||||
log.debug('Gunzip finished')
|
||||
try {
|
||||
execSync(`chmod 755 ${targetPath}`)
|
||||
console.log('[GitHub] Chmod binary finished')
|
||||
log.debug('Chmod binary finished')
|
||||
} catch (chmodError) {
|
||||
console.warn('[GitHub] Failed to chmod binary:', chmodError)
|
||||
log.warn('Failed to chmod binary', chmodError)
|
||||
}
|
||||
resolve()
|
||||
})
|
||||
@ -214,12 +217,12 @@ export async function installMihomoCore(version: string): Promise<void> {
|
||||
}
|
||||
|
||||
// 清理临时文件
|
||||
console.log(`[GitHub] Cleaning up temporary file ${tempZip}`)
|
||||
log.debug(`Cleaning up temporary file ${tempZip}`)
|
||||
rmSync(tempZip)
|
||||
|
||||
console.log(`[GitHub] Successfully installed mihomo core version ${version}`)
|
||||
log.info(`Successfully installed mihomo core version ${version}`)
|
||||
} catch (error) {
|
||||
console.error('[GitHub] Failed to install mihomo core:', error)
|
||||
log.error('Failed to install mihomo core', error)
|
||||
throw new Error(
|
||||
`Failed to install core: ${error instanceof Error ? error.message : String(error)}`
|
||||
)
|
||||
|
||||
@ -213,8 +213,12 @@ async function cleanup(): Promise<void> {
|
||||
// logs
|
||||
const { maxLogDays = 7 } = await getAppConfig()
|
||||
const logs = await readdir(logDir())
|
||||
const datePattern = /^\d{4}-\d{2}-\d{2}/
|
||||
for (const log of logs) {
|
||||
const date = new Date(log.split('.')[0])
|
||||
const match = log.match(datePattern)
|
||||
if (!match) continue
|
||||
const date = new Date(match[0])
|
||||
if (isNaN(date.getTime())) continue
|
||||
const diff = Date.now() - date.getTime()
|
||||
if (diff > maxLogDays * 24 * 60 * 60 * 1000) {
|
||||
try {
|
||||
|
||||
@ -104,7 +104,7 @@ import {
|
||||
updateTrayIconImmediate
|
||||
} from '../resolve/tray'
|
||||
import { registerShortcut } from '../resolve/shortcut'
|
||||
import { closeMainWindow, mainWindow, showMainWindow, triggerMainWindow } from '..'
|
||||
import { closeMainWindow, mainWindow, showMainWindow, triggerMainWindow } from '../window'
|
||||
import {
|
||||
applyTheme,
|
||||
fetchThemes,
|
||||
|
||||
177
src/main/window.ts
Normal file
177
src/main/window.ts
Normal file
@ -0,0 +1,177 @@
|
||||
import { BrowserWindow, Menu, shell } from 'electron'
|
||||
import { is } from '@electron-toolkit/utils'
|
||||
import windowStateKeeper from 'electron-window-state'
|
||||
import { join } from 'path'
|
||||
import { getAppConfig } from './config'
|
||||
import { quitWithoutCore, stopCore } from './core/manager'
|
||||
import { triggerSysProxy } from './sys/sysproxy'
|
||||
import { hideDockIcon, showDockIcon } from './resolve/tray'
|
||||
import icon from '../../resources/icon.png?asset'
|
||||
|
||||
export let mainWindow: BrowserWindow | null = null
|
||||
let quitTimeout: NodeJS.Timeout | null = null
|
||||
|
||||
export async function createWindow(): Promise<void> {
|
||||
const { useWindowFrame = false, silentStart = false, autoQuitWithoutCore = false, autoQuitWithoutCoreDelay = 60 } = await getAppConfig()
|
||||
const mainWindowState = windowStateKeeper({
|
||||
defaultWidth: 800,
|
||||
defaultHeight: 600,
|
||||
file: 'window-state.json'
|
||||
})
|
||||
|
||||
Menu.setApplicationMenu(null)
|
||||
mainWindow = new BrowserWindow({
|
||||
minWidth: 800,
|
||||
minHeight: 600,
|
||||
width: mainWindowState.width,
|
||||
height: mainWindowState.height,
|
||||
x: mainWindowState.x,
|
||||
y: mainWindowState.y,
|
||||
show: false,
|
||||
frame: useWindowFrame,
|
||||
fullscreenable: false,
|
||||
titleBarStyle: useWindowFrame ? 'default' : 'hidden',
|
||||
titleBarOverlay: useWindowFrame
|
||||
? false
|
||||
: {
|
||||
height: 49
|
||||
},
|
||||
autoHideMenuBar: true,
|
||||
...(process.platform === 'linux' ? { icon: icon } : {}),
|
||||
webPreferences: {
|
||||
preload: join(__dirname, '../preload/index.js'),
|
||||
spellcheck: false,
|
||||
sandbox: false,
|
||||
devTools: true
|
||||
}
|
||||
})
|
||||
|
||||
mainWindowState.manage(mainWindow)
|
||||
setupWindowEvents(mainWindow, mainWindowState, { silentStart, autoQuitWithoutCore, autoQuitWithoutCoreDelay })
|
||||
|
||||
if (is.dev) {
|
||||
mainWindow.webContents.openDevTools()
|
||||
}
|
||||
|
||||
if (is.dev && process.env['ELECTRON_RENDERER_URL']) {
|
||||
mainWindow.loadURL(process.env['ELECTRON_RENDERER_URL'])
|
||||
} else {
|
||||
mainWindow.loadFile(join(__dirname, '../renderer/index.html'))
|
||||
}
|
||||
}
|
||||
|
||||
interface WindowConfig {
|
||||
silentStart: boolean
|
||||
autoQuitWithoutCore: boolean
|
||||
autoQuitWithoutCoreDelay: number
|
||||
}
|
||||
|
||||
function setupWindowEvents(
|
||||
window: BrowserWindow,
|
||||
windowState: ReturnType<typeof windowStateKeeper>,
|
||||
config: WindowConfig
|
||||
): void {
|
||||
const { silentStart, autoQuitWithoutCore, autoQuitWithoutCoreDelay } = config
|
||||
|
||||
window.on('ready-to-show', () => {
|
||||
if (autoQuitWithoutCore && !window.isVisible()) {
|
||||
scheduleQuitWithoutCore(autoQuitWithoutCoreDelay)
|
||||
}
|
||||
|
||||
// 开发模式下始终显示窗口
|
||||
if (!silentStart || is.dev) {
|
||||
clearQuitTimeout()
|
||||
window.show()
|
||||
window.focusOnWebView()
|
||||
}
|
||||
})
|
||||
|
||||
window.webContents.on('did-fail-load', () => {
|
||||
window.webContents.reload()
|
||||
})
|
||||
|
||||
window.on('show', () => {
|
||||
showDockIcon()
|
||||
})
|
||||
|
||||
window.on('close', async (event) => {
|
||||
event.preventDefault()
|
||||
window.hide()
|
||||
|
||||
const {
|
||||
autoQuitWithoutCore = false,
|
||||
autoQuitWithoutCoreDelay = 60,
|
||||
useDockIcon = true
|
||||
} = await getAppConfig()
|
||||
|
||||
if (!useDockIcon) {
|
||||
hideDockIcon()
|
||||
}
|
||||
|
||||
if (autoQuitWithoutCore) {
|
||||
scheduleQuitWithoutCore(autoQuitWithoutCoreDelay)
|
||||
}
|
||||
})
|
||||
|
||||
window.on('resized', () => {
|
||||
windowState.saveState(window)
|
||||
})
|
||||
|
||||
window.on('move', () => {
|
||||
windowState.saveState(window)
|
||||
})
|
||||
|
||||
window.on('session-end', async () => {
|
||||
await triggerSysProxy(false)
|
||||
await stopCore()
|
||||
})
|
||||
|
||||
window.webContents.setWindowOpenHandler((details) => {
|
||||
shell.openExternal(details.url)
|
||||
return { action: 'deny' }
|
||||
})
|
||||
}
|
||||
|
||||
function scheduleQuitWithoutCore(delaySeconds: number): void {
|
||||
clearQuitTimeout()
|
||||
quitTimeout = setTimeout(async () => {
|
||||
await quitWithoutCore()
|
||||
}, delaySeconds * 1000)
|
||||
}
|
||||
|
||||
export function clearQuitTimeout(): void {
|
||||
if (quitTimeout) {
|
||||
clearTimeout(quitTimeout)
|
||||
quitTimeout = null
|
||||
}
|
||||
}
|
||||
|
||||
export function triggerMainWindow(force?: boolean): void {
|
||||
if (!mainWindow) return
|
||||
|
||||
getAppConfig()
|
||||
.then(({ triggerMainWindowBehavior = 'toggle' }) => {
|
||||
if (force === true || triggerMainWindowBehavior === 'toggle') {
|
||||
if (mainWindow?.isVisible()) {
|
||||
closeMainWindow()
|
||||
} else {
|
||||
showMainWindow()
|
||||
}
|
||||
} else {
|
||||
showMainWindow()
|
||||
}
|
||||
})
|
||||
.catch(showMainWindow)
|
||||
}
|
||||
|
||||
export function showMainWindow(): void {
|
||||
if (mainWindow) {
|
||||
clearQuitTimeout()
|
||||
mainWindow.show()
|
||||
mainWindow.focusOnWebView()
|
||||
}
|
||||
}
|
||||
|
||||
export function closeMainWindow(): void {
|
||||
mainWindow?.close()
|
||||
}
|
||||
@ -163,11 +163,7 @@ const validListenChannels = [
|
||||
] as const
|
||||
|
||||
// 允许的 send channels 白名单
|
||||
const validSendChannels = [
|
||||
'updateTrayMenu',
|
||||
'updateFloatingWindow',
|
||||
'trayIconUpdate'
|
||||
] as const
|
||||
const validSendChannels = ['updateTrayMenu', 'updateFloatingWindow', 'trayIconUpdate'] as const
|
||||
|
||||
type InvokeChannel = (typeof validInvokeChannels)[number]
|
||||
type ListenChannel = (typeof validListenChannels)[number]
|
||||
|
||||
@ -33,16 +33,13 @@ import { platform } from '@renderer/utils/init'
|
||||
import { TitleBarOverlayOptions } from 'electron'
|
||||
import SubStoreCard from '@renderer/components/sider/substore-card'
|
||||
import MihomoIcon from './components/base/mihomo-icon'
|
||||
import { driver } from 'driver.js'
|
||||
import { createTourDriver, getDriver, startTourIfNeeded } from '@renderer/utils/tour'
|
||||
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
|
||||
}
|
||||
export { getDriver }
|
||||
|
||||
const App: React.FC = () => {
|
||||
const { t } = useTranslation()
|
||||
@ -74,11 +71,13 @@ const App: React.FC = () => {
|
||||
const siderWidthValueRef = useRef(siderWidthValue)
|
||||
const [resizing, setResizing] = useState(false)
|
||||
const resizingRef = useRef(resizing)
|
||||
const tourInitialized = useRef(false)
|
||||
const sensors = useSensors(useSensor(PointerSensor))
|
||||
const { setTheme, systemTheme } = useTheme()
|
||||
navigate = useNavigate()
|
||||
const location = useLocation()
|
||||
const page = useRoutes(routes)
|
||||
|
||||
const setTitlebar = (): void => {
|
||||
if (!useWindowFrame && platform !== 'darwin') {
|
||||
const options = { height: 48 } as TitleBarOverlayOptions
|
||||
@ -103,186 +102,10 @@ 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')
|
||||
driverInstance.drive()
|
||||
if (!tourInitialized.current) {
|
||||
tourInitialized.current = true
|
||||
createTourDriver(t, navigate)
|
||||
startTourIfNeeded()
|
||||
}
|
||||
}, [t])
|
||||
|
||||
|
||||
@ -596,7 +596,9 @@ const EditRulesModal: React.FC<Props> = (props) => {
|
||||
if (Array.isArray(parsed['proxy-groups'])) {
|
||||
groups.push(
|
||||
...((parsed['proxy-groups'] as Array<Record<string, unknown>>)
|
||||
.map((group) => (group && typeof group['name'] === 'string' ? (group['name'] as string) : ''))
|
||||
.map((group) =>
|
||||
group && typeof group['name'] === 'string' ? (group['name'] as string) : ''
|
||||
)
|
||||
.filter(Boolean) as string[])
|
||||
)
|
||||
}
|
||||
@ -604,7 +606,9 @@ const EditRulesModal: React.FC<Props> = (props) => {
|
||||
if (Array.isArray(parsed['proxies'])) {
|
||||
groups.push(
|
||||
...((parsed['proxies'] as Array<Record<string, unknown>>)
|
||||
.map((proxy) => (proxy && typeof proxy['name'] === 'string' ? (proxy['name'] as string) : ''))
|
||||
.map((proxy) =>
|
||||
proxy && typeof proxy['name'] === 'string' ? (proxy['name'] as string) : ''
|
||||
)
|
||||
.filter(Boolean) as string[])
|
||||
)
|
||||
}
|
||||
|
||||
@ -125,6 +125,8 @@ export const ProfileConfigProvider: React.FC<{ children: ReactNode }> = ({ child
|
||||
}
|
||||
window.electron.ipcRenderer.on('profileConfigUpdated', handler)
|
||||
return (): void => {
|
||||
// 清理待处理任务,防止内存泄漏
|
||||
targetProfileId.current = null
|
||||
window.electron.ipcRenderer.removeListener('profileConfigUpdated', handler)
|
||||
}
|
||||
}, [])
|
||||
|
||||
@ -15,7 +15,8 @@ import { MdViewList, MdTableChart } from 'react-icons/md'
|
||||
import { HiOutlineAdjustmentsHorizontal } from 'react-icons/hi2'
|
||||
import { Dropdown, DropdownTrigger, DropdownMenu, DropdownItem } from '@heroui/react'
|
||||
import { includesIgnoreCase } from '@renderer/utils/includes'
|
||||
import { differenceWith, unionWith } from 'lodash'
|
||||
import differenceWith from 'lodash/differenceWith'
|
||||
import unionWith from 'lodash/unionWith'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { IoMdPause, IoMdPlay } from 'react-icons/io'
|
||||
|
||||
|
||||
@ -707,8 +707,8 @@ const Mihomo: React.FC = () => {
|
||||
max={65535}
|
||||
min={0}
|
||||
onValueChange={(v) => {
|
||||
const port = parseInt(v)
|
||||
if (!isNaN(port)) {
|
||||
const port = v === '' ? 0 : parseInt(v)
|
||||
if (!isNaN(port) && port >= 0 && port <= 65535) {
|
||||
setMixedPortInput(port)
|
||||
patchAppConfig({ showMixedPort: port })
|
||||
setIsManualPortChange(true)
|
||||
@ -768,8 +768,8 @@ const Mihomo: React.FC = () => {
|
||||
max={65535}
|
||||
min={0}
|
||||
onValueChange={(v) => {
|
||||
const port = parseInt(v)
|
||||
if (!isNaN(port)) {
|
||||
const port = v === '' ? 0 : parseInt(v)
|
||||
if (!isNaN(port) && port >= 0 && port <= 65535) {
|
||||
setSocksPortInput(port)
|
||||
patchAppConfig({ showSocksPort: port })
|
||||
setIsManualPortChange(true)
|
||||
@ -829,8 +829,8 @@ const Mihomo: React.FC = () => {
|
||||
max={65535}
|
||||
min={0}
|
||||
onValueChange={(v) => {
|
||||
const port = parseInt(v)
|
||||
if (!isNaN(port)) {
|
||||
const port = v === '' ? 0 : parseInt(v)
|
||||
if (!isNaN(port) && port >= 0 && port <= 65535) {
|
||||
setHttpPortInput(port)
|
||||
patchAppConfig({ showHttpPort: port })
|
||||
setIsManualPortChange(true)
|
||||
@ -891,8 +891,8 @@ const Mihomo: React.FC = () => {
|
||||
max={65535}
|
||||
min={0}
|
||||
onValueChange={(v) => {
|
||||
const port = parseInt(v)
|
||||
if (!isNaN(port)) {
|
||||
const port = v === '' ? 0 : parseInt(v)
|
||||
if (!isNaN(port) && port >= 0 && port <= 65535) {
|
||||
setRedirPortInput(port)
|
||||
patchAppConfig({ showRedirPort: port })
|
||||
setIsManualPortChange(true)
|
||||
@ -954,8 +954,8 @@ const Mihomo: React.FC = () => {
|
||||
max={65535}
|
||||
min={0}
|
||||
onValueChange={(v) => {
|
||||
const port = parseInt(v)
|
||||
if (!isNaN(port)) {
|
||||
const port = v === '' ? 0 : parseInt(v)
|
||||
if (!isNaN(port) && port >= 0 && port <= 65535) {
|
||||
setTproxyPortInput(port)
|
||||
patchAppConfig({ showTproxyPort: port })
|
||||
setIsManualPortChange(true)
|
||||
|
||||
@ -113,8 +113,7 @@ export const convertMrsRuleset = (path: string, behavior: string): Promise<strin
|
||||
export const getRuntimeConfig = (): Promise<IMihomoConfig> => invoke('getRuntimeConfig')
|
||||
export const getRuntimeConfigStr = (): Promise<string> => invoke('getRuntimeConfigStr')
|
||||
export const getRuleStr = (id: string): Promise<string> => invoke('getRuleStr', id)
|
||||
export const setRuleStr = (id: string, str: string): Promise<void> =>
|
||||
invoke('setRuleStr', id, str)
|
||||
export const setRuleStr = (id: string, str: string): Promise<void> => invoke('setRuleStr', id, str)
|
||||
export const getFilePath = (ext: string[]): Promise<string[] | undefined> =>
|
||||
invoke('getFilePath', ext)
|
||||
export const readTextFile = (filePath: string): Promise<string> => invoke('readTextFile', filePath)
|
||||
@ -130,8 +129,7 @@ export const startMonitor = (): Promise<void> => invoke('startMonitor')
|
||||
export const quitWithoutCore = (): Promise<void> => invoke('quitWithoutCore')
|
||||
|
||||
// System
|
||||
export const triggerSysProxy = (enable: boolean): Promise<void> =>
|
||||
invoke('triggerSysProxy', enable)
|
||||
export const triggerSysProxy = (enable: boolean): Promise<void> => invoke('triggerSysProxy', enable)
|
||||
export const checkTunPermissions = (): Promise<boolean> => invoke('checkTunPermissions')
|
||||
export const grantTunPermissions = (): Promise<void> => invoke('grantTunPermissions')
|
||||
export const manualGrantCorePermition = (): Promise<void> => invoke('manualGrantCorePermition')
|
||||
@ -168,8 +166,7 @@ export const clearMihomoVersionCache = (): Promise<void> => invoke('clearMihomoV
|
||||
|
||||
// Backup
|
||||
export const webdavBackup = (): Promise<boolean> => invoke('webdavBackup')
|
||||
export const webdavRestore = (filename: string): Promise<void> =>
|
||||
invoke('webdavRestore', filename)
|
||||
export const webdavRestore = (filename: string): Promise<void> => invoke('webdavRestore', filename)
|
||||
export const listWebdavBackups = (): Promise<string[]> => invoke('listWebdavBackups')
|
||||
export const webdavDelete = (filename: string): Promise<void> => invoke('webdavDelete', filename)
|
||||
export const reinitWebdavBackupScheduler = (): Promise<void> =>
|
||||
@ -180,10 +177,8 @@ export const importLocalBackup = (): Promise<boolean> => invoke('importLocalBack
|
||||
// SubStore
|
||||
export const startSubStoreFrontendServer = (): Promise<void> =>
|
||||
invoke('startSubStoreFrontendServer')
|
||||
export const stopSubStoreFrontendServer = (): Promise<void> =>
|
||||
invoke('stopSubStoreFrontendServer')
|
||||
export const startSubStoreBackendServer = (): Promise<void> =>
|
||||
invoke('startSubStoreBackendServer')
|
||||
export const stopSubStoreFrontendServer = (): Promise<void> => invoke('stopSubStoreFrontendServer')
|
||||
export const startSubStoreBackendServer = (): Promise<void> => invoke('startSubStoreBackendServer')
|
||||
export const stopSubStoreBackendServer = (): Promise<void> => invoke('stopSubStoreBackendServer')
|
||||
export const downloadSubStore = (): Promise<void> => invoke('downloadSubStore')
|
||||
export const subStorePort = (): Promise<number> => invoke('subStorePort')
|
||||
@ -201,10 +196,11 @@ export const writeTheme = (theme: string, css: string): Promise<void> =>
|
||||
invoke('writeTheme', theme, css)
|
||||
|
||||
let applyThemeRunning = false
|
||||
const applyThemeWaitList: string[] = []
|
||||
let pendingTheme: string | null = null
|
||||
|
||||
export async function applyTheme(theme: string): Promise<void> {
|
||||
if (applyThemeRunning) {
|
||||
applyThemeWaitList.push(theme)
|
||||
pendingTheme = theme
|
||||
return
|
||||
}
|
||||
applyThemeRunning = true
|
||||
@ -212,13 +208,12 @@ export async function applyTheme(theme: string): Promise<void> {
|
||||
await invoke<void>('applyTheme', theme)
|
||||
} finally {
|
||||
applyThemeRunning = false
|
||||
if (applyThemeWaitList.length > 0) {
|
||||
const nextTheme = applyThemeWaitList.shift()
|
||||
if (nextTheme) {
|
||||
if (pendingTheme !== null) {
|
||||
const nextTheme = pendingTheme
|
||||
pendingTheme = null
|
||||
await applyTheme(nextTheme)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Tray
|
||||
|
||||
195
src/renderer/src/utils/tour.ts
Normal file
195
src/renderer/src/utils/tour.ts
Normal file
@ -0,0 +1,195 @@
|
||||
import { driver } from 'driver.js'
|
||||
import { TFunction } from 'i18next'
|
||||
import { NavigateFunction } from 'react-router-dom'
|
||||
|
||||
let driverInstance: ReturnType<typeof driver> | null = null
|
||||
|
||||
export function getDriver(): ReturnType<typeof driver> | null {
|
||||
return driverInstance
|
||||
}
|
||||
|
||||
export function createTourDriver(t: TFunction, navigate: NavigateFunction): void {
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
})
|
||||
}
|
||||
|
||||
export function startTourIfNeeded(): void {
|
||||
const tourShown = window.localStorage.getItem('tourShown')
|
||||
if (!tourShown && driverInstance) {
|
||||
window.localStorage.setItem('tourShown', 'true')
|
||||
driverInstance.drive()
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user