mirror of
https://github.com/Macrame-App/Macrame
synced 2025-12-29 15:29:26 +00:00
Refactor: Renamed fe->ui
This commit is contained in:
parent
67bbbd6baf
commit
b85962a539
83 changed files with 0 additions and 4 deletions
98
ui/src/App.vue
Normal file
98
ui/src/App.vue
Normal 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>
|
||||
34
ui/src/assets/img/Macrame-Logo-duo.svg
Normal file
34
ui/src/assets/img/Macrame-Logo-duo.svg
Normal 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 |
60
ui/src/assets/img/Macrame-Logo-gradient.svg
Normal file
60
ui/src/assets/img/Macrame-Logo-gradient.svg
Normal 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 |
23
ui/src/assets/img/Macrame-Logo-white.svg
Normal file
23
ui/src/assets/img/Macrame-Logo-white.svg
Normal 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 |
41
ui/src/assets/img/bg-gradient.svg
Normal file
41
ui/src/assets/img/bg-gradient.svg
Normal 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
38
ui/src/assets/main.css
Normal 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;
|
||||
}
|
||||
68
ui/src/assets/style/_content.css
Normal file
68
ui/src/assets/style/_content.css
Normal 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;
|
||||
}
|
||||
49
ui/src/assets/style/_form.css
Normal file
49
ui/src/assets/style/_form.css
Normal 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;
|
||||
}
|
||||
}
|
||||
49
ui/src/assets/style/_macro.css
Normal file
49
ui/src/assets/style/_macro.css
Normal 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;
|
||||
}
|
||||
}
|
||||
123
ui/src/assets/style/_mcrm-block.css
Normal file
123
ui/src/assets/style/_mcrm-block.css
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
56
ui/src/assets/style/_panel.css
Normal file
56
ui/src/assets/style/_panel.css
Normal 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;
|
||||
}
|
||||
}
|
||||
44
ui/src/assets/style/_scrollbar.css
Normal file
44
ui/src/assets/style/_scrollbar.css
Normal 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;
|
||||
}
|
||||
119
ui/src/components/base/AccordionComp.vue
Normal file
119
ui/src/components/base/AccordionComp.vue
Normal 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>
|
||||
100
ui/src/components/base/AlertComp.vue
Normal file
100
ui/src/components/base/AlertComp.vue
Normal 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>
|
||||
189
ui/src/components/base/ButtonComp.vue
Normal file
189
ui/src/components/base/ButtonComp.vue
Normal 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>
|
||||
36
ui/src/components/base/ButtonGroup.vue
Normal file
36
ui/src/components/base/ButtonGroup.vue
Normal 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>
|
||||
103
ui/src/components/base/ContextMenu.vue
Normal file
103
ui/src/components/base/ContextMenu.vue
Normal 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>
|
||||
132
ui/src/components/base/DialogComp.vue
Normal file
132
ui/src/components/base/DialogComp.vue
Normal 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>
|
||||
59
ui/src/components/base/LoadComp.vue
Normal file
59
ui/src/components/base/LoadComp.vue
Normal 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>
|
||||
156
ui/src/components/base/MainMenu.vue
Normal file
156
ui/src/components/base/MainMenu.vue
Normal 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>
|
||||
107
ui/src/components/dashboard/RemoteView.vue
Normal file
107
ui/src/components/dashboard/RemoteView.vue
Normal 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>
|
||||
154
ui/src/components/dashboard/ServerView.vue
Normal file
154
ui/src/components/dashboard/ServerView.vue
Normal 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>
|
||||
225
ui/src/components/devices/RemoteView.vue
Normal file
225
ui/src/components/devices/RemoteView.vue
Normal 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>
|
||||
254
ui/src/components/devices/ServerView.vue
Normal file
254
ui/src/components/devices/ServerView.vue
Normal 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>
|
||||
176
ui/src/components/form/FormSelect.vue
Normal file
176
ui/src/components/form/FormSelect.vue
Normal 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>
|
||||
7
ui/src/components/icons/IconCommunity.vue
Normal file
7
ui/src/components/icons/IconCommunity.vue
Normal 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>
|
||||
7
ui/src/components/icons/IconDocumentation.vue
Normal file
7
ui/src/components/icons/IconDocumentation.vue
Normal 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>
|
||||
7
ui/src/components/icons/IconEcosystem.vue
Normal file
7
ui/src/components/icons/IconEcosystem.vue
Normal 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>
|
||||
7
ui/src/components/icons/IconSupport.vue
Normal file
7
ui/src/components/icons/IconSupport.vue
Normal 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>
|
||||
19
ui/src/components/icons/IconTooling.vue
Normal file
19
ui/src/components/icons/IconTooling.vue
Normal 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>
|
||||
164
ui/src/components/macros/MacroOverview.vue
Normal file
164
ui/src/components/macros/MacroOverview.vue
Normal 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>
|
||||
106
ui/src/components/macros/MacroRecorder.vue
Normal file
106
ui/src/components/macros/MacroRecorder.vue
Normal 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>
|
||||
78
ui/src/components/macros/components/DelaySpan.vue
Normal file
78
ui/src/components/macros/components/DelaySpan.vue
Normal 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>
|
||||
56
ui/src/components/macros/components/DeleteKeyDialog.vue
Normal file
56
ui/src/components/macros/components/DeleteKeyDialog.vue
Normal 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>
|
||||
'
|
||||
77
ui/src/components/macros/components/EditDelayDialog.vue
Normal file
77
ui/src/components/macros/components/EditDelayDialog.vue
Normal 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>
|
||||
124
ui/src/components/macros/components/EditKeyDialog.vue
Normal file
124
ui/src/components/macros/components/EditKeyDialog.vue
Normal 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')"
|
||||
>
|
||||
↓ Down
|
||||
</ButtonComp>
|
||||
<ButtonComp
|
||||
variant="secondary"
|
||||
:class="editable.newKey.direction === 'up' ? 'selected' : ''"
|
||||
size="sm"
|
||||
@click.prevent="handleNewDirection('up')"
|
||||
>
|
||||
↑ Up
|
||||
</ButtonComp>
|
||||
</div>
|
||||
<div class="flex justify-end">
|
||||
<ButtonComp variant="primary" size="sm" @click.prevent="changeKey()">
|
||||
Change key
|
||||
</ButtonComp>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import MacroKey from './MacroKey.vue'
|
||||
import ButtonComp from '@/components/base/ButtonComp.vue'
|
||||
import { useMacroRecorderStore } from '@/stores/macrorecorder'
|
||||
import { filterKey } from '@/services/MacroRecordService'
|
||||
|
||||
import { reactive, ref, onMounted } from 'vue'
|
||||
|
||||
const editable = reactive({
|
||||
key: {},
|
||||
newKey: {},
|
||||
})
|
||||
|
||||
const macroRecorder = useMacroRecorderStore()
|
||||
|
||||
const newKeyInput = ref(null)
|
||||
|
||||
onMounted(() => {
|
||||
editable.key = macroRecorder.getEditKey()
|
||||
editable.newKey.direction = editable.key.direction
|
||||
})
|
||||
|
||||
const handleNewKey = (e) => {
|
||||
editable.newKey.e = e
|
||||
editable.newKey.keyObj = filterKey(e)
|
||||
}
|
||||
|
||||
const handleNewDirection = (direction) => {
|
||||
editable.newKey.direction = direction
|
||||
editable.newKey.keyObj = filterKey(editable.key)
|
||||
}
|
||||
|
||||
const changeKey = () => {
|
||||
macroRecorder.recordStep(
|
||||
editable.newKey.e,
|
||||
editable.newKey.direction,
|
||||
macroRecorder.state.editKey,
|
||||
)
|
||||
|
||||
macroRecorder.state.editKey = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@reference "@/assets/main.css";
|
||||
|
||||
button.selected {
|
||||
@apply ring-2 ring-offset-1 ring-sky-500 bg-sky-500;
|
||||
}
|
||||
</style>
|
||||
90
ui/src/components/macros/components/FixedDelayMenu.vue
Normal file
90
ui/src/components/macros/components/FixedDelayMenu.vue
Normal 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>
|
||||
155
ui/src/components/macros/components/InsertKeyDialog.vue
Normal file
155
ui/src/components/macros/components/InsertKeyDialog.vue
Normal 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'"
|
||||
>
|
||||
↓ Down
|
||||
</ButtonComp>
|
||||
<ButtonComp
|
||||
variant="secondary"
|
||||
:class="keyObjs.insertDirection === 'up' ? 'selected' : ''"
|
||||
size="sm"
|
||||
@click.prevent="keyObjs.insertDirection = 'up'"
|
||||
>
|
||||
↑ Up
|
||||
</ButtonComp>
|
||||
</div>
|
||||
<div class="flex justify-end">
|
||||
<ButtonComp variant="primary" size="sm" @click="insertKey()">Insert key</ButtonComp>
|
||||
</div>
|
||||
</div>
|
||||
</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>
|
||||
130
ui/src/components/macros/components/MacroKey.vue
Normal file
130
ui/src/components/macros/components/MacroKey.vue
Normal 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' ? '↓' : '↑' }}</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>
|
||||
|
|
@ -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>
|
||||
142
ui/src/components/macros/parts/EditDialogs.vue
Normal file
142
ui/src/components/macros/parts/EditDialogs.vue
Normal 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>
|
||||
111
ui/src/components/macros/parts/RecorderFooter.vue
Normal file
111
ui/src/components/macros/parts/RecorderFooter.vue
Normal 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>
|
||||
124
ui/src/components/macros/parts/RecorderHeader.vue
Normal file
124
ui/src/components/macros/parts/RecorderHeader.vue
Normal 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>
|
||||
66
ui/src/components/macros/parts/RecorderInput.vue
Normal file
66
ui/src/components/macros/parts/RecorderInput.vue
Normal 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>
|
||||
79
ui/src/components/macros/parts/RecorderOutput.vue
Normal file
79
ui/src/components/macros/parts/RecorderOutput.vue
Normal 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>
|
||||
253
ui/src/components/panels/PanelEdit.vue
Normal file
253
ui/src/components/panels/PanelEdit.vue
Normal 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>
|
||||
103
ui/src/components/panels/PanelView.vue
Normal file
103
ui/src/components/panels/PanelView.vue
Normal 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>
|
||||
183
ui/src/components/panels/PanelsOverview.vue
Normal file
183
ui/src/components/panels/PanelsOverview.vue
Normal 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
37
ui/src/main.js
Normal 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
87
ui/src/router/index.js
Normal 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
|
||||
55
ui/src/services/ApiService.js
Normal file
55
ui/src/services/ApiService.js
Normal 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
|
||||
}
|
||||
77
ui/src/services/EncryptService.js
Normal file
77
ui/src/services/EncryptService.js
Normal 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}`
|
||||
}
|
||||
192
ui/src/services/MacroRecordService.js
Normal file
192
ui/src/services/MacroRecordService.js
Normal 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: '▲',
|
||||
ArrowRight: '▶',
|
||||
ArrowDown: '▼',
|
||||
ArrowLeft: '◀',
|
||||
// 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) }
|
||||
}
|
||||
47
ui/src/services/MacroService.js
Normal file
47
ui/src/services/MacroService.js
Normal 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)
|
||||
}
|
||||
120
ui/src/services/PanelService.js
Normal file
120
ui/src/services/PanelService.js
Normal 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()
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
117
ui/src/services/RobotKeys.md
Normal file
117
ui/src/services/RobotKeys.md
Normal 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
33
ui/src/stores/counter.js
Normal 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
153
ui/src/stores/device.js
Normal 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,
|
||||
}
|
||||
})
|
||||
260
ui/src/stores/macrorecorder.js
Normal file
260
ui/src/stores/macrorecorder.js
Normal 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
82
ui/src/stores/panel.js
Normal 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,
|
||||
}
|
||||
})
|
||||
105
ui/src/views/DashboardView.vue
Normal file
105
ui/src/views/DashboardView.vue
Normal 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>
|
||||
42
ui/src/views/DevicesView.vue
Normal file
42
ui/src/views/DevicesView.vue
Normal 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>
|
||||
7
ui/src/views/HomeView.vue
Normal file
7
ui/src/views/HomeView.vue
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
<template>
|
||||
<div id="dashboard"></div>
|
||||
</template>
|
||||
|
||||
<script setup></script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
48
ui/src/views/MacrosView.vue
Normal file
48
ui/src/views/MacrosView.vue
Normal 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>
|
||||
82
ui/src/views/PanelsView.vue
Normal file
82
ui/src/views/PanelsView.vue
Normal 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>
|
||||
28
ui/src/views/SettingsView.vue
Normal file
28
ui/src/views/SettingsView.vue
Normal 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>
|
||||
Loading…
Add table
Add a link
Reference in a new issue