Add provider file view/edit

This commit is contained in:
xishang0128 2024-11-20 18:07:54 +08:00
parent 7b1fc24be4
commit e2653170c0
13 changed files with 356 additions and 99 deletions

View File

@ -26,9 +26,11 @@
"@electron-toolkit/preload": "^3.0.1",
"@electron-toolkit/utils": "^3.0.0",
"@mihomo-party/sysproxy": "^2.0.4",
"@types/crypto-js": "^4.2.2",
"adm-zip": "^0.5.16",
"axios": "^1.7.7",
"chokidar": "^4.0.1",
"crypto-js": "^4.2.0",
"dayjs": "^1.11.13",
"express": "^5.0.1",
"iconv-lite": "^0.6.3",

16
pnpm-lock.yaml generated
View File

@ -17,6 +17,9 @@ importers:
'@mihomo-party/sysproxy':
specifier: ^2.0.4
version: 2.0.4
'@types/crypto-js':
specifier: ^4.2.2
version: 4.2.2
adm-zip:
specifier: ^0.5.16
version: 0.5.16
@ -26,6 +29,9 @@ importers:
chokidar:
specifier: ^4.0.1
version: 4.0.1
crypto-js:
specifier: ^4.2.0
version: 4.2.0
dayjs:
specifier: ^1.11.13
version: 1.11.13
@ -2013,6 +2019,9 @@ packages:
'@types/connect@3.4.38':
resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==}
'@types/crypto-js@4.2.2':
resolution: {integrity: sha512-sDOLlVbHhXpAUAL0YHDUUwDZf3iN4Bwi4W6a0W0b+QcAezUbRtH4FVb+9J4h+XFPW7l/gQ9F8qC7P+Ec4k8QVQ==}
'@types/d3-array@3.2.1':
resolution: {integrity: sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==}
@ -2666,6 +2675,9 @@ packages:
crypt@0.0.2:
resolution: {integrity: sha512-mCxBlsHFYh9C+HVpiEacem8FEBnMXgU9gy4zmNC+SXAZNB/1idgp/aulFJ4FgCi7GPEVbfyng092GqL2k2rmow==}
crypto-js@4.2.0:
resolution: {integrity: sha512-KALDyEYgpY+Rlob/iriUtjV6d5Eq+Y191A5g4UqLAi8CyGP9N1+FdVbkc1SxKc2r4YAYqG8JzO2KGL+AizD70Q==}
cssesc@3.0.0:
resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==}
engines: {node: '>=4'}
@ -7904,6 +7916,8 @@ snapshots:
dependencies:
'@types/node': 22.9.0
'@types/crypto-js@4.2.2': {}
'@types/d3-array@3.2.1': {}
'@types/d3-color@3.1.3': {}
@ -8694,6 +8708,8 @@ snapshots:
crypt@0.0.2: {}
crypto-js@4.2.0: {}
cssesc@3.0.0: {}
csstype@3.1.3: {}

View File

@ -5,6 +5,8 @@ export {
getCurrentProfileItem,
getProfileItem,
getProfileConfig,
getFileStr,
setFileStr,
setProfileConfig,
addProfileItem,
removeProfileItem,

View File

@ -140,10 +140,10 @@ export async function createProfile(item: Partial<IProfileItem>): Promise<IProfi
res = await axios.get(item.url, {
proxy: newItem.useProxy
? {
protocol: 'http',
host: '127.0.0.1',
port: mixedPort
}
protocol: 'http',
host: '127.0.0.1',
port: mixedPort
}
: false,
headers: {
'User-Agent': userAgent || 'clash.meta'
@ -220,3 +220,23 @@ function parseSubinfo(str: string): ISubscriptionUserInfo {
})
return obj
}
function isAbsolutePath(path: string): boolean {
return path.startsWith('/') || /^[a-zA-Z]:\\/.test(path);
}
export async function getFileStr(path: string): Promise<string> {
if (isAbsolutePath(path)) {
return await readFile(path, 'utf-8')
} else {
return await readFile(mihomoProfileWorkDir(path), 'utf-8')
}
}
export async function setFileStr(path: string, content: string): Promise<void> {
if (isAbsolutePath(path)) {
await writeFile(path, content, 'utf-8')
} else {
await writeFile(mihomoProfileWorkDir(path), content, 'utf-8')
}
}

View File

@ -114,6 +114,11 @@ export const mihomoUpdateProxyProviders = async (name: string): Promise<void> =>
return await instance.put(`/providers/proxies/${encodeURIComponent(name)}`)
}
export const mihomoRunProxyProviders = async (): Promise<IMihomoProxyProviders> => {
const runtime = await getRuntimeConfig()
return runtime?.['proxy-providers']
}
export const mihomoRuleProviders = async (): Promise<IMihomoRuleProviders> => {
const instance = await getAxios()
return await instance.get('/providers/rules')
@ -124,6 +129,11 @@ export const mihomoUpdateRuleProviders = async (name: string): Promise<void> =>
return await instance.put(`/providers/rules/${encodeURIComponent(name)}`)
}
export const mihomoRunRuleProviders = async (): Promise<IMihomoRuleProviders> => {
const runtime = await getRuntimeConfig()
return runtime?.['rule-providers']
}
export const mihomoChangeProxy = async (group: string, proxy: string): Promise<IMihomoProxy> => {
const instance = await getAxios()
return await instance.put(`/proxies/${encodeURIComponent(group)}`, { name: proxy })
@ -194,9 +204,9 @@ const mihomoTraffic = async (): Promise<void> => {
if (process.platform !== 'linux') {
tray?.setToolTip(
'↑' +
`${calcTraffic(json.up)}/s`.padStart(9) +
'\n↓' +
`${calcTraffic(json.down)}/s`.padStart(9)
`${calcTraffic(json.up)}/s`.padStart(9) +
'\n↓' +
`${calcTraffic(json.down)}/s`.padStart(9)
)
}
floatingWindow?.webContents.send('mihomoTraffic', json)

View File

@ -8,7 +8,9 @@ import {
mihomoProxies,
mihomoProxyDelay,
mihomoProxyProviders,
mihomoRunProxyProviders,
mihomoRuleProviders,
mihomoRunRuleProviders,
mihomoRules,
mihomoUnfixedProxy,
mihomoUpdateProxyProviders,
@ -31,6 +33,8 @@ import {
removeProfileItem,
changeCurrentProfile,
getProfileStr,
getFileStr,
setFileStr,
setProfileStr,
updateProfileItem,
setProfileConfig,
@ -115,10 +119,12 @@ export function registerIpcMainHandlers(): void {
ipcMain.handle('mihomoProxies', ipcErrorWrapper(mihomoProxies))
ipcMain.handle('mihomoGroups', ipcErrorWrapper(mihomoGroups))
ipcMain.handle('mihomoProxyProviders', ipcErrorWrapper(mihomoProxyProviders))
ipcMain.handle('mihomoRunProxyProviders', ipcErrorWrapper(mihomoRunProxyProviders))
ipcMain.handle('mihomoUpdateProxyProviders', (_e, name) =>
ipcErrorWrapper(mihomoUpdateProxyProviders)(name)
)
ipcMain.handle('mihomoRuleProviders', ipcErrorWrapper(mihomoRuleProviders))
ipcMain.handle('mihomoRunRuleProviders', ipcErrorWrapper(mihomoRunRuleProviders))
ipcMain.handle('mihomoUpdateRuleProviders', (_e, name) =>
ipcErrorWrapper(mihomoUpdateRuleProviders)(name)
)
@ -151,6 +157,8 @@ export function registerIpcMainHandlers(): void {
ipcMain.handle('getCurrentProfileItem', ipcErrorWrapper(getCurrentProfileItem))
ipcMain.handle('getProfileItem', (_e, id) => ipcErrorWrapper(getProfileItem)(id))
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('setProfileStr', (_e, id, str) => ipcErrorWrapper(setProfileStr)(id, str))
ipcMain.handle('updateProfileItem', (_e, item) => ipcErrorWrapper(updateProfileItem)(item))
ipcMain.handle('changeCurrentProfile', (_e, id) => ipcErrorWrapper(changeCurrentProfile)(id))

View File

@ -7,7 +7,7 @@ import pac from 'types-pac/pac.d.ts?raw'
import { useTheme } from 'next-themes'
import { nanoid } from 'nanoid'
import React from 'react'
type Language = 'yaml' | 'javascript' | 'css'
type Language = 'yaml' | 'javascript' | 'css' | 'json' | 'text'
interface Props {
value: string
@ -125,9 +125,9 @@ export const BaseEditor: React.FC<Props> = (props) => {
options={{
tabSize: ['yaml', 'javascript', 'json'].includes(language) ? 2 : 4, // 根据语言类型设置缩进大小
minimap: {
enabled: document.documentElement.clientWidth >= 1500 // 超过一定宽度显示minimap滚动条
enabled: document.documentElement.clientWidth >= 1500 // 超过一定宽度显示 minimap 滚动条
},
mouseWheelZoom: true, // 按住Ctrl滚轮调节缩放比例
mouseWheelZoom: true, // 按住 Ctrl 滚轮调节缩放比例
readOnly: readOnly, // 只读模式
renderValidationDecorations: 'on', // 只读模式下显示校验信息
quickSuggestions: {

View File

@ -1,14 +1,42 @@
import { mihomoProxyProviders, mihomoUpdateProxyProviders } from '@renderer/utils/ipc'
import { Fragment, useMemo, useState } from 'react'
import { mihomoProxyProviders, mihomoUpdateProxyProviders, mihomoRunProxyProviders } from '@renderer/utils/ipc'
import { Fragment, useEffect, useMemo, useState } from 'react'
import Viewer from './viewer'
import useSWR from 'swr'
import SettingCard from '../base/base-setting-card'
import SettingItem from '../base/base-setting-item'
import { Button, Chip } from '@nextui-org/react'
import { IoMdRefresh } from 'react-icons/io'
import { IoMdRefresh, IoMdEye } from 'react-icons/io'
import { CgLoadbarDoc } from 'react-icons/cg'
import dayjs from 'dayjs'
import { calcTraffic } from '@renderer/utils/calc'
import { getHash } from '@renderer/utils/hash'
const ProxyProvider: React.FC = () => {
const [ShowProvider, setShowProvider] = useState(false)
const [ShowPath, setShowPath] = useState('')
const [ShowType, setShowType] = useState('')
useEffect(() => {
const fetchProviderPath = async (name: string) => {
try {
const providers = await mihomoRunProxyProviders()
const provider = providers[name]
if (provider?.path) {
setShowPath(provider.path)
} else if (provider?.url) {
setShowPath(`proxies/` + getHash(provider.url))
}
setShowProvider(true)
} catch (error) {
setShowPath('')
}
}
if (ShowPath != '') {
fetchProviderPath(ShowPath)
}
}, [ShowProvider, ShowPath])
const { data, mutate } = useSWR('mihomoProxyProviders', mihomoProxyProviders)
const providers = useMemo(() => {
if (!data) return []
@ -45,6 +73,7 @@ const ProxyProvider: React.FC = () => {
return (
<SettingCard>
{ShowProvider && <Viewer onClose={() => { setShowProvider(false); setShowPath(''); setShowType('')}} path={ShowPath} type={ShowType} />}
<SettingItem title="代理集合" divider>
<Button
size="sm"
@ -58,56 +87,69 @@ const ProxyProvider: React.FC = () => {
</Button>
</SettingItem>
{providers.map((provider, index) => {
return (
<Fragment key={provider.name}>
{providers.map((provider, index) => (
<Fragment key={provider.name}>
<SettingItem
title={provider.name}
actions={
<Chip className="ml-2" size="sm">
{provider.proxies?.length || 0}
</Chip>
}
divider={!provider.subscriptionInfo && index !== providers.length - 1}
>
<div className="flex h-[32px] leading-[32px] text-foreground-500">
<div>{dayjs(provider.updatedAt).fromNow()}</div>
<Button
isIconOnly
className="ml-2"
size="sm"
onPress={() => {
onUpdate(provider.name, index)
}}
>
<IoMdRefresh className={`text-lg ${updating[index] ? 'animate-spin' : ''}`} />
</Button>
<Button
isIconOnly
className="ml-2"
size="sm"
>
<IoMdEye className="text-lg" />
</Button>
<Button
isIconOnly
className="ml-2"
size="sm"
onPress={() => {
setShowType(provider.vehicleType)
setShowPath(provider.name)
}}
>
<CgLoadbarDoc className="text-lg" />
</Button>
</div>
</SettingItem>
{provider.subscriptionInfo && (
<SettingItem
title={provider.name}
actions={
<Chip className="ml-2" size="sm">
{provider.proxies?.length || 0}
</Chip>
}
divider={!provider.subscriptionInfo && index !== providers.length - 1}
>
{
<div className="flex h-[32px] leading-[32px] text-foreground-500">
<div>{dayjs(provider.updatedAt).fromNow()}</div>
<Button
isIconOnly
className="ml-2"
size="sm"
onPress={() => {
onUpdate(provider.name, index)
}}
>
<IoMdRefresh className={`text-lg ${updating[index] ? 'animate-spin' : ''}`} />
</Button>
divider={index !== providers.length - 1}
title={
<div className="text-foreground-500">
{`${calcTraffic(
provider.subscriptionInfo.Upload + provider.subscriptionInfo.Download
)} / ${calcTraffic(provider.subscriptionInfo.Total)}`}
</div>
}
>
<div className="h-[32px] leading-[32px] text-foreground-500">
{provider.subscriptionInfo.Expire
? dayjs.unix(provider.subscriptionInfo.Expire).format('YYYY-MM-DD')
: '长期有效'}
</div>
</SettingItem>
{provider.subscriptionInfo && (
<SettingItem
divider={index !== providers.length - 1}
title={
<div className="text-foreground-500">{`${calcTraffic(
provider.subscriptionInfo.Upload + provider.subscriptionInfo.Download
)}
/${calcTraffic(provider.subscriptionInfo.Total)}`}</div>
}
>
{provider.subscriptionInfo && (
<div className="h-[32px] leading-[32px] text-foreground-500">
{provider.subscriptionInfo.Expire
? dayjs.unix(provider.subscriptionInfo.Expire).format('YYYY-MM-DD')
: '长期有效'}
</div>
)}
</SettingItem>
)}
</Fragment>
)
})}
)}
</Fragment>
))}
</SettingCard>
)
}

View File

@ -1,13 +1,21 @@
import { mihomoRuleProviders, mihomoUpdateRuleProviders } from '@renderer/utils/ipc'
import { Fragment, useMemo, useState } from 'react'
import { mihomoRuleProviders, mihomoUpdateRuleProviders, mihomoRunRuleProviders } from '@renderer/utils/ipc'
import { getHash } from '@renderer/utils/hash'
import Viewer from './viewer'
import { Fragment, useEffect, useMemo, useState } from 'react'
import useSWR from 'swr'
import SettingCard from '../base/base-setting-card'
import SettingItem from '../base/base-setting-item'
import { Button, Chip } from '@nextui-org/react'
import { IoMdRefresh } from 'react-icons/io'
import { CgLoadbarDoc } from 'react-icons/cg'
import dayjs from 'dayjs'
const RuleProvider: React.FC = () => {
const [ShowProvider, setShowProvider] = useState(false)
const [ShowPath, setShowPath] = useState('')
const [ShowType, setShowType] = useState('')
const [ShowFormat, setShowFormat] = useState('')
const { data, mutate } = useSWR('mihomoRuleProviders', mihomoRuleProviders)
const providers = useMemo(() => {
if (!data) return []
@ -16,6 +24,26 @@ const RuleProvider: React.FC = () => {
}, [data])
const [updating, setUpdating] = useState(Array(providers.length).fill(false))
useEffect(() => {
const fetchProviderPath = async (name: string) => {
try {
const providers = await mihomoRunRuleProviders()
const provider = providers[name]
if (provider?.path) {
setShowPath(provider.path)
} else if (provider?.url) {
setShowPath(`rules/` + getHash(provider.url))
}
setShowProvider(true)
} catch (error) {
setShowPath('')
}
}
if (ShowPath != '') {
fetchProviderPath(ShowPath)
}
}, [ShowProvider, ShowPath])
const onUpdate = async (name: string, index: number): Promise<void> => {
setUpdating((prev) => {
prev[index] = true
@ -40,6 +68,12 @@ const RuleProvider: React.FC = () => {
return (
<SettingCard>
{ShowProvider && <Viewer
path={ShowPath}
type={ShowType}
format={ShowFormat}
onClose={() => { setShowProvider(false); setShowPath(''); setShowType('') }}
/>}
<SettingItem title="规则集合" divider>
<Button
size="sm"
@ -53,44 +87,54 @@ const RuleProvider: React.FC = () => {
</Button>
</SettingItem>
{providers.map((provider, index) => {
return (
<Fragment key={provider.name}>
<SettingItem
title={provider.name}
actions={
<Chip className="ml-2" size="sm">
{provider.ruleCount}
</Chip>
}
>
{
<div className="flex h-[32px] leading-[32px] text-foreground-500">
<div>{dayjs(provider.updatedAt).fromNow()}</div>
<Button
isIconOnly
className="ml-2"
size="sm"
onPress={() => {
onUpdate(provider.name, index)
}}
>
<IoMdRefresh className={`text-lg ${updating[index] ? 'animate-spin' : ''}`} />
</Button>
</div>
}
</SettingItem>
<SettingItem
title={<div className="text-foreground-500">{provider.format}</div>}
divider={index !== providers.length - 1}
>
<div className="h-[32px] leading-[32px] text-foreground-500">
{provider.vehicleType}::{provider.behavior}
</div>
</SettingItem>
</Fragment>
)
})}
{providers.map((provider, index) => (
<Fragment key={provider.name}>
<SettingItem
title={provider.name}
actions={
<Chip className="ml-2" size="sm">
{provider.ruleCount}
</Chip>
}
>
<div className="flex h-[32px] leading-[32px] text-foreground-500">
<div>{dayjs(provider.updatedAt).fromNow()}</div>
<Button
isIconOnly
className="ml-2"
size="sm"
onPress={() => {
onUpdate(provider.name, index)
}}
>
<IoMdRefresh className={`text-lg ${updating[index] ? 'animate-spin' : ''}`} />
</Button>
{provider.format !== "MrsRule" && (
<Button
isIconOnly
className="ml-2"
size="sm"
onPress={() => {
setShowType(provider.vehicleType)
setShowFormat(provider.format)
setShowPath(provider.name)
}}
>
<CgLoadbarDoc className={`text-lg`} />
</Button>
)}
</div>
</SettingItem>
<SettingItem
title={<div className="text-foreground-500">{provider.format}</div>}
divider={index !== providers.length - 1}
>
<div className="h-[32px] leading-[32px] text-foreground-500">
{provider.vehicleType}::{provider.behavior}
</div>
</SettingItem>
</Fragment>
))}
</SettingCard>
)
}

View File

@ -0,0 +1,62 @@
import { Modal, ModalContent, ModalHeader, ModalBody, ModalFooter, Button } from '@nextui-org/react'
import React, { useEffect, useState } from 'react'
import { BaseEditor } from '../base/base-editor'
import { getFileStr, setFileStr } from '@renderer/utils/ipc'
type Language = 'yaml' | 'javascript' | 'css' | 'json' | 'text'
interface Props {
onClose: () => void
path: string
type: string
format?: string
}
const Viewer: React.FC<Props> = (props) => {
const { type, path, format, onClose } = props
const [currData, setCurrData] = useState('')
const language: Language = (!format || format === 'YamlRule') ? 'yaml' : 'text'
const getContent = async (): Promise<void> => {
setCurrData(await getFileStr(path))
}
useEffect(() => {
getContent()
}, [])
return (
<Modal
backdrop="blur"
classNames={{ backdrop: 'top-[48px]' }}
size="5xl"
hideCloseButton
isOpen={true}
onOpenChange={onClose}
scrollBehavior="inside"
>
<ModalContent className="h-full w-[calc(100%-100px)]">
<ModalHeader className="flex pb-0 app-drag">Provider </ModalHeader>
<ModalBody className="h-full">
<BaseEditor language={language} value={currData} readOnly={type != 'File'} onChange={(value) => setCurrData(value)}/>
</ModalBody>
<ModalFooter className="pt-0">
<Button size="sm" variant="light" onPress={onClose}>
</Button>
{type == 'File' &&
<Button
size="sm"
color="primary"
onPress={async () => {
await setFileStr(path, currData)
onClose()
}}
>
</Button>}
</ModalFooter>
</ModalContent>
</Modal>
)
}
export default Viewer

View File

@ -0,0 +1,31 @@
import { MD5 } from 'crypto-js';
export class HashType {
private hashValue: string;
constructor(hash: string) {
this.hashValue = hash;
}
static makeHash(data: string): HashType {
const hash = MD5(data).toString();
return new HashType(hash);
}
equal(hash: HashType): boolean {
return this.hashValue === hash.hashValue;
}
toString(): string {
return this.hashValue;
}
isValid(): boolean {
return this.hashValue.length === 32;
}
}
export function getHash(name: string): string {
const hash = HashType.makeHash(name);
return hash.toString();
}

View File

@ -43,6 +43,10 @@ export async function mihomoUpdateProxyProviders(name: string): Promise<void> {
)
}
export async function mihomoRunProxyProviders(): Promise<IMihomoRuleProviders> {
return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('mihomoRunProxyProviders'))
}
export async function mihomoRuleProviders(): Promise<IMihomoRuleProviders> {
return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('mihomoRuleProviders'))
}
@ -53,6 +57,10 @@ export async function mihomoUpdateRuleProviders(name: string): Promise<void> {
)
}
export async function mihomoRunRuleProviders(): Promise<IMihomoRuleProviders> {
return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('mihomoRunRuleProviders'))
}
export async function mihomoChangeProxy(group: string, proxy: string): Promise<IMihomoProxy> {
return ipcErrorWrapper(
await window.electron.ipcRenderer.invoke('mihomoChangeProxy', group, proxy)
@ -151,6 +159,14 @@ export async function getProfileStr(id: string): Promise<string> {
return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('getProfileStr', id))
}
export async function getFileStr(id: string): Promise<string> {
return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('getFileStr', id))
}
export async function setFileStr(id: string, str: string): Promise<void> {
return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('setFileStr', id, str))
}
export async function setProfileStr(id: string, str: string): Promise<void> {
return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('setProfileStr', id, str))
}

View File

@ -179,6 +179,8 @@ interface IMihomoRuleProvider {
type: string
updatedAt: string
vehicleType: string
url: string
path: string
}
interface IMihomoProxyProviders {
@ -201,6 +203,8 @@ interface IMihomoProxyProvider {
testUrl?: string
updatedAt?: string
vehicleType: string
url: string
path: string
}
interface ISysProxyConfig {