mirror of
https://github.com/Macrame-App/Macrame
synced 2025-12-29 07:19:26 +00:00
WIP: MacroRecorder. Component + parts + subcomponents for capturing keyboard input for macro recording.
This commit is contained in:
parent
f4a4bc5c4a
commit
68a2eda491
14 changed files with 785 additions and 109 deletions
|
|
@ -1,109 +0,0 @@
|
||||||
<template>
|
|
||||||
<div class="macro-input">
|
|
||||||
<div class="macro-input__output" ref="macroOutput"></div>
|
|
||||||
<input class="macro-input__input" type="text" ref="macroInput" @keydown.prevent="keyDown" />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup>
|
|
||||||
import { onMounted, ref } from 'vue'
|
|
||||||
|
|
||||||
const macroOutput = ref(null)
|
|
||||||
const macroInput = ref(null)
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
macroInput.value.focus()
|
|
||||||
})
|
|
||||||
|
|
||||||
const keyDown = (e) => {
|
|
||||||
console.log(e)
|
|
||||||
|
|
||||||
const modKeys = {
|
|
||||||
Control: 'Ctrl',
|
|
||||||
Shift: 'Shift',
|
|
||||||
Alt: 'Alt',
|
|
||||||
Meta: 'Win',
|
|
||||||
}
|
|
||||||
|
|
||||||
const specialKeys = {
|
|
||||||
PageUp: 'PgUp',
|
|
||||||
PageDown: 'PgDn',
|
|
||||||
ScrollLock: 'Scr Lk',
|
|
||||||
}
|
|
||||||
|
|
||||||
let key = e.key
|
|
||||||
|
|
||||||
if (e.shiftKey) {
|
|
||||||
key = e.key.toLowerCase()
|
|
||||||
}
|
|
||||||
|
|
||||||
const newKeyEl = document.createElement('kbd')
|
|
||||||
|
|
||||||
if (e.code === 'Space') {
|
|
||||||
newKeyEl.innerHTML = 'Space'
|
|
||||||
} else if (e.location === 1 && Object.keys(modKeys).includes(key)) {
|
|
||||||
newKeyEl.innerHTML = '<sup>left</sup> ' + (modKeys[key] || key)
|
|
||||||
} else if (e.location === 2 && Object.keys(modKeys).includes(key)) {
|
|
||||||
newKeyEl.innerHTML = '<sup>right</sup> ' + (modKeys[key] || key)
|
|
||||||
} else if (e.location === 3) {
|
|
||||||
newKeyEl.innerHTML = '<sup>num</sup> ' + (modKeys[key] || key)
|
|
||||||
} else if (Object.keys(specialKeys).includes(key)) {
|
|
||||||
newKeyEl.innerHTML = specialKeys[key] || key
|
|
||||||
} else {
|
|
||||||
newKeyEl.innerHTML = key
|
|
||||||
}
|
|
||||||
|
|
||||||
macroOutput.value.appendChild(newKeyEl)
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style>
|
|
||||||
@reference "@/assets/main.css";
|
|
||||||
|
|
||||||
.macro-input {
|
|
||||||
@apply relative
|
|
||||||
w-full
|
|
||||||
h-96
|
|
||||||
my-4
|
|
||||||
rounded-lg
|
|
||||||
bg-slate-900/50
|
|
||||||
border
|
|
||||||
border-white/10
|
|
||||||
overflow-auto;
|
|
||||||
|
|
||||||
.macro-input__input,
|
|
||||||
.macro-input__output {
|
|
||||||
@apply absolute
|
|
||||||
inset-0
|
|
||||||
size-full;
|
|
||||||
}
|
|
||||||
|
|
||||||
.macro-input__output {
|
|
||||||
@apply flex
|
|
||||||
flex-wrap
|
|
||||||
items-start
|
|
||||||
gap-2
|
|
||||||
p-4
|
|
||||||
h-fit;
|
|
||||||
}
|
|
||||||
|
|
||||||
kbd {
|
|
||||||
@apply flex
|
|
||||||
items-center
|
|
||||||
gap-2
|
|
||||||
px-4 py-1
|
|
||||||
h-9
|
|
||||||
bg-white/10
|
|
||||||
font-sans
|
|
||||||
font-bold
|
|
||||||
text-lg
|
|
||||||
uppercase
|
|
||||||
rounded-md
|
|
||||||
border;
|
|
||||||
|
|
||||||
sup {
|
|
||||||
@apply text-xs font-light -ml-1.5 mt-1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
76
fe/src/components/macros/MacroRecorder.vue
Normal file
76
fe/src/components/macros/MacroRecorder.vue
Normal file
|
|
@ -0,0 +1,76 @@
|
||||||
|
<template>
|
||||||
|
<div class="macro-recorder">
|
||||||
|
<!-- Recorder buttons -->
|
||||||
|
<RecorderHeader />
|
||||||
|
|
||||||
|
<!-- Recorder interface container -->
|
||||||
|
<div
|
||||||
|
:class="`recorder-interface__container ${macroRecorder.state.record && 'record'} ${macroRecorder.state.edit && 'edit'}`"
|
||||||
|
>
|
||||||
|
<!-- Shows the macro steps as kbd elements with delay and spacers-->
|
||||||
|
<RecorderOutput />
|
||||||
|
<!-- Input for recording macro steps -->
|
||||||
|
<RecorderInput />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<RecorderFooter />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
// TODO:
|
||||||
|
// X refactor filtering the keys
|
||||||
|
// X add keyup functionality
|
||||||
|
// X add delay between steps
|
||||||
|
// X restyle keys and delay elements
|
||||||
|
// X add lines between steps
|
||||||
|
// X record macro to object
|
||||||
|
// X refactor macro output based on object?
|
||||||
|
// X Make sure keydown is not spamming steps
|
||||||
|
// X Make record button work as a toggle
|
||||||
|
// X Make edit button work
|
||||||
|
// X Make fixed/custom delay work
|
||||||
|
// X Refactor into multiple components and state store
|
||||||
|
// X Make edit key function
|
||||||
|
// X Make edit delay function
|
||||||
|
// X Make delete key function
|
||||||
|
// X Make sure delay is paused when not recording.
|
||||||
|
// X Refactor macro recorder parts.
|
||||||
|
// X X Layout parts should be parts, smaller parts should be components.
|
||||||
|
// X Make reset function
|
||||||
|
// - Make insert button, menu and function
|
||||||
|
|
||||||
|
import RecorderOutput from './parts/RecorderOutput.vue'
|
||||||
|
import RecorderInput from './parts/RecorderInput.vue'
|
||||||
|
|
||||||
|
import { useMacroRecorderStore } from '@/stores/macrorecorder'
|
||||||
|
import RecorderHeader from './parts/RecorderHeader.vue'
|
||||||
|
import RecorderFooter from './parts/RecorderFooter.vue'
|
||||||
|
|
||||||
|
const macroRecorder = useMacroRecorderStore()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
@reference "@/assets/main.css";
|
||||||
|
|
||||||
|
.recorder-interface__container {
|
||||||
|
@apply relative
|
||||||
|
w-full
|
||||||
|
h-96
|
||||||
|
my-4
|
||||||
|
rounded-lg
|
||||||
|
bg-slate-900/50
|
||||||
|
border-2
|
||||||
|
border-white/10
|
||||||
|
overflow-auto
|
||||||
|
transition-colors;
|
||||||
|
|
||||||
|
&.record {
|
||||||
|
@apply border-rose-300 bg-rose-950/50;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.edit {
|
||||||
|
@apply border-sky-300 bg-sky-900/50;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
39
fe/src/components/macros/components/DelaySpan.vue
Normal file
39
fe/src/components/macros/components/DelaySpan.vue
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
<template>
|
||||||
|
<span :class="`delay ${active ? 'active' : ''}`">
|
||||||
|
{{ value < 10000 ? value + 'ms' : '>10s' }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
defineProps({
|
||||||
|
value: Number,
|
||||||
|
active: Boolean,
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
@reference "@/assets/main.css";
|
||||||
|
|
||||||
|
span.delay {
|
||||||
|
@apply flex
|
||||||
|
items-center
|
||||||
|
px-2 py-1
|
||||||
|
bg-slate-500
|
||||||
|
border
|
||||||
|
border-slate-400
|
||||||
|
text-slate-950
|
||||||
|
font-semibold
|
||||||
|
rounded-sm
|
||||||
|
text-sm
|
||||||
|
cursor-default;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit span.delay {
|
||||||
|
@apply cursor-pointer;
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&.active {
|
||||||
|
@apply bg-lime-700 border-lime-500 text-lime-200;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
38
fe/src/components/macros/components/DeleteKeyDialog.vue
Normal file
38
fe/src/components/macros/components/DeleteKeyDialog.vue
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
<template>
|
||||||
|
<div id="delete-key-dialog" class="dialog__content">
|
||||||
|
<h4 class="text-slate-50 mb-4">Delete key</h4>
|
||||||
|
<div class="flex justify-center w-full mb-4">
|
||||||
|
<MacroKey v-if="keyObj" :key-obj="keyObj" />
|
||||||
|
</div>
|
||||||
|
<p class="text-sm text-slate-300">Are you sure you want to delete this key?</p>
|
||||||
|
<div class="flex justify-end gap-2 mt-6">
|
||||||
|
<ButtonComp variant="danger" size="sm" @click="macroRecorder.deleteEditKey()">
|
||||||
|
Delete key
|
||||||
|
</ButtonComp>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import ButtonComp from '@/components/base/ButtonComp.vue'
|
||||||
|
import { useMacroRecorderStore } from '@/stores/macrorecorder'
|
||||||
|
import { onMounted, ref } from 'vue'
|
||||||
|
import MacroKey from './MacroKey.vue'
|
||||||
|
import { filterKey } from '@/services/MacroRecordService'
|
||||||
|
|
||||||
|
const macroRecorder = useMacroRecorderStore()
|
||||||
|
|
||||||
|
const keyObj = ref(null)
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
keyObj.value = filterKey(macroRecorder.getEditKey())
|
||||||
|
console.log(macroRecorder.getEditKey());
|
||||||
|
console.log(keyObj.value);
|
||||||
|
console.log('---------');
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
@reference "@/assets/main.css";
|
||||||
|
</style>
|
||||||
|
'
|
||||||
57
fe/src/components/macros/components/EditDelayDialog.vue
Normal file
57
fe/src/components/macros/components/EditDelayDialog.vue
Normal file
|
|
@ -0,0 +1,57 @@
|
||||||
|
<template>
|
||||||
|
<div id="edit-delay-dialog" class="dialog__content">
|
||||||
|
<h4 class="text-slate-50 mb-4">Edit delay</h4>
|
||||||
|
<div v-if="editable.delay.value" class="flex justify-center">
|
||||||
|
<DelaySpan class="!text-lg" :value="editable.delay.value" />
|
||||||
|
</div>
|
||||||
|
<form class="grid gap-4 mt-6" submit.prevent>
|
||||||
|
<div v-if="editable.newDelay.value">
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
max="3600000"
|
||||||
|
step="10"
|
||||||
|
v-model="editable.newDelay.value"
|
||||||
|
autofocus
|
||||||
|
/>
|
||||||
|
<span>ms</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-end">
|
||||||
|
<ButtonComp variant="primary" size="sm" @click.prevent="changeDelay()">
|
||||||
|
Change delay
|
||||||
|
</ButtonComp>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import ButtonComp from '@/components/base/ButtonComp.vue'
|
||||||
|
import { reactive, onMounted } from 'vue'
|
||||||
|
import { useMacroRecorderStore } from '@/stores/macrorecorder'
|
||||||
|
import DelaySpan from './DelaySpan.vue'
|
||||||
|
|
||||||
|
const macroRecorder = useMacroRecorderStore()
|
||||||
|
|
||||||
|
const editable = reactive({
|
||||||
|
delay: {},
|
||||||
|
newDelay: { value: 0 },
|
||||||
|
})
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
editable.delay = macroRecorder.getEditDelay()
|
||||||
|
editable.newDelay.value = editable.delay.value
|
||||||
|
console.log(editable)
|
||||||
|
})
|
||||||
|
|
||||||
|
const changeDelay = () => {
|
||||||
|
if (!editable.newDelay.value) return
|
||||||
|
|
||||||
|
macroRecorder.recordStep(editable.newDelay.value, false, macroRecorder.state.editDelay)
|
||||||
|
macroRecorder.state.editDelay = false
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
@reference "@/assets/main.css";
|
||||||
|
</style>
|
||||||
103
fe/src/components/macros/components/EditKeyDialog.vue
Normal file
103
fe/src/components/macros/components/EditKeyDialog.vue
Normal file
|
|
@ -0,0 +1,103 @@
|
||||||
|
<template>
|
||||||
|
<div id="edit-key-dialog" class="dialog__content">
|
||||||
|
<h4 class="text-slate-50 mb-4">Press a key</h4>
|
||||||
|
<div class="flex justify-center" @click="$refs.newKeyInput.focus()">
|
||||||
|
<MacroKey
|
||||||
|
v-if="editable.key.keyObj"
|
||||||
|
:key-obj="editable.key.keyObj"
|
||||||
|
:direction="editable.key.direction"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<template v-if="typeof editable.newKey.keyObj === 'object'">
|
||||||
|
<span class="px-4 flex items-center text-white"> >>> </span>
|
||||||
|
<MacroKey :key-obj="editable.newKey.keyObj" :direction="editable.newKey.direction" />
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
<form class="grid gap-4" submit.prevent>
|
||||||
|
<input
|
||||||
|
class="size-0 opacity-0"
|
||||||
|
type="text"
|
||||||
|
min="0"
|
||||||
|
max="1"
|
||||||
|
ref="newKeyInput"
|
||||||
|
placeholder="New key"
|
||||||
|
autofocus
|
||||||
|
@keydown.prevent="handleNewKey($event)"
|
||||||
|
/>
|
||||||
|
<div class="flex gap-2 justify-center">
|
||||||
|
<ButtonComp
|
||||||
|
variant="secondary"
|
||||||
|
:class="editable.newKey.direction === 'down' ? 'selected' : ''"
|
||||||
|
size="sm"
|
||||||
|
@click.prevent="handleNewDirection('down')"
|
||||||
|
>
|
||||||
|
↓ Down
|
||||||
|
</ButtonComp>
|
||||||
|
<ButtonComp
|
||||||
|
variant="secondary"
|
||||||
|
:class="editable.newKey.direction === 'up' ? 'selected' : ''"
|
||||||
|
size="sm"
|
||||||
|
@click.prevent="handleNewDirection('up')"
|
||||||
|
>
|
||||||
|
↑ Up
|
||||||
|
</ButtonComp>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-end">
|
||||||
|
<ButtonComp variant="primary" size="sm" @click.prevent="changeKey()">
|
||||||
|
Change key
|
||||||
|
</ButtonComp>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import MacroKey from './MacroKey.vue'
|
||||||
|
import ButtonComp from '@/components/base/ButtonComp.vue'
|
||||||
|
import { useMacroRecorderStore } from '@/stores/macrorecorder'
|
||||||
|
import { filterKey } from '@/services/MacroRecordService'
|
||||||
|
|
||||||
|
import { reactive, ref, onMounted } from 'vue'
|
||||||
|
|
||||||
|
const editable = reactive({
|
||||||
|
key: {},
|
||||||
|
newKey: {},
|
||||||
|
})
|
||||||
|
|
||||||
|
const macroRecorder = useMacroRecorderStore()
|
||||||
|
|
||||||
|
const newKeyInput = ref(null)
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
editable.key = macroRecorder.getEditKey()
|
||||||
|
editable.newKey.direction = editable.key.direction
|
||||||
|
})
|
||||||
|
|
||||||
|
const handleNewKey = (e) => {
|
||||||
|
editable.newKey.e = e
|
||||||
|
editable.newKey.keyObj = filterKey(e)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleNewDirection = (direction) => {
|
||||||
|
editable.newKey.direction = direction
|
||||||
|
editable.newKey.keyObj = filterKey(editable.key)
|
||||||
|
}
|
||||||
|
|
||||||
|
const changeKey = () => {
|
||||||
|
macroRecorder.recordStep(
|
||||||
|
editable.newKey.e,
|
||||||
|
editable.newKey.direction,
|
||||||
|
macroRecorder.state.editKey,
|
||||||
|
)
|
||||||
|
|
||||||
|
macroRecorder.state.editKey = false
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
@reference "@/assets/main.css";
|
||||||
|
|
||||||
|
button.selected {
|
||||||
|
@apply ring-2 ring-offset-1 ring-sky-500 bg-sky-500;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
64
fe/src/components/macros/components/FixedDelayMenu.vue
Normal file
64
fe/src/components/macros/components/FixedDelayMenu.vue
Normal file
|
|
@ -0,0 +1,64 @@
|
||||||
|
<template>
|
||||||
|
<ContextMenu>
|
||||||
|
<template #trigger>
|
||||||
|
<ButtonComp variant="secondary" size="sm"> <IconAlarmFilled />Fixed delay </ButtonComp>
|
||||||
|
</template>
|
||||||
|
<template #content>
|
||||||
|
<ul>
|
||||||
|
<li @click="macroRecorder.changeDelay(0)">0ms</li>
|
||||||
|
<li @click="macroRecorder.changeDelay(15)">15ms</li>
|
||||||
|
<li @click="macroRecorder.changeDelay(50)">50ms</li>
|
||||||
|
<li @click="macroRecorder.changeDelay(100)">100ms</li>
|
||||||
|
<li>
|
||||||
|
<DialogComp>
|
||||||
|
<template #trigger>
|
||||||
|
<span>Custom delay</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template #content>
|
||||||
|
<h4 class="text-slate-50 mb-4">Custom delay</h4>
|
||||||
|
<form
|
||||||
|
class="grid gap-4 w-44"
|
||||||
|
@submit.prevent="macroRecorder.changeDelay(parseInt($refs.customDelayInput.value))"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<input
|
||||||
|
type="number"
|
||||||
|
step="10"
|
||||||
|
min="0"
|
||||||
|
max="3600000"
|
||||||
|
ref="customDelayInput"
|
||||||
|
placeholder="100"
|
||||||
|
/>
|
||||||
|
<span>ms</span>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-end">
|
||||||
|
<ButtonComp variant="primary" size="sm">Set custom delay</ButtonComp>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</template>
|
||||||
|
</DialogComp>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</template>
|
||||||
|
</ContextMenu>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import ContextMenu from '@/components/base/ContextMenu.vue'
|
||||||
|
import { IconAlarmFilled } from '@tabler/icons-vue'
|
||||||
|
import ButtonComp from '@/components/base/ButtonComp.vue'
|
||||||
|
import DialogComp from '@/components/base/DialogComp.vue'
|
||||||
|
|
||||||
|
import { useMacroRecorderStore } from '@/stores/macrorecorder'
|
||||||
|
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
const macroRecorder = useMacroRecorderStore()
|
||||||
|
|
||||||
|
const delayMenu = ref(false)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
@reference "@/assets/main.css";
|
||||||
|
</style>
|
||||||
41
fe/src/components/macros/components/InsertKeyDialog.vue
Normal file
41
fe/src/components/macros/components/InsertKeyDialog.vue
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
<template>
|
||||||
|
<div id="insert-key-dialog" class="dialog__content w-80">
|
||||||
|
<h4 class="text-slate-50 mb-4">Insert key {{ position }}</h4>
|
||||||
|
<div class="flex justify-center w-full mb-4">
|
||||||
|
<MacroKey v-if="keyObjs.selected" :key-obj="keyObjs.selected" />
|
||||||
|
<MacroKey v-if="keyObjs.adjacent" :key-obj="keyObjs.adjacent" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { onMounted, reactive, ref } from 'vue'
|
||||||
|
import { useMacroRecorderStore } from '@/stores/macrorecorder'
|
||||||
|
import MacroKey from './MacroKey.vue'
|
||||||
|
import { filterKey } from '@/services/MacroRecordService'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
position: String,
|
||||||
|
})
|
||||||
|
|
||||||
|
const macroRecorder = useMacroRecorderStore()
|
||||||
|
|
||||||
|
const keyObjs = reactive({
|
||||||
|
selected: null,
|
||||||
|
insert: null,
|
||||||
|
adjacent: null,
|
||||||
|
adjacentDelay: null,
|
||||||
|
})
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
keyObjs.selected = filterKey(macroRecorder.getEditKey())
|
||||||
|
|
||||||
|
const adjacentKey = macroRecorder.getAdjacentKey(props.position, true)
|
||||||
|
if (adjacentKey) keyObjs.adjacent = filterKey(adjacentKey.key)
|
||||||
|
if (adjacentKey.delay) keyObjs.adjacentDelay = adjacentKey.delay
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
@reference "@/assets/main.css";
|
||||||
|
</style>
|
||||||
77
fe/src/components/macros/components/MacroKey.vue
Normal file
77
fe/src/components/macros/components/MacroKey.vue
Normal file
|
|
@ -0,0 +1,77 @@
|
||||||
|
<template>
|
||||||
|
<kbd :class="active ? 'active' : ''">
|
||||||
|
<sup v-if="keyObj.loc">
|
||||||
|
{{ keyObj.loc }}
|
||||||
|
</sup>
|
||||||
|
<span :innerHTML="keyObj.str" />
|
||||||
|
<span class="dir">{{ dir.value === 'down' ? '↓' : '↑' }}</span>
|
||||||
|
</kbd>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { onMounted, reactive } from 'vue'
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
keyObj: Object,
|
||||||
|
direction: String,
|
||||||
|
active: Boolean,
|
||||||
|
})
|
||||||
|
|
||||||
|
const dir = reactive({
|
||||||
|
value: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (props.direction) dir.value = props.direction
|
||||||
|
else dir.value = props.keyObj.direction
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
@reference "@/assets/main.css";
|
||||||
|
|
||||||
|
kbd {
|
||||||
|
@apply flex
|
||||||
|
items-center
|
||||||
|
gap-2
|
||||||
|
pl-4 pr-2 py-1
|
||||||
|
h-9
|
||||||
|
bg-slate-700
|
||||||
|
font-sans
|
||||||
|
font-bold
|
||||||
|
text-lg
|
||||||
|
text-white
|
||||||
|
uppercase
|
||||||
|
rounded-md
|
||||||
|
border
|
||||||
|
border-slate-500
|
||||||
|
transition-all
|
||||||
|
shadow-slate-500;
|
||||||
|
box-shadow: 0 0.2rem 0 0.2rem var(--tw-shadow-color);
|
||||||
|
|
||||||
|
&:has(sup) {
|
||||||
|
@apply pl-2;
|
||||||
|
}
|
||||||
|
|
||||||
|
sup {
|
||||||
|
@apply text-slate-200 text-xs font-light mt-1;
|
||||||
|
}
|
||||||
|
|
||||||
|
span.dir {
|
||||||
|
@apply text-slate-200 pl-1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:has(kdb):not(.edit) kbd {
|
||||||
|
@apply pointer-events-none cursor-default;
|
||||||
|
}
|
||||||
|
|
||||||
|
.edit kbd {
|
||||||
|
@apply cursor-pointer pointer-events-auto;
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&.active {
|
||||||
|
@apply bg-sky-900 border-sky-400 shadow-sky-700;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
95
fe/src/components/macros/parts/EditDialogs.vue
Normal file
95
fe/src/components/macros/parts/EditDialogs.vue
Normal file
|
|
@ -0,0 +1,95 @@
|
||||||
|
<template>
|
||||||
|
<div class="macro-edit__dialogs" v-if="macroRecorder.state.edit !== false">
|
||||||
|
<div
|
||||||
|
class="flex gap-2"
|
||||||
|
v-if="macroRecorder.state.editKey !== false && typeof macroRecorder.getEditKey() === 'object'"
|
||||||
|
>
|
||||||
|
<ContextMenu>
|
||||||
|
<template #trigger>
|
||||||
|
<ButtonComp variant="success" size="sm"> <IconPlus /> Insert key </ButtonComp>
|
||||||
|
</template>
|
||||||
|
<template #content>
|
||||||
|
<ul>
|
||||||
|
<li @click="insertPosition = 'before'"><IconArrowLeftCircle /> Before</li>
|
||||||
|
<li @click="insertPosition = 'after'"><IconArrowRightCircle /> After</li>
|
||||||
|
</ul>
|
||||||
|
</template>
|
||||||
|
</ContextMenu>
|
||||||
|
<DialogComp v-if="insertPosition !== null" :open="true" @on-close="insertPosition = null">
|
||||||
|
<template #content>
|
||||||
|
<InsertKeyDialog :position="insertPosition" />
|
||||||
|
</template>
|
||||||
|
</DialogComp>
|
||||||
|
|
||||||
|
<DialogComp>
|
||||||
|
<template #trigger>
|
||||||
|
<ButtonComp size="sm" variant="danger" @click="console.log('delete')">
|
||||||
|
<IconTrash />Delete key
|
||||||
|
</ButtonComp>
|
||||||
|
</template>
|
||||||
|
<template #content>
|
||||||
|
<DeleteKeyDialog />
|
||||||
|
</template>
|
||||||
|
</DialogComp>
|
||||||
|
<DialogComp
|
||||||
|
:id="`edit-key-${macroRecorder.state.editKey}`"
|
||||||
|
@on-close="macroRecorder.state.editKey = false"
|
||||||
|
>
|
||||||
|
<template #trigger>
|
||||||
|
<ButtonComp variant="primary" size="sm"> <IconKeyboard />Edit key </ButtonComp>
|
||||||
|
</template>
|
||||||
|
<template #content>
|
||||||
|
<EditKeyDialog />
|
||||||
|
</template>
|
||||||
|
</DialogComp>
|
||||||
|
</div>
|
||||||
|
<DialogComp
|
||||||
|
v-if="
|
||||||
|
macroRecorder.state.editDelay !== false && typeof macroRecorder.getEditDelay() === 'object'
|
||||||
|
"
|
||||||
|
@on-close="macroRecorder.state.editDelay = false"
|
||||||
|
>
|
||||||
|
<template #trigger>
|
||||||
|
<ButtonComp variant="primary" size="sm"> <IconAlarm />Edit delay </ButtonComp>
|
||||||
|
</template>
|
||||||
|
<template #content>
|
||||||
|
<EditDelayDialog />
|
||||||
|
</template>
|
||||||
|
</DialogComp>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import {
|
||||||
|
IconAlarm,
|
||||||
|
IconArrowLeftCircle,
|
||||||
|
IconArrowRightCircle,
|
||||||
|
IconKeyboard,
|
||||||
|
IconPlus,
|
||||||
|
IconTrash,
|
||||||
|
} from '@tabler/icons-vue'
|
||||||
|
import DialogComp from '@/components/base/DialogComp.vue'
|
||||||
|
import ButtonComp from '@/components/base/ButtonComp.vue'
|
||||||
|
|
||||||
|
import { useMacroRecorderStore } from '@/stores/macrorecorder'
|
||||||
|
import EditKeyDialog from '../components/EditKeyDialog.vue'
|
||||||
|
import EditDelayDialog from '../components/EditDelayDialog.vue'
|
||||||
|
import DeleteKeyDialog from '../components/DeleteKeyDialog.vue'
|
||||||
|
import ContextMenu from '@/components/base/ContextMenu.vue'
|
||||||
|
import InsertKeyDialog from '../components/InsertKeyDialog.vue'
|
||||||
|
import { ref } from 'vue'
|
||||||
|
|
||||||
|
const macroRecorder = useMacroRecorderStore()
|
||||||
|
|
||||||
|
const insertPosition = ref(null)
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
@reference "@/assets/main.css";
|
||||||
|
|
||||||
|
.macro-edit__dialogs {
|
||||||
|
@apply flex
|
||||||
|
flex-grow
|
||||||
|
justify-end;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
25
fe/src/components/macros/parts/RecorderFooter.vue
Normal file
25
fe/src/components/macros/parts/RecorderFooter.vue
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
<template>
|
||||||
|
<div class="macro-recorder__footer">
|
||||||
|
<ButtonComp
|
||||||
|
v-if="macroRecorder.steps.length > 0"
|
||||||
|
variant="danger"
|
||||||
|
size="sm"
|
||||||
|
@click="macroRecorder.reset()"
|
||||||
|
>
|
||||||
|
<IconRestore /> Reset
|
||||||
|
</ButtonComp>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import ButtonComp from '@/components/base/ButtonComp.vue'
|
||||||
|
import { IconRestore } from '@tabler/icons-vue'
|
||||||
|
|
||||||
|
import { useMacroRecorderStore } from '@/stores/macrorecorder'
|
||||||
|
|
||||||
|
const macroRecorder = useMacroRecorderStore()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
@reference "@/assets/main.css";
|
||||||
|
</style>
|
||||||
70
fe/src/components/macros/parts/RecorderHeader.vue
Normal file
70
fe/src/components/macros/parts/RecorderHeader.vue
Normal file
|
|
@ -0,0 +1,70 @@
|
||||||
|
<template>
|
||||||
|
<div class="macro-recorder__header">
|
||||||
|
<div :class="`recording__buttons ${macroRecorder.state.edit ? 'disabled' : ''}`">
|
||||||
|
<ButtonComp
|
||||||
|
v-if="!macroRecorder.state.record"
|
||||||
|
variant="primary"
|
||||||
|
@click="macroRecorder.state.record = true"
|
||||||
|
>
|
||||||
|
<IconPlayerRecordFilled class="text-red-500" />Start recording
|
||||||
|
</ButtonComp>
|
||||||
|
<ButtonComp
|
||||||
|
v-if="macroRecorder.state.record"
|
||||||
|
variant="danger"
|
||||||
|
@click="macroRecorder.state.record = false"
|
||||||
|
>
|
||||||
|
<IconPlayerStopFilled class="text-white" />Stop recording
|
||||||
|
</ButtonComp>
|
||||||
|
</div>
|
||||||
|
<div :class="`edit__buttons ${macroRecorder.state.record ? 'disabled' : ''}`">
|
||||||
|
<div>
|
||||||
|
<ButtonComp
|
||||||
|
v-if="!macroRecorder.state.edit"
|
||||||
|
variant="secondary"
|
||||||
|
@click="macroRecorder.state.edit = true"
|
||||||
|
>
|
||||||
|
<IconPencil />Edit macro
|
||||||
|
</ButtonComp>
|
||||||
|
<ButtonComp
|
||||||
|
v-if="macroRecorder.state.edit"
|
||||||
|
variant="dark"
|
||||||
|
@click="macroRecorder.resetEdit()"
|
||||||
|
>
|
||||||
|
<IconPlayerStopFilled />Stop editing
|
||||||
|
</ButtonComp>
|
||||||
|
</div>
|
||||||
|
<FixedDelayMenu v-if="macroRecorder.state.edit" />
|
||||||
|
<EditDialogs />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { IconPencil, IconPlayerRecordFilled, IconPlayerStopFilled } from '@tabler/icons-vue'
|
||||||
|
import ButtonComp from '@/components/base/ButtonComp.vue'
|
||||||
|
import FixedDelayMenu from '../components/FixedDelayMenu.vue'
|
||||||
|
|
||||||
|
import { useMacroRecorderStore } from '@/stores/macrorecorder'
|
||||||
|
import EditDialogs from './EditDialogs.vue'
|
||||||
|
|
||||||
|
const macroRecorder = useMacroRecorderStore()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
@reference "@/assets/main.css";
|
||||||
|
|
||||||
|
.macro-recorder__header {
|
||||||
|
@apply grid
|
||||||
|
grid-cols-[auto_1fr]
|
||||||
|
items-end
|
||||||
|
gap-4;
|
||||||
|
|
||||||
|
> div {
|
||||||
|
@apply flex gap-4 items-end;
|
||||||
|
|
||||||
|
&.disabled {
|
||||||
|
@apply opacity-50 pointer-events-none cursor-not-allowed;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
46
fe/src/components/macros/parts/RecorderInput.vue
Normal file
46
fe/src/components/macros/parts/RecorderInput.vue
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
<template>
|
||||||
|
<div :class="`recorder-input__container ${macroRecorder.state.record && 'record'}`">
|
||||||
|
<input
|
||||||
|
v-if="macroRecorder.state.record"
|
||||||
|
:class="`macro-recorder__input ${macroRecorder.state.record && 'record'}`"
|
||||||
|
type="text"
|
||||||
|
ref="macroInput"
|
||||||
|
@focus="console.log('focus')"
|
||||||
|
@keydown.prevent="macroRecorder.recordStep($event, 'down')"
|
||||||
|
@keyup.prevent="macroRecorder.recordStep($event, 'up')"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { useMacroRecorderStore } from '@/stores/macrorecorder'
|
||||||
|
import { ref, onUpdated } from 'vue'
|
||||||
|
|
||||||
|
const macroInput = ref(null)
|
||||||
|
|
||||||
|
const macroRecorder = useMacroRecorderStore()
|
||||||
|
|
||||||
|
onUpdated(() => {
|
||||||
|
if (macroRecorder.state.record) {
|
||||||
|
macroInput.value.focus()
|
||||||
|
if (macroRecorder.delay.start !== 0) macroRecorder.restartDelay()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
@reference "@/assets/main.css";
|
||||||
|
|
||||||
|
.recorder-input__container,
|
||||||
|
.macro-recorder__input {
|
||||||
|
@apply absolute
|
||||||
|
inset-0
|
||||||
|
size-full
|
||||||
|
opacity-0
|
||||||
|
hidden;
|
||||||
|
|
||||||
|
&.record {
|
||||||
|
@apply block;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
54
fe/src/components/macros/parts/RecorderOutput.vue
Normal file
54
fe/src/components/macros/parts/RecorderOutput.vue
Normal file
|
|
@ -0,0 +1,54 @@
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
:class="`macro-recorder__output ${macroRecorder.state.record && 'record'} ${macroRecorder.state.edit && 'edit'}`"
|
||||||
|
>
|
||||||
|
<template v-for="(step, key) in macroRecorder.steps">
|
||||||
|
<!-- Key element -->
|
||||||
|
<template v-if="step.type === 'key'">
|
||||||
|
<MacroKey
|
||||||
|
:key="key"
|
||||||
|
:key-obj="step.keyObj"
|
||||||
|
:direction="step.direction"
|
||||||
|
:active="macroRecorder.state.editKey === key"
|
||||||
|
@click="macroRecorder.state.edit ? macroRecorder.toggleEdit('key', key) : false"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- Delay element -->
|
||||||
|
<template v-else-if="step.type === 'delay'">
|
||||||
|
<DelaySpan
|
||||||
|
:key="key"
|
||||||
|
:value="step.value"
|
||||||
|
:active="macroRecorder.state.editDelay === key"
|
||||||
|
@click="macroRecorder.toggleEdit('delay', key)"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- Spacer element -->
|
||||||
|
<hr class="spacer" />
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { useMacroRecorderStore } from '@/stores/macrorecorder'
|
||||||
|
import MacroKey from '../components/MacroKey.vue'
|
||||||
|
import DelaySpan from '../components/DelaySpan.vue'
|
||||||
|
|
||||||
|
const macroRecorder = useMacroRecorderStore()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
@reference "@/assets/main.css";
|
||||||
|
|
||||||
|
.macro-recorder__output {
|
||||||
|
@apply flex
|
||||||
|
flex-wrap
|
||||||
|
items-center
|
||||||
|
gap-y-4
|
||||||
|
p-4
|
||||||
|
absolute
|
||||||
|
top-0 left-0
|
||||||
|
h-fit;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Loading…
Add table
Add a link
Reference in a new issue