add config

This commit is contained in:
Simon Vieille 2023-08-25 11:19:54 +02:00
parent e6a690272e
commit a3ee86f81b
Signed by: deblan
GPG key ID: 579388D585F70417
11 changed files with 240 additions and 169 deletions

View file

@ -19,4 +19,4 @@ build: deps
CGO_ENABLED=$(CGO_ENABLED) GOARCH=amd64 GOOS=linux $(CC) $(CFLAGS) -o $(BUILD_DIR)/$(LINUX_BIN) -ldflags="$(LDFLAGS)" -gcflags="$(GCFLAGS)" -asmflags="$(ASMFLAGS)"
watch:
gowatch -o build/app-live-linux-amd64
gowatch -o build/app-live-linux-amd64 -args='./config.yaml'

Binary file not shown.

44
config.go Normal file
View file

@ -0,0 +1,44 @@
package main
import (
"gopkg.in/yaml.v3"
"os"
)
type ServerAuthConfig struct {
Username string `yaml:"username"`
Password string `yaml:"password"`
}
type ServerConfig struct {
Listen string `yaml:"listen"`
Auth ServerAuthConfig `yaml:"auth"`
}
type RemoteConfigItem struct {
Type string `yaml:"type"`
Label string `yaml:"label"`
Items []map[string]string `yaml:"items"`
}
type Config struct {
Server ServerConfig `yaml:"server"`
Remote map[string][]RemoteConfigItem `yaml:"remote"`
}
func createConfigFromFile(file string) (Config, error) {
data, err := os.ReadFile(file)
value := Config{}
if err != nil {
return value, err
}
err = yaml.Unmarshal(data, &value)
if err != nil {
return value, err
}
return value, nil
}

48
config.yaml Normal file
View file

@ -0,0 +1,48 @@
server:
listen: 0.0.0.0:4000
auth:
username: admin
password: admin
remote:
Keyboard:
- type: live_text
label: Live text
- type: text
label: Text
- type: keys
label: Keys
- type: shortcuts
label: Shortcuts
i3:
- type: messages
label: Workspaces
items:
- '1. DBL': '{"type":"workspace","value":"1. DBL"}'
- '2. WWW': '{"type":"workspace","value":"2. WWW"}'
- '3. MAIL': '{"type":"workspace","value":"3. MAIL"}'
- '4. IM': '{"type":"workspace","value":"4. IM"}'
- '5': '{"type":"workspace","value":"5"}'
- '6. MEDIA': '{"type":"workspace","value":"6. MEDIA"}'
- '7. DEV': '{"type":"workspace","value":"7. DEV"}'
- '8. DEV': '{"type":"workspace","value":"8. DEV"}'
- '9. DEV': '{"type":"workspace","value":"9. DEV"}'
- '10': '{"type":"workspace","value":"10"}'
- '11': '{"type":"workspace","value":"11"}'
- '12': '{"type":"workspace","value":"12"}'
- type: messages
label: Software
items: []
Mouse:
- type: mouse
Media:
- type: spotify
label: Spotify
- type: volume
label: Volume
Desktop:
- type: screenshot
label: Screenshot
- type: live_video
label: Live video

1
go.mod
View file

@ -21,4 +21,5 @@ require (
golang.org/x/sys v0.11.0 // indirect
golang.org/x/text v0.12.0 // indirect
golang.org/x/time v0.3.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

2
go.sum
View file

@ -65,3 +65,5 @@ golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View file

@ -5,6 +5,12 @@ import (
"net/http"
)
func homeController(c echo.Context) error {
return c.HTML(http.StatusOK, view("views/page/home.html", nil))
type HomeViewParams struct {
Config Config
}
func homeController(c echo.Context) error {
return c.HTML(http.StatusOK, view("views/page/home.html", HomeViewParams{
Config: config,
}))
}

21
main.go
View file

@ -3,6 +3,7 @@ package main
import (
"crypto/subtle"
"embed"
"fmt"
rice "github.com/GeertJohan/go.rice"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
@ -16,7 +17,8 @@ var (
//go:embed static
staticFiles embed.FS
//go:embed views/layout views/page
views embed.FS
views embed.FS
config Config
)
func main() {
@ -31,6 +33,23 @@ func main() {
RI3_BIND = "0.0.0.0:4000"
}
if len(os.Args) != 2 {
fmt.Errorf("Configuration requried")
os.Exit(1)
}
value, err := createConfigFromFile(os.Args[1])
if err != nil {
fmt.Printf("%+v\n", err)
fmt.Errorf("Configuration error")
os.Exit(1)
}
config = value
fmt.Printf("%+v\n", config)
assetHandler := http.FileServer(rice.MustFindBox("static").HTTPBox())
e.Use(middleware.BasicAuth(func(username, password string, c echo.Context) (bool, error) {

View file

@ -36,9 +36,9 @@ func init() {
}
file6 := &embedded.EmbeddedFile{
Filename: "main.css",
FileModTime: time.Unix(1692909231, 0),
FileModTime: time.Unix(1692955090, 0),
Content: string("a {\n color: #1e3650;\n}\n\n.btn-primary {\n background: #1e3650;\n border-color: #0e2640;\n}\n\n.nav-pills .nav-link.active {\n background: #1e3650;\n}\n\n.nav-pills .nav-link {\n padding-left: 3px;\n padding-right: 3px;\n}\n\n.nav-link {\n font-size: 10px;\n}\n\n.legend {\n color: #777;\n margin: 3px 0;\n padding: 3px 0;\n border-bottom: 1px solid #eee;\n font-size: 11px;\n text-transform: uppercase;\n}\n\n.btn-sm {\n font-size: 9px;\n}\n\n.select2 {\n min-width: 100%;\n}\n\n.line {\n height: 3px;\n}\n\n.pane {\n display: none;\n}\n\n.no-margin {\n margin: 0;\n}\n\n.no-padding {\n padding: 0;\n}\n\n.no-radius {\n border-radius: 0 !important;\n}\n\n#pointer {\n height: calc(100vh - 80px);\n margin: auto;\n background: #ccc;\n -webkit-touch-callout: none;\n -webkit-user-select: none;\n -khtml-user-select: none;\n -moz-user-select: none;\n -ms-user-select: none;\n user-select: none;\n}\n\n#scrollbar {\n height: calc(100vh - 80px);\n width: 50px;\n background: #333;\n position: absolute;\n z-index: 100;\n right: 0;\n}\n\n.fullscreen #scrollbar {\n height: calc(100vh - 150px);\n}\n\n.fullscreen #pointer {\n height: calc(100vh - 150px);\n}\n\n#pane-pointer .form-group {\n padding: 0;\n margin: 0;\n}\n\n#pointer-buttons {\n position: absolute;\n margin-top: -42px;\n width: 100%;\n z-index: 110;\n}\n\n#pointer-buttons .btn {\n height: 50px;\n}\n\n#disconneced {\n position: absolute;\n top: 0;\n width: 100%;\n background: #ff6161;\n color: #fff;\n padding: 5px;\n}\n\n#disconneced a {\n color: #fff;\n font-weight: bold;\n}\n\n#nav {\n border-bottom: 2px solid #1e3650;\n}\n\n#shortcuts_special_keys input {\n display: none;\n}\n\n#response {\n position: absolute;\n bottom: 0;\n width: 100%;\n color: #fff;\n background: #748c26;\n padding: 5px;\n display: none;\n}\n\n#screenshot img {\n max-width: 100%;\n margin-top: 10px;\n cursor: pointer;\n}\n"),
Content: string("a {\n color: #1e3650;\n}\n\n.btn-primary {\n background: #1e3650;\n border-color: #0e2640;\n}\n\n.nav-pills .nav-link.active {\n background: #1e3650;\n}\n\n.nav-pills .nav-link {\n padding-left: 3px;\n padding-right: 3px;\n}\n\n.nav-link {\n font-size: 10px;\n}\n\n.legend {\n color: #777;\n margin: 3px 0;\n padding: 3px 0;\n border-bottom: 1px solid #eee;\n font-size: 11px;\n text-transform: uppercase;\n}\n\n.btn-sm {\n font-size: 9px;\n}\n\n.select2 {\n min-width: 100%;\n}\n\n.line {\n height: 3px;\n}\n\n.pane {\n display: none;\n}\n\n.no-margin {\n margin: 0;\n}\n\n.no-padding {\n padding: 0;\n}\n\n.no-radius {\n border-radius: 0 !important;\n}\n\n#pointer {\n height: calc(100vh - 80px);\n margin: auto;\n background: #ccc;\n -webkit-touch-callout: none;\n -webkit-user-select: none;\n -khtml-user-select: none;\n -moz-user-select: none;\n -ms-user-select: none;\n user-select: none;\n}\n\n#scrollbar {\n height: calc(100vh - 80px);\n width: 50px;\n background: #333;\n position: absolute;\n z-index: 100;\n right: 0;\n}\n\n.fullscreen #scrollbar {\n height: calc(100vh - 150px);\n}\n\n.fullscreen #pointer {\n height: calc(100vh - 150px);\n}\n\n#pane-pointer .form-group {\n padding: 0;\n margin: 0;\n}\n\n#pointer-buttons {\n margin-top: -42px;\n width: 100%;\n z-index: 110;\n}\n\n#pointer-buttons .btn {\n height: 50px;\n}\n\n#disconneced {\n position: absolute;\n top: 0;\n width: 100%;\n background: #ff6161;\n color: #fff;\n padding: 5px;\n}\n\n#disconneced a {\n color: #fff;\n font-weight: bold;\n}\n\n#nav {\n border-bottom: 2px solid #1e3650;\n}\n\n#shortcuts_special_keys input {\n display: none;\n}\n\n#response {\n position: absolute;\n bottom: 0;\n width: 100%;\n color: #fff;\n background: #748c26;\n padding: 5px;\n display: none;\n}\n\n#screenshot img {\n max-width: 100%;\n margin-top: 10px;\n cursor: pointer;\n}\n"),
}
file7 := &embedded.EmbeddedFile{
Filename: "main.js",
@ -50,7 +50,7 @@ func init() {
// define dirs
dir1 := &embedded.EmbeddedDir{
Filename: "",
DirModTime: time.Unix(1692909534, 0),
DirModTime: time.Unix(1692955097, 0),
ChildFiles: []*embedded.EmbeddedFile{
file2, // "bootstrap.bundle.min.js"
file3, // "bootstrap.min.css"
@ -68,7 +68,7 @@ func init() {
// register embeddedBox
embedded.RegisterEmbeddedBox(`static`, &embedded.EmbeddedBox{
Name: `static`,
Time: time.Unix(1692909534, 0),
Time: time.Unix(1692955097, 0),
Dirs: map[string]*embedded.EmbeddedDir{
"": dir1,
},

View file

@ -92,7 +92,6 @@ a {
}
#pointer-buttons {
position: absolute;
margin-top: -42px;
width: 100%;
z-index: 110;

View file

@ -4,21 +4,11 @@
<div class="row no-margin">
<div class="col-12 no-padding">
<ul class="nav nav-pills nav-fill" id="nav">
<li class="nav-item">
<a class="nav-link no-radius" href="#pane-keyboard">KEYBOARD</a>
</li>
<li class="nav-item">
<a class="nav-link no-radius" href="#pane-i3">I3</a>
</li>
<li class="nav-item">
<a class="nav-link no-radius" href="#pane-pointer">MOUSE</a>
</li>
<li class="nav-item">
<a class="nav-link no-radius" href="#pane-media">MEDIA</a>
</li>
<li class="nav-item">
<a class="nav-link no-radius" href="#pane-desktop">DESKTOP</a>
</li>
{{range $key, $values := .Config.Remote}}
<li class="nav-item">
<a class="nav-link no-radius" href="#{{$key}}">{{$key}}</a>
</li>
{{end}}
<li class="nav-item">
<a class="nav-link no-radius btn-fullscreen" data-target="html" href="#">💻</a>
</li>
@ -28,76 +18,114 @@
</div>
<div class="container-fluid">
<div id="pane-keyboard" class="pane">
<div class="row">
<div class="col-12">
<p class="legend">Live text</p>
</div>
<div class="form-group col-12">
<input type="text" class="form-control live-text" name="text">
</div>
{{range $key, $values := .Config.Remote}}
<div id="{{$key}}" class="pane">
{{range $key2, $value := $values}}
<div class="row" {{if eq $value.Type "mouse"}} id="pane-pointer"{{end}}>
{{if ne $value.Label ""}}
<div class="col-12">
<p class="legend">{{$value.Label}}</p>
</div>
{{end}}
{{if eq $value.Type "live_text"}}
<div class="form-group col-12">
<input type="text" class="form-control live-text" name="text">
</div>
{{end}}
{{if eq $value.Type "text"}}
<div class="form-group col-6">
<input type="text" class="form-control" id="text">
</div>
<div class="col-6">
<button type="button" id="text-send" class="btn btn-primary">Send</button>
<button type="button" id="text-clear" class="btn btn-secondary">Clear</button>
</div>
{{end}}
{{if eq $value.Type "keys"}}
<div class="col-12">
<button type="button" data-msg='{"type":"key","value":"left"}' class="btn btn-secondary"></button>
<button type="button" data-msg='{"type":"key","value":"up"}' class="btn btn-secondary"></button>
<button type="button" data-msg='{"type":"key","value":"down"}' class="btn btn-secondary"></button>
<button type="button" data-msg='{"type":"key","value":"right"}' class="btn btn-secondary"></button>
</div>
<div class="line col-12"></div>
<div class="col-12">
<button type="button" data-msg='{"type":"key","value":"escape"}' class="btn btn-secondary">Escape</button>
<button type="button" data-msg='{"type":"key","value":"tab"}' class="btn btn-secondary">TAB</button>
<button type="button" data-msg='{"type":"key","value":"backspace"}' class="btn btn-secondary">Backspace</button>
<button type="button" data-msg='{"type":"key","value":"enter"}' class="btn btn-secondary">Enter</button>
</div>
{{end}}
{{if eq $value.Type "shortcuts"}}
<div class="col-9" id="shortcuts_special_keys">
<label class="btn btn-secondary" for="shortcuts_special_key_ctrl">
<input type="checkbox" value="ctrl" id="shortcuts_special_key_ctrl">
ctrl
</label>
<label class="btn btn-secondary" for="shortcuts_special_key_shift">
<input type="checkbox" value="shift" id="shortcuts_special_key_shift">
shift
</label>
<label class="btn btn-secondary" for="shortcuts_special_key_alt">
<input type="checkbox" value="alt" id="shortcuts_special_key_alt">
alt
</label>
<label class="btn btn-secondary" for="shortcuts_special_key_win">
<input type="checkbox" value="win" id="shortcuts_special_key_win">
win
</label>
</div>
<div class="form-group col-3">
<input type="text" id="shortcut-key" class="form-control" name="shortcuts_char">
</div>
<div class="col-12">
<button type="button" id="shortcut-send" class="btn btn-primary">Send</button>
<button type="button" id="shortcut-clear" class="btn btn-secondary">Clear</button>
</div>
{{end}}
{{if eq $value.Type "spotify"}}
<div class="col-12">
<button type="button" data-msg='{"type":"media","value":"playpause"}' class="btn btn-secondary">Play/Pause</button>
<button type="button" data-msg='{"type":"media","value":"next"}' class="btn btn-secondary">Next</button>
<button type="button" data-msg='{"type":"media","value":"prev"}' class="btn btn-secondary">Previous</button>
</div>
{{end}}
{{if eq $value.Type "volume"}}
<div class="col-12">
<button type="button" data-msg='{"type":"volume","value":"0"}' class="btn btn-secondary">0%</button>
<button type="button" data-msg='{"type":"volume","value":"25"}' class="btn btn-secondary">25%</button>
<button type="button" data-msg='{"type":"volume","value":"50"}' class="btn btn-secondary">50%</button>
<button type="button" data-msg='{"type":"volume","value":"75"}' class="btn btn-secondary">75%</button>
<button type="button" data-msg='{"type":"volume","value":"100"}' class="btn btn-secondary">100%</button>
</div>
<div class="line col-12"></div>
<div class="col-12">
<button type="button" data-msg='{"type":"volume","value":"down"}' class="btn btn-secondary">-</button>
<button type="button" data-msg='{"type":"volume","value":"up"}' class="btn btn-secondary">+</button>
</div>
{{end}}
{{if eq $value.Type "mouse"}}
<div class="form-group col-12">
<input type="text" class="form-control live-text" placeholder="Live text" name="text">
</div/>
<div id="scrollbar"></div>
<div id="pointer"></div>
<div id="pointer-buttons">
<button type="button" data-msg='{"type":"pointer","click":"left"}' class="btn btn-primary no-radius col-5">&nbsp;</button><button type="button no-margin" data-msg='{"type":"pointer","click":"middle"}' class="btn btn-secondary no-radius col-2">&nbsp;</button><button type="button no-margin" data-msg='{"type":"pointer","click":"right"}' class="btn btn-primary no-radius col-5">&nbsp;</button>
</div>
{{end}}
</div>
{{end}}
</div>
<div class="row">
<div class="col-12">
<p class="legend">TEXT</p>
</div>
<div class="form-group col-6">
<input type="text" class="form-control" id="text">
</div>
<div class="col-6">
<button type="button" id="text-send" class="btn btn-primary">Send</button>
<button type="button" id="text-clear" class="btn btn-secondary">Clear</button>
</div>
</div>
<div class="row">
<div class="col-12">
<p class="legend">Keys</p>
</div>
<div class="col-12">
<button type="button" data-msg='{"type":"key","value":"left"}' class="btn btn-secondary"></button>
<button type="button" data-msg='{"type":"key","value":"up"}' class="btn btn-secondary"></button>
<button type="button" data-msg='{"type":"key","value":"down"}' class="btn btn-secondary"></button>
<button type="button" data-msg='{"type":"key","value":"right"}' class="btn btn-secondary"></button>
</div>
<div class="line col-12"></div>
<div class="col-12">
<button type="button" data-msg='{"type":"key","value":"escape"}' class="btn btn-secondary">Escape</button>
<button type="button" data-msg='{"type":"key","value":"tab"}' class="btn btn-secondary">TAB</button>
<button type="button" data-msg='{"type":"key","value":"backspace"}' class="btn btn-secondary">Backspace</button>
<button type="button" data-msg='{"type":"key","value":"enter"}' class="btn btn-secondary">Enter</button>
</div>
</div>
<div class="row">
<div class="col-12">
<p class="legend">Shortcuts</p>
</div>
<div class="col-9" id="shortcuts_special_keys">
<label class="btn btn-secondary" for="shortcuts_special_key_ctrl">
<input type="checkbox" value="ctrl" id="shortcuts_special_key_ctrl">
ctrl
</label>
<label class="btn btn-secondary" for="shortcuts_special_key_shift">
<input type="checkbox" value="shift" id="shortcuts_special_key_shift">
shift
</label>
<label class="btn btn-secondary" for="shortcuts_special_key_alt">
<input type="checkbox" value="alt" id="shortcuts_special_key_alt">
alt
</label>
<label class="btn btn-secondary" for="shortcuts_special_key_win">
<input type="checkbox" value="win" id="shortcuts_special_key_win">
win
</label>
</div>
<div class="form-group col-3">
<input type="text" id="shortcut-key" class="form-control" name="shortcuts_char">
</div>
<div class="col-12">
<button type="button" id="shortcut-send" class="btn btn-primary">Send</button>
<button type="button" id="shortcut-clear" class="btn btn-secondary">Clear</button>
</div>
</div>
</div>
{{end}}
<div id="pane-i3" class="pane">
<div class="row">
@ -121,84 +149,8 @@
<button type="button" data-msg='{"type":"workspace","value":"12"}' class="btn btn-secondary btn-sm">12</button>
</div>
</div>
<div class="row">
<div class="col-12">
<p class="legend">Software</p>
</div>
<div class="col-12">
<button type="button" data-msg='{"type":"messages","value":[{"type":"keys","value":"win,d"},{"type":"text","value":"urxvt"},{"type":"key","value":"enter"}]}' class="btn btn-secondary">urxvt</button>
<button type="button" data-msg='{"type":"keys","value":"win,d"}' class="btn btn-secondary">dmenu</button>
</div>
<div class="col-12">
<p class="legend">UI</p>
</div>
<div class="col-12">
<button type="button" data-msg='{"type":"keys","value":"win,z"}' class="btn btn-secondary">win+z</button>
<button type="button" data-msg='{"type":"keys","value":"win,x"}' class="btn btn-secondary">win+x</button>
<button type="button" data-msg='{"type":"keys","value":"win,c"}' class="btn btn-secondary">win+c</button>
<button type="button" data-msg='{"type":"messages","value":[{"type":"text","value":"zp"},{"type":"key","value":"enter"}]}' class="btn btn-secondary">zp</button>
<button type="button" data-msg='{"type":"messages","value":[{"type":"text","value":"zm"},{"type":"key","value":"enter"}]}' class="btn btn-secondary">zm</button>
</div>
<div class="line col-12"></div>
<div class="col-12">
<button type="button" data-msg='{"type":"messages","value":[{"type":"keys","value":"win,d"},{"type":"text","value":"no-screensaver"},{"type":"key","value":"enter"}]}' class="btn btn-secondary">no-screensaver[on]</button>
<button type="button" data-msg='{"type":"messages","value":[{"type":"keys","value":"win,d"},{"type":"text","value":"pkill no-screensaver"},{"type":"key","value":"enter"}]}' class="btn btn-secondary">no-screensaver[off]</button>
</div>
<div class="col-12">
<p class="legend">Movie</p>
</div>
<div class="col-12">
<button type="button" data-msg='{"type":"messages","value":[{"type":"text","value":"v;mll"},{"type":"key","value":"enter"}]}' class="btn btn-secondary">v;mll</button>
<button type="button" data-msg='{"type":"messages","value":[{"type":"text","value":"rt -l"},{"type":"key","value":"enter"}]}' class="btn btn-secondary">rt -l</button>
</div>
<div class="line col-12"></div>
<div class="col-12">
<button type="button" data-msg='{"type":"messages","value":[{"type":"text","value":"mug"},{"type":"key","value":"enter"}]}' class="btn btn-secondary">mug</button>
<button type="button" data-msg='{"type":"messages","value":[{"type":"text","value":"mup"},{"type":"key","value":"enter"}]}' class="btn btn-secondary">mup</button>
<button type="button" data-msg='{"type":"messages","value":[{"type":"text","value":"mug 1"},{"type":"key","value":"enter"}]}' class="btn btn-secondary">mug 1</button>
<button type="button" data-msg='{"type":"messages","value":[{"type":"text","value":"mup 1"},{"type":"key","value":"enter"}]}' class="btn btn-secondary">mup 1</button>
</div>
</div>
</div>
<div class="row pane" id="pane-pointer">
<div class="form-group col-12">
<input type="text" class="form-control live-text" placeholder="Live text" name="text">
</div/>
<div id="scrollbar"></div>
<div id="pointer"></div>
<div id="pointer-buttons">
<button type="button" data-msg='{"type":"pointer","click":"left"}' class="btn btn-primary no-radius col-5">&nbsp;</button><button type="button no-margin" data-msg='{"type":"pointer","click":"middle"}' class="btn btn-secondary no-radius col-2">&nbsp;</button><button type="button no-margin" data-msg='{"type":"pointer","click":"right"}' class="btn btn-primary no-radius col-5">&nbsp;</button>
</div>
</div>
<div class="row pane" id="pane-media">
<div class="col-12">
<p class="legend">Spotify</p>
</div>
<div class="col-12">
<button type="button" data-msg='{"type":"media","value":"playpause"}' class="btn btn-secondary">Play/Pause</button>
<button type="button" data-msg='{"type":"media","value":"next"}' class="btn btn-secondary">Next</button>
<button type="button" data-msg='{"type":"media","value":"prev"}' class="btn btn-secondary">Previous</button>
</div>
<div class="col-12">
<p class="legend">Volume</p>
</div>
<div class="col-12">
<button type="button" data-msg='{"type":"volume","value":"0"}' class="btn btn-secondary">0%</button>
<button type="button" data-msg='{"type":"volume","value":"25"}' class="btn btn-secondary">25%</button>
<button type="button" data-msg='{"type":"volume","value":"50"}' class="btn btn-secondary">50%</button>
<button type="button" data-msg='{"type":"volume","value":"75"}' class="btn btn-secondary">75%</button>
<button type="button" data-msg='{"type":"volume","value":"100"}' class="btn btn-secondary">100%</button>
</div>
<div class="line col-12"></div>
<div class="col-12">
<button type="button" data-msg='{"type":"volume","value":"down"}' class="btn btn-secondary">-</button>
<button type="button" data-msg='{"type":"volume","value":"up"}' class="btn btn-secondary">+</button>
</div>
</div>
<div class="row pane" id="pane-desktop">
<div class="col-12">
<p class="legend">Desktop</p>