sider design

This commit is contained in:
pompurin404 2024-07-30 16:12:45 +08:00
parent 1d2abbd7d5
commit bad31c2398
No known key found for this signature in database
20 changed files with 286 additions and 11 deletions

View File

@ -1,7 +1,7 @@
{
"name": "mihomo-party",
"version": "1.0.0",
"description": "An Electron application with React and TypeScript",
"description": "Mihomo Party",
"main": "./out/main/index.js",
"author": "mihomo-party",
"homepage": "https://mihomo.party",
@ -26,7 +26,9 @@
"@nextui-org/react": "^2.4.6",
"electron-updater": "^6.2.1",
"framer-motion": "^11.3.19",
"next-themes": "^0.3.0"
"next-themes": "^0.3.0",
"react-icons": "^5.2.1",
"react-router-dom": "^6.25.1"
},
"devDependencies": {
"@electron-toolkit/eslint-config-prettier": "^2.0.0",
@ -36,17 +38,17 @@
"@types/react": "^18.3.3",
"@types/react-dom": "^18.3.0",
"@vitejs/plugin-react": "^4.3.1",
"autoprefixer": "^10.4.19",
"electron": "^31.3.1",
"electron-builder": "^25.0.2",
"electron-vite": "^2.3.0",
"eslint": "^8.57.0",
"eslint-plugin-react": "^7.35.0",
"autoprefixer": "^10.4.19",
"postcss": "^8.4.40",
"tailwindcss": "^3.4.7",
"prettier": "^3.3.3",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"tailwindcss": "^3.4.7",
"typescript": "^5.5.4",
"vite": "^5.3.5"
}

View File

@ -26,6 +26,12 @@ importers:
next-themes:
specifier: ^0.3.0
version: 0.3.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
react-icons:
specifier: ^5.2.1
version: 5.2.1(react@18.3.1)
react-router-dom:
specifier: ^6.25.1
version: 6.25.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
devDependencies:
'@electron-toolkit/eslint-config-prettier':
specifier: ^2.0.0
@ -1556,6 +1562,10 @@ packages:
peerDependencies:
react: ^16.8.0 || ^17.0.0-rc.1 || ^18.0.0
'@remix-run/router@1.18.0':
resolution: {integrity: sha512-L3jkqmqoSVBVKHfpGZmLrex0lxR5SucGA0sUfFzGctehw+S/ggL9L/0NnC5mw6P8HUWpFZ3nQw3cRApjjWx9Sw==}
engines: {node: '>=14.0.0'}
'@rollup/rollup-android-arm-eabi@4.19.1':
resolution: {integrity: sha512-XzqSg714++M+FXhHfXpS1tDnNZNpgxxuGZWlRG/jSj+VEPmZ0yg6jV4E0AL3uyBKxO8mO3xtOsP5mQ+XLfrlww==}
cpu: [arm]
@ -3467,6 +3477,11 @@ packages:
peerDependencies:
react: ^18.3.1
react-icons@5.2.1:
resolution: {integrity: sha512-zdbW5GstTzXaVKvGSyTaBalt7HSfuK5ovrzlpyiWHAFXndXTdd/1hdDHI4xBM1Mn7YriT6aqESucFl9kEXzrdw==}
peerDependencies:
react: '*'
react-is@16.13.1:
resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==}
@ -3494,6 +3509,19 @@ packages:
'@types/react':
optional: true
react-router-dom@6.25.1:
resolution: {integrity: sha512-0tUDpbFvk35iv+N89dWNrJp+afLgd+y4VtorJZuOCXK0kkCWjEvb3vTJM++SYvMEpbVwXKf3FjeVveVEb6JpDQ==}
engines: {node: '>=14.0.0'}
peerDependencies:
react: '>=16.8'
react-dom: '>=16.8'
react-router@6.25.1:
resolution: {integrity: sha512-u8ELFr5Z6g02nUtpPAggP73Jigj1mRePSwhS/2nkTrlPU5yEkH1vYzWNyvSnSzeeE2DNqWdH+P8OhIh9wuXhTw==}
engines: {node: '>=14.0.0'}
peerDependencies:
react: '>=16.8'
react-style-singleton@2.2.1:
resolution: {integrity: sha512-ZWj0fHEMyWkHzKYUr2Bs/4zU6XLmq9HsgBURm7g5pAVfyn49DgUiNgY2d4lXRlYSiCif9YBGpQleewkcqddc7g==}
engines: {node: '>=10'}
@ -6471,6 +6499,8 @@ snapshots:
'@react-types/shared': 3.24.1(react@18.3.1)
react: 18.3.1
'@remix-run/router@1.18.0': {}
'@rollup/rollup-android-arm-eabi@4.19.1':
optional: true
@ -8662,6 +8692,10 @@ snapshots:
react: 18.3.1
scheduler: 0.23.2
react-icons@5.2.1(react@18.3.1):
dependencies:
react: 18.3.1
react-is@16.13.1: {}
react-refresh@0.14.2: {}
@ -8685,6 +8719,18 @@ snapshots:
optionalDependencies:
'@types/react': 18.3.3
react-router-dom@6.25.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1):
dependencies:
'@remix-run/router': 1.18.0
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
react-router: 6.25.1(react@18.3.1)
react-router@6.25.1(react@18.3.1):
dependencies:
'@remix-run/router': 1.18.0
react: 18.3.1
react-style-singleton@2.2.1(@types/react@18.3.3)(react@18.3.1):
dependencies:
get-nonce: 1.0.1

View File

@ -6,8 +6,10 @@ import icon from '../../resources/icon.png?asset'
function createWindow(): void {
// Create the browser window.
const mainWindow = new BrowserWindow({
width: 900,
height: 670,
minWidth: 800,
minHeight: 600,
width: 800,
height: 600,
show: false,
autoHideMenuBar: true,
...(process.platform === 'linux' ? { icon } : {}),

View File

@ -2,7 +2,7 @@
<html>
<head>
<meta charset="UTF-8" />
<title>Electron</title>
<title>Mihomo Party</title>
<!-- https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP -->
<meta
http-equiv="Content-Security-Policy"

View File

@ -1,10 +1,23 @@
import { Switch } from '@nextui-org/switch'
import { useTheme } from 'next-themes'
import { useEffect } from 'react'
import { useRoutes } from 'react-router-dom'
import OutboundModeSwitcher from '@renderer/components/sider/outbound-mode-switcher'
import SysproxySwitcher from '@renderer/components/sider/sysproxy-switcher'
import TunSwitcher from '@renderer/components/sider/tun-switcher'
import { Button } from '@nextui-org/react'
import { IoHome, IoSettings } from 'react-icons/io5'
import { IoWifi } from 'react-icons/io5'
import { IoGitNetwork } from 'react-icons/io5'
import { IoLogoGithub } from 'react-icons/io5'
import routes from '@renderer/routes'
import RouteItem from './components/sider/route-item'
import ProfileSwitcher from './components/sider/profile-switcher'
function App(): JSX.Element {
const { setTheme } = useTheme()
const page = useRoutes(routes)
useEffect(() => {
try {
if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
@ -24,7 +37,38 @@ function App(): JSX.Element {
}
}, [])
return <Switch />
return (
<div className="w-full h-[100vh] flex">
<div className="side w-[250px] h-full p-2 border-r border-neutral-700">
<div className="flex justify-between h-[32px] mb-2">
<h3 className="select-none text-lg font-bold leading-[32px]"></h3>
<Button
size="sm"
isIconOnly
variant="light"
onPress={() => {
open('https://github.com/pompurin404/mihomo-party')
}}
startContent={<IoLogoGithub className="text-[20px]" />}
/>
</div>
<OutboundModeSwitcher />
<h3 className="select-none text-lg font-bold my-2"></h3>
<div className="flex justify-between">
<SysproxySwitcher />
<TunSwitcher />
</div>
<h3 className="select-none text-lg font-bold my-2"></h3>
<ProfileSwitcher />
<RouteItem title="概览" pathname="/overview" icon={IoHome} />
<RouteItem title="代理" pathname="/proxies" icon={IoWifi} />
<RouteItem title="规则" pathname="/rules" icon={IoGitNetwork} />
<RouteItem title="设置" pathname="/settings" icon={IoSettings} />
</div>
<div className="main w-[calc(100%-250px)] h-full overflow-y-auto">{page}</div>
</div>
)
}
export default App

View File

@ -0,0 +1,30 @@
import { Tabs, Tab } from '@nextui-org/react'
import { Key, useState } from 'react'
export default function OutboundModeSwitcher(): JSX.Element {
const [mode, setMode] = useState<OutboundMode>('rule')
return (
<Tabs
fullWidth
color="primary"
selectedKey={mode}
onSelectionChange={(key: Key) => setMode(key as OutboundMode)}
>
<Tab
className={`select-none ${mode === 'rule' ? 'font-bold' : ''}`}
key="rule"
title="规则"
/>
<Tab
className={`select-none ${mode === 'global' ? 'font-bold' : ''}`}
key="global"
title="全局"
/>
<Tab
className={`select-none ${mode === 'direct' ? 'font-bold' : ''}`}
key="direct"
title="直连"
/>
</Tabs>
)
}

View File

@ -0,0 +1,29 @@
import { Button, Card, CardBody, CardFooter, Slider } from '@nextui-org/react'
import { IoMdRefresh } from 'react-icons/io'
import { useLocation, useNavigate } from 'react-router-dom'
export default function ProfileSwitcher(): JSX.Element {
const navigate = useNavigate()
const location = useLocation()
return (
<Card
fullWidth
className={`mb-2 ${location.pathname.includes('/profiles') ? 'bg-primary' : ''}`}
isPressable
onPress={() => navigate('/profiles')}
>
<CardBody>
<div className="flex justify-between h-[32px]">
<h3 className="select-none text-md font-bold leading-[32px]"></h3>
<Button isIconOnly size="sm" variant="light" color="default">
<IoMdRefresh color="default" className="text-[20px]" />
</Button>
</div>
</CardBody>
<CardFooter className="pt-1">
<Slider className="pointer-events-none" color="foreground" value={20} hideThumb />
</CardFooter>
</Card>
)
}

View File

@ -0,0 +1,28 @@
import { Button } from '@nextui-org/react'
import { IconType } from 'react-icons'
import { useLocation, useNavigate } from 'react-router-dom'
interface Props {
title: string
pathname: string
icon: IconType
}
export default function RouteItem(props: Props): JSX.Element {
const { pathname, icon: Icon, title } = props
const navigate = useNavigate()
const location = useLocation()
return (
<Button
fullWidth
color={location.pathname.includes(pathname) ? 'primary' : 'default'}
variant={location.pathname.includes(pathname) ? 'solid' : 'light'}
className="text-md mb-2"
startContent={<Icon className="text-[20px]" />}
onPress={() => navigate(pathname)}
>
<div className="w-full">{title}</div>
</Button>
)
}

View File

@ -0,0 +1,20 @@
import { Button, Card, CardBody, CardFooter, Switch } from '@nextui-org/react'
import { IoSettings } from 'react-icons/io5'
export default function SysproxySwitcher(): JSX.Element {
return (
<Card className="w-[48%]">
<CardBody className="pb-1 pt-0 px-0">
<div className="flex justify-between">
<Button isIconOnly className="bg-transparent" variant="flat" color="default">
<IoSettings color="default" className="text-lg" />
</Button>
<Switch />
</div>
</CardBody>
<CardFooter className="pt-1">
<h3 className="select-none text-md font-bold"></h3>
</CardFooter>
</Card>
)
}

View File

@ -0,0 +1,20 @@
import { Button, Card, CardBody, CardFooter, Switch } from '@nextui-org/react'
import { IoSettings } from 'react-icons/io5'
export default function SysproxySwitcher(): JSX.Element {
return (
<Card className="w-[48%]">
<CardBody className="pb-1 pt-0 px-0">
<div className="flex justify-between">
<Button isIconOnly className="bg-transparent" variant="flat" color="default">
<IoSettings color="default" className="text-lg" />
</Button>
<Switch />
</div>
</CardBody>
<CardFooter className="pt-1">
<h3 className="select-none text-md font-bold"></h3>
</CardFooter>
</Card>
)
}

View File

@ -1,5 +1,6 @@
import React from 'react'
import ReactDOM from 'react-dom/client'
import { BrowserRouter } from 'react-router-dom'
import { ThemeProvider as NextThemesProvider } from 'next-themes'
import { NextUIProvider } from '@nextui-org/react'
@ -10,7 +11,9 @@ ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
<React.StrictMode>
<NextUIProvider>
<NextThemesProvider attribute="class" defaultTheme="dark">
<App />
<BrowserRouter>
<App />
</BrowserRouter>
</NextThemesProvider>
</NextUIProvider>
</React.StrictMode>

View File

@ -0,0 +1,3 @@
export default function Overview(): JSX.Element {
return <div>Overview</div>
}

View File

@ -0,0 +1,3 @@
export default function Profiles(): JSX.Element {
return <div>Profiles</div>
}

View File

@ -0,0 +1,3 @@
export default function Proxies(): JSX.Element {
return <div>Proxies</div>
}

View File

@ -0,0 +1,3 @@
export default function Rules(): JSX.Element {
return <div>Rules</div>
}

View File

@ -0,0 +1,3 @@
export default function Settings(): JSX.Element {
return <div>Settings</div>
}

View File

@ -0,0 +1,35 @@
import { Navigate } from 'react-router-dom'
import Overview from '@renderer/pages/overview'
import Proxies from '@renderer/pages/proxies'
import Rules from '@renderer/pages/rules'
import Settings from '@renderer/pages/settings'
import Profiles from '@renderer/pages/profiles'
const routes = [
{
path: '/overview',
element: <Overview />
},
{
path: '/proxies',
element: <Proxies />
},
{
path: '/rules',
element: <Rules />
},
{
path: '/profiles',
element: <Profiles />
},
{
path: '/settings',
element: <Settings />
},
{
path: '/',
element: <Navigate to="/overview" />
}
]
export default routes

1
src/renderer/src/utils/types.d.ts vendored Normal file
View File

@ -0,0 +1 @@
type OutboundMode = 'rule' | 'global' | 'direct'

View File

@ -1,7 +1,7 @@
{
"extends": "@electron-toolkit/tsconfig/tsconfig.web.json",
"include": [
"src/renderer/src/env.d.ts",
"src/renderer/src/utils/env.d.ts",
"src/renderer/src/**/*",
"src/renderer/src/**/*.tsx",
"src/preload/*.d.ts"