Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| cb772b46e3 |
21
LICENSE.md
21
LICENSE.md
@ -1,21 +0,0 @@
|
|||||||
MIT License
|
|
||||||
|
|
||||||
Copyright (c) 2025 Fritz Heiden
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
||||||
of this software and associated documentation files (the "Software"), to deal
|
|
||||||
in the Software without restriction, including without limitation the rights
|
|
||||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
||||||
copies of the Software, and to permit persons to whom the Software is
|
|
||||||
furnished to do so, subject to the following conditions:
|
|
||||||
|
|
||||||
The above copyright notice and this permission notice shall be included in all
|
|
||||||
copies or substantial portions of the Software.
|
|
||||||
|
|
||||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
||||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
||||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
||||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
||||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
||||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
||||||
SOFTWARE.
|
|
||||||
11
Makefile
11
Makefile
@ -1,11 +0,0 @@
|
|||||||
build: build-server build-web-interface
|
|
||||||
echo "done"
|
|
||||||
|
|
||||||
build-server:
|
|
||||||
CGO_ENABLED=1 go build -C "./main" -o ../start
|
|
||||||
|
|
||||||
build-server-start: build-server
|
|
||||||
chmod +x ./start; ./start
|
|
||||||
|
|
||||||
build-web-interface:
|
|
||||||
cd www && npm run build
|
|
||||||
4
build.sh
Executable file
4
build.sh
Executable file
@ -0,0 +1,4 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||||
|
CGO_ENABLED=1 go build -C "$SCRIPT_DIR/main" -o ../start
|
||||||
@ -1,75 +0,0 @@
|
|||||||
package data
|
|
||||||
|
|
||||||
const SAMSUNG_PROTOCOL = "samsung"
|
|
||||||
const NEC_PROTOCOL = "nec"
|
|
||||||
const ONKYO_PROTOCOL = "onkyo"
|
|
||||||
const APPLE_PROTOCOL = "apple"
|
|
||||||
const DENON_PROTOCOL = "denon"
|
|
||||||
const SHARP_PROTOCOL = "sharp"
|
|
||||||
const PANASONIC_PROTOCOL = "panasonic"
|
|
||||||
const KASEIKYO_PROTOCOL = "kaseikyo"
|
|
||||||
const JVC_PROTOCOL = "jvc"
|
|
||||||
const LG_PROTOCOL = "lg"
|
|
||||||
const SONY_PROTOCOL = "sony"
|
|
||||||
const RC5_PROTOCOL = "rc5"
|
|
||||||
const RC6_PROTOCOL = "rc6"
|
|
||||||
const UNIVERSAL_PULSE_DISTANCE_PROTOCOL = "universal_pulse_distance"
|
|
||||||
const UNIVERSAL_PULSE_WIDTH_PROTOCOL = "universal_pulse_width"
|
|
||||||
const UNIVERSAL_PULSE_DISTANCE_WIDTH_PROTOCOL = "universal_pulse_distance_width"
|
|
||||||
const HASH_PROTOCOL = "hash"
|
|
||||||
const PRONTO_PROTOCOL = "pronto"
|
|
||||||
const BOSE_WAVE_PROTOCOL = "bose_wave"
|
|
||||||
const BANG_OLUFSEN_PROTOCOL = "bang_olufsen"
|
|
||||||
const LEGO_PROTOCOL = "lego"
|
|
||||||
const FAST_PROTOCOL = "fast"
|
|
||||||
const WHYNTER_PROTOCOL = "whynter"
|
|
||||||
const MAGIQUEST_PROTOCOL = "magiquest"
|
|
||||||
|
|
||||||
const POWER_COMMAND_TYPE = "power"
|
|
||||||
const INPUT_COMMAND_TYPE = "input"
|
|
||||||
const ONE_COMMAND_TYPE = "1"
|
|
||||||
const TWO_COMMAND_TYPE = "2"
|
|
||||||
const THREE_COMMAND_TYPE = "3"
|
|
||||||
const FOUR_COMMAND_TYPE = "4"
|
|
||||||
const FIVE_COMMAND_TYPE = "5"
|
|
||||||
const SIX_COMMAND_TYPE = "6"
|
|
||||||
const SEVEN_COMMAND_TYPE = "7"
|
|
||||||
const EIGHT_COMMAND_TYPE = "8"
|
|
||||||
const NINE_COMMAND_TYPE = "9"
|
|
||||||
const ZERO_COMMAND_TYPE = "0"
|
|
||||||
const VOLUME_UP_COMMAND_TYPE = "volume_up"
|
|
||||||
const VOLUME_DOWN_COMMAND_TYPE = "volume_down"
|
|
||||||
const MUTE_COMMAND_TYPE = "mute"
|
|
||||||
const CHANNEL_UP_COMMAND_TYPE = "channel_up"
|
|
||||||
const CHANNEL_DOWN_COMMAND_TYPE = "channel_down"
|
|
||||||
const MENU_COMMAND_TYPE = "menu"
|
|
||||||
const HOME_COMMAND_TYPE = "home"
|
|
||||||
const SETTINGS_COMMAND_TYPE = "settings"
|
|
||||||
const OPTIONS_COMMAND_TYPE = "options"
|
|
||||||
const UP_COMMAND_TYPE = "up"
|
|
||||||
const DOWN_COMMAND_TYPE = "down"
|
|
||||||
const LEFT_COMMAND_TYPE = "left"
|
|
||||||
const RIGHT_COMMAND_TYPE = "right"
|
|
||||||
const ENTER_COMMAND_TYPE = "enter"
|
|
||||||
const INFO_COMMAND_TYPE = "info"
|
|
||||||
const RETURN_COMMAND_TYPE = "return"
|
|
||||||
const EXIT_COMMAND_TYPE = "exit"
|
|
||||||
const RED_COMMAND_TYPE = "red"
|
|
||||||
const GREEN_COMMAND_TYPE = "green"
|
|
||||||
const YELLOW_COMMAND_TYPE = "yellow"
|
|
||||||
const BLUE_COMMAND_TYPE = "blue"
|
|
||||||
const REWIND_COMMAND_TYPE = "rewind"
|
|
||||||
const PLAY_COMMAND_TYPE = "play"
|
|
||||||
const PAUSE_COMMAND_TYPE = "pause"
|
|
||||||
const STOP_COMMAND_TYPE = "stop"
|
|
||||||
const FORWARD_COMMAND_TYPE = "forward"
|
|
||||||
const OTHER_COMMAND_TYPE = "other"
|
|
||||||
|
|
||||||
type Command struct {
|
|
||||||
Id string `json:"id"`
|
|
||||||
Protocol string `json:"protocol"`
|
|
||||||
CommandNumber int `json:"commandNumber"`
|
|
||||||
Device int `json:"device"`
|
|
||||||
CommandType string `json:"commandType"`
|
|
||||||
Title string `json:"title"`
|
|
||||||
}
|
|
||||||
@ -110,7 +110,12 @@ func (db *DeviceDatabase) CreateIntegration(name, token string) (string, error)
|
|||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = db.Connection.Exec("INSERT INTO integrations (id, name, token) VALUES (?, ?, ?)", id, name, token)
|
hashed_token, err := hashPassword(token)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = db.Connection.Exec("INSERT INTO integrations (id, name, token) VALUES (?, ?, ?)", id, name, hashed_token)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
@ -158,22 +163,6 @@ func (db *DeviceDatabase) GetIntegrations() ([]Integration, error) {
|
|||||||
return integrations, nil
|
return integrations, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (db *DeviceDatabase) GetIntegrationByToken(token string) (*Integration, error) {
|
|
||||||
exists, err := db.IntegrationTokenExists(token)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if !exists {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
var integration Integration
|
|
||||||
err = db.Connection.QueryRow("SELECT id, name FROM integrations WHERE token = ?", token).Scan(&integration.ID, &integration.Name)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &integration, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (db *DeviceDatabase) DeleteIntegration(id string) error {
|
func (db *DeviceDatabase) DeleteIntegration(id string) error {
|
||||||
_, err := db.Connection.Exec("DELETE FROM integrations WHERE id = ?", id)
|
_, err := db.Connection.Exec("DELETE FROM integrations WHERE id = ?", id)
|
||||||
return err
|
return err
|
||||||
@ -188,13 +177,22 @@ func (db *DeviceDatabase) IntegrationNameExists(name string) (bool, error) {
|
|||||||
return exists, nil
|
return exists, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (db *DeviceDatabase) IntegrationTokenExists(token string) (bool, error) {
|
func (db *DeviceDatabase) GetSession(sessionToken string) (*DeviceSession, error) {
|
||||||
var exists bool
|
var session DeviceSession
|
||||||
err := db.Connection.QueryRow("SELECT EXISTS(SELECT 1 FROM integrations WHERE token = ?)", token).Scan(&exists)
|
row := db.Connection.QueryRow("SELECT token, device_id, expiry_date FROM sessions WHERE token = ?", sessionToken)
|
||||||
|
err := row.Scan(&session.Token, &session.DeviceID, &session.ExpiryDate)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
if err == sql.ErrNoRows {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return nil, err
|
||||||
}
|
}
|
||||||
return exists, nil
|
return &session, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (db *DeviceDatabase) DeleteSessionByToken(token string) error {
|
||||||
|
_, err := db.Connection.Exec("DELETE FROM sessions WHERE token = ?", token)
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (db *DeviceDatabase) SetDirectory(directory string) {
|
func (db *DeviceDatabase) SetDirectory(directory string) {
|
||||||
|
|||||||
9
data/device_session.go
Normal file
9
data/device_session.go
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
package data
|
||||||
|
|
||||||
|
import "time"
|
||||||
|
|
||||||
|
type DeviceSession struct {
|
||||||
|
DeviceID string
|
||||||
|
Token string
|
||||||
|
ExpiryDate time.Time
|
||||||
|
}
|
||||||
@ -1,7 +0,0 @@
|
|||||||
package data
|
|
||||||
|
|
||||||
type Remote struct {
|
|
||||||
Id string `json:"id"`
|
|
||||||
Title string `json:"title"`
|
|
||||||
Commands []Command `json:"commands"`
|
|
||||||
}
|
|
||||||
@ -1,255 +0,0 @@
|
|||||||
package data
|
|
||||||
|
|
||||||
import (
|
|
||||||
"database/sql"
|
|
||||||
"fmt"
|
|
||||||
"path/filepath"
|
|
||||||
|
|
||||||
gonanoid "github.com/matoous/go-nanoid"
|
|
||||||
)
|
|
||||||
|
|
||||||
type RemoteDatabase struct {
|
|
||||||
Connection *sql.DB
|
|
||||||
databaseDirectory string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (db *RemoteDatabase) Initialize() error {
|
|
||||||
connection, error := sql.Open("sqlite3", filepath.Join(db.databaseDirectory, "remotes.db"))
|
|
||||||
if error != nil {
|
|
||||||
return error
|
|
||||||
}
|
|
||||||
db.Connection = connection
|
|
||||||
|
|
||||||
_, error = db.Connection.Exec(`PRAGMA foreign_keys = ON;`)
|
|
||||||
if error != nil {
|
|
||||||
return fmt.Errorf("error enabling foreign keys: %s", error)
|
|
||||||
}
|
|
||||||
|
|
||||||
_, error = db.Connection.Exec(`
|
|
||||||
CREATE TABLE IF NOT EXISTS Remotes (
|
|
||||||
id TEXT PRIMARY KEY,
|
|
||||||
title TEXT NOT NULL
|
|
||||||
);`)
|
|
||||||
if error != nil {
|
|
||||||
return fmt.Errorf("error creating remotes table: %s", error)
|
|
||||||
}
|
|
||||||
|
|
||||||
_, error = db.Connection.Exec(`
|
|
||||||
CREATE TABLE IF NOT EXISTS Commands (
|
|
||||||
id TEXT PRIMARY KEY,
|
|
||||||
protocol TEXT,
|
|
||||||
commandNumber INTEGER,
|
|
||||||
device INTEGER,
|
|
||||||
commandType TEXT,
|
|
||||||
title TEXT
|
|
||||||
);`)
|
|
||||||
if error != nil {
|
|
||||||
return fmt.Errorf("error creating commands table: %s", error)
|
|
||||||
}
|
|
||||||
|
|
||||||
_, error = db.Connection.Exec(`
|
|
||||||
CREATE TABLE IF NOT EXISTS RemoteCommands (
|
|
||||||
remote_id TEXT NOT NULL,
|
|
||||||
command_id TEXT NOT NULL,
|
|
||||||
PRIMARY KEY (remote_id, command_id),
|
|
||||||
FOREIGN KEY (remote_id) REFERENCES Remotes(id) ON DELETE CASCADE,
|
|
||||||
FOREIGN KEY (command_id) REFERENCES Commands(id) ON DELETE CASCADE
|
|
||||||
);`)
|
|
||||||
if error != nil {
|
|
||||||
return fmt.Errorf("error creating remote-commands table: %s", error)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (db *RemoteDatabase) Close() error {
|
|
||||||
return db.Connection.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (db *RemoteDatabase) CreateRemote(remote Remote) (string, error) {
|
|
||||||
remoteId, err := gonanoid.Nanoid(8)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
queryString := "INSERT INTO Remotes (id, title) VALUES (?, ?)"
|
|
||||||
_, err = db.Connection.Exec(queryString, remoteId, remote.Title)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
commandIds := []string{}
|
|
||||||
for _, command := range remote.Commands {
|
|
||||||
commandIds = append(commandIds, command.Id)
|
|
||||||
}
|
|
||||||
err = db.CreateRemoteCommands(remoteId, commandIds)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return remoteId, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (db *RemoteDatabase) CreateRemoteCommands(remoteId string, commandIds []string) error {
|
|
||||||
for _, commandId := range commandIds {
|
|
||||||
queryString := "INSERT INTO RemoteCommands (remote_id, command_id) VALUES (?, ?)"
|
|
||||||
_, err := db.Connection.Exec(queryString, remoteId, commandId)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (db *RemoteDatabase) UpdateRemoteCommands(remoteId string, commandIds []string) error {
|
|
||||||
queryString := "DELETE FROM RemoteCommands WHERE remote_id = ?"
|
|
||||||
_, err := db.Connection.Exec(queryString, remoteId)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
err = db.CreateRemoteCommands(remoteId, commandIds)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (db *RemoteDatabase) GetRemote(remoteId string) (Remote, error) {
|
|
||||||
var remote Remote
|
|
||||||
queryString := "SELECT id, title FROM Remotes WHERE id = ?"
|
|
||||||
row := db.Connection.QueryRow(queryString, remoteId)
|
|
||||||
error := row.Scan(&remote.Id, &remote.Title)
|
|
||||||
if error != nil {
|
|
||||||
return Remote{}, fmt.Errorf("error scanning remote: %s", error)
|
|
||||||
}
|
|
||||||
commands, error := db.GetCommandsByRemoteId(remoteId)
|
|
||||||
if error != nil {
|
|
||||||
return Remote{}, error
|
|
||||||
}
|
|
||||||
remote.Commands = commands
|
|
||||||
return remote, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (db *RemoteDatabase) GetRemotes() ([]Remote, error) {
|
|
||||||
rows, error := db.Connection.Query("SELECT id, title FROM Remotes")
|
|
||||||
if error != nil {
|
|
||||||
return nil, fmt.Errorf("error querying remotes: %s", error)
|
|
||||||
}
|
|
||||||
defer rows.Close()
|
|
||||||
|
|
||||||
remotes := []Remote{}
|
|
||||||
for rows.Next() {
|
|
||||||
var remote Remote
|
|
||||||
error = rows.Scan(&remote.Id, &remote.Title)
|
|
||||||
if error != nil {
|
|
||||||
return nil, fmt.Errorf("error scanning remote: %s", error)
|
|
||||||
}
|
|
||||||
remotes = append(remotes, remote)
|
|
||||||
}
|
|
||||||
return remotes, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (db *RemoteDatabase) UpdateRemote(remote Remote) error {
|
|
||||||
queryString := "UPDATE Remotes SET title = ? WHERE id = ?"
|
|
||||||
_, error := db.Connection.Exec(queryString, remote.Title, remote.Id)
|
|
||||||
if error != nil {
|
|
||||||
return fmt.Errorf("error updating remote %s: %s", remote.Id, error)
|
|
||||||
}
|
|
||||||
commandIds := []string{}
|
|
||||||
for _, command := range remote.Commands {
|
|
||||||
commandIds = append(commandIds, command.Id)
|
|
||||||
}
|
|
||||||
err := db.UpdateRemoteCommands(remote.Id, commandIds)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (db *RemoteDatabase) DeleteRemote(remoteId string) error {
|
|
||||||
queryString := "DELETE FROM Remotes WHERE id = ?"
|
|
||||||
_, error := db.Connection.Exec(queryString, remoteId)
|
|
||||||
if error != nil {
|
|
||||||
return fmt.Errorf("error deleting remote %s: %s", remoteId, error)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (db *RemoteDatabase) GetCommandsByRemoteId(remoteId string) ([]Command, error) {
|
|
||||||
rows, error := db.Connection.Query("SELECT id, protocol, commandNumber, device, commandType, title FROM Commands WHERE id IN (SELECT command_id FROM RemoteCommands WHERE remote_id = ?)", remoteId)
|
|
||||||
if error != nil {
|
|
||||||
return nil, fmt.Errorf("error querying commands for remote %s: %s", remoteId, error)
|
|
||||||
}
|
|
||||||
defer rows.Close()
|
|
||||||
|
|
||||||
commands := []Command{}
|
|
||||||
for rows.Next() {
|
|
||||||
var command Command
|
|
||||||
error = rows.Scan(&command.Id, &command.Protocol, &command.CommandNumber, &command.Device, &command.CommandType, &command.Title)
|
|
||||||
if error != nil {
|
|
||||||
return nil, fmt.Errorf("error scanning command: %s", error)
|
|
||||||
}
|
|
||||||
commands = append(commands, command)
|
|
||||||
}
|
|
||||||
return commands, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (db *RemoteDatabase) CreateCommand(command Command) (string, error) {
|
|
||||||
commandId, err := gonanoid.Nanoid(8)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
queryString := "INSERT INTO Commands (id, protocol, commandNumber, device, commandType, title) VALUES (?, ?, ?, ?, ?, ?)"
|
|
||||||
_, err = db.Connection.Exec(queryString, commandId, command.Protocol, command.CommandNumber, command.Device, command.CommandType, command.Title)
|
|
||||||
if err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
return commandId, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (db *RemoteDatabase) CreateCommands(commands []Command) error {
|
|
||||||
for _, command := range commands {
|
|
||||||
_, err := db.CreateCommand(command)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (db *RemoteDatabase) GetCommands() ([]Command, error) {
|
|
||||||
rows, error := db.Connection.Query("SELECT id, protocol, commandNumber, device, commandType, title FROM Commands")
|
|
||||||
if error != nil {
|
|
||||||
return nil, fmt.Errorf("error querying commands: %s", error)
|
|
||||||
}
|
|
||||||
defer rows.Close()
|
|
||||||
|
|
||||||
commands := []Command{}
|
|
||||||
for rows.Next() {
|
|
||||||
var command Command
|
|
||||||
error = rows.Scan(&command.Id, &command.Protocol, &command.CommandNumber, &command.Device, &command.CommandType, &command.Title)
|
|
||||||
if error != nil {
|
|
||||||
return nil, fmt.Errorf("error scanning command: %s", error)
|
|
||||||
}
|
|
||||||
commands = append(commands, command)
|
|
||||||
}
|
|
||||||
return commands, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (db *RemoteDatabase) UpdateCommand(command Command) error {
|
|
||||||
queryString := "UPDATE Commands SET protocol = ?, commandNumber = ?, device = ?, commandType = ?, title = ? WHERE id = ?"
|
|
||||||
_, error := db.Connection.Exec(queryString, command.Protocol, command.CommandNumber, command.Device, command.CommandType, command.Title, command.Id)
|
|
||||||
if error != nil {
|
|
||||||
return fmt.Errorf("error updating command %s: %s", command.Id, error)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (db *RemoteDatabase) DeleteCommand(commandId string) error {
|
|
||||||
queryString := "DELETE FROM Commands WHERE id = ?"
|
|
||||||
_, error := db.Connection.Exec(queryString, commandId)
|
|
||||||
if error != nil {
|
|
||||||
return fmt.Errorf("error deleting command %s: %s", commandId, error)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (db *RemoteDatabase) SetDirectory(directory string) {
|
|
||||||
db.databaseDirectory = directory
|
|
||||||
}
|
|
||||||
@ -86,15 +86,8 @@ func (db *UserDatabase) GetUserByUsername(username string) (*User, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (db *UserDatabase) GetUserById(id string) (*User, error) {
|
func (db *UserDatabase) GetUserById(id string) (*User, error) {
|
||||||
exists, err := db.UserIdExists(id)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if !exists {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
var user User
|
var user User
|
||||||
err = db.Connection.QueryRow("SELECT id, username, password, is_admin FROM users WHERE id = ?", id).Scan(&user.ID, &user.Username, &user.Password, &user.IsAdmin)
|
err := db.Connection.QueryRow("SELECT id, username, password, is_admin FROM users WHERE id = ?", id).Scan(&user.ID, &user.Username, &user.Password, &user.IsAdmin)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -152,16 +145,9 @@ func (db *UserDatabase) CheckCredentials(username, password string) (bool, error
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (db *UserDatabase) GetSession(sessionToken string) (*UserSession, error) {
|
func (db *UserDatabase) GetSession(sessionToken string) (*UserSession, error) {
|
||||||
exists, err := db.SessionTokenExists(sessionToken)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if !exists {
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
var session UserSession
|
var session UserSession
|
||||||
row := db.Connection.QueryRow("SELECT token, user_id, expiry_date FROM sessions WHERE token = ?", sessionToken)
|
row := db.Connection.QueryRow("SELECT token, user_id, expiry_date FROM sessions WHERE token = ?", sessionToken)
|
||||||
err = row.Scan(&session.Token, &session.UserID, &session.ExpiryDate)
|
err := row.Scan(&session.Token, &session.UserID, &session.ExpiryDate)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if err == sql.ErrNoRows {
|
if err == sql.ErrNoRows {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
@ -171,15 +157,6 @@ func (db *UserDatabase) GetSession(sessionToken string) (*UserSession, error) {
|
|||||||
return &session, nil
|
return &session, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (db *UserDatabase) SessionTokenExists(token string) (bool, error) {
|
|
||||||
var exists bool
|
|
||||||
err := db.Connection.QueryRow("SELECT EXISTS(SELECT 1 FROM sessions WHERE token = ?)", token).Scan(&exists)
|
|
||||||
if err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
return exists, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (db *UserDatabase) DeleteSessionByToken(token string) error {
|
func (db *UserDatabase) DeleteSessionByToken(token string) error {
|
||||||
_, err := db.Connection.Exec("DELETE FROM sessions WHERE token = ?", token)
|
_, err := db.Connection.Exec("DELETE FROM sessions WHERE token = ?", token)
|
||||||
return err
|
return err
|
||||||
|
|||||||
21
go.mod
21
go.mod
@ -3,18 +3,25 @@ module playback-device-server
|
|||||||
go 1.24.1
|
go 1.24.1
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/gorilla/websocket v1.5.3
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
github.com/labstack/echo/v4 v4.13.3
|
github.com/labstack/echo/v4 v4.13.3 // indirect
|
||||||
github.com/labstack/gommon v0.4.2 // indirect
|
github.com/labstack/gommon v0.4.2 // indirect
|
||||||
github.com/matoous/go-nanoid v1.5.1
|
github.com/matoous/go-nanoid v1.5.1 // indirect
|
||||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
github.com/mattn/go-sqlite3 v1.14.24
|
github.com/mattn/go-sqlite3 v1.14.24 // indirect
|
||||||
github.com/rs/zerolog v1.33.0
|
github.com/pion/dtls/v3 v3.0.1 // indirect
|
||||||
|
github.com/pion/logging v0.2.2 // indirect
|
||||||
|
github.com/pion/randutil v0.1.0 // indirect
|
||||||
|
github.com/pion/stun/v3 v3.0.0 // indirect
|
||||||
|
github.com/pion/transport/v3 v3.0.7 // indirect
|
||||||
|
github.com/pion/turn/v4 v4.0.0 // indirect
|
||||||
|
github.com/rs/zerolog v1.33.0 // indirect
|
||||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||||
github.com/valyala/fasttemplate v1.2.2 // indirect
|
github.com/valyala/fasttemplate v1.2.2 // indirect
|
||||||
github.com/ziflex/lecho/v3 v3.7.0
|
github.com/wlynxg/anet v0.0.3 // indirect
|
||||||
golang.org/x/crypto v0.36.0
|
github.com/ziflex/lecho/v3 v3.7.0 // indirect
|
||||||
|
golang.org/x/crypto v0.36.0 // indirect
|
||||||
golang.org/x/net v0.33.0 // indirect
|
golang.org/x/net v0.33.0 // indirect
|
||||||
golang.org/x/sys v0.31.0 // indirect
|
golang.org/x/sys v0.31.0 // indirect
|
||||||
golang.org/x/text v0.23.0 // indirect
|
golang.org/x/text v0.23.0 // indirect
|
||||||
|
|||||||
16
go.sum
16
go.sum
@ -2,8 +2,6 @@ github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSV
|
|||||||
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
|
||||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
|
||||||
github.com/labstack/echo/v4 v4.13.3 h1:pwhpCPrTl5qry5HRdM5FwdXnhXSLSY+WE+YQSeCaafY=
|
github.com/labstack/echo/v4 v4.13.3 h1:pwhpCPrTl5qry5HRdM5FwdXnhXSLSY+WE+YQSeCaafY=
|
||||||
github.com/labstack/echo/v4 v4.13.3/go.mod h1:o90YNEeQWjDozo584l7AwhJMHN0bOC4tAfg+Xox9q5g=
|
github.com/labstack/echo/v4 v4.13.3/go.mod h1:o90YNEeQWjDozo584l7AwhJMHN0bOC4tAfg+Xox9q5g=
|
||||||
github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0=
|
github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0=
|
||||||
@ -19,6 +17,18 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE
|
|||||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM=
|
github.com/mattn/go-sqlite3 v1.14.24 h1:tpSp2G2KyMnnQu99ngJ47EIkWVmliIizyZBfPrBWDRM=
|
||||||
github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
github.com/mattn/go-sqlite3 v1.14.24/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||||
|
github.com/pion/dtls/v3 v3.0.1 h1:0kmoaPYLAo0md/VemjcrAXQiSf8U+tuU3nDYVNpEKaw=
|
||||||
|
github.com/pion/dtls/v3 v3.0.1/go.mod h1:dfIXcFkKoujDQ+jtd8M6RgqKK3DuaUilm3YatAbGp5k=
|
||||||
|
github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY=
|
||||||
|
github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms=
|
||||||
|
github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA=
|
||||||
|
github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8=
|
||||||
|
github.com/pion/stun/v3 v3.0.0 h1:4h1gwhWLWuZWOJIJR9s2ferRO+W3zA/b6ijOI6mKzUw=
|
||||||
|
github.com/pion/stun/v3 v3.0.0/go.mod h1:HvCN8txt8mwi4FBvS3EmDghW6aQJ24T+y+1TKjB5jyU=
|
||||||
|
github.com/pion/transport/v3 v3.0.7 h1:iRbMH05BzSNwhILHoBoAPxoB9xQgOaJk+591KC9P1o0=
|
||||||
|
github.com/pion/transport/v3 v3.0.7/go.mod h1:YleKiTZ4vqNxVwh77Z0zytYi7rXHl7j6uPLGhhz9rwo=
|
||||||
|
github.com/pion/turn/v4 v4.0.0 h1:qxplo3Rxa9Yg1xXDxxH8xaqcyGUtbHYw4QSCvmFWvhM=
|
||||||
|
github.com/pion/turn/v4 v4.0.0/go.mod h1:MuPDkm15nYSklKpN8vWJ9W2M0PlyQZqYt1McGuxG7mA=
|
||||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
||||||
github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8=
|
github.com/rs/zerolog v1.33.0 h1:1cU2KZkvPxNyfgEmhHAz/1A9Bz+llsdYzklWFzgp0r8=
|
||||||
@ -27,6 +37,8 @@ github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6Kllzaw
|
|||||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||||
github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
|
github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
|
||||||
github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
|
github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
|
||||||
|
github.com/wlynxg/anet v0.0.3 h1:PvR53psxFXstc12jelG6f1Lv4MWqE0tI76/hHGjh9rg=
|
||||||
|
github.com/wlynxg/anet v0.0.3/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA=
|
||||||
github.com/ziflex/lecho/v3 v3.7.0 h1:MSzYINEHtAaCx2XpbdF1A85aSyXitNJxF4T9dG6jzRQ=
|
github.com/ziflex/lecho/v3 v3.7.0 h1:MSzYINEHtAaCx2XpbdF1A85aSyXitNJxF4T9dG6jzRQ=
|
||||||
github.com/ziflex/lecho/v3 v3.7.0/go.mod h1:LBlLsyIwa0MFxtJ2WU5WzHfuMR/jnq26TXddWfJ+s/0=
|
github.com/ziflex/lecho/v3 v3.7.0/go.mod h1:LBlLsyIwa0MFxtJ2WU5WzHfuMR/jnq26TXddWfJ+s/0=
|
||||||
golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
|
golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34=
|
||||||
|
|||||||
41
main/main.go
41
main/main.go
@ -39,15 +39,6 @@ func main() {
|
|||||||
}
|
}
|
||||||
defer deviceDatabase.Close()
|
defer deviceDatabase.Close()
|
||||||
|
|
||||||
remoteDatabase := data.RemoteDatabase{}
|
|
||||||
remoteDatabase.SetDirectory(configuration.DatabaseDirectory)
|
|
||||||
err = remoteDatabase.Initialize()
|
|
||||||
if err != nil {
|
|
||||||
log.Error().Err(err).Msg("failed to initialize remote database")
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
defer remoteDatabase.Close()
|
|
||||||
|
|
||||||
userManager := management.UserManager{}
|
userManager := management.UserManager{}
|
||||||
err = userManager.Initialize(&userDatabase)
|
err = userManager.Initialize(&userDatabase)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -60,12 +51,6 @@ func main() {
|
|||||||
log.Error().Err(err).Msg("failed to initialize device manager")
|
log.Error().Err(err).Msg("failed to initialize device manager")
|
||||||
}
|
}
|
||||||
|
|
||||||
remoteManager := management.RemoteManager{}
|
|
||||||
err = remoteManager.Initialize(&remoteDatabase)
|
|
||||||
if err != nil {
|
|
||||||
log.Error().Err(err).Msg("failed to initialize remote manager")
|
|
||||||
}
|
|
||||||
|
|
||||||
webServer := server.WebServer{}
|
webServer := server.WebServer{}
|
||||||
webServer.SetWebAppDirectoryPath("www")
|
webServer.SetWebAppDirectoryPath("www")
|
||||||
webServer.SetPort(configuration.Port)
|
webServer.SetPort(configuration.Port)
|
||||||
@ -74,7 +59,6 @@ func main() {
|
|||||||
|
|
||||||
authenticator := server.Authenticator{}
|
authenticator := server.Authenticator{}
|
||||||
authenticator.SetUserManager(&userManager)
|
authenticator.SetUserManager(&userManager)
|
||||||
authenticator.SetDeviceManager(&deviceManager)
|
|
||||||
|
|
||||||
userApiHandler := server.UsersApiHandler{}
|
userApiHandler := server.UsersApiHandler{}
|
||||||
userApiHandler.SetUserManager(&userManager)
|
userApiHandler.SetUserManager(&userManager)
|
||||||
@ -86,17 +70,11 @@ func main() {
|
|||||||
deviceApiHandler.SetRouter(webServer.Router())
|
deviceApiHandler.SetRouter(webServer.Router())
|
||||||
deviceApiHandler.Initialize(&authenticator)
|
deviceApiHandler.Initialize(&authenticator)
|
||||||
|
|
||||||
remoteApiHandler := server.RemoteApiHandler{}
|
turnServer := server.TurnServer{}
|
||||||
remoteApiHandler.SetRemoteManager(&remoteManager)
|
turnServer.SetPublicIp("192.168.178.23")
|
||||||
remoteApiHandler.SetRouter(webServer.Router())
|
turnServer.SetPort(3478)
|
||||||
remoteApiHandler.Initialize(&authenticator)
|
turnServer.SetRealm("pbrts.net")
|
||||||
|
turnServer.AddUser("user", "password")
|
||||||
webSocketServer := server.WebsocketServer{}
|
|
||||||
webSocketServer.SetRouter(webServer.Router())
|
|
||||||
webSocketServer.Initialize(&authenticator)
|
|
||||||
|
|
||||||
webRTCWSHandler := server.WebRTCWSHandler{}
|
|
||||||
webSocketServer.AddHandler(&webRTCWSHandler)
|
|
||||||
|
|
||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
wg.Add(1)
|
wg.Add(1)
|
||||||
@ -108,6 +86,15 @@ func main() {
|
|||||||
log.Error().Err(err).Msg("failed to start web server")
|
log.Error().Err(err).Msg("failed to start web server")
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
log.Info().Msg("starting turn server")
|
||||||
|
err := turnServer.Start()
|
||||||
|
if err != nil {
|
||||||
|
log.Error().Err(err).Msg("failed to start turn server")
|
||||||
|
}
|
||||||
|
}()
|
||||||
wg.Wait()
|
wg.Wait()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -49,6 +49,14 @@ func (dm *DeviceManager) DeviceIdExists(id string) (bool, error) {
|
|||||||
// return token, nil
|
// return token, nil
|
||||||
//}
|
//}
|
||||||
|
|
||||||
|
func (dm *DeviceManager) GetSession(sessionToken string) (*d.DeviceSession, error) {
|
||||||
|
session, error := dm.deviceDatabase.GetSession(sessionToken)
|
||||||
|
if error != nil {
|
||||||
|
return nil, error
|
||||||
|
}
|
||||||
|
return session, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (dm *DeviceManager) GetDeviceById(id string) (*d.PlaybackDevice, error) {
|
func (dm *DeviceManager) GetDeviceById(id string) (*d.PlaybackDevice, error) {
|
||||||
device, error := dm.deviceDatabase.GetDeviceById(id)
|
device, error := dm.deviceDatabase.GetDeviceById(id)
|
||||||
if error != nil {
|
if error != nil {
|
||||||
@ -62,6 +70,14 @@ func (dm *DeviceManager) UpdateDevice(device *d.PlaybackDevice) error {
|
|||||||
return error
|
return error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (dm *DeviceManager) DeleteSession(token string) error {
|
||||||
|
error := dm.deviceDatabase.DeleteSessionByToken(token)
|
||||||
|
if error != nil {
|
||||||
|
return error
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (dm *DeviceManager) GetDevices() (*[]d.PlaybackDevice, error) {
|
func (dm *DeviceManager) GetDevices() (*[]d.PlaybackDevice, error) {
|
||||||
users, error := dm.deviceDatabase.GetDevices()
|
users, error := dm.deviceDatabase.GetDevices()
|
||||||
return users, error
|
return users, error
|
||||||
@ -125,14 +141,6 @@ func (dm *DeviceManager) GetIntegrations() ([]d.Integration, error) {
|
|||||||
return integrations, nil
|
return integrations, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (dm *DeviceManager) GetIntegrationByToken(token string) (*d.Integration, error) {
|
|
||||||
integration, err := dm.deviceDatabase.GetIntegrationByToken(token)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return integration, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (dm *DeviceManager) DeleteIntegration(id string) error {
|
func (dm *DeviceManager) DeleteIntegration(id string) error {
|
||||||
error := dm.deviceDatabase.DeleteIntegration(id)
|
error := dm.deviceDatabase.DeleteIntegration(id)
|
||||||
return error
|
return error
|
||||||
|
|||||||
@ -1,66 +0,0 @@
|
|||||||
package management
|
|
||||||
|
|
||||||
import (
|
|
||||||
d "playback-device-server/data"
|
|
||||||
)
|
|
||||||
|
|
||||||
type RemoteManager struct {
|
|
||||||
remoteDatabase *d.RemoteDatabase
|
|
||||||
}
|
|
||||||
|
|
||||||
func (rm *RemoteManager) Initialize(remoteDatabase *d.RemoteDatabase) error {
|
|
||||||
rm.remoteDatabase = remoteDatabase
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (rm *RemoteManager) CreateRemote(remote d.Remote) (string, error) {
|
|
||||||
return rm.remoteDatabase.CreateRemote(remote)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (rm *RemoteManager) GetRemote(remoteID string) (d.Remote, error) {
|
|
||||||
remote, err := rm.remoteDatabase.GetRemote(remoteID)
|
|
||||||
if err != nil {
|
|
||||||
return d.Remote{}, err
|
|
||||||
}
|
|
||||||
return remote, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (rm *RemoteManager) GetRemotes() ([]d.Remote, error) {
|
|
||||||
remotes, err := rm.remoteDatabase.GetRemotes()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return remotes, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (rm *RemoteManager) UpdateRemote(remote d.Remote) error {
|
|
||||||
return rm.remoteDatabase.UpdateRemote(remote)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (rm *RemoteManager) DeleteRemote(remoteID string) error {
|
|
||||||
return rm.remoteDatabase.DeleteRemote(remoteID)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (rm *RemoteManager) CreateCommand(command d.Command) (string, error) {
|
|
||||||
return rm.remoteDatabase.CreateCommand(command)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (rm *RemoteManager) CreateCommands(commands []d.Command) error {
|
|
||||||
return rm.remoteDatabase.CreateCommands(commands)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (rm *RemoteManager) GetCommands() ([]d.Command, error) {
|
|
||||||
return rm.remoteDatabase.GetCommands()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (rm *RemoteManager) UpdateCommand(command d.Command) error {
|
|
||||||
return rm.remoteDatabase.UpdateCommand(command)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (rm *RemoteManager) SetRemoteDatabase(remoteDatabase *d.RemoteDatabase) {
|
|
||||||
rm.remoteDatabase = remoteDatabase
|
|
||||||
}
|
|
||||||
|
|
||||||
func (rm *RemoteManager) DeleteCommand(commandID string) error {
|
|
||||||
return rm.remoteDatabase.DeleteCommand(commandID)
|
|
||||||
}
|
|
||||||
@ -12,24 +12,18 @@ import (
|
|||||||
|
|
||||||
type AuthContext struct {
|
type AuthContext struct {
|
||||||
echo.Context
|
echo.Context
|
||||||
User *d.User
|
User *d.User
|
||||||
Session *d.UserSession
|
Session *d.UserSession
|
||||||
Integration *d.Integration
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type Authenticator struct {
|
type Authenticator struct {
|
||||||
userManager *m.UserManager
|
userManager *m.UserManager
|
||||||
deviceManager *m.DeviceManager
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Authenticator) SetUserManager(userManager *m.UserManager) {
|
func (r *Authenticator) SetUserManager(userManager *m.UserManager) {
|
||||||
r.userManager = userManager
|
r.userManager = userManager
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Authenticator) SetDeviceManager(deviceManager *m.DeviceManager) {
|
|
||||||
r.deviceManager = deviceManager
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Authenticator) Authenticate(path string, exceptions []string) func(next echo.HandlerFunc) echo.HandlerFunc {
|
func (r *Authenticator) Authenticate(path string, exceptions []string) func(next echo.HandlerFunc) echo.HandlerFunc {
|
||||||
return func(next echo.HandlerFunc) echo.HandlerFunc {
|
return func(next echo.HandlerFunc) echo.HandlerFunc {
|
||||||
return func(context echo.Context) error {
|
return func(context echo.Context) error {
|
||||||
@ -44,57 +38,29 @@ func (r *Authenticator) Authenticate(path string, exceptions []string) func(next
|
|||||||
}
|
}
|
||||||
cookie, err := context.Cookie("token")
|
cookie, err := context.Cookie("token")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
SendError(401, context, "no cookie for session token found")
|
SendError(401, context, "no session token found")
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
token := cookie.Value
|
session, error := r.userManager.GetSession(cookie.Value)
|
||||||
user, session, error := r.getUserAndSession(token)
|
if error != nil || session == nil {
|
||||||
|
SendError(401, context, fmt.Sprintf("session not found: %s", cookie.Value))
|
||||||
|
return fmt.Errorf("session not found: %s", cookie.Value)
|
||||||
|
}
|
||||||
|
|
||||||
|
user, error := r.userManager.GetUserById(session.UserID)
|
||||||
if error != nil {
|
if error != nil {
|
||||||
log.Error().Err(error).Msg("error authenticating user")
|
log.Error().Err(error).Msg("error getting user by id")
|
||||||
SendError(500, context, fmt.Sprintf("error authenticating user: %s", error))
|
SendError(401, context, "no user found for given session")
|
||||||
return error
|
return error
|
||||||
}
|
}
|
||||||
|
if user == nil {
|
||||||
integration, error := r.getIntegration(token)
|
SendError(401, context, "no user found for given session")
|
||||||
if error != nil {
|
return fmt.Errorf("no user found for session '%s'", cookie.Value)
|
||||||
log.Error().Err(error).Msg("error getting integration")
|
|
||||||
SendError(500, context, fmt.Sprintf("error getting integration: %s", error))
|
|
||||||
return error
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if integration == nil && user == nil {
|
authContext := AuthContext{Context: context, User: user, Session: session}
|
||||||
log.Error().Msg("no integration or user found for given token")
|
|
||||||
SendError(401, context, "no integration or user found for given token")
|
|
||||||
return fmt.Errorf("no integration or user found for given token")
|
|
||||||
}
|
|
||||||
|
|
||||||
authContext := AuthContext{Context: context, User: user, Session: session, Integration: integration}
|
|
||||||
|
|
||||||
return next(authContext)
|
return next(authContext)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Authenticator) getUserAndSession(token string) (*d.User, *d.UserSession, error) {
|
|
||||||
session, error := r.userManager.GetSession(token)
|
|
||||||
if error != nil {
|
|
||||||
return nil, nil, error
|
|
||||||
}
|
|
||||||
if session == nil {
|
|
||||||
return nil, nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
user, error := r.userManager.GetUserById(session.UserID)
|
|
||||||
if error != nil {
|
|
||||||
return nil, nil, error
|
|
||||||
}
|
|
||||||
return user, session, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Authenticator) getIntegration(token string) (*d.Integration, error) {
|
|
||||||
integration, error := r.deviceManager.GetIntegrationByToken(token)
|
|
||||||
if error != nil {
|
|
||||||
return nil, error
|
|
||||||
}
|
|
||||||
return integration, nil
|
|
||||||
}
|
|
||||||
|
|||||||
@ -2,7 +2,6 @@ package server
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
|
||||||
d "playback-device-server/data"
|
d "playback-device-server/data"
|
||||||
m "playback-device-server/management"
|
m "playback-device-server/management"
|
||||||
|
|
||||||
@ -24,17 +23,15 @@ func (r *DeviceApiHandler) Initialize(authenticator *Authenticator) {
|
|||||||
devicesApi.GET("", r.handleGetDevices)
|
devicesApi.GET("", r.handleGetDevices)
|
||||||
devicesApi.POST("", r.handleCreateDevice)
|
devicesApi.POST("", r.handleCreateDevice)
|
||||||
devicesApi.DELETE("/:id", r.handleDeleteDevice)
|
devicesApi.DELETE("/:id", r.handleDeleteDevice)
|
||||||
|
|
||||||
r.router.Use(authenticator.Authenticate("/api/integrations", []string{"/api/integrations/register"}))
|
|
||||||
integrationsApi := r.router.Group("/api/integrations")
|
integrationsApi := r.router.Group("/api/integrations")
|
||||||
integrationsApi.POST("/register", r.handleIntegrationRegistration)
|
integrationsApi.GET("/register", r.handleIntegrationRegistration)
|
||||||
integrationsApi.POST("", r.handleCreateIntegration)
|
integrationsApi.POST("", r.handleCreateIntegration)
|
||||||
integrationsApi.GET("", r.handleGetIntegrations)
|
integrationsApi.GET("", r.handleGetIntegrations)
|
||||||
integrationsApi.GET("/:id", r.handleGetIntegration)
|
integrationsApi.GET("/:id", r.handleGetIntegration)
|
||||||
integrationsApi.DELETE("/:id", r.handleDeleteIntegration)
|
integrationsApi.DELETE("/:id", r.handleDeleteIntegration)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *DeviceApiHandler) handleCreateIntegration(context echo.Context) error {
|
func (r *DeviceApiHandler) handleIntegrationRegistration(context echo.Context) error {
|
||||||
code, error := r.deviceManager.GetRegistrationCode()
|
code, error := r.deviceManager.GetRegistrationCode()
|
||||||
|
|
||||||
if error != nil {
|
if error != nil {
|
||||||
@ -51,7 +48,7 @@ func (r *DeviceApiHandler) handleCreateIntegration(context echo.Context) error {
|
|||||||
return context.JSON(200, response)
|
return context.JSON(200, response)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *DeviceApiHandler) handleIntegrationRegistration(context echo.Context) error {
|
func (r *DeviceApiHandler) handleCreateIntegration(context echo.Context) error {
|
||||||
var data struct {
|
var data struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Code string `json:"code"`
|
Code string `json:"code"`
|
||||||
@ -80,16 +77,6 @@ func (r *DeviceApiHandler) handleIntegrationRegistration(context echo.Context) e
|
|||||||
Token: integration.Token,
|
Token: integration.Token,
|
||||||
}
|
}
|
||||||
|
|
||||||
thirdyDays := 30 * 24 * 60 * 60
|
|
||||||
cookie := &http.Cookie{
|
|
||||||
Name: "token",
|
|
||||||
Value: integration.Token,
|
|
||||||
Path: "/",
|
|
||||||
HttpOnly: true,
|
|
||||||
MaxAge: thirdyDays,
|
|
||||||
}
|
|
||||||
context.SetCookie(cookie)
|
|
||||||
|
|
||||||
return context.JSON(200, integrationData)
|
return context.JSON(200, integrationData)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,146 +0,0 @@
|
|||||||
package server
|
|
||||||
|
|
||||||
import (
|
|
||||||
d "playback-device-server/data"
|
|
||||||
m "playback-device-server/management"
|
|
||||||
|
|
||||||
"github.com/labstack/echo/v4"
|
|
||||||
"github.com/rs/zerolog/log"
|
|
||||||
)
|
|
||||||
|
|
||||||
type RemoteApiHandler struct {
|
|
||||||
router *echo.Echo
|
|
||||||
remoteManager *m.RemoteManager
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *RemoteApiHandler) Initialize(authenticator *Authenticator) {
|
|
||||||
r.router.Use(authenticator.Authenticate("/api/remotes", []string{}))
|
|
||||||
remotesApi := r.router.Group("/api/remotes")
|
|
||||||
remotesApi.GET("", r.handleGetRemotes)
|
|
||||||
remotesApi.GET("/:id", r.handleGetRemote)
|
|
||||||
remotesApi.POST("", r.handleCreateRemote)
|
|
||||||
remotesApi.PUT("/:id", r.handleUpdateRemote)
|
|
||||||
remotesApi.DELETE("/:id", r.handleDeleteRemote)
|
|
||||||
|
|
||||||
r.router.Use(authenticator.Authenticate("/api/commands", []string{}))
|
|
||||||
commandsApi := r.router.Group("/api/commands")
|
|
||||||
commandsApi.GET("", r.handleGetCommands)
|
|
||||||
commandsApi.POST("", r.handleCreateCommands)
|
|
||||||
commandsApi.PUT("/:id", r.handleUpdateCommand)
|
|
||||||
commandsApi.DELETE("/:id", r.handleDeleteCommand)
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *RemoteApiHandler) handleCreateRemote(context echo.Context) error {
|
|
||||||
remote := d.Remote{}
|
|
||||||
if err := context.Bind(&remote); err != nil {
|
|
||||||
SendError(400, context, err.Error())
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
id, err := r.remoteManager.CreateRemote(remote)
|
|
||||||
if err != nil {
|
|
||||||
SendError(500, context, err.Error())
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
remote.Id = id
|
|
||||||
return context.JSON(200, remote)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *RemoteApiHandler) handleGetRemote(context echo.Context) error {
|
|
||||||
id := context.Param("id")
|
|
||||||
remote, err := r.remoteManager.GetRemote(id)
|
|
||||||
if err != nil {
|
|
||||||
SendError(500, context, err.Error())
|
|
||||||
log.Error().Err(err)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return context.JSON(200, remote)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *RemoteApiHandler) handleGetRemotes(context echo.Context) error {
|
|
||||||
remotes, err := r.remoteManager.GetRemotes()
|
|
||||||
if err != nil {
|
|
||||||
SendError(500, context, err.Error())
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return context.JSON(200, remotes)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *RemoteApiHandler) handleUpdateRemote(context echo.Context) error {
|
|
||||||
remote := d.Remote{}
|
|
||||||
if err := context.Bind(&remote); err != nil {
|
|
||||||
SendError(400, context, err.Error())
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
err := r.remoteManager.UpdateRemote(remote)
|
|
||||||
if err != nil {
|
|
||||||
SendError(500, context, err.Error())
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return context.JSON(200, "")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *RemoteApiHandler) handleDeleteRemote(context echo.Context) error {
|
|
||||||
id := context.Param("id")
|
|
||||||
err := r.remoteManager.DeleteRemote(id)
|
|
||||||
if err != nil {
|
|
||||||
SendError(500, context, err.Error())
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return context.JSON(200, "")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *RemoteApiHandler) handleCreateCommands(context echo.Context) error {
|
|
||||||
commands := []d.Command{}
|
|
||||||
if err := context.Bind(&commands); err != nil {
|
|
||||||
SendError(400, context, err.Error())
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
err := r.remoteManager.CreateCommands(commands)
|
|
||||||
if err != nil {
|
|
||||||
SendError(500, context, err.Error())
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return context.JSON(200, "")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *RemoteApiHandler) handleGetCommands(context echo.Context) error {
|
|
||||||
commands, err := r.remoteManager.GetCommands()
|
|
||||||
if err != nil {
|
|
||||||
SendError(500, context, err.Error())
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return context.JSON(200, commands)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *RemoteApiHandler) handleUpdateCommand(context echo.Context) error {
|
|
||||||
command := d.Command{}
|
|
||||||
if err := context.Bind(&command); err != nil {
|
|
||||||
SendError(400, context, err.Error())
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
err := r.remoteManager.UpdateCommand(command)
|
|
||||||
if err != nil {
|
|
||||||
SendError(500, context, err.Error())
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return context.JSON(200, "")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *RemoteApiHandler) handleDeleteCommand(context echo.Context) error {
|
|
||||||
id := context.Param("id")
|
|
||||||
err := r.remoteManager.DeleteCommand(id)
|
|
||||||
if err != nil {
|
|
||||||
SendError(500, context, err.Error())
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return context.JSON(200, "")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *RemoteApiHandler) SetRouter(router *echo.Echo) {
|
|
||||||
r.router = router
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *RemoteApiHandler) SetRemoteManager(remoteManager *m.RemoteManager) {
|
|
||||||
r.remoteManager = remoteManager
|
|
||||||
}
|
|
||||||
92
server/turn_server.go
Normal file
92
server/turn_server.go
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
// SPDX-FileCopyrightText: 2023 The Pion community <https://pion.ly>
|
||||||
|
// SPDX-License-Identifier: MIT
|
||||||
|
|
||||||
|
// Package main implements a simple TURN server
|
||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"strconv"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
|
"github.com/pion/turn/v4"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TurnServer struct {
|
||||||
|
publicIp string
|
||||||
|
port int
|
||||||
|
users map[string][]byte
|
||||||
|
realm string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *TurnServer) Start() error {
|
||||||
|
// Create a UDP listener to pass into pion/turn
|
||||||
|
// pion/turn itself doesn't allocate any UDP sockets, but lets the user pass them in
|
||||||
|
// this allows us to add logging, storage or modify inbound/outbound traffic
|
||||||
|
udpListener, err := net.ListenPacket("udp4", "0.0.0.0:"+strconv.Itoa(t.port))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create TURN server listener: %s", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
server, err := turn.NewServer(turn.ServerConfig{
|
||||||
|
Realm: t.realm,
|
||||||
|
// Set AuthHandler callback
|
||||||
|
// This is called every time a user tries to authenticate with the TURN server
|
||||||
|
// Return the key for that user, or false when no user is found
|
||||||
|
AuthHandler: func(username string, realm string, srcAddr net.Addr) ([]byte, bool) { // nolint: revive
|
||||||
|
if key, ok := t.users[username]; ok {
|
||||||
|
return key, true
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil, false
|
||||||
|
},
|
||||||
|
// PacketConnConfigs is a list of UDP Listeners and the configuration around them
|
||||||
|
PacketConnConfigs: []turn.PacketConnConfig{
|
||||||
|
{
|
||||||
|
PacketConn: udpListener,
|
||||||
|
RelayAddressGenerator: &turn.RelayAddressGeneratorStatic{
|
||||||
|
// Claim that we are listening on IP passed by user (This should be your Public IP)
|
||||||
|
RelayAddress: net.ParseIP(t.publicIp),
|
||||||
|
// But actually be listening on every interface
|
||||||
|
Address: "0.0.0.0",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Block until user sends SIGINT or SIGTERM
|
||||||
|
sigs := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
|
||||||
|
<-sigs
|
||||||
|
|
||||||
|
if err = server.Close(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *TurnServer) AddUser(username string, password string) {
|
||||||
|
if t.users == nil {
|
||||||
|
t.users = make(map[string][]byte)
|
||||||
|
}
|
||||||
|
t.users[username] = turn.GenerateAuthKey(username, t.realm, password)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *TurnServer) SetPublicIp(ip string) {
|
||||||
|
t.publicIp = ip
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *TurnServer) SetPort(port int) {
|
||||||
|
t.port = port
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *TurnServer) SetRealm(realm string) {
|
||||||
|
t.realm = realm
|
||||||
|
}
|
||||||
@ -1,50 +0,0 @@
|
|||||||
package server
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
|
|
||||||
"github.com/gorilla/websocket"
|
|
||||||
"github.com/rs/zerolog/log"
|
|
||||||
)
|
|
||||||
|
|
||||||
const TYPE_SIGNALING = "signaling"
|
|
||||||
|
|
||||||
type WebRTCWSHandler struct {
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *WebRTCWSHandler) Handle(messageObject map[string]any, senderId string, sockets map[string]*websocket.Conn) {
|
|
||||||
if _, ok := messageObject["type"]; !ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
messageType := messageObject["type"]
|
|
||||||
switch messageType {
|
|
||||||
case TYPE_SIGNALING:
|
|
||||||
target, ok := messageObject["target"].(string)
|
|
||||||
if !ok {
|
|
||||||
log.Error().Msgf("Invalid target in signaling message")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
message := messageObject["message"]
|
|
||||||
h.handleSignaling(target, message, senderId, sockets)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (h *WebRTCWSHandler) handleSignaling(target string, message any, senderId string, sockets map[string]*websocket.Conn) {
|
|
||||||
ws, ok := sockets[target]
|
|
||||||
if !ok {
|
|
||||||
log.Error().Msgf("No connection found for target %s", target)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
byteArray, err := json.Marshal(map[string]any{"sender": senderId, "message": message})
|
|
||||||
if err != nil {
|
|
||||||
log.Error().Msgf("Error marshaling signaling message: %s", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
log.Info().Str("target", target).Str("sender", senderId).Msg("sending signaling message to target")
|
|
||||||
err = ws.WriteMessage(websocket.TextMessage, byteArray)
|
|
||||||
if err != nil {
|
|
||||||
log.Error().Msgf("Error writing signaling message to target: %s", err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
38
server/websocket.go
Normal file
38
server/websocket.go
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"playback-device-server/data"
|
||||||
|
|
||||||
|
"github.com/labstack/echo/v4"
|
||||||
|
"golang.org/x/net/websocket"
|
||||||
|
)
|
||||||
|
|
||||||
|
type WebSocketConnection struct {
|
||||||
|
connection *websocket.Conn
|
||||||
|
integration data.Integration
|
||||||
|
}
|
||||||
|
|
||||||
|
type WebSocket struct {
|
||||||
|
router *echo.Echo
|
||||||
|
connections []WebSocketConnection
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *WebSocket) Initializer() {
|
||||||
|
s.router.GET("/ws", s.handle)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *WebSocket) handle(context echo.Context) error {
|
||||||
|
websocket.Handler(func(ws *websocket.Conn) {
|
||||||
|
defer ws.Close()
|
||||||
|
s.connections = append(s.connections, WebSocketConnection{
|
||||||
|
connection: ws,
|
||||||
|
integration: data.Integration{},
|
||||||
|
})
|
||||||
|
}).ServeHTTP(context.Response(), context.Request())
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *WebSocket) SetRouter(router *echo.Echo) {
|
||||||
|
s.router = router
|
||||||
|
}
|
||||||
@ -1,79 +0,0 @@
|
|||||||
package server
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/gorilla/websocket"
|
|
||||||
"github.com/labstack/echo/v4"
|
|
||||||
)
|
|
||||||
|
|
||||||
var upgrader = websocket.Upgrader{}
|
|
||||||
|
|
||||||
type WebsocketHandler interface {
|
|
||||||
Handle(message map[string]any, senderId string, sockets map[string]*websocket.Conn)
|
|
||||||
}
|
|
||||||
|
|
||||||
type WebsocketServer struct {
|
|
||||||
router *echo.Echo
|
|
||||||
sockets map[string]*websocket.Conn
|
|
||||||
handlers []WebsocketHandler
|
|
||||||
socketsMutex *sync.Mutex
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *WebsocketServer) Initialize(authenticator *Authenticator) {
|
|
||||||
s.socketsMutex = &sync.Mutex{}
|
|
||||||
s.sockets = make(map[string]*websocket.Conn)
|
|
||||||
s.router.Use(authenticator.Authenticate("/ws", []string{}))
|
|
||||||
s.router.GET("/ws", s.handle)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *WebsocketServer) handle(context echo.Context) error {
|
|
||||||
authContext := context.(AuthContext)
|
|
||||||
senderId := getAuthenticatedId(authContext)
|
|
||||||
ws, err := upgrader.Upgrade(context.Response(), context.Request(), nil)
|
|
||||||
s.socketsMutex.Lock()
|
|
||||||
s.sockets[senderId] = ws
|
|
||||||
s.socketsMutex.Unlock()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
defer func() {
|
|
||||||
ws.Close()
|
|
||||||
s.socketsMutex.Lock()
|
|
||||||
delete(s.sockets, senderId)
|
|
||||||
s.socketsMutex.Unlock()
|
|
||||||
}()
|
|
||||||
|
|
||||||
for {
|
|
||||||
messageType, messageBytes, err := ws.ReadMessage()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if messageType == websocket.TextMessage {
|
|
||||||
var messageObject map[string]any
|
|
||||||
json.Unmarshal(messageBytes, &messageObject)
|
|
||||||
for _, handler := range s.handlers {
|
|
||||||
handler.Handle(messageObject, senderId, s.sockets)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func getAuthenticatedId(authContext AuthContext) string {
|
|
||||||
if authContext.User != nil {
|
|
||||||
return authContext.User.ID
|
|
||||||
}
|
|
||||||
if authContext.Integration != nil {
|
|
||||||
return authContext.Integration.ID
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *WebsocketServer) AddHandler(handler WebsocketHandler) {
|
|
||||||
s.handlers = append(s.handlers, handler)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *WebsocketServer) SetRouter(router *echo.Echo) {
|
|
||||||
s.router = router
|
|
||||||
}
|
|
||||||
@ -14,9 +14,6 @@
|
|||||||
href="./src/lib/bootstrap-icons-1.11.3/font/bootstrap-icons.css"
|
href="./src/lib/bootstrap-icons-1.11.3/font/bootstrap-icons.css"
|
||||||
rel="stylesheet"
|
rel="stylesheet"
|
||||||
/>
|
/>
|
||||||
<script src="./lib/papaparse-5.5.2.min.js"></script>
|
|
||||||
<script src="./lib/fusejs-7.1.0.min.js"></script>
|
|
||||||
<script src="./lib/popper.min.js"></script>
|
|
||||||
<script src="./lib/bootstrap-5.3.3-dist/js/bootstrap.bundle.min.js"></script>
|
<script src="./lib/bootstrap-5.3.3-dist/js/bootstrap.bundle.min.js"></script>
|
||||||
<title>Playback Device Server</title>
|
<title>Playback Device Server</title>
|
||||||
</head>
|
</head>
|
||||||
|
|||||||
@ -1,183 +0,0 @@
|
|||||||
import { createMemo, createSignal, mergeProps } from "solid-js";
|
|
||||||
|
|
||||||
function ListManager(props) {
|
|
||||||
props = mergeProps(
|
|
||||||
{
|
|
||||||
items: [],
|
|
||||||
availableItems: [],
|
|
||||||
style: "",
|
|
||||||
itemsTitle: "Selected items",
|
|
||||||
availableItemsTitle: "Available items",
|
|
||||||
itemToString: () => "",
|
|
||||||
onItemSelect: () => {},
|
|
||||||
onItemDeselect: () => {},
|
|
||||||
itemsEqual: (a, b) => a === b,
|
|
||||||
},
|
|
||||||
props
|
|
||||||
);
|
|
||||||
|
|
||||||
const itemToString = (item) => props.itemToString(item);
|
|
||||||
const byLabel = (a, b) => itemToString(a).localeCompare(itemToString(b));
|
|
||||||
const [selectedAvailableItemIndex, setSelectedAvailableItemIndex] =
|
|
||||||
createSignal(-1);
|
|
||||||
const [selectedItemIndex, setSelectedItemIndex] = createSignal(-1);
|
|
||||||
const [itemsSearchString, setItemsSearchString] = createSignal("");
|
|
||||||
const [availableItemsSearchString, setAvailableItemsSearchString] =
|
|
||||||
createSignal("");
|
|
||||||
|
|
||||||
const itemsFuse = createMemo(
|
|
||||||
() =>
|
|
||||||
new Fuse(props.items, {
|
|
||||||
keys: [{ name: "label", getFn: (item) => props.itemToString(item) }],
|
|
||||||
})
|
|
||||||
);
|
|
||||||
const availableItemsFuse = createMemo(
|
|
||||||
() =>
|
|
||||||
new Fuse(props.availableItems, {
|
|
||||||
keys: [{ name: "label", getFn: (item) => props.itemToString(item) }],
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
const selectableAvailableItems = createMemo(() =>
|
|
||||||
(availableItemsSearchString()
|
|
||||||
? availableItemsFuse()
|
|
||||||
.search(availableItemsSearchString())
|
|
||||||
.map((item) => item.item)
|
|
||||||
: props.availableItems
|
|
||||||
).filter(
|
|
||||||
(availableItem) =>
|
|
||||||
!props.items.find((item) => props.itemsEqual(item, availableItem))
|
|
||||||
)
|
|
||||||
);
|
|
||||||
const selectableItems = createMemo(() =>
|
|
||||||
itemsSearchString()
|
|
||||||
? itemsFuse()
|
|
||||||
.search(itemsSearchString())
|
|
||||||
.map((item) => item.item)
|
|
||||||
: props.items
|
|
||||||
);
|
|
||||||
const canSelect = createMemo(
|
|
||||||
() =>
|
|
||||||
selectedAvailableItemIndex() >= 0 &&
|
|
||||||
selectedAvailableItemIndex() < selectableAvailableItems().length
|
|
||||||
);
|
|
||||||
const canDeselect = createMemo(
|
|
||||||
() =>
|
|
||||||
selectedItemIndex() >= 0 && selectedItemIndex() < selectableItems().length
|
|
||||||
);
|
|
||||||
|
|
||||||
function handleSelectItem() {
|
|
||||||
let itemIndex = selectedAvailableItemIndex();
|
|
||||||
let item = selectableAvailableItems()[itemIndex];
|
|
||||||
if (itemIndex === selectableAvailableItems().length - 1) {
|
|
||||||
setSelectedAvailableItemIndex(itemIndex - 1);
|
|
||||||
}
|
|
||||||
props.onItemSelect(item);
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleDeselectItem() {
|
|
||||||
let itemIndex = selectedItemIndex();
|
|
||||||
let item = selectableItems()[itemIndex];
|
|
||||||
if (itemIndex === selectableItems().length - 1) {
|
|
||||||
setSelectedItemIndex(itemIndex - 1);
|
|
||||||
}
|
|
||||||
props.onItemDeselect(item);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ListItem(props) {
|
|
||||||
props = mergeProps(
|
|
||||||
{ children: "", selected: false, onClick: () => {} },
|
|
||||||
props
|
|
||||||
);
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
class={"px-2" + (props.selected ? " bg-secondary" : "")}
|
|
||||||
role="button"
|
|
||||||
onClick={props.onClick}
|
|
||||||
>
|
|
||||||
{props.children}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ItemList(props) {
|
|
||||||
props = mergeProps(
|
|
||||||
{
|
|
||||||
items: [],
|
|
||||||
onItemSelected: () => {},
|
|
||||||
selectedItemIndex: -1,
|
|
||||||
onSearchStringChange: () => {},
|
|
||||||
},
|
|
||||||
props
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div class="input-group input-group-sm">
|
|
||||||
<span
|
|
||||||
class="input-group-text border-bottom-0 rounded-bottom-0"
|
|
||||||
id="basic-addon1"
|
|
||||||
>
|
|
||||||
<i class="bi bi-search"></i>
|
|
||||||
</span>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
class="form-control form-control-sm border-bottom-0 rounded-bottom-0"
|
|
||||||
onInput={(event) => props.onSearchStringChange(event.target.value)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="rounded rounded-top-0 border bg-body flex-fill overflow-y-scroll">
|
|
||||||
{props.items.sort(byLabel).map((item, index) => (
|
|
||||||
<ListItem
|
|
||||||
onClick={() => props.onItemSelected(index)}
|
|
||||||
selected={index === props.selectedItemIndex}
|
|
||||||
>
|
|
||||||
{itemToString(item)}
|
|
||||||
</ListItem>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div class={"d-flex"} style={props.style}>
|
|
||||||
<div class="flex-shrink-1 w-50 d-flex flex-column">
|
|
||||||
<div class="px-2">{props.itemsTitle}</div>
|
|
||||||
<ItemList
|
|
||||||
items={selectableItems()}
|
|
||||||
onItemSelected={(index) => setSelectedItemIndex(index)}
|
|
||||||
selectedItemIndex={selectedItemIndex()}
|
|
||||||
onSearchStringChange={(value) => setItemsSearchString(value)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="p-2 d-flex flex-column justify-content-center align-items-center">
|
|
||||||
<button
|
|
||||||
class="btn btn-sm btn-outline-secondary mb-2"
|
|
||||||
onClick={handleSelectItem}
|
|
||||||
disabled={!canSelect()}
|
|
||||||
>
|
|
||||||
<i class="bi bi-caret-left-fill"></i>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="btn btn-sm btn-outline-secondary"
|
|
||||||
onClick={handleDeselectItem}
|
|
||||||
disabled={!canDeselect()}
|
|
||||||
>
|
|
||||||
<i class="bi bi-caret-right-fill"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div class="flex-shrink-1 w-50 d-flex flex-column">
|
|
||||||
<div class="px-2">{props.availableItemsTitle}</div>
|
|
||||||
<ItemList
|
|
||||||
items={selectableAvailableItems()}
|
|
||||||
onItemSelected={(index) => setSelectedAvailableItemIndex(index)}
|
|
||||||
selectedItemIndex={selectedAvailableItemIndex()}
|
|
||||||
onSearchStringChange={(value) => setAvailableItemsSearchString(value)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ListManager;
|
|
||||||
@ -1,180 +0,0 @@
|
|||||||
import { createEffect, createMemo, mergeProps } from "solid-js";
|
|
||||||
import Remote from "../data/remote";
|
|
||||||
import Command from "../data/command";
|
|
||||||
|
|
||||||
function RemoteControl(props) {
|
|
||||||
props = mergeProps(
|
|
||||||
{
|
|
||||||
remote: new Remote(),
|
|
||||||
onCommand: () => {},
|
|
||||||
},
|
|
||||||
props
|
|
||||||
);
|
|
||||||
|
|
||||||
const BUTTON_SIZE_REGULAR = "regular";
|
|
||||||
const BUTTON_SIZE_SMALL = "small";
|
|
||||||
|
|
||||||
const BUTTON_HEIGHT = 2;
|
|
||||||
const BUTTON_WIDTH = BUTTON_HEIGHT * 1.4;
|
|
||||||
const SMALL_BUTTON_HEIGHT = 1.5;
|
|
||||||
const SMALL_BUTTON_WIDTH = SMALL_BUTTON_HEIGHT * 1.3334;
|
|
||||||
const TYPES = Command.TYPES;
|
|
||||||
|
|
||||||
const commandMap = createMemo(() =>
|
|
||||||
props.remote
|
|
||||||
.getCommands()
|
|
||||||
.reduce(
|
|
||||||
(map, command) => ({ ...map, [command.getCommandType()]: command }),
|
|
||||||
{}
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
let layout = [
|
|
||||||
[TYPES.POWER, null, TYPES.INPUT],
|
|
||||||
[TYPES.ONE, TYPES.TWO, TYPES.THREE],
|
|
||||||
[TYPES.FOUR, TYPES.FIVE, TYPES.SIX],
|
|
||||||
[TYPES.SEVEN, TYPES.EIGHT, TYPES.NINE],
|
|
||||||
[TYPES.VOLUME_UP, TYPES.ZERO, TYPES.CHANNEL_UP],
|
|
||||||
[TYPES.VOLUME_DOWN, TYPES.MUTE, TYPES.CHANNEL_DOWN],
|
|
||||||
[TYPES.MENU, TYPES.HOME, TYPES.SETTINGS],
|
|
||||||
[TYPES.OPTIONS, TYPES.UP, TYPES.INFO],
|
|
||||||
[TYPES.LEFT, TYPES.ENTER, TYPES.RIGHT],
|
|
||||||
[TYPES.RETURN, TYPES.DOWN, TYPES.EXIT],
|
|
||||||
[TYPES.RED, TYPES.GREEN, TYPES.YELLOW, TYPES.BLUE],
|
|
||||||
[TYPES.PLAY, TYPES.PAUSE, TYPES.STOP],
|
|
||||||
[TYPES.REWIND, null, TYPES.FORWARD],
|
|
||||||
];
|
|
||||||
|
|
||||||
function toButtonProps(type) {
|
|
||||||
let mapping = {
|
|
||||||
[TYPES.POWER]: () => ({ icon: "bi-power", text: "" }),
|
|
||||||
[TYPES.INPUT]: () => ({ icon: "bi-box-arrow-in-right", text: "" }),
|
|
||||||
[TYPES.VOLUME_UP]: () => ({ icon: "bi-volume-up", text: "" }),
|
|
||||||
[TYPES.VOLUME_DOWN]: () => ({ icon: "bi-volume-down", text: "" }),
|
|
||||||
[TYPES.MUTE]: () => ({ icon: "bi-volume-mute", text: "" }),
|
|
||||||
[TYPES.CHANNEL_UP]: () => ({ icon: "bi-chevron-up", text: "" }),
|
|
||||||
[TYPES.CHANNEL_DOWN]: () => ({ icon: "bi-chevron-down", text: "" }),
|
|
||||||
[TYPES.HOME]: () => ({ icon: "bi-house-door", text: "" }),
|
|
||||||
[TYPES.SETTINGS]: () => ({ icon: "bi-gear", text: "" }),
|
|
||||||
[TYPES.INFO]: () => ({ icon: "bi-info-circle", text: "" }),
|
|
||||||
[TYPES.UP]: () => ({ icon: "bi-arrow-up", text: "" }),
|
|
||||||
[TYPES.DOWN]: () => ({ icon: "bi-arrow-down", text: "" }),
|
|
||||||
[TYPES.LEFT]: () => ({ icon: "bi-arrow-left", text: "" }),
|
|
||||||
[TYPES.RIGHT]: () => ({ icon: "bi-arrow-right", text: "" }),
|
|
||||||
[TYPES.ENTER]: () => ({ icon: "", text: "OK" }),
|
|
||||||
[TYPES.EXIT]: () => ({ icon: "", text: "EXIT" }),
|
|
||||||
[TYPES.OPTIONS]: () => ({ icon: "bi-list-task", text: "" }),
|
|
||||||
[TYPES.RETURN]: () => ({ icon: "bi-arrow-return-left", text: "" }),
|
|
||||||
[TYPES.ONE]: () => ({ icon: "", text: "1" }),
|
|
||||||
[TYPES.TWO]: () => ({ icon: "", text: "2" }),
|
|
||||||
[TYPES.THREE]: () => ({ icon: "", text: "3" }),
|
|
||||||
[TYPES.FOUR]: () => ({ icon: "", text: "4" }),
|
|
||||||
[TYPES.FIVE]: () => ({ icon: "", text: "5" }),
|
|
||||||
[TYPES.SIX]: () => ({ icon: "", text: "6" }),
|
|
||||||
[TYPES.SEVEN]: () => ({ icon: "", text: "7" }),
|
|
||||||
[TYPES.EIGHT]: () => ({ icon: "", text: "8" }),
|
|
||||||
[TYPES.NINE]: () => ({ icon: "", text: "9" }),
|
|
||||||
[TYPES.ZERO]: () => ({ icon: "", text: "0" }),
|
|
||||||
[TYPES.MENU]: () => ({ icon: "", text: "MENU" }),
|
|
||||||
[TYPES.PLAY]: () => ({ icon: "bi-play", text: "" }),
|
|
||||||
[TYPES.PAUSE]: () => ({ icon: "bi-pause", text: "" }),
|
|
||||||
[TYPES.STOP]: () => ({ icon: "bi-stop", text: "" }),
|
|
||||||
[TYPES.REWIND]: () => ({ icon: "bi-rewind", text: "" }),
|
|
||||||
[TYPES.FORWARD]: () => ({ icon: "bi-fast-forward", text: "" }),
|
|
||||||
[TYPES.RED]: () => ({
|
|
||||||
class: "bg-danger",
|
|
||||||
buttonSize: BUTTON_SIZE_SMALL,
|
|
||||||
}),
|
|
||||||
[TYPES.GREEN]: () => ({
|
|
||||||
class: "bg-success",
|
|
||||||
buttonSize: BUTTON_SIZE_SMALL,
|
|
||||||
}),
|
|
||||||
[TYPES.YELLOW]: () => ({
|
|
||||||
class: "bg-warning",
|
|
||||||
buttonSize: BUTTON_SIZE_SMALL,
|
|
||||||
}),
|
|
||||||
[TYPES.BLUE]: () => ({
|
|
||||||
class: "bg-primary",
|
|
||||||
buttonSize: BUTTON_SIZE_SMALL,
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!(type in mapping)) return {};
|
|
||||||
let props = mapping[type]();
|
|
||||||
props.type = type;
|
|
||||||
props.text = props.text || "";
|
|
||||||
return props;
|
|
||||||
}
|
|
||||||
|
|
||||||
function PlaceholderButton() {
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
style={`width: ${BUTTON_WIDTH}em; height: ${BUTTON_HEIGHT}em; margin: 0.2em;`}
|
|
||||||
></div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function RemoteButton(props) {
|
|
||||||
props = mergeProps(
|
|
||||||
{
|
|
||||||
onClick: () => {},
|
|
||||||
disabled: false,
|
|
||||||
class: "",
|
|
||||||
buttonSize: BUTTON_SIZE_REGULAR,
|
|
||||||
icon: "",
|
|
||||||
text: "",
|
|
||||||
},
|
|
||||||
props
|
|
||||||
);
|
|
||||||
|
|
||||||
let buttonWidth = BUTTON_WIDTH;
|
|
||||||
let buttonHeight = BUTTON_HEIGHT;
|
|
||||||
if (props.buttonSize === BUTTON_SIZE_SMALL) {
|
|
||||||
buttonWidth = SMALL_BUTTON_WIDTH;
|
|
||||||
buttonHeight = SMALL_BUTTON_HEIGHT;
|
|
||||||
}
|
|
||||||
let buttonFontSize = buttonHeight * 0.4;
|
|
||||||
let iconSize = buttonHeight * 0.5;
|
|
||||||
return (
|
|
||||||
<button
|
|
||||||
class={
|
|
||||||
"btn btn-dark d-flex justify-content-center align-items-center " +
|
|
||||||
props.class
|
|
||||||
}
|
|
||||||
style={`width: ${buttonWidth}em; height: ${buttonHeight}em; margin: 0.2em;`}
|
|
||||||
onClick={props.onClick}
|
|
||||||
disabled={props.disabled}
|
|
||||||
>
|
|
||||||
<i style={`font-size: ${iconSize}em;`} class={"bi " + props.icon} />
|
|
||||||
<span style={`font-size: ${buttonFontSize}em;`}>{props.text}</span>
|
|
||||||
</button>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div class="d-flex flex-column justify-content-center align-items-center p-1 bg-secondary rounded">
|
|
||||||
{layout.map((row) => (
|
|
||||||
<div class="d-flex flex-row justify-content-center align-items-center">
|
|
||||||
{row
|
|
||||||
.map(toButtonProps)
|
|
||||||
.map(({ type, class: className, buttonSize, icon, text }) =>
|
|
||||||
!type ? (
|
|
||||||
<PlaceholderButton />
|
|
||||||
) : (
|
|
||||||
<RemoteButton
|
|
||||||
class={className}
|
|
||||||
buttonSize={buttonSize}
|
|
||||||
onClick={() => props.onCommand(commandMap()[type])}
|
|
||||||
disabled={!commandMap()[type]}
|
|
||||||
icon={icon}
|
|
||||||
text={text}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default RemoteControl;
|
|
||||||
@ -1,231 +0,0 @@
|
|||||||
function Command({
|
|
||||||
id = "",
|
|
||||||
protocol = "",
|
|
||||||
commandNumber = -1,
|
|
||||||
device = -1,
|
|
||||||
commandType = "",
|
|
||||||
title = "",
|
|
||||||
} = {}) {
|
|
||||||
if (typeof commandNumber !== "number")
|
|
||||||
throw new Error("Command number must be a number");
|
|
||||||
if (typeof device !== "number") throw new Error("Device must be a number");
|
|
||||||
let _id = id;
|
|
||||||
let _protocol = protocol;
|
|
||||||
let _commandNumber = commandNumber;
|
|
||||||
let _device = device;
|
|
||||||
let _commandType = commandType;
|
|
||||||
let _title = title;
|
|
||||||
|
|
||||||
function getId() {
|
|
||||||
return _id;
|
|
||||||
}
|
|
||||||
|
|
||||||
function setId(id) {
|
|
||||||
_id = id;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getProtocol() {
|
|
||||||
return _protocol;
|
|
||||||
}
|
|
||||||
|
|
||||||
function setProtocol(protocol) {
|
|
||||||
_protocol = protocol;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getCommandNumber() {
|
|
||||||
return _commandNumber;
|
|
||||||
}
|
|
||||||
|
|
||||||
function setCommandNumber(commandNumber) {
|
|
||||||
if (typeof commandNumber !== "number")
|
|
||||||
throw new Error("Command number must be a number");
|
|
||||||
_commandNumber = commandNumber;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getDevice() {
|
|
||||||
return _device;
|
|
||||||
}
|
|
||||||
|
|
||||||
function setDevice(device) {
|
|
||||||
if (typeof device !== "number") throw new Error("Device must be a number");
|
|
||||||
_device = device;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getCommandType() {
|
|
||||||
return _commandType;
|
|
||||||
}
|
|
||||||
|
|
||||||
function setCommandType(commandType) {
|
|
||||||
_commandType = commandType;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getTitle() {
|
|
||||||
if (_title) return _title;
|
|
||||||
return `${Command.getTypeString(_commandType)} (${Command.getProtocolString(_protocol)})`;
|
|
||||||
}
|
|
||||||
|
|
||||||
function setTitle(title) {
|
|
||||||
_title = title;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
getId,
|
|
||||||
setId,
|
|
||||||
getProtocol,
|
|
||||||
setProtocol,
|
|
||||||
getCommandNumber,
|
|
||||||
setCommandNumber,
|
|
||||||
getDevice,
|
|
||||||
setDevice,
|
|
||||||
getCommandType,
|
|
||||||
setCommandType,
|
|
||||||
getTitle,
|
|
||||||
setTitle,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
Command.PROTOCOLS = {
|
|
||||||
SAMSUNG: "samsung",
|
|
||||||
NEC: "nec",
|
|
||||||
ONKYO: "onkyo",
|
|
||||||
APPLE: "apple",
|
|
||||||
DENON: "denon",
|
|
||||||
SHARP: "sharp",
|
|
||||||
PANASONIC: "panasonic",
|
|
||||||
KASEIKYO: "kaseikyo",
|
|
||||||
JVC: "jvc",
|
|
||||||
LG: "lg",
|
|
||||||
SONY: "sony",
|
|
||||||
RC5: "rc5",
|
|
||||||
RC6: "rc6",
|
|
||||||
UNIVERSAL_PULSE_DISTANCE: "universal_pulse_distance",
|
|
||||||
UNIVERSAL_PULSE_WIDTH: "universal_pulse_width",
|
|
||||||
UNIVERSAL_PULSE_DISTANCE_WIDTH: "universal_pulse_distance_width",
|
|
||||||
HASH: "hash",
|
|
||||||
PRONTO: "pronto",
|
|
||||||
BOSE_WAVE: "bose_wave",
|
|
||||||
BANG_OLUFSEN: "bang_olufsen",
|
|
||||||
LEGO: "lego",
|
|
||||||
FAST: "fast",
|
|
||||||
WHYNTER: "whynter",
|
|
||||||
MAGIQUEST: "magiquest",
|
|
||||||
}
|
|
||||||
|
|
||||||
Command.getProtocolString = (protocol) => {
|
|
||||||
let mapping = {
|
|
||||||
[Command.PROTOCOLS.SAMSUNG]: "Samsung",
|
|
||||||
[Command.PROTOCOLS.NEC]: "NEC",
|
|
||||||
[Command.PROTOCOLS.ONKYO]: "Onkyo",
|
|
||||||
[Command.PROTOCOLS.APPLE]: "Apple",
|
|
||||||
[Command.PROTOCOLS.DENON]: "Denon",
|
|
||||||
[Command.PROTOCOLS.SHARP]: "Sharp",
|
|
||||||
[Command.PROTOCOLS.PANASONIC]: "Panasonic",
|
|
||||||
[Command.PROTOCOLS.KASEIKYO]: "Kaseikyo",
|
|
||||||
[Command.PROTOCOLS.JVC]: "JVC",
|
|
||||||
[Command.PROTOCOLS.LG]: "LG",
|
|
||||||
[Command.PROTOCOLS.SONY]: "Sony",
|
|
||||||
[Command.PROTOCOLS.RC5]: "RC5",
|
|
||||||
[Command.PROTOCOLS.RC6]: "RC6",
|
|
||||||
[Command.PROTOCOLS.UNIVERSAL_PULSE_DISTANCE]: "Universal Pulse Distance",
|
|
||||||
[Command.PROTOCOLS.UNIVERSAL_PULSE_WIDTH]: "Universal Pulse Width",
|
|
||||||
[Command.PROTOCOLS.UNIVERSAL_PULSE_DISTANCE_WIDTH]: "Universal Pulse Distance Width",
|
|
||||||
[Command.PROTOCOLS.HASH]: "Hash",
|
|
||||||
[Command.PROTOCOLS.PRONTO]: "Pronto",
|
|
||||||
[Command.PROTOCOLS.BOSE_WAVE]: "BoseWave",
|
|
||||||
[Command.PROTOCOLS.BANG_OLUFSEN]: "Bang & Olufsen",
|
|
||||||
[Command.PROTOCOLS.LEGO]: "Lego",
|
|
||||||
[Command.PROTOCOLS.FAST]: "FAST",
|
|
||||||
[Command.PROTOCOLS.WHYNTER]: "Whynter",
|
|
||||||
[Command.PROTOCOLS.MAGIQUEST]: "MagiQuest",
|
|
||||||
}
|
|
||||||
return mapping[protocol]
|
|
||||||
}
|
|
||||||
|
|
||||||
Command.TYPES = {
|
|
||||||
POWER: "power",
|
|
||||||
INPUT: "input",
|
|
||||||
ONE: "one",
|
|
||||||
TWO: "two",
|
|
||||||
THREE: "three",
|
|
||||||
FOUR: "four",
|
|
||||||
FIVE: "five",
|
|
||||||
SIX: "six",
|
|
||||||
SEVEN: "seven",
|
|
||||||
EIGHT: "eight",
|
|
||||||
NINE: "nine",
|
|
||||||
ZERO: "zero",
|
|
||||||
VOLUME_UP: "volume_up",
|
|
||||||
VOLUME_DOWN: "volume_down",
|
|
||||||
MUTE: "mute",
|
|
||||||
CHANNEL_UP: "channel_up",
|
|
||||||
CHANNEL_DOWN: "channel_down",
|
|
||||||
MENU: "menu",
|
|
||||||
HOME: "home",
|
|
||||||
SETTINGS: "settings",
|
|
||||||
OPTIONS: "options",
|
|
||||||
UP: "up",
|
|
||||||
DOWN: "down",
|
|
||||||
LEFT: "left",
|
|
||||||
RIGHT: "right",
|
|
||||||
ENTER: "enter",
|
|
||||||
INFO: "info",
|
|
||||||
RETURN: "return",
|
|
||||||
EXIT: "exit",
|
|
||||||
RED: "red",
|
|
||||||
GREEN: "green",
|
|
||||||
YELLOW: "yellow",
|
|
||||||
BLUE: "blue",
|
|
||||||
REWIND: "rewind",
|
|
||||||
PLAY: "play",
|
|
||||||
PAUSE: "pause",
|
|
||||||
STOP: "stop",
|
|
||||||
FORWARD: "forward",
|
|
||||||
OTHER: "other",
|
|
||||||
}
|
|
||||||
|
|
||||||
Command.getTypeString = (type) => {
|
|
||||||
let mapping = {
|
|
||||||
[Command.TYPES.POWER]: "Power",
|
|
||||||
[Command.TYPES.INPUT]: "Input",
|
|
||||||
[Command.TYPES.ONE]: "1",
|
|
||||||
[Command.TYPES.TWO]: "2",
|
|
||||||
[Command.TYPES.THREE]: "3",
|
|
||||||
[Command.TYPES.FOUR]: "4",
|
|
||||||
[Command.TYPES.FIVE]: "5",
|
|
||||||
[Command.TYPES.SIX]: "6",
|
|
||||||
[Command.TYPES.SEVEN]: "7",
|
|
||||||
[Command.TYPES.EIGHT]: "8",
|
|
||||||
[Command.TYPES.NINE]: "9",
|
|
||||||
[Command.TYPES.ZERO]: "0",
|
|
||||||
[Command.TYPES.VOLUME_UP]: "Volume Up",
|
|
||||||
[Command.TYPES.VOLUME_DOWN]: "Volume Down",
|
|
||||||
[Command.TYPES.MUTE]: "Mute",
|
|
||||||
[Command.TYPES.CHANNEL_UP]: "Channel Up",
|
|
||||||
[Command.TYPES.CHANNEL_DOWN]: "Channel Down",
|
|
||||||
[Command.TYPES.MENU]: "Menu",
|
|
||||||
[Command.TYPES.HOME]: "Home",
|
|
||||||
[Command.TYPES.SETTINGS]: "Settings",
|
|
||||||
[Command.TYPES.OPTIONS]: "Options",
|
|
||||||
[Command.TYPES.UP]: "Up",
|
|
||||||
[Command.TYPES.DOWN]: "Down",
|
|
||||||
[Command.TYPES.LEFT]: "Left",
|
|
||||||
[Command.TYPES.RIGHT]: "Right",
|
|
||||||
[Command.TYPES.ENTER]: "Enter",
|
|
||||||
[Command.TYPES.INFO]: "Info",
|
|
||||||
[Command.TYPES.RETURN]: "Return",
|
|
||||||
[Command.TYPES.EXIT]: "Exit",
|
|
||||||
[Command.TYPES.RED]: "Red",
|
|
||||||
[Command.TYPES.GREEN]: "Green",
|
|
||||||
[Command.TYPES.YELLOW]: "Yellow",
|
|
||||||
[Command.TYPES.BLUE]: "Blue",
|
|
||||||
[Command.TYPES.REWIND]: "Rewind",
|
|
||||||
[Command.TYPES.PLAY]: "Play",
|
|
||||||
[Command.TYPES.PAUSE]: "Pause",
|
|
||||||
[Command.TYPES.STOP]: "Stop",
|
|
||||||
[Command.TYPES.FORWARD]: "Forward",
|
|
||||||
[Command.TYPES.OTHER]: "Other",
|
|
||||||
}
|
|
||||||
return mapping[type]
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Command;
|
|
||||||
@ -1,40 +0,0 @@
|
|||||||
function Remote({ id = "", title = "", commands = [] } = {}) {
|
|
||||||
let _id = id;
|
|
||||||
let _title = title;
|
|
||||||
let _commands = commands;
|
|
||||||
|
|
||||||
function getId() {
|
|
||||||
return _id;
|
|
||||||
}
|
|
||||||
|
|
||||||
function setId(id) {
|
|
||||||
_id = id;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getTitle() {
|
|
||||||
return _title;
|
|
||||||
}
|
|
||||||
|
|
||||||
function setTitle(title) {
|
|
||||||
_title = title;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getCommands() {
|
|
||||||
return _commands;
|
|
||||||
}
|
|
||||||
|
|
||||||
function setCommands(commands) {
|
|
||||||
_commands = commands;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
getId,
|
|
||||||
setId,
|
|
||||||
getTitle,
|
|
||||||
setTitle,
|
|
||||||
getCommands,
|
|
||||||
setCommands,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Remote;
|
|
||||||
@ -1,8 +1,6 @@
|
|||||||
import PlaybackDevice from "./playback-device.js";
|
import PlaybackDevice from "./playback-device.js";
|
||||||
import User from "./user.js";
|
import User from "./user.js";
|
||||||
import Integration from "./integration.js";
|
import Integration from "./integration.js";
|
||||||
import Command from "./command.js";
|
|
||||||
import Remote from "./remote.js";
|
|
||||||
|
|
||||||
const Serializer = (function () {
|
const Serializer = (function () {
|
||||||
function deserializeUser(object) {
|
function deserializeUser(object) {
|
||||||
@ -32,52 +30,6 @@ const Serializer = (function () {
|
|||||||
return objects.map((object) => deserializeIntegration(object));
|
return objects.map((object) => deserializeIntegration(object));
|
||||||
}
|
}
|
||||||
|
|
||||||
function serializeCommand(command) {
|
|
||||||
return {
|
|
||||||
id: command.getId(),
|
|
||||||
protocol: command.getProtocol(),
|
|
||||||
commandNumber: command.getCommandNumber(),
|
|
||||||
device: command.getDevice(),
|
|
||||||
commandType: command.getCommandType(),
|
|
||||||
title: command.getTitle(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function serializeCommands(commands) {
|
|
||||||
if (!commands) return [];
|
|
||||||
return commands.map((command) => serializeCommand(command));
|
|
||||||
}
|
|
||||||
|
|
||||||
function deserializeCommand(object) {
|
|
||||||
return new Command(object);
|
|
||||||
}
|
|
||||||
|
|
||||||
function deserializeCommands(objects) {
|
|
||||||
if (!objects) return [];
|
|
||||||
return objects.map((object) => deserializeCommand(object));
|
|
||||||
}
|
|
||||||
|
|
||||||
function serializeRemote(remote) {
|
|
||||||
return {
|
|
||||||
id: remote.getId(),
|
|
||||||
title: remote.getTitle(),
|
|
||||||
commands: serializeCommands(remote.getCommands()),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function deserializeRemote(object) {
|
|
||||||
return new Remote({
|
|
||||||
id: object.id,
|
|
||||||
title: object.title,
|
|
||||||
commands: deserializeCommands(object.commands),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function deserializeRemotes(objects) {
|
|
||||||
if (!objects) return [];
|
|
||||||
return objects.map((object) => deserializeRemote(object));
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
deserializeUser,
|
deserializeUser,
|
||||||
deserializeUsers,
|
deserializeUsers,
|
||||||
@ -85,13 +37,6 @@ const Serializer = (function () {
|
|||||||
deserializeDevices,
|
deserializeDevices,
|
||||||
deserializeIntegration,
|
deserializeIntegration,
|
||||||
deserializeIntegrations,
|
deserializeIntegrations,
|
||||||
serializeCommand,
|
|
||||||
serializeCommands,
|
|
||||||
deserializeCommand,
|
|
||||||
deserializeCommands,
|
|
||||||
serializeRemote,
|
|
||||||
deserializeRemote,
|
|
||||||
deserializeRemotes,
|
|
||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
|||||||
9
www/src/lib/fusejs-7.1.0.min.js
vendored
9
www/src/lib/fusejs-7.1.0.min.js
vendored
File diff suppressed because one or more lines are too long
7
www/src/lib/papaparse-5.5.2.min.js
vendored
7
www/src/lib/papaparse-5.5.2.min.js
vendored
File diff suppressed because one or more lines are too long
6
www/src/lib/popper.min.js
vendored
6
www/src/lib/popper.min.js
vendored
File diff suppressed because one or more lines are too long
@ -1,187 +0,0 @@
|
|||||||
import { createEffect, createMemo, createSignal } from "solid-js";
|
|
||||||
import ValidatedTextInput from "../components/validated-text-input.jsx";
|
|
||||||
import EventEmitter from "../tools/event-emitter.js";
|
|
||||||
import ModalHandler from "./modal-handler.js";
|
|
||||||
import Modal from "./modal.jsx";
|
|
||||||
import RemotesService from "../services/remotes-service.js";
|
|
||||||
import Command from "../data/command.js";
|
|
||||||
|
|
||||||
const eventEmitter = new EventEmitter();
|
|
||||||
const COMMAND_CREATED_EVENT = "success";
|
|
||||||
const MIN_TITLE_LENGTH = 3;
|
|
||||||
|
|
||||||
function CreateCommandModal(props) {
|
|
||||||
const [protocol, setProtocol] = createSignal("");
|
|
||||||
const [commandNumber, setCommandNumber] = createSignal("");
|
|
||||||
const [device, setDevice] = createSignal("");
|
|
||||||
const [commandType, setCommandType] = createSignal("");
|
|
||||||
const [title, setTitle] = createSignal("");
|
|
||||||
const [error, setError] = createSignal("");
|
|
||||||
|
|
||||||
const isProtocolValid = createMemo(() => protocol() !== "");
|
|
||||||
const isCommandNumberValid = createMemo(
|
|
||||||
() => commandNumber() !== "" && !isNaN(commandNumber())
|
|
||||||
);
|
|
||||||
const isDeviceValid = createMemo(() => device() !== "" && !isNaN(device()));
|
|
||||||
const isCommandTypeValid = createMemo(() => commandType() !== "");
|
|
||||||
const isTitleValid = createMemo(
|
|
||||||
() => title() === "" || title().length >= MIN_TITLE_LENGTH
|
|
||||||
);
|
|
||||||
|
|
||||||
const isFormValid = createMemo(
|
|
||||||
() =>
|
|
||||||
isProtocolValid() &&
|
|
||||||
isCommandNumberValid() &&
|
|
||||||
isDeviceValid() &&
|
|
||||||
isCommandTypeValid() &&
|
|
||||||
isTitleValid()
|
|
||||||
);
|
|
||||||
|
|
||||||
async function handleCreateCommand() {
|
|
||||||
let command;
|
|
||||||
try {
|
|
||||||
command = await RemotesService.createCommand(
|
|
||||||
new Command({
|
|
||||||
protocol: protocol(),
|
|
||||||
commandNumber: parseInt(commandNumber()),
|
|
||||||
device: parseInt(device()),
|
|
||||||
commandType: commandType(),
|
|
||||||
title: title(),
|
|
||||||
})
|
|
||||||
);
|
|
||||||
} catch (e) {
|
|
||||||
setError(e.message);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
resetFields();
|
|
||||||
CreateCommandModal.Handler.hide();
|
|
||||||
eventEmitter.dispatchEvent(COMMAND_CREATED_EVENT, command);
|
|
||||||
}
|
|
||||||
|
|
||||||
function resetFields() {
|
|
||||||
setProtocol("");
|
|
||||||
setCommandNumber("");
|
|
||||||
setDevice("");
|
|
||||||
setCommandType("");
|
|
||||||
setTitle("");
|
|
||||||
setError("");
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Modal
|
|
||||||
ref={props.ref}
|
|
||||||
id="createCommandModal"
|
|
||||||
modalTitle="New Command"
|
|
||||||
centered={true}
|
|
||||||
>
|
|
||||||
<div class="modal-body" style="overflow-y:inherit !important;">
|
|
||||||
<Show when={error() !== ""}>
|
|
||||||
<div class="alert alert-danger" role="alert">
|
|
||||||
{error()}
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
<div class="mb-3 row">
|
|
||||||
<label for="new_command_protocol" class="col-form-label col-sm-3">
|
|
||||||
Protocol
|
|
||||||
</label>
|
|
||||||
<div class="col-sm-9">
|
|
||||||
<select
|
|
||||||
class="form-select"
|
|
||||||
onChange={(e) => setProtocol(e.target.value)}
|
|
||||||
>
|
|
||||||
<option value="" selected>
|
|
||||||
Please select
|
|
||||||
</option>
|
|
||||||
{Object.values(Command.PROTOCOLS).map((protocol) => (
|
|
||||||
<option value={protocol}>
|
|
||||||
{Command.getProtocolString(protocol)}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="mb-3 row">
|
|
||||||
<label for="new_command_number" class="col-form-label col-sm-3">
|
|
||||||
Command Number
|
|
||||||
</label>
|
|
||||||
<ValidatedTextInput
|
|
||||||
class="col-sm-9"
|
|
||||||
id="new_command_number"
|
|
||||||
valid={isCommandNumberValid()}
|
|
||||||
value={commandNumber()}
|
|
||||||
onInput={(e) => setCommandNumber(e.target.value)}
|
|
||||||
errorText={"Command number must be a number"}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="mb-3 row">
|
|
||||||
<label
|
|
||||||
for="new_command_device_number"
|
|
||||||
class="col-form-label col-sm-3"
|
|
||||||
>
|
|
||||||
Device Number
|
|
||||||
</label>
|
|
||||||
<ValidatedTextInput
|
|
||||||
class="col-sm-9"
|
|
||||||
id="new_command_device_number"
|
|
||||||
valid={isDeviceValid()}
|
|
||||||
value={device()}
|
|
||||||
onInput={(e) => setDevice(e.target.value)}
|
|
||||||
errorText={"Device number must be a number"}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="mb-3 row">
|
|
||||||
<label for="new_command_protocol" class="col-form-label col-sm-3">
|
|
||||||
Command Type
|
|
||||||
</label>
|
|
||||||
<div class="col-sm-9">
|
|
||||||
<select
|
|
||||||
class="form-select"
|
|
||||||
onChange={(e) => setCommandType(e.target.value)}
|
|
||||||
>
|
|
||||||
<option value="" selected>
|
|
||||||
Please select
|
|
||||||
</option>
|
|
||||||
{Object.values(Command.TYPES).map((commandType) => (
|
|
||||||
<option value={commandType}>
|
|
||||||
{Command.getTypeString(commandType)}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="mb-3 row">
|
|
||||||
<label for="new_command_title" class="col-form-label col-sm-3">
|
|
||||||
Title
|
|
||||||
</label>
|
|
||||||
<ValidatedTextInput
|
|
||||||
class="col-sm-9"
|
|
||||||
id="new_command_title"
|
|
||||||
valid={isTitleValid()}
|
|
||||||
value={title()}
|
|
||||||
onInput={(e) => setTitle(e.target.value)}
|
|
||||||
errorText={`Title must be at least ${MIN_TITLE_LENGTH} characters long`}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="modal-footer">
|
|
||||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
|
|
||||||
Cancel
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={handleCreateCommand}
|
|
||||||
class="btn btn-primary"
|
|
||||||
disabled={!isFormValid()}
|
|
||||||
>
|
|
||||||
Create
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
CreateCommandModal.Handler = new ModalHandler();
|
|
||||||
CreateCommandModal.onCommandCreated = (callback) =>
|
|
||||||
eventEmitter.on(COMMAND_CREATED_EVENT, callback);
|
|
||||||
|
|
||||||
export default CreateCommandModal;
|
|
||||||
@ -1,7 +1,7 @@
|
|||||||
import { createEffect, createSignal } from "solid-js";
|
import { createEffect, createSignal } from "solid-js";
|
||||||
import Modal from "./modal.jsx";
|
import Modal from "./modal.jsx";
|
||||||
import EventEmitter from "../tools/event-emitter.js";
|
import EventEmitter from "../tools/event-emitter.js";
|
||||||
import ValidatedTextInput from "../components/validated-text-input.jsx";
|
import ValidatedTextInput from "../modules/validated-text-input.jsx";
|
||||||
import ModalHandler from "./modal-handler.js";
|
import ModalHandler from "./modal-handler.js";
|
||||||
import DeviceService from "../services/device-service.js";
|
import DeviceService from "../services/device-service.js";
|
||||||
|
|
||||||
|
|||||||
@ -1,124 +0,0 @@
|
|||||||
import { createMemo, createResource, createSignal, onMount } from "solid-js";
|
|
||||||
import ValidatedTextInput from "../components/validated-text-input.jsx";
|
|
||||||
import RemotesService from "../services/remotes-service.js";
|
|
||||||
import EventEmitter from "../tools/event-emitter.js";
|
|
||||||
import ModalHandler from "./modal-handler.js";
|
|
||||||
import Modal from "./modal.jsx";
|
|
||||||
import ListManager from "../components/list-manager.jsx";
|
|
||||||
import Remote from "../data/remote.js";
|
|
||||||
|
|
||||||
const eventEmitter = new EventEmitter();
|
|
||||||
const REMOTE_CREATED_EVENT = "success";
|
|
||||||
const MIN_TITLE_LENGTH = 3;
|
|
||||||
const modalHandler = new ModalHandler();
|
|
||||||
|
|
||||||
function CreateRemoteModal(props) {
|
|
||||||
const [title, setTitle] = createSignal("");
|
|
||||||
const [commands, setCommands] = createSignal([]);
|
|
||||||
const [availableCommands, { refetch: refetchAvailableCommands }] =
|
|
||||||
createResource(RemotesService.getCommands);
|
|
||||||
const [error, setError] = createSignal("");
|
|
||||||
|
|
||||||
const isTitleValid = createMemo(() => title().length >= MIN_TITLE_LENGTH);
|
|
||||||
const isFormValid = createMemo(() => isTitleValid());
|
|
||||||
|
|
||||||
modalHandler.onShow(() => {
|
|
||||||
refetchAvailableCommands();
|
|
||||||
});
|
|
||||||
|
|
||||||
async function handleCreateRemote() {
|
|
||||||
let remote;
|
|
||||||
try {
|
|
||||||
remote = await RemotesService.createRemote(
|
|
||||||
new Remote({
|
|
||||||
title: title(),
|
|
||||||
commands: commands(),
|
|
||||||
})
|
|
||||||
);
|
|
||||||
} catch (e) {
|
|
||||||
setError(e.message);
|
|
||||||
console.error(e);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
resetFields();
|
|
||||||
CreateRemoteModal.Handler.hide();
|
|
||||||
eventEmitter.dispatchEvent(REMOTE_CREATED_EVENT, remote);
|
|
||||||
}
|
|
||||||
|
|
||||||
function resetFields() {
|
|
||||||
setTitle("");
|
|
||||||
setCommands([]);
|
|
||||||
setError("");
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleCommandSelect(item) {
|
|
||||||
setCommands([...commands(), item]);
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleCommandDeselect(item) {
|
|
||||||
setCommands(
|
|
||||||
commands().filter((command) => command.getId() !== item.getId())
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Modal
|
|
||||||
ref={props.ref}
|
|
||||||
id="createRemoteModal"
|
|
||||||
modalTitle="New Remote"
|
|
||||||
centered={true}
|
|
||||||
>
|
|
||||||
<div class="modal-body bg-body-tertiary">
|
|
||||||
<Show when={error() !== ""}>
|
|
||||||
<div class="alert alert-danger" role="alert">
|
|
||||||
{error()}
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
<div class="mb-3 row">
|
|
||||||
<label for="new_remote_title" class="col-form-label col-sm-1">
|
|
||||||
Title
|
|
||||||
</label>
|
|
||||||
<ValidatedTextInput
|
|
||||||
class="col-sm-11"
|
|
||||||
id="new_remote_title"
|
|
||||||
valid={isTitleValid()}
|
|
||||||
value={title()}
|
|
||||||
onInput={(e) => setTitle(e.target.value)}
|
|
||||||
errorText={`Title must be at least ${MIN_TITLE_LENGTH} characters long`}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<ListManager
|
|
||||||
style="height: 20em;"
|
|
||||||
items={commands()}
|
|
||||||
availableItems={availableCommands()}
|
|
||||||
itemToString={(command) =>
|
|
||||||
`${command.getTitle()} (${command.getProtocol()}:${command.getCommandType()})`
|
|
||||||
}
|
|
||||||
onItemSelect={handleCommandSelect}
|
|
||||||
onItemDeselect={handleCommandDeselect}
|
|
||||||
itemsTitle="Selected Commands"
|
|
||||||
availableItemsTitle="Available Commands"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="modal-footer">
|
|
||||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
|
|
||||||
Cancel
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={handleCreateRemote}
|
|
||||||
class="btn btn-primary"
|
|
||||||
disabled={!isFormValid()}
|
|
||||||
>
|
|
||||||
Create
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
CreateRemoteModal.Handler = modalHandler;
|
|
||||||
CreateRemoteModal.onRemoteCreated = (callback) =>
|
|
||||||
eventEmitter.on(REMOTE_CREATED_EVENT, callback);
|
|
||||||
|
|
||||||
export default CreateRemoteModal;
|
|
||||||
@ -2,7 +2,7 @@ import { createEffect, createSignal } from "solid-js";
|
|||||||
import Modal from "./modal.jsx";
|
import Modal from "./modal.jsx";
|
||||||
import UserService from "../services/user-service.js";
|
import UserService from "../services/user-service.js";
|
||||||
import EventEmitter from "../tools/event-emitter.js";
|
import EventEmitter from "../tools/event-emitter.js";
|
||||||
import ValidatedTextInput from "../components/validated-text-input.jsx";
|
import ValidatedTextInput from "../modules/validated-text-input.jsx";
|
||||||
import ModalHandler from "./modal-handler.js";
|
import ModalHandler from "./modal-handler.js";
|
||||||
|
|
||||||
const [users, setUsers] = createSignal([]);
|
const [users, setUsers] = createSignal([]);
|
||||||
|
|||||||
@ -1,66 +0,0 @@
|
|||||||
import { createSignal } from "solid-js";
|
|
||||||
import Command from "../data/command.js";
|
|
||||||
import RemotesService from "../services/remotes-service.js";
|
|
||||||
import EventEmitter from "../tools/event-emitter.js";
|
|
||||||
import ModalHandler from "./modal-handler.js";
|
|
||||||
import Modal from "./modal.jsx";
|
|
||||||
|
|
||||||
const [command, setCommand] = createSignal(new Command());
|
|
||||||
const eventEmitter = new EventEmitter();
|
|
||||||
const COMMAND_DELETED_EVENT = "success";
|
|
||||||
|
|
||||||
function DeleteCommandModal(props) {
|
|
||||||
const [error, setError] = createSignal("");
|
|
||||||
|
|
||||||
async function handleDeleteCommand() {
|
|
||||||
try {
|
|
||||||
await RemotesService.deleteCommand(command().getId());
|
|
||||||
} catch (e) {
|
|
||||||
setError(e.message);
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
DeleteCommandModal.Handler.hide();
|
|
||||||
eventEmitter.dispatchEvent(COMMAND_DELETED_EVENT, command);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Modal
|
|
||||||
ref={props.ref}
|
|
||||||
id="deleteCommandModal"
|
|
||||||
modalTitle="Delete command"
|
|
||||||
centered={true}
|
|
||||||
>
|
|
||||||
<div class="modal-body">
|
|
||||||
<Show when={error() !== ""}>
|
|
||||||
<div class="alert alert-danger" role="alert">
|
|
||||||
{error()}
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
<div class="mb-3 row">
|
|
||||||
<span>
|
|
||||||
Do you really want to delete the command {command().getTitle()} ({command().getId()})?
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="modal-footer">
|
|
||||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
|
|
||||||
Cancel
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={handleDeleteCommand}
|
|
||||||
class="btn btn-danger"
|
|
||||||
>
|
|
||||||
Delete
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
DeleteCommandModal.Handler = new ModalHandler();
|
|
||||||
DeleteCommandModal.setCommand = setCommand;
|
|
||||||
DeleteCommandModal.onCommandDeleted = (callback) =>
|
|
||||||
eventEmitter.on(COMMAND_DELETED_EVENT, callback);
|
|
||||||
|
|
||||||
export default DeleteCommandModal;
|
|
||||||
@ -1,9 +1,12 @@
|
|||||||
import { createSignal } from "solid-js";
|
import { createEffect, createSignal } from "solid-js";
|
||||||
|
import Modal from "./modal.jsx";
|
||||||
|
import UserService from "../services/user-service.js";
|
||||||
|
import EventEmitter from "../tools/event-emitter.js";
|
||||||
|
import User from "../data/user.js";
|
||||||
|
import ValidatedTextInput from "../modules/validated-text-input.jsx";
|
||||||
|
import ModalHandler from "./modal-handler.js";
|
||||||
import Integration from "../data/integration.js";
|
import Integration from "../data/integration.js";
|
||||||
import DeviceService from "../services/device-service.js";
|
import DeviceService from "../services/device-service.js";
|
||||||
import EventEmitter from "../tools/event-emitter.js";
|
|
||||||
import ModalHandler from "./modal-handler.js";
|
|
||||||
import Modal from "./modal.jsx";
|
|
||||||
|
|
||||||
const [integration, setIntegration] = createSignal(new Integration());
|
const [integration, setIntegration] = createSignal(new Integration());
|
||||||
const eventEmitter = new EventEmitter();
|
const eventEmitter = new EventEmitter();
|
||||||
|
|||||||
@ -1,66 +0,0 @@
|
|||||||
import { createSignal } from "solid-js";
|
|
||||||
import Remote from "../data/remote.js";
|
|
||||||
import RemotesService from "../services/remotes-service.js";
|
|
||||||
import EventEmitter from "../tools/event-emitter.js";
|
|
||||||
import ModalHandler from "./modal-handler.js";
|
|
||||||
import Modal from "./modal.jsx";
|
|
||||||
|
|
||||||
const [remote, setRemote] = createSignal(new Remote());
|
|
||||||
const eventEmitter = new EventEmitter();
|
|
||||||
const REMOTE_DELETED_EVENT = "success";
|
|
||||||
|
|
||||||
function DeleteRemoteModal(props) {
|
|
||||||
const [error, setError] = createSignal("");
|
|
||||||
|
|
||||||
async function handleDeleteRemote() {
|
|
||||||
try {
|
|
||||||
await RemotesService.deleteRemote(remote().getId());
|
|
||||||
} catch (e) {
|
|
||||||
setError(e.message);
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
DeleteRemoteModal.Handler.hide();
|
|
||||||
eventEmitter.dispatchEvent(REMOTE_DELETED_EVENT, remote);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Modal
|
|
||||||
ref={props.ref}
|
|
||||||
id="deleteRemoteModal"
|
|
||||||
modalTitle="Delete remote"
|
|
||||||
centered={true}
|
|
||||||
>
|
|
||||||
<div class="modal-body">
|
|
||||||
<Show when={error() !== ""}>
|
|
||||||
<div class="alert alert-danger" role="alert">
|
|
||||||
{error()}
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
<div class="mb-3 row">
|
|
||||||
<span>
|
|
||||||
Do you really want to delete the remote {remote().getTitle()} ({remote().getId()})?
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="modal-footer">
|
|
||||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
|
|
||||||
Cancel
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={handleDeleteRemote}
|
|
||||||
class="btn btn-danger"
|
|
||||||
>
|
|
||||||
Delete
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
DeleteRemoteModal.Handler = new ModalHandler();
|
|
||||||
DeleteRemoteModal.setRemote = setRemote;
|
|
||||||
DeleteRemoteModal.onRemoteDeleted = (callback) =>
|
|
||||||
eventEmitter.on(REMOTE_DELETED_EVENT, callback);
|
|
||||||
|
|
||||||
export default DeleteRemoteModal;
|
|
||||||
@ -3,7 +3,7 @@ import Modal from "./modal.jsx";
|
|||||||
import UserService from "../services/user-service.js";
|
import UserService from "../services/user-service.js";
|
||||||
import EventEmitter from "../tools/event-emitter.js";
|
import EventEmitter from "../tools/event-emitter.js";
|
||||||
import User from "../data/user.js";
|
import User from "../data/user.js";
|
||||||
import ValidatedTextInput from "../components/validated-text-input.jsx";
|
import ValidatedTextInput from "../modules/validated-text-input.jsx";
|
||||||
import ModalHandler from "./modal-handler.js";
|
import ModalHandler from "./modal-handler.js";
|
||||||
|
|
||||||
const [user, setUser] = createSignal(new User());
|
const [user, setUser] = createSignal(new User());
|
||||||
|
|||||||
@ -1,199 +0,0 @@
|
|||||||
import { createEffect, createMemo, createSignal } from "solid-js";
|
|
||||||
import ValidatedTextInput from "../components/validated-text-input.jsx";
|
|
||||||
import EventEmitter from "../tools/event-emitter.js";
|
|
||||||
import ModalHandler from "./modal-handler.js";
|
|
||||||
import Modal from "./modal.jsx";
|
|
||||||
import RemotesService from "../services/remotes-service.js";
|
|
||||||
import Command from "../data/command.js";
|
|
||||||
|
|
||||||
const eventEmitter = new EventEmitter();
|
|
||||||
const COMMAND_EDITED_EVENT = "success";
|
|
||||||
const MIN_TITLE_LENGTH = 3;
|
|
||||||
|
|
||||||
const [command, setCommand] = createSignal(new Command());
|
|
||||||
|
|
||||||
function EditCommandModal(props) {
|
|
||||||
const [protocol, setProtocol] = createSignal("");
|
|
||||||
const [commandNumber, setCommandNumber] = createSignal("");
|
|
||||||
const [device, setDevice] = createSignal("");
|
|
||||||
const [commandType, setCommandType] = createSignal("");
|
|
||||||
const [title, setTitle] = createSignal("");
|
|
||||||
const [error, setError] = createSignal("");
|
|
||||||
|
|
||||||
const isProtocolValid = createMemo(() => protocol() !== "");
|
|
||||||
const isCommandNumberValid = createMemo(
|
|
||||||
() => commandNumber() !== "" && !isNaN(commandNumber())
|
|
||||||
);
|
|
||||||
const isDeviceValid = createMemo(() => device() !== "" && !isNaN(device()));
|
|
||||||
const isCommandTypeValid = createMemo(() => commandType() !== "");
|
|
||||||
const isTitleValid = createMemo(
|
|
||||||
() => title() === "" || title().length >= MIN_TITLE_LENGTH
|
|
||||||
);
|
|
||||||
|
|
||||||
createEffect(() => {
|
|
||||||
setProtocol(command().getProtocol());
|
|
||||||
setCommandNumber(command().getCommandNumber());
|
|
||||||
setDevice(command().getDevice());
|
|
||||||
setCommandType(command().getCommandType());
|
|
||||||
setTitle(command().getTitle());
|
|
||||||
});
|
|
||||||
|
|
||||||
const isFormValid = createMemo(
|
|
||||||
() =>
|
|
||||||
isProtocolValid() &&
|
|
||||||
isCommandNumberValid() &&
|
|
||||||
isDeviceValid() &&
|
|
||||||
isCommandTypeValid() &&
|
|
||||||
isTitleValid()
|
|
||||||
);
|
|
||||||
|
|
||||||
async function handleEditCommand() {
|
|
||||||
try {
|
|
||||||
await RemotesService.updateCommand(
|
|
||||||
new Command({
|
|
||||||
id: command().getId(),
|
|
||||||
protocol: protocol(),
|
|
||||||
commandNumber: parseInt(commandNumber()),
|
|
||||||
device: parseInt(device()),
|
|
||||||
commandType: commandType(),
|
|
||||||
title: title(),
|
|
||||||
})
|
|
||||||
);
|
|
||||||
} catch (e) {
|
|
||||||
console.error(e);
|
|
||||||
setError(e.message);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
resetFields();
|
|
||||||
EditCommandModal.Handler.hide();
|
|
||||||
eventEmitter.dispatchEvent(COMMAND_EDITED_EVENT);
|
|
||||||
}
|
|
||||||
|
|
||||||
function resetFields() {
|
|
||||||
setProtocol("");
|
|
||||||
setCommandNumber("");
|
|
||||||
setDevice("");
|
|
||||||
setCommandType("");
|
|
||||||
setTitle("");
|
|
||||||
setError("");
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Modal
|
|
||||||
ref={props.ref}
|
|
||||||
id="editCommandModal"
|
|
||||||
modalTitle="Edit Command"
|
|
||||||
centered={true}
|
|
||||||
>
|
|
||||||
<div class="modal-body" style="overflow-y:inherit !important;">
|
|
||||||
<Show when={error() !== ""}>
|
|
||||||
<div class="alert alert-danger" role="alert">
|
|
||||||
{error()}
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
<div class="mb-3 row">
|
|
||||||
<label for="edit_command_protocol" class="col-form-label col-sm-3">
|
|
||||||
Protocol
|
|
||||||
</label>
|
|
||||||
<div class="col-sm-9">
|
|
||||||
<select
|
|
||||||
class="form-select"
|
|
||||||
onChange={(e) => setProtocol(e.target.value)}
|
|
||||||
>
|
|
||||||
{Object.values(Command.PROTOCOLS).map((protocol) => (
|
|
||||||
<option
|
|
||||||
value={protocol}
|
|
||||||
selected={protocol === command().getProtocol()}
|
|
||||||
>
|
|
||||||
{Command.getProtocolString(protocol)}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="mb-3 row">
|
|
||||||
<label for="edit_command_number" class="col-form-label col-sm-3">
|
|
||||||
Command Number
|
|
||||||
</label>
|
|
||||||
<ValidatedTextInput
|
|
||||||
class="col-sm-9"
|
|
||||||
id="edit_command_number"
|
|
||||||
valid={isCommandNumberValid()}
|
|
||||||
value={commandNumber()}
|
|
||||||
onInput={(e) => setCommandNumber(e.target.value)}
|
|
||||||
errorText={"Command number must be a number"}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="mb-3 row">
|
|
||||||
<label
|
|
||||||
for="edit_command_device_number"
|
|
||||||
class="col-form-label col-sm-3"
|
|
||||||
>
|
|
||||||
Device Number
|
|
||||||
</label>
|
|
||||||
<ValidatedTextInput
|
|
||||||
class="col-sm-9"
|
|
||||||
id="edit_command_device_number"
|
|
||||||
valid={isDeviceValid()}
|
|
||||||
value={device()}
|
|
||||||
onInput={(e) => setDevice(e.target.value)}
|
|
||||||
errorText={"Device number must be a number"}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="mb-3 row">
|
|
||||||
<label for="edit_command_protocol" class="col-form-label col-sm-3">
|
|
||||||
Command Type
|
|
||||||
</label>
|
|
||||||
<div class="col-sm-9">
|
|
||||||
<select
|
|
||||||
class="form-select"
|
|
||||||
onChange={(e) => setCommandType(e.target.value)}
|
|
||||||
>
|
|
||||||
{Object.values(Command.TYPES).map((commandType) => (
|
|
||||||
<option
|
|
||||||
value={commandType}
|
|
||||||
selected={commandType === command().getCommandType()}
|
|
||||||
>
|
|
||||||
{Command.getTypeString(commandType)}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="mb-3 row">
|
|
||||||
<label for="edit_command_title" class="col-form-label col-sm-3">
|
|
||||||
Title
|
|
||||||
</label>
|
|
||||||
<ValidatedTextInput
|
|
||||||
class="col-sm-9"
|
|
||||||
id="edit_command_title"
|
|
||||||
valid={isTitleValid()}
|
|
||||||
value={title()}
|
|
||||||
onInput={(e) => setTitle(e.target.value)}
|
|
||||||
errorText={`Title must be at least ${MIN_TITLE_LENGTH} characters long`}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="modal-footer">
|
|
||||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
|
|
||||||
Cancel
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={handleEditCommand}
|
|
||||||
class="btn btn-primary"
|
|
||||||
disabled={!isFormValid()}
|
|
||||||
>
|
|
||||||
Edit
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
EditCommandModal.Handler = new ModalHandler();
|
|
||||||
EditCommandModal.onCommandEdited = (callback) =>
|
|
||||||
eventEmitter.on(COMMAND_EDITED_EVENT, callback);
|
|
||||||
EditCommandModal.setCommand = setCommand;
|
|
||||||
|
|
||||||
export default EditCommandModal;
|
|
||||||
@ -1,145 +0,0 @@
|
|||||||
import {
|
|
||||||
createEffect,
|
|
||||||
createMemo,
|
|
||||||
createResource,
|
|
||||||
createSignal,
|
|
||||||
} from "solid-js";
|
|
||||||
import ListManager from "../components/list-manager.jsx";
|
|
||||||
import ValidatedTextInput from "../components/validated-text-input.jsx";
|
|
||||||
import Remote from "../data/remote.js";
|
|
||||||
import RemoteService from "../services/remotes-service.js";
|
|
||||||
import EventEmitter from "../tools/event-emitter.js";
|
|
||||||
import ModalHandler from "./modal-handler.js";
|
|
||||||
import Modal from "./modal.jsx";
|
|
||||||
|
|
||||||
const eventEmitter = new EventEmitter();
|
|
||||||
const REMOTE_EDITED_EVENT = "success";
|
|
||||||
const MIN_TITLE_LENGTH = 3;
|
|
||||||
const modalHandler = new ModalHandler();
|
|
||||||
|
|
||||||
const [remoteId, setRemoteId] = createSignal(null);
|
|
||||||
|
|
||||||
function EditRemoteModal(props) {
|
|
||||||
const [title, setTitle] = createSignal("");
|
|
||||||
const [commands, setCommands] = createSignal([]);
|
|
||||||
const [availableCommands, { refetch: refetchAvailableCommands }] =
|
|
||||||
createResource(RemoteService.getCommands);
|
|
||||||
const [remote, {}] = createResource(
|
|
||||||
remoteId,
|
|
||||||
() => RemoteService.getRemote(remoteId()),
|
|
||||||
{ initialValue: new Remote() }
|
|
||||||
);
|
|
||||||
const [error, setError] = createSignal("");
|
|
||||||
|
|
||||||
const isTitleValid = createMemo(() => title().length >= MIN_TITLE_LENGTH);
|
|
||||||
const isFormValid = createMemo(() => isTitleValid());
|
|
||||||
|
|
||||||
createEffect(() => {
|
|
||||||
if (!remote()) return;
|
|
||||||
setTitle(remote().getTitle());
|
|
||||||
setCommands(remote().getCommands());
|
|
||||||
});
|
|
||||||
|
|
||||||
modalHandler.onShow(() => {
|
|
||||||
refetchAvailableCommands();
|
|
||||||
});
|
|
||||||
|
|
||||||
async function handleEditRemote() {
|
|
||||||
try {
|
|
||||||
await RemoteService.updateRemote(
|
|
||||||
new Remote({
|
|
||||||
id: remote().getId(),
|
|
||||||
title: title(),
|
|
||||||
commands: commands(),
|
|
||||||
})
|
|
||||||
);
|
|
||||||
} catch (e) {
|
|
||||||
setError(e.message);
|
|
||||||
console.error(e);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
resetFields();
|
|
||||||
EditRemoteModal.Handler.hide();
|
|
||||||
eventEmitter.dispatchEvent(REMOTE_EDITED_EVENT);
|
|
||||||
}
|
|
||||||
|
|
||||||
function resetFields() {
|
|
||||||
setTitle("");
|
|
||||||
setCommands([]);
|
|
||||||
setRemoteId("");
|
|
||||||
setError("");
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleCommandSelect(item) {
|
|
||||||
setCommands([...commands(), item]);
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleCommandDeselect(item) {
|
|
||||||
setCommands(
|
|
||||||
commands().filter((command) => command.getId() !== item.getId())
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Modal
|
|
||||||
ref={props.ref}
|
|
||||||
id="editRemoteModal"
|
|
||||||
modalTitle="New Remote"
|
|
||||||
centered={true}
|
|
||||||
>
|
|
||||||
<div class="modal-body bg-body-tertiary">
|
|
||||||
<Show when={error() !== ""}>
|
|
||||||
<div class="alert alert-danger" role="alert">
|
|
||||||
{error()}
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
<div class="mb-3 row">
|
|
||||||
<label for="edit_remote_title" class="col-form-label col-sm-1">
|
|
||||||
Title
|
|
||||||
</label>
|
|
||||||
<ValidatedTextInput
|
|
||||||
class="col-sm-11"
|
|
||||||
id="edit_remote_title"
|
|
||||||
valid={isTitleValid()}
|
|
||||||
value={title()}
|
|
||||||
onInput={(e) => setTitle(e.target.value)}
|
|
||||||
errorText={`Title must be at least ${MIN_TITLE_LENGTH} characters long`}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<ListManager
|
|
||||||
style="height: 20em;"
|
|
||||||
items={commands()}
|
|
||||||
availableItems={availableCommands()}
|
|
||||||
itemToString={(command) =>
|
|
||||||
`${command.getTitle()} (${command.getProtocol()}:${command.getCommandType()})`
|
|
||||||
}
|
|
||||||
onItemSelect={handleCommandSelect}
|
|
||||||
onItemDeselect={handleCommandDeselect}
|
|
||||||
itemsTitle="Selected Commands"
|
|
||||||
availableItemsTitle="Available Commands"
|
|
||||||
itemsEqual={(a, b) => a.getId() === b.getId()}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="modal-footer">
|
|
||||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
|
|
||||||
Cancel
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={handleEditRemote}
|
|
||||||
class="btn btn-primary"
|
|
||||||
disabled={!isFormValid()}
|
|
||||||
>
|
|
||||||
Edit
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
EditRemoteModal.Handler = modalHandler;
|
|
||||||
EditRemoteModal.onRemoteEdited = (callback) =>
|
|
||||||
eventEmitter.on(REMOTE_EDITED_EVENT, callback);
|
|
||||||
EditRemoteModal.setRemoteId = setRemoteId;
|
|
||||||
|
|
||||||
export default EditRemoteModal;
|
|
||||||
@ -1,500 +0,0 @@
|
|||||||
import {
|
|
||||||
createEffect,
|
|
||||||
createMemo,
|
|
||||||
createSignal,
|
|
||||||
Match,
|
|
||||||
on,
|
|
||||||
Show,
|
|
||||||
Switch,
|
|
||||||
} from "solid-js";
|
|
||||||
import RemotesService from "../services/remotes-service.js";
|
|
||||||
import EventEmitter from "../tools/event-emitter.js";
|
|
||||||
import ModalHandler from "./modal-handler.js";
|
|
||||||
import Modal from "./modal.jsx";
|
|
||||||
import Command from "../data/command.js";
|
|
||||||
|
|
||||||
const eventEmitter = new EventEmitter();
|
|
||||||
const COMMANDS_IMPORTED_EVENT = "success";
|
|
||||||
|
|
||||||
const ENTER_CSV_STEP = 1;
|
|
||||||
const MAP_FIELDS_STEP = 2;
|
|
||||||
const MAP_VALUES_STEP = 3;
|
|
||||||
const CONFIRM_STEP = 4;
|
|
||||||
const TOTAL_STEPS = 4;
|
|
||||||
|
|
||||||
const PROTOCOL_FIELD = "protocol";
|
|
||||||
const COMMAND_NUMBER_FIELD = "commandNumber";
|
|
||||||
const DEVICE_FIELD = "device";
|
|
||||||
const COMMAND_TYPE_FIELD = "commandType";
|
|
||||||
const TITLE_FIELD = "title";
|
|
||||||
let FieldTitles = {};
|
|
||||||
FieldTitles[PROTOCOL_FIELD] = "Protocol";
|
|
||||||
FieldTitles[COMMAND_NUMBER_FIELD] = "Command";
|
|
||||||
FieldTitles[DEVICE_FIELD] = "Device";
|
|
||||||
FieldTitles[COMMAND_TYPE_FIELD] = "Type";
|
|
||||||
FieldTitles[TITLE_FIELD] = "Title";
|
|
||||||
|
|
||||||
const CommandFieldsRequiringValueMapping = ["protocol", "commandType"];
|
|
||||||
const commandTypeFuse = new Fuse(
|
|
||||||
Object.values(Command.TYPES).map((type) => Command.getTypeString(type))
|
|
||||||
);
|
|
||||||
const protocolsFuse = new Fuse(
|
|
||||||
Object.values(Command.PROTOCOLS).map((protocol) =>
|
|
||||||
Command.getProtocolString(protocol)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
function ImportCommandsModal(props) {
|
|
||||||
const [csvString, setCsvString] = createSignal("");
|
|
||||||
const [currentStep, setCurrentStep] = createSignal(ENTER_CSV_STEP);
|
|
||||||
const [error, setError] = createSignal("");
|
|
||||||
const [csvArray, setCsvArray] = createSignal([]);
|
|
||||||
const [commands, setCommands] = createSignal([]);
|
|
||||||
const [fieldMapping, setFieldMapping] = createSignal(
|
|
||||||
{},
|
|
||||||
{ equals: () => false }
|
|
||||||
);
|
|
||||||
const [isComputingValueMapping, setComputingValueMapping] =
|
|
||||||
createSignal(false);
|
|
||||||
const [valueMapping, setValueMapping] = createSignal(
|
|
||||||
(() => {
|
|
||||||
let mapping = {};
|
|
||||||
mapping[PROTOCOL_FIELD] = {};
|
|
||||||
mapping[COMMAND_TYPE_FIELD] = {};
|
|
||||||
return mapping;
|
|
||||||
})(),
|
|
||||||
{ equals: () => false }
|
|
||||||
);
|
|
||||||
|
|
||||||
const canMakeNextStep = createMemo(() => {
|
|
||||||
switch (currentStep()) {
|
|
||||||
case ENTER_CSV_STEP:
|
|
||||||
return csvString() !== "";
|
|
||||||
default:
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
const fields = createMemo(() =>
|
|
||||||
Object.keys(csvArray().find(() => true) || {})
|
|
||||||
);
|
|
||||||
|
|
||||||
createEffect(() => {
|
|
||||||
switch (currentStep()) {
|
|
||||||
case MAP_VALUES_STEP:
|
|
||||||
onMapValuesStep();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
function handleNextStep() {
|
|
||||||
switch (currentStep()) {
|
|
||||||
case ENTER_CSV_STEP:
|
|
||||||
handleEnterCsvStep();
|
|
||||||
break;
|
|
||||||
case MAP_FIELDS_STEP:
|
|
||||||
handleMapFieldsStep();
|
|
||||||
break;
|
|
||||||
case MAP_VALUES_STEP:
|
|
||||||
handleMapValuesStep();
|
|
||||||
break;
|
|
||||||
case CONFIRM_STEP:
|
|
||||||
handleImportCommands();
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
nextStep();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleImportCommands() {
|
|
||||||
let newCommands = [];
|
|
||||||
try {
|
|
||||||
newCommands = await RemotesService.createCommands(commands());
|
|
||||||
} catch (e) {
|
|
||||||
setError(e.message);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
resetFields();
|
|
||||||
ImportCommandsModal.Handler.hide();
|
|
||||||
eventEmitter.dispatchEvent(COMMANDS_IMPORTED_EVENT, newCommands);
|
|
||||||
}
|
|
||||||
|
|
||||||
function EnterCsvStep() {
|
|
||||||
return (
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="csvTextarea" class="form-label">
|
|
||||||
Enter CSV:
|
|
||||||
</label>
|
|
||||||
<textarea
|
|
||||||
class="form-control"
|
|
||||||
id="csvTextarea"
|
|
||||||
rows="10"
|
|
||||||
value={csvString()}
|
|
||||||
onInput={(e) => setCsvString(e.target.value)}
|
|
||||||
></textarea>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleEnterCsvStep() {
|
|
||||||
if (!csvString()) {
|
|
||||||
setError("Please enter a CSV string.");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let result = Papa.parse(csvString(), { header: true });
|
|
||||||
if (result.errors.length > 0) {
|
|
||||||
setError(result.errors[0].message);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
let csvArray = result.data;
|
|
||||||
setCsvArray(csvArray);
|
|
||||||
nextStep();
|
|
||||||
}
|
|
||||||
|
|
||||||
function MapFieldsStep() {
|
|
||||||
function setMapping(field, commandField) {
|
|
||||||
let mapping = fieldMapping();
|
|
||||||
mapping[field] = commandField;
|
|
||||||
setFieldMapping(mapping);
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<div class="mb-3">
|
|
||||||
<div>Map Fields:</div>
|
|
||||||
<div class="d-flex flex-column">
|
|
||||||
{fields().map((field) => (
|
|
||||||
<div class="d-flex flex-row align-items-center justify-content-center text-end mb-2">
|
|
||||||
<div class="col-sm-4">
|
|
||||||
<div>{field}</div>
|
|
||||||
<div class="fw-light lh-1">(e.g. {csvArray()[0][field]})</div>
|
|
||||||
</div>
|
|
||||||
<div style="font-size: 1.5rem; line-height: 1;">
|
|
||||||
<i class="bi bi-arrow-right-short"></i>
|
|
||||||
</div>
|
|
||||||
<div class="col-sm-4">
|
|
||||||
<select
|
|
||||||
class="form-select"
|
|
||||||
onChange={(e) => setMapping(field, e.target.value)}
|
|
||||||
>
|
|
||||||
<option value="" selected>
|
|
||||||
Please select
|
|
||||||
</option>
|
|
||||||
{Object.keys(FieldTitles).map((commandField) => (
|
|
||||||
<option
|
|
||||||
value={commandField}
|
|
||||||
selected={fieldMapping()[field] === commandField}
|
|
||||||
>
|
|
||||||
{FieldTitles[commandField]}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleMapFieldsStep() {
|
|
||||||
let usedCommandFields = [];
|
|
||||||
let mapping = fieldMapping();
|
|
||||||
for (let field in mapping) {
|
|
||||||
if (usedCommandFields.includes(mapping[field])) {
|
|
||||||
setError("Duplicate mapping found.");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
usedCommandFields.push(mapping[field]);
|
|
||||||
}
|
|
||||||
nextStep();
|
|
||||||
}
|
|
||||||
|
|
||||||
function MapValuesStep() {
|
|
||||||
function setMapping(field, value, commandValue) {
|
|
||||||
let mapping = valueMapping();
|
|
||||||
if (!mapping[field]) mapping[field] = {};
|
|
||||||
mapping[field][value] = commandValue;
|
|
||||||
setValueMapping(mapping);
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<div class="mb-3">
|
|
||||||
<div>Map Values:</div>
|
|
||||||
{Object.keys(fieldMapping())
|
|
||||||
.filter((field) =>
|
|
||||||
CommandFieldsRequiringValueMapping.includes(fieldMapping()[field])
|
|
||||||
)
|
|
||||||
.map((csvField) => {
|
|
||||||
let field = fieldMapping()[csvField];
|
|
||||||
return (
|
|
||||||
<div>
|
|
||||||
<div>{FieldTitles[field]}:</div>
|
|
||||||
<div class="d-flex flex-column">
|
|
||||||
{csvArray()
|
|
||||||
.map((row) => row[csvField])
|
|
||||||
.filter(
|
|
||||||
(value, index, array) => array.indexOf(value) === index
|
|
||||||
)
|
|
||||||
.map((value) => (
|
|
||||||
<div class="d-flex flex-row align-items-center justify-content-center text-end mb-2">
|
|
||||||
<div class="col-sm-4">
|
|
||||||
<div>{value}</div>
|
|
||||||
</div>
|
|
||||||
<div style="font-size: 1.5rem; line-height: 1;">
|
|
||||||
<i class="bi bi-arrow-right-short"></i>
|
|
||||||
</div>
|
|
||||||
<div class="col-sm-4">
|
|
||||||
<Switch>
|
|
||||||
<Match when={field === "protocol"}>
|
|
||||||
<select
|
|
||||||
class="form-select"
|
|
||||||
onChange={(e) =>
|
|
||||||
setMapping(field, value, e.target.value)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<option value="" selected>
|
|
||||||
Please select
|
|
||||||
</option>
|
|
||||||
{Object.values(Command.PROTOCOLS).map(
|
|
||||||
(protocol) => (
|
|
||||||
<option
|
|
||||||
value={protocol}
|
|
||||||
selected={
|
|
||||||
valueMapping()[PROTOCOL_FIELD][
|
|
||||||
value
|
|
||||||
] === protocol
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{Command.getProtocolString(protocol)}
|
|
||||||
</option>
|
|
||||||
)
|
|
||||||
)}
|
|
||||||
</select>
|
|
||||||
</Match>
|
|
||||||
<Match when={field === "commandType"}>
|
|
||||||
<select
|
|
||||||
class="form-select"
|
|
||||||
onChange={(e) =>
|
|
||||||
setMapping(field, value, e.target.value)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<option value="" selected>
|
|
||||||
Please select
|
|
||||||
</option>
|
|
||||||
{Object.values(Command.TYPES).map(
|
|
||||||
(commandType) => (
|
|
||||||
<option
|
|
||||||
value={commandType}
|
|
||||||
selected={
|
|
||||||
valueMapping()[COMMAND_TYPE_FIELD][
|
|
||||||
value
|
|
||||||
] === commandType
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{Command.getTypeString(commandType)}
|
|
||||||
</option>
|
|
||||||
)
|
|
||||||
)}
|
|
||||||
</select>
|
|
||||||
</Match>
|
|
||||||
</Switch>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function onMapValuesStep() {
|
|
||||||
setComputingValueMapping(true);
|
|
||||||
(async () => {
|
|
||||||
let valueMapping = {};
|
|
||||||
valueMapping[PROTOCOL_FIELD] = {};
|
|
||||||
valueMapping[COMMAND_TYPE_FIELD] = {};
|
|
||||||
let protocolField = Object.keys(fieldMapping()).find(
|
|
||||||
(field) => fieldMapping()[field] === PROTOCOL_FIELD
|
|
||||||
);
|
|
||||||
let commandTypeField = Object.keys(fieldMapping()).find(
|
|
||||||
(field) => fieldMapping()[field] === COMMAND_TYPE_FIELD
|
|
||||||
);
|
|
||||||
csvArray().forEach((row) => {
|
|
||||||
let protocolValue = row[protocolField];
|
|
||||||
let commandTypeValue = row[commandTypeField];
|
|
||||||
if (!valueMapping[PROTOCOL_FIELD][protocolValue]) {
|
|
||||||
let result = protocolsFuse.search(protocolValue).shift();
|
|
||||||
if (result) {
|
|
||||||
let protocol = Object.values(Command.PROTOCOLS).find(
|
|
||||||
(protocol) => Command.getProtocolString(protocol) === result.item
|
|
||||||
);
|
|
||||||
valueMapping[PROTOCOL_FIELD][protocolValue] = protocol;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!valueMapping[COMMAND_TYPE_FIELD][commandTypeValue]) {
|
|
||||||
let result = commandTypeFuse.search(commandTypeValue).shift();
|
|
||||||
if (result) {
|
|
||||||
let commandType = Object.values(Command.TYPES).find(
|
|
||||||
(commandType) =>
|
|
||||||
Command.getTypeString(commandType) === result.item
|
|
||||||
);
|
|
||||||
valueMapping[COMMAND_TYPE_FIELD][commandTypeValue] = commandType;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
setValueMapping(valueMapping);
|
|
||||||
setComputingValueMapping(false);
|
|
||||||
})();
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleMapValuesStep() {
|
|
||||||
let protocolField = Object.keys(fieldMapping()).find(
|
|
||||||
(field) => fieldMapping()[field] === PROTOCOL_FIELD
|
|
||||||
);
|
|
||||||
let commandNumberField = Object.keys(fieldMapping()).find(
|
|
||||||
(field) => fieldMapping()[field] === COMMAND_NUMBER_FIELD
|
|
||||||
);
|
|
||||||
let deviceField = Object.keys(fieldMapping()).find(
|
|
||||||
(field) => fieldMapping()[field] === DEVICE_FIELD
|
|
||||||
);
|
|
||||||
let commandTypeField = Object.keys(fieldMapping()).find(
|
|
||||||
(field) => fieldMapping()[field] === COMMAND_TYPE_FIELD
|
|
||||||
);
|
|
||||||
let titleField = Object.keys(fieldMapping()).find(
|
|
||||||
(field) => fieldMapping()[field] === TITLE_FIELD
|
|
||||||
);
|
|
||||||
let commands = csvArray()
|
|
||||||
.map((row) => {
|
|
||||||
let protocol = valueMapping()[PROTOCOL_FIELD][row[protocolField]];
|
|
||||||
if (!protocol) return null;
|
|
||||||
let commandNumber = row[commandNumberField];
|
|
||||||
if (isNaN(commandNumber)) return null;
|
|
||||||
commandNumber = parseInt(commandNumber);
|
|
||||||
let device = row[deviceField];
|
|
||||||
if (isNaN(device)) return null;
|
|
||||||
device = parseInt(device);
|
|
||||||
let commandType =
|
|
||||||
valueMapping()[COMMAND_TYPE_FIELD][row[commandTypeField]];
|
|
||||||
if (!commandType) return null;
|
|
||||||
let title = row[titleField];
|
|
||||||
let command = new Command({
|
|
||||||
protocol,
|
|
||||||
commandNumber,
|
|
||||||
device,
|
|
||||||
commandType,
|
|
||||||
title,
|
|
||||||
});
|
|
||||||
return command;
|
|
||||||
})
|
|
||||||
.filter((command) => command !== null);
|
|
||||||
setCommands(commands);
|
|
||||||
nextStep();
|
|
||||||
}
|
|
||||||
|
|
||||||
function ConfirmStep() {
|
|
||||||
return (
|
|
||||||
<div class="mb-3">
|
|
||||||
<div>Confirm:</div>
|
|
||||||
<div class="d-flex flex-column">
|
|
||||||
{commands().map((command) => (
|
|
||||||
<div class="d-flex flex-row align-items-center justify-content-center mb-2">
|
|
||||||
<div class="col-sm-2">{command.getProtocol()}</div>
|
|
||||||
<div class="col-sm-2">{command.getCommandNumber()}</div>
|
|
||||||
<div class="col-sm-2">{command.getDevice()}</div>
|
|
||||||
<div class="col-sm-2">{command.getCommandType()}</div>
|
|
||||||
<div class="col-sm-2">{command.getTitle()}</div>
|
|
||||||
</div>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function nextStep() {
|
|
||||||
if (currentStep() >= TOTAL_STEPS) return;
|
|
||||||
setError("");
|
|
||||||
setCurrentStep(currentStep() + 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
function previousStep() {
|
|
||||||
if (currentStep() <= 1) return;
|
|
||||||
setError("");
|
|
||||||
setCurrentStep(currentStep() - 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
function resetFields() {
|
|
||||||
setCsvString("");
|
|
||||||
setError("");
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Modal
|
|
||||||
ref={props.ref}
|
|
||||||
id="importCommandsModal"
|
|
||||||
modalTitle="Import Commands from CSV"
|
|
||||||
centered={true}
|
|
||||||
>
|
|
||||||
<div class="modal-body">
|
|
||||||
<Show when={error() !== ""}>
|
|
||||||
<div class="alert alert-danger" role="alert">
|
|
||||||
{error()}
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
<Switch>
|
|
||||||
<Match when={currentStep() === ENTER_CSV_STEP}>
|
|
||||||
<EnterCsvStep />
|
|
||||||
</Match>
|
|
||||||
<Match when={currentStep() === MAP_FIELDS_STEP}>
|
|
||||||
<MapFieldsStep />
|
|
||||||
</Match>
|
|
||||||
<Match when={currentStep() === MAP_VALUES_STEP}>
|
|
||||||
<MapValuesStep />
|
|
||||||
</Match>
|
|
||||||
<Match when={currentStep() === CONFIRM_STEP}>
|
|
||||||
<ConfirmStep />
|
|
||||||
</Match>
|
|
||||||
</Switch>
|
|
||||||
</div>
|
|
||||||
<div class="modal-footer">
|
|
||||||
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
|
|
||||||
Cancel
|
|
||||||
</button>
|
|
||||||
<Show when={currentStep() > 1}>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={previousStep}
|
|
||||||
class="btn btn-secondary"
|
|
||||||
disabled={false}
|
|
||||||
>
|
|
||||||
Back
|
|
||||||
</button>
|
|
||||||
</Show>
|
|
||||||
<Show when={currentStep() < TOTAL_STEPS}>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={handleNextStep}
|
|
||||||
class="btn btn-secondary"
|
|
||||||
disabled={!canMakeNextStep()}
|
|
||||||
>
|
|
||||||
Next
|
|
||||||
</button>
|
|
||||||
</Show>
|
|
||||||
<Show when={currentStep() === TOTAL_STEPS}>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
onClick={handleImportCommands}
|
|
||||||
class="btn btn-primary"
|
|
||||||
disabled={false}
|
|
||||||
>
|
|
||||||
Import
|
|
||||||
</button>
|
|
||||||
</Show>
|
|
||||||
</div>
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
ImportCommandsModal.Handler = new ModalHandler();
|
|
||||||
ImportCommandsModal.onCommandsImported = (callback) =>
|
|
||||||
eventEmitter.on(COMMANDS_IMPORTED_EVENT, callback);
|
|
||||||
|
|
||||||
export default ImportCommandsModal;
|
|
||||||
@ -1,23 +1,11 @@
|
|||||||
import EventEmitter from "../tools/event-emitter";
|
|
||||||
|
|
||||||
const SHOW_EVENT = "show";
|
|
||||||
const HIDE_EVENT = "hide";
|
|
||||||
|
|
||||||
function ModalHandler() {
|
function ModalHandler() {
|
||||||
let _ref;
|
let _ref;
|
||||||
let _modalRef;
|
let _modalRef;
|
||||||
let _modalId;
|
let _modalId;
|
||||||
let eventEmitter = new EventEmitter();
|
|
||||||
|
|
||||||
function setRef(ref) {
|
function setRef(ref) {
|
||||||
_ref = ref;
|
_ref = ref;
|
||||||
_modalRef = new bootstrap.Modal(ref);
|
_modalRef = new bootstrap.Modal(ref);
|
||||||
_ref.addEventListener('hidden.bs.modal', () => {
|
|
||||||
eventEmitter.dispatchEvent(HIDE_EVENT);
|
|
||||||
});
|
|
||||||
_ref.addEventListener('show.bs.modal', () => {
|
|
||||||
eventEmitter.dispatchEvent(SHOW_EVENT);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function show() {
|
function show() {
|
||||||
@ -33,11 +21,15 @@ function ModalHandler() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function onHidden(callback) {
|
function onHidden(callback) {
|
||||||
eventEmitter.on(HIDE_EVENT, callback);
|
_ref.addEventListener('hidden.bs.modal', () => {
|
||||||
|
callback();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function onShow(callback) {
|
function onShow(callback) {
|
||||||
eventEmitter.on(SHOW_EVENT, callback);
|
_ref.addEventListener('show.bs.modal', () => {
|
||||||
|
callback();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@ -6,13 +6,6 @@ import UserSettingsModal from "./user-settings-modal.jsx";
|
|||||||
import CreateDeviceModal from "./create-device-modal.jsx";
|
import CreateDeviceModal from "./create-device-modal.jsx";
|
||||||
import ShowRegistrationCodeModal from "./show-registration-code-modal.jsx";
|
import ShowRegistrationCodeModal from "./show-registration-code-modal.jsx";
|
||||||
import DeleteIntegrationModal from "./delete-integration-modal.jsx";
|
import DeleteIntegrationModal from "./delete-integration-modal.jsx";
|
||||||
import CreateCommandModal from "./create-command-modal.jsx";
|
|
||||||
import DeleteCommandModal from "./delete-command-modal.jsx";
|
|
||||||
import CreateRemoteModal from "./create-remote-modal.jsx";
|
|
||||||
import DeleteRemoteModal from "./delete-remote-modal.jsx";
|
|
||||||
import ImportCommandsModal from "./import-commands-modal.jsx";
|
|
||||||
import EditCommandModal from "./edit-command-modal.jsx";
|
|
||||||
import EditRemoteModal from "./edit-remote-modal.jsx";
|
|
||||||
|
|
||||||
const ModalRegistry = (function () {
|
const ModalRegistry = (function () {
|
||||||
const modals = [
|
const modals = [
|
||||||
@ -46,41 +39,6 @@ const ModalRegistry = (function () {
|
|||||||
component: DeleteIntegrationModal,
|
component: DeleteIntegrationModal,
|
||||||
ref: null,
|
ref: null,
|
||||||
},
|
},
|
||||||
{
|
|
||||||
id: "newCommandModal",
|
|
||||||
component: CreateCommandModal,
|
|
||||||
ref: null,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "deleteCommandModal",
|
|
||||||
component: DeleteCommandModal,
|
|
||||||
ref: null,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "createRemoteModal",
|
|
||||||
component: CreateRemoteModal,
|
|
||||||
ref: null,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "deleteRemoteModal",
|
|
||||||
component: DeleteRemoteModal,
|
|
||||||
ref: null,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "importCommandsModal",
|
|
||||||
component: ImportCommandsModal,
|
|
||||||
ref: null,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "editCommandModal",
|
|
||||||
component: EditCommandModal,
|
|
||||||
ref: null,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "editRemoteModal",
|
|
||||||
component: EditRemoteModal,
|
|
||||||
ref: null,
|
|
||||||
}
|
|
||||||
];
|
];
|
||||||
|
|
||||||
function getModals(props) {
|
function getModals(props) {
|
||||||
|
|||||||
@ -7,7 +7,7 @@ import {
|
|||||||
import EventEmitter from "../tools/event-emitter.js";
|
import EventEmitter from "../tools/event-emitter.js";
|
||||||
import User from "../data/user.js";
|
import User from "../data/user.js";
|
||||||
import Modal from "./modal.jsx";
|
import Modal from "./modal.jsx";
|
||||||
import ValidatedTextInput from "../components/validated-text-input.jsx";
|
import ValidatedTextInput from "../modules/validated-text-input.jsx";
|
||||||
import ModalHandler from "./modal-handler.js";
|
import ModalHandler from "./modal-handler.js";
|
||||||
import UserService from "../services/user-service.js";
|
import UserService from "../services/user-service.js";
|
||||||
|
|
||||||
|
|||||||
@ -8,17 +8,7 @@ import {
|
|||||||
} from "solid-js";
|
} from "solid-js";
|
||||||
|
|
||||||
function List(props) {
|
function List(props) {
|
||||||
props = mergeProps(
|
props = mergeProps({ items: [], showHeader: true, selectable: true }, props);
|
||||||
{
|
|
||||||
items: [],
|
|
||||||
showHeader: true,
|
|
||||||
selectable: true,
|
|
||||||
onListItemsSelect: () => {},
|
|
||||||
onLazyLoad: () => {},
|
|
||||||
onListItemClick: () => {},
|
|
||||||
},
|
|
||||||
props
|
|
||||||
);
|
|
||||||
const [listItems, setListItems] = createSignal([]);
|
const [listItems, setListItems] = createSignal([]);
|
||||||
const selectedItems = createMemo(() =>
|
const selectedItems = createMemo(() =>
|
||||||
listItems()
|
listItems()
|
||||||
@ -41,13 +31,16 @@ function List(props) {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
createEffect(() => {
|
createEffect(() => {
|
||||||
|
if (!props.onListItemsSelect) return;
|
||||||
props.onListItemsSelect(selectedItems());
|
props.onListItemsSelect(selectedItems());
|
||||||
});
|
});
|
||||||
|
|
||||||
const observer = new IntersectionObserver((entries) => {
|
const observer = new IntersectionObserver((entries) => {
|
||||||
entries.forEach((entry) => {
|
entries.forEach((entry) => {
|
||||||
if (entry.isIntersecting) {
|
if (entry.isIntersecting) {
|
||||||
props.onLazyLoad();
|
if (props.onLazyLoad) {
|
||||||
|
props.onLazyLoad();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -68,6 +61,7 @@ function List(props) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
function handleListItemClick(item) {
|
function handleListItemClick(item) {
|
||||||
|
if (!props.onListItemClick) return;
|
||||||
props.onListItemClick(item);
|
props.onListItemClick(item);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2,13 +2,7 @@ import { createSignal, mergeProps } from "solid-js";
|
|||||||
|
|
||||||
function ValidatedTextInput(props) {
|
function ValidatedTextInput(props) {
|
||||||
props = mergeProps(
|
props = mergeProps(
|
||||||
{
|
{ type: "text", valid: true, onInput: () => {}, errorText: "" },
|
||||||
type: "text",
|
|
||||||
valid: true,
|
|
||||||
onInput: () => {},
|
|
||||||
errorText: "",
|
|
||||||
placeholder: "",
|
|
||||||
},
|
|
||||||
props
|
props
|
||||||
);
|
);
|
||||||
let [isActive, setActive] = createSignal(false);
|
let [isActive, setActive] = createSignal(false);
|
||||||
@ -21,7 +15,6 @@ function ValidatedTextInput(props) {
|
|||||||
}
|
}
|
||||||
id={props.id}
|
id={props.id}
|
||||||
value={props.value}
|
value={props.value}
|
||||||
placeholder={props.placeholder}
|
|
||||||
onInput={props.onInput}
|
onInput={props.onInput}
|
||||||
onFocusOut={() => setActive(true)}
|
onFocusOut={() => setActive(true)}
|
||||||
/>
|
/>
|
||||||
@ -66,8 +66,8 @@ function DeviceService() {
|
|||||||
|
|
||||||
async function getRegistrationCode() {
|
async function getRegistrationCode() {
|
||||||
let response = await Net.sendRequest({
|
let response = await Net.sendRequest({
|
||||||
method: "POST",
|
method: "GET",
|
||||||
url: "/api/integrations",
|
url: "/api/integrations/register",
|
||||||
});
|
});
|
||||||
|
|
||||||
if (response.status !== 200) {
|
if (response.status !== 200) {
|
||||||
@ -79,23 +79,6 @@ function DeviceService() {
|
|||||||
return codeObject.code;
|
return codeObject.code;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getIntegration(id) {
|
|
||||||
if (!id) return null;
|
|
||||||
let response = await Net.sendRequest({
|
|
||||||
method: "GET",
|
|
||||||
url: "/api/integrations/" + id,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.status !== 200) {
|
|
||||||
let responseData = JSON.parse(response.data);
|
|
||||||
throw new Error(responseData.error);
|
|
||||||
}
|
|
||||||
|
|
||||||
let integration = JSON.parse(response.data);
|
|
||||||
integration = Serializer.deserializeIntegration(integration);
|
|
||||||
return integration;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getIntegrations() {
|
async function getIntegrations() {
|
||||||
let response = await Net.sendRequest({
|
let response = await Net.sendRequest({
|
||||||
method: "GET",
|
method: "GET",
|
||||||
@ -130,7 +113,6 @@ function DeviceService() {
|
|||||||
updateDevice,
|
updateDevice,
|
||||||
deleteDevice,
|
deleteDevice,
|
||||||
getRegistrationCode,
|
getRegistrationCode,
|
||||||
getIntegration,
|
|
||||||
getIntegrations,
|
getIntegrations,
|
||||||
deleteIntegration,
|
deleteIntegration,
|
||||||
};
|
};
|
||||||
|
|||||||
@ -1,163 +0,0 @@
|
|||||||
import Serializer from "../data/serializer";
|
|
||||||
import Net from "../tools/net";
|
|
||||||
import WebRTCService from "./webrtc-service";
|
|
||||||
|
|
||||||
const MESSAGE_TYPE_COMMAND = "command";
|
|
||||||
|
|
||||||
function RemoteService() {
|
|
||||||
async function getRemote(remoteId) {
|
|
||||||
if (!remoteId) return null;
|
|
||||||
let response = await Net.sendRequest({
|
|
||||||
method: "GET",
|
|
||||||
url: "/api/remotes/" + remoteId,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.status !== 200) {
|
|
||||||
let responseData = JSON.parse(response.data);
|
|
||||||
throw new Error(responseData.error);
|
|
||||||
}
|
|
||||||
|
|
||||||
let remoteObject = JSON.parse(response.data);
|
|
||||||
return Serializer.deserializeRemote(remoteObject);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getRemotes() {
|
|
||||||
let response = await Net.sendRequest({
|
|
||||||
method: "GET",
|
|
||||||
url: "/api/remotes",
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.status !== 200) {
|
|
||||||
let responseData = JSON.parse(response.data);
|
|
||||||
throw new Error(responseData.error);
|
|
||||||
}
|
|
||||||
|
|
||||||
let remoteObjects = JSON.parse(response.data);
|
|
||||||
return Serializer.deserializeRemotes(remoteObjects);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function createRemote(remote) {
|
|
||||||
let remoteObject = Serializer.serializeRemote(remote);
|
|
||||||
let response = await Net.sendJsonRequest({
|
|
||||||
method: "POST",
|
|
||||||
url: "/api/remotes",
|
|
||||||
data: remoteObject,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.status !== 200) {
|
|
||||||
let responseData = JSON.parse(response.data);
|
|
||||||
throw new Error(responseData.error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function updateRemote(remote) {
|
|
||||||
let remoteObject = Serializer.serializeRemote(remote);
|
|
||||||
let response = await Net.sendJsonRequest({
|
|
||||||
method: "PUT",
|
|
||||||
url: "/api/remotes/" + remote.getId(),
|
|
||||||
data: remoteObject,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.status !== 200) {
|
|
||||||
let responseData = JSON.parse(response.data);
|
|
||||||
throw new Error(responseData.error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function deleteRemote(remoteId) {
|
|
||||||
let response = await Net.sendRequest({
|
|
||||||
method: "DELETE",
|
|
||||||
url: "/api/remotes/" + remoteId,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.status !== 200) {
|
|
||||||
let responseData = JSON.parse(response.data);
|
|
||||||
throw new Error(responseData.error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getCommands() {
|
|
||||||
let response = await Net.sendRequest({
|
|
||||||
method: "GET",
|
|
||||||
url: "/api/commands",
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.status !== 200) {
|
|
||||||
let responseData = JSON.parse(response.data);
|
|
||||||
throw new Error(responseData.error);
|
|
||||||
}
|
|
||||||
|
|
||||||
let commandObjects = JSON.parse(response.data);
|
|
||||||
return Serializer.deserializeCommands(commandObjects);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function createCommand(command) {
|
|
||||||
return createCommands([command]);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function createCommands(commands) {
|
|
||||||
let commandObjects = Serializer.serializeCommands(commands);
|
|
||||||
let response = await Net.sendJsonRequest({
|
|
||||||
method: "POST",
|
|
||||||
url: "/api/commands",
|
|
||||||
data: commandObjects,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.status !== 200) {
|
|
||||||
let responseData = JSON.parse(response.data);
|
|
||||||
throw new Error(responseData.error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function updateCommand(command) {
|
|
||||||
let commandObject = Serializer.serializeCommand(command);
|
|
||||||
let response = await Net.sendJsonRequest({
|
|
||||||
method: "PUT",
|
|
||||||
url: "/api/commands/" + command.getId(),
|
|
||||||
data: commandObject,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.status !== 200) {
|
|
||||||
let responseData = JSON.parse(response.data);
|
|
||||||
throw new Error(responseData.error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function deleteCommand(commandId) {
|
|
||||||
let response = await Net.sendRequest({
|
|
||||||
method: "DELETE",
|
|
||||||
url: "/api/commands/" + commandId,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (response.status !== 200) {
|
|
||||||
let responseData = JSON.parse(response.data);
|
|
||||||
throw new Error(responseData.error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function sendCommand(command) {
|
|
||||||
let commandObject = Serializer.serializeCommand(command);
|
|
||||||
WebRTCService.sendDataJson({
|
|
||||||
type: MESSAGE_TYPE_COMMAND,
|
|
||||||
data: commandObject,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
getRemote,
|
|
||||||
getRemotes,
|
|
||||||
createRemote,
|
|
||||||
updateRemote,
|
|
||||||
deleteRemote,
|
|
||||||
getCommands,
|
|
||||||
createCommand,
|
|
||||||
createCommands,
|
|
||||||
updateCommand,
|
|
||||||
deleteCommand,
|
|
||||||
sendCommand,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
RemoteService = new RemoteService();
|
|
||||||
|
|
||||||
export default RemoteService;
|
|
||||||
@ -1,191 +0,0 @@
|
|||||||
import EventEmitter from "../tools/event-emitter";
|
|
||||||
import WebsocketService from "./websocket-service";
|
|
||||||
|
|
||||||
function WebRTCService() {
|
|
||||||
const TYPE_SIGNALING = "signaling";
|
|
||||||
const STATE_CONNECTED = "connected";
|
|
||||||
const STATE_DISCONNECTED = "disconnected";
|
|
||||||
const STATE_CLOSED = "closed";
|
|
||||||
const STATE_FAILED = "failed";
|
|
||||||
const ICE_CONNECTION_STATE_CHANGE_EVENT = "iceconnectionstatechange";
|
|
||||||
const DATA_CHANNEL_OPEN_EVENT = "datachannelopen";
|
|
||||||
|
|
||||||
let videoElement;
|
|
||||||
let peerConnection;
|
|
||||||
let peerId;
|
|
||||||
let dataChannel;
|
|
||||||
|
|
||||||
let eventEmitter = new EventEmitter();
|
|
||||||
|
|
||||||
WebsocketService.onMessage((data) => {
|
|
||||||
let dataObject = JSON.parse(data);
|
|
||||||
let sender = dataObject.sender;
|
|
||||||
if (sender !== peerId) return;
|
|
||||||
let message = dataObject.message;
|
|
||||||
handleSignalingMessage(peerId, message);
|
|
||||||
});
|
|
||||||
|
|
||||||
async function connect(targetId) {
|
|
||||||
peerId = targetId;
|
|
||||||
let configuration = getConfiguration();
|
|
||||||
peerConnection = new RTCPeerConnection(configuration);
|
|
||||||
console.log("ICE connection state:" + peerConnection.iceConnectionState);
|
|
||||||
peerConnection.addEventListener("iceconnectionstatechange", (event) => {
|
|
||||||
let state = peerConnection.iceConnectionState;
|
|
||||||
console.log("ICE connection state changed to:", state);
|
|
||||||
eventEmitter.dispatchEvent(ICE_CONNECTION_STATE_CHANGE_EVENT, state);
|
|
||||||
if (state === STATE_CONNECTED) {
|
|
||||||
videoElement.play();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
peerConnection.addEventListener("signalingstatechange", (event) => {
|
|
||||||
console.log("Signaling state changed to:", event.target.signalingState);
|
|
||||||
});
|
|
||||||
peerConnection.onicecandidate = (event) => {
|
|
||||||
if (event.candidate) {
|
|
||||||
sendIceCandidate(targetId, event.candidate);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
peerConnection.ontrack = (event) => {
|
|
||||||
console.log("Received track:", event.track);
|
|
||||||
if (videoElement) {
|
|
||||||
videoElement.srcObject.addTrack(event.track);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
peerConnection.onicecandidateerror = (event) => {
|
|
||||||
console.error("ICE candidate error:", event);
|
|
||||||
};
|
|
||||||
peerConnection.onnegotiationneeded = () => {
|
|
||||||
console.log("Negotiation needed");
|
|
||||||
negotiate(targetId);
|
|
||||||
};
|
|
||||||
dataChannel = peerConnection.createDataChannel("data");
|
|
||||||
dataChannel.addEventListener("open", () => {
|
|
||||||
eventEmitter.dispatchEvent(DATA_CHANNEL_OPEN_EVENT);
|
|
||||||
});
|
|
||||||
negotiate(targetId);
|
|
||||||
}
|
|
||||||
|
|
||||||
function disconnect() {
|
|
||||||
peerConnection.close();
|
|
||||||
eventEmitter.dispatchEvent(
|
|
||||||
ICE_CONNECTION_STATE_CHANGE_EVENT,
|
|
||||||
peerConnection.iceConnectionState
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function negotiate(targetId) {
|
|
||||||
try {
|
|
||||||
let offer = await peerConnection.createOffer({
|
|
||||||
offerToReceiveAudio: true,
|
|
||||||
offerToReceiveVideo: true,
|
|
||||||
});
|
|
||||||
console.log("Created offer:", offer);
|
|
||||||
await peerConnection.setLocalDescription(offer);
|
|
||||||
sendOffer(targetId, offer);
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error creating offer:", error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function sendOffer(integrationId, offer) {
|
|
||||||
WebsocketService.sendJson({
|
|
||||||
type: TYPE_SIGNALING,
|
|
||||||
target: integrationId,
|
|
||||||
message: offer,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function sendIceCandidate(integrationId, candidate) {
|
|
||||||
console.log("Sending ICE candidate:", candidate);
|
|
||||||
WebsocketService.sendJson({
|
|
||||||
type: TYPE_SIGNALING,
|
|
||||||
target: integrationId,
|
|
||||||
message: {
|
|
||||||
type: "ice_candidate",
|
|
||||||
candidate: candidate,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleSignalingMessage(targetId, message) {
|
|
||||||
//console.log("Received message:", message);
|
|
||||||
switch (message.type) {
|
|
||||||
case "answer":
|
|
||||||
handleAnswer(message);
|
|
||||||
break;
|
|
||||||
case "ice_candidate":
|
|
||||||
handleIceCandidate(message);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleAnswer(answer) {
|
|
||||||
console.log("Remote answer:", answer);
|
|
||||||
if (peerConnection.signalingState === "stable") return;
|
|
||||||
peerConnection.setRemoteDescription(new RTCSessionDescription(answer));
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleIceCandidate(message) {
|
|
||||||
let iceCandidate = new RTCIceCandidate(message.candidate);
|
|
||||||
console.log("Received ICE candidate:", iceCandidate);
|
|
||||||
peerConnection.addIceCandidate(iceCandidate);
|
|
||||||
}
|
|
||||||
|
|
||||||
function sendDataString(data) {
|
|
||||||
if (!dataChannel) return;
|
|
||||||
if (dataChannel.readyState !== "open") return;
|
|
||||||
dataChannel.send(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
function sendDataJson(data) {
|
|
||||||
let dataJson = JSON.stringify(data);
|
|
||||||
sendDataString(dataJson);
|
|
||||||
}
|
|
||||||
|
|
||||||
function onStateChanged(callback) {
|
|
||||||
eventEmitter.on(ICE_CONNECTION_STATE_CHANGE_EVENT, () => {
|
|
||||||
callback(peerConnection.iceConnectionState);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function onDataChannelOpen(callback) {
|
|
||||||
eventEmitter.on(DATA_CHANNEL_OPEN_EVENT, callback);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getConfiguration() {
|
|
||||||
return {
|
|
||||||
iceServers: [{ urls: "stun:stun.l.google.com:19302" }],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function setVideoElement(element) {
|
|
||||||
videoElement = element;
|
|
||||||
videoElement.srcObject = new MediaStream();
|
|
||||||
}
|
|
||||||
|
|
||||||
function getVideoElement() {
|
|
||||||
return videoElement;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
STATE_CONNECTED,
|
|
||||||
STATE_DISCONNECTED,
|
|
||||||
STATE_CLOSED,
|
|
||||||
STATE_FAILED,
|
|
||||||
connect,
|
|
||||||
disconnect,
|
|
||||||
setVideoElement,
|
|
||||||
getVideoElement,
|
|
||||||
sendDataString,
|
|
||||||
sendDataJson,
|
|
||||||
onStateChanged,
|
|
||||||
onDataChannelOpen,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
WebRTCService = new WebRTCService();
|
|
||||||
|
|
||||||
export default WebRTCService;
|
|
||||||
@ -1,40 +0,0 @@
|
|||||||
function WebsocketService() {
|
|
||||||
let websocket;
|
|
||||||
|
|
||||||
initialize();
|
|
||||||
function initialize() {
|
|
||||||
let url = "ws://" + location.host + "/ws";
|
|
||||||
websocket = new WebSocket(url);
|
|
||||||
}
|
|
||||||
|
|
||||||
function onMessage(callback) {
|
|
||||||
if (!websocket) throw new Error("Websocket is not open");
|
|
||||||
websocket.addEventListener("message", event => {
|
|
||||||
callback(event.data);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function sendMessage(message) {
|
|
||||||
if (!isWebsocketOpen()) throw new Error("Websocket is not open");
|
|
||||||
websocket.send(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
function sendJson(jsonObject) {
|
|
||||||
let jsonString = JSON.stringify(jsonObject);
|
|
||||||
sendMessage(jsonString);
|
|
||||||
}
|
|
||||||
|
|
||||||
function isWebsocketOpen() {
|
|
||||||
return websocket.readyState === WebSocket.OPEN;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
onMessage,
|
|
||||||
sendJson,
|
|
||||||
sendMessage,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
WebsocketService = new WebsocketService();
|
|
||||||
|
|
||||||
export default WebsocketService;
|
|
||||||
@ -1,30 +0,0 @@
|
|||||||
function FileUtils() {
|
|
||||||
function createJsonFile(jsonData, fileName) {
|
|
||||||
const json = JSON.stringify(jsonData, null, 2);
|
|
||||||
const file = new File([json], fileName, {
|
|
||||||
type: "text/json;charset=utf-8",
|
|
||||||
});
|
|
||||||
return file;
|
|
||||||
}
|
|
||||||
|
|
||||||
function downloadFile(file) {
|
|
||||||
console.log(file)
|
|
||||||
const url = URL.createObjectURL(file);
|
|
||||||
const link = document.createElement("a");
|
|
||||||
link.setAttribute("href", url);
|
|
||||||
link.setAttribute("download", file.name);
|
|
||||||
link.style.visibility = "hidden";
|
|
||||||
document.body.appendChild(link);
|
|
||||||
link.click();
|
|
||||||
document.body.removeChild(link);
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
createJsonFile,
|
|
||||||
downloadFile,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
FileUtils = new FileUtils();
|
|
||||||
|
|
||||||
export default FileUtils;
|
|
||||||
@ -1,5 +1,5 @@
|
|||||||
import { createSignal, onMount } from "solid-js";
|
import { createSignal, onMount } from "solid-js";
|
||||||
import Spinner from "../components/spinner";
|
import Spinner from "../modules/spinner";
|
||||||
import MainView from "./main-view";
|
import MainView from "./main-view";
|
||||||
import LoginView from "./login-view";
|
import LoginView from "./login-view";
|
||||||
import UserService from "../services/user-service";
|
import UserService from "../services/user-service";
|
||||||
|
|||||||
192
www/src/views/devices-view.jsx
Normal file
192
www/src/views/devices-view.jsx
Normal file
@ -0,0 +1,192 @@
|
|||||||
|
import {
|
||||||
|
createResource,
|
||||||
|
createSignal,
|
||||||
|
mergeProps,
|
||||||
|
onMount,
|
||||||
|
Show,
|
||||||
|
} from "solid-js";
|
||||||
|
|
||||||
|
import List from "../modules/list";
|
||||||
|
import CreateDeviceModal from "../modals/create-device-modal";
|
||||||
|
import DeviceService from "../services/device-service";
|
||||||
|
import ShowRegistrationCodeModal from "../modals/show-registration-code-modal";
|
||||||
|
import DeleteIntegrationModal from "../modals/delete-integration-modal";
|
||||||
|
|
||||||
|
function DevicesView(props) {
|
||||||
|
props = mergeProps({ onIntegrationClicked: () => {} }, props);
|
||||||
|
const DEVICES_LIST_TYPE = "devices";
|
||||||
|
const INTEGRATION_LIST_TYPE = "integrations";
|
||||||
|
|
||||||
|
const [listType, setListType] = createSignal(INTEGRATION_LIST_TYPE);
|
||||||
|
const [devices, setDevices] = createSignal([]);
|
||||||
|
const [integrations, { refetch: refetchIntegrations }] = createResource(
|
||||||
|
DeviceService.getIntegrations
|
||||||
|
);
|
||||||
|
|
||||||
|
CreateDeviceModal.onDeviceCreated(() => {
|
||||||
|
handleRefreshDevices();
|
||||||
|
});
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
handleRefreshDevices();
|
||||||
|
DeleteIntegrationModal.onIntegrationDeleted(() => {
|
||||||
|
refetchIntegrations();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function handleNewDevice() {
|
||||||
|
CreateDeviceModal.Handler.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function handleRefreshDevices() {
|
||||||
|
let devices = await DeviceService.getDevices();
|
||||||
|
setDevices(devices);
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleRegisterIntegration() {
|
||||||
|
ShowRegistrationCodeModal.Handler.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleDeleteIntegration(integration) {
|
||||||
|
DeleteIntegrationModal.setIntegration(integration);
|
||||||
|
DeleteIntegrationModal.Handler.show();
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleIntegrationItemClicked(item) {
|
||||||
|
props.onIntegrationClicked(item.integration);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
class={
|
||||||
|
"d-flex flex-column overflow-hidden flex-fill px-3 pb-2 pt-3 " +
|
||||||
|
props.class
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<div class="d-flex flex-row">
|
||||||
|
<div class="d-flex flex-row flex-fill">
|
||||||
|
<button
|
||||||
|
class={
|
||||||
|
"btn me-2 mb-3" +
|
||||||
|
(listType() === INTEGRATION_LIST_TYPE
|
||||||
|
? " btn-secondary"
|
||||||
|
: " btn-dark")
|
||||||
|
}
|
||||||
|
onClick={() => setListType(INTEGRATION_LIST_TYPE)}
|
||||||
|
>
|
||||||
|
<i class="bi bi-gear me-2"></i>
|
||||||
|
Integrations
|
||||||
|
</button>
|
||||||
|
<button
|
||||||
|
class={
|
||||||
|
"btn me-2 mb-3" +
|
||||||
|
(listType() === DEVICES_LIST_TYPE
|
||||||
|
? " btn-secondary"
|
||||||
|
: " btn-dark")
|
||||||
|
}
|
||||||
|
onClick={() => setListType(DEVICES_LIST_TYPE)}
|
||||||
|
>
|
||||||
|
<i class="bi bi-tv me-2"></i>
|
||||||
|
Devices
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="d-flex flex-row justify-content-end flex-fill">
|
||||||
|
<Show when={listType() === DEVICES_LIST_TYPE}>
|
||||||
|
<button class="btn btn-dark me-2 mb-3" onClick={handleNewDevice}>
|
||||||
|
<i class="bi bi-plus-square me-2"></i>
|
||||||
|
New Device
|
||||||
|
</button>
|
||||||
|
</Show>
|
||||||
|
<Show when={listType() === INTEGRATION_LIST_TYPE}>
|
||||||
|
<button
|
||||||
|
class="btn btn-dark me-2 mb-3"
|
||||||
|
onClick={handleRegisterIntegration}
|
||||||
|
>
|
||||||
|
<i class="bi bi-plus-square me-2"></i>
|
||||||
|
Register
|
||||||
|
</button>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Show when={listType() === DEVICES_LIST_TYPE}>
|
||||||
|
<List
|
||||||
|
onListItemClick={() => {}}
|
||||||
|
items={devices().map((device) => ({
|
||||||
|
id: {
|
||||||
|
html: <span class="font-monospace">{device.getId()}</span>,
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
text: device.getName(),
|
||||||
|
},
|
||||||
|
description: {
|
||||||
|
text: device.getDescription(),
|
||||||
|
},
|
||||||
|
device,
|
||||||
|
}))}
|
||||||
|
class={"flex-fill"}
|
||||||
|
columns={[
|
||||||
|
{
|
||||||
|
id: "id",
|
||||||
|
name: "id",
|
||||||
|
width: 6,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "name",
|
||||||
|
name: "Name",
|
||||||
|
width: 10,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "description",
|
||||||
|
name: "Description",
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
></List>
|
||||||
|
</Show>
|
||||||
|
<Show when={listType() === INTEGRATION_LIST_TYPE}>
|
||||||
|
<List
|
||||||
|
onListItemClick={handleIntegrationItemClicked}
|
||||||
|
items={(integrations() || []).map((integration) => ({
|
||||||
|
id: {
|
||||||
|
html: <span class="font-monospace">{integration.getId()}</span>,
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
text: integration.getName(),
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
html: (
|
||||||
|
<>
|
||||||
|
<button
|
||||||
|
class="btn btn-outline-secondary me-2"
|
||||||
|
onClick={() => handleDeleteIntegration(integration)}
|
||||||
|
>
|
||||||
|
<i class="bi bi-trash-fill"></i>
|
||||||
|
</button>
|
||||||
|
</>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
integration: integration,
|
||||||
|
}))}
|
||||||
|
class={"flex-fill"}
|
||||||
|
columns={[
|
||||||
|
{
|
||||||
|
id: "id",
|
||||||
|
name: "id",
|
||||||
|
width: 6,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "name",
|
||||||
|
name: "Name",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "options",
|
||||||
|
name: "",
|
||||||
|
width: 4,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
></List>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default DevicesView;
|
||||||
@ -1,76 +0,0 @@
|
|||||||
import { createSignal } from "solid-js";
|
|
||||||
import List from "../../components/list";
|
|
||||||
import CreateDeviceModal from "../../modals/create-device-modal";
|
|
||||||
import DeviceService from "../../services/device-service";
|
|
||||||
|
|
||||||
function DevicesListView(props) {
|
|
||||||
const [devices, setDevices] = createSignal([]);
|
|
||||||
|
|
||||||
handleRefreshDevices();
|
|
||||||
|
|
||||||
CreateDeviceModal.onDeviceCreated(() => {
|
|
||||||
handleRefreshDevices();
|
|
||||||
});
|
|
||||||
|
|
||||||
function handleNewDevice() {
|
|
||||||
CreateDeviceModal.Handler.show();
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleRefreshDevices() {
|
|
||||||
let devices = await DeviceService.getDevices();
|
|
||||||
setDevices(devices);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
class={
|
|
||||||
"d-flex flex-column overflow-hidden flex-fill px-3 pb-2 pt-3 " +
|
|
||||||
props.class
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<div class="d-flex flex-row">
|
|
||||||
{props.navigation}
|
|
||||||
<div class="d-flex flex-row justify-content-end flex-fill">
|
|
||||||
<button class="btn btn-dark me-2 mb-3" onClick={handleNewDevice}>
|
|
||||||
<i class="bi bi-plus-square me-2"></i>
|
|
||||||
New Device
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<List
|
|
||||||
onListItemClick={() => {}}
|
|
||||||
items={devices().map((device) => ({
|
|
||||||
id: {
|
|
||||||
html: <span class="font-monospace">{device.getId()}</span>,
|
|
||||||
},
|
|
||||||
name: {
|
|
||||||
text: device.getName(),
|
|
||||||
},
|
|
||||||
description: {
|
|
||||||
text: device.getDescription(),
|
|
||||||
},
|
|
||||||
device,
|
|
||||||
}))}
|
|
||||||
class={"flex-fill"}
|
|
||||||
columns={[
|
|
||||||
{
|
|
||||||
id: "id",
|
|
||||||
name: "id",
|
|
||||||
width: 6,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "name",
|
|
||||||
name: "Name",
|
|
||||||
width: 10,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "description",
|
|
||||||
name: "Description",
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
></List>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default DevicesListView;
|
|
||||||
@ -1,105 +0,0 @@
|
|||||||
import {
|
|
||||||
createEffect,
|
|
||||||
createSignal,
|
|
||||||
Match,
|
|
||||||
mergeProps,
|
|
||||||
onCleanup,
|
|
||||||
Switch,
|
|
||||||
} from "solid-js";
|
|
||||||
|
|
||||||
import DevicesListView from "./devices-list-view";
|
|
||||||
import IntegrationListView from "./integration-list-view";
|
|
||||||
import UrlUtils from "../../tools/url-utils";
|
|
||||||
import IntegrationView from "./integration-view";
|
|
||||||
|
|
||||||
function DevicesView(props) {
|
|
||||||
props = mergeProps({ onIntegrationClicked: () => {} }, props);
|
|
||||||
const DEVICES_LIST_VIEW = "devices";
|
|
||||||
const INTEGRATION_LIST_VIEW = "integrations";
|
|
||||||
const INTEGRATION_VIEW = "integration";
|
|
||||||
const VIEWS = [DEVICES_LIST_VIEW, INTEGRATION_LIST_VIEW, INTEGRATION_VIEW];
|
|
||||||
|
|
||||||
const [currentView, setCurrentView] = createSignal(INTEGRATION_LIST_VIEW);
|
|
||||||
|
|
||||||
createEffect(() => {
|
|
||||||
let url = UrlUtils.getUrl();
|
|
||||||
url = UrlUtils.addQueryParameter(url, "tab", currentView());
|
|
||||||
UrlUtils.setUrl(url);
|
|
||||||
});
|
|
||||||
|
|
||||||
onCleanup(() => {
|
|
||||||
window.removeEventListener("popstate", setViewFromUrl);
|
|
||||||
});
|
|
||||||
|
|
||||||
setViewFromUrl();
|
|
||||||
window.addEventListener("popstate", setViewFromUrl);
|
|
||||||
|
|
||||||
function setViewFromUrl() {
|
|
||||||
let view = UrlUtils.getQueryParameter("tab");
|
|
||||||
if (!view) return;
|
|
||||||
if (!VIEWS.includes(view)) return;
|
|
||||||
setCurrentView(view);
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleIntegrationClicked(integration) {
|
|
||||||
IntegrationView.setIntegration(integration);
|
|
||||||
setCurrentView(INTEGRATION_VIEW);
|
|
||||||
}
|
|
||||||
|
|
||||||
function Navigation() {
|
|
||||||
return (
|
|
||||||
<div class="d-flex flex-row flex-fill">
|
|
||||||
<button
|
|
||||||
class={
|
|
||||||
"btn me-2 mb-3" +
|
|
||||||
(currentView() === INTEGRATION_LIST_VIEW
|
|
||||||
? " btn-secondary"
|
|
||||||
: " btn-dark")
|
|
||||||
}
|
|
||||||
onClick={() => setCurrentView(INTEGRATION_LIST_VIEW)}
|
|
||||||
>
|
|
||||||
<i class="bi bi-gear me-2"></i>
|
|
||||||
Integrations
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class={
|
|
||||||
"btn me-2 mb-3" +
|
|
||||||
(currentView() === DEVICES_LIST_VIEW
|
|
||||||
? " btn-secondary"
|
|
||||||
: " btn-dark")
|
|
||||||
}
|
|
||||||
onClick={() => setCurrentView(DEVICES_LIST_VIEW)}
|
|
||||||
>
|
|
||||||
<i class="bi bi-tv me-2"></i>
|
|
||||||
Devices
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
class={
|
|
||||||
"d-flex flex-column overflow-hidden " +
|
|
||||||
props.class
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Switch>
|
|
||||||
<Match when={currentView() === INTEGRATION_LIST_VIEW}>
|
|
||||||
<IntegrationListView
|
|
||||||
navigation={<Navigation />}
|
|
||||||
onIntegrationClicked={handleIntegrationClicked}
|
|
||||||
/>
|
|
||||||
</Match>
|
|
||||||
<Match when={currentView() === DEVICES_LIST_VIEW}>
|
|
||||||
<DevicesListView navigation={<Navigation />} />
|
|
||||||
</Match>
|
|
||||||
<Match when={currentView() === INTEGRATION_VIEW}>
|
|
||||||
<IntegrationView />
|
|
||||||
</Match>
|
|
||||||
</Switch>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default DevicesView;
|
|
||||||
@ -1,103 +0,0 @@
|
|||||||
import { createResource, mergeProps } from "solid-js";
|
|
||||||
import List from "../../components/list";
|
|
||||||
import ShowRegistrationCodeModal from "../../modals/show-registration-code-modal";
|
|
||||||
import DeleteIntegrationModal from "../../modals/delete-integration-modal";
|
|
||||||
import DeviceService from "../../services/device-service";
|
|
||||||
|
|
||||||
function IntegrationListView(props) {
|
|
||||||
props = mergeProps(
|
|
||||||
{
|
|
||||||
onIntegrationClicked: () => {},
|
|
||||||
},
|
|
||||||
props
|
|
||||||
);
|
|
||||||
const [integrations, { refetch: refetchIntegrations }] = createResource(
|
|
||||||
DeviceService.getIntegrations,
|
|
||||||
{ initialValue: [] }
|
|
||||||
);
|
|
||||||
|
|
||||||
function handleRegisterIntegration() {
|
|
||||||
ShowRegistrationCodeModal.Handler.show();
|
|
||||||
}
|
|
||||||
|
|
||||||
DeleteIntegrationModal.onIntegrationDeleted(() => {
|
|
||||||
refetchIntegrations();
|
|
||||||
});
|
|
||||||
|
|
||||||
function handleDeleteIntegration(integration) {
|
|
||||||
DeleteIntegrationModal.setIntegration(integration);
|
|
||||||
DeleteIntegrationModal.Handler.show();
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleIntegrationItemClicked(item) {
|
|
||||||
props.onIntegrationClicked(item.integration);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
class={
|
|
||||||
"d-flex flex-column overflow-hidden flex-fill px-3 pb-2 pt-3 " +
|
|
||||||
props.class
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<div class="d-flex flex-row">
|
|
||||||
{props.navigation}
|
|
||||||
<div class="d-flex flex-row justify-content-end flex-fill">
|
|
||||||
<button
|
|
||||||
class="btn btn-dark me-2 mb-3"
|
|
||||||
onClick={handleRegisterIntegration}
|
|
||||||
>
|
|
||||||
<i class="bi bi-plus-square me-2"></i>
|
|
||||||
Register
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<List
|
|
||||||
onListItemClick={handleIntegrationItemClicked}
|
|
||||||
items={integrations().map((integration) => ({
|
|
||||||
id: {
|
|
||||||
html: <span class="font-monospace">{integration.getId()}</span>,
|
|
||||||
},
|
|
||||||
name: {
|
|
||||||
text: integration.getName(),
|
|
||||||
},
|
|
||||||
options: {
|
|
||||||
html: (
|
|
||||||
<>
|
|
||||||
<button
|
|
||||||
class="btn btn-sm btn-outline-secondary me-2"
|
|
||||||
onClick={(event) => {
|
|
||||||
event.stopPropagation();
|
|
||||||
handleDeleteIntegration(integration);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<i class="bi bi-trash-fill"></i>
|
|
||||||
</button>
|
|
||||||
</>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
integration: integration,
|
|
||||||
}))}
|
|
||||||
class={"flex-fill"}
|
|
||||||
columns={[
|
|
||||||
{
|
|
||||||
id: "id",
|
|
||||||
name: "id",
|
|
||||||
width: 6,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "name",
|
|
||||||
name: "Name",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "options",
|
|
||||||
name: "",
|
|
||||||
width: 4,
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
></List>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default IntegrationListView;
|
|
||||||
@ -1,181 +0,0 @@
|
|||||||
import {
|
|
||||||
createEffect,
|
|
||||||
createMemo,
|
|
||||||
createResource,
|
|
||||||
createSignal,
|
|
||||||
onCleanup,
|
|
||||||
Show,
|
|
||||||
} from "solid-js";
|
|
||||||
import WebRTCService from "../../services/webrtc-service";
|
|
||||||
import Integration from "../../data/integration";
|
|
||||||
import DeviceService from "../../services/device-service";
|
|
||||||
import UrlUtils from "../../tools/url-utils";
|
|
||||||
import RemoteControl from "../../components/remote-control";
|
|
||||||
import RemoteService from "../../services/remotes-service";
|
|
||||||
|
|
||||||
const [integration, setIntegration] = createSignal(new Integration());
|
|
||||||
|
|
||||||
function IntegrationView(props) {
|
|
||||||
const title = createMemo(() =>
|
|
||||||
integration && typeof integration === "function"
|
|
||||||
? integration().getName()
|
|
||||||
: "Integration"
|
|
||||||
);
|
|
||||||
const [connectionState, setConnectionState] = createSignal(
|
|
||||||
WebRTCService.STATE_DISCONNECTED
|
|
||||||
);
|
|
||||||
const showConnectButton = createMemo(
|
|
||||||
() =>
|
|
||||||
connectionState() === WebRTCService.STATE_DISCONNECTED ||
|
|
||||||
connectionState() === WebRTCService.STATE_FAILED ||
|
|
||||||
connectionState() === WebRTCService.STATE_CLOSED
|
|
||||||
);
|
|
||||||
WebRTCService.onStateChanged(handleConnectionStateChanged);
|
|
||||||
WebRTCService.onDataChannelOpen(handleDataChannelOpen);
|
|
||||||
let videoElement = null;
|
|
||||||
|
|
||||||
const [availableRemotes] = createResource(RemoteService.getRemotes, {
|
|
||||||
initialValue: [],
|
|
||||||
});
|
|
||||||
const [selectedRemote, setSelectedRemote] = createSignal();
|
|
||||||
const [remoteControlVisible, setRemoteControlVisible] = createSignal(false);
|
|
||||||
|
|
||||||
createEffect(() =>
|
|
||||||
handleRemoteSelected(
|
|
||||||
availableRemotes()
|
|
||||||
.find(() => true)
|
|
||||||
?.getId()
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
createEffect(() => {
|
|
||||||
let url = UrlUtils.getUrl();
|
|
||||||
url = UrlUtils.addQueryParameter(url, "id", integration()?.getId());
|
|
||||||
UrlUtils.setUrl(url);
|
|
||||||
});
|
|
||||||
|
|
||||||
onCleanup(() => {
|
|
||||||
window.removeEventListener("popstate", setIntegrationFromUrl);
|
|
||||||
});
|
|
||||||
|
|
||||||
setIntegrationFromUrl();
|
|
||||||
window.addEventListener("popstate", setIntegrationFromUrl);
|
|
||||||
|
|
||||||
async function setIntegrationFromUrl() {
|
|
||||||
let integrationId = UrlUtils.getQueryParameter("id");
|
|
||||||
if (!integrationId) return;
|
|
||||||
let integration = await DeviceService.getIntegration(integrationId);
|
|
||||||
setIntegration(integration);
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleConnectWebRTC() {
|
|
||||||
let integrationId = integration().getId();
|
|
||||||
WebRTCService.setVideoElement(videoElement);
|
|
||||||
WebRTCService.connect(integrationId);
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleDisconnectWebRTC() {
|
|
||||||
WebRTCService.disconnect();
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleConnectionStateChanged(state) {
|
|
||||||
setConnectionState(state);
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleDataChannelOpen() {
|
|
||||||
setInterval(() => {
|
|
||||||
WebRTCService.sendDataJson({ message: "ping" });
|
|
||||||
}, 1000);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleRemoteSelected(remoteId) {
|
|
||||||
if (!remoteId) return;
|
|
||||||
let remote = await RemoteService.getRemote(remoteId);
|
|
||||||
setSelectedRemote(remote);
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleRemoteButtonPressed(command) {
|
|
||||||
RemoteService.sendCommand(command);
|
|
||||||
}
|
|
||||||
|
|
||||||
function toggleRemoteControl() {
|
|
||||||
setRemoteControlVisible(!remoteControlVisible());
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
class={
|
|
||||||
"d-flex flex-column overflow-hidden flex-fill px-3 pb-2 pt-3 " +
|
|
||||||
props.class
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<a>
|
|
||||||
<i class="bi bi-arrow-left-short"></i>
|
|
||||||
<span>Integration</span>
|
|
||||||
</a>
|
|
||||||
<h1>{title}</h1>
|
|
||||||
<div class="d-flex flex-row">
|
|
||||||
<div class="d-flex flex-row justify-content-end flex-fill">
|
|
||||||
<Show when={showConnectButton()}>
|
|
||||||
<button
|
|
||||||
class="btn btn-dark me-2 mb-3"
|
|
||||||
onClick={handleConnectWebRTC}
|
|
||||||
>
|
|
||||||
<i class="bi bi-plug-fill me-2"></i>
|
|
||||||
Connect
|
|
||||||
</button>
|
|
||||||
</Show>
|
|
||||||
<Show when={!showConnectButton()}>
|
|
||||||
<button
|
|
||||||
class="btn btn-dark me-2 mb-3"
|
|
||||||
onClick={handleDisconnectWebRTC}
|
|
||||||
>
|
|
||||||
<i class="bi bi-x-lg me-2"></i>
|
|
||||||
Disconnect
|
|
||||||
</button>
|
|
||||||
</Show>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="flex-fill d-flex flex-row overflow-hidden">
|
|
||||||
<div
|
|
||||||
class="flex-fill rounded overflow-hidden"
|
|
||||||
style="position:relative;"
|
|
||||||
>
|
|
||||||
<video
|
|
||||||
ref={videoElement}
|
|
||||||
class="w-100 h-100"
|
|
||||||
style="background-color: #000"
|
|
||||||
></video>
|
|
||||||
<button
|
|
||||||
class="btn btn-dark mt-2 me-2"
|
|
||||||
style="transform: rotate(180deg);position:absolute;top:0;right:0;z-index:1000;"
|
|
||||||
onClick={toggleRemoteControl}
|
|
||||||
>
|
|
||||||
<i class="bi bi-building"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<Show when={remoteControlVisible()}>
|
|
||||||
<div class="d-flex flex-column ps-3">
|
|
||||||
<select
|
|
||||||
class="form-select mb-3"
|
|
||||||
onChange={(e) => handleRemoteSelected(e.target.value)}
|
|
||||||
>
|
|
||||||
{availableRemotes().map((remote, index) => (
|
|
||||||
<option value={remote.getId()} selected={index === 0}>
|
|
||||||
{remote.getTitle()}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
<RemoteControl
|
|
||||||
onCommand={handleRemoteButtonPressed}
|
|
||||||
remote={selectedRemote()}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</Show>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
IntegrationView.setIntegration = setIntegration;
|
|
||||||
export default IntegrationView;
|
|
||||||
51
www/src/views/integration-view.jsx
Normal file
51
www/src/views/integration-view.jsx
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
import { createMemo, createSignal } from "solid-js";
|
||||||
|
import Integration from "../data/integration";
|
||||||
|
|
||||||
|
const [integration, setIntegration] = createSignal(null);
|
||||||
|
|
||||||
|
const offer = {
|
||||||
|
type: "offer",
|
||||||
|
sdp: "v=0\r\no=- 3395492761835116674 2 IN IP4 127.0.0.1\r\ns=-\r\nt=0 0\r\na=group:BUNDLE 0\r\na=extmap-allow-mixed\r\na=msid-semantic: WMS 077a17e7-aec0-4aec-bef9-da6e02d3458f\r\nm=video 57551 UDP/TLS/RTP/SAVPF 96 97 103 104 107 108 109 114 115 116 117 118 39 40 45 46 98 99 100 101 119 120 121\r\nc=IN IP4 192.168.178.23\r\na=rtcp:9 IN IP4 0.0.0.0\r\na=candidate:4029581777 1 udp 2122260223 192.168.178.23 57551 typ host generation 0 network-id 1 network-cost 10\r\na=candidate:2397137737 1 tcp 1518280447 192.168.178.23 9 typ host tcptype active generation 0 network-id 1 network-cost 10\r\na=ice-ufrag:BJUp\r\na=ice-pwd:7yhav/1dA688oD7sU/tb3TMI\r\na=ice-options:trickle\r\na=fingerprint:sha-256 DD:1D:7E:BB:A4:69:BF:B9:B1:3B:F2:58:A8:4A:EB:F0:BE:B8:06:5E:A0:BB:42:26:5E:79:3B:DD:3D:4E:44:30\r\na=setup:actpass\r\na=mid:0\r\na=extmap:1 urn:ietf:params:rtp-hdrext:toffset\r\na=extmap:2 http://www.webrtc.org/experiments/rtp-hdrext/abs-send-time\r\na=extmap:3 urn:3gpp:video-orientation\r\na=extmap:4 http://www.ietf.org/id/draft-holmer-rmcat-transport-wide-cc-extensions-01\r\na=extmap:5 http://www.webrtc.org/experiments/rtp-hdrext/playout-delay\r\na=extmap:6 http://www.webrtc.org/experiments/rtp-hdrext/video-content-type\r\na=extmap:7 http://www.webrtc.org/experiments/rtp-hdrext/video-timing\r\na=extmap:8 http://www.webrtc.org/experiments/rtp-hdrext/color-space\r\na=extmap:9 urn:ietf:params:rtp-hdrext:sdes:mid\r\na=extmap:10 urn:ietf:params:rtp-hdrext:sdes:rtp-stream-id\r\na=extmap:11 urn:ietf:params:rtp-hdrext:sdes:repaired-rtp-stream-id\r\na=sendrecv\r\na=msid:077a17e7-aec0-4aec-bef9-da6e02d3458f 593c4b1b-5b4b-4bed-b64a-2da1011de475\r\na=rtcp-mux\r\na=rtcp-rsize\r\na=rtpmap:96 VP8/90000\r\na=rtcp-fb:96 goog-remb\r\na=rtcp-fb:96 transport-cc\r\na=rtcp-fb:96 ccm fir\r\na=rtcp-fb:96 nack\r\na=rtcp-fb:96 nack pli\r\na=rtpmap:97 rtx/90000\r\na=fmtp:97 apt=96\r\na=rtpmap:103 H264/90000\r\na=rtcp-fb:103 goog-remb\r\na=rtcp-fb:103 transport-cc\r\na=rtcp-fb:103 ccm fir\r\na=rtcp-fb:103 nack\r\na=rtcp-fb:103 nack pli\r\na=fmtp:103 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42001f\r\na=rtpmap:104 rtx/90000\r\na=fmtp:104 apt=103\r\na=rtpmap:107 H264/90000\r\na=rtcp-fb:107 goog-remb\r\na=rtcp-fb:107 transport-cc\r\na=rtcp-fb:107 ccm fir\r\na=rtcp-fb:107 nack\r\na=rtcp-fb:107 nack pli\r\na=fmtp:107 level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42001f\r\na=rtpmap:108 rtx/90000\r\na=fmtp:108 apt=107\r\na=rtpmap:109 H264/90000\r\na=rtcp-fb:109 goog-remb\r\na=rtcp-fb:109 transport-cc\r\na=rtcp-fb:109 ccm fir\r\na=rtcp-fb:109 nack\r\na=rtcp-fb:109 nack pli\r\na=fmtp:109 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=42e01f\r\na=rtpmap:114 rtx/90000\r\na=fmtp:114 apt=109\r\na=rtpmap:115 H264/90000\r\na=rtcp-fb:115 goog-remb\r\na=rtcp-fb:115 transport-cc\r\na=rtcp-fb:115 ccm fir\r\na=rtcp-fb:115 nack\r\na=rtcp-fb:115 nack pli\r\na=fmtp:115 level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=42e01f\r\na=rtpmap:116 rtx/90000\r\na=fmtp:116 apt=115\r\na=rtpmap:117 H264/90000\r\na=rtcp-fb:117 goog-remb\r\na=rtcp-fb:117 transport-cc\r\na=rtcp-fb:117 ccm fir\r\na=rtcp-fb:117 nack\r\na=rtcp-fb:117 nack pli\r\na=fmtp:117 level-asymmetry-allowed=1;packetization-mode=1;profile-level-id=4d001f\r\na=rtpmap:118 rtx/90000\r\na=fmtp:118 apt=117\r\na=rtpmap:39 H264/90000\r\na=rtcp-fb:39 goog-remb\r\na=rtcp-fb:39 transport-cc\r\na=rtcp-fb:39 ccm fir\r\na=rtcp-fb:39 nack\r\na=rtcp-fb:39 nack pli\r\na=fmtp:39 level-asymmetry-allowed=1;packetization-mode=0;profile-level-id=4d001f\r\na=rtpmap:40 rtx/90000\r\na=fmtp:40 apt=39\r\na=rtpmap:45 AV1/90000\r\na=rtcp-fb:45 goog-remb\r\na=rtcp-fb:45 transport-cc\r\na=rtcp-fb:45 ccm fir\r\na=rtcp-fb:45 nack\r\na=rtcp-fb:45 nack pli\r\na=fmtp:45 level-idx=5;profile=0;tier=0\r\na=rtpmap:46 rtx/90000\r\na=fmtp:46 apt=45\r\na=rtpmap:98 VP9/90000\r\na=rtcp-fb:98 goog-remb\r\na=rtcp-fb:98 transport-cc\r\na=rtcp-fb:98 ccm fir\r\na=rtcp-fb:98 nack\r\na=rtcp-fb:98 nack pli\r\na=fmtp:98 profile-id=0\r\na=rtpmap:99 rtx/90000\r\na=fmtp:99 apt=98\r\na=rtpmap:100 VP9/90000\r\na=rtcp-fb:100 goog-remb\r\na=rtcp-fb:100 transport-cc\r\na=rtcp-fb:100 ccm fir\r\na=rtcp-fb:100 nack\r\na=rtcp-fb:100 nack pli\r\na=fmtp:100 profile-id=2\r\na=rtpmap:101 rtx/90000\r\na=fmtp:101 apt=100\r\na=rtpmap:119 red/90000\r\na=rtpmap:120 rtx/90000\r\na=fmtp:120 apt=119\r\na=rtpmap:121 ulpfec/90000\r\na=ssrc-group:FID 1810451430 2342397672\r\na=ssrc:1810451430 cname:NKFwP7Fvk/3zlXSL\r\na=ssrc:1810451430 msid:077a17e7-aec0-4aec-bef9-da6e02d3458f 593c4b1b-5b4b-4bed-b64a-2da1011de475\r\na=ssrc:2342397672 cname:NKFwP7Fvk/3zlXSL\r\na=ssrc:2342397672 msid:077a17e7-aec0-4aec-bef9-da6e02d3458f 593c4b1b-5b4b-4bed-b64a-2da1011de475\r\n",
|
||||||
|
};
|
||||||
|
function IntegrationView(props) {
|
||||||
|
const title = createMemo(() =>
|
||||||
|
integration() && typeof integration === "function"
|
||||||
|
? integration().getName()
|
||||||
|
: "Integration"
|
||||||
|
);
|
||||||
|
|
||||||
|
async function getSDP() {
|
||||||
|
let configuration = {
|
||||||
|
iceServers: [],
|
||||||
|
};
|
||||||
|
let mediaTrack = await navigator.mediaDevices.getUserMedia({
|
||||||
|
audio: true,
|
||||||
|
video: false,
|
||||||
|
});
|
||||||
|
let rtcPeerConnection = new RTCPeerConnection(configuration);
|
||||||
|
mediaTrack
|
||||||
|
.getTracks()
|
||||||
|
.forEach((track) => rtcPeerConnection.addTrack(track, mediaTrack));
|
||||||
|
rtcPeerConnection.setRemoteDescription(new RTCSessionDescription(offer));
|
||||||
|
let response = await rtcPeerConnection.createAnswer();
|
||||||
|
console.log(JSON.stringify(response));
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
class={
|
||||||
|
"d-flex flex-column overflow-hidden flex-fill px-3 pb-2 pt-3 " +
|
||||||
|
props.class
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<span>Integration</span>
|
||||||
|
<h1>{title}</h1>
|
||||||
|
<button class="btn btn-primary" onClick={() => getSDP()}>
|
||||||
|
get SDP
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
IntegrationView.setIntegration = setIntegration;
|
||||||
|
export default IntegrationView;
|
||||||
@ -1,6 +1,6 @@
|
|||||||
import { createSignal, mergeProps } from "solid-js";
|
import { createSignal, mergeProps } from "solid-js";
|
||||||
import UserService from "../services/user-service";
|
import UserService from "../services/user-service";
|
||||||
import Spinner from "../components/spinner";
|
import Spinner from "../modules/spinner";
|
||||||
|
|
||||||
const IDLE = "idle";
|
const IDLE = "idle";
|
||||||
const LOGGING_IN = "logging-in";
|
const LOGGING_IN = "logging-in";
|
||||||
|
|||||||
@ -1,32 +1,31 @@
|
|||||||
import { autoUpdate, computePosition, offset, shift } from "@floating-ui/dom";
|
|
||||||
import {
|
import {
|
||||||
createEffect,
|
|
||||||
createResource,
|
|
||||||
createSignal,
|
createSignal,
|
||||||
mergeProps,
|
mergeProps,
|
||||||
onCleanup,
|
createResource,
|
||||||
|
createEffect,
|
||||||
onMount,
|
onMount,
|
||||||
|
onCleanup,
|
||||||
} from "solid-js";
|
} from "solid-js";
|
||||||
|
import { computePosition, shift, autoUpdate, offset } from "@floating-ui/dom";
|
||||||
|
|
||||||
import ModalRegistry from "../modals/modal-registry.jsx";
|
|
||||||
import UserService from "../services/user-service.js";
|
import UserService from "../services/user-service.js";
|
||||||
|
import ModalRegistry from "../modals/modal-registry.jsx";
|
||||||
import UrlUtils from "../tools/url-utils.js";
|
import UrlUtils from "../tools/url-utils.js";
|
||||||
import DevicesView from "./devices/devices-view.jsx";
|
|
||||||
import SettingsView from "./settings-view.jsx";
|
import SettingsView from "./settings-view.jsx";
|
||||||
|
import DevicesView from "./devices-view.jsx";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
DEVICES_VIEW,
|
DEVICES_VIEW,
|
||||||
INTEGRATION_VIEW,
|
|
||||||
RECORDINGS_VIEW,
|
|
||||||
REMOTES_VIEW,
|
REMOTES_VIEW,
|
||||||
|
RECORDINGS_VIEW,
|
||||||
SETTINGS_VIEW,
|
SETTINGS_VIEW,
|
||||||
|
INTEGRATION_VIEW,
|
||||||
} from "../data/constants.js";
|
} from "../data/constants.js";
|
||||||
import IntegrationView from "./devices/integration-view.jsx";
|
import IntegrationView from "./integration-view.jsx";
|
||||||
import RemotesView from "./remotes/remotes-view.jsx";
|
|
||||||
|
|
||||||
let [activeView, setActiveView] = createSignal(DEVICES_VIEW);
|
let [activeView, setActiveView] = createSignal(DEVICES_VIEW);
|
||||||
|
|
||||||
function MainView(props) {
|
const MainView = function (props) {
|
||||||
props = mergeProps({ onLogout: () => {} }, props);
|
props = mergeProps({ onLogout: () => {} }, props);
|
||||||
|
|
||||||
const [userInfo] = createResource(() => UserService.getUserInfo());
|
const [userInfo] = createResource(() => UserService.getUserInfo());
|
||||||
@ -55,6 +54,7 @@ function MainView(props) {
|
|||||||
|
|
||||||
function setViewFromUrl() {
|
function setViewFromUrl() {
|
||||||
let view = UrlUtils.getQueryParameter("view");
|
let view = UrlUtils.getQueryParameter("view");
|
||||||
|
console.log(view)
|
||||||
if (view) {
|
if (view) {
|
||||||
setActiveView(view);
|
setActiveView(view);
|
||||||
}
|
}
|
||||||
@ -99,8 +99,7 @@ function MainView(props) {
|
|||||||
if (activeView() === view) return;
|
if (activeView() === view) return;
|
||||||
setActiveView(view);
|
setActiveView(view);
|
||||||
let url = UrlUtils.getUrl();
|
let url = UrlUtils.getUrl();
|
||||||
url = UrlUtils.addQueryParameter(url, "view", activeView());
|
url = UrlUtils.addQueryParameter(url, "view", view);
|
||||||
url = UrlUtils.removeQueryParameter(url, "tab");
|
|
||||||
UrlUtils.setUrl(url);
|
UrlUtils.setUrl(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -114,7 +113,7 @@ function MainView(props) {
|
|||||||
<div class="d-flex flex-column" style="height:100vh">
|
<div class="d-flex flex-column" style="height:100vh">
|
||||||
<Modals></Modals>
|
<Modals></Modals>
|
||||||
<HeaderBar></HeaderBar>
|
<HeaderBar></HeaderBar>
|
||||||
<ActiveView class={"bg-body-tertiary flex-fill"}></ActiveView>
|
<ActiveView class={"bg-body-tertiary"}></ActiveView>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@ -211,24 +210,18 @@ function MainView(props) {
|
|||||||
<SettingsView {...props} />
|
<SettingsView {...props} />
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={activeView() === DEVICES_VIEW}>
|
<Match when={activeView() === DEVICES_VIEW}>
|
||||||
<DevicesView
|
<DevicesView {...props} onIntegrationClicked={handleIntegrationClicked} />
|
||||||
{...props}
|
|
||||||
onIntegrationClicked={handleIntegrationClicked}
|
|
||||||
/>
|
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={activeView() === INTEGRATION_VIEW}>
|
<Match when={activeView() === INTEGRATION_VIEW}>
|
||||||
<IntegrationView {...props} />
|
<IntegrationView {...props} />
|
||||||
</Match>
|
</Match>
|
||||||
<Match when={activeView() === REMOTES_VIEW}>
|
|
||||||
<RemotesView {...props} />
|
|
||||||
</Match>
|
|
||||||
</Switch>
|
</Switch>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return render();
|
return render();
|
||||||
}
|
};
|
||||||
|
|
||||||
MainView.setActiveView = setActiveView;
|
MainView.setActiveView = setActiveView;
|
||||||
|
|
||||||
|
|||||||
@ -1,142 +0,0 @@
|
|||||||
import { createResource, onMount } from "solid-js";
|
|
||||||
import List from "../../components/list";
|
|
||||||
import RemotesService from "../../services/remotes-service";
|
|
||||||
import CreateCommandModal from "../../modals/create-command-modal";
|
|
||||||
import DeleteCommandModal from "../../modals/delete-command-modal";
|
|
||||||
import ImportCommandsModal from "../../modals/import-commands-modal";
|
|
||||||
import EditCommandModal from "../../modals/edit-command-modal";
|
|
||||||
|
|
||||||
function CommandsList(props) {
|
|
||||||
const [commands, { refetch: refetchCommands }] = createResource(
|
|
||||||
RemotesService.getCommands
|
|
||||||
);
|
|
||||||
|
|
||||||
onMount(() => {
|
|
||||||
refetchCommands();
|
|
||||||
});
|
|
||||||
|
|
||||||
CreateCommandModal.onCommandCreated(() => {
|
|
||||||
refetchCommands();
|
|
||||||
});
|
|
||||||
|
|
||||||
ImportCommandsModal.onCommandsImported(() => {
|
|
||||||
refetchCommands();
|
|
||||||
});
|
|
||||||
|
|
||||||
DeleteCommandModal.onCommandDeleted(() => {
|
|
||||||
refetchCommands();
|
|
||||||
});
|
|
||||||
|
|
||||||
EditCommandModal.onCommandEdited(() => {
|
|
||||||
refetchCommands();
|
|
||||||
});
|
|
||||||
|
|
||||||
function handleNewCommand() {
|
|
||||||
refetchCommands();
|
|
||||||
CreateCommandModal.Handler.show();
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleDeleteCommand(command) {
|
|
||||||
DeleteCommandModal.setCommand(command);
|
|
||||||
DeleteCommandModal.Handler.show();
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleImportCommands() {
|
|
||||||
ImportCommandsModal.Handler.show();
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleEditCommand(command) {
|
|
||||||
EditCommandModal.setCommand(command);
|
|
||||||
EditCommandModal.Handler.show();
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div class="d-flex flex-row">
|
|
||||||
<div class="d-flex flex-row flex-fill">
|
|
||||||
{props.navigation ? props.navigation : null}
|
|
||||||
</div>
|
|
||||||
<div class="d-flex flex-row justify-content-end flex-fill">
|
|
||||||
<button class="btn btn-dark me-2 mb-3" onClick={handleImportCommands}>
|
|
||||||
<i class="bi bi-box-arrow-in-down me-2"></i>
|
|
||||||
Import
|
|
||||||
</button>
|
|
||||||
<button class="btn btn-dark me-2 mb-3" onClick={handleNewCommand}>
|
|
||||||
<i class="bi bi-plus-square me-2"></i>
|
|
||||||
New
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<List
|
|
||||||
class={"flex-fill"}
|
|
||||||
columns={[
|
|
||||||
{
|
|
||||||
id: "id",
|
|
||||||
name: "id",
|
|
||||||
width: 6,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "title",
|
|
||||||
name: "Title",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "protocol",
|
|
||||||
name: "Protocol",
|
|
||||||
width: 8,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "type",
|
|
||||||
name: "Type",
|
|
||||||
width: 6,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "options",
|
|
||||||
name: "",
|
|
||||||
width: 6,
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
items={(commands() || []).map((command) => ({
|
|
||||||
id: {
|
|
||||||
html: <span class="font-monospace">{command.getId()}</span>,
|
|
||||||
},
|
|
||||||
title: {
|
|
||||||
text: command.getTitle(),
|
|
||||||
},
|
|
||||||
protocol: {
|
|
||||||
text: command.getProtocol(),
|
|
||||||
},
|
|
||||||
type: {
|
|
||||||
text: command.getCommandType(),
|
|
||||||
},
|
|
||||||
options: {
|
|
||||||
html: (
|
|
||||||
<>
|
|
||||||
<button
|
|
||||||
class="btn btn-sm btn-outline-secondary me-2"
|
|
||||||
onClick={(event) => {
|
|
||||||
event.stopPropagation();
|
|
||||||
handleEditCommand(command);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<i class="bi bi-pencil-fill"></i>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="btn btn-sm btn-outline-secondary me-2"
|
|
||||||
onClick={(event) => {
|
|
||||||
event.stopPropagation();
|
|
||||||
handleDeleteCommand(command);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<i class="bi bi-trash-fill"></i>
|
|
||||||
</button>
|
|
||||||
</>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
command,
|
|
||||||
}))}
|
|
||||||
></List>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default CommandsList;
|
|
||||||
@ -1,146 +0,0 @@
|
|||||||
import { createMemo, createResource, createSignal, onMount } from "solid-js";
|
|
||||||
import List from "../../components/list";
|
|
||||||
import RemotesService from "../../services/remotes-service";
|
|
||||||
import CreateRemoteModal from "../../modals/create-remote-modal";
|
|
||||||
import DeleteRemoteModal from "../../modals/delete-remote-modal";
|
|
||||||
import FileUtils from "../../tools/file-utils";
|
|
||||||
import Serializer from "../../data/serializer";
|
|
||||||
import RemoteService from "../../services/remotes-service";
|
|
||||||
import EditRemoteModal from "../../modals/edit-remote-modal";
|
|
||||||
|
|
||||||
function RemotesList(props) {
|
|
||||||
const [remotes, { refetch: refetchRemotes }] = createResource(
|
|
||||||
RemotesService.getRemotes
|
|
||||||
);
|
|
||||||
const [selectedRemotes, setSelectedRemotes] = createSignal([]);
|
|
||||||
const canExport = createMemo(() => selectedRemotes().length > 0);
|
|
||||||
|
|
||||||
onMount(() => {
|
|
||||||
refetchRemotes();
|
|
||||||
});
|
|
||||||
|
|
||||||
CreateRemoteModal.onRemoteCreated(() => {
|
|
||||||
refetchRemotes();
|
|
||||||
});
|
|
||||||
|
|
||||||
DeleteRemoteModal.onRemoteDeleted(() => {
|
|
||||||
refetchRemotes();
|
|
||||||
});
|
|
||||||
|
|
||||||
EditRemoteModal.onRemoteEdited(() => {
|
|
||||||
refetchRemotes();
|
|
||||||
});
|
|
||||||
|
|
||||||
function handleNewRemote() {
|
|
||||||
CreateRemoteModal.Handler.show();
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleEditRemote(remote) {
|
|
||||||
EditRemoteModal.setRemoteId(remote.getId());
|
|
||||||
EditRemoteModal.Handler.show();
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleDeleteRemote(remote) {
|
|
||||||
DeleteRemoteModal.setRemote(remote);
|
|
||||||
DeleteRemoteModal.Handler.show();
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleListItemsSelect(items) {
|
|
||||||
setSelectedRemotes(items.map((item) => item.remote));
|
|
||||||
}
|
|
||||||
|
|
||||||
async function handleExport() {
|
|
||||||
let remotes = await Promise.all(
|
|
||||||
selectedRemotes().map((remote) => RemoteService.getRemote(remote.getId()))
|
|
||||||
);
|
|
||||||
let files = remotes.map((remote) =>
|
|
||||||
FileUtils.createJsonFile(
|
|
||||||
Serializer.serializeRemote(remote),
|
|
||||||
`${remote.getTitle()}.remote.json`
|
|
||||||
)
|
|
||||||
);
|
|
||||||
if (files.length >= 1) {
|
|
||||||
FileUtils.downloadFile(files[0]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div class="d-flex flex-row">
|
|
||||||
<div class="d-flex flex-row flex-fill">
|
|
||||||
{props.navigation ? props.navigation : null}
|
|
||||||
</div>
|
|
||||||
<div class="d-flex flex-row justify-content-end flex-fill">
|
|
||||||
<button
|
|
||||||
class="btn btn-dark me-2 mb-3"
|
|
||||||
disabled={!canExport()}
|
|
||||||
onClick={handleExport}
|
|
||||||
>
|
|
||||||
<i class="bi bi-box-arrow-up me-2"></i>
|
|
||||||
Export
|
|
||||||
</button>
|
|
||||||
<button class="btn btn-dark me-2 mb-3" onClick={handleNewRemote}>
|
|
||||||
<i class="bi bi-plus-square me-2"></i>
|
|
||||||
New
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<List
|
|
||||||
class={"flex-fill"}
|
|
||||||
columns={[
|
|
||||||
{
|
|
||||||
id: "id",
|
|
||||||
name: "id",
|
|
||||||
width: 6,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "title",
|
|
||||||
name: "Title",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
id: "options",
|
|
||||||
name: "",
|
|
||||||
width: 6,
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
onListItemClick={() => {}}
|
|
||||||
onListItemsSelect={handleListItemsSelect}
|
|
||||||
items={(remotes() || []).map((remote) => ({
|
|
||||||
id: {
|
|
||||||
html: <span class="font-monospace">{remote.getId()}</span>,
|
|
||||||
},
|
|
||||||
title: {
|
|
||||||
text: remote.getTitle(),
|
|
||||||
},
|
|
||||||
options: {
|
|
||||||
html: (
|
|
||||||
<>
|
|
||||||
<button
|
|
||||||
class="btn btn-sm btn-outline-secondary me-2"
|
|
||||||
onClick={(event) => {
|
|
||||||
event.stopPropagation();
|
|
||||||
handleEditRemote(remote);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<i class="bi bi-pencil-fill"></i>
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class="btn btn-sm btn-outline-secondary me-2"
|
|
||||||
onClick={(event) => {
|
|
||||||
event.stopPropagation();
|
|
||||||
handleDeleteRemote(remote);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<i class="bi bi-trash-fill"></i>
|
|
||||||
</button>
|
|
||||||
</>
|
|
||||||
),
|
|
||||||
},
|
|
||||||
remote,
|
|
||||||
}))}
|
|
||||||
></List>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default RemotesList;
|
|
||||||
@ -1,92 +0,0 @@
|
|||||||
import {
|
|
||||||
createEffect,
|
|
||||||
createSignal,
|
|
||||||
Match,
|
|
||||||
mergeProps,
|
|
||||||
onCleanup,
|
|
||||||
Switch,
|
|
||||||
} from "solid-js";
|
|
||||||
|
|
||||||
import UrlUtils from "../../tools/url-utils";
|
|
||||||
import CommandsList from "./commands-list-view";
|
|
||||||
import RemotesList from "./remotes-list-view";
|
|
||||||
|
|
||||||
function RemotesView(props) {
|
|
||||||
props = mergeProps({ onIntegrationClicked: () => {} }, props);
|
|
||||||
const REMOTES_LIST_VIEW = "remotes_list";
|
|
||||||
const COMMANDS_LIST_VIEW = "commands_list";
|
|
||||||
const VIEWS = [REMOTES_LIST_VIEW, COMMANDS_LIST_VIEW];
|
|
||||||
|
|
||||||
const [currentView, setCurrentView] = createSignal(REMOTES_LIST_VIEW);
|
|
||||||
|
|
||||||
createEffect(() => {
|
|
||||||
let url = UrlUtils.getUrl();
|
|
||||||
url = UrlUtils.addQueryParameter(url, "tab", currentView());
|
|
||||||
UrlUtils.setUrl(url);
|
|
||||||
});
|
|
||||||
|
|
||||||
onCleanup(() => {
|
|
||||||
window.removeEventListener("popstate", setViewFromUrl);
|
|
||||||
});
|
|
||||||
|
|
||||||
setViewFromUrl();
|
|
||||||
window.addEventListener("popstate", setViewFromUrl);
|
|
||||||
|
|
||||||
function setViewFromUrl() {
|
|
||||||
let view = UrlUtils.getQueryParameter("tab");
|
|
||||||
if (!view) return;
|
|
||||||
if (!VIEWS.includes(view)) return;
|
|
||||||
setCurrentView(view);
|
|
||||||
}
|
|
||||||
|
|
||||||
function Navigation(props) {
|
|
||||||
return (
|
|
||||||
<div class="d-flex flex-row flex-fill">
|
|
||||||
<button
|
|
||||||
class={
|
|
||||||
"btn me-2 mb-3" +
|
|
||||||
(currentView() === REMOTES_LIST_VIEW
|
|
||||||
? " btn-secondary"
|
|
||||||
: " btn-dark")
|
|
||||||
}
|
|
||||||
onClick={() => setCurrentView(REMOTES_LIST_VIEW)}
|
|
||||||
>
|
|
||||||
<i class="bi bi-tv me-2"></i>
|
|
||||||
Remotes
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
class={
|
|
||||||
"btn me-2 mb-3" +
|
|
||||||
(currentView() === COMMANDS_LIST_VIEW
|
|
||||||
? " btn-secondary"
|
|
||||||
: " btn-dark")
|
|
||||||
}
|
|
||||||
onClick={() => setCurrentView(COMMANDS_LIST_VIEW)}
|
|
||||||
>
|
|
||||||
<i class="bi bi-gear me-2"></i>
|
|
||||||
Commands
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div
|
|
||||||
class={
|
|
||||||
"d-flex flex-column overflow-hidden flex-fill px-3 pb-2 pt-3 " +
|
|
||||||
props.class
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Switch>
|
|
||||||
<Match when={currentView() === REMOTES_LIST_VIEW}>
|
|
||||||
<RemotesList navigation={<Navigation />} />
|
|
||||||
</Match>
|
|
||||||
<Match when={currentView() === COMMANDS_LIST_VIEW}>
|
|
||||||
<CommandsList navigation={<Navigation />} />
|
|
||||||
</Match>
|
|
||||||
</Switch>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default RemotesView;
|
|
||||||
@ -1,6 +1,6 @@
|
|||||||
import { Show, createEffect, createResource, createSignal } from "solid-js";
|
import { Show, createEffect, createResource, createSignal } from "solid-js";
|
||||||
import ChangePasswordSettings from "../components/settings/change-password";
|
import ChangePasswordSettings from "../modules/settings/change-password";
|
||||||
import UserList from "../components/settings/users-list";
|
import UserList from "../modules/settings/users-list";
|
||||||
import UserService from "../services/user-service";
|
import UserService from "../services/user-service";
|
||||||
|
|
||||||
function SettingsView(props) {
|
function SettingsView(props) {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user