mirror of
https://github.com/Macrame-App/Macrame
synced 2025-12-29 07:19:26 +00:00
Compare commits
18 commits
972bc9e970
...
a9d00d8d04
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a9d00d8d04 | ||
|
|
6b14d2b9ce | ||
|
|
195f9dbafc | ||
|
|
2760a80879 | ||
|
|
05fdaa3d3a | ||
|
|
3193127809 | ||
|
|
5dbd66ea0c | ||
|
|
2fe1266c34 | ||
|
|
bb5aedf04a | ||
|
|
b0d629da66 | ||
|
|
d914d0c3de | ||
|
|
f3cd497212 | ||
|
|
6bdace237b | ||
|
|
eb77287066 | ||
|
|
9a961079cd | ||
|
|
89b9c7a157 | ||
|
|
b85962a539 | ||
|
|
67bbbd6baf |
191 changed files with 533 additions and 42819 deletions
17
.air.toml
17
.air.toml
|
|
@ -1,17 +0,0 @@
|
||||||
# Working directory
|
|
||||||
root = "."
|
|
||||||
|
|
||||||
# The main Go file
|
|
||||||
main = "main.go"
|
|
||||||
|
|
||||||
bin = "tmp/main.exe"
|
|
||||||
|
|
||||||
# Watching all Go files, excluding certain directories
|
|
||||||
[build]
|
|
||||||
cmd = "go build -o ./tmp/main.exe main.go"
|
|
||||||
include_ext = ["go"]
|
|
||||||
exclude_dir = ["fe", "panels", "builds"]
|
|
||||||
|
|
||||||
# Restart on file changes
|
|
||||||
[log]
|
|
||||||
time = true
|
|
||||||
70
.github/workflows/windows-release.yml
vendored
Normal file
70
.github/workflows/windows-release.yml
vendored
Normal file
|
|
@ -0,0 +1,70 @@
|
||||||
|
name: Release Build (Windows)
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- "release/**"
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
windows-release-build:
|
||||||
|
runs-on: windows-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout the release branch
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Extract version from branch name
|
||||||
|
id: extract
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
version="${GITHUB_REF##*/}"
|
||||||
|
echo "version=$version" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Set up Git
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
git config user.name "github-actions"
|
||||||
|
git config user.email "github-actions@github.com"
|
||||||
|
|
||||||
|
- name: Run build script
|
||||||
|
shell: cmd
|
||||||
|
run: .\build-scripts\windows-build.bat
|
||||||
|
|
||||||
|
- name: Cleanup root directory
|
||||||
|
shell: pwsh
|
||||||
|
run: |
|
||||||
|
$folders = @("app", "ui", "build-scripts")
|
||||||
|
foreach ($folder in $folders) {
|
||||||
|
if (Test-Path $folder) {
|
||||||
|
Remove-Item -Recurse -Force -Path $folder
|
||||||
|
Write-Output "Removed development folder: $folder"
|
||||||
|
} else {
|
||||||
|
Write-Output "$folder does not exist"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
# Delete all files except: Macrame.exe, favicon.ico, README.md
|
||||||
|
Get-ChildItem -File | Where-Object { $_.Name -notin @("Macrame.exe", "favicon.ico", "README.md") } | ForEach-Object {
|
||||||
|
Write-Output "Deleting file: $($_.Name)"
|
||||||
|
Remove-Item -Force $_.FullName
|
||||||
|
}
|
||||||
|
|
||||||
|
- name: Commit and push build artifacts
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
git add -A
|
||||||
|
git commit -m "Automated release build for version: ${{ steps.extract.outputs.version }}"
|
||||||
|
git push origin ${{ github.ref }}
|
||||||
|
|
||||||
|
- name: Fetch and force merge into main
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
git fetch origin main
|
||||||
|
git checkout main
|
||||||
|
git merge -X theirs ${{ github.ref }} -m "Merging release version ${{ steps.extract.outputs.version }} into main"
|
||||||
|
git push origin main
|
||||||
14
.gitignore
vendored
14
.gitignore
vendored
|
|
@ -1,14 +0,0 @@
|
||||||
.env
|
|
||||||
config.js
|
|
||||||
|
|
||||||
devices/*
|
|
||||||
tmp/
|
|
||||||
log.txt
|
|
||||||
|
|
||||||
Macrame.exe
|
|
||||||
|
|
||||||
public
|
|
||||||
macros/*
|
|
||||||
builds
|
|
||||||
node_modules
|
|
||||||
ToDo.md
|
|
||||||
Binary file not shown.
13
README.md
Normal file
13
README.md
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
# <img src="favicon.ico" width="24" alt="Macrame icon" /> Macrame
|
||||||
|
|
||||||
|
**Version 1.0.0 is now available: [Macrame 1.0.0](https://github.com/Macrame-App/Macrame/archive/refs/heads/release/1.0.0.zip)**
|
||||||
|
|
||||||
|
Turn Any Device into a button panel. Open-source, easy, and built to supercharge your workflow or gaming.
|
||||||
|
Macrame is released under the GPL V3 licence. More information can be found at: [Macrame License](https://macrame-app.github.io/license.html)
|
||||||
|
|
||||||
|
Macrame is small application that can be used to record keyboard macros.
|
||||||
|
The macros can be linked to button panels and the panels can be used by devices on the same network.
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
Read the [official documentation](https://macrame-app.github.io/) about the Macrame app to get started.
|
||||||
|
|
@ -1,47 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# Directory to start searching from (default to current directory)
|
|
||||||
DIR="${1:-.}"
|
|
||||||
|
|
||||||
# Define the GPLv3 header content
|
|
||||||
GPL_HEADER="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/>."
|
|
||||||
|
|
||||||
# Loop through all files in the directory (and subdirectories), excluding node_modules
|
|
||||||
find "$DIR" \( -iname \*.go -o \( -path "$DIR/fe/src/*.js" -o -path "$DIR/fe/src/*.vue" -o -path "$DIR/fe/src/*.css" \) \) ! -path "*/node_modules/*" | while read file
|
|
||||||
do
|
|
||||||
# Check if the file already has a GPL header
|
|
||||||
if ! grep -q "Copyright (C) 2025 Jesse Malotaux" "$file"; then
|
|
||||||
# Check if it's a Vue file and handle it carefully
|
|
||||||
if [[ "$file" == *.vue ]]; then
|
|
||||||
echo "Adding GPL header to: $file"
|
|
||||||
# Prepend the GPL header to Vue files as raw text
|
|
||||||
# Make sure we don't add comment marks that break Vue syntax
|
|
||||||
echo -e "<!--\n$GPL_HEADER\n-->\n\n$(cat "$file")" > "$file"
|
|
||||||
else
|
|
||||||
echo "Adding GPL header to: $file"
|
|
||||||
# Prepend the GPL header to other files (go, js, ts, etc.) as comments
|
|
||||||
echo -e "/*\n$GPL_HEADER\n*/\n\n$(cat "$file")" > "$file"
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
echo "GPL header already present in: $file"
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
echo "GPL header addition complete!"
|
|
||||||
137
app/api.go
137
app/api.go
|
|
@ -1,137 +0,0 @@
|
||||||
/*
|
|
||||||
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/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package app
|
|
||||||
|
|
||||||
import (
|
|
||||||
"macrame/app/helper"
|
|
||||||
"mime"
|
|
||||||
"net/http"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
func ApiCORS(w http.ResponseWriter, r *http.Request) (http.ResponseWriter, *http.Request) {
|
|
||||||
origin := r.Header.Get("Origin")
|
|
||||||
|
|
||||||
w.Header().Set("Access-Control-Allow-Origin", "http://localhost:5173")
|
|
||||||
|
|
||||||
if strings.HasPrefix(r.Host, "192.168.") {
|
|
||||||
w.Header().Set("Access-Control-Allow-Origin", origin)
|
|
||||||
}
|
|
||||||
|
|
||||||
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
|
|
||||||
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Accept, Accept-Language, Accept-Encoding")
|
|
||||||
|
|
||||||
return w, r
|
|
||||||
}
|
|
||||||
|
|
||||||
func ApiGet(w http.ResponseWriter, r *http.Request) {
|
|
||||||
root, err := filepath.Abs("public")
|
|
||||||
if err != nil {
|
|
||||||
MCRMLog("ApiGet Abs Error: ", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var file string
|
|
||||||
|
|
||||||
if strings.Contains(r.URL.Path, "/config.js") {
|
|
||||||
file = filepath.Join(root, "config.js")
|
|
||||||
w.Header().Set("Content-Type", "text/javascript") // set content type header
|
|
||||||
} else if r.URL.Path != "/" {
|
|
||||||
file = filepath.Join(root, r.URL.Path)
|
|
||||||
}
|
|
||||||
|
|
||||||
contentType := mime.TypeByExtension(filepath.Ext(file)) // get content type
|
|
||||||
|
|
||||||
if contentType == "" {
|
|
||||||
file = filepath.Join(root, "index.html")
|
|
||||||
}
|
|
||||||
|
|
||||||
http.ServeFile(w, r, file)
|
|
||||||
}
|
|
||||||
|
|
||||||
func ApiPost(w http.ResponseWriter, r *http.Request) {
|
|
||||||
access, data, err := helper.EndpointAccess(w, r)
|
|
||||||
|
|
||||||
if !access || err != nil {
|
|
||||||
MCRMLog("ApiPost EndPointAccess Error: ", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if data != "" {
|
|
||||||
ApiAuth(data, w, r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
switch r.URL.Path {
|
|
||||||
case "/macro/check":
|
|
||||||
CheckMacro(w, r)
|
|
||||||
case "/macro/record":
|
|
||||||
SaveMacro(w, r)
|
|
||||||
case "/macro/list":
|
|
||||||
ListMacros(w, r)
|
|
||||||
case "/macro/open":
|
|
||||||
OpenMacro(w, r)
|
|
||||||
case "/macro/delete":
|
|
||||||
DeleteMacro(w, r)
|
|
||||||
case "/macro/play":
|
|
||||||
PlayMacro("", w, r)
|
|
||||||
case "/device/server/ip":
|
|
||||||
ListServerIP(w)
|
|
||||||
case "/device/list":
|
|
||||||
DeviceList(w, r)
|
|
||||||
case "/device/access/check":
|
|
||||||
DeviceAccessCheck(w, r)
|
|
||||||
case "/device/access/request":
|
|
||||||
DeviceAccessRequest(w, r)
|
|
||||||
case "/device/link/ping":
|
|
||||||
PingLink(w, r)
|
|
||||||
case "/device/link/start":
|
|
||||||
StartLink(w, r)
|
|
||||||
case "/device/link/poll":
|
|
||||||
PollLink(w, r)
|
|
||||||
case "/device/link/remove":
|
|
||||||
RemoveLink("", w, r)
|
|
||||||
case "/device/handshake":
|
|
||||||
Handshake(w, r)
|
|
||||||
case "/panel/list":
|
|
||||||
PanelList(w, r)
|
|
||||||
case "/panel/get":
|
|
||||||
GetPanel("", w, r)
|
|
||||||
case "/panel/save/json":
|
|
||||||
SavePanelJSON(w, r)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func ApiAuth(data string, w http.ResponseWriter, r *http.Request) {
|
|
||||||
switch r.URL.Path {
|
|
||||||
case "/macro/play":
|
|
||||||
PlayMacro(data, w, r)
|
|
||||||
case "/device/link/remove":
|
|
||||||
RemoveLink(data, w, r)
|
|
||||||
case "/panel/list":
|
|
||||||
MCRMLog("Authenticated Panellist")
|
|
||||||
PanelList(w, r)
|
|
||||||
case "/panel/get":
|
|
||||||
GetPanel(data, w, r)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
329
app/device.go
329
app/device.go
|
|
@ -1,329 +0,0 @@
|
||||||
/*
|
|
||||||
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/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package app
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"macrame/app/helper"
|
|
||||||
"macrame/app/structs"
|
|
||||||
"math/rand"
|
|
||||||
"net"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
func ListServerIP(w http.ResponseWriter) {
|
|
||||||
ip, err := GetServerIp()
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
MCRMLog("GetServerIP err: ", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
json.NewEncoder(w).Encode(ip)
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetServerIp() (string, error) {
|
|
||||||
ifs, err := net.Interfaces()
|
|
||||||
if err != nil {
|
|
||||||
MCRMLog(err)
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, ifi := range ifs {
|
|
||||||
// Skip interfaces that are down
|
|
||||||
if ifi.Flags&net.FlagUp == 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
// Skip loopback interfaces
|
|
||||||
if ifi.Flags&net.FlagLoopback != 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
addrs, err := ifi.Addrs()
|
|
||||||
if err != nil {
|
|
||||||
MCRMLog(err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, addr := range addrs {
|
|
||||||
var ip net.IP
|
|
||||||
|
|
||||||
switch v := addr.(type) {
|
|
||||||
case *net.IPNet:
|
|
||||||
ip = v.IP
|
|
||||||
case *net.IPAddr:
|
|
||||||
ip = v.IP
|
|
||||||
}
|
|
||||||
|
|
||||||
if ip == nil || ip.To4() == nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Skip APIPA (169.254.x.x) addresses
|
|
||||||
if ip.IsLinkLocalUnicast() {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Found a good IP, return it
|
|
||||||
return ip.String(), nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return "", fmt.Errorf("No IP found")
|
|
||||||
}
|
|
||||||
|
|
||||||
func DeviceList(w http.ResponseWriter, r *http.Request) {
|
|
||||||
dir := "devices"
|
|
||||||
|
|
||||||
files, err := os.ReadDir(dir)
|
|
||||||
if err != nil {
|
|
||||||
os.MkdirAll(dir, 0600)
|
|
||||||
files = nil
|
|
||||||
MCRMLog("DeviceList Error: ", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
devices := make(map[string]map[string]interface{})
|
|
||||||
|
|
||||||
for _, file := range files {
|
|
||||||
filePath := dir + "/" + file.Name()
|
|
||||||
ext := filepath.Ext(filePath)
|
|
||||||
device := strings.TrimSuffix(file.Name(), ext)
|
|
||||||
|
|
||||||
if _, ok := devices[device]; !ok {
|
|
||||||
devices[device] = make(map[string]interface{})
|
|
||||||
}
|
|
||||||
|
|
||||||
if ext == ".json" {
|
|
||||||
devices[device]["settings"] = readDeviceSettings(filePath)
|
|
||||||
}
|
|
||||||
if ext == ".key" {
|
|
||||||
devices[device]["key"] = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
result := map[string]interface{}{
|
|
||||||
"devices": devices,
|
|
||||||
}
|
|
||||||
|
|
||||||
json.NewEncoder(w).Encode(result)
|
|
||||||
}
|
|
||||||
|
|
||||||
func readDeviceSettings(filepath string) (settings structs.Settings) {
|
|
||||||
data, err := os.ReadFile(filepath)
|
|
||||||
if err != nil {
|
|
||||||
MCRMLog("readDeviceSettings Error: ", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = json.Unmarshal(data, &settings)
|
|
||||||
if err != nil {
|
|
||||||
MCRMLog("readDeviceSettings JSON Error: ", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return settings
|
|
||||||
}
|
|
||||||
|
|
||||||
func DeviceAccessCheck(w http.ResponseWriter, r *http.Request) {
|
|
||||||
var req structs.Check
|
|
||||||
|
|
||||||
err := json.NewDecoder(r.Body).Decode(&req)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
MCRMLog("DeviceAccessCheck Error: ", err)
|
|
||||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
_, errSett := os.Stat("devices/" + req.Uuid + ".json")
|
|
||||||
_, errKey := os.Stat("devices/" + req.Uuid + ".key")
|
|
||||||
|
|
||||||
if (errSett == nil) && (errKey == nil) {
|
|
||||||
json.NewEncoder(w).Encode("authorized")
|
|
||||||
} else if (errSett == nil) && (errKey != nil) {
|
|
||||||
MCRMLog("DeviceAccessCheck: UUID: ", req.Uuid, "; Access: Unauthorized")
|
|
||||||
json.NewEncoder(w).Encode("unauthorized")
|
|
||||||
} else {
|
|
||||||
MCRMLog("DeviceAccessCheck: UUID: ", req.Uuid, "; Access: Unlinked")
|
|
||||||
json.NewEncoder(w).Encode("unlinked")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func DeviceAccessRequest(w http.ResponseWriter, r *http.Request) {
|
|
||||||
var req structs.Request
|
|
||||||
|
|
||||||
err := json.NewDecoder(r.Body).Decode(&req)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
deviceSettings := structs.Settings{Name: req.Name, Type: req.Type}
|
|
||||||
|
|
||||||
settingsJSON, err := json.Marshal(deviceSettings)
|
|
||||||
if err != nil {
|
|
||||||
MCRMLog("DeviceAccessRequest JSON Error: ", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
err = os.WriteFile("devices/"+req.Uuid+".json", settingsJSON, 0644)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
MCRMLog("DeviceAccessRequest Error: ", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
json.NewEncoder(w).Encode("unauthorized")
|
|
||||||
}
|
|
||||||
|
|
||||||
func PingLink(w http.ResponseWriter, r *http.Request) {
|
|
||||||
var req structs.Check
|
|
||||||
err := json.NewDecoder(r.Body).Decode(&req)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
MCRMLog("PingLink Error: ", err)
|
|
||||||
json.NewEncoder(w).Encode(false)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
key, keyErr := os.ReadFile("devices/" + req.Uuid + ".key")
|
|
||||||
pin, pinErr := os.ReadFile("devices/" + req.Uuid + ".tmp")
|
|
||||||
|
|
||||||
encryptedKey, encErr := helper.EncryptAES(string(pin), string(key))
|
|
||||||
|
|
||||||
if keyErr == nil && pinErr == nil && encErr == nil {
|
|
||||||
MCRMLog("PINGLINK FIXED")
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
w.Write([]byte(encryptedKey))
|
|
||||||
return
|
|
||||||
} else {
|
|
||||||
MCRMLog("PingLink Error: keyErr:", keyErr, "; pinErr:", pinErr, "; encErr:", encErr)
|
|
||||||
}
|
|
||||||
|
|
||||||
json.NewEncoder(w).Encode(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
func StartLink(w http.ResponseWriter, r *http.Request) {
|
|
||||||
var req structs.Check
|
|
||||||
|
|
||||||
err := json.NewDecoder(r.Body).Decode(&req)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
MCRMLog("StartLink Error: ", err)
|
|
||||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
pin := fmt.Sprintf("%04d", rand.Intn(10000))
|
|
||||||
|
|
||||||
deviceKey := helper.GenerateKey()
|
|
||||||
|
|
||||||
errKey := helper.SaveDeviceKey(req.Uuid, deviceKey)
|
|
||||||
savedPin, errPin := helper.TempPinFile(req.Uuid, pin)
|
|
||||||
|
|
||||||
if errKey == nil && errPin == nil && savedPin {
|
|
||||||
json.NewEncoder(w).Encode(pin)
|
|
||||||
} else {
|
|
||||||
MCRMLog("StartLink Error: errKey:", errKey, "; errPin:", errPin)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func PollLink(w http.ResponseWriter, r *http.Request) {
|
|
||||||
var req structs.Check
|
|
||||||
|
|
||||||
err := json.NewDecoder(r.Body).Decode(&req)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
json.NewEncoder(w).Encode(false)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if helper.CheckPinFile(req.Uuid) {
|
|
||||||
json.NewEncoder(w).Encode(true)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
json.NewEncoder(w).Encode(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
func RemoveLink(data string, w http.ResponseWriter, r *http.Request) {
|
|
||||||
req := &structs.Check{}
|
|
||||||
_, err := helper.ParseRequest(req, data, r)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
MCRMLog("RemoveLink ParseRequest Error: ", err)
|
|
||||||
json.NewEncoder(w).Encode(false)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
err = os.Remove("devices/" + req.Uuid + ".key")
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
MCRMLog("RemoveLink Remove Error: ", err)
|
|
||||||
json.NewEncoder(w).Encode(false)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
json.NewEncoder(w).Encode(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
func Handshake(w http.ResponseWriter, r *http.Request) {
|
|
||||||
var req structs.Handshake
|
|
||||||
|
|
||||||
err := json.NewDecoder(r.Body).Decode(&req)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
deviceKey, err := helper.GetKeyByUuid(req.Uuid)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
MCRMLog("Handshake GetKeyByUuid Error: ", err)
|
|
||||||
json.NewEncoder(w).Encode(false)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
decryptShake, _ := helper.DecryptAES(deviceKey, req.Shake)
|
|
||||||
|
|
||||||
helper.RemovePinFile(req.Uuid)
|
|
||||||
|
|
||||||
if decryptShake == getDateStr() {
|
|
||||||
json.NewEncoder(w).Encode(true)
|
|
||||||
return
|
|
||||||
} else {
|
|
||||||
os.Remove("devices/" + req.Uuid + ".key")
|
|
||||||
}
|
|
||||||
|
|
||||||
json.NewEncoder(w).Encode(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
func getDateStr() string {
|
|
||||||
date := time.Now()
|
|
||||||
year, month, day := date.Date()
|
|
||||||
formattedDate := fmt.Sprintf("%04d%02d%02d", year, month, day)
|
|
||||||
return formattedDate
|
|
||||||
}
|
|
||||||
|
|
@ -1,123 +0,0 @@
|
||||||
/*
|
|
||||||
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/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package helper
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"net"
|
|
||||||
"net/http"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"macrame/app/structs"
|
|
||||||
. "macrame/app/structs"
|
|
||||||
)
|
|
||||||
|
|
||||||
func EndpointAccess(w http.ResponseWriter, r *http.Request) (bool, string, error) {
|
|
||||||
ip, _, err := net.SplitHostPort(r.RemoteAddr)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return false, "", errors.New(r.URL.Path + ": SplitHostPort error: " + err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isLocal(ip) && isEndpointAllowed("Local", r.URL.Path)) ||
|
|
||||||
(isLanRemote(ip) && isEndpointAllowed("Remote", r.URL.Path)) {
|
|
||||||
return true, "", nil
|
|
||||||
} else if isLanRemote(ip) && isEndpointAllowed("Auth", r.URL.Path) {
|
|
||||||
data, err := decryptAuth(r)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return false, "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
return data != "", data, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return false, "", errors.New(r.URL.Path + ": not authorized or accessible")
|
|
||||||
}
|
|
||||||
|
|
||||||
func isLocal(ip string) bool {
|
|
||||||
return ip == "127.0.0.1" || ip == "::1"
|
|
||||||
}
|
|
||||||
|
|
||||||
func isLanRemote(ip string) bool {
|
|
||||||
return strings.HasPrefix(ip, "192.168.")
|
|
||||||
}
|
|
||||||
|
|
||||||
func isEndpointAllowed(source string, endpoint string) bool {
|
|
||||||
var endpoints, err = getAllowedEndpoints(source)
|
|
||||||
if err != "" {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
if (endpoints != nil) && (len(endpoints) > 0) {
|
|
||||||
for _, e := range endpoints {
|
|
||||||
if e == endpoint {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func getAllowedEndpoints(source string) (endpoints []string, err string) {
|
|
||||||
if source == "Local" {
|
|
||||||
return Endpoints.Local, ""
|
|
||||||
}
|
|
||||||
if source == "Remote" {
|
|
||||||
return Endpoints.Remote, ""
|
|
||||||
}
|
|
||||||
if source == "Auth" {
|
|
||||||
return Endpoints.Auth, ""
|
|
||||||
}
|
|
||||||
|
|
||||||
return []string{}, "No allowed endpoints"
|
|
||||||
}
|
|
||||||
|
|
||||||
func decryptAuth(r *http.Request) (string, error) {
|
|
||||||
var req structs.Authcall
|
|
||||||
|
|
||||||
err := json.NewDecoder(r.Body).Decode(&req)
|
|
||||||
|
|
||||||
if err != nil || req.Uuid == "" || req.Data == "" {
|
|
||||||
return "", errors.New("DecryptAuth Error: " + err.Error() + "; UUID: " + req.Uuid + "; Data: " + req.Data)
|
|
||||||
}
|
|
||||||
|
|
||||||
deviceKey, errKey := GetKeyByUuid(req.Uuid)
|
|
||||||
decryptData, errDec := DecryptAES(deviceKey, req.Data)
|
|
||||||
|
|
||||||
if errKey != nil && errDec != nil || decryptData == "" {
|
|
||||||
return "", errors.New("DecryptAuth Error: " + errKey.Error() + "; " + errDec.Error() + "; UUID: " + req.Uuid + "; Data: " + req.Data)
|
|
||||||
}
|
|
||||||
|
|
||||||
return decryptData, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func ParseRequest(req interface{}, data string, r *http.Request) (d interface{}, err error) {
|
|
||||||
if data != "" {
|
|
||||||
dataBytes := []byte(data)
|
|
||||||
return req, json.Unmarshal(dataBytes, &req)
|
|
||||||
} else {
|
|
||||||
return req, json.NewDecoder(r.Body).Decode(&req)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,44 +0,0 @@
|
||||||
/*
|
|
||||||
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/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package helper
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os/exec"
|
|
||||||
"runtime"
|
|
||||||
)
|
|
||||||
|
|
||||||
func OpenBrowser(url string) bool {
|
|
||||||
var args []string
|
|
||||||
|
|
||||||
switch runtime.GOOS {
|
|
||||||
case "darwin":
|
|
||||||
args = []string{"open"}
|
|
||||||
case "windows":
|
|
||||||
args = []string{"cmd", "/c", "start"}
|
|
||||||
default:
|
|
||||||
args = []string{"xdg-open"}
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd := exec.Command(args[0], append(args[1:], url)...)
|
|
||||||
|
|
||||||
return cmd.Start() == nil
|
|
||||||
}
|
|
||||||
|
|
@ -1,66 +0,0 @@
|
||||||
/*
|
|
||||||
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/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package helper
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"os"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TempPinFile(Uuid string, pin string) (bool, error) {
|
|
||||||
err := os.WriteFile("devices/"+Uuid+".tmp", []byte(pin), 0644)
|
|
||||||
if err != nil {
|
|
||||||
return false, errors.New("TempPinFile Error: " + err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
time.AfterFunc(1*time.Minute, func() {
|
|
||||||
os.Remove("devices/" + Uuid + ".tmp")
|
|
||||||
})
|
|
||||||
|
|
||||||
return true, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func RemovePinFile(Uuid string) error { return os.Remove("devices/" + Uuid + ".tmp") }
|
|
||||||
|
|
||||||
func CheckPinFile(Uuid string) bool {
|
|
||||||
_, err := os.Stat("devices/" + Uuid + ".tmp")
|
|
||||||
return err == nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func SaveDeviceKey(Uuid string, key string) error {
|
|
||||||
err := os.WriteFile("devices/"+Uuid+".key", []byte(key), 0644)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetKeyByUuid(Uuid string) (string, error) {
|
|
||||||
data, err := os.ReadFile("devices/" + Uuid + ".key")
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return string(data), nil
|
|
||||||
}
|
|
||||||
|
|
@ -1,135 +0,0 @@
|
||||||
/*
|
|
||||||
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/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package helper
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"crypto/aes"
|
|
||||||
"crypto/cipher"
|
|
||||||
"crypto/rand"
|
|
||||||
"encoding/base64"
|
|
||||||
"errors"
|
|
||||||
mathRand "math/rand"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
func EncryptAES(key string, plaintext string) (string, error) {
|
|
||||||
origData := []byte(plaintext)
|
|
||||||
|
|
||||||
// Create AES cipher
|
|
||||||
block, err := aes.NewCipher(keyToBytes(key))
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
blockSize := block.BlockSize()
|
|
||||||
|
|
||||||
origData = PKCS5Padding(origData, blockSize)
|
|
||||||
|
|
||||||
iv := []byte(EnvGet("MCRM__IV"))
|
|
||||||
blockMode := cipher.NewCBCEncrypter(block, iv)
|
|
||||||
|
|
||||||
crypted := make([]byte, len(origData))
|
|
||||||
blockMode.CryptBlocks(crypted, origData)
|
|
||||||
|
|
||||||
cryptedString := base64.StdEncoding.EncodeToString(crypted)
|
|
||||||
|
|
||||||
return cryptedString, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func DecryptAES(key string, cryptedText string) (string, error) {
|
|
||||||
crypted, err := base64.StdEncoding.DecodeString(cryptedText)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
block, err := aes.NewCipher(keyToBytes(key))
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
iv := []byte(EnvGet("MCRM__IV"))
|
|
||||||
blockMode := cipher.NewCBCDecrypter(block, iv)
|
|
||||||
|
|
||||||
origData := make([]byte, len(crypted))
|
|
||||||
|
|
||||||
blockMode.CryptBlocks(origData, crypted)
|
|
||||||
origData, err = PKCS5UnPadding(origData)
|
|
||||||
|
|
||||||
if err != nil || len(origData) <= 3 {
|
|
||||||
return "", errors.New("invalid key")
|
|
||||||
}
|
|
||||||
|
|
||||||
origDataString := string(origData)
|
|
||||||
|
|
||||||
return origDataString, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func GenerateRandomString(length int) string {
|
|
||||||
b := make([]byte, length)
|
|
||||||
_, err := rand.Read(b)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
return base64.StdEncoding.EncodeToString(b)
|
|
||||||
}
|
|
||||||
|
|
||||||
func GenerateRandomIntegerString(length int) string {
|
|
||||||
var sb strings.Builder
|
|
||||||
for i := 0; i < length; i++ {
|
|
||||||
sb.WriteByte('0' + byte(mathRand.Intn(10)))
|
|
||||||
}
|
|
||||||
return sb.String()
|
|
||||||
}
|
|
||||||
|
|
||||||
func GenerateKey() string {
|
|
||||||
return strings.Replace(GenerateRandomString(24), "=", "", -1)
|
|
||||||
}
|
|
||||||
|
|
||||||
func keyToBytes(key string) []byte {
|
|
||||||
// Convert key to bytes
|
|
||||||
keyBytes := []byte(key)
|
|
||||||
|
|
||||||
// If key is 4 characters, append salt
|
|
||||||
if len(key) == 4 {
|
|
||||||
keyBytes = []byte(key + EnvGet("MCRM__SALT"))
|
|
||||||
}
|
|
||||||
|
|
||||||
return keyBytes
|
|
||||||
}
|
|
||||||
|
|
||||||
func PKCS5Padding(ciphertext []byte, blockSize int) []byte {
|
|
||||||
padding := blockSize - len(ciphertext)%blockSize
|
|
||||||
padtext := bytes.Repeat([]byte{byte(padding)}, padding)
|
|
||||||
return append(ciphertext, padtext...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func PKCS5UnPadding(origData []byte) ([]byte, error) {
|
|
||||||
length := len(origData)
|
|
||||||
unpadding := int(origData[length-1])
|
|
||||||
|
|
||||||
if (unpadding >= length) || (unpadding == 0) {
|
|
||||||
return nil, errors.New("unpadding error")
|
|
||||||
}
|
|
||||||
|
|
||||||
return origData[:(length - unpadding)], nil
|
|
||||||
}
|
|
||||||
|
|
@ -1,128 +0,0 @@
|
||||||
/*
|
|
||||||
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/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package helper
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"log"
|
|
||||||
"net"
|
|
||||||
"os"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
var configPath = "public/config.js"
|
|
||||||
|
|
||||||
func EnvGet(key string) string {
|
|
||||||
|
|
||||||
if !configFileExists() {
|
|
||||||
CreateConfigFile(configPath)
|
|
||||||
CheckFeDevDir()
|
|
||||||
}
|
|
||||||
|
|
||||||
data, err := os.ReadFile(configPath)
|
|
||||||
if err != nil {
|
|
||||||
log.Println("Error reading config.js:", err)
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
raw := strings.TrimSpace(string(data))
|
|
||||||
raw = strings.TrimPrefix(raw, "window.__CONFIG__ = ")
|
|
||||||
raw = strings.TrimSuffix(raw, ";")
|
|
||||||
|
|
||||||
var config map[string]string
|
|
||||||
if err := json.Unmarshal([]byte(raw), &config); err != nil {
|
|
||||||
log.Println("Error parsing config.js:", err)
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
return config[key]
|
|
||||||
}
|
|
||||||
|
|
||||||
func configFileExists() bool {
|
|
||||||
_, err := os.Stat(configPath)
|
|
||||||
return err == nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func CheckFeDevDir() {
|
|
||||||
log.Println("Checking FE dev directory...")
|
|
||||||
_, err := os.Stat("fe")
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
log.Println("Error checking FE dev directory:", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
copyConfigToFe()
|
|
||||||
}
|
|
||||||
|
|
||||||
func copyConfigToFe() {
|
|
||||||
data, err := os.ReadFile(configPath)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
log.Println("Error reading config.js:", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := os.WriteFile("fe/config.js", data, 0644); err != nil {
|
|
||||||
log.Println("Error writing config.js:", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func CreateConfigFile(filename string) {
|
|
||||||
port, _ := findOpenPort()
|
|
||||||
saltKey := GenerateKey()
|
|
||||||
salt := saltKey[:28]
|
|
||||||
iv := GenerateRandomIntegerString(16)
|
|
||||||
|
|
||||||
config := map[string]string{
|
|
||||||
"MCRM__PORT": port,
|
|
||||||
"MCRM__SALT": salt,
|
|
||||||
"MCRM__IV": iv,
|
|
||||||
}
|
|
||||||
|
|
||||||
jsonData, err := json.MarshalIndent(config, "", " ")
|
|
||||||
if err != nil {
|
|
||||||
log.Println("Error creating config JSON:", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
jsData := "window.__CONFIG__ = " + string(jsonData) + ";"
|
|
||||||
|
|
||||||
log.Println("Created JS config:", jsData)
|
|
||||||
|
|
||||||
if err := os.WriteFile(filename, []byte(jsData), 0644); err != nil {
|
|
||||||
log.Println("Error writing config.js:", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func findOpenPort() (string, error) {
|
|
||||||
addr, err := net.ResolveTCPAddr("tcp", "localhost:0")
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
l, err := net.ListenTCP("tcp", addr)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
defer l.Close()
|
|
||||||
return strconv.Itoa(l.Addr().(*net.TCPAddr).Port), nil
|
|
||||||
}
|
|
||||||
|
|
@ -1,114 +0,0 @@
|
||||||
/*
|
|
||||||
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/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package helper
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"regexp"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/go-vgo/robotgo"
|
|
||||||
)
|
|
||||||
|
|
||||||
func FormatMacroFileName(s string) string {
|
|
||||||
// Remove invalid characters
|
|
||||||
re := regexp.MustCompile(`[\/\?\*\>\<\:\"\|
|
|
||||||
]`)
|
|
||||||
s = re.ReplaceAllString(s, "")
|
|
||||||
|
|
||||||
// Replace spaces with underscores
|
|
||||||
s = strings.ReplaceAll(s, " ", "_")
|
|
||||||
|
|
||||||
// Remove special characters
|
|
||||||
re = regexp.MustCompile(`[!@#$%^&\(\)\[\]\{\}\~]`)
|
|
||||||
s = re.ReplaceAllString(s, "")
|
|
||||||
|
|
||||||
// Truncate the string
|
|
||||||
if len(s) > 255 {
|
|
||||||
s = s[:255]
|
|
||||||
}
|
|
||||||
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
|
|
||||||
func ReadMacroFile(filename string) (steps []map[string]interface{}, err error) {
|
|
||||||
content, err := os.ReadFile(filename)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
err = json.Unmarshal(content, &steps)
|
|
||||||
|
|
||||||
return steps, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func RunMacroSteps(steps []map[string]interface{}) error {
|
|
||||||
for _, step := range steps {
|
|
||||||
switch step["type"] {
|
|
||||||
case "key":
|
|
||||||
keyCode := step["code"].(string)
|
|
||||||
|
|
||||||
if strings.Contains(keyCode, "|") {
|
|
||||||
keyCode = handleToggleCode(keyCode, step["direction"].(string))
|
|
||||||
}
|
|
||||||
|
|
||||||
robotgo.KeyToggle(keyCode, step["direction"].(string))
|
|
||||||
case "delay":
|
|
||||||
|
|
||||||
time.Sleep(time.Duration(step["value"].(float64)) * time.Millisecond)
|
|
||||||
|
|
||||||
default:
|
|
||||||
return errors.New("RunMacroSteps Unknown step type: %v" + fmt.Sprint(step["type"]))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var toggleCodes = map[string]bool{}
|
|
||||||
|
|
||||||
func handleToggleCode(keyCode string, direction string) string {
|
|
||||||
splitCodes := strings.Split(keyCode, "|")
|
|
||||||
|
|
||||||
if direction == "down" {
|
|
||||||
if _, ok := toggleCodes[splitCodes[0]]; !ok {
|
|
||||||
toggleCodes[splitCodes[0]] = true
|
|
||||||
return splitCodes[0]
|
|
||||||
}
|
|
||||||
return splitCodes[1]
|
|
||||||
}
|
|
||||||
|
|
||||||
if direction == "up" {
|
|
||||||
if toggleCodes[splitCodes[0]] {
|
|
||||||
toggleCodes[splitCodes[0]] = false
|
|
||||||
return splitCodes[0]
|
|
||||||
}
|
|
||||||
delete(toggleCodes, splitCodes[0])
|
|
||||||
return splitCodes[1]
|
|
||||||
}
|
|
||||||
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
@ -1,101 +0,0 @@
|
||||||
/*
|
|
||||||
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/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package helper
|
|
||||||
|
|
||||||
import (
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
var translations = map[string]string{
|
|
||||||
"ArrowUp": "up",
|
|
||||||
"ArrowDown": "down",
|
|
||||||
"ArrowRight": "right",
|
|
||||||
"ArrowLeft": "left",
|
|
||||||
"Meta": "cmd",
|
|
||||||
"MetaLeft": "lcmd",
|
|
||||||
"MetaRight": "rcmd",
|
|
||||||
"Alt": "alt",
|
|
||||||
"AltLeft": "lalt",
|
|
||||||
"AltRight": "ralt",
|
|
||||||
"Control": "ctrl",
|
|
||||||
"ControlLeft": "lctrl",
|
|
||||||
"ControlRight": "rctrl",
|
|
||||||
"Shift": "shift",
|
|
||||||
"ShiftLeft": "lshift",
|
|
||||||
"ShiftRight": "rshift",
|
|
||||||
"AudioVolumeMute": "audio_mute",
|
|
||||||
"AudioVolumeDown": "audio_vol_down",
|
|
||||||
"AudioVolumeUp": "audio_vol_up",
|
|
||||||
"MediaTrackPrevious": "audio_prev",
|
|
||||||
"MediaTrackNext": "audio_next",
|
|
||||||
"MediaPlayPause": "audio_play|audio_pause",
|
|
||||||
"Numpad0": "num0",
|
|
||||||
"Numpad1": "num1",
|
|
||||||
"Numpad2": "num2",
|
|
||||||
"Numpad3": "num3",
|
|
||||||
"Numpad4": "num4",
|
|
||||||
"Numpad5": "num5",
|
|
||||||
"Numpad6": "num6",
|
|
||||||
"Numpad7": "num7",
|
|
||||||
"Numpad8": "num8",
|
|
||||||
"Numpad9": "num9",
|
|
||||||
"NumLock": "num_lock",
|
|
||||||
"NumpadDecimal": "num.",
|
|
||||||
"NumpadAdd": "num+",
|
|
||||||
"NumpadSubtract": "num-",
|
|
||||||
"NumpadMultiply": "num*",
|
|
||||||
"NumpadDivide": "num/",
|
|
||||||
"NumpadEnter": "num_enter",
|
|
||||||
"Clear": "num_clear",
|
|
||||||
"BracketLeft": "[",
|
|
||||||
"BracketRight": "]",
|
|
||||||
"Quote": "'",
|
|
||||||
"Semicolon": ";",
|
|
||||||
"Backquote": "`",
|
|
||||||
"Backslash": "\\",
|
|
||||||
"IntlBackslash": "\\",
|
|
||||||
"Slash": "/",
|
|
||||||
"Comma": ",",
|
|
||||||
"Period": ".",
|
|
||||||
"Equal": "=",
|
|
||||||
"Minus": "-",
|
|
||||||
}
|
|
||||||
|
|
||||||
func Translate(code string) string {
|
|
||||||
if val, ok := translations[code]; ok {
|
|
||||||
return val
|
|
||||||
}
|
|
||||||
return strings.ToLower(code)
|
|
||||||
}
|
|
||||||
|
|
||||||
func ReverseTranslate(name string) string {
|
|
||||||
if name == "\\" {
|
|
||||||
return "Backslash"
|
|
||||||
}
|
|
||||||
|
|
||||||
for key, value := range translations {
|
|
||||||
if value == name {
|
|
||||||
return key
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return name
|
|
||||||
}
|
|
||||||
45
app/log.go
45
app/log.go
|
|
@ -1,45 +0,0 @@
|
||||||
/*
|
|
||||||
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/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package app
|
|
||||||
|
|
||||||
import (
|
|
||||||
"log"
|
|
||||||
"os"
|
|
||||||
)
|
|
||||||
|
|
||||||
var logFile *os.File
|
|
||||||
|
|
||||||
func MCRMLogInit() {
|
|
||||||
var err error
|
|
||||||
// Open or create the log file with append permissions
|
|
||||||
logFile, err = os.OpenFile("log.txt", os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0666)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err) // Exit if we can't open the log file
|
|
||||||
}
|
|
||||||
|
|
||||||
// Optionally set log to write to file in addition to standard log output
|
|
||||||
log.SetOutput(logFile)
|
|
||||||
}
|
|
||||||
|
|
||||||
func MCRMLog(v ...interface{}) {
|
|
||||||
log.Println(v...) // Logs to terminal as well
|
|
||||||
}
|
|
||||||
229
app/macro.go
229
app/macro.go
|
|
@ -1,229 +0,0 @@
|
||||||
/*
|
|
||||||
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/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package app
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"log"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"macrame/app/helper"
|
|
||||||
"macrame/app/structs"
|
|
||||||
)
|
|
||||||
|
|
||||||
func CheckMacro(w http.ResponseWriter, r *http.Request) {
|
|
||||||
var req structs.MacroRequest
|
|
||||||
err := json.NewDecoder(r.Body).Decode(&req)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
MCRMLog("OpenMacro Decode Error: ", err)
|
|
||||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var filename = helper.FormatMacroFileName(req.Macro)
|
|
||||||
|
|
||||||
macroFile, err := helper.ReadMacroFile(fmt.Sprintf("macros/%s.json", filename))
|
|
||||||
|
|
||||||
if macroFile != nil && err == nil {
|
|
||||||
json.NewEncoder(w).Encode(true)
|
|
||||||
return
|
|
||||||
} else {
|
|
||||||
MCRMLog("OpenMacro ReadMacroFile Error: ", err)
|
|
||||||
json.NewEncoder(w).Encode(false)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func SaveMacro(w http.ResponseWriter, r *http.Request) {
|
|
||||||
var newMacro structs.NewMacro
|
|
||||||
|
|
||||||
body, err := io.ReadAll(r.Body)
|
|
||||||
if err != nil {
|
|
||||||
MCRMLog("SaveMacro ReadAll Error: ", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
err = json.Unmarshal(body, &newMacro)
|
|
||||||
if err != nil {
|
|
||||||
MCRMLog("SaveMacro Unmarshal Error: ", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
simplifiedSteps := make([]map[string]interface{}, 0)
|
|
||||||
for _, step := range newMacro.Steps {
|
|
||||||
simplifiedSteps = append(simplifiedSteps, simplifyMacro(step))
|
|
||||||
}
|
|
||||||
|
|
||||||
stepsJSON, err := json.Marshal(simplifiedSteps)
|
|
||||||
if err != nil {
|
|
||||||
MCRMLog("SaveMacro Marshal Error: ", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
err = os.WriteFile("macros/"+helper.FormatMacroFileName(newMacro.Name)+".json", stepsJSON, 0644)
|
|
||||||
if err != nil {
|
|
||||||
MCRMLog("SaveMacro WriteFile Error: ", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func simplifyMacro(step structs.Step) map[string]interface{} {
|
|
||||||
simplified := make(map[string]interface{})
|
|
||||||
|
|
||||||
simplified["type"] = step.Type
|
|
||||||
|
|
||||||
switch step.Type {
|
|
||||||
case "delay":
|
|
||||||
simplified["value"] = step.Value
|
|
||||||
case "key":
|
|
||||||
keyCode := step.Code
|
|
||||||
|
|
||||||
if keyCode == "" || (strings.Contains(keyCode, "Digit")) {
|
|
||||||
keyCode = step.Key
|
|
||||||
} else if strings.Contains(keyCode, "Key") {
|
|
||||||
keyCode = strings.Replace(keyCode, "Key", "", 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
simplified["code"] = helper.Translate(keyCode)
|
|
||||||
simplified["direction"] = step.Direction
|
|
||||||
}
|
|
||||||
|
|
||||||
return simplified
|
|
||||||
}
|
|
||||||
|
|
||||||
func ListMacros(w http.ResponseWriter, r *http.Request) {
|
|
||||||
dir := "macros"
|
|
||||||
files, err := os.ReadDir(dir)
|
|
||||||
if err != nil {
|
|
||||||
MCRMLog("ListMacros ReadDir Error: ", err)
|
|
||||||
json.NewEncoder(w).Encode(false)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var macroList []structs.MacroInfo
|
|
||||||
|
|
||||||
for _, file := range files {
|
|
||||||
filename := filepath.Base(file.Name())
|
|
||||||
macroname := strings.TrimSuffix(filename, filepath.Ext(filename))
|
|
||||||
nicename := strings.Replace(macroname, "_", " ", -1)
|
|
||||||
|
|
||||||
macroList = append(macroList, structs.MacroInfo{
|
|
||||||
Name: nicename,
|
|
||||||
Macroname: macroname,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
json.NewEncoder(w).Encode(macroList)
|
|
||||||
}
|
|
||||||
|
|
||||||
func DeleteMacro(w http.ResponseWriter, r *http.Request) {
|
|
||||||
var req structs.MacroRequest
|
|
||||||
|
|
||||||
err := json.NewDecoder(r.Body).Decode(&req)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
MCRMLog("DeleteMacro Decode Error: ", err)
|
|
||||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var filename = helper.FormatMacroFileName(req.Macro)
|
|
||||||
|
|
||||||
err = os.Remove("macros/" + filename + ".json")
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
MCRMLog("DeleteMacro Remove Error: ", err)
|
|
||||||
json.NewEncoder(w).Encode(false)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
log.Println("Deleted Macro:", req.Macro)
|
|
||||||
json.NewEncoder(w).Encode(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
func PlayMacro(data string, w http.ResponseWriter, r *http.Request) {
|
|
||||||
req := &structs.MacroRequest{}
|
|
||||||
_, err := helper.ParseRequest(req, data, r)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
MCRMLog("PlayMacro ParseRequest Error: ", err)
|
|
||||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
macro := req.Macro
|
|
||||||
|
|
||||||
MCRMLog("Playing Macro: ", macro)
|
|
||||||
|
|
||||||
var filename = helper.FormatMacroFileName(macro)
|
|
||||||
var filepath = fmt.Sprintf("macros/%s.json", filename)
|
|
||||||
|
|
||||||
macroFile, err := helper.ReadMacroFile(filepath)
|
|
||||||
if err != nil {
|
|
||||||
MCRMLog("PlayMacro ReadMacroFile Error: ", err)
|
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
err = helper.RunMacroSteps(macroFile)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
MCRMLog("PlayMacro RunMacroSteps Error: ", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func OpenMacro(w http.ResponseWriter, r *http.Request) {
|
|
||||||
var req structs.MacroRequest
|
|
||||||
|
|
||||||
err := json.NewDecoder(r.Body).Decode(&req)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
MCRMLog("OpenMacro Decode Error: ", err)
|
|
||||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
macroFile, err := helper.ReadMacroFile(fmt.Sprintf("macros/%s.json", req.Macro))
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
MCRMLog("OpenMacro ReadMacroFile Error: ", err)
|
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Walk through the macro file and reverse translate codes
|
|
||||||
for i, action := range macroFile {
|
|
||||||
if actionType, ok := action["type"].(string); ok && actionType == "key" {
|
|
||||||
if code, ok := action["code"].(string); ok {
|
|
||||||
macroFile[i]["code"] = helper.ReverseTranslate(code)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
json.NewEncoder(w).Encode(macroFile)
|
|
||||||
}
|
|
||||||
176
app/panel.go
176
app/panel.go
|
|
@ -1,176 +0,0 @@
|
||||||
/*
|
|
||||||
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/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package app
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/base64"
|
|
||||||
"encoding/json"
|
|
||||||
"macrame/app/helper"
|
|
||||||
"macrame/app/structs"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
func PanelList(w http.ResponseWriter, r *http.Request) {
|
|
||||||
panelDirs, err := os.ReadDir("panels")
|
|
||||||
if err != nil {
|
|
||||||
MCRMLog("PanelList ReadDir Error: ", err)
|
|
||||||
json.NewEncoder(w).Encode(false)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var panelList []interface{}
|
|
||||||
|
|
||||||
for _, panelDir := range panelDirs {
|
|
||||||
if panelDir.IsDir() {
|
|
||||||
panelList = append(panelList, getPanelInfo(panelDir.Name()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(panelList) == 0 {
|
|
||||||
MCRMLog("PanelList: No panels found")
|
|
||||||
json.NewEncoder(w).Encode(false)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
json.NewEncoder(w).Encode(panelList)
|
|
||||||
}
|
|
||||||
|
|
||||||
func getPanelInfo(dirname string) structs.PanelInfo {
|
|
||||||
var panelInfo structs.PanelInfo
|
|
||||||
|
|
||||||
jsonFile, err := os.ReadFile("panels/" + dirname + "/panel.json")
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
panelInfo.Name = strings.Replace(dirname, "_", " ", -1)
|
|
||||||
panelInfo.Description = "null"
|
|
||||||
panelInfo.AspectRatio = "null"
|
|
||||||
panelInfo.Macros = make(map[string]string)
|
|
||||||
} else {
|
|
||||||
err = json.Unmarshal(jsonFile, &panelInfo)
|
|
||||||
if err != nil {
|
|
||||||
MCRMLog("getPanelInfo Unmarshal Error: ", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
panelInfo.Dir = dirname
|
|
||||||
|
|
||||||
thumb := getPanelThumb(dirname)
|
|
||||||
panelInfo.Thumb = thumb
|
|
||||||
|
|
||||||
return panelInfo
|
|
||||||
}
|
|
||||||
|
|
||||||
func getPanelThumb(dirname string) string {
|
|
||||||
extensions := []string{".jpg", ".jpeg", ".png", ".webp"}
|
|
||||||
|
|
||||||
for _, ext := range extensions {
|
|
||||||
filename := "thumbnail" + ext
|
|
||||||
file, err := os.Open("panels/" + dirname + "/" + filename)
|
|
||||||
if err != nil {
|
|
||||||
MCRMLog("getPanelThumb Open Error: ", err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
defer file.Close()
|
|
||||||
|
|
||||||
return encodeImg(file)
|
|
||||||
}
|
|
||||||
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
func getPanelCode(dirname string) (html string, css string) {
|
|
||||||
htmlBytes, _ := os.ReadFile("panels/" + dirname + "/index.html")
|
|
||||||
cssBytes, _ := os.ReadFile("panels/" + dirname + "/output.css")
|
|
||||||
|
|
||||||
return string(htmlBytes), string(cssBytes)
|
|
||||||
}
|
|
||||||
|
|
||||||
func encodeImg(file *os.File) string {
|
|
||||||
contents, err := os.ReadFile(file.Name())
|
|
||||||
if err != nil {
|
|
||||||
MCRMLog("encodeImg ReadFile Error: ", err)
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
encoded := base64.StdEncoding.EncodeToString(contents)
|
|
||||||
return encoded
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetPanel(data string, w http.ResponseWriter, r *http.Request) {
|
|
||||||
req := &structs.PanelRequest{}
|
|
||||||
|
|
||||||
_, err := helper.ParseRequest(req, data, r)
|
|
||||||
if err != nil {
|
|
||||||
MCRMLog("GetPanel ParseRequest Error: ", err)
|
|
||||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
var response = structs.PanelResponse{}
|
|
||||||
|
|
||||||
panelInfo := getPanelInfo(req.Dir)
|
|
||||||
panelHtml, panelCss := getPanelCode(req.Dir)
|
|
||||||
|
|
||||||
response.Dir = panelInfo.Dir
|
|
||||||
response.Name = panelInfo.Name
|
|
||||||
response.Description = panelInfo.Description
|
|
||||||
response.AspectRatio = panelInfo.AspectRatio
|
|
||||||
response.Macros = panelInfo.Macros
|
|
||||||
response.Thumb = panelInfo.Thumb
|
|
||||||
response.HTML = panelHtml
|
|
||||||
response.Style = panelCss
|
|
||||||
|
|
||||||
json.NewEncoder(w).Encode(response)
|
|
||||||
}
|
|
||||||
|
|
||||||
func SavePanelJSON(w http.ResponseWriter, r *http.Request) {
|
|
||||||
var req structs.PanelSaveJSON
|
|
||||||
err := json.NewDecoder(r.Body).Decode(&req)
|
|
||||||
if err != nil {
|
|
||||||
MCRMLog("SavePanelJSON Decode Error: ", err)
|
|
||||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
filePath := "panels/" + req.Dir + "/panel.json"
|
|
||||||
|
|
||||||
req.Dir = ""
|
|
||||||
|
|
||||||
// Marshal the data to JSON without the dir field
|
|
||||||
jsonData, err := json.Marshal(req)
|
|
||||||
if err != nil {
|
|
||||||
MCRMLog("SavePanelJSON Marshal Error: ", err)
|
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
err = os.WriteFile(filePath, jsonData, 0644)
|
|
||||||
if err != nil {
|
|
||||||
MCRMLog("SavePanelJSON WriteFile Error: ", err)
|
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
}
|
|
||||||
|
|
@ -1,66 +0,0 @@
|
||||||
/*
|
|
||||||
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/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package structs
|
|
||||||
|
|
||||||
type Allowed struct {
|
|
||||||
Local []string
|
|
||||||
Remote []string
|
|
||||||
Auth []string
|
|
||||||
}
|
|
||||||
|
|
||||||
var Endpoints = Allowed{
|
|
||||||
Local: []string{
|
|
||||||
"/macro/check",
|
|
||||||
"/macro/record",
|
|
||||||
"/macro/list",
|
|
||||||
"/macro/open",
|
|
||||||
"/macro/delete",
|
|
||||||
"/macro/play",
|
|
||||||
"/device/server/ip",
|
|
||||||
"/device/list",
|
|
||||||
"/device/access/check",
|
|
||||||
"/device/access/request",
|
|
||||||
"/device/link/ping",
|
|
||||||
"/device/link/start",
|
|
||||||
"/device/link/poll",
|
|
||||||
"/device/link/remove",
|
|
||||||
"/device/handshake",
|
|
||||||
"/panel/get",
|
|
||||||
"/panel/list",
|
|
||||||
"/panel/save/json",
|
|
||||||
},
|
|
||||||
Remote: []string{
|
|
||||||
"/device/access/check",
|
|
||||||
"/device/access/request",
|
|
||||||
"/device/server/ip",
|
|
||||||
"/device/link/ping",
|
|
||||||
"/device/link/end",
|
|
||||||
"/device/handshake",
|
|
||||||
"/device/auth",
|
|
||||||
},
|
|
||||||
Auth: []string{
|
|
||||||
"/macro/play",
|
|
||||||
"/device/link/remove",
|
|
||||||
"/panel/get",
|
|
||||||
"/panel/list",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
@ -1,52 +0,0 @@
|
||||||
/*
|
|
||||||
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/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package structs
|
|
||||||
|
|
||||||
type Settings struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
Type string `json:"type"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type RemoteWebhook struct {
|
|
||||||
Event string `json:"event"`
|
|
||||||
Data string `json:"data"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Check struct {
|
|
||||||
Uuid string `json:"uuid"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Request struct {
|
|
||||||
Uuid string `json:"uuid"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
Type string `json:"type"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Handshake struct {
|
|
||||||
Uuid string `json:"uuid"`
|
|
||||||
Shake string `json:"shake"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Authcall struct {
|
|
||||||
Uuid string `json:"uuid"`
|
|
||||||
Data string `json:"d"`
|
|
||||||
}
|
|
||||||
|
|
@ -1,58 +0,0 @@
|
||||||
/*
|
|
||||||
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/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package structs
|
|
||||||
|
|
||||||
type MacroRequest struct {
|
|
||||||
Macro string `json:"macro"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Step struct {
|
|
||||||
Type string `json:"type"`
|
|
||||||
Key string `json:"key"`
|
|
||||||
Code string `json:"code"`
|
|
||||||
Location int `json:"location"`
|
|
||||||
Direction string `json:"direction"`
|
|
||||||
Value int `json:"value"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type NewMacro struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
Steps []Step `json:"steps"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type MacroInfo struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
Macroname string `json:"macroname"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type MacroKey struct {
|
|
||||||
Type string `json:"type"`
|
|
||||||
Key string `json:"key"`
|
|
||||||
Code string `json:"code"`
|
|
||||||
Location int `json:"location"`
|
|
||||||
Direction string `json:"direction"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type MacroDelay struct {
|
|
||||||
Type string `json:"type"`
|
|
||||||
Value int `json:"value"`
|
|
||||||
}
|
|
||||||
|
|
@ -1,65 +0,0 @@
|
||||||
/*
|
|
||||||
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/>.
|
|
||||||
*/
|
|
||||||
|
|
||||||
package structs
|
|
||||||
|
|
||||||
type PanelJSON struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
Description string `json:"description"`
|
|
||||||
AspectRatio string `json:"aspectRatio"`
|
|
||||||
Macros map[string]string `json:"macros"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type PanelSaveJSON struct {
|
|
||||||
// Name string `json:"name"`
|
|
||||||
// Description string `json:"description"`
|
|
||||||
// AspectRatio string `json:"aspectRatio"`
|
|
||||||
// Macros map[string]string `json:"macros"`
|
|
||||||
Dir string `json:"dir"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
Description string `json:"description"`
|
|
||||||
AspectRatio string `json:"aspectRatio"`
|
|
||||||
Macros interface{} `json:"macros"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type PanelInfo struct {
|
|
||||||
Dir string `json:"dir"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
Description string `json:"description"`
|
|
||||||
AspectRatio string `json:"aspectRatio"`
|
|
||||||
Macros map[string]string `json:"macros"`
|
|
||||||
Thumb string `json:"thumb"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type PanelRequest struct {
|
|
||||||
Dir string `json:"dir"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type PanelResponse struct {
|
|
||||||
Dir string `json:"dir"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
Description string `json:"description"`
|
|
||||||
AspectRatio string `json:"aspectRatio"`
|
|
||||||
Macros map[string]string `json:"macros"`
|
|
||||||
Thumb string `json:"thumb"`
|
|
||||||
HTML string `json:"html"`
|
|
||||||
Style string `json:"style"`
|
|
||||||
}
|
|
||||||
|
|
@ -1,64 +0,0 @@
|
||||||
package app
|
|
||||||
|
|
||||||
import (
|
|
||||||
"macrame/app/helper"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"github.com/getlantern/systray"
|
|
||||||
)
|
|
||||||
|
|
||||||
func InitSystray() {
|
|
||||||
go func() {
|
|
||||||
systray.Run(OnReady, OnExit)
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
func OnReady() {
|
|
||||||
systray.SetIcon(getIcon("favicon.ico"))
|
|
||||||
systray.SetTitle("Macrame")
|
|
||||||
systray.SetTooltip("Macrame - Server")
|
|
||||||
|
|
||||||
ip, err := GetServerIp()
|
|
||||||
|
|
||||||
if err == nil {
|
|
||||||
systray.AddMenuItem("IP: "+ip+":"+helper.EnvGet("MCRM__PORT"), "Server IP")
|
|
||||||
}
|
|
||||||
|
|
||||||
systray.AddSeparator()
|
|
||||||
|
|
||||||
addMCRMItem("Dashboard", "/")
|
|
||||||
addMCRMItem("Panels", "/panels")
|
|
||||||
addMCRMItem("Macros", "/macros")
|
|
||||||
addMCRMItem("Devices", "/devices")
|
|
||||||
|
|
||||||
systray.AddSeparator()
|
|
||||||
|
|
||||||
mQuit := systray.AddMenuItem("Quit Macrame", "Quit Macrame")
|
|
||||||
go func() {
|
|
||||||
<-mQuit.ClickedCh
|
|
||||||
os.Exit(0)
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
func addMCRMItem(name, urlPath string) {
|
|
||||||
m := systray.AddMenuItem(name, name)
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
<-m.ClickedCh
|
|
||||||
helper.OpenBrowser("http://localhost:" + helper.EnvGet("MCRM__PORT") + urlPath)
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
func OnExit() {
|
|
||||||
systray.Quit()
|
|
||||||
}
|
|
||||||
|
|
||||||
func getIcon(path string) []byte {
|
|
||||||
icon, err := os.ReadFile(path)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
MCRMLog("getIcon Error: ", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return icon
|
|
||||||
}
|
|
||||||
|
|
@ -1,99 +0,0 @@
|
||||||
package app
|
|
||||||
|
|
||||||
import (
|
|
||||||
"be/app/helper"
|
|
||||||
"log"
|
|
||||||
"mime"
|
|
||||||
"net/http"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
func ApiCORS(w http.ResponseWriter, r *http.Request) (http.ResponseWriter, *http.Request) {
|
|
||||||
origin := r.Header.Get("Origin")
|
|
||||||
|
|
||||||
w.Header().Set("Access-Control-Allow-Origin", "http://localhost:5173")
|
|
||||||
|
|
||||||
if strings.HasPrefix(r.Host, "192.168.") {
|
|
||||||
log.Println("lan device")
|
|
||||||
w.Header().Set("Access-Control-Allow-Origin", origin)
|
|
||||||
}
|
|
||||||
|
|
||||||
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
|
|
||||||
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Accept, Accept-Language, Accept-Encoding")
|
|
||||||
|
|
||||||
return w, r
|
|
||||||
}
|
|
||||||
|
|
||||||
func ApiGet(w http.ResponseWriter, r *http.Request) {
|
|
||||||
file := "" // base directory
|
|
||||||
|
|
||||||
if r.URL.Path != "/" {
|
|
||||||
file = "../public" + r.URL.Path // request
|
|
||||||
}
|
|
||||||
contentType := mime.TypeByExtension(filepath.Ext(file)) // get content type
|
|
||||||
|
|
||||||
if contentType != "" {
|
|
||||||
w.Header().Set("Content-Type", contentType) // set content type header
|
|
||||||
}
|
|
||||||
|
|
||||||
if contentType == "" {
|
|
||||||
file = "../public/index.html" // default
|
|
||||||
}
|
|
||||||
|
|
||||||
// log.Println("GET:", file)
|
|
||||||
|
|
||||||
http.ServeFile(w, r, file) // serve file
|
|
||||||
}
|
|
||||||
|
|
||||||
func ApiPost(w http.ResponseWriter, r *http.Request) {
|
|
||||||
|
|
||||||
access, data := helper.EndpointAccess(w, r)
|
|
||||||
|
|
||||||
if !access {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Println("api post", data == "")
|
|
||||||
if data != "" {
|
|
||||||
ApiAuth(data, w, r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
switch r.URL.Path {
|
|
||||||
case "/macro/record":
|
|
||||||
SaveMacro(w, r)
|
|
||||||
case "/macro/list":
|
|
||||||
ListMacros(w, r)
|
|
||||||
case "/macro/delete":
|
|
||||||
DeleteMacro(w, r)
|
|
||||||
case "/macro/play":
|
|
||||||
PlayMacro("", w, r)
|
|
||||||
case "/device/list":
|
|
||||||
DeviceList(w, r)
|
|
||||||
case "/device/access/check":
|
|
||||||
DeviceAccessCheck(w, r)
|
|
||||||
case "/device/access/request":
|
|
||||||
DeviceAccessRequest(w, r)
|
|
||||||
case "/device/link/ping":
|
|
||||||
PingLink(w, r)
|
|
||||||
case "/device/link/start":
|
|
||||||
StartLink(w, r)
|
|
||||||
case "/device/link/poll":
|
|
||||||
PollLink(w, r)
|
|
||||||
case "/device/link/remove":
|
|
||||||
RemoveLink("", w, r)
|
|
||||||
case "/device/handshake":
|
|
||||||
Handshake(w, r)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func ApiAuth(data string, w http.ResponseWriter, r *http.Request) {
|
|
||||||
log.Println("apiauth", data != "")
|
|
||||||
switch r.URL.Path {
|
|
||||||
case "/macro/play":
|
|
||||||
PlayMacro(data, w, r)
|
|
||||||
case "/device/link/remove":
|
|
||||||
RemoveLink(data, w, r)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
245
be/app/device.go
245
be/app/device.go
|
|
@ -1,245 +0,0 @@
|
||||||
package app
|
|
||||||
|
|
||||||
import (
|
|
||||||
"be/app/helper"
|
|
||||||
"be/app/structs"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
"math/rand"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
func DeviceList(w http.ResponseWriter, r *http.Request) {
|
|
||||||
log.Println("device list")
|
|
||||||
dir := "devices"
|
|
||||||
files, err := os.ReadDir(dir)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
devices := make(map[string]map[string]interface{})
|
|
||||||
|
|
||||||
for _, file := range files {
|
|
||||||
filePath := dir + "/" + file.Name()
|
|
||||||
ext := filepath.Ext(filePath)
|
|
||||||
device := strings.TrimSuffix(file.Name(), ext)
|
|
||||||
|
|
||||||
log.Println(device, ext)
|
|
||||||
|
|
||||||
if _, ok := devices[device]; !ok {
|
|
||||||
devices[device] = make(map[string]interface{})
|
|
||||||
}
|
|
||||||
|
|
||||||
if ext == ".json" {
|
|
||||||
devices[device]["settings"] = readDeviceSettings(filePath)
|
|
||||||
}
|
|
||||||
if ext == ".key" {
|
|
||||||
devices[device]["key"] = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
result := map[string]interface{}{
|
|
||||||
"devices": devices,
|
|
||||||
}
|
|
||||||
|
|
||||||
json.NewEncoder(w).Encode(result)
|
|
||||||
}
|
|
||||||
|
|
||||||
func readDeviceSettings(filepath string) (settings structs.Settings) {
|
|
||||||
data, err := os.ReadFile(filepath)
|
|
||||||
if err != nil {
|
|
||||||
log.Println(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = json.Unmarshal(data, &settings)
|
|
||||||
if err != nil {
|
|
||||||
log.Println(err)
|
|
||||||
}
|
|
||||||
log.Println(settings)
|
|
||||||
return settings
|
|
||||||
}
|
|
||||||
|
|
||||||
func DeviceAccessCheck(w http.ResponseWriter, r *http.Request) {
|
|
||||||
log.Println("device access check")
|
|
||||||
var req structs.Check
|
|
||||||
|
|
||||||
err := json.NewDecoder(r.Body).Decode(&req)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
_, errSett := os.Stat("devices/" + req.Uuid + ".json")
|
|
||||||
_, errKey := os.Stat("devices/" + req.Uuid + ".key")
|
|
||||||
|
|
||||||
if (errSett == nil) && (errKey == nil) {
|
|
||||||
log.Println("authorized")
|
|
||||||
json.NewEncoder(w).Encode("authorized")
|
|
||||||
} else if (errSett == nil) && (errKey != nil) {
|
|
||||||
log.Println("unauthorized")
|
|
||||||
json.NewEncoder(w).Encode("unauthorized")
|
|
||||||
} else {
|
|
||||||
log.Println("unauthorized")
|
|
||||||
json.NewEncoder(w).Encode("unlinked")
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func DeviceAccessRequest(w http.ResponseWriter, r *http.Request) {
|
|
||||||
var req structs.Request
|
|
||||||
|
|
||||||
err := json.NewDecoder(r.Body).Decode(&req)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
deviceSettings := structs.Settings{Name: req.Name, Type: req.Type}
|
|
||||||
|
|
||||||
settingsJSON, err := json.Marshal(deviceSettings)
|
|
||||||
if err != nil {
|
|
||||||
log.Println(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
err = os.WriteFile("devices/"+req.Uuid+".json", settingsJSON, 0644)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
log.Println(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
json.NewEncoder(w).Encode("unauthorized")
|
|
||||||
}
|
|
||||||
|
|
||||||
func PingLink(w http.ResponseWriter, r *http.Request) {
|
|
||||||
log.Println("ping link")
|
|
||||||
var req structs.Check
|
|
||||||
err := json.NewDecoder(r.Body).Decode(&req)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
json.NewEncoder(w).Encode(false)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
key, keyErr := os.ReadFile("devices/" + req.Uuid + ".key")
|
|
||||||
pin, pinErr := os.ReadFile("devices/" + req.Uuid + ".tmp")
|
|
||||||
|
|
||||||
encryptedKey, encErr := helper.EncryptAES(string(pin), string(key))
|
|
||||||
|
|
||||||
log.Println(encryptedKey, string(pin), string(key))
|
|
||||||
|
|
||||||
if keyErr == nil && pinErr == nil && encErr == nil {
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
w.Write([]byte(encryptedKey))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
json.NewEncoder(w).Encode(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
func StartLink(w http.ResponseWriter, r *http.Request) {
|
|
||||||
log.Println("start link")
|
|
||||||
var req structs.Check
|
|
||||||
|
|
||||||
err := json.NewDecoder(r.Body).Decode(&req)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
pin := fmt.Sprintf("%04d", rand.Intn(10000))
|
|
||||||
|
|
||||||
deviceKey := helper.GenerateKey()
|
|
||||||
|
|
||||||
err = helper.SaveDeviceKey(req.Uuid, deviceKey)
|
|
||||||
|
|
||||||
if err == nil && helper.TempPinFile(req.Uuid, pin) {
|
|
||||||
json.NewEncoder(w).Encode(pin)
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func PollLink(w http.ResponseWriter, r *http.Request) {
|
|
||||||
var req structs.Check
|
|
||||||
|
|
||||||
err := json.NewDecoder(r.Body).Decode(&req)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
json.NewEncoder(w).Encode(false)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if helper.CheckPinFile(req.Uuid) {
|
|
||||||
json.NewEncoder(w).Encode(true)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
json.NewEncoder(w).Encode(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
func RemoveLink(data string, w http.ResponseWriter, r *http.Request) {
|
|
||||||
req := &structs.Check{}
|
|
||||||
_, err := helper.ParseRequest(req, data, r)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
json.NewEncoder(w).Encode(false)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
err = os.Remove("devices/" + req.Uuid + ".key")
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
json.NewEncoder(w).Encode(false)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
json.NewEncoder(w).Encode(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
func Handshake(w http.ResponseWriter, r *http.Request) {
|
|
||||||
var req structs.Handshake
|
|
||||||
|
|
||||||
err := json.NewDecoder(r.Body).Decode(&req)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
deviceKey, err := helper.GetKeyByUuid(req.Uuid)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
json.NewEncoder(w).Encode(false)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
decryptShake, _ := helper.DecryptAES(deviceKey, req.Shake)
|
|
||||||
|
|
||||||
if decryptShake == getDateStr() {
|
|
||||||
os.Remove("devices/" + req.Uuid + ".tmp")
|
|
||||||
|
|
||||||
json.NewEncoder(w).Encode(true)
|
|
||||||
return
|
|
||||||
} else {
|
|
||||||
os.Remove("devices/" + req.Uuid + ".key")
|
|
||||||
}
|
|
||||||
|
|
||||||
json.NewEncoder(w).Encode(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
func getDateStr() string {
|
|
||||||
date := time.Now()
|
|
||||||
year, month, day := date.Date()
|
|
||||||
formattedDate := fmt.Sprintf("%04d%02d%02d", year, month, day)
|
|
||||||
return formattedDate
|
|
||||||
}
|
|
||||||
|
|
@ -1,102 +0,0 @@
|
||||||
package helper
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"log"
|
|
||||||
"net"
|
|
||||||
"net/http"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"be/app/structs"
|
|
||||||
. "be/app/structs"
|
|
||||||
)
|
|
||||||
|
|
||||||
func EndpointAccess(w http.ResponseWriter, r *http.Request) (bool, string) {
|
|
||||||
ip, _, err := net.SplitHostPort(r.RemoteAddr)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isLocal(ip) && isEndpointAllowed("Local", r.URL.Path)) ||
|
|
||||||
(isLanRemote(ip) && isEndpointAllowed("Remote", r.URL.Path)) {
|
|
||||||
log.Println(r.URL.Path, "endpoint access: accessible")
|
|
||||||
return true, ""
|
|
||||||
} else if isLanRemote(ip) && isEndpointAllowed("Auth", r.URL.Path) {
|
|
||||||
log.Println(r.URL.Path, "endpoint access: authorized")
|
|
||||||
|
|
||||||
data := decryptAuth(r)
|
|
||||||
|
|
||||||
return data != "", data
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Println(r.URL.Path, "endpoint access: not authorized or accessible")
|
|
||||||
|
|
||||||
return false, ""
|
|
||||||
}
|
|
||||||
|
|
||||||
func isLocal(ip string) bool {
|
|
||||||
return ip == "127.0.0.1" || ip == "::1"
|
|
||||||
}
|
|
||||||
|
|
||||||
func isLanRemote(ip string) bool {
|
|
||||||
return strings.HasPrefix(ip, "192.168.")
|
|
||||||
}
|
|
||||||
|
|
||||||
func isEndpointAllowed(source string, endpoint string) bool {
|
|
||||||
var endpoints, err = getAllowedEndpoints(source)
|
|
||||||
if err != "" {
|
|
||||||
log.Println(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (endpoints != nil) && (len(endpoints) > 0) {
|
|
||||||
for _, e := range endpoints {
|
|
||||||
if e == endpoint {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func getAllowedEndpoints(source string) (endpoints []string, err string) {
|
|
||||||
if source == "Local" {
|
|
||||||
return Endpoints.Local, ""
|
|
||||||
}
|
|
||||||
if source == "Remote" {
|
|
||||||
return Endpoints.Remote, ""
|
|
||||||
}
|
|
||||||
if source == "Auth" {
|
|
||||||
return Endpoints.Auth, ""
|
|
||||||
}
|
|
||||||
|
|
||||||
return []string{}, "No allowed endpoints"
|
|
||||||
}
|
|
||||||
|
|
||||||
func decryptAuth(r *http.Request) string {
|
|
||||||
var req structs.Authcall
|
|
||||||
|
|
||||||
err := json.NewDecoder(r.Body).Decode(&req)
|
|
||||||
|
|
||||||
if err != nil || req.Uuid == "" || req.Data == "" {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
deviceKey, errKey := GetKeyByUuid(req.Uuid)
|
|
||||||
decryptData, errDec := DecryptAES(deviceKey, req.Data)
|
|
||||||
|
|
||||||
if errKey != nil && errDec != nil || decryptData == "" {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
return decryptData
|
|
||||||
}
|
|
||||||
|
|
||||||
func ParseRequest(req interface{}, data string, r *http.Request) (d interface{}, err error) {
|
|
||||||
if data != "" {
|
|
||||||
dataBytes := []byte(data)
|
|
||||||
return req, json.Unmarshal(dataBytes, &req)
|
|
||||||
} else {
|
|
||||||
return req, json.NewDecoder(r.Body).Decode(&req)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,23 +0,0 @@
|
||||||
package helper
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os/exec"
|
|
||||||
"runtime"
|
|
||||||
)
|
|
||||||
|
|
||||||
func OpenBrowser(url string) bool {
|
|
||||||
var args []string
|
|
||||||
|
|
||||||
switch runtime.GOOS {
|
|
||||||
case "darwin":
|
|
||||||
args = []string{"open"}
|
|
||||||
case "windows":
|
|
||||||
args = []string{"cmd", "/c", "start"}
|
|
||||||
default:
|
|
||||||
args = []string{"xdg-open"}
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd := exec.Command(args[0], append(args[1:], url)...)
|
|
||||||
|
|
||||||
return cmd.Start() == nil
|
|
||||||
}
|
|
||||||
|
|
@ -1,46 +0,0 @@
|
||||||
package helper
|
|
||||||
|
|
||||||
import (
|
|
||||||
"log"
|
|
||||||
"os"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TempPinFile(Uuid string, pin string) bool {
|
|
||||||
log.Println("temp pin file", Uuid, pin)
|
|
||||||
err := os.WriteFile("devices/"+Uuid+".tmp", []byte(pin), 0644)
|
|
||||||
if err != nil {
|
|
||||||
log.Println(err)
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
time.AfterFunc(1*time.Minute, func() {
|
|
||||||
log.Println("deleting", Uuid, pin)
|
|
||||||
os.Remove("devices/" + Uuid + ".tmp")
|
|
||||||
})
|
|
||||||
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
|
|
||||||
func CheckPinFile(Uuid string) bool {
|
|
||||||
_, err := os.Stat("devices/" + Uuid + ".tmp")
|
|
||||||
return err == nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func SaveDeviceKey(Uuid string, key string) error {
|
|
||||||
err := os.WriteFile("devices/"+Uuid+".key", []byte(key), 0644)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func GetKeyByUuid(Uuid string) (string, error) {
|
|
||||||
data, err := os.ReadFile("devices/" + Uuid + ".key")
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return string(data), nil
|
|
||||||
}
|
|
||||||
|
|
@ -1,105 +0,0 @@
|
||||||
package helper
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"crypto/aes"
|
|
||||||
"crypto/cipher"
|
|
||||||
"crypto/rand"
|
|
||||||
"encoding/base64"
|
|
||||||
"errors"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
func EncryptAES(key string, plaintext string) (string, error) {
|
|
||||||
origData := []byte(plaintext)
|
|
||||||
|
|
||||||
// Create AES cipher
|
|
||||||
block, err := aes.NewCipher(keyToBytes(key))
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
blockSize := block.BlockSize()
|
|
||||||
|
|
||||||
origData = PKCS5Padding(origData, blockSize)
|
|
||||||
|
|
||||||
iv := []byte(EnvGet("MCRM__IV"))
|
|
||||||
blockMode := cipher.NewCBCEncrypter(block, iv)
|
|
||||||
|
|
||||||
crypted := make([]byte, len(origData))
|
|
||||||
blockMode.CryptBlocks(crypted, origData)
|
|
||||||
|
|
||||||
cryptedString := base64.StdEncoding.EncodeToString(crypted)
|
|
||||||
|
|
||||||
return cryptedString, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func DecryptAES(key string, cryptedText string) (string, error) {
|
|
||||||
crypted, err := base64.StdEncoding.DecodeString(cryptedText)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
block, err := aes.NewCipher(keyToBytes(key))
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
iv := []byte(EnvGet("MCRM__IV"))
|
|
||||||
blockMode := cipher.NewCBCDecrypter(block, iv)
|
|
||||||
|
|
||||||
origData := make([]byte, len(crypted))
|
|
||||||
|
|
||||||
blockMode.CryptBlocks(origData, crypted)
|
|
||||||
origData, err = PKCS5UnPadding(origData)
|
|
||||||
|
|
||||||
if err != nil || len(origData) <= 3 {
|
|
||||||
return "", errors.New("invalid key")
|
|
||||||
}
|
|
||||||
|
|
||||||
origDataString := string(origData)
|
|
||||||
|
|
||||||
return origDataString, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func generateRandomString(length int) string {
|
|
||||||
b := make([]byte, length)
|
|
||||||
_, err := rand.Read(b)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
return base64.StdEncoding.EncodeToString(b)
|
|
||||||
}
|
|
||||||
|
|
||||||
func GenerateKey() string {
|
|
||||||
return strings.Replace(generateRandomString(24), "=", "", -1)
|
|
||||||
}
|
|
||||||
|
|
||||||
func keyToBytes(key string) []byte {
|
|
||||||
// Convert key to bytes
|
|
||||||
keyBytes := []byte(key)
|
|
||||||
|
|
||||||
// If key is 4 characters, append salt
|
|
||||||
if len(key) == 4 {
|
|
||||||
keyBytes = []byte(key + EnvGet("MCRM__SALT"))
|
|
||||||
}
|
|
||||||
|
|
||||||
return keyBytes
|
|
||||||
}
|
|
||||||
|
|
||||||
func PKCS5Padding(ciphertext []byte, blockSize int) []byte {
|
|
||||||
padding := blockSize - len(ciphertext)%blockSize
|
|
||||||
padtext := bytes.Repeat([]byte{byte(padding)}, padding)
|
|
||||||
return append(ciphertext, padtext...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func PKCS5UnPadding(origData []byte) ([]byte, error) {
|
|
||||||
length := len(origData)
|
|
||||||
unpadding := int(origData[length-1])
|
|
||||||
|
|
||||||
if (unpadding >= length) || (unpadding == 0) {
|
|
||||||
return nil, errors.New("unpadding error")
|
|
||||||
}
|
|
||||||
|
|
||||||
return origData[:(length - unpadding)], nil
|
|
||||||
}
|
|
||||||
|
|
@ -1,16 +0,0 @@
|
||||||
package helper
|
|
||||||
|
|
||||||
import (
|
|
||||||
"log"
|
|
||||||
"os"
|
|
||||||
|
|
||||||
"github.com/joho/godotenv"
|
|
||||||
)
|
|
||||||
|
|
||||||
func EnvGet(key string) string {
|
|
||||||
err := godotenv.Load("../.env")
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal("Error loading .env file")
|
|
||||||
}
|
|
||||||
return os.Getenv("VITE_" + key)
|
|
||||||
}
|
|
||||||
|
|
@ -1,64 +0,0 @@
|
||||||
package helper
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"log"
|
|
||||||
"os"
|
|
||||||
"regexp"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"be/app/structs"
|
|
||||||
|
|
||||||
"github.com/go-vgo/robotgo"
|
|
||||||
)
|
|
||||||
|
|
||||||
func FormatMacroFileName(s string) string {
|
|
||||||
// Remove invalid characters
|
|
||||||
re := regexp.MustCompile(`[\/\?\*\>\<\:\\"\|\n]`)
|
|
||||||
s = re.ReplaceAllString(s, "")
|
|
||||||
|
|
||||||
// Replace spaces with underscores
|
|
||||||
s = strings.ReplaceAll(s, " ", "_")
|
|
||||||
|
|
||||||
// Remove special characters
|
|
||||||
re = regexp.MustCompile(`[!@#$%^&\(\)\[\]\{\}\~]`)
|
|
||||||
s = re.ReplaceAllString(s, "")
|
|
||||||
|
|
||||||
// Truncate the string
|
|
||||||
if len(s) > 255 {
|
|
||||||
s = s[:255]
|
|
||||||
}
|
|
||||||
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
|
|
||||||
func ReadMacroFile(filename string) (steps []structs.Step, err error) {
|
|
||||||
log.Println(filename)
|
|
||||||
|
|
||||||
content, err := os.ReadFile(filename)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal("Error when opening file: ", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = json.Unmarshal(content, &steps)
|
|
||||||
|
|
||||||
return steps, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func RunMacroSteps(steps []structs.Step) {
|
|
||||||
for _, step := range steps {
|
|
||||||
// log.Println(step)
|
|
||||||
switch step.Type {
|
|
||||||
case "key":
|
|
||||||
robotgo.KeyToggle(step.Key, step.Direction)
|
|
||||||
// log.Println("Toggling", step.Key, "to", step.Direction)
|
|
||||||
case "delay":
|
|
||||||
time.Sleep(time.Duration(step.Location) * time.Millisecond)
|
|
||||||
// log.Println("Sleeping for", step.Value, "milliseconds")
|
|
||||||
default:
|
|
||||||
log.Println("Unknown step type:", step.Type)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,88 +0,0 @@
|
||||||
package app
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"log"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"be/app/helper"
|
|
||||||
"be/app/structs"
|
|
||||||
)
|
|
||||||
|
|
||||||
func SaveMacro(w http.ResponseWriter, r *http.Request) {
|
|
||||||
var newMacro structs.NewMacro
|
|
||||||
|
|
||||||
body, err := io.ReadAll(r.Body)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Println(string(body))
|
|
||||||
|
|
||||||
err = json.Unmarshal(body, &newMacro)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
stepsJSON, err := json.Marshal(newMacro.Steps)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = os.WriteFile("../macros/"+helper.FormatMacroFileName(newMacro.Name)+".json", stepsJSON, 0644)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func ListMacros(w http.ResponseWriter, r *http.Request) {
|
|
||||||
log.Println("listing macros")
|
|
||||||
dir := "../macros"
|
|
||||||
files, err := os.ReadDir(dir)
|
|
||||||
if err != nil {
|
|
||||||
log.Println(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var fileNames []string
|
|
||||||
|
|
||||||
for _, file := range files {
|
|
||||||
filename := filepath.Base(file.Name())
|
|
||||||
filename = strings.TrimSuffix(filename, filepath.Ext(filename))
|
|
||||||
filename = strings.Replace(filename, "_", " ", -1)
|
|
||||||
|
|
||||||
fileNames = append(fileNames, filename)
|
|
||||||
}
|
|
||||||
|
|
||||||
json.NewEncoder(w).Encode(fileNames)
|
|
||||||
}
|
|
||||||
|
|
||||||
func DeleteMacro(w http.ResponseWriter, r *http.Request) {}
|
|
||||||
|
|
||||||
func PlayMacro(data string, w http.ResponseWriter, r *http.Request) {
|
|
||||||
req := &structs.MacroRequest{}
|
|
||||||
_, err := helper.ParseRequest(req, data, r)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
macro := req.Macro
|
|
||||||
|
|
||||||
var filename = helper.FormatMacroFileName(macro)
|
|
||||||
var filepath = fmt.Sprintf("../macros/%s.json", filename)
|
|
||||||
|
|
||||||
macroFile, err := helper.ReadMacroFile(filepath)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println(err)
|
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
helper.RunMacroSteps(macroFile)
|
|
||||||
}
|
|
||||||
|
|
@ -1,37 +0,0 @@
|
||||||
package structs
|
|
||||||
|
|
||||||
type Allowed struct {
|
|
||||||
Local []string
|
|
||||||
Remote []string
|
|
||||||
Auth []string
|
|
||||||
}
|
|
||||||
|
|
||||||
var Endpoints = Allowed{
|
|
||||||
Local: []string{
|
|
||||||
"/macro/record",
|
|
||||||
"/macro/list",
|
|
||||||
"/macro/delete",
|
|
||||||
"/macro/play",
|
|
||||||
"/device/list",
|
|
||||||
"/device/access/check",
|
|
||||||
"/device/access/request",
|
|
||||||
"/device/link/ping",
|
|
||||||
"/device/link/start",
|
|
||||||
"/device/link/poll",
|
|
||||||
"/device/link/remove",
|
|
||||||
"/device/handshake",
|
|
||||||
},
|
|
||||||
Remote: []string{
|
|
||||||
"/macro/list",
|
|
||||||
"/device/access/check",
|
|
||||||
"/device/access/request",
|
|
||||||
"/device/link/ping",
|
|
||||||
"/device/link/end",
|
|
||||||
"/device/handshake",
|
|
||||||
"/device/auth",
|
|
||||||
},
|
|
||||||
Auth: []string{
|
|
||||||
"/macro/play",
|
|
||||||
"/device/link/remove",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
@ -1,31 +0,0 @@
|
||||||
package structs
|
|
||||||
|
|
||||||
type Settings struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
Type string `json:"type"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type RemoteWebhook struct {
|
|
||||||
Event string `json:"event"`
|
|
||||||
Data string `json:"data"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Check struct {
|
|
||||||
Uuid string `json:"uuid"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Request struct {
|
|
||||||
Uuid string `json:"uuid"`
|
|
||||||
Name string `json:"name"`
|
|
||||||
Type string `json:"type"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Handshake struct {
|
|
||||||
Uuid string `json:"uuid"`
|
|
||||||
Shake string `json:"shake"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Authcall struct {
|
|
||||||
Uuid string `json:"uuid"`
|
|
||||||
Data string `json:"d"`
|
|
||||||
}
|
|
||||||
|
|
@ -1,19 +0,0 @@
|
||||||
package structs
|
|
||||||
|
|
||||||
type MacroRequest struct {
|
|
||||||
Macro string `json:"macro"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Step struct {
|
|
||||||
Type string `json:"type"`
|
|
||||||
Key string `json:"key"`
|
|
||||||
Code string `json:"code"`
|
|
||||||
Location int `json:"location"`
|
|
||||||
Direction string `json:"direction"`
|
|
||||||
Value int `json:"value"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type NewMacro struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
Steps []Step `json:"steps"`
|
|
||||||
}
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
{"name":"Unknown desktop","type":"desktop"}
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
3JYxP8LOq1Y2fhpgEtpXVJ3v4s3qdML3
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
{"name":"Unknown mobile","type":"mobile"}
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
4MqIbBoPsHizsWCyeqg6gd/wpQzfhc7e
|
|
||||||
38
be/go.mod
38
be/go.mod
|
|
@ -1,38 +0,0 @@
|
||||||
module be
|
|
||||||
|
|
||||||
go 1.24.0
|
|
||||||
|
|
||||||
require (
|
|
||||||
github.com/go-vgo/robotgo v0.110.6
|
|
||||||
github.com/joho/godotenv v1.5.1
|
|
||||||
)
|
|
||||||
|
|
||||||
require (
|
|
||||||
github.com/dblohm7/wingoes v0.0.0-20240820181039-f2b84150679e // indirect
|
|
||||||
github.com/ebitengine/purego v0.8.2 // indirect
|
|
||||||
github.com/gen2brain/shm v0.1.1 // indirect
|
|
||||||
github.com/go-ole/go-ole v1.3.0 // indirect
|
|
||||||
github.com/godbus/dbus/v5 v5.1.0 // indirect
|
|
||||||
github.com/jezek/xgb v1.1.1 // indirect
|
|
||||||
github.com/kbinani/screenshot v0.0.0-20250118074034-a3924b7bbc8c // indirect
|
|
||||||
github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683 // indirect
|
|
||||||
github.com/lxn/win v0.0.0-20210218163916-a377121e959e // indirect
|
|
||||||
github.com/otiai10/gosseract v2.2.1+incompatible // indirect
|
|
||||||
github.com/otiai10/mint v1.6.3 // indirect
|
|
||||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
|
|
||||||
github.com/robotn/xgb v0.10.0 // indirect
|
|
||||||
github.com/robotn/xgbutil v0.10.0 // indirect
|
|
||||||
github.com/shirou/gopsutil/v4 v4.25.1 // indirect
|
|
||||||
github.com/tailscale/win v0.0.0-20250213223159-5992cb43ca35 // indirect
|
|
||||||
github.com/tklauser/go-sysconf v0.3.14 // indirect
|
|
||||||
github.com/tklauser/numcpus v0.9.0 // indirect
|
|
||||||
github.com/vcaesar/gops v0.40.0 // indirect
|
|
||||||
github.com/vcaesar/imgo v0.40.2 // indirect
|
|
||||||
github.com/vcaesar/keycode v0.10.1 // indirect
|
|
||||||
github.com/vcaesar/tt v0.20.1 // indirect
|
|
||||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
|
||||||
golang.org/x/exp v0.0.0-20250215185904-eff6e970281f // indirect
|
|
||||||
golang.org/x/image v0.24.0 // indirect
|
|
||||||
golang.org/x/net v0.38.0 // indirect
|
|
||||||
golang.org/x/sys v0.31.0 // indirect
|
|
||||||
)
|
|
||||||
80
be/go.sum
80
be/go.sum
|
|
@ -1,80 +0,0 @@
|
||||||
github.com/BurntSushi/freetype-go v0.0.0-20160129220410-b763ddbfe298/go.mod h1:D+QujdIlUNfa0igpNMk6UIvlb6C252URs4yupRUV4lQ=
|
|
||||||
github.com/BurntSushi/graphics-go v0.0.0-20160129215708-b43f31a4a966/go.mod h1:Mid70uvE93zn9wgF92A/r5ixgnvX8Lh68fxp9KQBaI0=
|
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
|
||||||
github.com/dblohm7/wingoes v0.0.0-20240820181039-f2b84150679e h1:L+XrFvD0vBIBm+Wf9sFN6aU395t7JROoai0qXZraA4U=
|
|
||||||
github.com/dblohm7/wingoes v0.0.0-20240820181039-f2b84150679e/go.mod h1:SUxUaAK/0UG5lYyZR1L1nC4AaYYvSSYTWQSH3FPcxKU=
|
|
||||||
github.com/ebitengine/purego v0.8.2 h1:jPPGWs2sZ1UgOSgD2bClL0MJIqu58nOmIcBuXr62z1I=
|
|
||||||
github.com/ebitengine/purego v0.8.2/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
|
||||||
github.com/gen2brain/shm v0.1.1 h1:1cTVA5qcsUFixnDHl14TmRoxgfWEEZlTezpUj1vm5uQ=
|
|
||||||
github.com/gen2brain/shm v0.1.1/go.mod h1:UgIcVtvmOu+aCJpqJX7GOtiN7X2ct+TKLg4RTxwPIUA=
|
|
||||||
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
|
||||||
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
|
|
||||||
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
|
|
||||||
github.com/go-vgo/robotgo v0.110.6 h1:1tOxlmTXYg6F3Xs8IT++331MxY2nZ+Q3B6eW312llbo=
|
|
||||||
github.com/go-vgo/robotgo v0.110.6/go.mod h1:eBUjTHY1HYjzdi1+UWJUbxB+b9gE+l4Ei7vQU/9SnLw=
|
|
||||||
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
|
|
||||||
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
|
||||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
|
||||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
|
||||||
github.com/jezek/xgb v1.1.1 h1:bE/r8ZZtSv7l9gk6nU0mYx51aXrvnyb44892TwSaqS4=
|
|
||||||
github.com/jezek/xgb v1.1.1/go.mod h1:nrhwO0FX/enq75I7Y7G8iN1ubpSGZEiA3v9e9GyRFlk=
|
|
||||||
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
|
||||||
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
|
||||||
github.com/kbinani/screenshot v0.0.0-20250118074034-a3924b7bbc8c h1:1IlzDla/ZATV/FsRn1ETf7ir91PHS2mrd4VMunEtd9k=
|
|
||||||
github.com/kbinani/screenshot v0.0.0-20250118074034-a3924b7bbc8c/go.mod h1:Pmpz2BLf55auQZ67u3rvyI2vAQvNetkK/4zYUmpauZQ=
|
|
||||||
github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683 h1:7UMa6KCCMjZEMDtTVdcGu0B1GmmC7QJKiCCjyTAWQy0=
|
|
||||||
github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683/go.mod h1:ilwx/Dta8jXAgpFYFvSWEMwxmbWXyiUHkd5FwyKhb5k=
|
|
||||||
github.com/lxn/win v0.0.0-20210218163916-a377121e959e h1:H+t6A/QJMbhCSEH5rAuRxh+CtW96g0Or0Fxa9IKr4uc=
|
|
||||||
github.com/lxn/win v0.0.0-20210218163916-a377121e959e/go.mod h1:KxxjdtRkfNoYDCUP5ryK7XJJNTnpC8atvtmTheChOtk=
|
|
||||||
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=
|
|
||||||
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
|
|
||||||
github.com/otiai10/gosseract v2.2.1+incompatible h1:Ry5ltVdpdp4LAa2bMjsSJH34XHVOV7XMi41HtzL8X2I=
|
|
||||||
github.com/otiai10/gosseract v2.2.1+incompatible/go.mod h1:XrzWItCzCpFRZ35n3YtVTgq5bLAhFIkascoRo8G32QE=
|
|
||||||
github.com/otiai10/mint v1.6.3 h1:87qsV/aw1F5as1eH1zS/yqHY85ANKVMgkDrf9rcxbQs=
|
|
||||||
github.com/otiai10/mint v1.6.3/go.mod h1:MJm72SBthJjz8qhefc4z1PYEieWmy8Bku7CjcAqyUSM=
|
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
|
||||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=
|
|
||||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
|
|
||||||
github.com/robotn/xgb v0.0.0-20190912153532-2cb92d044934/go.mod h1:SxQhJskUJ4rleVU44YvnrdvxQr0tKy5SRSigBrCgyyQ=
|
|
||||||
github.com/robotn/xgb v0.10.0 h1:O3kFbIwtwZ3pgLbp1h5slCQ4OpY8BdwugJLrUe6GPIM=
|
|
||||||
github.com/robotn/xgb v0.10.0/go.mod h1:SxQhJskUJ4rleVU44YvnrdvxQr0tKy5SRSigBrCgyyQ=
|
|
||||||
github.com/robotn/xgbutil v0.10.0 h1:gvf7mGQqCWQ68aHRtCxgdewRk+/KAJui6l3MJQQRCKw=
|
|
||||||
github.com/robotn/xgbutil v0.10.0/go.mod h1:svkDXUDQjUiWzLrA0OZgHc4lbOts3C+uRfP6/yjwYnU=
|
|
||||||
github.com/shirou/gopsutil/v4 v4.25.1 h1:QSWkTc+fu9LTAWfkZwZ6j8MSUk4A2LV7rbH0ZqmLjXs=
|
|
||||||
github.com/shirou/gopsutil/v4 v4.25.1/go.mod h1:RoUCUpndaJFtT+2zsZzzmhvbfGoDCJ7nFXKJf8GqJbI=
|
|
||||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
|
||||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
|
||||||
github.com/tailscale/win v0.0.0-20250213223159-5992cb43ca35 h1:wAZbkTZkqDzWsqxPh2qkBd3KvFU7tcxV0BP0Rnhkxog=
|
|
||||||
github.com/tailscale/win v0.0.0-20250213223159-5992cb43ca35/go.mod h1:aMd4yDHLjbOuYP6fMxj1d9ACDQlSWwYztcpybGHCQc8=
|
|
||||||
github.com/tc-hib/winres v0.2.1 h1:YDE0FiP0VmtRaDn7+aaChp1KiF4owBiJa5l964l5ujA=
|
|
||||||
github.com/tc-hib/winres v0.2.1/go.mod h1:C/JaNhH3KBvhNKVbvdlDWkbMDO9H4fKKDaN7/07SSuk=
|
|
||||||
github.com/tklauser/go-sysconf v0.3.14 h1:g5vzr9iPFFz24v2KZXs/pvpvh8/V9Fw6vQK5ZZb78yU=
|
|
||||||
github.com/tklauser/go-sysconf v0.3.14/go.mod h1:1ym4lWMLUOhuBOPGtRcJm7tEGX4SCYNEEEtghGG/8uY=
|
|
||||||
github.com/tklauser/numcpus v0.9.0 h1:lmyCHtANi8aRUgkckBgoDk1nHCux3n2cgkJLXdQGPDo=
|
|
||||||
github.com/tklauser/numcpus v0.9.0/go.mod h1:SN6Nq1O3VychhC1npsWostA+oW+VOQTxZrS604NSRyI=
|
|
||||||
github.com/vcaesar/gops v0.40.0 h1:I+1RCGiV+LkZJUYNzAd373xs0uM2UyeFdZBmow8HfCM=
|
|
||||||
github.com/vcaesar/gops v0.40.0/go.mod h1:3u/USW7JovqUK6i13VOD3qWfvXXd2TIIKE4PYIv4TOM=
|
|
||||||
github.com/vcaesar/imgo v0.40.2 h1:5GWScRLdBCMtO1v2I1bs+ZmDLZFINxYSMZ+mtUw5qPM=
|
|
||||||
github.com/vcaesar/imgo v0.40.2/go.mod h1:MVCl+FxHI2gTgmiHoi0n5xNCbYcfv9SVtdEOUC92+eo=
|
|
||||||
github.com/vcaesar/keycode v0.10.1 h1:0DesGmMAPWpYTCYddOFiCMKCDKgNnwiQa2QXindVUHw=
|
|
||||||
github.com/vcaesar/keycode v0.10.1/go.mod h1:JNlY7xbKsh+LAGfY2j4M3znVrGEm5W1R8s/Uv6BJcfQ=
|
|
||||||
github.com/vcaesar/tt v0.20.1 h1:D/jUeeVCNbq3ad8M7hhtB3J9x5RZ6I1n1eZ0BJp7M+4=
|
|
||||||
github.com/vcaesar/tt v0.20.1/go.mod h1:cH2+AwGAJm19Wa6xvEa+0r+sXDJBT0QgNQey6mwqLeU=
|
|
||||||
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
|
|
||||||
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
|
||||||
golang.org/x/exp v0.0.0-20250215185904-eff6e970281f h1:oFMYAjX0867ZD2jcNiLBrI9BdpmEkvPyi5YrBGXbamg=
|
|
||||||
golang.org/x/exp v0.0.0-20250215185904-eff6e970281f/go.mod h1:BHOTPb3L19zxehTsLoJXVaTktb06DFgmdW6Wb9s8jqk=
|
|
||||||
golang.org/x/image v0.24.0 h1:AN7zRgVsbvmTfNyqIbbOraYL8mSwcKncEj8ofjgzcMQ=
|
|
||||||
golang.org/x/image v0.24.0/go.mod h1:4b/ITuLfqYq1hqZcjofwctIhi7sZh2WaCjvsBNjjya8=
|
|
||||||
golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
|
|
||||||
golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
|
|
||||||
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20201018230417-eeed37f84f13/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
|
||||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
|
|
||||||
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
|
||||||
|
|
@ -1,23 +0,0 @@
|
||||||
<!DOCTYPE html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
||||||
<title>404 - BALLS</title>
|
|
||||||
<style>
|
|
||||||
body {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
height: 100vh;
|
|
||||||
margin: 0;
|
|
||||||
background-color: #f0f0f0;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<h1>Balls found</h1>
|
|
||||||
<span>So not the content you're looking for.</span>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
|
@ -1,162 +0,0 @@
|
||||||
package macro
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"log"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"regexp"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/go-vgo/robotgo"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Step struct {
|
|
||||||
Type string `json:"type"`
|
|
||||||
Key string `json:"key"`
|
|
||||||
Code string `json:"code"`
|
|
||||||
Location int `json:"location"`
|
|
||||||
Direction string `json:"direction"`
|
|
||||||
Value int `json:"value"`
|
|
||||||
}
|
|
||||||
|
|
||||||
var newMacro struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
Steps []Step `json:"steps"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func Save(w http.ResponseWriter, r *http.Request) {
|
|
||||||
body, err := io.ReadAll(r.Body)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Println(string(body))
|
|
||||||
|
|
||||||
err = json.Unmarshal(body, &newMacro)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
stepsJSON, err := json.Marshal(newMacro.Steps)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
err = os.WriteFile("../macros/"+makeValidFilename(newMacro.Name)+".json", stepsJSON, 0644)
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func makeValidFilename(s string) string {
|
|
||||||
// Remove invalid characters
|
|
||||||
re := regexp.MustCompile(`[\/\?\*\>\<\:\\"\|\n]`)
|
|
||||||
s = re.ReplaceAllString(s, "")
|
|
||||||
|
|
||||||
// Replace spaces with underscores
|
|
||||||
s = strings.ReplaceAll(s, " ", "_")
|
|
||||||
|
|
||||||
// Remove special characters
|
|
||||||
re = regexp.MustCompile(`[!@#$%^&\(\)\[\]\{\}\~]`)
|
|
||||||
s = re.ReplaceAllString(s, "")
|
|
||||||
|
|
||||||
// Truncate the string
|
|
||||||
if len(s) > 255 {
|
|
||||||
s = s[:255]
|
|
||||||
}
|
|
||||||
|
|
||||||
return s
|
|
||||||
}
|
|
||||||
|
|
||||||
func List(w http.ResponseWriter, r *http.Request) {
|
|
||||||
log.Println("listing macros")
|
|
||||||
dir := "../macros"
|
|
||||||
files, err := os.ReadDir(dir)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var fileNames []string
|
|
||||||
|
|
||||||
for _, file := range files {
|
|
||||||
filename := filepath.Base(file.Name())
|
|
||||||
filename = strings.TrimSuffix(filename, filepath.Ext(filename))
|
|
||||||
filename = strings.Replace(filename, "_", " ", -1)
|
|
||||||
|
|
||||||
fileNames = append(fileNames, filename)
|
|
||||||
}
|
|
||||||
|
|
||||||
json.NewEncoder(w).Encode(fileNames)
|
|
||||||
}
|
|
||||||
|
|
||||||
func Delete(w http.ResponseWriter, r *http.Request) {}
|
|
||||||
|
|
||||||
func Play(w http.ResponseWriter, r *http.Request) {
|
|
||||||
type MacroRequest struct {
|
|
||||||
Macro string `json:"macro"`
|
|
||||||
}
|
|
||||||
|
|
||||||
var req MacroRequest
|
|
||||||
|
|
||||||
err := json.NewDecoder(r.Body).Decode(&req)
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
macro := req.Macro
|
|
||||||
|
|
||||||
macroFile, err := readMacroFile(fmt.Sprintf("../macros/%s.json", makeValidFilename(macro)))
|
|
||||||
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println(err)
|
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
playMacro(macroFile)
|
|
||||||
// fmt.Println(macroFile)
|
|
||||||
}
|
|
||||||
|
|
||||||
func readMacroFile(filename string) (steps []Step, err error) {
|
|
||||||
|
|
||||||
log.Println(filename)
|
|
||||||
// Let's first read the `config.json` file
|
|
||||||
content, err := os.ReadFile(filename)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal("Error when opening file: ", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Now let's unmarshall the data into `steps`
|
|
||||||
err = json.Unmarshal(content, &steps)
|
|
||||||
if err != nil {
|
|
||||||
log.Fatal("Error during Unmarshal(): ", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return steps, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func playMacro(steps []Step) {
|
|
||||||
for _, step := range steps {
|
|
||||||
// log.Println(step)
|
|
||||||
switch step.Type {
|
|
||||||
case "key":
|
|
||||||
robotgo.KeyToggle(step.Key, step.Direction)
|
|
||||||
// log.Println("Toggling", step.Key, "to", step.Direction)
|
|
||||||
case "delay":
|
|
||||||
time.Sleep(time.Duration(step.Location) * time.Millisecond)
|
|
||||||
// log.Println("Sleeping for", step.Value, "milliseconds")
|
|
||||||
default:
|
|
||||||
log.Println("Unknown step type:", step.Type)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
26
be/main.go
26
be/main.go
|
|
@ -1,26 +0,0 @@
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"log"
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"be/app"
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
apiInit(w, r)
|
|
||||||
})
|
|
||||||
|
|
||||||
log.Println(http.ListenAndServe(":6970", nil))
|
|
||||||
}
|
|
||||||
|
|
||||||
func apiInit(w http.ResponseWriter, r *http.Request) {
|
|
||||||
app.ApiCORS(w, r)
|
|
||||||
|
|
||||||
if r.Method == "GET" {
|
|
||||||
app.ApiGet(w, r)
|
|
||||||
} else if r.Method == "POST" {
|
|
||||||
app.ApiPost(w, r)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1 +0,0 @@
|
||||||
exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1
|
|
||||||
35
build.sh
35
build.sh
|
|
@ -1,35 +0,0 @@
|
||||||
#!/bin/bash
|
|
||||||
|
|
||||||
# Set the name of the build directory
|
|
||||||
BUILD_DIR="Macrame_$(date +'%m%d%H%M%S')"
|
|
||||||
|
|
||||||
# Build the Go application
|
|
||||||
go build -ldflags "-H=windowsgui" -o Macrame.exe main.go
|
|
||||||
|
|
||||||
# Build the frontend
|
|
||||||
cd fe
|
|
||||||
npm run build
|
|
||||||
|
|
||||||
cd ../builds
|
|
||||||
|
|
||||||
# Create the new build directory
|
|
||||||
mkdir $BUILD_DIR
|
|
||||||
mkdir $BUILD_DIR/macros
|
|
||||||
mkdir $BUILD_DIR/panels
|
|
||||||
mkdir $BUILD_DIR/panels/test_panel
|
|
||||||
mkdir $BUILD_DIR/public
|
|
||||||
mkdir $BUILD_DIR/public/assets
|
|
||||||
|
|
||||||
# Move the generated files to the new build directory
|
|
||||||
cp ../Macrame.exe $BUILD_DIR/Macrame.exe
|
|
||||||
cp ../favicon.ico $BUILD_DIR/favicon.ico
|
|
||||||
find ../macros -type f ! -name 'ED-*' -exec cp --parents {} "$BUILD_DIR/macros/" \;
|
|
||||||
cp -r ../panels/test_panel/* $BUILD_DIR/panels/test_panel/
|
|
||||||
mv ../public/* $BUILD_DIR/public/
|
|
||||||
|
|
||||||
# cp ../install.bat $BUILD_DIR/install.bat
|
|
||||||
|
|
||||||
powershell -Command "Compress-Archive -Path $BUILD_DIR/* -DestinationPath $BUILD_DIR.zip -Force"
|
|
||||||
|
|
||||||
# Print the path to the new build directory
|
|
||||||
echo "Build directory: ../$BUILD_DIR"
|
|
||||||
|
|
@ -1,9 +0,0 @@
|
||||||
[*.{js,jsx,mjs,cjs,ts,tsx,mts,cts,vue}]
|
|
||||||
charset = utf-8
|
|
||||||
indent_size = 2
|
|
||||||
indent_style = space
|
|
||||||
insert_final_newline = true
|
|
||||||
trim_trailing_whitespace = true
|
|
||||||
|
|
||||||
end_of_line = lf
|
|
||||||
max_line_length = 100
|
|
||||||
1
fe/.gitattributes
vendored
1
fe/.gitattributes
vendored
|
|
@ -1 +0,0 @@
|
||||||
* text=auto eol=lf
|
|
||||||
30
fe/.gitignore
vendored
30
fe/.gitignore
vendored
|
|
@ -1,30 +0,0 @@
|
||||||
# Logs
|
|
||||||
logs
|
|
||||||
*.log
|
|
||||||
npm-debug.log*
|
|
||||||
yarn-debug.log*
|
|
||||||
yarn-error.log*
|
|
||||||
pnpm-debug.log*
|
|
||||||
lerna-debug.log*
|
|
||||||
|
|
||||||
node_modules
|
|
||||||
.DS_Store
|
|
||||||
dist
|
|
||||||
dist-ssr
|
|
||||||
coverage
|
|
||||||
*.local
|
|
||||||
|
|
||||||
/cypress/videos/
|
|
||||||
/cypress/screenshots/
|
|
||||||
|
|
||||||
# Editor directories and files
|
|
||||||
.vscode/*
|
|
||||||
!.vscode/extensions.json
|
|
||||||
.idea
|
|
||||||
*.suo
|
|
||||||
*.ntvs*
|
|
||||||
*.njsproj
|
|
||||||
*.sln
|
|
||||||
*.sw?
|
|
||||||
|
|
||||||
*.tsbuildinfo
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
|
|
||||||
{
|
|
||||||
"$schema": "https://json.schemastore.org/prettierrc",
|
|
||||||
"semi": false,
|
|
||||||
"singleQuote": true,
|
|
||||||
"printWidth": 100
|
|
||||||
}
|
|
||||||
8
fe/.vscode/extensions.json
vendored
8
fe/.vscode/extensions.json
vendored
|
|
@ -1,8 +0,0 @@
|
||||||
{
|
|
||||||
"recommendations": [
|
|
||||||
"Vue.volar",
|
|
||||||
"dbaeumer.vscode-eslint",
|
|
||||||
"EditorConfig.EditorConfig",
|
|
||||||
"esbenp.prettier-vscode"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
35
fe/README.md
35
fe/README.md
|
|
@ -1,35 +0,0 @@
|
||||||
# fe
|
|
||||||
|
|
||||||
This template should help get you started developing with Vue 3 in Vite.
|
|
||||||
|
|
||||||
## Recommended IDE Setup
|
|
||||||
|
|
||||||
[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur).
|
|
||||||
|
|
||||||
## Customize configuration
|
|
||||||
|
|
||||||
See [Vite Configuration Reference](https://vite.dev/config/).
|
|
||||||
|
|
||||||
## Project Setup
|
|
||||||
|
|
||||||
```sh
|
|
||||||
npm install
|
|
||||||
```
|
|
||||||
|
|
||||||
### Compile and Hot-Reload for Development
|
|
||||||
|
|
||||||
```sh
|
|
||||||
npm run dev
|
|
||||||
```
|
|
||||||
|
|
||||||
### Compile and Minify for Production
|
|
||||||
|
|
||||||
```sh
|
|
||||||
npm run build
|
|
||||||
```
|
|
||||||
|
|
||||||
### Lint with [ESLint](https://eslint.org/)
|
|
||||||
|
|
||||||
```sh
|
|
||||||
npm run lint
|
|
||||||
```
|
|
||||||
|
|
@ -1,4 +0,0 @@
|
||||||
html,
|
|
||||||
body {
|
|
||||||
background-color: white;
|
|
||||||
}
|
|
||||||
|
|
@ -1,19 +0,0 @@
|
||||||
import js from '@eslint/js'
|
|
||||||
import pluginVue from 'eslint-plugin-vue'
|
|
||||||
import skipFormatting from '@vue/eslint-config-prettier/skip-formatting'
|
|
||||||
|
|
||||||
export default [
|
|
||||||
{
|
|
||||||
name: 'app/files-to-lint',
|
|
||||||
files: ['**/*.{js,mjs,jsx,vue}'],
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
name: 'app/files-to-ignore',
|
|
||||||
ignores: ['**/dist/**', '**/dist-ssr/**', '**/coverage/**'],
|
|
||||||
},
|
|
||||||
|
|
||||||
js.configs.recommended,
|
|
||||||
...pluginVue.configs['flat/essential'],
|
|
||||||
skipFormatting,
|
|
||||||
]
|
|
||||||
|
|
@ -1,20 +0,0 @@
|
||||||
<!doctype html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<link rel="icon" type="image/svg+xml" href="mcrm-icon.svg" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
||||||
<meta name="color-scheme" content="dark" />
|
|
||||||
<link rel="preconnect" href="https://fonts.bunny.net" />
|
|
||||||
<link
|
|
||||||
href="https://fonts.bunny.net/css?family=fira-code:300,500,700|roboto:100,300,700"
|
|
||||||
rel="stylesheet"
|
|
||||||
/>
|
|
||||||
<title>Macrame</title>
|
|
||||||
<script src="config.js"></script>
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="app"></div>
|
|
||||||
<script type="module" src="/src/main.js"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
{
|
|
||||||
"compilerOptions": {
|
|
||||||
"paths": {
|
|
||||||
"@/*": ["./src/*"]
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"exclude": ["node_modules", "dist"]
|
|
||||||
}
|
|
||||||
5334
fe/package-lock.json
generated
5334
fe/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -1,37 +0,0 @@
|
||||||
{
|
|
||||||
"name": "fe",
|
|
||||||
"version": "0.0.0",
|
|
||||||
"private": true,
|
|
||||||
"type": "module",
|
|
||||||
"scripts": {
|
|
||||||
"dev": "vite --host",
|
|
||||||
"build": "vite build --emptyOutDir",
|
|
||||||
"preview": "vite preview",
|
|
||||||
"lint": "eslint . --fix",
|
|
||||||
"format": "prettier --write src/"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"@tabler/icons-vue": "^3.30.0",
|
|
||||||
"@tailwindcss/vite": "^4.0.9",
|
|
||||||
"vue": "^3.5.13",
|
|
||||||
"vue-router": "^4.5.0"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@basitcodeenv/vue3-device-detect": "^1.0.3",
|
|
||||||
"@eslint/js": "^9.20.0",
|
|
||||||
"@vitejs/plugin-vue": "^5.2.1",
|
|
||||||
"@vue/eslint-config-prettier": "^10.2.0",
|
|
||||||
"axios": "^1.8.3",
|
|
||||||
"crypto-js": "^4.2.0",
|
|
||||||
"eslint": "^9.20.1",
|
|
||||||
"eslint-plugin-vue": "^9.32.0",
|
|
||||||
"pinia": "^3.0.1",
|
|
||||||
"prettier": "^3.5.1",
|
|
||||||
"qrcode": "^1.5.4",
|
|
||||||
"sass-embedded": "^1.85.1",
|
|
||||||
"tailwindcss": "^4.0.9",
|
|
||||||
"uuid": "^11.1.0",
|
|
||||||
"vite": "^6.1.0",
|
|
||||||
"vite-plugin-vue-devtools": "^7.7.2"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,14 +0,0 @@
|
||||||
<!doctype html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
||||||
<title>Vite + Vue</title>
|
|
||||||
<script type="module" crossorigin src="/assets/index-CNkZ911J.js"></script>
|
|
||||||
<link rel="stylesheet" crossorigin href="/assets/index-zqIqfzzx.css">
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="app"></div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
|
|
@ -1,98 +0,0 @@
|
||||||
<!--
|
|
||||||
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>
|
|
||||||
|
|
@ -1,34 +0,0 @@
|
||||||
<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>
|
|
||||||
|
Before Width: | Height: | Size: 1.8 KiB |
|
|
@ -1,60 +0,0 @@
|
||||||
<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>
|
|
||||||
|
Before Width: | Height: | Size: 3.5 KiB |
|
|
@ -1,23 +0,0 @@
|
||||||
<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>
|
|
||||||
|
Before Width: | Height: | Size: 1.7 KiB |
|
|
@ -1,41 +0,0 @@
|
||||||
<?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>
|
|
||||||
|
Before Width: | Height: | Size: 3.1 KiB |
|
|
@ -1,38 +0,0 @@
|
||||||
/*
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
@ -1,68 +0,0 @@
|
||||||
/*
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
@ -1,49 +0,0 @@
|
||||||
/*
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,49 +0,0 @@
|
||||||
/*
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,123 +0,0 @@
|
||||||
/*
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,56 +0,0 @@
|
||||||
/*
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,44 +0,0 @@
|
||||||
/*
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
@ -1,119 +0,0 @@
|
||||||
<!--
|
|
||||||
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>
|
|
||||||
|
|
@ -1,100 +0,0 @@
|
||||||
<!--
|
|
||||||
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>
|
|
||||||
|
|
@ -1,189 +0,0 @@
|
||||||
<!--
|
|
||||||
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>
|
|
||||||
|
|
@ -1,36 +0,0 @@
|
||||||
<!--
|
|
||||||
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>
|
|
||||||
|
|
@ -1,103 +0,0 @@
|
||||||
<!--
|
|
||||||
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>
|
|
||||||
|
|
@ -1,132 +0,0 @@
|
||||||
<!--
|
|
||||||
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>
|
|
||||||
|
|
@ -1,59 +0,0 @@
|
||||||
<!--
|
|
||||||
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>
|
|
||||||
|
|
@ -1,156 +0,0 @@
|
||||||
<!--
|
|
||||||
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>
|
|
||||||
|
|
@ -1,107 +0,0 @@
|
||||||
<!--
|
|
||||||
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>
|
|
||||||
|
|
@ -1,154 +0,0 @@
|
||||||
<!--
|
|
||||||
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>
|
|
||||||
|
|
@ -1,225 +0,0 @@
|
||||||
<!--
|
|
||||||
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>
|
|
||||||
|
|
@ -1,254 +0,0 @@
|
||||||
<!--
|
|
||||||
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>
|
|
||||||
|
|
@ -1,176 +0,0 @@
|
||||||
<!--
|
|
||||||
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>
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
<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>
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
<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>
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
<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>
|
|
||||||
|
|
@ -1,7 +0,0 @@
|
||||||
<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>
|
|
||||||
|
|
@ -1,19 +0,0 @@
|
||||||
<!-- 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>
|
|
||||||
|
|
@ -1,164 +0,0 @@
|
||||||
<!--
|
|
||||||
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>
|
|
||||||
|
|
@ -1,106 +0,0 @@
|
||||||
<!--
|
|
||||||
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>
|
|
||||||
|
|
@ -1,78 +0,0 @@
|
||||||
<!--
|
|
||||||
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>
|
|
||||||
|
|
@ -1,56 +0,0 @@
|
||||||
<!--
|
|
||||||
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>
|
|
||||||
'
|
|
||||||
|
|
@ -1,77 +0,0 @@
|
||||||
<!--
|
|
||||||
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>
|
|
||||||
|
|
@ -1,124 +0,0 @@
|
||||||
<!--
|
|
||||||
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>
|
|
||||||
|
|
@ -1,90 +0,0 @@
|
||||||
<!--
|
|
||||||
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>
|
|
||||||
|
|
@ -1,155 +0,0 @@
|
||||||
<!--
|
|
||||||
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>
|
|
||||||
|
|
@ -1,130 +0,0 @@
|
||||||
<!--
|
|
||||||
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>
|
|
||||||
|
|
@ -1,80 +0,0 @@
|
||||||
<!--
|
|
||||||
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>
|
|
||||||
|
|
@ -1,142 +0,0 @@
|
||||||
<!--
|
|
||||||
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>
|
|
||||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue