support custom sidebar

This commit is contained in:
pompurin404 2024-09-06 17:44:26 +08:00
parent 9bd4841b6e
commit 79b3da1da6
No known key found for this signature in database
21 changed files with 433 additions and 205 deletions

View File

@ -2,6 +2,12 @@
- 1.2.x YAML覆写语法有所变动请更新后参考文档进行修改 - 1.2.x YAML覆写语法有所变动请更新后参考文档进行修改
### New Features
- 支持自定义侧边栏卡片大小
- 支持隐藏侧边栏卡片
### Bug Fixes ### Bug Fixes
- 修复Ubuntu下每次开启Tun都需要密码的问题 - 修复Ubuntu下每次开启Tun都需要密码的问题
- 修复Sub-Store无法读取剪切板的问题

View File

@ -126,14 +126,14 @@ async function migration(): Promise<void> {
'tun', 'tun',
'profile', 'profile',
'proxy', 'proxy',
'mihomo',
'connection',
'dns',
'sniff',
'log',
'rule', 'rule',
'resource', 'resource',
'override', 'override',
'connection',
'mihomo',
'dns',
'sniff',
'log',
'substore' 'substore'
], ],
useSubStore = true useSubStore = true

View File

@ -18,19 +18,18 @@ export const defaultConfig: IAppConfig = {
controlSniff: true, controlSniff: true,
nameserverPolicy: {}, nameserverPolicy: {},
siderOrder: [ siderOrder: [
'mode',
'sysproxy', 'sysproxy',
'tun', 'tun',
'profile', 'profile',
'proxy', 'proxy',
'mihomo',
'connection',
'dns',
'sniff',
'log',
'rule', 'rule',
'resource', 'resource',
'override', 'override',
'connection',
'mihomo',
'dns',
'sniff',
'log',
'substore' 'substore'
], ],
sysProxy: { enable: false, mode: 'manual' } sysProxy: { enable: false, mode: 'manual' }

View File

@ -37,23 +37,20 @@ const App: React.FC = () => {
const { appConfig, patchAppConfig } = useAppConfig() const { appConfig, patchAppConfig } = useAppConfig()
const { const {
appTheme = 'system', appTheme = 'system',
controlDns = true,
controlSniff = true,
useSubStore = true,
useWindowFrame = false, useWindowFrame = false,
siderOrder = [ siderOrder = [
'sysproxy', 'sysproxy',
'tun', 'tun',
'profile', 'profile',
'proxy', 'proxy',
'mihomo',
'connection',
'dns',
'sniff',
'log',
'rule', 'rule',
'resource', 'resource',
'override', 'override',
'connection',
'mihomo',
'dns',
'sniff',
'log',
'substore' 'substore'
] ]
} = appConfig || {} } = appConfig || {}
@ -169,9 +166,6 @@ const App: React.FC = () => {
})} })}
> >
{order.map((key: string) => { {order.map((key: string) => {
if (key === 'dns' && controlDns === false) return null
if (key === 'sniff' && controlSniff === false) return null
if (key === 'substore' && useSubStore === false) return null
return componentMap[key] return componentMap[key]
})} })}
</SortableContext> </SortableContext>

View File

@ -0,0 +1,78 @@
import React from 'react'
import SettingCard from '../base/base-setting-card'
import SettingItem from '../base/base-setting-item'
import { RadioGroup, Radio } from '@nextui-org/react'
import { useAppConfig } from '@renderer/hooks/use-app-config'
const titleMap = {
sysproxyCardStatus: '系统代理',
tunCardStatus: '虚拟网卡',
profileCardStatus: '订阅管理',
proxyCardStatus: '代理组',
ruleCardStatus: '规则',
resourceCardStatus: '外部资源',
overrideCardStatus: '覆写',
connectionCardStatus: '连接',
mihomoCoreCardStatus: '内核',
dnsCardStatus: 'DNS',
sniffCardStatus: '域名嗅探',
logCardStatus: '日志',
substoreCardStatus: 'Sub-Store'
}
const SiderConfig: React.FC = () => {
const { appConfig, patchAppConfig } = useAppConfig()
const {
sysproxyCardStatus = 'col-span-1',
tunCardStatus = 'col-span-1',
profileCardStatus = 'col-span-2',
proxyCardStatus = 'col-span-1',
ruleCardStatus = 'col-span-1',
resourceCardStatus = 'col-span-1',
overrideCardStatus = 'col-span-1',
connectionCardStatus = 'col-span-2',
mihomoCoreCardStatus = 'col-span-2',
dnsCardStatus = 'col-span-1',
sniffCardStatus = 'col-span-1',
logCardStatus = 'col-span-1',
substoreCardStatus = 'col-span-1'
} = appConfig || {}
const cardStatus = {
sysproxyCardStatus,
tunCardStatus,
profileCardStatus,
proxyCardStatus,
ruleCardStatus,
resourceCardStatus,
overrideCardStatus,
connectionCardStatus,
mihomoCoreCardStatus,
dnsCardStatus,
sniffCardStatus,
logCardStatus,
substoreCardStatus
}
return (
<SettingCard>
{Object.keys(cardStatus).map((key, index, array) => {
return (
<SettingItem title={titleMap[key]} key={key} divider={index !== array.length - 1}>
<RadioGroup
orientation="horizontal"
value={cardStatus[key]}
onValueChange={(v) => {
patchAppConfig({ [key]: v as CardStatus })
}}
>
<Radio value="col-span-2"></Radio>
<Radio value="col-span-1"></Radio>
<Radio value="hidden"></Radio>
</RadioGroup>
</SettingItem>
)
})}
</SettingCard>
)
}
export default SiderConfig

View File

@ -20,7 +20,7 @@ let drawing = false
const ConnCard: React.FC = () => { const ConnCard: React.FC = () => {
const { theme = 'system', systemTheme = 'dark' } = useTheme() const { theme = 'system', systemTheme = 'dark' } = useTheme()
const { appConfig } = useAppConfig() const { appConfig } = useAppConfig()
const { showTraffic } = appConfig || {} const { showTraffic, connectionCardStatus = 'col-span-2' } = appConfig || {}
const navigate = useNavigate() const navigate = useNavigate()
const location = useLocation() const location = useLocation()
const match = location.pathname.includes('/connections') const match = location.pathname.includes('/connections')
@ -172,52 +172,86 @@ const ConnCard: React.FC = () => {
transition, transition,
zIndex: isDragging ? 'calc(infinity)' : undefined zIndex: isDragging ? 'calc(infinity)' : undefined
}} }}
className="col-span-2" className={connectionCardStatus}
> >
<Card {connectionCardStatus === 'col-span-2' ? (
fullWidth <>
className={`${match ? 'bg-primary' : ''}`} <Card
isPressable fullWidth
onPress={() => navigate('/connections')} className={`${match ? 'bg-primary' : ''}`}
> isPressable
<CardBody className="pb-0 pt-0 px-0"> onPress={() => navigate('/connections')}
<div ref={setNodeRef} {...attributes} {...listeners} className="flex justify-between"> >
<Button <CardBody className="pb-1 pt-0 px-0">
isIconOnly <div ref={setNodeRef} {...attributes} {...listeners} className="flex justify-between">
className="bg-transparent pointer-events-none" <Button
variant="flat" isIconOnly
color="default" className="bg-transparent pointer-events-none"
> variant="flat"
<IoLink color="default"
color="default" >
className={`${match ? 'text-white' : 'text-foreground'} text-[24px]`} <IoLink
/> color="default"
</Button> className={`${match ? 'text-white' : 'text-foreground'} text-[24px]`}
<div className={`p-2 w-full ${match ? 'text-white' : 'text-foreground'} `}> />
<div className="flex justify-between"> </Button>
<div className="w-full text-right mr-2">{calcTraffic(upload)}/s</div> <div className={`p-2 w-full ${match ? 'text-white' : 'text-foreground'} `}>
<FaCircleArrowUp className="h-[24px] leading-[24px]" /> <div className="flex justify-between">
<div className="w-full text-right mr-2">{calcTraffic(upload)}/s</div>
<FaCircleArrowUp className="h-[24px] leading-[24px]" />
</div>
<div className="flex justify-between">
<div className="w-full text-right mr-2">{calcTraffic(download)}/s</div>
<FaCircleArrowDown className="h-[24px] leading-[24px]" />
</div>
</div>
</div> </div>
<div className="flex justify-between"> </CardBody>
<div className="w-full text-right mr-2">{calcTraffic(download)}/s</div> <CardFooter className="pt-1">
<FaCircleArrowDown className="h-[24px] leading-[24px]" /> <h3 className={`text-md font-bold ${match ? 'text-white' : 'text-foreground'}`}>
</div>
</div> </h3>
</CardFooter>
</Card>
<div className="w-full h-full absolute top-0 left-0 pointer-events-none rounded-[14px] overflow-hidden">
<Chart
options={getApexChartOptions()}
series={[{ name: 'Total', data: series }]}
height={'100%'}
width={'100%'}
type="area"
/>
</div> </div>
</CardBody> </>
<CardFooter className="pt-1"> ) : (
<h3 className={`text-md font-bold ${match ? 'text-white' : 'text-foreground'}`}></h3> <Card
</CardFooter> fullWidth
</Card> className={`${match ? 'bg-primary' : ''}`}
<div className="w-full h-full absolute top-0 left-0 pointer-events-none rounded-[14px] overflow-hidden"> isPressable
<Chart onPress={() => navigate('/logs')}
options={getApexChartOptions()} >
series={[{ name: 'Total', data: series }]} <CardBody className="pb-1 pt-0 px-0">
height={'100%'} <div ref={setNodeRef} {...attributes} {...listeners} className="flex justify-between">
width={'100%'} <Button
type="area" isIconOnly
/> className="bg-transparent pointer-events-none"
</div> variant="flat"
color="default"
>
<IoLink
color="default"
className={`${match ? 'text-white' : 'text-foreground'} text-[24px] font-bold`}
/>
</Button>
</div>
</CardBody>
<CardFooter className="pt-1">
<h3 className={`text-md font-bold ${match ? 'text-white' : 'text-foreground'}`}>
</h3>
</CardFooter>
</Card>
)}
</div> </div>
) )
} }

View File

@ -6,7 +6,10 @@ import { useLocation, useNavigate } from 'react-router-dom'
import { patchMihomoConfig } from '@renderer/utils/ipc' import { patchMihomoConfig } from '@renderer/utils/ipc'
import { useSortable } from '@dnd-kit/sortable' import { useSortable } from '@dnd-kit/sortable'
import { CSS } from '@dnd-kit/utilities' import { CSS } from '@dnd-kit/utilities'
import { useAppConfig } from '@renderer/hooks/use-app-config'
const DNSCard: React.FC = () => { const DNSCard: React.FC = () => {
const { appConfig } = useAppConfig()
const { dnsCardStatus = 'col-span-1', controlDns = true } = appConfig || {}
const navigate = useNavigate() const navigate = useNavigate()
const location = useLocation() const location = useLocation()
const match = location.pathname.includes('/dns') const match = location.pathname.includes('/dns')
@ -37,7 +40,7 @@ const DNSCard: React.FC = () => {
transition, transition,
zIndex: isDragging ? 'calc(infinity)' : undefined zIndex: isDragging ? 'calc(infinity)' : undefined
}} }}
className="col-span-1" className={`${dnsCardStatus} ${!controlDns ? 'hidden' : ''}`}
> >
<Card <Card
fullWidth fullWidth

View File

@ -3,7 +3,10 @@ import { IoJournalOutline } from 'react-icons/io5'
import { useLocation, useNavigate } from 'react-router-dom' import { useLocation, useNavigate } from 'react-router-dom'
import { useSortable } from '@dnd-kit/sortable' import { useSortable } from '@dnd-kit/sortable'
import { CSS } from '@dnd-kit/utilities' import { CSS } from '@dnd-kit/utilities'
import { useAppConfig } from '@renderer/hooks/use-app-config'
const LogCard: React.FC = () => { const LogCard: React.FC = () => {
const { appConfig } = useAppConfig()
const { logCardStatus = 'col-span-1' } = appConfig || {}
const navigate = useNavigate() const navigate = useNavigate()
const location = useLocation() const location = useLocation()
const match = location.pathname.includes('/logs') const match = location.pathname.includes('/logs')
@ -26,11 +29,11 @@ const LogCard: React.FC = () => {
transition, transition,
zIndex: isDragging ? 'calc(infinity)' : undefined zIndex: isDragging ? 'calc(infinity)' : undefined
}} }}
className="col-span-1" className={logCardStatus}
> >
<Card <Card
fullWidth fullWidth
className={`col-span-1 ${match ? 'bg-primary' : ''}`} className={`${match ? 'bg-primary' : ''}`}
isPressable isPressable
onPress={() => navigate('/logs')} onPress={() => navigate('/logs')}
> >

View File

@ -8,8 +8,12 @@ import { CSS } from '@dnd-kit/utilities'
import { useLocation, useNavigate } from 'react-router-dom' import { useLocation, useNavigate } from 'react-router-dom'
import PubSub from 'pubsub-js' import PubSub from 'pubsub-js'
import useSWR from 'swr' import useSWR from 'swr'
import { useAppConfig } from '@renderer/hooks/use-app-config'
import { LuCpu } from 'react-icons/lu'
const MihomoCoreCard: React.FC = () => { const MihomoCoreCard: React.FC = () => {
const { appConfig } = useAppConfig()
const { mihomoCoreCardStatus = 'col-span-2' } = appConfig || {}
const { data: version, mutate } = useSWR('mihomoVersion', mihomoVersion) const { data: version, mutate } = useSWR('mihomoVersion', mihomoVersion)
const navigate = useNavigate() const navigate = useNavigate()
const location = useLocation() const location = useLocation()
@ -48,55 +52,87 @@ const MihomoCoreCard: React.FC = () => {
transition, transition,
zIndex: isDragging ? 'calc(infinity)' : undefined zIndex: isDragging ? 'calc(infinity)' : undefined
}} }}
className="col-span-2" className={mihomoCoreCardStatus}
> >
<Card {mihomoCoreCardStatus === 'col-span-2' ? (
fullWidth <Card
isPressable fullWidth
onPress={() => navigate('/mihomo')} isPressable
className={`${match ? 'bg-primary' : ''}`} onPress={() => navigate('/mihomo')}
> className={`${match ? 'bg-primary' : ''}`}
<CardBody> >
<div <CardBody>
ref={setNodeRef} <div
{...attributes} ref={setNodeRef}
{...listeners} {...attributes}
className="flex justify-between h-[32px]" {...listeners}
> className="flex justify-between h-[32px]"
<h3
className={`text-md font-bold leading-[32px] ${match ? 'text-white' : 'text-foreground'} `}
> >
{version?.version ?? '-'} <h3
</h3> className={`text-md font-bold leading-[32px] ${match ? 'text-white' : 'text-foreground'} `}
>
{version?.version ?? '-'}
</h3>
<Button <Button
isIconOnly isIconOnly
size="sm" size="sm"
variant="light" variant="light"
color="default" color="default"
onPress={async () => { onPress={async () => {
try { try {
await restartCore() await restartCore()
} catch (e) { } catch (e) {
alert(e) alert(e)
} finally { } finally {
mutate() mutate()
} }
}} }}
>
<IoMdRefresh
className={`${match ? 'text-white' : 'text-foreground'} text-[24px]`}
/>
</Button>
</div>
</CardBody>
<CardFooter className="pt-1">
<div
className={`flex justify-between w-full text-md font-bold ${match ? 'text-white' : 'text-foreground'}`}
> >
<IoMdRefresh className={`${match ? 'text-white' : 'text-foreground'} text-[24px]`} /> <h4></h4>
</Button> <h4>{calcTraffic(mem)}</h4>
</div> </div>
</CardBody> </CardFooter>
<CardFooter className="pt-1"> </Card>
<div ) : (
className={`flex justify-between w-full text-md font-bold ${match ? 'text-white' : 'text-foreground'}`} <Card
> fullWidth
<h4></h4> className={`${match ? 'bg-primary' : ''}`}
<h4>{calcTraffic(mem)}</h4> isPressable
</div> onPress={() => navigate('/mihomo')}
</CardFooter> >
</Card> <CardBody className="pb-1 pt-0 px-0">
<div ref={setNodeRef} {...attributes} {...listeners} className="flex justify-between">
<Button
isIconOnly
className="bg-transparent pointer-events-none"
variant="flat"
color="default"
>
<LuCpu
color="default"
className={`${match ? 'text-white' : 'text-foreground'} text-[24px] font-bold`}
/>
</Button>
</div>
</CardBody>
<CardFooter className="pt-1">
<h3 className={`text-md font-bold ${match ? 'text-white' : 'text-foreground'}`}>
</h3>
</CardFooter>
</Card>
)}
</div> </div>
) )
} }

View File

@ -4,8 +4,11 @@ import { MdFormatOverline } from 'react-icons/md'
import { useLocation, useNavigate } from 'react-router-dom' import { useLocation, useNavigate } from 'react-router-dom'
import { useSortable } from '@dnd-kit/sortable' import { useSortable } from '@dnd-kit/sortable'
import { CSS } from '@dnd-kit/utilities' import { CSS } from '@dnd-kit/utilities'
import { useAppConfig } from '@renderer/hooks/use-app-config'
const OverrideCard: React.FC = () => { const OverrideCard: React.FC = () => {
const { appConfig } = useAppConfig()
const { overrideCardStatus = 'col-span-1' } = appConfig || {}
const navigate = useNavigate() const navigate = useNavigate()
const location = useLocation() const location = useLocation()
const match = location.pathname.includes('/override') const match = location.pathname.includes('/override')
@ -28,11 +31,11 @@ const OverrideCard: React.FC = () => {
transition, transition,
zIndex: isDragging ? 'calc(infinity)' : undefined zIndex: isDragging ? 'calc(infinity)' : undefined
}} }}
className="col-span-1" className={overrideCardStatus}
> >
<Card <Card
fullWidth fullWidth
className={`col-span-1 ${match ? 'bg-primary' : ''}`} className={`${match ? 'bg-primary' : ''}`}
isPressable isPressable
onPress={() => navigate('/override')} onPress={() => navigate('/override')}
> >

View File

@ -11,11 +11,15 @@ import 'dayjs/locale/zh-cn'
import dayjs from 'dayjs' import dayjs from 'dayjs'
import { useState } from 'react' import { useState } from 'react'
import ConfigViewer from './config-viewer' import ConfigViewer from './config-viewer'
import { useAppConfig } from '@renderer/hooks/use-app-config'
import { TiFolder } from 'react-icons/ti'
dayjs.extend(relativeTime) dayjs.extend(relativeTime)
dayjs.locale('zh-cn') dayjs.locale('zh-cn')
const ProfileCard: React.FC = () => { const ProfileCard: React.FC = () => {
const { appConfig } = useAppConfig()
const { profileCardStatus = 'col-span-2' } = appConfig || {}
const navigate = useNavigate() const navigate = useNavigate()
const location = useLocation() const location = useLocation()
const match = location.pathname.includes('/profiles') const match = location.pathname.includes('/profiles')
@ -52,98 +56,128 @@ const ProfileCard: React.FC = () => {
transition, transition,
zIndex: isDragging ? 'calc(infinity)' : undefined zIndex: isDragging ? 'calc(infinity)' : undefined
}} }}
className="col-span-2" className={profileCardStatus}
> >
{showRuntimeConfig && <ConfigViewer onClose={() => setShowRuntimeConfig(false)} />} {showRuntimeConfig && <ConfigViewer onClose={() => setShowRuntimeConfig(false)} />}
<Card {profileCardStatus === 'col-span-2' ? (
fullWidth <Card
className={`${match ? 'bg-primary' : ''}`} fullWidth
isPressable className={`${match ? 'bg-primary' : ''}`}
onPress={() => navigate('/profiles')} isPressable
> onPress={() => navigate('/profiles')}
<CardBody className="pb-1"> >
<div <CardBody className="pb-1">
ref={setNodeRef} <div
{...attributes} ref={setNodeRef}
{...listeners} {...attributes}
className="flex justify-between h-[32px]" {...listeners}
> className="flex justify-between h-[32px]"
<h3
title={info?.name}
className={`text-ellipsis whitespace-nowrap overflow-hidden text-md font-bold leading-[32px] ${match ? 'text-white' : 'text-foreground'} `}
> >
{info?.name} <h3
</h3> title={info?.name}
<div className="flex"> className={`text-ellipsis whitespace-nowrap overflow-hidden text-md font-bold leading-[32px] ${match ? 'text-white' : 'text-foreground'} `}
<Button
isIconOnly
size="sm"
title="查看当前运行时配置"
variant="light"
color="default"
onPress={() => {
setShowRuntimeConfig(true)
}}
> >
<CgLoadbarDoc {info?.name}
className={`text-[24px] ${match ? 'text-white' : 'text-foreground'}`} </h3>
/> <div className="flex">
</Button>
{info.type === 'remote' && (
<Button <Button
isIconOnly isIconOnly
size="sm" size="sm"
title={dayjs(info.updated).fromNow()} title="查看当前运行时配置"
disabled={updating}
variant="light" variant="light"
color="default" color="default"
onPress={async () => { onPress={() => {
setUpdating(true) setShowRuntimeConfig(true)
await addProfileItem(info)
setUpdating(false)
}} }}
> >
<IoMdRefresh <CgLoadbarDoc
className={`text-[24px] ${match ? 'text-white' : 'text-foreground'} ${updating ? 'animate-spin' : ''}`} className={`text-[24px] ${match ? 'text-white' : 'text-foreground'}`}
/> />
</Button> </Button>
)} {info.type === 'remote' && (
<Button
isIconOnly
size="sm"
title={dayjs(info.updated).fromNow()}
disabled={updating}
variant="light"
color="default"
onPress={async () => {
setUpdating(true)
await addProfileItem(info)
setUpdating(false)
}}
>
<IoMdRefresh
className={`text-[24px] ${match ? 'text-white' : 'text-foreground'} ${updating ? 'animate-spin' : ''}`}
/>
</Button>
)}
</div>
</div> </div>
</div> {info.type === 'remote' && extra && (
{info.type === 'remote' && extra && ( <div
<div className={`mt-2 flex justify-between ${match ? 'text-white' : 'text-foreground'} `}
className={`mt-2 flex justify-between ${match ? 'text-white' : 'text-foreground'} `}
>
<small>{`${calcTraffic(usage)}/${calcTraffic(total)}`}</small>
<small>
{extra.expire ? dayjs.unix(extra.expire).format('YYYY-MM-DD') : '长期有效'}
</small>
</div>
)}
{info.type === 'local' && (
<div
className={`mt-2 flex justify-between ${match ? 'text-white' : 'text-foreground'}`}
>
<Chip
size="sm"
variant="bordered"
className={`${match ? 'text-white border-white' : 'border-primary text-primary'}`}
> >
<small>{`${calcTraffic(usage)}/${calcTraffic(total)}`}</small>
</Chip> <small>
{extra.expire ? dayjs.unix(extra.expire).format('YYYY-MM-DD') : '长期有效'}
</small>
</div>
)}
{info.type === 'local' && (
<div
className={`mt-2 flex justify-between ${match ? 'text-white' : 'text-foreground'}`}
>
<Chip
size="sm"
variant="bordered"
className={`${match ? 'text-white border-white' : 'border-primary text-primary'}`}
>
</Chip>
</div>
)}
</CardBody>
<CardFooter className="pt-0">
{extra && (
<Progress
className="w-full"
classNames={{ indicator: match ? 'bg-white' : 'bg-foreground' }}
value={calcPercent(extra?.upload, extra?.download, extra?.total)}
/>
)}
</CardFooter>
</Card>
) : (
<Card
fullWidth
className={`${match ? 'bg-primary' : ''}`}
isPressable
onPress={() => navigate('/profiles')}
>
<CardBody className="pb-1 pt-0 px-0">
<div ref={setNodeRef} {...attributes} {...listeners} className="flex justify-between">
<Button
isIconOnly
className="bg-transparent pointer-events-none"
variant="flat"
color="default"
>
<TiFolder
color="default"
className={`${match ? 'text-white' : 'text-foreground'} text-[24px]`}
/>
</Button>
</div> </div>
)} </CardBody>
</CardBody> <CardFooter className="pt-1">
<CardFooter className="pt-0"> <h3 className={`text-md font-bold ${match ? 'text-white' : 'text-foreground'}`}>
{extra && (
<Progress </h3>
className="w-full" </CardFooter>
classNames={{ indicator: match ? 'bg-white' : 'bg-foreground' }} </Card>
value={calcPercent(extra?.upload, extra?.download, extra?.total)} )}
/>
)}
</CardFooter>
</Card>
</div> </div>
) )
} }

View File

@ -4,8 +4,11 @@ import { CSS } from '@dnd-kit/utilities'
import { LuGroup } from 'react-icons/lu' import { LuGroup } from 'react-icons/lu'
import { useLocation, useNavigate } from 'react-router-dom' import { useLocation, useNavigate } from 'react-router-dom'
import { useGroups } from '@renderer/hooks/use-groups' import { useGroups } from '@renderer/hooks/use-groups'
import { useAppConfig } from '@renderer/hooks/use-app-config'
const ProxyCard: React.FC = () => { const ProxyCard: React.FC = () => {
const { appConfig } = useAppConfig()
const { proxyCardStatus = 'col-span-1' } = appConfig || {}
const navigate = useNavigate() const navigate = useNavigate()
const location = useLocation() const location = useLocation()
const match = location.pathname.includes('/proxies') const match = location.pathname.includes('/proxies')
@ -30,7 +33,7 @@ const ProxyCard: React.FC = () => {
transition, transition,
zIndex: isDragging ? 'calc(infinity)' : undefined zIndex: isDragging ? 'calc(infinity)' : undefined
}} }}
className="col-span-2" className={proxyCardStatus}
> >
<Card <Card
fullWidth fullWidth

View File

@ -4,7 +4,10 @@ import { useLocation, useNavigate } from 'react-router-dom'
import { useSortable } from '@dnd-kit/sortable' import { useSortable } from '@dnd-kit/sortable'
import { CSS } from '@dnd-kit/utilities' import { CSS } from '@dnd-kit/utilities'
import { IoLayersOutline } from 'react-icons/io5' import { IoLayersOutline } from 'react-icons/io5'
import { useAppConfig } from '@renderer/hooks/use-app-config'
const ResourceCard: React.FC = () => { const ResourceCard: React.FC = () => {
const { appConfig } = useAppConfig()
const { resourceCardStatus = 'col-span-1' } = appConfig || {}
const navigate = useNavigate() const navigate = useNavigate()
const location = useLocation() const location = useLocation()
const match = location.pathname.includes('/resources') const match = location.pathname.includes('/resources')
@ -27,11 +30,11 @@ const ResourceCard: React.FC = () => {
transition, transition,
zIndex: isDragging ? 'calc(infinity)' : undefined zIndex: isDragging ? 'calc(infinity)' : undefined
}} }}
className="col-span-1" className={resourceCardStatus}
> >
<Card <Card
fullWidth fullWidth
className={`col-span-1 ${match ? 'bg-primary' : ''}`} className={`${match ? 'bg-primary' : ''}`}
isPressable isPressable
onPress={() => navigate('/resources')} onPress={() => navigate('/resources')}
> >

View File

@ -4,8 +4,11 @@ import { useLocation, useNavigate } from 'react-router-dom'
import { useSortable } from '@dnd-kit/sortable' import { useSortable } from '@dnd-kit/sortable'
import { CSS } from '@dnd-kit/utilities' import { CSS } from '@dnd-kit/utilities'
import { useRules } from '@renderer/hooks/use-rules' import { useRules } from '@renderer/hooks/use-rules'
import { useAppConfig } from '@renderer/hooks/use-app-config'
const RuleCard: React.FC = () => { const RuleCard: React.FC = () => {
const { appConfig } = useAppConfig()
const { ruleCardStatus = 'col-span-1' } = appConfig || {}
const navigate = useNavigate() const navigate = useNavigate()
const location = useLocation() const location = useLocation()
const match = location.pathname.includes('/rules') const match = location.pathname.includes('/rules')
@ -29,11 +32,11 @@ const RuleCard: React.FC = () => {
transition, transition,
zIndex: isDragging ? 'calc(infinity)' : undefined zIndex: isDragging ? 'calc(infinity)' : undefined
}} }}
className="col-span-1" className={ruleCardStatus}
> >
<Card <Card
fullWidth fullWidth
className={`col-span-1 ${match ? 'bg-primary' : ''}`} className={`${match ? 'bg-primary' : ''}`}
isPressable isPressable
onPress={() => navigate('/rules')} onPress={() => navigate('/rules')}
> >

View File

@ -6,8 +6,11 @@ import { patchMihomoConfig } from '@renderer/utils/ipc'
import { useControledMihomoConfig } from '@renderer/hooks/use-controled-mihomo-config' import { useControledMihomoConfig } from '@renderer/hooks/use-controled-mihomo-config'
import { useSortable } from '@dnd-kit/sortable' import { useSortable } from '@dnd-kit/sortable'
import { CSS } from '@dnd-kit/utilities' import { CSS } from '@dnd-kit/utilities'
import { useAppConfig } from '@renderer/hooks/use-app-config'
const SniffCard: React.FC = () => { const SniffCard: React.FC = () => {
const { appConfig } = useAppConfig()
const { sniffCardStatus = 'col-span-1', controlSniff = true } = appConfig || {}
const navigate = useNavigate() const navigate = useNavigate()
const location = useLocation() const location = useLocation()
const match = location.pathname.includes('/sniffer') const match = location.pathname.includes('/sniffer')
@ -38,11 +41,11 @@ const SniffCard: React.FC = () => {
transition, transition,
zIndex: isDragging ? 'calc(infinity)' : undefined zIndex: isDragging ? 'calc(infinity)' : undefined
}} }}
className="col-span-1" className={`${sniffCardStatus} ${!controlSniff ? 'hidden' : ''}`}
> >
<Card <Card
fullWidth fullWidth
className={`col-span-1 ${match ? 'bg-primary' : ''}`} className={`${match ? 'bg-primary' : ''}`}
isPressable isPressable
onPress={() => navigate('/sniffer')} onPress={() => navigate('/sniffer')}
> >

View File

@ -3,7 +3,10 @@ import { useLocation, useNavigate } from 'react-router-dom'
import { useSortable } from '@dnd-kit/sortable' import { useSortable } from '@dnd-kit/sortable'
import { CSS } from '@dnd-kit/utilities' import { CSS } from '@dnd-kit/utilities'
import SubStoreIcon from '../base/substore-icon' import SubStoreIcon from '../base/substore-icon'
import { useAppConfig } from '@renderer/hooks/use-app-config'
const SubStoreCard: React.FC = () => { const SubStoreCard: React.FC = () => {
const { appConfig } = useAppConfig()
const { substoreCardStatus = 'col-span-1', useSubStore = true } = appConfig || {}
const navigate = useNavigate() const navigate = useNavigate()
const location = useLocation() const location = useLocation()
const match = location.pathname.includes('/substore') const match = location.pathname.includes('/substore')
@ -26,11 +29,11 @@ const SubStoreCard: React.FC = () => {
transition, transition,
zIndex: isDragging ? 'calc(infinity)' : undefined zIndex: isDragging ? 'calc(infinity)' : undefined
}} }}
className="col-span-1" className={`${substoreCardStatus} ${!useSubStore ? 'hidden' : ''}`}
> >
<Card <Card
fullWidth fullWidth
className={`col-span-1 ${match ? 'bg-primary' : ''}`} className={`${match ? 'bg-primary' : ''}`}
isPressable isPressable
onPress={() => navigate('/substore')} onPress={() => navigate('/substore')}
> >

View File

@ -13,7 +13,7 @@ const SysproxySwitcher: React.FC = () => {
const location = useLocation() const location = useLocation()
const match = location.pathname.includes('/sysproxy') const match = location.pathname.includes('/sysproxy')
const { appConfig, patchAppConfig } = useAppConfig() const { appConfig, patchAppConfig } = useAppConfig()
const { sysProxy } = appConfig || {} const { sysProxy, sysproxyCardStatus = 'col-span-1' } = appConfig || {}
const { enable } = sysProxy || {} const { enable } = sysProxy || {}
const { const {
attributes, attributes,
@ -44,7 +44,7 @@ const SysproxySwitcher: React.FC = () => {
transition, transition,
zIndex: isDragging ? 'calc(infinity)' : undefined zIndex: isDragging ? 'calc(infinity)' : undefined
}} }}
className="col-span-1 " className={sysproxyCardStatus}
> >
<Card <Card
fullWidth fullWidth

View File

@ -17,6 +17,7 @@ const TunSwitcher: React.FC = () => {
const match = location.pathname.includes('/tun') || false const match = location.pathname.includes('/tun') || false
const [openPasswordModal, setOpenPasswordModal] = useState(false) const [openPasswordModal, setOpenPasswordModal] = useState(false)
const { appConfig, patchAppConfig } = useAppConfig() const { appConfig, patchAppConfig } = useAppConfig()
const { tunCardStatus = 'col-span-1' } = appConfig || {}
const { controledMihomoConfig, patchControledMihomoConfig } = useControledMihomoConfig() const { controledMihomoConfig, patchControledMihomoConfig } = useControledMihomoConfig()
const { tun } = controledMihomoConfig || {} const { tun } = controledMihomoConfig || {}
const { enable } = tun || {} const { enable } = tun || {}
@ -62,7 +63,7 @@ const TunSwitcher: React.FC = () => {
transition, transition,
zIndex: isDragging ? 'calc(infinity)' : undefined zIndex: isDragging ? 'calc(infinity)' : undefined
}} }}
className="col-span-1" className={tunCardStatus}
> >
{openPasswordModal && ( {openPasswordModal && (
<BasePasswordModal <BasePasswordModal

View File

@ -96,7 +96,13 @@ const Connections: React.FC = () => {
{calcTraffic(connectionsInfo?.downloadTotal ?? 0)}{' '} {calcTraffic(connectionsInfo?.downloadTotal ?? 0)}{' '}
</span> </span>
</div> </div>
<Badge color="primary" variant="flat" content={`${filteredConnections.length}`}> <Badge
className="mt-2"
color="primary"
variant="flat"
showOutline={false}
content={`${filteredConnections.length}`}
>
<Button <Button
className="app-nodrag ml-1" className="app-nodrag ml-1"
title="关闭全部连接" title="关闭全部连接"

View File

@ -8,6 +8,7 @@ import MihomoConfig from '@renderer/components/settings/mihomo-config'
import Actions from '@renderer/components/settings/actions' import Actions from '@renderer/components/settings/actions'
import ShortcutConfig from '@renderer/components/settings/shortcut-config' import ShortcutConfig from '@renderer/components/settings/shortcut-config'
import { FaTelegramPlane } from 'react-icons/fa' import { FaTelegramPlane } from 'react-icons/fa'
import SiderConfig from '@renderer/components/settings/sider-config'
const Settings: React.FC = () => { const Settings: React.FC = () => {
return ( return (
@ -55,6 +56,7 @@ const Settings: React.FC = () => {
} }
> >
<GeneralConfig /> <GeneralConfig />
<SiderConfig />
<WebdavConfig /> <WebdavConfig />
<MihomoConfig /> <MihomoConfig />
<ShortcutConfig /> <ShortcutConfig />

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

@ -1,6 +1,7 @@
type OutboundMode = 'rule' | 'global' | 'direct' type OutboundMode = 'rule' | 'global' | 'direct'
type LogLevel = 'info' | 'debug' | 'warning' | 'error' | 'silent' type LogLevel = 'info' | 'debug' | 'warning' | 'error' | 'silent'
type SysProxyMode = 'auto' | 'manual' type SysProxyMode = 'auto' | 'manual'
type CardStatus = 'col-span-2' | 'col-span-1' | 'hidden'
type AppTheme = type AppTheme =
| 'system' | 'system'
| 'light' | 'light'
@ -219,6 +220,19 @@ interface IAppConfig {
proxyCols: 'auto' | '1' | '2' | '3' | '4' proxyCols: 'auto' | '1' | '2' | '3' | '4'
connectionDirection: 'asc' | 'desc' connectionDirection: 'asc' | 'desc'
connectionOrderBy: 'time' | 'upload' | 'download' | 'uploadSpeed' | 'downloadSpeed' connectionOrderBy: 'time' | 'upload' | 'download' | 'uploadSpeed' | 'downloadSpeed'
connectionCardStatus?: CardStatus
dnsCardStatus?: CardStatus
logCardStatus?: CardStatus
mihomoCoreCardStatus?: CardStatus
overrideCardStatus?: CardStatus
profileCardStatus?: CardStatus
proxyCardStatus?: CardStatus
resourceCardStatus?: CardStatus
ruleCardStatus?: CardStatus
sniffCardStatus?: CardStatus
substoreCardStatus?: CardStatus
sysproxyCardStatus?: CardStatus
tunCardStatus?: CardStatus
useSubStore: boolean useSubStore: boolean
useCustomSubStore?: boolean useCustomSubStore?: boolean
customSubStoreUrl?: string customSubStoreUrl?: string