diff --git a/web/src/App.vue b/web/src/App.vue index 81dbeaa3..220b0dfb 100644 --- a/web/src/App.vue +++ b/web/src/App.vue @@ -1,9 +1,7 @@ - - + diff --git a/web/src/components/HidDriverDialog.vue b/web/src/components/HidDriverDialog.vue index c41f4d4a..96953e07 100644 --- a/web/src/components/HidDriverDialog.vue +++ b/web/src/components/HidDriverDialog.vue @@ -7,20 +7,19 @@ import { useConfigStore } from '@/stores/config' import { request } from '@/api/request' import type { HidConfig, MsdConfig } from '@/types/generated' import { useHidConnection } from '@/composables/useHidConnection' -import { deviceRequest, selectionFrom, matchesSelection, writePendingHid, pendingHidKey, type PendingHid, type HidSelection } from '@/lib/hidGuide' +import { deviceRequest, matchesSelection, selectionFrom, type HidSelection } from '@/lib/hidGuide' import HidDriverForm from './HidDriverForm.vue' import HidWiringDiagram from './HidWiringDiagram.vue' import HidDeviceOverview from './HidDeviceOverview.vue' -const props = defineProps<{ dirty?: boolean; pending?: PendingHid | null }>() +const props = defineProps<{ dirty?: boolean }>() const emit = defineEmits<{ close: []; applied: []; discard: [] }>() const { t } = useI18n(), store = useConfigStore() -const draft = ref(props.pending ? JSON.parse(JSON.stringify(props.pending.selection)) : selectionFrom(store.hid)) +const draft = ref(selectionFrom(store.hid)) const valid = ref(false), busy = ref(false), error = ref(''), applied = ref(false), loaded = ref(false) const acknowledged = ref(!props.dirty), uncertain = ref(false) const pairingStarted = ref(false), autoPair = ref(false), hasApplied = ref(false) const connectionStarted = ref(Date.now()), connectionTimedOut = ref(false) const disabledUsb = ref([]) -const autoSubmitPending = ref(props.pending?.phase === 'selected') let disposed = false, closed = false async function cleanupPairing() { if (hasApplied.value && store.hid?.backend === 'bluetooth') { @@ -28,7 +27,7 @@ async function cleanupPairing() { } } onUnmounted(() => { - disposed = true; autoPair.value = false; autoSubmitPending.value = false + disposed = true; autoPair.value = false if (!closed && !busy.value) void cleanupPairing() }) const backend = computed(() => applied.value ? store.hid?.backend : undefined) @@ -36,7 +35,6 @@ const active = computed(() => applied.value && !busy.value) const { status, bluetooth, error: statusError, restart } = useHidConnection(active, backend) watch(status, () => { connectionTimedOut.value = Date.now() - connectionStarted.value >= 120000 }) const ready = computed(() => store.hid?.backend === 'none' || (store.hid?.backend === 'bluetooth' ? bluetooth.value?.ready : status.value?.online)) -function remember(phase: PendingHid['phase']) { if (props.pending) writePendingHid({ selection: draft.value, phase }) } async function readConfig(path: string): Promise { const controller = new AbortController() const timeout = setTimeout(() => controller.abort(), 5000) @@ -61,14 +59,12 @@ async function refreshConfigs() { } async function apply() { if (disposed || busy.value || !valid.value || !loaded.value) return - autoSubmitPending.value = false busy.value = true; error.value = ''; uncertain.value = false - remember('applying') const controller = new AbortController() const timeout = setTimeout(() => controller.abort(), 30000) try { await store.updateHid(deviceRequest(draft.value), controller.signal) - applied.value = true; hasApplied.value = true; connectionStarted.value = Date.now(); remember('applied'); autoPair.value = draft.value.backend === 'bluetooth' + applied.value = true; hasApplied.value = true; connectionStarted.value = Date.now(); autoPair.value = draft.value.backend === 'bluetooth' await refreshConfigs() } catch (e) { error.value = e instanceof Error ? e.message : String(e) @@ -99,16 +95,6 @@ watch(() => bluetooth.value?.initialized, initialized => { void action('pair') } }) -function applyPending() { - if (!autoSubmitPending.value || !valid.value || !loaded.value || disposed) return - if (props.pending && JSON.stringify(deviceRequest(draft.value)) !== JSON.stringify(deviceRequest(props.pending.selection))) { - autoSubmitPending.value = false - error.value = t('hidGuide.resumeRetry') - return - } - void apply() -} -watch(valid, applyPending) async function edit() { if (store.hid?.backend === 'bluetooth' && bluetooth.value?.initialized) { await action('close') @@ -123,7 +109,6 @@ async function close() { await action('close') if (error.value) return } - if (props.pending) sessionStorage.removeItem(pendingHidKey) closed = true emit('close') } @@ -131,19 +116,12 @@ async function load() { loaded.value = false; error.value = '' try { await refreshHid() - if (!props.pending) draft.value = selectionFrom(store.hid) + draft.value = selectionFrom(store.hid) if (store.hid?.backend === 'otg') { 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) } - if (props.pending && props.pending.phase !== 'selected') { - if (store.hid && matchesSelection(store.hid, props.pending.selection)) { - applied.value = true; hasApplied.value = true; remember('applied') - // The previous reset may have succeeded. Resume observation, never clear again. - } else error.value = t('hidGuide.resumeRetry') - } loaded.value = true - applyPending() } catch (e) { error.value = e instanceof Error ? e.message : String(e) } } onMounted(load) @@ -178,8 +156,8 @@ onMounted(load) {{ t('hidGuide.resetWarning') }} {{ error }} - {{ t(applied ? ready ? 'hidGuide.done' : 'hidGuide.later' : props.pending ? 'hidGuide.configureLater' : 'common.cancel') }} - {{ t('hidGuide.checkConnection') }} + {{ t(applied ? ready ? 'hidGuide.done' : 'hidGuide.later' : 'common.cancel') }} + {{ t('hidGuide.checkConnection') }} {{ t('common.refresh') }} {{ t(busy ? 'actionbar.applying' : 'common.apply') }} {{ t('hidGuide.reconfigure') }} diff --git a/web/src/components/HidDriverForm.vue b/web/src/components/HidDriverForm.vue index 48e84285..def5d5f4 100644 --- a/web/src/components/HidDriverForm.vue +++ b/web/src/components/HidDriverForm.vue @@ -3,6 +3,8 @@ import { computed, ref, watch, onUnmounted } from 'vue' import { useI18n } from 'vue-i18n' import { Button } from '@/components/ui/button' import { Input } from '@/components/ui/input' +import { HoverCard, HoverCardContent, HoverCardTrigger } from '@/components/ui/hover-card' +import { Cable, RefreshCw } from 'lucide-vue-next' import { configApi } from '@/api' import { request } from '@/api/request' import type { BluetoothAdapter } from '@/types/bluetooth' @@ -52,13 +54,40 @@ onUnmounted(() => generation++) - - {{ t('hidGuide.driver') }} - + + + {{ t('hidGuide.driver') }} + + + + + + {{ t('hidGuide.wiringHelp') }} + + + + + + + + + + + + {{ t(`hidGuide.driver_${driver}`) }} - - + + {{ t('hidGuide.disabledHelp') }} {{ t(`hidGuide.device_${modelValue.backend}`) }} @@ -79,6 +108,5 @@ onUnmounted(() => generation++) {{ missing }} {{ error }} - {{ t('common.refresh') }} diff --git a/web/src/i18n/en-US.ts b/web/src/i18n/en-US.ts index 7479fb38..e1933f93 100644 --- a/web/src/i18n/en-US.ts +++ b/web/src/i18n/en-US.ts @@ -8,6 +8,8 @@ export default { "configure": "Configure driver", "reconfigure": "Reconfigure", "driver": "HID driver", + "wiringHelp": "Wiring guide", + "refreshDevices": "Refresh HID devices", "driver_otg": "USB OTG", "driver_ch9329": "CH9329", "driver_bluetooth": "Bluetooth HID", diff --git a/web/src/i18n/zh-CN.ts b/web/src/i18n/zh-CN.ts index 442a7e1b..6b953099 100644 --- a/web/src/i18n/zh-CN.ts +++ b/web/src/i18n/zh-CN.ts @@ -8,6 +8,8 @@ export default { "configure": "配置驱动", "reconfigure": "重新配置", "driver": "HID 驱动", + "wiringHelp": "接线示意", + "refreshDevices": "刷新 HID 设备", "driver_otg": "USB OTG", "driver_ch9329": "CH9329", "driver_bluetooth": "蓝牙 HID", diff --git a/web/src/lib/hidGuide.ts b/web/src/lib/hidGuide.ts index cfdcbc69..ee6fb0dc 100644 --- a/web/src/lib/hidGuide.ts +++ b/web/src/lib/hidGuide.ts @@ -57,17 +57,3 @@ export function hidDeviceStage(status: HidDeviceStatus | null | undefined, bluet if (status.backend === 'otg' && status.error_code === 'udc_not_configured') return 'waiting' return status.online ? 'ready' : 'waiting' } -export const pendingHidKey = 'one-kvm.pending-hid.v1' -export interface PendingHid { selection: HidSelection; phase: 'selected' | 'applying' | 'applied' } -export function readPendingHid(): PendingHid | null { - try { - const value = JSON.parse(sessionStorage.getItem(pendingHidKey) ?? 'null') - if (value && ['selected', 'applying', 'applied'].includes(value.phase) - && ['otg', 'ch9329', 'bluetooth', 'none'].includes(value.selection?.backend) - && typeof value.selection.otg_udc === 'string' && typeof value.selection.ch9329_port === 'string' - && typeof value.selection.ch9329_baudrate === 'number' - && typeof value.selection.bluetooth?.adapter === 'string' && typeof value.selection.bluetooth?.name === 'string') return value - } catch { /* Invalid or unavailable session storage is not an applied configuration. */ } - return null -} -export function writePendingHid(value: PendingHid) { sessionStorage.setItem(pendingHidKey, JSON.stringify(value)) } diff --git a/web/src/views/SetupView.vue b/web/src/views/SetupView.vue index 865d2a72..8013b4e4 100644 --- a/web/src/views/SetupView.vue +++ b/web/src/views/SetupView.vue @@ -1,6 +1,6 @@
{{ t('hidGuide.resetWarning') }}
{{ error }}
{{ t('hidGuide.disabledHelp') }}
{{ missing }}