ADD: Action mapping
Including basic key capturing in renderer
This commit is contained in:
@@ -7,7 +7,7 @@
|
||||
@click="toggle = !toggle"
|
||||
>
|
||||
<component :is="iconComponent" v-if="iconComponent" class="mr-2 size-4" />
|
||||
<h2 class="py-4 text-sm">{{ title }}<slot name="title" /></h2>
|
||||
<h2 class="flex flex-1 items-center py-4 text-sm">{{ title }}<slot name="title" /></h2>
|
||||
<Switch
|
||||
v-if="showToggle"
|
||||
:checked="toggle"
|
||||
|
||||
105
src/renderer/src/components/config/actions/ActionCard.vue
Normal file
105
src/renderer/src/components/config/actions/ActionCard.vue
Normal file
@@ -0,0 +1,105 @@
|
||||
<template>
|
||||
<div class="overflow-hidden rounded-lg border border-zinc-800 bg-zinc-900/50">
|
||||
<div class="p-4">
|
||||
<span class="font-mono text-sm text-muted-foreground"
|
||||
>Action{{ actionIndex ? ` ${actionIndex}` : '' }}:</span
|
||||
>
|
||||
<span class="float-end mx-2 w-4 cursor-grab">
|
||||
<GripHorizontal class="action-handle mb-0.5 inline-block size-4 text-zinc-600" />
|
||||
</span>
|
||||
<Popover v-model:open="open">
|
||||
<PopoverTrigger as-child>
|
||||
<Button
|
||||
ref="comboboxButton"
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
:aria-expanded="open"
|
||||
class="my-2 w-full justify-between"
|
||||
>
|
||||
<ScrambleText :text="value ? actionOptions[value].label : 'Select an action...'" />
|
||||
<ChevronsUpDown class="ml-2 size-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent class="p-0" :style="{ width: `${width * 1.125}px` }">
|
||||
<Command>
|
||||
<CommandInput class="h-9" placeholder="Search actions..." />
|
||||
<CommandEmpty>
|
||||
<ScrambleText scramble-on-mount text="No actions found." />
|
||||
</CommandEmpty>
|
||||
<CommandList>
|
||||
<CommandGroup>
|
||||
<CommandItem
|
||||
v-for="(action, key) in actionOptions"
|
||||
:key="key"
|
||||
:value="action"
|
||||
@select="
|
||||
() => {
|
||||
value = key
|
||||
open = false
|
||||
}
|
||||
"
|
||||
>
|
||||
{{ action.label }}
|
||||
<Check
|
||||
:class="cn('ml-auto h-4 w-4', value === key ? 'opacity-100' : 'opacity-0')"
|
||||
/>
|
||||
</CommandItem>
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
<Separator />
|
||||
<component :is="actionOptions[value]?.component ? actionOptions[value]?.component : WIP" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import WIP from '@renderer/components/WIP.vue'
|
||||
import { Popover, PopoverTrigger, PopoverContent } from '@renderer/components/ui/popover'
|
||||
import { Button } from '@renderer/components/ui/button'
|
||||
import { Separator } from '@renderer/components/ui/separator'
|
||||
import {
|
||||
Command,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
CommandList
|
||||
} from '@renderer/components/ui/command'
|
||||
import { ref } from 'vue'
|
||||
import { cn } from '@renderer/lib/utils'
|
||||
import SendKeyAction from '@renderer/components/config/actions/SendKeyAction.vue'
|
||||
import SendStringAction from '@renderer/components/config/actions/SendStringAction.vue'
|
||||
import ScrambleText from '@renderer/components/common/ScrambleText.vue'
|
||||
import { ChevronsUpDown, Check, GripHorizontal } from 'lucide-vue-next'
|
||||
import { useElementSize } from '@vueuse/core'
|
||||
|
||||
defineProps({
|
||||
actionIndex: {
|
||||
type: Number,
|
||||
required: false
|
||||
}
|
||||
})
|
||||
|
||||
const actionOptions = ref({
|
||||
sendKey: { label: 'Press Key or Combination', component: SendKeyAction },
|
||||
sendString: { label: 'Type a String', component: SendStringAction },
|
||||
sendMouse: { label: 'Move, Scroll or Click', component: 'SendMouseAction' },
|
||||
sendGamepad: { label: 'Send a Gamepad Input', component: 'SendGamepadAction' },
|
||||
sendMidi: { label: 'Send a MIDI Message', component: 'SendMidiAction' },
|
||||
sendOsc: { label: 'Send an OSC Message', component: 'SendOscAction' },
|
||||
sendSerial: { label: 'Send a Serial Message', component: 'SendSerialAction' },
|
||||
controlMedia: { label: 'Control Media Playback', component: 'ControlMediaAction' },
|
||||
controlSystem: { label: 'Control your OS', component: 'ControlSystemAction' },
|
||||
runProgram: { label: 'Start a Program', component: 'RunProgramAction' },
|
||||
changeProfile: { label: 'Change Device Profile', component: 'ChangeProfileAction' }
|
||||
})
|
||||
|
||||
const comboboxButton = ref(null)
|
||||
const { width } = useElementSize(comboboxButton)
|
||||
|
||||
const open = ref(false)
|
||||
const value = ref('')
|
||||
</script>
|
||||
42
src/renderer/src/components/config/actions/ActionGroup.vue
Normal file
42
src/renderer/src/components/config/actions/ActionGroup.vue
Normal file
@@ -0,0 +1,42 @@
|
||||
<template>
|
||||
<div class="flex flex-col gap-2">
|
||||
<draggable
|
||||
key="actionsDraggable"
|
||||
class="flex flex-col gap-2"
|
||||
group="keyActions"
|
||||
item-key="id"
|
||||
handle=".action-handle"
|
||||
:list="actions"
|
||||
v-bind="dragOptions"
|
||||
>
|
||||
<template #item="dragAction">
|
||||
<div :key="dragAction.element.id">
|
||||
<ActionCard :action-index="dragAction.index + 1" />
|
||||
</div>
|
||||
</template>
|
||||
</draggable>
|
||||
<button
|
||||
class="flex flex-1 items-center justify-center rounded-lg border border-zinc-800 bg-zinc-900/50 p-2 text-sm text-muted-foreground hover:bg-zinc-800 hover:text-zinc-200"
|
||||
>
|
||||
<Plus class="mr-2" /> Add an action
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Plus } from 'lucide-vue-next'
|
||||
import ActionCard from '@renderer/components/config/actions/ActionCard.vue'
|
||||
import draggable from 'vuedraggable'
|
||||
import { ref } from 'vue'
|
||||
defineProps({
|
||||
actions: {
|
||||
type: Array,
|
||||
required: true
|
||||
}
|
||||
})
|
||||
const dragOptions = ref({
|
||||
ghostClass: 'ghost',
|
||||
animation: 150,
|
||||
direction: 'vertical'
|
||||
})
|
||||
</script>
|
||||
39
src/renderer/src/components/config/actions/SendKeyAction.vue
Normal file
39
src/renderer/src/components/config/actions/SendKeyAction.vue
Normal file
@@ -0,0 +1,39 @@
|
||||
<template>
|
||||
<div class="flex flex-col p-4">
|
||||
<Button
|
||||
class="flex-1"
|
||||
:class="{ 'bg-orange-600 hover:bg-orange-500': isCapturing }"
|
||||
@click="toggleCapture"
|
||||
>⬤
|
||||
{{ isCapturing ? 'Capturing Keyboard Input' : 'Capture Keyboard Input' }}
|
||||
</Button>
|
||||
<div class="mt-6 text-center font-mono text-sm text-muted-foreground">
|
||||
Key: {{ lastEvent?.key }} | Code: {{ lastEvent?.keyCode }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { Button } from '@renderer/components/ui/button'
|
||||
import { ref, Ref } from 'vue'
|
||||
|
||||
const isCapturing = ref(false)
|
||||
const keydownListener = (e: KeyboardEvent) => {
|
||||
lastEvent.value = e
|
||||
}
|
||||
const keyupListener = (e: KeyboardEvent) => {
|
||||
lastEvent.value = e
|
||||
}
|
||||
|
||||
const toggleCapture = () => {
|
||||
isCapturing.value = !isCapturing.value
|
||||
// TODO: Do this in the main process
|
||||
if (isCapturing.value) {
|
||||
window.addEventListener('keydown', keydownListener)
|
||||
window.addEventListener('keyup', keyupListener)
|
||||
} else {
|
||||
window.removeEventListener('keydown', keydownListener)
|
||||
window.removeEventListener('keyup', keyupListener)
|
||||
}
|
||||
}
|
||||
const lastEvent: Ref<KeyboardEvent | null> = ref(null)
|
||||
</script>
|
||||
@@ -0,0 +1,8 @@
|
||||
<template>
|
||||
<div class="p-4">
|
||||
<Input type="text" placeholder="String to be typed" />
|
||||
</div>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { Input } from '@renderer/components/ui/input'
|
||||
</script>
|
||||
@@ -1,92 +1,62 @@
|
||||
<template>
|
||||
<ConfigSection title="Key Mapping" :icon-component="PlusSquare">
|
||||
<template #title
|
||||
><span class="text-zinc-500"> ({{ store.selectedKey }})</span></template
|
||||
<ConfigSection :title="`${store.selectedKey} Pressed`" :icon-component="PanelBottomClose">
|
||||
<template #title>
|
||||
<span class="text-zinc-500"> ({{ actionsPressed.length }})</span></template
|
||||
>
|
||||
<div class="my-4 px-8">
|
||||
<span class="font-mono text-sm text-muted-foreground">Action:</span>
|
||||
<Popover v-model:open="open">
|
||||
<PopoverTrigger as-child>
|
||||
<Button
|
||||
ref="comboboxButton"
|
||||
variant="outline"
|
||||
role="combobox"
|
||||
:aria-expanded="open"
|
||||
class="my-2 w-full justify-between"
|
||||
>
|
||||
<ScrambleText :text="value ? actionOptions[value] : 'Select an action...'" />
|
||||
<ChevronsUpDown class="ml-2 size-4 shrink-0 opacity-50" />
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent class="p-0" :style="{ width: $refs.comboboxButton?.$el.offsetWidth }">
|
||||
<Command>
|
||||
<CommandInput class="h-9" placeholder="Search actions..." />
|
||||
<CommandEmpty>
|
||||
<ScrambleText scramble-on-mount text="No actions found." />
|
||||
</CommandEmpty>
|
||||
<CommandList>
|
||||
<CommandGroup>
|
||||
<CommandItem
|
||||
v-for="(action, key) in actionOptions"
|
||||
:key="key"
|
||||
:value="action"
|
||||
@select="
|
||||
() => {
|
||||
value = key
|
||||
open = false
|
||||
}
|
||||
"
|
||||
>
|
||||
{{ action }}
|
||||
<Check
|
||||
:class="cn('ml-auto h-4 w-4', value === key ? 'opacity-100' : 'opacity-0')"
|
||||
/>
|
||||
</CommandItem>
|
||||
</CommandGroup>
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
<WIP />
|
||||
<ActionGroup :actions="actionsPressed" class="p-2" />
|
||||
</ConfigSection>
|
||||
<ConfigSection :title="`${store.selectedKey} Released`" :icon-component="PanelBottomOpen">
|
||||
<template #title>
|
||||
<span class="text-zinc-500"> ({{ actionsReleased.length }})</span></template
|
||||
>
|
||||
<ActionGroup :actions="actionsReleased" class="p-2" />
|
||||
</ConfigSection>
|
||||
<ConfigSection :title="`${store.selectedKey} Held`" :icon-component="Clock2">
|
||||
<template #title>
|
||||
<span class="text-zinc-500"> ({{ actionsHeld.length }})</span></template
|
||||
>
|
||||
<ActionGroup :actions="actionsHeld" class="p-2" />
|
||||
</ConfigSection>
|
||||
</template>
|
||||
<script setup>
|
||||
import { PlusSquare, ChevronsUpDown, Check } from 'lucide-vue-next'
|
||||
import { PanelBottomClose, PanelBottomOpen, Clock2 } from 'lucide-vue-next'
|
||||
import ConfigSection from '@renderer/components/common/ConfigSection.vue'
|
||||
import WIP from '@renderer/components/WIP.vue'
|
||||
import { Popover, PopoverTrigger, PopoverContent } from '@renderer/components/ui/popover'
|
||||
import { Button } from '@renderer/components/ui/button'
|
||||
import {
|
||||
Command,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
CommandList
|
||||
} from '@renderer/components/ui/command'
|
||||
import { ref } from 'vue'
|
||||
import { cn } from '@renderer/lib/utils'
|
||||
import ScrambleText from '@renderer/components/common/ScrambleText.vue'
|
||||
import { useStore } from '@renderer/store'
|
||||
import { ref } from 'vue'
|
||||
import ActionGroup from '../actions/ActionGroup.vue'
|
||||
|
||||
const store = useStore()
|
||||
|
||||
const actionOptions = ref({
|
||||
sendKey: 'Press Key or Combination',
|
||||
sendString: 'Type a String',
|
||||
sendMouse: 'Move, Scroll or Click',
|
||||
sendGamepad: 'Send a Gamepad Input',
|
||||
sendMidi: 'Send a MIDI Message',
|
||||
sendOsc: 'Send an OSC Message',
|
||||
sendSerial: 'Send a Serial Message',
|
||||
controlMedia: 'Control Media Playback',
|
||||
controlSystem: 'Control your OS',
|
||||
runProgram: 'Start a Program'
|
||||
})
|
||||
|
||||
const comboboxButton = ref(null)
|
||||
|
||||
const open = ref(false)
|
||||
const value = ref('')
|
||||
const actionsPressed = ref([
|
||||
{
|
||||
id: '1'
|
||||
},
|
||||
{
|
||||
id: '2'
|
||||
},
|
||||
{
|
||||
id: '3'
|
||||
}
|
||||
])
|
||||
const actionsReleased = ref([
|
||||
{
|
||||
id: '4'
|
||||
},
|
||||
{
|
||||
id: '5'
|
||||
},
|
||||
{
|
||||
id: '6'
|
||||
}
|
||||
])
|
||||
const actionsHeld = ref([
|
||||
{
|
||||
id: '7'
|
||||
},
|
||||
{
|
||||
id: '8'
|
||||
},
|
||||
{
|
||||
id: '9'
|
||||
}
|
||||
])
|
||||
</script>
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
class="m-2 flex h-12 overflow-hidden rounded-lg transition-all"
|
||||
:class="{
|
||||
'border border-zinc-100 bg-zinc-300': selected,
|
||||
'border border-transparent bg-zinc-900/30 hover:border-zinc-900': !selected,
|
||||
'border border-zinc-800/50 bg-zinc-900/30': !selected,
|
||||
group: showHoverButtons
|
||||
}"
|
||||
>
|
||||
@@ -26,7 +26,7 @@
|
||||
class="h-full min-w-0 flex-1 rounded-lg bg-transparent pl-8 text-sm transition-all focus-visible:outline-none focus-visible:ring-0"
|
||||
:class="{
|
||||
'bg-zinc-300 font-semibold text-black hover:bg-zinc-200': selected,
|
||||
'text-muted-foreground hover:bg-zinc-900': !selected
|
||||
'text-muted-foreground hover:bg-zinc-800': !selected
|
||||
}"
|
||||
@blur="onNameInputBlur"
|
||||
/>
|
||||
@@ -35,7 +35,7 @@
|
||||
type="submit"
|
||||
:class="{
|
||||
'bg-zinc-300 text-black hover:bg-zinc-200': selected,
|
||||
'text-zinc-100 hover:bg-zinc-900': !selected
|
||||
'text-zinc-100 hover:bg-zinc-800': !selected
|
||||
}"
|
||||
class="flex aspect-square h-full shrink-0 items-center justify-center rounded-lg transition-all"
|
||||
>
|
||||
@@ -47,7 +47,7 @@
|
||||
v-else
|
||||
:class="{
|
||||
'bg-zinc-300 font-semibold text-black hover:bg-zinc-200': selected,
|
||||
'text-muted-foreground hover:bg-zinc-900': !selected
|
||||
'text-muted-foreground hover:bg-zinc-800': !selected
|
||||
}"
|
||||
class="flex-1 truncate rounded-lg pr-4 text-left text-sm transition-all"
|
||||
@click="!editing && $emit('select') && $refs.profileTitle.scramble()"
|
||||
@@ -56,7 +56,7 @@
|
||||
<GripHorizontal
|
||||
v-if="draggable"
|
||||
:class="{ 'text-zinc-600': selected, 'text-muted-foreground': !selected }"
|
||||
class="mb-0.5 inline-block size-4 opacity-0 transition-all group-hover:opacity-100"
|
||||
class="profile-handle mb-0.5 inline-block size-4 opacity-0 transition-all group-hover:opacity-100"
|
||||
/>
|
||||
</span>
|
||||
<ScrambleText
|
||||
@@ -74,7 +74,7 @@
|
||||
v-if="nameEditable"
|
||||
:class="{
|
||||
'bg-zinc-300 text-black hover:bg-zinc-200': selected,
|
||||
'text-zinc-100 hover:bg-zinc-900': !selected,
|
||||
'text-zinc-100 hover:bg-zinc-800': !selected,
|
||||
'group-focus-within:w-12 group-hover:w-12': !editing
|
||||
}"
|
||||
class="flex w-0 shrink-0 items-center justify-center rounded-lg transition-all"
|
||||
@@ -85,7 +85,7 @@
|
||||
<button
|
||||
:class="{
|
||||
'bg-zinc-300 text-black hover:bg-zinc-200': selected,
|
||||
'text-zinc-100 hover:bg-zinc-900': !selected,
|
||||
'text-zinc-100 hover:bg-zinc-800': !selected,
|
||||
'group-focus-within:w-12 group-hover:w-12': !editing,
|
||||
'rounded-l-lg': !nameEditable
|
||||
}"
|
||||
@@ -121,7 +121,7 @@
|
||||
<button
|
||||
:class="{
|
||||
'bg-zinc-300 text-black hover:bg-zinc-200': selected,
|
||||
'text-zinc-100 hover:bg-zinc-900': !selected,
|
||||
'text-zinc-100 hover:bg-zinc-800': !selected,
|
||||
'group-focus-within:w-12 group-hover:w-12': !editing
|
||||
}"
|
||||
class="flex w-0 shrink-0 items-center justify-center rounded-lg transition-all"
|
||||
|
||||
@@ -61,6 +61,7 @@
|
||||
item-key="name"
|
||||
:list="store.profileCategories"
|
||||
v-bind="dragOptions"
|
||||
handle=".category-handle"
|
||||
@start="drag = true"
|
||||
@end="drag = false"
|
||||
@change="onCategoryDrop"
|
||||
@@ -79,45 +80,44 @@
|
||||
({{ dragCategory.element.profiles?.length || 0 }})</span
|
||||
>
|
||||
<span class="float-right mx-4 w-4 cursor-grab text-zinc-600">
|
||||
<GripHorizontal class="mb-0.5 inline-block size-4" />
|
||||
<GripHorizontal class="category-handle mb-0.5 inline-block size-4" />
|
||||
</span>
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent>
|
||||
<TransitionGroup>
|
||||
<draggable
|
||||
key="profilesDraggable"
|
||||
group="profiles"
|
||||
item-key="id"
|
||||
:list="dragCategory.element.profiles"
|
||||
v-bind="dragOptions"
|
||||
@start="drag = true"
|
||||
@end="drag = false"
|
||||
@change="(event) => onProfileDrop(event, dragCategory.index)"
|
||||
>
|
||||
<template #header>
|
||||
<div class="hideable-header m-2 flex h-12 items-center justify-center">
|
||||
<MoreHorizontal class="w-4 text-zinc-600" />
|
||||
</div>
|
||||
</template>
|
||||
<template #item="dragProfile">
|
||||
<div :key="dragProfile.element.name">
|
||||
<ProfileButton
|
||||
:profile="dragProfile.element"
|
||||
:show-hover-buttons="!drag"
|
||||
:selected="store.selectedProfile?.id === dragProfile.element.id"
|
||||
@select="
|
||||
() => {
|
||||
store.selectProfile(dragProfile.element.id)
|
||||
showProfileConfig = true
|
||||
}
|
||||
"
|
||||
@duplicate="store.duplicateProfile(dragProfile.element.id)"
|
||||
@delete="store.removeProfile(dragProfile.element.id)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</draggable>
|
||||
</TransitionGroup>
|
||||
<draggable
|
||||
key="profilesDraggable"
|
||||
group="profiles"
|
||||
item-key="id"
|
||||
:list="dragCategory.element.profiles"
|
||||
v-bind="dragOptions"
|
||||
handle=".profile-handle"
|
||||
@start="drag = true"
|
||||
@end="drag = false"
|
||||
@change="(event) => onProfileDrop(event, dragCategory.index)"
|
||||
>
|
||||
<template #header>
|
||||
<div class="hideable-header m-2 flex h-12 items-center justify-center">
|
||||
<MoreHorizontal class="w-4 text-zinc-600" />
|
||||
</div>
|
||||
</template>
|
||||
<template #item="dragProfile">
|
||||
<div :key="dragProfile.element.name">
|
||||
<ProfileButton
|
||||
:profile="dragProfile.element"
|
||||
:show-hover-buttons="!drag"
|
||||
:selected="store.selectedProfile?.id === dragProfile.element.id"
|
||||
@select="
|
||||
() => {
|
||||
store.selectProfile(dragProfile.element.id)
|
||||
showProfileConfig = true
|
||||
}
|
||||
"
|
||||
@duplicate="store.duplicateProfile(dragProfile.element.id)"
|
||||
@delete="store.removeProfile(dragProfile.element.id)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
</draggable>
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user