├── .github └── FUNDING.yml ├── .gitignore ├── 98-belaui-audio.rules ├── 99-belaui-check-usb-devices.rules ├── LICENSE ├── belaUI.js ├── belaUI.service ├── belaUI.socket ├── config.json ├── install_service.sh ├── package.json ├── public ├── bootstrap.bundle.min.js ├── bootstrap.min.css ├── index.html ├── jquery-3.5.1.js ├── jquery-ui-1.12.1.css ├── jquery-ui-1.12.1.js ├── jquery.ui.touch-punch.js ├── script.js └── style.css └── setup.json /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: rationalsa 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | auth_tokens.json 2 | node_modules 3 | package-lock.json -------------------------------------------------------------------------------- /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 name might include part of the SN, discard it 19 | ATTR{id}=="OsmoAction5*", ATTR{id}="OsmoAction5", GOTO="signal_belaui" 20 | 21 | # set the id for the first USB audio card that's not a camlink 4K 22 | ATTR{id}="usbaudio" 23 | 24 | LABEL="signal_belaui" 25 | RUN+="/usr/bin/pkill -o -SIGUSR2 -f belaUI.js" 26 | 27 | LABEL="end" 28 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /config.json: -------------------------------------------------------------------------------- 1 | {"password":"changeme"} 2 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | 115 | 116 |
PortIPBitrate
117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 |
SensorMeasurement
127 | 128 | 181 | 182 |
183 | 184 |
185 | 186 |
187 |
188 |
190 | 194 |
195 | 196 |
197 |
198 |
199 | 200 |
201 |
202 | 203 | 204 |
205 |
206 | 207 | 208 |
209 |
210 |
211 | 212 |
213 |
214 |
215 | 216 |
217 |
218 |
219 | 220 | 223 |
224 |
225 |
226 |
227 | 228 |
229 |
231 | 235 |
236 | 237 |
238 |
239 |
240 | 241 | 242 |
243 | 247 |
248 | 249 | 250 |

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

251 |
252 |
253 | 254 | 255 |
256 |
257 | 258 |
259 | 260 |
261 | 262 |
263 |
264 |
265 |
266 |
267 | 268 |
269 |
270 |
271 | 272 |
273 |
274 |
275 |

Recommended latency: 1500-2500ms

276 |

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

277 |
278 |
279 |
280 |
281 | 282 |
283 |
285 | 289 |
290 | 291 |
292 |
293 | 294 |
295 | 296 |
297 | 298 |
299 | 300 | 301 |
302 |
303 | 304 | 307 | 310 |
311 | 312 | 316 | 317 | 321 | 322 |
323 |
324 |
325 | 326 |
327 |
329 | 333 |
334 | 335 |
336 |
337 | 338 |
339 |
340 | 343 |

344 |
345 | 346 |
347 | 348 | 349 |
350 |
351 |
352 |
353 | 354 |
355 |
356 | 359 |
360 | 361 |
362 | 363 | 364 |
365 |
366 |
367 |
368 | 369 |
370 |
371 |
372 | 373 | 376 |
377 |
378 | 379 |
380 |
381 |
382 | 383 | 386 | 387 | 390 | 393 | 396 |
397 | 398 |
399 |
400 |
401 | 402 |
403 |
404 | 405 |
406 | 407 | Theme: 408 | 413 | 414 | 415 | 416 | Slider locks: 417 | 422 | 423 |
424 |
425 | 426 | 427 | 428 | 429 | 430 | 431 | 432 | 433 | -------------------------------------------------------------------------------- /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/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/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 | 198 | for (const i in netifs) { 199 | data = netifs[i]; 200 | tpKbps = Math.round((data['tp'] * 8) / 1024); 201 | totalKbps += tpKbps; 202 | 203 | modemList.push(genNetifEntry(data.error, data.enabled, i, data.ip, `${tpKbps} Kbps`)); 204 | } 205 | 206 | if (Object.keys(netifs).length > 1) { 207 | modemList.push(genNetifEntry(undefined, undefined, '', '', `${totalKbps} Kbps`, true)); 208 | } 209 | 210 | $('#modems').html(modemList); 211 | } 212 | 213 | function updateSensors(sensors) { 214 | const sensorList = []; 215 | 216 | for (const i in sensors) { 217 | data = sensors[i]; 218 | 219 | const entryHtml = ` 220 | 221 | 222 | 223 | `; 224 | const entry = $($.parseHTML(entryHtml)); 225 | entry.find('.sensor_name').text(i); 226 | entry.find('.sensor_value').text(data); 227 | sensorList.push(entry); 228 | } 229 | 230 | $('#sensors').html(sensorList); 231 | } 232 | 233 | 234 | /* Remote status */ 235 | let remoteConnectedHideTimer; 236 | function showRemoteStatus(status) { 237 | if (remoteConnectedHideTimer) { 238 | clearTimeout(remoteConnectedHideTimer); 239 | remoteConnectedHideTimer = undefined; 240 | } 241 | 242 | if (status === true) { 243 | $('#remoteStatus').removeClass('alert-danger'); 244 | $('#remoteStatus').addClass('alert-success'); 245 | $('#remoteStatus').text("BELABOX cloud remote: connected"); 246 | remoteConnectedHideTimer = setTimeout(function() { 247 | $('#remoteStatus').addClass('d-none'); 248 | remoteConnectedHideTimer = undefined; 249 | }, 5000); 250 | } else if (status.error) { 251 | switch(status.error) { 252 | case 'network': 253 | $('#remoteStatus').text("BELABOX cloud remote: network error. Trying to reconnect...\n"); 254 | break; 255 | case 'key': 256 | $('#remoteStatus').text("BELABOX cloud remote: invalid key\n"); 257 | break; 258 | default: 259 | return; 260 | } 261 | 262 | $('#remoteStatus').addClass('alert-danger'); 263 | $('#remoteStatus').removeClass('alert-success'); 264 | } else { 265 | return; 266 | } 267 | $('#remoteStatus').removeClass('d-none'); 268 | } 269 | 270 | 271 | /* Software updates */ 272 | function showSoftwareUpdates(status) { 273 | if (status) { 274 | if (status.package_count) { 275 | $('#softwareUpdate span.desc').text(`(${status.package_count} packages, ${status.download_size})`); 276 | } else { 277 | $('#softwareUpdate span.desc').text('(up to date)'); 278 | } 279 | $('#softwareUpdate').attr('disabled', !status.package_count); 280 | } else if (status === null) { 281 | $('#softwareUpdate span.desc').text('(checking for updates...)'); 282 | $('#softwareUpdate').attr('disabled', true); 283 | } 284 | if (status === false) { 285 | $('#softwareUpdate').addClass('d-none'); 286 | } else { 287 | $('#softwareUpdate').removeClass('d-none'); 288 | } 289 | } 290 | 291 | function showSoftwareUpdateValue(cls, value, total) { 292 | if (value > 0) { 293 | $(`#softwareUpdateStatus .${cls} .value`).text(`${value} / ${total}`); 294 | $(`#softwareUpdateStatus .${cls}`).removeClass('d-none'); 295 | } else { 296 | $(`#softwareUpdateStatus .${cls}`).addClass('d-none'); 297 | } 298 | } 299 | 300 | function showSoftwareUpdateStatus(status) { 301 | if (!status) { 302 | $('#softwareUpdateStatus').addClass('d-none'); 303 | return; 304 | } 305 | 306 | $('#startStop, #softwareUpdate, .command-btn').attr('disabled', status.result === undefined); 307 | 308 | showSoftwareUpdateValue('downloading', status.downloading, status.total); 309 | showSoftwareUpdateValue('unpacking', status.unpacking, status.total); 310 | showSoftwareUpdateValue('setting-up', status.setting_up, status.total); 311 | 312 | if (status.result === 0) { 313 | $('#softwareUpdateStatus p.result').text('Update completed. Restarting the encoder...'); 314 | $('#softwareUpdateStatus p.result').removeClass('text-danger'); 315 | $('#softwareUpdateStatus p.result').addClass('text-success'); 316 | $('#softwareUpdateStatus .result').removeClass('d-none'); 317 | } else if (status.result !== undefined) { 318 | $('#softwareUpdateStatus p.result').text("Update error: " + status.result); 319 | $('#softwareUpdateStatus p.result').removeClass('text-success'); 320 | $('#softwareUpdateStatus p.result').addClass('text-danger'); 321 | $('#softwareUpdateStatus .result').removeClass('d-none'); 322 | } else { 323 | $('#softwareUpdateStatus .result').addClass('d-none'); 324 | } 325 | 326 | $('#softwareUpdateStatus').removeClass('d-none'); 327 | } 328 | 329 | $('#softwareUpdate').click(function() { 330 | const msg = 'Are you sure you want to start a software update? ' + 331 | 'This may take several minutes. ' + 332 | 'You won\'t be able to start a stream until it\'s completed. ' + 333 | 'The encoder will briefly disconnect after a successful upgrade. ' + 334 | 'Never remove power or reset the encoder while updating. If the encoder is powered from a battery, ensure it\'s fully charged.'; 335 | 336 | if (confirm(msg)) { 337 | send_command('update'); 338 | } 339 | }); 340 | 341 | 342 | /* SSH status / control */ 343 | let sshStatus; 344 | function showSshStatus(s) { 345 | if (s !== undefined) { 346 | sshStatus = s; 347 | } 348 | 349 | if (!sshStatus) return; 350 | 351 | const pass = !config.ssh_pass ? 'password not set' : (sshStatus.user_pass ? 'user-set password' : config.ssh_pass) 352 | $('label[for=sshPassword]').text(`SSH password (username: ${sshStatus.user})`); 353 | 354 | $('#sshPassword').val(pass); 355 | if (sshStatus.active) { 356 | $('#startSsh').addClass('d-none'); 357 | $('#stopSsh').removeClass('d-none'); 358 | } else { 359 | $('#stopSsh').addClass('d-none'); 360 | $('#startSsh').removeClass('d-none'); 361 | } 362 | $('#sshSettings').removeClass('d-none'); 363 | } 364 | 365 | $('#resetSshPass').click(function() { 366 | const msg = 'Are you sure you want to reset the SSH password?'; 367 | 368 | if (confirm(msg)) { 369 | send_command('reset_ssh_pass'); 370 | } 371 | }); 372 | 373 | 374 | /* Audio device / codec selection */ 375 | let audioSrcList = []; 376 | function updateAudioSrcs(list) { 377 | if (list !== null) { 378 | audioSrcList = list; 379 | } 380 | 381 | const audioSelect = document.getElementById("audioSource"); 382 | audioSelect.innerText = null; 383 | let asrcFound = false; 384 | 385 | for (const card of audioSrcList) { 386 | const option = document.createElement("option"); 387 | option.value = card; 388 | option.innerText = card; 389 | 390 | audioSelect.append(option); 391 | if (config.asrc && card == config.asrc) { 392 | option.selected = true; 393 | asrcFound = true; 394 | } 395 | } 396 | 397 | if (config.asrc && !asrcFound) { 398 | const option = document.createElement("option"); 399 | option.innerText = config.asrc + " (unavailable)"; 400 | option.value = config.asrc; 401 | option.selected = true; 402 | audioSelect.append(option); 403 | } 404 | } 405 | 406 | let audioCodecList = {}; 407 | function updateAudioCodecs(list) { 408 | if (list !== null) { 409 | audioCodecList = list; 410 | } 411 | 412 | const audioCodec = document.getElementById("audioCodec"); 413 | audioCodec.innerText = null; 414 | 415 | for (const codec in audioCodecList) { 416 | const option = document.createElement("option"); 417 | option.value = codec; 418 | option.innerText = audioCodecList[codec]; 419 | 420 | if (config.acodec && codec == config.acodec) { 421 | option.selected = true; 422 | } 423 | audioCodec.append(option); 424 | } 425 | } 426 | 427 | 428 | /* status updates */ 429 | function updateStatus(status) { 430 | if (status.is_streaming !== undefined) { 431 | isStreaming = status.is_streaming; 432 | if (isStreaming) { 433 | updateButtonAndSettingsShow({ 434 | add: "btn-danger", 435 | remove: "btn-success", 436 | text: "Stop", 437 | enabled: true, 438 | settingsShow: false, 439 | }); 440 | } else { 441 | updateButtonAndSettingsShow({ 442 | add: "btn-success", 443 | remove: "btn-danger", 444 | text: "Start", 445 | enabled: true, 446 | settingsShow: true, 447 | }); 448 | } 449 | } 450 | 451 | if (status.remote) { 452 | showRemoteStatus(status.remote); 453 | } 454 | 455 | if (status.set_password === true) { 456 | showInitialPasswordForm(); 457 | } 458 | 459 | if (status.available_updates !== undefined) { 460 | showSoftwareUpdates(status.available_updates); 461 | } 462 | 463 | if (status.updating !== undefined) { 464 | showSoftwareUpdateStatus(status.updating); 465 | } 466 | 467 | if (status.ssh) { 468 | showSshStatus(status.ssh); 469 | } 470 | 471 | if (status.wifi) { 472 | updateWifiState(status.wifi); 473 | } 474 | 475 | if (status.modems) { 476 | updateModemsState(status.modems); 477 | } 478 | 479 | if (status.asrcs) { 480 | updateAudioSrcs(status.asrcs); 481 | } 482 | } 483 | 484 | 485 | /* Configuration loading */ 486 | function loadConfig(c) { 487 | config = c; 488 | 489 | initBitrateSlider(config.max_br ?? 5000); 490 | initDelaySlider(config.delay ?? 0); 491 | initSrtLatencySlider(config.srt_latency ?? 2000); 492 | updatePipelines(null); 493 | updateAudioSrcs(null); 494 | updateRelays(null); 495 | 496 | const srtlaAddr = config.srtla_addr ?? ""; 497 | showHideRelayHint(srtlaAddr); 498 | $('#srtlaAddr').val(srtlaAddr); 499 | $('#srtlaPort').val(config.srtla_port ?? ""); 500 | $('#srtStreamid').val(config.srt_streamid ?? ""); 501 | 502 | $("#bitrateOverlay").prop('checked', config.bitrate_overlay) 503 | 504 | $('#autoStart').prop('checked', config.autostart ?? false); 505 | $('#autoStartForm button[type=submit]').prop('disabled', true); 506 | $('#remoteDeviceKey').val(config.remote_key); 507 | $('#remoteKeyForm button[type=submit]').prop('disabled', true); 508 | 509 | if (config.ssh_pass && sshStatus) { 510 | showSshStatus(); 511 | } 512 | } 513 | 514 | 515 | /* Pipelines */ 516 | function updateOptionList(select, options, selected) { 517 | const validIds = {}; 518 | 519 | let entriesToDeselect = []; 520 | let entryToSelect; 521 | let prevOption; 522 | 523 | for (const o in options) { 524 | for (const value in options[o]) { 525 | const id = `o_${o}_${value}`; 526 | validIds[id] = true; 527 | 528 | let entry = select.find(`.${id}`); 529 | if (entry.length == 0) { 530 | const html = '' 531 | entry = $($.parseHTML(html)); 532 | entry.addClass(id); 533 | entry.data('option_id', id); 534 | entry.attr('value', value); 535 | 536 | if (prevOption) { 537 | entry.insertAfter(prevOption); 538 | } else { 539 | select.prepend(entry); 540 | } 541 | } 542 | 543 | const contents = options[o][value].name; 544 | if (contents != entry.text()) { 545 | entry.text(contents); 546 | } 547 | const isDisabled = options[o][value].disabled; 548 | if (entry.attr('disabled') != isDisabled) { 549 | entry.attr('disabled', isDisabled); 550 | } 551 | const isSelected = (selected && value == selected); 552 | const wasSelected = entry.attr('selected') == 'selected'; 553 | if (isSelected && !wasSelected) { 554 | entryToSelect = entry; 555 | } 556 | if (!isSelected && wasSelected) { 557 | entriesToDeselect.push(entry); 558 | } 559 | 560 | prevOption = entry; 561 | } 562 | } // for o in options 563 | 564 | // Delete removed options 565 | select.find('option').each(function() { 566 | const option = $(this) 567 | const optionId = option.data('option_id'); 568 | if (optionId && !validIds[optionId]) { 569 | option.remove(); 570 | } 571 | }); 572 | 573 | // Update the selected entry if it's changed 574 | // First, we have to deselect any other entries 575 | for (const e of entriesToDeselect) { 576 | e.attr('selected', false); 577 | } 578 | 579 | if (entryToSelect) { 580 | entryToSelect.attr('selected', true); 581 | } 582 | } 583 | 584 | let pipelines = {}; 585 | function updatePipelines(ps) { 586 | if (ps != null) { 587 | pipelines = ps; 588 | } 589 | 590 | updateOptionList($('#pipelines'), [pipelines], config.pipeline); 591 | 592 | pipelineSelectHandler($('#pipelines').val()) 593 | } 594 | 595 | function pipelineSelectHandler(s) { 596 | const p = pipelines[s]; 597 | if (!p) return; 598 | 599 | if (p.asrc) { 600 | $('#selectAudioSource').removeClass('d-none'); 601 | } else { 602 | $('#selectAudioSource').addClass('d-none'); 603 | } 604 | 605 | if (p.acodec) { 606 | $('#selectAudioCodec').removeClass('d-none'); 607 | } else { 608 | $('#selectAudioCodec').addClass('d-none'); 609 | } 610 | } 611 | 612 | $("#pipelines").change(function(ev) { 613 | pipelineSelectHandler(ev.target.value); 614 | }); 615 | 616 | /* Remote relays config */ 617 | let isValidRelaySelection = true; 618 | function updateRelaySettings() { 619 | if ($('#relayServer').val() == 'manual') { 620 | $('.remote-relay-account').addClass('d-none'); 621 | $('.manual-relay-addr, .manual-streamid').removeClass('d-none'); 622 | isValidRelaySelection = true; 623 | } else { 624 | $('.manual-relay-addr').addClass('d-none'); 625 | $('.remote-relay-account').removeClass('d-none'); 626 | if ($('#relayAccount').val() == 'manual') { 627 | $('.manual-streamid').removeClass('d-none'); 628 | } else { 629 | $('.manual-streamid').addClass('d-none'); 630 | } 631 | isValidRelaySelection = ($('#relayAccount').val() !== null); 632 | } 633 | 634 | if (isValidRelaySelection) { 635 | removeNotification('relay_account_unavailable'); 636 | } else { 637 | showNotification({name: 'relay_account_unavailable', type: 'error', 638 | msg: 'Your selected relay server account is no longer available. ' + 639 | 'Please select a different one to start the stream.'}); 640 | } 641 | updateButtonEnabledDisabled(); 642 | } 643 | $('#relayServer, #relayAccount').change(function() { 644 | updateRelaySettings(); 645 | }); 646 | 647 | let relays; 648 | function updateRelays(r) { 649 | if (r && r.servers && r.accounts) { 650 | relays = r; 651 | } 652 | 653 | const preset = {manual: {name: 'Manual configuration'}}; 654 | 655 | let selectedServer = config.relay_server; 656 | if (!relays || config.srtla_addr || config.srtla_port) { 657 | selectedServer = 'manual'; 658 | } else if (!config.relay_server || !relays.servers[config.relay_server]) { 659 | for (const s in relays.servers) { 660 | if (relays.servers[s].default) { 661 | selectedServer = s; 662 | } 663 | } 664 | } 665 | updateOptionList($('#relayServer'), [relays ? relays.servers : {}, preset], selectedServer); 666 | 667 | let selectedAccount = config.relay_account; 668 | if (!relays || config.srt_streamid !== undefined) { 669 | selectedAccount = 'manual'; 670 | } else if (config.relay_account) { 671 | if (!relays.accounts[config.relay_account]) { 672 | preset['unavailable'] = {name: 'No longer available', disabled: true}; 673 | selectedAccount = 'unavailable'; 674 | } 675 | } 676 | updateOptionList($('#relayAccount'), [relays ? relays.accounts : {}, preset], selectedAccount); 677 | 678 | updateRelaySettings(); 679 | } 680 | 681 | /* Bitrate setting updates */ 682 | function updateBitrate(br) { 683 | $('#bitrateSlider').slider('option', 'value', br.max_br); 684 | showBitrate(br.max_br); 685 | } 686 | 687 | 688 | /* WiFi manager */ 689 | function wifiScan(button, deviceId) { 690 | if (!ws) return; 691 | 692 | // Disable the search button immediately 693 | const wifiManager = $(button).parents('.wifi-settings'); 694 | wifiManager.find('.wifi-scan-button').attr('disabled', true); 695 | 696 | // Send the request 697 | ws.send(JSON.stringify({wifi: {scan: deviceId}})); 698 | 699 | // Duration 700 | const searchDuration = 10000; 701 | 702 | setTimeout(function() { 703 | wifiManager.find('.wifi-scan-button').attr('disabled', false); 704 | wifiManager.find('.scanning').addClass('d-none'); 705 | }, searchDuration); 706 | 707 | wifiManager.find('.connect-error').addClass('d-none'); 708 | wifiManager.find('.scanning').removeClass('d-none'); 709 | } 710 | 711 | function wifiSendNewConnection() { 712 | $('#wifiNewErrAuth').addClass('d-none'); 713 | $('#wifiNewErrGeneric').addClass('d-none'); 714 | $('#wifiNewConnecting').removeClass('d-none'); 715 | 716 | $('#wifiConnectButton').attr('disabled', true); 717 | 718 | const device = $('#connection-device').val(); 719 | const ssid = $('#connection-ssid').val(); 720 | const password = $('#connection-password').val(); 721 | 722 | ws.send(JSON.stringify({ 723 | wifi: { 724 | new: { 725 | device, 726 | ssid, 727 | password 728 | } 729 | } 730 | })); 731 | 732 | return false; 733 | } 734 | 735 | function wifiConnect(e) { 736 | const network = $(e).parents('tr.network').data('network'); 737 | 738 | if (network.active) return; 739 | 740 | if (network.uuid) { 741 | ws.send(JSON.stringify({wifi: {connect: network.uuid}})); 742 | 743 | const wifiManager = $(e).parents('.wifi-settings'); 744 | wifiManager.find('.connect-error').addClass('d-none'); 745 | wifiManager.find('.connecting').removeClass('d-none'); 746 | } else { 747 | if (network.security === "") { 748 | if (confirm(`Connect to the open network ${network.ssid}?`)) { 749 | ws.send(JSON.stringify({ 750 | wifi: { 751 | new: { 752 | ssid: network.ssid, 753 | device: network.device 754 | } 755 | } 756 | })); 757 | } 758 | } else { 759 | if (network.security.match('802.1X')) { 760 | alert("This network uses 802.1X enterprise authentication, " + 761 | "which belaUI doesn't support at the moment"); 762 | } else if (network.security.match('WEP')) { 763 | alert("This network uses legacy WEP authentication, " + 764 | "which belaUI doesn't support"); 765 | } else { 766 | $('#connection-ssid').val(network.ssid); 767 | $('#connection-device').val(network.device); 768 | $('#connection-password').val(''); 769 | $('.wifi-new-status').addClass('d-none'); 770 | $('#wifiConnectButton').attr('disabled', false); 771 | $('#wifiModal').modal({ show: true }); 772 | 773 | setTimeout(() => { 774 | $('#connection-password').focus(); 775 | }, 500); 776 | } 777 | } 778 | } 779 | } 780 | 781 | function wifiDisconnect(e) { 782 | const network = $(e).parents('tr').data('network'); 783 | 784 | if (confirm(`Disconnect from ${network.ssid}?`)) { 785 | ws.send(JSON.stringify({ 786 | wifi: { 787 | disconnect: network.uuid 788 | }, 789 | })); 790 | } 791 | } 792 | 793 | function wifiForget(e) { 794 | const network = $(e).parents('tr').data('network'); 795 | 796 | if (confirm(`Forget network ${network.ssid}?`)) { 797 | ws.send(JSON.stringify({ 798 | wifi: { 799 | forget: network.uuid 800 | }, 801 | })); 802 | } 803 | } 804 | 805 | function wifiFindCardId(deviceId) { 806 | return `wifi-manager-${parseInt(deviceId)}`; 807 | } 808 | 809 | function wifiSignalSymbol(signal) { 810 | if (signal < 0) signal = 0; 811 | if (signal > 100) signal = 100; 812 | const symbol = 9601 + Math.floor(signal / 12.51); 813 | let cl = "text-success"; 814 | if (signal < 40) { 815 | cl = "text-danger"; 816 | } else if (signal < 75) { 817 | cl = "text-warning"; 818 | } 819 | return `&#${symbol}`; 820 | } 821 | 822 | function wifiListAvailableNetwork(device, deviceId, a) { 823 | const savedUuid = device.saved[a.ssid]; 824 | if (savedUuid) { 825 | delete device.saved[a.ssid]; 826 | } 827 | 828 | const html = ` 829 | 830 | 831 | 832 | 833 | 834 | Connected
835 | 836 | 837 | 838 | 847 | 852 | 853 | `; 854 | 855 | const network = $($.parseHTML(html)); 856 | network.find('.signal').html(wifiSignalSymbol(a.signal));// + '%'); 857 | network.find('.band').html((a.freq > 5000) ? '5㎓' : '2.4㎓'); 858 | const ssidEl = network.find('.ssid'); 859 | ssidEl.text(a.ssid); 860 | 861 | network.data('network', {active: a.active, uuid: savedUuid, ssid: a.ssid, device: deviceId, security: a.security}); 862 | 863 | if (a.security != '') { 864 | // show a cross mark for 802.1X or WEP networks (unsupported) 865 | // or a lock symbol for PSK networks (supported) 866 | network.find('.security').html(a.security.match(/802\.1X|WEP/) ? '❌' : '🔒'); 867 | } 868 | if (a.active) { 869 | network.find('.disconnect').removeClass('d-none'); 870 | network.find('.connected').removeClass('d-none'); 871 | } 872 | if (!a.active) { 873 | network.find('.ssid').addClass('can-connect'); 874 | } 875 | if (savedUuid) { 876 | network.find('.forget').removeClass('d-none'); 877 | } 878 | 879 | return network; 880 | } 881 | 882 | function wifiListSavedNetwork(ssid, uuid) { 883 | const html = ` 884 | 885 | 886 | 887 | 892 | 893 | `; 894 | 895 | const network = $($.parseHTML(html)); 896 | network.find('.ssid').text(ssid); 897 | 898 | network.data('network', {ssid, uuid}); 899 | 900 | return network; 901 | } 902 | 903 | function wifiCheckHotspotSettings(deviceId) { 904 | if (!wifiIfs[deviceId] || !wifiIfs[deviceId].hotspot) return; 905 | 906 | const cardId = wifiFindCardId(deviceId); 907 | const form = $(`#${cardId}`).find('.hotspot'); 908 | 909 | let anyValueChanged = false; 910 | let allValuesValid = true; 911 | 912 | const nameInput = form.find('.hotspot-name').val(); 913 | if (nameInput != wifiIfs[deviceId].hotspot.name) { 914 | anyValueChanged = true; 915 | const hint = form.find('.hotspot-name-hint'); 916 | if (nameInput.length < 1 || nameInput.length > 32) { 917 | hint.removeClass('d-none'); 918 | allValuesValid = false; 919 | } else { 920 | hint.addClass('d-none'); 921 | } 922 | } 923 | 924 | const passwordInput = form.find('.hotspot-password').val(); 925 | if (passwordInput != wifiIfs[deviceId].hotspot.password) { 926 | anyValueChanged = true; 927 | const hint = form.find('.hotspot-password-hint'); 928 | if (passwordInput.length < 8 || passwordInput.length > 64) { 929 | hint.removeClass('d-none'); 930 | allValuesValid = false; 931 | } else { 932 | hint.addClass('d-none'); 933 | } 934 | } 935 | 936 | const channelInput = form.find('.hotspot-channel'); 937 | if (channelInput.val() != wifiIfs[deviceId].hotspot.channel) { 938 | anyValueChanged = true; 939 | } 940 | 941 | form.find('.hotspot-config-save').attr('disabled', !anyValueChanged || !allValuesValid) 942 | } 943 | 944 | let wifiIfs = {}; 945 | function updateWifiState(msg) { 946 | for (const i in wifiIfs) { 947 | wifiIfs[i].removed = true; 948 | } 949 | 950 | for (let deviceId in msg) { 951 | // Mark the interface as not removed 952 | if (wifiIfs[deviceId]) { 953 | delete wifiIfs[deviceId].removed; 954 | } 955 | 956 | const cardId = wifiFindCardId(deviceId); 957 | const device = msg[deviceId]; 958 | let deviceCard = $(`#${cardId}`); 959 | 960 | if (deviceCard.length == 0) { 961 | const html = ` 962 |
963 |
964 | 967 |
968 | 969 |
970 |
971 |
972 |

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

973 | 974 |
975 | 976 |

The network name must be between 1 and 32 characters long

977 | 978 |
979 | 980 |
981 | 982 |

The password must be between 8 and 64 characters long

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