fix: 完善 MSD 错误提示;解决 MSD 文件名称过长问题;优化粘贴文本样式;优化所有下拉框样式;删除“画面静止时会自动降帧”提示

This commit is contained in:
mofeng-git
2026-07-26 23:13:47 +08:00
parent 376dc97134
commit 4f770a1eea
8 changed files with 284 additions and 177 deletions

View File

@@ -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
<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 }}
</NativeSelectOption>
</NativeSelect>
</SelectItem>
</SelectContent>
</Select>
</div>
<div class="space-y-2">

View File

@@ -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
<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 }}
</NativeSelectOption>
</NativeSelect>
</SelectItem>
</SelectContent>
</Select>
</div>
<Button

View File

@@ -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>
<div class="space-y-2">
<Label for="paste-text">{{ t('paste.label') }}</Label>
<Textarea
id="paste-text"
ref="textareaRef"
v-model="text"
:aria-label="t('paste.title')"
:placeholder="t('paste.placeholder')"
class="min-h-[120px] resize-none font-mono text-sm"
class="min-h-[120px] resize-none text-sm"
:disabled="isPasting"
@keydown="handleKeydown"
@keyup.stop
/>
</div>
<!-- Warning for untypable characters -->
<Alert v-if="hasUntypableChars && !isPasting" variant="warning">

View File

@@ -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',

View File

@@ -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: '当前模式',

View File

@@ -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
})

View File

@@ -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
<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') : '' }}
</NativeSelectOption>
</NativeSelect>
</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"
@update:model-value="value => config.otg_network_interface = value === EMPTY_SELECT_VALUE ? '' : String(value)"
>
<SelectTrigger
id="otg-network-interface"
class="w-full"
:aria-invalid="Boolean(otgNetworkInterfacesError)"
>
<NativeSelectOption
<SelectValue :placeholder="otgNetworkInterfacesError || t('settings.otgNetworkNone')" />
</SelectTrigger>
<SelectContent>
<SelectItem
v-if="otgNetworkInterfacesError"
:value="config.otg_network_interface"
:value="config.otg_network_interface || EMPTY_SELECT_VALUE"
>
{{ otgNetworkInterfacesError }}
</NativeSelectOption>
<NativeSelectOption v-else-if="otgNetworkInterfaces.length === 0" value="">
</SelectItem>
<SelectItem v-else-if="otgNetworkInterfaces.length === 0" :value="EMPTY_SELECT_VALUE">
{{ t('settings.otgNetworkNone') }}
</NativeSelectOption>
<NativeSelectOption
</SelectItem>
<SelectItem
v-for="item in otgNetworkInterfaces"
:key="item.name"
:value="item.name"
>
{{ item.name }} · {{ item.interface_type }}
</NativeSelectOption>
</NativeSelect>
</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
<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) }}
</NativeSelectOption>
</NativeSelect>
</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>

View File

@@ -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>