mirror of
https://github.com/mofeng-git/One-KVM.git
synced 2026-09-12 10:34:24 +08:00
fix: 完善 MSD 错误提示;解决 MSD 文件名称过长问题;优化粘贴文本样式;优化所有下拉框样式;删除“画面静止时会自动降帧”提示
This commit is contained in:
@@ -7,7 +7,7 @@ import { Loader2, RefreshCw, Volume2 } from 'lucide-vue-next'
|
||||
import { audioApi, configApi } from '@/api'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { NativeSelect, NativeSelectOption } from '@/components/ui/native-select'
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
@@ -48,6 +48,7 @@ const applying = ref(false)
|
||||
const audioEnabled = ref(false)
|
||||
const selectedDevice = ref('')
|
||||
const selectedQuality = ref<'voice' | 'balanced' | 'high'>('balanced')
|
||||
const EMPTY_SELECT_VALUE = '__one-kvm-empty-select-value__'
|
||||
|
||||
async function handleVolumeChange(value: number[] | undefined) {
|
||||
if (!value || value.length === 0 || value[0] === undefined) return
|
||||
@@ -226,23 +227,26 @@ onUnmounted(() => {
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label class="text-xs text-muted-foreground">{{ t('actionbar.audioDevice') }}</Label>
|
||||
<NativeSelect
|
||||
<Select
|
||||
:model-value="selectedDevice"
|
||||
:disabled="loadingDevices || devices.length === 0"
|
||||
size="sm"
|
||||
class="w-full text-xs"
|
||||
@update:model-value="selectedDevice = $event as string"
|
||||
@update:model-value="selectedDevice = $event === EMPTY_SELECT_VALUE ? '' : String($event)"
|
||||
>
|
||||
<NativeSelectOption value="">{{ t('actionbar.selectAudioDevice') }}</NativeSelectOption>
|
||||
<NativeSelectOption
|
||||
v-for="device in devices"
|
||||
:key="device.name"
|
||||
:value="device.name"
|
||||
class="text-xs"
|
||||
>
|
||||
{{ device.description || device.name }}
|
||||
</NativeSelectOption>
|
||||
</NativeSelect>
|
||||
<SelectTrigger size="sm" class="w-full text-xs">
|
||||
<SelectValue :placeholder="t('actionbar.selectAudioDevice')" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem :value="EMPTY_SELECT_VALUE" class="text-xs">{{ t('actionbar.selectAudioDevice') }}</SelectItem>
|
||||
<SelectItem
|
||||
v-for="device in devices"
|
||||
:key="device.name"
|
||||
:value="device.name"
|
||||
class="text-xs"
|
||||
>
|
||||
{{ device.description || device.name }}
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
|
||||
@@ -5,7 +5,7 @@ import { toast } from 'vue-sonner'
|
||||
import { Loader2, Mic, MicOff, RefreshCw } from 'lucide-vue-next'
|
||||
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { NativeSelect, NativeSelectOption } from '@/components/ui/native-select'
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
||||
import { getMicrophone } from '@/composables/useMicrophone'
|
||||
|
||||
const { t } = useI18n()
|
||||
@@ -69,25 +69,25 @@ async function selectDevice(value: unknown) {
|
||||
</Button>
|
||||
|
||||
<div v-else>
|
||||
<NativeSelect
|
||||
<Select
|
||||
:model-value="microphone.selectedDeviceId.value"
|
||||
:disabled="microphone.loadingDevices.value || microphone.busy.value || microphone.inputDevices.value.length === 0"
|
||||
size="sm"
|
||||
class="w-full text-xs"
|
||||
@update:model-value="selectDevice"
|
||||
>
|
||||
<NativeSelectOption v-if="microphone.inputDevices.value.length === 0" value="">
|
||||
{{ t('actionbar.noMicrophoneDevices') }}
|
||||
</NativeSelectOption>
|
||||
<NativeSelectOption
|
||||
v-for="device in microphone.inputDevices.value"
|
||||
:key="device.deviceId"
|
||||
:value="device.deviceId"
|
||||
class="text-xs"
|
||||
>
|
||||
{{ device.label }}
|
||||
</NativeSelectOption>
|
||||
</NativeSelect>
|
||||
<SelectTrigger size="sm" class="w-full text-xs">
|
||||
<SelectValue :placeholder="t('actionbar.noMicrophoneDevices')" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem
|
||||
v-for="device in microphone.inputDevices.value"
|
||||
:key="device.deviceId"
|
||||
:value="device.deviceId"
|
||||
class="text-xs"
|
||||
>
|
||||
{{ device.label }}
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
|
||||
@@ -3,7 +3,6 @@ import { ref, computed, onMounted, onUnmounted } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Textarea } from '@/components/ui/textarea'
|
||||
import { Label } from '@/components/ui/label'
|
||||
import { Progress } from '@/components/ui/progress'
|
||||
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert'
|
||||
import { CornerDownLeft, Square, AlertCircle } from 'lucide-vue-next'
|
||||
@@ -182,24 +181,19 @@ function handleKeydown(e: KeyboardEvent) {
|
||||
|
||||
<template>
|
||||
<div class="p-4 space-y-4">
|
||||
<div class="space-y-1">
|
||||
<h3 class="font-semibold text-sm">{{ t('paste.title') }}</h3>
|
||||
<p class="text-xs text-muted-foreground">{{ t('paste.description') }}</p>
|
||||
</div>
|
||||
<h3 class="font-semibold text-sm">{{ t('paste.title') }}</h3>
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label for="paste-text">{{ t('paste.label') }}</Label>
|
||||
<Textarea
|
||||
id="paste-text"
|
||||
ref="textareaRef"
|
||||
v-model="text"
|
||||
:placeholder="t('paste.placeholder')"
|
||||
class="min-h-[120px] resize-none font-mono text-sm"
|
||||
:disabled="isPasting"
|
||||
@keydown="handleKeydown"
|
||||
@keyup.stop
|
||||
/>
|
||||
</div>
|
||||
<Textarea
|
||||
id="paste-text"
|
||||
ref="textareaRef"
|
||||
v-model="text"
|
||||
:aria-label="t('paste.title')"
|
||||
:placeholder="t('paste.placeholder')"
|
||||
class="min-h-[120px] resize-none text-sm"
|
||||
:disabled="isPasting"
|
||||
@keydown="handleKeydown"
|
||||
@keyup.stop
|
||||
/>
|
||||
|
||||
<!-- Warning for untypable characters -->
|
||||
<Alert v-if="hasUntypableChars && !isPasting" variant="warning">
|
||||
|
||||
@@ -184,8 +184,6 @@ export default {
|
||||
},
|
||||
paste: {
|
||||
title: 'Paste Text',
|
||||
description: 'Enter text to send to remote host as keyboard input',
|
||||
label: 'Text Content',
|
||||
placeholder: 'Enter text to paste here...',
|
||||
hint: 'Press Ctrl+Enter to send quickly',
|
||||
confirm: 'Send',
|
||||
@@ -945,7 +943,6 @@ export default {
|
||||
fps: 'FPS',
|
||||
fpsTarget: 'Target FPS',
|
||||
fpsActual: 'Actual FPS',
|
||||
fpsStaticHint: 'Frame rate drops automatically while the image is static',
|
||||
paused: 'Paused',
|
||||
backend: 'Backend',
|
||||
currentMode: 'Current Mode',
|
||||
|
||||
@@ -184,8 +184,6 @@ export default {
|
||||
},
|
||||
paste: {
|
||||
title: '粘贴文本',
|
||||
description: '输入要发送到远程主机的文本,将以键盘输入方式发送',
|
||||
label: '文本内容',
|
||||
placeholder: '在此输入要粘贴的文本...',
|
||||
hint: '按 Ctrl+Enter 快速发送',
|
||||
confirm: '发送',
|
||||
@@ -944,7 +942,6 @@ export default {
|
||||
fps: '帧率',
|
||||
fpsTarget: '目标帧率',
|
||||
fpsActual: '实际帧率',
|
||||
fpsStaticHint: '画面静止时会自动降帧',
|
||||
paused: '已暂停',
|
||||
backend: '后端',
|
||||
currentMode: '当前模式',
|
||||
|
||||
@@ -308,13 +308,6 @@ const videoDetails = computed<StatusDetail[]>(() => {
|
||||
{ label: t('statusCard.fpsActual'), value: actualFpsValue, status: actualStatus },
|
||||
]
|
||||
|
||||
if (videoMode.value === 'mjpeg' && !paused && receivedFps > 0 && receivedFps < (stream.targetFps ?? 0)) {
|
||||
details.push({
|
||||
label: '',
|
||||
value: t('statusCard.fpsStaticHint'),
|
||||
})
|
||||
}
|
||||
|
||||
return details
|
||||
})
|
||||
|
||||
|
||||
@@ -71,7 +71,6 @@ import { Label } from '@/components/ui/label'
|
||||
import { Switch } from '@/components/ui/switch'
|
||||
import { Separator } from '@/components/ui/separator'
|
||||
import { Badge } from '@/components/ui/badge'
|
||||
import { NativeSelect, NativeSelectOption } from '@/components/ui/native-select'
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table'
|
||||
import { ButtonGroup } from '@/components/ui/button-group'
|
||||
@@ -151,6 +150,7 @@ const configStore = useConfigStore()
|
||||
const authStore = useAuthStore()
|
||||
const featureVisibility = useFeatureVisibility()
|
||||
const { theme, setTheme } = useTheme()
|
||||
const EMPTY_SELECT_VALUE = '__one-kvm-empty-select-value__'
|
||||
|
||||
const isWindows = computed(() => systemStore.platform?.mode === 'windows')
|
||||
|
||||
@@ -2927,38 +2927,57 @@ watch(isWindows, () => {
|
||||
<CardContent class="space-y-4">
|
||||
<div class="space-y-2">
|
||||
<Label for="video-device">{{ t('settings.videoDevice') }}</Label>
|
||||
<NativeSelect id="video-device" v-model="config.video_device" class="w-full">
|
||||
<NativeSelectOption value="">{{ t('settings.selectDevice') }}</NativeSelectOption>
|
||||
<NativeSelectOption v-for="dev in devices.video" :key="dev.path" :value="dev.path">{{ formatVideoDeviceLabel(dev) }}</NativeSelectOption>
|
||||
</NativeSelect>
|
||||
<Select
|
||||
:model-value="config.video_device"
|
||||
@update:model-value="value => config.video_device = value === EMPTY_SELECT_VALUE ? '' : String(value)"
|
||||
>
|
||||
<SelectTrigger id="video-device" class="w-full"><SelectValue :placeholder="t('settings.selectDevice')" /></SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem :value="EMPTY_SELECT_VALUE">{{ t('settings.selectDevice') }}</SelectItem>
|
||||
<SelectItem v-for="dev in devices.video" :key="dev.path" :value="dev.path">{{ formatVideoDeviceLabel(dev) }}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label for="video-format">{{ t('settings.videoFormat') }}</Label>
|
||||
<NativeSelect id="video-format" v-model="config.video_format" class="w-full" :disabled="!config.video_device">
|
||||
<NativeSelectOption value="">{{ t('settings.selectFormat') }}</NativeSelectOption>
|
||||
<NativeSelectOption
|
||||
v-for="fmt in availableFormatOptions"
|
||||
:key="fmt.format"
|
||||
:value="fmt.format"
|
||||
:disabled="fmt.disabled"
|
||||
>
|
||||
{{ fmt.format }} - {{ fmt.description }}{{ fmt.disabled ? t('common.notSupportedYet') : '' }}
|
||||
</NativeSelectOption>
|
||||
</NativeSelect>
|
||||
<Select
|
||||
:model-value="config.video_format"
|
||||
:disabled="!config.video_device"
|
||||
@update:model-value="value => config.video_format = value === EMPTY_SELECT_VALUE ? '' : String(value)"
|
||||
>
|
||||
<SelectTrigger id="video-format" class="w-full"><SelectValue :placeholder="t('settings.selectFormat')" /></SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem :value="EMPTY_SELECT_VALUE">{{ t('settings.selectFormat') }}</SelectItem>
|
||||
<SelectItem
|
||||
v-for="fmt in availableFormatOptions"
|
||||
:key="fmt.format"
|
||||
:value="fmt.format"
|
||||
:disabled="fmt.disabled"
|
||||
>
|
||||
{{ fmt.format }} - {{ fmt.description }}{{ fmt.disabled ? t('common.notSupportedYet') : '' }}
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div class="grid gap-4 sm:grid-cols-2">
|
||||
<div class="space-y-2">
|
||||
<Label for="video-resolution">{{ t('settings.resolution') }}</Label>
|
||||
<NativeSelect id="video-resolution" :model-value="`${config.video_width}x${config.video_height}`" class="w-full" :disabled="!config.video_format" @update:model-value="value => { const parts = String(value).split('x').map(Number); if (parts[0] && parts[1]) { config.video_width = parts[0]; config.video_height = parts[1]; } }">
|
||||
<NativeSelectOption v-for="res in availableResolutions" :key="`${res.width}x${res.height}`" :value="`${res.width}x${res.height}`">{{ res.width }}x{{ res.height }}</NativeSelectOption>
|
||||
</NativeSelect>
|
||||
<Select :model-value="`${config.video_width}x${config.video_height}`" :disabled="!config.video_format" @update:model-value="value => { const parts = String(value).split('x').map(Number); if (parts[0] && parts[1]) { config.video_width = parts[0]; config.video_height = parts[1]; } }">
|
||||
<SelectTrigger id="video-resolution" class="w-full"><SelectValue /></SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem v-for="res in availableResolutions" :key="`${res.width}x${res.height}`" :value="`${res.width}x${res.height}`">{{ res.width }}x{{ res.height }}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label for="video-fps">{{ t('settings.frameRate') }}</Label>
|
||||
<NativeSelect id="video-fps" :model-value="config.video_fps" class="w-full" :disabled="!config.video_format" @update:model-value="value => config.video_fps = Number(value)">
|
||||
<NativeSelectOption v-for="fps in availableFps" :key="fps" :value="fps">{{ formatFpsLabel(fps) }}</NativeSelectOption>
|
||||
<NativeSelectOption v-if="!availableFps.includes(config.video_fps)" :value="config.video_fps">{{ formatFpsLabel(config.video_fps) }}</NativeSelectOption>
|
||||
</NativeSelect>
|
||||
<Select :model-value="config.video_fps" :disabled="!config.video_format" @update:model-value="value => config.video_fps = Number(value)">
|
||||
<SelectTrigger id="video-fps" class="w-full"><SelectValue /></SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem v-for="fps in availableFps" :key="fps" :value="fps">{{ formatFpsLabel(fps) }}</SelectItem>
|
||||
<SelectItem v-if="!availableFps.includes(config.video_fps)" :value="config.video_fps">{{ formatFpsLabel(config.video_fps) }}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
@@ -3077,10 +3096,16 @@ watch(isWindows, () => {
|
||||
</div>
|
||||
<div v-if="config.hid_backend === 'ch9329'" class="space-y-2">
|
||||
<Label for="serial-device">{{ t('settings.serialDevice') }}</Label>
|
||||
<NativeSelect id="serial-device" v-model="config.hid_serial_device" class="w-full">
|
||||
<NativeSelectOption value="">{{ t('settings.selectDevice') }}</NativeSelectOption>
|
||||
<NativeSelectOption v-for="dev in devices.serial" :key="dev.path" :value="dev.path">{{ dev.name }} ({{ dev.path }})</NativeSelectOption>
|
||||
</NativeSelect>
|
||||
<Select
|
||||
:model-value="config.hid_serial_device"
|
||||
@update:model-value="value => config.hid_serial_device = value === EMPTY_SELECT_VALUE ? '' : String(value)"
|
||||
>
|
||||
<SelectTrigger id="serial-device" class="w-full"><SelectValue :placeholder="t('settings.selectDevice')" /></SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem :value="EMPTY_SELECT_VALUE">{{ t('settings.selectDevice') }}</SelectItem>
|
||||
<SelectItem v-for="dev in devices.serial" :key="dev.path" :value="dev.path">{{ dev.name }} ({{ dev.path }})</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div v-if="config.hid_backend === 'ch9329'" class="space-y-2">
|
||||
<Label for="serial-baudrate">{{ t('settings.baudRate') }}</Label>
|
||||
@@ -3093,10 +3118,16 @@ watch(isWindows, () => {
|
||||
</div>
|
||||
<div v-if="config.hid_backend === 'otg'" class="space-y-2">
|
||||
<Label for="otg-udc">{{ t('settings.otgUdc') }}</Label>
|
||||
<NativeSelect id="otg-udc" v-model="config.hid_otg_udc" class="w-full">
|
||||
<NativeSelectOption value="">{{ t('settings.autoRecommended') }}</NativeSelectOption>
|
||||
<NativeSelectOption v-for="udc in devices.udc" :key="udc.name" :value="udc.name">{{ udc.name }}</NativeSelectOption>
|
||||
</NativeSelect>
|
||||
<Select
|
||||
:model-value="config.hid_otg_udc"
|
||||
@update:model-value="value => config.hid_otg_udc = value === EMPTY_SELECT_VALUE ? '' : String(value)"
|
||||
>
|
||||
<SelectTrigger id="otg-udc" class="w-full"><SelectValue :placeholder="t('settings.autoRecommended')" /></SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem :value="EMPTY_SELECT_VALUE">{{ t('settings.autoRecommended') }}</SelectItem>
|
||||
<SelectItem v-for="udc in devices.udc" :key="udc.name" :value="udc.name">{{ udc.name }}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<template v-if="config.hid_backend === 'ch9329'">
|
||||
@@ -3343,30 +3374,37 @@ watch(isWindows, () => {
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label for="otg-network-interface">{{ t('settings.otgNetworkInterface') }}</Label>
|
||||
<NativeSelect
|
||||
id="otg-network-interface"
|
||||
v-model="config.otg_network_interface"
|
||||
class="w-full"
|
||||
<Select
|
||||
:model-value="config.otg_network_interface"
|
||||
:disabled="otgNetworkInterfaces.length === 0"
|
||||
:aria-invalid="Boolean(otgNetworkInterfacesError)"
|
||||
@update:model-value="value => config.otg_network_interface = value === EMPTY_SELECT_VALUE ? '' : String(value)"
|
||||
>
|
||||
<NativeSelectOption
|
||||
v-if="otgNetworkInterfacesError"
|
||||
:value="config.otg_network_interface"
|
||||
<SelectTrigger
|
||||
id="otg-network-interface"
|
||||
class="w-full"
|
||||
:aria-invalid="Boolean(otgNetworkInterfacesError)"
|
||||
>
|
||||
{{ otgNetworkInterfacesError }}
|
||||
</NativeSelectOption>
|
||||
<NativeSelectOption v-else-if="otgNetworkInterfaces.length === 0" value="">
|
||||
{{ t('settings.otgNetworkNone') }}
|
||||
</NativeSelectOption>
|
||||
<NativeSelectOption
|
||||
v-for="item in otgNetworkInterfaces"
|
||||
:key="item.name"
|
||||
:value="item.name"
|
||||
>
|
||||
{{ item.name }} · {{ item.interface_type }}
|
||||
</NativeSelectOption>
|
||||
</NativeSelect>
|
||||
<SelectValue :placeholder="otgNetworkInterfacesError || t('settings.otgNetworkNone')" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem
|
||||
v-if="otgNetworkInterfacesError"
|
||||
:value="config.otg_network_interface || EMPTY_SELECT_VALUE"
|
||||
>
|
||||
{{ otgNetworkInterfacesError }}
|
||||
</SelectItem>
|
||||
<SelectItem v-else-if="otgNetworkInterfaces.length === 0" :value="EMPTY_SELECT_VALUE">
|
||||
{{ t('settings.otgNetworkNone') }}
|
||||
</SelectItem>
|
||||
<SelectItem
|
||||
v-for="item in otgNetworkInterfaces"
|
||||
:key="item.name"
|
||||
:value="item.name"
|
||||
>
|
||||
{{ item.name }} · {{ item.interface_type }}
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
@@ -4058,17 +4096,23 @@ watch(isWindows, () => {
|
||||
<div v-if="['usbrelay', 'serial'].includes(atxConfig.driver)" class="grid gap-4 sm:grid-cols-2">
|
||||
<div v-if="['usbrelay', 'serial'].includes(atxConfig.driver)" class="space-y-2">
|
||||
<Label for="atx-device">{{ t('settings.atxDevice') }}</Label>
|
||||
<NativeSelect id="atx-device" v-model="atxConfig.device" class="w-full">
|
||||
<NativeSelectOption value="">{{ t('settings.selectDevice') }}</NativeSelectOption>
|
||||
<NativeSelectOption
|
||||
v-for="dev in getAtxDevicesForDriver(atxConfig.driver)"
|
||||
:key="dev"
|
||||
:value="dev"
|
||||
:disabled="atxConfig.driver === 'serial' && isAtxSerialDeviceReserved(dev)"
|
||||
>
|
||||
{{ formatAtxDeviceLabel(atxConfig.driver, dev) }}
|
||||
</NativeSelectOption>
|
||||
</NativeSelect>
|
||||
<Select
|
||||
:model-value="atxConfig.device"
|
||||
@update:model-value="value => atxConfig.device = value === EMPTY_SELECT_VALUE ? '' : String(value)"
|
||||
>
|
||||
<SelectTrigger id="atx-device" class="w-full"><SelectValue :placeholder="t('settings.selectDevice')" /></SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem :value="EMPTY_SELECT_VALUE">{{ t('settings.selectDevice') }}</SelectItem>
|
||||
<SelectItem
|
||||
v-for="dev in getAtxDevicesForDriver(atxConfig.driver)"
|
||||
:key="dev"
|
||||
:value="dev"
|
||||
:disabled="atxConfig.driver === 'serial' && isAtxSerialDeviceReserved(dev)"
|
||||
>
|
||||
{{ formatAtxDeviceLabel(atxConfig.driver, dev) }}
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div v-if="atxConfig.driver === 'serial'" class="space-y-2">
|
||||
<Label for="atx-baudrate">{{ t('settings.baudRate') }}</Label>
|
||||
@@ -4089,10 +4133,16 @@ watch(isWindows, () => {
|
||||
<div class="grid gap-3 sm:grid-cols-3">
|
||||
<div class="space-y-2">
|
||||
<Label for="power-device">{{ t('settings.atxGpioChip') }}</Label>
|
||||
<NativeSelect id="power-device" v-model="atxConfig.power.device" class="w-full">
|
||||
<NativeSelectOption value="">{{ t('settings.atxDriverNone') }}</NativeSelectOption>
|
||||
<NativeSelectOption v-for="dev in atxDevices.gpio_chips" :key="dev" :value="dev">{{ dev }}</NativeSelectOption>
|
||||
</NativeSelect>
|
||||
<Select
|
||||
:model-value="atxConfig.power.device"
|
||||
@update:model-value="value => atxConfig.power.device = value === EMPTY_SELECT_VALUE ? '' : String(value)"
|
||||
>
|
||||
<SelectTrigger id="power-device" class="w-full"><SelectValue :placeholder="t('settings.atxDriverNone')" /></SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem :value="EMPTY_SELECT_VALUE">{{ t('settings.atxDriverNone') }}</SelectItem>
|
||||
<SelectItem v-for="dev in atxDevices.gpio_chips" :key="dev" :value="dev">{{ dev }}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label for="power-pin">{{ t('settings.atxPin') }}</Label>
|
||||
@@ -4113,10 +4163,16 @@ watch(isWindows, () => {
|
||||
<div class="grid gap-3 sm:grid-cols-3">
|
||||
<div class="space-y-2">
|
||||
<Label for="reset-device">{{ t('settings.atxGpioChip') }}</Label>
|
||||
<NativeSelect id="reset-device" v-model="atxConfig.reset.device" class="w-full">
|
||||
<NativeSelectOption value="">{{ t('settings.atxDriverNone') }}</NativeSelectOption>
|
||||
<NativeSelectOption v-for="dev in atxDevices.gpio_chips" :key="dev" :value="dev">{{ dev }}</NativeSelectOption>
|
||||
</NativeSelect>
|
||||
<Select
|
||||
:model-value="atxConfig.reset.device"
|
||||
@update:model-value="value => atxConfig.reset.device = value === EMPTY_SELECT_VALUE ? '' : String(value)"
|
||||
>
|
||||
<SelectTrigger id="reset-device" class="w-full"><SelectValue :placeholder="t('settings.atxDriverNone')" /></SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem :value="EMPTY_SELECT_VALUE">{{ t('settings.atxDriverNone') }}</SelectItem>
|
||||
<SelectItem v-for="dev in atxDevices.gpio_chips" :key="dev" :value="dev">{{ dev }}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label for="reset-pin">{{ t('settings.atxPin') }}</Label>
|
||||
@@ -4137,10 +4193,16 @@ watch(isWindows, () => {
|
||||
<div class="grid gap-3 sm:grid-cols-3">
|
||||
<div class="space-y-2">
|
||||
<Label for="led-device">{{ t('settings.atxGpioChip') }}</Label>
|
||||
<NativeSelect id="led-device" v-model="atxConfig.led.device" class="w-full">
|
||||
<NativeSelectOption value="">{{ t('settings.atxDriverNone') }}</NativeSelectOption>
|
||||
<NativeSelectOption v-for="dev in atxDevices.gpio_chips" :key="dev" :value="dev">{{ dev }}</NativeSelectOption>
|
||||
</NativeSelect>
|
||||
<Select
|
||||
:model-value="atxConfig.led.device"
|
||||
@update:model-value="value => atxConfig.led.device = value === EMPTY_SELECT_VALUE ? '' : String(value)"
|
||||
>
|
||||
<SelectTrigger id="led-device" class="w-full"><SelectValue :placeholder="t('settings.atxDriverNone')" /></SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem :value="EMPTY_SELECT_VALUE">{{ t('settings.atxDriverNone') }}</SelectItem>
|
||||
<SelectItem v-for="dev in atxDevices.gpio_chips" :key="dev" :value="dev">{{ dev }}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label for="led-pin">{{ t('settings.atxPin') }}</Label>
|
||||
@@ -4161,10 +4223,16 @@ watch(isWindows, () => {
|
||||
<div class="grid gap-3 sm:grid-cols-3">
|
||||
<div class="space-y-2">
|
||||
<Label for="hdd-device">{{ t('settings.atxGpioChip') }}</Label>
|
||||
<NativeSelect id="hdd-device" v-model="atxConfig.hdd.device" class="w-full">
|
||||
<NativeSelectOption value="">{{ t('settings.atxDriverNone') }}</NativeSelectOption>
|
||||
<NativeSelectOption v-for="dev in atxDevices.gpio_chips" :key="dev" :value="dev">{{ dev }}</NativeSelectOption>
|
||||
</NativeSelect>
|
||||
<Select
|
||||
:model-value="atxConfig.hdd.device"
|
||||
@update:model-value="value => atxConfig.hdd.device = value === EMPTY_SELECT_VALUE ? '' : String(value)"
|
||||
>
|
||||
<SelectTrigger id="hdd-device" class="w-full"><SelectValue :placeholder="t('settings.atxDriverNone')" /></SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem :value="EMPTY_SELECT_VALUE">{{ t('settings.atxDriverNone') }}</SelectItem>
|
||||
<SelectItem v-for="dev in atxDevices.gpio_chips" :key="dev" :value="dev">{{ dev }}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div class="space-y-2">
|
||||
<Label for="hdd-pin">{{ t('settings.atxPin') }}</Label>
|
||||
|
||||
@@ -27,7 +27,6 @@ import {
|
||||
import { Switch } from '@/components/ui/switch'
|
||||
import { Stepper, StepperItem, StepperSeparator, StepperTitle, StepperTrigger } from '@/components/ui/stepper'
|
||||
import { Alert, AlertDescription } from '@/components/ui/alert'
|
||||
import { NativeSelect, NativeSelectOption } from '@/components/ui/native-select'
|
||||
import {
|
||||
Eye,
|
||||
EyeOff,
|
||||
@@ -75,6 +74,7 @@ const platform = ref<PlatformCapabilities | null>(null)
|
||||
const isWindows = computed(() => platform.value?.mode === 'windows')
|
||||
const audioSupported = computed(() => platform.value?.audio.available ?? true)
|
||||
const totalSteps = 4
|
||||
const EMPTY_SELECT_VALUE = '__one-kvm-empty-select-value__'
|
||||
|
||||
const hidBackend = ref('ch9329')
|
||||
const ch9329Port = ref('')
|
||||
@@ -698,12 +698,20 @@ const stepIcons = [User, Video, Keyboard, Puzzle]
|
||||
</HoverCardContent>
|
||||
</HoverCard>
|
||||
</div>
|
||||
<NativeSelect v-model="videoDevice" class="w-full">
|
||||
<NativeSelectOption value="">{{ t('setup.selectVideoDevice') }}</NativeSelectOption>
|
||||
<NativeSelectOption v-for="dev in devices.video" :key="dev.path" :value="dev.path">
|
||||
<Select
|
||||
:model-value="videoDevice"
|
||||
@update:model-value="value => videoDevice = value === EMPTY_SELECT_VALUE ? '' : String(value)"
|
||||
>
|
||||
<SelectTrigger id="videoDevice" class="w-full">
|
||||
<SelectValue :placeholder="t('setup.selectVideoDevice')" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem :value="EMPTY_SELECT_VALUE">{{ t('setup.selectVideoDevice') }}</SelectItem>
|
||||
<SelectItem v-for="dev in devices.video" :key="dev.path" :value="dev.path">
|
||||
{{ formatVideoDeviceLabel(dev) }}
|
||||
</NativeSelectOption>
|
||||
</NativeSelect>
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<Alert v-if="videoDevice && !selectedDeviceHasSignal" variant="warning">
|
||||
@@ -731,37 +739,58 @@ const stepIcons = [User, Video, Keyboard, Puzzle]
|
||||
</HoverCardContent>
|
||||
</HoverCard>
|
||||
</div>
|
||||
<NativeSelect v-model="videoFormat" class="w-full">
|
||||
<NativeSelectOption value="">{{ t('setup.selectFormat') }}</NativeSelectOption>
|
||||
<NativeSelectOption v-for="fmt in availableFormats" :key="fmt.format" :value="fmt.format">
|
||||
<Select
|
||||
:model-value="videoFormat"
|
||||
@update:model-value="value => videoFormat = value === EMPTY_SELECT_VALUE ? '' : String(value)"
|
||||
>
|
||||
<SelectTrigger id="videoFormat" class="w-full">
|
||||
<SelectValue :placeholder="t('setup.selectFormat')" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem :value="EMPTY_SELECT_VALUE">{{ t('setup.selectFormat') }}</SelectItem>
|
||||
<SelectItem v-for="fmt in availableFormats" :key="fmt.format" :value="fmt.format">
|
||||
{{ fmt.format }} - {{ fmt.description }}
|
||||
</NativeSelectOption>
|
||||
</NativeSelect>
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div v-if="videoFormat" class="grid grid-cols-2 gap-4">
|
||||
<div class="space-y-2">
|
||||
<Label for="videoResolution">{{ t('setup.resolution') }}</Label>
|
||||
<NativeSelect v-model="videoResolution" class="w-full">
|
||||
<NativeSelectOption value="">{{ t('setup.selectResolution') }}</NativeSelectOption>
|
||||
<NativeSelectOption
|
||||
<Select
|
||||
:model-value="videoResolution"
|
||||
@update:model-value="value => videoResolution = value === EMPTY_SELECT_VALUE ? '' : String(value)"
|
||||
>
|
||||
<SelectTrigger id="videoResolution" class="w-full">
|
||||
<SelectValue :placeholder="t('setup.selectResolution')" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem :value="EMPTY_SELECT_VALUE">{{ t('setup.selectResolution') }}</SelectItem>
|
||||
<SelectItem
|
||||
v-for="res in availableResolutions"
|
||||
:key="`${res.width}x${res.height}`"
|
||||
:value="`${res.width}x${res.height}`"
|
||||
>
|
||||
{{ res.width }}x{{ res.height }}
|
||||
</NativeSelectOption>
|
||||
</NativeSelect>
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label for="videoFps">{{ t('setup.fps') }}</Label>
|
||||
<NativeSelect :model-value="videoFps" class="w-full" @update:model-value="value => videoFps = value === '' ? null : Number(value)">
|
||||
<NativeSelectOption value="">{{ t('setup.selectFps') }}</NativeSelectOption>
|
||||
<NativeSelectOption v-for="fps in availableFps" :key="fps" :value="fps">
|
||||
<Select :model-value="videoFps" @update:model-value="value => videoFps = value === EMPTY_SELECT_VALUE ? null : Number(value)">
|
||||
<SelectTrigger id="videoFps" class="w-full">
|
||||
<SelectValue :placeholder="t('setup.selectFps')" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem :value="EMPTY_SELECT_VALUE">{{ t('setup.selectFps') }}</SelectItem>
|
||||
<SelectItem v-for="fps in availableFps" :key="fps" :value="fps">
|
||||
{{ formatFpsLabel(fps) }}
|
||||
</NativeSelectOption>
|
||||
</NativeSelect>
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -784,11 +813,20 @@ const stepIcons = [User, Video, Keyboard, Puzzle]
|
||||
</HoverCardContent>
|
||||
</HoverCard>
|
||||
</div>
|
||||
<NativeSelect v-model="audioDevice" class="w-full" :disabled="!audioEnabled">
|
||||
<NativeSelectOption value="">{{ t('setup.selectAudioDevice') }}</NativeSelectOption>
|
||||
<NativeSelectOption value="__none__">{{ t('setup.noAudio') }}</NativeSelectOption>
|
||||
<NativeSelectOption v-for="dev in devices.audio" :key="dev.name" :value="dev.name">{{ dev.description }}{{ dev.is_hdmi ? ' (HDMI)' : '' }}</NativeSelectOption>
|
||||
</NativeSelect>
|
||||
<Select
|
||||
:model-value="audioDevice"
|
||||
:disabled="!audioEnabled"
|
||||
@update:model-value="value => audioDevice = value === EMPTY_SELECT_VALUE ? '' : String(value)"
|
||||
>
|
||||
<SelectTrigger id="audioDevice" class="w-full">
|
||||
<SelectValue :placeholder="t('setup.selectAudioDevice')" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem :value="EMPTY_SELECT_VALUE">{{ t('setup.selectAudioDevice') }}</SelectItem>
|
||||
<SelectItem value="__none__">{{ t('setup.noAudio') }}</SelectItem>
|
||||
<SelectItem v-for="dev in devices.audio" :key="dev.name" :value="dev.name">{{ dev.description }}{{ dev.is_hdmi ? ' (HDMI)' : '' }}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<p v-if="!devices.audio.length" class="text-xs text-muted-foreground">
|
||||
{{ t('setup.noAudioDevices') }}
|
||||
</p>
|
||||
@@ -859,12 +897,20 @@ const stepIcons = [User, Video, Keyboard, Puzzle]
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label for="ch9329Port">{{ t('setup.serialPort') }}</Label>
|
||||
<NativeSelect v-model="ch9329Port" class="w-full">
|
||||
<NativeSelectOption value="">{{ t('setup.selectSerialPort') }}</NativeSelectOption>
|
||||
<NativeSelectOption v-for="port in devices.serial" :key="port.path" :value="port.path">
|
||||
<Select
|
||||
:model-value="ch9329Port"
|
||||
@update:model-value="value => ch9329Port = value === EMPTY_SELECT_VALUE ? '' : String(value)"
|
||||
>
|
||||
<SelectTrigger id="ch9329Port" class="w-full">
|
||||
<SelectValue :placeholder="t('setup.selectSerialPort')" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem :value="EMPTY_SELECT_VALUE">{{ t('setup.selectSerialPort') }}</SelectItem>
|
||||
<SelectItem v-for="port in devices.serial" :key="port.path" :value="port.path">
|
||||
{{ port.name }} ({{ port.path }})
|
||||
</NativeSelectOption>
|
||||
</NativeSelect>
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<p v-if="!devices.serial.length" class="text-xs text-muted-foreground">
|
||||
{{ t('setup.noSerialDevices') }}
|
||||
</p>
|
||||
@@ -894,12 +940,20 @@ const stepIcons = [User, Video, Keyboard, Puzzle]
|
||||
|
||||
<div class="space-y-2">
|
||||
<Label for="otgUdc">{{ t('setup.udc') }}</Label>
|
||||
<NativeSelect v-model="otgUdc" class="w-full">
|
||||
<NativeSelectOption value="">{{ t('setup.selectUdc') }}</NativeSelectOption>
|
||||
<NativeSelectOption v-for="udc in devices.udc" :key="udc.name" :value="udc.name">
|
||||
<Select
|
||||
:model-value="otgUdc"
|
||||
@update:model-value="value => otgUdc = value === EMPTY_SELECT_VALUE ? '' : String(value)"
|
||||
>
|
||||
<SelectTrigger id="otgUdc" class="w-full">
|
||||
<SelectValue :placeholder="t('setup.selectUdc')" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem :value="EMPTY_SELECT_VALUE">{{ t('setup.selectUdc') }}</SelectItem>
|
||||
<SelectItem v-for="udc in devices.udc" :key="udc.name" :value="udc.name">
|
||||
{{ udc.name }}
|
||||
</NativeSelectOption>
|
||||
</NativeSelect>
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<p v-if="!devices.udc.length" class="text-xs text-muted-foreground">
|
||||
{{ t('setup.noUdcDevices') }}
|
||||
</p>
|
||||
|
||||
Reference in New Issue
Block a user