mirror of
https://github.com/Macrame-App/Macrame
synced 2025-12-29 07:19:26 +00:00
Redundant commit: only to switch branches. Can be removed.
This commit is contained in:
commit
59dd711ab3
102 changed files with 34954 additions and 0 deletions
1
.gitignore
vendored
Normal file
1
.gitignore
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
node_modules
|
||||||
BIN
Screenshot 2025-03-08 004134.jpg
Normal file
BIN
Screenshot 2025-03-08 004134.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 100 KiB |
99
be/app/api.go
Normal file
99
be/app/api.go
Normal file
|
|
@ -0,0 +1,99 @@
|
||||||
|
package app
|
||||||
|
|
||||||
|
import (
|
||||||
|
"be/app/helper"
|
||||||
|
"log"
|
||||||
|
"mime"
|
||||||
|
"net/http"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ApiCORS(w http.ResponseWriter, r *http.Request) (http.ResponseWriter, *http.Request) {
|
||||||
|
origin := r.Header.Get("Origin")
|
||||||
|
|
||||||
|
w.Header().Set("Access-Control-Allow-Origin", "http://localhost:5173")
|
||||||
|
|
||||||
|
if strings.HasPrefix(r.Host, "192.168.") {
|
||||||
|
log.Println("lan device")
|
||||||
|
w.Header().Set("Access-Control-Allow-Origin", origin)
|
||||||
|
}
|
||||||
|
|
||||||
|
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
|
||||||
|
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Accept, Accept-Language, Accept-Encoding")
|
||||||
|
|
||||||
|
return w, r
|
||||||
|
}
|
||||||
|
|
||||||
|
func ApiGet(w http.ResponseWriter, r *http.Request) {
|
||||||
|
file := "" // base directory
|
||||||
|
|
||||||
|
if r.URL.Path != "/" {
|
||||||
|
file = "../public" + r.URL.Path // request
|
||||||
|
}
|
||||||
|
contentType := mime.TypeByExtension(filepath.Ext(file)) // get content type
|
||||||
|
|
||||||
|
if contentType != "" {
|
||||||
|
w.Header().Set("Content-Type", contentType) // set content type header
|
||||||
|
}
|
||||||
|
|
||||||
|
if contentType == "" {
|
||||||
|
file = "../public/index.html" // default
|
||||||
|
}
|
||||||
|
|
||||||
|
// log.Println("GET:", file)
|
||||||
|
|
||||||
|
http.ServeFile(w, r, file) // serve file
|
||||||
|
}
|
||||||
|
|
||||||
|
func ApiPost(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
|
access, data := helper.EndpointAccess(w, r)
|
||||||
|
|
||||||
|
if !access {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println("api post", data == "")
|
||||||
|
if data != "" {
|
||||||
|
ApiAuth(data, w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
switch r.URL.Path {
|
||||||
|
case "/macro/record":
|
||||||
|
SaveMacro(w, r)
|
||||||
|
case "/macro/list":
|
||||||
|
ListMacros(w, r)
|
||||||
|
case "/macro/delete":
|
||||||
|
DeleteMacro(w, r)
|
||||||
|
case "/macro/play":
|
||||||
|
PlayMacro("", w, r)
|
||||||
|
case "/device/list":
|
||||||
|
DeviceList(w, r)
|
||||||
|
case "/device/access/check":
|
||||||
|
DeviceAccessCheck(w, r)
|
||||||
|
case "/device/access/request":
|
||||||
|
DeviceAccessRequest(w, r)
|
||||||
|
case "/device/link/ping":
|
||||||
|
PingLink(w, r)
|
||||||
|
case "/device/link/start":
|
||||||
|
StartLink(w, r)
|
||||||
|
case "/device/link/poll":
|
||||||
|
PollLink(w, r)
|
||||||
|
case "/device/link/remove":
|
||||||
|
RemoveLink("", w, r)
|
||||||
|
case "/device/handshake":
|
||||||
|
Handshake(w, r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ApiAuth(data string, w http.ResponseWriter, r *http.Request) {
|
||||||
|
log.Println("apiauth", data != "")
|
||||||
|
switch r.URL.Path {
|
||||||
|
case "/macro/play":
|
||||||
|
PlayMacro(data, w, r)
|
||||||
|
case "/device/link/remove":
|
||||||
|
RemoveLink(data, w, r)
|
||||||
|
}
|
||||||
|
}
|
||||||
245
be/app/device.go
Normal file
245
be/app/device.go
Normal file
|
|
@ -0,0 +1,245 @@
|
||||||
|
package app
|
||||||
|
|
||||||
|
import (
|
||||||
|
"be/app/helper"
|
||||||
|
"be/app/structs"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"math/rand"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func DeviceList(w http.ResponseWriter, r *http.Request) {
|
||||||
|
log.Println("device list")
|
||||||
|
dir := "devices"
|
||||||
|
files, err := os.ReadDir(dir)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
devices := make(map[string]map[string]interface{})
|
||||||
|
|
||||||
|
for _, file := range files {
|
||||||
|
filePath := dir + "/" + file.Name()
|
||||||
|
ext := filepath.Ext(filePath)
|
||||||
|
device := strings.TrimSuffix(file.Name(), ext)
|
||||||
|
|
||||||
|
log.Println(device, ext)
|
||||||
|
|
||||||
|
if _, ok := devices[device]; !ok {
|
||||||
|
devices[device] = make(map[string]interface{})
|
||||||
|
}
|
||||||
|
|
||||||
|
if ext == ".json" {
|
||||||
|
devices[device]["settings"] = readDeviceSettings(filePath)
|
||||||
|
}
|
||||||
|
if ext == ".key" {
|
||||||
|
devices[device]["key"] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
result := map[string]interface{}{
|
||||||
|
"devices": devices,
|
||||||
|
}
|
||||||
|
|
||||||
|
json.NewEncoder(w).Encode(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func readDeviceSettings(filepath string) (settings structs.Settings) {
|
||||||
|
data, err := os.ReadFile(filepath)
|
||||||
|
if err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = json.Unmarshal(data, &settings)
|
||||||
|
if err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
}
|
||||||
|
log.Println(settings)
|
||||||
|
return settings
|
||||||
|
}
|
||||||
|
|
||||||
|
func DeviceAccessCheck(w http.ResponseWriter, r *http.Request) {
|
||||||
|
log.Println("device access check")
|
||||||
|
var req structs.Check
|
||||||
|
|
||||||
|
err := json.NewDecoder(r.Body).Decode(&req)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_, errSett := os.Stat("devices/" + req.Uuid + ".json")
|
||||||
|
_, errKey := os.Stat("devices/" + req.Uuid + ".key")
|
||||||
|
|
||||||
|
if (errSett == nil) && (errKey == nil) {
|
||||||
|
log.Println("authorized")
|
||||||
|
json.NewEncoder(w).Encode("authorized")
|
||||||
|
} else if (errSett == nil) && (errKey != nil) {
|
||||||
|
log.Println("unauthorized")
|
||||||
|
json.NewEncoder(w).Encode("unauthorized")
|
||||||
|
} else {
|
||||||
|
log.Println("unauthorized")
|
||||||
|
json.NewEncoder(w).Encode("unlinked")
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func DeviceAccessRequest(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var req structs.Request
|
||||||
|
|
||||||
|
err := json.NewDecoder(r.Body).Decode(&req)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
deviceSettings := structs.Settings{Name: req.Name, Type: req.Type}
|
||||||
|
|
||||||
|
settingsJSON, err := json.Marshal(deviceSettings)
|
||||||
|
if err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = os.WriteFile("devices/"+req.Uuid+".json", settingsJSON, 0644)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
json.NewEncoder(w).Encode("unauthorized")
|
||||||
|
}
|
||||||
|
|
||||||
|
func PingLink(w http.ResponseWriter, r *http.Request) {
|
||||||
|
log.Println("ping link")
|
||||||
|
var req structs.Check
|
||||||
|
err := json.NewDecoder(r.Body).Decode(&req)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
json.NewEncoder(w).Encode(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
key, keyErr := os.ReadFile("devices/" + req.Uuid + ".key")
|
||||||
|
pin, pinErr := os.ReadFile("devices/" + req.Uuid + ".tmp")
|
||||||
|
|
||||||
|
encryptedKey, encErr := helper.EncryptAES(string(pin), string(key))
|
||||||
|
|
||||||
|
log.Println(encryptedKey, string(pin), string(key))
|
||||||
|
|
||||||
|
if keyErr == nil && pinErr == nil && encErr == nil {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.Write([]byte(encryptedKey))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
json.NewEncoder(w).Encode(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func StartLink(w http.ResponseWriter, r *http.Request) {
|
||||||
|
log.Println("start link")
|
||||||
|
var req structs.Check
|
||||||
|
|
||||||
|
err := json.NewDecoder(r.Body).Decode(&req)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
pin := fmt.Sprintf("%04d", rand.Intn(10000))
|
||||||
|
|
||||||
|
deviceKey := helper.GenerateKey()
|
||||||
|
|
||||||
|
err = helper.SaveDeviceKey(req.Uuid, deviceKey)
|
||||||
|
|
||||||
|
if err == nil && helper.TempPinFile(req.Uuid, pin) {
|
||||||
|
json.NewEncoder(w).Encode(pin)
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func PollLink(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var req structs.Check
|
||||||
|
|
||||||
|
err := json.NewDecoder(r.Body).Decode(&req)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
json.NewEncoder(w).Encode(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if helper.CheckPinFile(req.Uuid) {
|
||||||
|
json.NewEncoder(w).Encode(true)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
json.NewEncoder(w).Encode(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func RemoveLink(data string, w http.ResponseWriter, r *http.Request) {
|
||||||
|
req := &structs.Check{}
|
||||||
|
_, err := helper.ParseRequest(req, data, r)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
json.NewEncoder(w).Encode(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err = os.Remove("devices/" + req.Uuid + ".key")
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
json.NewEncoder(w).Encode(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
json.NewEncoder(w).Encode(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Handshake(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var req structs.Handshake
|
||||||
|
|
||||||
|
err := json.NewDecoder(r.Body).Decode(&req)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
deviceKey, err := helper.GetKeyByUuid(req.Uuid)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
json.NewEncoder(w).Encode(false)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
decryptShake, _ := helper.DecryptAES(deviceKey, req.Shake)
|
||||||
|
|
||||||
|
if decryptShake == getDateStr() {
|
||||||
|
os.Remove("devices/" + req.Uuid + ".tmp")
|
||||||
|
|
||||||
|
json.NewEncoder(w).Encode(true)
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
os.Remove("devices/" + req.Uuid + ".key")
|
||||||
|
}
|
||||||
|
|
||||||
|
json.NewEncoder(w).Encode(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getDateStr() string {
|
||||||
|
date := time.Now()
|
||||||
|
year, month, day := date.Date()
|
||||||
|
formattedDate := fmt.Sprintf("%04d%02d%02d", year, month, day)
|
||||||
|
return formattedDate
|
||||||
|
}
|
||||||
102
be/app/helper/api-helper.go
Normal file
102
be/app/helper/api-helper.go
Normal file
|
|
@ -0,0 +1,102 @@
|
||||||
|
package helper
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"log"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"be/app/structs"
|
||||||
|
. "be/app/structs"
|
||||||
|
)
|
||||||
|
|
||||||
|
func EndpointAccess(w http.ResponseWriter, r *http.Request) (bool, string) {
|
||||||
|
ip, _, err := net.SplitHostPort(r.RemoteAddr)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isLocal(ip) && isEndpointAllowed("Local", r.URL.Path)) ||
|
||||||
|
(isLanRemote(ip) && isEndpointAllowed("Remote", r.URL.Path)) {
|
||||||
|
log.Println(r.URL.Path, "endpoint access: accessible")
|
||||||
|
return true, ""
|
||||||
|
} else if isLanRemote(ip) && isEndpointAllowed("Auth", r.URL.Path) {
|
||||||
|
log.Println(r.URL.Path, "endpoint access: authorized")
|
||||||
|
|
||||||
|
data := decryptAuth(r)
|
||||||
|
|
||||||
|
return data != "", data
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println(r.URL.Path, "endpoint access: not authorized or accessible")
|
||||||
|
|
||||||
|
return false, ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func isLocal(ip string) bool {
|
||||||
|
return ip == "127.0.0.1" || ip == "::1"
|
||||||
|
}
|
||||||
|
|
||||||
|
func isLanRemote(ip string) bool {
|
||||||
|
return strings.HasPrefix(ip, "192.168.")
|
||||||
|
}
|
||||||
|
|
||||||
|
func isEndpointAllowed(source string, endpoint string) bool {
|
||||||
|
var endpoints, err = getAllowedEndpoints(source)
|
||||||
|
if err != "" {
|
||||||
|
log.Println(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if (endpoints != nil) && (len(endpoints) > 0) {
|
||||||
|
for _, e := range endpoints {
|
||||||
|
if e == endpoint {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func getAllowedEndpoints(source string) (endpoints []string, err string) {
|
||||||
|
if source == "Local" {
|
||||||
|
return Endpoints.Local, ""
|
||||||
|
}
|
||||||
|
if source == "Remote" {
|
||||||
|
return Endpoints.Remote, ""
|
||||||
|
}
|
||||||
|
if source == "Auth" {
|
||||||
|
return Endpoints.Auth, ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return []string{}, "No allowed endpoints"
|
||||||
|
}
|
||||||
|
|
||||||
|
func decryptAuth(r *http.Request) string {
|
||||||
|
var req structs.Authcall
|
||||||
|
|
||||||
|
err := json.NewDecoder(r.Body).Decode(&req)
|
||||||
|
|
||||||
|
if err != nil || req.Uuid == "" || req.Data == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
deviceKey, errKey := GetKeyByUuid(req.Uuid)
|
||||||
|
decryptData, errDec := DecryptAES(deviceKey, req.Data)
|
||||||
|
|
||||||
|
if errKey != nil && errDec != nil || decryptData == "" {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return decryptData
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseRequest(req interface{}, data string, r *http.Request) (d interface{}, err error) {
|
||||||
|
if data != "" {
|
||||||
|
dataBytes := []byte(data)
|
||||||
|
return req, json.Unmarshal(dataBytes, &req)
|
||||||
|
} else {
|
||||||
|
return req, json.NewDecoder(r.Body).Decode(&req)
|
||||||
|
}
|
||||||
|
}
|
||||||
23
be/app/helper/browser-helper.go
Normal file
23
be/app/helper/browser-helper.go
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
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
|
||||||
|
}
|
||||||
46
be/app/helper/device-helper.go
Normal file
46
be/app/helper/device-helper.go
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
package helper
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TempPinFile(Uuid string, pin string) bool {
|
||||||
|
log.Println("temp pin file", Uuid, pin)
|
||||||
|
err := os.WriteFile("devices/"+Uuid+".tmp", []byte(pin), 0644)
|
||||||
|
if err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
time.AfterFunc(1*time.Minute, func() {
|
||||||
|
log.Println("deleting", Uuid, pin)
|
||||||
|
os.Remove("devices/" + Uuid + ".tmp")
|
||||||
|
})
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func CheckPinFile(Uuid string) bool {
|
||||||
|
_, err := os.Stat("devices/" + Uuid + ".tmp")
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func SaveDeviceKey(Uuid string, key string) error {
|
||||||
|
err := os.WriteFile("devices/"+Uuid+".key", []byte(key), 0644)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetKeyByUuid(Uuid string) (string, error) {
|
||||||
|
data, err := os.ReadFile("devices/" + Uuid + ".key")
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return string(data), nil
|
||||||
|
}
|
||||||
105
be/app/helper/encrypt-helper.go
Normal file
105
be/app/helper/encrypt-helper.go
Normal file
|
|
@ -0,0 +1,105 @@
|
||||||
|
package helper
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto/aes"
|
||||||
|
"crypto/cipher"
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/base64"
|
||||||
|
"errors"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func EncryptAES(key string, plaintext string) (string, error) {
|
||||||
|
origData := []byte(plaintext)
|
||||||
|
|
||||||
|
// Create AES cipher
|
||||||
|
block, err := aes.NewCipher(keyToBytes(key))
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
blockSize := block.BlockSize()
|
||||||
|
|
||||||
|
origData = PKCS5Padding(origData, blockSize)
|
||||||
|
|
||||||
|
iv := []byte(EnvGet("MCRM__IV"))
|
||||||
|
blockMode := cipher.NewCBCEncrypter(block, iv)
|
||||||
|
|
||||||
|
crypted := make([]byte, len(origData))
|
||||||
|
blockMode.CryptBlocks(crypted, origData)
|
||||||
|
|
||||||
|
cryptedString := base64.StdEncoding.EncodeToString(crypted)
|
||||||
|
|
||||||
|
return cryptedString, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func DecryptAES(key string, cryptedText string) (string, error) {
|
||||||
|
crypted, err := base64.StdEncoding.DecodeString(cryptedText)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
block, err := aes.NewCipher(keyToBytes(key))
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
iv := []byte(EnvGet("MCRM__IV"))
|
||||||
|
blockMode := cipher.NewCBCDecrypter(block, iv)
|
||||||
|
|
||||||
|
origData := make([]byte, len(crypted))
|
||||||
|
|
||||||
|
blockMode.CryptBlocks(origData, crypted)
|
||||||
|
origData, err = PKCS5UnPadding(origData)
|
||||||
|
|
||||||
|
if err != nil || len(origData) <= 3 {
|
||||||
|
return "", errors.New("invalid key")
|
||||||
|
}
|
||||||
|
|
||||||
|
origDataString := string(origData)
|
||||||
|
|
||||||
|
return origDataString, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func generateRandomString(length int) string {
|
||||||
|
b := make([]byte, length)
|
||||||
|
_, err := rand.Read(b)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
return base64.StdEncoding.EncodeToString(b)
|
||||||
|
}
|
||||||
|
|
||||||
|
func GenerateKey() string {
|
||||||
|
return strings.Replace(generateRandomString(24), "=", "", -1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func keyToBytes(key string) []byte {
|
||||||
|
// Convert key to bytes
|
||||||
|
keyBytes := []byte(key)
|
||||||
|
|
||||||
|
// If key is 4 characters, append salt
|
||||||
|
if len(key) == 4 {
|
||||||
|
keyBytes = []byte(key + EnvGet("MCRM__SALT"))
|
||||||
|
}
|
||||||
|
|
||||||
|
return keyBytes
|
||||||
|
}
|
||||||
|
|
||||||
|
func PKCS5Padding(ciphertext []byte, blockSize int) []byte {
|
||||||
|
padding := blockSize - len(ciphertext)%blockSize
|
||||||
|
padtext := bytes.Repeat([]byte{byte(padding)}, padding)
|
||||||
|
return append(ciphertext, padtext...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func PKCS5UnPadding(origData []byte) ([]byte, error) {
|
||||||
|
length := len(origData)
|
||||||
|
unpadding := int(origData[length-1])
|
||||||
|
|
||||||
|
if (unpadding >= length) || (unpadding == 0) {
|
||||||
|
return nil, errors.New("unpadding error")
|
||||||
|
}
|
||||||
|
|
||||||
|
return origData[:(length - unpadding)], nil
|
||||||
|
}
|
||||||
16
be/app/helper/env-helper.go
Normal file
16
be/app/helper/env-helper.go
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
package helper
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/joho/godotenv"
|
||||||
|
)
|
||||||
|
|
||||||
|
func EnvGet(key string) string {
|
||||||
|
err := godotenv.Load("../.env")
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal("Error loading .env file")
|
||||||
|
}
|
||||||
|
return os.Getenv("VITE_" + key)
|
||||||
|
}
|
||||||
64
be/app/helper/macro-helper.go
Normal file
64
be/app/helper/macro-helper.go
Normal file
|
|
@ -0,0 +1,64 @@
|
||||||
|
package helper
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"be/app/structs"
|
||||||
|
|
||||||
|
"github.com/go-vgo/robotgo"
|
||||||
|
)
|
||||||
|
|
||||||
|
func FormatMacroFileName(s string) string {
|
||||||
|
// Remove invalid characters
|
||||||
|
re := regexp.MustCompile(`[\/\?\*\>\<\:\\"\|\n]`)
|
||||||
|
s = re.ReplaceAllString(s, "")
|
||||||
|
|
||||||
|
// Replace spaces with underscores
|
||||||
|
s = strings.ReplaceAll(s, " ", "_")
|
||||||
|
|
||||||
|
// Remove special characters
|
||||||
|
re = regexp.MustCompile(`[!@#$%^&\(\)\[\]\{\}\~]`)
|
||||||
|
s = re.ReplaceAllString(s, "")
|
||||||
|
|
||||||
|
// Truncate the string
|
||||||
|
if len(s) > 255 {
|
||||||
|
s = s[:255]
|
||||||
|
}
|
||||||
|
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
func ReadMacroFile(filename string) (steps []structs.Step, err error) {
|
||||||
|
log.Println(filename)
|
||||||
|
|
||||||
|
content, err := os.ReadFile(filename)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal("Error when opening file: ", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = json.Unmarshal(content, &steps)
|
||||||
|
|
||||||
|
return steps, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func RunMacroSteps(steps []structs.Step) {
|
||||||
|
for _, step := range steps {
|
||||||
|
// log.Println(step)
|
||||||
|
switch step.Type {
|
||||||
|
case "key":
|
||||||
|
robotgo.KeyToggle(step.Key, step.Direction)
|
||||||
|
// log.Println("Toggling", step.Key, "to", step.Direction)
|
||||||
|
case "delay":
|
||||||
|
time.Sleep(time.Duration(step.Location) * time.Millisecond)
|
||||||
|
// log.Println("Sleeping for", step.Value, "milliseconds")
|
||||||
|
default:
|
||||||
|
log.Println("Unknown step type:", step.Type)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
88
be/app/macro.go
Normal file
88
be/app/macro.go
Normal file
|
|
@ -0,0 +1,88 @@
|
||||||
|
package app
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"be/app/helper"
|
||||||
|
"be/app/structs"
|
||||||
|
)
|
||||||
|
|
||||||
|
func SaveMacro(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var newMacro structs.NewMacro
|
||||||
|
|
||||||
|
body, err := io.ReadAll(r.Body)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println(string(body))
|
||||||
|
|
||||||
|
err = json.Unmarshal(body, &newMacro)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
stepsJSON, err := json.Marshal(newMacro.Steps)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = os.WriteFile("../macros/"+helper.FormatMacroFileName(newMacro.Name)+".json", stepsJSON, 0644)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ListMacros(w http.ResponseWriter, r *http.Request) {
|
||||||
|
log.Println("listing macros")
|
||||||
|
dir := "../macros"
|
||||||
|
files, err := os.ReadDir(dir)
|
||||||
|
if err != nil {
|
||||||
|
log.Println(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var fileNames []string
|
||||||
|
|
||||||
|
for _, file := range files {
|
||||||
|
filename := filepath.Base(file.Name())
|
||||||
|
filename = strings.TrimSuffix(filename, filepath.Ext(filename))
|
||||||
|
filename = strings.Replace(filename, "_", " ", -1)
|
||||||
|
|
||||||
|
fileNames = append(fileNames, filename)
|
||||||
|
}
|
||||||
|
|
||||||
|
json.NewEncoder(w).Encode(fileNames)
|
||||||
|
}
|
||||||
|
|
||||||
|
func DeleteMacro(w http.ResponseWriter, r *http.Request) {}
|
||||||
|
|
||||||
|
func PlayMacro(data string, w http.ResponseWriter, r *http.Request) {
|
||||||
|
req := &structs.MacroRequest{}
|
||||||
|
_, err := helper.ParseRequest(req, data, r)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
macro := req.Macro
|
||||||
|
|
||||||
|
var filename = helper.FormatMacroFileName(macro)
|
||||||
|
var filepath = fmt.Sprintf("../macros/%s.json", filename)
|
||||||
|
|
||||||
|
macroFile, err := helper.ReadMacroFile(filepath)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
helper.RunMacroSteps(macroFile)
|
||||||
|
}
|
||||||
37
be/app/structs/api-struct.go
Normal file
37
be/app/structs/api-struct.go
Normal file
|
|
@ -0,0 +1,37 @@
|
||||||
|
package structs
|
||||||
|
|
||||||
|
type Allowed struct {
|
||||||
|
Local []string
|
||||||
|
Remote []string
|
||||||
|
Auth []string
|
||||||
|
}
|
||||||
|
|
||||||
|
var Endpoints = Allowed{
|
||||||
|
Local: []string{
|
||||||
|
"/macro/record",
|
||||||
|
"/macro/list",
|
||||||
|
"/macro/delete",
|
||||||
|
"/macro/play",
|
||||||
|
"/device/list",
|
||||||
|
"/device/access/check",
|
||||||
|
"/device/access/request",
|
||||||
|
"/device/link/ping",
|
||||||
|
"/device/link/start",
|
||||||
|
"/device/link/poll",
|
||||||
|
"/device/link/remove",
|
||||||
|
"/device/handshake",
|
||||||
|
},
|
||||||
|
Remote: []string{
|
||||||
|
"/macro/list",
|
||||||
|
"/device/access/check",
|
||||||
|
"/device/access/request",
|
||||||
|
"/device/link/ping",
|
||||||
|
"/device/link/end",
|
||||||
|
"/device/handshake",
|
||||||
|
"/device/auth",
|
||||||
|
},
|
||||||
|
Auth: []string{
|
||||||
|
"/macro/play",
|
||||||
|
"/device/link/remove",
|
||||||
|
},
|
||||||
|
}
|
||||||
31
be/app/structs/device-struct.go
Normal file
31
be/app/structs/device-struct.go
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
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"`
|
||||||
|
}
|
||||||
19
be/app/structs/macro-struct.go
Normal file
19
be/app/structs/macro-struct.go
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
package structs
|
||||||
|
|
||||||
|
type MacroRequest struct {
|
||||||
|
Macro string `json:"macro"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Step struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
Key string `json:"key"`
|
||||||
|
Code string `json:"code"`
|
||||||
|
Location int `json:"location"`
|
||||||
|
Direction string `json:"direction"`
|
||||||
|
Value int `json:"value"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type NewMacro struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Steps []Step `json:"steps"`
|
||||||
|
}
|
||||||
1
be/devices/a42e16a8-0e99-4bb9-a93f-363740c45b24.json
Normal file
1
be/devices/a42e16a8-0e99-4bb9-a93f-363740c45b24.json
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
{"name":"Unknown desktop","type":"desktop"}
|
||||||
1
be/devices/a42e16a8-0e99-4bb9-a93f-363740c45b24.key
Normal file
1
be/devices/a42e16a8-0e99-4bb9-a93f-363740c45b24.key
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
3JYxP8LOq1Y2fhpgEtpXVJ3v4s3qdML3
|
||||||
1
be/devices/f70778be-99c1-4c5c-b1a2-36ef73d971a0.json
Normal file
1
be/devices/f70778be-99c1-4c5c-b1a2-36ef73d971a0.json
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
{"name":"Unknown mobile","type":"mobile"}
|
||||||
1
be/devices/f70778be-99c1-4c5c-b1a2-36ef73d971a0.key
Normal file
1
be/devices/f70778be-99c1-4c5c-b1a2-36ef73d971a0.key
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
4MqIbBoPsHizsWCyeqg6gd/wpQzfhc7e
|
||||||
38
be/go.mod
Normal file
38
be/go.mod
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
module be
|
||||||
|
|
||||||
|
go 1.24.0
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/go-vgo/robotgo v0.110.6
|
||||||
|
github.com/joho/godotenv v1.5.1
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/dblohm7/wingoes v0.0.0-20240820181039-f2b84150679e // indirect
|
||||||
|
github.com/ebitengine/purego v0.8.2 // indirect
|
||||||
|
github.com/gen2brain/shm v0.1.1 // indirect
|
||||||
|
github.com/go-ole/go-ole v1.3.0 // indirect
|
||||||
|
github.com/godbus/dbus/v5 v5.1.0 // indirect
|
||||||
|
github.com/jezek/xgb v1.1.1 // indirect
|
||||||
|
github.com/kbinani/screenshot v0.0.0-20250118074034-a3924b7bbc8c // indirect
|
||||||
|
github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683 // indirect
|
||||||
|
github.com/lxn/win v0.0.0-20210218163916-a377121e959e // indirect
|
||||||
|
github.com/otiai10/gosseract v2.2.1+incompatible // indirect
|
||||||
|
github.com/otiai10/mint v1.6.3 // indirect
|
||||||
|
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
|
||||||
|
github.com/robotn/xgb v0.10.0 // indirect
|
||||||
|
github.com/robotn/xgbutil v0.10.0 // indirect
|
||||||
|
github.com/shirou/gopsutil/v4 v4.25.1 // indirect
|
||||||
|
github.com/tailscale/win v0.0.0-20250213223159-5992cb43ca35 // indirect
|
||||||
|
github.com/tklauser/go-sysconf v0.3.14 // indirect
|
||||||
|
github.com/tklauser/numcpus v0.9.0 // indirect
|
||||||
|
github.com/vcaesar/gops v0.40.0 // indirect
|
||||||
|
github.com/vcaesar/imgo v0.40.2 // indirect
|
||||||
|
github.com/vcaesar/keycode v0.10.1 // indirect
|
||||||
|
github.com/vcaesar/tt v0.20.1 // indirect
|
||||||
|
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||||
|
golang.org/x/exp v0.0.0-20250215185904-eff6e970281f // indirect
|
||||||
|
golang.org/x/image v0.24.0 // indirect
|
||||||
|
golang.org/x/net v0.38.0 // indirect
|
||||||
|
golang.org/x/sys v0.31.0 // indirect
|
||||||
|
)
|
||||||
80
be/go.sum
Normal file
80
be/go.sum
Normal file
|
|
@ -0,0 +1,80 @@
|
||||||
|
github.com/BurntSushi/freetype-go v0.0.0-20160129220410-b763ddbfe298/go.mod h1:D+QujdIlUNfa0igpNMk6UIvlb6C252URs4yupRUV4lQ=
|
||||||
|
github.com/BurntSushi/graphics-go v0.0.0-20160129215708-b43f31a4a966/go.mod h1:Mid70uvE93zn9wgF92A/r5ixgnvX8Lh68fxp9KQBaI0=
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/dblohm7/wingoes v0.0.0-20240820181039-f2b84150679e h1:L+XrFvD0vBIBm+Wf9sFN6aU395t7JROoai0qXZraA4U=
|
||||||
|
github.com/dblohm7/wingoes v0.0.0-20240820181039-f2b84150679e/go.mod h1:SUxUaAK/0UG5lYyZR1L1nC4AaYYvSSYTWQSH3FPcxKU=
|
||||||
|
github.com/ebitengine/purego v0.8.2 h1:jPPGWs2sZ1UgOSgD2bClL0MJIqu58nOmIcBuXr62z1I=
|
||||||
|
github.com/ebitengine/purego v0.8.2/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
||||||
|
github.com/gen2brain/shm v0.1.1 h1:1cTVA5qcsUFixnDHl14TmRoxgfWEEZlTezpUj1vm5uQ=
|
||||||
|
github.com/gen2brain/shm v0.1.1/go.mod h1:UgIcVtvmOu+aCJpqJX7GOtiN7X2ct+TKLg4RTxwPIUA=
|
||||||
|
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
||||||
|
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
|
||||||
|
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
|
||||||
|
github.com/go-vgo/robotgo v0.110.6 h1:1tOxlmTXYg6F3Xs8IT++331MxY2nZ+Q3B6eW312llbo=
|
||||||
|
github.com/go-vgo/robotgo v0.110.6/go.mod h1:eBUjTHY1HYjzdi1+UWJUbxB+b9gE+l4Ei7vQU/9SnLw=
|
||||||
|
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
|
||||||
|
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||||
|
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||||
|
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
|
github.com/jezek/xgb v1.1.1 h1:bE/r8ZZtSv7l9gk6nU0mYx51aXrvnyb44892TwSaqS4=
|
||||||
|
github.com/jezek/xgb v1.1.1/go.mod h1:nrhwO0FX/enq75I7Y7G8iN1ubpSGZEiA3v9e9GyRFlk=
|
||||||
|
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
|
||||||
|
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
|
||||||
|
github.com/kbinani/screenshot v0.0.0-20250118074034-a3924b7bbc8c h1:1IlzDla/ZATV/FsRn1ETf7ir91PHS2mrd4VMunEtd9k=
|
||||||
|
github.com/kbinani/screenshot v0.0.0-20250118074034-a3924b7bbc8c/go.mod h1:Pmpz2BLf55auQZ67u3rvyI2vAQvNetkK/4zYUmpauZQ=
|
||||||
|
github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683 h1:7UMa6KCCMjZEMDtTVdcGu0B1GmmC7QJKiCCjyTAWQy0=
|
||||||
|
github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683/go.mod h1:ilwx/Dta8jXAgpFYFvSWEMwxmbWXyiUHkd5FwyKhb5k=
|
||||||
|
github.com/lxn/win v0.0.0-20210218163916-a377121e959e h1:H+t6A/QJMbhCSEH5rAuRxh+CtW96g0Or0Fxa9IKr4uc=
|
||||||
|
github.com/lxn/win v0.0.0-20210218163916-a377121e959e/go.mod h1:KxxjdtRkfNoYDCUP5ryK7XJJNTnpC8atvtmTheChOtk=
|
||||||
|
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=
|
||||||
|
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
|
||||||
|
github.com/otiai10/gosseract v2.2.1+incompatible h1:Ry5ltVdpdp4LAa2bMjsSJH34XHVOV7XMi41HtzL8X2I=
|
||||||
|
github.com/otiai10/gosseract v2.2.1+incompatible/go.mod h1:XrzWItCzCpFRZ35n3YtVTgq5bLAhFIkascoRo8G32QE=
|
||||||
|
github.com/otiai10/mint v1.6.3 h1:87qsV/aw1F5as1eH1zS/yqHY85ANKVMgkDrf9rcxbQs=
|
||||||
|
github.com/otiai10/mint v1.6.3/go.mod h1:MJm72SBthJjz8qhefc4z1PYEieWmy8Bku7CjcAqyUSM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=
|
||||||
|
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
|
||||||
|
github.com/robotn/xgb v0.0.0-20190912153532-2cb92d044934/go.mod h1:SxQhJskUJ4rleVU44YvnrdvxQr0tKy5SRSigBrCgyyQ=
|
||||||
|
github.com/robotn/xgb v0.10.0 h1:O3kFbIwtwZ3pgLbp1h5slCQ4OpY8BdwugJLrUe6GPIM=
|
||||||
|
github.com/robotn/xgb v0.10.0/go.mod h1:SxQhJskUJ4rleVU44YvnrdvxQr0tKy5SRSigBrCgyyQ=
|
||||||
|
github.com/robotn/xgbutil v0.10.0 h1:gvf7mGQqCWQ68aHRtCxgdewRk+/KAJui6l3MJQQRCKw=
|
||||||
|
github.com/robotn/xgbutil v0.10.0/go.mod h1:svkDXUDQjUiWzLrA0OZgHc4lbOts3C+uRfP6/yjwYnU=
|
||||||
|
github.com/shirou/gopsutil/v4 v4.25.1 h1:QSWkTc+fu9LTAWfkZwZ6j8MSUk4A2LV7rbH0ZqmLjXs=
|
||||||
|
github.com/shirou/gopsutil/v4 v4.25.1/go.mod h1:RoUCUpndaJFtT+2zsZzzmhvbfGoDCJ7nFXKJf8GqJbI=
|
||||||
|
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||||
|
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
|
github.com/tailscale/win v0.0.0-20250213223159-5992cb43ca35 h1:wAZbkTZkqDzWsqxPh2qkBd3KvFU7tcxV0BP0Rnhkxog=
|
||||||
|
github.com/tailscale/win v0.0.0-20250213223159-5992cb43ca35/go.mod h1:aMd4yDHLjbOuYP6fMxj1d9ACDQlSWwYztcpybGHCQc8=
|
||||||
|
github.com/tc-hib/winres v0.2.1 h1:YDE0FiP0VmtRaDn7+aaChp1KiF4owBiJa5l964l5ujA=
|
||||||
|
github.com/tc-hib/winres v0.2.1/go.mod h1:C/JaNhH3KBvhNKVbvdlDWkbMDO9H4fKKDaN7/07SSuk=
|
||||||
|
github.com/tklauser/go-sysconf v0.3.14 h1:g5vzr9iPFFz24v2KZXs/pvpvh8/V9Fw6vQK5ZZb78yU=
|
||||||
|
github.com/tklauser/go-sysconf v0.3.14/go.mod h1:1ym4lWMLUOhuBOPGtRcJm7tEGX4SCYNEEEtghGG/8uY=
|
||||||
|
github.com/tklauser/numcpus v0.9.0 h1:lmyCHtANi8aRUgkckBgoDk1nHCux3n2cgkJLXdQGPDo=
|
||||||
|
github.com/tklauser/numcpus v0.9.0/go.mod h1:SN6Nq1O3VychhC1npsWostA+oW+VOQTxZrS604NSRyI=
|
||||||
|
github.com/vcaesar/gops v0.40.0 h1:I+1RCGiV+LkZJUYNzAd373xs0uM2UyeFdZBmow8HfCM=
|
||||||
|
github.com/vcaesar/gops v0.40.0/go.mod h1:3u/USW7JovqUK6i13VOD3qWfvXXd2TIIKE4PYIv4TOM=
|
||||||
|
github.com/vcaesar/imgo v0.40.2 h1:5GWScRLdBCMtO1v2I1bs+ZmDLZFINxYSMZ+mtUw5qPM=
|
||||||
|
github.com/vcaesar/imgo v0.40.2/go.mod h1:MVCl+FxHI2gTgmiHoi0n5xNCbYcfv9SVtdEOUC92+eo=
|
||||||
|
github.com/vcaesar/keycode v0.10.1 h1:0DesGmMAPWpYTCYddOFiCMKCDKgNnwiQa2QXindVUHw=
|
||||||
|
github.com/vcaesar/keycode v0.10.1/go.mod h1:JNlY7xbKsh+LAGfY2j4M3znVrGEm5W1R8s/Uv6BJcfQ=
|
||||||
|
github.com/vcaesar/tt v0.20.1 h1:D/jUeeVCNbq3ad8M7hhtB3J9x5RZ6I1n1eZ0BJp7M+4=
|
||||||
|
github.com/vcaesar/tt v0.20.1/go.mod h1:cH2+AwGAJm19Wa6xvEa+0r+sXDJBT0QgNQey6mwqLeU=
|
||||||
|
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
|
||||||
|
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
||||||
|
golang.org/x/exp v0.0.0-20250215185904-eff6e970281f h1:oFMYAjX0867ZD2jcNiLBrI9BdpmEkvPyi5YrBGXbamg=
|
||||||
|
golang.org/x/exp v0.0.0-20250215185904-eff6e970281f/go.mod h1:BHOTPb3L19zxehTsLoJXVaTktb06DFgmdW6Wb9s8jqk=
|
||||||
|
golang.org/x/image v0.24.0 h1:AN7zRgVsbvmTfNyqIbbOraYL8mSwcKncEj8ofjgzcMQ=
|
||||||
|
golang.org/x/image v0.24.0/go.mod h1:4b/ITuLfqYq1hqZcjofwctIhi7sZh2WaCjvsBNjjya8=
|
||||||
|
golang.org/x/net v0.38.0 h1:vRMAPTMaeGqVhG5QyLJHqNDwecKTomGeqbnfZyKlBI8=
|
||||||
|
golang.org/x/net v0.38.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8=
|
||||||
|
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20201018230417-eeed37f84f13/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik=
|
||||||
|
golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
23
be/index.html
Normal file
23
be/index.html
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
<!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>
|
||||||
162
be/macroOLD/macro.go
Normal file
162
be/macroOLD/macro.go
Normal file
|
|
@ -0,0 +1,162 @@
|
||||||
|
package macro
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"regexp"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/go-vgo/robotgo"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Step struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
Key string `json:"key"`
|
||||||
|
Code string `json:"code"`
|
||||||
|
Location int `json:"location"`
|
||||||
|
Direction string `json:"direction"`
|
||||||
|
Value int `json:"value"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var newMacro struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Steps []Step `json:"steps"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func Save(w http.ResponseWriter, r *http.Request) {
|
||||||
|
body, err := io.ReadAll(r.Body)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Println(string(body))
|
||||||
|
|
||||||
|
err = json.Unmarshal(body, &newMacro)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
stepsJSON, err := json.Marshal(newMacro.Steps)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = os.WriteFile("../macros/"+makeValidFilename(newMacro.Name)+".json", stepsJSON, 0644)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeValidFilename(s string) string {
|
||||||
|
// Remove invalid characters
|
||||||
|
re := regexp.MustCompile(`[\/\?\*\>\<\:\\"\|\n]`)
|
||||||
|
s = re.ReplaceAllString(s, "")
|
||||||
|
|
||||||
|
// Replace spaces with underscores
|
||||||
|
s = strings.ReplaceAll(s, " ", "_")
|
||||||
|
|
||||||
|
// Remove special characters
|
||||||
|
re = regexp.MustCompile(`[!@#$%^&\(\)\[\]\{\}\~]`)
|
||||||
|
s = re.ReplaceAllString(s, "")
|
||||||
|
|
||||||
|
// Truncate the string
|
||||||
|
if len(s) > 255 {
|
||||||
|
s = s[:255]
|
||||||
|
}
|
||||||
|
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
|
||||||
|
func List(w http.ResponseWriter, r *http.Request) {
|
||||||
|
log.Println("listing macros")
|
||||||
|
dir := "../macros"
|
||||||
|
files, err := os.ReadDir(dir)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var fileNames []string
|
||||||
|
|
||||||
|
for _, file := range files {
|
||||||
|
filename := filepath.Base(file.Name())
|
||||||
|
filename = strings.TrimSuffix(filename, filepath.Ext(filename))
|
||||||
|
filename = strings.Replace(filename, "_", " ", -1)
|
||||||
|
|
||||||
|
fileNames = append(fileNames, filename)
|
||||||
|
}
|
||||||
|
|
||||||
|
json.NewEncoder(w).Encode(fileNames)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Delete(w http.ResponseWriter, r *http.Request) {}
|
||||||
|
|
||||||
|
func Play(w http.ResponseWriter, r *http.Request) {
|
||||||
|
type MacroRequest struct {
|
||||||
|
Macro string `json:"macro"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var req MacroRequest
|
||||||
|
|
||||||
|
err := json.NewDecoder(r.Body).Decode(&req)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
macro := req.Macro
|
||||||
|
|
||||||
|
macroFile, err := readMacroFile(fmt.Sprintf("../macros/%s.json", makeValidFilename(macro)))
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
playMacro(macroFile)
|
||||||
|
// fmt.Println(macroFile)
|
||||||
|
}
|
||||||
|
|
||||||
|
func readMacroFile(filename string) (steps []Step, err error) {
|
||||||
|
|
||||||
|
log.Println(filename)
|
||||||
|
// Let's first read the `config.json` file
|
||||||
|
content, err := os.ReadFile(filename)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal("Error when opening file: ", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now let's unmarshall the data into `steps`
|
||||||
|
err = json.Unmarshal(content, &steps)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal("Error during Unmarshal(): ", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return steps, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func playMacro(steps []Step) {
|
||||||
|
for _, step := range steps {
|
||||||
|
// log.Println(step)
|
||||||
|
switch step.Type {
|
||||||
|
case "key":
|
||||||
|
robotgo.KeyToggle(step.Key, step.Direction)
|
||||||
|
// log.Println("Toggling", step.Key, "to", step.Direction)
|
||||||
|
case "delay":
|
||||||
|
time.Sleep(time.Duration(step.Location) * time.Millisecond)
|
||||||
|
// log.Println("Sleeping for", step.Value, "milliseconds")
|
||||||
|
default:
|
||||||
|
log.Println("Unknown step type:", step.Type)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
26
be/main.go
Normal file
26
be/main.go
Normal file
|
|
@ -0,0 +1,26 @@
|
||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"be/app"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
apiInit(w, r)
|
||||||
|
})
|
||||||
|
|
||||||
|
log.Println(http.ListenAndServe(":6970", nil))
|
||||||
|
}
|
||||||
|
|
||||||
|
func apiInit(w http.ResponseWriter, r *http.Request) {
|
||||||
|
app.ApiCORS(w, r)
|
||||||
|
|
||||||
|
if r.Method == "GET" {
|
||||||
|
app.ApiGet(w, r)
|
||||||
|
} else if r.Method == "POST" {
|
||||||
|
app.ApiPost(w, r)
|
||||||
|
}
|
||||||
|
}
|
||||||
1
be/tmp/build-errors.log
Normal file
1
be/tmp/build-errors.log
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1exit status 1
|
||||||
BIN
be/tmp/main.exe
Normal file
BIN
be/tmp/main.exe
Normal file
Binary file not shown.
35
fe/README.md
Normal file
35
fe/README.md
Normal file
|
|
@ -0,0 +1,35 @@
|
||||||
|
# fe
|
||||||
|
|
||||||
|
This template should help get you started developing with Vue 3 in Vite.
|
||||||
|
|
||||||
|
## Recommended IDE Setup
|
||||||
|
|
||||||
|
[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur).
|
||||||
|
|
||||||
|
## Customize configuration
|
||||||
|
|
||||||
|
See [Vite Configuration Reference](https://vite.dev/config/).
|
||||||
|
|
||||||
|
## Project Setup
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm install
|
||||||
|
```
|
||||||
|
|
||||||
|
### Compile and Hot-Reload for Development
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
### Compile and Minify for Production
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
### Lint with [ESLint](https://eslint.org/)
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm run lint
|
||||||
|
```
|
||||||
4
fe/assets/main.css
Normal file
4
fe/assets/main.css
Normal file
|
|
@ -0,0 +1,4 @@
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
background-color: white;
|
||||||
|
}
|
||||||
19
fe/eslint.config.js
Normal file
19
fe/eslint.config.js
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
import js from '@eslint/js'
|
||||||
|
import pluginVue from 'eslint-plugin-vue'
|
||||||
|
import skipFormatting from '@vue/eslint-config-prettier/skip-formatting'
|
||||||
|
|
||||||
|
export default [
|
||||||
|
{
|
||||||
|
name: 'app/files-to-lint',
|
||||||
|
files: ['**/*.{js,mjs,jsx,vue}'],
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
name: 'app/files-to-ignore',
|
||||||
|
ignores: ['**/dist/**', '**/dist-ssr/**', '**/coverage/**'],
|
||||||
|
},
|
||||||
|
|
||||||
|
js.configs.recommended,
|
||||||
|
...pluginVue.configs['flat/essential'],
|
||||||
|
skipFormatting,
|
||||||
|
]
|
||||||
18
fe/index.html
Normal file
18
fe/index.html
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="src/assets/Macrame-Logo-gradient.svg" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<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>Vite + Vue</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script type="module" src="/src/main.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
8
fe/jsconfig.json
Normal file
8
fe/jsconfig.json
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"paths": {
|
||||||
|
"@/*": ["./src/*"]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"exclude": ["node_modules", "dist"]
|
||||||
|
}
|
||||||
5454
fe/package-lock.json
generated
Normal file
5454
fe/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
36
fe/package.json
Normal file
36
fe/package.json
Normal file
|
|
@ -0,0 +1,36 @@
|
||||||
|
{
|
||||||
|
"name": "fe",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite --profile",
|
||||||
|
"build": "vite build --emptyOutDir",
|
||||||
|
"preview": "vite preview",
|
||||||
|
"lint": "eslint . --fix",
|
||||||
|
"format": "prettier --write src/"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@tabler/icons-vue": "^3.30.0",
|
||||||
|
"@tailwindcss/vite": "^4.0.9",
|
||||||
|
"vue": "^3.5.13",
|
||||||
|
"vue-router": "^4.5.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@basitcodeenv/vue3-device-detect": "^1.0.3",
|
||||||
|
"@eslint/js": "^9.20.0",
|
||||||
|
"@vitejs/plugin-vue": "^5.2.1",
|
||||||
|
"@vue/eslint-config-prettier": "^10.2.0",
|
||||||
|
"axios": "^1.8.3",
|
||||||
|
"crypto-js": "^4.2.0",
|
||||||
|
"eslint": "^9.20.1",
|
||||||
|
"eslint-plugin-vue": "^9.32.0",
|
||||||
|
"pinia": "^3.0.1",
|
||||||
|
"prettier": "^3.5.1",
|
||||||
|
"sass-embedded": "^1.85.1",
|
||||||
|
"tailwindcss": "^4.0.9",
|
||||||
|
"uuid": "^11.1.0",
|
||||||
|
"vite": "^6.1.0",
|
||||||
|
"vite-plugin-vue-devtools": "^7.7.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
25
fe/public/assets/index-CNkZ911J.js
Normal file
25
fe/public/assets/index-CNkZ911J.js
Normal file
File diff suppressed because one or more lines are too long
1
fe/public/assets/index-zqIqfzzx.css
Normal file
1
fe/public/assets/index-zqIqfzzx.css
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
:root{--vt-c-white: #ffffff;--vt-c-white-soft: #f8f8f8;--vt-c-white-mute: #f2f2f2;--vt-c-black: #181818;--vt-c-black-soft: #222222;--vt-c-black-mute: #282828;--vt-c-indigo: #2c3e50;--vt-c-divider-light-1: rgba(60, 60, 60, .29);--vt-c-divider-light-2: rgba(60, 60, 60, .12);--vt-c-divider-dark-1: rgba(84, 84, 84, .65);--vt-c-divider-dark-2: rgba(84, 84, 84, .48);--vt-c-text-light-1: var(--vt-c-indigo);--vt-c-text-light-2: rgba(60, 60, 60, .66);--vt-c-text-dark-1: var(--vt-c-white);--vt-c-text-dark-2: rgba(235, 235, 235, .64)}:root{--color-background: var(--vt-c-white);--color-background-soft: var(--vt-c-white-soft);--color-background-mute: var(--vt-c-white-mute);--color-border: var(--vt-c-divider-light-2);--color-border-hover: var(--vt-c-divider-light-1);--color-heading: var(--vt-c-text-light-1);--color-text: var(--vt-c-text-light-1);--section-gap: 160px}@media (prefers-color-scheme: dark){:root{--color-background: var(--vt-c-black);--color-background-soft: var(--vt-c-black-soft);--color-background-mute: var(--vt-c-black-mute);--color-border: var(--vt-c-divider-dark-2);--color-border-hover: var(--vt-c-divider-dark-1);--color-heading: var(--vt-c-text-dark-1);--color-text: var(--vt-c-text-dark-2)}}*,*:before,*:after{box-sizing:border-box;margin:0;font-weight:400}body{min-height:100vh;color:var(--color-text);background:var(--color-background);transition:color .5s,background-color .5s;line-height:1.6;font-family:Inter,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu,Cantarell,Fira Sans,Droid Sans,Helvetica Neue,sans-serif;font-size:15px;text-rendering:optimizeLegibility;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale}#app{max-width:1280px;margin:0 auto;padding:2rem;font-weight:400}a,.green{text-decoration:none;color:#00bd7e;transition:.4s;padding:3px}@media (hover: hover){a:hover{background-color:#00bd7e33}}@media (min-width: 1024px){body{display:flex;place-items:center}#app{display:grid;grid-template-columns:1fr 1fr;padding:0 2rem}}
|
||||||
14
fe/public/index.html
Normal file
14
fe/public/index.html
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<title>Vite + Vue</title>
|
||||||
|
<script type="module" crossorigin src="/assets/index-CNkZ911J.js"></script>
|
||||||
|
<link rel="stylesheet" crossorigin href="/assets/index-zqIqfzzx.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
53
fe/src/App.vue
Normal file
53
fe/src/App.vue
Normal file
|
|
@ -0,0 +1,53 @@
|
||||||
|
<template>
|
||||||
|
<div class="app-background">
|
||||||
|
<img src="./assets/img/bg-gradient.svg" aria-hidden="true" />
|
||||||
|
<img src="@/assets/img/Macrame-Logo-white.svg" class="logo" aria-hidden="true" />
|
||||||
|
</div>
|
||||||
|
<MainMenu />
|
||||||
|
<RouterView />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import MainMenu from '@/components/base/MainMenu.vue'
|
||||||
|
import { onMounted } from 'vue'
|
||||||
|
import { RouterView } from 'vue-router'
|
||||||
|
import { useDeviceStore } from './stores/device'
|
||||||
|
|
||||||
|
const device = useDeviceStore()
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
// Setting device uuid from localstorage
|
||||||
|
// If not present in LocalStorage a new uuidV4 will be generated
|
||||||
|
device.uuid()
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
@reference "@/assets/main.css";
|
||||||
|
|
||||||
|
.app-background {
|
||||||
|
@apply fixed
|
||||||
|
inset-0
|
||||||
|
size-full
|
||||||
|
overflow-hidden
|
||||||
|
pointer-events-none
|
||||||
|
opacity-40
|
||||||
|
z-[-1];
|
||||||
|
|
||||||
|
img {
|
||||||
|
@apply absolute
|
||||||
|
size-full
|
||||||
|
object-cover;
|
||||||
|
}
|
||||||
|
|
||||||
|
.logo {
|
||||||
|
@apply absolute
|
||||||
|
top-[10%]
|
||||||
|
left-[10%]
|
||||||
|
scale-[1.8]
|
||||||
|
p-28
|
||||||
|
opacity-35
|
||||||
|
mix-blend-overlay;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
34
fe/src/assets/img/Macrame-Logo-duo.svg
Normal file
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
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
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
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 |
89
fe/src/assets/main.css
Normal file
89
fe/src/assets/main.css
Normal file
|
|
@ -0,0 +1,89 @@
|
||||||
|
@import "./style/_macro.css";
|
||||||
|
@import "./style/_mcrm-block.css";
|
||||||
|
@import "./style/_panel.css";
|
||||||
|
|
||||||
|
@import "tailwindcss";
|
||||||
|
|
||||||
|
@variant dark (&:where(.dark, .dark *));
|
||||||
|
|
||||||
|
@theme {
|
||||||
|
--font-sans: "Roboto", sans-serif;
|
||||||
|
--font-mono: "Fira Code", monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
input {
|
||||||
|
@apply w-full
|
||||||
|
px-2 py-1
|
||||||
|
border
|
||||||
|
border-slate-400
|
||||||
|
text-white
|
||||||
|
rounded-md
|
||||||
|
bg-black/20;
|
||||||
|
}
|
||||||
|
|
||||||
|
:has(> input + span) {
|
||||||
|
@apply flex;
|
||||||
|
|
||||||
|
input {
|
||||||
|
@apply rounded-r-none;
|
||||||
|
}
|
||||||
|
|
||||||
|
span {
|
||||||
|
@apply flex
|
||||||
|
items-center
|
||||||
|
px-2
|
||||||
|
rounded-r-md
|
||||||
|
text-white
|
||||||
|
bg-slate-400;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ul {
|
||||||
|
@apply list-disc
|
||||||
|
list-inside;
|
||||||
|
}
|
||||||
|
|
||||||
|
strong {
|
||||||
|
@apply font-bold;
|
||||||
|
}
|
||||||
28
fe/src/assets/style/_macro.css
Normal file
28
fe/src/assets/style/_macro.css
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
/* @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;
|
||||||
|
}
|
||||||
|
}
|
||||||
107
fe/src/assets/style/_mcrm-block.css
Normal file
107
fe/src/assets/style/_mcrm-block.css
Normal file
|
|
@ -0,0 +1,107 @@
|
||||||
|
.mcrm-block {
|
||||||
|
@apply relative
|
||||||
|
p-6
|
||||||
|
gap-x-6
|
||||||
|
gap-y-2
|
||||||
|
backdrop-blur-lg
|
||||||
|
rounded-2xl
|
||||||
|
overflow-hidden;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
@apply content-['']
|
||||||
|
absolute
|
||||||
|
inset-0
|
||||||
|
p-px
|
||||||
|
rounded-2xl
|
||||||
|
size-full
|
||||||
|
bg-gradient-to-br
|
||||||
|
to-transparent
|
||||||
|
z-[-1];
|
||||||
|
|
||||||
|
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/40;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
@apply from-sky-100/40;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.block__secondary {
|
||||||
|
@apply bg-amber-300/40;
|
||||||
|
|
||||||
|
&::before {
|
||||||
|
@apply from-amber-100/40;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
42
fe/src/assets/style/_panel.css
Normal file
42
fe/src/assets/style/_panel.css
Normal file
|
|
@ -0,0 +1,42 @@
|
||||||
|
.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 bg-gradient-to-r
|
||||||
|
w-fit
|
||||||
|
from-amber-300
|
||||||
|
to-white/50
|
||||||
|
pt-3
|
||||||
|
pl-16 sm:pl-4
|
||||||
|
bg-clip-text
|
||||||
|
text-transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel__content {
|
||||||
|
@apply grid
|
||||||
|
h-full
|
||||||
|
pt-4 sm:pt-0
|
||||||
|
pl-0 sm:pl-4
|
||||||
|
overflow-auto;
|
||||||
|
}
|
||||||
|
}
|
||||||
39
fe/src/components/base/AccordionComp.vue
Normal file
39
fe/src/components/base/AccordionComp.vue
Normal file
|
|
@ -0,0 +1,39 @@
|
||||||
|
<template>
|
||||||
|
<div class="accordion">
|
||||||
|
<header>
|
||||||
|
<slot name="title" />
|
||||||
|
</header>
|
||||||
|
<section :class="`accordion__content ${open ? 'open' : ''}`">
|
||||||
|
<div>
|
||||||
|
<slot name="content" />
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
defineProps({
|
||||||
|
open: Boolean,
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
@reference "@/assets/main.css";
|
||||||
|
|
||||||
|
.accordion {
|
||||||
|
@apply grid;
|
||||||
|
|
||||||
|
.accordion__content {
|
||||||
|
@apply grid
|
||||||
|
grid-rows-[0fr]
|
||||||
|
overflow-hidden
|
||||||
|
duration-300
|
||||||
|
ease-in-out;
|
||||||
|
|
||||||
|
div {
|
||||||
|
@apply grid
|
||||||
|
grid-rows-[0fr];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
55
fe/src/components/base/AlertComp.vue
Normal file
55
fe/src/components/base/AlertComp.vue
Normal file
|
|
@ -0,0 +1,55 @@
|
||||||
|
<template>
|
||||||
|
<div :class="`alert alert__${type}`">
|
||||||
|
<IconInfoCircle v-if="type === 'info'" />
|
||||||
|
<IconCheck v-if="type === 'success'" />
|
||||||
|
<IconExclamationCircle v-if="type === 'warning'" />
|
||||||
|
<IconAlertTriangle v-if="type === 'error'" />
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import {
|
||||||
|
IconAlertTriangle,
|
||||||
|
IconCheck,
|
||||||
|
IconExclamationCircle,
|
||||||
|
IconInfoCircle,
|
||||||
|
} from '@tabler/icons-vue'
|
||||||
|
|
||||||
|
defineProps({
|
||||||
|
type: String, // info, success, warning, error
|
||||||
|
})
|
||||||
|
</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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
157
fe/src/components/base/ButtonComp.vue
Normal file
157
fe/src/components/base/ButtonComp.vue
Normal file
|
|
@ -0,0 +1,157 @@
|
||||||
|
<template>
|
||||||
|
<template v-if="href">
|
||||||
|
<a :href="href" :class="classString">
|
||||||
|
<slot />
|
||||||
|
</a>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<button :class="classString">
|
||||||
|
<slot />
|
||||||
|
</button>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { computed, onMounted } 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;
|
||||||
|
|
||||||
|
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 svg {
|
||||||
|
@apply size-4;
|
||||||
|
}
|
||||||
|
|
||||||
|
&.btn__lg svg {
|
||||||
|
@apply size-6;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
@apply !text-white;
|
||||||
|
|
||||||
|
svg {
|
||||||
|
@apply !stroke-white;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&.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>
|
||||||
15
fe/src/components/base/ButtonGroup.vue
Normal file
15
fe/src/components/base/ButtonGroup.vue
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
<template>
|
||||||
|
<div class="button-group">
|
||||||
|
<slot />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
defineProps({
|
||||||
|
variant: String,
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
@reference "@/assets/main.css";
|
||||||
|
</style>
|
||||||
84
fe/src/components/base/ContextMenu.vue
Normal file
84
fe/src/components/base/ContextMenu.vue
Normal file
|
|
@ -0,0 +1,84 @@
|
||||||
|
<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() {
|
||||||
|
console.log('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>
|
||||||
111
fe/src/components/base/DialogComp.vue
Normal file
111
fe/src/components/base/DialogComp.vue
Normal file
|
|
@ -0,0 +1,111 @@
|
||||||
|
<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>
|
||||||
142
fe/src/components/base/MainMenu.vue
Normal file
142
fe/src/components/base/MainMenu.vue
Normal file
|
|
@ -0,0 +1,142 @@
|
||||||
|
<template>
|
||||||
|
<nav id="main-menu">
|
||||||
|
<button
|
||||||
|
id="menu-toggle"
|
||||||
|
:class="menuOpen ? 'open' : ''"
|
||||||
|
@click="menuOpen = !menuOpen"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
class="logo p-1"
|
||||||
|
: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>
|
||||||
|
<RouterLink @click="menuOpen = false" to="/macros">
|
||||||
|
<IconKeyboard />Macros
|
||||||
|
</RouterLink>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<RouterLink @click="menuOpen = false" to="/devices">
|
||||||
|
<IconDevices />Device
|
||||||
|
</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";
|
||||||
|
|
||||||
|
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
|
||||||
|
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>
|
||||||
196
fe/src/components/devices/RemoteView.vue
Normal file
196
fe/src/components/devices/RemoteView.vue
Normal file
|
|
@ -0,0 +1,196 @@
|
||||||
|
<template>
|
||||||
|
<div class="server-overview">
|
||||||
|
<AlertComp type="info">
|
||||||
|
<div class="grid">
|
||||||
|
<strong>This is a remote device.</strong>
|
||||||
|
<em>UUID: {{ device.uuid() }} </em>
|
||||||
|
</div>
|
||||||
|
</AlertComp>
|
||||||
|
|
||||||
|
<div class="mcrm-block block__light grid gap-4">
|
||||||
|
<h4 class="text-lg flex gap-4 items-center justify-between">
|
||||||
|
<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'" type="success">Authorized</AlertComp>
|
||||||
|
<AlertComp v-if="server.status === 'unlinked'" type="warning">Not linked</AlertComp>
|
||||||
|
<AlertComp v-if="server.status === 'unauthorized'" type="info">
|
||||||
|
<div class="grid gap-2">
|
||||||
|
<strong>Access requested</strong>
|
||||||
|
<p>
|
||||||
|
Navigate to <em class="font-semibold">http://localhost:6970/devices</em> on your pc to
|
||||||
|
authorize.
|
||||||
|
</p>
|
||||||
|
<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
|
||||||
|
v-if="server.status === 'unauthorized'"
|
||||||
|
variant="primary"
|
||||||
|
@click="requestAccess()"
|
||||||
|
>
|
||||||
|
<IconKey />
|
||||||
|
Request access
|
||||||
|
</ButtonComp>
|
||||||
|
<ButtonComp
|
||||||
|
variant="danger"
|
||||||
|
v-if="server.status === 'authorized'"
|
||||||
|
@click="disonnectFromServer()"
|
||||||
|
>
|
||||||
|
<IconPlugConnectedX />
|
||||||
|
Disconnect
|
||||||
|
</ButtonComp>
|
||||||
|
</div>
|
||||||
|
<DialogComp ref="linkPinDialog">
|
||||||
|
<template #content>
|
||||||
|
<div class="grid gap-4 w-64">
|
||||||
|
<h3>Server link pin:</h3>
|
||||||
|
<form class="grid gap-4" @submit.prevent="decryptKey()">
|
||||||
|
<input
|
||||||
|
class="input"
|
||||||
|
id="input-pin"
|
||||||
|
type="text"
|
||||||
|
pattern="[0-9]{4}"
|
||||||
|
v-model="server.inputPin"
|
||||||
|
/>
|
||||||
|
<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, 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 server = reactive({
|
||||||
|
host: '',
|
||||||
|
status: false,
|
||||||
|
link: false,
|
||||||
|
inputPin: '',
|
||||||
|
encryptedKey: '',
|
||||||
|
key: '',
|
||||||
|
})
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
server.host = window.location.host
|
||||||
|
})
|
||||||
|
|
||||||
|
onUpdated(() => {
|
||||||
|
if (!server.status) checkServerStatus()
|
||||||
|
})
|
||||||
|
|
||||||
|
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)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
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>
|
||||||
151
fe/src/components/devices/ServerView.vue
Normal file
151
fe/src/components/devices/ServerView.vue
Normal file
|
|
@ -0,0 +1,151 @@
|
||||||
|
<template>
|
||||||
|
<div class="device-overview">
|
||||||
|
<AlertComp type="info">
|
||||||
|
<div class="grid">
|
||||||
|
<strong>This is a server!</strong>
|
||||||
|
<em>UUID: {{ device.uuid() }} </em>
|
||||||
|
</div>
|
||||||
|
</AlertComp>
|
||||||
|
|
||||||
|
<div class="mcrm-block block__light flex flex-wrap items-start gap-4">
|
||||||
|
<h4 class="w-full flex gap-4 items-center justify-between mb-4">
|
||||||
|
<span class="flex gap-4"> <IconDevices />Remote devices </span>
|
||||||
|
<ButtonComp variant="primary" @click="device.serverGetRemotes()"><IconReload /></ButtonComp>
|
||||||
|
</h4>
|
||||||
|
<!-- {{ Object.keys(remote.devices).length }} -->
|
||||||
|
<template v-if="Object.keys(remote.devices).length > 0">
|
||||||
|
<div
|
||||||
|
class="mcrm-block block__dark block-size__sm w-64 grid !gap-4 content-start"
|
||||||
|
v-for="(remoteDevice, id) in remote.devices"
|
||||||
|
:key="id"
|
||||||
|
>
|
||||||
|
<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 type="success">Authorized</AlertComp>
|
||||||
|
<ButtonComp variant="danger" @click="unlinkDevice(id)">
|
||||||
|
<IconLinkOff />Unlink device
|
||||||
|
</ButtonComp>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<AlertComp type="warning">Unauthorized</AlertComp>
|
||||||
|
<ButtonComp variant="primary" @click="startLink(id)">
|
||||||
|
<IconLink />Link device
|
||||||
|
</ButtonComp>
|
||||||
|
</template>
|
||||||
|
<template v-if="remote.pinlink.uuid == id">
|
||||||
|
<AlertComp type="info">One time pin: {{ remote.pinlink.pin }}</AlertComp>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<div class="grid w-full gap-4">
|
||||||
|
<em class="text-slate-300">No remote devices</em>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<DialogComp ref="pinDialog">
|
||||||
|
<template #content>
|
||||||
|
<div class="grid gap-4">
|
||||||
|
<h3>Pin code</h3>
|
||||||
|
<span class="text-4xl font-mono tracking-wide">{{ remote.pinlink.pin }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</DialogComp>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
// TODO
|
||||||
|
// - startLink -> responsePin also in device block
|
||||||
|
// - startLink -> poll removal of pin file, if removed close dialog, update device list
|
||||||
|
// - Make unlink work
|
||||||
|
|
||||||
|
import { onMounted, 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'
|
||||||
|
|
||||||
|
const device = useDeviceStore()
|
||||||
|
|
||||||
|
const pinDialog = ref()
|
||||||
|
|
||||||
|
const remote = reactive({ devices: [], pinlink: false })
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
device.serverGetRemotes()
|
||||||
|
|
||||||
|
device.$subscribe((mutation, state) => {
|
||||||
|
if (Object.keys(state.remote).length) remote.devices = device.remote
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
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>
|
||||||
7
fe/src/components/icons/IconCommunity.vue
Normal file
7
fe/src/components/icons/IconCommunity.vue
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
<template>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor">
|
||||||
|
<path
|
||||||
|
d="M15 4a1 1 0 1 0 0 2V4zm0 11v-1a1 1 0 0 0-1 1h1zm0 4l-.707.707A1 1 0 0 0 16 19h-1zm-4-4l.707-.707A1 1 0 0 0 11 14v1zm-4.707-1.293a1 1 0 0 0-1.414 1.414l1.414-1.414zm-.707.707l-.707-.707.707.707zM9 11v-1a1 1 0 0 0-.707.293L9 11zm-4 0h1a1 1 0 0 0-1-1v1zm0 4H4a1 1 0 0 0 1.707.707L5 15zm10-9h2V4h-2v2zm2 0a1 1 0 0 1 1 1h2a3 3 0 0 0-3-3v2zm1 1v6h2V7h-2zm0 6a1 1 0 0 1-1 1v2a3 3 0 0 0 3-3h-2zm-1 1h-2v2h2v-2zm-3 1v4h2v-4h-2zm1.707 3.293l-4-4-1.414 1.414 4 4 1.414-1.414zM11 14H7v2h4v-2zm-4 0c-.276 0-.525-.111-.707-.293l-1.414 1.414C5.42 15.663 6.172 16 7 16v-2zm-.707 1.121l3.414-3.414-1.414-1.414-3.414 3.414 1.414 1.414zM9 12h4v-2H9v2zm4 0a3 3 0 0 0 3-3h-2a1 1 0 0 1-1 1v2zm3-3V3h-2v6h2zm0-6a3 3 0 0 0-3-3v2a1 1 0 0 1 1 1h2zm-3-3H3v2h10V0zM3 0a3 3 0 0 0-3 3h2a1 1 0 0 1 1-1V0zM0 3v6h2V3H0zm0 6a3 3 0 0 0 3 3v-2a1 1 0 0 1-1-1H0zm3 3h2v-2H3v2zm1-1v4h2v-4H4zm1.707 4.707l.586-.586-1.414-1.414-.586.586 1.414 1.414z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
7
fe/src/components/icons/IconDocumentation.vue
Normal file
7
fe/src/components/icons/IconDocumentation.vue
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
<template>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="17" fill="currentColor">
|
||||||
|
<path
|
||||||
|
d="M11 2.253a1 1 0 1 0-2 0h2zm-2 13a1 1 0 1 0 2 0H9zm.447-12.167a1 1 0 1 0 1.107-1.666L9.447 3.086zM1 2.253L.447 1.42A1 1 0 0 0 0 2.253h1zm0 13H0a1 1 0 0 0 1.553.833L1 15.253zm8.447.833a1 1 0 1 0 1.107-1.666l-1.107 1.666zm0-14.666a1 1 0 1 0 1.107 1.666L9.447 1.42zM19 2.253h1a1 1 0 0 0-.447-.833L19 2.253zm0 13l-.553.833A1 1 0 0 0 20 15.253h-1zm-9.553-.833a1 1 0 1 0 1.107 1.666L9.447 14.42zM9 2.253v13h2v-13H9zm1.553-.833C9.203.523 7.42 0 5.5 0v2c1.572 0 2.961.431 3.947 1.086l1.107-1.666zM5.5 0C3.58 0 1.797.523.447 1.42l1.107 1.666C2.539 2.431 3.928 2 5.5 2V0zM0 2.253v13h2v-13H0zm1.553 13.833C2.539 15.431 3.928 15 5.5 15v-2c-1.92 0-3.703.523-5.053 1.42l1.107 1.666zM5.5 15c1.572 0 2.961.431 3.947 1.086l1.107-1.666C9.203 13.523 7.42 13 5.5 13v2zm5.053-11.914C11.539 2.431 12.928 2 14.5 2V0c-1.92 0-3.703.523-5.053 1.42l1.107 1.666zM14.5 2c1.573 0 2.961.431 3.947 1.086l1.107-1.666C18.203.523 16.421 0 14.5 0v2zm3.5.253v13h2v-13h-2zm1.553 12.167C18.203 13.523 16.421 13 14.5 13v2c1.573 0 2.961.431 3.947 1.086l1.107-1.666zM14.5 13c-1.92 0-3.703.523-5.053 1.42l1.107 1.666C11.539 15.431 12.928 15 14.5 15v-2z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
7
fe/src/components/icons/IconEcosystem.vue
Normal file
7
fe/src/components/icons/IconEcosystem.vue
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
<template>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="20" fill="currentColor">
|
||||||
|
<path
|
||||||
|
d="M11.447 8.894a1 1 0 1 0-.894-1.789l.894 1.789zm-2.894-.789a1 1 0 1 0 .894 1.789l-.894-1.789zm0 1.789a1 1 0 1 0 .894-1.789l-.894 1.789zM7.447 7.106a1 1 0 1 0-.894 1.789l.894-1.789zM10 9a1 1 0 1 0-2 0h2zm-2 2.5a1 1 0 1 0 2 0H8zm9.447-5.606a1 1 0 1 0-.894-1.789l.894 1.789zm-2.894-.789a1 1 0 1 0 .894 1.789l-.894-1.789zm2 .789a1 1 0 1 0 .894-1.789l-.894 1.789zm-1.106-2.789a1 1 0 1 0-.894 1.789l.894-1.789zM18 5a1 1 0 1 0-2 0h2zm-2 2.5a1 1 0 1 0 2 0h-2zm-5.447-4.606a1 1 0 1 0 .894-1.789l-.894 1.789zM9 1l.447-.894a1 1 0 0 0-.894 0L9 1zm-2.447.106a1 1 0 1 0 .894 1.789l-.894-1.789zm-6 3a1 1 0 1 0 .894 1.789L.553 4.106zm2.894.789a1 1 0 1 0-.894-1.789l.894 1.789zm-2-.789a1 1 0 1 0-.894 1.789l.894-1.789zm1.106 2.789a1 1 0 1 0 .894-1.789l-.894 1.789zM2 5a1 1 0 1 0-2 0h2zM0 7.5a1 1 0 1 0 2 0H0zm8.553 12.394a1 1 0 1 0 .894-1.789l-.894 1.789zm-1.106-2.789a1 1 0 1 0-.894 1.789l.894-1.789zm1.106 1a1 1 0 1 0 .894 1.789l-.894-1.789zm2.894.789a1 1 0 1 0-.894-1.789l.894 1.789zM8 19a1 1 0 1 0 2 0H8zm2-2.5a1 1 0 1 0-2 0h2zm-7.447.394a1 1 0 1 0 .894-1.789l-.894 1.789zM1 15H0a1 1 0 0 0 .553.894L1 15zm1-2.5a1 1 0 1 0-2 0h2zm12.553 2.606a1 1 0 1 0 .894 1.789l-.894-1.789zM17 15l.447.894A1 1 0 0 0 18 15h-1zm1-2.5a1 1 0 1 0-2 0h2zm-7.447-5.394l-2 1 .894 1.789 2-1-.894-1.789zm-1.106 1l-2-1-.894 1.789 2 1 .894-1.789zM8 9v2.5h2V9H8zm8.553-4.894l-2 1 .894 1.789 2-1-.894-1.789zm.894 0l-2-1-.894 1.789 2 1 .894-1.789zM16 5v2.5h2V5h-2zm-4.553-3.894l-2-1-.894 1.789 2 1 .894-1.789zm-2.894-1l-2 1 .894 1.789 2-1L8.553.106zM1.447 5.894l2-1-.894-1.789-2 1 .894 1.789zm-.894 0l2 1 .894-1.789-2-1-.894 1.789zM0 5v2.5h2V5H0zm9.447 13.106l-2-1-.894 1.789 2 1 .894-1.789zm0 1.789l2-1-.894-1.789-2 1 .894 1.789zM10 19v-2.5H8V19h2zm-6.553-3.894l-2-1-.894 1.789 2 1 .894-1.789zM2 15v-2.5H0V15h2zm13.447 1.894l2-1-.894-1.789-2 1 .894 1.789zM18 15v-2.5h-2V15h2z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
7
fe/src/components/icons/IconSupport.vue
Normal file
7
fe/src/components/icons/IconSupport.vue
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
<template>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor">
|
||||||
|
<path
|
||||||
|
d="M10 3.22l-.61-.6a5.5 5.5 0 0 0-7.666.105 5.5 5.5 0 0 0-.114 7.665L10 18.78l8.39-8.4a5.5 5.5 0 0 0-.114-7.665 5.5 5.5 0 0 0-7.666-.105l-.61.61z"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
19
fe/src/components/icons/IconTooling.vue
Normal file
19
fe/src/components/icons/IconTooling.vue
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
<!-- This icon is from <https://github.com/Templarian/MaterialDesign>, distributed under Apache 2.0 (https://www.apache.org/licenses/LICENSE-2.0) license-->
|
||||||
|
<template>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||||
|
aria-hidden="true"
|
||||||
|
role="img"
|
||||||
|
class="iconify iconify--mdi"
|
||||||
|
width="24"
|
||||||
|
height="24"
|
||||||
|
preserveAspectRatio="xMidYMid meet"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
d="M20 18v-4h-3v1h-2v-1H9v1H7v-1H4v4h16M6.33 8l-1.74 4H7v-1h2v1h6v-1h2v1h2.41l-1.74-4H6.33M9 5v1h6V5H9m12.84 7.61c.1.22.16.48.16.8V18c0 .53-.21 1-.6 1.41c-.4.4-.85.59-1.4.59H4c-.55 0-1-.19-1.4-.59C2.21 19 2 18.53 2 18v-4.59c0-.32.06-.58.16-.8L4.5 7.22C4.84 6.41 5.45 6 6.33 6H7V5c0-.55.18-1 .57-1.41C7.96 3.2 8.44 3 9 3h6c.56 0 1.04.2 1.43.59c.39.41.57.86.57 1.41v1h.67c.88 0 1.49.41 1.83 1.22l2.34 5.39z"
|
||||||
|
fill="currentColor"
|
||||||
|
></path>
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
73
fe/src/components/macros/MacroOverview.vue
Normal file
73
fe/src/components/macros/MacroOverview.vue
Normal file
|
|
@ -0,0 +1,73 @@
|
||||||
|
<template>
|
||||||
|
<div class="macro-overview mcrm-block block__dark">
|
||||||
|
<h4 class="border-b-2 border-transparent">Saved Macros</h4>
|
||||||
|
<div class="macro-overview__list">
|
||||||
|
<div class="macro-item" v-for="(macro, i) in macros.list" :key="i">
|
||||||
|
<ButtonComp variant="dark" class="w-full" size="sm" @click.prevent="runMacro(macro)">
|
||||||
|
<IconKeyboard /> {{ macro }}
|
||||||
|
</ButtonComp>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import { IconKeyboard } from '@tabler/icons-vue'
|
||||||
|
import ButtonComp from '../base/ButtonComp.vue'
|
||||||
|
import { onMounted, reactive } from 'vue'
|
||||||
|
import axios from 'axios'
|
||||||
|
import { appUrl, isLocal } from '@/services/ApiService'
|
||||||
|
import { AuthCall } from '@/services/EncryptService'
|
||||||
|
|
||||||
|
const macros = reactive({
|
||||||
|
list: [],
|
||||||
|
})
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
axios.post(appUrl() + '/macro/list').then((data) => {
|
||||||
|
if (data.data.length > 0) macros.list = data.data
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
function runMacro(macro) {
|
||||||
|
const data = isLocal() ? { macro: macro } : AuthCall({ macro: macro })
|
||||||
|
|
||||||
|
axios.post(appUrl() + '/macro/play', data).then((data) => {
|
||||||
|
console.log(data)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
@reference "@/assets/main.css";
|
||||||
|
|
||||||
|
.macro-overview {
|
||||||
|
@apply relative
|
||||||
|
grid
|
||||||
|
grid-rows-[auto_1fr];
|
||||||
|
|
||||||
|
&::after {
|
||||||
|
@apply content-['']
|
||||||
|
absolute
|
||||||
|
top-0
|
||||||
|
left-full
|
||||||
|
h-full
|
||||||
|
w-px
|
||||||
|
bg-slate-600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.macro-overview__list {
|
||||||
|
@apply grid
|
||||||
|
gap-1
|
||||||
|
content-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.macro-item {
|
||||||
|
@apply flex items-center;
|
||||||
|
|
||||||
|
button {
|
||||||
|
@apply w-full;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
85
fe/src/components/macros/MacroRecorder.vue
Normal file
85
fe/src/components/macros/MacroRecorder.vue
Normal file
|
|
@ -0,0 +1,85 @@
|
||||||
|
<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>
|
||||||
57
fe/src/components/macros/components/DelaySpan.vue
Normal file
57
fe/src/components/macros/components/DelaySpan.vue
Normal file
|
|
@ -0,0 +1,57 @@
|
||||||
|
<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>
|
||||||
38
fe/src/components/macros/components/DeleteKeyDialog.vue
Normal file
38
fe/src/components/macros/components/DeleteKeyDialog.vue
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
<template>
|
||||||
|
<div id="delete-key-dialog" class="dialog__content">
|
||||||
|
<h4 class="text-slate-50 mb-4">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())
|
||||||
|
// console.log(macroRecorder.getEditKey());
|
||||||
|
// console.log(keyObj.value);
|
||||||
|
// console.log('---------');
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
@reference "@/assets/main.css";
|
||||||
|
</style>
|
||||||
|
'
|
||||||
57
fe/src/components/macros/components/EditDelayDialog.vue
Normal file
57
fe/src/components/macros/components/EditDelayDialog.vue
Normal file
|
|
@ -0,0 +1,57 @@
|
||||||
|
<template>
|
||||||
|
<div id="edit-delay-dialog" class="dialog__content">
|
||||||
|
<h4 class="text-slate-50 mb-4">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
|
||||||
|
console.log(editable)
|
||||||
|
})
|
||||||
|
|
||||||
|
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>
|
||||||
103
fe/src/components/macros/components/EditKeyDialog.vue
Normal file
103
fe/src/components/macros/components/EditKeyDialog.vue
Normal file
|
|
@ -0,0 +1,103 @@
|
||||||
|
<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>
|
||||||
69
fe/src/components/macros/components/FixedDelayMenu.vue
Normal file
69
fe/src/components/macros/components/FixedDelayMenu.vue
Normal file
|
|
@ -0,0 +1,69 @@
|
||||||
|
<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>
|
||||||
134
fe/src/components/macros/components/InsertKeyDialog.vue
Normal file
134
fe/src/components/macros/components/InsertKeyDialog.vue
Normal file
|
|
@ -0,0 +1,134 @@
|
||||||
|
<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>
|
||||||
109
fe/src/components/macros/components/MacroKey.vue
Normal file
109
fe/src/components/macros/components/MacroKey.vue
Normal file
|
|
@ -0,0 +1,109 @@
|
||||||
|
<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,60 @@
|
||||||
|
<template>
|
||||||
|
<div id="validation-error__dialog" class="dialog__content">
|
||||||
|
<h4 class="text-slate-50 mb-4">There's an error in your macro</h4>
|
||||||
|
|
||||||
|
<div class="grid gap-4" v-if="(errors && errors.up.length > 0) || errors.down.length > 0">
|
||||||
|
<div v-if="errors.down.length > 0">
|
||||||
|
<p>
|
||||||
|
The following keys have been <strong>pressed</strong> down, but
|
||||||
|
<strong>not released</strong>.
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
<li v-for="key in errors.down" :key="key">{{ key.toUpperCase() }}</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div v-if="errors.up.length > 0">
|
||||||
|
<p>
|
||||||
|
The following keys have been <strong>released</strong>, but
|
||||||
|
<strong>not pressed</strong> down.
|
||||||
|
</p>
|
||||||
|
<ul>
|
||||||
|
<li v-for="key in errors.up" :key="key">{{ key.toUpperCase() }}</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="flex justify-end mt-4">
|
||||||
|
<ButtonComp size="sm" variant="danger" @click="macroRecorder.state.validationErrors = false">
|
||||||
|
Close
|
||||||
|
</ButtonComp>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import ButtonComp from '@/components/base/ButtonComp.vue'
|
||||||
|
import { useMacroRecorderStore } from '@/stores/macrorecorder'
|
||||||
|
import { onMounted, reactive } from 'vue'
|
||||||
|
|
||||||
|
const macroRecorder = useMacroRecorderStore()
|
||||||
|
|
||||||
|
const errors = reactive({
|
||||||
|
up: [],
|
||||||
|
down: [],
|
||||||
|
})
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
macroRecorder.$subscribe((mutation) => {
|
||||||
|
if (mutation.events && mutation.events.key == 'validationErrors') {
|
||||||
|
errors.up = mutation.events.newValue !== false ? macroRecorder.state.validationErrors.up : []
|
||||||
|
errors.down =
|
||||||
|
mutation.events.newValue !== false ? macroRecorder.state.validationErrors.down : []
|
||||||
|
}
|
||||||
|
console.log(mutation)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
@reference "@/assets/main.css";
|
||||||
|
</style>
|
||||||
121
fe/src/components/macros/parts/EditDialogs.vue
Normal file
121
fe/src/components/macros/parts/EditDialogs.vue
Normal file
|
|
@ -0,0 +1,121 @@
|
||||||
|
<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>
|
||||||
65
fe/src/components/macros/parts/RecorderFooter.vue
Normal file
65
fe/src/components/macros/parts/RecorderFooter.vue
Normal file
|
|
@ -0,0 +1,65 @@
|
||||||
|
<template>
|
||||||
|
<div class="macro-recorder__footer">
|
||||||
|
<ButtonComp
|
||||||
|
v-if="macroRecorder.steps.length > 0"
|
||||||
|
variant="danger"
|
||||||
|
size="sm"
|
||||||
|
@click="macroRecorder.reset()"
|
||||||
|
>
|
||||||
|
<IconRestore /> Reset
|
||||||
|
</ButtonComp>
|
||||||
|
|
||||||
|
<DialogComp ref="errorDialog">
|
||||||
|
<template #content>
|
||||||
|
<ValidationErrorDialog />
|
||||||
|
</template>
|
||||||
|
</DialogComp>
|
||||||
|
|
||||||
|
<ButtonComp
|
||||||
|
v-if="macroRecorder.steps.length > 0"
|
||||||
|
:disabled="macroRecorder.state.record || macroRecorder.state.edit"
|
||||||
|
variant="success"
|
||||||
|
size="sm"
|
||||||
|
@click="toggleSave()"
|
||||||
|
>
|
||||||
|
<IconDeviceFloppy />
|
||||||
|
Save
|
||||||
|
</ButtonComp>
|
||||||
|
</div>
|
||||||
|
</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()
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
macroRecorder.$subscribe((mutation) => {
|
||||||
|
if (mutation.events && mutation.events.key == 'validationErrors') {
|
||||||
|
errorDialog.value.toggleDialog(mutation.events.newValue !== false)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const toggleSave = () => {
|
||||||
|
if (!macroRecorder.save()) errorDialog.value.toggleDialog(true)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
@reference "@/assets/main.css";
|
||||||
|
|
||||||
|
.macro-recorder__footer {
|
||||||
|
@apply flex
|
||||||
|
justify-between
|
||||||
|
gap-2;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
100
fe/src/components/macros/parts/RecorderHeader.vue
Normal file
100
fe/src/components/macros/parts/RecorderHeader.vue
Normal file
|
|
@ -0,0 +1,100 @@
|
||||||
|
<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)"
|
||||||
|
placeholder="New macro"
|
||||||
|
/>
|
||||||
|
<div :class="`recording__buttons ${!nameSet || macroRecorder.state.edit ? 'disabled' : ''}`">
|
||||||
|
{{ macroRecorder.name }}
|
||||||
|
<ButtonComp
|
||||||
|
v-if="!macroRecorder.state.record"
|
||||||
|
variant="primary"
|
||||||
|
size="sm"
|
||||||
|
@click="macroRecorder.state.record = true"
|
||||||
|
>
|
||||||
|
<IconPlayerRecordFilled class="text-red-500" />Record
|
||||||
|
</ButtonComp>
|
||||||
|
<ButtonComp
|
||||||
|
v-if="macroRecorder.state.record"
|
||||||
|
variant="danger"
|
||||||
|
size="sm"
|
||||||
|
@click="macroRecorder.state.record = false"
|
||||||
|
>
|
||||||
|
<IconPlayerStopFilled class="text-white" />Stop
|
||||||
|
</ButtonComp>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="macroRecorder.steps.length > 0"
|
||||||
|
:class="`edit__buttons ${macroRecorder.state.record ? 'disabled' : ''}`"
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<ButtonComp
|
||||||
|
v-if="!macroRecorder.state.edit"
|
||||||
|
variant="secondary"
|
||||||
|
size="sm"
|
||||||
|
@click="macroRecorder.state.edit = true"
|
||||||
|
>
|
||||||
|
<IconPencil />Edit
|
||||||
|
</ButtonComp>
|
||||||
|
<ButtonComp
|
||||||
|
v-if="macroRecorder.state.edit"
|
||||||
|
variant="danger"
|
||||||
|
size="sm"
|
||||||
|
@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, onMounted, onUpdated, ref } from 'vue'
|
||||||
|
|
||||||
|
const macroRecorder = useMacroRecorderStore()
|
||||||
|
|
||||||
|
const nameSet = ref(false)
|
||||||
|
|
||||||
|
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>
|
||||||
46
fe/src/components/macros/parts/RecorderInput.vue
Normal file
46
fe/src/components/macros/parts/RecorderInput.vue
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
<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"
|
||||||
|
@focus="console.log('focus')"
|
||||||
|
@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>
|
||||||
58
fe/src/components/macros/parts/RecorderOutput.vue
Normal file
58
fe/src/components/macros/parts/RecorderOutput.vue
Normal file
|
|
@ -0,0 +1,58 @@
|
||||||
|
<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>
|
||||||
15
fe/src/main.js
Normal file
15
fe/src/main.js
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
// import './assets/jemx.scss'
|
||||||
|
import "@/assets/main.css";
|
||||||
|
|
||||||
|
import { createApp } from "vue";
|
||||||
|
import { createPinia } from "pinia";
|
||||||
|
|
||||||
|
import App from "@/App.vue";
|
||||||
|
import router from "@/router";
|
||||||
|
|
||||||
|
const app = createApp(App);
|
||||||
|
|
||||||
|
app.use(createPinia());
|
||||||
|
app.use(router);
|
||||||
|
|
||||||
|
app.mount("#app");
|
||||||
43
fe/src/router/index.js
Normal file
43
fe/src/router/index.js
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
import { createRouter, createWebHistory } from 'vue-router'
|
||||||
|
import HomeView from '../views/HomeView.vue'
|
||||||
|
|
||||||
|
const router = createRouter({
|
||||||
|
history: createWebHistory(import.meta.env.BASE_URL),
|
||||||
|
routes: [
|
||||||
|
{
|
||||||
|
path: '/',
|
||||||
|
name: 'home',
|
||||||
|
component: HomeView,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/panels',
|
||||||
|
name: 'panels',
|
||||||
|
component: () => import('../views/PanelsView.vue'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/macros',
|
||||||
|
name: 'macros',
|
||||||
|
component: () => import('../views/MacrosView.vue'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/devices',
|
||||||
|
name: 'devices',
|
||||||
|
component: () => import('../views/DevicesView.vue'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/settings',
|
||||||
|
name: 'settings',
|
||||||
|
component: () => import('../views/SettingsView.vue'),
|
||||||
|
},
|
||||||
|
// {
|
||||||
|
// path: '/about',
|
||||||
|
// name: 'about',
|
||||||
|
// // route level code-splitting
|
||||||
|
// // this generates a separate chunk (About.[hash].js) for this route
|
||||||
|
// // which is lazy-loaded when the route is visited.
|
||||||
|
// component: () => import('../views/AboutView.vue'),
|
||||||
|
// },
|
||||||
|
],
|
||||||
|
})
|
||||||
|
|
||||||
|
export default router
|
||||||
19
fe/src/services/ApiService.js
Normal file
19
fe/src/services/ApiService.js
Normal file
|
|
@ -0,0 +1,19 @@
|
||||||
|
import CryptoJS from 'crypto-js'
|
||||||
|
|
||||||
|
export const appUrl = () => {
|
||||||
|
return window.location.port !== 6970 ? `http://${window.location.hostname}:6970` : ''
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
51
fe/src/services/EncryptService.js
Normal file
51
fe/src/services/EncryptService.js
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
import { useDeviceStore } from '@/stores/device'
|
||||||
|
import { AES, enc, pad } from 'crypto-js'
|
||||||
|
|
||||||
|
export const encryptAES = (key, str) => {
|
||||||
|
key = keyPad(key)
|
||||||
|
|
||||||
|
let iv = enc.Utf8.parse(import.meta.env.VITE_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(import.meta.env.VITE_MCRM__IV)
|
||||||
|
let encrypted = AES.decrypt(str.toString(), key, {
|
||||||
|
iv: iv,
|
||||||
|
padding: pad.Pkcs7,
|
||||||
|
})
|
||||||
|
return encrypted.toString(enc.Utf8)
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AuthCall = (data) => {
|
||||||
|
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 + import.meta.env.VITE_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}`
|
||||||
|
}
|
||||||
127
fe/src/services/MacroRecordService.js
Normal file
127
fe/src/services/MacroRecordService.js
Normal file
|
|
@ -0,0 +1,127 @@
|
||||||
|
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 }
|
||||||
|
}
|
||||||
117
fe/src/services/RobotKeys.md
Normal file
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
|
||||||
12
fe/src/stores/counter.js
Normal file
12
fe/src/stores/counter.js
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
import { defineStore } from 'pinia'
|
||||||
|
|
||||||
|
export const useCounterStore = defineStore('counter', () => {
|
||||||
|
const count = ref(0)
|
||||||
|
const doubleCount = computed(() => count.value * 2)
|
||||||
|
function increment() {
|
||||||
|
count.value++
|
||||||
|
}
|
||||||
|
|
||||||
|
return { count, doubleCount, increment }
|
||||||
|
})
|
||||||
118
fe/src/stores/device.js
Normal file
118
fe/src/stores/device.js
Normal file
|
|
@ -0,0 +1,118 @@
|
||||||
|
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')
|
||||||
|
}
|
||||||
|
|
||||||
|
// Server application
|
||||||
|
const serverGetRemotes = async (remoteUuid) => {
|
||||||
|
axios.post(appUrl() + '/device/list', { uuid: remoteUuid }).then((data) => {
|
||||||
|
if (data.data.devices) remote.value = data.data.devices
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
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 linkRequest = await axios.post(appUrl() + '/device/link/ping', { uuid: deviceUuid })
|
||||||
|
// if (linkRequest.data)
|
||||||
|
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 (key) => {
|
||||||
|
const handshake = await axios.post(appUrl() + '/device/handshake', {
|
||||||
|
uuid: uuid(),
|
||||||
|
shake: encryptAES(key, getDateStr()),
|
||||||
|
})
|
||||||
|
console.log(handshake)
|
||||||
|
|
||||||
|
return handshake.data
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
remote,
|
||||||
|
server,
|
||||||
|
uuid,
|
||||||
|
setDeviceId,
|
||||||
|
key,
|
||||||
|
setDeviceKey,
|
||||||
|
removeDeviceKey,
|
||||||
|
serverGetRemotes,
|
||||||
|
serverStartLink,
|
||||||
|
remoteCheckServerAccess,
|
||||||
|
remoteRequestServerAccess,
|
||||||
|
remotePingLink,
|
||||||
|
remoteHandshake,
|
||||||
|
}
|
||||||
|
})
|
||||||
206
fe/src/stores/macrorecorder.js
Normal file
206
fe/src/stores/macrorecorder.js
Normal file
|
|
@ -0,0 +1,206 @@
|
||||||
|
import { ref } from 'vue'
|
||||||
|
import { defineStore } from 'pinia'
|
||||||
|
|
||||||
|
import { filterKey, isRepeat, invalidMacro } 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) => {
|
||||||
|
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
|
||||||
|
console.log(macroName.value)
|
||||||
|
}
|
||||||
|
|
||||||
|
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 reset = () => {
|
||||||
|
state.value.record = false
|
||||||
|
delay.value.start = 0
|
||||||
|
steps.value = []
|
||||||
|
|
||||||
|
if (state.value.edit) resetEdit()
|
||||||
|
}
|
||||||
|
|
||||||
|
const save = () => {
|
||||||
|
state.value.validationErrors = invalidMacro(steps.value)
|
||||||
|
|
||||||
|
if (state.value.validationErrors) return false
|
||||||
|
|
||||||
|
axios
|
||||||
|
.post(appUrl() + '/macro/record', { name: macroName.value, steps: steps.value })
|
||||||
|
.then((data) => {
|
||||||
|
console.log(data)
|
||||||
|
})
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
state,
|
||||||
|
steps,
|
||||||
|
delay,
|
||||||
|
getEditKey,
|
||||||
|
getAdjacentKey,
|
||||||
|
getEditDelay,
|
||||||
|
recordStep,
|
||||||
|
insertKey,
|
||||||
|
deleteEditKey,
|
||||||
|
restartDelay,
|
||||||
|
changeName,
|
||||||
|
changeDelay,
|
||||||
|
toggleEdit,
|
||||||
|
resetEdit,
|
||||||
|
reset,
|
||||||
|
save,
|
||||||
|
}
|
||||||
|
})
|
||||||
21
fe/src/views/DevicesView.vue
Normal file
21
fe/src/views/DevicesView.vue
Normal file
|
|
@ -0,0 +1,21 @@
|
||||||
|
<template>
|
||||||
|
<div id="devices-view" class="panel">
|
||||||
|
<h1 class="panel__title">
|
||||||
|
Devices <span class="text-sm">{{ isLocal() ? 'remote' : 'servers' }}</span>
|
||||||
|
</h1>
|
||||||
|
<div class="panel__content grid gap-8">
|
||||||
|
<ServerView v-if="isLocal()" />
|
||||||
|
<RemoteView v-else />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup>
|
||||||
|
import ServerView from '@/components/devices/ServerView.vue'
|
||||||
|
import RemoteView from '@/components/devices/RemoteView.vue'
|
||||||
|
import { isLocal } from '@/services/ApiService'
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
@reference "@/assets/main.css";
|
||||||
|
</style>
|
||||||
7
fe/src/views/HomeView.vue
Normal file
7
fe/src/views/HomeView.vue
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
<template>
|
||||||
|
<div id="dashboard"></div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup></script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped></style>
|
||||||
40
fe/src/views/MacrosView.vue
Normal file
40
fe/src/views/MacrosView.vue
Normal file
|
|
@ -0,0 +1,40 @@
|
||||||
|
<template>
|
||||||
|
<div id="macros" class="panel">
|
||||||
|
<h1 class="panel__title">Macros</h1>
|
||||||
|
<div class="panel__content !p-0">
|
||||||
|
<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'
|
||||||
|
import { onMounted, ref } from 'vue'
|
||||||
|
|
||||||
|
const recordMacro = ref(false)
|
||||||
|
|
||||||
|
const macroInput = ref(null)
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
// macroInput.value.focus()
|
||||||
|
})
|
||||||
|
|
||||||
|
const keyDown = (e) => {
|
||||||
|
console.log(e)
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
@reference "@/assets/main.css";
|
||||||
|
|
||||||
|
.macro-panel__content {
|
||||||
|
@apply grid
|
||||||
|
grid-cols-[25ch_1fr]
|
||||||
|
gap-6
|
||||||
|
pt-2;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
7
fe/src/views/PanelsView.vue
Normal file
7
fe/src/views/PanelsView.vue
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
<template>
|
||||||
|
<div></div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup></script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped></style>
|
||||||
7
fe/src/views/SettingsView.vue
Normal file
7
fe/src/views/SettingsView.vue
Normal file
|
|
@ -0,0 +1,7 @@
|
||||||
|
<template>
|
||||||
|
<div></div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup></script>
|
||||||
|
|
||||||
|
<style lang="scss" scoped></style>
|
||||||
31
fe/vite.config.js
Normal file
31
fe/vite.config.js
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
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({
|
||||||
|
server: {
|
||||||
|
host: 'localhost',
|
||||||
|
port: 5173,
|
||||||
|
watch: {
|
||||||
|
usePolling: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
plugins: [vue(), vueDevTools(), tailwindcss()],
|
||||||
|
envDir: '../',
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
'@': fileURLToPath(new URL('./src', import.meta.url)),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
base: '/',
|
||||||
|
// publicDir: "../public",
|
||||||
|
build: {
|
||||||
|
outDir: '../public',
|
||||||
|
sourcemap: false,
|
||||||
|
minify: false,
|
||||||
|
},
|
||||||
|
})
|
||||||
1
macros/desktop.json
Normal file
1
macros/desktop.json
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
[{"type":"key","key":"Meta","code":"MetaLeft","location":1,"direction":"down","value":0},{"type":"delay","key":"","code":"","location":0,"direction":"","value":240},{"type":"key","key":"d","code":"KeyD","location":0,"direction":"down","value":0},{"type":"delay","key":"","code":"","location":0,"direction":"","value":10},{"type":"key","key":"d","code":"KeyD","location":0,"direction":"up","value":0},{"type":"delay","key":"","code":"","location":0,"direction":"","value":10},{"type":"key","key":"Meta","code":"MetaLeft","location":1,"direction":"up","value":0}]
|
||||||
1
macros/task_manager.json
Normal file
1
macros/task_manager.json
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
[{"type":"key","key":"Control","code":"ControlLeft","location":1,"direction":"down","value":0},{"type":"delay","key":"","code":"","location":0,"direction":"","value":15},{"type":"key","key":"Shift","code":"ShiftLeft","location":1,"direction":"down","value":0},{"type":"delay","key":"","code":"","location":0,"direction":"","value":15},{"type":"key","key":"Escape","code":"Escape","location":0,"direction":"down","value":0},{"type":"delay","key":"","code":"","location":0,"direction":"","value":15},{"type":"key","key":"Escape","code":"Escape","location":0,"direction":"up","value":0},{"type":"delay","key":"","code":"","location":0,"direction":"","value":15},{"type":"key","key":"Shift","code":"ShiftLeft","location":1,"direction":"up","value":0},{"type":"delay","key":"","code":"","location":0,"direction":"","value":15},{"type":"key","key":"Control","code":"ControlLeft","location":1,"direction":"up","value":0}]
|
||||||
1758
public/assets/DevicesView-DqasecOn.js
Normal file
1758
public/assets/DevicesView-DqasecOn.js
Normal file
File diff suppressed because it is too large
Load diff
87
public/assets/DevicesView-Dw_Mls3X.css
Normal file
87
public/assets/DevicesView-Dw_Mls3X.css
Normal file
|
|
@ -0,0 +1,87 @@
|
||||||
|
/*! tailwindcss v4.0.9 | MIT License | https://tailwindcss.com */
|
||||||
|
.alert[data-v-87f6de25] {
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: calc(var(--spacing, .25rem) * 4);
|
||||||
|
border-radius: var(--radius-md, .375rem);
|
||||||
|
border-style: var(--tw-border-style);
|
||||||
|
border-width: 1px;
|
||||||
|
border-color: color-mix(in oklab, var(--color-white, #fff) 10%, transparent);
|
||||||
|
background-color: color-mix(in oklab, var(--color-white, #fff) 10%, transparent);
|
||||||
|
padding: calc(var(--spacing, .25rem) * 4);
|
||||||
|
--tw-backdrop-blur: blur(var(--blur-md, 12px));
|
||||||
|
-webkit-backdrop-filter: var(--tw-backdrop-blur, ) var(--tw-backdrop-brightness, ) var(--tw-backdrop-contrast, ) var(--tw-backdrop-grayscale, ) var(--tw-backdrop-hue-rotate, ) var(--tw-backdrop-invert, ) var(--tw-backdrop-opacity, ) var(--tw-backdrop-saturate, ) var(--tw-backdrop-sepia, );
|
||||||
|
backdrop-filter: var(--tw-backdrop-blur, ) var(--tw-backdrop-brightness, ) var(--tw-backdrop-contrast, ) var(--tw-backdrop-grayscale, ) var(--tw-backdrop-hue-rotate, ) var(--tw-backdrop-invert, ) var(--tw-backdrop-opacity, ) var(--tw-backdrop-saturate, ) var(--tw-backdrop-sepia, );
|
||||||
|
grid-template-columns: 1rem 1fr;
|
||||||
|
display: grid;
|
||||||
|
}
|
||||||
|
.alert.alert__info[data-v-87f6de25] {
|
||||||
|
background-color: color-mix(in oklab, var(--color-sky-400, oklch(.746 .16 232.661)) 40%, transparent);
|
||||||
|
color: var(--color-sky-100, oklch(.951 .026 236.824));
|
||||||
|
}
|
||||||
|
.alert.alert__success[data-v-87f6de25] {
|
||||||
|
background-color: color-mix(in oklab, var(--color-lime-400, oklch(.841 .238 128.85)) 10%, transparent);
|
||||||
|
color: var(--color-lime-400, oklch(.841 .238 128.85));
|
||||||
|
}
|
||||||
|
.alert.alert__warning[data-v-87f6de25] {
|
||||||
|
background-color: color-mix(in oklab, var(--color-amber-400, oklch(.828 .189 84.429)) 10%, transparent);
|
||||||
|
color: var(--color-amber-400, oklch(.828 .189 84.429));
|
||||||
|
}
|
||||||
|
.alert.alert__error[data-v-87f6de25] {
|
||||||
|
background-color: color-mix(in oklab, var(--color-rose-400, oklch(.712 .194 13.428)) 10%, transparent);
|
||||||
|
color: var(--color-rose-400, oklch(.712 .194 13.428));
|
||||||
|
}
|
||||||
|
@property --tw-border-style {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false;
|
||||||
|
initial-value: solid;
|
||||||
|
}
|
||||||
|
@property --tw-backdrop-blur {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false
|
||||||
|
}
|
||||||
|
@property --tw-backdrop-brightness {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false
|
||||||
|
}
|
||||||
|
@property --tw-backdrop-contrast {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false
|
||||||
|
}
|
||||||
|
@property --tw-backdrop-grayscale {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false
|
||||||
|
}
|
||||||
|
@property --tw-backdrop-hue-rotate {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false
|
||||||
|
}
|
||||||
|
@property --tw-backdrop-invert {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false
|
||||||
|
}
|
||||||
|
@property --tw-backdrop-opacity {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false
|
||||||
|
}
|
||||||
|
@property --tw-backdrop-saturate {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false
|
||||||
|
}
|
||||||
|
@property --tw-backdrop-sepia {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false
|
||||||
|
}
|
||||||
|
/*! tailwindcss v4.0.9 | MIT License | https://tailwindcss.com */
|
||||||
|
.device-overview[data-v-f4165abd] {
|
||||||
|
align-content: flex-start;
|
||||||
|
gap: calc(var(--spacing, .25rem) * 4);
|
||||||
|
display: grid;
|
||||||
|
}
|
||||||
|
/*! tailwindcss v4.0.9 | MIT License | https://tailwindcss.com */
|
||||||
|
.server-overview[data-v-3109048f] {
|
||||||
|
align-content: flex-start;
|
||||||
|
gap: calc(var(--spacing, .25rem) * 4);
|
||||||
|
display: grid;
|
||||||
|
}
|
||||||
|
/*! tailwindcss v4.0.9 | MIT License | https://tailwindcss.com */
|
||||||
|
|
||||||
106
public/assets/DialogComp-Ba5-BUTe.js
Normal file
106
public/assets/DialogComp-Ba5-BUTe.js
Normal file
|
|
@ -0,0 +1,106 @@
|
||||||
|
import { D as computed, c as createElementBlock, o as openBlock, v as renderSlot, q as normalizeClass, m as ref, b as onMounted, n as onUpdated, f as createBaseVNode, h as createVNode, w as withCtx, u as unref, E as IconX } from "./index-GNAKlyBz.js";
|
||||||
|
const _hoisted_1$1 = ["href"];
|
||||||
|
const _sfc_main$1 = {
|
||||||
|
__name: "ButtonComp",
|
||||||
|
props: {
|
||||||
|
href: String,
|
||||||
|
variant: String,
|
||||||
|
size: String
|
||||||
|
},
|
||||||
|
setup(__props) {
|
||||||
|
const props = __props;
|
||||||
|
const classString = computed(() => {
|
||||||
|
let classes = "btn";
|
||||||
|
if (props.variant) classes += ` btn__${props.variant}`;
|
||||||
|
if (props.size) classes += ` btn__${props.size}`;
|
||||||
|
return classes;
|
||||||
|
});
|
||||||
|
return (_ctx, _cache) => {
|
||||||
|
return __props.href ? (openBlock(), createElementBlock("a", {
|
||||||
|
key: 0,
|
||||||
|
href: __props.href,
|
||||||
|
class: normalizeClass(classString.value)
|
||||||
|
}, [
|
||||||
|
renderSlot(_ctx.$slots, "default")
|
||||||
|
], 10, _hoisted_1$1)) : (openBlock(), createElementBlock("button", {
|
||||||
|
key: 1,
|
||||||
|
class: normalizeClass(classString.value)
|
||||||
|
}, [
|
||||||
|
renderSlot(_ctx.$slots, "default")
|
||||||
|
], 2));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const _hoisted_1 = { class: "dialog-container" };
|
||||||
|
const _sfc_main = {
|
||||||
|
__name: "DialogComp",
|
||||||
|
props: {
|
||||||
|
open: Boolean
|
||||||
|
},
|
||||||
|
emits: ["onOpen", "onClose", "onToggle"],
|
||||||
|
setup(__props, { expose: __expose, emit: __emit }) {
|
||||||
|
const dialog = ref(null);
|
||||||
|
const openDialog = ref();
|
||||||
|
const emit = __emit;
|
||||||
|
__expose({ toggleDialog });
|
||||||
|
const props = __props;
|
||||||
|
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();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return (_ctx, _cache) => {
|
||||||
|
return openBlock(), createElementBlock("div", _hoisted_1, [
|
||||||
|
createBaseVNode("div", {
|
||||||
|
class: "trigger",
|
||||||
|
onClick: _cache[0] || (_cache[0] = ($event) => toggleDialog(true))
|
||||||
|
}, [
|
||||||
|
renderSlot(_ctx.$slots, "trigger")
|
||||||
|
]),
|
||||||
|
createBaseVNode("dialog", {
|
||||||
|
ref_key: "dialog",
|
||||||
|
ref: dialog,
|
||||||
|
class: "mcrm-block block__dark"
|
||||||
|
}, [
|
||||||
|
createVNode(_sfc_main$1, {
|
||||||
|
class: "dialog__close p-0",
|
||||||
|
variant: "ghost",
|
||||||
|
size: "sm",
|
||||||
|
tabindex: "-1",
|
||||||
|
onClick: _cache[1] || (_cache[1] = ($event) => toggleDialog(false))
|
||||||
|
}, {
|
||||||
|
default: withCtx(() => [
|
||||||
|
createVNode(unref(IconX))
|
||||||
|
]),
|
||||||
|
_: 1
|
||||||
|
}),
|
||||||
|
renderSlot(_ctx.$slots, "content")
|
||||||
|
], 512)
|
||||||
|
]);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
};
|
||||||
|
export {
|
||||||
|
_sfc_main$1 as _,
|
||||||
|
_sfc_main as a
|
||||||
|
};
|
||||||
357
public/assets/DialogComp-ByJn29_w.css
Normal file
357
public/assets/DialogComp-ByJn29_w.css
Normal file
|
|
@ -0,0 +1,357 @@
|
||||||
|
/*! tailwindcss v4.0.9 | MIT License | https://tailwindcss.com */
|
||||||
|
button, .btn {
|
||||||
|
cursor: pointer;
|
||||||
|
align-items: center;
|
||||||
|
gap: calc(var(--spacing, .25rem) * 3);
|
||||||
|
border-radius: var(--radius-lg, .5rem);
|
||||||
|
border-style: var(--tw-border-style);
|
||||||
|
--tw-border-style: solid;
|
||||||
|
height: fit-content;
|
||||||
|
padding-inline: calc(var(--spacing, .25rem) * 4);
|
||||||
|
padding-block: calc(var(--spacing, .25rem) * 2);
|
||||||
|
--tw-font-weight: var(--font-weight-normal, 400);
|
||||||
|
font-weight: var(--font-weight-normal, 400);
|
||||||
|
--tw-tracking: var(--tracking-wide, .025em);
|
||||||
|
letter-spacing: var(--tracking-wide, .025em);
|
||||||
|
transition-property: all;
|
||||||
|
transition-timing-function: var(--tw-ease, var(--default-transition-timing-function, cubic-bezier(.4, 0, .2, 1)));
|
||||||
|
transition-duration: var(--tw-duration, var(--default-transition-duration, .15s));
|
||||||
|
border-style: solid;
|
||||||
|
border-width: 1px;
|
||||||
|
transition: border-color .1s ease-in-out, background-color .2s;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
:is(button, .btn):not(.button__subtle, .button__ghost):hover {
|
||||||
|
--tw-shadow-color: var(--color-black, #000);
|
||||||
|
}
|
||||||
|
:is(button, .btn)[disabled], :is(button, .btn).disabled {
|
||||||
|
pointer-events: none;
|
||||||
|
cursor: not-allowed;
|
||||||
|
opacity: .5;
|
||||||
|
}
|
||||||
|
:is(button, .btn) svg {
|
||||||
|
width: calc(var(--spacing, .25rem) * 5);
|
||||||
|
height: calc(var(--spacing, .25rem) * 5);
|
||||||
|
transition-property: stroke;
|
||||||
|
transition-timing-function: var(--tw-ease, var(--default-transition-timing-function, cubic-bezier(.4, 0, .2, 1)));
|
||||||
|
transition-duration: var(--tw-duration, var(--default-transition-duration, .15s));
|
||||||
|
--tw-duration: .4s;
|
||||||
|
--tw-ease: var(--ease-in-out, cubic-bezier(.4, 0, .2, 1));
|
||||||
|
transition-duration: .4s;
|
||||||
|
transition-timing-function: var(--ease-in-out, cubic-bezier(.4, 0, .2, 1));
|
||||||
|
}
|
||||||
|
:is(button, .btn).btn__sm svg {
|
||||||
|
width: calc(var(--spacing, .25rem) * 4);
|
||||||
|
height: calc(var(--spacing, .25rem) * 4);
|
||||||
|
}
|
||||||
|
:is(button, .btn).btn__lg svg {
|
||||||
|
width: calc(var(--spacing, .25rem) * 6);
|
||||||
|
height: calc(var(--spacing, .25rem) * 6);
|
||||||
|
}
|
||||||
|
:is(button, .btn):hover {
|
||||||
|
color: var(--color-white, #fff) !important;
|
||||||
|
}
|
||||||
|
:is(button, .btn):hover svg {
|
||||||
|
stroke: var(--color-white, #fff) !important;
|
||||||
|
}
|
||||||
|
:is(button, .btn).btn__primary {
|
||||||
|
border-color: var(--color-sky-100, oklch(.951 .026 236.824));
|
||||||
|
background-color: color-mix(in oklab, var(--color-sky-100, oklch(.951 .026 236.824)) 10%, transparent);
|
||||||
|
color: var(--color-sky-100, oklch(.951 .026 236.824));
|
||||||
|
}
|
||||||
|
:is(button, .btn).btn__primary svg {
|
||||||
|
stroke: var(--color-sky-200, oklch(.901 .058 230.902));
|
||||||
|
}
|
||||||
|
:is(button, .btn).btn__primary:hover {
|
||||||
|
border-color: var(--color-sky-300, oklch(.828 .111 230.318));
|
||||||
|
background-color: color-mix(in oklab, var(--color-sky-400, oklch(.746 .16 232.661)) 40%, transparent);
|
||||||
|
}
|
||||||
|
:is(button, .btn).btn__secondary {
|
||||||
|
border-color: var(--color-amber-100, oklch(.962 .059 95.617));
|
||||||
|
background-color: color-mix(in oklab, var(--color-amber-100, oklch(.962 .059 95.617)) 10%, transparent);
|
||||||
|
color: var(--color-amber-100, oklch(.962 .059 95.617));
|
||||||
|
}
|
||||||
|
:is(button, .btn).btn__secondary svg {
|
||||||
|
stroke: var(--color-amber-300, oklch(.879 .169 91.605));
|
||||||
|
}
|
||||||
|
:is(button, .btn).btn__secondary:hover {
|
||||||
|
border-color: var(--color-amber-400, oklch(.828 .189 84.429));
|
||||||
|
background-color: color-mix(in oklab, var(--color-amber-400, oklch(.828 .189 84.429)) 40%, transparent);
|
||||||
|
}
|
||||||
|
:is(button, .btn).btn__danger {
|
||||||
|
border-color: var(--color-rose-100, oklch(.941 .03 12.58));
|
||||||
|
background-color: color-mix(in oklab, var(--color-rose-200, oklch(.892 .058 10.001)) 20%, transparent);
|
||||||
|
color: var(--color-rose-200, oklch(.892 .058 10.001));
|
||||||
|
}
|
||||||
|
:is(button, .btn).btn__danger svg {
|
||||||
|
stroke: var(--color-rose-400, oklch(.712 .194 13.428));
|
||||||
|
}
|
||||||
|
:is(button, .btn).btn__danger:hover {
|
||||||
|
border-color: var(--color-rose-500, oklch(.645 .246 16.439));
|
||||||
|
background-color: color-mix(in oklab, var(--color-rose-400, oklch(.712 .194 13.428)) 40%, transparent);
|
||||||
|
color: var(--color-white, #fff);
|
||||||
|
}
|
||||||
|
:is(button, .btn).btn__dark {
|
||||||
|
border-color: var(--color-slate-400, oklch(.704 .04 256.788));
|
||||||
|
background-color: color-mix(in oklab, var(--color-slate-200, oklch(.929 .013 255.508)) 10%, transparent);
|
||||||
|
color: var(--color-slate-100, oklch(.968 .007 247.896));
|
||||||
|
}
|
||||||
|
:is(button, .btn).btn__dark svg {
|
||||||
|
stroke: var(--color-slate-300, oklch(.869 .022 252.894));
|
||||||
|
}
|
||||||
|
:is(button, .btn).btn__dark:hover {
|
||||||
|
border-color: var(--color-slate-200, oklch(.929 .013 255.508));
|
||||||
|
background-color: color-mix(in oklab, var(--color-slate-400, oklch(.704 .04 256.788)) 40%, transparent);
|
||||||
|
color: var(--color-white, #fff);
|
||||||
|
}
|
||||||
|
:is(button, .btn).btn__success {
|
||||||
|
border-color: var(--color-lime-100, oklch(.967 .067 122.328));
|
||||||
|
background-color: color-mix(in oklab, var(--color-lime-200, oklch(.938 .127 124.321)) 10%, transparent);
|
||||||
|
color: var(--color-lime-100, oklch(.967 .067 122.328));
|
||||||
|
}
|
||||||
|
:is(button, .btn).btn__success svg {
|
||||||
|
stroke: var(--color-lime-400, oklch(.841 .238 128.85));
|
||||||
|
}
|
||||||
|
:is(button, .btn).btn__success:hover {
|
||||||
|
border-color: var(--color-lime-500, oklch(.768 .233 130.85));
|
||||||
|
background-color: color-mix(in oklab, var(--color-lime-400, oklch(.841 .238 128.85)) 40%, transparent);
|
||||||
|
color: var(--color-white, #fff);
|
||||||
|
}
|
||||||
|
:is(button, .btn).btn__subtle {
|
||||||
|
color: var(--color-white, #fff);
|
||||||
|
background-color: #0000;
|
||||||
|
border-color: #0000;
|
||||||
|
}
|
||||||
|
@media (hover: hover) {
|
||||||
|
:is(button, .btn).btn__subtle:hover {
|
||||||
|
background-color: color-mix(in oklab, var(--color-white, #fff) 10%, transparent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
:is(button, .btn).btn__subtle:hover {
|
||||||
|
border-color: color-mix(in oklab, var(--color-white, #fff) 40%, transparent);
|
||||||
|
background-color: color-mix(in oklab, var(--color-white, #fff) 20%, transparent);
|
||||||
|
--tw-gradient-to: color-mix(in oklab, var(--color-white, #fff) 30%, transparent);
|
||||||
|
--tw-gradient-stops: var(--tw-gradient-via-stops, var(--tw-gradient-position), var(--tw-gradient-from) var(--tw-gradient-from-position), var(--tw-gradient-to) var(--tw-gradient-to-position));
|
||||||
|
}
|
||||||
|
:is(button, .btn).btn__ghost {
|
||||||
|
color: color-mix(in oklab, var(--color-white, #fff) 80%, transparent);
|
||||||
|
background-color: #0000;
|
||||||
|
border-color: #0000;
|
||||||
|
}
|
||||||
|
@media (hover: hover) {
|
||||||
|
:is(button, .btn).btn__ghost:hover {
|
||||||
|
color: var(--color-white, #fff);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@property --tw-border-style {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false;
|
||||||
|
initial-value: solid;
|
||||||
|
}
|
||||||
|
@property --tw-font-weight {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false
|
||||||
|
}
|
||||||
|
@property --tw-tracking {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false
|
||||||
|
}
|
||||||
|
@property --tw-shadow {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false;
|
||||||
|
initial-value: 0 0 #0000;
|
||||||
|
}
|
||||||
|
@property --tw-shadow-color {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false
|
||||||
|
}
|
||||||
|
@property --tw-inset-shadow {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false;
|
||||||
|
initial-value: 0 0 #0000;
|
||||||
|
}
|
||||||
|
@property --tw-inset-shadow-color {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false
|
||||||
|
}
|
||||||
|
@property --tw-ring-color {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false
|
||||||
|
}
|
||||||
|
@property --tw-ring-shadow {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false;
|
||||||
|
initial-value: 0 0 #0000;
|
||||||
|
}
|
||||||
|
@property --tw-inset-ring-color {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false
|
||||||
|
}
|
||||||
|
@property --tw-inset-ring-shadow {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false;
|
||||||
|
initial-value: 0 0 #0000;
|
||||||
|
}
|
||||||
|
@property --tw-ring-inset {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false
|
||||||
|
}
|
||||||
|
@property --tw-ring-offset-width {
|
||||||
|
syntax: "<length>";
|
||||||
|
inherits: false;
|
||||||
|
initial-value: 0;
|
||||||
|
}
|
||||||
|
@property --tw-ring-offset-color {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false;
|
||||||
|
initial-value: #fff;
|
||||||
|
}
|
||||||
|
@property --tw-ring-offset-shadow {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false;
|
||||||
|
initial-value: 0 0 #0000;
|
||||||
|
}
|
||||||
|
@property --tw-duration {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false
|
||||||
|
}
|
||||||
|
@property --tw-ease {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false
|
||||||
|
}
|
||||||
|
@property --tw-gradient-position {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false
|
||||||
|
}
|
||||||
|
@property --tw-gradient-from {
|
||||||
|
syntax: "<color>";
|
||||||
|
inherits: false;
|
||||||
|
initial-value: #0000;
|
||||||
|
}
|
||||||
|
@property --tw-gradient-via {
|
||||||
|
syntax: "<color>";
|
||||||
|
inherits: false;
|
||||||
|
initial-value: #0000;
|
||||||
|
}
|
||||||
|
@property --tw-gradient-to {
|
||||||
|
syntax: "<color>";
|
||||||
|
inherits: false;
|
||||||
|
initial-value: #0000;
|
||||||
|
}
|
||||||
|
@property --tw-gradient-stops {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false
|
||||||
|
}
|
||||||
|
@property --tw-gradient-via-stops {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false
|
||||||
|
}
|
||||||
|
@property --tw-gradient-from-position {
|
||||||
|
syntax: "<length-percentage>";
|
||||||
|
inherits: false;
|
||||||
|
initial-value: 0%;
|
||||||
|
}
|
||||||
|
@property --tw-gradient-via-position {
|
||||||
|
syntax: "<length-percentage>";
|
||||||
|
inherits: false;
|
||||||
|
initial-value: 50%;
|
||||||
|
}
|
||||||
|
@property --tw-gradient-to-position {
|
||||||
|
syntax: "<length-percentage>";
|
||||||
|
inherits: false;
|
||||||
|
initial-value: 100%;
|
||||||
|
}
|
||||||
|
/*! tailwindcss v4.0.9 | MIT License | https://tailwindcss.com */
|
||||||
|
.dialog-container {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.dialog-container dialog {
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 50;
|
||||||
|
--tw-translate-x: calc(calc(1 / 2 * 100%) * -1);
|
||||||
|
max-width: calc(100vw - 2rem);
|
||||||
|
translate: var(--tw-translate-x) var(--tw-translate-y);
|
||||||
|
--tw-translate-y: calc(calc(1 / 2 * 100%) * -1);
|
||||||
|
translate: var(--tw-translate-x) var(--tw-translate-y);
|
||||||
|
color: var(--color-slate-200, oklch(.929 .013 255.508));
|
||||||
|
position: fixed;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
}
|
||||||
|
.dialog-container dialog[open] {
|
||||||
|
pointer-events: auto;
|
||||||
|
}
|
||||||
|
.dialog-container dialog::backdrop {
|
||||||
|
background-color: color-mix(in oklab, var(--color-black, #000) 50%, transparent);
|
||||||
|
--tw-backdrop-blur: blur(var(--blur-xs, 4px));
|
||||||
|
-webkit-backdrop-filter: var(--tw-backdrop-blur, ) var(--tw-backdrop-brightness, ) var(--tw-backdrop-contrast, ) var(--tw-backdrop-grayscale, ) var(--tw-backdrop-hue-rotate, ) var(--tw-backdrop-invert, ) var(--tw-backdrop-opacity, ) var(--tw-backdrop-saturate, ) var(--tw-backdrop-sepia, );
|
||||||
|
backdrop-filter: var(--tw-backdrop-blur, ) var(--tw-backdrop-brightness, ) var(--tw-backdrop-contrast, ) var(--tw-backdrop-grayscale, ) var(--tw-backdrop-hue-rotate, ) var(--tw-backdrop-invert, ) var(--tw-backdrop-opacity, ) var(--tw-backdrop-saturate, ) var(--tw-backdrop-sepia, );
|
||||||
|
transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to, opacity, box-shadow, transform, translate, scale, rotate, filter, -webkit-backdrop-filter, backdrop-filter;
|
||||||
|
transition-timing-function: var(--tw-ease, var(--default-transition-timing-function, cubic-bezier(.4, 0, .2, 1)));
|
||||||
|
transition-duration: var(--tw-duration, var(--default-transition-duration, .15s));
|
||||||
|
}
|
||||||
|
.dialog-container dialog .dialog__close {
|
||||||
|
top: calc(var(--spacing, .25rem) * 4);
|
||||||
|
right: calc(var(--spacing, .25rem) * 4);
|
||||||
|
padding: calc(var(--spacing, .25rem) * 0);
|
||||||
|
color: var(--color-white, #fff);
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
.dialog-container dialog .dialog__close svg {
|
||||||
|
width: calc(var(--spacing, .25rem) * 5);
|
||||||
|
height: calc(var(--spacing, .25rem) * 5);
|
||||||
|
}
|
||||||
|
.dialog__content > :first-child {
|
||||||
|
padding-right: calc(var(--spacing, .25rem) * 8);
|
||||||
|
}
|
||||||
|
@property --tw-translate-x {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false;
|
||||||
|
initial-value: 0;
|
||||||
|
}
|
||||||
|
@property --tw-translate-y {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false;
|
||||||
|
initial-value: 0;
|
||||||
|
}
|
||||||
|
@property --tw-translate-z {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false;
|
||||||
|
initial-value: 0;
|
||||||
|
}
|
||||||
|
@property --tw-backdrop-blur {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false
|
||||||
|
}
|
||||||
|
@property --tw-backdrop-brightness {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false
|
||||||
|
}
|
||||||
|
@property --tw-backdrop-contrast {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false
|
||||||
|
}
|
||||||
|
@property --tw-backdrop-grayscale {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false
|
||||||
|
}
|
||||||
|
@property --tw-backdrop-hue-rotate {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false
|
||||||
|
}
|
||||||
|
@property --tw-backdrop-invert {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false
|
||||||
|
}
|
||||||
|
@property --tw-backdrop-opacity {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false
|
||||||
|
}
|
||||||
|
@property --tw-backdrop-saturate {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false
|
||||||
|
}
|
||||||
|
@property --tw-backdrop-sepia {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false
|
||||||
|
}
|
||||||
624
public/assets/MacrosView-B-ccNLSC.css
Normal file
624
public/assets/MacrosView-B-ccNLSC.css
Normal file
|
|
@ -0,0 +1,624 @@
|
||||||
|
/*! tailwindcss v4.0.9 | MIT License | https://tailwindcss.com */
|
||||||
|
.macro-overview[data-v-f9a187e3] {
|
||||||
|
grid-template-rows: auto 1fr;
|
||||||
|
display: grid;
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.macro-overview[data-v-f9a187e3]:after {
|
||||||
|
top: calc(var(--spacing, .25rem) * 0);
|
||||||
|
background-color: var(--color-slate-600, oklch(.446 .043 257.281));
|
||||||
|
--tw-content: "";
|
||||||
|
content: var(--tw-content);
|
||||||
|
width: 1px;
|
||||||
|
height: 100%;
|
||||||
|
position: absolute;
|
||||||
|
left: 100%;
|
||||||
|
}
|
||||||
|
.macro-overview .macro-overview__list[data-v-f9a187e3] {
|
||||||
|
align-content: flex-start;
|
||||||
|
gap: calc(var(--spacing, .25rem) * 1);
|
||||||
|
display: grid;
|
||||||
|
}
|
||||||
|
.macro-overview .macro-item[data-v-f9a187e3] {
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
.macro-overview .macro-item button[data-v-f9a187e3] {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
@property --tw-content {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false;
|
||||||
|
initial-value: "";
|
||||||
|
}
|
||||||
|
/*! tailwindcss v4.0.9 | MIT License | https://tailwindcss.com */
|
||||||
|
kbd {
|
||||||
|
height: calc(var(--spacing, .25rem) * 9);
|
||||||
|
align-items: center;
|
||||||
|
gap: calc(var(--spacing, .25rem) * 2);
|
||||||
|
border-radius: var(--radius-md, .375rem);
|
||||||
|
border-style: var(--tw-border-style);
|
||||||
|
border-width: 1px;
|
||||||
|
border-color: var(--color-slate-500, oklch(.554 .046 257.417));
|
||||||
|
background-color: var(--color-slate-700, oklch(.372 .044 257.287));
|
||||||
|
padding-block: calc(var(--spacing, .25rem) * 1);
|
||||||
|
padding-right: calc(var(--spacing, .25rem) * 2);
|
||||||
|
padding-left: calc(var(--spacing, .25rem) * 4);
|
||||||
|
font-family: var(--font-mono, "Fira Code", monospace);
|
||||||
|
font-size: var(--text-lg, 1.125rem);
|
||||||
|
line-height: var(--tw-leading, var(--text-lg--line-height, calc(1.75 / 1.125)));
|
||||||
|
--tw-font-weight: var(--font-weight-bold, 700);
|
||||||
|
font-weight: var(--font-weight-bold, 700);
|
||||||
|
white-space: nowrap;
|
||||||
|
color: var(--color-white, #fff);
|
||||||
|
text-transform: uppercase;
|
||||||
|
--tw-shadow-color: var(--color-slate-500, oklch(.554 .046 257.417));
|
||||||
|
transition-property: all;
|
||||||
|
transition-timing-function: var(--tw-ease, var(--default-transition-timing-function, cubic-bezier(.4, 0, .2, 1)));
|
||||||
|
transition-duration: var(--tw-duration, var(--default-transition-duration, .15s));
|
||||||
|
box-shadow: 0 .2rem 0 .2rem var(--tw-shadow-color);
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
kbd:has(sup) {
|
||||||
|
padding-left: calc(var(--spacing, .25rem) * 2);
|
||||||
|
}
|
||||||
|
kbd sup {
|
||||||
|
margin-top: calc(var(--spacing, .25rem) * 1);
|
||||||
|
font-size: var(--text-xs, .75rem);
|
||||||
|
line-height: var(--tw-leading, var(--text-xs--line-height, calc(1 / .75)));
|
||||||
|
--tw-font-weight: var(--font-weight-light, 300);
|
||||||
|
font-weight: var(--font-weight-light, 300);
|
||||||
|
color: var(--color-slate-200, oklch(.929 .013 255.508));
|
||||||
|
}
|
||||||
|
kbd span.dir {
|
||||||
|
padding-left: calc(var(--spacing, .25rem) * 1);
|
||||||
|
color: var(--color-slate-200, oklch(.929 .013 255.508));
|
||||||
|
}
|
||||||
|
kbd.empty {
|
||||||
|
cursor: pointer;
|
||||||
|
border-color: var(--color-sky-300, oklch(.828 .111 230.318));
|
||||||
|
background-color: color-mix(in oklab, var(--color-sky-400, oklch(.746 .16 232.661)) 50%, transparent);
|
||||||
|
padding-right: calc(var(--spacing, .25rem) * 3);
|
||||||
|
padding-left: calc(var(--spacing, .25rem) * 3);
|
||||||
|
--tw-tracking: var(--tracking-widest, .1em);
|
||||||
|
letter-spacing: var(--tracking-widest, .1em);
|
||||||
|
--tw-shadow-color: var(--color-sky-600, oklch(.588 .158 241.966));
|
||||||
|
}
|
||||||
|
kbd.insert {
|
||||||
|
cursor: pointer;
|
||||||
|
border-color: var(--color-yellow-300, oklch(.905 .182 98.111));
|
||||||
|
background-color: color-mix(in oklab, var(--color-yellow-500, oklch(.795 .184 86.047)) 50%, transparent);
|
||||||
|
--tw-shadow-color: var(--color-yellow-600, oklch(.681 .162 75.834));
|
||||||
|
}
|
||||||
|
:has(kdb):not(.edit) kbd {
|
||||||
|
pointer-events: none;
|
||||||
|
cursor: default;
|
||||||
|
}
|
||||||
|
.edit kbd {
|
||||||
|
pointer-events: auto;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.edit kbd:hover, .edit kbd.active {
|
||||||
|
border-color: var(--color-sky-400, oklch(.746 .16 232.661));
|
||||||
|
background-color: var(--color-sky-900, oklch(.391 .09 240.876));
|
||||||
|
--tw-shadow-color: var(--color-sky-700, oklch(.5 .134 242.749));
|
||||||
|
}
|
||||||
|
@property --tw-border-style {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false;
|
||||||
|
initial-value: solid;
|
||||||
|
}
|
||||||
|
@property --tw-font-weight {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false
|
||||||
|
}
|
||||||
|
@property --tw-shadow {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false;
|
||||||
|
initial-value: 0 0 #0000;
|
||||||
|
}
|
||||||
|
@property --tw-shadow-color {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false
|
||||||
|
}
|
||||||
|
@property --tw-inset-shadow {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false;
|
||||||
|
initial-value: 0 0 #0000;
|
||||||
|
}
|
||||||
|
@property --tw-inset-shadow-color {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false
|
||||||
|
}
|
||||||
|
@property --tw-ring-color {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false
|
||||||
|
}
|
||||||
|
@property --tw-ring-shadow {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false;
|
||||||
|
initial-value: 0 0 #0000;
|
||||||
|
}
|
||||||
|
@property --tw-inset-ring-color {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false
|
||||||
|
}
|
||||||
|
@property --tw-inset-ring-shadow {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false;
|
||||||
|
initial-value: 0 0 #0000;
|
||||||
|
}
|
||||||
|
@property --tw-ring-inset {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false
|
||||||
|
}
|
||||||
|
@property --tw-ring-offset-width {
|
||||||
|
syntax: "<length>";
|
||||||
|
inherits: false;
|
||||||
|
initial-value: 0;
|
||||||
|
}
|
||||||
|
@property --tw-ring-offset-color {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false;
|
||||||
|
initial-value: #fff;
|
||||||
|
}
|
||||||
|
@property --tw-ring-offset-shadow {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false;
|
||||||
|
initial-value: 0 0 #0000;
|
||||||
|
}
|
||||||
|
@property --tw-tracking {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false
|
||||||
|
}
|
||||||
|
/*! tailwindcss v4.0.9 | MIT License | https://tailwindcss.com */
|
||||||
|
span.delay[data-v-05e04cbb] {
|
||||||
|
cursor: default;
|
||||||
|
border-radius: var(--radius-sm, .25rem);
|
||||||
|
border-style: var(--tw-border-style);
|
||||||
|
border-width: 1px;
|
||||||
|
border-color: var(--color-slate-400, oklch(.704 .04 256.788));
|
||||||
|
background-color: var(--color-slate-500, oklch(.554 .046 257.417));
|
||||||
|
padding-inline: calc(var(--spacing, .25rem) * 2);
|
||||||
|
padding-block: calc(var(--spacing, .25rem) * 1);
|
||||||
|
font-family: var(--font-sans, "Roboto", sans-serif);
|
||||||
|
font-size: var(--text-sm, .875rem);
|
||||||
|
line-height: var(--tw-leading, var(--text-sm--line-height, calc(1.25 / .875)));
|
||||||
|
--tw-font-weight: var(--font-weight-semibold, 600);
|
||||||
|
font-weight: var(--font-weight-semibold, 600);
|
||||||
|
color: var(--color-slate-950, oklch(.129 .042 264.695));
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
span.delay.preset[data-v-05e04cbb] {
|
||||||
|
border-color: color-mix(in oklab, var(--color-amber-300, oklch(.879 .169 91.605)) 80%, transparent);
|
||||||
|
background-color: color-mix(in oklab, var(--color-amber-100, oklch(.962 .059 95.617)) 60%, transparent);
|
||||||
|
color: var(--color-amber-400, oklch(.828 .189 84.429));
|
||||||
|
}
|
||||||
|
span.delay i[data-v-05e04cbb] {
|
||||||
|
padding-left: calc(var(--spacing, .25rem) * 1);
|
||||||
|
--tw-font-weight: var(--font-weight-normal, 400);
|
||||||
|
font-weight: var(--font-weight-normal, 400);
|
||||||
|
opacity: .8;
|
||||||
|
font-style: normal;
|
||||||
|
}
|
||||||
|
.edit span.delay[data-v-05e04cbb] {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.edit span.delay[data-v-05e04cbb]:hover, .edit span.delay.active[data-v-05e04cbb] {
|
||||||
|
border-color: var(--color-lime-500, oklch(.768 .233 130.85));
|
||||||
|
background-color: var(--color-lime-700, oklch(.532 .157 131.589));
|
||||||
|
color: var(--color-lime-200, oklch(.938 .127 124.321));
|
||||||
|
}
|
||||||
|
@property --tw-border-style {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false;
|
||||||
|
initial-value: solid;
|
||||||
|
}
|
||||||
|
@property --tw-font-weight {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false
|
||||||
|
}
|
||||||
|
/*! tailwindcss v4.0.9 | MIT License | https://tailwindcss.com */
|
||||||
|
.macro-recorder__output[data-v-33cbf1af] {
|
||||||
|
top: calc(var(--spacing, .25rem) * 0);
|
||||||
|
left: calc(var(--spacing, .25rem) * 0);
|
||||||
|
align-items: center;
|
||||||
|
row-gap: calc(var(--spacing, .25rem) * 4);
|
||||||
|
height: fit-content;
|
||||||
|
padding: calc(var(--spacing, .25rem) * 4);
|
||||||
|
flex-wrap: wrap;
|
||||||
|
display: flex;
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
hr.spacer[data-v-33cbf1af]:last-of-type {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
/*! tailwindcss v4.0.9 | MIT License | https://tailwindcss.com */
|
||||||
|
.recorder-input__container[data-v-9a99c4ac], .macro-recorder__input[data-v-9a99c4ac] {
|
||||||
|
inset: calc(var(--spacing, .25rem) * 0);
|
||||||
|
opacity: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
display: none;
|
||||||
|
position: absolute;
|
||||||
|
}
|
||||||
|
:is(.recorder-input__container, .macro-recorder__input).record[data-v-9a99c4ac] {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
/*! tailwindcss v4.0.9 | MIT License | https://tailwindcss.com */
|
||||||
|
.context-menu {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.context-menu .context-menu__content {
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 50;
|
||||||
|
margin-top: calc(var(--spacing, .25rem) * 2);
|
||||||
|
--tw-translate-y: -100%;
|
||||||
|
min-width: 100%;
|
||||||
|
translate: var(--tw-translate-x) var(--tw-translate-y);
|
||||||
|
border-radius: var(--radius-md, .375rem);
|
||||||
|
border-style: var(--tw-border-style);
|
||||||
|
border-width: 1px;
|
||||||
|
border-color: color-mix(in oklab, var(--color-white, #fff) 50%, transparent);
|
||||||
|
background-color: color-mix(in oklab, var(--color-slate-100, oklch(.968 .007 247.896)) 60%, transparent);
|
||||||
|
color: var(--color-slate-800, oklch(.279 .041 260.031));
|
||||||
|
opacity: 0;
|
||||||
|
--tw-backdrop-blur: blur(var(--blur-3xl, 64px));
|
||||||
|
-webkit-backdrop-filter: var(--tw-backdrop-blur, ) var(--tw-backdrop-brightness, ) var(--tw-backdrop-contrast, ) var(--tw-backdrop-grayscale, ) var(--tw-backdrop-hue-rotate, ) var(--tw-backdrop-invert, ) var(--tw-backdrop-opacity, ) var(--tw-backdrop-saturate, ) var(--tw-backdrop-sepia, );
|
||||||
|
backdrop-filter: var(--tw-backdrop-blur, ) var(--tw-backdrop-brightness, ) var(--tw-backdrop-contrast, ) var(--tw-backdrop-grayscale, ) var(--tw-backdrop-hue-rotate, ) var(--tw-backdrop-invert, ) var(--tw-backdrop-opacity, ) var(--tw-backdrop-saturate, ) var(--tw-backdrop-sepia, );
|
||||||
|
transition-property: all;
|
||||||
|
transition-timing-function: var(--tw-ease, var(--default-transition-timing-function, cubic-bezier(.4, 0, .2, 1)));
|
||||||
|
transition-duration: var(--tw-duration, var(--default-transition-duration, .15s));
|
||||||
|
display: grid;
|
||||||
|
position: absolute;
|
||||||
|
top: 100%;
|
||||||
|
}
|
||||||
|
.context-menu .context-menu__content.open {
|
||||||
|
pointer-events: auto;
|
||||||
|
--tw-translate-y: calc(var(--spacing, .25rem) * 0);
|
||||||
|
translate: var(--tw-translate-x) var(--tw-translate-y);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
.context-menu ul {
|
||||||
|
color: var(--color-slate-800, oklch(.279 .041 260.031));
|
||||||
|
}
|
||||||
|
:where(.context-menu ul > :not(:last-child)) {
|
||||||
|
--tw-divide-y-reverse: 0;
|
||||||
|
border-bottom-style: var(--tw-border-style);
|
||||||
|
border-top-style: var(--tw-border-style);
|
||||||
|
border-top-width: calc(1px * var(--tw-divide-y-reverse));
|
||||||
|
border-bottom-width: calc(1px * calc(1 - var(--tw-divide-y-reverse)));
|
||||||
|
border-color: var(--color-slate-300, oklch(.869 .022 252.894));
|
||||||
|
}
|
||||||
|
.context-menu ul li {
|
||||||
|
cursor: pointer;
|
||||||
|
align-items: center;
|
||||||
|
gap: calc(var(--spacing, .25rem) * 2);
|
||||||
|
padding: calc(var(--spacing, .25rem) * 2);
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
@media (hover: hover) {
|
||||||
|
.context-menu ul li:hover {
|
||||||
|
background-color: color-mix(in oklab, var(--color-black, #000) 10%, transparent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.context-menu ul li svg {
|
||||||
|
width: calc(var(--spacing, .25rem) * 5);
|
||||||
|
height: calc(var(--spacing, .25rem) * 5);
|
||||||
|
}
|
||||||
|
@property --tw-translate-x {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false;
|
||||||
|
initial-value: 0;
|
||||||
|
}
|
||||||
|
@property --tw-translate-y {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false;
|
||||||
|
initial-value: 0;
|
||||||
|
}
|
||||||
|
@property --tw-translate-z {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false;
|
||||||
|
initial-value: 0;
|
||||||
|
}
|
||||||
|
@property --tw-border-style {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false;
|
||||||
|
initial-value: solid;
|
||||||
|
}
|
||||||
|
@property --tw-backdrop-blur {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false
|
||||||
|
}
|
||||||
|
@property --tw-backdrop-brightness {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false
|
||||||
|
}
|
||||||
|
@property --tw-backdrop-contrast {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false
|
||||||
|
}
|
||||||
|
@property --tw-backdrop-grayscale {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false
|
||||||
|
}
|
||||||
|
@property --tw-backdrop-hue-rotate {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false
|
||||||
|
}
|
||||||
|
@property --tw-backdrop-invert {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false
|
||||||
|
}
|
||||||
|
@property --tw-backdrop-opacity {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false
|
||||||
|
}
|
||||||
|
@property --tw-backdrop-saturate {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false
|
||||||
|
}
|
||||||
|
@property --tw-backdrop-sepia {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false
|
||||||
|
}
|
||||||
|
@property --tw-divide-y-reverse {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false;
|
||||||
|
initial-value: 0;
|
||||||
|
}
|
||||||
|
/*! tailwindcss v4.0.9 | MIT License | https://tailwindcss.com */
|
||||||
|
|
||||||
|
/*! tailwindcss v4.0.9 | MIT License | https://tailwindcss.com */
|
||||||
|
button.selected[data-v-601167b6] {
|
||||||
|
background-color: var(--color-sky-500, oklch(.685 .169 237.323));
|
||||||
|
--tw-ring-shadow: var(--tw-ring-inset, ) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentColor);
|
||||||
|
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
|
||||||
|
--tw-ring-color: var(--color-sky-500, oklch(.685 .169 237.323));
|
||||||
|
--tw-ring-offset-width: 1px;
|
||||||
|
--tw-ring-offset-shadow: var(--tw-ring-inset, ) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
|
||||||
|
}
|
||||||
|
@property --tw-shadow {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false;
|
||||||
|
initial-value: 0 0 #0000;
|
||||||
|
}
|
||||||
|
@property --tw-shadow-color {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false
|
||||||
|
}
|
||||||
|
@property --tw-inset-shadow {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false;
|
||||||
|
initial-value: 0 0 #0000;
|
||||||
|
}
|
||||||
|
@property --tw-inset-shadow-color {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false
|
||||||
|
}
|
||||||
|
@property --tw-ring-color {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false
|
||||||
|
}
|
||||||
|
@property --tw-ring-shadow {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false;
|
||||||
|
initial-value: 0 0 #0000;
|
||||||
|
}
|
||||||
|
@property --tw-inset-ring-color {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false
|
||||||
|
}
|
||||||
|
@property --tw-inset-ring-shadow {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false;
|
||||||
|
initial-value: 0 0 #0000;
|
||||||
|
}
|
||||||
|
@property --tw-ring-inset {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false
|
||||||
|
}
|
||||||
|
@property --tw-ring-offset-width {
|
||||||
|
syntax: "<length>";
|
||||||
|
inherits: false;
|
||||||
|
initial-value: 0;
|
||||||
|
}
|
||||||
|
@property --tw-ring-offset-color {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false;
|
||||||
|
initial-value: #fff;
|
||||||
|
}
|
||||||
|
@property --tw-ring-offset-shadow {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false;
|
||||||
|
initial-value: 0 0 #0000;
|
||||||
|
}
|
||||||
|
/*! tailwindcss v4.0.9 | MIT License | https://tailwindcss.com */
|
||||||
|
|
||||||
|
/*! tailwindcss v4.0.9 | MIT License | https://tailwindcss.com */
|
||||||
|
|
||||||
|
/*! tailwindcss v4.0.9 | MIT License | https://tailwindcss.com */
|
||||||
|
.insert-output[data-v-d2aab140] {
|
||||||
|
margin-bottom: calc(var(--spacing, .25rem) * 4);
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
.insert-key__direction[data-v-d2aab140] {
|
||||||
|
margin-top: calc(var(--spacing, .25rem) * 6);
|
||||||
|
justify-content: center;
|
||||||
|
gap: calc(var(--spacing, .25rem) * 2);
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
button.selected[data-v-d2aab140] {
|
||||||
|
background-color: var(--color-sky-500, oklch(.685 .169 237.323));
|
||||||
|
--tw-ring-shadow: var(--tw-ring-inset, ) 0 0 0 calc(2px + var(--tw-ring-offset-width)) var(--tw-ring-color, currentColor);
|
||||||
|
box-shadow: var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow);
|
||||||
|
--tw-ring-color: var(--color-sky-500, oklch(.685 .169 237.323));
|
||||||
|
--tw-ring-offset-width: 1px;
|
||||||
|
--tw-ring-offset-shadow: var(--tw-ring-inset, ) 0 0 0 var(--tw-ring-offset-width) var(--tw-ring-offset-color);
|
||||||
|
}
|
||||||
|
@property --tw-shadow {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false;
|
||||||
|
initial-value: 0 0 #0000;
|
||||||
|
}
|
||||||
|
@property --tw-shadow-color {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false
|
||||||
|
}
|
||||||
|
@property --tw-inset-shadow {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false;
|
||||||
|
initial-value: 0 0 #0000;
|
||||||
|
}
|
||||||
|
@property --tw-inset-shadow-color {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false
|
||||||
|
}
|
||||||
|
@property --tw-ring-color {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false
|
||||||
|
}
|
||||||
|
@property --tw-ring-shadow {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false;
|
||||||
|
initial-value: 0 0 #0000;
|
||||||
|
}
|
||||||
|
@property --tw-inset-ring-color {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false
|
||||||
|
}
|
||||||
|
@property --tw-inset-ring-shadow {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false;
|
||||||
|
initial-value: 0 0 #0000;
|
||||||
|
}
|
||||||
|
@property --tw-ring-inset {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false
|
||||||
|
}
|
||||||
|
@property --tw-ring-offset-width {
|
||||||
|
syntax: "<length>";
|
||||||
|
inherits: false;
|
||||||
|
initial-value: 0;
|
||||||
|
}
|
||||||
|
@property --tw-ring-offset-color {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false;
|
||||||
|
initial-value: #fff;
|
||||||
|
}
|
||||||
|
@property --tw-ring-offset-shadow {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false;
|
||||||
|
initial-value: 0 0 #0000;
|
||||||
|
}
|
||||||
|
/*! tailwindcss v4.0.9 | MIT License | https://tailwindcss.com */
|
||||||
|
.macro-edit__dialogs[data-v-bf9e32be] {
|
||||||
|
flex-grow: 1;
|
||||||
|
justify-content: flex-end;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
/*! tailwindcss v4.0.9 | MIT License | https://tailwindcss.com */
|
||||||
|
.macro-recorder__header[data-v-19251359] {
|
||||||
|
gap: calc(var(--spacing, .25rem) * 4);
|
||||||
|
width: 100%;
|
||||||
|
display: grid;
|
||||||
|
}
|
||||||
|
.macro-recorder__header .edit__buttons[data-v-19251359] {
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: calc(var(--spacing, .25rem) * 2);
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
.macro-recorder__header > div[data-v-19251359] {
|
||||||
|
align-items: flex-end;
|
||||||
|
gap: calc(var(--spacing, .25rem) * 2);
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
/*! tailwindcss v4.0.9 | MIT License | https://tailwindcss.com */
|
||||||
|
|
||||||
|
/*! tailwindcss v4.0.9 | MIT License | https://tailwindcss.com */
|
||||||
|
.macro-recorder__footer[data-v-fec5e8b6] {
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: calc(var(--spacing, .25rem) * 2);
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
/*! tailwindcss v4.0.9 | MIT License | https://tailwindcss.com */
|
||||||
|
.macro-recorder {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
.recorder-interface {
|
||||||
|
gap: calc(var(--spacing, .25rem) * 4);
|
||||||
|
height: 100%;
|
||||||
|
transition-property: grid-template-rows;
|
||||||
|
transition-timing-function: var(--tw-ease, var(--default-transition-timing-function, cubic-bezier(.4, 0, .2, 1)));
|
||||||
|
transition-duration: var(--tw-duration, var(--default-transition-duration, .15s));
|
||||||
|
grid-template-rows: auto 1fr auto;
|
||||||
|
display: grid;
|
||||||
|
}
|
||||||
|
.recorder-interface__container {
|
||||||
|
border-radius: var(--radius-lg, .5rem);
|
||||||
|
border-style: var(--tw-border-style);
|
||||||
|
border-width: 1px;
|
||||||
|
border-color: var(--color-slate-600, oklch(.446 .043 257.281));
|
||||||
|
background-color: color-mix(in oklab, var(--color-slate-950, oklch(.129 .042 264.695)) 50%, transparent);
|
||||||
|
width: 100%;
|
||||||
|
transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to;
|
||||||
|
transition-timing-function: var(--tw-ease, var(--default-transition-timing-function, cubic-bezier(.4, 0, .2, 1)));
|
||||||
|
transition-duration: var(--tw-duration, var(--default-transition-duration, .15s));
|
||||||
|
position: relative;
|
||||||
|
overflow: auto;
|
||||||
|
}
|
||||||
|
.recorder-interface__container.record {
|
||||||
|
border-color: var(--color-rose-300, oklch(.81 .117 11.638));
|
||||||
|
background-color: color-mix(in oklab, var(--color-rose-400, oklch(.712 .194 13.428)) 10%, transparent);
|
||||||
|
}
|
||||||
|
.recorder-interface__container.edit {
|
||||||
|
border-color: var(--color-sky-300, oklch(.828 .111 230.318));
|
||||||
|
background-color: color-mix(in oklab, var(--color-sky-900, oklch(.391 .09 240.876)) 10%, transparent);
|
||||||
|
}
|
||||||
|
#macro-name {
|
||||||
|
border-color: #0000;
|
||||||
|
border-bottom-color: var(--color-slate-300, oklch(.869 .022 252.894));
|
||||||
|
width: 100%;
|
||||||
|
padding-block: calc(var(--spacing, .25rem) * 0);
|
||||||
|
font-size: var(--text-lg, 1.125rem);
|
||||||
|
line-height: var(--tw-leading, var(--text-lg--line-height, calc(1.75 / 1.125)));
|
||||||
|
outline-style: var(--tw-outline-style);
|
||||||
|
transition-property: color, background-color, border-color, outline-color, text-decoration-color, fill, stroke, --tw-gradient-from, --tw-gradient-via, --tw-gradient-to;
|
||||||
|
transition-timing-function: var(--tw-ease, var(--default-transition-timing-function, cubic-bezier(.4, 0, .2, 1)));
|
||||||
|
transition-duration: var(--tw-duration, var(--default-transition-duration, .15s));
|
||||||
|
background-color: #0000;
|
||||||
|
border-radius: 0;
|
||||||
|
outline-width: 0;
|
||||||
|
}
|
||||||
|
#macro-name:focus {
|
||||||
|
border-color: #0000;
|
||||||
|
border-bottom-color: var(--color-sky-400, oklch(.746 .16 232.661));
|
||||||
|
background-color: color-mix(in oklab, var(--color-sky-400, oklch(.746 .16 232.661)) 10%, transparent);
|
||||||
|
}
|
||||||
|
.disabled {
|
||||||
|
pointer-events: none;
|
||||||
|
cursor: not-allowed;
|
||||||
|
opacity: .5;
|
||||||
|
}
|
||||||
|
@property --tw-border-style {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false;
|
||||||
|
initial-value: solid;
|
||||||
|
}
|
||||||
|
@property --tw-outline-style {
|
||||||
|
syntax: "*";
|
||||||
|
inherits: false;
|
||||||
|
initial-value: solid;
|
||||||
|
}
|
||||||
|
/*! tailwindcss v4.0.9 | MIT License | https://tailwindcss.com */
|
||||||
|
.macro-panel__content[data-v-c7be9772] {
|
||||||
|
gap: calc(var(--spacing, .25rem) * 6);
|
||||||
|
padding-top: calc(var(--spacing, .25rem) * 2);
|
||||||
|
grid-template-columns: 25ch 1fr;
|
||||||
|
display: grid;
|
||||||
|
}
|
||||||
1419
public/assets/MacrosView-Bf1eb3go.js
Normal file
1419
public/assets/MacrosView-Bf1eb3go.js
Normal file
File diff suppressed because it is too large
Load diff
9
public/assets/PanelsView-DHxhdGwy.js
Normal file
9
public/assets/PanelsView-DHxhdGwy.js
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
import { _ as _export_sfc, c as createElementBlock, o as openBlock } from "./index-GNAKlyBz.js";
|
||||||
|
const _sfc_main = {};
|
||||||
|
function _sfc_render(_ctx, _cache) {
|
||||||
|
return openBlock(), createElementBlock("div");
|
||||||
|
}
|
||||||
|
const PanelsView = /* @__PURE__ */ _export_sfc(_sfc_main, [["render", _sfc_render]]);
|
||||||
|
export {
|
||||||
|
PanelsView as default
|
||||||
|
};
|
||||||
9
public/assets/SettingsView-CVQl1jsc.js
Normal file
9
public/assets/SettingsView-CVQl1jsc.js
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
import { _ as _export_sfc, c as createElementBlock, o as openBlock } from "./index-GNAKlyBz.js";
|
||||||
|
const _sfc_main = {};
|
||||||
|
function _sfc_render(_ctx, _cache) {
|
||||||
|
return openBlock(), createElementBlock("div");
|
||||||
|
}
|
||||||
|
const SettingsView = /* @__PURE__ */ _export_sfc(_sfc_main, [["render", _sfc_render]]);
|
||||||
|
export {
|
||||||
|
SettingsView as default
|
||||||
|
};
|
||||||
2256
public/assets/index-DjeOPht9.css
Normal file
2256
public/assets/index-DjeOPht9.css
Normal file
File diff suppressed because it is too large
Load diff
18013
public/assets/index-GNAKlyBz.js
Normal file
18013
public/assets/index-GNAKlyBz.js
Normal file
File diff suppressed because it is too large
Load diff
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue