mirror of
https://github.com/mofeng-git/One-KVM.git
synced 2026-01-29 09:01:54 +08:00
Add support for PiKVM Switch and related features
This commit introduces several new components and improvements: - Added Switch module with firmware update and configuration support - Implemented new media streaming capabilities - Updated various UI elements and CSS styles - Enhanced keyboard and mouse event handling - Added new validators and configuration options - Updated Python version support to 3.13 - Improved error handling and logging
This commit is contained in:
@@ -142,7 +142,7 @@
|
||||
</div>
|
||||
</li>
|
||||
</div>
|
||||
<li class="right" id="system-dropdown"><a class="menu-button" href="#"><img class="led-gray" id="link-led" src="/share/svg/led-link.svg"><img class="led-gray" id="stream-led" src="/share/svg/led-stream.svg"><img class="led-gray" id="hid-keyboard-led" src="/share/svg/led-hid-keyboard.svg"><img class="led-gray" id="hid-mouse-led" src="/share/svg/led-hid-mouse.svg"><span i18n="kvm_text3">System</span></a>
|
||||
<li class="right" id="system-dropdown"><a class="menu-button" href="#"><img class="led-gray" id="link-led" src="/share/svg/led-link.svg"><img class="led-gray" id="stream-led" src="/share/svg/led-video.svg"><img class="led-gray" id="hid-keyboard-led" src="/share/svg/led-hid-keyboard.svg"><img class="led-gray" id="hid-mouse-led" src="/share/svg/led-hid-mouse.svg"><span>System</span></a>
|
||||
<div class="menu" id="system-menu">
|
||||
<table class="kv">
|
||||
<tr>
|
||||
@@ -173,6 +173,17 @@
|
||||
</div>
|
||||
<hr>
|
||||
</div>
|
||||
<div class="hidden" id="stream-message-no-vd">
|
||||
<div class="text">
|
||||
<table>
|
||||
<tr>
|
||||
<td rowspan="2"><img class="sign " src="/share/svg/warning.svg"></td>
|
||||
<td style="line-height:1.5"><b>Direct HTTP H.264 streaming is not supported</b></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<hr>
|
||||
</div>
|
||||
<div class="hidden" id="stream-message-no-h264">
|
||||
<div class="text">
|
||||
<table>
|
||||
@@ -223,10 +234,12 @@
|
||||
<td i18n="kvm_text14">Video <a target="_blank" href="https://docs.pikvm.org/webrtc">mode</a>:</td>
|
||||
<td>
|
||||
<div class="radio-box">
|
||||
<input checked type="radio" id="stream-mode-radio-mjpeg" name="stream-mode-radio" value="mjpeg">
|
||||
<label for="stream-mode-radio-mjpeg">MJPEG / HTTP</label>
|
||||
<input type="radio" id="stream-mode-radio-janus" name="stream-mode-radio" value="janus">
|
||||
<label for="stream-mode-radio-janus">H.264 / WebRTC</label>
|
||||
<label for="stream-mode-radio-janus">WebRTC</label>
|
||||
<input type="radio" id="stream-mode-radio-media" name="stream-mode-radio" value="media">
|
||||
<label for="stream-mode-radio-media">H.264</label>
|
||||
<input checked type="radio" id="stream-mode-radio-mjpeg" name="stream-mode-radio" value="mjpeg">
|
||||
<label for="stream-mode-radio-mjpeg">MJPEG</label>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -252,6 +265,15 @@
|
||||
</td>
|
||||
<td class="value-number" id="stream-audio-volume-value"></td>
|
||||
</tr>
|
||||
<tr class="feature-disabled" id="stream-mic">
|
||||
<td>Microphone:</td>
|
||||
<td align="right">
|
||||
<div class="switch-box">
|
||||
<input disabled type="checkbox" id="stream-mic-switch">
|
||||
<label for="stream-mic-switch"><span class="switch-inner"></span><span class="switch"></span></label>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<hr>
|
||||
<div class="buttons buttons-row">
|
||||
@@ -280,6 +302,7 @@
|
||||
</tr>
|
||||
</table>
|
||||
<details>
|
||||
<summary>Keyboard & mouse (HID) settings</summary>
|
||||
<summary i18n="kvm_text25">Keyboard & Mouse (HID) settings</summary>
|
||||
<div class="spoiler">
|
||||
<table class="kv">
|
||||
@@ -396,6 +419,15 @@
|
||||
</div>
|
||||
</details>
|
||||
<table class="kv">
|
||||
<tr>
|
||||
<td>Bad link mode (release keys immediately):</td>
|
||||
<td align="right">
|
||||
<div class="switch-box">
|
||||
<input type="checkbox" id="hid-keyboard-bad-link-switch">
|
||||
<label for="hid-keyboard-bad-link-switch"><span class="switch-inner"></span><span class="switch"></span></label>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="feature-disabled" id="hid-connect">
|
||||
<td i18n="hid-connect-switch">Connect HID to Server:</td>
|
||||
<td align="right">
|
||||
@@ -416,6 +448,7 @@
|
||||
</tr>
|
||||
<tr>
|
||||
<td i18n="hid-mute-switch">Mute HID input events:</td>
|
||||
<td>Mute all input HID events:</td>
|
||||
<td align="right">
|
||||
<div class="switch-box">
|
||||
<input type="checkbox" id="hid-mute-switch">
|
||||
@@ -502,15 +535,15 @@
|
||||
</div>
|
||||
<hr>
|
||||
</div>
|
||||
<div class="hidden" id="msd-message-too-big-for-cdrom">
|
||||
<div class="hidden" id="msd-message-too-big-for-dvd">
|
||||
<div class="text">
|
||||
<table>
|
||||
<tr>
|
||||
<td rowspan="2"><img class="sign msd-message-too-big-for-cdrom" src="/share/svg/warning.svg"></td>
|
||||
<td rowspan="2"><img class="sign " src="/share/svg/warning.svg"></td>
|
||||
<td style="line-height:1.5"><b>Current image is too big for CD-ROM!</b></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><sup style="line-height:1">The device filesystem will be truncated to 2.2GiB</sup></td>
|
||||
<td><sup style="line-height:1">The maximum is 31.6GiB. Please switch to the Flash mode.</sup></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
@@ -580,7 +613,7 @@
|
||||
<td>
|
||||
<div class="radio-box">
|
||||
<input checked type="radio" id="msd-mode-radio-cdrom" name="msd-mode-radio" value="1">
|
||||
<label for="msd-mode-radio-cdrom">CD-ROM</label>
|
||||
<label for="msd-mode-radio-cdrom">CD/DVD</label>
|
||||
<input type="radio" id="msd-mode-radio-flash" name="msd-mode-radio" value="0">
|
||||
<label for="msd-mode-radio-flash">Flash</label>
|
||||
</div>
|
||||
@@ -754,16 +787,16 @@
|
||||
</table>
|
||||
<table class="kv">
|
||||
<tr>
|
||||
<td i18n="hid-pak-ask-switch">Ask paste confirmation:</td>
|
||||
<td>Slow typing:</td>
|
||||
<td align="right">
|
||||
<div class="switch-box">
|
||||
<input checked type="checkbox" id="hid-pak-ask-switch">
|
||||
<label for="hid-pak-ask-switch"><span class="switch-inner"></span><span class="switch"></span></label>
|
||||
<input type="checkbox" id="hid-pak-slow-switch">
|
||||
<label for="hid-pak-slow-switch"><span class="switch-inner"></span><span class="switch"></span></label>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="feature-disabled" id="hid-pak-secure">
|
||||
<td i18n="hid-pak-secure-switch">Hide input text:</td>
|
||||
<tr>
|
||||
<td>Hide input text:</td>
|
||||
<td align="right">
|
||||
<div class="switch-box">
|
||||
<input type="checkbox" id="hid-pak-secure-switch">
|
||||
@@ -771,6 +804,15 @@
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Ask paste confirmation:</td>
|
||||
<td align="right">
|
||||
<div class="switch-box">
|
||||
<input checked type="checkbox" id="hid-pak-ask-switch">
|
||||
<label for="hid-pak-ask-switch"><span class="switch-inner"></span><span class="switch"></span></label>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<div class="feature-disabled" id="stream-ocr">
|
||||
<hr><br>
|
||||
@@ -809,7 +851,7 @@
|
||||
<hr>
|
||||
<div class="buttons">
|
||||
<div class="buttons-row">
|
||||
<button class="row50" data-force-hide-menu data-shortcut="CapsLock">• Caps Lock <img class="inline-lamp hid-keyboard-caps-led led-gray" src="/share/svg/led-square.svg"></button>
|
||||
<button class="row50" data-force-hide-menu data-shortcut="CapsLock">• Caps Lock <img class="inline-lamp-small hid-keyboard-caps-led led-gray" src="/share/svg/led-square.svg"></button>
|
||||
<button class="row50" data-force-hide-menu data-shortcut="MetaLeft">• Left Win</button>
|
||||
</div>
|
||||
<hr>
|
||||
@@ -884,6 +926,50 @@
|
||||
<li class="right feature-disabled" id="gpio-dropdown"><a class="menu-button" id="gpio-menu-button" href="#"><span>GPIO</span></a>
|
||||
<div class="menu" id="gpio-menu"></div>
|
||||
</li>
|
||||
<li class="right feature-disabled" id="switch-dropdown"><a class="menu-button" id="switch-menu-button" href="#"><img class="led-gray" id="switch-atx-power-led" src="/share/svg/led-atx-power.svg"><img class="led-gray" id="switch-atx-hdd-led" src="/share/svg/led-atx-hdd.svg"><span>Switch <i><sub id="switch-active-port"></sub></i></span></a>
|
||||
<div class="menu" id="switch-menu">
|
||||
<table style="border-spacing: 0px;">
|
||||
<tr>
|
||||
<td>
|
||||
<div class="text"><b><a target="_blank" href="https://docs.pikvm.org/switch">PiKVM Switch</a> is attached<br></b><sub>Select a port or perform any available action like ATX click</sub></div>
|
||||
</td>
|
||||
<td>
|
||||
<div class="text">
|
||||
<button class="small" data-force-hide-menu data-show-window="switch-window">• Settings</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<hr>
|
||||
<div class="hidden" id="switch-message-update">
|
||||
<div class="text">
|
||||
<table>
|
||||
<tr>
|
||||
<td rowspan="2"><img class="sign " src="/share/svg/info.svg"></td>
|
||||
<td style="line-height:1.5"><b>Good news! Your switch is ready to get the firmware update</b></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><sup style="line-height:1">Please <a target="_blank" href="https://docs.pikvm.org/switch/#firmware-updating">follow the instructions</a> when you decide to install it.</sup></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<hr>
|
||||
</div>
|
||||
<table class="kv">
|
||||
<tr>
|
||||
<td>Ask ATX click confirmation:</td>
|
||||
<td align="right">
|
||||
<div class="switch-box">
|
||||
<input checked type="checkbox" id="switch-atx-ask-switch">
|
||||
<label for="switch-atx-ask-switch"><span class="switch-inner"></span><span class="switch"></span></label>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<hr>
|
||||
<table class="kv" id="switch-chain"></table>
|
||||
</div>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="window" id="stream-ocr-window">
|
||||
<div class="hidden" id="stream-ocr-selection"></div>
|
||||
@@ -901,6 +987,7 @@
|
||||
<button class="window-button-exit-full-tab">▼</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" disablePictureInPicture="true" autoplay playsinline muted></video>
|
||||
<canvas class="hidden" id="stream-canvas"></canvas>
|
||||
<div id="stream-fullscreen-active"></div>
|
||||
</div>
|
||||
<div class="keypad" id="stream-mouse-buttons" align="center">
|
||||
@@ -1168,7 +1255,7 @@
|
||||
</div>
|
||||
<div class="keypad-row">
|
||||
<div class="key wide-2 left small" data-code="CapsLock">
|
||||
<div class="label"><img class="inline-lamp hid-keyboard-caps-led led-gray" src="/share/svg/led-square.svg"><br> Caps Lock
|
||||
<div class="label"><img class="inline-lamp-small hid-keyboard-caps-led led-gray" src="/share/svg/led-square.svg"><br> Caps Lock
|
||||
</div>
|
||||
</div>
|
||||
<div class="spacer"></div>
|
||||
@@ -1343,7 +1430,7 @@
|
||||
</div>
|
||||
<div class="spacer-fixed"></div>
|
||||
<div class="key small" data-code="ScrollLock">
|
||||
<div class="label"><img class="inline-lamp hid-keyboard-scroll-led led-gray" src="/share/svg/led-square.svg"><br> ScrLk
|
||||
<div class="label"><img class="inline-lamp-small hid-keyboard-scroll-led led-gray" src="/share/svg/led-square.svg"><br> ScrLk
|
||||
</div>
|
||||
</div>
|
||||
<div class="spacer-fixed"></div>
|
||||
@@ -1439,7 +1526,7 @@
|
||||
<hr>
|
||||
<div class="keypad-row">
|
||||
<div class="key small" data-code="NumLock">
|
||||
<div class="label"><img class="inline-lamp hid-keyboard-num-led led-gray" src="/share/svg/led-square.svg"><br> NmLk
|
||||
<div class="label"><img class="inline-lamp-small hid-keyboard-num-led led-gray" src="/share/svg/led-square.svg"><br> NmLk
|
||||
</div>
|
||||
</div>
|
||||
<div class="spacer-fixed"></div>
|
||||
@@ -1645,7 +1732,7 @@
|
||||
</div>
|
||||
<div class="spacer"></div>
|
||||
<div class="key small" data-code="ScrollLock">
|
||||
<div class="label"><img class="inline-lamp hid-keyboard-scroll-led led-gray" src="/share/svg/led-square.svg"><br> ScrLk
|
||||
<div class="label"><img class="inline-lamp-small hid-keyboard-scroll-led led-gray" src="/share/svg/led-square.svg"><br> ScrLk
|
||||
</div>
|
||||
</div>
|
||||
<div class="spacer"></div>
|
||||
@@ -1818,7 +1905,7 @@
|
||||
</div>
|
||||
<div class="keypad-row">
|
||||
<div class="key wide-2 left small" data-code="CapsLock">
|
||||
<div class="label"><img class="inline-lamp hid-keyboard-caps-led led-gray" src="/share/svg/led-square.svg"><br> Caps Lock
|
||||
<div class="label"><img class="inline-lamp-small hid-keyboard-caps-led led-gray" src="/share/svg/led-square.svg"><br> Caps Lock
|
||||
</div>
|
||||
</div>
|
||||
<div class="spacer"></div>
|
||||
@@ -2017,6 +2104,170 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="window" id="switch-window" style="width:min-content">
|
||||
<div class="window-header">
|
||||
<div class="window-grab">Switch settings</div>
|
||||
<button class="window-button-close"><b>×</b></button>
|
||||
</div>
|
||||
<div class="tabs-box">
|
||||
<input checked type="radio" name="switch-tab-button" id="switch-tab-edid-button">
|
||||
<label for="switch-tab-edid-button">EDIDs collection</label>
|
||||
<div class="tab">
|
||||
<table>
|
||||
<tr>
|
||||
<td colspan="2">
|
||||
<select id="switch-edid-selector" size="8"></select>
|
||||
</td>
|
||||
<td rowspan="2" style="vertical-align:top">
|
||||
<table class="kv">
|
||||
<tr>
|
||||
<td>Manufacturer:</td>
|
||||
<td class="value" id="switch-edid-info-mfc-id"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Product ID:</td>
|
||||
<td class="value" id="switch-edid-info-product-id"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Serial:</td>
|
||||
<td class="value" id="switch-edid-info-serial"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Monitor name:</td>
|
||||
<td class="value" id="switch-edid-info-monitor-name"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Extra serial:</td>
|
||||
<td class="value" id="switch-edid-info-monitor-serial"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Audio enabled:</td>
|
||||
<td class="value" id="switch-edid-info-audio"></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>Data:</td>
|
||||
<td>
|
||||
<button class="small" disabled id="switch-edid-copy-data-button">Copy</button>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<button id="switch-edid-add-button">Add new</button>
|
||||
</td>
|
||||
<td style="float:right">
|
||||
<button disabled id="switch-edid-remove-button">Remove</button>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
<input type="radio" name="switch-tab-button" id="switch-tab-colors-button">
|
||||
<label for="switch-tab-colors-button">Color scheme</label>
|
||||
<div class="tab">
|
||||
<table>
|
||||
<!--tr
|
||||
td Role
|
||||
td Color
|
||||
td Brightness
|
||||
td
|
||||
td Reset
|
||||
-->
|
||||
<!--trtd
|
||||
<hr>
|
||||
td
|
||||
<hr>
|
||||
td
|
||||
<hr>
|
||||
td
|
||||
td
|
||||
<hr>
|
||||
-->
|
||||
<tr>
|
||||
<td style="white-space: nowrap">Selected port:</td>
|
||||
<td>
|
||||
<input type="color" id="switch-color-active-input">
|
||||
</td>
|
||||
<td>
|
||||
<input type="range" id="switch-color-active-brightness-slider" style="min-width:150px">
|
||||
</td>
|
||||
<td> </td>
|
||||
<td>
|
||||
<button class="small" id="switch-color-active-default-button" title="Reset default">↻</button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="white-space: nowrap">Inactive port:</td>
|
||||
<td>
|
||||
<input type="color" id="switch-color-inactive-input">
|
||||
</td>
|
||||
<td>
|
||||
<input type="range" id="switch-color-inactive-brightness-slider" style="min-width:150px">
|
||||
</td>
|
||||
<td> </td>
|
||||
<td>
|
||||
<button class="small" id="switch-color-inactive-default-button" title="Reset default">↻</button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="white-space: nowrap">Blinking beacon:</td>
|
||||
<td>
|
||||
<input type="color" id="switch-color-beacon-input">
|
||||
</td>
|
||||
<td>
|
||||
<input type="range" id="switch-color-beacon-brightness-slider" style="min-width:150px">
|
||||
</td>
|
||||
<td> </td>
|
||||
<td>
|
||||
<button class="small" id="switch-color-beacon-default-button" title="Reset default">↻</button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>
|
||||
<hr>
|
||||
</td>
|
||||
<td>
|
||||
<hr>
|
||||
</td>
|
||||
<td>
|
||||
<hr>
|
||||
</td>
|
||||
<td></td>
|
||||
<td>
|
||||
<hr>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="white-space: nowrap">Flashing downlink:</td>
|
||||
<td>
|
||||
<input type="color" id="switch-color-flashing-input">
|
||||
</td>
|
||||
<td>
|
||||
<input type="range" id="switch-color-flashing-brightness-slider" style="min-width:150px">
|
||||
</td>
|
||||
<td> </td>
|
||||
<td>
|
||||
<button class="small" id="switch-color-flashing-default-button" title="Reset default">↻</button>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td style="white-space: nowrap">Bootloader mode:</td>
|
||||
<td>
|
||||
<input type="color" id="switch-color-bootloader-input">
|
||||
</td>
|
||||
<td>
|
||||
<input type="range" id="switch-color-bootloader-brightness-slider" style="min-width:150px">
|
||||
</td>
|
||||
<td> </td>
|
||||
<td>
|
||||
<button class="small" id="switch-color-bootloader-default-button" title="Reset default">↻</button>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="window" id="about-window">
|
||||
<div class="window-header">
|
||||
<div class="window-grab" i18n="kvm_text1">About</div>
|
||||
@@ -2102,6 +2353,7 @@
|
||||
<li>Alok Anand</li>
|
||||
<li>Alucard</li>
|
||||
<li>Ananthaneshan Elampoornan</li>
|
||||
<li>Andreas Grundler</li>
|
||||
<li>Andreas Marufke</li>
|
||||
<li>Andreas Schmid</li>
|
||||
<li>Andrew Brant</li>
|
||||
@@ -2162,6 +2414,7 @@
|
||||
<li>Brian T Mulcahy</li>
|
||||
<li>Brian Vecchiarelli</li>
|
||||
<li>Brian White</li>
|
||||
<li>brodonalds</li>
|
||||
<li>Bruno Gomes</li>
|
||||
<li>Bryan Adams</li>
|
||||
<li>Bryan Montgomery</li>
|
||||
@@ -2498,6 +2751,7 @@
|
||||
<li>Mikael Wikström</li>
|
||||
<li>Mike Mason</li>
|
||||
<li>Mikhael Mariano</li>
|
||||
<li>Milan Burda</li>
|
||||
<li>Milan Múčka</li>
|
||||
<li>Miles Davis</li>
|
||||
<li>Minh Tang</li>
|
||||
@@ -2516,6 +2770,7 @@
|
||||
<li>Nick Roethemeier</li>
|
||||
<li>Nico Baumgartner</li>
|
||||
<li>Nicolai Kragh-Hansen</li>
|
||||
<li>Nicolas Christener</li>
|
||||
<li>Nigel Smith</li>
|
||||
<li>Nihal Fernando</li>
|
||||
<li>Nils Orbat</li>
|
||||
@@ -2523,6 +2778,7 @@
|
||||
<li>Nithin Philips</li>
|
||||
<li>Nod Swal</li>
|
||||
<li>Nolan Haynes</li>
|
||||
<li>Noxigen LLC</li>
|
||||
<li>nubbn</li>
|
||||
<li>nybble</li>
|
||||
<li>Oh Be</li>
|
||||
@@ -2607,6 +2863,7 @@
|
||||
<li>Scuba</li>
|
||||
<li>Sean</li>
|
||||
<li>Sean Akers</li>
|
||||
<li>Sean c Rickard</li>
|
||||
<li>SEAT</li>
|
||||
<li>Sebastian</li>
|
||||
<li>Seonwoo Lee</li>
|
||||
@@ -2675,6 +2932,7 @@
|
||||
<li>Udo Schroeter</li>
|
||||
<li>Uli Fahrer</li>
|
||||
<li>Vasily Lazarev</li>
|
||||
<li>Venmo</li>
|
||||
<li>Vidru Eduard</li>
|
||||
<li>Vicente Salvador Cubedo</li>
|
||||
<li>Viktor Aschenbrenner</li>
|
||||
|
||||
@@ -15,9 +15,9 @@ li(id="msd-dropdown" class="right feature-disabled")
|
||||
+menu_message("warning", "Current image is broken!", "msd-message-image-broken")
|
||||
| Perhaps uploading was interrupted#[br]
|
||||
hr
|
||||
div(id="msd-message-too-big-for-cdrom" class="hidden")
|
||||
+menu_message("warning", "Current image is too big for CD-ROM!", "msd-message-too-big-for-cdrom")
|
||||
| The device filesystem will be truncated to 2.2GiB
|
||||
div(id="msd-message-too-big-for-dvd" class="hidden")
|
||||
+menu_message("warning", "Current image is too big for DVD!", "msd-message-too-big-for-dvd")
|
||||
| The maximum is 31.6GiB. Please switch to the Flash mode.
|
||||
hr
|
||||
div(id="msd-message-out-of-storage" class="hidden")
|
||||
+menu_message("warning", "Current image is out of storage", "msd-message-out-of-storage")
|
||||
@@ -45,7 +45,7 @@ li(id="msd-dropdown" class="right feature-disabled")
|
||||
td
|
||||
div(class="radio-box")
|
||||
input(checked type="radio" id="msd-mode-radio-cdrom" name="msd-mode-radio" value="1")
|
||||
label(for="msd-mode-radio-cdrom") CD-ROM
|
||||
label(for="msd-mode-radio-cdrom") CD/DVD
|
||||
input(type="radio" id="msd-mode-radio-flash" name="msd-mode-radio" value="0")
|
||||
label(for="msd-mode-radio-flash") Flash
|
||||
td
|
||||
|
||||
@@ -9,7 +9,7 @@ li(id="shortcuts-dropdown" class="right")
|
||||
div(class="buttons-row")
|
||||
button(data-force-hide-menu data-shortcut="CapsLock" class="row50")
|
||||
| • Caps Lock
|
||||
img(class="inline-lamp hid-keyboard-caps-led led-gray" src=`${svg_dir}/led-square.svg`)
|
||||
img(class="inline-lamp-small hid-keyboard-caps-led led-gray" src=`${svg_dir}/led-square.svg`)
|
||||
button(data-force-hide-menu data-shortcut="MetaLeft" class="row50") • Left Win
|
||||
hr
|
||||
div(class="buttons-row")
|
||||
|
||||
23
web/kvm/navbar-switch.pug
Normal file
23
web/kvm/navbar-switch.pug
Normal file
@@ -0,0 +1,23 @@
|
||||
li(id="switch-dropdown" class="right feature-disabled")
|
||||
a(class="menu-button" id="switch-menu-button" href="#")
|
||||
+navbar_led("switch-atx-power-led", "led-atx-power")
|
||||
+navbar_led("switch-atx-hdd-led", "led-atx-hdd")
|
||||
span Switch #[i #[sub(id="switch-active-port") ]]
|
||||
div(id="switch-menu" class="menu")
|
||||
table(style="border-spacing: 0px;")
|
||||
tr
|
||||
td
|
||||
div(class="text")
|
||||
b #[a(target="_blank" href="https://docs.pikvm.org/switch") PiKVM Switch] is attached#[br]
|
||||
sub Select a port or perform any available action like ATX click
|
||||
td
|
||||
div(class="text")
|
||||
button(data-force-hide-menu data-show-window="switch-window" class="small") • Settings
|
||||
hr
|
||||
div(id="switch-message-update" class="hidden")
|
||||
+menu_message("info", "Good news! Your switch is ready to get the firmware update")
|
||||
| Please #[a(target="_blank" href="https://docs.pikvm.org/switch/#firmware-updating") follow the instructions] when you decide to install it.
|
||||
hr
|
||||
+menu_switch("switch-atx-ask-switch", "Ask ATX click confirmation", true, true)
|
||||
hr
|
||||
table(id="switch-chain" class="kv")
|
||||
@@ -1,7 +1,7 @@
|
||||
li(id="system-dropdown" class="right")
|
||||
a(class="menu-button" href="#")
|
||||
+navbar_led("link-led", "led-link")
|
||||
+navbar_led("stream-led", "led-stream")
|
||||
+navbar_led("stream-led", "led-video")
|
||||
+navbar_led("hid-keyboard-led", "led-hid-keyboard")
|
||||
+navbar_led("hid-mouse-led", "led-hid-mouse")
|
||||
span(i18n="kvm_text3") System
|
||||
@@ -19,6 +19,9 @@ li(id="system-dropdown" class="right")
|
||||
div(id="stream-message-no-webrtc" class="hidden")
|
||||
+menu_message("warning", "WebRTC is not supported by this browser", "stream-message-no-webrtc")
|
||||
hr
|
||||
div(id="stream-message-no-vd" class="hidden")
|
||||
+menu_message("warning", "Direct HTTP H.264 streaming is not supported")
|
||||
hr
|
||||
div(id="stream-message-no-h264" class="hidden")
|
||||
+menu_message("warning", "H.264 is not supported by this browser", "stream-message-no-h264")
|
||||
hr
|
||||
@@ -46,10 +49,12 @@ li(id="system-dropdown" class="right")
|
||||
td(i18n="kvm_text14") Video #[a(target="_blank" href="https://docs.pikvm.org/webrtc") mode]:
|
||||
td
|
||||
div(class="radio-box")
|
||||
input(checked type="radio" id="stream-mode-radio-mjpeg" name="stream-mode-radio" value="mjpeg")
|
||||
label(for="stream-mode-radio-mjpeg") MJPEG / HTTP
|
||||
input(type="radio" id="stream-mode-radio-janus" name="stream-mode-radio" value="janus")
|
||||
label(for="stream-mode-radio-janus") H.264 / WebRTC
|
||||
label(for="stream-mode-radio-janus") WebRTC
|
||||
input(type="radio" id="stream-mode-radio-media" name="stream-mode-radio" value="media")
|
||||
label(for="stream-mode-radio-media") H.264
|
||||
input(checked type="radio" id="stream-mode-radio-mjpeg" name="stream-mode-radio" value="mjpeg")
|
||||
label(for="stream-mode-radio-mjpeg") MJPEG
|
||||
tr(id="stream-orient" class="feature-disabled")
|
||||
td(i18n="kvm_text17") Orientation:
|
||||
td
|
||||
@@ -66,6 +71,8 @@ li(id="system-dropdown" class="right")
|
||||
td(i18n="kvm_text19") Audio volume:
|
||||
td(class="value-slider") #[input(type="range" id="stream-audio-volume-slider" class="slider")]
|
||||
td(id="stream-audio-volume-value" class="value-number")
|
||||
tr(id="stream-mic" class="feature-disabled")
|
||||
+menu_switch_notable("stream-mic-switch", "Microphone", false, false)
|
||||
hr
|
||||
div(class="buttons buttons-row")
|
||||
button(data-force-hide-menu data-show-window="stream-window" class="row33" i18n="kvm_text20") • Show stream
|
||||
@@ -128,6 +135,8 @@ li(id="system-dropdown" class="right")
|
||||
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
|
||||
+menu_switch_notable("hid-keyboard-bad-link-switch", "Bad link mode (release keys immediately)", true, false)
|
||||
tr(id="hid-connect" class="feature-disabled")
|
||||
+menu_switch_notable("hid-connect-switch", "Connect HID to Server", true, true, "hid-connect-switch")
|
||||
tr(id="hid-jiggler" class="feature-disabled")
|
||||
|
||||
@@ -17,6 +17,7 @@ li(id="text-dropdown" class="right")
|
||||
td
|
||||
select(id="hid-pak-keymap-selector")
|
||||
table(class="kv")
|
||||
+menu_switch_notable("hid-pak-slow-switch", "Slow typing", true, false, "hid-pak-slow-switch")
|
||||
tr
|
||||
+menu_switch_notable("hid-pak-ask-switch", "Ask paste confirmation", true, true, "hid-pak-ask-switch")
|
||||
tr(id="hid-pak-secure" class="feature-disabled")
|
||||
|
||||
@@ -51,3 +51,4 @@ ul(id="navbar")
|
||||
include navbar-text.pug
|
||||
include navbar-shortcuts.pug
|
||||
include navbar-gpio.pug
|
||||
include navbar-switch.pug
|
||||
|
||||
@@ -79,6 +79,7 @@ div(id="about-window" class="window")
|
||||
li Alok Anand
|
||||
li Alucard
|
||||
li Ananthaneshan Elampoornan
|
||||
li Andreas Grundler
|
||||
li Andreas Marufke
|
||||
li Andreas Schmid
|
||||
li Andrew Brant
|
||||
@@ -139,6 +140,7 @@ div(id="about-window" class="window")
|
||||
li Brian T Mulcahy
|
||||
li Brian Vecchiarelli
|
||||
li Brian White
|
||||
li brodonalds
|
||||
li Bruno Gomes
|
||||
li Bryan Adams
|
||||
li Bryan Montgomery
|
||||
@@ -475,6 +477,7 @@ div(id="about-window" class="window")
|
||||
li Mikael Wikström
|
||||
li Mike Mason
|
||||
li Mikhael Mariano
|
||||
li Milan Burda
|
||||
li Milan Múčka
|
||||
li Miles Davis
|
||||
li Minh Tang
|
||||
@@ -493,6 +496,7 @@ div(id="about-window" class="window")
|
||||
li Nick Roethemeier
|
||||
li Nico Baumgartner
|
||||
li Nicolai Kragh-Hansen
|
||||
li Nicolas Christener
|
||||
li Nigel Smith
|
||||
li Nihal Fernando
|
||||
li Nils Orbat
|
||||
@@ -500,6 +504,7 @@ div(id="about-window" class="window")
|
||||
li Nithin Philips
|
||||
li Nod Swal
|
||||
li Nolan Haynes
|
||||
li Noxigen LLC
|
||||
li nubbn
|
||||
li nybble
|
||||
li Oh Be
|
||||
@@ -584,6 +589,7 @@ div(id="about-window" class="window")
|
||||
li Scuba
|
||||
li Sean
|
||||
li Sean Akers
|
||||
li Sean c Rickard
|
||||
li SEAT
|
||||
li Sebastian
|
||||
li Seonwoo Lee
|
||||
@@ -652,6 +658,7 @@ div(id="about-window" class="window")
|
||||
li Udo Schroeter
|
||||
li Uli Fahrer
|
||||
li Vasily Lazarev
|
||||
li Venmo
|
||||
li Vidru Eduard
|
||||
li Vicente Salvador Cubedo
|
||||
li Viktor Aschenbrenner
|
||||
|
||||
@@ -26,7 +26,7 @@ mixin empty(spacer, classes="", width=0)
|
||||
div(class="spacer-fixed")
|
||||
|
||||
mixin lamp(cls)
|
||||
img(class=`inline-lamp ${cls} led-gray` src=`${svg_dir}/led-square.svg`)
|
||||
img(class=`inline-lamp-small ${cls} led-gray` src=`${svg_dir}/led-square.svg`)
|
||||
|
||||
div(id="keyboard-window" class="window")
|
||||
div(id="keyboard-window-header" class="window-header")
|
||||
|
||||
@@ -16,6 +16,7 @@ div(id="stream-window" class="window window-resizable")
|
||||
div(id="stream-box" class="stream-box-offline")
|
||||
img(id="stream-image" src=`${png_dir}/blank-stream.png`)
|
||||
video(id="stream-video" class="hidden" disablePictureInPicture="true" autoplay playsinline muted)
|
||||
canvas(id="stream-canvas" class="hidden")
|
||||
div(id="stream-fullscreen-active")
|
||||
|
||||
div(id="stream-mouse-buttons" class="keypad" align="center")
|
||||
|
||||
95
web/kvm/window-switch.pug
Normal file
95
web/kvm/window-switch.pug
Normal file
@@ -0,0 +1,95 @@
|
||||
mixin switch_tab(name, title, checked=false)
|
||||
- let button_id = `switch-tab-${name}-button`
|
||||
input(checked=checked type="radio" name="switch-tab-button", id=button_id)
|
||||
label(for=button_id) #{title}
|
||||
div(class="tab")
|
||||
block
|
||||
|
||||
div(id="switch-window" class="window" style="width:min-content")
|
||||
div(class="window-header")
|
||||
div(class="window-grab") Switch settings
|
||||
button(class="window-button-close") #[b ×]
|
||||
|
||||
div(class="tabs-box")
|
||||
+switch_tab("edid", "EDIDs collection", true)
|
||||
table
|
||||
tr
|
||||
td(colspan="2")
|
||||
select(id="switch-edid-selector" size="8")
|
||||
td(rowspan="2" style="vertical-align:top")
|
||||
table(class="kv")
|
||||
tr
|
||||
td Manufacturer:
|
||||
td(id="switch-edid-info-mfc-id" class="value")
|
||||
tr
|
||||
td Product ID:
|
||||
td(id="switch-edid-info-product-id" class="value")
|
||||
tr
|
||||
td Serial:
|
||||
td(id="switch-edid-info-serial" class="value")
|
||||
tr
|
||||
td Monitor name:
|
||||
td(id="switch-edid-info-monitor-name" class="value")
|
||||
tr
|
||||
td Extra serial:
|
||||
td(id="switch-edid-info-monitor-serial" class="value")
|
||||
tr
|
||||
td Audio enabled:
|
||||
td(id="switch-edid-info-audio" class="value")
|
||||
tr
|
||||
td Data:
|
||||
td #[button(disabled id="switch-edid-copy-data-button" class="small") Copy]
|
||||
tr
|
||||
td #[button(id="switch-edid-add-button") Add new]
|
||||
td(style="float:right") #[button(disabled id="switch-edid-remove-button") Remove]
|
||||
|
||||
+switch_tab("colors", "Color scheme")
|
||||
table
|
||||
//tr
|
||||
td Role
|
||||
td Color
|
||||
td Brightness
|
||||
td
|
||||
td Reset
|
||||
//tr
|
||||
td #[hr]
|
||||
td #[hr]
|
||||
td #[hr]
|
||||
td
|
||||
td #[hr]
|
||||
tr
|
||||
td(style="white-space: nowrap") Selected port:
|
||||
td #[input(type="color" id="switch-color-active-input")]
|
||||
td #[input(type="range" id="switch-color-active-brightness-slider" style="min-width:150px")]
|
||||
td
|
||||
td #[button(id="switch-color-active-default-button" class="small" title="Reset default") ↻]
|
||||
tr
|
||||
td(style="white-space: nowrap") Inactive port:
|
||||
td #[input(type="color" id="switch-color-inactive-input")]
|
||||
td #[input(type="range" id="switch-color-inactive-brightness-slider" style="min-width:150px")]
|
||||
td
|
||||
td #[button(id="switch-color-inactive-default-button" class="small" title="Reset default") ↻]
|
||||
tr
|
||||
td(style="white-space: nowrap") Blinking beacon:
|
||||
td #[input(type="color" id="switch-color-beacon-input")]
|
||||
td #[input(type="range" id="switch-color-beacon-brightness-slider" style="min-width:150px")]
|
||||
td
|
||||
td #[button(id="switch-color-beacon-default-button" class="small" title="Reset default") ↻]
|
||||
tr
|
||||
td #[hr]
|
||||
td #[hr]
|
||||
td #[hr]
|
||||
td
|
||||
td #[hr]
|
||||
tr
|
||||
td(style="white-space: nowrap") Flashing downlink:
|
||||
td #[input(type="color" id="switch-color-flashing-input")]
|
||||
td #[input(type="range" id="switch-color-flashing-brightness-slider" style="min-width:150px")]
|
||||
td
|
||||
td #[button(id="switch-color-flashing-default-button" class="small" title="Reset default") ↻]
|
||||
tr
|
||||
td(style="white-space: nowrap") Bootloader mode:
|
||||
td #[input(type="color" id="switch-color-bootloader-input")]
|
||||
td #[input(type="range" id="switch-color-bootloader-brightness-slider" style="min-width:150px")]
|
||||
td
|
||||
td #[button(id="switch-color-bootloader-default-button" class="small" title="Reset default") ↻]
|
||||
@@ -1,4 +1,5 @@
|
||||
include window-stream.pug
|
||||
include window-keyboard.pug
|
||||
include window-switch.pug
|
||||
include window-about.pug
|
||||
include window-webterm.pug
|
||||
|
||||
@@ -30,7 +30,7 @@ block body
|
||||
option(id='en' i18n="english") English
|
||||
tr
|
||||
td
|
||||
td #[button(id="login-button" class="key" i18n="login") Login]
|
||||
td #[button(id="login-button" class="key" style="width:100%" i18n="login") Login]
|
||||
|
||||
ul(class="footer")
|
||||
li(class="left" i18n="footer-left")
|
||||
|
||||
@@ -28,3 +28,7 @@ div#msd-menu div.msd-message,
|
||||
div#msd-menu input.msd-message {
|
||||
display: none;
|
||||
}
|
||||
|
||||
div#msd-menu select#msd-image-selector {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@@ -85,7 +85,8 @@ div.stream-box-mouse-none {
|
||||
}
|
||||
|
||||
img#stream-image,
|
||||
video#stream-video {
|
||||
video#stream-video,
|
||||
canvas#stream-canvas {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: contain;
|
||||
|
||||
@@ -41,6 +41,13 @@
|
||||
--led-spin-slow: spin 6s linear infinite;
|
||||
--led-spin-medium: spin 3s linear infinite;
|
||||
--led-spin-fast: spin 2s linear infinite;
|
||||
|
||||
/* Additional colors for GPIO */
|
||||
--led-filter-blue: invert(0.5) sepia(1) saturate(5) hue-rotate(170deg);
|
||||
--led-filter-cyan: invert(0.5) sepia(1) saturate(5) hue-rotate(130deg);
|
||||
--led-filter-magenta: invert(0.5) sepia(1) saturate(5) hue-rotate(200deg);
|
||||
--led-filter-pink: invert(0.5) sepia(1) saturate(5) hue-rotate(300deg);
|
||||
--led-filter-white: invert(1) sepia(1);
|
||||
}
|
||||
|
||||
img.led-gray {
|
||||
@@ -48,19 +55,16 @@ img.led-gray {
|
||||
-webkit-filter: var(--led-filter-gray);
|
||||
filter: var(--led-filter-gray);
|
||||
}
|
||||
|
||||
img.led-green {
|
||||
-webkit-transform: translateZ(0);
|
||||
-webkit-filter: var(--led-filter-green);
|
||||
filter: var(--led-filter-green);
|
||||
}
|
||||
|
||||
img.led-red {
|
||||
-webkit-transform: translateZ(0);
|
||||
-webkit-filter: var(--led-filter-red);
|
||||
filter: var(--led-filter-red);
|
||||
}
|
||||
|
||||
img.led-yellow {
|
||||
-webkit-transform: translateZ(0);
|
||||
-webkit-filter: var(--led-filter-yellow);
|
||||
@@ -73,10 +77,36 @@ img.led-red-rotating-fast {
|
||||
-webkit-animation: var(--led-spin-fast);
|
||||
animation: var(--led-spin-fast);
|
||||
}
|
||||
|
||||
img.led-yellow-rotating-fast {
|
||||
-webkit-filter: var(--led-filter-yellow);
|
||||
filter: var(--led-filter-yellow);
|
||||
-webkit-animation: var(--led-spin-fast);
|
||||
animation: var(--led-spin-fast);
|
||||
}
|
||||
|
||||
/* Additional colors for GPIO */
|
||||
img.led-blue {
|
||||
-webkit-transform: translateZ(0);
|
||||
-webkit-filter: var(--led-filter-blue);
|
||||
filter: var(--led-filter-blue);
|
||||
}
|
||||
img.led-cyan {
|
||||
-webkit-transform: translateZ(0);
|
||||
-webkit-filter: var(--led-filter-cyan);
|
||||
filter: var(--led-filter-cyan);
|
||||
}
|
||||
img.led-magenta {
|
||||
-webkit-transform: translateZ(0);
|
||||
-webkit-filter: var(--led-filter-magenta);
|
||||
filter: var(--led-filter-magenta);
|
||||
}
|
||||
img.led-pink {
|
||||
-webkit-transform: translateZ(0);
|
||||
-webkit-filter: var(--led-filter-pink);
|
||||
filter: var(--led-filter-pink);
|
||||
}
|
||||
img.led-white {
|
||||
-webkit-transform: translateZ(0);
|
||||
-webkit-filter: var(--led-filter-white);
|
||||
filter: var(--led-filter-white);
|
||||
}
|
||||
|
||||
@@ -88,12 +88,17 @@ img.svg-gray {
|
||||
}
|
||||
|
||||
img.inline-lamp {
|
||||
vertical-align: middle;
|
||||
height: 1em;
|
||||
margin-left: 2px;
|
||||
margin-right: 2px;
|
||||
}
|
||||
img.inline-lamp-small {
|
||||
vertical-align: middle;
|
||||
height: 8px;
|
||||
margin-left: 2px;
|
||||
margin-right: 2px;
|
||||
}
|
||||
|
||||
img.inline-lamp-big {
|
||||
vertical-align: middle;
|
||||
height: 20px;
|
||||
@@ -104,7 +109,8 @@ img.inline-lamp-big {
|
||||
button,
|
||||
select,
|
||||
input[type=file]::-webkit-file-selector-button,
|
||||
input[type=file]::file-selector-button {
|
||||
input[type=file]::file-selector-button,
|
||||
input[type=color] {
|
||||
border: none;
|
||||
border-radius: 4px;
|
||||
color: var(--cs-control-default-fg);
|
||||
@@ -117,11 +123,9 @@ input[type=file]::file-selector-button {
|
||||
}
|
||||
button {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
select {
|
||||
display: block;
|
||||
width: 100%;
|
||||
padding-left: 5px;
|
||||
}
|
||||
select[size] {
|
||||
@@ -194,6 +198,7 @@ select:not([size]) option.comment {
|
||||
input[type=text], input[type=password] {
|
||||
overflow-x: auto;
|
||||
font-family: monospace;
|
||||
box-sizing: border-box;
|
||||
border-radius: 4px;
|
||||
border: var(--border-default-thin);
|
||||
color: var(--cs-code-default-fg);
|
||||
@@ -223,42 +228,35 @@ textarea::-webkit-input-placeholder {
|
||||
}
|
||||
|
||||
div.buttons-row {
|
||||
display: flex;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-size: 0;
|
||||
}
|
||||
|
||||
.row50 {
|
||||
display: inline-block;
|
||||
width: 50%;
|
||||
}
|
||||
.row33 {
|
||||
display: inline-block;
|
||||
width: 33.33%;
|
||||
}
|
||||
.row25 {
|
||||
display: inline-block;
|
||||
width: 25%;
|
||||
}
|
||||
.row16 {
|
||||
display: inline-block;
|
||||
width: 16.66%;
|
||||
}
|
||||
.row50:not(:first-child),
|
||||
.row33:not(:first-child),
|
||||
.row25:not(:first-child),
|
||||
.row16:not(:first-child) {
|
||||
div.buttons-row button:not(:first-child) {
|
||||
border-top-left-radius: 0;
|
||||
border-bottom-left-radius: 0;
|
||||
border-left: var(--border-control-thin) !important;
|
||||
}
|
||||
.row50:not(:last-child),
|
||||
.row33:not(:last-child),
|
||||
.row25:not(:last-child),
|
||||
.row16:not(:last-child) {
|
||||
div.buttons-row button:not(:last-child) {
|
||||
border-top-right-radius: 0;
|
||||
border-bottom-right-radius: 0;
|
||||
}
|
||||
button.row100 {
|
||||
width: 100% !important;
|
||||
}
|
||||
button.row50 {
|
||||
width: 50% !important;
|
||||
}
|
||||
button.row33 {
|
||||
width: 33.33% !important;
|
||||
}
|
||||
button.row25 {
|
||||
width: 25% !important;
|
||||
}
|
||||
button.row16 {
|
||||
width: 16.66% !important;
|
||||
}
|
||||
|
||||
table.kv {
|
||||
border-spacing: 5px;
|
||||
|
||||
@@ -63,9 +63,11 @@ div.modal div.modal-window div.modal-content {
|
||||
|
||||
div.modal div.modal-window div.modal-buttons {
|
||||
border-top: var(--border-control-thin);
|
||||
display: flex;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
font-size: 0;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
div.modal div.modal-window div.modal-buttons button {
|
||||
|
||||
@@ -172,6 +172,7 @@ ul#navbar li div.menu div.buttons select {
|
||||
border-radius: 0;
|
||||
text-align: left;
|
||||
padding: 0 16px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
ul#navbar li div.menu input[type=text] {
|
||||
|
||||
@@ -21,7 +21,7 @@
|
||||
|
||||
|
||||
@supports (-webkit-appearance:none) {
|
||||
input[type=range].slider {
|
||||
input[type=range] {
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
width: 100%;
|
||||
@@ -33,7 +33,7 @@
|
||||
}
|
||||
}
|
||||
@supports not (-webkit-appearance:none) {
|
||||
input[type=range].slider {
|
||||
input[type=range] {
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
width: 100%;
|
||||
@@ -42,20 +42,20 @@
|
||||
margin-right: 0;
|
||||
}
|
||||
}
|
||||
input[type=range].slider:disabled {
|
||||
input[type=range]:disabled {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
input[type=range].slider::-webkit-slider-runnable-track {
|
||||
input[type=range]::-webkit-slider-runnable-track {
|
||||
height: 5px;
|
||||
background: var(--cs-control-default-bg);
|
||||
border-radius: 3px;
|
||||
}
|
||||
input[type=range].slider:disabled::-webkit-slider-runnable-track {
|
||||
input[type=range]:disabled::-webkit-slider-runnable-track {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
input[type=range].slider::-webkit-slider-thumb {
|
||||
input[type=range]::-webkit-slider-thumb {
|
||||
border: var(--border-intensive-2px);
|
||||
height: 18px;
|
||||
width: 18px;
|
||||
@@ -64,29 +64,29 @@ input[type=range].slider::-webkit-slider-thumb {
|
||||
-webkit-appearance: none;
|
||||
margin-top: -7px;
|
||||
}
|
||||
input[type=range].slider:disabled::-webkit-slider-thumb {
|
||||
input[type=range]:disabled::-webkit-slider-thumb {
|
||||
cursor: default;
|
||||
border: var(--border-default-2px);
|
||||
background: var(--cs-thumb-disabled-bg);
|
||||
}
|
||||
|
||||
input[type=range].slider::-moz-range-track {
|
||||
input[type=range]::-moz-range-track {
|
||||
height: 5px;
|
||||
background: var(--cs-control-default-bg);
|
||||
border-radius: 3px;
|
||||
}
|
||||
input[type=range].slider:disabled::-moz-range-track {
|
||||
input[type=range]:disabled::-moz-range-track {
|
||||
cursor: default;
|
||||
}
|
||||
|
||||
input[type=range].slider::-moz-range-thumb {
|
||||
input[type=range]::-moz-range-thumb {
|
||||
border: var(--border-intensive-2px);
|
||||
height: 18px;
|
||||
width: 18px;
|
||||
border-radius: 25px;
|
||||
background: var(--cs-thumb-default-bg);
|
||||
}
|
||||
input[type=range].slider:disabled::-moz-range-thumb {
|
||||
input[type=range]:disabled::-moz-range-thumb {
|
||||
cursor: default;
|
||||
border: var(--border-default-2px);
|
||||
background: var(--cs-thumb-disabled-bg);
|
||||
|
||||
@@ -25,7 +25,8 @@
|
||||
button:enabled:hover,
|
||||
select:not([size]):enabled:hover,
|
||||
input[type=file]:enabled:hover::-webkit-file-selector-button,
|
||||
input[type=file]:enabled:hover::file-selector-button {
|
||||
input[type=file]:enabled:hover::file-selector-button,
|
||||
input[type=color]:enabled:hover {
|
||||
color: var(--cs-control-hovered-fg);
|
||||
background-color: var(--cs-control-hovered-bg);
|
||||
}
|
||||
@@ -33,7 +34,8 @@ input[type=file]:enabled:hover::file-selector-button {
|
||||
button:active,
|
||||
select:not([size]):active,
|
||||
input[type=file]:active::-webkit-file-selector-button,
|
||||
input[type=file]:active::file-selector-button {
|
||||
input[type=file]:active::file-selector-button,
|
||||
input[type=color]:active {
|
||||
color: var(--cs-control-pressed-fg) !important;
|
||||
background-color: var(--cs-control-pressed-bg) !important;
|
||||
}
|
||||
@@ -60,12 +62,12 @@ div.radio-box input[type=radio]:not(:checked):not(:disabled) + label:hover {
|
||||
/* ===== slider.css ===== */
|
||||
|
||||
/*div.switch-box label span.switch-inner:not(:disabled):hover::before {*/
|
||||
input[type=range].slider:not(:disabled):hover::-webkit-slider-runnable-track {
|
||||
input[type=range]:not(:disabled):hover::-webkit-slider-runnable-track {
|
||||
background-color: var(--cs-control-hovered-bg);
|
||||
}
|
||||
|
||||
/*div.switch-box label span.switch-inner:not(:disabled):hover::before {*/
|
||||
input[type=range].slider:not(:disabled):hover::-moz-range-track {
|
||||
input[type=range]:not(:disabled):hover::-moz-range-track {
|
||||
background-color: var(--cs-control-hovered-bg);
|
||||
}
|
||||
|
||||
|
||||
@@ -92,7 +92,7 @@ ul#navbar li a.menu-button:hover:not(.active) {
|
||||
|
||||
/*@media only screen and (orientation: portrait) {
|
||||
@supports (-webkit-appearance: none) {
|
||||
input[type=range].slider {
|
||||
input[type=range] {
|
||||
margin: 20px 0 20px 0 !important;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,6 +32,7 @@ export function Atx(__recorder) {
|
||||
|
||||
/************************************************************************/
|
||||
|
||||
var __has_switch = null; // Or true/false
|
||||
var __state = null;
|
||||
|
||||
var __init__ = function() {
|
||||
@@ -54,12 +55,12 @@ export function Atx(__recorder) {
|
||||
}
|
||||
if (state.enabled !== undefined) {
|
||||
__state.enabled = state.enabled;
|
||||
tools.feature.setEnabled($("atx-dropdown"), __state.enabled);
|
||||
tools.feature.setEnabled($("atx-dropdown"), (__state.enabled && !__has_switch));
|
||||
}
|
||||
if (__state.enabled !== undefined) {
|
||||
if (state.busy !== undefined) {
|
||||
__updateButtons(!state.busy);
|
||||
__state.busy = state.busy;
|
||||
__updateButtons(!__state.busy);
|
||||
}
|
||||
if (state.leds !== undefined) {
|
||||
__state.leds = state.leds;
|
||||
@@ -75,6 +76,11 @@ export function Atx(__recorder) {
|
||||
}
|
||||
};
|
||||
|
||||
self.setHasSwitch = function(has_switch) {
|
||||
__has_switch = has_switch;
|
||||
self.setState(__state);
|
||||
};
|
||||
|
||||
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");
|
||||
@@ -101,7 +107,7 @@ export function Atx(__recorder) {
|
||||
if ($("atx-ask-switch").checked) {
|
||||
wm.confirm(`
|
||||
Are you sure you want to press the <b>${button}</b> button?<br>
|
||||
Warning! This could case data loss on the server.
|
||||
Warning! This could cause data loss on the server.
|
||||
`).then(function(ok) {
|
||||
if (ok) {
|
||||
click_button();
|
||||
|
||||
@@ -52,6 +52,7 @@ export function Keyboard(__recordWsEvent) {
|
||||
window.addEventListener("focusin", __updateOnlineLeds);
|
||||
window.addEventListener("focusout", __updateOnlineLeds);
|
||||
|
||||
tools.storage.bindSimpleSwitch($("hid-keyboard-bad-link-switch"), "hid.keyboard.bad_link", false);
|
||||
tools.storage.bindSimpleSwitch($("hid-keyboard-swap-cc-switch"), "hid.keyboard.swap_cc", false);
|
||||
};
|
||||
|
||||
@@ -140,11 +141,16 @@ export function Keyboard(__recordWsEvent) {
|
||||
}
|
||||
let event = {
|
||||
"event_type": "key",
|
||||
"event": {"key": code, "state": state},
|
||||
"event": {
|
||||
"key": code,
|
||||
"state": state,
|
||||
"finish": $("hid-keyboard-bad-link-switch").checked,
|
||||
},
|
||||
};
|
||||
if (__ws && !$("hid-mute-switch").checked) {
|
||||
__ws.sendHidEvent(event);
|
||||
}
|
||||
delete event.event.finish;
|
||||
__recordWsEvent(event);
|
||||
};
|
||||
|
||||
|
||||
@@ -127,7 +127,7 @@ export function Msd() {
|
||||
|
||||
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-too-big-for-dvd"), (o && d.cdrom && d.image && d.image.size >= 33957083136));
|
||||
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));
|
||||
|
||||
@@ -184,7 +184,7 @@ export function Ocr(__getGeometry) {
|
||||
"ocr_left": __sel.left,
|
||||
"ocr_top": __sel.top,
|
||||
"ocr_right": __sel.right,
|
||||
"orc_bottom": __sel.bottom,
|
||||
"ocr_bottom": __sel.bottom,
|
||||
};
|
||||
tools.httpGet("/api/streamer/snapshot", params, function(http) {
|
||||
if (http.status === 200) {
|
||||
|
||||
@@ -34,15 +34,10 @@ export function Paste(__recorder) {
|
||||
|
||||
var __init__ = function() {
|
||||
tools.storage.bindSimpleSwitch($("hid-pak-ask-switch"), "hid.pak.ask", true);
|
||||
tools.storage.bindSimpleSwitch($("hid-pak-slow-switch"), "hid.pak.slow", false);
|
||||
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);
|
||||
});
|
||||
@@ -73,10 +68,11 @@ export function Paste(__recorder) {
|
||||
tools.el.setEnabled($("hid-pak-keymap-selector"), false);
|
||||
|
||||
let keymap = $("hid-pak-keymap-selector").value;
|
||||
let slow = $("hid-pak-slow-switch").checked;
|
||||
|
||||
tools.debug(`HID: paste-as-keys ${keymap}: ${text}`);
|
||||
|
||||
tools.httpPost("/api/hid/print", {"limit": 0, "keymap": keymap}, function(http) {
|
||||
tools.httpPost("/api/hid/print", {"limit": 0, "keymap": keymap, "slow": slow}, function(http) {
|
||||
tools.el.setEnabled($("hid-pak-text"), true);
|
||||
tools.el.setEnabled($("hid-pak-button"), true);
|
||||
tools.el.setEnabled($("hid-pak-keymap-selector"), true);
|
||||
@@ -86,7 +82,7 @@ export function Paste(__recorder) {
|
||||
} else if (http.status !== 200) {
|
||||
wm.error("HID paste error", http.responseText);
|
||||
} else if (http.status === 200) {
|
||||
__recorder.recordPrintEvent(text, keymap);
|
||||
__recorder.recordPrintEvent(text, keymap, slow);
|
||||
}
|
||||
}, text, "text/plain");
|
||||
};
|
||||
|
||||
@@ -67,8 +67,8 @@ export function Recorder() {
|
||||
__recordEvent(event);
|
||||
};
|
||||
|
||||
self.recordPrintEvent = function(text, keymap) {
|
||||
__recordEvent({"event_type": "print", "event": {"text": text, "keymap": keymap}});
|
||||
self.recordPrintEvent = function(text, keymap, slow) {
|
||||
__recordEvent({"event_type": "print", "event": {"text": text, "keymap": keymap, "slow": slow}});
|
||||
};
|
||||
|
||||
self.recordAtxButtonEvent = function(button) {
|
||||
@@ -159,9 +159,12 @@ export function Recorder() {
|
||||
|
||||
} else if (event.event_type === "print") {
|
||||
__checkType(event.event.text, "string", "Non-string print text");
|
||||
if (event.event.keymap) {
|
||||
if (event.event.keymap !== undefined) {
|
||||
__checkType(event.event.keymap, "string", "Non-string keymap");
|
||||
}
|
||||
if (event.event.slow !== undefined) {
|
||||
__checkType(event.event.slow, "boolean", "Non-bool slow");
|
||||
}
|
||||
|
||||
} else if (event.event_type === "key") {
|
||||
__checkType(event.event.key, "string", "Non-string key code");
|
||||
@@ -284,9 +287,12 @@ export function Recorder() {
|
||||
|
||||
} else if (event.event_type === "print") {
|
||||
let params = {"limit": 0};
|
||||
if (event.event.keymap) {
|
||||
if (event.event.keymap !== undefined) {
|
||||
params["keymap"] = event.event.keymap;
|
||||
}
|
||||
if (event.event.slow !== undefined) {
|
||||
params["slow"] = event.event.slow;
|
||||
}
|
||||
tools.httpPost("/api/hid/print", params, function(http) {
|
||||
if (http.status === 413) {
|
||||
wm.error("Too many text for paste!");
|
||||
@@ -330,7 +336,11 @@ export function Recorder() {
|
||||
});
|
||||
return;
|
||||
|
||||
} else if (["key", "mouse_button", "mouse_move", "mouse_wheel", "mouse_relative"].includes(event.event_type)) {
|
||||
} else if (event.event_type === "key") {
|
||||
event.event.finish = $("hid-keyboard-bad-link-switch").checked;
|
||||
__ws.sendHidEvent(event);
|
||||
|
||||
} else if (["mouse_button", "mouse_move", "mouse_wheel", "mouse_relative"].includes(event.event_type)) {
|
||||
__ws.sendHidEvent(event);
|
||||
|
||||
} else if (event.event_type === "mouse_move_random") {
|
||||
|
||||
@@ -34,6 +34,7 @@ import {Msd} from "./msd.js";
|
||||
import {Streamer} from "./stream.js";
|
||||
import {Gpio} from "./gpio.js";
|
||||
import {Ocr} from "./ocr.js";
|
||||
import {Switch} from "./switch.js";
|
||||
|
||||
|
||||
export function Session() {
|
||||
@@ -54,6 +55,7 @@ export function Session() {
|
||||
var __msd = new Msd();
|
||||
var __gpio = new Gpio(__recorder);
|
||||
var __ocr = new Ocr(__streamer.getGeometry);
|
||||
var __switch = new Switch();
|
||||
|
||||
var __info_hw_state = null;
|
||||
var __info_fan_state = null;
|
||||
@@ -291,7 +293,7 @@ export function Session() {
|
||||
|
||||
tools.httpGet("/api/auth/check", null, function(http) {
|
||||
if (http.status === 200) {
|
||||
__ws = new WebSocket(`${tools.is_https ? "wss" : "ws"}://${location.host}/api/ws?legacy=0`);
|
||||
__ws = new WebSocket(`${tools.is_https ? "wss" : "ws"}://${location.host}/api/ws`);
|
||||
__ws.sendHidEvent = (event) => __sendHidEvent(__ws, event.event_type, event.event);
|
||||
__ws.onopen = __wsOpenHandler;
|
||||
__ws.onmessage = __wsMessageHandler;
|
||||
@@ -314,6 +316,9 @@ export function Session() {
|
||||
if (event_type == "key") {
|
||||
let data = __ascii_encoder.encode("\x01\x00" + event.key);
|
||||
data[1] = (event.state ? 1 : 0);
|
||||
if (event.finish === true) { // Optional
|
||||
data[1] |= 0x02;
|
||||
}
|
||||
ws.send(data);
|
||||
|
||||
} else if (event_type == "mouse_button") {
|
||||
@@ -363,14 +368,29 @@ export function Session() {
|
||||
let data = JSON.parse(event.data);
|
||||
switch (data.event_type) {
|
||||
case "pong": __missed_heartbeats = 0; break;
|
||||
case "info_state": __setInfoState(data.event); break;
|
||||
case "gpio_state": __gpio.setState(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 "ocr_state": __ocr.setState(data.event); break;
|
||||
case "info": __setInfoState(data.event); break;
|
||||
case "gpio": __gpio.setState(data.event); break;
|
||||
case "hid": __hid.setState(data.event); break;
|
||||
case "hid_keymaps": __paste.setState(data.event); break;
|
||||
case "atx": __atx.setState(data.event); break;
|
||||
case "streamer": __streamer.setState(data.event); break;
|
||||
case "ocr": __ocr.setState(data.event); break;
|
||||
|
||||
case "msd":
|
||||
if (data.event.online === false) {
|
||||
__switch.setMsdConnected(false);
|
||||
} else if (data.event.drive !== undefined) {
|
||||
__switch.setMsdConnected(data.event.drive.connected);
|
||||
}
|
||||
__msd.setState(data.event);
|
||||
break;
|
||||
|
||||
case "switch":
|
||||
if (data.event.model) {
|
||||
__atx.setHasSwitch(data.event.model.ports.length > 0);
|
||||
}
|
||||
__switch.setState(data.event);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -401,6 +421,7 @@ export function Session() {
|
||||
__streamer.setState(null);
|
||||
__ocr.setState(null);
|
||||
__recorder.setSocket(null);
|
||||
__switch.setState(null);
|
||||
__ws = null;
|
||||
|
||||
setTimeout(function() {
|
||||
|
||||
@@ -27,6 +27,7 @@ import {tools, $} from "../tools.js";
|
||||
import {wm} from "../wm.js";
|
||||
|
||||
import {JanusStreamer} from "./stream_janus.js";
|
||||
import {MediaStreamer} from "./stream_media.js";
|
||||
import {MjpegStreamer} from "./stream_mjpeg.js";
|
||||
|
||||
|
||||
@@ -93,6 +94,15 @@ export function Streamer() {
|
||||
__resetStream();
|
||||
}
|
||||
}
|
||||
tools.el.setEnabled($("stream-mic-switch"), !!value);
|
||||
});
|
||||
|
||||
tools.storage.bindSimpleSwitch($("stream-mic-switch"), "stream.mic", false, function(allow_mic) {
|
||||
if (__streamer.getMode() === "janus") {
|
||||
if (__streamer.isMicAllowed() !== allow_mic) {
|
||||
__resetStream();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
tools.el.setOnClick($("stream-screenshot-button"), __clickScreenshotButton);
|
||||
@@ -174,17 +184,20 @@ export function Streamer() {
|
||||
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
|
||||
let sup_h264 = $("stream-video").canPlayType("video/mp4; codecs=\"avc1.42E01F\"");
|
||||
let sup_vd = MediaStreamer.is_videodecoder_available();
|
||||
let sup_webrtc = JanusStreamer.is_webrtc_available();
|
||||
let has_media = (f.h264 && sup_vd); // Don't check sup_h264 for sure
|
||||
let has_janus = (__janus_imported && f.h264 && sup_webrtc); // Same
|
||||
|
||||
tools.info(
|
||||
`Stream: Janus WebRTC state: features.h264=${f.h264},`
|
||||
+ ` webrtc=${has_webrtc}, h264=${has_h264}, janus_imported=${__janus_imported}`
|
||||
+ ` webrtc=${sup_webrtc}, h264=${sup_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.hidden.setVisible($("stream-message-no-webrtc"), __janus_imported && f.h264 && !sup_webrtc);
|
||||
tools.hidden.setVisible($("stream-message-no-vd"), f.h264 && !sup_vd);
|
||||
tools.hidden.setVisible($("stream-message-no-h264"), __janus_imported && f.h264 && !sup_h264);
|
||||
|
||||
tools.slider.setRange($("stream-desired-fps-slider"), l.desired_fps.min, l.desired_fps.max);
|
||||
if (f.resolution) {
|
||||
@@ -196,21 +209,28 @@ export function Streamer() {
|
||||
} else {
|
||||
$("stream-resolution-selector").options.length = 0;
|
||||
}
|
||||
if (has_janus) {
|
||||
if (f.h264) {
|
||||
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);
|
||||
}
|
||||
|
||||
// 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-h264-bitrate"), f.h264);
|
||||
tools.feature.setEnabled($("stream-h264-gop"), f.h264);
|
||||
tools.feature.setEnabled($("stream-mode"), f.h264);
|
||||
if (!f.h264) {
|
||||
tools.feature.setEnabled($("stream-audio"), false);
|
||||
tools.feature.setEnabled($("stream-mic"), false);
|
||||
}
|
||||
|
||||
let mode = (has_janus ? tools.storage.get("stream.mode", "janus") : "mjpeg");
|
||||
let mode = tools.storage.get("stream.mode", "janus");
|
||||
if (mode === "janus" && !has_janus) {
|
||||
mode = "media";
|
||||
}
|
||||
if (mode === "media" && !has_media) {
|
||||
mode = "mjpeg";
|
||||
}
|
||||
tools.radio.clickValue("stream-mode-radio", mode);
|
||||
}
|
||||
|
||||
@@ -287,14 +307,19 @@ export function Streamer() {
|
||||
__streamer.stopStream();
|
||||
if (mode === "janus") {
|
||||
__streamer = new JanusStreamer(__setActive, __setInactive, __setInfo,
|
||||
tools.storage.getInt("stream.orient", 0), !$("stream-video").muted);
|
||||
tools.storage.getInt("stream.orient", 0), !$("stream-video").muted, $("stream-mic-switch").checked);
|
||||
// Firefox doesn't support RTP orientation:
|
||||
// - https://bugzilla.mozilla.org/show_bug.cgi?id=1316448
|
||||
tools.feature.setEnabled($("stream-orient"), !tools.browser.is_firefox);
|
||||
} else { // mjpeg
|
||||
__streamer = new MjpegStreamer(__setActive, __setInactive, __setInfo);
|
||||
} else {
|
||||
if (mode === "media") {
|
||||
__streamer = new MediaStreamer(__setActive, __setInactive, __setInfo);
|
||||
} else { // mjpeg
|
||||
__streamer = new MjpegStreamer(__setActive, __setInactive, __setInfo);
|
||||
}
|
||||
tools.feature.setEnabled($("stream-orient"), false);
|
||||
tools.feature.setEnabled($("stream-audio"), false); // Enabling in stream_janus.js
|
||||
tools.feature.setEnabled($("stream-mic"), false); // Ditto
|
||||
}
|
||||
if (wm.isWindowVisible($("stream-window"))) {
|
||||
__streamer.ensureStream((__state && __state.streamer !== undefined) ? __state.streamer : null);
|
||||
@@ -305,7 +330,8 @@ export function Streamer() {
|
||||
let mode = tools.radio.getValue("stream-mode-radio");
|
||||
tools.storage.set("stream.mode", mode);
|
||||
if (mode !== __streamer.getMode()) {
|
||||
tools.hidden.setVisible($("stream-image"), (mode !== "janus"));
|
||||
tools.hidden.setVisible($("stream-canvas"), (mode === "media"));
|
||||
tools.hidden.setVisible($("stream-image"), (mode === "mjpeg"));
|
||||
tools.hidden.setVisible($("stream-video"), (mode === "janus"));
|
||||
__resetStream(mode);
|
||||
}
|
||||
|
||||
@@ -29,9 +29,13 @@ import {tools, $} from "../tools.js";
|
||||
var _Janus = null;
|
||||
|
||||
|
||||
export function JanusStreamer(__setActive, __setInactive, __setInfo, __orient, __allow_audio) {
|
||||
export function JanusStreamer(__setActive, __setInactive, __setInfo, __orient, __allow_audio, __allow_mic) {
|
||||
var self = this;
|
||||
|
||||
/************************************************************************/
|
||||
|
||||
__allow_mic = (__allow_audio && __allow_mic); // XXX: Mic only with audio
|
||||
|
||||
var __stop = false;
|
||||
var __ensuring = false;
|
||||
|
||||
@@ -45,10 +49,22 @@ export function JanusStreamer(__setActive, __setInactive, __setInfo, __orient, _
|
||||
var __state = null;
|
||||
var __frames = 0;
|
||||
|
||||
/************************************************************************/
|
||||
|
||||
self.getOrientation = () => __orient;
|
||||
self.isAudioAllowed = () => __allow_audio;
|
||||
self.isMicAllowed = () => __allow_mic;
|
||||
|
||||
self.getName = () => (__allow_audio ? "H.264 + Audio" : "H.264");
|
||||
self.getName = function() {
|
||||
let name = "WebRTC H.264";
|
||||
if (__allow_audio) {
|
||||
name += " + Audio";
|
||||
if (__allow_mic) {
|
||||
name += " + Mic";
|
||||
}
|
||||
}
|
||||
return name;
|
||||
};
|
||||
self.getMode = () => "janus";
|
||||
|
||||
self.getResolution = function() {
|
||||
@@ -75,9 +91,9 @@ export function JanusStreamer(__setActive, __setInactive, __setInfo, __orient, _
|
||||
|
||||
var __ensureJanus = function(internal) {
|
||||
if (__janus === null && !__stop && (!__ensuring || internal)) {
|
||||
__ensuring = true;
|
||||
__setInactive();
|
||||
__setInfo(false, false, "");
|
||||
__ensuring = true;
|
||||
__logInfo("Starting Janus ...");
|
||||
__janus = new _Janus({
|
||||
"server": `${tools.is_https ? "wss" : "ws"}://${location.host}/janus/ws`,
|
||||
@@ -148,6 +164,16 @@ export function JanusStreamer(__setActive, __setInactive, __setInfo, __orient, _
|
||||
el.srcObject = new MediaStream();
|
||||
}
|
||||
el.srcObject.addTrack(track);
|
||||
// FIXME: Задержка уменьшается, но начинаются заикания на кейфреймах.
|
||||
// XXX: Этот пример переехал из януса 0.x, перед использованием адаптировать к 1.x.
|
||||
// - https://github.com/Glimesh/janus-ftl-plugin/issues/101
|
||||
/*if (__handle && __handle.webrtcStuff && __handle.webrtcStuff.pc) {
|
||||
for (let receiver of __handle.webrtcStuff.pc.getReceivers()) {
|
||||
if (receiver.track && receiver.track.kind === "video" && receiver.playoutDelayHint !== undefined) {
|
||||
receiver.playoutDelayHint = 0;
|
||||
}
|
||||
}
|
||||
}*/
|
||||
};
|
||||
|
||||
var __removeTrack = function(track) {
|
||||
@@ -215,6 +241,7 @@ export function JanusStreamer(__setActive, __setInactive, __setInfo, __orient, _
|
||||
__setInfo(false, false, "");
|
||||
} else if (msg.result.status === "features") {
|
||||
tools.feature.setEnabled($("stream-audio"), msg.result.features.audio);
|
||||
tools.feature.setEnabled($("stream-mic"), msg.result.features.mic);
|
||||
}
|
||||
} else if (msg.error_code || msg.error) {
|
||||
__logError("Got uStreamer error message:", msg.error_code, "-", msg.error);
|
||||
@@ -237,17 +264,13 @@ export function JanusStreamer(__setActive, __setInactive, __setInfo, __orient, _
|
||||
__logInfo("Handling SDP:", jsep);
|
||||
let tracks = [{"type": "video", "capture": false, "recv": true, "add": true}];
|
||||
if (__allow_audio) {
|
||||
tracks.push({"type": "audio", "capture": false, "recv": true, "add": true});
|
||||
tracks.push({"type": "audio", "capture": __allow_mic, "recv": true, "add": true});
|
||||
}
|
||||
__handle.createAnswer({
|
||||
"jsep": jsep,
|
||||
|
||||
// Janus 1.x
|
||||
"tracks": tracks,
|
||||
|
||||
// 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
|
||||
@@ -288,50 +311,6 @@ export function JanusStreamer(__setActive, __setInactive, __setInfo, __orient, _
|
||||
}
|
||||
},
|
||||
|
||||
// Janus 0.x
|
||||
"onremotestream": function(stream) {
|
||||
if (stream === null) {
|
||||
// https://github.com/pikvm/pikvm/issues/1084
|
||||
// Этого вообще не должно происходить, но почему-то янусу в unmute
|
||||
// может прилететь null-эвент. Костыляем, наблюдаем.
|
||||
__logError("Got invalid onremotestream(null). Restarting Janus...");
|
||||
__destroyJanus();
|
||||
return;
|
||||
}
|
||||
|
||||
let tracks = stream.getTracks();
|
||||
__logInfo("Got a remote stream changes:", stream, tracks);
|
||||
|
||||
let has_video = false;
|
||||
for (let track of tracks) {
|
||||
if (track.kind == "video") {
|
||||
has_video = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!has_video && __isOnline()) {
|
||||
// Chrome sends `muted` notifiation for tracks in `disconnected` ICE state
|
||||
// and Janus.js just removes muted track from list of available tracks.
|
||||
// But track still exists actually so it's safe to just ignore that case.
|
||||
return;
|
||||
}
|
||||
|
||||
_Janus.attachMediaStream($("stream-video"), stream);
|
||||
__sendKeyRequired();
|
||||
__startInfoInterval();
|
||||
|
||||
// FIXME: Задержка уменьшается, но начинаются заикания на кейфреймах.
|
||||
// - https://github.com/Glimesh/janus-ftl-plugin/issues/101
|
||||
/*if (__handle && __handle.webrtcStuff && __handle.webrtcStuff.pc) {
|
||||
for (let receiver of __handle.webrtcStuff.pc.getReceivers()) {
|
||||
if (receiver.track && receiver.track.kind === "video" && receiver.playoutDelayHint !== undefined) {
|
||||
receiver.playoutDelayHint = 0;
|
||||
}
|
||||
}
|
||||
}*/
|
||||
},
|
||||
|
||||
"oncleanup": function() {
|
||||
__logInfo("Got a cleanup notification");
|
||||
__stopInfoInterval();
|
||||
@@ -388,11 +367,12 @@ export function JanusStreamer(__setActive, __setInactive, __setInfo, __orient, _
|
||||
|
||||
var __sendWatch = function() {
|
||||
if (__handle) {
|
||||
__logInfo(`Sending WATCH(orient=${__orient}, audio=${__allow_audio}) + FEATURES ...`);
|
||||
__logInfo(`Sending WATCH(orient=${__orient}, audio=${__allow_audio}, mic=${__allow_mic}) + FEATURES ...`);
|
||||
__handle.send({"message": {"request": "features"}});
|
||||
__handle.send({"message": {"request": "watch", "params": {
|
||||
"orientation": __orient,
|
||||
"audio": __allow_audio,
|
||||
"mic": __allow_mic,
|
||||
}}});
|
||||
}
|
||||
};
|
||||
@@ -447,11 +427,3 @@ JanusStreamer.ensure_janus = function(callback) {
|
||||
JanusStreamer.is_webrtc_available = function() {
|
||||
return !!window.RTCPeerConnection;
|
||||
};
|
||||
|
||||
JanusStreamer.is_h264_available = function() {
|
||||
let ok = true;
|
||||
if ($("stream-video").canPlayType) {
|
||||
ok = $("stream-video").canPlayType("video/mp4; codecs=\"avc1.42E01F\"");
|
||||
}
|
||||
return ok;
|
||||
};
|
||||
|
||||
241
web/share/js/kvm/stream_media.js
Normal file
241
web/share/js/kvm/stream_media.js
Normal file
@@ -0,0 +1,241 @@
|
||||
/*****************************************************************************
|
||||
# #
|
||||
# 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";
|
||||
|
||||
|
||||
export function MediaStreamer(__setActive, __setInactive, __setInfo) {
|
||||
var self = this;
|
||||
|
||||
/************************************************************************/
|
||||
|
||||
var __stop = false;
|
||||
var __ensuring = false;
|
||||
|
||||
var __ws = null;
|
||||
var __ping_timer = null;
|
||||
var __missed_heartbeats = 0;
|
||||
var __decoder = null;
|
||||
var __codec = "";
|
||||
var __canvas = $("stream-canvas");
|
||||
var __ctx = __canvas.getContext("2d");
|
||||
|
||||
var __state = null;
|
||||
var __frames = 0;
|
||||
|
||||
/************************************************************************/
|
||||
|
||||
self.getName = () => "HTTP H.264";
|
||||
self.getMode = () => "media";
|
||||
|
||||
self.getResolution = function() {
|
||||
return {
|
||||
// Разрешение видео или элемента
|
||||
"real_width": (__canvas.width || __canvas.offsetWidth),
|
||||
"real_height": (__canvas.height || __canvas.offsetHeight),
|
||||
"view_width": __canvas.offsetWidth,
|
||||
"view_height": __canvas.offsetHeight,
|
||||
};
|
||||
};
|
||||
|
||||
self.ensureStream = function(state) {
|
||||
__state = state;
|
||||
__stop = false;
|
||||
__ensureMedia(false);
|
||||
};
|
||||
|
||||
self.stopStream = function() {
|
||||
__stop = true;
|
||||
__ensuring = false;
|
||||
__wsForceClose();
|
||||
__setInfo(false, false, "");
|
||||
};
|
||||
|
||||
var __ensureMedia = function(internal) {
|
||||
if (__ws === null && !__stop && (!__ensuring || internal)) {
|
||||
__ensuring = true;
|
||||
__setInactive();
|
||||
__setInfo(false, false, "");
|
||||
__logInfo("Starting Media ...");
|
||||
__ws = new WebSocket(`${tools.is_https ? "wss" : "ws"}://${location.host}/api/media/ws`);
|
||||
__ws.binaryType = "arraybuffer";
|
||||
__ws.onopen = __wsOpenHandler;
|
||||
__ws.onerror = __wsErrorHandler;
|
||||
__ws.onclose = __wsCloseHandler;
|
||||
__ws.onmessage = async (event) => {
|
||||
if (typeof event.data === "string") {
|
||||
__wsJsonHandler(JSON.parse(event.data));
|
||||
} else { // Binary
|
||||
await __wsBinHandler(event.data);
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
var __wsOpenHandler = function(event) {
|
||||
__logInfo("Socket opened:", event);
|
||||
__missed_heartbeats = 0;
|
||||
__ping_timer = setInterval(__ping, 1000);
|
||||
};
|
||||
|
||||
var __ping = function() {
|
||||
try {
|
||||
__missed_heartbeats += 1;
|
||||
if (__missed_heartbeats >= 5) {
|
||||
throw new Error("Too many missed heartbeats");
|
||||
}
|
||||
__ws.send(new Uint8Array([0]));
|
||||
|
||||
if (__decoder && __decoder.state === "configured") {
|
||||
let online = !!(__state && __state.source.online);
|
||||
let info = `${__frames} fps dynamic`;
|
||||
__frames = 0;
|
||||
__setInfo(true, online, info);
|
||||
}
|
||||
} catch (ex) {
|
||||
__wsErrorHandler(ex.message);
|
||||
}
|
||||
};
|
||||
|
||||
var __wsForceClose = function() {
|
||||
if (__ws) {
|
||||
__ws.onclose = null;
|
||||
__ws.close();
|
||||
}
|
||||
__wsCloseHandler(null);
|
||||
__setInactive();
|
||||
};
|
||||
|
||||
var __wsErrorHandler = function(event) {
|
||||
__logInfo("Socket error:", event);
|
||||
__setInfo(false, false, event);
|
||||
__wsForceClose();
|
||||
};
|
||||
|
||||
var __wsCloseHandler = function(event) {
|
||||
__logInfo("Socket closed:", event);
|
||||
if (__ping_timer) {
|
||||
clearInterval(__ping_timer);
|
||||
__ping_timer = null;
|
||||
}
|
||||
if (__decoder) {
|
||||
__decoder.close();
|
||||
__decoder = null;
|
||||
}
|
||||
__missed_heartbeats = 0;
|
||||
__frames = 0;
|
||||
__ws = null;
|
||||
if (!__stop) {
|
||||
setTimeout(() => __ensureMedia(true), 1000);
|
||||
}
|
||||
};
|
||||
|
||||
var __wsJsonHandler = function(event) {
|
||||
if (event.event_type === "media") {
|
||||
__decoderCreate(event.event.video);
|
||||
}
|
||||
};
|
||||
|
||||
var __wsBinHandler = async (data) => {
|
||||
let header = new Uint8Array(data.slice(0, 2));
|
||||
|
||||
if (header[0] === 255) { // Pong
|
||||
__missed_heartbeats = 0;
|
||||
|
||||
} else if (header[0] === 1 && __decoder !== null) { // Video frame
|
||||
let key = !!header[1];
|
||||
if (__decoder.state !== "configured") {
|
||||
if (!key) {
|
||||
return;
|
||||
}
|
||||
await __decoder.configure({"codec": __codec, "optimizeForLatency": true});
|
||||
__setActive();
|
||||
}
|
||||
|
||||
let chunk = new EncodedVideoChunk({ // eslint-disable-line no-undef
|
||||
"timestamp": (performance.now() + performance.timeOrigin) * 1000,
|
||||
"type": (key ? "key" : "delta"),
|
||||
"data": data.slice(2),
|
||||
});
|
||||
await __decoder.decode(chunk);
|
||||
}
|
||||
};
|
||||
|
||||
var __decoderCreate = function(formats) {
|
||||
__decoderDestroy();
|
||||
|
||||
if (formats.h264 === undefined) {
|
||||
let msg = "No H.264 stream available on PiKVM";
|
||||
__setInfo(false, false, msg);
|
||||
__logInfo(msg);
|
||||
return;
|
||||
}
|
||||
if (!window.VideoDecoder) {
|
||||
let msg = "This browser can't handle direct H.264 stream";
|
||||
if (!tools.is_https) {
|
||||
msg = "Direct H.264 requires HTTPS";
|
||||
}
|
||||
__setInfo(false, false, msg);
|
||||
__logInfo(msg);
|
||||
return;
|
||||
}
|
||||
|
||||
__decoder = new VideoDecoder({ // eslint-disable-line no-undef
|
||||
"output": (frame) => {
|
||||
try {
|
||||
if (__canvas.width !== frame.displayWidth || __canvas.height !== frame.displayHeight) {
|
||||
__canvas.width = frame.displayWidth;
|
||||
__canvas.height = frame.displayHeight;
|
||||
}
|
||||
__ctx.drawImage(frame, 0, 0);
|
||||
__frames += 1;
|
||||
} finally {
|
||||
frame.close();
|
||||
}
|
||||
},
|
||||
"error": (err) => __logInfo(err.message),
|
||||
});
|
||||
__codec = `avc1.${formats.h264.profile_level_id}`;
|
||||
|
||||
__ws.send(JSON.stringify({
|
||||
"event_type": "start",
|
||||
"event": {"type": "video", "format": "h264"},
|
||||
}));
|
||||
};
|
||||
|
||||
var __decoderDestroy = function() {
|
||||
if (__decoder !== null) {
|
||||
__decoder.close();
|
||||
__decoder = null;
|
||||
__codec = "";
|
||||
}
|
||||
};
|
||||
|
||||
var __logInfo = (...args) => tools.info("Stream [Media]:", ...args);
|
||||
}
|
||||
|
||||
MediaStreamer.is_videodecoder_available = function() {
|
||||
return !!window.VideoDecoder;
|
||||
};
|
||||
@@ -41,7 +41,7 @@ export function MjpegStreamer(__setActive, __setInactive, __setInfo) {
|
||||
|
||||
/************************************************************************/
|
||||
|
||||
self.getName = () => "MJPEG";
|
||||
self.getName = () => "HTTP MJPEG";
|
||||
self.getMode = () => "mjpeg";
|
||||
|
||||
self.getResolution = function() {
|
||||
|
||||
610
web/share/js/kvm/switch.js
Normal file
610
web/share/js/kvm/switch.js
Normal file
@@ -0,0 +1,610 @@
|
||||
/*****************************************************************************
|
||||
# #
|
||||
# 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 Switch() {
|
||||
var self = this;
|
||||
|
||||
/************************************************************************/
|
||||
|
||||
var __state = null;
|
||||
var __msd_connected = false;
|
||||
|
||||
var __init__ = function() {
|
||||
tools.selector.addOption($("switch-edid-selector"), "Default", "default");
|
||||
$("switch-edid-selector").onchange = __selectEdid;
|
||||
|
||||
tools.el.setOnClick($("switch-edid-add-button"), __clickAddEdidButton);
|
||||
tools.el.setOnClick($("switch-edid-remove-button"), __clickRemoveEdidButton);
|
||||
tools.el.setOnClick($("switch-edid-copy-data-button"), __clickCopyEdidDataButton);
|
||||
|
||||
tools.storage.bindSimpleSwitch($("switch-atx-ask-switch"), "switch.atx.ask", true);
|
||||
|
||||
for (let role of ["inactive", "active", "flashing", "beacon", "bootloader"]) {
|
||||
let el_brightness = $(`switch-color-${role}-brightness-slider`);
|
||||
tools.slider.setParams(el_brightness, 0, 255, 1, 0);
|
||||
el_brightness.onchange = $(`switch-color-${role}-input`).onchange = tools.partial(__selectColor, role);
|
||||
tools.el.setOnClick($(`switch-color-${role}-default-button`), tools.partial(__clickSetDefaultColorButton, role));
|
||||
}
|
||||
};
|
||||
|
||||
/************************************************************************/
|
||||
|
||||
self.setMsdConnected = function(connected) {
|
||||
__msd_connected = connected;
|
||||
};
|
||||
|
||||
self.setState = function(state) {
|
||||
if (state) {
|
||||
if (!__state) {
|
||||
__state = {};
|
||||
}
|
||||
if (state.model) {
|
||||
__state = {};
|
||||
__applyModel(state.model);
|
||||
}
|
||||
if (__state.model) {
|
||||
if (state.summary) {
|
||||
__applySummary(state.summary);
|
||||
}
|
||||
if (state.beacons) {
|
||||
__applyBeacons(state.beacons);
|
||||
}
|
||||
if (state.usb) {
|
||||
__applyUsb(state.usb);
|
||||
}
|
||||
if (state.video) {
|
||||
__applyVideo(state.video);
|
||||
}
|
||||
if (state.atx) {
|
||||
__applyAtx(state.atx);
|
||||
}
|
||||
if (state.edids) {
|
||||
__applyEdids(state.edids);
|
||||
}
|
||||
if (state.colors) {
|
||||
__applyColors(state.colors);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
tools.feature.setEnabled($("switch-dropdown"), false);
|
||||
$("switch-chain").innerText = "";
|
||||
$("switch-active-port").innerText = "N/A";
|
||||
__setPowerLedState($("switch-atx-power-led"), false, false);
|
||||
__setLedState($("switch-atx-hdd-led"), "red", false);
|
||||
__state = null;
|
||||
}
|
||||
};
|
||||
|
||||
var __applyColors = function(colors) {
|
||||
for (let role in colors) {
|
||||
let color = colors[role];
|
||||
$(`switch-color-${role}-input`).value = (
|
||||
"#"
|
||||
+ color.red.toString(16).padStart(2, "0")
|
||||
+ color.green.toString(16).padStart(2, "0")
|
||||
+ color.blue.toString(16).padStart(2, "0")
|
||||
);
|
||||
$(`switch-color-${role}-brightness-slider`).value = color.brightness;
|
||||
}
|
||||
__state.colors = colors;
|
||||
};
|
||||
|
||||
var __selectColor = function(role) {
|
||||
let el_color = $(`switch-color-${role}-input`);
|
||||
let el_brightness = $(`switch-color-${role}-brightness-slider`);
|
||||
let color = __state.colors[role];
|
||||
let brightness = parseInt(el_brightness.value);
|
||||
let rgbx = (
|
||||
el_color.value.slice(1)
|
||||
+ ":" + brightness.toString(16).padStart(2, "0")
|
||||
+ ":" + color.blink_ms.toString(16).padStart(4, "0")
|
||||
);
|
||||
__sendPost("/api/switch/set_colors", {[role]: rgbx}, function() {
|
||||
el_color.value = (
|
||||
"#"
|
||||
+ color.red.toString(16).padStart(2, "0")
|
||||
+ color.green.toString(16).padStart(2, "0")
|
||||
+ color.blue.toString(16).padStart(2, "0")
|
||||
);
|
||||
el_brightness.value = color.brightness;
|
||||
});
|
||||
};
|
||||
|
||||
var __clickSetDefaultColorButton = function(role) {
|
||||
__sendPost("/api/switch/set_colors", {[role]: "default"});
|
||||
};
|
||||
|
||||
var __applyEdids = function(edids) {
|
||||
let el = $("switch-edid-selector");
|
||||
let old_edid_id = el.value;
|
||||
el.options.length = 1;
|
||||
for (let kv of Object.entries(edids.all)) {
|
||||
if (kv[0] !== "default") {
|
||||
tools.selector.addOption(el, kv[1].name, kv[0]);
|
||||
}
|
||||
}
|
||||
el.value = (old_edid_id in edids.all ? old_edid_id : "default");
|
||||
|
||||
for (let port in __state.model.ports) {
|
||||
let custom = (edids.used[port] !== "default");
|
||||
$(`__switch-custom-edid-p${port}`).style.visibility = (custom ? "unset" : "hidden");
|
||||
}
|
||||
|
||||
__state.edids = edids;
|
||||
__selectEdid();
|
||||
};
|
||||
|
||||
var __selectEdid = function() {
|
||||
let edid_id = $("switch-edid-selector").value;
|
||||
let edid = null;
|
||||
try { edid = __state.edids.all[edid_id]; } catch { edid_id = ""; }
|
||||
let parsed = (edid ? edid.parsed : null);
|
||||
let na = "<i><Not Available></i>";
|
||||
$("switch-edid-info-mfc-id").innerHTML = (parsed ? tools.escape(parsed.mfc_id) : na);
|
||||
$("switch-edid-info-product-id").innerHTML = (parsed ? tools.escape(`0x${parsed.product_id.toString(16).toUpperCase()}`) : na);
|
||||
$("switch-edid-info-serial").innerHTML = (parsed ? tools.escape(`0x${parsed.serial.toString(16).toUpperCase()}`) : na);
|
||||
$("switch-edid-info-monitor-name").innerHTML = ((parsed && parsed.monitor_name) ? tools.escape(parsed.monitor_name) : na);
|
||||
$("switch-edid-info-monitor-serial").innerHTML = ((parsed && parsed.monitor_serial) ? tools.escape(parsed.monitor_serial) : na);
|
||||
$("switch-edid-info-audio").innerHTML = (parsed ? (parsed.audio ? "Yes" : "No") : na);
|
||||
tools.el.setEnabled($("switch-edid-remove-button"), (edid_id && (edid_id !== "default")));
|
||||
tools.el.setEnabled($("switch-edid-copy-data-button"), !!edid_id);
|
||||
};
|
||||
|
||||
var __clickAddEdidButton = function() {
|
||||
let create_content = function(el_parent, el_ok_button) {
|
||||
tools.el.setEnabled(el_ok_button, false);
|
||||
el_parent.innerHTML = `
|
||||
<table>
|
||||
<tr>
|
||||
<td>Name:</td>
|
||||
<td><input
|
||||
type="text" autocomplete="off" id="__switch-edid-new-name-input"
|
||||
placeholder="Enter some meaningful name"
|
||||
style="width:100%"
|
||||
/></td>
|
||||
</tr>
|
||||
<tr><td colspan="2">HEX data:</td></tr>
|
||||
<tr>
|
||||
<td colspan="2"><textarea
|
||||
id="__switch-edid-new-data-text" placeholder="Like 0123ABCD..."
|
||||
style="min-width:350px"
|
||||
></textarea><td>
|
||||
</table>
|
||||
`;
|
||||
let el_name = $("__switch-edid-new-name-input");
|
||||
let el_data = $("__switch-edid-new-data-text");
|
||||
el_name.oninput = el_data.oninput = function() {
|
||||
let name = el_name.value.replace(/\s+/g, "");
|
||||
let data = el_data.value.replace(/\s+/g, "");
|
||||
tools.el.setEnabled(el_ok_button, ((name.length > 0) && /[0-9a-fA-F]{512}/.test(data)));
|
||||
};
|
||||
};
|
||||
|
||||
wm.modal("Add new EDID", create_content, true, true).then(function(ok) {
|
||||
if (ok) {
|
||||
let name = $("__switch-edid-new-name-input").value;
|
||||
let data = $("__switch-edid-new-data-text").value;
|
||||
__sendPost("/api/switch/edids/create", {"name": name, "data": data});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
var __clickRemoveEdidButton = function() {
|
||||
let edid_id = $("switch-edid-selector").value;
|
||||
if (edid_id && __state && __state.edids) {
|
||||
let name = __state.edids.all[edid_id].name;
|
||||
let html = "Are you sure to remove this EDID?<br>Ports that used it will change it to the default.";
|
||||
wm.confirm(html, name).then(function(ok) {
|
||||
if (ok) {
|
||||
__sendPost("/api/switch/edids/remove", {"id": edid_id});
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
var __clickCopyEdidDataButton = function() {
|
||||
let edid_id = $("switch-edid-selector").value;
|
||||
if (edid_id && __state && __state.edids) {
|
||||
let data = __state.edids.all[edid_id].data;
|
||||
data = data.replace(/(.{32})/g, "$1\n");
|
||||
wm.copyTextToClipboard(data);
|
||||
}
|
||||
};
|
||||
|
||||
var __applyUsb = function(usb) {
|
||||
for (let port = 0; port < __state.model.ports.length; ++port) {
|
||||
if (!__state.usb || __state.usb.links[port] !== usb.links[port]) {
|
||||
__setLedState($(`__switch-usb-led-p${port}`), "green", usb.links[port]);
|
||||
}
|
||||
}
|
||||
__state.usb = usb;
|
||||
};
|
||||
|
||||
var __applyVideo = function(video) {
|
||||
for (let port = 0; port < __state.model.ports.length; ++port) {
|
||||
if (!__state.video || __state.video.links[port] !== video.links[port]) {
|
||||
__setLedState($(`__switch-video-led-p${port}`), "green", video.links[port]);
|
||||
}
|
||||
}
|
||||
__state.video = video;
|
||||
};
|
||||
|
||||
var __applyAtx = function(atx) {
|
||||
for (let port = 0; port < __state.model.ports.length; ++port) {
|
||||
let busy = atx.busy[port];
|
||||
if (!__state.atx || __state.atx.leds.power[port] !== atx.leds.power[port] || __state.atx.busy[port] !== busy) {
|
||||
let power = atx.leds.power[port];
|
||||
__setPowerLedState($(`__switch-atx-power-led-p${port}`), power, busy);
|
||||
if (port === __state.summary.active_port) {
|
||||
// summary есть всегда, если есть model, и atx обновляется последним в setState()
|
||||
__setPowerLedState($("switch-atx-power-led"), power, busy);
|
||||
}
|
||||
}
|
||||
if (!__state.atx || __state.atx.leds.hdd[port] !== atx.leds.hdd[port]) {
|
||||
let hdd = atx.leds.hdd[port];
|
||||
__setLedState($(`__switch-atx-hdd-led-p${port}`), "red", hdd);
|
||||
if (port === __state.summary.active_port) {
|
||||
__setLedState($("switch-atx-hdd-led"), "red", hdd);
|
||||
}
|
||||
}
|
||||
if (!__state.atx || __state.atx.busy[port] !== busy) {
|
||||
tools.el.setEnabled($(`__switch-atx-power-button-p${port}`), !busy);
|
||||
tools.el.setEnabled($(`__switch-atx-power-long-button-p${port}`), !busy);
|
||||
tools.el.setEnabled($(`__switch-atx-reset-button-p${port}`), !busy);
|
||||
}
|
||||
}
|
||||
__state.atx = atx;
|
||||
};
|
||||
|
||||
var __applyBeacons = function(beacons) {
|
||||
for (let unit = 0; unit < __state.model.units.length; ++unit) {
|
||||
if (!__state.beacons || __state.beacons.uplinks[unit] !== beacons.uplinks[unit]) {
|
||||
__setLedState($(`__switch-beacon-led-u${unit}`), "green", beacons.uplinks[unit]);
|
||||
}
|
||||
if (!__state.beacons || __state.beacons.downlinks[unit] !== beacons.downlinks[unit]) {
|
||||
__setLedState($(`__switch-beacon-led-d${unit}`), "green", beacons.downlinks[unit]);
|
||||
}
|
||||
}
|
||||
for (let port = 0; port < __state.model.ports.length; ++port) {
|
||||
if (!__state.beacons || __state.beacons.ports[port] !== beacons.ports[port]) {
|
||||
__setLedState($(`__switch-beacon-led-p${port}`), "green", beacons.ports[port]);
|
||||
}
|
||||
}
|
||||
__state.beacons = beacons;
|
||||
};
|
||||
|
||||
var __applySummary = function(summary) {
|
||||
let active = summary.active_port;
|
||||
if (!__state.summary || __state.summary.active_port !== active) {
|
||||
if (active < 0 || active >= __state.model.ports.length) {
|
||||
$("switch-active-port").innerText = "N/A";
|
||||
} else {
|
||||
$("switch-active-port").innerText = "p" + __formatPort(__state.model, active);
|
||||
}
|
||||
for (let port = 0; port < __state.model.ports.length; ++port) {
|
||||
__setLedState($(`__switch-port-led-p${port}`), "green", (port === active));
|
||||
}
|
||||
}
|
||||
if (__state.atx) {
|
||||
// Синхронизация светодиодов ATX при смене порта
|
||||
let power = false;
|
||||
let busy = false;
|
||||
let hdd = false;
|
||||
if (active >= 0 && active < __state.model.ports.length) {
|
||||
power = __state.atx.leds.power[active];
|
||||
hdd = __state.atx.leds.hdd[active];
|
||||
busy = __state.atx.busy[active];
|
||||
}
|
||||
__setPowerLedState($("switch-atx-power-led"), power, busy);
|
||||
__setLedState($("switch-atx-hdd-led"), "red", hdd);
|
||||
}
|
||||
__state.summary = summary;
|
||||
};
|
||||
|
||||
var __applyModel = function(model) {
|
||||
tools.feature.setEnabled($("switch-dropdown"), model.ports.length);
|
||||
|
||||
let content = "";
|
||||
let unit = -1;
|
||||
for (let port = 0; port < model.ports.length; ++port) {
|
||||
let pa = model.ports[port]; // pa == port attrs
|
||||
if (unit !== pa.unit) {
|
||||
unit = pa.unit;
|
||||
content += `${unit > 0 ? "<tr><td colspan=100><hr></td></tr>" : ""}
|
||||
<tr>
|
||||
<td></td><td></td><td></td>
|
||||
<td class="value">Unit: ${unit + 1}</td>
|
||||
<td></td>
|
||||
<td colspan=100>
|
||||
<div class="buttons-row">
|
||||
<button id="__switch-beacon-button-u${unit}" class="small" title="Toggle uplink Beacon Led">
|
||||
<img id="__switch-beacon-led-u${unit}" class="inline-lamp led-gray" src="/share/svg/led-beacon.svg"/>
|
||||
Uplink
|
||||
</button>
|
||||
<button id="__switch-beacon-button-d${unit}" class="small" title="Toggle downlink Beacon Led">
|
||||
<img id="__switch-beacon-led-d${unit}" class="inline-lamp led-gray" src="/share/svg/led-beacon.svg"/>
|
||||
Downlink
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr><td colspan=100><hr></td></tr>
|
||||
`;
|
||||
}
|
||||
content += `
|
||||
<tr>
|
||||
<td>Port:</td>
|
||||
<td class="value">${__formatPort(model, port)}</td>
|
||||
<td> </td>
|
||||
<td>
|
||||
<div class="buttons-row">
|
||||
<button id="__switch-port-button-p${port}" title="Activate this port">
|
||||
<img id="__switch-port-led-p${port}" class="inline-lamp led-gray" src="/share/svg/led-circle.svg"/>
|
||||
</button>
|
||||
<button id="__switch-params-button-p${port}" title="Configure this port">
|
||||
<img id="__switch-params-led-p${port}" class="inline-lamp led-gray" src="/share/svg/led-gear.svg"/>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<span
|
||||
id="__switch-custom-edid-p${port}" style="visibility:hidden"
|
||||
title="A non-default EDID is used on this port"
|
||||
>
|
||||
⚹
|
||||
</span>
|
||||
|
||||
${pa.name.length > 0 ? tools.escape(pa.name) : ("Host " + (port + 1))}
|
||||
|
||||
</td>
|
||||
<td style="font-size:1em">
|
||||
<button id="__switch-beacon-button-p${port}" class="small" title="Toggle Beacon Led on this port">
|
||||
<img id="__switch-beacon-led-p${port}" class="inline-lamp led-gray" src="/share/svg/led-beacon.svg"/>
|
||||
</button>
|
||||
</td>
|
||||
<td>
|
||||
<img id="__switch-video-led-p${port}" class="inline-lamp led-gray" src="/share/svg/led-video.svg" title="Video Link"/>
|
||||
<img id="__switch-usb-led-p${port}" class="inline-lamp led-gray" src="/share/svg/led-usb.svg" title="USB Link"/>
|
||||
<img id="__switch-atx-power-led-p${port}" class="inline-lamp led-gray" src="/share/svg/led-atx-power.svg" title="Power Led"/>
|
||||
<img id="__switch-atx-hdd-led-p${port}" class="inline-lamp led-gray" src="/share/svg/led-atx-hdd.svg" title="HDD Led"/>
|
||||
</td>
|
||||
<td>
|
||||
<div class="buttons-row">
|
||||
<button id="__switch-atx-power-button-p${port}" class="small">Power <sup><i>short</i></sup></button>
|
||||
<button id="__switch-atx-power-long-button-p${port}" class="small"><sup><i>long</i></sup></button>
|
||||
<button id="__switch-atx-reset-button-p${port}" class="small">Reset</button>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
`;
|
||||
}
|
||||
$("switch-chain").innerHTML = content;
|
||||
|
||||
if (model.units.length > 0) {
|
||||
tools.hidden.setVisible($("switch-message-update"), (model.firmware.version > model.units[0].firmware.version));
|
||||
}
|
||||
|
||||
for (let unit = 0; unit < model.units.length; ++unit) {
|
||||
tools.el.setOnClick($(`__switch-beacon-button-u${unit}`), tools.partial(__switchUplinkBeacon, unit));
|
||||
tools.el.setOnClick($(`__switch-beacon-button-d${unit}`), tools.partial(__switchDownlinkBeacon, unit));
|
||||
}
|
||||
|
||||
for (let port = 0; port < model.ports.length; ++port) {
|
||||
tools.el.setOnClick($(`__switch-port-button-p${port}`), tools.partial(__switchActivePort, port));
|
||||
tools.el.setOnClick($(`__switch-params-button-p${port}`), tools.partial(__showParamsDialog, port));
|
||||
tools.el.setOnClick($(`__switch-beacon-button-p${port}`), tools.partial(__switchPortBeacon, port));
|
||||
tools.el.setOnClick($(`__switch-atx-power-button-p${port}`), tools.partial(__atxClick, port, "power"));
|
||||
tools.el.setOnClick($(`__switch-atx-power-long-button-p${port}`), tools.partial(__atxClick, port, "power_long"));
|
||||
tools.el.setOnClick($(`__switch-atx-reset-button-p${port}`), tools.partial(__atxClick, port, "reset"));
|
||||
}
|
||||
|
||||
__setPowerLedState($("switch-atx-power-led"), false, false);
|
||||
__setLedState($("switch-atx-hdd-led"), "red", false);
|
||||
|
||||
__state.model = model;
|
||||
};
|
||||
|
||||
var __showParamsDialog = function(port) {
|
||||
if (!__state || !__state.model || !__state.edids) {
|
||||
return;
|
||||
}
|
||||
|
||||
let model = __state.model;
|
||||
let edids = __state.edids;
|
||||
|
||||
let atx_actions = {
|
||||
"power": "ATX power click",
|
||||
"power_long": "Power long",
|
||||
"reset": "Reset click",
|
||||
};
|
||||
|
||||
let add_edid_option = function(el, attrs, id) {
|
||||
tools.selector.addOption(el, attrs.name, id, (edids.used[port] === id));
|
||||
if (attrs.parsed !== null) {
|
||||
let parsed = attrs.parsed;
|
||||
let text = "\xA0\xA0\xA0\xA0\xA0\u2570 ";
|
||||
text += (parsed.monitor_name !== null ? parsed.monitor_name : parsed.mfc_id);
|
||||
text += (parsed.audio ? "; +Audio" : "; -Audio");
|
||||
tools.selector.addComment(el, text);
|
||||
}
|
||||
};
|
||||
|
||||
let create_content = function(el_parent) {
|
||||
let html = `
|
||||
<table>
|
||||
<tr>
|
||||
<td>Port name:</td>
|
||||
<td><input
|
||||
type="text" autocomplete="off" id="__switch-port-name-input"
|
||||
value="${tools.escape(model.ports[port].name)}" placeholder="Host ${port + 1}"
|
||||
style="width:100%"
|
||||
/></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>EDID:</td>
|
||||
<td><select id="__switch-port-edid-selector" style="width: 100%"></select></td>
|
||||
</tr>
|
||||
</table>
|
||||
<hr>
|
||||
<table>
|
||||
`;
|
||||
for (let kv of Object.entries(atx_actions)) {
|
||||
html += `
|
||||
<tr>
|
||||
<td style="white-space: nowrap">${tools.escape(kv[1])}:</td>
|
||||
<td style="width: 100%"><input type="range" id="__switch-port-atx-click-${kv[0]}-delay-slider"/></td>
|
||||
<td id="__switch-port-atx-click-${kv[0]}-delay-value"></td>
|
||||
<td> </td>
|
||||
<td><button
|
||||
id="__switch-port-atx-click-${kv[0]}-delay-default-button"
|
||||
class="small" title="Reset default"
|
||||
>↻</button></td>
|
||||
</tr>
|
||||
`;
|
||||
}
|
||||
html += "</table>";
|
||||
el_parent.innerHTML = html;
|
||||
|
||||
let el_selector = $("__switch-port-edid-selector");
|
||||
add_edid_option(el_selector, edids.all["default"], "default");
|
||||
for (let kv of Object.entries(edids.all)) {
|
||||
if (kv[0] !== "default") {
|
||||
tools.selector.addSeparator(el_selector, 20);
|
||||
add_edid_option(el_selector, kv[1], kv[0]);
|
||||
}
|
||||
}
|
||||
|
||||
for (let action of Object.keys(atx_actions)) {
|
||||
let limits = model.limits.atx.click_delays[action];
|
||||
let el_slider = $(`__switch-port-atx-click-${action}-delay-slider`);
|
||||
let display_value = tools.partial(function(action, value) {
|
||||
$(`__switch-port-atx-click-${action}-delay-value`).innerText = `${value.toFixed(1)}`;
|
||||
}, action);
|
||||
let reset_default = tools.partial(function(el_slider, limits) {
|
||||
tools.slider.setValue(el_slider, limits["default"]);
|
||||
}, el_slider, limits);
|
||||
tools.slider.setParams(el_slider, limits.min, limits.max, 0.5, model.ports[port].atx.click_delays[action], display_value);
|
||||
tools.el.setOnClick($(`__switch-port-atx-click-${action}-delay-default-button`), reset_default);
|
||||
}
|
||||
};
|
||||
|
||||
wm.modal(`Port ${__formatPort(__state.model, port)} settings`, create_content, true, true).then(function(ok) {
|
||||
if (ok) {
|
||||
let params = {
|
||||
"port": port,
|
||||
"edid_id": $("__switch-port-edid-selector").value,
|
||||
"name": $("__switch-port-name-input").value,
|
||||
};
|
||||
for (let action of Object.keys(atx_actions)) {
|
||||
params[`atx_click_${action}_delay`] = tools.slider.getValue($(`__switch-port-atx-click-${action}-delay-slider`));
|
||||
};
|
||||
__sendPost("/api/switch/set_port_params", params);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
var __formatPort = function(model, port) {
|
||||
if (model.units.length > 1) {
|
||||
return `${model.ports[port].unit + 1}.${model.ports[port].channel + 1}`;
|
||||
} else {
|
||||
return `${port + 1}`;
|
||||
}
|
||||
};
|
||||
|
||||
var __setLedState = function(el, color, on) {
|
||||
el.classList.toggle(`led-${color}`, on);
|
||||
el.classList.toggle("led-gray", !on);
|
||||
};
|
||||
|
||||
var __setPowerLedState = function(el, power, busy) {
|
||||
el.classList.toggle("led-green", (power && !busy));
|
||||
el.classList.toggle("led-yellow", busy);
|
||||
el.classList.toggle("led-gray", !(power || busy));
|
||||
};
|
||||
|
||||
var __switchActivePort = function(port) {
|
||||
if (__msd_connected) {
|
||||
wm.error(`
|
||||
Oops! Before port switching, please disconnect an active Mass Storage Drive image first.
|
||||
Otherwise, it will break a current USB operation (OS installation, Live CD, or whatever).
|
||||
`);
|
||||
} else {
|
||||
__sendPost("/api/switch/set_active", {"port": port});
|
||||
}
|
||||
};
|
||||
|
||||
var __switchUplinkBeacon = function(unit) {
|
||||
let state = false;
|
||||
try { state = !__state.beacons.uplinks[unit]; } catch {}; // eslint-disable-line no-empty
|
||||
__sendPost("/api/switch/set_beacon", {"uplink": unit, "state": state});
|
||||
};
|
||||
|
||||
var __switchDownlinkBeacon = function(unit) {
|
||||
let state = false;
|
||||
try { state = !__state.beacons.downlinks[unit]; } catch {}; // eslint-disable-line no-empty
|
||||
__sendPost("/api/switch/set_beacon", {"downlink": unit, "state": state});
|
||||
};
|
||||
|
||||
var __switchPortBeacon = function(port) {
|
||||
let state = false;
|
||||
try { state = !__state.beacons.ports[port]; } catch {}; // eslint-disable-line no-empty
|
||||
__sendPost("/api/switch/set_beacon", {"port": port, "state": state});
|
||||
};
|
||||
|
||||
var __atxClick = function(port, button) {
|
||||
let click_button = function() {
|
||||
__sendPost("/api/switch/atx/click", {"port": port, "button": button});
|
||||
};
|
||||
if ($("switch-atx-ask-switch").checked) {
|
||||
wm.confirm(`
|
||||
Are you sure you want to press the <b>${button}</b> button?<br>
|
||||
Warning! This could cause data loss on the server.
|
||||
`).then(function(ok) {
|
||||
if (ok) {
|
||||
click_button();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
click_button();
|
||||
}
|
||||
};
|
||||
|
||||
var __sendPost = function(url, params, error_callback=null) {
|
||||
tools.httpPost(url, params, function(http) {
|
||||
if (http.status !== 200) {
|
||||
if (error_callback) {
|
||||
error_callback();
|
||||
}
|
||||
wm.error("Switch error", http.responseText);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
__init__();
|
||||
}
|
||||
@@ -78,7 +78,7 @@ export var tools = new function() {
|
||||
};
|
||||
|
||||
self.partial = function(func, ...args) {
|
||||
return () => func(...args);
|
||||
return (...rest) => func(...args, ...rest);
|
||||
};
|
||||
|
||||
self.upperFirst = function(text) {
|
||||
@@ -104,10 +104,6 @@ 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) );
|
||||
|
||||
4
web/share/svg/led-beacon.svg
Normal file
4
web/share/svg/led-beacon.svg
Normal file
@@ -0,0 +1,4 @@
|
||||
<?xml version="1.0" encoding="utf-8"?><!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg width="800px" height="800px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M3.51472 0.514648C1.34424 2.68513 0 5.6865 0 8.99993C0 12.3134 1.34424 15.3147 3.51472 17.4852L4.92893 16.071C3.11819 14.2603 2 11.7616 2 8.99993C2 6.23823 3.11819 3.7396 4.92893 1.92886L3.51472 0.514648ZM6.34315 3.34308C4.89653 4.7897 4 6.79107 4 8.99993C4 11.2088 4.89653 13.2102 6.34315 14.6568L7.75736 13.2426C6.67048 12.1557 6 10.6571 6 8.99993C6 7.3428 6.67048 5.84417 7.75736 4.75729L6.34315 3.34308ZM12 4.99995C9.79086 4.99995 8 6.79081 8 8.99995C8 10.8638 9.27477 12.4299 11 12.8739V23H13V12.8739C14.7252 12.4299 16 10.8638 16 8.99995C16 6.79081 14.2091 4.99995 12 4.99995ZM10 8.99995C10 7.89538 10.8954 6.99995 12 6.99995C13.1046 6.99995 14 7.89538 14 8.99995C14 10.1045 13.1046 11 12 11C10.8954 11 10 10.1045 10 8.99995ZM17.6568 3.34308C19.1034 4.7897 20 6.79107 20 8.99993C20 11.2088 19.1034 13.2102 17.6568 14.6568L16.2426 13.2426C17.3295 12.1557 18 10.6571 18 8.99993C18 7.3428 17.3295 5.84417 16.2426 4.75729L17.6568 3.34308ZM20.4852 0.514648C22.6557 2.68513 23.9999 5.6865 23.9999 8.99993C23.9999 12.3134 22.6557 15.3147 20.4852 17.4852L19.071 16.071C20.8817 14.2603 21.9999 11.7616 21.9999 8.99993C21.9999 6.23823 20.8817 3.7396 19.071 1.92886L20.4852 0.514648Z" fill="#000000"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.4 KiB |
22
web/share/svg/led-usb.svg
Normal file
22
web/share/svg/led-usb.svg
Normal file
@@ -0,0 +1,22 @@
|
||||
<?xml version="1.0" encoding="iso-8859-1"?>
|
||||
<!-- Uploaded to: SVG Repo, www.svgrepo.com, Generator: SVG Repo Mixer Tools -->
|
||||
<svg fill="#000000" version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
viewBox="0 0 512 512" xml:space="preserve">
|
||||
<g>
|
||||
<g>
|
||||
<g>
|
||||
<rect x="191.996" y="68.27" width="17.067" height="17.067"/>
|
||||
<rect x="123.729" y="68.27" width="17.067" height="17.067"/>
|
||||
<path d="M448,0h-34.133c-4.719,0-8.533,3.814-8.533,8.533V409.6c0,28.237-22.972,51.2-51.2,51.2H243.2
|
||||
c-28.237,0-51.2-22.963-51.2-51.2v-17.067c9.412,0,17.067-7.654,17.067-17.067v-34.133h51.2c9.412,0,17.067-7.654,17.067-17.067
|
||||
V153.6c0-9.412-7.654-17.067-17.067-17.067v-128c0-4.719-3.823-8.533-8.533-8.533H81.067c-4.719,0-8.533,3.814-8.533,8.533v128
|
||||
c-9.421,0-17.067,7.654-17.067,17.067v170.667c0,9.412,7.646,17.067,17.067,17.067h51.2v34.133
|
||||
c0,9.412,7.646,17.067,17.067,17.067V409.6c0,56.465,45.935,102.4,102.4,102.4h110.933c56.457,0,102.4-45.935,102.4-102.4V8.533
|
||||
C456.533,3.814,452.71,0,448,0z M174.933,59.733c0-4.719,3.814-8.533,8.533-8.533H217.6c4.71,0,8.533,3.814,8.533,8.533v34.133
|
||||
c0,4.719-3.823,8.533-8.533,8.533h-34.133c-4.719,0-8.533-3.814-8.533-8.533V59.733z M115.2,102.4
|
||||
c-4.719,0-8.533-3.814-8.533-8.533V59.733c0-4.719,3.814-8.533,8.533-8.533h34.133c4.71,0,8.533,3.814,8.533,8.533v34.133
|
||||
c0,4.719-3.823,8.533-8.533,8.533H115.2z M149.333,375.467H140.8v-34.133H192l0.009,34.133h-8.542H149.333z"/>
|
||||
</g>
|
||||
</g>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 3.0 KiB |
Reference in New Issue
Block a user