support profile override script

This commit is contained in:
pompurin404 2024-08-11 19:53:49 +08:00
parent d434352bc3
commit 7785c2237e
No known key found for this signature in database
17 changed files with 817 additions and 19 deletions

View File

@ -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
View 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')
}

View File

@ -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
}
}

View File

@ -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))
}

View File

@ -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')
}

View File

@ -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']
})
}

View File

@ -94,6 +94,10 @@ export const defaultProfileConfig: IProfileConfig = {
items: []
}
export const defaultOverrideConfig: IOverrideConfig = {
items: []
}
export const defaultProfile: Partial<IMihomoConfig> = {
proxies: [],
'proxy-groups': [],

View 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

View 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

View 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

View File

@ -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}>

View File

@ -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">

View 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
}
}

View File

@ -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

View File

@ -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()

View File

@ -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
View File

@ -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
}