mirror of
https://gh.catmak.name/https://github.com/mihomo-party-org/mihomo-party
synced 2026-02-10 19:50:28 +08:00
refactor: split main process into modules and extract tour logic
This commit is contained in:
parent
3f1d1f84a1
commit
c1f7a862aa
@ -33,7 +33,7 @@ import {
|
|||||||
import chokidar from 'chokidar'
|
import chokidar from 'chokidar'
|
||||||
import { readFile, rm, writeFile } from 'fs/promises'
|
import { readFile, rm, writeFile } from 'fs/promises'
|
||||||
import { promisify } from 'util'
|
import { promisify } from 'util'
|
||||||
import { mainWindow } from '..'
|
import { mainWindow } from '../window'
|
||||||
import path from 'path'
|
import path from 'path'
|
||||||
import os from 'os'
|
import os from 'os'
|
||||||
import { createWriteStream, existsSync } from 'fs'
|
import { createWriteStream, existsSync } from 'fs'
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import axios, { AxiosInstance } from 'axios'
|
import axios, { AxiosInstance } from 'axios'
|
||||||
import { getAppConfig, getControledMihomoConfig } from '../config'
|
import { getAppConfig, getControledMihomoConfig } from '../config'
|
||||||
import { mainWindow } from '..'
|
import { mainWindow } from '../window'
|
||||||
import WebSocket from 'ws'
|
import WebSocket from 'ws'
|
||||||
import { tray } from '../resolve/tray'
|
import { tray } from '../resolve/tray'
|
||||||
import { calcTraffic } from '../utils/calc'
|
import { calcTraffic } from '../utils/calc'
|
||||||
|
|||||||
@ -5,13 +5,17 @@ import { getAppConfig } from '../config'
|
|||||||
export async function subStoreSubs(): Promise<ISubStoreSub[]> {
|
export async function subStoreSubs(): Promise<ISubStoreSub[]> {
|
||||||
const { useCustomSubStore = false, customSubStoreUrl = '' } = await getAppConfig()
|
const { useCustomSubStore = false, customSubStoreUrl = '' } = await getAppConfig()
|
||||||
const baseUrl = useCustomSubStore ? customSubStoreUrl : `http://127.0.0.1:${subStorePort}`
|
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
|
return res.data.data
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function subStoreCollections(): Promise<ISubStoreSub[]> {
|
export async function subStoreCollections(): Promise<ISubStoreSub[]> {
|
||||||
const { useCustomSubStore = false, customSubStoreUrl = '' } = await getAppConfig()
|
const { useCustomSubStore = false, customSubStoreUrl = '' } = await getAppConfig()
|
||||||
const baseUrl = useCustomSubStore ? customSubStoreUrl : `http://127.0.0.1:${subStorePort}`
|
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
|
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,43 @@
|
|||||||
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 { registerIpcMainHandlers } from './utils/ipc'
|
||||||
import windowStateKeeper from 'electron-window-state'
|
import { getAppConfig, patchAppConfig } from './config'
|
||||||
import { app, shell, BrowserWindow, Menu, dialog, Notification, powerMonitor } from 'electron'
|
|
||||||
import { addProfileItem, getAppConfig, patchAppConfig } from './config'
|
|
||||||
import {
|
import {
|
||||||
quitWithoutCore,
|
|
||||||
startCore,
|
startCore,
|
||||||
stopCore,
|
|
||||||
checkAdminRestartForTun,
|
checkAdminRestartForTun,
|
||||||
checkHighPrivilegeCore,
|
checkHighPrivilegeCore,
|
||||||
restartAsAdmin,
|
restartAsAdmin,
|
||||||
initAdminStatus
|
initAdminStatus,
|
||||||
|
checkAdminPrivileges
|
||||||
} from './core/manager'
|
} from './core/manager'
|
||||||
import { triggerSysProxy } from './sys/sysproxy'
|
import { createTray } from './resolve/tray'
|
||||||
import icon from '../../resources/icon.png?asset'
|
|
||||||
import { createTray, hideDockIcon, showDockIcon } from './resolve/tray'
|
|
||||||
import { init, initBasic, safeShowErrorBox } from './utils/init'
|
import { init, initBasic, safeShowErrorBox } from './utils/init'
|
||||||
import { join } from 'path'
|
|
||||||
import { initShortcut } from './resolve/shortcut'
|
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 { initProfileUpdater } from './core/profileUpdater'
|
||||||
import { existsSync } from 'fs'
|
|
||||||
import { exePath } from './utils/dirs'
|
|
||||||
import { startMonitor } from './resolve/trafficMonitor'
|
import { startMonitor } from './resolve/trafficMonitor'
|
||||||
import { showFloatingWindow } from './resolve/floatingWindow'
|
import { showFloatingWindow } from './resolve/floatingWindow'
|
||||||
import { initI18n } from '../shared/i18n'
|
import { initI18n } from '../shared/i18n'
|
||||||
import i18next from 'i18next'
|
import i18next from 'i18next'
|
||||||
import { logger } from './utils/logger'
|
import { logger } from './utils/logger'
|
||||||
import { initWebdavBackupScheduler } from './resolve/backup'
|
import { initWebdavBackupScheduler } from './resolve/backup'
|
||||||
|
import {
|
||||||
|
createWindow,
|
||||||
|
mainWindow,
|
||||||
|
showMainWindow,
|
||||||
|
triggerMainWindow,
|
||||||
|
closeMainWindow
|
||||||
|
} from './window'
|
||||||
|
import { handleDeepLink } from './deeplink'
|
||||||
|
import {
|
||||||
|
fixUserDataPermissions,
|
||||||
|
setupPlatformSpecifics,
|
||||||
|
setupAppLifecycle,
|
||||||
|
getSystemLanguage
|
||||||
|
} from './lifecycle'
|
||||||
|
|
||||||
async function fixUserDataPermissions(): Promise<void> {
|
export { mainWindow, showMainWindow, triggerMainWindow, closeMainWindow }
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let quitTimeout: NodeJS.Timeout | null = null
|
|
||||||
export let mainWindow: BrowserWindow | null = null
|
|
||||||
|
|
||||||
const gotTheLock = app.requestSingleInstanceLock()
|
const gotTheLock = app.requestSingleInstanceLock()
|
||||||
|
|
||||||
if (!gotTheLock) {
|
if (!gotTheLock) {
|
||||||
app.quit()
|
app.quit()
|
||||||
}
|
}
|
||||||
@ -72,85 +51,67 @@ initApp().catch((e) => {
|
|||||||
app.quit()
|
app.quit()
|
||||||
})
|
})
|
||||||
|
|
||||||
export function customRelaunch(): void {
|
setupPlatformSpecifics()
|
||||||
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'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
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> {
|
async function checkHighPrivilegeCoreEarly(): Promise<void> {
|
||||||
if (process.platform !== 'win32') {
|
if (process.platform !== 'win32') return
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await initBasic()
|
await initBasic()
|
||||||
|
|
||||||
const { checkAdminPrivileges } = await import('./core/manager')
|
|
||||||
const isCurrentAppAdmin = await checkAdminPrivileges()
|
const isCurrentAppAdmin = await checkAdminPrivileges()
|
||||||
|
if (isCurrentAppAdmin) return
|
||||||
if (isCurrentAppAdmin) {
|
|
||||||
console.log('Current app is running as administrator, skipping privilege check')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const hasHighPrivilegeCore = await checkHighPrivilegeCore()
|
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')
|
||||||
|
await initI18n({ lng: language })
|
||||||
|
} catch {
|
||||||
|
await initI18n({ lng: 'zh-CN' })
|
||||||
|
}
|
||||||
|
|
||||||
|
const choice = dialog.showMessageBoxSync({
|
||||||
|
type: 'warning',
|
||||||
|
title: i18next.t('core.highPrivilege.title'),
|
||||||
|
message: i18next.t('core.highPrivilege.message'),
|
||||||
|
buttons: [i18next.t('common.confirm'), i18next.t('common.cancel')],
|
||||||
|
defaultId: 0,
|
||||||
|
cancelId: 1
|
||||||
|
})
|
||||||
|
|
||||||
|
if (choice === 0) {
|
||||||
try {
|
try {
|
||||||
const appConfig = await getAppConfig()
|
await restartAsAdmin(false)
|
||||||
const language =
|
|
||||||
appConfig.language || (app.getLocale().startsWith('zh') ? 'zh-CN' : 'en-US')
|
|
||||||
await initI18n({ lng: language })
|
|
||||||
} catch {
|
|
||||||
await initI18n({ lng: 'zh-CN' })
|
|
||||||
}
|
|
||||||
|
|
||||||
const choice = dialog.showMessageBoxSync({
|
|
||||||
type: 'warning',
|
|
||||||
title: i18next.t('core.highPrivilege.title'),
|
|
||||||
message: i18next.t('core.highPrivilege.message'),
|
|
||||||
buttons: [i18next.t('common.confirm'), i18next.t('common.cancel')],
|
|
||||||
defaultId: 0,
|
|
||||||
cancelId: 1
|
|
||||||
})
|
|
||||||
|
|
||||||
if (choice === 0) {
|
|
||||||
try {
|
|
||||||
// Windows 平台重启应用获取管理员权限
|
|
||||||
await restartAsAdmin(false)
|
|
||||||
process.exit(0)
|
|
||||||
} catch (error) {
|
|
||||||
safeShowErrorBox('common.error.adminRequired', `${error}`)
|
|
||||||
process.exit(1)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
process.exit(0)
|
process.exit(0)
|
||||||
|
} catch (error) {
|
||||||
|
safeShowErrorBox('common.error.adminRequired', `${error}`)
|
||||||
|
process.exit(1)
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
process.exit(0)
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.error('Failed to check high privilege core:', e)
|
console.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) {
|
||||||
|
console.warn('Failed to read hardware acceleration config:', e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
initHardwareAcceleration()
|
||||||
|
setupAppLifecycle()
|
||||||
|
|
||||||
app.on('second-instance', async (_event, commandline) => {
|
app.on('second-instance', async (_event, commandline) => {
|
||||||
showMainWindow()
|
showMainWindow()
|
||||||
const url = commandline.pop()
|
const url = commandline.pop()
|
||||||
@ -164,58 +125,16 @@ app.on('open-url', async (_event, url) => {
|
|||||||
await handleDeepLink(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 () => {
|
app.whenReady().then(async () => {
|
||||||
// Set app user model id for windows
|
|
||||||
electronApp.setAppUserModelId('party.mihomo.app')
|
electronApp.setAppUserModelId('party.mihomo.app')
|
||||||
|
|
||||||
await initBasic()
|
await initBasic()
|
||||||
|
|
||||||
await checkHighPrivilegeCoreEarly()
|
await checkHighPrivilegeCoreEarly()
|
||||||
|
|
||||||
await initAdminStatus()
|
await initAdminStatus()
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await init()
|
await init()
|
||||||
|
|
||||||
const appConfig = await getAppConfig()
|
const appConfig = await getAppConfig()
|
||||||
// 如果配置中没有语言设置,则使用系统语言
|
|
||||||
if (!appConfig.language) {
|
if (!appConfig.language) {
|
||||||
const systemLanguage = getSystemLanguage()
|
const systemLanguage = getSystemLanguage()
|
||||||
await patchAppConfig({ language: systemLanguage })
|
await patchAppConfig({ language: systemLanguage })
|
||||||
@ -231,28 +150,27 @@ app.whenReady().then(async () => {
|
|||||||
const [startPromise] = await startCore()
|
const [startPromise] = await startCore()
|
||||||
startPromise.then(async () => {
|
startPromise.then(async () => {
|
||||||
await initProfileUpdater()
|
await initProfileUpdater()
|
||||||
await initWebdavBackupScheduler() // 初始化 WebDAV 定时备份任务
|
await initWebdavBackupScheduler()
|
||||||
// 上次是否为了开启 TUN 而重启
|
|
||||||
await checkAdminRestartForTun()
|
await checkAdminRestartForTun()
|
||||||
})
|
})
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
safeShowErrorBox('mihomo.error.coreStartFailed', `${e}`)
|
safeShowErrorBox('mihomo.error.coreStartFailed', `${e}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await startMonitor()
|
await startMonitor()
|
||||||
} catch {
|
} catch {
|
||||||
// ignore
|
// 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) => {
|
app.on('browser-window-created', (_, window) => {
|
||||||
optimizer.watchWindowShortcuts(window)
|
optimizer.watchWindowShortcuts(window)
|
||||||
})
|
})
|
||||||
|
|
||||||
const { showFloatingWindow: showFloating = false, disableTray = false } = await getAppConfig()
|
const { showFloatingWindow: showFloating = false, disableTray = false } = await getAppConfig()
|
||||||
registerIpcMainHandlers()
|
registerIpcMainHandlers()
|
||||||
await createWindow()
|
await createWindow()
|
||||||
|
|
||||||
if (showFloating) {
|
if (showFloating) {
|
||||||
try {
|
try {
|
||||||
await showFloatingWindow()
|
await showFloatingWindow()
|
||||||
@ -260,192 +178,14 @@ app.whenReady().then(async () => {
|
|||||||
await logger.error('Failed to create floating window on startup', error)
|
await logger.error('Failed to create floating window on startup', error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!disableTray) {
|
if (!disableTray) {
|
||||||
await createTray()
|
await createTray()
|
||||||
}
|
}
|
||||||
|
|
||||||
await initShortcut()
|
await initShortcut()
|
||||||
app.on('activate', function () {
|
|
||||||
// On macOS it's common to re-create a window in the app when the
|
app.on('activate', () => {
|
||||||
// dock icon is clicked and there are no other windows open.
|
|
||||||
showMainWindow()
|
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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
75
src/main/lifecycle.ts
Normal file
75
src/main/lifecycle.ts
Normal file
@ -0,0 +1,75 @@
|
|||||||
|
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}"`], {
|
||||||
|
shell: true,
|
||||||
|
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()
|
||||||
|
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 { app, globalShortcut, ipcMain, Notification } from 'electron'
|
||||||
import { mainWindow, triggerMainWindow } from '..'
|
import { mainWindow, triggerMainWindow } from '../window'
|
||||||
import {
|
import {
|
||||||
getAppConfig,
|
getAppConfig,
|
||||||
getControledMihomoConfig,
|
getControledMihomoConfig,
|
||||||
|
|||||||
@ -5,7 +5,7 @@ import * as chromeRequest from '../utils/chromeRequest'
|
|||||||
import AdmZip from 'adm-zip'
|
import AdmZip from 'adm-zip'
|
||||||
import { getControledMihomoConfig } from '../config'
|
import { getControledMihomoConfig } from '../config'
|
||||||
import { existsSync } from 'fs'
|
import { existsSync } from 'fs'
|
||||||
import { mainWindow } from '..'
|
import { mainWindow } from '../window'
|
||||||
import { floatingWindow } from './floatingWindow'
|
import { floatingWindow } from './floatingWindow'
|
||||||
import { t } from 'i18next'
|
import { t } from 'i18next'
|
||||||
|
|
||||||
|
|||||||
@ -23,7 +23,7 @@ import {
|
|||||||
getTrayIconStatus,
|
getTrayIconStatus,
|
||||||
calculateTrayIconStatus
|
calculateTrayIconStatus
|
||||||
} from '../core/mihomoApi'
|
} 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 { app, clipboard, ipcMain, Menu, nativeImage, shell, Tray } from 'electron'
|
||||||
import { dataDir, logDir, mihomoCoreDir, mihomoWorkDir } from '../utils/dirs'
|
import { dataDir, logDir, mihomoCoreDir, mihomoWorkDir } from '../utils/dirs'
|
||||||
import { triggerSysProxy } from '../sys/sysproxy'
|
import { triggerSysProxy } from '../sys/sysproxy'
|
||||||
|
|||||||
@ -2,7 +2,7 @@ import { exec } from 'child_process'
|
|||||||
import { promisify } from 'util'
|
import { promisify } from 'util'
|
||||||
import { getAppConfig, patchControledMihomoConfig } from '../config'
|
import { getAppConfig, patchControledMihomoConfig } from '../config'
|
||||||
import { patchMihomoConfig } from '../core/mihomoApi'
|
import { patchMihomoConfig } from '../core/mihomoApi'
|
||||||
import { mainWindow } from '..'
|
import { mainWindow } from '../window'
|
||||||
import { ipcMain, net } from 'electron'
|
import { ipcMain, net } from 'electron'
|
||||||
import { getDefaultDevice } from '../core/manager'
|
import { getDefaultDevice } from '../core/manager'
|
||||||
|
|
||||||
|
|||||||
@ -104,7 +104,7 @@ import {
|
|||||||
updateTrayIconImmediate
|
updateTrayIconImmediate
|
||||||
} from '../resolve/tray'
|
} from '../resolve/tray'
|
||||||
import { registerShortcut } from '../resolve/shortcut'
|
import { registerShortcut } from '../resolve/shortcut'
|
||||||
import { closeMainWindow, mainWindow, showMainWindow, triggerMainWindow } from '..'
|
import { closeMainWindow, mainWindow, showMainWindow, triggerMainWindow } from '../window'
|
||||||
import {
|
import {
|
||||||
applyTheme,
|
applyTheme,
|
||||||
fetchThemes,
|
fetchThemes,
|
||||||
|
|||||||
173
src/main/window.ts
Normal file
173
src/main/window.ts
Normal file
@ -0,0 +1,173 @@
|
|||||||
|
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 } = 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)
|
||||||
|
|
||||||
|
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'))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setupWindowEvents(
|
||||||
|
window: BrowserWindow,
|
||||||
|
windowState: ReturnType<typeof windowStateKeeper>
|
||||||
|
): void {
|
||||||
|
window.on('ready-to-show', async () => {
|
||||||
|
const {
|
||||||
|
silentStart = false,
|
||||||
|
autoQuitWithoutCore = false,
|
||||||
|
autoQuitWithoutCoreDelay = 60
|
||||||
|
} = await getAppConfig()
|
||||||
|
|
||||||
|
if (autoQuitWithoutCore && !window.isVisible()) {
|
||||||
|
scheduleQuitWithoutCore(autoQuitWithoutCoreDelay)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!silentStart) {
|
||||||
|
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 () => {
|
||||||
|
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
|
] as const
|
||||||
|
|
||||||
// 允许的 send channels 白名单
|
// 允许的 send channels 白名单
|
||||||
const validSendChannels = [
|
const validSendChannels = ['updateTrayMenu', 'updateFloatingWindow', 'trayIconUpdate'] as const
|
||||||
'updateTrayMenu',
|
|
||||||
'updateFloatingWindow',
|
|
||||||
'trayIconUpdate'
|
|
||||||
] as const
|
|
||||||
|
|
||||||
type InvokeChannel = (typeof validInvokeChannels)[number]
|
type InvokeChannel = (typeof validInvokeChannels)[number]
|
||||||
type ListenChannel = (typeof validListenChannels)[number]
|
type ListenChannel = (typeof validListenChannels)[number]
|
||||||
|
|||||||
@ -33,16 +33,13 @@ import { platform } from '@renderer/utils/init'
|
|||||||
import { TitleBarOverlayOptions } from 'electron'
|
import { TitleBarOverlayOptions } from 'electron'
|
||||||
import SubStoreCard from '@renderer/components/sider/substore-card'
|
import SubStoreCard from '@renderer/components/sider/substore-card'
|
||||||
import MihomoIcon from './components/base/mihomo-icon'
|
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 'driver.js/dist/driver.css'
|
||||||
import { useTranslation } from 'react-i18next'
|
import { useTranslation } from 'react-i18next'
|
||||||
|
|
||||||
let navigate: NavigateFunction
|
let navigate: NavigateFunction
|
||||||
let driverInstance: ReturnType<typeof driver> | null = null
|
|
||||||
|
|
||||||
export function getDriver(): ReturnType<typeof driver> | null {
|
export { getDriver }
|
||||||
return driverInstance
|
|
||||||
}
|
|
||||||
|
|
||||||
const App: React.FC = () => {
|
const App: React.FC = () => {
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
@ -74,11 +71,13 @@ const App: React.FC = () => {
|
|||||||
const siderWidthValueRef = useRef(siderWidthValue)
|
const siderWidthValueRef = useRef(siderWidthValue)
|
||||||
const [resizing, setResizing] = useState(false)
|
const [resizing, setResizing] = useState(false)
|
||||||
const resizingRef = useRef(resizing)
|
const resizingRef = useRef(resizing)
|
||||||
|
const tourInitialized = useRef(false)
|
||||||
const sensors = useSensors(useSensor(PointerSensor))
|
const sensors = useSensors(useSensor(PointerSensor))
|
||||||
const { setTheme, systemTheme } = useTheme()
|
const { setTheme, systemTheme } = useTheme()
|
||||||
navigate = useNavigate()
|
navigate = useNavigate()
|
||||||
const location = useLocation()
|
const location = useLocation()
|
||||||
const page = useRoutes(routes)
|
const page = useRoutes(routes)
|
||||||
|
|
||||||
const setTitlebar = (): void => {
|
const setTitlebar = (): void => {
|
||||||
if (!useWindowFrame && platform !== 'darwin') {
|
if (!useWindowFrame && platform !== 'darwin') {
|
||||||
const options = { height: 48 } as TitleBarOverlayOptions
|
const options = { height: 48 } as TitleBarOverlayOptions
|
||||||
@ -103,186 +102,10 @@ const App: React.FC = () => {
|
|||||||
}, [siderWidthValue, resizing])
|
}, [siderWidthValue, resizing])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
driverInstance = driver({
|
if (!tourInitialized.current) {
|
||||||
showProgress: true,
|
tourInitialized.current = true
|
||||||
nextBtnText: t('common.next'),
|
createTourDriver(t, navigate)
|
||||||
prevBtnText: t('common.prev'),
|
startTourIfNeeded()
|
||||||
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()
|
|
||||||
}
|
}
|
||||||
}, [t])
|
}, [t])
|
||||||
|
|
||||||
|
|||||||
@ -421,80 +421,80 @@ const RuleListItemBase: React.FC<RuleListItemProps> = ({
|
|||||||
onMoveDown,
|
onMoveDown,
|
||||||
onRemove
|
onRemove
|
||||||
}) => {
|
}) => {
|
||||||
let bgColorClass = 'bg-content2'
|
let bgColorClass = 'bg-content2'
|
||||||
let textStyleClass = ''
|
let textStyleClass = ''
|
||||||
|
|
||||||
if (isDeleted) {
|
if (isDeleted) {
|
||||||
bgColorClass = 'bg-danger-50 opacity-70'
|
bgColorClass = 'bg-danger-50 opacity-70'
|
||||||
textStyleClass = 'line-through text-foreground-500'
|
textStyleClass = 'line-through text-foreground-500'
|
||||||
} else if (isPrependOrAppend) {
|
} else if (isPrependOrAppend) {
|
||||||
bgColorClass = 'bg-success-50'
|
bgColorClass = 'bg-success-50'
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`flex items-center gap-2 p-2 rounded-lg ${bgColorClass}`}>
|
<div className={`flex items-center gap-2 p-2 rounded-lg ${bgColorClass}`}>
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
<Chip size="sm" variant="flat">
|
<Chip size="sm" variant="flat">
|
||||||
{rule.type}
|
{rule.type}
|
||||||
</Chip>
|
</Chip>
|
||||||
{/* 显示附加参数 */}
|
{/* 显示附加参数 */}
|
||||||
<div className="flex gap-1">
|
<div className="flex gap-1">
|
||||||
{rule.additionalParams &&
|
{rule.additionalParams &&
|
||||||
rule.additionalParams.length > 0 &&
|
rule.additionalParams.length > 0 &&
|
||||||
rule.additionalParams.map((param, idx) => (
|
rule.additionalParams.map((param, idx) => (
|
||||||
<Chip key={idx} size="sm" variant="flat" color="secondary">
|
<Chip key={idx} size="sm" variant="flat" color="secondary">
|
||||||
{param}
|
{param}
|
||||||
</Chip>
|
</Chip>
|
||||||
))}
|
))}
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-1 min-w-0">
|
|
||||||
<div className={`font-medium truncate ${textStyleClass}`}>
|
|
||||||
{rule.type === 'MATCH' ? rule.proxy : rule.payload}
|
|
||||||
</div>
|
|
||||||
{rule.proxy && rule.type !== 'MATCH' && (
|
|
||||||
<div className={`text-sm text-foreground-500 truncate ${textStyleClass}`}>
|
|
||||||
{rule.proxy}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<div className="flex gap-1">
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant="light"
|
|
||||||
onPress={() => originalIndex !== -1 && onMoveUp(originalIndex)}
|
|
||||||
isIconOnly
|
|
||||||
isDisabled={originalIndex === -1 || originalIndex === 0 || isDeleted}
|
|
||||||
>
|
|
||||||
<IoMdArrowUp className="text-lg" />
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
variant="light"
|
|
||||||
onPress={() => originalIndex !== -1 && onMoveDown(originalIndex)}
|
|
||||||
isIconOnly
|
|
||||||
isDisabled={originalIndex === -1 || originalIndex === rulesLength - 1 || isDeleted}
|
|
||||||
>
|
|
||||||
<IoMdArrowDown className="text-lg" />
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
size="sm"
|
|
||||||
color={originalIndex !== -1 && isDeleted ? 'success' : 'danger'}
|
|
||||||
variant="light"
|
|
||||||
onPress={() => originalIndex !== -1 && onRemove(originalIndex)}
|
|
||||||
isIconOnly
|
|
||||||
>
|
|
||||||
{originalIndex !== -1 && isDeleted ? (
|
|
||||||
<IoMdUndo className="text-lg" />
|
|
||||||
) : (
|
|
||||||
<IoMdTrash className="text-lg" />
|
|
||||||
)}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)
|
<div className="flex-1 min-w-0">
|
||||||
|
<div className={`font-medium truncate ${textStyleClass}`}>
|
||||||
|
{rule.type === 'MATCH' ? rule.proxy : rule.payload}
|
||||||
|
</div>
|
||||||
|
{rule.proxy && rule.type !== 'MATCH' && (
|
||||||
|
<div className={`text-sm text-foreground-500 truncate ${textStyleClass}`}>
|
||||||
|
{rule.proxy}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex gap-1">
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="light"
|
||||||
|
onPress={() => originalIndex !== -1 && onMoveUp(originalIndex)}
|
||||||
|
isIconOnly
|
||||||
|
isDisabled={originalIndex === -1 || originalIndex === 0 || isDeleted}
|
||||||
|
>
|
||||||
|
<IoMdArrowUp className="text-lg" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="light"
|
||||||
|
onPress={() => originalIndex !== -1 && onMoveDown(originalIndex)}
|
||||||
|
isIconOnly
|
||||||
|
isDisabled={originalIndex === -1 || originalIndex === rulesLength - 1 || isDeleted}
|
||||||
|
>
|
||||||
|
<IoMdArrowDown className="text-lg" />
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
color={originalIndex !== -1 && isDeleted ? 'success' : 'danger'}
|
||||||
|
variant="light"
|
||||||
|
onPress={() => originalIndex !== -1 && onRemove(originalIndex)}
|
||||||
|
isIconOnly
|
||||||
|
>
|
||||||
|
{originalIndex !== -1 && isDeleted ? (
|
||||||
|
<IoMdUndo className="text-lg" />
|
||||||
|
) : (
|
||||||
|
<IoMdTrash className="text-lg" />
|
||||||
|
)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
const RuleListItem = memo(RuleListItemBase, (prevProps, nextProps) => {
|
const RuleListItem = memo(RuleListItemBase, (prevProps, nextProps) => {
|
||||||
@ -596,7 +596,9 @@ const EditRulesModal: React.FC<Props> = (props) => {
|
|||||||
if (Array.isArray(parsed['proxy-groups'])) {
|
if (Array.isArray(parsed['proxy-groups'])) {
|
||||||
groups.push(
|
groups.push(
|
||||||
...((parsed['proxy-groups'] as Array<Record<string, unknown>>)
|
...((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[])
|
.filter(Boolean) as string[])
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@ -604,7 +606,9 @@ const EditRulesModal: React.FC<Props> = (props) => {
|
|||||||
if (Array.isArray(parsed['proxies'])) {
|
if (Array.isArray(parsed['proxies'])) {
|
||||||
groups.push(
|
groups.push(
|
||||||
...((parsed['proxies'] as Array<Record<string, unknown>>)
|
...((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[])
|
.filter(Boolean) as string[])
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -125,6 +125,8 @@ export const ProfileConfigProvider: React.FC<{ children: ReactNode }> = ({ child
|
|||||||
}
|
}
|
||||||
window.electron.ipcRenderer.on('profileConfigUpdated', handler)
|
window.electron.ipcRenderer.on('profileConfigUpdated', handler)
|
||||||
return (): void => {
|
return (): void => {
|
||||||
|
// 清理待处理任务,防止内存泄漏
|
||||||
|
targetProfileId.current = null
|
||||||
window.electron.ipcRenderer.removeListener('profileConfigUpdated', handler)
|
window.electron.ipcRenderer.removeListener('profileConfigUpdated', handler)
|
||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|||||||
@ -15,7 +15,8 @@ import { MdViewList, MdTableChart } from 'react-icons/md'
|
|||||||
import { HiOutlineAdjustmentsHorizontal } from 'react-icons/hi2'
|
import { HiOutlineAdjustmentsHorizontal } from 'react-icons/hi2'
|
||||||
import { Dropdown, DropdownTrigger, DropdownMenu, DropdownItem } from '@heroui/react'
|
import { Dropdown, DropdownTrigger, DropdownMenu, DropdownItem } from '@heroui/react'
|
||||||
import { includesIgnoreCase } from '@renderer/utils/includes'
|
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 { useTranslation } from 'react-i18next'
|
||||||
import { IoMdPause, IoMdPlay } from 'react-icons/io'
|
import { IoMdPause, IoMdPlay } from 'react-icons/io'
|
||||||
|
|
||||||
|
|||||||
@ -113,8 +113,7 @@ export const convertMrsRuleset = (path: string, behavior: string): Promise<strin
|
|||||||
export const getRuntimeConfig = (): Promise<IMihomoConfig> => invoke('getRuntimeConfig')
|
export const getRuntimeConfig = (): Promise<IMihomoConfig> => invoke('getRuntimeConfig')
|
||||||
export const getRuntimeConfigStr = (): Promise<string> => invoke('getRuntimeConfigStr')
|
export const getRuntimeConfigStr = (): Promise<string> => invoke('getRuntimeConfigStr')
|
||||||
export const getRuleStr = (id: string): Promise<string> => invoke('getRuleStr', id)
|
export const getRuleStr = (id: string): Promise<string> => invoke('getRuleStr', id)
|
||||||
export const setRuleStr = (id: string, str: string): Promise<void> =>
|
export const setRuleStr = (id: string, str: string): Promise<void> => invoke('setRuleStr', id, str)
|
||||||
invoke('setRuleStr', id, str)
|
|
||||||
export const getFilePath = (ext: string[]): Promise<string[] | undefined> =>
|
export const getFilePath = (ext: string[]): Promise<string[] | undefined> =>
|
||||||
invoke('getFilePath', ext)
|
invoke('getFilePath', ext)
|
||||||
export const readTextFile = (filePath: string): Promise<string> => invoke('readTextFile', filePath)
|
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')
|
export const quitWithoutCore = (): Promise<void> => invoke('quitWithoutCore')
|
||||||
|
|
||||||
// System
|
// System
|
||||||
export const triggerSysProxy = (enable: boolean): Promise<void> =>
|
export const triggerSysProxy = (enable: boolean): Promise<void> => invoke('triggerSysProxy', enable)
|
||||||
invoke('triggerSysProxy', enable)
|
|
||||||
export const checkTunPermissions = (): Promise<boolean> => invoke('checkTunPermissions')
|
export const checkTunPermissions = (): Promise<boolean> => invoke('checkTunPermissions')
|
||||||
export const grantTunPermissions = (): Promise<void> => invoke('grantTunPermissions')
|
export const grantTunPermissions = (): Promise<void> => invoke('grantTunPermissions')
|
||||||
export const manualGrantCorePermition = (): Promise<void> => invoke('manualGrantCorePermition')
|
export const manualGrantCorePermition = (): Promise<void> => invoke('manualGrantCorePermition')
|
||||||
@ -168,8 +166,7 @@ export const clearMihomoVersionCache = (): Promise<void> => invoke('clearMihomoV
|
|||||||
|
|
||||||
// Backup
|
// Backup
|
||||||
export const webdavBackup = (): Promise<boolean> => invoke('webdavBackup')
|
export const webdavBackup = (): Promise<boolean> => invoke('webdavBackup')
|
||||||
export const webdavRestore = (filename: string): Promise<void> =>
|
export const webdavRestore = (filename: string): Promise<void> => invoke('webdavRestore', filename)
|
||||||
invoke('webdavRestore', filename)
|
|
||||||
export const listWebdavBackups = (): Promise<string[]> => invoke('listWebdavBackups')
|
export const listWebdavBackups = (): Promise<string[]> => invoke('listWebdavBackups')
|
||||||
export const webdavDelete = (filename: string): Promise<void> => invoke('webdavDelete', filename)
|
export const webdavDelete = (filename: string): Promise<void> => invoke('webdavDelete', filename)
|
||||||
export const reinitWebdavBackupScheduler = (): Promise<void> =>
|
export const reinitWebdavBackupScheduler = (): Promise<void> =>
|
||||||
@ -180,10 +177,8 @@ export const importLocalBackup = (): Promise<boolean> => invoke('importLocalBack
|
|||||||
// SubStore
|
// SubStore
|
||||||
export const startSubStoreFrontendServer = (): Promise<void> =>
|
export const startSubStoreFrontendServer = (): Promise<void> =>
|
||||||
invoke('startSubStoreFrontendServer')
|
invoke('startSubStoreFrontendServer')
|
||||||
export const stopSubStoreFrontendServer = (): Promise<void> =>
|
export const stopSubStoreFrontendServer = (): Promise<void> => invoke('stopSubStoreFrontendServer')
|
||||||
invoke('stopSubStoreFrontendServer')
|
export const startSubStoreBackendServer = (): Promise<void> => invoke('startSubStoreBackendServer')
|
||||||
export const startSubStoreBackendServer = (): Promise<void> =>
|
|
||||||
invoke('startSubStoreBackendServer')
|
|
||||||
export const stopSubStoreBackendServer = (): Promise<void> => invoke('stopSubStoreBackendServer')
|
export const stopSubStoreBackendServer = (): Promise<void> => invoke('stopSubStoreBackendServer')
|
||||||
export const downloadSubStore = (): Promise<void> => invoke('downloadSubStore')
|
export const downloadSubStore = (): Promise<void> => invoke('downloadSubStore')
|
||||||
export const subStorePort = (): Promise<number> => invoke('subStorePort')
|
export const subStorePort = (): Promise<number> => invoke('subStorePort')
|
||||||
|
|||||||
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