import profile

This commit is contained in:
pompurin404 2024-08-01 21:55:35 +08:00
parent bfcd195b4d
commit 34ed8898a5
No known key found for this signature in database
6 changed files with 152 additions and 9 deletions

View File

@ -7,6 +7,8 @@ import {
defaultProfileConfig defaultProfileConfig
} from './template' } from './template'
import { appConfigPath, controledMihomoConfigPath, profileConfigPath, profilePath } from './dirs' import { appConfigPath, controledMihomoConfigPath, profileConfigPath, profilePath } from './dirs'
import axios from 'axios'
import { app } from 'electron'
export let appConfig: IAppConfig // config.yaml export let appConfig: IAppConfig // config.yaml
export let profileConfig: IProfileConfig // profile.yaml export let profileConfig: IProfileConfig // profile.yaml
@ -68,11 +70,14 @@ export function getProfileItem(id: string | undefined): IProfileItem {
return items?.find((item) => item.id === id) || { id: 'default', type: 'local', name: '空白订阅' } return items?.find((item) => item.id === id) || { id: 'default', type: 'local', name: '空白订阅' }
} }
export function addProfileItem(item: IProfileItem): void { export async function addProfileItem(item: Partial<IProfileItem>): Promise<void> {
profileConfig.items.push(item) const newItem = await createProfile(item)
profileConfig.items.push(newItem)
console.log(!profileConfig.current)
if (!profileConfig.current) { if (!profileConfig.current) {
profileConfig.current = item.id profileConfig.current = newItem.id
} }
console.log(profileConfig.current)
fs.writeFileSync(profileConfigPath(), yaml.stringify(profileConfig)) fs.writeFileSync(profileConfigPath(), yaml.stringify(profileConfig))
} }
@ -97,3 +102,64 @@ export function getCurrentProfile(force = false): Partial<IMihomoConfig> {
} }
return currentProfile return currentProfile
} }
export async function createProfile(item: Partial<IProfileItem>): Promise<IProfileItem> {
const id = item.id || new Date().getTime().toString(16)
const newItem = {
id,
name: item.name || 'Local File',
type: item.type || 'local',
url: item.url,
updated: new Date().getTime()
} as IProfileItem
switch (newItem.type) {
case 'remote': {
if (!item.url) {
throw new Error('URL is required for remote profile')
}
try {
const res = await axios.get(item.url, {
proxy: {
protocol: 'http',
host: '127.0.0.1',
port: controledMihomoConfig['mixed-port'] || 7890
},
headers: {
'User-Agent': `Mihomo.Party.${app.getVersion()}`
},
responseType: 'text'
})
const data = res.data
const headers = res.headers
if (headers['content-disposition']) {
newItem.name = headers['content-disposition'].split('filename=')[1]
}
if (headers['subscription-userinfo']) {
const extra = headers['subscription-userinfo']
.split(';')
.map((item: string) => item.split('=')[1].trim())
newItem.extra = {
upload: parseInt(extra[0]),
download: parseInt(extra[1]),
total: parseInt(extra[2]),
expire: parseInt(extra[3])
}
}
fs.writeFileSync(profilePath(id), data, 'utf-8')
} catch (e) {
throw new Error(`Failed to fetch remote profile ${e}`)
}
break
}
case 'local': {
if (!item.file) {
throw new Error('File is required for local profile')
}
const data = item.file
fs.writeFileSync(profilePath(id), yaml.stringify(data))
break
}
}
return newItem
}

View File

@ -1,16 +1,29 @@
import { Button, Card, CardBody, CardFooter, Progress } from '@nextui-org/react' import { Button, Card, CardBody, CardFooter, Progress } from '@nextui-org/react'
import { getCurrentProfileItem } from '@renderer/utils/ipc' import { getCurrentProfileItem } from '@renderer/utils/ipc'
import { useEffect } from 'react'
import { IoMdRefresh } from 'react-icons/io' import { IoMdRefresh } from 'react-icons/io'
import { useLocation, useNavigate } from 'react-router-dom' import { useLocation, useNavigate } from 'react-router-dom'
import { calcTraffic } from '@renderer/utils/calc'
import useSWR from 'swr' import useSWR from 'swr'
const ProfileCard: React.FC = () => { 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 { data: info } = useSWR('getCurrentProfileItem', getCurrentProfileItem) const { data: info, mutate } = useSWR('getCurrentProfileItem', getCurrentProfileItem)
const extra = info?.extra const extra = info?.extra
const usage = (extra?.upload ?? 0) + (extra?.download ?? 0)
const total = extra?.total ?? 0
useEffect(() => {
window.electron.ipcRenderer.on('profileConfigUpdated', () => {
mutate()
})
return (): void => {
window.electron.ipcRenderer.removeAllListeners('profileConfigUpdated')
}
})
return ( return (
<Card <Card
fullWidth fullWidth
@ -20,7 +33,9 @@ const ProfileCard: React.FC = () => {
> >
<CardBody> <CardBody>
<div className="flex justify-between h-[32px]"> <div className="flex justify-between h-[32px]">
<h3 className="select-none text-md font-bold leading-[32px]">{info?.name}</h3> <h3 className="select-none text-ellipsis whitespace-nowrap overflow-hidden text-md font-bold leading-[32px]">
{info?.name}
</h3>
<Button isIconOnly size="sm" variant="light" color="default"> <Button isIconOnly size="sm" variant="light" color="default">
<IoMdRefresh color="default" className="text-[24px]" /> <IoMdRefresh color="default" className="text-[24px]" />
</Button> </Button>
@ -29,6 +44,7 @@ const ProfileCard: React.FC = () => {
<CardFooter className="pt-1"> <CardFooter className="pt-1">
<Progress <Progress
classNames={{ indicator: 'bg-foreground' }} classNames={{ indicator: 'bg-foreground' }}
label={`${calcTraffic(usage)}/${calcTraffic(total)}`}
value={calcPercent(extra?.upload, extra?.download, extra?.total)} value={calcPercent(extra?.upload, extra?.download, extra?.total)}
className="max-w-md" className="max-w-md"
/> />

View File

@ -0,0 +1,48 @@
import useSWR from 'swr'
import {
getProfileConfig,
addProfileItem as add,
removeProfileItem as remove
} from '@renderer/utils/ipc'
import { useEffect } from 'react'
interface RetuenType {
profileConfig: IProfileConfig | undefined
mutateProfileConfig: () => void
addProfileItem: (item: Partial<IProfileItem>) => Promise<void>
removeProfileItem: (id: string) => void
}
export const useProfileConfig = (): RetuenType => {
const { data: profileConfig, mutate: mutateProfileConfig } = useSWR('getProfileConfig', () =>
getProfileConfig()
)
const addProfileItem = async (item: Partial<IProfileItem>): Promise<void> => {
await add(item)
mutateProfileConfig()
window.electron.ipcRenderer.send('profileConfigUpdated')
}
const removeProfileItem = async (id: string): Promise<void> => {
await remove(id)
mutateProfileConfig()
window.electron.ipcRenderer.send('profileConfigUpdated')
}
useEffect(() => {
window.electron.ipcRenderer.on('profileConfigUpdated', () => {
mutateProfileConfig()
})
return (): void => {
window.electron.ipcRenderer.removeAllListeners('profileConfigUpdated')
}
}, [])
return {
profileConfig,
mutateProfileConfig,
addProfileItem,
removeProfileItem
}
}

View File

@ -1,14 +1,25 @@
import { Button, Input } from '@nextui-org/react' import { Button, Input } from '@nextui-org/react'
import BasePage from '@renderer/components/base/base-page' import BasePage from '@renderer/components/base/base-page'
import { useProfileConfig } from '@renderer/hooks/use-profile'
import { useState } from 'react' import { useState } from 'react'
import { MdContentPaste } from 'react-icons/md' import { MdContentPaste } from 'react-icons/md'
const Profiles: React.FC = () => { const Profiles: React.FC = () => {
const { profileConfig, addProfileItem } = useProfileConfig()
const [importing, setImporting] = useState(false)
const [url, setUrl] = useState('') const [url, setUrl] = useState('')
const handleImport = async (): Promise<void> => { const handleImport = async (): Promise<void> => {
console.log('import', url) setImporting(true)
try {
await addProfileItem({ name: 'Remote File', type: 'remote', url })
} catch (e) {
console.error(e)
} finally {
setImporting(false)
} }
}
return ( return (
<BasePage title="订阅"> <BasePage title="订阅">
<div className="flex m-2"> <div className="flex m-2">
@ -33,10 +44,11 @@ const Profiles: React.FC = () => {
</Button> </Button>
} }
/> />
<Button size="sm" color="primary" onPress={handleImport}> <Button size="sm" color="primary" isLoading={importing} onPress={handleImport}>
</Button> </Button>
</div> </div>
{JSON.stringify(profileConfig)}
</BasePage> </BasePage>
) )
} }

View File

@ -58,7 +58,7 @@ export async function getProfileItem(id: string | undefined): Promise<IProfileIt
return await window.electron.ipcRenderer.invoke('getProfileItem', id) return await window.electron.ipcRenderer.invoke('getProfileItem', id)
} }
export async function addProfileItem(item: IProfileItem): Promise<void> { export async function addProfileItem(item: Partial<IProfileItem>): Promise<void> {
await window.electron.ipcRenderer.invoke('addProfileItem', item) await window.electron.ipcRenderer.invoke('addProfileItem', item)
} }

View File

@ -131,7 +131,8 @@ interface IProfileItem {
id: string id: string
type: 'remote' | 'local' type: 'remote' | 'local'
name: string name: string
url?: string url?: string // remote
file?: string // local
updated?: number updated?: number
extra?: { extra?: {
upload: number upload: number