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" "yaml": "^2.5.0"
}, },
"devDependencies": { "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-prettier": "^2.0.0",
"@electron-toolkit/eslint-config-ts": "^2.0.0", "@electron-toolkit/eslint-config-ts": "^2.0.0",
"@electron-toolkit/tsconfig": "^1.0.1", "@electron-toolkit/tsconfig": "^1.0.1",

View File

@ -27,6 +27,15 @@ importers:
specifier: ^2.5.0 specifier: ^2.5.0
version: 2.5.0 version: 2.5.0
devDependencies: 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': '@electron-toolkit/eslint-config-prettier':
specifier: ^2.0.0 specifier: ^2.0.0
version: 2.0.0(eslint@8.57.0)(prettier@3.3.3) 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==} resolution: {integrity: sha512-0cp4PsWQ/9avqTVMCtZ+GirikIA36ikvjtHweU4/j8yLtgObI0+JUPhYFScgwlteveGB1rt3Cm8UhN04XayDig==}
engines: {node: '>= 8.9.0'} 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': '@electron-toolkit/eslint-config-prettier@2.0.0':
resolution: {integrity: sha512-L+uG1FvJcAZkPZpSi6B1pmdpyJFyOxWDTjr1Vs47vSryxv/EX1Ch6o4HVsachlDq3fMEkDgojuP2F3ZvVZMoLw==} resolution: {integrity: sha512-L+uG1FvJcAZkPZpSi6B1pmdpyJFyOxWDTjr1Vs47vSryxv/EX1Ch6o4HVsachlDq3fMEkDgojuP2F3ZvVZMoLw==}
peerDependencies: peerDependencies:
@ -4545,6 +4576,31 @@ snapshots:
ajv: 6.12.6 ajv: 6.12.6
ajv-keywords: 3.5.2(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)': '@electron-toolkit/eslint-config-prettier@2.0.0(eslint@8.57.0)(prettier@3.3.3)':
dependencies: dependencies:
eslint: 8.57.0 eslint: 8.57.0

View File

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

View File

@ -1,5 +1,5 @@
import { useTheme } from 'next-themes' import { useTheme } from 'next-themes'
import { useEffect } from 'react' import { useEffect, useState } from 'react'
import { useLocation, useNavigate, useRoutes } from 'react-router-dom' import { useLocation, useNavigate, useRoutes } from 'react-router-dom'
import OutboundModeSwitcher from '@renderer/components/sider/outbound-mode-switcher' import OutboundModeSwitcher from '@renderer/components/sider/outbound-mode-switcher'
import SysproxySwitcher from '@renderer/components/sider/sysproxy-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 { Button, Divider } from '@nextui-org/react'
import { IoSettings } from 'react-icons/io5' import { IoSettings } from 'react-icons/io5'
import routes from '@renderer/routes' 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 ProfileCard from '@renderer/components/sider/profile-card'
import ProxyCard from '@renderer/components/sider/proxy-card' import ProxyCard from '@renderer/components/sider/proxy-card'
import RuleCard from '@renderer/components/sider/rule-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' import { useAppConfig } from './hooks/use-app-config'
const App: React.FC = () => { const App: React.FC = () => {
const { appConfig } = useAppConfig() const { appConfig, patchAppConfig } = useAppConfig()
const { appTheme = 'system' } = appConfig || {} 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 { setTheme } = useTheme()
const navigate = useNavigate() const navigate = useNavigate()
const location = useLocation() const location = useLocation()
@ -32,6 +64,36 @@ const App: React.FC = () => {
setTheme(appTheme) setTheme(appTheme)
}, [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 ( return (
<div className="w-full h-[100vh] flex"> <div className="w-full h-[100vh] flex">
<div className="side w-[250px] h-full overflow-y-auto no-scrollbar"> <div className="side w-[250px] h-full overflow-y-auto no-scrollbar">
@ -51,44 +113,22 @@ const App: React.FC = () => {
/> />
</div> </div>
</div> </div>
<div className="grid grid-cols-2 gap-2 mx-2"> <div className="mt-2 mx-2">
<OutboundModeSwitcher /> <OutboundModeSwitcher />
<SysproxySwitcher />
<TunSwitcher />
<ProfileCard />
<ProxyCard />
<MihomoCoreCard />
<ConnCard />
<DNSCard />
<SniffCard />
<LogCard />
<RuleCard />
<ResourceCard />
<OverrideCard />
</div> </div>
{/* <div className="flex justify-between mx-2 mb-2"> <DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={onDragEnd}>
<SysproxySwitcher /> <div className="grid grid-cols-2 gap-2 m-2">
<TunSwitcher /> <SortableContext
items={order.map((x) => {
return x
})}
>
{order.map((key: string) => {
return componentMap[key]
})}
</SortableContext>
</div> </div>
<div className="mx-2"> </DndContext>
<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> */}
</div> </div>
<Divider orientation="vertical" /> <Divider orientation="vertical" />
<div className="main w-[calc(100%-251px)] h-full overflow-y-auto">{page}</div> <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 { useLocation, useNavigate } from 'react-router-dom'
import { calcTraffic } from '@renderer/utils/calc' import { calcTraffic } from '@renderer/utils/calc'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { useSortable } from '@dnd-kit/sortable'
import { CSS } from '@dnd-kit/utilities'
import { IoLink } from 'react-icons/io5' import { IoLink } from 'react-icons/io5'
const ConnCard: React.FC = () => { const ConnCard: React.FC = () => {
@ -12,6 +14,9 @@ const ConnCard: React.FC = () => {
const [upload, setUpload] = useState(0) const [upload, setUpload] = useState(0)
const [download, setDownload] = useState(0) const [download, setDownload] = useState(0)
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
id: 'connection'
})
useEffect(() => { useEffect(() => {
window.electron.ipcRenderer.on('mihomoTraffic', (_e, info: IMihomoTrafficInfo) => { window.electron.ipcRenderer.on('mihomoTraffic', (_e, info: IMihomoTrafficInfo) => {
@ -24,14 +29,23 @@ const ConnCard: React.FC = () => {
}, []) }, [])
return ( return (
<div
style={{
position: 'relative',
transform: CSS.Transform.toString(transform),
transition,
zIndex: isDragging ? 'calc(infinity)' : undefined
}}
className="col-span-2"
>
<Card <Card
fullWidth fullWidth
className={`col-span-2 ${match ? 'bg-primary' : ''}`} className={`${match ? 'bg-primary' : ''}`}
isPressable isPressable
onPress={() => navigate('/connections')} onPress={() => navigate('/connections')}
> >
<CardBody className="pb-1 pt-0 px-0"> <CardBody className="pb-0 pt-0 px-0">
<div className="flex justify-between"> <div ref={setNodeRef} {...attributes} {...listeners} className="flex justify-between">
<Button <Button
isIconOnly isIconOnly
className="bg-transparent pointer-events-none" className="bg-transparent pointer-events-none"
@ -59,6 +73,7 @@ const ConnCard: React.FC = () => {
<h3 className={`text-md font-bold ${match ? 'text-white' : 'text-foreground'}`}></h3> <h3 className={`text-md font-bold ${match ? 'text-white' : 'text-foreground'}`}></h3>
</CardFooter> </CardFooter>
</Card> </Card>
</div>
) )
} }

View File

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

View File

@ -1,19 +1,33 @@
import { Button, Card, CardBody, CardFooter } from '@nextui-org/react' import { Button, Card, CardBody, CardFooter } from '@nextui-org/react'
import { IoJournal } from 'react-icons/io5' import { IoJournal } 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 { CSS } from '@dnd-kit/utilities'
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') const match = location.pathname.includes('/logs')
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
id: 'log'
})
return ( return (
<div
style={{
position: 'relative',
transform: CSS.Transform.toString(transform),
transition,
zIndex: isDragging ? 'calc(infinity)' : undefined
}}
className="col-span-1"
>
<Card <Card
fullWidth
className={`col-span-1 ${match ? 'bg-primary' : ''}`} className={`col-span-1 ${match ? 'bg-primary' : ''}`}
isPressable isPressable
onPress={() => navigate('/logs')} onPress={() => navigate('/logs')}
> >
<CardBody className="pb-1 pt-0 px-0"> <CardBody className="pb-1 pt-0 px-0">
<div className="flex justify-between"> <div ref={setNodeRef} {...attributes} {...listeners} className="flex justify-between">
<Button <Button
isIconOnly isIconOnly
className="bg-transparent pointer-events-none" className="bg-transparent pointer-events-none"
@ -31,6 +45,7 @@ const LogCard: React.FC = () => {
<h3 className={`text-md font-bold ${match ? 'text-white' : 'text-foreground'}`}></h3> <h3 className={`text-md font-bold ${match ? 'text-white' : 'text-foreground'}`}></h3>
</CardFooter> </CardFooter>
</Card> </Card>
</div>
) )
} }

View File

@ -3,6 +3,8 @@ import { calcTraffic } from '@renderer/utils/calc'
import { mihomoVersion, restartCore } from '@renderer/utils/ipc' import { mihomoVersion, restartCore } from '@renderer/utils/ipc'
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react'
import { IoMdRefresh } from 'react-icons/io' 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 { useLocation, useNavigate } from 'react-router-dom'
import useSWR from 'swr' import useSWR from 'swr'
@ -11,6 +13,9 @@ const MihomoCoreCard: React.FC = () => {
const navigate = useNavigate() const navigate = useNavigate()
const location = useLocation() const location = useLocation()
const match = location.pathname.includes('/mihomo') const match = location.pathname.includes('/mihomo')
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
id: 'mihomo'
})
const [mem, setMem] = useState(0) const [mem, setMem] = useState(0)
@ -31,14 +36,28 @@ const MihomoCoreCard: React.FC = () => {
}, []) }, [])
return ( return (
<div
style={{
position: 'relative',
transform: CSS.Transform.toString(transform),
transition,
zIndex: isDragging ? 'calc(infinity)' : undefined
}}
className="col-span-2"
>
<Card <Card
fullWidth fullWidth
isPressable isPressable
onPress={() => navigate('/mihomo')} onPress={() => navigate('/mihomo')}
className={`col-span-2 ${match ? 'bg-primary' : ''}`} className={`${match ? 'bg-primary' : ''}`}
> >
<CardBody> <CardBody>
<div className="flex justify-between h-[32px]"> <div
ref={setNodeRef}
{...attributes}
{...listeners}
className="flex justify-between h-[32px]"
>
<h3 <h3
className={`text-md font-bold leading-[32px] ${match ? 'text-white' : 'text-foreground'} `} className={`text-md font-bold leading-[32px] ${match ? 'text-white' : 'text-foreground'} `}
> >
@ -71,6 +90,7 @@ const MihomoCoreCard: React.FC = () => {
</div> </div>
</CardFooter> </CardFooter>
</Card> </Card>
</div>
) )
} }

View File

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

View File

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

View File

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

View File

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

View File

@ -2,20 +2,33 @@ import { Button, Card, CardBody, CardFooter } from '@nextui-org/react'
import React from 'react' import React from 'react'
import { FaLayerGroup } from 'react-icons/fa6' import { FaLayerGroup } from 'react-icons/fa6'
import { useLocation, useNavigate } from 'react-router-dom' import { useLocation, useNavigate } from 'react-router-dom'
import { useSortable } from '@dnd-kit/sortable'
import { CSS } from '@dnd-kit/utilities'
const ResourceCard: React.FC = () => { const ResourceCard: React.FC = () => {
const navigate = useNavigate() const navigate = useNavigate()
const location = useLocation() const location = useLocation()
const match = location.pathname.includes('/resources') const match = location.pathname.includes('/resources')
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
id: 'resource'
})
return ( return (
<div
style={{
position: 'relative',
transform: CSS.Transform.toString(transform),
transition,
zIndex: isDragging ? 'calc(infinity)' : undefined
}}
className="col-span-1"
>
<Card <Card
fullWidth
className={`col-span-1 ${match ? 'bg-primary' : ''}`} className={`col-span-1 ${match ? 'bg-primary' : ''}`}
isPressable isPressable
onPress={() => navigate('/resources')} onPress={() => navigate('/resources')}
> >
<CardBody className="pb-1 pt-0 px-0"> <CardBody className="pb-1 pt-0 px-0">
<div className="flex justify-between"> <div ref={setNodeRef} {...attributes} {...listeners} className="flex justify-between">
<Button <Button
isIconOnly isIconOnly
className="bg-transparent pointer-events-none" className="bg-transparent pointer-events-none"
@ -35,6 +48,7 @@ const ResourceCard: React.FC = () => {
</h3> </h3>
</CardFooter> </CardFooter>
</Card> </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 { mihomoRules } from '@renderer/utils/ipc'
import { MdOutlineAltRoute } from 'react-icons/md' import { MdOutlineAltRoute } 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 { CSS } from '@dnd-kit/utilities'
import useSWR from 'swr' import useSWR from 'swr'
const RuleCard: React.FC = () => { const RuleCard: React.FC = () => {
@ -11,15 +13,28 @@ const RuleCard: React.FC = () => {
const { data: rules } = useSWR<IMihomoRulesInfo>('mihomoRules', mihomoRules, { const { data: rules } = useSWR<IMihomoRulesInfo>('mihomoRules', mihomoRules, {
refreshInterval: 5000 refreshInterval: 5000
}) })
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
id: 'rule'
})
return ( return (
<div
style={{
position: 'relative',
transform: CSS.Transform.toString(transform),
transition,
zIndex: isDragging ? 'calc(infinity)' : undefined
}}
className="col-span-1"
>
<Card <Card
fullWidth
className={`col-span-1 ${match ? 'bg-primary' : ''}`} className={`col-span-1 ${match ? 'bg-primary' : ''}`}
isPressable isPressable
onPress={() => navigate('/rules')} onPress={() => navigate('/rules')}
> >
<CardBody className="pb-1 pt-0 px-0"> <CardBody className="pb-1 pt-0 px-0">
<div className="flex justify-between"> <div ref={setNodeRef} {...attributes} {...listeners} className="flex justify-between">
<Button <Button
isIconOnly isIconOnly
className="bg-transparent pointer-events-none" className="bg-transparent pointer-events-none"
@ -55,6 +70,7 @@ const RuleCard: React.FC = () => {
<h3 className={`text-md font-bold ${match ? 'text-white' : 'text-foreground'}`}></h3> <h3 className={`text-md font-bold ${match ? 'text-white' : 'text-foreground'}`}></h3>
</CardFooter> </CardFooter>
</Card> </Card>
</div>
) )
} }

View File

@ -4,6 +4,8 @@ import { RiScan2Fill } from 'react-icons/ri'
import { useLocation, useNavigate } from 'react-router-dom' import { useLocation, useNavigate } from 'react-router-dom'
import { patchMihomoConfig } from '@renderer/utils/ipc' 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 { CSS } from '@dnd-kit/utilities'
const SniffCard: React.FC = () => { const SniffCard: React.FC = () => {
const navigate = useNavigate() const navigate = useNavigate()
@ -12,6 +14,9 @@ const SniffCard: React.FC = () => {
const { controledMihomoConfig, patchControledMihomoConfig } = useControledMihomoConfig(true) const { controledMihomoConfig, patchControledMihomoConfig } = useControledMihomoConfig(true)
const { sniffer } = controledMihomoConfig || {} const { sniffer } = controledMihomoConfig || {}
const { enable } = sniffer || {} const { enable } = sniffer || {}
const { attributes, listeners, setNodeRef, transform, transition, isDragging } = useSortable({
id: 'sniff'
})
const onChange = async (enable: boolean): Promise<void> => { const onChange = async (enable: boolean): Promise<void> => {
await patchControledMihomoConfig({ sniffer: { enable } }) await patchControledMihomoConfig({ sniffer: { enable } })
@ -19,13 +24,23 @@ const SniffCard: React.FC = () => {
} }
return ( return (
<div
style={{
position: 'relative',
transform: CSS.Transform.toString(transform),
transition,
zIndex: isDragging ? 'calc(infinity)' : undefined
}}
className="col-span-1"
>
<Card <Card
fullWidth
className={`col-span-1 ${match ? 'bg-primary' : ''}`} className={`col-span-1 ${match ? 'bg-primary' : ''}`}
isPressable isPressable
onPress={() => navigate('/sniffer')} onPress={() => navigate('/sniffer')}
> >
<CardBody className="pb-1 pt-0 px-0"> <CardBody className="pb-1 pt-0 px-0">
<div className="flex justify-between"> <div ref={setNodeRef} {...attributes} {...listeners} className="flex justify-between">
<Button <Button
isIconOnly isIconOnly
className="bg-transparent pointer-events-none" className="bg-transparent pointer-events-none"
@ -50,6 +65,7 @@ const SniffCard: React.FC = () => {
</h3> </h3>
</CardFooter> </CardFooter>
</Card> </Card>
</div>
) )
} }

View File

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

View File

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

View File

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