ui: optimize menu bar ui

This commit is contained in:
wj-xiao
2025-12-26 10:22:59 +08:00
parent 9a93ab24f5
commit 14614e5e35
34 changed files with 397 additions and 153 deletions

View File

@@ -1,3 +1,3 @@
VITE_SERVER_IP=192.168.0.65
VITE_SERVER_IP=192.168.3.69
VITE_SERVER_PORT=80
VITE_WITH_CREDENTIALS=false

View File

@@ -12,6 +12,7 @@
},
"dependencies": {
"@ant-design/icons": "^5.6.1",
"@radix-ui/react-scroll-area": "^1.2.10",
"@xterm/addon-attach": "^0.11.0",
"@xterm/addon-fit": "^0.10.0",
"@xterm/xterm": "^5.5.0",

179
web/pnpm-lock.yaml generated
View File

@@ -11,6 +11,9 @@ importers:
'@ant-design/icons':
specifier: ^5.6.1
version: 5.6.1(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-scroll-area':
specifier: ^1.2.10
version: 1.2.10(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@xterm/addon-attach':
specifier: ^0.11.0
version: 0.11.0(@xterm/xterm@5.5.0)
@@ -640,9 +643,15 @@ packages:
'@open-draft/until@2.1.0':
resolution: {integrity: sha512-U69T3ItWHvLwGg5eJ0n3I62nWuE6ilHlmz7zM0npLBRvPRd7e6NYmg54vvRtP5mZG7kZqZCFVdsTWo7BPtBujg==}
'@radix-ui/number@1.1.1':
resolution: {integrity: sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==}
'@radix-ui/primitive@1.1.0':
resolution: {integrity: sha512-4Z8dn6Upk0qk4P74xBhZ6Hd/w0mPEzOOLxy4xiPXOXqjF7jZS0VAKk7/x/H6FyY2zCkYJqePf1G5KmkmNJ4RBA==}
'@radix-ui/primitive@1.1.3':
resolution: {integrity: sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==}
'@radix-ui/react-compose-refs@1.1.0':
resolution: {integrity: sha512-b4inOtiaOnYf9KWyO3jAeeCG6FeyfY6ldiEPanbUjWd+xIk5wZeHa8yVwmrJ2vderhu/BQvzCrJI0lHd+wIiqw==}
peerDependencies:
@@ -652,6 +661,15 @@ packages:
'@types/react':
optional: true
'@radix-ui/react-compose-refs@1.1.2':
resolution: {integrity: sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==}
peerDependencies:
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@radix-ui/react-context@1.1.1':
resolution: {integrity: sha512-UASk9zi+crv9WteK/NU4PLvOoL3OuE6BWVKNF6hPRBtYBDXQ2u5iu3O59zUlJiTVvkyuycnqrztsHVJwcK9K+Q==}
peerDependencies:
@@ -661,6 +679,15 @@ packages:
'@types/react':
optional: true
'@radix-ui/react-context@1.1.2':
resolution: {integrity: sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==}
peerDependencies:
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@radix-ui/react-dialog@1.1.2':
resolution: {integrity: sha512-Yj4dZtqa2o+kG61fzB0H2qUvmwBA2oyQroGLyNtBj1beo1khoQ3q1a2AO8rrQYjd8256CO9+N8L9tvsS+bnIyA==}
peerDependencies:
@@ -674,6 +701,15 @@ packages:
'@types/react-dom':
optional: true
'@radix-ui/react-direction@1.1.1':
resolution: {integrity: sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==}
peerDependencies:
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@radix-ui/react-dismissable-layer@1.1.1':
resolution: {integrity: sha512-QSxg29lfr/xcev6kSz7MAlmDnzbP1eI/Dwn3Tp1ip0KT5CUELsxkekFEMVBEoykI3oV39hKT4TKZzBNMbcTZYQ==}
peerDependencies:
@@ -744,6 +780,19 @@ packages:
'@types/react-dom':
optional: true
'@radix-ui/react-presence@1.1.5':
resolution: {integrity: sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
'@radix-ui/react-primitive@2.0.0':
resolution: {integrity: sha512-ZSpFm0/uHa8zTvKBDjLFWLo8dkr4MBsiDLz0g3gMUwqgLHz9rTaRRGYDgvZPtBJgYCBKXkS9fzmoySgr8CO6Cw==}
peerDependencies:
@@ -757,6 +806,32 @@ packages:
'@types/react-dom':
optional: true
'@radix-ui/react-primitive@2.1.3':
resolution: {integrity: sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
'@radix-ui/react-scroll-area@1.2.10':
resolution: {integrity: sha512-tAXIa1g3sM5CGpVT0uIbUx/U3Gs5N8T52IICuCtObaos1S8fzsrPXG5WObkQN3S6NVl6wKgPhAIiBGbWnvc97A==}
peerDependencies:
'@types/react': '*'
'@types/react-dom': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@types/react-dom':
optional: true
'@radix-ui/react-slot@1.1.0':
resolution: {integrity: sha512-FUCf5XMfmW4dtYl69pdS4DbxKy8nj4M7SafBgPllysxmdachynNflAdp/gCsnYWNDnge6tI9onzMp5ARYc1KNw==}
peerDependencies:
@@ -766,6 +841,15 @@ packages:
'@types/react':
optional: true
'@radix-ui/react-slot@1.2.3':
resolution: {integrity: sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==}
peerDependencies:
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@radix-ui/react-use-callback-ref@1.1.0':
resolution: {integrity: sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw==}
peerDependencies:
@@ -775,6 +859,15 @@ packages:
'@types/react':
optional: true
'@radix-ui/react-use-callback-ref@1.1.1':
resolution: {integrity: sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==}
peerDependencies:
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@radix-ui/react-use-controllable-state@1.1.0':
resolution: {integrity: sha512-MtfMVJiSr2NjzS0Aa90NPTnvTSg6C/JLCV7ma0W6+OMV78vd8OyRpID+Ng9LxzsPbLeuBnWBA1Nq30AtBIDChw==}
peerDependencies:
@@ -802,6 +895,15 @@ packages:
'@types/react':
optional: true
'@radix-ui/react-use-layout-effect@1.1.1':
resolution: {integrity: sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==}
peerDependencies:
'@types/react': '*'
react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc
peerDependenciesMeta:
'@types/react':
optional: true
'@rc-component/async-validator@5.0.4':
resolution: {integrity: sha512-qgGdcVIF604M9EqjNF0hbUTz42bz/RDtxWdWuU5EQe3hi7M8ob54B6B35rOsvX5eSvIHIzT9iH1R3n+hk3CGfg==}
engines: {node: '>=14.x'}
@@ -3551,20 +3653,36 @@ snapshots:
'@open-draft/until@2.1.0': {}
'@radix-ui/number@1.1.1': {}
'@radix-ui/primitive@1.1.0': {}
'@radix-ui/primitive@1.1.3': {}
'@radix-ui/react-compose-refs@1.1.0(@types/react@18.3.12)(react@18.3.1)':
dependencies:
react: 18.3.1
optionalDependencies:
'@types/react': 18.3.12
'@radix-ui/react-compose-refs@1.1.2(@types/react@18.3.12)(react@18.3.1)':
dependencies:
react: 18.3.1
optionalDependencies:
'@types/react': 18.3.12
'@radix-ui/react-context@1.1.1(@types/react@18.3.12)(react@18.3.1)':
dependencies:
react: 18.3.1
optionalDependencies:
'@types/react': 18.3.12
'@radix-ui/react-context@1.1.2(@types/react@18.3.12)(react@18.3.1)':
dependencies:
react: 18.3.1
optionalDependencies:
'@types/react': 18.3.12
'@radix-ui/react-dialog@1.1.2(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@radix-ui/primitive': 1.1.0
@@ -3587,6 +3705,12 @@ snapshots:
'@types/react': 18.3.12
'@types/react-dom': 18.3.1
'@radix-ui/react-direction@1.1.1(@types/react@18.3.12)(react@18.3.1)':
dependencies:
react: 18.3.1
optionalDependencies:
'@types/react': 18.3.12
'@radix-ui/react-dismissable-layer@1.1.1(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@radix-ui/primitive': 1.1.0
@@ -3644,6 +3768,16 @@ snapshots:
'@types/react': 18.3.12
'@types/react-dom': 18.3.1
'@radix-ui/react-presence@1.1.5(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.12)(react@18.3.1)
'@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.12)(react@18.3.1)
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
optionalDependencies:
'@types/react': 18.3.12
'@types/react-dom': 18.3.1
'@radix-ui/react-primitive@2.0.0(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@radix-ui/react-slot': 1.1.0(@types/react@18.3.12)(react@18.3.1)
@@ -3653,6 +3787,32 @@ snapshots:
'@types/react': 18.3.12
'@types/react-dom': 18.3.1
'@radix-ui/react-primitive@2.1.3(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@radix-ui/react-slot': 1.2.3(@types/react@18.3.12)(react@18.3.1)
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
optionalDependencies:
'@types/react': 18.3.12
'@types/react-dom': 18.3.1
'@radix-ui/react-scroll-area@1.2.10(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies:
'@radix-ui/number': 1.1.1
'@radix-ui/primitive': 1.1.3
'@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.12)(react@18.3.1)
'@radix-ui/react-context': 1.1.2(@types/react@18.3.12)(react@18.3.1)
'@radix-ui/react-direction': 1.1.1(@types/react@18.3.12)(react@18.3.1)
'@radix-ui/react-presence': 1.1.5(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-primitive': 2.1.3(@types/react-dom@18.3.1)(@types/react@18.3.12)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@radix-ui/react-use-callback-ref': 1.1.1(@types/react@18.3.12)(react@18.3.1)
'@radix-ui/react-use-layout-effect': 1.1.1(@types/react@18.3.12)(react@18.3.1)
react: 18.3.1
react-dom: 18.3.1(react@18.3.1)
optionalDependencies:
'@types/react': 18.3.12
'@types/react-dom': 18.3.1
'@radix-ui/react-slot@1.1.0(@types/react@18.3.12)(react@18.3.1)':
dependencies:
'@radix-ui/react-compose-refs': 1.1.0(@types/react@18.3.12)(react@18.3.1)
@@ -3660,12 +3820,25 @@ snapshots:
optionalDependencies:
'@types/react': 18.3.12
'@radix-ui/react-slot@1.2.3(@types/react@18.3.12)(react@18.3.1)':
dependencies:
'@radix-ui/react-compose-refs': 1.1.2(@types/react@18.3.12)(react@18.3.1)
react: 18.3.1
optionalDependencies:
'@types/react': 18.3.12
'@radix-ui/react-use-callback-ref@1.1.0(@types/react@18.3.12)(react@18.3.1)':
dependencies:
react: 18.3.1
optionalDependencies:
'@types/react': 18.3.12
'@radix-ui/react-use-callback-ref@1.1.1(@types/react@18.3.12)(react@18.3.1)':
dependencies:
react: 18.3.1
optionalDependencies:
'@types/react': 18.3.12
'@radix-ui/react-use-controllable-state@1.1.0(@types/react@18.3.12)(react@18.3.1)':
dependencies:
'@radix-ui/react-use-callback-ref': 1.1.0(@types/react@18.3.12)(react@18.3.1)
@@ -3686,6 +3859,12 @@ snapshots:
optionalDependencies:
'@types/react': 18.3.12
'@radix-ui/react-use-layout-effect@1.1.1(@types/react@18.3.12)(react@18.3.1)':
dependencies:
react: 18.3.1
optionalDependencies:
'@types/react': 18.3.12
'@rc-component/async-validator@5.0.4':
dependencies:
'@babel/runtime': 7.26.0

View File

@@ -0,0 +1,53 @@
import * as React from 'react';
import * as ScrollAreaPrimitive from '@radix-ui/react-scroll-area';
import clsx from 'clsx';
function ScrollArea({
className,
children,
...props
}: React.ComponentProps<typeof ScrollAreaPrimitive.Root>) {
return (
<ScrollAreaPrimitive.Root
data-slot="scroll-area"
className={clsx('relative', className)}
{...props}
>
<ScrollAreaPrimitive.Viewport
data-slot="scroll-area-viewport"
className="focus-visible:ring-ring/50 size-full rounded-[inherit] outline-none transition-[color,box-shadow] focus-visible:outline-1 focus-visible:ring-[3px]"
>
{children}
</ScrollAreaPrimitive.Viewport>
<ScrollBar />
<ScrollAreaPrimitive.Corner />
</ScrollAreaPrimitive.Root>
);
}
function ScrollBar({
className,
orientation = 'vertical',
...props
}: React.ComponentProps<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>) {
return (
<ScrollAreaPrimitive.ScrollAreaScrollbar
data-slot="scroll-area-scrollbar"
orientation={orientation}
className={clsx(
'flex touch-none select-none p-px transition-colors',
orientation === 'vertical' && 'h-full w-2 border-l border-l-transparent',
orientation === 'horizontal' && 'h-2 flex-col border-t border-t-transparent',
className
)}
{...props}
>
<ScrollAreaPrimitive.ScrollAreaThumb
data-slot="scroll-area-thumb"
className="bg-border relative flex-1 rounded-full bg-neutral-500/30"
/>
</ScrollAreaPrimitive.ScrollAreaScrollbar>
);
}
export { ScrollArea, ScrollBar };

View File

@@ -8,9 +8,10 @@ export const Fullscreen = () => {
const [isFullscreen, setIsFullscreen] = useState(false);
useEffect(() => {
function onFullscreenChange() {
const onFullscreenChange = () => {
setIsFullscreen(!!document.fullscreenElement);
}
};
onFullscreenChange();
document.addEventListener('fullscreenchange', onFullscreenChange);
@@ -24,8 +25,14 @@ export const Fullscreen = () => {
if (!document.fullscreenElement) {
const element = document.documentElement;
element.requestFullscreen();
// @ts-expect-error - https://developer.mozilla.org/en-US/docs/Web/API/Keyboard/lock
navigator.keyboard?.lock();
} else {
document.exitFullscreen();
// @ts-expect-error - https://developer.mozilla.org/en-US/docs/Web/API/Keyboard/unlock
navigator.keyboard?.unlock();
}
}

View File

@@ -48,7 +48,7 @@ export const Power = () => {
}
const icon = (
<div className={clsx('h-[18px] w-[18px]', isPowerOn ? 'text-green-600' : 'text-neutral-500')}>
<div className={clsx('h-[18px] w-[18px]', isPowerOn ? 'text-green-600' : 'text-neutral-300')}>
{isLoading ? (
<LoaderCircleIcon className="animate-spin" size={18} />
) : (

View File

@@ -71,10 +71,10 @@ export const Screen = () => {
const content = (
<div className="flex flex-col space-y-1">
<VideoMode />
<Scale />
<Resolution />
<Quality quality={quality} setQuality={setQuality} />
<Fps fps={fps} setFps={setFps} />
<Scale />
{videoMode !== 'mjpeg' && <Gop gop={gop} setGop={setGop} />}
{videoMode === 'mjpeg' && <FrameDetect />}
<Reset />

View File

@@ -1,49 +1,56 @@
import { ReactElement, useEffect } from 'react'
import { Popover, Slider } from 'antd'
import { useAtom } from 'jotai'
import { ScalingIcon } from 'lucide-react'
import { useTranslation } from 'react-i18next'
import { ReactElement, useEffect } from 'react';
import { Popover } from 'antd';
import { useAtom } from 'jotai';
import { CheckIcon, PercentIcon, ScalingIcon } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import { videoScaleAtom } from '@/jotai/screen.ts'
import * as storage from '@/lib/localstorage.ts'
import * as storage from '@/lib/localstorage.ts';
import { videoScaleAtom } from '@/jotai/screen.ts';
const ScaleList = [
{ label: '200', value: 2 },
{ label: '150', value: 1.5 },
{ label: '100', value: 1 },
{ label: '75', value: 0.75 },
{ label: '50', value: 0.5 }
];
export const Scale = (): ReactElement => {
const { t } = useTranslation()
const { t } = useTranslation();
const [videoScale, setVideoScale] = useAtom(videoScaleAtom)
const [videoScale, setVideoScale] = useAtom(videoScaleAtom);
useEffect(() => {
const scale = storage.getVideoScale()
const scale = storage.getVideoScale();
if (scale) {
setVideoScale(scale)
setVideoScale(scale);
}
}, [setVideoScale])
}, [setVideoScale]);
async function updateScale(scale: number): Promise<void> {
setVideoScale(scale)
storage.setVideoScale(scale)
async function update(scale: number): Promise<void> {
setVideoScale(scale);
storage.setVideoScale(scale);
}
const content = (
<div className="h-[150px] w-[60px] py-3">
<Slider
vertical
marks={{
0.5: <span>x0.5</span>,
1: <span>x1.0</span>,
1.5: <span>x1.5</span>,
2: <span>x2.0</span>
}}
range={false}
included={false}
min={0.5}
max={2}
step={0.1}
defaultValue={videoScale}
onChange={updateScale}
/>
</div>
)
<>
{ScaleList.map((scale) => (
<div
key={scale.value}
className="flex h-[30px] cursor-pointer select-none items-center rounded pl-1 pr-5 hover:bg-neutral-700/70"
onClick={() => update(scale.value)}
>
<div className="flex h-[14px] w-[20px] items-end text-blue-500">
{scale.value === videoScale && <CheckIcon size={14} />}
</div>
<div className="flex items-center space-x-0.5">
<span>{scale.label}</span>
<PercentIcon size={12} />
</div>
</div>
))}
</>
);
return (
<Popover content={content} placement="rightTop" arrow={false} align={{ offset: [13, 0] }}>
@@ -54,5 +61,5 @@ export const Scale = (): ReactElement => {
<span>{t('screen.scale')}</span>
</div>
</Popover>
)
}
);
};

View File

@@ -41,9 +41,9 @@ export const VideoMode = () => {
<>
{!isDirectSupported && (
<Tooltip
placement="right"
title={t('screen.videoDirectTips')}
overlayStyle={{ maxWidth: '270px' }}
placement="right"
styles={{ root: { maxWidth: '270px' } }}
>
<div className="flex cursor-not-allowed select-none items-center rounded py-1.5 pl-1 pr-5 text-neutral-500 hover:bg-neutral-700/70">
<div className="flex h-[14px] w-[20px] items-end text-blue-500"></div>

View File

@@ -31,9 +31,9 @@ export const Community = () => {
return (
<>
<div className="pb-5 text-neutral-400">{t('settings.about.community')}</div>
<div className="text-neutral-400">{t('settings.about.community')}</div>
<div className="my-3 flex space-x-3">
<div className="mt-5 flex flex-wrap gap-3">
{communities.map((community) => (
<a
key={community.name}

View File

@@ -65,8 +65,8 @@ export const Hostname = () => {
}
return (
<>
<div className="flex w-full items-center justify-between pt-4">
<div className="space-y-1">
<div className="flex w-full items-center justify-between">
<span>{t('settings.about.hostname')}</span>
{editState === 'editing' ? (
@@ -94,10 +94,10 @@ export const Hostname = () => {
</div>
{editState === 'edited' && (
<div className="flex w-full justify-end pt-1 text-xs text-green-500">
<div className="flex w-full justify-end text-xs text-green-500">
{t('settings.about.hostnameUpdated')}
</div>
)}
</>
</div>
);
};

View File

@@ -2,7 +2,6 @@ import { Divider } from 'antd';
import { useTranslation } from 'react-i18next';
import { Community } from './community.tsx';
import { Hostname } from './hostname.tsx';
import { Information } from './information.tsx';
export const About = () => {
@@ -10,12 +9,11 @@ export const About = () => {
return (
<>
<div className="text-base font-bold">{t('settings.about.title')}</div>
<Divider />
<div className="text-base">{t('settings.about.title')}</div>
<Divider className="opacity-50" />
<Information />
<Hostname />
<Divider />
<Divider className="opacity-50" />
<Community />
</>

View File

@@ -5,6 +5,8 @@ import { useTranslation } from 'react-i18next';
import * as api from '@/api/vm.ts';
import { Hostname } from './hostname.tsx';
type IP = {
name: string;
addr: string;
@@ -38,9 +40,9 @@ export const Information = () => {
return (
<>
<div className="pb-5 text-neutral-400">{t('settings.about.information')}</div>
<div className="text-neutral-400">{t('settings.about.information')}</div>
<div className="flex w-full flex-col space-y-4">
<div className="mt-5 flex w-full flex-col space-y-5">
{/* IP list */}
<div className="flex w-full items-start justify-between">
<span>{t('settings.about.ip')}</span>
@@ -103,6 +105,8 @@ export const Information = () => {
<span>{information ? information.application : '-'}</span>
</div>
<Hostname />
</div>
</>
);

View File

@@ -1,5 +1,5 @@
import { useEffect, useState } from 'react';
import { Divider } from 'antd';
import { Button, Divider } from 'antd';
import { useTranslation } from 'react-i18next';
import { useNavigate } from 'react-router-dom';
@@ -27,10 +27,10 @@ export const Account = () => {
return (
<>
<div className="text-base font-bold">{t('settings.account.title')}</div>
<Divider />
<div className="text-base">{t('settings.account.title')}</div>
<Divider className="opacity-50" />
<div className="flex flex-col space-y-5">
<div className="flex flex-col space-y-8">
<div className="flex items-center justify-between">
<span>{t('settings.account.webAccount')}</span>
<span>{username ? username : '-'}</span>
@@ -38,15 +38,13 @@ export const Account = () => {
<div className="flex items-center justify-between">
<span>{t('settings.account.password')}</span>
<span
className="cursor-pointer text-blue-500 hover:text-blue-500/80"
onClick={changePassword}
>
<Button type="primary" onClick={changePassword}>
{t('settings.account.updateBtn')}
</span>
</Button>
</div>
</div>
<Divider />
<Divider className="opacity-50" />
<Logout />
</>

View File

@@ -10,14 +10,14 @@ export const Appearance = () => {
return (
<>
<div className="text-base font-bold">{t('settings.appearance.title')}</div>
<Divider />
<div className="text-base">{t('settings.appearance.title')}</div>
<Divider className="opacity-50" />
<div className="flex flex-col space-y-6">
<Language />
<WebTitle />
</div>
<Divider />
<div className="text-neutral-400">{t('settings.appearance.display')}</div>
<Language />
<WebTitle />
<Divider className="opacity-50" style={{ margin: '32px 0' }} />
<MenuBar />
</>

View File

@@ -1,5 +1,4 @@
import { Select } from 'antd';
import { LanguagesIcon } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import languages from '@/i18n/languages.ts';
@@ -21,18 +20,20 @@ export const Language = () => {
}
return (
<div className="flex items-center justify-between">
<div className="flex items-center space-x-1">
<LanguagesIcon size={16} />
<div className="mt-5 flex items-center justify-between space-x-5">
<div className="flex flex-col space-y-1">
<span>{t('settings.appearance.language')}</span>
<span className="text-xs text-neutral-500">{t('settings.appearance.languageDesc')}</span>
</div>
<Select
defaultValue={i18n.language}
style={{ width: 180 }}
options={options}
onSelect={changeLanguage}
/>
<div>
<Select
defaultValue={i18n.language}
style={{ width: 180 }}
options={options}
onSelect={changeLanguage}
/>
</div>
</div>
);
};

View File

@@ -19,12 +19,12 @@ export const MenuBar = () => {
const [menuDisabledItems, setMenuDisabledItems] = useAtom(menuDisabledItemsAtom);
const items = [
{ key: 'image', icon: <DiscIcon size={16} /> },
{ key: 'download', icon: <DownloadIcon size={16} /> },
{ key: 'script', icon: <FileJsonIcon size={15} /> },
{ key: 'terminal', icon: <TerminalSquareIcon size={16} /> },
{ key: 'wol', icon: <NetworkIcon size={16} /> },
{ key: 'power', icon: <PowerIcon size={16} /> }
{ key: 'image', icon: <DiscIcon size={16} className="text-neutral-400" /> },
{ key: 'download', icon: <DownloadIcon size={16} className="text-neutral-400" /> },
{ key: 'script', icon: <FileJsonIcon size={16} className="text-neutral-400" /> },
{ key: 'terminal', icon: <TerminalSquareIcon size={16} className="text-neutral-400" /> },
{ key: 'wol', icon: <NetworkIcon size={16} className="text-neutral-400" /> },
{ key: 'power', icon: <PowerIcon size={16} className="text-neutral-400" /> }
];
function updateItems(key: string) {
@@ -45,10 +45,10 @@ export const MenuBar = () => {
<span className="text-xs text-neutral-500">{t('settings.appearance.menuBarDesc')}</span>
</div>
<div className="flex flex-col space-y-4 pt-5">
<div className="mt-8 flex flex-col space-y-5">
{items.map((item) => (
<div key={item.key} className="flex items-center justify-between">
<div className="flex items-center space-x-1">
<div className="flex items-center space-x-2">
{item.icon}
<span>{t(`${item.key}.title`)}</span>
</div>

View File

@@ -1,7 +1,6 @@
import { useEffect, useState } from 'react';
import { Input } from 'antd';
import { useAtom } from 'jotai';
import { GlobeIcon } from 'lucide-react';
import { useTranslation } from 'react-i18next';
import * as api from '@/api/vm.ts';
@@ -46,26 +45,23 @@ export const WebTitle = () => {
}
return (
<div className="flex items-center justify-between">
<div className="mt-8 flex items-center justify-between space-x-5">
<div className="flex flex-col">
<div className="flex items-center space-x-1">
<div className="flex size-[16px] items-center justify-center">
<GlobeIcon size={14} />
</div>
<span>{t('settings.appearance.webTitle')}</span>
</div>
<span>{t('settings.appearance.webTitle')}</span>
<span className="text-xs text-neutral-500">{t('settings.appearance.webTitleDesc')}</span>
</div>
<Input
disabled={isLoading}
style={{ width: 180 }}
value={webTitle}
onChange={(e) => setWebTitle(e.target.value)}
onPressEnter={submit}
onBlur={submit}
placeholder="NanoKVM"
/>
<div>
<Input
disabled={isLoading}
style={{ width: 180 }}
value={webTitle}
onChange={(e) => setWebTitle(e.target.value)}
onPressEnter={submit}
onBlur={submit}
placeholder="NanoKVM"
/>
</div>
</div>
);
};

View File

@@ -2,12 +2,11 @@ import { Collapse } from 'antd';
import { useTranslation } from 'react-i18next';
import { Swap } from './swap.tsx';
import { Autostart } from './autostart.tsx';
const children = (
<div className="py-3 space-y-6">
<div className="space-y-6 py-3">
<Swap />
<Autostart />
{/*<Autostart />*/}
</div>
);

View File

@@ -59,17 +59,17 @@ export const Swap = () => {
return (
<div className="flex items-center justify-between">
<div className="flex flex-col">
<div className="flex flex-col space-y-1">
<div className="flex items-center space-x-2">
<span>{t('settings.device.swap.title')}</span>
<Tooltip
title={t('settings.device.swap.tip')}
className="cursor-pointer"
placement="bottom"
overlayStyle={{ maxWidth: '300px' }}
placement="right"
styles={{ root: { maxWidth: '400px' } }}
>
<CircleAlertIcon size={15} />
<CircleAlertIcon className="text-neutral-500" size={14} />
</Tooltip>
</div>
<span className="text-xs text-neutral-500">{t('settings.device.swap.description')}</span>

View File

@@ -61,7 +61,7 @@ export const Hdmi = () => {
<>
{isPcie && (
<div className="flex items-center justify-between">
<div className="flex flex-col">
<div className="flex flex-col space-y-1">
<span>HDMI</span>
<span className="text-xs text-neutral-500">

View File

@@ -43,17 +43,17 @@ export const Mdns = () => {
return (
<div className="flex items-center justify-between">
<div className="flex flex-col">
<div className="flex flex-col space-y-1">
<div className="flex items-center space-x-2">
<span>mDNS</span>
<Tooltip
title={t('settings.device.mdns.tip')}
className="cursor-pointer"
placement="bottom"
overlayStyle={{ maxWidth: '300px' }}
placement="right"
styles={{ root: { maxWidth: '400px' } }}
>
<CircleAlertIcon size={15} />
<CircleAlertIcon className="text-neutral-500" size={14} />
</Tooltip>
</div>

View File

@@ -84,7 +84,7 @@ export const MouseJiggler = () => {
return (
<div className="flex items-center justify-between">
<div className="flex flex-col">
<div className="flex flex-col space-y-1">
<span>{t('settings.device.mouseJiggler.title')}</span>
<span className="text-xs text-neutral-500">
{t('settings.device.mouseJiggler.description')}

View File

@@ -54,7 +54,7 @@ export const Oled = () => {
return (
<div className="flex items-center justify-between">
<div className="flex flex-col">
<div className="flex flex-col space-y-1">
<span>{t('settings.device.oled.title')}</span>
<span className="text-xs text-neutral-500">{t('settings.device.oled.description')}</span>
</div>

View File

@@ -43,17 +43,17 @@ export const Ssh = () => {
return (
<div className="flex items-center justify-between">
<div className="flex flex-col">
<div className="flex flex-col space-y-1">
<div className="flex items-center space-x-2">
<span>SSH</span>
<Tooltip
title={t('settings.device.ssh.tip')}
className="cursor-pointer"
placement="bottom"
overlayStyle={{ maxWidth: '300px' }}
placement="right"
styles={{ root: { maxWidth: '400px' } }}
>
<CircleAlertIcon size={15} />
<CircleAlertIcon className="text-neutral-500" size={14} />
</Tooltip>
</div>
<span className="text-xs text-neutral-500">{t('settings.device.ssh.description')}</span>

View File

@@ -47,17 +47,17 @@ export const Tls = () => {
return (
<div className="flex items-center justify-between">
<div className="flex flex-col">
<div className="flex flex-col space-y-1">
<div className="flex items-center space-x-2">
<span>HTTPS</span>
<Tooltip
title={t('settings.device.tls.tip')}
className="cursor-pointer"
placement="bottom"
overlayStyle={{ maxWidth: '300px' }}
placement="right"
styles={{ root: { maxWidth: '400px' } }}
>
<CircleAlertIcon size={15} />
<CircleAlertIcon className="text-neutral-500" size={14} />
</Tooltip>
</div>
<span className="text-xs text-neutral-500">{t('settings.device.tls.description')}</span>

View File

@@ -126,7 +126,7 @@ export const Wifi = () => {
return (
<>
<div className="flex items-center justify-between">
<div className="flex flex-col">
<div className="flex flex-col space-y-1">
<span>{t('settings.device.wifi.title')}</span>
<span className="text-xs text-neutral-500">{t('settings.device.wifi.description')}</span>
</div>

View File

@@ -17,6 +17,7 @@ import * as api from '@/api/application.ts';
import * as ls from '@/lib/localstorage.ts';
import { isKeyboardEnableAtom } from '@/jotai/keyboard.ts';
import { Tailscale as TailscaleIcon } from '@/components/icons/tailscale';
import { ScrollArea } from '@/components/ui/scroll-area';
import { About } from './about';
import { Account } from './account';
@@ -119,23 +120,21 @@ export const Settings = () => {
width={'80%'}
centered={true}
footer={null}
destroyOnClose={true}
destroyOnHidden={true}
onCancel={closeModal}
style={{ maxWidth: '1080px' }}
styles={{ content: { padding: 0 } }}
>
<div className="flex h-[80vh] max-h-[700px] rounded-lg outline outline-1 outline-neutral-700">
<div className="flex h-full max-w-[240px] flex-col space-y-0.5 rounded-l-lg bg-neutral-800 px-1 sm:w-1/5 md:px-2">
<div className="flex h-full max-w-[260px] flex-col space-y-0.5 rounded-l-lg bg-neutral-800/90 px-1 sm:w-1/5 md:w-1/4 md:px-2">
<div className="hidden px-3 pt-10 text-xl sm:block">{t('settings.title')}</div>
<div className="h-10 sm:h-5" />
{tabs.map((tab) => (
<div
key={tab.id}
className={clsx(
'flex cursor-pointer select-none items-center space-x-2 rounded-lg p-2 sm:px-3',
currentTab === tab.id ? 'bg-neutral-700/70' : 'hover:bg-neutral-700'
currentTab === tab.id ? 'bg-neutral-700/50' : 'hover:bg-neutral-700/50'
)}
onClick={() => changeTab(tab.id)}
>
@@ -156,11 +155,13 @@ export const Settings = () => {
))}
</div>
<div className="flex h-full w-full flex-col items-center overflow-y-auto rounded-r-lg bg-neutral-900 px-3">
<div className="w-full max-w-[600px] pb-10 pt-14">
<>{tabs.find((tab) => tab.id === currentTab)?.component}</>
<ScrollArea className="h-full w-full rounded-r-lg bg-neutral-900/50 px-3">
<div className="flex h-full w-full justify-center">
<div className="w-full max-w-[600px] pb-10 pt-14">
<>{tabs.find((tab) => tab.id === currentTab)?.component}</>
</div>
</div>
</div>
</ScrollArea>
</div>
</Modal>
</>

View File

@@ -44,7 +44,7 @@ export const Header = ({ state, onSuccess }: HeaderProps) => {
return (
<div className="flex items-center justify-between">
<span className="text-base font-bold">{t('settings.tailscale.title')}</span>
<span className="text-base">{t('settings.tailscale.title')}</span>
<div className="flex items-center space-x-2">
{state && ['notLogin', 'stopped', 'running'].includes(state) && (
@@ -101,7 +101,7 @@ export const Header = ({ state, onSuccess }: HeaderProps) => {
placement="bottom"
trigger="click"
>
<div className="flex cursor-pointer rounded p-1 text-white hover:bg-neutral-600">
<div className="flex cursor-pointer rounded p-1 text-white hover:bg-neutral-700/50">
<EllipsisIcon size={18} />
</div>
</Popover>

View File

@@ -49,10 +49,10 @@ export const Tailscale = ({ setIsLocked }: TailscaleProps) => {
return (
<>
<Header state={status?.state} onSuccess={getStatus} />
<Divider />
<Divider className="opacity-50" />
{isLoading ? (
<div className="flex items-center justify-center space-x-2 pt-5 text-neutral-500">
<div className="flex w-full items-center justify-center space-x-2 pt-5 text-neutral-500">
<LoaderCircleIcon className="animate-spin" size={18} />
<span>{t('settings.tailscale.loading')}</span>
</div>

View File

@@ -52,7 +52,7 @@ export const Memory = () => {
title={t('settings.tailscale.memory.tip')}
className="cursor-pointer text-neutral-500"
placement="top"
overlayStyle={{ maxWidth: '400px' }}
styles={{ root: { maxWidth: '400px' } }}
>
<CircleHelpIcon size={15} />
</Tooltip>

View File

@@ -59,7 +59,7 @@ export const Swap = () => {
title={t('settings.tailscale.swap.tip')}
className="cursor-pointer text-neutral-500"
placement="top"
overlayStyle={{ maxWidth: '400px' }}
styles={{ root: { maxWidth: '400px' } }}
>
<CircleHelpIcon size={15} />
</Tooltip>

View File

@@ -77,12 +77,12 @@ export const Update = ({ setIsLocked }: UpdateProps) => {
return (
<>
<div className="text-base font-bold">{t('settings.update.title')}</div>
<Divider />
<div className="text-base">{t('settings.update.title')}</div>
<Divider className="opacity-50" />
<Preview />
<div className="my-[40px] h-px bg-neutral-500/10" />
<Divider style={{ margin: '50px 0 10px 0', opacity: 0.5 }} />
<div className="flex min-h-[400px] flex-col justify-between">
{status === 'loading' && (

View File

@@ -56,17 +56,17 @@ export const Preview = () => {
return (
<div className="flex items-center justify-between">
<div className="flex flex-col">
<div className="flex flex-col space-y-1">
<div className="flex items-center space-x-2">
<span>{t('settings.update.preview')}</span>
<Tooltip
title={t('settings.update.previewTip')}
className="cursor-pointer"
placement="bottom"
overlayStyle={{ maxWidth: '350px' }}
placement="right"
styles={{ root: { maxWidth: '400px' } }}
>
<CircleAlertIcon size={15} />
<CircleAlertIcon className="text-neutral-500" size={15} />
</Tooltip>
</div>