mirror of
https://github.com/mofeng-git/One-KVM.git
synced 2026-09-12 10:34:24 +08:00
feat: 统一和完善 UI
This commit is contained in:
@@ -304,7 +304,7 @@ const hasRightOverflow = computed(() => {
|
||||
<Tooltip>
|
||||
<TooltipTrigger as-child>
|
||||
<Button variant="ghost" size="sm" class="h-8 gap-1.5 text-xs" @click="msdDialogOpen = true">
|
||||
<HardDrive class="h-4 w-4" />
|
||||
<HardDrive class="size-4" />
|
||||
<span v-if="visibleSet.get('msd') === 'label'">{{ t('actionbar.virtualMedia') }}</span>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
@@ -320,7 +320,7 @@ const hasRightOverflow = computed(() => {
|
||||
<Popover v-model:open="atxOpen">
|
||||
<PopoverTrigger as-child>
|
||||
<Button variant="ghost" size="sm" class="h-8 gap-1.5 text-xs">
|
||||
<Power class="h-4 w-4" />
|
||||
<Power class="size-4" />
|
||||
<span v-if="visibleSet.get('atx') === 'label'">{{ t('actionbar.power') }}</span>
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
@@ -341,7 +341,7 @@ const hasRightOverflow = computed(() => {
|
||||
<Popover v-model:open="pasteOpen">
|
||||
<PopoverTrigger as-child>
|
||||
<Button variant="ghost" size="sm" class="h-8 gap-1.5 text-xs">
|
||||
<ClipboardPaste class="h-4 w-4" />
|
||||
<ClipboardPaste class="size-4" />
|
||||
<span v-if="visibleSet.get('paste') === 'label'">{{ t('actionbar.paste') }}</span>
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
@@ -360,7 +360,7 @@ const hasRightOverflow = computed(() => {
|
||||
<Tooltip>
|
||||
<TooltipTrigger as-child>
|
||||
<Button variant="ghost" size="sm" class="h-8 gap-1.5 text-xs" @click="emit('toggleStats')">
|
||||
<BarChart3 class="h-4 w-4" />
|
||||
<BarChart3 class="size-4" />
|
||||
<span v-if="visibleSet.get('stats') === 'label'">{{ t('actionbar.stats') }}</span>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
@@ -383,7 +383,7 @@ const hasRightOverflow = computed(() => {
|
||||
:disabled="!props.ttydRunning"
|
||||
@click="emit('openTerminal')"
|
||||
>
|
||||
<Terminal class="h-4 w-4" />
|
||||
<Terminal class="size-4" />
|
||||
<span v-if="visibleSet.get('terminal') === 'label'">{{ t('actionbar.webTerminal') }}</span>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
@@ -401,10 +401,10 @@ const hasRightOverflow = computed(() => {
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
class="h-7 w-7 sm:h-8 sm:w-auto p-0 sm:px-2 sm:gap-1.5 text-xs"
|
||||
class="size-8 sm:w-auto p-0 sm:px-2 sm:gap-1.5 text-xs"
|
||||
@click="emit('openComputerUse')"
|
||||
>
|
||||
<Bot class="h-3.5 w-3.5 sm:h-4 sm:w-4" />
|
||||
<Bot class="size-3.5 sm:size-4" />
|
||||
<span class="hidden xl:inline">AI</span>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
@@ -420,7 +420,7 @@ const hasRightOverflow = computed(() => {
|
||||
<Tooltip>
|
||||
<TooltipTrigger as-child>
|
||||
<Button variant="ghost" size="sm" class="h-8 gap-1.5 text-xs" @click="router.push('/settings')">
|
||||
<Settings class="h-4 w-4" />
|
||||
<Settings class="size-4" />
|
||||
<span v-if="visibleSet.get('settings') === 'label'">{{ t('actionbar.settings') }}</span>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
@@ -444,10 +444,10 @@ const hasRightOverflow = computed(() => {
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
class="h-7 w-7 sm:h-8 sm:w-auto p-0 sm:px-2 sm:gap-1.5 text-xs"
|
||||
class="size-8 sm:w-auto p-0 sm:px-2 sm:gap-1.5 text-xs"
|
||||
@click="emit('toggleVirtualKeyboard')"
|
||||
>
|
||||
<Keyboard class="h-3.5 w-3.5 sm:h-4 sm:w-4" />
|
||||
<Keyboard class="size-3.5 sm:size-4" />
|
||||
<span class="hidden xl:inline">{{ t('actionbar.keyboard') }}</span>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
@@ -464,10 +464,10 @@ const hasRightOverflow = computed(() => {
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
class="h-7 w-7 sm:h-8 sm:w-auto p-0 sm:px-2 sm:gap-1.5 text-xs"
|
||||
class="size-8 sm:w-auto p-0 sm:px-2 sm:gap-1.5 text-xs"
|
||||
@click="emit('toggleFullscreen')"
|
||||
>
|
||||
<Maximize class="h-3.5 w-3.5 sm:h-4 sm:w-4" />
|
||||
<Maximize class="size-3.5 sm:size-4" />
|
||||
<span class="hidden xl:inline">{{ t('actionbar.fullscreen') }}</span>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
@@ -480,26 +480,26 @@ const hasRightOverflow = computed(() => {
|
||||
<!-- Overflow Menu - Only show if there are overflowed items -->
|
||||
<DropdownMenu v-if="hasOverflow" v-model:open="overflowMenuOpen">
|
||||
<DropdownMenuTrigger as-child>
|
||||
<Button variant="ghost" size="sm" class="h-7 w-7 sm:h-8 sm:w-8 p-0">
|
||||
<MoreHorizontal class="h-3.5 w-3.5 sm:h-4 sm:w-4" />
|
||||
<Button variant="ghost" size="sm" class="size-8 p-0">
|
||||
<MoreHorizontal class="size-3.5 sm:size-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" class="w-48">
|
||||
<!-- MSD -->
|
||||
<DropdownMenuItem v-if="showMsd && !isVisible('msd')" @click="openFromOverflow(() => msdDialogOpen = true)">
|
||||
<HardDrive class="h-4 w-4 mr-2" />
|
||||
<HardDrive class="size-4 mr-2" />
|
||||
{{ t('actionbar.virtualMedia') }}
|
||||
</DropdownMenuItem>
|
||||
|
||||
<!-- ATX -->
|
||||
<DropdownMenuItem v-if="showAtx && !isVisible('atx')" @click="openMobileAtx">
|
||||
<Power class="h-4 w-4 mr-2" />
|
||||
<Power class="size-4 mr-2" />
|
||||
{{ t('actionbar.power') }}
|
||||
</DropdownMenuItem>
|
||||
|
||||
<!-- Paste -->
|
||||
<DropdownMenuItem v-if="showPasteText && !isVisible('paste')" @click="openMobilePaste">
|
||||
<ClipboardPaste class="h-4 w-4 mr-2" />
|
||||
<ClipboardPaste class="size-4 mr-2" />
|
||||
{{ t('actionbar.paste') }}
|
||||
</DropdownMenuItem>
|
||||
|
||||
@@ -507,7 +507,7 @@ const hasRightOverflow = computed(() => {
|
||||
|
||||
<!-- Stats -->
|
||||
<DropdownMenuItem v-if="showStats && !isVisible('stats')" @click="openFromOverflow(() => emit('toggleStats'))">
|
||||
<BarChart3 class="h-4 w-4 mr-2" />
|
||||
<BarChart3 class="size-4 mr-2" />
|
||||
{{ t('actionbar.stats') }}
|
||||
</DropdownMenuItem>
|
||||
|
||||
@@ -517,13 +517,13 @@ const hasRightOverflow = computed(() => {
|
||||
:disabled="!props.ttydRunning"
|
||||
@click="openFromOverflow(() => emit('openTerminal'))"
|
||||
>
|
||||
<Terminal class="h-4 w-4 mr-2" />
|
||||
<Terminal class="size-4 mr-2" />
|
||||
{{ t('actionbar.webTerminal') }}
|
||||
</DropdownMenuItem>
|
||||
|
||||
<!-- Settings -->
|
||||
<DropdownMenuItem v-if="!isVisible('settings')" @click="openFromOverflow(() => router.push('/settings'))">
|
||||
<Settings class="h-4 w-4 mr-2" />
|
||||
<Settings class="size-4 mr-2" />
|
||||
{{ t('actionbar.settings') }}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
@@ -577,30 +577,30 @@ const hasRightOverflow = computed(() => {
|
||||
<div ref="measureRef" aria-hidden="true" class="fixed pointer-events-none" style="visibility: hidden; top: -9999px; left: -9999px; white-space: nowrap;">
|
||||
<div class="flex items-center gap-0.5 sm:gap-1.5 px-2 sm:px-4 py-1 sm:py-1.5">
|
||||
<!-- MSD -->
|
||||
<Button data-measure="msd-icon" variant="ghost" size="sm" class="h-8 gap-1.5 text-xs"><HardDrive class="h-4 w-4" /></Button>
|
||||
<Button data-measure="msd-label" variant="ghost" size="sm" class="h-8 gap-1.5 text-xs"><HardDrive class="h-4 w-4" />{{ t('actionbar.virtualMedia') }}</Button>
|
||||
<Button data-measure="msd-icon" variant="ghost" size="sm" class="h-8 gap-1.5 text-xs"><HardDrive class="size-4" /></Button>
|
||||
<Button data-measure="msd-label" variant="ghost" size="sm" class="h-8 gap-1.5 text-xs"><HardDrive class="size-4" />{{ t('actionbar.virtualMedia') }}</Button>
|
||||
<!-- ATX -->
|
||||
<Button data-measure="atx-icon" variant="ghost" size="sm" class="h-8 gap-1.5 text-xs"><Power class="h-4 w-4" /></Button>
|
||||
<Button data-measure="atx-label" variant="ghost" size="sm" class="h-8 gap-1.5 text-xs"><Power class="h-4 w-4" />{{ t('actionbar.power') }}</Button>
|
||||
<Button data-measure="atx-icon" variant="ghost" size="sm" class="h-8 gap-1.5 text-xs"><Power class="size-4" /></Button>
|
||||
<Button data-measure="atx-label" variant="ghost" size="sm" class="h-8 gap-1.5 text-xs"><Power class="size-4" />{{ t('actionbar.power') }}</Button>
|
||||
<!-- Paste -->
|
||||
<Button data-measure="paste-icon" variant="ghost" size="sm" class="h-8 gap-1.5 text-xs"><ClipboardPaste class="h-4 w-4" /></Button>
|
||||
<Button data-measure="paste-label" variant="ghost" size="sm" class="h-8 gap-1.5 text-xs"><ClipboardPaste class="h-4 w-4" />{{ t('actionbar.paste') }}</Button>
|
||||
<Button data-measure="paste-icon" variant="ghost" size="sm" class="h-8 gap-1.5 text-xs"><ClipboardPaste class="size-4" /></Button>
|
||||
<Button data-measure="paste-label" variant="ghost" size="sm" class="h-8 gap-1.5 text-xs"><ClipboardPaste class="size-4" />{{ t('actionbar.paste') }}</Button>
|
||||
<!-- Stats -->
|
||||
<Button data-measure="stats-icon" variant="ghost" size="sm" class="h-8 gap-1.5 text-xs"><BarChart3 class="h-4 w-4" /></Button>
|
||||
<Button data-measure="stats-label" variant="ghost" size="sm" class="h-8 gap-1.5 text-xs"><BarChart3 class="h-4 w-4" />{{ t('actionbar.stats') }}</Button>
|
||||
<Button data-measure="stats-icon" variant="ghost" size="sm" class="h-8 gap-1.5 text-xs"><BarChart3 class="size-4" /></Button>
|
||||
<Button data-measure="stats-label" variant="ghost" size="sm" class="h-8 gap-1.5 text-xs"><BarChart3 class="size-4" />{{ t('actionbar.stats') }}</Button>
|
||||
<!-- Web Terminal -->
|
||||
<Button data-measure="terminal-icon" variant="ghost" size="sm" class="h-8 gap-1.5 text-xs"><Terminal class="h-4 w-4" /></Button>
|
||||
<Button data-measure="terminal-label" variant="ghost" size="sm" class="h-8 gap-1.5 text-xs"><Terminal class="h-4 w-4" />{{ t('actionbar.webTerminal') }}</Button>
|
||||
<Button data-measure="terminal-icon" variant="ghost" size="sm" class="h-8 gap-1.5 text-xs"><Terminal class="size-4" /></Button>
|
||||
<Button data-measure="terminal-label" variant="ghost" size="sm" class="h-8 gap-1.5 text-xs"><Terminal class="size-4" />{{ t('actionbar.webTerminal') }}</Button>
|
||||
<!-- Settings -->
|
||||
<Button data-measure="settings-icon" variant="ghost" size="sm" class="h-8 gap-1.5 text-xs"><Settings class="h-4 w-4" /></Button>
|
||||
<Button data-measure="settings-label" variant="ghost" size="sm" class="h-8 gap-1.5 text-xs"><Settings class="h-4 w-4" />{{ t('actionbar.settings') }}</Button>
|
||||
<Button data-measure="settings-icon" variant="ghost" size="sm" class="h-8 gap-1.5 text-xs"><Settings class="size-4" /></Button>
|
||||
<Button data-measure="settings-label" variant="ghost" size="sm" class="h-8 gap-1.5 text-xs"><Settings class="size-4" />{{ t('actionbar.settings') }}</Button>
|
||||
<!-- Always-visible items (for measuring their actual width) -->
|
||||
<Button data-measure="video-icon" variant="ghost" size="sm" class="h-8 gap-1.5 text-xs"><HardDrive class="h-4 w-4" /></Button>
|
||||
<Button data-measure="video-label" variant="ghost" size="sm" class="h-8 gap-1.5 text-xs"><HardDrive class="h-4 w-4" /></Button>
|
||||
<Button data-measure="audio-icon" variant="ghost" size="sm" class="h-8 gap-1.5 text-xs"><HardDrive class="h-4 w-4" /></Button>
|
||||
<Button data-measure="audio-label" variant="ghost" size="sm" class="h-8 gap-1.5 text-xs"><HardDrive class="h-4 w-4" /></Button>
|
||||
<Button data-measure="hid-icon" variant="ghost" size="sm" class="h-8 gap-1.5 text-xs"><HardDrive class="h-4 w-4" /></Button>
|
||||
<Button data-measure="hid-label" variant="ghost" size="sm" class="h-8 gap-1.5 text-xs"><HardDrive class="h-4 w-4" /></Button>
|
||||
<Button data-measure="video-icon" variant="ghost" size="sm" class="h-8 gap-1.5 text-xs"><HardDrive class="size-4" /></Button>
|
||||
<Button data-measure="video-label" variant="ghost" size="sm" class="h-8 gap-1.5 text-xs"><HardDrive class="size-4" /></Button>
|
||||
<Button data-measure="audio-icon" variant="ghost" size="sm" class="h-8 gap-1.5 text-xs"><HardDrive class="size-4" /></Button>
|
||||
<Button data-measure="audio-label" variant="ghost" size="sm" class="h-8 gap-1.5 text-xs"><HardDrive class="size-4" /></Button>
|
||||
<Button data-measure="hid-icon" variant="ghost" size="sm" class="h-8 gap-1.5 text-xs"><HardDrive class="size-4" /></Button>
|
||||
<Button data-measure="hid-label" variant="ghost" size="sm" class="h-8 gap-1.5 text-xs"><HardDrive class="size-4" /></Button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -52,9 +52,9 @@ async function handleLogout() {
|
||||
</span>
|
||||
|
||||
<!-- Theme Toggle -->
|
||||
<Button variant="ghost" size="icon" class="h-8 w-8" :aria-label="t('common.toggleTheme')" @click="toggleTheme">
|
||||
<Sun class="h-4 w-4 rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
|
||||
<Moon class="absolute h-4 w-4 rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
|
||||
<Button variant="ghost" size="icon-sm" :aria-label="t('common.toggleTheme')" @click="toggleTheme">
|
||||
<Sun class="size-4 rotate-0 scale-100 transition-all dark:-rotate-90 dark:scale-0" />
|
||||
<Moon class="absolute size-4 rotate-90 scale-0 transition-all dark:rotate-0 dark:scale-100" />
|
||||
<span class="sr-only">{{ t('common.toggleTheme') }}</span>
|
||||
</Button>
|
||||
|
||||
@@ -64,21 +64,21 @@ async function handleLogout() {
|
||||
<!-- Mobile Menu -->
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger as-child class="md:hidden">
|
||||
<Button variant="ghost" size="icon" class="h-8 w-8" :aria-label="t('common.menu')">
|
||||
<Menu class="h-4 w-4" />
|
||||
<Button variant="ghost" size="icon-sm" :aria-label="t('common.menu')">
|
||||
<Menu class="size-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<DropdownMenuItem @click="handleLogout">
|
||||
<LogOut class="h-4 w-4 mr-2" />
|
||||
<LogOut class="size-4 mr-2" />
|
||||
{{ t('nav.logout') }}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
|
||||
<!-- Logout Button (Desktop) -->
|
||||
<Button variant="ghost" size="icon" class="hidden md:flex h-8 w-8" :aria-label="t('nav.logout')" @click="handleLogout">
|
||||
<LogOut class="h-4 w-4" />
|
||||
<Button variant="ghost" size="icon-sm" class="hidden md:flex" :aria-label="t('nav.logout')" @click="handleLogout">
|
||||
<LogOut class="size-4" />
|
||||
<span class="sr-only">{{ t('nav.logout') }}</span>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -30,7 +30,7 @@ const emit = defineEmits<{
|
||||
const { t } = useI18n()
|
||||
|
||||
const activeTab = ref('atx')
|
||||
const tabTriggerClass = 'h-9 rounded-md border-0 bg-transparent text-center text-xs text-muted-foreground shadow-none hover:text-foreground data-[state=active]:border-0 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm'
|
||||
const tabTriggerClass = 'h-8 rounded-md border-0 bg-transparent text-center text-xs text-muted-foreground shadow-none hover:text-foreground data-[state=active]:border-0 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm'
|
||||
|
||||
const powerState = ref<'on' | 'off' | 'unknown'>('unknown')
|
||||
const hddState = ref<'active' | 'inactive' | 'unknown'>('unknown')
|
||||
@@ -199,19 +199,19 @@ watch(
|
||||
<template>
|
||||
<div class="p-2.5 space-y-2.5">
|
||||
<Tabs v-model="activeTab">
|
||||
<TabsList class="grid h-auto w-full grid-cols-2 gap-1 rounded-md border border-border bg-muted p-1">
|
||||
<TabsList class="grid h-auto w-full grid-cols-2 gap-1 rounded-md border border-border bg-muted p-0.5">
|
||||
<TabsTrigger
|
||||
value="atx"
|
||||
:class="tabTriggerClass"
|
||||
>
|
||||
<Power class="h-3 w-3 mr-1" />
|
||||
<Power class="size-3 mr-1" />
|
||||
{{ t('atx.title') }}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="wol"
|
||||
:class="tabTriggerClass"
|
||||
>
|
||||
<Wifi class="h-3 w-3 mr-1" />
|
||||
<Wifi class="size-3 mr-1" />
|
||||
WOL
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
@@ -221,14 +221,14 @@ watch(
|
||||
<!-- Status -->
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
<div class="flex min-w-0 items-center gap-2 rounded-md border bg-muted/40 px-2 py-1.5">
|
||||
<Power :class="['h-4 w-4 shrink-0', powerStateIconColor]" />
|
||||
<Power :class="['size-4 shrink-0', powerStateIconColor]" />
|
||||
<div class="min-w-0">
|
||||
<p class="truncate text-[11px] leading-none text-muted-foreground">{{ t('atx.powerState') }}</p>
|
||||
<p :class="['mt-1 truncate text-xs font-medium leading-none', powerStateTextColor]">{{ powerStateText }}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex min-w-0 items-center gap-2 rounded-md border bg-muted/40 px-2 py-1.5">
|
||||
<HardDrive :class="['h-4 w-4 shrink-0', hddStateIconColor]" />
|
||||
<HardDrive :class="['size-4 shrink-0', hddStateIconColor]" />
|
||||
<div class="min-w-0">
|
||||
<p class="truncate text-[11px] leading-none text-muted-foreground">{{ t('atx.hddState') }}</p>
|
||||
<p :class="['mt-1 truncate text-xs font-medium leading-none', hddStateTextColor]">{{ hddStateText }}</p>
|
||||
@@ -245,12 +245,12 @@ watch(
|
||||
size="sm"
|
||||
:disabled="actionBusy"
|
||||
:class="[
|
||||
'w-full justify-start gap-2 h-7 text-xs',
|
||||
'w-full justify-start gap-2 h-8 text-xs',
|
||||
activeAction === 'short' ? 'bg-muted text-muted-foreground' : '',
|
||||
]"
|
||||
@click="handleAction('short')"
|
||||
>
|
||||
<Power class="h-3 w-3" />
|
||||
<Power class="size-3" />
|
||||
{{ t('atx.shortPress') }}
|
||||
</Button>
|
||||
|
||||
@@ -259,12 +259,12 @@ watch(
|
||||
size="sm"
|
||||
:disabled="actionBusy"
|
||||
:class="[
|
||||
'h-7 w-full justify-start gap-2 text-xs text-warning hover:bg-warning/10 hover:text-warning',
|
||||
'h-8 w-full justify-start gap-2 text-xs text-warning hover:bg-warning/10 hover:text-warning',
|
||||
activeAction === 'long' ? 'bg-muted text-muted-foreground hover:text-muted-foreground hover:bg-muted dark:hover:bg-muted' : '',
|
||||
]"
|
||||
@click="handleAction('long')"
|
||||
>
|
||||
<CircleDot class="h-3 w-3" />
|
||||
<CircleDot class="size-3" />
|
||||
{{ t('atx.longPress') }}
|
||||
</Button>
|
||||
|
||||
@@ -273,12 +273,12 @@ watch(
|
||||
size="sm"
|
||||
:disabled="actionBusy"
|
||||
:class="[
|
||||
'h-7 w-full justify-start gap-2 text-xs text-destructive hover:bg-destructive/10 hover:text-destructive',
|
||||
'h-8 w-full justify-start gap-2 text-xs text-destructive hover:bg-destructive/10 hover:text-destructive',
|
||||
activeAction === 'reset' ? 'bg-muted text-muted-foreground hover:text-muted-foreground hover:bg-muted dark:hover:bg-muted' : '',
|
||||
]"
|
||||
@click="handleAction('reset')"
|
||||
>
|
||||
<RotateCcw class="h-3 w-3" />
|
||||
<RotateCcw class="size-3" />
|
||||
{{ t('atx.reset') }}
|
||||
</Button>
|
||||
</div>
|
||||
@@ -302,7 +302,7 @@ watch(
|
||||
:disabled="!isValidMac || wolSending"
|
||||
@click="sendWol"
|
||||
>
|
||||
<Send class="h-3.5 w-3.5" />
|
||||
<Send class="size-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
<p v-if="wolMacAddress && !isValidMac" class="text-xs text-destructive">
|
||||
|
||||
@@ -142,8 +142,8 @@ watch(() => props.open, (isOpen) => {
|
||||
<template>
|
||||
<Popover :open="open" @update:open="emit('update:open', $event)">
|
||||
<PopoverTrigger as-child>
|
||||
<Button variant="ghost" size="sm" class="h-7 w-7 sm:h-8 sm:w-auto p-0 sm:px-2 sm:gap-1.5 text-xs">
|
||||
<Volume2 class="h-3.5 w-3.5 sm:h-4 sm:w-4" />
|
||||
<Button variant="ghost" size="sm" class="size-8 sm:w-auto p-0 sm:px-2 sm:gap-1.5 text-xs">
|
||||
<Volume2 class="size-3.5 sm:size-4" />
|
||||
<span class="hidden sm:inline">{{ t('actionbar.audioConfig') }}</span>
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
@@ -166,7 +166,7 @@ watch(() => props.open, (isOpen) => {
|
||||
<span class="text-xs font-mono">{{ Math.round(localVolume[0] ?? 0) }}%</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<Volume2 class="h-3.5 w-3.5 text-muted-foreground opacity-50" />
|
||||
<Volume2 class="size-3.5 text-muted-foreground opacity-50" />
|
||||
<Slider
|
||||
:model-value="localVolume"
|
||||
@update:model-value="handleVolumeChange"
|
||||
@@ -176,7 +176,7 @@ watch(() => props.open, (isOpen) => {
|
||||
:disabled="!systemStore.audio?.streaming"
|
||||
class="flex-1"
|
||||
/>
|
||||
<Volume2 class="h-3.5 w-3.5 text-muted-foreground" />
|
||||
<Volume2 class="size-3.5 text-muted-foreground" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -191,12 +191,11 @@ watch(() => props.open, (isOpen) => {
|
||||
</h5>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-6 w-6"
|
||||
size="icon-xs"
|
||||
:disabled="loadingDevices"
|
||||
@click="loadDevices"
|
||||
>
|
||||
<RefreshCw :class="['h-3.5 w-3.5', loadingDevices && 'animate-spin']" />
|
||||
<RefreshCw :class="['size-3.5', loadingDevices && 'animate-spin']" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -230,7 +229,7 @@ watch(() => props.open, (isOpen) => {
|
||||
:model-value="selectedDevice"
|
||||
@update:model-value="(v) => selectedDevice = v as string"
|
||||
:disabled="loadingDevices || devices.length === 0"
|
||||
class="h-8 w-full text-xs"
|
||||
size="sm" class="w-full text-xs"
|
||||
>
|
||||
<NativeSelectOption value="">{{ t('actionbar.selectAudioDevice') }}</NativeSelectOption>
|
||||
<NativeSelectOption
|
||||
@@ -281,7 +280,7 @@ watch(() => props.open, (isOpen) => {
|
||||
:disabled="applying"
|
||||
@click="applyConfig"
|
||||
>
|
||||
<Loader2 v-if="applying" class="h-3.5 w-3.5 mr-1.5 animate-spin" />
|
||||
<Loader2 v-if="applying" class="size-3.5 mr-1.5 animate-spin" />
|
||||
<span>{{ applying ? t('actionbar.applying') : t('common.apply') }}</span>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -13,17 +13,17 @@ const props = withDefaults(
|
||||
const dim = computed(() => {
|
||||
switch (props.size) {
|
||||
case 'xs':
|
||||
return 'h-4 w-4'
|
||||
return 'size-4'
|
||||
case 'sm':
|
||||
return 'h-5 w-5'
|
||||
return 'size-5'
|
||||
case 'md':
|
||||
return 'h-6 w-6'
|
||||
return 'size-6'
|
||||
case 'lg':
|
||||
return 'h-10 w-10'
|
||||
return 'size-10'
|
||||
case 'xl':
|
||||
return 'h-14 w-14'
|
||||
return 'size-14'
|
||||
default:
|
||||
return 'h-6 w-6'
|
||||
return 'size-6'
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
} from 'lucide-vue-next'
|
||||
import { toast } from 'vue-sonner'
|
||||
import { computerUseApi, type ComputerUseAction, type ComputerUseConfig, type ComputerUseSession } from '@/api'
|
||||
import { ApiError } from '@/api/request'
|
||||
import type { ComputerUseTimelineItem } from '@/types/computerUseTimeline'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Input } from '@/components/ui/input'
|
||||
@@ -107,6 +108,8 @@ async function loadConfig() {
|
||||
try {
|
||||
config.value = await computerUseApi.config()
|
||||
} catch (err) {
|
||||
// Auth failures are handled globally (redirect to login); don't toast over it.
|
||||
if (err instanceof ApiError && err.status === 401) return
|
||||
toast.error(t('computerUse.errors.configLoadFailed'), {
|
||||
description: err instanceof Error ? err.message : undefined,
|
||||
})
|
||||
@@ -217,15 +220,15 @@ onMounted(loadConfig)
|
||||
>
|
||||
<div class="flex h-full min-h-0 flex-col">
|
||||
<header class="flex h-12 shrink-0 items-center gap-2 border-b px-3">
|
||||
<Bot class="h-5 w-5 shrink-0" />
|
||||
<Bot class="size-5 shrink-0" />
|
||||
<div class="min-w-0 flex-1">
|
||||
<div class="truncate text-sm font-semibold">{{ t('computerUse.title') }}</div>
|
||||
<div
|
||||
class="flex min-w-0 items-center gap-1 text-[11px]"
|
||||
:class="connected ? 'text-emerald-600 dark:text-emerald-400' : 'text-muted-foreground'"
|
||||
>
|
||||
<Wifi v-if="connected" class="h-3 w-3 shrink-0" />
|
||||
<WifiOff v-else class="h-3 w-3 shrink-0" />
|
||||
<Wifi v-if="connected" class="size-3 shrink-0" />
|
||||
<WifiOff v-else class="size-3 shrink-0" />
|
||||
<span class="truncate">
|
||||
{{ connected ? t('computerUse.connection.connected') : (wsError || t('computerUse.connection.disconnected')) }}
|
||||
</span>
|
||||
@@ -235,39 +238,39 @@ onMounted(loadConfig)
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-8 w-8"
|
||||
class="size-8"
|
||||
:title="t('computerUse.buttons.settings')"
|
||||
:aria-label="t('computerUse.buttons.settings')"
|
||||
@click="settingsOpen = true"
|
||||
>
|
||||
<Settings class="h-4 w-4" />
|
||||
<Settings class="size-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-8 w-8"
|
||||
class="size-8"
|
||||
:title="t('computerUse.buttons.clear')"
|
||||
:aria-label="t('computerUse.buttons.clear')"
|
||||
:disabled="isRunning || timeline.length === 0"
|
||||
@click="emit('clear')"
|
||||
>
|
||||
<Trash2 class="h-4 w-4" />
|
||||
<Trash2 class="size-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-8 w-8"
|
||||
class="size-8"
|
||||
:title="t('computerUse.buttons.close')"
|
||||
:aria-label="t('computerUse.buttons.close')"
|
||||
@click="emit('update:open', false)"
|
||||
>
|
||||
<X class="h-4 w-4" />
|
||||
<X class="size-4" />
|
||||
</Button>
|
||||
</header>
|
||||
|
||||
<div ref="messagesRef" class="min-h-0 flex-1 overflow-y-auto px-4 py-4">
|
||||
<div v-if="timeline.length === 0" class="flex h-full items-center justify-center text-muted-foreground/45">
|
||||
<Bot class="h-10 w-10" />
|
||||
<Bot class="size-10" />
|
||||
</div>
|
||||
|
||||
<div v-else class="space-y-5">
|
||||
@@ -288,9 +291,9 @@ onMounted(loadConfig)
|
||||
class="flex w-full items-center gap-1.5 py-1 text-left font-medium hover:text-foreground"
|
||||
@click="toggleReasoning(item)"
|
||||
>
|
||||
<ChevronDown v-if="reasoningOpen(item)" class="h-3.5 w-3.5 shrink-0" />
|
||||
<ChevronRight v-else class="h-3.5 w-3.5 shrink-0" />
|
||||
<BrainCircuit class="h-3.5 w-3.5 shrink-0" />
|
||||
<ChevronDown v-if="reasoningOpen(item)" class="size-3.5 shrink-0" />
|
||||
<ChevronRight v-else class="size-3.5 shrink-0" />
|
||||
<BrainCircuit class="size-3.5 shrink-0" />
|
||||
<span>
|
||||
{{ item.failed
|
||||
? t('computerUse.reasoning.failed')
|
||||
@@ -308,7 +311,7 @@ onMounted(loadConfig)
|
||||
<div v-else-if="item.type === 'screenshot'" class="overflow-hidden rounded-md border bg-card">
|
||||
<div class="flex items-center justify-between border-b px-2.5 py-1.5 text-[11px] text-muted-foreground">
|
||||
<span class="inline-flex items-center gap-1.5">
|
||||
<Image class="h-3.5 w-3.5" />{{ t('computerUse.trace.screenshot') }}
|
||||
<Image class="size-3.5" />{{ t('computerUse.trace.screenshot') }}
|
||||
</span>
|
||||
<span>{{ item.screenshot.width }}x{{ item.screenshot.height }}</span>
|
||||
</div>
|
||||
@@ -363,24 +366,24 @@ onMounted(loadConfig)
|
||||
<Button
|
||||
v-if="!isRunning"
|
||||
size="icon"
|
||||
class="absolute bottom-2 right-2 h-8 w-8"
|
||||
class="absolute bottom-2 right-2 size-8"
|
||||
:title="t('computerUse.buttons.send')"
|
||||
:aria-label="t('computerUse.buttons.send')"
|
||||
:disabled="!canStart || starting"
|
||||
@click="start"
|
||||
>
|
||||
<SendHorizontal class="h-4 w-4" />
|
||||
<SendHorizontal class="size-4" />
|
||||
</Button>
|
||||
<Button
|
||||
v-else
|
||||
size="icon"
|
||||
variant="destructive"
|
||||
class="absolute bottom-2 right-2 h-8 w-8"
|
||||
class="absolute bottom-2 right-2 size-8"
|
||||
:title="t('computerUse.buttons.stop')"
|
||||
:aria-label="t('computerUse.buttons.stop')"
|
||||
@click="emit('stop')"
|
||||
>
|
||||
<Square class="h-3.5 w-3.5 fill-current" />
|
||||
<Square class="size-3.5 fill-current" />
|
||||
</Button>
|
||||
</div>
|
||||
<p v-if="config && !config.api_key_configured" class="mt-2 text-xs text-muted-foreground">
|
||||
@@ -422,7 +425,7 @@ onMounted(loadConfig)
|
||||
|
||||
<div class="space-y-1.5">
|
||||
<Label for="cua-key" class="flex items-center gap-1.5">
|
||||
<KeyRound class="h-3.5 w-3.5" />
|
||||
<KeyRound class="size-3.5" />
|
||||
{{ t('computerUse.settings.apiKey') }}
|
||||
</Label>
|
||||
<Input
|
||||
|
||||
@@ -25,9 +25,9 @@ defineProps<{
|
||||
variant="ghost"
|
||||
size="icon-xs"
|
||||
class="text-muted-foreground/60 hover:text-muted-foreground"
|
||||
:class="iconSize === 'sm' ? 'h-3.5 w-3.5' : 'h-4 w-4'"
|
||||
:class="iconSize === 'sm' ? 'size-3.5' : 'size-4'"
|
||||
>
|
||||
<HelpCircle :class="iconSize === 'sm' ? 'h-3.5 w-3.5' : 'h-4 w-4'" />
|
||||
<HelpCircle :class="iconSize === 'sm' ? 'size-3.5' : 'size-4'" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent
|
||||
|
||||
@@ -232,9 +232,9 @@ watch(() => props.open, (isOpen) => {
|
||||
<template>
|
||||
<Popover :open="open" @update:open="emit('update:open', $event)">
|
||||
<PopoverTrigger as-child>
|
||||
<Button variant="ghost" size="sm" class="h-7 w-7 sm:h-8 sm:w-auto p-0 sm:px-2 sm:gap-1.5 text-xs">
|
||||
<MousePointer v-if="mouseMode === 'absolute'" class="h-3.5 w-3.5 sm:h-4 sm:w-4" />
|
||||
<Move v-else class="h-3.5 w-3.5 sm:h-4 sm:w-4" />
|
||||
<Button variant="ghost" size="sm" class="size-8 sm:w-auto p-0 sm:px-2 sm:gap-1.5 text-xs">
|
||||
<MousePointer v-if="mouseMode === 'absolute'" class="size-3.5 sm:size-4" />
|
||||
<Move v-else class="size-3.5 sm:size-4" />
|
||||
<span class="hidden sm:inline">{{ buttonText }}</span>
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
@@ -261,7 +261,7 @@ watch(() => props.open, (isOpen) => {
|
||||
class="flex-1 h-8 text-xs"
|
||||
@click="toggleMouseMode"
|
||||
>
|
||||
<MousePointer class="h-3.5 w-3.5 mr-1" />
|
||||
<MousePointer class="size-3.5 mr-1" />
|
||||
{{ t('actionbar.absolute') }}
|
||||
</Button>
|
||||
<Button
|
||||
@@ -270,7 +270,7 @@ watch(() => props.open, (isOpen) => {
|
||||
class="flex-1 h-8 text-xs"
|
||||
@click="toggleMouseMode"
|
||||
>
|
||||
<Move class="h-3.5 w-3.5 mr-1" />
|
||||
<Move class="size-3.5 mr-1" />
|
||||
{{ t('actionbar.relative') }}
|
||||
</Button>
|
||||
</div>
|
||||
@@ -314,12 +314,11 @@ watch(() => props.open, (isOpen) => {
|
||||
<h5 class="text-xs font-medium text-muted-foreground">{{ t('actionbar.hidDeviceSettings') }}</h5>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-6 w-6"
|
||||
size="icon-xs"
|
||||
:disabled="loadingDevices"
|
||||
@click="loadDevices"
|
||||
>
|
||||
<RefreshCw :class="['h-3.5 w-3.5', loadingDevices && 'animate-spin']" />
|
||||
<RefreshCw :class="['size-3.5', loadingDevices && 'animate-spin']" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -330,7 +329,7 @@ watch(() => props.open, (isOpen) => {
|
||||
:model-value="hidBackend"
|
||||
@update:model-value="handleBackendChange"
|
||||
>
|
||||
<SelectTrigger class="h-8 w-full text-xs">
|
||||
<SelectTrigger size="sm" class="w-full text-xs">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -348,7 +347,7 @@ watch(() => props.open, (isOpen) => {
|
||||
:model-value="devicePath"
|
||||
@update:model-value="handleDevicePathChange"
|
||||
:disabled="availableDevicePaths.length === 0"
|
||||
class="h-8 w-full text-xs"
|
||||
size="sm" class="w-full text-xs"
|
||||
>
|
||||
<NativeSelectOption value="">{{ t('actionbar.selectDevice') }}</NativeSelectOption>
|
||||
<NativeSelectOption
|
||||
@@ -369,7 +368,7 @@ watch(() => props.open, (isOpen) => {
|
||||
:model-value="String(baudrate)"
|
||||
@update:model-value="handleBaudrateChange"
|
||||
>
|
||||
<SelectTrigger class="h-8 text-xs">
|
||||
<SelectTrigger size="sm" class="text-xs">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -388,7 +387,7 @@ watch(() => props.open, (isOpen) => {
|
||||
:disabled="applying"
|
||||
@click="applyHidConfig"
|
||||
>
|
||||
<Loader2 v-if="applying" class="h-3.5 w-3.5 mr-1.5 animate-spin" />
|
||||
<Loader2 v-if="applying" class="size-3.5 mr-1.5 animate-spin" />
|
||||
<span>{{ applying ? t('actionbar.applying') : t('common.apply') }}</span>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -48,13 +48,13 @@ const keysDisplay = computed(() => {
|
||||
<div class="flex items-center justify-between text-xs px-2 py-0.5">
|
||||
<div v-if="keyboardLedEnabled" class="flex items-center gap-1">
|
||||
<Badge variant="outline" class="h-4 gap-1 px-1 text-[10px] text-foreground">
|
||||
<span class="h-1.5 w-1.5 rounded-full" :class="capsLock ? 'bg-success' : 'bg-warning'" />C
|
||||
<span class="size-1.5 rounded-full" :class="capsLock ? 'bg-success' : 'bg-warning'" />C
|
||||
</Badge>
|
||||
<Badge variant="outline" class="h-4 gap-1 px-1 text-[10px] text-foreground">
|
||||
<span class="h-1.5 w-1.5 rounded-full" :class="numLock ? 'bg-success' : 'bg-warning'" />N
|
||||
<span class="size-1.5 rounded-full" :class="numLock ? 'bg-success' : 'bg-warning'" />N
|
||||
</Badge>
|
||||
<Badge variant="outline" class="h-4 gap-1 px-1 text-[10px] text-foreground">
|
||||
<span class="h-1.5 w-1.5 rounded-full" :class="scrollLock ? 'bg-success' : 'bg-warning'" />S
|
||||
<span class="size-1.5 rounded-full" :class="scrollLock ? 'bg-success' : 'bg-warning'" />S
|
||||
</Badge>
|
||||
</div>
|
||||
<div v-else class="text-[10px] text-muted-foreground/60">
|
||||
@@ -87,15 +87,15 @@ const keysDisplay = computed(() => {
|
||||
<Separator orientation="vertical" class="h-5" />
|
||||
<template v-if="keyboardLedEnabled">
|
||||
<Badge variant="outline" class="mx-1 gap-1.5 text-foreground">
|
||||
<span class="h-1.5 w-1.5 rounded-full" :class="capsLock ? 'bg-success' : 'bg-warning'" />
|
||||
<span class="size-1.5 rounded-full" :class="capsLock ? 'bg-success' : 'bg-warning'" />
|
||||
{{ t('infobar.caps') }}
|
||||
</Badge>
|
||||
<Badge variant="outline" class="mx-1 gap-1.5 text-foreground">
|
||||
<span class="h-1.5 w-1.5 rounded-full" :class="numLock ? 'bg-success' : 'bg-warning'" />
|
||||
<span class="size-1.5 rounded-full" :class="numLock ? 'bg-success' : 'bg-warning'" />
|
||||
{{ t('infobar.num') }}
|
||||
</Badge>
|
||||
<Badge variant="outline" class="mx-1 gap-1.5 text-foreground">
|
||||
<span class="h-1.5 w-1.5 rounded-full" :class="scrollLock ? 'bg-success' : 'bg-warning'" />
|
||||
<span class="size-1.5 rounded-full" :class="scrollLock ? 'bg-success' : 'bg-warning'" />
|
||||
{{ t('infobar.scroll') }}
|
||||
</Badge>
|
||||
</template>
|
||||
|
||||
@@ -43,7 +43,7 @@ function toggleLanguage() {
|
||||
:aria-label="t('common.toggleLanguage')"
|
||||
@click="toggleLanguage"
|
||||
>
|
||||
<Languages class="h-4 w-4" />
|
||||
<Languages class="size-4" />
|
||||
<span v-if="props.labelMode !== 'hidden'">{{ buttonLabel }}</span>
|
||||
<span class="sr-only">{{ t('common.toggleLanguage') }}</span>
|
||||
</Button>
|
||||
|
||||
@@ -153,8 +153,8 @@ const downloadProgress = ref<{
|
||||
} | null>(null)
|
||||
|
||||
const TWO_POINT_TWO_GB = 2.2 * 1024 * 1024 * 1024
|
||||
const tabTriggerClass = 'h-9 rounded-md border-0 bg-transparent text-center text-muted-foreground shadow-none hover:text-foreground data-[state=active]:border-0 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm'
|
||||
const segmentedGroupClass = 'grid w-full grid-cols-2 items-center gap-1 rounded-md border border-border bg-muted p-1'
|
||||
const tabTriggerClass = 'h-8 rounded-md border-0 bg-transparent text-center text-muted-foreground shadow-none hover:text-foreground data-[state=active]:border-0 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm'
|
||||
const segmentedGroupClass = 'grid w-full grid-cols-2 items-center gap-1 rounded-md border border-border bg-muted p-0.5'
|
||||
const segmentedItemClass = 'h-8 w-full justify-center rounded-md border-0 bg-transparent px-3 text-center text-xs text-muted-foreground shadow-none hover:bg-transparent hover:text-foreground data-[state=on]:border-0 data-[state=on]:bg-background data-[state=on]:text-foreground data-[state=on]:shadow-sm data-[state=on]:hover:bg-background'
|
||||
|
||||
const diskMode = computed(() => systemStore.msd?.diskMode ?? 'single')
|
||||
@@ -688,17 +688,17 @@ onUnmounted(() => {
|
||||
<TooltipProvider>
|
||||
<Dialog :open="open" @update:open="emit('update:open', $event)">
|
||||
<DialogContent class="max-h-[90vh] overflow-hidden p-0 sm:max-w-[760px] flex flex-col">
|
||||
<DialogHeader class="px-6 pt-6 shrink-0">
|
||||
<DialogHeader class="px-5 pt-4 shrink-0">
|
||||
<DialogTitle class="flex items-center gap-2">
|
||||
<HardDrive class="h-5 w-5" />
|
||||
<HardDrive class="size-5" />
|
||||
{{ t('msd.title') }}
|
||||
</DialogTitle>
|
||||
<DialogDescription as="div" class="mt-1 flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
|
||||
<span class="flex min-w-0 flex-wrap items-center gap-x-2 gap-y-1">
|
||||
<span :class="msdConnected ? 'text-success' : 'text-muted-foreground'" class="flex items-center gap-1.5">
|
||||
<span class="relative flex h-2 w-2">
|
||||
<span class="relative flex size-2">
|
||||
<span v-if="msdConnected" class="absolute inline-flex h-full w-full animate-ping rounded-full bg-success opacity-75"></span>
|
||||
<span :class="msdConnected ? 'bg-success' : 'bg-muted-foreground'" class="relative inline-flex h-2 w-2 rounded-full"></span>
|
||||
<span :class="msdConnected ? 'bg-success' : 'bg-muted-foreground'" class="relative inline-flex size-2 rounded-full"></span>
|
||||
</span>
|
||||
{{ msdConnected ? t('common.connected') : t('common.disconnected') }}
|
||||
</span>
|
||||
@@ -720,8 +720,8 @@ onUnmounted(() => {
|
||||
<span class="font-medium text-foreground">{{ t('msd.diskMode') }}</span>
|
||||
<Tooltip>
|
||||
<TooltipTrigger as-child>
|
||||
<span class="inline-flex h-4 w-4 items-center justify-center rounded-full text-muted-foreground hover:text-foreground">
|
||||
<Info class="h-3.5 w-3.5" />
|
||||
<span class="inline-flex size-4 items-center justify-center rounded-full text-muted-foreground hover:text-foreground">
|
||||
<Info class="size-3.5" />
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
@@ -747,15 +747,15 @@ onUnmounted(() => {
|
||||
|
||||
<Separator class="shrink-0" />
|
||||
|
||||
<div class="flex-1 min-h-0 flex flex-col px-6 pb-6 pt-4">
|
||||
<div class="flex-1 min-h-0 flex flex-col px-5 pb-4 pt-3">
|
||||
<Tabs v-model="activeTab" class="flex-1 flex flex-col min-h-0">
|
||||
<TabsList class="grid h-auto w-full shrink-0 grid-cols-2 gap-1 rounded-md border border-border bg-muted p-1">
|
||||
<TabsList class="grid h-auto w-full shrink-0 grid-cols-2 gap-1 rounded-md border border-border bg-muted p-0.5">
|
||||
<TabsTrigger value="images" :class="tabTriggerClass">
|
||||
<Disc class="h-4 w-4 mr-1.5" />
|
||||
<Disc class="size-4 mr-1.5" />
|
||||
{{ t('msd.images') }}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="drive" :class="tabTriggerClass">
|
||||
<HardDrive class="h-4 w-4 mr-1.5" />
|
||||
<HardDrive class="size-4 mr-1.5" />
|
||||
{{ t('msd.drive') }}
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
@@ -775,7 +775,7 @@ onUnmounted(() => {
|
||||
@change="handleImageUpload"
|
||||
/>
|
||||
<Button variant="outline" size="sm" as="span" class="h-8 cursor-pointer px-2.5 text-xs">
|
||||
<Upload class="mr-1.5 h-3.5 w-3.5" />
|
||||
<Upload class="mr-1.5 size-3.5" />
|
||||
{{ t('msd.uploadImage') }}
|
||||
</Button>
|
||||
</label>
|
||||
@@ -785,11 +785,11 @@ onUnmounted(() => {
|
||||
class="h-8 px-2.5 text-xs"
|
||||
@click="showUrlDialog = true"
|
||||
>
|
||||
<Globe class="mr-1.5 h-3.5 w-3.5" />
|
||||
<Globe class="mr-1.5 size-3.5" />
|
||||
{{ t('msd.downloadFromUrl') }}
|
||||
</Button>
|
||||
<Button variant="ghost" size="icon" class="h-8 w-8" @click="loadImages">
|
||||
<RefreshCw class="h-3.5 w-3.5" :class="{ 'animate-spin': loadingImages }" />
|
||||
<Button variant="ghost" size="icon-sm" @click="loadImages">
|
||||
<RefreshCw class="size-3.5" :class="{ 'animate-spin': loadingImages }" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -817,8 +817,8 @@ onUnmounted(() => {
|
||||
>
|
||||
<div class="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div class="flex min-w-0 flex-1 items-start gap-2">
|
||||
<span v-if="mountedImage(image.id)" class="mt-1.5 h-2 w-2 shrink-0 rounded-full bg-primary" />
|
||||
<Disc v-else class="mt-0.5 h-4 w-4 shrink-0 text-muted-foreground" />
|
||||
<span v-if="mountedImage(image.id)" class="mt-1.5 size-2 shrink-0 rounded-full bg-primary" />
|
||||
<Disc v-else class="mt-0.5 size-4 shrink-0 text-muted-foreground" />
|
||||
<div class="w-0 flex-1">
|
||||
<Tooltip>
|
||||
<TooltipTrigger as-child>
|
||||
@@ -836,7 +836,7 @@ onUnmounted(() => {
|
||||
variant="outline"
|
||||
class="h-4 cursor-help border-warning/50 px-1.5 text-[10px] text-warning"
|
||||
>
|
||||
<AlertCircle class="h-2.5 w-2.5 mr-0.5" />
|
||||
<AlertCircle class="size-2.5 mr-0.5" />
|
||||
{{ t('msd.largeFileWarning') }}
|
||||
</Badge>
|
||||
</TooltipTrigger>
|
||||
@@ -856,7 +856,7 @@ onUnmounted(() => {
|
||||
:disabled="operationInProgress"
|
||||
@click="unmountImageById(image.id)"
|
||||
>
|
||||
<Unlink class="h-3.5 w-3.5 mr-1" />
|
||||
<Unlink class="size-3.5 mr-1" />
|
||||
{{ t('msd.disconnect') }}
|
||||
</Button>
|
||||
</template>
|
||||
@@ -868,7 +868,7 @@ onUnmounted(() => {
|
||||
:disabled="operationInProgress || mediaSlotsFull"
|
||||
@click="connectImage(image)"
|
||||
>
|
||||
<Link v-if="!connecting" class="h-3.5 w-3.5 mr-1" />
|
||||
<Link v-if="!connecting" class="size-3.5 mr-1" />
|
||||
<span v-if="connecting">{{ t('common.connecting') }}...</span>
|
||||
<span v-else>{{ t('msd.connect') }}</span>
|
||||
</Button>
|
||||
@@ -876,11 +876,11 @@ onUnmounted(() => {
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-8 w-8 text-muted-foreground hover:bg-destructive/10 hover:text-destructive"
|
||||
class="size-8 text-muted-foreground hover:bg-destructive/10 hover:text-destructive"
|
||||
:disabled="operationInProgress || !!mountedImage(image.id)"
|
||||
@click="confirmDelete('image', image.id, image.name)"
|
||||
>
|
||||
<Trash2 class="h-3.5 w-3.5" />
|
||||
<Trash2 class="size-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -900,7 +900,7 @@ onUnmounted(() => {
|
||||
<TabsContent value="drive" class="m-0 flex min-h-0 flex-1 flex-col space-y-4 pt-3">
|
||||
<template v-if="!driveInitialized">
|
||||
<div class="shrink-0 text-center py-8 space-y-4">
|
||||
<HardDrive class="h-10 w-10 mx-auto text-muted-foreground" />
|
||||
<HardDrive class="size-10 mx-auto text-muted-foreground" />
|
||||
<p class="text-sm text-muted-foreground">{{ t('msd.driveNotInitialized') }}</p>
|
||||
<Button size="sm" @click="initializeDrive">
|
||||
{{ t('msd.initializeDrive') }}
|
||||
@@ -920,7 +920,7 @@ onUnmounted(() => {
|
||||
>
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center gap-2">
|
||||
<HardDrive class="h-4 w-4 text-muted-foreground" />
|
||||
<HardDrive class="size-4 text-muted-foreground" />
|
||||
<span class="text-sm font-medium">{{ t('msd.drive') }}</span>
|
||||
<!-- Show size badge only when info is available -->
|
||||
<Badge v-if="driveInfo" variant="outline" class="text-xs">
|
||||
@@ -933,8 +933,8 @@ onUnmounted(() => {
|
||||
</Badge>
|
||||
<Tooltip>
|
||||
<TooltipTrigger as-child>
|
||||
<span class="inline-flex h-4 w-4 items-center justify-center text-muted-foreground hover:text-foreground">
|
||||
<Info class="h-3.5 w-3.5" />
|
||||
<span class="inline-flex size-4 items-center justify-center text-muted-foreground hover:text-foreground">
|
||||
<Info class="size-3.5" />
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
@@ -958,9 +958,9 @@ onUnmounted(() => {
|
||||
</template>
|
||||
<template v-else-if="driveConnectedToTarget">
|
||||
<Badge variant="default" class="h-8 px-2 text-xs">
|
||||
<span class="relative flex h-1.5 w-1.5 mr-1.5">
|
||||
<span class="relative flex size-1.5 mr-1.5">
|
||||
<span class="absolute inline-flex h-full w-full animate-ping rounded-full bg-primary-foreground opacity-75"></span>
|
||||
<span class="relative inline-flex h-1.5 w-1.5 rounded-full bg-primary-foreground"></span>
|
||||
<span class="relative inline-flex size-1.5 rounded-full bg-primary-foreground"></span>
|
||||
</span>
|
||||
{{ t('common.connected') }}
|
||||
</Badge>
|
||||
@@ -972,7 +972,7 @@ onUnmounted(() => {
|
||||
:disabled="operationInProgress"
|
||||
@click="unmountMedia(driveMedia)"
|
||||
>
|
||||
<Unlink class="h-3.5 w-3.5 mr-1" />
|
||||
<Unlink class="size-3.5 mr-1" />
|
||||
{{ t('msd.disconnect') }}
|
||||
</Button>
|
||||
</template>
|
||||
@@ -984,7 +984,7 @@ onUnmounted(() => {
|
||||
:disabled="operationInProgress || mediaSlotsFull || !!driveError"
|
||||
@click="connectDrive"
|
||||
>
|
||||
<Link v-if="!connecting" class="h-3.5 w-3.5 mr-1" />
|
||||
<Link v-if="!connecting" class="size-3.5 mr-1" />
|
||||
<span v-if="connecting">{{ t('common.connecting') }}...</span>
|
||||
<span v-else>{{ t('msd.connect') }}</span>
|
||||
</Button>
|
||||
@@ -992,11 +992,11 @@ onUnmounted(() => {
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-8 w-8 text-muted-foreground hover:bg-destructive/10 hover:text-destructive"
|
||||
class="size-8 text-muted-foreground hover:bg-destructive/10 hover:text-destructive"
|
||||
:disabled="operationInProgress || driveConnectedToTarget"
|
||||
@click="showDeleteDriveDialog = true"
|
||||
>
|
||||
<Trash2 class="h-3.5 w-3.5" />
|
||||
<Trash2 class="size-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1024,15 +1024,15 @@ onUnmounted(() => {
|
||||
v-if="currentPath !== '/'"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-8 w-8 shrink-0"
|
||||
class="size-8 shrink-0"
|
||||
:disabled="driveConnectedToTarget"
|
||||
@click="navigateUp"
|
||||
>
|
||||
<ArrowLeft class="h-3.5 w-3.5" />
|
||||
<ArrowLeft class="size-3.5" />
|
||||
</Button>
|
||||
<nav class="flex items-center text-xs min-w-0 overflow-hidden">
|
||||
<template v-for="(crumb, index) in breadcrumbs" :key="crumb.path">
|
||||
<ChevronRight v-if="index > 0" class="h-3 w-3 text-muted-foreground mx-0.5 shrink-0" />
|
||||
<ChevronRight v-if="index > 0" class="size-3 text-muted-foreground mx-0.5 shrink-0" />
|
||||
<Button
|
||||
variant="link"
|
||||
size="sm"
|
||||
@@ -1059,10 +1059,10 @@ onUnmounted(() => {
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
as="span"
|
||||
class="h-8 w-8"
|
||||
class="size-8"
|
||||
:class="driveConnectedToTarget ? 'cursor-not-allowed opacity-40' : 'cursor-pointer'"
|
||||
>
|
||||
<Upload class="h-3.5 w-3.5" />
|
||||
<Upload class="size-3.5" />
|
||||
</Button>
|
||||
</label>
|
||||
</TooltipTrigger>
|
||||
@@ -1076,11 +1076,11 @@ onUnmounted(() => {
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-8 w-8"
|
||||
class="size-8"
|
||||
:disabled="driveConnectedToTarget"
|
||||
@click="showNewFolderDialog = true"
|
||||
>
|
||||
<FolderPlus class="h-3.5 w-3.5" />
|
||||
<FolderPlus class="size-3.5" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent v-if="driveConnectedToTarget">
|
||||
@@ -1090,11 +1090,11 @@ onUnmounted(() => {
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-8 w-8"
|
||||
class="size-8"
|
||||
:disabled="driveConnectedToTarget"
|
||||
@click="loadDriveFiles"
|
||||
>
|
||||
<RefreshCw class="h-3.5 w-3.5" :class="{ 'animate-spin': loadingDrive }" />
|
||||
<RefreshCw class="size-3.5" :class="{ 'animate-spin': loadingDrive }" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1129,8 +1129,8 @@ onUnmounted(() => {
|
||||
class="flex items-center gap-2 cursor-pointer flex-1 min-w-0"
|
||||
@click="file.is_dir && navigateTo(file.path)"
|
||||
>
|
||||
<Folder v-if="file.is_dir" class="h-4 w-4 shrink-0 text-info" />
|
||||
<File v-else class="h-4 w-4 text-muted-foreground shrink-0" />
|
||||
<Folder v-if="file.is_dir" class="size-4 shrink-0 text-info" />
|
||||
<File v-else class="size-4 text-muted-foreground shrink-0" />
|
||||
<div class="min-w-0">
|
||||
<Tooltip>
|
||||
<TooltipTrigger as-child>
|
||||
@@ -1150,22 +1150,22 @@ onUnmounted(() => {
|
||||
v-if="!file.is_dir"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-8 w-8"
|
||||
class="size-8"
|
||||
as="a"
|
||||
:href="msdApi.downloadDriveFile(file.path)"
|
||||
download
|
||||
>
|
||||
<Download class="h-3.5 w-3.5" />
|
||||
<Download class="size-3.5" />
|
||||
</Button>
|
||||
<!-- ③ Delete disabled when drive connected to target -->
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-8 w-8 text-muted-foreground hover:bg-destructive/10 hover:text-destructive"
|
||||
class="size-8 text-muted-foreground hover:bg-destructive/10 hover:text-destructive"
|
||||
:disabled="driveConnectedToTarget"
|
||||
@click="confirmDelete('file', file.path, file.name)"
|
||||
>
|
||||
<Trash2 class="h-3.5 w-3.5" />
|
||||
<Trash2 class="size-3.5" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1339,7 +1339,7 @@ onUnmounted(() => {
|
||||
{{ t('common.cancel') }}
|
||||
</Button>
|
||||
<Button @click="confirmImageMount" :disabled="connecting || mediaSlotsFull">
|
||||
<Link v-if="!connecting" class="h-4 w-4 mr-1" />
|
||||
<Link v-if="!connecting" class="size-4 mr-1" />
|
||||
<span v-if="connecting">{{ t('common.connecting') }}...</span>
|
||||
<span v-else>{{ t('msd.connect') }}</span>
|
||||
</Button>
|
||||
@@ -1366,7 +1366,7 @@ onUnmounted(() => {
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle class="flex items-center gap-2">
|
||||
<Globe class="h-5 w-5" />
|
||||
<Globe class="size-5" />
|
||||
{{ t('msd.downloadFromUrl') }}
|
||||
</DialogTitle>
|
||||
<DialogDescription>{{ t('msd.downloadFromUrlDesc') }}</DialogDescription>
|
||||
@@ -1425,11 +1425,11 @@ onUnmounted(() => {
|
||||
:disabled="!downloadUrl.trim()"
|
||||
@click="startUrlDownload"
|
||||
>
|
||||
<Download class="h-4 w-4 mr-1" />
|
||||
<Download class="size-4 mr-1" />
|
||||
{{ t('msd.download') }}
|
||||
</Button>
|
||||
<Button v-else variant="destructive" @click="cancelUrlDownload">
|
||||
<X class="h-4 w-4 mr-1" />
|
||||
<X class="size-4 mr-1" />
|
||||
{{ t('common.cancel') }}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
|
||||
@@ -205,7 +205,7 @@ function handleKeydown(e: KeyboardEvent) {
|
||||
|
||||
<!-- Warning for untypable characters -->
|
||||
<Alert v-if="hasUntypableChars && !isPasting" variant="warning">
|
||||
<AlertCircle class="h-4 w-4 shrink-0 mt-0.5" />
|
||||
<AlertCircle class="size-4 shrink-0 mt-0.5" />
|
||||
<AlertTitle>{{ t('paste.untypableWarning') }}</AlertTitle>
|
||||
<AlertDescription>
|
||||
{{ t('paste.untypableChars', { chars: textAnalysis?.untypableChars.slice(0, 5).map(c => c === '\n' ? '\\n' : c === '\r' ? '\\r' : c === '\t' ? '\\t' : c).join(', ') }) }}
|
||||
@@ -234,7 +234,7 @@ function handleKeydown(e: KeyboardEvent) {
|
||||
{{ t('common.cancel') }}
|
||||
</Button>
|
||||
<Button v-else variant="ghost" size="sm" @click="cancelPaste">
|
||||
<Square class="h-3 w-3 mr-1.5 fill-current" />
|
||||
<Square class="size-3 mr-1.5 fill-current" />
|
||||
{{ t('paste.stop') }}
|
||||
</Button>
|
||||
<Button
|
||||
@@ -242,7 +242,7 @@ function handleKeydown(e: KeyboardEvent) {
|
||||
:disabled="!text.trim() || isPasting"
|
||||
@click="handlePaste"
|
||||
>
|
||||
<CornerDownLeft class="h-4 w-4 mr-1.5" />
|
||||
<CornerDownLeft class="size-4 mr-1.5" />
|
||||
{{ t('paste.confirm') }}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -141,7 +141,7 @@ const statusBadgeText = computed(() => {
|
||||
<template v-if="compact">
|
||||
<!-- Compact: single row with dot + abbreviated title -->
|
||||
<div class="flex items-center gap-1">
|
||||
<span :class="cn('h-1.5 w-1.5 rounded-full shrink-0', statusColor)" />
|
||||
<span :class="cn('size-1.5 rounded-full shrink-0', statusColor)" />
|
||||
<span class="text-[10px] text-muted-foreground leading-tight truncate">{{ title }}</span>
|
||||
</div>
|
||||
</template>
|
||||
@@ -150,7 +150,7 @@ const statusBadgeText = computed(() => {
|
||||
<span class="font-medium text-foreground text-xs truncate">{{ title }}</span>
|
||||
<!-- Bottom: Status dot + Quick info -->
|
||||
<div class="flex items-center gap-1.5">
|
||||
<span :class="cn('h-2 w-2 rounded-full shrink-0', statusColor)" />
|
||||
<span :class="cn('size-2 rounded-full shrink-0', statusColor)" />
|
||||
<span class="text-[11px] text-muted-foreground leading-tight truncate">
|
||||
{{ quickInfo || subtitle || statusText }}
|
||||
</span>
|
||||
@@ -169,7 +169,7 @@ const statusBadgeText = computed(() => {
|
||||
status === 'error' ? 'bg-destructive/10 text-destructive' :
|
||||
'bg-muted text-muted-foreground'
|
||||
)">
|
||||
<component :is="StatusIcon" class="h-5 w-5" />
|
||||
<component :is="StatusIcon" class="size-5" />
|
||||
</div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<h4 class="font-semibold text-sm">{{ title }}</h4>
|
||||
@@ -178,7 +178,7 @@ const statusBadgeText = computed(() => {
|
||||
v-if="statusIcon"
|
||||
:is="statusIcon"
|
||||
:class="cn(
|
||||
'h-3.5 w-3.5',
|
||||
'size-3.5',
|
||||
status === 'connected' ? 'text-success' :
|
||||
status === 'connecting' ? 'text-warning animate-spin' :
|
||||
status === 'error' ? 'text-destructive' :
|
||||
@@ -229,7 +229,7 @@ const statusBadgeText = computed(() => {
|
||||
<template v-if="compact">
|
||||
<!-- Compact: single row with dot + abbreviated title -->
|
||||
<div class="flex items-center gap-1">
|
||||
<span :class="cn('h-1.5 w-1.5 rounded-full shrink-0', statusColor)" />
|
||||
<span :class="cn('size-1.5 rounded-full shrink-0', statusColor)" />
|
||||
<span class="text-[10px] text-muted-foreground leading-tight truncate">{{ title }}</span>
|
||||
</div>
|
||||
</template>
|
||||
@@ -238,7 +238,7 @@ const statusBadgeText = computed(() => {
|
||||
<span class="font-medium text-foreground text-xs truncate">{{ title }}</span>
|
||||
<!-- Bottom: Status dot + Quick info -->
|
||||
<div class="flex items-center gap-1.5">
|
||||
<span :class="cn('h-2 w-2 rounded-full shrink-0', statusColor)" />
|
||||
<span :class="cn('size-2 rounded-full shrink-0', statusColor)" />
|
||||
<span class="text-[11px] text-muted-foreground leading-tight truncate">
|
||||
{{ quickInfo || subtitle || statusText }}
|
||||
</span>
|
||||
@@ -257,7 +257,7 @@ const statusBadgeText = computed(() => {
|
||||
status === 'error' ? 'bg-destructive/10 text-destructive' :
|
||||
'bg-muted text-muted-foreground'
|
||||
)">
|
||||
<component :is="StatusIcon" class="h-5 w-5" />
|
||||
<component :is="StatusIcon" class="size-5" />
|
||||
</div>
|
||||
<div class="flex-1 min-w-0">
|
||||
<h4 class="font-semibold text-sm">{{ title }}</h4>
|
||||
@@ -266,7 +266,7 @@ const statusBadgeText = computed(() => {
|
||||
v-if="statusIcon"
|
||||
:is="statusIcon"
|
||||
:class="cn(
|
||||
'h-3.5 w-3.5',
|
||||
'size-3.5',
|
||||
status === 'connected' ? 'text-success' :
|
||||
status === 'connecting' ? 'text-warning animate-spin' :
|
||||
status === 'error' ? 'text-destructive' :
|
||||
|
||||
@@ -70,23 +70,22 @@ function hideFrameScrollbars(event: Event) {
|
||||
<DialogHeader class="px-3 sm:px-4 py-1 border-b shrink-0">
|
||||
<DialogTitle class="flex h-8 items-center justify-between w-full">
|
||||
<div class="flex items-center gap-2 min-w-0">
|
||||
<Terminal class="h-4 w-4 shrink-0" />
|
||||
<Terminal class="size-4 shrink-0" />
|
||||
<span class="truncate text-sm font-semibold">{{ t('extensions.ttyd.title') }}</span>
|
||||
</div>
|
||||
<div class="flex items-center gap-1 shrink-0">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-7 w-7"
|
||||
size="icon-sm"
|
||||
@click="openTerminalInNewTab"
|
||||
:aria-label="t('extensions.ttyd.openInNewTab')"
|
||||
:title="t('extensions.ttyd.openInNewTab')"
|
||||
>
|
||||
<ExternalLink class="h-3.5 w-3.5" />
|
||||
<ExternalLink class="size-3.5" />
|
||||
</Button>
|
||||
<DialogClose as-child>
|
||||
<Button variant="ghost" size="icon" class="h-7 w-7">
|
||||
<X class="h-3.5 w-3.5" />
|
||||
<Button variant="ghost" size="icon-sm">
|
||||
<X class="size-3.5" />
|
||||
<span class="sr-only">Close</span>
|
||||
</Button>
|
||||
</DialogClose>
|
||||
|
||||
@@ -249,11 +249,11 @@ onUnmounted(() => {
|
||||
</CardContent>
|
||||
<CardFooter class="border-t pt-4 justify-end">
|
||||
<Button v-if="!enabled" :disabled="statusLoading" @click="enrollmentOpen = true">
|
||||
<ShieldCheck class="h-4 w-4" />
|
||||
<ShieldCheck class="size-4" />
|
||||
{{ t('settings.totp.enable') }}
|
||||
</Button>
|
||||
<Button v-else variant="destructive" :disabled="statusLoading" @click="disableOpen = true">
|
||||
<ShieldOff class="h-4 w-4" />
|
||||
<ShieldOff class="size-4" />
|
||||
{{ t('settings.totp.disable') }}
|
||||
</Button>
|
||||
</CardFooter>
|
||||
@@ -283,7 +283,7 @@ onUnmounted(() => {
|
||||
<div class="space-y-2">
|
||||
<Label for="totp-enrollment-code">{{ t('auth.totpCode') }}</Label>
|
||||
<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-enrollment-code" v-model="enrollmentCode" inputmode="numeric" autocomplete="one-time-code" maxlength="6" class="pl-10 font-mono" @input="enrollmentCode = normalizeCode(enrollmentCode)" />
|
||||
</div>
|
||||
</div>
|
||||
@@ -296,11 +296,11 @@ onUnmounted(() => {
|
||||
<DialogFooter>
|
||||
<Button variant="outline" :disabled="enrollmentLoading" @click="enrollmentOpen = false">{{ t('common.cancel') }}</Button>
|
||||
<Button v-if="enrollmentStep === 'password'" :disabled="enrollmentLoading" @click="beginEnrollment">
|
||||
<Loader2 v-if="enrollmentLoading" class="h-4 w-4 animate-spin" />
|
||||
<Loader2 v-if="enrollmentLoading" class="size-4 animate-spin" />
|
||||
{{ t('common.next') }}
|
||||
</Button>
|
||||
<Button v-else :disabled="enrollmentLoading" @click="confirmEnrollment">
|
||||
<Loader2 v-if="enrollmentLoading" class="h-4 w-4 animate-spin" />
|
||||
<Loader2 v-if="enrollmentLoading" class="size-4 animate-spin" />
|
||||
{{ t('common.confirm') }}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
@@ -329,7 +329,7 @@ onUnmounted(() => {
|
||||
<DialogFooter>
|
||||
<Button variant="outline" :disabled="disableLoading" @click="disableOpen = false">{{ t('common.cancel') }}</Button>
|
||||
<Button variant="destructive" :disabled="disableLoading" @click="disableTotp">
|
||||
<Loader2 v-if="disableLoading" class="h-4 w-4 animate-spin" />
|
||||
<Loader2 v-if="disableLoading" class="size-4 animate-spin" />
|
||||
{{ t('settings.totp.disable') }}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
|
||||
@@ -594,8 +594,8 @@ watch(
|
||||
<template>
|
||||
<Popover :open="open" @update:open="emit('update:open', $event)">
|
||||
<PopoverTrigger as-child>
|
||||
<Button variant="ghost" size="sm" class="h-7 w-7 sm:h-8 sm:w-auto p-0 sm:px-2 sm:gap-1.5 text-xs">
|
||||
<Monitor class="h-3.5 w-3.5 sm:h-4 sm:w-4" />
|
||||
<Button variant="ghost" size="sm" class="size-8 sm:w-auto p-0 sm:px-2 sm:gap-1.5 text-xs">
|
||||
<Monitor class="size-3.5 sm:size-4" />
|
||||
<span class="hidden sm:inline">{{ buttonText }}</span>
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
@@ -612,7 +612,7 @@ watch(
|
||||
<!-- Mode Selection -->
|
||||
<div class="space-y-2">
|
||||
<div class="flex items-center gap-1">
|
||||
<Label class="text-xs">{{ t('actionbar.videoMode') }}</Label>
|
||||
<Label class="text-xs text-muted-foreground">{{ t('actionbar.videoMode') }}</Label>
|
||||
<HelpTooltip :content="t('actionbar.videoModeHint')" icon-size="sm" side="right" />
|
||||
</div>
|
||||
<Select
|
||||
@@ -620,7 +620,7 @@ watch(
|
||||
@update:model-value="handleVideoModeChange"
|
||||
:disabled="loadingCodecs || availableCodecs.length === 0 || isRtspCodecLocked"
|
||||
>
|
||||
<SelectTrigger class="h-8 w-full text-xs">
|
||||
<SelectTrigger size="sm" class="w-full text-xs">
|
||||
<div v-if="selectedCodecInfo" class="flex items-center gap-1.5 truncate">
|
||||
<span class="truncate">{{ selectedCodecInfo.name }}</span>
|
||||
<span
|
||||
@@ -673,7 +673,7 @@ watch(
|
||||
<!-- Bitrate Preset - Only shown for WebRTC modes -->
|
||||
<div v-if="props.videoMode !== 'mjpeg'" class="space-y-2">
|
||||
<div class="flex items-center gap-1">
|
||||
<Label class="text-xs">{{ t('actionbar.bitratePreset') }}</Label>
|
||||
<Label class="text-xs text-muted-foreground">{{ t('actionbar.bitratePreset') }}</Label>
|
||||
<HelpTooltip :content="t('help.videoBitratePreset')" icon-size="sm" />
|
||||
</div>
|
||||
<div class="grid grid-cols-3 gap-1.5">
|
||||
@@ -687,7 +687,7 @@ watch(
|
||||
:disabled="applyingBitrate"
|
||||
@click="handleBitratePresetChange('Speed')"
|
||||
>
|
||||
<Zap class="h-3.5 w-3.5" />
|
||||
<Zap class="size-3.5" />
|
||||
<span class="text-[10px] font-medium">{{ t('actionbar.bitrateSpeed') }}</span>
|
||||
</Button>
|
||||
<Button
|
||||
@@ -700,7 +700,7 @@ watch(
|
||||
:disabled="applyingBitrate"
|
||||
@click="handleBitratePresetChange('Balanced')"
|
||||
>
|
||||
<Scale class="h-3.5 w-3.5" />
|
||||
<Scale class="size-3.5" />
|
||||
<span class="text-[10px] font-medium">{{ t('actionbar.bitrateBalanced') }}</span>
|
||||
</Button>
|
||||
<Button
|
||||
@@ -713,7 +713,7 @@ watch(
|
||||
:disabled="applyingBitrate"
|
||||
@click="handleBitratePresetChange('Quality')"
|
||||
>
|
||||
<Image class="h-3.5 w-3.5" />
|
||||
<Image class="size-3.5" />
|
||||
<span class="text-[10px] font-medium">{{ t('actionbar.bitrateQuality') }}</span>
|
||||
</Button>
|
||||
</div>
|
||||
@@ -733,23 +733,22 @@ watch(
|
||||
<h5 class="text-xs font-medium text-muted-foreground">{{ t('actionbar.deviceSettings') }}</h5>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
class="h-6 w-6"
|
||||
size="icon-xs"
|
||||
:disabled="loadingDevices"
|
||||
@click="loadDevices"
|
||||
>
|
||||
<RefreshCw :class="['h-3.5 w-3.5', loadingDevices && 'animate-spin']" />
|
||||
<RefreshCw :class="['size-3.5', loadingDevices && 'animate-spin']" />
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<!-- Device Selection -->
|
||||
<div class="space-y-2">
|
||||
<Label class="text-xs">{{ t('actionbar.videoDevice') }}</Label>
|
||||
<Label class="text-xs text-muted-foreground">{{ t('actionbar.videoDevice') }}</Label>
|
||||
<NativeSelect
|
||||
:model-value="selectedDevice"
|
||||
@update:model-value="handleDeviceChange"
|
||||
:disabled="loadingDevices || devices.length === 0"
|
||||
class="h-8 w-full text-xs"
|
||||
size="sm" class="w-full text-xs"
|
||||
>
|
||||
<NativeSelectOption value="">{{ loadingDevices ? t('common.loading') : t('actionbar.selectDevice') }}</NativeSelectOption>
|
||||
<NativeSelectOption
|
||||
@@ -765,13 +764,13 @@ watch(
|
||||
|
||||
<!-- Format Selection -->
|
||||
<div class="space-y-2">
|
||||
<Label class="text-xs">{{ t('actionbar.videoFormat') }}</Label>
|
||||
<Label class="text-xs text-muted-foreground">{{ t('actionbar.videoFormat') }}</Label>
|
||||
<Select
|
||||
:model-value="selectedFormat"
|
||||
@update:model-value="handleFormatChange"
|
||||
:disabled="!selectedDevice || availableFormats.length === 0"
|
||||
>
|
||||
<SelectTrigger class="h-8 w-full text-xs">
|
||||
<SelectTrigger size="sm" class="w-full text-xs">
|
||||
<div v-if="selectedFormatInfo" class="flex min-w-0 items-center gap-1.5">
|
||||
<span class="truncate">{{ selectedFormatInfo.description }}</span>
|
||||
<span
|
||||
@@ -825,12 +824,12 @@ watch(
|
||||
|
||||
<!-- Resolution Selection -->
|
||||
<div class="space-y-2">
|
||||
<Label class="text-xs">{{ t('actionbar.videoResolution') }}</Label>
|
||||
<Label class="text-xs text-muted-foreground">{{ t('actionbar.videoResolution') }}</Label>
|
||||
<NativeSelect
|
||||
:model-value="selectedResolution"
|
||||
@update:model-value="handleResolutionChange"
|
||||
:disabled="!selectedFormat || availableResolutions.length === 0"
|
||||
class="h-8 w-full text-xs"
|
||||
size="sm" class="w-full text-xs"
|
||||
>
|
||||
<NativeSelectOption value="">{{ t('actionbar.selectResolution') }}</NativeSelectOption>
|
||||
<NativeSelectOption
|
||||
@@ -846,12 +845,12 @@ watch(
|
||||
|
||||
<!-- FPS Selection -->
|
||||
<div class="space-y-2">
|
||||
<Label class="text-xs">{{ t('actionbar.videoFps') }}</Label>
|
||||
<Label class="text-xs text-muted-foreground">{{ t('actionbar.videoFps') }}</Label>
|
||||
<NativeSelect
|
||||
:model-value="String(selectedFps)"
|
||||
@update:model-value="handleFpsChange"
|
||||
:disabled="!selectedResolution || availableFps.length === 0"
|
||||
class="h-8 w-full text-xs"
|
||||
size="sm" class="w-full text-xs"
|
||||
>
|
||||
<NativeSelectOption value="">{{ t('actionbar.selectFps') }}</NativeSelectOption>
|
||||
<NativeSelectOption
|
||||
@@ -871,7 +870,7 @@ watch(
|
||||
:disabled="applying || !selectedDevice || !selectedFormat"
|
||||
@click="applyVideoConfig"
|
||||
>
|
||||
<Loader2 v-if="applying" class="h-3.5 w-3.5 mr-1.5 animate-spin" />
|
||||
<Loader2 v-if="applying" class="size-3.5 mr-1.5 animate-spin" />
|
||||
<span>{{ applying ? t('actionbar.applying') : t('common.apply') }}</span>
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -3,11 +3,15 @@ import type { HTMLAttributes } from "vue"
|
||||
import { useVModel } from "@vueuse/core"
|
||||
import { cn } from "@/lib/utils"
|
||||
|
||||
const props = defineProps<{
|
||||
defaultValue?: string | number
|
||||
modelValue?: string | number
|
||||
class?: HTMLAttributes["class"]
|
||||
}>()
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
defaultValue?: string | number
|
||||
modelValue?: string | number
|
||||
class?: HTMLAttributes["class"]
|
||||
size?: "sm" | "default"
|
||||
}>(),
|
||||
{ size: "default" },
|
||||
)
|
||||
|
||||
const emits = defineEmits<{
|
||||
(e: "update:modelValue", payload: string | number): void
|
||||
@@ -23,8 +27,9 @@ const modelValue = useVModel(props, "modelValue", emits, {
|
||||
<input
|
||||
v-model="modelValue"
|
||||
data-slot="input"
|
||||
:data-size="size"
|
||||
:class="cn(
|
||||
'file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input h-9 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
|
||||
'file:text-foreground placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 border-input data-[size=default]:h-9 data-[size=sm]:h-8 w-full min-w-0 rounded-md border bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm',
|
||||
'focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-3',
|
||||
'aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive',
|
||||
props.class,
|
||||
|
||||
@@ -9,7 +9,10 @@ defineOptions({
|
||||
inheritAttrs: false,
|
||||
})
|
||||
|
||||
const props = defineProps<{ modelValue?: AcceptableValue | AcceptableValue[], class?: HTMLAttributes["class"] }>()
|
||||
const props = withDefaults(
|
||||
defineProps<{ modelValue?: AcceptableValue | AcceptableValue[], class?: HTMLAttributes["class"], size?: "sm" | "default" }>(),
|
||||
{ size: "default" },
|
||||
)
|
||||
|
||||
const emit = defineEmits<{
|
||||
"update:modelValue": [value: AcceptableValue]
|
||||
@@ -20,7 +23,7 @@ const modelValue = useVModel(props, "modelValue", emit, {
|
||||
defaultValue: "",
|
||||
})
|
||||
|
||||
const delegatedProps = reactiveOmit(props, "class")
|
||||
const delegatedProps = reactiveOmit(props, "class", "size")
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -32,8 +35,9 @@ const delegatedProps = reactiveOmit(props, "class")
|
||||
v-bind="{ ...$attrs, ...delegatedProps }"
|
||||
v-model="modelValue"
|
||||
data-slot="native-select"
|
||||
:data-size="size"
|
||||
:class="cn(
|
||||
'border-input placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 dark:hover:bg-input/50 h-9 w-full min-w-0 appearance-none rounded-md border bg-transparent px-3 py-2 pr-9 text-sm shadow-xs transition-[color,box-shadow] outline-none disabled:pointer-events-none disabled:cursor-not-allowed',
|
||||
'border-input placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground dark:bg-input/30 dark:hover:bg-input/50 data-[size=default]:h-9 data-[size=sm]:h-8 w-full min-w-0 appearance-none rounded-md border bg-transparent px-3 py-2 pr-9 text-sm shadow-xs transition-[color,box-shadow] outline-none disabled:pointer-events-none disabled:cursor-not-allowed',
|
||||
'focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-3',
|
||||
'aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive',
|
||||
props.class,
|
||||
|
||||
@@ -18,7 +18,7 @@ const { toggleSidebar } = useSidebar()
|
||||
data-slot="sidebar-trigger"
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
:class="cn('h-7 w-7', props.class)"
|
||||
:class="cn('size-7', props.class)"
|
||||
@click="toggleSidebar"
|
||||
>
|
||||
<PanelLeft />
|
||||
|
||||
@@ -3,7 +3,7 @@ import { createContext } from "reka-ui"
|
||||
|
||||
export const SIDEBAR_COOKIE_NAME = "sidebar_state"
|
||||
export const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7
|
||||
export const SIDEBAR_WIDTH = "16rem"
|
||||
export const SIDEBAR_WIDTH = "14rem"
|
||||
export const SIDEBAR_WIDTH_MOBILE = "18rem"
|
||||
export const SIDEBAR_WIDTH_ICON = "3rem"
|
||||
export const SIDEBAR_KEYBOARD_SHORTCUT = "b"
|
||||
|
||||
@@ -155,3 +155,30 @@ body {
|
||||
#app {
|
||||
min-height: 100vh;
|
||||
}
|
||||
|
||||
@layer components {
|
||||
/* Shared dotted backdrop: console video area, login & setup pages */
|
||||
.dot-grid-bg {
|
||||
background-color: var(--muted);
|
||||
background-image: radial-gradient(circle, color-mix(in oklab, var(--muted-foreground) 35%, transparent) 1px, transparent 1px);
|
||||
background-size: 20px 20px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Settings page density: compact cards.
|
||||
Unlayered on purpose — must outrank Tailwind's @layer utilities (py-6/px-6/gap-6). */
|
||||
.settings-dense [data-slot="card"] {
|
||||
gap: 1rem;
|
||||
padding-block: 1rem;
|
||||
}
|
||||
.settings-dense [data-slot="card-header"],
|
||||
.settings-dense [data-slot="card-content"],
|
||||
.settings-dense [data-slot="card-footer"] {
|
||||
padding-inline: 1rem;
|
||||
}
|
||||
.settings-dense [data-slot="card-header"].border-b {
|
||||
padding-bottom: 1rem;
|
||||
}
|
||||
.settings-dense [data-slot="card-footer"].border-t {
|
||||
padding-top: 1rem;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user