mirror of
https://gh.catmak.name/https://github.com/mihomo-party-org/mihomo-party
synced 2025-12-27 05:00:30 +08:00
support profile override script
This commit is contained in:
parent
d434352bc3
commit
7785c2237e
@ -14,3 +14,14 @@ export {
|
||||
changeCurrentProfile,
|
||||
updateProfileItem
|
||||
} from './profile'
|
||||
export {
|
||||
getOverrideConfig,
|
||||
setOverrideConfig,
|
||||
getOverrideItem,
|
||||
addOverrideItem,
|
||||
removeOverrideItem,
|
||||
createOverride,
|
||||
getOverride,
|
||||
setOverride,
|
||||
updateOverrideItem
|
||||
} from './override'
|
||||
|
||||
108
src/main/config/override.ts
Normal file
108
src/main/config/override.ts
Normal file
@ -0,0 +1,108 @@
|
||||
import { overrideConfigPath, overridePath } from '../utils/dirs'
|
||||
import yaml from 'yaml'
|
||||
import fs from 'fs'
|
||||
import { dialog } from 'electron'
|
||||
import axios from 'axios'
|
||||
import { getControledMihomoConfig } from './controledMihomo'
|
||||
|
||||
let overrideConfig: IOverrideConfig // override.yaml
|
||||
|
||||
export function getOverrideConfig(force = false): IOverrideConfig {
|
||||
if (force || !overrideConfig) {
|
||||
overrideConfig = yaml.parse(fs.readFileSync(overrideConfigPath(), 'utf-8'))
|
||||
}
|
||||
return overrideConfig
|
||||
}
|
||||
|
||||
export function setOverrideConfig(config: IOverrideConfig): void {
|
||||
overrideConfig = config
|
||||
fs.writeFileSync(overrideConfigPath(), yaml.stringify(overrideConfig))
|
||||
}
|
||||
|
||||
export function getOverrideItem(id: string): IOverrideItem | undefined {
|
||||
return overrideConfig.items.find((item) => item.id === id)
|
||||
}
|
||||
export function updateOverrideItem(item: IOverrideItem): void {
|
||||
const index = overrideConfig.items.findIndex((i) => i.id === item.id)
|
||||
overrideConfig.items[index] = item
|
||||
fs.writeFileSync(overrideConfigPath(), yaml.stringify(overrideConfig))
|
||||
}
|
||||
|
||||
export async function addOverrideItem(item: Partial<IOverrideItem>): Promise<void> {
|
||||
const newItem = await createOverride(item)
|
||||
if (overrideConfig.items.find((i) => i.id === newItem.id)) {
|
||||
updateOverrideItem(newItem)
|
||||
} else {
|
||||
overrideConfig.items.push(newItem)
|
||||
}
|
||||
fs.writeFileSync(overrideConfigPath(), yaml.stringify(overrideConfig))
|
||||
}
|
||||
|
||||
export function removeOverrideItem(id: string): void {
|
||||
overrideConfig.items = overrideConfig.items?.filter((item) => item.id !== id)
|
||||
fs.writeFileSync(overrideConfigPath(), yaml.stringify(overrideConfig))
|
||||
fs.rmSync(overridePath(id))
|
||||
}
|
||||
|
||||
export async function createOverride(item: Partial<IOverrideItem>): Promise<IOverrideItem> {
|
||||
const id = item.id || new Date().getTime().toString(16)
|
||||
const newItem = {
|
||||
id,
|
||||
name: item.name || (item.type === 'remote' ? 'Remote File' : 'Local File'),
|
||||
type: item.type,
|
||||
url: item.url,
|
||||
updated: new Date().getTime()
|
||||
} as IOverrideItem
|
||||
switch (newItem.type) {
|
||||
case 'remote': {
|
||||
if (!item.url) {
|
||||
dialog.showErrorBox(
|
||||
'URL is required for remote script',
|
||||
'URL is required for remote script'
|
||||
)
|
||||
throw new Error('URL is required for remote script')
|
||||
}
|
||||
try {
|
||||
const res = await axios.get(item.url, {
|
||||
proxy: {
|
||||
protocol: 'http',
|
||||
host: '127.0.0.1',
|
||||
port: getControledMihomoConfig()['mixed-port'] || 7890
|
||||
},
|
||||
responseType: 'text'
|
||||
})
|
||||
const data = res.data
|
||||
setOverride(id, data)
|
||||
} catch (e) {
|
||||
dialog.showErrorBox('Failed to fetch remote script', `${e}\nurl: ${item.url}`)
|
||||
throw new Error(`Failed to fetch remote script ${e}`)
|
||||
}
|
||||
break
|
||||
}
|
||||
case 'local': {
|
||||
if (!item.file) {
|
||||
dialog.showErrorBox(
|
||||
'File is required for local script',
|
||||
'File is required for local script'
|
||||
)
|
||||
throw new Error('File is required for local script')
|
||||
}
|
||||
const data = item.file
|
||||
setOverride(id, data)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
return newItem
|
||||
}
|
||||
|
||||
export function getOverride(id: string): string {
|
||||
if (!fs.existsSync(overridePath(id))) {
|
||||
return `function main(config){ return config }`
|
||||
}
|
||||
return fs.readFileSync(overridePath(id), 'utf-8')
|
||||
}
|
||||
|
||||
export function setOverride(id: string, content: string): void {
|
||||
fs.writeFileSync(overridePath(id), content, 'utf-8')
|
||||
}
|
||||
@ -1,11 +1,17 @@
|
||||
import { getControledMihomoConfig, getProfileConfig, getProfile } from '../config'
|
||||
import {
|
||||
getControledMihomoConfig,
|
||||
getProfileConfig,
|
||||
getProfile,
|
||||
getProfileItem,
|
||||
getOverride
|
||||
} from '../config'
|
||||
import { mihomoWorkConfigPath } from '../utils/dirs'
|
||||
import yaml from 'yaml'
|
||||
import fs from 'fs'
|
||||
|
||||
export function generateProfile(): void {
|
||||
const current = getProfileConfig().current
|
||||
const currentProfile = getProfile(current)
|
||||
const currentProfile = overrideProfile(current, getProfile(current))
|
||||
const controledMihomoConfig = getControledMihomoConfig()
|
||||
const { tun: profileTun = {} } = currentProfile
|
||||
const { tun: controledTun } = controledMihomoConfig
|
||||
@ -22,3 +28,25 @@ export function generateProfile(): void {
|
||||
profile.sniffer = sniffer
|
||||
fs.writeFileSync(mihomoWorkConfigPath(), yaml.stringify(profile))
|
||||
}
|
||||
|
||||
function overrideProfile(current: string | undefined, profile: IMihomoConfig): IMihomoConfig {
|
||||
const overrideScriptList = getProfileItem(current).override || []
|
||||
for (const override of overrideScriptList) {
|
||||
const script = getOverride(override)
|
||||
profile = runOverrideScript(profile, script)
|
||||
}
|
||||
return profile
|
||||
}
|
||||
|
||||
function runOverrideScript(profile: IMihomoConfig, script: string): IMihomoConfig {
|
||||
try {
|
||||
const func = eval(`${script} main`)
|
||||
const newProfile = func(profile)
|
||||
if (typeof newProfile !== 'object') {
|
||||
throw new Error('Override script must return an object')
|
||||
}
|
||||
return newProfile
|
||||
} catch (e) {
|
||||
return profile
|
||||
}
|
||||
}
|
||||
|
||||
@ -5,6 +5,8 @@ import {
|
||||
logDir,
|
||||
mihomoTestDir,
|
||||
mihomoWorkDir,
|
||||
overrideConfigPath,
|
||||
overrideDir,
|
||||
profileConfigPath,
|
||||
profilePath,
|
||||
profilesDir,
|
||||
@ -13,6 +15,7 @@ import {
|
||||
import {
|
||||
defaultConfig,
|
||||
defaultControledMihomoConfig,
|
||||
defaultOverrideConfig,
|
||||
defaultProfile,
|
||||
defaultProfileConfig
|
||||
} from '../utils/template'
|
||||
@ -31,6 +34,9 @@ function initDirs(): void {
|
||||
if (!fs.existsSync(profilesDir())) {
|
||||
fs.mkdirSync(profilesDir())
|
||||
}
|
||||
if (!fs.existsSync(overrideDir())) {
|
||||
fs.mkdirSync(overrideDir())
|
||||
}
|
||||
if (!fs.existsSync(mihomoWorkDir())) {
|
||||
fs.mkdirSync(mihomoWorkDir())
|
||||
}
|
||||
@ -49,6 +55,9 @@ function initConfig(): void {
|
||||
if (!fs.existsSync(profileConfigPath())) {
|
||||
fs.writeFileSync(profileConfigPath(), yaml.stringify(defaultProfileConfig))
|
||||
}
|
||||
if (!fs.existsSync(overrideConfigPath())) {
|
||||
fs.writeFileSync(overrideConfigPath(), yaml.stringify(defaultOverrideConfig))
|
||||
}
|
||||
if (!fs.existsSync(profilePath('default'))) {
|
||||
fs.writeFileSync(profilePath('default'), yaml.stringify(defaultProfile))
|
||||
}
|
||||
|
||||
@ -53,6 +53,18 @@ export function profilePath(id: string): string {
|
||||
return path.join(profilesDir(), `${id}.yaml`)
|
||||
}
|
||||
|
||||
export function overrideDir(): string {
|
||||
return path.join(dataDir, 'override')
|
||||
}
|
||||
|
||||
export function overrideConfigPath(): string {
|
||||
return path.join(dataDir, 'override.yaml')
|
||||
}
|
||||
|
||||
export function overridePath(id: string): string {
|
||||
return path.join(overrideDir(), `${id}.js`)
|
||||
}
|
||||
|
||||
export function mihomoWorkDir(): string {
|
||||
return path.join(dataDir, 'work')
|
||||
}
|
||||
|
||||
@ -34,7 +34,15 @@ import {
|
||||
getProfileStr,
|
||||
setProfileStr,
|
||||
updateProfileItem,
|
||||
setProfileConfig
|
||||
setProfileConfig,
|
||||
getOverrideConfig,
|
||||
setOverrideConfig,
|
||||
getOverrideItem,
|
||||
addOverrideItem,
|
||||
removeOverrideItem,
|
||||
getOverride,
|
||||
setOverride,
|
||||
updateOverrideItem
|
||||
} from '../config'
|
||||
import { isEncryptionAvailable, restartCore } from '../core/manager'
|
||||
import { triggerSysProxy } from '../resolve/sysproxy'
|
||||
@ -81,11 +89,19 @@ export function registerIpcMainHandlers(): void {
|
||||
ipcMain.handle('changeCurrentProfile', (_e, id) => changeCurrentProfile(id))
|
||||
ipcMain.handle('addProfileItem', (_e, item) => addProfileItem(item))
|
||||
ipcMain.handle('removeProfileItem', (_e, id) => removeProfileItem(id))
|
||||
ipcMain.handle('getOverrideConfig', (_e, force) => getOverrideConfig(force))
|
||||
ipcMain.handle('setOverrideConfig', (_e, config) => setOverrideConfig(config))
|
||||
ipcMain.handle('getOverrideItem', (_e, id) => getOverrideItem(id))
|
||||
ipcMain.handle('addOverrideItem', (_e, item) => addOverrideItem(item))
|
||||
ipcMain.handle('removeOverrideItem', (_e, id) => removeOverrideItem(id))
|
||||
ipcMain.handle('updateOverrideItem', (_e, item) => updateOverrideItem(item))
|
||||
ipcMain.handle('getOverride', (_e, id) => getOverride(id))
|
||||
ipcMain.handle('setOverride', (_e, id, str) => setOverride(id, str))
|
||||
ipcMain.handle('restartCore', restartCore)
|
||||
ipcMain.handle('triggerSysProxy', (_e, enable) => triggerSysProxy(enable))
|
||||
ipcMain.handle('isEncryptionAvailable', isEncryptionAvailable)
|
||||
ipcMain.handle('encryptString', (_e, str) => safeStorage.encryptString(str))
|
||||
ipcMain.handle('getFilePath', getFilePath)
|
||||
ipcMain.handle('getFilePath', (_e, ext) => getFilePath(ext))
|
||||
ipcMain.handle('readTextFile', (_e, filePath) => readTextFile(filePath))
|
||||
ipcMain.handle('getRuntimeConfigStr', getRuntimeConfigStr)
|
||||
ipcMain.handle('getRuntimeConfig', getRuntimeConfig)
|
||||
@ -97,10 +113,10 @@ export function registerIpcMainHandlers(): void {
|
||||
ipcMain.handle('quitApp', () => app.quit())
|
||||
}
|
||||
|
||||
function getFilePath(): string[] | undefined {
|
||||
function getFilePath(ext: string[]): string[] | undefined {
|
||||
return dialog.showOpenDialogSync({
|
||||
title: '选择订阅文件',
|
||||
filters: [{ name: 'Yaml Files', extensions: ['yml', 'yaml'] }],
|
||||
filters: [{ name: 'Yaml Files', extensions: ext }],
|
||||
properties: ['openFile']
|
||||
})
|
||||
}
|
||||
|
||||
@ -94,6 +94,10 @@ export const defaultProfileConfig: IProfileConfig = {
|
||||
items: []
|
||||
}
|
||||
|
||||
export const defaultOverrideConfig: IOverrideConfig = {
|
||||
items: []
|
||||
}
|
||||
|
||||
export const defaultProfile: Partial<IMihomoConfig> = {
|
||||
proxies: [],
|
||||
'proxy-groups': [],
|
||||
|
||||
58
src/renderer/src/components/override/edit-file-modal.tsx
Normal file
58
src/renderer/src/components/override/edit-file-modal.tsx
Normal file
@ -0,0 +1,58 @@
|
||||
import { Modal, ModalContent, ModalHeader, ModalBody, ModalFooter, Button } from '@nextui-org/react'
|
||||
import React, { useEffect, useState } from 'react'
|
||||
import { BaseEditor } from '../base/base-editor'
|
||||
import { getOverride, setOverride } from '@renderer/utils/ipc'
|
||||
interface Props {
|
||||
id: string
|
||||
onClose: () => void
|
||||
}
|
||||
const EditFileModal: React.FC<Props> = (props) => {
|
||||
const { id, onClose } = props
|
||||
const [currData, setCurrData] = useState('')
|
||||
|
||||
const getContent = async (): Promise<void> => {
|
||||
setCurrData(await getOverride(id))
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
getContent()
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<Modal
|
||||
backdrop="blur"
|
||||
size="5xl"
|
||||
hideCloseButton
|
||||
isOpen={true}
|
||||
onOpenChange={onClose}
|
||||
scrollBehavior="inside"
|
||||
>
|
||||
<ModalContent className="h-full w-[calc(100%-100px)]">
|
||||
<ModalHeader className="flex">编辑覆写脚本</ModalHeader>
|
||||
<ModalBody className="h-full">
|
||||
<BaseEditor
|
||||
language="javascript"
|
||||
value={currData}
|
||||
onChange={(value) => setCurrData(value)}
|
||||
/>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button variant="light" onPress={onClose}>
|
||||
取消
|
||||
</Button>
|
||||
<Button
|
||||
color="primary"
|
||||
onPress={async () => {
|
||||
await setOverride(id, currData)
|
||||
onClose()
|
||||
}}
|
||||
>
|
||||
确认
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
export default EditFileModal
|
||||
73
src/renderer/src/components/override/edit-info-modal.tsx
Normal file
73
src/renderer/src/components/override/edit-info-modal.tsx
Normal file
@ -0,0 +1,73 @@
|
||||
import {
|
||||
Modal,
|
||||
ModalContent,
|
||||
ModalHeader,
|
||||
ModalBody,
|
||||
ModalFooter,
|
||||
Button,
|
||||
Input
|
||||
} from '@nextui-org/react'
|
||||
import React, { useState } from 'react'
|
||||
import SettingItem from '../base/base-setting-item'
|
||||
interface Props {
|
||||
item: IOverrideItem
|
||||
updateOverrideItem: (item: IOverrideItem) => Promise<void>
|
||||
onClose: () => void
|
||||
}
|
||||
const EditInfoModal: React.FC<Props> = (props) => {
|
||||
const { item, updateOverrideItem, onClose } = props
|
||||
const [values, setValues] = useState(item)
|
||||
|
||||
const onSave = async (): Promise<void> => {
|
||||
await updateOverrideItem(values)
|
||||
onClose()
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
backdrop="blur"
|
||||
hideCloseButton
|
||||
isOpen={true}
|
||||
onOpenChange={onClose}
|
||||
scrollBehavior="inside"
|
||||
>
|
||||
<ModalContent>
|
||||
<ModalHeader className="flex">编辑信息</ModalHeader>
|
||||
<ModalBody>
|
||||
<SettingItem title="名称">
|
||||
<Input
|
||||
size="sm"
|
||||
className="w-[200px]"
|
||||
value={values.name}
|
||||
onValueChange={(v) => {
|
||||
setValues({ ...values, name: v })
|
||||
}}
|
||||
/>
|
||||
</SettingItem>
|
||||
{values.type === 'remote' && (
|
||||
<SettingItem title="地址">
|
||||
<Input
|
||||
size="sm"
|
||||
className="w-[200px]"
|
||||
value={values.url}
|
||||
onValueChange={(v) => {
|
||||
setValues({ ...values, url: v })
|
||||
}}
|
||||
/>
|
||||
</SettingItem>
|
||||
)}
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button variant="light" onPress={onClose}>
|
||||
取消
|
||||
</Button>
|
||||
<Button color="primary" onPress={onSave}>
|
||||
保存
|
||||
</Button>
|
||||
</ModalFooter>
|
||||
</ModalContent>
|
||||
</Modal>
|
||||
)
|
||||
}
|
||||
|
||||
export default EditInfoModal
|
||||
197
src/renderer/src/components/override/override-item.tsx
Normal file
197
src/renderer/src/components/override/override-item.tsx
Normal file
@ -0,0 +1,197 @@
|
||||
import {
|
||||
Button,
|
||||
Card,
|
||||
CardBody,
|
||||
Chip,
|
||||
Dropdown,
|
||||
DropdownItem,
|
||||
DropdownMenu,
|
||||
DropdownTrigger
|
||||
} from '@nextui-org/react'
|
||||
import { IoMdMore, IoMdRefresh } from 'react-icons/io'
|
||||
import dayjs from 'dayjs'
|
||||
import React, { Key, useEffect, useState } from 'react'
|
||||
import EditFileModal from './edit-file-modal'
|
||||
import EditInfoModal from './edit-info-modal'
|
||||
import { useSortable } from '@dnd-kit/sortable'
|
||||
import { CSS } from '@dnd-kit/utilities'
|
||||
|
||||
interface Props {
|
||||
info: IOverrideItem
|
||||
addOverrideItem: (item: Partial<IOverrideItem>) => Promise<void>
|
||||
updateOverrideItem: (item: IOverrideItem) => Promise<void>
|
||||
removeOverrideItem: (id: string) => Promise<void>
|
||||
mutateOverrideConfig: () => void
|
||||
}
|
||||
|
||||
interface MenuItem {
|
||||
key: string
|
||||
label: string
|
||||
showDivider: boolean
|
||||
color: 'default' | 'danger'
|
||||
className: string
|
||||
}
|
||||
|
||||
const menuItems: MenuItem[] = [
|
||||
{
|
||||
key: 'edit-info',
|
||||
label: '编辑信息',
|
||||
showDivider: false,
|
||||
color: 'default',
|
||||
className: ''
|
||||
} as MenuItem,
|
||||
{
|
||||
key: 'edit-file',
|
||||
label: '编辑文件',
|
||||
showDivider: true,
|
||||
color: 'default',
|
||||
className: ''
|
||||
} as MenuItem,
|
||||
{
|
||||
key: 'delete',
|
||||
label: '删除',
|
||||
showDivider: false,
|
||||
color: 'danger',
|
||||
className: 'text-danger'
|
||||
} as MenuItem
|
||||
]
|
||||
|
||||
const OverrideItem: React.FC<Props> = (props) => {
|
||||
const { info, addOverrideItem, removeOverrideItem, mutateOverrideConfig, updateOverrideItem } =
|
||||
props
|
||||
const [updating, setUpdating] = useState(false)
|
||||
const [openInfo, setOpenInfo] = useState(false)
|
||||
const [openFile, setOpenFile] = useState(false)
|
||||
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
|
||||
id: info.id
|
||||
})
|
||||
const [disableOpen, setDisableOpen] = useState(false)
|
||||
|
||||
const onMenuAction = (key: Key): void => {
|
||||
switch (key) {
|
||||
case 'edit-info': {
|
||||
setOpenInfo(true)
|
||||
break
|
||||
}
|
||||
case 'edit-file': {
|
||||
setOpenFile(true)
|
||||
break
|
||||
}
|
||||
case 'delete': {
|
||||
removeOverrideItem(info.id)
|
||||
mutateOverrideConfig()
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (isDragging) {
|
||||
setTimeout(() => {
|
||||
setDisableOpen(true)
|
||||
}, 200)
|
||||
} else {
|
||||
setTimeout(() => {
|
||||
setDisableOpen(false)
|
||||
}, 200)
|
||||
}
|
||||
}, [isDragging])
|
||||
|
||||
return (
|
||||
<div
|
||||
className="grid col-span-1"
|
||||
style={{
|
||||
position: 'relative',
|
||||
transform: CSS.Transform.toString(transform),
|
||||
transition,
|
||||
zIndex: isDragging ? 'calc(infinity)' : undefined
|
||||
}}
|
||||
>
|
||||
{openFile && <EditFileModal id={info.id} onClose={() => setOpenFile(false)} />}
|
||||
{openInfo && (
|
||||
<EditInfoModal
|
||||
item={info}
|
||||
onClose={() => setOpenInfo(false)}
|
||||
updateOverrideItem={updateOverrideItem}
|
||||
/>
|
||||
)}
|
||||
<Card
|
||||
fullWidth
|
||||
isPressable
|
||||
onPress={() => {
|
||||
if (disableOpen) return
|
||||
setOpenFile(true)
|
||||
}}
|
||||
>
|
||||
<CardBody>
|
||||
<div className="flex justify-between h-[32px]">
|
||||
<h3
|
||||
ref={setNodeRef}
|
||||
{...attributes}
|
||||
{...listeners}
|
||||
className={`text-ellipsis whitespace-nowrap overflow-hidden text-md font-bold leading-[32px] text-foreground`}
|
||||
>
|
||||
{info?.name}
|
||||
</h3>
|
||||
<div className="flex">
|
||||
{info.type === 'remote' && (
|
||||
<Button
|
||||
isIconOnly
|
||||
size="sm"
|
||||
variant="light"
|
||||
color="default"
|
||||
disabled={updating}
|
||||
onPress={() => {
|
||||
setUpdating(true)
|
||||
addOverrideItem(info).finally(() => {
|
||||
setUpdating(false)
|
||||
})
|
||||
}}
|
||||
>
|
||||
<IoMdRefresh
|
||||
color="default"
|
||||
className={`text-[24px] ${updating ? 'animate-spin' : ''}`}
|
||||
/>
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<Dropdown>
|
||||
<DropdownTrigger>
|
||||
<Button isIconOnly size="sm" variant="light" color="default">
|
||||
<IoMdMore color="default" className={`text-[24px]`} />
|
||||
</Button>
|
||||
</DropdownTrigger>
|
||||
<DropdownMenu onAction={onMenuAction}>
|
||||
{menuItems.map((item) => (
|
||||
<DropdownItem
|
||||
showDivider={item.showDivider}
|
||||
key={item.key}
|
||||
color={item.color}
|
||||
className={item.className}
|
||||
>
|
||||
{item.label}
|
||||
</DropdownItem>
|
||||
))}
|
||||
</DropdownMenu>
|
||||
</Dropdown>
|
||||
</div>
|
||||
</div>
|
||||
{info.type === 'remote' && (
|
||||
<div className={`mt-2 flex justify-end`}>
|
||||
<small>{dayjs(info.updated).fromNow()}</small>
|
||||
</div>
|
||||
)}
|
||||
{info.type === 'local' && (
|
||||
<div className={`mt-2 flex justify-between`}>
|
||||
<Chip size="sm" variant="bordered">
|
||||
本地
|
||||
</Chip>
|
||||
</div>
|
||||
)}
|
||||
</CardBody>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default OverrideItem
|
||||
@ -5,11 +5,14 @@ import {
|
||||
ModalBody,
|
||||
ModalFooter,
|
||||
Button,
|
||||
Input
|
||||
Input,
|
||||
Select,
|
||||
SelectItem
|
||||
} from '@nextui-org/react'
|
||||
import React, { useState } from 'react'
|
||||
import SettingItem from '../base/base-setting-item'
|
||||
import dayjs from 'dayjs'
|
||||
import { useOverrideConfig } from '@renderer/hooks/use-override-config'
|
||||
interface Props {
|
||||
item: IProfileItem
|
||||
updateProfileItem: (item: IProfileItem) => Promise<void>
|
||||
@ -17,6 +20,8 @@ interface Props {
|
||||
}
|
||||
const EditInfoModal: React.FC<Props> = (props) => {
|
||||
const { item, updateProfileItem, onClose } = props
|
||||
const { overrideConfig } = useOverrideConfig()
|
||||
const { items: overrideItems = [] } = overrideConfig || {}
|
||||
const [values, setValues] = useState(item)
|
||||
|
||||
const onSave = async (): Promise<void> => {
|
||||
@ -77,6 +82,21 @@ const EditInfoModal: React.FC<Props> = (props) => {
|
||||
/>
|
||||
</SettingItem>
|
||||
)}
|
||||
<SettingItem title="覆写脚本">
|
||||
<Select
|
||||
className="w-[200px]"
|
||||
size="sm"
|
||||
selectionMode="multiple"
|
||||
selectedKeys={new Set(values.override || [])}
|
||||
onSelectionChange={(v) => {
|
||||
setValues({ ...values, override: Array.from(v).map((i) => i.toString()) })
|
||||
}}
|
||||
>
|
||||
{overrideItems.map((i) => (
|
||||
<SelectItem key={i.id}>{i.name}</SelectItem>
|
||||
))}
|
||||
</Select>
|
||||
</SettingItem>
|
||||
</ModalBody>
|
||||
<ModalFooter>
|
||||
<Button variant="light" onPress={onClose}>
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
import { Button, Card, CardBody, CardFooter } from '@nextui-org/react'
|
||||
import BorderSwitch from '@renderer/components/base/border-swtich'
|
||||
import React, { useState } from 'react'
|
||||
import React from 'react'
|
||||
import { MdFormatOverline } from 'react-icons/md'
|
||||
import { useLocation, useNavigate } from 'react-router-dom'
|
||||
import { useSortable } from '@dnd-kit/sortable'
|
||||
@ -10,7 +9,6 @@ const OverrideCard: React.FC = () => {
|
||||
const navigate = useNavigate()
|
||||
const location = useLocation()
|
||||
const match = location.pathname.includes('/override')
|
||||
const [enable, setEnable] = useState(false)
|
||||
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
|
||||
id: 'override'
|
||||
})
|
||||
@ -43,11 +41,6 @@ const OverrideCard: React.FC = () => {
|
||||
className={`${match ? 'text-white' : 'text-foreground'} text-[24px]`}
|
||||
/>
|
||||
</Button>
|
||||
<BorderSwitch
|
||||
isShowBorder={match && enable}
|
||||
isSelected={enable}
|
||||
onValueChange={setEnable}
|
||||
/>
|
||||
</div>
|
||||
</CardBody>
|
||||
<CardFooter className="pt-1">
|
||||
|
||||
52
src/renderer/src/hooks/use-override-config.tsx
Normal file
52
src/renderer/src/hooks/use-override-config.tsx
Normal file
@ -0,0 +1,52 @@
|
||||
import useSWR from 'swr'
|
||||
import {
|
||||
getOverrideConfig,
|
||||
setOverrideConfig as set,
|
||||
addOverrideItem as add,
|
||||
removeOverrideItem as remove,
|
||||
updateOverrideItem as update
|
||||
} from '@renderer/utils/ipc'
|
||||
|
||||
interface RetuenType {
|
||||
overrideConfig: IOverrideConfig | undefined
|
||||
setOverrideConfig: (config: IOverrideConfig) => Promise<void>
|
||||
mutateOverrideConfig: () => void
|
||||
addOverrideItem: (item: Partial<IOverrideItem>) => Promise<void>
|
||||
updateOverrideItem: (item: IOverrideItem) => Promise<void>
|
||||
removeOverrideItem: (id: string) => Promise<void>
|
||||
}
|
||||
|
||||
export const useOverrideConfig = (): RetuenType => {
|
||||
const { data: overrideConfig, mutate: mutateOverrideConfig } = useSWR('getOverrideConfig', () =>
|
||||
getOverrideConfig()
|
||||
)
|
||||
|
||||
const setOverrideConfig = async (config: IOverrideConfig): Promise<void> => {
|
||||
await set(config)
|
||||
mutateOverrideConfig()
|
||||
}
|
||||
|
||||
const addOverrideItem = async (item: Partial<IOverrideItem>): Promise<void> => {
|
||||
await add(item)
|
||||
mutateOverrideConfig()
|
||||
}
|
||||
|
||||
const removeOverrideItem = async (id: string): Promise<void> => {
|
||||
await remove(id)
|
||||
mutateOverrideConfig()
|
||||
}
|
||||
|
||||
const updateOverrideItem = async (item: IOverrideItem): Promise<void> => {
|
||||
await update(item)
|
||||
mutateOverrideConfig()
|
||||
}
|
||||
|
||||
return {
|
||||
overrideConfig,
|
||||
setOverrideConfig,
|
||||
mutateOverrideConfig,
|
||||
addOverrideItem,
|
||||
removeOverrideItem,
|
||||
updateOverrideItem
|
||||
}
|
||||
}
|
||||
@ -1,7 +1,174 @@
|
||||
import { Button, Input } from '@nextui-org/react'
|
||||
import BasePage from '@renderer/components/base/base-page'
|
||||
import { getFilePath, readTextFile } from '@renderer/utils/ipc'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { MdContentPaste } from 'react-icons/md'
|
||||
import {
|
||||
DndContext,
|
||||
closestCenter,
|
||||
PointerSensor,
|
||||
useSensor,
|
||||
useSensors,
|
||||
DragEndEvent
|
||||
} from '@dnd-kit/core'
|
||||
import { SortableContext } from '@dnd-kit/sortable'
|
||||
import { useOverrideConfig } from '@renderer/hooks/use-override-config'
|
||||
import OverrideItem from '@renderer/components/override/override-item'
|
||||
|
||||
const Override: React.FC = () => {
|
||||
return <BasePage title="覆写"></BasePage>
|
||||
const {
|
||||
overrideConfig,
|
||||
setOverrideConfig,
|
||||
addOverrideItem,
|
||||
updateOverrideItem,
|
||||
removeOverrideItem,
|
||||
mutateOverrideConfig
|
||||
} = useOverrideConfig()
|
||||
const { items = [] } = overrideConfig || {}
|
||||
const [sortedItems, setSortedItems] = useState(items)
|
||||
const [importing, setImporting] = useState(false)
|
||||
const [fileOver, setFileOver] = useState(false)
|
||||
const [url, setUrl] = useState('')
|
||||
const sensors = useSensors(useSensor(PointerSensor))
|
||||
const handleImport = async (): Promise<void> => {
|
||||
setImporting(true)
|
||||
try {
|
||||
await addOverrideItem({ name: '', type: 'remote', url })
|
||||
} finally {
|
||||
setImporting(false)
|
||||
}
|
||||
}
|
||||
const pageRef = useRef<HTMLDivElement>(null)
|
||||
|
||||
const onDragEnd = async (event: DragEndEvent): Promise<void> => {
|
||||
const { active, over } = event
|
||||
if (over) {
|
||||
if (active.id !== over.id) {
|
||||
const newOrder = sortedItems.slice()
|
||||
const activeIndex = newOrder.findIndex((item) => item.id === active.id)
|
||||
const overIndex = newOrder.findIndex((item) => item.id === over.id)
|
||||
newOrder.splice(activeIndex, 1)
|
||||
newOrder.splice(overIndex, 0, items[activeIndex])
|
||||
setSortedItems(newOrder)
|
||||
await setOverrideConfig({ items: newOrder })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
pageRef.current?.addEventListener('dragover', (e) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
setFileOver(true)
|
||||
})
|
||||
pageRef.current?.addEventListener('dragleave', (e) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
setFileOver(false)
|
||||
})
|
||||
pageRef.current?.addEventListener('drop', async (event) => {
|
||||
event.preventDefault()
|
||||
event.stopPropagation()
|
||||
if (event.dataTransfer?.files) {
|
||||
const file = event.dataTransfer.files[0]
|
||||
if (file.name.endsWith('.js')) {
|
||||
const content = await readTextFile(file.path)
|
||||
try {
|
||||
await addOverrideItem({ name: file.name, type: 'local', file: content })
|
||||
} finally {
|
||||
setFileOver(false)
|
||||
}
|
||||
} else {
|
||||
alert('不支持的文件类型')
|
||||
}
|
||||
}
|
||||
setFileOver(false)
|
||||
})
|
||||
return (): void => {
|
||||
pageRef.current?.removeEventListener('dragover', () => {})
|
||||
pageRef.current?.removeEventListener('dragleave', () => {})
|
||||
pageRef.current?.removeEventListener('drop', () => {})
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
setSortedItems(items)
|
||||
}, [items])
|
||||
|
||||
return (
|
||||
<BasePage ref={pageRef} title="覆写脚本">
|
||||
<div className="sticky top-[48px] z-40 backdrop-blur bg-background/40 flex p-2">
|
||||
<Input
|
||||
variant="bordered"
|
||||
size="sm"
|
||||
value={url}
|
||||
onValueChange={setUrl}
|
||||
endContent={
|
||||
<Button
|
||||
size="sm"
|
||||
isIconOnly
|
||||
variant="light"
|
||||
onPress={() => {
|
||||
navigator.clipboard.readText().then((text) => {
|
||||
setUrl(text)
|
||||
})
|
||||
}}
|
||||
>
|
||||
<MdContentPaste className="text-lg" />
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
<Button
|
||||
size="sm"
|
||||
color="primary"
|
||||
className="ml-2"
|
||||
isDisabled={url === ''}
|
||||
isLoading={importing}
|
||||
onPress={handleImport}
|
||||
>
|
||||
导入
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
color="primary"
|
||||
className="ml-2"
|
||||
onPress={() => {
|
||||
getFilePath(['js']).then(async (files) => {
|
||||
if (files?.length) {
|
||||
const content = await readTextFile(files[0])
|
||||
const fileName = files[0].split('/').pop()?.split('\\').pop()
|
||||
await addOverrideItem({ name: fileName, type: 'local', file: content })
|
||||
}
|
||||
})
|
||||
}}
|
||||
>
|
||||
打开
|
||||
</Button>
|
||||
</div>
|
||||
<DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={onDragEnd}>
|
||||
<div
|
||||
className={`${fileOver ? 'blur-sm' : ''} grid sm:grid-cols-2 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-2 mx-2`}
|
||||
>
|
||||
<SortableContext
|
||||
items={sortedItems.map((item) => {
|
||||
return item.id
|
||||
})}
|
||||
>
|
||||
{sortedItems.map((item) => (
|
||||
<OverrideItem
|
||||
key={item.id}
|
||||
addOverrideItem={addOverrideItem}
|
||||
removeOverrideItem={removeOverrideItem}
|
||||
mutateOverrideConfig={mutateOverrideConfig}
|
||||
updateOverrideItem={updateOverrideItem}
|
||||
info={item}
|
||||
/>
|
||||
))}
|
||||
</SortableContext>
|
||||
</div>
|
||||
</DndContext>
|
||||
</BasePage>
|
||||
)
|
||||
}
|
||||
|
||||
export default Override
|
||||
|
||||
@ -92,6 +92,10 @@ const Profiles: React.FC = () => {
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
setSortedItems(items)
|
||||
}, [items])
|
||||
|
||||
return (
|
||||
<BasePage ref={pageRef} title="订阅管理">
|
||||
<div className="sticky top-[48px] z-40 backdrop-blur bg-background/40 flex p-2">
|
||||
@ -130,7 +134,7 @@ const Profiles: React.FC = () => {
|
||||
color="primary"
|
||||
className="ml-2"
|
||||
onPress={() => {
|
||||
getFilePath().then(async (files) => {
|
||||
getFilePath(['yml', 'yaml']).then(async (files) => {
|
||||
if (files?.length) {
|
||||
const content = await readTextFile(files[0])
|
||||
const fileName = files[0].split('/').pop()?.split('\\').pop()
|
||||
|
||||
@ -138,6 +138,38 @@ export async function setProfileStr(id: string, str: string): Promise<void> {
|
||||
return await window.electron.ipcRenderer.invoke('setProfileStr', id, str)
|
||||
}
|
||||
|
||||
export async function getOverrideConfig(force = false): Promise<IOverrideConfig> {
|
||||
return await window.electron.ipcRenderer.invoke('getOverrideConfig', force)
|
||||
}
|
||||
|
||||
export async function setOverrideConfig(config: IOverrideConfig): Promise<void> {
|
||||
return await window.electron.ipcRenderer.invoke('setOverrideConfig', config)
|
||||
}
|
||||
|
||||
export async function getOverrideItem(id: string): Promise<IOverrideItem | undefined> {
|
||||
return await window.electron.ipcRenderer.invoke('getOverrideItem', id)
|
||||
}
|
||||
|
||||
export async function addOverrideItem(item: Partial<IOverrideItem>): Promise<void> {
|
||||
return await window.electron.ipcRenderer.invoke('addOverrideItem', item)
|
||||
}
|
||||
|
||||
export async function removeOverrideItem(id: string): Promise<void> {
|
||||
return await window.electron.ipcRenderer.invoke('removeOverrideItem', id)
|
||||
}
|
||||
|
||||
export async function updateOverrideItem(item: IOverrideItem): Promise<void> {
|
||||
return await window.electron.ipcRenderer.invoke('updateOverrideItem', item)
|
||||
}
|
||||
|
||||
export async function getOverride(id: string): Promise<string> {
|
||||
return await window.electron.ipcRenderer.invoke('getOverride', id)
|
||||
}
|
||||
|
||||
export async function setOverride(id: string, str: string): Promise<void> {
|
||||
return await window.electron.ipcRenderer.invoke('setOverride', id, str)
|
||||
}
|
||||
|
||||
export async function restartCore(): Promise<void> {
|
||||
return await window.electron.ipcRenderer.invoke('restartCore')
|
||||
}
|
||||
@ -154,8 +186,8 @@ export async function encryptString(str: string): Promise<Buffer> {
|
||||
return await window.electron.ipcRenderer.invoke('encryptString', str)
|
||||
}
|
||||
|
||||
export async function getFilePath(): Promise<string[] | undefined> {
|
||||
return await window.electron.ipcRenderer.invoke('getFilePath')
|
||||
export async function getFilePath(ext: string[]): Promise<string[] | undefined> {
|
||||
return await window.electron.ipcRenderer.invoke('getFilePath', ext)
|
||||
}
|
||||
|
||||
export async function readTextFile(filePath: string): Promise<string> {
|
||||
|
||||
14
src/shared/types.d.ts
vendored
14
src/shared/types.d.ts
vendored
@ -311,6 +311,19 @@ interface IProfileConfig {
|
||||
items: IProfileItem[]
|
||||
}
|
||||
|
||||
interface IOverrideItem {
|
||||
id: string
|
||||
type: 'remote' | 'local'
|
||||
name: string
|
||||
updated: number
|
||||
url?: string
|
||||
file?: string
|
||||
}
|
||||
|
||||
interface IOverrideConfig {
|
||||
items: IOverrideItem[]
|
||||
}
|
||||
|
||||
interface ISubscriptionUserInfo {
|
||||
upload: number
|
||||
download: number
|
||||
@ -327,5 +340,6 @@ interface IProfileItem {
|
||||
interval?: number
|
||||
home?: string
|
||||
updated?: number
|
||||
override?: string[]
|
||||
extra?: ISubscriptionUserInfo
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user