support for sorting sidebar items

This commit is contained in:
pompurin404 2024-08-11 14:53:40 +08:00
parent a8e886b9bb
commit 5c7f3f48f7
No known key found for this signature in database
18 changed files with 662 additions and 359 deletions

View File

@ -27,6 +27,9 @@
"yaml": "^2.5.0"
},
"devDependencies": {
"@dnd-kit/core": "^6.1.0",
"@dnd-kit/sortable": "^8.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@electron-toolkit/eslint-config-prettier": "^2.0.0",
"@electron-toolkit/eslint-config-ts": "^2.0.0",
"@electron-toolkit/tsconfig": "^1.0.1",

View File

@ -27,6 +27,15 @@ importers:
specifier: ^2.5.0
version: 2.5.0
devDependencies:
'@dnd-kit/core':
specifier: ^6.1.0
version: 6.1.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@dnd-kit/sortable':
specifier: ^8.0.0
version: 8.0.0(@dnd-kit/core@6.1.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)
'@dnd-kit/utilities':
specifier: ^3.2.2
version: 3.2.2(react@18.3.1)
'@electron-toolkit/eslint-config-prettier':
specifier: ^2.0.0
version: 2.0.0(eslint@8.57.0)(prettier@3.3.3)
@ -262,6 +271,28 @@ packages:
resolution: {integrity: sha512-0cp4PsWQ/9avqTVMCtZ+GirikIA36ikvjtHweU4/j8yLtgObI0+JUPhYFScgwlteveGB1rt3Cm8UhN04XayDig==}
engines: {node: '>= 8.9.0'}
'@dnd-kit/accessibility@3.1.0':
resolution: {integrity: sha512-ea7IkhKvlJUv9iSHJOnxinBcoOI3ppGnnL+VDJ75O45Nss6HtZd8IdN8touXPDtASfeI2T2LImb8VOZcL47wjQ==}
peerDependencies:
react: '>=16.8.0'
'@dnd-kit/core@6.1.0':
resolution: {integrity: sha512-J3cQBClB4TVxwGo3KEjssGEXNJqGVWx17aRTZ1ob0FliR5IjYgTxl5YJbKTzA6IzrtelotH19v6y7uoIRUZPSg==}
peerDependencies:
react: '>=16.8.0'
react-dom: '>=16.8.0'
'@dnd-kit/sortable@8.0.0':
resolution: {integrity: sha512-U3jk5ebVXe1Lr7c2wU7SBZjcWdQP+j7peHJfCspnA81enlu88Mgd7CC8Q+pub9ubP7eKVETzJW+IBAhsqbSu/g==}
peerDependencies:
'@dnd-kit/core': ^6.1.0
react: '>=16.8.0'
'@dnd-kit/utilities@3.2.2':
resolution: {integrity: sha512-+MKAJEOfaBe5SmV6t34p80MMKhjvUz0vRrvVJbPT0WElzaOJ/1xs+D+KDv+tD/NE5ujfrChEcshd4fLn0wpiqg==}
peerDependencies:
react: '>=16.8.0'
'@electron-toolkit/eslint-config-prettier@2.0.0':
resolution: {integrity: sha512-L+uG1FvJcAZkPZpSi6B1pmdpyJFyOxWDTjr1Vs47vSryxv/EX1Ch6o4HVsachlDq3fMEkDgojuP2F3ZvVZMoLw==}
peerDependencies:
@ -4545,6 +4576,31 @@ snapshots:
ajv: 6.12.6
ajv-keywords: 3.5.2(ajv@6.12.6)
'@dnd-kit/accessibility@3.1.0(react@18.3.1)':
dependencies:
react: 18.3.1
tslib: 2.6.3
'@dnd-kit/core@6.1.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@dnd-kit/accessibility': 3.1.0(react@18.3.1)
'@dnd-kit/utilities': 3.2.2(react@18.3.1)
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
tslib: 2.6.3
'@dnd-kit/sortable@8.0.0(@dnd-kit/core@6.1.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1))(react@18.3.1)':
dependencies:
'@dnd-kit/core': 6.1.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@dnd-kit/utilities': 3.2.2(react@18.3.1)
react: 18.3.1
tslib: 2.6.3
'@dnd-kit/utilities@3.2.2(react@18.3.1)':
dependencies:
react: 18.3.1
tslib: 2.6.3
'@electron-toolkit/eslint-config-prettier@2.0.0(eslint@8.57.0)(prettier@3.3.3)':
dependencies:
eslint: 8.57.0

View File

@ -8,6 +8,21 @@ export const defaultConfig: IAppConfig = {
autoCloseConnection: true,
useNameserverPolicy: false,
nameserverPolicy: {},
siderOrder: [
'mode',
'sysproxy',
'tun',
'profile',
'proxy',
'mihomo',
'connection',
'dns',
'sniff',
'log',
'rule',
'resource',
'override'
],
sysProxy: { enable: false, mode: 'manual' }
}

View File

@ -1,5 +1,5 @@
import { useTheme } from 'next-themes'
import { useEffect } from 'react'
import { useEffect, useState } from 'react'
import { useLocation, useNavigate, useRoutes } from 'react-router-dom'
import OutboundModeSwitcher from '@renderer/components/sider/outbound-mode-switcher'
import SysproxySwitcher from '@renderer/components/sider/sysproxy-switcher'
@ -7,6 +7,15 @@ import TunSwitcher from '@renderer/components/sider/tun-switcher'
import { Button, Divider } from '@nextui-org/react'
import { IoSettings } from 'react-icons/io5'
import routes from '@renderer/routes'
import {
DndContext,
closestCenter,
PointerSensor,
useSensor,
useSensors,
DragEndEvent
} from '@dnd-kit/core'
import { SortableContext } from '@dnd-kit/sortable'
import ProfileCard from '@renderer/components/sider/profile-card'
import ProxyCard from '@renderer/components/sider/proxy-card'
import RuleCard from '@renderer/components/sider/rule-card'
@ -21,8 +30,31 @@ import UpdaterButton from '@renderer/components/updater/updater-button'
import { useAppConfig } from './hooks/use-app-config'
const App: React.FC = () => {
const { appConfig } = useAppConfig()
const { appTheme = 'system' } = appConfig || {}
const { appConfig, patchAppConfig } = useAppConfig()
const {
appTheme = 'system',
siderOrder = [
'sysproxy',
'tun',
'profile',
'proxy',
'mihomo',
'connection',
'dns',
'sniff',
'log',
'rule',
'resource',
'override'
]
} = appConfig || {}
const [order, setOrder] = useState(siderOrder)
const sensors = useSensors(
useSensor(PointerSensor)
// useSensor(KeyboardSensor, {
// coordinateGetter: sortableKeyboardCoordinates
// })
)
const { setTheme } = useTheme()
const navigate = useNavigate()
const location = useLocation()
@ -32,6 +64,36 @@ const App: React.FC = () => {
setTheme(appTheme)
}, [appTheme])
const onDragEnd = async (event: DragEndEvent): Promise<void> => {
const { active, over } = event
if (over) {
if (active.id !== over.id) {
const newOrder = order.slice()
const activeIndex = newOrder.indexOf(active.id as string)
const overIndex = newOrder.indexOf(over.id as string)
newOrder.splice(activeIndex, 1)
newOrder.splice(overIndex, 0, active.id as string)
setOrder(newOrder)
await patchAppConfig({ siderOrder: newOrder })
}
}
}
const componentMap = {
sysproxy: <SysproxySwitcher />,
tun: <TunSwitcher />,
profile: <ProfileCard />,
proxy: <ProxyCard />,
mihomo: <MihomoCoreCard />,
connection: <ConnCard />,
dns: <DNSCard />,
sniff: <SniffCard />,
log: <LogCard />,
rule: <RuleCard />,
resource: <ResourceCard />,
override: <OverrideCard />
}
return (
<div className="w-full h-[100vh] flex">
<div className="side w-[250px] h-full overflow-y-auto no-scrollbar">
@ -51,44 +113,22 @@ const App: React.FC = () => {
/>
</div>
</div>
<div className="grid grid-cols-2 gap-2 mx-2">
<div className="mt-2 mx-2">
<OutboundModeSwitcher />
<SysproxySwitcher />
<TunSwitcher />
<ProfileCard />
<ProxyCard />
<MihomoCoreCard />
<ConnCard />
<DNSCard />
<SniffCard />
<LogCard />
<RuleCard />
<ResourceCard />
<OverrideCard />
</div>
{/* <div className="flex justify-between mx-2 mb-2">
<SysproxySwitcher />
<TunSwitcher />
</div>
<div className="mx-2">
<ProfileCard />
<ProxyCard />
<MihomoCoreCard />
<ConnCard />
</div>
<div className="flex justify-between mx-2">
<DNSCard />
<SniffCard />
</div>
<div className="flex justify-between mx-2">
<LogCard />
<RuleCard />
</div>
<div className="flex justify-between mx-2">
<ResourceCard />
<OverrideCard />
</div> */}
<DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={onDragEnd}>
<div className="grid grid-cols-2 gap-2 m-2">
<SortableContext
items={order.map((x) => {
return x
})}
>
{order.map((key: string) => {
return componentMap[key]
})}
</SortableContext>
</div>
</DndContext>
</div>
<Divider orientation="vertical" />
<div className="main w-[calc(100%-251px)] h-full overflow-y-auto">{page}</div>

View File

@ -3,6 +3,8 @@ import { FaCircleArrowDown, FaCircleArrowUp } from 'react-icons/fa6'
import { useLocation, useNavigate } from 'react-router-dom'
import { calcTraffic } from '@renderer/utils/calc'
import { useEffect, useState } from 'react'
import { useSortable } from '@dnd-kit/sortable'
import { CSS } from '@dnd-kit/utilities'
import { IoLink } from 'react-icons/io5'
const ConnCard: React.FC = () => {
@ -12,6 +14,9 @@ const ConnCard: React.FC = () => {
const [upload, setUpload] = useState(0)
const [download, setDownload] = useState(0)
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
id: 'connection'
})
useEffect(() => {
window.electron.ipcRenderer.on('mihomoTraffic', (_e, info: IMihomoTrafficInfo) => {
@ -24,41 +29,51 @@ const ConnCard: React.FC = () => {
}, [])
return (
<Card
fullWidth
className={`col-span-2 ${match ? 'bg-primary' : ''}`}
isPressable
onPress={() => navigate('/connections')}
<div
style={{
position: 'relative',
transform: CSS.Transform.toString(transform),
transition,
zIndex: isDragging ? 'calc(infinity)' : undefined
}}
className="col-span-2"
>
<CardBody className="pb-1 pt-0 px-0">
<div className="flex justify-between">
<Button
isIconOnly
className="bg-transparent pointer-events-none"
variant="flat"
color="default"
>
<IoLink
<Card
fullWidth
className={`${match ? 'bg-primary' : ''}`}
isPressable
onPress={() => navigate('/connections')}
>
<CardBody className="pb-0 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"
className={`${match ? 'text-white' : 'text-foreground'} text-[24px]`}
/>
</Button>
<div className={`p-2 w-full ${match ? 'text-white' : 'text-foreground'} `}>
<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]" />
>
<IoLink
color="default"
className={`${match ? 'text-white' : 'text-foreground'} text-[24px]`}
/>
</Button>
<div className={`p-2 w-full ${match ? 'text-white' : 'text-foreground'} `}>
<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>
</CardBody>
<CardFooter className="pt-1">
<h3 className={`text-md font-bold ${match ? 'text-white' : 'text-foreground'}`}></h3>
</CardFooter>
</Card>
</CardBody>
<CardFooter className="pt-1">
<h3 className={`text-md font-bold ${match ? 'text-white' : 'text-foreground'}`}></h3>
</CardFooter>
</Card>
</div>
)
}

View File

@ -4,7 +4,8 @@ import BorderSwitch from '@renderer/components/base/border-swtich'
import { LuServer } from 'react-icons/lu'
import { useLocation, useNavigate } from 'react-router-dom'
import { patchMihomoConfig } from '@renderer/utils/ipc'
import { useSortable } from '@dnd-kit/sortable'
import { CSS } from '@dnd-kit/utilities'
const DNSCard: React.FC = () => {
const navigate = useNavigate()
const location = useLocation()
@ -12,42 +13,55 @@ const DNSCard: React.FC = () => {
const { controledMihomoConfig, patchControledMihomoConfig } = useControledMihomoConfig(true)
const { dns, tun } = controledMihomoConfig || {}
const { enable } = dns || {}
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
id: 'dns'
})
const onChange = async (enable: boolean): Promise<void> => {
await patchControledMihomoConfig({ dns: { enable } })
await patchMihomoConfig({ dns: { enable } })
}
return (
<Card
className={`col-span-1 ${match ? 'bg-primary' : ''}`}
isPressable
onPress={() => navigate('/dns')}
<div
style={{
position: 'relative',
transform: CSS.Transform.toString(transform),
transition,
zIndex: isDragging ? 'calc(infinity)' : undefined
}}
className="col-span-1"
>
<CardBody className="pb-1 pt-0 px-0">
<div className="flex justify-between">
<Button
isIconOnly
className="bg-transparent pointer-events-none"
variant="flat"
color="default"
>
<LuServer
className={`${match ? 'text-white' : 'text-foreground'} text-[24px] font-bold`}
<Card
fullWidth
className={`${match ? 'bg-primary' : ''}`}
isPressable
onPress={() => navigate('/dns')}
>
<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"
>
<LuServer
className={`${match ? 'text-white' : 'text-foreground'} text-[24px] font-bold`}
/>
</Button>
<BorderSwitch
isShowBorder={match && enable}
isSelected={enable}
isDisabled={tun?.enable}
onValueChange={onChange}
/>
</Button>
<BorderSwitch
isShowBorder={match && enable}
isSelected={enable}
isDisabled={tun?.enable}
onValueChange={onChange}
/>
</div>
</CardBody>
<CardFooter className="pt-1">
<h3 className={`text-md font-bold ${match ? 'text-white' : 'text-foreground'}`}>DNS</h3>
</CardFooter>
</Card>
</div>
</CardBody>
<CardFooter className="pt-1">
<h3 className={`text-md font-bold ${match ? 'text-white' : 'text-foreground'}`}>DNS</h3>
</CardFooter>
</Card>
</div>
)
}

View File

@ -1,36 +1,51 @@
import { Button, Card, CardBody, CardFooter } from '@nextui-org/react'
import { IoJournal } from 'react-icons/io5'
import { useLocation, useNavigate } from 'react-router-dom'
import { useSortable } from '@dnd-kit/sortable'
import { CSS } from '@dnd-kit/utilities'
const LogCard: React.FC = () => {
const navigate = useNavigate()
const location = useLocation()
const match = location.pathname.includes('/logs')
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
id: 'log'
})
return (
<Card
className={`col-span-1 ${match ? 'bg-primary' : ''}`}
isPressable
onPress={() => navigate('/logs')}
<div
style={{
position: 'relative',
transform: CSS.Transform.toString(transform),
transition,
zIndex: isDragging ? 'calc(infinity)' : undefined
}}
className="col-span-1"
>
<CardBody className="pb-1 pt-0 px-0">
<div className="flex justify-between">
<Button
isIconOnly
className="bg-transparent pointer-events-none"
variant="flat"
color="default"
>
<IoJournal
<Card
fullWidth
className={`col-span-1 ${match ? 'bg-primary' : ''}`}
isPressable
onPress={() => navigate('/logs')}
>
<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"
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>
>
<IoJournal
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>
)
}

View File

@ -3,6 +3,8 @@ import { calcTraffic } from '@renderer/utils/calc'
import { mihomoVersion, restartCore } from '@renderer/utils/ipc'
import { useEffect, useState } from 'react'
import { IoMdRefresh } from 'react-icons/io'
import { useSortable } from '@dnd-kit/sortable'
import { CSS } from '@dnd-kit/utilities'
import { useLocation, useNavigate } from 'react-router-dom'
import useSWR from 'swr'
@ -11,6 +13,9 @@ const MihomoCoreCard: React.FC = () => {
const navigate = useNavigate()
const location = useLocation()
const match = location.pathname.includes('/mihomo')
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
id: 'mihomo'
})
const [mem, setMem] = useState(0)
@ -31,46 +36,61 @@ const MihomoCoreCard: React.FC = () => {
}, [])
return (
<Card
fullWidth
isPressable
onPress={() => navigate('/mihomo')}
className={`col-span-2 ${match ? 'bg-primary' : ''}`}
<div
style={{
position: 'relative',
transform: CSS.Transform.toString(transform),
transition,
zIndex: isDragging ? 'calc(infinity)' : undefined
}}
className="col-span-2"
>
<CardBody>
<div className="flex justify-between h-[32px]">
<h3
className={`text-md font-bold leading-[32px] ${match ? 'text-white' : 'text-foreground'} `}
<Card
fullWidth
isPressable
onPress={() => navigate('/mihomo')}
className={`${match ? 'bg-primary' : ''}`}
>
<CardBody>
<div
ref={setNodeRef}
{...attributes}
{...listeners}
className="flex justify-between h-[32px]"
>
{version?.version ?? '-'}
</h3>
<h3
className={`text-md font-bold leading-[32px] ${match ? 'text-white' : 'text-foreground'} `}
>
{version?.version ?? '-'}
</h3>
<Button
isIconOnly
size="sm"
variant="light"
color="default"
onPress={async () => {
await restartCore()
mutate()
setTimeout(() => {
<Button
isIconOnly
size="sm"
variant="light"
color="default"
onPress={async () => {
await restartCore()
mutate()
}, 2000)
}}
setTimeout(() => {
mutate()
}, 2000)
}}
>
<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]`} />
</Button>
</div>
</CardBody>
<CardFooter className="pt-1">
<div
className={`flex justify-between w-full text-md font-bold ${match ? 'text-white' : 'text-foreground'}`}
>
<h4></h4>
<h4>{calcTraffic(mem)}</h4>
</div>
</CardFooter>
</Card>
<h4></h4>
<h4>{calcTraffic(mem)}</h4>
</div>
</CardFooter>
</Card>
</div>
)
}

View File

@ -22,7 +22,6 @@ const OutboundModeSwitcher: React.FC = () => {
<Tabs
fullWidth
color="primary"
className="col-span-2"
selectedKey={mode}
onSelectionChange={(key: Key) => onChangeMode(key as OutboundMode)}
>

View File

@ -3,43 +3,58 @@ import BorderSwitch from '@renderer/components/base/border-swtich'
import React, { useState } from 'react'
import { MdFormatOverline } from 'react-icons/md'
import { useLocation, useNavigate } from 'react-router-dom'
import { useSortable } from '@dnd-kit/sortable'
import { CSS } from '@dnd-kit/utilities'
const OverrideCard: React.FC = () => {
const navigate = useNavigate()
const location = useLocation()
const match = location.pathname.includes('/override')
const [enable, setEnable] = useState(false)
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
id: 'override'
})
return (
<Card
className={`col-span-1 ${match ? 'bg-primary' : ''}`}
isPressable
onPress={() => navigate('/override')}
<div
style={{
position: 'relative',
transform: CSS.Transform.toString(transform),
transition,
zIndex: isDragging ? 'calc(infinity)' : undefined
}}
className="col-span-1"
>
<CardBody className="pb-1 pt-0 px-0">
<div className="flex justify-between">
<Button
isIconOnly
className="bg-transparent pointer-events-none"
variant="flat"
color="default"
>
<MdFormatOverline
<Card
fullWidth
className={`col-span-1 ${match ? 'bg-primary' : ''}`}
isPressable
onPress={() => navigate('/override')}
>
<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"
className={`${match ? 'text-white' : 'text-foreground'} text-[24px]`}
>
<MdFormatOverline
color="default"
className={`${match ? 'text-white' : 'text-foreground'} text-[24px]`}
/>
</Button>
<BorderSwitch
isShowBorder={match && enable}
isSelected={enable}
onValueChange={setEnable}
/>
</Button>
<BorderSwitch
isShowBorder={match && enable}
isSelected={enable}
onValueChange={setEnable}
/>
</div>
</CardBody>
<CardFooter className="pt-1">
<h3 className={`text-md font-bold ${match ? 'text-white' : 'text-foreground'}`}></h3>
</CardFooter>
</Card>
</div>
</CardBody>
<CardFooter className="pt-1">
<h3 className={`text-md font-bold ${match ? 'text-white' : 'text-foreground'}`}></h3>
</CardFooter>
</Card>
</div>
)
}

View File

@ -5,6 +5,8 @@ import { calcTraffic, calcPercent } from '@renderer/utils/calc'
import { CgLoadbarDoc } from 'react-icons/cg'
import { IoMdRefresh } from 'react-icons/io'
import relativeTime from 'dayjs/plugin/relativeTime'
import { useSortable } from '@dnd-kit/sortable'
import { CSS } from '@dnd-kit/utilities'
import 'dayjs/locale/zh-cn'
import dayjs from 'dayjs'
import { useState } from 'react'
@ -21,6 +23,9 @@ const ProfileCard: React.FC = () => {
const [showRuntimeConfig, setShowRuntimeConfig] = useState(false)
const { profileConfig, addProfileItem } = useProfileConfig()
const { current, items } = profileConfig ?? {}
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
id: 'profile'
})
const info = items?.find((item) => item.id === current) ?? {
id: 'default',
type: 'local',
@ -32,16 +37,29 @@ const ProfileCard: React.FC = () => {
const total = extra?.total ?? 0
return (
<>
<div
style={{
position: 'relative',
transform: CSS.Transform.toString(transform),
transition,
zIndex: isDragging ? 'calc(infinity)' : undefined
}}
className="col-span-2"
>
{showRuntimeConfig && <ConfigViewer onClose={() => setShowRuntimeConfig(false)} />}
<Card
fullWidth
className={`col-span-2 ${match ? 'bg-primary' : ''}`}
className={`${match ? 'bg-primary' : ''}`}
isPressable
onPress={() => navigate('/profiles')}
>
<CardBody className="pb-1">
<div className="flex justify-between h-[32px]">
<div
ref={setNodeRef}
{...attributes}
{...listeners}
className="flex justify-between h-[32px]"
>
<h3
className={`text-ellipsis whitespace-nowrap overflow-hidden text-md font-bold leading-[32px] ${match ? 'text-white' : 'text-foreground'} `}
>
@ -115,7 +133,7 @@ const ProfileCard: React.FC = () => {
)}
</CardFooter>
</Card>
</>
</div>
)
}

View File

@ -1,6 +1,8 @@
import { Button, Card, CardBody, CardFooter, Chip } from '@nextui-org/react'
import { mihomoProxies } from '@renderer/utils/ipc'
import { useMemo } from 'react'
import { useSortable } from '@dnd-kit/sortable'
import { CSS } from '@dnd-kit/utilities'
import { LuGroup } from 'react-icons/lu'
import { useLocation, useNavigate } from 'react-router-dom'
import useSWR from 'swr'
@ -10,54 +12,68 @@ const ProxyCard: React.FC = () => {
const location = useLocation()
const match = location.pathname.includes('/proxies')
const { data: proxies = { proxies: {} } } = useSWR('mihomoProxies', mihomoProxies)
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
id: 'proxy'
})
const filtered = useMemo(() => {
return Object.keys(proxies.proxies).filter((key) => 'all' in proxies.proxies[key])
}, [proxies])
return (
<Card
fullWidth
className={`col-span-2 ${match ? 'bg-primary' : ''}`}
isPressable
onPress={() => navigate('/proxies')}
<div
style={{
position: 'relative',
transform: CSS.Transform.toString(transform),
transition,
zIndex: isDragging ? 'calc(infinity)' : undefined
}}
className="col-span-2"
>
<CardBody className="pb-1 pt-0 px-0">
<div className="flex justify-between">
<Button
isIconOnly
className="bg-transparent pointer-events-none"
variant="flat"
color="default"
>
<LuGroup
className={`${match ? 'text-white' : 'text-foreground'} text-[24px] font-bold`}
/>
</Button>
<Chip
classNames={
match
? {
base: 'border-white',
content: 'text-white'
}
: {
base: 'border-primary',
content: 'text-primary'
}
}
size="sm"
variant="bordered"
className="mr-3 mt-2"
>
{filtered.length}
</Chip>
</div>
</CardBody>
<CardFooter className="pt-1">
<h3 className={`text-md font-bold ${match ? 'text-white' : 'text-foreground'}`}></h3>
</CardFooter>
</Card>
<Card
fullWidth
className={`${match ? 'bg-primary' : ''}`}
isPressable
onPress={() => navigate('/proxies')}
>
<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"
>
<LuGroup
className={`${match ? 'text-white' : 'text-foreground'} text-[24px] font-bold`}
/>
</Button>
<Chip
classNames={
match
? {
base: 'border-white',
content: 'text-white'
}
: {
base: 'border-primary',
content: 'text-primary'
}
}
size="sm"
variant="bordered"
className="mr-3 mt-2"
>
{filtered.length}
</Chip>
</div>
</CardBody>
<CardFooter className="pt-1">
<h3 className={`text-md font-bold ${match ? 'text-white' : 'text-foreground'}`}>
</h3>
</CardFooter>
</Card>
</div>
)
}

View File

@ -2,39 +2,53 @@ import { Button, Card, CardBody, CardFooter } from '@nextui-org/react'
import React from 'react'
import { FaLayerGroup } from 'react-icons/fa6'
import { useLocation, useNavigate } from 'react-router-dom'
import { useSortable } from '@dnd-kit/sortable'
import { CSS } from '@dnd-kit/utilities'
const ResourceCard: React.FC = () => {
const navigate = useNavigate()
const location = useLocation()
const match = location.pathname.includes('/resources')
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
id: 'resource'
})
return (
<Card
className={`col-span-1 ${match ? 'bg-primary' : ''}`}
isPressable
onPress={() => navigate('/resources')}
<div
style={{
position: 'relative',
transform: CSS.Transform.toString(transform),
transition,
zIndex: isDragging ? 'calc(infinity)' : undefined
}}
className="col-span-1"
>
<CardBody className="pb-1 pt-0 px-0">
<div className="flex justify-between">
<Button
isIconOnly
className="bg-transparent pointer-events-none"
variant="flat"
color="default"
>
<FaLayerGroup
<Card
fullWidth
className={`col-span-1 ${match ? 'bg-primary' : ''}`}
isPressable
onPress={() => navigate('/resources')}
>
<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"
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>
>
<FaLayerGroup
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>
)
}

View File

@ -2,6 +2,8 @@ import { Button, Card, CardBody, CardFooter, Chip } from '@nextui-org/react'
import { mihomoRules } from '@renderer/utils/ipc'
import { MdOutlineAltRoute } from 'react-icons/md'
import { useLocation, useNavigate } from 'react-router-dom'
import { useSortable } from '@dnd-kit/sortable'
import { CSS } from '@dnd-kit/utilities'
import useSWR from 'swr'
const RuleCard: React.FC = () => {
@ -11,50 +13,64 @@ const RuleCard: React.FC = () => {
const { data: rules } = useSWR<IMihomoRulesInfo>('mihomoRules', mihomoRules, {
refreshInterval: 5000
})
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
id: 'rule'
})
return (
<Card
className={`col-span-1 ${match ? 'bg-primary' : ''}`}
isPressable
onPress={() => navigate('/rules')}
<div
style={{
position: 'relative',
transform: CSS.Transform.toString(transform),
transition,
zIndex: isDragging ? 'calc(infinity)' : undefined
}}
className="col-span-1"
>
<CardBody className="pb-1 pt-0 px-0">
<div className="flex justify-between">
<Button
isIconOnly
className="bg-transparent pointer-events-none"
variant="flat"
color="default"
>
<MdOutlineAltRoute
<Card
fullWidth
className={`col-span-1 ${match ? 'bg-primary' : ''}`}
isPressable
onPress={() => navigate('/rules')}
>
<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"
className={`${match ? 'text-white' : 'text-foreground'} text-[24px]`}
/>
</Button>
<Chip
classNames={
match
? {
base: 'border-white',
content: 'text-white'
}
: {
base: 'border-primary',
content: 'text-primary'
}
}
size="sm"
variant="bordered"
className="mr-3 mt-2"
>
{rules?.rules?.length ?? 0}
</Chip>
</div>
</CardBody>
<CardFooter className="pt-1">
<h3 className={`text-md font-bold ${match ? 'text-white' : 'text-foreground'}`}></h3>
</CardFooter>
</Card>
>
<MdOutlineAltRoute
color="default"
className={`${match ? 'text-white' : 'text-foreground'} text-[24px]`}
/>
</Button>
<Chip
classNames={
match
? {
base: 'border-white',
content: 'text-white'
}
: {
base: 'border-primary',
content: 'text-primary'
}
}
size="sm"
variant="bordered"
className="mr-3 mt-2"
>
{rules?.rules?.length ?? 0}
</Chip>
</div>
</CardBody>
<CardFooter className="pt-1">
<h3 className={`text-md font-bold ${match ? 'text-white' : 'text-foreground'}`}></h3>
</CardFooter>
</Card>
</div>
)
}

View File

@ -4,6 +4,8 @@ import { RiScan2Fill } from 'react-icons/ri'
import { useLocation, useNavigate } from 'react-router-dom'
import { patchMihomoConfig } from '@renderer/utils/ipc'
import { useControledMihomoConfig } from '@renderer/hooks/use-controled-mihomo-config'
import { useSortable } from '@dnd-kit/sortable'
import { CSS } from '@dnd-kit/utilities'
const SniffCard: React.FC = () => {
const navigate = useNavigate()
@ -12,6 +14,9 @@ const SniffCard: React.FC = () => {
const { controledMihomoConfig, patchControledMihomoConfig } = useControledMihomoConfig(true)
const { sniffer } = controledMihomoConfig || {}
const { enable } = sniffer || {}
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
id: 'sniff'
})
const onChange = async (enable: boolean): Promise<void> => {
await patchControledMihomoConfig({ sniffer: { enable } })
@ -19,37 +24,48 @@ const SniffCard: React.FC = () => {
}
return (
<Card
className={`col-span-1 ${match ? 'bg-primary' : ''}`}
isPressable
onPress={() => navigate('/sniffer')}
<div
style={{
position: 'relative',
transform: CSS.Transform.toString(transform),
transition,
zIndex: isDragging ? 'calc(infinity)' : undefined
}}
className="col-span-1"
>
<CardBody className="pb-1 pt-0 px-0">
<div className="flex justify-between">
<Button
isIconOnly
className="bg-transparent pointer-events-none"
variant="flat"
color="default"
>
<RiScan2Fill
<Card
fullWidth
className={`col-span-1 ${match ? 'bg-primary' : ''}`}
isPressable
onPress={() => navigate('/sniffer')}
>
<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"
className={`${match ? 'text-white' : 'text-foreground'} text-[24px]`}
>
<RiScan2Fill
color="default"
className={`${match ? 'text-white' : 'text-foreground'} text-[24px]`}
/>
</Button>
<BorderSwitch
isShowBorder={match && enable}
isSelected={enable}
onValueChange={onChange}
/>
</Button>
<BorderSwitch
isShowBorder={match && enable}
isSelected={enable}
onValueChange={onChange}
/>
</div>
</CardBody>
<CardFooter className="pt-1">
<h3 className={`text-md font-bold ${match ? 'text-white' : 'text-foreground'}`}>
</h3>
</CardFooter>
</Card>
</div>
</CardBody>
<CardFooter className="pt-1">
<h3 className={`text-md font-bold ${match ? 'text-white' : 'text-foreground'}`}>
</h3>
</CardFooter>
</Card>
</div>
)
}

View File

@ -5,6 +5,8 @@ import { useAppConfig } from '@renderer/hooks/use-app-config'
import { triggerSysProxy } from '@renderer/utils/ipc'
import { AiOutlineGlobal } from 'react-icons/ai'
import React from 'react'
import { useSortable } from '@dnd-kit/sortable'
import { CSS } from '@dnd-kit/utilities'
const SysproxySwitcher: React.FC = () => {
const navigate = useNavigate()
@ -13,7 +15,9 @@ const SysproxySwitcher: React.FC = () => {
const { appConfig, patchAppConfig } = useAppConfig(true)
const { sysProxy } = appConfig || {}
const { enable } = sysProxy || {}
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
id: 'sysproxy'
})
const onChange = async (enable: boolean): Promise<void> => {
try {
await triggerSysProxy(enable)
@ -24,36 +28,47 @@ const SysproxySwitcher: React.FC = () => {
}
return (
<Card
className={`col-span-1 ${match ? 'bg-primary' : ''}`}
isPressable
onPress={() => navigate('/sysproxy')}
<div
style={{
position: 'relative',
transform: CSS.Transform.toString(transform),
transition,
zIndex: isDragging ? 'calc(infinity)' : undefined
}}
className="col-span-1 "
>
<CardBody className="pb-1 pt-0 px-0">
<div className="flex justify-between">
<Button
isIconOnly
className="bg-transparent pointer-events-none"
variant="flat"
color="default"
>
<AiOutlineGlobal
className={`${match ? 'text-white' : 'text-foreground'} text-[24px] font-bold`}
<Card
fullWidth
className={`${match ? 'bg-primary' : ''}`}
isPressable
onPress={() => navigate('/sysproxy')}
>
<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"
>
<AiOutlineGlobal
className={`${match ? 'text-white' : 'text-foreground'} text-[24px] font-bold`}
/>
</Button>
<BorderSwitch
isShowBorder={match && enable}
isSelected={enable}
onValueChange={onChange}
/>
</Button>
<BorderSwitch
isShowBorder={match && enable}
isSelected={enable}
onValueChange={onChange}
/>
</div>
</CardBody>
<CardFooter className="pt-1">
<h3 className={`text-md font-bold ${match ? 'text-white' : 'text-foreground'}`}>
</h3>
</CardFooter>
</Card>
</div>
</CardBody>
<CardFooter className="pt-1">
<h3 className={`text-md font-bold ${match ? 'text-white' : 'text-foreground'}`}>
</h3>
</CardFooter>
</Card>
</div>
)
}

View File

@ -4,6 +4,8 @@ import BorderSwitch from '@renderer/components/base/border-swtich'
import { TbDeviceIpadHorizontalBolt } from 'react-icons/tb'
import { useLocation, useNavigate } from 'react-router-dom'
import { encryptString, patchMihomoConfig, isEncryptionAvailable } from '@renderer/utils/ipc'
import { useSortable } from '@dnd-kit/sortable'
import { CSS } from '@dnd-kit/utilities'
import { platform } from '@renderer/utils/init'
import React, { useState } from 'react'
import { useAppConfig } from '@renderer/hooks/use-app-config'
@ -12,12 +14,15 @@ import BasePasswordModal from '../base/base-password-modal'
const TunSwitcher: React.FC = () => {
const navigate = useNavigate()
const location = useLocation()
const match = location.pathname.includes('/tun')
const match = location.pathname.includes('/tun') || false
const [openPasswordModal, setOpenPasswordModal] = useState(false)
const { appConfig, patchAppConfig } = useAppConfig()
const { controledMihomoConfig, patchControledMihomoConfig } = useControledMihomoConfig(true)
const { tun } = controledMihomoConfig || {}
const { enable } = tun || {}
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
id: 'tun'
})
const onChange = async (enable: boolean): Promise<void> => {
if (enable && platform !== 'win32') {
@ -40,7 +45,15 @@ const TunSwitcher: React.FC = () => {
}
return (
<>
<div
style={{
position: 'relative',
transform: CSS.Transform.toString(transform),
transition,
zIndex: isDragging ? 'calc(infinity)' : undefined
}}
className="col-span-1"
>
{openPasswordModal && (
<BasePasswordModal
onCancel={() => setOpenPasswordModal(false)}
@ -51,13 +64,15 @@ const TunSwitcher: React.FC = () => {
}}
/>
)}
<Card
className={`col-span-1 ${match ? 'bg-primary' : ''}`}
fullWidth
className={`${match ? 'bg-primary' : ''}`}
isPressable
onPress={() => navigate('/tun')}
>
<CardBody className="pb-1 pt-0 px-0">
<div className="flex justify-between">
<div ref={setNodeRef} {...attributes} {...listeners} className="flex justify-between">
<Button
isIconOnly
className="bg-transparent pointer-events-none"
@ -81,7 +96,7 @@ const TunSwitcher: React.FC = () => {
</h3>
</CardFooter>
</Card>
</>
</div>
)
}

View File

@ -194,6 +194,7 @@ interface IAppConfig {
core: 'mihomo' | 'mihomo-alpha'
proxyDisplayMode: 'simple' | 'full'
proxyDisplayOrder: 'default' | 'delay' | 'name'
siderOrder: string[]
appTheme: AppTheme
autoCheckUpdate: boolean
silentStart: boolean