├── .gitignore ├── LICENSE.md ├── README.md ├── bible ├── .gitignore ├── README.md ├── assets │ ├── bundle.css │ ├── bundle.css.map │ ├── bundle.js │ ├── bundle.js.map │ └── global.css ├── config.dist.js ├── controller.html ├── css │ └── bootstrap-darkly.min.css ├── overlay.html ├── package.json ├── pnpm-lock.yaml ├── rollup.config.js └── src │ ├── App.svelte │ ├── Keypad.svelte │ ├── WebsocketConfig.svelte │ ├── main.js │ └── websocket-client.js ├── clock └── clock.html ├── css ├── bootstrap-darkly.min.css └── bootstrap.min.css ├── js ├── bootstrap.min.js └── controller.js ├── lower-third-simple ├── .gitignore ├── README.md ├── assets │ ├── bundle.css │ ├── bundle.css.map │ ├── bundle.js │ ├── bundle.js.map │ └── global.css ├── config.dist.js ├── controller-old.html ├── controller.html ├── css │ └── bootstrap-darkly.min.css ├── overlay.html ├── package.json ├── rollup.config.js └── src │ ├── App.svelte │ ├── WebsocketConfig.svelte │ ├── main.js │ └── websocket-client.js ├── lower-third ├── controller.html └── overlay.html ├── package.json ├── pnpm-lock.yaml ├── requirements.txt ├── scoreboard ├── controller.html ├── css │ └── overlay.css └── overlay.html ├── server.js └── server.py /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .DS_Store? 3 | ._* 4 | .Spotlight-V100 5 | .Trashes 6 | ehthumbs.db 7 | Thumbs.db 8 | node_modules 9 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 Filip Hanes 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 | # Websocket Overlay 2 | My other project `web-overlays` implements websocket, mqtt and gun protocols for syncing data. 3 | 4 | Simple and powerfull remote controlled html pages useful for overlays in OBS Studio, Casper CD, XSplit or simply fullscreen browser. 5 | 6 | Check out overlays project using [gundb](https://gun.eco/): https://github.com/filiphanes/gun-overlays 7 | 8 | ## Features 9 | - server holds overlay state, not only relay commands 10 | - on refresh or reconnect, state is updated from server for overlay and controller so you don't loose texts etc. 11 | - multiple overlay-controller groups on 1 server instance (via different websocket paths) 12 | - automatic reconnection every 5s 13 | 14 | # Install 15 | You can run server with Python or Nodejs. 16 | 17 | Python 3.6+ is needed. You can download it from https://www.python.org/downloads/ 18 | 19 | pip3 install websockets 20 | 21 | NodeJS is needed. You can download it from https://nodejs.org/en/ 22 | 23 | npm install ws 24 | 25 | # Run 26 | ## 1. Run websocket server 27 | Python server: 28 | 29 | python3 server.py 127.0.0.1 8089 30 | 31 | Node server: 32 | 33 | node server.js 127.0.0.1 8089 34 | 35 | ## 2. Open controller 36 | Open in browser `lower-third-simple/controller.html`. 37 | 38 | ## 3. Open overlay 39 | Open in browser `lower-third-simple/overlay.html` 40 | 41 | # Setup in playout software 42 | Setting you might need to change is websocket URI in `overlay.html` and `controller.html` in directory `your-overlay/`. 43 | FSet it to the same IP address and port as your server is running on. 44 | 45 | WEBSOCKET_URI = "ws://127.0.0.1:8089/" 46 | 47 | ## OBS Studio 48 | 1. Click the plus button under Sources 49 | 2. Select BrowserSource 50 | 3. Name the source and click "OK" 51 | 4. Check the "Local file" box 52 | 5. Click the "Browse" button on the right and select the client.html you want to use 53 | 6. Set the Resolution to 1920x1080 (Width: 1920; Height: 1080) or the overlay resolution 54 | 7. Set FPS to you stream FPS (examples: 25, 30, 50, 60) 55 | 56 | ## Caspar CG 57 | https://github.com/CasparCG/help/wiki/Media:-HTML-Templates 58 | 59 | ## ProPresenter 60 | https://learn.renewedvision.com/propresenter6/the-features-of-propresenter/web-view 61 | 62 | ## XSplit 63 | https://help.xsplit.com/en/articles/5142996-webpage 64 | 65 | 66 | # New overlays 67 | You can create your own overlay and associated controller without implementing server. 68 | 69 | ## Server API 70 | Server API is made simple and universal. Server only keeps state of overlay and broadcasts state updates from controller to all connected clients. 71 | 72 | - Controller and overlay connects to the same websocket `ws://host:port/path`. Path can be any string you choose. 73 | - Server keeps 1 state object for each path, so you can have multiple overlays with different state on one server with different path. State object is not persisted between server restarts. State is kept only in memory. 74 | - Each websocket message is json. Server keeps state of overlay in js object (dict in python). 75 | - When server recieves json it updates all key/value pairs, but not removes existing. 76 | - When new client (overlay or controller) connects to websocket, servers sends him full state in json message. 77 | - Controller is meant to send state updates (websocket messages) to server which are broadcasted to all connected overlays and controllers (you can control multiple opened overlays with multiple opened controllers). 78 | - When controller recieves message he updates his internal state. 79 | - Overlay usually only recieves updates from server, but in some situation may want to send updates from overlay (i.e. set value state after finishing animation). 80 | 81 | Usually you want to send commands to overlay like show or hide something. With state updating aproach you don't send `{"command":"show-counter"}` and `{"command":"hide-counter"}`, but rather `{"show-counter":true}` and `{"show-counter":false}`. 82 | 83 | This approach is more robust against errors on network. Controller and overlay can connect and disconnect any time and he recieves full state and don't need to replay commands from history. 84 | 85 | # Thanks 86 | This project was inspired by 87 | - https://github.com/lebaston100/htmlOverlayFramework 88 | - https://github.com/hberntsen/websocket-png-overlayer 89 | - https://github.com/Scrxtchy/ts3-overlay-ws 90 | - https://github.com/slugalisk/win-loss-overlay 91 | 92 | # TODO 93 | - more overlays 94 | - remember controller config in browser session storage 95 | - public websocket server with public overlays 96 | -------------------------------------------------------------------------------- /bible/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | config.js 3 | -------------------------------------------------------------------------------- /bible/README.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/filiphanes/websocket-overlays/14a4688f6c1b26d65e5c9a87b66004eb53723f6a/bible/README.md -------------------------------------------------------------------------------- /bible/assets/bundle.css: -------------------------------------------------------------------------------- 1 | .control-button.svelte-18fg1qh{width:6rem}.books-filter.svelte-18fg1qh,.address-filter.svelte-18fg1qh{display:block;width:49%;margin:0 1% 0 0;padding:0;height:10rem;overflow:scroll;float:left}.book-item.svelte-18fg1qh{width:100%;margin:0 0 .25rem 0;padding:.25rem .5rem;text-align:left}.address-item.svelte-18fg1qh{width:100%;margin:0 0 .25rem 0}.address-set.svelte-18fg1qh{width:auto;padding:.25rem .5rem;margin:0;text-align:left}.address-remove.svelte-18fg1qh{margin:0;max-width:2rem;padding:.25rem .5rem}.vers.svelte-18fg1qh,.address.svelte-18fg1qh{color:white}body{color:white} 2 | .keypad.svelte-1bz6mfx{display:grid;grid-template-columns:repeat(3, 2.8rem);grid-template-rows:repeat(5, 2.8rem);grid-gap:0.2rem;margin:0 0 0.5rem 0}button.svelte-1bz6mfx{margin:0;line-height:2rem;width:2.8rem} 3 | 4 | /*# sourceMappingURL=bundle.css.map */ -------------------------------------------------------------------------------- /bible/assets/bundle.css.map: -------------------------------------------------------------------------------- 1 | { 2 | "version": 3, 3 | "file": "bundle.css", 4 | "sources": [ 5 | "../src/App.svelte", 6 | "../src/Keypad.svelte" 7 | ], 8 | "sourcesContent": [ 9 | "\n\n\n\n
\n \n \n
\n
\n {#each filteredBooks as book}\n \n {/each}\n
\n
\n {#each filteredLastAddresses as address, i}\n
\n \n \n
\n {/each}\n
\n\n
\n Kapitola: {selected.chapter}\n \n
\n
\n Verš: {selected.verse}\n \n
\n\n\n\n
{line1}
\n{@html line2}\n\n", 10 | "\n\n\n\n
\n \n \n \n\n \n \n \n\n \n \n \n\n \n \n \n\n \n \n
\n" 11 | ], 12 | "names": [], 13 | "mappings": "AAgIE,eAAe,eAAC,CAAC,AACf,KAAK,CAAE,IAAI,AAEb,CAAC,AACD,4BAAa,CACb,eAAe,eAAC,CAAC,AACf,OAAO,CAAE,KAAK,CACd,KAAK,CAAE,GAAG,CACV,MAAM,CAAE,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAChB,OAAO,CAAE,CAAC,CACV,MAAM,CAAE,KAAK,CACb,QAAQ,CAAE,MAAM,CAChB,KAAK,CAAE,IAAI,AACb,CAAC,AACD,UAAU,eAAC,CAAC,AACV,KAAK,CAAE,IAAI,CACX,MAAM,CAAE,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CACpB,OAAO,CAAE,MAAM,CAAC,KAAK,CACrB,UAAU,CAAE,IAAI,AAClB,CAAC,AACD,aAAa,eAAC,CAAC,AACb,KAAK,CAAE,IAAI,CACX,MAAM,CAAE,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,AACtB,CAAC,AACD,YAAY,eAAC,CAAC,AACZ,KAAK,CAAE,IAAI,CACX,OAAO,CAAE,MAAM,CAAC,KAAK,CACrB,MAAM,CAAE,CAAC,CACT,UAAU,CAAE,IAAI,AAClB,CAAC,AACD,eAAe,eAAC,CAAC,AACf,MAAM,CAAE,CAAC,CACT,SAAS,CAAE,IAAI,CACf,OAAO,CAAE,MAAM,CAAC,KAAK,AACvB,CAAC,AACD,oBAAK,CACL,QAAQ,eAAC,CAAC,AACR,KAAK,CAAE,KAAK,AACd,CAAC,AACO,IAAI,AAAE,CAAC,AACb,KAAK,CAAE,KAAK,AACd,CAAC;ACzJD,OAAO,eAAC,CAAC,AACP,OAAO,CAAE,IAAI,CACb,qBAAqB,CAAE,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,CACxC,kBAAkB,CAAE,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,CACrC,QAAQ,CAAE,MAAM,CAChB,MAAM,CAAE,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,AACtB,CAAC,AAED,MAAM,eAAC,CAAC,AACN,MAAM,CAAE,CAAC,CACT,WAAW,CAAE,IAAI,CACjB,KAAK,CAAE,MAAM,AACf,CAAC" 14 | } -------------------------------------------------------------------------------- /bible/assets/global.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | position: relative; 3 | width: 100%; 4 | height: 100%; 5 | } 6 | 7 | body { 8 | color: #333; 9 | margin: 0; 10 | padding: 8px; 11 | box-sizing: border-box; 12 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; 13 | } 14 | 15 | a { 16 | color: rgb(0,100,200); 17 | text-decoration: none; 18 | } 19 | 20 | a:hover { 21 | text-decoration: underline; 22 | } 23 | 24 | a:visited { 25 | color: rgb(0,80,160); 26 | } 27 | 28 | label { 29 | display: block; 30 | } 31 | 32 | input, button, select, textarea { 33 | font-family: inherit; 34 | font-size: inherit; 35 | padding: 0.4em; 36 | margin: 0 0 0.5em 0; 37 | box-sizing: border-box; 38 | border: 1px solid #ccc; 39 | border-radius: 2px; 40 | } 41 | 42 | input:disabled { 43 | color: #ccc; 44 | } 45 | 46 | input[type="range"] { 47 | height: 0; 48 | } 49 | 50 | button { 51 | background-color: #f4f4f4; 52 | outline: none; 53 | } 54 | 55 | button:active { 56 | background-color: #ddd; 57 | } 58 | 59 | button:focus { 60 | border-color: #666; 61 | } -------------------------------------------------------------------------------- /bible/config.dist.js: -------------------------------------------------------------------------------- 1 | WEBSOCKET_URI = "ws://127.0.0.1:8089/bible" 2 | -------------------------------------------------------------------------------- /bible/controller.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Bible verses 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /bible/overlay.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Lower Third 6 | 7 | 49 | 50 | 51 |
52 |
Main header line
53 |
54 |
55 | 104 | 105 | 106 | -------------------------------------------------------------------------------- /bible/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "svelte-app", 3 | "version": "1.0.0", 4 | "devDependencies": { 5 | "@rollup/plugin-json": "^4.0.0", 6 | "npm-run-all": "^4.1.5", 7 | "rollup": "^1.27.5", 8 | "rollup-plugin-commonjs": "^9.3.4", 9 | "rollup-plugin-livereload": "^1.0.4", 10 | "rollup-plugin-node-resolve": "^4.2.4", 11 | "rollup-plugin-svelte": "^5.1.1", 12 | "rollup-plugin-terser": "^4.0.4", 13 | "sirv-cli": "^0.4.5", 14 | "svelte": "^3.15.0" 15 | }, 16 | "scripts": { 17 | "build": "rollup -c", 18 | "autobuild": "rollup -c -w", 19 | "dev": "run-p start:dev autobuild", 20 | "start": "sirv public", 21 | "start:dev": "sirv public --dev" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /bible/rollup.config.js: -------------------------------------------------------------------------------- 1 | import svelte from 'rollup-plugin-svelte'; 2 | import resolve from 'rollup-plugin-node-resolve'; 3 | import commonjs from 'rollup-plugin-commonjs'; 4 | import json from '@rollup/plugin-json'; 5 | import livereload from 'rollup-plugin-livereload'; 6 | import { terser } from 'rollup-plugin-terser'; 7 | 8 | const production = !process.env.ROLLUP_WATCH; 9 | 10 | export default { 11 | input: 'src/main.js', 12 | output: { 13 | sourcemap: true, 14 | format: 'iife', 15 | name: 'app', 16 | file: 'assets/bundle.js' 17 | }, 18 | plugins: [ 19 | json(), 20 | svelte({ 21 | // enable run-time checks when not in production 22 | dev: !production, 23 | // we'll extract any component CSS out into 24 | // a separate file  better for performance 25 | css: css => { 26 | css.write('assets/bundle.css'); 27 | } 28 | }), 29 | 30 | // If you have external dependencies installed from 31 | // npm, you'll most likely need these plugins. In 32 | // some cases you'll need additional configuration  33 | // consult the documentation for details: 34 | // https://github.com/rollup/rollup-plugin-commonjs 35 | resolve({ browser: true }), 36 | commonjs(), 37 | 38 | // Watch the `public` directory and refresh the 39 | // browser on changes when not in production 40 | !production && livereload('assets'), 41 | 42 | // If we're building for production (npm run build 43 | // instead of npm run dev), minify 44 | production && terser() 45 | ], 46 | watch: { 47 | clearScreen: false 48 | } 49 | }; 50 | -------------------------------------------------------------------------------- /bible/src/App.svelte: -------------------------------------------------------------------------------- 1 | 127 | 128 | 172 | 173 |
174 | 175 | 176 |
177 |
178 | {#each filteredBooks as book} 179 | 180 | {/each} 181 |
182 |
183 | {#each filteredLastAddresses as address, i} 184 |
185 | 186 | 187 |
188 | {/each} 189 |
190 | 191 |
192 | Kapitola: {selected.chapter} 193 | 194 |
195 |
196 | Verš: {selected.verse} 197 | 198 |
199 | 200 | 203 | 204 |
{line1}
205 | {@html line2} 206 | 207 | -------------------------------------------------------------------------------- /bible/src/Keypad.svelte: -------------------------------------------------------------------------------- 1 | 15 | 16 | 31 | 32 |
33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 |
52 | -------------------------------------------------------------------------------- /bible/src/WebsocketConfig.svelte: -------------------------------------------------------------------------------- 1 | 4 |
5 |
6 |
Connection settings
7 |
8 |
9 | 10 |
11 | 12 | 13 | 14 |
15 |
16 |
17 |
18 |
19 | 20 |
21 | 22 |
23 | 24 |
25 |
26 |
27 |
28 |
29 |
30 | 31 | 32 |
33 |
34 |
35 |
-------------------------------------------------------------------------------- /bible/src/main.js: -------------------------------------------------------------------------------- 1 | import App from './App.svelte'; 2 | 3 | var app = new App({ 4 | target: document.body 5 | }); 6 | 7 | export default app; -------------------------------------------------------------------------------- /bible/src/websocket-client.js: -------------------------------------------------------------------------------- 1 | //Modify this to point to the ip address where your server is running on 2 | WEBSOCKET_URI = WEBSOCKET_URI || "ws://127.0.0.1:8089/"; 3 | 4 | let websocket; 5 | let socketIsOpen = false; 6 | let intervalID = 0; 7 | let closedByUser = false; 8 | 9 | export function sendCommand(obj) { 10 | console.log(JSON.stringify(obj)); 11 | if (socketIsOpen) { 12 | websocket.send(JSON.stringify(obj)); 13 | } else { 14 | console.error('Not connected\n'); 15 | } 16 | } 17 | 18 | 19 | export function doConnect(onMessage) { 20 | websocket = new WebSocket(WEBSOCKET_URI); 21 | websocket.onopen = function(evt) { 22 | socketIsOpen = true; 23 | console.info("Connection opened"); 24 | clearInterval(intervalID); 25 | intervalID = 0; 26 | }; 27 | websocket.onclose = function(evt) { 28 | socketIsOpen = true; 29 | if (!intervalID && !closedByUser) { 30 | intervalID = setInterval(doConnect, 5000); 31 | } else if (closedByUser) { 32 | closedByUser = false; 33 | } 34 | console.info("Connection closed"); 35 | }; 36 | websocket.onmessage = function(evt) { 37 | var jsonOBJ = JSON.parse(evt.data); 38 | console.log(evt.data); 39 | onMessage(jsonOBJ); 40 | }; 41 | websocket.onerror = function(evt) { 42 | console.error('Connection failed, is the Server running?'); 43 | socketIsOpen = false; 44 | if (!intervalID) { 45 | intervalID = setInterval(doConnect, 5000); 46 | } 47 | }; 48 | } 49 | 50 | export function doDisconnect() { 51 | socketIsOpen = false; 52 | closedByUser = true; 53 | websocket.close(); 54 | } -------------------------------------------------------------------------------- /clock/clock.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 20 | 21 | 22 | 23 | 24 |
29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /js/bootstrap.min.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * Bootstrap v4.3.1 (https://getbootstrap.com/) 3 | * Copyright 2011-2019 The Bootstrap Authors (https://github.com/twbs/bootstrap/graphs/contributors) 4 | * Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE) 5 | */ 6 | !function(t,e){"object"==typeof exports&&"undefined"!=typeof module?e(exports,require("jquery"),require("popper.js")):"function"==typeof define&&define.amd?define(["exports","jquery","popper.js"],e):e((t=t||self).bootstrap={},t.jQuery,t.Popper)}(this,function(t,g,u){"use strict";function i(t,e){for(var n=0;nthis._items.length-1||t<0))if(this._isSliding)g(this._element).one(Q.SLID,function(){return e.to(t)});else{if(n===t)return this.pause(),void this.cycle();var i=ndocument.documentElement.clientHeight;!this._isBodyOverflowing&&t&&(this._element.style.paddingLeft=this._scrollbarWidth+"px"),this._isBodyOverflowing&&!t&&(this._element.style.paddingRight=this._scrollbarWidth+"px")},t._resetAdjustments=function(){this._element.style.paddingLeft="",this._element.style.paddingRight=""},t._checkScrollbar=function(){var t=document.body.getBoundingClientRect();this._isBodyOverflowing=t.left+t.right
',trigger:"hover focus",title:"",delay:0,html:!1,selector:!1,placement:"top",offset:0,container:!1,fallbackPlacement:"flip",boundary:"scrollParent",sanitize:!0,sanitizeFn:null,whiteList:Ee},je="show",He="out",Re={HIDE:"hide"+De,HIDDEN:"hidden"+De,SHOW:"show"+De,SHOWN:"shown"+De,INSERTED:"inserted"+De,CLICK:"click"+De,FOCUSIN:"focusin"+De,FOCUSOUT:"focusout"+De,MOUSEENTER:"mouseenter"+De,MOUSELEAVE:"mouseleave"+De},xe="fade",Fe="show",Ue=".tooltip-inner",We=".arrow",qe="hover",Me="focus",Ke="click",Qe="manual",Be=function(){function i(t,e){if("undefined"==typeof u)throw new TypeError("Bootstrap's tooltips require Popper.js (https://popper.js.org/)");this._isEnabled=!0,this._timeout=0,this._hoverState="",this._activeTrigger={},this._popper=null,this.element=t,this.config=this._getConfig(e),this.tip=null,this._setListeners()}var t=i.prototype;return t.enable=function(){this._isEnabled=!0},t.disable=function(){this._isEnabled=!1},t.toggleEnabled=function(){this._isEnabled=!this._isEnabled},t.toggle=function(t){if(this._isEnabled)if(t){var e=this.constructor.DATA_KEY,n=g(t.currentTarget).data(e);n||(n=new this.constructor(t.currentTarget,this._getDelegateConfig()),g(t.currentTarget).data(e,n)),n._activeTrigger.click=!n._activeTrigger.click,n._isWithActiveTrigger()?n._enter(null,n):n._leave(null,n)}else{if(g(this.getTipElement()).hasClass(Fe))return void this._leave(null,this);this._enter(null,this)}},t.dispose=function(){clearTimeout(this._timeout),g.removeData(this.element,this.constructor.DATA_KEY),g(this.element).off(this.constructor.EVENT_KEY),g(this.element).closest(".modal").off("hide.bs.modal"),this.tip&&g(this.tip).remove(),this._isEnabled=null,this._timeout=null,this._hoverState=null,(this._activeTrigger=null)!==this._popper&&this._popper.destroy(),this._popper=null,this.element=null,this.config=null,this.tip=null},t.show=function(){var e=this;if("none"===g(this.element).css("display"))throw new Error("Please use show on visible elements");var t=g.Event(this.constructor.Event.SHOW);if(this.isWithContent()&&this._isEnabled){g(this.element).trigger(t);var n=_.findShadowRoot(this.element),i=g.contains(null!==n?n:this.element.ownerDocument.documentElement,this.element);if(t.isDefaultPrevented()||!i)return;var o=this.getTipElement(),r=_.getUID(this.constructor.NAME);o.setAttribute("id",r),this.element.setAttribute("aria-describedby",r),this.setContent(),this.config.animation&&g(o).addClass(xe);var s="function"==typeof this.config.placement?this.config.placement.call(this,o,this.element):this.config.placement,a=this._getAttachment(s);this.addAttachmentClass(a);var l=this._getContainer();g(o).data(this.constructor.DATA_KEY,this),g.contains(this.element.ownerDocument.documentElement,this.tip)||g(o).appendTo(l),g(this.element).trigger(this.constructor.Event.INSERTED),this._popper=new u(this.element,o,{placement:a,modifiers:{offset:this._getOffset(),flip:{behavior:this.config.fallbackPlacement},arrow:{element:We},preventOverflow:{boundariesElement:this.config.boundary}},onCreate:function(t){t.originalPlacement!==t.placement&&e._handlePopperPlacementChange(t)},onUpdate:function(t){return e._handlePopperPlacementChange(t)}}),g(o).addClass(Fe),"ontouchstart"in document.documentElement&&g(document.body).children().on("mouseover",null,g.noop);var c=function(){e.config.animation&&e._fixTransition();var t=e._hoverState;e._hoverState=null,g(e.element).trigger(e.constructor.Event.SHOWN),t===He&&e._leave(null,e)};if(g(this.tip).hasClass(xe)){var h=_.getTransitionDurationFromElement(this.tip);g(this.tip).one(_.TRANSITION_END,c).emulateTransitionEnd(h)}else c()}},t.hide=function(t){var e=this,n=this.getTipElement(),i=g.Event(this.constructor.Event.HIDE),o=function(){e._hoverState!==je&&n.parentNode&&n.parentNode.removeChild(n),e._cleanTipClass(),e.element.removeAttribute("aria-describedby"),g(e.element).trigger(e.constructor.Event.HIDDEN),null!==e._popper&&e._popper.destroy(),t&&t()};if(g(this.element).trigger(i),!i.isDefaultPrevented()){if(g(n).removeClass(Fe),"ontouchstart"in document.documentElement&&g(document.body).children().off("mouseover",null,g.noop),this._activeTrigger[Ke]=!1,this._activeTrigger[Me]=!1,this._activeTrigger[qe]=!1,g(this.tip).hasClass(xe)){var r=_.getTransitionDurationFromElement(n);g(n).one(_.TRANSITION_END,o).emulateTransitionEnd(r)}else o();this._hoverState=""}},t.update=function(){null!==this._popper&&this._popper.scheduleUpdate()},t.isWithContent=function(){return Boolean(this.getTitle())},t.addAttachmentClass=function(t){g(this.getTipElement()).addClass(Ae+"-"+t)},t.getTipElement=function(){return this.tip=this.tip||g(this.config.template)[0],this.tip},t.setContent=function(){var t=this.getTipElement();this.setElementContent(g(t.querySelectorAll(Ue)),this.getTitle()),g(t).removeClass(xe+" "+Fe)},t.setElementContent=function(t,e){"object"!=typeof e||!e.nodeType&&!e.jquery?this.config.html?(this.config.sanitize&&(e=Se(e,this.config.whiteList,this.config.sanitizeFn)),t.html(e)):t.text(e):this.config.html?g(e).parent().is(t)||t.empty().append(e):t.text(g(e).text())},t.getTitle=function(){var t=this.element.getAttribute("data-original-title");return t||(t="function"==typeof this.config.title?this.config.title.call(this.element):this.config.title),t},t._getOffset=function(){var e=this,t={};return"function"==typeof this.config.offset?t.fn=function(t){return t.offsets=l({},t.offsets,e.config.offset(t.offsets,e.element)||{}),t}:t.offset=this.config.offset,t},t._getContainer=function(){return!1===this.config.container?document.body:_.isElement(this.config.container)?g(this.config.container):g(document).find(this.config.container)},t._getAttachment=function(t){return Pe[t.toUpperCase()]},t._setListeners=function(){var i=this;this.config.trigger.split(" ").forEach(function(t){if("click"===t)g(i.element).on(i.constructor.Event.CLICK,i.config.selector,function(t){return i.toggle(t)});else if(t!==Qe){var e=t===qe?i.constructor.Event.MOUSEENTER:i.constructor.Event.FOCUSIN,n=t===qe?i.constructor.Event.MOUSELEAVE:i.constructor.Event.FOCUSOUT;g(i.element).on(e,i.config.selector,function(t){return i._enter(t)}).on(n,i.config.selector,function(t){return i._leave(t)})}}),g(this.element).closest(".modal").on("hide.bs.modal",function(){i.element&&i.hide()}),this.config.selector?this.config=l({},this.config,{trigger:"manual",selector:""}):this._fixTitle()},t._fixTitle=function(){var t=typeof this.element.getAttribute("data-original-title");(this.element.getAttribute("title")||"string"!==t)&&(this.element.setAttribute("data-original-title",this.element.getAttribute("title")||""),this.element.setAttribute("title",""))},t._enter=function(t,e){var n=this.constructor.DATA_KEY;(e=e||g(t.currentTarget).data(n))||(e=new this.constructor(t.currentTarget,this._getDelegateConfig()),g(t.currentTarget).data(n,e)),t&&(e._activeTrigger["focusin"===t.type?Me:qe]=!0),g(e.getTipElement()).hasClass(Fe)||e._hoverState===je?e._hoverState=je:(clearTimeout(e._timeout),e._hoverState=je,e.config.delay&&e.config.delay.show?e._timeout=setTimeout(function(){e._hoverState===je&&e.show()},e.config.delay.show):e.show())},t._leave=function(t,e){var n=this.constructor.DATA_KEY;(e=e||g(t.currentTarget).data(n))||(e=new this.constructor(t.currentTarget,this._getDelegateConfig()),g(t.currentTarget).data(n,e)),t&&(e._activeTrigger["focusout"===t.type?Me:qe]=!1),e._isWithActiveTrigger()||(clearTimeout(e._timeout),e._hoverState=He,e.config.delay&&e.config.delay.hide?e._timeout=setTimeout(function(){e._hoverState===He&&e.hide()},e.config.delay.hide):e.hide())},t._isWithActiveTrigger=function(){for(var t in this._activeTrigger)if(this._activeTrigger[t])return!0;return!1},t._getConfig=function(t){var e=g(this.element).data();return Object.keys(e).forEach(function(t){-1!==Oe.indexOf(t)&&delete e[t]}),"number"==typeof(t=l({},this.constructor.Default,e,"object"==typeof t&&t?t:{})).delay&&(t.delay={show:t.delay,hide:t.delay}),"number"==typeof t.title&&(t.title=t.title.toString()),"number"==typeof t.content&&(t.content=t.content.toString()),_.typeCheckConfig(be,t,this.constructor.DefaultType),t.sanitize&&(t.template=Se(t.template,t.whiteList,t.sanitizeFn)),t},t._getDelegateConfig=function(){var t={};if(this.config)for(var e in this.config)this.constructor.Default[e]!==this.config[e]&&(t[e]=this.config[e]);return t},t._cleanTipClass=function(){var t=g(this.getTipElement()),e=t.attr("class").match(Ne);null!==e&&e.length&&t.removeClass(e.join(""))},t._handlePopperPlacementChange=function(t){var e=t.instance;this.tip=e.popper,this._cleanTipClass(),this.addAttachmentClass(this._getAttachment(t.placement))},t._fixTransition=function(){var t=this.getTipElement(),e=this.config.animation;null===t.getAttribute("x-placement")&&(g(t).removeClass(xe),this.config.animation=!1,this.hide(),this.show(),this.config.animation=e)},i._jQueryInterface=function(n){return this.each(function(){var t=g(this).data(Ie),e="object"==typeof n&&n;if((t||!/dispose|hide/.test(n))&&(t||(t=new i(this,e),g(this).data(Ie,t)),"string"==typeof n)){if("undefined"==typeof t[n])throw new TypeError('No method named "'+n+'"');t[n]()}})},s(i,null,[{key:"VERSION",get:function(){return"4.3.1"}},{key:"Default",get:function(){return Le}},{key:"NAME",get:function(){return be}},{key:"DATA_KEY",get:function(){return Ie}},{key:"Event",get:function(){return Re}},{key:"EVENT_KEY",get:function(){return De}},{key:"DefaultType",get:function(){return ke}}]),i}();g.fn[be]=Be._jQueryInterface,g.fn[be].Constructor=Be,g.fn[be].noConflict=function(){return g.fn[be]=we,Be._jQueryInterface};var Ve="popover",Ye="bs.popover",ze="."+Ye,Xe=g.fn[Ve],$e="bs-popover",Ge=new RegExp("(^|\\s)"+$e+"\\S+","g"),Je=l({},Be.Default,{placement:"right",trigger:"click",content:"",template:''}),Ze=l({},Be.DefaultType,{content:"(string|element|function)"}),tn="fade",en="show",nn=".popover-header",on=".popover-body",rn={HIDE:"hide"+ze,HIDDEN:"hidden"+ze,SHOW:"show"+ze,SHOWN:"shown"+ze,INSERTED:"inserted"+ze,CLICK:"click"+ze,FOCUSIN:"focusin"+ze,FOCUSOUT:"focusout"+ze,MOUSEENTER:"mouseenter"+ze,MOUSELEAVE:"mouseleave"+ze},sn=function(t){var e,n;function i(){return t.apply(this,arguments)||this}n=t,(e=i).prototype=Object.create(n.prototype),(e.prototype.constructor=e).__proto__=n;var o=i.prototype;return o.isWithContent=function(){return this.getTitle()||this._getContent()},o.addAttachmentClass=function(t){g(this.getTipElement()).addClass($e+"-"+t)},o.getTipElement=function(){return this.tip=this.tip||g(this.config.template)[0],this.tip},o.setContent=function(){var t=g(this.getTipElement());this.setElementContent(t.find(nn),this.getTitle());var e=this._getContent();"function"==typeof e&&(e=e.call(this.element)),this.setElementContent(t.find(on),e),t.removeClass(tn+" "+en)},o._getContent=function(){return this.element.getAttribute("data-content")||this.config.content},o._cleanTipClass=function(){var t=g(this.getTipElement()),e=t.attr("class").match(Ge);null!==e&&0=this._offsets[o]&&("undefined"==typeof this._offsets[o+1]||t\n.control-button {\n\tmax-width: 6rem;\n}\n\n\n\n
\n\t{#each lines as line, i}\n\t
\n\t\tchangeLine(i)}>\n\t\t\n\t
\n\t{/each}\n
\n" 9 | ], 10 | "names": [], 11 | "mappings": "AACA,eAAe,eAAC,CAAC,AAChB,SAAS,CAAE,IAAI,AAChB,CAAC" 12 | } -------------------------------------------------------------------------------- /lower-third-simple/assets/bundle.js: -------------------------------------------------------------------------------- 1 | var app=function(){"use strict";function n(){}function t(n){return n()}function e(){return Object.create(null)}function o(n){n.forEach(t)}function r(n){return"function"==typeof n}function c(n,t){return n!=n?t==t:n!==t||n&&"object"==typeof n||"function"==typeof n}function i(n,t){n.appendChild(t)}function l(n,t,e){n.insertBefore(t,e||null)}function u(n){n.parentNode.removeChild(n)}function s(n){return document.createElement(n)}function a(n){return document.createTextNode(n)}function f(){return a(" ")}function d(n,t,e,o){return n.addEventListener(t,e,o),()=>n.removeEventListener(t,e,o)}function h(n,t,e){null==e?n.removeAttribute(t):n.setAttribute(t,e)}function p(n,t){(null!=t||n.value)&&(n.value=t)}let g;function $(n){g=n}function m(){if(!g)throw new Error("Function called outside component initialization");return g}const v=[],y=[],_=[],b=[],w=Promise.resolve();let x=!1;function E(n){_.push(n)}function O(){const n=new Set;do{for(;v.length;){const n=v.shift();$(n),S(n.$$)}for(;y.length;)y.pop()();for(let t=0;t<_.length;t+=1){const e=_[t];n.has(e)||(e(),n.add(e))}_.length=0}while(v.length);for(;b.length;)b.pop()();x=!1}function S(n){n.fragment&&(n.update(n.dirty),o(n.before_update),n.fragment.p(n.dirty,n.ctx),n.dirty=null,n.after_update.forEach(E))}const C=new Set;function k(n,t){n.$$.dirty||(v.push(n),x||(x=!0,w.then(O)),n.$$.dirty=e()),n.$$.dirty[t]=!0}function N(c,i,l,u,s,a){const f=g;$(c);const d=i.props||{},h=c.$$={fragment:null,ctx:null,props:a,update:n,not_equal:s,bound:e(),on_mount:[],on_destroy:[],before_update:[],after_update:[],context:new Map(f?f.$$.context:[]),callbacks:e(),dirty:null};let p=!1;var m,v,y;h.ctx=l?l(c,d,(n,t,e=t)=>(h.ctx&&s(h.ctx[n],h.ctx[n]=e)&&(h.bound[n]&&h.bound[n](e),p&&k(c,n)),t)):d,h.update(),p=!0,o(h.before_update),h.fragment=u(h.ctx),i.target&&(i.hydrate?h.fragment.l((y=i.target,Array.from(y.childNodes))):h.fragment.c(),i.intro&&((m=c.$$.fragment)&&m.i&&(C.delete(m),m.i(v))),function(n,e,c){const{fragment:i,on_mount:l,on_destroy:u,after_update:s}=n.$$;i.m(e,c),E(()=>{const e=l.map(t).filter(r);u?u.push(...e):o(e),n.$$.on_mount=[]}),s.forEach(E)}(c,i.target,i.anchor),O()),$(f)}class I{$destroy(){var t,e;e=1,(t=this).$$.fragment&&(o(t.$$.on_destroy),t.$$.fragment.d(e),t.$$.on_destroy=t.$$.fragment=null,t.$$.ctx={}),this.$destroy=n}$on(n,t){const e=this.$$.callbacks[n]||(this.$$.callbacks[n]=[]);return e.push(t),()=>{const n=e.indexOf(t);-1!==n&&e.splice(n,1)}}$set(){}}let T;WEBSOCKET_URI=WEBSOCKET_URI||"ws://127.0.0.1:8089/";let B=!1,L=0,W=!1;function j(n){console.log(JSON.stringify(n)),B?T.send(JSON.stringify(n)):console.error("Not connected\n")}function A(n){(T=new WebSocket(WEBSOCKET_URI)).onopen=function(n){B=!0,console.info("Connection opened"),clearInterval(L),L=0},T.onclose=function(n){B=!0,L||W?W&&(W=!1):L=setInterval(A,5e3),console.info("Connection closed")},T.onmessage=function(t){var e=JSON.parse(t.data);console.log(t.data),n(e)},T.onerror=function(n){console.error("Connection failed, is the Server running?"),B=!1,L||(L=setInterval(A,5e3))}}function J(n,t,e){const o=Object.create(n);return o.line=t[e],o.each_value=t,o.i=e,o}function K(n){var t;return{c(){t=a("Show")},m(n,e){l(n,t,e)},d(n){n&&u(t)}}}function P(n){var t;return{c(){t=a("Hide")},m(n,e){l(n,t,e)},d(n){n&&u(t)}}}function R(n){var t,e,r,c,a,g,$;function m(){n.input_input_handler.call(e,n)}function v(...t){return n.change_handler(n,...t)}function y(n,t){return t.shown===t.i?P:K}var _=y(0,n),b=_(n);function w(...t){return n.click_handler(n,...t)}return{c(){t=s("div"),e=s("input"),r=f(),c=s("button"),b.c(),g=f(),h(e,"class","form-control"),h(e,"type","text"),h(e,"placeholder","Text"),h(c,"class",a="form-control btn "+(n.shown===n.i?"btn-danger":"btn-primary")+" control-button svelte-12wxbio"),h(t,"class","input-group"),$=[d(e,"input",m),d(e,"change",v),d(c,"click",w)]},m(o,u){l(o,t,u),i(t,e),p(e,n.line),i(t,r),i(t,c),b.m(c,null),i(t,g)},p(t,o){n=o,t.lines&&e.value!==n.line&&p(e,n.line),_!==(_=y(0,n))&&(b.d(1),(b=_(n))&&(b.c(),b.m(c,null))),t.shown&&a!==(a="form-control btn "+(n.shown===n.i?"btn-danger":"btn-primary")+" control-button svelte-12wxbio")&&h(c,"class",a)},d(n){n&&u(t),b.d(),o($)}}}function U(t){var e;let o=t.lines,r=[];for(let n=0;n=0})}function l(){e("lines",o=[...o.filter(n=>n),""])}function u(n){console.log(n),n.hasOwnProperty("lines")&&(e("lines",o=n.lines),l()),n.hasOwnProperty("shown")&&e("shown",r=n.shown)}function s(n){let t={lines:o};r===n&&(t.line1=o[n]),j(t),l()}c=(async()=>{A(u)}),m().$$.on_mount.push(c),function(n){m().$$.on_destroy.push(n)}(async()=>{B=!1,W=!0,T.close()}),l();return{lines:o,shown:r,toggleLine:i,changeLine:s,input_input_handler:function({line:n,each_value:t,i:r}){t[r]=this.value,e("lines",o)},change_handler:({i:n},t)=>s(n),click_handler:({i:n},t)=>i(n)}}return new class extends I{constructor(n){super(),N(this,n,q,U,c,[])}}({target:document.body})}(); 2 | //# sourceMappingURL=bundle.js.map 3 | -------------------------------------------------------------------------------- /lower-third-simple/assets/bundle.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"file":"bundle.js","sources":["../node_modules/.registry.npmjs.org/svelte/3.12.1/node_modules/svelte/internal/index.mjs","../src/websocket-client.js","../src/App.svelte","../src/main.js"],"sourcesContent":["function noop() { }\nconst identity = x => x;\nfunction assign(tar, src) {\n // @ts-ignore\n for (const k in src)\n tar[k] = src[k];\n return tar;\n}\nfunction is_promise(value) {\n return value && typeof value === 'object' && typeof value.then === 'function';\n}\nfunction add_location(element, file, line, column, char) {\n element.__svelte_meta = {\n loc: { file, line, column, char }\n };\n}\nfunction run(fn) {\n return fn();\n}\nfunction blank_object() {\n return Object.create(null);\n}\nfunction run_all(fns) {\n fns.forEach(run);\n}\nfunction is_function(thing) {\n return typeof thing === 'function';\n}\nfunction safe_not_equal(a, b) {\n return a != a ? b == b : a !== b || ((a && typeof a === 'object') || typeof a === 'function');\n}\nfunction not_equal(a, b) {\n return a != a ? b == b : a !== b;\n}\nfunction validate_store(store, name) {\n if (!store || typeof store.subscribe !== 'function') {\n throw new Error(`'${name}' is not a store with a 'subscribe' method`);\n }\n}\nfunction subscribe(store, callback) {\n const unsub = store.subscribe(callback);\n return unsub.unsubscribe ? () => unsub.unsubscribe() : unsub;\n}\nfunction get_store_value(store) {\n let value;\n subscribe(store, _ => value = _)();\n return value;\n}\nfunction component_subscribe(component, store, callback) {\n component.$$.on_destroy.push(subscribe(store, callback));\n}\nfunction create_slot(definition, ctx, fn) {\n if (definition) {\n const slot_ctx = get_slot_context(definition, ctx, fn);\n return definition[0](slot_ctx);\n }\n}\nfunction get_slot_context(definition, ctx, fn) {\n return definition[1]\n ? assign({}, assign(ctx.$$scope.ctx, definition[1](fn ? fn(ctx) : {})))\n : ctx.$$scope.ctx;\n}\nfunction get_slot_changes(definition, ctx, changed, fn) {\n return definition[1]\n ? assign({}, assign(ctx.$$scope.changed || {}, definition[1](fn ? fn(changed) : {})))\n : ctx.$$scope.changed || {};\n}\nfunction exclude_internal_props(props) {\n const result = {};\n for (const k in props)\n if (k[0] !== '$')\n result[k] = props[k];\n return result;\n}\nfunction once(fn) {\n let ran = false;\n return function (...args) {\n if (ran)\n return;\n ran = true;\n fn.call(this, ...args);\n };\n}\nfunction null_to_empty(value) {\n return value == null ? '' : value;\n}\nfunction set_store_value(store, ret, value = ret) {\n store.set(value);\n return ret;\n}\n\nconst is_client = typeof window !== 'undefined';\nlet now = is_client\n ? () => window.performance.now()\n : () => Date.now();\nlet raf = is_client ? cb => requestAnimationFrame(cb) : noop;\n// used internally for testing\nfunction set_now(fn) {\n now = fn;\n}\nfunction set_raf(fn) {\n raf = fn;\n}\n\nconst tasks = new Set();\nlet running = false;\nfunction run_tasks() {\n tasks.forEach(task => {\n if (!task[0](now())) {\n tasks.delete(task);\n task[1]();\n }\n });\n running = tasks.size > 0;\n if (running)\n raf(run_tasks);\n}\nfunction clear_loops() {\n // for testing...\n tasks.forEach(task => tasks.delete(task));\n running = false;\n}\nfunction loop(fn) {\n let task;\n if (!running) {\n running = true;\n raf(run_tasks);\n }\n return {\n promise: new Promise(fulfil => {\n tasks.add(task = [fn, fulfil]);\n }),\n abort() {\n tasks.delete(task);\n }\n };\n}\n\nfunction append(target, node) {\n target.appendChild(node);\n}\nfunction insert(target, node, anchor) {\n target.insertBefore(node, anchor || null);\n}\nfunction detach(node) {\n node.parentNode.removeChild(node);\n}\nfunction destroy_each(iterations, detaching) {\n for (let i = 0; i < iterations.length; i += 1) {\n if (iterations[i])\n iterations[i].d(detaching);\n }\n}\nfunction element(name) {\n return document.createElement(name);\n}\nfunction element_is(name, is) {\n return document.createElement(name, { is });\n}\nfunction object_without_properties(obj, exclude) {\n // eslint-disable-next-line @typescript-eslint/no-object-literal-type-assertion\n const target = {};\n for (const k in obj) {\n if (Object.prototype.hasOwnProperty.call(obj, k)\n // @ts-ignore\n && exclude.indexOf(k) === -1) {\n // @ts-ignore\n target[k] = obj[k];\n }\n }\n return target;\n}\nfunction svg_element(name) {\n return document.createElementNS('http://www.w3.org/2000/svg', name);\n}\nfunction text(data) {\n return document.createTextNode(data);\n}\nfunction space() {\n return text(' ');\n}\nfunction empty() {\n return text('');\n}\nfunction listen(node, event, handler, options) {\n node.addEventListener(event, handler, options);\n return () => node.removeEventListener(event, handler, options);\n}\nfunction prevent_default(fn) {\n return function (event) {\n event.preventDefault();\n // @ts-ignore\n return fn.call(this, event);\n };\n}\nfunction stop_propagation(fn) {\n return function (event) {\n event.stopPropagation();\n // @ts-ignore\n return fn.call(this, event);\n };\n}\nfunction self(fn) {\n return function (event) {\n // @ts-ignore\n if (event.target === this)\n fn.call(this, event);\n };\n}\nfunction attr(node, attribute, value) {\n if (value == null)\n node.removeAttribute(attribute);\n else\n node.setAttribute(attribute, value);\n}\nfunction set_attributes(node, attributes) {\n for (const key in attributes) {\n if (key === 'style') {\n node.style.cssText = attributes[key];\n }\n else if (key in node) {\n node[key] = attributes[key];\n }\n else {\n attr(node, key, attributes[key]);\n }\n }\n}\nfunction set_svg_attributes(node, attributes) {\n for (const key in attributes) {\n attr(node, key, attributes[key]);\n }\n}\nfunction set_custom_element_data(node, prop, value) {\n if (prop in node) {\n node[prop] = value;\n }\n else {\n attr(node, prop, value);\n }\n}\nfunction xlink_attr(node, attribute, value) {\n node.setAttributeNS('http://www.w3.org/1999/xlink', attribute, value);\n}\nfunction get_binding_group_value(group) {\n const value = [];\n for (let i = 0; i < group.length; i += 1) {\n if (group[i].checked)\n value.push(group[i].__value);\n }\n return value;\n}\nfunction to_number(value) {\n return value === '' ? undefined : +value;\n}\nfunction time_ranges_to_array(ranges) {\n const array = [];\n for (let i = 0; i < ranges.length; i += 1) {\n array.push({ start: ranges.start(i), end: ranges.end(i) });\n }\n return array;\n}\nfunction children(element) {\n return Array.from(element.childNodes);\n}\nfunction claim_element(nodes, name, attributes, svg) {\n for (let i = 0; i < nodes.length; i += 1) {\n const node = nodes[i];\n if (node.nodeName === name) {\n for (let j = 0; j < node.attributes.length; j += 1) {\n const attribute = node.attributes[j];\n if (!attributes[attribute.name])\n node.removeAttribute(attribute.name);\n }\n return nodes.splice(i, 1)[0]; // TODO strip unwanted attributes\n }\n }\n return svg ? svg_element(name) : element(name);\n}\nfunction claim_text(nodes, data) {\n for (let i = 0; i < nodes.length; i += 1) {\n const node = nodes[i];\n if (node.nodeType === 3) {\n node.data = '' + data;\n return nodes.splice(i, 1)[0];\n }\n }\n return text(data);\n}\nfunction claim_space(nodes) {\n return claim_text(nodes, ' ');\n}\nfunction set_data(text, data) {\n data = '' + data;\n if (text.data !== data)\n text.data = data;\n}\nfunction set_input_value(input, value) {\n if (value != null || input.value) {\n input.value = value;\n }\n}\nfunction set_input_type(input, type) {\n try {\n input.type = type;\n }\n catch (e) {\n // do nothing\n }\n}\nfunction set_style(node, key, value, important) {\n node.style.setProperty(key, value, important ? 'important' : '');\n}\nfunction select_option(select, value) {\n for (let i = 0; i < select.options.length; i += 1) {\n const option = select.options[i];\n if (option.__value === value) {\n option.selected = true;\n return;\n }\n }\n}\nfunction select_options(select, value) {\n for (let i = 0; i < select.options.length; i += 1) {\n const option = select.options[i];\n option.selected = ~value.indexOf(option.__value);\n }\n}\nfunction select_value(select) {\n const selected_option = select.querySelector(':checked') || select.options[0];\n return selected_option && selected_option.__value;\n}\nfunction select_multiple_value(select) {\n return [].map.call(select.querySelectorAll(':checked'), option => option.__value);\n}\nfunction add_resize_listener(element, fn) {\n if (getComputedStyle(element).position === 'static') {\n element.style.position = 'relative';\n }\n const object = document.createElement('object');\n object.setAttribute('style', 'display: block; position: absolute; top: 0; left: 0; height: 100%; width: 100%; overflow: hidden; pointer-events: none; z-index: -1;');\n object.type = 'text/html';\n object.tabIndex = -1;\n let win;\n object.onload = () => {\n win = object.contentDocument.defaultView;\n win.addEventListener('resize', fn);\n };\n if (/Trident/.test(navigator.userAgent)) {\n element.appendChild(object);\n object.data = 'about:blank';\n }\n else {\n object.data = 'about:blank';\n element.appendChild(object);\n }\n return {\n cancel: () => {\n win && win.removeEventListener && win.removeEventListener('resize', fn);\n element.removeChild(object);\n }\n };\n}\nfunction toggle_class(element, name, toggle) {\n element.classList[toggle ? 'add' : 'remove'](name);\n}\nfunction custom_event(type, detail) {\n const e = document.createEvent('CustomEvent');\n e.initCustomEvent(type, false, false, detail);\n return e;\n}\nclass HtmlTag {\n constructor(html, anchor = null) {\n this.e = element('div');\n this.a = anchor;\n this.u(html);\n }\n m(target, anchor = null) {\n for (let i = 0; i < this.n.length; i += 1) {\n insert(target, this.n[i], anchor);\n }\n this.t = target;\n }\n u(html) {\n this.e.innerHTML = html;\n this.n = Array.from(this.e.childNodes);\n }\n p(html) {\n this.d();\n this.u(html);\n this.m(this.t, this.a);\n }\n d() {\n this.n.forEach(detach);\n }\n}\n\nlet stylesheet;\nlet active = 0;\nlet current_rules = {};\n// https://github.com/darkskyapp/string-hash/blob/master/index.js\nfunction hash(str) {\n let hash = 5381;\n let i = str.length;\n while (i--)\n hash = ((hash << 5) - hash) ^ str.charCodeAt(i);\n return hash >>> 0;\n}\nfunction create_rule(node, a, b, duration, delay, ease, fn, uid = 0) {\n const step = 16.666 / duration;\n let keyframes = '{\\n';\n for (let p = 0; p <= 1; p += step) {\n const t = a + (b - a) * ease(p);\n keyframes += p * 100 + `%{${fn(t, 1 - t)}}\\n`;\n }\n const rule = keyframes + `100% {${fn(b, 1 - b)}}\\n}`;\n const name = `__svelte_${hash(rule)}_${uid}`;\n if (!current_rules[name]) {\n if (!stylesheet) {\n const style = element('style');\n document.head.appendChild(style);\n stylesheet = style.sheet;\n }\n current_rules[name] = true;\n stylesheet.insertRule(`@keyframes ${name} ${rule}`, stylesheet.cssRules.length);\n }\n const animation = node.style.animation || '';\n node.style.animation = `${animation ? `${animation}, ` : ``}${name} ${duration}ms linear ${delay}ms 1 both`;\n active += 1;\n return name;\n}\nfunction delete_rule(node, name) {\n node.style.animation = (node.style.animation || '')\n .split(', ')\n .filter(name\n ? anim => anim.indexOf(name) < 0 // remove specific animation\n : anim => anim.indexOf('__svelte') === -1 // remove all Svelte animations\n )\n .join(', ');\n if (name && !--active)\n clear_rules();\n}\nfunction clear_rules() {\n raf(() => {\n if (active)\n return;\n let i = stylesheet.cssRules.length;\n while (i--)\n stylesheet.deleteRule(i);\n current_rules = {};\n });\n}\n\nfunction create_animation(node, from, fn, params) {\n if (!from)\n return noop;\n const to = node.getBoundingClientRect();\n if (from.left === to.left && from.right === to.right && from.top === to.top && from.bottom === to.bottom)\n return noop;\n const { delay = 0, duration = 300, easing = identity, \n // @ts-ignore todo: should this be separated from destructuring? Or start/end added to public api and documentation?\n start: start_time = now() + delay, \n // @ts-ignore todo:\n end = start_time + duration, tick = noop, css } = fn(node, { from, to }, params);\n let running = true;\n let started = false;\n let name;\n function start() {\n if (css) {\n name = create_rule(node, 0, 1, duration, delay, easing, css);\n }\n if (!delay) {\n started = true;\n }\n }\n function stop() {\n if (css)\n delete_rule(node, name);\n running = false;\n }\n loop(now => {\n if (!started && now >= start_time) {\n started = true;\n }\n if (started && now >= end) {\n tick(1, 0);\n stop();\n }\n if (!running) {\n return false;\n }\n if (started) {\n const p = now - start_time;\n const t = 0 + 1 * easing(p / duration);\n tick(t, 1 - t);\n }\n return true;\n });\n start();\n tick(0, 1);\n return stop;\n}\nfunction fix_position(node) {\n const style = getComputedStyle(node);\n if (style.position !== 'absolute' && style.position !== 'fixed') {\n const { width, height } = style;\n const a = node.getBoundingClientRect();\n node.style.position = 'absolute';\n node.style.width = width;\n node.style.height = height;\n add_transform(node, a);\n }\n}\nfunction add_transform(node, a) {\n const b = node.getBoundingClientRect();\n if (a.left !== b.left || a.top !== b.top) {\n const style = getComputedStyle(node);\n const transform = style.transform === 'none' ? '' : style.transform;\n node.style.transform = `${transform} translate(${a.left - b.left}px, ${a.top - b.top}px)`;\n }\n}\n\nlet current_component;\nfunction set_current_component(component) {\n current_component = component;\n}\nfunction get_current_component() {\n if (!current_component)\n throw new Error(`Function called outside component initialization`);\n return current_component;\n}\nfunction beforeUpdate(fn) {\n get_current_component().$$.before_update.push(fn);\n}\nfunction onMount(fn) {\n get_current_component().$$.on_mount.push(fn);\n}\nfunction afterUpdate(fn) {\n get_current_component().$$.after_update.push(fn);\n}\nfunction onDestroy(fn) {\n get_current_component().$$.on_destroy.push(fn);\n}\nfunction createEventDispatcher() {\n const component = current_component;\n return (type, detail) => {\n const callbacks = component.$$.callbacks[type];\n if (callbacks) {\n // TODO are there situations where events could be dispatched\n // in a server (non-DOM) environment?\n const event = custom_event(type, detail);\n callbacks.slice().forEach(fn => {\n fn.call(component, event);\n });\n }\n };\n}\nfunction setContext(key, context) {\n get_current_component().$$.context.set(key, context);\n}\nfunction getContext(key) {\n return get_current_component().$$.context.get(key);\n}\n// TODO figure out if we still want to support\n// shorthand events, or if we want to implement\n// a real bubbling mechanism\nfunction bubble(component, event) {\n const callbacks = component.$$.callbacks[event.type];\n if (callbacks) {\n callbacks.slice().forEach(fn => fn(event));\n }\n}\n\nconst dirty_components = [];\nconst intros = { enabled: false };\nconst binding_callbacks = [];\nconst render_callbacks = [];\nconst flush_callbacks = [];\nconst resolved_promise = Promise.resolve();\nlet update_scheduled = false;\nfunction schedule_update() {\n if (!update_scheduled) {\n update_scheduled = true;\n resolved_promise.then(flush);\n }\n}\nfunction tick() {\n schedule_update();\n return resolved_promise;\n}\nfunction add_render_callback(fn) {\n render_callbacks.push(fn);\n}\nfunction add_flush_callback(fn) {\n flush_callbacks.push(fn);\n}\nfunction flush() {\n const seen_callbacks = new Set();\n do {\n // first, call beforeUpdate functions\n // and update components\n while (dirty_components.length) {\n const component = dirty_components.shift();\n set_current_component(component);\n update(component.$$);\n }\n while (binding_callbacks.length)\n binding_callbacks.pop()();\n // then, once components are updated, call\n // afterUpdate functions. This may cause\n // subsequent updates...\n for (let i = 0; i < render_callbacks.length; i += 1) {\n const callback = render_callbacks[i];\n if (!seen_callbacks.has(callback)) {\n callback();\n // ...so guard against infinite loops\n seen_callbacks.add(callback);\n }\n }\n render_callbacks.length = 0;\n } while (dirty_components.length);\n while (flush_callbacks.length) {\n flush_callbacks.pop()();\n }\n update_scheduled = false;\n}\nfunction update($$) {\n if ($$.fragment) {\n $$.update($$.dirty);\n run_all($$.before_update);\n $$.fragment.p($$.dirty, $$.ctx);\n $$.dirty = null;\n $$.after_update.forEach(add_render_callback);\n }\n}\n\nlet promise;\nfunction wait() {\n if (!promise) {\n promise = Promise.resolve();\n promise.then(() => {\n promise = null;\n });\n }\n return promise;\n}\nfunction dispatch(node, direction, kind) {\n node.dispatchEvent(custom_event(`${direction ? 'intro' : 'outro'}${kind}`));\n}\nconst outroing = new Set();\nlet outros;\nfunction group_outros() {\n outros = {\n r: 0,\n c: [],\n p: outros // parent group\n };\n}\nfunction check_outros() {\n if (!outros.r) {\n run_all(outros.c);\n }\n outros = outros.p;\n}\nfunction transition_in(block, local) {\n if (block && block.i) {\n outroing.delete(block);\n block.i(local);\n }\n}\nfunction transition_out(block, local, detach, callback) {\n if (block && block.o) {\n if (outroing.has(block))\n return;\n outroing.add(block);\n outros.c.push(() => {\n outroing.delete(block);\n if (callback) {\n if (detach)\n block.d(1);\n callback();\n }\n });\n block.o(local);\n }\n}\nconst null_transition = { duration: 0 };\nfunction create_in_transition(node, fn, params) {\n let config = fn(node, params);\n let running = false;\n let animation_name;\n let task;\n let uid = 0;\n function cleanup() {\n if (animation_name)\n delete_rule(node, animation_name);\n }\n function go() {\n const { delay = 0, duration = 300, easing = identity, tick = noop, css } = config || null_transition;\n if (css)\n animation_name = create_rule(node, 0, 1, duration, delay, easing, css, uid++);\n tick(0, 1);\n const start_time = now() + delay;\n const end_time = start_time + duration;\n if (task)\n task.abort();\n running = true;\n add_render_callback(() => dispatch(node, true, 'start'));\n task = loop(now => {\n if (running) {\n if (now >= end_time) {\n tick(1, 0);\n dispatch(node, true, 'end');\n cleanup();\n return running = false;\n }\n if (now >= start_time) {\n const t = easing((now - start_time) / duration);\n tick(t, 1 - t);\n }\n }\n return running;\n });\n }\n let started = false;\n return {\n start() {\n if (started)\n return;\n delete_rule(node);\n if (is_function(config)) {\n config = config();\n wait().then(go);\n }\n else {\n go();\n }\n },\n invalidate() {\n started = false;\n },\n end() {\n if (running) {\n cleanup();\n running = false;\n }\n }\n };\n}\nfunction create_out_transition(node, fn, params) {\n let config = fn(node, params);\n let running = true;\n let animation_name;\n const group = outros;\n group.r += 1;\n function go() {\n const { delay = 0, duration = 300, easing = identity, tick = noop, css } = config || null_transition;\n if (css)\n animation_name = create_rule(node, 1, 0, duration, delay, easing, css);\n const start_time = now() + delay;\n const end_time = start_time + duration;\n add_render_callback(() => dispatch(node, false, 'start'));\n loop(now => {\n if (running) {\n if (now >= end_time) {\n tick(0, 1);\n dispatch(node, false, 'end');\n if (!--group.r) {\n // this will result in `end()` being called,\n // so we don't need to clean up here\n run_all(group.c);\n }\n return false;\n }\n if (now >= start_time) {\n const t = easing((now - start_time) / duration);\n tick(1 - t, t);\n }\n }\n return running;\n });\n }\n if (is_function(config)) {\n wait().then(() => {\n // @ts-ignore\n config = config();\n go();\n });\n }\n else {\n go();\n }\n return {\n end(reset) {\n if (reset && config.tick) {\n config.tick(1, 0);\n }\n if (running) {\n if (animation_name)\n delete_rule(node, animation_name);\n running = false;\n }\n }\n };\n}\nfunction create_bidirectional_transition(node, fn, params, intro) {\n let config = fn(node, params);\n let t = intro ? 0 : 1;\n let running_program = null;\n let pending_program = null;\n let animation_name = null;\n function clear_animation() {\n if (animation_name)\n delete_rule(node, animation_name);\n }\n function init(program, duration) {\n const d = program.b - t;\n duration *= Math.abs(d);\n return {\n a: t,\n b: program.b,\n d,\n duration,\n start: program.start,\n end: program.start + duration,\n group: program.group\n };\n }\n function go(b) {\n const { delay = 0, duration = 300, easing = identity, tick = noop, css } = config || null_transition;\n const program = {\n start: now() + delay,\n b\n };\n if (!b) {\n // @ts-ignore todo: improve typings\n program.group = outros;\n outros.r += 1;\n }\n if (running_program) {\n pending_program = program;\n }\n else {\n // if this is an intro, and there's a delay, we need to do\n // an initial tick and/or apply CSS animation immediately\n if (css) {\n clear_animation();\n animation_name = create_rule(node, t, b, duration, delay, easing, css);\n }\n if (b)\n tick(0, 1);\n running_program = init(program, duration);\n add_render_callback(() => dispatch(node, b, 'start'));\n loop(now => {\n if (pending_program && now > pending_program.start) {\n running_program = init(pending_program, duration);\n pending_program = null;\n dispatch(node, running_program.b, 'start');\n if (css) {\n clear_animation();\n animation_name = create_rule(node, t, running_program.b, running_program.duration, 0, easing, config.css);\n }\n }\n if (running_program) {\n if (now >= running_program.end) {\n tick(t = running_program.b, 1 - t);\n dispatch(node, running_program.b, 'end');\n if (!pending_program) {\n // we're done\n if (running_program.b) {\n // intro — we can tidy up immediately\n clear_animation();\n }\n else {\n // outro — needs to be coordinated\n if (!--running_program.group.r)\n run_all(running_program.group.c);\n }\n }\n running_program = null;\n }\n else if (now >= running_program.start) {\n const p = now - running_program.start;\n t = running_program.a + running_program.d * easing(p / running_program.duration);\n tick(t, 1 - t);\n }\n }\n return !!(running_program || pending_program);\n });\n }\n }\n return {\n run(b) {\n if (is_function(config)) {\n wait().then(() => {\n // @ts-ignore\n config = config();\n go(b);\n });\n }\n else {\n go(b);\n }\n },\n end() {\n clear_animation();\n running_program = pending_program = null;\n }\n };\n}\n\nfunction handle_promise(promise, info) {\n const token = info.token = {};\n function update(type, index, key, value) {\n if (info.token !== token)\n return;\n info.resolved = key && { [key]: value };\n const child_ctx = assign(assign({}, info.ctx), info.resolved);\n const block = type && (info.current = type)(child_ctx);\n if (info.block) {\n if (info.blocks) {\n info.blocks.forEach((block, i) => {\n if (i !== index && block) {\n group_outros();\n transition_out(block, 1, 1, () => {\n info.blocks[i] = null;\n });\n check_outros();\n }\n });\n }\n else {\n info.block.d(1);\n }\n block.c();\n transition_in(block, 1);\n block.m(info.mount(), info.anchor);\n flush();\n }\n info.block = block;\n if (info.blocks)\n info.blocks[index] = block;\n }\n if (is_promise(promise)) {\n const current_component = get_current_component();\n promise.then(value => {\n set_current_component(current_component);\n update(info.then, 1, info.value, value);\n set_current_component(null);\n }, error => {\n set_current_component(current_component);\n update(info.catch, 2, info.error, error);\n set_current_component(null);\n });\n // if we previously had a then/catch block, destroy it\n if (info.current !== info.pending) {\n update(info.pending, 0);\n return true;\n }\n }\n else {\n if (info.current !== info.then) {\n update(info.then, 1, info.value, promise);\n return true;\n }\n info.resolved = { [info.value]: promise };\n }\n}\n\nconst globals = (typeof window !== 'undefined' ? window : global);\n\nfunction destroy_block(block, lookup) {\n block.d(1);\n lookup.delete(block.key);\n}\nfunction outro_and_destroy_block(block, lookup) {\n transition_out(block, 1, 1, () => {\n lookup.delete(block.key);\n });\n}\nfunction fix_and_destroy_block(block, lookup) {\n block.f();\n destroy_block(block, lookup);\n}\nfunction fix_and_outro_and_destroy_block(block, lookup) {\n block.f();\n outro_and_destroy_block(block, lookup);\n}\nfunction update_keyed_each(old_blocks, changed, get_key, dynamic, ctx, list, lookup, node, destroy, create_each_block, next, get_context) {\n let o = old_blocks.length;\n let n = list.length;\n let i = o;\n const old_indexes = {};\n while (i--)\n old_indexes[old_blocks[i].key] = i;\n const new_blocks = [];\n const new_lookup = new Map();\n const deltas = new Map();\n i = n;\n while (i--) {\n const child_ctx = get_context(ctx, list, i);\n const key = get_key(child_ctx);\n let block = lookup.get(key);\n if (!block) {\n block = create_each_block(key, child_ctx);\n block.c();\n }\n else if (dynamic) {\n block.p(changed, child_ctx);\n }\n new_lookup.set(key, new_blocks[i] = block);\n if (key in old_indexes)\n deltas.set(key, Math.abs(i - old_indexes[key]));\n }\n const will_move = new Set();\n const did_move = new Set();\n function insert(block) {\n transition_in(block, 1);\n block.m(node, next);\n lookup.set(block.key, block);\n next = block.first;\n n--;\n }\n while (o && n) {\n const new_block = new_blocks[n - 1];\n const old_block = old_blocks[o - 1];\n const new_key = new_block.key;\n const old_key = old_block.key;\n if (new_block === old_block) {\n // do nothing\n next = new_block.first;\n o--;\n n--;\n }\n else if (!new_lookup.has(old_key)) {\n // remove old block\n destroy(old_block, lookup);\n o--;\n }\n else if (!lookup.has(new_key) || will_move.has(new_key)) {\n insert(new_block);\n }\n else if (did_move.has(old_key)) {\n o--;\n }\n else if (deltas.get(new_key) > deltas.get(old_key)) {\n did_move.add(new_key);\n insert(new_block);\n }\n else {\n will_move.add(old_key);\n o--;\n }\n }\n while (o--) {\n const old_block = old_blocks[o];\n if (!new_lookup.has(old_block.key))\n destroy(old_block, lookup);\n }\n while (n)\n insert(new_blocks[n - 1]);\n return new_blocks;\n}\nfunction measure(blocks) {\n const rects = {};\n let i = blocks.length;\n while (i--)\n rects[blocks[i].key] = blocks[i].node.getBoundingClientRect();\n return rects;\n}\n\nfunction get_spread_update(levels, updates) {\n const update = {};\n const to_null_out = {};\n const accounted_for = { $$scope: 1 };\n let i = levels.length;\n while (i--) {\n const o = levels[i];\n const n = updates[i];\n if (n) {\n for (const key in o) {\n if (!(key in n))\n to_null_out[key] = 1;\n }\n for (const key in n) {\n if (!accounted_for[key]) {\n update[key] = n[key];\n accounted_for[key] = 1;\n }\n }\n levels[i] = n;\n }\n else {\n for (const key in o) {\n accounted_for[key] = 1;\n }\n }\n }\n for (const key in to_null_out) {\n if (!(key in update))\n update[key] = undefined;\n }\n return update;\n}\nfunction get_spread_object(spread_props) {\n return typeof spread_props === 'object' && spread_props !== null ? spread_props : {};\n}\n\nconst invalid_attribute_name_character = /[\\s'\">/=\\u{FDD0}-\\u{FDEF}\\u{FFFE}\\u{FFFF}\\u{1FFFE}\\u{1FFFF}\\u{2FFFE}\\u{2FFFF}\\u{3FFFE}\\u{3FFFF}\\u{4FFFE}\\u{4FFFF}\\u{5FFFE}\\u{5FFFF}\\u{6FFFE}\\u{6FFFF}\\u{7FFFE}\\u{7FFFF}\\u{8FFFE}\\u{8FFFF}\\u{9FFFE}\\u{9FFFF}\\u{AFFFE}\\u{AFFFF}\\u{BFFFE}\\u{BFFFF}\\u{CFFFE}\\u{CFFFF}\\u{DFFFE}\\u{DFFFF}\\u{EFFFE}\\u{EFFFF}\\u{FFFFE}\\u{FFFFF}\\u{10FFFE}\\u{10FFFF}]/u;\n// https://html.spec.whatwg.org/multipage/syntax.html#attributes-2\n// https://infra.spec.whatwg.org/#noncharacter\nfunction spread(args) {\n const attributes = Object.assign({}, ...args);\n let str = '';\n Object.keys(attributes).forEach(name => {\n if (invalid_attribute_name_character.test(name))\n return;\n const value = attributes[name];\n if (value === undefined)\n return;\n if (value === true)\n str += \" \" + name;\n const escaped = String(value)\n .replace(/\"/g, '"')\n .replace(/'/g, ''');\n str += \" \" + name + \"=\" + JSON.stringify(escaped);\n });\n return str;\n}\nconst escaped = {\n '\"': '"',\n \"'\": ''',\n '&': '&',\n '<': '<',\n '>': '>'\n};\nfunction escape(html) {\n return String(html).replace(/[\"'&<>]/g, match => escaped[match]);\n}\nfunction each(items, fn) {\n let str = '';\n for (let i = 0; i < items.length; i += 1) {\n str += fn(items[i], i);\n }\n return str;\n}\nconst missing_component = {\n $$render: () => ''\n};\nfunction validate_component(component, name) {\n if (!component || !component.$$render) {\n if (name === 'svelte:component')\n name += ' this={...}';\n throw new Error(`<${name}> is not a valid SSR component. You may need to review your build config to ensure that dependencies are compiled, rather than imported as pre-compiled modules`);\n }\n return component;\n}\nfunction debug(file, line, column, values) {\n console.log(`{@debug} ${file ? file + ' ' : ''}(${line}:${column})`); // eslint-disable-line no-console\n console.log(values); // eslint-disable-line no-console\n return '';\n}\nlet on_destroy;\nfunction create_ssr_component(fn) {\n function $$render(result, props, bindings, slots) {\n const parent_component = current_component;\n const $$ = {\n on_destroy,\n context: new Map(parent_component ? parent_component.$$.context : []),\n // these will be immediately discarded\n on_mount: [],\n before_update: [],\n after_update: [],\n callbacks: blank_object()\n };\n set_current_component({ $$ });\n const html = fn(result, props, bindings, slots);\n set_current_component(parent_component);\n return html;\n }\n return {\n render: (props = {}, options = {}) => {\n on_destroy = [];\n const result = { head: '', css: new Set() };\n const html = $$render(result, props, {}, options);\n run_all(on_destroy);\n return {\n html,\n css: {\n code: Array.from(result.css).map(css => css.code).join('\\n'),\n map: null // TODO\n },\n head: result.head\n };\n },\n $$render\n };\n}\nfunction add_attribute(name, value, boolean) {\n if (value == null || (boolean && !value))\n return '';\n return ` ${name}${value === true ? '' : `=${typeof value === 'string' ? JSON.stringify(escape(value)) : `\"${value}\"`}`}`;\n}\nfunction add_classes(classes) {\n return classes ? ` class=\"${classes}\"` : ``;\n}\n\nfunction bind(component, name, callback) {\n if (component.$$.props.indexOf(name) === -1)\n return;\n component.$$.bound[name] = callback;\n callback(component.$$.ctx[name]);\n}\nfunction mount_component(component, target, anchor) {\n const { fragment, on_mount, on_destroy, after_update } = component.$$;\n fragment.m(target, anchor);\n // onMount happens before the initial afterUpdate\n add_render_callback(() => {\n const new_on_destroy = on_mount.map(run).filter(is_function);\n if (on_destroy) {\n on_destroy.push(...new_on_destroy);\n }\n else {\n // Edge case - component was destroyed immediately,\n // most likely as a result of a binding initialising\n run_all(new_on_destroy);\n }\n component.$$.on_mount = [];\n });\n after_update.forEach(add_render_callback);\n}\nfunction destroy_component(component, detaching) {\n if (component.$$.fragment) {\n run_all(component.$$.on_destroy);\n component.$$.fragment.d(detaching);\n // TODO null out other refs, including component.$$ (but need to\n // preserve final state?)\n component.$$.on_destroy = component.$$.fragment = null;\n component.$$.ctx = {};\n }\n}\nfunction make_dirty(component, key) {\n if (!component.$$.dirty) {\n dirty_components.push(component);\n schedule_update();\n component.$$.dirty = blank_object();\n }\n component.$$.dirty[key] = true;\n}\nfunction init(component, options, instance, create_fragment, not_equal, prop_names) {\n const parent_component = current_component;\n set_current_component(component);\n const props = options.props || {};\n const $$ = component.$$ = {\n fragment: null,\n ctx: null,\n // state\n props: prop_names,\n update: noop,\n not_equal,\n bound: blank_object(),\n // lifecycle\n on_mount: [],\n on_destroy: [],\n before_update: [],\n after_update: [],\n context: new Map(parent_component ? parent_component.$$.context : []),\n // everything else\n callbacks: blank_object(),\n dirty: null\n };\n let ready = false;\n $$.ctx = instance\n ? instance(component, props, (key, ret, value = ret) => {\n if ($$.ctx && not_equal($$.ctx[key], $$.ctx[key] = value)) {\n if ($$.bound[key])\n $$.bound[key](value);\n if (ready)\n make_dirty(component, key);\n }\n return ret;\n })\n : props;\n $$.update();\n ready = true;\n run_all($$.before_update);\n $$.fragment = create_fragment($$.ctx);\n if (options.target) {\n if (options.hydrate) {\n // eslint-disable-next-line @typescript-eslint/no-non-null-assertion\n $$.fragment.l(children(options.target));\n }\n else {\n // eslint-disable-next-line @typescript-eslint/no-non-null-assertion\n $$.fragment.c();\n }\n if (options.intro)\n transition_in(component.$$.fragment);\n mount_component(component, options.target, options.anchor);\n flush();\n }\n set_current_component(parent_component);\n}\nlet SvelteElement;\nif (typeof HTMLElement !== 'undefined') {\n SvelteElement = class extends HTMLElement {\n constructor() {\n super();\n this.attachShadow({ mode: 'open' });\n }\n connectedCallback() {\n // @ts-ignore todo: improve typings\n for (const key in this.$$.slotted) {\n // @ts-ignore todo: improve typings\n this.appendChild(this.$$.slotted[key]);\n }\n }\n attributeChangedCallback(attr, _oldValue, newValue) {\n this[attr] = newValue;\n }\n $destroy() {\n destroy_component(this, 1);\n this.$destroy = noop;\n }\n $on(type, callback) {\n // TODO should this delegate to addEventListener?\n const callbacks = (this.$$.callbacks[type] || (this.$$.callbacks[type] = []));\n callbacks.push(callback);\n return () => {\n const index = callbacks.indexOf(callback);\n if (index !== -1)\n callbacks.splice(index, 1);\n };\n }\n $set() {\n // overridden by instance, if it has props\n }\n };\n}\nclass SvelteComponent {\n $destroy() {\n destroy_component(this, 1);\n this.$destroy = noop;\n }\n $on(type, callback) {\n const callbacks = (this.$$.callbacks[type] || (this.$$.callbacks[type] = []));\n callbacks.push(callback);\n return () => {\n const index = callbacks.indexOf(callback);\n if (index !== -1)\n callbacks.splice(index, 1);\n };\n }\n $set() {\n // overridden by instance, if it has props\n }\n}\n\nfunction dispatch_dev(type, detail) {\n document.dispatchEvent(custom_event(type, detail));\n}\nfunction append_dev(target, node) {\n dispatch_dev(\"SvelteDOMInsert\", { target, node });\n append(target, node);\n}\nfunction insert_dev(target, node, anchor) {\n dispatch_dev(\"SvelteDOMInsert\", { target, node, anchor });\n insert(target, node, anchor);\n}\nfunction detach_dev(node) {\n dispatch_dev(\"SvelteDOMRemove\", { node });\n detach(node);\n}\nfunction detach_between_dev(before, after) {\n while (before.nextSibling && before.nextSibling !== after) {\n detach_dev(before.nextSibling);\n }\n}\nfunction detach_before_dev(after) {\n while (after.previousSibling) {\n detach_dev(after.previousSibling);\n }\n}\nfunction detach_after_dev(before) {\n while (before.nextSibling) {\n detach_dev(before.nextSibling);\n }\n}\nfunction listen_dev(node, event, handler, options, has_prevent_default, has_stop_propagation) {\n const modifiers = options === true ? [\"capture\"] : options ? Array.from(Object.keys(options)) : [];\n if (has_prevent_default)\n modifiers.push('preventDefault');\n if (has_stop_propagation)\n modifiers.push('stopPropagation');\n dispatch_dev(\"SvelteDOMAddEventListener\", { node, event, handler, modifiers });\n const dispose = listen(node, event, handler, options);\n return () => {\n dispatch_dev(\"SvelteDOMRemoveEventListener\", { node, event, handler, modifiers });\n dispose();\n };\n}\nfunction attr_dev(node, attribute, value) {\n attr(node, attribute, value);\n if (value == null)\n dispatch_dev(\"SvelteDOMRemoveAttribute\", { node, attribute });\n else\n dispatch_dev(\"SvelteDOMSetAttribute\", { node, attribute, value });\n}\nfunction prop_dev(node, property, value) {\n node[property] = value;\n dispatch_dev(\"SvelteDOMSetProperty\", { node, property, value });\n}\nfunction dataset_dev(node, property, value) {\n node.dataset[property] = value;\n dispatch_dev(\"SvelteDOMSetDataset\", { node, property, value });\n}\nfunction set_data_dev(text, data) {\n data = '' + data;\n if (text.data === data)\n return;\n dispatch_dev(\"SvelteDOMSetData\", { node: text, data });\n text.data = data;\n}\nclass SvelteComponentDev extends SvelteComponent {\n constructor(options) {\n if (!options || (!options.target && !options.$$inline)) {\n throw new Error(`'target' is a required option`);\n }\n super();\n }\n $destroy() {\n super.$destroy();\n this.$destroy = () => {\n console.warn(`Component was already destroyed`); // eslint-disable-line no-console\n };\n }\n}\n\nexport { HtmlTag, SvelteComponent, SvelteComponentDev, SvelteElement, add_attribute, add_classes, add_flush_callback, add_location, add_render_callback, add_resize_listener, add_transform, afterUpdate, append, append_dev, assign, attr, attr_dev, beforeUpdate, bind, binding_callbacks, blank_object, bubble, check_outros, children, claim_element, claim_space, claim_text, clear_loops, component_subscribe, createEventDispatcher, create_animation, create_bidirectional_transition, create_in_transition, create_out_transition, create_slot, create_ssr_component, current_component, custom_event, dataset_dev, debug, destroy_block, destroy_component, destroy_each, detach, detach_after_dev, detach_before_dev, detach_between_dev, detach_dev, dirty_components, dispatch_dev, each, element, element_is, empty, escape, escaped, exclude_internal_props, fix_and_destroy_block, fix_and_outro_and_destroy_block, fix_position, flush, getContext, get_binding_group_value, get_current_component, get_slot_changes, get_slot_context, get_spread_object, get_spread_update, get_store_value, globals, group_outros, handle_promise, identity, init, insert, insert_dev, intros, invalid_attribute_name_character, is_client, is_function, is_promise, listen, listen_dev, loop, measure, missing_component, mount_component, noop, not_equal, now, null_to_empty, object_without_properties, onDestroy, onMount, once, outro_and_destroy_block, prevent_default, prop_dev, raf, run, run_all, safe_not_equal, schedule_update, select_multiple_value, select_option, select_options, select_value, self, setContext, set_attributes, set_current_component, set_custom_element_data, set_data, set_data_dev, set_input_type, set_input_value, set_now, set_raf, set_store_value, set_style, set_svg_attributes, space, spread, stop_propagation, subscribe, svg_element, text, tick, time_ranges_to_array, to_number, toggle_class, transition_in, transition_out, update_keyed_each, validate_component, validate_store, xlink_attr };\n","//Modify this to point to the ip address where your server is running on\nWEBSOCKET_URI = WEBSOCKET_URI || \"ws://127.0.0.1:8089/\";\n\nlet websocket;\nlet socketIsOpen = false;\nlet intervalID = 0;\nlet closedByUser = false;\n\nexport function sendCommand(obj) {\n console.log(JSON.stringify(obj));\n\tif (socketIsOpen) {\n\t\twebsocket.send(JSON.stringify(obj));\n\t} else {\n\t\tconsole.error('Not connected\\n');\n\t}\n}\n\n\nexport function doConnect(onMessage) {\n\twebsocket = new WebSocket(WEBSOCKET_URI);\n\twebsocket.onopen = function(evt) {\n\t\tsocketIsOpen = true;\n\t\tconsole.info(\"Connection opened\");\n\t\tclearInterval(intervalID);\n\t\tintervalID = 0;\n\t};\n\twebsocket.onclose = function(evt) {\n\t\tsocketIsOpen = true;\n\t\tif (!intervalID && !closedByUser) {\n\t\t\tintervalID = setInterval(doConnect, 5000);\n\t\t} else if (closedByUser) {\n\t\t\tclosedByUser = false;\n\t\t}\n\t\tconsole.info(\"Connection closed\");\n\t};\n\twebsocket.onmessage = function(evt) {\n\t\tvar jsonOBJ = JSON.parse(evt.data);\n\t\tconsole.log(evt.data);\n\t\tonMessage(jsonOBJ);\n\t};\n\twebsocket.onerror = function(evt) {\n\t\tconsole.error('Connection failed, is the Server running?');\n\t\tsocketIsOpen = false;\n\t\tif (!intervalID) {\n\t\t\tintervalID = setInterval(doConnect, 5000);\n\t\t}\n\t};\n}\n\nexport function doDisconnect() {\n\tsocketIsOpen = false;\n\tclosedByUser = true;\n\twebsocket.close();\n}","\n\n\n
\n\t{#each lines as line, i}\n\t
\n\t\tchangeLine(i)}>\n\t\t\n\t
\n\t{/each}\n
\n","import App from './App.svelte';\n\nvar app = new App({\n\ttarget: document.body\n});\n\nexport default app;"],"names":["noop","run","fn","blank_object","Object","create","run_all","fns","forEach","is_function","thing","safe_not_equal","a","b","append","target","node","appendChild","insert","anchor","insertBefore","detach","parentNode","removeChild","element","name","document","createElement","text","data","createTextNode","space","listen","event","handler","options","addEventListener","removeEventListener","attr","attribute","value","removeAttribute","setAttribute","set_input_value","input","current_component","set_current_component","component","get_current_component","Error","dirty_components","binding_callbacks","render_callbacks","flush_callbacks","resolved_promise","Promise","resolve","update_scheduled","add_render_callback","push","flush","seen_callbacks","Set","length","shift","update","$$","pop","i","callback","has","add","fragment","dirty","before_update","p","ctx","after_update","outroing","make_dirty","key","then","init","instance","create_fragment","not_equal","prop_names","parent_component","props","bound","on_mount","on_destroy","context","Map","callbacks","ready","block","local","ret","hydrate","l","Array","from","childNodes","c","intro","delete","m","new_on_destroy","map","filter","mount_component","SvelteComponent","[object Object]","detaching","this","d","$destroy","type","index","indexOf","splice","websocket","WEBSOCKET_URI","socketIsOpen","intervalID","closedByUser","sendCommand","obj","console","log","JSON","stringify","send","error","doConnect","onMessage","WebSocket","onopen","evt","info","clearInterval","onclose","setInterval","onmessage","jsonOBJ","parse","onerror","shown","change_handler","click_handler","line","lines","iterations","toggleLine","line1","show","cleanLines","hasOwnProperty","changeLine","command","async","onDestroy","close","body"],"mappings":"gCAAA,SAASA,KAgBT,SAASC,EAAIC,GACT,OAAOA,IAEX,SAASC,IACL,OAAOC,OAAOC,OAAO,MAEzB,SAASC,EAAQC,GACbA,EAAIC,QAAQP,GAEhB,SAASQ,EAAYC,GACjB,MAAwB,mBAAVA,EAElB,SAASC,EAAeC,EAAGC,GACvB,OAAOD,GAAKA,EAAIC,GAAKA,EAAID,IAAMC,GAAOD,GAAkB,iBAANA,GAAgC,mBAANA,EA6GhF,SAASE,EAAOC,EAAQC,GACpBD,EAAOE,YAAYD,GAEvB,SAASE,EAAOH,EAAQC,EAAMG,GAC1BJ,EAAOK,aAAaJ,EAAMG,GAAU,MAExC,SAASE,EAAOL,GACZA,EAAKM,WAAWC,YAAYP,GAQhC,SAASQ,EAAQC,GACb,OAAOC,SAASC,cAAcF,GAqBlC,SAASG,EAAKC,GACV,OAAOH,SAASI,eAAeD,GAEnC,SAASE,IACL,OAAOH,EAAK,KAKhB,SAASI,EAAOhB,EAAMiB,EAAOC,EAASC,GAElC,OADAnB,EAAKoB,iBAAiBH,EAAOC,EAASC,GAC/B,IAAMnB,EAAKqB,oBAAoBJ,EAAOC,EAASC,GAuB1D,SAASG,EAAKtB,EAAMuB,EAAWC,GACd,MAATA,EACAxB,EAAKyB,gBAAgBF,GAErBvB,EAAK0B,aAAaH,EAAWC,GAoFrC,SAASG,EAAgBC,EAAOJ,IACf,MAATA,GAAiBI,EAAMJ,SACvBI,EAAMJ,MAAQA,GA+NtB,IAAIK,EACJ,SAASC,EAAsBC,GAC3BF,EAAoBE,EAExB,SAASC,IACL,IAAKH,EACD,MAAM,IAAII,MAAM,oDACpB,OAAOJ,EA4CX,MAAMK,EAAmB,GAEnBC,EAAoB,GACpBC,EAAmB,GACnBC,EAAkB,GAClBC,EAAmBC,QAAQC,UACjC,IAAIC,GAAmB,EAWvB,SAASC,EAAoBxD,GACzBkD,EAAiBO,KAAKzD,GAK1B,SAAS0D,IACL,MAAMC,EAAiB,IAAIC,IAC3B,EAAG,CAGC,KAAOZ,EAAiBa,QAAQ,CAC5B,MAAMhB,EAAYG,EAAiBc,QACnClB,EAAsBC,GACtBkB,EAAOlB,EAAUmB,IAErB,KAAOf,EAAkBY,QACrBZ,EAAkBgB,KAAlBhB,GAIJ,IAAK,IAAIiB,EAAI,EAAGA,EAAIhB,EAAiBW,OAAQK,GAAK,EAAG,CACjD,MAAMC,EAAWjB,EAAiBgB,GAC7BP,EAAeS,IAAID,KACpBA,IAEAR,EAAeU,IAAIF,IAG3BjB,EAAiBW,OAAS,QACrBb,EAAiBa,QAC1B,KAAOV,EAAgBU,QACnBV,EAAgBc,KAAhBd,GAEJI,GAAmB,EAEvB,SAASQ,EAAOC,GACRA,EAAGM,WACHN,EAAGD,OAAOC,EAAGO,OACbnE,EAAQ4D,EAAGQ,eACXR,EAAGM,SAASG,EAAET,EAAGO,MAAOP,EAAGU,KAC3BV,EAAGO,MAAQ,KACXP,EAAGW,aAAarE,QAAQkD,IAiBhC,MAAMoB,EAAW,IAAIhB,IAglBrB,SAASiB,EAAWhC,EAAWiC,GACtBjC,EAAUmB,GAAGO,QACdvB,EAAiBS,KAAKZ,GAtpBrBU,IACDA,GAAmB,EACnBH,EAAiB2B,KAAKrB,IAspBtBb,EAAUmB,GAAGO,MAAQtE,KAEzB4C,EAAUmB,GAAGO,MAAMO,IAAO,EAE9B,SAASE,EAAKnC,EAAWZ,EAASgD,EAAUC,EAAiBC,EAAWC,GACpE,MAAMC,EAAmB1C,EACzBC,EAAsBC,GACtB,MAAMyC,EAAQrD,EAAQqD,OAAS,GACzBtB,EAAKnB,EAAUmB,GAAK,CACtBM,SAAU,KACVI,IAAK,KAELY,MAAOF,EACPrB,OAAQjE,EACRqF,UAAAA,EACAI,MAAOtF,IAEPuF,SAAU,GACVC,WAAY,GACZjB,cAAe,GACfG,aAAc,GACde,QAAS,IAAIC,IAAIN,EAAmBA,EAAiBrB,GAAG0B,QAAU,IAElEE,UAAW3F,IACXsE,MAAO,MAEX,IAAIsB,GAAQ,EA/lBhB,IAAuBC,EAAOC,EAlZZzE,EAk/Bd0C,EAAGU,IAAMO,EACHA,EAASpC,EAAWyC,EAAO,CAACR,EAAKkB,EAAK1D,EAAQ0D,KACxChC,EAAGU,KAAOS,EAAUnB,EAAGU,IAAII,GAAMd,EAAGU,IAAII,GAAOxC,KAC3C0B,EAAGuB,MAAMT,IACTd,EAAGuB,MAAMT,GAAKxC,GACduD,GACAhB,EAAWhC,EAAWiC,IAEvBkB,IAETV,EACNtB,EAAGD,SACH8B,GAAQ,EACRzF,EAAQ4D,EAAGQ,eACXR,EAAGM,SAAWY,EAAgBlB,EAAGU,KAC7BzC,EAAQpB,SACJoB,EAAQgE,QAERjC,EAAGM,SAAS4B,GApgCN5E,EAogCiBW,EAAQpB,OAngChCsF,MAAMC,KAAK9E,EAAQ+E,cAugClBrC,EAAGM,SAASgC,IAEZrE,EAAQsE,SAxnBGT,EAynBGjD,EAAUmB,GAAGM,WAxnBtBwB,EAAM5B,IACfU,EAAS4B,OAAOV,GAChBA,EAAM5B,EAAE6B,KAkiBhB,SAAyBlD,EAAWhC,EAAQI,GACxC,MAAMqD,SAAEA,EAAQkB,SAAEA,EAAQC,WAAEA,EAAUd,aAAEA,GAAiB9B,EAAUmB,GACnEM,EAASmC,EAAE5F,EAAQI,GAEnBuC,EAAoB,KAChB,MAAMkD,EAAiBlB,EAASmB,IAAI5G,GAAK6G,OAAOrG,GAC5CkF,EACAA,EAAWhC,QAAQiD,GAKnBtG,EAAQsG,GAEZ7D,EAAUmB,GAAGwB,SAAW,KAE5Bb,EAAarE,QAAQkD,GAqEjBqD,CAAgBhE,EAAWZ,EAAQpB,OAAQoB,EAAQhB,QACnDyC,KAEJd,EAAsByC,GAsC1B,MAAMyB,EACFC,WA7GJ,IAA2BlE,EAAWmE,EAAAA,EA8GN,GA9GLnE,EA8GDoE,MA7GRjD,GAAGM,WACblE,EAAQyC,EAAUmB,GAAGyB,YACrB5C,EAAUmB,GAAGM,SAAS4C,EAAEF,GAGxBnE,EAAUmB,GAAGyB,WAAa5C,EAAUmB,GAAGM,SAAW,KAClDzB,EAAUmB,GAAGU,IAAM,IAwGnBuC,KAAKE,SAAWrH,EAEpBiH,IAAIK,EAAMjD,GACN,MAAMyB,EAAaqB,KAAKjD,GAAG4B,UAAUwB,KAAUH,KAAKjD,GAAG4B,UAAUwB,GAAQ,IAEzE,OADAxB,EAAUnC,KAAKU,GACR,KACH,MAAMkD,EAAQzB,EAAU0B,QAAQnD,IACjB,IAAXkD,GACAzB,EAAU2B,OAAOF,EAAO,IAGpCN,SCt0CJ,IAAIS,EAFJC,cAAgBA,eAAiB,uBAGjC,IAAIC,GAAe,EACfC,EAAa,EACbC,GAAe,EAEZ,SAASC,EAAYC,GACxBC,QAAQC,IAAIC,KAAKC,UAAUJ,IAC1BJ,EACHF,EAAUW,KAAKF,KAAKC,UAAUJ,IAE9BC,QAAQK,MAAM,mBAKT,SAASC,EAAUC,IACzBd,EAAY,IAAIe,UAAUd,gBAChBe,OAAS,SAASC,GAC3Bf,GAAe,EACfK,QAAQW,KAAK,qBACbC,cAAchB,GACdA,EAAa,GAEdH,EAAUoB,QAAU,SAASH,GAC5Bf,GAAe,EACVC,GAAeC,EAETA,IACVA,GAAe,GAFfD,EAAakB,YAAYR,EAAW,KAIrCN,QAAQW,KAAK,sBAEdlB,EAAUsB,UAAY,SAASL,GAC9B,IAAIM,EAAUd,KAAKe,MAAMP,EAAI9G,MAC7BoG,QAAQC,IAAIS,EAAI9G,MAChB2G,EAAUS,IAEXvB,EAAUyB,QAAU,SAASR,GAC5BV,QAAQK,MAAM,6CACdV,GAAe,EACVC,IACJA,EAAakB,YAAYR,EAAW,2YCgC/Ba,UAAUhF,2PAFiBgF,UAAUhF,EAAE,aAAa,2GAD7CiF,eAEDC,kCAHGC,wEAAAA,YAAAA,oGAEkBH,UAAUhF,EAAE,aAAa,wHALpDoF,2BAALzF,mEAAAA,sEAAAA,sEAAKyF,cAALzF,4FAAAA,wBAAAA,SAAAA,8BF8EH,SAAsB0F,EAAYvC,GAC9B,IAAK,IAAI9C,EAAI,EAAGA,EAAIqF,EAAW1F,OAAQK,GAAK,EACpCqF,EAAWrF,IACXqF,EAAWrF,GAAGgD,EAAEF,6BE7I5B,IAAIsC,EAAQ,CAAC,EAAE,EAAE,EAAE,GACfJ,GAAS,EF4gBb,IAAiBlJ,EElgBjB,SAASwJ,EAAWtF,aAElBgF,EADGA,IAAUhF,GACJ,EAEDA,GAET2D,EAAY,CACXqB,MAASA,EACTO,MAASH,EAAMpF,GACfwF,KAAQR,GAAS,IAGnB,SAASS,cAERL,EAAQ,IAAIA,EAAM1C,OAAOV,GAAGA,GAAI,KAGjC,SAASoC,EAAU3G,GAClBoG,QAAQC,IAAIrG,GACLA,EAAKiI,eAAe,qBAC1BN,EAAQ3H,EAAK2H,OACbK,KAEMhI,EAAKiI,eAAe,oBACpBV,EAAQvH,EAAKuH,OAUrB,SAASW,EAAW3F,GACnB,IAAI4F,EAAU,CACbR,MAASA,GAENJ,IAAUhF,IACb4F,EAAe,MAAIR,EAAMpF,IAE1B2D,EAAYiC,GACZH,IFwdgB3J,EE1gBT+J,WACP1B,EAAUC,KF0gBPxF,IAAwBkB,GAAGwB,SAAS/B,KAAKzD,GAK7C,SAAmBA,GACf8C,IAAwBkB,GAAGyB,WAAWhC,KAAKzD,GE7gB/CgK,CAAUD,UDkCTrC,GAAe,EACfE,GAAe,EACfJ,EAAUyC,UCaXN,gNC/DU,gEAAQ,CACjB9I,OAAQW,SAAS0I"} -------------------------------------------------------------------------------- /lower-third-simple/assets/global.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | position: relative; 3 | width: 100%; 4 | height: 100%; 5 | } 6 | 7 | body { 8 | color: #333; 9 | margin: 0; 10 | padding: 8px; 11 | box-sizing: border-box; 12 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; 13 | } 14 | 15 | a { 16 | color: rgb(0,100,200); 17 | text-decoration: none; 18 | } 19 | 20 | a:hover { 21 | text-decoration: underline; 22 | } 23 | 24 | a:visited { 25 | color: rgb(0,80,160); 26 | } 27 | 28 | label { 29 | display: block; 30 | } 31 | 32 | input, button, select, textarea { 33 | font-family: inherit; 34 | font-size: inherit; 35 | padding: 0.4em; 36 | margin: 0 0 0.5em 0; 37 | box-sizing: border-box; 38 | border: 1px solid #ccc; 39 | border-radius: 2px; 40 | } 41 | 42 | input:disabled { 43 | color: #ccc; 44 | } 45 | 46 | input[type="range"] { 47 | height: 0; 48 | } 49 | 50 | button { 51 | background-color: #f4f4f4; 52 | outline: none; 53 | } 54 | 55 | button:active { 56 | background-color: #ddd; 57 | } 58 | 59 | button:focus { 60 | border-color: #666; 61 | } -------------------------------------------------------------------------------- /lower-third-simple/config.dist.js: -------------------------------------------------------------------------------- 1 | WEBSOCKET_URI = "ws://127.0.0.1:8089/bible" 2 | -------------------------------------------------------------------------------- /lower-third-simple/controller-old.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Lower Third Controller 6 | 9 | 10 | 15 | 16 | 17 |
18 |
19 |
20 |
21 |
22 |
Lower Third control
23 |
24 | 25 | 26 | 27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
Connection settings
35 |
36 |
37 | 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 | 98 | 99 | -------------------------------------------------------------------------------- /lower-third-simple/controller.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Lower third simple 7 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /lower-third-simple/css/bootstrap-darkly.min.css: -------------------------------------------------------------------------------- 1 | ../../css/bootstrap-darkly.min.css -------------------------------------------------------------------------------- /lower-third-simple/overlay.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Lower Third 6 | 9 | 45 | 46 | 47 |
48 |
49 | Main header line 50 |
51 |
52 | 98 | 99 | 100 | -------------------------------------------------------------------------------- /lower-third-simple/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "svelte-app", 3 | "version": "1.0.0", 4 | "devDependencies": { 5 | "npm-run-all": "^4.1.5", 6 | "rollup": "^1.27.4", 7 | "rollup-plugin-commonjs": "^9.3.4", 8 | "rollup-plugin-livereload": "^1.0.4", 9 | "rollup-plugin-node-resolve": "^4.2.4", 10 | "rollup-plugin-svelte": "^5.1.1", 11 | "rollup-plugin-terser": "^4.0.4", 12 | "sirv-cli": "^0.4.5", 13 | "svelte": "^3.15.0" 14 | }, 15 | "scripts": { 16 | "build": "rollup -c", 17 | "autobuild": "rollup -c -w", 18 | "dev": "run-p start:dev autobuild", 19 | "start": "sirv public", 20 | "start:dev": "sirv public --dev" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /lower-third-simple/rollup.config.js: -------------------------------------------------------------------------------- 1 | import svelte from 'rollup-plugin-svelte'; 2 | import resolve from 'rollup-plugin-node-resolve'; 3 | import commonjs from 'rollup-plugin-commonjs'; 4 | import livereload from 'rollup-plugin-livereload'; 5 | import { terser } from 'rollup-plugin-terser'; 6 | 7 | const production = !process.env.ROLLUP_WATCH; 8 | 9 | export default { 10 | input: 'src/main.js', 11 | output: { 12 | sourcemap: true, 13 | format: 'iife', 14 | name: 'app', 15 | file: 'assets/bundle.js' 16 | }, 17 | plugins: [ 18 | svelte({ 19 | // enable run-time checks when not in production 20 | dev: !production, 21 | // we'll extract any component CSS out into 22 | // a separate file  better for performance 23 | css: css => { 24 | css.write('assets/bundle.css'); 25 | } 26 | }), 27 | 28 | // If you have external dependencies installed from 29 | // npm, you'll most likely need these plugins. In 30 | // some cases you'll need additional configuration  31 | // consult the documentation for details: 32 | // https://github.com/rollup/rollup-plugin-commonjs 33 | resolve({ browser: true }), 34 | commonjs(), 35 | 36 | // Watch the `public` directory and refresh the 37 | // browser on changes when not in production 38 | !production && livereload('assets'), 39 | 40 | // If we're building for production (npm run build 41 | // instead of npm run dev), minify 42 | production && terser() 43 | ], 44 | watch: { 45 | clearScreen: false 46 | } 47 | }; 48 | -------------------------------------------------------------------------------- /lower-third-simple/src/App.svelte: -------------------------------------------------------------------------------- 1 | 6 | 68 | 69 |
70 | {#each lines as line, i} 71 |
72 | changeLine(i)}> 75 | 79 |
80 | {/each} 81 |
82 | -------------------------------------------------------------------------------- /lower-third-simple/src/WebsocketConfig.svelte: -------------------------------------------------------------------------------- 1 | 4 |
5 |
6 |
Connection settings
7 |
8 |
9 | 10 |
11 | 12 | 13 | 14 |
15 |
16 |
17 |
18 |
19 | 20 |
21 | 22 |
23 | 24 |
25 |
26 |
27 |
28 |
29 |
30 | 31 | 32 |
33 |
34 |
35 |
-------------------------------------------------------------------------------- /lower-third-simple/src/main.js: -------------------------------------------------------------------------------- 1 | import App from './App.svelte'; 2 | 3 | var app = new App({ 4 | target: document.body 5 | }); 6 | 7 | export default app; -------------------------------------------------------------------------------- /lower-third-simple/src/websocket-client.js: -------------------------------------------------------------------------------- 1 | //Modify this to point to the ip address where your server is running on 2 | WEBSOCKET_URI = WEBSOCKET_URI || "ws://127.0.0.1:8089/"; 3 | 4 | let websocket; 5 | let socketIsOpen = false; 6 | let intervalID = 0; 7 | let closedByUser = false; 8 | 9 | export function sendCommand(obj) { 10 | console.log(JSON.stringify(obj)); 11 | if (socketIsOpen) { 12 | websocket.send(JSON.stringify(obj)); 13 | } else { 14 | console.error('Not connected\n'); 15 | } 16 | } 17 | 18 | 19 | export function doConnect(onMessage) { 20 | websocket = new WebSocket(WEBSOCKET_URI); 21 | websocket.onopen = function(evt) { 22 | socketIsOpen = true; 23 | console.info("Connection opened"); 24 | clearInterval(intervalID); 25 | intervalID = 0; 26 | }; 27 | websocket.onclose = function(evt) { 28 | socketIsOpen = true; 29 | if (!intervalID && !closedByUser) { 30 | intervalID = setInterval(doConnect, 5000); 31 | } else if (closedByUser) { 32 | closedByUser = false; 33 | } 34 | console.info("Connection closed"); 35 | }; 36 | websocket.onmessage = function(evt) { 37 | var jsonOBJ = JSON.parse(evt.data); 38 | console.log(evt.data); 39 | onMessage(jsonOBJ); 40 | }; 41 | websocket.onerror = function(evt) { 42 | console.error('Connection failed, is the Server running?'); 43 | socketIsOpen = false; 44 | if (!intervalID) { 45 | intervalID = setInterval(doConnect, 5000); 46 | } 47 | }; 48 | } 49 | 50 | export function doDisconnect() { 51 | socketIsOpen = false; 52 | closedByUser = true; 53 | websocket.close(); 54 | } -------------------------------------------------------------------------------- /lower-third/controller.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Lower Third Controller 6 | 9 | 10 | 12 | 13 | 14 |
15 |
16 |
17 |
18 |
19 |
Lower Third control
20 |
21 | 22 | 23 |
24 |
25 | 26 | 27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
Connection settings
35 |
36 |
37 | 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 | 95 | 96 | -------------------------------------------------------------------------------- /lower-third/overlay.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Lower Third 6 | 9 | 61 | 62 | 63 |
64 |
65 | Main header line 66 | Sub header line 67 |
68 |
69 | 113 | 114 | 115 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "websocket-overlay", 3 | "version": "1.0.0", 4 | "description": "HTML5 overlays controlled via websocket server from controller.html", 5 | "main": "server.js", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "start": "node server.js" 9 | }, 10 | "repository": { 11 | "type": "git", 12 | "url": "git+https://github.com/filiphanes/websocket-overlays.git" 13 | }, 14 | "keywords": [ 15 | "html5", 16 | "overlay", 17 | "websocket", 18 | "obs-studio", 19 | "splitx", 20 | "casparcg", 21 | "broadcast" 22 | ], 23 | "author": "Filip Hanes ", 24 | "license": "MIT", 25 | "bugs": { 26 | "url": "https://github.com/filiphanes/websocket-overlays/issues" 27 | }, 28 | "homepage": "https://github.com/filiphanes/websocket-overlays#readme", 29 | "dependencies": { 30 | "ws": "^7.0.1" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /pnpm-lock.yaml: -------------------------------------------------------------------------------- 1 | dependencies: 2 | gun: 0.2020.301 3 | ws: 7.2.3 4 | lockfileVersion: 5.1 5 | packages: 6 | /@peculiar/asn1-schema/1.0.5: 7 | dependencies: 8 | asn1js: 2.0.26 9 | tslib: 1.11.1 10 | dev: false 11 | engines: 12 | node: '>=8.0.0' 13 | optional: true 14 | resolution: 15 | integrity: sha512-rzzorGYnQNmVHleLvC8gJSbbdNYtg+EB9s075dHvwpxs10teXHYnRmTWhCVuWjbSVSofwdm7IYPtMTWTbcNUWA== 16 | /@peculiar/json-schema/1.1.10: 17 | dependencies: 18 | tslib: 1.11.1 19 | dev: false 20 | engines: 21 | node: '>=8.0.0' 22 | optional: true 23 | resolution: 24 | integrity: sha512-kbpnG9CkF1y6wwGkW7YtSA+yYK4X5uk4rAwsd1hxiaYE3Hkw2EsGlbGh/COkMLyFf+Fe830BoFiMSB3QnC/ItA== 25 | /@peculiar/webcrypto/1.0.26: 26 | dependencies: 27 | '@peculiar/asn1-schema': 1.0.5 28 | '@peculiar/json-schema': 1.1.10 29 | asn1js: 2.0.26 30 | pvtsutils: 1.0.10 31 | tslib: 1.11.1 32 | webcrypto-core: 1.0.18 33 | dev: false 34 | engines: 35 | node: '>=10.12.0' 36 | optional: true 37 | resolution: 38 | integrity: sha512-7Ws4t+eUiLPSL9q8OCcbevd9S8qjqBDAXJrbnjzbuWu3nI8NgBvc7qZ97DZO8y8qxEoh5cGqK2p+p+UhxLw9Kg== 39 | /@unimodules/core/5.1.0: 40 | dependencies: 41 | compare-versions: 3.6.0 42 | dev: false 43 | optional: true 44 | resolution: 45 | integrity: sha512-gaamGkJ4PVwusWEfsZyPo4uhrVWPDE0BmHc/lTYfkZCv2oIAswC7gG/ULRdtZpYdwnYqFIZng+WQxwuVrJUNDw== 46 | /@unimodules/react-native-adapter/5.1.1: 47 | dependencies: 48 | invariant: 2.2.4 49 | lodash: 4.17.15 50 | prop-types: 15.7.2 51 | dev: false 52 | optional: true 53 | peerDependencies: 54 | react-native: '*' 55 | resolution: 56 | integrity: sha512-PlP6QQ2Z3ckORhS07tWcIweK+CkkxyzitJ1j1FD+N+G7G/CB99/vSfCEQ7BFVAPRO5vPrcS2QcwSDgvz06wKVA== 57 | /addressparser/0.3.2: 58 | dev: false 59 | optional: true 60 | resolution: 61 | integrity: sha1-WYc/Nej89sc2HBAjkmHXbhU0i7I= 62 | /asmcrypto.js/0.22.0: 63 | dev: false 64 | optional: true 65 | resolution: 66 | integrity: sha512-usgMoyXjMbx/ZPdzTSXExhMPur2FTdz/Vo5PVx2gIaBcdAAJNOFlsdgqveM8Cff7W0v+xrf9BwjOV26JSAF9qA== 67 | /asn1js/2.0.26: 68 | dependencies: 69 | pvutils: 1.0.17 70 | dev: false 71 | engines: 72 | node: '>=6.0.0' 73 | optional: true 74 | resolution: 75 | integrity: sha512-yG89F0j9B4B0MKIcFyWWxnpZPLaNTjCj4tkE3fjbAoo0qmpGw0PYYqSbX/4ebnd9Icn8ZgK4K1fvDyEtW1JYtQ== 76 | /b64-lite/1.4.0: 77 | dependencies: 78 | base-64: 0.1.0 79 | dev: false 80 | optional: true 81 | resolution: 82 | integrity: sha512-aHe97M7DXt+dkpa8fHlCcm1CnskAHrJqEfMI0KN7dwqlzml/aUe1AGt6lk51HzrSfVD67xOso84sOpr+0wIe2w== 83 | /b64u-lite/1.1.0: 84 | dependencies: 85 | b64-lite: 1.4.0 86 | dev: false 87 | optional: true 88 | resolution: 89 | integrity: sha512-929qWGDVCRph7gQVTC6koHqQIpF4vtVaSbwLltFQo44B1bYUquALswZdBKFfrJCPEnsCOvWkJsPdQYZ/Ukhw8A== 90 | /base-64/0.1.0: 91 | dev: false 92 | optional: true 93 | resolution: 94 | integrity: sha1-eAqZyE59YAJgNhURxId2E78k9rs= 95 | /base64-js/1.3.1: 96 | dev: false 97 | resolution: 98 | integrity: sha512-mLQ4i2QO1ytvGWFWmcngKO//JXAQueZvwEKtjgQFM4jIK0kU+ytMfplL8j+n5mspOfjHwoAg+9yhb7BwAHm36g== 99 | /buffer/5.5.0: 100 | dependencies: 101 | base64-js: 1.3.1 102 | ieee754: 1.1.13 103 | dev: false 104 | resolution: 105 | integrity: sha512-9FTEDjLjwoAkEwyMGDjYJQN2gfRgOKBKRfiglhvibGbpeeU/pQn1bJxQqm32OD/AIeEuHxU9roxXxg34Byp/Ww== 106 | /bufferutil/4.0.1: 107 | dependencies: 108 | node-gyp-build: 3.7.0 109 | dev: false 110 | requiresBuild: true 111 | resolution: 112 | integrity: sha512-xowrxvpxojqkagPcWRQVXZl0YXhRhAtBEIq3VoER1NH5Mw1n1o0ojdspp+GS2J//2gCVyrzQDApQ4unGF+QOoA== 113 | /compare-versions/3.6.0: 114 | dev: false 115 | optional: true 116 | resolution: 117 | integrity: sha512-W6Af2Iw1z4CB7q4uU4hv646dW9GQuBM+YpC0UvUCWSD8w90SJjp+ujJuXaEMtAXBtSqGfMPuFOVn4/+FlaqfBA== 118 | /emailjs-base64/1.1.4: 119 | dev: false 120 | optional: true 121 | resolution: 122 | integrity: sha512-4h0xp1jgVTnIQBHxSJWXWanNnmuc5o+k4aHEpcLXSToN8asjB5qbXAexs7+PEsUKcEyBteNYsSvXUndYT2CGGA== 123 | /emailjs-mime-codec/2.0.9: 124 | dependencies: 125 | emailjs-base64: 1.1.4 126 | ramda: 0.26.1 127 | text-encoding: 0.7.0 128 | dev: false 129 | optional: true 130 | resolution: 131 | integrity: sha512-7qJo4pFGcKlWh/kCeNjmcgj34YoJWY0ekZXEHYtluWg4MVBnXqGM4CRMtZQkfYwitOhUgaKN5EQktJddi/YIDQ== 132 | /emailjs/2.2.0: 133 | dependencies: 134 | addressparser: 0.3.2 135 | emailjs-mime-codec: 2.0.9 136 | dev: false 137 | optional: true 138 | resolution: 139 | integrity: sha1-ulsj5KSwpFEPZS6HOxVOlAe2ygM= 140 | /expo-random/8.1.0: 141 | dependencies: 142 | base64-js: 1.3.1 143 | dev: false 144 | optional: true 145 | resolution: 146 | integrity: sha512-9n2gg83Hpg3ErkKu+a3FFOGmaPIxaHn6RuzjW24xFckdfmnrAKtbs1aU1aAcmoL1kXPvDeufRSEV/3lW93u6ug== 147 | /gun/0.2020.301: 148 | dependencies: 149 | buffer: 5.5.0 150 | ws: 7.2.3_5290a7aab7631971258e1bd11475725e 151 | dev: false 152 | engines: 153 | node: '>=0.8.4' 154 | optionalDependencies: 155 | bufferutil: 4.0.1 156 | emailjs: 2.2.0 157 | isomorphic-webcrypto: 2.3.6 158 | text-encoding: 0.7.0 159 | utf-8-validate: 5.0.2 160 | resolution: 161 | integrity: sha512-Jb5VNKgBt2my+XHB/K65gTOMbBICgglc4kVaMmQ/lD7ZyxLvwUSXissfcLHAwBxUWPU3P4GqTi+fQmsK068FQw== 162 | /ieee754/1.1.13: 163 | dev: false 164 | resolution: 165 | integrity: sha512-4vf7I2LYV/HaWerSo3XmlMkp5eZ83i+/CDluXi/IGTs/O1sejBNhTtnxzmRZfvOUqj7lZjqHkeTvpgSFDlWZTg== 166 | /invariant/2.2.4: 167 | dependencies: 168 | loose-envify: 1.4.0 169 | dev: false 170 | optional: true 171 | resolution: 172 | integrity: sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA== 173 | /isomorphic-webcrypto/2.3.6: 174 | dependencies: 175 | '@peculiar/webcrypto': 1.0.26 176 | asmcrypto.js: 0.22.0 177 | b64-lite: 1.4.0 178 | b64u-lite: 1.1.0 179 | msrcrypto: 1.5.8 180 | str2buf: 1.3.0 181 | webcrypto-shim: 0.1.5 182 | dev: false 183 | optional: true 184 | optionalDependencies: 185 | '@unimodules/core': 5.1.0 186 | '@unimodules/react-native-adapter': 5.1.1 187 | expo-random: 8.1.0 188 | react-native-securerandom: 0.1.1 189 | resolution: 190 | integrity: sha512-d1prB3b0UMWOao5DK3+O2Dr5ZJCakzB5Q+2kCWNkNuM9ln7VB8TSw2SwUjbnErzg7cgsYja+VPQaeBtXEojpew== 191 | /js-tokens/4.0.0: 192 | dev: false 193 | optional: true 194 | resolution: 195 | integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== 196 | /lodash/4.17.15: 197 | dev: false 198 | optional: true 199 | resolution: 200 | integrity: sha512-8xOcRHvCjnocdS5cpwXQXVzmmh5e5+saE2QGoeQmbKmRS6J3VQppPOIt0MnmE+4xlZoumy0GPG0D0MVIQbNA1A== 201 | /loose-envify/1.4.0: 202 | dependencies: 203 | js-tokens: 4.0.0 204 | dev: false 205 | hasBin: true 206 | optional: true 207 | resolution: 208 | integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q== 209 | /msrcrypto/1.5.8: 210 | dev: false 211 | optional: true 212 | resolution: 213 | integrity: sha512-ujZ0TRuozHKKm6eGbKHfXef7f+esIhEckmThVnz7RNyiOJd7a6MXj2JGBoL9cnPDW+JMG16MoTUh5X+XXjI66Q== 214 | /node-gyp-build/3.7.0: 215 | dev: false 216 | hasBin: true 217 | resolution: 218 | integrity: sha512-L/Eg02Epx6Si2NXmedx+Okg+4UHqmaf3TNcxd50SF9NQGcJaON3AtU++kax69XV7YWz4tUspqZSAsVofhFKG2w== 219 | /object-assign/4.1.1: 220 | dev: false 221 | engines: 222 | node: '>=0.10.0' 223 | optional: true 224 | resolution: 225 | integrity: sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM= 226 | /prop-types/15.7.2: 227 | dependencies: 228 | loose-envify: 1.4.0 229 | object-assign: 4.1.1 230 | react-is: 16.13.1 231 | dev: false 232 | optional: true 233 | resolution: 234 | integrity: sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ== 235 | /pvtsutils/1.0.10: 236 | dependencies: 237 | tslib: 1.11.1 238 | dev: false 239 | optional: true 240 | resolution: 241 | integrity: sha512-8ZKQcxnZKTn+fpDh7wL4yKax5fdl3UJzT8Jv49djZpB/dzPxacyN1Sez90b6YLdOmvIr9vaySJ5gw4aUA1EdSw== 242 | /pvutils/1.0.17: 243 | dev: false 244 | engines: 245 | node: '>=6.0.0' 246 | optional: true 247 | resolution: 248 | integrity: sha512-wLHYUQxWaXVQvKnwIDWFVKDJku9XDCvyhhxoq8dc5MFdIlRenyPI9eSfEtcvgHgD7FlvCyGAlWgOzRnZD99GZQ== 249 | /ramda/0.26.1: 250 | dev: false 251 | optional: true 252 | resolution: 253 | integrity: sha512-hLWjpy7EnsDBb0p+Z3B7rPi3GDeRG5ZtiI33kJhTt+ORCd38AbAIjB/9zRIUoeTbE/AVX5ZkU7m6bznsvrf8eQ== 254 | /react-is/16.13.1: 255 | dev: false 256 | optional: true 257 | resolution: 258 | integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ== 259 | /react-native-securerandom/0.1.1: 260 | dependencies: 261 | base64-js: 1.3.1 262 | dev: false 263 | optional: true 264 | peerDependencies: 265 | react-native: '*' 266 | resolution: 267 | integrity: sha1-8TBiOkEsM4sK+t7bwgTFy7i/IHA= 268 | /str2buf/1.3.0: 269 | dev: false 270 | optional: true 271 | resolution: 272 | integrity: sha512-xIBmHIUHYZDP4HyoXGHYNVmxlXLXDrtFHYT0eV6IOdEj3VO9ccaF1Ejl9Oq8iFjITllpT8FhaXb4KsNmw+3EuA== 273 | /text-encoding/0.7.0: 274 | deprecated: no longer maintained 275 | dev: false 276 | optional: true 277 | resolution: 278 | integrity: sha512-oJQ3f1hrOnbRLOcwKz0Liq2IcrvDeZRHXhd9RgLrsT+DjWY/nty1Hi7v3dtkaEYbPYe0mUoOfzRrMwfXXwgPUA== 279 | /tslib/1.11.1: 280 | dev: false 281 | optional: true 282 | resolution: 283 | integrity: sha512-aZW88SY8kQbU7gpV19lN24LtXh/yD4ZZg6qieAJDDg+YBsJcSmLGK9QpnUjAKVG/xefmvJGd1WUmfpT/g6AJGA== 284 | /utf-8-validate/5.0.2: 285 | dependencies: 286 | node-gyp-build: 3.7.0 287 | dev: false 288 | requiresBuild: true 289 | resolution: 290 | integrity: sha512-SwV++i2gTD5qh2XqaPzBnNX88N6HdyhQrNNRykvcS0QKvItV9u3vPEJr+X5Hhfb1JC0r0e1alL0iB09rY8+nmw== 291 | /webcrypto-core/1.0.18: 292 | dependencies: 293 | pvtsutils: 1.0.10 294 | tslib: 1.11.1 295 | dev: false 296 | optional: true 297 | resolution: 298 | integrity: sha512-wHRMXYxtDUWsTXNyRdaYlbcbq1OJF9pQov5THqvn5OBvixpCjnjU2spvEscxqRY8bLlpHk2S7RtROIMyNoEyFg== 299 | /webcrypto-shim/0.1.5: 300 | dev: false 301 | optional: true 302 | resolution: 303 | integrity: sha512-mE+E00gulvbLjHaAwl0kph60oOLQRsKyivEFgV9DMM/3Y05F1vZvGq12hAcNzHRnYxyEOABBT/XMtwGSg5xA7A== 304 | /ws/7.2.3: 305 | dev: false 306 | engines: 307 | node: '>=8.3.0' 308 | peerDependencies: 309 | bufferutil: ^4.0.1 310 | utf-8-validate: ^5.0.2 311 | peerDependenciesMeta: 312 | bufferutil: 313 | optional: true 314 | utf-8-validate: 315 | optional: true 316 | resolution: 317 | integrity: sha512-HTDl9G9hbkNDk98naoR/cHDws7+EyYMOdL1BmjsZXRUjf7d+MficC4B7HLUPlSiho0vg+CWKrGIt/VJBd1xunQ== 318 | /ws/7.2.3_5290a7aab7631971258e1bd11475725e: 319 | dependencies: 320 | bufferutil: 4.0.1 321 | utf-8-validate: 5.0.2 322 | dev: false 323 | engines: 324 | node: '>=8.3.0' 325 | peerDependencies: 326 | bufferutil: ^4.0.1 327 | utf-8-validate: ^5.0.2 328 | peerDependenciesMeta: 329 | bufferutil: 330 | optional: true 331 | utf-8-validate: 332 | optional: true 333 | resolution: 334 | integrity: sha512-HTDl9G9hbkNDk98naoR/cHDws7+EyYMOdL1BmjsZXRUjf7d+MficC4B7HLUPlSiho0vg+CWKrGIt/VJBd1xunQ== 335 | specifiers: 336 | gun: ^0.2020.301 337 | ws: ^7.0.1 338 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | websockets -------------------------------------------------------------------------------- /scoreboard/controller.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Scoreboard Controller 6 | 9 | 10 | 18 | 19 | 20 |
21 |
22 |
23 |
24 |
25 |
Scoreboard control
26 |
27 |
28 | 29 | 30 |
31 |
32 | 33 | 34 |
35 |
36 |
37 | 38 |
39 | 40 | - 41 | 42 |
43 | Team names and Scores are updated automatically on change and show. 44 |
45 |
46 | 47 | 48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 |
Connection settings
56 |
57 |
58 | 59 |
60 | 61 | 62 | 63 |
64 |
65 |
66 |
67 |
68 | 69 |
70 | 71 |
72 | 73 |
74 |
75 |
76 |
77 |
78 |
79 | 80 | 81 |
82 |
83 |
84 |
85 |
86 |
87 | The scoreboard animation in this example was made by glowdragon.de 88 |
89 | 90 | 135 | 136 | -------------------------------------------------------------------------------- /scoreboard/css/overlay.css: -------------------------------------------------------------------------------- 1 | @import url('https://fonts.googleapis.com/css?family=Montserrat'); 2 | 3 | html, body { 4 | height: 1080px; 5 | width: 1920px; 6 | margin: 0; 7 | overflow: hidden; 8 | font-family: 'Montserrat'; 9 | background-color: transparent; 10 | } 11 | 12 | .clear { 13 | clear: both; 14 | } 15 | 16 | #scoreboard { 17 | -webkit-animation-timing-function: ease-in-out; 18 | -webkit-animation-duration: 2s; 19 | width: 128px; 20 | height: 50px; 21 | margin: 1080px auto 0; 22 | border-radius: 25px 25px 25px 25px; 23 | box-shadow: 0px 0px 5px 1px rgba(0,0,0,0.75); 24 | } 25 | 26 | @-webkit-keyframes animation-scoreboard{ 27 | 0% { width: 128px; margin-top: 1080px; } 28 | 33% { width: 128px; margin-top: 800px; } 29 | 66% { width: 640px; margin-top: 800px; } 30 | 100% { width: 640px; margin-top: 800px; } 31 | } 32 | 33 | .team { 34 | -webkit-animation-timing-function: ease-in-out; 35 | animation-timing-function: ease-in-out; 36 | -webkit-animation-duration: 2s; 37 | animation-duration: 2s; 38 | float: left; 39 | display: block; 40 | width: 0px; 41 | height: 44px; 42 | padding-top: 6px; 43 | border: 0 solid black; 44 | text-align: center; 45 | font-size: 22pt; 46 | color: white; 47 | } 48 | 49 | #team1div { 50 | background-color: #3093c7; 51 | background-image: -webkit-gradient(linear, left top, left bottom, from(#3093c7), to(#1c5a85)); 52 | background-image: -webkit-linear-gradient(top, #3093c7, #1c5a85); 53 | background-image: -moz-linear-gradient(top, #3093c7, #1c5a85); 54 | background-image: -ms-linear-gradient(top, #3093c7, #1c5a85); 55 | background-image: -o-linear-gradient(top, #3093c7, #1c5a85); 56 | background-image: linear-gradient(to bottom, #3093c7, #1c5a85); 57 | filter:progid:DXImageTransform.Microsoft.gradient(GradientType=0,startColorstr=#3093c7, endColorstr=#1c5a85); 58 | border-radius: 25px 0px 0px 25px; 59 | } 60 | 61 | #team2div { 62 | background-color: #a90329; 63 | background-image: -webkit-gradient(linear, left top, left bottom, from(#a90329), to(#6d0019)); 64 | background-image: -webkit-linear-gradient(top, #a90329, #6d0019); 65 | background-image: -moz-linear-gradient(top, #a90329, #6d0019); 66 | background-image: -ms-linear-gradient(top, #a90329, #6d0019); 67 | background-image: -o-linear-gradient(top, #a90329, #6d0019); 68 | background-image: linear-gradient(to bottom, #a90329, #6d0019); 69 | filter:progid:DXImageTransform.Microsoft.gradient(GradientType=0,startColorstr=#a90329, endColorstr=#6d0019); 70 | border-radius: 0px 25px 25px 0px; 71 | } 72 | 73 | @-webkit-keyframes animation-team { 74 | 0% { width: 0px; } 75 | 33% { width: 0px; } 76 | 66% { width: 256px; } 77 | 100% { width: 256px; } 78 | } 79 | 80 | .team-name { 81 | -webkit-animation-timing-function: ease-in-out; 82 | -webkit-animation-duration: 2s; 83 | text-shadow: 0px 0px 2px rgba(0, 0, 0, 1); 84 | width: 0px; 85 | } 86 | 87 | @-webkit-keyframes animation-team-name { 88 | 0% { opacity: 0; } 89 | 66% { opacity: 0; } 90 | 100% { opacity: 1; } 91 | } 92 | 93 | #scorePlaceholder { 94 | float: left; 95 | width: 128px; 96 | } 97 | 98 | #score { 99 | -webkit-animation-timing-function: ease-in-out; 100 | animation-timing-function: ease-in-out; 101 | -webkit-animation-duration: 2s; 102 | animation-duration: 2s; 103 | position: absolute; 104 | top: 1080px; 105 | left: 893px; 106 | width: 134px; 107 | height: 47px; 108 | padding-top: 9px; 109 | text-align: center; 110 | background-color: white; 111 | font-size: 22pt; 112 | color: black; 113 | border-radius: 3px; 114 | border: 0 solid black; 115 | box-shadow: 0px 0px 5px 1px rgba(0,0,0,0.75); 116 | } 117 | 118 | @-webkit-keyframes animation-score { 119 | 0% { top: 1080px; } 120 | 33% { top: 797px; } 121 | 100% { top: 797px; } 122 | } 123 | 124 | .show #score { 125 | top: 797px; 126 | } 127 | .show #scoreboard { 128 | margin-top: 800px; 129 | width: 640px; 130 | } 131 | .show .team { 132 | width: 256px; 133 | } 134 | .show .team-name { 135 | width: 256px; 136 | } 137 | 138 | .animated #score { 139 | visibility: visible; 140 | -webkit-animation-name: "animation-score"; 141 | animation-name: "animation-score"; 142 | animation-direction: normal; 143 | } 144 | .animated #scoreboard { 145 | visibility: visible; 146 | -webkit-animation-name: "animation-scoreboard"; 147 | animation-name: "animation-scoreboard"; 148 | animation-direction: normal; 149 | } 150 | .animated .team { 151 | -webkit-animation-name: "animation-team"; 152 | animation-name: "animation-team"; 153 | animation-direction: normal; 154 | } 155 | .animated .team-name { 156 | -webkit-animation-name: "animation-team-name"; 157 | animation-name: "animation-team-name"; 158 | animation-direction: normal; 159 | } 160 | 161 | .show.animated #score, 162 | .show.animated #scoreboard, 163 | .show.animated .team, 164 | .show.animated .team-name { 165 | animation-direction: reverse; 166 | } -------------------------------------------------------------------------------- /scoreboard/overlay.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Scoreboard 6 | 9 | 10 | 70 | 71 | 72 |
73 | 0 - 0 74 |
75 |
76 |
77 | Team 1 78 |
79 |
.
80 |
81 | Team 2 82 |
83 |
84 |
85 | 86 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | const WebSocket = require('ws'); 2 | const url = require('url'); 3 | 4 | const server = new WebSocket.Server({ 5 | // host: process.argv[1] || '0.0.0.0', 6 | port: process.argv[2] || 8089, 7 | perMessageDeflate: { 8 | zlibDeflateOptions: { 9 | // See zlib defaults. 10 | chunkSize: 1024, 11 | memLevel: 7, 12 | level: 3 13 | }, 14 | zlibInflateOptions: { 15 | chunkSize: 10 * 1024 16 | }, 17 | // Other options settable: 18 | clientNoContextTakeover: true, // Defaults to negotiated value. 19 | serverNoContextTakeover: true, // Defaults to negotiated value. 20 | serverMaxWindowBits: 10, // Defaults to negotiated value. 21 | // Below options specified as default values. 22 | concurrencyLimit: 10, // Limits zlib concurrency for perf. 23 | threshold: 1024 // Size (in bytes) below which messages 24 | // should not be compressed. 25 | } 26 | }); 27 | 28 | let STATE = {}; 29 | 30 | server.on('connection', function connection(ws, req) { 31 | const ip = req.connection.remoteAddress; 32 | const port = req.connection.remotePort; 33 | const pathname = url.parse(req.url).pathname; 34 | console.log(ip, port, pathname, 'connected'); 35 | if (!STATE[pathname]) { 36 | STATE[pathname] = {}; 37 | } 38 | ws.send(JSON.stringify(STATE[pathname])); 39 | 40 | ws.on('message', function incoming(message) { 41 | console.log(ip, port, message) 42 | const data = JSON.parse(message); 43 | Object.assign(STATE[pathname], data); 44 | // Broadcast to everyone else. 45 | server.clients.forEach(function each(client) { 46 | if (client !== ws && client.readyState === WebSocket.OPEN) { 47 | client.send(message); 48 | } 49 | }); 50 | }); 51 | }); 52 | -------------------------------------------------------------------------------- /server.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | "Websocket server that synchronizes state across clients" 4 | 5 | import asyncio 6 | import json 7 | import sys 8 | import websockets 9 | from collections import defaultdict 10 | 11 | CLIENTS = defaultdict(set) 12 | STATE = defaultdict(dict) 13 | 14 | async def distributor(websocket, path): 15 | CLIENTS[path].add(websocket) 16 | await websocket.send(json.dumps(STATE[path])) 17 | print('%s:%s connected' % websocket.remote_address, path) 18 | try: 19 | async for message in websocket: 20 | try: 21 | STATE[path].update(json.loads(message)) 22 | except Exception: 23 | print('Message is not valid JSON', message) 24 | if len(CLIENTS[path]) > 1: # asyncio.wait doesn't accept an empty list 25 | await asyncio.wait([client.send(message) for client in CLIENTS[path] if client != websocket]) 26 | print('%s:%s' % websocket.remote_address, message) 27 | finally: 28 | CLIENTS[path].remove(websocket) 29 | if not CLIENTS[path]: 30 | del CLIENTS[path] 31 | print('%s:%s disconnected' % websocket.remote_address) 32 | 33 | def help(): 34 | print('''Websocket sync server 35 | Relays each message to all clients. 36 | Usage:', sys.argv[0], '[host [port]]''') 37 | 38 | if __name__ == '__main__': 39 | if '-h' in sys.argv or '--help' in sys.argv: 40 | help() 41 | else: 42 | host = 'localhost' 43 | port = 8000 44 | 45 | try: 46 | host = sys.argv[1] 47 | port = int(sys.argv[2]) 48 | except (IndexError, ValueError): 49 | pass 50 | 51 | try: 52 | asyncio.get_event_loop().run_until_complete( 53 | websockets.serve(distributor, host, port)) 54 | asyncio.get_event_loop().run_forever() 55 | except Exception: 56 | help() 57 | raise 58 | --------------------------------------------------------------------------------