fix(web): 优化 HID 兼容配置入口并调整引导布局

This commit is contained in:
mofeng-git
2026-09-26 09:28:21 +08:00
parent 0a041c2caa
commit 03cff9d9a8
7 changed files with 183 additions and 82 deletions

View File

@@ -13,10 +13,10 @@ import {
PopoverContent, PopoverContent,
PopoverTrigger, PopoverTrigger,
} from '@/components/ui/popover' } from '@/components/ui/popover'
import { MousePointer, Move } from 'lucide-vue-next' import { MousePointer, Move, Loader2 } from 'lucide-vue-next'
import HelpTooltip from '@/components/HelpTooltip.vue' import HelpTooltip from '@/components/HelpTooltip.vue'
import { useConfigStore } from '@/stores/config' import { useConfigStore } from '@/stores/config'
import { HidBackend } from '@/types/generated' import { HidBackend, OtgHidProfile } from '@/types/generated'
const props = defineProps<{ const props = defineProps<{
open: boolean open: boolean
@@ -64,18 +64,63 @@ watch(showCursor, (newValue, oldValue) => {
}) })
const buttonText = computed(() => t('actionbar.hidConfig')) const buttonText = computed(() => t('actionbar.hidConfig'))
const hidSaving = ref(false)
const hidControlsDisabled = computed(() => hidSaving.value || configStore.hidLoading || !configStore.hid)
const supportsLinuxCompatibility = computed(() => configStore.hid?.backend === HidBackend.Ch9329)
const supportsMacosCompatibility = computed(() =>
supportsLinuxCompatibility.value || configStore.hid?.backend === HidBackend.Otg
)
const macosDragRequiresInterfaces = computed(() => {
const hid = configStore.hid
if (hid?.backend !== HidBackend.Otg) return false
switch (hid.otg_profile) {
case OtgHidProfile.LegacyKeyboard:
case OtgHidProfile.LegacyMouseRelative:
return true
case OtgHidProfile.Custom:
return hid.otg_functions?.mouse_relative === false || hid.otg_functions?.mouse_absolute === false
default:
return false
}
})
type MouseCompatibilityOption = 'ch9329_hybrid_mouse' | 'mouse_macos_drag'
async function updateMouseCompatibility(option: MouseCompatibilityOption, enabled: boolean) {
if (hidControlsDisabled.value || !supportsMacosCompatibility.value) return
if (option === 'ch9329_hybrid_mouse' && !supportsLinuxCompatibility.value) return
if (option === 'mouse_macos_drag' && enabled && macosDragRequiresInterfaces.value) return
if ((configStore.hid?.[option] ?? false) === enabled) return
hidSaving.value = true
try {
// Keep both preferences: the backend gives macOS priority when both are enabled.
await configStore.updateHid({ [option]: enabled })
} catch (error) {
toast.error(t('config.updateFailed'), {
description: error instanceof Error ? error.message : undefined,
})
// Read back persisted state, including when the response was lost after applying.
await configStore.refreshHid().catch(() => undefined)
} finally {
hidSaving.value = false
}
}
function toggleMouseMode() { function toggleMouseMode() {
if (configStore.hid?.backend === HidBackend.Bluetooth) return if (hidControlsDisabled.value || configStore.hid?.backend === HidBackend.Bluetooth) return
const newMode = props.mouseMode === 'absolute' ? 'relative' : 'absolute' const newMode = props.mouseMode === 'absolute' ? 'relative' : 'absolute'
emit('update:mouseMode', newMode) emit('update:mouseMode', newMode)
// Update backend config // Update backend config
hidSaving.value = true
configStore.updateHid({ configStore.updateHid({
mouse_absolute: newMode === 'absolute', mouse_absolute: newMode === 'absolute',
}).catch(_e => { }).catch(_e => {
console.info('[HidConfig] Failed to update mouse mode') console.info('[HidConfig] Failed to update mouse mode')
toast.error(t('config.updateFailed')) toast.error(t('config.updateFailed'))
}).finally(() => {
hidSaving.value = false
}) })
} }
@@ -93,7 +138,7 @@ watch(() => props.open, (open) => {
if (!open) return if (!open) return
mouseThrottle.value = loadMouseMoveSendIntervalFromStorage() mouseThrottle.value = loadMouseMoveSendIntervalFromStorage()
showCursor.value = localStorage.getItem('hidShowCursor') !== 'false' showCursor.value = localStorage.getItem('hidShowCursor') !== 'false'
void configStore.refreshHid().catch(() => undefined) if (!hidSaving.value) void configStore.refreshHid().catch(() => undefined)
}) })
</script> </script>
@@ -136,7 +181,7 @@ watch(() => props.open, (open) => {
<div class="flex gap-2"> <div class="flex gap-2">
<Button <Button
:variant="mouseMode === 'absolute' ? 'default' : 'outline'" :variant="mouseMode === 'absolute' ? 'default' : 'outline'"
:disabled="configStore.hid?.backend === HidBackend.Bluetooth" :disabled="hidControlsDisabled || configStore.hid?.backend === HidBackend.Bluetooth"
size="sm" size="sm"
class="flex-1 h-8 text-xs" class="flex-1 h-8 text-xs"
@click="toggleMouseMode" @click="toggleMouseMode"
@@ -146,6 +191,7 @@ watch(() => props.open, (open) => {
</Button> </Button>
<Button <Button
:variant="mouseMode === 'relative' ? 'default' : 'outline'" :variant="mouseMode === 'relative' ? 'default' : 'outline'"
:disabled="hidControlsDisabled || configStore.hid?.backend === HidBackend.Bluetooth"
size="sm" size="sm"
class="flex-1 h-8 text-xs" class="flex-1 h-8 text-xs"
@click="toggleMouseMode" @click="toggleMouseMode"
@@ -185,6 +231,45 @@ watch(() => props.open, (open) => {
<Switch v-model="showCursor" /> <Switch v-model="showCursor" />
</div> </div>
</div> </div>
<template v-if="supportsMacosCompatibility">
<Separator />
<div class="space-y-3" :aria-busy="hidSaving">
<h5 class="text-xs font-medium text-muted-foreground">{{ t('actionbar.mouseCompatibility') }}</h5>
<div v-if="supportsLinuxCompatibility" class="flex items-center justify-between gap-3">
<div class="flex items-center gap-1">
<Label for="hid-linux-compatibility" class="text-xs">{{ t('settings.ch9329HybridMouse') }}</Label>
<HelpTooltip :content="t('settings.ch9329HybridMouseDesc')" icon-size="sm" />
</div>
<Switch
id="hid-linux-compatibility"
:model-value="configStore.hid?.ch9329_hybrid_mouse ?? false"
:disabled="hidControlsDisabled"
@update:model-value="updateMouseCompatibility('ch9329_hybrid_mouse', $event)"
/>
</div>
<div class="flex items-center justify-between gap-3">
<div class="flex items-center gap-1">
<Label for="hid-macos-compatibility" class="text-xs">{{ t('settings.mouseMacosDrag') }}</Label>
<HelpTooltip :content="t('settings.mouseMacosDragDesc')" icon-size="sm" />
</div>
<Switch
id="hid-macos-compatibility"
:model-value="configStore.hid?.mouse_macos_drag ?? false"
:disabled="hidControlsDisabled || (macosDragRequiresInterfaces && !configStore.hid?.mouse_macos_drag)"
@update:model-value="updateMouseCompatibility('mouse_macos_drag', $event)"
/>
</div>
<p v-if="macosDragRequiresInterfaces" class="text-xs text-warning">{{ t('actionbar.macosDragRequiresBoth') }}</p>
<p v-if="hidSaving" role="status" class="flex items-center gap-1.5 text-xs text-muted-foreground">
<Loader2 class="size-3 shrink-0 animate-spin" />
{{ t('actionbar.applying') }}
</p>
</div>
</template>
</div> </div>
</PopoverContent> </PopoverContent>
</Popover> </Popover>

View File

@@ -3,6 +3,7 @@ import { computed, ref, watch, onUnmounted } from 'vue'
import { useI18n } from 'vue-i18n' import { useI18n } from 'vue-i18n'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { Input } from '@/components/ui/input' import { Input } from '@/components/ui/input'
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
import { HoverCard, HoverCardContent, HoverCardTrigger } from '@/components/ui/hover-card' import { HoverCard, HoverCardContent, HoverCardTrigger } from '@/components/ui/hover-card'
import { Cable, RefreshCw } from 'lucide-vue-next' import { Cable, RefreshCw } from 'lucide-vue-next'
import { configApi } from '@/api' import { configApi } from '@/api'
@@ -13,6 +14,7 @@ import HidWiringDiagram from './HidWiringDiagram.vue'
const props = defineProps<{ modelValue: HidSelection; locked?: boolean }>() const props = defineProps<{ modelValue: HidSelection; locked?: boolean }>()
const emit = defineEmits<{ 'update:modelValue': [HidSelection]; valid: [boolean] }>() const emit = defineEmits<{ 'update:modelValue': [HidSelection]; valid: [boolean] }>()
const { t } = useI18n() const { t } = useI18n()
const EMPTY_SELECT_VALUE = '__one-kvm-empty-select-value__'
const options = ref<Array<{ value: string; label: string }>>([]) const options = ref<Array<{ value: string; label: string }>>([])
const loading = ref(false), error = ref(''), missing = ref('') const loading = ref(false), error = ref(''), missing = ref('')
let generation = 0 let generation = 0
@@ -83,23 +85,54 @@ onUnmounted(() => generation++)
</Button> </Button>
</div> </div>
</div> </div>
<select id="hid-driver" class="w-full rounded-md border bg-background px-3 py-2" :value="modelValue.backend" @change="emit('update:modelValue', { ...modelValue, backend: ($event.target as HTMLSelectElement).value as Driver })"> <Select
<option v-for="driver in drivers" :key="driver" :value="driver">{{ t(`hidGuide.driver_${driver}`) }}</option> :model-value="modelValue.backend"
</select> @update:model-value="value => emit('update:modelValue', { ...modelValue, backend: String(value) as Driver })"
>
<SelectTrigger id="hid-driver" class="w-full">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem v-for="driver in drivers" :key="driver" :value="driver">
{{ t(`hidGuide.driver_${driver}`) }}
</SelectItem>
</SelectContent>
</Select>
</div> </div>
<p v-if="modelValue.backend === 'none'" class="text-sm text-muted-foreground">{{ t('hidGuide.disabledHelp') }}</p> <p v-if="modelValue.backend === 'none'" class="text-sm text-muted-foreground">{{ t('hidGuide.disabledHelp') }}</p>
<label v-if="modelValue.backend !== 'none'" class="block space-y-1 text-sm"> <label v-if="modelValue.backend !== 'none'" class="block space-y-1 text-sm">
<span>{{ t(`hidGuide.device_${modelValue.backend}`) }}</span> <span>{{ t(`hidGuide.device_${modelValue.backend}`) }}</span>
<select class="w-full rounded-md border bg-background px-3 py-2" :value="selected" :disabled="loading" @change="device(($event.target as HTMLSelectElement).value)"> <Select
<option value="" disabled>{{ t('hidGuide.selectDevice') }}</option> :model-value="selected || EMPTY_SELECT_VALUE"
<option v-for="option in options" :key="option.value" :value="option.value">{{ option.label }}</option> :disabled="loading"
</select> @update:model-value="value => device(value === EMPTY_SELECT_VALUE ? '' : String(value))"
>
<SelectTrigger class="w-full">
<SelectValue :placeholder="t('hidGuide.selectDevice')" />
</SelectTrigger>
<SelectContent>
<SelectItem :value="EMPTY_SELECT_VALUE" disabled>{{ t('hidGuide.selectDevice') }}</SelectItem>
<SelectItem v-for="option in options" :key="option.value" :value="option.value">
{{ option.label }}
</SelectItem>
</SelectContent>
</Select>
</label> </label>
<label v-if="modelValue.backend === 'ch9329'" class="block space-y-1 text-sm"> <label v-if="modelValue.backend === 'ch9329'" class="block space-y-1 text-sm">
<span>{{ t('actionbar.baudrate') }}</span> <span>{{ t('actionbar.baudrate') }}</span>
<select class="w-full rounded-md border bg-background px-3 py-2" :value="modelValue.ch9329_baudrate" @change="emit('update:modelValue', { ...modelValue, ch9329_baudrate: Number(($event.target as HTMLSelectElement).value) })"> <Select
<option v-for="rate in [9600, 19200, 38400, 57600, 115200]" :key="rate">{{ rate }}</option> :model-value="String(modelValue.ch9329_baudrate)"
</select> @update:model-value="value => emit('update:modelValue', { ...modelValue, ch9329_baudrate: Number(value) })"
>
<SelectTrigger class="w-full">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem v-for="rate in [9600, 19200, 38400, 57600, 115200]" :key="rate" :value="String(rate)">
{{ rate }}
</SelectItem>
</SelectContent>
</Select>
</label> </label>
<label v-if="modelValue.backend === 'bluetooth'" class="block space-y-1 text-sm"> <label v-if="modelValue.backend === 'bluetooth'" class="block space-y-1 text-sm">
<span>{{ t('bluetoothHid.name') }}</span> <span>{{ t('bluetoothHid.name') }}</span>

View File

@@ -16,6 +16,7 @@ const props = defineProps<{
resolution: string resolution: string
fps: number | null fps: number | null
compact?: boolean compact?: boolean
resolutionFpsInline?: boolean
refreshing?: boolean refreshing?: boolean
}>() }>()
@@ -98,32 +99,34 @@ const { t } = useI18n()
</Select> </Select>
</div> </div>
<div class="space-y-2"> <div :class="resolutionFpsInline ? 'grid grid-cols-2 gap-3' : 'space-y-4'">
<Label :class="compact ? 'text-xs text-muted-foreground' : undefined">{{ t('videoInput.resolution') }}</Label> <div class="space-y-2">
<Select :model-value="resolution" @update:model-value="value => emit('update:resolution', String(value))"> <Label :class="compact ? 'text-xs text-muted-foreground' : undefined">{{ t('videoInput.resolution') }}</Label>
<SelectTrigger :size="compact ? 'sm' : 'default'" class="w-full" :class="compact ? 'text-xs' : undefined"> <Select :model-value="resolution" @update:model-value="value => emit('update:resolution', String(value))">
<SelectValue :placeholder="t('videoInput.selectResolution')" /> <SelectTrigger :size="compact ? 'sm' : 'default'" class="w-full" :class="compact ? 'text-xs' : undefined">
</SelectTrigger> <SelectValue :placeholder="t('videoInput.selectResolution')" />
<SelectContent> </SelectTrigger>
<SelectItem v-for="item in resolutions" :key="`${item.width}x${item.height}`" :value="`${item.width}x${item.height}`"> <SelectContent>
{{ item.width }}x{{ item.height }} <SelectItem v-for="item in resolutions" :key="`${item.width}x${item.height}`" :value="`${item.width}x${item.height}`">
</SelectItem> {{ item.width }}x{{ item.height }}
</SelectContent> </SelectItem>
</Select> </SelectContent>
</div> </Select>
</div>
<div class="space-y-2"> <div class="space-y-2">
<Label :class="compact ? 'text-xs text-muted-foreground' : undefined">{{ t('videoInput.frameRate') }}</Label> <Label :class="compact ? 'text-xs text-muted-foreground' : undefined">{{ t('videoInput.frameRate') }}</Label>
<Select :model-value="fps === null ? '' : String(fps)" @update:model-value="value => emit('update:fps', Number(value))"> <Select :model-value="fps === null ? '' : String(fps)" @update:model-value="value => emit('update:fps', Number(value))">
<SelectTrigger :size="compact ? 'sm' : 'default'" class="w-full" :class="compact ? 'text-xs' : undefined"> <SelectTrigger :size="compact ? 'sm' : 'default'" class="w-full" :class="compact ? 'text-xs' : undefined">
<SelectValue :placeholder="t('videoInput.selectFps')" /> <SelectValue :placeholder="t('videoInput.selectFps')" />
</SelectTrigger> </SelectTrigger>
<SelectContent> <SelectContent>
<SelectItem v-for="item in fpsOptions" :key="item" :value="String(item)"> <SelectItem v-for="item in fpsOptions" :key="item" :value="String(item)">
{{ formatFpsLabel(item) }} {{ formatFpsLabel(item) }}
</SelectItem> </SelectItem>
</SelectContent> </SelectContent>
</Select> </Select>
</div>
</div> </div>
</template> </template>
</template> </template>

View File

@@ -259,6 +259,8 @@ export default {
multiSourceVideoParamsWarning: '{sources} are enabled. Changing video device and input parameters will interrupt the stream.', multiSourceVideoParamsWarning: '{sources} are enabled. Changing video device and input parameters will interrupt the stream.',
hidConfig: 'HID', hidConfig: 'HID',
mouseSettings: 'Mouse Settings', mouseSettings: 'Mouse Settings',
mouseCompatibility: 'Mouse Compatibility',
macosDragRequiresBoth: 'Enable and save both relative and absolute mouse interfaces in Settings → HID Settings first.',
hidDeviceSettings: 'HID Device Settings', hidDeviceSettings: 'HID Device Settings',
positioningMode: 'Positioning Mode', positioningMode: 'Positioning Mode',
sendInterval: 'Send Interval', sendInterval: 'Send Interval',
@@ -821,8 +823,8 @@ export default {
ch9329HybridMouse: 'Linux Absolute Mouse Compatibility', ch9329HybridMouse: 'Linux Absolute Mouse Compatibility',
ch9329HybridMouseDesc: 'Keep absolute movement on absolute packets, but send buttons and wheel through relative packets', ch9329HybridMouseDesc: 'Keep absolute movement on absolute packets, but send buttons and wheel through relative packets',
mouseMacosDrag: 'macOS Drag Compatibility', mouseMacosDrag: 'macOS Drag Compatibility',
mouseMacosDragRequiresBoth: 'Enable both relative and absolute mouse interfaces before saving.', mouseMacosDragRequiresBoth: 'macOS drag compatibility requires both relative and absolute mouse interfaces. Keep both enabled, or turn off this mode in the console HID menu first.',
mouseMacosDragDesc: 'Experimental workaround for interrupted drags in absolute mouse mode (OTG / CH9329). OTG requires both mouse interfaces. Tracking speed and drop position may vary with macOS settings; this version has not been tested on a Mac.', mouseMacosDragDesc: 'Experimental workaround for interrupted drags in macOS absolute mouse mode',
ch9329Descriptor: 'CH9329 USB Device Descriptor', ch9329Descriptor: 'CH9329 USB Device Descriptor',
ch9329DescriptorDesc: 'Read USB identification fields from the CH9329 chip before editing', ch9329DescriptorDesc: 'Read USB identification fields from the CH9329 chip before editing',
ch9329DescriptorLoading: 'Reading CH9329 descriptor...', ch9329DescriptorLoading: 'Reading CH9329 descriptor...',

View File

@@ -259,6 +259,8 @@ export default {
multiSourceVideoParamsWarning: '{sources} 已启用,修改视频设备和输入参数将导致流中断', multiSourceVideoParamsWarning: '{sources} 已启用,修改视频设备和输入参数将导致流中断',
hidConfig: '鼠键配置', hidConfig: '鼠键配置',
mouseSettings: '鼠标设置', mouseSettings: '鼠标设置',
mouseCompatibility: '鼠标兼容性',
macosDragRequiresBoth: '请先在系统设置 → HID 设置中启用并保存相对鼠标和绝对鼠标接口。',
hidDeviceSettings: 'HID 设备设置', hidDeviceSettings: 'HID 设备设置',
positioningMode: '定位模式', positioningMode: '定位模式',
sendInterval: '发送间隔', sendInterval: '发送间隔',
@@ -820,8 +822,8 @@ export default {
ch9329HybridMouse: 'Linux 绝对鼠标兼容模式', ch9329HybridMouse: 'Linux 绝对鼠标兼容模式',
ch9329HybridMouseDesc: '绝对移动仍使用绝对鼠标包,点击和滚轮改用相对鼠标包发送', ch9329HybridMouseDesc: '绝对移动仍使用绝对鼠标包,点击和滚轮改用相对鼠标包发送',
mouseMacosDrag: 'macOS 拖拽兼容模式', mouseMacosDrag: 'macOS 拖拽兼容模式',
mouseMacosDragRequiresBoth: '请同时启用相对鼠标和绝对鼠标接口后保存。', mouseMacosDragRequiresBoth: 'macOS 拖拽兼容模式需要同时启用相对鼠标和绝对鼠标接口。请保留两个接口,或先在控制台鼠键配置中关闭该模式。',
mouseMacosDragDesc: '实验性缓解绝对鼠标模式下的拖拽中断,适用于 OTG / CH9329。OTG 需同时启用两种鼠标接口。移动速度和落点可能受 macOS 设置影响;本版本尚未进行 Mac 实机验证。', mouseMacosDragDesc: '实验性缓解 macOS 绝对鼠标模式下的拖拽中断',
ch9329Descriptor: 'CH9329 USB 设备描述符', ch9329Descriptor: 'CH9329 USB 设备描述符',
ch9329DescriptorDesc: '先从 CH9329 芯片读取 USB 标识信息,读取成功后再修改', ch9329DescriptorDesc: '先从 CH9329 芯片读取 USB 标识信息,读取成功后再修改',
ch9329DescriptorLoading: '正在读取 CH9329 描述符...', ch9329DescriptorLoading: '正在读取 CH9329 描述符...',

View File

@@ -691,8 +691,6 @@ const config = ref({
consumer: true, consumer: true,
} as OtgHidFunctions, } as OtgHidFunctions,
hid_otg_keyboard_leds: false, hid_otg_keyboard_leds: false,
hid_ch9329_hybrid_mouse: false,
hid_mouse_macos_drag: false,
msd_enabled: false, msd_enabled: false,
msd_dir: '', msd_dir: '',
msd_flash_inquiry_string: 'One-KVM Virtual Flash', msd_flash_inquiry_string: 'One-KVM Virtual Flash',
@@ -1175,10 +1173,14 @@ const isCh9329DescriptorDirty = computed(() => {
|| current.serialNumber !== baseline.serialNumber || current.serialNumber !== baseline.serialNumber
}) })
const hasMacosDragInterfaceConflict = computed(() =>
config.value.hid_backend === 'otg' && configStore.hid?.mouse_macos_drag
&& (!effectiveOtgFunctions.value.mouse_relative || !effectiveOtgFunctions.value.mouse_absolute)
)
const isHidSettingsValid = computed(() => const isHidSettingsValid = computed(() =>
isHidFunctionSelectionValid.value isHidFunctionSelectionValid.value
&& !(config.value.hid_backend === 'otg' && config.value.hid_mouse_macos_drag && !hasMacosDragInterfaceConflict.value
&& (!effectiveOtgFunctions.value.mouse_relative || !effectiveOtgFunctions.value.mouse_absolute))
&& isCh9329DescriptorValid.value && isCh9329DescriptorValid.value
&& areMsdInquiryStringsValid.value && areMsdInquiryStringsValid.value
) )
@@ -1456,13 +1458,7 @@ async function saveConfig() {
if (!isHidSettingsValid.value) { if (!isHidSettingsValid.value) {
return return
} }
const hidUpdate: HidConfigUpdate = configStore.hid?.backend === 'ch9329' const hidUpdate: HidConfigUpdate = {}
? {
ch9329_hybrid_mouse: config.value.hid_ch9329_hybrid_mouse,
} : {}
if (['otg', 'ch9329'].includes(config.value.hid_backend)) {
hidUpdate.mouse_macos_drag = config.value.hid_mouse_macos_drag
}
if (config.value.hid_backend === 'ch9329' && isCh9329DescriptorDirty.value) { if (config.value.hid_backend === 'ch9329' && isCh9329DescriptorDirty.value) {
hidUpdate.ch9329_descriptor = { hidUpdate.ch9329_descriptor = {
vendor_id: parseInt(ch9329VendorIdHex.value, 16) || 0x1a86, vendor_id: parseInt(ch9329VendorIdHex.value, 16) || 0x1a86,
@@ -1532,7 +1528,7 @@ async function saveConfig() {
const hidFeatureBaseline = ref('') const hidFeatureBaseline = ref('')
function hidFeatureSnapshot() { function hidFeatureSnapshot() {
return JSON.stringify({ return JSON.stringify({
fields: Object.fromEntries(Object.entries(config.value).filter(([key]) => key.startsWith('msd_') || key.startsWith('otg_network_') || key.startsWith('uac_') || ['hid_otg_functions', 'hid_otg_keyboard_leds', 'hid_ch9329_hybrid_mouse', 'hid_mouse_macos_drag'].includes(key))), fields: Object.fromEntries(Object.entries(config.value).filter(([key]) => key.startsWith('msd_') || key.startsWith('otg_network_') || key.startsWith('uac_') || ['hid_otg_functions', 'hid_otg_keyboard_leds'].includes(key))),
descriptor: [otgVendorIdHex.value, otgProductIdHex.value, otgManufacturer.value, otgProduct.value, otgSerialNumber.value], descriptor: [otgVendorIdHex.value, otgProductIdHex.value, otgManufacturer.value, otgProduct.value, otgSerialNumber.value],
}) })
} }
@@ -1573,8 +1569,6 @@ async function loadConfig() {
consumer: hid.otg_functions?.consumer ?? true, consumer: hid.otg_functions?.consumer ?? true,
} as OtgHidFunctions, } as OtgHidFunctions,
hid_otg_keyboard_leds: hid.otg_keyboard_leds ?? false, hid_otg_keyboard_leds: hid.otg_keyboard_leds ?? false,
hid_ch9329_hybrid_mouse: hid.ch9329_hybrid_mouse ?? false,
hid_mouse_macos_drag: hid.mouse_macos_drag ?? false,
msd_enabled: msd.enabled || false, msd_enabled: msd.enabled || false,
msd_dir: msd.msd_dir || '', msd_dir: msd.msd_dir || '',
msd_flash_inquiry_string: msd.flash_inquiry_string || 'One-KVM Virtual Flash', msd_flash_inquiry_string: msd.flash_inquiry_string || 'One-KVM Virtual Flash',
@@ -3215,33 +3209,9 @@ watch(isWindows, () => {
{{ t('settings.ch9329DescriptorWarning') }} {{ t('settings.ch9329DescriptorWarning') }}
</p> </p>
</div> </div>
<Separator class="my-4" />
<div class="space-y-4">
<div>
<h4 class="text-sm font-medium">{{ t('settings.ch9329Options') }}</h4>
<p class="text-sm text-muted-foreground">{{ t('settings.ch9329OptionsDesc') }}</p>
</div>
<div class="space-y-3 rounded-md border border-border/60 p-3">
<div class="flex items-center justify-between gap-4">
<div>
<Label>{{ t('settings.ch9329HybridMouse') }}</Label>
<p class="text-xs text-muted-foreground">{{ t('settings.ch9329HybridMouseDesc') }}</p>
</div>
<Switch v-model="config.hid_ch9329_hybrid_mouse" />
</div>
</div>
</div>
</template> </template>
<!-- OTG Descriptor Settings --> <!-- OTG Descriptor Settings -->
<div v-if="['otg', 'ch9329'].includes(config.hid_backend)" class="flex items-center justify-between gap-4 rounded-md border border-border/60 p-3">
<div>
<Label>{{ t('settings.mouseMacosDrag') }}</Label>
<p class="text-xs text-muted-foreground">{{ t('settings.mouseMacosDragDesc') }}</p>
<p v-if="config.hid_backend === 'otg' && config.hid_mouse_macos_drag && (!effectiveOtgFunctions.mouse_relative || !effectiveOtgFunctions.mouse_absolute)" class="text-xs text-warning">{{ t('settings.mouseMacosDragRequiresBoth') }}</p>
</div>
<Switch v-model="config.hid_mouse_macos_drag" />
</div>
<template v-if="config.hid_backend === 'otg'"> <template v-if="config.hid_backend === 'otg'">
<Separator class="my-4" /> <Separator class="my-4" />
<div class="space-y-4"> <div class="space-y-4">
@@ -3344,6 +3314,7 @@ watch(isWindows, () => {
</div> </div>
<Switch v-model="config.hid_otg_functions.mouse_absolute" /> <Switch v-model="config.hid_otg_functions.mouse_absolute" />
</div> </div>
<p v-if="hasMacosDragInterfaceConflict" class="text-xs text-warning">{{ t('settings.mouseMacosDragRequiresBoth') }}</p>
<Separator /> <Separator />
<div class="flex items-center justify-between gap-4"> <div class="flex items-center justify-between gap-4">
<Label>{{ t('settings.uacMic') }}</Label> <Label>{{ t('settings.uacMic') }}</Label>
@@ -5414,6 +5385,10 @@ watch(isWindows, () => {
<AlertTriangle class="size-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' && hasMacosDragInterfaceConflict" class="flex min-w-0 items-center gap-1.5 text-xs text-warning">
<AlertTriangle class="size-3.5 shrink-0" />
<span>{{ t('settings.mouseMacosDragRequiresBoth') }}</span>
</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="size-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>

View File

@@ -603,6 +603,7 @@ const stepIcons = [User, Video, Keyboard, Puzzle]
:format="videoFormat" :format="videoFormat"
:resolution="videoResolution" :resolution="videoResolution"
:fps="videoFps" :fps="videoFps"
resolution-fps-inline
:refreshing="refreshingInputStatus" :refreshing="refreshingInputStatus"
@update:format="videoFormat = $event" @update:format="videoFormat = $event"
@update:resolution="videoResolution = $event" @update:resolution="videoResolution = $event"