mirror of
https://github.com/Macrame-App/Macrame
synced 2025-12-29 15:29:26 +00:00
Macro Recorder added
This commit is contained in:
parent
c514ba151e
commit
a6024f22e7
12 changed files with 510 additions and 122 deletions
71
fe/src/components/macros/MacroOverview.vue
Normal file
71
fe/src/components/macros/MacroOverview.vue
Normal file
|
|
@ -0,0 +1,71 @@
|
||||||
|
<template>
|
||||||
|
<div class="macro-overview mcrm-block block__dark">
|
||||||
|
<h4 class="border-b-2 border-transparent">Saved Macros</h4>
|
||||||
|
<div class="macro-overview__list">
|
||||||
|
<div class="macro-item" v-for="(macro, i) in macros.list" :key="i">
|
||||||
|
<ButtonComp variant="dark" class="w-full" size="sm" @click.prevent="runMacro(macro)">
|
||||||
|
<IconKeyboard /> {{ macro }}
|
||||||
|
</ButtonComp>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { IconKeyboard } from '@tabler/icons-vue'
|
||||||
|
import ButtonComp from '../base/ButtonComp.vue'
|
||||||
|
import { onMounted, reactive } from 'vue'
|
||||||
|
import axios from 'axios'
|
||||||
|
import { appUrl } from '@/services/ApiService'
|
||||||
|
|
||||||
|
const macros = reactive({
|
||||||
|
list: [],
|
||||||
|
})
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
axios.post(appUrl() + '/macro/list').then((data) => {
|
||||||
|
if (data.data.length > 0) macros.list = data.data
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
function runMacro(macro) {
|
||||||
|
console.log(macro)
|
||||||
|
axios.post(appUrl() + '/macro/play', { macro: macro }).then((data) => {
|
||||||
|
console.log(data)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
@reference "@/assets/main.css";
|
||||||
|
|
||||||
|
.macro-overview {
|
||||||
|
@apply relative
|
||||||
|
grid
|
||||||
|
grid-rows-[auto_1fr];
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
@apply content-['']
|
||||||
|
absolute
|
||||||
|
top-0
|
||||||
|
left-full
|
||||||
|
h-full
|
||||||
|
w-px
|
||||||
|
bg-slate-600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.macro-overview__list {
|
||||||
|
@apply grid
|
||||||
|
gap-1
|
||||||
|
content-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.macro-item {
|
||||||
|
@apply flex items-center;
|
||||||
|
|
||||||
|
button {
|
||||||
|
@apply w-full;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
@ -1,45 +1,25 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="macro-recorder">
|
<div class="macro-recorder mcrm-block block__light">
|
||||||
<!-- Recorder buttons -->
|
<div class="recorder-interface">
|
||||||
<RecorderHeader />
|
<!-- Recorder buttons -->
|
||||||
|
<RecorderHeader />
|
||||||
|
|
||||||
<!-- Recorder interface container -->
|
<!-- Recorder interface container -->
|
||||||
<div
|
<div
|
||||||
:class="`recorder-interface__container ${macroRecorder.state.record && 'record'} ${macroRecorder.state.edit && 'edit'}`"
|
:class="`recorder-interface__container ${macroRecorder.state.record && 'record'} ${macroRecorder.state.edit && 'edit'}`"
|
||||||
>
|
>
|
||||||
<!-- Shows the macro steps as kbd elements with delay and spacers-->
|
<!-- Shows the macro steps as kbd elements with delay and spacers-->
|
||||||
<RecorderOutput />
|
<RecorderOutput />
|
||||||
<!-- Input for recording macro steps -->
|
<!-- Input for recording macro steps -->
|
||||||
<RecorderInput />
|
<RecorderInput />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<RecorderFooter />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<RecorderFooter />
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<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 RecorderOutput from './parts/RecorderOutput.vue'
|
||||||
import RecorderInput from './parts/RecorderInput.vue'
|
import RecorderInput from './parts/RecorderInput.vue'
|
||||||
|
|
||||||
|
|
@ -53,24 +33,53 @@ const macroRecorder = useMacroRecorderStore()
|
||||||
<style>
|
<style>
|
||||||
@reference "@/assets/main.css";
|
@reference "@/assets/main.css";
|
||||||
|
|
||||||
|
.macro-recorder {
|
||||||
|
@apply h-full;
|
||||||
|
}
|
||||||
|
|
||||||
|
.recorder-interface {
|
||||||
|
@apply grid
|
||||||
|
grid-rows-[auto_1fr_auto]
|
||||||
|
gap-4
|
||||||
|
h-full
|
||||||
|
transition-[grid-template-rows];
|
||||||
|
}
|
||||||
|
|
||||||
.recorder-interface__container {
|
.recorder-interface__container {
|
||||||
@apply relative
|
@apply relative
|
||||||
w-full
|
w-full
|
||||||
h-96
|
|
||||||
my-4
|
|
||||||
rounded-lg
|
rounded-lg
|
||||||
bg-slate-900/50
|
bg-slate-950/50
|
||||||
border-2
|
border
|
||||||
border-white/10
|
border-slate-600
|
||||||
overflow-auto
|
overflow-auto
|
||||||
transition-colors;
|
transition-colors;
|
||||||
|
|
||||||
&.record {
|
&.record {
|
||||||
@apply border-rose-300 bg-rose-950/50;
|
@apply border-rose-300 bg-rose-400/10;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.edit {
|
&.edit {
|
||||||
@apply border-sky-300 bg-sky-900/50;
|
@apply border-sky-300 bg-sky-900/10;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#macro-name {
|
||||||
|
@apply w-full
|
||||||
|
bg-transparent
|
||||||
|
py-0
|
||||||
|
outline-0
|
||||||
|
border-transparent
|
||||||
|
border-b-slate-300
|
||||||
|
focus:border-transparent
|
||||||
|
focus:border-b-sky-400
|
||||||
|
focus:bg-sky-400/10
|
||||||
|
transition-colors
|
||||||
|
text-lg
|
||||||
|
rounded-none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.disabled {
|
||||||
|
@apply opacity-50 pointer-events-none cursor-not-allowed;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,17 @@
|
||||||
<template>
|
<template>
|
||||||
<span :class="`delay ${active ? 'active' : ''}`">
|
<span :class="`delay ${active ? 'active' : ''} ${preset ? 'preset' : ''}`">
|
||||||
{{ value < 10000 ? value + 'ms' : '>10s' }}
|
<template v-if="value < 10000"> {{ value }} <i>ms</i> </template>
|
||||||
|
<template v-else> >10 <i>s</i> </template>
|
||||||
</span>
|
</span>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
import { IconTimeDuration10 } from '@tabler/icons-vue'
|
||||||
|
|
||||||
defineProps({
|
defineProps({
|
||||||
value: Number,
|
value: Number,
|
||||||
active: Boolean,
|
active: Boolean,
|
||||||
|
preset: Boolean,
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
@ -22,10 +26,24 @@ span.delay {
|
||||||
border
|
border
|
||||||
border-slate-400
|
border-slate-400
|
||||||
text-slate-950
|
text-slate-950
|
||||||
|
font-sans
|
||||||
font-semibold
|
font-semibold
|
||||||
rounded-sm
|
rounded-sm
|
||||||
text-sm
|
text-sm
|
||||||
cursor-default;
|
cursor-default;
|
||||||
|
|
||||||
|
&.preset {
|
||||||
|
@apply text-amber-400
|
||||||
|
border-amber-300/80
|
||||||
|
bg-amber-100/60;
|
||||||
|
}
|
||||||
|
|
||||||
|
i {
|
||||||
|
@apply pl-1
|
||||||
|
font-normal
|
||||||
|
not-italic
|
||||||
|
opacity-80;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.edit span.delay {
|
.edit span.delay {
|
||||||
|
|
|
||||||
|
|
@ -26,9 +26,9 @@ const keyObj = ref(null)
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
keyObj.value = filterKey(macroRecorder.getEditKey())
|
keyObj.value = filterKey(macroRecorder.getEditKey())
|
||||||
console.log(macroRecorder.getEditKey());
|
// console.log(macroRecorder.getEditKey());
|
||||||
console.log(keyObj.value);
|
// console.log(keyObj.value);
|
||||||
console.log('---------');
|
// console.log('---------');
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,14 @@
|
||||||
<template>
|
<template>
|
||||||
<ContextMenu>
|
<ContextMenu ref="ctxtMenu">
|
||||||
<template #trigger>
|
<template #trigger>
|
||||||
<ButtonComp variant="secondary" size="sm"> <IconAlarmFilled />Fixed delay </ButtonComp>
|
<ButtonComp variant="secondary" size="sm"> <IconTimeDuration15 />Fixed delay </ButtonComp>
|
||||||
</template>
|
</template>
|
||||||
<template #content>
|
<template #content>
|
||||||
<ul>
|
<ul>
|
||||||
<li @click="macroRecorder.changeDelay(0)">0ms</li>
|
<li @click="changeDelay(0)">0ms</li>
|
||||||
<li @click="macroRecorder.changeDelay(15)">15ms</li>
|
<li @click="changeDelay(15)">15ms</li>
|
||||||
<li @click="macroRecorder.changeDelay(50)">50ms</li>
|
<li @click="changeDelay(50)">50ms</li>
|
||||||
<li @click="macroRecorder.changeDelay(100)">100ms</li>
|
<li @click="changeDelay(100)">100ms</li>
|
||||||
<li>
|
<li>
|
||||||
<DialogComp>
|
<DialogComp>
|
||||||
<template #trigger>
|
<template #trigger>
|
||||||
|
|
@ -19,7 +19,7 @@
|
||||||
<h4 class="text-slate-50 mb-4">Custom delay</h4>
|
<h4 class="text-slate-50 mb-4">Custom delay</h4>
|
||||||
<form
|
<form
|
||||||
class="grid gap-4 w-44"
|
class="grid gap-4 w-44"
|
||||||
@submit.prevent="macroRecorder.changeDelay(parseInt($refs.customDelayInput.value))"
|
@submit.prevent="changeDelay(parseInt($refs.customDelayInput.value))"
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
<input
|
<input
|
||||||
|
|
@ -46,7 +46,7 @@
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import ContextMenu from '@/components/base/ContextMenu.vue'
|
import ContextMenu from '@/components/base/ContextMenu.vue'
|
||||||
import { IconAlarmFilled } from '@tabler/icons-vue'
|
import { IconTimeDuration15 } from '@tabler/icons-vue'
|
||||||
import ButtonComp from '@/components/base/ButtonComp.vue'
|
import ButtonComp from '@/components/base/ButtonComp.vue'
|
||||||
import DialogComp from '@/components/base/DialogComp.vue'
|
import DialogComp from '@/components/base/DialogComp.vue'
|
||||||
|
|
||||||
|
|
@ -56,7 +56,12 @@ import { ref } from 'vue'
|
||||||
|
|
||||||
const macroRecorder = useMacroRecorderStore()
|
const macroRecorder = useMacroRecorderStore()
|
||||||
|
|
||||||
const delayMenu = ref(false)
|
const ctxtMenu = ref()
|
||||||
|
|
||||||
|
function changeDelay(num) {
|
||||||
|
macroRecorder.changeDelay(num)
|
||||||
|
ctxtMenu.value.toggle()
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,72 @@
|
||||||
<template>
|
<template>
|
||||||
<div id="insert-key-dialog" class="dialog__content w-80">
|
<div id="insert-key-dialog" class="dialog__content w-96">
|
||||||
<h4 class="text-slate-50 mb-4">Insert key {{ position }}</h4>
|
<h4 class="text-slate-50 mb-4">Insert key {{ position }}</h4>
|
||||||
<div class="flex justify-center w-full mb-4">
|
<p v-if="inputFocus" class="text-center">[Press a key]</p>
|
||||||
|
<input
|
||||||
|
class="size-0 opacity-0"
|
||||||
|
type="text"
|
||||||
|
min="0"
|
||||||
|
max="1"
|
||||||
|
ref="insertKeyInput"
|
||||||
|
placeholder="New key"
|
||||||
|
@focusin="inputFocus = true"
|
||||||
|
@focusout="inputFocus = false"
|
||||||
|
@keydown.prevent="handleInsertKey($event)"
|
||||||
|
autofocus
|
||||||
|
/>
|
||||||
|
<div class="insert-output" :class="position == 'before' ? 'flex-row-reverse' : ''">
|
||||||
<MacroKey v-if="keyObjs.selected" :key-obj="keyObjs.selected" />
|
<MacroKey v-if="keyObjs.selected" :key-obj="keyObjs.selected" />
|
||||||
<MacroKey v-if="keyObjs.adjacent" :key-obj="keyObjs.adjacent" />
|
<hr class="spacer" />
|
||||||
|
<DelaySpan :preset="true" :value="10" />
|
||||||
|
<hr class="spacer" />
|
||||||
|
<MacroKey
|
||||||
|
v-if="keyObjs.insert"
|
||||||
|
class="insert"
|
||||||
|
:key-obj="keyObjs.insert"
|
||||||
|
:direction="keyObjs.insertDirection"
|
||||||
|
@click="insertKeyInput.focus()"
|
||||||
|
/>
|
||||||
|
<MacroKey v-if="!keyObjs.insert" :empty="true" @click="insertKeyInput.focus()" />
|
||||||
|
<template v-if="keyObjs.adjacentDelay">
|
||||||
|
<hr class="spacer" />
|
||||||
|
<DelaySpan :value="keyObjs.adjacentDelay.value" />
|
||||||
|
</template>
|
||||||
|
<template v-if="keyObjs.adjacent">
|
||||||
|
<hr class="spacer" />
|
||||||
|
<MacroKey :key-obj="keyObjs.adjacent" />
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
<div class="insert-key__direction">
|
||||||
|
<ButtonComp
|
||||||
|
variant="secondary"
|
||||||
|
:class="keyObjs.insertDirection === 'down' ? 'selected' : ''"
|
||||||
|
size="sm"
|
||||||
|
@click.prevent="keyObjs.insertDirection = 'down'"
|
||||||
|
>
|
||||||
|
↓ Down
|
||||||
|
</ButtonComp>
|
||||||
|
<ButtonComp
|
||||||
|
variant="secondary"
|
||||||
|
:class="keyObjs.insertDirection === 'up' ? 'selected' : ''"
|
||||||
|
size="sm"
|
||||||
|
@click.prevent="keyObjs.insertDirection = 'up'"
|
||||||
|
>
|
||||||
|
↑ Up
|
||||||
|
</ButtonComp>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-end">
|
||||||
|
<ButtonComp variant="primary" size="sm" @click="insertKey()">Insert key</ButtonComp>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
|
import MacroKey from './MacroKey.vue'
|
||||||
|
import DelaySpan from './DelaySpan.vue'
|
||||||
|
import ButtonComp from '@/components/base/ButtonComp.vue'
|
||||||
|
|
||||||
import { onMounted, reactive, ref } from 'vue'
|
import { onMounted, reactive, ref } from 'vue'
|
||||||
import { useMacroRecorderStore } from '@/stores/macrorecorder'
|
import { useMacroRecorderStore } from '@/stores/macrorecorder'
|
||||||
import MacroKey from './MacroKey.vue'
|
|
||||||
import { filterKey } from '@/services/MacroRecordService'
|
import { filterKey } from '@/services/MacroRecordService'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
|
|
@ -23,19 +78,57 @@ const macroRecorder = useMacroRecorderStore()
|
||||||
const keyObjs = reactive({
|
const keyObjs = reactive({
|
||||||
selected: null,
|
selected: null,
|
||||||
insert: null,
|
insert: null,
|
||||||
|
insertEvent: null,
|
||||||
|
insertDirection: 'down',
|
||||||
adjacent: null,
|
adjacent: null,
|
||||||
adjacentDelay: null,
|
adjacentDelay: null,
|
||||||
|
adjacentDelayIndex: null,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const insertKeyInput = ref(null)
|
||||||
|
const inputFocus = ref(false)
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
keyObjs.selected = filterKey(macroRecorder.getEditKey())
|
keyObjs.selected = filterKey(macroRecorder.getEditKey())
|
||||||
|
|
||||||
const adjacentKey = macroRecorder.getAdjacentKey(props.position, true)
|
const adjacentKey = macroRecorder.getAdjacentKey(props.position, true)
|
||||||
if (adjacentKey) keyObjs.adjacent = filterKey(adjacentKey.key)
|
if (adjacentKey) keyObjs.adjacent = filterKey(adjacentKey.key)
|
||||||
if (adjacentKey.delay) keyObjs.adjacentDelay = adjacentKey.delay
|
if (adjacentKey.delay) {
|
||||||
|
keyObjs.adjacentDelay = adjacentKey.delay
|
||||||
|
keyObjs.adjacentDelayIndex = adjacentKey.delayIndex
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const handleInsertKey = (e) => {
|
||||||
|
keyObjs.insert = filterKey(e)
|
||||||
|
keyObjs.insertEvent = e
|
||||||
|
}
|
||||||
|
|
||||||
|
const insertKey = () => {
|
||||||
|
macroRecorder.insertKey(keyObjs.insertEvent, keyObjs.insertDirection, keyObjs.adjacentDelayIndex)
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
@reference "@/assets/main.css";
|
@reference "@/assets/main.css";
|
||||||
|
|
||||||
|
.insert-output {
|
||||||
|
@apply flex
|
||||||
|
justify-center
|
||||||
|
items-center
|
||||||
|
w-full
|
||||||
|
mb-4;
|
||||||
|
}
|
||||||
|
.insert-key__direction {
|
||||||
|
@apply flex
|
||||||
|
justify-center
|
||||||
|
gap-2
|
||||||
|
mt-6;
|
||||||
|
}
|
||||||
|
button.selected {
|
||||||
|
@apply bg-sky-500
|
||||||
|
ring-2
|
||||||
|
ring-offset-1
|
||||||
|
ring-sky-500;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,27 @@
|
||||||
<template>
|
<template>
|
||||||
<kbd :class="active ? 'active' : ''">
|
<kbd :class="`${active ? 'active' : ''} ${empty ? 'empty' : ''}`">
|
||||||
<sup v-if="keyObj.loc">
|
<template v-if="keyObj">
|
||||||
{{ keyObj.loc }}
|
<sup v-if="keyObj.loc">
|
||||||
</sup>
|
{{ keyObj.loc }}
|
||||||
<span :innerHTML="keyObj.str" />
|
</sup>
|
||||||
<span class="dir">{{ dir.value === 'down' ? '↓' : '↑' }}</span>
|
<span :innerHTML="keyObj.str" />
|
||||||
|
<span class="dir">{{ dir.value === 'down' ? '↓' : '↑' }}</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-else-if="empty">
|
||||||
|
<span>[ ]</span>
|
||||||
|
</template>
|
||||||
</kbd>
|
</kbd>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { onMounted, reactive } from 'vue'
|
import { onMounted, onUpdated, reactive } from 'vue'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
keyObj: Object,
|
keyObj: Object,
|
||||||
direction: String,
|
direction: String,
|
||||||
active: Boolean,
|
active: Boolean,
|
||||||
|
empty: Boolean,
|
||||||
})
|
})
|
||||||
|
|
||||||
const dir = reactive({
|
const dir = reactive({
|
||||||
|
|
@ -22,9 +29,18 @@ const dir = reactive({
|
||||||
})
|
})
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
if (props.empty) return
|
||||||
|
setDirection()
|
||||||
|
})
|
||||||
|
|
||||||
|
onUpdated(() => {
|
||||||
|
setDirection()
|
||||||
|
})
|
||||||
|
|
||||||
|
const setDirection = () => {
|
||||||
if (props.direction) dir.value = props.direction
|
if (props.direction) dir.value = props.direction
|
||||||
else dir.value = props.keyObj.direction
|
else dir.value = props.keyObj.direction
|
||||||
})
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
|
|
@ -37,10 +53,11 @@ kbd {
|
||||||
pl-4 pr-2 py-1
|
pl-4 pr-2 py-1
|
||||||
h-9
|
h-9
|
||||||
bg-slate-700
|
bg-slate-700
|
||||||
font-sans
|
font-mono
|
||||||
font-bold
|
font-bold
|
||||||
text-lg
|
text-lg
|
||||||
text-white
|
text-white
|
||||||
|
whitespace-nowrap
|
||||||
uppercase
|
uppercase
|
||||||
rounded-md
|
rounded-md
|
||||||
border
|
border
|
||||||
|
|
@ -60,6 +77,21 @@ kbd {
|
||||||
span.dir {
|
span.dir {
|
||||||
@apply text-slate-200 pl-1;
|
@apply text-slate-200 pl-1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.empty {
|
||||||
|
@apply pl-3 pr-3
|
||||||
|
bg-sky-400/50
|
||||||
|
border-sky-300
|
||||||
|
shadow-sky-600
|
||||||
|
tracking-widest
|
||||||
|
cursor-pointer;
|
||||||
|
}
|
||||||
|
&.insert {
|
||||||
|
@apply bg-yellow-500/50
|
||||||
|
border-yellow-300
|
||||||
|
shadow-yellow-600
|
||||||
|
cursor-pointer;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
:has(kdb):not(.edit) kbd {
|
:has(kdb):not(.edit) kbd {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,60 @@
|
||||||
|
<template>
|
||||||
|
<div id="validation-error__dialog" class="dialog__content">
|
||||||
|
<h4 class="text-slate-50 mb-4">There's an error in your macro</h4>
|
||||||
|
|
||||||
|
<div class="grid gap-4" v-if="(errors && errors.up.length > 0) || errors.down.length > 0">
|
||||||
|
<div v-if="errors.down.length > 0">
|
||||||
|
<p>
|
||||||
|
The following keys have been <strong>pressed</strong> down, but
|
||||||
|
<strong>not released</strong>.
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
<li v-for="key in errors.down" :key="key">{{ key.toUpperCase() }}</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="errors.up.length > 0">
|
||||||
|
<p>
|
||||||
|
The following keys have been <strong>released</strong>, but
|
||||||
|
<strong>not pressed</strong> down.
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
<li v-for="key in errors.up" :key="key">{{ key.toUpperCase() }}</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-end mt-4">
|
||||||
|
<ButtonComp size="sm" variant="danger" @click="macroRecorder.state.validationErrors = false">
|
||||||
|
Close
|
||||||
|
</ButtonComp>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import ButtonComp from '@/components/base/ButtonComp.vue'
|
||||||
|
import { useMacroRecorderStore } from '@/stores/macrorecorder'
|
||||||
|
import { onMounted, reactive } from 'vue'
|
||||||
|
|
||||||
|
const macroRecorder = useMacroRecorderStore()
|
||||||
|
|
||||||
|
const errors = reactive({
|
||||||
|
up: [],
|
||||||
|
down: [],
|
||||||
|
})
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
macroRecorder.$subscribe((mutation) => {
|
||||||
|
if (mutation.events && mutation.events.key == 'validationErrors') {
|
||||||
|
errors.up = mutation.events.newValue !== false ? macroRecorder.state.validationErrors.up : []
|
||||||
|
errors.down =
|
||||||
|
mutation.events.newValue !== false ? macroRecorder.state.validationErrors.down : []
|
||||||
|
}
|
||||||
|
console.log(mutation)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
@reference "@/assets/main.css";
|
||||||
|
</style>
|
||||||
|
|
@ -4,53 +4,60 @@
|
||||||
class="flex gap-2"
|
class="flex gap-2"
|
||||||
v-if="macroRecorder.state.editKey !== false && typeof macroRecorder.getEditKey() === 'object'"
|
v-if="macroRecorder.state.editKey !== false && typeof macroRecorder.getEditKey() === 'object'"
|
||||||
>
|
>
|
||||||
<ContextMenu>
|
<ContextMenu ref="ctxtMenu">
|
||||||
<template #trigger>
|
<template #trigger>
|
||||||
<ButtonComp variant="success" size="sm"> <IconPlus /> Insert key </ButtonComp>
|
<ButtonComp variant="dark" size="sm"> <IconPlus /> Insert </ButtonComp>
|
||||||
</template>
|
</template>
|
||||||
<template #content>
|
<template #content>
|
||||||
<ul>
|
<ul>
|
||||||
<li @click="insertPosition = 'before'"><IconArrowLeftCircle /> Before</li>
|
<li @click="insert.position = 'before'"><IconArrowLeftCircle /> Before</li>
|
||||||
<li @click="insertPosition = 'after'"><IconArrowRightCircle /> After</li>
|
<li @click="insert.position = 'after'"><IconArrowRightCircle /> After</li>
|
||||||
</ul>
|
</ul>
|
||||||
</template>
|
</template>
|
||||||
</ContextMenu>
|
</ContextMenu>
|
||||||
<DialogComp v-if="insertPosition !== null" :open="true" @on-close="insertPosition = null">
|
|
||||||
|
<DialogComp
|
||||||
|
v-if="insert.position !== null"
|
||||||
|
:open="insert.position !== null"
|
||||||
|
@on-open="onOpenDialog"
|
||||||
|
@on-close="onCloseDialog"
|
||||||
|
>
|
||||||
<template #content>
|
<template #content>
|
||||||
<InsertKeyDialog :position="insertPosition" />
|
<InsertKeyDialog :position="insert.position" />
|
||||||
</template>
|
</template>
|
||||||
</DialogComp>
|
</DialogComp>
|
||||||
|
|
||||||
<DialogComp>
|
|
||||||
<template #trigger>
|
|
||||||
<ButtonComp size="sm" variant="danger" @click="console.log('delete')">
|
|
||||||
<IconTrash />Delete key
|
|
||||||
</ButtonComp>
|
|
||||||
</template>
|
|
||||||
<template #content>
|
|
||||||
<DeleteKeyDialog />
|
|
||||||
</template>
|
|
||||||
</DialogComp>
|
|
||||||
<DialogComp
|
<DialogComp
|
||||||
:id="`edit-key-${macroRecorder.state.editKey}`"
|
:id="`edit-key-${macroRecorder.state.editKey}`"
|
||||||
@on-close="macroRecorder.state.editKey = false"
|
@on-open="onOpenDialog"
|
||||||
|
@on-close="onCloseDialog"
|
||||||
>
|
>
|
||||||
<template #trigger>
|
<template #trigger>
|
||||||
<ButtonComp variant="primary" size="sm"> <IconKeyboard />Edit key </ButtonComp>
|
<ButtonComp variant="secondary" size="sm"> <IconPencil />Edit </ButtonComp>
|
||||||
</template>
|
</template>
|
||||||
<template #content>
|
<template #content>
|
||||||
<EditKeyDialog />
|
<EditKeyDialog />
|
||||||
</template>
|
</template>
|
||||||
</DialogComp>
|
</DialogComp>
|
||||||
|
|
||||||
|
<DialogComp @on-open="onOpenDialog" @on-close="onCloseDialog">
|
||||||
|
<template #trigger>
|
||||||
|
<ButtonComp size="sm" variant="danger"> <IconTrash />Delete </ButtonComp>
|
||||||
|
</template>
|
||||||
|
<template #content>
|
||||||
|
<DeleteKeyDialog />
|
||||||
|
</template>
|
||||||
|
</DialogComp>
|
||||||
</div>
|
</div>
|
||||||
<DialogComp
|
<DialogComp
|
||||||
v-if="
|
v-if="
|
||||||
macroRecorder.state.editDelay !== false && typeof macroRecorder.getEditDelay() === 'object'
|
macroRecorder.state.editDelay !== false && typeof macroRecorder.getEditDelay() === 'object'
|
||||||
"
|
"
|
||||||
@on-close="macroRecorder.state.editDelay = false"
|
@on-open="onOpenDialog"
|
||||||
|
@on-close="onCloseDialog"
|
||||||
>
|
>
|
||||||
<template #trigger>
|
<template #trigger>
|
||||||
<ButtonComp variant="primary" size="sm"> <IconAlarm />Edit delay </ButtonComp>
|
<ButtonComp variant="secondary" size="sm"> <IconAlarm />Edit </ButtonComp>
|
||||||
</template>
|
</template>
|
||||||
<template #content>
|
<template #content>
|
||||||
<EditDelayDialog />
|
<EditDelayDialog />
|
||||||
|
|
@ -64,7 +71,7 @@ import {
|
||||||
IconAlarm,
|
IconAlarm,
|
||||||
IconArrowLeftCircle,
|
IconArrowLeftCircle,
|
||||||
IconArrowRightCircle,
|
IconArrowRightCircle,
|
||||||
IconKeyboard,
|
IconPencil,
|
||||||
IconPlus,
|
IconPlus,
|
||||||
IconTrash,
|
IconTrash,
|
||||||
} from '@tabler/icons-vue'
|
} from '@tabler/icons-vue'
|
||||||
|
|
@ -77,11 +84,30 @@ import EditDelayDialog from '../components/EditDelayDialog.vue'
|
||||||
import DeleteKeyDialog from '../components/DeleteKeyDialog.vue'
|
import DeleteKeyDialog from '../components/DeleteKeyDialog.vue'
|
||||||
import ContextMenu from '@/components/base/ContextMenu.vue'
|
import ContextMenu from '@/components/base/ContextMenu.vue'
|
||||||
import InsertKeyDialog from '../components/InsertKeyDialog.vue'
|
import InsertKeyDialog from '../components/InsertKeyDialog.vue'
|
||||||
import { ref } from 'vue'
|
import { onMounted, reactive, ref } from 'vue'
|
||||||
|
|
||||||
const macroRecorder = useMacroRecorderStore()
|
const macroRecorder = useMacroRecorderStore()
|
||||||
|
|
||||||
const insertPosition = ref(null)
|
const insert = reactive({ position: null })
|
||||||
|
const ctxtMenu = ref()
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
macroRecorder.$subscribe((mutation) => {
|
||||||
|
if (mutation.events && mutation.events.key == 'editKey' && mutation.events.newValue === false) {
|
||||||
|
insert.position = null
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
function onOpenDialog() {
|
||||||
|
if (insert.position !== null) ctxtMenu.value.toggle()
|
||||||
|
}
|
||||||
|
function onCloseDialog() {
|
||||||
|
macroRecorder.state.editKey = false
|
||||||
|
macroRecorder.state.editDelay = false
|
||||||
|
|
||||||
|
insert.position = null
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|
|
||||||
|
|
@ -8,18 +8,58 @@
|
||||||
>
|
>
|
||||||
<IconRestore /> Reset
|
<IconRestore /> Reset
|
||||||
</ButtonComp>
|
</ButtonComp>
|
||||||
|
|
||||||
|
<DialogComp ref="errorDialog">
|
||||||
|
<template #content>
|
||||||
|
<ValidationErrorDialog />
|
||||||
|
</template>
|
||||||
|
</DialogComp>
|
||||||
|
|
||||||
|
<ButtonComp
|
||||||
|
v-if="macroRecorder.steps.length > 0"
|
||||||
|
:disabled="macroRecorder.state.record || macroRecorder.state.edit"
|
||||||
|
variant="success"
|
||||||
|
size="sm"
|
||||||
|
@click="toggleSave()"
|
||||||
|
>
|
||||||
|
<IconDeviceFloppy />
|
||||||
|
Save
|
||||||
|
</ButtonComp>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import ButtonComp from '@/components/base/ButtonComp.vue'
|
import ButtonComp from '@/components/base/ButtonComp.vue'
|
||||||
import { IconRestore } from '@tabler/icons-vue'
|
import { IconDeviceFloppy, IconRestore } from '@tabler/icons-vue'
|
||||||
|
|
||||||
import { useMacroRecorderStore } from '@/stores/macrorecorder'
|
import { useMacroRecorderStore } from '@/stores/macrorecorder'
|
||||||
|
import DialogComp from '@/components/base/DialogComp.vue'
|
||||||
|
import ValidationErrorDialog from '../components/ValidationErrorDialog.vue'
|
||||||
|
import { onMounted, ref } from 'vue'
|
||||||
|
|
||||||
const macroRecorder = useMacroRecorderStore()
|
const macroRecorder = useMacroRecorderStore()
|
||||||
|
|
||||||
|
const errorDialog = ref()
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
macroRecorder.$subscribe((mutation) => {
|
||||||
|
if (mutation.events && mutation.events.key == 'validationErrors') {
|
||||||
|
errorDialog.value.toggleDialog(mutation.events.newValue !== false)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const toggleSave = () => {
|
||||||
|
if (!macroRecorder.save()) errorDialog.value.toggleDialog(true)
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
@reference "@/assets/main.css";
|
@reference "@/assets/main.css";
|
||||||
|
|
||||||
|
.macro-recorder__footer {
|
||||||
|
@apply flex
|
||||||
|
justify-between
|
||||||
|
gap-2;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -1,39 +1,59 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="macro-recorder__header">
|
<div class="macro-recorder__header">
|
||||||
<div :class="`recording__buttons ${macroRecorder.state.edit ? 'disabled' : ''}`">
|
<div class="w-full grid grid-cols-[auto_1fr_auto] gap-2">
|
||||||
<ButtonComp
|
<h4 class="">Name:</h4>
|
||||||
v-if="!macroRecorder.state.record"
|
|
||||||
variant="primary"
|
<input
|
||||||
@click="macroRecorder.state.record = true"
|
id="macro-name"
|
||||||
>
|
type="text"
|
||||||
<IconPlayerRecordFilled class="text-red-500" />Start recording
|
@input.prevent="changeName($event.target.value)"
|
||||||
</ButtonComp>
|
placeholder="New macro"
|
||||||
<ButtonComp
|
/>
|
||||||
v-if="macroRecorder.state.record"
|
<div :class="`recording__buttons ${!nameSet || macroRecorder.state.edit ? 'disabled' : ''}`">
|
||||||
variant="danger"
|
{{ macroRecorder.name }}
|
||||||
@click="macroRecorder.state.record = false"
|
<ButtonComp
|
||||||
>
|
v-if="!macroRecorder.state.record"
|
||||||
<IconPlayerStopFilled class="text-white" />Stop recording
|
variant="primary"
|
||||||
</ButtonComp>
|
size="sm"
|
||||||
|
@click="macroRecorder.state.record = true"
|
||||||
|
>
|
||||||
|
<IconPlayerRecordFilled class="text-red-500" />Record
|
||||||
|
</ButtonComp>
|
||||||
|
<ButtonComp
|
||||||
|
v-if="macroRecorder.state.record"
|
||||||
|
variant="danger"
|
||||||
|
size="sm"
|
||||||
|
@click="macroRecorder.state.record = false"
|
||||||
|
>
|
||||||
|
<IconPlayerStopFilled class="text-white" />Stop
|
||||||
|
</ButtonComp>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div :class="`edit__buttons ${macroRecorder.state.record ? 'disabled' : ''}`">
|
<div
|
||||||
|
v-if="macroRecorder.steps.length > 0"
|
||||||
|
:class="`edit__buttons ${macroRecorder.state.record ? 'disabled' : ''}`"
|
||||||
|
>
|
||||||
<div>
|
<div>
|
||||||
<ButtonComp
|
<ButtonComp
|
||||||
v-if="!macroRecorder.state.edit"
|
v-if="!macroRecorder.state.edit"
|
||||||
variant="secondary"
|
variant="secondary"
|
||||||
|
size="sm"
|
||||||
@click="macroRecorder.state.edit = true"
|
@click="macroRecorder.state.edit = true"
|
||||||
>
|
>
|
||||||
<IconPencil />Edit macro
|
<IconPencil />Edit
|
||||||
</ButtonComp>
|
</ButtonComp>
|
||||||
<ButtonComp
|
<ButtonComp
|
||||||
v-if="macroRecorder.state.edit"
|
v-if="macroRecorder.state.edit"
|
||||||
variant="dark"
|
variant="danger"
|
||||||
|
size="sm"
|
||||||
@click="macroRecorder.resetEdit()"
|
@click="macroRecorder.resetEdit()"
|
||||||
>
|
>
|
||||||
<IconPlayerStopFilled />Stop editing
|
<IconPlayerStopFilled />Stop
|
||||||
</ButtonComp>
|
</ButtonComp>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<FixedDelayMenu v-if="macroRecorder.state.edit" />
|
<FixedDelayMenu v-if="macroRecorder.state.edit" />
|
||||||
|
|
||||||
<EditDialogs />
|
<EditDialogs />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -46,8 +66,16 @@ import FixedDelayMenu from '../components/FixedDelayMenu.vue'
|
||||||
|
|
||||||
import { useMacroRecorderStore } from '@/stores/macrorecorder'
|
import { useMacroRecorderStore } from '@/stores/macrorecorder'
|
||||||
import EditDialogs from './EditDialogs.vue'
|
import EditDialogs from './EditDialogs.vue'
|
||||||
|
import { computed, onMounted, onUpdated, ref } from 'vue'
|
||||||
|
|
||||||
const macroRecorder = useMacroRecorderStore()
|
const macroRecorder = useMacroRecorderStore()
|
||||||
|
|
||||||
|
const nameSet = ref(false)
|
||||||
|
|
||||||
|
function changeName(name) {
|
||||||
|
macroRecorder.changeName(name)
|
||||||
|
nameSet.value = name.length > 0
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
|
@ -55,16 +83,18 @@ const macroRecorder = useMacroRecorderStore()
|
||||||
|
|
||||||
.macro-recorder__header {
|
.macro-recorder__header {
|
||||||
@apply grid
|
@apply grid
|
||||||
grid-cols-[auto_1fr]
|
gap-4
|
||||||
items-end
|
w-full;
|
||||||
gap-4;
|
|
||||||
|
.edit__buttons {
|
||||||
|
@apply flex
|
||||||
|
justify-between
|
||||||
|
gap-2
|
||||||
|
w-full;
|
||||||
|
}
|
||||||
|
|
||||||
> div {
|
> div {
|
||||||
@apply flex gap-4 items-end;
|
@apply flex gap-2 items-end;
|
||||||
|
|
||||||
&.disabled {
|
|
||||||
@apply opacity-50 pointer-events-none cursor-not-allowed;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
|
|
@ -51,4 +51,8 @@ const macroRecorder = useMacroRecorderStore()
|
||||||
top-0 left-0
|
top-0 left-0
|
||||||
h-fit;
|
h-fit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
hr.spacer:last-of-type {
|
||||||
|
@apply hidden;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue