support deeplink

This commit is contained in:
pompurin404 2024-08-04 13:30:40 +08:00
parent 2e194e9d35
commit 2eb3370df7
No known key found for this signature in database
8 changed files with 90 additions and 13 deletions

View File

@ -14,6 +14,11 @@ asarUnpack:
extraResources: extraResources:
- from: './resources/' - from: './resources/'
to: '' to: ''
protocols:
name: 'Mihomo Party URI Scheme'
schemes:
- 'clash'
- 'mihomo'
win: win:
target: target:
- nsis - nsis
@ -41,6 +46,8 @@ mac:
dmg: dmg:
artifactName: ${name}-macos-${version}-${arch}-installer.${ext} artifactName: ${name}-macos-${version}-${arch}-installer.${ext}
linux: linux:
desktop:
MimeType: 'x-scheme-handler/clash;x-scheme-handler/mihomo'
target: target:
- deb - deb
- rpm - rpm

View File

@ -6,6 +6,7 @@ import { window } from '..'
import axios from 'axios' import axios from 'axios'
import yaml from 'yaml' import yaml from 'yaml'
import fs from 'fs' import fs from 'fs'
import { dialog } from 'electron'
let profileConfig: IProfileConfig // profile.yaml let profileConfig: IProfileConfig // profile.yaml
let currentProfile: Partial<IMihomoConfig> // profiles/xxx.yaml let currentProfile: Partial<IMihomoConfig> // profiles/xxx.yaml
@ -95,6 +96,10 @@ export async function createProfile(item: Partial<IProfileItem>): Promise<IProfi
switch (newItem.type) { switch (newItem.type) {
case 'remote': { case 'remote': {
if (!item.url) { if (!item.url) {
dialog.showErrorBox(
'URL is required for remote profile',
'URL is required for remote profile'
)
throw new Error('URL is required for remote profile') throw new Error('URL is required for remote profile')
} }
try { try {
@ -126,12 +131,17 @@ export async function createProfile(item: Partial<IProfileItem>): Promise<IProfi
} }
fs.writeFileSync(profilePath(id), data, 'utf-8') fs.writeFileSync(profilePath(id), data, 'utf-8')
} catch (e) { } catch (e) {
dialog.showErrorBox('Failed to fetch remote profile', `${e}\nurl: ${item.url}`)
throw new Error(`Failed to fetch remote profile ${e}`) throw new Error(`Failed to fetch remote profile ${e}`)
} }
break break
} }
case 'local': { case 'local': {
if (!item.file) { if (!item.file) {
dialog.showErrorBox(
'File is required for local profile',
'File is required for local profile'
)
throw new Error('File is required for local profile') throw new Error('File is required for local profile')
} }
const data = item.file const data = item.file

View File

@ -8,7 +8,7 @@ import {
} from '../utils/dirs' } from '../utils/dirs'
import { generateProfile } from '../resolve/factory' import { generateProfile } from '../resolve/factory'
import { getAppConfig, setAppConfig } from '../config' import { getAppConfig, setAppConfig } from '../config'
import { safeStorage } from 'electron' import { dialog, safeStorage } from 'electron'
import fs from 'fs' import fs from 'fs'
let child: ChildProcess let child: ChildProcess
@ -51,7 +51,12 @@ export function restartCore(): void {
export function checkProfile(): void { export function checkProfile(): void {
const corePath = mihomoCorePath(getAppConfig().core ?? 'mihomo') const corePath = mihomoCorePath(getAppConfig().core ?? 'mihomo')
try {
execFileSync(corePath, ['-t', '-f', mihomoWorkConfigPath(), '-d', mihomoTestDir()]) execFileSync(corePath, ['-t', '-f', mihomoWorkConfigPath(), '-d', mihomoTestDir()])
} catch (e) {
dialog.showErrorBox('Profile check failed', `${e}`)
throw new Error('Profile check failed')
}
} }
export function grantCorePermition(corePath: string): void { export function grantCorePermition(corePath: string): void {

View File

@ -6,7 +6,7 @@ import { triggerSysProxy } from './resolve/sysproxy'
import icon from '../../resources/icon.png?asset' import icon from '../../resources/icon.png?asset'
import { createTray } from './core/tray' import { createTray } from './core/tray'
import { init } from './resolve/init' import { init } from './resolve/init'
import { getAppConfig } from './config' import { addProfileItem, getAppConfig } from './config'
import { join } from 'path' import { join } from 'path'
import { import {
startMihomoMemory, startMihomoMemory,
@ -23,11 +23,19 @@ if (!gotTheLock) {
app.quit() app.quit()
} else { } else {
init() init()
app.on('second-instance', () => { app.on('second-instance', (_event, commandline) => {
window?.show() window?.show()
window?.focusOnWebView() window?.focusOnWebView()
const url = commandline.pop()
if (url) {
handleDeepLink(url)
}
})
app.on('open-url', (_event, url) => {
window?.show()
window?.focusOnWebView()
handleDeepLink(url)
}) })
// Quit when all windows are closed, except on macOS. There, it's common // Quit when all windows are closed, except on macOS. There, it's common
// for applications and their menu bar to stay active until the user quits // for applications and their menu bar to stay active until the user quits
// explicitly with Cmd + Q. // explicitly with Cmd + Q.
@ -67,6 +75,27 @@ if (!gotTheLock) {
}) })
} }
function handleDeepLink(url: string): void {
if (url.startsWith('clash://install-config')) {
url = url.replace('clash://install-config/?url=', '').replace('clash://install-config?url=', '')
addProfileItem({
type: 'remote',
name: 'Remote File',
url
})
}
if (url.startsWith('mihomo://install-config')) {
url = url
.replace('mihomo://install-config/?url=', '')
.replace('mihomo://install-config?url=', '')
addProfileItem({
type: 'remote',
name: 'Remote File',
url
})
}
}
function createWindow(): void { function createWindow(): void {
// Create the browser window. // Create the browser window.
window = new BrowserWindow({ window = new BrowserWindow({

View File

@ -22,6 +22,7 @@ import path from 'path'
import { startPacServer } from './server' import { startPacServer } from './server'
import { triggerSysProxy } from './sysproxy' import { triggerSysProxy } from './sysproxy'
import { getAppConfig } from '../config' import { getAppConfig } from '../config'
import { app } from 'electron'
function initDirs(): void { function initDirs(): void {
if (!fs.existsSync(dataDir)) { if (!fs.existsSync(dataDir)) {
@ -71,10 +72,21 @@ function initFiles(): void {
} }
} }
function initDeeplink(): void {
if (process.defaultApp) {
if (process.argv.length >= 2) {
app.setAsDefaultProtocolClient('clash', process.execPath, [path.resolve(process.argv[1])])
}
} else {
app.setAsDefaultProtocolClient('clash')
}
}
export function init(): void { export function init(): void {
initDirs() initDirs()
initConfig() initConfig()
initFiles() initFiles()
initDeeplink()
startPacServer().then(() => { startPacServer().then(() => {
triggerSysProxy(getAppConfig().sysProxy.enable) triggerSysProxy(getAppConfig().sysProxy.enable)
}) })

View File

@ -59,5 +59,6 @@ export function registerIpcMainHandlers(): void {
ipcMain.handle('triggerSysProxy', (_e, enable) => triggerSysProxy(enable)) ipcMain.handle('triggerSysProxy', (_e, enable) => triggerSysProxy(enable))
ipcMain.handle('isEncryptionAvailable', isEncryptionAvailable) ipcMain.handle('isEncryptionAvailable', isEncryptionAvailable)
ipcMain.handle('encryptString', (_e, str) => safeStorage.encryptString(str)) ipcMain.handle('encryptString', (_e, str) => safeStorage.encryptString(str))
ipcMain.handle('platform', () => process.platform)
ipcMain.handle('quitApp', () => app.quit()) ipcMain.handle('quitApp', () => app.quit())
} }

View File

@ -3,7 +3,12 @@ import { useControledMihomoConfig } from '@renderer/hooks/use-controled-mihomo-c
import BorderSwitch from '@renderer/components/base/border-swtich' import BorderSwitch from '@renderer/components/base/border-swtich'
import { TbDeviceIpadHorizontalBolt } from 'react-icons/tb' import { TbDeviceIpadHorizontalBolt } from 'react-icons/tb'
import { useLocation, useNavigate } from 'react-router-dom' import { useLocation, useNavigate } from 'react-router-dom'
import { encryptString, patchMihomoConfig, isEncryptionAvailable } from '@renderer/utils/ipc' import {
platform,
encryptString,
patchMihomoConfig,
isEncryptionAvailable
} from '@renderer/utils/ipc'
import React, { useState } from 'react' import React, { useState } from 'react'
import { useAppConfig } from '@renderer/hooks/use-app-config' import { useAppConfig } from '@renderer/hooks/use-app-config'
import BasePasswordModal from '../base/base-password-modal' import BasePasswordModal from '../base/base-password-modal'
@ -19,6 +24,7 @@ const TunSwitcher: React.FC = () => {
const { enable } = tun || {} const { enable } = tun || {}
const onChange = async (enable: boolean): Promise<void> => { const onChange = async (enable: boolean): Promise<void> => {
if (enable && (await platform()) !== 'win32') {
const encryptionAvailable = await isEncryptionAvailable() const encryptionAvailable = await isEncryptionAvailable()
if (!appConfig?.encryptedPassword && encryptionAvailable) { if (!appConfig?.encryptedPassword && encryptionAvailable) {
setOpenPasswordModal(true) setOpenPasswordModal(true)
@ -27,6 +33,8 @@ const TunSwitcher: React.FC = () => {
if (!encryptionAvailable) { if (!encryptionAvailable) {
alert('加密不可用,请手动给内核授权') alert('加密不可用,请手动给内核授权')
} }
}
await patchControledMihomoConfig({ tun: { enable } }) await patchControledMihomoConfig({ tun: { enable } })
await patchMihomoConfig({ tun: { enable } }) await patchMihomoConfig({ tun: { enable } })
} }

View File

@ -113,6 +113,11 @@ export async function isEncryptionAvailable(): Promise<boolean> {
export async function encryptString(str: string): Promise<Buffer> { export async function encryptString(str: string): Promise<Buffer> {
return await window.electron.ipcRenderer.invoke('encryptString', str) return await window.electron.ipcRenderer.invoke('encryptString', str)
} }
export async function platform(): Promise<NodeJS.Platform> {
return await window.electron.ipcRenderer.invoke('platform')
}
export async function quitApp(): Promise<void> { export async function quitApp(): Promise<void> {
return await window.electron.ipcRenderer.invoke('quitApp') return await window.electron.ipcRenderer.invoke('quitApp')
} }