├── .DS_Store ├── .gitignore ├── .gitmodules ├── LICENSE ├── README.md ├── app.pem ├── app ├── app.html ├── css │ ├── app.css │ └── materialdesignicons.css ├── fonts │ ├── materialdesignicons-webfont.eot │ ├── materialdesignicons-webfont.ttf │ ├── materialdesignicons-webfont.woff │ └── materialdesignicons-webfont.woff2 ├── img │ └── icon.png ├── js │ ├── app.js │ ├── background.js │ ├── remote.js │ └── utils.js ├── manifest.json ├── package.json ├── remote.html ├── ts │ ├── actions.ts │ ├── app.ts │ ├── background.ts │ ├── global.d.ts │ ├── remote.ts │ ├── serial.ts │ ├── store.ts │ ├── utils.ts │ └── view.ts ├── tsconfig.json ├── utils.html ├── webpack.config.js ├── yarn-error.log └── yarn.lock ├── build.sh ├── flash.sh ├── inc ├── api.h ├── app.h ├── btn.h ├── def.h ├── led.h └── net.h ├── lib ├── RGBLed │ └── RGBLed.h └── transport │ └── transport.h ├── platformio.ini └── src ├── main.cpp ├── receiver.h └── transmitter.h /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iphong/esp-visual-led/182fd7f56f7653ed1b7748b784acf2e71dca25e6/.DS_Store -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .pio 2 | .idea 3 | .vscode 4 | .vscode/.browse.c_cpp.db* 5 | .vscode/c_cpp_properties.json 6 | .vscode/launch.json 7 | .vscode/ipch 8 | node_modules 9 | **/dist 10 | **/build 11 | -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "lib/EspRC"] 2 | path = lib/EspRC 3 | url = https://github.com/iphong/lib-esp-rc.git 4 | [submodule "lib/Button"] 5 | path = lib/Button 6 | url = https://github.com/iphong/lib-button.git 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Phong Vu 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [my_github]: https://github.com/iphong "My GitHub URL" 2 | 3 | # ESP VISUAL LED 4 | 5 | These project is focusing on using the ESP8266 to deliver visually complex and beautiful light shows. 6 | 7 | These are a few basic aspects in which we want to achieve in project: 8 | 9 | ### 1. Color Sequence 10 | 11 | Everything begins with a pre-composed light sequences created by show producers based on some audio tracks. A basic light sequences is a timeline of keyframes of color changes throughout the show. A light sequence runs at 1Khz frequency which should updates every milliseconds. A sequence is controlled and rendered individually, and each of these tasks of rendering light sequences from start to end is called an output channel. 12 | 13 | ### 2. Receiver Nodes 14 | 15 | A receiver node is a device module which may have one or more output channels, and should be able to render them simultenously. Multiple receiver can be combined if a large number of output channels are required in a set. 16 | These receivers need to synchronized in realtime. 17 | 18 | ### 3. Master Controller 19 | 20 | A light show may have tens or even hundreds of these output channels. A master computer which play the music track instruct when all receiver should begin playing light show and must be in perfect time sync with milliseconds accuracy throughout all devices. 21 | 22 | *** 23 | 24 | # Building the hardware 25 | 26 | The basic components for a LED driver circuit includes MCU, MOSFET and PSU. Everything needs to be very small as they will be mostly be mounted on instruments and clothes. size of a fullsize sim-card should be ideal. 27 | 28 | ### 1 - MCU 29 | 30 | Any esp8266 modules should be fine. And for this specific project, we use the ESP-12F for several reasons: 31 | 32 | * It has all the GPIO pins and yes, we definitly need all of them 33 | * It has 4MB of flash memory which can be used for OTA and light show uploads 34 | * It has builtin antenna which is good enough for our purpose 35 | * It is nicely packed in a small form factor with metal shields which is FCC certified 36 | * And they are insanely cheap 37 | 38 | ### 2 - PSU 39 | 40 | We need a 3.3v power supply for the ESP8266, a few resistors and a status indicator LED. For 5v power source, a linear LDO voltage regulator should be perfect. For power source greater than 9V, a switch regulator is required to keep the temperature low. 41 | 42 | ### 3 - MOSFET 43 | 44 | Average continuous current drawn of 2-4 amps running at 5V or 12V power sources or from 1-3 cells lithium batteries. 45 | 46 | A typical RGB led has 3 channels representing red, green and blue color. We controls the brightness of each channel using PWM. 47 | Most popular LED strip on the market has 4 pins with common anode terminal which connect directly to the main power source positive terminal. Then we use N-channel MOSFET w/ logic-level gate connect to the kathode terminal of each channels. To control set the gate level to HIGH to turn on the LED. 48 | 49 | There are some other uncommon LED with common kathode configuration which need a P-channel MOSFET to control. To keep things simple, we just going to use the common anode type products. 50 | 51 | *** 52 | 53 | ## Light Show Binary file format (.LSB) 54 | 55 | The content of this file is a sequence of 16 bytes frames. Which has the following structure: 56 | 57 | Type | RED | GREEN | BLUE | START | DURATION | TRANSITION 58 | ---- | --- | ----- | ---- | ----- | -------- | ---------- 59 | UINT8(1) | UINT8(1) | UINT8(1) | UINT8(1) | UINT32(4) | UINT32(4) | UINT32(4) 60 | 61 | #### Frame types 62 | 63 | * 0x01 - RGB FRAME 64 | * 0x02 - END FRAME 65 | * 0x03 - LOOP FRAME 66 | 67 | ## GPIO Pin Mappings 68 | 69 | Function | Pin # 70 | ---------|------ 71 | Battery Voltage | ***ADC0*** 72 | Setup Button | ***GPIO0*** 73 | Status LED | ***GPIO2*** 74 | Serial TX | ***GPIO1*** 75 | Serial RX | ***GPIO3*** 76 | Channel 1 Green | ***GPIO12*** 77 | Channel 1 Red | ***GPIO13*** 78 | Channel 1 Blue | ***GPIO14*** 79 | Channel 2 Green | ***GPIO15*** 80 | Channel 2 Red | ***GPIO5*** 81 | Channel 2 Blue | ***GPIO4*** 82 | -------------------------------------------------------------------------------- /app.pem: -------------------------------------------------------------------------------- 1 | -----BEGIN PRIVATE KEY----- 2 | MIIEvAIBADANBgkqhkiG9w0BAQEFAASCBKYwggSiAgEAAoIBAQDMHJ3norw61sjx 3 | MxMhSLl3lEEpBzRgQvoqCPQgbCEDA3bdJolerxaeHCQyVt9hQJ3zOefZ3SJF62ik 4 | rnlui07iDEGgmT3iHmhyZTbZFPNspWSWkvqIqHlEtrbURBv8XJBA/IIUkNwn4IbC 5 | XmqdqR9/7/gn3QFGkkil0GSc3Ir0ZEjCoZtTVRNa6ICppAlxrAkURrhpG7SWuIpX 6 | uU28wnm1jUE4OX0b7YaI3tXgBX6VTZiEdumlhuRsM3sXK9yHBLCyaSbdhBLxDtIA 7 | cswbLNgeiImfFl7djyq3jQ3ux3dAxB3hO9cxmxZxpXgnJGfrI7nYPwpEvUOYG28S 8 | UFWygqHZAgMBAAECggEAOQT1h7QKaVV/JQJjobSzOxiLa267zvhm8j82E6ihQDpD 9 | hlwxm+XFp72szvM9y+lFNqFCh7Yi0O2GehrTiXIZQ0SxQLAOfZCJFQv5WoNjzUIW 10 | 64l7u5l05yEzyfCM8N7a3YysuhoNpoYunzXJCuooBpF+/YPby18qNEgXE0r49nRd 11 | t3Xg40S5kj94k7MNNzhMx1WmBi0UbA6c5NG/YTa27wLvzlNbetXXydSGgmL9zdaY 12 | eb0ot3ICOvPO6iZzhWbSHwLS6G0hvxe7Y/eT7BHbugw0/T9YcS+i/vK1plC0Bwz/ 13 | 9oPFdAZ5krcPvNfZRq2eV1+FZx158Fir8dodGHr9kQKBgQDluBnhebfwnLbmbqP1 14 | que8qU3Vu2JJDLvnbfOEfWNKJlLgV8TPyR0PgaFcwRs3tDNFGO6GRc03p70kKMe7 15 | 3pbT/1ofKx8WcCfk622WL7UeN2sgUBxxdbEF+evxN9nOiq3sguwGosXSEKzhJ6Jt 16 | Rz/8KfkorpeiznadlKTHOXh6gwKBgQDjdopLQbyAcXDSZcsY1Z+bdGQIJ8ezyXuk 17 | 70XeA0WRIur4xR3mym8bXE7c+Wt6C5r+How2kGdgLu2PZR2EqoWVePz+QT5rPtNt 18 | VofsxBEYF+Nsa5vTjsuSVLu6NEtsiDVOAyWH2XJ4GV6TQIGdJIxcdRWoODJ6R/UT 19 | kSlcqD6zcwKBgH3mLlkC6qq2WQ9lp/qmVidx6rSu5CkBD6LBAeulBNvIsTc/IyB2 20 | KrUq6JL7Sr12x3qhNWjlrJlKF0FQEFeIoMVDd9MJQRp9EYBG/2KGdw8+dDnbbhtI 21 | 02JhHMyxPXATVUsAXfctEpoUhYtIu56EpC7BmkqPlY+m9B9dLgn6F2udAoGASs1A 22 | zV7gzpx/rbEsCQ63Xjf4bXYnhkhMEQFeyJPq3L/O5eBs5OjNgQHqLWEYpxoJ7me9 23 | VHRIiqjkFrP58RbitzUCfdqW3E3c2agLKyGPPY0djRoWNIxRBd43nhR0eUyRuwXt 24 | 4a7wpe4x29rqxPKv9ffLF3bjorLnNXgXUhFCDWECgYAi5HPTqEXBDmElDJSs713e 25 | HgxkP8RtU0PT3i8K5Jozrq67WwNJ+xDpLsnJgBazP/ZHCzNnVozmfgzLuEB6kO1Z 26 | VDxSuaAdqqLHtlnfLM/UP3rrYlpUUEYJ2ZEIjm7FHggnRkrQndt5p5PUITxHnXow 27 | bWagch2YY6QQJqBo+I2WCw== 28 | -----END PRIVATE KEY----- 29 | -------------------------------------------------------------------------------- /app/app.html: -------------------------------------------------------------------------------- 1 | 2 | SDC LED TECHNOLOGY 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 53 |
54 |
55 |
56 |
57 |
58 |
59 | 60 |
61 | 62 | 63 | -------------------------------------------------------------------------------- /app/css/app.css: -------------------------------------------------------------------------------- 1 | 2 | * { 3 | -webkit-tap-highlight-color: rgba(0, 0, 0, 0); 4 | font-family: "system-ui", "Helvetica Neue", Verdana, Geneva, Tahoma, sans-serif; 5 | } 6 | 7 | *:not(input) { 8 | user-select: none; 9 | -webkit-user-select: none; 10 | -khtml-user-select: none; 11 | -moz-user-select: none; 12 | -ms-user-select: none; 13 | } 14 | 15 | header { 16 | -webkit-app-region: drag; 17 | } 18 | 19 | button, input, select, label { 20 | -webkit-app-region: no-drag; 21 | vertical-align: middle; 22 | } 23 | 24 | html { 25 | height: 100%; 26 | width: 100%; 27 | padding: 0; 28 | margin: 0; 29 | } 30 | 31 | body { 32 | display: grid; 33 | font-size: 14px; 34 | font-family: system-ui, serif; 35 | grid-template-rows: 60px auto; 36 | background: black; 37 | color: white; 38 | height: 100%; 39 | width: 100%; 40 | margin: 0; 41 | padding: 0; 42 | } 43 | 44 | button { 45 | font-size: 14px; 46 | white-space: nowrap; 47 | outline: none; 48 | transition: all ease 0.1s; 49 | } 50 | button.mdi { 51 | font-size: 20px; 52 | } 53 | 54 | nav { 55 | display: flex; 56 | flex-wrap: nowrap; 57 | } 58 | 59 | nav > * { 60 | vertical-align: middle; 61 | margin-left: 2px; 62 | } 63 | button, select { 64 | vertical-align: middle; 65 | height: 35px; 66 | min-width: 40px; 67 | padding: 0 10px; 68 | background: rgba(255,255,255,0.05); 69 | border: 1px solid rgba(0,0,0,0.1); 70 | color: white; 71 | -webkit-appearance: none; 72 | outline: none; 73 | border-radius: 5px; 74 | font-weight: 500; 75 | } 76 | button { 77 | } 78 | button:focus, input:focus, select:focus { 79 | 80 | } 81 | 82 | button:hover { 83 | text-shadow: 0 0 10px #c000ff; 84 | background: rgba(255,255,255,0.1); 85 | } 86 | button:active, 87 | button.selected { 88 | background: rgba(0,0,0,0.2); 89 | } 90 | button.selected { 91 | color: #d143ff; 92 | text-shadow: 0 0 10px #c000ff; 93 | } 94 | 95 | button[disabled], 96 | select[disabled] { 97 | opacity: 0.5; 98 | pointer-events: none; 99 | } 100 | select { 101 | max-width: 150px; 102 | padding: 2px 10px 0px 13px; 103 | } 104 | 105 | #header { 106 | position: relative; 107 | display: grid; 108 | padding: 5px 20px; 109 | grid-gap: 10px; 110 | background: #222326; 111 | grid-template-columns: 120px 1fr 2fr 4fr auto; 112 | align-items: center; 113 | justify-content: center; 114 | } 115 | #logo { 116 | text-shadow: 0 0 10px #bd00f1; 117 | } 118 | #logo strong { 119 | font-size: 30px; 120 | display: block; 121 | line-height: 1em; 122 | } 123 | 124 | #logo small { 125 | display: block; 126 | font-size: small; 127 | letter-spacing: 0.5em; 128 | line-height: 1em; 129 | } 130 | 131 | #main { 132 | position: relative; 133 | width: 100%; 134 | display: grid; 135 | /* grid-template-rows: auto 20px 60px; */ 136 | grid-template-rows: 30px auto 0px 0px; 137 | overflow-x: hidden; 138 | background: #191919; 139 | } 140 | 141 | #tracks { 142 | display: grid; 143 | padding: 2px 0; 144 | grid-gap: 2px; 145 | box-sizing: border-box; 146 | /* background: url(../img/checker.jpg); */ 147 | } 148 | 149 | #tracks .track { 150 | position: relative; 151 | background: #111; 152 | } 153 | 154 | #tracks .track .index { 155 | position: absolute; 156 | left: 0; 157 | top: 0; 158 | bottom: 0; 159 | display: flex; 160 | z-index: 10; 161 | color: white; 162 | text-shadow: 0 5px 5px #000; 163 | width: 30px; 164 | align-items: center; 165 | padding-left: 10px; 166 | } 167 | 168 | #tracks .track > span { 169 | display: block; 170 | position: absolute; 171 | /* top: 50%; */ 172 | height: 100%; 173 | /* max-height: 20px; */ 174 | /* box-sizing: border-box; */ 175 | /* transform: translateY(-50%); */ 176 | /* border-radius: 0px; */ 177 | } 178 | #tracks .track:focus { 179 | z-index: 2; 180 | } 181 | #tracks .track.selected { 182 | /* background: #b905ff66; */ 183 | } 184 | #tracks .track.selected:before { 185 | content: ""; 186 | display: block; 187 | position: absolute; 188 | left: 0; 189 | top: 0; 190 | height: 100%; 191 | width: 100%; 192 | background: rgba(36, 36, 37, 0.712); 193 | outline: 1px solid rgb(255, 255, 255); 194 | outline-offset: 1px; 195 | z-index: 5; 196 | } 197 | 198 | #timeline { 199 | background: #111; 200 | color: white; 201 | white-space: nowrap; 202 | overflow: hidden; 203 | } 204 | #timeline span { 205 | height: 100%; 206 | display: inline-flex; 207 | align-items: center; 208 | font-size: 10px; 209 | border-left: 1px dashed #ffffff2e; 210 | margin-left: -1px; 211 | padding-left: 3px; 212 | color: #ffffff; 213 | } 214 | 215 | #tempo { 216 | white-space: nowrap; 217 | display: none; 218 | } 219 | 220 | #waveform { 221 | background: #222326; 222 | display: none; 223 | } 224 | 225 | #handle { 226 | position: fixed; 227 | left: 0; 228 | top: 60px; 229 | bottom: 0px; 230 | /* height: 34px; */ 231 | width: 2px; 232 | /* border-radius: 50%; */ 233 | background-color: white; 234 | box-shadow: 0px 0px 2px 2px #673ab7, 0px 0px 10px 2px #9c27b0; 235 | transform: translateX(-100%); 236 | z-index: 20; 237 | } 238 | 239 | .block { 240 | display: inline-block; 241 | height: 100%; 242 | border: 1px solid #222; 243 | box-sizing: border-box; 244 | background-color: #000; 245 | font-size: 12px; 246 | line-height: 18px; 247 | text-align: center; 248 | font-weight: bold; 249 | color: white; 250 | } 251 | -------------------------------------------------------------------------------- /app/fonts/materialdesignicons-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iphong/esp-visual-led/182fd7f56f7653ed1b7748b784acf2e71dca25e6/app/fonts/materialdesignicons-webfont.eot -------------------------------------------------------------------------------- /app/fonts/materialdesignicons-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iphong/esp-visual-led/182fd7f56f7653ed1b7748b784acf2e71dca25e6/app/fonts/materialdesignicons-webfont.ttf -------------------------------------------------------------------------------- /app/fonts/materialdesignicons-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iphong/esp-visual-led/182fd7f56f7653ed1b7748b784acf2e71dca25e6/app/fonts/materialdesignicons-webfont.woff -------------------------------------------------------------------------------- /app/fonts/materialdesignicons-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iphong/esp-visual-led/182fd7f56f7653ed1b7748b784acf2e71dca25e6/app/fonts/materialdesignicons-webfont.woff2 -------------------------------------------------------------------------------- /app/img/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/iphong/esp-visual-led/182fd7f56f7653ed1b7748b784acf2e71dca25e6/app/img/icon.png -------------------------------------------------------------------------------- /app/js/background.js: -------------------------------------------------------------------------------- 1 | /******/ (function(modules) { // webpackBootstrap 2 | /******/ // The module cache 3 | /******/ var installedModules = {}; 4 | /******/ 5 | /******/ // The require function 6 | /******/ function __webpack_require__(moduleId) { 7 | /******/ 8 | /******/ // Check if module is in cache 9 | /******/ if(installedModules[moduleId]) { 10 | /******/ return installedModules[moduleId].exports; 11 | /******/ } 12 | /******/ // Create a new module (and put it into the cache) 13 | /******/ var module = installedModules[moduleId] = { 14 | /******/ i: moduleId, 15 | /******/ l: false, 16 | /******/ exports: {} 17 | /******/ }; 18 | /******/ 19 | /******/ // Execute the module function 20 | /******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); 21 | /******/ 22 | /******/ // Flag the module as loaded 23 | /******/ module.l = true; 24 | /******/ 25 | /******/ // Return the exports of the module 26 | /******/ return module.exports; 27 | /******/ } 28 | /******/ 29 | /******/ 30 | /******/ // expose the modules object (__webpack_modules__) 31 | /******/ __webpack_require__.m = modules; 32 | /******/ 33 | /******/ // expose the module cache 34 | /******/ __webpack_require__.c = installedModules; 35 | /******/ 36 | /******/ // define getter function for harmony exports 37 | /******/ __webpack_require__.d = function(exports, name, getter) { 38 | /******/ if(!__webpack_require__.o(exports, name)) { 39 | /******/ Object.defineProperty(exports, name, { enumerable: true, get: getter }); 40 | /******/ } 41 | /******/ }; 42 | /******/ 43 | /******/ // define __esModule on exports 44 | /******/ __webpack_require__.r = function(exports) { 45 | /******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) { 46 | /******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); 47 | /******/ } 48 | /******/ Object.defineProperty(exports, '__esModule', { value: true }); 49 | /******/ }; 50 | /******/ 51 | /******/ // create a fake namespace object 52 | /******/ // mode & 1: value is a module id, require it 53 | /******/ // mode & 2: merge all properties of value into the ns 54 | /******/ // mode & 4: return value when already ns object 55 | /******/ // mode & 8|1: behave like require 56 | /******/ __webpack_require__.t = function(value, mode) { 57 | /******/ if(mode & 1) value = __webpack_require__(value); 58 | /******/ if(mode & 8) return value; 59 | /******/ if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value; 60 | /******/ var ns = Object.create(null); 61 | /******/ __webpack_require__.r(ns); 62 | /******/ Object.defineProperty(ns, 'default', { enumerable: true, value: value }); 63 | /******/ if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key)); 64 | /******/ return ns; 65 | /******/ }; 66 | /******/ 67 | /******/ // getDefaultExport function for compatibility with non-harmony modules 68 | /******/ __webpack_require__.n = function(module) { 69 | /******/ var getter = module && module.__esModule ? 70 | /******/ function getDefault() { return module['default']; } : 71 | /******/ function getModuleExports() { return module; }; 72 | /******/ __webpack_require__.d(getter, 'a', getter); 73 | /******/ return getter; 74 | /******/ }; 75 | /******/ 76 | /******/ // Object.prototype.hasOwnProperty.call 77 | /******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); }; 78 | /******/ 79 | /******/ // __webpack_public_path__ 80 | /******/ __webpack_require__.p = "./js/"; 81 | /******/ 82 | /******/ 83 | /******/ // Load entry module and return exports 84 | /******/ return __webpack_require__(__webpack_require__.s = "./ts/background.ts"); 85 | /******/ }) 86 | /************************************************************************/ 87 | /******/ ({ 88 | 89 | /***/ "./ts/background.ts": 90 | /*!**************************!*\ 91 | !*** ./ts/background.ts ***! 92 | \**************************/ 93 | /*! no static exports found */ 94 | /***/ (function(module, exports) { 95 | 96 | chrome['app'].runtime.onLaunched.addListener(function () { 97 | chrome['app'].window.create('../app.html', { 98 | id: 'app', 99 | frame: "none", 100 | minWidth: 900, 101 | minHeight: 100 102 | }); 103 | }); 104 | 105 | 106 | /***/ }) 107 | 108 | /******/ }); 109 | //# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIndlYnBhY2s6Ly8vd2VicGFjay9ib290c3RyYXAiLCJ3ZWJwYWNrOi8vLy4vdHMvYmFja2dyb3VuZC50cyJdLCJuYW1lcyI6W10sIm1hcHBpbmdzIjoiO1FBQUE7UUFDQTs7UUFFQTtRQUNBOztRQUVBO1FBQ0E7UUFDQTtRQUNBO1FBQ0E7UUFDQTtRQUNBO1FBQ0E7UUFDQTtRQUNBOztRQUVBO1FBQ0E7O1FBRUE7UUFDQTs7UUFFQTtRQUNBO1FBQ0E7OztRQUdBO1FBQ0E7O1FBRUE7UUFDQTs7UUFFQTtRQUNBO1FBQ0E7UUFDQSwwQ0FBMEMsZ0NBQWdDO1FBQzFFO1FBQ0E7O1FBRUE7UUFDQTtRQUNBO1FBQ0Esd0RBQXdELGtCQUFrQjtRQUMxRTtRQUNBLGlEQUFpRCxjQUFjO1FBQy9EOztRQUVBO1FBQ0E7UUFDQTtRQUNBO1FBQ0E7UUFDQTtRQUNBO1FBQ0E7UUFDQTtRQUNBO1FBQ0E7UUFDQSx5Q0FBeUMsaUNBQWlDO1FBQzFFLGdIQUFnSCxtQkFBbUIsRUFBRTtRQUNySTtRQUNBOztRQUVBO1FBQ0E7UUFDQTtRQUNBLDJCQUEyQiwwQkFBMEIsRUFBRTtRQUN2RCxpQ0FBaUMsZUFBZTtRQUNoRDtRQUNBO1FBQ0E7O1FBRUE7UUFDQSxzREFBc0QsK0RBQStEOztRQUVySDtRQUNBOzs7UUFHQTtRQUNBOzs7Ozs7Ozs7Ozs7QUNsRkEsTUFBTSxDQUFDLEtBQUssQ0FBQyxDQUFDLE9BQU8sQ0FBQyxVQUFVLENBQUMsV0FBVyxDQUFDO0lBQzVDLE1BQU0sQ0FBQyxLQUFLLENBQUMsQ0FBQyxNQUFNLENBQUMsTUFBTSxDQUFDLGFBQWEsRUFBRTtRQUMxQyxFQUFFLEVBQUUsS0FBSztRQUNULEtBQUssRUFBRSxNQUFNO1FBQ2IsUUFBUSxFQUFFLEdBQUc7UUFDYixTQUFTLEVBQUUsR0FBRztLQUNkLENBQUMsQ0FBQztBQUNKLENBQUMsQ0FBQyxDQUFDIiwiZmlsZSI6ImJhY2tncm91bmQuanMiLCJzb3VyY2VzQ29udGVudCI6WyIgXHQvLyBUaGUgbW9kdWxlIGNhY2hlXG4gXHR2YXIgaW5zdGFsbGVkTW9kdWxlcyA9IHt9O1xuXG4gXHQvLyBUaGUgcmVxdWlyZSBmdW5jdGlvblxuIFx0ZnVuY3Rpb24gX193ZWJwYWNrX3JlcXVpcmVfXyhtb2R1bGVJZCkge1xuXG4gXHRcdC8vIENoZWNrIGlmIG1vZHVsZSBpcyBpbiBjYWNoZVxuIFx0XHRpZihpbnN0YWxsZWRNb2R1bGVzW21vZHVsZUlkXSkge1xuIFx0XHRcdHJldHVybiBpbnN0YWxsZWRNb2R1bGVzW21vZHVsZUlkXS5leHBvcnRzO1xuIFx0XHR9XG4gXHRcdC8vIENyZWF0ZSBhIG5ldyBtb2R1bGUgKGFuZCBwdXQgaXQgaW50byB0aGUgY2FjaGUpXG4gXHRcdHZhciBtb2R1bGUgPSBpbnN0YWxsZWRNb2R1bGVzW21vZHVsZUlkXSA9IHtcbiBcdFx0XHRpOiBtb2R1bGVJZCxcbiBcdFx0XHRsOiBmYWxzZSxcbiBcdFx0XHRleHBvcnRzOiB7fVxuIFx0XHR9O1xuXG4gXHRcdC8vIEV4ZWN1dGUgdGhlIG1vZHVsZSBmdW5jdGlvblxuIFx0XHRtb2R1bGVzW21vZHVsZUlkXS5jYWxsKG1vZHVsZS5leHBvcnRzLCBtb2R1bGUsIG1vZHVsZS5leHBvcnRzLCBfX3dlYnBhY2tfcmVxdWlyZV9fKTtcblxuIFx0XHQvLyBGbGFnIHRoZSBtb2R1bGUgYXMgbG9hZGVkXG4gXHRcdG1vZHVsZS5sID0gdHJ1ZTtcblxuIFx0XHQvLyBSZXR1cm4gdGhlIGV4cG9ydHMgb2YgdGhlIG1vZHVsZVxuIFx0XHRyZXR1cm4gbW9kdWxlLmV4cG9ydHM7XG4gXHR9XG5cblxuIFx0Ly8gZXhwb3NlIHRoZSBtb2R1bGVzIG9iamVjdCAoX193ZWJwYWNrX21vZHVsZXNfXylcbiBcdF9fd2VicGFja19yZXF1aXJlX18ubSA9IG1vZHVsZXM7XG5cbiBcdC8vIGV4cG9zZSB0aGUgbW9kdWxlIGNhY2hlXG4gXHRfX3dlYnBhY2tfcmVxdWlyZV9fLmMgPSBpbnN0YWxsZWRNb2R1bGVzO1xuXG4gXHQvLyBkZWZpbmUgZ2V0dGVyIGZ1bmN0aW9uIGZvciBoYXJtb255IGV4cG9ydHNcbiBcdF9fd2VicGFja19yZXF1aXJlX18uZCA9IGZ1bmN0aW9uKGV4cG9ydHMsIG5hbWUsIGdldHRlcikge1xuIFx0XHRpZighX193ZWJwYWNrX3JlcXVpcmVfXy5vKGV4cG9ydHMsIG5hbWUpKSB7XG4gXHRcdFx0T2JqZWN0LmRlZmluZVByb3BlcnR5KGV4cG9ydHMsIG5hbWUsIHsgZW51bWVyYWJsZTogdHJ1ZSwgZ2V0OiBnZXR0ZXIgfSk7XG4gXHRcdH1cbiBcdH07XG5cbiBcdC8vIGRlZmluZSBfX2VzTW9kdWxlIG9uIGV4cG9ydHNcbiBcdF9fd2VicGFja19yZXF1aXJlX18uciA9IGZ1bmN0aW9uKGV4cG9ydHMpIHtcbiBcdFx0aWYodHlwZW9mIFN5bWJvbCAhPT0gJ3VuZGVmaW5lZCcgJiYgU3ltYm9sLnRvU3RyaW5nVGFnKSB7XG4gXHRcdFx0T2JqZWN0LmRlZmluZVByb3BlcnR5KGV4cG9ydHMsIFN5bWJvbC50b1N0cmluZ1RhZywgeyB2YWx1ZTogJ01vZHVsZScgfSk7XG4gXHRcdH1cbiBcdFx0T2JqZWN0LmRlZmluZVByb3BlcnR5KGV4cG9ydHMsICdfX2VzTW9kdWxlJywgeyB2YWx1ZTogdHJ1ZSB9KTtcbiBcdH07XG5cbiBcdC8vIGNyZWF0ZSBhIGZha2UgbmFtZXNwYWNlIG9iamVjdFxuIFx0Ly8gbW9kZSAmIDE6IHZhbHVlIGlzIGEgbW9kdWxlIGlkLCByZXF1aXJlIGl0XG4gXHQvLyBtb2RlICYgMjogbWVyZ2UgYWxsIHByb3BlcnRpZXMgb2YgdmFsdWUgaW50byB0aGUgbnNcbiBcdC8vIG1vZGUgJiA0OiByZXR1cm4gdmFsdWUgd2hlbiBhbHJlYWR5IG5zIG9iamVjdFxuIFx0Ly8gbW9kZSAmIDh8MTogYmVoYXZlIGxpa2UgcmVxdWlyZVxuIFx0X193ZWJwYWNrX3JlcXVpcmVfXy50ID0gZnVuY3Rpb24odmFsdWUsIG1vZGUpIHtcbiBcdFx0aWYobW9kZSAmIDEpIHZhbHVlID0gX193ZWJwYWNrX3JlcXVpcmVfXyh2YWx1ZSk7XG4gXHRcdGlmKG1vZGUgJiA4KSByZXR1cm4gdmFsdWU7XG4gXHRcdGlmKChtb2RlICYgNCkgJiYgdHlwZW9mIHZhbHVlID09PSAnb2JqZWN0JyAmJiB2YWx1ZSAmJiB2YWx1ZS5fX2VzTW9kdWxlKSByZXR1cm4gdmFsdWU7XG4gXHRcdHZhciBucyA9IE9iamVjdC5jcmVhdGUobnVsbCk7XG4gXHRcdF9fd2VicGFja19yZXF1aXJlX18ucihucyk7XG4gXHRcdE9iamVjdC5kZWZpbmVQcm9wZXJ0eShucywgJ2RlZmF1bHQnLCB7IGVudW1lcmFibGU6IHRydWUsIHZhbHVlOiB2YWx1ZSB9KTtcbiBcdFx0aWYobW9kZSAmIDIgJiYgdHlwZW9mIHZhbHVlICE9ICdzdHJpbmcnKSBmb3IodmFyIGtleSBpbiB2YWx1ZSkgX193ZWJwYWNrX3JlcXVpcmVfXy5kKG5zLCBrZXksIGZ1bmN0aW9uKGtleSkgeyByZXR1cm4gdmFsdWVba2V5XTsgfS5iaW5kKG51bGwsIGtleSkpO1xuIFx0XHRyZXR1cm4gbnM7XG4gXHR9O1xuXG4gXHQvLyBnZXREZWZhdWx0RXhwb3J0IGZ1bmN0aW9uIGZvciBjb21wYXRpYmlsaXR5IHdpdGggbm9uLWhhcm1vbnkgbW9kdWxlc1xuIFx0X193ZWJwYWNrX3JlcXVpcmVfXy5uID0gZnVuY3Rpb24obW9kdWxlKSB7XG4gXHRcdHZhciBnZXR0ZXIgPSBtb2R1bGUgJiYgbW9kdWxlLl9fZXNNb2R1bGUgP1xuIFx0XHRcdGZ1bmN0aW9uIGdldERlZmF1bHQoKSB7IHJldHVybiBtb2R1bGVbJ2RlZmF1bHQnXTsgfSA6XG4gXHRcdFx0ZnVuY3Rpb24gZ2V0TW9kdWxlRXhwb3J0cygpIHsgcmV0dXJuIG1vZHVsZTsgfTtcbiBcdFx0X193ZWJwYWNrX3JlcXVpcmVfXy5kKGdldHRlciwgJ2EnLCBnZXR0ZXIpO1xuIFx0XHRyZXR1cm4gZ2V0dGVyO1xuIFx0fTtcblxuIFx0Ly8gT2JqZWN0LnByb3RvdHlwZS5oYXNPd25Qcm9wZXJ0eS5jYWxsXG4gXHRfX3dlYnBhY2tfcmVxdWlyZV9fLm8gPSBmdW5jdGlvbihvYmplY3QsIHByb3BlcnR5KSB7IHJldHVybiBPYmplY3QucHJvdG90eXBlLmhhc093blByb3BlcnR5LmNhbGwob2JqZWN0LCBwcm9wZXJ0eSk7IH07XG5cbiBcdC8vIF9fd2VicGFja19wdWJsaWNfcGF0aF9fXG4gXHRfX3dlYnBhY2tfcmVxdWlyZV9fLnAgPSBcIi4vanMvXCI7XG5cblxuIFx0Ly8gTG9hZCBlbnRyeSBtb2R1bGUgYW5kIHJldHVybiBleHBvcnRzXG4gXHRyZXR1cm4gX193ZWJwYWNrX3JlcXVpcmVfXyhfX3dlYnBhY2tfcmVxdWlyZV9fLnMgPSBcIi4vdHMvYmFja2dyb3VuZC50c1wiKTtcbiIsImNocm9tZVsnYXBwJ10ucnVudGltZS5vbkxhdW5jaGVkLmFkZExpc3RlbmVyKGZ1bmN0aW9uICgpIHtcblx0Y2hyb21lWydhcHAnXS53aW5kb3cuY3JlYXRlKCcuLi9hcHAuaHRtbCcsIHtcblx0XHRpZDogJ2FwcCcsXG5cdFx0ZnJhbWU6IFwibm9uZVwiLFxuXHRcdG1pbldpZHRoOiA5MDAsXG5cdFx0bWluSGVpZ2h0OiAxMDBcblx0fSk7XG59KTtcbiJdLCJzb3VyY2VSb290IjoiIn0= -------------------------------------------------------------------------------- /app/js/remote.js: -------------------------------------------------------------------------------- 1 | /******/ (function(modules) { // webpackBootstrap 2 | /******/ // The module cache 3 | /******/ var installedModules = {}; 4 | /******/ 5 | /******/ // The require function 6 | /******/ function __webpack_require__(moduleId) { 7 | /******/ 8 | /******/ // Check if module is in cache 9 | /******/ if(installedModules[moduleId]) { 10 | /******/ return installedModules[moduleId].exports; 11 | /******/ } 12 | /******/ // Create a new module (and put it into the cache) 13 | /******/ var module = installedModules[moduleId] = { 14 | /******/ i: moduleId, 15 | /******/ l: false, 16 | /******/ exports: {} 17 | /******/ }; 18 | /******/ 19 | /******/ // Execute the module function 20 | /******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); 21 | /******/ 22 | /******/ // Flag the module as loaded 23 | /******/ module.l = true; 24 | /******/ 25 | /******/ // Return the exports of the module 26 | /******/ return module.exports; 27 | /******/ } 28 | /******/ 29 | /******/ 30 | /******/ // expose the modules object (__webpack_modules__) 31 | /******/ __webpack_require__.m = modules; 32 | /******/ 33 | /******/ // expose the module cache 34 | /******/ __webpack_require__.c = installedModules; 35 | /******/ 36 | /******/ // define getter function for harmony exports 37 | /******/ __webpack_require__.d = function(exports, name, getter) { 38 | /******/ if(!__webpack_require__.o(exports, name)) { 39 | /******/ Object.defineProperty(exports, name, { enumerable: true, get: getter }); 40 | /******/ } 41 | /******/ }; 42 | /******/ 43 | /******/ // define __esModule on exports 44 | /******/ __webpack_require__.r = function(exports) { 45 | /******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) { 46 | /******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); 47 | /******/ } 48 | /******/ Object.defineProperty(exports, '__esModule', { value: true }); 49 | /******/ }; 50 | /******/ 51 | /******/ // create a fake namespace object 52 | /******/ // mode & 1: value is a module id, require it 53 | /******/ // mode & 2: merge all properties of value into the ns 54 | /******/ // mode & 4: return value when already ns object 55 | /******/ // mode & 8|1: behave like require 56 | /******/ __webpack_require__.t = function(value, mode) { 57 | /******/ if(mode & 1) value = __webpack_require__(value); 58 | /******/ if(mode & 8) return value; 59 | /******/ if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value; 60 | /******/ var ns = Object.create(null); 61 | /******/ __webpack_require__.r(ns); 62 | /******/ Object.defineProperty(ns, 'default', { enumerable: true, value: value }); 63 | /******/ if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key)); 64 | /******/ return ns; 65 | /******/ }; 66 | /******/ 67 | /******/ // getDefaultExport function for compatibility with non-harmony modules 68 | /******/ __webpack_require__.n = function(module) { 69 | /******/ var getter = module && module.__esModule ? 70 | /******/ function getDefault() { return module['default']; } : 71 | /******/ function getModuleExports() { return module; }; 72 | /******/ __webpack_require__.d(getter, 'a', getter); 73 | /******/ return getter; 74 | /******/ }; 75 | /******/ 76 | /******/ // Object.prototype.hasOwnProperty.call 77 | /******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); }; 78 | /******/ 79 | /******/ // __webpack_public_path__ 80 | /******/ __webpack_require__.p = "./js/"; 81 | /******/ 82 | /******/ 83 | /******/ // Load entry module and return exports 84 | /******/ return __webpack_require__(__webpack_require__.s = "./ts/remote.ts"); 85 | /******/ }) 86 | /************************************************************************/ 87 | /******/ ({ 88 | 89 | /***/ "./ts/remote.ts": 90 | /*!**********************!*\ 91 | !*** ./ts/remote.ts ***! 92 | \**********************/ 93 | /*! no static exports found */ 94 | /***/ (function(module, exports) { 95 | 96 | addEventListener('message', e => { 97 | console.debug('message', e.data); 98 | }); 99 | addEventListener('click', e => { 100 | const target = e.target; 101 | const data = (target.dataset.send || '').split(' ').map(hex => parseInt(hex, 16)); 102 | const app = chrome['app'].window.get('app').contentWindow; 103 | for (let i = 0; i < 1; i++) { 104 | setTimeout(function () { 105 | app.postMessage(new Uint8Array([2, ...data])); 106 | }, i * 10); 107 | } 108 | }); 109 | addEventListener('load', e => { 110 | document.body.focus(); 111 | }); 112 | 113 | 114 | /***/ }) 115 | 116 | /******/ }); 117 | //# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIndlYnBhY2s6Ly8vd2VicGFjay9ib290c3RyYXAiLCJ3ZWJwYWNrOi8vLy4vdHMvcmVtb3RlLnRzIl0sIm5hbWVzIjpbXSwibWFwcGluZ3MiOiI7UUFBQTtRQUNBOztRQUVBO1FBQ0E7O1FBRUE7UUFDQTtRQUNBO1FBQ0E7UUFDQTtRQUNBO1FBQ0E7UUFDQTtRQUNBO1FBQ0E7O1FBRUE7UUFDQTs7UUFFQTtRQUNBOztRQUVBO1FBQ0E7UUFDQTs7O1FBR0E7UUFDQTs7UUFFQTtRQUNBOztRQUVBO1FBQ0E7UUFDQTtRQUNBLDBDQUEwQyxnQ0FBZ0M7UUFDMUU7UUFDQTs7UUFFQTtRQUNBO1FBQ0E7UUFDQSx3REFBd0Qsa0JBQWtCO1FBQzFFO1FBQ0EsaURBQWlELGNBQWM7UUFDL0Q7O1FBRUE7UUFDQTtRQUNBO1FBQ0E7UUFDQTtRQUNBO1FBQ0E7UUFDQTtRQUNBO1FBQ0E7UUFDQTtRQUNBLHlDQUF5QyxpQ0FBaUM7UUFDMUUsZ0hBQWdILG1CQUFtQixFQUFFO1FBQ3JJO1FBQ0E7O1FBRUE7UUFDQTtRQUNBO1FBQ0EsMkJBQTJCLDBCQUEwQixFQUFFO1FBQ3ZELGlDQUFpQyxlQUFlO1FBQ2hEO1FBQ0E7UUFDQTs7UUFFQTtRQUNBLHNEQUFzRCwrREFBK0Q7O1FBRXJIO1FBQ0E7OztRQUdBO1FBQ0E7Ozs7Ozs7Ozs7OztBQ2pGQSxnQkFBZ0IsQ0FBQyxTQUFTLEVBQUUsQ0FBQyxDQUFDLEVBQUU7SUFDL0IsT0FBTyxDQUFDLEtBQUssQ0FBQyxTQUFTLEVBQUUsQ0FBQyxDQUFDLElBQUksQ0FBQztBQUNqQyxDQUFDLENBQUM7QUFDRixnQkFBZ0IsQ0FBQyxPQUFPLEVBQUUsQ0FBQyxDQUFDLEVBQUU7SUFDN0IsTUFBTSxNQUFNLEdBQUcsQ0FBQyxDQUFDLE1BQTJCO0lBQzVDLE1BQU0sSUFBSSxHQUFHLENBQUMsTUFBTSxDQUFDLE9BQU8sQ0FBQyxJQUFJLElBQUksRUFBRSxDQUFDLENBQUMsS0FBSyxDQUFDLEdBQUcsQ0FBQyxDQUFDLEdBQUcsQ0FBQyxHQUFHLENBQUMsRUFBRSxDQUFDLFFBQVEsQ0FBQyxHQUFHLEVBQUUsRUFBRSxDQUFDLENBQUM7SUFDakYsTUFBTSxHQUFHLEdBQUcsTUFBTSxDQUFDLEtBQUssQ0FBQyxDQUFDLE1BQU0sQ0FBQyxHQUFHLENBQUMsS0FBSyxDQUFDLENBQUMsYUFBYTtJQUN6RCxLQUFLLElBQUksQ0FBQyxHQUFDLENBQUMsRUFBRSxDQUFDLEdBQUMsQ0FBQyxFQUFFLENBQUMsRUFBRSxFQUFFO1FBQ3ZCLFVBQVUsQ0FBQztZQUNWLEdBQUcsQ0FBQyxXQUFXLENBQUMsSUFBSSxVQUFVLENBQUMsQ0FBQyxDQUFDLEVBQUUsR0FBRyxJQUFJLENBQUMsQ0FBQyxDQUFDO1FBQzlDLENBQUMsRUFBRSxDQUFDLEdBQUcsRUFBRSxDQUFDO0tBQ1Y7QUFDRixDQUFDLENBQUM7QUFVRixnQkFBZ0IsQ0FBQyxNQUFNLEVBQUUsQ0FBQyxDQUFDLEVBQUU7SUFDNUIsUUFBUSxDQUFDLElBQUksQ0FBQyxLQUFLLEVBQUU7QUFDdEIsQ0FBQyxDQUFDIiwiZmlsZSI6InJlbW90ZS5qcyIsInNvdXJjZXNDb250ZW50IjpbIiBcdC8vIFRoZSBtb2R1bGUgY2FjaGVcbiBcdHZhciBpbnN0YWxsZWRNb2R1bGVzID0ge307XG5cbiBcdC8vIFRoZSByZXF1aXJlIGZ1bmN0aW9uXG4gXHRmdW5jdGlvbiBfX3dlYnBhY2tfcmVxdWlyZV9fKG1vZHVsZUlkKSB7XG5cbiBcdFx0Ly8gQ2hlY2sgaWYgbW9kdWxlIGlzIGluIGNhY2hlXG4gXHRcdGlmKGluc3RhbGxlZE1vZHVsZXNbbW9kdWxlSWRdKSB7XG4gXHRcdFx0cmV0dXJuIGluc3RhbGxlZE1vZHVsZXNbbW9kdWxlSWRdLmV4cG9ydHM7XG4gXHRcdH1cbiBcdFx0Ly8gQ3JlYXRlIGEgbmV3IG1vZHVsZSAoYW5kIHB1dCBpdCBpbnRvIHRoZSBjYWNoZSlcbiBcdFx0dmFyIG1vZHVsZSA9IGluc3RhbGxlZE1vZHVsZXNbbW9kdWxlSWRdID0ge1xuIFx0XHRcdGk6IG1vZHVsZUlkLFxuIFx0XHRcdGw6IGZhbHNlLFxuIFx0XHRcdGV4cG9ydHM6IHt9XG4gXHRcdH07XG5cbiBcdFx0Ly8gRXhlY3V0ZSB0aGUgbW9kdWxlIGZ1bmN0aW9uXG4gXHRcdG1vZHVsZXNbbW9kdWxlSWRdLmNhbGwobW9kdWxlLmV4cG9ydHMsIG1vZHVsZSwgbW9kdWxlLmV4cG9ydHMsIF9fd2VicGFja19yZXF1aXJlX18pO1xuXG4gXHRcdC8vIEZsYWcgdGhlIG1vZHVsZSBhcyBsb2FkZWRcbiBcdFx0bW9kdWxlLmwgPSB0cnVlO1xuXG4gXHRcdC8vIFJldHVybiB0aGUgZXhwb3J0cyBvZiB0aGUgbW9kdWxlXG4gXHRcdHJldHVybiBtb2R1bGUuZXhwb3J0cztcbiBcdH1cblxuXG4gXHQvLyBleHBvc2UgdGhlIG1vZHVsZXMgb2JqZWN0IChfX3dlYnBhY2tfbW9kdWxlc19fKVxuIFx0X193ZWJwYWNrX3JlcXVpcmVfXy5tID0gbW9kdWxlcztcblxuIFx0Ly8gZXhwb3NlIHRoZSBtb2R1bGUgY2FjaGVcbiBcdF9fd2VicGFja19yZXF1aXJlX18uYyA9IGluc3RhbGxlZE1vZHVsZXM7XG5cbiBcdC8vIGRlZmluZSBnZXR0ZXIgZnVuY3Rpb24gZm9yIGhhcm1vbnkgZXhwb3J0c1xuIFx0X193ZWJwYWNrX3JlcXVpcmVfXy5kID0gZnVuY3Rpb24oZXhwb3J0cywgbmFtZSwgZ2V0dGVyKSB7XG4gXHRcdGlmKCFfX3dlYnBhY2tfcmVxdWlyZV9fLm8oZXhwb3J0cywgbmFtZSkpIHtcbiBcdFx0XHRPYmplY3QuZGVmaW5lUHJvcGVydHkoZXhwb3J0cywgbmFtZSwgeyBlbnVtZXJhYmxlOiB0cnVlLCBnZXQ6IGdldHRlciB9KTtcbiBcdFx0fVxuIFx0fTtcblxuIFx0Ly8gZGVmaW5lIF9fZXNNb2R1bGUgb24gZXhwb3J0c1xuIFx0X193ZWJwYWNrX3JlcXVpcmVfXy5yID0gZnVuY3Rpb24oZXhwb3J0cykge1xuIFx0XHRpZih0eXBlb2YgU3ltYm9sICE9PSAndW5kZWZpbmVkJyAmJiBTeW1ib2wudG9TdHJpbmdUYWcpIHtcbiBcdFx0XHRPYmplY3QuZGVmaW5lUHJvcGVydHkoZXhwb3J0cywgU3ltYm9sLnRvU3RyaW5nVGFnLCB7IHZhbHVlOiAnTW9kdWxlJyB9KTtcbiBcdFx0fVxuIFx0XHRPYmplY3QuZGVmaW5lUHJvcGVydHkoZXhwb3J0cywgJ19fZXNNb2R1bGUnLCB7IHZhbHVlOiB0cnVlIH0pO1xuIFx0fTtcblxuIFx0Ly8gY3JlYXRlIGEgZmFrZSBuYW1lc3BhY2Ugb2JqZWN0XG4gXHQvLyBtb2RlICYgMTogdmFsdWUgaXMgYSBtb2R1bGUgaWQsIHJlcXVpcmUgaXRcbiBcdC8vIG1vZGUgJiAyOiBtZXJnZSBhbGwgcHJvcGVydGllcyBvZiB2YWx1ZSBpbnRvIHRoZSBuc1xuIFx0Ly8gbW9kZSAmIDQ6IHJldHVybiB2YWx1ZSB3aGVuIGFscmVhZHkgbnMgb2JqZWN0XG4gXHQvLyBtb2RlICYgOHwxOiBiZWhhdmUgbGlrZSByZXF1aXJlXG4gXHRfX3dlYnBhY2tfcmVxdWlyZV9fLnQgPSBmdW5jdGlvbih2YWx1ZSwgbW9kZSkge1xuIFx0XHRpZihtb2RlICYgMSkgdmFsdWUgPSBfX3dlYnBhY2tfcmVxdWlyZV9fKHZhbHVlKTtcbiBcdFx0aWYobW9kZSAmIDgpIHJldHVybiB2YWx1ZTtcbiBcdFx0aWYoKG1vZGUgJiA0KSAmJiB0eXBlb2YgdmFsdWUgPT09ICdvYmplY3QnICYmIHZhbHVlICYmIHZhbHVlLl9fZXNNb2R1bGUpIHJldHVybiB2YWx1ZTtcbiBcdFx0dmFyIG5zID0gT2JqZWN0LmNyZWF0ZShudWxsKTtcbiBcdFx0X193ZWJwYWNrX3JlcXVpcmVfXy5yKG5zKTtcbiBcdFx0T2JqZWN0LmRlZmluZVByb3BlcnR5KG5zLCAnZGVmYXVsdCcsIHsgZW51bWVyYWJsZTogdHJ1ZSwgdmFsdWU6IHZhbHVlIH0pO1xuIFx0XHRpZihtb2RlICYgMiAmJiB0eXBlb2YgdmFsdWUgIT0gJ3N0cmluZycpIGZvcih2YXIga2V5IGluIHZhbHVlKSBfX3dlYnBhY2tfcmVxdWlyZV9fLmQobnMsIGtleSwgZnVuY3Rpb24oa2V5KSB7IHJldHVybiB2YWx1ZVtrZXldOyB9LmJpbmQobnVsbCwga2V5KSk7XG4gXHRcdHJldHVybiBucztcbiBcdH07XG5cbiBcdC8vIGdldERlZmF1bHRFeHBvcnQgZnVuY3Rpb24gZm9yIGNvbXBhdGliaWxpdHkgd2l0aCBub24taGFybW9ueSBtb2R1bGVzXG4gXHRfX3dlYnBhY2tfcmVxdWlyZV9fLm4gPSBmdW5jdGlvbihtb2R1bGUpIHtcbiBcdFx0dmFyIGdldHRlciA9IG1vZHVsZSAmJiBtb2R1bGUuX19lc01vZHVsZSA/XG4gXHRcdFx0ZnVuY3Rpb24gZ2V0RGVmYXVsdCgpIHsgcmV0dXJuIG1vZHVsZVsnZGVmYXVsdCddOyB9IDpcbiBcdFx0XHRmdW5jdGlvbiBnZXRNb2R1bGVFeHBvcnRzKCkgeyByZXR1cm4gbW9kdWxlOyB9O1xuIFx0XHRfX3dlYnBhY2tfcmVxdWlyZV9fLmQoZ2V0dGVyLCAnYScsIGdldHRlcik7XG4gXHRcdHJldHVybiBnZXR0ZXI7XG4gXHR9O1xuXG4gXHQvLyBPYmplY3QucHJvdG90eXBlLmhhc093blByb3BlcnR5LmNhbGxcbiBcdF9fd2VicGFja19yZXF1aXJlX18ubyA9IGZ1bmN0aW9uKG9iamVjdCwgcHJvcGVydHkpIHsgcmV0dXJuIE9iamVjdC5wcm90b3R5cGUuaGFzT3duUHJvcGVydHkuY2FsbChvYmplY3QsIHByb3BlcnR5KTsgfTtcblxuIFx0Ly8gX193ZWJwYWNrX3B1YmxpY19wYXRoX19cbiBcdF9fd2VicGFja19yZXF1aXJlX18ucCA9IFwiLi9qcy9cIjtcblxuXG4gXHQvLyBMb2FkIGVudHJ5IG1vZHVsZSBhbmQgcmV0dXJuIGV4cG9ydHNcbiBcdHJldHVybiBfX3dlYnBhY2tfcmVxdWlyZV9fKF9fd2VicGFja19yZXF1aXJlX18ucyA9IFwiLi90cy9yZW1vdGUudHNcIik7XG4iLCJcbmFkZEV2ZW50TGlzdGVuZXIoJ21lc3NhZ2UnLCBlID0+IHtcblx0Y29uc29sZS5kZWJ1ZygnbWVzc2FnZScsIGUuZGF0YSlcbn0pXG5hZGRFdmVudExpc3RlbmVyKCdjbGljaycsIGUgPT4ge1xuXHRjb25zdCB0YXJnZXQgPSBlLnRhcmdldCBhcyBIVE1MQnV0dG9uRWxlbWVudFxuXHRjb25zdCBkYXRhID0gKHRhcmdldC5kYXRhc2V0LnNlbmQgfHwgJycpLnNwbGl0KCcgJykubWFwKGhleCA9PiBwYXJzZUludChoZXgsIDE2KSlcblx0Y29uc3QgYXBwID0gY2hyb21lWydhcHAnXS53aW5kb3cuZ2V0KCdhcHAnKS5jb250ZW50V2luZG93XG5cdGZvciAobGV0IGk9MDsgaTwxOyBpKyspIHtcblx0XHRzZXRUaW1lb3V0KGZ1bmN0aW9uKCkge1xuXHRcdFx0YXBwLnBvc3RNZXNzYWdlKG5ldyBVaW50OEFycmF5KFsyLCAuLi5kYXRhXSkpXG5cdFx0fSwgaSAqIDEwKVxuXHR9XG59KVxuLy8gbGV0IHN0YXRlID0gZmFsc2Vcbi8vIGFkZEV2ZW50TGlzdGVuZXIoJ2tleWRvd24nLCBlID0+IHtcbi8vIFx0aWYgKHN0YXRlKSB7XG4vLyBcdFx0ZG9jdW1lbnQuZ2V0RWxlbWVudEJ5SWQoJ29mZicpLmNsaWNrKClcbi8vIFx0fSBlbHNlIHtcbi8vIFx0XHRkb2N1bWVudC5nZXRFbGVtZW50QnlJZCgnZ3JlZW4nKS5jbGljaygpXG4vLyBcdH1cbi8vIFx0c3RhdGUgPSAhc3RhdGVcbi8vIH0pXG5hZGRFdmVudExpc3RlbmVyKCdsb2FkJywgZSA9PiB7XG5cdGRvY3VtZW50LmJvZHkuZm9jdXMoKVxufSlcbiJdLCJzb3VyY2VSb290IjoiIn0= -------------------------------------------------------------------------------- /app/js/utils.js: -------------------------------------------------------------------------------- 1 | /******/ (function(modules) { // webpackBootstrap 2 | /******/ // The module cache 3 | /******/ var installedModules = {}; 4 | /******/ 5 | /******/ // The require function 6 | /******/ function __webpack_require__(moduleId) { 7 | /******/ 8 | /******/ // Check if module is in cache 9 | /******/ if(installedModules[moduleId]) { 10 | /******/ return installedModules[moduleId].exports; 11 | /******/ } 12 | /******/ // Create a new module (and put it into the cache) 13 | /******/ var module = installedModules[moduleId] = { 14 | /******/ i: moduleId, 15 | /******/ l: false, 16 | /******/ exports: {} 17 | /******/ }; 18 | /******/ 19 | /******/ // Execute the module function 20 | /******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); 21 | /******/ 22 | /******/ // Flag the module as loaded 23 | /******/ module.l = true; 24 | /******/ 25 | /******/ // Return the exports of the module 26 | /******/ return module.exports; 27 | /******/ } 28 | /******/ 29 | /******/ 30 | /******/ // expose the modules object (__webpack_modules__) 31 | /******/ __webpack_require__.m = modules; 32 | /******/ 33 | /******/ // expose the module cache 34 | /******/ __webpack_require__.c = installedModules; 35 | /******/ 36 | /******/ // define getter function for harmony exports 37 | /******/ __webpack_require__.d = function(exports, name, getter) { 38 | /******/ if(!__webpack_require__.o(exports, name)) { 39 | /******/ Object.defineProperty(exports, name, { enumerable: true, get: getter }); 40 | /******/ } 41 | /******/ }; 42 | /******/ 43 | /******/ // define __esModule on exports 44 | /******/ __webpack_require__.r = function(exports) { 45 | /******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) { 46 | /******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' }); 47 | /******/ } 48 | /******/ Object.defineProperty(exports, '__esModule', { value: true }); 49 | /******/ }; 50 | /******/ 51 | /******/ // create a fake namespace object 52 | /******/ // mode & 1: value is a module id, require it 53 | /******/ // mode & 2: merge all properties of value into the ns 54 | /******/ // mode & 4: return value when already ns object 55 | /******/ // mode & 8|1: behave like require 56 | /******/ __webpack_require__.t = function(value, mode) { 57 | /******/ if(mode & 1) value = __webpack_require__(value); 58 | /******/ if(mode & 8) return value; 59 | /******/ if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value; 60 | /******/ var ns = Object.create(null); 61 | /******/ __webpack_require__.r(ns); 62 | /******/ Object.defineProperty(ns, 'default', { enumerable: true, value: value }); 63 | /******/ if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key)); 64 | /******/ return ns; 65 | /******/ }; 66 | /******/ 67 | /******/ // getDefaultExport function for compatibility with non-harmony modules 68 | /******/ __webpack_require__.n = function(module) { 69 | /******/ var getter = module && module.__esModule ? 70 | /******/ function getDefault() { return module['default']; } : 71 | /******/ function getModuleExports() { return module; }; 72 | /******/ __webpack_require__.d(getter, 'a', getter); 73 | /******/ return getter; 74 | /******/ }; 75 | /******/ 76 | /******/ // Object.prototype.hasOwnProperty.call 77 | /******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); }; 78 | /******/ 79 | /******/ // __webpack_public_path__ 80 | /******/ __webpack_require__.p = "./js/"; 81 | /******/ 82 | /******/ 83 | /******/ // Load entry module and return exports 84 | /******/ return __webpack_require__(__webpack_require__.s = "./ts/utils.ts"); 85 | /******/ }) 86 | /************************************************************************/ 87 | /******/ ({ 88 | 89 | /***/ "./ts/utils.ts": 90 | /*!*********************!*\ 91 | !*** ./ts/utils.ts ***! 92 | \*********************/ 93 | /*! no static exports found */ 94 | /***/ (function(module, exports) { 95 | 96 | const $txt = document.getElementById('txt'); 97 | const $hex = document.getElementById('hex'); 98 | const $dec = document.getElementById('dec'); 99 | const $bin = document.getElementById('bin'); 100 | const $int = document.getElementById('int'); 101 | const $res = document.getElementById('res'); 102 | const $array = document.getElementById('arr'); 103 | const $message = document.getElementById('message'); 104 | const $send = document.getElementById('send'); 105 | chrome.storage.local.get('utils_txt', ({ utils_txt: codes }) => { 106 | if (!codes) 107 | return; 108 | $bin.innerHTML = codes.map(c => c.toString(2).toUpperCase().padStart(8, '0')).join(' '); 109 | $hex.innerHTML = codes.map(c => c.toString(16).toUpperCase().padStart(2, '0')).join(' '); 110 | $dec.innerHTML = codes.map(c => c.toString()).join(' '); 111 | $txt.innerHTML = codes.map(c => String.fromCharCode(c)).join(''); 112 | $array.innerHTML = codes.map(c => '0x' + c.toString(16).toUpperCase().padStart(2, '0')).join(', '); 113 | }); 114 | let codes = []; 115 | addEventListener('input', (e) => { 116 | let value = e.target.innerText.replace(/\s+/, ''); 117 | switch (e.target.id) { 118 | case 'txt': 119 | codes = value ? value.split('').map(c => c.charCodeAt(0)) : []; 120 | $bin.innerHTML = codes.map(c => c.toString(2).toUpperCase().padStart(8, '0')).join(' '); 121 | $hex.innerHTML = codes.map(c => c.toString(16).toUpperCase().padStart(2, '0')).join(' '); 122 | $dec.innerHTML = codes.map(c => c.toString()).join(' '); 123 | $array.innerHTML = codes.map(c => '0x' + c.toString(16).toUpperCase().padStart(2, '0')).join(', '); 124 | break; 125 | case 'hex': 126 | value = e.target.innerText.replace(/[^0-9a-f ]/ig, '').trim(); 127 | codes = value ? value.split(' ').map(c => parseInt(c, 16)) : []; 128 | $bin.innerHTML = codes.map(c => c.toString(2).toUpperCase().padStart(8, '0')).join(' '); 129 | $dec.innerHTML = codes.map(c => c.toString()).join(' '); 130 | $txt.innerHTML = codes.map(c => String.fromCharCode(c)).join(''); 131 | $array.innerHTML = codes.map(c => '0x' + c.toString(16).toUpperCase().padStart(2, '0')).join(', '); 132 | break; 133 | case 'dec': 134 | value = e.target.innerText.replace(/[^0-9 ]/ig, '').trim(); 135 | codes = value ? value.split(' ').map(c => parseInt(c, 10)) : []; 136 | $bin.innerHTML = codes.map(c => c.toString(2).toUpperCase().padStart(8, '0')).join(' '); 137 | $hex.innerHTML = codes.map(c => c.toString(16).toUpperCase().padStart(2, '0')).join(' '); 138 | $txt.innerHTML = codes.map(c => String.fromCharCode(c)).join(''); 139 | $array.innerHTML = codes.map(c => '0x' + c.toString(16).toUpperCase().padStart(2, '0')).join(', '); 140 | break; 141 | case 'bin': 142 | value = e.target.innerText.replace(/[^01 ]/ig, '').trim(); 143 | codes = value ? value.split(' ').map(c => parseInt(c, 2)) : []; 144 | $hex.innerHTML = codes.map(c => c.toString(16).toUpperCase().padStart(2, '0')).join(' '); 145 | $dec.innerHTML = codes.map(c => c.toString()).join(' '); 146 | $txt.innerHTML = codes.map(c => String.fromCharCode(c)).join(''); 147 | $array.innerHTML = codes.map(c => '0x' + c.toString(16).toUpperCase().padStart(2, '0')).join(', '); 148 | break; 149 | case 'int': 150 | let res = '\n'; 151 | value = e.target.innerText.replace(/[^0-9]/ig, '').trim(); 152 | if (value) { 153 | const num = parseFloat(value); 154 | const buf = new Uint8Array(4); 155 | const view = new DataView(buf.buffer); 156 | view.setUint16(0, num); 157 | res += 'U16 BE = ' + toHEX(buf.slice(0, 2)); 158 | view.setUint16(0, num, true); 159 | res += 'U16 LE = ' + toHEX(buf.slice(0, 2)); 160 | view.setUint32(0, num); 161 | res += 'U32 BE = ' + toHEX(buf); 162 | view.setUint32(0, num, true); 163 | res += 'U32 LE = ' + toHEX(buf); 164 | view.setFloat32(0, num); 165 | res += 'F32 BE = ' + toHEX(buf); 166 | view.setFloat32(0, num, true); 167 | res += 'F32 LE = ' + toHEX(buf); 168 | } 169 | $res.innerHTML = res; 170 | break; 171 | default: return; 172 | } 173 | chrome.storage.local.set({ 'utils_txt': codes }); 174 | }); 175 | function toHEX(bytes) { 176 | return [...bytes].map(c => c.toString(16).toUpperCase().padStart(2, '0')).join(' ') + '\n'; 177 | } 178 | $send.addEventListener('click', e => { 179 | chrome['app'].window.get('app').contentWindow.postMessage(new Uint8Array([1, ...codes])); 180 | }); 181 | 182 | 183 | /***/ }) 184 | 185 | /******/ }); 186 | //# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJzb3VyY2VzIjpbIndlYnBhY2s6Ly8vd2VicGFjay9ib290c3RyYXAiLCJ3ZWJwYWNrOi8vLy4vdHMvdXRpbHMudHMiXSwibmFtZXMiOltdLCJtYXBwaW5ncyI6IjtRQUFBO1FBQ0E7O1FBRUE7UUFDQTs7UUFFQTtRQUNBO1FBQ0E7UUFDQTtRQUNBO1FBQ0E7UUFDQTtRQUNBO1FBQ0E7UUFDQTs7UUFFQTtRQUNBOztRQUVBO1FBQ0E7O1FBRUE7UUFDQTtRQUNBOzs7UUFHQTtRQUNBOztRQUVBO1FBQ0E7O1FBRUE7UUFDQTtRQUNBO1FBQ0EsMENBQTBDLGdDQUFnQztRQUMxRTtRQUNBOztRQUVBO1FBQ0E7UUFDQTtRQUNBLHdEQUF3RCxrQkFBa0I7UUFDMUU7UUFDQSxpREFBaUQsY0FBYztRQUMvRDs7UUFFQTtRQUNBO1FBQ0E7UUFDQTtRQUNBO1FBQ0E7UUFDQTtRQUNBO1FBQ0E7UUFDQTtRQUNBO1FBQ0EseUNBQXlDLGlDQUFpQztRQUMxRSxnSEFBZ0gsbUJBQW1CLEVBQUU7UUFDckk7UUFDQTs7UUFFQTtRQUNBO1FBQ0E7UUFDQSwyQkFBMkIsMEJBQTBCLEVBQUU7UUFDdkQsaUNBQWlDLGVBQWU7UUFDaEQ7UUFDQTtRQUNBOztRQUVBO1FBQ0Esc0RBQXNELCtEQUErRDs7UUFFckg7UUFDQTs7O1FBR0E7UUFDQTs7Ozs7Ozs7Ozs7O0FDbEZBLE1BQU0sSUFBSSxHQUFHLFFBQVEsQ0FBQyxjQUFjLENBQUMsS0FBSyxDQUFzQjtBQUNoRSxNQUFNLElBQUksR0FBRyxRQUFRLENBQUMsY0FBYyxDQUFDLEtBQUssQ0FBc0I7QUFDaEUsTUFBTSxJQUFJLEdBQUcsUUFBUSxDQUFDLGNBQWMsQ0FBQyxLQUFLLENBQXNCO0FBQ2hFLE1BQU0sSUFBSSxHQUFHLFFBQVEsQ0FBQyxjQUFjLENBQUMsS0FBSyxDQUFzQjtBQUNoRSxNQUFNLElBQUksR0FBRyxRQUFRLENBQUMsY0FBYyxDQUFDLEtBQUssQ0FBc0I7QUFDaEUsTUFBTSxJQUFJLEdBQUcsUUFBUSxDQUFDLGNBQWMsQ0FBQyxLQUFL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QUFDO1lBQ3hGLElBQUksQ0FBQyxTQUFTLEdBQUcsS0FBSyxDQUFDLEdBQUcsQ0FBQyxDQUFDLENBQUMsRUFBRSxDQUFDLENBQUMsQ0FBQyxRQUFRLEVBQUUsQ0FBQyxDQUFDLElBQUksQ0FBQyxHQUFHLENBQUM7WUFDdkQsTUFBTSxDQUFDLFNBQVMsR0FBRyxLQUFLLENBQUMsR0FBRyxDQUFDLENBQUMsQ0FBQyxFQUFFLENBQUMsSUFBSSxHQUFHLENBQUMsQ0FBQyxRQUFRLENBQUMsRUFBRSxDQUFDLENBQUMsV0FBVyxFQUFFLENBQUMsUUFBUSxDQUFDLENBQUMsRUFBRSxHQUFHLENBQUMsQ0FBQyxDQUFDLElBQUksQ0FBQyxJQUFJLENBQUM7WUFDbEcsTUFBSztRQUNOLEtBQUssS0FBSztZQUNULEtBQUssR0FBRyxDQUFDLENBQUMsTUFBTSxDQUFDLFNBQVMsQ0FBQyxPQUFPLENBQUMsY0FBYyxFQUFFLEVBQUUsQ0FBQyxDQUFDLElBQUksRUFBRTtZQUM3RCxLQUFLLEdBQUcsS0FBSyxDQUFDLENBQUMsQ0FBQyxLQUFLLENBQUMsS0FBSyxDQUFDLEdBQUcsQ0FBQyxDQUFDLEdBQUcsQ0FBQyxDQUFDLENBQUMsRUFBRSxDQUFDLFFBQVEsQ0FBQyxDQUFDLEVBQUUsRUFBRSxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsRUFBRTtZQUMvRCxJQUFJLENBQUMsU0FBUyxHQUFHLEtBQUssQ0FBQyxHQUFHLENBQUMsQ0FBQyxDQUFDLEVBQUUsQ0FBQyxDQUFDLENBQUMsUUFBUSxDQUFDLENBQUMsQ0FBQyxDQUFDLFdBQVcsRUFBRSxDQUFDLFFBQVEsQ0FBQyxDQUFDLEVBQUUsR0FBRyxDQUFDLENBQUMsQ0FBQyxJQUFJLENBQUMsR0FBRyxDQUFDO1lBQ3ZGLElBQUksQ0FBQyxTQUFTLEdBQUcsS0FBSyxDQUFDLEdBQUcsQ0FBQyxDQUFDLENBQUMsRUFBRSxDQUFDLENBQUMsQ0FBQyxRQUFRLEVBQUUsQ0FBQyxDQUFDLElBQUksQ0FBQyxHQUFHLENBQUM7WUFDdkQsSUFBSSxDQUFDLFNBQVMsR0FBRyxLQUFLLENBQUMsR0FBRyxDQUFDLENBQUMsQ0FBQyxFQUFFLENBQUMsTUFBTSxDQUFDLFlBQVksQ0FBQyxDQUFDLENBQUMsQ0FBQyxDQUFDLElBQUksQ0FBQyxFQUFFLENBQUM7WUFDaEUsTUFBTSxDQUFDLFNBQVMsR0FBRyxLQUFLLENBQUMsR0FBRyxDQUFDLENBQUMsQ0FBQyxFQUFFLENBQUMsSUFBSSxHQUFHLENBQUMsQ0FBQyxRQUFRLENBQUMsRUFBRSxDQUFDLENBQUMsV0FBVyxFQUFFLENBQUMsUUFBUSxDQUFDLENBQUMsRUFBRSxHQUFHLENBQUMsQ0FBQyxDQUFDLElBQUksQ0FBQyxJQUFJLENBQUM7WUFDbEcsTUFBSztRQUNOLEtBQUssS0FBSztZQUNULEtBQUssR0FBRyxDQUFDLENBQUMsTUFBTSxDQUFDLFNBQVMsQ0FBQyxPQUFPLENBQUMsV0FBVyxFQUFFLEVBQUUsQ0FBQyxDQUFDLElBQUksRUFBRTtZQUMxRCxLQUFLLEdBQUcsS0FBSyxDQUFDLENBQUMsQ0FBQyxLQUFLLENBQUMsS0FBSyxDQUFDLEdBQUcsQ0FBQyxDQUFDLEdBQUcsQ0FBQyxDQUFDLENBQUMsRUFBRSxDQUFDLFFBQVEsQ0FBQyxDQUFDLEVBQUUsRUFBRSxDQUFDLENBQUMsQ0FBQyxDQUFDLENBQUMsRUFBRTtZQUMvRCxJQUFJLENBQUMsU0FBUyxHQUFHLEtBQUssQ0FBQyxHQUFHLENBQUMsQ0FBQyxDQUFDLEVBQUUsQ0FBQyxDQUFDLENBQUMsUUFBUSxDQUFDLENBQUMsQ0FBQyxDQUFDLFdBQVcsRUFBRSxDQUFDLFFBQVEsQ0FBQyxDQUFDLEVBQUUsR0FBRyxDQUFDLENBQUMsQ0FBQyxJQUFJLENBQUMsR0FBRyxDQUFDO1lBQ3ZGLElBQUksQ0FBQyxTQUFTLEdBQUcsS0FBSyxDQUFDLEdBQUcsQ0FBQyxDQUFDLENBQUMsRUFBRSxDQUFDLENBQUMsQ0FBQyxRQUFRLENBQUMsRUFBRSxDQUFDLENBQUMsV0FBVyxFQUFFLENBQUMsUUFBUSxDQUFDLENBQUMsRUFBRSxHQUFHLENBQUMsQ0FBQyxDQUFDLElBQUksQ0FBQyxHQUFH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QUFVLENBQUMsQ0FBQyxDQUFDLEVBQUUsR0FBRyxLQUFLLENBQUMsQ0FBQyxDQUFDO0FBQ3pGLENBQUMsQ0FBQyIsImZpbGUiOiJ1dGlscy5qcyIsInNvdXJjZXNDb250ZW50IjpbIiBcdC8vIFRoZSBtb2R1bGUgY2FjaGVcbiBcdHZhciBpbnN0YWxsZWRNb2R1bGVzID0ge307XG5cbiBcdC8vIFRoZSByZXF1aXJlIGZ1bmN0aW9uXG4gXHRmdW5jdGlvbiBfX3dlYnBhY2tfcmVxdWlyZV9fKG1vZHVsZUlkKSB7XG5cbiBcdFx0Ly8gQ2hlY2sgaWYgbW9kdWxlIGlzIGluIGNhY2hlXG4gXHRcdGlmKGluc3RhbGxlZE1vZHVsZXNbbW9kdWxlSWRdKSB7XG4gXHRcdFx0cmV0dXJuIGluc3RhbGxlZE1vZHVsZXNbbW9kdWxlSWRdLmV4cG9ydHM7XG4gXHRcdH1cbiBcdFx0Ly8gQ3JlYXRlIGEgbmV3IG1vZHVsZSAoYW5kIHB1dCBpdCBpbnRvIHRoZSBjYWNoZSlcbiBcdFx0dmFyIG1vZHVsZSA9IGluc3RhbGxlZE1vZHVsZXNbbW9kdWxlSWRdID0ge1xuIFx0XHRcdGk6IG1vZHVsZUlkLFxuIFx0XHRcdGw6IGZhbHNlLFxuIFx0XHRcdGV4cG9ydHM6IHt9XG4gXHRcdH07XG5cbiBcdFx0Ly8gRXhlY3V0ZSB0aGUgbW9kdWxlIGZ1bmN0aW9uXG4gXHRcdG1vZHVsZXNbbW9kdWxlSWRdLmNhbGwobW9kdWxlLmV4cG9ydHMsIG1vZHVsZSwgbW9kdWxlLmV4cG9ydHMsIF9fd2VicGFja19yZXF1aXJlX18pO1xuXG4gXHRcdC8vIEZsYWcgdGhlIG1vZHVsZSBhcyBsb2FkZWRcbiBcdFx0bW9kdWxlLmwgPSB0cnVlO1xuXG4gXHRcdC8vIFJldHVybiB0aGUgZXhwb3J0cyBvZiB0aGUgbW9kdWxlXG4gXHRcdHJldHVybiBtb2R1bGUuZXhwb3J0cztcbiBcdH1cblxuXG4gXHQvLyBleHBvc2UgdGhlIG1vZHVsZXMgb2JqZWN0IChfX3dlYnBhY2tfbW9kdWxlc19fKVxuIFx0X193ZWJwYWNrX3JlcXVpcmVfXy5tID0gbW9kdWxlcztcblxuIFx0Ly8gZXhwb3NlIHRoZSBtb2R1bGUgY2FjaGVcbiBcdF9fd2VicGFja19yZXF1aXJlX18uYyA9IGluc3RhbGxlZE1vZHVsZXM7XG5cbiBcdC8vIGRlZmluZSBnZXR0ZXIgZnVuY3Rpb24gZm9yIGhhcm1vbnkgZXhwb3J0c1xuIFx0X193ZWJwYWNrX3JlcXVpcmVfXy5kID0gZnVuY3Rpb24oZXhwb3J0cywgbmFtZSwgZ2V0dGVyKSB7XG4gXHRcdGlmKCFfX3dlYnBhY2tfcmVxdWlyZV9fLm8oZXhwb3J0cywgbmFtZSkpIHtcbiBcdFx0XHRPYmplY3QuZGVmaW5lUHJvcGVydHkoZXhwb3J0cywgbmFtZSwgeyBlbnVtZXJhYmxlOiB0cnVlLCBnZXQ6IGdldHRlciB9KTtcbiBcdFx0fVxuIFx0fTtcblxuIFx0Ly8gZGVmaW5lIF9fZXNNb2R1bGUgb24gZXhwb3J0c1xuIFx0X193ZWJwYWNrX3JlcXVpcmVfXy5yID0gZnVuY3Rpb24oZXhwb3J0cykge1xuIFx0XHRpZih0eXBlb2YgU3ltYm9sICE9PSAndW5kZWZpbmVkJyAmJiBTeW1ib2wudG9TdHJpbmdUYWcpIHtcbiBcdFx0XHRPYmplY3QuZGVmaW5lUHJvcGVydHkoZXhwb3J0cywgU3ltYm9sLnRvU3RyaW5nVGFnLCB7IHZhbHVlOiAnTW9kdWxlJyB9KTtcbiBcdFx0fVxuIFx0XHRPYmplY3QuZGVmaW5lUHJvcGVydHkoZXhwb3J0cywgJ19fZXNNb2R1bGUnLCB7IHZhbHVlOiB0cnVlIH0pO1xuIFx0fTtcblxuIFx0Ly8gY3JlYXRlIGEgZmFrZSBuYW1lc3BhY2Ugb2JqZWN0XG4gXHQvLyBtb2RlICYgMTogdmFsdWUgaXMgYSBtb2R1bGUgaWQsIHJlcXVpcmUgaXRcbiBcdC8vIG1vZGUgJiAyOiBtZXJnZSBhbGwgcHJvcGVydGllcyBvZiB2YWx1ZSBpbnRvIHRoZSBuc1xuIFx0Ly8gbW9kZSAmIDQ6IHJldHVybiB2YWx1ZSB3aGVuIGFscmVhZHkgbnMgb2JqZWN0XG4gXHQvLyBtb2RlICYgOHwxOiBiZWhhdmUgbGlrZSByZXF1aXJlXG4gXHRfX3dlYnBhY2tfcmVxdWlyZV9fLnQgPSBmdW5jdGlvbih2YWx1ZSwgbW9kZSkge1xuIFx0XHRpZihtb2RlICYgMSkgdmFsdWUgPSBfX3dlYnBhY2tfcmVxdWlyZV9fKHZhbHVlKTtcbiBcdFx0aWYobW9kZSAmIDgpIHJldHVybiB2YWx1ZTtcbiBcdFx0aWYoKG1vZGUgJiA0KSAmJiB0eXBlb2YgdmFsdWUgPT09ICdvYmplY3QnICYmIHZhbHVlICYmIHZhbHVlLl9fZXNNb2R1bGUpIHJldHVybiB2YWx1ZTtcbiBcdFx0dmFyIG5zID0gT2JqZWN0LmNyZWF0ZShudWxsKTtcbiBcdFx0X193ZWJwYWNrX3JlcXVpcmVfXy5yKG5zKTtcbiBcdFx0T2JqZWN0LmRlZmluZVByb3BlcnR5KG5zLCAnZGVmYXVsdCcsIHsgZW51bWVyYWJsZTogdHJ1ZSwgdmFsdWU6IHZhbHVlIH0pO1xuIFx0XHRpZihtb2RlICYgMiAmJiB0eXBlb2YgdmFsdWUgIT0gJ3N0cmluZycpIGZvcih2YXIga2V5IGluIHZhbHVlKSBfX3dlYnBhY2tfcmVxdWlyZV9fLmQobnMsIGtleSwgZnVuY3Rpb24oa2V5KSB7IHJldHVybiB2YWx1ZVtrZXldOyB9LmJpbmQobnVsbCwga2V5KSk7XG4gXHRcdHJldHVybiBucztcbiBcdH07XG5cbiBcdC8vIGdldERlZmF1bHRFeHBvcnQgZnVuY3Rpb24gZm9yIGNvbXBhdGliaWxpdHkgd2l0aCBub24taGFybW9ueSBtb2R1bGVzXG4gXHRfX3dlYnBhY2tfcmVxdWlyZV9fLm4gPSBmdW5jdGlvbihtb2R1bGUpIHtcbiBcdFx0dmFyIGdldHRlciA9IG1vZHVsZSAmJiBtb2R1bGUuX19lc01vZHVsZSA/XG4gXHRcdFx0ZnVuY3Rpb24gZ2V0RGVmYXVsdCgpIHsgcmV0dXJuIG1vZHVsZVsnZGVmYXVsdCddOyB9IDpcbiBcdFx0XHRmdW5jdGlvbiBnZXRNb2R1bGVFeHBvcnRzKCkgeyByZXR1cm4gbW9kdWxlOyB9O1xuIFx0XHRfX3dlYnBhY2tfcmVxdWlyZV9fLmQoZ2V0dGVyLCAnYScsIGdldHRlcik7XG4gXHRcdHJldHVybiBnZXR0ZXI7XG4gXHR9O1xuXG4gXHQvLyBPYmplY3QucHJvdG90eXBlLmhhc093blByb3BlcnR5LmNhbGxcbiBcdF9fd2VicGFja19yZXF1aXJlX18ubyA9IGZ1bmN0aW9uKG9iamVjdCwgcHJvcGVydHkpIHsgcmV0dXJuIE9iamVjdC5wcm90b3R5cGUuaGFzT3duUHJvcGVydHkuY2FsbChvYmplY3QsIHByb3BlcnR5KTsgfTtcblxuIFx0Ly8gX193ZWJwYWNrX3B1YmxpY19wYXRoX19cbiBcdF9fd2VicGFja19yZXF1aXJlX18ucCA9IFwiLi9qcy9cIjtcblxuXG4gXHQvLyBMb2FkIGVudHJ5IG1vZHVsZSBhbmQgcmV0dXJuIGV4cG9ydHNcbiBcdHJldHVybiBfX3dlYnBhY2tfcmVxdWlyZV9fKF9fd2VicGFja19yZXF1aXJlX18ucyA9IFwiLi90cy91dGlscy50c1wiKTtcbiIsImNvbnN0ICR0eHQgPSBkb2N1bWVudC5nZXRFbGVtZW50QnlJZCgndHh0JykgYXMgSFRNTE91dHB1dEVsZW1lbnRcbmNvbnN0ICRoZXggPSBkb2N1bWVudC5nZXRFbGVtZW50QnlJZCgnaGV4JykgYXMgSFRNTE91dHB1dEVsZW1lbnRcbmNvbnN0ICRkZWMgPSBkb2N1bWVudC5nZXRFbGVtZW50QnlJZCgnZGVjJykgYXMgSFRNTE91dHB1dEVsZW1lbnRcbmNvbnN0ICRiaW4gPSBkb2N1bWVudC5nZXRFbGVtZW50QnlJZCgnYmluJykgYXMgSFRNTE91dHB1dEVsZW1lbnRcbmNvbnN0ICRpbnQgPSBkb2N1bWVudC5nZXRFbGVtZW50QnlJZCgnaW50JykgYXMgSFRNTE91dHB1dEVsZW1lbnRcbmNvbnN0ICRyZXMgPSBkb2N1bWVudC5nZXRFbGVtZW50QnlJZCgncmVzJykgYXMgSFRNTE91dHB1dEVsZW1lbnRcbmNvbnN0ICRhcnJheSA9IGRvY3VtZW50LmdldEVsZW1lbnRCeUlkKCdhcnInKSBhcyBIVE1MT3V0cHV0RWxlbWVudFxuY29uc3QgJG1lc3NhZ2UgPSBkb2N1bWVudC5nZXRFbGVtZW50QnlJZCgnbWVzc2FnZScpIGFzIEhUTUxPdXRwdXRFbGVtZW50XG5jb25zdCAkc2VuZCA9IGRvY3VtZW50LmdldEVsZW1lbnRCeUlkKCdzZW5kJykgYXMgSFRNTE91dHB1dEVsZW1lbnRcblxuY2hyb21lLnN0b3JhZ2UubG9jYWwuZ2V0KCd1dGlsc190eHQnLCAoeyB1dGlsc190eHQ6Y29kZXMgfSkgPT4ge1xuXHRpZiAoIWNvZGVzKSByZXR1cm5cblx0JGJpbi5pbm5lckhUTUwgPSBjb2Rlcy5tYXAoYyA9PiBjLnRvU3RyaW5nKDIpLnRvVXBwZXJDYXNlKCkucGFkU3RhcnQoOCwgJzAnKSkuam9pbignICcpXG5cdCRoZXguaW5uZXJIVE1MID0gY29kZXMubWFwKGMgPT4gYy50b1N0cmluZygxNikudG9VcHBlckNhc2UoKS5wYWRTdGFydCgyLCAnMCcpKS5qb2luKCcgJylcblx0JGRlYy5pbm5lckhUTUwgPSBjb2Rlcy5tYXAoYyA9PiBjLnRvU3RyaW5nKCkpLmpvaW4oJyAnKVxuXHQkdHh0LmlubmVySFRNTCA9IGNvZGVzLm1hcChjID0+IFN0cmluZy5mcm9tQ2hhckNvZGUoYykpLmpvaW4oJycpXG5cdCRhcnJheS5pbm5lckhUTUwgPSBjb2Rlcy5tYXAoYyA9PiAnMHgnICsgYy50b1N0cmluZygxNikudG9VcHBlckNhc2UoKS5wYWRTdGFydCgyLCAnMCcpKS5qb2luKCcsICcpXG59KVxubGV0IGNvZGVzOiBudW1iZXJbXSA9IFtdXG5hZGRFdmVudExpc3RlbmVyKCdpbnB1dCcsIChlOiBhbnkpID0+IHtcblx0bGV0IHZhbHVlOiBzdHJpbmcgPSBlLnRhcmdldC5pbm5lclRleHQucmVwbGFjZSgvXFxzKy8sICcnKVxuXHRzd2l0Y2ggKGUudGFyZ2V0LmlkKSB7XG5cdFx0Y2FzZSAndHh0Jzpcblx0XHRcdGNvZGVzID0gdmFsdWUgPyB2YWx1ZS5zcGxpdCgnJykubWFwKGMgPT4gYy5jaGFyQ29kZUF0KDApKSA6IFtdXG5cdFx0XHQkYmluLmlubmVySFRNTCA9IGNvZGVzLm1hcChjID0+IGMudG9TdHJpbmcoMikudG9VcHBlckNhc2UoKS5wYWRTdGFydCg4LCAnMCcpKS5qb2luKCcgJylcblx0XHRcdCRoZXguaW5uZXJIVE1MID0gY29kZXMubWFwKGMgPT4gYy50b1N0cmluZygxNikudG9VcHBlckNhc2UoKS5wYWRTdGFydCgyLCAnMCcpKS5qb2luKCcgJylcblx0XHRcdCRkZWMuaW5uZXJIVE1MID0gY29kZXMubWFwKGMgPT4gYy50b1N0cmluZygpKS5qb2luKCcgJylcblx0XHRcdCRhcnJheS5pbm5lckhUTUwgPSBjb2Rlcy5tYXAoYyA9PiAnMHgnICsgYy50b1N0cmluZygxNikudG9VcHBlckNhc2UoKS5wYWRTdGFydCgyLCAnMCcpKS5qb2luKCcsICcpXG5cdFx0XHRicmVha1xuXHRcdGNhc2UgJ2hleCc6XG5cdFx0XHR2YWx1ZSA9IGUudGFyZ2V0LmlubmVyVGV4dC5yZXBsYWNlKC9bXjAtOWEtZiBdL2lnLCAnJykudHJpbSgpXG5cdFx0XHRjb2RlcyA9IHZhbHVlID8gdmFsdWUuc3BsaXQoJyAnKS5tYXAoYyA9PiBwYXJzZUludChjLCAxNikpIDogW11cblx0XHRcdCRiaW4uaW5uZXJIVE1MID0gY29kZXMubWFwKGMgPT4gYy50b1N0cmluZygyKS50b1VwcGVyQ2FzZSgpLnBhZFN0YXJ0KDgsICcwJykpLmpvaW4oJyAnKVxuXHRcdFx0JGRlYy5pbm5lckhUTUwgPSBjb2Rlcy5tYXAoYyA9PiBjLnRvU3RyaW5nKCkpLmpvaW4oJyAnKVxuXHRcdFx0JHR4dC5pbm5lckhUTUwgPSBjb2Rlcy5tYXAoYyA9PiBTdHJpbmcuZnJvbUNoYXJDb2RlKGMpKS5qb2luKCcnKVxuXHRcdFx0JGFycmF5LmlubmVySFRNTCA9IGNvZGVzLm1hcChjID0+ICcweCcgKyBjLnRvU3RyaW5nKDE2KS50b1VwcGVyQ2FzZSgpLnBhZFN0YXJ0KDIsICcwJykpLmpvaW4oJywgJylcblx0XHRcdGJyZWFrXG5cdFx0Y2FzZSAnZGVjJzpcblx0XHRcdHZhbHVlID0gZS50YXJnZXQuaW5uZXJUZXh0LnJlcGxhY2UoL1teMC05IF0vaWcsICcnKS50cmltKClcblx0XHRcdGNvZGVzID0gdmFsdWUgPyB2YWx1ZS5zcGxpdCgnICcpLm1hcChjID0+IHBhcnNlSW50KGMsIDEwKSkgOiBbXVxuXHRcdFx0JGJpbi5pbm5lckhUTUwgPSBjb2Rlcy5tYXAoYyA9PiBjLnRvU3RyaW5nKDIpLnRvVXBwZXJDYXNlKCkucGFkU3RhcnQoOCwgJzAnKSkuam9pbignICcpXG5cdFx0XHQkaGV4LmlubmVySFRNTCA9IGNvZGVzLm1hcChjID0+IGMudG9TdHJpbmcoMTYpLnRvVXBwZXJDYXNlKCkucGFkU3RhcnQoMiwgJzAnKSkuam9pbignICcpXG5cdFx0XHQkdHh0LmlubmVySFRNTCA9IGNvZGVzLm1hcChjID0+IFN0cmluZy5mcm9tQ2hhckNvZGUoYykpLmpvaW4oJycpXG5cdFx0XHQkYXJyYXkuaW5uZXJIVE1MID0gY29kZXMubWFwKGMgPT4gJzB4JyArIGMudG9TdHJpbmcoMTYpLnRvVXBwZXJDYXNlKCkucGFkU3RhcnQoMiwgJzAnKSkuam9pbignLCAnKVxuXHRcdFx0YnJlYWtcblx0XHRjYXNlICdiaW4nOlxuXHRcdFx0dmFsdWUgPSBlLnRhcmdldC5pbm5lclRleHQucmVwbGFjZSgvW14wMSBdL2lnLCAnJykudHJpbSgpXG5cdFx0XHRjb2RlcyA9IHZhbHVlID8gdmFsdWUuc3BsaXQoJyAnKS5tYXAoYyA9PiBwYXJzZUludChjLCAyKSkgOiBbXVxuXHRcdFx0JGhleC5pbm5lckhUTUwgPSBjb2Rlcy5tYXAoYyA9PiBjLnRvU3RyaW5nKDE2KS50b1VwcGVyQ2FzZSgpLnBhZFN0YXJ0KDIsICcwJykpLmpvaW4oJyAnKVxuXHRcdFx0JGRlYy5pbm5lckhUTUwgPSBjb2Rlcy5tYXAoYyA9PiBjLnRvU3RyaW5nKCkpLmpvaW4oJyAnKVxuXHRcdFx0JHR4dC5pbm5lckhUTUwgPSBjb2Rlcy5tYXAoYyA9PiBTdHJpbmcuZnJvbUNoYXJDb2RlKGMpKS5qb2luKCcnKVxuXHRcdFx0JGFycmF5LmlubmVySFRNTCA9IGNvZGVzLm1hcChjID0+ICcweCcgKyBjLnRvU3RyaW5nKDE2KS50b1VwcGVyQ2FzZSgpLnBhZFN0YXJ0KDIsICcwJykpLmpvaW4oJywgJylcblx0XHRcdGJyZWFrXG5cdFx0Y2FzZSAnaW50Jzpcblx0XHRcdGxldCByZXMgPSAnXFxuJ1xuXHRcdFx0dmFsdWUgPSBlLnRhcmdldC5pbm5lclRleHQucmVwbGFjZSgvW14wLTldL2lnLCAnJykudHJpbSgpXG5cdFx0XHRpZiAodmFsdWUpIHtcblx0XHRcdFx0Y29uc3QgbnVtID0gcGFyc2VGbG9hdCh2YWx1ZSlcblx0XHRcdFx0Y29uc3QgYnVmID0gbmV3IFVpbnQ4QXJyYXkoNClcblx0XHRcdFx0Y29uc3QgdmlldyA9IG5ldyBEYXRhVmlldyhidWYuYnVmZmVyKVxuXG5cdFx0XHRcdHZpZXcuc2V0VWludDE2KDAsIG51bSlcblx0XHRcdFx0cmVzICs9ICdVMTYgQkUgPSAnICsgdG9IRVgoYnVmLnNsaWNlKDAsIDIpKVxuXHRcdFx0XHR2aWV3LnNldFVpbnQxNigwLCBudW0sIHRydWUpXG5cdFx0XHRcdHJlcyArPSAnVTE2IExFID0gJyArIHRvSEVYKGJ1Zi5zbGljZSgwLCAyKSlcblxuXHRcdFx0XHR2aWV3LnNldFVpbnQzMigwLCBudW0pXG5cdFx0XHRcdHJlcyArPSAnVTMyIEJFID0gJyArIHRvSEVYKGJ1Zilcblx0XHRcdFx0dmlldy5zZXRVaW50MzIoMCwgbnVtLCB0cnVlKVxuXHRcdFx0XHRyZXMgKz0gJ1UzMiBMRSA9ICcgKyB0b0hFWChidWYpXG5cblx0XHRcdFx0dmlldy5zZXRGbG9hdDMyKDAsIG51bSlcblx0XHRcdFx0cmVzICs9ICdGMzIgQkUgPSAnICsgdG9IRVgoYnVmKVxuXHRcdFx0XHR2aWV3LnNldEZsb2F0MzIoMCwgbnVtLCB0cnVlKVxuXHRcdFx0XHRyZXMgKz0gJ0YzMiBMRSA9ICcgKyB0b0hFWChidWYpXG5cdFx0XHR9XG5cdFx0XHQkcmVzLmlubmVySFRNTCA9IHJlc1xuXHRcdFx0YnJlYWtcblx0XHRkZWZhdWx0OiByZXR1cm5cblx0fVxuXHRjaHJvbWUuc3RvcmFnZS5sb2NhbC5zZXQoeyAndXRpbHNfdHh0JzogY29kZXMgfSlcbn0pXG5cbmZ1bmN0aW9uIHRvSEVYKGJ5dGVzOiBVaW50OEFycmF5KTogc3RyaW5nIHtcblx0cmV0dXJuIFsuLi5ieXRlc10ubWFwKGMgPT4gYy50b1N0cmluZygxNikudG9VcHBlckNhc2UoKS5wYWRTdGFydCgyLCAnMCcpKS5qb2luKCcgJykgKyAnXFxuJ1xufVxuXG4kc2VuZC5hZGRFdmVudExpc3RlbmVyKCdjbGljaycsIGUgPT4ge1xuXHRjaHJvbWVbJ2FwcCddLndpbmRvdy5nZXQoJ2FwcCcpLmNvbnRlbnRXaW5kb3cucG9zdE1lc3NhZ2UobmV3IFVpbnQ4QXJyYXkoWzEsIC4uLmNvZGVzXSkpXG59KVxuIl0sInNvdXJjZVJvb3QiOiIifQ== -------------------------------------------------------------------------------- /app/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "SDC", 3 | "description": "Light composer and player", 4 | "version": "2.1.2", 5 | "manifest_version": 2, 6 | "app": { 7 | "background": { 8 | "scripts": [ 9 | "js/background.js" 10 | ] 11 | } 12 | }, 13 | "icons": { 14 | "128": "img/icon.png" 15 | }, 16 | "permissions": [ 17 | "usb", 18 | "serial", 19 | "storage", 20 | { 21 | "fileSystem": [ 22 | "write", 23 | "retainEntries", 24 | "directory" 25 | ] 26 | } 27 | ] 28 | } 29 | -------------------------------------------------------------------------------- /app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "@mdi/font": "^5.9.55", 4 | "jquery": "^3.5.1", 5 | "jquery-slim": "^3.0.0", 6 | "jszip": "^3.7.0", 7 | "lodash": "^4.17.21", 8 | "material-design-icons": "^3.0.1", 9 | "micromodal": "^0.4.10", 10 | "music-tempo": "^1.0.3", 11 | "style-loader": "^3.0.0", 12 | "styled-components": "^5.2.0", 13 | "unzip": "^0.1.11", 14 | "unzip-js": "^1.0.0", 15 | "url-loader": "^4.1.1", 16 | "webpack": "^4.44.1" 17 | }, 18 | "devDependencies": { 19 | "@babel/core": "^7.11.6", 20 | "@types/chrome": "^0.0.125", 21 | "@types/jquery": "^3.5.4", 22 | "@types/unzip": "^0.1.1", 23 | "babel-loader": "^8.1.0", 24 | "css-loader": "^4.3.0", 25 | "file-loader": "^6.2.0", 26 | "ts-loader": "^8.0.5", 27 | "typescript": "^4.5.4", 28 | "webpack-cli": "^3.3.12" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /app/remote.html: -------------------------------------------------------------------------------- 1 | 2 | Utils 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 74 | 75 | 76 | -------------------------------------------------------------------------------- /app/ts/actions.ts: -------------------------------------------------------------------------------- 1 | import { sendCommand, sendFile, sendSync, serialConnect, serialDisconnect } from './serial' 2 | export * from './serial' 3 | import { 4 | set, 5 | store, 6 | cache, 7 | openShowEntry, 8 | saveShowEntry, 9 | convertTracks, 10 | DEFAULT_STORE_DATA 11 | } from './store' 12 | import { $player, $tracks } from './view' 13 | 14 | export { logHex } from './store' 15 | 16 | export async function clear() { 17 | $player.src = '' 18 | cache.audio = null 19 | cache.show = null 20 | await set({ ...DEFAULT_STORE_DATA }) 21 | } 22 | export async function save() { 23 | return new Promise(async (resolve) => { 24 | if (store.file) { 25 | chrome['fileSystem'].restoreEntry(store.file, async (entry: FileEntry) => { 26 | if (entry) await saveShowEntry(entry) 27 | resolve(null) 28 | }) 29 | } else await saveAs() 30 | }) 31 | } 32 | export async function saveAs() { 33 | return new Promise(async (resolve) => { 34 | chrome['fileSystem'].chooseEntry({ type: 'saveFile' }, async (entry: FileEntry) => { 35 | if (entry) await saveShowEntry(entry) 36 | resolve(null) 37 | }) 38 | }) 39 | } 40 | 41 | export async function open() { 42 | return new Promise((resolve) => { 43 | chrome['fileSystem'].chooseEntry({ type: 'openWritableFile' }, async (entry: FileEntry) => { 44 | if (entry) { 45 | await clear() 46 | await openShowEntry(entry) 47 | if (cache.audio) { 48 | $player.src = URL.createObjectURL(cache.audio) 49 | console.debug('set player src') 50 | } 51 | resolve(null) 52 | } else { 53 | resolve(null) 54 | } 55 | }) 56 | }) 57 | } 58 | export async function bind() { 59 | await sendCommand('#', 'PAIR') 60 | } 61 | export async function reset() { 62 | await sendCommand('#', 'RESET') 63 | } 64 | export async function restart() { 65 | await sendCommand('#', 'RESTART') 66 | } 67 | export async function play() { 68 | if (!store.connected) { 69 | await serialConnect() 70 | } 71 | await set('sync', true) 72 | $player.play() 73 | } 74 | export async function stop() { 75 | $player.currentTime = 0 76 | $player.pause() 77 | $player.src = $player.src 78 | await set({ sync: false, time: 0, ended: true, paused: false }) 79 | await sendCommand('#', 'RESTART') 80 | } 81 | export async function connect() { 82 | if (store.connected) 83 | await serialConnect() 84 | else 85 | await serialDisconnect() 86 | } 87 | export async function openUtils() { 88 | chrome['app'].window.create('../utils.html', { 89 | id: 'utils', 90 | width: 270, 91 | height: 400 92 | }) 93 | } 94 | export async function openRemote() { 95 | await stop() 96 | chrome['app'].window.create('../remote.html', { 97 | id: 'remote', 98 | width: 250, 99 | height: 250 100 | }) 101 | } 102 | export async function sendToChannelA() { 103 | await uploadShowHandle('A') 104 | // await sendCommand('#', 'RESTART') 105 | } 106 | export async function sendToChannelB() { 107 | await uploadShowHandle('B') 108 | // await sendCommand('#', 'RESTART') 109 | } 110 | export async function sendToChannelAB() { 111 | await uploadShowHandle('A') 112 | await uploadShowHandle('B') 113 | // await sendCommand('#', 'RESTART') 114 | } 115 | export async function uploadShowHandle(channel) { 116 | let index = 0 117 | const $selected: HTMLElement = $tracks.querySelector('.selected') 118 | if ($selected) { 119 | index = parseInt($selected.dataset.index, 10) 120 | } 121 | await uploadFile(createLightShowBinaryFile(index), `/show/${store.slot}${channel}.lsb`) 122 | console.info('upload completed.', index, store.slot, channel) 123 | } 124 | 125 | const NODE_ADDR = '10.1.1.1' 126 | const NODE_PORT = '11111' 127 | 128 | export async function uploadFile(file: Blob, path: string) { 129 | return new Promise((resolve, reject) => { 130 | if (!file || !path) throw 'Missing file or path' 131 | if (!path.startsWith("/")) path = "/" + path; 132 | let req = new XMLHttpRequest(); 133 | var form = new FormData(); 134 | form.append("data", file, path); 135 | // req.timeout = 5000 136 | req.open("POST", `http://${NODE_ADDR}:${NODE_PORT}/edit`, true); 137 | req.send(form); 138 | const done = () => setTimeout(() => resolve(null), 500) 139 | const fail = (err:any) => setTimeout(() => reject(err), 500) 140 | req.addEventListener('load', done) 141 | req.addEventListener('loadend', done) 142 | req.addEventListener('abort', done) 143 | req.addEventListener('error', fail) 144 | req.addEventListener('timeout', fail) 145 | // setTimeout(() => { 146 | // if (req.readyState !== req.DONE) { 147 | // req.abort() 148 | // } 149 | // }, 5000) 150 | }) 151 | } 152 | 153 | export function createLightShowBinaryFile(trackIndex: number) { 154 | return new Blob(convertTracks(store.tracks[trackIndex].frames)) 155 | } 156 | -------------------------------------------------------------------------------- /app/ts/app.ts: -------------------------------------------------------------------------------- 1 | 2 | import { init, set, store, parseAudioFile, parseShowFile, cache, openShowEntry } from './store' 3 | import { $player, $tracks, renderBeats, renderSerial, renderTimeline, renderTracks, renderWaveform, updateSize, updateTime } from './view' 4 | import { encodeMsg, sendRaw, sendSync } from './serial' 5 | import { serialConnect } from './serial' 6 | import * as actions from './actions' 7 | import $ from 'jquery' 8 | import { openRemote, openUtils } from './actions' 9 | 10 | Object.assign(window, actions) 11 | window['player'] = $player 12 | 13 | function update() { 14 | document.querySelectorAll('[data-key]').forEach((control: any) => { 15 | const key = control.dataset.key 16 | if ((control instanceof HTMLInputElement) || (control instanceof HTMLSelectElement)) { 17 | if (control.type === 'checkbox') { 18 | control['checked'] = store[key] 19 | } else 20 | control.value = store[key] 21 | } else { 22 | if (control instanceof HTMLButtonElement) { 23 | let selected = !!store[key] 24 | if (control.hasAttribute('value')) { 25 | selected = store[key] == control.getAttribute('value') 26 | } 27 | control.classList[selected ? 'add' : 'remove']('selected') 28 | } 29 | } 30 | }) 31 | } 32 | chrome.serial.onReceiveError.addListener(({ connectionId: id }) => { 33 | if (id === store.serial_connection) { 34 | const current = chrome.app.window.current() 35 | chrome.app.window.getAll().forEach(view => { 36 | if (view !== current) view.close() 37 | }) 38 | } 39 | }) 40 | 41 | chrome.storage.onChanged.addListener(async (changes) => { 42 | update() 43 | if (changes.port || changes.connection || changes.connected) { 44 | await renderSerial() 45 | } 46 | if (changes.waveform || changes.beats) { 47 | await renderWaveform() 48 | await renderBeats() 49 | } 50 | if (changes.tracks) { 51 | await renderTracks() 52 | } 53 | }) 54 | 55 | addEventListener('load', async () => { 56 | await init() 57 | await update() 58 | store.connected ? await serialConnect(true) : await renderSerial() 59 | await renderTracks() 60 | await renderWaveform() 61 | await renderBeats() 62 | requestAnimationFrame(function tick() { 63 | updateTime() 64 | requestAnimationFrame(tick) 65 | }) 66 | setInterval(renderSerial, 1000) 67 | setInterval(() => { 68 | if (store.sync) { 69 | sendSync(store.time, store.slot, store.ended, store.paused) 70 | } 71 | }, 100) 72 | if (store.file) { 73 | chrome['fileSystem'].restoreEntry(store.file, async (entry: FileEntry) => { 74 | if (entry) { 75 | await openShowEntry(entry) 76 | if (cache.audio) { 77 | $player.src = URL.createObjectURL(cache.audio) 78 | console.debug('set player src') 79 | await renderTimeline() 80 | await renderTracks() 81 | await renderWaveform() 82 | await renderBeats() 83 | } 84 | } else { 85 | console.debug('unable to restore show entry', [store.file]) 86 | await set('file', '') 87 | } 88 | }) 89 | } 90 | }) 91 | 92 | addEventListener('pause', async e => { 93 | store.paused = true 94 | store.ended = false 95 | }, true) 96 | 97 | addEventListener('play', async e => { 98 | store.paused = false 99 | store.ended = false 100 | }, true) 101 | 102 | addEventListener('ended', async e => { 103 | store.paused = false 104 | store.ended = true 105 | }, true) 106 | 107 | addEventListener('durationchange', async e => { 108 | await set('duration', Math.max($player.duration * 1000, store.duration)) 109 | // store.duration = Math.max($player.duration * 1000, store.duration) 110 | await updateSize() 111 | }, true) 112 | 113 | addEventListener('click', async (e: MouseEvent) => { 114 | const target = e.target as HTMLElement 115 | if (target.closest('*[data-key]')) { 116 | let actionTarget = target.closest('*[data-key]') as HTMLElement 117 | if (actionTarget && actionTarget instanceof HTMLButtonElement) { 118 | const { key } = actionTarget.dataset 119 | let value:any = !store[key] 120 | if (actionTarget.hasAttribute('value')) { 121 | value = actionTarget.getAttribute('value') 122 | } 123 | await set(key, value) 124 | } 125 | } 126 | if (target.closest('*[data-action]')) { 127 | let actionTarget = target.closest('*[data-action]') as HTMLElement 128 | if (actionTarget) { 129 | const { action } = actionTarget.dataset 130 | if (typeof actions[action] === 'function') { 131 | const prevContent = actionTarget.innerHTML 132 | const prevClassNames = actionTarget.getAttribute('class') 133 | actionTarget.setAttribute('class', 'mdi mdi-loading mdi-spin') 134 | actionTarget.innerHTML = '' 135 | actionTarget.setAttribute('disabled', 'true') 136 | 137 | try { 138 | await actions[action].call(actionTarget, e) 139 | actionTarget.style.color = '' 140 | } catch(err) { 141 | actionTarget.style.color = 'red' 142 | } 143 | 144 | actionTarget.setAttribute('class', prevClassNames) 145 | actionTarget.innerHTML = prevContent 146 | actionTarget.removeAttribute('disabled') 147 | } 148 | } 149 | } 150 | }) 151 | 152 | addEventListener('change', async (e) => { 153 | if (e.target instanceof HTMLInputElement || e.target instanceof HTMLSelectElement) { 154 | const { key } = e.target.dataset 155 | if (key) { 156 | if (e.target.type === 'checkbox') { 157 | await set(key, e.target['checked']) 158 | } else { 159 | await set(key, e.target.value) 160 | } 161 | } 162 | } 163 | }) 164 | 165 | addEventListener('dragover', async (e) => { 166 | e.preventDefault() 167 | }) 168 | 169 | addEventListener('drop', async (e: DragEvent) => { 170 | e.preventDefault() 171 | if (!e.dataTransfer) return 172 | for (let file of e.dataTransfer.files) { 173 | if (file.type.startsWith('audio/')) { 174 | const audio = await parseAudioFile(file) 175 | if (audio) { 176 | cache.audio = file 177 | $player.src = URL.createObjectURL(file) 178 | console.debug('set player src') 179 | } 180 | } 181 | if (file.name.endsWith('lt3')) { 182 | const show = await parseShowFile(file) 183 | if (show) await set(show) 184 | } 185 | } 186 | }) 187 | 188 | addEventListener('wheel', (e) => { 189 | const delta = (e.deltaX + e.deltaY) / 2 190 | if ($player.src) $player.currentTime = Math.max(0, Math.min(store.duration / 1000, $player.currentTime + (delta / (e.altKey ? 200 : 10)))) 191 | else store.time = Math.max(0, Math.min(store.duration, store.time + (delta * (e.altKey ? 200 : 10)))) 192 | }) 193 | 194 | addEventListener('mousedown', (e) => { 195 | const $track = e.target.closest('.track') 196 | if ($track) { 197 | const $selected = $track.parentElement.querySelector('.selected') 198 | if ($selected) { 199 | $selected.classList.remove('selected') 200 | } 201 | $track.classList.add('selected') 202 | $track.focus() 203 | } 204 | }) 205 | 206 | addEventListener('keydown', (e) => { 207 | e.preventDefault() 208 | switch (e.key) { 209 | case ' ': 210 | store.ended || $player.paused ? actions.play() : $player.pause() 211 | // $player.paused || $player.ended ? $player.play() : $player.pause() 212 | break 213 | case 'ArrowLeft': 214 | $player.currentTime -= e.altKey ? 0.01 : 1 215 | break 216 | case 'ArrowRight': 217 | $player.currentTime += e.altKey ? 0.01 : 1 218 | break 219 | case 'ArrowUp': 220 | $player.volume = Math.min($player.volume + 0.1, 1) 221 | break 222 | case 'ArrowDown': 223 | $player.volume = Math.max($player.volume - 0.1, 0) 224 | break 225 | } 226 | }) 227 | 228 | addEventListener('message', e => { 229 | const type = e.data[0] 230 | const data = e.data.slice(1) 231 | switch (type) { 232 | case 0: 233 | sendRaw(data) 234 | break 235 | case 1: 236 | sendRaw(encodeMsg(data)) 237 | break 238 | case 2: 239 | sendRaw(encodeMsg([35, 62, ...data])) 240 | break 241 | } 242 | }) 243 | -------------------------------------------------------------------------------- /app/ts/background.ts: -------------------------------------------------------------------------------- 1 | chrome['app'].runtime.onLaunched.addListener(function () { 2 | chrome['app'].window.create('../app.html', { 3 | id: 'app', 4 | frame: "none", 5 | minWidth: 900, 6 | minHeight: 100 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /app/ts/global.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace chrome { 2 | const app: any 3 | const serial: any 4 | const storage: any 5 | } 6 | 7 | declare namespace window {} 8 | 9 | declare interface EventTarget extends EventTarget { 10 | closest(...args:any): any 11 | } 12 | declare interface HTMLSelectElement extends HTMLSelectElement { 13 | value: string|number 14 | } 15 | 16 | declare interface Track { 17 | [key: string]: any 18 | } 19 | 20 | declare interface TrackData { 21 | [key:string]: any 22 | } 23 | 24 | declare type WaveformData = [ 25 | number, // average positive 26 | number, // average negative 27 | number, // max positive 28 | number, // max negative 29 | ] 30 | 31 | declare interface AudioData { 32 | name?: string 33 | duration?: number 34 | tempo?: number 35 | beats?: number[] 36 | waveform?: WaveformData[] 37 | } 38 | 39 | declare interface ShowData { 40 | [key:string]: any 41 | } 42 | 43 | declare interface StoreData { 44 | 45 | port: string 46 | connection: number 47 | connected: boolean 48 | 49 | sync: boolean 50 | file: string 51 | time: number 52 | slot: number 53 | channel: string 54 | duration: number 55 | paused: boolean 56 | ended: boolean 57 | 58 | tempo: number 59 | division: number 60 | 61 | beats: number[] 62 | tracks: TrackData[] 63 | waveform: WaveformData[] 64 | 65 | show: ShowData 66 | audio: AudioData 67 | } 68 | 69 | declare interface CacheData { 70 | audio: file, 71 | show: file 72 | } 73 | 74 | declare function map(value: number, fromMin: number, fromMax: number, toMin: number, toMax: number): number 75 | declare function constrain(value: number, min: number, max: number): number 76 | -------------------------------------------------------------------------------- /app/ts/remote.ts: -------------------------------------------------------------------------------- 1 | 2 | addEventListener('message', e => { 3 | console.debug('message', e.data) 4 | }) 5 | addEventListener('click', e => { 6 | const target = e.target as HTMLButtonElement 7 | const data = (target.dataset.send || '').split(' ').map(hex => parseInt(hex, 16)) 8 | const app = chrome['app'].window.get('app').contentWindow 9 | for (let i=0; i<1; i++) { 10 | setTimeout(function() { 11 | app.postMessage(new Uint8Array([2, ...data])) 12 | }, i * 10) 13 | } 14 | }) 15 | // let state = false 16 | // addEventListener('keydown', e => { 17 | // if (state) { 18 | // document.getElementById('off').click() 19 | // } else { 20 | // document.getElementById('green').click() 21 | // } 22 | // state = !state 23 | // }) 24 | addEventListener('load', e => { 25 | document.body.focus() 26 | }) 27 | -------------------------------------------------------------------------------- /app/ts/serial.ts: -------------------------------------------------------------------------------- 1 | import { set, store } from "./store" 2 | 3 | let handler = null 4 | 5 | chrome.serial.on 6 | chrome.serial.onReceive.addListener(async ({ connectionId: id, data }) => { 7 | if (id === store.connection) { 8 | (new Uint8Array(data)).forEach((byte) => decodeMsg(byte)) 9 | } 10 | }) 11 | chrome.serial.onReceiveError.addListener(async ({ connectionId: id, error }) => { 12 | if (id === store.connection) { 13 | console.debug("serial closed:", error) 14 | await set({ connection: 0, connected: false }) 15 | } 16 | }) 17 | 18 | export async function onSerialMessage(cb) { 19 | handler = cb 20 | } 21 | 22 | export async function serialConnect(force = false) { 23 | await set({ connected: false }) 24 | return new Promise(resolve => { 25 | if (!force && store.connection) { 26 | chrome.serial.getConnections(async (connections) => { 27 | const found = connections.find((conn) => conn.connectionId == store.connection) 28 | console.debug(connections) 29 | if (!found) { 30 | resolve(await serialConnect(true)) 31 | } else { 32 | console.debug('serial resumed:', found.connectionId, store.port) 33 | await set({ connection: found.connectionId, connected: true }) 34 | resolve(found.connectionId) 35 | } 36 | }) 37 | } else { 38 | const options = { 39 | name: 'connection', 40 | bufferSize: 255, 41 | bitrate: 115200, 42 | receiveTimeout: 0 43 | } 44 | chrome.serial.connect(store.port, options, async (conn) => { 45 | if (conn) { 46 | console.debug('serial connected:', conn.connectionId, store.port) 47 | await set({ connection: conn.connectionId, connected: true }) 48 | } else { 49 | console.debug('can not connect to serial port', store.port) 50 | await set({ connection: 0, connected: false }) 51 | } 52 | resolve(conn) 53 | }) 54 | } 55 | }) 56 | } 57 | 58 | export async function serialDisconnect() { 59 | await set('connected', false) 60 | return new Promise(resolve => { 61 | chrome.serial.disconnect(store.connection, resolve) 62 | }) 63 | } 64 | 65 | let responseHandler:Function; 66 | 67 | export async function sendRaw(data: ArrayBuffer, waitResponse = false) { 68 | // if (responseHandler) return 69 | return new Promise(async (resolve, reject) => { 70 | if (store.port && !store.connected) { 71 | await serialConnect() 72 | if (store.connected) { 73 | chrome.serial.send(store.connection, data, resolve) 74 | console.log('serial send>>>>>', data) 75 | } else resolve(null) 76 | } 77 | else if (store.connected) { 78 | chrome.serial.send(store.connection, data, resolve) 79 | // if (waitResponse) { 80 | // responseHandler = async (err = true) => { 81 | // if (err) { 82 | // reject() 83 | // } else { 84 | // resolve(null) 85 | // } 86 | // responseHandler = null 87 | // } 88 | // setTimeout(responseHandler, 100) 89 | // } else resolve(null) 90 | } else resolve(null) 91 | }) 92 | } 93 | 94 | export async function sendCommand(id: string, head: string, ...bytes: number[]): Promise { 95 | // console.debug('send', id, head, ...bytes.map(c => c.toString(16).padStart(2, '0'))) 96 | const i = id.split('').map(c => c.charCodeAt(0)) 97 | const h = head.split('').map(c => c.charCodeAt(0)) 98 | await sendRaw(encodeMsg([...i, 62, ...h, ...bytes])) 99 | } 100 | 101 | export function hexStr(arr: number[]) { 102 | return arr.map(n => n.toString(16).padStart(2, '0')).join(' ') 103 | } 104 | 105 | export function readStr(arr: number[], len = 1, pos = 0) { 106 | let output = '' 107 | for (let i = pos; i < pos + len; i++) 108 | output += String.fromCharCode(arr[i]) 109 | return output 110 | } 111 | 112 | export function equals(a: number[], b: number[], size: number, offset: number = 0): boolean { 113 | for (let i = offset; i < offset + size; i++) { 114 | if (a[i] !== b[i]) return false 115 | } 116 | return true 117 | } 118 | 119 | const header = [36] 120 | const buffer: number[] = [] 121 | let crc = 0; 122 | let length = 0; 123 | let synced = false; 124 | 125 | function isValidStart() { 126 | return synced = synced || equals(buffer, header, header.length); 127 | } 128 | function isValidSize() { 129 | return length >= header.length + buffer[1] + 2; 130 | } 131 | function checksum() { 132 | return buffer[length - 1] == crc % 256; 133 | } 134 | export function decodeMsg(byte: number) { 135 | buffer[length++] = byte; 136 | if (isValidStart() && isValidSize() && checksum()) { 137 | synced = false; 138 | let frame = buffer.slice(2, buffer[1] + 2) 139 | if (responseHandler && equals(frame, [35, 60, 79, 75], 4)) { 140 | responseHandler(false) 141 | } else if (handler) handler(frame) 142 | } 143 | if (!synced) { 144 | crc = 0; 145 | length = 0; 146 | } else { 147 | crc += byte; 148 | } 149 | } 150 | export function encodeMsg(input: number[]) { 151 | const output: number[] = [] 152 | output[0] = 36 // header 153 | output[1] = input.length 154 | let crc = output[0] + output[1] 155 | for (let i = 0; i < input.length; i++) { 156 | output[i + 2] = input[i] 157 | crc += output[i + 2]; 158 | } 159 | output.push(crc) 160 | return new Uint8Array(output) 161 | } 162 | 163 | export async function sendSync(time: number, show: number, ended: boolean, paused: boolean) { 164 | const data = new Uint8Array(8) 165 | const view = new DataView(data.buffer) 166 | view.setUint32(0, time, true) 167 | view.setUint8(4, show) 168 | view.setUint8(5, ended ? 1 : 0) 169 | view.setUint8(6, paused ? 1 : 0) 170 | view.setUint8(7, 0xFF) 171 | await sendCommand('#', 'SYNC', ...data); 172 | } 173 | export async function sendFile(file: File | Blob, path?: string, id: string = '#') { 174 | console.debug('begin uploading') 175 | let bytesSent = 0 176 | if (file instanceof File) 177 | file = new Blob([file]) 178 | const bytesLength = file.size; 179 | const buffer = await file.arrayBuffer() 180 | await sendCommand(id, 'FBEGIN' + path) 181 | await delay(500); 182 | let count = 0 183 | while (bytesSent < bytesLength) { 184 | const start = bytesSent 185 | const end = Math.min(bytesSent + 16, bytesLength) 186 | await sendCommand(id, 'FWRITE', ...new Uint8Array(buffer, start, end - start)) 187 | bytesSent = end 188 | if (count++) await delay(2); 189 | else await delay(500); 190 | } 191 | await sendCommand(id, 'FCLOSE') 192 | await delay(100); 193 | console.debug('file uploaded') 194 | } 195 | 196 | export async function delay(ms: number = 1000) { 197 | return new Promise(resolve => { 198 | setTimeout(resolve, ms) 199 | }) 200 | } 201 | -------------------------------------------------------------------------------- /app/ts/store.ts: -------------------------------------------------------------------------------- 1 | import { $player } from './view' 2 | 3 | Object.defineProperty(window, 'localStorage', { value: null }) 4 | 5 | import JSZip from 'jszip' 6 | import unzip from 'unzip-js' 7 | import MusicTempo from 'music-tempo' 8 | import flattenDeep from 'lodash/flattenDeep' 9 | import { serialDisconnect } from './serial' 10 | 11 | export const cache: CacheData = { 12 | audio: null, 13 | show: null 14 | } 15 | export const DEFAULT_SHOW_DATA = { 16 | selected: 1, 17 | time: 0, 18 | duration: 0, 19 | markers: [], 20 | tracks: [] 21 | } 22 | export const DEFAULT_AUDIO_DATA = { 23 | url: null, 24 | beats: [], 25 | duration: 0, 26 | waveform: [] 27 | } 28 | export const DEFAULT_STORE_DATA: StoreData = { 29 | 30 | port: '', 31 | connection: 0, 32 | connected: false, 33 | 34 | sync: false, 35 | file: '', 36 | time: 0, 37 | slot: 1, 38 | channel: 'A', 39 | duration: 0, 40 | paused: false, 41 | ended: false, 42 | 43 | tempo: 120, 44 | division: 4, 45 | 46 | tracks: [], 47 | beats: [], 48 | waveform: [], 49 | 50 | show: null, 51 | audio: null 52 | } 53 | export const store: StoreData | any = Object.create(DEFAULT_STORE_DATA) 54 | window['store'] = store 55 | 56 | chrome.storage.onChanged.addListener(async (changes) => { 57 | for (let key in changes) { 58 | store[key] = changes[key].newValue 59 | } 60 | if (changes.port) { 61 | serialDisconnect() 62 | } 63 | // console.debug('changed', [changes]) 64 | // await renderTracks() 65 | // await renderWaveform() 66 | // await renderBeats() 67 | }) 68 | 69 | export async function init() { 70 | console.debug('init storage') 71 | return new Promise(resolve => { 72 | chrome.storage.local.get(async (res: any) => { 73 | // const { tracks, duration, solution, ...data } = res 74 | Object.assign(store, res) 75 | resolve(store) 76 | }) 77 | }) 78 | } 79 | 80 | export async function set(key: string | object, value?: any) { 81 | return new Promise(resolve => { 82 | if (typeof key === 'string') { 83 | chrome.storage.local.set({ [key]: value } as any, resolve) 84 | } else if (typeof key === 'object') { 85 | chrome.storage.local.set(key, resolve) 86 | } else { 87 | resolve(null) 88 | } 89 | }) 90 | } 91 | 92 | export async function parseShowFile(file: File | Blob): Promise { 93 | console.debug('parse show file') 94 | return new Promise(async (resolve) => { 95 | if (file instanceof File) file = new Blob([file], { type: file.type }) 96 | let result = null 97 | const body = await file.text() 98 | try { 99 | result = JSON.parse(body) 100 | } catch (e) { 101 | console.debug(e) 102 | console.debug(await file.text()) 103 | } 104 | if (file.type !== 'lmp' && result) result = { 105 | tracks: parseShowTracks(result.tracks) 106 | } 107 | resolve(result) 108 | }) 109 | } 110 | 111 | export async function parseAudioFile(file: File | Blob): Promise { 112 | console.debug('parse audio file') 113 | return {} 114 | // const { duration, waveform, beats, tempo }:any = await parseAudio(file) 115 | // return { duration: Math.max(duration, store.duration), waveform, beats, tempo } 116 | } 117 | 118 | export async function parseAudio(file: File | Blob | ArrayBuffer): Promise { 119 | return new Promise(async (resolve, reject) => { 120 | let name = '' 121 | let buffer = file 122 | if (buffer instanceof File) { 123 | name = buffer.name 124 | buffer = new Blob([buffer]) 125 | } 126 | if (buffer instanceof Blob) 127 | buffer = await buffer.arrayBuffer() 128 | new AudioContext().decodeAudioData(buffer, res => { 129 | console.debug('decoded audio', [res]) 130 | const data = res.getChannelData(0) 131 | if (res.numberOfChannels == 2) { 132 | const data2 = res.getChannelData(1) 133 | for (let i in data) { 134 | data[i] = (data[i] + data2[i]) / 2 135 | } 136 | } 137 | console.debug('getting music tempo') 138 | const { beats, tempo } = new MusicTempo(data) as { beats: Float32Array, tempo: number } 139 | resolve({ 140 | name, 141 | tempo, 142 | beats: [...new Uint32Array(beats.map(v => v * 1000))], 143 | duration: Math.round(res.duration * 1000), 144 | waveform: parseWaveform(data, res.duration * 100) 145 | } as AudioData) 146 | }, reject) 147 | }) 148 | } 149 | 150 | export function parseWaveform(audioData: Float32Array, width: number): WaveformData[] { 151 | const step = Math.round(audioData.length / width) 152 | const waveformData: WaveformData[] = [] 153 | let x = 0, 154 | sumPositive = 0, 155 | sumNegative = 0, 156 | maxPositive = 0, 157 | maxNegative = 0, 158 | kNegative = 0, 159 | kPositive = 0, 160 | drawIdx = step 161 | for (let i = 0; i < audioData.length; i++) { 162 | if (i == drawIdx) { 163 | waveformData.push([ 164 | Math.round(sumPositive / kPositive * 100) || 0, 165 | Math.round(sumNegative / kNegative * 100) || 0, 166 | Math.round(maxPositive * 100) || 0, 167 | Math.round(maxNegative * 100) || 0 168 | ]) 169 | x++ 170 | drawIdx += step 171 | sumPositive = 0 172 | sumNegative = 0 173 | maxPositive = 0 174 | maxNegative = 0 175 | kNegative = 0 176 | kPositive = 0 177 | } else { 178 | if (audioData[i] < 0) { 179 | sumNegative += audioData[i] 180 | kNegative++ 181 | if (maxNegative > audioData[i]) maxNegative = audioData[i] 182 | } else { 183 | sumPositive += audioData[i] 184 | kPositive++ 185 | if (maxPositive < audioData[i]) maxPositive = audioData[i] 186 | } 187 | } 188 | } 189 | return waveformData 190 | } 191 | 192 | export function parseShowTracks(tracks: any[]) { 193 | function rgb({ r, g, b }: { r: number, g: number, b: number }) { 194 | return [r, g, b].map(v => Math.round(v * 255)) 195 | } 196 | function hex(num: number) { 197 | return num.toString(16).padStart(2, '0').toUpperCase() 198 | } 199 | function frame({ type, startTime: start, endTime: end, color, colorEnd, colorStart, period, spacing, ratio }: any) { 200 | const data: any = { 201 | type, 202 | start, 203 | duration: end - start, 204 | color: [] 205 | } 206 | if (color) data.color[0] = '#' + rgb(color).map(hex).join('') 207 | if (colorStart) data.color[0] = '#' + rgb(colorStart).map(hex).join('') 208 | if (colorEnd) data.color[1] = '#' + rgb(colorEnd).map(hex).join('') 209 | if (typeof period !== 'undefined') data.period = period 210 | if (typeof ratio !== 'undefined') data.ratio = ratio 211 | if (typeof spacing !== 'undefined') data.spacing = spacing 212 | return data 213 | } 214 | return tracks.map(({ name, trackType: type, device, elements }: any) => { 215 | return { 216 | type, 217 | name, 218 | device, 219 | frames: (elements || []).map(frame) 220 | } 221 | }) 222 | } 223 | 224 | export async function openShowEntry(entry: FileEntry) { 225 | return new Promise(async (resolve, reject) => { 226 | const upName = entry.name.toUpperCase() 227 | if (upName.endsWith('LMP') || upName.endsWith('LTP')) { 228 | await set('file', chrome['fileSystem'].retainEntry(entry)) 229 | } 230 | entry.file(file => { 231 | unzip(file, (err: any, zip) => { 232 | if (err) reject(err) 233 | function callback(err: any, entries) { 234 | let ended = 0 235 | entries.forEach(entry => { 236 | const { name } = entry 237 | let show, audio 238 | zip.readEntryData(entry, false, (err, stream) => { 239 | if (err) return resolve(null) 240 | let content: Uint8Array[] = [] 241 | stream.on('data', (data: Uint8Array) => { 242 | content.push(data) 243 | }) 244 | stream.on('end', async () => { 245 | switch (name) { 246 | case 'project.lt3': 247 | cache.show = new Blob(content, { type: 'lt3' }) 248 | show = await parseShowFile(cache.show) 249 | if (show) await set(show) 250 | break 251 | case 'project.json': 252 | cache.show = new Blob(content, { type: 'lmp' }) 253 | show = await parseShowFile(cache.show) 254 | if (show) await set(show) 255 | break 256 | default: 257 | if (name.endsWith('.mp3')) { 258 | cache.audio = new Blob(content, { type: 'mp3' }) 259 | audio = await parseAudioFile(cache.audio) 260 | if (audio) { 261 | await set(audio) 262 | $player.src = URL.createObjectURL(cache.audio) 263 | } 264 | } 265 | } 266 | if (++ended === entries.length) { 267 | resolve(null) 268 | } 269 | }) 270 | }) 271 | }) 272 | } 273 | zip.readEntries(callback, reject) 274 | }) 275 | }) 276 | }) 277 | } 278 | 279 | export async function saveShowEntry(entry: FileEntry) { 280 | if (!entry) return 281 | const upName = entry.name.toUpperCase() 282 | console.debug(`open file`, [entry.fullPath]) 283 | if (upName.endsWith('LMP')) { 284 | try { 285 | await set('file', chrome['fileSystem'].retainEntry(entry)) 286 | } 287 | catch (e) { 288 | console.debug(e) 289 | } 290 | } 291 | return new Promise((resolve, reject) => { 292 | chrome.storage.local.get(async ({ show, audio, ...data }) => { 293 | entry.createWriter(async (writer: FileWriter) => { 294 | const zip = new JSZip() 295 | zip.file('project.json', JSON.stringify(data)) 296 | if (cache.audio) zip.file('audio.mp3', cache.audio) 297 | zip.generateAsync({ type: 'blob' }).then(content => { 298 | writer.write(content) 299 | writer.addEventListener('writeend', async () => { 300 | if (entry.fullPath.toUpperCase().endsWith('LMP')) { 301 | await set('file', chrome['fileSystem'].retainEntry(entry)) 302 | } else { 303 | await set('file', '') 304 | } 305 | console.debug(`save LMP file`, [entry.fullPath]) 306 | resolve(null) 307 | }) 308 | }).catch(reject) 309 | }) 310 | }) 311 | }) 312 | } 313 | 314 | interface Frame { 315 | type: number 316 | start: number 317 | duration?: number 318 | transition?: number 319 | r?: number 320 | g?: number 321 | b?: number 322 | } 323 | 324 | export function createBinaryFrame({ type, start, duration, transition, r, g, b }: Frame) { 325 | const data = new Uint8Array(16) 326 | const view = new DataView(data.buffer) 327 | view.setUint8(0, type || 1) 328 | view.setUint8(1, r || 0) 329 | view.setUint8(2, g || 0) 330 | view.setUint8(3, b || 0) 331 | view.setUint32(4, start || 0, true) 332 | view.setUint32(8, duration || 0, true) 333 | view.setUint32(12, transition || 0, true) 334 | return data 335 | } 336 | 337 | export function createColorFrame(start: number, duration: number, transition: number, r: number, g: number, b: number) { 338 | return createBinaryFrame({ type: 1, start, duration, transition, r, g, b }) 339 | } 340 | 341 | export function createEndFrame(start: number) { 342 | return createBinaryFrame({ type: 2, start }) 343 | } 344 | 345 | export function createLoopFrame(start: number, duration: number) { 346 | return createBinaryFrame({ type: 3, start, duration }) 347 | } 348 | 349 | function hex2rgb(hex: string): [number, number, number] { 350 | return [ 351 | parseInt(hex[1] + hex[2], 16), 352 | parseInt(hex[3] + hex[4], 16), 353 | parseInt(hex[5] + hex[6], 16) 354 | ] 355 | } 356 | 357 | export function convertFrame({ type, color, start, duration, ratio, spacing, period }) { 358 | let output 359 | switch (type) { 360 | case 2: // solid 361 | output = [ 362 | createColorFrame(start, duration, 0, ...hex2rgb(color[0])) 363 | ] 364 | break 365 | case 3: // gradient 366 | output = [ 367 | createColorFrame(start, 0, 0, ...hex2rgb(color[0])), 368 | createColorFrame(start, duration, duration, ...hex2rgb(color[1])) 369 | ] 370 | break 371 | case 4: // flash 372 | const dur = period * (ratio / 100) 373 | output = [ 374 | createLoopFrame(start, duration), 375 | createColorFrame(0, dur, 0, ...hex2rgb(color[0])), 376 | createColorFrame(dur, period - dur, 0, ...hex2rgb(color[1])), 377 | createEndFrame(period) 378 | ] 379 | break 380 | case 5: // rainbow 381 | const seg = Math.floor(period * 10 / 7) 382 | console.log(seg) 383 | output = [ 384 | createLoopFrame(start, duration), 385 | createColorFrame(seg * 0, seg, seg, 255, 0, 0), 386 | createColorFrame(seg * 1, seg, seg, 255, 165, 0), 387 | createColorFrame(seg * 2, seg, seg, 255, 255, 0), 388 | createColorFrame(seg * 3, seg, seg, 0, 128, 0), 389 | createColorFrame(seg * 4, seg, seg, 0, 255, 255), 390 | createColorFrame(seg * 5, seg, seg, 0, 0, 225), 391 | createColorFrame(seg * 6, seg, seg, 238, 130, 238), 392 | createEndFrame(seg * 7) 393 | ] 394 | break 395 | case 6: // dots 396 | output = [ 397 | createLoopFrame(start, duration), 398 | createColorFrame(0, 1, 0, ...hex2rgb(color[0])), 399 | createColorFrame(1, spacing, 0, 0, 0, 0), 400 | createEndFrame(spacing + 1) 401 | ] 402 | break 403 | case 7: // pulse 404 | // period = period * 10 405 | if (period < 24) period = 24 406 | output = [ 407 | createLoopFrame(start, duration), 408 | createColorFrame(0, 10, 0, 0, 0, 0), 409 | createColorFrame(10, 2, 0, 255, 255, 255), 410 | createColorFrame(12, 10, 0, 0, 0, 0), 411 | createColorFrame(22, period - 22, 0, ...hex2rgb(color[0])), 412 | createEndFrame(period) 413 | ] 414 | break 415 | default: 416 | output = [] 417 | } 418 | return output 419 | } 420 | 421 | export function convertTracks(tracks) { 422 | const buf = [] 423 | tracks.forEach((track: any, index: any) => { 424 | if (index === 0) { 425 | buf.push(...new Array(createColorFrame(0, track.start, 0, 0, 0, 0))) 426 | } else if (index > 0 && index < tracks.length) { 427 | const last = tracks[index - 1] 428 | const gap = track.start - (last.start + last.duration) 429 | if (gap > 0) buf.push(...new Array(createColorFrame(last.start + last.duration, gap, 0, 0, 0, 0))) 430 | } 431 | buf.push(...new Array(convertFrame(track))) 432 | if (index === tracks.length - 1) { 433 | buf.push(...new Array(createEndFrame(track.start + track.duration))) 434 | } 435 | }) 436 | return flattenDeep(buf) 437 | } 438 | 439 | export function logHex(data) { 440 | console.debug(data.map((c: number) => c.toString(16).padStart(2, '0')).join(' ')) 441 | } 442 | 443 | 444 | window['convert'] = convertTracks 445 | -------------------------------------------------------------------------------- /app/ts/utils.ts: -------------------------------------------------------------------------------- 1 | const $txt = document.getElementById('txt') as HTMLOutputElement 2 | const $hex = document.getElementById('hex') as HTMLOutputElement 3 | const $dec = document.getElementById('dec') as HTMLOutputElement 4 | const $bin = document.getElementById('bin') as HTMLOutputElement 5 | const $int = document.getElementById('int') as HTMLOutputElement 6 | const $res = document.getElementById('res') as HTMLOutputElement 7 | const $array = document.getElementById('arr') as HTMLOutputElement 8 | const $message = document.getElementById('message') as HTMLOutputElement 9 | const $send = document.getElementById('send') as HTMLOutputElement 10 | 11 | chrome.storage.local.get('utils_txt', ({ utils_txt:codes }) => { 12 | if (!codes) return 13 | $bin.innerHTML = codes.map(c => c.toString(2).toUpperCase().padStart(8, '0')).join(' ') 14 | $hex.innerHTML = codes.map(c => c.toString(16).toUpperCase().padStart(2, '0')).join(' ') 15 | $dec.innerHTML = codes.map(c => c.toString()).join(' ') 16 | $txt.innerHTML = codes.map(c => String.fromCharCode(c)).join('') 17 | $array.innerHTML = codes.map(c => '0x' + c.toString(16).toUpperCase().padStart(2, '0')).join(', ') 18 | }) 19 | let codes: number[] = [] 20 | addEventListener('input', (e: any) => { 21 | let value: string = e.target.innerText.replace(/\s+/, '') 22 | switch (e.target.id) { 23 | case 'txt': 24 | codes = value ? value.split('').map(c => c.charCodeAt(0)) : [] 25 | $bin.innerHTML = codes.map(c => c.toString(2).toUpperCase().padStart(8, '0')).join(' ') 26 | $hex.innerHTML = codes.map(c => c.toString(16).toUpperCase().padStart(2, '0')).join(' ') 27 | $dec.innerHTML = codes.map(c => c.toString()).join(' ') 28 | $array.innerHTML = codes.map(c => '0x' + c.toString(16).toUpperCase().padStart(2, '0')).join(', ') 29 | break 30 | case 'hex': 31 | value = e.target.innerText.replace(/[^0-9a-f ]/ig, '').trim() 32 | codes = value ? value.split(' ').map(c => parseInt(c, 16)) : [] 33 | $bin.innerHTML = codes.map(c => c.toString(2).toUpperCase().padStart(8, '0')).join(' ') 34 | $dec.innerHTML = codes.map(c => c.toString()).join(' ') 35 | $txt.innerHTML = codes.map(c => String.fromCharCode(c)).join('') 36 | $array.innerHTML = codes.map(c => '0x' + c.toString(16).toUpperCase().padStart(2, '0')).join(', ') 37 | break 38 | case 'dec': 39 | value = e.target.innerText.replace(/[^0-9 ]/ig, '').trim() 40 | codes = value ? value.split(' ').map(c => parseInt(c, 10)) : [] 41 | $bin.innerHTML = codes.map(c => c.toString(2).toUpperCase().padStart(8, '0')).join(' ') 42 | $hex.innerHTML = codes.map(c => c.toString(16).toUpperCase().padStart(2, '0')).join(' ') 43 | $txt.innerHTML = codes.map(c => String.fromCharCode(c)).join('') 44 | $array.innerHTML = codes.map(c => '0x' + c.toString(16).toUpperCase().padStart(2, '0')).join(', ') 45 | break 46 | case 'bin': 47 | value = e.target.innerText.replace(/[^01 ]/ig, '').trim() 48 | codes = value ? value.split(' ').map(c => parseInt(c, 2)) : [] 49 | $hex.innerHTML = codes.map(c => c.toString(16).toUpperCase().padStart(2, '0')).join(' ') 50 | $dec.innerHTML = codes.map(c => c.toString()).join(' ') 51 | $txt.innerHTML = codes.map(c => String.fromCharCode(c)).join('') 52 | $array.innerHTML = codes.map(c => '0x' + c.toString(16).toUpperCase().padStart(2, '0')).join(', ') 53 | break 54 | case 'int': 55 | let res = '\n' 56 | value = e.target.innerText.replace(/[^0-9]/ig, '').trim() 57 | if (value) { 58 | const num = parseFloat(value) 59 | const buf = new Uint8Array(4) 60 | const view = new DataView(buf.buffer) 61 | 62 | view.setUint16(0, num) 63 | res += 'U16 BE = ' + toHEX(buf.slice(0, 2)) 64 | view.setUint16(0, num, true) 65 | res += 'U16 LE = ' + toHEX(buf.slice(0, 2)) 66 | 67 | view.setUint32(0, num) 68 | res += 'U32 BE = ' + toHEX(buf) 69 | view.setUint32(0, num, true) 70 | res += 'U32 LE = ' + toHEX(buf) 71 | 72 | view.setFloat32(0, num) 73 | res += 'F32 BE = ' + toHEX(buf) 74 | view.setFloat32(0, num, true) 75 | res += 'F32 LE = ' + toHEX(buf) 76 | } 77 | $res.innerHTML = res 78 | break 79 | default: return 80 | } 81 | chrome.storage.local.set({ 'utils_txt': codes }) 82 | }) 83 | 84 | function toHEX(bytes: Uint8Array): string { 85 | return [...bytes].map(c => c.toString(16).toUpperCase().padStart(2, '0')).join(' ') + '\n' 86 | } 87 | 88 | $send.addEventListener('click', e => { 89 | chrome['app'].window.get('app').contentWindow.postMessage(new Uint8Array([1, ...codes])) 90 | }) 91 | -------------------------------------------------------------------------------- /app/ts/view.ts: -------------------------------------------------------------------------------- 1 | import { store } from './store' 2 | import $ from 'jquery' 3 | 4 | export const $player = $('#player').get(0) as HTMLAudioElement 5 | export const $tempo = $('#tempo').get(0) as HTMLDivElement 6 | export const $waveform = $('#waveform').get(0) as HTMLDivElement 7 | export const $handle = $('#handle').get(0) as HTMLDivElement 8 | export const $main = $('#main').get(0) as HTMLDivElement 9 | export const $tracks = $('#tracks').get(0) as HTMLDivElement 10 | export const $timeline = $('#timeline').get(0) as HTMLDivElement 11 | 12 | export async function updateTime() { 13 | if (store.duration) { 14 | if ($player.duration && !$player.ended) { 15 | store.time = Math.round($player.currentTime * 1000) 16 | } 17 | const ratio = store.time / store.duration 18 | $main.scrollLeft = ($main.scrollWidth - $main.offsetWidth) * ratio 19 | $handle.style.left = ratio * 100 + '%' 20 | $handle.style.transform = `translateX(-${100 - Math.round(ratio * 100)}%)` 21 | } 22 | } 23 | export async function updateSize() { 24 | if (store.duration) { 25 | const width = store.duration / 10 26 | $tracks.style.width = width + 'px' 27 | $waveform.style.width = width + 'px' 28 | $tempo.style.width = width + 'px' 29 | } 30 | } 31 | 32 | let portsList = '' 33 | export async function renderSerial() { 34 | return new Promise(resolve => { 35 | chrome.serial.getDevices(devices => { 36 | // const list = devices.filter(dev => dev.path.startsWith('/dev/cu.')).map(dev => dev.path.replace('/dev/cu.', '')) 37 | // const list = devices 38 | // .filter(dev => { 39 | // return dev.path.startsWith('/dev/cu.') || dev.path.matches(/COM[0-9]+/) 40 | // }).map(dev => { 41 | // return dev.path.replace('/dev/cu.', '').replace(/^.*(COM[0-9]+)$/, '$1') 42 | // }) 43 | const list = devices.map(dev => dev.path) 44 | if (list.join() != portsList) { 45 | portsList = list.join() 46 | console.debug('serial devices', list) 47 | $('[data-key="port"]') 48 | .html('') 49 | .append(list.map(path => $(`