diff --git a/web/src/components/HidConfigPopover.vue b/web/src/components/HidConfigPopover.vue index f8cfda3d..a15fc674 100644 --- a/web/src/components/HidConfigPopover.vue +++ b/web/src/components/HidConfigPopover.vue @@ -13,10 +13,10 @@ import { PopoverContent, PopoverTrigger, } 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 { useConfigStore } from '@/stores/config' -import { HidBackend } from '@/types/generated' +import { HidBackend, OtgHidProfile } from '@/types/generated' const props = defineProps<{ open: boolean @@ -64,18 +64,63 @@ 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 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() { - if (configStore.hid?.backend === HidBackend.Bluetooth) return + if (hidControlsDisabled.value || configStore.hid?.backend === HidBackend.Bluetooth) return const newMode = props.mouseMode === 'absolute' ? 'relative' : 'absolute' emit('update:mouseMode', newMode) // Update backend config + hidSaving.value = true configStore.updateHid({ mouse_absolute: newMode === 'absolute', }).catch(_e => { console.info('[HidConfig] Failed to update mouse mode') toast.error(t('config.updateFailed')) + }).finally(() => { + hidSaving.value = false }) } @@ -93,7 +138,7 @@ watch(() => props.open, (open) => { if (!open) return mouseThrottle.value = loadMouseMoveSendIntervalFromStorage() showCursor.value = localStorage.getItem('hidShowCursor') !== 'false' - void configStore.refreshHid().catch(() => undefined) + if (!hidSaving.value) void configStore.refreshHid().catch(() => undefined) }) @@ -136,7 +181,7 @@ watch(() => props.open, (open) => {
{{ t('actionbar.macosDragRequiresBoth') }}
+
+
{{ t('hidGuide.disabledHelp') }}