├── LICENSE.md
├── Makefile
├── README.md
├── Release
├── com.github.sbelectronics.streamdeck.binclock.streamDeckPlugin
├── com.github.sbelectronics.streamdeck.demo.streamDeckPlugin
└── com.github.sbelectronics.streamdeck.http.streamDeckPlugin
├── cmd
├── binclock
│ └── binclock.go
├── demo
│ └── demo.go
├── http
│ └── http.go
└── profile_switcher
│ └── profile_switcher.go
├── com.github.sbelectronics.streamdeck.binclock.sdPlugin
├── binclock.exe
├── defaultImage.png
├── icon.png
├── index_pi.html
├── js
│ └── common.js
├── manifest.json
├── pluginIcon.png
└── sdpi.css
├── com.github.sbelectronics.streamdeck.demo.sdPlugin
├── defaultImage.png
├── demo.exe
├── icon.png
├── manifest.json
└── pluginIcon.png
├── com.github.sbelectronics.streamdeck.http.sdPlugin
├── defaultImage.png
├── http.exe
├── icon.png
├── index_pi.html
├── js
│ └── common.js
├── manifest.json
├── pluginIcon.png
└── sdpi.css
├── com.github.sbelectronics.streamdeck.profileswitcher.sdPlugin
├── defaultImage.png
├── icon.png
├── manifest.json
├── patterns.json
├── pluginIcon.png
├── profile_switcher.exe
├── profileswitcher1.streamDeckProfile
└── profileswitcher2.streamDeckProfile
├── go.mod
├── go.sum
└── pkg
├── binclock
├── binclock.go
└── binclock_test.go
├── globaloptions
└── globaloptions.go
├── platform
└── windows
│ └── platform_windows.go
├── streamdeck
├── messages.go
├── notifications.go
├── streamdeck.go
└── streamdeck_test.go
└── util
└── util.go
/LICENSE.md:
--------------------------------------------------------------------------------
1 | Copyright 2019-Present Scott M Baker
2 |
3 | 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:
4 |
5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
6 |
7 | 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.
8 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | PROFILE_SWITCHER_DIR=com.github.sbelectronics.streamdeck.profileswitcher.sdPlugin
2 | PROFILE_SWITCHER_EXE=$(PROFILE_SWITCHER_DIR)\profile_switcher.exe
3 |
4 | DEMO_DIR=com.github.sbelectronics.streamdeck.demo.sdPlugin
5 | DEMO_EXE=$(DEMO_DIR)\demo.exe
6 |
7 | BINCLOCK_DIR=com.github.sbelectronics.streamdeck.binclock.sdPlugin
8 | BINCLOCK_EXE=$(BINCLOCK_DIR)\binclock.exe
9 |
10 | HTTP_DIR=com.github.sbelectronics.streamdeck.http.sdPlugin
11 | HTTP_EXE=$(HTTP_DIR)\http.exe
12 |
13 | PKG_FILES=$(wildcard pkg/**/*.go)
14 |
15 | # redefine this!
16 | APPDATA=C:\Users\smbaker\AppData\Roaming
17 |
18 |
19 | all: $(PROFILE_SWITCHER_EXE) $(DEMO_EXE) $(BINCLOCK_EXE) $(HTTP_EXE)
20 |
21 | $(PROFILE_SWITCHER_EXE): cmd/profile_switcher/*.go $(PKG_FILES)
22 | go build -o $(PROFILE_SWITCHER_EXE) cmd/profile_switcher/profile_switcher.go
23 |
24 | $(DEMO_EXE): cmd/demo/*.go $(PKG_FILES)
25 | go build -o $(DEMO_EXE) cmd/demo/demo.go
26 |
27 | $(BINCLOCK_EXE): cmd/binclock/*.go $(PKG_FILES)
28 | go build -o $(BINCLOCK_EXE) cmd/binclock/binclock.go
29 |
30 | $(HTTP_EXE): cmd/http/*.go $(PKG_FILES)
31 | go build -o $(HTTP_EXE) cmd/http/http.go
32 |
33 | install:
34 | xcopy /h /k /e /c /y /i $(BINCLOCK_DIR) $(APPDATA)\Elgato\StreamDeck\Plugins\$(BINCLOCK_DIR)
35 | xcopy /h /k /e /c /y /i $(DEMO_DIR) $(APPDATA)\Elgato\StreamDeck\Plugins\$(DEMO_DIR)
36 | xcopy /h /k /e /c /y /i $(PROFILE_SWITCHER_DIR) $(APPDATA)\Elgato\StreamDeck\Plugins\$(PROFILE_SWITCHER_DIR)
37 | xcopy /h /k /e /c /y /i $(HTTP_DIR) $(APPDATA)\Elgato\StreamDeck\Plugins\$(HTTP_DIR)
38 |
39 | distrib:
40 | rm Release/*.streamdeckPlugin
41 | DistributionTool -b -i $(BINCLOCK_DIR) -o Release || echo
42 | DistributionTool -b -i $(DEMO_DIR) -o Release || echo
43 | DistributionTool -b -i $(HTTP_DIR) -o Release || echo
44 |
45 | supersize:
46 | convert $(BINCLOCK_DIR)/pluginIcon.png -resize 144x144 $(BINCLOCK_DIR)/pluginIcon@2x.png
47 | convert $(BINCLOCK_DIR)/icon.png -resize 40x40 $(BINCLOCK_DIR)/icon@2x.png
48 | convert $(BINCLOCK_DIR)/defaultImage.png -resize 144x144 $(BINCLOCK_DIR)/defaultImage@2x.png
49 | convert $(DEMO_DIR)/pluginIcon.png -resize 144x144 $(DEMO_DIR)/pluginIcon@2x.png
50 | convert $(DEMO_DIR)/icon.png -resize 40x40 $(DEMO_DIR)/icon@2x.png
51 | convert $(DEMO_DIR)/defaultImage.png -resize 144x144 $(DEMO_DIR)/defaultImage@2x.png
52 | convert $(HTTP_DIR)/pluginIcon.png -resize 144x144 $(HTTP_DIR)/pluginIcon@2x.png
53 | convert $(HTTP_DIR)/icon.png -resize 40x40 $(HTTP_DIR)/icon@2x.png
54 | convert $(HTTP_DIR)/defaultImage.png -resize 144x144 $(HTTP_DIR)/defaultImage@2x.png
55 |
56 | clean:
57 | rm $(PROFILE_SWITCHER_EXE) $(DEMO_EXE) $(BINCLOCK_EXE) $(HTTP_EXE)
58 |
59 | test:
60 | go test ./...
61 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## smbaker's Streamdeck Plugins ##
2 | Copyright Scott M Baker
3 | http://www.smbaker.com/
4 |
5 | | Note: All of these plugins only work with Windows. I'm a Windows (and Linux) user, not a Mac user. Sorry, Mac peoples.
6 |
7 | This is a collection of plugins that I wrote for my Elgato Stream Deck. They are as-is without any warranty.
8 |
9 | * binclock - binary (actually more BCD than binary, but that is the trend) clock
10 | * http - performs HTTP GET, POST, PUT, PATCH, or DELETE operations to a given URL
11 | * demo - a playground for testing the golang-based plugin framework
12 | * profile_switcher - switch profiles based on active window regex match
13 |
14 | Once installed, all of the plugins will show up in the "Custom" section of the Streamdeck software.
15 |
16 | ## binclock
17 |
18 | A Binary Clock is a clock that displays the time in ... binary. This plugin will display a tiny binary clock inside
19 | a button on your Streamdeck. Why would you want to do this? It's just a gadget, or a conversation piece. Use of the
20 | plugin is relatively straightforward, just drag the button onto your button bar and it will start displaying the clock.
21 |
22 | Windows Download: https://github.com/sbelectronics/streamdeck/blob/master/Release/com.github.sbelectronics.streamdeck.binclock.streamDeckPlugin?raw=true
23 |
24 | ## demo
25 |
26 | This one displays a rotating box with a counter. Push the button and the color of the box will change.
27 |
28 | ## http
29 |
30 | This plugin will send a HTTP request to a URL when the button is pushed and released. The can be useful for integrating with home automation software, websites, or any service that can be triggered from an HTTP request.
31 |
32 | I use mine for integration with Autohotkey, which has a Socket library that allows it to respond to HTTP requests instead of keypresses.
33 |
34 | Windows Download: https://github.com/sbelectronics/streamdeck/blob/master/Release/com.github.sbelectronics.streamdeck.http.streamDeckPlugin?raw=true
35 |
36 | ## profile_switcher
37 |
38 | This is a regex-based profile switcher. I did this because I wanted different profiles for different websites. The websites run as tabs in my Chrome browser. So, whenever you open a tab, Stream Deck doesn't know anything other than that you're running an Application called "Google Chrome". I made this plugin to watch for the window title of the active window, and match based on the window title. When you switch tabs in chrome, the title changes.
39 |
40 | | Example: The reason I wrote this was that I wanted a custom profile when visiting Tinkercad.com in Chrome, but a different profile elsewhere.
41 |
42 | This one is hellishly complicated. It can't be released and installed, because you'll actually need to modify some of the included files yourself. You don't have to recompile it, but you do have to edit a text file and overwrite some profiles. The reason for this complexity is that the Stream Deck API only supports switching profiles to a read-only profile that is part of the plugin. You can't switch to a user-created profile, nor can you switch to a profile that was created by another plugin. So you have to make sure _this plugin_ contains your profiles.
43 |
44 | 1. Export the profiles you want to use from the Stream Deck software, save them under the names `profileswitcher1.streamDeckProfile`, `profileSwitcher2.streamDeckProfile`, etc.
45 | 2. Download this project from github if you have not already
46 | 3. Copy the contents of the directory `com.github.sbelectronics.streamdeck.profileswitcher.sdPlugin` to `%appdata%\Elgato\StreamDeck\Plugins\com.github.sbelectronics.streamdeck.profileswitcher.sdPlugin`. This installs the plugin in your StreamDeck software.
47 | 4. Go inside the directory `%appdata%\Elgato\StreamDeck\Plugins\com.github.sbelectronics.streamdeck.profileswitcher.sdPlugin`
48 | 4. Modify patterns.json to specify your regular expressions. You can add as patterns/profiles as you like.
49 | 5. Copy the profiles you exported in step #1 into the `%appdata%\Elgato\StreamDeck\Plugins\com.github.sbelectronics.streamdeck.profileswitcher.sdPlugin` directory, overwriting the profiles that are there. Now we have your custom patterns.json here and your custom profiles here.
50 | 6. Restart the Stream Deck software (if it's already running)
51 |
52 | When you open a window that matches the Regex defined in `patterns.json`, Stream Deck will ask you to import the profile. Say yes, and then using the Stream Deck software, switch back to your default profile. After this, the profiles will change with window activations as expected.
53 |
54 | | Note The profiles it imports are read-only, so you can't modify them after they've been updated, but you can always export another profile (create a new one as necessary), and replace the files in the `%appdata%\Elgato\StreamDeck\Plugins\com.github.sbelectronics.streamdeck.profileswitcher.sdPlugin` directory. Then, `delete` the readonly profile in Stream Deck, restart Stream Deck, and next time you visit a window that matches the regex, it'll ask you to import the profile again.
55 |
56 | As I said, complicated, but not quite as complicated as it sounds once you get the hang of it.
57 |
58 | | Note: ElGato developers if you ever happen across this README, please add the ability to call `switchToProfile` on profiles that don't belong to the current plugin. It would have made life way simpler.
59 |
60 | ## Building
61 |
62 | Although I'm a Linux developer by trade, this software was written and developed in a Windows environment. I installed the following three toolsets:
63 |
64 | * git: https://git-scm.com/download/win, version 2.24.0.2
65 | * go: https://golang.org/dl/, version 1.12.14, 32-bit
66 | * gcc and related: http://win-builds.org/doku.php/download_and_installation_from_windows, version 1.5.0, 32-bit (i686)
67 |
68 | Setup your PATH variable accordingly to point to all these tools, and then running `make` should build the makefile. Expect `go` to have to spend some time downloading dependencies via `git`. `make install` (with some customization of the Makefile for your %appdata% directory) can be used to copy the plugins to the Streamdeck software plugin directory.
69 |
70 | | Note: All of these plugins include compiled binaries in this repository, so you have no need to build them yourself unless you wish to modify them.
71 |
72 | | Note 2: The `binclock` and `demo` plugins even have Streamdeck installers in the `Release` directory, so all you have to do is run those `.streamDeckPlugin` files and the Streamdeck software should take it from there.
73 |
--------------------------------------------------------------------------------
/Release/com.github.sbelectronics.streamdeck.binclock.streamDeckPlugin:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sbelectronics/streamdeck/f0175932e1283d59d50e7410ead230b293fa39cc/Release/com.github.sbelectronics.streamdeck.binclock.streamDeckPlugin
--------------------------------------------------------------------------------
/Release/com.github.sbelectronics.streamdeck.demo.streamDeckPlugin:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sbelectronics/streamdeck/f0175932e1283d59d50e7410ead230b293fa39cc/Release/com.github.sbelectronics.streamdeck.demo.streamDeckPlugin
--------------------------------------------------------------------------------
/Release/com.github.sbelectronics.streamdeck.http.streamDeckPlugin:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sbelectronics/streamdeck/f0175932e1283d59d50e7410ead230b293fa39cc/Release/com.github.sbelectronics.streamdeck.http.streamDeckPlugin
--------------------------------------------------------------------------------
/cmd/binclock/binclock.go:
--------------------------------------------------------------------------------
1 | /* binclock.go
2 | (c) Scott M Baker, http://www.smbaker.com/
3 | */
4 |
5 | package main
6 |
7 | import (
8 | "bytes"
9 | "github.com/sbelectronics/streamdeck/pkg/binclock"
10 | "github.com/sbelectronics/streamdeck/pkg/globaloptions"
11 | "github.com/sbelectronics/streamdeck/pkg/streamdeck"
12 | "github.com/sbelectronics/streamdeck/pkg/util"
13 | "image/png"
14 | "log"
15 | "os"
16 | "time"
17 | )
18 |
19 | type MyButtonHandler struct {
20 | color int
21 | }
22 |
23 | // On every keyup, change the color
24 | func (mbh *MyButtonHandler) OnKeyUp(*streamdeck.Button) {}
25 | func (mbh *MyButtonHandler) OnKeyDown(*streamdeck.Button) {}
26 | func (mbh *MyButtonHandler) GetDefaultSettings(button *streamdeck.Button) {
27 | button.Settings["colorlit"] = binclock.DEFAULT_LIT_COLOR
28 | button.Settings["colorunlit"] = binclock.DEFAULT_UNLIT_COLOR
29 | button.Settings["colorback"] = binclock.DEFAULT_BACK_COLOR
30 | }
31 |
32 | func main() {
33 | // Create the file c:\junk\binclock_plugin.log and we will append
34 | // log messages to it.
35 | if _, err := os.Stat("c:\\junk\\binclock_plugin.log"); err == nil {
36 | logf, err := os.OpenFile("c:\\junk\\binclock_plugin.log", os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666)
37 | if err != nil {
38 | log.Fatalf("error opening file: %v", err)
39 | }
40 | defer logf.Close()
41 | log.SetOutput(logf)
42 | globaloptions.ForceVerbose = true
43 | }
44 |
45 | globaloptions.ParseCmdline()
46 |
47 | sd := streamdeck.StreamDeck{}
48 | err := sd.Init(globaloptions.Port,
49 | globaloptions.PluginUUID,
50 | globaloptions.RegisterEvent,
51 | globaloptions.Info,
52 | globaloptions.Verbose,
53 | &MyButtonHandler{})
54 | if err != nil {
55 | log.Fatalf("Error initializing streamdeck plugin %v", err)
56 | }
57 |
58 | err = sd.Start()
59 | if err != nil {
60 | log.Fatalf("Error starting streamdeck plugin %v", err)
61 | }
62 |
63 | bc := &binclock.BinClock{LitDotColor: binclock.DEFAULT_LIT_COLOR,
64 | UnlitDotColor: binclock.DEFAULT_UNLIT_COLOR,
65 | BackColor: binclock.DEFAULT_BACK_COLOR}
66 | bc.Create()
67 |
68 | lastSecond := -1
69 | for {
70 | // only update if the second has changed
71 | tNow := time.Now()
72 | if tNow.Second() == lastSecond {
73 | time.Sleep(100 * time.Millisecond)
74 | continue
75 | }
76 | lastSecond = tNow.Second()
77 |
78 | for context, button := range sd.Buttons {
79 | // override the colors with what might have come from the property inspector
80 | bc.LitDotColor = util.StringMapGetDefault(button.Settings, "colorlit", binclock.DEFAULT_LIT_COLOR)
81 | bc.UnlitDotColor = util.StringMapGetDefault(button.Settings, "colorunlit", binclock.DEFAULT_UNLIT_COLOR)
82 | bc.BackColor = util.StringMapGetDefault(button.Settings, "colorback", binclock.DEFAULT_BACK_COLOR)
83 |
84 | bc.DrawTime(tNow)
85 |
86 | // Encode the image into a png and set it on the StreamDeck
87 | buf := new(bytes.Buffer)
88 | err := png.Encode(buf, bc.Image())
89 | if err != nil {
90 | log.Printf("Error encoding png %v", err)
91 | }
92 | err = sd.SetImage(context, buf.Bytes(), streamdeck.TYPE_PNG, streamdeck.TARGET_BOTH)
93 | if err != nil {
94 | log.Printf("Error setimage: %v", err)
95 | }
96 | }
97 |
98 | time.Sleep(100 * time.Millisecond)
99 | }
100 | }
101 |
--------------------------------------------------------------------------------
/cmd/demo/demo.go:
--------------------------------------------------------------------------------
1 | /* demo.go
2 | (c) Scott M Baker, http://www.smbaker.com/
3 |
4 | NOTE: Make sure to leave the title blank when adding the button
5 |
6 | This is a simple demo that will show a rotating square with a counter
7 | that increments in the middle of it. Pressing the button will change
8 | the color red -> Green -> Blue, and back to red again. If the counter
9 | doesn't count, then you didn't heed the above note about making sure
10 | the title is blank.
11 | */
12 |
13 | package main
14 |
15 | import (
16 | "bytes"
17 | "fmt"
18 | "github.com/BurntSushi/graphics-go/graphics"
19 | "github.com/sbelectronics/streamdeck/pkg/globaloptions"
20 | "github.com/sbelectronics/streamdeck/pkg/streamdeck"
21 | "image"
22 | "image/color"
23 | "image/draw"
24 | "image/png"
25 | "log"
26 | "math"
27 | "os"
28 | "time"
29 | )
30 |
31 | var (
32 | color_presets []color.RGBA = []color.RGBA{color.RGBA{200, 0, 0, 255},
33 | color.RGBA{0, 200, 0, 255},
34 | color.RGBA{0, 0, 200, 255}}
35 | )
36 |
37 | type MyButtonHandler struct {
38 | color int
39 | }
40 |
41 | // On every keyup, change the color
42 | func (mbh *MyButtonHandler) OnKeyUp(*streamdeck.Button) {
43 | mbh.color++
44 | if mbh.color >= 3 {
45 | mbh.color = 0
46 | }
47 |
48 | }
49 | func (mbh *MyButtonHandler) OnKeyDown(*streamdeck.Button) {}
50 | func (mbh *MyButtonHandler) GetDefaultSettings(*streamdeck.Button) {}
51 |
52 | func main() {
53 | // Create the file c:\junk\demo_plugin.log and we will append
54 | // log messages to it.
55 | if _, err := os.Stat("c:\\junk\\demo_plugin.log"); err == nil {
56 | logf, err := os.OpenFile("c:\\junk\\demo_plugin.log", os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666)
57 | if err != nil {
58 | log.Fatalf("error opening file: %v", err)
59 | }
60 | defer logf.Close()
61 | log.SetOutput(logf)
62 | globaloptions.ForceVerbose = true
63 | }
64 |
65 | globaloptions.ParseCmdline()
66 |
67 | sd := streamdeck.StreamDeck{}
68 | err := sd.Init(globaloptions.Port,
69 | globaloptions.PluginUUID,
70 | globaloptions.RegisterEvent,
71 | globaloptions.Info,
72 | globaloptions.Verbose,
73 | &MyButtonHandler{})
74 | if err != nil {
75 | log.Fatalf("Error initializing streamdeck plugin %v", err)
76 | }
77 |
78 | err = sd.Start()
79 | if err != nil {
80 | log.Fatalf("Error starting streamdeck plugin %v", err)
81 | }
82 |
83 | counter := 0
84 | angle := math.Pi * 0.01
85 | for {
86 | for context, button := range sd.Buttons {
87 | // Draw a colored box and rotate it
88 | thisColor := color_presets[button.Handler.(*MyButtonHandler).color]
89 | img := image.NewRGBA(image.Rect(0, 0, 72, 72))
90 | draw.Draw(img, image.Rect(12, 12, 60, 60), &image.Uniform{thisColor}, image.ZP, draw.Src)
91 | srcDim := img.Bounds()
92 | rotatedImg := image.NewRGBA(image.Rect(0, 0, srcDim.Dy(), srcDim.Dx()))
93 | graphics.Rotate(rotatedImg, img, &graphics.RotateOptions{angle})
94 | angle += math.Pi * 0.01
95 |
96 | // Encode the image into a png and set it on the StreamDeck
97 | buf := new(bytes.Buffer)
98 | err := png.Encode(buf, rotatedImg)
99 | if err != nil {
100 | log.Printf("Error encoding png %v", err)
101 | }
102 | err = sd.SetImage(context, buf.Bytes(), streamdeck.TYPE_PNG, streamdeck.TARGET_BOTH)
103 | if err != nil {
104 | log.Printf("Error setimage: %v", err)
105 | }
106 |
107 | // Set the title to the number
108 | err = sd.SetTitle(context, fmt.Sprintf("%d", counter%1000), streamdeck.TARGET_BOTH)
109 | if err != nil {
110 | log.Printf("Error settitle: %v", err)
111 | }
112 | }
113 | counter += 1
114 |
115 | time.Sleep(100 * time.Millisecond)
116 | }
117 | }
118 |
--------------------------------------------------------------------------------
/cmd/http/http.go:
--------------------------------------------------------------------------------
1 | /* http.go
2 | (c) Scott M Baker, http://www.smbaker.com/
3 |
4 | This is a streamdeck plugin that will perform an HTTP operation
5 | against a given URL. You can specify the type of HTTP operation
6 | to perform (GET, POST, PUT, PATCH, DELETE) and for operations
7 | that send data (POST, PUT, etc) you can specify what data to
8 | send.
9 | */
10 |
11 | package main
12 |
13 | import (
14 | "github.com/sbelectronics/streamdeck/pkg/globaloptions"
15 | "github.com/sbelectronics/streamdeck/pkg/streamdeck"
16 | "github.com/sbelectronics/streamdeck/pkg/util"
17 | "log"
18 | "net/http"
19 | "os"
20 | "strings"
21 | "time"
22 | )
23 |
24 | type MyButtonHandler struct {
25 | }
26 |
27 | // On every keyup, change the color
28 | func (mbh *MyButtonHandler) OnKeyUp(button *streamdeck.Button) {
29 | url, ok := button.Settings["url"]
30 | if !ok || (url == "") {
31 | button.StreamDeck.Errorf("Error - no url defined for button")
32 | return
33 | }
34 |
35 | url, err := util.SanitizeUrl(url)
36 | if err != nil {
37 | button.StreamDeck.Errorf("Error sanitizing url: %v", err)
38 | }
39 | button.StreamDeck.Debugf("%s", url)
40 |
41 | var resp *http.Response
42 | var req *http.Request
43 |
44 | operation := util.StringMapGetDefault(button.Settings, "operation", "GET")
45 | data := util.StringMapGetDefault(button.Settings, "data", "")
46 | mimetype := util.StringMapGetDefault(button.Settings, "mimetype", "text/plain")
47 |
48 | switch operation {
49 | case "POST":
50 | resp, err = http.Post(url, "text/plain", strings.NewReader(data))
51 | case "PUT":
52 | req, err = http.NewRequest("PUT", url, strings.NewReader(data))
53 | if err == nil {
54 | req.Header.Set("Content-Type", mimetype)
55 | resp, err = http.DefaultClient.Do(req)
56 | }
57 | case "PATCH":
58 | req, err = http.NewRequest("PATCH", url, strings.NewReader(data))
59 | if err == nil {
60 | req.Header.Set("Content-Type", mimetype)
61 | resp, err = http.DefaultClient.Do(req)
62 | }
63 | case "DELETE":
64 | req, err = http.NewRequest("DELETE", url, strings.NewReader(data))
65 | if err == nil {
66 | req.Header.Set("Content-Type", mimetype)
67 | resp, err = http.DefaultClient.Do(req)
68 | }
69 | default:
70 | resp, err = http.Get(url)
71 | }
72 |
73 | if err != nil {
74 | button.StreamDeck.Errorf("Error while performing operation %s on url %s: %v", operation, url, err)
75 | } else {
76 | button.StreamDeck.Debugf("Http response %v", resp)
77 | }
78 |
79 | // suppress error
80 | _ = resp
81 | }
82 | func (mbh *MyButtonHandler) OnKeyDown(*streamdeck.Button) {}
83 | func (mbh *MyButtonHandler) GetDefaultSettings(*streamdeck.Button) {}
84 |
85 | func main() {
86 | // Create the file c:\junk\demo_plugin.log and we will append
87 | // log messages to it.
88 | if _, err := os.Stat("c:\\junk\\http_plugin.log"); err == nil {
89 | logf, err := os.OpenFile("c:\\junk\\http_plugin.log", os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666)
90 | if err != nil {
91 | log.Fatalf("error opening file: %v", err)
92 | }
93 | defer logf.Close()
94 | log.SetOutput(logf)
95 | globaloptions.ForceVerbose = true
96 | }
97 |
98 | globaloptions.ParseCmdline()
99 |
100 | sd := streamdeck.StreamDeck{}
101 | err := sd.Init(globaloptions.Port,
102 | globaloptions.PluginUUID,
103 | globaloptions.RegisterEvent,
104 | globaloptions.Info,
105 | globaloptions.Verbose,
106 | &MyButtonHandler{})
107 | if err != nil {
108 | log.Fatalf("Error initializing streamdeck plugin %v", err)
109 | }
110 |
111 | err = sd.Start()
112 | if err != nil {
113 | log.Fatalf("Error starting streamdeck plugin %v", err)
114 | }
115 |
116 | for {
117 | time.Sleep(1000 * time.Millisecond)
118 | }
119 | }
120 |
--------------------------------------------------------------------------------
/cmd/profile_switcher/profile_switcher.go:
--------------------------------------------------------------------------------
1 | /* profile_switcher.go
2 | (c) Scott M Baker, http://www.smbaker.com/
3 | */
4 |
5 | package main
6 |
7 | import (
8 | "encoding/json"
9 | "fmt"
10 | "github.com/sbelectronics/streamdeck/pkg/globaloptions"
11 | "github.com/sbelectronics/streamdeck/pkg/platform/windows"
12 | "github.com/sbelectronics/streamdeck/pkg/streamdeck"
13 | "io/ioutil"
14 | "log"
15 | "os"
16 | "regexp"
17 | "time"
18 | )
19 |
20 | type Pattern struct {
21 | Regex string
22 | Profile string
23 | }
24 |
25 | func loadPatterns() ([]Pattern, error) {
26 | var patterns []Pattern
27 |
28 | jsonFile, err := os.Open("patterns.json")
29 | if err != nil {
30 | return nil, fmt.Errorf("Failed to open patterns.json: %v", err)
31 | }
32 | jsonData, err := ioutil.ReadAll(jsonFile)
33 | if err != nil {
34 | return nil, fmt.Errorf("Failed to open patterns.json: %v", err)
35 | }
36 | err = json.Unmarshal(jsonData, &patterns)
37 | if err != nil {
38 | return nil, fmt.Errorf("Failed to unmarshal patterns.json: %v", err)
39 | }
40 |
41 | return patterns, nil
42 | }
43 |
44 | func main() {
45 | // Create the file c:\junk\profile_switcher_plugin.log and we will append
46 | // log messages to it.
47 | if _, err := os.Stat("c:\\junk\\profile_switcher_plugin.log"); err == nil {
48 | logf, err := os.OpenFile("c:\\junk\\profile_switcher_plugin.log", os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666)
49 | if err != nil {
50 | log.Fatalf("error opening file: %v", err)
51 | }
52 | defer logf.Close()
53 | log.SetOutput(logf)
54 | globaloptions.ForceVerbose = true
55 | }
56 |
57 | globaloptions.ParseCmdline()
58 |
59 | patterns, err := loadPatterns()
60 | if err != nil {
61 | log.Fatalf("Error loading patterns: %v", err)
62 | }
63 |
64 | sd := streamdeck.StreamDeck{}
65 | err = sd.Init(globaloptions.Port,
66 | globaloptions.PluginUUID,
67 | globaloptions.RegisterEvent,
68 | globaloptions.Info,
69 | globaloptions.Verbose,
70 | nil)
71 | if err != nil {
72 | log.Fatalf("Error initializing streamdeck plugin %v", err)
73 | }
74 |
75 | err = sd.Start()
76 | if err != nil {
77 | log.Fatalf("Error starting streamdeck plugin %v", err)
78 | }
79 |
80 | lastTitle := ""
81 | for {
82 | title, err := windows.GetTopmostWindowTitle()
83 | if err != nil {
84 | log.Printf("Error %v\n", err)
85 | time.Sleep(1 * time.Second)
86 | continue
87 | }
88 |
89 | if lastTitle != title {
90 | if globaloptions.Verbose {
91 | log.Printf("Title: %s", title)
92 | }
93 |
94 | lastTitle = title
95 |
96 | matchedSomething := false
97 | for _, p := range patterns {
98 | if matched, _ := regexp.MatchString(p.Regex, title); matched {
99 | if globaloptions.Verbose {
100 | log.Printf("Matched %v", p)
101 | }
102 | sd.SwitchProfileIfChanged(p.Profile)
103 | matchedSomething = true
104 | }
105 | }
106 | if !matchedSomething {
107 | sd.SwitchProfileIfChanged("") // this will switch to the default
108 | }
109 | }
110 |
111 | time.Sleep(100 * time.Millisecond)
112 | }
113 | }
114 |
--------------------------------------------------------------------------------
/com.github.sbelectronics.streamdeck.binclock.sdPlugin/binclock.exe:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sbelectronics/streamdeck/f0175932e1283d59d50e7410ead230b293fa39cc/com.github.sbelectronics.streamdeck.binclock.sdPlugin/binclock.exe
--------------------------------------------------------------------------------
/com.github.sbelectronics.streamdeck.binclock.sdPlugin/defaultImage.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sbelectronics/streamdeck/f0175932e1283d59d50e7410ead230b293fa39cc/com.github.sbelectronics.streamdeck.binclock.sdPlugin/defaultImage.png
--------------------------------------------------------------------------------
/com.github.sbelectronics.streamdeck.binclock.sdPlugin/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sbelectronics/streamdeck/f0175932e1283d59d50e7410ead230b293fa39cc/com.github.sbelectronics.streamdeck.binclock.sdPlugin/icon.png
--------------------------------------------------------------------------------
/com.github.sbelectronics.streamdeck.binclock.sdPlugin/index_pi.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Binary Clock Property Inspector
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
Colors
14 |
15 |
16 |
Lit LED Color
17 |
18 |
19 |
20 |
Unlit LED Color
21 |
22 |
23 |
24 |
Background Color
25 |
26 |
27 |
28 | #c80000
29 | #ff0000
30 | #00c800
31 | #00ff00
32 | #0000c8
33 | #0000ff
34 | #505050
35 | #808080
36 | #000000
37 |
38 |
39 |
40 |
96 |
97 |
98 |
--------------------------------------------------------------------------------
/com.github.sbelectronics.streamdeck.binclock.sdPlugin/js/common.js:
--------------------------------------------------------------------------------
1 | /* global $SD, $localizedStrings */
2 | /* exported, $localizedStrings */
3 | /* eslint no-undef: "error",
4 | curly: 0,
5 | no-caller: 0,
6 | wrap-iife: 0,
7 | one-var: 0,
8 | no-var: 0,
9 | vars-on-top: 0
10 | */
11 |
12 | // don't change this to let or const, because we rely on var's hoisting
13 | // eslint-disable-next-line no-use-before-define, no-var
14 | var $localizedStrings = $localizedStrings || {},
15 | REMOTESETTINGS = REMOTESETTINGS || {},
16 | DestinationEnum = Object.freeze({
17 | HARDWARE_AND_SOFTWARE: 0,
18 | HARDWARE_ONLY: 1,
19 | SOFTWARE_ONLY: 2
20 | }),
21 | // eslint-disable-next-line no-unused-vars
22 | isQT = navigator.appVersion.includes('QtWebEngine'),
23 | debug = debug || false,
24 | debugLog = function () {},
25 | MIMAGECACHE = MIMAGECACHE || {};
26 |
27 | const setDebugOutput = (debug) => (debug === true) ? console.log.bind(window.console) : function () {};
28 | debugLog = setDebugOutput(debug);
29 |
30 | // Create a wrapper to allow passing JSON to the socket
31 | WebSocket.prototype.sendJSON = function (jsn, log) {
32 | if (log) {
33 | console.log('SendJSON', this, jsn);
34 | }
35 | // if (this.readyState) {
36 | this.send(JSON.stringify(jsn));
37 | // }
38 | };
39 |
40 | /* eslint no-extend-native: ["error", { "exceptions": ["String"] }] */
41 | String.prototype.lox = function () {
42 | var a = String(this);
43 | try {
44 | a = $localizedStrings[a] || a;
45 | } catch (b) {}
46 | return a;
47 | };
48 |
49 | String.prototype.sprintf = function (inArr) {
50 | let i = 0;
51 | const args = (inArr && Array.isArray(inArr)) ? inArr : arguments;
52 | return this.replace(/%s/g, function () {
53 | return args[i++];
54 | });
55 | };
56 |
57 | // eslint-disable-next-line no-unused-vars
58 | const sprintf = (s, ...args) => {
59 | let i = 0;
60 | return s.replace(/%s/g, function () {
61 | return args[i++];
62 | });
63 | };
64 |
65 | const loadLocalization = (lang, pathPrefix, cb) => {
66 | Utils.readJson(`${pathPrefix}${lang}.json`, function (jsn) {
67 | const manifest = Utils.parseJson(jsn);
68 | $localizedStrings = manifest && manifest.hasOwnProperty('Localization') ? manifest['Localization'] : {};
69 | debugLog($localizedStrings);
70 | if (cb && typeof cb === 'function') cb();
71 | });
72 | }
73 |
74 | var Utils = {
75 | sleep: function (milliseconds) {
76 | return new Promise(resolve => setTimeout(resolve, milliseconds));
77 | },
78 | isUndefined: function (value) {
79 | return typeof value === 'undefined';
80 | },
81 | isObject: function (o) {
82 | return (
83 | typeof o === 'object' &&
84 | o !== null &&
85 | o.constructor &&
86 | o.constructor === Object
87 | );
88 | },
89 | isPlainObject: function (o) {
90 | return (
91 | typeof o === 'object' &&
92 | o !== null &&
93 | o.constructor &&
94 | o.constructor === Object
95 | );
96 | },
97 | isArray: function (value) {
98 | return Array.isArray(value);
99 | },
100 | isNumber: function (value) {
101 | return typeof value === 'number' && value !== null;
102 | },
103 | isInteger (value) {
104 | return typeof value === 'number' && value === Number(value);
105 | },
106 | isString (value) {
107 | return typeof value === 'string';
108 | },
109 | isImage (value) {
110 | return value instanceof HTMLImageElement;
111 | },
112 | isCanvas (value) {
113 | return value instanceof HTMLCanvasElement;
114 | },
115 | isValue: function (value) {
116 | return !this.isObject(value) && !this.isArray(value);
117 | },
118 | isNull: function (value) {
119 | return value === null;
120 | },
121 | toInteger: function (value) {
122 | const INFINITY = 1 / 0,
123 | MAX_INTEGER = 1.7976931348623157e308;
124 | if (!value) {
125 | return value === 0 ? value : 0;
126 | }
127 | value = Number(value);
128 | if (value === INFINITY || value === -INFINITY) {
129 | const sign = value < 0 ? -1 : 1;
130 | return sign * MAX_INTEGER;
131 | }
132 | return value === value ? value : 0;
133 | }
134 | };
135 | Utils.minmax = function (v, min = 0, max = 100) {
136 | return Math.min(max, Math.max(min, v));
137 | };
138 |
139 | Utils.rangeToPercent = function (value, min, max) {
140 | return ((value - min) / (max - min));
141 | };
142 |
143 | Utils.percentToRange = function (percent, min, max) {
144 | return ((max - min) * percent + min);
145 | };
146 |
147 | Utils.setDebugOutput = (debug) => {
148 | return (debug === true) ? console.log.bind(window.console) : function () {};
149 | };
150 |
151 | Utils.randomComponentName = function (len = 6) {
152 | return `${Utils.randomLowerString(len)}-${Utils.randomLowerString(len)}`;
153 | };
154 |
155 | Utils.randomString = function (len = 8) {
156 | return Array.apply(0, Array(len))
157 | .map(function () {
158 | return (function (charset) {
159 | return charset.charAt(
160 | Math.floor(Math.random() * charset.length)
161 | );
162 | })(
163 | 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
164 | );
165 | })
166 | .join('');
167 | };
168 |
169 | Utils.rs = function (len = 8) {
170 | return [...Array(len)].map(i => (~~(Math.random() * 36)).toString(36)).join('');
171 | };
172 |
173 | Utils.randomLowerString = function (len = 8) {
174 | return Array.apply(0, Array(len))
175 | .map(function () {
176 | return (function (charset) {
177 | return charset.charAt(
178 | Math.floor(Math.random() * charset.length)
179 | );
180 | })('abcdefghijklmnopqrstuvwxyz');
181 | })
182 | .join('');
183 | };
184 |
185 | Utils.capitalize = function (str) {
186 | return str.charAt(0).toUpperCase() + str.slice(1);
187 | };
188 |
189 | Utils.measureText = (text, font) => {
190 | const canvas = Utils.measureText.canvas || (Utils.measureText.canvas = document.createElement("canvas"));
191 | const ctx = canvas.getContext("2d");
192 | ctx.font = font || 'bold 10pt system-ui';
193 | return ctx.measureText(text).width;
194 | };
195 |
196 | Utils.fixName = (d, dName) => {
197 | let i = 1;
198 | const base = dName;
199 | while (d[dName]) {
200 | dName = `${base} (${i})`
201 | i++;
202 | }
203 | return dName;
204 | };
205 |
206 | Utils.isEmptyString = (str) => {
207 | return (!str || str.length === 0);
208 | };
209 |
210 | Utils.isBlankString = (str) => {
211 | return (!str || /^\s*$/.test(str));
212 | };
213 |
214 | Utils.log = function () {};
215 | Utils.count = 0;
216 | Utils.counter = function () {
217 | return (this.count += 1);
218 | };
219 | Utils.getPrefix = function () {
220 | return this.prefix + this.counter();
221 | };
222 |
223 | Utils.prefix = Utils.randomString() + '_';
224 |
225 | Utils.getUrlParameter = function (name) {
226 | const nameA = name.replace(/[\[]/, '\\[').replace(/[\]]/, '\\]');
227 | const regex = new RegExp('[\\?&]' + nameA + '=([^]*)');
228 | const results = regex.exec(location.search.replace(/\/$/, ''));
229 | return results === null
230 | ? null
231 | : decodeURIComponent(results[1].replace(/\+/g, ' '));
232 | };
233 |
234 | Utils.debounce = function (func, wait = 100) {
235 | let timeout;
236 | return function (...args) {
237 | clearTimeout(timeout);
238 | timeout = setTimeout(() => {
239 | func.apply(this, args);
240 | }, wait);
241 | };
242 | };
243 |
244 | Utils.getRandomColor = function () {
245 | return '#' + (((1 << 24) * Math.random()) | 0).toString(16).padStart(6, 0); // just a random color padded to 6 characters
246 | };
247 |
248 | /*
249 | Quick utility to lighten or darken a color (doesn't take color-drifting, etc. into account)
250 | Usage:
251 | fadeColor('#061261', 100); // will lighten the color
252 | fadeColor('#200867'), -100); // will darken the color
253 | */
254 |
255 | Utils.fadeColor = function (col, amt) {
256 | const min = Math.min, max = Math.max;
257 | const num = parseInt(col.replace(/#/g, ''), 16);
258 | const r = min(255, max((num >> 16) + amt, 0));
259 | const g = min(255, max((num & 0x0000FF) + amt, 0));
260 | const b = min(255, max(((num >> 8) & 0x00FF) + amt, 0));
261 | return '#' + (g | (b << 8) | (r << 16)).toString(16).padStart(6, 0);
262 | }
263 |
264 | Utils.lerpColor = function (startColor, targetColor, amount) {
265 | const ah = parseInt(startColor.replace(/#/g, ''), 16);
266 | const ar = ah >> 16;
267 | const ag = (ah >> 8) & 0xff;
268 | const ab = ah & 0xff;
269 | const bh = parseInt(targetColor.replace(/#/g, ''), 16);
270 | const br = bh >> 16;
271 | var bg = (bh >> 8) & 0xff;
272 | var bb = bh & 0xff;
273 | const rr = ar + amount * (br - ar);
274 | const rg = ag + amount * (bg - ag);
275 | const rb = ab + amount * (bb - ab);
276 |
277 | return (
278 | '#' +
279 | (((1 << 24) + (rr << 16) + (rg << 8) + rb) | 0)
280 | .toString(16)
281 | .slice(1)
282 | .toUpperCase()
283 | );
284 | };
285 |
286 | Utils.hexToRgb = function (hex) {
287 | const match = hex.replace(/#/, '').match(/.{1,2}/g);
288 | return {
289 | r: parseInt(match[0], 16),
290 | g: parseInt(match[1], 16),
291 | b: parseInt(match[2], 16)
292 | };
293 | };
294 |
295 | Utils.rgbToHex = (r, g, b) => '#' + [r, g, b].map(x => {
296 | return x.toString(16).padStart(2,0)
297 | }).join('')
298 |
299 |
300 | Utils.nscolorToRgb = function (rP, gP, bP) {
301 | return {
302 | r : Math.round(rP * 255),
303 | g : Math.round(gP * 255),
304 | b : Math.round(bP * 255)
305 | }
306 | };
307 |
308 | Utils.nsColorToHex = function (rP, gP, bP) {
309 | const c = Utils.nscolorToRgb(rP, gP, bP);
310 | return Utils.rgbToHex(c.r, c.g, c.b);
311 | };
312 |
313 | Utils.miredToKelvin = function (mired) {
314 | return Math.round(1e6 / mired);
315 | };
316 |
317 | Utils.kelvinToMired = function (kelvin, roundTo) {
318 | return roundTo ? Utils.roundBy(Math.round(1e6 / kelvin), roundTo) : Math.round(1e6 / kelvin);
319 | };
320 |
321 | Utils.roundBy = function(num, x) {
322 | return Math.round((num - 10) / x) * x;
323 | }
324 |
325 | Utils.getBrightness = function (hexColor) {
326 | // http://www.w3.org/TR/AERT#color-contrast
327 | if (typeof hexColor === 'string' && hexColor.charAt(0) === '#') {
328 | var rgb = Utils.hexToRgb(hexColor);
329 | return (rgb.r * 299 + rgb.g * 587 + rgb.b * 114) / 1000;
330 | }
331 | return 0;
332 | };
333 |
334 | Utils.readJson = function (file, callback) {
335 | var req = new XMLHttpRequest();
336 | req.onerror = function (e) {
337 | // Utils.log(`[Utils][readJson] Error while trying to read ${file}`, e);
338 | };
339 | req.overrideMimeType('application/json');
340 | req.open('GET', file, true);
341 | req.onreadystatechange = function () {
342 | if (req.readyState === 4) {
343 | // && req.status == "200") {
344 | if (callback) callback(req.responseText);
345 | }
346 | };
347 | req.send(null);
348 | };
349 |
350 | Utils.loadScript = function (url, callback) {
351 | const el = document.createElement('script');
352 | el.src = url;
353 | el.onload = function () {
354 | callback(url, true);
355 | };
356 | el.onerror = function () {
357 | console.error('Failed to load file: ' + url);
358 | callback(url, false);
359 | };
360 | document.body.appendChild(el);
361 | };
362 |
363 | Utils.parseJson = function (jsonString) {
364 | if (typeof jsonString === 'object') return jsonString;
365 | try {
366 | const o = JSON.parse(jsonString);
367 |
368 | // Handle non-exception-throwing cases:
369 | // Neither JSON.parse(false) or JSON.parse(1234) throw errors, hence the type-checking,
370 | // but... JSON.parse(null) returns null, and typeof null === "object",
371 | // so we must check for that, too. Thankfully, null is falsey, so this suffices:
372 | if (o && typeof o === 'object') {
373 | return o;
374 | }
375 | } catch (e) {}
376 |
377 | return false;
378 | };
379 |
380 | Utils.parseJSONPromise = function (jsonString) {
381 | // fetch('/my-json-doc-as-string')
382 | // .then(Utils.parseJSONPromise)
383 | // .then(heresYourValidJSON)
384 | // .catch(error - or return default JSON)
385 |
386 | return new Promise((resolve, reject) => {
387 | try {
388 | resolve(JSON.parse(jsonString));
389 | } catch (e) {
390 | reject(e);
391 | }
392 | });
393 | };
394 |
395 | /* eslint-disable import/prefer-default-export */
396 | Utils.getProperty = function (obj, dotSeparatedKeys, defaultValue) {
397 | if (arguments.length > 1 && typeof dotSeparatedKeys !== 'string')
398 | return undefined;
399 | if (typeof obj !== 'undefined' && typeof dotSeparatedKeys === 'string') {
400 | const pathArr = dotSeparatedKeys.split('.');
401 | pathArr.forEach((key, idx, arr) => {
402 | if (typeof key === 'string' && key.includes('[')) {
403 | try {
404 | // extract the array index as string
405 | const pos = /\[([^)]+)\]/.exec(key)[1];
406 | // get the index string length (i.e. '21'.length === 2)
407 | const posLen = pos.length;
408 | arr.splice(idx + 1, 0, Number(pos));
409 |
410 | // keep the key (array name) without the index comprehension:
411 | // (i.e. key without [] (string of length 2)
412 | // and the length of the index (posLen))
413 | arr[idx] = key.slice(0, -2 - posLen); // eslint-disable-line no-param-reassign
414 | } catch (e) {
415 | // do nothing
416 | }
417 | }
418 | });
419 | // eslint-disable-next-line no-param-reassign, no-confusing-arrow
420 | obj = pathArr.reduce(
421 | (o, key) => (o && o[key] !== 'undefined' ? o[key] : undefined),
422 | obj
423 | );
424 | }
425 | return obj === undefined ? defaultValue : obj;
426 | };
427 |
428 | Utils.getProp = (jsn, str, defaultValue = {}, sep = '.') => {
429 | const arr = str.split(sep);
430 | return arr.reduce((obj, key) =>
431 | (obj && obj.hasOwnProperty(key)) ? obj[key] : defaultValue, jsn);
432 | };
433 |
434 | Utils.setProp = function (jsonObj, path, value) {
435 | const names = path.split('.');
436 | let jsn = jsonObj;
437 |
438 | // createNestedObject(jsn, names, values);
439 | // If a value is given, remove the last name and keep it for later:
440 | var targetProperty = arguments.length === 3 ? names.pop() : false;
441 |
442 | // Walk the hierarchy, creating new objects where needed.
443 | // If the lastName was removed, then the last object is not set yet:
444 | for (var i = 0; i < names.length; i++) {
445 | jsn = jsn[names[i]] = jsn[names[i]] || {};
446 | }
447 |
448 | // If a value was given, set it to the target property (the last one):
449 | if (targetProperty) jsn = jsn[targetProperty] = value;
450 |
451 | // Return the last object in the hierarchy:
452 | return jsn;
453 | };
454 |
455 | Utils.getDataUri = function (url, callback, inCanvas, inFillcolor) {
456 | var image = new Image();
457 |
458 | image.onload = function () {
459 | const canvas =
460 | inCanvas && Utils.isCanvas(inCanvas)
461 | ? inCanvas
462 | : document.createElement('canvas');
463 |
464 | canvas.width = this.naturalWidth; // or 'width' if you want a special/scaled size
465 | canvas.height = this.naturalHeight; // or 'height' if you want a special/scaled size
466 |
467 | const ctx = canvas.getContext('2d');
468 | if (inFillcolor) {
469 | ctx.fillStyle = inFillcolor;
470 | ctx.fillRect(0, 0, canvas.width, canvas.height);
471 | }
472 | ctx.drawImage(this, 0, 0);
473 | // Get raw image data
474 | // callback && callback(canvas.toDataURL('image/png').replace(/^data:image\/(png|jpg);base64,/, ''));
475 |
476 | // ... or get as Data URI
477 | callback(canvas.toDataURL('image/png'));
478 | };
479 |
480 | image.src = url;
481 | };
482 |
483 | /** Quick utility to inject a style to the DOM
484 | * e.g. injectStyle('.localbody { background-color: green;}')
485 | */
486 | Utils.injectStyle = function (styles, styleId) {
487 | const node = document.createElement('style');
488 | const tempID = styleId || Utils.randomString(8);
489 | node.setAttribute('id', tempID);
490 | node.innerHTML = styles;
491 | document.body.appendChild(node);
492 | return node;
493 | };
494 |
495 |
496 | Utils.loadImage = function (inUrl, callback, inCanvas, inFillcolor) {
497 | /** Convert to array, so we may load multiple images at once */
498 | const aUrl = !Array.isArray(inUrl) ? [inUrl] : inUrl;
499 | const canvas = inCanvas && inCanvas instanceof HTMLCanvasElement
500 | ? inCanvas
501 | : document.createElement('canvas');
502 | var imgCount = aUrl.length - 1;
503 | const imgCache = {};
504 |
505 | var ctx = canvas.getContext('2d');
506 | ctx.globalCompositeOperation = 'source-over';
507 |
508 | for (let url of aUrl) {
509 | let image = new Image();
510 | let cnt = imgCount;
511 | let w = 144, h = 144;
512 |
513 | image.onload = function () {
514 | imgCache[url] = this;
515 | // look at the size of the first image
516 | if (url === aUrl[0]) {
517 | canvas.width = this.naturalWidth; // or 'width' if you want a special/scaled size
518 | canvas.height = this.naturalHeight; // or 'height' if you want a special/scaled size
519 | }
520 | // if (Object.keys(imgCache).length == aUrl.length) {
521 | if (cnt < 1) {
522 | if (inFillcolor) {
523 | ctx.fillStyle = inFillcolor;
524 | ctx.fillRect(0, 0, canvas.width, canvas.height);
525 | }
526 | // draw in the proper sequence FIFO
527 | aUrl.forEach(e => {
528 | if (!imgCache[e]) {
529 | console.warn(imgCache[e], imgCache);
530 | }
531 |
532 | if (imgCache[e]) {
533 | ctx.drawImage(imgCache[e], 0, 0);
534 | ctx.save();
535 | }
536 | });
537 |
538 | callback(canvas.toDataURL('image/png'));
539 | // or to get raw image data
540 | // callback && callback(canvas.toDataURL('image/png').replace(/^data:image\/(png|jpg);base64,/, ''));
541 | }
542 | };
543 |
544 | imgCount--;
545 | image.src = url;
546 | }
547 | };
548 |
549 | Utils.getData = function (url) {
550 | // Return a new promise.
551 | return new Promise(function (resolve, reject) {
552 | // Do the usual XHR stuff
553 | var req = new XMLHttpRequest();
554 | // Make sure to call .open asynchronously
555 | req.open('GET', url, true);
556 |
557 | req.onload = function () {
558 | // This is called even on 404 etc
559 | // so check the status
560 | if (req.status === 200) {
561 | // Resolve the promise with the response text
562 | resolve(req.response);
563 | } else {
564 | // Otherwise reject with the status text
565 | // which will hopefully be a meaningful error
566 | reject(Error(req.statusText));
567 | }
568 | };
569 |
570 | // Handle network errors
571 | req.onerror = function () {
572 | reject(Error('Network Error'));
573 | };
574 |
575 | // Make the request
576 | req.send();
577 | });
578 | };
579 |
580 | Utils.negArray = function (arr) {
581 | /** http://h3manth.com/new/blog/2013/negative-array-index-in-javascript/ */
582 | return Proxy.create({
583 | set: function (proxy, index, value) {
584 | index = parseInt(index);
585 | return index < 0 ? (arr[arr.length + index] = value) : (arr[index] = value);
586 | },
587 | get: function (proxy, index) {
588 | index = parseInt(index);
589 | return index < 0 ? arr[arr.length + index] : arr[index];
590 | }
591 | });
592 | };
593 |
594 | Utils.onChange = function (object, callback) {
595 | /** https://github.com/sindresorhus/on-change */
596 | 'use strict';
597 | const handler = {
598 | get (target, property, receiver) {
599 | try {
600 | console.log('get via Proxy: ', property, target, receiver);
601 | return new Proxy(target[property], handler);
602 | } catch (err) {
603 | console.log('get via Reflect: ', err, property, target, receiver);
604 | return Reflect.get(target, property, receiver);
605 | }
606 | },
607 | set (target, property, value, receiver) {
608 | console.log('Utils.onChange:set1:', target, property, value, receiver);
609 | // target[property] = value;
610 | const b = Reflect.set(target, property, value);
611 | console.log('Utils.onChange:set2:', target, property, value, receiver);
612 | return b;
613 | },
614 | defineProperty (target, property, descriptor) {
615 | console.log('Utils.onChange:defineProperty:', target, property, descriptor);
616 | callback(target, property, descriptor);
617 | return Reflect.defineProperty(target, property, descriptor);
618 | },
619 | deleteProperty (target, property) {
620 | console.log('Utils.onChange:deleteProperty:', target, property);
621 | callback(target, property);
622 | return Reflect.deleteProperty(target, property);
623 | }
624 | };
625 |
626 | return new Proxy(object, handler);
627 | };
628 |
629 | Utils.observeArray = function (object, callback) {
630 | 'use strict';
631 | const array = [];
632 | const handler = {
633 | get (target, property, receiver) {
634 | try {
635 | return new Proxy(target[property], handler);
636 | } catch (err) {
637 | return Reflect.get(target, property, receiver);
638 | }
639 | },
640 | set (target, property, value, receiver) {
641 | console.log('XXXUtils.observeArray:set1:', target, property, value, array);
642 | target[property] = value;
643 | console.log('XXXUtils.observeArray:set2:', target, property, value, array);
644 | },
645 | defineProperty (target, property, descriptor) {
646 | callback(target, property, descriptor);
647 | return Reflect.defineProperty(target, property, descriptor);
648 | },
649 | deleteProperty (target, property) {
650 | callback(target, property, descriptor);
651 | return Reflect.deleteProperty(target, property);
652 | }
653 | };
654 |
655 | return new Proxy(object, handler);
656 | };
657 |
658 | window['_'] = Utils;
659 |
660 | /*
661 | * connectElgatoStreamDeckSocket
662 | * This is the first function StreamDeck Software calls, when
663 | * establishing the connection to the plugin or the Property Inspector
664 | * @param {string} inPort - The socket's port to communicate with StreamDeck software.
665 | * @param {string} inUUID - A unique identifier, which StreamDeck uses to communicate with the plugin
666 | * @param {string} inMessageType - Identifies, if the event is meant for the property inspector or the plugin.
667 | * @param {string} inApplicationInfo - Information about the host (StreamDeck) application
668 | * @param {string} inActionInfo - Context is an internal identifier used to communicate to the host application.
669 | */
670 |
671 |
672 | // eslint-disable-next-line no-unused-vars
673 | function connectElgatoStreamDeckSocket (
674 | inPort,
675 | inUUID,
676 | inMessageType,
677 | inApplicationInfo,
678 | inActionInfo
679 | ) {
680 | StreamDeck.getInstance().connect(arguments);
681 | window.$SD.api = Object.assign({ send: SDApi.send }, SDApi.common, SDApi[inMessageType]);
682 | }
683 |
684 | /* legacy support */
685 |
686 | function connectSocket (
687 | inPort,
688 | inUUID,
689 | inMessageType,
690 | inApplicationInfo,
691 | inActionInfo
692 | ) {
693 | connectElgatoStreamDeckSocket(
694 | inPort,
695 | inUUID,
696 | inMessageType,
697 | inApplicationInfo,
698 | inActionInfo
699 | );
700 | }
701 |
702 | /**
703 | * StreamDeck object containing all required code to establish
704 | * communication with SD-Software and the Property Inspector
705 | */
706 |
707 | const StreamDeck = (function () {
708 | // Hello it's me
709 | var instance;
710 | /*
711 | Populate and initialize internally used properties
712 | */
713 |
714 | function init () {
715 | // *** PRIVATE ***
716 |
717 | var inPort,
718 | inUUID,
719 | inMessageType,
720 | inApplicationInfo,
721 | inActionInfo,
722 | websocket = null;
723 |
724 | var events = ELGEvents.eventEmitter();
725 | var logger = SDDebug.logger();
726 |
727 | function showVars () {
728 | debugLog('---- showVars');
729 | debugLog('- port', inPort);
730 | debugLog('- uuid', inUUID);
731 | debugLog('- messagetype', inMessageType);
732 | debugLog('- info', inApplicationInfo);
733 | debugLog('- inActionInfo', inActionInfo);
734 | debugLog('----< showVars');
735 | }
736 |
737 | function connect (args) {
738 | inPort = args[0];
739 | inUUID = args[1];
740 | inMessageType = args[2];
741 | inApplicationInfo = Utils.parseJson(args[3]);
742 | inActionInfo = args[4] !== 'undefined' ? Utils.parseJson(args[4]) : args[4];
743 |
744 | /** Debug variables */
745 | if (debug) {
746 | showVars();
747 | }
748 |
749 | const lang = Utils.getProp(inApplicationInfo,'application.language', false);
750 | if (lang) {
751 | loadLocalization(lang, inMessageType === 'registerPropertyInspector' ? '../' : './', function() {
752 | events.emit('localizationLoaded', {language:lang});
753 | });
754 | };
755 |
756 | /** restrict the API to what's possible
757 | * within Plugin or Property Inspector
758 | *
759 | */
760 | // $SD.api = SDApi[inMessageType];
761 |
762 | if (websocket) {
763 | websocket.close();
764 | websocket = null;
765 | };
766 |
767 | websocket = new WebSocket('ws://127.0.0.1:' + inPort);
768 |
769 | websocket.onopen = function () {
770 | var json = {
771 | event: inMessageType,
772 | uuid: inUUID
773 | };
774 |
775 | // console.log('***************', inMessageType + " websocket:onopen", inUUID, json);
776 |
777 | websocket.sendJSON(json);
778 | $SD.uuid = inUUID;
779 | $SD.actionInfo = inActionInfo;
780 | $SD.applicationInfo = inApplicationInfo;
781 | $SD.messageType = inMessageType;
782 | $SD.connection = websocket;
783 |
784 | instance.emit('connected', {
785 | connection: websocket,
786 | port: inPort,
787 | uuid: inUUID,
788 | actionInfo: inActionInfo,
789 | applicationInfo: inApplicationInfo,
790 | messageType: inMessageType
791 | });
792 | };
793 |
794 | websocket.onerror = function (evt) {
795 | console.warn('WEBOCKET ERROR', evt, evt.data);
796 | };
797 |
798 | websocket.onclose = function (evt) {
799 | // Websocket is closed
800 | var reason = WEBSOCKETERROR(evt);
801 | console.warn(
802 | '[STREAMDECK]***** WEBOCKET CLOSED **** reason:',
803 | reason
804 | );
805 | };
806 |
807 | websocket.onmessage = function (evt) {
808 | var jsonObj = Utils.parseJson(evt.data),
809 | m;
810 |
811 | //console.log('[STREAMDECK] websocket.onmessage ... ', jsonObj.event, jsonObj);
812 |
813 | if (!jsonObj.hasOwnProperty('action')) {
814 | m = jsonObj.event;
815 | //console.log('%c%s', 'color: white; background: red; font-size: 12px;', '[common.js]onmessage:', m);
816 | } else {
817 | switch (inMessageType) {
818 | case 'registerPlugin':
819 | m = jsonObj['action'] + '.' + jsonObj['event'];
820 | break;
821 | case 'registerPropertyInspector':
822 | m = 'sendToPropertyInspector';
823 | break;
824 | default:
825 | console.log('%c%s', 'color: white; background: red; font-size: 12px;', '[STREAMDECK] websocket.onmessage +++++++++ PROBLEM ++++++++');
826 | console.warn('UNREGISTERED MESSAGETYPE:', inMessageType);
827 | }
828 | }
829 |
830 | if (m && m !== '')
831 | events.emit(m, jsonObj);
832 | };
833 |
834 | instance.connection = websocket;
835 | }
836 |
837 | return {
838 | // *** PUBLIC ***
839 |
840 | uuid: inUUID,
841 | on: events.on,
842 | emit: events.emit,
843 | connection: websocket,
844 | connect: connect,
845 | api: null,
846 | logger: logger
847 | };
848 | }
849 |
850 | return {
851 | getInstance: function () {
852 | if (!instance) {
853 | instance = init();
854 | }
855 | return instance;
856 | }
857 | };
858 | })();
859 |
860 | // eslint-disable-next-line no-unused-vars
861 | function initializeControlCenterClient () {
862 | const settings = Object.assign(REMOTESETTINGS || {}, { debug: false });
863 | var $CC = new ControlCenterClient(settings);
864 | window['$CC'] = $CC;
865 | return $CC;
866 | }
867 |
868 | /** ELGEvents
869 | * Publish/Subscribe pattern to quickly signal events to
870 | * the plugin, property inspector and data.
871 | */
872 |
873 | const ELGEvents = {
874 | eventEmitter: function (name, fn) {
875 | const eventList = new Map();
876 |
877 | const on = (name, fn) => {
878 | if (!eventList.has(name)) eventList.set(name, ELGEvents.pubSub());
879 |
880 | return eventList.get(name).sub(fn);
881 | };
882 |
883 | const has = (name) =>
884 | eventList.has(name);
885 |
886 | const emit = (name, data) =>
887 | eventList.has(name) && eventList.get(name).pub(data);
888 |
889 | return Object.freeze({ on, has, emit, eventList });
890 | },
891 |
892 | pubSub: function pubSub () {
893 | const subscribers = new Set();
894 |
895 | const sub = fn => {
896 | subscribers.add(fn);
897 | return () => {
898 | subscribers.delete(fn);
899 | };
900 | };
901 |
902 | const pub = data => subscribers.forEach(fn => fn(data));
903 | return Object.freeze({ pub, sub });
904 | }
905 | };
906 |
907 | /** SDApi
908 | * This ist the main API to communicate between plugin, property inspector and
909 | * application host.
910 | * Internal functions:
911 | * - setContext: sets the context of the current plugin
912 | * - exec: prepare the correct JSON structure and send
913 | *
914 | * Methods exposed in the $SD.api alias
915 | * Messages send from the plugin
916 | * -----------------------------
917 | * - showAlert
918 | * - showOK
919 | * - setSettings
920 | * - setTitle
921 | * - setImage
922 | * - sendToPropertyInspector
923 | *
924 | * Messages send from Property Inspector
925 | * -------------------------------------
926 | * - sendToPlugin
927 | *
928 | * Messages received in the plugin
929 | * -------------------------------
930 | * willAppear
931 | * willDisappear
932 | * keyDown
933 | * keyUp
934 | */
935 |
936 | const SDApi = {
937 | send: function (context, fn, payload, debug) {
938 | /** Combine the passed JSON with the name of the event and it's context
939 | * If the payload contains 'event' or 'context' keys, it will overwrite existing 'event' or 'context'.
940 | * This function is non-mutating and thereby creates a new object containing
941 | * all keys of the original JSON objects.
942 | */
943 | const pl = Object.assign({}, { event: fn, context: context }, payload);
944 |
945 | /** Check, if we have a connection, and if, send the JSON payload */
946 | if (debug) {
947 | console.log('-----SDApi.send-----');
948 | console.log('context', context);
949 | console.log(pl);
950 | console.log(payload.payload);
951 | console.log(JSON.stringify(payload.payload));
952 | console.log('-------');
953 | }
954 | $SD.connection && $SD.connection.sendJSON(pl);
955 |
956 | /**
957 | * DEBUG-Utility to quickly show the current payload in the Property Inspector.
958 | */
959 |
960 | if (
961 | $SD.connection &&
962 | [
963 | 'sendToPropertyInspector',
964 | 'showOK',
965 | 'showAlert',
966 | 'setSettings'
967 | ].indexOf(fn) === -1
968 | ) {
969 | // console.log("send.sendToPropertyInspector", payload);
970 | // this.sendToPropertyInspector(context, typeof payload.payload==='object' ? JSON.stringify(payload.payload) : JSON.stringify({'payload':payload.payload}), pl['action']);
971 | }
972 | },
973 |
974 | registerPlugin: {
975 |
976 | /** Messages send from the plugin */
977 | showAlert: function (context) {
978 | SDApi.send(context, 'showAlert', {});
979 | },
980 |
981 | showOk: function (context) {
982 | SDApi.send(context, 'showOk', {});
983 | },
984 |
985 |
986 | setState: function (context, payload) {
987 | SDApi.send(context, 'setState', {
988 | payload: {
989 | 'state': 1 - Number(payload === 0)
990 | }
991 | });
992 | },
993 |
994 | setTitle: function (context, title, target) {
995 | SDApi.send(context, 'setTitle', {
996 | payload: {
997 | title: '' + title || '',
998 | target: target || DestinationEnum.HARDWARE_AND_SOFTWARE
999 | }
1000 | });
1001 | },
1002 |
1003 | setImage: function (context, img, target) {
1004 | SDApi.send(context, 'setImage', {
1005 | payload: {
1006 | image: img || '',
1007 | target: target || DestinationEnum.HARDWARE_AND_SOFTWARE
1008 | }
1009 | });
1010 | },
1011 |
1012 | sendToPropertyInspector: function (context, payload, action) {
1013 | SDApi.send(context, 'sendToPropertyInspector', {
1014 | action: action,
1015 | payload: payload
1016 | });
1017 | },
1018 |
1019 | showUrl2: function (context, urlToOpen) {
1020 | SDApi.send(context, 'openUrl', {
1021 | payload: {
1022 | url: urlToOpen
1023 | }
1024 | });
1025 | }
1026 | },
1027 |
1028 | /** Messages send from Property Inspector */
1029 |
1030 | registerPropertyInspector: {
1031 |
1032 | sendToPlugin: function (piUUID, action, payload) {
1033 | SDApi.send(
1034 | piUUID,
1035 | 'sendToPlugin',
1036 | {
1037 | action: action,
1038 | payload: payload || {}
1039 | },
1040 | false
1041 | );
1042 | }
1043 | },
1044 |
1045 | /** COMMON */
1046 |
1047 | common: {
1048 |
1049 | getSettings: function (context, payload) {
1050 | SDApi.send(context, 'getSettings', {});
1051 | },
1052 |
1053 | setSettings: function (context, payload) {
1054 | SDApi.send(context, 'setSettings', {
1055 | payload: payload
1056 | });
1057 | },
1058 |
1059 | getGlobalSettings: function (context, payload) {
1060 | SDApi.send(context, 'getGlobalSettings', {});
1061 | },
1062 |
1063 | setGlobalSettings: function (context, payload) {
1064 | SDApi.send(context, 'setGlobalSettings', {
1065 | payload: payload
1066 | });
1067 | },
1068 |
1069 | logMessage: function (context, payload) {
1070 | SDApi.send(context, 'logMessage', {
1071 | payload: payload
1072 | });
1073 | },
1074 |
1075 | openUrl: function (context, urlToOpen) {
1076 | SDApi.send(context, 'openUrl', {
1077 | payload: {
1078 | url: urlToOpen
1079 | }
1080 | });
1081 | },
1082 |
1083 | test: function () {
1084 | console.log(this);
1085 | console.log(SDApi);
1086 | },
1087 |
1088 | debugPrint: function (context, inString) {
1089 | // console.log("------------ DEBUGPRINT");
1090 | // console.log([].slice.apply(arguments).join());
1091 | // console.log("------------ DEBUGPRINT");
1092 | SDApi.send(context, 'debugPrint', {
1093 | payload: [].slice.apply(arguments).join('.') || ''
1094 | });
1095 | },
1096 |
1097 | dbgSend: function (fn, context) {
1098 | /** lookup if an appropriate function exists */
1099 | if ($SD.connection && this[fn] && typeof this[fn] === 'function') {
1100 | /** verify if type of payload is an object/json */
1101 | const payload = this[fn]();
1102 | if (typeof payload === 'object') {
1103 | Object.assign({ event: fn, context: context }, payload);
1104 | $SD.connection && $SD.connection.sendJSON(payload);
1105 | }
1106 | }
1107 | console.log(this, fn, typeof this[fn], this[fn]());
1108 | }
1109 |
1110 | }
1111 | };
1112 |
1113 | /** SDDebug
1114 | * Utility to log the JSON structure of an incoming object
1115 | */
1116 |
1117 | const SDDebug = {
1118 | logger: function (name, fn) {
1119 | const logEvent = jsn => {
1120 | console.log('____SDDebug.logger.logEvent');
1121 | console.log(jsn);
1122 | debugLog('-->> Received Obj:', jsn);
1123 | debugLog('jsonObj', jsn);
1124 | debugLog('event', jsn['event']);
1125 | debugLog('actionType', jsn['actionType']);
1126 | debugLog('settings', jsn['settings']);
1127 | debugLog('coordinates', jsn['coordinates']);
1128 | debugLog('---');
1129 | };
1130 |
1131 | const logSomething = jsn =>
1132 | console.log('____SDDebug.logger.logSomething');
1133 |
1134 | return { logEvent, logSomething };
1135 | }
1136 | };
1137 |
1138 | /**
1139 | * This is the instance of the StreamDeck object.
1140 | * There's only one StreamDeck object, which carries
1141 | * connection parameters and handles communication
1142 | * to/from the software's PluginManager.
1143 | */
1144 |
1145 | window.$SD = StreamDeck.getInstance();
1146 | window.$SD.api = SDApi;
1147 |
1148 | function WEBSOCKETERROR (evt) {
1149 | // Websocket is closed
1150 | var reason = '';
1151 | if (evt.code === 1000) {
1152 | reason = 'Normal Closure. The purpose for which the connection was established has been fulfilled.';
1153 | } else if (evt.code === 1001) {
1154 | reason = 'Going Away. An endpoint is "going away", such as a server going down or a browser having navigated away from a page.';
1155 | } else if (evt.code === 1002) {
1156 | reason = 'Protocol error. An endpoint is terminating the connection due to a protocol error';
1157 | } else if (evt.code === 1003) {
1158 | reason = "Unsupported Data. An endpoint received a type of data it doesn't support.";
1159 | } else if (evt.code === 1004) {
1160 | reason = '--Reserved--. The specific meaning might be defined in the future.';
1161 | } else if (evt.code === 1005) {
1162 | reason = 'No Status. No status code was actually present.';
1163 | } else if (evt.code === 1006) {
1164 | reason = 'Abnormal Closure. The connection was closed abnormally, e.g., without sending or receiving a Close control frame';
1165 | } else if (evt.code === 1007) {
1166 | reason = 'Invalid frame payload data. The connection was closed, because the received data was not consistent with the type of the message (e.g., non-UTF-8 [http://tools.ietf.org/html/rfc3629]).';
1167 | } else if (evt.code === 1008) {
1168 | reason = 'Policy Violation. The connection was closed, because current message data "violates its policy". This reason is given either if there is no other suitable reason, or if there is a need to hide specific details about the policy.';
1169 | } else if (evt.code === 1009) {
1170 | reason = 'Message Too Big. Connection closed because the message is too big for it to process.';
1171 | } else if (evt.code === 1010) { // Note that this status code is not used by the server, because it can fail the WebSocket handshake instead.
1172 | reason = "Mandatory Ext. Connection is terminated the connection because the server didn't negotiate one or more extensions in the WebSocket handshake. Mandatory extensions were: " + evt.reason;
1173 | } else if (evt.code === 1011) {
1174 | reason = 'Internl Server Error. Connection closed because it encountered an unexpected condition that prevented it from fulfilling the request.';
1175 | } else if (evt.code === 1015) {
1176 | reason = "TLS Handshake. The connection was closed due to a failure to perform a TLS handshake (e.g., the server certificate can't be verified).";
1177 | } else {
1178 | reason = 'Unknown reason';
1179 | }
1180 |
1181 | return reason;
1182 | }
1183 |
1184 | const SOCKETERRORS = {
1185 | '0': 'The connection has not yet been established',
1186 | '1': 'The connection is established and communication is possible',
1187 | '2': 'The connection is going through the closing handshake',
1188 | '3': 'The connection has been closed or could not be opened'
1189 | };
1190 |
1191 |
--------------------------------------------------------------------------------
/com.github.sbelectronics.streamdeck.binclock.sdPlugin/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "Actions": [
3 | {
4 | "Icon": "icon",
5 | "Name": "binclock",
6 | "States": [
7 | {
8 | "Image": "defaultImage",
9 | "TitleAlignment": "middle",
10 | "FontSize": "17"
11 | }
12 | ],
13 | "SupportedInMultiActions": false,
14 | "Tooltip": "BinaryClock",
15 | "UUID": "com.github.sbelectronics.streamdeck.binclock"
16 | }
17 | ],
18 | "SDKVersion": 2,
19 | "Author": "Scott M Baker",
20 | "CodePathWin": "binclock.exe",
21 | "PropertyInspectorPath": "index_pi.html",
22 | "Description": "Binary/BCD Clock.",
23 | "Name": "binclock",
24 | "Icon": "pluginIcon",
25 | "URL": "https://www.github.com/sbelectronics/streamdeck",
26 | "Version": "1.1",
27 | "OS": [
28 | {
29 | "Platform": "windows",
30 | "MinimumVersion" : "10"
31 | }
32 | ],
33 | "Software":
34 | {
35 | "MinimumVersion" : "4.1"
36 | }
37 | }
38 |
--------------------------------------------------------------------------------
/com.github.sbelectronics.streamdeck.binclock.sdPlugin/pluginIcon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sbelectronics/streamdeck/f0175932e1283d59d50e7410ead230b293fa39cc/com.github.sbelectronics.streamdeck.binclock.sdPlugin/pluginIcon.png
--------------------------------------------------------------------------------
/com.github.sbelectronics.streamdeck.demo.sdPlugin/defaultImage.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sbelectronics/streamdeck/f0175932e1283d59d50e7410ead230b293fa39cc/com.github.sbelectronics.streamdeck.demo.sdPlugin/defaultImage.png
--------------------------------------------------------------------------------
/com.github.sbelectronics.streamdeck.demo.sdPlugin/demo.exe:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sbelectronics/streamdeck/f0175932e1283d59d50e7410ead230b293fa39cc/com.github.sbelectronics.streamdeck.demo.sdPlugin/demo.exe
--------------------------------------------------------------------------------
/com.github.sbelectronics.streamdeck.demo.sdPlugin/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sbelectronics/streamdeck/f0175932e1283d59d50e7410ead230b293fa39cc/com.github.sbelectronics.streamdeck.demo.sdPlugin/icon.png
--------------------------------------------------------------------------------
/com.github.sbelectronics.streamdeck.demo.sdPlugin/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "Actions": [
3 | {
4 | "Icon": "icon",
5 | "Name": "golang-demo",
6 | "States": [
7 | {
8 | "Image": "defaultImage",
9 | "TitleAlignment": "middle",
10 | "FontSize": "17"
11 | }
12 | ],
13 | "SupportedInMultiActions": false,
14 | "Tooltip": "Golang Demo",
15 | "UUID": "com.github.sbelectronics.streamdeck.demo"
16 | }
17 | ],
18 | "SDKVersion": 2,
19 | "Author": "Scott M Baker",
20 | "CodePathWin": "demo.exe",
21 | "Description": "Golang plugin demo.",
22 | "Name": "golang-demo",
23 | "Icon": "pluginIcon",
24 | "URL": "https://www.github.com/sbelectronics/streamdeck",
25 | "Version": "1.0",
26 | "OS": [
27 | {
28 | "Platform": "windows",
29 | "MinimumVersion" : "10"
30 | }
31 | ],
32 | "Software":
33 | {
34 | "MinimumVersion" : "4.1"
35 | }
36 | }
--------------------------------------------------------------------------------
/com.github.sbelectronics.streamdeck.demo.sdPlugin/pluginIcon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sbelectronics/streamdeck/f0175932e1283d59d50e7410ead230b293fa39cc/com.github.sbelectronics.streamdeck.demo.sdPlugin/pluginIcon.png
--------------------------------------------------------------------------------
/com.github.sbelectronics.streamdeck.http.sdPlugin/defaultImage.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sbelectronics/streamdeck/f0175932e1283d59d50e7410ead230b293fa39cc/com.github.sbelectronics.streamdeck.http.sdPlugin/defaultImage.png
--------------------------------------------------------------------------------
/com.github.sbelectronics.streamdeck.http.sdPlugin/http.exe:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sbelectronics/streamdeck/f0175932e1283d59d50e7410ead230b293fa39cc/com.github.sbelectronics.streamdeck.http.sdPlugin/http.exe
--------------------------------------------------------------------------------
/com.github.sbelectronics.streamdeck.http.sdPlugin/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sbelectronics/streamdeck/f0175932e1283d59d50e7410ead230b293fa39cc/com.github.sbelectronics.streamdeck.http.sdPlugin/icon.png
--------------------------------------------------------------------------------
/com.github.sbelectronics.streamdeck.http.sdPlugin/index_pi.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Binary Clock Property Inspector
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
Request
14 |
15 |
19 |
20 |
21 |
Operation
22 |
23 | GET
24 | PUT
25 | POST
26 | DELETE
27 |
28 |
29 |
30 |
31 |
32 | Data (for POST/PUT/PATCH)
33 |
34 |
38 |
42 |
43 |
44 |
45 |
106 |
107 |
108 |
--------------------------------------------------------------------------------
/com.github.sbelectronics.streamdeck.http.sdPlugin/js/common.js:
--------------------------------------------------------------------------------
1 | /* global $SD, $localizedStrings */
2 | /* exported, $localizedStrings */
3 | /* eslint no-undef: "error",
4 | curly: 0,
5 | no-caller: 0,
6 | wrap-iife: 0,
7 | one-var: 0,
8 | no-var: 0,
9 | vars-on-top: 0
10 | */
11 |
12 | // don't change this to let or const, because we rely on var's hoisting
13 | // eslint-disable-next-line no-use-before-define, no-var
14 | var $localizedStrings = $localizedStrings || {},
15 | REMOTESETTINGS = REMOTESETTINGS || {},
16 | DestinationEnum = Object.freeze({
17 | HARDWARE_AND_SOFTWARE: 0,
18 | HARDWARE_ONLY: 1,
19 | SOFTWARE_ONLY: 2
20 | }),
21 | // eslint-disable-next-line no-unused-vars
22 | isQT = navigator.appVersion.includes('QtWebEngine'),
23 | debug = debug || false,
24 | debugLog = function () {},
25 | MIMAGECACHE = MIMAGECACHE || {};
26 |
27 | const setDebugOutput = (debug) => (debug === true) ? console.log.bind(window.console) : function () {};
28 | debugLog = setDebugOutput(debug);
29 |
30 | // Create a wrapper to allow passing JSON to the socket
31 | WebSocket.prototype.sendJSON = function (jsn, log) {
32 | if (log) {
33 | console.log('SendJSON', this, jsn);
34 | }
35 | // if (this.readyState) {
36 | this.send(JSON.stringify(jsn));
37 | // }
38 | };
39 |
40 | /* eslint no-extend-native: ["error", { "exceptions": ["String"] }] */
41 | String.prototype.lox = function () {
42 | var a = String(this);
43 | try {
44 | a = $localizedStrings[a] || a;
45 | } catch (b) {}
46 | return a;
47 | };
48 |
49 | String.prototype.sprintf = function (inArr) {
50 | let i = 0;
51 | const args = (inArr && Array.isArray(inArr)) ? inArr : arguments;
52 | return this.replace(/%s/g, function () {
53 | return args[i++];
54 | });
55 | };
56 |
57 | // eslint-disable-next-line no-unused-vars
58 | const sprintf = (s, ...args) => {
59 | let i = 0;
60 | return s.replace(/%s/g, function () {
61 | return args[i++];
62 | });
63 | };
64 |
65 | const loadLocalization = (lang, pathPrefix, cb) => {
66 | Utils.readJson(`${pathPrefix}${lang}.json`, function (jsn) {
67 | const manifest = Utils.parseJson(jsn);
68 | $localizedStrings = manifest && manifest.hasOwnProperty('Localization') ? manifest['Localization'] : {};
69 | debugLog($localizedStrings);
70 | if (cb && typeof cb === 'function') cb();
71 | });
72 | }
73 |
74 | var Utils = {
75 | sleep: function (milliseconds) {
76 | return new Promise(resolve => setTimeout(resolve, milliseconds));
77 | },
78 | isUndefined: function (value) {
79 | return typeof value === 'undefined';
80 | },
81 | isObject: function (o) {
82 | return (
83 | typeof o === 'object' &&
84 | o !== null &&
85 | o.constructor &&
86 | o.constructor === Object
87 | );
88 | },
89 | isPlainObject: function (o) {
90 | return (
91 | typeof o === 'object' &&
92 | o !== null &&
93 | o.constructor &&
94 | o.constructor === Object
95 | );
96 | },
97 | isArray: function (value) {
98 | return Array.isArray(value);
99 | },
100 | isNumber: function (value) {
101 | return typeof value === 'number' && value !== null;
102 | },
103 | isInteger (value) {
104 | return typeof value === 'number' && value === Number(value);
105 | },
106 | isString (value) {
107 | return typeof value === 'string';
108 | },
109 | isImage (value) {
110 | return value instanceof HTMLImageElement;
111 | },
112 | isCanvas (value) {
113 | return value instanceof HTMLCanvasElement;
114 | },
115 | isValue: function (value) {
116 | return !this.isObject(value) && !this.isArray(value);
117 | },
118 | isNull: function (value) {
119 | return value === null;
120 | },
121 | toInteger: function (value) {
122 | const INFINITY = 1 / 0,
123 | MAX_INTEGER = 1.7976931348623157e308;
124 | if (!value) {
125 | return value === 0 ? value : 0;
126 | }
127 | value = Number(value);
128 | if (value === INFINITY || value === -INFINITY) {
129 | const sign = value < 0 ? -1 : 1;
130 | return sign * MAX_INTEGER;
131 | }
132 | return value === value ? value : 0;
133 | }
134 | };
135 | Utils.minmax = function (v, min = 0, max = 100) {
136 | return Math.min(max, Math.max(min, v));
137 | };
138 |
139 | Utils.rangeToPercent = function (value, min, max) {
140 | return ((value - min) / (max - min));
141 | };
142 |
143 | Utils.percentToRange = function (percent, min, max) {
144 | return ((max - min) * percent + min);
145 | };
146 |
147 | Utils.setDebugOutput = (debug) => {
148 | return (debug === true) ? console.log.bind(window.console) : function () {};
149 | };
150 |
151 | Utils.randomComponentName = function (len = 6) {
152 | return `${Utils.randomLowerString(len)}-${Utils.randomLowerString(len)}`;
153 | };
154 |
155 | Utils.randomString = function (len = 8) {
156 | return Array.apply(0, Array(len))
157 | .map(function () {
158 | return (function (charset) {
159 | return charset.charAt(
160 | Math.floor(Math.random() * charset.length)
161 | );
162 | })(
163 | 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
164 | );
165 | })
166 | .join('');
167 | };
168 |
169 | Utils.rs = function (len = 8) {
170 | return [...Array(len)].map(i => (~~(Math.random() * 36)).toString(36)).join('');
171 | };
172 |
173 | Utils.randomLowerString = function (len = 8) {
174 | return Array.apply(0, Array(len))
175 | .map(function () {
176 | return (function (charset) {
177 | return charset.charAt(
178 | Math.floor(Math.random() * charset.length)
179 | );
180 | })('abcdefghijklmnopqrstuvwxyz');
181 | })
182 | .join('');
183 | };
184 |
185 | Utils.capitalize = function (str) {
186 | return str.charAt(0).toUpperCase() + str.slice(1);
187 | };
188 |
189 | Utils.measureText = (text, font) => {
190 | const canvas = Utils.measureText.canvas || (Utils.measureText.canvas = document.createElement("canvas"));
191 | const ctx = canvas.getContext("2d");
192 | ctx.font = font || 'bold 10pt system-ui';
193 | return ctx.measureText(text).width;
194 | };
195 |
196 | Utils.fixName = (d, dName) => {
197 | let i = 1;
198 | const base = dName;
199 | while (d[dName]) {
200 | dName = `${base} (${i})`
201 | i++;
202 | }
203 | return dName;
204 | };
205 |
206 | Utils.isEmptyString = (str) => {
207 | return (!str || str.length === 0);
208 | };
209 |
210 | Utils.isBlankString = (str) => {
211 | return (!str || /^\s*$/.test(str));
212 | };
213 |
214 | Utils.log = function () {};
215 | Utils.count = 0;
216 | Utils.counter = function () {
217 | return (this.count += 1);
218 | };
219 | Utils.getPrefix = function () {
220 | return this.prefix + this.counter();
221 | };
222 |
223 | Utils.prefix = Utils.randomString() + '_';
224 |
225 | Utils.getUrlParameter = function (name) {
226 | const nameA = name.replace(/[\[]/, '\\[').replace(/[\]]/, '\\]');
227 | const regex = new RegExp('[\\?&]' + nameA + '=([^]*)');
228 | const results = regex.exec(location.search.replace(/\/$/, ''));
229 | return results === null
230 | ? null
231 | : decodeURIComponent(results[1].replace(/\+/g, ' '));
232 | };
233 |
234 | Utils.debounce = function (func, wait = 100) {
235 | let timeout;
236 | return function (...args) {
237 | clearTimeout(timeout);
238 | timeout = setTimeout(() => {
239 | func.apply(this, args);
240 | }, wait);
241 | };
242 | };
243 |
244 | Utils.getRandomColor = function () {
245 | return '#' + (((1 << 24) * Math.random()) | 0).toString(16).padStart(6, 0); // just a random color padded to 6 characters
246 | };
247 |
248 | /*
249 | Quick utility to lighten or darken a color (doesn't take color-drifting, etc. into account)
250 | Usage:
251 | fadeColor('#061261', 100); // will lighten the color
252 | fadeColor('#200867'), -100); // will darken the color
253 | */
254 |
255 | Utils.fadeColor = function (col, amt) {
256 | const min = Math.min, max = Math.max;
257 | const num = parseInt(col.replace(/#/g, ''), 16);
258 | const r = min(255, max((num >> 16) + amt, 0));
259 | const g = min(255, max((num & 0x0000FF) + amt, 0));
260 | const b = min(255, max(((num >> 8) & 0x00FF) + amt, 0));
261 | return '#' + (g | (b << 8) | (r << 16)).toString(16).padStart(6, 0);
262 | }
263 |
264 | Utils.lerpColor = function (startColor, targetColor, amount) {
265 | const ah = parseInt(startColor.replace(/#/g, ''), 16);
266 | const ar = ah >> 16;
267 | const ag = (ah >> 8) & 0xff;
268 | const ab = ah & 0xff;
269 | const bh = parseInt(targetColor.replace(/#/g, ''), 16);
270 | const br = bh >> 16;
271 | var bg = (bh >> 8) & 0xff;
272 | var bb = bh & 0xff;
273 | const rr = ar + amount * (br - ar);
274 | const rg = ag + amount * (bg - ag);
275 | const rb = ab + amount * (bb - ab);
276 |
277 | return (
278 | '#' +
279 | (((1 << 24) + (rr << 16) + (rg << 8) + rb) | 0)
280 | .toString(16)
281 | .slice(1)
282 | .toUpperCase()
283 | );
284 | };
285 |
286 | Utils.hexToRgb = function (hex) {
287 | const match = hex.replace(/#/, '').match(/.{1,2}/g);
288 | return {
289 | r: parseInt(match[0], 16),
290 | g: parseInt(match[1], 16),
291 | b: parseInt(match[2], 16)
292 | };
293 | };
294 |
295 | Utils.rgbToHex = (r, g, b) => '#' + [r, g, b].map(x => {
296 | return x.toString(16).padStart(2,0)
297 | }).join('')
298 |
299 |
300 | Utils.nscolorToRgb = function (rP, gP, bP) {
301 | return {
302 | r : Math.round(rP * 255),
303 | g : Math.round(gP * 255),
304 | b : Math.round(bP * 255)
305 | }
306 | };
307 |
308 | Utils.nsColorToHex = function (rP, gP, bP) {
309 | const c = Utils.nscolorToRgb(rP, gP, bP);
310 | return Utils.rgbToHex(c.r, c.g, c.b);
311 | };
312 |
313 | Utils.miredToKelvin = function (mired) {
314 | return Math.round(1e6 / mired);
315 | };
316 |
317 | Utils.kelvinToMired = function (kelvin, roundTo) {
318 | return roundTo ? Utils.roundBy(Math.round(1e6 / kelvin), roundTo) : Math.round(1e6 / kelvin);
319 | };
320 |
321 | Utils.roundBy = function(num, x) {
322 | return Math.round((num - 10) / x) * x;
323 | }
324 |
325 | Utils.getBrightness = function (hexColor) {
326 | // http://www.w3.org/TR/AERT#color-contrast
327 | if (typeof hexColor === 'string' && hexColor.charAt(0) === '#') {
328 | var rgb = Utils.hexToRgb(hexColor);
329 | return (rgb.r * 299 + rgb.g * 587 + rgb.b * 114) / 1000;
330 | }
331 | return 0;
332 | };
333 |
334 | Utils.readJson = function (file, callback) {
335 | var req = new XMLHttpRequest();
336 | req.onerror = function (e) {
337 | // Utils.log(`[Utils][readJson] Error while trying to read ${file}`, e);
338 | };
339 | req.overrideMimeType('application/json');
340 | req.open('GET', file, true);
341 | req.onreadystatechange = function () {
342 | if (req.readyState === 4) {
343 | // && req.status == "200") {
344 | if (callback) callback(req.responseText);
345 | }
346 | };
347 | req.send(null);
348 | };
349 |
350 | Utils.loadScript = function (url, callback) {
351 | const el = document.createElement('script');
352 | el.src = url;
353 | el.onload = function () {
354 | callback(url, true);
355 | };
356 | el.onerror = function () {
357 | console.error('Failed to load file: ' + url);
358 | callback(url, false);
359 | };
360 | document.body.appendChild(el);
361 | };
362 |
363 | Utils.parseJson = function (jsonString) {
364 | if (typeof jsonString === 'object') return jsonString;
365 | try {
366 | const o = JSON.parse(jsonString);
367 |
368 | // Handle non-exception-throwing cases:
369 | // Neither JSON.parse(false) or JSON.parse(1234) throw errors, hence the type-checking,
370 | // but... JSON.parse(null) returns null, and typeof null === "object",
371 | // so we must check for that, too. Thankfully, null is falsey, so this suffices:
372 | if (o && typeof o === 'object') {
373 | return o;
374 | }
375 | } catch (e) {}
376 |
377 | return false;
378 | };
379 |
380 | Utils.parseJSONPromise = function (jsonString) {
381 | // fetch('/my-json-doc-as-string')
382 | // .then(Utils.parseJSONPromise)
383 | // .then(heresYourValidJSON)
384 | // .catch(error - or return default JSON)
385 |
386 | return new Promise((resolve, reject) => {
387 | try {
388 | resolve(JSON.parse(jsonString));
389 | } catch (e) {
390 | reject(e);
391 | }
392 | });
393 | };
394 |
395 | /* eslint-disable import/prefer-default-export */
396 | Utils.getProperty = function (obj, dotSeparatedKeys, defaultValue) {
397 | if (arguments.length > 1 && typeof dotSeparatedKeys !== 'string')
398 | return undefined;
399 | if (typeof obj !== 'undefined' && typeof dotSeparatedKeys === 'string') {
400 | const pathArr = dotSeparatedKeys.split('.');
401 | pathArr.forEach((key, idx, arr) => {
402 | if (typeof key === 'string' && key.includes('[')) {
403 | try {
404 | // extract the array index as string
405 | const pos = /\[([^)]+)\]/.exec(key)[1];
406 | // get the index string length (i.e. '21'.length === 2)
407 | const posLen = pos.length;
408 | arr.splice(idx + 1, 0, Number(pos));
409 |
410 | // keep the key (array name) without the index comprehension:
411 | // (i.e. key without [] (string of length 2)
412 | // and the length of the index (posLen))
413 | arr[idx] = key.slice(0, -2 - posLen); // eslint-disable-line no-param-reassign
414 | } catch (e) {
415 | // do nothing
416 | }
417 | }
418 | });
419 | // eslint-disable-next-line no-param-reassign, no-confusing-arrow
420 | obj = pathArr.reduce(
421 | (o, key) => (o && o[key] !== 'undefined' ? o[key] : undefined),
422 | obj
423 | );
424 | }
425 | return obj === undefined ? defaultValue : obj;
426 | };
427 |
428 | Utils.getProp = (jsn, str, defaultValue = {}, sep = '.') => {
429 | const arr = str.split(sep);
430 | return arr.reduce((obj, key) =>
431 | (obj && obj.hasOwnProperty(key)) ? obj[key] : defaultValue, jsn);
432 | };
433 |
434 | Utils.setProp = function (jsonObj, path, value) {
435 | const names = path.split('.');
436 | let jsn = jsonObj;
437 |
438 | // createNestedObject(jsn, names, values);
439 | // If a value is given, remove the last name and keep it for later:
440 | var targetProperty = arguments.length === 3 ? names.pop() : false;
441 |
442 | // Walk the hierarchy, creating new objects where needed.
443 | // If the lastName was removed, then the last object is not set yet:
444 | for (var i = 0; i < names.length; i++) {
445 | jsn = jsn[names[i]] = jsn[names[i]] || {};
446 | }
447 |
448 | // If a value was given, set it to the target property (the last one):
449 | if (targetProperty) jsn = jsn[targetProperty] = value;
450 |
451 | // Return the last object in the hierarchy:
452 | return jsn;
453 | };
454 |
455 | Utils.getDataUri = function (url, callback, inCanvas, inFillcolor) {
456 | var image = new Image();
457 |
458 | image.onload = function () {
459 | const canvas =
460 | inCanvas && Utils.isCanvas(inCanvas)
461 | ? inCanvas
462 | : document.createElement('canvas');
463 |
464 | canvas.width = this.naturalWidth; // or 'width' if you want a special/scaled size
465 | canvas.height = this.naturalHeight; // or 'height' if you want a special/scaled size
466 |
467 | const ctx = canvas.getContext('2d');
468 | if (inFillcolor) {
469 | ctx.fillStyle = inFillcolor;
470 | ctx.fillRect(0, 0, canvas.width, canvas.height);
471 | }
472 | ctx.drawImage(this, 0, 0);
473 | // Get raw image data
474 | // callback && callback(canvas.toDataURL('image/png').replace(/^data:image\/(png|jpg);base64,/, ''));
475 |
476 | // ... or get as Data URI
477 | callback(canvas.toDataURL('image/png'));
478 | };
479 |
480 | image.src = url;
481 | };
482 |
483 | /** Quick utility to inject a style to the DOM
484 | * e.g. injectStyle('.localbody { background-color: green;}')
485 | */
486 | Utils.injectStyle = function (styles, styleId) {
487 | const node = document.createElement('style');
488 | const tempID = styleId || Utils.randomString(8);
489 | node.setAttribute('id', tempID);
490 | node.innerHTML = styles;
491 | document.body.appendChild(node);
492 | return node;
493 | };
494 |
495 |
496 | Utils.loadImage = function (inUrl, callback, inCanvas, inFillcolor) {
497 | /** Convert to array, so we may load multiple images at once */
498 | const aUrl = !Array.isArray(inUrl) ? [inUrl] : inUrl;
499 | const canvas = inCanvas && inCanvas instanceof HTMLCanvasElement
500 | ? inCanvas
501 | : document.createElement('canvas');
502 | var imgCount = aUrl.length - 1;
503 | const imgCache = {};
504 |
505 | var ctx = canvas.getContext('2d');
506 | ctx.globalCompositeOperation = 'source-over';
507 |
508 | for (let url of aUrl) {
509 | let image = new Image();
510 | let cnt = imgCount;
511 | let w = 144, h = 144;
512 |
513 | image.onload = function () {
514 | imgCache[url] = this;
515 | // look at the size of the first image
516 | if (url === aUrl[0]) {
517 | canvas.width = this.naturalWidth; // or 'width' if you want a special/scaled size
518 | canvas.height = this.naturalHeight; // or 'height' if you want a special/scaled size
519 | }
520 | // if (Object.keys(imgCache).length == aUrl.length) {
521 | if (cnt < 1) {
522 | if (inFillcolor) {
523 | ctx.fillStyle = inFillcolor;
524 | ctx.fillRect(0, 0, canvas.width, canvas.height);
525 | }
526 | // draw in the proper sequence FIFO
527 | aUrl.forEach(e => {
528 | if (!imgCache[e]) {
529 | console.warn(imgCache[e], imgCache);
530 | }
531 |
532 | if (imgCache[e]) {
533 | ctx.drawImage(imgCache[e], 0, 0);
534 | ctx.save();
535 | }
536 | });
537 |
538 | callback(canvas.toDataURL('image/png'));
539 | // or to get raw image data
540 | // callback && callback(canvas.toDataURL('image/png').replace(/^data:image\/(png|jpg);base64,/, ''));
541 | }
542 | };
543 |
544 | imgCount--;
545 | image.src = url;
546 | }
547 | };
548 |
549 | Utils.getData = function (url) {
550 | // Return a new promise.
551 | return new Promise(function (resolve, reject) {
552 | // Do the usual XHR stuff
553 | var req = new XMLHttpRequest();
554 | // Make sure to call .open asynchronously
555 | req.open('GET', url, true);
556 |
557 | req.onload = function () {
558 | // This is called even on 404 etc
559 | // so check the status
560 | if (req.status === 200) {
561 | // Resolve the promise with the response text
562 | resolve(req.response);
563 | } else {
564 | // Otherwise reject with the status text
565 | // which will hopefully be a meaningful error
566 | reject(Error(req.statusText));
567 | }
568 | };
569 |
570 | // Handle network errors
571 | req.onerror = function () {
572 | reject(Error('Network Error'));
573 | };
574 |
575 | // Make the request
576 | req.send();
577 | });
578 | };
579 |
580 | Utils.negArray = function (arr) {
581 | /** http://h3manth.com/new/blog/2013/negative-array-index-in-javascript/ */
582 | return Proxy.create({
583 | set: function (proxy, index, value) {
584 | index = parseInt(index);
585 | return index < 0 ? (arr[arr.length + index] = value) : (arr[index] = value);
586 | },
587 | get: function (proxy, index) {
588 | index = parseInt(index);
589 | return index < 0 ? arr[arr.length + index] : arr[index];
590 | }
591 | });
592 | };
593 |
594 | Utils.onChange = function (object, callback) {
595 | /** https://github.com/sindresorhus/on-change */
596 | 'use strict';
597 | const handler = {
598 | get (target, property, receiver) {
599 | try {
600 | console.log('get via Proxy: ', property, target, receiver);
601 | return new Proxy(target[property], handler);
602 | } catch (err) {
603 | console.log('get via Reflect: ', err, property, target, receiver);
604 | return Reflect.get(target, property, receiver);
605 | }
606 | },
607 | set (target, property, value, receiver) {
608 | console.log('Utils.onChange:set1:', target, property, value, receiver);
609 | // target[property] = value;
610 | const b = Reflect.set(target, property, value);
611 | console.log('Utils.onChange:set2:', target, property, value, receiver);
612 | return b;
613 | },
614 | defineProperty (target, property, descriptor) {
615 | console.log('Utils.onChange:defineProperty:', target, property, descriptor);
616 | callback(target, property, descriptor);
617 | return Reflect.defineProperty(target, property, descriptor);
618 | },
619 | deleteProperty (target, property) {
620 | console.log('Utils.onChange:deleteProperty:', target, property);
621 | callback(target, property);
622 | return Reflect.deleteProperty(target, property);
623 | }
624 | };
625 |
626 | return new Proxy(object, handler);
627 | };
628 |
629 | Utils.observeArray = function (object, callback) {
630 | 'use strict';
631 | const array = [];
632 | const handler = {
633 | get (target, property, receiver) {
634 | try {
635 | return new Proxy(target[property], handler);
636 | } catch (err) {
637 | return Reflect.get(target, property, receiver);
638 | }
639 | },
640 | set (target, property, value, receiver) {
641 | console.log('XXXUtils.observeArray:set1:', target, property, value, array);
642 | target[property] = value;
643 | console.log('XXXUtils.observeArray:set2:', target, property, value, array);
644 | },
645 | defineProperty (target, property, descriptor) {
646 | callback(target, property, descriptor);
647 | return Reflect.defineProperty(target, property, descriptor);
648 | },
649 | deleteProperty (target, property) {
650 | callback(target, property, descriptor);
651 | return Reflect.deleteProperty(target, property);
652 | }
653 | };
654 |
655 | return new Proxy(object, handler);
656 | };
657 |
658 | window['_'] = Utils;
659 |
660 | /*
661 | * connectElgatoStreamDeckSocket
662 | * This is the first function StreamDeck Software calls, when
663 | * establishing the connection to the plugin or the Property Inspector
664 | * @param {string} inPort - The socket's port to communicate with StreamDeck software.
665 | * @param {string} inUUID - A unique identifier, which StreamDeck uses to communicate with the plugin
666 | * @param {string} inMessageType - Identifies, if the event is meant for the property inspector or the plugin.
667 | * @param {string} inApplicationInfo - Information about the host (StreamDeck) application
668 | * @param {string} inActionInfo - Context is an internal identifier used to communicate to the host application.
669 | */
670 |
671 |
672 | // eslint-disable-next-line no-unused-vars
673 | function connectElgatoStreamDeckSocket (
674 | inPort,
675 | inUUID,
676 | inMessageType,
677 | inApplicationInfo,
678 | inActionInfo
679 | ) {
680 | StreamDeck.getInstance().connect(arguments);
681 | window.$SD.api = Object.assign({ send: SDApi.send }, SDApi.common, SDApi[inMessageType]);
682 | }
683 |
684 | /* legacy support */
685 |
686 | function connectSocket (
687 | inPort,
688 | inUUID,
689 | inMessageType,
690 | inApplicationInfo,
691 | inActionInfo
692 | ) {
693 | connectElgatoStreamDeckSocket(
694 | inPort,
695 | inUUID,
696 | inMessageType,
697 | inApplicationInfo,
698 | inActionInfo
699 | );
700 | }
701 |
702 | /**
703 | * StreamDeck object containing all required code to establish
704 | * communication with SD-Software and the Property Inspector
705 | */
706 |
707 | const StreamDeck = (function () {
708 | // Hello it's me
709 | var instance;
710 | /*
711 | Populate and initialize internally used properties
712 | */
713 |
714 | function init () {
715 | // *** PRIVATE ***
716 |
717 | var inPort,
718 | inUUID,
719 | inMessageType,
720 | inApplicationInfo,
721 | inActionInfo,
722 | websocket = null;
723 |
724 | var events = ELGEvents.eventEmitter();
725 | var logger = SDDebug.logger();
726 |
727 | function showVars () {
728 | debugLog('---- showVars');
729 | debugLog('- port', inPort);
730 | debugLog('- uuid', inUUID);
731 | debugLog('- messagetype', inMessageType);
732 | debugLog('- info', inApplicationInfo);
733 | debugLog('- inActionInfo', inActionInfo);
734 | debugLog('----< showVars');
735 | }
736 |
737 | function connect (args) {
738 | inPort = args[0];
739 | inUUID = args[1];
740 | inMessageType = args[2];
741 | inApplicationInfo = Utils.parseJson(args[3]);
742 | inActionInfo = args[4] !== 'undefined' ? Utils.parseJson(args[4]) : args[4];
743 |
744 | /** Debug variables */
745 | if (debug) {
746 | showVars();
747 | }
748 |
749 | const lang = Utils.getProp(inApplicationInfo,'application.language', false);
750 | if (lang) {
751 | loadLocalization(lang, inMessageType === 'registerPropertyInspector' ? '../' : './', function() {
752 | events.emit('localizationLoaded', {language:lang});
753 | });
754 | };
755 |
756 | /** restrict the API to what's possible
757 | * within Plugin or Property Inspector
758 | *
759 | */
760 | // $SD.api = SDApi[inMessageType];
761 |
762 | if (websocket) {
763 | websocket.close();
764 | websocket = null;
765 | };
766 |
767 | websocket = new WebSocket('ws://127.0.0.1:' + inPort);
768 |
769 | websocket.onopen = function () {
770 | var json = {
771 | event: inMessageType,
772 | uuid: inUUID
773 | };
774 |
775 | // console.log('***************', inMessageType + " websocket:onopen", inUUID, json);
776 |
777 | websocket.sendJSON(json);
778 | $SD.uuid = inUUID;
779 | $SD.actionInfo = inActionInfo;
780 | $SD.applicationInfo = inApplicationInfo;
781 | $SD.messageType = inMessageType;
782 | $SD.connection = websocket;
783 |
784 | instance.emit('connected', {
785 | connection: websocket,
786 | port: inPort,
787 | uuid: inUUID,
788 | actionInfo: inActionInfo,
789 | applicationInfo: inApplicationInfo,
790 | messageType: inMessageType
791 | });
792 | };
793 |
794 | websocket.onerror = function (evt) {
795 | console.warn('WEBOCKET ERROR', evt, evt.data);
796 | };
797 |
798 | websocket.onclose = function (evt) {
799 | // Websocket is closed
800 | var reason = WEBSOCKETERROR(evt);
801 | console.warn(
802 | '[STREAMDECK]***** WEBOCKET CLOSED **** reason:',
803 | reason
804 | );
805 | };
806 |
807 | websocket.onmessage = function (evt) {
808 | var jsonObj = Utils.parseJson(evt.data),
809 | m;
810 |
811 | //console.log('[STREAMDECK] websocket.onmessage ... ', jsonObj.event, jsonObj);
812 |
813 | if (!jsonObj.hasOwnProperty('action')) {
814 | m = jsonObj.event;
815 | //console.log('%c%s', 'color: white; background: red; font-size: 12px;', '[common.js]onmessage:', m);
816 | } else {
817 | switch (inMessageType) {
818 | case 'registerPlugin':
819 | m = jsonObj['action'] + '.' + jsonObj['event'];
820 | break;
821 | case 'registerPropertyInspector':
822 | m = 'sendToPropertyInspector';
823 | break;
824 | default:
825 | console.log('%c%s', 'color: white; background: red; font-size: 12px;', '[STREAMDECK] websocket.onmessage +++++++++ PROBLEM ++++++++');
826 | console.warn('UNREGISTERED MESSAGETYPE:', inMessageType);
827 | }
828 | }
829 |
830 | if (m && m !== '')
831 | events.emit(m, jsonObj);
832 | };
833 |
834 | instance.connection = websocket;
835 | }
836 |
837 | return {
838 | // *** PUBLIC ***
839 |
840 | uuid: inUUID,
841 | on: events.on,
842 | emit: events.emit,
843 | connection: websocket,
844 | connect: connect,
845 | api: null,
846 | logger: logger
847 | };
848 | }
849 |
850 | return {
851 | getInstance: function () {
852 | if (!instance) {
853 | instance = init();
854 | }
855 | return instance;
856 | }
857 | };
858 | })();
859 |
860 | // eslint-disable-next-line no-unused-vars
861 | function initializeControlCenterClient () {
862 | const settings = Object.assign(REMOTESETTINGS || {}, { debug: false });
863 | var $CC = new ControlCenterClient(settings);
864 | window['$CC'] = $CC;
865 | return $CC;
866 | }
867 |
868 | /** ELGEvents
869 | * Publish/Subscribe pattern to quickly signal events to
870 | * the plugin, property inspector and data.
871 | */
872 |
873 | const ELGEvents = {
874 | eventEmitter: function (name, fn) {
875 | const eventList = new Map();
876 |
877 | const on = (name, fn) => {
878 | if (!eventList.has(name)) eventList.set(name, ELGEvents.pubSub());
879 |
880 | return eventList.get(name).sub(fn);
881 | };
882 |
883 | const has = (name) =>
884 | eventList.has(name);
885 |
886 | const emit = (name, data) =>
887 | eventList.has(name) && eventList.get(name).pub(data);
888 |
889 | return Object.freeze({ on, has, emit, eventList });
890 | },
891 |
892 | pubSub: function pubSub () {
893 | const subscribers = new Set();
894 |
895 | const sub = fn => {
896 | subscribers.add(fn);
897 | return () => {
898 | subscribers.delete(fn);
899 | };
900 | };
901 |
902 | const pub = data => subscribers.forEach(fn => fn(data));
903 | return Object.freeze({ pub, sub });
904 | }
905 | };
906 |
907 | /** SDApi
908 | * This ist the main API to communicate between plugin, property inspector and
909 | * application host.
910 | * Internal functions:
911 | * - setContext: sets the context of the current plugin
912 | * - exec: prepare the correct JSON structure and send
913 | *
914 | * Methods exposed in the $SD.api alias
915 | * Messages send from the plugin
916 | * -----------------------------
917 | * - showAlert
918 | * - showOK
919 | * - setSettings
920 | * - setTitle
921 | * - setImage
922 | * - sendToPropertyInspector
923 | *
924 | * Messages send from Property Inspector
925 | * -------------------------------------
926 | * - sendToPlugin
927 | *
928 | * Messages received in the plugin
929 | * -------------------------------
930 | * willAppear
931 | * willDisappear
932 | * keyDown
933 | * keyUp
934 | */
935 |
936 | const SDApi = {
937 | send: function (context, fn, payload, debug) {
938 | /** Combine the passed JSON with the name of the event and it's context
939 | * If the payload contains 'event' or 'context' keys, it will overwrite existing 'event' or 'context'.
940 | * This function is non-mutating and thereby creates a new object containing
941 | * all keys of the original JSON objects.
942 | */
943 | const pl = Object.assign({}, { event: fn, context: context }, payload);
944 |
945 | /** Check, if we have a connection, and if, send the JSON payload */
946 | if (debug) {
947 | console.log('-----SDApi.send-----');
948 | console.log('context', context);
949 | console.log(pl);
950 | console.log(payload.payload);
951 | console.log(JSON.stringify(payload.payload));
952 | console.log('-------');
953 | }
954 | $SD.connection && $SD.connection.sendJSON(pl);
955 |
956 | /**
957 | * DEBUG-Utility to quickly show the current payload in the Property Inspector.
958 | */
959 |
960 | if (
961 | $SD.connection &&
962 | [
963 | 'sendToPropertyInspector',
964 | 'showOK',
965 | 'showAlert',
966 | 'setSettings'
967 | ].indexOf(fn) === -1
968 | ) {
969 | // console.log("send.sendToPropertyInspector", payload);
970 | // this.sendToPropertyInspector(context, typeof payload.payload==='object' ? JSON.stringify(payload.payload) : JSON.stringify({'payload':payload.payload}), pl['action']);
971 | }
972 | },
973 |
974 | registerPlugin: {
975 |
976 | /** Messages send from the plugin */
977 | showAlert: function (context) {
978 | SDApi.send(context, 'showAlert', {});
979 | },
980 |
981 | showOk: function (context) {
982 | SDApi.send(context, 'showOk', {});
983 | },
984 |
985 |
986 | setState: function (context, payload) {
987 | SDApi.send(context, 'setState', {
988 | payload: {
989 | 'state': 1 - Number(payload === 0)
990 | }
991 | });
992 | },
993 |
994 | setTitle: function (context, title, target) {
995 | SDApi.send(context, 'setTitle', {
996 | payload: {
997 | title: '' + title || '',
998 | target: target || DestinationEnum.HARDWARE_AND_SOFTWARE
999 | }
1000 | });
1001 | },
1002 |
1003 | setImage: function (context, img, target) {
1004 | SDApi.send(context, 'setImage', {
1005 | payload: {
1006 | image: img || '',
1007 | target: target || DestinationEnum.HARDWARE_AND_SOFTWARE
1008 | }
1009 | });
1010 | },
1011 |
1012 | sendToPropertyInspector: function (context, payload, action) {
1013 | SDApi.send(context, 'sendToPropertyInspector', {
1014 | action: action,
1015 | payload: payload
1016 | });
1017 | },
1018 |
1019 | showUrl2: function (context, urlToOpen) {
1020 | SDApi.send(context, 'openUrl', {
1021 | payload: {
1022 | url: urlToOpen
1023 | }
1024 | });
1025 | }
1026 | },
1027 |
1028 | /** Messages send from Property Inspector */
1029 |
1030 | registerPropertyInspector: {
1031 |
1032 | sendToPlugin: function (piUUID, action, payload) {
1033 | SDApi.send(
1034 | piUUID,
1035 | 'sendToPlugin',
1036 | {
1037 | action: action,
1038 | payload: payload || {}
1039 | },
1040 | false
1041 | );
1042 | }
1043 | },
1044 |
1045 | /** COMMON */
1046 |
1047 | common: {
1048 |
1049 | getSettings: function (context, payload) {
1050 | SDApi.send(context, 'getSettings', {});
1051 | },
1052 |
1053 | setSettings: function (context, payload) {
1054 | SDApi.send(context, 'setSettings', {
1055 | payload: payload
1056 | });
1057 | },
1058 |
1059 | getGlobalSettings: function (context, payload) {
1060 | SDApi.send(context, 'getGlobalSettings', {});
1061 | },
1062 |
1063 | setGlobalSettings: function (context, payload) {
1064 | SDApi.send(context, 'setGlobalSettings', {
1065 | payload: payload
1066 | });
1067 | },
1068 |
1069 | logMessage: function (context, payload) {
1070 | SDApi.send(context, 'logMessage', {
1071 | payload: payload
1072 | });
1073 | },
1074 |
1075 | openUrl: function (context, urlToOpen) {
1076 | SDApi.send(context, 'openUrl', {
1077 | payload: {
1078 | url: urlToOpen
1079 | }
1080 | });
1081 | },
1082 |
1083 | test: function () {
1084 | console.log(this);
1085 | console.log(SDApi);
1086 | },
1087 |
1088 | debugPrint: function (context, inString) {
1089 | // console.log("------------ DEBUGPRINT");
1090 | // console.log([].slice.apply(arguments).join());
1091 | // console.log("------------ DEBUGPRINT");
1092 | SDApi.send(context, 'debugPrint', {
1093 | payload: [].slice.apply(arguments).join('.') || ''
1094 | });
1095 | },
1096 |
1097 | dbgSend: function (fn, context) {
1098 | /** lookup if an appropriate function exists */
1099 | if ($SD.connection && this[fn] && typeof this[fn] === 'function') {
1100 | /** verify if type of payload is an object/json */
1101 | const payload = this[fn]();
1102 | if (typeof payload === 'object') {
1103 | Object.assign({ event: fn, context: context }, payload);
1104 | $SD.connection && $SD.connection.sendJSON(payload);
1105 | }
1106 | }
1107 | console.log(this, fn, typeof this[fn], this[fn]());
1108 | }
1109 |
1110 | }
1111 | };
1112 |
1113 | /** SDDebug
1114 | * Utility to log the JSON structure of an incoming object
1115 | */
1116 |
1117 | const SDDebug = {
1118 | logger: function (name, fn) {
1119 | const logEvent = jsn => {
1120 | console.log('____SDDebug.logger.logEvent');
1121 | console.log(jsn);
1122 | debugLog('-->> Received Obj:', jsn);
1123 | debugLog('jsonObj', jsn);
1124 | debugLog('event', jsn['event']);
1125 | debugLog('actionType', jsn['actionType']);
1126 | debugLog('settings', jsn['settings']);
1127 | debugLog('coordinates', jsn['coordinates']);
1128 | debugLog('---');
1129 | };
1130 |
1131 | const logSomething = jsn =>
1132 | console.log('____SDDebug.logger.logSomething');
1133 |
1134 | return { logEvent, logSomething };
1135 | }
1136 | };
1137 |
1138 | /**
1139 | * This is the instance of the StreamDeck object.
1140 | * There's only one StreamDeck object, which carries
1141 | * connection parameters and handles communication
1142 | * to/from the software's PluginManager.
1143 | */
1144 |
1145 | window.$SD = StreamDeck.getInstance();
1146 | window.$SD.api = SDApi;
1147 |
1148 | function WEBSOCKETERROR (evt) {
1149 | // Websocket is closed
1150 | var reason = '';
1151 | if (evt.code === 1000) {
1152 | reason = 'Normal Closure. The purpose for which the connection was established has been fulfilled.';
1153 | } else if (evt.code === 1001) {
1154 | reason = 'Going Away. An endpoint is "going away", such as a server going down or a browser having navigated away from a page.';
1155 | } else if (evt.code === 1002) {
1156 | reason = 'Protocol error. An endpoint is terminating the connection due to a protocol error';
1157 | } else if (evt.code === 1003) {
1158 | reason = "Unsupported Data. An endpoint received a type of data it doesn't support.";
1159 | } else if (evt.code === 1004) {
1160 | reason = '--Reserved--. The specific meaning might be defined in the future.';
1161 | } else if (evt.code === 1005) {
1162 | reason = 'No Status. No status code was actually present.';
1163 | } else if (evt.code === 1006) {
1164 | reason = 'Abnormal Closure. The connection was closed abnormally, e.g., without sending or receiving a Close control frame';
1165 | } else if (evt.code === 1007) {
1166 | reason = 'Invalid frame payload data. The connection was closed, because the received data was not consistent with the type of the message (e.g., non-UTF-8 [http://tools.ietf.org/html/rfc3629]).';
1167 | } else if (evt.code === 1008) {
1168 | reason = 'Policy Violation. The connection was closed, because current message data "violates its policy". This reason is given either if there is no other suitable reason, or if there is a need to hide specific details about the policy.';
1169 | } else if (evt.code === 1009) {
1170 | reason = 'Message Too Big. Connection closed because the message is too big for it to process.';
1171 | } else if (evt.code === 1010) { // Note that this status code is not used by the server, because it can fail the WebSocket handshake instead.
1172 | reason = "Mandatory Ext. Connection is terminated the connection because the server didn't negotiate one or more extensions in the WebSocket handshake. Mandatory extensions were: " + evt.reason;
1173 | } else if (evt.code === 1011) {
1174 | reason = 'Internl Server Error. Connection closed because it encountered an unexpected condition that prevented it from fulfilling the request.';
1175 | } else if (evt.code === 1015) {
1176 | reason = "TLS Handshake. The connection was closed due to a failure to perform a TLS handshake (e.g., the server certificate can't be verified).";
1177 | } else {
1178 | reason = 'Unknown reason';
1179 | }
1180 |
1181 | return reason;
1182 | }
1183 |
1184 | const SOCKETERRORS = {
1185 | '0': 'The connection has not yet been established',
1186 | '1': 'The connection is established and communication is possible',
1187 | '2': 'The connection is going through the closing handshake',
1188 | '3': 'The connection has been closed or could not be opened'
1189 | };
1190 |
1191 |
--------------------------------------------------------------------------------
/com.github.sbelectronics.streamdeck.http.sdPlugin/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "Actions": [
3 | {
4 | "Icon": "icon",
5 | "Name": "http",
6 | "States": [
7 | {
8 | "Image": "defaultImage",
9 | "TitleAlignment": "middle",
10 | "FontSize": "17"
11 | }
12 | ],
13 | "SupportedInMultiActions": false,
14 | "Tooltip": "http",
15 | "UUID": "com.github.sbelectronics.streamdeck.http"
16 | }
17 | ],
18 | "SDKVersion": 2,
19 | "Author": "Scott M Baker",
20 | "CodePathWin": "http.exe",
21 | "PropertyInspectorPath": "index_pi.html",
22 | "Description": "simple http request",
23 | "Name": "http",
24 | "Icon": "pluginIcon",
25 | "URL": "https://www.github.com/sbelectronics/streamdeck",
26 | "Version": "1.0",
27 | "OS": [
28 | {
29 | "Platform": "windows",
30 | "MinimumVersion" : "10"
31 | }
32 | ],
33 | "Software":
34 | {
35 | "MinimumVersion" : "4.1"
36 | }
37 | }
--------------------------------------------------------------------------------
/com.github.sbelectronics.streamdeck.http.sdPlugin/pluginIcon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sbelectronics/streamdeck/f0175932e1283d59d50e7410ead230b293fa39cc/com.github.sbelectronics.streamdeck.http.sdPlugin/pluginIcon.png
--------------------------------------------------------------------------------
/com.github.sbelectronics.streamdeck.http.sdPlugin/sdpi.css:
--------------------------------------------------------------------------------
1 | :root {
2 | --sdpi-bgcolor: #2D2D2D;
3 | --sdpi-background: #3D3D3D;
4 | --sdpi-color: #d8d8d8;
5 | --sdpi-bordercolor: #3a3a3a;
6 | --sdpi-buttonbordercolor: #969696;
7 | --sdpi-borderradius: 0px;
8 | --sdpi-width: 224px;
9 | --sdpi-fontweight: 600;
10 | --sdpi-letterspacing: -0.25pt;
11 | }
12 |
13 | html {
14 | --sdpi-bgcolor: #2D2D2D;
15 | --sdpi-background: #3D3D3D;
16 | --sdpi-color: #d8d8d8;
17 | --sdpi-bordercolor: #3a3a3a;
18 | --sdpi-buttonbordercolor: #969696;
19 | --sdpi-borderradius: 0px;
20 | --sdpi-width: 224px;
21 | --sdpi-fontweight: 600;
22 | --sdpi-letterspacing: -0.25pt;
23 | height: 100%;
24 | width: 100%;
25 | overflow: hidden;
26 | touch-action:none;
27 | }
28 |
29 | html, body {
30 | --sdpi-bgcolor: #2D2D2D;
31 | --sdpi-background: #3D3D3D;
32 | --sdpi-color: #d8d8d8;
33 | font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
34 | font-size: 9pt;
35 | background-color: var(--sdpi-bgcolor);
36 | color: #9a9a9a;
37 | }
38 |
39 | body {
40 | height: 100%;
41 | padding: 0;
42 | overflow-x: hidden;
43 | overflow-y: auto;
44 | margin: 0;
45 | -webkit-overflow-scrolling: touch;
46 | -webkit-text-size-adjust: 100%;
47 | -webkit-font-smoothing: antialiased;
48 | }
49 |
50 | mark {
51 | background-color: var(--sdpi-bgcolor);
52 | color: var(--sdpi-color);
53 | }
54 |
55 | hr, hr2 {
56 | -webkit-margin-before: 1em;
57 | -webkit-margin-after: 1em;
58 | border-style: none;
59 | background: var(--sdpi-background);
60 | height: 1px;
61 | }
62 |
63 | hr2,
64 | .sdpi-heading {
65 | display: flex;
66 | flex-basis: 100%;
67 | align-items: center;
68 | color: inherit;
69 | font-size: 9pt;
70 | margin: 8px 0px;
71 | }
72 |
73 | .sdpi-heading::before,
74 | .sdpi-heading::after {
75 | content: "";
76 | flex-grow: 1;
77 | background: var(--sdpi-background);
78 | height: 1px;
79 | font-size: 0px;
80 | line-height: 0px;
81 | margin: 0px 16px;
82 | }
83 |
84 | hr2 {
85 | height: 2px;
86 | }
87 |
88 | hr, hr2 {
89 | margin-left:16px;
90 | margin-right:16px;
91 | }
92 |
93 | .sdpi-item-value,
94 | option,
95 | input,
96 | select,
97 | button {
98 | font-size: 10pt;
99 | font-weight: var(--sdpi-fontweight);
100 | letter-spacing: var(--sdpi-letterspacing);
101 | }
102 |
103 |
104 |
105 | .win .sdpi-item-value,
106 | .win option,
107 | .win input,
108 | .win select,
109 | .win button {
110 | font-size: 11px;
111 | font-style: normal;
112 | letter-spacing: inherit;
113 | font-weight: 100;
114 | }
115 |
116 | .win button {
117 | font-size: 12px;
118 | }
119 |
120 | ::-webkit-progress-value,
121 | meter::-webkit-meter-optimum-value {
122 | border-radius: 2px;
123 | /* background: linear-gradient(#ccf, #99f 20%, #77f 45%, #77f 55%, #cdf); */
124 | }
125 |
126 | ::-webkit-progress-bar,
127 | meter::-webkit-meter-bar {
128 | border-radius: 3px;
129 | background: var(--sdpi-background);
130 | }
131 |
132 | ::-webkit-progress-bar:active,
133 | meter::-webkit-meter-bar:active {
134 | border-radius: 3px;
135 | background: #222222;
136 | }
137 | ::-webkit-progress-value:active,
138 | meter::-webkit-meter-optimum-value:active {
139 | background: #99f;
140 | }
141 |
142 | progress,
143 | progress.sdpi-item-value {
144 | min-height: 5px !important;
145 | height: 5px;
146 | background-color: #303030;
147 | }
148 |
149 | progress {
150 | margin-top: 8px !important;
151 | margin-bottom: 8px !important;
152 | }
153 |
154 | .full progress,
155 | progress.full {
156 | margin-top: 3px !important;
157 | }
158 |
159 | ::-webkit-progress-inner-element {
160 | background-color: transparent;
161 | }
162 |
163 |
164 | .sdpi-item[type="progress"] {
165 | margin-top: 4px !important;
166 | margin-bottom: 12px;
167 | min-height: 15px;
168 | }
169 |
170 | .sdpi-item-child.full:last-child {
171 | margin-bottom: 4px;
172 | }
173 |
174 | .tabs {
175 | /**
176 | * Setting display to flex makes this container lay
177 | * out its children using flexbox, the exact same
178 | * as in the above "Stepper input" example.
179 | */
180 | display: flex;
181 |
182 | border-bottom: 1px solid #D7DBDD;
183 | }
184 |
185 | .tab {
186 | cursor: pointer;
187 | padding: 5px 30px;
188 | color: #16a2d7;
189 | font-size: 9pt;
190 | border-bottom: 2px solid transparent;
191 | }
192 |
193 | .tab.is-tab-selected {
194 | border-bottom-color: #4ebbe4;
195 | }
196 |
197 | select {
198 | -webkit-appearance: none;
199 | -moz-appearance: none;
200 | -o-appearance: none;
201 | appearance: none;
202 | background: url(caret.svg) no-repeat 97% center;
203 | }
204 |
205 | label.sdpi-file-label,
206 | input[type="button"],
207 | input[type="submit"],
208 | input[type="reset"],
209 | input[type="file"],
210 | input[type=file]::-webkit-file-upload-button,
211 | button,
212 | select {
213 | color: var(--sdpi-color);
214 | border: 1pt solid #303030;
215 | font-size: 8pt;
216 | background-color: var(--sdpi-background);
217 | border-radius: var(--sdpi-borderradius);
218 | }
219 |
220 | label.sdpi-file-label,
221 | input[type="button"],
222 | input[type="submit"],
223 | input[type="reset"],
224 | input[type="file"],
225 | input[type=file]::-webkit-file-upload-button,
226 | button {
227 | border: 1pt solid var(--sdpi-buttonbordercolor);
228 | border-radius: var(--sdpi-borderradius);
229 | border-color: var(--sdpi-buttonbordercolor);
230 | min-height: 23px !important;
231 | height: 23px !important;
232 | margin-right: 8px;
233 | }
234 |
235 | input[type=number]::-webkit-inner-spin-button,
236 | input[type=number]::-webkit-outer-spin-button {
237 | -webkit-appearance: none;
238 | margin: 0;
239 | }
240 |
241 | input[type="file"] {
242 | border-radius: var(--sdpi-borderradius);
243 | max-width: 220px;
244 | }
245 |
246 | option {
247 | height: 1.5em;
248 | padding: 4px;
249 | }
250 |
251 | /* SDPI */
252 |
253 | .sdpi-wrapper {
254 | overflow-x: hidden;
255 | height: 100%;
256 | }
257 |
258 | .sdpi-item {
259 | display: flex;
260 | flex-direction: row;
261 | min-height: 32px;
262 | align-items: center;
263 | margin-top: 2px;
264 | max-width: 344px;
265 | -webkit-user-drag: none;
266 | }
267 |
268 | .sdpi-item:first-child {
269 | margin-top:-1px;
270 | }
271 |
272 | .sdpi-item:last-child {
273 | margin-bottom: 0px;
274 | }
275 |
276 | .sdpi-item > *:not(.sdpi-item-label):not(meter):not(details):not(canvas) {
277 | min-height: 26px;
278 | padding: 0px 4px 0px 4px;
279 | }
280 |
281 | .sdpi-item > *:not(.sdpi-item-label.empty):not(meter) {
282 | min-height: 26px;
283 | padding: 0px 4px 0px 4px;
284 | }
285 |
286 |
287 | .sdpi-item-group {
288 | padding: 0 !important;
289 | }
290 |
291 | meter.sdpi-item-value {
292 | margin-left: 6px;
293 | }
294 |
295 | .sdpi-item[type="group"] {
296 | display: block;
297 | margin-top: 12px;
298 | margin-bottom: 12px;
299 | /* border: 1px solid white; */
300 | flex-direction: unset;
301 | text-align: left;
302 | }
303 |
304 | .sdpi-item[type="group"] > .sdpi-item-label,
305 | .sdpi-item[type="group"].sdpi-item-label {
306 | width: 96%;
307 | text-align: left;
308 | font-weight: 700;
309 | margin-bottom: 4px;
310 | padding-left: 4px;
311 | }
312 |
313 | dl,
314 | ul,
315 | ol {
316 | -webkit-margin-before: 0px;
317 | -webkit-margin-after: 4px;
318 | -webkit-padding-start: 1em;
319 | max-height: 90px;
320 | overflow-y: scroll;
321 | cursor: pointer;
322 | user-select: none;
323 | }
324 |
325 | table.sdpi-item-value,
326 | dl.sdpi-item-value,
327 | ul.sdpi-item-value,
328 | ol.sdpi-item-value {
329 | -webkit-margin-before: 4px;
330 | -webkit-margin-after: 8px;
331 | -webkit-padding-start: 1em;
332 | width: var(--sdpi-width);
333 | text-align: center;
334 | }
335 |
336 | table > caption {
337 | margin: 2px;
338 | }
339 |
340 | .list,
341 | .sdpi-item[type="list"] {
342 | align-items: baseline;
343 | }
344 |
345 | .sdpi-item-label {
346 | text-align: right;
347 | flex: none;
348 | width: 94px;
349 | padding-right: 4px;
350 | font-weight: 600;
351 | -webkit-user-select: none;
352 | }
353 |
354 | .win .sdpi-item-label,
355 | .sdpi-item-label > small{
356 | font-weight: normal;
357 | }
358 |
359 | .sdpi-item-label:after {
360 | content: ": ";
361 | }
362 |
363 | .sdpi-item-label.empty:after {
364 | content: "";
365 | }
366 |
367 | .sdpi-test,
368 | .sdpi-item-value {
369 | flex: 1 0 0;
370 | /* flex-grow: 1;
371 | flex-shrink: 0; */
372 | margin-right: 14px;
373 | margin-left: 4px;
374 | justify-content: space-evenly;
375 | }
376 |
377 | canvas.sdpi-item-value {
378 | max-width: 144px;
379 | max-height: 144px;
380 | width: 144px;
381 | height: 144px;
382 | margin: 0 auto;
383 | cursor: pointer;
384 | }
385 |
386 | input.sdpi-item-value {
387 | margin-left: 5px;
388 | }
389 |
390 | .sdpi-item-value button,
391 | button.sdpi-item-value {
392 | margin-left: 6px;
393 | margin-right: 14px;
394 | }
395 |
396 | .sdpi-item-value.range {
397 | margin-left: 0px;
398 | }
399 |
400 | table,
401 | dl.sdpi-item-value,
402 | ul.sdpi-item-value,
403 | ol.sdpi-item-value,
404 | .sdpi-item-value > dl,
405 | .sdpi-item-value > ul,
406 | .sdpi-item-value > ol
407 | {
408 | list-style-type: none;
409 | list-style-position: outside;
410 | margin-left: -4px;
411 | margin-right: -4px;
412 | padding: 4px;
413 | border: 1px solid var(--sdpi-bordercolor);
414 | }
415 |
416 | dl.sdpi-item-value,
417 | ul.sdpi-item-value,
418 | ol.sdpi-item-value,
419 | .sdpi-item-value > ol {
420 | list-style-type: none;
421 | list-style-position: inside;
422 | margin-left: 5px;
423 | margin-right: 12px;
424 | padding: 4px !important;
425 | display: flex;
426 | flex-direction: column;
427 | }
428 |
429 | .two-items li {
430 | display: flex;
431 | }
432 | .two-items li > *:first-child {
433 | flex: 0 0 50%;
434 | text-align: left;
435 | }
436 | .two-items.thirtyseventy li > *:first-child {
437 | flex: 0 0 30%;
438 | }
439 |
440 | ol.sdpi-item-value,
441 | .sdpi-item-value > ol[listtype="none"] {
442 | list-style-type: none;
443 | }
444 | ol.sdpi-item-value[type="decimal"],
445 | .sdpi-item-value > ol[type="decimal"] {
446 | list-style-type: decimal;
447 | }
448 |
449 | ol.sdpi-item-value[type="decimal-leading-zero"],
450 | .sdpi-item-value > ol[type="decimal-leading-zero"] {
451 | list-style-type: decimal-leading-zero;
452 | }
453 |
454 | ol.sdpi-item-value[type="lower-alpha"],
455 | .sdpi-item-value > ol[type="lower-alpha"] {
456 | list-style-type: lower-alpha;
457 | }
458 |
459 | ol.sdpi-item-value[type="upper-alpha"],
460 | .sdpi-item-value > ol[type="upper-alpha"] {
461 | list-style-type: upper-alpha;
462 | }
463 |
464 | ol.sdpi-item-value[type="upper-roman"],
465 | .sdpi-item-value > ol[type="upper-roman"] {
466 | list-style-type: upper-roman;
467 | }
468 |
469 | ol.sdpi-item-value[type="lower-roman"],
470 | .sdpi-item-value > ol[type="lower-roman"] {
471 | list-style-type: upper-roman;
472 | }
473 |
474 | tr:nth-child(even),
475 | .sdpi-item-value > ul > li:nth-child(even),
476 | .sdpi-item-value > ol > li:nth-child(even),
477 | li:nth-child(even) {
478 | background-color: rgba(0,0,0,.2)
479 | }
480 |
481 | td:hover,
482 | .sdpi-item-value > ul > li:hover:nth-child(even),
483 | .sdpi-item-value > ol > li:hover:nth-child(even),
484 | li:hover:nth-child(even),
485 | li:hover {
486 | background-color: rgba(255,255,255,.1);
487 | }
488 |
489 | td.selected,
490 | td.selected:hover,
491 | li.selected:hover,
492 | li.selected {
493 | color: white;
494 | background-color: #77f;
495 | }
496 |
497 | tr {
498 | border: 1px solid var(--sdpi-bordercolor);
499 | }
500 |
501 | td {
502 | border-right: 1px solid var(--sdpi-bordercolor);
503 | -webkit-user-select: none;
504 | }
505 |
506 | tr:last-child,
507 | td:last-child {
508 | border: none;
509 | }
510 |
511 | .sdpi-item-value.select,
512 | .sdpi-item-value > select {
513 | margin-right: 13px;
514 | margin-left: 4px;
515 | }
516 |
517 | .sdpi-item-child,
518 | .sdpi-item-group > .sdpi-item > input[type="color"] {
519 | margin-top: 0.4em;
520 | margin-right: 4px;
521 | }
522 |
523 | .full,
524 | .full *,
525 | .sdpi-item-value.full,
526 | .sdpi-item-child > full > *,
527 | .sdpi-item-child.full,
528 | .sdpi-item-child.full > *,
529 | .full > .sdpi-item-child,
530 | .full > .sdpi-item-child > *{
531 | display: flex;
532 | flex: 1 1 0;
533 | margin-bottom: 4px;
534 | margin-left: 0px;
535 | width: 100%;
536 |
537 | justify-content: space-evenly;
538 | }
539 |
540 | .sdpi-item-group > .sdpi-item > input[type="color"] {
541 | margin-top: 0px;
542 | }
543 |
544 | ::-webkit-calendar-picker-indicator:focus,
545 | input[type=file]::-webkit-file-upload-button:focus,
546 | button:focus,
547 | textarea:focus,
548 | input:focus,
549 | select:focus,
550 | option:focus,
551 | details:focus,
552 | summary:focus,
553 | .custom-select select {
554 | outline: none;
555 | }
556 |
557 | summary {
558 | cursor: default;
559 | -webkit-user-select: none;
560 | }
561 |
562 | .pointer,
563 | summary .pointer {
564 | cursor: pointer;
565 | }
566 |
567 | details * {
568 | font-size: 12px;
569 | font-weight: normal;
570 | }
571 |
572 | details.message {
573 | padding: 4px 18px 4px 12px;
574 | }
575 |
576 | details.message summary {
577 | font-size: 10pt;
578 | font-weight: 600;
579 | min-height: 18px;
580 | }
581 |
582 | details.message:first-child {
583 | margin-top: 4px;
584 | margin-left: 0;
585 | padding-left: 102px;
586 | }
587 |
588 | details.message h1 {
589 | text-align: left;
590 | }
591 |
592 | .message > summary::-webkit-details-marker {
593 | display: none;
594 | }
595 |
596 | .info20,
597 | .question,
598 | .caution,
599 | .info {
600 | background-repeat: no-repeat;
601 | background-position: 72px center;
602 | }
603 |
604 | .info20 {
605 | background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20'%3E%3Cpath fill='%23999' d='M10,20 C4.4771525,20 0,15.5228475 0,10 C0,4.4771525 4.4771525,0 10,0 C15.5228475,0 20,4.4771525 20,10 C20,15.5228475 15.5228475,20 10,20 Z M10,8 C8.8954305,8 8,8.84275812 8,9.88235294 L8,16.1176471 C8,17.1572419 8.8954305,18 10,18 C11.1045695,18 12,17.1572419 12,16.1176471 L12,9.88235294 C12,8.84275812 11.1045695,8 10,8 Z M10,3 C8.8954305,3 8,3.88165465 8,4.96923077 L8,5.03076923 C8,6.11834535 8.8954305,7 10,7 C11.1045695,7 12,6.11834535 12,5.03076923 L12,4.96923077 C12,3.88165465 11.1045695,3 10,3 Z'/%3E%3C/svg%3E%0A");
606 | }
607 |
608 | .info {
609 | background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20'%3E%3Cpath fill='%23999' d='M10,18 C5.581722,18 2,14.418278 2,10 C2,5.581722 5.581722,2 10,2 C14.418278,2 18,5.581722 18,10 C18,14.418278 14.418278,18 10,18 Z M10,8 C9.44771525,8 9,8.42137906 9,8.94117647 L9,14.0588235 C9,14.5786209 9.44771525,15 10,15 C10.5522847,15 11,14.5786209 11,14.0588235 L11,8.94117647 C11,8.42137906 10.5522847,8 10,8 Z M10,5 C9.44771525,5 9,5.44082732 9,5.98461538 L9,6.01538462 C9,6.55917268 9.44771525,7 10,7 C10.5522847,7 11,6.55917268 11,6.01538462 L11,5.98461538 C11,5.44082732 10.5522847,5 10,5 Z'/%3E%3C/svg%3E%0A");
610 | }
611 |
612 | .info2 {
613 | background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='15' height='15' viewBox='0 0 15 15'%3E%3Cpath fill='%23999' d='M7.5,15 C3.35786438,15 0,11.6421356 0,7.5 C0,3.35786438 3.35786438,0 7.5,0 C11.6421356,0 15,3.35786438 15,7.5 C15,11.6421356 11.6421356,15 7.5,15 Z M7.5,2 C6.67157287,2 6,2.66124098 6,3.47692307 L6,3.52307693 C6,4.33875902 6.67157287,5 7.5,5 C8.32842705,5 9,4.33875902 9,3.52307693 L9,3.47692307 C9,2.66124098 8.32842705,2 7.5,2 Z M5,6 L5,7.02155172 L6,7 L6,12 L5,12.0076778 L5,13 L10,13 L10,12 L9,12.0076778 L9,6 L5,6 Z'/%3E%3C/svg%3E%0A");
614 | }
615 |
616 | .sdpi-more-info {
617 | background-image: linear-gradient(to right, #00000000 0%,#00000040 80%), url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' viewBox='0 0 16 16'%3E%3Cpolygon fill='%23999' points='4 7 8 7 8 5 12 8 8 11 8 9 4 9'/%3E%3C/svg%3E%0A");
618 | }
619 | .caution {
620 | background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20'%3E%3Cpath fill='%23999' fill-rule='evenodd' d='M9.03952676,0.746646542 C9.57068894,-0.245797319 10.4285735,-0.25196227 10.9630352,0.746646542 L19.7705903,17.2030214 C20.3017525,18.1954653 19.8777595,19 18.8371387,19 L1.16542323,19 C0.118729947,19 -0.302490098,18.2016302 0.231971607,17.2030214 L9.03952676,0.746646542 Z M10,2.25584053 L1.9601405,17.3478261 L18.04099,17.3478261 L10,2.25584053 Z M10,5.9375 C10.531043,5.9375 10.9615385,6.37373537 10.9615385,6.91185897 L10.9615385,11.6923077 C10.9615385,12.2304313 10.531043,12.6666667 10,12.6666667 C9.46895697,12.6666667 9.03846154,12.2304313 9.03846154,11.6923077 L9.03846154,6.91185897 C9.03846154,6.37373537 9.46895697,5.9375 10,5.9375 Z M10,13.4583333 C10.6372516,13.4583333 11.1538462,13.9818158 11.1538462,14.6275641 L11.1538462,14.6641026 C11.1538462,15.3098509 10.6372516,15.8333333 10,15.8333333 C9.36274837,15.8333333 8.84615385,15.3098509 8.84615385,14.6641026 L8.84615385,14.6275641 C8.84615385,13.9818158 9.36274837,13.4583333 10,13.4583333 Z'/%3E%3C/svg%3E%0A");
621 | }
622 |
623 | .question {
624 | background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20'%3E%3Cpath fill='%23999' d='M10,18 C5.581722,18 2,14.418278 2,10 C2,5.581722 5.581722,2 10,2 C14.418278,2 18,5.581722 18,10 C18,14.418278 14.418278,18 10,18 Z M6.77783203,7.65332031 C6.77783203,7.84798274 6.85929281,8.02888914 7.0222168,8.19604492 C7.18514079,8.36320071 7.38508996,8.44677734 7.62207031,8.44677734 C8.02409055,8.44677734 8.29703704,8.20768468 8.44091797,7.72949219 C8.59326248,7.27245865 8.77945854,6.92651485 8.99951172,6.69165039 C9.2195649,6.45678594 9.56233491,6.33935547 10.027832,6.33935547 C10.4256205,6.33935547 10.7006836,6.37695313 11.0021973,6.68847656 C11.652832,7.53271484 10.942627,8.472229 10.3750916,9.1321106 C9.80755615,9.79199219 8.29492188,11.9897461 10.027832,12.1347656 C10.4498423,12.1700818 10.7027991,11.9147157 10.7832031,11.4746094 C11.0021973,9.59857178 13.1254883,8.82415771 13.1254883,7.53271484 C13.1254883,7.07568131 12.9974785,6.65250846 12.7414551,6.26318359 C12.4854317,5.87385873 12.1225609,5.56600048 11.652832,5.33959961 C11.1831031,5.11319874 10.6414419,5 10.027832,5 C9.36767248,5 8.79004154,5.13541531 8.29492187,5.40625 C7.79980221,5.67708469 7.42317837,6.01879677 7.16503906,6.43139648 C6.90689975,6.8439962 6.77783203,7.25130007 6.77783203,7.65332031 Z M10.0099668,15 C10.2713191,15 10.5016601,14.9108147 10.7009967,14.7324415 C10.9003332,14.5540682 11,14.3088087 11,13.9966555 C11,13.7157177 10.9047629,13.4793767 10.7142857,13.2876254 C10.5238086,13.0958742 10.2890379,13 10.0099668,13 C9.72646591,13 9.48726565,13.0958742 9.2923588,13.2876254 C9.09745196,13.4793767 9,13.7157177 9,13.9966555 C9,14.313268 9.10077419,14.5596424 9.30232558,14.735786 C9.50387698,14.9119295 9.73975502,15 10.0099668,15 Z'/%3E%3C/svg%3E%0A");
625 | }
626 |
627 |
628 | .sdpi-more-info {
629 | position: fixed;
630 | left: 0px;
631 | right: 0px;
632 | bottom: 0px;
633 | min-height:16px;
634 | padding-right: 16px;
635 | text-align: right;
636 | -webkit-touch-callout: none;
637 | cursor: pointer;
638 | user-select: none;
639 | background-position: right center;
640 | background-repeat: no-repeat;
641 | border-radius: var(--sdpi-borderradius);
642 | text-decoration: none;
643 | color: var(--sdpi-color);
644 | }
645 |
646 | .sdpi-bottom-bar {
647 | display: flex;
648 | align-self: right;
649 | margin-left: auto;
650 | position: fixed;
651 | right: 17px;
652 | bottom: 0px;
653 | user-select: none;
654 | }
655 |
656 | .sdpi-bottom-bar.right {
657 | right: 0px;
658 | }
659 |
660 | .sdpi-bottom-bar button {
661 | min-height: 20px !important;
662 | height: 20px !important;
663 | }
664 |
665 |
666 | .sdpi-more-info-button {
667 | display: flex;
668 | align-self: right;
669 | margin-left: auto;
670 | position: fixed;
671 | right: 17px;
672 | bottom: 0px;
673 | user-select: none;
674 | }
675 |
676 | details a {
677 | background-position: right !important;
678 | min-height: 24px;
679 | display: inline-block;
680 | line-height: 24px;
681 | padding-right: 28px;
682 | }
683 |
684 |
685 | input:not([type="range"]),
686 | textarea {
687 | -webkit-appearance: none;
688 | background: var(--sdpi-background);
689 | color: var(--sdpi-color);
690 | font-weight: normal;
691 | font-size: 9pt;
692 | border: none;
693 | margin-top: 2px;
694 | margin-bottom: 2px;
695 | min-width: 219px;
696 | }
697 |
698 | textarea + label {
699 | display: flex;
700 | justify-content: flex-end
701 | }
702 | input[type="radio"],
703 | input[type="checkbox"] {
704 | display: none;
705 | }
706 | input[type="radio"] + label,
707 | input[type="checkbox"] + label {
708 | font-size: 9pt;
709 | color: var(--sdpi-color);
710 | font-weight: normal;
711 | margin-right: 8px;
712 | -webkit-user-select: none;
713 | }
714 |
715 | input[type="radio"] + label:after,
716 | input[type="checkbox"] + label:after {
717 | content: " " !important;
718 | }
719 |
720 | .sdpi-item[type="radio"] > .sdpi-item-value,
721 | .sdpi-item[type="checkbox"] > .sdpi-item-value {
722 | padding-top: 2px;
723 | }
724 |
725 | .sdpi-item[type="checkbox"] > .sdpi-item-value > * {
726 | margin-top: 4px;
727 | }
728 |
729 | .sdpi-item[type="checkbox"] .sdpi-item-child,
730 | .sdpi-item[type="radio"] .sdpi-item-child {
731 | display: inline-block;
732 | }
733 |
734 | .sdpi-item[type="range"] .sdpi-item-value,
735 | .sdpi-item[type="meter"] .sdpi-item-child,
736 | .sdpi-item[type="progress"] .sdpi-item-child {
737 | display: flex;
738 | }
739 |
740 | .sdpi-item[type="range"] .sdpi-item-value {
741 | min-height: 26px;
742 | }
743 |
744 | .sdpi-item[type="range"] .sdpi-item-value span,
745 | .sdpi-item[type="meter"] .sdpi-item-child span,
746 | .sdpi-item[type="progress"] .sdpi-item-child span {
747 | margin-top: -2px;
748 | min-width: 8px;
749 | text-align: right;
750 | user-select: none;
751 | cursor: pointer;
752 | -webkit-user-select: none;
753 | user-select: none;
754 | }
755 |
756 | .sdpi-item[type="range"] .sdpi-item-value span {
757 | margin-top: 7px;
758 | text-align: right;
759 | }
760 |
761 | span + input[type="range"] {
762 | display: flex;
763 | max-width: 168px;
764 |
765 | }
766 |
767 | .sdpi-item[type="range"] .sdpi-item-value span:first-child,
768 | .sdpi-item[type="meter"] .sdpi-item-child span:first-child,
769 | .sdpi-item[type="progress"] .sdpi-item-child span:first-child {
770 | margin-right: 4px;
771 | }
772 |
773 | .sdpi-item[type="range"] .sdpi-item-value span:last-child,
774 | .sdpi-item[type="meter"] .sdpi-item-child span:last-child,
775 | .sdpi-item[type="progress"] .sdpi-item-child span:last-child {
776 | margin-left: 4px;
777 | }
778 |
779 | .reverse {
780 | transform: rotate(180deg);
781 | }
782 |
783 | .sdpi-item[type="meter"] .sdpi-item-child meter + span:last-child {
784 | margin-left: -10px;
785 | }
786 |
787 | .sdpi-item[type="progress"] .sdpi-item-child meter + span:last-child {
788 | margin-left: -14px;
789 | }
790 |
791 | .sdpi-item[type="radio"] > .sdpi-item-value > * {
792 | margin-top: 2px;
793 | }
794 |
795 | details {
796 | padding: 8px 18px 8px 12px;
797 | min-width: 86px;
798 | }
799 |
800 | details > h4 {
801 | border-bottom: 1px solid var(--sdpi-bordercolor);
802 | }
803 |
804 | legend {
805 | display: none;
806 | }
807 | .sdpi-item-value > textarea {
808 | padding: 0px;
809 | width: 219px;
810 | margin-left: 1px;
811 | margin-top: 3px;
812 | padding: 4px;
813 | }
814 |
815 | input[type="radio"] + label span,
816 | input[type="checkbox"] + label span {
817 | display: inline-block;
818 | width: 16px;
819 | height: 16px;
820 | margin: 2px 4px 2px 0;
821 | border-radius: 3px;
822 | vertical-align: middle;
823 | background: var(--sdpi-background);
824 | cursor: pointer;
825 | border: 1px solid rgb(0,0,0,.2);
826 | }
827 |
828 | input[type="radio"] + label span {
829 | border-radius: 100%;
830 | }
831 |
832 | input[type="radio"]:checked + label span,
833 | input[type="checkbox"]:checked + label span {
834 | background-color: #77f;
835 | background-image: url(check.svg);
836 | background-repeat: no-repeat;
837 | background-position: center center;
838 | border: 1px solid rgb(0,0,0,.4);
839 | }
840 |
841 | input[type="radio"]:active:checked + label span,
842 | input[type="radio"]:active + label span,
843 | input[type="checkbox"]:active:checked + label span,
844 | input[type="checkbox"]:active + label span {
845 | background-color: #303030;
846 | }
847 |
848 | input[type="radio"]:checked + label span {
849 | background-image: url(rcheck.svg);
850 | }
851 |
852 | input[type="range"] {
853 | width: var(--sdpi-width);
854 | height: 30px;
855 | overflow: hidden;
856 | cursor: pointer;
857 | background: transparent !important;
858 | }
859 |
860 | .sdpi-item > input[type="range"] {
861 | margin-left: 2px;
862 | max-width: var(--sdpi-width);
863 | width: var(--sdpi-width);
864 | padding: 0px;
865 | margin-top: 2px;
866 | }
867 |
868 | /*
869 | input[type="range"],
870 | input[type="range"]::-webkit-slider-runnable-track,
871 | input[type="range"]::-webkit-slider-thumb {
872 | -webkit-appearance: none;
873 | }
874 | */
875 |
876 | input[type="range"]::-webkit-slider-runnable-track {
877 | height: 5px;
878 | background: #979797;
879 | border-radius: 3px;
880 | padding:0px !important;
881 | border: 1px solid var(--sdpi-background);
882 | }
883 |
884 | input[type="range"]::-webkit-slider-thumb {
885 | position: relative;
886 | -webkit-appearance: none;
887 | background-color: var(--sdpi-color);
888 | width: 12px;
889 | height: 12px;
890 | border-radius: 20px;
891 | margin-top: -5px;
892 | border: none;
893 | }
894 | input[type="range" i]{
895 | margin: 0;
896 | }
897 |
898 | input[type="range"]::-webkit-slider-thumb::before {
899 | position: absolute;
900 | content: "";
901 | height: 5px; /* equal to height of runnable track or 1 less */
902 | width: 500px; /* make this bigger than the widest range input element */
903 | left: -502px; /* this should be -2px - width */
904 | top: 8px; /* don't change this */
905 | background: #77f;
906 | }
907 |
908 | input[type="color"] {
909 | min-width: 32px;
910 | min-height: 32px;
911 | width: 32px;
912 | height: 32px;
913 | padding: 0;
914 | background-color: var(--sdpi-bgcolor);
915 | flex: none;
916 | }
917 |
918 | ::-webkit-color-swatch {
919 | min-width: 24px;
920 | }
921 |
922 | textarea {
923 | height: 3em;
924 | word-break: break-word;
925 | line-height: 1.5em;
926 | }
927 |
928 | .textarea {
929 | padding: 0px !important;
930 | }
931 |
932 | textarea {
933 | width: 219px; /*98%;*/
934 | height: 96%;
935 | min-height: 6em;
936 | resize: none;
937 | border-radius: var(--sdpi-borderradius);
938 | }
939 |
940 | /* CAROUSEL */
941 |
942 | .sdpi-item[type="carousel"]{
943 |
944 | }
945 |
946 | .sdpi-item.card-carousel-wrapper,
947 | .sdpi-item > .card-carousel-wrapper {
948 | padding: 0;
949 | }
950 |
951 |
952 | .card-carousel-wrapper {
953 | display: flex;
954 | align-items: center;
955 | justify-content: center;
956 | margin: 12px auto;
957 | color: #666a73;
958 | }
959 |
960 | .card-carousel {
961 | display: flex;
962 | justify-content: center;
963 | width: 278px;
964 | }
965 | .card-carousel--overflow-container {
966 | overflow: hidden;
967 | }
968 | .card-carousel--nav__left,
969 | .card-carousel--nav__right {
970 | /* display: inline-block; */
971 | width: 12px;
972 | height: 12px;
973 | border-top: 2px solid #42b883;
974 | border-right: 2px solid #42b883;
975 | cursor: pointer;
976 | margin: 0 4px;
977 | transition: transform 150ms linear;
978 | }
979 | .card-carousel--nav__left[disabled],
980 | .card-carousel--nav__right[disabled] {
981 | opacity: 0.2;
982 | border-color: black;
983 | }
984 | .card-carousel--nav__left {
985 | transform: rotate(-135deg);
986 | }
987 | .card-carousel--nav__left:active {
988 | transform: rotate(-135deg) scale(0.85);
989 | }
990 | .card-carousel--nav__right {
991 | transform: rotate(45deg);
992 | }
993 | .card-carousel--nav__right:active {
994 | transform: rotate(45deg) scale(0.85);
995 | }
996 | .card-carousel-cards {
997 | display: flex;
998 | transition: transform 150ms ease-out;
999 | transform: translatex(0px);
1000 | }
1001 | .card-carousel-cards .card-carousel--card {
1002 | margin: 0 5px;
1003 | cursor: pointer;
1004 | /* box-shadow: 0 4px 15px 0 rgba(40, 44, 53, 0.06), 0 2px 2px 0 rgba(40, 44, 53, 0.08); */
1005 | background-color: #fff;
1006 | border-radius: 4px;
1007 | z-index: 3;
1008 | }
1009 | .xxcard-carousel-cards .card-carousel--card:first-child {
1010 | margin-left: 0;
1011 | }
1012 | .xxcard-carousel-cards .card-carousel--card:last-child {
1013 | margin-right: 0;
1014 | }
1015 | .card-carousel-cards .card-carousel--card img {
1016 | vertical-align: bottom;
1017 | border-top-left-radius: 4px;
1018 | border-top-right-radius: 4px;
1019 | transition: opacity 150ms linear;
1020 | width: 60px;
1021 | }
1022 | .card-carousel-cards .card-carousel--card img:hover {
1023 | opacity: 0.5;
1024 | }
1025 | .card-carousel-cards .card-carousel--card--footer {
1026 | border-top: 0;
1027 | max-width: 80px;
1028 | overflow: hidden;
1029 | display: flex;
1030 | height: 100%;
1031 | flex-direction: column;
1032 | }
1033 | .card-carousel-cards .card-carousel--card--footer p {
1034 | padding: 3px 0;
1035 | margin: 0;
1036 | margin-bottom: 2px;
1037 | font-size: 15px;
1038 | font-weight: 500;
1039 | color: #2c3e50;
1040 | }
1041 | .card-carousel-cards .card-carousel--card--footer p:nth-of-type(2) {
1042 | font-size: 12px;
1043 | font-weight: 300;
1044 | padding: 6px;
1045 | color: #666a73;
1046 | }
1047 |
1048 |
1049 | h1 {
1050 | font-size: 1.3em;
1051 | font-weight: 500;
1052 | text-align: center;
1053 | margin-bottom: 12px;
1054 | }
1055 |
1056 | ::-webkit-datetime-edit {
1057 | font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
1058 | background: url(elg_calendar_inv.svg) no-repeat left center;
1059 | padding-right: 1em;
1060 | padding-left: 25px;
1061 | background-position: 4px 0px;
1062 | }
1063 | ::-webkit-datetime-edit-fields-wrapper {
1064 |
1065 | }
1066 | ::-webkit-datetime-edit-text { padding: 0 0.3em; }
1067 | ::-webkit-datetime-edit-month-field { }
1068 | ::-webkit-datetime-edit-day-field {}
1069 | ::-webkit-datetime-edit-year-field {}
1070 | ::-webkit-inner-spin-button {
1071 |
1072 | /* display: none; */
1073 | }
1074 | ::-webkit-calendar-picker-indicator {
1075 | background: transparent;
1076 | font-size: 17px;
1077 | }
1078 |
1079 | ::-webkit-calendar-picker-indicator:focus {
1080 | background-color: rgba(0,0,0,0.2);
1081 | }
1082 |
1083 | input[type="date"] {
1084 | -webkit-align-items: center;
1085 | display: -webkit-inline-flex;
1086 | font-family: monospace;
1087 | overflow: hidden;
1088 | padding: 0;
1089 | -webkit-padding-start: 1px;
1090 | }
1091 |
1092 | input::-webkit-datetime-edit {
1093 | -webkit-flex: 1;
1094 | -webkit-user-modify: read-only !important;
1095 | display: inline-block;
1096 | min-width: 0;
1097 | overflow: hidden;
1098 | }
1099 |
1100 | /*
1101 | input::-webkit-datetime-edit-fields-wrapper {
1102 | -webkit-user-modify: read-only !important;
1103 | display: inline-block;
1104 | padding: 1px 0;
1105 | white-space: pre;
1106 | }
1107 | */
1108 |
1109 | /*
1110 | input[type="date"] {
1111 | background-color: red;
1112 | outline: none;
1113 | }
1114 | input[type="date"]::-webkit-clear-button {
1115 | font-size: 18px;
1116 | height: 30px;
1117 | position: relative;
1118 | }
1119 | input[type="date"]::-webkit-inner-spin-button {
1120 | height: 28px;
1121 | }
1122 | input[type="date"]::-webkit-calendar-picker-indicator {
1123 | font-size: 15px;
1124 | } */
1125 |
1126 | input[type="file"] {
1127 | opacity: 0;
1128 | display: none;
1129 | }
1130 |
1131 | .sdpi-item > input[type="file"] {
1132 | opacity: 1;
1133 | display: flex;
1134 | }
1135 |
1136 | input[type="file"] + span {
1137 | display: flex;
1138 | flex: 0 1 auto;
1139 | background-color: #0000ff50;
1140 | }
1141 |
1142 | label.sdpi-file-label {
1143 | cursor: pointer;
1144 | user-select: none;
1145 | display: inline-block;
1146 | min-height: 21px !important;
1147 | height: 21px !important;
1148 | line-height: 20px;
1149 | padding: 0px 4px;
1150 | margin: auto;
1151 | margin-right: 0px;
1152 | float:right;
1153 | }
1154 |
1155 | .sdpi-file-label > label:active,
1156 | .sdpi-file-label.file:active,
1157 | label.sdpi-file-label:active,
1158 | label.sdpi-file-info:active,
1159 | input[type="file"]::-webkit-file-upload-button:active,
1160 | button:active {
1161 | background-color: var(--sdpi-color);
1162 | color:#303030;
1163 | }
1164 |
1165 | input:required:invalid, input:focus:invalid {
1166 | background: var(--sdpi-background) url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI5IiBoZWlnaHQ9IjkiIHZpZXdCb3g9IjAgMCA5IDkiPgogICAgPHBhdGggZmlsbD0iI0Q4RDhEOCIgZD0iTTQuNSwwIEM2Ljk4NTI4MTM3LC00LjU2NTM4NzgyZS0xNiA5LDIuMDE0NzE4NjMgOSw0LjUgQzksNi45ODUyODEzNyA2Ljk4NTI4MTM3LDkgNC41LDkgQzIuMDE0NzE4NjMsOSAzLjA0MzU5MTg4ZS0xNiw2Ljk4NTI4MTM3IDAsNC41IEMtMy4wNDM1OTE4OGUtMTYsMi4wMTQ3MTg2MyAyLjAxNDcxODYzLDQuNTY1Mzg3ODJlLTE2IDQuNSwwIFogTTQsMSBMNCw2IEw1LDYgTDUsMSBMNCwxIFogTTQuNSw4IEM0Ljc3NjE0MjM3LDggNSw3Ljc3NjE0MjM3IDUsNy41IEM1LDcuMjIzODU3NjMgNC43NzYxNDIzNyw3IDQuNSw3IEM0LjIyMzg1NzYzLDcgNCw3LjIyMzg1NzYzIDQsNy41IEM0LDcuNzc2MTQyMzcgNC4yMjM4NTc2Myw4IDQuNSw4IFoiLz4KICA8L3N2Zz4) no-repeat 98% center;
1167 | }
1168 |
1169 | input:required:valid {
1170 | background: var(--sdpi-background) url(data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSI5IiBoZWlnaHQ9IjkiIHZpZXdCb3g9IjAgMCA5IDkiPjxwb2x5Z29uIGZpbGw9IiNEOEQ4RDgiIHBvaW50cz0iNS4yIDEgNi4yIDEgNi4yIDcgMy4yIDcgMy4yIDYgNS4yIDYiIHRyYW5zZm9ybT0icm90YXRlKDQwIDQuNjc3IDQpIi8+PC9zdmc+) no-repeat 98% center;
1171 | }
1172 |
1173 | .tooltip,
1174 | :tooltip,
1175 | :title {
1176 | color: yellow;
1177 | }
1178 | /*
1179 | [title]:hover {
1180 | display: flex;
1181 | align-items: center;
1182 | justify-content: center;
1183 | }
1184 | [title]:hover::after {
1185 | content: '';
1186 | position: absolute;
1187 | bottom: -1000px;
1188 | left: 8px;
1189 | display: none;
1190 | color: #fff;
1191 | border: 8px solid transparent;
1192 | border-bottom: 8px solid #000;
1193 | }
1194 | [title]:hover::before {
1195 | content: attr(title);
1196 | display: flex;
1197 | justify-content: center;
1198 | align-self: center;
1199 | padding: 6px 12px;
1200 | border-radius: 5px;
1201 | background: rgba(0,0,0,0.8);
1202 | color: var(--sdpi-color);
1203 | font-size: 9pt;
1204 | font-family: sans-serif;
1205 | opacity: 1;
1206 | position: absolute;
1207 | height: auto;
1208 |
1209 | text-align: center;
1210 | bottom: 2px;
1211 | z-index: 100;
1212 | box-shadow: 0px 3px 6px rgba(0, 0, 0, .5);
1213 | }
1214 | */
1215 |
1216 | .sdpi-item-group.file {
1217 | width: 232px;
1218 | display: flex;
1219 | align-items: center;
1220 | }
1221 |
1222 | .sdpi-file-info {
1223 | overflow-wrap: break-word;
1224 | word-wrap: break-word;
1225 | hyphens: auto;
1226 |
1227 | min-width: 132px;
1228 | max-width: 144px;
1229 | max-height: 32px;
1230 | margin-top: 0px;
1231 | margin-left: 5px;
1232 | display: inline-block;
1233 | overflow: hidden;
1234 | padding: 6px 4px;
1235 | background-color: var(--sdpi-background);
1236 | }
1237 |
1238 |
1239 | ::-webkit-scrollbar {
1240 | width: 8px;
1241 | }
1242 |
1243 | ::-webkit-scrollbar-track {
1244 | -webkit-box-shadow: inset 0 0 6px rgba(0,0,0,0.3);
1245 | }
1246 |
1247 | ::-webkit-scrollbar-thumb {
1248 | background-color: #999999;
1249 | outline: 1px solid slategrey;
1250 | border-radius: 8px;
1251 | }
1252 |
1253 | a {
1254 | color: #7397d2;
1255 | }
1256 |
1257 | .testcontainer {
1258 | display: flex;
1259 | background-color: #0000ff20;
1260 | max-width: 400px;
1261 | height: 200px;
1262 | align-content: space-evenly;
1263 | }
1264 |
1265 | input[type=range] {
1266 | -webkit-appearance: none;
1267 | /* background-color: green; */
1268 | height:6px;
1269 | margin-top: 12px;
1270 | z-index: 0;
1271 | overflow: visible;
1272 | }
1273 |
1274 | /*
1275 | input[type="range"]::-webkit-slider-thumb {
1276 | -webkit-appearance: none;
1277 | background-color: var(--sdpi-color);
1278 | width: 12px;
1279 | height: 12px;
1280 | border-radius: 20px;
1281 | margin-top: -6px;
1282 | border: none;
1283 | } */
1284 |
1285 | :-webkit-slider-thumb {
1286 | -webkit-appearance: none;
1287 | background-color: var(--sdpi-color);
1288 | width: 16px;
1289 | height: 16px;
1290 | border-radius: 20px;
1291 | margin-top: -6px;
1292 | border: 1px solid #999999;
1293 | }
1294 |
1295 | .sdpi-item[type="range"] .sdpi-item-group {
1296 | display: flex;
1297 | flex-direction: column;
1298 | }
1299 |
1300 | .xxsdpi-item[type="range"] .sdpi-item-group input {
1301 | max-width: 204px;
1302 | }
1303 |
1304 | .sdpi-item[type="range"] .sdpi-item-group span {
1305 | margin-left: 0px !important;
1306 | }
1307 |
1308 | .sdpi-item[type="range"] .sdpi-item-group > .sdpi-item-child {
1309 | display: flex;
1310 | flex-direction: row;
1311 | }
1312 |
1313 | .rangeLabel {
1314 | position:absolute;
1315 | font-weight:normal;
1316 | margin-top:22px;
1317 | }
1318 |
1319 | :disabled {
1320 | color: #993333;
1321 | }
1322 |
1323 | select,
1324 | select option {
1325 | color: var(--sdpi-color);
1326 | }
1327 |
1328 | select.disabled,
1329 | select option:disabled {
1330 | color: #fd9494;
1331 | font-style: italic;
1332 | }
1333 |
1334 | .runningAppsContainer {
1335 | display: none;
1336 | }
1337 |
1338 | /* debug
1339 | div {
1340 | background-color: rgba(64,128,255,0.2);
1341 | }
1342 | */
1343 |
1344 | .one-line {
1345 | min-height: 1.5em;
1346 | }
1347 |
1348 | .two-lines {
1349 | min-height: 3em;
1350 | }
1351 |
1352 | .three-lines {
1353 | min-height: 4.5em;
1354 | }
1355 |
1356 | .four-lines {
1357 | min-height: 6em;
1358 | }
1359 |
1360 | .min80 > .sdpi-item-child {
1361 | min-width: 80px;
1362 | }
1363 |
1364 | .min100 > .sdpi-item-child {
1365 | min-width: 100px;
1366 | }
1367 |
1368 | .min120 > .sdpi-item-child {
1369 | min-width: 120px;
1370 | }
1371 |
1372 | .min140 > .sdpi-item-child {
1373 | min-width: 140px;
1374 | }
1375 |
1376 | .min160 > .sdpi-item-child {
1377 | min-width: 160px;
1378 | }
1379 |
1380 | .min200 > .sdpi-item-child {
1381 | min-width: 200px;
1382 | }
1383 |
1384 | .max40 {
1385 | flex-basis: 40%;
1386 | flex-grow: 0;
1387 | }
1388 |
1389 | .max30 {
1390 | flex-basis: 30%;
1391 | flex-grow: 0;
1392 | }
1393 |
1394 | .max20 {
1395 | flex-basis: 20%;
1396 | flex-grow: 0;
1397 | }
1398 |
1399 | .up20 {
1400 | margin-top: -20px;
1401 | }
1402 |
1403 | .alignCenter {
1404 | align-items: center;
1405 | }
1406 |
1407 | .alignTop {
1408 | align-items: flex-start;
1409 | }
1410 |
1411 | .alignBaseline {
1412 | align-items: baseline;
1413 | }
1414 |
1415 | .noMargins,
1416 | .noMargins *,
1417 | .noInnerMargins * {
1418 | margin: 0;
1419 | padding: 0;
1420 | }
1421 |
1422 | .hidden {
1423 | display: none;
1424 | }
1425 |
1426 | .icon-help,
1427 | .icon-help-line,
1428 | .icon-help-fill,
1429 | .icon-help-inv,
1430 | .icon-brighter,
1431 | .icon-darker,
1432 | .icon-warmer,
1433 | .icon-cooler {
1434 | min-width: 20px;
1435 | width: 20px;
1436 | background-repeat: no-repeat;
1437 | opacity: 1;
1438 | }
1439 |
1440 | .icon-help:active,
1441 | .icon-help-line:active,
1442 | .icon-help-fill:active,
1443 | .icon-help-inv:active,
1444 | .icon-brighter:active,
1445 | .icon-darker:active,
1446 | .icon-warmer:active,
1447 | .icon-cooler:active {
1448 | opacity: 0.5;
1449 | }
1450 |
1451 | .icon-brighter,
1452 | .icon-darker,
1453 | .icon-warmer,
1454 | .icon-cooler {
1455 | margin-top: 5px !important;
1456 | }
1457 |
1458 | .icon-help,
1459 | .icon-help-line,
1460 | .icon-help-fill,
1461 | .icon-help-inv {
1462 | cursor: pointer;
1463 | margin: 0px;
1464 | margin-left: 4px;
1465 | }
1466 |
1467 | .icon-brighter {
1468 | background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20'%3E%3Cg fill='%23999' fill-rule='evenodd'%3E%3Ccircle cx='10' cy='10' r='4'/%3E%3Cpath d='M14.8532861,7.77530426 C14.7173255,7.4682615 14.5540843,7.17599221 14.3666368,6.90157083 L16.6782032,5.5669873 L17.1782032,6.4330127 L14.8532861,7.77530426 Z M10.5,4.5414007 C10.2777625,4.51407201 10.051423,4.5 9.82179677,4.5 C9.71377555,4.5 9.60648167,4.50311409 9.5,4.50925739 L9.5,2 L10.5,2 L10.5,4.5414007 Z M5.38028092,6.75545367 C5.18389364,7.02383457 5.01124349,7.31068015 4.86542112,7.61289977 L2.82179677,6.4330127 L3.32179677,5.5669873 L5.38028092,6.75545367 Z M4.86542112,12.3871002 C5.01124349,12.6893198 5.18389364,12.9761654 5.38028092,13.2445463 L3.32179677,14.4330127 L2.82179677,13.5669873 L4.86542112,12.3871002 Z M9.5,15.4907426 C9.60648167,15.4968859 9.71377555,15.5 9.82179677,15.5 C10.051423,15.5 10.2777625,15.485928 10.5,15.4585993 L10.5,18 L9.5,18 L9.5,15.4907426 Z M14.3666368,13.0984292 C14.5540843,12.8240078 14.7173255,12.5317385 14.8532861,12.2246957 L17.1782032,13.5669873 L16.6782032,14.4330127 L14.3666368,13.0984292 Z'/%3E%3C/g%3E%3C/svg%3E");
1469 | }
1470 | .icon-darker {
1471 | background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20'%3E%3Cg fill='%23999' fill-rule='evenodd'%3E%3Cpath d='M10 14C7.790861 14 6 12.209139 6 10 6 7.790861 7.790861 6 10 6 12.209139 6 14 7.790861 14 10 14 12.209139 12.209139 14 10 14zM10 13C11.6568542 13 13 11.6568542 13 10 13 8.34314575 11.6568542 7 10 7 8.34314575 7 7 8.34314575 7 10 7 11.6568542 8.34314575 13 10 13zM14.8532861 7.77530426C14.7173255 7.4682615 14.5540843 7.17599221 14.3666368 6.90157083L16.6782032 5.5669873 17.1782032 6.4330127 14.8532861 7.77530426zM10.5 4.5414007C10.2777625 4.51407201 10.051423 4.5 9.82179677 4.5 9.71377555 4.5 9.60648167 4.50311409 9.5 4.50925739L9.5 2 10.5 2 10.5 4.5414007zM5.38028092 6.75545367C5.18389364 7.02383457 5.01124349 7.31068015 4.86542112 7.61289977L2.82179677 6.4330127 3.32179677 5.5669873 5.38028092 6.75545367zM4.86542112 12.3871002C5.01124349 12.6893198 5.18389364 12.9761654 5.38028092 13.2445463L3.32179677 14.4330127 2.82179677 13.5669873 4.86542112 12.3871002zM9.5 15.4907426C9.60648167 15.4968859 9.71377555 15.5 9.82179677 15.5 10.051423 15.5 10.2777625 15.485928 10.5 15.4585993L10.5 18 9.5 18 9.5 15.4907426zM14.3666368 13.0984292C14.5540843 12.8240078 14.7173255 12.5317385 14.8532861 12.2246957L17.1782032 13.5669873 16.6782032 14.4330127 14.3666368 13.0984292z'/%3E%3C/g%3E%3C/svg%3E");
1472 | }
1473 | .icon-warmer {
1474 | background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20'%3E%3Cg fill='%23999' fill-rule='evenodd'%3E%3Cpath d='M12.3247275 11.4890349C12.0406216 11.0007637 11.6761954 10.5649925 11.2495475 10.1998198 11.0890394 9.83238991 11 9.42659309 11 9 11 7.34314575 12.3431458 6 14 6 15.6568542 6 17 7.34314575 17 9 17 10.6568542 15.6568542 12 14 12 13.3795687 12 12.8031265 11.8116603 12.3247275 11.4890349zM17.6232392 11.6692284C17.8205899 11.4017892 17.9890383 11.1117186 18.123974 10.8036272L20.3121778 12.0669873 19.8121778 12.9330127 17.6232392 11.6692284zM18.123974 7.19637279C17.9890383 6.88828142 17.8205899 6.5982108 17.6232392 6.33077158L19.8121778 5.0669873 20.3121778 5.9330127 18.123974 7.19637279zM14.5 4.52746439C14.3358331 4.50931666 14.1690045 4.5 14 4.5 13.8309955 4.5 13.6641669 4.50931666 13.5 4.52746439L13.5 2 14.5 2 14.5 4.52746439zM13.5 13.4725356C13.6641669 13.4906833 13.8309955 13.5 14 13.5 14.1690045 13.5 14.3358331 13.4906833 14.5 13.4725356L14.5 16 13.5 16 13.5 13.4725356zM14 11C15.1045695 11 16 10.1045695 16 9 16 7.8954305 15.1045695 7 14 7 12.8954305 7 12 7.8954305 12 9 12 10.1045695 12.8954305 11 14 11zM9.5 11C10.6651924 11.4118364 11.5 12.5 11.5 14 11.5 16 10 17.5 8 17.5 6 17.5 4.5 16 4.5 14 4.5 12.6937812 5 11.5 6.5 11L6.5 7 9.5 7 9.5 11z'/%3E%3Cpath d='M12,14 C12,16.209139 10.209139,18 8,18 C5.790861,18 4,16.209139 4,14 C4,12.5194353 4.80439726,11.2267476 6,10.5351288 L6,4 C6,2.8954305 6.8954305,2 8,2 C9.1045695,2 10,2.8954305 10,4 L10,10.5351288 C11.1956027,11.2267476 12,12.5194353 12,14 Z M11,14 C11,12.6937812 10.1651924,11.5825421 9,11.1707057 L9,4 C9,3.44771525 8.55228475,3 8,3 C7.44771525,3 7,3.44771525 7,4 L7,11.1707057 C5.83480763,11.5825421 5,12.6937812 5,14 C5,15.6568542 6.34314575,17 8,17 C9.65685425,17 11,15.6568542 11,14 Z'/%3E%3C/g%3E%3C/svg%3E");
1475 | }
1476 |
1477 | .icon-cooler {
1478 | background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20' viewBox='0 0 20 20'%3E%3Cg fill='%23999' fill-rule='evenodd'%3E%3Cpath d='M10.4004569 11.6239517C10.0554735 10.9863849 9.57597206 10.4322632 9 9.99963381L9 9.7450467 9.53471338 9.7450467 10.8155381 8.46422201C10.7766941 8.39376637 10.7419749 8.32071759 10.7117062 8.2454012L9 8.2454012 9 6.96057868 10.6417702 6.96057868C10.6677696 6.86753378 10.7003289 6.77722682 10.7389179 6.69018783L9.44918707 5.40045694 9 5.40045694 9 4.34532219 9.32816127 4.34532219 9.34532219 2.91912025 10.4004569 2.91912025 10.4004569 4.53471338 11.6098599 5.74411634C11.7208059 5.68343597 11.8381332 5.63296451 11.9605787 5.59396526L11.9605787 3.8884898 10.8181818 2.74609294 11.5642748 2 12.5727518 3.00847706 13.5812289 2 14.3273218 2.74609294 13.2454012 3.82801356 13.2454012 5.61756719C13.3449693 5.65339299 13.4408747 5.69689391 13.5324038 5.74735625L14.7450467 4.53471338 14.7450467 2.91912025 15.8001815 2.91912025 15.8001815 4.34532219 17.2263834 4.34532219 17.2263834 5.40045694 15.6963166 5.40045694 14.4002441 6.69652946C14.437611 6.78161093 14.4692249 6.86979146 14.4945934 6.96057868L16.2570138 6.96057868 17.3994107 5.81818182 18.1455036 6.56427476 17.1370266 7.57275182 18.1455036 8.58122888 17.3994107 9.32732182 16.3174901 8.2454012 14.4246574 8.2454012C14.3952328 8.31861737 14.3616024 8.38969062 14.3240655 8.45832192L15.6107903 9.7450467 17.2263834 9.7450467 17.2263834 10.8001815 15.8001815 10.8001815 15.8001815 12.2263834 14.7450467 12.2263834 14.7450467 10.6963166 13.377994 9.32926387C13.3345872 9.34850842 13.2903677 9.36625331 13.2454012 9.38243281L13.2454012 11.3174901 14.3273218 12.3994107 13.5812289 13.1455036 12.5848864 12.1491612 11.5642748 13.1455036 10.8181818 12.3994107 11.9605787 11.2570138 11.9605787 9.40603474C11.8936938 9.38473169 11.828336 9.36000556 11.7647113 9.33206224L10.4004569 10.6963166 10.4004569 11.6239517zM12.75 8.5C13.3022847 8.5 13.75 8.05228475 13.75 7.5 13.75 6.94771525 13.3022847 6.5 12.75 6.5 12.1977153 6.5 11.75 6.94771525 11.75 7.5 11.75 8.05228475 12.1977153 8.5 12.75 8.5zM9.5 14C8.5 16.3333333 7.33333333 17.5 6 17.5 4.66666667 17.5 3.5 16.3333333 2.5 14L9.5 14z'/%3E%3Cpath d='M10,14 C10,16.209139 8.209139,18 6,18 C3.790861,18 2,16.209139 2,14 C2,12.5194353 2.80439726,11.2267476 4,10.5351288 L4,4 C4,2.8954305 4.8954305,2 6,2 C7.1045695,2 8,2.8954305 8,4 L8,10.5351288 C9.19560274,11.2267476 10,12.5194353 10,14 Z M9,14 C9,12.6937812 8.16519237,11.5825421 7,11.1707057 L7,4 C7,3.44771525 6.55228475,3 6,3 C5.44771525,3 5,3.44771525 5,4 L5,11.1707057 C3.83480763,11.5825421 3,12.6937812 3,14 C3,15.6568542 4.34314575,17 6,17 C7.65685425,17 9,15.6568542 9,14 Z'/%3E%3C/g%3E%3C/svg%3E");
1479 | }
1480 |
1481 | .icon-help {
1482 | background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20'%3E%3Cpath fill='%23999' d='M11.292 12.516l.022 1.782H9.07v-1.804c0-1.98 1.276-2.574 2.662-3.278h-.022c.814-.44 1.65-.88 1.694-2.2.044-1.386-1.122-2.728-3.234-2.728-1.518 0-2.662.902-3.366 2.354L5 5.608C5.946 3.584 7.662 2 10.17 2c3.564 0 5.632 2.442 5.588 5.06-.066 2.618-1.716 3.41-3.102 4.158-.704.374-1.364.682-1.364 1.298zm-1.122 2.442c.858 0 1.452.594 1.452 1.452 0 .682-.594 1.408-1.452 1.408-.77 0-1.386-.726-1.386-1.408 0-.858.616-1.452 1.386-1.452z'/%3E%3C/svg%3E");
1483 | }
1484 |
1485 | .icon-help-line {
1486 | background-image: url("data:image/svg+xml,%3Csvg width='20' height='20' xmlns='http://www.w3.org/2000/svg'%3E%3Cg fill='%23999' fill-rule='evenodd'%3E%3Cpath d='M10 20C4.477 20 0 15.523 0 10S4.477 0 10 0s10 4.477 10 10-4.477 10-10 10zm0-1a9 9 0 1 0 0-18 9 9 0 0 0 0 18z'/%3E%3Cpath d='M10.848 12.307l.02 1.578H8.784v-1.597c0-1.753 1.186-2.278 2.474-2.901h-.02c.756-.39 1.533-.78 1.574-1.948.041-1.226-1.043-2.414-3.006-2.414-1.41 0-2.474.798-3.128 2.083L5 6.193C5.88 4.402 7.474 3 9.805 3 13.118 3 15.04 5.161 15 7.478c-.061 2.318-1.595 3.019-2.883 3.68-.654.332-1.268.604-1.268 1.15zM9.805 14.47c.798 0 1.35.525 1.35 1.285 0 .603-.552 1.246-1.35 1.246-.715 0-1.288-.643-1.288-1.246 0-.76.573-1.285 1.288-1.285z' fill-rule='nonzero'/%3E%3C/g%3E%3C/svg%3E");}
1487 |
1488 | .icon-help-fill {
1489 | background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20'%3E%3Cg fill='none' fill-rule='evenodd'%3E%3Ccircle cx='10' cy='10' r='10' fill='%23999'/%3E%3Cpath fill='%23FFF' fill-rule='nonzero' d='M8.368 7.189H5C5 3.5 7.668 2 10.292 2 13.966 2 16 4.076 16 7.012c0 3.754-3.849 3.136-3.849 5.211v1.656H8.455v-1.832c0-2.164 1.4-2.893 2.778-3.6.437-.242 1.006-.574 1.006-1.236 0-2.208-3.871-2.142-3.871-.022zM10.25 18a1.75 1.75 0 1 1 0-3.5 1.75 1.75 0 0 1 0 3.5z'/%3E%3C/g%3E%3C/svg%3E");
1490 | }
1491 |
1492 | .icon-help-inv {
1493 | background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='20' height='20'%3E%3Cpath fill='%23999' fill-rule='evenodd' d='M10 20C4.477 20 0 15.523 0 10S4.477 0 10 0s10 4.477 10 10-4.477 10-10 10zM8.368 7.189c0-2.12 3.87-2.186 3.87.022 0 .662-.568.994-1.005 1.236-1.378.707-2.778 1.436-2.778 3.6v1.832h3.696v-1.656c0-2.075 3.849-1.457 3.849-5.21C16 4.075 13.966 2 10.292 2 7.668 2 5 3.501 5 7.189h3.368zM10.25 18a1.75 1.75 0 1 0 0-3.5 1.75 1.75 0 0 0 0 3.5z'/%3E%3C/svg%3E");
1494 | }
1495 |
1496 | .kelvin::after {
1497 | content: "K";
1498 | }
1499 |
1500 | .mired::after {
1501 | content: " Mired";
1502 | }
1503 |
1504 | .percent::after {
1505 | content: "%";
1506 | }
1507 |
1508 | .sdpi-item-value + .icon-cooler,
1509 | .sdpi-item-value + .icon-warmer {
1510 | margin-left: 0px !important;
1511 | margin-top: 15px !important;
1512 | }
1513 |
1514 | /**
1515 | CONTROL-CENTER STYLES
1516 | */
1517 | input[type="range"].colorbrightness::-webkit-slider-runnable-track,
1518 | input[type="range"].colortemperature::-webkit-slider-runnable-track {
1519 | height: 8px;
1520 | background: #979797;
1521 | border-radius: 4px;
1522 | background-image: linear-gradient(to right,#94d0ec, #ffb165);
1523 | }
1524 |
1525 | input[type="range"].colorbrightness::-webkit-slider-runnable-track {
1526 | background-color: #efefef;
1527 | background-image: linear-gradient(to right, black , rgba(0,0,0,0));
1528 | }
1529 |
1530 |
1531 | input[type="range"].colorbrightness::-webkit-slider-thumb,
1532 | input[type="range"].colortemperature::-webkit-slider-thumb {
1533 | width: 16px;
1534 | height: 16px;
1535 | border-radius: 20px;
1536 | margin-top: -5px;
1537 | background-color: #86c6e8;
1538 | box-shadow: 0px 0px 1px #000000;
1539 | border: 1px solid #d8d8d8;
1540 | }
1541 | .sdpi-info-label {
1542 | display: inline-block;
1543 | user-select: none;
1544 | position: absolute;
1545 | height: 15px;
1546 | width: auto;
1547 | text-align: center;
1548 | border-radius: 4px;
1549 | min-width: 44px;
1550 | max-width: 80px;
1551 | background: white;
1552 | font-size: 11px;
1553 | color: black;
1554 | z-index: 1000;
1555 | box-shadow: 0px 0px 12px rgba(0,0,0,.8);
1556 | padding: 2px;
1557 |
1558 | }
1559 |
1560 | .sdpi-info-label.hidden {
1561 | opacity: 0;
1562 | transition: opacity 0.25s linear;
1563 | }
1564 |
1565 | .sdpi-info-label.shown {
1566 | position: absolute;
1567 | opacity: 1;
1568 | transition: opacity 0.25s ease-out;
1569 | }
1570 |
--------------------------------------------------------------------------------
/com.github.sbelectronics.streamdeck.profileswitcher.sdPlugin/defaultImage.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sbelectronics/streamdeck/f0175932e1283d59d50e7410ead230b293fa39cc/com.github.sbelectronics.streamdeck.profileswitcher.sdPlugin/defaultImage.png
--------------------------------------------------------------------------------
/com.github.sbelectronics.streamdeck.profileswitcher.sdPlugin/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sbelectronics/streamdeck/f0175932e1283d59d50e7410ead230b293fa39cc/com.github.sbelectronics.streamdeck.profileswitcher.sdPlugin/icon.png
--------------------------------------------------------------------------------
/com.github.sbelectronics.streamdeck.profileswitcher.sdPlugin/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "Actions": [
3 | {
4 | "Icon": "icon",
5 | "Name": "ProfileSwitcher",
6 | "States": [
7 | {
8 | "Image": "defaultImage",
9 | "TitleAlignment": "middle",
10 | "FontSize": "17"
11 | }
12 | ],
13 | "SupportedInMultiActions": false,
14 | "Tooltip": "Profile switcher",
15 | "UUID": "com.github.sbelectronics.streamdeck.profileswitcher"
16 | }
17 | ],
18 | "SDKVersion": 2,
19 | "Author": "Scott M Baker",
20 | "CodePathWin": "profile_switcher.exe",
21 | "Description": "Advanced Profile Switcher.",
22 | "Name": "profileswitcher",
23 | "Icon": "pluginIcon",
24 | "URL": "https://www.github.com/sbelectronics/streamdeck",
25 | "Version": "1.0",
26 | "OS": [
27 | {
28 | "Platform": "windows",
29 | "MinimumVersion" : "10"
30 | }
31 | ],
32 | "Software":
33 | {
34 | "MinimumVersion" : "4.1"
35 | },
36 | "Profiles": [
37 | {
38 | "Name": "profileswitcher1",
39 | "ReadOnly": true,
40 | "DeviceType": 0
41 | },
42 | {
43 | "Name": "profileswitcher2",
44 | "ReadOnly": true,
45 | "DeviceType": 0
46 | },
47 | {
48 | "Name": "profileswitcher3",
49 | "ReadOnly": true,
50 | "DeviceType": 0
51 | },
52 | {
53 | "Name": "profileswitcher4",
54 | "ReadOnly": true,
55 | "DeviceType": 0
56 | },
57 | {
58 | "Name": "profileswitcher5",
59 | "ReadOnly": true,
60 | "DeviceType": 0
61 | },
62 | {
63 | "Name": "profileswitcher6",
64 | "ReadOnly": true,
65 | "DeviceType": 0
66 | },
67 | {
68 | "Name": "profileswitcher7",
69 | "ReadOnly": true,
70 | "DeviceType": 0
71 | },
72 | {
73 | "Name": "profileswitcher8",
74 | "ReadOnly": true,
75 | "DeviceType": 0
76 | },
77 | {
78 | "Name": "profileswitcher9",
79 | "ReadOnly": true,
80 | "DeviceType": 0
81 | },
82 | {
83 | "Name": "profileswitcher10",
84 | "ReadOnly": true,
85 | "DeviceType": 0
86 | }
87 | ]
88 | }
--------------------------------------------------------------------------------
/com.github.sbelectronics.streamdeck.profileswitcher.sdPlugin/patterns.json:
--------------------------------------------------------------------------------
1 | [{"Regex": "Tinkercad", "Profile": "profileswitcher1"},
2 | {"Regex": "Happy", "Profile": "profileswitcher2"}
3 | ]
--------------------------------------------------------------------------------
/com.github.sbelectronics.streamdeck.profileswitcher.sdPlugin/pluginIcon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sbelectronics/streamdeck/f0175932e1283d59d50e7410ead230b293fa39cc/com.github.sbelectronics.streamdeck.profileswitcher.sdPlugin/pluginIcon.png
--------------------------------------------------------------------------------
/com.github.sbelectronics.streamdeck.profileswitcher.sdPlugin/profile_switcher.exe:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sbelectronics/streamdeck/f0175932e1283d59d50e7410ead230b293fa39cc/com.github.sbelectronics.streamdeck.profileswitcher.sdPlugin/profile_switcher.exe
--------------------------------------------------------------------------------
/com.github.sbelectronics.streamdeck.profileswitcher.sdPlugin/profileswitcher1.streamDeckProfile:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sbelectronics/streamdeck/f0175932e1283d59d50e7410ead230b293fa39cc/com.github.sbelectronics.streamdeck.profileswitcher.sdPlugin/profileswitcher1.streamDeckProfile
--------------------------------------------------------------------------------
/com.github.sbelectronics.streamdeck.profileswitcher.sdPlugin/profileswitcher2.streamDeckProfile:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/sbelectronics/streamdeck/f0175932e1283d59d50e7410ead230b293fa39cc/com.github.sbelectronics.streamdeck.profileswitcher.sdPlugin/profileswitcher2.streamDeckProfile
--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
1 | module github.com/sbelectronics/streamdeck
2 |
3 | go 1.12
4 |
5 | require (
6 | github.com/BurntSushi/graphics-go v0.0.0-20160129215708-b43f31a4a966
7 | github.com/JamesHovious/w32 v1.1.0
8 | github.com/fogleman/gg v1.3.0
9 | github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
10 | github.com/gorilla/websocket v1.4.1
11 | github.com/stretchr/testify v1.4.0
12 | golang.org/x/image v0.0.0-20191206065243-da761ea9ff43 // indirect
13 | )
14 |
--------------------------------------------------------------------------------
/go.sum:
--------------------------------------------------------------------------------
1 | github.com/BurntSushi/graphics-go v0.0.0-20160129215708-b43f31a4a966 h1:lTG4HQym5oPKjL7nGs+csTgiDna685ZXjxijkne828g=
2 | github.com/BurntSushi/graphics-go v0.0.0-20160129215708-b43f31a4a966/go.mod h1:Mid70uvE93zn9wgF92A/r5ixgnvX8Lh68fxp9KQBaI0=
3 | github.com/JamesHovious/w32 v1.1.0 h1:Cghhl7dY4oawL2BvmsuxEb5cPwUw1ctPAq85VIt5qtQ=
4 | github.com/JamesHovious/w32 v1.1.0/go.mod h1:/Y2i/iSYfWJypxp4NnirO9/V19yV2dJvNFCdU0hyW68=
5 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8=
6 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
7 | github.com/fogleman/gg v1.3.0 h1:/7zJX8F6AaYQc57WQCyN9cAIz+4bCJGO9B+dyW29am8=
8 | github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k=
9 | github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 h1:DACJavvAHhabrF08vX0COfcOBJRhZ8lUbR+ZWIs0Y5g=
10 | github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k=
11 | github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM=
12 | github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
13 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
14 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
15 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
16 | github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk=
17 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
18 | golang.org/x/image v0.0.0-20191206065243-da761ea9ff43 h1:gQ6GUSD102fPgli+Yb4cR/cGaHF7tNBt+GYoRCpGC7s=
19 | golang.org/x/image v0.0.0-20191206065243-da761ea9ff43/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
20 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
21 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
22 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
23 | gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw=
24 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
25 |
--------------------------------------------------------------------------------
/pkg/binclock/binclock.go:
--------------------------------------------------------------------------------
1 | /* binclock.go
2 | (c) Scott M Baker, http://www.smbaker.com/
3 |
4 | Binary/BCD Clock implementation in an Image.
5 |
6 | The clock displays hours, minutes, and seconds. Each decimal digit is
7 | encoded as a separate binary number, commonly referred to as binary-coded
8 | decimal.
9 |
10 | For example the time 11:07:53 is represented as:
11 | Hours: 01 0001
12 | Minutes: 000 0111
13 | Seconds: 101 0011
14 |
15 | The most significant digit for hours has only two bits because there are
16 | only 24 hours in a day, and "2" requires two bits. The most sigificany
17 | digit for minutes and seconds has three bits, as it may need to represent
18 | a "5".
19 | */
20 |
21 | package binclock
22 |
23 | import (
24 | "github.com/fogleman/gg"
25 | "image"
26 | "time"
27 | )
28 |
29 | const (
30 | dotSize = 3
31 | width = 72
32 | height = 72
33 | xOffset = 8
34 | yOffset = 22
35 | vOffset = 8
36 | hOffset = 8
37 | space = 24
38 |
39 | DEFAULT_LIT_COLOR = "#0000C8"
40 | DEFAULT_UNLIT_COLOR = "#505050"
41 | DEFAULT_BACK_COLOR = "#000000"
42 | )
43 |
44 | type BinClock struct {
45 | Img *gg.Context
46 | LitDotColor string
47 | UnlitDotColor string
48 | BackColor string
49 | }
50 |
51 | func (bc *BinClock) Create() {
52 | bc.Img = gg.NewContext(width, height)
53 | }
54 |
55 | func (bc *BinClock) DrawDot(x int, y int, lit bool) {
56 | bc.Img.Push()
57 | bc.Img.DrawCircle(float64(x), float64(height-y), float64(dotSize))
58 | if lit {
59 | bc.Img.SetHexColor(bc.LitDotColor)
60 | } else {
61 | bc.Img.SetHexColor(bc.UnlitDotColor)
62 | }
63 | bc.Img.Fill()
64 | bc.Img.Pop()
65 | }
66 |
67 | func (bc *BinClock) DrawNibble(x int, y int, bits int, v int) {
68 | for i := 0; i < bits; i++ {
69 | bc.DrawDot(x, y, (v&1) == 1)
70 | v = v >> 1
71 | y = y + vOffset
72 | }
73 | }
74 |
75 | func (bc *BinClock) DrawDigit(x int, y int, bits int, v int) {
76 | bc.DrawNibble(x, y, bits-4, v/10)
77 | bc.DrawNibble(x+hOffset, y, 4, v%10)
78 | }
79 |
80 | func (bc *BinClock) DrawTime(t time.Time) {
81 | bc.Img.Push()
82 | bc.Img.DrawRectangle(0, 0, width, height)
83 | bc.Img.SetHexColor(bc.BackColor)
84 | bc.Img.Fill()
85 | bc.Img.Pop()
86 |
87 | bc.DrawDigit(xOffset, yOffset, 6, t.Hour())
88 | bc.DrawDigit(xOffset+space, yOffset, 7, t.Minute())
89 | bc.DrawDigit(xOffset+space*2, yOffset, 7, t.Second())
90 | }
91 |
92 | func (bc *BinClock) Image() image.Image {
93 | return bc.Img.Image()
94 | }
95 |
--------------------------------------------------------------------------------
/pkg/binclock/binclock_test.go:
--------------------------------------------------------------------------------
1 | package binclock
2 |
3 | import (
4 | "image/png"
5 | "os"
6 | "testing"
7 | "time"
8 | )
9 |
10 | func TestBinClock(t *testing.T) {
11 | bc := &BinClock{LitDotColor: DEFAULT_LIT_COLOR,
12 | UnlitDotColor: DEFAULT_UNLIT_COLOR,
13 | BackColor: DEFAULT_BACK_COLOR}
14 | bc.Create()
15 |
16 | ts := time.Now()
17 |
18 | bc.DrawTime(ts)
19 |
20 | f, err := os.Create("binclock_test.png")
21 | if err != nil {
22 | t.Fatalf("Failed to create binclock_test.png: %v", err)
23 | }
24 | defer f.Close()
25 |
26 | err = png.Encode(f, bc.Image())
27 | if err != nil {
28 | t.Fatalf("Failed to encode png: %v", err)
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/pkg/globaloptions/globaloptions.go:
--------------------------------------------------------------------------------
1 | /* globaloptions.go
2 | (c) Scott M Baker, http://www.smbaker.com/
3 | */
4 |
5 | package globaloptions
6 |
7 | import (
8 | "flag"
9 | "fmt"
10 | "log"
11 | )
12 |
13 | var (
14 | Port int
15 | PluginUUID string
16 | RegisterEvent string
17 | Info string
18 | Verbose bool
19 |
20 | ForceVerbose = false
21 | )
22 |
23 | func ParseCmdline() {
24 | log.Printf("Loading command line")
25 |
26 | help := fmt.Sprintf("Port number for websocket")
27 | flag.IntVar(&(Port), "port", 0, help)
28 |
29 | help = fmt.Sprintf("Plugin UUID")
30 | flag.StringVar(&(PluginUUID), "pluginUUID", "", help)
31 |
32 | help = fmt.Sprintf("Register Event")
33 | flag.StringVar(&(RegisterEvent), "registerEvent", "", help)
34 |
35 | help = fmt.Sprintf("Info")
36 | flag.StringVar(&(Info), "info", "", help)
37 |
38 | help = fmt.Sprintf("Verbose mode")
39 | flag.BoolVar(&(Verbose), "v", false, help)
40 |
41 | flag.Parse()
42 |
43 | if ForceVerbose {
44 | Verbose = true
45 | }
46 | }
47 |
--------------------------------------------------------------------------------
/pkg/platform/windows/platform_windows.go:
--------------------------------------------------------------------------------
1 | package windows
2 |
3 | import (
4 | "github.com/JamesHovious/w32"
5 | )
6 |
7 | // Get the title of the topmost window
8 | func GetTopmostWindowTitle() (string, error) {
9 | fgw, err := w32.GetForegroundWindow()
10 | if err != nil {
11 | return "", err
12 | }
13 |
14 | windowTitle := make([]uint16, 1024)
15 | _, err = w32.GetWindowTextW(fgw, &windowTitle[0], 1024)
16 | if err != nil {
17 | return "", err
18 | }
19 |
20 | return w32.UTF16PtrToString(&windowTitle[0]), err
21 | }
22 |
--------------------------------------------------------------------------------
/pkg/streamdeck/messages.go:
--------------------------------------------------------------------------------
1 | /* messages.go
2 | (c) Scott M Baker, http://www.smbaker.com/
3 |
4 | JSON message structures, sent from Plugin to Streamdeck
5 | */
6 |
7 | package streamdeck
8 |
9 | const (
10 | TARGET_BOTH = 0
11 | TARGET_HARDWARE = 1
12 | TARGET_SOFTWARE = 2
13 |
14 | TYPE_JPG = "image/jpg"
15 | TYPE_PNG = "image/png"
16 | TYPE_BMP = "image/bmp"
17 | )
18 |
19 | type RegisterMessage struct {
20 | Event string `json:"event"`
21 | Uuid string `json:"uuid"`
22 | }
23 |
24 | type ProfilePayload struct {
25 | Profile string `json:"profile"`
26 | }
27 |
28 | type SwitchProfileMessage struct {
29 | Event string `json:"event"`
30 | Context string `json:"context"`
31 | Device string `json:"device"`
32 | Payload ProfilePayload `json:"payload"`
33 | }
34 |
35 | type SetImagePayload struct {
36 | Image string `json:"image"`
37 | Target int `json:"target"`
38 | }
39 |
40 | type SetImageMessage struct {
41 | Event string `json:"event"`
42 | Context string `json:"context"`
43 | Payload SetImagePayload `json:"payload"`
44 | }
45 |
46 | type SetTitlePayload struct {
47 | Title string `json:"title"`
48 | Target int `json:"target"`
49 | }
50 |
51 | type SetTitleMessage struct {
52 | Event string `json:"event"`
53 | Context string `json:"context"`
54 | Payload SetTitlePayload `json:"payload"`
55 | }
56 |
57 | type ShowAlertMessage struct {
58 | Event string `json:"event"`
59 | Context string `json:"context"`
60 | }
61 |
62 | type ShowOkMessage struct {
63 | Event string `json:"event"`
64 | Context string `json:"context"`
65 | }
66 |
67 | type GetSettingsMessage struct {
68 | Event string `json:"event"`
69 | Context string `json:"context"`
70 | }
71 |
--------------------------------------------------------------------------------
/pkg/streamdeck/notifications.go:
--------------------------------------------------------------------------------
1 | /* messages.go
2 | (c) Scott M Baker, http://www.smbaker.com/
3 |
4 | JSON message structures, sent from Streamdeck to Plugin.
5 | */
6 |
7 | package streamdeck
8 |
9 | type NotificationHeader struct {
10 | Action string `json:"action"`
11 | Event string `json:"event"`
12 | Context string `json:"context"`
13 | Device string `json:"device"`
14 | }
15 |
16 | type Coordinates struct {
17 | Column int `json:"column"`
18 | Row int `json:"row"`
19 | }
20 |
21 | type WillAppearPayload struct {
22 | Coordinates Coordinates `json:"coordinates"`
23 | State int `json:"state"`
24 | IsInMultiAction bool `json:"isInMultiAction"`
25 | }
26 |
27 | type WillAppearNotification struct {
28 | Action string `json:"action"`
29 | Event string `json:"event"`
30 | Context string `json:"context"`
31 | Device string `json:"device"`
32 | Payload WillAppearPayload `json:"payload"`
33 | }
34 |
35 | type WillDisappearNotification struct {
36 | Action string `json:"action"`
37 | Event string `json:"event"`
38 | Context string `json:"context"`
39 | Device string `json:"device"`
40 | Payload WillAppearPayload `json:"payload"`
41 | }
42 |
43 | type KeyPayload struct {
44 | Coordinates Coordinates `json:"coordinates"`
45 | State int `json:"state"`
46 | UserDesiredState bool `json:"userDesiredState"`
47 | IsInMultiAction bool `json:"isInMultiAction"`
48 | }
49 |
50 | type KeyDownNotification struct {
51 | Action string `json:"action"`
52 | Event string `json:"event"`
53 | Context string `json:"context"`
54 | Device string `json:"device"`
55 | Payload KeyPayload `json:"payload"`
56 | }
57 |
58 | type KeyUpNotification struct {
59 | Action string `json:"action"`
60 | Event string `json:"event"`
61 | Context string `json:"context"`
62 | Device string `json:"device"`
63 | Payload KeyPayload `json:"payload"`
64 | }
65 |
66 | type PropertyInspectorDidAppearNotification struct {
67 | Action string `json:"action"`
68 | Event string `json:"event"`
69 | Context string `json:"context"`
70 | Device string `json:"device"`
71 | }
72 |
--------------------------------------------------------------------------------
/pkg/streamdeck/streamdeck.go:
--------------------------------------------------------------------------------
1 | /* streamdeck.go
2 | (c) Scott M Baker, http://www.smbaker.com/
3 |
4 | This is a golang package for interfacing with the ElGato Streamdeck.
5 | See examples in the cmd/ directory for example usage.
6 | */
7 |
8 | package streamdeck
9 |
10 | import (
11 | b64 "encoding/base64"
12 | "encoding/json"
13 | "fmt"
14 | "github.com/gorilla/websocket"
15 | "github.com/sbelectronics/streamdeck/pkg/util"
16 | "log"
17 | )
18 |
19 | type HandlerInterface interface {
20 | OnKeyDown(button *Button)
21 | OnKeyUp(button *Button)
22 | GetDefaultSettings(button *Button)
23 | }
24 |
25 | type Button struct {
26 | Action string
27 | Context string
28 | Column int
29 | Row int
30 | Handler HandlerInterface
31 | Settings map[string]string
32 | StreamDeck *StreamDeck
33 | }
34 |
35 | type StreamDeck struct {
36 | Port int
37 | PluginUUID string
38 | RegisterEvent string
39 | Info string
40 | Verbose bool
41 |
42 | DeviceName string
43 | DeviceId string
44 |
45 | lastProfile string
46 |
47 | Buttons map[string]*Button
48 |
49 | ws *websocket.Conn
50 | defaultHandler HandlerInterface
51 | }
52 |
53 | func (sd *StreamDeck) Init(
54 | port int,
55 | pluginUUID string,
56 | registerEvent string,
57 | info string,
58 | verbose bool,
59 | defaultHandler HandlerInterface) error {
60 | sd.Port = port
61 | sd.PluginUUID = pluginUUID
62 | sd.RegisterEvent = registerEvent
63 | sd.Info = info
64 | sd.Verbose = verbose
65 | sd.Buttons = make(map[string]*Button)
66 | sd.defaultHandler = defaultHandler
67 |
68 | err := sd.decodeInfo()
69 | if err != nil {
70 | return err
71 | }
72 |
73 | sd.Debugf("Port: %d\n", sd.Port)
74 | sd.Debugf("PluginUUID: %s", sd.PluginUUID)
75 | sd.Debugf("RegisterEvent: %s", sd.RegisterEvent)
76 | sd.Debugf("Info %s", sd.Info)
77 | sd.Debugf("Device Name %s", sd.DeviceName)
78 | sd.Debugf("Device Id: %s", sd.DeviceId)
79 |
80 | return nil
81 | }
82 |
83 | func (sd *StreamDeck) Start() error {
84 | err := sd.initWebsocket()
85 | if err != nil {
86 | return err
87 | }
88 | go sd.processWebsocketIncoming()
89 |
90 | err = sd.register()
91 | if err != nil {
92 | return err
93 | }
94 |
95 | return nil
96 | }
97 |
98 | func (sd *StreamDeck) Debugf(fmt string, args ...interface{}) {
99 | if sd.Verbose {
100 | log.Printf(fmt, args...)
101 | }
102 | }
103 |
104 | func (sd *StreamDeck) Infof(fmt string, args ...interface{}) {
105 | log.Printf(fmt, args...)
106 | }
107 |
108 | func (sd *StreamDeck) Errorf(fmt string, args ...interface{}) {
109 | log.Printf(fmt, args...)
110 | }
111 |
112 | func (sd *StreamDeck) Fatalf(fmt string, args ...interface{}) {
113 | // this will cause an os.exit()
114 | log.Fatalf(fmt, args...)
115 | }
116 |
117 | // Extract the device id from the Info JSON
118 | func (sd *StreamDeck) decodeInfo() error {
119 | var f interface{}
120 |
121 | if sd.Info != "" {
122 | err := json.Unmarshal([]byte(sd.Info), &f)
123 | if err != nil {
124 | return err
125 | }
126 |
127 | root := f.(map[string]interface{})
128 | devices := root["devices"].([]interface{})
129 | device := devices[0].(map[string]interface{})
130 |
131 | sd.DeviceName = device["name"].(string)
132 | sd.DeviceId = device["id"].(string)
133 | }
134 |
135 | return nil
136 | }
137 |
138 | func (sd *StreamDeck) initWebsocket() error {
139 | url := fmt.Sprintf("ws://127.0.0.1:%d", sd.Port)
140 | c, _, err := websocket.DefaultDialer.Dial(url, nil)
141 | if err != nil {
142 | return err
143 | }
144 | sd.ws = c
145 | return nil
146 | }
147 |
148 | func (sd *StreamDeck) onWillAppear(message []byte) {
149 | var willAppear WillAppearNotification
150 |
151 | err := json.Unmarshal([]byte(message), &willAppear)
152 | if err != nil {
153 | sd.Errorf("Failed to unmarshal WillAppear")
154 | return
155 | }
156 |
157 | _, ok := sd.Buttons[willAppear.Context]
158 | if ok {
159 |
160 | } else {
161 | newButton := Button{Action: willAppear.Action,
162 | Context: willAppear.Context,
163 | Column: willAppear.Payload.Coordinates.Column,
164 | Row: willAppear.Payload.Coordinates.Row,
165 | Settings: make(map[string]string),
166 | Handler: sd.defaultHandler,
167 | StreamDeck: sd}
168 |
169 | if newButton.Handler != nil {
170 | newButton.Handler.GetDefaultSettings(&newButton)
171 | }
172 |
173 | sd.Buttons[willAppear.Context] = &newButton
174 | sd.Infof("New button %v", newButton)
175 |
176 | // Try to get settings for this button
177 | sd.GetSettings(willAppear.Context)
178 | }
179 | }
180 |
181 | func (sd *StreamDeck) onKeyDown(message []byte) {
182 | var keyDown KeyDownNotification
183 |
184 | err := json.Unmarshal([]byte(message), &keyDown)
185 | if err != nil {
186 | sd.Errorf("Failed to unmarshal KeyDown")
187 | return
188 | }
189 |
190 | button, ok := sd.Buttons[keyDown.Context]
191 | if ok {
192 | if button.Handler != nil {
193 | button.Handler.OnKeyDown(button)
194 | }
195 | }
196 | }
197 |
198 | func (sd *StreamDeck) onKeyUp(message []byte) {
199 | var keyUp KeyUpNotification
200 |
201 | err := json.Unmarshal([]byte(message), &keyUp)
202 | if err != nil {
203 | sd.Errorf("Failed to unmarshal KeyUp")
204 | return
205 | }
206 |
207 | button, ok := sd.Buttons[keyUp.Context]
208 | if ok {
209 | if button.Handler != nil {
210 | button.Handler.OnKeyUp(button)
211 | }
212 | }
213 | }
214 |
215 | func (sd *StreamDeck) onSendToPlugin(message []byte) {
216 | var f interface{}
217 |
218 | sd.Debugf("onSendToPlugin")
219 |
220 | err := json.Unmarshal(message, &f)
221 | if err != nil {
222 | sd.Errorf("Error unmarshaling SendToPlugin: %v", err)
223 | return
224 | }
225 |
226 | root := f.(map[string]interface{})
227 |
228 | context, err := util.StringFromJson(root, "context")
229 | if err != nil {
230 | sd.Errorf("failed to get onSendToPlugin.context: %v", err)
231 | return
232 | }
233 |
234 | payload, err := util.MapInterfaceFromJson(root, "payload")
235 | if err != nil {
236 | sd.Errorf("failed to get onSendToPlugin.context: %v", err)
237 | return
238 | }
239 |
240 | button, ok := sd.Buttons[context]
241 | if !ok {
242 | sd.Errorf("Failed to find SendToPlugin.button %v", context)
243 | return
244 | }
245 |
246 | for k, vInterface := range payload {
247 | v, ok := vInterface.(string)
248 | if !ok {
249 | sd.Errorf("Error converting SendtoPlugin.vInterface %v", vInterface)
250 | continue
251 | }
252 | sd.Infof("Received PropertyInspector k=%s, v=%s", k, v)
253 |
254 | button.Settings[k] = v
255 | }
256 |
257 | // Tell the streamdeck to store the settings persistently
258 | sd.SetSettings(context, button.Settings)
259 | }
260 |
261 | func (sd *StreamDeck) onDidReceiveSettings(message []byte) {
262 | var f interface{}
263 |
264 | sd.Debugf("onDidReceiveSettings")
265 |
266 | err := json.Unmarshal(message, &f)
267 | if err != nil {
268 | sd.Errorf("Error unmarshaling DidReceiveSettings: %v", err)
269 | return
270 | }
271 |
272 | root := f.(map[string]interface{})
273 |
274 | context, err := util.StringFromJson(root, "context")
275 | if err != nil {
276 | sd.Errorf("failed to get onDidReceiveSettings.context: %v", err)
277 | return
278 | }
279 |
280 | payload, err := util.MapInterfaceFromJson(root, "payload")
281 | if err != nil {
282 | sd.Errorf("failed to get onDidReceiveSettings.payload: %v", err)
283 | return
284 | }
285 |
286 | settings, err := util.MapInterfaceFromJson(payload, "settings")
287 | if err != nil {
288 | sd.Errorf("failed to get onDidReceiveSettings.setings: %v", err)
289 | return
290 | }
291 |
292 | button, ok := sd.Buttons[context]
293 | if !ok {
294 | sd.Errorf("Failed to find DidReceiveSettings.button %v", context)
295 | return
296 | }
297 |
298 | for k, vInterface := range settings {
299 | v, ok := vInterface.(string)
300 | if !ok {
301 | sd.Errorf("Error converting DidReceiveSettings.vInterface %v", vInterface)
302 | continue
303 | }
304 | sd.Infof("Received DidReceiveSettings k=%s, v=%s", k, v)
305 |
306 | button.Settings[k] = v
307 | }
308 |
309 | // Tell the streamdeck to store the settings persistently
310 | // (did this in onPropertyInspectorDidAppear instead)
311 | //sd.SendToPropertyInspector(action, context, button.Settings)
312 | }
313 |
314 | func (sd *StreamDeck) onPropertyInspectorDidAppear(message []byte) {
315 | var pda PropertyInspectorDidAppearNotification
316 |
317 | err := json.Unmarshal([]byte(message), &pda)
318 | if err != nil {
319 | sd.Errorf("Failed to unmarshal PropertyInspectorDidAppear")
320 | return
321 | }
322 |
323 | button, ok := sd.Buttons[pda.Context]
324 | if ok {
325 | sd.SendToPropertyInspector(button.Action, button.Context, button.Settings)
326 | }
327 | }
328 |
329 | func (sd *StreamDeck) processWebsocketIncoming() {
330 | for {
331 | _, message, err := sd.ws.ReadMessage()
332 | if err != nil {
333 | sd.Errorf("Read error: %v", err)
334 | sd.Fatalf("FATAL: Websocket has failed -- exiting plugin")
335 | return // this will probably not be executed
336 | }
337 |
338 | sd.Debugf("Websocket Receive: %s", message)
339 |
340 | var header NotificationHeader
341 | err = json.Unmarshal([]byte(message), &header)
342 | if err != nil {
343 | sd.Errorf("Error unmarshaling header %v", err)
344 | continue
345 | }
346 |
347 | sd.Debugf("Websocket Event: %s", header.Event)
348 |
349 | switch header.Event {
350 | case "willAppear":
351 | sd.onWillAppear(message)
352 | case "willDisappear":
353 | case "keyDown":
354 | sd.onKeyDown(message)
355 | case "keyUp":
356 | sd.onKeyUp(message)
357 | case "sendToPlugin":
358 | sd.onSendToPlugin(message)
359 | case "didReceiveSettings":
360 | sd.onDidReceiveSettings(message)
361 | case "propertyInspectorDidAppear":
362 | sd.onPropertyInspectorDidAppear(message)
363 | }
364 | }
365 | }
366 |
367 | func (sd *StreamDeck) SwitchProfile(profile string) error {
368 | msg := SwitchProfileMessage{"switchToProfile",
369 | sd.PluginUUID,
370 | sd.DeviceId,
371 | ProfilePayload{profile}}
372 |
373 | b, err := json.Marshal(msg)
374 | if err != nil {
375 | return err
376 | }
377 |
378 | sd.Debugf("Send JSON %s", string(b))
379 |
380 | err = sd.ws.WriteMessage(websocket.TextMessage, b)
381 | if err != nil {
382 | return err
383 | }
384 |
385 | return nil
386 | }
387 |
388 | func (sd *StreamDeck) SwitchProfileIfChanged(profile string) error {
389 | if sd.lastProfile != profile {
390 | sd.lastProfile = profile
391 | return sd.SwitchProfile(profile)
392 | }
393 | return nil
394 | }
395 |
396 | // untested
397 | func (sd *StreamDeck) SetImage(context string, image []byte, mimeType string, target int) error {
398 | sEnc := b64.StdEncoding.EncodeToString(image)
399 | imageStr := "data:" + mimeType + ";base64," + sEnc
400 |
401 | msg := SetImageMessage{"setImage",
402 | context, //sd.PluginUUID,
403 | SetImagePayload{imageStr, target}}
404 |
405 | b, err := json.Marshal(msg)
406 | if err != nil {
407 | return err
408 | }
409 |
410 | sd.Debugf("Send JSON %s", string(b))
411 |
412 | err = sd.ws.WriteMessage(websocket.TextMessage, b)
413 | if err != nil {
414 | return err
415 | }
416 |
417 | return nil
418 | }
419 |
420 | // untested
421 | func (sd *StreamDeck) SetTitle(context string, title string, target int) error {
422 | msg := SetTitleMessage{"setTitle",
423 | context, //sd.PluginUUID,
424 | SetTitlePayload{title, target}}
425 |
426 | b, err := json.Marshal(msg)
427 | if err != nil {
428 | return err
429 | }
430 |
431 | sd.Debugf("Send JSON %s", string(b))
432 |
433 | err = sd.ws.WriteMessage(websocket.TextMessage, b)
434 | if err != nil {
435 | return err
436 | }
437 |
438 | return nil
439 | }
440 |
441 | func (sd *StreamDeck) ShowAlert(context string) error {
442 | msg := ShowAlertMessage{"showAlert",
443 | context, //sd.PluginUUID,
444 | }
445 |
446 | b, err := json.Marshal(msg)
447 | if err != nil {
448 | return err
449 | }
450 |
451 | sd.Debugf("Send JSON %s", string(b))
452 |
453 | err = sd.ws.WriteMessage(websocket.TextMessage, b)
454 | if err != nil {
455 | return err
456 | }
457 |
458 | return nil
459 | }
460 |
461 | func (sd *StreamDeck) GetSettings(context string) error {
462 | msg := GetSettingsMessage{"getSettings",
463 | context,
464 | }
465 |
466 | b, err := json.Marshal(msg)
467 | if err != nil {
468 | return err
469 | }
470 |
471 | sd.Debugf("Send JSON %s", string(b))
472 |
473 | err = sd.ws.WriteMessage(websocket.TextMessage, b)
474 | if err != nil {
475 | return err
476 | }
477 |
478 | return nil
479 | }
480 |
481 | func (sd *StreamDeck) SetSettings(context string, settings map[string]string) error {
482 | msg := make(map[string]interface{})
483 | msg["event"] = "setSettings"
484 | msg["context"] = context
485 | msg["payload"] = settings
486 |
487 | b, err := json.Marshal(msg)
488 | if err != nil {
489 | return err
490 | }
491 |
492 | sd.Debugf("Send JSON %s", string(b))
493 |
494 | err = sd.ws.WriteMessage(websocket.TextMessage, b)
495 | if err != nil {
496 | return err
497 | }
498 |
499 | return nil
500 | }
501 |
502 | func (sd *StreamDeck) SendToPropertyInspector(action string, context string, settings map[string]string) error {
503 | msg := make(map[string]interface{})
504 | msg["action"] = action
505 | msg["event"] = "sendToPropertyInspector"
506 | msg["context"] = context
507 | msg["payload"] = settings
508 |
509 | b, err := json.Marshal(msg)
510 | if err != nil {
511 | return err
512 | }
513 |
514 | sd.Debugf("Send JSON %s", string(b))
515 |
516 | err = sd.ws.WriteMessage(websocket.TextMessage, b)
517 | if err != nil {
518 | return err
519 | }
520 |
521 | return nil
522 | }
523 |
524 | func (sd *StreamDeck) register() error {
525 | msg := RegisterMessage{sd.RegisterEvent, sd.PluginUUID}
526 |
527 | b, err := json.Marshal(msg)
528 | if err != nil {
529 | return err
530 | }
531 |
532 | sd.Debugf("Send JSON %s", string(b))
533 |
534 | err = sd.ws.WriteMessage(websocket.TextMessage, b)
535 | if err != nil {
536 | return err
537 | }
538 |
539 | return nil
540 | }
541 |
--------------------------------------------------------------------------------
/pkg/streamdeck/streamdeck_test.go:
--------------------------------------------------------------------------------
1 | package streamdeck
2 |
3 | import (
4 | "github.com/stretchr/testify/assert"
5 | "testing"
6 | )
7 |
8 | func TestDeviceInfo(t *testing.T) {
9 | info := `{
10 | "application": {
11 | "language": "en",
12 | "platform": "mac",
13 | "version": "4.1.0"
14 | },
15 | "plugin": {
16 | "version": "1.1"
17 | },
18 | "devicePixelRatio": 2,
19 | "devices": [
20 | {
21 | "id": "55F16B35884A859CCE4FFA1FC8D3DE5B",
22 | "name": "Device Name",
23 | "size": {
24 | "columns": 5,
25 | "rows": 3
26 | },
27 | "type": 0
28 | },
29 | {
30 | "id": "B8F04425B95855CF417199BCB97CD2BB",
31 | "name": "Another Device",
32 | "size": {
33 | "columns": 3,
34 | "rows": 2
35 | },
36 | "type": 1
37 | }
38 | ]
39 | }`
40 |
41 | sd := StreamDeck{}
42 | sd.Init(0, "uuid", "reg", info, false, nil)
43 |
44 | err := sd.decodeInfo()
45 |
46 | assert.Nil(t, err)
47 | assert.Equal(t, "55F16B35884A859CCE4FFA1FC8D3DE5B", sd.DeviceId)
48 | assert.Equal(t, "Device Name", sd.DeviceName)
49 | }
50 |
--------------------------------------------------------------------------------
/pkg/util/util.go:
--------------------------------------------------------------------------------
1 | package util
2 |
3 | import (
4 | "fmt"
5 | neturl "net/url"
6 | "strconv"
7 | "strings"
8 | )
9 |
10 | func StringMapGetDefault(m map[string]string, k string, def string) string {
11 | v, ok := m[k]
12 | if !ok {
13 | v = def
14 | }
15 | return v
16 | }
17 |
18 | func StringFromJson(m map[string]interface{}, k string) (string, error) {
19 |
20 | sInterface, ok := m[k]
21 | if !ok {
22 | return "", fmt.Errorf("Failed to find key %v", k)
23 | }
24 |
25 | s, ok := sInterface.(string)
26 | if !ok {
27 | return "", fmt.Errorf("Failed to convert key %v to string", k)
28 | }
29 |
30 | return s, nil
31 | }
32 |
33 | func MapInterfaceFromJson(m map[string]interface{}, k string) (map[string]interface{}, error) {
34 |
35 | m2Interface, ok := m[k]
36 | if !ok {
37 | return nil, fmt.Errorf("Failed to find key %v", k)
38 | }
39 |
40 | m2, ok := m2Interface.(map[string]interface{})
41 | if !ok {
42 | return nil, fmt.Errorf("Failed to convert key %v to string", k)
43 | }
44 |
45 | return m2, nil
46 | }
47 |
48 | func SanitizeUrl(url string) (string, error) {
49 | // Remove any leading and trailing spaces
50 | url = strings.TrimSpace(url)
51 |
52 | // See if the user put any quotes around it
53 | uqs, err := strconv.Unquote(url)
54 | if err == nil {
55 | url = uqs
56 | }
57 |
58 | // Make sure the URL isn't horibbly misformatted
59 | u, err := neturl.Parse(url)
60 | if err != nil {
61 | return "", fmt.Errorf("Error parsing url %v: %v", url, err)
62 | }
63 |
64 | // In case user forgets to put http:// on the front of the url
65 | if u.Scheme != "http" && u.Scheme != "https" {
66 | url = "http://" + url
67 | }
68 |
69 | return url, nil
70 | }
71 |
--------------------------------------------------------------------------------