├── .github └── FUNDING.yml ├── config.json ├── .gitignore ├── 99-belaui-check-usb-devices.rules ├── package.json ├── setup.json ├── belaUI.socket ├── install_service.sh ├── belaUI.service ├── 98-belaui-audio.rules ├── public ├── style.css ├── jquery.ui.touch-punch.js ├── index.html ├── jquery-ui-1.12.1.css └── script.js └── LICENSE /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: rationalsa 2 | -------------------------------------------------------------------------------- /config.json: -------------------------------------------------------------------------------- 1 | {"password":"changeme"} 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | auth_tokens.json 2 | node_modules 3 | package-lock.json -------------------------------------------------------------------------------- /99-belaui-check-usb-devices.rules: -------------------------------------------------------------------------------- 1 | ENV{DEVTYPE}=="usb_device", ENV{ID_VENDOR_ID}=="0fd9", RUN+="/usr/bin/pkill -o -SIGUSR2 -f belaUI.js" 2 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "serve-static": "^1.14.1", 4 | "finalhandler": "^1.1.2", 5 | "bcrypt": "^3.0.8", 6 | "ws": "^7.4.4", 7 | "xml2js": "^0.6.2" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /setup.json: -------------------------------------------------------------------------------- 1 | { 2 | "hw": "jetson", 3 | "belacoder_path": "/home/nvidia/belacoder/", 4 | "srtla_path": "/home/nvidia/srtla/", 5 | "bitrate_file": "/tmp/belacoder_br", 6 | "ips_file": "/tmp/srtla_ips" 7 | } 8 | -------------------------------------------------------------------------------- /belaUI.socket: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=Listener socket for belaUI's HTTP server 3 | Before=nginx.service 4 | 5 | [Socket] 6 | ListenStream=80 7 | Accept=false 8 | 9 | [Install] 10 | WantedBy=multi-user.target 11 | -------------------------------------------------------------------------------- /install_service.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | sed "s#WorkingDirectory=.*#WorkingDirectory=$(pwd)#g" belaUI.service > /etc/systemd/system/belaUI.service && 3 | cp belaUI.socket /etc/systemd/system/ 4 | systemctl daemon-reload && 5 | systemctl restart belaUI && 6 | systemctl enable belaUI.socket 7 | systemctl enable belaUI.service 8 | cp *.rules /etc/udev/rules.d/ 9 | -------------------------------------------------------------------------------- /belaUI.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=belaUI service 3 | After=network.target 4 | 5 | [Service] 6 | User=root 7 | Group=root 8 | # install_service.sh automatically sets WorkingDirectory to point to the current directory 9 | WorkingDirectory=/opt/belaUI 10 | ExecStart=/usr/bin/nodejs ./belaUI.js 11 | KillMode=mixed 12 | Restart=always 13 | SyslogIdentifier=belaUI 14 | 15 | [Install] 16 | WantedBy=multi-user.target 17 | -------------------------------------------------------------------------------- /98-belaui-audio.rules: -------------------------------------------------------------------------------- 1 | SUBSYSTEM!="sound", GOTO="end" 2 | ENV{SOUND_INITIALIZED}!="1", GOTO="end" 3 | ENV{ID_BUS}!="usb" GOTO="end" 4 | 5 | ACTION=="remove", GOTO="signal_belaui" 6 | 7 | ACTION!="change", GOTO="end" 8 | 9 | # the Cam Link 4K's audio id defaults to C4K - don't modify it 10 | ATTR{id}=="C4K", GOTO="signal_belaui" 11 | 12 | # it looks like sometimes OA4 comes up without an ID, try to set it 13 | ENV{ID_MODEL}=="OsmoAction4", ATTR{id}="OsmoAction4", GOTO="signal_belaui" 14 | 15 | # don't rename the OP3 audio input 16 | ATTR{id}=="DJIPocket3", GOTO="signal_belaui" 17 | 18 | # the OA5's and OA6's names might include part of the SN, discard it 19 | ATTR{id}=="OsmoAction5*", ATTR{id}="OsmoAction5", GOTO="signal_belaui" 20 | ATTR{id}=="OsmoAction6*", ATTR{id}="OsmoAction6", GOTO="signal_belaui" 21 | 22 | # set the id for the first USB audio card that's not a camlink 4K 23 | ATTR{id}="usbaudio" 24 | 25 | LABEL="signal_belaui" 26 | RUN+="/usr/bin/pkill -o -SIGUSR2 -f belaUI.js" 27 | 28 | LABEL="end" 29 | -------------------------------------------------------------------------------- /public/style.css: -------------------------------------------------------------------------------- 1 | /* 2 | belaUI - web UI for the BELABOX project 3 | Copyright (C) 2020-2022 BELABOX project 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | You should have received a copy of the GNU General Public License 15 | along with this program. If not, see . 16 | */ 17 | 18 | #page { 19 | max-width: 700px; 20 | } 21 | 22 | #main { 23 | display: flex; 24 | flex-direction: column; 25 | } 26 | 27 | #login, #initialPasswordForm { 28 | max-width: 400px; 29 | } 30 | 31 | td.signal { 32 | width: 20px; 33 | } 34 | 35 | td.band { 36 | width: 40px; 37 | font-family: monospace; 38 | font-size: 12px; 39 | } 40 | 41 | td.security { 42 | text-align: right; 43 | width: 20px; 44 | } 45 | 46 | .can-connect { 47 | cursor: pointer; 48 | } 49 | 50 | .networks td { 51 | vertical-align: bottom; 52 | } 53 | 54 | @media screen and (max-width: 500px) { 55 | .button-text { 56 | display: none; 57 | } 58 | } 59 | 60 | @media screen and (min-width: 500px) { 61 | .button-icon { 62 | display: none; 63 | } 64 | } 65 | 66 | .modem-status { 67 | background: #eee; 68 | } 69 | 70 | .button-slider-lock-unlock { 71 | background-color: #e9ecef; 72 | border: 1px solid #ced4da; 73 | height: 100%; font-size: 30px; 74 | } 75 | 76 | .button-slider-lock-unlock:hover { 77 | background-color: #6c757d; 78 | } 79 | 80 | td.sensor_name { 81 | width: 160px; 82 | } 83 | 84 | /* Dark mode theme */ 85 | 86 | body.dark { 87 | background: #1e2326; 88 | color: #FFF; 89 | } 90 | 91 | body.dark .card { 92 | background: #1e2326; 93 | border-color: rgba(255, 255, 255, 0.5); 94 | } 95 | 96 | body.dark .table { 97 | color: #fff; 98 | } 99 | 100 | body.dark .form-control, body.dark .ui-widget-content, body.dark .custom-select { 101 | background-color: #eee; 102 | } 103 | 104 | body.dark .btn-outline-secondary { 105 | color: #eee; 106 | } 107 | 108 | body.dark .text-secondary { 109 | color: #9ca5ad !important; 110 | } 111 | 112 | body.dark .modal-content { 113 | background-color: #1e2326; 114 | } 115 | 116 | body.dark .close { 117 | color: #fff; 118 | } 119 | 120 | body.dark .table-hover tbody tr:hover { 121 | color: #aaa; 122 | } 123 | 124 | body.dark .modem-status { 125 | background: #5a6268; 126 | } 127 | 128 | body.dark .button-slider-lock-unlock { 129 | background-color: #676a6c; 130 | border: 1px solid #676a6c; 131 | } 132 | 133 | body.dark .button-slider-lock-unlock:hover { 134 | background-color: #9a9fa2; 135 | } 136 | 137 | /* layoutSetting: bitrate info on top */ 138 | 139 | body.netif-first #netifTable { 140 | order: -2; 141 | } 142 | 143 | body.netif-first #sensorsTable { 144 | order: -1; 145 | } 146 | -------------------------------------------------------------------------------- /public/jquery.ui.touch-punch.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * jQuery UI Touch Punch 0.2.3 3 | * 4 | * Copyright 2011–2014, Dave Furfero 5 | * Dual licensed under the MIT or GPL Version 2 licenses. 6 | * 7 | * Depends: 8 | * jquery.ui.widget.js 9 | * jquery.ui.mouse.js 10 | */ 11 | (function ($) { 12 | 13 | // Detect touch support 14 | $.support.touch = 'ontouchend' in document; 15 | 16 | // Ignore browsers without touch support 17 | if (!$.support.touch) { 18 | return; 19 | } 20 | 21 | var mouseProto = $.ui.mouse.prototype, 22 | _mouseInit = mouseProto._mouseInit, 23 | _mouseDestroy = mouseProto._mouseDestroy, 24 | touchHandled; 25 | 26 | /** 27 | * Simulate a mouse event based on a corresponding touch event 28 | * @param {Object} event A touch event 29 | * @param {String} simulatedType The corresponding mouse event 30 | */ 31 | function simulateMouseEvent (event, simulatedType) { 32 | 33 | // Ignore multi-touch events 34 | if (event.originalEvent.touches.length > 1) { 35 | return; 36 | } 37 | 38 | event.preventDefault(); 39 | 40 | var touch = event.originalEvent.changedTouches[0], 41 | simulatedEvent = document.createEvent('MouseEvents'); 42 | 43 | // Initialize the simulated mouse event using the touch event's coordinates 44 | simulatedEvent.initMouseEvent( 45 | simulatedType, // type 46 | true, // bubbles 47 | true, // cancelable 48 | window, // view 49 | 1, // detail 50 | touch.screenX, // screenX 51 | touch.screenY, // screenY 52 | touch.clientX, // clientX 53 | touch.clientY, // clientY 54 | false, // ctrlKey 55 | false, // altKey 56 | false, // shiftKey 57 | false, // metaKey 58 | 0, // button 59 | null // relatedTarget 60 | ); 61 | 62 | // Dispatch the simulated event to the target element 63 | event.target.dispatchEvent(simulatedEvent); 64 | } 65 | 66 | /** 67 | * Handle the jQuery UI widget's touchstart events 68 | * @param {Object} event The widget element's touchstart event 69 | */ 70 | mouseProto._touchStart = function (event) { 71 | 72 | var self = this; 73 | 74 | // Ignore the event if another widget is already being handled 75 | if (touchHandled || !self._mouseCapture(event.originalEvent.changedTouches[0])) { 76 | return; 77 | } 78 | 79 | // Set the flag to prevent other widgets from inheriting the touch event 80 | touchHandled = true; 81 | 82 | // Track movement to determine if interaction was a click 83 | self._touchMoved = false; 84 | 85 | // Simulate the mouseover event 86 | simulateMouseEvent(event, 'mouseover'); 87 | 88 | // Simulate the mousemove event 89 | simulateMouseEvent(event, 'mousemove'); 90 | 91 | // Simulate the mousedown event 92 | simulateMouseEvent(event, 'mousedown'); 93 | }; 94 | 95 | /** 96 | * Handle the jQuery UI widget's touchmove events 97 | * @param {Object} event The document's touchmove event 98 | */ 99 | mouseProto._touchMove = function (event) { 100 | 101 | // Ignore event if not handled 102 | if (!touchHandled) { 103 | return; 104 | } 105 | 106 | // Interaction was not a click 107 | this._touchMoved = true; 108 | 109 | // Simulate the mousemove event 110 | simulateMouseEvent(event, 'mousemove'); 111 | }; 112 | 113 | /** 114 | * Handle the jQuery UI widget's touchend events 115 | * @param {Object} event The document's touchend event 116 | */ 117 | mouseProto._touchEnd = function (event) { 118 | 119 | // Ignore event if not handled 120 | if (!touchHandled) { 121 | return; 122 | } 123 | 124 | // Simulate the mouseup event 125 | simulateMouseEvent(event, 'mouseup'); 126 | 127 | // Simulate the mouseout event 128 | simulateMouseEvent(event, 'mouseout'); 129 | 130 | // If the touch interaction did not move, it should trigger a click 131 | if (!this._touchMoved) { 132 | 133 | // Simulate the click event 134 | simulateMouseEvent(event, 'click'); 135 | } 136 | 137 | // Unset the flag to allow other widgets to inherit the touch event 138 | touchHandled = false; 139 | }; 140 | 141 | /** 142 | * A duck punch of the $.ui.mouse _mouseInit method to support touch events. 143 | * This method extends the widget with bound touch event handlers that 144 | * translate touch events to mouse events and pass them to the widget's 145 | * original mouse event handling methods. 146 | */ 147 | mouseProto._mouseInit = function () { 148 | 149 | var self = this; 150 | 151 | // Delegate the touch handlers to the widget's element 152 | self.element.bind({ 153 | touchstart: $.proxy(self, '_touchStart'), 154 | touchmove: $.proxy(self, '_touchMove'), 155 | touchend: $.proxy(self, '_touchEnd') 156 | }); 157 | 158 | // Call the original $.ui.mouse init method 159 | _mouseInit.call(self); 160 | }; 161 | 162 | /** 163 | * Remove the touch event handlers 164 | */ 165 | mouseProto._mouseDestroy = function () { 166 | 167 | var self = this; 168 | 169 | // Delegate the touch handlers to the widget's element 170 | self.element.unbind({ 171 | touchstart: $.proxy(self, '_touchStart'), 172 | touchmove: $.proxy(self, '_touchMove'), 173 | touchend: $.proxy(self, '_touchEnd') 174 | }); 175 | 176 | // Call the original $.ui.mouse destroy method 177 | _mouseDestroy.call(self); 178 | }; 179 | 180 | })(jQuery); -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 17 | 18 | 19 | 20 | 21 | BELABOX 22 | 23 | 24 | 28 | 29 | 30 | 31 | 32 | 33 | 34 |
35 |
36 |
37 | 38 | 41 | 42 | 44 | 45 | 58 | 59 |
60 |
61 | 62 | 63 | 64 |
65 |
66 | 67 |
68 |
Initial BELABOX setup
69 |
70 | 71 |
72 |
73 | 74 |

75 |
76 | 77 |
78 | 79 |
80 |
81 | 82 | 83 |
84 |
85 | 86 |
87 |
88 | 89 |
90 |
91 | 94 | 95 |
96 |
97 | 98 |
99 |
100 |
101 | 102 |
103 |
104 |
105 | 106 | 107 | 108 |
109 | 110 | 111 | 112 |
113 | 114 | 167 | 168 |
169 | 170 |
171 | 172 |
173 |
174 |
176 | 180 |
181 | 182 |
183 |
184 |
185 | 186 |
187 |
188 | 189 | 190 |
191 |
192 | 193 | 194 |
195 |
196 |
197 | 198 |
199 |
200 |
201 | 202 |
203 |
204 |
205 | 206 | 209 |
210 |
211 |
212 |
213 | 214 |
215 |
217 | 221 |
222 | 223 |
224 |
225 |
226 | 227 | 228 |
229 | 233 |
234 | 235 | 236 |

Did you know? You could use a BELABOX Cloud relay for improved stream reliability and bitrate

237 |
238 |
239 | 240 | 241 |
242 |
243 | 244 |
245 | 246 |
247 | 248 |
249 |
250 |
251 |
252 |
253 | 254 |
255 |
256 |
257 | 258 |
259 |
260 |
261 |

Recommended latency: 1500-2500ms

262 |

Setting the latency too low will increase glitching and reduce the bitrate that can be sustained

263 |
264 |
265 |
266 |
267 | 268 |
269 |
271 | 275 |
276 | 277 |
278 |
279 | 280 |
281 | 282 |
283 | 284 |
285 | 286 | 287 |
288 |
289 | 290 | 293 | 296 |
297 | 298 | 302 | 303 | 307 | 308 |
309 |
310 |
311 | 312 |
313 |
315 | 319 |
320 | 321 |
322 |
323 | 324 |
325 |
326 | 329 |

330 |
331 | 332 |
333 | 334 | 335 |
336 |
337 |
338 |
339 | 340 |
341 |
342 | 345 |
346 | 347 |
348 | 349 | 350 |
351 |
352 |
353 |
354 | 355 |
356 |
357 |
358 | 359 | 362 |
363 |
364 | 365 |
366 |
367 |
368 | 369 | 372 | 373 | 376 | 379 | 382 |
383 | 384 |
385 |
386 |
387 | 388 |
389 |
390 | 391 |
392 | 393 | Theme: 394 | 399 | 400 | 401 | 402 | Slider locks: 403 | 408 | 409 | 410 | 411 | Layout: 412 | 416 | 417 |
418 |
419 | 420 | 421 | 422 | 423 | 424 | 425 | 426 | 427 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | Preamble 9 | 10 | The GNU General Public License is a free, copyleft license for 11 | software and other kinds of works. 12 | 13 | The licenses for most software and other practical works are designed 14 | to take away your freedom to share and change the works. By contrast, 15 | the GNU General Public License is intended to guarantee your freedom to 16 | share and change all versions of a program--to make sure it remains free 17 | software for all its users. We, the Free Software Foundation, use the 18 | GNU General Public License for most of our software; it applies also to 19 | any other work released this way by its authors. You can apply it to 20 | your programs, too. 21 | 22 | When we speak of free software, we are referring to freedom, not 23 | price. Our General Public Licenses are designed to make sure that you 24 | have the freedom to distribute copies of free software (and charge for 25 | them if you wish), that you receive source code or can get it if you 26 | want it, that you can change the software or use pieces of it in new 27 | free programs, and that you know you can do these things. 28 | 29 | To protect your rights, we need to prevent others from denying you 30 | these rights or asking you to surrender the rights. Therefore, you have 31 | certain responsibilities if you distribute copies of the software, or if 32 | you modify it: responsibilities to respect the freedom of others. 33 | 34 | For example, if you distribute copies of such a program, whether 35 | gratis or for a fee, you must pass on to the recipients the same 36 | freedoms that you received. You must make sure that they, too, receive 37 | or can get the source code. And you must show them these terms so they 38 | know their rights. 39 | 40 | Developers that use the GNU GPL protect your rights with two steps: 41 | (1) assert copyright on the software, and (2) offer you this License 42 | giving you legal permission to copy, distribute and/or modify it. 43 | 44 | For the developers' and authors' protection, the GPL clearly explains 45 | that there is no warranty for this free software. For both users' and 46 | authors' sake, the GPL requires that modified versions be marked as 47 | changed, so that their problems will not be attributed erroneously to 48 | authors of previous versions. 49 | 50 | Some devices are designed to deny users access to install or run 51 | modified versions of the software inside them, although the manufacturer 52 | can do so. This is fundamentally incompatible with the aim of 53 | protecting users' freedom to change the software. The systematic 54 | pattern of such abuse occurs in the area of products for individuals to 55 | use, which is precisely where it is most unacceptable. Therefore, we 56 | have designed this version of the GPL to prohibit the practice for those 57 | products. If such problems arise substantially in other domains, we 58 | stand ready to extend this provision to those domains in future versions 59 | of the GPL, as needed to protect the freedom of users. 60 | 61 | Finally, every program is threatened constantly by software patents. 62 | States should not allow patents to restrict development and use of 63 | software on general-purpose computers, but in those that do, we wish to 64 | avoid the special danger that patents applied to a free program could 65 | make it effectively proprietary. To prevent this, the GPL assures that 66 | patents cannot be used to render the program non-free. 67 | 68 | The precise terms and conditions for copying, distribution and 69 | modification follow. 70 | 71 | TERMS AND CONDITIONS 72 | 73 | 0. Definitions. 74 | 75 | "This License" refers to version 3 of the GNU General Public License. 76 | 77 | "Copyright" also means copyright-like laws that apply to other kinds of 78 | works, such as semiconductor masks. 79 | 80 | "The Program" refers to any copyrightable work licensed under this 81 | License. Each licensee is addressed as "you". "Licensees" and 82 | "recipients" may be individuals or organizations. 83 | 84 | To "modify" a work means to copy from or adapt all or part of the work 85 | in a fashion requiring copyright permission, other than the making of an 86 | exact copy. The resulting work is called a "modified version" of the 87 | earlier work or a work "based on" the earlier work. 88 | 89 | A "covered work" means either the unmodified Program or a work based 90 | on the Program. 91 | 92 | To "propagate" a work means to do anything with it that, without 93 | permission, would make you directly or secondarily liable for 94 | infringement under applicable copyright law, except executing it on a 95 | computer or modifying a private copy. Propagation includes copying, 96 | distribution (with or without modification), making available to the 97 | public, and in some countries other activities as well. 98 | 99 | To "convey" a work means any kind of propagation that enables other 100 | parties to make or receive copies. Mere interaction with a user through 101 | a computer network, with no transfer of a copy, is not conveying. 102 | 103 | An interactive user interface displays "Appropriate Legal Notices" 104 | to the extent that it includes a convenient and prominently visible 105 | feature that (1) displays an appropriate copyright notice, and (2) 106 | tells the user that there is no warranty for the work (except to the 107 | extent that warranties are provided), that licensees may convey the 108 | work under this License, and how to view a copy of this License. If 109 | the interface presents a list of user commands or options, such as a 110 | menu, a prominent item in the list meets this criterion. 111 | 112 | 1. Source Code. 113 | 114 | The "source code" for a work means the preferred form of the work 115 | for making modifications to it. "Object code" means any non-source 116 | form of a work. 117 | 118 | A "Standard Interface" means an interface that either is an official 119 | standard defined by a recognized standards body, or, in the case of 120 | interfaces specified for a particular programming language, one that 121 | is widely used among developers working in that language. 122 | 123 | The "System Libraries" of an executable work include anything, other 124 | than the work as a whole, that (a) is included in the normal form of 125 | packaging a Major Component, but which is not part of that Major 126 | Component, and (b) serves only to enable use of the work with that 127 | Major Component, or to implement a Standard Interface for which an 128 | implementation is available to the public in source code form. A 129 | "Major Component", in this context, means a major essential component 130 | (kernel, window system, and so on) of the specific operating system 131 | (if any) on which the executable work runs, or a compiler used to 132 | produce the work, or an object code interpreter used to run it. 133 | 134 | The "Corresponding Source" for a work in object code form means all 135 | the source code needed to generate, install, and (for an executable 136 | work) run the object code and to modify the work, including scripts to 137 | control those activities. However, it does not include the work's 138 | System Libraries, or general-purpose tools or generally available free 139 | programs which are used unmodified in performing those activities but 140 | which are not part of the work. For example, Corresponding Source 141 | includes interface definition files associated with source files for 142 | the work, and the source code for shared libraries and dynamically 143 | linked subprograms that the work is specifically designed to require, 144 | such as by intimate data communication or control flow between those 145 | subprograms and other parts of the work. 146 | 147 | The Corresponding Source need not include anything that users 148 | can regenerate automatically from other parts of the Corresponding 149 | Source. 150 | 151 | The Corresponding Source for a work in source code form is that 152 | same work. 153 | 154 | 2. Basic Permissions. 155 | 156 | All rights granted under this License are granted for the term of 157 | copyright on the Program, and are irrevocable provided the stated 158 | conditions are met. This License explicitly affirms your unlimited 159 | permission to run the unmodified Program. The output from running a 160 | covered work is covered by this License only if the output, given its 161 | content, constitutes a covered work. This License acknowledges your 162 | rights of fair use or other equivalent, as provided by copyright law. 163 | 164 | You may make, run and propagate covered works that you do not 165 | convey, without conditions so long as your license otherwise remains 166 | in force. You may convey covered works to others for the sole purpose 167 | of having them make modifications exclusively for you, or provide you 168 | with facilities for running those works, provided that you comply with 169 | the terms of this License in conveying all material for which you do 170 | not control copyright. Those thus making or running the covered works 171 | for you must do so exclusively on your behalf, under your direction 172 | and control, on terms that prohibit them from making any copies of 173 | your copyrighted material outside their relationship with you. 174 | 175 | Conveying under any other circumstances is permitted solely under 176 | the conditions stated below. Sublicensing is not allowed; section 10 177 | makes it unnecessary. 178 | 179 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 180 | 181 | No covered work shall be deemed part of an effective technological 182 | measure under any applicable law fulfilling obligations under article 183 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 184 | similar laws prohibiting or restricting circumvention of such 185 | measures. 186 | 187 | When you convey a covered work, you waive any legal power to forbid 188 | circumvention of technological measures to the extent such circumvention 189 | is effected by exercising rights under this License with respect to 190 | the covered work, and you disclaim any intention to limit operation or 191 | modification of the work as a means of enforcing, against the work's 192 | users, your or third parties' legal rights to forbid circumvention of 193 | technological measures. 194 | 195 | 4. Conveying Verbatim Copies. 196 | 197 | You may convey verbatim copies of the Program's source code as you 198 | receive it, in any medium, provided that you conspicuously and 199 | appropriately publish on each copy an appropriate copyright notice; 200 | keep intact all notices stating that this License and any 201 | non-permissive terms added in accord with section 7 apply to the code; 202 | keep intact all notices of the absence of any warranty; and give all 203 | recipients a copy of this License along with the Program. 204 | 205 | You may charge any price or no price for each copy that you convey, 206 | and you may offer support or warranty protection for a fee. 207 | 208 | 5. Conveying Modified Source Versions. 209 | 210 | You may convey a work based on the Program, or the modifications to 211 | produce it from the Program, in the form of source code under the 212 | terms of section 4, provided that you also meet all of these conditions: 213 | 214 | a) The work must carry prominent notices stating that you modified 215 | it, and giving a relevant date. 216 | 217 | b) The work must carry prominent notices stating that it is 218 | released under this License and any conditions added under section 219 | 7. This requirement modifies the requirement in section 4 to 220 | "keep intact all notices". 221 | 222 | c) You must license the entire work, as a whole, under this 223 | License to anyone who comes into possession of a copy. This 224 | License will therefore apply, along with any applicable section 7 225 | additional terms, to the whole of the work, and all its parts, 226 | regardless of how they are packaged. This License gives no 227 | permission to license the work in any other way, but it does not 228 | invalidate such permission if you have separately received it. 229 | 230 | d) If the work has interactive user interfaces, each must display 231 | Appropriate Legal Notices; however, if the Program has interactive 232 | interfaces that do not display Appropriate Legal Notices, your 233 | work need not make them do so. 234 | 235 | A compilation of a covered work with other separate and independent 236 | works, which are not by their nature extensions of the covered work, 237 | and which are not combined with it such as to form a larger program, 238 | in or on a volume of a storage or distribution medium, is called an 239 | "aggregate" if the compilation and its resulting copyright are not 240 | used to limit the access or legal rights of the compilation's users 241 | beyond what the individual works permit. Inclusion of a covered work 242 | in an aggregate does not cause this License to apply to the other 243 | parts of the aggregate. 244 | 245 | 6. Conveying Non-Source Forms. 246 | 247 | You may convey a covered work in object code form under the terms 248 | of sections 4 and 5, provided that you also convey the 249 | machine-readable Corresponding Source under the terms of this License, 250 | in one of these ways: 251 | 252 | a) Convey the object code in, or embodied in, a physical product 253 | (including a physical distribution medium), accompanied by the 254 | Corresponding Source fixed on a durable physical medium 255 | customarily used for software interchange. 256 | 257 | b) Convey the object code in, or embodied in, a physical product 258 | (including a physical distribution medium), accompanied by a 259 | written offer, valid for at least three years and valid for as 260 | long as you offer spare parts or customer support for that product 261 | model, to give anyone who possesses the object code either (1) a 262 | copy of the Corresponding Source for all the software in the 263 | product that is covered by this License, on a durable physical 264 | medium customarily used for software interchange, for a price no 265 | more than your reasonable cost of physically performing this 266 | conveying of source, or (2) access to copy the 267 | Corresponding Source from a network server at no charge. 268 | 269 | c) Convey individual copies of the object code with a copy of the 270 | written offer to provide the Corresponding Source. This 271 | alternative is allowed only occasionally and noncommercially, and 272 | only if you received the object code with such an offer, in accord 273 | with subsection 6b. 274 | 275 | d) Convey the object code by offering access from a designated 276 | place (gratis or for a charge), and offer equivalent access to the 277 | Corresponding Source in the same way through the same place at no 278 | further charge. You need not require recipients to copy the 279 | Corresponding Source along with the object code. If the place to 280 | copy the object code is a network server, the Corresponding Source 281 | may be on a different server (operated by you or a third party) 282 | that supports equivalent copying facilities, provided you maintain 283 | clear directions next to the object code saying where to find the 284 | Corresponding Source. Regardless of what server hosts the 285 | Corresponding Source, you remain obligated to ensure that it is 286 | available for as long as needed to satisfy these requirements. 287 | 288 | e) Convey the object code using peer-to-peer transmission, provided 289 | you inform other peers where the object code and Corresponding 290 | Source of the work are being offered to the general public at no 291 | charge under subsection 6d. 292 | 293 | A separable portion of the object code, whose source code is excluded 294 | from the Corresponding Source as a System Library, need not be 295 | included in conveying the object code work. 296 | 297 | A "User Product" is either (1) a "consumer product", which means any 298 | tangible personal property which is normally used for personal, family, 299 | or household purposes, or (2) anything designed or sold for incorporation 300 | into a dwelling. In determining whether a product is a consumer product, 301 | doubtful cases shall be resolved in favor of coverage. For a particular 302 | product received by a particular user, "normally used" refers to a 303 | typical or common use of that class of product, regardless of the status 304 | of the particular user or of the way in which the particular user 305 | actually uses, or expects or is expected to use, the product. A product 306 | is a consumer product regardless of whether the product has substantial 307 | commercial, industrial or non-consumer uses, unless such uses represent 308 | the only significant mode of use of the product. 309 | 310 | "Installation Information" for a User Product means any methods, 311 | procedures, authorization keys, or other information required to install 312 | and execute modified versions of a covered work in that User Product from 313 | a modified version of its Corresponding Source. The information must 314 | suffice to ensure that the continued functioning of the modified object 315 | code is in no case prevented or interfered with solely because 316 | modification has been made. 317 | 318 | If you convey an object code work under this section in, or with, or 319 | specifically for use in, a User Product, and the conveying occurs as 320 | part of a transaction in which the right of possession and use of the 321 | User Product is transferred to the recipient in perpetuity or for a 322 | fixed term (regardless of how the transaction is characterized), the 323 | Corresponding Source conveyed under this section must be accompanied 324 | by the Installation Information. But this requirement does not apply 325 | if neither you nor any third party retains the ability to install 326 | modified object code on the User Product (for example, the work has 327 | been installed in ROM). 328 | 329 | The requirement to provide Installation Information does not include a 330 | requirement to continue to provide support service, warranty, or updates 331 | for a work that has been modified or installed by the recipient, or for 332 | the User Product in which it has been modified or installed. Access to a 333 | network may be denied when the modification itself materially and 334 | adversely affects the operation of the network or violates the rules and 335 | protocols for communication across the network. 336 | 337 | Corresponding Source conveyed, and Installation Information provided, 338 | in accord with this section must be in a format that is publicly 339 | documented (and with an implementation available to the public in 340 | source code form), and must require no special password or key for 341 | unpacking, reading or copying. 342 | 343 | 7. Additional Terms. 344 | 345 | "Additional permissions" are terms that supplement the terms of this 346 | License by making exceptions from one or more of its conditions. 347 | Additional permissions that are applicable to the entire Program shall 348 | be treated as though they were included in this License, to the extent 349 | that they are valid under applicable law. If additional permissions 350 | apply only to part of the Program, that part may be used separately 351 | under those permissions, but the entire Program remains governed by 352 | this License without regard to the additional permissions. 353 | 354 | When you convey a copy of a covered work, you may at your option 355 | remove any additional permissions from that copy, or from any part of 356 | it. (Additional permissions may be written to require their own 357 | removal in certain cases when you modify the work.) You may place 358 | additional permissions on material, added by you to a covered work, 359 | for which you have or can give appropriate copyright permission. 360 | 361 | Notwithstanding any other provision of this License, for material you 362 | add to a covered work, you may (if authorized by the copyright holders of 363 | that material) supplement the terms of this License with terms: 364 | 365 | a) Disclaiming warranty or limiting liability differently from the 366 | terms of sections 15 and 16 of this License; or 367 | 368 | b) Requiring preservation of specified reasonable legal notices or 369 | author attributions in that material or in the Appropriate Legal 370 | Notices displayed by works containing it; or 371 | 372 | c) Prohibiting misrepresentation of the origin of that material, or 373 | requiring that modified versions of such material be marked in 374 | reasonable ways as different from the original version; or 375 | 376 | d) Limiting the use for publicity purposes of names of licensors or 377 | authors of the material; or 378 | 379 | e) Declining to grant rights under trademark law for use of some 380 | trade names, trademarks, or service marks; or 381 | 382 | f) Requiring indemnification of licensors and authors of that 383 | material by anyone who conveys the material (or modified versions of 384 | it) with contractual assumptions of liability to the recipient, for 385 | any liability that these contractual assumptions directly impose on 386 | those licensors and authors. 387 | 388 | All other non-permissive additional terms are considered "further 389 | restrictions" within the meaning of section 10. If the Program as you 390 | received it, or any part of it, contains a notice stating that it is 391 | governed by this License along with a term that is a further 392 | restriction, you may remove that term. If a license document contains 393 | a further restriction but permits relicensing or conveying under this 394 | License, you may add to a covered work material governed by the terms 395 | of that license document, provided that the further restriction does 396 | not survive such relicensing or conveying. 397 | 398 | If you add terms to a covered work in accord with this section, you 399 | must place, in the relevant source files, a statement of the 400 | additional terms that apply to those files, or a notice indicating 401 | where to find the applicable terms. 402 | 403 | Additional terms, permissive or non-permissive, may be stated in the 404 | form of a separately written license, or stated as exceptions; 405 | the above requirements apply either way. 406 | 407 | 8. Termination. 408 | 409 | You may not propagate or modify a covered work except as expressly 410 | provided under this License. Any attempt otherwise to propagate or 411 | modify it is void, and will automatically terminate your rights under 412 | this License (including any patent licenses granted under the third 413 | paragraph of section 11). 414 | 415 | However, if you cease all violation of this License, then your 416 | license from a particular copyright holder is reinstated (a) 417 | provisionally, unless and until the copyright holder explicitly and 418 | finally terminates your license, and (b) permanently, if the copyright 419 | holder fails to notify you of the violation by some reasonable means 420 | prior to 60 days after the cessation. 421 | 422 | Moreover, your license from a particular copyright holder is 423 | reinstated permanently if the copyright holder notifies you of the 424 | violation by some reasonable means, this is the first time you have 425 | received notice of violation of this License (for any work) from that 426 | copyright holder, and you cure the violation prior to 30 days after 427 | your receipt of the notice. 428 | 429 | Termination of your rights under this section does not terminate the 430 | licenses of parties who have received copies or rights from you under 431 | this License. If your rights have been terminated and not permanently 432 | reinstated, you do not qualify to receive new licenses for the same 433 | material under section 10. 434 | 435 | 9. Acceptance Not Required for Having Copies. 436 | 437 | You are not required to accept this License in order to receive or 438 | run a copy of the Program. Ancillary propagation of a covered work 439 | occurring solely as a consequence of using peer-to-peer transmission 440 | to receive a copy likewise does not require acceptance. However, 441 | nothing other than this License grants you permission to propagate or 442 | modify any covered work. These actions infringe copyright if you do 443 | not accept this License. Therefore, by modifying or propagating a 444 | covered work, you indicate your acceptance of this License to do so. 445 | 446 | 10. Automatic Licensing of Downstream Recipients. 447 | 448 | Each time you convey a covered work, the recipient automatically 449 | receives a license from the original licensors, to run, modify and 450 | propagate that work, subject to this License. You are not responsible 451 | for enforcing compliance by third parties with this License. 452 | 453 | An "entity transaction" is a transaction transferring control of an 454 | organization, or substantially all assets of one, or subdividing an 455 | organization, or merging organizations. If propagation of a covered 456 | work results from an entity transaction, each party to that 457 | transaction who receives a copy of the work also receives whatever 458 | licenses to the work the party's predecessor in interest had or could 459 | give under the previous paragraph, plus a right to possession of the 460 | Corresponding Source of the work from the predecessor in interest, if 461 | the predecessor has it or can get it with reasonable efforts. 462 | 463 | You may not impose any further restrictions on the exercise of the 464 | rights granted or affirmed under this License. For example, you may 465 | not impose a license fee, royalty, or other charge for exercise of 466 | rights granted under this License, and you may not initiate litigation 467 | (including a cross-claim or counterclaim in a lawsuit) alleging that 468 | any patent claim is infringed by making, using, selling, offering for 469 | sale, or importing the Program or any portion of it. 470 | 471 | 11. Patents. 472 | 473 | A "contributor" is a copyright holder who authorizes use under this 474 | License of the Program or a work on which the Program is based. The 475 | work thus licensed is called the contributor's "contributor version". 476 | 477 | A contributor's "essential patent claims" are all patent claims 478 | owned or controlled by the contributor, whether already acquired or 479 | hereafter acquired, that would be infringed by some manner, permitted 480 | by this License, of making, using, or selling its contributor version, 481 | but do not include claims that would be infringed only as a 482 | consequence of further modification of the contributor version. For 483 | purposes of this definition, "control" includes the right to grant 484 | patent sublicenses in a manner consistent with the requirements of 485 | this License. 486 | 487 | Each contributor grants you a non-exclusive, worldwide, royalty-free 488 | patent license under the contributor's essential patent claims, to 489 | make, use, sell, offer for sale, import and otherwise run, modify and 490 | propagate the contents of its contributor version. 491 | 492 | In the following three paragraphs, a "patent license" is any express 493 | agreement or commitment, however denominated, not to enforce a patent 494 | (such as an express permission to practice a patent or covenant not to 495 | sue for patent infringement). To "grant" such a patent license to a 496 | party means to make such an agreement or commitment not to enforce a 497 | patent against the party. 498 | 499 | If you convey a covered work, knowingly relying on a patent license, 500 | and the Corresponding Source of the work is not available for anyone 501 | to copy, free of charge and under the terms of this License, through a 502 | publicly available network server or other readily accessible means, 503 | then you must either (1) cause the Corresponding Source to be so 504 | available, or (2) arrange to deprive yourself of the benefit of the 505 | patent license for this particular work, or (3) arrange, in a manner 506 | consistent with the requirements of this License, to extend the patent 507 | license to downstream recipients. "Knowingly relying" means you have 508 | actual knowledge that, but for the patent license, your conveying the 509 | covered work in a country, or your recipient's use of the covered work 510 | in a country, would infringe one or more identifiable patents in that 511 | country that you have reason to believe are valid. 512 | 513 | If, pursuant to or in connection with a single transaction or 514 | arrangement, you convey, or propagate by procuring conveyance of, a 515 | covered work, and grant a patent license to some of the parties 516 | receiving the covered work authorizing them to use, propagate, modify 517 | or convey a specific copy of the covered work, then the patent license 518 | you grant is automatically extended to all recipients of the covered 519 | work and works based on it. 520 | 521 | A patent license is "discriminatory" if it does not include within 522 | the scope of its coverage, prohibits the exercise of, or is 523 | conditioned on the non-exercise of one or more of the rights that are 524 | specifically granted under this License. You may not convey a covered 525 | work if you are a party to an arrangement with a third party that is 526 | in the business of distributing software, under which you make payment 527 | to the third party based on the extent of your activity of conveying 528 | the work, and under which the third party grants, to any of the 529 | parties who would receive the covered work from you, a discriminatory 530 | patent license (a) in connection with copies of the covered work 531 | conveyed by you (or copies made from those copies), or (b) primarily 532 | for and in connection with specific products or compilations that 533 | contain the covered work, unless you entered into that arrangement, 534 | or that patent license was granted, prior to 28 March 2007. 535 | 536 | Nothing in this License shall be construed as excluding or limiting 537 | any implied license or other defenses to infringement that may 538 | otherwise be available to you under applicable patent law. 539 | 540 | 12. No Surrender of Others' Freedom. 541 | 542 | If conditions are imposed on you (whether by court order, agreement or 543 | otherwise) that contradict the conditions of this License, they do not 544 | excuse you from the conditions of this License. If you cannot convey a 545 | covered work so as to satisfy simultaneously your obligations under this 546 | License and any other pertinent obligations, then as a consequence you may 547 | not convey it at all. For example, if you agree to terms that obligate you 548 | to collect a royalty for further conveying from those to whom you convey 549 | the Program, the only way you could satisfy both those terms and this 550 | License would be to refrain entirely from conveying the Program. 551 | 552 | 13. Use with the GNU Affero General Public License. 553 | 554 | Notwithstanding any other provision of this License, you have 555 | permission to link or combine any covered work with a work licensed 556 | under version 3 of the GNU Affero General Public License into a single 557 | combined work, and to convey the resulting work. The terms of this 558 | License will continue to apply to the part which is the covered work, 559 | but the special requirements of the GNU Affero General Public License, 560 | section 13, concerning interaction through a network will apply to the 561 | combination as such. 562 | 563 | 14. Revised Versions of this License. 564 | 565 | The Free Software Foundation may publish revised and/or new versions of 566 | the GNU General Public License from time to time. Such new versions will 567 | be similar in spirit to the present version, but may differ in detail to 568 | address new problems or concerns. 569 | 570 | Each version is given a distinguishing version number. If the 571 | Program specifies that a certain numbered version of the GNU General 572 | Public License "or any later version" applies to it, you have the 573 | option of following the terms and conditions either of that numbered 574 | version or of any later version published by the Free Software 575 | Foundation. If the Program does not specify a version number of the 576 | GNU General Public License, you may choose any version ever published 577 | by the Free Software Foundation. 578 | 579 | If the Program specifies that a proxy can decide which future 580 | versions of the GNU General Public License can be used, that proxy's 581 | public statement of acceptance of a version permanently authorizes you 582 | to choose that version for the Program. 583 | 584 | Later license versions may give you additional or different 585 | permissions. However, no additional obligations are imposed on any 586 | author or copyright holder as a result of your choosing to follow a 587 | later version. 588 | 589 | 15. Disclaimer of Warranty. 590 | 591 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 592 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 593 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 594 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 595 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 596 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 597 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 598 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 599 | 600 | 16. Limitation of Liability. 601 | 602 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 603 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 604 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 605 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 606 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 607 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 608 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 609 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 610 | SUCH DAMAGES. 611 | 612 | 17. Interpretation of Sections 15 and 16. 613 | 614 | If the disclaimer of warranty and limitation of liability provided 615 | above cannot be given local legal effect according to their terms, 616 | reviewing courts shall apply local law that most closely approximates 617 | an absolute waiver of all civil liability in connection with the 618 | Program, unless a warranty or assumption of liability accompanies a 619 | copy of the Program in return for a fee. 620 | 621 | END OF TERMS AND CONDITIONS 622 | 623 | How to Apply These Terms to Your New Programs 624 | 625 | If you develop a new program, and you want it to be of the greatest 626 | possible use to the public, the best way to achieve this is to make it 627 | free software which everyone can redistribute and change under these terms. 628 | 629 | To do so, attach the following notices to the program. It is safest 630 | to attach them to the start of each source file to most effectively 631 | state the exclusion of warranty; and each file should have at least 632 | the "copyright" line and a pointer to where the full notice is found. 633 | 634 | 635 | Copyright (C) 636 | 637 | This program is free software: you can redistribute it and/or modify 638 | it under the terms of the GNU General Public License as published by 639 | the Free Software Foundation, either version 3 of the License, or 640 | (at your option) any later version. 641 | 642 | This program is distributed in the hope that it will be useful, 643 | but WITHOUT ANY WARRANTY; without even the implied warranty of 644 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 645 | GNU General Public License for more details. 646 | 647 | You should have received a copy of the GNU General Public License 648 | along with this program. If not, see . 649 | 650 | Also add information on how to contact you by electronic and paper mail. 651 | 652 | If the program does terminal interaction, make it output a short 653 | notice like this when it starts in an interactive mode: 654 | 655 | Copyright (C) 656 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 657 | This is free software, and you are welcome to redistribute it 658 | under certain conditions; type `show c' for details. 659 | 660 | The hypothetical commands `show w' and `show c' should show the appropriate 661 | parts of the General Public License. Of course, your program's commands 662 | might be different; for a GUI interface, you would use an "about box". 663 | 664 | You should also get your employer (if you work as a programmer) or school, 665 | if any, to sign a "copyright disclaimer" for the program, if necessary. 666 | For more information on this, and how to apply and follow the GNU GPL, see 667 | . 668 | 669 | The GNU General Public License does not permit incorporating your program 670 | into proprietary programs. If your program is a subroutine library, you 671 | may consider it more useful to permit linking proprietary applications with 672 | the library. If this is what you want to do, use the GNU Lesser General 673 | Public License instead of this License. But first, please read 674 | . 675 | -------------------------------------------------------------------------------- /public/jquery-ui-1.12.1.css: -------------------------------------------------------------------------------- 1 | /*! jQuery UI - v1.12.1 - 2016-09-14 2 | * http://jqueryui.com 3 | * Includes: core.css, accordion.css, autocomplete.css, menu.css, button.css, controlgroup.css, checkboxradio.css, datepicker.css, dialog.css, draggable.css, resizable.css, progressbar.css, selectable.css, selectmenu.css, slider.css, sortable.css, spinner.css, tabs.css, tooltip.css, theme.css 4 | * To view and modify this theme, visit http://jqueryui.com/themeroller/?bgShadowXPos=&bgOverlayXPos=&bgErrorXPos=&bgHighlightXPos=&bgContentXPos=&bgHeaderXPos=&bgActiveXPos=&bgHoverXPos=&bgDefaultXPos=&bgShadowYPos=&bgOverlayYPos=&bgErrorYPos=&bgHighlightYPos=&bgContentYPos=&bgHeaderYPos=&bgActiveYPos=&bgHoverYPos=&bgDefaultYPos=&bgShadowRepeat=&bgOverlayRepeat=&bgErrorRepeat=&bgHighlightRepeat=&bgContentRepeat=&bgHeaderRepeat=&bgActiveRepeat=&bgHoverRepeat=&bgDefaultRepeat=&iconsHover=url(%22images%2Fui-icons_555555_256x240.png%22)&iconsHighlight=url(%22images%2Fui-icons_777620_256x240.png%22)&iconsHeader=url(%22images%2Fui-icons_444444_256x240.png%22)&iconsError=url(%22images%2Fui-icons_cc0000_256x240.png%22)&iconsDefault=url(%22images%2Fui-icons_777777_256x240.png%22)&iconsContent=url(%22images%2Fui-icons_444444_256x240.png%22)&iconsActive=url(%22images%2Fui-icons_ffffff_256x240.png%22)&bgImgUrlShadow=&bgImgUrlOverlay=&bgImgUrlHover=&bgImgUrlHighlight=&bgImgUrlHeader=&bgImgUrlError=&bgImgUrlDefault=&bgImgUrlContent=&bgImgUrlActive=&opacityFilterShadow=Alpha(Opacity%3D30)&opacityFilterOverlay=Alpha(Opacity%3D30)&opacityShadowPerc=30&opacityOverlayPerc=30&iconColorHover=%23555555&iconColorHighlight=%23777620&iconColorHeader=%23444444&iconColorError=%23cc0000&iconColorDefault=%23777777&iconColorContent=%23444444&iconColorActive=%23ffffff&bgImgOpacityShadow=0&bgImgOpacityOverlay=0&bgImgOpacityError=95&bgImgOpacityHighlight=55&bgImgOpacityContent=75&bgImgOpacityHeader=75&bgImgOpacityActive=65&bgImgOpacityHover=75&bgImgOpacityDefault=75&bgTextureShadow=flat&bgTextureOverlay=flat&bgTextureError=flat&bgTextureHighlight=flat&bgTextureContent=flat&bgTextureHeader=flat&bgTextureActive=flat&bgTextureHover=flat&bgTextureDefault=flat&cornerRadius=3px&fwDefault=normal&ffDefault=Arial%2CHelvetica%2Csans-serif&fsDefault=1em&cornerRadiusShadow=8px&thicknessShadow=5px&offsetLeftShadow=0px&offsetTopShadow=0px&opacityShadow=.3&bgColorShadow=%23666666&opacityOverlay=.3&bgColorOverlay=%23aaaaaa&fcError=%235f3f3f&borderColorError=%23f1a899&bgColorError=%23fddfdf&fcHighlight=%23777620&borderColorHighlight=%23dad55e&bgColorHighlight=%23fffa90&fcContent=%23333333&borderColorContent=%23dddddd&bgColorContent=%23ffffff&fcHeader=%23333333&borderColorHeader=%23dddddd&bgColorHeader=%23e9e9e9&fcActive=%23ffffff&borderColorActive=%23003eff&bgColorActive=%23007fff&fcHover=%232b2b2b&borderColorHover=%23cccccc&bgColorHover=%23ededed&fcDefault=%23454545&borderColorDefault=%23c5c5c5&bgColorDefault=%23f6f6f6 5 | * Copyright jQuery Foundation and other contributors; Licensed MIT */ 6 | 7 | /* Layout helpers 8 | ----------------------------------*/ 9 | .ui-helper-hidden { 10 | display: none; 11 | } 12 | .ui-helper-hidden-accessible { 13 | border: 0; 14 | clip: rect(0 0 0 0); 15 | height: 1px; 16 | margin: -1px; 17 | overflow: hidden; 18 | padding: 0; 19 | position: absolute; 20 | width: 1px; 21 | } 22 | .ui-helper-reset { 23 | margin: 0; 24 | padding: 0; 25 | border: 0; 26 | outline: 0; 27 | line-height: 1.3; 28 | text-decoration: none; 29 | font-size: 100%; 30 | list-style: none; 31 | } 32 | .ui-helper-clearfix:before, 33 | .ui-helper-clearfix:after { 34 | content: ""; 35 | display: table; 36 | border-collapse: collapse; 37 | } 38 | .ui-helper-clearfix:after { 39 | clear: both; 40 | } 41 | .ui-helper-zfix { 42 | width: 100%; 43 | height: 100%; 44 | top: 0; 45 | left: 0; 46 | position: absolute; 47 | opacity: 0; 48 | filter:Alpha(Opacity=0); /* support: IE8 */ 49 | } 50 | 51 | .ui-front { 52 | z-index: 100; 53 | } 54 | 55 | 56 | /* Interaction Cues 57 | ----------------------------------*/ 58 | .ui-state-disabled { 59 | cursor: default !important; 60 | pointer-events: none; 61 | } 62 | 63 | 64 | /* Icons 65 | ----------------------------------*/ 66 | .ui-icon { 67 | display: inline-block; 68 | vertical-align: middle; 69 | margin-top: -.25em; 70 | position: relative; 71 | text-indent: -99999px; 72 | overflow: hidden; 73 | background-repeat: no-repeat; 74 | } 75 | 76 | .ui-widget-icon-block { 77 | left: 50%; 78 | margin-left: -8px; 79 | display: block; 80 | } 81 | 82 | /* Misc visuals 83 | ----------------------------------*/ 84 | 85 | /* Overlays */ 86 | .ui-widget-overlay { 87 | position: fixed; 88 | top: 0; 89 | left: 0; 90 | width: 100%; 91 | height: 100%; 92 | } 93 | .ui-accordion .ui-accordion-header { 94 | display: block; 95 | cursor: pointer; 96 | position: relative; 97 | margin: 2px 0 0 0; 98 | padding: .5em .5em .5em .7em; 99 | font-size: 100%; 100 | } 101 | .ui-accordion .ui-accordion-content { 102 | padding: 1em 2.2em; 103 | border-top: 0; 104 | overflow: auto; 105 | } 106 | .ui-autocomplete { 107 | position: absolute; 108 | top: 0; 109 | left: 0; 110 | cursor: default; 111 | } 112 | .ui-menu { 113 | list-style: none; 114 | padding: 0; 115 | margin: 0; 116 | display: block; 117 | outline: 0; 118 | } 119 | .ui-menu .ui-menu { 120 | position: absolute; 121 | } 122 | .ui-menu .ui-menu-item { 123 | margin: 0; 124 | cursor: pointer; 125 | /* support: IE10, see #8844 */ 126 | list-style-image: url("data:image/gif;base64,R0lGODlhAQABAIAAAAAAAP///yH5BAEAAAAALAAAAAABAAEAAAIBRAA7"); 127 | } 128 | .ui-menu .ui-menu-item-wrapper { 129 | position: relative; 130 | padding: 3px 1em 3px .4em; 131 | } 132 | .ui-menu .ui-menu-divider { 133 | margin: 5px 0; 134 | height: 0; 135 | font-size: 0; 136 | line-height: 0; 137 | border-width: 1px 0 0 0; 138 | } 139 | .ui-menu .ui-state-focus, 140 | .ui-menu .ui-state-active { 141 | margin: -1px; 142 | } 143 | 144 | /* icon support */ 145 | .ui-menu-icons { 146 | position: relative; 147 | } 148 | .ui-menu-icons .ui-menu-item-wrapper { 149 | padding-left: 2em; 150 | } 151 | 152 | /* left-aligned */ 153 | .ui-menu .ui-icon { 154 | position: absolute; 155 | top: 0; 156 | bottom: 0; 157 | left: .2em; 158 | margin: auto 0; 159 | } 160 | 161 | /* right-aligned */ 162 | .ui-menu .ui-menu-icon { 163 | left: auto; 164 | right: 0; 165 | } 166 | .ui-button { 167 | padding: .4em 1em; 168 | display: inline-block; 169 | position: relative; 170 | line-height: normal; 171 | margin-right: .1em; 172 | cursor: pointer; 173 | vertical-align: middle; 174 | text-align: center; 175 | -webkit-user-select: none; 176 | -moz-user-select: none; 177 | -ms-user-select: none; 178 | user-select: none; 179 | 180 | /* Support: IE <= 11 */ 181 | overflow: visible; 182 | } 183 | 184 | .ui-button, 185 | .ui-button:link, 186 | .ui-button:visited, 187 | .ui-button:hover, 188 | .ui-button:active { 189 | text-decoration: none; 190 | } 191 | 192 | /* to make room for the icon, a width needs to be set here */ 193 | .ui-button-icon-only { 194 | width: 2em; 195 | box-sizing: border-box; 196 | text-indent: -9999px; 197 | white-space: nowrap; 198 | } 199 | 200 | /* no icon support for input elements */ 201 | input.ui-button.ui-button-icon-only { 202 | text-indent: 0; 203 | } 204 | 205 | /* button icon element(s) */ 206 | .ui-button-icon-only .ui-icon { 207 | position: absolute; 208 | top: 50%; 209 | left: 50%; 210 | margin-top: -8px; 211 | margin-left: -8px; 212 | } 213 | 214 | .ui-button.ui-icon-notext .ui-icon { 215 | padding: 0; 216 | width: 2.1em; 217 | height: 2.1em; 218 | text-indent: -9999px; 219 | white-space: nowrap; 220 | 221 | } 222 | 223 | input.ui-button.ui-icon-notext .ui-icon { 224 | width: auto; 225 | height: auto; 226 | text-indent: 0; 227 | white-space: normal; 228 | padding: .4em 1em; 229 | } 230 | 231 | /* workarounds */ 232 | /* Support: Firefox 5 - 40 */ 233 | input.ui-button::-moz-focus-inner, 234 | button.ui-button::-moz-focus-inner { 235 | border: 0; 236 | padding: 0; 237 | } 238 | .ui-controlgroup { 239 | vertical-align: middle; 240 | display: inline-block; 241 | } 242 | .ui-controlgroup > .ui-controlgroup-item { 243 | float: left; 244 | margin-left: 0; 245 | margin-right: 0; 246 | } 247 | .ui-controlgroup > .ui-controlgroup-item:focus, 248 | .ui-controlgroup > .ui-controlgroup-item.ui-visual-focus { 249 | z-index: 9999; 250 | } 251 | .ui-controlgroup-vertical > .ui-controlgroup-item { 252 | display: block; 253 | float: none; 254 | width: 100%; 255 | margin-top: 0; 256 | margin-bottom: 0; 257 | text-align: left; 258 | } 259 | .ui-controlgroup-vertical .ui-controlgroup-item { 260 | box-sizing: border-box; 261 | } 262 | .ui-controlgroup .ui-controlgroup-label { 263 | padding: .4em 1em; 264 | } 265 | .ui-controlgroup .ui-controlgroup-label span { 266 | font-size: 80%; 267 | } 268 | .ui-controlgroup-horizontal .ui-controlgroup-label + .ui-controlgroup-item { 269 | border-left: none; 270 | } 271 | .ui-controlgroup-vertical .ui-controlgroup-label + .ui-controlgroup-item { 272 | border-top: none; 273 | } 274 | .ui-controlgroup-horizontal .ui-controlgroup-label.ui-widget-content { 275 | border-right: none; 276 | } 277 | .ui-controlgroup-vertical .ui-controlgroup-label.ui-widget-content { 278 | border-bottom: none; 279 | } 280 | 281 | /* Spinner specific style fixes */ 282 | .ui-controlgroup-vertical .ui-spinner-input { 283 | 284 | /* Support: IE8 only, Android < 4.4 only */ 285 | width: 75%; 286 | width: calc( 100% - 2.4em ); 287 | } 288 | .ui-controlgroup-vertical .ui-spinner .ui-spinner-up { 289 | border-top-style: solid; 290 | } 291 | 292 | .ui-checkboxradio-label .ui-icon-background { 293 | box-shadow: inset 1px 1px 1px #ccc; 294 | border-radius: .12em; 295 | border: none; 296 | } 297 | .ui-checkboxradio-radio-label .ui-icon-background { 298 | width: 16px; 299 | height: 16px; 300 | border-radius: 1em; 301 | overflow: visible; 302 | border: none; 303 | } 304 | .ui-checkboxradio-radio-label.ui-checkboxradio-checked .ui-icon, 305 | .ui-checkboxradio-radio-label.ui-checkboxradio-checked:hover .ui-icon { 306 | background-image: none; 307 | width: 8px; 308 | height: 8px; 309 | border-width: 4px; 310 | border-style: solid; 311 | } 312 | .ui-checkboxradio-disabled { 313 | pointer-events: none; 314 | } 315 | .ui-datepicker { 316 | width: 17em; 317 | padding: .2em .2em 0; 318 | display: none; 319 | } 320 | .ui-datepicker .ui-datepicker-header { 321 | position: relative; 322 | padding: .2em 0; 323 | } 324 | .ui-datepicker .ui-datepicker-prev, 325 | .ui-datepicker .ui-datepicker-next { 326 | position: absolute; 327 | top: 2px; 328 | width: 1.8em; 329 | height: 1.8em; 330 | } 331 | .ui-datepicker .ui-datepicker-prev-hover, 332 | .ui-datepicker .ui-datepicker-next-hover { 333 | top: 1px; 334 | } 335 | .ui-datepicker .ui-datepicker-prev { 336 | left: 2px; 337 | } 338 | .ui-datepicker .ui-datepicker-next { 339 | right: 2px; 340 | } 341 | .ui-datepicker .ui-datepicker-prev-hover { 342 | left: 1px; 343 | } 344 | .ui-datepicker .ui-datepicker-next-hover { 345 | right: 1px; 346 | } 347 | .ui-datepicker .ui-datepicker-prev span, 348 | .ui-datepicker .ui-datepicker-next span { 349 | display: block; 350 | position: absolute; 351 | left: 50%; 352 | margin-left: -8px; 353 | top: 50%; 354 | margin-top: -8px; 355 | } 356 | .ui-datepicker .ui-datepicker-title { 357 | margin: 0 2.3em; 358 | line-height: 1.8em; 359 | text-align: center; 360 | } 361 | .ui-datepicker .ui-datepicker-title select { 362 | font-size: 1em; 363 | margin: 1px 0; 364 | } 365 | .ui-datepicker select.ui-datepicker-month, 366 | .ui-datepicker select.ui-datepicker-year { 367 | width: 45%; 368 | } 369 | .ui-datepicker table { 370 | width: 100%; 371 | font-size: .9em; 372 | border-collapse: collapse; 373 | margin: 0 0 .4em; 374 | } 375 | .ui-datepicker th { 376 | padding: .7em .3em; 377 | text-align: center; 378 | font-weight: bold; 379 | border: 0; 380 | } 381 | .ui-datepicker td { 382 | border: 0; 383 | padding: 1px; 384 | } 385 | .ui-datepicker td span, 386 | .ui-datepicker td a { 387 | display: block; 388 | padding: .2em; 389 | text-align: right; 390 | text-decoration: none; 391 | } 392 | .ui-datepicker .ui-datepicker-buttonpane { 393 | background-image: none; 394 | margin: .7em 0 0 0; 395 | padding: 0 .2em; 396 | border-left: 0; 397 | border-right: 0; 398 | border-bottom: 0; 399 | } 400 | .ui-datepicker .ui-datepicker-buttonpane button { 401 | float: right; 402 | margin: .5em .2em .4em; 403 | cursor: pointer; 404 | padding: .2em .6em .3em .6em; 405 | width: auto; 406 | overflow: visible; 407 | } 408 | .ui-datepicker .ui-datepicker-buttonpane button.ui-datepicker-current { 409 | float: left; 410 | } 411 | 412 | /* with multiple calendars */ 413 | .ui-datepicker.ui-datepicker-multi { 414 | width: auto; 415 | } 416 | .ui-datepicker-multi .ui-datepicker-group { 417 | float: left; 418 | } 419 | .ui-datepicker-multi .ui-datepicker-group table { 420 | width: 95%; 421 | margin: 0 auto .4em; 422 | } 423 | .ui-datepicker-multi-2 .ui-datepicker-group { 424 | width: 50%; 425 | } 426 | .ui-datepicker-multi-3 .ui-datepicker-group { 427 | width: 33.3%; 428 | } 429 | .ui-datepicker-multi-4 .ui-datepicker-group { 430 | width: 25%; 431 | } 432 | .ui-datepicker-multi .ui-datepicker-group-last .ui-datepicker-header, 433 | .ui-datepicker-multi .ui-datepicker-group-middle .ui-datepicker-header { 434 | border-left-width: 0; 435 | } 436 | .ui-datepicker-multi .ui-datepicker-buttonpane { 437 | clear: left; 438 | } 439 | .ui-datepicker-row-break { 440 | clear: both; 441 | width: 100%; 442 | font-size: 0; 443 | } 444 | 445 | /* RTL support */ 446 | .ui-datepicker-rtl { 447 | direction: rtl; 448 | } 449 | .ui-datepicker-rtl .ui-datepicker-prev { 450 | right: 2px; 451 | left: auto; 452 | } 453 | .ui-datepicker-rtl .ui-datepicker-next { 454 | left: 2px; 455 | right: auto; 456 | } 457 | .ui-datepicker-rtl .ui-datepicker-prev:hover { 458 | right: 1px; 459 | left: auto; 460 | } 461 | .ui-datepicker-rtl .ui-datepicker-next:hover { 462 | left: 1px; 463 | right: auto; 464 | } 465 | .ui-datepicker-rtl .ui-datepicker-buttonpane { 466 | clear: right; 467 | } 468 | .ui-datepicker-rtl .ui-datepicker-buttonpane button { 469 | float: left; 470 | } 471 | .ui-datepicker-rtl .ui-datepicker-buttonpane button.ui-datepicker-current, 472 | .ui-datepicker-rtl .ui-datepicker-group { 473 | float: right; 474 | } 475 | .ui-datepicker-rtl .ui-datepicker-group-last .ui-datepicker-header, 476 | .ui-datepicker-rtl .ui-datepicker-group-middle .ui-datepicker-header { 477 | border-right-width: 0; 478 | border-left-width: 1px; 479 | } 480 | 481 | /* Icons */ 482 | .ui-datepicker .ui-icon { 483 | display: block; 484 | text-indent: -99999px; 485 | overflow: hidden; 486 | background-repeat: no-repeat; 487 | left: .5em; 488 | top: .3em; 489 | } 490 | .ui-dialog { 491 | position: absolute; 492 | top: 0; 493 | left: 0; 494 | padding: .2em; 495 | outline: 0; 496 | } 497 | .ui-dialog .ui-dialog-titlebar { 498 | padding: .4em 1em; 499 | position: relative; 500 | } 501 | .ui-dialog .ui-dialog-title { 502 | float: left; 503 | margin: .1em 0; 504 | white-space: nowrap; 505 | width: 90%; 506 | overflow: hidden; 507 | text-overflow: ellipsis; 508 | } 509 | .ui-dialog .ui-dialog-titlebar-close { 510 | position: absolute; 511 | right: .3em; 512 | top: 50%; 513 | width: 20px; 514 | margin: -10px 0 0 0; 515 | padding: 1px; 516 | height: 20px; 517 | } 518 | .ui-dialog .ui-dialog-content { 519 | position: relative; 520 | border: 0; 521 | padding: .5em 1em; 522 | background: none; 523 | overflow: auto; 524 | } 525 | .ui-dialog .ui-dialog-buttonpane { 526 | text-align: left; 527 | border-width: 1px 0 0 0; 528 | background-image: none; 529 | margin-top: .5em; 530 | padding: .3em 1em .5em .4em; 531 | } 532 | .ui-dialog .ui-dialog-buttonpane .ui-dialog-buttonset { 533 | float: right; 534 | } 535 | .ui-dialog .ui-dialog-buttonpane button { 536 | margin: .5em .4em .5em 0; 537 | cursor: pointer; 538 | } 539 | .ui-dialog .ui-resizable-n { 540 | height: 2px; 541 | top: 0; 542 | } 543 | .ui-dialog .ui-resizable-e { 544 | width: 2px; 545 | right: 0; 546 | } 547 | .ui-dialog .ui-resizable-s { 548 | height: 2px; 549 | bottom: 0; 550 | } 551 | .ui-dialog .ui-resizable-w { 552 | width: 2px; 553 | left: 0; 554 | } 555 | .ui-dialog .ui-resizable-se, 556 | .ui-dialog .ui-resizable-sw, 557 | .ui-dialog .ui-resizable-ne, 558 | .ui-dialog .ui-resizable-nw { 559 | width: 7px; 560 | height: 7px; 561 | } 562 | .ui-dialog .ui-resizable-se { 563 | right: 0; 564 | bottom: 0; 565 | } 566 | .ui-dialog .ui-resizable-sw { 567 | left: 0; 568 | bottom: 0; 569 | } 570 | .ui-dialog .ui-resizable-ne { 571 | right: 0; 572 | top: 0; 573 | } 574 | .ui-dialog .ui-resizable-nw { 575 | left: 0; 576 | top: 0; 577 | } 578 | .ui-draggable .ui-dialog-titlebar { 579 | cursor: move; 580 | } 581 | .ui-draggable-handle { 582 | -ms-touch-action: none; 583 | touch-action: none; 584 | } 585 | .ui-resizable { 586 | position: relative; 587 | } 588 | .ui-resizable-handle { 589 | position: absolute; 590 | font-size: 0.1px; 591 | display: block; 592 | -ms-touch-action: none; 593 | touch-action: none; 594 | } 595 | .ui-resizable-disabled .ui-resizable-handle, 596 | .ui-resizable-autohide .ui-resizable-handle { 597 | display: none; 598 | } 599 | .ui-resizable-n { 600 | cursor: n-resize; 601 | height: 7px; 602 | width: 100%; 603 | top: -5px; 604 | left: 0; 605 | } 606 | .ui-resizable-s { 607 | cursor: s-resize; 608 | height: 7px; 609 | width: 100%; 610 | bottom: -5px; 611 | left: 0; 612 | } 613 | .ui-resizable-e { 614 | cursor: e-resize; 615 | width: 7px; 616 | right: -5px; 617 | top: 0; 618 | height: 100%; 619 | } 620 | .ui-resizable-w { 621 | cursor: w-resize; 622 | width: 7px; 623 | left: -5px; 624 | top: 0; 625 | height: 100%; 626 | } 627 | .ui-resizable-se { 628 | cursor: se-resize; 629 | width: 12px; 630 | height: 12px; 631 | right: 1px; 632 | bottom: 1px; 633 | } 634 | .ui-resizable-sw { 635 | cursor: sw-resize; 636 | width: 9px; 637 | height: 9px; 638 | left: -5px; 639 | bottom: -5px; 640 | } 641 | .ui-resizable-nw { 642 | cursor: nw-resize; 643 | width: 9px; 644 | height: 9px; 645 | left: -5px; 646 | top: -5px; 647 | } 648 | .ui-resizable-ne { 649 | cursor: ne-resize; 650 | width: 9px; 651 | height: 9px; 652 | right: -5px; 653 | top: -5px; 654 | } 655 | .ui-progressbar { 656 | height: 2em; 657 | text-align: left; 658 | overflow: hidden; 659 | } 660 | .ui-progressbar .ui-progressbar-value { 661 | margin: -1px; 662 | height: 100%; 663 | } 664 | .ui-progressbar .ui-progressbar-overlay { 665 | background: url("data:image/gif;base64,R0lGODlhKAAoAIABAAAAAP///yH/C05FVFNDQVBFMi4wAwEAAAAh+QQJAQABACwAAAAAKAAoAAACkYwNqXrdC52DS06a7MFZI+4FHBCKoDeWKXqymPqGqxvJrXZbMx7Ttc+w9XgU2FB3lOyQRWET2IFGiU9m1frDVpxZZc6bfHwv4c1YXP6k1Vdy292Fb6UkuvFtXpvWSzA+HycXJHUXiGYIiMg2R6W459gnWGfHNdjIqDWVqemH2ekpObkpOlppWUqZiqr6edqqWQAAIfkECQEAAQAsAAAAACgAKAAAApSMgZnGfaqcg1E2uuzDmmHUBR8Qil95hiPKqWn3aqtLsS18y7G1SzNeowWBENtQd+T1JktP05nzPTdJZlR6vUxNWWjV+vUWhWNkWFwxl9VpZRedYcflIOLafaa28XdsH/ynlcc1uPVDZxQIR0K25+cICCmoqCe5mGhZOfeYSUh5yJcJyrkZWWpaR8doJ2o4NYq62lAAACH5BAkBAAEALAAAAAAoACgAAAKVDI4Yy22ZnINRNqosw0Bv7i1gyHUkFj7oSaWlu3ovC8GxNso5fluz3qLVhBVeT/Lz7ZTHyxL5dDalQWPVOsQWtRnuwXaFTj9jVVh8pma9JjZ4zYSj5ZOyma7uuolffh+IR5aW97cHuBUXKGKXlKjn+DiHWMcYJah4N0lYCMlJOXipGRr5qdgoSTrqWSq6WFl2ypoaUAAAIfkECQEAAQAsAAAAACgAKAAAApaEb6HLgd/iO7FNWtcFWe+ufODGjRfoiJ2akShbueb0wtI50zm02pbvwfWEMWBQ1zKGlLIhskiEPm9R6vRXxV4ZzWT2yHOGpWMyorblKlNp8HmHEb/lCXjcW7bmtXP8Xt229OVWR1fod2eWqNfHuMjXCPkIGNileOiImVmCOEmoSfn3yXlJWmoHGhqp6ilYuWYpmTqKUgAAIfkECQEAAQAsAAAAACgAKAAAApiEH6kb58biQ3FNWtMFWW3eNVcojuFGfqnZqSebuS06w5V80/X02pKe8zFwP6EFWOT1lDFk8rGERh1TTNOocQ61Hm4Xm2VexUHpzjymViHrFbiELsefVrn6XKfnt2Q9G/+Xdie499XHd2g4h7ioOGhXGJboGAnXSBnoBwKYyfioubZJ2Hn0RuRZaflZOil56Zp6iioKSXpUAAAh+QQJAQABACwAAAAAKAAoAAACkoQRqRvnxuI7kU1a1UU5bd5tnSeOZXhmn5lWK3qNTWvRdQxP8qvaC+/yaYQzXO7BMvaUEmJRd3TsiMAgswmNYrSgZdYrTX6tSHGZO73ezuAw2uxuQ+BbeZfMxsexY35+/Qe4J1inV0g4x3WHuMhIl2jXOKT2Q+VU5fgoSUI52VfZyfkJGkha6jmY+aaYdirq+lQAACH5BAkBAAEALAAAAAAoACgAAAKWBIKpYe0L3YNKToqswUlvznigd4wiR4KhZrKt9Upqip61i9E3vMvxRdHlbEFiEXfk9YARYxOZZD6VQ2pUunBmtRXo1Lf8hMVVcNl8JafV38aM2/Fu5V16Bn63r6xt97j09+MXSFi4BniGFae3hzbH9+hYBzkpuUh5aZmHuanZOZgIuvbGiNeomCnaxxap2upaCZsq+1kAACH5BAkBAAEALAAAAAAoACgAAAKXjI8By5zf4kOxTVrXNVlv1X0d8IGZGKLnNpYtm8Lr9cqVeuOSvfOW79D9aDHizNhDJidFZhNydEahOaDH6nomtJjp1tutKoNWkvA6JqfRVLHU/QUfau9l2x7G54d1fl995xcIGAdXqMfBNadoYrhH+Mg2KBlpVpbluCiXmMnZ2Sh4GBqJ+ckIOqqJ6LmKSllZmsoq6wpQAAAh+QQJAQABACwAAAAAKAAoAAAClYx/oLvoxuJDkU1a1YUZbJ59nSd2ZXhWqbRa2/gF8Gu2DY3iqs7yrq+xBYEkYvFSM8aSSObE+ZgRl1BHFZNr7pRCavZ5BW2142hY3AN/zWtsmf12p9XxxFl2lpLn1rseztfXZjdIWIf2s5dItwjYKBgo9yg5pHgzJXTEeGlZuenpyPmpGQoKOWkYmSpaSnqKileI2FAAACH5BAkBAAEALAAAAAAoACgAAAKVjB+gu+jG4kORTVrVhRlsnn2dJ3ZleFaptFrb+CXmO9OozeL5VfP99HvAWhpiUdcwkpBH3825AwYdU8xTqlLGhtCosArKMpvfa1mMRae9VvWZfeB2XfPkeLmm18lUcBj+p5dnN8jXZ3YIGEhYuOUn45aoCDkp16hl5IjYJvjWKcnoGQpqyPlpOhr3aElaqrq56Bq7VAAAOw=="); 666 | height: 100%; 667 | filter: alpha(opacity=25); /* support: IE8 */ 668 | opacity: 0.25; 669 | } 670 | .ui-progressbar-indeterminate .ui-progressbar-value { 671 | background-image: none; 672 | } 673 | .ui-selectable { 674 | -ms-touch-action: none; 675 | touch-action: none; 676 | } 677 | .ui-selectable-helper { 678 | position: absolute; 679 | z-index: 100; 680 | border: 1px dotted black; 681 | } 682 | .ui-selectmenu-menu { 683 | padding: 0; 684 | margin: 0; 685 | position: absolute; 686 | top: 0; 687 | left: 0; 688 | display: none; 689 | } 690 | .ui-selectmenu-menu .ui-menu { 691 | overflow: auto; 692 | overflow-x: hidden; 693 | padding-bottom: 1px; 694 | } 695 | .ui-selectmenu-menu .ui-menu .ui-selectmenu-optgroup { 696 | font-size: 1em; 697 | font-weight: bold; 698 | line-height: 1.5; 699 | padding: 2px 0.4em; 700 | margin: 0.5em 0 0 0; 701 | height: auto; 702 | border: 0; 703 | } 704 | .ui-selectmenu-open { 705 | display: block; 706 | } 707 | .ui-selectmenu-text { 708 | display: block; 709 | margin-right: 20px; 710 | overflow: hidden; 711 | text-overflow: ellipsis; 712 | } 713 | .ui-selectmenu-button.ui-button { 714 | text-align: left; 715 | white-space: nowrap; 716 | width: 14em; 717 | } 718 | .ui-selectmenu-icon.ui-icon { 719 | float: right; 720 | margin-top: 0; 721 | } 722 | .ui-slider { 723 | position: relative; 724 | text-align: left; 725 | } 726 | .ui-slider .ui-slider-handle { 727 | position: absolute; 728 | z-index: 2; 729 | width: 1.2em; 730 | height: 1.2em; 731 | cursor: default; 732 | -ms-touch-action: none; 733 | touch-action: none; 734 | } 735 | .ui-slider .ui-slider-range { 736 | position: absolute; 737 | z-index: 1; 738 | font-size: .7em; 739 | display: block; 740 | border: 0; 741 | background-position: 0 0; 742 | } 743 | 744 | /* support: IE8 - See #6727 */ 745 | .ui-slider.ui-state-disabled .ui-slider-handle, 746 | .ui-slider.ui-state-disabled .ui-slider-range { 747 | filter: inherit; 748 | } 749 | 750 | .ui-slider-horizontal { 751 | height: .8em; 752 | } 753 | .ui-slider-horizontal .ui-slider-handle { 754 | top: -.3em; 755 | margin-left: -.6em; 756 | } 757 | .ui-slider-horizontal .ui-slider-range { 758 | top: 0; 759 | height: 100%; 760 | } 761 | .ui-slider-horizontal .ui-slider-range-min { 762 | left: 0; 763 | } 764 | .ui-slider-horizontal .ui-slider-range-max { 765 | right: 0; 766 | } 767 | 768 | .ui-slider-vertical { 769 | width: .8em; 770 | height: 100px; 771 | } 772 | .ui-slider-vertical .ui-slider-handle { 773 | left: -.3em; 774 | margin-left: 0; 775 | margin-bottom: -.6em; 776 | } 777 | .ui-slider-vertical .ui-slider-range { 778 | left: 0; 779 | width: 100%; 780 | } 781 | .ui-slider-vertical .ui-slider-range-min { 782 | bottom: 0; 783 | } 784 | .ui-slider-vertical .ui-slider-range-max { 785 | top: 0; 786 | } 787 | .ui-sortable-handle { 788 | -ms-touch-action: none; 789 | touch-action: none; 790 | } 791 | .ui-spinner { 792 | position: relative; 793 | display: inline-block; 794 | overflow: hidden; 795 | padding: 0; 796 | vertical-align: middle; 797 | } 798 | .ui-spinner-input { 799 | border: none; 800 | background: none; 801 | color: inherit; 802 | padding: .222em 0; 803 | margin: .2em 0; 804 | vertical-align: middle; 805 | margin-left: .4em; 806 | margin-right: 2em; 807 | } 808 | .ui-spinner-button { 809 | width: 1.6em; 810 | height: 50%; 811 | font-size: .5em; 812 | padding: 0; 813 | margin: 0; 814 | text-align: center; 815 | position: absolute; 816 | cursor: default; 817 | display: block; 818 | overflow: hidden; 819 | right: 0; 820 | } 821 | /* more specificity required here to override default borders */ 822 | .ui-spinner a.ui-spinner-button { 823 | border-top-style: none; 824 | border-bottom-style: none; 825 | border-right-style: none; 826 | } 827 | .ui-spinner-up { 828 | top: 0; 829 | } 830 | .ui-spinner-down { 831 | bottom: 0; 832 | } 833 | .ui-tabs { 834 | position: relative;/* position: relative prevents IE scroll bug (element with position: relative inside container with overflow: auto appear as "fixed") */ 835 | padding: .2em; 836 | } 837 | .ui-tabs .ui-tabs-nav { 838 | margin: 0; 839 | padding: .2em .2em 0; 840 | } 841 | .ui-tabs .ui-tabs-nav li { 842 | list-style: none; 843 | float: left; 844 | position: relative; 845 | top: 0; 846 | margin: 1px .2em 0 0; 847 | border-bottom-width: 0; 848 | padding: 0; 849 | white-space: nowrap; 850 | } 851 | .ui-tabs .ui-tabs-nav .ui-tabs-anchor { 852 | float: left; 853 | padding: .5em 1em; 854 | text-decoration: none; 855 | } 856 | .ui-tabs .ui-tabs-nav li.ui-tabs-active { 857 | margin-bottom: -1px; 858 | padding-bottom: 1px; 859 | } 860 | .ui-tabs .ui-tabs-nav li.ui-tabs-active .ui-tabs-anchor, 861 | .ui-tabs .ui-tabs-nav li.ui-state-disabled .ui-tabs-anchor, 862 | .ui-tabs .ui-tabs-nav li.ui-tabs-loading .ui-tabs-anchor { 863 | cursor: text; 864 | } 865 | .ui-tabs-collapsible .ui-tabs-nav li.ui-tabs-active .ui-tabs-anchor { 866 | cursor: pointer; 867 | } 868 | .ui-tabs .ui-tabs-panel { 869 | display: block; 870 | border-width: 0; 871 | padding: 1em 1.4em; 872 | background: none; 873 | } 874 | .ui-tooltip { 875 | padding: 8px; 876 | position: absolute; 877 | z-index: 9999; 878 | max-width: 300px; 879 | } 880 | body .ui-tooltip { 881 | border-width: 2px; 882 | } 883 | 884 | /* Component containers 885 | ----------------------------------*/ 886 | .ui-widget { 887 | font-family: Arial,Helvetica,sans-serif; 888 | font-size: 1em; 889 | } 890 | .ui-widget .ui-widget { 891 | font-size: 1em; 892 | } 893 | .ui-widget input, 894 | .ui-widget select, 895 | .ui-widget textarea, 896 | .ui-widget button { 897 | font-family: Arial,Helvetica,sans-serif; 898 | font-size: 1em; 899 | } 900 | .ui-widget.ui-widget-content { 901 | border: 1px solid #c5c5c5; 902 | } 903 | .ui-widget-content { 904 | border: 1px solid #dddddd; 905 | background: #ffffff; 906 | color: #333333; 907 | } 908 | .ui-widget-content a { 909 | color: #333333; 910 | } 911 | .ui-widget-header { 912 | border: 1px solid #dddddd; 913 | background: #e9e9e9; 914 | color: #333333; 915 | font-weight: bold; 916 | } 917 | .ui-widget-header a { 918 | color: #333333; 919 | } 920 | 921 | /* Interaction states 922 | ----------------------------------*/ 923 | .ui-state-default, 924 | .ui-widget-content .ui-state-default, 925 | .ui-widget-header .ui-state-default, 926 | .ui-button, 927 | 928 | /* We use html here because we need a greater specificity to make sure disabled 929 | works properly when clicked or hovered */ 930 | html .ui-button.ui-state-disabled:hover, 931 | html .ui-button.ui-state-disabled:active { 932 | border: 1px solid #c5c5c5; 933 | background: #f6f6f6; 934 | font-weight: normal; 935 | color: #454545; 936 | } 937 | .ui-state-default a, 938 | .ui-state-default a:link, 939 | .ui-state-default a:visited, 940 | a.ui-button, 941 | a:link.ui-button, 942 | a:visited.ui-button, 943 | .ui-button { 944 | color: #454545; 945 | text-decoration: none; 946 | } 947 | .ui-state-hover, 948 | .ui-widget-content .ui-state-hover, 949 | .ui-widget-header .ui-state-hover, 950 | .ui-state-focus, 951 | .ui-widget-content .ui-state-focus, 952 | .ui-widget-header .ui-state-focus, 953 | .ui-button:hover, 954 | .ui-button:focus { 955 | border: 1px solid #cccccc; 956 | background: #ededed; 957 | font-weight: normal; 958 | color: #2b2b2b; 959 | } 960 | .ui-state-hover a, 961 | .ui-state-hover a:hover, 962 | .ui-state-hover a:link, 963 | .ui-state-hover a:visited, 964 | .ui-state-focus a, 965 | .ui-state-focus a:hover, 966 | .ui-state-focus a:link, 967 | .ui-state-focus a:visited, 968 | a.ui-button:hover, 969 | a.ui-button:focus { 970 | color: #2b2b2b; 971 | text-decoration: none; 972 | } 973 | 974 | .ui-visual-focus { 975 | box-shadow: 0 0 3px 1px rgb(94, 158, 214); 976 | } 977 | .ui-state-active, 978 | .ui-widget-content .ui-state-active, 979 | .ui-widget-header .ui-state-active, 980 | a.ui-button:active, 981 | .ui-button:active, 982 | .ui-button.ui-state-active:hover { 983 | border: 1px solid #003eff; 984 | background: #007fff; 985 | font-weight: normal; 986 | color: #ffffff; 987 | } 988 | .ui-icon-background, 989 | .ui-state-active .ui-icon-background { 990 | border: #003eff; 991 | background-color: #ffffff; 992 | } 993 | .ui-state-active a, 994 | .ui-state-active a:link, 995 | .ui-state-active a:visited { 996 | color: #ffffff; 997 | text-decoration: none; 998 | } 999 | 1000 | /* Interaction Cues 1001 | ----------------------------------*/ 1002 | .ui-state-highlight, 1003 | .ui-widget-content .ui-state-highlight, 1004 | .ui-widget-header .ui-state-highlight { 1005 | border: 1px solid #dad55e; 1006 | background: #fffa90; 1007 | color: #777620; 1008 | } 1009 | .ui-state-checked { 1010 | border: 1px solid #dad55e; 1011 | background: #fffa90; 1012 | } 1013 | .ui-state-highlight a, 1014 | .ui-widget-content .ui-state-highlight a, 1015 | .ui-widget-header .ui-state-highlight a { 1016 | color: #777620; 1017 | } 1018 | .ui-state-error, 1019 | .ui-widget-content .ui-state-error, 1020 | .ui-widget-header .ui-state-error { 1021 | border: 1px solid #f1a899; 1022 | background: #fddfdf; 1023 | color: #5f3f3f; 1024 | } 1025 | .ui-state-error a, 1026 | .ui-widget-content .ui-state-error a, 1027 | .ui-widget-header .ui-state-error a { 1028 | color: #5f3f3f; 1029 | } 1030 | .ui-state-error-text, 1031 | .ui-widget-content .ui-state-error-text, 1032 | .ui-widget-header .ui-state-error-text { 1033 | color: #5f3f3f; 1034 | } 1035 | .ui-priority-primary, 1036 | .ui-widget-content .ui-priority-primary, 1037 | .ui-widget-header .ui-priority-primary { 1038 | font-weight: bold; 1039 | } 1040 | .ui-priority-secondary, 1041 | .ui-widget-content .ui-priority-secondary, 1042 | .ui-widget-header .ui-priority-secondary { 1043 | opacity: .7; 1044 | filter:Alpha(Opacity=70); /* support: IE8 */ 1045 | font-weight: normal; 1046 | } 1047 | .ui-state-disabled, 1048 | .ui-widget-content .ui-state-disabled, 1049 | .ui-widget-header .ui-state-disabled { 1050 | opacity: .35; 1051 | filter:Alpha(Opacity=35); /* support: IE8 */ 1052 | background-image: none; 1053 | } 1054 | .ui-state-disabled .ui-icon { 1055 | filter:Alpha(Opacity=35); /* support: IE8 - See #6059 */ 1056 | } 1057 | 1058 | /* Icons 1059 | ----------------------------------*/ 1060 | 1061 | /* states and images */ 1062 | .ui-icon { 1063 | width: 16px; 1064 | height: 16px; 1065 | } 1066 | .ui-icon, 1067 | .ui-widget-content .ui-icon { 1068 | background-image: url("images/ui-icons_444444_256x240.png"); 1069 | } 1070 | .ui-widget-header .ui-icon { 1071 | background-image: url("images/ui-icons_444444_256x240.png"); 1072 | } 1073 | .ui-state-hover .ui-icon, 1074 | .ui-state-focus .ui-icon, 1075 | .ui-button:hover .ui-icon, 1076 | .ui-button:focus .ui-icon { 1077 | background-image: url("images/ui-icons_555555_256x240.png"); 1078 | } 1079 | .ui-state-active .ui-icon, 1080 | .ui-button:active .ui-icon { 1081 | background-image: url("images/ui-icons_ffffff_256x240.png"); 1082 | } 1083 | .ui-state-highlight .ui-icon, 1084 | .ui-button .ui-state-highlight.ui-icon { 1085 | background-image: url("images/ui-icons_777620_256x240.png"); 1086 | } 1087 | .ui-state-error .ui-icon, 1088 | .ui-state-error-text .ui-icon { 1089 | background-image: url("images/ui-icons_cc0000_256x240.png"); 1090 | } 1091 | .ui-button .ui-icon { 1092 | background-image: url("images/ui-icons_777777_256x240.png"); 1093 | } 1094 | 1095 | /* positioning */ 1096 | .ui-icon-blank { background-position: 16px 16px; } 1097 | .ui-icon-caret-1-n { background-position: 0 0; } 1098 | .ui-icon-caret-1-ne { background-position: -16px 0; } 1099 | .ui-icon-caret-1-e { background-position: -32px 0; } 1100 | .ui-icon-caret-1-se { background-position: -48px 0; } 1101 | .ui-icon-caret-1-s { background-position: -65px 0; } 1102 | .ui-icon-caret-1-sw { background-position: -80px 0; } 1103 | .ui-icon-caret-1-w { background-position: -96px 0; } 1104 | .ui-icon-caret-1-nw { background-position: -112px 0; } 1105 | .ui-icon-caret-2-n-s { background-position: -128px 0; } 1106 | .ui-icon-caret-2-e-w { background-position: -144px 0; } 1107 | .ui-icon-triangle-1-n { background-position: 0 -16px; } 1108 | .ui-icon-triangle-1-ne { background-position: -16px -16px; } 1109 | .ui-icon-triangle-1-e { background-position: -32px -16px; } 1110 | .ui-icon-triangle-1-se { background-position: -48px -16px; } 1111 | .ui-icon-triangle-1-s { background-position: -65px -16px; } 1112 | .ui-icon-triangle-1-sw { background-position: -80px -16px; } 1113 | .ui-icon-triangle-1-w { background-position: -96px -16px; } 1114 | .ui-icon-triangle-1-nw { background-position: -112px -16px; } 1115 | .ui-icon-triangle-2-n-s { background-position: -128px -16px; } 1116 | .ui-icon-triangle-2-e-w { background-position: -144px -16px; } 1117 | .ui-icon-arrow-1-n { background-position: 0 -32px; } 1118 | .ui-icon-arrow-1-ne { background-position: -16px -32px; } 1119 | .ui-icon-arrow-1-e { background-position: -32px -32px; } 1120 | .ui-icon-arrow-1-se { background-position: -48px -32px; } 1121 | .ui-icon-arrow-1-s { background-position: -65px -32px; } 1122 | .ui-icon-arrow-1-sw { background-position: -80px -32px; } 1123 | .ui-icon-arrow-1-w { background-position: -96px -32px; } 1124 | .ui-icon-arrow-1-nw { background-position: -112px -32px; } 1125 | .ui-icon-arrow-2-n-s { background-position: -128px -32px; } 1126 | .ui-icon-arrow-2-ne-sw { background-position: -144px -32px; } 1127 | .ui-icon-arrow-2-e-w { background-position: -160px -32px; } 1128 | .ui-icon-arrow-2-se-nw { background-position: -176px -32px; } 1129 | .ui-icon-arrowstop-1-n { background-position: -192px -32px; } 1130 | .ui-icon-arrowstop-1-e { background-position: -208px -32px; } 1131 | .ui-icon-arrowstop-1-s { background-position: -224px -32px; } 1132 | .ui-icon-arrowstop-1-w { background-position: -240px -32px; } 1133 | .ui-icon-arrowthick-1-n { background-position: 1px -48px; } 1134 | .ui-icon-arrowthick-1-ne { background-position: -16px -48px; } 1135 | .ui-icon-arrowthick-1-e { background-position: -32px -48px; } 1136 | .ui-icon-arrowthick-1-se { background-position: -48px -48px; } 1137 | .ui-icon-arrowthick-1-s { background-position: -64px -48px; } 1138 | .ui-icon-arrowthick-1-sw { background-position: -80px -48px; } 1139 | .ui-icon-arrowthick-1-w { background-position: -96px -48px; } 1140 | .ui-icon-arrowthick-1-nw { background-position: -112px -48px; } 1141 | .ui-icon-arrowthick-2-n-s { background-position: -128px -48px; } 1142 | .ui-icon-arrowthick-2-ne-sw { background-position: -144px -48px; } 1143 | .ui-icon-arrowthick-2-e-w { background-position: -160px -48px; } 1144 | .ui-icon-arrowthick-2-se-nw { background-position: -176px -48px; } 1145 | .ui-icon-arrowthickstop-1-n { background-position: -192px -48px; } 1146 | .ui-icon-arrowthickstop-1-e { background-position: -208px -48px; } 1147 | .ui-icon-arrowthickstop-1-s { background-position: -224px -48px; } 1148 | .ui-icon-arrowthickstop-1-w { background-position: -240px -48px; } 1149 | .ui-icon-arrowreturnthick-1-w { background-position: 0 -64px; } 1150 | .ui-icon-arrowreturnthick-1-n { background-position: -16px -64px; } 1151 | .ui-icon-arrowreturnthick-1-e { background-position: -32px -64px; } 1152 | .ui-icon-arrowreturnthick-1-s { background-position: -48px -64px; } 1153 | .ui-icon-arrowreturn-1-w { background-position: -64px -64px; } 1154 | .ui-icon-arrowreturn-1-n { background-position: -80px -64px; } 1155 | .ui-icon-arrowreturn-1-e { background-position: -96px -64px; } 1156 | .ui-icon-arrowreturn-1-s { background-position: -112px -64px; } 1157 | .ui-icon-arrowrefresh-1-w { background-position: -128px -64px; } 1158 | .ui-icon-arrowrefresh-1-n { background-position: -144px -64px; } 1159 | .ui-icon-arrowrefresh-1-e { background-position: -160px -64px; } 1160 | .ui-icon-arrowrefresh-1-s { background-position: -176px -64px; } 1161 | .ui-icon-arrow-4 { background-position: 0 -80px; } 1162 | .ui-icon-arrow-4-diag { background-position: -16px -80px; } 1163 | .ui-icon-extlink { background-position: -32px -80px; } 1164 | .ui-icon-newwin { background-position: -48px -80px; } 1165 | .ui-icon-refresh { background-position: -64px -80px; } 1166 | .ui-icon-shuffle { background-position: -80px -80px; } 1167 | .ui-icon-transfer-e-w { background-position: -96px -80px; } 1168 | .ui-icon-transferthick-e-w { background-position: -112px -80px; } 1169 | .ui-icon-folder-collapsed { background-position: 0 -96px; } 1170 | .ui-icon-folder-open { background-position: -16px -96px; } 1171 | .ui-icon-document { background-position: -32px -96px; } 1172 | .ui-icon-document-b { background-position: -48px -96px; } 1173 | .ui-icon-note { background-position: -64px -96px; } 1174 | .ui-icon-mail-closed { background-position: -80px -96px; } 1175 | .ui-icon-mail-open { background-position: -96px -96px; } 1176 | .ui-icon-suitcase { background-position: -112px -96px; } 1177 | .ui-icon-comment { background-position: -128px -96px; } 1178 | .ui-icon-person { background-position: -144px -96px; } 1179 | .ui-icon-print { background-position: -160px -96px; } 1180 | .ui-icon-trash { background-position: -176px -96px; } 1181 | .ui-icon-locked { background-position: -192px -96px; } 1182 | .ui-icon-unlocked { background-position: -208px -96px; } 1183 | .ui-icon-bookmark { background-position: -224px -96px; } 1184 | .ui-icon-tag { background-position: -240px -96px; } 1185 | .ui-icon-home { background-position: 0 -112px; } 1186 | .ui-icon-flag { background-position: -16px -112px; } 1187 | .ui-icon-calendar { background-position: -32px -112px; } 1188 | .ui-icon-cart { background-position: -48px -112px; } 1189 | .ui-icon-pencil { background-position: -64px -112px; } 1190 | .ui-icon-clock { background-position: -80px -112px; } 1191 | .ui-icon-disk { background-position: -96px -112px; } 1192 | .ui-icon-calculator { background-position: -112px -112px; } 1193 | .ui-icon-zoomin { background-position: -128px -112px; } 1194 | .ui-icon-zoomout { background-position: -144px -112px; } 1195 | .ui-icon-search { background-position: -160px -112px; } 1196 | .ui-icon-wrench { background-position: -176px -112px; } 1197 | .ui-icon-gear { background-position: -192px -112px; } 1198 | .ui-icon-heart { background-position: -208px -112px; } 1199 | .ui-icon-star { background-position: -224px -112px; } 1200 | .ui-icon-link { background-position: -240px -112px; } 1201 | .ui-icon-cancel { background-position: 0 -128px; } 1202 | .ui-icon-plus { background-position: -16px -128px; } 1203 | .ui-icon-plusthick { background-position: -32px -128px; } 1204 | .ui-icon-minus { background-position: -48px -128px; } 1205 | .ui-icon-minusthick { background-position: -64px -128px; } 1206 | .ui-icon-close { background-position: -80px -128px; } 1207 | .ui-icon-closethick { background-position: -96px -128px; } 1208 | .ui-icon-key { background-position: -112px -128px; } 1209 | .ui-icon-lightbulb { background-position: -128px -128px; } 1210 | .ui-icon-scissors { background-position: -144px -128px; } 1211 | .ui-icon-clipboard { background-position: -160px -128px; } 1212 | .ui-icon-copy { background-position: -176px -128px; } 1213 | .ui-icon-contact { background-position: -192px -128px; } 1214 | .ui-icon-image { background-position: -208px -128px; } 1215 | .ui-icon-video { background-position: -224px -128px; } 1216 | .ui-icon-script { background-position: -240px -128px; } 1217 | .ui-icon-alert { background-position: 0 -144px; } 1218 | .ui-icon-info { background-position: -16px -144px; } 1219 | .ui-icon-notice { background-position: -32px -144px; } 1220 | .ui-icon-help { background-position: -48px -144px; } 1221 | .ui-icon-check { background-position: -64px -144px; } 1222 | .ui-icon-bullet { background-position: -80px -144px; } 1223 | .ui-icon-radio-on { background-position: -96px -144px; } 1224 | .ui-icon-radio-off { background-position: -112px -144px; } 1225 | .ui-icon-pin-w { background-position: -128px -144px; } 1226 | .ui-icon-pin-s { background-position: -144px -144px; } 1227 | .ui-icon-play { background-position: 0 -160px; } 1228 | .ui-icon-pause { background-position: -16px -160px; } 1229 | .ui-icon-seek-next { background-position: -32px -160px; } 1230 | .ui-icon-seek-prev { background-position: -48px -160px; } 1231 | .ui-icon-seek-end { background-position: -64px -160px; } 1232 | .ui-icon-seek-start { background-position: -80px -160px; } 1233 | /* ui-icon-seek-first is deprecated, use ui-icon-seek-start instead */ 1234 | .ui-icon-seek-first { background-position: -80px -160px; } 1235 | .ui-icon-stop { background-position: -96px -160px; } 1236 | .ui-icon-eject { background-position: -112px -160px; } 1237 | .ui-icon-volume-off { background-position: -128px -160px; } 1238 | .ui-icon-volume-on { background-position: -144px -160px; } 1239 | .ui-icon-power { background-position: 0 -176px; } 1240 | .ui-icon-signal-diag { background-position: -16px -176px; } 1241 | .ui-icon-signal { background-position: -32px -176px; } 1242 | .ui-icon-battery-0 { background-position: -48px -176px; } 1243 | .ui-icon-battery-1 { background-position: -64px -176px; } 1244 | .ui-icon-battery-2 { background-position: -80px -176px; } 1245 | .ui-icon-battery-3 { background-position: -96px -176px; } 1246 | .ui-icon-circle-plus { background-position: 0 -192px; } 1247 | .ui-icon-circle-minus { background-position: -16px -192px; } 1248 | .ui-icon-circle-close { background-position: -32px -192px; } 1249 | .ui-icon-circle-triangle-e { background-position: -48px -192px; } 1250 | .ui-icon-circle-triangle-s { background-position: -64px -192px; } 1251 | .ui-icon-circle-triangle-w { background-position: -80px -192px; } 1252 | .ui-icon-circle-triangle-n { background-position: -96px -192px; } 1253 | .ui-icon-circle-arrow-e { background-position: -112px -192px; } 1254 | .ui-icon-circle-arrow-s { background-position: -128px -192px; } 1255 | .ui-icon-circle-arrow-w { background-position: -144px -192px; } 1256 | .ui-icon-circle-arrow-n { background-position: -160px -192px; } 1257 | .ui-icon-circle-zoomin { background-position: -176px -192px; } 1258 | .ui-icon-circle-zoomout { background-position: -192px -192px; } 1259 | .ui-icon-circle-check { background-position: -208px -192px; } 1260 | .ui-icon-circlesmall-plus { background-position: 0 -208px; } 1261 | .ui-icon-circlesmall-minus { background-position: -16px -208px; } 1262 | .ui-icon-circlesmall-close { background-position: -32px -208px; } 1263 | .ui-icon-squaresmall-plus { background-position: -48px -208px; } 1264 | .ui-icon-squaresmall-minus { background-position: -64px -208px; } 1265 | .ui-icon-squaresmall-close { background-position: -80px -208px; } 1266 | .ui-icon-grip-dotted-vertical { background-position: 0 -224px; } 1267 | .ui-icon-grip-dotted-horizontal { background-position: -16px -224px; } 1268 | .ui-icon-grip-solid-vertical { background-position: -32px -224px; } 1269 | .ui-icon-grip-solid-horizontal { background-position: -48px -224px; } 1270 | .ui-icon-gripsmall-diagonal-se { background-position: -64px -224px; } 1271 | .ui-icon-grip-diagonal-se { background-position: -80px -224px; } 1272 | 1273 | 1274 | /* Misc visuals 1275 | ----------------------------------*/ 1276 | 1277 | /* Corner radius */ 1278 | .ui-corner-all, 1279 | .ui-corner-top, 1280 | .ui-corner-left, 1281 | .ui-corner-tl { 1282 | border-top-left-radius: 3px; 1283 | } 1284 | .ui-corner-all, 1285 | .ui-corner-top, 1286 | .ui-corner-right, 1287 | .ui-corner-tr { 1288 | border-top-right-radius: 3px; 1289 | } 1290 | .ui-corner-all, 1291 | .ui-corner-bottom, 1292 | .ui-corner-left, 1293 | .ui-corner-bl { 1294 | border-bottom-left-radius: 3px; 1295 | } 1296 | .ui-corner-all, 1297 | .ui-corner-bottom, 1298 | .ui-corner-right, 1299 | .ui-corner-br { 1300 | border-bottom-right-radius: 3px; 1301 | } 1302 | 1303 | /* Overlays */ 1304 | .ui-widget-overlay { 1305 | background: #aaaaaa; 1306 | opacity: .003; 1307 | filter: Alpha(Opacity=.3); /* support: IE8 */ 1308 | } 1309 | .ui-widget-shadow { 1310 | -webkit-box-shadow: 0px 0px 5px #666666; 1311 | box-shadow: 0px 0px 5px #666666; 1312 | } 1313 | -------------------------------------------------------------------------------- /public/script.js: -------------------------------------------------------------------------------- 1 | /* 2 | belaUI - web UI for the BELABOX project 3 | Copyright (C) 2020-2022 BELABOX project 4 | 5 | This program is free software: you can redistribute it and/or modify 6 | it under the terms of the GNU General Public License as published by 7 | the Free Software Foundation, either version 3 of the License, or 8 | (at your option) any later version. 9 | 10 | This program is distributed in the hope that it will be useful, 11 | but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | GNU General Public License for more details. 14 | You should have received a copy of the GNU General Public License 15 | along with this program. If not, see . 16 | */ 17 | 18 | let isStreaming = false; 19 | let config = {}; 20 | 21 | let ws = null; 22 | 23 | function getThemeSetting() { 24 | return $('#themeSelector>select').val(); 25 | } 26 | 27 | function updateTheme(theme) { 28 | if (!theme) { 29 | theme = getThemeSetting(); 30 | } 31 | 32 | if (theme == 'auto') { 33 | if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) { 34 | theme = 'dark'; 35 | } 36 | } 37 | 38 | if (theme == 'dark') { 39 | $('body').addClass('dark'); 40 | } else { 41 | $('body').removeClass('dark'); 42 | } 43 | } 44 | 45 | // Load the persistent setting, if available 46 | function loadThemeSetting() { 47 | const s = localStorage.getItem('theme'); 48 | if (s) { 49 | $('#themeSelector>select').val(s); 50 | } 51 | updateTheme(); 52 | } 53 | loadThemeSetting(); 54 | 55 | // Update the theme if the selector is changed 56 | $('#themeSelector>select').change(function () { 57 | const s = getThemeSetting(); 58 | localStorage.setItem('theme', s); 59 | updateTheme(s); 60 | }); 61 | 62 | // Update the theme if the system preference changes 63 | if (window.matchMedia) { 64 | window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', function() { 65 | updateTheme(); 66 | }); 67 | } 68 | 69 | 70 | function tryConnect() { 71 | let c = new WebSocket("ws://" + window.location.host); 72 | c.addEventListener('message', function (event) { 73 | handleMessage(JSON.parse(event.data)); 74 | }); 75 | 76 | c.addEventListener('close', function (event) { 77 | ws = null; 78 | 79 | showError("Disconnected from BELABOX. Trying to reconnect..."); 80 | setTimeout(tryConnect, 1000); 81 | 82 | updateNetact(false); 83 | }); 84 | 85 | c.addEventListener('open', function (event) { 86 | ws = c; 87 | 88 | hideError(); 89 | $('#notifications').empty(); 90 | tryTokenAuth(); 91 | updateNetact(true); 92 | }); 93 | } 94 | 95 | tryConnect(); 96 | 97 | /* WS keep-alive */ 98 | /* If the browser / tab is in the background, the Javascript may be suspended, 99 | while the WS stays connected. In that case we don't want to receive periodic 100 | updates from the belaUI server as we'll have to walk through a potentially 101 | long list of stale data when the browser / tab regains focus and wakes up. 102 | 103 | The periodic keep-alive packets let the server know that this client is still 104 | active and should receive updates. 105 | */ 106 | setInterval(function() { 107 | if (ws) { 108 | ws.send(JSON.stringify({keepalive: null})); 109 | } 110 | }, 10000); 111 | 112 | 113 | /* Authentication */ 114 | function tryTokenAuth() { 115 | let authToken = localStorage.getItem('authToken'); 116 | if (authToken) { 117 | ws.send(JSON.stringify({auth: {token: authToken}})); 118 | } else { 119 | showLoginForm(); 120 | } 121 | } 122 | 123 | function handleAuthResult(msg) { 124 | if (msg.success === true) { 125 | if (msg.auth_token) { 126 | localStorage.setItem('authToken', msg.auth_token); 127 | } 128 | // Reset state 129 | modems = {}; 130 | wifiIfs = {}; 131 | 132 | // Reset the UI 133 | $('#login').addClass('d-none'); 134 | $('#initialPasswordForm').addClass('d-none'); 135 | hideError(); 136 | $('#notifications').empty(); 137 | $('#wifi').empty(); 138 | $('#modemManager').empty(); 139 | $('#main').removeClass('d-none'); 140 | $('#localSettings').removeClass('d-none'); 141 | } else if (!isShowingInitialPasswordForm) { 142 | showLoginForm(); 143 | } 144 | } 145 | 146 | /* Show the revision number */ 147 | function setRevisions(revs) { 148 | let list = ''; 149 | for (s in revs) { 150 | if (list != '') list += ', '; 151 | list += `${s}\xa0${revs[s]}`; 152 | } 153 | 154 | $('#revisions').text(list); 155 | } 156 | 157 | 158 | /* Network interfaces list */ 159 | function setNetif(name, ip, enabled) { 160 | ws.send(JSON.stringify({'netif': {'name': name, 'ip': ip, 'enabled': enabled}})); 161 | } 162 | 163 | function genNetifEntry(error, enabled, name, ip, throughput, isBold = false) { 164 | let checkbox = ''; 165 | if (enabled != undefined) { 166 | const esc_name = name.replaceAll("'", "\\'"); 167 | const esc_ip = ip.replaceAll("'", "\\'"); 168 | checkbox = ``; 171 | } 172 | 173 | const html = ` 174 | 175 | ${checkbox} 176 | 177 | 178 | 179 | `; 180 | 181 | const entry = $($.parseHTML(html)); 182 | entry.find('.netif_name').text(name); 183 | entry.find('.netif_ip').text(ip); 184 | entry.find('.netif_tp').text(throughput); 185 | if (error) { 186 | const cb = entry.find('input'); 187 | cb.attr('disabled', true); 188 | cb.attr('title', `Can't enable: ${error}`); 189 | } 190 | 191 | return entry; 192 | } 193 | 194 | function updateNetif(netifs) { 195 | let modemList = []; 196 | let totalKbps = 0; 197 | let connsEnabled = 0; 198 | let connsCount = 0; 199 | 200 | for (const i in netifs) { 201 | data = netifs[i]; 202 | tpKbps = Math.round((data['tp'] * 8) / 1024); 203 | totalKbps += tpKbps; 204 | 205 | modemList.push(genNetifEntry(data.error, data.enabled, i, data.ip, `${tpKbps} Kbps`)); 206 | 207 | connsCount += 1; 208 | if (data.enabled) { 209 | connsEnabled += 1; 210 | } 211 | } 212 | 213 | if (connsCount > 1) { 214 | let countLabel; 215 | if (connsEnabled == connsCount) { 216 | countLabel = `${connsCount} conns`; 217 | } else { 218 | countLabel = `${connsEnabled} / ${connsCount} conns`; 219 | } 220 | const totalRow = genNetifEntry(undefined, undefined, 'Total', countLabel, `${totalKbps} Kbps`, true); 221 | modemList.unshift(totalRow); 222 | } 223 | 224 | $('#netifTable>tbody').html(modemList); 225 | } 226 | 227 | function updateSensors(sensors) { 228 | const sensorList = []; 229 | 230 | for (const i in sensors) { 231 | data = sensors[i]; 232 | 233 | const entryHtml = ` 234 | 235 | 236 | 237 | `; 238 | const entry = $($.parseHTML(entryHtml)); 239 | entry.find('.sensor_name').text(i); 240 | entry.find('.sensor_value').text(data); 241 | sensorList.push(entry); 242 | } 243 | 244 | $('#sensorsTable>tbody').html(sensorList); 245 | } 246 | 247 | 248 | /* Remote status */ 249 | let remoteConnectedHideTimer; 250 | function showRemoteStatus(status) { 251 | if (remoteConnectedHideTimer) { 252 | clearTimeout(remoteConnectedHideTimer); 253 | remoteConnectedHideTimer = undefined; 254 | } 255 | 256 | if (status === true) { 257 | $('#remoteStatus').removeClass('alert-danger'); 258 | $('#remoteStatus').addClass('alert-success'); 259 | $('#remoteStatus').text("BELABOX cloud remote: connected"); 260 | remoteConnectedHideTimer = setTimeout(function() { 261 | $('#remoteStatus').addClass('d-none'); 262 | remoteConnectedHideTimer = undefined; 263 | }, 5000); 264 | } else if (status.error) { 265 | switch(status.error) { 266 | case 'network': 267 | $('#remoteStatus').text("BELABOX cloud remote: network error. Trying to reconnect...\n"); 268 | break; 269 | case 'key': 270 | $('#remoteStatus').text("BELABOX cloud remote: invalid key\n"); 271 | break; 272 | default: 273 | return; 274 | } 275 | 276 | $('#remoteStatus').addClass('alert-danger'); 277 | $('#remoteStatus').removeClass('alert-success'); 278 | } else { 279 | return; 280 | } 281 | $('#remoteStatus').removeClass('d-none'); 282 | } 283 | 284 | 285 | /* Software updates */ 286 | function showSoftwareUpdates(status) { 287 | if (status) { 288 | if (status.package_count) { 289 | $('#softwareUpdate span.desc').text(`(${status.package_count} packages, ${status.download_size})`); 290 | } else { 291 | $('#softwareUpdate span.desc').text('(up to date)'); 292 | } 293 | $('#softwareUpdate').attr('disabled', !status.package_count); 294 | } else if (status === null) { 295 | $('#softwareUpdate span.desc').text('(checking for updates...)'); 296 | $('#softwareUpdate').attr('disabled', true); 297 | } 298 | if (status === false) { 299 | $('#softwareUpdate').addClass('d-none'); 300 | } else { 301 | $('#softwareUpdate').removeClass('d-none'); 302 | } 303 | } 304 | 305 | function showSoftwareUpdateValue(cls, value, total) { 306 | if (value > 0) { 307 | $(`#softwareUpdateStatus .${cls} .value`).text(`${value} / ${total}`); 308 | $(`#softwareUpdateStatus .${cls}`).removeClass('d-none'); 309 | } else { 310 | $(`#softwareUpdateStatus .${cls}`).addClass('d-none'); 311 | } 312 | } 313 | 314 | function showSoftwareUpdateStatus(status) { 315 | if (!status) { 316 | $('#softwareUpdateStatus').addClass('d-none'); 317 | return; 318 | } 319 | 320 | $('#startStop, #softwareUpdate, .command-btn').attr('disabled', status.result === undefined); 321 | 322 | showSoftwareUpdateValue('downloading', status.downloading, status.total); 323 | showSoftwareUpdateValue('unpacking', status.unpacking, status.total); 324 | showSoftwareUpdateValue('setting-up', status.setting_up, status.total); 325 | 326 | if (status.result === 0) { 327 | $('#softwareUpdateStatus p.result').text('Update completed. Restarting the encoder...'); 328 | $('#softwareUpdateStatus p.result').removeClass('text-danger'); 329 | $('#softwareUpdateStatus p.result').addClass('text-success'); 330 | $('#softwareUpdateStatus .result').removeClass('d-none'); 331 | } else if (status.result !== undefined) { 332 | $('#softwareUpdateStatus p.result').text("Update error: " + status.result); 333 | $('#softwareUpdateStatus p.result').removeClass('text-success'); 334 | $('#softwareUpdateStatus p.result').addClass('text-danger'); 335 | $('#softwareUpdateStatus .result').removeClass('d-none'); 336 | } else { 337 | $('#softwareUpdateStatus .result').addClass('d-none'); 338 | } 339 | 340 | $('#softwareUpdateStatus').removeClass('d-none'); 341 | } 342 | 343 | $('#softwareUpdate').click(function() { 344 | const msg = 'Are you sure you want to start a software update? ' + 345 | 'This may take several minutes. ' + 346 | 'You won\'t be able to start a stream until it\'s completed. ' + 347 | 'The encoder will briefly disconnect after a successful upgrade. ' + 348 | 'Never remove power or reset the encoder while updating. If the encoder is powered from a battery, ensure it\'s fully charged.'; 349 | 350 | if (confirm(msg)) { 351 | send_command('update'); 352 | } 353 | }); 354 | 355 | 356 | /* SSH status / control */ 357 | let sshStatus; 358 | function showSshStatus(s) { 359 | if (s !== undefined) { 360 | sshStatus = s; 361 | } 362 | 363 | if (!sshStatus) return; 364 | 365 | const pass = !config.ssh_pass ? 'password not set' : (sshStatus.user_pass ? 'user-set password' : config.ssh_pass) 366 | $('label[for=sshPassword]').text(`SSH password (username: ${sshStatus.user})`); 367 | 368 | $('#sshPassword').val(pass); 369 | if (sshStatus.active) { 370 | $('#startSsh').addClass('d-none'); 371 | $('#stopSsh').removeClass('d-none'); 372 | } else { 373 | $('#stopSsh').addClass('d-none'); 374 | $('#startSsh').removeClass('d-none'); 375 | } 376 | $('#sshSettings').removeClass('d-none'); 377 | } 378 | 379 | $('#resetSshPass').click(function() { 380 | const msg = 'Are you sure you want to reset the SSH password?'; 381 | 382 | if (confirm(msg)) { 383 | send_command('reset_ssh_pass'); 384 | } 385 | }); 386 | 387 | 388 | /* Audio device / codec selection */ 389 | let audioSrcList = []; 390 | function updateAudioSrcs(list) { 391 | if (list !== null) { 392 | audioSrcList = list; 393 | } 394 | 395 | const audioSelect = document.getElementById("audioSource"); 396 | audioSelect.innerText = null; 397 | let asrcFound = false; 398 | 399 | for (const card of audioSrcList) { 400 | const option = document.createElement("option"); 401 | option.value = card; 402 | option.innerText = card; 403 | 404 | audioSelect.append(option); 405 | if (config.asrc && card == config.asrc) { 406 | option.selected = true; 407 | asrcFound = true; 408 | } 409 | } 410 | 411 | if (config.asrc && !asrcFound) { 412 | const option = document.createElement("option"); 413 | option.innerText = config.asrc + " (unavailable)"; 414 | option.value = config.asrc; 415 | option.selected = true; 416 | audioSelect.append(option); 417 | } 418 | } 419 | 420 | let audioCodecList = {}; 421 | function updateAudioCodecs(list) { 422 | if (list !== null) { 423 | audioCodecList = list; 424 | } 425 | 426 | const audioCodec = document.getElementById("audioCodec"); 427 | audioCodec.innerText = null; 428 | 429 | for (const codec in audioCodecList) { 430 | const option = document.createElement("option"); 431 | option.value = codec; 432 | option.innerText = audioCodecList[codec]; 433 | 434 | if (config.acodec && codec == config.acodec) { 435 | option.selected = true; 436 | } 437 | audioCodec.append(option); 438 | } 439 | } 440 | 441 | 442 | /* status updates */ 443 | function updateStatus(status) { 444 | if (status.is_streaming !== undefined) { 445 | isStreaming = status.is_streaming; 446 | if (isStreaming) { 447 | updateButtonAndSettingsShow({ 448 | add: "btn-danger", 449 | remove: "btn-success", 450 | text: "Stop", 451 | enabled: true, 452 | settingsShow: false, 453 | }); 454 | } else { 455 | updateButtonAndSettingsShow({ 456 | add: "btn-success", 457 | remove: "btn-danger", 458 | text: "Start", 459 | enabled: true, 460 | settingsShow: true, 461 | }); 462 | } 463 | } 464 | 465 | if (status.remote) { 466 | showRemoteStatus(status.remote); 467 | } 468 | 469 | if (status.set_password === true) { 470 | showInitialPasswordForm(); 471 | } 472 | 473 | if (status.available_updates !== undefined) { 474 | showSoftwareUpdates(status.available_updates); 475 | } 476 | 477 | if (status.updating !== undefined) { 478 | showSoftwareUpdateStatus(status.updating); 479 | } 480 | 481 | if (status.ssh) { 482 | showSshStatus(status.ssh); 483 | } 484 | 485 | if (status.wifi) { 486 | updateWifiState(status.wifi); 487 | } 488 | 489 | if (status.modems) { 490 | updateModemsState(status.modems); 491 | } 492 | 493 | if (status.asrcs) { 494 | updateAudioSrcs(status.asrcs); 495 | } 496 | } 497 | 498 | 499 | /* Configuration loading */ 500 | function loadConfig(c) { 501 | config = c; 502 | 503 | initBitrateSlider(config.max_br ?? 5000); 504 | initDelaySlider(config.delay ?? 0); 505 | initSrtLatencySlider(config.srt_latency ?? 2000); 506 | updatePipelines(null); 507 | updateAudioSrcs(null); 508 | updateRelays(null); 509 | 510 | const srtlaAddr = config.srtla_addr ?? ""; 511 | showHideRelayHint(srtlaAddr); 512 | $('#srtlaAddr').val(srtlaAddr); 513 | $('#srtlaPort').val(config.srtla_port ?? ""); 514 | $('#srtStreamid').val(config.srt_streamid ?? ""); 515 | 516 | $("#bitrateOverlay").prop('checked', config.bitrate_overlay) 517 | 518 | $('#autoStart').prop('checked', config.autostart ?? false); 519 | $('#autoStartForm button[type=submit]').prop('disabled', true); 520 | $('#remoteDeviceKey').val(config.remote_key); 521 | $('#remoteKeyForm button[type=submit]').prop('disabled', true); 522 | 523 | if (config.ssh_pass && sshStatus) { 524 | showSshStatus(); 525 | } 526 | } 527 | 528 | 529 | /* Pipelines */ 530 | function updateOptionList(select, options, selected) { 531 | const validIds = {}; 532 | 533 | let entriesToDeselect = []; 534 | let entryToSelect; 535 | let prevOption; 536 | 537 | for (const o in options) { 538 | for (const value in options[o]) { 539 | const id = `o_${o}_${value}`; 540 | validIds[id] = true; 541 | 542 | let entry = select.find(`.${id}`); 543 | if (entry.length == 0) { 544 | const html = '' 545 | entry = $($.parseHTML(html)); 546 | entry.addClass(id); 547 | entry.data('option_id', id); 548 | entry.attr('value', value); 549 | 550 | if (prevOption) { 551 | entry.insertAfter(prevOption); 552 | } else { 553 | select.prepend(entry); 554 | } 555 | } 556 | 557 | const contents = options[o][value].name; 558 | if (contents != entry.text()) { 559 | entry.text(contents); 560 | } 561 | const isDisabled = options[o][value].disabled; 562 | if (entry.attr('disabled') != isDisabled) { 563 | entry.attr('disabled', isDisabled); 564 | } 565 | const isSelected = (selected && value == selected); 566 | const wasSelected = entry.attr('selected') == 'selected'; 567 | if (isSelected && !wasSelected) { 568 | entryToSelect = entry; 569 | } 570 | if (!isSelected && wasSelected) { 571 | entriesToDeselect.push(entry); 572 | } 573 | 574 | prevOption = entry; 575 | } 576 | } // for o in options 577 | 578 | // Delete removed options 579 | select.find('option').each(function() { 580 | const option = $(this) 581 | const optionId = option.data('option_id'); 582 | if (optionId && !validIds[optionId]) { 583 | option.remove(); 584 | } 585 | }); 586 | 587 | // Update the selected entry if it's changed 588 | // First, we have to deselect any other entries 589 | for (const e of entriesToDeselect) { 590 | e.attr('selected', false); 591 | } 592 | 593 | if (entryToSelect) { 594 | entryToSelect.attr('selected', true); 595 | } 596 | } 597 | 598 | let pipelines = {}; 599 | function updatePipelines(ps) { 600 | if (ps != null) { 601 | pipelines = ps; 602 | } 603 | 604 | updateOptionList($('#pipelines'), [pipelines], config.pipeline); 605 | 606 | pipelineSelectHandler($('#pipelines').val()) 607 | } 608 | 609 | function pipelineSelectHandler(s) { 610 | const p = pipelines[s]; 611 | if (!p) return; 612 | 613 | if (p.asrc) { 614 | $('#selectAudioSource').removeClass('d-none'); 615 | } else { 616 | $('#selectAudioSource').addClass('d-none'); 617 | } 618 | 619 | if (p.acodec) { 620 | $('#selectAudioCodec').removeClass('d-none'); 621 | } else { 622 | $('#selectAudioCodec').addClass('d-none'); 623 | } 624 | } 625 | 626 | $("#pipelines").change(function(ev) { 627 | pipelineSelectHandler(ev.target.value); 628 | }); 629 | 630 | /* Remote relays config */ 631 | let isValidRelaySelection = true; 632 | function updateRelaySettings() { 633 | if ($('#relayServer').val() == 'manual') { 634 | $('.remote-relay-account').addClass('d-none'); 635 | $('.manual-relay-addr, .manual-streamid').removeClass('d-none'); 636 | isValidRelaySelection = true; 637 | } else { 638 | $('.manual-relay-addr').addClass('d-none'); 639 | $('.remote-relay-account').removeClass('d-none'); 640 | if ($('#relayAccount').val() == 'manual') { 641 | $('.manual-streamid').removeClass('d-none'); 642 | } else { 643 | $('.manual-streamid').addClass('d-none'); 644 | } 645 | isValidRelaySelection = ($('#relayAccount').val() !== null); 646 | } 647 | 648 | if (isValidRelaySelection) { 649 | removeNotification('relay_account_unavailable'); 650 | } else { 651 | showNotification({name: 'relay_account_unavailable', type: 'error', 652 | msg: 'Your selected relay server account is no longer available. ' + 653 | 'Please select a different one to start the stream.'}); 654 | } 655 | updateButtonEnabledDisabled(); 656 | } 657 | $('#relayServer, #relayAccount').change(function() { 658 | updateRelaySettings(); 659 | }); 660 | 661 | let relays; 662 | function updateRelays(r) { 663 | if (r && r.servers && r.accounts) { 664 | relays = r; 665 | } 666 | 667 | const preset = {manual: {name: 'Manual configuration'}}; 668 | 669 | let selectedServer = config.relay_server; 670 | if (!relays || config.srtla_addr || config.srtla_port) { 671 | selectedServer = 'manual'; 672 | } else if (!config.relay_server || !relays.servers[config.relay_server]) { 673 | for (const s in relays.servers) { 674 | if (relays.servers[s].default) { 675 | selectedServer = s; 676 | } 677 | } 678 | } 679 | updateOptionList($('#relayServer'), [relays ? relays.servers : {}, preset], selectedServer); 680 | 681 | let selectedAccount = config.relay_account; 682 | if (!relays || config.srt_streamid !== undefined) { 683 | selectedAccount = 'manual'; 684 | } else if (config.relay_account) { 685 | if (!relays.accounts[config.relay_account]) { 686 | preset['unavailable'] = {name: 'No longer available', disabled: true}; 687 | selectedAccount = 'unavailable'; 688 | } 689 | } 690 | updateOptionList($('#relayAccount'), [relays ? relays.accounts : {}, preset], selectedAccount); 691 | 692 | updateRelaySettings(); 693 | } 694 | 695 | /* Bitrate setting updates */ 696 | function updateBitrate(br) { 697 | $('#bitrateSlider').slider('option', 'value', br.max_br); 698 | showBitrate(br.max_br); 699 | } 700 | 701 | 702 | /* WiFi manager */ 703 | function wifiScan(button, deviceId) { 704 | if (!ws) return; 705 | 706 | // Disable the search button immediately 707 | const wifiManager = $(button).parents('.wifi-settings'); 708 | wifiManager.find('.wifi-scan-button').attr('disabled', true); 709 | 710 | // Send the request 711 | ws.send(JSON.stringify({wifi: {scan: deviceId}})); 712 | 713 | // Duration 714 | const searchDuration = 10000; 715 | 716 | setTimeout(function() { 717 | wifiManager.find('.wifi-scan-button').attr('disabled', false); 718 | wifiManager.find('.scanning').addClass('d-none'); 719 | }, searchDuration); 720 | 721 | wifiManager.find('.connect-error').addClass('d-none'); 722 | wifiManager.find('.scanning').removeClass('d-none'); 723 | } 724 | 725 | function wifiSendNewConnection() { 726 | $('#wifiNewErrAuth').addClass('d-none'); 727 | $('#wifiNewErrGeneric').addClass('d-none'); 728 | $('#wifiNewConnecting').removeClass('d-none'); 729 | 730 | $('#wifiConnectButton').attr('disabled', true); 731 | 732 | const device = $('#connection-device').val(); 733 | const ssid = $('#connection-ssid').val(); 734 | const password = $('#connection-password').val(); 735 | 736 | ws.send(JSON.stringify({ 737 | wifi: { 738 | new: { 739 | device, 740 | ssid, 741 | password 742 | } 743 | } 744 | })); 745 | 746 | return false; 747 | } 748 | 749 | function wifiConnect(e) { 750 | const network = $(e).parents('tr.network').data('network'); 751 | 752 | if (network.active) return; 753 | 754 | if (network.uuid) { 755 | ws.send(JSON.stringify({wifi: {connect: network.uuid}})); 756 | 757 | const wifiManager = $(e).parents('.wifi-settings'); 758 | wifiManager.find('.connect-error').addClass('d-none'); 759 | wifiManager.find('.connecting').removeClass('d-none'); 760 | } else { 761 | if (network.security === "") { 762 | if (confirm(`Connect to the open network ${network.ssid}?`)) { 763 | ws.send(JSON.stringify({ 764 | wifi: { 765 | new: { 766 | ssid: network.ssid, 767 | device: network.device 768 | } 769 | } 770 | })); 771 | } 772 | } else { 773 | if (network.security.match('802.1X')) { 774 | alert("This network uses 802.1X enterprise authentication, " + 775 | "which belaUI doesn't support at the moment"); 776 | } else if (network.security.match('WEP')) { 777 | alert("This network uses legacy WEP authentication, " + 778 | "which belaUI doesn't support"); 779 | } else { 780 | $('#connection-ssid').val(network.ssid); 781 | $('#connection-device').val(network.device); 782 | $('#connection-password').val(''); 783 | $('.wifi-new-status').addClass('d-none'); 784 | $('#wifiConnectButton').attr('disabled', false); 785 | $('#wifiModal').modal({ show: true }); 786 | 787 | setTimeout(() => { 788 | $('#connection-password').focus(); 789 | }, 500); 790 | } 791 | } 792 | } 793 | } 794 | 795 | function wifiDisconnect(e) { 796 | const network = $(e).parents('tr').data('network'); 797 | 798 | if (confirm(`Disconnect from ${network.ssid}?`)) { 799 | ws.send(JSON.stringify({ 800 | wifi: { 801 | disconnect: network.uuid 802 | }, 803 | })); 804 | } 805 | } 806 | 807 | function wifiForget(e) { 808 | const network = $(e).parents('tr').data('network'); 809 | 810 | if (confirm(`Forget network ${network.ssid}?`)) { 811 | ws.send(JSON.stringify({ 812 | wifi: { 813 | forget: network.uuid 814 | }, 815 | })); 816 | } 817 | } 818 | 819 | function wifiFindCardId(deviceId) { 820 | return `wifi-manager-${parseInt(deviceId)}`; 821 | } 822 | 823 | function wifiSignalSymbol(signal) { 824 | if (signal < 0) signal = 0; 825 | if (signal > 100) signal = 100; 826 | const symbol = 9601 + Math.floor(signal / 12.51); 827 | let cl = "text-success"; 828 | if (signal < 40) { 829 | cl = "text-danger"; 830 | } else if (signal < 75) { 831 | cl = "text-warning"; 832 | } 833 | return `&#${symbol}`; 834 | } 835 | 836 | function wifiListAvailableNetwork(device, deviceId, a) { 837 | const savedUuid = device.saved[a.ssid]; 838 | if (savedUuid) { 839 | delete device.saved[a.ssid]; 840 | } 841 | 842 | const html = ` 843 | 844 | 845 | 846 | 847 | 848 | Connected
849 | 850 | 851 | 852 | 861 | 866 | 867 | `; 868 | 869 | const network = $($.parseHTML(html)); 870 | network.find('.signal').html(wifiSignalSymbol(a.signal));// + '%'); 871 | network.find('.band').html((a.freq > 5000) ? '5㎓' : '2.4㎓'); 872 | const ssidEl = network.find('.ssid'); 873 | ssidEl.text(a.ssid); 874 | 875 | network.data('network', {active: a.active, uuid: savedUuid, ssid: a.ssid, device: deviceId, security: a.security}); 876 | 877 | if (a.security != '') { 878 | // show a cross mark for 802.1X or WEP networks (unsupported) 879 | // or a lock symbol for PSK networks (supported) 880 | network.find('.security').html(a.security.match(/802\.1X|WEP/) ? '❌' : '🔒'); 881 | } 882 | if (a.active) { 883 | network.find('.disconnect').removeClass('d-none'); 884 | network.find('.connected').removeClass('d-none'); 885 | } 886 | if (!a.active) { 887 | network.find('.ssid').addClass('can-connect'); 888 | } 889 | if (savedUuid) { 890 | network.find('.forget').removeClass('d-none'); 891 | } 892 | 893 | return network; 894 | } 895 | 896 | function wifiListSavedNetwork(ssid, uuid) { 897 | const html = ` 898 | 899 | 900 | 901 | 906 | 907 | `; 908 | 909 | const network = $($.parseHTML(html)); 910 | network.find('.ssid').text(ssid); 911 | 912 | network.data('network', {ssid, uuid}); 913 | 914 | return network; 915 | } 916 | 917 | function wifiCheckHotspotSettings(deviceId) { 918 | if (!wifiIfs[deviceId] || !wifiIfs[deviceId].hotspot) return; 919 | 920 | const cardId = wifiFindCardId(deviceId); 921 | const form = $(`#${cardId}`).find('.hotspot'); 922 | 923 | let anyValueChanged = false; 924 | let allValuesValid = true; 925 | 926 | const nameInput = form.find('.hotspot-name').val(); 927 | if (nameInput != wifiIfs[deviceId].hotspot.name) { 928 | anyValueChanged = true; 929 | const hint = form.find('.hotspot-name-hint'); 930 | if (nameInput.length < 1 || nameInput.length > 32) { 931 | hint.removeClass('d-none'); 932 | allValuesValid = false; 933 | } else { 934 | hint.addClass('d-none'); 935 | } 936 | } 937 | 938 | const passwordInput = form.find('.hotspot-password').val(); 939 | if (passwordInput != wifiIfs[deviceId].hotspot.password) { 940 | anyValueChanged = true; 941 | const hint = form.find('.hotspot-password-hint'); 942 | if (passwordInput.length < 8 || passwordInput.length > 64) { 943 | hint.removeClass('d-none'); 944 | allValuesValid = false; 945 | } else { 946 | hint.addClass('d-none'); 947 | } 948 | } 949 | 950 | const channelInput = form.find('.hotspot-channel'); 951 | if (channelInput.val() != wifiIfs[deviceId].hotspot.channel) { 952 | anyValueChanged = true; 953 | } 954 | 955 | form.find('.hotspot-config-save').attr('disabled', !anyValueChanged || !allValuesValid) 956 | } 957 | 958 | let wifiIfs = {}; 959 | function updateWifiState(msg) { 960 | for (const i in wifiIfs) { 961 | wifiIfs[i].removed = true; 962 | } 963 | 964 | for (let deviceId in msg) { 965 | // Mark the interface as not removed 966 | if (wifiIfs[deviceId]) { 967 | delete wifiIfs[deviceId].removed; 968 | } 969 | 970 | const cardId = wifiFindCardId(deviceId); 971 | const device = msg[deviceId]; 972 | let deviceCard = $(`#${cardId}`); 973 | 974 | if (deviceCard.length == 0) { 975 | const html = ` 976 |
977 |
978 | 981 |
982 | 983 |
984 |
985 |
986 |

The NetworkManager connection for the hotspot has been modified from the BELABOX defaults. Correct functionality can't be guaranteed. If you experience issues, please delete it via command line

987 | 988 |
989 | 990 |

The network name must be between 1 and 32 characters long

991 | 992 |
993 | 994 |
995 | 996 |

The password must be between 8 and 64 characters long

997 |
998 | 999 |
1000 | 1001 |
1002 |
1003 |
1004 | 1005 |
1006 | 1007 | 1009 |
1010 | 1011 |
1012 |
Saving...
1013 |
Saved
1014 | 1015 | 1016 | 1017 |
1018 | 1019 |
1020 | 1023 | 1024 |
1025 |
1026 |
1027 | Connecting... 1028 |
1029 | 1030 |
1031 | Error connecting to the network. Has the password changed? 1032 |
1033 | 1034 |
1035 |
1036 |
1037 | Scanning... 1038 |
1039 | 1040 | 1041 | 1042 |
1043 | 1044 | 1045 | 1046 | 1047 | 1048 | 1049 |
Other saved networks
1050 | 1051 | 1052 |
1053 |
1054 |
1055 |
`; 1056 | 1057 | deviceCard = $($.parseHTML(html)); 1058 | 1059 | deviceCard.find('button.showHidePassword').click(showHidePassword); 1060 | 1061 | deviceCard.find('button.hotspot-mode').click(function() { 1062 | if (confirm('This will immediately disconnect the WiFi adapter from any connected networks and turn on the hotspot. Proceed?')) { 1063 | ws.send(JSON.stringify({wifi: {hotspot: {start: {device: deviceId}}}})); 1064 | } 1065 | }); 1066 | 1067 | deviceCard.find('button.client-mode').click(function() { 1068 | if (confirm('This will immediately disconnect any connected clients and disable the hotspot. Proceed?')) { 1069 | ws.send(JSON.stringify({wifi: {hotspot: {stop: {device: deviceId}}}})); 1070 | } 1071 | }); 1072 | 1073 | deviceCard.find('.hotspot-name, .hotspot-password, .hotspot-channel').on('input', function() {wifiCheckHotspotSettings(deviceId)}); 1074 | 1075 | deviceCard.find('button.hotspot-config-save').click(function() { 1076 | let config = { 1077 | device: deviceId, 1078 | name: deviceCard.find('input.hotspot-name').val(), 1079 | password: deviceCard.find('input.hotspot-password').val(), 1080 | channel: deviceCard.find('select.hotspot-channel').val(), 1081 | }; 1082 | ws.send(JSON.stringify({wifi: {hotspot: {config}}})); 1083 | 1084 | $(this).attr('disabled', true); 1085 | deviceCard.find('.save-error, .saved').addClass('d-none'); 1086 | deviceCard.find('.saving').removeClass('d-none'); 1087 | }); 1088 | 1089 | deviceCard.appendTo('#wifi'); 1090 | } 1091 | 1092 | // Update the card's header 1093 | deviceCard.find('.device-name').text(device.ifname); 1094 | deviceCard.find('.device-hw').text(device.hw ? ` (${device.hw})` : ''); 1095 | 1096 | // Disable or enable the hotspot mode button depending on whether the hardware supports it 1097 | deviceCard.find('button.hotspot-mode').attr('disabled', (!device.supports_hotspot && !device.hotspot)); 1098 | 1099 | if (device.hotspot) { 1100 | if (!wifiIfs[deviceId] || !wifiIfs[deviceId].hotspot || wifiIfs[deviceId].hotspot.name != device.hotspot.name) { 1101 | deviceCard.find('.hotspot-name').val(device.hotspot.name); 1102 | } 1103 | if (!wifiIfs[deviceId] || !wifiIfs[deviceId].hotspot || wifiIfs[deviceId].hotspot.password != device.hotspot.password) { 1104 | deviceCard.find('.hotspot-password').val(device.hotspot.password); 1105 | } 1106 | if (!wifiIfs[deviceId] || !wifiIfs[deviceId].hotspot || wifiIfs[deviceId].hotspot.channel != device.hotspot.channel) { 1107 | updateOptionList(deviceCard.find('select.hotspot-channel'), 1108 | [device.hotspot.available_channels], device.hotspot.channel); 1109 | } 1110 | 1111 | if (device.hotspot.warnings && device.hotspot.warnings.includes('modified')) { 1112 | deviceCard.find('.hotspot-modified').removeClass('d-none'); 1113 | } else { 1114 | deviceCard.find('.hotspot-modified').addClass('d-none'); 1115 | } 1116 | 1117 | deviceCard.find('.client').addClass('d-none'); 1118 | deviceCard.find('.hotspot').removeClass('d-none'); 1119 | } else { 1120 | // Show the available networks 1121 | let networkList = []; 1122 | 1123 | for (const a of msg[deviceId].available) { 1124 | if (a.active) { 1125 | networkList.push(wifiListAvailableNetwork(device, deviceId, a)); 1126 | } 1127 | } 1128 | 1129 | for (const a of msg[deviceId].available) { 1130 | if (!a.active) { 1131 | networkList.push(wifiListAvailableNetwork(device, deviceId, a)); 1132 | } 1133 | } 1134 | 1135 | deviceCard.find('.available-networks').html(networkList); 1136 | 1137 | // Show the saved networks 1138 | networkList = []; 1139 | for (const ssid in msg[deviceId].saved) { 1140 | const uuid = msg[deviceId].saved[ssid]; 1141 | networkList.push(wifiListSavedNetwork(ssid, uuid)); 1142 | } 1143 | 1144 | if (networkList.length) { 1145 | deviceCard.find('tbody.saved-networks').html(networkList); 1146 | deviceCard.find('table.saved-networks').removeClass('d-none'); 1147 | } else { 1148 | deviceCard.find('table.saved-networks').addClass('d-none'); 1149 | } 1150 | 1151 | deviceCard.find('.hotspot').addClass('d-none'); 1152 | deviceCard.find('.client').removeClass('d-none'); 1153 | } 1154 | } 1155 | 1156 | for (const i in wifiIfs) { 1157 | if (wifiIfs[i].removed) { 1158 | const cardId = wifiFindCardId(i); 1159 | $(`#${cardId}`).remove(); 1160 | } 1161 | } 1162 | 1163 | wifiIfs = msg; 1164 | } 1165 | 1166 | function handleWifiResult(msg) { 1167 | if (msg.connect !== undefined) { 1168 | const wifiManagerId = `#${wifiFindCardId(msg.device)}`; 1169 | $(wifiManagerId).find('.connecting').addClass('d-none'); 1170 | if (msg.connect === false) { 1171 | $(wifiManagerId).find('.connect-error').removeClass('d-none'); 1172 | } 1173 | } else if (msg.new) { 1174 | if (msg.new.error) { 1175 | $('#wifiNewConnecting').addClass('d-none'); 1176 | 1177 | switch (msg.new.error) { 1178 | case 'auth': 1179 | $('#wifiNewErrAuth').removeClass('d-none'); 1180 | break; 1181 | case 'generic': 1182 | $('#wifiNewErrGeneric').removeClass('d-none'); 1183 | break; 1184 | } 1185 | 1186 | $('#wifiConnectButton').attr('disabled', false); 1187 | } 1188 | if (msg.new.success) { 1189 | $('#wifiModal').modal('hide'); 1190 | } 1191 | } else if (msg.hotspot) { 1192 | if (msg.hotspot.config) { 1193 | const wifiManager = $(`#${wifiFindCardId(msg.hotspot.config.device)}`); 1194 | 1195 | if (msg.hotspot.config.success) { 1196 | wifiManager.find('.save-error, .saving').addClass('d-none'); 1197 | wifiManager.find('.saved').removeClass('d-none'); 1198 | 1199 | } else if (msg.hotspot.config.error) { 1200 | let errMsg; 1201 | 1202 | switch (msg.hotspot.config.error) { 1203 | case 'name': 1204 | case 'password': 1205 | case 'channel': 1206 | errMsg = `invalid ${msg.hotspot.config.error}`; 1207 | break; 1208 | case 'saving': 1209 | case 'activating': 1210 | errMsg = 'couldn\'t apply the new settings'; 1211 | break; 1212 | } 1213 | if (errMsg) { 1214 | const errorField = wifiManager.find('.save-error'); 1215 | errorField.text('Failed to save the settings: ' + errMsg); 1216 | wifiManager.find('.saved, .saving').addClass('d-none'); 1217 | errorField.removeClass('d-none'); 1218 | } 1219 | } 1220 | } 1221 | } 1222 | } 1223 | 1224 | 1225 | /* Modem manager */ 1226 | function modemFindCardId(deviceId) { 1227 | return `modemManager${parseInt(deviceId)}`; 1228 | } 1229 | 1230 | let modems = {}; 1231 | function updateModemsState(msg) { 1232 | for (const i in modems) { 1233 | modems[i].removed = true; 1234 | } 1235 | 1236 | for (let deviceId in msg) { 1237 | if (modems[deviceId]) { 1238 | delete modems[deviceId].removed; 1239 | } 1240 | 1241 | const cardId = modemFindCardId(deviceId); 1242 | const device = msg[deviceId]; 1243 | const modem = modems[deviceId]; 1244 | 1245 | let deviceCard = $(`#${cardId}`); 1246 | 1247 | if (deviceCard.length == 0) { 1248 | const html = ` 1249 |
1250 |
1251 | 1254 |
1255 | 1256 |
1257 |
1258 | 1259 | 1260 | No SIM card 1261 |
1262 |
1263 |
1264 | 1265 | 1266 |
1267 |
1268 | 1269 | 1270 |
1271 |
1272 | 1273 |
1274 | 1275 |
1276 | 1277 |
1278 |
1279 |
1280 |
1281 | 1282 | 1283 |
1284 |
1285 |
1286 | 1287 | 1288 |
1289 |
1290 | 1291 | 1292 |
1293 |
1294 | 1295 | 1296 |
1297 |
1298 | 1299 | 1300 |
1301 |
1302 |
`; 1303 | 1304 | deviceCard = $($.parseHTML(html)); 1305 | 1306 | // Set the name 1307 | if (device.ifname) { 1308 | deviceCard.find('.device-ifname').text(device.ifname); 1309 | deviceCard.find('.device-name').text(` (${device.name})`); 1310 | } else { 1311 | deviceCard.find('.device-name').text(device.name); 1312 | } 1313 | 1314 | // Show the status bar, either with the no SIM message or actual signal info 1315 | if (device.no_sim) { 1316 | deviceCard.find('.no-sim').removeClass('d-none'); 1317 | } else { 1318 | deviceCard.find('.signal, .status, .card-body').removeClass('d-none'); 1319 | } 1320 | 1321 | // Dynamically show and hide network selection depending on the roaming checkbox 1322 | const showHideNetworkSelection = function() { 1323 | const checkbox = $(this); 1324 | const networkSelection = $(this).parents('.card-body').find('.network-selection-group'); 1325 | if (checkbox.prop('checked')) { 1326 | networkSelection.removeClass('d-none'); 1327 | } else { 1328 | networkSelection.addClass('d-none'); 1329 | } 1330 | }; 1331 | deviceCard.find('.roaming-input').on('change', showHideNetworkSelection); 1332 | 1333 | // Check if the device supports GSM autoconfiguration 1334 | if (device.config && device.config.autoconfig !== undefined) { 1335 | // Dynamically show and hide APN settings depending on the autoconfig checkbox 1336 | const showHideApnConfig = function() { 1337 | const checkbox = $(this); 1338 | const apnConfigForm = $(this).parents('.card-body').find('.apn-manual-config'); 1339 | if (checkbox.prop('checked')) { 1340 | apnConfigForm.addClass('d-none'); 1341 | } else { 1342 | apnConfigForm.removeClass('d-none'); 1343 | } 1344 | }; 1345 | const checkbox = deviceCard.find('.autoconfig-input'); 1346 | checkbox.on('change', showHideApnConfig); 1347 | checkbox.prop('disabled', false); 1348 | deviceCard.find('.autoconfig-group').removeClass('d-none'); 1349 | } 1350 | 1351 | const scanButton = deviceCard.find('.network-scan-button'); 1352 | scanButton.click(function() { 1353 | if (confirm('Scanning for networks will temporarily disable the data connection of this modem. Proceed?')) { 1354 | scanButton.prop('disabled', true); 1355 | scanButton.text('Scanning...'); 1356 | ws.send(JSON.stringify({modems: {scan: {device: deviceId}}})); 1357 | } 1358 | }); 1359 | 1360 | const getUserConfig = function(deviceCard) { 1361 | const network_type = deviceCard.find('.network-type-input').val(); 1362 | const roaming = deviceCard.find('.roaming-input').prop('checked'); 1363 | const network = deviceCard.find('.network-selection-input').val(); 1364 | const autoconfig = deviceCard.find('.autoconfig-input').prop('checked'); 1365 | const apn = deviceCard.find('.apn-input').val(); 1366 | const username = deviceCard.find('.username-input').val(); 1367 | const password = deviceCard.find('.password-input').val(); 1368 | 1369 | return {network_type, roaming, network, autoconfig, apn, username, password}; 1370 | }; 1371 | 1372 | deviceCard.find('.save-button').click(function() { 1373 | const config = getUserConfig(deviceCard); 1374 | config.device = deviceId; 1375 | 1376 | ws.send(JSON.stringify({modems: {config}})); 1377 | 1378 | $(this).prop('disabled', true); 1379 | }); 1380 | 1381 | // Disable or enable the save button depending on whether any values have changed 1382 | const inputs = deviceCard.find('input, select'); 1383 | inputs.on('change, input', function() { 1384 | if (!modems[deviceId] || !modems[deviceId].config) return false; 1385 | 1386 | const userConfig = getUserConfig(deviceCard); 1387 | const savedConfig = Object.assign({network_type: modems[deviceId].network_type.active}, modems[deviceId].config); 1388 | let changed = false; 1389 | for (const i in savedConfig) { 1390 | if (userConfig[i] !== savedConfig[i]) { 1391 | console.log(`${i} changed`); 1392 | changed = true; 1393 | break; 1394 | } 1395 | } 1396 | deviceCard.find('.save-button').prop('disabled', !changed); 1397 | }); 1398 | 1399 | deviceCard.appendTo('#modemManager'); 1400 | } 1401 | 1402 | // The following settings may be updated for an existing modem 1403 | if (device.network_type) { 1404 | const options = {}; 1405 | for (const i in device.network_type.supported) { 1406 | const value = device.network_type.supported[i]; 1407 | const name = value.replace(/g/g, 'G / ').replace(/ \/ $/, ''); 1408 | options[value] = {name}; 1409 | } 1410 | updateOptionList(deviceCard.find('.network-type-input'), 1411 | [options], device.network_type.active); 1412 | } 1413 | 1414 | if (device.config) { 1415 | deviceCard.find('.apn-input').attr('value', device.config.apn); 1416 | deviceCard.find('.username-input').attr('value', device.config.username); 1417 | deviceCard.find('.password-input').attr('value', device.config.password); 1418 | deviceCard.find('.roaming-input').attr('checked', device.config.roaming); 1419 | 1420 | // Trigger UI updates 1421 | if (device.config.autoconfig !== undefined) { 1422 | deviceCard.find('.autoconfig-input').attr('checked', device.config.autoconfig); 1423 | deviceCard.find('.autoconfig-input').trigger('change'); 1424 | } 1425 | deviceCard.find('.roaming-input').trigger('change'); 1426 | } 1427 | 1428 | if (device.status) { 1429 | deviceCard.find('.signal').html(wifiSignalSymbol(device.status.signal)); 1430 | const statusText = `${device.status.signal}% ${device.status.network_type || ''} `+ 1431 | `${device.status.network || ''}${device.status.roaming ? ' (R)': ''} - ${device.status.connection}` 1432 | deviceCard.find('.status').text(statusText); 1433 | } 1434 | 1435 | const networkSelect = deviceCard.find('.network-selection-input'); 1436 | if (device.available_networks || device.config || networkSelect.find('option').length == 0) { 1437 | const selectedNetwork = (device.config ? device.config.network : ((modem && modem.config) ? modem.config.network : undefined)); 1438 | const availableNetworks = device.available_networks || (modem ? modem.available_networks : {}); 1439 | const auto = {'': {name: 'Automatic' + ((selectedNetwork == '') ? ' (selected)' : '')}}; 1440 | const options = {}; 1441 | for (const i in availableNetworks) { 1442 | let name = availableNetworks[i].name; 1443 | let availability = ''; 1444 | if (i == selectedNetwork) { 1445 | availability = 'selected'; 1446 | } 1447 | if (availableNetworks[i].availability) { 1448 | if (availability) { 1449 | availability += ' & '; 1450 | } 1451 | availability += availableNetworks[i].availability; 1452 | } 1453 | if (availability) { 1454 | name += ` (${availability})` 1455 | } 1456 | options[i] = { 1457 | name, 1458 | disabled: (availableNetworks[i].availability == 'forbidden') 1459 | }; 1460 | } 1461 | updateOptionList(networkSelect, [auto, options], selectedNetwork); 1462 | 1463 | // Re-enable the scan button after receiving the results 1464 | if (device.available_networks) { 1465 | const scanButton = deviceCard.find('.network-scan-button'); 1466 | scanButton.prop('disabled', false); 1467 | scanButton.text('Scan'); 1468 | } 1469 | } 1470 | 1471 | // Update the cached modem state 1472 | modems[deviceId] = Object.assign(modem || {}, device); 1473 | 1474 | // Disable or enable the save button if any settings have been updated 1475 | if (device.network_type || device.config) { 1476 | deviceCard.find('.network-type-input').trigger('input'); 1477 | } 1478 | } 1479 | 1480 | for (const i in modems) { 1481 | if (modems[i].removed) { 1482 | const cardId = modemFindCardId(i); 1483 | $(`#${cardId}`).remove(); 1484 | delete modems[i]; 1485 | } 1486 | } 1487 | } 1488 | 1489 | 1490 | /* Error messages */ 1491 | function showError(message) { 1492 | $("#errorMsg>span").text(message); 1493 | $("#errorMsg").removeClass('d-none'); 1494 | } 1495 | 1496 | function hideError() { 1497 | $("#errorMsg").addClass('d-none'); 1498 | } 1499 | 1500 | 1501 | /* Notifications */ 1502 | function notificationId(name) { 1503 | return `notification-${name}`; 1504 | } 1505 | 1506 | function showNotification(n) { 1507 | if (!n.name || !n.type || !n.msg) return; 1508 | const alertId = notificationId(n.name); 1509 | 1510 | let alert = $(`#${alertId}`); 1511 | if (alert.length == 0) { 1512 | const html = ` 1513 |
1514 | 1515 | 1518 |
`; 1519 | alert = $($.parseHTML(html)); 1520 | 1521 | alert.attr('id', alertId); 1522 | if (n.is_dismissable) { 1523 | alert.addClass('alert-dismissible'); 1524 | alert.find('button').removeClass('d-none'); 1525 | } 1526 | 1527 | alert.appendTo('#notifications'); 1528 | 1529 | // If we've shown a new notification, scroll to the top 1530 | $('html, body').animate({ 1531 | scrollTop: 0, 1532 | scrollLeft: 0 1533 | }, 200); 1534 | } else { 1535 | alert.removeClass(['alert-secondary', 'alert-danger', 'alert-warning', 'alert-success']); 1536 | const t = alert.data('timerHide'); 1537 | if (t) { 1538 | clearTimeout(t); 1539 | } 1540 | } 1541 | 1542 | let colorClass = 'alert-secondary' 1543 | switch(n.type) { 1544 | case 'error': 1545 | alert.addClass(`alert-danger`); 1546 | break; 1547 | case 'warning': 1548 | case 'success': 1549 | alert.addClass(`alert-${n.type}`); 1550 | break; 1551 | } 1552 | alert.addClass(colorClass); 1553 | 1554 | alert.find('span.msg').text(n.msg); 1555 | 1556 | if (n.duration) { 1557 | alert.data('timerHide', setTimeout(function() { 1558 | alert.slideUp(300, function() { 1559 | $(this).remove(); 1560 | }); 1561 | }, n.duration * 1000)); 1562 | } 1563 | } 1564 | 1565 | function removeNotification(name) { 1566 | const alertId = notificationId(name); 1567 | $(`#${alertId}`).remove(); 1568 | } 1569 | 1570 | function handleNotification(msg) { 1571 | if (msg.show) { 1572 | for (const n of msg.show) { 1573 | showNotification(n); 1574 | } 1575 | } 1576 | if (msg.remove) { 1577 | for (const n of msg.remove) { 1578 | removeNotification(n); 1579 | } 1580 | } 1581 | } 1582 | 1583 | 1584 | /* Log download */ 1585 | function downloadLog(msg) { 1586 | const blob = new Blob([msg.contents], {type: 'text/plain'}) 1587 | 1588 | const a = window.document.createElement('a'); 1589 | a.href = window.URL.createObjectURL(blob); 1590 | a.download = msg.name; 1591 | a.click(); 1592 | 1593 | window.URL.revokeObjectURL(blob); 1594 | } 1595 | 1596 | 1597 | /* Handle server-to-client messages */ 1598 | function handleMessage(msg) { 1599 | console.log(msg); 1600 | for (const type in msg) { 1601 | switch(type) { 1602 | case 'auth': 1603 | handleAuthResult(msg[type]); 1604 | break; 1605 | case 'revisions': 1606 | setRevisions(msg[type]); 1607 | break; 1608 | case 'netif': 1609 | updateNetif(msg[type]); 1610 | break; 1611 | case 'sensors': 1612 | updateSensors(msg[type]); 1613 | break; 1614 | case 'status': 1615 | updateStatus(msg[type]); 1616 | break; 1617 | case 'config': 1618 | loadConfig(msg[type]); 1619 | break; 1620 | case 'pipelines': 1621 | updatePipelines(msg[type]); 1622 | break; 1623 | case 'relays': 1624 | updateRelays(msg[type]); 1625 | break; 1626 | case 'bitrate': 1627 | updateBitrate(msg[type]); 1628 | break; 1629 | case 'wifi': 1630 | handleWifiResult(msg[type]); 1631 | break; 1632 | case 'error': 1633 | showError(msg[type].msg); 1634 | break; 1635 | case 'notification': 1636 | handleNotification(msg[type]); 1637 | break; 1638 | case 'log': 1639 | downloadLog(msg[type]); 1640 | break; 1641 | case 'acodecs': 1642 | updateAudioCodecs(msg[type]); 1643 | break; 1644 | } 1645 | } 1646 | } 1647 | 1648 | 1649 | /* Start / stop */ 1650 | function getConfig() { 1651 | const maxBr = $("#bitrateSlider").slider("value"); 1652 | 1653 | let config = {}; 1654 | config.pipeline = document.getElementById("pipelines").value; 1655 | if (pipelines[config.pipeline].asrc) { 1656 | config.asrc = document.getElementById("audioSource").value; 1657 | } 1658 | if (pipelines[config.pipeline].acodec) { 1659 | config.acodec = document.getElementById("audioCodec").value; 1660 | } 1661 | config.delay = $("#delaySlider").slider("value"); 1662 | config.max_br = maxBr; 1663 | config.srt_latency = $("#srtLatencySlider").slider("value"); 1664 | config.bitrate_overlay = $("#bitrateOverlay").prop('checked'); 1665 | 1666 | const relayServer = $('#relayServer').val(); 1667 | if (relayServer !== 'manual') { 1668 | config.relay_server = relayServer; 1669 | } else { 1670 | config.srtla_addr = $("#srtlaAddr").val(); 1671 | config.srtla_port = $("#srtlaPort").val(); 1672 | } 1673 | 1674 | const relayAccount = $('#relayAccount').val(); 1675 | if (relayServer !== 'manual' && relayAccount !== 'manual') { 1676 | config.relay_account = relayAccount; 1677 | } else { 1678 | config.srt_streamid = $("#srtStreamid").val(); 1679 | } 1680 | 1681 | return config; 1682 | } 1683 | 1684 | async function start() { 1685 | hideError(); 1686 | 1687 | ws.send(JSON.stringify({start: getConfig()})); 1688 | } 1689 | 1690 | async function stop() { 1691 | ws.send(JSON.stringify({stop: 0})); 1692 | } 1693 | 1694 | async function send_command(cmd) { 1695 | ws.send(JSON.stringify({command: cmd})); 1696 | } 1697 | 1698 | 1699 | /* UI */ 1700 | let startStopButtonIsEnabled; 1701 | function updateButtonEnabledDisabled(isEnabled) { 1702 | if (isEnabled !== undefined) { 1703 | startStopButtonIsEnabled = isEnabled; 1704 | } 1705 | const button = $("#startStop"); 1706 | button.attr('disabled', !startStopButtonIsEnabled || !isValidRelaySelection); 1707 | } 1708 | 1709 | function updateButton({ add, remove, text, enabled }) { 1710 | const button = document.getElementById("startStop"); 1711 | 1712 | button.classList.add(add); 1713 | button.classList.remove(remove); 1714 | 1715 | button.innerHTML = text; 1716 | updateButtonEnabledDisabled(enabled); 1717 | } 1718 | 1719 | function updateButtonAndSettingsShow({ add, remove, text, enabled, settingsShow }) { 1720 | const settingsDivs = document.getElementById("settings"); 1721 | 1722 | if (settingsShow) { 1723 | settingsDivs.classList.remove("d-none"); 1724 | } else { 1725 | settingsDivs.classList.add("d-none"); 1726 | } 1727 | 1728 | updateButton({add, remove, text, enabled }); 1729 | } 1730 | 1731 | 1732 | function setBitrate(max) { 1733 | if (isStreaming) { 1734 | ws.send(JSON.stringify({bitrate: {max_br: max}})); 1735 | } 1736 | } 1737 | 1738 | function showBitrate(value) { 1739 | document.getElementById( 1740 | "bitrateValues" 1741 | ).value = `Max bitrate: ${value} Kbps`; 1742 | } 1743 | 1744 | function initBitrateSlider(bitrateDefault) { 1745 | const s = $("#bitrateSlider"); 1746 | s.slider({ 1747 | range: false, 1748 | min: 500, 1749 | max: 12000, 1750 | step: 250, 1751 | value: bitrateDefault, 1752 | slide: (event, ui) => { 1753 | showBitrate(ui.value); 1754 | setBitrate(ui.value); 1755 | setSliderAutolockTimer(s); 1756 | }, 1757 | }); 1758 | initSliderLock(s); 1759 | showBitrate(bitrateDefault); 1760 | } 1761 | 1762 | function showDelay(value) { 1763 | document.getElementById("delayValue").value = `Audio delay: ${value} ms`; 1764 | } 1765 | 1766 | function initDelaySlider(defaultDelay) { 1767 | const s = $("#delaySlider"); 1768 | s.slider({ 1769 | min: -2000, 1770 | max: 2000, 1771 | step: 20, 1772 | value: defaultDelay, 1773 | slide: (event, ui) => { 1774 | showDelay(ui.value); 1775 | setSliderAutolockTimer(s); 1776 | }, 1777 | }); 1778 | initSliderLock(s); 1779 | showDelay(defaultDelay); 1780 | } 1781 | 1782 | function showSrtLatency(value) { 1783 | document.getElementById("srtLatencyValue").value = `SRT latency: ${value} ms`; 1784 | 1785 | if (value < 1500) { 1786 | $('#latencyWarning').removeClass('d-none'); 1787 | } else { 1788 | $('#latencyWarning').addClass('d-none'); 1789 | } 1790 | } 1791 | 1792 | function initSrtLatencySlider(defaultLatency) { 1793 | const s = $("#srtLatencySlider"); 1794 | s.slider({ 1795 | min: 100, 1796 | max: 4000, 1797 | step: 100, 1798 | value: defaultLatency, 1799 | slide: (event, ui) => { 1800 | showSrtLatency(ui.value); 1801 | setSliderAutolockTimer(s); 1802 | }, 1803 | }); 1804 | initSliderLock(s); 1805 | showSrtLatency(defaultLatency); 1806 | } 1807 | 1808 | 1809 | /* UI event handlers */ 1810 | document.getElementById("startStop").addEventListener("click", () => { 1811 | if (!isStreaming) { 1812 | updateButton({text: "Starting...", enabled: false}); 1813 | start(); 1814 | } else { 1815 | updateButton({text: "Stopping...", enabled: false}); 1816 | stop(); 1817 | } 1818 | }); 1819 | 1820 | function updateNetact(isActive) { 1821 | if (isActive) { 1822 | $('.netact, .recheck-netact').attr('disabled', false); 1823 | $('.recheck-netact').trigger('input'); 1824 | showSoftwareUpdates(false); 1825 | } else { 1826 | $('.netact, .recheck-netact').attr('disabled', true); 1827 | updateButtonEnabledDisabled(false); 1828 | } 1829 | } 1830 | 1831 | 1832 | function showLoginForm() { 1833 | $('#main').addClass('d-none'); 1834 | $('#initialPasswordForm').addClass('d-none'); 1835 | $('#login').removeClass('d-none'); 1836 | $('#localSettings').removeClass('d-none'); 1837 | } 1838 | 1839 | function sendAuthMsg(password, isPersistent) { 1840 | let auth_req = {auth: {password, persistent_token: isPersistent}}; 1841 | ws.send(JSON.stringify(auth_req)); 1842 | } 1843 | 1844 | $('#login>form').submit(function() { 1845 | const password = $('#password').val(); 1846 | const rememberMe = $('#login .rememberMe').prop('checked'); 1847 | sendAuthMsg(password, rememberMe); 1848 | 1849 | $('#password').val(''); 1850 | 1851 | return false; 1852 | }); 1853 | 1854 | let isShowingInitialPasswordForm = false; 1855 | function showInitialPasswordForm() { 1856 | $('#main').addClass('d-none'); 1857 | $('#login').addClass('d-none'); 1858 | $('#initialPasswordForm').removeClass('d-none'); 1859 | $('#localSettings').removeClass('d-none'); 1860 | isShowingInitialPasswordForm = true; 1861 | } 1862 | 1863 | function checkPassword() { 1864 | const form = $(this).parents('form'); 1865 | 1866 | const p = $(this).val(); 1867 | let isValid = false; 1868 | 1869 | if (p.length < 8) { 1870 | $(form).find('.hint').text('Minimum length: 8 characters'); 1871 | } else { 1872 | $(form).find('.hint').text(''); 1873 | isValid = true; 1874 | } 1875 | 1876 | $(form).find('button[type=submit]').prop('disabled', !isValid); 1877 | } 1878 | $('.set-password').on('input', checkPassword); 1879 | 1880 | function sendPasswordFromInput(form) { 1881 | const passwordInput = $(form).find('input.set-password'); 1882 | const password = passwordInput.val(); 1883 | 1884 | passwordInput.val(''); 1885 | $(form).find('button[type=submit]').prop('disabled', true); 1886 | 1887 | ws.send(JSON.stringify({config: {password}})); 1888 | 1889 | return password; 1890 | } 1891 | 1892 | $('#initialPasswordForm form').submit(function() { 1893 | const password = sendPasswordFromInput(this); 1894 | const remember = $(this).find('.rememberMe').prop('checked'); 1895 | sendAuthMsg(password, remember); 1896 | 1897 | return false; 1898 | }); 1899 | 1900 | $('form#updatePasswordForm').submit(function() { 1901 | sendPasswordFromInput(this); 1902 | 1903 | return false; 1904 | }); 1905 | 1906 | function checkRemoteKey() { 1907 | const remote_key = $('#remoteDeviceKey').val(); 1908 | const disabled = (remote_key == config.remote_key); 1909 | $('#remoteKeyForm button[type=submit]').prop('disabled', disabled); 1910 | } 1911 | $('#remoteDeviceKey').on('input', checkRemoteKey); 1912 | 1913 | $('#remoteKeyForm').submit(function() { 1914 | const remote_key = $('#remoteDeviceKey').val(); 1915 | ws.send(JSON.stringify({config: {remote_key}})); 1916 | return false; 1917 | }); 1918 | 1919 | $('#logout').click(function() { 1920 | localStorage.removeItem('authToken'); 1921 | ws.send(JSON.stringify({logout: true})); 1922 | showLoginForm(); 1923 | }); 1924 | 1925 | $('.command-btn').click(function() { 1926 | const confirmationMsg = $(this).attr('data-confirmation'); 1927 | if (!confirmationMsg || confirm(confirmationMsg)) { 1928 | // convert to snake case 1929 | const cmd = this.id.split(/(?=[A-Z])/).join('_').toLowerCase(); 1930 | send_command(cmd); 1931 | } 1932 | }); 1933 | 1934 | function showHidePassword() { 1935 | const inputField = $(this).parents('.input-group').find('input'); 1936 | if(inputField.attr('type') == 'password') { 1937 | inputField.attr('type', 'text'); 1938 | $(this).text('Hide'); 1939 | } else { 1940 | inputField.attr('type', 'password'); 1941 | $(this).text('Show'); 1942 | } 1943 | } 1944 | $('button.showHidePassword').click(showHidePassword); 1945 | 1946 | function showHideRelayHint(addr) { 1947 | const isCloudRelay = addr.match(/belabox.net$/); 1948 | if (isCloudRelay) { 1949 | $('#cloudRelay').addClass('d-none'); 1950 | } else { 1951 | $('#cloudRelay').removeClass('d-none'); 1952 | } 1953 | } 1954 | 1955 | $('input#srtlaAddr').change(function() { 1956 | showHideRelayHint($(this).val()); 1957 | }); 1958 | 1959 | $('#autoStart').change(function() { 1960 | if(this.checked) { 1961 | if (!confirm('Warning: Enabling this option will cause the encoder to start streaming automatically upon power-up or reset, potentially at unintended times. Do not enable this setting if automatic streaming poses any privacy or safety risks.')) { 1962 | this.checked = false; 1963 | } 1964 | } 1965 | 1966 | const settingChanged = (this.checked != (config.autostart ?? false)); 1967 | const form = $(this).parents('form'); 1968 | $(form).find('button[type=submit]').prop('disabled', !settingChanged); 1969 | }); 1970 | 1971 | $('#autoStartForm').submit(function() { 1972 | const autostart = $('#autoStart').prop('checked'); 1973 | ws.send(JSON.stringify({config: {autostart}})); 1974 | 1975 | return false; 1976 | }); 1977 | 1978 | /* Input fields automatically copied to clipboard when clicked */ 1979 | function copyInputValToClipboard(obj) { 1980 | if (!document.queryCommandSupported || !document.queryCommandSupported("copy")) { 1981 | return false; 1982 | } 1983 | 1984 | let input = $(obj); 1985 | let valField = input; 1986 | 1987 | valField = $(''); 1988 | valField.css('position', 'fixed'); 1989 | valField.css('top', '100000px'); 1990 | valField.val(input.val()); 1991 | $('body').append(valField); 1992 | 1993 | let success = false; 1994 | try { 1995 | valField.select(); 1996 | document.execCommand("copy"); 1997 | success = true; 1998 | } catch (err) { 1999 | console.log("Copying failed: " + err.message); 2000 | } 2001 | 2002 | valField.remove(); 2003 | 2004 | return success; 2005 | } 2006 | 2007 | $('input.click-copy').tooltip({title: 'Copied', trigger: 'manual'}); 2008 | $('input.click-copy').click(function(ev) { 2009 | const target = ev.target; 2010 | let input = $(ev.target); 2011 | 2012 | if (copyInputValToClipboard(target)) { 2013 | input.tooltip('show'); 2014 | if (target.copiedTooltipTimer) { 2015 | clearTimeout(target.copiedTooltipTimer); 2016 | } 2017 | target.copiedTooltipTimer = setTimeout(function() { 2018 | input.tooltip('hide'); 2019 | delete target.copiedTooltipTimer; 2020 | }, 3000); 2021 | } 2022 | }); 2023 | 2024 | 2025 | /* Slider locking */ 2026 | function getSliderLockBtn(slider) { 2027 | return slider.parents('.form-group').find('.button-slider-lock-unlock'); 2028 | } 2029 | 2030 | function updateSliderLockState(slider, btn, isLocked) { 2031 | if (!btn) { 2032 | btn = getSliderLockBtn(slider); 2033 | } 2034 | 2035 | slider.slider('option', 'disabled', isLocked); 2036 | btn.text(isLocked ? "\u{1F512}" : "\u{1F513}"); 2037 | 2038 | if (!isLocked && sliderLockSetting == 'autolock') { 2039 | setSliderAutolockTimer(slider); 2040 | } 2041 | } 2042 | 2043 | function setSliderAutolockTimer(slider) { 2044 | let lockTimer = slider.data('lockTimer'); 2045 | if (lockTimer) { 2046 | clearTimeout(lockTimer); 2047 | } 2048 | 2049 | // If auto-locking is disabled, don't set another timer 2050 | if (sliderLockSetting != 'autolock') { 2051 | slider.data('lockTimer', null); 2052 | return; 2053 | } 2054 | 2055 | lockTimer = setTimeout(function() { 2056 | /* We have to check here too, in case autolocking was disabled 2057 | between the event being set up and firing */ 2058 | if (sliderLockSetting == 'autolock') { 2059 | updateSliderLockState(slider, undefined, true); 2060 | } 2061 | slider.data('lockTimer', null); 2062 | }, 5000); 2063 | slider.data('lockTimer', lockTimer); 2064 | } 2065 | 2066 | function initSliderLock(slider) { 2067 | const btn = getSliderLockBtn(slider); 2068 | 2069 | // If the slider locks are disabled, remove the event handler and hide the lock button 2070 | if (!sliderLockSetting) { 2071 | btn.parent().addClass('d-none'); 2072 | btn.off('click'); 2073 | return; 2074 | } 2075 | 2076 | const lockWasHidden = btn.parent().hasClass('d-none'); 2077 | const isLocked = lockWasHidden ? true : slider.slider('option', 'disabled'); 2078 | 2079 | updateSliderLockState(slider, btn, isLocked); 2080 | 2081 | if (lockWasHidden) { 2082 | btn.click(function () { 2083 | const slider = $(btn).parents('.form-group').find('.slider'); 2084 | const isLocked = slider.slider('option', 'disabled'); 2085 | updateSliderLockState(slider, btn, !isLocked); 2086 | }); 2087 | btn.parent().removeClass('d-none'); 2088 | } 2089 | } 2090 | 2091 | /* Slider lock setting: load and update */ 2092 | function isTouchDevice() { 2093 | return (('ontouchstart' in window) || 2094 | (navigator.maxTouchPoints > 0) || 2095 | (navigator.msMaxTouchPoints > 0)); 2096 | } 2097 | function loadSliderLockSetting() { 2098 | let s = localStorage.getItem('sliderLocks'); 2099 | switch (s) { 2100 | case 'autolock': 2101 | case 'on': 2102 | break; 2103 | case 'off': 2104 | break; 2105 | default: 2106 | s = 'off'; 2107 | // if (isTouchDevice()) s = 'autolock'; 2108 | } 2109 | 2110 | $('#sliderLockSetting>select').val(s); 2111 | 2112 | if (s != 'off') return s; 2113 | } 2114 | let sliderLockSetting = loadSliderLockSetting(); 2115 | 2116 | $('#sliderLockSetting>select').change(function () { 2117 | let s = $(this).val(); 2118 | localStorage.setItem('sliderLocks', s); 2119 | if (s != 'on' && s != 'autolock') { 2120 | s = undefined; 2121 | } 2122 | sliderLockSetting = s; 2123 | 2124 | $('.slider').each(function () { 2125 | initSliderLock($(this)); 2126 | }); 2127 | }); 2128 | 2129 | /* Layout setting */ 2130 | let layoutSetting; 2131 | function updateLayout(layout) { 2132 | if (layout == 'netif-first') { 2133 | $('body').addClass('netif-first'); 2134 | } else { 2135 | $('body').removeClass('netif-first'); 2136 | } 2137 | layoutSetting = layout; 2138 | } 2139 | 2140 | function loadLayoutSetting() { 2141 | let s = localStorage.getItem('layout'); 2142 | switch (s) { 2143 | case 'standard': 2144 | case 'netif-first': 2145 | break; 2146 | default: 2147 | s = 'standard'; 2148 | } 2149 | 2150 | $('#layoutSetting>select').val(s); 2151 | 2152 | updateLayout(s); 2153 | } 2154 | loadLayoutSetting(); 2155 | 2156 | $('#layoutSetting>select').change(function () { 2157 | const s = $(this).val(); 2158 | localStorage.setItem('layout', s); 2159 | updateLayout(s); 2160 | }); 2161 | --------------------------------------------------------------------------------