feat: 统一和完善 UI

This commit is contained in:
mofeng-git
2026-07-19 21:43:27 +08:00
parent 5d871e8b21
commit 275c2ca343
25 changed files with 431 additions and 402 deletions

View File

@@ -3117,22 +3117,22 @@ onUnmounted(() => {
/>
</div>
<div class="mx-1 hidden h-6 w-px bg-border md:block" />
<Button variant="ghost" size="icon" class="h-8 w-8 hidden md:flex" :aria-label="t('common.toggleTheme')" @click="toggleTheme">
<Sun v-if="isDark" class="h-4 w-4" />
<Moon v-else class="h-4 w-4" />
<Button variant="ghost" size="icon-sm" class="hidden md:flex" :aria-label="t('common.toggleTheme')" @click="toggleTheme">
<Sun v-if="isDark" class="size-4" />
<Moon v-else class="size-4" />
</Button>
<LanguageToggleButton class="h-8 w-8 hidden md:flex" />
<LanguageToggleButton class="size-8 hidden md:flex" />
<DropdownMenu>
<DropdownMenuTrigger as-child>
<Button variant="outline" size="sm" class="gap-1 sm:gap-1.5 h-7 sm:h-9 px-2 sm:px-3">
<Button variant="outline" size="sm" class="gap-1 sm:gap-1.5 px-2 sm:px-3">
<span class="text-xs max-w-[60px] sm:max-w-[100px] truncate">{{ authStore.user || 'admin' }}</span>
<ChevronDown class="h-3 w-3 sm:h-3.5 sm:w-3.5" />
<ChevronDown class="size-3 sm:size-3.5" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem class="md:hidden" @click="toggleTheme">
<Sun v-if="isDark" class="h-4 w-4 mr-2" />
<Moon v-else class="h-4 w-4 mr-2" />
<Sun v-if="isDark" class="size-4 mr-2" />
<Moon v-else class="size-4 mr-2" />
{{ isDark ? t('settings.lightMode') : t('settings.darkMode') }}
</DropdownMenuItem>
<DropdownMenuItem as-child class="md:hidden p-0">
@@ -3145,12 +3145,12 @@ onUnmounted(() => {
</DropdownMenuItem>
<DropdownMenuSeparator class="md:hidden" />
<DropdownMenuItem @click="changePasswordDialogOpen = true">
<KeyRound class="h-4 w-4 mr-2" />
<KeyRound class="size-4 mr-2" />
{{ t('auth.changePassword') }}
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem @click="logout">
<LogOut class="h-4 w-4 mr-2" />
<LogOut class="size-4 mr-2" />
{{ t('auth.logout') }}
</DropdownMenuItem>
</DropdownMenuContent>
@@ -3179,13 +3179,7 @@ onUnmounted(() => {
@open-computer-use="openComputerUse"
/>
<div class="flex-1 overflow-hidden relative">
<div
class="absolute inset-0 bg-slate-100/80 dark:bg-slate-800/40 opacity-80"
style="
background-image: radial-gradient(circle, rgb(148 163 184 / 0.4) 1px, transparent 1px);
background-size: 20px 20px;
"
/>
<div class="absolute inset-0 dot-grid-bg" />
<div class="relative flex h-full w-full min-w-0 items-stretch gap-3 p-1 sm:p-4">
<div
class="flex min-w-0 flex-1 items-center justify-center transition-all duration-300"
@@ -3293,7 +3287,7 @@ onUnmounted(() => {
<div class="absolute w-full h-0.5 bg-gradient-to-r from-transparent via-primary/40 to-transparent animate-pulse" style="top: 50%; animation-duration: 1.5s;" />
</div>
<Spinner class="h-10 w-10 sm:h-16 sm:w-16 text-white mb-2 sm:mb-4" />
<Spinner class="size-10 sm:size-16 text-white mb-2 sm:mb-4" />
<p class="text-white/90 text-sm sm:text-lg font-medium text-center px-4 flex items-baseline justify-center gap-2 flex-wrap">
<span>{{ webrtcLoadingMessage }}</span>
<span
@@ -3339,7 +3333,7 @@ onUnmounted(() => {
}"
>
<MonitorOff
class="h-10 w-10 sm:h-16 sm:w-16"
class="size-10 sm:size-16"
:class="{
'text-slate-200': signalOverlayInfo.tone === 'info',
'text-red-300': signalOverlayInfo.tone === 'error',
@@ -3368,7 +3362,7 @@ onUnmounted(() => {
v-if="videoError && !videoLoading"
class="absolute inset-0 flex flex-col items-center justify-center bg-black/85 text-white gap-4 transition-opacity duration-300 p-4"
>
<MonitorOff class="h-10 w-10 sm:h-16 sm:w-16 text-slate-400" />
<MonitorOff class="size-10 sm:size-16 text-slate-400" />
<div class="text-center max-w-md px-2">
<p class="font-medium text-sm sm:text-lg mb-1 sm:mb-2">{{ t('console.connectionFailed') }}</p>
<p class="text-xs sm:text-sm text-slate-300 mb-2 sm:mb-3">{{ t('console.connectionFailedDesc') }}</p>
@@ -3379,7 +3373,7 @@ onUnmounted(() => {
</div>
<div class="flex gap-2">
<Button variant="secondary" size="sm" @click="reloadPage">
<RefreshCw class="h-4 w-4 mr-2" />
<RefreshCw class="size-4 mr-2" />
{{ t('console.reconnect') }}
</Button>
</div>
@@ -3470,7 +3464,7 @@ onUnmounted(() => {
{{ t('common.cancel') }}
</Button>
<Button @click="handleChangePassword" :disabled="changingPassword">
<Loader2 v-if="changingPassword" class="h-4 w-4 mr-2 animate-spin" />
<Loader2 v-if="changingPassword" class="size-4 mr-2 animate-spin" />
{{ t('common.confirm') }}
</Button>
</DialogFooter>

View File

@@ -120,27 +120,27 @@ function returnToPassword(keepError = false) {
</script>
<template>
<div class="min-h-screen min-h-dvh flex items-center justify-center bg-background p-4">
<div class="min-h-screen min-h-dvh flex items-center justify-center dot-grid-bg p-4">
<Card class="relative w-full max-w-sm">
<div class="absolute top-4 right-4">
<LanguageToggleButton />
</div>
<CardHeader class="space-y-2 pt-10 text-center sm:pt-12">
<CardHeader class="space-y-2 pt-8 text-center">
<div class="mx-auto flex justify-center">
<BrandMark size="xl" />
<BrandMark size="lg" />
</div>
<CardTitle class="text-xl sm:text-2xl">One-KVM</CardTitle>
<CardTitle class="text-xl">One-KVM</CardTitle>
<CardDescription>{{ step === 'password' ? t('auth.login') : t('auth.totpPrompt') }}</CardDescription>
</CardHeader>
<CardContent>
<form v-if="step === 'password'" @submit.prevent="handleLogin">
<FieldGroup>
<FieldGroup class="gap-5">
<Field>
<FieldLabel for="username">{{ t('auth.username') }}</FieldLabel>
<div class="relative">
<User class="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
<User class="absolute left-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" />
<Input
id="username"
v-model="username"
@@ -164,7 +164,7 @@ function returnToPassword(keepError = false) {
class="h-auto gap-1 p-0 text-xs text-muted-foreground"
>
{{ t('auth.forgotPassword') }}
<CircleHelp class="h-3.5 w-3.5" />
<CircleHelp class="size-3.5" />
</Button>
</PopoverTrigger>
<PopoverContent class="w-80 p-3" align="end">
@@ -175,7 +175,7 @@ function returnToPassword(keepError = false) {
</Popover>
</div>
<div class="relative">
<Lock class="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
<Lock class="absolute left-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" />
<Input
id="password"
v-model="password"
@@ -211,11 +211,11 @@ function returnToPassword(keepError = false) {
</FieldGroup>
</form>
<form v-else @submit.prevent="handleTotpLogin">
<FieldGroup>
<FieldGroup class="gap-5">
<Field>
<FieldLabel for="totp-code">{{ t('auth.totpCode') }}</FieldLabel>
<div class="relative">
<KeyRound class="absolute left-3 top-1/2 h-4 w-4 -translate-y-1/2 text-muted-foreground" />
<KeyRound class="absolute left-3 top-1/2 size-4 -translate-y-1/2 text-muted-foreground" />
<Input
id="totp-code"
v-model="totpCode"
@@ -240,7 +240,7 @@ function returnToPassword(keepError = false) {
<span v-else>{{ t('auth.verifyAndLogin') }}</span>
</Button>
<Button type="button" variant="ghost" class="w-full" :disabled="loading" @click="returnToPassword()">
<ArrowLeft class="h-4 w-4" />
<ArrowLeft class="size-4" />
{{ t('auth.backToPassword') }}
</Button>
</FieldGroup>

View File

@@ -2700,19 +2700,19 @@ watch(isWindows, () => {
<AppLayout>
<SidebarProvider class="h-full min-h-0 overflow-hidden">
<Sidebar class="top-10 h-[calc(100dvh-2.5rem)] sm:top-14 sm:h-[calc(100dvh-3.5rem)]" collapsible="offcanvas">
<SidebarHeader class="gap-1 px-6 pt-6 pb-3 text-foreground md:pt-10">
<SidebarHeader class="gap-1 px-6 pt-4 pb-2 text-foreground md:pt-6">
<h1 class="text-xl font-semibold">{{ t('settings.title') }}</h1>
<p class="text-xs text-muted-foreground">{{ t('settings.sidebarSubtitle') }}</p>
</SidebarHeader>
<SidebarContent class="px-3 pb-6 md:pb-10">
<SidebarGroup v-for="group in navGroups" :key="group.title" class="px-0 py-2">
<SidebarGroup v-for="group in navGroups" :key="group.title" class="px-0 py-1">
<SidebarGroupLabel class="uppercase">{{ group.title }}</SidebarGroupLabel>
<SidebarMenu>
<SidebarMenuItem v-for="item in group.items" :key="item.id">
<SidebarMenuButton
:is-active="activeSection === item.id"
:tooltip="item.label"
class="h-10 px-3 text-foreground data-[active=true]:bg-primary data-[active=true]:text-primary-foreground data-[active=true]:shadow-sm data-[active=true]:hover:bg-primary data-[active=true]:hover:text-primary-foreground"
class="h-8 px-3 text-foreground data-[active=true]:bg-primary data-[active=true]:text-primary-foreground data-[active=true]:shadow-sm data-[active=true]:hover:bg-primary data-[active=true]:hover:text-primary-foreground"
@click="selectSection(item.id)"
>
<component :is="item.icon" />
@@ -2727,19 +2727,19 @@ watch(isWindows, () => {
<SidebarInset class="min-w-0 overflow-y-auto">
<!-- Mobile Header -->
<div class="md:hidden sticky top-0 z-20 flex items-center px-3 sm:px-4 py-2 sm:py-3 border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/70">
<SidebarTrigger class="mr-1.5 h-8 w-8 sm:mr-2 sm:h-9 sm:w-9" />
<SidebarTrigger class="mr-1.5 size-8 sm:mr-2 sm:size-9" />
<div class="flex items-center gap-2 min-w-0">
<component :is="sectionMeta.icon" class="h-4 w-4 text-muted-foreground shrink-0" />
<component :is="sectionMeta.icon" class="size-4 text-muted-foreground shrink-0" />
<h1 class="text-sm sm:text-base font-semibold truncate">{{ sectionMeta.title }}</h1>
</div>
</div>
<div class="mx-auto w-full max-w-3xl px-3 sm:px-6 lg:px-8 pt-6 md:pt-10 pb-10 space-y-6">
<div class="mx-auto w-full max-w-4xl px-3 sm:px-6 lg:px-8 pt-6 pb-10 space-y-4 settings-dense">
<!-- Section Header -->
<header class="space-y-1.5 pb-2 border-b">
<div class="flex items-center gap-2.5">
<component :is="sectionMeta.icon" class="h-5 w-5 text-muted-foreground" />
<h1 class="text-xl sm:text-2xl font-semibold tracking-tight">{{ sectionMeta.title }}</h1>
<component :is="sectionMeta.icon" class="size-5 text-muted-foreground" />
<h1 class="text-xl font-semibold tracking-tight">{{ sectionMeta.title }}</h1>
</div>
<p v-if="sectionMeta.description" class="text-sm text-muted-foreground">
{{ sectionMeta.description }}
@@ -2747,7 +2747,7 @@ watch(isWindows, () => {
</header>
<!-- Appearance Section -->
<div v-show="activeSection === 'appearance'" class="space-y-6">
<div v-show="activeSection === 'appearance'" class="space-y-4">
<Card>
<CardHeader>
<CardTitle>{{ t('settings.theme') }}</CardTitle>
@@ -2756,13 +2756,13 @@ watch(isWindows, () => {
<CardContent>
<div class="grid grid-cols-3 gap-2 sm:max-w-md">
<Button :variant="theme === 'light' ? 'default' : 'outline'" size="sm" class="justify-center" @click="setTheme('light')">
<Sun class="h-4 w-4 mr-1.5" />{{ t('settings.lightMode') }}
<Sun class="size-4 mr-1.5" />{{ t('settings.lightMode') }}
</Button>
<Button :variant="theme === 'dark' ? 'default' : 'outline'" size="sm" class="justify-center" @click="setTheme('dark')">
<Moon class="h-4 w-4 mr-1.5" />{{ t('settings.darkMode') }}
<Moon class="size-4 mr-1.5" />{{ t('settings.darkMode') }}
</Button>
<Button :variant="theme === 'system' ? 'default' : 'outline'" size="sm" class="justify-center" @click="setTheme('system')">
<Monitor class="h-4 w-4 mr-1.5" />{{ t('settings.systemMode') }}
<Monitor class="size-4 mr-1.5" />{{ t('settings.systemMode') }}
</Button>
</div>
</CardContent>
@@ -2786,21 +2786,21 @@ watch(isWindows, () => {
<CardContent class="space-y-1">
<div class="flex items-center justify-between gap-4 px-3 py-3">
<Label for="feature-web-terminal" class="flex min-w-0 items-center gap-2 font-normal">
<Terminal class="h-4 w-4 shrink-0 text-muted-foreground" />
<Terminal class="size-4 shrink-0 text-muted-foreground" />
<span class="truncate">{{ t('actionbar.webTerminal') }}</span>
</Label>
<Switch id="feature-web-terminal" v-model="featureVisibility.webTerminal" />
</div>
<div class="flex items-center justify-between gap-4 px-3 py-3">
<Label for="feature-computer-use" class="flex min-w-0 items-center gap-2 font-normal">
<Bot class="h-4 w-4 shrink-0 text-muted-foreground" />
<Bot class="size-4 shrink-0 text-muted-foreground" />
<span class="truncate">{{ t('settings.computerUseAgent') }}</span>
</Label>
<Switch id="feature-computer-use" v-model="featureVisibility.computerUse" />
</div>
<div class="flex items-center justify-between gap-4 px-3 py-3">
<Label for="feature-paste-text" class="flex min-w-0 items-center gap-2 font-normal">
<ClipboardPaste class="h-4 w-4 shrink-0 text-muted-foreground" />
<ClipboardPaste class="size-4 shrink-0 text-muted-foreground" />
<span class="truncate">{{ t('settings.pasteText') }}</span>
</Label>
<Switch id="feature-paste-text" v-model="featureVisibility.pasteText" />
@@ -2810,7 +2810,7 @@ watch(isWindows, () => {
</div>
<!-- Account Section -->
<div v-show="activeSection === 'account'" class="space-y-6">
<div v-show="activeSection === 'account'" class="space-y-4">
<Card>
<CardHeader>
<CardTitle>{{ t('settings.username') }}</CardTitle>
@@ -2826,12 +2826,12 @@ watch(isWindows, () => {
<Input id="account-username-password" v-model="usernamePassword" type="password" autocomplete="current-password" />
</div>
<p v-if="usernameError" class="text-xs text-destructive">{{ usernameError }}</p>
<p v-else-if="usernameSaved" class="flex items-center gap-1.5 text-xs text-success"><Check class="h-3.5 w-3.5" />{{ t('common.success') }}</p>
<p v-else-if="usernameSaved" class="flex items-center gap-1.5 text-xs text-success"><Check class="size-3.5" />{{ t('common.success') }}</p>
</CardContent>
<CardFooter class="border-t pt-4 justify-end">
<Button @click="changeUsername" :disabled="usernameSaving">
<Loader2 v-if="usernameSaving" class="h-4 w-4 mr-2 animate-spin" />
<Save v-else class="h-4 w-4 mr-2" />
<Loader2 v-if="usernameSaving" class="size-4 mr-2 animate-spin" />
<Save v-else class="size-4 mr-2" />
{{ t('common.save') }}
</Button>
</CardFooter>
@@ -2856,12 +2856,12 @@ watch(isWindows, () => {
<Input id="account-confirm-password" v-model="confirmPassword" type="password" autocomplete="new-password" />
</div>
<p v-if="passwordError" class="text-xs text-destructive">{{ passwordError }}</p>
<p v-else-if="passwordSaved" class="flex items-center gap-1.5 text-xs text-success"><Check class="h-3.5 w-3.5" />{{ t('common.success') }}</p>
<p v-else-if="passwordSaved" class="flex items-center gap-1.5 text-xs text-success"><Check class="size-3.5" />{{ t('common.success') }}</p>
</CardContent>
<CardFooter class="border-t pt-4 justify-end">
<Button @click="changePassword" :disabled="passwordSaving">
<Loader2 v-if="passwordSaving" class="h-4 w-4 mr-2 animate-spin" />
<Save v-else class="h-4 w-4 mr-2" />
<Loader2 v-if="passwordSaving" class="size-4 mr-2 animate-spin" />
<Save v-else class="size-4 mr-2" />
{{ t('common.save') }}
</Button>
</CardFooter>
@@ -2885,8 +2885,8 @@ watch(isWindows, () => {
</CardContent>
<CardFooter class="border-t pt-4 justify-end">
<Button @click="saveAuthConfig" :disabled="authConfigLoading">
<Loader2 v-if="authConfigLoading" class="h-4 w-4 mr-2 animate-spin" />
<Save v-else class="h-4 w-4 mr-2" />
<Loader2 v-if="authConfigLoading" class="size-4 mr-2 animate-spin" />
<Save v-else class="size-4 mr-2" />
{{ t('common.save') }}
</Button>
</CardFooter>
@@ -2894,7 +2894,7 @@ watch(isWindows, () => {
</div>
<!-- Video Section -->
<div v-show="activeSection === 'video'" class="space-y-6">
<div v-show="activeSection === 'video'" class="space-y-4">
<!-- Video Device Settings -->
<Card>
<CardHeader class="flex flex-row items-start justify-between space-y-0">
@@ -2902,8 +2902,8 @@ watch(isWindows, () => {
<CardTitle>{{ t('settings.videoSettings') }}</CardTitle>
<CardDescription>{{ t('settings.videoSettingsDesc') }}</CardDescription>
</div>
<Button variant="ghost" size="icon" class="h-8 w-8" :aria-label="t('common.refresh')" @click="loadDevices">
<RefreshCw class="h-4 w-4" />
<Button variant="ghost" size="icon-sm" :aria-label="t('common.refresh')" @click="loadDevices">
<RefreshCw class="size-4" />
</Button>
</CardHeader>
<CardContent class="space-y-4">
@@ -3023,8 +3023,8 @@ watch(isWindows, () => {
:aria-label="showPasswords ? t('extensions.rustdesk.hidePassword') : t('extensions.rustdesk.showPassword')"
@click="showPasswords = !showPasswords"
>
<Eye v-if="!showPasswords" class="h-4 w-4" />
<EyeOff v-else class="h-4 w-4" />
<Eye v-if="!showPasswords" class="size-4" />
<EyeOff v-else class="size-4" />
</Button>
</div>
</div>
@@ -3034,15 +3034,15 @@ watch(isWindows, () => {
</div>
<!-- HID Section -->
<div v-show="activeSection === 'hid'" class="space-y-6">
<div v-show="activeSection === 'hid'" class="space-y-4">
<Card>
<CardHeader class="flex flex-row items-start justify-between space-y-0">
<div class="space-y-1.5">
<CardTitle>{{ t('settings.hidSettings') }}</CardTitle>
<CardDescription>{{ t('settings.hidSettingsDesc') }}</CardDescription>
</div>
<Button variant="ghost" size="icon" class="h-8 w-8" :aria-label="t('common.refresh')" @click="loadDevices">
<RefreshCw class="h-4 w-4" />
<Button variant="ghost" size="icon-sm" :aria-label="t('common.refresh')" @click="loadDevices">
<RefreshCw class="size-4" />
</Button>
</CardHeader>
<CardContent class="space-y-4">
@@ -3090,13 +3090,13 @@ watch(isWindows, () => {
<h4 class="text-sm font-medium">{{ t('settings.ch9329Descriptor') }}</h4>
<p class="text-sm text-muted-foreground">{{ t('settings.ch9329DescriptorDesc') }}</p>
</div>
<Button variant="ghost" size="icon" class="h-8 w-8 shrink-0" :aria-label="t('common.refresh')" :disabled="ch9329DescriptorLoading" @click="loadCh9329Descriptor">
<RefreshCw class="h-4 w-4" :class="{ 'animate-spin': ch9329DescriptorLoading }" />
<Button variant="ghost" size="icon-sm" class="shrink-0" :aria-label="t('common.refresh')" :disabled="ch9329DescriptorLoading" @click="loadCh9329Descriptor">
<RefreshCw class="size-4" :class="{ 'animate-spin': ch9329DescriptorLoading }" />
</Button>
</div>
</div>
<p v-if="ch9329DescriptorLoading" class="text-sm text-muted-foreground flex items-center gap-2">
<Loader2 class="h-4 w-4 animate-spin" />
<Loader2 class="size-4 animate-spin" />
{{ t('settings.ch9329DescriptorLoading') }}
</p>
<p v-else-if="ch9329DescriptorError" class="text-sm text-destructive">
@@ -3355,7 +3355,7 @@ watch(isWindows, () => {
</div>
<!-- Environment Section -->
<div v-show="activeSection === 'environment'" class="space-y-4 max-w-3xl">
<div v-show="activeSection === 'environment'" class="space-y-4">
<Card v-if="systemStore.deviceInfo">
<CardHeader>
<CardTitle>{{ t('settings.deviceInfo') }}</CardTitle>
@@ -3408,7 +3408,7 @@ watch(isWindows, () => {
]"
@click="onRunOtgSelfCheckClick"
>
<RefreshCw class="h-4 w-4 mr-2" :class="{ 'animate-spin': otgSelfCheckLoading }" />
<RefreshCw class="size-4 mr-2" :class="{ 'animate-spin': otgSelfCheckLoading }" />
{{ t('settings.otgSelfCheck.run') }}
</Button>
</CardHeader>
@@ -3445,7 +3445,7 @@ watch(isWindows, () => {
>
<summary class="list-none cursor-pointer px-4 py-3 flex items-center justify-between gap-3 hover:bg-muted/40">
<div class="flex items-center gap-2 min-w-0">
<span class="inline-block h-2 w-2 rounded-full shrink-0" :class="otgGroupStatusClass(group.status)" />
<span class="inline-block size-2 rounded-full shrink-0" :class="otgGroupStatusClass(group.status)" />
<span class="text-sm font-medium truncate leading-6">{{ t(group.titleKey) }}</span>
</div>
<div class="flex items-center gap-2 shrink-0">
@@ -3461,7 +3461,7 @@ watch(isWindows, () => {
class="px-4 py-3 border-b last:border-b-0"
>
<div class="flex items-start gap-2">
<span class="inline-block h-2 w-2 rounded-full mt-1.5 shrink-0" :class="otgCheckLevelClass(item.level)" />
<span class="inline-block size-2 rounded-full mt-1.5 shrink-0" :class="otgCheckLevelClass(item.level)" />
<div class="min-w-0 space-y-1">
<div class="flex items-center gap-2">
<p class="text-sm leading-5">{{ otgCheckMessage(item) }}</p>
@@ -3503,7 +3503,7 @@ watch(isWindows, () => {
]"
@click="onRunVideoEncoderSelfCheckClick"
>
<RefreshCw class="h-4 w-4 mr-2" :class="{ 'animate-spin': videoEncoderSelfCheckLoading }" />
<RefreshCw class="size-4 mr-2" :class="{ 'animate-spin': videoEncoderSelfCheckLoading }" />
{{ t('settings.encoderSelfCheck.run') }}
</Button>
</CardHeader>
@@ -3569,7 +3569,7 @@ watch(isWindows, () => {
</div>
<div v-show="activeSection === 'other'" class="space-y-6">
<div v-show="activeSection === 'other'" class="space-y-4">
<Card>
<CardHeader>
<div class="flex items-start justify-between gap-4">
@@ -3587,8 +3587,8 @@ watch(isWindows, () => {
</CardHeader>
<CardContent class="space-y-4">
<div class="flex items-center gap-2 text-sm">
<Loader2 v-if="watchdogLoading" class="h-4 w-4 animate-spin text-muted-foreground" />
<span v-else class="h-2.5 w-2.5 rounded-full" :class="watchdogStatusClass" />
<Loader2 v-if="watchdogLoading" class="size-4 animate-spin text-muted-foreground" />
<span v-else class="size-2.5 rounded-full" :class="watchdogStatusClass" />
<span class="font-medium">
{{ watchdogLoading
? t('common.loading')
@@ -3617,7 +3617,7 @@ watch(isWindows, () => {
:disabled="usbDevicesLoading"
@click="fetchUsbDevices()"
>
<RefreshCw class="h-4 w-4 mr-2" :class="{ 'animate-spin': usbDevicesLoading }" />
<RefreshCw class="size-4 mr-2" :class="{ 'animate-spin': usbDevicesLoading }" />
{{ t('settings.usbDevices.refresh') }}
</Button>
</CardHeader>
@@ -3657,7 +3657,7 @@ watch(isWindows, () => {
v-if="dev.authorized != null"
variant="outline"
size="sm"
class="h-7 text-xs"
class="text-xs"
:disabled="usbResetLoading"
@click="usbResetTarget = dev"
>
@@ -3701,14 +3701,14 @@ watch(isWindows, () => {
</AlertDialogContent>
</AlertDialog>
</div>
<div v-show="activeSection === 'network'" class="space-y-6">
<div v-show="activeSection === 'network'" class="space-y-4">
<!-- Auto-restart: restarting progress -->
<div
v-if="autoRestarting"
class="flex items-center gap-3 rounded-lg border bg-card px-4 py-3 text-sm shadow-sm"
>
<RefreshCw class="h-4 w-4 animate-spin text-primary shrink-0" />
<RefreshCw class="size-4 animate-spin text-primary shrink-0" />
<div class="flex-1 min-w-0">
<p class="font-medium">{{ t('settings.autoRestarting') }}</p>
<p class="text-xs text-muted-foreground">
@@ -3739,7 +3739,7 @@ watch(isWindows, () => {
<AlertDescription class="flex items-center justify-between gap-3">
<span>{{ t('settings.autoRestartFailed') }}</span>
<Button variant="outline" size="sm" class="text-foreground" @click="triggerAutoRestart">
<RefreshCw class="h-3 w-3 mr-1" />
<RefreshCw class="size-3 mr-1" />
{{ t('common.retry') }}
</Button>
</AlertDescription>
@@ -3807,33 +3807,33 @@ watch(isWindows, () => {
<div class="flex items-center gap-2">
<code class="font-mono text-xs sm:text-sm break-all flex-1 min-w-0">{{ previewAccessUrl }}</code>
<Button
variant="ghost" size="icon" class="h-7 w-7 shrink-0"
variant="ghost" size="icon-sm" class="shrink-0"
:title="t('settings.copyUrl')"
:aria-label="t('settings.copyUrl')"
@click="copyPreviewUrl"
>
<Check v-if="previewUrlCopied" class="h-3.5 w-3.5 text-success" />
<Copy v-else class="h-3.5 w-3.5" />
<Check v-if="previewUrlCopied" class="size-3.5 text-success" />
<Copy v-else class="size-3.5" />
</Button>
<Button
variant="ghost" size="icon" class="h-7 w-7 shrink-0"
variant="ghost" size="icon-sm" class="shrink-0"
:title="t('settings.openInBrowser')"
:aria-label="t('settings.openInBrowser')"
@click="openPreviewUrl"
>
<ExternalLink class="h-3.5 w-3.5" />
<ExternalLink class="size-3.5" />
</Button>
</div>
</div>
</CardContent>
<CardFooter class="flex items-center justify-between gap-3 border-t pt-4">
<p class="flex min-w-0 items-center gap-1.5 text-xs text-muted-foreground">
<AlertTriangle class="h-3.5 w-3.5 shrink-0 text-warning" />
<AlertTriangle class="size-3.5 shrink-0 text-warning" />
<span class="truncate">{{ t('settings.restartRequiredHint') }}</span>
</p>
<Button @click="saveWebServerConfig" :disabled="webServerLoading || autoRestarting">
<RefreshCw v-if="autoRestarting" class="h-4 w-4 mr-2 animate-spin" />
<Save v-else class="h-4 w-4 mr-2" />
<RefreshCw v-if="autoRestarting" class="size-4 mr-2 animate-spin" />
<Save v-else class="size-4 mr-2" />
{{ autoRestarting ? t('settings.restarting') : t('common.save') }}
</Button>
</CardFooter>
@@ -3891,11 +3891,11 @@ watch(isWindows, () => {
<div v-for="(_, i) in bindAddressList" :key="`bind-${i}`" class="flex gap-2">
<Input v-model="bindAddressList[i]" placeholder="192.168.1.10" />
<Button variant="ghost" size="icon" :aria-label="t('common.delete')" @click="removeBindAddress(i)">
<Trash2 class="h-4 w-4" />
<Trash2 class="size-4" />
</Button>
</div>
<Button variant="outline" size="sm" @click="addBindAddress">
<Plus class="h-4 w-4 mr-1" />
<Plus class="size-4 mr-1" />
{{ t('settings.addBindAddress') }}
</Button>
<p v-if="bindAddressError" class="text-xs text-destructive">{{ bindAddressError }}</p>
@@ -3911,12 +3911,12 @@ watch(isWindows, () => {
</CardContent>
<CardFooter class="flex items-center justify-between gap-3 border-t pt-4">
<p class="flex min-w-0 items-center gap-1.5 text-xs text-muted-foreground">
<AlertTriangle class="h-3.5 w-3.5 shrink-0 text-warning" />
<AlertTriangle class="size-3.5 shrink-0 text-warning" />
<span class="truncate">{{ t('settings.restartRequiredHint') }}</span>
</p>
<Button @click="saveWebServerConfig" :disabled="webServerLoading || !!bindAddressError || autoRestarting">
<RefreshCw v-if="autoRestarting" class="h-4 w-4 mr-2 animate-spin" />
<Save v-else class="h-4 w-4 mr-2" />
<RefreshCw v-if="autoRestarting" class="size-4 mr-2 animate-spin" />
<Save v-else class="size-4 mr-2" />
{{ autoRestarting ? t('settings.restarting') : t('common.save') }}
</Button>
</CardFooter>
@@ -3942,12 +3942,12 @@ watch(isWindows, () => {
<Button
variant="ghost"
size="sm"
class="text-destructive hover:text-destructive h-7 text-xs"
class="text-destructive hover:text-destructive text-xs"
:disabled="certClearing || autoRestarting"
@click="clearCertificate"
>
<RefreshCw v-if="certClearing || autoRestarting" class="h-3 w-3 mr-1 animate-spin" />
<Trash2 v-else class="h-3 w-3 mr-1" />
<RefreshCw v-if="certClearing || autoRestarting" class="size-3 mr-1 animate-spin" />
<Trash2 v-else class="size-3 mr-1" />
{{ autoRestarting ? t('settings.restarting') : t('settings.sslCertClear') }}
</Button>
</AlertDescription>
@@ -3980,15 +3980,15 @@ watch(isWindows, () => {
</CardContent>
<CardFooter class="flex items-center justify-between gap-3 border-t pt-4">
<p class="flex min-w-0 items-center gap-1.5 text-xs text-muted-foreground">
<AlertTriangle class="h-3.5 w-3.5 shrink-0 text-warning" />
<AlertTriangle class="size-3.5 shrink-0 text-warning" />
<span class="truncate">{{ t('settings.restartRequiredHint') }}</span>
</p>
<Button
:disabled="certSaving || autoRestarting || !sslCertPem.trim() || !sslKeyPem.trim()"
@click="saveCertificate"
>
<RefreshCw v-if="certSaving || autoRestarting" class="h-4 w-4 mr-2 animate-spin" />
<Save v-else class="h-4 w-4 mr-2" />
<RefreshCw v-if="certSaving || autoRestarting" class="size-4 mr-2 animate-spin" />
<Save v-else class="size-4 mr-2" />
{{ autoRestarting ? t('settings.restarting') : t('settings.sslCertSave') }}
</Button>
</CardFooter>
@@ -3996,7 +3996,7 @@ watch(isWindows, () => {
</div>
<!-- ATX Section -->
<div v-show="activeSection === 'atx'" class="space-y-6">
<div v-show="activeSection === 'atx'" class="space-y-4">
<Card>
<CardHeader>
<CardTitle>{{ t('settings.atxPowerManagement') }}</CardTitle>
@@ -4012,8 +4012,8 @@ watch(isWindows, () => {
</SelectContent>
</Select>
</div>
<Button variant="ghost" size="icon" class="h-9 w-9 shrink-0" :aria-label="t('common.refresh')" @click="loadAtxDevices">
<RefreshCw class="h-4 w-4" />
<Button variant="ghost" size="icon-sm" class="shrink-0" :aria-label="t('common.refresh')" @click="loadAtxDevices">
<RefreshCw class="size-4" />
</Button>
</div>
@@ -4167,7 +4167,7 @@ watch(isWindows, () => {
</CardContent>
<CardFooter class="border-t pt-4 justify-end">
<Button :disabled="atxSaving" @click="saveAtxSettings">
<Loader2 v-if="atxSaving" class="h-4 w-4 mr-2 animate-spin" /><Check v-else-if="atxSaved" class="h-4 w-4 mr-2" /><Save v-else class="h-4 w-4 mr-2" />{{ atxSaving ? t('actionbar.applying') : atxSaved ? t('common.success') : t('common.save') }}
<Loader2 v-if="atxSaving" class="size-4 mr-2 animate-spin" /><Check v-else-if="atxSaved" class="size-4 mr-2" /><Save v-else class="size-4 mr-2" />{{ atxSaving ? t('actionbar.applying') : atxSaved ? t('common.success') : t('common.save') }}
</Button>
</CardFooter>
</Card>
@@ -4190,14 +4190,14 @@ watch(isWindows, () => {
</CardContent>
<CardFooter class="border-t pt-4 justify-end">
<Button :disabled="wolSaving" @click="saveWolSettings">
<Loader2 v-if="wolSaving" class="h-4 w-4 mr-2 animate-spin" /><Check v-else-if="wolSaved" class="h-4 w-4 mr-2" /><Save v-else class="h-4 w-4 mr-2" />{{ wolSaving ? t('actionbar.applying') : wolSaved ? t('common.success') : t('common.save') }}
<Loader2 v-if="wolSaving" class="size-4 mr-2 animate-spin" /><Check v-else-if="wolSaved" class="size-4 mr-2" /><Save v-else class="size-4 mr-2" />{{ wolSaving ? t('actionbar.applying') : wolSaved ? t('common.success') : t('common.save') }}
</Button>
</CardFooter>
</Card>
</div>
<!-- ttyd Section -->
<div v-show="activeSection === 'ext-ttyd'" class="space-y-6">
<div v-show="activeSection === 'ext-ttyd'" class="space-y-4">
<Card>
<CardHeader>
<div class="flex items-center justify-between">
@@ -4228,7 +4228,7 @@ watch(isWindows, () => {
variant="default"
@click="openTerminal"
>
<Terminal class="h-4 w-4 mr-1" />
<Terminal class="size-4 mr-1" />
{{ t('extensions.ttyd.open') }}
</Button>
<Button
@@ -4237,7 +4237,7 @@ watch(isWindows, () => {
@click="startExtension('ttyd')"
:disabled="extensionsLoading"
>
<Play class="h-4 w-4 mr-1" />
<Play class="size-4 mr-1" />
{{ t('extensions.start') }}
</Button>
<Button
@@ -4247,7 +4247,7 @@ watch(isWindows, () => {
@click="stopExtension('ttyd')"
:disabled="extensionsLoading"
>
<Square class="h-4 w-4 mr-1" />
<Square class="size-4 mr-1" />
{{ t('extensions.stop') }}
</Button>
</div>
@@ -4267,11 +4267,11 @@ watch(isWindows, () => {
<!-- Logs -->
<div class="space-y-2">
<Collapsible v-model:open="showLogs.ttyd" @update:open="open => open && refreshExtensionLogs('ttyd')">
<CollapsibleTrigger as-child><Button type="button" variant="ghost" size="sm" class="gap-2 text-muted-foreground"><ChevronRight :class="['h-4 w-4 transition-transform', showLogs.ttyd ? 'rotate-90' : '']" />{{ t('extensions.viewLogs') }}</Button></CollapsibleTrigger>
<CollapsibleTrigger as-child><Button type="button" variant="ghost" size="sm" class="gap-2 text-muted-foreground"><ChevronRight :class="['size-4 transition-transform', showLogs.ttyd ? 'rotate-90' : '']" />{{ t('extensions.viewLogs') }}</Button></CollapsibleTrigger>
<CollapsibleContent class="space-y-2 pt-2">
<pre class="p-3 bg-muted rounded-md text-xs max-h-48 overflow-auto font-mono">{{ (extensionLogs.ttyd || []).join('\n') || t('extensions.noLogs') }}</pre>
<Button variant="ghost" size="sm" @click="refreshExtensionLogs('ttyd')">
<RefreshCw class="h-3 w-3 mr-1" />
<RefreshCw class="size-3 mr-1" />
{{ t('common.refresh') }}
</Button>
</CollapsibleContent>
@@ -4281,14 +4281,14 @@ watch(isWindows, () => {
</CardContent>
<CardFooter v-if="extensions?.ttyd?.available" class="border-t pt-4 justify-end">
<Button :disabled="loading || isExtRunning(extensions?.ttyd?.status)" @click="saveExtensionConfig('ttyd')">
<Loader2 v-if="loading" class="h-4 w-4 mr-2 animate-spin" /><Check v-else-if="saved" class="h-4 w-4 mr-2" /><Save v-else class="h-4 w-4 mr-2" />{{ loading ? t('actionbar.applying') : saved ? t('common.success') : t('common.save') }}
<Loader2 v-if="loading" class="size-4 mr-2 animate-spin" /><Check v-else-if="saved" class="size-4 mr-2" /><Save v-else class="size-4 mr-2" />{{ loading ? t('actionbar.applying') : saved ? t('common.success') : t('common.save') }}
</Button>
</CardFooter>
</Card>
</div>
<!-- Remote Access Section -->
<div v-show="activeSection === 'ext-remote-access'" class="space-y-6">
<div v-show="activeSection === 'ext-remote-access'" class="space-y-4">
<Card>
<CardHeader>
<div class="flex items-center justify-between">
@@ -4319,7 +4319,7 @@ watch(isWindows, () => {
@click="startExtension('gostc')"
:disabled="extensionsLoading || !!gostcValidationMessage"
>
<Play class="h-4 w-4 mr-1" />
<Play class="size-4 mr-1" />
{{ t('extensions.start') }}
</Button>
<Button
@@ -4329,7 +4329,7 @@ watch(isWindows, () => {
@click="stopExtension('gostc')"
:disabled="extensionsLoading"
>
<Square class="h-4 w-4 mr-1" />
<Square class="size-4 mr-1" />
{{ t('extensions.stop') }}
</Button>
</div>
@@ -4365,11 +4365,11 @@ watch(isWindows, () => {
<!-- Logs -->
<div class="space-y-2">
<Collapsible v-model:open="showLogs.gostc" @update:open="open => open && refreshExtensionLogs('gostc')">
<CollapsibleTrigger as-child><Button type="button" variant="ghost" size="sm" class="gap-2 text-muted-foreground"><ChevronRight :class="['h-4 w-4 transition-transform', showLogs.gostc ? 'rotate-90' : '']" />{{ t('extensions.viewLogs') }}</Button></CollapsibleTrigger>
<CollapsibleTrigger as-child><Button type="button" variant="ghost" size="sm" class="gap-2 text-muted-foreground"><ChevronRight :class="['size-4 transition-transform', showLogs.gostc ? 'rotate-90' : '']" />{{ t('extensions.viewLogs') }}</Button></CollapsibleTrigger>
<CollapsibleContent class="space-y-2 pt-2">
<pre class="p-3 bg-muted rounded-md text-xs max-h-48 overflow-auto font-mono">{{ (extensionLogs.gostc || []).join('\n') || t('extensions.noLogs') }}</pre>
<Button variant="ghost" size="sm" @click="refreshExtensionLogs('gostc')">
<RefreshCw class="h-3 w-3 mr-1" />
<RefreshCw class="size-3 mr-1" />
{{ t('common.refresh') }}
</Button>
</CollapsibleContent>
@@ -4379,7 +4379,7 @@ watch(isWindows, () => {
</CardContent>
<CardFooter v-if="extensions?.gostc?.available" class="border-t pt-4 justify-end">
<Button :disabled="loading || isExtRunning(extensions?.gostc?.status)" @click="saveExtensionConfig('gostc')">
<Loader2 v-if="loading" class="h-4 w-4 mr-2 animate-spin" /><Check v-else-if="saved" class="h-4 w-4 mr-2" /><Save v-else class="h-4 w-4 mr-2" />{{ loading ? t('actionbar.applying') : saved ? t('common.success') : t('common.save') }}
<Loader2 v-if="loading" class="size-4 mr-2 animate-spin" /><Check v-else-if="saved" class="size-4 mr-2" /><Save v-else class="size-4 mr-2" />{{ loading ? t('actionbar.applying') : saved ? t('common.success') : t('common.save') }}
</Button>
</CardFooter>
</Card>
@@ -4414,7 +4414,7 @@ watch(isWindows, () => {
@click="startExtension('easytier')"
:disabled="extensionsLoading || !!easytierValidationMessage"
>
<Play class="h-4 w-4 mr-1" />
<Play class="size-4 mr-1" />
{{ t('extensions.start') }}
</Button>
<Button
@@ -4424,7 +4424,7 @@ watch(isWindows, () => {
@click="stopExtension('easytier')"
:disabled="extensionsLoading"
>
<Square class="h-4 w-4 mr-1" />
<Square class="size-4 mr-1" />
{{ t('extensions.stop') }}
</Button>
</div>
@@ -4453,11 +4453,11 @@ watch(isWindows, () => {
<div v-for="(_, i) in extConfig.easytier.peer_urls" :key="i" class="flex gap-2">
<Input v-model="extConfig.easytier.peer_urls[i]" placeholder="tcp://1.2.3.4:11010" :disabled="isExtRunning(extensions?.easytier?.status)" />
<Button variant="ghost" size="icon" :aria-label="t('common.delete')" @click="removeEasytierPeer(i)" :disabled="isExtRunning(extensions?.easytier?.status)">
<Trash2 class="h-4 w-4" />
<Trash2 class="size-4" />
</Button>
</div>
<Button variant="outline" size="sm" @click="addEasytierPeer" :disabled="isExtRunning(extensions?.easytier?.status)">
<Plus class="h-4 w-4 mr-1" />
<Plus class="size-4 mr-1" />
{{ t('extensions.easytier.addPeer') }}
</Button>
</div>
@@ -4472,11 +4472,11 @@ watch(isWindows, () => {
<!-- Logs -->
<div class="space-y-2">
<Collapsible v-model:open="showLogs.easytier" @update:open="open => open && refreshExtensionLogs('easytier')">
<CollapsibleTrigger as-child><Button type="button" variant="ghost" size="sm" class="gap-2 text-muted-foreground"><ChevronRight :class="['h-4 w-4 transition-transform', showLogs.easytier ? 'rotate-90' : '']" />{{ t('extensions.viewLogs') }}</Button></CollapsibleTrigger>
<CollapsibleTrigger as-child><Button type="button" variant="ghost" size="sm" class="gap-2 text-muted-foreground"><ChevronRight :class="['size-4 transition-transform', showLogs.easytier ? 'rotate-90' : '']" />{{ t('extensions.viewLogs') }}</Button></CollapsibleTrigger>
<CollapsibleContent class="space-y-2 pt-2">
<pre class="p-3 bg-muted rounded-md text-xs max-h-48 overflow-auto font-mono">{{ (extensionLogs.easytier || []).join('\n') || t('extensions.noLogs') }}</pre>
<Button variant="ghost" size="sm" @click="refreshExtensionLogs('easytier')">
<RefreshCw class="h-3 w-3 mr-1" />
<RefreshCw class="size-3 mr-1" />
{{ t('common.refresh') }}
</Button>
</CollapsibleContent>
@@ -4486,7 +4486,7 @@ watch(isWindows, () => {
</CardContent>
<CardFooter v-if="extensions?.easytier?.available" class="border-t pt-4 justify-end">
<Button :disabled="loading || isExtRunning(extensions?.easytier?.status)" @click="saveExtensionConfig('easytier')">
<Loader2 v-if="loading" class="h-4 w-4 mr-2 animate-spin" /><Check v-else-if="saved" class="h-4 w-4 mr-2" /><Save v-else class="h-4 w-4 mr-2" />{{ loading ? t('actionbar.applying') : saved ? t('common.success') : t('common.save') }}
<Loader2 v-if="loading" class="size-4 mr-2 animate-spin" /><Check v-else-if="saved" class="size-4 mr-2" /><Save v-else class="size-4 mr-2" />{{ loading ? t('actionbar.applying') : saved ? t('common.success') : t('common.save') }}
</Button>
</CardFooter>
</Card>
@@ -4520,7 +4520,7 @@ watch(isWindows, () => {
@click="startExtension('frpc')"
:disabled="extensionsLoading || !!frpcValidationMessage"
>
<Play class="h-4 w-4 mr-1" />
<Play class="size-4 mr-1" />
{{ t('extensions.start') }}
</Button>
<Button
@@ -4530,7 +4530,7 @@ watch(isWindows, () => {
@click="stopExtension('frpc')"
:disabled="extensionsLoading"
>
<Square class="h-4 w-4 mr-1" />
<Square class="size-4 mr-1" />
{{ t('extensions.stop') }}
</Button>
</div>
@@ -4642,11 +4642,11 @@ watch(isWindows, () => {
</div>
<div class="space-y-2">
<Collapsible v-model:open="showLogs.frpc" @update:open="open => open && refreshExtensionLogs('frpc')">
<CollapsibleTrigger as-child><Button type="button" variant="ghost" size="sm" class="gap-2 text-muted-foreground"><ChevronRight :class="['h-4 w-4 transition-transform', showLogs.frpc ? 'rotate-90' : '']" />{{ t('extensions.viewLogs') }}</Button></CollapsibleTrigger>
<CollapsibleTrigger as-child><Button type="button" variant="ghost" size="sm" class="gap-2 text-muted-foreground"><ChevronRight :class="['size-4 transition-transform', showLogs.frpc ? 'rotate-90' : '']" />{{ t('extensions.viewLogs') }}</Button></CollapsibleTrigger>
<CollapsibleContent class="space-y-2 pt-2">
<pre class="p-3 bg-muted rounded-md text-xs max-h-48 overflow-auto font-mono">{{ (extensionLogs.frpc || []).join('\n') || t('extensions.noLogs') }}</pre>
<Button variant="ghost" size="sm" @click="refreshExtensionLogs('frpc')">
<RefreshCw class="h-3 w-3 mr-1" />
<RefreshCw class="size-3 mr-1" />
{{ t('common.refresh') }}
</Button>
</CollapsibleContent>
@@ -4656,14 +4656,14 @@ watch(isWindows, () => {
</CardContent>
<CardFooter v-if="extensions?.frpc?.available" class="border-t pt-4 justify-end">
<Button :disabled="loading || isExtRunning(extensions?.frpc?.status)" @click="saveExtensionConfig('frpc')">
<Loader2 v-if="loading" class="h-4 w-4 mr-2 animate-spin" /><Check v-else-if="saved" class="h-4 w-4 mr-2" /><Save v-else class="h-4 w-4 mr-2" />{{ loading ? t('actionbar.applying') : saved ? t('common.success') : t('common.save') }}
<Loader2 v-if="loading" class="size-4 mr-2 animate-spin" /><Check v-else-if="saved" class="size-4 mr-2" /><Save v-else class="size-4 mr-2" />{{ loading ? t('actionbar.applying') : saved ? t('common.success') : t('common.save') }}
</Button>
</CardFooter>
</Card>
</div>
<!-- RTSP Section -->
<div v-show="activeSection === 'third-party-access'" class="space-y-6">
<div v-show="activeSection === 'third-party-access'" class="space-y-4">
<Card>
<CardHeader>
<div class="flex items-center justify-between">
@@ -4675,8 +4675,8 @@ watch(isWindows, () => {
<Badge :variant="rtspStatus?.service_status === 'running' ? 'default' : 'secondary'">
{{ getRtspServiceStatusText(rtspStatus?.service_status) }}
</Badge>
<Button variant="ghost" size="icon" class="h-8 w-8" :aria-label="t('common.refresh')" @click="loadRtspConfig" :disabled="rtspLoading">
<RefreshCw :class="['h-4 w-4', rtspLoading ? 'animate-spin' : '']" />
<Button variant="ghost" size="icon-sm" :aria-label="t('common.refresh')" @click="loadRtspConfig" :disabled="rtspLoading">
<RefreshCw :class="['size-4', rtspLoading ? 'animate-spin' : '']" />
</Button>
</div>
</div>
@@ -4694,7 +4694,7 @@ watch(isWindows, () => {
@click="startRtsp"
:disabled="rtspLoading || rtspStatus?.service_status === 'starting'"
>
<Play class="h-4 w-4 mr-1" />
<Play class="size-4 mr-1" />
{{ t('extensions.start') }}
</Button>
<Button
@@ -4704,7 +4704,7 @@ watch(isWindows, () => {
@click="stopRtsp"
:disabled="rtspLoading"
>
<Square class="h-4 w-4 mr-1" />
<Square class="size-4 mr-1" />
{{ t('extensions.stop') }}
</Button>
</div>
@@ -4767,8 +4767,8 @@ watch(isWindows, () => {
:aria-label="showPasswords ? t('extensions.rustdesk.hidePassword') : t('extensions.rustdesk.showPassword')"
@click="showPasswords = !showPasswords"
>
<Eye v-if="!showPasswords" class="h-4 w-4" />
<EyeOff v-else class="h-4 w-4" />
<Eye v-if="!showPasswords" class="size-4" />
<EyeOff v-else class="size-4" />
</Button>
</div>
</div>
@@ -4782,14 +4782,14 @@ watch(isWindows, () => {
<code class="truncate font-mono">{{ rtspStreamUrl }}</code>
</div>
<Button class="shrink-0" :disabled="loading || rtspLoading" @click="saveRtspConfig">
<Loader2 v-if="loading" class="h-4 w-4 mr-2 animate-spin" /><Check v-else-if="saved" class="h-4 w-4 mr-2" /><Save v-else class="h-4 w-4 mr-2" />{{ loading ? t('actionbar.applying') : saved ? t('common.success') : t('common.save') }}
<Loader2 v-if="loading" class="size-4 mr-2 animate-spin" /><Check v-else-if="saved" class="size-4 mr-2" /><Save v-else class="size-4 mr-2" />{{ loading ? t('actionbar.applying') : saved ? t('common.success') : t('common.save') }}
</Button>
</CardFooter>
</Card>
</div>
<!-- VNC Section -->
<div v-show="activeSection === 'third-party-access'" class="space-y-6">
<div v-show="activeSection === 'third-party-access'" class="space-y-4">
<Card>
<CardHeader>
<div class="flex items-center justify-between">
@@ -4801,8 +4801,8 @@ watch(isWindows, () => {
<Badge :variant="vncStatus?.service_status === 'running' ? 'default' : 'secondary'">
{{ getVncServiceStatusText(vncStatus?.service_status) }}
</Badge>
<Button variant="ghost" size="icon" class="h-8 w-8" :aria-label="t('common.refresh')" @click="loadVncConfig" :disabled="vncLoading">
<RefreshCw :class="['h-4 w-4', vncLoading ? 'animate-spin' : '']" />
<Button variant="ghost" size="icon-sm" :aria-label="t('common.refresh')" @click="loadVncConfig" :disabled="vncLoading">
<RefreshCw :class="['size-4', vncLoading ? 'animate-spin' : '']" />
</Button>
</div>
</div>
@@ -4824,7 +4824,7 @@ watch(isWindows, () => {
@click="startVnc"
:disabled="vncLoading || vncStatus?.service_status === 'starting'"
>
<Play class="h-4 w-4 mr-1" />
<Play class="size-4 mr-1" />
{{ t('extensions.start') }}
</Button>
<Button
@@ -4834,7 +4834,7 @@ watch(isWindows, () => {
@click="stopVnc"
:disabled="vncLoading"
>
<Square class="h-4 w-4 mr-1" />
<Square class="size-4 mr-1" />
{{ t('extensions.stop') }}
</Button>
</div>
@@ -4889,8 +4889,8 @@ watch(isWindows, () => {
:aria-label="showPasswords ? t('extensions.rustdesk.hidePassword') : t('extensions.rustdesk.showPassword')"
@click="showPasswords = !showPasswords"
>
<Eye v-if="!showPasswords" class="h-4 w-4" />
<EyeOff v-else class="h-4 w-4" />
<Eye v-if="!showPasswords" class="size-4" />
<EyeOff v-else class="size-4" />
</Button>
</div>
</div>
@@ -4904,14 +4904,14 @@ watch(isWindows, () => {
<code class="truncate font-mono">{{ vncStreamUrl }}</code>
</div>
<Button class="shrink-0" :disabled="loading || vncLoading" @click="saveVncConfig">
<Loader2 v-if="loading" class="h-4 w-4 mr-2 animate-spin" /><Check v-else-if="saved" class="h-4 w-4 mr-2" /><Save v-else class="h-4 w-4 mr-2" />{{ loading ? t('actionbar.applying') : saved ? t('common.success') : t('common.save') }}
<Loader2 v-if="loading" class="size-4 mr-2 animate-spin" /><Check v-else-if="saved" class="size-4 mr-2" /><Save v-else class="size-4 mr-2" />{{ loading ? t('actionbar.applying') : saved ? t('common.success') : t('common.save') }}
</Button>
</CardFooter>
</Card>
</div>
<!-- RustDesk Section -->
<div v-show="activeSection === 'third-party-access'" class="space-y-6">
<div v-show="activeSection === 'third-party-access'" class="space-y-4">
<Card>
<CardHeader>
<div class="flex items-center justify-between">
@@ -4923,8 +4923,8 @@ watch(isWindows, () => {
<Badge :variant="rustdeskStatus?.service_status === 'running' ? 'default' : 'secondary'">
{{ getRustdeskServiceStatusText(rustdeskStatus?.service_status) }}
</Badge>
<Button variant="ghost" size="icon" class="h-8 w-8" :aria-label="t('common.refresh')" @click="loadRustdeskConfig" :disabled="rustdeskLoading">
<RefreshCw :class="['h-4 w-4', rustdeskLoading ? 'animate-spin' : '']" />
<Button variant="ghost" size="icon-sm" :aria-label="t('common.refresh')" @click="loadRustdeskConfig" :disabled="rustdeskLoading">
<RefreshCw :class="['size-4', rustdeskLoading ? 'animate-spin' : '']" />
</Button>
</div>
</div>
@@ -4948,7 +4948,7 @@ watch(isWindows, () => {
@click="startRustdesk"
:disabled="rustdeskLoading"
>
<Play class="h-4 w-4 mr-1" />
<Play class="size-4 mr-1" />
{{ t('extensions.start') }}
</Button>
<Button
@@ -4958,7 +4958,7 @@ watch(isWindows, () => {
@click="stopRustdesk"
:disabled="rustdeskLoading"
>
<Square class="h-4 w-4 mr-1" />
<Square class="size-4 mr-1" />
{{ t('extensions.stop') }}
</Button>
</div>
@@ -5022,8 +5022,8 @@ watch(isWindows, () => {
:aria-label="showPasswords ? t('extensions.rustdesk.hidePassword') : t('extensions.rustdesk.showPassword')"
@click="showPasswords = !showPasswords"
>
<Eye v-if="!showPasswords" class="h-4 w-4" />
<EyeOff v-else class="h-4 w-4" />
<Eye v-if="!showPasswords" class="size-4" />
<EyeOff v-else class="size-4" />
</Button>
</div>
</div>
@@ -5043,16 +5043,16 @@ watch(isWindows, () => {
<Button
variant="ghost"
size="icon"
class="h-8 w-8"
class="size-8"
:aria-label="t('extensions.rustdesk.copyId')"
@click="copyToClipboard(rustdeskConfig?.device_id || '', 'id')"
:disabled="!rustdeskConfig?.device_id"
>
<Check v-if="rustdeskCopied === 'id'" class="h-4 w-4 text-success" />
<Copy v-else class="h-4 w-4" />
<Check v-if="rustdeskCopied === 'id'" class="size-4 text-success" />
<Copy v-else class="size-4" />
</Button>
<Button variant="outline" size="sm" @click="regenerateRustdeskId" :disabled="rustdeskLoading">
<RefreshCw class="h-4 w-4 mr-1" />
<RefreshCw class="size-4 mr-1" />
{{ t('extensions.rustdesk.regenerateId') }}
</Button>
</div>
@@ -5066,16 +5066,16 @@ watch(isWindows, () => {
<Button
variant="ghost"
size="icon"
class="h-8 w-8"
class="size-8"
:aria-label="t('extensions.rustdesk.copyPassword')"
@click="copyToClipboard(rustdeskPassword?.device_password || '', 'password')"
:disabled="!rustdeskPassword?.device_password"
>
<Check v-if="rustdeskCopied === 'password'" class="h-4 w-4 text-success" />
<Copy v-else class="h-4 w-4" />
<Check v-if="rustdeskCopied === 'password'" class="size-4 text-success" />
<Copy v-else class="size-4" />
</Button>
<Button variant="outline" size="sm" @click="regenerateRustdeskPassword" :disabled="rustdeskLoading">
<RefreshCw class="h-4 w-4 mr-1" />
<RefreshCw class="size-4 mr-1" />
{{ t('extensions.rustdesk.regeneratePassword') }}
</Button>
</div>
@@ -5094,20 +5094,20 @@ watch(isWindows, () => {
</CardContent>
<CardFooter class="border-t pt-4 justify-end">
<Button :disabled="loading || rustdeskLoading" @click="saveRustdeskConfig">
<Loader2 v-if="loading" class="h-4 w-4 mr-2 animate-spin" /><Check v-else-if="saved" class="h-4 w-4 mr-2" /><Save v-else class="h-4 w-4 mr-2" />{{ loading ? t('actionbar.applying') : saved ? t('common.success') : t('common.save') }}
<Loader2 v-if="loading" class="size-4 mr-2 animate-spin" /><Check v-else-if="saved" class="size-4 mr-2" /><Save v-else class="size-4 mr-2" />{{ loading ? t('actionbar.applying') : saved ? t('common.success') : t('common.save') }}
</Button>
</CardFooter>
</Card>
</div>
<!-- Redfish Section -->
<div v-show="activeSection === 'third-party-access'" class="space-y-6">
<div v-show="activeSection === 'third-party-access'" class="space-y-4">
<!-- Auto-restart: restarting progress -->
<div
v-if="autoRestarting"
class="flex items-center gap-3 rounded-lg border bg-card px-4 py-3 text-sm shadow-sm"
>
<RefreshCw class="h-4 w-4 animate-spin text-primary shrink-0" />
<RefreshCw class="size-4 animate-spin text-primary shrink-0" />
<div class="flex-1 min-w-0">
<p class="font-medium">{{ t('settings.autoRestarting') }}</p>
<p class="text-xs text-muted-foreground">
@@ -5138,7 +5138,7 @@ watch(isWindows, () => {
<AlertDescription class="flex items-center justify-between gap-3">
<span>{{ t('settings.autoRestartFailed') }}</span>
<Button variant="outline" size="sm" class="text-foreground" @click="triggerAutoRestart">
<RefreshCw class="h-3 w-3 mr-1" />
<RefreshCw class="size-3 mr-1" />
{{ t('common.retry') }}
</Button>
</AlertDescription>
@@ -5161,12 +5161,12 @@ watch(isWindows, () => {
</CardContent>
<CardFooter class="flex items-center justify-between gap-3 border-t pt-4">
<p class="flex min-w-0 items-center gap-1.5 text-xs text-muted-foreground">
<AlertTriangle class="h-3.5 w-3.5 shrink-0 text-warning" />
<AlertTriangle class="size-3.5 shrink-0 text-warning" />
<span class="truncate">{{ t('settings.restartRequiredHint') }}</span>
</p>
<Button @click="saveRedfishConfig" :disabled="redfishSaving || autoRestarting">
<RefreshCw v-if="autoRestarting || redfishSaving" class="h-4 w-4 mr-2 animate-spin" />
<Save v-else class="h-4 w-4 mr-2" />
<RefreshCw v-if="autoRestarting || redfishSaving" class="size-4 mr-2 animate-spin" />
<Save v-else class="size-4 mr-2" />
{{ autoRestarting ? t('settings.restarting') : t('common.save') }}
</Button>
</CardFooter>
@@ -5174,7 +5174,7 @@ watch(isWindows, () => {
</div>
<!-- About Section -->
<div v-show="activeSection === 'about'" class="space-y-6">
<div v-show="activeSection === 'about'" class="space-y-4">
<Card>
<CardHeader class="flex flex-row items-start justify-between space-y-0">
<div class="space-y-1.5">
@@ -5184,12 +5184,12 @@ watch(isWindows, () => {
<Button
variant="ghost"
size="icon"
class="h-8 w-8"
class="size-8"
:aria-label="t('common.refresh')"
:disabled="updateRunning || updateLoading"
@click="loadUpdateOverview"
>
<RefreshCw :class="['h-4 w-4', (updateLoading || updateRunning) ? 'animate-spin' : '']" />
<RefreshCw :class="['size-4', (updateLoading || updateRunning) ? 'animate-spin' : '']" />
</Button>
</CardHeader>
<CardContent class="space-y-4">
@@ -5253,7 +5253,7 @@ watch(isWindows, () => {
:disabled="updateRunning || !updateOverview?.upgrade_available"
@click="startOnlineUpgrade"
>
<RefreshCw class="h-4 w-4 mr-2" :class="updateRunning ? 'animate-spin' : ''" />
<RefreshCw class="size-4 mr-2" :class="updateRunning ? 'animate-spin' : ''" />
{{ t('settings.startUpgrade') }}
</Button>
</div>
@@ -5267,21 +5267,21 @@ watch(isWindows, () => {
<div v-if="['video', 'hid'].includes(activeSection)" class="sticky bottom-0 pt-3 sm:pt-4 pb-3 bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/80 border-t -mx-3 px-3 sm:-mx-6 sm:px-6 lg:-mx-8 lg:px-8">
<div class="flex items-center justify-between gap-2 sm:gap-3">
<p v-if="activeSection === 'hid' && !isHidFunctionSelectionValid" class="flex min-w-0 items-center gap-1.5 text-xs text-warning">
<AlertTriangle class="h-3.5 w-3.5 shrink-0" />
<AlertTriangle class="size-3.5 shrink-0" />
<span class="truncate">{{ t('settings.otgFunctionMinWarning') }}</span>
</p>
<p v-else-if="activeSection === 'hid' && !isCh9329DescriptorValid" class="flex min-w-0 items-center gap-1.5 text-xs text-warning">
<AlertTriangle class="h-3.5 w-3.5 shrink-0" />
<AlertTriangle class="size-3.5 shrink-0" />
<span class="truncate">{{ t('settings.ch9329StringLengthWarning') }}</span>
</p>
<p v-else-if="activeSection === 'hid' && config.hid_backend === 'ch9329' && ch9329DescriptorLoading" class="flex min-w-0 items-center gap-1.5 text-xs text-warning">
<AlertTriangle class="h-3.5 w-3.5 shrink-0" />
<AlertTriangle class="size-3.5 shrink-0" />
<span class="truncate">{{ t('settings.ch9329DescriptorLoading') }}</span>
</p>
<p v-if="saveError" class="text-xs text-destructive">{{ saveError }}</p>
<p v-else class="text-xs text-muted-foreground hidden sm:block">{{ t('settings.unsavedChangesHint') }}</p>
<Button class="shrink-0 ml-auto" :disabled="loading || (activeSection === 'hid' && !isHidSettingsValid)" @click="saveConfig">
<Loader2 v-if="loading" class="h-4 w-4 mr-2 animate-spin" /><Check v-else-if="saved" class="h-4 w-4 mr-2" /><Save v-else class="h-4 w-4 mr-2" />{{ loading ? t('actionbar.applying') : saved ? t('common.success') : t('common.save') }}
<Loader2 v-if="loading" class="size-4 mr-2 animate-spin" /><Check v-else-if="saved" class="size-4 mr-2" /><Save v-else class="size-4 mr-2" />{{ loading ? t('actionbar.applying') : saved ? t('common.success') : t('common.save') }}
</Button>
</div>
</div>
@@ -5307,7 +5307,7 @@ watch(isWindows, () => {
{{ t('common.later') }}
</Button>
<Button @click="restartServer" :disabled="restarting">
<RefreshCw v-if="restarting" class="h-4 w-4 mr-2 animate-spin" />
<RefreshCw v-if="restarting" class="size-4 mr-2 animate-spin" />
{{ restarting ? t('settings.restarting') : t('common.restartNow') }}
</Button>
</DialogFooter>

View File

@@ -535,18 +535,18 @@ const stepIcons = [User, Video, Keyboard, Puzzle]
</script>
<template>
<div class="min-h-screen min-h-dvh flex items-start sm:items-center justify-center bg-background px-4 py-6 sm:py-10">
<div class="min-h-screen min-h-dvh flex items-start sm:items-center justify-center dot-grid-bg px-4 py-6 sm:py-10">
<Card class="w-full max-w-lg relative">
<!-- Language Switcher -->
<div class="absolute top-4 right-4">
<LanguageToggleButton />
</div>
<CardHeader class="text-center space-y-2 pt-10 sm:pt-12">
<CardHeader class="text-center space-y-2 pt-8">
<div class="mx-auto flex justify-center">
<BrandMark size="xl" />
<BrandMark size="lg" />
</div>
<CardTitle class="text-xl sm:text-2xl">{{ t('setup.welcome') }}</CardTitle>
<CardTitle class="text-xl">{{ t('setup.welcome') }}</CardTitle>
<CardDescription>{{ t('setup.description') }}</CardDescription>
</CardHeader>
@@ -807,7 +807,7 @@ const stepIcons = [User, Video, Keyboard, Puzzle]
{{ t('setup.advancedEncoder') }} ({{ t('common.optional') }})
</span>
<ChevronRight
class="h-4 w-4 transition-transform duration-200"
class="size-4 transition-transform duration-200"
:class="{ 'rotate-90': showAdvancedEncoder }"
/>
</Button>