update profile

This commit is contained in:
pompurin404 2024-08-05 15:28:39 +08:00
parent 279f8fe1d3
commit 179a87d4bf
No known key found for this signature in database
10 changed files with 343 additions and 61 deletions

View File

@ -7,5 +7,9 @@ export {
getProfileConfig, getProfileConfig,
addProfileItem, addProfileItem,
removeProfileItem, removeProfileItem,
createProfile createProfile,
getProfileStr,
setProfileStr,
changeCurrentProfile,
updateProfileItem
} from './profile' } from './profile'

View File

@ -38,9 +38,21 @@ export async function changeCurrentProfile(id: string): Promise<void> {
} }
} }
export async function updateProfileItem(item: IProfileItem): Promise<void> {
const index = profileConfig.items.findIndex((i) => i.id === item.id)
profileConfig.items[index] = item
fs.writeFileSync(profileConfigPath(), yaml.stringify(profileConfig))
window?.webContents.send('profileConfigUpdated')
}
export async function addProfileItem(item: Partial<IProfileItem>): Promise<void> { export async function addProfileItem(item: Partial<IProfileItem>): Promise<void> {
const newItem = await createProfile(item) const newItem = await createProfile(item)
if (profileConfig.items.find((i) => i.id === newItem.id)) {
updateProfileItem(newItem)
} else {
profileConfig.items.push(newItem) profileConfig.items.push(newItem)
}
if (!getProfileConfig().current) { if (!getProfileConfig().current) {
changeCurrentProfile(newItem.id) changeCurrentProfile(newItem.id)
} }
@ -134,7 +146,7 @@ export async function createProfile(item: Partial<IProfileItem>): Promise<IProfi
if (headers['subscription-userinfo']) { if (headers['subscription-userinfo']) {
newItem.extra = parseSubinfo(headers['subscription-userinfo']) newItem.extra = parseSubinfo(headers['subscription-userinfo'])
} }
fs.writeFileSync(profilePath(id), data, 'utf-8') setProfileStr(id, data)
} catch (e) { } catch (e) {
dialog.showErrorBox('Failed to fetch remote profile', `${e}\nurl: ${item.url}`) dialog.showErrorBox('Failed to fetch remote profile', `${e}\nurl: ${item.url}`)
throw new Error(`Failed to fetch remote profile ${e}`) throw new Error(`Failed to fetch remote profile ${e}`)
@ -150,7 +162,7 @@ export async function createProfile(item: Partial<IProfileItem>): Promise<IProfi
throw new Error('File is required for local profile') throw new Error('File is required for local profile')
} }
const data = item.file const data = item.file
fs.writeFileSync(profilePath(id), yaml.stringify(data)) setProfileStr(id, data)
break break
} }
} }
@ -158,13 +170,25 @@ export async function createProfile(item: Partial<IProfileItem>): Promise<IProfi
return newItem return newItem
} }
export function getProfileStr(id: string): string {
return fs.readFileSync(profilePath(id), 'utf-8')
}
export function setProfileStr(id: string, content: string): void {
fs.writeFileSync(profilePath(id), content, 'utf-8')
if (id === getProfileConfig().current) {
getCurrentProfile(true)
restartCore()
}
}
export function getCurrentProfile(force = false): Partial<IMihomoConfig> { export function getCurrentProfile(force = false): Partial<IMihomoConfig> {
if (force || !currentProfile) { if (force || !currentProfile) {
const current = getProfileConfig().current const current = getProfileConfig().current
if (current) { if (current) {
currentProfile = yaml.parse(fs.readFileSync(profilePath(current), 'utf-8')) currentProfile = yaml.parse(getProfileStr(current))
} else { } else {
currentProfile = yaml.parse(fs.readFileSync(profilePath('default'), 'utf-8')) currentProfile = yaml.parse(getProfileStr('default'))
} }
} }
return currentProfile return currentProfile

View File

@ -23,11 +23,14 @@ import {
getCurrentProfileItem, getCurrentProfileItem,
getProfileItem, getProfileItem,
addProfileItem, addProfileItem,
removeProfileItem removeProfileItem,
changeCurrentProfile,
getProfileStr,
setProfileStr,
updateProfileItem
} from '../config' } from '../config'
import { isEncryptionAvailable, restartCore } from '../core/manager' import { isEncryptionAvailable, restartCore } from '../core/manager'
import { triggerSysProxy } from '../resolve/sysproxy' import { triggerSysProxy } from '../resolve/sysproxy'
import { changeCurrentProfile } from '../config/profile'
export function registerIpcMainHandlers(): void { export function registerIpcMainHandlers(): void {
ipcMain.handle('mihomoVersion', mihomoVersion) ipcMain.handle('mihomoVersion', mihomoVersion)
@ -52,6 +55,9 @@ export function registerIpcMainHandlers(): void {
ipcMain.handle('getProfileConfig', (_e, force) => getProfileConfig(force)) ipcMain.handle('getProfileConfig', (_e, force) => getProfileConfig(force))
ipcMain.handle('getCurrentProfileItem', getCurrentProfileItem) ipcMain.handle('getCurrentProfileItem', getCurrentProfileItem)
ipcMain.handle('getProfileItem', (_e, id) => getProfileItem(id)) ipcMain.handle('getProfileItem', (_e, id) => getProfileItem(id))
ipcMain.handle('getProfileStr', (_e, id) => getProfileStr(id))
ipcMain.handle('setProfileStr', (_e, id, str) => setProfileStr(id, str))
ipcMain.handle('updateProfileItem', (_e, item) => updateProfileItem(item))
ipcMain.handle('changeCurrentProfile', (_e, id) => changeCurrentProfile(id)) ipcMain.handle('changeCurrentProfile', (_e, id) => changeCurrentProfile(id))
ipcMain.handle('addProfileItem', (_e, item) => addProfileItem(item)) ipcMain.handle('addProfileItem', (_e, item) => addProfileItem(item))
ipcMain.handle('removeProfileItem', (_e, id) => removeProfileItem(id)) ipcMain.handle('removeProfileItem', (_e, id) => removeProfileItem(id))

View File

@ -0,0 +1,77 @@
import { Modal, ModalContent, ModalHeader, ModalBody, ModalFooter, Button } from '@nextui-org/react'
import React, { useEffect, useState } from 'react'
import MonacoEditor, { monaco } from 'react-monaco-editor'
import { useTheme } from 'next-themes'
import { getProfileStr, setProfileStr } 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 { theme } = useTheme()
const editorDidMount = (editor: monaco.editor.IStandaloneCodeEditor): void => {
window.electron.ipcRenderer.on('resize', () => {
editor.layout()
})
}
const editorWillUnmount = (editor: monaco.editor.IStandaloneCodeEditor): void => {
window.electron.ipcRenderer.removeAllListeners('resize')
editor.dispose()
}
const getContent = async (): Promise<void> => {
setCurrData(await getProfileStr(id))
}
useEffect(() => {
getContent()
}, [])
return (
<Modal size="5xl" hideCloseButton isOpen={true} scrollBehavior="inside">
<ModalContent className="h-full w-[calc(100%-100px)]">
<ModalHeader className="flex"></ModalHeader>
<ModalBody className="h-full">
<MonacoEditor
height="100%"
language="yaml"
value={currData}
theme={theme === 'dark' ? 'vs-dark' : 'vs'}
options={{
minimap: {
enabled: false
},
mouseWheelZoom: true,
fontFamily: `Fira Code, JetBrains Mono, Roboto Mono, "Source Code Pro", Consolas, Menlo, Monaco, monospace, "Courier New", "Apple Color Emoji"`,
fontLigatures: true, // 连字符
smoothScrolling: true // 平滑滚动
}}
editorDidMount={editorDidMount}
editorWillUnmount={editorWillUnmount}
onChange={(value) => setCurrData(value)}
/>
</ModalBody>
<ModalFooter>
<Button variant="light" onPress={onClose}>
</Button>
<Button
color="primary"
onPress={async () => {
await setProfileStr(id, currData)
onClose()
}}
>
</Button>
</ModalFooter>
</ModalContent>
</Modal>
)
}
export default EditFileModal

View File

@ -0,0 +1,79 @@
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: IProfileItem
updateProfileItem: (item: IProfileItem) => Promise<void>
onClose: () => void
}
const EditInfoModal: React.FC<Props> = (props) => {
const { item, updateProfileItem, onClose } = props
const [values, setValues] = useState(item)
const onSave = async (): Promise<void> => {
await updateProfileItem(values)
onClose()
}
return (
<Modal hideCloseButton isOpen={true} 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.url && (
<SettingItem title="订阅地址">
<Input
size="sm"
className="w-[200px]"
value={values.url}
onValueChange={(v) => {
setValues({ ...values, url: v })
}}
/>
</SettingItem>
)}
<SettingItem title="更新间隔(分钟)">
<Input
size="sm"
type="number"
className="w-[200px]"
value={values.interval?.toString() ?? ''}
onValueChange={(v) => {
setValues({ ...values, interval: parseInt(v) })
}}
/>
</SettingItem>
</ModalBody>
<ModalFooter>
<Button variant="light" onPress={onClose}>
</Button>
<Button color="primary" onPress={onSave}>
</Button>
</ModalFooter>
</ModalContent>
</Modal>
)
}
export default EditInfoModal

View File

@ -12,11 +12,15 @@ import {
import { calcPercent, calcTraffic } from '@renderer/utils/calc' import { calcPercent, calcTraffic } from '@renderer/utils/calc'
import { IoMdMore, IoMdRefresh } from 'react-icons/io' import { IoMdMore, IoMdRefresh } from 'react-icons/io'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import React, { Key, useMemo } from 'react' import React, { Key, useMemo, useState } from 'react'
import EditFileModal from './edit-file-modal'
import EditInfoModal from './edit-info-modal'
interface Props { interface Props {
info: IProfileItem info: IProfileItem
isCurrent: boolean isCurrent: boolean
addProfileItem: (item: Partial<IProfileItem>) => Promise<void>
updateProfileItem: (item: IProfileItem) => Promise<void>
removeProfileItem: (id: string) => Promise<void> removeProfileItem: (id: string) => Promise<void>
mutateProfileConfig: () => void mutateProfileConfig: () => void
onClick: () => Promise<void> onClick: () => Promise<void>
@ -30,15 +34,33 @@ interface MenuItem {
className: string className: string
} }
const ProfileItem: React.FC<Props> = (props) => { const ProfileItem: React.FC<Props> = (props) => {
const { info, removeProfileItem, mutateProfileConfig, onClick, isCurrent } = props const {
info,
addProfileItem,
removeProfileItem,
mutateProfileConfig,
updateProfileItem,
onClick,
isCurrent
} = props
const extra = info?.extra const extra = info?.extra
const usage = (extra?.upload ?? 0) + (extra?.download ?? 0) const usage = (extra?.upload ?? 0) + (extra?.download ?? 0)
const total = extra?.total ?? 0 const total = extra?.total ?? 0
const [updating, setUpdating] = useState(false)
const [openInfo, setOpenInfo] = useState(false)
const [openFile, setOpenFile] = useState(false)
const menuItems: MenuItem[] = useMemo(() => { const menuItems: MenuItem[] = useMemo(() => {
const list = [ const list = [
{ {
key: 'edit', key: 'edit-info',
label: '编辑信息',
showDivider: false,
color: 'default',
className: ''
} as MenuItem,
{
key: 'edit-file',
label: '编辑文件', label: '编辑文件',
showDivider: true, showDivider: true,
color: 'default', color: 'default',
@ -66,8 +88,14 @@ const ProfileItem: React.FC<Props> = (props) => {
const onMenuAction = (key: Key): void => { const onMenuAction = (key: Key): void => {
switch (key) { switch (key) {
case 'edit': case 'edit-info': {
setOpenInfo(true)
break break
}
case 'edit-file': {
setOpenFile(true)
break
}
case 'delete': { case 'delete': {
removeProfileItem(info.id) removeProfileItem(info.id)
mutateProfileConfig() mutateProfileConfig()
@ -82,6 +110,15 @@ const ProfileItem: React.FC<Props> = (props) => {
} }
return ( return (
<>
{openFile && <EditFileModal id={info.id} onClose={() => setOpenFile(false)} />}
{openInfo && (
<EditInfoModal
item={info}
onClose={() => setOpenInfo(false)}
updateProfileItem={updateProfileItem}
/>
)}
<Card fullWidth isPressable onPress={onClick} className={isCurrent ? 'bg-primary' : ''}> <Card fullWidth isPressable onPress={onClick} className={isCurrent ? 'bg-primary' : ''}>
<CardBody className="pb-1"> <CardBody className="pb-1">
<div className="flex justify-between h-[32px]"> <div className="flex justify-between h-[32px]">
@ -89,8 +126,23 @@ const ProfileItem: React.FC<Props> = (props) => {
{info?.name} {info?.name}
</h3> </h3>
<div className="flex"> <div className="flex">
<Button isIconOnly size="sm" variant="light" color="default"> <Button
<IoMdRefresh color="default" className="text-[24px]" /> isIconOnly
size="sm"
variant="light"
color="default"
disabled={updating}
onPress={() => {
setUpdating(true)
addProfileItem(info).finally(() => {
setUpdating(false)
})
}}
>
<IoMdRefresh
color="default"
className={`text-[24px] ${updating ? 'animate-spin' : ''}`}
/>
</Button> </Button>
<Dropdown> <Dropdown>
<DropdownTrigger> <DropdownTrigger>
@ -128,6 +180,7 @@ const ProfileItem: React.FC<Props> = (props) => {
)} )}
</CardFooter> </CardFooter>
</Card> </Card>
</>
) )
} }

View File

@ -6,6 +6,7 @@ import { IoMdRefresh } from 'react-icons/io'
import relativeTime from 'dayjs/plugin/relativeTime' import relativeTime from 'dayjs/plugin/relativeTime'
import 'dayjs/locale/zh-cn' import 'dayjs/locale/zh-cn'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import { useState } from 'react'
dayjs.extend(relativeTime) dayjs.extend(relativeTime)
dayjs.locale('zh-cn') dayjs.locale('zh-cn')
@ -14,8 +15,8 @@ const ProfileCard: React.FC = () => {
const navigate = useNavigate() const navigate = useNavigate()
const location = useLocation() const location = useLocation()
const match = location.pathname.includes('/profiles') const match = location.pathname.includes('/profiles')
const [updating, setUpdating] = useState(false)
const { profileConfig } = useProfileConfig() const { profileConfig, addProfileItem } = useProfileConfig()
const { current, items } = profileConfig ?? {} const { current, items } = profileConfig ?? {}
const info = items?.find((item) => item.id === current) ?? { const info = items?.find((item) => item.id === current) ?? {
id: 'default', id: 'default',
@ -39,8 +40,23 @@ const ProfileCard: React.FC = () => {
<h3 className="select-none text-ellipsis whitespace-nowrap overflow-hidden text-md font-bold leading-[32px]"> <h3 className="select-none text-ellipsis whitespace-nowrap overflow-hidden text-md font-bold leading-[32px]">
{info?.name} {info?.name}
</h3> </h3>
<Button isIconOnly size="sm" variant="light" color="default"> <Button
<IoMdRefresh color="default" className="text-[24px]" /> isIconOnly
size="sm"
disabled={updating}
variant="light"
color="default"
onPress={() => {
setUpdating(true)
addProfileItem(info).finally(() => {
setUpdating(false)
})
}}
>
<IoMdRefresh
color="default"
className={`text-[24px] ${updating ? 'animate-spin' : ''}`}
/>
</Button> </Button>
</div> </div>
<div className="mt-2 flex justify-between"> <div className="mt-2 flex justify-between">

View File

@ -3,6 +3,7 @@ import {
getProfileConfig, getProfileConfig,
addProfileItem as add, addProfileItem as add,
removeProfileItem as remove, removeProfileItem as remove,
updateProfileItem as update,
changeCurrentProfile as change changeCurrentProfile as change
} from '@renderer/utils/ipc' } from '@renderer/utils/ipc'
import { useEffect } from 'react' import { useEffect } from 'react'
@ -11,6 +12,7 @@ interface RetuenType {
profileConfig: IProfileConfig | undefined profileConfig: IProfileConfig | undefined
mutateProfileConfig: () => void mutateProfileConfig: () => void
addProfileItem: (item: Partial<IProfileItem>) => Promise<void> addProfileItem: (item: Partial<IProfileItem>) => Promise<void>
updateProfileItem: (item: IProfileItem) => Promise<void>
removeProfileItem: (id: string) => Promise<void> removeProfileItem: (id: string) => Promise<void>
changeCurrentProfile: (id: string) => Promise<void> changeCurrentProfile: (id: string) => Promise<void>
} }
@ -30,6 +32,11 @@ export const useProfileConfig = (): RetuenType => {
mutateProfileConfig() mutateProfileConfig()
} }
const updateProfileItem = async (item: IProfileItem): Promise<void> => {
await update(item)
mutateProfileConfig()
}
const changeCurrentProfile = async (id: string): Promise<void> => { const changeCurrentProfile = async (id: string): Promise<void> => {
await change(id) await change(id)
mutateProfileConfig() mutateProfileConfig()
@ -49,6 +56,7 @@ export const useProfileConfig = (): RetuenType => {
mutateProfileConfig, mutateProfileConfig,
addProfileItem, addProfileItem,
removeProfileItem, removeProfileItem,
updateProfileItem,
changeCurrentProfile changeCurrentProfile
} }
} }

View File

@ -9,6 +9,7 @@ const Profiles: React.FC = () => {
const { const {
profileConfig, profileConfig,
addProfileItem, addProfileItem,
updateProfileItem,
removeProfileItem, removeProfileItem,
changeCurrentProfile, changeCurrentProfile,
mutateProfileConfig mutateProfileConfig
@ -61,8 +62,10 @@ const Profiles: React.FC = () => {
<ProfileItem <ProfileItem
key={item.id} key={item.id}
isCurrent={item.id === current} isCurrent={item.id === current}
addProfileItem={addProfileItem}
removeProfileItem={removeProfileItem} removeProfileItem={removeProfileItem}
mutateProfileConfig={mutateProfileConfig} mutateProfileConfig={mutateProfileConfig}
updateProfileItem={updateProfileItem}
info={item} info={item}
onClick={async () => { onClick={async () => {
await changeCurrentProfile(item.id) await changeCurrentProfile(item.id)

View File

@ -99,6 +99,18 @@ export async function removeProfileItem(id: string): Promise<void> {
return await window.electron.ipcRenderer.invoke('removeProfileItem', id) return await window.electron.ipcRenderer.invoke('removeProfileItem', id)
} }
export async function updateProfileItem(item: IProfileItem): Promise<void> {
return await window.electron.ipcRenderer.invoke('updateProfileItem', item)
}
export async function getProfileStr(id: string): Promise<string> {
return await window.electron.ipcRenderer.invoke('getProfileStr', id)
}
export async function setProfileStr(id: string, str: string): Promise<void> {
return await window.electron.ipcRenderer.invoke('setProfileStr', id, str)
}
export async function restartCore(): Promise<void> { export async function restartCore(): Promise<void> {
return await window.electron.ipcRenderer.invoke('restartCore') return await window.electron.ipcRenderer.invoke('restartCore')
} }