mirror of
https://gh.catmak.name/https://github.com/mihomo-party-org/mihomo-party
synced 2025-12-26 20:50:30 +08:00
feat: add mrs ruleset preview suppor
This commit is contained in:
parent
19ae63b253
commit
94f52cf636
@ -14,7 +14,8 @@ export {
|
||||
getProfileStr,
|
||||
setProfileStr,
|
||||
changeCurrentProfile,
|
||||
updateProfileItem
|
||||
updateProfileItem,
|
||||
convertMrsRuleset
|
||||
} from './profile'
|
||||
export {
|
||||
getOverrideConfig,
|
||||
|
||||
@ -329,3 +329,43 @@ export async function setFileStr(path: string, content: string): Promise<void> {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
export async function convertMrsRuleset(filePath: string, behavior: string): Promise<string> {
|
||||
const { exec } = await import('child_process')
|
||||
const { promisify } = await import('util')
|
||||
const execAsync = promisify(exec)
|
||||
const { mihomoCorePath } = await import('../utils/dirs')
|
||||
const { getAppConfig } = await import('./app')
|
||||
const { tmpdir } = await import('os')
|
||||
const { randomBytes } = await import('crypto')
|
||||
const { unlink } = await import('fs/promises')
|
||||
|
||||
const { core = 'mihomo' } = await getAppConfig()
|
||||
const corePath = mihomoCorePath(core)
|
||||
const { diffWorkDir = false } = await getAppConfig()
|
||||
const { current } = await getProfileConfig()
|
||||
let fullPath: string
|
||||
if (isAbsolutePath(filePath)) {
|
||||
fullPath = filePath
|
||||
} else {
|
||||
fullPath = join(diffWorkDir ? mihomoProfileWorkDir(current) : mihomoWorkDir(), filePath)
|
||||
}
|
||||
|
||||
const tempFileName = `mrs-convert-${randomBytes(8).toString('hex')}.txt`
|
||||
const tempFilePath = join(tmpdir(), tempFileName)
|
||||
|
||||
try {
|
||||
// 使用 mihomo convert-ruleset 命令转换 MRS 文件为 text 格式
|
||||
// 命令格式: mihomo convert-ruleset <behavior> <format> <source>
|
||||
await execAsync(`"${corePath}" convert-ruleset ${behavior} mrs "${fullPath}" "${tempFilePath}"`)
|
||||
const content = await readFile(tempFilePath, 'utf-8')
|
||||
await unlink(tempFilePath)
|
||||
|
||||
return content
|
||||
} catch (error) {
|
||||
try {
|
||||
await unlink(tempFilePath)
|
||||
} catch {}
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
@ -48,7 +48,8 @@ import {
|
||||
removeOverrideItem,
|
||||
getOverride,
|
||||
setOverride,
|
||||
updateOverrideItem
|
||||
updateOverrideItem,
|
||||
convertMrsRuleset
|
||||
} from '../config'
|
||||
import {
|
||||
startSubStoreFrontendServer,
|
||||
@ -215,6 +216,7 @@ export function registerIpcMainHandlers(): void {
|
||||
ipcMain.handle('getProfileStr', (_e, id) => ipcErrorWrapper(getProfileStr)(id))
|
||||
ipcMain.handle('getFileStr', (_e, path) => ipcErrorWrapper(getFileStr)(path))
|
||||
ipcMain.handle('setFileStr', (_e, path, str) => ipcErrorWrapper(setFileStr)(path, str))
|
||||
ipcMain.handle('convertMrsRuleset', (_e, path, behavior) => ipcErrorWrapper(convertMrsRuleset)(path, behavior))
|
||||
ipcMain.handle('setProfileStr', (_e, id, str) => ipcErrorWrapper(setProfileStr)(id, str))
|
||||
ipcMain.handle('updateProfileItem', (_e, item) => ipcErrorWrapper(updateProfileItem)(item))
|
||||
ipcMain.handle('changeCurrentProfile', (_e, id) => ipcErrorWrapper(changeCurrentProfile)(id))
|
||||
|
||||
@ -25,7 +25,8 @@ const RuleProvider: React.FC = () => {
|
||||
type: '',
|
||||
title: '',
|
||||
format: '',
|
||||
privderType: ''
|
||||
privderType: '',
|
||||
behavior: ''
|
||||
})
|
||||
useEffect(() => {
|
||||
if (showDetails.title) {
|
||||
@ -37,11 +38,12 @@ const RuleProvider: React.FC = () => {
|
||||
setShowDetails((prev) => ({
|
||||
...prev,
|
||||
show: true,
|
||||
path: provider?.path || `rules/${getHash(provider?.url)}`
|
||||
path: provider?.path || `rules/${getHash(provider?.url)}`,
|
||||
behavior: provider?.behavior || 'domain'
|
||||
}))
|
||||
}
|
||||
} catch {
|
||||
setShowDetails((prev) => ({ ...prev, path: '' }))
|
||||
setShowDetails((prev) => ({ ...prev, path: '', behavior: '' }))
|
||||
}
|
||||
}
|
||||
fetchProviderPath(showDetails.title)
|
||||
@ -94,7 +96,8 @@ const RuleProvider: React.FC = () => {
|
||||
title={showDetails.title}
|
||||
format={showDetails.format}
|
||||
privderType={showDetails.privderType}
|
||||
onClose={() => setShowDetails({ show: false, path: '', type: '', title: '', format: '', privderType: '' })}
|
||||
behavior={showDetails.behavior}
|
||||
onClose={() => setShowDetails({ show: false, path: '', type: '', title: '', format: '', privderType: '', behavior: '' })}
|
||||
/>
|
||||
)}
|
||||
<SettingItem title={t('resources.ruleProviders.title')} divider>
|
||||
@ -122,7 +125,6 @@ const RuleProvider: React.FC = () => {
|
||||
>
|
||||
<div className="flex h-[32px] leading-[32px] text-foreground-500">
|
||||
<div>{dayjs(provider.updatedAt).fromNow()}</div>
|
||||
{provider.format !== 'MrsRule' && (
|
||||
<Button
|
||||
isIconOnly
|
||||
title={provider.vehicleType == 'File' ? t('common.editor.edit') : t('common.viewer.view')}
|
||||
@ -135,7 +137,8 @@ const RuleProvider: React.FC = () => {
|
||||
path: provider.name,
|
||||
type: provider.vehicleType,
|
||||
title: provider.name,
|
||||
format: provider.format
|
||||
format: provider.format,
|
||||
behavior: provider.behavior || 'domain'
|
||||
})
|
||||
}}
|
||||
>
|
||||
@ -145,7 +148,6 @@ const RuleProvider: React.FC = () => {
|
||||
<CgLoadbarDoc className={`text-lg`} />
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
<Button
|
||||
isIconOnly
|
||||
title={t('common.updater.update')}
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import { Modal, ModalContent, ModalHeader, ModalBody, ModalFooter, Button } from '@heroui/react'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { BaseEditor } from '../base/base-editor'
|
||||
import { getFileStr, setFileStr } from '@renderer/utils/ipc'
|
||||
import { getFileStr, setFileStr, convertMrsRuleset, getRuntimeConfig } from '@renderer/utils/ipc'
|
||||
import yaml from 'js-yaml'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
type Language = 'yaml' | 'javascript' | 'css' | 'json' | 'text'
|
||||
@ -13,15 +13,38 @@ interface Props {
|
||||
title: string
|
||||
privderType: string
|
||||
format?: string
|
||||
behavior?: string
|
||||
}
|
||||
const Viewer: React.FC<Props> = (props) => {
|
||||
const { t } = useTranslation()
|
||||
const { type, path, title, format, privderType, onClose } = props
|
||||
const { type, path, title, format, privderType, behavior, onClose } = props
|
||||
const [currData, setCurrData] = useState('')
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
let language: Language = !format || format === 'YamlRule' ? 'yaml' : 'text'
|
||||
|
||||
const getContent = async (): Promise<void> => {
|
||||
setIsLoading(true)
|
||||
try {
|
||||
let fileContent: React.SetStateAction<string>
|
||||
|
||||
if (format === 'MrsRule') {
|
||||
language = 'text'
|
||||
let ruleBehavior: string = behavior || 'domain'
|
||||
if (!behavior) {
|
||||
try {
|
||||
const runtimeConfig = await getRuntimeConfig()
|
||||
const provider = runtimeConfig['rule-providers']?.[title]
|
||||
ruleBehavior = provider?.behavior || 'domain'
|
||||
} catch {
|
||||
ruleBehavior = 'domain'
|
||||
}
|
||||
}
|
||||
|
||||
fileContent = await convertMrsRuleset(path, ruleBehavior)
|
||||
setCurrData(fileContent)
|
||||
return
|
||||
}
|
||||
|
||||
if (type === 'Inline') {
|
||||
fileContent = await getFileStr('config.yaml')
|
||||
language = 'yaml'
|
||||
@ -42,6 +65,9 @@ const Viewer: React.FC<Props> = (props) => {
|
||||
} catch (error) {
|
||||
setCurrData(fileContent)
|
||||
}
|
||||
} finally {
|
||||
setIsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
@ -61,18 +87,24 @@ const Viewer: React.FC<Props> = (props) => {
|
||||
<ModalContent className="h-full w-[calc(100%-100px)]">
|
||||
<ModalHeader className="flex pb-0 app-drag">{title}</ModalHeader>
|
||||
<ModalBody className="h-full">
|
||||
{isLoading ? (
|
||||
<div className="flex items-center justify-center h-full">
|
||||
<div className="text-foreground-500">{t('common.loading')}</div>
|
||||
</div>
|
||||
) : (
|
||||
<BaseEditor
|
||||
language={language}
|
||||
value={currData}
|
||||
readOnly={type != 'File'}
|
||||
readOnly={type != 'File' || format === 'MrsRule'}
|
||||
onChange={(value) => setCurrData(value)}
|
||||
/>
|
||||
)}
|
||||
</ModalBody>
|
||||
<ModalFooter className="pt-0">
|
||||
<Button size="sm" variant="light" onPress={onClose}>
|
||||
{t('common.close')}
|
||||
</Button>
|
||||
{type == 'File' && (
|
||||
{type == 'File' && format !== 'MrsRule' && (
|
||||
<Button
|
||||
size="sm"
|
||||
color="primary"
|
||||
|
||||
@ -16,6 +16,7 @@
|
||||
"common.auto": "Auto",
|
||||
"common.default": "Default",
|
||||
"common.close": "Close",
|
||||
"common.loading": "Loading...",
|
||||
"common.pinWindow": "Pin Window",
|
||||
"common.next": "Next",
|
||||
"common.prev": "Previous",
|
||||
|
||||
@ -16,6 +16,7 @@
|
||||
"common.auto": "خودکار",
|
||||
"common.default": "پیشفرض",
|
||||
"common.close": "بستن",
|
||||
"common.loading": "در حال بارگذاری...",
|
||||
"common.pinWindow": "پین کردن پنجره",
|
||||
"common.next": "بعدی",
|
||||
"common.prev": "قبلی",
|
||||
|
||||
@ -16,6 +16,7 @@
|
||||
"common.auto": "Авто",
|
||||
"common.default": "По умолчанию",
|
||||
"common.close": "Закрыть",
|
||||
"common.loading": "Загрузка...",
|
||||
"common.pinWindow": "Закрепить окно",
|
||||
"common.next": "Далее",
|
||||
"common.prev": "Назад",
|
||||
|
||||
@ -16,6 +16,7 @@
|
||||
"common.auto": "自动",
|
||||
"common.default": "默认",
|
||||
"common.close": "关闭",
|
||||
"common.loading": "加载中...",
|
||||
"common.pinWindow": "窗口置顶",
|
||||
"common.next": "下一步",
|
||||
"common.prev": "上一步",
|
||||
|
||||
@ -208,6 +208,10 @@ export async function setProfileStr(id: string, str: string): Promise<void> {
|
||||
return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('setProfileStr', id, str))
|
||||
}
|
||||
|
||||
export async function convertMrsRuleset(path: string, behavior: string): Promise<string> {
|
||||
return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('convertMrsRuleset', path, behavior))
|
||||
}
|
||||
|
||||
export async function getOverrideConfig(force = false): Promise<IOverrideConfig> {
|
||||
return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('getOverrideConfig', force))
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user