Merge remote-tracking branch 'upstream/master'

This commit is contained in:
mofeng-git
2024-11-20 15:18:34 +00:00
166 changed files with 5421 additions and 2645 deletions

View File

@@ -372,6 +372,31 @@
</table>
</div>
</details>
<details>
<summary i18n="kvm_text83">Web UI settings</summary>
<div class="spoiler">
<table class="kv">
<tr>
<td i18n="page-close-ask-switch">Ask page close confirmation:</td>
<td align="right">
<div class="switch-box">
<input checked type="checkbox" id="page-close-ask-switch">
<label for="page-close-ask-switch"><span class="switch-inner"></span><span class="switch"></span></label>
</div>
</td>
</tr>
<tr>
<td i18n="page-full-tab-stream-switch">Expand for the entire tab by default:</td>
<td align="right">
<div class="switch-box">
<input type="checkbox" id="page-full-tab-stream-switch">
<label for="page-full-tab-stream-switch"><span class="switch-inner"></span><span class="switch"></span></label>
</div>
</td>
</tr>
</table>
</div>
</details>
<table class="kv">
<tr class="feature-disabled" id="hid-connect">
<td i18n="hid-connect-switch">Connect HID to Server:</td>
@@ -404,8 +429,8 @@
<td>Connect main USB to Server:</td>
<td align="right">
<div class="switch-box">
<input class="gpio-switch" disabled type="checkbox" id="gpio-switch-__v3_usb_breaker__" data-channel="__v3_usb_breaker__" data-confirm-off="Turning off this switch will disconnect the main USB&lt;br&gt;from the server. Are you sure you want to continue?">
<label for="gpio-switch-__v3_usb_breaker__"><span class="switch-inner"></span><span class="switch"></span></label>
<input class="__gpio-switch-__v3_usb_breaker__ gpio-switch" disabled type="checkbox" id="__gpio-switch-__v3_usb_breaker__" data-channel="__v3_usb_breaker__" data-confirm-off="Turning off this switch will disconnect the main USB&lt;br&gt;from the server. Are you sure you want to continue?">
<label for="__gpio-switch-__v3_usb_breaker__"><span class="switch-inner"></span><span class="switch"></span></label>
</div>
</td>
</tr>
@@ -413,17 +438,8 @@
<td>Enable locator LED:</td>
<td align="right">
<div class="switch-box">
<input class="gpio-switch" disabled type="checkbox" id="gpio-switch-__v4_locator__" data-channel="__v4_locator__" data-confirm-off="">
<label for="gpio-switch-__v4_locator__"><span class="switch-inner"></span><span class="switch"></span></label>
</div>
</td>
</tr>
<tr>
<td i18n="page-close-ask-switch">Ask page close confirmation:</td>
<td align="right">
<div class="switch-box">
<input checked type="checkbox" id="page-close-ask-switch">
<label for="page-close-ask-switch"><span class="switch-inner"></span><span class="switch"></span></label>
<input class="__gpio-switch-__v4_locator__ gpio-switch" disabled type="checkbox" id="__gpio-switch-__v4_locator__" data-channel="__v4_locator__" data-confirm-off="">
<label for="__gpio-switch-__v4_locator__"><span class="switch-inner"></span><span class="switch"></span></label>
</div>
</td>
</tr>
@@ -622,17 +638,6 @@
</td>
</tr>
</table>
<hr>
<table class="kv">
<tr>
<td class="value" i18n="kvm_text71">Note:</td>
<td i18n="kvm_text72">&bull; Don't close the browser page until the upload is complete.</td>
</tr>
<tr>
<td></td>
<td i18n="kvm_text73">&bull; To speed up the upload, close the stream window.</td>
</tr>
</table>
</div>
<div class="hidden" id="msd-uploading-sub">
<hr>
@@ -650,6 +655,19 @@
<div class="progress" id="msd-uploading-progress"><span class="progress-value" id="msd-uploading-progress-value"></span></div>
</div>
</div>
<div class="hidden" id="msd-new-tips">
<hr>
<table class="kv">
<tr>
<td class="value" i18n="kvm_text71">Note:</td>
<td i18n="kvm_text72">&bull; Don't close the browser page until the upload is complete.</td>
</tr>
<tr>
<td></td>
<td i18n="kvm_text73">&bull; To speed up the upload, close the stream window.</td>
</tr>
</table>
</div>
<hr>
<div class="buttons buttons-row">
<button class="row50" disabled id="msd-connect-button" i18n="kvm_text76">Connect drive to Server</button>
@@ -866,7 +884,7 @@
<div id="stream-info"></div>
<button class="window-button-exit-full-tab">&#9660;</button>
<div class="stream-box-offline" id="stream-box"><img id="stream-image" src="/share/png/blank-stream.png">
<video class="hidden" id="stream-video" autoplay playsinline muted></video>
<video class="hidden" id="stream-video" disablePictureInPicture="true" autoplay playsinline muted></video>
<div id="stream-fullscreen-active"></div>
</div>
<div class="keypad" id="stream-mouse-buttons" align="center">
@@ -2033,6 +2051,639 @@
// If you also want to support this project,<br>
// you can donate on <a target="_blank" href="https://www.patreon.com/pikvm">Patreon</a>
or <a target="_blank" href="https://paypal.me/pikvm">Paypal</a>.</span>
<ul>
<li>A. Isenring</li>
<li>Aaron Graubert</li>
<li>Aaron Heise</li>
<li>Aaron Stein</li>
<li>Accalia</li>
<li>Adam Goodbar</li>
<li>Adam S</li>
<li>Adam Stuart</li>
<li>AdamBomb</li>
<li>adipisicing</li>
<li>Adrian Basham</li>
<li>Adrian Popescu</li>
<li>Ahmed Syed</li>
<li>Alberto Bassi</li>
<li>alejandro</li>
<li>Aleksei Brusianskii</li>
<li>Alessio Curri</li>
<li>Alex T</li>
<li>Alex Z</li>
<li>Alexander Karmanov</li>
<li>Alexander Lahuerta</li>
<li>Alexander Martin</li>
<li>Alexander Pankov</li>
<li>Alexandre Jablonski</li>
<li>Alexey Kamenskiy</li>
<li>alm0241</li>
<li>Alok Anand</li>
<li>Alucard</li>
<li>Ananthaneshan Elampoornan</li>
<li>Andreas Marufke</li>
<li>Andreas Schmid</li>
<li>Andrew Brant</li>
<li>Andrew Melton</li>
<li>Andrew Reusch</li>
<li>Andrew Ruan</li>
<li>Andrzej V</li>
<li>Andy</li>
<li>Andy Keys</li>
<li>Anish Patel</li>
<li>Anix</li>
<li>Anonymous</li>
<li>Anthony Junk</li>
<li>Anton Kovalenko</li>
<li>Armen</li>
<li>Aron Green</li>
<li>Aron Perelman</li>
<li>Artem Simonov</li>
<li>Arthur Mayer</li>
<li>Arthur Woimbée</li>
<li>Ashlesh Chaudhari</li>
<li>Asim Shakour</li>
<li>Augusto Becciu</li>
<li>AVS Computer</li>
<li>awkspace</li>
<li>Badal Patel</li>
<li>baddog</li>
<li>Bao Tin Hoang</li>
<li>Bean Co.</li>
<li>Bela Bargel</li>
<li>Belf Igor</li>
<li>Ben Gordon</li>
<li>Ben Scott</li>
<li>Benedikt Heine</li>
<li>Benedikt Meier</li>
<li>Benjamin Frewert</li>
<li>Benjamin Melancon</li>
<li>Benjamin Schwartz</li>
<li>Benjamin Stegmann</li>
<li>Benni Stauder</li>
<li>Bernhard Fitzke</li>
<li>Beu</li>
<li>bikmaek</li>
<li>bitjoe</li>
<li>Bits and Bytes Computers LLC</li>
<li>Bjoern Petsch</li>
<li>Blair Hasler</li>
<li>Blindside</li>
<li>Blue Frog LLC</li>
<li>Bootstrapper - Programmierung erklärt</li>
<li>Bosco</li>
<li>Bradford King</li>
<li>Brainspore Networks</li>
<li>Branden Shaulis</li>
<li>Brandon Daniels</li>
<li>Brian</li>
<li>Brian Moses</li>
<li>Brian T Mulcahy</li>
<li>Brian Vecchiarelli</li>
<li>Brian White</li>
<li>Bruno Gomes</li>
<li>Bryan Adams</li>
<li>Bryan Montgomery</li>
<li>Buzzer</li>
<li>C P ELSE</li>
<li>Calanish</li>
<li>Cameron Hatcher</li>
<li>Cameron Tacklind</li>
<li>Carl Mercier</li>
<li>Carl-Fredrik Johansson</li>
<li>Carlos Eduardo Porter Herrera</li>
<li>Carlos Garcia</li>
<li>Carlos Manuel Torres</li>
<li>cbad536</li>
<li>César Nascimento</li>
<li>CHINATERA LIMITED</li>
<li>Chris Blackmon</li>
<li>Chris Burton</li>
<li>Chris Campbell</li>
<li>Chris Jackson</li>
<li>Chris Lewis</li>
<li>Chris Rizio</li>
<li>Christi King</li>
<li>Christian Schlögl</li>
<li>Christian Svensson</li>
<li>Christof Maluck</li>
<li>Christoph Dette</li>
<li>Christoffer Lund</li>
<li>Christopher Bulla</li>
<li>Christopher Gelatt</li>
<li>Christopher Hearn</li>
<li>Christopher Mandlbaur</li>
<li>Christopher Mendoza</li>
<li>Christopher Simms</li>
<li>Chucktastic</li>
<li>Cihan VURAL</li>
<li>clauskj3r</li>
<li>Clifford Coleman</li>
<li>Clinton Lee Taylor</li>
<li>Cole Imhoff</li>
<li>Colin Goodman</li>
<li>Corey Layton</li>
<li>Corey Lista</li>
<li>Craig Keenan</li>
<li>Crossfactor</li>
<li>Cruzzer</li>
<li>ctag</li>
<li>Curt Sammer</li>
<li>CyB0rgg</li>
<li>DeMentor</li>
<li>Desmond Whitt</li>
<li>Daegara</li>
<li>DailyAneurism</li>
<li>Damon Meledones</li>
<li>Dan Berkowitz</li>
<li>Dan Brakeley</li>
<li>Daniel Bowder</li>
<li>Daniel Cabrera</li>
<li>Daniel Davila</li>
<li>Danilo Saft</li>
<li>Danne</li>
<li>Dariusz Techmański</li>
<li>David</li>
<li>David Brausewetter</li>
<li>David Davis</li>
<li>David Godibadze</li>
<li>David Howell</li>
<li>David Irvine</li>
<li>David Klinkman</li>
<li>David Niemann</li>
<li>David Shay</li>
<li>David Ye</li>
<li>David York</li>
<li>Denis</li>
<li>Denis Andreev</li>
<li>Denis Yatsenko</li>
<li>Dennis Becker</li>
<li>Dennis Joslin</li>
<li>Dennis Lomet</li>
<li>Derek Jarvis</li>
<li>Derek Yap</li>
<li>Didrik</li>
<li>digitalbaconbits</li>
<li>Dimitrij Jedich</li>
<li>dixon wong</li>
<li>dizztrukshin</li>
<li>Dmitry Shilov</li>
<li>DogeLabs</li>
<li>Dominic Phoon</li>
<li>Dominik Klonowski</li>
<li>Donald Hays</li>
<li>Edmon Abdul Nur</li>
<li>Edward Wang</li>
<li>Egan Ford</li>
<li>Elani Ferri</li>
<li>Elliot Woo</li>
<li>Entt</li>
<li>Eric Phenix</li>
<li>Ethan Shold</li>
<li>Eugene Sukhodolin</li>
<li>ewook</li>
<li>eye-catcher.com</li>
<li>Fabian Druschke</li>
<li>Fabiano Sidler</li>
<li>Far Pin Solutions, LLC</li>
<li>Felyx Gabryel</li>
<li>Fergus McKay</li>
<li>Finn Ebenritter</li>
<li>floppy</li>
<li>fo0bar</li>
<li>Foad Yousef</li>
<li>Foamy</li>
<li>Foli Ayivoh</li>
<li>Folkert Weistra</li>
<li>Francisco Pavon</li>
<li>Frank</li>
<li>Frank Sander</li>
<li>Frederick Czajka</li>
<li>Fredrik Idréus</li>
<li>Garrett Dangerfield</li>
<li>Ge Men</li>
<li>Geekworm</li>
<li>Genkinger Andreas</li>
<li>Geijer</li>
<li>Geoffrey Wright</li>
<li>George Becker</li>
<li>Georgy Brodsky</li>
<li>Gerald</li>
<li>Gerardus Vernimmen</li>
<li>Gernot Neuschröer</li>
<li>Giovanni Fulco</li>
<li>GK</li>
<li>Glen Dragon</li>
<li>Greg Winterstein</li>
<li>Gregory Smith</li>
<li>Gregory Treantos</li>
<li>grewil</li>
<li>Grey Cynic</li>
<li>Guenter Honisch</li>
<li>Guido Bernacchi</li>
<li>Gustin Johnson</li>
<li>György Tamás Vizi</li>
<li>Haiberg GmbH</li>
<li>Haven Zheng</li>
<li>Heibunny</li>
<li>Heikki Tiittanen</li>
<li>Helio Leonardo Pinheiro e Mota</li>
<li>Henrik Ählström</li>
<li>Henry Hood</li>
<li>HimKo</li>
<li>HouseFPV</li>
<li>Howard Simons</li>
<li>HyunohRyu</li>
<li>Icculus</li>
<li>iks</li>
<li>INFO TRX INC</li>
<li>Invader Monks</li>
<li>Ioannis Karageorgos</li>
<li>Isaac</li>
<li>IT Lifesaver</li>
<li>Ivan Ganev</li>
<li>Ivan Josiah Lapis</li>
<li>Ivan Shapovalov</li>
<li>iwbjhbweriuhf</li>
<li>J</li>
<li>J L</li>
<li>Jaanus</li>
<li>Jackson Wyatt</li>
<li>Jacob Karaffa</li>
<li>Jacob Morgan</li>
<li>James Cadd</li>
<li>James Cobb</li>
<li>James Edwards</li>
<li>James Kocher</li>
<li>James Mayhugh</li>
<li>James Noonan</li>
<li>James Ye</li>
<li>Jamie Murphy</li>
<li>Jamie Scott</li>
<li>Jan Niehusmann</li>
<li>Jannick Oursin</li>
<li>Jari Hiltunen</li>
<li>Jason Crossley</li>
<li>Jason Downey</li>
<li>Jason Toland</li>
<li>Jasper Backer</li>
<li>Jay Davis</li>
<li>Jay Isaacs</li>
<li>Jazereel Goh</li>
<li>Jean-Daniel Croteau</li>
<li>Jean-Philippe Guilbault</li>
<li>Jeff</li>
<li>Jeff Bowman</li>
<li>Jeff Urlwin</li>
<li>Jennifer Herting</li>
<li>Jennifer Rowlett</li>
<li>Jeremy Abel</li>
<li>Jeremy Combs</li>
<li>Jeremy Hines</li>
<li>Jerremy Holland</li>
<li>Jerry Nall</li>
<li>Jerry Y. Chen</li>
<li>Jim Bailey</li>
<li>Jim Harbin</li>
<li>Jimmy Burgett</li>
<li>Jimmy Stanley</li>
<li>Joachim Bruening</li>
<li>Joe Hanson</li>
<li>Joe Hinteregger</li>
<li>Joe Ventura</li>
<li>Joel Jacobs</li>
<li>Johannes Heigermose</li>
<li>John Andersen</li>
<li>John Copeland</li>
<li>John F Glenn</li>
<li>John Holmes</li>
<li>John Kelley</li>
<li>John McGovern</li>
<li>Johnny Henson</li>
<li>Jon Ferguy</li>
<li>Jon-Eric</li>
<li>Joni Ruuskanen</li>
<li>Jonas Fischer</li>
<li>Jonathan Slenders</li>
<li>Jonathan Vaughn</li>
<li>Joost Backer</li>
<li>Jordan Blake</li>
<li>Jordi Pakey-Rodriguez</li>
<li>Joris van Embden</li>
<li>Joseph Swift</li>
<li>Josh Nethery</li>
<li>Josh Ricker</li>
<li>Josh VanDeraa</li>
<li>Joshua Futterer</li>
<li>Jozef Riha</li>
<li>Jörgen Fredriksson</li>
<li>Julian Forero</li>
<li>Julian Schneider</li>
<li>Julien Angelier</li>
<li>Justin</li>
<li>Justin Waters</li>
<li>Kai Hadler</li>
<li>Kamil Chyba</li>
<li>Kari Matti Korpi</li>
<li>Karl Dunne</li>
<li>Karl Moos</li>
<li>Keith Muggleton</li>
<li>Ken Lee</li>
<li>Kenneth Younger III</li>
<li>Kenny Hui</li>
<li>KeonWoo PARK</li>
<li>Kevin Bajohr</li>
<li>Kevin Schwartz</li>
<li>Kevin Sherwood</li>
<li>Kiera Kujisawa</li>
<li>Kiran Schuler</li>
<li>Koloman</li>
<li>Konrad Neitzel</li>
<li>Krzysztof Żelaśkiewicz</li>
<li>Lance Ward</li>
<li>Larry Meaney</li>
<li>Lars</li>
<li>Lars Reinhardt</li>
<li>Lee Wilkinson</li>
<li>LeeNX</li>
<li>Leon Siegl</li>
<li>Leonard Feineis</li>
<li>Lewis Wild</li>
<li>Liran</li>
<li>Liviu Dimitriu</li>
<li>Lizardo Hernandez</li>
<li>LoCascio</li>
<li>Lordbob75</li>
<li>Lothar Schweikle-Droll</li>
<li>Louis Müller</li>
<li>LSDTripp</li>
<li>Ľubor Slušný</li>
<li>Luca Di Diomede</li>
<li>Lucio De Carli</li>
<li>Luiz Bizzio</li>
<li>Lukas Bischof</li>
<li>Lukas Kammerer</li>
<li>Lukas Söder</li>
<li>Maksim Terehin</li>
<li>Malcolm Cameron</li>
<li>Manfred Radeschnig</li>
<li>Marc Khouri</li>
<li>Marcin Wilk</li>
<li>Marcio Zimbres</li>
<li>Marco Rossi</li>
<li>Marcos Wolf</li>
<li>Marek Marczykowski-Górecki</li>
<li>Marius</li>
<li>Mar. Balske</li>
<li>Mark Farrell</li>
<li>Mark Gilbert</li>
<li>Mark Knam</li>
<li>Mark Robinson</li>
<li>Markrosoft</li>
<li>Markus Halm</li>
<li>Markus Schicker</li>
<li>Markus Sobczack</li>
<li>Marshall Bjerke</li>
<li>Marten Hermans</li>
<li>Martin Gasser</li>
<li>Martin Hofbauer</li>
<li>Martin Raine</li>
<li>Martin Suelmann</li>
<li>Martin Wilhelmi</li>
<li>Marvin Honderboom</li>
<li>Mateusz Grabowski</li>
<li>Mathias Uhl</li>
<li>Matt Kane</li>
<li>Matthew Cameron</li>
<li>Mauricio Allende</li>
<li>Max Evans</li>
<li>Mecky</li>
<li>Mehmet Aydoğdu</li>
<li>Michael Bartholomew</li>
<li>Michael Bell</li>
<li>Michael Bombe</li>
<li>Michael Collins</li>
<li>Michael Copeland</li>
<li>Michael Ho</li>
<li>Michael Kovacs</li>
<li>Michael Lynch</li>
<li>Michael Pennington</li>
<li>Michael Sage</li>
<li>Michael Stella</li>
<li>Michael Thalmann</li>
<li>Michael Wu</li>
<li>MichaelZ</li>
<li>Michel Bissonnette</li>
<li>Mikael Wikström</li>
<li>Mike Mason</li>
<li>Mikhael Mariano</li>
<li>Milan Múčka</li>
<li>Miles Davis</li>
<li>Minh Tang</li>
<li>Moez Tharani</li>
<li>Morgan Helton</li>
<li>Myron Weber</li>
<li>Murad Khasawneh</li>
<li>N Patel</li>
<li>Nathaniel Griswold</li>
<li>Nelson Lee</li>
<li>nezu</li>
<li>Nicholas Jeppson</li>
<li>Nicholas Kopas</li>
<li>Nicholas Walczak</li>
<li>Nick Leffler</li>
<li>Nick Roethemeier</li>
<li>Nico Baumgartner</li>
<li>Nicolai Kragh-Hansen</li>
<li>Nigel Smith</li>
<li>Nihal Fernando</li>
<li>Nils Orbat</li>
<li>Nis Wechselberg</li>
<li>Nithin Philips</li>
<li>Nod Swal</li>
<li>Nolan Haynes</li>
<li>nubbn</li>
<li>nybble</li>
<li>Oh Be</li>
<li>Oliver Schwarz</li>
<li>Oliver Zimmer</li>
<li>Omar El-Domeiri</li>
<li>Omar Siam</li>
<li>Oscar</li>
<li>Patrick</li>
<li>Patrick Fortin-Ducharme</li>
<li>Patrick McDowell</li>
<li>Patrick Wagstrom</li>
<li>Paul Bishop</li>
<li>Paul De La Rosa</li>
<li>Paul Pietkiewicz</li>
<li>Paul Tan</li>
<li>Pawel Trofimiuk</li>
<li>Peder Madsen</li>
<li>Peter</li>
<li>Peter Drayton</li>
<li>Peter Farrelly</li>
<li>Peter Okelmann</li>
<li>Petra Lohmann</li>
<li>Petri Heiskanen</li>
<li>Phil Wu</li>
<li>Philip Edwards</li>
<li>Philip Merricks</li>
<li>Pierre Brassart</li>
<li>Pierre Peine</li>
<li>posicat</li>
<li>pozitron03</li>
<li>Przemysław Szypowicz</li>
<li>P_Dmitrij</li>
<li>Qteal</li>
<li>Quattro Uno</li>
<li>Quentin Peten</li>
<li>Ralph Borchers</li>
<li>Ranc1d</li>
<li>Randall D Bilbrey</li>
<li>RandomJerk</li>
<li>Raphael Schitz</li>
<li>Ref Chowdhury</li>
<li>René Rathenau</li>
<li>ReysDad</li>
<li>Ricardo Marques</li>
<li>Richard</li>
<li>Richard Bernarts</li>
<li>Richard Fancher</li>
<li>Richard Freemantle</li>
<li>Richard Michael</li>
<li>Rico Cantrell</li>
<li>Rob</li>
<li>Rob Holden</li>
<li>Rob Tongue</li>
<li>Robert Klauco</li>
<li>Robert Weemhoff</li>
<li>Robin Gfatter</li>
<li>Rodion DENISYUK</li>
<li>Rohit Priyadarshi</li>
<li>Rolfs 3D UG</li>
<li>Ronald LeBaron</li>
<li>Ronald Wells</li>
<li>Ronny Haldorsen</li>
<li>rotx</li>
<li>Rufo Sanchez</li>
<li>Russell Scott</li>
<li>Ryan</li>
<li>Ryan Peacock</li>
<li>Samed Ozoglu</li>
<li>Sameul Davies</li>
<li>Samuel Cote</li>
<li>Samuel Vetsch</li>
<li>Samuel Walker</li>
<li>Sarah Foster</li>
<li>Sarten X</li>
<li>Satish Alwani</li>
<li>Scott</li>
<li>Scott Gagon</li>
<li>Scott Spicola</li>
<li>Scott Tusing</li>
<li>Scott Worthington</li>
<li>Scuba</li>
<li>Sean</li>
<li>Sean Akers</li>
<li>SEAT</li>
<li>Sebastian</li>
<li>Seonwoo Lee</li>
<li>Sergey Lukjanov</li>
<li>Seth Jennings</li>
<li>Shane Selling</li>
<li>Shawn Butts</li>
<li>Sheran Gunasekera</li>
<li>Shichun Chen</li>
<li>Shin Guey Wong</li>
<li>Simon Evans</li>
<li>Simon Sundgaard</li>
<li>Simplistic Realities</li>
<li>Sirmo</li>
<li>Snowy Maslov</li>
<li>Solve Technology</li>
<li>srepac</li>
<li>Stefan Bautz</li>
<li>Stefan Müller</li>
<li>Stefan Stemmer</li>
<li>Stefan Vaillant</li>
<li>Stephan Schmidt</li>
<li>Stephen</li>
<li>Stephen Hocking</li>
<li>Steve Jones</li>
<li>Steve Kerr</li>
<li>Steve Ovens</li>
<li>Steve Stringham</li>
<li>Steven Richter</li>
<li>Stratagem Solutions Ltd</li>
<li>Sven Breckler</li>
<li>sudo34</li>
<li>SuperHiTech</li>
<li>Tango_Echo_Alpha</li>
<li>Tarlak Desaydrone</li>
<li>TechBear</li>
<li>techlobo</li>
<li>Ted</li>
<li>Tejun Heo</li>
<li>TheSnowedOne</li>
<li>TheTechGiant</li>
<li>Thomas Charisoulis</li>
<li>Thomas Gitlin</li>
<li>Thomas Hagenmaier</li>
<li>Thomas Hedberg Jensen</li>
<li>Thomas Price</li>
<li>Thomas Søfteland</li>
<li>Tim Lenz</li>
<li>Tim Wilkinson</li>
<li>Timo Brinkmann</li>
<li>Timothee Besset</li>
<li>TitomusPrime</li>
<li>Tobias Schafferhans</li>
<li>Tom Lawson</li>
<li>Tom York</li>
<li>Tomas Kuchta</li>
<li>Tomáš hrubý</li>
<li>Torsten Droste</li>
<li>Torsten Knoll</li>
<li>Tracy Fitch</li>
<li>Tristan Schoening</li>
<li>Truman Kilen</li>
<li>turbochris</li>
<li>tutanak</li>
<li>Tyler</li>
<li>Udo Schroeter</li>
<li>Uli Fahrer</li>
<li>Vasily Lazarev</li>
<li>Vidru Eduard</li>
<li>Vicente Salvador Cubedo</li>
<li>Viktor Aschenbrenner</li>
<li>Viktor Ekmark</li>
<li>Vincent Chov</li>
<li>Vlad Sterescu</li>
<li>Volker Gropp</li>
<li>Walli</li>
<li>Walter_Ego</li>
<li>William Wenzel</li>
<li>Will Froning</li>
<li>William Hooper</li>
<li>William Perrin</li>
<li>William Stearns</li>
<li>Woojin Son</li>
<li>xMdb</li>
<li>Yanko Kaneti</li>
<li>Yaroslav Kulikovskikh</li>
<li>Yethal</li>
<li>Yevgeniy Kuksenko</li>
<li>Yew Kay Yan</li>
<li>Yigal Dar</li>
<li>Yogi</li>
<li>YURI LEE</li>
<li>Yurii Ostapchuk</li>
<li>Zeljko</li>
<li>zgen</li>
<li>Zoltan Magyari</li>
<li>Zsombor Vari</li>
</ul>
</div>
</div>
</div><br>
@@ -2049,9 +2700,17 @@
</div>
<iframe id="webterm-iframe" src="" style="width: 100%; height: 100%"></iframe>
</div>
<ul class="navbar-bg-tips">
<li class="left">
<pre id="kvmd-meta-tips-left"></pre>
</li>
<li class="right">
<pre id="kvmd-meta-tips-right"></pre>
</li>
</ul>
<ul class="footer">
<li class="footer-left"><span id="kvmd-meta-server-host" title="Server name (see System/About)"></span>&nbsp; | &nbsp;<span id="kvmd-version-kvmd" title="KVMD version"></span>&nbsp; | &nbsp;<span id="kvmd-version-streamer" title="Streamer version"></span></li>
<li class="footer-right"><a target="_blank" href="https://pikvm.org" i18n="index_text_10">PiKVM Project</a>&nbsp; | &nbsp;<a target="_blank" href="https://docs.pikvm.org" i18n="index_text_11">Documentation</a>&nbsp; | &nbsp;<a target="_blank" href="https://github.com/mofeng-git/One-KVM" i18n="index_text_12">One-KVM Project</a>&nbsp; | &nbsp;<a target="_blank" href="https://one-kvm.mofeng.run" i18n="index_text_13">One-KVM Documentation</a></li>
<li class="left"><span id="kvmd-meta-server-host" title="Server name (see System/About)"></span>&nbsp; | &nbsp;<span id="kvmd-version-kvmd" title="KVMD version"></span>&nbsp; | &nbsp;<span id="kvmd-version-streamer" title="Streamer version"></span></li>
<li class="right"><a target="_blank" href="https://pikvm.org" i18n="index_text_10">PiKVM Project</a>&nbsp; | &nbsp;<a target="_blank" href="https://docs.pikvm.org" i18n="index_text_11">Documentation</a>&nbsp; | &nbsp;<a target="_blank" href="https://github.com/mofeng-git/One-KVM" i18n="index_text_12">One-KVM Project</a>&nbsp; | &nbsp;<a target="_blank" href="https://one-kvm.mofeng.run" i18n="index_text_13">One-KVM Documentation</a></li>
</ul>
</body>
</html>

View File

@@ -11,14 +11,20 @@ block body
include navbar.pug
include windows.pug
ul(class="navbar-bg-tips")
li(class="left")
pre(id="kvmd-meta-tips-left")
li(class="right")
pre(id="kvmd-meta-tips-right")
ul(class="footer")
li(class="footer-left")
li(class="left")
span(id="kvmd-meta-server-host" title="Server name (see System/About)")
| &nbsp; | &nbsp;
span(id="kvmd-version-kvmd" title="KVMD version")
| &nbsp; | &nbsp;
span(id="kvmd-version-streamer" title="Streamer version")
li(class="footer-right")
li(class="right")
a(target="_blank" href="https://pikvm.org" i18n="index_text_10") PiKVM Project
| &nbsp; | &nbsp;
a(target="_blank" href="https://docs.pikvm.org" i18n="index_text_11") Documentation

View File

@@ -72,14 +72,6 @@ li(id="msd-dropdown" class="right feature-disabled")
tr(id="msd-new-part" class="hidden")
td(i18n="kvm_text70") Upload partition:
td(width="100%") #[select(id="msd-new-part-selector")]
hr
table(class="kv")
tr
td(class="value" i18n="kvm_text71") Note:
td(i18n="kvm_text72") &bull; Don't close the browser page until the upload is complete.
tr
td
td(i18n="kvm_text73") &bull; To speed up the upload, close the stream window.
div(id="msd-uploading-sub" class="hidden")
hr
table(class="kv")
@@ -92,6 +84,15 @@ li(id="msd-dropdown" class="right feature-disabled")
div(class="text")
div(id="msd-uploading-progress" class="progress")
span(id="msd-uploading-progress-value" class="progress-value")
div(id="msd-new-tips" class="hidden")
hr
table(class="kv")
tr
td(class="value" i18n="kvm_text71") Note:
td(i18n="kvm_text72") &bull; Don't close the browser page until the upload is complete.
tr
td
td(i18n="kvm_text73") &bull; To speed up the upload, close the stream window.
hr
div(class="buttons buttons-row")
button(disabled id="msd-connect-button" class="row50" i18n="kvm_text76") Connect drive to Server

View File

@@ -120,6 +120,14 @@ li(id="system-dropdown" class="right")
td(id="hid-mouse-scroll-value" class="value-number")
tr
+menu_switch_notable("hid-mouse-dot-switch", "Show the blue dot", true, true, "hid-mouse-dot-switch")
details
summary(i18n="kvm_text83") Web UI settings
div(class="spoiler")
table(class="kv")
tr
+menu_switch_notable("page-close-ask-switch", "Ask page close confirmation", true, true, "page-close-ask-switch")
tr
+menu_switch_notable("page-full-tab-stream-switch", "Expand for the entire tab by default", true, false,"page-full-tab-stream-switch")
table(class="kv")
tr(id="hid-connect" class="feature-disabled")
+menu_switch_notable("hid-connect-switch", "Connect HID to Server", true, true, "hid-connect-switch")
@@ -132,8 +140,6 @@ li(id="system-dropdown" class="right")
"Turning off this switch will disconnect the main USB<br>from the server. Are you sure you want to continue?")
tr(id="v4-locator" class="feature-disabled")
+menu_switch_notable_gpio("__v4_locator__", "Enable locator LED")
tr
+menu_switch_notable("page-close-ask-switch", "Ask page close confirmation", true, true, "page-close-ask-switch")
hr
div(class="buttons buttons-row")
button(data-force-hide-menu data-show-window="keyboard-window" class="row50" i18n="kvm_text30") &bull; Show keyboard

View File

@@ -17,9 +17,9 @@ mixin menu_switch_notable_gpio(channel, title, confirm_off="")
td !{title}:
td(align="right")
div(class="switch-box")
input(disabled type="checkbox" id=`gpio-switch-${channel}` class="gpio-switch"
input(disabled type="checkbox" id=`__gpio-switch-${channel}` class=`__gpio-switch-${channel} gpio-switch`
data-channel=channel data-confirm-off=confirm_off)
label(for=`gpio-switch-${channel}`)
label(for=`__gpio-switch-${channel}`)
span(class="switch-inner")
span(class="switch")

View File

@@ -41,6 +41,638 @@ div(id="about-window" class="window")
| // If you also want to support this project,#[br]
| // you can donate on #[a(target="_blank" href="https://www.patreon.com/pikvm") Patreon]
| or #[a(target="_blank" href="https://paypal.me/pikvm") Paypal].
ul
li A. Isenring
li Aaron Graubert
li Aaron Heise
li Aaron Stein
li Accalia
li Adam Goodbar
li Adam S
li Adam Stuart
li AdamBomb
li adipisicing
li Adrian Basham
li Adrian Popescu
li Ahmed Syed
li Alberto Bassi
li alejandro
li Aleksei Brusianskii
li Alessio Curri
li Alex T
li Alex Z
li Alexander Karmanov
li Alexander Lahuerta
li Alexander Martin
li Alexander Pankov
li Alexandre Jablonski
li Alexey Kamenskiy
li alm0241
li Alok Anand
li Alucard
li Ananthaneshan Elampoornan
li Andreas Marufke
li Andreas Schmid
li Andrew Brant
li Andrew Melton
li Andrew Reusch
li Andrew Ruan
li Andrzej V
li Andy
li Andy Keys
li Anish Patel
li Anix
li Anonymous
li Anthony Junk
li Anton Kovalenko
li Armen
li Aron Green
li Aron Perelman
li Artem Simonov
li Arthur Mayer
li Arthur Woimbée
li Ashlesh Chaudhari
li Asim Shakour
li Augusto Becciu
li AVS Computer
li awkspace
li Badal Patel
li baddog
li Bao Tin Hoang
li Bean Co.
li Bela Bargel
li Belf Igor
li Ben Gordon
li Ben Scott
li Benedikt Heine
li Benedikt Meier
li Benjamin Frewert
li Benjamin Melancon
li Benjamin Schwartz
li Benjamin Stegmann
li Benni Stauder
li Bernhard Fitzke
li Beu
li bikmaek
li bitjoe
li Bits and Bytes Computers LLC
li Bjoern Petsch
li Blair Hasler
li Blindside
li Blue Frog LLC
li Bootstrapper - Programmierung erklärt
li Bosco
li Bradford King
li Brainspore Networks
li Branden Shaulis
li Brandon Daniels
li Brian
li Brian Moses
li Brian T Mulcahy
li Brian Vecchiarelli
li Brian White
li Bruno Gomes
li Bryan Adams
li Bryan Montgomery
li Buzzer
li C P ELSE
li Calanish
li Cameron Hatcher
li Cameron Tacklind
li Carl Mercier
li Carl-Fredrik Johansson
li Carlos Eduardo Porter Herrera
li Carlos Garcia
li Carlos Manuel Torres
li cbad536
li César Nascimento
li CHINATERA LIMITED
li Chris Blackmon
li Chris Burton
li Chris Campbell
li Chris Jackson
li Chris Lewis
li Chris Rizio
li Christi King
li Christian Schlögl
li Christian Svensson
li Christof Maluck
li Christoph Dette
li Christoffer Lund
li Christopher Bulla
li Christopher Gelatt
li Christopher Hearn
li Christopher Mandlbaur
li Christopher Mendoza
li Christopher Simms
li Chucktastic
li Cihan VURAL
li clauskj3r
li Clifford Coleman
li Clinton Lee Taylor
li Cole Imhoff
li Colin Goodman
li Corey Layton
li Corey Lista
li Craig Keenan
li Crossfactor
li Cruzzer
li ctag
li Curt Sammer
li CyB0rgg
li DeMentor
li Desmond Whitt
li Daegara
li DailyAneurism
li Damon Meledones
li Dan Berkowitz
li Dan Brakeley
li Daniel Bowder
li Daniel Cabrera
li Daniel Davila
li Danilo Saft
li Danne
li Dariusz Techmański
li David
li David Brausewetter
li David Davis
li David Godibadze
li David Howell
li David Irvine
li David Klinkman
li David Niemann
li David Shay
li David Ye
li David York
li Denis
li Denis Andreev
li Denis Yatsenko
li Dennis Becker
li Dennis Joslin
li Dennis Lomet
li Derek Jarvis
li Derek Yap
li Didrik
li digitalbaconbits
li Dimitrij Jedich
li dixon wong
li dizztrukshin
li Dmitry Shilov
li DogeLabs
li Dominic Phoon
li Dominik Klonowski
li Donald Hays
li Edmon Abdul Nur
li Edward Wang
li Egan Ford
li Elani Ferri
li Elliot Woo
li Entt
li Eric Phenix
li Ethan Shold
li Eugene Sukhodolin
li ewook
li eye-catcher.com
li Fabian Druschke
li Fabiano Sidler
li Far Pin Solutions, LLC
li Felyx Gabryel
li Fergus McKay
li Finn Ebenritter
li floppy
li fo0bar
li Foad Yousef
li Foamy
li Foli Ayivoh
li Folkert Weistra
li Francisco Pavon
li Frank
li Frank Sander
li Frederick Czajka
li Fredrik Idréus
li Garrett Dangerfield
li Ge Men
li Geekworm
li Genkinger Andreas
li Geijer
li Geoffrey Wright
li George Becker
li Georgy Brodsky
li Gerald
li Gerardus Vernimmen
li Gernot Neuschröer
li Giovanni Fulco
li GK
li Glen Dragon
li Greg Winterstein
li Gregory Smith
li Gregory Treantos
li grewil
li Grey Cynic
li Guenter Honisch
li Guido Bernacchi
li Gustin Johnson
li György Tamás Vizi
li Haiberg GmbH
li Haven Zheng
li Heibunny
li Heikki Tiittanen
li Helio Leonardo Pinheiro e Mota
li Henrik Ählström
li Henry Hood
li HimKo
li HouseFPV
li Howard Simons
li HyunohRyu
li Icculus
li iks
li INFO TRX INC
li Invader Monks
li Ioannis Karageorgos
li Isaac
li IT Lifesaver
li Ivan Ganev
li Ivan Josiah Lapis
li Ivan Shapovalov
li iwbjhbweriuhf
li J
li J L
li Jaanus
li Jackson Wyatt
li Jacob Karaffa
li Jacob Morgan
li James Cadd
li James Cobb
li James Edwards
li James Kocher
li James Mayhugh
li James Noonan
li James Ye
li Jamie Murphy
li Jamie Scott
li Jan Niehusmann
li Jannick Oursin
li Jari Hiltunen
li Jason Crossley
li Jason Downey
li Jason Toland
li Jasper Backer
li Jay Davis
li Jay Isaacs
li Jazereel Goh
li Jean-Daniel Croteau
li Jean-Philippe Guilbault
li Jeff
li Jeff Bowman
li Jeff Urlwin
li Jennifer Herting
li Jennifer Rowlett
li Jeremy Abel
li Jeremy Combs
li Jeremy Hines
li Jerremy Holland
li Jerry Nall
li Jerry Y. Chen
li Jim Bailey
li Jim Harbin
li Jimmy Burgett
li Jimmy Stanley
li Joachim Bruening
li Joe Hanson
li Joe Hinteregger
li Joe Ventura
li Joel Jacobs
li Johannes Heigermose
li John Andersen
li John Copeland
li John F Glenn
li John Holmes
li John Kelley
li John McGovern
li Johnny Henson
li Jon Ferguy
li Jon-Eric
li Joni Ruuskanen
li Jonas Fischer
li Jonathan Slenders
li Jonathan Vaughn
li Joost Backer
li Jordan Blake
li Jordi Pakey-Rodriguez
li Joris van Embden
li Joseph Swift
li Josh Nethery
li Josh Ricker
li Josh VanDeraa
li Joshua Futterer
li Jozef Riha
li Jörgen Fredriksson
li Julian Forero
li Julian Schneider
li Julien Angelier
li Justin
li Justin Waters
li Kai Hadler
li Kamil Chyba
li Kari Matti Korpi
li Karl Dunne
li Karl Moos
li Keith Muggleton
li Ken Lee
li Kenneth Younger III
li Kenny Hui
li KeonWoo PARK
li Kevin Bajohr
li Kevin Schwartz
li Kevin Sherwood
li Kiera Kujisawa
li Kiran Schuler
li Koloman
li Konrad Neitzel
li Krzysztof Żelaśkiewicz
li Lance Ward
li Larry Meaney
li Lars
li Lars Reinhardt
li Lee Wilkinson
li LeeNX
li Leon Siegl
li Leonard Feineis
li Lewis Wild
li Liran
li Liviu Dimitriu
li Lizardo Hernandez
li LoCascio
li Lordbob75
li Lothar Schweikle-Droll
li Louis Müller
li LSDTripp
li Ľubor Slušný
li Luca Di Diomede
li Lucio De Carli
li Luiz Bizzio
li Lukas Bischof
li Lukas Kammerer
li Lukas Söder
li Maksim Terehin
li Malcolm Cameron
li Manfred Radeschnig
li Marc Khouri
li Marcin Wilk
li Marcio Zimbres
li Marco Rossi
li Marcos Wolf
li Marek Marczykowski-Górecki
li Marius
li Mar. Balske
li Mark Farrell
li Mark Gilbert
li Mark Knam
li Mark Robinson
li Markrosoft
li Markus Halm
li Markus Schicker
li Markus Sobczack
li Marshall Bjerke
li Marten Hermans
li Martin Gasser
li Martin Hofbauer
li Martin Raine
li Martin Suelmann
li Martin Wilhelmi
li Marvin Honderboom
li Mateusz Grabowski
li Mathias Uhl
li Matt Kane
li Matthew Cameron
li Mauricio Allende
li Max Evans
li Mecky
li Mehmet Aydoğdu
li Michael Bartholomew
li Michael Bell
li Michael Bombe
li Michael Collins
li Michael Copeland
li Michael Ho
li Michael Kovacs
li Michael Lynch
li Michael Pennington
li Michael Sage
li Michael Stella
li Michael Thalmann
li Michael Wu
li MichaelZ
li Michel Bissonnette
li Mikael Wikström
li Mike Mason
li Mikhael Mariano
li Milan Múčka
li Miles Davis
li Minh Tang
li Moez Tharani
li Morgan Helton
li Myron Weber
li Murad Khasawneh
li N Patel
li Nathaniel Griswold
li Nelson Lee
li nezu
li Nicholas Jeppson
li Nicholas Kopas
li Nicholas Walczak
li Nick Leffler
li Nick Roethemeier
li Nico Baumgartner
li Nicolai Kragh-Hansen
li Nigel Smith
li Nihal Fernando
li Nils Orbat
li Nis Wechselberg
li Nithin Philips
li Nod Swal
li Nolan Haynes
li nubbn
li nybble
li Oh Be
li Oliver Schwarz
li Oliver Zimmer
li Omar El-Domeiri
li Omar Siam
li Oscar
li Patrick
li Patrick Fortin-Ducharme
li Patrick McDowell
li Patrick Wagstrom
li Paul Bishop
li Paul De La Rosa
li Paul Pietkiewicz
li Paul Tan
li Pawel Trofimiuk
li Peder Madsen
li Peter
li Peter Drayton
li Peter Farrelly
li Peter Okelmann
li Petra Lohmann
li Petri Heiskanen
li Phil Wu
li Philip Edwards
li Philip Merricks
li Pierre Brassart
li Pierre Peine
li posicat
li pozitron03
li Przemysław Szypowicz
li P_Dmitrij
li Qteal
li Quattro Uno
li Quentin Peten
li Ralph Borchers
li Ranc1d
li Randall D Bilbrey
li RandomJerk
li Raphael Schitz
li Ref Chowdhury
li René Rathenau
li ReysDad
li Ricardo Marques
li Richard
li Richard Bernarts
li Richard Fancher
li Richard Freemantle
li Richard Michael
li Rico Cantrell
li Rob
li Rob Holden
li Rob Tongue
li Robert Klauco
li Robert Weemhoff
li Robin Gfatter
li Rodion DENISYUK
li Rohit Priyadarshi
li Rolfs 3D UG
li Ronald LeBaron
li Ronald Wells
li Ronny Haldorsen
li rotx
li Rufo Sanchez
li Russell Scott
li Ryan
li Ryan Peacock
li Samed Ozoglu
li Sameul Davies
li Samuel Cote
li Samuel Vetsch
li Samuel Walker
li Sarah Foster
li Sarten X
li Satish Alwani
li Scott
li Scott Gagon
li Scott Spicola
li Scott Tusing
li Scott Worthington
li Scuba
li Sean
li Sean Akers
li SEAT
li Sebastian
li Seonwoo Lee
li Sergey Lukjanov
li Seth Jennings
li Shane Selling
li Shawn Butts
li Sheran Gunasekera
li Shichun Chen
li Shin Guey Wong
li Simon Evans
li Simon Sundgaard
li Simplistic Realities
li Sirmo
li Snowy Maslov
li Solve Technology
li srepac
li Stefan Bautz
li Stefan Müller
li Stefan Stemmer
li Stefan Vaillant
li Stephan Schmidt
li Stephen
li Stephen Hocking
li Steve Jones
li Steve Kerr
li Steve Ovens
li Steve Stringham
li Steven Richter
li Stratagem Solutions Ltd
li Sven Breckler
li sudo34
li SuperHiTech
li Tango_Echo_Alpha
li Tarlak Desaydrone
li TechBear
li techlobo
li Ted
li Tejun Heo
li TheSnowedOne
li TheTechGiant
li Thomas Charisoulis
li Thomas Gitlin
li Thomas Hagenmaier
li Thomas Hedberg Jensen
li Thomas Price
li Thomas Søfteland
li Tim Lenz
li Tim Wilkinson
li Timo Brinkmann
li Timothee Besset
li TitomusPrime
li Tobias Schafferhans
li Tom Lawson
li Tom York
li Tomas Kuchta
li Tomáš hrubý
li Torsten Droste
li Torsten Knoll
li Tracy Fitch
li Tristan Schoening
li Truman Kilen
li turbochris
li tutanak
li Tyler
li Udo Schroeter
li Uli Fahrer
li Vasily Lazarev
li Vidru Eduard
li Vicente Salvador Cubedo
li Viktor Aschenbrenner
li Viktor Ekmark
li Vincent Chov
li Vlad Sterescu
li Volker Gropp
li Walli
li Walter_Ego
li William Wenzel
li Will Froning
li William Hooper
li William Perrin
li William Stearns
li Woojin Son
li xMdb
li Yanko Kaneti
li Yaroslav Kulikovskikh
li Yethal
li Yevgeniy Kuksenko
li Yew Kay Yan
li Yigal Dar
li Yogi
li YURI LEE
li Yurii Ostapchuk
li Zeljko
li zgen
li Zoltan Magyari
li Zsombor Vari
br
p(class="text credits")
a(target="_blank" href="https://pikvm.org") PiKVM Project

View File

@@ -15,7 +15,7 @@ div(id="stream-window" class="window window-resizable")
button(class="window-button-exit-full-tab") &#9660;
div(id="stream-box" class="stream-box-offline")
img(id="stream-image" src=`${png_dir}/blank-stream.png`)
video(id="stream-video" class="hidden" autoplay playsinline muted)
video(id="stream-video" class="hidden" disablePictureInPicture="true" autoplay playsinline muted)
div(id="stream-fullscreen-active")
div(id="stream-mouse-buttons" class="keypad" align="center")

View File

@@ -94,7 +94,7 @@
</div>
</form>
<ul class="footer">
<li class="footer-left" i18n="footer-left">This site is actively using JavaScript.<br>
<li class="left" i18n="footer-left">This site is actively using JavaScript.<br>
It doesn't contain ads, but is blocked by some ad filters.<br>
Please turn it off to continue and reload the page.
</li>

View File

@@ -33,7 +33,7 @@ block body
td #[button(id="login-button" class="key" i18n="login") Login]
ul(class="footer")
li(class="footer-left" i18n="footer-left")
li(class="left" i18n="footer-left")
| This site is actively using JavaScript.#[br]
| It doesn't contain ads, but is blocked by some ad filters.#[br]
| Please turn it off to continue and reload the page.

View File

@@ -24,28 +24,6 @@ div#text-menu {
width: 340px;
}
textarea#hid-pak-text {
display: block;
resize: none;
height: 120px;
width: 100%;
border: var(--border-default-thin);
border-radius: 4px;
color: var(--cs-code-default-fg);
background-color: var(--cs-code-default-bg);
-webkit-appearance:none;
}
textarea#hid-pak-text::-moz-placeholder {
line-height: 60px;
text-align: center;
}
textarea#hid-pak-text::-webkit-input-placeholder {
line-height: 60px;
text-align: center;
}
input#hid-recorder-new-script-file {
display: none;
}

View File

@@ -123,6 +123,26 @@ select {
display: block;
width: 100%;
padding-left: 5px;
}
select[size] {
height: auto;
padding: 5px;
}
select[size]::-webkit-scrollbar {
width: 8px;
height: 8px;
}
select[size]::-webkit-scrollbar-thumb {
border-radius: 4px;
background: var(--cs-scroll-default-bg);
}
@-moz-document url-prefix() {
select[size] {
scrollbar-width: 8px;
scrollbar-color: var(--cs-scroll-default-bg) var(--cs-code-default-bg);
}
}
select:not([size]) {
padding-right: 25px;
}
button.small {
@@ -149,22 +169,24 @@ select {
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
}
select:not([size]) {
background-image: url("../svg/select-arrow-normal.svg");
background-position: center right;
background-repeat: no-repeat;
}
select:disabled {
select:not([size]):disabled {
background-image: url("../svg/select-arrow-inactive.svg") !important;
}
select:active {
select:not([size]):active {
color: var(--cs-control-intensive-fg) !important;
background-image: url("../svg/select-arrow-intensive.svg") !important;
}
select option {
select:not([size]) option {
color: var(--cs-control-default-fg);
background-color: var(--cs-control-default-bg);
}
select option.comment {
select:not([size]) option.comment {
color: var(--cs-control-disabled-fg);
font-style: italic;
}
@@ -180,6 +202,26 @@ input[type=text], input[type=password] {
height: 30px;
}
textarea {
display: block;
resize: none;
height: 120px;
width: 100%;
border: var(--border-default-thin);
border-radius: 4px;
color: var(--cs-code-default-fg);
background-color: var(--cs-code-default-bg);
-webkit-appearance:none;
}
textarea::-moz-placeholder {
line-height: 60px;
text-align: center;
}
textarea::-webkit-input-placeholder {
line-height: 60px;
text-align: center;
}
div.buttons-row {
margin: 0;
padding: 0;
@@ -218,6 +260,30 @@ div.buttons-row {
border-bottom-right-radius: 0;
}
table.kv {
border-spacing: 5px;
margin: 0 10px 0 10px;
font-size: 12px;
}
table.kv td {
text-align: left;
}
table.kv td.value {
font-weight: bold;
max-width: 310px;
overflow: hidden;
}
table.kv td.value-slider {
width: 100%;
}
table.kv td.value-number {
font-weight: bold;
max-width: 310px;
overflow: hidden;
min-width: 40px;
max-width: 40px;
}
ul.footer {
list-style-type: none;
bottom: 0;
@@ -231,10 +297,10 @@ ul.footer {
ul.footer li {
padding: 0 10px;
}
ul.footer li.footer-left {
ul.footer li.left {
float: left;
}
ul.footer li.footer-right {
ul.footer li.right {
float: right;
}
ul.footer li a {

View File

@@ -167,30 +167,6 @@ ul#navbar li div.menu div.text {
font-size: 14px;
}
ul#navbar li div.menu table.kv {
border-spacing: 5px;
margin: 0 10px 0 10px;
font-size: 12px;
}
ul#navbar li div.menu table.kv td {
text-align: left;
}
ul#navbar li div.menu table.kv td.value {
font-weight: bold;
max-width: 310px;
overflow: hidden;
}
ul#navbar li div.menu table.kv td.value-slider {
width: 100%;
}
ul#navbar li div.menu table.kv td.value-number {
font-weight: bold;
max-width: 310px;
overflow: hidden;
min-width: 40px;
max-width: 40px;
}
ul#navbar li div.menu div.buttons button,
ul#navbar li div.menu div.buttons select {
border-radius: 0;
@@ -222,3 +198,33 @@ ul#navbar li div.menu img.sign {
margin-right: 10px;
height: 20px;
}
ul.navbar-bg-tips {
list-style-type: none;
top: 50px;
position: fixed;
width: 100%;
padding: 0;
font-size: 0.7em;
line-height: 1.5em;
color: var(--cs-page-obscure-fg);
z-index: -10;
}
ul.navbar-bg-tips li {
padding: 0 10px;
max-width: 20%;
}
ul.navbar-bg-tips li pre {
word-break: break-word;
white-space: break-spaces;
text-align: justify;
}
ul.navbar-bg-tips li.left {
float: left;
}
ul.navbar-bg-tips li.right {
float: right;
}
ul.navbar-bg-tips li a {
color: var(--cs-page-obscure-fg);
}

View File

@@ -23,7 +23,7 @@
/* ===== main.css ===== */
button:enabled:hover,
select:enabled:hover,
select:not([size]):enabled:hover,
input[type=file]:enabled:hover::-webkit-file-selector-button,
input[type=file]:enabled:hover::file-selector-button {
color: var(--cs-control-hovered-fg);
@@ -31,7 +31,7 @@ input[type=file]:enabled:hover::file-selector-button {
}
button:active,
select:active,
select:not([size]):active,
input[type=file]:active::-webkit-file-selector-button,
input[type=file]:active::file-selector-button {
color: var(--cs-control-pressed-fg) !important;
@@ -42,7 +42,7 @@ button.key:active,
select.key:active {
box-shadow: none;
}
select:enabled:hover {
select:not([size]):enabled:hover {
background-image: url("../svg/select-arrow-intensive.svg") !important;
}

View File

@@ -113,6 +113,7 @@
"kvm_text80":"Record video using the browser API, and will be downloaded automatically",
"kvm_text81":"Start recording",
"kvm_text82":"End recording",
"kvm_text83":"Web UI settings",
"atx-ask-switch":"Ask click confirmation",
"hid-recorder-loop-switch":"Infinite loop playback",
@@ -140,5 +141,6 @@
"msd-message-out-of-storage":"Current image is out of storage",
"msd-message-rw-enabled":"Read-write mode is enabled",
"msd-message-downloads":"The image is being downloaded from One-KVM",
"msd-message-another-user-uploads":"Another user uploads an image"
"msd-message-another-user-uploads":"Another user uploads an image",
"page-full-tab-stream-switch":"Expand for the entire tab by default"
}

View File

@@ -113,6 +113,7 @@
"kvm_text80":"使用浏览器 API 录制视频,结束录制后视频文件会自动下载",
"kvm_text81":"开始录制",
"kvm_text82":"结束录制",
"kvm_text83":"网页界面设置",
"atx-ask-switch":"点击二次确认",
"hid-recorder-loop-switch":"无限循环重放",
@@ -140,5 +141,6 @@
"msd-message-out-of-storage":"当前镜像大小超出存储空间",
"msd-message-rw-enabled":"读写模式以启用",
"msd-message-downloads":"正在从 One-KVM 下载镜像",
"msd-message-another-user-uploads":"另一个用户正在上传镜像"
"msd-message-another-user-uploads":"另一个用户正在上传镜像",
"page-full-tab-stream-switch":"自动全屏视频窗口"
}

View File

@@ -38,13 +38,13 @@ export function main() {
function __loadKvmdInfo() {
tools.httpGet("/api/info?fields=auth,meta,extras", function(http) {
tools.httpGet("/api/info", {"fields": "auth,meta,extras"}, function(http) {
if (http.status === 200) {
let info = JSON.parse(http.responseText).result;
let apps = [];
if (info.extras === null) {
wm.error("Not all applications in the menu can be displayed<br>due an error. See KVMD logs for details.");
wm.error("Not all applications in the menu can be displayed due an error.<br>See KVMD logs for details.");
} else {
apps = Object.values(info.extras).sort(function(a, b) {
if (a.place < b.place) {
@@ -100,7 +100,7 @@ function __makeApp(id, path, icon, name) {
<a href="${path}">
<div>
<img class="svg-gray" src="${icon}">
${name}
${tools.escape(name)}
</div>
</a>
</div>
@@ -108,11 +108,11 @@ function __makeApp(id, path, icon, name) {
}
function __logout() {
tools.httpPost("/api/auth/logout", function(http) {
tools.httpPost("/api/auth/logout", null, function(http) {
if (http.status === 200 || http.status === 401 || http.status === 403) {
document.location.href = "/login";
} else {
wm.error("Logout error:<br>", http.responseText);
wm.error("Logout error", http.responseText);
}
});
}

View File

@@ -31,7 +31,7 @@ export function main() {
}
function __loadKvmdInfo() {
tools.httpGet("/api/info", function(http) {
tools.httpGet("/api/info", null, function(http) {
if (http.status === 200) {
let ipmi_port = JSON.parse(http.responseText).result.extras.ipmi.port;
let make_item = (comment, ipmi, api) => `

View File

@@ -32,59 +32,77 @@ export function Atx(__recorder) {
/************************************************************************/
var __state = null;
var __init__ = function() {
$("atx-power-led").title = "Power Led";
$("atx-hdd-led").title = "Disk Activity Led";
tools.storage.bindSimpleSwitch($("atx-ask-switch"), "atx.ask", true);
for (let args of [
["atx-power-button", "power", "Are you sure you want to press the power button?"],
["atx-power-button-long", "power_long", `
Are you sure you want to long press the power button?<br>
Warning! This could cause data loss on the server.
`],
["atx-reset-button", "reset", `
Are you sure you want to press the reset button?<br>
Warning! This could case data loss on the server.
`],
]) {
tools.el.setOnClick($(args[0]), () => __clickButton(args[1], args[2]));
}
tools.el.setOnClick($("atx-power-button"), () => __clickAtx("power"));
tools.el.setOnClick($("atx-power-button-long"), () => __clickAtx("power_long"));
tools.el.setOnClick($("atx-reset-button"), () => __clickAtx("reset"));
};
/************************************************************************/
self.setState = function(state) {
let buttons_enabled = false;
if (state) {
tools.feature.setEnabled($("atx-dropdown"), state.enabled);
$("atx-power-led").className = (state.busy ? "led-yellow" : (state.leds.power ? "led-green" : "led-gray"));
$("atx-hdd-led").className = (state.leds.hdd ? "led-red" : "led-gray");
buttons_enabled = !state.busy;
if (!__state) {
__state = {"leds": {}};
}
if (state.enabled !== undefined) {
__state.enabled = state.enabled;
tools.feature.setEnabled($("atx-dropdown"), __state.enabled);
}
if (__state.enabled !== undefined) {
if (state.busy !== undefined) {
__state.busy = state.busy;
__updateButtons(!__state.busy);
}
if (state.leds !== undefined) {
__state.leds = state.leds;
}
if (state.busy !== undefined || state.leds !== undefined) {
__updateLeds(__state.leds.power, __state.leds.hdd, __state.busy);
}
}
} else {
$("atx-power-led").className = "led-gray";
$("atx-hdd-led").className = "led-gray";
}
for (let id of ["atx-power-button", "atx-power-button-long", "atx-reset-button"]) {
tools.el.setEnabled($(id), buttons_enabled);
__state = null;
__updateLeds(false, false, false);
__updateButtons(false);
}
};
var __clickButton = function(button, confirm_msg) {
var __updateLeds = function(power, hdd, busy) {
$("atx-power-led").className = (busy ? "led-yellow" : (power ? "led-green" : "led-gray"));
$("atx-hdd-led").className = (hdd ? "led-red" : "led-gray");
};
var __updateButtons = function(enabled) {
for (let id of ["atx-power-button", "atx-power-button-long", "atx-reset-button"]) {
tools.el.setEnabled($(id), enabled);
}
};
var __clickAtx = function(button) {
let click_button = function() {
tools.httpPost(`/api/atx/click?button=${button}`, function(http) {
tools.httpPost("/api/atx/click", {"button": button}, function(http) {
if (http.status === 409) {
wm.error("Performing another ATX operation for other client.<br>Please try again later");
wm.error("Performing another ATX operation for other client.<br>Please try again later.");
} else if (http.status !== 200) {
wm.error("Click error:<br>", http.responseText);
wm.error("Click error", http.responseText);
}
});
__recorder.recordAtxButtonEvent(button);
};
if ($("atx-ask-switch").checked) {
wm.confirm(confirm_msg).then(function(ok) {
wm.confirm(`
Are you sure you want to press the <b>${button}</b> button?<br>
Warning! This could case data loss on the server.
`).then(function(ok) {
if (ok) {
click_button();
}

View File

@@ -23,7 +23,7 @@
"use strict";
import {tools, $, $$$} from "../tools.js";
import {tools, $, $$} from "../tools.js";
import {wm} from "../wm.js";
@@ -32,44 +32,59 @@ export function Gpio(__recorder) {
/************************************************************************/
var __state = null;
var __has_model = false;
/************************************************************************/
self.setState = function(state) {
if (state) {
for (let channel in state.inputs) {
let el = $(`gpio-led-${channel}`);
if (el) {
__setLedState(el, state.inputs[channel].state);
}
if (state.model !== undefined) {
__has_model = true;
__updateModel(state.model);
}
for (let channel in state.outputs) {
for (let type of ["switch", "button"]) {
let el = $(`gpio-${type}-${channel}`);
if (el) {
tools.el.setEnabled(el, state.outputs[channel].online && !state.outputs[channel].busy);
}
if (__has_model && state.state !== undefined) {
if (state.state.inputs !== undefined) {
__updateInputs(state.state.inputs);
}
let el = $(`gpio-switch-${channel}`);
if (el) {
el.checked = state.outputs[channel].state;
if (state.state.outputs !== undefined) {
__updateOutputs(state.state.outputs);
}
}
} else {
for (let el of $$$(".gpio-led")) {
__has_model = false;
for (let el of $$("__gpio-led")) {
__setLedState(el, false);
}
for (let selector of [".gpio-switch", ".gpio-button"]) {
for (let el of $$$(selector)) {
for (let selector of ["__gpio-switch", "__gpio-button"]) {
for (let el of $$(selector)) {
tools.el.setEnabled(el, false);
}
}
}
__state = state;
};
self.setModel = function(model) {
var __updateInputs = function(inputs) {
for (let ch in inputs) {
for (let el of $$(`__gpio-led-${ch}`)) {
__setLedState(el, inputs[ch].state);
}
}
};
var __updateOutputs = function(outputs) {
for (let ch in outputs) {
for (let type of ["switch", "button"]) {
for (let el of $$(`__gpio-${type}-${ch}`)) {
tools.el.setEnabled(el, (outputs[ch].online && !outputs[ch].busy));
}
}
for (let el of $$(`__gpio-switch-${ch}`)) {
el.checked = outputs[ch].state;
}
}
};
var __updateModel = function(model) {
tools.feature.setEnabled($("gpio-dropdown"), model.view.table.length);
if (model.view.table.length) {
let title = [];
@@ -84,44 +99,36 @@ export function Gpio(__recorder) {
$("gpio-menu-button").innerHTML = title.join(" ");
}
let content = "<table class=\"kv\">";
let html = "<table class=\"kv\">";
for (let row of model.view.table) {
if (row === null) {
content += "</table><hr><table class=\"kv\">";
html += "</table><hr><table class=\"kv\">";
} else {
content += "<tr>";
html += "<tr>";
for (let item of row) {
if (item.type === "output") {
item.scheme = model.scheme.outputs[item.channel];
}
content += `<td align="center">${__createItem(item)}</td>`;
html += `<td align="center">${__createItem(item)}</td>`;
}
content += "</tr>";
html += "</tr>";
}
}
content += "</table>";
$("gpio-menu").innerHTML = content;
html += "</table>";
$("gpio-menu").innerHTML = html;
for (let channel in model.scheme.outputs) {
let el = $(`gpio-switch-${channel}`);
if (el) {
tools.el.setOnClick(el, __createAction(el, __switchChannel));
for (let ch in model.scheme.outputs) {
for (let el of $$(`__gpio-switch-${ch}`)) {
tools.el.setOnClick(el, tools.partial(__switchChannel, el));
}
el = $(`gpio-button-${channel}`);
if (el) {
tools.el.setOnClick(el, __createAction(el, __pulseChannel));
for (let el of $$(`__gpio-button-${ch}`)) {
tools.el.setOnClick(el, tools.partial(__pulseChannel, el));
}
}
tools.feature.setEnabled($("v3-usb-breaker"), ("__v3_usb_breaker__" in model.scheme.outputs));
tools.feature.setEnabled($("v4-locator"), ("__v4_locator__" in model.scheme.outputs));
tools.feature.setEnabled($("system-tool-wol"), ("__wol__" in model.scheme.outputs));
self.setState(__state);
};
var __createAction = function(el, action) {
return () => action(el);
};
var __createItem = function(item) {
@@ -129,18 +136,28 @@ export function Gpio(__recorder) {
return item.text;
} else if (item.type === "input") {
return `
<img id="gpio-led-${item.channel}" class="gpio-led inline-lamp-big led-gray"
src="/share/svg/led-circle.svg" data-color="${item.color}" />
<img
class="__gpio-led __gpio-led-${item.channel} inline-lamp-big led-gray"
src="/share/svg/led-circle.svg"
data-color="${item.color}"
/>
`;
} else if (item.type === "output") {
let controls = [];
let confirm = (item.confirm ? "Are you sure you want to perform this action?" : "");
if (item.scheme["switch"]) {
let id = tools.makeId();
controls.push(`
<td><div class="switch-box">
<input disabled type="checkbox" id="gpio-switch-${item.channel}" class="gpio-switch"
data-channel="${item.channel}" data-confirm="${confirm}" />
<label for="gpio-switch-${item.channel}">
<input
disabled
type="checkbox"
id="__gpio-switch-${id}"
class="__gpio-switch __gpio-switch-${item.channel}"
data-channel="${item.channel}"
data-confirm="${confirm}"
/>
<label for="__gpio-switch-${id}">
<span class="switch-inner"></span>
<span class="switch"></span>
</label>
@@ -149,10 +166,14 @@ export function Gpio(__recorder) {
}
if (item.scheme.pulse.delay) {
controls.push(`
<td><button disabled id="gpio-button-${item.channel}" class="gpio-button"
${item.hide ? "data-force-hide-menu" : ""}
data-channel="${item.channel}" data-confirm="${confirm}">
${(item.hide ? "&bull; " : "") + item.text}
<td><button
disabled
class="__gpio-button __gpio-button-${item.channel}"
${item.hide ? "data-force-hide-menu" : ""}
data-channel="${item.channel}"
data-confirm="${confirm}"
>
${(item.hide ? "&bull; " : "") + item.text}
</button></td>
`);
}
@@ -162,9 +183,9 @@ export function Gpio(__recorder) {
}
};
var __setLedState = function(el, state) {
var __setLedState = function(el, on) {
let color = el.getAttribute("data-color");
if (state) {
if (on) {
el.classList.add(`led-${color}`);
el.classList.remove("led-gray");
} else {
@@ -174,22 +195,20 @@ export function Gpio(__recorder) {
};
var __switchChannel = function(el) {
let channel = el.getAttribute("data-channel");
let ch = el.getAttribute("data-channel");
let confirm = el.getAttribute("data-confirm");
let to = ($(`gpio-switch-${channel}`).checked ? "1" : "0");
let to = (el.checked ? "1" : "0");
if (to === "0" && el.hasAttribute("data-confirm-off")) {
confirm = el.getAttribute("data-confirm-off");
}
let act = () => {
__sendPost(`/api/gpio/switch?channel=${channel}&state=${to}`);
__recorder.recordGpioSwitchEvent(channel, to);
__sendPost("/api/gpio/switch", {"channel": ch, "state": to});
__recorder.recordGpioSwitchEvent(ch, to);
};
if (confirm) {
wm.confirm(confirm).then(function(ok) {
wm.confirm(tools.escape(confirm)).then(function(ok) {
if (ok) {
act();
} else {
self.setState(__state); // Switch back
}
});
} else {
@@ -198,25 +217,29 @@ export function Gpio(__recorder) {
};
var __pulseChannel = function(el) {
let channel = el.getAttribute("data-channel");
let ch = el.getAttribute("data-channel");
let confirm = el.getAttribute("data-confirm");
let act = () => {
__sendPost(`/api/gpio/pulse?channel=${channel}`);
__recorder.recordGpioPulseEvent(channel);
__sendPost("/api/gpio/pulse", {"channel": ch});
__recorder.recordGpioPulseEvent(ch);
};
if (confirm) {
wm.confirm(confirm).then(function(ok) { if (ok) act(); });
wm.confirm(tools.escape(confirm)).then(function(ok) {
if (ok) {
act();
}
});
} else {
act();
}
};
var __sendPost = function(url) {
tools.httpPost(url, function(http) {
var __sendPost = function(url, params) {
tools.httpPost(url, params, function(http) {
if (http.status === 409) {
wm.error("Performing another operation for this GPIO channel.<br>Please try again later");
wm.error("Performing another operation for this GPIO channel.<br>Please try again later.");
} else if (http.status !== 200) {
wm.error("GPIO error:<br>", http.responseText);
wm.error("GPIO error", http.responseText);
}
});
};

View File

@@ -35,6 +35,7 @@ export function Hid(__getGeometry, __recorder) {
/************************************************************************/
var __state = null;
var __keyboard = null;
var __mouse = null;
@@ -71,21 +72,6 @@ export function Hid(__getGeometry, __recorder) {
window.addEventListener("pagehide", __releaseAll);
window.addEventListener("blur", __releaseAll);
tools.storage.bindSimpleSwitch($("hid-pak-ask-switch"), "hid.pak.ask", true);
tools.storage.bindSimpleSwitch($("hid-pak-secure-switch"), "hid.pak.secure", false, function(value) {
$("hid-pak-text").style.setProperty("-webkit-text-security", (value ? "disc" : "none"));
});
tools.feature.setEnabled($("hid-pak-secure"), (
tools.browser.is_chrome
|| tools.browser.is_safari
|| tools.browser.is_opera
));
$("hid-pak-keymap-selector").addEventListener("change", function() {
tools.storage.set("hid.pak.keymap", $("hid-pak-keymap-selector").value);
});
tools.el.setOnClick($("hid-pak-button"), __clickPasteAsKeysButton);
tools.el.setOnClick($("hid-connect-switch"), __clickConnectSwitch);
tools.el.setOnClick($("hid-reset-button"), __clickResetButton);
@@ -98,8 +84,7 @@ export function Hid(__getGeometry, __recorder) {
}
let codes = el_shortcut.getAttribute("data-shortcut").split(" ");
if (ask) {
let confirm_msg = `Do you want to press <b>${codes.join(" + ")}</b>?`;
wm.confirm(confirm_msg).then(function(ok) {
wm.confirm("Do you want to press this hotkey?", codes.join(" + ")).then(function(ok) {
if (ok) {
__emitShortcut(codes);
}
@@ -118,10 +103,6 @@ export function Hid(__getGeometry, __recorder) {
/************************************************************************/
self.setSocket = function(ws) {
tools.el.setEnabled($("hid-pak-text"), ws);
tools.el.setEnabled($("hid-pak-button"), ws);
tools.el.setEnabled($("hid-reset-button"), ws);
tools.el.setEnabled($("hid-jiggler-switch"), ws);
if (!ws) {
self.setState(null);
}
@@ -130,84 +111,135 @@ export function Hid(__getGeometry, __recorder) {
};
self.setState = function(state) {
let has_relative_squash = false;
if (state) {
tools.feature.setEnabled($("hid-jiggler"), state.jiggler.enabled);
$("hid-jiggler-switch").checked = state.jiggler.active;
}
if (state && state.online) {
let keyboard_outputs = state.keyboard.outputs.available;
let mouse_outputs = state.mouse.outputs.available;
if (keyboard_outputs.length) {
if ($("hid-outputs-keyboard-box").outputs !== keyboard_outputs) {
let html = "";
for (let args of [
["USB", "usb"],
["PS/2", "ps2"],
["Off", "disabled"],
]) {
if (keyboard_outputs.includes(args[1])) {
html += tools.radio.makeItem("hid-outputs-keyboard-radio", args[0], args[1]);
}
}
$("hid-outputs-keyboard-box").innerHTML = html;
$("hid-outputs-keyboard-box").outputs = keyboard_outputs;
tools.radio.setOnClick("hid-outputs-keyboard-radio", () => __clickOutputsRadio("keyboard"));
}
tools.radio.setValue("hid-outputs-keyboard-radio", state.keyboard.outputs.active);
if (!__state) {
__state = {"keyboard": {}, "mouse": {}};
}
let has_relative = false;
if (mouse_outputs.length) {
if ($("hid-outputs-mouse-box").outputs !== mouse_outputs) {
let html = "";
for (let args of [
["Absolute", "usb", false],
["Abs-Win98", "usb_win98", false],
["Relative", "usb_rel", true],
["PS/2", "ps2", true],
["Off", "disabled"],
]) {
if (mouse_outputs.includes(args[1])) {
html += tools.radio.makeItem("hid-outputs-mouse-radio", args[0], args[1]);
has_relative = (has_relative || args[2]);
}
}
$("hid-outputs-mouse-box").innerHTML = html;
$("hid-outputs-mouse-box").outputs = mouse_outputs;
tools.radio.setOnClick("hid-outputs-mouse-radio", () => __clickOutputsRadio("mouse"));
}
tools.radio.setValue("hid-outputs-mouse-radio", state.mouse.outputs.active);
has_relative_squash = ["usb_rel", "ps2"].includes(state.mouse.outputs.active);
} else {
has_relative = !state.mouse.absolute;
has_relative_squash = has_relative;
if (state.enabled !== undefined) {
__state.enabled = state.enabled; // Currently unused, always true
}
tools.feature.setEnabled($("hid-outputs"), (keyboard_outputs.length || mouse_outputs.length));
tools.feature.setEnabled($("hid-outputs-keyboard"), keyboard_outputs.length);
tools.feature.setEnabled($("hid-outputs-mouse"), mouse_outputs.length);
tools.feature.setEnabled($("hid-mouse-squash"), has_relative);
tools.feature.setEnabled($("hid-mouse-sens"), has_relative);
tools.feature.setEnabled($("hid-connect"), (state.connected !== null));
$("hid-connect-switch").checked = !!state.connected;
}
tools.radio.setEnabled("hid-outputs-keyboard-radio", (state && state.online && !state.busy));
tools.radio.setEnabled("hid-outputs-mouse-radio", (state && state.online && !state.busy));
tools.el.setEnabled($("hid-mouse-squash-switch"), (has_relative_squash && !state.busy));
tools.el.setEnabled($("hid-mouse-sens-slider"), (has_relative_squash && !state.busy));
tools.el.setEnabled($("hid-connect-switch"), (state && state.online && !state.busy));
if (state) {
__keyboard.setState(state.keyboard, state.online, state.busy);
__mouse.setState(state.mouse, state.online, state.busy);
if (__state.enabled !== undefined) {
for (let key of ["online", "busy", "connected", "jiggler"]) {
if (state[key] !== undefined) {
__state[key] = state[key];
}
}
for (let hid of ["keyboard", "mouse"]) {
if (state[hid] === undefined) {
state[hid] = {}; // Add some stubs for processing
}
for (let key of ["online", "outputs", (hid === "keyboard" ? "leds" : "absolute")]) {
__state[hid][key] = state[hid][key];
}
}
if (state.connected !== undefined) {
tools.feature.setEnabled($("hid-connect"), (__state.connected !== null));
$("hid-connect-switch").checked = !!__state.connected;
}
if (state.jiggler !== undefined) {
tools.feature.setEnabled($("hid-jiggler"), __state.jiggler.enabled);
$("hid-jiggler-switch").checked = __state.jiggler.active;
}
if (state.keyboard.outputs !== undefined) {
__updateKeyboardOutputs(__state.keyboard.outputs);
}
if (state.mouse.outputs !== undefined) {
__updateMouseOutputs(__state.mouse.outputs, __state.mouse.absolute); // Follows together
}
if (
state.keyboard.online !== undefined || state.keyboard.leds !== undefined
|| state.online !== undefined || state.busy !== undefined
) {
__keyboard.setState(__state.keyboard.online, __state.keyboard.leds, __state.online, __state.busy);
}
if (
state.mouse.online !== undefined || state.mouse.absolute !== undefined
|| state.online !== undefined || state.busy !== undefined
) {
__mouse.setState(__state.mouse.online, __state.mouse.absolute, __state.online, __state.busy);
}
if (state.online !== undefined || state.busy !== undefined) {
tools.radio.setEnabled("hid-outputs-keyboard-radio", (__state.online && !__state.busy));
tools.radio.setEnabled("hid-outputs-mouse-radio", (__state.online && !__state.busy));
tools.el.setEnabled($("hid-connect-switch"), (__state.online && !__state.busy));
}
}
} else {
__state = null;
tools.radio.setEnabled("hid-outputs-keyboard-radio", false);
tools.radio.setEnabled("hid-outputs-mouse-radio", false);
tools.el.setEnabled($("hid-connect-switch"), false);
tools.el.setEnabled($("hid-mouse-squash-switch"), false);
tools.el.setEnabled($("hid-mouse-sens-slider"), false);
}
tools.el.setEnabled($("hid-reset-button"), __state);
tools.el.setEnabled($("hid-jiggler-switch"), __state);
};
self.setKeymaps = function(state) {
let el = $("hid-pak-keymap-selector");
tools.selector.setValues(el, state.keymaps.available);
tools.selector.setSelectedValue(el, tools.storage.get("hid.pak.keymap", state.keymaps["default"]));
var __updateKeyboardOutputs = function(outputs) {
let avail = outputs.available;
if (avail.length > 0) {
let el = $("hid-outputs-keyboard-box");
let avail_json = JSON.stringify(avail);
if (el.__avail_json !== avail_json) {
let html = "";
for (let pair of [
["USB", "usb"],
["PS/2", "ps2"],
["Off", "disabled"],
]) {
if (avail.includes(pair[1])) {
html += tools.radio.makeItem("hid-outputs-keyboard-radio", pair[0], pair[1]);
}
}
el.innerHTML = html;
tools.radio.setOnClick("hid-outputs-keyboard-radio", () => __clickOutputsRadio("keyboard"));
el.__avail_json = avail_json;
}
tools.radio.setValue("hid-outputs-keyboard-radio", outputs.active);
}
tools.feature.setEnabled($("hid-outputs-keyboard"), (avail.length > 0));
};
var __updateMouseOutputs = function(outputs, absolute) {
let has_relative = null;
let has_relative_squash = null;
let avail = outputs.available;
if (avail.length > 0) {
let el = $("hid-outputs-mouse-box");
let avail_json = JSON.stringify(avail);
if (el.__avail_json !== avail_json) {
has_relative = false;
let html = "";
for (let pair of [
["Absolute", "usb", false],
["Abs-Win98", "usb_win98", false],
["Relative", "usb_rel", true],
["PS/2", "ps2", true],
["Off", "disabled", false],
]) {
if (avail.includes(pair[1])) {
html += tools.radio.makeItem("hid-outputs-mouse-radio", pair[0], pair[1]);
has_relative = (has_relative || pair[2]);
}
}
el.innerHTML = html;
tools.radio.setOnClick("hid-outputs-mouse-radio", () => __clickOutputsRadio("mouse"));
el.__avail_json = avail_json;
}
tools.radio.setValue("hid-outputs-mouse-radio", outputs.active);
has_relative_squash = (["usb_rel", "ps2"].includes(outputs.active));
} else {
has_relative = !absolute;
has_relative_squash = has_relative;
}
if (has_relative !== null) {
tools.feature.setEnabled($("hid-mouse-squash"), has_relative);
tools.feature.setEnabled($("hid-mouse-sens"), has_relative);
}
tools.feature.setEnabled($("hid-outputs-mouse"), (avail.length > 0));
tools.el.setEnabled($("hid-mouse-squash-switch"), has_relative_squash);
tools.el.setEnabled($("hid-mouse-sens-slider"), has_relative_squash);
};
var __releaseAll = function() {
@@ -241,72 +273,29 @@ export function Hid(__getGeometry, __recorder) {
});
};
var __clickPasteAsKeysButton = function() {
let text = $("hid-pak-text").value;
if (text) {
let paste_as_keys = function() {
tools.el.setEnabled($("hid-pak-text"), false);
tools.el.setEnabled($("hid-pak-button"), false);
tools.el.setEnabled($("hid-pak-keymap-selector"), false);
let keymap = $("hid-pak-keymap-selector").value;
tools.debug(`HID: paste-as-keys ${keymap}: ${text}`);
tools.httpPost(`/api/hid/print?limit=0&keymap=${keymap}`, function(http) {
tools.el.setEnabled($("hid-pak-text"), true);
tools.el.setEnabled($("hid-pak-button"), true);
tools.el.setEnabled($("hid-pak-keymap-selector"), true);
$("hid-pak-text").value = "";
if (http.status === 413) {
wm.error("Too many text for paste!");
} else if (http.status !== 200) {
wm.error("HID paste error:<br>", http.responseText);
} else if (http.status === 200) {
__recorder.recordPrintEvent(text);
}
}, text, "text/plain");
};
if ($("hid-pak-ask-switch").checked) {
let confirm_msg = `You're going to paste ${text.length} character${text.length ? "s" : ""}.<br>`;
confirm_msg += "Are you sure you want to continue?";
wm.confirm(confirm_msg).then(function(ok) {
if (ok) {
paste_as_keys();
} else {
$("hid-pak-text").value = "";
}
});
} else {
paste_as_keys();
}
}
};
var __clickOutputsRadio = function(hid) {
let output = tools.radio.getValue(`hid-outputs-${hid}-radio`);
tools.httpPost(`/api/hid/set_params?${hid}_output=${output}`, function(http) {
tools.httpPost("/api/hid/set_params", {[`${hid}_output`]: output}, function(http) {
if (http.status !== 200) {
wm.error("Can't configure HID:<br>", http.responseText);
wm.error("Can't configure HID", http.responseText);
}
});
};
var __clickJigglerSwitch = function() {
let enabled = $("hid-jiggler-switch").checked;
tools.httpPost(`/api/hid/set_params?jiggler=${enabled}`, function(http) {
tools.httpPost("/api/hid/set_params", {"jiggler": enabled}, function(http) {
if (http.status !== 200) {
wm.error(`Can't ${enabled ? "enabled" : "disable"} mouse juggler:<br>`, http.responseText);
wm.error(`Can't ${enabled ? "enabled" : "disable"} mouse jiggler`, http.responseText);
}
});
};
var __clickConnectSwitch = function() {
let connected = $("hid-connect-switch").checked;
tools.httpPost(`/api/hid/set_connected?connected=${connected}`, function(http) {
tools.httpPost("/api/hid/set_connected", {"connected": connected}, function(http) {
if (http.status !== 200) {
wm.error(`Can't ${connected ? "connect" : "disconnect"} HID:<br>`, http.responseText);
wm.error(`Can't ${connected ? "connect" : "disconnect"} HID`, http.responseText);
}
});
};
@@ -314,9 +303,9 @@ export function Hid(__getGeometry, __recorder) {
var __clickResetButton = function() {
wm.confirm("Are you sure you want to reset HID (keyboard & mouse)?").then(function(ok) {
if (ok) {
tools.httpPost("/api/hid/reset", function(http) {
tools.httpPost("/api/hid/reset", null, function(http) {
if (http.status !== 200) {
wm.error("HID reset error:<br>", http.responseText);
wm.error("HID reset error", http.responseText);
}
});
}

View File

@@ -65,17 +65,17 @@ export function Keyboard(__recordWsEvent) {
__updateOnlineLeds();
};
self.setState = function(state, hid_online, hid_busy) {
self.setState = function(online, leds, hid_online, hid_busy) {
if (!hid_online) {
__online = null;
} else {
__online = (state.online && !hid_busy);
__online = (online && !hid_busy);
}
__updateOnlineLeds();
for (let led of ["caps", "scroll", "num"]) {
for (let el of $$$(`.hid-keyboard-${led}-led`)) {
if (state.leds[led]) {
if (leds[led]) {
el.classList.add("led-green");
el.classList.remove("led-gray");
} else {

View File

@@ -50,9 +50,14 @@ export function main() {
tools.el.setOnClick($("open-log-button"), () => window.open("/api/log?seek=3600&follow=1", "_blank"));
if (tools.config.getBool("kvm--full-tab-stream", false)) {
wm.toggleFullTabWindow($("stream-window"), true);
tools.storage.bindSimpleSwitch(
$("page-full-tab-stream-switch"),
"page.full_tab_stream",
tools.config.getBool("kvm--full-tab-stream", false));
if ($("page-full-tab-stream-switch").checked) {
wm.setFullTabWindow($("stream-window"), true);
}
wm.showWindow($("stream-window"));
new Session();

View File

@@ -90,20 +90,20 @@ export function Mouse(__getGeometry, __recordWsEvent) {
__updateOnlineLeds();
};
self.setState = function(state, hid_online, hid_busy) {
self.setState = function(online, absolute, hid_online, hid_busy) {
if (!hid_online) {
__online = null;
} else {
__online = (state.online && !hid_busy);
__online = (online && !hid_busy);
}
if (!__absolute && state.absolute && __isRelativeCaptured()) {
if (!__absolute && absolute && __isRelativeCaptured()) {
document.exitPointerLock();
}
if (__absolute && !state.absolute) {
if (__absolute && !absolute) {
__relative_deltas = [];
__relative_touch_pos = null;
}
__absolute = state.absolute;
__absolute = absolute;
__updateOnlineLeds();
};

View File

@@ -35,10 +35,6 @@ export function Msd() {
var __state = null;
var __http = null;
var __parts_names_json = "";
var __parts_names_len = 0;
var __parts = {};
var __init__ = function() {
$("msd-led").title = "Unknown state";
@@ -68,8 +64,201 @@ export function Msd() {
/************************************************************************/
self.setState = function(state) {
__state = state;
__applyState();
if (state) {
if (!__state) {
__state = {"storage": {}};
}
if (state.enabled !== undefined) {
__state.enabled = state.enabled;
tools.feature.setEnabled($("msd-dropdown"), __state.enabled);
}
if (__state.enabled !== undefined) {
if (state.online !== undefined) {
__state.online = state.online;
}
if (state.busy !== undefined) {
__state.busy = state.busy;
}
if (state.drive) { // Null on offline, ignore
__state.drive = state.drive;
}
if (state.storage) { // Null on offline, ignore
if (state.storage.parts !== undefined) {
__state.storage.parts = state.storage.parts;
__updateParts(__state.storage.parts);
}
if (state.storage.uploading !== undefined) {
__state.storage.uploading = state.storage.uploading;
__updateUploading(__state.storage.uploading);
}
if (state.storage.downloading !== undefined) {
__state.storage.downloading = state.storage.downloading;
}
if (state.storage.images !== undefined) {
__state.storage.images = state.storage.images;
}
}
if (state.drive || (state.storage && state.storage.images !== undefined)) {
__updateImageSelector(__state.drive, __state.storage.images);
}
}
} else {
__state = null;
}
__refreshControls();
};
var __refreshControls = function() {
__updateControls(__state && (__state.online !== undefined) ? __state : null);
};
var __updateControls = function(state) {
let o = (state && state.online);
let d = (state ? state.drive : null);
let s = (state ? state.storage : null);
let busy = !!(state && state.busy);
tools.hidden.setVisible($("msd-message-offline"), (state && !state.online));
tools.hidden.setVisible($("msd-message-image-broken"), (o && d.image && !d.image.complete && !s.uploading));
tools.hidden.setVisible($("msd-message-too-big-for-cdrom"), (o && d.cdrom && d.image && d.image.size >= 2359296000));
tools.hidden.setVisible($("msd-message-out-of-storage"), (o && d.image && !d.image.in_storage));
tools.hidden.setVisible($("msd-message-rw-enabled"), (o && d.rw));
tools.hidden.setVisible($("msd-message-another-user-uploads"), (o && s.uploading && !__http));
tools.hidden.setVisible($("msd-message-downloads"), (o && s.downloading));
tools.el.setEnabled($("msd-image-selector"), (o && !d.connected && !busy));
tools.el.setEnabled($("msd-download-button"), (o && d.image && !d.connected && !busy));
tools.el.setEnabled($("msd-remove-button"), (o && d.image && d.image.removable && !d.connected && !busy));
tools.radio.setEnabled("msd-mode-radio", (o && !d.connected && !busy));
tools.radio.setValue("msd-mode-radio", `${Number(o && d.cdrom)}`);
tools.el.setEnabled($("msd-rw-switch"), (o && !d.connected && !busy));
$("msd-rw-switch").checked = (o && d.rw);
tools.el.setEnabled($("msd-connect-button"), (o && d.image && !d.connected && !busy));
tools.el.setEnabled($("msd-disconnect-button"), (o && d.connected && !busy));
tools.el.setEnabled($("msd-select-new-button"), (o && !d.connected && !__http && !busy));
tools.el.setEnabled($("msd-upload-new-button"),
(o && !d.connected && (tools.input.getFile($("msd-new-file")) || $("msd-new-url").value.length > 0) && !busy));
tools.el.setEnabled($("msd-abort-new-button"), (o && __http));
tools.el.setEnabled($("msd-reset-button"), (state && state.enabled && !busy));
tools.el.setEnabled($("msd-new-file"), (o && !d.connected && !__http && !busy));
tools.el.setEnabled($("msd-new-url"), (o && !d.connected && !__http && !busy));
tools.el.setEnabled($("msd-new-part-selector"), (o && !d.connected && !__http && !busy));
if (o && s.uploading) {
tools.hidden.setVisible($("msd-new-sub"), false);
$("msd-new-file").value = "";
$("msd-new-url").value = "";
}
tools.hidden.setVisible($("msd-uploading-sub"), (o && s.uploading));
tools.hidden.setVisible($("msd-new-tips"), (o && s.uploading && __http));
let led_cls = "led-gray";
let msg = "Unavailable";
if (o && d.connected) {
led_cls = "led-green";
msg = "Connected to Server";
} else if (o && s.uploading) {
led_cls = "led-yellow-rotating-fast";
msg = "Uploading new image";
} else if (o && s.downloading) {
led_cls = "led-yellow-rotating-fast";
msg = "Serving the image to download";
} else if (o) { // Sic!
msg = "Disconnected";
}
$("msd-led").className = led_cls;
$("msd-status").innerText = $("msd-led").title = msg;
};
var __updateUploading = function(uploading) {
$("msd-uploading-name").innerText = (uploading ? uploading.name : "");
$("msd-uploading-size").innerText = (uploading ? tools.formatSize(uploading.size) : "");
if (uploading) {
tools.progress.setPercentOf($("msd-uploading-progress"), uploading.size, uploading.written);
}
};
var __updateParts = function(parts) {
let names = Object.keys(parts).sort();
{
let writable = names.filter(name => (name === "" || parts[name].writable));
let writable_json = JSON.stringify(writable);
let el = $("msd-new-part-selector");
if (el.__writable_json !== writable_json) {
let sel = (el.value || "");
el.options.length = 0;
for (let name of writable) {
let title = (name || "\u2500 Internal \u2500");
tools.selector.addOption(el, title, name, (name === sel));
}
tools.hidden.setVisible($("msd-new-part"), (writable.length > 1));
el.__writable_json = writable_json;
}
}
{
let names_json = JSON.stringify(names);
let el = $("msd-storages");
if (el.__names_json !== names_json) {
el.innerHTML = names.map(name => `
<div class="text">
<div id="__msd-storage-${tools.makeIdByText(name)}-progress" class="progress">
<span class="progress-value"></span>
</div>
</div>
`).join("<hr>");
el.__names_json = names_json;
}
}
for (let name of names) {
let part = parts[name];
let title = (
name === ""
? `${names.length === 1 ? "Storage: %s" : "Internal storage: %s"}` // eslint-disable-line
: `Storage [${name}${part.writable ? "]" : ", read-only]"}: %s` // eslint-disable-line
);
let id = `__msd-storage-${tools.makeIdByText(name)}-progress`;
tools.progress.setSizeOf($(id), title, part.size, part.free);
}
};
var __updateImageSelector = function(drive, images) {
let sel = "";
let el = $("msd-image-selector");
el.options.length = 1;
for (let name of Object.keys(images).sort()) {
tools.selector.addSeparator(el);
tools.selector.addOption(el, name, name);
tools.selector.addComment(el, __makeImageSelectorInfo(images[name]));
if (drive.image && drive.image.name === name && drive.image.in_storage) {
sel = name;
}
}
if (drive.image && !drive.image.in_storage) {
sel = ".__external__"; // Just some magic name
tools.selector.addOption(el, drive.image.name, sel);
tools.selector.addComment(el, __makeImageSelectorInfo(drive.image));
}
el.value = sel;
};
var __makeImageSelectorInfo = function(image) {
let text = `\xA0\xA0\xA0\xA0\xA0\u2570 ${tools.formatSize(image.size)}`;
if (!image.complete) {
text += ", broken";
}
if (image.in_storage !== undefined && !image.in_storage) {
text += ", out of storage";
}
let ts = new Date(image.mod_ts * 1000);
ts = new Date(ts.getTime() - (ts.getTimezoneOffset() * 60000));
ts = ts.toISOString().slice(0, -8).replaceAll("-", ".").replace("T", "-");
return `${text} \u2500 ${ts}`;
};
var __selectImage = function() {
@@ -80,17 +269,17 @@ export function Msd() {
};
var __clickDownloadButton = function() {
let name = $("msd-image-selector").value;
window.open(`/api/msd/read?image=${name}`);
let image = encodeURIComponent($("msd-image-selector").value);
window.open(`/api/msd/read?image=${image}`);
};
var __clickRemoveButton = function() {
let name = $("msd-image-selector").value;
wm.confirm(`Are you sure you want to remove the image<br><b>${name}</b> from PiKVM?`).then(function(ok) {
wm.confirm("Are you sure you want to remove this image?", name).then(function(ok) {
if (ok) {
tools.httpPost(`/api/msd/remove?image=${name}`, function(http) {
tools.httpPost("/api/msd/remove", {"image": name}, function(http) {
if (http.status !== 200) {
wm.error("Can't remove image:<br>", http.responseText);
wm.error("Can't remove image", http.responseText);
}
});
}
@@ -98,10 +287,11 @@ export function Msd() {
};
var __sendParam = function(name, value) {
tools.httpPost(`/api/msd/set_params?${name}=${encodeURIComponent(value)}`, function(http) {
tools.httpPost("/api/msd/set_params", {[name]: value}, function(http) {
if (http.status !== 200) {
wm.error("Can't configure MSD:<br>", http.responseText);
wm.error("Can't configure Mass Storage", http.responseText);
}
__refreshControls();
});
};
@@ -110,80 +300,84 @@ export function Msd() {
__http = new XMLHttpRequest();
let prefix = encodeURIComponent($("msd-new-part-selector").value);
if (file) {
__http.open("POST", `/api/msd/write?prefix=${prefix}&image=${encodeURIComponent(file.name)}&remove_incomplete=1`, true);
let image = encodeURIComponent(file.name);
__http.open("POST", `/api/msd/write?prefix=${prefix}&image=${image}&remove_incomplete=1`, true);
} else {
let url = $("msd-new-url").value;
__http.open("POST", `/api/msd/write_remote?prefix=${prefix}&url=${encodeURIComponent(url)}&remove_incomplete=1`, true);
let url = encodeURIComponent($("msd-new-url").value);
__http.open("POST", `/api/msd/write_remote?prefix=${prefix}&url=${url}&remove_incomplete=1`, true);
}
__http.upload.timeout = 7 * 24 * 3600;
__http.onreadystatechange = __httpStateChange;
__http.onreadystatechange = __uploadStateChange;
__http.send(file);
__applyState();
__refreshControls();
};
var __httpStateChange = function() {
if (__http.readyState === 4) {
if (__http.status !== 200) {
wm.error("Can't upload image to the Mass Storage Drive:<br>", __http.responseText);
} else if ($("msd-new-url").value.length > 0) {
let msg = "";
try {
let end = __http.responseText.lastIndexOf("\r\n");
if (end < 0) {
end = __http.responseText.length;
}
let begin = __http.responseText.lastIndexOf("\r\n", end - 2);
if (begin < 0) {
end = 0;
}
let result_str = __http.responseText.slice(begin, end);
let result = JSON.parse(result_str);
if (!result.ok) {
msg = `Can't upload image to the Mass Storage Drive:<br>${result_str}`;
}
} catch (err) {
msg = `Can't parse upload result:<br>${err}`;
}
if (msg.length > 0) {
wm.error(msg);
}
}
tools.hidden.setVisible($("msd-new-sub"), false);
$("msd-new-file").value = "";
$("msd-new-url").value = "";
__http = null;
__applyState();
var __uploadStateChange = function() {
if (__http.readyState !== 4) {
return;
}
if (__http.status !== 200) {
wm.error("Can't upload image", __http.responseText);
} else if ($("msd-new-url").value.length > 0) {
let html = "";
let msg = "";
try {
let end = __http.responseText.lastIndexOf("\r\n");
if (end < 0) {
end = __http.responseText.length;
}
let begin = __http.responseText.lastIndexOf("\r\n", end - 2);
if (begin < 0) {
end = 0;
}
let result_str = __http.responseText.slice(begin, end);
let result = JSON.parse(result_str);
if (!result.ok) {
html = "Can't upload image";
msg = result_str;
}
} catch (ex) {
html = "Can't parse upload result";
msg = `${ex}`;
}
if (html.length > 0) {
wm.error(html, msg);
}
}
tools.hidden.setVisible($("msd-new-sub"), false);
$("msd-new-file").value = "";
$("msd-new-url").value = "";
__http = null;
__refreshControls();
};
var __clickAbortNewButton = function() {
__http.onreadystatechange = null;
__http.abort();
__http = null;
tools.progress.setValue($("msd-uploading-progress"), "Aborted", 0);
__refreshControls();
tools.hidden.setVisible($("msd-new-sub"), true);
};
var __clickConnectButton = function(connected) {
tools.httpPost(`/api/msd/set_connected?connected=${connected}`, function(http) {
tools.httpPost("/api/msd/set_connected", {"connected": connected}, function(http) {
if (http.status !== 200) {
wm.error("Switch error:<br>", http.responseText);
wm.error("Can't switch Mass Storage", http.responseText);
}
__applyState();
__refreshControls();
});
__applyState();
__refreshControls();
tools.el.setEnabled($(`msd-${connected ? "connect" : "disconnect"}-button`), false);
};
var __clickResetButton = function() {
wm.confirm("Are you sure you want to reset Mass Storage Drive?").then(function(ok) {
wm.confirm("Are you sure you want to reset Mass Storage?").then(function(ok) {
if (ok) {
tools.httpPost("/api/msd/reset", function(http) {
tools.httpPost("/api/msd/reset", null, function(http) {
if (http.status !== 200) {
wm.error("MSD reset error:<br>", http.responseText);
wm.error("Mass Storage reset error", http.responseText);
}
__applyState();
});
__applyState();
}
});
};
@@ -191,193 +385,35 @@ export function Msd() {
var __toggleSelectSub = function() {
let el_sub = $("msd-new-sub");
let visible = tools.hidden.isVisible(el_sub);
tools.hidden.setVisible(el_sub, !visible);
if (visible) {
$("msd-new-file").value = "";
$("msd-new-url").value = "";
}
tools.hidden.setVisible(el_sub, !visible);
__applyState();
__refreshControls();
};
var __selectNewFile = function() {
let el_input = $("msd-new-file");
let file = tools.input.getFile($("msd-new-file"));
let el = $("msd-new-file");
let file = tools.input.getFile(el);
if (file) {
$("msd-new-url").value = "";
let part = __state.storage.parts[$("msd-new-part-selector").value];
if (file.size > part.size) {
wm.error("New image is too big for the MSD partition.<br>Maximum:", tools.formatSize(part.size));
el_input.value = "";
if (__state && __state.storage && __state.storage.parts) {
let part = __state.storage.parts[$("msd-new-part-selector").value];
if (part && (file.size > part.size)) {
wm.error(`The new image is too big for the Mass Storage partition.<br>Maximum: ${tools.formatSize(part.size)}`);
el.value = "";
}
}
}
__applyState();
__refreshControls();
};
var __selectNewUrl = function() {
if ($("msd-new-url").value.length > 0) {
$("msd-new-file").value = "";
}
__applyState();
};
var __applyState = function() {
__applyStateStatus();
let s = __state;
let online = (s && s.online);
if (s) {
tools.feature.setEnabled($("msd-dropdown"), s.enabled);
tools.feature.setEnabled($("msd-reset-button"), s.enabled);
}
tools.hidden.setVisible($("msd-message-offline"), (s && !s.online));
tools.hidden.setVisible($("msd-message-image-broken"), (online && s.drive.image && !s.drive.image.complete && !s.storage.uploading));
tools.hidden.setVisible($("msd-message-too-big-for-cdrom"), (online && s.drive.cdrom && s.drive.image && s.drive.image.size >= 2359296000));
tools.hidden.setVisible($("msd-message-out-of-storage"), (online && s.drive.image && !s.drive.image.in_storage));
tools.hidden.setVisible($("msd-message-rw-enabled"), (online && s.drive.rw));
tools.hidden.setVisible($("msd-message-another-user-uploads"), (online && s.storage.uploading && !__http));
tools.hidden.setVisible($("msd-message-downloads"), (online && s.storage.downloading));
if (online) {
let names = Object.keys(s.storage.parts).sort();
let parts_names_json = JSON.stringify(names);
if (__parts_names_json !== parts_names_json) {
$("msd-storages").innerHTML = names.map(name => `
<div class="text">
<div id="msd-storage-${tools.makeIdByText(name)}-progress" class="progress">
<span class="progress-value"></span>
</div>
</div>
`).join("<hr>");
__parts_names_json = parts_names_json;
__parts_names_len = names.length;
}
__parts = s.storage.parts;
}
for (let name in __parts) {
let part = __parts[name];
let title = (
name.length === 0
? `${__parts_names_len === 1 ? "Storage: %s" : "Internal storage: %s"}` // eslint-disable-line
: `Storage [${name}${part.writable ? "]" : ", read-only]"}: %s` // eslint-disable-line
);
let id = `msd-storage-${tools.makeIdByText(name)}-progress`;
if (online) {
tools.progress.setSizeOf($(id), title, part.size, part.free);
} else {
tools.progress.setValue($(id), title.replace("%s", "unavailable"), 0);
}
}
tools.el.setEnabled($("msd-image-selector"), (online && !s.drive.connected && !s.busy));
__applyStateImageSelector();
tools.el.setEnabled($("msd-download-button"), (online && s.drive.image && !s.drive.connected && !s.busy));
tools.el.setEnabled($("msd-remove-button"), (online && s.drive.image && s.drive.image.removable && !s.drive.connected && !s.busy));
tools.radio.setEnabled("msd-mode-radio", (online && !s.drive.connected && !s.busy));
tools.radio.setValue("msd-mode-radio", `${Number(online && s.drive.cdrom)}`);
tools.el.setEnabled($("msd-rw-switch"), (online && !s.drive.connected && !s.busy));
$("msd-rw-switch").checked = (online && s.drive.rw);
tools.el.setEnabled($("msd-connect-button"), (online && s.drive.image && !s.drive.connected && !s.busy));
tools.el.setEnabled($("msd-disconnect-button"), (online && s.drive.connected && !s.busy));
tools.el.setEnabled($("msd-select-new-button"), (online && !s.drive.connected && !__http && !s.busy));
tools.el.setEnabled($("msd-upload-new-button"),
(online && !s.drive.connected && (tools.input.getFile($("msd-new-file")) || $("msd-new-url").value.length > 0) && !s.busy));
tools.el.setEnabled($("msd-abort-new-button"), (online && __http));
tools.el.setEnabled($("msd-reset-button"), (s && s.enabled && !s.busy));
tools.el.setEnabled($("msd-new-file"), (online && !s.drive.connected && !__http && !s.busy));
tools.el.setEnabled($("msd-new-url"), (online && !s.drive.connected && !__http && !s.busy));
tools.el.setEnabled($("msd-new-part-selector"), (online && !s.drive.connected && !__http && !s.busy));
if (online && !s.storage.uploading && !s.storage.downloading) {
let parts = Object.keys(s.storage.parts).sort().filter(name => (name === "" || s.storage.parts[name].writable));
tools.selector.setValues($("msd-new-part-selector"), parts, "\u2500 Internal \u2500");
tools.hidden.setVisible($("msd-new-part"), (parts.length > 1));
}
tools.hidden.setVisible($("msd-uploading-sub"), (online && s.storage.uploading));
$("msd-uploading-name").innerHTML = ((online && s.storage.uploading) ? s.storage.uploading.name : "");
$("msd-uploading-size").innerHTML = ((online && s.storage.uploading) ? tools.formatSize(s.storage.uploading.size) : "");
if (online) {
if (s.storage.uploading) {
tools.progress.setPercentOf($("msd-uploading-progress"), s.storage.uploading.size, s.storage.uploading.written);
} else if (!__http) {
tools.progress.setValue($("msd-uploading-progress"), "Waiting for upload (press UPLOAD button) ...", 0);
}
} else {
$("msd-new-file").value = "";
$("msd-new-url").value = "";
tools.progress.setValue($("msd-uploading-progress"), "", 0);
}
};
var __applyStateStatus = function() {
let s = __state;
let online = (s && s.online);
let led_cls = "led-gray";
let msg = "Unavailable";
if (online && s.drive.connected) {
led_cls = "led-green";
msg = "Connected to Server";
} else if (online && s.storage.uploading) {
led_cls = "led-yellow-rotating-fast";
msg = "Uploading new image";
} else if (online && s.storage.downloading) {
led_cls = "led-yellow-rotating-fast";
msg = "Serving the image to download";
} else if (online) { // Sic!
msg = "Disconnected";
}
$("msd-led").className = led_cls;
$("msd-status").innerHTML = $("msd-led").title = msg;
};
var __applyStateImageSelector = function() {
let s = __state;
if (!(s && s.online) || s.storage.uploading || s.storage.downloading) {
return;
}
let el = $("msd-image-selector");
el.options.length = 1;
let selected = "";
for (let name of Object.keys(s.storage.images).sort()) {
tools.selector.addSeparator(el);
tools.selector.addOption(el, name, name);
tools.selector.addComment(el, __makeImageSelectorInfo(s.storage.images[name]));
if (s.drive.image && s.drive.image.name === name && s.drive.image.in_storage) {
selected = name;
}
}
if (s.drive.image && !s.drive.image.in_storage) {
selected = ".__external";
tools.selector.addOption(el, s.drive.image.name, selected);
tools.selector.addComment(el, __makeImageSelectorInfo(s.drive.image));
}
el.value = selected;
};
var __makeImageSelectorInfo = function(image) {
let info = `\xA0\xA0\xA0\xA0\xA0\u2570 ${tools.formatSize(image.size)}`;
info += (image.complete ? "" : ", broken");
if (image.in_storage !== undefined && !image.in_storage) {
info += ", out of storage";
}
let dt = new Date(image.mod_ts * 1000);
dt = new Date(dt.getTime() - (dt.getTimezoneOffset() * 60000));
info += " \u2500 " + dt.toISOString().slice(0, -8).replaceAll("-", ".").replace("T", "-");
return info;
__refreshControls();
};
__init__();

View File

@@ -32,9 +32,11 @@ export function Ocr(__getGeometry) {
/************************************************************************/
var __enabled = null;
var __start_pos = null;
var __end_pos = null;
var __selection = null;
var __sel = null;
var __init__ = function() {
tools.el.setOnClick($("stream-ocr-button"), function() {
@@ -54,7 +56,7 @@ export function Ocr(__getGeometry) {
$("stream-ocr-window").onkeyup = function(event) {
event.preventDefault();
if (event.code === "Enter") {
if (__selection) {
if (__sel) {
__recognizeSelection();
wm.closeWindow($("stream-ocr-window"));
}
@@ -71,14 +73,29 @@ export function Ocr(__getGeometry) {
/************************************************************************/
self.setState = function(state) {
let enabled = (state && state.ocr.enabled && !tools.browser.is_mobile);
if (enabled) {
let el = $("stream-ocr-lang-selector");
tools.selector.setValues(el, state.ocr.langs.available);
tools.selector.setSelectedValue(el, tools.storage.get("stream.ocr.lang", state.ocr.langs["default"]));
if (state) {
if (state.enabled !== undefined) {
__enabled = (state.enabled && !tools.browser.is_mobile);
tools.feature.setEnabled($("stream-ocr"), __enabled);
$("stream-ocr-led").className = (__enabled ? "led-gray" : "hidden");
}
if (__enabled && state.langs !== undefined) {
__updateLangs(state.langs);
}
} else {
__enabled = false;
tools.feature.setEnabled($("stream-ocr"), false);
$("stream-ocr-led").className = "hidden";
}
tools.feature.setEnabled($("stream-ocr"), enabled);
$("stream-ocr-led").className = (enabled ? "led-gray" : "hidden");
};
var __updateLangs = function(langs) {
let el = $("stream-ocr-lang-selector");
el.options.length = 0;
for (let lang of langs.available) {
tools.selector.addOption(el, lang, lang);
}
el.value = tools.storage.get("stream.ocr.lang", langs["default"]);
};
var __startSelection = function(event) {
@@ -94,23 +111,23 @@ export function Ocr(__getGeometry) {
__end_pos = __getGlobalPosition(event);
let width = Math.abs(__start_pos.x - __end_pos.x);
let height = Math.abs(__start_pos.y - __end_pos.y);
let el_selection = $("stream-ocr-selection");
el_selection.style.left = Math.min(__start_pos.x, __end_pos.x) + "px";
el_selection.style.top = Math.min(__start_pos.y, __end_pos.y) + "px";
el_selection.style.width = width + "px";
el_selection.style.height = height + "px";
tools.hidden.setVisible(el_selection, (width > 1 || height > 1));
let el = $("stream-ocr-selection");
el.style.left = Math.min(__start_pos.x, __end_pos.x) + "px";
el.style.top = Math.min(__start_pos.y, __end_pos.y) + "px";
el.style.width = width + "px";
el.style.height = height + "px";
tools.hidden.setVisible(el, (width > 1 || height > 1));
}
};
var __endSelection = function(event) {
__changeSelection(event);
let el_selection = $("stream-ocr-selection");
let el = $("stream-ocr-selection");
let ok = (
el_selection.offsetWidth > 1 && el_selection.offsetHeight > 1
el.offsetWidth > 1 && el.offsetHeight > 1
&& __start_pos !== null && __end_pos !== null
);
tools.hidden.setVisible(el_selection, ok);
tools.hidden.setVisible(el, ok);
if (ok) {
let rect = $("stream-box").getBoundingClientRect();
let rel_left = Math.min(__start_pos.x, __end_pos.x) - rect.left;
@@ -119,14 +136,14 @@ export function Ocr(__getGeometry) {
let rel_top = Math.min(__start_pos.y, __end_pos.y) - rect.top + offset;
let rel_bottom = Math.max(__start_pos.y, __end_pos.y) - rect.top + offset;
let geo = __getGeometry();
__selection = {
__sel = {
"left": tools.remap(rel_left, geo.x, geo.width, 0, geo.real_width),
"right": tools.remap(rel_right, geo.x, geo.width, 0, geo.real_width),
"top": tools.remap(rel_top, geo.y, geo.height, 0, geo.real_height),
"bottom": tools.remap(rel_bottom, geo.y, geo.height, 0, geo.real_height),
};
} else {
__selection = null;
__sel = null;
}
__start_pos = null;
__end_pos = null;
@@ -154,20 +171,22 @@ export function Ocr(__getGeometry) {
tools.hidden.setVisible($("stream-ocr-selection"), false);
__start_pos = null;
__end_pos = null;
__selection = null;
__sel = null;
};
var __recognizeSelection = function() {
tools.el.setEnabled($("stream-ocr-button"), false);
tools.el.setEnabled($("stream-ocr-lang-selector"), false);
$("stream-ocr-led").className = "led-yellow-rotating-fast";
let lang = $("stream-ocr-lang-selector").value;
let url = `/api/streamer/snapshot?ocr=1&ocr_langs=${lang}`;
url += `&ocr_left=${__selection.left}&ocr_top=${__selection.top}`;
url += `&ocr_right=${__selection.right}&ocr_bottom=${__selection.bottom}`;
tools.httpGet(url, function(http) {
let params = {
"ocr": 1,
"ocr_langs": $("stream-ocr-lang-selector").value,
"ocr_left": __sel.left,
"ocr_top": __sel.top,
"ocr_right": __sel.right,
"orc_bottom": __sel.bottom,
};
tools.httpGet("/api/streamer/snapshot", params, function(http) {
if (http.status === 200) {
wm.copyTextToClipboard(http.responseText);
} else {

112
web/share/js/kvm/paste.js Normal file
View File

@@ -0,0 +1,112 @@
/*****************************************************************************
# #
# KVMD - The main PiKVM daemon. #
# #
# Copyright (C) 2018-2024 Maxim Devaev <mdevaev@gmail.com> #
# #
# This program is free software: you can redistribute it and/or modify #
# it under the terms of the GNU General Public License as published by #
# the Free Software Foundation, either version 3 of the License, or #
# (at your option) any later version. #
# #
# This program is distributed in the hope that it will be useful, #
# but WITHOUT ANY WARRANTY; without even the implied warranty of #
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the #
# GNU General Public License for more details. #
# #
# You should have received a copy of the GNU General Public License #
# along with this program. If not, see <https://www.gnu.org/licenses/>. #
# #
*****************************************************************************/
"use strict";
import {tools, $} from "../tools.js";
import {wm} from "../wm.js";
export function Paste(__recorder) {
var self = this;
/************************************************************************/
var __init__ = function() {
tools.storage.bindSimpleSwitch($("hid-pak-ask-switch"), "hid.pak.ask", true);
tools.storage.bindSimpleSwitch($("hid-pak-secure-switch"), "hid.pak.secure", false, function(value) {
$("hid-pak-text").style.setProperty("-webkit-text-security", (value ? "disc" : "none"));
});
tools.feature.setEnabled($("hid-pak-secure"), (
tools.browser.is_chrome
|| tools.browser.is_safari
|| tools.browser.is_opera
));
$("hid-pak-keymap-selector").addEventListener("change", function() {
tools.storage.set("hid.pak.keymap", $("hid-pak-keymap-selector").value);
});
tools.el.setOnClick($("hid-pak-button"), __clickPasteAsKeysButton);
};
/************************************************************************/
self.setState = function(state) {
tools.el.setEnabled($("hid-pak-text"), state);
tools.el.setEnabled($("hid-pak-button"), state);
if (state) {
let el = $("hid-pak-keymap-selector");
let sel = tools.storage.get("hid.pak.keymap", state.keymaps["default"]);
el.options.length = 0;
for (let keymap of state.keymaps.available) {
tools.selector.addOption(el, keymap, keymap, (keymap === sel));
}
}
};
var __clickPasteAsKeysButton = function() {
let text = $("hid-pak-text").value;
if (text) {
let paste_as_keys = function() {
tools.el.setEnabled($("hid-pak-text"), false);
tools.el.setEnabled($("hid-pak-button"), false);
tools.el.setEnabled($("hid-pak-keymap-selector"), false);
let keymap = $("hid-pak-keymap-selector").value;
tools.debug(`HID: paste-as-keys ${keymap}: ${text}`);
tools.httpPost("/api/hid/print", {"limit": 0, "keymap": keymap}, function(http) {
tools.el.setEnabled($("hid-pak-text"), true);
tools.el.setEnabled($("hid-pak-button"), true);
tools.el.setEnabled($("hid-pak-keymap-selector"), true);
$("hid-pak-text").value = "";
if (http.status === 413) {
wm.error("Too many text for paste!");
} else if (http.status !== 200) {
wm.error("HID paste error", http.responseText);
} else if (http.status === 200) {
__recorder.recordPrintEvent(text, keymap);
}
}, text, "text/plain");
};
if ($("hid-pak-ask-switch").checked) {
wm.confirm(`
You're going to paste ${text.length} character${text.length ? "s" : ""}.<br>
Are you sure you want to continue?
`).then(function(ok) {
if (ok) {
paste_as_keys();
} else {
$("hid-pak-text").value = "";
}
});
} else {
paste_as_keys();
}
}
};
__init__();
}

View File

@@ -67,8 +67,8 @@ export function Recorder() {
__recordEvent(event);
};
self.recordPrintEvent = function(text) {
__recordEvent({"event_type": "print", "event": {"text": text}});
self.recordPrintEvent = function(text, keymap) {
__recordEvent({"event_type": "print", "event": {"text": text, "keymap": keymap}});
};
self.recordAtxButtonEvent = function(button) {
@@ -159,6 +159,9 @@ export function Recorder() {
} else if (event.event_type === "print") {
__checkType(event.event.text, "string", "Non-string print text");
if (event.event.keymap) {
__checkType(event.event.keymap, "string", "Non-string keymap");
}
} else if (event.event_type === "key") {
__checkType(event.event.key, "string", "Non-string key code");
@@ -214,8 +217,8 @@ export function Recorder() {
__events = events;
__events_time = events_time;
} catch (err) {
wm.error(`Invalid script: ${err}`);
} catch (ex) {
wm.error("Invalid script", `${ex}`);
}
el_input.value = "";
@@ -280,12 +283,16 @@ export function Recorder() {
return;
} else if (event.event_type === "print") {
tools.httpPost("/api/hid/print?limit=0", function(http) {
let params = {"limit": 0};
if (event.event.keymap) {
params["keymap"] = event.event.keymap;
}
tools.httpPost("/api/hid/print", params, function(http) {
if (http.status === 413) {
wm.error("Too many text for paste!");
__stopProcess();
} else if (http.status !== 200) {
wm.error("HID paste error:<br>", http.responseText);
wm.error("HID paste error", http.responseText);
__stopProcess();
} else if (http.status === 200) {
__play_timer = setTimeout(() => __runEvents(index + 1, time), 0);
@@ -294,9 +301,9 @@ export function Recorder() {
return;
} else if (event.event_type === "atx_button") {
tools.httpPost(`/api/atx/click?button=${event.event.button}`, function(http) {
tools.httpPost("/api/atx/click", {"button": event.event.button}, function(http) {
if (http.status !== 200) {
wm.error("ATX error:<br>", http.responseText);
wm.error("ATX error", http.responseText);
__stopProcess();
} else if (http.status === 200) {
__play_timer = setTimeout(() => __runEvents(index + 1, time), 0);
@@ -306,14 +313,16 @@ export function Recorder() {
} else if (["gpio_switch", "gpio_pulse"].includes(event.event_type)) {
let path = "/api/gpio";
let params = {"channel": event.event.channel};
if (event.event_type === "gpio_switch") {
path += `/switch?channel=${event.event.channel}&state=${event.event.to}`;
path += "/switch";
params["state"] = event.event.to;
} else { // gpio_pulse
path += `/pulse?channel=${event.event.channel}`;
path += "/pulse";
}
tools.httpPost(path, function(http) {
tools.httpPost(path, params, function(http) {
if (http.status !== 200) {
wm.error("GPIO error:<br>", http.responseText);
wm.error("GPIO error", http.responseText);
__stopProcess();
} else if (http.status === 200) {
__play_timer = setTimeout(() => __runEvents(index + 1, time), 0);

View File

@@ -28,6 +28,7 @@ import {wm} from "../wm.js";
import {Recorder} from "./recorder.js";
import {Hid} from "./hid.js";
import {Paste} from "./paste.js";
import {Atx} from "./atx.js";
import {Msd} from "./msd.js";
import {Streamer} from "./stream.js";
@@ -48,6 +49,7 @@ export function Session() {
var __streamer = new Streamer();
var __recorder = new Recorder();
var __hid = new Hid(__streamer.getGeometry, __recorder);
var __paste = new Paste(__recorder);
var __atx = new Atx(__recorder);
var __msd = new Msd();
var __gpio = new Gpio(__recorder);
@@ -57,30 +59,42 @@ export function Session() {
var __info_fan_state = null;
var __init__ = function() {
__startSession();
__streamer.ensureDeps(() => __startSession());
};
/************************************************************************/
var __setAboutInfoMeta = function(state) {
var __setInfoState = function(state) {
for (let key of Object.keys(state)) {
switch (key) {
case "meta": __setInfoStateMeta(state.meta); break;
case "hw": __setInfoStateHw(state.hw); break;
case "fan": __setInfoStateFan(state.fan); break;
case "system": __setInfoStateSystem(state.system); break;
case "extras": __setInfoStateExtras(state.extras); break;
}
}
};
var __setInfoStateMeta = function(state) {
if (state !== null) {
let text = JSON.stringify(state, undefined, 4).replace(/ /g, "&nbsp;").replace(/\n/g, "<br>");
$("about-meta").innerHTML = `
<span class="code-comment">// The PiKVM metadata.<br>
// You can get this JSON using handle <a target="_blank" href="/api/info?fields=meta">/api/info?fields=meta</a>.<br>
// In the standard configuration this data<br>
// is specified in the file /etc/kvmd/meta.yaml.</span><br>
<br>
${text}
`;
$("kvmd-meta-json").innerText = JSON.stringify(state, undefined, 4);
if (state.server && state.server.host) {
$("kvmd-meta-server-host").innerHTML = `Server: ${state.server.host}`;
$("kvmd-meta-server-host").innerText = `Server: ${state.server.host}`;
document.title = `PiKVM Session: ${state.server.host}`;
} else {
$("kvmd-meta-server-host").innerHTML = "";
$("kvmd-meta-server-host").innerText = "";
document.title = "PiKVM Session";
}
if (state.tips && state.tips.left) {
$("kvmd-meta-tips-left").innerText = `${state.tips.left}`;
}
if (state.tips && state.tips.right) {
$("kvmd-meta-tips-right").innerText = `${state.tips.right}`;
}
// Don't use this option, it may be removed in any time
if (state.web && state.web.confirm_session_exit === false) {
window.onbeforeunload = null; // See main.js
@@ -88,7 +102,7 @@ export function Session() {
}
};
var __setAboutInfoHw = function(state) {
var __setInfoStateHw = function(state) {
if (state.health.throttling !== null) {
let flags = state.health.throttling.parsed_flags;
let ignore_past = state.health.throttling.ignore_past;
@@ -105,7 +119,7 @@ export function Session() {
__renderAboutInfoHardware();
};
var __setAboutInfoFan = function(state) {
var __setInfoStateFan = function(state) {
let failed = false;
let failed_past = false;
if (state.monitored) {
@@ -207,11 +221,11 @@ export function Session() {
}
};
var __colored = function(color, text) {
return `<font color="${color}">${text}</font>`;
var __colored = function(color, html) {
return `<font color="${color}">${html}</font>`;
};
var __setAboutInfoSystem = function(state) {
var __setInfoStateSystem = function(state) {
$("about-version").innerHTML = `
KVMD: <span class="code-comment">${state.kvmd.version}</span><br>
<hr>
@@ -221,8 +235,8 @@ export function Session() {
${state.kernel.system} kernel:
${__formatUname(state.kernel)}
`;
$("kvmd-version-kvmd").innerHTML = state.kvmd.version;
$("kvmd-version-streamer").innerHTML = state.streamer.version;
$("kvmd-version-kvmd").innerText = state.kvmd.version;
$("kvmd-version-streamer").innerText = state.streamer.version;
};
var __formatStreamerFeatures = function(features) {
@@ -244,14 +258,14 @@ export function Session() {
};
var __formatUl = function(pairs) {
let text = "<ul>";
let html = "";
for (let pair of pairs) {
text += `<li>${pair[0]}: <span class="code-comment">${pair[1]}</span></li>`;
html += `<li>${pair[0]}: <span class="code-comment">${pair[1]}</span></li>`;
}
return text + "</ul>";
return `<ul>${html}</ul>`;
};
var __setExtras = function(state) {
var __setInfoStateExtras = function(state) {
let show_hook = null;
let close_hook = null;
let has_webterm = (state.webterm && (state.webterm.enabled || state.webterm.started));
@@ -269,20 +283,15 @@ export function Session() {
tools.feature.setEnabled($("system-tool-webterm"), has_webterm);
$("webterm-window").show_hook = show_hook;
$("webterm-window").close_hook = close_hook;
__streamer.setJanusEnabled(
(state.janus && (state.janus.enabled || state.janus.started))
|| (state.janus_static && (state.janus_static.enabled || state.janus_static.started))
);
};
var __startSession = function() {
$("link-led").className = "led-yellow";
$("link-led").title = "Connecting...";
tools.httpGet("/api/auth/check", function(http) {
tools.httpGet("/api/auth/check", null, function(http) {
if (http.status === 200) {
__ws = new WebSocket(`${tools.is_https ? "wss" : "ws"}://${location.host}/api/ws`);
__ws = new WebSocket(`${tools.is_https ? "wss" : "ws"}://${location.host}/api/ws?legacy=0`);
__ws.sendHidEvent = (event) => __sendHidEvent(__ws, event.event_type, event.event);
__ws.onopen = __wsOpenHandler;
__ws.onmessage = __wsMessageHandler;
@@ -354,19 +363,14 @@ export function Session() {
let data = JSON.parse(event.data);
switch (data.event_type) {
case "pong": __missed_heartbeats = 0; break;
case "info_meta_state": __setAboutInfoMeta(data.event); break;
case "info_hw_state": __setAboutInfoHw(data.event); break;
case "info_fan_state": __setAboutInfoFan(data.event); break;
case "info_system_state": __setAboutInfoSystem(data.event); break;
case "info_extras_state": __setExtras(data.event); break;
case "gpio_model_state": __gpio.setModel(data.event); break;
case "info_state": __setInfoState(data.event); break;
case "gpio_state": __gpio.setState(data.event); break;
case "hid_keymaps_state": __hid.setKeymaps(data.event); break;
case "hid_state": __hid.setState(data.event); break;
case "hid_keymaps_state": __paste.setState(data.event); break;
case "atx_state": __atx.setState(data.event); break;
case "msd_state": __msd.setState(data.event); break;
case "streamer_state": __streamer.setState(data.event); break;
case "streamer_ocr_state": __ocr.setState(data.event); break;
case "ocr_state": __ocr.setState(data.event); break;
}
};
@@ -389,13 +393,14 @@ export function Session() {
__ping_timer = null;
}
__ocr.setState(null);
__gpio.setState(null);
__hid.setSocket(null);
__recorder.setSocket(null);
__hid.setSocket(null); // auto setState(null);
__paste.setState(null);
__atx.setState(null);
__msd.setState(null);
__streamer.setState(null);
__ocr.setState(null);
__recorder.setSocket(null);
__ws = null;
setTimeout(function() {
@@ -411,8 +416,8 @@ export function Session() {
throw new Error("Too many missed heartbeats");
}
__ws.send("{\"event_type\": \"ping\", \"event\": {}}");
} catch (err) {
__wsErrorHandler(err.message);
} catch (ex) {
__wsErrorHandler(ex.message);
}
};

View File

@@ -35,11 +35,11 @@ export function Streamer() {
/************************************************************************/
var __janus_enabled = null;
var __janus_imported = null;
var __streamer = null;
var __state = null;
var __resolution = {"width": 640, "height": 480};
var __res = {"width": 640, "height": 480};
var __init__ = function() {
__streamer = new MjpegStreamer(__setActive, __setInactive, __setInfo);
@@ -47,22 +47,22 @@ export function Streamer() {
$("stream-led").title = "Stream inactive";
tools.slider.setParams($("stream-quality-slider"), 5, 100, 5, 80, function(value) {
$("stream-quality-value").innerHTML = `${value}%`;
$("stream-quality-value").innerText = `${value}%`;
});
tools.slider.setOnUpDelayed($("stream-quality-slider"), 1000, (value) => __sendParam("quality", value));
tools.slider.setParams($("stream-h264-bitrate-slider"), 25, 20000, 25, 5000, function(value) {
$("stream-h264-bitrate-value").innerHTML = value;
$("stream-h264-bitrate-value").innerText = value;
});
tools.slider.setOnUpDelayed($("stream-h264-bitrate-slider"), 1000, (value) => __sendParam("h264_bitrate", value));
tools.slider.setParams($("stream-h264-gop-slider"), 0, 60, 1, 30, function(value) {
$("stream-h264-gop-value").innerHTML = value;
$("stream-h264-gop-value").innerText = value;
});
tools.slider.setOnUpDelayed($("stream-h264-gop-slider"), 1000, (value) => __sendParam("h264_gop", value));
tools.slider.setParams($("stream-desired-fps-slider"), 0, 120, 1, 0, function(value) {
$("stream-desired-fps-value").innerHTML = (value === 0 ? "Unlimited" : value);
$("stream-desired-fps-value").innerText = (value === 0 ? "Unlimited" : value);
});
tools.slider.setOnUpDelayed($("stream-desired-fps-slider"), 1000, (value) => __sendParam("desired_fps", value));
@@ -86,7 +86,7 @@ export function Streamer() {
tools.slider.setParams($("stream-audio-volume-slider"), 0, 100, 1, 0, function(value) {
$("stream-video").muted = !value;
$("stream-video").volume = value / 100;
$("stream-audio-volume-value").innerHTML = value + "%";
$("stream-audio-volume-value").innerText = value + "%";
if (__streamer.getMode() === "janus") {
let allow_audio = !$("stream-video").muted;
if (__streamer.isAudioAllowed() !== allow_audio) {
@@ -110,6 +110,13 @@ export function Streamer() {
/************************************************************************/
self.ensureDeps = function(callback) {
JanusStreamer.ensure_janus(function(avail) {
__janus_imported = avail;
callback();
});
};
self.getGeometry = function() {
// Первоначально обновление геометрии считалось через ResizeObserver.
// Но оно не ловило некоторые события, например в последовательности:
@@ -132,90 +139,103 @@ export function Streamer() {
};
};
self.setJanusEnabled = function(enabled) {
let has_webrtc = JanusStreamer.is_webrtc_available();
let has_h264 = JanusStreamer.is_h264_available();
let set_enabled = function(imported) {
tools.hidden.setVisible($("stream-message-no-webrtc"), enabled && !has_webrtc);
tools.hidden.setVisible($("stream-message-no-h264"), enabled && !has_h264);
__janus_enabled = (enabled && has_webrtc && imported); // Don't check has_h264 for sure
tools.feature.setEnabled($("stream-mode"), __janus_enabled);
tools.info(
`Stream: Janus WebRTC state: enabled=${enabled},`
+ ` webrtc=${has_webrtc}, h264=${has_h264}, imported=${imported}`
);
let mode = (__janus_enabled ? tools.storage.get("stream.mode", "janus") : "mjpeg");
tools.radio.clickValue("stream-mode-radio", mode);
if (!__janus_enabled) {
tools.feature.setEnabled($("stream-audio"), false); // Enabling in stream_janus.js
}
self.setState(__state);
};
if (enabled && has_webrtc) {
JanusStreamer.ensure_janus(set_enabled);
} else {
set_enabled(false);
}
};
self.setState = function(state) {
__state = state;
if (__janus_enabled !== null) {
__applyState(wm.isWindowVisible($("stream-window")) ? __state : null);
if (state) {
if (!__state) {
__state = {};
}
if (state.features !== undefined) {
__state.features = state.features;
__state.limits = state.limits; // Following together with features
}
if (__state.features !== undefined && state.streamer !== undefined) {
__state.streamer = state.streamer;
__setControlsEnabled(!!state.streamer);
}
} else {
__state = null;
__setControlsEnabled(false);
}
let visible = wm.isWindowVisible($("stream-window"));
__applyState((visible && __state && __state.features) ? state : null);
};
var __applyState = function(state) {
if (state) {
tools.feature.setEnabled($("stream-quality"), state.features.quality && (state.streamer === null || state.streamer.encoder.quality > 0));
tools.feature.setEnabled($("stream-h264-bitrate"), state.features.h264 && __janus_enabled);
tools.feature.setEnabled($("stream-h264-gop"), state.features.h264 && __janus_enabled);
tools.feature.setEnabled($("stream-resolution"), state.features.resolution);
if (__janus_imported === null) {
alert("__janus_imported is null, please report");
return;
}
if (state.streamer) {
tools.el.setEnabled($("stream-quality-slider"), true);
tools.slider.setValue($("stream-quality-slider"), state.streamer.encoder.quality);
if (!state) {
__streamer.stopStream();
return;
}
if (state.features.h264 && __janus_enabled) {
__setLimitsAndValue($("stream-h264-bitrate-slider"), state.limits.h264_bitrate, state.streamer.h264.bitrate);
tools.el.setEnabled($("stream-h264-bitrate-slider"), true);
if (state.features) {
let f = state.features;
let l = state.limits;
let has_webrtc = JanusStreamer.is_webrtc_available();
let has_h264 = JanusStreamer.is_h264_available();
let has_janus = (__janus_imported && f.h264 && has_webrtc); // Don't check has_h264 for sure
__setLimitsAndValue($("stream-h264-gop-slider"), state.limits.h264_gop, state.streamer.h264.gop);
tools.el.setEnabled($("stream-h264-gop-slider"), true);
tools.info(
`Stream: Janus WebRTC state: features.h264=${f.h264},`
+ ` webrtc=${has_webrtc}, h264=${has_h264}, janus_imported=${__janus_imported}`
);
tools.hidden.setVisible($("stream-message-no-webrtc"), __janus_imported && f.h264 && !has_webrtc);
tools.hidden.setVisible($("stream-message-no-h264"), __janus_imported && f.h264 && !has_h264);
tools.slider.setRange($("stream-desired-fps-slider"), l.desired_fps.min, l.desired_fps.max);
if (f.resolution) {
let el = $("stream-resolution-selector");
el.options.length = 0;
for (let res of l.available_resolutions) {
tools.selector.addOption(el, res, res);
}
__setLimitsAndValue($("stream-desired-fps-slider"), state.limits.desired_fps, state.streamer.source.desired_fps);
tools.el.setEnabled($("stream-desired-fps-slider"), true);
let resolution_str = __makeStringResolution(state.streamer.source.resolution);
if (__makeStringResolution(__resolution) !== resolution_str) {
__resolution = state.streamer.source.resolution;
}
if (state.features.resolution) {
let el = $("stream-resolution-selector");
if (!state.limits.available_resolutions.includes(resolution_str)) {
state.limits.available_resolutions.push(resolution_str);
}
tools.selector.setValues(el, state.limits.available_resolutions);
tools.selector.setSelectedValue(el, resolution_str);
tools.el.setEnabled(el, true);
}
} else {
tools.el.setEnabled($("stream-quality-slider"), false);
tools.el.setEnabled($("stream-h264-bitrate-slider"), false);
tools.el.setEnabled($("stream-h264-gop-slider"), false);
tools.el.setEnabled($("stream-desired-fps-slider"), false);
tools.el.setEnabled($("stream-resolution-selector"), false);
$("stream-resolution-selector").options.length = 0;
}
if (has_janus) {
tools.slider.setRange($("stream-h264-bitrate-slider"), l.h264_bitrate.min, l.h264_bitrate.max);
tools.slider.setRange($("stream-h264-gop-slider"), l.h264_gop.min, l.h264_gop.max);
}
__streamer.ensureStream(state.streamer);
// tools.feature.setEnabled($("stream-quality"), f.quality); // Only on s.encoder.quality
tools.feature.setEnabled($("stream-resolution"), f.resolution);
tools.feature.setEnabled($("stream-h264-bitrate"), has_janus);
tools.feature.setEnabled($("stream-h264-gop"), has_janus);
tools.feature.setEnabled($("stream-mode"), has_janus);
if (!has_janus) {
tools.feature.setEnabled($("stream-audio"), false);
}
} else {
__streamer.stopStream();
let mode = (has_janus ? tools.storage.get("stream.mode", "janus") : "mjpeg");
tools.radio.clickValue("stream-mode-radio", mode);
}
if (state.streamer) {
let s = state.streamer;
__res = s.source.resolution;
{
let res = `${__res.width}x${__res.height}`;
let el = $("stream-resolution-selector");
if (!tools.selector.hasValue(el, res)) {
tools.selector.addOption(el, res, res);
}
el.value = res;
}
tools.slider.setValue($("stream-quality-slider"), Math.max(s.encoder.quality, 1));
tools.slider.setValue($("stream-desired-fps-slider"), s.source.desired_fps);
if (s.h264 && s.h264.bitrate) {
tools.slider.setValue($("stream-h264-bitrate-slider"), s.h264.bitrate);
tools.slider.setValue($("stream-h264-gop-slider"), s.h264.gop); // Following together with gop
}
tools.feature.setEnabled($("stream-quality"), (s.encoder.quality > 0));
__streamer.ensureStream(s);
}
};
@@ -229,16 +249,24 @@ export function Streamer() {
$("stream-led").title = "Stream inactive";
};
var __setControlsEnabled = function(enabled) {
tools.el.setEnabled($("stream-quality-slider"), enabled);
tools.el.setEnabled($("stream-desired-fps-slider"), enabled);
tools.el.setEnabled($("stream-resolution-selector"), enabled);
tools.el.setEnabled($("stream-h264-bitrate-slider"), enabled);
tools.el.setEnabled($("stream-h264-gop-slider"), enabled);
};
var __setInfo = function(is_active, online, text) {
$("stream-box").classList.toggle("stream-box-offline", !online);
let el_grab = document.querySelector("#stream-window-header .window-grab");
let el_info = $("stream-info");
let title = `${__streamer.getName()} &ndash; `;
let title = `${__streamer.getName()} - `;
if (is_active) {
if (!online) {
title += "No signal / ";
}
title += __makeStringResolution(__resolution);
title += `${__res.width}x${__res.height}`;
if (text.length > 0) {
title += " / " + text;
}
@@ -249,12 +277,7 @@ export function Streamer() {
title += "Inactive";
}
}
el_grab.innerHTML = el_info.innerHTML = title;
};
var __setLimitsAndValue = function(el, limits, value) {
tools.slider.setRange(el, limits.min, limits.max);
tools.slider.setValue(el, value);
el_grab.innerText = el_info.innerText = title;
};
var __resetStream = function(mode=null) {
@@ -274,7 +297,7 @@ export function Streamer() {
tools.feature.setEnabled($("stream-audio"), false); // Enabling in stream_janus.js
}
if (wm.isWindowVisible($("stream-window"))) {
__streamer.ensureStream(__state ? __state.streamer : null);
__streamer.ensureStream((__state && __state.streamer !== undefined) ? __state.streamer : null);
}
};
@@ -298,12 +321,12 @@ export function Streamer() {
};
var __clickResetButton = function() {
wm.confirm("Are you sure you want to reset stream?").then(function (ok) {
wm.confirm("Are you sure you want to reset stream?").then(function(ok) {
if (ok) {
__resetStream();
tools.httpPost("/api/streamer/reset", function(http) {
tools.httpPost("/api/streamer/reset", null, function(http) {
if (http.status !== 200) {
wm.error("Can't reset stream:<br>", http.responseText);
wm.error("Can't reset stream", http.responseText);
}
});
}
@@ -382,16 +405,12 @@ export function Streamer() {
};
var __sendParam = function(name, value) {
tools.httpPost(`/api/streamer/set_params?${name}=${value}`, function(http) {
tools.httpPost("/api/streamer/set_params", {[name]: value}, function(http) {
if (http.status !== 200) {
wm.error("Can't configure stream:<br>", http.responseText);
wm.error("Can't configure stream", http.responseText);
}
});
};
var __makeStringResolution = function(resolution) {
return `${resolution.width}x${resolution.height}`;
};
__init__();
}

View File

@@ -248,6 +248,13 @@ export function JanusStreamer(__setActive, __setInactive, __setInfo, __orient, _
// Janus 0.x
"media": {"audioSend": false, "videoSend": false, "data": false},
// Chrome is playing OPUS as mono without this hack
// - https://issues.webrtc.org/issues/41481053 - IT'S NOT FIXED!
// - https://github.com/ossrs/srs/pull/2683/files
"customizeSdp": function(jsep) {
jsep.sdp = jsep.sdp.replace("useinbandfec=1", "useinbandfec=1;stereo=1");
},
"success": function(jsep) {
__logInfo("Got SDP:", jsep);
__sendStart(jsep);
@@ -376,7 +383,7 @@ export function JanusStreamer(__setActive, __setInactive, __setInfo, __orient, _
};
var __isOnline = function() {
return !!(__state && __state.source && __state.source.online);
return !!(__state && __state.source.online);
};
var __sendWatch = function() {
@@ -428,8 +435,8 @@ JanusStreamer.ensure_janus = function(callback) {
callback(true);
},
});
}).catch((err) => {
tools.error("Stream: Can't import Janus module:", err);
}).catch((ex) => {
tools.error("Stream: Can't import Janus module:", ex);
callback(false);
});
} else {

View File

@@ -117,10 +117,10 @@ export function MjpegStreamer(__setActive, __setInactive, __setInfo) {
};
var __findId = function() {
let stream_client = tools.cookies.get("stream_client");
if (__id.length === 0 && stream_client && stream_client.startsWith(__key + "/")) {
__logInfo("Found acceptable stream_client cookie:", stream_client);
__id = stream_client.slice(stream_client.indexOf("/") + 1);
let sc = tools.cookies.get("stream_client");
if (__id.length === 0 && sc && sc.startsWith(__key + "/")) {
__logInfo("Found acceptable stream_client cookie:", sc);
__id = sc.slice(sc.indexOf("/") + 1);
}
};

View File

@@ -51,7 +51,7 @@ function __login() {
} else {
let passwd = $("passwd-input").value + $("code-input").value;
let body = `user=${encodeURIComponent(user)}&passwd=${encodeURIComponent(passwd)}`;
tools.httpPost("/api/auth/login", function(http) {
tools.httpPost("/api/auth/login", null, function(http) {
if (http.status === 200) {
document.location.href = "/";
} else if (http.status === 403) {
@@ -59,12 +59,12 @@ function __login() {
} else {
let error = "";
if (http.status === 400) {
try { error = JSON.parse(http.responseText)["result"]["error"]; } catch (_) { /* Nah */ }
try { error = JSON.parse(http.responseText)["result"]["error"]; } catch { /* Nah */ }
}
if (error === "ValidatorError") {
wm.error("Invalid characters in credentials").then(__tryAgain);
} else {
wm.error("Login error:<br>", http.responseText).then(__tryAgain);
wm.error("Login error", http.responseText).then(__tryAgain);
}
}
}, body, "application/x-www-form-urlencoded");

View File

@@ -39,7 +39,13 @@ export var tools = new function() {
/************************************************************************/
self.httpRequest = function(method, url, callback, body=null, content_type=null, timeout=15000) {
self.httpRequest = function(method, url, params, callback, body=null, content_type=null, timeout=15000) {
if (params) {
params = new URLSearchParams(params);
if (params) {
url += "?" + params;
}
}
let http = new XMLHttpRequest();
http.open(method, url, true);
if (content_type) {
@@ -54,16 +60,27 @@ export var tools = new function() {
http.send(body);
};
self.httpGet = function(url, callback, body=null, content_type=null, timeout=15000) {
self.httpRequest("GET", url, callback, body, content_type, timeout);
self.httpGet = function(url, params, callback, body=null, content_type=null, timeout=15000) {
self.httpRequest("GET", url, params, callback, body, content_type, timeout);
};
self.httpPost = function(url, callback, body=null, content_type=null, timeout=15000) {
self.httpRequest("POST", url, callback, body, content_type, timeout);
self.httpPost = function(url, params, callback, body=null, content_type=null, timeout=15000) {
self.httpRequest("POST", url, params, callback, body, content_type, timeout);
};
/************************************************************************/
self.escape = function(text) {
return text.replace(
/[^0-9A-Za-z ]/g,
ch => "&#" + ch.charCodeAt(0) + ";"
);
};
self.partial = function(func, ...args) {
return () => func(...args);
};
self.upperFirst = function(text) {
return text[0].toUpperCase() + text.slice(1);
};
@@ -87,6 +104,10 @@ export var tools = new function() {
return Math.floor(Math.random() * (max - min + 1)) + min;
};
self.formatHex = function(value) {
return `0x${value.toString(16).toUpperCase()}`;
};
self.formatSize = function(size) {
if (size > 0) {
let index = Math.floor( Math.log(size) / Math.log(1024) );
@@ -283,34 +304,18 @@ export var tools = new function() {
option.className = "comment";
el.add(option);
},
"addSeparator": function(el) {
"addSeparator": function(el, repeat=30) {
if (!self.browser.is_mobile) {
self.selector.addComment(el, "\u2500".repeat(30));
self.selector.addComment(el, "\u2500".repeat(repeat));
}
},
"setValues": function(el, values, empty_title=null) {
if (values.constructor == Object) {
values = Object.keys(values).sort();
}
let values_json = JSON.stringify(values);
if (el.__values_json !== values_json) {
el.options.length = 0;
for (let value of values) {
let title = value;
if (title.length === 0 && empty_title !== null) {
title = empty_title;
}
self.selector.addOption(el, title, value);
"hasValue": function(el, value) {
for (let el_op of el.options) {
if (el_op.value === value) {
return true;
}
el.__values_json = values_json;
el.__values = values;
}
},
"setSelectedValue": function(el, value) {
if (el.__values && el.__values.includes(value)) {
el.value = value;
}
return false;
},
};
};

View File

@@ -31,7 +31,7 @@ export function main() {
}
function __loadKvmdInfo() {
tools.httpGet("/api/info", function(http) {
tools.httpGet("/api/info", null, function(http) {
if (http.status === 200) {
let vnc_port = JSON.parse(http.responseText).result.extras.vnc.port;
$("vnc-text").innerHTML = `

View File

@@ -111,8 +111,8 @@ function __WindowManager() {
let el_exit_full_tab_button = el_window.querySelector(".window-button-exit-full-tab");
if (el_enter_full_tab_button && el_exit_full_tab_button) {
el_enter_full_tab_button.title = "Stretch to the entire tab";
tools.el.setOnClick(el_enter_full_tab_button, () => self.toggleFullTabWindow(el_window, true));
tools.el.setOnClick(el_exit_full_tab_button, () => self.toggleFullTabWindow(el_window, false));
tools.el.setOnClick(el_enter_full_tab_button, () => self.setFullTabWindow(el_window, true));
tools.el.setOnClick(el_exit_full_tab_button, () => self.setFullTabWindow(el_window, false));
}
let el_full_screen_button = el_window.querySelector(".window-header .window-button-full-screen");
@@ -145,10 +145,10 @@ function __WindowManager() {
/************************************************************************/
self.copyTextToClipboard = function(text) {
let workaround = function(err) {
let workaround = function(ex) {
// https://stackoverflow.com/questions/60317969/document-execcommandcopy-not-working-even-though-the-dom-element-is-created
let callback = function() {
tools.error("copyTextToClipboard(): navigator.clipboard.writeText() is not working:", err);
__modalDialog("Info", "Press OK to copy the text to the clipboard", true, false).then(function() {
tools.error("copyTextToClipboard(): navigator.clipboard.writeText() is not working:", ex);
tools.info("copyTextToClipboard(): Trying a workaround...");
let el = document.createElement("textarea");
@@ -164,36 +164,46 @@ function __WindowManager() {
el.setSelectionRange(0, el.value.length); // iOS
try {
err = (document.execCommand("copy") ? null : "Unknown error");
} catch (err) { // eslint-disable-line no-empty
ex = (document.execCommand("copy") ? null : "Unknown error");
} catch (ex) { // eslint-disable-line no-unused-vars
}
// Remove the added textarea again:
document.body.removeChild(el);
if (err) {
tools.error("copyTextToClipboard(): Workaround failed:", err);
wm.error("Can't copy text to the clipboard:<br>", err);
if (ex) {
tools.error("copyTextToClipboard(): Workaround failed:", ex);
self.error("Can't copy text to the clipboard", `${ex}`);
}
};
__modalDialog("Info", "Press OK to copy the text to the clipboard", true, false, callback);
});
};
if (navigator.clipboard) {
navigator.clipboard.writeText(text).then(function() {
wm.info("The text has been copied to the clipboard");
}, function(err) {
workaround(err);
self.info("The text has been copied to the clipboard");
}, function(ex) {
workaround(ex);
});
} else {
workaround("navigator.clipboard is not available");
}
};
self.info = (...args) => __modalDialog("Info", args.join(" "), true, false);
self.error = (...args) => __modalDialog("Error", args.join(" "), true, false);
self.confirm = (...args) => __modalDialog("Question", args.join(" "), true, true);
self.info = (html, ...args) => __modalCodeDialog("Info", html, args.join("\n"), true, false);
self.error = (html, ...args) => __modalCodeDialog("Error", html, args.join("\n"), true, false);
self.confirm = (html, ...args) => __modalCodeDialog("Question", html, args.join("\n"), true, true);
self.modal = (header, html, ok, cancel) => __modalDialog(header, html, ok, cancel);
var __modalDialog = function(header, text, ok, cancel, callback=null, parent=null) {
var __modalCodeDialog = function(header, html, code, ok, cancel) {
let create_content = function(el_content) {
if (code) {
html += `<br><br><div class="code"><pre style="margin:0px">${tools.escape(code)}</pre></div>`;
}
el_content.innerHTML = html;
};
return __modalDialog(header, create_content, ok, cancel);
};
var __modalDialog = function(header, html, ok, cancel, parent=null) {
let el_active_menu = (document.activeElement && document.activeElement.closest(".menu"));
let el_modal = document.createElement("div");
@@ -207,27 +217,50 @@ function __WindowManager() {
let el_header = document.createElement("div");
el_header.className = "modal-header";
el_header.innerHTML = header;
el_header.innerText = header;
el_window.appendChild(el_header);
let el_content = document.createElement("div");
el_content.className = "modal-content";
el_content.innerHTML = text;
el_window.appendChild(el_content);
let el_buttons = document.createElement("div");
el_buttons.classList.add("modal-buttons", "buttons-row");
el_window.appendChild(el_buttons);
let el_cancel_button = null;
let el_ok_button = null;
if (cancel) {
el_cancel_button = document.createElement("button");
el_cancel_button.className = "row100";
el_cancel_button.innerText = "Cancel";
el_buttons.appendChild(el_cancel_button);
}
if (ok) {
el_ok_button = document.createElement("button");
el_ok_button.className = "row100";
el_ok_button.innerText = "OK";
el_buttons.appendChild(el_ok_button);
}
if (ok && cancel) {
el_ok_button.className = "row50";
el_cancel_button.className = "row50";
}
el_window.onkeyup = function(event) {
event.preventDefault();
if (ok && event.code === "Enter") {
el_ok_button.click();
} else if (cancel && event.code === "Escape") {
el_cancel_button.click();
}
};
let promise = null;
if (ok || cancel) {
promise = new Promise(function(resolve) {
let el_buttons = document.createElement("div");
el_buttons.className = "modal-buttons";
el_window.appendChild(el_buttons);
function close(retval) {
if (callback) {
callback(retval);
}
__closeWindow(el_window);
el_modal.outerHTML = "";
let index = __windows.indexOf(el_modal);
if (index !== -1) {
__windows.splice(index, 1);
@@ -238,38 +271,27 @@ function __WindowManager() {
__activateLastWindow(el_modal);
}
resolve(retval);
// Так как resolve() асинхронный, надо выполнить в эвентлупе после него
setTimeout(function() { el_modal.outerHTML = ""; }, 0);
}
if (cancel) {
var el_cancel_button = document.createElement("button");
el_cancel_button.innerHTML = "Cancel";
tools.el.setOnClick(el_cancel_button, () => close(false));
el_buttons.appendChild(el_cancel_button);
}
if (ok) {
var el_ok_button = document.createElement("button");
el_ok_button.innerHTML = "OK";
tools.el.setOnClick(el_ok_button, () => close(true));
el_buttons.appendChild(el_ok_button);
}
if (ok && cancel) {
el_ok_button.className = "row50";
el_cancel_button.className = "row50";
}
el_window.onkeyup = function(event) {
event.preventDefault();
if (ok && event.code === "Enter") {
el_ok_button.click();
} else if (cancel && event.code === "Escape") {
el_cancel_button.click();
}
};
});
}
__windows.push(el_modal);
(parent || document.fullscreenElement || document.body).appendChild(el_modal);
if (typeof html === "function") {
// Это должно быть здесь, потому что элемент должен иметь родителя чтобы существовать
html(el_content, el_ok_button);
} else {
el_content.innerHTML = html;
}
__activateWindow(el_modal);
return promise;
@@ -312,7 +334,7 @@ function __WindowManager() {
__activateLastWindow(el_window);
};
self.toggleFullTabWindow = function(el_window, enabled) {
self.setFullTabWindow = function(el_window, enabled) {
el_window.classList.toggle("window-full-tab", enabled);
__activateLastWindow(el_window);
let el_navbar = $("navbar");
@@ -625,7 +647,7 @@ function __WindowManager() {
+ "In Chrome use HTTPS and enable <i>system-keyboard-lock</i><br>"
+ "by putting at URL <i>chrome://flags/#system-keyboard-lock</i>"
);
__modalDialog("Keyboard lock is unsupported", msg, true, false, null, el_window);
__modalDialog("Keyboard lock is unsupported", msg, true, false, el_window);
}
};