mirror of
https://github.com/mofeng-git/One-KVM.git
synced 2026-09-12 18:44:25 +08:00
feat: 统一和完善 UI
This commit is contained in:
@@ -304,7 +304,7 @@ const hasRightOverflow = computed(() => {
|
|||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger as-child>
|
<TooltipTrigger as-child>
|
||||||
<Button variant="ghost" size="sm" class="h-8 gap-1.5 text-xs" @click="msdDialogOpen = true">
|
<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>
|
<span v-if="visibleSet.get('msd') === 'label'">{{ t('actionbar.virtualMedia') }}</span>
|
||||||
</Button>
|
</Button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
@@ -320,7 +320,7 @@ const hasRightOverflow = computed(() => {
|
|||||||
<Popover v-model:open="atxOpen">
|
<Popover v-model:open="atxOpen">
|
||||||
<PopoverTrigger as-child>
|
<PopoverTrigger as-child>
|
||||||
<Button variant="ghost" size="sm" class="h-8 gap-1.5 text-xs">
|
<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>
|
<span v-if="visibleSet.get('atx') === 'label'">{{ t('actionbar.power') }}</span>
|
||||||
</Button>
|
</Button>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
@@ -341,7 +341,7 @@ const hasRightOverflow = computed(() => {
|
|||||||
<Popover v-model:open="pasteOpen">
|
<Popover v-model:open="pasteOpen">
|
||||||
<PopoverTrigger as-child>
|
<PopoverTrigger as-child>
|
||||||
<Button variant="ghost" size="sm" class="h-8 gap-1.5 text-xs">
|
<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>
|
<span v-if="visibleSet.get('paste') === 'label'">{{ t('actionbar.paste') }}</span>
|
||||||
</Button>
|
</Button>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
@@ -360,7 +360,7 @@ const hasRightOverflow = computed(() => {
|
|||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger as-child>
|
<TooltipTrigger as-child>
|
||||||
<Button variant="ghost" size="sm" class="h-8 gap-1.5 text-xs" @click="emit('toggleStats')">
|
<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>
|
<span v-if="visibleSet.get('stats') === 'label'">{{ t('actionbar.stats') }}</span>
|
||||||
</Button>
|
</Button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
@@ -383,7 +383,7 @@ const hasRightOverflow = computed(() => {
|
|||||||
:disabled="!props.ttydRunning"
|
:disabled="!props.ttydRunning"
|
||||||
@click="emit('openTerminal')"
|
@click="emit('openTerminal')"
|
||||||
>
|
>
|
||||||
<Terminal class="h-4 w-4" />
|
<Terminal class="size-4" />
|
||||||
<span v-if="visibleSet.get('terminal') === 'label'">{{ t('actionbar.webTerminal') }}</span>
|
<span v-if="visibleSet.get('terminal') === 'label'">{{ t('actionbar.webTerminal') }}</span>
|
||||||
</Button>
|
</Button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
@@ -401,10 +401,10 @@ const hasRightOverflow = computed(() => {
|
|||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
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')"
|
@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>
|
<span class="hidden xl:inline">AI</span>
|
||||||
</Button>
|
</Button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
@@ -420,7 +420,7 @@ const hasRightOverflow = computed(() => {
|
|||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger as-child>
|
<TooltipTrigger as-child>
|
||||||
<Button variant="ghost" size="sm" class="h-8 gap-1.5 text-xs" @click="router.push('/settings')">
|
<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>
|
<span v-if="visibleSet.get('settings') === 'label'">{{ t('actionbar.settings') }}</span>
|
||||||
</Button>
|
</Button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
@@ -444,10 +444,10 @@ const hasRightOverflow = computed(() => {
|
|||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
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')"
|
@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>
|
<span class="hidden xl:inline">{{ t('actionbar.keyboard') }}</span>
|
||||||
</Button>
|
</Button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
@@ -464,10 +464,10 @@ const hasRightOverflow = computed(() => {
|
|||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
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')"
|
@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>
|
<span class="hidden xl:inline">{{ t('actionbar.fullscreen') }}</span>
|
||||||
</Button>
|
</Button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
@@ -480,26 +480,26 @@ const hasRightOverflow = computed(() => {
|
|||||||
<!-- Overflow Menu - Only show if there are overflowed items -->
|
<!-- Overflow Menu - Only show if there are overflowed items -->
|
||||||
<DropdownMenu v-if="hasOverflow" v-model:open="overflowMenuOpen">
|
<DropdownMenu v-if="hasOverflow" v-model:open="overflowMenuOpen">
|
||||||
<DropdownMenuTrigger as-child>
|
<DropdownMenuTrigger as-child>
|
||||||
<Button variant="ghost" size="sm" class="h-7 w-7 sm:h-8 sm:w-8 p-0">
|
<Button variant="ghost" size="sm" class="size-8 p-0">
|
||||||
<MoreHorizontal class="h-3.5 w-3.5 sm:h-4 sm:w-4" />
|
<MoreHorizontal class="size-3.5 sm:size-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="end" class="w-48">
|
<DropdownMenuContent align="end" class="w-48">
|
||||||
<!-- MSD -->
|
<!-- MSD -->
|
||||||
<DropdownMenuItem v-if="showMsd && !isVisible('msd')" @click="openFromOverflow(() => msdDialogOpen = true)">
|
<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') }}
|
{{ t('actionbar.virtualMedia') }}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
|
|
||||||
<!-- ATX -->
|
<!-- ATX -->
|
||||||
<DropdownMenuItem v-if="showAtx && !isVisible('atx')" @click="openMobileAtx">
|
<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') }}
|
{{ t('actionbar.power') }}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
|
|
||||||
<!-- Paste -->
|
<!-- Paste -->
|
||||||
<DropdownMenuItem v-if="showPasteText && !isVisible('paste')" @click="openMobilePaste">
|
<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') }}
|
{{ t('actionbar.paste') }}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
|
|
||||||
@@ -507,7 +507,7 @@ const hasRightOverflow = computed(() => {
|
|||||||
|
|
||||||
<!-- Stats -->
|
<!-- Stats -->
|
||||||
<DropdownMenuItem v-if="showStats && !isVisible('stats')" @click="openFromOverflow(() => emit('toggleStats'))">
|
<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') }}
|
{{ t('actionbar.stats') }}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
|
|
||||||
@@ -517,13 +517,13 @@ const hasRightOverflow = computed(() => {
|
|||||||
:disabled="!props.ttydRunning"
|
:disabled="!props.ttydRunning"
|
||||||
@click="openFromOverflow(() => emit('openTerminal'))"
|
@click="openFromOverflow(() => emit('openTerminal'))"
|
||||||
>
|
>
|
||||||
<Terminal class="h-4 w-4 mr-2" />
|
<Terminal class="size-4 mr-2" />
|
||||||
{{ t('actionbar.webTerminal') }}
|
{{ t('actionbar.webTerminal') }}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
|
|
||||||
<!-- Settings -->
|
<!-- Settings -->
|
||||||
<DropdownMenuItem v-if="!isVisible('settings')" @click="openFromOverflow(() => router.push('/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') }}
|
{{ t('actionbar.settings') }}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</DropdownMenuContent>
|
</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 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">
|
<div class="flex items-center gap-0.5 sm:gap-1.5 px-2 sm:px-4 py-1 sm:py-1.5">
|
||||||
<!-- MSD -->
|
<!-- 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-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="h-4 w-4" />{{ t('actionbar.virtualMedia') }}</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 -->
|
<!-- 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-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="h-4 w-4" />{{ t('actionbar.power') }}</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 -->
|
<!-- 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-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="h-4 w-4" />{{ t('actionbar.paste') }}</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 -->
|
<!-- 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-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="h-4 w-4" />{{ t('actionbar.stats') }}</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 -->
|
<!-- 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-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="h-4 w-4" />{{ t('actionbar.webTerminal') }}</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 -->
|
<!-- 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-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="h-4 w-4" />{{ t('actionbar.settings') }}</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) -->
|
<!-- 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-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="h-4 w-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="h-4 w-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="h-4 w-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="h-4 w-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="h-4 w-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>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -52,9 +52,9 @@ async function handleLogout() {
|
|||||||
</span>
|
</span>
|
||||||
|
|
||||||
<!-- Theme Toggle -->
|
<!-- Theme Toggle -->
|
||||||
<Button variant="ghost" size="icon" class="h-8 w-8" :aria-label="t('common.toggleTheme')" @click="toggleTheme">
|
<Button variant="ghost" size="icon-sm" :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" />
|
<Sun class="size-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" />
|
<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>
|
<span class="sr-only">{{ t('common.toggleTheme') }}</span>
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
@@ -64,21 +64,21 @@ async function handleLogout() {
|
|||||||
<!-- Mobile Menu -->
|
<!-- Mobile Menu -->
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger as-child class="md:hidden">
|
<DropdownMenuTrigger as-child class="md:hidden">
|
||||||
<Button variant="ghost" size="icon" class="h-8 w-8" :aria-label="t('common.menu')">
|
<Button variant="ghost" size="icon-sm" :aria-label="t('common.menu')">
|
||||||
<Menu class="h-4 w-4" />
|
<Menu class="size-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="end">
|
<DropdownMenuContent align="end">
|
||||||
<DropdownMenuItem @click="handleLogout">
|
<DropdownMenuItem @click="handleLogout">
|
||||||
<LogOut class="h-4 w-4 mr-2" />
|
<LogOut class="size-4 mr-2" />
|
||||||
{{ t('nav.logout') }}
|
{{ t('nav.logout') }}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
|
|
||||||
<!-- Logout Button (Desktop) -->
|
<!-- Logout Button (Desktop) -->
|
||||||
<Button variant="ghost" size="icon" class="hidden md:flex h-8 w-8" :aria-label="t('nav.logout')" @click="handleLogout">
|
<Button variant="ghost" size="icon-sm" class="hidden md:flex" :aria-label="t('nav.logout')" @click="handleLogout">
|
||||||
<LogOut class="h-4 w-4" />
|
<LogOut class="size-4" />
|
||||||
<span class="sr-only">{{ t('nav.logout') }}</span>
|
<span class="sr-only">{{ t('nav.logout') }}</span>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ const emit = defineEmits<{
|
|||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
|
|
||||||
const activeTab = ref('atx')
|
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 powerState = ref<'on' | 'off' | 'unknown'>('unknown')
|
||||||
const hddState = ref<'active' | 'inactive' | 'unknown'>('unknown')
|
const hddState = ref<'active' | 'inactive' | 'unknown'>('unknown')
|
||||||
@@ -199,19 +199,19 @@ watch(
|
|||||||
<template>
|
<template>
|
||||||
<div class="p-2.5 space-y-2.5">
|
<div class="p-2.5 space-y-2.5">
|
||||||
<Tabs v-model="activeTab">
|
<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
|
<TabsTrigger
|
||||||
value="atx"
|
value="atx"
|
||||||
:class="tabTriggerClass"
|
:class="tabTriggerClass"
|
||||||
>
|
>
|
||||||
<Power class="h-3 w-3 mr-1" />
|
<Power class="size-3 mr-1" />
|
||||||
{{ t('atx.title') }}
|
{{ t('atx.title') }}
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
<TabsTrigger
|
<TabsTrigger
|
||||||
value="wol"
|
value="wol"
|
||||||
:class="tabTriggerClass"
|
:class="tabTriggerClass"
|
||||||
>
|
>
|
||||||
<Wifi class="h-3 w-3 mr-1" />
|
<Wifi class="size-3 mr-1" />
|
||||||
WOL
|
WOL
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
@@ -221,14 +221,14 @@ watch(
|
|||||||
<!-- Status -->
|
<!-- Status -->
|
||||||
<div class="grid grid-cols-2 gap-2">
|
<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">
|
<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">
|
<div class="min-w-0">
|
||||||
<p class="truncate text-[11px] leading-none text-muted-foreground">{{ t('atx.powerState') }}</p>
|
<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>
|
<p :class="['mt-1 truncate text-xs font-medium leading-none', powerStateTextColor]">{{ powerStateText }}</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex min-w-0 items-center gap-2 rounded-md border bg-muted/40 px-2 py-1.5">
|
<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">
|
<div class="min-w-0">
|
||||||
<p class="truncate text-[11px] leading-none text-muted-foreground">{{ t('atx.hddState') }}</p>
|
<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>
|
<p :class="['mt-1 truncate text-xs font-medium leading-none', hddStateTextColor]">{{ hddStateText }}</p>
|
||||||
@@ -245,12 +245,12 @@ watch(
|
|||||||
size="sm"
|
size="sm"
|
||||||
:disabled="actionBusy"
|
:disabled="actionBusy"
|
||||||
:class="[
|
: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' : '',
|
activeAction === 'short' ? 'bg-muted text-muted-foreground' : '',
|
||||||
]"
|
]"
|
||||||
@click="handleAction('short')"
|
@click="handleAction('short')"
|
||||||
>
|
>
|
||||||
<Power class="h-3 w-3" />
|
<Power class="size-3" />
|
||||||
{{ t('atx.shortPress') }}
|
{{ t('atx.shortPress') }}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
@@ -259,12 +259,12 @@ watch(
|
|||||||
size="sm"
|
size="sm"
|
||||||
:disabled="actionBusy"
|
:disabled="actionBusy"
|
||||||
:class="[
|
: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' : '',
|
activeAction === 'long' ? 'bg-muted text-muted-foreground hover:text-muted-foreground hover:bg-muted dark:hover:bg-muted' : '',
|
||||||
]"
|
]"
|
||||||
@click="handleAction('long')"
|
@click="handleAction('long')"
|
||||||
>
|
>
|
||||||
<CircleDot class="h-3 w-3" />
|
<CircleDot class="size-3" />
|
||||||
{{ t('atx.longPress') }}
|
{{ t('atx.longPress') }}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
@@ -273,12 +273,12 @@ watch(
|
|||||||
size="sm"
|
size="sm"
|
||||||
:disabled="actionBusy"
|
:disabled="actionBusy"
|
||||||
:class="[
|
: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' : '',
|
activeAction === 'reset' ? 'bg-muted text-muted-foreground hover:text-muted-foreground hover:bg-muted dark:hover:bg-muted' : '',
|
||||||
]"
|
]"
|
||||||
@click="handleAction('reset')"
|
@click="handleAction('reset')"
|
||||||
>
|
>
|
||||||
<RotateCcw class="h-3 w-3" />
|
<RotateCcw class="size-3" />
|
||||||
{{ t('atx.reset') }}
|
{{ t('atx.reset') }}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -302,7 +302,7 @@ watch(
|
|||||||
:disabled="!isValidMac || wolSending"
|
:disabled="!isValidMac || wolSending"
|
||||||
@click="sendWol"
|
@click="sendWol"
|
||||||
>
|
>
|
||||||
<Send class="h-3.5 w-3.5" />
|
<Send class="size-3.5" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<p v-if="wolMacAddress && !isValidMac" class="text-xs text-destructive">
|
<p v-if="wolMacAddress && !isValidMac" class="text-xs text-destructive">
|
||||||
|
|||||||
@@ -142,8 +142,8 @@ watch(() => props.open, (isOpen) => {
|
|||||||
<template>
|
<template>
|
||||||
<Popover :open="open" @update:open="emit('update:open', $event)">
|
<Popover :open="open" @update:open="emit('update:open', $event)">
|
||||||
<PopoverTrigger as-child>
|
<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">
|
<Button variant="ghost" size="sm" class="size-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" />
|
<Volume2 class="size-3.5 sm:size-4" />
|
||||||
<span class="hidden sm:inline">{{ t('actionbar.audioConfig') }}</span>
|
<span class="hidden sm:inline">{{ t('actionbar.audioConfig') }}</span>
|
||||||
</Button>
|
</Button>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
@@ -166,7 +166,7 @@ watch(() => props.open, (isOpen) => {
|
|||||||
<span class="text-xs font-mono">{{ Math.round(localVolume[0] ?? 0) }}%</span>
|
<span class="text-xs font-mono">{{ Math.round(localVolume[0] ?? 0) }}%</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-2">
|
<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
|
<Slider
|
||||||
:model-value="localVolume"
|
:model-value="localVolume"
|
||||||
@update:model-value="handleVolumeChange"
|
@update:model-value="handleVolumeChange"
|
||||||
@@ -176,7 +176,7 @@ watch(() => props.open, (isOpen) => {
|
|||||||
:disabled="!systemStore.audio?.streaming"
|
:disabled="!systemStore.audio?.streaming"
|
||||||
class="flex-1"
|
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>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -191,12 +191,11 @@ watch(() => props.open, (isOpen) => {
|
|||||||
</h5>
|
</h5>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon-xs"
|
||||||
class="h-6 w-6"
|
|
||||||
:disabled="loadingDevices"
|
:disabled="loadingDevices"
|
||||||
@click="loadDevices"
|
@click="loadDevices"
|
||||||
>
|
>
|
||||||
<RefreshCw :class="['h-3.5 w-3.5', loadingDevices && 'animate-spin']" />
|
<RefreshCw :class="['size-3.5', loadingDevices && 'animate-spin']" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -230,7 +229,7 @@ watch(() => props.open, (isOpen) => {
|
|||||||
:model-value="selectedDevice"
|
:model-value="selectedDevice"
|
||||||
@update:model-value="(v) => selectedDevice = v as string"
|
@update:model-value="(v) => selectedDevice = v as string"
|
||||||
:disabled="loadingDevices || devices.length === 0"
|
: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 value="">{{ t('actionbar.selectAudioDevice') }}</NativeSelectOption>
|
||||||
<NativeSelectOption
|
<NativeSelectOption
|
||||||
@@ -281,7 +280,7 @@ watch(() => props.open, (isOpen) => {
|
|||||||
:disabled="applying"
|
:disabled="applying"
|
||||||
@click="applyConfig"
|
@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>
|
<span>{{ applying ? t('actionbar.applying') : t('common.apply') }}</span>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -13,17 +13,17 @@ const props = withDefaults(
|
|||||||
const dim = computed(() => {
|
const dim = computed(() => {
|
||||||
switch (props.size) {
|
switch (props.size) {
|
||||||
case 'xs':
|
case 'xs':
|
||||||
return 'h-4 w-4'
|
return 'size-4'
|
||||||
case 'sm':
|
case 'sm':
|
||||||
return 'h-5 w-5'
|
return 'size-5'
|
||||||
case 'md':
|
case 'md':
|
||||||
return 'h-6 w-6'
|
return 'size-6'
|
||||||
case 'lg':
|
case 'lg':
|
||||||
return 'h-10 w-10'
|
return 'size-10'
|
||||||
case 'xl':
|
case 'xl':
|
||||||
return 'h-14 w-14'
|
return 'size-14'
|
||||||
default:
|
default:
|
||||||
return 'h-6 w-6'
|
return 'size-6'
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import {
|
|||||||
} from 'lucide-vue-next'
|
} from 'lucide-vue-next'
|
||||||
import { toast } from 'vue-sonner'
|
import { toast } from 'vue-sonner'
|
||||||
import { computerUseApi, type ComputerUseAction, type ComputerUseConfig, type ComputerUseSession } from '@/api'
|
import { computerUseApi, type ComputerUseAction, type ComputerUseConfig, type ComputerUseSession } from '@/api'
|
||||||
|
import { ApiError } from '@/api/request'
|
||||||
import type { ComputerUseTimelineItem } from '@/types/computerUseTimeline'
|
import type { ComputerUseTimelineItem } from '@/types/computerUseTimeline'
|
||||||
import { Button } from '@/components/ui/button'
|
import { Button } from '@/components/ui/button'
|
||||||
import { Input } from '@/components/ui/input'
|
import { Input } from '@/components/ui/input'
|
||||||
@@ -107,6 +108,8 @@ async function loadConfig() {
|
|||||||
try {
|
try {
|
||||||
config.value = await computerUseApi.config()
|
config.value = await computerUseApi.config()
|
||||||
} catch (err) {
|
} 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'), {
|
toast.error(t('computerUse.errors.configLoadFailed'), {
|
||||||
description: err instanceof Error ? err.message : undefined,
|
description: err instanceof Error ? err.message : undefined,
|
||||||
})
|
})
|
||||||
@@ -217,15 +220,15 @@ onMounted(loadConfig)
|
|||||||
>
|
>
|
||||||
<div class="flex h-full min-h-0 flex-col">
|
<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">
|
<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="min-w-0 flex-1">
|
||||||
<div class="truncate text-sm font-semibold">{{ t('computerUse.title') }}</div>
|
<div class="truncate text-sm font-semibold">{{ t('computerUse.title') }}</div>
|
||||||
<div
|
<div
|
||||||
class="flex min-w-0 items-center gap-1 text-[11px]"
|
class="flex min-w-0 items-center gap-1 text-[11px]"
|
||||||
:class="connected ? 'text-emerald-600 dark:text-emerald-400' : 'text-muted-foreground'"
|
:class="connected ? 'text-emerald-600 dark:text-emerald-400' : 'text-muted-foreground'"
|
||||||
>
|
>
|
||||||
<Wifi v-if="connected" class="h-3 w-3 shrink-0" />
|
<Wifi v-if="connected" class="size-3 shrink-0" />
|
||||||
<WifiOff v-else class="h-3 w-3 shrink-0" />
|
<WifiOff v-else class="size-3 shrink-0" />
|
||||||
<span class="truncate">
|
<span class="truncate">
|
||||||
{{ connected ? t('computerUse.connection.connected') : (wsError || t('computerUse.connection.disconnected')) }}
|
{{ connected ? t('computerUse.connection.connected') : (wsError || t('computerUse.connection.disconnected')) }}
|
||||||
</span>
|
</span>
|
||||||
@@ -235,39 +238,39 @@ onMounted(loadConfig)
|
|||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
class="h-8 w-8"
|
class="size-8"
|
||||||
:title="t('computerUse.buttons.settings')"
|
:title="t('computerUse.buttons.settings')"
|
||||||
:aria-label="t('computerUse.buttons.settings')"
|
:aria-label="t('computerUse.buttons.settings')"
|
||||||
@click="settingsOpen = true"
|
@click="settingsOpen = true"
|
||||||
>
|
>
|
||||||
<Settings class="h-4 w-4" />
|
<Settings class="size-4" />
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
class="h-8 w-8"
|
class="size-8"
|
||||||
:title="t('computerUse.buttons.clear')"
|
:title="t('computerUse.buttons.clear')"
|
||||||
:aria-label="t('computerUse.buttons.clear')"
|
:aria-label="t('computerUse.buttons.clear')"
|
||||||
:disabled="isRunning || timeline.length === 0"
|
:disabled="isRunning || timeline.length === 0"
|
||||||
@click="emit('clear')"
|
@click="emit('clear')"
|
||||||
>
|
>
|
||||||
<Trash2 class="h-4 w-4" />
|
<Trash2 class="size-4" />
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
class="h-8 w-8"
|
class="size-8"
|
||||||
:title="t('computerUse.buttons.close')"
|
:title="t('computerUse.buttons.close')"
|
||||||
:aria-label="t('computerUse.buttons.close')"
|
:aria-label="t('computerUse.buttons.close')"
|
||||||
@click="emit('update:open', false)"
|
@click="emit('update:open', false)"
|
||||||
>
|
>
|
||||||
<X class="h-4 w-4" />
|
<X class="size-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<div ref="messagesRef" class="min-h-0 flex-1 overflow-y-auto px-4 py-4">
|
<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">
|
<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>
|
||||||
|
|
||||||
<div v-else class="space-y-5">
|
<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"
|
class="flex w-full items-center gap-1.5 py-1 text-left font-medium hover:text-foreground"
|
||||||
@click="toggleReasoning(item)"
|
@click="toggleReasoning(item)"
|
||||||
>
|
>
|
||||||
<ChevronDown v-if="reasoningOpen(item)" class="h-3.5 w-3.5 shrink-0" />
|
<ChevronDown v-if="reasoningOpen(item)" class="size-3.5 shrink-0" />
|
||||||
<ChevronRight v-else class="h-3.5 w-3.5 shrink-0" />
|
<ChevronRight v-else class="size-3.5 shrink-0" />
|
||||||
<BrainCircuit class="h-3.5 w-3.5 shrink-0" />
|
<BrainCircuit class="size-3.5 shrink-0" />
|
||||||
<span>
|
<span>
|
||||||
{{ item.failed
|
{{ item.failed
|
||||||
? t('computerUse.reasoning.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 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">
|
<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">
|
<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>
|
||||||
<span>{{ item.screenshot.width }}x{{ item.screenshot.height }}</span>
|
<span>{{ item.screenshot.width }}x{{ item.screenshot.height }}</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -363,24 +366,24 @@ onMounted(loadConfig)
|
|||||||
<Button
|
<Button
|
||||||
v-if="!isRunning"
|
v-if="!isRunning"
|
||||||
size="icon"
|
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')"
|
:title="t('computerUse.buttons.send')"
|
||||||
:aria-label="t('computerUse.buttons.send')"
|
:aria-label="t('computerUse.buttons.send')"
|
||||||
:disabled="!canStart || starting"
|
:disabled="!canStart || starting"
|
||||||
@click="start"
|
@click="start"
|
||||||
>
|
>
|
||||||
<SendHorizontal class="h-4 w-4" />
|
<SendHorizontal class="size-4" />
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
v-else
|
v-else
|
||||||
size="icon"
|
size="icon"
|
||||||
variant="destructive"
|
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')"
|
:title="t('computerUse.buttons.stop')"
|
||||||
:aria-label="t('computerUse.buttons.stop')"
|
:aria-label="t('computerUse.buttons.stop')"
|
||||||
@click="emit('stop')"
|
@click="emit('stop')"
|
||||||
>
|
>
|
||||||
<Square class="h-3.5 w-3.5 fill-current" />
|
<Square class="size-3.5 fill-current" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<p v-if="config && !config.api_key_configured" class="mt-2 text-xs text-muted-foreground">
|
<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">
|
<div class="space-y-1.5">
|
||||||
<Label for="cua-key" class="flex items-center gap-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') }}
|
{{ t('computerUse.settings.apiKey') }}
|
||||||
</Label>
|
</Label>
|
||||||
<Input
|
<Input
|
||||||
|
|||||||
@@ -25,9 +25,9 @@ defineProps<{
|
|||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon-xs"
|
size="icon-xs"
|
||||||
class="text-muted-foreground/60 hover:text-muted-foreground"
|
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>
|
</Button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent
|
<TooltipContent
|
||||||
|
|||||||
@@ -232,9 +232,9 @@ watch(() => props.open, (isOpen) => {
|
|||||||
<template>
|
<template>
|
||||||
<Popover :open="open" @update:open="emit('update:open', $event)">
|
<Popover :open="open" @update:open="emit('update:open', $event)">
|
||||||
<PopoverTrigger as-child>
|
<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">
|
<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="h-3.5 w-3.5 sm:h-4 sm:w-4" />
|
<MousePointer v-if="mouseMode === 'absolute'" class="size-3.5 sm:size-4" />
|
||||||
<Move v-else class="h-3.5 w-3.5 sm:h-4 sm:w-4" />
|
<Move v-else class="size-3.5 sm:size-4" />
|
||||||
<span class="hidden sm:inline">{{ buttonText }}</span>
|
<span class="hidden sm:inline">{{ buttonText }}</span>
|
||||||
</Button>
|
</Button>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
@@ -261,7 +261,7 @@ watch(() => props.open, (isOpen) => {
|
|||||||
class="flex-1 h-8 text-xs"
|
class="flex-1 h-8 text-xs"
|
||||||
@click="toggleMouseMode"
|
@click="toggleMouseMode"
|
||||||
>
|
>
|
||||||
<MousePointer class="h-3.5 w-3.5 mr-1" />
|
<MousePointer class="size-3.5 mr-1" />
|
||||||
{{ t('actionbar.absolute') }}
|
{{ t('actionbar.absolute') }}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
@@ -270,7 +270,7 @@ watch(() => props.open, (isOpen) => {
|
|||||||
class="flex-1 h-8 text-xs"
|
class="flex-1 h-8 text-xs"
|
||||||
@click="toggleMouseMode"
|
@click="toggleMouseMode"
|
||||||
>
|
>
|
||||||
<Move class="h-3.5 w-3.5 mr-1" />
|
<Move class="size-3.5 mr-1" />
|
||||||
{{ t('actionbar.relative') }}
|
{{ t('actionbar.relative') }}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -314,12 +314,11 @@ watch(() => props.open, (isOpen) => {
|
|||||||
<h5 class="text-xs font-medium text-muted-foreground">{{ t('actionbar.hidDeviceSettings') }}</h5>
|
<h5 class="text-xs font-medium text-muted-foreground">{{ t('actionbar.hidDeviceSettings') }}</h5>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon-xs"
|
||||||
class="h-6 w-6"
|
|
||||||
:disabled="loadingDevices"
|
:disabled="loadingDevices"
|
||||||
@click="loadDevices"
|
@click="loadDevices"
|
||||||
>
|
>
|
||||||
<RefreshCw :class="['h-3.5 w-3.5', loadingDevices && 'animate-spin']" />
|
<RefreshCw :class="['size-3.5', loadingDevices && 'animate-spin']" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -330,7 +329,7 @@ watch(() => props.open, (isOpen) => {
|
|||||||
:model-value="hidBackend"
|
:model-value="hidBackend"
|
||||||
@update:model-value="handleBackendChange"
|
@update:model-value="handleBackendChange"
|
||||||
>
|
>
|
||||||
<SelectTrigger class="h-8 w-full text-xs">
|
<SelectTrigger size="sm" class="w-full text-xs">
|
||||||
<SelectValue />
|
<SelectValue />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
@@ -348,7 +347,7 @@ watch(() => props.open, (isOpen) => {
|
|||||||
:model-value="devicePath"
|
:model-value="devicePath"
|
||||||
@update:model-value="handleDevicePathChange"
|
@update:model-value="handleDevicePathChange"
|
||||||
:disabled="availableDevicePaths.length === 0"
|
: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 value="">{{ t('actionbar.selectDevice') }}</NativeSelectOption>
|
||||||
<NativeSelectOption
|
<NativeSelectOption
|
||||||
@@ -369,7 +368,7 @@ watch(() => props.open, (isOpen) => {
|
|||||||
:model-value="String(baudrate)"
|
:model-value="String(baudrate)"
|
||||||
@update:model-value="handleBaudrateChange"
|
@update:model-value="handleBaudrateChange"
|
||||||
>
|
>
|
||||||
<SelectTrigger class="h-8 text-xs">
|
<SelectTrigger size="sm" class="text-xs">
|
||||||
<SelectValue />
|
<SelectValue />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
@@ -388,7 +387,7 @@ watch(() => props.open, (isOpen) => {
|
|||||||
:disabled="applying"
|
:disabled="applying"
|
||||||
@click="applyHidConfig"
|
@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>
|
<span>{{ applying ? t('actionbar.applying') : t('common.apply') }}</span>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -48,13 +48,13 @@ const keysDisplay = computed(() => {
|
|||||||
<div class="flex items-center justify-between text-xs px-2 py-0.5">
|
<div class="flex items-center justify-between text-xs px-2 py-0.5">
|
||||||
<div v-if="keyboardLedEnabled" class="flex items-center gap-1">
|
<div v-if="keyboardLedEnabled" class="flex items-center gap-1">
|
||||||
<Badge variant="outline" class="h-4 gap-1 px-1 text-[10px] text-foreground">
|
<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>
|
||||||
<Badge variant="outline" class="h-4 gap-1 px-1 text-[10px] text-foreground">
|
<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>
|
||||||
<Badge variant="outline" class="h-4 gap-1 px-1 text-[10px] text-foreground">
|
<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>
|
</Badge>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="text-[10px] text-muted-foreground/60">
|
<div v-else class="text-[10px] text-muted-foreground/60">
|
||||||
@@ -87,15 +87,15 @@ const keysDisplay = computed(() => {
|
|||||||
<Separator orientation="vertical" class="h-5" />
|
<Separator orientation="vertical" class="h-5" />
|
||||||
<template v-if="keyboardLedEnabled">
|
<template v-if="keyboardLedEnabled">
|
||||||
<Badge variant="outline" class="mx-1 gap-1.5 text-foreground">
|
<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') }}
|
{{ t('infobar.caps') }}
|
||||||
</Badge>
|
</Badge>
|
||||||
<Badge variant="outline" class="mx-1 gap-1.5 text-foreground">
|
<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') }}
|
{{ t('infobar.num') }}
|
||||||
</Badge>
|
</Badge>
|
||||||
<Badge variant="outline" class="mx-1 gap-1.5 text-foreground">
|
<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') }}
|
{{ t('infobar.scroll') }}
|
||||||
</Badge>
|
</Badge>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ function toggleLanguage() {
|
|||||||
:aria-label="t('common.toggleLanguage')"
|
:aria-label="t('common.toggleLanguage')"
|
||||||
@click="toggleLanguage"
|
@click="toggleLanguage"
|
||||||
>
|
>
|
||||||
<Languages class="h-4 w-4" />
|
<Languages class="size-4" />
|
||||||
<span v-if="props.labelMode !== 'hidden'">{{ buttonLabel }}</span>
|
<span v-if="props.labelMode !== 'hidden'">{{ buttonLabel }}</span>
|
||||||
<span class="sr-only">{{ t('common.toggleLanguage') }}</span>
|
<span class="sr-only">{{ t('common.toggleLanguage') }}</span>
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -153,8 +153,8 @@ const downloadProgress = ref<{
|
|||||||
} | null>(null)
|
} | null>(null)
|
||||||
|
|
||||||
const TWO_POINT_TWO_GB = 2.2 * 1024 * 1024 * 1024
|
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 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-1'
|
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 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')
|
const diskMode = computed(() => systemStore.msd?.diskMode ?? 'single')
|
||||||
@@ -688,17 +688,17 @@ onUnmounted(() => {
|
|||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
<Dialog :open="open" @update:open="emit('update:open', $event)">
|
<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">
|
<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">
|
<DialogTitle class="flex items-center gap-2">
|
||||||
<HardDrive class="h-5 w-5" />
|
<HardDrive class="size-5" />
|
||||||
{{ t('msd.title') }}
|
{{ t('msd.title') }}
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
<DialogDescription as="div" class="mt-1 flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
|
<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="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="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 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>
|
</span>
|
||||||
{{ msdConnected ? t('common.connected') : t('common.disconnected') }}
|
{{ msdConnected ? t('common.connected') : t('common.disconnected') }}
|
||||||
</span>
|
</span>
|
||||||
@@ -720,8 +720,8 @@ onUnmounted(() => {
|
|||||||
<span class="font-medium text-foreground">{{ t('msd.diskMode') }}</span>
|
<span class="font-medium text-foreground">{{ t('msd.diskMode') }}</span>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger as-child>
|
<TooltipTrigger as-child>
|
||||||
<span class="inline-flex h-4 w-4 items-center justify-center rounded-full text-muted-foreground hover:text-foreground">
|
<span class="inline-flex size-4 items-center justify-center rounded-full text-muted-foreground hover:text-foreground">
|
||||||
<Info class="h-3.5 w-3.5" />
|
<Info class="size-3.5" />
|
||||||
</span>
|
</span>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>
|
<TooltipContent>
|
||||||
@@ -747,15 +747,15 @@ onUnmounted(() => {
|
|||||||
|
|
||||||
<Separator class="shrink-0" />
|
<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">
|
<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">
|
<TabsTrigger value="images" :class="tabTriggerClass">
|
||||||
<Disc class="h-4 w-4 mr-1.5" />
|
<Disc class="size-4 mr-1.5" />
|
||||||
{{ t('msd.images') }}
|
{{ t('msd.images') }}
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
<TabsTrigger value="drive" :class="tabTriggerClass">
|
<TabsTrigger value="drive" :class="tabTriggerClass">
|
||||||
<HardDrive class="h-4 w-4 mr-1.5" />
|
<HardDrive class="size-4 mr-1.5" />
|
||||||
{{ t('msd.drive') }}
|
{{ t('msd.drive') }}
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
@@ -775,7 +775,7 @@ onUnmounted(() => {
|
|||||||
@change="handleImageUpload"
|
@change="handleImageUpload"
|
||||||
/>
|
/>
|
||||||
<Button variant="outline" size="sm" as="span" class="h-8 cursor-pointer px-2.5 text-xs">
|
<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') }}
|
{{ t('msd.uploadImage') }}
|
||||||
</Button>
|
</Button>
|
||||||
</label>
|
</label>
|
||||||
@@ -785,11 +785,11 @@ onUnmounted(() => {
|
|||||||
class="h-8 px-2.5 text-xs"
|
class="h-8 px-2.5 text-xs"
|
||||||
@click="showUrlDialog = true"
|
@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') }}
|
{{ t('msd.downloadFromUrl') }}
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="ghost" size="icon" class="h-8 w-8" @click="loadImages">
|
<Button variant="ghost" size="icon-sm" @click="loadImages">
|
||||||
<RefreshCw class="h-3.5 w-3.5" :class="{ 'animate-spin': loadingImages }" />
|
<RefreshCw class="size-3.5" :class="{ 'animate-spin': loadingImages }" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</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 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">
|
<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" />
|
<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 h-4 w-4 shrink-0 text-muted-foreground" />
|
<Disc v-else class="mt-0.5 size-4 shrink-0 text-muted-foreground" />
|
||||||
<div class="w-0 flex-1">
|
<div class="w-0 flex-1">
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger as-child>
|
<TooltipTrigger as-child>
|
||||||
@@ -836,7 +836,7 @@ onUnmounted(() => {
|
|||||||
variant="outline"
|
variant="outline"
|
||||||
class="h-4 cursor-help border-warning/50 px-1.5 text-[10px] text-warning"
|
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') }}
|
{{ t('msd.largeFileWarning') }}
|
||||||
</Badge>
|
</Badge>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
@@ -856,7 +856,7 @@ onUnmounted(() => {
|
|||||||
:disabled="operationInProgress"
|
:disabled="operationInProgress"
|
||||||
@click="unmountImageById(image.id)"
|
@click="unmountImageById(image.id)"
|
||||||
>
|
>
|
||||||
<Unlink class="h-3.5 w-3.5 mr-1" />
|
<Unlink class="size-3.5 mr-1" />
|
||||||
{{ t('msd.disconnect') }}
|
{{ t('msd.disconnect') }}
|
||||||
</Button>
|
</Button>
|
||||||
</template>
|
</template>
|
||||||
@@ -868,7 +868,7 @@ onUnmounted(() => {
|
|||||||
:disabled="operationInProgress || mediaSlotsFull"
|
:disabled="operationInProgress || mediaSlotsFull"
|
||||||
@click="connectImage(image)"
|
@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-if="connecting">{{ t('common.connecting') }}...</span>
|
||||||
<span v-else>{{ t('msd.connect') }}</span>
|
<span v-else>{{ t('msd.connect') }}</span>
|
||||||
</Button>
|
</Button>
|
||||||
@@ -876,11 +876,11 @@ onUnmounted(() => {
|
|||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
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)"
|
:disabled="operationInProgress || !!mountedImage(image.id)"
|
||||||
@click="confirmDelete('image', image.id, image.name)"
|
@click="confirmDelete('image', image.id, image.name)"
|
||||||
>
|
>
|
||||||
<Trash2 class="h-3.5 w-3.5" />
|
<Trash2 class="size-3.5" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</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">
|
<TabsContent value="drive" class="m-0 flex min-h-0 flex-1 flex-col space-y-4 pt-3">
|
||||||
<template v-if="!driveInitialized">
|
<template v-if="!driveInitialized">
|
||||||
<div class="shrink-0 text-center py-8 space-y-4">
|
<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>
|
<p class="text-sm text-muted-foreground">{{ t('msd.driveNotInitialized') }}</p>
|
||||||
<Button size="sm" @click="initializeDrive">
|
<Button size="sm" @click="initializeDrive">
|
||||||
{{ t('msd.initializeDrive') }}
|
{{ t('msd.initializeDrive') }}
|
||||||
@@ -920,7 +920,7 @@ onUnmounted(() => {
|
|||||||
>
|
>
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<div class="flex items-center gap-2">
|
<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>
|
<span class="text-sm font-medium">{{ t('msd.drive') }}</span>
|
||||||
<!-- Show size badge only when info is available -->
|
<!-- Show size badge only when info is available -->
|
||||||
<Badge v-if="driveInfo" variant="outline" class="text-xs">
|
<Badge v-if="driveInfo" variant="outline" class="text-xs">
|
||||||
@@ -933,8 +933,8 @@ onUnmounted(() => {
|
|||||||
</Badge>
|
</Badge>
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger as-child>
|
<TooltipTrigger as-child>
|
||||||
<span class="inline-flex h-4 w-4 items-center justify-center text-muted-foreground hover:text-foreground">
|
<span class="inline-flex size-4 items-center justify-center text-muted-foreground hover:text-foreground">
|
||||||
<Info class="h-3.5 w-3.5" />
|
<Info class="size-3.5" />
|
||||||
</span>
|
</span>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>
|
<TooltipContent>
|
||||||
@@ -958,9 +958,9 @@ onUnmounted(() => {
|
|||||||
</template>
|
</template>
|
||||||
<template v-else-if="driveConnectedToTarget">
|
<template v-else-if="driveConnectedToTarget">
|
||||||
<Badge variant="default" class="h-8 px-2 text-xs">
|
<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="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>
|
</span>
|
||||||
{{ t('common.connected') }}
|
{{ t('common.connected') }}
|
||||||
</Badge>
|
</Badge>
|
||||||
@@ -972,7 +972,7 @@ onUnmounted(() => {
|
|||||||
:disabled="operationInProgress"
|
:disabled="operationInProgress"
|
||||||
@click="unmountMedia(driveMedia)"
|
@click="unmountMedia(driveMedia)"
|
||||||
>
|
>
|
||||||
<Unlink class="h-3.5 w-3.5 mr-1" />
|
<Unlink class="size-3.5 mr-1" />
|
||||||
{{ t('msd.disconnect') }}
|
{{ t('msd.disconnect') }}
|
||||||
</Button>
|
</Button>
|
||||||
</template>
|
</template>
|
||||||
@@ -984,7 +984,7 @@ onUnmounted(() => {
|
|||||||
:disabled="operationInProgress || mediaSlotsFull || !!driveError"
|
:disabled="operationInProgress || mediaSlotsFull || !!driveError"
|
||||||
@click="connectDrive"
|
@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-if="connecting">{{ t('common.connecting') }}...</span>
|
||||||
<span v-else>{{ t('msd.connect') }}</span>
|
<span v-else>{{ t('msd.connect') }}</span>
|
||||||
</Button>
|
</Button>
|
||||||
@@ -992,11 +992,11 @@ onUnmounted(() => {
|
|||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
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"
|
:disabled="operationInProgress || driveConnectedToTarget"
|
||||||
@click="showDeleteDriveDialog = true"
|
@click="showDeleteDriveDialog = true"
|
||||||
>
|
>
|
||||||
<Trash2 class="h-3.5 w-3.5" />
|
<Trash2 class="size-3.5" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1024,15 +1024,15 @@ onUnmounted(() => {
|
|||||||
v-if="currentPath !== '/'"
|
v-if="currentPath !== '/'"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
class="h-8 w-8 shrink-0"
|
class="size-8 shrink-0"
|
||||||
:disabled="driveConnectedToTarget"
|
:disabled="driveConnectedToTarget"
|
||||||
@click="navigateUp"
|
@click="navigateUp"
|
||||||
>
|
>
|
||||||
<ArrowLeft class="h-3.5 w-3.5" />
|
<ArrowLeft class="size-3.5" />
|
||||||
</Button>
|
</Button>
|
||||||
<nav class="flex items-center text-xs min-w-0 overflow-hidden">
|
<nav class="flex items-center text-xs min-w-0 overflow-hidden">
|
||||||
<template v-for="(crumb, index) in breadcrumbs" :key="crumb.path">
|
<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
|
<Button
|
||||||
variant="link"
|
variant="link"
|
||||||
size="sm"
|
size="sm"
|
||||||
@@ -1059,10 +1059,10 @@ onUnmounted(() => {
|
|||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
as="span"
|
as="span"
|
||||||
class="h-8 w-8"
|
class="size-8"
|
||||||
:class="driveConnectedToTarget ? 'cursor-not-allowed opacity-40' : 'cursor-pointer'"
|
:class="driveConnectedToTarget ? 'cursor-not-allowed opacity-40' : 'cursor-pointer'"
|
||||||
>
|
>
|
||||||
<Upload class="h-3.5 w-3.5" />
|
<Upload class="size-3.5" />
|
||||||
</Button>
|
</Button>
|
||||||
</label>
|
</label>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
@@ -1076,11 +1076,11 @@ onUnmounted(() => {
|
|||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
class="h-8 w-8"
|
class="size-8"
|
||||||
:disabled="driveConnectedToTarget"
|
:disabled="driveConnectedToTarget"
|
||||||
@click="showNewFolderDialog = true"
|
@click="showNewFolderDialog = true"
|
||||||
>
|
>
|
||||||
<FolderPlus class="h-3.5 w-3.5" />
|
<FolderPlus class="size-3.5" />
|
||||||
</Button>
|
</Button>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent v-if="driveConnectedToTarget">
|
<TooltipContent v-if="driveConnectedToTarget">
|
||||||
@@ -1090,11 +1090,11 @@ onUnmounted(() => {
|
|||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
class="h-8 w-8"
|
class="size-8"
|
||||||
:disabled="driveConnectedToTarget"
|
:disabled="driveConnectedToTarget"
|
||||||
@click="loadDriveFiles"
|
@click="loadDriveFiles"
|
||||||
>
|
>
|
||||||
<RefreshCw class="h-3.5 w-3.5" :class="{ 'animate-spin': loadingDrive }" />
|
<RefreshCw class="size-3.5" :class="{ 'animate-spin': loadingDrive }" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1129,8 +1129,8 @@ onUnmounted(() => {
|
|||||||
class="flex items-center gap-2 cursor-pointer flex-1 min-w-0"
|
class="flex items-center gap-2 cursor-pointer flex-1 min-w-0"
|
||||||
@click="file.is_dir && navigateTo(file.path)"
|
@click="file.is_dir && navigateTo(file.path)"
|
||||||
>
|
>
|
||||||
<Folder v-if="file.is_dir" class="h-4 w-4 shrink-0 text-info" />
|
<Folder v-if="file.is_dir" class="size-4 shrink-0 text-info" />
|
||||||
<File v-else class="h-4 w-4 text-muted-foreground shrink-0" />
|
<File v-else class="size-4 text-muted-foreground shrink-0" />
|
||||||
<div class="min-w-0">
|
<div class="min-w-0">
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger as-child>
|
<TooltipTrigger as-child>
|
||||||
@@ -1150,22 +1150,22 @@ onUnmounted(() => {
|
|||||||
v-if="!file.is_dir"
|
v-if="!file.is_dir"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
class="h-8 w-8"
|
class="size-8"
|
||||||
as="a"
|
as="a"
|
||||||
:href="msdApi.downloadDriveFile(file.path)"
|
:href="msdApi.downloadDriveFile(file.path)"
|
||||||
download
|
download
|
||||||
>
|
>
|
||||||
<Download class="h-3.5 w-3.5" />
|
<Download class="size-3.5" />
|
||||||
</Button>
|
</Button>
|
||||||
<!-- ③ Delete disabled when drive connected to target -->
|
<!-- ③ Delete disabled when drive connected to target -->
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
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"
|
:disabled="driveConnectedToTarget"
|
||||||
@click="confirmDelete('file', file.path, file.name)"
|
@click="confirmDelete('file', file.path, file.name)"
|
||||||
>
|
>
|
||||||
<Trash2 class="h-3.5 w-3.5" />
|
<Trash2 class="size-3.5" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1339,7 +1339,7 @@ onUnmounted(() => {
|
|||||||
{{ t('common.cancel') }}
|
{{ t('common.cancel') }}
|
||||||
</Button>
|
</Button>
|
||||||
<Button @click="confirmImageMount" :disabled="connecting || mediaSlotsFull">
|
<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-if="connecting">{{ t('common.connecting') }}...</span>
|
||||||
<span v-else>{{ t('msd.connect') }}</span>
|
<span v-else>{{ t('msd.connect') }}</span>
|
||||||
</Button>
|
</Button>
|
||||||
@@ -1366,7 +1366,7 @@ onUnmounted(() => {
|
|||||||
<DialogContent>
|
<DialogContent>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle class="flex items-center gap-2">
|
<DialogTitle class="flex items-center gap-2">
|
||||||
<Globe class="h-5 w-5" />
|
<Globe class="size-5" />
|
||||||
{{ t('msd.downloadFromUrl') }}
|
{{ t('msd.downloadFromUrl') }}
|
||||||
</DialogTitle>
|
</DialogTitle>
|
||||||
<DialogDescription>{{ t('msd.downloadFromUrlDesc') }}</DialogDescription>
|
<DialogDescription>{{ t('msd.downloadFromUrlDesc') }}</DialogDescription>
|
||||||
@@ -1425,11 +1425,11 @@ onUnmounted(() => {
|
|||||||
:disabled="!downloadUrl.trim()"
|
:disabled="!downloadUrl.trim()"
|
||||||
@click="startUrlDownload"
|
@click="startUrlDownload"
|
||||||
>
|
>
|
||||||
<Download class="h-4 w-4 mr-1" />
|
<Download class="size-4 mr-1" />
|
||||||
{{ t('msd.download') }}
|
{{ t('msd.download') }}
|
||||||
</Button>
|
</Button>
|
||||||
<Button v-else variant="destructive" @click="cancelUrlDownload">
|
<Button v-else variant="destructive" @click="cancelUrlDownload">
|
||||||
<X class="h-4 w-4 mr-1" />
|
<X class="size-4 mr-1" />
|
||||||
{{ t('common.cancel') }}
|
{{ t('common.cancel') }}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
|
|||||||
@@ -205,7 +205,7 @@ function handleKeydown(e: KeyboardEvent) {
|
|||||||
|
|
||||||
<!-- Warning for untypable characters -->
|
<!-- Warning for untypable characters -->
|
||||||
<Alert v-if="hasUntypableChars && !isPasting" variant="warning">
|
<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>
|
<AlertTitle>{{ t('paste.untypableWarning') }}</AlertTitle>
|
||||||
<AlertDescription>
|
<AlertDescription>
|
||||||
{{ t('paste.untypableChars', { chars: textAnalysis?.untypableChars.slice(0, 5).map(c => c === '\n' ? '\\n' : c === '\r' ? '\\r' : c === '\t' ? '\\t' : c).join(', ') }) }}
|
{{ 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') }}
|
{{ t('common.cancel') }}
|
||||||
</Button>
|
</Button>
|
||||||
<Button v-else variant="ghost" size="sm" @click="cancelPaste">
|
<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') }}
|
{{ t('paste.stop') }}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
@@ -242,7 +242,7 @@ function handleKeydown(e: KeyboardEvent) {
|
|||||||
:disabled="!text.trim() || isPasting"
|
:disabled="!text.trim() || isPasting"
|
||||||
@click="handlePaste"
|
@click="handlePaste"
|
||||||
>
|
>
|
||||||
<CornerDownLeft class="h-4 w-4 mr-1.5" />
|
<CornerDownLeft class="size-4 mr-1.5" />
|
||||||
{{ t('paste.confirm') }}
|
{{ t('paste.confirm') }}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -141,7 +141,7 @@ const statusBadgeText = computed(() => {
|
|||||||
<template v-if="compact">
|
<template v-if="compact">
|
||||||
<!-- Compact: single row with dot + abbreviated title -->
|
<!-- Compact: single row with dot + abbreviated title -->
|
||||||
<div class="flex items-center gap-1">
|
<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>
|
<span class="text-[10px] text-muted-foreground leading-tight truncate">{{ title }}</span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -150,7 +150,7 @@ const statusBadgeText = computed(() => {
|
|||||||
<span class="font-medium text-foreground text-xs truncate">{{ title }}</span>
|
<span class="font-medium text-foreground text-xs truncate">{{ title }}</span>
|
||||||
<!-- Bottom: Status dot + Quick info -->
|
<!-- Bottom: Status dot + Quick info -->
|
||||||
<div class="flex items-center gap-1.5">
|
<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">
|
<span class="text-[11px] text-muted-foreground leading-tight truncate">
|
||||||
{{ quickInfo || subtitle || statusText }}
|
{{ quickInfo || subtitle || statusText }}
|
||||||
</span>
|
</span>
|
||||||
@@ -169,7 +169,7 @@ const statusBadgeText = computed(() => {
|
|||||||
status === 'error' ? 'bg-destructive/10 text-destructive' :
|
status === 'error' ? 'bg-destructive/10 text-destructive' :
|
||||||
'bg-muted text-muted-foreground'
|
'bg-muted text-muted-foreground'
|
||||||
)">
|
)">
|
||||||
<component :is="StatusIcon" class="h-5 w-5" />
|
<component :is="StatusIcon" class="size-5" />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-1 min-w-0">
|
<div class="flex-1 min-w-0">
|
||||||
<h4 class="font-semibold text-sm">{{ title }}</h4>
|
<h4 class="font-semibold text-sm">{{ title }}</h4>
|
||||||
@@ -178,7 +178,7 @@ const statusBadgeText = computed(() => {
|
|||||||
v-if="statusIcon"
|
v-if="statusIcon"
|
||||||
:is="statusIcon"
|
:is="statusIcon"
|
||||||
:class="cn(
|
:class="cn(
|
||||||
'h-3.5 w-3.5',
|
'size-3.5',
|
||||||
status === 'connected' ? 'text-success' :
|
status === 'connected' ? 'text-success' :
|
||||||
status === 'connecting' ? 'text-warning animate-spin' :
|
status === 'connecting' ? 'text-warning animate-spin' :
|
||||||
status === 'error' ? 'text-destructive' :
|
status === 'error' ? 'text-destructive' :
|
||||||
@@ -229,7 +229,7 @@ const statusBadgeText = computed(() => {
|
|||||||
<template v-if="compact">
|
<template v-if="compact">
|
||||||
<!-- Compact: single row with dot + abbreviated title -->
|
<!-- Compact: single row with dot + abbreviated title -->
|
||||||
<div class="flex items-center gap-1">
|
<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>
|
<span class="text-[10px] text-muted-foreground leading-tight truncate">{{ title }}</span>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -238,7 +238,7 @@ const statusBadgeText = computed(() => {
|
|||||||
<span class="font-medium text-foreground text-xs truncate">{{ title }}</span>
|
<span class="font-medium text-foreground text-xs truncate">{{ title }}</span>
|
||||||
<!-- Bottom: Status dot + Quick info -->
|
<!-- Bottom: Status dot + Quick info -->
|
||||||
<div class="flex items-center gap-1.5">
|
<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">
|
<span class="text-[11px] text-muted-foreground leading-tight truncate">
|
||||||
{{ quickInfo || subtitle || statusText }}
|
{{ quickInfo || subtitle || statusText }}
|
||||||
</span>
|
</span>
|
||||||
@@ -257,7 +257,7 @@ const statusBadgeText = computed(() => {
|
|||||||
status === 'error' ? 'bg-destructive/10 text-destructive' :
|
status === 'error' ? 'bg-destructive/10 text-destructive' :
|
||||||
'bg-muted text-muted-foreground'
|
'bg-muted text-muted-foreground'
|
||||||
)">
|
)">
|
||||||
<component :is="StatusIcon" class="h-5 w-5" />
|
<component :is="StatusIcon" class="size-5" />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-1 min-w-0">
|
<div class="flex-1 min-w-0">
|
||||||
<h4 class="font-semibold text-sm">{{ title }}</h4>
|
<h4 class="font-semibold text-sm">{{ title }}</h4>
|
||||||
@@ -266,7 +266,7 @@ const statusBadgeText = computed(() => {
|
|||||||
v-if="statusIcon"
|
v-if="statusIcon"
|
||||||
:is="statusIcon"
|
:is="statusIcon"
|
||||||
:class="cn(
|
:class="cn(
|
||||||
'h-3.5 w-3.5',
|
'size-3.5',
|
||||||
status === 'connected' ? 'text-success' :
|
status === 'connected' ? 'text-success' :
|
||||||
status === 'connecting' ? 'text-warning animate-spin' :
|
status === 'connecting' ? 'text-warning animate-spin' :
|
||||||
status === 'error' ? 'text-destructive' :
|
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">
|
<DialogHeader class="px-3 sm:px-4 py-1 border-b shrink-0">
|
||||||
<DialogTitle class="flex h-8 items-center justify-between w-full">
|
<DialogTitle class="flex h-8 items-center justify-between w-full">
|
||||||
<div class="flex items-center gap-2 min-w-0">
|
<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>
|
<span class="truncate text-sm font-semibold">{{ t('extensions.ttyd.title') }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-1 shrink-0">
|
<div class="flex items-center gap-1 shrink-0">
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon-sm"
|
||||||
class="h-7 w-7"
|
|
||||||
@click="openTerminalInNewTab"
|
@click="openTerminalInNewTab"
|
||||||
:aria-label="t('extensions.ttyd.openInNewTab')"
|
:aria-label="t('extensions.ttyd.openInNewTab')"
|
||||||
:title="t('extensions.ttyd.openInNewTab')"
|
:title="t('extensions.ttyd.openInNewTab')"
|
||||||
>
|
>
|
||||||
<ExternalLink class="h-3.5 w-3.5" />
|
<ExternalLink class="size-3.5" />
|
||||||
</Button>
|
</Button>
|
||||||
<DialogClose as-child>
|
<DialogClose as-child>
|
||||||
<Button variant="ghost" size="icon" class="h-7 w-7">
|
<Button variant="ghost" size="icon-sm">
|
||||||
<X class="h-3.5 w-3.5" />
|
<X class="size-3.5" />
|
||||||
<span class="sr-only">Close</span>
|
<span class="sr-only">Close</span>
|
||||||
</Button>
|
</Button>
|
||||||
</DialogClose>
|
</DialogClose>
|
||||||
|
|||||||
@@ -249,11 +249,11 @@ onUnmounted(() => {
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
<CardFooter class="border-t pt-4 justify-end">
|
<CardFooter class="border-t pt-4 justify-end">
|
||||||
<Button v-if="!enabled" :disabled="statusLoading" @click="enrollmentOpen = true">
|
<Button v-if="!enabled" :disabled="statusLoading" @click="enrollmentOpen = true">
|
||||||
<ShieldCheck class="h-4 w-4" />
|
<ShieldCheck class="size-4" />
|
||||||
{{ t('settings.totp.enable') }}
|
{{ t('settings.totp.enable') }}
|
||||||
</Button>
|
</Button>
|
||||||
<Button v-else variant="destructive" :disabled="statusLoading" @click="disableOpen = true">
|
<Button v-else variant="destructive" :disabled="statusLoading" @click="disableOpen = true">
|
||||||
<ShieldOff class="h-4 w-4" />
|
<ShieldOff class="size-4" />
|
||||||
{{ t('settings.totp.disable') }}
|
{{ t('settings.totp.disable') }}
|
||||||
</Button>
|
</Button>
|
||||||
</CardFooter>
|
</CardFooter>
|
||||||
@@ -283,7 +283,7 @@ onUnmounted(() => {
|
|||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<Label for="totp-enrollment-code">{{ t('auth.totpCode') }}</Label>
|
<Label for="totp-enrollment-code">{{ t('auth.totpCode') }}</Label>
|
||||||
<div class="relative">
|
<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)" />
|
<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>
|
||||||
</div>
|
</div>
|
||||||
@@ -296,11 +296,11 @@ onUnmounted(() => {
|
|||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button variant="outline" :disabled="enrollmentLoading" @click="enrollmentOpen = false">{{ t('common.cancel') }}</Button>
|
<Button variant="outline" :disabled="enrollmentLoading" @click="enrollmentOpen = false">{{ t('common.cancel') }}</Button>
|
||||||
<Button v-if="enrollmentStep === 'password'" :disabled="enrollmentLoading" @click="beginEnrollment">
|
<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') }}
|
{{ t('common.next') }}
|
||||||
</Button>
|
</Button>
|
||||||
<Button v-else :disabled="enrollmentLoading" @click="confirmEnrollment">
|
<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') }}
|
{{ t('common.confirm') }}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
@@ -329,7 +329,7 @@ onUnmounted(() => {
|
|||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button variant="outline" :disabled="disableLoading" @click="disableOpen = false">{{ t('common.cancel') }}</Button>
|
<Button variant="outline" :disabled="disableLoading" @click="disableOpen = false">{{ t('common.cancel') }}</Button>
|
||||||
<Button variant="destructive" :disabled="disableLoading" @click="disableTotp">
|
<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') }}
|
{{ t('settings.totp.disable') }}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
|
|||||||
@@ -594,8 +594,8 @@ watch(
|
|||||||
<template>
|
<template>
|
||||||
<Popover :open="open" @update:open="emit('update:open', $event)">
|
<Popover :open="open" @update:open="emit('update:open', $event)">
|
||||||
<PopoverTrigger as-child>
|
<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">
|
<Button variant="ghost" size="sm" class="size-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" />
|
<Monitor class="size-3.5 sm:size-4" />
|
||||||
<span class="hidden sm:inline">{{ buttonText }}</span>
|
<span class="hidden sm:inline">{{ buttonText }}</span>
|
||||||
</Button>
|
</Button>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
@@ -612,7 +612,7 @@ watch(
|
|||||||
<!-- Mode Selection -->
|
<!-- Mode Selection -->
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<div class="flex items-center gap-1">
|
<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" />
|
<HelpTooltip :content="t('actionbar.videoModeHint')" icon-size="sm" side="right" />
|
||||||
</div>
|
</div>
|
||||||
<Select
|
<Select
|
||||||
@@ -620,7 +620,7 @@ watch(
|
|||||||
@update:model-value="handleVideoModeChange"
|
@update:model-value="handleVideoModeChange"
|
||||||
:disabled="loadingCodecs || availableCodecs.length === 0 || isRtspCodecLocked"
|
: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">
|
<div v-if="selectedCodecInfo" class="flex items-center gap-1.5 truncate">
|
||||||
<span class="truncate">{{ selectedCodecInfo.name }}</span>
|
<span class="truncate">{{ selectedCodecInfo.name }}</span>
|
||||||
<span
|
<span
|
||||||
@@ -673,7 +673,7 @@ watch(
|
|||||||
<!-- Bitrate Preset - Only shown for WebRTC modes -->
|
<!-- Bitrate Preset - Only shown for WebRTC modes -->
|
||||||
<div v-if="props.videoMode !== 'mjpeg'" class="space-y-2">
|
<div v-if="props.videoMode !== 'mjpeg'" class="space-y-2">
|
||||||
<div class="flex items-center gap-1">
|
<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" />
|
<HelpTooltip :content="t('help.videoBitratePreset')" icon-size="sm" />
|
||||||
</div>
|
</div>
|
||||||
<div class="grid grid-cols-3 gap-1.5">
|
<div class="grid grid-cols-3 gap-1.5">
|
||||||
@@ -687,7 +687,7 @@ watch(
|
|||||||
:disabled="applyingBitrate"
|
:disabled="applyingBitrate"
|
||||||
@click="handleBitratePresetChange('Speed')"
|
@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>
|
<span class="text-[10px] font-medium">{{ t('actionbar.bitrateSpeed') }}</span>
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
@@ -700,7 +700,7 @@ watch(
|
|||||||
:disabled="applyingBitrate"
|
:disabled="applyingBitrate"
|
||||||
@click="handleBitratePresetChange('Balanced')"
|
@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>
|
<span class="text-[10px] font-medium">{{ t('actionbar.bitrateBalanced') }}</span>
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
@@ -713,7 +713,7 @@ watch(
|
|||||||
:disabled="applyingBitrate"
|
:disabled="applyingBitrate"
|
||||||
@click="handleBitratePresetChange('Quality')"
|
@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>
|
<span class="text-[10px] font-medium">{{ t('actionbar.bitrateQuality') }}</span>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -733,23 +733,22 @@ watch(
|
|||||||
<h5 class="text-xs font-medium text-muted-foreground">{{ t('actionbar.deviceSettings') }}</h5>
|
<h5 class="text-xs font-medium text-muted-foreground">{{ t('actionbar.deviceSettings') }}</h5>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon-xs"
|
||||||
class="h-6 w-6"
|
|
||||||
:disabled="loadingDevices"
|
:disabled="loadingDevices"
|
||||||
@click="loadDevices"
|
@click="loadDevices"
|
||||||
>
|
>
|
||||||
<RefreshCw :class="['h-3.5 w-3.5', loadingDevices && 'animate-spin']" />
|
<RefreshCw :class="['size-3.5', loadingDevices && 'animate-spin']" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Device Selection -->
|
<!-- Device Selection -->
|
||||||
<div class="space-y-2">
|
<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
|
<NativeSelect
|
||||||
:model-value="selectedDevice"
|
:model-value="selectedDevice"
|
||||||
@update:model-value="handleDeviceChange"
|
@update:model-value="handleDeviceChange"
|
||||||
:disabled="loadingDevices || devices.length === 0"
|
: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 value="">{{ loadingDevices ? t('common.loading') : t('actionbar.selectDevice') }}</NativeSelectOption>
|
||||||
<NativeSelectOption
|
<NativeSelectOption
|
||||||
@@ -765,13 +764,13 @@ watch(
|
|||||||
|
|
||||||
<!-- Format Selection -->
|
<!-- Format Selection -->
|
||||||
<div class="space-y-2">
|
<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
|
<Select
|
||||||
:model-value="selectedFormat"
|
:model-value="selectedFormat"
|
||||||
@update:model-value="handleFormatChange"
|
@update:model-value="handleFormatChange"
|
||||||
:disabled="!selectedDevice || availableFormats.length === 0"
|
: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">
|
<div v-if="selectedFormatInfo" class="flex min-w-0 items-center gap-1.5">
|
||||||
<span class="truncate">{{ selectedFormatInfo.description }}</span>
|
<span class="truncate">{{ selectedFormatInfo.description }}</span>
|
||||||
<span
|
<span
|
||||||
@@ -825,12 +824,12 @@ watch(
|
|||||||
|
|
||||||
<!-- Resolution Selection -->
|
<!-- Resolution Selection -->
|
||||||
<div class="space-y-2">
|
<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
|
<NativeSelect
|
||||||
:model-value="selectedResolution"
|
:model-value="selectedResolution"
|
||||||
@update:model-value="handleResolutionChange"
|
@update:model-value="handleResolutionChange"
|
||||||
:disabled="!selectedFormat || availableResolutions.length === 0"
|
: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 value="">{{ t('actionbar.selectResolution') }}</NativeSelectOption>
|
||||||
<NativeSelectOption
|
<NativeSelectOption
|
||||||
@@ -846,12 +845,12 @@ watch(
|
|||||||
|
|
||||||
<!-- FPS Selection -->
|
<!-- FPS Selection -->
|
||||||
<div class="space-y-2">
|
<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
|
<NativeSelect
|
||||||
:model-value="String(selectedFps)"
|
:model-value="String(selectedFps)"
|
||||||
@update:model-value="handleFpsChange"
|
@update:model-value="handleFpsChange"
|
||||||
:disabled="!selectedResolution || availableFps.length === 0"
|
: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 value="">{{ t('actionbar.selectFps') }}</NativeSelectOption>
|
||||||
<NativeSelectOption
|
<NativeSelectOption
|
||||||
@@ -871,7 +870,7 @@ watch(
|
|||||||
:disabled="applying || !selectedDevice || !selectedFormat"
|
:disabled="applying || !selectedDevice || !selectedFormat"
|
||||||
@click="applyVideoConfig"
|
@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>
|
<span>{{ applying ? t('actionbar.applying') : t('common.apply') }}</span>
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,11 +3,15 @@ import type { HTMLAttributes } from "vue"
|
|||||||
import { useVModel } from "@vueuse/core"
|
import { useVModel } from "@vueuse/core"
|
||||||
import { cn } from "@/lib/utils"
|
import { cn } from "@/lib/utils"
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = withDefaults(
|
||||||
|
defineProps<{
|
||||||
defaultValue?: string | number
|
defaultValue?: string | number
|
||||||
modelValue?: string | number
|
modelValue?: string | number
|
||||||
class?: HTMLAttributes["class"]
|
class?: HTMLAttributes["class"]
|
||||||
}>()
|
size?: "sm" | "default"
|
||||||
|
}>(),
|
||||||
|
{ size: "default" },
|
||||||
|
)
|
||||||
|
|
||||||
const emits = defineEmits<{
|
const emits = defineEmits<{
|
||||||
(e: "update:modelValue", payload: string | number): void
|
(e: "update:modelValue", payload: string | number): void
|
||||||
@@ -23,8 +27,9 @@ const modelValue = useVModel(props, "modelValue", emits, {
|
|||||||
<input
|
<input
|
||||||
v-model="modelValue"
|
v-model="modelValue"
|
||||||
data-slot="input"
|
data-slot="input"
|
||||||
|
:data-size="size"
|
||||||
:class="cn(
|
: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',
|
'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',
|
'aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive',
|
||||||
props.class,
|
props.class,
|
||||||
|
|||||||
@@ -9,7 +9,10 @@ defineOptions({
|
|||||||
inheritAttrs: false,
|
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<{
|
const emit = defineEmits<{
|
||||||
"update:modelValue": [value: AcceptableValue]
|
"update:modelValue": [value: AcceptableValue]
|
||||||
@@ -20,7 +23,7 @@ const modelValue = useVModel(props, "modelValue", emit, {
|
|||||||
defaultValue: "",
|
defaultValue: "",
|
||||||
})
|
})
|
||||||
|
|
||||||
const delegatedProps = reactiveOmit(props, "class")
|
const delegatedProps = reactiveOmit(props, "class", "size")
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -32,8 +35,9 @@ const delegatedProps = reactiveOmit(props, "class")
|
|||||||
v-bind="{ ...$attrs, ...delegatedProps }"
|
v-bind="{ ...$attrs, ...delegatedProps }"
|
||||||
v-model="modelValue"
|
v-model="modelValue"
|
||||||
data-slot="native-select"
|
data-slot="native-select"
|
||||||
|
:data-size="size"
|
||||||
:class="cn(
|
: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',
|
'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',
|
'aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive',
|
||||||
props.class,
|
props.class,
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ const { toggleSidebar } = useSidebar()
|
|||||||
data-slot="sidebar-trigger"
|
data-slot="sidebar-trigger"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
:class="cn('h-7 w-7', props.class)"
|
:class="cn('size-7', props.class)"
|
||||||
@click="toggleSidebar"
|
@click="toggleSidebar"
|
||||||
>
|
>
|
||||||
<PanelLeft />
|
<PanelLeft />
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { createContext } from "reka-ui"
|
|||||||
|
|
||||||
export const SIDEBAR_COOKIE_NAME = "sidebar_state"
|
export const SIDEBAR_COOKIE_NAME = "sidebar_state"
|
||||||
export const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7
|
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_MOBILE = "18rem"
|
||||||
export const SIDEBAR_WIDTH_ICON = "3rem"
|
export const SIDEBAR_WIDTH_ICON = "3rem"
|
||||||
export const SIDEBAR_KEYBOARD_SHORTCUT = "b"
|
export const SIDEBAR_KEYBOARD_SHORTCUT = "b"
|
||||||
|
|||||||
@@ -155,3 +155,30 @@ body {
|
|||||||
#app {
|
#app {
|
||||||
min-height: 100vh;
|
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>
|
||||||
<div class="mx-1 hidden h-6 w-px bg-border md:block" />
|
<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">
|
<Button variant="ghost" size="icon-sm" class="hidden md:flex" :aria-label="t('common.toggleTheme')" @click="toggleTheme">
|
||||||
<Sun v-if="isDark" class="h-4 w-4" />
|
<Sun v-if="isDark" class="size-4" />
|
||||||
<Moon v-else class="h-4 w-4" />
|
<Moon v-else class="size-4" />
|
||||||
</Button>
|
</Button>
|
||||||
<LanguageToggleButton class="h-8 w-8 hidden md:flex" />
|
<LanguageToggleButton class="size-8 hidden md:flex" />
|
||||||
<DropdownMenu>
|
<DropdownMenu>
|
||||||
<DropdownMenuTrigger as-child>
|
<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>
|
<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>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="end">
|
<DropdownMenuContent align="end">
|
||||||
<DropdownMenuItem class="md:hidden" @click="toggleTheme">
|
<DropdownMenuItem class="md:hidden" @click="toggleTheme">
|
||||||
<Sun v-if="isDark" class="h-4 w-4 mr-2" />
|
<Sun v-if="isDark" class="size-4 mr-2" />
|
||||||
<Moon v-else class="h-4 w-4 mr-2" />
|
<Moon v-else class="size-4 mr-2" />
|
||||||
{{ isDark ? t('settings.lightMode') : t('settings.darkMode') }}
|
{{ isDark ? t('settings.lightMode') : t('settings.darkMode') }}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem as-child class="md:hidden p-0">
|
<DropdownMenuItem as-child class="md:hidden p-0">
|
||||||
@@ -3145,12 +3145,12 @@ onUnmounted(() => {
|
|||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuSeparator class="md:hidden" />
|
<DropdownMenuSeparator class="md:hidden" />
|
||||||
<DropdownMenuItem @click="changePasswordDialogOpen = true">
|
<DropdownMenuItem @click="changePasswordDialogOpen = true">
|
||||||
<KeyRound class="h-4 w-4 mr-2" />
|
<KeyRound class="size-4 mr-2" />
|
||||||
{{ t('auth.changePassword') }}
|
{{ t('auth.changePassword') }}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
<DropdownMenuItem @click="logout">
|
<DropdownMenuItem @click="logout">
|
||||||
<LogOut class="h-4 w-4 mr-2" />
|
<LogOut class="size-4 mr-2" />
|
||||||
{{ t('auth.logout') }}
|
{{ t('auth.logout') }}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
@@ -3179,13 +3179,7 @@ onUnmounted(() => {
|
|||||||
@open-computer-use="openComputerUse"
|
@open-computer-use="openComputerUse"
|
||||||
/>
|
/>
|
||||||
<div class="flex-1 overflow-hidden relative">
|
<div class="flex-1 overflow-hidden relative">
|
||||||
<div
|
<div class="absolute inset-0 dot-grid-bg" />
|
||||||
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="relative flex h-full w-full min-w-0 items-stretch gap-3 p-1 sm:p-4">
|
<div class="relative flex h-full w-full min-w-0 items-stretch gap-3 p-1 sm:p-4">
|
||||||
<div
|
<div
|
||||||
class="flex min-w-0 flex-1 items-center justify-center transition-all duration-300"
|
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 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>
|
</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">
|
<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>{{ webrtcLoadingMessage }}</span>
|
||||||
<span
|
<span
|
||||||
@@ -3339,7 +3333,7 @@ onUnmounted(() => {
|
|||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<MonitorOff
|
<MonitorOff
|
||||||
class="h-10 w-10 sm:h-16 sm:w-16"
|
class="size-10 sm:size-16"
|
||||||
:class="{
|
:class="{
|
||||||
'text-slate-200': signalOverlayInfo.tone === 'info',
|
'text-slate-200': signalOverlayInfo.tone === 'info',
|
||||||
'text-red-300': signalOverlayInfo.tone === 'error',
|
'text-red-300': signalOverlayInfo.tone === 'error',
|
||||||
@@ -3368,7 +3362,7 @@ onUnmounted(() => {
|
|||||||
v-if="videoError && !videoLoading"
|
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"
|
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">
|
<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="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>
|
<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>
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<Button variant="secondary" size="sm" @click="reloadPage">
|
<Button variant="secondary" size="sm" @click="reloadPage">
|
||||||
<RefreshCw class="h-4 w-4 mr-2" />
|
<RefreshCw class="size-4 mr-2" />
|
||||||
{{ t('console.reconnect') }}
|
{{ t('console.reconnect') }}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -3470,7 +3464,7 @@ onUnmounted(() => {
|
|||||||
{{ t('common.cancel') }}
|
{{ t('common.cancel') }}
|
||||||
</Button>
|
</Button>
|
||||||
<Button @click="handleChangePassword" :disabled="changingPassword">
|
<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') }}
|
{{ t('common.confirm') }}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
|
|||||||
@@ -120,27 +120,27 @@ function returnToPassword(keepError = false) {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<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">
|
<Card class="relative w-full max-w-sm">
|
||||||
<div class="absolute top-4 right-4">
|
<div class="absolute top-4 right-4">
|
||||||
<LanguageToggleButton />
|
<LanguageToggleButton />
|
||||||
</div>
|
</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">
|
<div class="mx-auto flex justify-center">
|
||||||
<BrandMark size="xl" />
|
<BrandMark size="lg" />
|
||||||
</div>
|
</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>
|
<CardDescription>{{ step === 'password' ? t('auth.login') : t('auth.totpPrompt') }}</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
|
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<form v-if="step === 'password'" @submit.prevent="handleLogin">
|
<form v-if="step === 'password'" @submit.prevent="handleLogin">
|
||||||
<FieldGroup>
|
<FieldGroup class="gap-5">
|
||||||
<Field>
|
<Field>
|
||||||
<FieldLabel for="username">{{ t('auth.username') }}</FieldLabel>
|
<FieldLabel for="username">{{ t('auth.username') }}</FieldLabel>
|
||||||
<div class="relative">
|
<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
|
<Input
|
||||||
id="username"
|
id="username"
|
||||||
v-model="username"
|
v-model="username"
|
||||||
@@ -164,7 +164,7 @@ function returnToPassword(keepError = false) {
|
|||||||
class="h-auto gap-1 p-0 text-xs text-muted-foreground"
|
class="h-auto gap-1 p-0 text-xs text-muted-foreground"
|
||||||
>
|
>
|
||||||
{{ t('auth.forgotPassword') }}
|
{{ t('auth.forgotPassword') }}
|
||||||
<CircleHelp class="h-3.5 w-3.5" />
|
<CircleHelp class="size-3.5" />
|
||||||
</Button>
|
</Button>
|
||||||
</PopoverTrigger>
|
</PopoverTrigger>
|
||||||
<PopoverContent class="w-80 p-3" align="end">
|
<PopoverContent class="w-80 p-3" align="end">
|
||||||
@@ -175,7 +175,7 @@ function returnToPassword(keepError = false) {
|
|||||||
</Popover>
|
</Popover>
|
||||||
</div>
|
</div>
|
||||||
<div class="relative">
|
<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
|
<Input
|
||||||
id="password"
|
id="password"
|
||||||
v-model="password"
|
v-model="password"
|
||||||
@@ -211,11 +211,11 @@ function returnToPassword(keepError = false) {
|
|||||||
</FieldGroup>
|
</FieldGroup>
|
||||||
</form>
|
</form>
|
||||||
<form v-else @submit.prevent="handleTotpLogin">
|
<form v-else @submit.prevent="handleTotpLogin">
|
||||||
<FieldGroup>
|
<FieldGroup class="gap-5">
|
||||||
<Field>
|
<Field>
|
||||||
<FieldLabel for="totp-code">{{ t('auth.totpCode') }}</FieldLabel>
|
<FieldLabel for="totp-code">{{ t('auth.totpCode') }}</FieldLabel>
|
||||||
<div class="relative">
|
<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
|
<Input
|
||||||
id="totp-code"
|
id="totp-code"
|
||||||
v-model="totpCode"
|
v-model="totpCode"
|
||||||
@@ -240,7 +240,7 @@ function returnToPassword(keepError = false) {
|
|||||||
<span v-else>{{ t('auth.verifyAndLogin') }}</span>
|
<span v-else>{{ t('auth.verifyAndLogin') }}</span>
|
||||||
</Button>
|
</Button>
|
||||||
<Button type="button" variant="ghost" class="w-full" :disabled="loading" @click="returnToPassword()">
|
<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') }}
|
{{ t('auth.backToPassword') }}
|
||||||
</Button>
|
</Button>
|
||||||
</FieldGroup>
|
</FieldGroup>
|
||||||
|
|||||||
@@ -2700,19 +2700,19 @@ watch(isWindows, () => {
|
|||||||
<AppLayout>
|
<AppLayout>
|
||||||
<SidebarProvider class="h-full min-h-0 overflow-hidden">
|
<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">
|
<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>
|
<h1 class="text-xl font-semibold">{{ t('settings.title') }}</h1>
|
||||||
<p class="text-xs text-muted-foreground">{{ t('settings.sidebarSubtitle') }}</p>
|
<p class="text-xs text-muted-foreground">{{ t('settings.sidebarSubtitle') }}</p>
|
||||||
</SidebarHeader>
|
</SidebarHeader>
|
||||||
<SidebarContent class="px-3 pb-6 md:pb-10">
|
<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>
|
<SidebarGroupLabel class="uppercase">{{ group.title }}</SidebarGroupLabel>
|
||||||
<SidebarMenu>
|
<SidebarMenu>
|
||||||
<SidebarMenuItem v-for="item in group.items" :key="item.id">
|
<SidebarMenuItem v-for="item in group.items" :key="item.id">
|
||||||
<SidebarMenuButton
|
<SidebarMenuButton
|
||||||
:is-active="activeSection === item.id"
|
:is-active="activeSection === item.id"
|
||||||
:tooltip="item.label"
|
: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)"
|
@click="selectSection(item.id)"
|
||||||
>
|
>
|
||||||
<component :is="item.icon" />
|
<component :is="item.icon" />
|
||||||
@@ -2727,19 +2727,19 @@ watch(isWindows, () => {
|
|||||||
<SidebarInset class="min-w-0 overflow-y-auto">
|
<SidebarInset class="min-w-0 overflow-y-auto">
|
||||||
<!-- Mobile Header -->
|
<!-- 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">
|
<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">
|
<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>
|
<h1 class="text-sm sm:text-base font-semibold truncate">{{ sectionMeta.title }}</h1>
|
||||||
</div>
|
</div>
|
||||||
</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 -->
|
<!-- Section Header -->
|
||||||
<header class="space-y-1.5 pb-2 border-b">
|
<header class="space-y-1.5 pb-2 border-b">
|
||||||
<div class="flex items-center gap-2.5">
|
<div class="flex items-center gap-2.5">
|
||||||
<component :is="sectionMeta.icon" class="h-5 w-5 text-muted-foreground" />
|
<component :is="sectionMeta.icon" class="size-5 text-muted-foreground" />
|
||||||
<h1 class="text-xl sm:text-2xl font-semibold tracking-tight">{{ sectionMeta.title }}</h1>
|
<h1 class="text-xl font-semibold tracking-tight">{{ sectionMeta.title }}</h1>
|
||||||
</div>
|
</div>
|
||||||
<p v-if="sectionMeta.description" class="text-sm text-muted-foreground">
|
<p v-if="sectionMeta.description" class="text-sm text-muted-foreground">
|
||||||
{{ sectionMeta.description }}
|
{{ sectionMeta.description }}
|
||||||
@@ -2747,7 +2747,7 @@ watch(isWindows, () => {
|
|||||||
</header>
|
</header>
|
||||||
|
|
||||||
<!-- Appearance Section -->
|
<!-- Appearance Section -->
|
||||||
<div v-show="activeSection === 'appearance'" class="space-y-6">
|
<div v-show="activeSection === 'appearance'" class="space-y-4">
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>{{ t('settings.theme') }}</CardTitle>
|
<CardTitle>{{ t('settings.theme') }}</CardTitle>
|
||||||
@@ -2756,13 +2756,13 @@ watch(isWindows, () => {
|
|||||||
<CardContent>
|
<CardContent>
|
||||||
<div class="grid grid-cols-3 gap-2 sm:max-w-md">
|
<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')">
|
<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>
|
||||||
<Button :variant="theme === 'dark' ? 'default' : 'outline'" size="sm" class="justify-center" @click="setTheme('dark')">
|
<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>
|
||||||
<Button :variant="theme === 'system' ? 'default' : 'outline'" size="sm" class="justify-center" @click="setTheme('system')">
|
<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>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
@@ -2786,21 +2786,21 @@ watch(isWindows, () => {
|
|||||||
<CardContent class="space-y-1">
|
<CardContent class="space-y-1">
|
||||||
<div class="flex items-center justify-between gap-4 px-3 py-3">
|
<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">
|
<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>
|
<span class="truncate">{{ t('actionbar.webTerminal') }}</span>
|
||||||
</Label>
|
</Label>
|
||||||
<Switch id="feature-web-terminal" v-model="featureVisibility.webTerminal" />
|
<Switch id="feature-web-terminal" v-model="featureVisibility.webTerminal" />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center justify-between gap-4 px-3 py-3">
|
<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">
|
<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>
|
<span class="truncate">{{ t('settings.computerUseAgent') }}</span>
|
||||||
</Label>
|
</Label>
|
||||||
<Switch id="feature-computer-use" v-model="featureVisibility.computerUse" />
|
<Switch id="feature-computer-use" v-model="featureVisibility.computerUse" />
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center justify-between gap-4 px-3 py-3">
|
<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">
|
<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>
|
<span class="truncate">{{ t('settings.pasteText') }}</span>
|
||||||
</Label>
|
</Label>
|
||||||
<Switch id="feature-paste-text" v-model="featureVisibility.pasteText" />
|
<Switch id="feature-paste-text" v-model="featureVisibility.pasteText" />
|
||||||
@@ -2810,7 +2810,7 @@ watch(isWindows, () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Account Section -->
|
<!-- Account Section -->
|
||||||
<div v-show="activeSection === 'account'" class="space-y-6">
|
<div v-show="activeSection === 'account'" class="space-y-4">
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>{{ t('settings.username') }}</CardTitle>
|
<CardTitle>{{ t('settings.username') }}</CardTitle>
|
||||||
@@ -2826,12 +2826,12 @@ watch(isWindows, () => {
|
|||||||
<Input id="account-username-password" v-model="usernamePassword" type="password" autocomplete="current-password" />
|
<Input id="account-username-password" v-model="usernamePassword" type="password" autocomplete="current-password" />
|
||||||
</div>
|
</div>
|
||||||
<p v-if="usernameError" class="text-xs text-destructive">{{ usernameError }}</p>
|
<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>
|
</CardContent>
|
||||||
<CardFooter class="border-t pt-4 justify-end">
|
<CardFooter class="border-t pt-4 justify-end">
|
||||||
<Button @click="changeUsername" :disabled="usernameSaving">
|
<Button @click="changeUsername" :disabled="usernameSaving">
|
||||||
<Loader2 v-if="usernameSaving" class="h-4 w-4 mr-2 animate-spin" />
|
<Loader2 v-if="usernameSaving" class="size-4 mr-2 animate-spin" />
|
||||||
<Save v-else class="h-4 w-4 mr-2" />
|
<Save v-else class="size-4 mr-2" />
|
||||||
{{ t('common.save') }}
|
{{ t('common.save') }}
|
||||||
</Button>
|
</Button>
|
||||||
</CardFooter>
|
</CardFooter>
|
||||||
@@ -2856,12 +2856,12 @@ watch(isWindows, () => {
|
|||||||
<Input id="account-confirm-password" v-model="confirmPassword" type="password" autocomplete="new-password" />
|
<Input id="account-confirm-password" v-model="confirmPassword" type="password" autocomplete="new-password" />
|
||||||
</div>
|
</div>
|
||||||
<p v-if="passwordError" class="text-xs text-destructive">{{ passwordError }}</p>
|
<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>
|
</CardContent>
|
||||||
<CardFooter class="border-t pt-4 justify-end">
|
<CardFooter class="border-t pt-4 justify-end">
|
||||||
<Button @click="changePassword" :disabled="passwordSaving">
|
<Button @click="changePassword" :disabled="passwordSaving">
|
||||||
<Loader2 v-if="passwordSaving" class="h-4 w-4 mr-2 animate-spin" />
|
<Loader2 v-if="passwordSaving" class="size-4 mr-2 animate-spin" />
|
||||||
<Save v-else class="h-4 w-4 mr-2" />
|
<Save v-else class="size-4 mr-2" />
|
||||||
{{ t('common.save') }}
|
{{ t('common.save') }}
|
||||||
</Button>
|
</Button>
|
||||||
</CardFooter>
|
</CardFooter>
|
||||||
@@ -2885,8 +2885,8 @@ watch(isWindows, () => {
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
<CardFooter class="border-t pt-4 justify-end">
|
<CardFooter class="border-t pt-4 justify-end">
|
||||||
<Button @click="saveAuthConfig" :disabled="authConfigLoading">
|
<Button @click="saveAuthConfig" :disabled="authConfigLoading">
|
||||||
<Loader2 v-if="authConfigLoading" class="h-4 w-4 mr-2 animate-spin" />
|
<Loader2 v-if="authConfigLoading" class="size-4 mr-2 animate-spin" />
|
||||||
<Save v-else class="h-4 w-4 mr-2" />
|
<Save v-else class="size-4 mr-2" />
|
||||||
{{ t('common.save') }}
|
{{ t('common.save') }}
|
||||||
</Button>
|
</Button>
|
||||||
</CardFooter>
|
</CardFooter>
|
||||||
@@ -2894,7 +2894,7 @@ watch(isWindows, () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Video Section -->
|
<!-- Video Section -->
|
||||||
<div v-show="activeSection === 'video'" class="space-y-6">
|
<div v-show="activeSection === 'video'" class="space-y-4">
|
||||||
<!-- Video Device Settings -->
|
<!-- Video Device Settings -->
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader class="flex flex-row items-start justify-between space-y-0">
|
<CardHeader class="flex flex-row items-start justify-between space-y-0">
|
||||||
@@ -2902,8 +2902,8 @@ watch(isWindows, () => {
|
|||||||
<CardTitle>{{ t('settings.videoSettings') }}</CardTitle>
|
<CardTitle>{{ t('settings.videoSettings') }}</CardTitle>
|
||||||
<CardDescription>{{ t('settings.videoSettingsDesc') }}</CardDescription>
|
<CardDescription>{{ t('settings.videoSettingsDesc') }}</CardDescription>
|
||||||
</div>
|
</div>
|
||||||
<Button variant="ghost" size="icon" class="h-8 w-8" :aria-label="t('common.refresh')" @click="loadDevices">
|
<Button variant="ghost" size="icon-sm" :aria-label="t('common.refresh')" @click="loadDevices">
|
||||||
<RefreshCw class="h-4 w-4" />
|
<RefreshCw class="size-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent class="space-y-4">
|
<CardContent class="space-y-4">
|
||||||
@@ -3023,8 +3023,8 @@ watch(isWindows, () => {
|
|||||||
:aria-label="showPasswords ? t('extensions.rustdesk.hidePassword') : t('extensions.rustdesk.showPassword')"
|
:aria-label="showPasswords ? t('extensions.rustdesk.hidePassword') : t('extensions.rustdesk.showPassword')"
|
||||||
@click="showPasswords = !showPasswords"
|
@click="showPasswords = !showPasswords"
|
||||||
>
|
>
|
||||||
<Eye v-if="!showPasswords" class="h-4 w-4" />
|
<Eye v-if="!showPasswords" class="size-4" />
|
||||||
<EyeOff v-else class="h-4 w-4" />
|
<EyeOff v-else class="size-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -3034,15 +3034,15 @@ watch(isWindows, () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- HID Section -->
|
<!-- HID Section -->
|
||||||
<div v-show="activeSection === 'hid'" class="space-y-6">
|
<div v-show="activeSection === 'hid'" class="space-y-4">
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader class="flex flex-row items-start justify-between space-y-0">
|
<CardHeader class="flex flex-row items-start justify-between space-y-0">
|
||||||
<div class="space-y-1.5">
|
<div class="space-y-1.5">
|
||||||
<CardTitle>{{ t('settings.hidSettings') }}</CardTitle>
|
<CardTitle>{{ t('settings.hidSettings') }}</CardTitle>
|
||||||
<CardDescription>{{ t('settings.hidSettingsDesc') }}</CardDescription>
|
<CardDescription>{{ t('settings.hidSettingsDesc') }}</CardDescription>
|
||||||
</div>
|
</div>
|
||||||
<Button variant="ghost" size="icon" class="h-8 w-8" :aria-label="t('common.refresh')" @click="loadDevices">
|
<Button variant="ghost" size="icon-sm" :aria-label="t('common.refresh')" @click="loadDevices">
|
||||||
<RefreshCw class="h-4 w-4" />
|
<RefreshCw class="size-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent class="space-y-4">
|
<CardContent class="space-y-4">
|
||||||
@@ -3090,13 +3090,13 @@ watch(isWindows, () => {
|
|||||||
<h4 class="text-sm font-medium">{{ t('settings.ch9329Descriptor') }}</h4>
|
<h4 class="text-sm font-medium">{{ t('settings.ch9329Descriptor') }}</h4>
|
||||||
<p class="text-sm text-muted-foreground">{{ t('settings.ch9329DescriptorDesc') }}</p>
|
<p class="text-sm text-muted-foreground">{{ t('settings.ch9329DescriptorDesc') }}</p>
|
||||||
</div>
|
</div>
|
||||||
<Button variant="ghost" size="icon" class="h-8 w-8 shrink-0" :aria-label="t('common.refresh')" :disabled="ch9329DescriptorLoading" @click="loadCh9329Descriptor">
|
<Button variant="ghost" size="icon-sm" class="shrink-0" :aria-label="t('common.refresh')" :disabled="ch9329DescriptorLoading" @click="loadCh9329Descriptor">
|
||||||
<RefreshCw class="h-4 w-4" :class="{ 'animate-spin': ch9329DescriptorLoading }" />
|
<RefreshCw class="size-4" :class="{ 'animate-spin': ch9329DescriptorLoading }" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<p v-if="ch9329DescriptorLoading" class="text-sm text-muted-foreground flex items-center gap-2">
|
<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') }}
|
{{ t('settings.ch9329DescriptorLoading') }}
|
||||||
</p>
|
</p>
|
||||||
<p v-else-if="ch9329DescriptorError" class="text-sm text-destructive">
|
<p v-else-if="ch9329DescriptorError" class="text-sm text-destructive">
|
||||||
@@ -3355,7 +3355,7 @@ watch(isWindows, () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Environment Section -->
|
<!-- 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">
|
<Card v-if="systemStore.deviceInfo">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>{{ t('settings.deviceInfo') }}</CardTitle>
|
<CardTitle>{{ t('settings.deviceInfo') }}</CardTitle>
|
||||||
@@ -3408,7 +3408,7 @@ watch(isWindows, () => {
|
|||||||
]"
|
]"
|
||||||
@click="onRunOtgSelfCheckClick"
|
@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') }}
|
{{ t('settings.otgSelfCheck.run') }}
|
||||||
</Button>
|
</Button>
|
||||||
</CardHeader>
|
</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">
|
<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">
|
<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>
|
<span class="text-sm font-medium truncate leading-6">{{ t(group.titleKey) }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex items-center gap-2 shrink-0">
|
<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"
|
class="px-4 py-3 border-b last:border-b-0"
|
||||||
>
|
>
|
||||||
<div class="flex items-start gap-2">
|
<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="min-w-0 space-y-1">
|
||||||
<div class="flex items-center gap-2">
|
<div class="flex items-center gap-2">
|
||||||
<p class="text-sm leading-5">{{ otgCheckMessage(item) }}</p>
|
<p class="text-sm leading-5">{{ otgCheckMessage(item) }}</p>
|
||||||
@@ -3503,7 +3503,7 @@ watch(isWindows, () => {
|
|||||||
]"
|
]"
|
||||||
@click="onRunVideoEncoderSelfCheckClick"
|
@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') }}
|
{{ t('settings.encoderSelfCheck.run') }}
|
||||||
</Button>
|
</Button>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
@@ -3569,7 +3569,7 @@ watch(isWindows, () => {
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-show="activeSection === 'other'" class="space-y-6">
|
<div v-show="activeSection === 'other'" class="space-y-4">
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<div class="flex items-start justify-between gap-4">
|
<div class="flex items-start justify-between gap-4">
|
||||||
@@ -3587,8 +3587,8 @@ watch(isWindows, () => {
|
|||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent class="space-y-4">
|
<CardContent class="space-y-4">
|
||||||
<div class="flex items-center gap-2 text-sm">
|
<div class="flex items-center gap-2 text-sm">
|
||||||
<Loader2 v-if="watchdogLoading" class="h-4 w-4 animate-spin text-muted-foreground" />
|
<Loader2 v-if="watchdogLoading" class="size-4 animate-spin text-muted-foreground" />
|
||||||
<span v-else class="h-2.5 w-2.5 rounded-full" :class="watchdogStatusClass" />
|
<span v-else class="size-2.5 rounded-full" :class="watchdogStatusClass" />
|
||||||
<span class="font-medium">
|
<span class="font-medium">
|
||||||
{{ watchdogLoading
|
{{ watchdogLoading
|
||||||
? t('common.loading')
|
? t('common.loading')
|
||||||
@@ -3617,7 +3617,7 @@ watch(isWindows, () => {
|
|||||||
:disabled="usbDevicesLoading"
|
:disabled="usbDevicesLoading"
|
||||||
@click="fetchUsbDevices()"
|
@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') }}
|
{{ t('settings.usbDevices.refresh') }}
|
||||||
</Button>
|
</Button>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
@@ -3657,7 +3657,7 @@ watch(isWindows, () => {
|
|||||||
v-if="dev.authorized != null"
|
v-if="dev.authorized != null"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="sm"
|
size="sm"
|
||||||
class="h-7 text-xs"
|
class="text-xs"
|
||||||
:disabled="usbResetLoading"
|
:disabled="usbResetLoading"
|
||||||
@click="usbResetTarget = dev"
|
@click="usbResetTarget = dev"
|
||||||
>
|
>
|
||||||
@@ -3701,14 +3701,14 @@ watch(isWindows, () => {
|
|||||||
</AlertDialogContent>
|
</AlertDialogContent>
|
||||||
</AlertDialog>
|
</AlertDialog>
|
||||||
</div>
|
</div>
|
||||||
<div v-show="activeSection === 'network'" class="space-y-6">
|
<div v-show="activeSection === 'network'" class="space-y-4">
|
||||||
|
|
||||||
<!-- Auto-restart: restarting progress -->
|
<!-- Auto-restart: restarting progress -->
|
||||||
<div
|
<div
|
||||||
v-if="autoRestarting"
|
v-if="autoRestarting"
|
||||||
class="flex items-center gap-3 rounded-lg border bg-card px-4 py-3 text-sm shadow-sm"
|
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">
|
<div class="flex-1 min-w-0">
|
||||||
<p class="font-medium">{{ t('settings.autoRestarting') }}</p>
|
<p class="font-medium">{{ t('settings.autoRestarting') }}</p>
|
||||||
<p class="text-xs text-muted-foreground">
|
<p class="text-xs text-muted-foreground">
|
||||||
@@ -3739,7 +3739,7 @@ watch(isWindows, () => {
|
|||||||
<AlertDescription class="flex items-center justify-between gap-3">
|
<AlertDescription class="flex items-center justify-between gap-3">
|
||||||
<span>{{ t('settings.autoRestartFailed') }}</span>
|
<span>{{ t('settings.autoRestartFailed') }}</span>
|
||||||
<Button variant="outline" size="sm" class="text-foreground" @click="triggerAutoRestart">
|
<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') }}
|
{{ t('common.retry') }}
|
||||||
</Button>
|
</Button>
|
||||||
</AlertDescription>
|
</AlertDescription>
|
||||||
@@ -3807,33 +3807,33 @@ watch(isWindows, () => {
|
|||||||
<div class="flex items-center gap-2">
|
<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>
|
<code class="font-mono text-xs sm:text-sm break-all flex-1 min-w-0">{{ previewAccessUrl }}</code>
|
||||||
<Button
|
<Button
|
||||||
variant="ghost" size="icon" class="h-7 w-7 shrink-0"
|
variant="ghost" size="icon-sm" class="shrink-0"
|
||||||
:title="t('settings.copyUrl')"
|
:title="t('settings.copyUrl')"
|
||||||
:aria-label="t('settings.copyUrl')"
|
:aria-label="t('settings.copyUrl')"
|
||||||
@click="copyPreviewUrl"
|
@click="copyPreviewUrl"
|
||||||
>
|
>
|
||||||
<Check v-if="previewUrlCopied" class="h-3.5 w-3.5 text-success" />
|
<Check v-if="previewUrlCopied" class="size-3.5 text-success" />
|
||||||
<Copy v-else class="h-3.5 w-3.5" />
|
<Copy v-else class="size-3.5" />
|
||||||
</Button>
|
</Button>
|
||||||
<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')"
|
:title="t('settings.openInBrowser')"
|
||||||
:aria-label="t('settings.openInBrowser')"
|
:aria-label="t('settings.openInBrowser')"
|
||||||
@click="openPreviewUrl"
|
@click="openPreviewUrl"
|
||||||
>
|
>
|
||||||
<ExternalLink class="h-3.5 w-3.5" />
|
<ExternalLink class="size-3.5" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
<CardFooter class="flex items-center justify-between gap-3 border-t pt-4">
|
<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">
|
<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>
|
<span class="truncate">{{ t('settings.restartRequiredHint') }}</span>
|
||||||
</p>
|
</p>
|
||||||
<Button @click="saveWebServerConfig" :disabled="webServerLoading || autoRestarting">
|
<Button @click="saveWebServerConfig" :disabled="webServerLoading || autoRestarting">
|
||||||
<RefreshCw v-if="autoRestarting" class="h-4 w-4 mr-2 animate-spin" />
|
<RefreshCw v-if="autoRestarting" class="size-4 mr-2 animate-spin" />
|
||||||
<Save v-else class="h-4 w-4 mr-2" />
|
<Save v-else class="size-4 mr-2" />
|
||||||
{{ autoRestarting ? t('settings.restarting') : t('common.save') }}
|
{{ autoRestarting ? t('settings.restarting') : t('common.save') }}
|
||||||
</Button>
|
</Button>
|
||||||
</CardFooter>
|
</CardFooter>
|
||||||
@@ -3891,11 +3891,11 @@ watch(isWindows, () => {
|
|||||||
<div v-for="(_, i) in bindAddressList" :key="`bind-${i}`" class="flex gap-2">
|
<div v-for="(_, i) in bindAddressList" :key="`bind-${i}`" class="flex gap-2">
|
||||||
<Input v-model="bindAddressList[i]" placeholder="192.168.1.10" />
|
<Input v-model="bindAddressList[i]" placeholder="192.168.1.10" />
|
||||||
<Button variant="ghost" size="icon" :aria-label="t('common.delete')" @click="removeBindAddress(i)">
|
<Button variant="ghost" size="icon" :aria-label="t('common.delete')" @click="removeBindAddress(i)">
|
||||||
<Trash2 class="h-4 w-4" />
|
<Trash2 class="size-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<Button variant="outline" size="sm" @click="addBindAddress">
|
<Button variant="outline" size="sm" @click="addBindAddress">
|
||||||
<Plus class="h-4 w-4 mr-1" />
|
<Plus class="size-4 mr-1" />
|
||||||
{{ t('settings.addBindAddress') }}
|
{{ t('settings.addBindAddress') }}
|
||||||
</Button>
|
</Button>
|
||||||
<p v-if="bindAddressError" class="text-xs text-destructive">{{ bindAddressError }}</p>
|
<p v-if="bindAddressError" class="text-xs text-destructive">{{ bindAddressError }}</p>
|
||||||
@@ -3911,12 +3911,12 @@ watch(isWindows, () => {
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
<CardFooter class="flex items-center justify-between gap-3 border-t pt-4">
|
<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">
|
<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>
|
<span class="truncate">{{ t('settings.restartRequiredHint') }}</span>
|
||||||
</p>
|
</p>
|
||||||
<Button @click="saveWebServerConfig" :disabled="webServerLoading || !!bindAddressError || autoRestarting">
|
<Button @click="saveWebServerConfig" :disabled="webServerLoading || !!bindAddressError || autoRestarting">
|
||||||
<RefreshCw v-if="autoRestarting" class="h-4 w-4 mr-2 animate-spin" />
|
<RefreshCw v-if="autoRestarting" class="size-4 mr-2 animate-spin" />
|
||||||
<Save v-else class="h-4 w-4 mr-2" />
|
<Save v-else class="size-4 mr-2" />
|
||||||
{{ autoRestarting ? t('settings.restarting') : t('common.save') }}
|
{{ autoRestarting ? t('settings.restarting') : t('common.save') }}
|
||||||
</Button>
|
</Button>
|
||||||
</CardFooter>
|
</CardFooter>
|
||||||
@@ -3942,12 +3942,12 @@ watch(isWindows, () => {
|
|||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="sm"
|
size="sm"
|
||||||
class="text-destructive hover:text-destructive h-7 text-xs"
|
class="text-destructive hover:text-destructive text-xs"
|
||||||
:disabled="certClearing || autoRestarting"
|
:disabled="certClearing || autoRestarting"
|
||||||
@click="clearCertificate"
|
@click="clearCertificate"
|
||||||
>
|
>
|
||||||
<RefreshCw v-if="certClearing || autoRestarting" class="h-3 w-3 mr-1 animate-spin" />
|
<RefreshCw v-if="certClearing || autoRestarting" class="size-3 mr-1 animate-spin" />
|
||||||
<Trash2 v-else class="h-3 w-3 mr-1" />
|
<Trash2 v-else class="size-3 mr-1" />
|
||||||
{{ autoRestarting ? t('settings.restarting') : t('settings.sslCertClear') }}
|
{{ autoRestarting ? t('settings.restarting') : t('settings.sslCertClear') }}
|
||||||
</Button>
|
</Button>
|
||||||
</AlertDescription>
|
</AlertDescription>
|
||||||
@@ -3980,15 +3980,15 @@ watch(isWindows, () => {
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
<CardFooter class="flex items-center justify-between gap-3 border-t pt-4">
|
<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">
|
<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>
|
<span class="truncate">{{ t('settings.restartRequiredHint') }}</span>
|
||||||
</p>
|
</p>
|
||||||
<Button
|
<Button
|
||||||
:disabled="certSaving || autoRestarting || !sslCertPem.trim() || !sslKeyPem.trim()"
|
:disabled="certSaving || autoRestarting || !sslCertPem.trim() || !sslKeyPem.trim()"
|
||||||
@click="saveCertificate"
|
@click="saveCertificate"
|
||||||
>
|
>
|
||||||
<RefreshCw v-if="certSaving || autoRestarting" class="h-4 w-4 mr-2 animate-spin" />
|
<RefreshCw v-if="certSaving || autoRestarting" class="size-4 mr-2 animate-spin" />
|
||||||
<Save v-else class="h-4 w-4 mr-2" />
|
<Save v-else class="size-4 mr-2" />
|
||||||
{{ autoRestarting ? t('settings.restarting') : t('settings.sslCertSave') }}
|
{{ autoRestarting ? t('settings.restarting') : t('settings.sslCertSave') }}
|
||||||
</Button>
|
</Button>
|
||||||
</CardFooter>
|
</CardFooter>
|
||||||
@@ -3996,7 +3996,7 @@ watch(isWindows, () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ATX Section -->
|
<!-- ATX Section -->
|
||||||
<div v-show="activeSection === 'atx'" class="space-y-6">
|
<div v-show="activeSection === 'atx'" class="space-y-4">
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>{{ t('settings.atxPowerManagement') }}</CardTitle>
|
<CardTitle>{{ t('settings.atxPowerManagement') }}</CardTitle>
|
||||||
@@ -4012,8 +4012,8 @@ watch(isWindows, () => {
|
|||||||
</SelectContent>
|
</SelectContent>
|
||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
<Button variant="ghost" size="icon" class="h-9 w-9 shrink-0" :aria-label="t('common.refresh')" @click="loadAtxDevices">
|
<Button variant="ghost" size="icon-sm" class="shrink-0" :aria-label="t('common.refresh')" @click="loadAtxDevices">
|
||||||
<RefreshCw class="h-4 w-4" />
|
<RefreshCw class="size-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -4167,7 +4167,7 @@ watch(isWindows, () => {
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
<CardFooter class="border-t pt-4 justify-end">
|
<CardFooter class="border-t pt-4 justify-end">
|
||||||
<Button :disabled="atxSaving" @click="saveAtxSettings">
|
<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>
|
</Button>
|
||||||
</CardFooter>
|
</CardFooter>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -4190,14 +4190,14 @@ watch(isWindows, () => {
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
<CardFooter class="border-t pt-4 justify-end">
|
<CardFooter class="border-t pt-4 justify-end">
|
||||||
<Button :disabled="wolSaving" @click="saveWolSettings">
|
<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>
|
</Button>
|
||||||
</CardFooter>
|
</CardFooter>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ttyd Section -->
|
<!-- ttyd Section -->
|
||||||
<div v-show="activeSection === 'ext-ttyd'" class="space-y-6">
|
<div v-show="activeSection === 'ext-ttyd'" class="space-y-4">
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
@@ -4228,7 +4228,7 @@ watch(isWindows, () => {
|
|||||||
variant="default"
|
variant="default"
|
||||||
@click="openTerminal"
|
@click="openTerminal"
|
||||||
>
|
>
|
||||||
<Terminal class="h-4 w-4 mr-1" />
|
<Terminal class="size-4 mr-1" />
|
||||||
{{ t('extensions.ttyd.open') }}
|
{{ t('extensions.ttyd.open') }}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
@@ -4237,7 +4237,7 @@ watch(isWindows, () => {
|
|||||||
@click="startExtension('ttyd')"
|
@click="startExtension('ttyd')"
|
||||||
:disabled="extensionsLoading"
|
:disabled="extensionsLoading"
|
||||||
>
|
>
|
||||||
<Play class="h-4 w-4 mr-1" />
|
<Play class="size-4 mr-1" />
|
||||||
{{ t('extensions.start') }}
|
{{ t('extensions.start') }}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
@@ -4247,7 +4247,7 @@ watch(isWindows, () => {
|
|||||||
@click="stopExtension('ttyd')"
|
@click="stopExtension('ttyd')"
|
||||||
:disabled="extensionsLoading"
|
:disabled="extensionsLoading"
|
||||||
>
|
>
|
||||||
<Square class="h-4 w-4 mr-1" />
|
<Square class="size-4 mr-1" />
|
||||||
{{ t('extensions.stop') }}
|
{{ t('extensions.stop') }}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -4267,11 +4267,11 @@ watch(isWindows, () => {
|
|||||||
<!-- Logs -->
|
<!-- Logs -->
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<Collapsible v-model:open="showLogs.ttyd" @update:open="open => open && refreshExtensionLogs('ttyd')">
|
<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">
|
<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>
|
<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')">
|
<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') }}
|
{{ t('common.refresh') }}
|
||||||
</Button>
|
</Button>
|
||||||
</CollapsibleContent>
|
</CollapsibleContent>
|
||||||
@@ -4281,14 +4281,14 @@ watch(isWindows, () => {
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
<CardFooter v-if="extensions?.ttyd?.available" class="border-t pt-4 justify-end">
|
<CardFooter v-if="extensions?.ttyd?.available" class="border-t pt-4 justify-end">
|
||||||
<Button :disabled="loading || isExtRunning(extensions?.ttyd?.status)" @click="saveExtensionConfig('ttyd')">
|
<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>
|
</Button>
|
||||||
</CardFooter>
|
</CardFooter>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Remote Access Section -->
|
<!-- 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>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
@@ -4319,7 +4319,7 @@ watch(isWindows, () => {
|
|||||||
@click="startExtension('gostc')"
|
@click="startExtension('gostc')"
|
||||||
:disabled="extensionsLoading || !!gostcValidationMessage"
|
:disabled="extensionsLoading || !!gostcValidationMessage"
|
||||||
>
|
>
|
||||||
<Play class="h-4 w-4 mr-1" />
|
<Play class="size-4 mr-1" />
|
||||||
{{ t('extensions.start') }}
|
{{ t('extensions.start') }}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
@@ -4329,7 +4329,7 @@ watch(isWindows, () => {
|
|||||||
@click="stopExtension('gostc')"
|
@click="stopExtension('gostc')"
|
||||||
:disabled="extensionsLoading"
|
:disabled="extensionsLoading"
|
||||||
>
|
>
|
||||||
<Square class="h-4 w-4 mr-1" />
|
<Square class="size-4 mr-1" />
|
||||||
{{ t('extensions.stop') }}
|
{{ t('extensions.stop') }}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -4365,11 +4365,11 @@ watch(isWindows, () => {
|
|||||||
<!-- Logs -->
|
<!-- Logs -->
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<Collapsible v-model:open="showLogs.gostc" @update:open="open => open && refreshExtensionLogs('gostc')">
|
<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">
|
<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>
|
<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')">
|
<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') }}
|
{{ t('common.refresh') }}
|
||||||
</Button>
|
</Button>
|
||||||
</CollapsibleContent>
|
</CollapsibleContent>
|
||||||
@@ -4379,7 +4379,7 @@ watch(isWindows, () => {
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
<CardFooter v-if="extensions?.gostc?.available" class="border-t pt-4 justify-end">
|
<CardFooter v-if="extensions?.gostc?.available" class="border-t pt-4 justify-end">
|
||||||
<Button :disabled="loading || isExtRunning(extensions?.gostc?.status)" @click="saveExtensionConfig('gostc')">
|
<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>
|
</Button>
|
||||||
</CardFooter>
|
</CardFooter>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -4414,7 +4414,7 @@ watch(isWindows, () => {
|
|||||||
@click="startExtension('easytier')"
|
@click="startExtension('easytier')"
|
||||||
:disabled="extensionsLoading || !!easytierValidationMessage"
|
:disabled="extensionsLoading || !!easytierValidationMessage"
|
||||||
>
|
>
|
||||||
<Play class="h-4 w-4 mr-1" />
|
<Play class="size-4 mr-1" />
|
||||||
{{ t('extensions.start') }}
|
{{ t('extensions.start') }}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
@@ -4424,7 +4424,7 @@ watch(isWindows, () => {
|
|||||||
@click="stopExtension('easytier')"
|
@click="stopExtension('easytier')"
|
||||||
:disabled="extensionsLoading"
|
:disabled="extensionsLoading"
|
||||||
>
|
>
|
||||||
<Square class="h-4 w-4 mr-1" />
|
<Square class="size-4 mr-1" />
|
||||||
{{ t('extensions.stop') }}
|
{{ t('extensions.stop') }}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -4453,11 +4453,11 @@ watch(isWindows, () => {
|
|||||||
<div v-for="(_, i) in extConfig.easytier.peer_urls" :key="i" class="flex gap-2">
|
<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)" />
|
<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)">
|
<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>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<Button variant="outline" size="sm" @click="addEasytierPeer" :disabled="isExtRunning(extensions?.easytier?.status)">
|
<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') }}
|
{{ t('extensions.easytier.addPeer') }}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -4472,11 +4472,11 @@ watch(isWindows, () => {
|
|||||||
<!-- Logs -->
|
<!-- Logs -->
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<Collapsible v-model:open="showLogs.easytier" @update:open="open => open && refreshExtensionLogs('easytier')">
|
<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">
|
<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>
|
<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')">
|
<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') }}
|
{{ t('common.refresh') }}
|
||||||
</Button>
|
</Button>
|
||||||
</CollapsibleContent>
|
</CollapsibleContent>
|
||||||
@@ -4486,7 +4486,7 @@ watch(isWindows, () => {
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
<CardFooter v-if="extensions?.easytier?.available" class="border-t pt-4 justify-end">
|
<CardFooter v-if="extensions?.easytier?.available" class="border-t pt-4 justify-end">
|
||||||
<Button :disabled="loading || isExtRunning(extensions?.easytier?.status)" @click="saveExtensionConfig('easytier')">
|
<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>
|
</Button>
|
||||||
</CardFooter>
|
</CardFooter>
|
||||||
</Card>
|
</Card>
|
||||||
@@ -4520,7 +4520,7 @@ watch(isWindows, () => {
|
|||||||
@click="startExtension('frpc')"
|
@click="startExtension('frpc')"
|
||||||
:disabled="extensionsLoading || !!frpcValidationMessage"
|
:disabled="extensionsLoading || !!frpcValidationMessage"
|
||||||
>
|
>
|
||||||
<Play class="h-4 w-4 mr-1" />
|
<Play class="size-4 mr-1" />
|
||||||
{{ t('extensions.start') }}
|
{{ t('extensions.start') }}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
@@ -4530,7 +4530,7 @@ watch(isWindows, () => {
|
|||||||
@click="stopExtension('frpc')"
|
@click="stopExtension('frpc')"
|
||||||
:disabled="extensionsLoading"
|
:disabled="extensionsLoading"
|
||||||
>
|
>
|
||||||
<Square class="h-4 w-4 mr-1" />
|
<Square class="size-4 mr-1" />
|
||||||
{{ t('extensions.stop') }}
|
{{ t('extensions.stop') }}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -4642,11 +4642,11 @@ watch(isWindows, () => {
|
|||||||
</div>
|
</div>
|
||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<Collapsible v-model:open="showLogs.frpc" @update:open="open => open && refreshExtensionLogs('frpc')">
|
<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">
|
<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>
|
<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')">
|
<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') }}
|
{{ t('common.refresh') }}
|
||||||
</Button>
|
</Button>
|
||||||
</CollapsibleContent>
|
</CollapsibleContent>
|
||||||
@@ -4656,14 +4656,14 @@ watch(isWindows, () => {
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
<CardFooter v-if="extensions?.frpc?.available" class="border-t pt-4 justify-end">
|
<CardFooter v-if="extensions?.frpc?.available" class="border-t pt-4 justify-end">
|
||||||
<Button :disabled="loading || isExtRunning(extensions?.frpc?.status)" @click="saveExtensionConfig('frpc')">
|
<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>
|
</Button>
|
||||||
</CardFooter>
|
</CardFooter>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- RTSP Section -->
|
<!-- 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>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
@@ -4675,8 +4675,8 @@ watch(isWindows, () => {
|
|||||||
<Badge :variant="rtspStatus?.service_status === 'running' ? 'default' : 'secondary'">
|
<Badge :variant="rtspStatus?.service_status === 'running' ? 'default' : 'secondary'">
|
||||||
{{ getRtspServiceStatusText(rtspStatus?.service_status) }}
|
{{ getRtspServiceStatusText(rtspStatus?.service_status) }}
|
||||||
</Badge>
|
</Badge>
|
||||||
<Button variant="ghost" size="icon" class="h-8 w-8" :aria-label="t('common.refresh')" @click="loadRtspConfig" :disabled="rtspLoading">
|
<Button variant="ghost" size="icon-sm" :aria-label="t('common.refresh')" @click="loadRtspConfig" :disabled="rtspLoading">
|
||||||
<RefreshCw :class="['h-4 w-4', rtspLoading ? 'animate-spin' : '']" />
|
<RefreshCw :class="['size-4', rtspLoading ? 'animate-spin' : '']" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -4694,7 +4694,7 @@ watch(isWindows, () => {
|
|||||||
@click="startRtsp"
|
@click="startRtsp"
|
||||||
:disabled="rtspLoading || rtspStatus?.service_status === 'starting'"
|
:disabled="rtspLoading || rtspStatus?.service_status === 'starting'"
|
||||||
>
|
>
|
||||||
<Play class="h-4 w-4 mr-1" />
|
<Play class="size-4 mr-1" />
|
||||||
{{ t('extensions.start') }}
|
{{ t('extensions.start') }}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
@@ -4704,7 +4704,7 @@ watch(isWindows, () => {
|
|||||||
@click="stopRtsp"
|
@click="stopRtsp"
|
||||||
:disabled="rtspLoading"
|
:disabled="rtspLoading"
|
||||||
>
|
>
|
||||||
<Square class="h-4 w-4 mr-1" />
|
<Square class="size-4 mr-1" />
|
||||||
{{ t('extensions.stop') }}
|
{{ t('extensions.stop') }}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -4767,8 +4767,8 @@ watch(isWindows, () => {
|
|||||||
:aria-label="showPasswords ? t('extensions.rustdesk.hidePassword') : t('extensions.rustdesk.showPassword')"
|
:aria-label="showPasswords ? t('extensions.rustdesk.hidePassword') : t('extensions.rustdesk.showPassword')"
|
||||||
@click="showPasswords = !showPasswords"
|
@click="showPasswords = !showPasswords"
|
||||||
>
|
>
|
||||||
<Eye v-if="!showPasswords" class="h-4 w-4" />
|
<Eye v-if="!showPasswords" class="size-4" />
|
||||||
<EyeOff v-else class="h-4 w-4" />
|
<EyeOff v-else class="size-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -4782,14 +4782,14 @@ watch(isWindows, () => {
|
|||||||
<code class="truncate font-mono">{{ rtspStreamUrl }}</code>
|
<code class="truncate font-mono">{{ rtspStreamUrl }}</code>
|
||||||
</div>
|
</div>
|
||||||
<Button class="shrink-0" :disabled="loading || rtspLoading" @click="saveRtspConfig">
|
<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>
|
</Button>
|
||||||
</CardFooter>
|
</CardFooter>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- VNC Section -->
|
<!-- 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>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
@@ -4801,8 +4801,8 @@ watch(isWindows, () => {
|
|||||||
<Badge :variant="vncStatus?.service_status === 'running' ? 'default' : 'secondary'">
|
<Badge :variant="vncStatus?.service_status === 'running' ? 'default' : 'secondary'">
|
||||||
{{ getVncServiceStatusText(vncStatus?.service_status) }}
|
{{ getVncServiceStatusText(vncStatus?.service_status) }}
|
||||||
</Badge>
|
</Badge>
|
||||||
<Button variant="ghost" size="icon" class="h-8 w-8" :aria-label="t('common.refresh')" @click="loadVncConfig" :disabled="vncLoading">
|
<Button variant="ghost" size="icon-sm" :aria-label="t('common.refresh')" @click="loadVncConfig" :disabled="vncLoading">
|
||||||
<RefreshCw :class="['h-4 w-4', vncLoading ? 'animate-spin' : '']" />
|
<RefreshCw :class="['size-4', vncLoading ? 'animate-spin' : '']" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -4824,7 +4824,7 @@ watch(isWindows, () => {
|
|||||||
@click="startVnc"
|
@click="startVnc"
|
||||||
:disabled="vncLoading || vncStatus?.service_status === 'starting'"
|
:disabled="vncLoading || vncStatus?.service_status === 'starting'"
|
||||||
>
|
>
|
||||||
<Play class="h-4 w-4 mr-1" />
|
<Play class="size-4 mr-1" />
|
||||||
{{ t('extensions.start') }}
|
{{ t('extensions.start') }}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
@@ -4834,7 +4834,7 @@ watch(isWindows, () => {
|
|||||||
@click="stopVnc"
|
@click="stopVnc"
|
||||||
:disabled="vncLoading"
|
:disabled="vncLoading"
|
||||||
>
|
>
|
||||||
<Square class="h-4 w-4 mr-1" />
|
<Square class="size-4 mr-1" />
|
||||||
{{ t('extensions.stop') }}
|
{{ t('extensions.stop') }}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -4889,8 +4889,8 @@ watch(isWindows, () => {
|
|||||||
:aria-label="showPasswords ? t('extensions.rustdesk.hidePassword') : t('extensions.rustdesk.showPassword')"
|
:aria-label="showPasswords ? t('extensions.rustdesk.hidePassword') : t('extensions.rustdesk.showPassword')"
|
||||||
@click="showPasswords = !showPasswords"
|
@click="showPasswords = !showPasswords"
|
||||||
>
|
>
|
||||||
<Eye v-if="!showPasswords" class="h-4 w-4" />
|
<Eye v-if="!showPasswords" class="size-4" />
|
||||||
<EyeOff v-else class="h-4 w-4" />
|
<EyeOff v-else class="size-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -4904,14 +4904,14 @@ watch(isWindows, () => {
|
|||||||
<code class="truncate font-mono">{{ vncStreamUrl }}</code>
|
<code class="truncate font-mono">{{ vncStreamUrl }}</code>
|
||||||
</div>
|
</div>
|
||||||
<Button class="shrink-0" :disabled="loading || vncLoading" @click="saveVncConfig">
|
<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>
|
</Button>
|
||||||
</CardFooter>
|
</CardFooter>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- RustDesk Section -->
|
<!-- 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>
|
<Card>
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
@@ -4923,8 +4923,8 @@ watch(isWindows, () => {
|
|||||||
<Badge :variant="rustdeskStatus?.service_status === 'running' ? 'default' : 'secondary'">
|
<Badge :variant="rustdeskStatus?.service_status === 'running' ? 'default' : 'secondary'">
|
||||||
{{ getRustdeskServiceStatusText(rustdeskStatus?.service_status) }}
|
{{ getRustdeskServiceStatusText(rustdeskStatus?.service_status) }}
|
||||||
</Badge>
|
</Badge>
|
||||||
<Button variant="ghost" size="icon" class="h-8 w-8" :aria-label="t('common.refresh')" @click="loadRustdeskConfig" :disabled="rustdeskLoading">
|
<Button variant="ghost" size="icon-sm" :aria-label="t('common.refresh')" @click="loadRustdeskConfig" :disabled="rustdeskLoading">
|
||||||
<RefreshCw :class="['h-4 w-4', rustdeskLoading ? 'animate-spin' : '']" />
|
<RefreshCw :class="['size-4', rustdeskLoading ? 'animate-spin' : '']" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -4948,7 +4948,7 @@ watch(isWindows, () => {
|
|||||||
@click="startRustdesk"
|
@click="startRustdesk"
|
||||||
:disabled="rustdeskLoading"
|
:disabled="rustdeskLoading"
|
||||||
>
|
>
|
||||||
<Play class="h-4 w-4 mr-1" />
|
<Play class="size-4 mr-1" />
|
||||||
{{ t('extensions.start') }}
|
{{ t('extensions.start') }}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
@@ -4958,7 +4958,7 @@ watch(isWindows, () => {
|
|||||||
@click="stopRustdesk"
|
@click="stopRustdesk"
|
||||||
:disabled="rustdeskLoading"
|
:disabled="rustdeskLoading"
|
||||||
>
|
>
|
||||||
<Square class="h-4 w-4 mr-1" />
|
<Square class="size-4 mr-1" />
|
||||||
{{ t('extensions.stop') }}
|
{{ t('extensions.stop') }}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -5022,8 +5022,8 @@ watch(isWindows, () => {
|
|||||||
:aria-label="showPasswords ? t('extensions.rustdesk.hidePassword') : t('extensions.rustdesk.showPassword')"
|
:aria-label="showPasswords ? t('extensions.rustdesk.hidePassword') : t('extensions.rustdesk.showPassword')"
|
||||||
@click="showPasswords = !showPasswords"
|
@click="showPasswords = !showPasswords"
|
||||||
>
|
>
|
||||||
<Eye v-if="!showPasswords" class="h-4 w-4" />
|
<Eye v-if="!showPasswords" class="size-4" />
|
||||||
<EyeOff v-else class="h-4 w-4" />
|
<EyeOff v-else class="size-4" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -5043,16 +5043,16 @@ watch(isWindows, () => {
|
|||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
class="h-8 w-8"
|
class="size-8"
|
||||||
:aria-label="t('extensions.rustdesk.copyId')"
|
:aria-label="t('extensions.rustdesk.copyId')"
|
||||||
@click="copyToClipboard(rustdeskConfig?.device_id || '', 'id')"
|
@click="copyToClipboard(rustdeskConfig?.device_id || '', 'id')"
|
||||||
:disabled="!rustdeskConfig?.device_id"
|
:disabled="!rustdeskConfig?.device_id"
|
||||||
>
|
>
|
||||||
<Check v-if="rustdeskCopied === 'id'" class="h-4 w-4 text-success" />
|
<Check v-if="rustdeskCopied === 'id'" class="size-4 text-success" />
|
||||||
<Copy v-else class="h-4 w-4" />
|
<Copy v-else class="size-4" />
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="outline" size="sm" @click="regenerateRustdeskId" :disabled="rustdeskLoading">
|
<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') }}
|
{{ t('extensions.rustdesk.regenerateId') }}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -5066,16 +5066,16 @@ watch(isWindows, () => {
|
|||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
class="h-8 w-8"
|
class="size-8"
|
||||||
:aria-label="t('extensions.rustdesk.copyPassword')"
|
:aria-label="t('extensions.rustdesk.copyPassword')"
|
||||||
@click="copyToClipboard(rustdeskPassword?.device_password || '', 'password')"
|
@click="copyToClipboard(rustdeskPassword?.device_password || '', 'password')"
|
||||||
:disabled="!rustdeskPassword?.device_password"
|
:disabled="!rustdeskPassword?.device_password"
|
||||||
>
|
>
|
||||||
<Check v-if="rustdeskCopied === 'password'" class="h-4 w-4 text-success" />
|
<Check v-if="rustdeskCopied === 'password'" class="size-4 text-success" />
|
||||||
<Copy v-else class="h-4 w-4" />
|
<Copy v-else class="size-4" />
|
||||||
</Button>
|
</Button>
|
||||||
<Button variant="outline" size="sm" @click="regenerateRustdeskPassword" :disabled="rustdeskLoading">
|
<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') }}
|
{{ t('extensions.rustdesk.regeneratePassword') }}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -5094,20 +5094,20 @@ watch(isWindows, () => {
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
<CardFooter class="border-t pt-4 justify-end">
|
<CardFooter class="border-t pt-4 justify-end">
|
||||||
<Button :disabled="loading || rustdeskLoading" @click="saveRustdeskConfig">
|
<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>
|
</Button>
|
||||||
</CardFooter>
|
</CardFooter>
|
||||||
</Card>
|
</Card>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Redfish Section -->
|
<!-- 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 -->
|
<!-- Auto-restart: restarting progress -->
|
||||||
<div
|
<div
|
||||||
v-if="autoRestarting"
|
v-if="autoRestarting"
|
||||||
class="flex items-center gap-3 rounded-lg border bg-card px-4 py-3 text-sm shadow-sm"
|
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">
|
<div class="flex-1 min-w-0">
|
||||||
<p class="font-medium">{{ t('settings.autoRestarting') }}</p>
|
<p class="font-medium">{{ t('settings.autoRestarting') }}</p>
|
||||||
<p class="text-xs text-muted-foreground">
|
<p class="text-xs text-muted-foreground">
|
||||||
@@ -5138,7 +5138,7 @@ watch(isWindows, () => {
|
|||||||
<AlertDescription class="flex items-center justify-between gap-3">
|
<AlertDescription class="flex items-center justify-between gap-3">
|
||||||
<span>{{ t('settings.autoRestartFailed') }}</span>
|
<span>{{ t('settings.autoRestartFailed') }}</span>
|
||||||
<Button variant="outline" size="sm" class="text-foreground" @click="triggerAutoRestart">
|
<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') }}
|
{{ t('common.retry') }}
|
||||||
</Button>
|
</Button>
|
||||||
</AlertDescription>
|
</AlertDescription>
|
||||||
@@ -5161,12 +5161,12 @@ watch(isWindows, () => {
|
|||||||
</CardContent>
|
</CardContent>
|
||||||
<CardFooter class="flex items-center justify-between gap-3 border-t pt-4">
|
<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">
|
<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>
|
<span class="truncate">{{ t('settings.restartRequiredHint') }}</span>
|
||||||
</p>
|
</p>
|
||||||
<Button @click="saveRedfishConfig" :disabled="redfishSaving || autoRestarting">
|
<Button @click="saveRedfishConfig" :disabled="redfishSaving || autoRestarting">
|
||||||
<RefreshCw v-if="autoRestarting || redfishSaving" class="h-4 w-4 mr-2 animate-spin" />
|
<RefreshCw v-if="autoRestarting || redfishSaving" class="size-4 mr-2 animate-spin" />
|
||||||
<Save v-else class="h-4 w-4 mr-2" />
|
<Save v-else class="size-4 mr-2" />
|
||||||
{{ autoRestarting ? t('settings.restarting') : t('common.save') }}
|
{{ autoRestarting ? t('settings.restarting') : t('common.save') }}
|
||||||
</Button>
|
</Button>
|
||||||
</CardFooter>
|
</CardFooter>
|
||||||
@@ -5174,7 +5174,7 @@ watch(isWindows, () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- About Section -->
|
<!-- About Section -->
|
||||||
<div v-show="activeSection === 'about'" class="space-y-6">
|
<div v-show="activeSection === 'about'" class="space-y-4">
|
||||||
<Card>
|
<Card>
|
||||||
<CardHeader class="flex flex-row items-start justify-between space-y-0">
|
<CardHeader class="flex flex-row items-start justify-between space-y-0">
|
||||||
<div class="space-y-1.5">
|
<div class="space-y-1.5">
|
||||||
@@ -5184,12 +5184,12 @@ watch(isWindows, () => {
|
|||||||
<Button
|
<Button
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
size="icon"
|
size="icon"
|
||||||
class="h-8 w-8"
|
class="size-8"
|
||||||
:aria-label="t('common.refresh')"
|
:aria-label="t('common.refresh')"
|
||||||
:disabled="updateRunning || updateLoading"
|
:disabled="updateRunning || updateLoading"
|
||||||
@click="loadUpdateOverview"
|
@click="loadUpdateOverview"
|
||||||
>
|
>
|
||||||
<RefreshCw :class="['h-4 w-4', (updateLoading || updateRunning) ? 'animate-spin' : '']" />
|
<RefreshCw :class="['size-4', (updateLoading || updateRunning) ? 'animate-spin' : '']" />
|
||||||
</Button>
|
</Button>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent class="space-y-4">
|
<CardContent class="space-y-4">
|
||||||
@@ -5253,7 +5253,7 @@ watch(isWindows, () => {
|
|||||||
:disabled="updateRunning || !updateOverview?.upgrade_available"
|
:disabled="updateRunning || !updateOverview?.upgrade_available"
|
||||||
@click="startOnlineUpgrade"
|
@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') }}
|
{{ t('settings.startUpgrade') }}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</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 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">
|
<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">
|
<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>
|
<span class="truncate">{{ t('settings.otgFunctionMinWarning') }}</span>
|
||||||
</p>
|
</p>
|
||||||
<p v-else-if="activeSection === 'hid' && !isCh9329DescriptorValid" class="flex min-w-0 items-center gap-1.5 text-xs text-warning">
|
<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>
|
<span class="truncate">{{ t('settings.ch9329StringLengthWarning') }}</span>
|
||||||
</p>
|
</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">
|
<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>
|
<span class="truncate">{{ t('settings.ch9329DescriptorLoading') }}</span>
|
||||||
</p>
|
</p>
|
||||||
<p v-if="saveError" class="text-xs text-destructive">{{ saveError }}</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>
|
<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">
|
<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>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -5307,7 +5307,7 @@ watch(isWindows, () => {
|
|||||||
{{ t('common.later') }}
|
{{ t('common.later') }}
|
||||||
</Button>
|
</Button>
|
||||||
<Button @click="restartServer" :disabled="restarting">
|
<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') }}
|
{{ restarting ? t('settings.restarting') : t('common.restartNow') }}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
|
|||||||
@@ -535,18 +535,18 @@ const stepIcons = [User, Video, Keyboard, Puzzle]
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<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">
|
<Card class="w-full max-w-lg relative">
|
||||||
<!-- Language Switcher -->
|
<!-- Language Switcher -->
|
||||||
<div class="absolute top-4 right-4">
|
<div class="absolute top-4 right-4">
|
||||||
<LanguageToggleButton />
|
<LanguageToggleButton />
|
||||||
</div>
|
</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">
|
<div class="mx-auto flex justify-center">
|
||||||
<BrandMark size="xl" />
|
<BrandMark size="lg" />
|
||||||
</div>
|
</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>
|
<CardDescription>{{ t('setup.description') }}</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
|
|
||||||
@@ -807,7 +807,7 @@ const stepIcons = [User, Video, Keyboard, Puzzle]
|
|||||||
{{ t('setup.advancedEncoder') }} ({{ t('common.optional') }})
|
{{ t('setup.advancedEncoder') }} ({{ t('common.optional') }})
|
||||||
</span>
|
</span>
|
||||||
<ChevronRight
|
<ChevronRight
|
||||||
class="h-4 w-4 transition-transform duration-200"
|
class="size-4 transition-transform duration-200"
|
||||||
:class="{ 'rotate-90': showAdvancedEncoder }"
|
:class="{ 'rotate-90': showAdvancedEncoder }"
|
||||||
/>
|
/>
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
Reference in New Issue
Block a user