Compare commits
18 Commits
feat-list-
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 0a41b1072a | |||
| d7b8ad9976 | |||
| 6a9c69a535 | |||
| 8f3832657b | |||
| e09d42ecf6 | |||
| 23245dd549 | |||
| b0c3a48da6 | |||
| f83ffac0ce | |||
| 83ab71f845 | |||
| 6940782024 | |||
| 6cefa7392c | |||
| db1beac033 | |||
| 318185bd4a | |||
| 7a20ae1536 | |||
| 3ba3f5efd5 | |||
| 820caf137f | |||
| 0c6a302a92 | |||
| 01aff1635d |
21
LICENSE.md
Normal file
21
LICENSE.md
Normal file
@ -0,0 +1,21 @@
|
||||
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.
|
||||
75
data/command.go
Normal file
75
data/command.go
Normal file
@ -0,0 +1,75 @@
|
||||
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"`
|
||||
}
|
||||
7
data/remote.go
Normal file
7
data/remote.go
Normal file
@ -0,0 +1,7 @@
|
||||
package data
|
||||
|
||||
type Remote struct {
|
||||
Id string `json:"id"`
|
||||
Title string `json:"title"`
|
||||
Commands []Command `json:"commands"`
|
||||
}
|
||||
255
data/remote_database.go
Normal file
255
data/remote_database.go
Normal file
@ -0,0 +1,255 @@
|
||||
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
|
||||
}
|
||||
20
main/main.go
20
main/main.go
@ -39,6 +39,15 @@ func main() {
|
||||
}
|
||||
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{}
|
||||
err = userManager.Initialize(&userDatabase)
|
||||
if err != nil {
|
||||
@ -51,6 +60,12 @@ func main() {
|
||||
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.SetWebAppDirectoryPath("www")
|
||||
webServer.SetPort(configuration.Port)
|
||||
@ -71,6 +86,11 @@ func main() {
|
||||
deviceApiHandler.SetRouter(webServer.Router())
|
||||
deviceApiHandler.Initialize(&authenticator)
|
||||
|
||||
remoteApiHandler := server.RemoteApiHandler{}
|
||||
remoteApiHandler.SetRemoteManager(&remoteManager)
|
||||
remoteApiHandler.SetRouter(webServer.Router())
|
||||
remoteApiHandler.Initialize(&authenticator)
|
||||
|
||||
webSocketServer := server.WebsocketServer{}
|
||||
webSocketServer.SetRouter(webServer.Router())
|
||||
webSocketServer.Initialize(&authenticator)
|
||||
|
||||
66
management/remote_manager.go
Normal file
66
management/remote_manager.go
Normal file
@ -0,0 +1,66 @@
|
||||
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)
|
||||
}
|
||||
146
server/remote_api_handler.go
Normal file
146
server/remote_api_handler.go
Normal file
@ -0,0 +1,146 @@
|
||||
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
|
||||
}
|
||||
@ -14,6 +14,7 @@
|
||||
href="./src/lib/bootstrap-icons-1.11.3/font/bootstrap-icons.css"
|
||||
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>
|
||||
|
||||
@ -1,21 +1,23 @@
|
||||
import { createEffect, createMemo, createSignal, mergeProps } from "solid-js";
|
||||
import { createMemo, createSignal, mergeProps } from "solid-js";
|
||||
|
||||
function ListManager(props) {
|
||||
props = mergeProps(
|
||||
{
|
||||
items: [],
|
||||
availableItems: [],
|
||||
itemToString: () => "",
|
||||
style: "",
|
||||
onItemSelect: () => {},
|
||||
onItemDeselect: () => {},
|
||||
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);
|
||||
@ -36,21 +38,22 @@ function ListManager(props) {
|
||||
})
|
||||
);
|
||||
|
||||
createEffect(() =>
|
||||
console.log(availableItemsFuse().search(availableItemsSearchString()))
|
||||
);
|
||||
|
||||
const selectableAvailableItems = createMemo(() =>
|
||||
(availableItemsSearchString()
|
||||
? availableItemsFuse()
|
||||
.search(availableItemsSearchString())
|
||||
.map((item) => item.item)
|
||||
: props.availableItems
|
||||
).filter((item) => !props.items.includes(item))
|
||||
).filter(
|
||||
(availableItem) =>
|
||||
!props.items.find((item) => props.itemsEqual(item, availableItem))
|
||||
)
|
||||
);
|
||||
const selectableItems = createMemo(() =>
|
||||
itemsSearchString()
|
||||
? itemsFuse().search(itemsSearchString()).map((item) => item.item)
|
||||
? itemsFuse()
|
||||
.search(itemsSearchString())
|
||||
.map((item) => item.item)
|
||||
: props.items
|
||||
);
|
||||
const canSelect = createMemo(
|
||||
@ -124,7 +127,7 @@ function ListManager(props) {
|
||||
/>
|
||||
</div>
|
||||
<div class="rounded rounded-top-0 border bg-body flex-fill overflow-y-scroll">
|
||||
{props.items.map((item, index) => (
|
||||
{props.items.sort(byLabel).map((item, index) => (
|
||||
<ListItem
|
||||
onClick={() => props.onItemSelected(index)}
|
||||
selected={index === props.selectedItemIndex}
|
||||
|
||||
@ -8,7 +8,17 @@ import {
|
||||
} from "solid-js";
|
||||
|
||||
function List(props) {
|
||||
props = mergeProps({ items: [], showHeader: true, selectable: true }, props);
|
||||
props = mergeProps(
|
||||
{
|
||||
items: [],
|
||||
showHeader: true,
|
||||
selectable: true,
|
||||
onListItemsSelect: () => {},
|
||||
onLazyLoad: () => {},
|
||||
onListItemClick: () => {},
|
||||
},
|
||||
props
|
||||
);
|
||||
const [listItems, setListItems] = createSignal([]);
|
||||
const selectedItems = createMemo(() =>
|
||||
listItems()
|
||||
@ -31,16 +41,13 @@ function List(props) {
|
||||
);
|
||||
});
|
||||
createEffect(() => {
|
||||
if (!props.onListItemsSelect) return;
|
||||
props.onListItemsSelect(selectedItems());
|
||||
});
|
||||
|
||||
const observer = new IntersectionObserver((entries) => {
|
||||
entries.forEach((entry) => {
|
||||
if (entry.isIntersecting) {
|
||||
if (props.onLazyLoad) {
|
||||
props.onLazyLoad();
|
||||
}
|
||||
props.onLazyLoad();
|
||||
}
|
||||
});
|
||||
});
|
||||
@ -61,7 +68,6 @@ function List(props) {
|
||||
});
|
||||
|
||||
function handleListItemClick(item) {
|
||||
if (!props.onListItemClick) return;
|
||||
props.onListItemClick(item);
|
||||
}
|
||||
|
||||
|
||||
180
www/src/components/remote-control.jsx
Normal file
180
www/src/components/remote-control.jsx
Normal file
@ -0,0 +1,180 @@
|
||||
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;
|
||||
@ -2,7 +2,13 @@ import { createSignal, mergeProps } from "solid-js";
|
||||
|
||||
function ValidatedTextInput(props) {
|
||||
props = mergeProps(
|
||||
{ type: "text", valid: true, onInput: () => {}, errorText: "" },
|
||||
{
|
||||
type: "text",
|
||||
valid: true,
|
||||
onInput: () => {},
|
||||
errorText: "",
|
||||
placeholder: "",
|
||||
},
|
||||
props
|
||||
);
|
||||
let [isActive, setActive] = createSignal(false);
|
||||
@ -15,6 +21,7 @@ function ValidatedTextInput(props) {
|
||||
}
|
||||
id={props.id}
|
||||
value={props.value}
|
||||
placeholder={props.placeholder}
|
||||
onInput={props.onInput}
|
||||
onFocusOut={() => setActive(true)}
|
||||
/>
|
||||
|
||||
@ -1,11 +1,14 @@
|
||||
function Command({
|
||||
id,
|
||||
protocol,
|
||||
command: commandNumber,
|
||||
device,
|
||||
commandType,
|
||||
title,
|
||||
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;
|
||||
@ -34,6 +37,8 @@ function Command({
|
||||
}
|
||||
|
||||
function setCommandNumber(commandNumber) {
|
||||
if (typeof commandNumber !== "number")
|
||||
throw new Error("Command number must be a number");
|
||||
_commandNumber = commandNumber;
|
||||
}
|
||||
|
||||
@ -42,6 +47,7 @@ function Command({
|
||||
}
|
||||
|
||||
function setDevice(device) {
|
||||
if (typeof device !== "number") throw new Error("Device must be a number");
|
||||
_device = device;
|
||||
}
|
||||
|
||||
@ -54,7 +60,8 @@ function Command({
|
||||
}
|
||||
|
||||
function getTitle() {
|
||||
return _title;
|
||||
if (_title) return _title;
|
||||
return `${Command.getTypeString(_commandType)} (${Command.getProtocolString(_protocol)})`;
|
||||
}
|
||||
|
||||
function setTitle(title) {
|
||||
@ -77,73 +84,148 @@ function Command({
|
||||
};
|
||||
}
|
||||
|
||||
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: "BoseWave",
|
||||
bang_olufsen: "Bang & Olufsen",
|
||||
lego: "Lego",
|
||||
fast: "FAST",
|
||||
whynter: "Whynter",
|
||||
magiquest: "MagiQuest",
|
||||
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.CommandTypes = {
|
||||
power: "Power",
|
||||
input: "Input",
|
||||
one: "1",
|
||||
two: "2",
|
||||
three: "3",
|
||||
four: "4",
|
||||
five: "5",
|
||||
six: "6",
|
||||
seven: "7",
|
||||
eight: "8",
|
||||
nine: "9",
|
||||
zero: "0",
|
||||
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_arrow: "Up Arrow",
|
||||
down_arrow: "Down Arrow",
|
||||
left_arrow: "Left Arrow",
|
||||
right_arrow: "Right Arrow",
|
||||
select: "Select",
|
||||
info: "Info",
|
||||
back: "Back",
|
||||
exit: "Exit",
|
||||
red: "Red",
|
||||
green: "Green",
|
||||
yellow: "Yellow",
|
||||
blue: "Blue",
|
||||
rewind: "Rewind",
|
||||
play: "Play",
|
||||
pause: "Pause",
|
||||
stop: "Stop",
|
||||
forward: "Forward",
|
||||
other: "Other",
|
||||
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,4 +1,4 @@
|
||||
function Remote({ id, title, commands } = {}) {
|
||||
function Remote({ id = "", title = "", commands = [] } = {}) {
|
||||
let _id = id;
|
||||
let _title = title;
|
||||
let _commands = commands;
|
||||
|
||||
@ -31,20 +31,48 @@ const Serializer = (function () {
|
||||
if (!objects) return [];
|
||||
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 deserializeRemote(object) {
|
||||
return new Remote(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));
|
||||
@ -57,8 +85,11 @@ const Serializer = (function () {
|
||||
deserializeDevices,
|
||||
deserializeIntegration,
|
||||
deserializeIntegrations,
|
||||
serializeCommand,
|
||||
serializeCommands,
|
||||
deserializeCommand,
|
||||
deserializeCommands,
|
||||
serializeRemote,
|
||||
deserializeRemote,
|
||||
deserializeRemotes,
|
||||
};
|
||||
|
||||
7
www/src/lib/papaparse-5.5.2.min.js
vendored
Normal file
7
www/src/lib/papaparse-5.5.2.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
@ -24,13 +24,9 @@ function CreateCommandModal(props) {
|
||||
);
|
||||
const isDeviceValid = createMemo(() => device() !== "" && !isNaN(device()));
|
||||
const isCommandTypeValid = createMemo(() => commandType() !== "");
|
||||
const isTitleValid = createMemo(() => title().length >= MIN_TITLE_LENGTH);
|
||||
|
||||
createEffect(() => {
|
||||
let commandString = commandType() ? Command.CommandTypes[commandType()] : "";
|
||||
let protocolString = protocol() ? Command.Protocols[protocol()] : "";
|
||||
setTitle(commandString + " " + protocolString);
|
||||
});
|
||||
const isTitleValid = createMemo(
|
||||
() => title() === "" || title().length >= MIN_TITLE_LENGTH
|
||||
);
|
||||
|
||||
const isFormValid = createMemo(
|
||||
() =>
|
||||
@ -44,13 +40,15 @@ function CreateCommandModal(props) {
|
||||
async function handleCreateCommand() {
|
||||
let command;
|
||||
try {
|
||||
command = await RemotesService.createCommand({
|
||||
protocol: protocol(),
|
||||
commandNumber: commandNumber(),
|
||||
device: device(),
|
||||
commandType: commandType(),
|
||||
title: title(),
|
||||
});
|
||||
command = await RemotesService.createCommand(
|
||||
new Command({
|
||||
protocol: protocol(),
|
||||
commandNumber: parseInt(commandNumber()),
|
||||
device: parseInt(device()),
|
||||
commandType: commandType(),
|
||||
title: title(),
|
||||
})
|
||||
);
|
||||
} catch (e) {
|
||||
setError(e.message);
|
||||
return;
|
||||
@ -87,31 +85,19 @@ function CreateCommandModal(props) {
|
||||
Protocol
|
||||
</label>
|
||||
<div class="col-sm-9">
|
||||
<div class="btn-group">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-secondary dropdown-toggle"
|
||||
data-bs-toggle="dropdown"
|
||||
aria-expanded="false"
|
||||
>
|
||||
{protocol() ? Command.Protocols[protocol()] : "Please select"}
|
||||
</button>
|
||||
<ul
|
||||
class="dropdown-menu"
|
||||
style="max-height: 10em; overflow-y: auto;"
|
||||
>
|
||||
{Object.keys(Command.Protocols).map((protocol) => (
|
||||
<li>
|
||||
<a
|
||||
class="dropdown-item pe-auto"
|
||||
onClick={() => setProtocol(protocol)}
|
||||
>
|
||||
{Command.Protocols[protocol]}
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
<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">
|
||||
@ -148,33 +134,19 @@ function CreateCommandModal(props) {
|
||||
Command Type
|
||||
</label>
|
||||
<div class="col-sm-9">
|
||||
<div class="btn-group">
|
||||
<button
|
||||
type="button"
|
||||
class="btn btn-secondary dropdown-toggle"
|
||||
data-bs-toggle="dropdown"
|
||||
aria-expanded="false"
|
||||
>
|
||||
{commandType()
|
||||
? Command.CommandTypes[commandType()]
|
||||
: "Please select"}
|
||||
</button>
|
||||
<ul
|
||||
class="dropdown-menu"
|
||||
style="max-height: 10em; overflow-y: auto;"
|
||||
>
|
||||
{Object.keys(Command.CommandTypes).map((commandType) => (
|
||||
<li>
|
||||
<a
|
||||
class="dropdown-item pe-auto"
|
||||
onClick={() => setCommandType(commandType)}
|
||||
>
|
||||
{Command.CommandTypes[commandType]}
|
||||
</a>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
<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">
|
||||
|
||||
@ -1,34 +1,43 @@
|
||||
import { createMemo, createResource, createSignal } from "solid-js";
|
||||
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] = createResource(RemotesService.getCommands);
|
||||
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({
|
||||
title: title(),
|
||||
commands: commands(),
|
||||
});
|
||||
remote = await RemotesService.createRemote(
|
||||
new Remote({
|
||||
title: title(),
|
||||
commands: commands(),
|
||||
})
|
||||
);
|
||||
} catch (e) {
|
||||
setError(e.message);
|
||||
console.error(e);
|
||||
return;
|
||||
}
|
||||
resetFields();
|
||||
@ -66,11 +75,11 @@ function CreateRemoteModal(props) {
|
||||
</div>
|
||||
</Show>
|
||||
<div class="mb-3 row">
|
||||
<label for="new_remote_title" class="col-form-label col-sm-3">
|
||||
<label for="new_remote_title" class="col-form-label col-sm-1">
|
||||
Title
|
||||
</label>
|
||||
<ValidatedTextInput
|
||||
class="col-sm-9"
|
||||
class="col-sm-11"
|
||||
id="new_remote_title"
|
||||
valid={isTitleValid()}
|
||||
value={title()}
|
||||
@ -108,7 +117,7 @@ function CreateRemoteModal(props) {
|
||||
);
|
||||
}
|
||||
|
||||
CreateRemoteModal.Handler = new ModalHandler();
|
||||
CreateRemoteModal.Handler = modalHandler;
|
||||
CreateRemoteModal.onRemoteCreated = (callback) =>
|
||||
eventEmitter.on(REMOTE_CREATED_EVENT, callback);
|
||||
|
||||
|
||||
66
www/src/modals/delete-remote-modal.jsx
Normal file
66
www/src/modals/delete-remote-modal.jsx
Normal file
@ -0,0 +1,66 @@
|
||||
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;
|
||||
199
www/src/modals/edit-command-modal.jsx
Normal file
199
www/src/modals/edit-command-modal.jsx
Normal file
@ -0,0 +1,199 @@
|
||||
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;
|
||||
145
www/src/modals/edit-remote-modal.jsx
Normal file
145
www/src/modals/edit-remote-modal.jsx
Normal file
@ -0,0 +1,145 @@
|
||||
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;
|
||||
500
www/src/modals/import-commands-modal.jsx
Normal file
500
www/src/modals/import-commands-modal.jsx
Normal file
@ -0,0 +1,500 @@
|
||||
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,11 +1,23 @@
|
||||
import EventEmitter from "../tools/event-emitter";
|
||||
|
||||
const SHOW_EVENT = "show";
|
||||
const HIDE_EVENT = "hide";
|
||||
|
||||
function ModalHandler() {
|
||||
let _ref;
|
||||
let _modalRef;
|
||||
let _modalId;
|
||||
let eventEmitter = new EventEmitter();
|
||||
|
||||
function setRef(ref) {
|
||||
_ref = 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() {
|
||||
@ -21,15 +33,11 @@ function ModalHandler() {
|
||||
}
|
||||
|
||||
function onHidden(callback) {
|
||||
_ref.addEventListener('hidden.bs.modal', () => {
|
||||
callback();
|
||||
});
|
||||
eventEmitter.on(HIDE_EVENT, callback);
|
||||
}
|
||||
|
||||
function onShow(callback) {
|
||||
_ref.addEventListener('show.bs.modal', () => {
|
||||
callback();
|
||||
});
|
||||
eventEmitter.on(SHOW_EVENT, callback);
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@ -9,6 +9,10 @@ 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 modals = [
|
||||
@ -57,6 +61,26 @@ const ModalRegistry = (function () {
|
||||
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) {
|
||||
|
||||
@ -79,6 +79,23 @@ function DeviceService() {
|
||||
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() {
|
||||
let response = await Net.sendRequest({
|
||||
method: "GET",
|
||||
@ -113,6 +130,7 @@ function DeviceService() {
|
||||
updateDevice,
|
||||
deleteDevice,
|
||||
getRegistrationCode,
|
||||
getIntegration,
|
||||
getIntegrations,
|
||||
deleteIntegration,
|
||||
};
|
||||
|
||||
@ -1,75 +1,163 @@
|
||||
import Command from "../data/command";
|
||||
import Serializer from "../data/serializer";
|
||||
import Net from "../tools/net";
|
||||
import WebRTCService from "./webrtc-service";
|
||||
|
||||
function RemotesService() {
|
||||
let commands = [
|
||||
new Command({
|
||||
id: 1,
|
||||
protocol: "samsung",
|
||||
commandNumber: 1,
|
||||
device: 7,
|
||||
commandType: "power",
|
||||
title: "Power Samsung",
|
||||
}),
|
||||
new Command({
|
||||
id: 2,
|
||||
protocol: "samsung",
|
||||
commandNumber: 2,
|
||||
device: 7,
|
||||
commandType: "input",
|
||||
title: "Input Samsung",
|
||||
}),
|
||||
new Command({
|
||||
id: 3,
|
||||
protocol: "samsung",
|
||||
commandNumber: 3,
|
||||
device: 7,
|
||||
commandType: "volume_up",
|
||||
title: "Volume Up Samsung",
|
||||
}),
|
||||
];
|
||||
let remotes = [];
|
||||
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() {
|
||||
return [].concat(remotes);
|
||||
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(remoteObject) {
|
||||
let remote = Serializer.deserializeRemote(remoteObject);
|
||||
let id = Math.random().toString(36).substr(2, 9);
|
||||
remote.setId(id);
|
||||
remotes.push(remote);
|
||||
return remote;
|
||||
}
|
||||
async function createRemote(remote) {
|
||||
let remoteObject = Serializer.serializeRemote(remote);
|
||||
let response = await Net.sendJsonRequest({
|
||||
method: "POST",
|
||||
url: "/api/remotes",
|
||||
data: remoteObject,
|
||||
});
|
||||
|
||||
async function getCommands() {
|
||||
return [].concat(commands);
|
||||
}
|
||||
|
||||
async function createCommand(commandObject) {
|
||||
let command = Serializer.deserializeCommand(commandObject);
|
||||
let id = Math.random().toString(36).substr(2, 9);
|
||||
command.setId(id);
|
||||
commands.push(command);
|
||||
return command;
|
||||
}
|
||||
|
||||
async function deleteCommand(commandId) {
|
||||
let index = commands.findIndex((command) => command.getId() === commandId);
|
||||
if (index >= 0) {
|
||||
commands.splice(index, 1);
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
RemotesService = new RemotesService();
|
||||
RemoteService = new RemoteService();
|
||||
|
||||
export default RemotesService;
|
||||
export default RemoteService;
|
||||
|
||||
@ -8,10 +8,12 @@ function WebRTCService() {
|
||||
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();
|
||||
|
||||
@ -57,12 +59,19 @@ function WebRTCService() {
|
||||
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);
|
||||
eventEmitter.dispatchEvent(
|
||||
ICE_CONNECTION_STATE_CHANGE_EVENT,
|
||||
peerConnection.iceConnectionState
|
||||
);
|
||||
}
|
||||
|
||||
async function negotiate(targetId) {
|
||||
@ -125,12 +134,27 @@ function WebRTCService() {
|
||||
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" }],
|
||||
@ -155,7 +179,10 @@ function WebRTCService() {
|
||||
disconnect,
|
||||
setVideoElement,
|
||||
getVideoElement,
|
||||
sendDataString,
|
||||
sendDataJson,
|
||||
onStateChanged,
|
||||
onDataChannelOpen,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
30
www/src/tools/file-utils.js
Normal file
30
www/src/tools/file-utils.js
Normal file
@ -0,0 +1,30 @@
|
||||
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,195 +0,0 @@
|
||||
import {
|
||||
createResource,
|
||||
createSignal,
|
||||
mergeProps,
|
||||
onMount,
|
||||
Show,
|
||||
} from "solid-js";
|
||||
|
||||
import List from "../components/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={(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>
|
||||
</Show>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default DevicesView;
|
||||
76
www/src/views/devices/devices-list-view.jsx
Normal file
76
www/src/views/devices/devices-list-view.jsx
Normal file
@ -0,0 +1,76 @@
|
||||
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;
|
||||
105
www/src/views/devices/devices-view.jsx
Normal file
105
www/src/views/devices/devices-view.jsx
Normal file
@ -0,0 +1,105 @@
|
||||
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;
|
||||
103
www/src/views/devices/integration-list-view.jsx
Normal file
103
www/src/views/devices/integration-list-view.jsx
Normal file
@ -0,0 +1,103 @@
|
||||
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;
|
||||
181
www/src/views/devices/integration-view.jsx
Normal file
181
www/src/views/devices/integration-view.jsx
Normal file
@ -0,0 +1,181 @@
|
||||
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;
|
||||
@ -1,82 +0,0 @@
|
||||
import { createMemo, createSignal } from "solid-js";
|
||||
import Integration from "../data/integration";
|
||||
import WebRTCService from "../services/webrtc-service";
|
||||
|
||||
const [integration, setIntegration] = createSignal(null);
|
||||
|
||||
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);
|
||||
let videoElement = null;
|
||||
|
||||
function handleConnectWebRTC() {
|
||||
let integrationId = integration().getId();
|
||||
WebRTCService.setVideoElement(videoElement);
|
||||
WebRTCService.connect(integrationId);
|
||||
}
|
||||
|
||||
function handleDisconnectWebRTC() {
|
||||
WebRTCService.disconnect();
|
||||
}
|
||||
|
||||
function handleConnectionStateChanged(state) {
|
||||
setConnectionState(state);
|
||||
}
|
||||
|
||||
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>
|
||||
<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-column justify-content-center align-items-center rounded overflow-hidden">
|
||||
<video
|
||||
ref={videoElement}
|
||||
class="w-100 h-100"
|
||||
style="background-color: #000"
|
||||
></video>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
IntegrationView.setIntegration = setIntegration;
|
||||
export default IntegrationView;
|
||||
@ -11,7 +11,7 @@ import {
|
||||
import ModalRegistry from "../modals/modal-registry.jsx";
|
||||
import UserService from "../services/user-service.js";
|
||||
import UrlUtils from "../tools/url-utils.js";
|
||||
import DevicesView from "./devices-view.jsx";
|
||||
import DevicesView from "./devices/devices-view.jsx";
|
||||
import SettingsView from "./settings-view.jsx";
|
||||
|
||||
import {
|
||||
@ -21,12 +21,12 @@ import {
|
||||
REMOTES_VIEW,
|
||||
SETTINGS_VIEW,
|
||||
} from "../data/constants.js";
|
||||
import IntegrationView from "./integration-view.jsx";
|
||||
import IntegrationView from "./devices/integration-view.jsx";
|
||||
import RemotesView from "./remotes/remotes-view.jsx";
|
||||
|
||||
let [activeView, setActiveView] = createSignal(DEVICES_VIEW);
|
||||
|
||||
const MainView = function (props) {
|
||||
function MainView(props) {
|
||||
props = mergeProps({ onLogout: () => {} }, props);
|
||||
|
||||
const [userInfo] = createResource(() => UserService.getUserInfo());
|
||||
@ -55,7 +55,6 @@ const MainView = function (props) {
|
||||
|
||||
function setViewFromUrl() {
|
||||
let view = UrlUtils.getQueryParameter("view");
|
||||
console.log(view)
|
||||
if (view) {
|
||||
setActiveView(view);
|
||||
}
|
||||
@ -100,7 +99,8 @@ const MainView = function (props) {
|
||||
if (activeView() === view) return;
|
||||
setActiveView(view);
|
||||
let url = UrlUtils.getUrl();
|
||||
url = UrlUtils.addQueryParameter(url, "view", view);
|
||||
url = UrlUtils.addQueryParameter(url, "view", activeView());
|
||||
url = UrlUtils.removeQueryParameter(url, "tab");
|
||||
UrlUtils.setUrl(url);
|
||||
}
|
||||
|
||||
@ -114,7 +114,7 @@ const MainView = function (props) {
|
||||
<div class="d-flex flex-column" style="height:100vh">
|
||||
<Modals></Modals>
|
||||
<HeaderBar></HeaderBar>
|
||||
<ActiveView class={"bg-body-tertiary"}></ActiveView>
|
||||
<ActiveView class={"bg-body-tertiary flex-fill"}></ActiveView>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -211,7 +211,10 @@ const MainView = function (props) {
|
||||
<SettingsView {...props} />
|
||||
</Match>
|
||||
<Match when={activeView() === DEVICES_VIEW}>
|
||||
<DevicesView {...props} onIntegrationClicked={handleIntegrationClicked} />
|
||||
<DevicesView
|
||||
{...props}
|
||||
onIntegrationClicked={handleIntegrationClicked}
|
||||
/>
|
||||
</Match>
|
||||
<Match when={activeView() === INTEGRATION_VIEW}>
|
||||
<IntegrationView {...props} />
|
||||
@ -225,7 +228,7 @@ const MainView = function (props) {
|
||||
}
|
||||
|
||||
return render();
|
||||
};
|
||||
}
|
||||
|
||||
MainView.setActiveView = setActiveView;
|
||||
|
||||
|
||||
@ -3,6 +3,8 @@ 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(
|
||||
@ -16,21 +18,38 @@ function CommandsList(props) {
|
||||
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">
|
||||
@ -38,9 +57,13 @@ function CommandsList(props) {
|
||||
{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 Command
|
||||
New
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@ -59,7 +82,7 @@ function CommandsList(props) {
|
||||
{
|
||||
id: "protocol",
|
||||
name: "Protocol",
|
||||
width: 10,
|
||||
width: 8,
|
||||
},
|
||||
{
|
||||
id: "type",
|
||||
@ -69,7 +92,7 @@ function CommandsList(props) {
|
||||
{
|
||||
id: "options",
|
||||
name: "",
|
||||
width: 4,
|
||||
width: 6,
|
||||
},
|
||||
]}
|
||||
items={(commands() || []).map((command) => ({
|
||||
@ -88,6 +111,15 @@ function CommandsList(props) {
|
||||
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) => {
|
||||
|
||||
@ -1,12 +1,19 @@
|
||||
import { createResource, onMount } from "solid-js";
|
||||
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();
|
||||
@ -16,11 +23,46 @@ function RemotesList(props) {
|
||||
refetchRemotes();
|
||||
});
|
||||
|
||||
DeleteRemoteModal.onRemoteDeleted(() => {
|
||||
refetchRemotes();
|
||||
});
|
||||
|
||||
EditRemoteModal.onRemoteEdited(() => {
|
||||
refetchRemotes();
|
||||
});
|
||||
|
||||
function handleNewRemote() {
|
||||
CreateRemoteModal.Handler.show();
|
||||
}
|
||||
|
||||
function handleDeleteRemote(remote) {}
|
||||
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 (
|
||||
<>
|
||||
@ -29,9 +71,17 @@ function RemotesList(props) {
|
||||
{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 Remote
|
||||
New
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@ -50,10 +100,11 @@ function RemotesList(props) {
|
||||
{
|
||||
id: "options",
|
||||
name: "",
|
||||
width: 4,
|
||||
width: 6,
|
||||
},
|
||||
]}
|
||||
onListItemClick={() => {}}
|
||||
onListItemsSelect={handleListItemsSelect}
|
||||
items={(remotes() || []).map((remote) => ({
|
||||
id: {
|
||||
html: <span class="font-monospace">{remote.getId()}</span>,
|
||||
@ -64,6 +115,15 @@ function RemotesList(props) {
|
||||
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) => {
|
||||
|
||||
@ -1,10 +1,13 @@
|
||||
import {
|
||||
createEffect,
|
||||
createSignal,
|
||||
Match,
|
||||
mergeProps,
|
||||
Switch
|
||||
onCleanup,
|
||||
Switch,
|
||||
} from "solid-js";
|
||||
|
||||
import UrlUtils from "../../tools/url-utils";
|
||||
import CommandsList from "./commands-list-view";
|
||||
import RemotesList from "./remotes-list-view";
|
||||
|
||||
@ -12,16 +15,39 @@ 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")
|
||||
(currentView() === REMOTES_LIST_VIEW
|
||||
? " btn-secondary"
|
||||
: " btn-dark")
|
||||
}
|
||||
onClick={() => setCurrentView(REMOTES_LIST_VIEW)}
|
||||
>
|
||||
@ -31,7 +57,9 @@ function RemotesView(props) {
|
||||
<button
|
||||
class={
|
||||
"btn me-2 mb-3" +
|
||||
(currentView() === COMMANDS_LIST_VIEW ? " btn-secondary" : " btn-dark")
|
||||
(currentView() === COMMANDS_LIST_VIEW
|
||||
? " btn-secondary"
|
||||
: " btn-dark")
|
||||
}
|
||||
onClick={() => setCurrentView(COMMANDS_LIST_VIEW)}
|
||||
>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user