Refactor: Renamed fe->ui

This commit is contained in:
JaxxMoss 2025-05-08 23:12:20 +02:00
parent 67bbbd6baf
commit b85962a539
83 changed files with 0 additions and 4 deletions

98
ui/src/App.vue Normal file
View file

@ -0,0 +1,98 @@
<!--
Macrame is a program that enables the user to create keyboard macros and button panels.
The macros are saved as simple JSON files and can be linked to the button panels. The panels can
be created with HTML and CSS.
Copyright (C) 2025 Jesse Malotaux
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<template>
<div class="app-background">
<img src="./assets/img/bg-gradient.svg" aria-hidden="true" />
<img src="@/assets/img/Macrame-Logo-white.svg" class="logo" aria-hidden="true" />
</div>
<MainMenu />
<RouterView />
<AlertComp
v-if="!isLocal && !handshake && route.fullPath !== '/devices'"
variant="warning"
:page-wide="true"
href="/devices"
>
<h4>Not authorized!</h4>
<p>Click here to start authorization and open the "Devices" page on your PC.</p>
</AlertComp>
</template>
<script setup>
import MainMenu from '@/components/base/MainMenu.vue'
import { onMounted, ref } from 'vue'
import { RouterView, useRoute } from 'vue-router'
import { useDeviceStore } from './stores/device'
import { isLocal } from './services/ApiService'
import AlertComp from './components/base/AlertComp.vue'
const device = useDeviceStore()
const route = useRoute()
const handshake = ref(false)
onMounted(() => {
// Setting device uuid from localstorage
// If not present in LocalStorage a new uuidV4 will be generated
device.uuid()
if (!isLocal) appHandshake()
device.$subscribe(() => {
if (device.key()) handshake.value = true
})
})
async function appHandshake() {
const hsReq = await device.remoteHandshake()
handshake.value = hsReq
}
</script>
<style scoped>
@reference "@/assets/main.css";
.app-background {
@apply fixed
inset-0
size-full
overflow-hidden
pointer-events-none
opacity-40
z-[-1];
img {
@apply absolute
size-full
object-cover;
}
.logo {
@apply absolute
top-[10%]
left-[10%]
scale-[1.8]
opacity-35
mix-blend-overlay;
}
}
</style>

View file

@ -0,0 +1,34 @@
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 140 80"
width="140"
height="80"
>
<g>
<path style="fill:#FFB900;" d="M95.5,18.3l-0.2-0.1C95.2,18.1,95,18,94.8,18c-0.3,0-0.5,0.1-0.7,0.3L82.8,29.6l8.5,8.5l12-12
L95.5,18.3z"/>
<path style="fill:#00BCFF;" d="M46,18.3c-0.2-0.2-0.5-0.3-0.7-0.3c-0.2,0-0.4,0-0.5,0.1l-0.2,0.1l-7.8,7.8h0l12,12l8.5-8.5L46,18.3
z"/>
<path style="fill:#00BCFF;" d="M94.8,67.1L94.7,67l-14-14l-8.5,8.5l11.3,11.3c1,1,2.1,1.8,3.2,2.5c2.5,1.5,5.3,2.2,8.1,2.2
c2.8,0,5.6-0.7,8.1-2.2L94.8,67.1z"/>
<path style="fill:#00BCFF;" d="M127.4,28.9l-13.4-13.4l-7.8-7.8c-0.2-0.2-0.5-0.5-0.7-0.7c-5.3-4.6-12.8-5.2-18.7-1.8
c-1.1,0.7-2.2,1.5-3.2,2.5L72.2,19l2.6,2.6l5.9,5.9L92,16.2c0.8-0.8,1.8-1.1,2.8-1.1c0.7,0,1.4,0.2,2,0.5l0.1-0.1l8.5,8.5
l13.4,13.4c0.8,0.8,1.1,1.8,1.1,2.8c0,1-0.4,2.1-1.1,2.8l-11.3,11.3l5,5l3.5,3.5l11.3-11.3v0c3.1-3.1,4.7-7.2,4.7-11.3
C132,36.1,130.5,32,127.4,28.9z"/>
<g>
<path style="fill:#FFB900;" d="M110.4,61.5l-5-5l0,0l-3.5-3.5l-4.5-4.5L81.2,32.2l-8.5-8.5l-2.6-2.6L56.6,7.7
c-1-1-2.1-1.8-3.2-2.5C47.6,1.8,40,2.4,34.8,7c-0.3,0.2-0.5,0.4-0.7,0.7l-7.8,7.8L12.8,28.9v0C9.7,32,8.1,36.1,8.1,40.2
c0,4.1,1.6,8.2,4.7,11.3l11.3,11.3l3.5-3.5l5-5L21.3,43c-0.8-0.8-1.1-1.8-1.1-2.8c0-1,0.4-2.1,1.1-2.8l13.4-13.4l8.5-8.5l0.1,0.1
c1.5-0.9,3.6-0.7,4.8,0.6l11.3,11.3l0,0l2.1,2.1l8.5,8.5l2.1,2.1l0,0l8.5,8.5l16.2,16.2l0,0L97,65l8.4,8.4
c0.3-0.2,0.5-0.4,0.7-0.7l7.8-7.8L110.4,61.5z"/>
</g>
<g>
<path style="fill:#FFB900;" d="M70.1,42.3L70.1,42.3l-8.5,8.5L45.4,67l-0.1,0.1L40.4,72l-3.2,3.2c2.5,1.5,5.3,2.2,8.1,2.2
c2.8,0,5.6-0.8,8.1-2.2c1.1-0.7,2.2-1.5,3.2-2.5l13.4-13.4l8.5-8.5L70.1,42.3z"/>
</g>
<g>
<path style="fill:#00BCFF;" d="M59.5,31.7L51,40.2L38.2,53l-3.5,3.5l0,0l-5,5L26.2,65l7.8,7.8c0.2,0.2,0.5,0.5,0.7,0.7l3.5-3.5
l4.9-4.9l0.1-0.1l16.2-16.2l8.5-8.5L59.5,31.7z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.8 KiB

View file

@ -0,0 +1,60 @@
<svg
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
x="0px" y="0px"
viewBox="0 0 140 80"
style="enable-background:new 0 0 140 80;"
xml:space="preserve">
<style type="text/css">
.st0{fill:url(#SVGID_1_);}
.st1{fill:url(#SVGID_2_);}
.st2{fill:url(#SVGID_3_);}
.st3{fill:url(#SVGID_4_);}
.st4{fill:url(#SVGID_5_);}
.st5{fill:url(#SVGID_6_);}
.st6{fill:url(#SVGID_7_);}
</style>
<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="-9.094947e-13" y1="28.05" x2="140" y2="28.05">
<stop offset="0" style="stop-color:#FFB900"/>
<stop offset="1" style="stop-color:#00BCFF"/>
</linearGradient>
<path class="st0" d="M95.5,18.3l-0.2-0.1C95.2,18.1,95,18,94.8,18c-0.3,0-0.5,0.1-0.7,0.3L82.8,29.6l8.5,8.5l12-12L95.5,18.3z"/>
<linearGradient id="SVGID_2_" gradientUnits="userSpaceOnUse" x1="-9.094947e-13" y1="28" x2="140" y2="28">
<stop offset="0" style="stop-color:#FFB900"/>
<stop offset="1" style="stop-color:#00BCFF"/>
</linearGradient>
<path class="st1" d="M57.3,29.5L46,18.3c-0.2-0.2-0.5-0.3-0.7-0.3s-0.4,0-0.5,0.1l-0.2,0.1L36.8,26l12,12L57.3,29.5z"/>
<linearGradient id="SVGID_3_" gradientUnits="userSpaceOnUse" x1="-9.094947e-13" y1="65.25" x2="140" y2="65.25">
<stop offset="0" style="stop-color:#FFB900"/>
<stop offset="1" style="stop-color:#00BCFF"/>
</linearGradient>
<path class="st2" d="M94.7,67l-14-14l-8.5,8.5l11.3,11.3c1,1,2.1,1.8,3.2,2.5c2.5,1.5,5.3,2.2,8.1,2.2s5.6-0.7,8.1-2.2L94.7,67
L94.7,67z"/>
<linearGradient id="SVGID_4_" gradientUnits="userSpaceOnUse" x1="-9.094947e-13" y1="32.9162" x2="140" y2="32.9162">
<stop offset="0" style="stop-color:#FFB900"/>
<stop offset="1" style="stop-color:#00BCFF"/>
</linearGradient>
<path class="st3" d="M114,15.5l-7.8-7.8c-0.2-0.2-0.5-0.5-0.7-0.7c-5.3-4.6-12.8-5.2-18.7-1.8c-1.1,0.7-2.2,1.5-3.2,2.5L72.2,19
l2.6,2.6l5.9,5.9L92,16.2c0.8-0.8,1.8-1.1,2.8-1.1c0.7,0,1.4,0.2,2,0.5l0.1-0.1l8.5,8.5l13.4,13.4c0.8,0.8,1.1,1.8,1.1,2.8
s-0.4,2.1-1.1,2.8l-11.3,11.3l5,5l3.5,3.5l11.3-11.3c3.1-3.1,4.7-7.2,4.7-11.3c0-4.1-1.5-8.2-4.6-11.3L114,15.5z"/>
<linearGradient id="SVGID_5_" gradientUnits="userSpaceOnUse" x1="-9.094947e-13" y1="38.2163" x2="140" y2="38.2163">
<stop offset="0" style="stop-color:#FFB900"/>
<stop offset="1" style="stop-color:#00BCFF"/>
</linearGradient>
<path class="st4" d="M105.4,56.5l-3.5-3.5l-4.5-4.5L81.2,32.2l-8.5-8.5l-2.6-2.6L56.6,7.7c-1-1-2.1-1.8-3.2-2.5
C47.6,1.8,40,2.4,34.8,7c-0.3,0.2-0.5,0.4-0.7,0.7l-7.8,7.8L12.8,28.9C9.7,32,8.1,36.1,8.1,40.2c0,4.1,1.6,8.2,4.7,11.3l11.3,11.3
l3.5-3.5l5-5L21.3,43c-0.8-0.8-1.1-1.8-1.1-2.8s0.4-2.1,1.1-2.8L34.7,24l8.5-8.5l0.1,0.1c1.5-0.9,3.6-0.7,4.8,0.6l11.3,11.3l2.1,2.1
l8.5,8.5l2.1,2.1l8.5,8.5l16.2,16.2L97,65l8.4,8.4c0.3-0.2,0.5-0.4,0.7-0.7l7.8-7.8l-3.5-3.4L105.4,56.5z"/>
<linearGradient id="SVGID_6_" gradientUnits="userSpaceOnUse" x1="-9.094947e-13" y1="59.85" x2="140" y2="59.85">
<stop offset="0" style="stop-color:#FFB900"/>
<stop offset="1" style="stop-color:#00BCFF"/>
</linearGradient>
<path class="st5" d="M70.1,42.3l-8.5,8.5L45.4,67l-0.1,0.1L40.4,72l-3.2,3.2c2.5,1.5,5.3,2.2,8.1,2.2s5.6-0.8,8.1-2.2
c1.1-0.7,2.2-1.5,3.2-2.5L70,59.3l8.5-8.5L70.1,42.3z"/>
<linearGradient id="SVGID_7_" gradientUnits="userSpaceOnUse" x1="-9.094947e-13" y1="52.6" x2="140" y2="52.6">
<stop offset="0" style="stop-color:#FFB900"/>
<stop offset="1" style="stop-color:#00BCFF"/>
</linearGradient>
<path class="st6" d="M43.1,65.1l0.1-0.1l16.2-16.2l8.5-8.5l-8.4-8.6L51,40.2L38.2,53l-3.5,3.5l-5,5L26.2,65l7.8,7.8
c0.2,0.2,0.5,0.5,0.7,0.7l3.5-3.5L43.1,65.1z"/>
</svg>

After

Width:  |  Height:  |  Size: 3.5 KiB

View file

@ -0,0 +1,23 @@
<svg
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
x="0px" y="0px"
viewBox="0 0 140 80"
style="enable-background:new 0 0 140 80;"
xml:space="preserve">
<path style="fill:#fff" d="M95.5,18.3l-0.2-0.1C95.2,18.1,95,18,94.8,18c-0.3,0-0.5,0.1-0.7,0.3L82.8,29.6l8.5,8.5l12-12L95.5,18.3z"/>
<path style="fill:#fff" d="M57.3,29.5L46,18.3c-0.2-0.2-0.5-0.3-0.7-0.3s-0.4,0-0.5,0.1l-0.2,0.1L36.8,26l12,12L57.3,29.5z"/>
<path style="fill:#fff" d="M94.7,67l-14-14l-8.5,8.5l11.3,11.3c1,1,2.1,1.8,3.2,2.5c2.5,1.5,5.3,2.2,8.1,2.2s5.6-0.7,8.1-2.2L94.7,67
L94.7,67z"/>
<path style="fill:#fff" d="M114,15.5l-7.8-7.8c-0.2-0.2-0.5-0.5-0.7-0.7c-5.3-4.6-12.8-5.2-18.7-1.8c-1.1,0.7-2.2,1.5-3.2,2.5L72.2,19
l2.6,2.6l5.9,5.9L92,16.2c0.8-0.8,1.8-1.1,2.8-1.1c0.7,0,1.4,0.2,2,0.5l0.1-0.1l8.5,8.5l13.4,13.4c0.8,0.8,1.1,1.8,1.1,2.8
s-0.4,2.1-1.1,2.8l-11.3,11.3l5,5l3.5,3.5l11.3-11.3c3.1-3.1,4.7-7.2,4.7-11.3c0-4.1-1.5-8.2-4.6-11.3L114,15.5z"/>
<path style="fill:#fff" d="M105.4,56.5l-3.5-3.5l-4.5-4.5L81.2,32.2l-8.5-8.5l-2.6-2.6L56.6,7.7c-1-1-2.1-1.8-3.2-2.5
C47.6,1.8,40,2.4,34.8,7c-0.3,0.2-0.5,0.4-0.7,0.7l-7.8,7.8L12.8,28.9C9.7,32,8.1,36.1,8.1,40.2c0,4.1,1.6,8.2,4.7,11.3l11.3,11.3
l3.5-3.5l5-5L21.3,43c-0.8-0.8-1.1-1.8-1.1-2.8s0.4-2.1,1.1-2.8L34.7,24l8.5-8.5l0.1,0.1c1.5-0.9,3.6-0.7,4.8,0.6l11.3,11.3l2.1,2.1
l8.5,8.5l2.1,2.1l8.5,8.5l16.2,16.2L97,65l8.4,8.4c0.3-0.2,0.5-0.4,0.7-0.7l7.8-7.8l-3.5-3.4L105.4,56.5z"/>
<path style="fill:#fff" d="M70.1,42.3l-8.5,8.5L45.4,67l-0.1,0.1L40.4,72l-3.2,3.2c2.5,1.5,5.3,2.2,8.1,2.2s5.6-0.8,8.1-2.2
c1.1-0.7,2.2-1.5,3.2-2.5L70,59.3l8.5-8.5L70.1,42.3z"/>
<path style="fill:#fff" d="M43.1,65.1l0.1-0.1l16.2-16.2l8.5-8.5l-8.4-8.6L51,40.2L38.2,53l-3.5,3.5l-5,5L26.2,65l7.8,7.8
c0.2,0.2,0.5,0.5,0.7,0.7l3.5-3.5L43.1,65.1z"/>
</svg>

After

Width:  |  Height:  |  Size: 1.7 KiB

View file

@ -0,0 +1,41 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 24.2.3, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 2560 1440" style="enable-background:new 0 0 2560 1440;" xml:space="preserve">
<rect style="fill:#020618;" width="2560" height="1440"/>
<radialGradient id="SVGID_1_" cx="1280" cy="720" r="507.7116" fx="1274.7371" fy="1155.8185" gradientTransform="matrix(1 0 0 2.2985 0 -934.9553)" gradientUnits="userSpaceOnUse">
<stop offset="0" style="stop-color:#00BCFF;stop-opacity:0.5"/>
<stop offset="1" style="stop-color:#00BCFF;stop-opacity:0"/>
</radialGradient>
<rect style="opacity:0.55;fill:url(#SVGID_1_);" width="2560" height="1440"/>
<radialGradient id="SVGID_2_" cx="1352.0476" cy="1354.1904" r="1334.0841" gradientUnits="userSpaceOnUse">
<stop offset="0" style="stop-color:#00BCFF;stop-opacity:0.5"/>
<stop offset="1" style="stop-color:#00BCFF;stop-opacity:0"/>
</radialGradient>
<rect style="opacity:0.55;fill:url(#SVGID_2_);" width="2560" height="1440"/>
<radialGradient id="SVGID_3_" cx="1292.0344" cy="1255.0016" r="2246.7517" gradientTransform="matrix(-0.7144 -0.6998 0.1899 -0.1939 1976.6873 2402.437)" gradientUnits="userSpaceOnUse">
<stop offset="0" style="stop-color:#00BCFF;stop-opacity:0.5"/>
<stop offset="1" style="stop-color:#00BCFF;stop-opacity:0"/>
</radialGradient>
<polygon style="opacity:0.55;fill:url(#SVGID_3_);" points="2560,1440 0,1440 0,-7 2560,0 "/>
<radialGradient id="SVGID_4_" cx="1292.0344" cy="1255.8966" r="2246.5256" fx="334.4712" fy="1265.3895" gradientTransform="matrix(0.7144 -0.6998 -0.1899 -0.1939 583.4827 2403.5054)" gradientUnits="userSpaceOnUse">
<stop offset="0" style="stop-color:#00BCFF;stop-opacity:0.5"/>
<stop offset="1" style="stop-color:#00BCFF;stop-opacity:0"/>
</radialGradient>
<polygon style="opacity:0.55;fill:url(#SVGID_4_);" points="0,1440 2560,1440 2560,0 0,0 "/>
<radialGradient id="SVGID_5_" cx="1239.8966" cy="1737.5518" r="877.3733" gradientUnits="userSpaceOnUse">
<stop offset="0" style="stop-color:#FFB900"/>
<stop offset="1" style="stop-color:#FFB900;stop-opacity:0"/>
</radialGradient>
<rect style="fill:url(#SVGID_5_);" width="2560" height="1440"/>
<radialGradient id="SVGID_6_" cx="1287.069" cy="950.5172" r="845.7465" fx="1276.8361" fy="325.8423" gradientTransform="matrix(-1 3.730347e-03 -1.479320e-03 -0.3966 2575.5352 1322.6541)" gradientUnits="userSpaceOnUse">
<stop offset="0" style="stop-color:#FFB900;stop-opacity:0.3"/>
<stop offset="1" style="stop-color:#FFB900;stop-opacity:0"/>
</radialGradient>
<rect style="fill:url(#SVGID_6_);" width="2560" height="1440"/>
<radialGradient id="SVGID_7_" cx="1316.8621" cy="1417.2759" r="1888.6272" gradientTransform="matrix(0.6652 -0.7467 0.1801 0.1604 185.7137 2173.2124)" gradientUnits="userSpaceOnUse">
<stop offset="0" style="stop-color:#FFB900;stop-opacity:0.38"/>
<stop offset="1" style="stop-color:#FFB900;stop-opacity:0"/>
</radialGradient>
<rect style="fill:url(#SVGID_7_);" width="2560" height="1440"/>
</svg>

After

Width:  |  Height:  |  Size: 3.1 KiB

38
ui/src/assets/main.css Normal file
View file

@ -0,0 +1,38 @@
/*
Macrame is a program that enables the user to create keyboard macros and button panels.
The macros are saved as simple JSON files and can be linked to the button panels. The panels can
be created with HTML and CSS.
Copyright (C) 2025 Jesse Malotaux
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
@import './style/_content.css';
@import './style/_form.css';
@import './style/_scrollbar.css';
@import './style/_macro.css';
@import './style/_mcrm-block.css';
@import './style/_panel.css';
@import 'tailwindcss';
@variant dark (&:where(.dark, .dark *));
html,
body,
:not(#panel-html__body) {
--font-sans: 'Roboto', sans-serif;
--font-mono: 'Fira Code', monospace;
}

View file

@ -0,0 +1,68 @@
/*
Macrame is a program that enables the user to create keyboard macros and button panels.
The macros are saved as simple JSON files and can be linked to the button panels. The panels can
be created with HTML and CSS.
Copyright (C) 2025 Jesse Malotaux
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
body {
@apply font-sans font-light tracking-wide bg-slate-900 text-slate-50;
}
h1,
h2 {
@apply font-mono font-bold;
}
h3,
h4,
h5,
h6 {
@apply font-semibold;
}
h1 {
@apply text-4xl;
}
h2 {
@apply text-3xl;
}
h3 {
@apply text-2xl;
}
h4 {
@apply text-xl;
}
h5 {
@apply text-lg;
}
ul {
@apply list-disc list-inside;
}
strong {
@apply font-bold;
}
a {
@apply underline text-amber-400 hover:text-amber-300;
}

View file

@ -0,0 +1,49 @@
/*
Macrame is a program that enables the user to create keyboard macros and button panels.
The macros are saved as simple JSON files and can be linked to the button panels. The panels can
be created with HTML and CSS.
Copyright (C) 2025 Jesse Malotaux
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
.input-group {
@apply grid gap-2;
}
input,
select {
@apply w-full px-2 py-1 text-white border rounded-md border-slate-400 bg-black/20;
}
:has(> input + span) {
@apply flex;
input {
@apply rounded-r-none;
}
span {
@apply flex items-center px-2 text-white rounded-r-md bg-slate-400;
}
}
select option {
@apply bg-slate-700;
&:not([disabled]) {
@apply cursor-pointer;
}
}

View file

@ -0,0 +1,49 @@
/*
Macrame is a program that enables the user to create keyboard macros and button panels.
The macros are saved as simple JSON files and can be linked to the button panels. The panels can
be created with HTML and CSS.
Copyright (C) 2025 Jesse Malotaux
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
/* @reference "main"; */
hr.spacer {
@apply relative
w-6
border
border-gray-300
opacity-80
overflow-visible;
&::before,
&::after {
@apply content-['']
absolute
top-1/2
-translate-y-1/2
size-2
bg-gray-300
rounded-full;
}
&::before {
@apply -left-1;
}
&::after {
@apply -right-1;
}
}

View file

@ -0,0 +1,123 @@
/*
Macrame is a program that enables the user to create keyboard macros and button panels.
The macros are saved as simple JSON files and can be linked to the button panels. The panels can
be created with HTML and CSS.
Copyright (C) 2025 Jesse Malotaux
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
.mcrm-block {
@apply relative p-6 overflow-hidden gap-x-6 gap-y-2 backdrop-blur-lg rounded-2xl;
&::before {
@apply content-['']
absolute
inset-0
p-px
rounded-2xl
size-full
bg-gradient-to-br
to-transparent
z-[10]
pointer-events-none;
mask:
linear-gradient(#000 0 0) exclude,
linear-gradient(#000 0 0) content-box;
}
&.block__light {
@apply bg-white/20;
&::before {
@apply from-white/20;
}
}
&.block__dark {
@apply bg-slate-900/70;
&::before {
@apply from-slate-400/40;
}
}
&.block__primary {
@apply bg-sky-300/20;
&::before {
@apply from-sky-100/20;
}
}
&.block__secondary {
@apply bg-amber-300/20;
&::before {
@apply from-amber-100/20;
}
}
&.block__success {
@apply bg-emerald-300/40;
&::before {
@apply from-emerald-100/40;
}
}
&.block__warning {
@apply bg-orange-300/40;
&::before {
@apply from-orange-100/40;
}
}
&.block__danger {
@apply bg-rose-300/40;
&::before {
@apply from-rose-100/40;
}
}
&.block-spacing__sm,
&.block-size__sm {
@apply p-4 gap-x-4 gap-y-2;
}
&.block-size__sm {
@apply rounded-lg;
&::before {
@apply rounded-lg;
}
}
&.block-spacing__lg,
&.block-size__lg {
@apply p-8 gap-x-8 gap-y-4;
}
&.block-size__lg {
@apply rounded-3xl;
&::before {
@apply rounded-3xl;
}
}
}

View file

@ -0,0 +1,56 @@
/*
Macrame is a program that enables the user to create keyboard macros and button panels.
The macros are saved as simple JSON files and can be linked to the button panels. The panels can
be created with HTML and CSS.
Copyright (C) 2025 Jesse Malotaux
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
.panel {
@apply grid
grid-rows-[auto_1fr]
fixed
top-2
left-4 sm:left-16
right-4 sm:right-16
bottom-2
overflow-hidden;
> .panel__header,
> .panel__title {
@apply px-4 py-2;
/* &:first-child {
@apply pt-4;
}
&:last-child {
@apply pb-4;
} */
}
.panel__title {
@apply pt-3 pl-16 text-transparent bg-gradient-to-r w-fit from-amber-300 to-white/50 sm:pl-4 bg-clip-text;
}
.panel__content {
@apply grid
h-[calc(100%-1rem)]
pt-4 sm:pt-0
pl-0 sm:pl-4
overflow-auto;
}
}

View file

@ -0,0 +1,44 @@
/*
Macrame is a program that enables the user to create keyboard macros and button panels.
The macros are saved as simple JSON files and can be linked to the button panels. The panels can
be created with HTML and CSS.
Copyright (C) 2025 Jesse Malotaux
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
::-webkit-scrollbar {
@apply w-2;
}
::-moz-scrollbar {
@apply w-2;
}
::-webkit-scrollbar-thumb {
@apply rounded bg-slate-400/80;
}
::-moz-scrollbar-thumb {
@apply rounded bg-slate-400/80;
}
::-webkit-scrollbar-track {
@apply mr-1 rounded bg-slate-100/10;
}
::-moz-scrollbar-track {
@apply mr-1 rounded bg-slate-100/10;
}

View file

@ -0,0 +1,119 @@
<!--
Macrame is a program that enables the user to create keyboard macros and button panels.
The macros are saved as simple JSON files and can be linked to the button panels. The panels can
be created with HTML and CSS.
Copyright (C) 2025 Jesse Malotaux
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<template>
<div class="accordion">
<header @click="toggleAccordion(!accordionOpen)">
<h4>{{ title }}</h4>
<ButtonComp variant="ghost" size="sm" class="!px-1">
<IconChevronDown v-if="!accordionOpen" />
<IconChevronUp v-else />
</ButtonComp>
</header>
<section :class="`accordion__wrapper ${accordionOpen ? 'open' : ''}`">
<div class="accordion__content">
<slot />
</div>
</section>
</div>
</template>
<script setup>
import { onMounted, onUpdated, ref } from 'vue'
import ButtonComp from './ButtonComp.vue'
import { IconChevronDown, IconChevronUp } from '@tabler/icons-vue'
const emit = defineEmits(['onOpen', 'onClose', 'onToggle'])
defineExpose({ toggleAccordion })
const props = defineProps({
title: String,
open: Boolean,
})
const accordionOpen = ref(false)
onMounted(() => {
if (props.open) toggleAccordion(props.open)
})
onUpdated(() => {
if (props.open) toggleAccordion(props.open)
})
function toggleAccordion(open = false) {
if (open) {
accordionOpen.value = true
emit('onOpen')
} else {
accordionOpen.value = false
emit('onClose')
}
emit('onToggle')
}
</script>
<style scoped>
@reference "@/assets/main.css";
.accordion {
@apply grid;
header {
@apply grid
grid-cols-[1fr_auto]
px-4 py-2
cursor-pointer;
}
.accordion__wrapper {
@apply grid
grid-rows-[0fr]
border-y
border-b-white/60
border-t-transparent
duration-300
ease-in-out;
.accordion__content {
@apply grid
grid-rows-[0fr]
overflow-hidden
opacity-0
transition-opacity
delay-0;
}
&.open {
@apply grid-rows-[1fr]
border-t-white/20;
.accordion__content {
@apply grid-rows-[1fr]
opacity-100
delay-200;
}
}
}
}
</style>

View file

@ -0,0 +1,100 @@
<!--
Macrame is a program that enables the user to create keyboard macros and button panels.
The macros are saved as simple JSON files and can be linked to the button panels. The panels can
be created with HTML and CSS.
Copyright (C) 2025 Jesse Malotaux
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<template>
<div
:class="`alert alert__${variant} ${pageWide ? 'page-wide' : ''}`"
@click="href ? router.push(href) : null"
>
<IconInfoCircle v-if="variant === 'info'" />
<IconCheck v-if="variant === 'success'" />
<IconExclamationCircle v-if="variant === 'warning'" />
<IconAlertTriangle v-if="variant === 'error'" />
<div class="alert__content">
<slot />
</div>
</div>
</template>
<script setup>
import {
IconAlertTriangle,
IconCheck,
IconExclamationCircle,
IconInfoCircle,
} from '@tabler/icons-vue'
import { useRouter } from 'vue-router'
defineProps({
variant: String, // info, success, warning, error
pageWide: Boolean,
href: String,
})
const router = useRouter()
</script>
<style scoped>
@reference "@/assets/main.css";
.alert {
@apply grid
grid-cols-[1rem_1fr]
items-start
gap-4
p-4
border
border-white/10
bg-white/10
rounded-md
backdrop-blur-md;
&.alert__info {
@apply text-sky-100 bg-sky-400/40;
}
&.alert__success {
@apply text-lime-400 bg-lime-400/10;
}
&.alert__warning {
@apply text-amber-400 bg-amber-400/10;
}
&.alert__error {
@apply text-rose-400 bg-rose-400/10;
}
&.page-wide {
@apply fixed
bottom-0 left-0
w-full;
}
&[href] {
@apply cursor-pointer;
}
.alert__content {
@apply grid gap-2;
}
}
</style>

View file

@ -0,0 +1,189 @@
<!--
Macrame is a program that enables the user to create keyboard macros and button panels.
The macros are saved as simple JSON files and can be linked to the button panels. The panels can
be created with HTML and CSS.
Copyright (C) 2025 Jesse Malotaux
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<template>
<template v-if="href">
<RouterLink :to="href" :class="classString">
<slot />
</RouterLink>
</template>
<template v-else>
<button :class="classString">
<slot />
</button>
</template>
</template>
<script setup>
import { computed } from 'vue'
const props = defineProps({
href: String,
variant: String,
size: String,
})
const classString = computed(() => {
let classes = 'btn'
if (props.variant) classes += ` btn__${props.variant}`
if (props.size) classes += ` btn__${props.size}`
return classes
})
</script>
<style>
@reference "@/assets/main.css";
button,
.btn {
@apply flex
items-center
gap-3
h-fit
px-4 py-2
border
border-solid
rounded-lg
tracking-wide
font-normal
transition-all
cursor-pointer
no-underline;
transition:
border-color 0.1s ease-in-out,
background-color 0.2s ease;
&:not(.button__subtle, .button__ghost):hover {
@apply shadow-black;
}
&[disabled],
&.disabled {
@apply opacity-50 pointer-events-none cursor-not-allowed;
}
svg {
@apply size-5 transition-[stroke] duration-400 ease-in-out;
}
&.btn__sm {
@apply px-3 py-1
text-sm;
svg {
@apply size-4;
}
}
&.btn__lg {
@apply px-6 py-3
text-lg;
svg {
@apply size-6;
}
}
&:hover {
@apply text-white;
svg {
@apply stroke-current;
}
}
&.btn__primary {
@apply bg-sky-100/10 border-sky-100 text-sky-100;
svg {
@apply stroke-sky-200;
}
&:hover {
@apply bg-sky-400/40 border-sky-300;
}
}
&.btn__secondary {
@apply bg-amber-100/10 border-amber-100 text-amber-100;
svg {
@apply stroke-amber-300;
}
&:hover {
@apply bg-amber-400/40 border-amber-400;
}
}
&.btn__danger {
@apply bg-rose-200/20 border-rose-100 text-rose-200;
svg {
@apply stroke-rose-400;
}
&:hover {
@apply bg-rose-400/40 border-rose-500 text-white;
}
}
&.btn__dark {
/* @apply bg-slate-700/80 hover:bg-slate-700 text-white border-slate-600; */
@apply bg-slate-200/10 border-slate-400 text-slate-100;
svg {
@apply stroke-slate-300;
}
&:hover {
@apply bg-slate-400/40 border-slate-200 text-white;
}
}
&.btn__success {
/* @apply bg-lime-500/80 hover:bg-lime-500 text-white border-lime-600; */
@apply bg-lime-200/10 border-lime-100 text-lime-100;
svg {
@apply stroke-lime-400;
}
&:hover {
@apply bg-lime-400/40 border-lime-500 text-white;
}
}
&.btn__subtle {
@apply bg-transparent hover:bg-white/10 text-white border-transparent;
&:hover {
@apply bg-white/20 to-white/30 border-white/40;
}
}
&.btn__ghost {
@apply bg-transparent text-white/80 border-transparent hover:text-white;
}
}
</style>

View file

@ -0,0 +1,36 @@
<!--
Macrame is a program that enables the user to create keyboard macros and button panels.
The macros are saved as simple JSON files and can be linked to the button panels. The panels can
be created with HTML and CSS.
Copyright (C) 2025 Jesse Malotaux
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<template>
<div class="button-group">
<slot />
</div>
</template>
<script setup>
defineProps({
variant: String,
})
</script>
<style scoped>
@reference "@/assets/main.css";
</style>

View file

@ -0,0 +1,103 @@
<!--
Macrame is a program that enables the user to create keyboard macros and button panels.
The macros are saved as simple JSON files and can be linked to the button panels. The panels can
be created with HTML and CSS.
Copyright (C) 2025 Jesse Malotaux
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<template>
<div class="context-menu">
<div class="context-menu__trigger" @click="toggle">
<slot name="trigger" />
</div>
<div :class="`context-menu__content ${menuOpen ? 'open' : ''}`">
<slot name="content" />
</div>
</div>
</template>
<script setup>
import { ref, onMounted, onUpdated } from 'vue'
defineExpose({ toggle })
const props = defineProps({
open: Boolean,
})
const menuOpen = ref(false)
onMounted(() => {
menuOpen.value = props.open
})
function toggle() {
menuOpen.value = !menuOpen.value
}
</script>
<style>
@reference "@/assets/main.css";
.context-menu {
@apply relative;
.context-menu__content {
@apply absolute
top-full
-translate-y-full
opacity-0
pointer-events-none
mt-2
min-w-full
grid
border
border-white/50
bg-slate-100/60
backdrop-blur-3xl
text-slate-800
rounded-md
z-50
transition-all;
&.open {
@apply translate-y-0
opacity-100
pointer-events-auto;
}
}
}
.context-menu ul {
@apply text-slate-800
divide-y
divide-slate-300;
li {
@apply flex
gap-2
items-center
p-2
hover:bg-black/10
cursor-pointer;
svg {
@apply size-5;
}
}
}
</style>

View file

@ -0,0 +1,132 @@
<!--
Macrame is a program that enables the user to create keyboard macros and button panels.
The macros are saved as simple JSON files and can be linked to the button panels. The panels can
be created with HTML and CSS.
Copyright (C) 2025 Jesse Malotaux
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<template>
<div class="dialog-container">
<div class="trigger" @click="toggleDialog(true)">
<slot name="trigger" />
</div>
<dialog ref="dialog" class="mcrm-block block__dark">
<ButtonComp
class="dialog__close p-0"
variant="ghost"
size="sm"
tabindex="-1"
@click="toggleDialog(false)"
>
<IconX />
</ButtonComp>
<slot name="content" />
</dialog>
</div>
</template>
<script setup>
import ButtonComp from './ButtonComp.vue'
import { IconX } from '@tabler/icons-vue'
import { onMounted, onUpdated, ref } from 'vue'
const dialog = ref(null)
const openDialog = ref()
const emit = defineEmits(['onOpen', 'onClose', 'onToggle'])
defineExpose({ toggleDialog })
const props = defineProps({
open: Boolean,
})
onMounted(() => {
if (props.open === true) toggleDialog(props.open)
})
onUpdated(() => {
if (props.open === true) toggleDialog(props.open)
})
function toggleDialog(openToggle) {
if (openToggle) {
dialog.value.showModal()
emit('onOpen')
} else {
dialog.value.close()
emit('onClose')
}
openDialog.value = openToggle
emit('onToggle')
}
onMounted(() => {
openDialog.value = props.open
if (dialog.value.innerHTML.includes('form')) {
dialog.value.querySelector('form').addEventListener('submit', () => {
toggleDialog()
})
}
})
</script>
<style>
@reference "@/assets/main.css";
.dialog-container {
@apply relative;
dialog {
@apply fixed
top-1/2 left-1/2
-translate-x-1/2 -translate-y-1/2
max-w-[calc(100vw-2rem)]
text-slate-200
/* shadow-md */
/* shadow-black */
z-50
pointer-events-none;
&[open] {
@apply pointer-events-auto;
}
&::backdrop {
@apply bg-black/50 backdrop-blur-xs transition;
}
.dialog__close {
@apply absolute
top-4 right-4
p-0
text-white;
svg {
@apply size-5;
}
}
}
}
.dialog__content {
> *:first-child {
@apply pr-8;
}
}
</style>

View file

@ -0,0 +1,59 @@
<!--
Macrame is a program that enables the user to create keyboard macros and button panels.
The macros are saved as simple JSON files and can be linked to the button panels. The panels can
be created with HTML and CSS.
Copyright (C) 2025 Jesse Malotaux
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<template>
<div v-if="loading" class="loading-component">
<span v-if="text">
{{ text }}
</span>
<IconLoader3 class="duration-1000 animate-spin" />
</div>
</template>
<script setup>
import { IconLoader3 } from '@tabler/icons-vue'
defineProps({
loading: Boolean,
text: String,
})
</script>
<style>
@reference "@/assets/main.css";
&:has(.loading-component) {
@apply relative;
}
.loading-component {
@apply absolute
inset-0
size-full
flex gap-2
flex-col
justify-center
items-center
text-sm
bg-black/50
backdrop-blur-md;
}
</style>

View file

@ -0,0 +1,156 @@
<!--
Macrame is a program that enables the user to create keyboard macros and button panels.
The macros are saved as simple JSON files and can be linked to the button panels. The panels can
be created with HTML and CSS.
Copyright (C) 2025 Jesse Malotaux
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<template>
<nav id="main-menu">
<button id="menu-toggle" :class="menuOpen ? 'open' : ''" @click="menuOpen = !menuOpen">
<img
class="p-1 logo"
:class="{ 'opacity-0': menuOpen }"
src="@/assets/img/Macrame-Logo-gradient.svg"
aria-hidden="true"
/>
<IconX :class="{ 'opacity-0': !menuOpen }" />
</button>
<ul :class="menuOpen ? 'open' : ''">
<li>
<RouterLink @click="menuOpen = false" to="/"> <IconHome />Dashboard </RouterLink>
</li>
<li>
<RouterLink @click="menuOpen = false" to="/panels"> <IconLayoutGrid />Panels </RouterLink>
</li>
<li v-if="isLocal()">
<RouterLink @click="menuOpen = false" to="/macros"> <IconKeyboard />Macros </RouterLink>
</li>
<li>
<RouterLink @click="menuOpen = false" to="/devices">
<IconDevices />{{ isLocal() ? 'Devices' : 'Server' }}
</RouterLink>
</li>
<!-- <li>
<RouterLink @click="menuOpen = false" to="/settings">
<IconSettings />Settings
</RouterLink>
</li> -->
</ul>
</nav>
</template>
<script setup>
import { RouterLink } from 'vue-router'
import {
IconDevices,
IconHome,
IconKeyboard,
IconLayoutGrid,
IconSettings,
IconX,
} from '@tabler/icons-vue'
import { ref } from 'vue'
import { isLocal } from '@/services/ApiService'
const menuOpen = ref(false)
</script>
<style>
@reference "@/assets/main.css";
nav {
@apply relative flex z-50;
button {
@apply absolute
top-4 left-4
size-12
rounded-full
aspect-square
bg-white/20 hover:bg-white/40
border-0
cursor-pointer
transition-colors
backdrop-blur-md;
.logo,
svg {
@apply absolute
inset-1/2
-translate-1/2
transition-opacity
duration-400
ease-in-out;
}
.logo {
@apply w-full;
}
}
ul {
@apply absolute
top-20 left-0
-translate-x-full
grid
list-none
rounded-xl
overflow-hidden
bg-white/10
backdrop-blur-md
divide-y
divide-slate-600
transition-transform
duration-300
ease-in-out;
&.open {
@apply left-4 translate-x-0;
}
li {
a {
@apply flex
items-center
gap-2
px-4 py-2
text-white
no-underline
border-transparent
transition-colors;
svg {
@apply text-white/40 transition-colors;
}
&:hover {
@apply bg-white/20;
svg {
@apply text-white;
}
}
&.router-link-active {
@apply text-sky-300
bg-sky-200/20;
}
}
}
}
}
</style>

View file

@ -0,0 +1,107 @@
<!--
Macrame is a program that enables the user to create keyboard macros and button panels.
The macros are saved as simple JSON files and can be linked to the button panels. The panels can
be created with HTML and CSS.
Copyright (C) 2025 Jesse Malotaux
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<template>
<div id="remote-dashboard">
<div id="panels" class="dashboard-block mcrm-block block__light" v-if="server.handshake">
<div class="icon__container">
<IconLayoutGrid />
</div>
<h4>{{ server.panelCount }} {{ server.panelCount != 1 ? 'Panels' : 'Panel' }}</h4>
<template v-if="server.panelCount == 0">
<p><em>No panels found. </em></p>
<p>Learn how to create a panel <a href="#" target="_blank">here</a>.</p>
</template>
<template v-else>
<p>Start using a panel!</p>
<ButtonComp variant="danger" href="/panels"> <IconLayoutGrid /> View panels </ButtonComp>
</template>
</div>
<div id="server" class="dashboard-block mcrm-block block__light">
<div class="icon__container">
<IconServer />
</div>
<h4>Server</h4>
<template v-if="server.handshake">
<p>
Linked with: <strong class="text-center">{{ server.ip }}</strong>
</p>
<ButtonComp variant="primary" href="/devices"> <IconServer /> View server</ButtonComp>
</template>
<template v-else>
<p>
<em>Not linked</em>
</p>
<ButtonComp variant="primary" href="/devices"> <IconLink /> Link with server</ButtonComp>
</template>
</div>
</div>
</template>
<script setup>
import { IconLayoutGrid, IconLink, IconServer } from '@tabler/icons-vue'
import { onMounted, reactive } from 'vue'
import ButtonComp from '../base/ButtonComp.vue'
import { useDeviceStore } from '@/stores/device'
import { usePanelStore } from '@/stores/panel'
const device = useDeviceStore()
const panel = usePanelStore()
const server = reactive({
ip: '',
handshake: '',
panelCount: 0,
})
onMounted(async () => {
const serverIp = await device.serverGetIP()
server.ip = serverIp
if (device.key()) server.handshake = true
device.$subscribe(() => {
if (device.key()) server.handshake = true
})
const panelCount = await panel.getList(true)
server.panelCount = panelCount
})
</script>
<style scoped>
@reference "@/assets/main.css";
#remote-dashboard {
@apply grid
pt-8
gap-4
md:w-fit
h-fit
content-start;
&.not__linked #server {
@apply row-start-1 md:col-start-1;
}
}
</style>

View file

@ -0,0 +1,154 @@
<!--
Macrame is a program that enables the user to create keyboard macros and button panels.
The macros are saved as simple JSON files and can be linked to the button panels. The panels can
be created with HTML and CSS.
Copyright (C) 2025 Jesse Malotaux
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<template>
<div
id="server-dashboard"
:class="`${server.remoteCount == 0 ? 'no__devices' : 'devices__found'} ${server.macroCount == 0 ? 'no__macros' : 'macros__found'}`"
>
<div id="devices" class="dashboard-block mcrm-block block__light">
<div class="icon__container">
<IconDevices />
</div>
<h4>{{ server.remoteCount }} {{ server.remoteCount != 1 ? 'Devices' : 'Device' }}</h4>
<template v-if="server.remoteCount == 0">
<p><em>No devices found.</em></p>
<ButtonComp variant="primary" href="/devices"> <IconLink /> Link a device</ButtonComp>
</template>
<template v-else>
<p>Unlink a device or add new devices.</p>
<ButtonComp variant="primary" href="/devices"><IconDevices /> View devices</ButtonComp>
</template>
</div>
<div id="macros" class="dashboard-block mcrm-block block__light">
<div class="icon__container">
<IconKeyboard />
</div>
<h4>{{ server.macroCount }} {{ server.macroCount != 1 ? 'Macros' : 'Macro' }}</h4>
<template v-if="server.macroCount == 0">
<p><em>No macros found.</em></p>
<ButtonComp variant="secondary" href="/macros"> <IconLayoutGrid /> Create macro</ButtonComp>
</template>
<template v-else>
<p>Edit and view your macros.</p>
<ButtonComp variant="secondary" href="/macros"><IconKeyboard /> View macros</ButtonComp>
</template>
</div>
<div id="panels" class="dashboard-block mcrm-block block__light">
<div class="icon__container">
<IconLayoutGrid />
</div>
<h4>{{ server.panelCount }} {{ server.panelCount != 1 ? 'Panels' : 'Panel' }}</h4>
<template v-if="server.panelCount == 0">
<p><em>No panels found. </em></p>
<p>Learn how to create a panel <a href="#" target="_blank">here</a>.</p>
</template>
<template v-else>
<p>Link macros to panels or view a panel.</p>
<ButtonComp variant="danger" href="/panels"> <IconLayoutGrid /> View panels </ButtonComp>
</template>
</div>
</div>
</template>
<script setup>
import { useDeviceStore } from '@/stores/device'
import { usePanelStore } from '@/stores/panel'
import { IconDevices, IconKeyboard, IconLayoutGrid, IconLink } from '@tabler/icons-vue'
import { onMounted, reactive } from 'vue'
import ButtonComp from '../base/ButtonComp.vue'
import { GetMacroList } from '@/services/MacroService'
const device = useDeviceStore()
const panel = usePanelStore()
const server = reactive({
ip: '',
port: '',
fullPath: '',
remoteCount: 0,
macroCount: 0,
panelCount: 0,
})
onMounted(async () => {
const serverIP = await device.serverGetIP()
server.ip = serverIP
// server.port = window.__CONFIG__.MCRM__PORT
// server.fullPath = `http://${server.ip}:${server.port}`
const remoteCount = await device.serverGetRemotes(true)
server.remoteCount = remoteCount
const macroCount = await GetMacroList(true)
server.macroCount = macroCount
const panelCount = await panel.getList(true)
server.panelCount = panelCount
console.log(server)
})
</script>
<style scoped>
@reference "@/assets/main.css";
#server-dashboard {
@apply grid
grid-cols-1
grid-rows-3
md:grid-cols-3
md:grid-rows-1
gap-4
w-fit
h-fit
pt-8;
&.no__devices #devices {
@apply row-start-1 md:col-start-1;
}
&.no__macros.devices__found #devices {
@apply row-start-3 md:col-start-3;
}
&.devices__found #devices {
@apply row-start-3 md:col-start-3;
}
&.no__devices.no__macros #macros {
@apply row-start-2 md:col-start-2;
}
&.no__macros #macros {
@apply row-start-1 md:col-start-1;
}
&.macros__found #macros {
@apply row-start-2 md:col-start-2;
}
&.no__devices.macros__found #macros {
@apply row-start-3 md:col-start-3;
}
}
</style>

View file

@ -0,0 +1,225 @@
<!--
Macrame is a program that enables the user to create keyboard macros and button panels.
The macros are saved as simple JSON files and can be linked to the button panels. The panels can
be created with HTML and CSS.
Copyright (C) 2025 Jesse Malotaux
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<template>
<div class="server-overview">
<AlertComp variant="info">
<strong>This is a remote device.</strong>
<em>UUID: {{ device.uuid() }} </em>
</AlertComp>
<div class="grid gap-4 mcrm-block block__light">
<h4 class="flex items-center justify-between gap-4 text-lg">
<span class="flex gap-4"><IconServer />Server</span>
<ButtonComp variant="primary" @click="checkServerStatus()"><IconReload /></ButtonComp>
</h4>
<p>
Connected to: <strong>{{ server.host }}</strong>
</p>
<!-- Alerts -->
<AlertComp v-if="server.status === 'authorized'" variant="success">Authorized</AlertComp>
<AlertComp v-if="server.status === 'unlinked'" variant="warning">Not linked</AlertComp>
<AlertComp v-if="server.status === 'unauthorized'" variant="info">
<div class="grid gap-2">
<strong>Access requested</strong>
<ul class="mb-4">
<li>
Navigate to <em class="font-semibold">http://localhost:{{ server.port }}/devices</em>.
</li>
<li>
<div class="inline-flex flex-wrap items-center gap-2 w-fit">
Click on
<span class="flex items-center gap-1 p-1 text-sm border rounded-sm">
<IconLink class="size-4" /> Link device
</span>
</div>
</li>
<li>Enter the the pin shown on the desktop in the dialog that will appear.</li>
</ul>
<template v-if="server.link == 'checking'">
<div class="grid grid-cols-[2rem_1fr] gap-2">
<IconReload class="animate-spin" />
Checking server for link...
</div>
</template>
<template v-if="server.link === false">
<ButtonComp variant="subtle" @click="pingLink()" class="w-fit">
<IconReload />Check for server link
</ButtonComp>
</template>
</div>
</AlertComp>
<ButtonComp
variant="danger"
v-if="server.status === 'authorized'"
@click="disonnectFromServer()"
>
<IconPlugConnectedX />
Disconnect
</ButtonComp>
</div>
<DialogComp ref="linkPinDialog">
<template #content>
<div class="grid w-64 gap-4">
<h3>Server link pin:</h3>
<form class="grid gap-4" @submit.prevent="decryptKey()">
<input
ref="linkPinInput"
class="input"
id="input-pin"
type="text"
pattern="[0-9]{4}"
v-model="server.inputPin"
autocomplete="off"
/>
<ButtonComp variant="primary">Enter</ButtonComp>
</form>
</div>
</template>
</DialogComp>
</div>
</template>
<script setup>
// TODO
// - [Delete local key button]
// - if not local key
// - - if !checkAccess -> requestAccess -> put settings.json (go)
// - - if checkAccess -> pingLink -> check for device.tmp (go)
// - - if [devicePin] -> handshake -> save key local, close dialog, update server status
import { IconKey, IconLink, IconPlugConnectedX, IconReload, IconServer } from '@tabler/icons-vue'
import AlertComp from '../base/AlertComp.vue'
import ButtonComp from '../base/ButtonComp.vue'
import { onMounted, onUpdated, reactive, ref } from 'vue'
import { useDeviceStore } from '@/stores/device'
import { deviceType, deviceModel, deviceVendor } from '@basitcodeenv/vue3-device-detect'
import DialogComp from '../base/DialogComp.vue'
import { AuthCall, decryptAES } from '@/services/EncryptService'
import axios from 'axios'
import { appUrl } from '@/services/ApiService'
const device = useDeviceStore()
const linkPinDialog = ref()
const linkPinInput = ref()
const server = reactive({
host: '',
port: window.__CONFIG__.MCRM__PORT,
status: false,
link: false,
inputPin: '',
encryptedKey: '',
key: '',
})
onMounted(async () => {
server.host = window.location.host
})
onUpdated(() => {
if (!server.status) checkServerStatus()
if (server.status === 'authorized' && server.inputPin) server.inputPin = ''
})
async function checkServerStatus(request = true) {
const status = await device.remoteCheckServerAccess()
server.status = status
if (status === 'unlinked' || status === 'unauthorized') {
if (request) requestAccess()
return true
}
if (!device.key()) {
server.status = 'unauthorized'
return true
}
const handshake = await device.remoteHandshake(device.key())
if (handshake) server.key = device.key()
else {
device.removeDeviceKey()
server.status = 'unlinked'
if (request) requestAccess()
}
return true
}
function requestAccess() {
let deviceName = `${deviceVendor() ? deviceVendor() : 'Unknown'} ${deviceVendor() ? deviceModel() : deviceType()}`
device.remoteRequestServerAccess(deviceName, deviceType()).then((data) => {
if (data.data) (server.status = data.data), pingLink()
})
}
function pingLink() {
server.link = 'checking'
device.remotePingLink((encryptedKey) => {
server.link = true
server.encryptedKey = encryptedKey
linkPinDialog.value.toggleDialog(true)
linkPinInput.value.focus()
})
}
async function decryptKey() {
const decryptedKey = decryptAES(server.inputPin, server.encryptedKey)
const handshake = await device.remoteHandshake(decryptedKey)
if (handshake) {
device.setDeviceKey(decryptedKey)
server.key = decryptedKey
linkPinDialog.value.toggleDialog(false)
server.status = 'authorized'
}
}
function disonnectFromServer() {
axios.post(appUrl() + '/device/link/remove', AuthCall({ uuid: device.uuid() })).then((data) => {
if (data.data) checkServerStatus(false)
})
}
</script>
<style scoped>
@reference "@/assets/main.css";
.server-overview {
@apply grid
gap-4
content-start;
}
#input-pin {
}
</style>

View file

@ -0,0 +1,254 @@
<!--
Macrame is a program that enables the user to create keyboard macros and button panels.
The macros are saved as simple JSON files and can be linked to the button panels. The panels can
be created with HTML and CSS.
Copyright (C) 2025 Jesse Malotaux
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<template>
<div class="device-overview">
<AlertComp variant="info">
<strong>This is a server!</strong>
<em>UUID: {{ device.uuid() }} </em>
</AlertComp>
<div class="flex flex-wrap items-start gap-4 mcrm-block block__light">
<h4 class="flex items-center justify-between w-full gap-4 mb-4">
<span class="flex gap-4">
<IconDevices />{{ Object.keys(remote.devices).length }}
{{ Object.keys(remote.devices).length == 1 ? 'Device' : 'Devices' }}
</span>
<ButtonComp v-if="!remote.poll" variant="primary" @click="device.serverGetRemotes()"
><IconReload
/></ButtonComp>
</h4>
<template v-if="Object.keys(remote.devices).length > 0">
<template v-for="(remoteDevice, id) in remote.devices" :key="id">
<div class="mcrm-block block__dark block-size__sm w-64 grid !gap-4 content-start">
<div class="grid gap-2">
<h5 class="grid grid-cols-[auto_1fr] gap-2">
<IconDeviceUnknown v-if="remoteDevice.settings.type == 'unknown'" />
<IconDeviceMobile v-if="remoteDevice.settings.type == 'mobile'" />
<IconDeviceTablet v-if="remoteDevice.settings.type == 'tablet'" />
<IconDeviceDesktop v-if="remoteDevice.settings.type == 'desktop'" />
<span class="w-full truncate">
{{ remoteDevice.settings.name }}
</span>
</h5>
<em>{{ id }}</em>
</div>
<template v-if="remoteDevice.key">
<AlertComp variant="success">Authorized</AlertComp>
<ButtonComp variant="danger" @click="unlinkDevice(id)">
<IconLinkOff />Unlink device
</ButtonComp>
</template>
<template v-else>
<AlertComp variant="warning">Unauthorized</AlertComp>
<ButtonComp variant="primary" @click="startLink(id)">
<IconLink />Link device
</ButtonComp>
</template>
<template v-if="remote.pinlink.uuid == id">
<AlertComp variant="info">One time pin: {{ remote.pinlink.pin }}</AlertComp>
</template>
</div>
</template>
</template>
<!-- <template v-else>
<div class="grid w-full gap-4">
<em class="text-slate-300">No remote devices</em>
</div>
</template> -->
<AccordionComp
class="w-full mt-8 border-t border-t-white/50"
title="How to connect a device?"
:open="Object.keys(remote.devices).length == 0"
>
<div class="grid py-4">
<ul class="space-y-2">
<li>
Scan the QR code with the remote device.
<div class="grid gap-4 py-4 pl-6">
<canvas ref="serverQr"></canvas>
<p>
Or manually type the IP address: <br />
<strong>{{ server.ip }}/devices</strong>
</p>
</div>
</li>
<li>
The device will automatically request access, if you see "Access requested" on the
device.
</li>
<li v-if="!remote.poll">
<div class="inline-flex items-center gap-2">
Click the
<span class="p-1 border rounded-sm"><IconReload class="size-4" /></span> to reload
the devices.
</div>
</li>
<li>
<div class="inline-flex flex-wrap items-center gap-2 w-fit">
Click on
<span class="flex items-center gap-1 p-1 text-sm border rounded-sm">
<IconLink class="size-4" /> Link device
</span>
A one-time-pin will be shown in a dialog.
</div>
</li>
<li>Enter the pin on the remote device.</li>
<li>
Congratulations! You have linked a device! You can now start using panels on that
device.
</li>
</ul>
</div>
</AccordionComp>
<DialogComp ref="pinDialog">
<template #content>
<div class="grid gap-4">
<h3>Pin code</h3>
<span class="font-mono text-4xl tracking-wide">{{ remote.pinlink.pin }}</span>
</div>
</template>
</DialogComp>
</div>
</div>
</template>
<script setup>
import { onMounted, onUpdated, reactive, ref } from 'vue'
import AlertComp from '../base/AlertComp.vue'
import { useDeviceStore } from '@/stores/device'
import {
IconDevices,
IconDeviceDesktop,
IconDeviceMobile,
IconDeviceTablet,
IconDeviceUnknown,
IconLink,
IconLinkOff,
IconReload,
} from '@tabler/icons-vue'
import ButtonComp from '../base/ButtonComp.vue'
import DialogComp from '../base/DialogComp.vue'
import axios from 'axios'
import { appUrl } from '@/services/ApiService'
import AccordionComp from '../base/AccordionComp.vue'
import QRCode from 'qrcode'
const device = useDeviceStore()
const pinDialog = ref()
const serverQr = ref()
const server = reactive({
ip: '',
})
const remote = reactive({ devices: [], pinlink: false, poll: false })
onMounted(async () => {
device.serverGetRemotes()
device.$subscribe((mutation, state) => {
if (state.remote !== remote.devices) remote.devices = device.remote
})
getIp()
})
onUpdated(() => {
getIp()
if (Object.keys(remote.devices).length == 0 && !remote.poll) {
remote.poll = setInterval(() => {
device.serverGetRemotes()
}, 1000)
}
if (Object.keys(remote.devices).length > 0 && remote.poll) {
clearInterval(remote.poll)
remote.poll = false
}
})
async function getIp() {
const serverIP = await device.serverGetIP()
server.ip = serverIP
QRCode.toCanvas(serverQr.value, `${server.ip}/devices`, (error) => {
if (error) console.log('QRCode error: ', error)
})
}
async function startLink(deviceUuid) {
const pin = await device.serverStartLink(deviceUuid)
remote.pinlink = { uuid: deviceUuid, pin: pin }
pinDialog.value.toggleDialog(true)
pollLink()
setTimeout(() => {
resetPinLink()
}, 60000)
}
function pollLink() {
const pollInterval = setInterval(() => {
axios.post(appUrl() + '/device/link/poll', { uuid: remote.pinlink.uuid }).then((data) => {
if (!data.data) {
clearInterval(pollInterval)
resetPinLink()
device.serverGetRemotes()
}
})
}, 1000)
}
function resetPinLink() {
remote.pinlink = false
if (pinDialog.value) pinDialog.value.toggleDialog(false)
}
function unlinkDevice(id) {
axios.post(appUrl() + '/device/link/remove', { uuid: id }).then((data) => {
if (data.data) {
device.serverGetRemotes()
}
})
}
</script>
<style scoped>
@reference "@/assets/main.css";
.device-overview {
@apply grid
gap-4
content-start;
}
</style>

View file

@ -0,0 +1,176 @@
<!--
Macrame is a program that enables the user to create keyboard macros and button panels.
The macros are saved as simple JSON files and can be linked to the button panels. The panels can
be created with HTML and CSS.
Copyright (C) 2025 Jesse Malotaux
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<template>
<div class="input-group form-select">
<label v-if="label">
{{ label }}
</label>
<div class="select__container">
<template v-if="search">
<div class="select__search-bar">
<input
type="search"
ref="selectSearch"
:list="`${name}-search__options`"
v-model="select.search"
@change="selectSearchValue($event)"
:disabled="!select.searchActive"
autocomplete="on"
/>
<datalist :id="`${name}-search__options`">
<option v-for="option in select.options" :key="option.value" :value="option.value">
{{ option.label }}
</option>
</datalist>
<ButtonComp v-if="!select.searchActive" variant="ghost" size="sm" @click="initSearch">
<IconSearch />
</ButtonComp>
<ButtonComp v-else variant="ghost" size="sm" @click="resetSearch">
<IconSearchOff />
</ButtonComp>
</div>
</template>
<select :name="name" ref="selectEl" v-model="select.value" @change="changeSelect($event)">
<option value="" disabled>- Select {{ label.toLocaleLowerCase() }} -</option>
<option v-for="option in select.options" :key="option.value" :value="option.value">
{{ option.label }}
</option>
</select>
</div>
</div>
</template>
<script setup>
import { IconSearch, IconSearchOff } from '@tabler/icons-vue'
import { onMounted, onUpdated, reactive, ref } from 'vue'
import ButtonComp from '../base/ButtonComp.vue'
const emit = defineEmits(['change'])
const props = defineProps({
label: String,
name: String,
options: [Array, Object],
search: Boolean,
value: String,
})
const select = reactive({
options: [],
search: '',
searchActive: false,
changed: false,
value: '',
})
const selectEl = ref(null)
const selectSearch = ref(null)
onMounted(() => {
setValue()
if (typeof props.options == 'object') select.options = Object.values(props.options)
})
onUpdated(() => {
setValue()
})
const setValue = () => {
if ((select.value == '' && props.value) || (!select.changed && props.value != select.value)) {
select.value = props.value
}
select.changed = false
}
const initSearch = () => {
select.searchActive = true
select.search = ''
selectEl.value.classList = 'search__is-active'
setTimeout(() => {
selectSearch.value.focus()
}, 50)
}
const resetSearch = () => {
select.search = ''
select.searchActive = false
selectEl.value.classList = ''
}
const selectSearchValue = (event) => {
changeSelect(event)
resetSearch()
}
const changeSelect = (event) => {
select.changed = true
select.value = event.target.value
emit('change', select.value)
}
</script>
<style scoped>
@reference "@/assets/main.css";
.select__container {
@apply relative
h-8;
select,
.select__search-bar {
@apply absolute top-0 h-8;
}
}
.select__search-bar {
@apply right-0
grid
grid-cols-[1fr_auto]
items-center
w-full
pr-4
z-10
pointer-events-none;
button {
@apply pointer-events-auto;
}
input {
@apply border-0 bg-transparent pointer-events-auto px-2 py-0 focus:outline-0;
&[disabled] {
@apply pointer-events-none;
}
}
datalist {
@apply absolute
top-full left-0;
}
}
select.search__is-active {
@apply text-transparent;
}
</style>

View file

@ -0,0 +1,7 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor">
<path
d="M15 4a1 1 0 1 0 0 2V4zm0 11v-1a1 1 0 0 0-1 1h1zm0 4l-.707.707A1 1 0 0 0 16 19h-1zm-4-4l.707-.707A1 1 0 0 0 11 14v1zm-4.707-1.293a1 1 0 0 0-1.414 1.414l1.414-1.414zm-.707.707l-.707-.707.707.707zM9 11v-1a1 1 0 0 0-.707.293L9 11zm-4 0h1a1 1 0 0 0-1-1v1zm0 4H4a1 1 0 0 0 1.707.707L5 15zm10-9h2V4h-2v2zm2 0a1 1 0 0 1 1 1h2a3 3 0 0 0-3-3v2zm1 1v6h2V7h-2zm0 6a1 1 0 0 1-1 1v2a3 3 0 0 0 3-3h-2zm-1 1h-2v2h2v-2zm-3 1v4h2v-4h-2zm1.707 3.293l-4-4-1.414 1.414 4 4 1.414-1.414zM11 14H7v2h4v-2zm-4 0c-.276 0-.525-.111-.707-.293l-1.414 1.414C5.42 15.663 6.172 16 7 16v-2zm-.707 1.121l3.414-3.414-1.414-1.414-3.414 3.414 1.414 1.414zM9 12h4v-2H9v2zm4 0a3 3 0 0 0 3-3h-2a1 1 0 0 1-1 1v2zm3-3V3h-2v6h2zm0-6a3 3 0 0 0-3-3v2a1 1 0 0 1 1 1h2zm-3-3H3v2h10V0zM3 0a3 3 0 0 0-3 3h2a1 1 0 0 1 1-1V0zM0 3v6h2V3H0zm0 6a3 3 0 0 0 3 3v-2a1 1 0 0 1-1-1H0zm3 3h2v-2H3v2zm1-1v4h2v-4H4zm1.707 4.707l.586-.586-1.414-1.414-.586.586 1.414 1.414z"
/>
</svg>
</template>

View file

@ -0,0 +1,7 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="17" fill="currentColor">
<path
d="M11 2.253a1 1 0 1 0-2 0h2zm-2 13a1 1 0 1 0 2 0H9zm.447-12.167a1 1 0 1 0 1.107-1.666L9.447 3.086zM1 2.253L.447 1.42A1 1 0 0 0 0 2.253h1zm0 13H0a1 1 0 0 0 1.553.833L1 15.253zm8.447.833a1 1 0 1 0 1.107-1.666l-1.107 1.666zm0-14.666a1 1 0 1 0 1.107 1.666L9.447 1.42zM19 2.253h1a1 1 0 0 0-.447-.833L19 2.253zm0 13l-.553.833A1 1 0 0 0 20 15.253h-1zm-9.553-.833a1 1 0 1 0 1.107 1.666L9.447 14.42zM9 2.253v13h2v-13H9zm1.553-.833C9.203.523 7.42 0 5.5 0v2c1.572 0 2.961.431 3.947 1.086l1.107-1.666zM5.5 0C3.58 0 1.797.523.447 1.42l1.107 1.666C2.539 2.431 3.928 2 5.5 2V0zM0 2.253v13h2v-13H0zm1.553 13.833C2.539 15.431 3.928 15 5.5 15v-2c-1.92 0-3.703.523-5.053 1.42l1.107 1.666zM5.5 15c1.572 0 2.961.431 3.947 1.086l1.107-1.666C9.203 13.523 7.42 13 5.5 13v2zm5.053-11.914C11.539 2.431 12.928 2 14.5 2V0c-1.92 0-3.703.523-5.053 1.42l1.107 1.666zM14.5 2c1.573 0 2.961.431 3.947 1.086l1.107-1.666C18.203.523 16.421 0 14.5 0v2zm3.5.253v13h2v-13h-2zm1.553 12.167C18.203 13.523 16.421 13 14.5 13v2c1.573 0 2.961.431 3.947 1.086l1.107-1.666zM14.5 13c-1.92 0-3.703.523-5.053 1.42l1.107 1.666C11.539 15.431 12.928 15 14.5 15v-2z"
/>
</svg>
</template>

View file

@ -0,0 +1,7 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="20" fill="currentColor">
<path
d="M11.447 8.894a1 1 0 1 0-.894-1.789l.894 1.789zm-2.894-.789a1 1 0 1 0 .894 1.789l-.894-1.789zm0 1.789a1 1 0 1 0 .894-1.789l-.894 1.789zM7.447 7.106a1 1 0 1 0-.894 1.789l.894-1.789zM10 9a1 1 0 1 0-2 0h2zm-2 2.5a1 1 0 1 0 2 0H8zm9.447-5.606a1 1 0 1 0-.894-1.789l.894 1.789zm-2.894-.789a1 1 0 1 0 .894 1.789l-.894-1.789zm2 .789a1 1 0 1 0 .894-1.789l-.894 1.789zm-1.106-2.789a1 1 0 1 0-.894 1.789l.894-1.789zM18 5a1 1 0 1 0-2 0h2zm-2 2.5a1 1 0 1 0 2 0h-2zm-5.447-4.606a1 1 0 1 0 .894-1.789l-.894 1.789zM9 1l.447-.894a1 1 0 0 0-.894 0L9 1zm-2.447.106a1 1 0 1 0 .894 1.789l-.894-1.789zm-6 3a1 1 0 1 0 .894 1.789L.553 4.106zm2.894.789a1 1 0 1 0-.894-1.789l.894 1.789zm-2-.789a1 1 0 1 0-.894 1.789l.894-1.789zm1.106 2.789a1 1 0 1 0 .894-1.789l-.894 1.789zM2 5a1 1 0 1 0-2 0h2zM0 7.5a1 1 0 1 0 2 0H0zm8.553 12.394a1 1 0 1 0 .894-1.789l-.894 1.789zm-1.106-2.789a1 1 0 1 0-.894 1.789l.894-1.789zm1.106 1a1 1 0 1 0 .894 1.789l-.894-1.789zm2.894.789a1 1 0 1 0-.894-1.789l.894 1.789zM8 19a1 1 0 1 0 2 0H8zm2-2.5a1 1 0 1 0-2 0h2zm-7.447.394a1 1 0 1 0 .894-1.789l-.894 1.789zM1 15H0a1 1 0 0 0 .553.894L1 15zm1-2.5a1 1 0 1 0-2 0h2zm12.553 2.606a1 1 0 1 0 .894 1.789l-.894-1.789zM17 15l.447.894A1 1 0 0 0 18 15h-1zm1-2.5a1 1 0 1 0-2 0h2zm-7.447-5.394l-2 1 .894 1.789 2-1-.894-1.789zm-1.106 1l-2-1-.894 1.789 2 1 .894-1.789zM8 9v2.5h2V9H8zm8.553-4.894l-2 1 .894 1.789 2-1-.894-1.789zm.894 0l-2-1-.894 1.789 2 1 .894-1.789zM16 5v2.5h2V5h-2zm-4.553-3.894l-2-1-.894 1.789 2 1 .894-1.789zm-2.894-1l-2 1 .894 1.789 2-1L8.553.106zM1.447 5.894l2-1-.894-1.789-2 1 .894 1.789zm-.894 0l2 1 .894-1.789-2-1-.894 1.789zM0 5v2.5h2V5H0zm9.447 13.106l-2-1-.894 1.789 2 1 .894-1.789zm0 1.789l2-1-.894-1.789-2 1 .894 1.789zM10 19v-2.5H8V19h2zm-6.553-3.894l-2-1-.894 1.789 2 1 .894-1.789zM2 15v-2.5H0V15h2zm13.447 1.894l2-1-.894-1.789-2 1 .894 1.789zM18 15v-2.5h-2V15h2z"
/>
</svg>
</template>

View file

@ -0,0 +1,7 @@
<template>
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor">
<path
d="M10 3.22l-.61-.6a5.5 5.5 0 0 0-7.666.105 5.5 5.5 0 0 0-.114 7.665L10 18.78l8.39-8.4a5.5 5.5 0 0 0-.114-7.665 5.5 5.5 0 0 0-7.666-.105l-.61.61z"
/>
</svg>
</template>

View file

@ -0,0 +1,19 @@
<!-- This icon is from <https://github.com/Templarian/MaterialDesign>, distributed under Apache 2.0 (https://www.apache.org/licenses/LICENSE-2.0) license-->
<template>
<svg
xmlns="http://www.w3.org/2000/svg"
xmlns:xlink="http://www.w3.org/1999/xlink"
aria-hidden="true"
role="img"
class="iconify iconify--mdi"
width="24"
height="24"
preserveAspectRatio="xMidYMid meet"
viewBox="0 0 24 24"
>
<path
d="M20 18v-4h-3v1h-2v-1H9v1H7v-1H4v4h16M6.33 8l-1.74 4H7v-1h2v1h6v-1h2v1h2.41l-1.74-4H6.33M9 5v1h6V5H9m12.84 7.61c.1.22.16.48.16.8V18c0 .53-.21 1-.6 1.41c-.4.4-.85.59-1.4.59H4c-.55 0-1-.19-1.4-.59C2.21 19 2 18.53 2 18v-4.59c0-.32.06-.58.16-.8L4.5 7.22C4.84 6.41 5.45 6 6.33 6H7V5c0-.55.18-1 .57-1.41C7.96 3.2 8.44 3 9 3h6c.56 0 1.04.2 1.43.59c.39.41.57.86.57 1.41v1h.67c.88 0 1.49.41 1.83 1.22l2.34 5.39z"
fill="currentColor"
></path>
</svg>
</template>

View file

@ -0,0 +1,164 @@
<!--
Macrame is a program that enables the user to create keyboard macros and button panels.
The macros are saved as simple JSON files and can be linked to the button panels. The panels can
be created with HTML and CSS.
Copyright (C) 2025 Jesse Malotaux
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<template>
<div class="macro-overview mcrm-block block__dark">
<h4 class="border-b-2 border-transparent">Saved Macros</h4>
<div class="macro-overview__list">
<LoadComp :loading="macros.loading" text="Loading macros..." />
<div class="macro-item" v-for="(macro, i) in macros.list" :key="i">
<ButtonComp
:variant="macroRecorder.macroName === macro.name ? 'secondary' : 'dark'"
class="overview__macro-open"
size="sm"
@click="macroRecorder.openMacro(macro.macroname, macro.name)"
>
<IconKeyboard /> <span>{{ macro.name }}</span>
</ButtonComp>
<div class="overview__macro-delete">
<ButtonComp
class="!text-red-500 hover:!text-red-300"
variant="ghost"
size="sm"
@click="startDelete(macro.name)"
>
<IconTrash />
</ButtonComp>
</div>
</div>
</div>
<DialogComp ref="deleteDialog">
<template #content>
<div class="grid gap-2">
<h4 class="pr-4">Are you sure you want to delete:</h4>
<h3 class="mb-2 text-center text-sky-500">{{ macroToBeDeleted }}</h3>
<div class="flex justify-between">
<ButtonComp size="sm" variant="subtle" @click="deleteDialog.toggleDialog(false)">
No
</ButtonComp>
<ButtonComp size="sm" variant="danger" @click="deleteMacro()">Yes</ButtonComp>
</div>
</div>
</template>
</DialogComp>
</div>
</template>
<script setup>
// TODO
// - delete macro
import { IconKeyboard, IconTrash } from '@tabler/icons-vue'
import ButtonComp from '../base/ButtonComp.vue'
import { onMounted, reactive, ref } from 'vue'
import { GetMacroList } from '@/services/MacroService'
import LoadComp from '../base/LoadComp.vue'
import { useMacroRecorderStore } from '@/stores/macrorecorder'
import DialogComp from '../base/DialogComp.vue'
const macros = reactive({
loading: true,
list: [],
})
const macroRecorder = useMacroRecorderStore()
const macroToBeDeleted = ref('')
const deleteDialog = ref()
onMounted(() => {
loadMacroList()
})
const loadMacroList = async () => {
const list = await GetMacroList()
macros.list = list
macros.loading = false
}
const startDelete = (macroFilename) => {
macroToBeDeleted.value = macroFilename
deleteDialog.value.toggleDialog(true)
}
const deleteMacro = async () => {
const resp = await macroRecorder.deleteMacro(macroToBeDeleted.value)
if (resp) {
deleteDialog.value.toggleDialog(false)
if (macroToBeDeleted.value === macroRecorder.macroName) macroRecorder.resetMacro()
macroToBeDeleted.value = ''
loadMacroList()
}
}
</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 flex
flex-col
pr-1
-mr-1
gap-1
h-[calc(100vh-11.7rem)]
overflow-auto;
}
.macro-item {
@apply grid items-center grid-cols-[1fr_0fr] transition-[grid-template-columns] delay-0 duration-300;
&:hover {
@apply grid-cols-[1fr_auto] delay-500;
}
button.overview__macro-open {
@apply w-full grid grid-cols-[1rem_1fr] justify-items-start;
span {
@apply truncate w-full text-left;
}
}
div.overview__macro-delete {
@apply grid overflow-hidden transition;
}
}
}
</style>

View file

@ -0,0 +1,106 @@
<!--
Macrame is a program that enables the user to create keyboard macros and button panels.
The macros are saved as simple JSON files and can be linked to the button panels. The panels can
be created with HTML and CSS.
Copyright (C) 2025 Jesse Malotaux
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<template>
<div class="macro-recorder mcrm-block block__light">
<div class="recorder-interface">
<!-- 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>
</div>
</template>
<script setup>
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";
.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 {
@apply relative
w-full
rounded-lg
bg-slate-950/50
border
border-slate-600
overflow-auto
transition-colors;
&.record {
@apply border-rose-300 bg-rose-400/10;
}
&.edit {
@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>

View file

@ -0,0 +1,78 @@
<!--
Macrame is a program that enables the user to create keyboard macros and button panels.
The macros are saved as simple JSON files and can be linked to the button panels. The panels can
be created with HTML and CSS.
Copyright (C) 2025 Jesse Malotaux
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<template>
<span :class="`delay ${active ? 'active' : ''} ${preset ? 'preset' : ''}`">
<template v-if="value < 10000"> {{ value }} <i>ms</i> </template>
<template v-else> >10 <i>s</i> </template>
</span>
</template>
<script setup>
import { IconTimeDuration10 } from '@tabler/icons-vue'
defineProps({
value: Number,
active: Boolean,
preset: 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-sans
font-semibold
rounded-sm
text-sm
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 {
@apply cursor-pointer;
&:hover,
&.active {
@apply bg-lime-700 border-lime-500 text-lime-200;
}
}
</style>

View file

@ -0,0 +1,56 @@
<!--
Macrame is a program that enables the user to create keyboard macros and button panels.
The macros are saved as simple JSON files and can be linked to the button panels. The panels can
be created with HTML and CSS.
Copyright (C) 2025 Jesse Malotaux
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<template>
<div id="delete-key-dialog" class="dialog__content">
<h4 class="mb-4 text-slate-50">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())
})
</script>
<style scoped>
@reference "@/assets/main.css";
</style>
'

View file

@ -0,0 +1,77 @@
<!--
Macrame is a program that enables the user to create keyboard macros and button panels.
The macros are saved as simple JSON files and can be linked to the button panels. The panels can
be created with HTML and CSS.
Copyright (C) 2025 Jesse Malotaux
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<template>
<div id="edit-delay-dialog" class="dialog__content">
<h4 class="mb-4 text-slate-50">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
})
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>

View file

@ -0,0 +1,124 @@
<!--
Macrame is a program that enables the user to create keyboard macros and button panels.
The macros are saved as simple JSON files and can be linked to the button panels. The panels can
be created with HTML and CSS.
Copyright (C) 2025 Jesse Malotaux
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<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')"
>
&darr; Down
</ButtonComp>
<ButtonComp
variant="secondary"
:class="editable.newKey.direction === 'up' ? 'selected' : ''"
size="sm"
@click.prevent="handleNewDirection('up')"
>
&uarr; 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>

View file

@ -0,0 +1,90 @@
<!--
Macrame is a program that enables the user to create keyboard macros and button panels.
The macros are saved as simple JSON files and can be linked to the button panels. The panels can
be created with HTML and CSS.
Copyright (C) 2025 Jesse Malotaux
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<template>
<ContextMenu ref="ctxtMenu">
<template #trigger>
<ButtonComp variant="secondary" size="sm"> <IconTimeDuration15 />Fixed delay </ButtonComp>
</template>
<template #content>
<ul>
<li @click="changeDelay(0)">0ms</li>
<li @click="changeDelay(15)">15ms</li>
<li @click="changeDelay(50)">50ms</li>
<li @click="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="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 { IconTimeDuration15 } 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 ctxtMenu = ref()
function changeDelay(num) {
macroRecorder.changeDelay(num)
ctxtMenu.value.toggle()
}
</script>
<style scoped>
@reference "@/assets/main.css";
</style>

View file

@ -0,0 +1,155 @@
<!--
Macrame is a program that enables the user to create keyboard macros and button panels.
The macros are saved as simple JSON files and can be linked to the button panels. The panels can
be created with HTML and CSS.
Copyright (C) 2025 Jesse Malotaux
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<template>
<div id="insert-key-dialog" class="dialog__content w-96">
<h4 class="text-slate-50 mb-4">Insert key {{ position }}</h4>
<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" />
<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'"
>
&darr; Down
</ButtonComp>
<ButtonComp
variant="secondary"
:class="keyObjs.insertDirection === 'up' ? 'selected' : ''"
size="sm"
@click.prevent="keyObjs.insertDirection = 'up'"
>
&uarr; Up
</ButtonComp>
</div>
<div class="flex justify-end">
<ButtonComp variant="primary" size="sm" @click="insertKey()">Insert key</ButtonComp>
</div>
</div>
</template>
<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 { useMacroRecorderStore } from '@/stores/macrorecorder'
import { filterKey } from '@/services/MacroRecordService'
const props = defineProps({
position: String,
})
const macroRecorder = useMacroRecorderStore()
const keyObjs = reactive({
selected: null,
insert: null,
insertEvent: null,
insertDirection: 'down',
adjacent: null,
adjacentDelay: null,
adjacentDelayIndex: null,
})
const insertKeyInput = ref(null)
const inputFocus = ref(false)
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
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>
<style scoped>
@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>

View file

@ -0,0 +1,130 @@
<!--
Macrame is a program that enables the user to create keyboard macros and button panels.
The macros are saved as simple JSON files and can be linked to the button panels. The panels can
be created with HTML and CSS.
Copyright (C) 2025 Jesse Malotaux
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<template>
<kbd :class="`${active ? 'active' : ''} ${empty ? 'empty' : ''}`">
<template v-if="keyObj">
<sup v-if="keyObj.loc">
{{ keyObj.loc }}
</sup>
<span :innerHTML="keyObj.str" />
<span class="dir">{{ dir.value === 'down' ? '&darr;' : '&uarr;' }}</span>
</template>
<template v-else-if="empty">
<span>[ ]</span>
</template>
</kbd>
</template>
<script setup>
import { onMounted, onUpdated, reactive } from 'vue'
const props = defineProps({
keyObj: Object,
direction: String,
active: Boolean,
empty: Boolean,
})
const dir = reactive({
value: false,
})
onMounted(() => {
if (props.empty) return
setDirection()
})
onUpdated(() => {
setDirection()
})
const setDirection = () => {
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-mono
font-bold
text-lg
text-white
whitespace-nowrap
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;
}
&.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 {
@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>

View file

@ -0,0 +1,80 @@
<!--
Macrame is a program that enables the user to create keyboard macros and button panels.
The macros are saved as simple JSON files and can be linked to the button panels. The panels can
be created with HTML and CSS.
Copyright (C) 2025 Jesse Malotaux
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<template>
<div id="validation-error__dialog" class="dialog__content">
<h4 class="mb-4 text-slate-50">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 : []
}
})
})
</script>
<style scoped>
@reference "@/assets/main.css";
</style>

View file

@ -0,0 +1,142 @@
<!--
Macrame is a program that enables the user to create keyboard macros and button panels.
The macros are saved as simple JSON files and can be linked to the button panels. The panels can
be created with HTML and CSS.
Copyright (C) 2025 Jesse Malotaux
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<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 ref="ctxtMenu">
<template #trigger>
<ButtonComp variant="dark" size="sm"> <IconPlus /> Insert </ButtonComp>
</template>
<template #content>
<ul>
<li @click="insert.position = 'before'"><IconArrowLeftCircle /> Before</li>
<li @click="insert.position = 'after'"><IconArrowRightCircle /> After</li>
</ul>
</template>
</ContextMenu>
<DialogComp
v-if="insert.position !== null"
:open="insert.position !== null"
@on-open="onOpenDialog"
@on-close="onCloseDialog"
>
<template #content>
<InsertKeyDialog :position="insert.position" />
</template>
</DialogComp>
<DialogComp
:id="`edit-key-${macroRecorder.state.editKey}`"
@on-open="onOpenDialog"
@on-close="onCloseDialog"
>
<template #trigger>
<ButtonComp variant="secondary" size="sm"> <IconPencil />Edit </ButtonComp>
</template>
<template #content>
<EditKeyDialog />
</template>
</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>
<DialogComp
v-if="
macroRecorder.state.editDelay !== false && typeof macroRecorder.getEditDelay() === 'object'
"
@on-open="onOpenDialog"
@on-close="onCloseDialog"
>
<template #trigger>
<ButtonComp variant="secondary" size="sm"> <IconAlarm />Edit </ButtonComp>
</template>
<template #content>
<EditDelayDialog />
</template>
</DialogComp>
</div>
</template>
<script setup>
import {
IconAlarm,
IconArrowLeftCircle,
IconArrowRightCircle,
IconPencil,
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 { onMounted, reactive, ref } from 'vue'
const macroRecorder = useMacroRecorderStore()
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>
<style scoped>
@reference "@/assets/main.css";
.macro-edit__dialogs {
@apply flex
flex-grow
justify-end;
}
</style>

View file

@ -0,0 +1,111 @@
<!--
Macrame is a program that enables the user to create keyboard macros and button panels.
The macros are saved as simple JSON files and can be linked to the button panels. The panels can
be created with HTML and CSS.
Copyright (C) 2025 Jesse Malotaux
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<template>
<div class="macro-recorder__footer">
<ButtonComp
v-if="macroRecorder.steps.length > 0"
variant="danger"
@click="macroRecorder.reset()"
>
<IconRestore /> Reset
</ButtonComp>
<DialogComp ref="errorDialog">
<template #content>
<ValidationErrorDialog />
</template>
</DialogComp>
<DialogComp ref="overwriteDialog">
<template #content>
<div class="grid gap-2">
<h4 class="pr-4">Are you sure you want to overwrite:</h4>
<h3 class="mb-2 text-center text-sky-500">{{ macroRecorder.macroName }}</h3>
<div class="flex justify-between">
<ButtonComp size="sm" variant="subtle" @click="overwriteDialog.toggleDialog(false)"
>No</ButtonComp
>
<ButtonComp size="sm" variant="primary" @click="saveMacro()">Yes</ButtonComp>
</div>
</div>
</template>
</DialogComp>
<ButtonComp
v-if="macroRecorder.steps.length > 0"
:disabled="macroRecorder.state.record || macroRecorder.state.edit"
variant="success"
@click="startCheck()"
>
<IconDeviceFloppy />
Save
</ButtonComp>
</div>
</template>
<script setup>
import ButtonComp from '@/components/base/ButtonComp.vue'
import { IconDeviceFloppy, IconRestore } from '@tabler/icons-vue'
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 errorDialog = ref()
const overwriteDialog = ref()
onMounted(() => {
macroRecorder.$subscribe((mutation) => {
if (mutation.events && mutation.events.key == 'validationErrors') {
errorDialog.value.toggleDialog(mutation.events.newValue !== false)
}
})
})
const startCheck = async () => {
const checkResp = await macroRecorder.checkMacro()
if (checkResp) overwriteDialog.value.toggleDialog(true)
else saveMacro()
}
const saveMacro = async () => {
overwriteDialog.value.toggleDialog(false)
const saveResp = await macroRecorder.saveMacro()
if (!saveResp) errorDialog.value.toggleDialog(true)
else window.location.reload()
}
</script>
<style scoped>
@reference "@/assets/main.css";
.macro-recorder__footer {
@apply flex
justify-between
gap-2;
}
</style>

View file

@ -0,0 +1,124 @@
<!--
Macrame is a program that enables the user to create keyboard macros and button panels.
The macros are saved as simple JSON files and can be linked to the button panels. The panels can
be created with HTML and CSS.
Copyright (C) 2025 Jesse Malotaux
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<template>
<div class="macro-recorder__header">
<div class="w-full grid grid-cols-[auto_1fr_auto] gap-2">
<h4 class="">Name:</h4>
<input
id="macro-name"
type="text"
@input.prevent="changeName($event.target.value)"
:value="macroName"
placeholder="New macro"
/>
<div :class="`recording__buttons ${!nameSet || macroRecorder.state.edit ? 'disabled' : ''}`">
{{ macroRecorder.name }}
<ButtonComp
v-if="!macroRecorder.state.record"
variant="primary"
@click="macroRecorder.state.record = true"
>
<IconPlayerRecordFilled class="text-red-500" />Record
</ButtonComp>
<ButtonComp
v-if="macroRecorder.state.record"
variant="danger"
@click="macroRecorder.state.record = false"
>
<IconPlayerStopFilled class="text-white" />Stop
</ButtonComp>
</div>
</div>
<div
v-if="macroRecorder.steps.length > 0"
:class="`edit__buttons ${macroRecorder.state.record ? 'disabled' : ''}`"
>
<div>
<ButtonComp
v-if="!macroRecorder.state.edit"
variant="secondary"
@click="macroRecorder.state.edit = true"
>
<IconPencil />Edit
</ButtonComp>
<ButtonComp
v-if="macroRecorder.state.edit"
variant="danger"
@click="macroRecorder.resetEdit()"
>
<IconPlayerStopFilled />Stop
</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'
import { computed, onUpdated, ref } from 'vue'
const macroRecorder = useMacroRecorderStore()
const macroName = computed(() => macroRecorder.macroName)
const nameSet = ref(false)
onUpdated(() => {
nameSet.value = macroName.value && macroName.value.length > 0
})
function changeName(name) {
macroRecorder.changeName(name)
nameSet.value = name.length > 0
}
</script>
<style scoped>
@reference "@/assets/main.css";
.macro-recorder__header {
@apply grid
gap-4
w-full;
.edit__buttons {
@apply flex
justify-between
gap-2
w-full;
}
> div {
@apply flex gap-2 items-end;
}
}
</style>

View file

@ -0,0 +1,66 @@
<!--
Macrame is a program that enables the user to create keyboard macros and button panels.
The macros are saved as simple JSON files and can be linked to the button panels. The panels can
be created with HTML and CSS.
Copyright (C) 2025 Jesse Malotaux
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<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"
@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>

View file

@ -0,0 +1,79 @@
<!--
Macrame is a program that enables the user to create keyboard macros and button panels.
The macros are saved as simple JSON files and can be linked to the button panels. The panels can
be created with HTML and CSS.
Copyright (C) 2025 Jesse Malotaux
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<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;
}
hr.spacer:last-of-type {
@apply hidden;
}
</style>

View file

@ -0,0 +1,253 @@
<!--
Macrame is a program that enables the user to create keyboard macros and button panels.
The macros are saved as simple JSON files and can be linked to the button panels. The panels can
be created with HTML and CSS.
Copyright (C) 2025 Jesse Malotaux
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<template>
<div id="panel-edit" class="mcrm-block block__dark !p-0 !gap-0" v-if="editPanel">
<div class="panel-preview">
<div class="panel-preview__content" ref="panelPreview" v-html="editPanel.html"></div>
</div>
<div class="panel-settings">
<AccordionComp title="Panel info" ref="infoAccordion">
<div class="grid grid-cols-[auto_1fr] gap-2 p-4">
<span>Name:</span><strong class="text-right">{{ editPanel.name }}</strong>
<span>Aspect ratio:</span><strong class="text-right">{{ editPanel.aspectRatio }}</strong>
<template v-if="editPanel.macros">
<span>Linked Macros:</span>
<strong class="text-right">{{ Object.keys(editPanel.macros).length }}</strong>
</template>
</div>
</AccordionComp>
<div>
<AccordionComp
v-if="editButton.id"
title="Button"
ref="buttonAccordion"
:open="editButton.id != ''"
>
<div class="grid gap-4 p-4">
<div class="grid grid-cols-[auto_1fr] gap-2">
<span>Button ID:</span>
<strong class="text-right">{{ editButton.id }}</strong>
</div>
<div class="grid">
<FormSelect
name="button_macro"
label="Button macro"
:search="true"
:options="macroList"
:value="editButton.macro"
@change="checkNewMacro(editButton.id, $event)"
/>
<div class="grid grid-cols-2 mt-4">
<ButtonComp
v-if="editButton.macro != ''"
class="col-start-1 w-fit"
size="sm"
variant="danger"
@click="unlinkMacro(editButton.id)"
ref="unlinkButton"
>
<IconTrash /> Unlink
</ButtonComp>
<ButtonComp
v-if="editButton.changed"
class="col-start-2 w-fit justify-self-end"
size="sm"
variant="primary"
@click="linkMacro(editButton.id)"
ref="linkButton"
>
<IconLink /> Link
</ButtonComp>
</div>
</div>
</div>
</AccordionComp>
</div>
<footer class="flex items-end justify-end h-full p-4">
<ButtonComp v-if="panelMacros.changed" variant="success" @click="savePanelChanges()">
<IconDeviceFloppy /> Save changes
</ButtonComp>
</footer>
</div>
</div>
</template>
<script setup>
import { CheckMacroListChange, GetMacroList } from '@/services/MacroService'
import {
PanelButtonListeners,
PanelDialogListeners,
RemovePanelStyle,
SetPanelStyle,
StripPanelHTML,
} from '@/services/PanelService'
import { usePanelStore } from '@/stores/panel'
import { onMounted, onUnmounted, reactive, ref } from 'vue'
import AccordionComp from '../base/AccordionComp.vue'
import FormSelect from '../form/FormSelect.vue'
import ButtonComp from '../base/ButtonComp.vue'
import { IconDeviceFloppy, IconLink, IconTrash } from '@tabler/icons-vue'
import axios from 'axios'
import { appUrl } from '@/services/ApiService'
const props = defineProps({
dirname: String,
})
const panel = usePanelStore()
const panelPreview = ref(false)
const editPanel = ref({})
const panelMacros = reactive({
old: {},
changed: false,
})
const macroList = ref({})
const infoAccordion = ref(false)
const buttonAccordion = ref(false)
const unlinkButton = ref(null)
const linkButton = ref(null)
const editButton = reactive({
id: '',
macro: '',
newMacro: '',
changed: false,
})
onMounted(async () => {
const currentPanel = await panel.get(props.dirname)
editPanel.value = currentPanel
editPanel.value.dir = props.dirname
editPanel.value.html = StripPanelHTML(editPanel.value.html, editPanel.value.aspectRatio)
panelMacros.old = JSON.stringify(currentPanel.macros)
infoAccordion.value.toggleAccordion(true)
const macros = await GetMacroList()
macroList.value = Object.assign(
{},
...Object.keys(macros).map((key) => ({
[key]: { value: macros[key].macroname, label: macros[key].name },
})),
)
SetPanelStyle(editPanel.value.style)
EditButtonListeners()
})
onUnmounted(() => {
RemovePanelStyle()
})
function EditButtonListeners() {
const callback = (button) => {
infoAccordion.value.toggleAccordion(false)
setEditButton(button.id)
}
PanelButtonListeners(panelPreview.value, callback)
PanelDialogListeners(panelPreview.value)
}
function setEditButton(id) {
editButton.id = id
editButton.macro = editPanel.value.macros[id] ? editPanel.value.macros[id] : ''
}
function checkNewMacro(id, macro) {
editButton.changed = editPanel.value.macros[id] != macro
editButton.newMacro = macro
}
function linkMacro(id) {
editPanel.value.macros[id] = editButton.newMacro
editButton.macro = editButton.newMacro
editButton.newMacro = ''
panelMacros.changed = CheckMacroListChange(panelMacros.old, editPanel.value.macros)
}
function unlinkMacro(id) {
delete editPanel.value.macros[id]
buttonAccordion.value.toggleAccordion(false)
panelMacros.changed = CheckMacroListChange(panelMacros.old, editPanel.value.macros)
}
function savePanelChanges() {
const panelData = {
dir: editPanel.value.dir,
name: editPanel.value.name,
description: editPanel.value.description,
aspectRatio: editPanel.value.aspectRatio,
macros: editPanel.value.macros,
}
axios.post(appUrl() + '/panel/save/json', panelData)
}
</script>
<style>
@reference "@/assets/main.css";
[mcrm__button] {
@apply cursor-pointer;
}
#panel-edit {
@apply grid
grid-cols-[1fr_30ch]
size-full
overflow-hidden;
.panel-preview {
@apply border-r
border-slate-700;
.panel-preview__content {
@apply relative
grid
justify-center
size-full
p-8;
#panel-html__body {
@apply size-full
max-w-full max-h-full;
}
}
}
.panel-settings {
@apply grid
grid-rows-[auto_auto_1fr]
bg-black/30;
}
}
</style>

View file

@ -0,0 +1,103 @@
<!--
Macrame is a program that enables the user to create keyboard macros and button panels.
The macros are saved as simple JSON files and can be linked to the button panels. The panels can
be created with HTML and CSS.
Copyright (C) 2025 Jesse Malotaux
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<template>
<div id="panel-view">
<div class="panel-preview__content" ref="panelView" v-html="viewPanel.html"></div>
</div>
</template>
<script setup>
import { RunMacro } from '@/services/MacroService'
import {
PanelButtonListeners,
PanelDialogListeners,
RemovePanelScripts,
RemovePanelStyle,
SetPanelStyle,
StripPanelHTML,
} from '@/services/PanelService'
import { usePanelStore } from '@/stores/panel'
import { onMounted, onUnmounted, ref } from 'vue'
const panel = usePanelStore()
const props = defineProps({
dirname: String,
})
const panelView = ref(null)
const viewPanel = ref({})
onMounted(async () => {
const currentPanel = await panel.get(props.dirname)
viewPanel.value = currentPanel
viewPanel.value.html = StripPanelHTML(viewPanel.value.html, viewPanel.value.aspectRatio)
SetPanelStyle(viewPanel.value.style)
setTimeout(() => {
viewPanelListeners()
if (typeof window.onPanelLoaded === 'function') {
window.onPanelLoaded()
}
}, 50)
})
onUnmounted(() => {
RemovePanelStyle()
RemovePanelScripts()
})
const viewPanelListeners = () => {
const callback = (button) => {
RunMacro(viewPanel.value.macros[button.id])
}
PanelButtonListeners(panelView.value, callback)
PanelDialogListeners(panelView.value)
}
</script>
<style scoped>
@reference "@/assets/main.css";
#panel-view {
@apply fixed
inset-0
size-full
bg-black;
.panel-preview__content {
@apply relative
grid
justify-center
size-full;
#panel-html__body {
@apply size-full
max-w-full max-h-full;
}
}
}
</style>

View file

@ -0,0 +1,183 @@
<!--
Macrame is a program that enables the user to create keyboard macros and button panels.
The macros are saved as simple JSON files and can be linked to the button panels. The panels can
be created with HTML and CSS.
Copyright (C) 2025 Jesse Malotaux
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<template>
<div id="panels-overview">
<AlertComp v-if="Object.keys(panels.list).length == 0" variant="info">
No panels found
</AlertComp>
<div class="panel-list">
<div class="panel-item mcrm-block block__dark" v-for="(panel, i) in panels.list" :key="i">
<div class="panel-item__content" @click="panelItemClick(panel.dir)">
<div class="thumb">
<img v-if="panel.thumb" :src="`data:image/jpeg;base64,${panel.thumb}`" alt="" />
<IconLayoutGrid v-else />
</div>
<h4>{{ panel.name }}</h4>
<div class="description" v-if="isLocal()">
<div class="content">
<strong class="block mb-1 text-slate-400">{{ panel.name }}</strong>
<hr class="mb-2 border-slate-600" />
<p v-if="panel.description != 'null'" class="text-slate-200">
{{ panel.description }}
</p>
</div>
<footer>
<ButtonComp variant="subtle" size="sm" :href="`/panel/view/${panel.dir}`">
<IconEye /> Preview
</ButtonComp>
<ButtonComp variant="primary" size="sm" :href="`/panel/edit/${panel.dir}`">
<IconPencil /> Edit
</ButtonComp>
</footer>
</div>
</div>
<template v-if="!isLocal()"> </template>
</div>
</div>
</div>
</template>
<script setup>
import { usePanelStore } from '@/stores/panel'
import { onMounted, reactive } from 'vue'
import AlertComp from '../base/AlertComp.vue'
import { IconEye, IconLayoutGrid, IconPencil } from '@tabler/icons-vue'
import ButtonComp from '../base/ButtonComp.vue'
import { isLocal } from '@/services/ApiService'
import { useRouter } from 'vue-router'
const panel = usePanelStore()
const panels = reactive({
list: {},
})
const router = useRouter()
onMounted(async () => {
const panelList = await panel.getList()
panels.list = panelList
})
function panelItemClick(dir) {
if (isLocal()) return
router.push(`/panel/view/${dir}`)
}
</script>
<style scoped>
@reference "@/assets/main.css";
.panel-list {
@apply grid
grid-cols-2
md:grid-cols-4
lg:grid-cols-6
gap-4
w-full h-fit;
}
.panel-item {
@apply p-px
overflow-hidden;
.thumb {
@apply flex
justify-center
items-center
w-full
aspect-[4/3];
img {
@apply size-full
object-cover;
}
&:not(:has(img)) {
@apply bg-sky-950;
}
svg {
@apply size-12;
}
}
h4 {
@apply px-4 py-2
h-12
truncate;
}
&:hover .description {
@apply opacity-100;
}
.description {
@apply absolute
inset-0
size-full
pt-2
pr-1
pb-13
bg-slate-900/60
backdrop-blur-md
text-slate-100
opacity-0
transition-opacity
cursor-default
z-10;
.content {
@apply h-full
p-4
pt-2
overflow-y-auto;
}
footer {
@apply absolute
bottom-0 left-0
w-full
h-12
grid
grid-cols-2
bg-slate-900
border-t
border-slate-600;
.btn {
@apply size-full
rounded-none
justify-center
border-0;
&:last-child {
@apply border-l
border-slate-600;
}
}
}
}
}
</style>

37
ui/src/main.js Normal file
View file

@ -0,0 +1,37 @@
/*
Macrame is a program that enables the user to create keyboard macros and button panels.
The macros are saved as simple JSON files and can be linked to the button panels. The panels can
be created with HTML and CSS.
Copyright (C) 2025 Jesse Malotaux
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
// import './assets/jemx.scss'
import '@/assets/main.css'
import '@/assets/img/Macrame-Logo-gradient.svg'
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from '@/App.vue'
import router from '@/router'
const app = createApp(App)
app.use(createPinia())
app.use(router)
app.mount('#app')

87
ui/src/router/index.js Normal file
View file

@ -0,0 +1,87 @@
/*
Macrame is a program that enables the user to create keyboard macros and button panels.
The macros are saved as simple JSON files and can be linked to the button panels. The panels can
be created with HTML and CSS.
Copyright (C) 2025 Jesse Malotaux
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { createRouter, createWebHistory } from 'vue-router'
import DashboardView from '../views/DashboardView.vue'
import { checkAuth, isLocal } from '@/services/ApiService'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{
path: '/',
name: 'dashboard',
component: DashboardView,
},
{
path: '/panels',
name: 'panels',
component: () => import('../views/PanelsView.vue'),
meta: { requiresAuth: true },
},
{
path: '/panel/edit/:dirname',
name: 'panel-edit',
component: () => import('../views/PanelsView.vue'),
meta: { requiresAuth: true },
},
{
path: '/panel/view/:dirname',
name: 'panel-view',
component: () => import('../views/PanelsView.vue'),
meta: { requiresAuth: true },
},
{
path: '/macros',
name: 'macros',
component: () => import('../views/MacrosView.vue'),
meta: { localOnly: true },
},
{
path: '/devices',
name: 'devices',
component: () => import('../views/DevicesView.vue'),
},
// {
// path: '/settings',
// name: 'settings',
// component: () => import('../views/SettingsView.vue'),
// },
// {
// path: '/about',
// name: 'about',
// // route level code-splitting
// // this generates a separate chunk (About.[hash].js) for this route
// // which is lazy-loaded when the route is visited.
// component: () => import('../views/AboutView.vue'),
// },
],
})
router.beforeEach(async (to, from, next) => {
const auth = await checkAuth()
if (to.meta.requiresAuth && !auth && !isLocal()) next('/devices')
else if (to.meta.localOnly && !isLocal()) next('/')
else next()
})
export default router

View file

@ -0,0 +1,55 @@
/*
Macrame is a program that enables the user to create keyboard macros and button panels.
The macros are saved as simple JSON files and can be linked to the button panels. The panels can
be created with HTML and CSS.
Copyright (C) 2025 Jesse Malotaux
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { useDeviceStore } from '@/stores/device'
import CryptoJS from 'crypto-js'
export const appUrl = () => {
const port = window.location.port == 5173 ? window.__CONFIG__.MCRM__PORT : window.location.port
return `http://${window.location.hostname}:${port}`
}
export const isLocal = () => {
return window.location.hostname === '127.0.0.1' || window.location.hostname === 'localhost'
}
export const encrypt = (data, key = false) => {
const pk = !key ? localStorage.getItem('Macrame__pk') : key
if (pk) {
return CryptoJS.RSA.encrypt(JSON.stringify(data), pk).toString()
} else {
return false
}
}
export const checkAuth = async () => {
const device = useDeviceStore()
const handshake = await device.remoteHandshake()
if (handshake === true) return true
if (device.key()) return true
return false
}

View file

@ -0,0 +1,77 @@
/*
Macrame is a program that enables the user to create keyboard macros and button panels.
The macros are saved as simple JSON files and can be linked to the button panels. The panels can
be created with HTML and CSS.
Copyright (C) 2025 Jesse Malotaux
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { useDeviceStore } from '@/stores/device'
import { AES, enc, pad } from 'crypto-js'
import { isLocal } from './ApiService'
export const encryptAES = (key, str) => {
key = keyPad(key)
let iv = enc.Utf8.parse(window.__CONFIG__.MCRM__IV)
let encrypted = AES.encrypt(str, key, {
iv: iv,
padding: pad.Pkcs7,
})
return encrypted.toString()
}
export const decryptAES = (key, str) => {
key = keyPad(key)
let iv = enc.Utf8.parse(window.__CONFIG__.MCRM__IV)
let encrypted = AES.decrypt(str.toString(), key, {
iv: iv,
padding: pad.Pkcs7,
})
return encrypted.toString(enc.Utf8)
}
export const AuthCall = (data = false) => {
if (isLocal()) return data
if (!data) data = {empty: true}
const device = useDeviceStore()
return {
uuid: device.uuid(),
d: encryptAES(device.key(), JSON.stringify(data)),
}
}
function keyPad(key) {
let returnKey = key
if (key.length == 4) {
returnKey = key + window.__CONFIG__.MCRM__SALT
}
return enc.Utf8.parse(returnKey)
}
export const getDateStr = () => {
const date = new Date()
const year = date.getFullYear()
const month = String(date.getMonth() + 1).padStart(2, '0')
const day = String(date.getDate()).padStart(2, '0')
return `${year}${month}${day}`
}

View file

@ -0,0 +1,192 @@
/*
Macrame is a program that enables the user to create keyboard macros and button panels.
The macros are saved as simple JSON files and can be linked to the button panels. The panels can
be created with HTML and CSS.
Copyright (C) 2025 Jesse Malotaux
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
const keyMap = {
// Modifier keys
Control: 'Ctrl',
Shift: 'Shift',
Alt: 'Alt',
Meta: 'Win',
CapsLock: 'Caps',
// Special keys
PageUp: 'PgUp',
PageDown: 'PgDn',
ScrollLock: 'Scr Lk',
Insert: 'Ins',
Delete: 'Del',
Escape: 'Esc',
Space: 'Space',
// Symbol keys
Backquote: '`',
Backslash: '\\',
BracketLeft: '[',
BracketRight: ']',
Comma: ',',
Equal: '=',
Minus: '-',
Period: '.',
Quote: "'",
Semicolon: ';',
Slash: '/',
// Arrow keys
ArrowUp: '&#9650;',
ArrowRight: '&#9654;',
ArrowDown: '&#9660;',
ArrowLeft: '&#9664;',
// Media keys
MediaPlayPause: 'Play',
MediaStop: 'Stop',
MediaTrackNext: 'Next',
MediaTrackPrevious: 'Prev',
MediaVolumeDown: 'Down',
MediaVolumeUp: 'Up',
AudioVolumeMute: 'Mute',
AudioVolumeDown: 'Down',
AudioVolumeUp: 'Up',
}
/**
* Filters a keyboard event and returns an object with two properties:
* loc (optional) and str.
* loc is the location of the key (either 'left', 'right', or 'num').
* str is the string representation of the key (e.g. 'a', 'A', 'Enter', etc.).
* If the key is a modifier key, it is represented by its name (e.g. 'Ctrl', 'Shift', etc.).
* If the key is not a modifier key, it is represented by its character (e.g. 'a', 'A', etc.).
* If the key is not a character key, it is represented by its symbol (e.g. ',', '.', etc.).
* @param {KeyboardEvent} e - The keyboard event to filter.
* @return {Object} An object with two properties: loc (optional) and str.
*/
export const filterKey = (e) => {
const k = {} // Object k (key)
// If location is set, set loc (location)
if (e.location === 1) k.loc = 'left'
if (e.location === 2) k.loc = 'right'
if (e.location === 3) k.loc = 'num'
if (e.key.includes('Media') || e.key.includes('Audio')) k.loc = mediaPrefix(e)
// If code is in keyMap, set str by code
if (keyMap[e.code] || keyMap[e.key]) {
k.str = keyMap[e.code] || keyMap[e.key]
} else {
// If code is not in keyMap, set str by e.key
k.str = e.key.toLowerCase()
}
// return k object
return k
}
/**
* Returns a string prefix for the given media key.
* @param {KeyboardEvent} e - The keyboard event to get the prefix for.
* @return {string} The prefix for the key (either 'Media' or 'Volume').
*/
const mediaPrefix = (e) => {
switch (e.key) {
case 'MediaPlayPause':
case 'MediaStop':
case 'MediaTrackNext':
case 'MediaTrackPrevious':
return 'Media'
case 'MediaVolumeDown':
case 'MediaVolumeUp':
case 'AudioVolumeDown':
case 'AudioVolumeUp':
case 'AudioVolumeMute':
return 'Volume'
}
}
export const isRepeat = (lastStep, e, direction) => {
return (
lastStep &&
lastStep.type === 'key' &&
lastStep.code === e.code &&
lastStep.direction === direction
)
}
export const invalidMacro = (steps) => {
const downKeys = []
const upKeys = []
Object.keys(steps).forEach((stepKey) => {
const step = steps[stepKey]
if (step.type !== 'key') return
if (step.direction == 'down') downKeys.push(step.key)
if (step.direction == 'up') {
if (!downKeys.includes(step.key)) upKeys.push(step.key)
else downKeys.splice(downKeys.indexOf(step.key), 1)
}
})
if (upKeys.length === 0 && downKeys.length === 0) return false
return { down: downKeys, up: upKeys }
}
export const translateJSON = (json) => {
const steps = []
json.forEach((step) => {
if (step.type === 'delay') steps.push(step)
if (step.type === 'key') steps.push(codeToStep(step.code, step.direction))
})
return steps
}
export const codeToStep = (code, direction) => {
let key = ''
let location = 0
let codeStr = code
if (code.includes('Left')) {
key = code.replace('Left', '')
location = 1
}
if (code.includes('Right')) {
key = code.replace('Right', '')
location = 2
}
if (code.includes('Numpad')) {
key = code.replace('Numpad', '')
location = 3
}
if (code.includes('Media')) codeStr = ''
if (key === '') key = code
const stepObj = {
type: 'key',
code: codeStr,
key: key,
location: location,
direction: direction,
}
return { ...stepObj, keyObj: filterKey(stepObj) }
}

View file

@ -0,0 +1,47 @@
/*
Macrame is a program that enables the user to create keyboard macros and button panels.
The macros are saved as simple JSON files and can be linked to the button panels. The panels can
be created with HTML and CSS.
Copyright (C) 2025 Jesse Malotaux
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import axios from 'axios'
import { appUrl, isLocal } from './ApiService'
import { AuthCall } from './EncryptService'
export const GetMacroList = async (count = false) => {
const request = await axios.post(appUrl() + '/macro/list')
if (!request.data) return 0
if (!count) return sortMacroList(request.data)
else return request.data.length
}
const sortMacroList = (list) => {
return [...list].sort((a, b) => a.name.localeCompare(b.name))
}
export const RunMacro = async (macro) => {
const data = isLocal() ? { macro: macro } : AuthCall({ macro: macro })
const request = await axios.post(appUrl() + '/macro/play', data)
return request.data
}
export const CheckMacroListChange = (oldList, newList) => {
return oldList !== JSON.stringify(newList)
}

View file

@ -0,0 +1,120 @@
/*
Macrame is a program that enables the user to create keyboard macros and button panels.
The macros are saved as simple JSON files and can be linked to the button panels. The panels can
be created with HTML and CSS.
Copyright (C) 2025 Jesse Malotaux
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
export const SetPanelStyle = (styleStr) => {
const styleEl = document.createElement('style')
styleEl.setAttribute('custom_panel_style', true)
styleEl.innerHTML = styleStr
document.head.appendChild(styleEl)
}
export const RemovePanelStyle = () => {
const styleEl = document.querySelector('style[custom_panel_style]')
if (styleEl) {
styleEl.remove()
}
}
export const StripPanelHTML = (html, aspectRatio) => {
const parser = new DOMParser()
const doc = parser.parseFromString(html, 'text/html')
let scripts = []
if (doc.querySelectorAll('script').length > 0) {
const stripped = StripPanelScripts(doc)
doc.body = stripped.body
scripts = stripped.scripts
}
const body = doc.body
const bodyContents = body.innerHTML
const panelBody = document.createElement('div')
panelBody.id = 'panel-html__body'
panelBody.style = `aspect-ratio: ${aspectRatio}`
panelBody.innerHTML = bodyContents
if (scripts.length > 0) {
SetPanelScripts(scripts)
}
return panelBody.outerHTML
}
export const StripPanelScripts = (doc) => {
const scriptEls = doc.querySelectorAll('script')
const scripts = []
scriptEls.forEach((script) => {
if (script.getAttribute('no-compile') != '') scripts.push(script.innerHTML)
script.remove()
})
return { body: doc.body, scripts }
}
export const SetPanelScripts = (scripts) => {
scripts.forEach((script) => {
const scriptEl = document.createElement('script')
scriptEl.setAttribute('custom_panel_script', true)
scriptEl.innerHTML = script
document.body.appendChild(scriptEl)
})
}
export const RemovePanelScripts = () => {
const scripts = document.querySelectorAll('script[custom_panel_script]')
scripts.forEach((script) => {
script.remove()
})
}
export const PanelButtonListeners = (panelEl, callback) => {
panelEl.querySelectorAll('[mcrm__button]').forEach((button) => {
button.addEventListener('click', () => {
callback(button)
})
})
}
export const PanelDialogListeners = (panelEl) => {
panelEl.querySelectorAll('[mcrm__dialog-trigger]').forEach((dialogTrigger) => {
const dialogEl = document.querySelector(dialogTrigger.getAttribute('dialog-trigger'))
if (dialogEl) {
dialogTrigger.addEventListener('click', () => {
dialogEl.show()
})
}
})
document.querySelectorAll('dialog, dialog .dialog__close').forEach((dialogClose) => {
dialogClose.addEventListener('click', (e) => {
if (
e.target.classList.contains('dialog__close') ||
e.target.closest('.dialog__close') ||
e.target.tagName == 'DIALOG'
) {
dialogClose.closest('dialog').close()
}
})
})
}

View file

@ -0,0 +1,117 @@
"A-Z a-z 0-9"
"backspace"
"delete"
"enter"
"tab"
"esc"
"escape"
"up" Up arrow key
"down" Down arrow key
"right" Right arrow key
"left" Left arrow key
"home"
"end"
"pageup"
"pagedown"
"f1"
"f2"
"f3"
"f4"
"f5"
"f6"
"f7"
"f8"
"f9"
"f10"
"f11"
"f12"
"f13"
"f14"
"f15"
"f16"
"f17"
"f18"
"f19"
"f20"
"f21"
"f22"
"f23"
"f24"
"cmd" is the "win" key for windows
"lcmd" left command
"rcmd" right command
// "command"
"alt"
"lalt" left alt
"ralt" right alt
"ctrl"
"lctrl" left ctrl
"rctrl" right ctrl
"control"
"shift"
"lshift" left shift
"rshift" right shift
// "right_shift"
"capslock"
"space"
"print"
"printscreen" // No Mac support
"insert"
"menu" Windows only
"audio_mute" Mute the volume
"audio_vol_down" Lower the volume
"audio_vol_up" Increase the volume
"audio_play"
"audio_stop"
"audio_pause"
"audio_prev" Previous Track
"audio_next" Next Track
"audio_rewind" Linux only
"audio_forward" Linux only
"audio_repeat" Linux only
"audio_random" Linux only
"num0"
"num1"
"num2"
"num3"
"num4"
"num5"
"num6"
"num7"
"num8"
"num9"
"num_lock"
"num."
"num+"
"num-"
"num*"
"num/"
"num_clear"
"num_enter"
"num_equal"
// // "numpad_0" No Linux support
// "numpad_0"
// "numpad_1"
// "numpad_2"
// "numpad_3"
// "numpad_4"
// "numpad_5"
// "numpad_6"
// "numpad_7"
// "numpad_8"
// "numpad_9"
// "numpad_lock"
"lights_mon_up" Turn up monitor brightness No Windows support
"lights_mon_down" Turn down monitor brightness No Windows support
"lights_kbd_toggle" Toggle keyboard backlight on/off No Windows support
"lights_kbd_up" Turn up keyboard backlight brightness No Windows support
"lights_kbd_down" Turn down keyboard backlight brightness No Windows support

33
ui/src/stores/counter.js Normal file
View file

@ -0,0 +1,33 @@
/*
Macrame is a program that enables the user to create keyboard macros and button panels.
The macros are saved as simple JSON files and can be linked to the button panels. The panels can
be created with HTML and CSS.
Copyright (C) 2025 Jesse Malotaux
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { ref, computed } from 'vue'
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', () => {
const count = ref(0)
const doubleCount = computed(() => count.value * 2)
function increment() {
count.value++
}
return { count, doubleCount, increment }
})

153
ui/src/stores/device.js Normal file
View file

@ -0,0 +1,153 @@
/*
Macrame is a program that enables the user to create keyboard macros and button panels.
The macros are saved as simple JSON files and can be linked to the button panels. The panels can
be created with HTML and CSS.
Copyright (C) 2025 Jesse Malotaux
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { ref } from 'vue'
import { defineStore } from 'pinia'
import axios from 'axios'
import { appUrl, encrypt } from '@/services/ApiService'
import { v4 as uuidv4 } from 'uuid'
import { encryptAES, getDateStr } from '@/services/EncryptService'
export const useDeviceStore = defineStore('device', () => {
// Properties - State values
const current = ref({
uuid: false,
key: false,
})
const remote = ref([])
const server = ref({
status: false,
})
// Current device
const uuid = () => {
if (!current.value.uuid && localStorage.getItem('deviceId')) {
current.value.uuid = localStorage.getItem('deviceId')
} else if (!current.value.uuid) {
current.value.uuid = setDeviceId()
}
return current.value.uuid
}
const setDeviceId = () => {
const uuid = uuidv4()
localStorage.setItem('deviceId', uuid)
return uuid
}
const key = () => {
if (!current.value.key && localStorage.getItem('deviceKey')) {
current.value.key = localStorage.getItem('deviceKey')
}
return current.value.key
}
const setDeviceKey = (key) => {
current.value.key = key
localStorage.setItem('deviceKey', key)
}
const removeDeviceKey = () => {
current.value.key = false
localStorage.removeItem('deviceKey')
}
const serverGetIP = async () => {
const request = await axios.post(appUrl() + '/device/server/ip')
return `http://${request.data}:${window.__CONFIG__.MCRM__PORT}`
}
// Server application
const serverGetRemotes = async (count = false) => {
const request = await axios.post(appUrl() + '/device/list')
if (!request.data.devices) return false
remote.value = request.data.devices
if (!count) return remote.value
else return Object.keys(remote.value).length
}
const serverStartLink = async (deviceUuid) => {
const request = await axios.post(appUrl() + '/device/link/start', { uuid: deviceUuid })
return request.data
}
// Remote application
const remoteCheckServerAccess = async () => {
const check = await axios.post(appUrl() + '/device/access/check', { uuid: uuid() })
server.value.access = check.data
return check.data
}
const remoteRequestServerAccess = async (deviceName, deviceType) => {
const request = await axios.post(appUrl() + '/device/access/request', {
uuid: uuid(),
name: deviceName,
type: deviceType,
})
return request
}
const remotePingLink = async (cb) => {
const pingInterval = setInterval(() => {
axios.post(appUrl() + '/device/link/ping', { uuid: uuid() }).then((data) => {
if (data.data) {
clearInterval(pingInterval)
cb(data.data)
}
})
}, 1000)
}
const remoteHandshake = async (keyStr = false) => {
if (!keyStr) keyStr = key()
if (!keyStr) return false
const handshake = await axios.post(appUrl() + '/device/handshake', {
uuid: uuid(),
shake: encryptAES(keyStr, getDateStr()),
})
if (!handshake.data) removeDeviceKey()
return handshake.data
}
return {
remote,
server,
uuid,
setDeviceId,
key,
setDeviceKey,
removeDeviceKey,
serverGetIP,
serverGetRemotes,
serverStartLink,
remoteCheckServerAccess,
remoteRequestServerAccess,
remotePingLink,
remoteHandshake,
}
})

View file

@ -0,0 +1,260 @@
/*
Macrame is a program that enables the user to create keyboard macros and button panels.
The macros are saved as simple JSON files and can be linked to the button panels. The panels can
be created with HTML and CSS.
Copyright (C) 2025 Jesse Malotaux
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { ref } from 'vue'
import { defineStore } from 'pinia'
import { filterKey, isRepeat, invalidMacro, translateJSON } from '../services/MacroRecordService'
import axios from 'axios'
import { appUrl } from '@/services/ApiService'
export const useMacroRecorderStore = defineStore('macrorecorder', () => {
// Properties - State values
const state = ref({
record: false,
edit: false,
editKey: false,
editDelay: false,
validationErrors: false,
})
const macroName = ref('')
const steps = ref([])
const delay = ref({
start: 0,
fixed: false,
})
// Getters - Computed values
const getEditKey = () => steps.value[state.value.editKey]
const getAdjacentKey = (pos, includeDelay = false) => {
let returnVal = false
const mod = pos == 'before' ? -1 : 1
const keyIndex = state.value.editKey + 2 * mod
const delayIndex = includeDelay ? state.value.editKey + 1 * mod : false
if (steps.value[keyIndex]) returnVal = steps.value[keyIndex]
if (delayIndex && steps.value[delayIndex])
returnVal = {
delay: steps.value[delayIndex],
key: steps.value[keyIndex],
delayIndex: delayIndex,
}
return returnVal
}
const getEditDelay = () => steps.value[state.value.editDelay]
// Setters - Actions
const recordStep = (e, direction = false, key = false) => {
if ((e.ctrlKey, e.shiftKey, e.altKey, e.metaKey)) e.preventDefault()
const lastStep = steps.value[steps.value.length - 1]
let stepVal = {}
if (typeof e === 'object' && !isRepeat(lastStep, e, direction)) {
if (key === false) recordDelay()
stepVal = {
type: 'key',
key: e.key,
code: e.code,
location: e.location,
direction: direction,
keyObj: filterKey(e),
}
} else if (direction && key !== false) {
stepVal = steps.value[key]
stepVal.direction = direction
} else if (typeof e === 'number') {
stepVal = { type: 'delay', value: parseFloat(e.toFixed()) }
}
if (key !== false) steps.value[key] = stepVal
else steps.value.push(stepVal)
}
const recordDelay = () => {
if (delay.value.fixed !== false)
recordStep(delay.value.fixed) // Record fixed delay
else if (delay.value.start == 0)
delay.value.start = performance.now() // Record start of delay
else {
recordStep(performance.now() - delay.value.start) // Record end of delay
delay.value.start = performance.now() // Reset start
}
}
const insertKey = (e, direction, adjacentDelayIndex) => {
let min, max, newKeyIndex, newDelayIndex
if (adjacentDelayIndex === null) {
min = state.value.editKey == 0 ? 0 : state.value.editKey
max = state.value.editKey == 0 ? 1 : false
newKeyIndex = max === false ? min + 2 : min
newDelayIndex = min + 1
} else if (state.value.editKey < adjacentDelayIndex) {
min = state.value.editKey
max = adjacentDelayIndex
newKeyIndex = min + 2
newDelayIndex = min + 1
} else {
min = adjacentDelayIndex
max = state.value.editKey
newKeyIndex = min + 1
newDelayIndex = min + 2
}
if (max !== false) {
for (let i = Object.keys(steps.value).length - 1; i >= max; i--) {
steps.value[i + 2] = steps.value[i]
}
}
recordStep(e, direction, newKeyIndex)
recordStep(10, false, newDelayIndex)
state.value.editKey = false
}
const deleteEditKey = () => {
if (state.value.editKey === 0) steps.value.splice(state.value.editKey, 2)
else steps.value.splice(state.value.editKey - 1, 2)
state.value.editKey = false
}
const restartDelay = () => {
delay.value.start = performance.now()
}
const changeName = (name) => {
macroName.value = name
}
const changeDelay = (fixed) => {
delay.value.fixed = fixed
formatDelays()
}
const formatDelays = () => {
steps.value = steps.value.map((step) => {
if (step.type === 'delay' && delay.value.fixed !== false) step.value = delay.value.fixed
return step
})
}
const toggleEdit = (type, key) => {
if (type === 'key') {
state.value.editKey = key
state.value.editDelay = false
}
if (type === 'delay') {
state.value.editKey = false
state.value.editDelay = key
}
}
const resetEdit = () => {
state.value.edit = false
state.value.editKey = false
state.value.editDelay = false
}
const resetMacro = () => {
state.value.record = false
delay.value.start = 0
macroName.value = ''
steps.value = []
if (state.value.edit) resetEdit()
}
const checkMacro = async () => {
const resp = await axios.post(appUrl() + '/macro/check', {
macro: macroName.value,
})
return resp.data
}
const saveMacro = async () => {
state.value.validationErrors = invalidMacro(steps.value)
if (state.value.validationErrors) return false
const resp = await axios.post(appUrl() + '/macro/record', {
name: macroName.value,
steps: steps.value,
})
return resp.status == 200
}
const deleteMacro = async (macroFilename) => {
const resp = await axios.post(appUrl() + '/macro/delete', {
macro: macroFilename,
})
if (resp.status == 200) return resp.data
else return false
}
const openMacro = async (macroFileName, name) => {
const openResp = await axios.post(appUrl() + '/macro/open', {
macro: macroFileName,
})
if (openResp.data) steps.value = translateJSON(openResp.data)
macroName.value = name
}
return {
state,
macroName,
steps,
delay,
getEditKey,
getAdjacentKey,
getEditDelay,
recordStep,
insertKey,
deleteEditKey,
restartDelay,
changeName,
changeDelay,
toggleEdit,
resetEdit,
resetMacro,
checkMacro,
saveMacro,
deleteMacro,
openMacro,
}
})

82
ui/src/stores/panel.js Normal file
View file

@ -0,0 +1,82 @@
/*
Macrame is a program that enables the user to create keyboard macros and button panels.
The macros are saved as simple JSON files and can be linked to the button panels. The panels can
be created with HTML and CSS.
Copyright (C) 2025 Jesse Malotaux
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { appUrl } from '@/services/ApiService'
import { AuthCall } from '@/services/EncryptService'
import axios from 'axios'
import { defineStore } from 'pinia'
import { ref } from 'vue'
export const usePanelStore = defineStore('panel', () => {
const current = ref({
dir: false,
name: false,
description: false,
aspectRatio: false,
macros: false,
thumb: false,
html: false,
style: false,
})
const list = ref([])
const get = async (dir) => {
const data = AuthCall({ dir: dir })
const resp = await axios.post(appUrl() + '/panel/get', data)
if (!resp.data && !current.value.html) return false
current.value.name = resp.data.name
current.value.description = resp.data.description
current.value.aspectRatio = resp.data.aspectRatio
current.value.macros = resp.data.macros
current.value.thumb = resp.data.thumb
current.value.html = resp.data.html
current.value.style = resp.data.style
return current.value
}
const getList = async (count = false) => {
if (list.value.length > 0 && !count) return list.value
else if (list.value.length > 0 && count) return list.value.length
const data = AuthCall()
const resp = await axios.post(appUrl() + '/panel/list', data)
list.value = resp.data
if (!resp.data && !count) return false
else if (!resp.data && count) return 0
if (!count) return list.value
else return list.value.length
}
return {
current,
list,
get,
getList,
}
})

View file

@ -0,0 +1,105 @@
<!--
Macrame is a program that enables the user to create keyboard macros and button panels.
The macros are saved as simple JSON files and can be linked to the button panels. The panels can
be created with HTML and CSS.
Copyright (C) 2025 Jesse Malotaux
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<template>
<div id="dashboard" class="panel">
<div class="panel__title">
<h1>Dashboard</h1>
<div>
<em v-if="isLocal()">This is the server dashboard.</em>
<em v-else>This is the remote dashboard.</em>
</div>
</div>
<div class="panel__content !h-fit !gap-y-16">
<ServerView v-if="isLocal()" />
<RemoteView v-else />
<div class="grid gap-2 text-slate-300">
<h3>About Macrame</h3>
<p>
Macrame is an open-source application designed to turn any device into a customizable
button panel. Whether you're optimizing your workflow or enhancing your gaming experience,
Macrame makes it simple to create and link macros to your button panels.
</p>
<p>
For more information, including details on licensing, visit
<a href="https://macrame.github.io" target="_blank">https://macrame.github.io</a>
</p>
</div>
</div>
</div>
</template>
<script setup>
import { isLocal } from '@/services/ApiService'
import ServerView from '@/components/dashboard/ServerView.vue'
import RemoteView from '@/components/dashboard/RemoteView.vue'
</script>
<style>
@reference "@/assets/main.css";
.dashboard-block {
@apply md:!row-start-1
grid
justify-items-center
gap-4;
&#devices .icon__container,
&#server .icon__container {
@apply bg-sky-300/30
text-sky-400
border-sky-300/60;
}
&#macros .icon__container {
@apply bg-amber-300/30
text-amber-400
border-amber-300/60;
}
&#panels .icon__container {
@apply bg-rose-300/30
text-rose-400
border-rose-300/60;
}
.icon__container {
@apply flex
justify-center
items-center
size-16
aspect-square
rounded-full
border;
svg {
@apply size-8;
}
}
p {
@apply opacity-50
w-42
text-center;
}
}
</style>

View file

@ -0,0 +1,42 @@
<!--
Macrame is a program that enables the user to create keyboard macros and button panels.
The macros are saved as simple JSON files and can be linked to the button panels. The panels can
be created with HTML and CSS.
Copyright (C) 2025 Jesse Malotaux
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<template>
<div id="devices-view" class="panel">
<h1 class="panel__title">
{{ isLocal() ? 'Remote devices' : 'Server' }}
</h1>
<div class="grid gap-8 pr-2 panel__content">
<ServerView v-if="isLocal()" />
<RemoteView v-else />
</div>
</div>
</template>
<script setup>
import ServerView from '@/components/devices/ServerView.vue'
import RemoteView from '@/components/devices/RemoteView.vue'
import { isLocal } from '@/services/ApiService'
</script>
<style scoped>
@reference "@/assets/main.css";
</style>

View file

@ -0,0 +1,7 @@
<template>
<div id="dashboard"></div>
</template>
<script setup></script>
<style lang="scss" scoped></style>

View file

@ -0,0 +1,48 @@
<!--
Macrame is a program that enables the user to create keyboard macros and button panels.
The macros are saved as simple JSON files and can be linked to the button panels. The panels can
be created with HTML and CSS.
Copyright (C) 2025 Jesse Malotaux
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<template>
<div id="macros" class="panel">
<h1 class="panel__title">Macros</h1>
<div class="panel__content !p-0 !overflow-hidden">
<div class="macro-panel__content">
<MacroOverview />
<MacroRecorder />
</div>
</div>
</div>
</template>
<script setup>
import MacroOverview from '@/components/macros/MacroOverview.vue'
import MacroRecorder from '../components/macros/MacroRecorder.vue'
</script>
<style scoped>
@reference "@/assets/main.css";
.macro-panel__content {
@apply grid
grid-cols-[25ch_1fr]
gap-6
pt-2;
}
</style>

View file

@ -0,0 +1,82 @@
<!--
Macrame is a program that enables the user to create keyboard macros and button panels.
The macros are saved as simple JSON files and can be linked to the button panels. The panels can
be created with HTML and CSS.
Copyright (C) 2025 Jesse Malotaux
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<template>
<div id="panels" class="panel">
<h1 class="flex items-end justify-between !w-full panel__title">
<div>Panels</div>
<ButtonComp
v-if="panel.function != 'overview'"
variant="subtle"
size="sm"
@click="router.push('/panels')"
>
<IconArrowLeft /> Overview
</ButtonComp>
</h1>
<div :class="`panel__content !p-0 !pt-4 ${panel.function == 'overview' ?? '!pr-4'}`">
<PanelsOverview v-if="panel.function == 'overview'" />
<PanelEdit v-if="panel.function == 'edit'" :dirname="panel.dirname" />
<PanelView v-if="panel.function == 'preview'" :dirname="panel.dirname" />
</div>
</div>
</template>
<script setup>
import ButtonComp from '@/components/base/ButtonComp.vue'
import PanelEdit from '@/components/panels/PanelEdit.vue'
import PanelView from '@/components/panels/PanelView.vue'
import PanelsOverview from '@/components/panels/PanelsOverview.vue'
import { isLocal } from '@/services/ApiService'
import { IconArrowLeft } from '@tabler/icons-vue'
import { onMounted, onUpdated, reactive } from 'vue'
import { useRoute, useRouter } from 'vue-router'
const route = useRoute()
const router = useRouter()
const panel = reactive({
function: '',
dirname: '',
})
onMounted(() => {
setVarsByRoute()
})
onUpdated(() => {
setVarsByRoute()
})
const setVarsByRoute = () => {
if (route.name.includes('panel-')) {
panel.function = route.name == 'panel-edit' ? 'edit' : 'preview'
} else {
panel.function = 'overview'
}
panel.dirname = route.params.dirname
}
</script>
<style scoped>
@reference "@/assets/main.css";
</style>

View file

@ -0,0 +1,28 @@
<!--
Macrame is a program that enables the user to create keyboard macros and button panels.
The macros are saved as simple JSON files and can be linked to the button panels. The panels can
be created with HTML and CSS.
Copyright (C) 2025 Jesse Malotaux
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<template>
<div></div>
</template>
<script setup></script>
<style lang="scss" scoped></style>