├── 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 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 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 |
16 |
URL
17 | 18 |
19 | 20 |
21 |
Operation
22 | 28 |
29 | 30 |
31 | 32 |
Data (for POST/PUT/PATCH)
33 | 34 |
35 |
Data
36 | 37 |
38 |
39 |
Mime Type
40 | 41 |
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 | --------------------------------------------------------------------------------