17
.air.toml
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
# 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
|
||||
14
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
.env
|
||||
config.js
|
||||
|
||||
devices/*
|
||||
tmp/
|
||||
log.txt
|
||||
|
||||
Macrame.exe
|
||||
|
||||
public
|
||||
macros/*
|
||||
builds
|
||||
node_modules
|
||||
ToDo.md
|
||||
47
add-gpl-header.sh
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
#!/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
Normal file
|
|
@ -0,0 +1,137 @@
|
|||
/*
|
||||
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
Normal file
|
|
@ -0,0 +1,329 @@
|
|||
/*
|
||||
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
|
||||
}
|
||||
123
app/helper/api-helper.go
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
/*
|
||||
Macrame is a program that enables the user to create keyboard macros and button panels.
|
||||
The macros are saved as simple JSON files and can be linked to the button panels. The panels can
|
||||
be created with HTML and CSS.
|
||||
|
||||
Copyright (C) 2025 Jesse Malotaux
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
44
app/helper/browser-helper.go
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
/*
|
||||
Macrame is a program that enables the user to create keyboard macros and button panels.
|
||||
The macros are saved as simple JSON files and can be linked to the button panels. The panels can
|
||||
be created with HTML and CSS.
|
||||
|
||||
Copyright (C) 2025 Jesse Malotaux
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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
|
||||
}
|
||||
66
app/helper/device-helper.go
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
/*
|
||||
Macrame is a program that enables the user to create keyboard macros and button panels.
|
||||
The macros are saved as simple JSON files and can be linked to the button panels. The panels can
|
||||
be created with HTML and CSS.
|
||||
|
||||
Copyright (C) 2025 Jesse Malotaux
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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
|
||||
}
|
||||
135
app/helper/encrypt-helper.go
Normal file
|
|
@ -0,0 +1,135 @@
|
|||
/*
|
||||
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
|
||||
}
|
||||
128
app/helper/env-helper.go
Normal file
|
|
@ -0,0 +1,128 @@
|
|||
/*
|
||||
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
|
||||
}
|
||||
114
app/helper/macro-helper.go
Normal file
|
|
@ -0,0 +1,114 @@
|
|||
/*
|
||||
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 ""
|
||||
}
|
||||
101
app/helper/translation-helper.go
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
/*
|
||||
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
Normal file
|
|
@ -0,0 +1,45 @@
|
|||
/*
|
||||
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
Normal file
|
|
@ -0,0 +1,229 @@
|
|||
/*
|
||||
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
Normal file
|
|
@ -0,0 +1,176 @@
|
|||
/*
|
||||
Macrame is a program that enables the user to create keyboard macros and button panels.
|
||||
The macros are saved as simple JSON files and can be linked to the button panels. The panels can
|
||||
be created with HTML and CSS.
|
||||
|
||||
Copyright (C) 2025 Jesse Malotaux
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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)
|
||||
}
|
||||
66
app/structs/api-struct.go
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
/*
|
||||
Macrame is a program that enables the user to create keyboard macros and button panels.
|
||||
The macros are saved as simple JSON files and can be linked to the button panels. The panels can
|
||||
be created with HTML and CSS.
|
||||
|
||||
Copyright (C) 2025 Jesse Malotaux
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
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",
|
||||
},
|
||||
}
|
||||
52
app/structs/device-struct.go
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
/*
|
||||
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"`
|
||||
}
|
||||
58
app/structs/macro-struct.go
Normal file
|
|
@ -0,0 +1,58 @@
|
|||
/*
|
||||
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"`
|
||||
}
|
||||
65
app/structs/panel-struct.go
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
/*
|
||||
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"`
|
||||
}
|
||||
64
app/systray.go
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
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
|
||||
}
|
||||
52
be/.air.toml
|
|
@ -1,52 +0,0 @@
|
|||
root = "."
|
||||
testdata_dir = "testdata"
|
||||
tmp_dir = "tmp"
|
||||
|
||||
[build]
|
||||
args_bin = []
|
||||
bin = "tmp\\main.exe"
|
||||
cmd = "go build -o ./tmp/main.exe ."
|
||||
delay = 1000
|
||||
exclude_dir = ["assets", "tmp", "vendor", "testdata"]
|
||||
exclude_file = []
|
||||
exclude_regex = ["_test.go"]
|
||||
exclude_unchanged = false
|
||||
follow_symlink = false
|
||||
full_bin = ""
|
||||
include_dir = []
|
||||
include_ext = ["go", "tpl", "tmpl", "html"]
|
||||
include_file = []
|
||||
kill_delay = "0s"
|
||||
log = "build-errors.log"
|
||||
poll = false
|
||||
poll_interval = 0
|
||||
post_cmd = []
|
||||
pre_cmd = []
|
||||
rerun = false
|
||||
rerun_delay = 500
|
||||
send_interrupt = false
|
||||
stop_on_error = false
|
||||
|
||||
[color]
|
||||
app = ""
|
||||
build = "yellow"
|
||||
main = "magenta"
|
||||
runner = "green"
|
||||
watcher = "cyan"
|
||||
|
||||
[log]
|
||||
main_only = false
|
||||
silent = false
|
||||
time = false
|
||||
|
||||
[misc]
|
||||
clean_on_exit = false
|
||||
|
||||
[proxy]
|
||||
app_port = 0
|
||||
enabled = false
|
||||
proxy_port = 0
|
||||
|
||||
[screen]
|
||||
clear_on_rebuild = false
|
||||
keep_scroll = true
|
||||
|
|
@ -1,3 +0,0 @@
|
|||
module example.com/m
|
||||
|
||||
go 1.24.0
|
||||
|
|
@ -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>
|
||||
38
be/main.go
|
|
@ -1,38 +0,0 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"mime"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
func main() {
|
||||
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
// log.Println(r.URL.Path)
|
||||
|
||||
file := "../public" + r.URL.Path
|
||||
|
||||
if r.URL.Path == "/" {
|
||||
file = "../public/index.html"
|
||||
// } else if strings.HasSuffix(r.URL.Path, ".js") {
|
||||
// log.Println("js")
|
||||
// w.Header().Set("Content-Type", "application/javascript")
|
||||
// } else if strings.HasSuffix(r.URL.Path, ".css") {
|
||||
// log.Println("css")
|
||||
// w.Header().Set("Content-Type", "text/css")
|
||||
// }
|
||||
} else {
|
||||
contentType := mime.TypeByExtension(filepath.Ext(file))
|
||||
if contentType != "" {
|
||||
w.Header().Set("Content-Type", contentType)
|
||||
}
|
||||
}
|
||||
|
||||
log.Println(file)
|
||||
log.Println("-------------")
|
||||
http.ServeFile(w, r, file)
|
||||
})
|
||||
|
||||
log.Fatal(http.ListenAndServe(":6970", nil))
|
||||
}
|
||||
|
|
@ -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 1
|
||||
BIN
be/tmp/main.exe
35
build.sh
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
#!/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"
|
||||
BIN
favicon.ico
Normal file
|
After Width: | Height: | Size: 191 KiB |
|
|
@ -1,4 +0,0 @@
|
|||
html,
|
||||
body {
|
||||
background-color: white;
|
||||
}
|
||||
BIN
fe/favicon.ico
|
Before Width: | Height: | Size: 4.2 KiB |
|
|
@ -2,9 +2,16 @@
|
|||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||
<link rel="icon" type="image/svg+xml" href="mcrm-icon.svg" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>Vite + Vue</title>
|
||||
<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>
|
||||
|
|
|
|||
60
fe/mcrm-icon.svg
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
x="0px" y="0px"
|
||||
viewBox="0 0 140 80"
|
||||
style="enable-background:new 0 0 140 80;"
|
||||
xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:url(#SVGID_1_);}
|
||||
.st1{fill:url(#SVGID_2_);}
|
||||
.st2{fill:url(#SVGID_3_);}
|
||||
.st3{fill:url(#SVGID_4_);}
|
||||
.st4{fill:url(#SVGID_5_);}
|
||||
.st5{fill:url(#SVGID_6_);}
|
||||
.st6{fill:url(#SVGID_7_);}
|
||||
</style>
|
||||
<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="-9.094947e-13" y1="28.05" x2="140" y2="28.05">
|
||||
<stop offset="0" style="stop-color:#FFB900"/>
|
||||
<stop offset="1" style="stop-color:#00BCFF"/>
|
||||
</linearGradient>
|
||||
<path class="st0" d="M95.5,18.3l-0.2-0.1C95.2,18.1,95,18,94.8,18c-0.3,0-0.5,0.1-0.7,0.3L82.8,29.6l8.5,8.5l12-12L95.5,18.3z"/>
|
||||
<linearGradient id="SVGID_2_" gradientUnits="userSpaceOnUse" x1="-9.094947e-13" y1="28" x2="140" y2="28">
|
||||
<stop offset="0" style="stop-color:#FFB900"/>
|
||||
<stop offset="1" style="stop-color:#00BCFF"/>
|
||||
</linearGradient>
|
||||
<path class="st1" d="M57.3,29.5L46,18.3c-0.2-0.2-0.5-0.3-0.7-0.3s-0.4,0-0.5,0.1l-0.2,0.1L36.8,26l12,12L57.3,29.5z"/>
|
||||
<linearGradient id="SVGID_3_" gradientUnits="userSpaceOnUse" x1="-9.094947e-13" y1="65.25" x2="140" y2="65.25">
|
||||
<stop offset="0" style="stop-color:#FFB900"/>
|
||||
<stop offset="1" style="stop-color:#00BCFF"/>
|
||||
</linearGradient>
|
||||
<path class="st2" d="M94.7,67l-14-14l-8.5,8.5l11.3,11.3c1,1,2.1,1.8,3.2,2.5c2.5,1.5,5.3,2.2,8.1,2.2s5.6-0.7,8.1-2.2L94.7,67
|
||||
L94.7,67z"/>
|
||||
<linearGradient id="SVGID_4_" gradientUnits="userSpaceOnUse" x1="-9.094947e-13" y1="32.9162" x2="140" y2="32.9162">
|
||||
<stop offset="0" style="stop-color:#FFB900"/>
|
||||
<stop offset="1" style="stop-color:#00BCFF"/>
|
||||
</linearGradient>
|
||||
<path class="st3" d="M114,15.5l-7.8-7.8c-0.2-0.2-0.5-0.5-0.7-0.7c-5.3-4.6-12.8-5.2-18.7-1.8c-1.1,0.7-2.2,1.5-3.2,2.5L72.2,19
|
||||
l2.6,2.6l5.9,5.9L92,16.2c0.8-0.8,1.8-1.1,2.8-1.1c0.7,0,1.4,0.2,2,0.5l0.1-0.1l8.5,8.5l13.4,13.4c0.8,0.8,1.1,1.8,1.1,2.8
|
||||
s-0.4,2.1-1.1,2.8l-11.3,11.3l5,5l3.5,3.5l11.3-11.3c3.1-3.1,4.7-7.2,4.7-11.3c0-4.1-1.5-8.2-4.6-11.3L114,15.5z"/>
|
||||
<linearGradient id="SVGID_5_" gradientUnits="userSpaceOnUse" x1="-9.094947e-13" y1="38.2163" x2="140" y2="38.2163">
|
||||
<stop offset="0" style="stop-color:#FFB900"/>
|
||||
<stop offset="1" style="stop-color:#00BCFF"/>
|
||||
</linearGradient>
|
||||
<path class="st4" d="M105.4,56.5l-3.5-3.5l-4.5-4.5L81.2,32.2l-8.5-8.5l-2.6-2.6L56.6,7.7c-1-1-2.1-1.8-3.2-2.5
|
||||
C47.6,1.8,40,2.4,34.8,7c-0.3,0.2-0.5,0.4-0.7,0.7l-7.8,7.8L12.8,28.9C9.7,32,8.1,36.1,8.1,40.2c0,4.1,1.6,8.2,4.7,11.3l11.3,11.3
|
||||
l3.5-3.5l5-5L21.3,43c-0.8-0.8-1.1-1.8-1.1-2.8s0.4-2.1,1.1-2.8L34.7,24l8.5-8.5l0.1,0.1c1.5-0.9,3.6-0.7,4.8,0.6l11.3,11.3l2.1,2.1
|
||||
l8.5,8.5l2.1,2.1l8.5,8.5l16.2,16.2L97,65l8.4,8.4c0.3-0.2,0.5-0.4,0.7-0.7l7.8-7.8l-3.5-3.4L105.4,56.5z"/>
|
||||
<linearGradient id="SVGID_6_" gradientUnits="userSpaceOnUse" x1="-9.094947e-13" y1="59.85" x2="140" y2="59.85">
|
||||
<stop offset="0" style="stop-color:#FFB900"/>
|
||||
<stop offset="1" style="stop-color:#00BCFF"/>
|
||||
</linearGradient>
|
||||
<path class="st5" d="M70.1,42.3l-8.5,8.5L45.4,67l-0.1,0.1L40.4,72l-3.2,3.2c2.5,1.5,5.3,2.2,8.1,2.2s5.6-0.8,8.1-2.2
|
||||
c1.1-0.7,2.2-1.5,3.2-2.5L70,59.3l8.5-8.5L70.1,42.3z"/>
|
||||
<linearGradient id="SVGID_7_" gradientUnits="userSpaceOnUse" x1="-9.094947e-13" y1="52.6" x2="140" y2="52.6">
|
||||
<stop offset="0" style="stop-color:#FFB900"/>
|
||||
<stop offset="1" style="stop-color:#00BCFF"/>
|
||||
</linearGradient>
|
||||
<path class="st6" d="M43.1,65.1l0.1-0.1l16.2-16.2l8.5-8.5l-8.4-8.6L51,40.2L38.2,53l-3.5,3.5l-5,5L26.2,65l7.8,7.8
|
||||
c0.2,0.2,0.5,0.5,0.7,0.7l3.5-3.5L43.1,65.1z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.5 KiB |
2314
fe/package-lock.json
generated
|
|
@ -4,24 +4,33 @@
|
|||
"private": true,
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
"build": "vite build",
|
||||
"dev": "vite --host",
|
||||
"build": "vite build --emptyOutDir",
|
||||
"preview": "vite preview",
|
||||
"lint": "eslint . --fix",
|
||||
"format": "prettier --write src/"
|
||||
},
|
||||
"dependencies": {
|
||||
"pinia": "^3.0.1",
|
||||
"@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,13 +1,98 @@
|
|||
<!--
|
||||
Macrame is a program that enables the user to create keyboard macros and button panels.
|
||||
The macros are saved as simple JSON files and can be linked to the button panels. The panels can
|
||||
be created with HTML and CSS.
|
||||
|
||||
Copyright (C) 2025 Jesse Malotaux
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<RouterView />
|
||||
<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 { RouterView } from 'vue-router'
|
||||
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'
|
||||
|
||||
console.log('app.vue loaded')
|
||||
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 lang="scss" scoped></style>
|
||||
<style scoped>
|
||||
@reference "@/assets/main.css";
|
||||
|
||||
.app-background {
|
||||
@apply fixed
|
||||
inset-0
|
||||
size-full
|
||||
overflow-hidden
|
||||
pointer-events-none
|
||||
opacity-40
|
||||
z-[-1];
|
||||
|
||||
img {
|
||||
@apply absolute
|
||||
size-full
|
||||
object-cover;
|
||||
}
|
||||
|
||||
.logo {
|
||||
@apply absolute
|
||||
top-[10%]
|
||||
left-[10%]
|
||||
scale-[1.8]
|
||||
opacity-35
|
||||
mix-blend-overlay;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
|||
34
fe/src/assets/img/Macrame-Logo-duo.svg
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 140 80"
|
||||
width="140"
|
||||
height="80"
|
||||
>
|
||||
<g>
|
||||
<path style="fill:#FFB900;" d="M95.5,18.3l-0.2-0.1C95.2,18.1,95,18,94.8,18c-0.3,0-0.5,0.1-0.7,0.3L82.8,29.6l8.5,8.5l12-12
|
||||
L95.5,18.3z"/>
|
||||
<path style="fill:#00BCFF;" d="M46,18.3c-0.2-0.2-0.5-0.3-0.7-0.3c-0.2,0-0.4,0-0.5,0.1l-0.2,0.1l-7.8,7.8h0l12,12l8.5-8.5L46,18.3
|
||||
z"/>
|
||||
<path style="fill:#00BCFF;" d="M94.8,67.1L94.7,67l-14-14l-8.5,8.5l11.3,11.3c1,1,2.1,1.8,3.2,2.5c2.5,1.5,5.3,2.2,8.1,2.2
|
||||
c2.8,0,5.6-0.7,8.1-2.2L94.8,67.1z"/>
|
||||
<path style="fill:#00BCFF;" d="M127.4,28.9l-13.4-13.4l-7.8-7.8c-0.2-0.2-0.5-0.5-0.7-0.7c-5.3-4.6-12.8-5.2-18.7-1.8
|
||||
c-1.1,0.7-2.2,1.5-3.2,2.5L72.2,19l2.6,2.6l5.9,5.9L92,16.2c0.8-0.8,1.8-1.1,2.8-1.1c0.7,0,1.4,0.2,2,0.5l0.1-0.1l8.5,8.5
|
||||
l13.4,13.4c0.8,0.8,1.1,1.8,1.1,2.8c0,1-0.4,2.1-1.1,2.8l-11.3,11.3l5,5l3.5,3.5l11.3-11.3v0c3.1-3.1,4.7-7.2,4.7-11.3
|
||||
C132,36.1,130.5,32,127.4,28.9z"/>
|
||||
<g>
|
||||
<path style="fill:#FFB900;" d="M110.4,61.5l-5-5l0,0l-3.5-3.5l-4.5-4.5L81.2,32.2l-8.5-8.5l-2.6-2.6L56.6,7.7
|
||||
c-1-1-2.1-1.8-3.2-2.5C47.6,1.8,40,2.4,34.8,7c-0.3,0.2-0.5,0.4-0.7,0.7l-7.8,7.8L12.8,28.9v0C9.7,32,8.1,36.1,8.1,40.2
|
||||
c0,4.1,1.6,8.2,4.7,11.3l11.3,11.3l3.5-3.5l5-5L21.3,43c-0.8-0.8-1.1-1.8-1.1-2.8c0-1,0.4-2.1,1.1-2.8l13.4-13.4l8.5-8.5l0.1,0.1
|
||||
c1.5-0.9,3.6-0.7,4.8,0.6l11.3,11.3l0,0l2.1,2.1l8.5,8.5l2.1,2.1l0,0l8.5,8.5l16.2,16.2l0,0L97,65l8.4,8.4
|
||||
c0.3-0.2,0.5-0.4,0.7-0.7l7.8-7.8L110.4,61.5z"/>
|
||||
</g>
|
||||
<g>
|
||||
<path style="fill:#FFB900;" d="M70.1,42.3L70.1,42.3l-8.5,8.5L45.4,67l-0.1,0.1L40.4,72l-3.2,3.2c2.5,1.5,5.3,2.2,8.1,2.2
|
||||
c2.8,0,5.6-0.8,8.1-2.2c1.1-0.7,2.2-1.5,3.2-2.5l13.4-13.4l8.5-8.5L70.1,42.3z"/>
|
||||
</g>
|
||||
<g>
|
||||
<path style="fill:#00BCFF;" d="M59.5,31.7L51,40.2L38.2,53l-3.5,3.5l0,0l-5,5L26.2,65l7.8,7.8c0.2,0.2,0.5,0.5,0.7,0.7l3.5-3.5
|
||||
l4.9-4.9l0.1-0.1l16.2-16.2l8.5-8.5L59.5,31.7z"/>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.8 KiB |
60
fe/src/assets/img/Macrame-Logo-gradient.svg
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
x="0px" y="0px"
|
||||
viewBox="0 0 140 80"
|
||||
style="enable-background:new 0 0 140 80;"
|
||||
xml:space="preserve">
|
||||
<style type="text/css">
|
||||
.st0{fill:url(#SVGID_1_);}
|
||||
.st1{fill:url(#SVGID_2_);}
|
||||
.st2{fill:url(#SVGID_3_);}
|
||||
.st3{fill:url(#SVGID_4_);}
|
||||
.st4{fill:url(#SVGID_5_);}
|
||||
.st5{fill:url(#SVGID_6_);}
|
||||
.st6{fill:url(#SVGID_7_);}
|
||||
</style>
|
||||
<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="-9.094947e-13" y1="28.05" x2="140" y2="28.05">
|
||||
<stop offset="0" style="stop-color:#FFB900"/>
|
||||
<stop offset="1" style="stop-color:#00BCFF"/>
|
||||
</linearGradient>
|
||||
<path class="st0" d="M95.5,18.3l-0.2-0.1C95.2,18.1,95,18,94.8,18c-0.3,0-0.5,0.1-0.7,0.3L82.8,29.6l8.5,8.5l12-12L95.5,18.3z"/>
|
||||
<linearGradient id="SVGID_2_" gradientUnits="userSpaceOnUse" x1="-9.094947e-13" y1="28" x2="140" y2="28">
|
||||
<stop offset="0" style="stop-color:#FFB900"/>
|
||||
<stop offset="1" style="stop-color:#00BCFF"/>
|
||||
</linearGradient>
|
||||
<path class="st1" d="M57.3,29.5L46,18.3c-0.2-0.2-0.5-0.3-0.7-0.3s-0.4,0-0.5,0.1l-0.2,0.1L36.8,26l12,12L57.3,29.5z"/>
|
||||
<linearGradient id="SVGID_3_" gradientUnits="userSpaceOnUse" x1="-9.094947e-13" y1="65.25" x2="140" y2="65.25">
|
||||
<stop offset="0" style="stop-color:#FFB900"/>
|
||||
<stop offset="1" style="stop-color:#00BCFF"/>
|
||||
</linearGradient>
|
||||
<path class="st2" d="M94.7,67l-14-14l-8.5,8.5l11.3,11.3c1,1,2.1,1.8,3.2,2.5c2.5,1.5,5.3,2.2,8.1,2.2s5.6-0.7,8.1-2.2L94.7,67
|
||||
L94.7,67z"/>
|
||||
<linearGradient id="SVGID_4_" gradientUnits="userSpaceOnUse" x1="-9.094947e-13" y1="32.9162" x2="140" y2="32.9162">
|
||||
<stop offset="0" style="stop-color:#FFB900"/>
|
||||
<stop offset="1" style="stop-color:#00BCFF"/>
|
||||
</linearGradient>
|
||||
<path class="st3" d="M114,15.5l-7.8-7.8c-0.2-0.2-0.5-0.5-0.7-0.7c-5.3-4.6-12.8-5.2-18.7-1.8c-1.1,0.7-2.2,1.5-3.2,2.5L72.2,19
|
||||
l2.6,2.6l5.9,5.9L92,16.2c0.8-0.8,1.8-1.1,2.8-1.1c0.7,0,1.4,0.2,2,0.5l0.1-0.1l8.5,8.5l13.4,13.4c0.8,0.8,1.1,1.8,1.1,2.8
|
||||
s-0.4,2.1-1.1,2.8l-11.3,11.3l5,5l3.5,3.5l11.3-11.3c3.1-3.1,4.7-7.2,4.7-11.3c0-4.1-1.5-8.2-4.6-11.3L114,15.5z"/>
|
||||
<linearGradient id="SVGID_5_" gradientUnits="userSpaceOnUse" x1="-9.094947e-13" y1="38.2163" x2="140" y2="38.2163">
|
||||
<stop offset="0" style="stop-color:#FFB900"/>
|
||||
<stop offset="1" style="stop-color:#00BCFF"/>
|
||||
</linearGradient>
|
||||
<path class="st4" d="M105.4,56.5l-3.5-3.5l-4.5-4.5L81.2,32.2l-8.5-8.5l-2.6-2.6L56.6,7.7c-1-1-2.1-1.8-3.2-2.5
|
||||
C47.6,1.8,40,2.4,34.8,7c-0.3,0.2-0.5,0.4-0.7,0.7l-7.8,7.8L12.8,28.9C9.7,32,8.1,36.1,8.1,40.2c0,4.1,1.6,8.2,4.7,11.3l11.3,11.3
|
||||
l3.5-3.5l5-5L21.3,43c-0.8-0.8-1.1-1.8-1.1-2.8s0.4-2.1,1.1-2.8L34.7,24l8.5-8.5l0.1,0.1c1.5-0.9,3.6-0.7,4.8,0.6l11.3,11.3l2.1,2.1
|
||||
l8.5,8.5l2.1,2.1l8.5,8.5l16.2,16.2L97,65l8.4,8.4c0.3-0.2,0.5-0.4,0.7-0.7l7.8-7.8l-3.5-3.4L105.4,56.5z"/>
|
||||
<linearGradient id="SVGID_6_" gradientUnits="userSpaceOnUse" x1="-9.094947e-13" y1="59.85" x2="140" y2="59.85">
|
||||
<stop offset="0" style="stop-color:#FFB900"/>
|
||||
<stop offset="1" style="stop-color:#00BCFF"/>
|
||||
</linearGradient>
|
||||
<path class="st5" d="M70.1,42.3l-8.5,8.5L45.4,67l-0.1,0.1L40.4,72l-3.2,3.2c2.5,1.5,5.3,2.2,8.1,2.2s5.6-0.8,8.1-2.2
|
||||
c1.1-0.7,2.2-1.5,3.2-2.5L70,59.3l8.5-8.5L70.1,42.3z"/>
|
||||
<linearGradient id="SVGID_7_" gradientUnits="userSpaceOnUse" x1="-9.094947e-13" y1="52.6" x2="140" y2="52.6">
|
||||
<stop offset="0" style="stop-color:#FFB900"/>
|
||||
<stop offset="1" style="stop-color:#00BCFF"/>
|
||||
</linearGradient>
|
||||
<path class="st6" d="M43.1,65.1l0.1-0.1l16.2-16.2l8.5-8.5l-8.4-8.6L51,40.2L38.2,53l-3.5,3.5l-5,5L26.2,65l7.8,7.8
|
||||
c0.2,0.2,0.5,0.5,0.7,0.7l3.5-3.5L43.1,65.1z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.5 KiB |
23
fe/src/assets/img/Macrame-Logo-white.svg
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
x="0px" y="0px"
|
||||
viewBox="0 0 140 80"
|
||||
style="enable-background:new 0 0 140 80;"
|
||||
xml:space="preserve">
|
||||
<path style="fill:#fff" d="M95.5,18.3l-0.2-0.1C95.2,18.1,95,18,94.8,18c-0.3,0-0.5,0.1-0.7,0.3L82.8,29.6l8.5,8.5l12-12L95.5,18.3z"/>
|
||||
<path style="fill:#fff" d="M57.3,29.5L46,18.3c-0.2-0.2-0.5-0.3-0.7-0.3s-0.4,0-0.5,0.1l-0.2,0.1L36.8,26l12,12L57.3,29.5z"/>
|
||||
<path style="fill:#fff" d="M94.7,67l-14-14l-8.5,8.5l11.3,11.3c1,1,2.1,1.8,3.2,2.5c2.5,1.5,5.3,2.2,8.1,2.2s5.6-0.7,8.1-2.2L94.7,67
|
||||
L94.7,67z"/>
|
||||
<path style="fill:#fff" d="M114,15.5l-7.8-7.8c-0.2-0.2-0.5-0.5-0.7-0.7c-5.3-4.6-12.8-5.2-18.7-1.8c-1.1,0.7-2.2,1.5-3.2,2.5L72.2,19
|
||||
l2.6,2.6l5.9,5.9L92,16.2c0.8-0.8,1.8-1.1,2.8-1.1c0.7,0,1.4,0.2,2,0.5l0.1-0.1l8.5,8.5l13.4,13.4c0.8,0.8,1.1,1.8,1.1,2.8
|
||||
s-0.4,2.1-1.1,2.8l-11.3,11.3l5,5l3.5,3.5l11.3-11.3c3.1-3.1,4.7-7.2,4.7-11.3c0-4.1-1.5-8.2-4.6-11.3L114,15.5z"/>
|
||||
<path style="fill:#fff" d="M105.4,56.5l-3.5-3.5l-4.5-4.5L81.2,32.2l-8.5-8.5l-2.6-2.6L56.6,7.7c-1-1-2.1-1.8-3.2-2.5
|
||||
C47.6,1.8,40,2.4,34.8,7c-0.3,0.2-0.5,0.4-0.7,0.7l-7.8,7.8L12.8,28.9C9.7,32,8.1,36.1,8.1,40.2c0,4.1,1.6,8.2,4.7,11.3l11.3,11.3
|
||||
l3.5-3.5l5-5L21.3,43c-0.8-0.8-1.1-1.8-1.1-2.8s0.4-2.1,1.1-2.8L34.7,24l8.5-8.5l0.1,0.1c1.5-0.9,3.6-0.7,4.8,0.6l11.3,11.3l2.1,2.1
|
||||
l8.5,8.5l2.1,2.1l8.5,8.5l16.2,16.2L97,65l8.4,8.4c0.3-0.2,0.5-0.4,0.7-0.7l7.8-7.8l-3.5-3.4L105.4,56.5z"/>
|
||||
<path style="fill:#fff" d="M70.1,42.3l-8.5,8.5L45.4,67l-0.1,0.1L40.4,72l-3.2,3.2c2.5,1.5,5.3,2.2,8.1,2.2s5.6-0.8,8.1-2.2
|
||||
c1.1-0.7,2.2-1.5,3.2-2.5L70,59.3l8.5-8.5L70.1,42.3z"/>
|
||||
<path style="fill:#fff" d="M43.1,65.1l0.1-0.1l16.2-16.2l8.5-8.5l-8.4-8.6L51,40.2L38.2,53l-3.5,3.5l-5,5L26.2,65l7.8,7.8
|
||||
c0.2,0.2,0.5,0.5,0.7,0.7l3.5-3.5L43.1,65.1z"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.7 KiB |
41
fe/src/assets/img/bg-gradient.svg
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<!-- Generator: Adobe Illustrator 24.2.3, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||
viewBox="0 0 2560 1440" style="enable-background:new 0 0 2560 1440;" xml:space="preserve">
|
||||
<rect style="fill:#020618;" width="2560" height="1440"/>
|
||||
<radialGradient id="SVGID_1_" cx="1280" cy="720" r="507.7116" fx="1274.7371" fy="1155.8185" gradientTransform="matrix(1 0 0 2.2985 0 -934.9553)" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0" style="stop-color:#00BCFF;stop-opacity:0.5"/>
|
||||
<stop offset="1" style="stop-color:#00BCFF;stop-opacity:0"/>
|
||||
</radialGradient>
|
||||
<rect style="opacity:0.55;fill:url(#SVGID_1_);" width="2560" height="1440"/>
|
||||
<radialGradient id="SVGID_2_" cx="1352.0476" cy="1354.1904" r="1334.0841" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0" style="stop-color:#00BCFF;stop-opacity:0.5"/>
|
||||
<stop offset="1" style="stop-color:#00BCFF;stop-opacity:0"/>
|
||||
</radialGradient>
|
||||
<rect style="opacity:0.55;fill:url(#SVGID_2_);" width="2560" height="1440"/>
|
||||
<radialGradient id="SVGID_3_" cx="1292.0344" cy="1255.0016" r="2246.7517" gradientTransform="matrix(-0.7144 -0.6998 0.1899 -0.1939 1976.6873 2402.437)" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0" style="stop-color:#00BCFF;stop-opacity:0.5"/>
|
||||
<stop offset="1" style="stop-color:#00BCFF;stop-opacity:0"/>
|
||||
</radialGradient>
|
||||
<polygon style="opacity:0.55;fill:url(#SVGID_3_);" points="2560,1440 0,1440 0,-7 2560,0 "/>
|
||||
<radialGradient id="SVGID_4_" cx="1292.0344" cy="1255.8966" r="2246.5256" fx="334.4712" fy="1265.3895" gradientTransform="matrix(0.7144 -0.6998 -0.1899 -0.1939 583.4827 2403.5054)" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0" style="stop-color:#00BCFF;stop-opacity:0.5"/>
|
||||
<stop offset="1" style="stop-color:#00BCFF;stop-opacity:0"/>
|
||||
</radialGradient>
|
||||
<polygon style="opacity:0.55;fill:url(#SVGID_4_);" points="0,1440 2560,1440 2560,0 0,0 "/>
|
||||
<radialGradient id="SVGID_5_" cx="1239.8966" cy="1737.5518" r="877.3733" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0" style="stop-color:#FFB900"/>
|
||||
<stop offset="1" style="stop-color:#FFB900;stop-opacity:0"/>
|
||||
</radialGradient>
|
||||
<rect style="fill:url(#SVGID_5_);" width="2560" height="1440"/>
|
||||
<radialGradient id="SVGID_6_" cx="1287.069" cy="950.5172" r="845.7465" fx="1276.8361" fy="325.8423" gradientTransform="matrix(-1 3.730347e-03 -1.479320e-03 -0.3966 2575.5352 1322.6541)" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0" style="stop-color:#FFB900;stop-opacity:0.3"/>
|
||||
<stop offset="1" style="stop-color:#FFB900;stop-opacity:0"/>
|
||||
</radialGradient>
|
||||
<rect style="fill:url(#SVGID_6_);" width="2560" height="1440"/>
|
||||
<radialGradient id="SVGID_7_" cx="1316.8621" cy="1417.2759" r="1888.6272" gradientTransform="matrix(0.6652 -0.7467 0.1801 0.1604 185.7137 2173.2124)" gradientUnits="userSpaceOnUse">
|
||||
<stop offset="0" style="stop-color:#FFB900;stop-opacity:0.38"/>
|
||||
<stop offset="1" style="stop-color:#FFB900;stop-opacity:0"/>
|
||||
</radialGradient>
|
||||
<rect style="fill:url(#SVGID_7_);" width="2560" height="1440"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 3.1 KiB |
|
|
@ -1 +0,0 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 261.76 226.69"><path d="M161.096.001l-30.225 52.351L100.647.001H-.005l130.877 226.688L261.749.001z" fill="#41b883"/><path d="M161.096.001l-30.225 52.351L100.647.001H52.346l78.526 136.01L209.398.001z" fill="#34495e"/></svg>
|
||||
|
Before Width: | Height: | Size: 276 B |
|
|
@ -0,0 +1,38 @@
|
|||
/*
|
||||
Macrame is a program that enables the user to create keyboard macros and button panels.
|
||||
The macros are saved as simple JSON files and can be linked to the button panels. The panels can
|
||||
be created with HTML and CSS.
|
||||
|
||||
Copyright (C) 2025 Jesse Malotaux
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
@import './style/_content.css';
|
||||
@import './style/_form.css';
|
||||
@import './style/_scrollbar.css';
|
||||
@import './style/_macro.css';
|
||||
@import './style/_mcrm-block.css';
|
||||
@import './style/_panel.css';
|
||||
|
||||
@import 'tailwindcss';
|
||||
|
||||
@variant dark (&:where(.dark, .dark *));
|
||||
|
||||
html,
|
||||
body,
|
||||
:not(#panel-html__body) {
|
||||
--font-sans: 'Roboto', sans-serif;
|
||||
--font-mono: 'Fira Code', monospace;
|
||||
}
|
||||
68
fe/src/assets/style/_content.css
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
/*
|
||||
Macrame is a program that enables the user to create keyboard macros and button panels.
|
||||
The macros are saved as simple JSON files and can be linked to the button panels. The panels can
|
||||
be created with HTML and CSS.
|
||||
|
||||
Copyright (C) 2025 Jesse Malotaux
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
body {
|
||||
@apply font-sans font-light tracking-wide bg-slate-900 text-slate-50;
|
||||
}
|
||||
|
||||
h1,
|
||||
h2 {
|
||||
@apply font-mono font-bold;
|
||||
}
|
||||
|
||||
h3,
|
||||
h4,
|
||||
h5,
|
||||
h6 {
|
||||
@apply font-semibold;
|
||||
}
|
||||
|
||||
h1 {
|
||||
@apply text-4xl;
|
||||
}
|
||||
|
||||
h2 {
|
||||
@apply text-3xl;
|
||||
}
|
||||
|
||||
h3 {
|
||||
@apply text-2xl;
|
||||
}
|
||||
|
||||
h4 {
|
||||
@apply text-xl;
|
||||
}
|
||||
|
||||
h5 {
|
||||
@apply text-lg;
|
||||
}
|
||||
|
||||
ul {
|
||||
@apply list-disc list-inside;
|
||||
}
|
||||
|
||||
strong {
|
||||
@apply font-bold;
|
||||
}
|
||||
|
||||
a {
|
||||
@apply underline text-amber-400 hover:text-amber-300;
|
||||
}
|
||||
49
fe/src/assets/style/_form.css
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
/*
|
||||
Macrame is a program that enables the user to create keyboard macros and button panels.
|
||||
The macros are saved as simple JSON files and can be linked to the button panels. The panels can
|
||||
be created with HTML and CSS.
|
||||
|
||||
Copyright (C) 2025 Jesse Malotaux
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
.input-group {
|
||||
@apply grid gap-2;
|
||||
}
|
||||
|
||||
input,
|
||||
select {
|
||||
@apply w-full px-2 py-1 text-white border rounded-md border-slate-400 bg-black/20;
|
||||
}
|
||||
|
||||
:has(> input + span) {
|
||||
@apply flex;
|
||||
|
||||
input {
|
||||
@apply rounded-r-none;
|
||||
}
|
||||
|
||||
span {
|
||||
@apply flex items-center px-2 text-white rounded-r-md bg-slate-400;
|
||||
}
|
||||
}
|
||||
|
||||
select option {
|
||||
@apply bg-slate-700;
|
||||
|
||||
&:not([disabled]) {
|
||||
@apply cursor-pointer;
|
||||
}
|
||||
}
|
||||
49
fe/src/assets/style/_macro.css
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
/*
|
||||
Macrame is a program that enables the user to create keyboard macros and button panels.
|
||||
The macros are saved as simple JSON files and can be linked to the button panels. The panels can
|
||||
be created with HTML and CSS.
|
||||
|
||||
Copyright (C) 2025 Jesse Malotaux
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
/* @reference "main"; */
|
||||
hr.spacer {
|
||||
@apply relative
|
||||
w-6
|
||||
border
|
||||
border-gray-300
|
||||
opacity-80
|
||||
overflow-visible;
|
||||
|
||||
&::before,
|
||||
&::after {
|
||||
@apply content-['']
|
||||
absolute
|
||||
top-1/2
|
||||
-translate-y-1/2
|
||||
size-2
|
||||
bg-gray-300
|
||||
rounded-full;
|
||||
}
|
||||
|
||||
&::before {
|
||||
@apply -left-1;
|
||||
}
|
||||
|
||||
&::after {
|
||||
@apply -right-1;
|
||||
}
|
||||
}
|
||||
123
fe/src/assets/style/_mcrm-block.css
Normal file
|
|
@ -0,0 +1,123 @@
|
|||
/*
|
||||
Macrame is a program that enables the user to create keyboard macros and button panels.
|
||||
The macros are saved as simple JSON files and can be linked to the button panels. The panels can
|
||||
be created with HTML and CSS.
|
||||
|
||||
Copyright (C) 2025 Jesse Malotaux
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
.mcrm-block {
|
||||
@apply relative p-6 overflow-hidden gap-x-6 gap-y-2 backdrop-blur-lg rounded-2xl;
|
||||
|
||||
&::before {
|
||||
@apply content-['']
|
||||
absolute
|
||||
inset-0
|
||||
p-px
|
||||
rounded-2xl
|
||||
size-full
|
||||
bg-gradient-to-br
|
||||
to-transparent
|
||||
z-[10]
|
||||
pointer-events-none;
|
||||
|
||||
mask:
|
||||
linear-gradient(#000 0 0) exclude,
|
||||
linear-gradient(#000 0 0) content-box;
|
||||
}
|
||||
|
||||
&.block__light {
|
||||
@apply bg-white/20;
|
||||
|
||||
&::before {
|
||||
@apply from-white/20;
|
||||
}
|
||||
}
|
||||
|
||||
&.block__dark {
|
||||
@apply bg-slate-900/70;
|
||||
|
||||
&::before {
|
||||
@apply from-slate-400/40;
|
||||
}
|
||||
}
|
||||
|
||||
&.block__primary {
|
||||
@apply bg-sky-300/20;
|
||||
|
||||
&::before {
|
||||
@apply from-sky-100/20;
|
||||
}
|
||||
}
|
||||
|
||||
&.block__secondary {
|
||||
@apply bg-amber-300/20;
|
||||
|
||||
&::before {
|
||||
@apply from-amber-100/20;
|
||||
}
|
||||
}
|
||||
|
||||
&.block__success {
|
||||
@apply bg-emerald-300/40;
|
||||
|
||||
&::before {
|
||||
@apply from-emerald-100/40;
|
||||
}
|
||||
}
|
||||
|
||||
&.block__warning {
|
||||
@apply bg-orange-300/40;
|
||||
|
||||
&::before {
|
||||
@apply from-orange-100/40;
|
||||
}
|
||||
}
|
||||
|
||||
&.block__danger {
|
||||
@apply bg-rose-300/40;
|
||||
|
||||
&::before {
|
||||
@apply from-rose-100/40;
|
||||
}
|
||||
}
|
||||
|
||||
&.block-spacing__sm,
|
||||
&.block-size__sm {
|
||||
@apply p-4 gap-x-4 gap-y-2;
|
||||
}
|
||||
|
||||
&.block-size__sm {
|
||||
@apply rounded-lg;
|
||||
|
||||
&::before {
|
||||
@apply rounded-lg;
|
||||
}
|
||||
}
|
||||
|
||||
&.block-spacing__lg,
|
||||
&.block-size__lg {
|
||||
@apply p-8 gap-x-8 gap-y-4;
|
||||
}
|
||||
|
||||
&.block-size__lg {
|
||||
@apply rounded-3xl;
|
||||
|
||||
&::before {
|
||||
@apply rounded-3xl;
|
||||
}
|
||||
}
|
||||
}
|
||||
56
fe/src/assets/style/_panel.css
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
/*
|
||||
Macrame is a program that enables the user to create keyboard macros and button panels.
|
||||
The macros are saved as simple JSON files and can be linked to the button panels. The panels can
|
||||
be created with HTML and CSS.
|
||||
|
||||
Copyright (C) 2025 Jesse Malotaux
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
.panel {
|
||||
@apply grid
|
||||
grid-rows-[auto_1fr]
|
||||
fixed
|
||||
top-2
|
||||
left-4 sm:left-16
|
||||
right-4 sm:right-16
|
||||
bottom-2
|
||||
overflow-hidden;
|
||||
|
||||
> .panel__header,
|
||||
> .panel__title {
|
||||
@apply px-4 py-2;
|
||||
|
||||
/* &:first-child {
|
||||
@apply pt-4;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
@apply pb-4;
|
||||
} */
|
||||
}
|
||||
|
||||
.panel__title {
|
||||
@apply pt-3 pl-16 text-transparent bg-gradient-to-r w-fit from-amber-300 to-white/50 sm:pl-4 bg-clip-text;
|
||||
}
|
||||
|
||||
.panel__content {
|
||||
@apply grid
|
||||
h-[calc(100%-1rem)]
|
||||
pt-4 sm:pt-0
|
||||
pl-0 sm:pl-4
|
||||
overflow-auto;
|
||||
}
|
||||
}
|
||||
44
fe/src/assets/style/_scrollbar.css
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
/*
|
||||
Macrame is a program that enables the user to create keyboard macros and button panels.
|
||||
The macros are saved as simple JSON files and can be linked to the button panels. The panels can
|
||||
be created with HTML and CSS.
|
||||
|
||||
Copyright (C) 2025 Jesse Malotaux
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
::-webkit-scrollbar {
|
||||
@apply w-2;
|
||||
}
|
||||
|
||||
::-moz-scrollbar {
|
||||
@apply w-2;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
@apply rounded bg-slate-400/80;
|
||||
}
|
||||
|
||||
::-moz-scrollbar-thumb {
|
||||
@apply rounded bg-slate-400/80;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
@apply mr-1 rounded bg-slate-100/10;
|
||||
}
|
||||
|
||||
::-moz-scrollbar-track {
|
||||
@apply mr-1 rounded bg-slate-100/10;
|
||||
}
|
||||
119
fe/src/components/base/AccordionComp.vue
Normal file
|
|
@ -0,0 +1,119 @@
|
|||
<!--
|
||||
Macrame is a program that enables the user to create keyboard macros and button panels.
|
||||
The macros are saved as simple JSON files and can be linked to the button panels. The panels can
|
||||
be created with HTML and CSS.
|
||||
|
||||
Copyright (C) 2025 Jesse Malotaux
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div class="accordion">
|
||||
<header @click="toggleAccordion(!accordionOpen)">
|
||||
<h4>{{ title }}</h4>
|
||||
<ButtonComp variant="ghost" size="sm" class="!px-1">
|
||||
<IconChevronDown v-if="!accordionOpen" />
|
||||
<IconChevronUp v-else />
|
||||
</ButtonComp>
|
||||
</header>
|
||||
<section :class="`accordion__wrapper ${accordionOpen ? 'open' : ''}`">
|
||||
<div class="accordion__content">
|
||||
<slot />
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted, onUpdated, ref } from 'vue'
|
||||
import ButtonComp from './ButtonComp.vue'
|
||||
import { IconChevronDown, IconChevronUp } from '@tabler/icons-vue'
|
||||
|
||||
const emit = defineEmits(['onOpen', 'onClose', 'onToggle'])
|
||||
|
||||
defineExpose({ toggleAccordion })
|
||||
|
||||
const props = defineProps({
|
||||
title: String,
|
||||
open: Boolean,
|
||||
})
|
||||
|
||||
const accordionOpen = ref(false)
|
||||
|
||||
onMounted(() => {
|
||||
if (props.open) toggleAccordion(props.open)
|
||||
})
|
||||
|
||||
onUpdated(() => {
|
||||
if (props.open) toggleAccordion(props.open)
|
||||
})
|
||||
|
||||
function toggleAccordion(open = false) {
|
||||
if (open) {
|
||||
accordionOpen.value = true
|
||||
emit('onOpen')
|
||||
} else {
|
||||
accordionOpen.value = false
|
||||
emit('onClose')
|
||||
}
|
||||
|
||||
emit('onToggle')
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@reference "@/assets/main.css";
|
||||
|
||||
.accordion {
|
||||
@apply grid;
|
||||
|
||||
header {
|
||||
@apply grid
|
||||
grid-cols-[1fr_auto]
|
||||
px-4 py-2
|
||||
cursor-pointer;
|
||||
}
|
||||
|
||||
.accordion__wrapper {
|
||||
@apply grid
|
||||
grid-rows-[0fr]
|
||||
border-y
|
||||
border-b-white/60
|
||||
border-t-transparent
|
||||
duration-300
|
||||
ease-in-out;
|
||||
|
||||
.accordion__content {
|
||||
@apply grid
|
||||
grid-rows-[0fr]
|
||||
overflow-hidden
|
||||
opacity-0
|
||||
transition-opacity
|
||||
delay-0;
|
||||
}
|
||||
|
||||
&.open {
|
||||
@apply grid-rows-[1fr]
|
||||
border-t-white/20;
|
||||
|
||||
.accordion__content {
|
||||
@apply grid-rows-[1fr]
|
||||
opacity-100
|
||||
delay-200;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
100
fe/src/components/base/AlertComp.vue
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
<!--
|
||||
Macrame is a program that enables the user to create keyboard macros and button panels.
|
||||
The macros are saved as simple JSON files and can be linked to the button panels. The panels can
|
||||
be created with HTML and CSS.
|
||||
|
||||
Copyright (C) 2025 Jesse Malotaux
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div
|
||||
:class="`alert alert__${variant} ${pageWide ? 'page-wide' : ''}`"
|
||||
@click="href ? router.push(href) : null"
|
||||
>
|
||||
<IconInfoCircle v-if="variant === 'info'" />
|
||||
<IconCheck v-if="variant === 'success'" />
|
||||
<IconExclamationCircle v-if="variant === 'warning'" />
|
||||
<IconAlertTriangle v-if="variant === 'error'" />
|
||||
<div class="alert__content">
|
||||
<slot />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {
|
||||
IconAlertTriangle,
|
||||
IconCheck,
|
||||
IconExclamationCircle,
|
||||
IconInfoCircle,
|
||||
} from '@tabler/icons-vue'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
defineProps({
|
||||
variant: String, // info, success, warning, error
|
||||
pageWide: Boolean,
|
||||
href: String,
|
||||
})
|
||||
|
||||
const router = useRouter()
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@reference "@/assets/main.css";
|
||||
|
||||
.alert {
|
||||
@apply grid
|
||||
grid-cols-[1rem_1fr]
|
||||
items-start
|
||||
gap-4
|
||||
p-4
|
||||
border
|
||||
border-white/10
|
||||
bg-white/10
|
||||
rounded-md
|
||||
backdrop-blur-md;
|
||||
|
||||
&.alert__info {
|
||||
@apply text-sky-100 bg-sky-400/40;
|
||||
}
|
||||
|
||||
&.alert__success {
|
||||
@apply text-lime-400 bg-lime-400/10;
|
||||
}
|
||||
|
||||
&.alert__warning {
|
||||
@apply text-amber-400 bg-amber-400/10;
|
||||
}
|
||||
|
||||
&.alert__error {
|
||||
@apply text-rose-400 bg-rose-400/10;
|
||||
}
|
||||
|
||||
&.page-wide {
|
||||
@apply fixed
|
||||
bottom-0 left-0
|
||||
w-full;
|
||||
}
|
||||
|
||||
&[href] {
|
||||
@apply cursor-pointer;
|
||||
}
|
||||
|
||||
.alert__content {
|
||||
@apply grid gap-2;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
189
fe/src/components/base/ButtonComp.vue
Normal file
|
|
@ -0,0 +1,189 @@
|
|||
<!--
|
||||
Macrame is a program that enables the user to create keyboard macros and button panels.
|
||||
The macros are saved as simple JSON files and can be linked to the button panels. The panels can
|
||||
be created with HTML and CSS.
|
||||
|
||||
Copyright (C) 2025 Jesse Malotaux
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-->
|
||||
|
||||
<template>
|
||||
<template v-if="href">
|
||||
<RouterLink :to="href" :class="classString">
|
||||
<slot />
|
||||
</RouterLink>
|
||||
</template>
|
||||
<template v-else>
|
||||
<button :class="classString">
|
||||
<slot />
|
||||
</button>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { computed } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
href: String,
|
||||
variant: String,
|
||||
size: String,
|
||||
})
|
||||
|
||||
const classString = computed(() => {
|
||||
let classes = 'btn'
|
||||
if (props.variant) classes += ` btn__${props.variant}`
|
||||
if (props.size) classes += ` btn__${props.size}`
|
||||
|
||||
return classes
|
||||
})
|
||||
</script>
|
||||
|
||||
<style>
|
||||
@reference "@/assets/main.css";
|
||||
|
||||
button,
|
||||
.btn {
|
||||
@apply flex
|
||||
items-center
|
||||
gap-3
|
||||
h-fit
|
||||
px-4 py-2
|
||||
border
|
||||
border-solid
|
||||
rounded-lg
|
||||
tracking-wide
|
||||
font-normal
|
||||
transition-all
|
||||
cursor-pointer
|
||||
no-underline;
|
||||
|
||||
transition:
|
||||
border-color 0.1s ease-in-out,
|
||||
background-color 0.2s ease;
|
||||
|
||||
&:not(.button__subtle, .button__ghost):hover {
|
||||
@apply shadow-black;
|
||||
}
|
||||
|
||||
&[disabled],
|
||||
&.disabled {
|
||||
@apply opacity-50 pointer-events-none cursor-not-allowed;
|
||||
}
|
||||
|
||||
svg {
|
||||
@apply size-5 transition-[stroke] duration-400 ease-in-out;
|
||||
}
|
||||
|
||||
&.btn__sm {
|
||||
@apply px-3 py-1
|
||||
text-sm;
|
||||
|
||||
svg {
|
||||
@apply size-4;
|
||||
}
|
||||
}
|
||||
|
||||
&.btn__lg {
|
||||
@apply px-6 py-3
|
||||
text-lg;
|
||||
|
||||
svg {
|
||||
@apply size-6;
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
@apply text-white;
|
||||
|
||||
svg {
|
||||
@apply stroke-current;
|
||||
}
|
||||
}
|
||||
|
||||
&.btn__primary {
|
||||
@apply bg-sky-100/10 border-sky-100 text-sky-100;
|
||||
|
||||
svg {
|
||||
@apply stroke-sky-200;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
@apply bg-sky-400/40 border-sky-300;
|
||||
}
|
||||
}
|
||||
|
||||
&.btn__secondary {
|
||||
@apply bg-amber-100/10 border-amber-100 text-amber-100;
|
||||
|
||||
svg {
|
||||
@apply stroke-amber-300;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
@apply bg-amber-400/40 border-amber-400;
|
||||
}
|
||||
}
|
||||
|
||||
&.btn__danger {
|
||||
@apply bg-rose-200/20 border-rose-100 text-rose-200;
|
||||
|
||||
svg {
|
||||
@apply stroke-rose-400;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
@apply bg-rose-400/40 border-rose-500 text-white;
|
||||
}
|
||||
}
|
||||
|
||||
&.btn__dark {
|
||||
/* @apply bg-slate-700/80 hover:bg-slate-700 text-white border-slate-600; */
|
||||
@apply bg-slate-200/10 border-slate-400 text-slate-100;
|
||||
|
||||
svg {
|
||||
@apply stroke-slate-300;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
@apply bg-slate-400/40 border-slate-200 text-white;
|
||||
}
|
||||
}
|
||||
|
||||
&.btn__success {
|
||||
/* @apply bg-lime-500/80 hover:bg-lime-500 text-white border-lime-600; */
|
||||
@apply bg-lime-200/10 border-lime-100 text-lime-100;
|
||||
|
||||
svg {
|
||||
@apply stroke-lime-400;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
@apply bg-lime-400/40 border-lime-500 text-white;
|
||||
}
|
||||
}
|
||||
|
||||
&.btn__subtle {
|
||||
@apply bg-transparent hover:bg-white/10 text-white border-transparent;
|
||||
|
||||
&:hover {
|
||||
@apply bg-white/20 to-white/30 border-white/40;
|
||||
}
|
||||
}
|
||||
|
||||
&.btn__ghost {
|
||||
@apply bg-transparent text-white/80 border-transparent hover:text-white;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
36
fe/src/components/base/ButtonGroup.vue
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
<!--
|
||||
Macrame is a program that enables the user to create keyboard macros and button panels.
|
||||
The macros are saved as simple JSON files and can be linked to the button panels. The panels can
|
||||
be created with HTML and CSS.
|
||||
|
||||
Copyright (C) 2025 Jesse Malotaux
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div class="button-group">
|
||||
<slot />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
defineProps({
|
||||
variant: String,
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@reference "@/assets/main.css";
|
||||
</style>
|
||||
103
fe/src/components/base/ContextMenu.vue
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
<!--
|
||||
Macrame is a program that enables the user to create keyboard macros and button panels.
|
||||
The macros are saved as simple JSON files and can be linked to the button panels. The panels can
|
||||
be created with HTML and CSS.
|
||||
|
||||
Copyright (C) 2025 Jesse Malotaux
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div class="context-menu">
|
||||
<div class="context-menu__trigger" @click="toggle">
|
||||
<slot name="trigger" />
|
||||
</div>
|
||||
<div :class="`context-menu__content ${menuOpen ? 'open' : ''}`">
|
||||
<slot name="content" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted, onUpdated } from 'vue'
|
||||
|
||||
defineExpose({ toggle })
|
||||
|
||||
const props = defineProps({
|
||||
open: Boolean,
|
||||
})
|
||||
|
||||
const menuOpen = ref(false)
|
||||
|
||||
onMounted(() => {
|
||||
menuOpen.value = props.open
|
||||
})
|
||||
|
||||
function toggle() {
|
||||
menuOpen.value = !menuOpen.value
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
@reference "@/assets/main.css";
|
||||
|
||||
.context-menu {
|
||||
@apply relative;
|
||||
|
||||
.context-menu__content {
|
||||
@apply absolute
|
||||
top-full
|
||||
-translate-y-full
|
||||
opacity-0
|
||||
pointer-events-none
|
||||
mt-2
|
||||
min-w-full
|
||||
grid
|
||||
border
|
||||
border-white/50
|
||||
bg-slate-100/60
|
||||
backdrop-blur-3xl
|
||||
text-slate-800
|
||||
rounded-md
|
||||
z-50
|
||||
transition-all;
|
||||
|
||||
&.open {
|
||||
@apply translate-y-0
|
||||
opacity-100
|
||||
pointer-events-auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.context-menu ul {
|
||||
@apply text-slate-800
|
||||
divide-y
|
||||
divide-slate-300;
|
||||
|
||||
li {
|
||||
@apply flex
|
||||
gap-2
|
||||
items-center
|
||||
p-2
|
||||
hover:bg-black/10
|
||||
cursor-pointer;
|
||||
|
||||
svg {
|
||||
@apply size-5;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
132
fe/src/components/base/DialogComp.vue
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
<!--
|
||||
Macrame is a program that enables the user to create keyboard macros and button panels.
|
||||
The macros are saved as simple JSON files and can be linked to the button panels. The panels can
|
||||
be created with HTML and CSS.
|
||||
|
||||
Copyright (C) 2025 Jesse Malotaux
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div class="dialog-container">
|
||||
<div class="trigger" @click="toggleDialog(true)">
|
||||
<slot name="trigger" />
|
||||
</div>
|
||||
<dialog ref="dialog" class="mcrm-block block__dark">
|
||||
<ButtonComp
|
||||
class="dialog__close p-0"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
tabindex="-1"
|
||||
@click="toggleDialog(false)"
|
||||
>
|
||||
<IconX />
|
||||
</ButtonComp>
|
||||
<slot name="content" />
|
||||
</dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import ButtonComp from './ButtonComp.vue'
|
||||
import { IconX } from '@tabler/icons-vue'
|
||||
import { onMounted, onUpdated, ref } from 'vue'
|
||||
|
||||
const dialog = ref(null)
|
||||
const openDialog = ref()
|
||||
|
||||
const emit = defineEmits(['onOpen', 'onClose', 'onToggle'])
|
||||
|
||||
defineExpose({ toggleDialog })
|
||||
|
||||
const props = defineProps({
|
||||
open: Boolean,
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
if (props.open === true) toggleDialog(props.open)
|
||||
})
|
||||
|
||||
onUpdated(() => {
|
||||
if (props.open === true) toggleDialog(props.open)
|
||||
})
|
||||
|
||||
function toggleDialog(openToggle) {
|
||||
if (openToggle) {
|
||||
dialog.value.showModal()
|
||||
emit('onOpen')
|
||||
} else {
|
||||
dialog.value.close()
|
||||
emit('onClose')
|
||||
}
|
||||
|
||||
openDialog.value = openToggle
|
||||
emit('onToggle')
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
openDialog.value = props.open
|
||||
|
||||
if (dialog.value.innerHTML.includes('form')) {
|
||||
dialog.value.querySelector('form').addEventListener('submit', () => {
|
||||
toggleDialog()
|
||||
})
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style>
|
||||
@reference "@/assets/main.css";
|
||||
|
||||
.dialog-container {
|
||||
@apply relative;
|
||||
|
||||
dialog {
|
||||
@apply fixed
|
||||
top-1/2 left-1/2
|
||||
-translate-x-1/2 -translate-y-1/2
|
||||
max-w-[calc(100vw-2rem)]
|
||||
text-slate-200
|
||||
/* shadow-md */
|
||||
/* shadow-black */
|
||||
z-50
|
||||
pointer-events-none;
|
||||
|
||||
&[open] {
|
||||
@apply pointer-events-auto;
|
||||
}
|
||||
|
||||
&::backdrop {
|
||||
@apply bg-black/50 backdrop-blur-xs transition;
|
||||
}
|
||||
|
||||
.dialog__close {
|
||||
@apply absolute
|
||||
top-4 right-4
|
||||
p-0
|
||||
text-white;
|
||||
|
||||
svg {
|
||||
@apply size-5;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.dialog__content {
|
||||
> *:first-child {
|
||||
@apply pr-8;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
59
fe/src/components/base/LoadComp.vue
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
<!--
|
||||
Macrame is a program that enables the user to create keyboard macros and button panels.
|
||||
The macros are saved as simple JSON files and can be linked to the button panels. The panels can
|
||||
be created with HTML and CSS.
|
||||
|
||||
Copyright (C) 2025 Jesse Malotaux
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div v-if="loading" class="loading-component">
|
||||
<span v-if="text">
|
||||
{{ text }}
|
||||
</span>
|
||||
<IconLoader3 class="duration-1000 animate-spin" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { IconLoader3 } from '@tabler/icons-vue'
|
||||
|
||||
defineProps({
|
||||
loading: Boolean,
|
||||
text: String,
|
||||
})
|
||||
</script>
|
||||
|
||||
<style>
|
||||
@reference "@/assets/main.css";
|
||||
|
||||
&:has(.loading-component) {
|
||||
@apply relative;
|
||||
}
|
||||
|
||||
.loading-component {
|
||||
@apply absolute
|
||||
inset-0
|
||||
size-full
|
||||
flex gap-2
|
||||
flex-col
|
||||
justify-center
|
||||
items-center
|
||||
text-sm
|
||||
bg-black/50
|
||||
backdrop-blur-md;
|
||||
}
|
||||
</style>
|
||||
156
fe/src/components/base/MainMenu.vue
Normal file
|
|
@ -0,0 +1,156 @@
|
|||
<!--
|
||||
Macrame is a program that enables the user to create keyboard macros and button panels.
|
||||
The macros are saved as simple JSON files and can be linked to the button panels. The panels can
|
||||
be created with HTML and CSS.
|
||||
|
||||
Copyright (C) 2025 Jesse Malotaux
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-->
|
||||
|
||||
<template>
|
||||
<nav id="main-menu">
|
||||
<button id="menu-toggle" :class="menuOpen ? 'open' : ''" @click="menuOpen = !menuOpen">
|
||||
<img
|
||||
class="p-1 logo"
|
||||
:class="{ 'opacity-0': menuOpen }"
|
||||
src="@/assets/img/Macrame-Logo-gradient.svg"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<IconX :class="{ 'opacity-0': !menuOpen }" />
|
||||
</button>
|
||||
<ul :class="menuOpen ? 'open' : ''">
|
||||
<li>
|
||||
<RouterLink @click="menuOpen = false" to="/"> <IconHome />Dashboard </RouterLink>
|
||||
</li>
|
||||
<li>
|
||||
<RouterLink @click="menuOpen = false" to="/panels"> <IconLayoutGrid />Panels </RouterLink>
|
||||
</li>
|
||||
<li v-if="isLocal()">
|
||||
<RouterLink @click="menuOpen = false" to="/macros"> <IconKeyboard />Macros </RouterLink>
|
||||
</li>
|
||||
<li>
|
||||
<RouterLink @click="menuOpen = false" to="/devices">
|
||||
<IconDevices />{{ isLocal() ? 'Devices' : 'Server' }}
|
||||
</RouterLink>
|
||||
</li>
|
||||
<!-- <li>
|
||||
<RouterLink @click="menuOpen = false" to="/settings">
|
||||
<IconSettings />Settings
|
||||
</RouterLink>
|
||||
</li> -->
|
||||
</ul>
|
||||
</nav>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { RouterLink } from 'vue-router'
|
||||
import {
|
||||
IconDevices,
|
||||
IconHome,
|
||||
IconKeyboard,
|
||||
IconLayoutGrid,
|
||||
IconSettings,
|
||||
IconX,
|
||||
} from '@tabler/icons-vue'
|
||||
import { ref } from 'vue'
|
||||
import { isLocal } from '@/services/ApiService'
|
||||
|
||||
const menuOpen = ref(false)
|
||||
</script>
|
||||
|
||||
<style>
|
||||
@reference "@/assets/main.css";
|
||||
nav {
|
||||
@apply relative flex z-50;
|
||||
|
||||
button {
|
||||
@apply absolute
|
||||
top-4 left-4
|
||||
size-12
|
||||
rounded-full
|
||||
aspect-square
|
||||
bg-white/20 hover:bg-white/40
|
||||
border-0
|
||||
cursor-pointer
|
||||
transition-colors
|
||||
backdrop-blur-md;
|
||||
|
||||
.logo,
|
||||
svg {
|
||||
@apply absolute
|
||||
inset-1/2
|
||||
-translate-1/2
|
||||
transition-opacity
|
||||
duration-400
|
||||
ease-in-out;
|
||||
}
|
||||
|
||||
.logo {
|
||||
@apply w-full;
|
||||
}
|
||||
}
|
||||
|
||||
ul {
|
||||
@apply absolute
|
||||
top-20 left-0
|
||||
-translate-x-full
|
||||
grid
|
||||
list-none
|
||||
rounded-xl
|
||||
overflow-hidden
|
||||
bg-white/10
|
||||
backdrop-blur-md
|
||||
divide-y
|
||||
divide-slate-600
|
||||
transition-transform
|
||||
duration-300
|
||||
ease-in-out;
|
||||
|
||||
&.open {
|
||||
@apply left-4 translate-x-0;
|
||||
}
|
||||
|
||||
li {
|
||||
a {
|
||||
@apply flex
|
||||
items-center
|
||||
gap-2
|
||||
px-4 py-2
|
||||
text-white
|
||||
no-underline
|
||||
border-transparent
|
||||
transition-colors;
|
||||
|
||||
svg {
|
||||
@apply text-white/40 transition-colors;
|
||||
}
|
||||
|
||||
&:hover {
|
||||
@apply bg-white/20;
|
||||
|
||||
svg {
|
||||
@apply text-white;
|
||||
}
|
||||
}
|
||||
|
||||
&.router-link-active {
|
||||
@apply text-sky-300
|
||||
bg-sky-200/20;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
107
fe/src/components/dashboard/RemoteView.vue
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
<!--
|
||||
Macrame is a program that enables the user to create keyboard macros and button panels.
|
||||
The macros are saved as simple JSON files and can be linked to the button panels. The panels can
|
||||
be created with HTML and CSS.
|
||||
|
||||
Copyright (C) 2025 Jesse Malotaux
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div id="remote-dashboard">
|
||||
<div id="panels" class="dashboard-block mcrm-block block__light" v-if="server.handshake">
|
||||
<div class="icon__container">
|
||||
<IconLayoutGrid />
|
||||
</div>
|
||||
<h4>{{ server.panelCount }} {{ server.panelCount != 1 ? 'Panels' : 'Panel' }}</h4>
|
||||
<template v-if="server.panelCount == 0">
|
||||
<p><em>No panels found. </em></p>
|
||||
<p>Learn how to create a panel <a href="#" target="_blank">here</a>.</p>
|
||||
</template>
|
||||
<template v-else>
|
||||
<p>Start using a panel!</p>
|
||||
<ButtonComp variant="danger" href="/panels"> <IconLayoutGrid /> View panels </ButtonComp>
|
||||
</template>
|
||||
</div>
|
||||
<div id="server" class="dashboard-block mcrm-block block__light">
|
||||
<div class="icon__container">
|
||||
<IconServer />
|
||||
</div>
|
||||
<h4>Server</h4>
|
||||
<template v-if="server.handshake">
|
||||
<p>
|
||||
Linked with: <strong class="text-center">{{ server.ip }}</strong>
|
||||
</p>
|
||||
<ButtonComp variant="primary" href="/devices"> <IconServer /> View server</ButtonComp>
|
||||
</template>
|
||||
<template v-else>
|
||||
<p>
|
||||
<em>Not linked</em>
|
||||
</p>
|
||||
<ButtonComp variant="primary" href="/devices"> <IconLink /> Link with server</ButtonComp>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { IconLayoutGrid, IconLink, IconServer } from '@tabler/icons-vue'
|
||||
import { onMounted, reactive } from 'vue'
|
||||
|
||||
import ButtonComp from '../base/ButtonComp.vue'
|
||||
|
||||
import { useDeviceStore } from '@/stores/device'
|
||||
import { usePanelStore } from '@/stores/panel'
|
||||
|
||||
const device = useDeviceStore()
|
||||
const panel = usePanelStore()
|
||||
|
||||
const server = reactive({
|
||||
ip: '',
|
||||
handshake: '',
|
||||
panelCount: 0,
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
const serverIp = await device.serverGetIP()
|
||||
server.ip = serverIp
|
||||
|
||||
if (device.key()) server.handshake = true
|
||||
|
||||
device.$subscribe(() => {
|
||||
if (device.key()) server.handshake = true
|
||||
})
|
||||
|
||||
const panelCount = await panel.getList(true)
|
||||
server.panelCount = panelCount
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@reference "@/assets/main.css";
|
||||
|
||||
#remote-dashboard {
|
||||
@apply grid
|
||||
pt-8
|
||||
gap-4
|
||||
md:w-fit
|
||||
h-fit
|
||||
content-start;
|
||||
|
||||
&.not__linked #server {
|
||||
@apply row-start-1 md:col-start-1;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
154
fe/src/components/dashboard/ServerView.vue
Normal file
|
|
@ -0,0 +1,154 @@
|
|||
<!--
|
||||
Macrame is a program that enables the user to create keyboard macros and button panels.
|
||||
The macros are saved as simple JSON files and can be linked to the button panels. The panels can
|
||||
be created with HTML and CSS.
|
||||
|
||||
Copyright (C) 2025 Jesse Malotaux
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div
|
||||
id="server-dashboard"
|
||||
:class="`${server.remoteCount == 0 ? 'no__devices' : 'devices__found'} ${server.macroCount == 0 ? 'no__macros' : 'macros__found'}`"
|
||||
>
|
||||
<div id="devices" class="dashboard-block mcrm-block block__light">
|
||||
<div class="icon__container">
|
||||
<IconDevices />
|
||||
</div>
|
||||
<h4>{{ server.remoteCount }} {{ server.remoteCount != 1 ? 'Devices' : 'Device' }}</h4>
|
||||
<template v-if="server.remoteCount == 0">
|
||||
<p><em>No devices found.</em></p>
|
||||
<ButtonComp variant="primary" href="/devices"> <IconLink /> Link a device</ButtonComp>
|
||||
</template>
|
||||
<template v-else>
|
||||
<p>Unlink a device or add new devices.</p>
|
||||
<ButtonComp variant="primary" href="/devices"><IconDevices /> View devices</ButtonComp>
|
||||
</template>
|
||||
</div>
|
||||
<div id="macros" class="dashboard-block mcrm-block block__light">
|
||||
<div class="icon__container">
|
||||
<IconKeyboard />
|
||||
</div>
|
||||
<h4>{{ server.macroCount }} {{ server.macroCount != 1 ? 'Macros' : 'Macro' }}</h4>
|
||||
<template v-if="server.macroCount == 0">
|
||||
<p><em>No macros found.</em></p>
|
||||
<ButtonComp variant="secondary" href="/macros"> <IconLayoutGrid /> Create macro</ButtonComp>
|
||||
</template>
|
||||
<template v-else>
|
||||
<p>Edit and view your macros.</p>
|
||||
<ButtonComp variant="secondary" href="/macros"><IconKeyboard /> View macros</ButtonComp>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<div id="panels" class="dashboard-block mcrm-block block__light">
|
||||
<div class="icon__container">
|
||||
<IconLayoutGrid />
|
||||
</div>
|
||||
<h4>{{ server.panelCount }} {{ server.panelCount != 1 ? 'Panels' : 'Panel' }}</h4>
|
||||
<template v-if="server.panelCount == 0">
|
||||
<p><em>No panels found. </em></p>
|
||||
<p>Learn how to create a panel <a href="#" target="_blank">here</a>.</p>
|
||||
</template>
|
||||
<template v-else>
|
||||
<p>Link macros to panels or view a panel.</p>
|
||||
<ButtonComp variant="danger" href="/panels"> <IconLayoutGrid /> View panels </ButtonComp>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useDeviceStore } from '@/stores/device'
|
||||
import { usePanelStore } from '@/stores/panel'
|
||||
import { IconDevices, IconKeyboard, IconLayoutGrid, IconLink } from '@tabler/icons-vue'
|
||||
import { onMounted, reactive } from 'vue'
|
||||
import ButtonComp from '../base/ButtonComp.vue'
|
||||
import { GetMacroList } from '@/services/MacroService'
|
||||
|
||||
const device = useDeviceStore()
|
||||
const panel = usePanelStore()
|
||||
|
||||
const server = reactive({
|
||||
ip: '',
|
||||
port: '',
|
||||
fullPath: '',
|
||||
remoteCount: 0,
|
||||
macroCount: 0,
|
||||
panelCount: 0,
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
const serverIP = await device.serverGetIP()
|
||||
server.ip = serverIP
|
||||
// server.port = window.__CONFIG__.MCRM__PORT
|
||||
// server.fullPath = `http://${server.ip}:${server.port}`
|
||||
|
||||
const remoteCount = await device.serverGetRemotes(true)
|
||||
server.remoteCount = remoteCount
|
||||
|
||||
const macroCount = await GetMacroList(true)
|
||||
server.macroCount = macroCount
|
||||
|
||||
const panelCount = await panel.getList(true)
|
||||
server.panelCount = panelCount
|
||||
|
||||
console.log(server)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@reference "@/assets/main.css";
|
||||
|
||||
#server-dashboard {
|
||||
@apply grid
|
||||
grid-cols-1
|
||||
grid-rows-3
|
||||
md:grid-cols-3
|
||||
md:grid-rows-1
|
||||
gap-4
|
||||
w-fit
|
||||
h-fit
|
||||
pt-8;
|
||||
|
||||
&.no__devices #devices {
|
||||
@apply row-start-1 md:col-start-1;
|
||||
}
|
||||
|
||||
&.no__macros.devices__found #devices {
|
||||
@apply row-start-3 md:col-start-3;
|
||||
}
|
||||
|
||||
&.devices__found #devices {
|
||||
@apply row-start-3 md:col-start-3;
|
||||
}
|
||||
|
||||
&.no__devices.no__macros #macros {
|
||||
@apply row-start-2 md:col-start-2;
|
||||
}
|
||||
|
||||
&.no__macros #macros {
|
||||
@apply row-start-1 md:col-start-1;
|
||||
}
|
||||
|
||||
&.macros__found #macros {
|
||||
@apply row-start-2 md:col-start-2;
|
||||
}
|
||||
|
||||
&.no__devices.macros__found #macros {
|
||||
@apply row-start-3 md:col-start-3;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
225
fe/src/components/devices/RemoteView.vue
Normal file
|
|
@ -0,0 +1,225 @@
|
|||
<!--
|
||||
Macrame is a program that enables the user to create keyboard macros and button panels.
|
||||
The macros are saved as simple JSON files and can be linked to the button panels. The panels can
|
||||
be created with HTML and CSS.
|
||||
|
||||
Copyright (C) 2025 Jesse Malotaux
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div class="server-overview">
|
||||
<AlertComp variant="info">
|
||||
<strong>This is a remote device.</strong>
|
||||
<em>UUID: {{ device.uuid() }} </em>
|
||||
</AlertComp>
|
||||
|
||||
<div class="grid gap-4 mcrm-block block__light">
|
||||
<h4 class="flex items-center justify-between gap-4 text-lg">
|
||||
<span class="flex gap-4"><IconServer />Server</span>
|
||||
<ButtonComp variant="primary" @click="checkServerStatus()"><IconReload /></ButtonComp>
|
||||
</h4>
|
||||
|
||||
<p>
|
||||
Connected to: <strong>{{ server.host }}</strong>
|
||||
</p>
|
||||
|
||||
<!-- Alerts -->
|
||||
<AlertComp v-if="server.status === 'authorized'" variant="success">Authorized</AlertComp>
|
||||
<AlertComp v-if="server.status === 'unlinked'" variant="warning">Not linked</AlertComp>
|
||||
<AlertComp v-if="server.status === 'unauthorized'" variant="info">
|
||||
<div class="grid gap-2">
|
||||
<strong>Access requested</strong>
|
||||
<ul class="mb-4">
|
||||
<li>
|
||||
Navigate to <em class="font-semibold">http://localhost:{{ server.port }}/devices</em>.
|
||||
</li>
|
||||
<li>
|
||||
<div class="inline-flex flex-wrap items-center gap-2 w-fit">
|
||||
Click on
|
||||
<span class="flex items-center gap-1 p-1 text-sm border rounded-sm">
|
||||
<IconLink class="size-4" /> Link device
|
||||
</span>
|
||||
</div>
|
||||
</li>
|
||||
<li>Enter the the pin shown on the desktop in the dialog that will appear.</li>
|
||||
</ul>
|
||||
<template v-if="server.link == 'checking'">
|
||||
<div class="grid grid-cols-[2rem_1fr] gap-2">
|
||||
<IconReload class="animate-spin" />
|
||||
Checking server for link...
|
||||
</div>
|
||||
</template>
|
||||
<template v-if="server.link === false">
|
||||
<ButtonComp variant="subtle" @click="pingLink()" class="w-fit">
|
||||
<IconReload />Check for server link
|
||||
</ButtonComp>
|
||||
</template>
|
||||
</div>
|
||||
</AlertComp>
|
||||
<ButtonComp
|
||||
variant="danger"
|
||||
v-if="server.status === 'authorized'"
|
||||
@click="disonnectFromServer()"
|
||||
>
|
||||
<IconPlugConnectedX />
|
||||
Disconnect
|
||||
</ButtonComp>
|
||||
</div>
|
||||
<DialogComp ref="linkPinDialog">
|
||||
<template #content>
|
||||
<div class="grid w-64 gap-4">
|
||||
<h3>Server link pin:</h3>
|
||||
<form class="grid gap-4" @submit.prevent="decryptKey()">
|
||||
<input
|
||||
ref="linkPinInput"
|
||||
class="input"
|
||||
id="input-pin"
|
||||
type="text"
|
||||
pattern="[0-9]{4}"
|
||||
v-model="server.inputPin"
|
||||
autocomplete="off"
|
||||
/>
|
||||
<ButtonComp variant="primary">Enter</ButtonComp>
|
||||
</form>
|
||||
</div>
|
||||
</template>
|
||||
</DialogComp>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
// TODO
|
||||
// - [Delete local key button]
|
||||
// - if not local key
|
||||
// - - if !checkAccess -> requestAccess -> put settings.json (go)
|
||||
// - - if checkAccess -> pingLink -> check for device.tmp (go)
|
||||
// - - if [devicePin] -> handshake -> save key local, close dialog, update server status
|
||||
|
||||
import { IconKey, IconLink, IconPlugConnectedX, IconReload, IconServer } from '@tabler/icons-vue'
|
||||
import AlertComp from '../base/AlertComp.vue'
|
||||
import ButtonComp from '../base/ButtonComp.vue'
|
||||
import { onMounted, onUpdated, reactive, ref } from 'vue'
|
||||
import { useDeviceStore } from '@/stores/device'
|
||||
import { deviceType, deviceModel, deviceVendor } from '@basitcodeenv/vue3-device-detect'
|
||||
import DialogComp from '../base/DialogComp.vue'
|
||||
import { AuthCall, decryptAES } from '@/services/EncryptService'
|
||||
import axios from 'axios'
|
||||
import { appUrl } from '@/services/ApiService'
|
||||
|
||||
const device = useDeviceStore()
|
||||
|
||||
const linkPinDialog = ref()
|
||||
const linkPinInput = ref()
|
||||
|
||||
const server = reactive({
|
||||
host: '',
|
||||
port: window.__CONFIG__.MCRM__PORT,
|
||||
status: false,
|
||||
link: false,
|
||||
inputPin: '',
|
||||
encryptedKey: '',
|
||||
key: '',
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
server.host = window.location.host
|
||||
})
|
||||
|
||||
onUpdated(() => {
|
||||
if (!server.status) checkServerStatus()
|
||||
|
||||
if (server.status === 'authorized' && server.inputPin) server.inputPin = ''
|
||||
})
|
||||
|
||||
async function checkServerStatus(request = true) {
|
||||
const status = await device.remoteCheckServerAccess()
|
||||
|
||||
server.status = status
|
||||
|
||||
if (status === 'unlinked' || status === 'unauthorized') {
|
||||
if (request) requestAccess()
|
||||
return true
|
||||
}
|
||||
|
||||
if (!device.key()) {
|
||||
server.status = 'unauthorized'
|
||||
return true
|
||||
}
|
||||
|
||||
const handshake = await device.remoteHandshake(device.key())
|
||||
|
||||
if (handshake) server.key = device.key()
|
||||
else {
|
||||
device.removeDeviceKey()
|
||||
server.status = 'unlinked'
|
||||
if (request) requestAccess()
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
function requestAccess() {
|
||||
let deviceName = `${deviceVendor() ? deviceVendor() : 'Unknown'} ${deviceVendor() ? deviceModel() : deviceType()}`
|
||||
|
||||
device.remoteRequestServerAccess(deviceName, deviceType()).then((data) => {
|
||||
if (data.data) (server.status = data.data), pingLink()
|
||||
})
|
||||
}
|
||||
|
||||
function pingLink() {
|
||||
server.link = 'checking'
|
||||
|
||||
device.remotePingLink((encryptedKey) => {
|
||||
server.link = true
|
||||
server.encryptedKey = encryptedKey
|
||||
|
||||
linkPinDialog.value.toggleDialog(true)
|
||||
linkPinInput.value.focus()
|
||||
})
|
||||
}
|
||||
|
||||
async function decryptKey() {
|
||||
const decryptedKey = decryptAES(server.inputPin, server.encryptedKey)
|
||||
|
||||
const handshake = await device.remoteHandshake(decryptedKey)
|
||||
|
||||
if (handshake) {
|
||||
device.setDeviceKey(decryptedKey)
|
||||
server.key = decryptedKey
|
||||
linkPinDialog.value.toggleDialog(false)
|
||||
server.status = 'authorized'
|
||||
}
|
||||
}
|
||||
|
||||
function disonnectFromServer() {
|
||||
axios.post(appUrl() + '/device/link/remove', AuthCall({ uuid: device.uuid() })).then((data) => {
|
||||
if (data.data) checkServerStatus(false)
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@reference "@/assets/main.css";
|
||||
|
||||
.server-overview {
|
||||
@apply grid
|
||||
gap-4
|
||||
content-start;
|
||||
}
|
||||
|
||||
#input-pin {
|
||||
}
|
||||
</style>
|
||||
254
fe/src/components/devices/ServerView.vue
Normal file
|
|
@ -0,0 +1,254 @@
|
|||
<!--
|
||||
Macrame is a program that enables the user to create keyboard macros and button panels.
|
||||
The macros are saved as simple JSON files and can be linked to the button panels. The panels can
|
||||
be created with HTML and CSS.
|
||||
|
||||
Copyright (C) 2025 Jesse Malotaux
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div class="device-overview">
|
||||
<AlertComp variant="info">
|
||||
<strong>This is a server!</strong>
|
||||
<em>UUID: {{ device.uuid() }} </em>
|
||||
</AlertComp>
|
||||
|
||||
<div class="flex flex-wrap items-start gap-4 mcrm-block block__light">
|
||||
<h4 class="flex items-center justify-between w-full gap-4 mb-4">
|
||||
<span class="flex gap-4">
|
||||
<IconDevices />{{ Object.keys(remote.devices).length }}
|
||||
{{ Object.keys(remote.devices).length == 1 ? 'Device' : 'Devices' }}
|
||||
</span>
|
||||
|
||||
<ButtonComp v-if="!remote.poll" variant="primary" @click="device.serverGetRemotes()"
|
||||
><IconReload
|
||||
/></ButtonComp>
|
||||
</h4>
|
||||
<template v-if="Object.keys(remote.devices).length > 0">
|
||||
<template v-for="(remoteDevice, id) in remote.devices" :key="id">
|
||||
<div class="mcrm-block block__dark block-size__sm w-64 grid !gap-4 content-start">
|
||||
<div class="grid gap-2">
|
||||
<h5 class="grid grid-cols-[auto_1fr] gap-2">
|
||||
<IconDeviceUnknown v-if="remoteDevice.settings.type == 'unknown'" />
|
||||
<IconDeviceMobile v-if="remoteDevice.settings.type == 'mobile'" />
|
||||
<IconDeviceTablet v-if="remoteDevice.settings.type == 'tablet'" />
|
||||
<IconDeviceDesktop v-if="remoteDevice.settings.type == 'desktop'" />
|
||||
<span class="w-full truncate">
|
||||
{{ remoteDevice.settings.name }}
|
||||
</span>
|
||||
</h5>
|
||||
<em>{{ id }}</em>
|
||||
</div>
|
||||
|
||||
<template v-if="remoteDevice.key">
|
||||
<AlertComp variant="success">Authorized</AlertComp>
|
||||
<ButtonComp variant="danger" @click="unlinkDevice(id)">
|
||||
<IconLinkOff />Unlink device
|
||||
</ButtonComp>
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<AlertComp variant="warning">Unauthorized</AlertComp>
|
||||
<ButtonComp variant="primary" @click="startLink(id)">
|
||||
<IconLink />Link device
|
||||
</ButtonComp>
|
||||
</template>
|
||||
|
||||
<template v-if="remote.pinlink.uuid == id">
|
||||
<AlertComp variant="info">One time pin: {{ remote.pinlink.pin }}</AlertComp>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<!-- <template v-else>
|
||||
<div class="grid w-full gap-4">
|
||||
<em class="text-slate-300">No remote devices</em>
|
||||
</div>
|
||||
</template> -->
|
||||
|
||||
<AccordionComp
|
||||
class="w-full mt-8 border-t border-t-white/50"
|
||||
title="How to connect a device?"
|
||||
:open="Object.keys(remote.devices).length == 0"
|
||||
>
|
||||
<div class="grid py-4">
|
||||
<ul class="space-y-2">
|
||||
<li>
|
||||
Scan the QR code with the remote device.
|
||||
<div class="grid gap-4 py-4 pl-6">
|
||||
<canvas ref="serverQr"></canvas>
|
||||
<p>
|
||||
Or manually type the IP address: <br />
|
||||
<strong>{{ server.ip }}/devices</strong>
|
||||
</p>
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
The device will automatically request access, if you see "Access requested" on the
|
||||
device.
|
||||
</li>
|
||||
<li v-if="!remote.poll">
|
||||
<div class="inline-flex items-center gap-2">
|
||||
Click the
|
||||
<span class="p-1 border rounded-sm"><IconReload class="size-4" /></span> to reload
|
||||
the devices.
|
||||
</div>
|
||||
</li>
|
||||
<li>
|
||||
<div class="inline-flex flex-wrap items-center gap-2 w-fit">
|
||||
Click on
|
||||
<span class="flex items-center gap-1 p-1 text-sm border rounded-sm">
|
||||
<IconLink class="size-4" /> Link device
|
||||
</span>
|
||||
A one-time-pin will be shown in a dialog.
|
||||
</div>
|
||||
</li>
|
||||
<li>Enter the pin on the remote device.</li>
|
||||
<li>
|
||||
Congratulations! You have linked a device! You can now start using panels on that
|
||||
device.
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</AccordionComp>
|
||||
|
||||
<DialogComp ref="pinDialog">
|
||||
<template #content>
|
||||
<div class="grid gap-4">
|
||||
<h3>Pin code</h3>
|
||||
<span class="font-mono text-4xl tracking-wide">{{ remote.pinlink.pin }}</span>
|
||||
</div>
|
||||
</template>
|
||||
</DialogComp>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted, onUpdated, reactive, ref } from 'vue'
|
||||
import AlertComp from '../base/AlertComp.vue'
|
||||
import { useDeviceStore } from '@/stores/device'
|
||||
import {
|
||||
IconDevices,
|
||||
IconDeviceDesktop,
|
||||
IconDeviceMobile,
|
||||
IconDeviceTablet,
|
||||
IconDeviceUnknown,
|
||||
IconLink,
|
||||
IconLinkOff,
|
||||
IconReload,
|
||||
} from '@tabler/icons-vue'
|
||||
import ButtonComp from '../base/ButtonComp.vue'
|
||||
import DialogComp from '../base/DialogComp.vue'
|
||||
import axios from 'axios'
|
||||
import { appUrl } from '@/services/ApiService'
|
||||
import AccordionComp from '../base/AccordionComp.vue'
|
||||
import QRCode from 'qrcode'
|
||||
|
||||
const device = useDeviceStore()
|
||||
|
||||
const pinDialog = ref()
|
||||
const serverQr = ref()
|
||||
|
||||
const server = reactive({
|
||||
ip: '',
|
||||
})
|
||||
|
||||
const remote = reactive({ devices: [], pinlink: false, poll: false })
|
||||
|
||||
onMounted(async () => {
|
||||
device.serverGetRemotes()
|
||||
|
||||
device.$subscribe((mutation, state) => {
|
||||
if (state.remote !== remote.devices) remote.devices = device.remote
|
||||
})
|
||||
|
||||
getIp()
|
||||
})
|
||||
|
||||
onUpdated(() => {
|
||||
getIp()
|
||||
|
||||
if (Object.keys(remote.devices).length == 0 && !remote.poll) {
|
||||
remote.poll = setInterval(() => {
|
||||
device.serverGetRemotes()
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
if (Object.keys(remote.devices).length > 0 && remote.poll) {
|
||||
clearInterval(remote.poll)
|
||||
remote.poll = false
|
||||
}
|
||||
})
|
||||
|
||||
async function getIp() {
|
||||
const serverIP = await device.serverGetIP()
|
||||
server.ip = serverIP
|
||||
|
||||
QRCode.toCanvas(serverQr.value, `${server.ip}/devices`, (error) => {
|
||||
if (error) console.log('QRCode error: ', error)
|
||||
})
|
||||
}
|
||||
|
||||
async function startLink(deviceUuid) {
|
||||
const pin = await device.serverStartLink(deviceUuid)
|
||||
|
||||
remote.pinlink = { uuid: deviceUuid, pin: pin }
|
||||
pinDialog.value.toggleDialog(true)
|
||||
|
||||
pollLink()
|
||||
|
||||
setTimeout(() => {
|
||||
resetPinLink()
|
||||
}, 60000)
|
||||
}
|
||||
|
||||
function pollLink() {
|
||||
const pollInterval = setInterval(() => {
|
||||
axios.post(appUrl() + '/device/link/poll', { uuid: remote.pinlink.uuid }).then((data) => {
|
||||
if (!data.data) {
|
||||
clearInterval(pollInterval)
|
||||
resetPinLink()
|
||||
device.serverGetRemotes()
|
||||
}
|
||||
})
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
function resetPinLink() {
|
||||
remote.pinlink = false
|
||||
if (pinDialog.value) pinDialog.value.toggleDialog(false)
|
||||
}
|
||||
|
||||
function unlinkDevice(id) {
|
||||
axios.post(appUrl() + '/device/link/remove', { uuid: id }).then((data) => {
|
||||
if (data.data) {
|
||||
device.serverGetRemotes()
|
||||
}
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@reference "@/assets/main.css";
|
||||
|
||||
.device-overview {
|
||||
@apply grid
|
||||
gap-4
|
||||
content-start;
|
||||
}
|
||||
</style>
|
||||
176
fe/src/components/form/FormSelect.vue
Normal file
|
|
@ -0,0 +1,176 @@
|
|||
<!--
|
||||
Macrame is a program that enables the user to create keyboard macros and button panels.
|
||||
The macros are saved as simple JSON files and can be linked to the button panels. The panels can
|
||||
be created with HTML and CSS.
|
||||
|
||||
Copyright (C) 2025 Jesse Malotaux
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div class="input-group form-select">
|
||||
<label v-if="label">
|
||||
{{ label }}
|
||||
</label>
|
||||
<div class="select__container">
|
||||
<template v-if="search">
|
||||
<div class="select__search-bar">
|
||||
<input
|
||||
type="search"
|
||||
ref="selectSearch"
|
||||
:list="`${name}-search__options`"
|
||||
v-model="select.search"
|
||||
@change="selectSearchValue($event)"
|
||||
:disabled="!select.searchActive"
|
||||
autocomplete="on"
|
||||
/>
|
||||
<datalist :id="`${name}-search__options`">
|
||||
<option v-for="option in select.options" :key="option.value" :value="option.value">
|
||||
{{ option.label }}
|
||||
</option>
|
||||
</datalist>
|
||||
<ButtonComp v-if="!select.searchActive" variant="ghost" size="sm" @click="initSearch">
|
||||
<IconSearch />
|
||||
</ButtonComp>
|
||||
<ButtonComp v-else variant="ghost" size="sm" @click="resetSearch">
|
||||
<IconSearchOff />
|
||||
</ButtonComp>
|
||||
</div>
|
||||
</template>
|
||||
<select :name="name" ref="selectEl" v-model="select.value" @change="changeSelect($event)">
|
||||
<option value="" disabled>- Select {{ label.toLocaleLowerCase() }} -</option>
|
||||
<option v-for="option in select.options" :key="option.value" :value="option.value">
|
||||
{{ option.label }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { IconSearch, IconSearchOff } from '@tabler/icons-vue'
|
||||
import { onMounted, onUpdated, reactive, ref } from 'vue'
|
||||
import ButtonComp from '../base/ButtonComp.vue'
|
||||
|
||||
const emit = defineEmits(['change'])
|
||||
|
||||
const props = defineProps({
|
||||
label: String,
|
||||
name: String,
|
||||
options: [Array, Object],
|
||||
search: Boolean,
|
||||
value: String,
|
||||
})
|
||||
|
||||
const select = reactive({
|
||||
options: [],
|
||||
search: '',
|
||||
searchActive: false,
|
||||
changed: false,
|
||||
value: '',
|
||||
})
|
||||
|
||||
const selectEl = ref(null)
|
||||
const selectSearch = ref(null)
|
||||
|
||||
onMounted(() => {
|
||||
setValue()
|
||||
|
||||
if (typeof props.options == 'object') select.options = Object.values(props.options)
|
||||
})
|
||||
|
||||
onUpdated(() => {
|
||||
setValue()
|
||||
})
|
||||
|
||||
const setValue = () => {
|
||||
if ((select.value == '' && props.value) || (!select.changed && props.value != select.value)) {
|
||||
select.value = props.value
|
||||
}
|
||||
|
||||
select.changed = false
|
||||
}
|
||||
|
||||
const initSearch = () => {
|
||||
select.searchActive = true
|
||||
select.search = ''
|
||||
selectEl.value.classList = 'search__is-active'
|
||||
setTimeout(() => {
|
||||
selectSearch.value.focus()
|
||||
}, 50)
|
||||
}
|
||||
|
||||
const resetSearch = () => {
|
||||
select.search = ''
|
||||
select.searchActive = false
|
||||
selectEl.value.classList = ''
|
||||
}
|
||||
|
||||
const selectSearchValue = (event) => {
|
||||
changeSelect(event)
|
||||
resetSearch()
|
||||
}
|
||||
|
||||
const changeSelect = (event) => {
|
||||
select.changed = true
|
||||
select.value = event.target.value
|
||||
|
||||
emit('change', select.value)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@reference "@/assets/main.css";
|
||||
|
||||
.select__container {
|
||||
@apply relative
|
||||
h-8;
|
||||
|
||||
select,
|
||||
.select__search-bar {
|
||||
@apply absolute top-0 h-8;
|
||||
}
|
||||
}
|
||||
.select__search-bar {
|
||||
@apply right-0
|
||||
grid
|
||||
grid-cols-[1fr_auto]
|
||||
items-center
|
||||
w-full
|
||||
pr-4
|
||||
z-10
|
||||
pointer-events-none;
|
||||
|
||||
button {
|
||||
@apply pointer-events-auto;
|
||||
}
|
||||
|
||||
input {
|
||||
@apply border-0 bg-transparent pointer-events-auto px-2 py-0 focus:outline-0;
|
||||
|
||||
&[disabled] {
|
||||
@apply pointer-events-none;
|
||||
}
|
||||
}
|
||||
datalist {
|
||||
@apply absolute
|
||||
top-full left-0;
|
||||
}
|
||||
}
|
||||
|
||||
select.search__is-active {
|
||||
@apply text-transparent;
|
||||
}
|
||||
</style>
|
||||
164
fe/src/components/macros/MacroOverview.vue
Normal file
|
|
@ -0,0 +1,164 @@
|
|||
<!--
|
||||
Macrame is a program that enables the user to create keyboard macros and button panels.
|
||||
The macros are saved as simple JSON files and can be linked to the button panels. The panels can
|
||||
be created with HTML and CSS.
|
||||
|
||||
Copyright (C) 2025 Jesse Malotaux
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div class="macro-overview mcrm-block block__dark">
|
||||
<h4 class="border-b-2 border-transparent">Saved Macros</h4>
|
||||
<div class="macro-overview__list">
|
||||
<LoadComp :loading="macros.loading" text="Loading macros..." />
|
||||
<div class="macro-item" v-for="(macro, i) in macros.list" :key="i">
|
||||
<ButtonComp
|
||||
:variant="macroRecorder.macroName === macro.name ? 'secondary' : 'dark'"
|
||||
class="overview__macro-open"
|
||||
size="sm"
|
||||
@click="macroRecorder.openMacro(macro.macroname, macro.name)"
|
||||
>
|
||||
<IconKeyboard /> <span>{{ macro.name }}</span>
|
||||
</ButtonComp>
|
||||
<div class="overview__macro-delete">
|
||||
<ButtonComp
|
||||
class="!text-red-500 hover:!text-red-300"
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
@click="startDelete(macro.name)"
|
||||
>
|
||||
<IconTrash />
|
||||
</ButtonComp>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<DialogComp ref="deleteDialog">
|
||||
<template #content>
|
||||
<div class="grid gap-2">
|
||||
<h4 class="pr-4">Are you sure you want to delete:</h4>
|
||||
<h3 class="mb-2 text-center text-sky-500">{{ macroToBeDeleted }}</h3>
|
||||
<div class="flex justify-between">
|
||||
<ButtonComp size="sm" variant="subtle" @click="deleteDialog.toggleDialog(false)">
|
||||
No
|
||||
</ButtonComp>
|
||||
<ButtonComp size="sm" variant="danger" @click="deleteMacro()">Yes</ButtonComp>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</DialogComp>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
// TODO
|
||||
// - delete macro
|
||||
|
||||
import { IconKeyboard, IconTrash } from '@tabler/icons-vue'
|
||||
import ButtonComp from '../base/ButtonComp.vue'
|
||||
import { onMounted, reactive, ref } from 'vue'
|
||||
import { GetMacroList } from '@/services/MacroService'
|
||||
import LoadComp from '../base/LoadComp.vue'
|
||||
import { useMacroRecorderStore } from '@/stores/macrorecorder'
|
||||
import DialogComp from '../base/DialogComp.vue'
|
||||
|
||||
const macros = reactive({
|
||||
loading: true,
|
||||
list: [],
|
||||
})
|
||||
|
||||
const macroRecorder = useMacroRecorderStore()
|
||||
|
||||
const macroToBeDeleted = ref('')
|
||||
const deleteDialog = ref()
|
||||
|
||||
onMounted(() => {
|
||||
loadMacroList()
|
||||
})
|
||||
|
||||
const loadMacroList = async () => {
|
||||
const list = await GetMacroList()
|
||||
macros.list = list
|
||||
macros.loading = false
|
||||
}
|
||||
|
||||
const startDelete = (macroFilename) => {
|
||||
macroToBeDeleted.value = macroFilename
|
||||
deleteDialog.value.toggleDialog(true)
|
||||
}
|
||||
|
||||
const deleteMacro = async () => {
|
||||
const resp = await macroRecorder.deleteMacro(macroToBeDeleted.value)
|
||||
|
||||
if (resp) {
|
||||
deleteDialog.value.toggleDialog(false)
|
||||
|
||||
if (macroToBeDeleted.value === macroRecorder.macroName) macroRecorder.resetMacro()
|
||||
|
||||
macroToBeDeleted.value = ''
|
||||
loadMacroList()
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@reference "@/assets/main.css";
|
||||
|
||||
.macro-overview {
|
||||
@apply relative
|
||||
grid
|
||||
grid-rows-[auto_1fr];
|
||||
|
||||
&::after {
|
||||
@apply content-['']
|
||||
absolute
|
||||
top-0
|
||||
left-full
|
||||
h-full
|
||||
w-px
|
||||
bg-slate-600;
|
||||
}
|
||||
|
||||
.macro-overview__list {
|
||||
@apply flex
|
||||
flex-col
|
||||
pr-1
|
||||
-mr-1
|
||||
gap-1
|
||||
h-[calc(100vh-11.7rem)]
|
||||
overflow-auto;
|
||||
}
|
||||
|
||||
.macro-item {
|
||||
@apply grid items-center grid-cols-[1fr_0fr] transition-[grid-template-columns] delay-0 duration-300;
|
||||
|
||||
&:hover {
|
||||
@apply grid-cols-[1fr_auto] delay-500;
|
||||
}
|
||||
|
||||
button.overview__macro-open {
|
||||
@apply w-full grid grid-cols-[1rem_1fr] justify-items-start;
|
||||
|
||||
span {
|
||||
@apply truncate w-full text-left;
|
||||
}
|
||||
}
|
||||
|
||||
div.overview__macro-delete {
|
||||
@apply grid overflow-hidden transition;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
106
fe/src/components/macros/MacroRecorder.vue
Normal file
|
|
@ -0,0 +1,106 @@
|
|||
<!--
|
||||
Macrame is a program that enables the user to create keyboard macros and button panels.
|
||||
The macros are saved as simple JSON files and can be linked to the button panels. The panels can
|
||||
be created with HTML and CSS.
|
||||
|
||||
Copyright (C) 2025 Jesse Malotaux
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div class="macro-recorder mcrm-block block__light">
|
||||
<div class="recorder-interface">
|
||||
<!-- Recorder buttons -->
|
||||
<RecorderHeader />
|
||||
|
||||
<!-- Recorder interface container -->
|
||||
<div
|
||||
:class="`recorder-interface__container ${macroRecorder.state.record && 'record'} ${macroRecorder.state.edit && 'edit'}`"
|
||||
>
|
||||
<!-- Shows the macro steps as kbd elements with delay and spacers-->
|
||||
<RecorderOutput />
|
||||
<!-- Input for recording macro steps -->
|
||||
<RecorderInput />
|
||||
</div>
|
||||
|
||||
<RecorderFooter />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import RecorderOutput from './parts/RecorderOutput.vue'
|
||||
import RecorderInput from './parts/RecorderInput.vue'
|
||||
|
||||
import { useMacroRecorderStore } from '@/stores/macrorecorder'
|
||||
import RecorderHeader from './parts/RecorderHeader.vue'
|
||||
import RecorderFooter from './parts/RecorderFooter.vue'
|
||||
|
||||
const macroRecorder = useMacroRecorderStore()
|
||||
</script>
|
||||
|
||||
<style>
|
||||
@reference "@/assets/main.css";
|
||||
|
||||
.macro-recorder {
|
||||
@apply h-full;
|
||||
}
|
||||
|
||||
.recorder-interface {
|
||||
@apply grid
|
||||
grid-rows-[auto_1fr_auto]
|
||||
gap-4
|
||||
h-full
|
||||
transition-[grid-template-rows];
|
||||
}
|
||||
|
||||
.recorder-interface__container {
|
||||
@apply relative
|
||||
w-full
|
||||
rounded-lg
|
||||
bg-slate-950/50
|
||||
border
|
||||
border-slate-600
|
||||
overflow-auto
|
||||
transition-colors;
|
||||
|
||||
&.record {
|
||||
@apply border-rose-300 bg-rose-400/10;
|
||||
}
|
||||
|
||||
&.edit {
|
||||
@apply border-sky-300 bg-sky-900/10;
|
||||
}
|
||||
}
|
||||
|
||||
#macro-name {
|
||||
@apply w-full
|
||||
bg-transparent
|
||||
py-0
|
||||
outline-0
|
||||
border-transparent
|
||||
border-b-slate-300
|
||||
focus:border-transparent
|
||||
focus:border-b-sky-400
|
||||
focus:bg-sky-400/10
|
||||
transition-colors
|
||||
text-lg
|
||||
rounded-none;
|
||||
}
|
||||
|
||||
.disabled {
|
||||
@apply opacity-50 pointer-events-none cursor-not-allowed;
|
||||
}
|
||||
</style>
|
||||
78
fe/src/components/macros/components/DelaySpan.vue
Normal file
|
|
@ -0,0 +1,78 @@
|
|||
<!--
|
||||
Macrame is a program that enables the user to create keyboard macros and button panels.
|
||||
The macros are saved as simple JSON files and can be linked to the button panels. The panels can
|
||||
be created with HTML and CSS.
|
||||
|
||||
Copyright (C) 2025 Jesse Malotaux
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-->
|
||||
|
||||
<template>
|
||||
<span :class="`delay ${active ? 'active' : ''} ${preset ? 'preset' : ''}`">
|
||||
<template v-if="value < 10000"> {{ value }} <i>ms</i> </template>
|
||||
<template v-else> >10 <i>s</i> </template>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { IconTimeDuration10 } from '@tabler/icons-vue'
|
||||
|
||||
defineProps({
|
||||
value: Number,
|
||||
active: Boolean,
|
||||
preset: Boolean,
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@reference "@/assets/main.css";
|
||||
|
||||
span.delay {
|
||||
@apply flex
|
||||
items-center
|
||||
px-2 py-1
|
||||
bg-slate-500
|
||||
border
|
||||
border-slate-400
|
||||
text-slate-950
|
||||
font-sans
|
||||
font-semibold
|
||||
rounded-sm
|
||||
text-sm
|
||||
cursor-default;
|
||||
|
||||
&.preset {
|
||||
@apply text-amber-400
|
||||
border-amber-300/80
|
||||
bg-amber-100/60;
|
||||
}
|
||||
|
||||
i {
|
||||
@apply pl-1
|
||||
font-normal
|
||||
not-italic
|
||||
opacity-80;
|
||||
}
|
||||
}
|
||||
|
||||
.edit span.delay {
|
||||
@apply cursor-pointer;
|
||||
|
||||
&:hover,
|
||||
&.active {
|
||||
@apply bg-lime-700 border-lime-500 text-lime-200;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
56
fe/src/components/macros/components/DeleteKeyDialog.vue
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
<!--
|
||||
Macrame is a program that enables the user to create keyboard macros and button panels.
|
||||
The macros are saved as simple JSON files and can be linked to the button panels. The panels can
|
||||
be created with HTML and CSS.
|
||||
|
||||
Copyright (C) 2025 Jesse Malotaux
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div id="delete-key-dialog" class="dialog__content">
|
||||
<h4 class="mb-4 text-slate-50">Delete key</h4>
|
||||
<div class="flex justify-center w-full mb-4">
|
||||
<MacroKey v-if="keyObj" :key-obj="keyObj" />
|
||||
</div>
|
||||
<p class="text-sm text-slate-300">Are you sure you want to delete this key?</p>
|
||||
<div class="flex justify-end gap-2 mt-6">
|
||||
<ButtonComp variant="danger" size="sm" @click="macroRecorder.deleteEditKey()">
|
||||
Delete key
|
||||
</ButtonComp>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import ButtonComp from '@/components/base/ButtonComp.vue'
|
||||
import { useMacroRecorderStore } from '@/stores/macrorecorder'
|
||||
import { onMounted, ref } from 'vue'
|
||||
import MacroKey from './MacroKey.vue'
|
||||
import { filterKey } from '@/services/MacroRecordService'
|
||||
|
||||
const macroRecorder = useMacroRecorderStore()
|
||||
|
||||
const keyObj = ref(null)
|
||||
|
||||
onMounted(() => {
|
||||
keyObj.value = filterKey(macroRecorder.getEditKey())
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@reference "@/assets/main.css";
|
||||
</style>
|
||||
'
|
||||
77
fe/src/components/macros/components/EditDelayDialog.vue
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
<!--
|
||||
Macrame is a program that enables the user to create keyboard macros and button panels.
|
||||
The macros are saved as simple JSON files and can be linked to the button panels. The panels can
|
||||
be created with HTML and CSS.
|
||||
|
||||
Copyright (C) 2025 Jesse Malotaux
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div id="edit-delay-dialog" class="dialog__content">
|
||||
<h4 class="mb-4 text-slate-50">Edit delay</h4>
|
||||
<div v-if="editable.delay.value" class="flex justify-center">
|
||||
<DelaySpan class="!text-lg" :value="editable.delay.value" />
|
||||
</div>
|
||||
<form class="grid gap-4 mt-6" submit.prevent>
|
||||
<div v-if="editable.newDelay.value">
|
||||
<input
|
||||
type="number"
|
||||
min="0"
|
||||
max="3600000"
|
||||
step="10"
|
||||
v-model="editable.newDelay.value"
|
||||
autofocus
|
||||
/>
|
||||
<span>ms</span>
|
||||
</div>
|
||||
<div class="flex justify-end">
|
||||
<ButtonComp variant="primary" size="sm" @click.prevent="changeDelay()">
|
||||
Change delay
|
||||
</ButtonComp>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import ButtonComp from '@/components/base/ButtonComp.vue'
|
||||
import { reactive, onMounted } from 'vue'
|
||||
import { useMacroRecorderStore } from '@/stores/macrorecorder'
|
||||
import DelaySpan from './DelaySpan.vue'
|
||||
|
||||
const macroRecorder = useMacroRecorderStore()
|
||||
|
||||
const editable = reactive({
|
||||
delay: {},
|
||||
newDelay: { value: 0 },
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
editable.delay = macroRecorder.getEditDelay()
|
||||
editable.newDelay.value = editable.delay.value
|
||||
})
|
||||
|
||||
const changeDelay = () => {
|
||||
if (!editable.newDelay.value) return
|
||||
|
||||
macroRecorder.recordStep(editable.newDelay.value, false, macroRecorder.state.editDelay)
|
||||
macroRecorder.state.editDelay = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@reference "@/assets/main.css";
|
||||
</style>
|
||||
124
fe/src/components/macros/components/EditKeyDialog.vue
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
<!--
|
||||
Macrame is a program that enables the user to create keyboard macros and button panels.
|
||||
The macros are saved as simple JSON files and can be linked to the button panels. The panels can
|
||||
be created with HTML and CSS.
|
||||
|
||||
Copyright (C) 2025 Jesse Malotaux
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div id="edit-key-dialog" class="dialog__content">
|
||||
<h4 class="text-slate-50 mb-4">Press a key</h4>
|
||||
<div class="flex justify-center" @click="$refs.newKeyInput.focus()">
|
||||
<MacroKey
|
||||
v-if="editable.key.keyObj"
|
||||
:key-obj="editable.key.keyObj"
|
||||
:direction="editable.key.direction"
|
||||
/>
|
||||
|
||||
<template v-if="typeof editable.newKey.keyObj === 'object'">
|
||||
<span class="px-4 flex items-center text-white"> >>> </span>
|
||||
<MacroKey :key-obj="editable.newKey.keyObj" :direction="editable.newKey.direction" />
|
||||
</template>
|
||||
</div>
|
||||
<form class="grid gap-4" submit.prevent>
|
||||
<input
|
||||
class="size-0 opacity-0"
|
||||
type="text"
|
||||
min="0"
|
||||
max="1"
|
||||
ref="newKeyInput"
|
||||
placeholder="New key"
|
||||
autofocus
|
||||
@keydown.prevent="handleNewKey($event)"
|
||||
/>
|
||||
<div class="flex gap-2 justify-center">
|
||||
<ButtonComp
|
||||
variant="secondary"
|
||||
:class="editable.newKey.direction === 'down' ? 'selected' : ''"
|
||||
size="sm"
|
||||
@click.prevent="handleNewDirection('down')"
|
||||
>
|
||||
↓ Down
|
||||
</ButtonComp>
|
||||
<ButtonComp
|
||||
variant="secondary"
|
||||
:class="editable.newKey.direction === 'up' ? 'selected' : ''"
|
||||
size="sm"
|
||||
@click.prevent="handleNewDirection('up')"
|
||||
>
|
||||
↑ Up
|
||||
</ButtonComp>
|
||||
</div>
|
||||
<div class="flex justify-end">
|
||||
<ButtonComp variant="primary" size="sm" @click.prevent="changeKey()">
|
||||
Change key
|
||||
</ButtonComp>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import MacroKey from './MacroKey.vue'
|
||||
import ButtonComp from '@/components/base/ButtonComp.vue'
|
||||
import { useMacroRecorderStore } from '@/stores/macrorecorder'
|
||||
import { filterKey } from '@/services/MacroRecordService'
|
||||
|
||||
import { reactive, ref, onMounted } from 'vue'
|
||||
|
||||
const editable = reactive({
|
||||
key: {},
|
||||
newKey: {},
|
||||
})
|
||||
|
||||
const macroRecorder = useMacroRecorderStore()
|
||||
|
||||
const newKeyInput = ref(null)
|
||||
|
||||
onMounted(() => {
|
||||
editable.key = macroRecorder.getEditKey()
|
||||
editable.newKey.direction = editable.key.direction
|
||||
})
|
||||
|
||||
const handleNewKey = (e) => {
|
||||
editable.newKey.e = e
|
||||
editable.newKey.keyObj = filterKey(e)
|
||||
}
|
||||
|
||||
const handleNewDirection = (direction) => {
|
||||
editable.newKey.direction = direction
|
||||
editable.newKey.keyObj = filterKey(editable.key)
|
||||
}
|
||||
|
||||
const changeKey = () => {
|
||||
macroRecorder.recordStep(
|
||||
editable.newKey.e,
|
||||
editable.newKey.direction,
|
||||
macroRecorder.state.editKey,
|
||||
)
|
||||
|
||||
macroRecorder.state.editKey = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@reference "@/assets/main.css";
|
||||
|
||||
button.selected {
|
||||
@apply ring-2 ring-offset-1 ring-sky-500 bg-sky-500;
|
||||
}
|
||||
</style>
|
||||
90
fe/src/components/macros/components/FixedDelayMenu.vue
Normal file
|
|
@ -0,0 +1,90 @@
|
|||
<!--
|
||||
Macrame is a program that enables the user to create keyboard macros and button panels.
|
||||
The macros are saved as simple JSON files and can be linked to the button panels. The panels can
|
||||
be created with HTML and CSS.
|
||||
|
||||
Copyright (C) 2025 Jesse Malotaux
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-->
|
||||
|
||||
<template>
|
||||
<ContextMenu ref="ctxtMenu">
|
||||
<template #trigger>
|
||||
<ButtonComp variant="secondary" size="sm"> <IconTimeDuration15 />Fixed delay </ButtonComp>
|
||||
</template>
|
||||
<template #content>
|
||||
<ul>
|
||||
<li @click="changeDelay(0)">0ms</li>
|
||||
<li @click="changeDelay(15)">15ms</li>
|
||||
<li @click="changeDelay(50)">50ms</li>
|
||||
<li @click="changeDelay(100)">100ms</li>
|
||||
<li>
|
||||
<DialogComp>
|
||||
<template #trigger>
|
||||
<span>Custom delay</span>
|
||||
</template>
|
||||
|
||||
<template #content>
|
||||
<h4 class="text-slate-50 mb-4">Custom delay</h4>
|
||||
<form
|
||||
class="grid gap-4 w-44"
|
||||
@submit.prevent="changeDelay(parseInt($refs.customDelayInput.value))"
|
||||
>
|
||||
<div>
|
||||
<input
|
||||
type="number"
|
||||
step="10"
|
||||
min="0"
|
||||
max="3600000"
|
||||
ref="customDelayInput"
|
||||
placeholder="100"
|
||||
/>
|
||||
<span>ms</span>
|
||||
</div>
|
||||
<div class="flex justify-end">
|
||||
<ButtonComp variant="primary" size="sm">Set custom delay</ButtonComp>
|
||||
</div>
|
||||
</form>
|
||||
</template>
|
||||
</DialogComp>
|
||||
</li>
|
||||
</ul>
|
||||
</template>
|
||||
</ContextMenu>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import ContextMenu from '@/components/base/ContextMenu.vue'
|
||||
import { IconTimeDuration15 } from '@tabler/icons-vue'
|
||||
import ButtonComp from '@/components/base/ButtonComp.vue'
|
||||
import DialogComp from '@/components/base/DialogComp.vue'
|
||||
|
||||
import { useMacroRecorderStore } from '@/stores/macrorecorder'
|
||||
|
||||
import { ref } from 'vue'
|
||||
|
||||
const macroRecorder = useMacroRecorderStore()
|
||||
|
||||
const ctxtMenu = ref()
|
||||
|
||||
function changeDelay(num) {
|
||||
macroRecorder.changeDelay(num)
|
||||
ctxtMenu.value.toggle()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@reference "@/assets/main.css";
|
||||
</style>
|
||||
155
fe/src/components/macros/components/InsertKeyDialog.vue
Normal file
|
|
@ -0,0 +1,155 @@
|
|||
<!--
|
||||
Macrame is a program that enables the user to create keyboard macros and button panels.
|
||||
The macros are saved as simple JSON files and can be linked to the button panels. The panels can
|
||||
be created with HTML and CSS.
|
||||
|
||||
Copyright (C) 2025 Jesse Malotaux
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div id="insert-key-dialog" class="dialog__content w-96">
|
||||
<h4 class="text-slate-50 mb-4">Insert key {{ position }}</h4>
|
||||
<p v-if="inputFocus" class="text-center">[Press a key]</p>
|
||||
<input
|
||||
class="size-0 opacity-0"
|
||||
type="text"
|
||||
min="0"
|
||||
max="1"
|
||||
ref="insertKeyInput"
|
||||
placeholder="New key"
|
||||
@focusin="inputFocus = true"
|
||||
@focusout="inputFocus = false"
|
||||
@keydown.prevent="handleInsertKey($event)"
|
||||
autofocus
|
||||
/>
|
||||
<div class="insert-output" :class="position == 'before' ? 'flex-row-reverse' : ''">
|
||||
<MacroKey v-if="keyObjs.selected" :key-obj="keyObjs.selected" />
|
||||
<hr class="spacer" />
|
||||
<DelaySpan :preset="true" :value="10" />
|
||||
<hr class="spacer" />
|
||||
<MacroKey
|
||||
v-if="keyObjs.insert"
|
||||
class="insert"
|
||||
:key-obj="keyObjs.insert"
|
||||
:direction="keyObjs.insertDirection"
|
||||
@click="insertKeyInput.focus()"
|
||||
/>
|
||||
<MacroKey v-if="!keyObjs.insert" :empty="true" @click="insertKeyInput.focus()" />
|
||||
<template v-if="keyObjs.adjacentDelay">
|
||||
<hr class="spacer" />
|
||||
<DelaySpan :value="keyObjs.adjacentDelay.value" />
|
||||
</template>
|
||||
<template v-if="keyObjs.adjacent">
|
||||
<hr class="spacer" />
|
||||
<MacroKey :key-obj="keyObjs.adjacent" />
|
||||
</template>
|
||||
</div>
|
||||
<div class="insert-key__direction">
|
||||
<ButtonComp
|
||||
variant="secondary"
|
||||
:class="keyObjs.insertDirection === 'down' ? 'selected' : ''"
|
||||
size="sm"
|
||||
@click.prevent="keyObjs.insertDirection = 'down'"
|
||||
>
|
||||
↓ Down
|
||||
</ButtonComp>
|
||||
<ButtonComp
|
||||
variant="secondary"
|
||||
:class="keyObjs.insertDirection === 'up' ? 'selected' : ''"
|
||||
size="sm"
|
||||
@click.prevent="keyObjs.insertDirection = 'up'"
|
||||
>
|
||||
↑ Up
|
||||
</ButtonComp>
|
||||
</div>
|
||||
<div class="flex justify-end">
|
||||
<ButtonComp variant="primary" size="sm" @click="insertKey()">Insert key</ButtonComp>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import MacroKey from './MacroKey.vue'
|
||||
import DelaySpan from './DelaySpan.vue'
|
||||
import ButtonComp from '@/components/base/ButtonComp.vue'
|
||||
|
||||
import { onMounted, reactive, ref } from 'vue'
|
||||
import { useMacroRecorderStore } from '@/stores/macrorecorder'
|
||||
import { filterKey } from '@/services/MacroRecordService'
|
||||
|
||||
const props = defineProps({
|
||||
position: String,
|
||||
})
|
||||
|
||||
const macroRecorder = useMacroRecorderStore()
|
||||
|
||||
const keyObjs = reactive({
|
||||
selected: null,
|
||||
insert: null,
|
||||
insertEvent: null,
|
||||
insertDirection: 'down',
|
||||
adjacent: null,
|
||||
adjacentDelay: null,
|
||||
adjacentDelayIndex: null,
|
||||
})
|
||||
|
||||
const insertKeyInput = ref(null)
|
||||
const inputFocus = ref(false)
|
||||
|
||||
onMounted(() => {
|
||||
keyObjs.selected = filterKey(macroRecorder.getEditKey())
|
||||
|
||||
const adjacentKey = macroRecorder.getAdjacentKey(props.position, true)
|
||||
if (adjacentKey) keyObjs.adjacent = filterKey(adjacentKey.key)
|
||||
if (adjacentKey.delay) {
|
||||
keyObjs.adjacentDelay = adjacentKey.delay
|
||||
keyObjs.adjacentDelayIndex = adjacentKey.delayIndex
|
||||
}
|
||||
})
|
||||
|
||||
const handleInsertKey = (e) => {
|
||||
keyObjs.insert = filterKey(e)
|
||||
keyObjs.insertEvent = e
|
||||
}
|
||||
|
||||
const insertKey = () => {
|
||||
macroRecorder.insertKey(keyObjs.insertEvent, keyObjs.insertDirection, keyObjs.adjacentDelayIndex)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@reference "@/assets/main.css";
|
||||
|
||||
.insert-output {
|
||||
@apply flex
|
||||
justify-center
|
||||
items-center
|
||||
w-full
|
||||
mb-4;
|
||||
}
|
||||
.insert-key__direction {
|
||||
@apply flex
|
||||
justify-center
|
||||
gap-2
|
||||
mt-6;
|
||||
}
|
||||
button.selected {
|
||||
@apply bg-sky-500
|
||||
ring-2
|
||||
ring-offset-1
|
||||
ring-sky-500;
|
||||
}
|
||||
</style>
|
||||
130
fe/src/components/macros/components/MacroKey.vue
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
<!--
|
||||
Macrame is a program that enables the user to create keyboard macros and button panels.
|
||||
The macros are saved as simple JSON files and can be linked to the button panels. The panels can
|
||||
be created with HTML and CSS.
|
||||
|
||||
Copyright (C) 2025 Jesse Malotaux
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-->
|
||||
|
||||
<template>
|
||||
<kbd :class="`${active ? 'active' : ''} ${empty ? 'empty' : ''}`">
|
||||
<template v-if="keyObj">
|
||||
<sup v-if="keyObj.loc">
|
||||
{{ keyObj.loc }}
|
||||
</sup>
|
||||
<span :innerHTML="keyObj.str" />
|
||||
<span class="dir">{{ dir.value === 'down' ? '↓' : '↑' }}</span>
|
||||
</template>
|
||||
|
||||
<template v-else-if="empty">
|
||||
<span>[ ]</span>
|
||||
</template>
|
||||
</kbd>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { onMounted, onUpdated, reactive } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
keyObj: Object,
|
||||
direction: String,
|
||||
active: Boolean,
|
||||
empty: Boolean,
|
||||
})
|
||||
|
||||
const dir = reactive({
|
||||
value: false,
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
if (props.empty) return
|
||||
setDirection()
|
||||
})
|
||||
|
||||
onUpdated(() => {
|
||||
setDirection()
|
||||
})
|
||||
|
||||
const setDirection = () => {
|
||||
if (props.direction) dir.value = props.direction
|
||||
else dir.value = props.keyObj.direction
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
@reference "@/assets/main.css";
|
||||
|
||||
kbd {
|
||||
@apply flex
|
||||
items-center
|
||||
gap-2
|
||||
pl-4 pr-2 py-1
|
||||
h-9
|
||||
bg-slate-700
|
||||
font-mono
|
||||
font-bold
|
||||
text-lg
|
||||
text-white
|
||||
whitespace-nowrap
|
||||
uppercase
|
||||
rounded-md
|
||||
border
|
||||
border-slate-500
|
||||
transition-all
|
||||
shadow-slate-500;
|
||||
box-shadow: 0 0.2rem 0 0.2rem var(--tw-shadow-color);
|
||||
|
||||
&:has(sup) {
|
||||
@apply pl-2;
|
||||
}
|
||||
|
||||
sup {
|
||||
@apply text-slate-200 text-xs font-light mt-1;
|
||||
}
|
||||
|
||||
span.dir {
|
||||
@apply text-slate-200 pl-1;
|
||||
}
|
||||
|
||||
&.empty {
|
||||
@apply pl-3 pr-3
|
||||
bg-sky-400/50
|
||||
border-sky-300
|
||||
shadow-sky-600
|
||||
tracking-widest
|
||||
cursor-pointer;
|
||||
}
|
||||
&.insert {
|
||||
@apply bg-yellow-500/50
|
||||
border-yellow-300
|
||||
shadow-yellow-600
|
||||
cursor-pointer;
|
||||
}
|
||||
}
|
||||
|
||||
:has(kdb):not(.edit) kbd {
|
||||
@apply pointer-events-none cursor-default;
|
||||
}
|
||||
|
||||
.edit kbd {
|
||||
@apply cursor-pointer pointer-events-auto;
|
||||
|
||||
&:hover,
|
||||
&.active {
|
||||
@apply bg-sky-900 border-sky-400 shadow-sky-700;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -0,0 +1,80 @@
|
|||
<!--
|
||||
Macrame is a program that enables the user to create keyboard macros and button panels.
|
||||
The macros are saved as simple JSON files and can be linked to the button panels. The panels can
|
||||
be created with HTML and CSS.
|
||||
|
||||
Copyright (C) 2025 Jesse Malotaux
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div id="validation-error__dialog" class="dialog__content">
|
||||
<h4 class="mb-4 text-slate-50">There's an error in your macro</h4>
|
||||
|
||||
<div class="grid gap-4" v-if="(errors && errors.up.length > 0) || errors.down.length > 0">
|
||||
<div v-if="errors.down.length > 0">
|
||||
<p>
|
||||
The following keys have been <strong>pressed</strong> down, but
|
||||
<strong>not released</strong>.
|
||||
</p>
|
||||
<ul>
|
||||
<li v-for="key in errors.down" :key="key">{{ key.toUpperCase() }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div v-if="errors.up.length > 0">
|
||||
<p>
|
||||
The following keys have been <strong>released</strong>, but
|
||||
<strong>not pressed</strong> down.
|
||||
</p>
|
||||
<ul>
|
||||
<li v-for="key in errors.up" :key="key">{{ key.toUpperCase() }}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex justify-end mt-4">
|
||||
<ButtonComp size="sm" variant="danger" @click="macroRecorder.state.validationErrors = false">
|
||||
Close
|
||||
</ButtonComp>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import ButtonComp from '@/components/base/ButtonComp.vue'
|
||||
import { useMacroRecorderStore } from '@/stores/macrorecorder'
|
||||
import { onMounted, reactive } from 'vue'
|
||||
|
||||
const macroRecorder = useMacroRecorderStore()
|
||||
|
||||
const errors = reactive({
|
||||
up: [],
|
||||
down: [],
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
macroRecorder.$subscribe((mutation) => {
|
||||
if (mutation.events && mutation.events.key == 'validationErrors') {
|
||||
errors.up = mutation.events.newValue !== false ? macroRecorder.state.validationErrors.up : []
|
||||
errors.down =
|
||||
mutation.events.newValue !== false ? macroRecorder.state.validationErrors.down : []
|
||||
}
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@reference "@/assets/main.css";
|
||||
</style>
|
||||
142
fe/src/components/macros/parts/EditDialogs.vue
Normal file
|
|
@ -0,0 +1,142 @@
|
|||
<!--
|
||||
Macrame is a program that enables the user to create keyboard macros and button panels.
|
||||
The macros are saved as simple JSON files and can be linked to the button panels. The panels can
|
||||
be created with HTML and CSS.
|
||||
|
||||
Copyright (C) 2025 Jesse Malotaux
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div class="macro-edit__dialogs" v-if="macroRecorder.state.edit !== false">
|
||||
<div
|
||||
class="flex gap-2"
|
||||
v-if="macroRecorder.state.editKey !== false && typeof macroRecorder.getEditKey() === 'object'"
|
||||
>
|
||||
<ContextMenu ref="ctxtMenu">
|
||||
<template #trigger>
|
||||
<ButtonComp variant="dark" size="sm"> <IconPlus /> Insert </ButtonComp>
|
||||
</template>
|
||||
<template #content>
|
||||
<ul>
|
||||
<li @click="insert.position = 'before'"><IconArrowLeftCircle /> Before</li>
|
||||
<li @click="insert.position = 'after'"><IconArrowRightCircle /> After</li>
|
||||
</ul>
|
||||
</template>
|
||||
</ContextMenu>
|
||||
|
||||
<DialogComp
|
||||
v-if="insert.position !== null"
|
||||
:open="insert.position !== null"
|
||||
@on-open="onOpenDialog"
|
||||
@on-close="onCloseDialog"
|
||||
>
|
||||
<template #content>
|
||||
<InsertKeyDialog :position="insert.position" />
|
||||
</template>
|
||||
</DialogComp>
|
||||
|
||||
<DialogComp
|
||||
:id="`edit-key-${macroRecorder.state.editKey}`"
|
||||
@on-open="onOpenDialog"
|
||||
@on-close="onCloseDialog"
|
||||
>
|
||||
<template #trigger>
|
||||
<ButtonComp variant="secondary" size="sm"> <IconPencil />Edit </ButtonComp>
|
||||
</template>
|
||||
<template #content>
|
||||
<EditKeyDialog />
|
||||
</template>
|
||||
</DialogComp>
|
||||
|
||||
<DialogComp @on-open="onOpenDialog" @on-close="onCloseDialog">
|
||||
<template #trigger>
|
||||
<ButtonComp size="sm" variant="danger"> <IconTrash />Delete </ButtonComp>
|
||||
</template>
|
||||
<template #content>
|
||||
<DeleteKeyDialog />
|
||||
</template>
|
||||
</DialogComp>
|
||||
</div>
|
||||
<DialogComp
|
||||
v-if="
|
||||
macroRecorder.state.editDelay !== false && typeof macroRecorder.getEditDelay() === 'object'
|
||||
"
|
||||
@on-open="onOpenDialog"
|
||||
@on-close="onCloseDialog"
|
||||
>
|
||||
<template #trigger>
|
||||
<ButtonComp variant="secondary" size="sm"> <IconAlarm />Edit </ButtonComp>
|
||||
</template>
|
||||
<template #content>
|
||||
<EditDelayDialog />
|
||||
</template>
|
||||
</DialogComp>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import {
|
||||
IconAlarm,
|
||||
IconArrowLeftCircle,
|
||||
IconArrowRightCircle,
|
||||
IconPencil,
|
||||
IconPlus,
|
||||
IconTrash,
|
||||
} from '@tabler/icons-vue'
|
||||
import DialogComp from '@/components/base/DialogComp.vue'
|
||||
import ButtonComp from '@/components/base/ButtonComp.vue'
|
||||
|
||||
import { useMacroRecorderStore } from '@/stores/macrorecorder'
|
||||
import EditKeyDialog from '../components/EditKeyDialog.vue'
|
||||
import EditDelayDialog from '../components/EditDelayDialog.vue'
|
||||
import DeleteKeyDialog from '../components/DeleteKeyDialog.vue'
|
||||
import ContextMenu from '@/components/base/ContextMenu.vue'
|
||||
import InsertKeyDialog from '../components/InsertKeyDialog.vue'
|
||||
import { onMounted, reactive, ref } from 'vue'
|
||||
|
||||
const macroRecorder = useMacroRecorderStore()
|
||||
|
||||
const insert = reactive({ position: null })
|
||||
const ctxtMenu = ref()
|
||||
|
||||
onMounted(() => {
|
||||
macroRecorder.$subscribe((mutation) => {
|
||||
if (mutation.events && mutation.events.key == 'editKey' && mutation.events.newValue === false) {
|
||||
insert.position = null
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
function onOpenDialog() {
|
||||
if (insert.position !== null) ctxtMenu.value.toggle()
|
||||
}
|
||||
function onCloseDialog() {
|
||||
macroRecorder.state.editKey = false
|
||||
macroRecorder.state.editDelay = false
|
||||
|
||||
insert.position = null
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@reference "@/assets/main.css";
|
||||
|
||||
.macro-edit__dialogs {
|
||||
@apply flex
|
||||
flex-grow
|
||||
justify-end;
|
||||
}
|
||||
</style>
|
||||
111
fe/src/components/macros/parts/RecorderFooter.vue
Normal file
|
|
@ -0,0 +1,111 @@
|
|||
<!--
|
||||
Macrame is a program that enables the user to create keyboard macros and button panels.
|
||||
The macros are saved as simple JSON files and can be linked to the button panels. The panels can
|
||||
be created with HTML and CSS.
|
||||
|
||||
Copyright (C) 2025 Jesse Malotaux
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div class="macro-recorder__footer">
|
||||
<ButtonComp
|
||||
v-if="macroRecorder.steps.length > 0"
|
||||
variant="danger"
|
||||
@click="macroRecorder.reset()"
|
||||
>
|
||||
<IconRestore /> Reset
|
||||
</ButtonComp>
|
||||
|
||||
<DialogComp ref="errorDialog">
|
||||
<template #content>
|
||||
<ValidationErrorDialog />
|
||||
</template>
|
||||
</DialogComp>
|
||||
<DialogComp ref="overwriteDialog">
|
||||
<template #content>
|
||||
<div class="grid gap-2">
|
||||
<h4 class="pr-4">Are you sure you want to overwrite:</h4>
|
||||
<h3 class="mb-2 text-center text-sky-500">{{ macroRecorder.macroName }}</h3>
|
||||
<div class="flex justify-between">
|
||||
<ButtonComp size="sm" variant="subtle" @click="overwriteDialog.toggleDialog(false)"
|
||||
>No</ButtonComp
|
||||
>
|
||||
<ButtonComp size="sm" variant="primary" @click="saveMacro()">Yes</ButtonComp>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</DialogComp>
|
||||
|
||||
<ButtonComp
|
||||
v-if="macroRecorder.steps.length > 0"
|
||||
:disabled="macroRecorder.state.record || macroRecorder.state.edit"
|
||||
variant="success"
|
||||
@click="startCheck()"
|
||||
>
|
||||
<IconDeviceFloppy />
|
||||
Save
|
||||
</ButtonComp>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import ButtonComp from '@/components/base/ButtonComp.vue'
|
||||
import { IconDeviceFloppy, IconRestore } from '@tabler/icons-vue'
|
||||
|
||||
import { useMacroRecorderStore } from '@/stores/macrorecorder'
|
||||
import DialogComp from '@/components/base/DialogComp.vue'
|
||||
import ValidationErrorDialog from '../components/ValidationErrorDialog.vue'
|
||||
import { onMounted, ref } from 'vue'
|
||||
|
||||
const macroRecorder = useMacroRecorderStore()
|
||||
|
||||
const errorDialog = ref()
|
||||
const overwriteDialog = ref()
|
||||
|
||||
onMounted(() => {
|
||||
macroRecorder.$subscribe((mutation) => {
|
||||
if (mutation.events && mutation.events.key == 'validationErrors') {
|
||||
errorDialog.value.toggleDialog(mutation.events.newValue !== false)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
const startCheck = async () => {
|
||||
const checkResp = await macroRecorder.checkMacro()
|
||||
|
||||
if (checkResp) overwriteDialog.value.toggleDialog(true)
|
||||
else saveMacro()
|
||||
}
|
||||
|
||||
const saveMacro = async () => {
|
||||
overwriteDialog.value.toggleDialog(false)
|
||||
|
||||
const saveResp = await macroRecorder.saveMacro()
|
||||
|
||||
if (!saveResp) errorDialog.value.toggleDialog(true)
|
||||
else window.location.reload()
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@reference "@/assets/main.css";
|
||||
|
||||
.macro-recorder__footer {
|
||||
@apply flex
|
||||
justify-between
|
||||
gap-2;
|
||||
}
|
||||
</style>
|
||||
124
fe/src/components/macros/parts/RecorderHeader.vue
Normal file
|
|
@ -0,0 +1,124 @@
|
|||
<!--
|
||||
Macrame is a program that enables the user to create keyboard macros and button panels.
|
||||
The macros are saved as simple JSON files and can be linked to the button panels. The panels can
|
||||
be created with HTML and CSS.
|
||||
|
||||
Copyright (C) 2025 Jesse Malotaux
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div class="macro-recorder__header">
|
||||
<div class="w-full grid grid-cols-[auto_1fr_auto] gap-2">
|
||||
<h4 class="">Name:</h4>
|
||||
|
||||
<input
|
||||
id="macro-name"
|
||||
type="text"
|
||||
@input.prevent="changeName($event.target.value)"
|
||||
:value="macroName"
|
||||
placeholder="New macro"
|
||||
/>
|
||||
<div :class="`recording__buttons ${!nameSet || macroRecorder.state.edit ? 'disabled' : ''}`">
|
||||
{{ macroRecorder.name }}
|
||||
<ButtonComp
|
||||
v-if="!macroRecorder.state.record"
|
||||
variant="primary"
|
||||
@click="macroRecorder.state.record = true"
|
||||
>
|
||||
<IconPlayerRecordFilled class="text-red-500" />Record
|
||||
</ButtonComp>
|
||||
<ButtonComp
|
||||
v-if="macroRecorder.state.record"
|
||||
variant="danger"
|
||||
@click="macroRecorder.state.record = false"
|
||||
>
|
||||
<IconPlayerStopFilled class="text-white" />Stop
|
||||
</ButtonComp>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="macroRecorder.steps.length > 0"
|
||||
:class="`edit__buttons ${macroRecorder.state.record ? 'disabled' : ''}`"
|
||||
>
|
||||
<div>
|
||||
<ButtonComp
|
||||
v-if="!macroRecorder.state.edit"
|
||||
variant="secondary"
|
||||
@click="macroRecorder.state.edit = true"
|
||||
>
|
||||
<IconPencil />Edit
|
||||
</ButtonComp>
|
||||
<ButtonComp
|
||||
v-if="macroRecorder.state.edit"
|
||||
variant="danger"
|
||||
@click="macroRecorder.resetEdit()"
|
||||
>
|
||||
<IconPlayerStopFilled />Stop
|
||||
</ButtonComp>
|
||||
</div>
|
||||
|
||||
<FixedDelayMenu v-if="macroRecorder.state.edit" />
|
||||
|
||||
<EditDialogs />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { IconPencil, IconPlayerRecordFilled, IconPlayerStopFilled } from '@tabler/icons-vue'
|
||||
import ButtonComp from '@/components/base/ButtonComp.vue'
|
||||
import FixedDelayMenu from '../components/FixedDelayMenu.vue'
|
||||
|
||||
import { useMacroRecorderStore } from '@/stores/macrorecorder'
|
||||
import EditDialogs from './EditDialogs.vue'
|
||||
import { computed, onUpdated, ref } from 'vue'
|
||||
|
||||
const macroRecorder = useMacroRecorderStore()
|
||||
|
||||
const macroName = computed(() => macroRecorder.macroName)
|
||||
|
||||
const nameSet = ref(false)
|
||||
|
||||
onUpdated(() => {
|
||||
nameSet.value = macroName.value && macroName.value.length > 0
|
||||
})
|
||||
|
||||
function changeName(name) {
|
||||
macroRecorder.changeName(name)
|
||||
nameSet.value = name.length > 0
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@reference "@/assets/main.css";
|
||||
|
||||
.macro-recorder__header {
|
||||
@apply grid
|
||||
gap-4
|
||||
w-full;
|
||||
|
||||
.edit__buttons {
|
||||
@apply flex
|
||||
justify-between
|
||||
gap-2
|
||||
w-full;
|
||||
}
|
||||
|
||||
> div {
|
||||
@apply flex gap-2 items-end;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
66
fe/src/components/macros/parts/RecorderInput.vue
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
<!--
|
||||
Macrame is a program that enables the user to create keyboard macros and button panels.
|
||||
The macros are saved as simple JSON files and can be linked to the button panels. The panels can
|
||||
be created with HTML and CSS.
|
||||
|
||||
Copyright (C) 2025 Jesse Malotaux
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div :class="`recorder-input__container ${macroRecorder.state.record && 'record'}`">
|
||||
<input
|
||||
v-if="macroRecorder.state.record"
|
||||
:class="`macro-recorder__input ${macroRecorder.state.record && 'record'}`"
|
||||
type="text"
|
||||
ref="macroInput"
|
||||
@keydown.prevent="macroRecorder.recordStep($event, 'down')"
|
||||
@keyup.prevent="macroRecorder.recordStep($event, 'up')"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useMacroRecorderStore } from '@/stores/macrorecorder'
|
||||
import { ref, onUpdated } from 'vue'
|
||||
|
||||
const macroInput = ref(null)
|
||||
|
||||
const macroRecorder = useMacroRecorderStore()
|
||||
|
||||
onUpdated(() => {
|
||||
if (macroRecorder.state.record) {
|
||||
macroInput.value.focus()
|
||||
if (macroRecorder.delay.start !== 0) macroRecorder.restartDelay()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@reference "@/assets/main.css";
|
||||
|
||||
.recorder-input__container,
|
||||
.macro-recorder__input {
|
||||
@apply absolute
|
||||
inset-0
|
||||
size-full
|
||||
opacity-0
|
||||
hidden;
|
||||
|
||||
&.record {
|
||||
@apply block;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
79
fe/src/components/macros/parts/RecorderOutput.vue
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
<!--
|
||||
Macrame is a program that enables the user to create keyboard macros and button panels.
|
||||
The macros are saved as simple JSON files and can be linked to the button panels. The panels can
|
||||
be created with HTML and CSS.
|
||||
|
||||
Copyright (C) 2025 Jesse Malotaux
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div
|
||||
:class="`macro-recorder__output ${macroRecorder.state.record && 'record'} ${macroRecorder.state.edit && 'edit'}`"
|
||||
>
|
||||
<template v-for="(step, key) in macroRecorder.steps">
|
||||
<!-- Key element -->
|
||||
<template v-if="step.type === 'key'">
|
||||
<MacroKey
|
||||
:key="key"
|
||||
:key-obj="step.keyObj"
|
||||
:direction="step.direction"
|
||||
:active="macroRecorder.state.editKey === key"
|
||||
@click="macroRecorder.state.edit ? macroRecorder.toggleEdit('key', key) : false"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<!-- Delay element -->
|
||||
<template v-else-if="step.type === 'delay'">
|
||||
<DelaySpan
|
||||
:key="key"
|
||||
:value="step.value"
|
||||
:active="macroRecorder.state.editDelay === key"
|
||||
@click="macroRecorder.toggleEdit('delay', key)"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<!-- Spacer element -->
|
||||
<hr class="spacer" />
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { useMacroRecorderStore } from '@/stores/macrorecorder'
|
||||
import MacroKey from '../components/MacroKey.vue'
|
||||
import DelaySpan from '../components/DelaySpan.vue'
|
||||
|
||||
const macroRecorder = useMacroRecorderStore()
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@reference "@/assets/main.css";
|
||||
|
||||
.macro-recorder__output {
|
||||
@apply flex
|
||||
flex-wrap
|
||||
items-center
|
||||
gap-y-4
|
||||
p-4
|
||||
absolute
|
||||
top-0 left-0
|
||||
h-fit;
|
||||
}
|
||||
|
||||
hr.spacer:last-of-type {
|
||||
@apply hidden;
|
||||
}
|
||||
</style>
|
||||
253
fe/src/components/panels/PanelEdit.vue
Normal file
|
|
@ -0,0 +1,253 @@
|
|||
<!--
|
||||
Macrame is a program that enables the user to create keyboard macros and button panels.
|
||||
The macros are saved as simple JSON files and can be linked to the button panels. The panels can
|
||||
be created with HTML and CSS.
|
||||
|
||||
Copyright (C) 2025 Jesse Malotaux
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div id="panel-edit" class="mcrm-block block__dark !p-0 !gap-0" v-if="editPanel">
|
||||
<div class="panel-preview">
|
||||
<div class="panel-preview__content" ref="panelPreview" v-html="editPanel.html"></div>
|
||||
</div>
|
||||
<div class="panel-settings">
|
||||
<AccordionComp title="Panel info" ref="infoAccordion">
|
||||
<div class="grid grid-cols-[auto_1fr] gap-2 p-4">
|
||||
<span>Name:</span><strong class="text-right">{{ editPanel.name }}</strong>
|
||||
|
||||
<span>Aspect ratio:</span><strong class="text-right">{{ editPanel.aspectRatio }}</strong>
|
||||
|
||||
<template v-if="editPanel.macros">
|
||||
<span>Linked Macros:</span>
|
||||
<strong class="text-right">{{ Object.keys(editPanel.macros).length }}</strong>
|
||||
</template>
|
||||
</div>
|
||||
</AccordionComp>
|
||||
<div>
|
||||
<AccordionComp
|
||||
v-if="editButton.id"
|
||||
title="Button"
|
||||
ref="buttonAccordion"
|
||||
:open="editButton.id != ''"
|
||||
>
|
||||
<div class="grid gap-4 p-4">
|
||||
<div class="grid grid-cols-[auto_1fr] gap-2">
|
||||
<span>Button ID:</span>
|
||||
<strong class="text-right">{{ editButton.id }}</strong>
|
||||
</div>
|
||||
<div class="grid">
|
||||
<FormSelect
|
||||
name="button_macro"
|
||||
label="Button macro"
|
||||
:search="true"
|
||||
:options="macroList"
|
||||
:value="editButton.macro"
|
||||
@change="checkNewMacro(editButton.id, $event)"
|
||||
/>
|
||||
<div class="grid grid-cols-2 mt-4">
|
||||
<ButtonComp
|
||||
v-if="editButton.macro != ''"
|
||||
class="col-start-1 w-fit"
|
||||
size="sm"
|
||||
variant="danger"
|
||||
@click="unlinkMacro(editButton.id)"
|
||||
ref="unlinkButton"
|
||||
>
|
||||
<IconTrash /> Unlink
|
||||
</ButtonComp>
|
||||
<ButtonComp
|
||||
v-if="editButton.changed"
|
||||
class="col-start-2 w-fit justify-self-end"
|
||||
size="sm"
|
||||
variant="primary"
|
||||
@click="linkMacro(editButton.id)"
|
||||
ref="linkButton"
|
||||
>
|
||||
<IconLink /> Link
|
||||
</ButtonComp>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</AccordionComp>
|
||||
</div>
|
||||
<footer class="flex items-end justify-end h-full p-4">
|
||||
<ButtonComp v-if="panelMacros.changed" variant="success" @click="savePanelChanges()">
|
||||
<IconDeviceFloppy /> Save changes
|
||||
</ButtonComp>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { CheckMacroListChange, GetMacroList } from '@/services/MacroService'
|
||||
import {
|
||||
PanelButtonListeners,
|
||||
PanelDialogListeners,
|
||||
RemovePanelStyle,
|
||||
SetPanelStyle,
|
||||
StripPanelHTML,
|
||||
} from '@/services/PanelService'
|
||||
import { usePanelStore } from '@/stores/panel'
|
||||
import { onMounted, onUnmounted, reactive, ref } from 'vue'
|
||||
import AccordionComp from '../base/AccordionComp.vue'
|
||||
import FormSelect from '../form/FormSelect.vue'
|
||||
import ButtonComp from '../base/ButtonComp.vue'
|
||||
import { IconDeviceFloppy, IconLink, IconTrash } from '@tabler/icons-vue'
|
||||
import axios from 'axios'
|
||||
import { appUrl } from '@/services/ApiService'
|
||||
|
||||
const props = defineProps({
|
||||
dirname: String,
|
||||
})
|
||||
|
||||
const panel = usePanelStore()
|
||||
|
||||
const panelPreview = ref(false)
|
||||
const editPanel = ref({})
|
||||
const panelMacros = reactive({
|
||||
old: {},
|
||||
changed: false,
|
||||
})
|
||||
|
||||
const macroList = ref({})
|
||||
|
||||
const infoAccordion = ref(false)
|
||||
const buttonAccordion = ref(false)
|
||||
|
||||
const unlinkButton = ref(null)
|
||||
const linkButton = ref(null)
|
||||
|
||||
const editButton = reactive({
|
||||
id: '',
|
||||
macro: '',
|
||||
newMacro: '',
|
||||
changed: false,
|
||||
})
|
||||
|
||||
onMounted(async () => {
|
||||
const currentPanel = await panel.get(props.dirname)
|
||||
editPanel.value = currentPanel
|
||||
editPanel.value.dir = props.dirname
|
||||
editPanel.value.html = StripPanelHTML(editPanel.value.html, editPanel.value.aspectRatio)
|
||||
|
||||
panelMacros.old = JSON.stringify(currentPanel.macros)
|
||||
|
||||
infoAccordion.value.toggleAccordion(true)
|
||||
|
||||
const macros = await GetMacroList()
|
||||
macroList.value = Object.assign(
|
||||
{},
|
||||
...Object.keys(macros).map((key) => ({
|
||||
[key]: { value: macros[key].macroname, label: macros[key].name },
|
||||
})),
|
||||
)
|
||||
|
||||
SetPanelStyle(editPanel.value.style)
|
||||
|
||||
EditButtonListeners()
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
RemovePanelStyle()
|
||||
})
|
||||
|
||||
function EditButtonListeners() {
|
||||
const callback = (button) => {
|
||||
infoAccordion.value.toggleAccordion(false)
|
||||
setEditButton(button.id)
|
||||
}
|
||||
|
||||
PanelButtonListeners(panelPreview.value, callback)
|
||||
PanelDialogListeners(panelPreview.value)
|
||||
}
|
||||
|
||||
function setEditButton(id) {
|
||||
editButton.id = id
|
||||
editButton.macro = editPanel.value.macros[id] ? editPanel.value.macros[id] : ''
|
||||
}
|
||||
|
||||
function checkNewMacro(id, macro) {
|
||||
editButton.changed = editPanel.value.macros[id] != macro
|
||||
editButton.newMacro = macro
|
||||
}
|
||||
|
||||
function linkMacro(id) {
|
||||
editPanel.value.macros[id] = editButton.newMacro
|
||||
editButton.macro = editButton.newMacro
|
||||
editButton.newMacro = ''
|
||||
|
||||
panelMacros.changed = CheckMacroListChange(panelMacros.old, editPanel.value.macros)
|
||||
}
|
||||
|
||||
function unlinkMacro(id) {
|
||||
delete editPanel.value.macros[id]
|
||||
buttonAccordion.value.toggleAccordion(false)
|
||||
panelMacros.changed = CheckMacroListChange(panelMacros.old, editPanel.value.macros)
|
||||
}
|
||||
|
||||
function savePanelChanges() {
|
||||
const panelData = {
|
||||
dir: editPanel.value.dir,
|
||||
name: editPanel.value.name,
|
||||
description: editPanel.value.description,
|
||||
aspectRatio: editPanel.value.aspectRatio,
|
||||
macros: editPanel.value.macros,
|
||||
}
|
||||
|
||||
axios.post(appUrl() + '/panel/save/json', panelData)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
@reference "@/assets/main.css";
|
||||
|
||||
[mcrm__button] {
|
||||
@apply cursor-pointer;
|
||||
}
|
||||
|
||||
#panel-edit {
|
||||
@apply grid
|
||||
grid-cols-[1fr_30ch]
|
||||
size-full
|
||||
overflow-hidden;
|
||||
|
||||
.panel-preview {
|
||||
@apply border-r
|
||||
border-slate-700;
|
||||
|
||||
.panel-preview__content {
|
||||
@apply relative
|
||||
grid
|
||||
justify-center
|
||||
size-full
|
||||
p-8;
|
||||
|
||||
#panel-html__body {
|
||||
@apply size-full
|
||||
max-w-full max-h-full;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.panel-settings {
|
||||
@apply grid
|
||||
grid-rows-[auto_auto_1fr]
|
||||
bg-black/30;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
103
fe/src/components/panels/PanelView.vue
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
<!--
|
||||
Macrame is a program that enables the user to create keyboard macros and button panels.
|
||||
The macros are saved as simple JSON files and can be linked to the button panels. The panels can
|
||||
be created with HTML and CSS.
|
||||
|
||||
Copyright (C) 2025 Jesse Malotaux
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div id="panel-view">
|
||||
<div class="panel-preview__content" ref="panelView" v-html="viewPanel.html"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { RunMacro } from '@/services/MacroService'
|
||||
import {
|
||||
PanelButtonListeners,
|
||||
PanelDialogListeners,
|
||||
RemovePanelScripts,
|
||||
RemovePanelStyle,
|
||||
SetPanelStyle,
|
||||
StripPanelHTML,
|
||||
} from '@/services/PanelService'
|
||||
import { usePanelStore } from '@/stores/panel'
|
||||
import { onMounted, onUnmounted, ref } from 'vue'
|
||||
|
||||
const panel = usePanelStore()
|
||||
|
||||
const props = defineProps({
|
||||
dirname: String,
|
||||
})
|
||||
|
||||
const panelView = ref(null)
|
||||
|
||||
const viewPanel = ref({})
|
||||
|
||||
onMounted(async () => {
|
||||
const currentPanel = await panel.get(props.dirname)
|
||||
viewPanel.value = currentPanel
|
||||
|
||||
viewPanel.value.html = StripPanelHTML(viewPanel.value.html, viewPanel.value.aspectRatio)
|
||||
SetPanelStyle(viewPanel.value.style)
|
||||
|
||||
setTimeout(() => {
|
||||
viewPanelListeners()
|
||||
|
||||
if (typeof window.onPanelLoaded === 'function') {
|
||||
window.onPanelLoaded()
|
||||
}
|
||||
}, 50)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
RemovePanelStyle()
|
||||
RemovePanelScripts()
|
||||
})
|
||||
|
||||
const viewPanelListeners = () => {
|
||||
const callback = (button) => {
|
||||
RunMacro(viewPanel.value.macros[button.id])
|
||||
}
|
||||
|
||||
PanelButtonListeners(panelView.value, callback)
|
||||
PanelDialogListeners(panelView.value)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@reference "@/assets/main.css";
|
||||
|
||||
#panel-view {
|
||||
@apply fixed
|
||||
inset-0
|
||||
size-full
|
||||
bg-black;
|
||||
|
||||
.panel-preview__content {
|
||||
@apply relative
|
||||
grid
|
||||
justify-center
|
||||
size-full;
|
||||
|
||||
#panel-html__body {
|
||||
@apply size-full
|
||||
max-w-full max-h-full;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
183
fe/src/components/panels/PanelsOverview.vue
Normal file
|
|
@ -0,0 +1,183 @@
|
|||
<!--
|
||||
Macrame is a program that enables the user to create keyboard macros and button panels.
|
||||
The macros are saved as simple JSON files and can be linked to the button panels. The panels can
|
||||
be created with HTML and CSS.
|
||||
|
||||
Copyright (C) 2025 Jesse Malotaux
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div id="panels-overview">
|
||||
<AlertComp v-if="Object.keys(panels.list).length == 0" variant="info">
|
||||
No panels found
|
||||
</AlertComp>
|
||||
<div class="panel-list">
|
||||
<div class="panel-item mcrm-block block__dark" v-for="(panel, i) in panels.list" :key="i">
|
||||
<div class="panel-item__content" @click="panelItemClick(panel.dir)">
|
||||
<div class="thumb">
|
||||
<img v-if="panel.thumb" :src="`data:image/jpeg;base64,${panel.thumb}`" alt="" />
|
||||
<IconLayoutGrid v-else />
|
||||
</div>
|
||||
<h4>{{ panel.name }}</h4>
|
||||
<div class="description" v-if="isLocal()">
|
||||
<div class="content">
|
||||
<strong class="block mb-1 text-slate-400">{{ panel.name }}</strong>
|
||||
<hr class="mb-2 border-slate-600" />
|
||||
<p v-if="panel.description != 'null'" class="text-slate-200">
|
||||
{{ panel.description }}
|
||||
</p>
|
||||
</div>
|
||||
<footer>
|
||||
<ButtonComp variant="subtle" size="sm" :href="`/panel/view/${panel.dir}`">
|
||||
<IconEye /> Preview
|
||||
</ButtonComp>
|
||||
<ButtonComp variant="primary" size="sm" :href="`/panel/edit/${panel.dir}`">
|
||||
<IconPencil /> Edit
|
||||
</ButtonComp>
|
||||
</footer>
|
||||
</div>
|
||||
</div>
|
||||
<template v-if="!isLocal()"> </template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { usePanelStore } from '@/stores/panel'
|
||||
import { onMounted, reactive } from 'vue'
|
||||
import AlertComp from '../base/AlertComp.vue'
|
||||
import { IconEye, IconLayoutGrid, IconPencil } from '@tabler/icons-vue'
|
||||
import ButtonComp from '../base/ButtonComp.vue'
|
||||
import { isLocal } from '@/services/ApiService'
|
||||
import { useRouter } from 'vue-router'
|
||||
|
||||
const panel = usePanelStore()
|
||||
|
||||
const panels = reactive({
|
||||
list: {},
|
||||
})
|
||||
|
||||
const router = useRouter()
|
||||
|
||||
onMounted(async () => {
|
||||
const panelList = await panel.getList()
|
||||
|
||||
panels.list = panelList
|
||||
})
|
||||
|
||||
function panelItemClick(dir) {
|
||||
if (isLocal()) return
|
||||
|
||||
router.push(`/panel/view/${dir}`)
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@reference "@/assets/main.css";
|
||||
|
||||
.panel-list {
|
||||
@apply grid
|
||||
grid-cols-2
|
||||
md:grid-cols-4
|
||||
lg:grid-cols-6
|
||||
gap-4
|
||||
w-full h-fit;
|
||||
}
|
||||
|
||||
.panel-item {
|
||||
@apply p-px
|
||||
overflow-hidden;
|
||||
|
||||
.thumb {
|
||||
@apply flex
|
||||
justify-center
|
||||
items-center
|
||||
w-full
|
||||
aspect-[4/3];
|
||||
|
||||
img {
|
||||
@apply size-full
|
||||
object-cover;
|
||||
}
|
||||
|
||||
&:not(:has(img)) {
|
||||
@apply bg-sky-950;
|
||||
}
|
||||
|
||||
svg {
|
||||
@apply size-12;
|
||||
}
|
||||
}
|
||||
|
||||
h4 {
|
||||
@apply px-4 py-2
|
||||
h-12
|
||||
truncate;
|
||||
}
|
||||
|
||||
&:hover .description {
|
||||
@apply opacity-100;
|
||||
}
|
||||
|
||||
.description {
|
||||
@apply absolute
|
||||
inset-0
|
||||
size-full
|
||||
pt-2
|
||||
pr-1
|
||||
pb-13
|
||||
bg-slate-900/60
|
||||
backdrop-blur-md
|
||||
text-slate-100
|
||||
opacity-0
|
||||
transition-opacity
|
||||
cursor-default
|
||||
z-10;
|
||||
|
||||
.content {
|
||||
@apply h-full
|
||||
p-4
|
||||
pt-2
|
||||
overflow-y-auto;
|
||||
}
|
||||
|
||||
footer {
|
||||
@apply absolute
|
||||
bottom-0 left-0
|
||||
w-full
|
||||
h-12
|
||||
grid
|
||||
grid-cols-2
|
||||
bg-slate-900
|
||||
border-t
|
||||
border-slate-600;
|
||||
|
||||
.btn {
|
||||
@apply size-full
|
||||
rounded-none
|
||||
justify-center
|
||||
border-0;
|
||||
|
||||
&:last-child {
|
||||
@apply border-l
|
||||
border-slate-600;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
@ -1,10 +1,33 @@
|
|||
import './assets/main.css'
|
||||
/*
|
||||
Macrame is a program that enables the user to create keyboard macros and button panels.
|
||||
The macros are saved as simple JSON files and can be linked to the button panels. The panels can
|
||||
be created with HTML and CSS.
|
||||
|
||||
Copyright (C) 2025 Jesse Malotaux
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
// import './assets/jemx.scss'
|
||||
import '@/assets/main.css'
|
||||
import '@/assets/img/Macrame-Logo-gradient.svg'
|
||||
|
||||
import { createApp } from 'vue'
|
||||
import { createPinia } from 'pinia'
|
||||
|
||||
import App from './App.vue'
|
||||
import router from './router'
|
||||
import App from '@/App.vue'
|
||||
import router from '@/router'
|
||||
|
||||
const app = createApp(App)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,14 +1,70 @@
|
|||
/*
|
||||
Macrame is a program that enables the user to create keyboard macros and button panels.
|
||||
The macros are saved as simple JSON files and can be linked to the button panels. The panels can
|
||||
be created with HTML and CSS.
|
||||
|
||||
Copyright (C) 2025 Jesse Malotaux
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { createRouter, createWebHistory } from 'vue-router'
|
||||
import HomeView from '../views/HomeView.vue'
|
||||
import DashboardView from '../views/DashboardView.vue'
|
||||
import { checkAuth, isLocal } from '@/services/ApiService'
|
||||
|
||||
const router = createRouter({
|
||||
history: createWebHistory(import.meta.env.BASE_URL),
|
||||
routes: [
|
||||
{
|
||||
path: '/',
|
||||
name: 'home',
|
||||
component: HomeView,
|
||||
name: 'dashboard',
|
||||
component: DashboardView,
|
||||
},
|
||||
{
|
||||
path: '/panels',
|
||||
name: 'panels',
|
||||
component: () => import('../views/PanelsView.vue'),
|
||||
meta: { requiresAuth: true },
|
||||
},
|
||||
{
|
||||
path: '/panel/edit/:dirname',
|
||||
name: 'panel-edit',
|
||||
component: () => import('../views/PanelsView.vue'),
|
||||
meta: { requiresAuth: true },
|
||||
},
|
||||
{
|
||||
path: '/panel/view/:dirname',
|
||||
name: 'panel-view',
|
||||
component: () => import('../views/PanelsView.vue'),
|
||||
meta: { requiresAuth: true },
|
||||
},
|
||||
{
|
||||
path: '/macros',
|
||||
name: 'macros',
|
||||
component: () => import('../views/MacrosView.vue'),
|
||||
meta: { localOnly: true },
|
||||
},
|
||||
{
|
||||
path: '/devices',
|
||||
name: 'devices',
|
||||
component: () => import('../views/DevicesView.vue'),
|
||||
},
|
||||
// {
|
||||
// path: '/settings',
|
||||
// name: 'settings',
|
||||
// component: () => import('../views/SettingsView.vue'),
|
||||
// },
|
||||
// {
|
||||
// path: '/about',
|
||||
// name: 'about',
|
||||
|
|
@ -20,4 +76,12 @@ const router = createRouter({
|
|||
],
|
||||
})
|
||||
|
||||
router.beforeEach(async (to, from, next) => {
|
||||
const auth = await checkAuth()
|
||||
|
||||
if (to.meta.requiresAuth && !auth && !isLocal()) next('/devices')
|
||||
else if (to.meta.localOnly && !isLocal()) next('/')
|
||||
else next()
|
||||
})
|
||||
|
||||
export default router
|
||||
|
|
|
|||
55
fe/src/services/ApiService.js
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
/*
|
||||
Macrame is a program that enables the user to create keyboard macros and button panels.
|
||||
The macros are saved as simple JSON files and can be linked to the button panels. The panels can
|
||||
be created with HTML and CSS.
|
||||
|
||||
Copyright (C) 2025 Jesse Malotaux
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { useDeviceStore } from '@/stores/device'
|
||||
import CryptoJS from 'crypto-js'
|
||||
|
||||
export const appUrl = () => {
|
||||
const port = window.location.port == 5173 ? window.__CONFIG__.MCRM__PORT : window.location.port
|
||||
|
||||
return `http://${window.location.hostname}:${port}`
|
||||
}
|
||||
|
||||
export const isLocal = () => {
|
||||
return window.location.hostname === '127.0.0.1' || window.location.hostname === 'localhost'
|
||||
}
|
||||
|
||||
export const encrypt = (data, key = false) => {
|
||||
const pk = !key ? localStorage.getItem('Macrame__pk') : key
|
||||
|
||||
if (pk) {
|
||||
return CryptoJS.RSA.encrypt(JSON.stringify(data), pk).toString()
|
||||
} else {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
export const checkAuth = async () => {
|
||||
const device = useDeviceStore()
|
||||
|
||||
const handshake = await device.remoteHandshake()
|
||||
|
||||
if (handshake === true) return true
|
||||
|
||||
if (device.key()) return true
|
||||
|
||||
return false
|
||||
}
|
||||
77
fe/src/services/EncryptService.js
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
/*
|
||||
Macrame is a program that enables the user to create keyboard macros and button panels.
|
||||
The macros are saved as simple JSON files and can be linked to the button panels. The panels can
|
||||
be created with HTML and CSS.
|
||||
|
||||
Copyright (C) 2025 Jesse Malotaux
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { useDeviceStore } from '@/stores/device'
|
||||
import { AES, enc, pad } from 'crypto-js'
|
||||
import { isLocal } from './ApiService'
|
||||
|
||||
export const encryptAES = (key, str) => {
|
||||
key = keyPad(key)
|
||||
|
||||
let iv = enc.Utf8.parse(window.__CONFIG__.MCRM__IV)
|
||||
let encrypted = AES.encrypt(str, key, {
|
||||
iv: iv,
|
||||
padding: pad.Pkcs7,
|
||||
})
|
||||
return encrypted.toString()
|
||||
}
|
||||
|
||||
export const decryptAES = (key, str) => {
|
||||
key = keyPad(key)
|
||||
|
||||
let iv = enc.Utf8.parse(window.__CONFIG__.MCRM__IV)
|
||||
let encrypted = AES.decrypt(str.toString(), key, {
|
||||
iv: iv,
|
||||
padding: pad.Pkcs7,
|
||||
})
|
||||
return encrypted.toString(enc.Utf8)
|
||||
}
|
||||
|
||||
export const AuthCall = (data = false) => {
|
||||
if (isLocal()) return data
|
||||
|
||||
if (!data) data = {empty: true}
|
||||
|
||||
const device = useDeviceStore()
|
||||
|
||||
return {
|
||||
uuid: device.uuid(),
|
||||
d: encryptAES(device.key(), JSON.stringify(data)),
|
||||
}
|
||||
}
|
||||
|
||||
function keyPad(key) {
|
||||
let returnKey = key
|
||||
|
||||
if (key.length == 4) {
|
||||
returnKey = key + window.__CONFIG__.MCRM__SALT
|
||||
}
|
||||
|
||||
return enc.Utf8.parse(returnKey)
|
||||
}
|
||||
|
||||
export const getDateStr = () => {
|
||||
const date = new Date()
|
||||
const year = date.getFullYear()
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(date.getDate()).padStart(2, '0')
|
||||
return `${year}${month}${day}`
|
||||
}
|
||||
192
fe/src/services/MacroRecordService.js
Normal file
|
|
@ -0,0 +1,192 @@
|
|||
/*
|
||||
Macrame is a program that enables the user to create keyboard macros and button panels.
|
||||
The macros are saved as simple JSON files and can be linked to the button panels. The panels can
|
||||
be created with HTML and CSS.
|
||||
|
||||
Copyright (C) 2025 Jesse Malotaux
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
const keyMap = {
|
||||
// Modifier keys
|
||||
Control: 'Ctrl',
|
||||
Shift: 'Shift',
|
||||
Alt: 'Alt',
|
||||
Meta: 'Win',
|
||||
CapsLock: 'Caps',
|
||||
// Special keys
|
||||
PageUp: 'PgUp',
|
||||
PageDown: 'PgDn',
|
||||
ScrollLock: 'Scr Lk',
|
||||
Insert: 'Ins',
|
||||
Delete: 'Del',
|
||||
Escape: 'Esc',
|
||||
Space: 'Space',
|
||||
// Symbol keys
|
||||
Backquote: '`',
|
||||
Backslash: '\\',
|
||||
BracketLeft: '[',
|
||||
BracketRight: ']',
|
||||
Comma: ',',
|
||||
Equal: '=',
|
||||
Minus: '-',
|
||||
Period: '.',
|
||||
Quote: "'",
|
||||
Semicolon: ';',
|
||||
Slash: '/',
|
||||
// Arrow keys
|
||||
ArrowUp: '▲',
|
||||
ArrowRight: '▶',
|
||||
ArrowDown: '▼',
|
||||
ArrowLeft: '◀',
|
||||
// Media keys
|
||||
MediaPlayPause: 'Play',
|
||||
MediaStop: 'Stop',
|
||||
MediaTrackNext: 'Next',
|
||||
MediaTrackPrevious: 'Prev',
|
||||
MediaVolumeDown: 'Down',
|
||||
MediaVolumeUp: 'Up',
|
||||
AudioVolumeMute: 'Mute',
|
||||
AudioVolumeDown: 'Down',
|
||||
AudioVolumeUp: 'Up',
|
||||
}
|
||||
|
||||
/**
|
||||
* Filters a keyboard event and returns an object with two properties:
|
||||
* loc (optional) and str.
|
||||
* loc is the location of the key (either 'left', 'right', or 'num').
|
||||
* str is the string representation of the key (e.g. 'a', 'A', 'Enter', etc.).
|
||||
* If the key is a modifier key, it is represented by its name (e.g. 'Ctrl', 'Shift', etc.).
|
||||
* If the key is not a modifier key, it is represented by its character (e.g. 'a', 'A', etc.).
|
||||
* If the key is not a character key, it is represented by its symbol (e.g. ',', '.', etc.).
|
||||
* @param {KeyboardEvent} e - The keyboard event to filter.
|
||||
* @return {Object} An object with two properties: loc (optional) and str.
|
||||
*/
|
||||
export const filterKey = (e) => {
|
||||
const k = {} // Object k (key)
|
||||
|
||||
// If location is set, set loc (location)
|
||||
if (e.location === 1) k.loc = 'left'
|
||||
if (e.location === 2) k.loc = 'right'
|
||||
if (e.location === 3) k.loc = 'num'
|
||||
|
||||
if (e.key.includes('Media') || e.key.includes('Audio')) k.loc = mediaPrefix(e)
|
||||
|
||||
// If code is in keyMap, set str by code
|
||||
if (keyMap[e.code] || keyMap[e.key]) {
|
||||
k.str = keyMap[e.code] || keyMap[e.key]
|
||||
} else {
|
||||
// If code is not in keyMap, set str by e.key
|
||||
k.str = e.key.toLowerCase()
|
||||
}
|
||||
|
||||
// return k object
|
||||
return k
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a string prefix for the given media key.
|
||||
* @param {KeyboardEvent} e - The keyboard event to get the prefix for.
|
||||
* @return {string} The prefix for the key (either 'Media' or 'Volume').
|
||||
*/
|
||||
const mediaPrefix = (e) => {
|
||||
switch (e.key) {
|
||||
case 'MediaPlayPause':
|
||||
case 'MediaStop':
|
||||
case 'MediaTrackNext':
|
||||
case 'MediaTrackPrevious':
|
||||
return 'Media'
|
||||
case 'MediaVolumeDown':
|
||||
case 'MediaVolumeUp':
|
||||
case 'AudioVolumeDown':
|
||||
case 'AudioVolumeUp':
|
||||
case 'AudioVolumeMute':
|
||||
return 'Volume'
|
||||
}
|
||||
}
|
||||
|
||||
export const isRepeat = (lastStep, e, direction) => {
|
||||
return (
|
||||
lastStep &&
|
||||
lastStep.type === 'key' &&
|
||||
lastStep.code === e.code &&
|
||||
lastStep.direction === direction
|
||||
)
|
||||
}
|
||||
|
||||
export const invalidMacro = (steps) => {
|
||||
const downKeys = []
|
||||
const upKeys = []
|
||||
|
||||
Object.keys(steps).forEach((stepKey) => {
|
||||
const step = steps[stepKey]
|
||||
|
||||
if (step.type !== 'key') return
|
||||
|
||||
if (step.direction == 'down') downKeys.push(step.key)
|
||||
if (step.direction == 'up') {
|
||||
if (!downKeys.includes(step.key)) upKeys.push(step.key)
|
||||
else downKeys.splice(downKeys.indexOf(step.key), 1)
|
||||
}
|
||||
})
|
||||
|
||||
if (upKeys.length === 0 && downKeys.length === 0) return false
|
||||
|
||||
return { down: downKeys, up: upKeys }
|
||||
}
|
||||
|
||||
export const translateJSON = (json) => {
|
||||
const steps = []
|
||||
|
||||
json.forEach((step) => {
|
||||
if (step.type === 'delay') steps.push(step)
|
||||
if (step.type === 'key') steps.push(codeToStep(step.code, step.direction))
|
||||
})
|
||||
|
||||
return steps
|
||||
}
|
||||
|
||||
export const codeToStep = (code, direction) => {
|
||||
let key = ''
|
||||
let location = 0
|
||||
let codeStr = code
|
||||
|
||||
if (code.includes('Left')) {
|
||||
key = code.replace('Left', '')
|
||||
location = 1
|
||||
}
|
||||
if (code.includes('Right')) {
|
||||
key = code.replace('Right', '')
|
||||
location = 2
|
||||
}
|
||||
if (code.includes('Numpad')) {
|
||||
key = code.replace('Numpad', '')
|
||||
location = 3
|
||||
}
|
||||
|
||||
if (code.includes('Media')) codeStr = ''
|
||||
|
||||
if (key === '') key = code
|
||||
|
||||
const stepObj = {
|
||||
type: 'key',
|
||||
code: codeStr,
|
||||
key: key,
|
||||
location: location,
|
||||
direction: direction,
|
||||
}
|
||||
|
||||
return { ...stepObj, keyObj: filterKey(stepObj) }
|
||||
}
|
||||
47
fe/src/services/MacroService.js
Normal file
|
|
@ -0,0 +1,47 @@
|
|||
/*
|
||||
Macrame is a program that enables the user to create keyboard macros and button panels.
|
||||
The macros are saved as simple JSON files and can be linked to the button panels. The panels can
|
||||
be created with HTML and CSS.
|
||||
|
||||
Copyright (C) 2025 Jesse Malotaux
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import axios from 'axios'
|
||||
import { appUrl, isLocal } from './ApiService'
|
||||
import { AuthCall } from './EncryptService'
|
||||
|
||||
export const GetMacroList = async (count = false) => {
|
||||
const request = await axios.post(appUrl() + '/macro/list')
|
||||
|
||||
if (!request.data) return 0
|
||||
|
||||
if (!count) return sortMacroList(request.data)
|
||||
else return request.data.length
|
||||
}
|
||||
|
||||
const sortMacroList = (list) => {
|
||||
return [...list].sort((a, b) => a.name.localeCompare(b.name))
|
||||
}
|
||||
|
||||
export const RunMacro = async (macro) => {
|
||||
const data = isLocal() ? { macro: macro } : AuthCall({ macro: macro })
|
||||
const request = await axios.post(appUrl() + '/macro/play', data)
|
||||
return request.data
|
||||
}
|
||||
|
||||
export const CheckMacroListChange = (oldList, newList) => {
|
||||
return oldList !== JSON.stringify(newList)
|
||||
}
|
||||
120
fe/src/services/PanelService.js
Normal file
|
|
@ -0,0 +1,120 @@
|
|||
/*
|
||||
Macrame is a program that enables the user to create keyboard macros and button panels.
|
||||
The macros are saved as simple JSON files and can be linked to the button panels. The panels can
|
||||
be created with HTML and CSS.
|
||||
|
||||
Copyright (C) 2025 Jesse Malotaux
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
export const SetPanelStyle = (styleStr) => {
|
||||
const styleEl = document.createElement('style')
|
||||
styleEl.setAttribute('custom_panel_style', true)
|
||||
styleEl.innerHTML = styleStr
|
||||
document.head.appendChild(styleEl)
|
||||
}
|
||||
|
||||
export const RemovePanelStyle = () => {
|
||||
const styleEl = document.querySelector('style[custom_panel_style]')
|
||||
if (styleEl) {
|
||||
styleEl.remove()
|
||||
}
|
||||
}
|
||||
|
||||
export const StripPanelHTML = (html, aspectRatio) => {
|
||||
const parser = new DOMParser()
|
||||
const doc = parser.parseFromString(html, 'text/html')
|
||||
let scripts = []
|
||||
|
||||
if (doc.querySelectorAll('script').length > 0) {
|
||||
const stripped = StripPanelScripts(doc)
|
||||
doc.body = stripped.body
|
||||
scripts = stripped.scripts
|
||||
}
|
||||
|
||||
const body = doc.body
|
||||
const bodyContents = body.innerHTML
|
||||
|
||||
const panelBody = document.createElement('div')
|
||||
panelBody.id = 'panel-html__body'
|
||||
panelBody.style = `aspect-ratio: ${aspectRatio}`
|
||||
panelBody.innerHTML = bodyContents
|
||||
|
||||
if (scripts.length > 0) {
|
||||
SetPanelScripts(scripts)
|
||||
}
|
||||
|
||||
return panelBody.outerHTML
|
||||
}
|
||||
|
||||
export const StripPanelScripts = (doc) => {
|
||||
const scriptEls = doc.querySelectorAll('script')
|
||||
const scripts = []
|
||||
|
||||
scriptEls.forEach((script) => {
|
||||
if (script.getAttribute('no-compile') != '') scripts.push(script.innerHTML)
|
||||
script.remove()
|
||||
})
|
||||
|
||||
return { body: doc.body, scripts }
|
||||
}
|
||||
|
||||
export const SetPanelScripts = (scripts) => {
|
||||
scripts.forEach((script) => {
|
||||
const scriptEl = document.createElement('script')
|
||||
scriptEl.setAttribute('custom_panel_script', true)
|
||||
scriptEl.innerHTML = script
|
||||
document.body.appendChild(scriptEl)
|
||||
})
|
||||
}
|
||||
|
||||
export const RemovePanelScripts = () => {
|
||||
const scripts = document.querySelectorAll('script[custom_panel_script]')
|
||||
scripts.forEach((script) => {
|
||||
script.remove()
|
||||
})
|
||||
}
|
||||
|
||||
export const PanelButtonListeners = (panelEl, callback) => {
|
||||
panelEl.querySelectorAll('[mcrm__button]').forEach((button) => {
|
||||
button.addEventListener('click', () => {
|
||||
callback(button)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
export const PanelDialogListeners = (panelEl) => {
|
||||
panelEl.querySelectorAll('[mcrm__dialog-trigger]').forEach((dialogTrigger) => {
|
||||
const dialogEl = document.querySelector(dialogTrigger.getAttribute('dialog-trigger'))
|
||||
|
||||
if (dialogEl) {
|
||||
dialogTrigger.addEventListener('click', () => {
|
||||
dialogEl.show()
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
document.querySelectorAll('dialog, dialog .dialog__close').forEach((dialogClose) => {
|
||||
dialogClose.addEventListener('click', (e) => {
|
||||
if (
|
||||
e.target.classList.contains('dialog__close') ||
|
||||
e.target.closest('.dialog__close') ||
|
||||
e.target.tagName == 'DIALOG'
|
||||
) {
|
||||
dialogClose.closest('dialog').close()
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
117
fe/src/services/RobotKeys.md
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
"A-Z a-z 0-9"
|
||||
|
||||
"backspace"
|
||||
"delete"
|
||||
"enter"
|
||||
"tab"
|
||||
"esc"
|
||||
"escape"
|
||||
"up" Up arrow key
|
||||
"down" Down arrow key
|
||||
"right" Right arrow key
|
||||
"left" Left arrow key
|
||||
"home"
|
||||
"end"
|
||||
"pageup"
|
||||
"pagedown"
|
||||
|
||||
"f1"
|
||||
"f2"
|
||||
"f3"
|
||||
"f4"
|
||||
"f5"
|
||||
"f6"
|
||||
"f7"
|
||||
"f8"
|
||||
"f9"
|
||||
"f10"
|
||||
"f11"
|
||||
"f12"
|
||||
"f13"
|
||||
"f14"
|
||||
"f15"
|
||||
"f16"
|
||||
"f17"
|
||||
"f18"
|
||||
"f19"
|
||||
"f20"
|
||||
"f21"
|
||||
"f22"
|
||||
"f23"
|
||||
"f24"
|
||||
|
||||
"cmd" is the "win" key for windows
|
||||
"lcmd" left command
|
||||
"rcmd" right command
|
||||
// "command"
|
||||
"alt"
|
||||
"lalt" left alt
|
||||
"ralt" right alt
|
||||
"ctrl"
|
||||
"lctrl" left ctrl
|
||||
"rctrl" right ctrl
|
||||
"control"
|
||||
"shift"
|
||||
"lshift" left shift
|
||||
"rshift" right shift
|
||||
// "right_shift"
|
||||
"capslock"
|
||||
"space"
|
||||
"print"
|
||||
"printscreen" // No Mac support
|
||||
"insert"
|
||||
"menu" Windows only
|
||||
|
||||
"audio_mute" Mute the volume
|
||||
"audio_vol_down" Lower the volume
|
||||
"audio_vol_up" Increase the volume
|
||||
"audio_play"
|
||||
"audio_stop"
|
||||
"audio_pause"
|
||||
"audio_prev" Previous Track
|
||||
"audio_next" Next Track
|
||||
"audio_rewind" Linux only
|
||||
"audio_forward" Linux only
|
||||
"audio_repeat" Linux only
|
||||
"audio_random" Linux only
|
||||
|
||||
|
||||
"num0"
|
||||
"num1"
|
||||
"num2"
|
||||
"num3"
|
||||
"num4"
|
||||
"num5"
|
||||
"num6"
|
||||
"num7"
|
||||
"num8"
|
||||
"num9"
|
||||
"num_lock"
|
||||
|
||||
"num."
|
||||
"num+"
|
||||
"num-"
|
||||
"num*"
|
||||
"num/"
|
||||
"num_clear"
|
||||
"num_enter"
|
||||
"num_equal"
|
||||
|
||||
// // "numpad_0" No Linux support
|
||||
// "numpad_0"
|
||||
// "numpad_1"
|
||||
// "numpad_2"
|
||||
// "numpad_3"
|
||||
// "numpad_4"
|
||||
// "numpad_5"
|
||||
// "numpad_6"
|
||||
// "numpad_7"
|
||||
// "numpad_8"
|
||||
// "numpad_9"
|
||||
// "numpad_lock"
|
||||
|
||||
"lights_mon_up" Turn up monitor brightness No Windows support
|
||||
"lights_mon_down" Turn down monitor brightness No Windows support
|
||||
"lights_kbd_toggle" Toggle keyboard backlight on/off No Windows support
|
||||
"lights_kbd_up" Turn up keyboard backlight brightness No Windows support
|
||||
"lights_kbd_down" Turn down keyboard backlight brightness No Windows support
|
||||
|
|
@ -1,3 +1,24 @@
|
|||
/*
|
||||
Macrame is a program that enables the user to create keyboard macros and button panels.
|
||||
The macros are saved as simple JSON files and can be linked to the button panels. The panels can
|
||||
be created with HTML and CSS.
|
||||
|
||||
Copyright (C) 2025 Jesse Malotaux
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { ref, computed } from 'vue'
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
|
|
|
|||
153
fe/src/stores/device.js
Normal file
|
|
@ -0,0 +1,153 @@
|
|||
/*
|
||||
Macrame is a program that enables the user to create keyboard macros and button panels.
|
||||
The macros are saved as simple JSON files and can be linked to the button panels. The panels can
|
||||
be created with HTML and CSS.
|
||||
|
||||
Copyright (C) 2025 Jesse Malotaux
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { ref } from 'vue'
|
||||
import { defineStore } from 'pinia'
|
||||
import axios from 'axios'
|
||||
import { appUrl, encrypt } from '@/services/ApiService'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { encryptAES, getDateStr } from '@/services/EncryptService'
|
||||
|
||||
export const useDeviceStore = defineStore('device', () => {
|
||||
// Properties - State values
|
||||
const current = ref({
|
||||
uuid: false,
|
||||
key: false,
|
||||
})
|
||||
|
||||
const remote = ref([])
|
||||
const server = ref({
|
||||
status: false,
|
||||
})
|
||||
|
||||
// Current device
|
||||
const uuid = () => {
|
||||
if (!current.value.uuid && localStorage.getItem('deviceId')) {
|
||||
current.value.uuid = localStorage.getItem('deviceId')
|
||||
} else if (!current.value.uuid) {
|
||||
current.value.uuid = setDeviceId()
|
||||
}
|
||||
return current.value.uuid
|
||||
}
|
||||
|
||||
const setDeviceId = () => {
|
||||
const uuid = uuidv4()
|
||||
localStorage.setItem('deviceId', uuid)
|
||||
return uuid
|
||||
}
|
||||
|
||||
const key = () => {
|
||||
if (!current.value.key && localStorage.getItem('deviceKey')) {
|
||||
current.value.key = localStorage.getItem('deviceKey')
|
||||
}
|
||||
return current.value.key
|
||||
}
|
||||
|
||||
const setDeviceKey = (key) => {
|
||||
current.value.key = key
|
||||
localStorage.setItem('deviceKey', key)
|
||||
}
|
||||
|
||||
const removeDeviceKey = () => {
|
||||
current.value.key = false
|
||||
localStorage.removeItem('deviceKey')
|
||||
}
|
||||
|
||||
const serverGetIP = async () => {
|
||||
const request = await axios.post(appUrl() + '/device/server/ip')
|
||||
return `http://${request.data}:${window.__CONFIG__.MCRM__PORT}`
|
||||
}
|
||||
|
||||
// Server application
|
||||
const serverGetRemotes = async (count = false) => {
|
||||
const request = await axios.post(appUrl() + '/device/list')
|
||||
|
||||
if (!request.data.devices) return false
|
||||
|
||||
remote.value = request.data.devices
|
||||
|
||||
if (!count) return remote.value
|
||||
else return Object.keys(remote.value).length
|
||||
}
|
||||
|
||||
const serverStartLink = async (deviceUuid) => {
|
||||
const request = await axios.post(appUrl() + '/device/link/start', { uuid: deviceUuid })
|
||||
return request.data
|
||||
}
|
||||
|
||||
// Remote application
|
||||
const remoteCheckServerAccess = async () => {
|
||||
const check = await axios.post(appUrl() + '/device/access/check', { uuid: uuid() })
|
||||
server.value.access = check.data
|
||||
return check.data
|
||||
}
|
||||
|
||||
const remoteRequestServerAccess = async (deviceName, deviceType) => {
|
||||
const request = await axios.post(appUrl() + '/device/access/request', {
|
||||
uuid: uuid(),
|
||||
name: deviceName,
|
||||
type: deviceType,
|
||||
})
|
||||
return request
|
||||
}
|
||||
const remotePingLink = async (cb) => {
|
||||
const pingInterval = setInterval(() => {
|
||||
axios.post(appUrl() + '/device/link/ping', { uuid: uuid() }).then((data) => {
|
||||
if (data.data) {
|
||||
clearInterval(pingInterval)
|
||||
cb(data.data)
|
||||
}
|
||||
})
|
||||
}, 1000)
|
||||
}
|
||||
|
||||
const remoteHandshake = async (keyStr = false) => {
|
||||
if (!keyStr) keyStr = key()
|
||||
|
||||
if (!keyStr) return false
|
||||
|
||||
const handshake = await axios.post(appUrl() + '/device/handshake', {
|
||||
uuid: uuid(),
|
||||
shake: encryptAES(keyStr, getDateStr()),
|
||||
})
|
||||
|
||||
if (!handshake.data) removeDeviceKey()
|
||||
|
||||
return handshake.data
|
||||
}
|
||||
|
||||
return {
|
||||
remote,
|
||||
server,
|
||||
uuid,
|
||||
setDeviceId,
|
||||
key,
|
||||
setDeviceKey,
|
||||
removeDeviceKey,
|
||||
serverGetIP,
|
||||
serverGetRemotes,
|
||||
serverStartLink,
|
||||
remoteCheckServerAccess,
|
||||
remoteRequestServerAccess,
|
||||
remotePingLink,
|
||||
remoteHandshake,
|
||||
}
|
||||
})
|
||||
260
fe/src/stores/macrorecorder.js
Normal file
|
|
@ -0,0 +1,260 @@
|
|||
/*
|
||||
Macrame is a program that enables the user to create keyboard macros and button panels.
|
||||
The macros are saved as simple JSON files and can be linked to the button panels. The panels can
|
||||
be created with HTML and CSS.
|
||||
|
||||
Copyright (C) 2025 Jesse Malotaux
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { ref } from 'vue'
|
||||
import { defineStore } from 'pinia'
|
||||
|
||||
import { filterKey, isRepeat, invalidMacro, translateJSON } from '../services/MacroRecordService'
|
||||
import axios from 'axios'
|
||||
import { appUrl } from '@/services/ApiService'
|
||||
|
||||
export const useMacroRecorderStore = defineStore('macrorecorder', () => {
|
||||
// Properties - State values
|
||||
const state = ref({
|
||||
record: false,
|
||||
edit: false,
|
||||
editKey: false,
|
||||
editDelay: false,
|
||||
validationErrors: false,
|
||||
})
|
||||
|
||||
const macroName = ref('')
|
||||
|
||||
const steps = ref([])
|
||||
|
||||
const delay = ref({
|
||||
start: 0,
|
||||
fixed: false,
|
||||
})
|
||||
|
||||
// Getters - Computed values
|
||||
const getEditKey = () => steps.value[state.value.editKey]
|
||||
const getAdjacentKey = (pos, includeDelay = false) => {
|
||||
let returnVal = false
|
||||
|
||||
const mod = pos == 'before' ? -1 : 1
|
||||
const keyIndex = state.value.editKey + 2 * mod
|
||||
const delayIndex = includeDelay ? state.value.editKey + 1 * mod : false
|
||||
|
||||
if (steps.value[keyIndex]) returnVal = steps.value[keyIndex]
|
||||
if (delayIndex && steps.value[delayIndex])
|
||||
returnVal = {
|
||||
delay: steps.value[delayIndex],
|
||||
key: steps.value[keyIndex],
|
||||
delayIndex: delayIndex,
|
||||
}
|
||||
|
||||
return returnVal
|
||||
}
|
||||
|
||||
const getEditDelay = () => steps.value[state.value.editDelay]
|
||||
|
||||
// Setters - Actions
|
||||
const recordStep = (e, direction = false, key = false) => {
|
||||
if ((e.ctrlKey, e.shiftKey, e.altKey, e.metaKey)) e.preventDefault()
|
||||
|
||||
const lastStep = steps.value[steps.value.length - 1]
|
||||
|
||||
let stepVal = {}
|
||||
|
||||
if (typeof e === 'object' && !isRepeat(lastStep, e, direction)) {
|
||||
if (key === false) recordDelay()
|
||||
|
||||
stepVal = {
|
||||
type: 'key',
|
||||
key: e.key,
|
||||
code: e.code,
|
||||
location: e.location,
|
||||
direction: direction,
|
||||
keyObj: filterKey(e),
|
||||
}
|
||||
} else if (direction && key !== false) {
|
||||
stepVal = steps.value[key]
|
||||
stepVal.direction = direction
|
||||
} else if (typeof e === 'number') {
|
||||
stepVal = { type: 'delay', value: parseFloat(e.toFixed()) }
|
||||
}
|
||||
|
||||
if (key !== false) steps.value[key] = stepVal
|
||||
else steps.value.push(stepVal)
|
||||
}
|
||||
|
||||
const recordDelay = () => {
|
||||
if (delay.value.fixed !== false)
|
||||
recordStep(delay.value.fixed) // Record fixed delay
|
||||
else if (delay.value.start == 0)
|
||||
delay.value.start = performance.now() // Record start of delay
|
||||
else {
|
||||
recordStep(performance.now() - delay.value.start) // Record end of delay
|
||||
delay.value.start = performance.now() // Reset start
|
||||
}
|
||||
}
|
||||
|
||||
const insertKey = (e, direction, adjacentDelayIndex) => {
|
||||
let min, max, newKeyIndex, newDelayIndex
|
||||
|
||||
if (adjacentDelayIndex === null) {
|
||||
min = state.value.editKey == 0 ? 0 : state.value.editKey
|
||||
max = state.value.editKey == 0 ? 1 : false
|
||||
|
||||
newKeyIndex = max === false ? min + 2 : min
|
||||
newDelayIndex = min + 1
|
||||
} else if (state.value.editKey < adjacentDelayIndex) {
|
||||
min = state.value.editKey
|
||||
max = adjacentDelayIndex
|
||||
newKeyIndex = min + 2
|
||||
newDelayIndex = min + 1
|
||||
} else {
|
||||
min = adjacentDelayIndex
|
||||
max = state.value.editKey
|
||||
newKeyIndex = min + 1
|
||||
newDelayIndex = min + 2
|
||||
}
|
||||
|
||||
if (max !== false) {
|
||||
for (let i = Object.keys(steps.value).length - 1; i >= max; i--) {
|
||||
steps.value[i + 2] = steps.value[i]
|
||||
}
|
||||
}
|
||||
|
||||
recordStep(e, direction, newKeyIndex)
|
||||
recordStep(10, false, newDelayIndex)
|
||||
|
||||
state.value.editKey = false
|
||||
}
|
||||
|
||||
const deleteEditKey = () => {
|
||||
if (state.value.editKey === 0) steps.value.splice(state.value.editKey, 2)
|
||||
else steps.value.splice(state.value.editKey - 1, 2)
|
||||
state.value.editKey = false
|
||||
}
|
||||
|
||||
const restartDelay = () => {
|
||||
delay.value.start = performance.now()
|
||||
}
|
||||
|
||||
const changeName = (name) => {
|
||||
macroName.value = name
|
||||
}
|
||||
|
||||
const changeDelay = (fixed) => {
|
||||
delay.value.fixed = fixed
|
||||
|
||||
formatDelays()
|
||||
}
|
||||
|
||||
const formatDelays = () => {
|
||||
steps.value = steps.value.map((step) => {
|
||||
if (step.type === 'delay' && delay.value.fixed !== false) step.value = delay.value.fixed
|
||||
return step
|
||||
})
|
||||
}
|
||||
|
||||
const toggleEdit = (type, key) => {
|
||||
if (type === 'key') {
|
||||
state.value.editKey = key
|
||||
state.value.editDelay = false
|
||||
}
|
||||
|
||||
if (type === 'delay') {
|
||||
state.value.editKey = false
|
||||
state.value.editDelay = key
|
||||
}
|
||||
}
|
||||
|
||||
const resetEdit = () => {
|
||||
state.value.edit = false
|
||||
state.value.editKey = false
|
||||
state.value.editDelay = false
|
||||
}
|
||||
|
||||
const resetMacro = () => {
|
||||
state.value.record = false
|
||||
delay.value.start = 0
|
||||
macroName.value = ''
|
||||
steps.value = []
|
||||
|
||||
if (state.value.edit) resetEdit()
|
||||
}
|
||||
|
||||
const checkMacro = async () => {
|
||||
const resp = await axios.post(appUrl() + '/macro/check', {
|
||||
macro: macroName.value,
|
||||
})
|
||||
|
||||
return resp.data
|
||||
}
|
||||
|
||||
const saveMacro = async () => {
|
||||
state.value.validationErrors = invalidMacro(steps.value)
|
||||
|
||||
if (state.value.validationErrors) return false
|
||||
|
||||
const resp = await axios.post(appUrl() + '/macro/record', {
|
||||
name: macroName.value,
|
||||
steps: steps.value,
|
||||
})
|
||||
|
||||
return resp.status == 200
|
||||
}
|
||||
|
||||
const deleteMacro = async (macroFilename) => {
|
||||
const resp = await axios.post(appUrl() + '/macro/delete', {
|
||||
macro: macroFilename,
|
||||
})
|
||||
|
||||
if (resp.status == 200) return resp.data
|
||||
else return false
|
||||
}
|
||||
|
||||
const openMacro = async (macroFileName, name) => {
|
||||
const openResp = await axios.post(appUrl() + '/macro/open', {
|
||||
macro: macroFileName,
|
||||
})
|
||||
|
||||
if (openResp.data) steps.value = translateJSON(openResp.data)
|
||||
|
||||
macroName.value = name
|
||||
}
|
||||
|
||||
return {
|
||||
state,
|
||||
macroName,
|
||||
steps,
|
||||
delay,
|
||||
getEditKey,
|
||||
getAdjacentKey,
|
||||
getEditDelay,
|
||||
recordStep,
|
||||
insertKey,
|
||||
deleteEditKey,
|
||||
restartDelay,
|
||||
changeName,
|
||||
changeDelay,
|
||||
toggleEdit,
|
||||
resetEdit,
|
||||
resetMacro,
|
||||
checkMacro,
|
||||
saveMacro,
|
||||
deleteMacro,
|
||||
openMacro,
|
||||
}
|
||||
})
|
||||
82
fe/src/stores/panel.js
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
/*
|
||||
Macrame is a program that enables the user to create keyboard macros and button panels.
|
||||
The macros are saved as simple JSON files and can be linked to the button panels. The panels can
|
||||
be created with HTML and CSS.
|
||||
|
||||
Copyright (C) 2025 Jesse Malotaux
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
import { appUrl } from '@/services/ApiService'
|
||||
import { AuthCall } from '@/services/EncryptService'
|
||||
import axios from 'axios'
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref } from 'vue'
|
||||
|
||||
export const usePanelStore = defineStore('panel', () => {
|
||||
const current = ref({
|
||||
dir: false,
|
||||
name: false,
|
||||
description: false,
|
||||
aspectRatio: false,
|
||||
macros: false,
|
||||
thumb: false,
|
||||
html: false,
|
||||
style: false,
|
||||
})
|
||||
|
||||
const list = ref([])
|
||||
|
||||
const get = async (dir) => {
|
||||
const data = AuthCall({ dir: dir })
|
||||
|
||||
const resp = await axios.post(appUrl() + '/panel/get', data)
|
||||
|
||||
if (!resp.data && !current.value.html) return false
|
||||
|
||||
current.value.name = resp.data.name
|
||||
current.value.description = resp.data.description
|
||||
current.value.aspectRatio = resp.data.aspectRatio
|
||||
current.value.macros = resp.data.macros
|
||||
current.value.thumb = resp.data.thumb
|
||||
current.value.html = resp.data.html
|
||||
current.value.style = resp.data.style
|
||||
|
||||
return current.value
|
||||
}
|
||||
|
||||
const getList = async (count = false) => {
|
||||
if (list.value.length > 0 && !count) return list.value
|
||||
else if (list.value.length > 0 && count) return list.value.length
|
||||
|
||||
const data = AuthCall()
|
||||
|
||||
const resp = await axios.post(appUrl() + '/panel/list', data)
|
||||
list.value = resp.data
|
||||
|
||||
if (!resp.data && !count) return false
|
||||
else if (!resp.data && count) return 0
|
||||
|
||||
if (!count) return list.value
|
||||
else return list.value.length
|
||||
}
|
||||
|
||||
return {
|
||||
current,
|
||||
list,
|
||||
get,
|
||||
getList,
|
||||
}
|
||||
})
|
||||
105
fe/src/views/DashboardView.vue
Normal file
|
|
@ -0,0 +1,105 @@
|
|||
<!--
|
||||
Macrame is a program that enables the user to create keyboard macros and button panels.
|
||||
The macros are saved as simple JSON files and can be linked to the button panels. The panels can
|
||||
be created with HTML and CSS.
|
||||
|
||||
Copyright (C) 2025 Jesse Malotaux
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div id="dashboard" class="panel">
|
||||
<div class="panel__title">
|
||||
<h1>Dashboard</h1>
|
||||
<div>
|
||||
<em v-if="isLocal()">This is the server dashboard.</em>
|
||||
<em v-else>This is the remote dashboard.</em>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel__content !h-fit !gap-y-16">
|
||||
<ServerView v-if="isLocal()" />
|
||||
<RemoteView v-else />
|
||||
<div class="grid gap-2 text-slate-300">
|
||||
<h3>About Macrame</h3>
|
||||
<p>
|
||||
Macrame is an open-source application designed to turn any device into a customizable
|
||||
button panel. Whether you're optimizing your workflow or enhancing your gaming experience,
|
||||
Macrame makes it simple to create and link macros to your button panels.
|
||||
</p>
|
||||
<p>
|
||||
For more information, including details on licensing, visit
|
||||
<a href="https://macrame.github.io" target="_blank">https://macrame.github.io</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { isLocal } from '@/services/ApiService'
|
||||
import ServerView from '@/components/dashboard/ServerView.vue'
|
||||
import RemoteView from '@/components/dashboard/RemoteView.vue'
|
||||
</script>
|
||||
|
||||
<style>
|
||||
@reference "@/assets/main.css";
|
||||
|
||||
.dashboard-block {
|
||||
@apply md:!row-start-1
|
||||
grid
|
||||
justify-items-center
|
||||
gap-4;
|
||||
|
||||
&#devices .icon__container,
|
||||
&#server .icon__container {
|
||||
@apply bg-sky-300/30
|
||||
text-sky-400
|
||||
border-sky-300/60;
|
||||
}
|
||||
|
||||
&#macros .icon__container {
|
||||
@apply bg-amber-300/30
|
||||
text-amber-400
|
||||
border-amber-300/60;
|
||||
}
|
||||
|
||||
&#panels .icon__container {
|
||||
@apply bg-rose-300/30
|
||||
text-rose-400
|
||||
border-rose-300/60;
|
||||
}
|
||||
|
||||
.icon__container {
|
||||
@apply flex
|
||||
justify-center
|
||||
items-center
|
||||
size-16
|
||||
aspect-square
|
||||
rounded-full
|
||||
border;
|
||||
|
||||
svg {
|
||||
@apply size-8;
|
||||
}
|
||||
}
|
||||
|
||||
p {
|
||||
@apply opacity-50
|
||||
w-42
|
||||
text-center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
42
fe/src/views/DevicesView.vue
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
<!--
|
||||
Macrame is a program that enables the user to create keyboard macros and button panels.
|
||||
The macros are saved as simple JSON files and can be linked to the button panels. The panels can
|
||||
be created with HTML and CSS.
|
||||
|
||||
Copyright (C) 2025 Jesse Malotaux
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div id="devices-view" class="panel">
|
||||
<h1 class="panel__title">
|
||||
{{ isLocal() ? 'Remote devices' : 'Server' }}
|
||||
</h1>
|
||||
<div class="grid gap-8 pr-2 panel__content">
|
||||
<ServerView v-if="isLocal()" />
|
||||
<RemoteView v-else />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import ServerView from '@/components/devices/ServerView.vue'
|
||||
import RemoteView from '@/components/devices/RemoteView.vue'
|
||||
import { isLocal } from '@/services/ApiService'
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@reference "@/assets/main.css";
|
||||
</style>
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
<template>
|
||||
<div>
|
||||
<img src="../assets/logo.svg" alt="" />
|
||||
<h1>Home</h1>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup></script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
48
fe/src/views/MacrosView.vue
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
<!--
|
||||
Macrame is a program that enables the user to create keyboard macros and button panels.
|
||||
The macros are saved as simple JSON files and can be linked to the button panels. The panels can
|
||||
be created with HTML and CSS.
|
||||
|
||||
Copyright (C) 2025 Jesse Malotaux
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div id="macros" class="panel">
|
||||
<h1 class="panel__title">Macros</h1>
|
||||
<div class="panel__content !p-0 !overflow-hidden">
|
||||
<div class="macro-panel__content">
|
||||
<MacroOverview />
|
||||
<MacroRecorder />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import MacroOverview from '@/components/macros/MacroOverview.vue'
|
||||
import MacroRecorder from '../components/macros/MacroRecorder.vue'
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@reference "@/assets/main.css";
|
||||
|
||||
.macro-panel__content {
|
||||
@apply grid
|
||||
grid-cols-[25ch_1fr]
|
||||
gap-6
|
||||
pt-2;
|
||||
}
|
||||
</style>
|
||||
82
fe/src/views/PanelsView.vue
Normal file
|
|
@ -0,0 +1,82 @@
|
|||
<!--
|
||||
Macrame is a program that enables the user to create keyboard macros and button panels.
|
||||
The macros are saved as simple JSON files and can be linked to the button panels. The panels can
|
||||
be created with HTML and CSS.
|
||||
|
||||
Copyright (C) 2025 Jesse Malotaux
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div id="panels" class="panel">
|
||||
<h1 class="flex items-end justify-between !w-full panel__title">
|
||||
<div>Panels</div>
|
||||
<ButtonComp
|
||||
v-if="panel.function != 'overview'"
|
||||
variant="subtle"
|
||||
size="sm"
|
||||
@click="router.push('/panels')"
|
||||
>
|
||||
<IconArrowLeft /> Overview
|
||||
</ButtonComp>
|
||||
</h1>
|
||||
<div :class="`panel__content !p-0 !pt-4 ${panel.function == 'overview' ?? '!pr-4'}`">
|
||||
<PanelsOverview v-if="panel.function == 'overview'" />
|
||||
<PanelEdit v-if="panel.function == 'edit'" :dirname="panel.dirname" />
|
||||
<PanelView v-if="panel.function == 'preview'" :dirname="panel.dirname" />
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import ButtonComp from '@/components/base/ButtonComp.vue'
|
||||
import PanelEdit from '@/components/panels/PanelEdit.vue'
|
||||
import PanelView from '@/components/panels/PanelView.vue'
|
||||
import PanelsOverview from '@/components/panels/PanelsOverview.vue'
|
||||
import { isLocal } from '@/services/ApiService'
|
||||
import { IconArrowLeft } from '@tabler/icons-vue'
|
||||
import { onMounted, onUpdated, reactive } from 'vue'
|
||||
import { useRoute, useRouter } from 'vue-router'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
|
||||
const panel = reactive({
|
||||
function: '',
|
||||
dirname: '',
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
setVarsByRoute()
|
||||
})
|
||||
|
||||
onUpdated(() => {
|
||||
setVarsByRoute()
|
||||
})
|
||||
|
||||
const setVarsByRoute = () => {
|
||||
if (route.name.includes('panel-')) {
|
||||
panel.function = route.name == 'panel-edit' ? 'edit' : 'preview'
|
||||
} else {
|
||||
panel.function = 'overview'
|
||||
}
|
||||
|
||||
panel.dirname = route.params.dirname
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@reference "@/assets/main.css";
|
||||
</style>
|
||||
28
fe/src/views/SettingsView.vue
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
<!--
|
||||
Macrame is a program that enables the user to create keyboard macros and button panels.
|
||||
The macros are saved as simple JSON files and can be linked to the button panels. The panels can
|
||||
be created with HTML and CSS.
|
||||
|
||||
Copyright (C) 2025 Jesse Malotaux
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
the Free Software Foundation, either version 3 of the License, or
|
||||
(at your option) any later version.
|
||||
|
||||
This program is distributed in the hope that it will be useful,
|
||||
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
GNU General Public License for more details.
|
||||
|
||||
You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
-->
|
||||
|
||||
<template>
|
||||
<div></div>
|
||||
</template>
|
||||
|
||||
<script setup></script>
|
||||
|
||||
<style lang="scss" scoped></style>
|
||||
|
|
@ -3,25 +3,29 @@ import { fileURLToPath, URL } from 'node:url'
|
|||
import { defineConfig } from 'vite'
|
||||
import vue from '@vitejs/plugin-vue'
|
||||
import vueDevTools from 'vite-plugin-vue-devtools'
|
||||
import tailwindcss from '@tailwindcss/vite'
|
||||
|
||||
// https://vite.dev/config/
|
||||
export default defineConfig({
|
||||
plugins: [vue(), vueDevTools()],
|
||||
server: {
|
||||
host: 'localhost',
|
||||
port: 5173,
|
||||
watch: {
|
||||
usePolling: true,
|
||||
},
|
||||
},
|
||||
plugins: [vue(), vueDevTools(), tailwindcss()],
|
||||
envDir: '../',
|
||||
assets: ['assets'],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': fileURLToPath(new URL('./src', import.meta.url)),
|
||||
},
|
||||
},
|
||||
// server: {
|
||||
// port: 6969,
|
||||
// hmr: {
|
||||
// host: 'localhost',
|
||||
// },
|
||||
// },
|
||||
base: '/',
|
||||
publicDir: '../public',
|
||||
build: {
|
||||
outDir: '../public',
|
||||
sourcemap: true,
|
||||
minify: true,
|
||||
},
|
||||
})
|
||||
|
|
|
|||
46
go.mod
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
module macrame
|
||||
|
||||
go 1.24.1
|
||||
|
||||
require (
|
||||
github.com/getlantern/systray v1.2.2
|
||||
github.com/go-vgo/robotgo v0.110.7
|
||||
)
|
||||
|
||||
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/getlantern/context v0.0.0-20190109183933-c447772a6520 // indirect
|
||||
github.com/getlantern/errors v0.0.0-20190325191628-abdb3e3e36f7 // indirect
|
||||
github.com/getlantern/golog v0.0.0-20190830074920-4ef2e798c2d7 // indirect
|
||||
github.com/getlantern/hex v0.0.0-20190417191902-c6586a6fe0b7 // indirect
|
||||
github.com/getlantern/hidden v0.0.0-20190325191715-f02dbb02be55 // indirect
|
||||
github.com/getlantern/ops v0.0.0-20190325191751-d70cb0d6f85f // indirect
|
||||
github.com/go-ole/go-ole v1.3.0 // indirect
|
||||
github.com/go-stack/stack v1.8.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/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c // 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.39.0 // indirect
|
||||
golang.org/x/sys v0.32.0 // indirect
|
||||
)
|
||||
102
go.sum
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
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.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
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/getlantern/context v0.0.0-20190109183933-c447772a6520 h1:NRUJuo3v3WGC/g5YiyF790gut6oQr5f3FBI88Wv0dx4=
|
||||
github.com/getlantern/context v0.0.0-20190109183933-c447772a6520/go.mod h1:L+mq6/vvYHKjCX2oez0CgEAJmbq1fbb/oNJIWQkBybY=
|
||||
github.com/getlantern/errors v0.0.0-20190325191628-abdb3e3e36f7 h1:6uJ+sZ/e03gkbqZ0kUG6mfKoqDb4XMAzMIwlajq19So=
|
||||
github.com/getlantern/errors v0.0.0-20190325191628-abdb3e3e36f7/go.mod h1:l+xpFBrCtDLpK9qNjxs+cHU6+BAdlBaxHqikB6Lku3A=
|
||||
github.com/getlantern/golog v0.0.0-20190830074920-4ef2e798c2d7 h1:guBYzEaLz0Vfc/jv0czrr2z7qyzTOGC9hiQ0VC+hKjk=
|
||||
github.com/getlantern/golog v0.0.0-20190830074920-4ef2e798c2d7/go.mod h1:zx/1xUUeYPy3Pcmet8OSXLbF47l+3y6hIPpyLWoR9oc=
|
||||
github.com/getlantern/hex v0.0.0-20190417191902-c6586a6fe0b7 h1:micT5vkcr9tOVk1FiH8SWKID8ultN44Z+yzd2y/Vyb0=
|
||||
github.com/getlantern/hex v0.0.0-20190417191902-c6586a6fe0b7/go.mod h1:dD3CgOrwlzca8ed61CsZouQS5h5jIzkK9ZWrTcf0s+o=
|
||||
github.com/getlantern/hidden v0.0.0-20190325191715-f02dbb02be55 h1:XYzSdCbkzOC0FDNrgJqGRo8PCMFOBFL9py72DRs7bmc=
|
||||
github.com/getlantern/hidden v0.0.0-20190325191715-f02dbb02be55/go.mod h1:6mmzY2kW1TOOrVy+r41Za2MxXM+hhqTtY3oBKd2AgFA=
|
||||
github.com/getlantern/ops v0.0.0-20190325191751-d70cb0d6f85f h1:wrYrQttPS8FHIRSlsrcuKazukx/xqO/PpLZzZXsF+EA=
|
||||
github.com/getlantern/ops v0.0.0-20190325191751-d70cb0d6f85f/go.mod h1:D5ao98qkA6pxftxoqzibIBBrLSUli+kYnJqrgBf9cIA=
|
||||
github.com/getlantern/systray v1.2.2 h1:dCEHtfmvkJG7HZ8lS/sLklTH4RKUcIsKrAD9sThoEBE=
|
||||
github.com/getlantern/systray v1.2.2/go.mod h1:pXFOI1wwqwYXEhLPm9ZGjS2u/vVELeIgNMY5HvhHhcE=
|
||||
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-stack/stack v1.8.0 h1:5SgMzNM5HxrEjV0ww2lTmX6E2Izsfxas4+YHWRs3Lsk=
|
||||
github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY=
|
||||
github.com/go-vgo/robotgo v0.110.7 h1:4scqQrJOBHoFCfcMROYEVFBxHvB3nF/UN6DWoRIFzBE=
|
||||
github.com/go-vgo/robotgo v0.110.7/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/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/walk v0.0.0-20210112085537-c389da54e794/go.mod h1:E23UucZGqpuUANJooIbHWCufXvOcT6E7Stq81gU+CSQ=
|
||||
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/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c h1:rp5dCmg/yLR3mgFuSOe4oEnDDmGLROTvMragMUXpTQw=
|
||||
github.com/oxtoacart/bpool v0.0.0-20190530202638-03653db5a59c/go.mod h1:X07ZCGwUbLaax7L0S3Tw4hpejzu63ZrrQiUe6W0hcy0=
|
||||
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/skratchdot/open-golang v0.0.0-20200116055534-eef842397966/go.mod h1:sUM3LWHvSMaG192sy56D9F7CNvL7jUJVXoqM1QKLnog=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
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.39.0 h1:ZCu7HMWDxpXpaiKdhzIfaltL9Lp31x/3fCP11bc6/fY=
|
||||
golang.org/x/net v0.39.0/go.mod h1:X7NRbYVEA+ewNkCNyJ513WmMdQ3BineSwVtN2zD/d+E=
|
||||
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.32.0 h1:s77OFDvIQeibCmezSnk/q6iAfkdiQaJi4VzroCFrN20=
|
||||
golang.org/x/sys v0.32.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
gopkg.in/Knetic/govaluate.v3 v3.0.0/go.mod h1:csKLBORsPbafmSCGTEh3U7Ozmsuq8ZSIlKk1bcqph0E=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
32
install.bat
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
@echo off
|
||||
|
||||
set ruleName="Macrame LAN Access"
|
||||
set exePath=%~dp0be\Macrame.exe
|
||||
|
||||
:: Check if rule exists
|
||||
netsh advfirewall firewall show rule name=%ruleName% >nul 2>&1
|
||||
if %errorlevel%==1 (
|
||||
netsh advfirewall firewall add rule name=%ruleName% dir=in action=allow program=%exePath% protocol=tcp profile=private enabled=true
|
||||
echo Firewall rule '%ruleName%' added for %exePath%
|
||||
) else (
|
||||
echo Firewall rule '%ruleName%' already exists
|
||||
)
|
||||
|
||||
:: Navigate to the "be" directory
|
||||
cd /d "%~dp0be"
|
||||
|
||||
echo Moved to Backend directory
|
||||
|
||||
:: Run setup.exe to generate configuration and necessary files
|
||||
start /wait Setup.exe
|
||||
|
||||
:: Run Caddy to generate certificates and serve content
|
||||
:: start /wait caddy.exe start --config CaddyFile
|
||||
|
||||
:: taskkill /f /im caddy.exe
|
||||
|
||||
:: Now start macrame.exe
|
||||
start Macrame.exe
|
||||
|
||||
:: End of script
|
||||
exit
|
||||