setup profile

This commit is contained in:
pompurin404 2024-08-01 17:30:49 +08:00
parent 4d9fb1f457
commit 4f335bddd5
No known key found for this signature in database
16 changed files with 210 additions and 41 deletions

View File

@ -10,9 +10,13 @@ import { checkAutoRun, disableAutoRun, enableAutoRun } from './autoRun'
import { import {
getAppConfig, getAppConfig,
setAppConfig, setAppConfig,
getProfileConfig,
getControledMihomoConfig, getControledMihomoConfig,
setControledMihomoConfig setControledMihomoConfig,
getProfileConfig,
getCurrentProfileItem,
getProfileItem,
addProfileItem,
removeProfileItem
} from './config' } from './config'
import { restartCore } from './manager' import { restartCore } from './manager'
@ -30,5 +34,9 @@ export function registerIpcMainHandlers(): void {
ipcMain.handle('getControledMihomoConfig', (_e, force) => getControledMihomoConfig(force)) ipcMain.handle('getControledMihomoConfig', (_e, force) => getControledMihomoConfig(force))
ipcMain.handle('setControledMihomoConfig', (_e, config) => setControledMihomoConfig(config)) ipcMain.handle('setControledMihomoConfig', (_e, config) => setControledMihomoConfig(config))
ipcMain.handle('getProfileConfig', (_e, force) => getProfileConfig(force)) ipcMain.handle('getProfileConfig', (_e, force) => getProfileConfig(force))
ipcMain.handle('getCurrentProfileItem', getCurrentProfileItem)
ipcMain.handle('getProfileItem', (_e, id) => getProfileItem(id))
ipcMain.handle('addProfileItem', (_e, item) => addProfileItem(item))
ipcMain.handle('removeProfileItem', (_e, id) => removeProfileItem(id))
ipcMain.handle('restartCore', () => restartCore()) ipcMain.handle('restartCore', () => restartCore())
} }

View File

@ -8,10 +8,10 @@ import {
} from './template' } from './template'
import { appConfigPath, controledMihomoConfigPath, profileConfigPath, profilePath } from './dirs' import { appConfigPath, controledMihomoConfigPath, profileConfigPath, profilePath } from './dirs'
export let appConfig: IAppConfig export let appConfig: IAppConfig // config.yaml
export let profileConfig: IProfileConfig export let profileConfig: IProfileConfig // profile.yaml
export let currentProfile: Partial<IMihomoConfig> export let currentProfile: Partial<IMihomoConfig> // profiles/xxx.yaml
export let controledMihomoConfig: Partial<IMihomoConfig> export let controledMihomoConfig: Partial<IMihomoConfig> // mihomo.yaml
export function initConfig(): void { export function initConfig(): void {
if (!fs.existsSync(appConfigPath())) { if (!fs.existsSync(appConfigPath())) {
@ -63,11 +63,30 @@ export function getProfileConfig(force = false): IProfileConfig {
return profileConfig return profileConfig
} }
export function setProfileConfig(patch: Partial<IProfileConfig>): void { export function getProfileItem(id: string | undefined): IProfileItem {
profileConfig = Object.assign(profileConfig, patch) const items = profileConfig.items
return items?.find((item) => item.id === id) || { id: 'default', type: 'local', name: '空白订阅' }
}
export function addProfileItem(item: IProfileItem): void {
profileConfig.items.push(item)
if (!profileConfig.current) {
profileConfig.current = item.id
}
fs.writeFileSync(profileConfigPath(), yaml.stringify(profileConfig)) fs.writeFileSync(profileConfigPath(), yaml.stringify(profileConfig))
} }
export function removeProfileItem(id: string): void {
profileConfig.items = profileConfig.items?.filter((item) => item.id !== id)
if (profileConfig.current === id) {
profileConfig.current = profileConfig.items[0]?.id
}
fs.writeFileSync(profileConfigPath(), yaml.stringify(profileConfig))
}
export function getCurrentProfileItem(): IProfileItem {
return getProfileItem(profileConfig.current)
}
export function getCurrentProfile(force = false): Partial<IMihomoConfig> { export function getCurrentProfile(force = false): Partial<IMihomoConfig> {
if (force || !currentProfile) { if (force || !currentProfile) {
if (profileConfig.current) { if (profileConfig.current) {

View File

@ -13,14 +13,7 @@ export const defaultControledMihomoConfig: Partial<IMihomoConfig> = {
} }
export const defaultProfileConfig: IProfileConfig = { export const defaultProfileConfig: IProfileConfig = {
current: 'default', items: []
profiles: [
{
id: 'default',
type: 'local',
name: '默认'
}
]
} }
export const defaultProfile: Partial<IMihomoConfig> = { export const defaultProfile: Partial<IMihomoConfig> = {

View File

@ -8,6 +8,7 @@ import { mihomoConnections } from '@renderer/utils/ipc'
const ConnCard: React.FC = () => { const ConnCard: React.FC = () => {
const navigate = useNavigate() const navigate = useNavigate()
const location = useLocation() const location = useLocation()
const match = location.pathname.includes('/connections')
const { data: connections } = useSWR<IMihomoConnectionsInfo>('/connections', mihomoConnections, { const { data: connections } = useSWR<IMihomoConnectionsInfo>('/connections', mihomoConnections, {
refreshInterval: 5000 refreshInterval: 5000
@ -24,7 +25,7 @@ const ConnCard: React.FC = () => {
return ( return (
<Card <Card
className={`w-[50%] mr-1 mb-2 ${location.pathname.includes('/connections') ? 'bg-primary' : ''}`} className={`w-[50%] mr-1 mb-2 ${match ? 'bg-primary' : ''}`}
isPressable isPressable
onPress={() => navigate('/connections')} onPress={() => navigate('/connections')}
> >
@ -38,7 +39,22 @@ const ConnCard: React.FC = () => {
> >
<IoLink color="default" className="text-[20px]" /> <IoLink color="default" className="text-[20px]" />
</Button> </Button>
<Chip size="sm" color="secondary" variant="bordered" className="mr-3 mt-2"> <Chip
classNames={
match
? {
base: 'border-foreground',
content: 'text-foreground'
}
: {
base: 'border-primary',
content: 'text-primary'
}
}
size="sm"
variant="bordered"
className="mr-3 mt-2"
>
{connections?.connections?.length ?? 0} {connections?.connections?.length ?? 0}
</Chip> </Chip>
</div> </div>

View File

@ -5,10 +5,10 @@ import { useLocation, useNavigate } from 'react-router-dom'
const LogCard: React.FC = () => { const LogCard: React.FC = () => {
const navigate = useNavigate() const navigate = useNavigate()
const location = useLocation() const location = useLocation()
const match = location.pathname.includes('/logs')
return ( return (
<Card <Card
className={`w-[50%] ml-1 mb-2 ${location.pathname.includes('/logs') ? 'bg-primary' : ''}`} className={`w-[50%] ml-1 mb-2 ${match ? 'bg-primary' : ''}`}
isPressable isPressable
onPress={() => navigate('/logs')} onPress={() => navigate('/logs')}
> >

View File

@ -25,13 +25,13 @@ const MihomoCoreCard: React.FC = () => {
const { core } = appConfig || {} const { core } = appConfig || {}
const navigate = useNavigate() const navigate = useNavigate()
const location = useLocation() const location = useLocation()
const match = location.pathname.includes('/mihomo')
return ( return (
<Card <Card
fullWidth fullWidth
isPressable isPressable
onPress={() => navigate('/mihomo')} onPress={() => navigate('/mihomo')}
className={`mb-2 ${location.pathname.includes('/mihomo') ? 'bg-primary' : ''}`} className={`mb-2 ${match ? 'bg-primary' : ''}`}
> >
<CardBody> <CardBody>
<div className="flex justify-between h-[32px]"> <div className="flex justify-between h-[32px]">

View File

@ -1,14 +1,17 @@
import { Button, Card, CardBody, CardFooter, Switch } from '@nextui-org/react' import { Button, Card, CardBody, CardFooter, Switch } from '@nextui-org/react'
import React, { useState } from 'react'
import { MdFormatOverline } from 'react-icons/md' import { MdFormatOverline } from 'react-icons/md'
import { useLocation, useNavigate } from 'react-router-dom' import { useLocation, useNavigate } from 'react-router-dom'
const OverrideCard: React.FC = () => { const OverrideCard: React.FC = () => {
const navigate = useNavigate() const navigate = useNavigate()
const location = useLocation() const location = useLocation()
const match = location.pathname.includes('/override')
const [enable, setEnable] = useState(false)
return ( return (
<Card <Card
className={`w-[50%] ml-1 mb-2 ${location.pathname.includes('/override') ? 'bg-primary' : ''}`} className={`w-[50%] ml-1 mb-2 ${match ? 'bg-primary' : ''}`}
isPressable isPressable
onPress={() => navigate('/override')} onPress={() => navigate('/override')}
> >
@ -22,7 +25,14 @@ const OverrideCard: React.FC = () => {
> >
<MdFormatOverline color="default" className="text-[24px]" /> <MdFormatOverline color="default" className="text-[24px]" />
</Button> </Button>
<Switch size="sm" /> <Switch
classNames={{
wrapper: `${match && enable ? 'border-2' : ''}`
}}
size="sm"
isSelected={enable}
onValueChange={setEnable}
/>
</div> </div>
</CardBody> </CardBody>
<CardFooter className="pt-1"> <CardFooter className="pt-1">

View File

@ -1,31 +1,51 @@
import { Button, Card, CardBody, CardFooter, Slider } from '@nextui-org/react' import { Button, Card, CardBody, CardFooter, Progress } from '@nextui-org/react'
import { getCurrentProfileItem } from '@renderer/utils/ipc'
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 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 { data: info } = useSWR('getCurrentProfileItem', getCurrentProfileItem)
const extra = info?.extra
return ( return (
<Card <Card
fullWidth fullWidth
className={`mb-2 ${location.pathname.includes('/profiles') ? 'bg-primary' : ''}`} className={`mb-2 ${match ? 'bg-primary' : ''}`}
isPressable isPressable
onPress={() => navigate('/profiles')} onPress={() => navigate('/profiles')}
> >
<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]"></h3> <h3 className="select-none 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>
</div> </div>
</CardBody> </CardBody>
<CardFooter className="pt-1"> <CardFooter className="pt-1">
<Slider className="pointer-events-none" color="foreground" value={20} hideThumb /> <Progress
classNames={{ indicator: 'bg-foreground' }}
value={calcPercent(extra?.upload, extra?.download, extra?.total)}
className="max-w-md"
/>
</CardFooter> </CardFooter>
</Card> </Card>
) )
} }
export default ProfileCard export default ProfileCard
function calcPercent(
upload: number | undefined,
download: number | undefined,
total: number | undefined
): number {
if (upload === undefined || download === undefined || total === undefined) {
return 100
}
return Math.round(((upload + download) / total) * 100)
}

View File

@ -5,11 +5,11 @@ import { useLocation, useNavigate } from 'react-router-dom'
const ProxyCard: React.FC = () => { const ProxyCard: React.FC = () => {
const navigate = useNavigate() const navigate = useNavigate()
const location = useLocation() const location = useLocation()
const match = location.pathname.includes('/proxies')
return ( return (
<Card <Card
fullWidth fullWidth
className={`mb-2 ${location.pathname.includes('/proxies') ? 'bg-primary' : ''}`} className={`mb-2 ${match ? 'bg-primary' : ''}`}
isPressable isPressable
onPress={() => navigate('/proxies')} onPress={() => navigate('/proxies')}
> >

View File

@ -7,14 +7,14 @@ import useSWR from 'swr'
const RuleCard: React.FC = () => { const RuleCard: React.FC = () => {
const navigate = useNavigate() const navigate = useNavigate()
const location = useLocation() const location = useLocation()
const match = location.pathname.includes('/rules')
const { data: rules } = useSWR<IMihomoRulesInfo>('/connections', mihomoRules, { const { data: rules } = useSWR<IMihomoRulesInfo>('/connections', mihomoRules, {
refreshInterval: 5000 refreshInterval: 5000
}) })
return ( return (
<Card <Card
className={`w-[50%] mr-1 mb-2 ${location.pathname.includes('/rules') ? 'bg-primary' : ''}`} className={`w-[50%] mr-1 mb-2 ${match ? 'bg-primary' : ''}`}
isPressable isPressable
onPress={() => navigate('/rules')} onPress={() => navigate('/rules')}
> >
@ -28,7 +28,22 @@ const RuleCard: React.FC = () => {
> >
<IoGitNetwork color="default" className="text-[20px]" /> <IoGitNetwork color="default" className="text-[20px]" />
</Button> </Button>
<Chip size="sm" color="secondary" variant="bordered" className="mr-3 mt-2"> <Chip
classNames={
match
? {
base: 'border-foreground',
content: 'text-foreground'
}
: {
base: 'border-primary',
content: 'text-primary'
}
}
size="sm"
variant="bordered"
className="mr-3 mt-2"
>
{rules?.rules?.length ?? 0} {rules?.rules?.length ?? 0}
</Chip> </Chip>
</div> </div>

View File

@ -1,14 +1,17 @@
import { Button, Card, CardBody, CardFooter, Switch } from '@nextui-org/react' import { Button, Card, CardBody, CardFooter, Switch } from '@nextui-org/react'
import React, { useState } from 'react'
import { AiOutlineGlobal } from 'react-icons/ai' import { AiOutlineGlobal } from 'react-icons/ai'
import { useLocation, useNavigate } from 'react-router-dom' import { useLocation, useNavigate } from 'react-router-dom'
const SysproxySwitcher: React.FC = () => { const SysproxySwitcher: React.FC = () => {
const navigate = useNavigate() const navigate = useNavigate()
const location = useLocation() const location = useLocation()
const match = location.pathname.includes('/sysproxy')
const [enable, setEnable] = useState(false)
return ( return (
<Card <Card
className={`w-[50%] mr-1 ${location.pathname.includes('/sysproxy') ? 'bg-primary' : ''}`} className={`w-[50%] mr-1 ${match ? 'bg-primary' : ''}`}
isPressable isPressable
onPress={() => navigate('/sysproxy')} onPress={() => navigate('/sysproxy')}
> >
@ -22,7 +25,12 @@ const SysproxySwitcher: React.FC = () => {
> >
<AiOutlineGlobal color="default" className="text-[24px]" /> <AiOutlineGlobal color="default" className="text-[24px]" />
</Button> </Button>
<Switch size="sm" /> <Switch
classNames={{ wrapper: `${match && enable ? 'border-2' : ''}` }}
size="sm"
isSelected={enable}
onValueChange={setEnable}
/>
</div> </div>
</CardBody> </CardBody>
<CardFooter className="pt-1"> <CardFooter className="pt-1">

View File

@ -1,23 +1,36 @@
import { Button, Card, CardBody, CardFooter, Switch } from '@nextui-org/react' import { Button, Card, CardBody, CardFooter, Switch } from '@nextui-org/react'
import React, { useState } from 'react'
import { TbDeviceIpadHorizontalBolt } from 'react-icons/tb' import { TbDeviceIpadHorizontalBolt } from 'react-icons/tb'
import { useLocation, useNavigate } from 'react-router-dom' import { useLocation, useNavigate } from 'react-router-dom'
const TunSwitcher: React.FC = () => { const TunSwitcher: React.FC = () => {
const navigate = useNavigate() const navigate = useNavigate()
const location = useLocation() const location = useLocation()
const match = location.pathname.includes('/tun')
const [enable, setEnable] = useState(false)
return ( return (
<Card <Card
className={`w-[50%] ml-1 ${location.pathname.includes('/tun') ? 'bg-primary' : ''}`} className={`w-[50%] ml-1 ${match ? 'bg-primary' : ''}`}
isPressable isPressable
onPress={() => navigate('/tun')} onPress={() => navigate('/tun')}
> >
<CardBody className="pb-1 pt-0 px-0"> <CardBody className="pb-1 pt-0 px-0">
<div className="flex justify-between"> <div className="flex justify-between">
<Button isIconOnly className="bg-transparent" variant="flat" color="default"> <Button
isIconOnly
className="bg-transparent pointer-events-none"
variant="flat"
color="default"
>
<TbDeviceIpadHorizontalBolt color="default" className="text-[24px] font-bold" /> <TbDeviceIpadHorizontalBolt color="default" className="text-[24px] font-bold" />
</Button> </Button>
<Switch size="sm" /> <Switch
classNames={{ wrapper: `${match && enable ? 'border-2' : ''}` }}
size="sm"
isSelected={enable}
onValueChange={setEnable}
/>
</div> </div>
</CardBody> </CardBody>
<CardFooter className="pt-1"> <CardFooter className="pt-1">

View File

View File

@ -1,5 +1,44 @@
import { Button, Input } from '@nextui-org/react'
import BasePage from '@renderer/components/base/base-page'
import { useState } from 'react'
import { MdContentPaste } from 'react-icons/md'
const Profiles: React.FC = () => { const Profiles: React.FC = () => {
return <div>Profiles</div> const [url, setUrl] = useState('')
const handleImport = async (): Promise<void> => {
console.log('import', url)
}
return (
<BasePage title="订阅">
<div className="flex m-2">
<Input
variant="bordered"
className="mr-2"
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" onPress={handleImport}>
</Button>
</div>
</BasePage>
)
} }
export default Profiles export default Profiles

View File

@ -46,6 +46,26 @@ export async function setControledMihomoConfig(patch: Partial<IMihomoConfig>): P
await window.electron.ipcRenderer.invoke('setControledMihomoConfig', patch) await window.electron.ipcRenderer.invoke('setControledMihomoConfig', patch)
} }
export async function getProfileConfig(force = false): Promise<IProfileConfig> {
return await window.electron.ipcRenderer.invoke('getProfileConfig', force)
}
export async function getCurrentProfileItem(): Promise<IProfileItem> {
return await window.electron.ipcRenderer.invoke('getCurrentProfileItem')
}
export async function getProfileItem(id: string | undefined): Promise<IProfileItem> {
return await window.electron.ipcRenderer.invoke('getProfileItem', id)
}
export async function addProfileItem(item: IProfileItem): Promise<void> {
await window.electron.ipcRenderer.invoke('addProfileItem', item)
}
export async function removeProfileItem(id: string): Promise<void> {
await window.electron.ipcRenderer.invoke('removeProfileItem', id)
}
export async function restartCore(): Promise<void> { export async function restartCore(): Promise<void> {
await window.electron.ipcRenderer.invoke('restartCore') await window.electron.ipcRenderer.invoke('restartCore')
} }

10
src/shared/types.d.ts vendored
View File

@ -85,11 +85,19 @@ interface IMihomoConfig {
interface IProfileConfig { interface IProfileConfig {
current?: string current?: string
profiles?: IProfileItem[] items: IProfileItem[]
} }
interface IProfileItem { interface IProfileItem {
id: string id: string
type: 'remote' | 'local' type: 'remote' | 'local'
name: string name: string
url?: string
updated?: number
extra?: {
upload: number
download: number
total: number
expire: number
}
} }