mirror of
https://gh.catmak.name/https://github.com/mihomo-party-org/mihomo-party
synced 2025-12-28 05:30:29 +08:00
import profile
This commit is contained in:
parent
bfcd195b4d
commit
34ed8898a5
@ -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
|
||||||
|
}
|
||||||
|
|||||||
@ -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"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
3
src/shared/types.d.ts
vendored
3
src/shared/types.d.ts
vendored
@ -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
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user