From e2653170c009a7ae8fcea754574bdbc358fb3336 Mon Sep 17 00:00:00 2001 From: xishang0128 Date: Wed, 20 Nov 2024 18:07:54 +0800 Subject: [PATCH] Add provider file view/edit --- package.json | 2 + pnpm-lock.yaml | 16 ++ src/main/config/index.ts | 2 + src/main/config/profile.ts | 28 +++- src/main/core/mihomoApi.ts | 16 +- src/main/utils/ipc.ts | 8 + .../src/components/base/base-editor.tsx | 6 +- .../components/resources/proxy-provider.tsx | 140 ++++++++++++------ .../components/resources/rule-provider.tsx | 124 +++++++++++----- .../src/components/resources/viewer.tsx | 62 ++++++++ src/renderer/src/utils/hash.ts | 31 ++++ src/renderer/src/utils/ipc.ts | 16 ++ src/shared/types.d.ts | 4 + 13 files changed, 356 insertions(+), 99 deletions(-) create mode 100644 src/renderer/src/components/resources/viewer.tsx create mode 100644 src/renderer/src/utils/hash.ts diff --git a/package.json b/package.json index 8b70ee8..3fba736 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a87e09e..03fe6ae 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -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: {} diff --git a/src/main/config/index.ts b/src/main/config/index.ts index ac62fd5..c51114f 100644 --- a/src/main/config/index.ts +++ b/src/main/config/index.ts @@ -5,6 +5,8 @@ export { getCurrentProfileItem, getProfileItem, getProfileConfig, + getFileStr, + setFileStr, setProfileConfig, addProfileItem, removeProfileItem, diff --git a/src/main/config/profile.ts b/src/main/config/profile.ts index 2e3d881..735b4b7 100644 --- a/src/main/config/profile.ts +++ b/src/main/config/profile.ts @@ -140,10 +140,10 @@ export async function createProfile(item: Partial): Promise { + 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 { + if (isAbsolutePath(path)) { + await writeFile(path, content, 'utf-8') + } else { + await writeFile(mihomoProfileWorkDir(path), content, 'utf-8') + } +} \ No newline at end of file diff --git a/src/main/core/mihomoApi.ts b/src/main/core/mihomoApi.ts index 89d309d..7a7ca2d 100644 --- a/src/main/core/mihomoApi.ts +++ b/src/main/core/mihomoApi.ts @@ -114,6 +114,11 @@ export const mihomoUpdateProxyProviders = async (name: string): Promise => return await instance.put(`/providers/proxies/${encodeURIComponent(name)}`) } +export const mihomoRunProxyProviders = async (): Promise => { + const runtime = await getRuntimeConfig() + return runtime?.['proxy-providers'] +} + export const mihomoRuleProviders = async (): Promise => { const instance = await getAxios() return await instance.get('/providers/rules') @@ -124,6 +129,11 @@ export const mihomoUpdateRuleProviders = async (name: string): Promise => return await instance.put(`/providers/rules/${encodeURIComponent(name)}`) } +export const mihomoRunRuleProviders = async (): Promise => { + const runtime = await getRuntimeConfig() + return runtime?.['rule-providers'] +} + export const mihomoChangeProxy = async (group: string, proxy: string): Promise => { const instance = await getAxios() return await instance.put(`/proxies/${encodeURIComponent(group)}`, { name: proxy }) @@ -194,9 +204,9 @@ const mihomoTraffic = async (): Promise => { 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) diff --git a/src/main/utils/ipc.ts b/src/main/utils/ipc.ts index dd3b9a5..48ab902 100644 --- a/src/main/utils/ipc.ts +++ b/src/main/utils/ipc.ts @@ -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)) diff --git a/src/renderer/src/components/base/base-editor.tsx b/src/renderer/src/components/base/base-editor.tsx index 79c7160..e328328 100644 --- a/src/renderer/src/components/base/base-editor.tsx +++ b/src/renderer/src/components/base/base-editor.tsx @@ -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) => { 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: { diff --git a/src/renderer/src/components/resources/proxy-provider.tsx b/src/renderer/src/components/resources/proxy-provider.tsx index 70ba307..da1d0a3 100644 --- a/src/renderer/src/components/resources/proxy-provider.tsx +++ b/src/renderer/src/components/resources/proxy-provider.tsx @@ -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 ( + {ShowProvider && { setShowProvider(false); setShowPath(''); setShowType('')}} path={ShowPath} type={ShowType} />} - {providers.map((provider, index) => { - return ( - + {providers.map((provider, index) => ( + + + {provider.proxies?.length || 0} + + } + divider={!provider.subscriptionInfo && index !== providers.length - 1} + > +
+
{dayjs(provider.updatedAt).fromNow()}
+ + + +
+
+ {provider.subscriptionInfo && ( - {provider.proxies?.length || 0} - - } - divider={!provider.subscriptionInfo && index !== providers.length - 1} - > - { -
-
{dayjs(provider.updatedAt).fromNow()}
- + divider={index !== providers.length - 1} + title={ +
+ {`${calcTraffic( + provider.subscriptionInfo.Upload + provider.subscriptionInfo.Download + )} / ${calcTraffic(provider.subscriptionInfo.Total)}`}
} + > +
+ {provider.subscriptionInfo.Expire + ? dayjs.unix(provider.subscriptionInfo.Expire).format('YYYY-MM-DD') + : '长期有效'} +
- {provider.subscriptionInfo && ( - {`${calcTraffic( - provider.subscriptionInfo.Upload + provider.subscriptionInfo.Download - )} - /${calcTraffic(provider.subscriptionInfo.Total)}`}
- } - > - {provider.subscriptionInfo && ( -
- {provider.subscriptionInfo.Expire - ? dayjs.unix(provider.subscriptionInfo.Expire).format('YYYY-MM-DD') - : '长期有效'} -
- )} -
- )} -
- ) - })} + )} +
+ ))}
) } diff --git a/src/renderer/src/components/resources/rule-provider.tsx b/src/renderer/src/components/resources/rule-provider.tsx index b298db5..05d5e32 100644 --- a/src/renderer/src/components/resources/rule-provider.tsx +++ b/src/renderer/src/components/resources/rule-provider.tsx @@ -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 => { setUpdating((prev) => { prev[index] = true @@ -40,6 +68,12 @@ const RuleProvider: React.FC = () => { return ( + {ShowProvider && { setShowProvider(false); setShowPath(''); setShowType('') }} + />} - {providers.map((provider, index) => { - return ( - - - {provider.ruleCount} - - } - > - { -
-
{dayjs(provider.updatedAt).fromNow()}
- -
- } -
- {provider.format}} - divider={index !== providers.length - 1} - > -
- {provider.vehicleType}::{provider.behavior} -
-
-
- ) - })} + {providers.map((provider, index) => ( + + + {provider.ruleCount} + + } + > +
+
{dayjs(provider.updatedAt).fromNow()}
+ + {provider.format !== "MrsRule" && ( + + )} +
+
+ {provider.format}} + divider={index !== providers.length - 1} + > +
+ {provider.vehicleType}::{provider.behavior} +
+
+
+ ))}
) } diff --git a/src/renderer/src/components/resources/viewer.tsx b/src/renderer/src/components/resources/viewer.tsx new file mode 100644 index 0000000..dff7e0a --- /dev/null +++ b/src/renderer/src/components/resources/viewer.tsx @@ -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) => { + const { type, path, format, onClose } = props + const [currData, setCurrData] = useState('') + const language: Language = (!format || format === 'YamlRule') ? 'yaml' : 'text' + + const getContent = async (): Promise => { + setCurrData(await getFileStr(path)) + } + + useEffect(() => { + getContent() + }, []) + + return ( + + + Provider 内容 + + setCurrData(value)}/> + + + + {type == 'File' && + } + + + + ) +} + +export default Viewer diff --git a/src/renderer/src/utils/hash.ts b/src/renderer/src/utils/hash.ts new file mode 100644 index 0000000..da36fb9 --- /dev/null +++ b/src/renderer/src/utils/hash.ts @@ -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(); +} diff --git a/src/renderer/src/utils/ipc.ts b/src/renderer/src/utils/ipc.ts index 1097967..89d477b 100644 --- a/src/renderer/src/utils/ipc.ts +++ b/src/renderer/src/utils/ipc.ts @@ -43,6 +43,10 @@ export async function mihomoUpdateProxyProviders(name: string): Promise { ) } +export async function mihomoRunProxyProviders(): Promise { + return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('mihomoRunProxyProviders')) +} + export async function mihomoRuleProviders(): Promise { return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('mihomoRuleProviders')) } @@ -53,6 +57,10 @@ export async function mihomoUpdateRuleProviders(name: string): Promise { ) } +export async function mihomoRunRuleProviders(): Promise { + return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('mihomoRunRuleProviders')) +} + export async function mihomoChangeProxy(group: string, proxy: string): Promise { return ipcErrorWrapper( await window.electron.ipcRenderer.invoke('mihomoChangeProxy', group, proxy) @@ -151,6 +159,14 @@ export async function getProfileStr(id: string): Promise { return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('getProfileStr', id)) } +export async function getFileStr(id: string): Promise { + return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('getFileStr', id)) +} + +export async function setFileStr(id: string, str: string): Promise { + return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('setFileStr', id, str)) +} + export async function setProfileStr(id: string, str: string): Promise { return ipcErrorWrapper(await window.electron.ipcRenderer.invoke('setProfileStr', id, str)) } diff --git a/src/shared/types.d.ts b/src/shared/types.d.ts index 4644c00..ee70e76 100644 --- a/src/shared/types.d.ts +++ b/src/shared/types.d.ts @@ -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 {