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

View File

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

View File

@ -8,7 +8,7 @@ import {
} from '../utils/dirs'
import { generateProfile } from '../resolve/factory'
import { getAppConfig, setAppConfig } from '../config'
import { safeStorage } from 'electron'
import { dialog, safeStorage } from 'electron'
import fs from 'fs'
let child: ChildProcess
@ -51,7 +51,12 @@ export function restartCore(): void {
export function checkProfile(): void {
const corePath = mihomoCorePath(getAppConfig().core ?? 'mihomo')
execFileSync(corePath, ['-t', '-f', mihomoWorkConfigPath(), '-d', mihomoTestDir()])
try {
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 {

View File

@ -6,7 +6,7 @@ import { triggerSysProxy } from './resolve/sysproxy'
import icon from '../../resources/icon.png?asset'
import { createTray } from './core/tray'
import { init } from './resolve/init'
import { getAppConfig } from './config'
import { addProfileItem, getAppConfig } from './config'
import { join } from 'path'
import {
startMihomoMemory,
@ -23,11 +23,19 @@ if (!gotTheLock) {
app.quit()
} else {
init()
app.on('second-instance', () => {
app.on('second-instance', (_event, commandline) => {
window?.show()
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
// for applications and their menu bar to stay active until the user quits
// 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 {
// Create the browser window.
window = new BrowserWindow({

View File

@ -22,6 +22,7 @@ import path from 'path'
import { startPacServer } from './server'
import { triggerSysProxy } from './sysproxy'
import { getAppConfig } from '../config'
import { app } from 'electron'
function initDirs(): void {
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 {
initDirs()
initConfig()
initFiles()
initDeeplink()
startPacServer().then(() => {
triggerSysProxy(getAppConfig().sysProxy.enable)
})

View File

@ -59,5 +59,6 @@ export function registerIpcMainHandlers(): void {
ipcMain.handle('triggerSysProxy', (_e, enable) => triggerSysProxy(enable))
ipcMain.handle('isEncryptionAvailable', isEncryptionAvailable)
ipcMain.handle('encryptString', (_e, str) => safeStorage.encryptString(str))
ipcMain.handle('platform', () => process.platform)
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 { TbDeviceIpadHorizontalBolt } from 'react-icons/tb'
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 { useAppConfig } from '@renderer/hooks/use-app-config'
import BasePasswordModal from '../base/base-password-modal'
@ -19,14 +24,17 @@ const TunSwitcher: React.FC = () => {
const { enable } = tun || {}
const onChange = async (enable: boolean): Promise<void> => {
const encryptionAvailable = await isEncryptionAvailable()
if (!appConfig?.encryptedPassword && encryptionAvailable) {
setOpenPasswordModal(true)
return
}
if (!encryptionAvailable) {
alert('加密不可用,请手动给内核授权')
if (enable && (await platform()) !== 'win32') {
const encryptionAvailable = await isEncryptionAvailable()
if (!appConfig?.encryptedPassword && encryptionAvailable) {
setOpenPasswordModal(true)
return
}
if (!encryptionAvailable) {
alert('加密不可用,请手动给内核授权')
}
}
await patchControledMihomoConfig({ 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> {
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> {
return await window.electron.ipcRenderer.invoke('quitApp')
}