mirror of
https://github.com/mofeng-git/One-KVM.git
synced 2026-10-28 16:30:48 +08:00
feat(hid): 支持 CH9329 + OTG 组合模式
CH9329 负责键鼠,OTG 提供虚拟媒体、USB 网络和音频;分别配置设备、报告运行状态,并在 OTG 配置变更和回滚时保留未变化的 CH9329 实例。 补齐双设备校验、串口冲突检查、初始化引导和功能入口,修复组合模式下鼠标兼容选项缺失。 验证:独立功能版本的 591 项 Rust 库测试、前端生产构建、3 项引导逻辑测试及 2 组浏览器回归通过。未进行目标 KVM 硬件验证。
This commit is contained in:
@@ -164,7 +164,7 @@ export const systemApi = {
|
||||
video_width?: number
|
||||
video_height?: number
|
||||
video_fps?: number
|
||||
hid_backend?: 'otg' | 'ch9329' | 'bluetooth' | 'none'
|
||||
hid_backend?: 'otg' | 'ch9329' | 'ch9329_otg' | 'bluetooth' | 'none'
|
||||
hid_bluetooth?: { adapter: string; name: string }
|
||||
hid_ch9329_port?: string
|
||||
hid_ch9329_baudrate?: number
|
||||
@@ -411,6 +411,8 @@ export const hidApi = {
|
||||
request<{
|
||||
available: boolean
|
||||
backend: string
|
||||
configured_backend: string
|
||||
otg?: import('@/lib/hidGuide').OtgDeviceStatus
|
||||
initialized: boolean
|
||||
online: boolean
|
||||
supports_absolute_mouse: boolean
|
||||
|
||||
@@ -58,11 +58,7 @@ const systemStore = useSystemStore()
|
||||
|
||||
const overflowMenuOpen = ref(false)
|
||||
|
||||
const hidBackend = computed(() => (systemStore.hid?.backend ?? '').toLowerCase())
|
||||
const isCh9329Backend = computed(() => hidBackend.value.includes('ch9329'))
|
||||
const showMsd = computed(() => {
|
||||
return !!systemStore.msd?.available && !isCh9329Backend.value
|
||||
})
|
||||
const showMsd = computed(() => !!systemStore.msd?.available)
|
||||
const props = defineProps<{
|
||||
layout?: ConsoleLayout
|
||||
mouseMode?: 'absolute' | 'relative'
|
||||
|
||||
@@ -17,6 +17,7 @@ import { MousePointer, Move, Loader2 } from 'lucide-vue-next'
|
||||
import HelpTooltip from '@/components/HelpTooltip.vue'
|
||||
import { useConfigStore } from '@/stores/config'
|
||||
import { HidBackend, OtgHidProfile } from '@/types/generated'
|
||||
import { usesCh9329 } from '@/lib/hidGuide'
|
||||
|
||||
const props = defineProps<{
|
||||
open: boolean
|
||||
@@ -66,7 +67,7 @@ watch(showCursor, (newValue, oldValue) => {
|
||||
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 supportsLinuxCompatibility = computed(() => usesCh9329(configStore.hid?.backend))
|
||||
const supportsMacosCompatibility = computed(() =>
|
||||
supportsLinuxCompatibility.value || configStore.hid?.backend === HidBackend.Otg
|
||||
)
|
||||
|
||||
@@ -3,7 +3,7 @@ import { computed } from 'vue'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import type { HidConfig } from '@/types/generated'
|
||||
import type { BluetoothStatus } from '@/types/bluetooth'
|
||||
import { hidDeviceError, hidDeviceStage, type HidDeviceStatus } from '@/lib/hidGuide'
|
||||
import { hidDeviceError, hidDeviceStage, otgDeviceStage, usesCh9329, supportsOtgFunctions, type HidDeviceStatus } from '@/lib/hidGuide'
|
||||
const props = defineProps<{ hid: HidConfig | null; status?: HidDeviceStatus | null; bluetooth?: BluetoothStatus | null; error?: string }>()
|
||||
const { t } = useI18n()
|
||||
const connectionError = computed(() => props.error || hidDeviceError(props.status) || props.bluetooth?.error)
|
||||
@@ -13,8 +13,8 @@ const stage = computed(() => hidDeviceStage(props.status, props.bluetooth ?? nul
|
||||
<div class="space-y-2 text-sm">
|
||||
<p class="font-medium">{{ !hid ? t('hidGuide.unconfigured') : hid.backend === 'none' ? t('hidGuide.disabled') : t(`hidGuide.driver_${hid.backend}`) }}</p>
|
||||
<dl v-if="hid && hid.backend !== 'none'" class="space-y-1 break-words">
|
||||
<div v-if="hid.backend === 'otg'"><dt class="inline text-muted-foreground">UDC: </dt><dd class="inline">{{ hid.otg_udc || status?.backend === 'otg' && t('hidGuide.legacyAuto') || '—' }}</dd></div>
|
||||
<template v-if="hid.backend === 'ch9329'">
|
||||
<div v-if="supportsOtgFunctions(hid.backend)"><dt class="inline text-muted-foreground">UDC: </dt><dd class="inline">{{ hid.otg_udc || status?.backend === 'otg' && t('hidGuide.legacyAuto') || '—' }}</dd></div>
|
||||
<template v-if="usesCh9329(hid.backend)">
|
||||
<div><dt class="inline text-muted-foreground">{{ t('hidGuide.device_ch9329') }}: </dt><dd class="inline">{{ hid.ch9329_port || '—' }}</dd></div>
|
||||
<div><dt class="inline text-muted-foreground">{{ t('actionbar.baudrate') }}: </dt><dd class="inline">{{ hid.ch9329_baudrate }}</dd></div>
|
||||
</template>
|
||||
@@ -25,9 +25,13 @@ const stage = computed(() => hidDeviceStage(props.status, props.bluetooth ?? nul
|
||||
</template>
|
||||
</dl>
|
||||
<p v-if="hid && hid.backend !== 'none'" role="status" :class="!connectionError && stage === 'ready' ? 'text-green-600 dark:text-green-400' : 'text-muted-foreground'">
|
||||
{{ t(`hidGuide.${stage}`) }}
|
||||
<span v-if="hid.backend === 'ch9329_otg'">{{ t('hidGuide.keyboardChannel') }}: </span>{{ t(`hidGuide.${stage}`) }}
|
||||
<span v-if="bluetooth?.pairing_seconds"> · {{ bluetooth.pairing_seconds }}s</span>
|
||||
</p>
|
||||
<template v-if="hid?.backend === 'ch9329_otg'">
|
||||
<p role="status" class="text-muted-foreground">{{ t('hidGuide.otgChannel') }}: {{ t(`hidGuide.${otgDeviceStage(status?.otg)}`) }}</p>
|
||||
<p v-if="status?.otg?.error" role="alert" class="text-destructive break-words">{{ status.otg.error }}</p>
|
||||
</template>
|
||||
<p v-if="connectionError" role="alert" class="text-destructive break-words">{{ connectionError }}</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -8,7 +8,7 @@ import { useSystemStore } from '@/stores/system'
|
||||
import { request } from '@/api/request'
|
||||
import type { HidConfig, MsdConfig } from '@/types/generated'
|
||||
import { useHidConnection } from '@/composables/useHidConnection'
|
||||
import { deviceRequest, matchesSelection, selectionFrom, type HidSelection } from '@/lib/hidGuide'
|
||||
import { deviceRequest, matchesSelection, selectionFrom, supportsOtgFunctions, type HidSelection } from '@/lib/hidGuide'
|
||||
import HidDriverForm from './HidDriverForm.vue'
|
||||
import HidWiringDiagram from './HidWiringDiagram.vue'
|
||||
import HidDeviceOverview from './HidDeviceOverview.vue'
|
||||
@@ -127,7 +127,7 @@ async function load() {
|
||||
if (!systemStore.platform) await systemStore.fetchSystemInfo()
|
||||
await refreshHid()
|
||||
draft.value = selectionFrom(store.hid)
|
||||
if (otgSupported.value && store.hid?.backend === 'otg') {
|
||||
if (otgSupported.value && supportsOtgFunctions(store.hid?.backend)) {
|
||||
const [msd, network, audio] = await Promise.all([refreshMsd(), readConfig<{ enabled: boolean }>('/config/otg-network'), readConfig<{ enabled: boolean }>('/config/uac')])
|
||||
disabledUsb.value = [msd.enabled ? t('hidGuide.msd') : '', network.enabled ? t('hidGuide.network') : '', audio.enabled ? t('hidGuide.audio') : ''].filter(Boolean)
|
||||
}
|
||||
@@ -162,7 +162,7 @@ onMounted(load)
|
||||
</template>
|
||||
<p v-if="connectionTimedOut && !ready && draft.backend !== 'bluetooth'" class="text-sm text-warning">{{ t('hidGuide.connectionTimeout') }}</p>
|
||||
</template>
|
||||
<p v-if="!applied && store.hid?.backend === 'otg' && draft.backend !== 'otg' && disabledUsb.length" class="text-sm text-warning">{{ t('hidGuide.disableUsb', { functions: disabledUsb.join('、') }) }}</p>
|
||||
<p v-if="!applied && supportsOtgFunctions(store.hid?.backend) && !supportsOtgFunctions(draft.backend) && disabledUsb.length" class="text-sm text-warning">{{ t('hidGuide.disableUsb', { functions: disabledUsb.join('、') }) }}</p>
|
||||
<p v-if="!applied && draft.backend === 'bluetooth'" class="text-sm text-warning">{{ t('hidGuide.resetWarning') }}</p>
|
||||
<p v-if="error" role="alert" class="text-sm text-destructive break-words">{{ error }}</p>
|
||||
<DialogFooter class="gap-2">
|
||||
|
||||
@@ -9,57 +9,82 @@ import { Cable, RefreshCw } from 'lucide-vue-next'
|
||||
import { configApi, type PlatformCapabilities } from '@/api'
|
||||
import { request } from '@/api/request'
|
||||
import type { BluetoothAdapter } from '@/types/bluetooth'
|
||||
import { selectDevice, validName, type HidSelection, type Driver } from '@/lib/hidGuide'
|
||||
import { selectDevice, validName, usesCh9329, supportsOtgFunctions, type HidSelection, type Driver } from '@/lib/hidGuide'
|
||||
import HidWiringDiagram from './HidWiringDiagram.vue'
|
||||
const props = defineProps<{ modelValue: HidSelection; platform: PlatformCapabilities | null; locked?: boolean }>()
|
||||
const props = withDefaults(defineProps<{ modelValue: HidSelection; platform: PlatformCapabilities | null; locked?: boolean; deviceOnly?: boolean; idPrefix?: string }>(), { idPrefix: 'hid' })
|
||||
const emit = defineEmits<{ 'update:modelValue': [HidSelection]; valid: [boolean] }>()
|
||||
const { t } = useI18n()
|
||||
const EMPTY_SELECT_VALUE = '__one-kvm-empty-select-value__'
|
||||
const options = ref<Array<{ value: string; label: string }>>([])
|
||||
const loading = ref(false), error = ref(''), missing = ref('')
|
||||
type DeviceField = 'serial' | 'udc' | 'bluetooth'
|
||||
type DeviceOption = { value: string; label: string }
|
||||
const options = ref<Record<DeviceField, DeviceOption[]>>({ serial: [], udc: [], bluetooth: [] })
|
||||
const loading = ref(false), error = ref('')
|
||||
let generation = 0
|
||||
const drivers = computed(() => (['otg', 'ch9329', 'bluetooth', 'none'] as Driver[])
|
||||
const drivers = computed(() => (['otg', 'ch9329', 'ch9329_otg', 'bluetooth', 'none'] as Driver[])
|
||||
.filter(driver => props.platform?.hid.backends.includes(driver)))
|
||||
const selected = computed(() => props.modelValue.backend === 'otg' ? props.modelValue.otg_udc : props.modelValue.backend === 'ch9329' ? props.modelValue.ch9329_port : props.modelValue.bluetooth.adapter)
|
||||
function device(value: string) {
|
||||
const fields = computed<DeviceField[]>(() => {
|
||||
if (props.modelValue.backend === 'bluetooth') return ['bluetooth']
|
||||
const result: DeviceField[] = []
|
||||
if (usesCh9329(props.modelValue.backend)) result.push('serial')
|
||||
if (supportsOtgFunctions(props.modelValue.backend)) result.push('udc')
|
||||
return result
|
||||
})
|
||||
function selected(field: DeviceField): string {
|
||||
return field === 'serial' ? props.modelValue.ch9329_port : field === 'udc' ? props.modelValue.otg_udc : props.modelValue.bluetooth.adapter
|
||||
}
|
||||
function deviceLabel(field: DeviceField): string {
|
||||
return options.value[field].find(option => option.value === selected(field))?.label
|
||||
|| selected(field) || t('hidGuide.selectDevice')
|
||||
}
|
||||
function assign(draft: HidSelection, field: DeviceField, value: string) {
|
||||
if (field === 'serial') draft.ch9329_port = value
|
||||
if (field === 'udc') draft.otg_udc = value
|
||||
if (field === 'bluetooth') draft.bluetooth.adapter = value
|
||||
}
|
||||
function device(field: DeviceField, value: string) {
|
||||
if (value === selected(field)) return
|
||||
const draft = { ...props.modelValue, bluetooth: { ...props.modelValue.bluetooth } }
|
||||
if (draft.backend === 'otg') draft.otg_udc = value
|
||||
if (draft.backend === 'ch9329') draft.ch9329_port = value
|
||||
if (draft.backend === 'bluetooth') draft.bluetooth.adapter = value
|
||||
assign(draft, field, value)
|
||||
emit('update:modelValue', draft)
|
||||
}
|
||||
function fieldLabel(field: DeviceField): string {
|
||||
return field === 'udc' && props.deviceOnly ? t('hidPage.udcLabel') : t(`hidGuide.device_${field === 'serial' ? 'ch9329' : field === 'udc' ? 'otg' : 'bluetooth'}`)
|
||||
}
|
||||
async function refresh() {
|
||||
const own = ++generation
|
||||
options.value = []; error.value = ''; missing.value = ''; loading.value = true
|
||||
options.value = { serial: [], udc: [], bluetooth: [] }; error.value = ''; loading.value = true
|
||||
try {
|
||||
if (!drivers.value.includes(props.modelValue.backend) || props.modelValue.backend === 'none') return
|
||||
let next: typeof options.value
|
||||
const next: Record<DeviceField, DeviceOption[]> = { serial: [], udc: [], bluetooth: [] }
|
||||
if (props.modelValue.backend === 'bluetooth') {
|
||||
next = (await request<BluetoothAdapter[]>('/hid/bluetooth/adapters')).map(a => ({ value: a.name, label: `${a.name} · ${a.address}` }))
|
||||
next.bluetooth = (await request<BluetoothAdapter[]>('/hid/bluetooth/adapters')).map(a => ({ value: a.name, label: `${a.name} · ${a.address}` }))
|
||||
} else {
|
||||
const devices = await configApi.listDevices()
|
||||
next = props.modelValue.backend === 'otg' ? devices.udc.map(d => ({ value: d.name, label: d.name })) : devices.serial.map(d => ({ value: d.path, label: `${d.name} · ${d.path}` }))
|
||||
next.udc = devices.udc.map(d => ({ value: d.name, label: d.name }))
|
||||
next.serial = devices.serial.map(d => ({ value: d.path, label: `${d.name} · ${d.path}` }))
|
||||
}
|
||||
if (own !== generation) return
|
||||
options.value = next
|
||||
if (selected.value && !next.some(d => d.value === selected.value)) missing.value = t('hidGuide.missingDevice', { device: selected.value })
|
||||
device(selectDevice(selected.value, next.map(d => d.value)))
|
||||
if (!next.length) error.value = t('hidGuide.noDevices')
|
||||
const draft = { ...props.modelValue, bluetooth: { ...props.modelValue.bluetooth } }
|
||||
for (const field of fields.value) assign(draft, field, selectDevice(selected(field), next[field].map(d => d.value)))
|
||||
if (JSON.stringify(draft) !== JSON.stringify(props.modelValue)) emit('update:modelValue', draft)
|
||||
} catch (e) { if (own === generation) error.value = e instanceof Error ? e.message : String(e) }
|
||||
finally { if (own === generation) loading.value = false }
|
||||
}
|
||||
const valid = computed(() => drivers.value.includes(props.modelValue.backend) && (props.modelValue.backend === 'none' || (!loading.value && !error.value && options.value.some(d => d.value === selected.value)
|
||||
const valid = computed(() => drivers.value.includes(props.modelValue.backend) && (props.modelValue.backend === 'none' || (!loading.value && !error.value
|
||||
&& fields.value.every(field => options.value[field].some(d => d.value === selected(field)))
|
||||
&& (props.modelValue.backend !== 'bluetooth' || validName(props.modelValue.bluetooth.name))
|
||||
&& (props.modelValue.backend !== 'ch9329' || [9600, 19200, 38400, 57600, 115200].includes(props.modelValue.ch9329_baudrate)))))
|
||||
&& (!usesCh9329(props.modelValue.backend) || [9600, 19200, 38400, 57600, 115200].includes(props.modelValue.ch9329_baudrate)))))
|
||||
watch(valid, value => emit('valid', value), { immediate: true })
|
||||
watch(() => [props.modelValue.backend, drivers.value.includes(props.modelValue.backend)], refresh, { immediate: true })
|
||||
watch([() => props.modelValue.backend, () => drivers.value.includes(props.modelValue.backend)], refresh, { immediate: true })
|
||||
onUnmounted(() => generation++)
|
||||
defineExpose({ refresh, loading })
|
||||
</script>
|
||||
<template>
|
||||
<fieldset :disabled="locked" class="space-y-4 min-w-0">
|
||||
<div class="space-y-1 text-sm">
|
||||
<fieldset :disabled="loading || locked" class="space-y-4 min-w-0">
|
||||
<div v-if="!deviceOnly" class="space-y-1 text-sm">
|
||||
<div class="flex min-h-8 items-center justify-between gap-2">
|
||||
<label for="hid-driver">{{ t('hidGuide.driver') }}</label>
|
||||
<label :for="`${idPrefix}-driver`">{{ t('hidGuide.driver') }}</label>
|
||||
<div class="flex items-center gap-1">
|
||||
<HoverCard v-if="modelValue.backend !== 'none'" :open-delay="150" :close-delay="100">
|
||||
<HoverCardTrigger as-child>
|
||||
@@ -77,7 +102,7 @@ onUnmounted(() => generation++)
|
||||
type="button"
|
||||
variant="ghost"
|
||||
size="icon-sm"
|
||||
:disabled="loading"
|
||||
:disabled="loading || locked"
|
||||
:aria-label="t('hidGuide.refreshDevices')"
|
||||
:title="t('hidGuide.refreshDevices')"
|
||||
@click="refresh"
|
||||
@@ -88,9 +113,10 @@ onUnmounted(() => generation++)
|
||||
</div>
|
||||
<Select
|
||||
:model-value="modelValue.backend"
|
||||
:disabled="loading || locked"
|
||||
@update:model-value="value => emit('update:modelValue', { ...modelValue, backend: String(value) as Driver })"
|
||||
>
|
||||
<SelectTrigger id="hid-driver" class="w-full">
|
||||
<SelectTrigger :id="`${idPrefix}-driver`" class="w-full">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
@@ -101,46 +127,48 @@ onUnmounted(() => generation++)
|
||||
</Select>
|
||||
</div>
|
||||
<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">
|
||||
<span>{{ t(`hidGuide.device_${modelValue.backend}`) }}</span>
|
||||
<Select
|
||||
:model-value="selected || EMPTY_SELECT_VALUE"
|
||||
:disabled="loading"
|
||||
@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 v-if="modelValue.backend === 'ch9329'" class="block space-y-1 text-sm">
|
||||
<span>{{ t('actionbar.baudrate') }}</span>
|
||||
<Select
|
||||
:model-value="String(modelValue.ch9329_baudrate)"
|
||||
@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 v-if="modelValue.backend === 'bluetooth'" class="block space-y-1 text-sm">
|
||||
<p v-if="!deviceOnly && modelValue.backend === 'ch9329_otg'" class="text-sm text-muted-foreground">{{ t('hidGuide.compositeHelp') }}</p>
|
||||
<div v-for="field in fields" :key="field" class="grid min-w-0 gap-4"
|
||||
:class="{ 'grid-cols-[minmax(0,1fr)_7rem]': field === 'serial' }">
|
||||
<div class="min-w-0 space-y-1 text-sm">
|
||||
<label :for="`${idPrefix}-device-${field}`">{{ fieldLabel(field) }}</label>
|
||||
<Select :model-value="selected(field) || EMPTY_SELECT_VALUE" :disabled="loading || locked"
|
||||
@update:model-value="value => device(field, value === EMPTY_SELECT_VALUE ? '' : String(value))">
|
||||
<SelectTrigger :id="`${idPrefix}-device-${field}`" class="w-full">
|
||||
<SelectValue :placeholder="t('hidGuide.selectDevice')">{{ deviceLabel(field) }}</SelectValue>
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem :value="EMPTY_SELECT_VALUE" disabled>{{ t('hidGuide.selectDevice') }}</SelectItem>
|
||||
<SelectItem v-if="selected(field) && !options[field].some(d => d.value === selected(field))" :value="selected(field)" disabled>{{ selected(field) }}</SelectItem>
|
||||
<SelectItem v-for="option in options[field]" :key="option.value" :value="option.value">{{ option.label }}</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<p v-if="!loading && selected(field) && !options[field].some(d => d.value === selected(field))" class="text-warning">{{ t('hidGuide.missingDevice', { device: selected(field) }) }}</p>
|
||||
<p v-if="!loading && !options[field].length" class="text-destructive">{{ fieldLabel(field) }}: {{ t('hidGuide.noDevices') }}</p>
|
||||
</div>
|
||||
<div v-if="field === 'serial'" class="min-w-0 space-y-1 text-sm">
|
||||
<label :for="`${idPrefix}-baudrate`">{{ t('actionbar.baudrate') }}</label>
|
||||
<Select
|
||||
:model-value="String(modelValue.ch9329_baudrate)"
|
||||
:disabled="loading || locked"
|
||||
@update:model-value="value => emit('update:modelValue', { ...modelValue, ch9329_baudrate: Number(value) })"
|
||||
>
|
||||
<SelectTrigger :id="`${idPrefix}-baudrate`" 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>
|
||||
</div>
|
||||
</div>
|
||||
<label v-if="modelValue.backend === 'bluetooth'" :for="`${idPrefix}-bluetooth-name`" class="block space-y-1 text-sm">
|
||||
<span>{{ t('bluetoothHid.name') }}</span>
|
||||
<Input :model-value="modelValue.bluetooth.name" @update:model-value="emit('update:modelValue', { ...modelValue, bluetooth: { ...modelValue.bluetooth, name: String($event) } })" />
|
||||
<Input :id="`${idPrefix}-bluetooth-name`" :disabled="loading || locked" :model-value="modelValue.bluetooth.name" @update:model-value="emit('update:modelValue', { ...modelValue, bluetooth: { ...modelValue.bluetooth, name: String($event) } })" />
|
||||
<span v-if="!validName(modelValue.bluetooth.name)" class="text-destructive text-xs">{{ t('hidGuide.nameInvalid') }}</span>
|
||||
</label>
|
||||
<p v-if="missing" class="text-sm text-warning">{{ missing }}</p>
|
||||
<p v-if="error" role="alert" class="text-sm text-destructive break-words">{{ error }}</p>
|
||||
</fieldset>
|
||||
</template>
|
||||
|
||||
@@ -8,7 +8,27 @@ const { t } = useI18n()
|
||||
</script>
|
||||
<template>
|
||||
<figure v-if="backend !== 'none'" class="wiring-diagram rounded-lg border border-border/60 bg-muted/20 p-3 space-y-2">
|
||||
<svg viewBox="0 0 460 184" class="block w-full text-foreground" role="img" :aria-label="t(`hidGuide.wiring_${backend}`)">
|
||||
<svg v-if="backend === 'ch9329_otg'" viewBox="0 0 480 190" class="block w-full text-foreground" role="img" :aria-label="t('hidGuide.wiring_ch9329_otg')">
|
||||
<g fill="none" stroke="currentColor" stroke-width="1.2">
|
||||
<rect x="8" y="51" width="112" height="100" rx="8" />
|
||||
<rect x="174" y="24" width="112" height="35" rx="6" />
|
||||
<rect x="332" y="19" width="140" height="151" rx="8" />
|
||||
</g>
|
||||
<g fill="currentColor" text-anchor="middle" font-size="12">
|
||||
<text x="64" y="80">One-KVM</text>
|
||||
<text x="230" y="46">CH9329</text>
|
||||
<text x="402" y="43">{{ t('hidGuide.kvmSwitch') }}</text>
|
||||
<text x="402" y="84">{{ t('hidGuide.dedicatedHidPort') }}</text>
|
||||
<text x="402" y="143">{{ t('hidGuide.generalUsbPort') }}</text>
|
||||
<text x="146" y="33">UART</text>
|
||||
<text x="229" y="127">OTG USB</text>
|
||||
</g>
|
||||
<g class="connection" fill="none" stroke="currentColor" stroke-linejoin="round">
|
||||
<path d="M120 90h22V42h32m112 0h20v35h26m-8-5 8 5-8 5" />
|
||||
<path d="M120 136h212m-8-5 8 5-8 5" />
|
||||
</g>
|
||||
</svg>
|
||||
<svg v-else viewBox="0 0 460 184" class="block w-full text-foreground" role="img" :aria-label="t(`hidGuide.wiring_${backend}`)">
|
||||
<!-- A faint ground plane anchors the perspective without shadows or filters. -->
|
||||
<g fill="none" stroke="currentColor" stroke-width=".65" opacity=".07" aria-hidden="true">
|
||||
<path d="m7 87 101-46 75 38-101 46Zm270-5 98-45 79 39-98 46M31 99l101-46m-77 58 101-46M302 94l98-45m-71 58 98-45" />
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { ref, watch, onMounted, onUnmounted, onDeactivated, onActivated, type Ref } from 'vue'
|
||||
import type { hidApi } from '@/api'
|
||||
import { runtimeBackend } from '@/lib/hidGuide'
|
||||
import { request } from '@/api/request'
|
||||
import type { BluetoothStatus } from '@/types/bluetooth'
|
||||
export function useHidConnection(active: Ref<boolean>, backend: Ref<string | undefined>) {
|
||||
@@ -26,8 +27,8 @@ export function useHidConnection(active: Ref<boolean>, backend: Ref<string | und
|
||||
if (hidResult.status === 'rejected') throw hidResult.reason
|
||||
if (btResult.status === 'rejected') throw btResult.reason
|
||||
const hid = hidResult.value, bt = btResult.value
|
||||
status.value = hid.backend === backend.value ? hid : null
|
||||
bluetooth.value = hid.backend === backend.value ? bt : null
|
||||
status.value = hid.backend === runtimeBackend(backend.value) ? hid : null
|
||||
bluetooth.value = hid.backend === runtimeBackend(backend.value) ? bt : null
|
||||
error.value = ''
|
||||
} catch (e) {
|
||||
if (own !== generation) return
|
||||
|
||||
@@ -1,5 +1,17 @@
|
||||
export default {
|
||||
hidGuide: {
|
||||
driver_ch9329_otg: 'CH9329 + OTG composite mode',
|
||||
compositeHelp: 'CH9329 handles keyboard and mouse. OTG provides virtual media, USB networking and USB audio. Enable advanced functions in their settings.',
|
||||
wiring_ch9329_otg: 'Connect CH9329 to the dedicated keyboard/mouse port and OTG to a general USB port. Use an operation that switches video, keyboard/mouse and USB together.',
|
||||
keyboardChannel: 'CH9329 input',
|
||||
otgChannel: 'OTG functions',
|
||||
kvmSwitch: 'KVM switch',
|
||||
dedicatedHidPort: 'Keyboard/mouse port',
|
||||
generalUsbPort: 'General USB port',
|
||||
advancedFunctions: 'OTG advanced USB functions',
|
||||
otgInactive: 'Not enabled',
|
||||
otgConfigured: 'USB configured by host',
|
||||
otgWaiting: 'Bound, waiting for host',
|
||||
missingDevice: 'The previous device {device} is unavailable. Check the current selection.',
|
||||
connectionTimeout: 'Connection timed out. Check the data cable and the controlled computer’s USB port. You can connect later or reconfigure.',
|
||||
"deviceTitle": "Driver and device",
|
||||
@@ -50,7 +62,7 @@ export default {
|
||||
"pairTimeout": "The pairing window ended. You can open it again.",
|
||||
"reopenPairing": "Reopen pairing",
|
||||
"resetWarning": "Previous One-KVM HID bonds will be cleared. Remove the old device on the controlled computer before adding it again.",
|
||||
"disableUsb": "Leaving USB OTG will disable: {functions}.",
|
||||
"disableUsb": "Switching drivers will disable: {functions}.",
|
||||
"msd": "Virtual disk",
|
||||
"network": "USB network",
|
||||
"audio": "USB audio",
|
||||
|
||||
@@ -1,5 +1,17 @@
|
||||
export default {
|
||||
hidGuide: {
|
||||
driver_ch9329_otg: 'CH9329 + OTG 复合模式',
|
||||
compositeHelp: 'CH9329 负责键盘和鼠标,OTG 负责虚拟磁盘、USB 网络和 USB 音频。高级功能需在各自设置中启用。',
|
||||
wiring_ch9329_otg: 'CH9329 接 KVM 专用键鼠口,OTG 接通用 USB 口。请使用同时切换视频、键鼠和通用 USB 的操作。',
|
||||
keyboardChannel: 'CH9329 键鼠',
|
||||
otgChannel: 'OTG 高级功能',
|
||||
kvmSwitch: 'KVM 切换器',
|
||||
dedicatedHidPort: '专用键鼠口',
|
||||
generalUsbPort: '通用 USB 口',
|
||||
advancedFunctions: 'OTG 高级 USB 功能',
|
||||
otgInactive: '未启用',
|
||||
otgConfigured: '主机已配置 USB',
|
||||
otgWaiting: '已绑定,等待主机连接',
|
||||
missingDevice: '原设备 {device} 已不可用,请检查当前选择。',
|
||||
connectionTimeout: '等待连接超时,请检查数据线及被控机 USB 端口。可稍后连接或重新配置。',
|
||||
"deviceTitle": "驱动与设备",
|
||||
@@ -50,7 +62,7 @@ export default {
|
||||
"pairTimeout": "配对窗口已结束,可重新开启。",
|
||||
"reopenPairing": "重新开启配对",
|
||||
"resetWarning": "将清除原 One-KVM HID 绑定;请在被控机中删除旧设备后重新添加。",
|
||||
"disableUsb": "切出 USB OTG 将关闭:{functions}。",
|
||||
"disableUsb": "切换驱动将关闭:{functions}。",
|
||||
"msd": "虚拟磁盘",
|
||||
"network": "USB 网络",
|
||||
"audio": "USB 音频",
|
||||
|
||||
@@ -1,6 +1,15 @@
|
||||
import type { HidConfig, HidConfigUpdate } from '../types/generated'
|
||||
import type { BluetoothStatus } from '../types/bluetooth'
|
||||
export type Driver = 'otg' | 'ch9329' | 'bluetooth' | 'none'
|
||||
export type Driver = 'otg' | 'ch9329' | 'ch9329_otg' | 'bluetooth' | 'none'
|
||||
export function usesCh9329(backend?: string): boolean {
|
||||
return backend === 'ch9329' || backend === 'ch9329_otg'
|
||||
}
|
||||
export function supportsOtgFunctions(backend?: string): boolean {
|
||||
return backend === 'otg' || backend === 'ch9329_otg'
|
||||
}
|
||||
export function runtimeBackend(backend?: string): string | undefined {
|
||||
return backend === 'ch9329_otg' ? 'ch9329' : backend
|
||||
}
|
||||
export interface HidSelection {
|
||||
backend: Driver; otg_udc: string; ch9329_port: string; ch9329_baudrate: number
|
||||
bluetooth: { adapter: string; name: string }
|
||||
@@ -11,7 +20,7 @@ export function selectionFrom(hid?: HidConfig | null): HidSelection {
|
||||
bluetooth: { adapter: hid?.bluetooth.adapter ?? '', name: hid?.bluetooth.name ?? 'One-KVM HID' } }
|
||||
}
|
||||
export function selectDevice(saved: string, candidates: string[]): string {
|
||||
return candidates.includes(saved) ? saved : candidates.length === 1 ? candidates[0]! : ''
|
||||
return saved || (candidates.length === 1 ? candidates[0]! : '')
|
||||
}
|
||||
export function validName(name: string): boolean {
|
||||
const length = new TextEncoder().encode(name).length
|
||||
@@ -21,6 +30,7 @@ export function deviceRequest(draft: HidSelection): HidConfigUpdate {
|
||||
const backend = draft.backend as HidConfigUpdate['backend']
|
||||
switch (draft.backend) {
|
||||
case 'otg': return { backend, otg_udc: draft.otg_udc }
|
||||
case 'ch9329_otg': return { backend, otg_udc: draft.otg_udc, ch9329_port: draft.ch9329_port, ch9329_baudrate: draft.ch9329_baudrate }
|
||||
case 'ch9329': return { backend, ch9329_port: draft.ch9329_port, ch9329_baudrate: draft.ch9329_baudrate }
|
||||
case 'bluetooth': return { backend, bluetooth: { ...draft.bluetooth }, bluetooth_reset_pairing: true }
|
||||
case 'none': return { backend }
|
||||
@@ -30,6 +40,7 @@ export function matchesSelection(hid: HidConfig, draft: HidSelection): boolean {
|
||||
if (hid.backend !== draft.backend) return false
|
||||
switch (draft.backend) {
|
||||
case 'otg': return hid.otg_udc === draft.otg_udc
|
||||
case 'ch9329_otg': return hid.otg_udc === draft.otg_udc && hid.ch9329_port === draft.ch9329_port && hid.ch9329_baudrate === draft.ch9329_baudrate
|
||||
case 'ch9329': return hid.ch9329_port === draft.ch9329_port && hid.ch9329_baudrate === draft.ch9329_baudrate
|
||||
case 'bluetooth': return hid.bluetooth.adapter === draft.bluetooth.adapter && hid.bluetooth.name === draft.bluetooth.name
|
||||
case 'none': return true
|
||||
@@ -43,7 +54,25 @@ export function bluetoothStage(status: BluetoothStatus | null): string {
|
||||
if (status.pairing_seconds > 0) return 'pairing'
|
||||
return status.initialized ? 'waiting' : 'initializing'
|
||||
}
|
||||
export interface OtgDeviceStatus {
|
||||
enabled: boolean
|
||||
health: 'healthy' | 'applying' | 'degraded'
|
||||
gadget_active: boolean
|
||||
udc: string | null
|
||||
udc_state: string | null
|
||||
error: string | null
|
||||
}
|
||||
export function otgDeviceStage(status?: OtgDeviceStatus | null): string {
|
||||
if (!status) return 'unknown'
|
||||
if (!status.enabled) return 'otgInactive'
|
||||
if (status.health === 'applying') return 'initializing'
|
||||
if (status.health === 'degraded' || status.error) return 'unknown'
|
||||
if (!status.gadget_active) return 'unknown'
|
||||
return status.udc_state === 'configured' ? 'otgConfigured' : 'otgWaiting'
|
||||
}
|
||||
export interface HidDeviceStatus {
|
||||
otg?: OtgDeviceStatus | null
|
||||
configured_backend?: string
|
||||
backend: string; online: boolean; error?: string | null; error_code?: string | null
|
||||
}
|
||||
export function hidDeviceError(status?: HidDeviceStatus | null): string | null {
|
||||
|
||||
@@ -25,6 +25,7 @@ export interface BluetoothHidConfig {
|
||||
export enum HidBackend {
|
||||
Otg = "otg",
|
||||
Ch9329 = "ch9329",
|
||||
Ch9329Otg = "ch9329_otg",
|
||||
Bluetooth = "bluetooth",
|
||||
None = "none",
|
||||
}
|
||||
@@ -548,7 +549,17 @@ export interface OtgNetworkStatus {
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export interface OtgRuntimeStatus {
|
||||
enabled: boolean;
|
||||
health: OtgRuntimeHealth;
|
||||
gadget_active: boolean;
|
||||
udc?: string;
|
||||
udc_state?: string;
|
||||
error?: string;
|
||||
}
|
||||
|
||||
export interface OtgConfigResponse {
|
||||
uac: UacConfig;
|
||||
hid: HidConfig;
|
||||
msd: MsdConfig;
|
||||
network: OtgNetworkConfig;
|
||||
@@ -564,6 +575,7 @@ export interface OtgNetworkConfigUpdate {
|
||||
}
|
||||
|
||||
export interface OtgConfigUpdate {
|
||||
uac?: UacConfig;
|
||||
hid?: HidConfigUpdate;
|
||||
msd?: MsdConfigUpdate;
|
||||
network?: OtgNetworkConfigUpdate;
|
||||
|
||||
@@ -736,7 +736,7 @@ watch(videoLoading, (loading) => {
|
||||
|
||||
|
||||
const showMsdStatusCard = computed(() => {
|
||||
return !!(systemStore.msd?.available && systemStore.hid?.backend !== 'ch9329')
|
||||
return !!(systemStore.msd?.available)
|
||||
})
|
||||
|
||||
const hidHoverAlign = computed<'start' | 'end'>(() => {
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { usesCh9329, supportsOtgFunctions } from '@/lib/hidGuide'
|
||||
import McpSettingsCard from '@/components/McpSettingsCard.vue'
|
||||
import HidDeviceSettings from '@/components/HidDeviceSettings.vue'
|
||||
import ConsoleLayoutPreview from '@/components/ConsoleLayoutPreview.vue'
|
||||
@@ -1152,7 +1153,7 @@ function clearCh9329DescriptorState() {
|
||||
}
|
||||
|
||||
const isCh9329DescriptorSourceCurrent = computed(() => {
|
||||
if (config.value.hid_backend !== 'ch9329') return false
|
||||
if (!usesCh9329(config.value.hid_backend)) return false
|
||||
const source = ch9329DescriptorSource.value
|
||||
if (!source) return false
|
||||
const current = currentCh9329DescriptorSource()
|
||||
@@ -1160,7 +1161,7 @@ const isCh9329DescriptorSourceCurrent = computed(() => {
|
||||
})
|
||||
|
||||
async function loadCh9329Descriptor() {
|
||||
if (config.value.hid_backend !== 'ch9329') return
|
||||
if (!usesCh9329(config.value.hid_backend)) return
|
||||
const source = currentCh9329DescriptorSource()
|
||||
ch9329DescriptorLoading.value = true
|
||||
ch9329DescriptorLoaded.value = false
|
||||
@@ -1182,14 +1183,14 @@ async function loadCh9329Descriptor() {
|
||||
}
|
||||
|
||||
const isCh9329DescriptorValid = computed(() => {
|
||||
if (config.value.hid_backend !== 'ch9329') return true
|
||||
if (!usesCh9329(config.value.hid_backend)) return true
|
||||
return utf8ByteLength(ch9329Manufacturer.value) <= 23
|
||||
&& utf8ByteLength(ch9329Product.value) <= 23
|
||||
&& utf8ByteLength(ch9329SerialNumber.value) <= 23
|
||||
})
|
||||
|
||||
const canEditCh9329Descriptor = computed(() =>
|
||||
config.value.hid_backend === 'ch9329'
|
||||
usesCh9329(config.value.hid_backend)
|
||||
&& ch9329DescriptorLoaded.value
|
||||
&& isCh9329DescriptorSourceCurrent.value
|
||||
&& !ch9329DescriptorLoading.value
|
||||
@@ -1268,7 +1269,7 @@ const atxDevices = ref<AtxDevices>({
|
||||
})
|
||||
|
||||
const ch9329ReservedSerialDevice = computed(() => {
|
||||
if (config.value.hid_backend !== 'ch9329') return ''
|
||||
if (!usesCh9329(config.value.hid_backend)) return ''
|
||||
return config.value.hid_serial_device.trim()
|
||||
})
|
||||
|
||||
@@ -1492,7 +1493,7 @@ async function saveConfig() {
|
||||
return
|
||||
}
|
||||
const hidUpdate: HidConfigUpdate = {}
|
||||
if (config.value.hid_backend === 'ch9329' && isCh9329DescriptorDirty.value) {
|
||||
if (usesCh9329(config.value.hid_backend) && isCh9329DescriptorDirty.value) {
|
||||
hidUpdate.ch9329_descriptor = {
|
||||
vendor_id: parseInt(ch9329VendorIdHex.value, 16) || 0x1a86,
|
||||
product_id: parseInt(ch9329ProductIdHex.value, 16) || 0xe129,
|
||||
@@ -1501,7 +1502,7 @@ async function saveConfig() {
|
||||
serial_number: ch9329SerialNumber.value || '',
|
||||
}
|
||||
}
|
||||
if (config.value.hid_backend === 'otg') {
|
||||
if (supportsOtgFunctions(config.value.hid_backend)) {
|
||||
hidUpdate.otg_descriptor = {
|
||||
vendor_id: parseInt(otgVendorIdHex.value, 16) || 0x1d6b,
|
||||
product_id: parseInt(otgProductIdHex.value, 16) || 0x0104,
|
||||
@@ -1509,12 +1510,14 @@ async function saveConfig() {
|
||||
product: otgProduct.value || 'One-KVM USB Device',
|
||||
serial_number: otgSerialNumber.value || undefined,
|
||||
}
|
||||
hidUpdate.otg_profile = 'custom' as OtgHidProfile
|
||||
hidUpdate.otg_functions = { ...config.value.hid_otg_functions }
|
||||
hidUpdate.otg_keyboard_leds = config.value.hid_otg_keyboard_leds
|
||||
if (config.value.hid_backend === 'otg') {
|
||||
hidUpdate.otg_profile = 'custom' as OtgHidProfile
|
||||
hidUpdate.otg_functions = { ...config.value.hid_otg_functions }
|
||||
hidUpdate.otg_keyboard_leds = config.value.hid_otg_keyboard_leds
|
||||
}
|
||||
}
|
||||
if (configStore.hid?.backend === 'otg') {
|
||||
const otgEnabled = config.value.hid_backend === 'otg'
|
||||
if (supportsOtgFunctions(configStore.hid?.backend)) {
|
||||
const otgEnabled = supportsOtgFunctions(config.value.hid_backend)
|
||||
const response = await configStore.updateOtg({
|
||||
hid: hidUpdate,
|
||||
msd: {
|
||||
@@ -1528,15 +1531,14 @@ async function saveConfig() {
|
||||
driver_mode: config.value.otg_network_driver as any,
|
||||
bridge_interface: config.value.otg_network_interface,
|
||||
},
|
||||
uac: {
|
||||
enabled: otgEnabled && config.value.uac_enabled,
|
||||
sample_rate: 48000,
|
||||
channels: 2,
|
||||
},
|
||||
})
|
||||
otgNetworkStatus.value = response.status
|
||||
|
||||
await uacApi.update({
|
||||
enabled: otgEnabled && config.value.uac_enabled,
|
||||
sample_rate: 48000,
|
||||
channels: 2,
|
||||
})
|
||||
} else if (configStore.hid?.backend === 'ch9329') {
|
||||
} else if (usesCh9329(configStore.hid?.backend)) {
|
||||
await configStore.updateHid(hidUpdate)
|
||||
}
|
||||
await loadConfig()
|
||||
@@ -1632,11 +1634,11 @@ async function loadConfig() {
|
||||
otgSerialNumber.value = hid.otg_descriptor.serial_number || ''
|
||||
}
|
||||
if (hid.ch9329_descriptor) {
|
||||
if (hid.backend !== 'ch9329') {
|
||||
if (!usesCh9329(hid.backend)) {
|
||||
applyCh9329DescriptorForm(hid.ch9329_descriptor, true)
|
||||
}
|
||||
}
|
||||
if (hid.backend === 'ch9329') {
|
||||
if (usesCh9329(hid.backend)) {
|
||||
await loadCh9329Descriptor()
|
||||
} else {
|
||||
clearCh9329DescriptorState()
|
||||
@@ -2773,7 +2775,7 @@ watch(updateChannel, async () => {
|
||||
watch(() => config.value.hid_backend, () => {
|
||||
otgSelfCheckResult.value = null
|
||||
otgSelfCheckError.value = ''
|
||||
if (config.value.hid_backend === 'ch9329') {
|
||||
if (usesCh9329(config.value.hid_backend)) {
|
||||
void loadCh9329Descriptor()
|
||||
} else {
|
||||
clearCh9329DescriptorState()
|
||||
@@ -2783,7 +2785,7 @@ watch(() => config.value.hid_backend, () => {
|
||||
watch(
|
||||
() => [config.value.hid_serial_device, config.value.hid_serial_baudrate],
|
||||
() => {
|
||||
if (config.value.hid_backend === 'ch9329' && !isCh9329DescriptorSourceCurrent.value) {
|
||||
if (usesCh9329(config.value.hid_backend) && !isCh9329DescriptorSourceCurrent.value) {
|
||||
clearCh9329DescriptorState()
|
||||
}
|
||||
},
|
||||
@@ -3170,10 +3172,10 @@ watch(isWindows, () => {
|
||||
<!-- HID Section -->
|
||||
<div v-show="activeSection === 'hid'" class="space-y-4">
|
||||
<HidDeviceSettings :active="activeSection === 'hid'" :dirty="hidFeaturesDirty" @applied="loadConfig" @discard="loadConfig" />
|
||||
<Card v-if="configStore.hid?.backend === 'otg' || configStore.hid?.backend === 'ch9329'">
|
||||
<Card v-if="supportsOtgFunctions(configStore.hid?.backend) || usesCh9329(configStore.hid?.backend)">
|
||||
<CardHeader><CardTitle>{{ t('hidGuide.features') }}</CardTitle></CardHeader>
|
||||
<CardContent class="space-y-4">
|
||||
<template v-if="config.hid_backend === 'ch9329'">
|
||||
<template v-if="usesCh9329(config.hid_backend)">
|
||||
<Separator class="my-4" />
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
@@ -3261,7 +3263,7 @@ watch(isWindows, () => {
|
||||
</template>
|
||||
|
||||
<!-- OTG Descriptor Settings -->
|
||||
<template v-if="config.hid_backend === 'otg'">
|
||||
<template v-if="supportsOtgFunctions(config.hid_backend)">
|
||||
<Separator class="my-4" />
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
@@ -3323,10 +3325,10 @@ watch(isWindows, () => {
|
||||
<Separator class="my-4" />
|
||||
<div class="space-y-4">
|
||||
<div>
|
||||
<h4 class="text-sm font-medium">{{ t('settings.otgHidProfile') }}</h4>
|
||||
<h4 class="text-sm font-medium">{{ t(config.hid_backend === 'otg' ? 'settings.otgHidProfile' : 'hidGuide.advancedFunctions') }}</h4>
|
||||
</div>
|
||||
<div class="space-y-3">
|
||||
<div class="grid gap-3 md:grid-cols-2">
|
||||
<div v-if="config.hid_backend === 'otg'" class="grid gap-3 md:grid-cols-2">
|
||||
<div class="space-y-3 rounded-md border border-border/60 p-3">
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
<div>
|
||||
@@ -3364,13 +3366,13 @@ watch(isWindows, () => {
|
||||
<Switch v-model="config.hid_otg_functions.mouse_absolute" />
|
||||
</div>
|
||||
<p v-if="hasMacosDragInterfaceConflict" class="text-xs text-warning">{{ t('settings.mouseMacosDragRequiresBoth') }}</p>
|
||||
<Separator />
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
<Label>{{ t('settings.uacMic') }}</Label>
|
||||
<Switch v-model="config.uac_enabled" />
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex items-center justify-between gap-4 rounded-md border border-border/60 p-3">
|
||||
<Label>{{ t('settings.uacMic') }}</Label>
|
||||
<Switch v-model="config.uac_enabled" />
|
||||
</div>
|
||||
<div class="space-y-3 rounded-md border border-border/60 p-3">
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
<div>
|
||||
@@ -3466,7 +3468,7 @@ watch(isWindows, () => {
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-xs text-warning">
|
||||
<p v-if="config.hid_backend === 'otg'" class="text-xs text-warning">
|
||||
{{ t('settings.otgProfileWarning') }}
|
||||
</p>
|
||||
</div>
|
||||
@@ -5419,7 +5421,7 @@ watch(isWindows, () => {
|
||||
</div>
|
||||
|
||||
<!-- Save Button (sticky) -->
|
||||
<div v-if="activeSection === 'video' || (activeSection === 'hid' && ['otg', 'ch9329'].includes(configStore.hid?.backend ?? ''))" 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="activeSection === 'video' || (activeSection === 'hid' && (supportsOtgFunctions(configStore.hid?.backend) || usesCh9329(configStore.hid?.backend)))" class="sticky bottom-0 pt-3 sm:pt-4 pb-3 bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/80 border-t -mx-3 px-3 sm:-mx-6 sm:px-6 lg:-mx-8 lg:px-8">
|
||||
<div class="flex items-center justify-between gap-2 sm:gap-3">
|
||||
<p v-if="activeSection === 'hid' && !isHidFunctionSelectionValid" class="flex min-w-0 items-center gap-1.5 text-xs text-warning">
|
||||
<AlertTriangle class="size-3.5 shrink-0" />
|
||||
@@ -5433,7 +5435,7 @@ watch(isWindows, () => {
|
||||
<AlertTriangle class="size-3.5 shrink-0" />
|
||||
<span class="truncate">{{ t('settings.ch9329StringLengthWarning') }}</span>
|
||||
</p>
|
||||
<p v-else-if="activeSection === 'hid' && config.hid_backend === 'ch9329' && ch9329DescriptorLoading" class="flex min-w-0 items-center gap-1.5 text-xs text-warning">
|
||||
<p v-else-if="activeSection === 'hid' && usesCh9329(config.hid_backend) && ch9329DescriptorLoading" class="flex min-w-0 items-center gap-1.5 text-xs text-warning">
|
||||
<AlertTriangle class="size-3.5 shrink-0" />
|
||||
<span class="truncate">{{ t('settings.ch9329DescriptorLoading') }}</span>
|
||||
</p>
|
||||
|
||||
@@ -387,10 +387,11 @@ async function handleSetup() {
|
||||
|
||||
const hid = hidSelection.value
|
||||
setupData.hid_backend = hid.backend
|
||||
if (hid.backend === 'ch9329') {
|
||||
if (hid.backend === 'ch9329' || hid.backend === 'ch9329_otg') {
|
||||
setupData.hid_ch9329_port = hid.ch9329_port
|
||||
setupData.hid_ch9329_baudrate = hid.ch9329_baudrate
|
||||
} else if (hid.backend === 'otg') {
|
||||
}
|
||||
if (hid.backend === 'otg' || hid.backend === 'ch9329_otg') {
|
||||
setupData.hid_otg_udc = hid.otg_udc
|
||||
} else if (hid.backend === 'bluetooth') {
|
||||
setupData.hid_bluetooth = { ...hid.bluetooth }
|
||||
|
||||
Reference in New Issue
Block a user