初步整合:

1. python 内置服务器
2. 将配置文件统一目录
This commit is contained in:
mofeng-git
2025-01-01 14:26:22 +00:00
parent 5db37797ea
commit d5a0b1a8b3
411 changed files with 7387 additions and 7263 deletions

View File

@@ -0,0 +1,66 @@
doctype html
//
==============================================================================
# #
# 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/>. #
# #
==============================================================================
- var css_dir = "/share/css"
- var js_dir = "/share/js"
- var svg_dir = "/share/svg"
- var png_dir = "/share/png"
- var title = ""
- var main_js = ""
- var body_class = ""
- var css_list = ["vars", "main"]
block vars
html(lang="en")
head
meta(charset="utf-8")
title #{title}
link(rel="apple-touch-icon" sizes="180x180" href="/share/apple-touch-icon.png")
link(rel="icon" type="image/png" sizes="32x32" href="/share/favicon-32x32.png")
link(rel="icon" type="image/png" sizes="16x16" href="/share/favicon-16x16.png")
link(rel="manifest" href="/share/site.webmanifest")
link(rel="mask-icon" href="/share/safari-pinned-tab.svg" color="#5bbad5")
meta(name="msapplication-TileColor" content="#2b5797")
meta(name="theme-color" content="#ffffff")
each name in css_list
link(rel="stylesheet" href=`${css_dir}/${name}.css`)
link(rel="stylesheet" href=`${css_dir}/user.css`)
script(src=`${js_dir}/i18n/jquery-3.7.1.min.js`)
script(src=`${js_dir}/i18n/jquery.i18n.min.js`)
script(src=`${js_dir}/i18n/i18n.js`)
if main_js
script(type="module")
| import {main} from "#{js_dir}/#{main_js}.js";
| main();
body(class=body_class)
block body

View File

View File

@@ -0,0 +1,74 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 18.1.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Capa_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 31.586 31.586" style="enable-background:new 0 0 31.586 31.586;" xml:space="preserve">
<g>
<path d="M29.331,2.256H2.259C1.01,2.256,0,3.265,0,4.511v22.565c0,1.244,1.01,2.255,2.259,2.255h27.072
c1.242,0,2.255-1.011,2.255-2.255V4.511C31.586,3.265,30.573,2.256,29.331,2.256z M10.788,3.95c0.623,0,1.126,0.502,1.126,1.128
c0,0.623-0.503,1.125-1.126,1.125S9.659,5.701,9.659,5.078C9.659,4.452,10.165,3.95,10.788,3.95z M7.334,3.95
c0.623,0,1.129,0.502,1.129,1.128c0,0.623-0.506,1.125-1.129,1.125c-0.625,0-1.131-0.503-1.131-1.125
C6.203,4.452,6.709,3.95,7.334,3.95z M3.947,3.95c0.623,0,1.129,0.502,1.129,1.128c0,0.623-0.506,1.125-1.129,1.125
c-0.621,0-1.126-0.503-1.126-1.125C2.821,4.452,3.326,3.95,3.947,3.95z M29.331,27.076H2.259V7.922h27.072
C29.331,7.922,29.331,27.076,29.331,27.076z M29.331,5.665H13.536V4.537h15.795C29.331,4.537,29.331,5.665,29.331,5.665z"/>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -0,0 +1,93 @@
<!DOCTYPE html>
<!--
==============================================================================
# #
# 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/>. #
# #
==============================================================================
-->
<html lang="en">
<head>
<meta charset="utf-8">
<title>One-KVM Index</title>
<link rel="apple-touch-icon" sizes="180x180" href="/share/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="/share/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/share/favicon-16x16.png">
<link rel="manifest" href="/share/site.webmanifest">
<link rel="mask-icon" href="/share/safari-pinned-tab.svg" color="#5bbad5">
<meta name="msapplication-TileColor" content="#2b5797">
<meta name="theme-color" content="#ffffff">
<link rel="stylesheet" href="/share/css/vars.css">
<link rel="stylesheet" href="/share/css/main.css">
<link rel="stylesheet" href="/share/css/start.css">
<link rel="stylesheet" href="/share/css/window.css">
<link rel="stylesheet" href="/share/css/modal.css">
<link rel="stylesheet" href="/share/css/index/index.css">
<link rel="stylesheet" href="/share/css/user.css">
<script src="/share/js/i18n/jquery-3.7.1.min.js"></script>
<script src="/share/js/i18n/jquery.i18n.min.js"></script>
<script src="/share/js/i18n/i18n.js"></script>
<script type="module">import {main} from "/share/js/index/main.js";
main();
</script>
</head>
<body>
<div class="start-box">
<div class="start">
<table>
<tr>
<td class="logo"><a href="https://pikvm.org" target="_blank"><img class="svg-gray" src="/share/svg/logo.svg" alt="PiKVM" height="40"></a></td>
<td>
<table>
<tr>
<td class="title" colspan="2" i18n="index_title">The Open Source KVM over IP</td>
</tr>
<tr>
<td class="copyright" colspan="2" i18n="copyright">Copyright &copy; 2018-2024 Maxim Devaev | Modified by SilentWind</td>
</tr>
</table>
</td>
</tr>
</table>
<hr>
<div id="apps-box">
<h4>Loading ...</h4>
</div>
<hr>
<table>
<td class="server">
<td i18n="serve_name">Server:</td>
<td><a id="kvmd-meta-server-host" target="_blank" href="/api/info"></a></td>
</td>
</table>
<div id="app-keyboard-warning">
<p class="text" i18n="index_text_1">
Please note that when you are working with a KVM session or another application that captures the keyboard,
you can't use some keyboard shortcuts such as Ctrl+Alt+Del (which will be caught by your OS) or Ctrl+W (caught by your browser).
</p>
<p class="text" i18n="index_text_2">To override this limitation you can use <a target="_blank" href="https://google.com/chrome">Google Chrome</a>
or <a target="_blank" href="https://chromium.org/Home">Chromium</a> in application mode.
</p>
</div>
<hr>
<p class="text credits"><a target="_blank" href="https://pikvm.org" i18n="index_text_10">PiKVM Project</a>&nbsp; | &nbsp;<a target="_blank" href="https://docs.pikvm.org" i18n="index_text_11">PiKVM Documentation</a>&nbsp; | &nbsp;<a target="_blank" href="https://github.com/mofeng-git/One-KVM" i18n="index_text_12">One-KVM Project</a>&nbsp; | &nbsp;<a target="_blank" href="https://one-kvm.mofeng.run" i18n="index_text_13">One-KVM Documentation</a></p>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,48 @@
extends start.pug
append vars
- title = "One-KVM Index"
- main_js = "index/main"
- css_list = css_list.concat(["window", "modal", "index/index"])
block start
table
tr
td(class="logo")
a(href="https://pikvm.org" target="_blank")
img(class="svg-gray" src=`${svg_dir}/logo.svg` alt="PiKVM" height="40")
td
table
tr #[td(colspan="2" class="title" i18n="index_title") The Open Source KVM over IP]
tr
td(colspan="2" class="copyright" i18n="copyright")
| Copyright &copy; 2018-2024 Maxim Devaev | Modified by SilentWind
hr
div(id="apps-box")
h4 Loading ...
hr
table
td(class="server")
td(i18n="serve_name") Server:
td #[a(id="kvmd-meta-server-host" target="_blank" href="/api/info")]
div(id="app-keyboard-warning")
p(class="text" i18n="index_text_1")
| Please note that when you are working with a KVM session or another application that captures the keyboard,
| you can't use some keyboard shortcuts such as Ctrl+Alt+Del (which will be caught by your OS) or Ctrl+W (caught by your browser).
p(class="text" i18n="index_text_2")
| To override this limitation you can use #[a(target="_blank" href="https://google.com/chrome") Google Chrome]
| or #[a(target="_blank" href="https://chromium.org/Home") Chromium] in application mode.
hr
p(class="text credits")
a(target="_blank" href="https://pikvm.org" i18n="index_text_10") PiKVM Project
| &nbsp; | &nbsp;
a(target="_blank" href="https://docs.pikvm.org" i18n="index_text_11") PiKVM Documentation
| &nbsp; | &nbsp;
a(target="_blank" href="https://github.com/mofeng-git/One-KVM" i18n="index_text_12") One-KVM Project
| &nbsp; | &nbsp;
a(target="_blank" href="https://one-kvm.mofeng.run" i18n="index_text_13") One-KVM Documentation

View File

@@ -0,0 +1,66 @@
<!DOCTYPE html>
<!--
==============================================================================
# #
# 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/>. #
# #
==============================================================================
-->
<html lang="en">
<head>
<meta charset="utf-8">
<title>One-KVM IPMI Info</title>
<link rel="apple-touch-icon" sizes="180x180" href="/share/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="/share/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/share/favicon-16x16.png">
<link rel="manifest" href="/share/site.webmanifest">
<link rel="mask-icon" href="/share/safari-pinned-tab.svg" color="#5bbad5">
<meta name="msapplication-TileColor" content="#2b5797">
<meta name="theme-color" content="#ffffff">
<link rel="stylesheet" href="/share/css/vars.css">
<link rel="stylesheet" href="/share/css/main.css">
<link rel="stylesheet" href="/share/css/start.css">
<link rel="stylesheet" href="/share/css/user.css">
<script src="/share/js/i18n/jquery-3.7.1.min.js"></script>
<script src="/share/js/i18n/jquery.i18n.min.js"></script>
<script src="/share/js/i18n/i18n.js"></script>
<script type="module">import {main} from "/share/js/ipmi/main.js";
main();
</script>
</head>
<body>
<div class="start-box">
<div class="start"><a style="display:inline-block; margin-top:4px; color:#5c90bc; text-decoration:none" href="/" i18n="index">&nbsp;&nbsp;&larr;&nbsp;&nbsp; [ One-KVM Index ]</a>
<hr>
<p class="text" i18n="ipmi_text1">This PiKVM device has running <b>kvmd-ipmi</b> daemon and provides IPMI 2.0 interface for some basic
BMC operations like on/off/reset the server.
</p>
<p class="text" i18n="ipmi_text2"><b>WARNING!</b> We strongly don't recommend you to use IPMI in untrusted networks because
this protocol is completely unsafe by design. In short, the authentication process for IPMI mandates
that the server send a salted SHA1 or MD5 hash of the requested user's password to the client,
prior to the client authenticating.
</p>
<p class="text" i18n="ipmi_text3"><b>NEVER</b> use the same passwords for KVMD and IPMI users. And even better not to use IPMI.
Instead, you can directly use KVMD API via curl. Here some examples:
</p>
<div class="code" id="ipmi-text" style="max-height:200px"></div>
</div>
</div>
</body>
</html>

View File

@@ -0,0 +1,20 @@
extends ../start.pug
append vars
- title = "One-KVM IPMI Info"
- main_js = "ipmi/main"
- index_link = true
block start
p(class="text" i18n="ipmi_text1")
| This PiKVM device has running #[b kvmd-ipmi] daemon and provides IPMI 2.0 interface for some basic
| BMC operations like on/off/reset the server.
p(class="text" i18n="ipmi_text2")
| #[b WARNING!] We strongly don't recommend you to use IPMI in untrusted networks because
| this protocol is completely unsafe by design. In short, the authentication process for IPMI mandates
| that the server send a salted SHA1 or MD5 hash of the requested user's password to the client,
| prior to the client authenticating.
p(class="text" i18n="ipmi_text3")
| #[b NEVER] use the same passwords for KVMD and IPMI users. And even better not to use IPMI.
| Instead, you can directly use KVMD API via curl. Here some examples:
div(id="ipmi-text" class="code" style="max-height:200px")

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,34 @@
extends ../base.pug
append vars
- title = "One-KVM Session"
- main_js = "kvm/main"
- body_class = "body-no-select"
- css_list = css_list.concat(["navbar", "window", "modal", "led", "slider", "switch", "radio", "progress", "keypad", "tabs"])
- css_list = css_list.concat(["kvm/stream", "kvm/hid", "kvm/msd", "kvm/system", "kvm/keyboard", "kvm/about"])
block body
include navbar.pug
include windows.pug
ul(class="navbar-bg-tips")
li(class="left")
pre(id="kvmd-meta-tips-left")
li(class="right")
pre(id="kvmd-meta-tips-right")
ul(class="footer")
li(class="left")
span(id="kvmd-meta-server-host" title="Server name (see System/About)")
| &nbsp; | &nbsp;
span(id="kvmd-version-kvmd" title="KVMD version")
| &nbsp; | &nbsp;
span(id="kvmd-version-streamer" title="Streamer version")
li(class="right")
a(target="_blank" href="https://pikvm.org" i18n="index_text_10") PiKVM Project
| &nbsp; | &nbsp;
a(target="_blank" href="https://docs.pikvm.org" i18n="index_text_11") Documentation
| &nbsp; | &nbsp;
a(target="_blank" href="https://github.com/mofeng-git/One-KVM" i18n="index_text_12") One-KVM Project
| &nbsp; | &nbsp;
a(target="_blank" href="https://one-kvm.mofeng.run" i18n="index_text_13") One-KVM Documentation

View File

@@ -0,0 +1,17 @@
li(id="atx-dropdown" class="right feature-disabled")
a(class="menu-button" href="#")
+navbar_led("atx-power-led", "led-atx-power")
+navbar_led("atx-hdd-led", "led-atx-hdd")
span ATX
div(class="menu")
div(class="text")
b Control the server's power#[br]
sub Use the short click for ACPI shutdown
hr
+menu_switch("atx-ask-switch", "Ask click confirmation", true, true,"atx-ask-switch")
hr
div(class="buttons")
button(disabled data-force-hide-menu id="atx-power-button") &bull; Click Power #[sup #[i short]]
button(disabled data-force-hide-menu id="atx-power-button-long") &bull; Click Power #[sup #[i long]]
hr
button(disabled data-force-hide-menu id="atx-reset-button") &bull; Click Reset

View File

@@ -0,0 +1,4 @@
li(id="gpio-dropdown" class="right feature-disabled")
a(class="menu-button" id="gpio-menu-button" href="#")
span GPIO
div(id="gpio-menu" class="menu")

View File

@@ -0,0 +1,32 @@
div(id="hw-health-dropdown" class="hidden")
li(class="left")
a(class="menu-button" href="#")
+navbar_led("hw-health-undervoltage-led", "led-undervoltage", "hidden")
+navbar_led("hw-health-overheating-led", "led-overheating", "hidden")
div(class="menu")
+menu_message("warning", "Raspberry Pi's health is at risk")
| This is not a drill! A red icon indicates a current issue,#[br]
| a yellow one that was observed since the device booted up
div(id="hw-health-message-undervoltage" class="hidden")
hr
+menu_message("led-undervoltage", "Undervoltage detected", "led-gray")
| Make sure your power supply and cabling are providing#[br]
| enough power to the Raspberry Pi (3A minimum)
div(id="hw-health-message-overheating" class="hidden")
hr
+menu_message("led-overheating", "Overheating detected", "led-gray")
| Frequency capping due to overheating,#[br]
| please improve cooling of the Raspberry Pi
div(id="fan-health-dropdown" class="hidden")
li(class="left")
a(class="menu-button" href="#")
+navbar_led("fan-health-led", "led-fan", "hidden")
div(class="menu")
+menu_message("warning", "Raspberry Pi's health is at risk")
| This is not a drill! A red icon indicates a current issue,#[br]
| a yellow one that was observed in the past
div(id="fan-health-message-fail")
hr
+menu_message("led-fan", "Fan failed", "led-gray")
| A fan error occured, please #[a(href="/api/log?seek=3600&follow=1" target="_blank") check the log]

View File

@@ -0,0 +1,30 @@
li(id="macro-dropdown" class="right")
a(class="menu-button" href="#")
+navbar_led("hid-recorder-led", "led-gear")
span(i18n="kvm_text32") Macro
div(class="menu")
div(class="text")
b(i18n="kvm_text33") Record and play HID/ATX/GPIO actions#[br]
sub(i18n="kvm_text34") For security reasons, the record will not be saved on the PiKVM
hr
div(class="buttons buttons-row")
button(disabled data-force-hide-menu id="hid-recorder-record" class="row25" i18n="kvm_text35") &bull; Rec
button(disabled id="hid-recorder-stop" class="row25" i18n="kvm_text36") Stop
button(disabled id="hid-recorder-play" class="row25" i18n="kvm_text37") Play
button(disabled id="hid-recorder-clear" class="row25" i18n="kvm_text38") Clear
hr
table(class="kv")
tr
td(i18n="kvm_text39") Script time:
td(colspan="2" id="hid-recorder-time" class="value") 00:00:00.0
tr
td(i18n="kvm_text40") Scripted events:
td(id="hid-recorder-events-count" class="value") 0
td #[sup #[i(i18n="kvm_text41") include delays]]
hr
+menu_switch("hid-recorder-loop-switch", "Infinite loop playback", false, false, "hid-recorder-loop-switch")
hr
input(type="file" id="hid-recorder-new-script-file")
div(class="buttons buttons-row")
button(disabled id="hid-recorder-upload" class="row50" i18n="kvm_text42") Upload script
button(disabled id="hid-recorder-download" class="row50" i18n="kvm_text43") Download script

View File

@@ -0,0 +1,124 @@
li(id="msd-dropdown" class="right feature-disabled")
a(class="menu-button" href="#")
+navbar_led("msd-led", "led-msd")
span(i18n="kvm_text60") Drive
div(id="msd-menu" class="menu")
div(class="text")
b(i18n="kvm_text61") Mass Storage Drive:
span(id="msd-status")
br
hr
div(id="msd-message-offline" class="hidden")
+menu_message("warning", "Mass Storage Drive is offline", "msd-message-offline")
hr
div(id="msd-message-image-broken" class="hidden")
+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
hr
div(id="msd-message-out-of-storage" class="hidden")
+menu_message("warning", "Current image is out of storage", "msd-message-out-of-storage")
| This image was connected manually using #[b kvmd-otgmsd]
hr
div(id="msd-message-rw-enabled" class="hidden")
+menu_message("warning", "Read-write mode is enabled", "msd-message-rw-enabled")
| Do not turn off PiKVM while this is active to prevent#[br]
| filesystem corruption. Use read-only mode where possible,#[br]
| as writing to SD card often can reduce its lifespan.
hr
div(id="msd-message-downloads" class="hidden")
+menu_message("info", "The image is being downloaded from PiKVM", "msd-message-downloads")
| Please wait
hr
table(class="kv")
tr
td(i18n="kvm_text62") Image:
td(width="100%") #[select(disabled id="msd-image-selector")]
td #[button(disabled id="msd-download-button" title="Download image") #[b &nbsp;&nbsp;&#x21E9;&nbsp;&nbsp;]]
td #[button(disabled id="msd-remove-button" title="Remove image") #[b &nbsp;&nbsp;&times;&nbsp;&nbsp;]]
table(class="kv")
tr
td(i18n="kvm_text63") Drive #[a(target="_blank" href="https://docs.pikvm.org/msd") mode]:
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
input(type="radio" id="msd-mode-radio-flash" name="msd-mode-radio" value="0")
label(for="msd-mode-radio-flash") Flash
td &nbsp;
+menu_switch_notable("msd-rw-switch", "Writable", false, false, "msd-rw-switch")
tr
td(i18n="kvm_text84") 文件内容:
td
div(class="radio-box")
input(checked type="radio" id="msd-mode-radio-image" name="file-mode-radio" value="1")
label(for="msd-mode-radio-image" i18n="kvm_text90") ImageFiles
input(type="radio" id="msd-mode-radio-file" name="file-mode-radio" value="0")
label(for="msd-mode-radio-file" i18n="kvm_text91") NormalFiles
div(id="msd-storages")
hr
div(class="buttons buttons-row")
button(disabled id="msd-select-new-button" class="row50" i18n="kvm_text64") Select image to upload
button(disabled id="msd-upload-new-button" class="row25" i18n="kvm_text65") Upload
button(disabled id="msd-abort-new-button" class="row25" i18n="kvm_text66") Abort
div(id="msd-message-another-user-uploads" class="hidden")
hr
+menu_message("info", "Another user uploads an image", "msd-message-another-user-uploads")
div(id="msd-new-sub" class="hidden")
hr
table(class="kv")
tr
td(i18n="kvm_text68") Specify a local file:
td #[input(type="file" id="msd-new-file")]
tr
td(i18n="kvm_text69") #[b Or] paste a URL:
td #[input(type="text" id="msd-new-url" style="width: 100%")]
tr(id="msd-new-part" class="hidden")
td(i18n="kvm_text70") Upload partition:
td(width="100%") #[select(id="msd-new-part-selector")]
div(id="msd-uploading-sub" class="hidden")
hr
table(class="kv")
tr
td(i18n="kvm_text74") New image:
td(id="msd-uploading-name" class="value")
tr
td(i18n="kvm_text75") Upload size:
td(id="msd-uploading-size" class="value")
div(class="text")
div(id="msd-uploading-progress" class="progress")
span(id="msd-uploading-progress-value" class="progress-value")
div(id="msd-new-tips" class="hidden")
hr
table(class="kv")
tr
td(class="value" i18n="kvm_text71") Note:
td(i18n="kvm_text72") &bull; Don't close the browser page until the upload is complete.
tr
td
td(i18n="kvm_text73") &bull; To speed up the upload, close the stream window.
hr
div(class="buttons buttons-row")
button(disabled id="msd-connect-button" class="row50" i18n="kvm_text76") Connect drive to Server
button(disabled id="msd-disconnect-button" class="row25" i18n="kvm_text77") Disconnect
button(disabled id="msd-reset-button" class="row25" i18n="kvm_text78") Reset
hr
div(class="text")
b(i18n="kvm_text85") Quick file transfer:
br
sub(i18n="kvm_text86") &bull; Select NormalFiles tab to upload, package them and mount image
br
sub(i18n="kvm_text87") &bull; Disconnect MSD, unpackage it, select tab to download
br
hr
div(class="buttons buttons-row")
button(id="msd-file-image-update-button" class="row50" i18n="kvm_text88") Package files into image
button(id="msd-file-image-unzip-button" class="row50" i18n="kvm_text89") Unpackage files from image
hr

View File

@@ -0,0 +1,68 @@
li(id="shortcuts-dropdown" class="right")
a(class="menu-button" href="#" i18n="kvm_text56") Shortcuts
div(id="shortcuts-menu" class="menu")
div(class="text")
b(i18n="kvm_text57") Quick keyboard shortcuts#[br]
sub(i18n="kvm_text58") Also see #[i System &rarr; Show keyboard]
hr
div(class="buttons")
div(class="buttons-row")
button(data-force-hide-menu data-shortcut="CapsLock" class="row50")
| &bull; Caps Lock &nbsp;
img(class="inline-lamp hid-keyboard-caps-led led-gray" src=`${svg_dir}/led-square.svg`)
button(data-force-hide-menu data-shortcut="MetaLeft" class="row50") &bull; Left Win
hr
div(class="buttons-row")
button(data-force-hide-menu data-shortcut="AltLeft ShiftLeft" class="row50") &bull; Alt+Shift
button(data-force-hide-menu data-shortcut="ControlLeft KeyW" class="row50") &bull; Ctrl+W
div(class="buttons-row")
button(data-force-hide-menu data-shortcut="ControlLeft ShiftLeft" class="row50") &bull; Ctrl+Shift
button(data-force-hide-menu data-shortcut="AltLeft Tab" class="row50") &bull; Alt+Tab
div(class="buttons-row")
button(data-force-hide-menu data-shortcut="ShiftLeft ShiftRight" class="row50") &bull; Shift+Shift
button(data-force-hide-menu data-shortcut="AltLeft Enter" class="row50") &bull; Alt+Enter
div(class="buttons-row")
button(data-force-hide-menu data-shortcut="MetaLeft Space" class="row50") &bull; Win+Space
button(data-force-hide-menu data-shortcut="AltLeft F4" class="row50") &bull; Alt+F4
hr
div(class="buttons-row")
button(data-force-hide-menu data-shortcut="ControlLeft AltLeft F1" class="row50") &bull; Ctrl+Alt+F1
button(data-force-hide-menu data-shortcut="MetaLeft KeyL" class="row50") &bull; Win+L
div(class="buttons-row")
button(data-force-hide-menu data-shortcut="ControlLeft AltLeft F2" class="row50") &bull; Ctrl+Alt+F2
button(data-force-hide-menu data-shortcut="PrintScreen" class="row50") &bull; Print Screen
hr
div(class="buttons-row")
button(data-force-hide-menu data-shortcut="ControlLeft AltLeft Delete" class="row50") &bull; Ctrl+Alt+Del
button(data-force-hide-menu data-shortcut="Power" class="row50") &bull; Power
hr
div(class="text")
| &darr; &bull; Alt+SysRq+... <sup><i>linux magic
| #[a(target="_blank" href="https://www.kernel.org/doc/html/latest/admin-guide/sysrq.html" i18n="kvm_text59") help]</i></sup>
hr
div(class="buttons")
div(class="buttons-row")
-
let sysrq = {
"F": "Call the OOM killer to kill a memory hog process",
"M": "Dump current memory info to the console",
"D": "Show all locks that are held",
"T": "Dump a list of current tasks and their information to the console",
}
each title, key in sysrq
button(data-shortcut=`AltLeft PrintScreen Key${key}` data-shortcut-confirm="hid-sysrq-ask-switch" class="row25" style="text-align: center;" title=`${title}`) #{key}
hr
div(class="buttons-row")
-
sysrq = {
"R": "Turn off keyboard raw mode, set it to XLATE",
"E": "Send a SIGTERM to all processes, except for init",
"I": "Send a SIGKILL to all processes, except for init",
"S": "Attempt to sync all mounted filesystems",
"U": "Attempt to remount all mounted filesystems read-only",
"B": "Immediately reboot the system without syncing or unmounting disks",
}
each title, key in sysrq
button(data-shortcut=`AltLeft PrintScreen Key${key}` data-shortcut-confirm="hid-sysrq-ask-switch" class="row16" style="text-align: center;" title=`${title}`) #{key}
hr
+menu_switch("hid-sysrq-ask-switch", "Ask the magic confirmation", true, true,"hid-sysrq-ask-switch")

View File

@@ -0,0 +1,145 @@
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("hid-keyboard-led", "led-hid-keyboard")
+navbar_led("hid-mouse-led", "led-hid-mouse")
span(i18n="kvm_text3") System
div(id="system-menu" class="menu")
table(class="kv")
tr
td(class="value" i18n="kvm_text4") Runtime settings &amp; tools
td(id="system-tool-webterm" class="feature-disabled") #[button(data-force-hide-menu data-show-window="webterm-window" class="small" i18n="kvm_text5") &bull; Term]
td(id="system-tool-about") #[button(data-force-hide-menu data-show-window="about-window" class="small" i18n="kvm_text6") &bull; About]
td(id="system-tool-log") #[button(data-force-hide-menu id="open-log-button" class="small" i18n="kvm_text7") &bull; Log]
td(id="system-tool-wol" class="feature-disabled")
button(data-force-hide-menu class="__gpio-button-__wol__ __gpio-button small" data-channel="__wol__"
data-confirm="Are you sure to send Wake-on-LAN packet to the server?" i18n="kvm_text8") &bull; WoL
hr
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-h264" class="hidden")
+menu_message("warning", "H.264 is not supported by this browser", "stream-message-no-h264")
hr
table(class="kv")
tr(id="stream-resolution" class="feature-disabled")
td(i18n="kvm_text9") Resolution:
td #[select(disabled id="stream-resolution-selector")]
tr(id="stream-quality" class="feature-disabled")
td(i18n="kvm_text10") JPEG quality:
td(class="value-slider") #[input(disabled type="range" id="stream-quality-slider" class="slider")]
td(id="stream-quality-value" class="value-number")
tr
td(i18n="kvm_text11") JPEG max fps:
td(class="value-slider") #[input(disabled type="range" id="stream-desired-fps-slider" class="slider")]
td(id="stream-desired-fps-value" class="value-number")
tr(id="stream-h264-bitrate" class="feature-disabled")
td(i18n="kvm_text12") H.264 kbps:
td(class="value-slider") #[input(disabled type="range" id="stream-h264-bitrate-slider" class="slider")]
td(id="stream-h264-bitrate-value" class="value-number")
tr(id="stream-h264-gop" class="feature-disabled")
td(i18n="kvm_text13") H.264 #[a(target="_blank" href="https://docs.pikvm.org/webrtc") gop]:
td(class="value-slider") #[input(disabled type="range" id="stream-h264-gop-slider" class="slider")]
td(id="stream-h264-gop-value" class="value-number")
tr(id="stream-mode" class="feature-disabled")
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
tr(id="stream-orient" class="feature-disabled")
td(i18n="kvm_text17") Orientation:
td
div(class="radio-box")
input(checked type="radio" id="stream-orient-radio-0" name="stream-orient-radio" value="0")
label(for="stream-orient-radio-0" i18n="kvm_text18") Default
input(type="radio" id="stream-orient-radio-90" name="stream-orient-radio" value="90")
label(for="stream-orient-radio-90") 90&deg;
input(type="radio" id="stream-orient-radio-180" name="stream-orient-radio" value="180")
label(for="stream-orient-radio-180") 180&deg;
input(type="radio" id="stream-orient-radio-270" name="stream-orient-radio" value="270")
label(for="stream-orient-radio-270") 270&deg;
tr(id="stream-audio", class="feature-disabled")
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")
hr
div(class="buttons buttons-row")
button(data-force-hide-menu data-show-window="stream-window" class="row33" i18n="kvm_text20") &bull; Show stream
button(data-force-hide-menu id="stream-screenshot-button" class="row33" i18n="kvm_text21") &bull; Screenshot
button(id="stream-reset-button" class="row33" i18n="kvm_text22") Reset stream
div(class="text")
b(i18n="kvm_text79") Video Record#[br]
sub(i18n="kvm_text80") Record video using the browser API, and will be downloaded automatically
div(class="buttons buttons-row")
button(data-force-hide-menu id="stream-record-start-button" class="row50" i18n="kvm_text81") &bull; Start recording
button(data-force-hide-menu id="stream-record-stop-button" class="row50" i18n="kvm_text82") &bull; End recording
hr
table(class="kv")
tr(id="hid-outputs-keyboard", class="feature-disabled")
td(i18n="kvm_text23") Keyboard mode:
td #[div(id="hid-outputs-keyboard-box" class="radio-box")]
tr(id="hid-outputs-mouse", class="feature-disabled")
td(i18n="kvm_text24") Mouse #[a(target="_blank" href="https://docs.pikvm.org/mouse") mode]:
td #[div(id="hid-outputs-mouse-box" class="radio-box")]
details
summary(i18n="kvm_text25") Keyboard &amp; Mouse (HID) settings
div(class="spoiler")
table(class="kv")
tr
+menu_switch_notable("hid-keyboard-swap-cc-switch", "Swap Left Ctrl and Caps keys", true, false, "hid-keyboard-swap-cc-switch")
hr
table(class="kv")
tr
td(i18n="kvm_text26") Mouse polling:
td(class="value-slider") #[input(type="range" id="hid-mouse-rate-slider" class="slider")]
td(id="hid-mouse-rate-value" class="value-number")
tr(id="hid-mouse-sens" class="feature-disabled")
td(i18n="kvm_text27") Relative sensitivity:
td(class="value-slider") #[input(disabled type="range" id="hid-mouse-sens-slider" class="slider")]
td(id="hid-mouse-sens-value" class="value-number")
tr(id="hid-mouse-squash" class="feature-disabled")
+menu_switch_notable("hid-mouse-squash-switch", "Squash relative moves", true, true, "hid-mouse-squash-switch")
tr
td(i18n="kvm_text28") Reverse scrolling:
td
table
tr
+menu_switch_notable("hid-mouse-reverse-scrolling-switch", "Y", true, false)
td &nbsp;&nbsp;
+menu_switch_notable("hid-mouse-reverse-panning-switch", "X", true, false)
tr
+menu_switch_notable("hid-mouse-cumulative-scrolling-switch", "Cumulative scrolling", true, false,"hid-mouse-cumulative-scrolling-switch")
tr
td(i18n="kvm_text29") Scroll rate:
td(class="value-slider") #[input(type="range" id="hid-mouse-scroll-slider" class="slider")]
td(id="hid-mouse-scroll-value" class="value-number")
tr
+menu_switch_notable("hid-mouse-dot-switch", "Show the blue dot", true, true, "hid-mouse-dot-switch")
details
summary(i18n="kvm_text83") Web UI settings
div(class="spoiler")
table(class="kv")
tr
+menu_switch_notable("page-close-ask-switch", "Ask page close confirmation", true, true, "page-close-ask-switch")
tr
+menu_switch_notable("page-full-tab-stream-switch", "Expand for the entire tab by default", true, false,"page-full-tab-stream-switch")
table(class="kv")
tr(id="hid-connect" class="feature-disabled")
+menu_switch_notable("hid-connect-switch", "Connect HID to Server", true, true, "hid-connect-switch")
tr(id="hid-jiggler" class="feature-disabled")
+menu_switch_notable("hid-jiggler-switch", "<a href=\"https://docs.pikvm.org/mouse_jiggler\" target=\"_blank\">Mouse jiggler</a>", false, false,"hid-jiggler-switch")
tr
+menu_switch_notable("hid-mute-switch", "Mute HID input events", true, false, "hid-mute-switch")
tr(id="v3-usb-breaker" class="feature-disabled")
+menu_switch_notable_gpio("__v3_usb_breaker__", "Connect main USB to Server",
"Turning off this switch will disconnect the main USB<br>from the server. Are you sure you want to continue?")
tr(id="v4-locator" class="feature-disabled")
+menu_switch_notable_gpio("__v4_locator__", "Enable locator LED")
hr
div(class="buttons buttons-row")
button(data-force-hide-menu data-show-window="keyboard-window" class="row50" i18n="kvm_text30") &bull; Show keyboard
button(disabled id="hid-reset-button" class="row50" i18n="kvm_text31") Reset HID

View File

@@ -0,0 +1,46 @@
li(id="text-dropdown" class="right")
a(class="menu-button" href="#")
+navbar_led("stream-ocr-led", "led-gear", "feature-disabled")
span(i18n="kvm_text44") Text
div(id="text-menu" class="menu")
div(class="text")
b(i18n="kvm_text45") Paste text as keypress sequence#[br]
sub(i18n="kvm_text46") Please note that PiKVM cannot switch the keyboard layout
hr
div(class="text" style="margin-right: 20px")
textarea(id="hid-pak-text" data-focus placeholder="Enter your text here")
table(class="kv")
tr
td
button(disabled data-force-hide-menu id="hid-pak-button" i18n="kvm_text47") &bull; Paste
td(i18n="kvm_text48") using host keymap
td
select(id="hid-pak-keymap-selector")
table(class="kv")
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")
+menu_switch_notable("hid-pak-secure-switch", "Hide input text", true, false,"hid-pak-secure-switch")
div(id="stream-ocr" class="feature-disabled")
hr
br
hr
div(class="text")
b(i18n="kvm_text49") Text recognition <sup><i>&beta;</i></sup>#[br]
sub(i18n="kvm_text50") #[a(target="_blank" href="https://docs.pikvm.org/ocr") OCR] works locally on PiKVM
hr
table(class="kv")
tr
td
button(data-force-hide-menu id="stream-ocr-button" i18n="kvm_text51") &bull; Select area
td(i18n="kvm_text52") for
td
select(id="stream-ocr-lang-selector")
td(i18n="kvm_text53") text recognition
table(class="kv")
tr
td(colspan="4" i18n="kvm_text54") &bull; Press #[b Enter] to recognize and copy text to clipboard
tr
td(colspan="4" i18n="kvm_text55") &bull; Press #[b Esc] to cancel selection
tr
td

View File

@@ -0,0 +1,53 @@
mixin navbar_led(id, icon, cls="led-gray")
img(id=id, class=cls src=`${svg_dir}/${icon}.svg`)
mixin menu_message(icon, short, classes="", i18nid)
div(class="text")
table
tr
td(rowspan="2") #[img(class=`sign ${classes}` src=`${svg_dir}/${icon}.svg`)]
td(style="line-height:1.5") #[b(i18n=i18nid) #{short}]
if block
tr
td
sup(style="line-height:1")
block
mixin menu_switch_notable_gpio(channel, title, confirm_off="")
td !{title}:
td(align="right")
div(class="switch-box")
input(disabled type="checkbox" id=`__gpio-switch-${channel}` class=`__gpio-switch-${channel} gpio-switch`
data-channel=channel data-confirm-off=confirm_off)
label(for=`__gpio-switch-${channel}`)
span(class="switch-inner")
span(class="switch")
mixin menu_switch_notable(id, title, enabled, checked, i18nid)
td(i18n=i18nid) !{title}:
td(align="right")
div(class="switch-box")
input(checked=checked disabled=!enabled type="checkbox" id=id)
label(for=id)
span(class="switch-inner")
span(class="switch")
mixin menu_switch(id, title, enabled, checked, i18nid)
table(class="kv")
tr
+menu_switch_notable(id, title, enabled, checked, i18nid)
ul(id="navbar")
li(class="left")
a(id="logo" href="/") &larr;&nbsp;&nbsp;
img(class="svg-gray" src=`${svg_dir}/logo.svg` alt="&pi;-kvm")
include navbar-health.pug
include navbar-system.pug
include navbar-atx.pug
include navbar-msd.pug
include navbar-macro.pug
include navbar-text.pug
include navbar-shortcuts.pug
include navbar-gpio.pug

View File

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

View File

@@ -0,0 +1,232 @@
mixin key(spacer, code, classes="", width=0)
div(data-code=code, class=`key ${classes}`, style=(width ? `width:${width}px` : ""))
div(class="label")
block
if spacer == 1
div(class="spacer")
else if spacer == 2
div(class="spacer-fixed")
mixin modifier(spacer, code, classes="", width=0)
div(data-code=code class=`modifier ${classes}` style=(width ? `width:${width}px` : ""))
div(class="label")
| #[b &bull;]#[br]
block
if spacer == 1
div(class="spacer")
else if spacer == 2
div(class="spacer-fixed")
mixin empty(spacer, classes="", width=0)
div(class=`empty ${classes}` style=(width ? `width:${width}px` : ""))
div(class="label") &nbsp;
if spacer == 1
div(class="spacer")
else if spacer == 2
div(class="spacer-fixed")
mixin lamp(cls)
img(class=`inline-lamp ${cls} led-gray` src=`${svg_dir}/led-square.svg`)
div(id="keyboard-window" class="window")
div(id="keyboard-window-header" class="window-header")
div(class="window-grab" i18n="kvm_text15") Virtual Keyboard
button(class="window-button-close") #[b &times;]
div(id="keyboard-desktop" class="keypad" align="center")
div(class="keypad-block")
div(class="keypad-row")
+key(2, "Escape", "small") Esc
+empty(1, "", 24)
each key in ["F1", "F2", "F3", "F4", "F5", "F6", "F7", "F8", "F9", "F10", "F11", "F12"]
+key((key != "F12" ? 1 : 0), key, "small") #{key}
if key == "F4" || key == "F8"
+empty(1, "", 10)
hr
div(class="keypad-row")
+key(1, "Backquote") ~#[br]`
each key, index in ["!", "@", "#", "$", "%", "^", "&", "*", "("]
+key(1, `Digit${index + 1}`) #{key}#[br]#{index + 1}
+key(1, "Digit0") )#[br]0
+key(1, "Minus") _#[br]-
+key(1, "Equal") +#[br]=
+key(0, "Backspace", "wide-1 right") &#8612;
div(class="keypad-row")
+key(1, "Tab", "wide-1 left") &#8676;#[br]&#8677;
each key in ["Q", "W", "E", "R", "T", "Y", "U", "I", "O", "P"]
+key(1, `Key${key}`, "single") #{key}
+key(1, "BracketLeft") {#[br][
+key(1, "BracketRight") }#[br]]
+key(0, "Backslash") |#[br]&bsol;
div(class="keypad-row")
+key(1, "CapsLock", "wide-2 left small")
+lamp("hid-keyboard-caps-led")
| #[br] Caps Lock
each key in ["A", "S", "D", "F", "G", "H", "J", "K", "L"]
+key(1, `Key${key}`, "single") #{key}
+key(1, "Semicolon") :#[br];
+key(1, "Quote") "#[br]'
+key(0, "Enter", "wide-2 right small") Enter#[br]&crarr;
div(class="keypad-row")
+modifier(1, "ShiftLeft", "wide-3 left small") Shift
each key in ["Z", "X", "C", "V", "B", "N", "M"]
+key(1, `Key${key}`, "single") #{key}
+key(1, "Comma") &lt;#[br],
+key(1, "Period") &gt;#[br].
+key(1, "Slash") ?#[br]/
+modifier(0, "ShiftRight", "wide-3 right small") Shift
div(class="keypad-row")
+modifier(2, "ControlLeft", "wide-1 left small") Ctrl
+modifier(2, "MetaLeft", "wide-1 left small") Win
+modifier(2, "AltLeft", "wide-1 left small") Alt
+key(2, "Space", "wide-4")
+modifier(2, "AltRight", "wide-1 right small") Alt
+modifier(2, "MetaRight", "wide-1 right small") Win
+key(2, "ContextMenu", "small") #[br]Menu
+modifier(0, "ControlRight", "wide-1 right small") Ctrl
div(class="keypad-block")
div(class="keypad-row")
+modifier(2, "PrintScreen", "small") Pt/Sq
+key(2, "ScrollLock", "small")
+lamp("hid-keyboard-scroll-led")
| #[br] ScrLk
+key(0, "Pause", "small") P/Brk
hr
div(class="keypad-row")
+key(2, "Insert", "small") Ins
+key(2, "Home", "small") Home
+key(0, "PageUp", "small") PgUp
div(class="keypad-row")
+key(2, "Delete", "small") Del
+key(2, "End", "small") End
+key(0, "PageDown", "small") PgDn
div(class="keypad-row")
div(class="keypad-row")
+empty(1, "")
+key(2, "ArrowUp") &uarr;
+empty(0, "")
div(class="keypad-row")
+key(2, "ArrowLeft") &larr;
+key(2, "ArrowDown") &darr;
+key(0, "ArrowRight") &rarr;
div(class="keypad-block")
div(class="keypad-row")
+empty(2, "small")
+empty(2, "small")
+empty(2, "small")
+key(0, "Power", "small") PWR
hr
div(class="keypad-row")
+key(2, "NumLock", "small")
+lamp("hid-keyboard-num-led")
| #[br] NmLk
+key(2, "NumpadDivide") /
+key(2, "NumpadMultiply") *
+key(0, "NumpadSubtract") -
div(class="keypad-row")
+key(2, "Numpad7", "small") 7#[br]Home
+key(2, "Numpad8", "small") 8#[br]&uarr;
+key(2, "Numpad9", "small") 9#[br]PgUp
+empty(0, "")
div(class="keypad-row")
+key(2, "Numpad4", "small") 4#[br]&larr;
+key(2, "Numpad5", "small") 5#[br]#[br]
+key(2, "Numpad6", "small") 6#[br]&rarr;
+key(0, "NumpadAdd") +
div(class="keypad-row")
+key(2, "Numpad1", "small") 1#[br]End
+key(2, "Numpad2", "small") 2#[br]&darr;
+key(2, "Numpad3", "small") 3#[br]PgDn
+empty(0, "")
div(class="keypad-row")
+key(2, "Numpad0", "small") 0#[br]Ins
+empty(2, "")
+key(2, "NumpadDecimal", "small") .#[br]Del
+key(0, "NumpadEnter", "small") Ent
div(class="keypad-block")
div(class="keypad-row")
+key(0, "IntlBackslash", "small") &bsol;#[br]|
hr
div(class="keypad-row")
+key(0, "IntlYen", "small") ¥#[br]_
div(class="keypad-row")
+key(0, "IntlRo", "small") &bsol;#[br]ろ
div(class="keypad-row")
+modifier(0, "KanaMode", "small") Kana
div(class="keypad-row")
+modifier(0, "NonConvert", "small") N/Cnv
div(class="keypad-row")
+modifier(0, "Convert", "small") Cnv
div(id="keyboard-mobile" class="keypad" align="center")
div(class="keypad-block")
div(class="keypad-row")
+key(1, "Escape", "small") Esc
+key(0, "F1", "wide-0 small rounded-left") F1
+key(0, "F2", "wide-0 small rounded-none") F2
+key(0, "F3", "wide-0 small rounded-none") F3
+key(2, "F4", "wide-0 small rounded-right") F4
+key(0, "F5", "wide-0 small rounded-left") F5
+key(0, "F6", "wide-0 small rounded-none") F6
+key(0, "F7", "wide-0 small rounded-none") F7
+key(2, "F8", "wide-0 small rounded-right") F8
+key(0, "F9", "wide-0 small rounded-left") F9
+key(0, "F10", "wide-0 small rounded-none") F10
+key(0, "F11", "wide-0 small rounded-none") F11
+key(2, "F12", "wide-0 small rounded-right") F12
+modifier(1, "PrintScreen", "small") Pt/Sq
+key(1, "ScrollLock", "small")
+lamp("hid-keyboard-scroll-led")
| #[br] ScrLk
+key(1, "Pause", "small") P/Brk
+key(1, "Insert", "small") Ins
+key(1, "Home", "small") Home
+key(1, "End", "small") End
+key(0, "Delete", "small") Del
div(class="keypad-row")
+key(1, "Backquote") ~#[br]`
each key, index in ["!", "@", "#", "$", "%", "^", "&", "*", "("]
+key(1, `Digit${index + 1}`) #{key}#[br]#{index + 1}
+key(1, "Digit0") )#[br]0
+key(1, "Minus") _#[br]-
+key(1, "Equal") +#[br]=
+key(0, "Backspace", "wide-2 right", 101) &#8612;
div(class="keypad-row")
+key(1, "Tab", "wide-1 left") &#8676;<br>&#8677;
each key in ["Q", "W", "E", "R", "T", "Y", "U", "I", "O", "P"]
+key(1, `Key${key}`, "single") #{key}
+key(1, "BracketLeft") {#[br][
+key(1, "BracketRight") }#[br]]
+key(0, "Backslash", "wide-1 left", 78) |#[br]&bsol;
div(class="keypad-row")
+key(1, "CapsLock", "wide-2 left small")
+lamp("hid-keyboard-caps-led")
| #[br] Caps Lock
each key in ["A", "S", "D", "F", "G", "H", "J", "K", "L"]
+key(1, `Key${key}`, "single") #{key}
+key(1, "Semicolon") :#[br];
+key(1, "Quote") `#[br]'
+key(0, "Enter", "wide-3 right small", 116) Enter#[br]&crarr;
div(class="keypad-row")
+modifier(1, "ShiftLeft", "wide-3 left small") Shift
each key in ["Z", "X", "C", "V", "B", "N", "M"]
+key(1, `Key${key}`, "single") #{key}
+key(1, "Comma") &lt;#[br],
+key(1, "Period") &gt;#[br].
+key(1, "Slash") ?#[br]/
+key(2, "PageUp", "small") PgUp
+key(2, "ArrowUp") &uarr;
+key(0, "PageDown", "small") PgDn
div(class="keypad-row")
+modifier(1, "ControlLeft", "wide-1 left small") Ctrl
+modifier(1, "MetaLeft", "wide-1 left small") Win
+modifier(1, "AltLeft", "wide-1 left small") Alt
+key(1, "Space", "", 190)
+modifier(1, "AltRight", "right small") Alt
+modifier(1, "MetaRight", "right small") Win
+key(1, "ContextMenu", "small") #[br]Menu
+modifier(1, "ShiftRight", "right small") Shift
+modifier(1, "ControlRight", "right small") Ctrl
+key(2, "ArrowLeft") &larr;
+key(2, "ArrowDown") &darr;
+key(0, "ArrowRight") &rarr;

View File

@@ -0,0 +1,49 @@
div(id="stream-ocr-window" class="window")
div(id="stream-ocr-selection" class="hidden")
div(id="stream-window" class="window window-resizable")
div(id="stream-window-header" class="window-header")
div(class="window-grab") MJPEG
button(class="window-button-close") #[b &times;]
button(class="window-button-maximize") &#9744;
button(class="window-button-original") &bull;
button(class="window-button-enter-full-tab") &#9650;
button(class="window-button-full-screen") &#10530;
div(id="stream-info")
button(class="window-button-exit-full-tab") &#9660;
div(id="stream-box" class="stream-box-offline")
img(id="stream-image" src=`${png_dir}/blank-stream.png`)
video(id="stream-video" class="hidden" disablePictureInPicture="true" autoplay playsinline muted)
div(id="stream-fullscreen-active")
div(id="stream-mouse-buttons" class="keypad" align="center")
div(class="keypad-block")
div(class="keypad-row")
div(data-code="left" class="key wide-3 left rounded-left")
div(class="label") Left
div(data-code="left" class="modifier left small rounded-right")
div(class="label") #[b &bull;]#[br]Hold
div(class="empty" style="width:15px")
div(data-code="middle" class="key wide-1 left rounded-left")
div(class="label") Mid
div(data-code="middle" class="modifier left small rounded-right")
div(class="label") #[b &bull;]#[br]Hold
div(class="empty" style="width:15px")
div(data-code="right" class="modifier right small rounded-left")
div(class="label") #[b &bull;]#[br]Hold
div(data-code="right" class="key wide-3 right rounded-right")
div(class="label") Right
div(class="empty" style="width:30px")
div(data-code="up" class="key small rounded-left")
div(class="label") Up
div(data-code="down" class="key small rounded-right")
div(class="label") Down
canvas(id="stream-mjpeg-canvas" class="hidden")

View File

@@ -0,0 +1,8 @@
div(id="webterm-window" class="window window-resizable" style="width: 640px; height: 480px")
div(class="window-header")
div(class="window-grab" i18n="kvm_text16") Terminal
button(class="window-button-close") #[b &times;]
button(class="window-button-maximize") &#9744;
// Терминал глючит из-за зажимаемой клавиши ESC для выхода
// button(class="window-button-full-screen") &#10530;
iframe(id="webterm-iframe" src="" style="width: 100%; height: 100%")

View File

@@ -0,0 +1,4 @@
include window-stream.pug
include window-keyboard.pug
include window-about.pug
include window-webterm.pug

View File

@@ -0,0 +1,103 @@
<!DOCTYPE html>
<!--
==============================================================================
# #
# 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/>. #
# #
==============================================================================
-->
<html lang="en">
<head>
<meta charset="utf-8">
<title>One-KVM Login</title>
<link rel="apple-touch-icon" sizes="180x180" href="/share/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="/share/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/share/favicon-16x16.png">
<link rel="manifest" href="/share/site.webmanifest">
<link rel="mask-icon" href="/share/safari-pinned-tab.svg" color="#5bbad5">
<meta name="msapplication-TileColor" content="#2b5797">
<meta name="theme-color" content="#ffffff">
<link rel="stylesheet" href="/share/css/vars.css">
<link rel="stylesheet" href="/share/css/main.css">
<link rel="stylesheet" href="/share/css/window.css">
<link rel="stylesheet" href="/share/css/modal.css">
<link rel="stylesheet" href="/share/css/login/login.css">
<link rel="stylesheet" href="/share/css/user.css">
<script src="/share/js/i18n/jquery-3.7.1.min.js"></script>
<script src="/share/js/i18n/jquery.i18n.min.js"></script>
<script src="/share/js/i18n/i18n.js"></script>
<script type="module">import {main} from "/share/js/login/main.js";
main();
</script>
</head>
<body>
<form action="javascript:void(0)">
<div id="login-box">
<div id="login">
<table>
<tr>
<td i18n="username">Username:&nbsp;</td>
<td>
<input type="text" id="user-input" autocapitalize="off">
</td>
</tr>
<tr>
<td i18n="password">Password:&nbsp;</td>
<td>
<input type="password" id="passwd-input" autocapitalize="off">
</td>
</tr>
<tr>
<td i18n="2fa_code">2FA code:&nbsp;</td>
<td>
<input type="text" id="code-input" placeholder="if enabled" i18n="if_enabled">
</td>
</tr>
<tr>
<td colspan="2">
<hr>
</td>
</tr>
<tr>
<td i18n="select_language">Select language:&nbsp;</td>
<td>
<select id="selectLanguage">
<option id="zh" selected="selected" i18n="chinese">Simplified Chinese</option>
<option id="en" i18n="english">English</option>
</select>
</td>
</tr>
<tr>
<td></td>
<td>
<button class="key" id="login-button" i18n="login">Login</button>
</td>
</tr>
</table>
</div>
</div>
</form>
<ul class="footer">
<li class="left" i18n="footer-left">This site is actively using JavaScript.<br>
It doesn't contain ads, but is blocked by some ad filters.<br>
Please turn it off to continue and reload the page.
</li>
</ul>
</body>
</html>

View File

@@ -0,0 +1,39 @@
extends ../base.pug
append vars
- title = "One-KVM Login"
- main_js = "login/main"
- css_list = css_list.concat(["window", "modal", "login/login"])
block body
form(action="javascript:void(0)")
div(id="login-box")
div(id="login")
table
tr
td(i18n="username") Username:&nbsp;
td #[input(type="text" id="user-input" autocapitalize="off")]
tr
td(i18n="password") Password:&nbsp;
td #[input(type="password" id="passwd-input" autocapitalize="off")]
tr
td(i18n="2fa_code") 2FA code:&nbsp;
td #[input(type="text" id="code-input" placeholder="if enabled" i18n="if_enabled")]
tr
td(colspan=2)
hr
tr
td(i18n="select_language") Select language:&nbsp;
td
select(id="selectLanguage")
option(id='zh', selected="selected" i18n="chinese") Simplified Chinese
option(id='en' i18n="english") English
tr
td
td #[button(id="login-button" class="key" i18n="login") Login]
ul(class="footer")
li(class="left" i18n="footer-left")
| This site is actively using JavaScript.#[br]
| It doesn't contain ads, but is blocked by some ad filters.#[br]
| Please turn it off to continue and reload the page.

View File

@@ -0,0 +1,2 @@
User-Agent: *
Disallow: /

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

View File

@@ -0,0 +1,97 @@
/*****************************************************************************
# #
# 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/>. #
# #
*****************************************************************************/
div#apps-box {
display: table;
margin: 0 auto;
}
div#apps-box ul#apps {
list-style-type: none;
padding: 0;
margin: 0;
}
div#apps-box ul#apps li {
float: left;
margin-left: 5px;
margin-right: 5px;
}
div#apps-box ul#apps li div.app {
height: 100px;
width: 100px;
text-align: center;
background-color: var(--cs-control-default-bg);
box-shadow: var(--shadow-micro);
border: var(--border-key-thin);
border-radius: 8px;
}
div#apps-box ul#apps li div:hover.app {
border: var(--border-intensive-thin);
box-shadow: none;
}
div#apps-box ul#apps li div:active.app {
color: var(--cs-control-pressed-fg) !important;
background-color: var(--cs-control-pressed-bg) !important;
border: var(--border-key-thin);
box-shadow: none;
}
div#apps-box ul#apps li div.app img {
display: block;
margin: auto;
height: 50px;
padding-bottom: 5px;
}
div#apps-box ul#apps li div.app a {
display: flex;
flex-direction: column;
justify-content: center;
height: 100%;
text-decoration: none;
font-weight: bold;
}
td.logo {
padding-right: 25px;
}
td.title {
font-size: 1.2em;
}
td.copyright {
font-size: 0.8em;
}
tr.server {
font-size: 1.4em;
font-weight: bold;
font-family: monospace;
}
@media only screen and (display-mode: standalone) {
div#app-keyboard-warning {
display: none;
}
}

View File

@@ -0,0 +1,144 @@
/*****************************************************************************
# #
# 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/>. #
# #
*****************************************************************************/
div.keypad {
zoom: 0.8;
}
div.keypad div.keypad-block {
display: table-cell;
padding-right: 0;
}
div.keypad div.keypad-block:not(:first-child) {
padding-left: 15px;
}
div.keypad div.keypad-row {
display: flex;
flex-wrap: wrap;
white-space: nowrap;
height: 40px;
margin-bottom: 5px;
}
div.keypad div.keypad-row:last-child {
margin-bottom: 0;
}
div.keypad div.keypad-row div.spacer {
margin: 2px;
flex-grow: 1;
}
div.keypad div.keypad-row div.spacer-fixed {
margin: 3px;
}
div.keypad div.key,
div.keypad div.modifier,
div.keypad div.empty {
vertical-align: top;
font-size: 0.9em;
text-align: center;
box-sizing: border-box;
padding: 0;
width: 40px;
height: 40px;
}
div.keypad div.empty {
border: thin solid transparent;
}
div.keypad div.key,
div.keypad div.modifier {
box-shadow: var(--shadow-micro);
border: var(--border-key-thin);
border-radius: 6px;
color: var(--cs-key-default-fg);
background-color: var(--cs-key-default-bg);
cursor: pointer;
}
div.keypad div.key:hover,
div.keypad div.modifier:hover {
color: var(--cs-key-hovered-fg);
background-color: var(--cs-key-hovered-bg);
}
div.keypad div.rounded-left {
border-radius: 6px 0px 0px 6px !important;
}
div.keypad div.rounded-right {
border-radius: 0px 6px 6px 0px !important;
}
div.keypad div.rounded-none {
border-radius: 0px !important;
}
div.keypad div.pressed {
box-shadow: none;
color: var(--cs-key-pressed-fg) !important;
background-color: var(--cs-key-pressed-bg) !important;
}
div.keypad div.holded {
box-shadow: none;
color: var(--cs-key-default-fg) !important;
background-color: var(--cs-key-holded-bg) !important;
}
div.keypad div.key:last-child,
div.keypad div.empty:last-child,
div.keypad div.modifier:last-child {
margin-right: 0;
}
div.keypad div.wide-0 {
width: 28px;
}
div.keypad div.wide-1 {
width: 61px;
}
div.keypad div.wide-2 {
width: 77px;
}
div.keypad div.wide-3 {
width: 102px;
}
div.keypad div.wide-4 {
flex-grow: 1;
width: 288px;
}
div.keypad div.left {
text-align: left !important;
padding-left: 6px !important;
}
div.keypad div.right {
text-align: right !important;
padding-right: 6px !important;
}
div.keypad div.small {
font-size: 0.7em;
}
div.keypad div.label {
margin: 0;
position: relative;
top: 50%;
-webkit-transform: translateY(-50%);
transform: translateY(-50%);
}
div.keypad b {
color: var(--cs-key-holded-bg);
}

View File

@@ -0,0 +1,46 @@
/*****************************************************************************
# #
# 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/>. #
# #
*****************************************************************************/
div#about {
max-width: 600px;
white-space: normal;
padding: 5px 5px 5px 5px;
}
div#about td.logo {
padding-right: 25px;
}
div#about td.title {
font-size: 1.2em;
}
div#about td.copyright {
font-size: 0.8em;
}
div#about div.tabs-box div.tab div.code {
-webkit-user-select: text;
-moz-user-select: text;
user-select: text;
height: 250px;
}

View File

@@ -0,0 +1,29 @@
/*****************************************************************************
# #
# 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/>. #
# #
*****************************************************************************/
div#text-menu {
width: 340px;
}
input#hid-recorder-new-script-file {
display: none;
}

View File

@@ -0,0 +1,28 @@
/*****************************************************************************
# #
# 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/>. #
# #
*****************************************************************************/
div#keyboard-desktop {
display: block;
}
div#keyboard-mobile {
display: none;
}

View File

@@ -0,0 +1,30 @@
/*****************************************************************************
# #
# 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/>. #
# #
*****************************************************************************/
div#msd-menu {
width: 450px;
}
div#msd-menu div.msd-message,
div#msd-menu input.msd-message {
display: none;
}

View File

@@ -0,0 +1,111 @@
/*****************************************************************************
# #
# 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/>. #
# #
*****************************************************************************/
div#stream-window {
min-width: 400px;
min-height: 200px;
/*padding-top: 23px;
padding-bottom: 2px;
padding-left: 2px;
padding-right: 2px;*/
}
div#stream-info {
display: none;
}
div#stream-ocr-window {
cursor: crosshair;
position: absolute;
width: 100%;
height: 100%;
top: 0;
left: 0;
z-index: 1;
background-color: unset !important;
border-radius: unset !important;
border: unset !important;
padding: 0px !important;
background: radial-gradient(transparent 15%, black);
}
div#stream-ocr-selection {
position: relative;
background-color: #5b90bb50;
box-shadow: inset 0 0 0px 1px #e8e8e8cd;
}
div#stream-box {
width: 100%;
height: 100%;
object-fit: contain;
position: relative;
display: inline-block;
border: var(--border-window-default-thin);
margin: -1px -1px -1px -1px; /* See pikvm/kvm#86, pikvm/pikvm#599 */
}
div.stream-box-offline {
-webkit-filter: grayscale(100%) brightness(75%) sepia(75%);
filter: grayscale(100%) brightness(75%) sepia(75%);
}
div.stream-box-offline::after {
cursor: wait;
content: "";
position: absolute;
left: 0;
top: 0;
width: 100%;
height: 100%;
display: inline-block;
background: radial-gradient(transparent 20%, black);
}
div.stream-box-mouse-dot {
cursor: url("../../svg/stream-mouse-cursor.svg") 5 5, pointer;
}
div.stream-box-mouse-none {
cursor: none;
}
img#stream-image,
video#stream-video {
width: 100%;
height: 100%;
object-fit: contain;
display: block;
background-color: black;
}
div#stream-window.window-active:fullscreen {
border: 0px !important;
border-radius: 0px !important;
}
div#stream-window.window-active:fullscreen div#stream-box div#stream-fullscreen-active {
position: absolute;
width: 100%;
height: 100%;
top: 0;
left: 0;
border: 0;
box-shadow: var(--shadow-window-fullscreen-active);
}
div#stream-mouse-buttons {
display: none;
}

View File

@@ -0,0 +1,26 @@
/*****************************************************************************
# #
# 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/>. #
# #
*****************************************************************************/
div#system-menu {
min-width: 400px;
max-width: 400px;
}

View File

@@ -0,0 +1,89 @@
/*****************************************************************************
# #
# 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/>. #
# #
*****************************************************************************/
/* ===== stream.css ===== */
div#stream-window {
padding-top: 3px !important;
padding-bottom: 80px !important;
border-top: 0 !important;
border-left: 0 !important;
border-right: 0 !important;
border-radius: 0 !important;
top: 50px !important;
left: 50% !important;
width: 100% !important;
-webkit-transform: translateX(-50%) !important;
transform: translateX(-50%) !important;
}
div#stream-window::after {
display: none;
}
div#stream-window-header {
display: none !important;
}
div#stream-info {
display: block !important;
margin: 0;
padding: 0;
padding-bottom: 3px;
font-size: 0.8em;
color: var(--cs-window-header-default-fg);
}
div#stream-mouse-buttons {
display: block !important;
}
/* ===== keyboard.css ===== */
div#keyboard-window {
visibility: visible !important;
padding-top: 9px !important;
padding-bottom: 30px !important;
border-bottom: 0 !important;
border-left: 0 !important;
border-right: 0 !important;
border-radius: 0 !important;
top: unset !important;
bottom: 0 !important;
width: 100% !important;
left: 50% !important;
-webkit-transform: translateX(-50%) !important;
transform: translateX(-50%) !important;
}
div#keyboard-window-header {
display: none !important
}
div#keyboard-desktop {
display: none !important;
}
div#keyboard-mobile {
display: block !important;
}

View File

@@ -0,0 +1,82 @@
/*****************************************************************************
# #
# 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/>. #
# #
*****************************************************************************/
@-webkit-keyframes spin {
100% {
-webkit-transform: rotate(360deg);
}
}
@keyframes spin {
100% {
-webkit-transform: rotate(360deg);
transform: rotate(360deg);
}
}
:root {
--led-filter-gray: invert(0.5);
--led-filter-green: invert(0.5) sepia(1) saturate(5) hue-rotate(100deg);
--led-filter-red: invert(0.5) sepia(1) saturate(15) hue-rotate(320deg);
--led-filter-yellow: invert(0.5) sepia(1) saturate(5) hue-rotate(0deg);
--led-spin-slow: spin 6s linear infinite;
--led-spin-medium: spin 3s linear infinite;
--led-spin-fast: spin 2s linear infinite;
}
img.led-gray {
-webkit-transform: translateZ(0); /* Без этого новый сафари не переключает иконки */
-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);
filter: var(--led-filter-yellow);
}
img.led-red-rotating-fast {
-webkit-filter: var(--led-filter-red);
filter: var(--led-filter-red);
-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);
}

View File

@@ -0,0 +1,50 @@
/*****************************************************************************
# #
# 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/>. #
# #
*****************************************************************************/
div#login-box {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
text-align: center;
min-height: 100vh;
}
div#login {
text-align: left;
outline: none;
word-wrap: break-word;
max-width: 400px;
border: var(--border-window-default-thin);
border-radius: 8px;
box-sizing: border-box;
box-shadow: var(--shadow-big);
background-color: var(--cs-window-default-bg);
padding: 15px;
}
input[type="text"]#user-input,
input[type="password"]#passwd-input,
input[type="text"]#code-input {
text-align: center;
border: thin;
}

View File

@@ -0,0 +1,320 @@
/*****************************************************************************
# #
# 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/>. #
# #
*****************************************************************************/
body {
margin: 0;
overflow: hidden;
color: var(--cs-page-default-fg);
background-color: var(--cs-page-default-bg);
font-family: arial, sans-serif !important;
}
body.body-no-select {
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
touch-action: manipulation;
}
a {
color: var(--cs-page-default-fg);
}
hr {
border: none;
border-top: var(--border-default-thin);
}
p.text {
text-align: justify;
}
p:not(:first-child).text {
margin-top: 0;
}
p:last-child.text {
margin-bottom: 0;
}
div.code {
white-space: nowrap;
overflow-x: auto;
font-family: monospace;
border-radius: 4px;
color: var(--cs-code-default-fg);
background-color: var(--cs-code-default-bg);
padding: 10px;
}
div.code::-webkit-scrollbar {
width: 8px;
height: 8px;
}
div.code::-webkit-scrollbar-thumb {
border-radius: 4px;
background: var(--cs-scroll-default-bg);
}
@-moz-document url-prefix() {
div.code {
scrollbar-width: 8px;
scrollbar-color: var(--cs-scroll-default-bg) var(--cs-code-default-bg);
}
}
div.code span.code-comment {
color: var(--cs-code-comment-fg);
}
img.svg-gray {
-webkit-filter: invert(0.7);
filter: invert(0.7);
vertical-align: middle;
}
img.inline-lamp {
vertical-align: middle;
height: 8px;
margin-left: 2px;
margin-right: 2px;
}
img.inline-lamp-big {
vertical-align: middle;
height: 20px;
margin-left: 2px;
margin-right: 2px;
}
button,
select,
input[type=file]::-webkit-file-selector-button,
input[type=file]::file-selector-button {
border: none;
border-radius: 4px;
color: var(--cs-control-default-fg);
background-color: var(--cs-control-default-bg);
height: 30px;
font-family: inherit;
font-size: 16px;
outline: none;
cursor: pointer;
}
button {
display: block;
width: 100%;
}
select {
display: block;
width: 100%;
padding-left: 5px;
}
select[size] {
height: auto;
padding: 5px;
}
select[size]::-webkit-scrollbar {
width: 8px;
height: 8px;
}
select[size]::-webkit-scrollbar-thumb {
border-radius: 4px;
background: var(--cs-scroll-default-bg);
}
@-moz-document url-prefix() {
select[size] {
scrollbar-width: 8px;
scrollbar-color: var(--cs-scroll-default-bg) var(--cs-code-default-bg);
}
}
select:not([size]) {
padding-right: 25px;
}
button.small {
font-size: 12px;
height: 20px;
}
button.key,
select.key {
border: var(--border-key-thin);
box-shadow: var(--shadow-micro);
}
button:disabled,
select:disabled,
input[type=file]:disabled::-webkit-file-selector-button,
input[type=file]:disabled::file-selector-button {
color: var(--cs-control-disabled-fg);
cursor: default;
}
select {
-webkit-appearance: none;
-moz-appearance: none;
appearance: none;
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
}
select:not([size]) {
background-image: url("../svg/select-arrow-normal.svg");
background-position: center right;
background-repeat: no-repeat;
}
select:not([size]):disabled {
background-image: url("../svg/select-arrow-inactive.svg") !important;
}
select:not([size]):active {
color: var(--cs-control-intensive-fg) !important;
background-image: url("../svg/select-arrow-intensive.svg") !important;
}
select:not([size]) option {
color: var(--cs-control-default-fg);
background-color: var(--cs-control-default-bg);
}
select:not([size]) option.comment {
color: var(--cs-control-disabled-fg);
font-style: italic;
}
input[type=text], input[type=password] {
overflow-x: auto;
font-family: monospace;
border-radius: 4px;
border: var(--border-default-thin);
color: var(--cs-code-default-fg);
background-color: var(--cs-code-default-bg);
padding: 2px;
height: 30px;
}
textarea {
display: block;
resize: none;
height: 120px;
width: 100%;
border: var(--border-default-thin);
border-radius: 4px;
color: var(--cs-code-default-fg);
background-color: var(--cs-code-default-bg);
-webkit-appearance:none;
}
textarea::-moz-placeholder {
line-height: 60px;
text-align: center;
}
textarea::-webkit-input-placeholder {
line-height: 60px;
text-align: center;
}
div.buttons-row {
margin: 0;
padding: 0;
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) {
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) {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
table.kv {
border-spacing: 5px;
margin: 0 10px 0 10px;
font-size: 12px;
}
table.kv td {
text-align: left;
}
table.kv td.value {
font-weight: bold;
max-width: 310px;
overflow: hidden;
}
table.kv td.value-slider {
width: 100%;
}
table.kv td.value-number {
font-weight: bold;
max-width: 310px;
overflow: hidden;
min-width: 40px;
max-width: 40px;
}
ul.footer {
list-style-type: none;
bottom: 0;
position: fixed;
width: 100%;
padding: 0;
font-size: 0.7em;
color: var(--cs-page-obscure-fg);
z-index: -10;
}
ul.footer li {
padding: 0 10px;
}
ul.footer li.left {
float: left;
}
ul.footer li.right {
float: right;
}
ul.footer li a {
color: var(--cs-page-obscure-fg);
}
.credits {
font-size: 0.8em;
}
.feature-disabled {
display: none !important;
}
.hidden {
display: none !important;
}

View File

@@ -0,0 +1,76 @@
/*****************************************************************************
# #
# 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/>. #
# #
*****************************************************************************/
div.modal {
visibility: hidden;
position: fixed;
left: 0;
top: 0;
width: 100%;
height: 100%;
background-color: rgb(0, 0, 0);
background-color: rgba(0, 0, 0, 0.5);
z-index: 2147483647;
}
div.modal div.modal-window {
display: table;
outline: none;
margin: 15% auto;
overflow: hidden;
border: var(--border-window-default-2px);
border-radius: 8px;
box-sizing: border-box;
box-shadow: var(--shadow-big);
background-color: var(--cs-window-default-bg);
padding: 0;
}
div.modal div.modal-window-active {
border: var(--border-window-active-2px) !important;
}
div.modal div.modal-window div.modal-header {
text-align: center;
font-weight: bold;
padding: 3px 9px 3px 9px;
border-bottom: var(--border-default-thin);
}
div.modal div.modal-window div.modal-content {
max-width: 500px;
max-height: 500px;
padding: 16px 9px 16px 9px;
}
div.modal div.modal-window div.modal-buttons {
border-top: var(--border-control-thin);
margin: 0;
padding: 0;
font-size: 0;
}
div.modal div.modal-window div.modal-buttons button {
box-shadow: none;
border: none;
border-radius: 0;
height: 40px;
}

View File

@@ -0,0 +1,230 @@
/*****************************************************************************
# #
# 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/>. #
# #
*****************************************************************************/
ul#navbar {
box-shadow: var(--shadow-small);
list-style-type: none;
margin: 0;
padding: 0;
background-color: var(--cs-navbar-default-bg);
position: fixed;
top: 0;
width: 100%;
height: 50px;
z-index: 2147483646;
}
ul#navbar li.right {
border-left: var(--border-navbar-item-thin);
float: right;
}
ul#navbar li.left {
border-right: var(--border-navbar-item-thin);
float: left;
}
ul#navbar li a#logo {
height: 50px; /* Чтобы вертикальные разделители не вылезали за пределы навбара */
line-height: 50px;
outline: none;
cursor: pointer;
display: inline-block;
color: var(--cs-navbar-default-fg);
padding-left: 16px;
padding-right: 16px;
text-decoration: none;
}
ul#navbar li a.menu-button {
height: 50px; /* То же самое */
line-height: 50px;
outline: none;
cursor: pointer;
display: inline-block;
color: var(--cs-navbar-default-fg);
padding-left: 16px;
padding-right: 16px;
text-decoration: none;
position: relative;
}
/*ul#navbar li a.menu-button:before {
content: "";
position: absolute;
top: calc(100% - 10px);
right: 0;
width: 0;
border-bottom: 10px solid var(--cs-navbar-item-pressed-bg);
border-left: 10px solid transparent;
}*/
ul#navbar li a#logo:hover:not(.active),
ul#navbar li a.menu-button:hover:not(.active) {
background-color: var(--cs-navbar-item-hovered-bg);
}
ul#navbar li a#logo img {
margin-top: -2px;
height: 24px;
}
ul#navbar li a.menu-button span,
ul#navbar li a.menu-button img {
vertical-align: middle;
height: 20px;
}
ul#navbar li a.menu-button span:not(:last-child),
ul#navbar li a.menu-button img:not(:last-child) {
margin-right: 10px;
}
ul#navbar li a.menu-button-pressed {
box-shadow: var(--shadow-navbar-item-pressed);
background-color: var(--cs-navbar-item-pressed-bg) !important;
}
ul#navbar li div.menu {
visibility: hidden;
outline: none;
overflow-x: hidden;
overflow-y: auto;
max-height: calc(100vh - 55px);
white-space: nowrap;
border: var(--border-navbar-menu-default-2px);
border-top: var(--border-navbar-menu-top-thin);
border-radius: 0 0 8px 8px;
position: absolute;
background-color: var(--cs-navbar-default-bg);
box-shadow: var(--shadow-big);
z-index: 2147483645;
}
ul#navbar li div.menu-active {
border: var(--border-navbar-menu-active-2px) !important;
border-top: var(--border-navbar-menu-top-thin) !important;
}
ul#navbar li div.menu::-webkit-scrollbar {
width: 8px;
height: 8px;
}
ul#navbar li div.menu::-webkit-scrollbar-thumb {
border-radius: 4px;
background: var(--cs-scroll-default-bg);
}
@-moz-document url-prefix() {
ul#navbar li div.menu {
scrollbar-width: 8px;
scrollbar-color: var(--cs-scroll-default-bg) var(--cs-code-default-bg);
}
}
ul#navbar li div.menu details summary {
cursor: pointer;
outline: none;
text-decoration: underline;
margin: 8px 15px 8px 15px;
font-size: 12px;
}
ul#navbar li div.menu details div.spoiler {
margin-left: 20px !important;
border-left: var(--border-default-thin);
border-bottom: var(--border-default-thin);
}
ul#navbar li div.menu details div.spoiler hr {
border: none;
border-top: var(--border-default-thin);
}
ul#navbar li div.menu details summary::marker {
color: var(--cs-marker-fg);
}
ul#navbar li div.menu div.buttons {
background-color: var(--cs-control-default-bg);
}
ul#navbar li div.menu div.text {
margin: 10px 15px 10px 15px;
font-size: 14px;
}
ul#navbar li div.menu div.buttons button,
ul#navbar li div.menu div.buttons select {
border-radius: 0;
text-align: left;
padding: 0 16px;
}
ul#navbar li div.menu input[type=text] {
height: 1.5em;
}
ul#navbar li div.menu input[type=text]::-moz-placeholder {
text-align: center;
}
ul#navbar li div.menu input[type=text]::-webkit-input-placeholder {
text-align: center;
}
ul#navbar li div.menu hr {
margin: 0;
display: block;
height: 0px;
padding: 0;
border: none;
border-top: var(--border-control-thin);
}
ul#navbar li div.menu img.sign {
vertical-align: middle;
margin-right: 10px;
height: 20px;
}
ul.navbar-bg-tips {
list-style-type: none;
top: 50px;
position: fixed;
width: 100%;
padding: 0;
font-size: 0.7em;
line-height: 1.5em;
color: var(--cs-page-obscure-fg);
z-index: -10;
}
ul.navbar-bg-tips li {
padding: 0 10px;
max-width: 20%;
}
ul.navbar-bg-tips li pre {
word-break: break-word;
white-space: break-spaces;
text-align: justify;
}
ul.navbar-bg-tips li.left {
float: left;
}
ul.navbar-bg-tips li.right {
float: right;
}
ul.navbar-bg-tips li a {
color: var(--cs-page-obscure-fg);
}

View File

@@ -0,0 +1,45 @@
/*****************************************************************************
# #
# 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/>. #
# #
*****************************************************************************/
div.progress {
background-color: var(--cs-progress-default-bg);
height: 1.5em;
width: 100%;
position: relative;
}
div.progress:before {
color: var(--cs-progress-default-fg);
content: attr(data-label);
font-size: 0.8em;
position: absolute;
text-align: center;
top: 4px;
left: 0;
right: 0;
}
div.progress span.progress-value {
background-color: var(--cs-progress-bar-bg);
display: inline-block;
height: 100%;
}

View File

@@ -0,0 +1,79 @@
/*****************************************************************************
# #
# 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/>. #
# #
*****************************************************************************/
div.radio-box {
display: flex;
flex-wrap: wrap;
}
div.radio-box input[type=radio] {
display: none;
}
div.radio-box label {
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
display: flex;
align-items: center;
text-align: center;
vertical-align: middle;
padding: 4px 8px;
cursor: pointer;
position: relative;
}
div.radio-box label:not(:last-of-type) {
margin-right: 1px;
box-shadow: 1px 0 0 0 var(--cs-control-pressed-bg);
}
div.radio-box label:first-of-type {
border-top-left-radius: 4px;
border-bottom-left-radius: 4px;
}
div.radio-box label:last-of-type {
border-top-right-radius: 4px;
border-bottom-right-radius: 4px;
}
div.radio-box input[type=radio]:checked + label {
/*font-weight: bold;*/
text-shadow: 0.5px 0 0 currentColor;
border: var(--border-intensive-2px);
color: var(--cs-control-intensive-fg);
background-color: var(--cs-thumb-default-bg);
}
div.radio-box input[type=radio] + label {
border: var(--border-default-2px);
color: var(--cs-control-default-fg);
background-color: var(--cs-control-default-bg);
}
div.radio-box input[type=radio]:checked:disabled + label {
cursor: default;
border: var(--border-default-2px);
color: var(--cs-control-default-fg);
background-color: var(--cs-control-default-bg);
}
div.radio-box input[type=radio]:not(:checked):disabled + label {
cursor: default;
color: var(--cs-control-disabled-fg);
background-color: var(--cs-thumb-disabled-bg);
}

View File

@@ -0,0 +1,93 @@
/*****************************************************************************
# #
# 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/>. #
# #
*****************************************************************************/
@supports (-webkit-appearance:none) {
input[type=range].slider {
cursor: pointer;
outline: none;
width: 100%;
box-shadow: none;
background: transparent;
margin: 8px 0 8px 0;
-webkit-appearance: none;
-webkit-tap-highlight-color: transparent;
}
}
@supports not (-webkit-appearance:none) {
input[type=range].slider {
cursor: pointer;
outline: none;
width: 100%;
box-shadow: none;
margin-left: 0;
margin-right: 0;
}
}
input[type=range].slider:disabled {
cursor: default;
}
input[type=range].slider::-webkit-slider-runnable-track {
height: 5px;
background: var(--cs-control-default-bg);
border-radius: 3px;
}
input[type=range].slider:disabled::-webkit-slider-runnable-track {
cursor: default;
}
input[type=range].slider::-webkit-slider-thumb {
border: var(--border-intensive-2px);
height: 18px;
width: 18px;
border-radius: 25px;
background: var(--cs-thumb-default-bg);
-webkit-appearance: none;
margin-top: -7px;
}
input[type=range].slider:disabled::-webkit-slider-thumb {
cursor: default;
border: var(--border-default-2px);
background: var(--cs-thumb-disabled-bg);
}
input[type=range].slider::-moz-range-track {
height: 5px;
background: var(--cs-control-default-bg);
border-radius: 3px;
}
input[type=range].slider:disabled::-moz-range-track {
cursor: default;
}
input[type=range].slider::-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 {
cursor: default;
border: var(--border-default-2px);
background: var(--cs-thumb-disabled-bg);
}

View File

@@ -0,0 +1,43 @@
/*****************************************************************************
# #
# 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/>. #
# #
*****************************************************************************/
div.start-box {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
text-align: center;
min-height: 100vh;
}
div.start-box div.start {
text-align: left;
outline: none;
word-wrap: break-word;
max-width: 800px;
border: var(--border-window-default-thin);
border-radius: 8px;
box-sizing: border-box;
box-shadow: var(--shadow-big);
background-color: var(--cs-window-default-bg);
padding: 15px;
}

View File

@@ -0,0 +1,105 @@
/*****************************************************************************
# #
# 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/>. #
# #
*****************************************************************************/
div.switch-box {
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
display: inline-block;
vertical-align: middle;
position: relative;
width: 50px;
margin-top: 2px;
margin-bottom: 2px;
}
div.switch-box input[type=checkbox] {
display: none;
}
div.switch-box label {
display: block;
overflow: hidden;
cursor: pointer;
border: none;
border-radius: 13px;
}
div.switch-box label span.switch-inner {
display: block;
width: 200%;
margin-left: -100%;
}
div.switch-box label span.switch-inner:before,
div.switch-box label span.switch-inner:after {
display: block;
float: left;
width: 50%;
height: 17px;
padding: 0;
line-height: 18px;
font-size: 10px;
font-family: sans-serif !important;
font-weight: bold;
box-sizing: border-box;
}
div.switch-box label span.switch-inner:before {
content: "ON";
padding-left: 5px;
background-color: var(--cs-control-default-bg);
color: var(--cs-control-default-fg);
text-align: left;
}
div.switch-box label span.switch-inner:after {
content: "OFF";
padding-right: 5px;
background-color: var(--cs-control-default-bg);
color: var(--cs-control-disabled-fg);
text-align: right;
}
div.switch-box label span.switch {
display: block;
width: 13px;
margin: 0px;
background: var(--cs-thumb-default-bg);
position: absolute;
top: 0;
bottom: 0;
border: var(--border-intensive-2px);
border-radius: 13px;
}
div.switch-box input[type=checkbox]:checked + label span.switch-inner {
margin-left: 0;
}
div.switch-box input[type=checkbox]:checked + label span.switch {
right: 0px;
}
div.switch-box input[type=checkbox]:disabled + label span.switch {
background: var(--cs-thumb-disabled-bg);
border: var(--border-default-2px);
}

View File

@@ -0,0 +1,67 @@
/*****************************************************************************
# #
# 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/>. #
# #
*****************************************************************************/
div.tabs-box {
display: flex;
flex-wrap: wrap;
}
div.tabs-box input[type="radio"] {
display: none;
}
div.tabs-box div.tab {
order: 99;
display: none;
border: var(--border-default-thin);
padding: 10px 10px;
width: 100%;
}
div.tabs-box label {
-webkit-user-select: none;
-moz-user-select: none;
user-select: none;
order: 1;
display: flex;
align-items: center;
text-align: center;
vertical-align: middle;
border-top: var(--border-default-thin);
border-left: var(--border-default-thin);
padding: 4px 8px;
cursor: pointer;
position: relative;
top: 1px;
}
div.tabs-box label:last-of-type {
border-right: var(--border-default-thin);
}
div.tabs-box input[type="radio"]:checked + label {
background-color: var(--cs-control-default-bg);
}
div.tabs-box input[type="radio"]:checked + label + .tab {
display: block;
}

View File

@@ -0,0 +1,94 @@
/*****************************************************************************
# #
# 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/>. #
# #
*****************************************************************************/
:root {
--cs-page-default-bg: #36393f;
--cs-page-default-fg: #c3c3c3;
--cs-page-obscure-fg: #6c7481;
--cs-control-default-bg: #36393f;
--cs-control-default-fg: #c3c3c3;
--cs-control-intensive-fg: white;
--cs-control-hovered-bg: #2a2d31;
--cs-control-hovered-fg: white;
--cs-control-pressed-bg: #17191d;
--cs-control-pressed-fg: #6c7481;
--cs-control-disabled-fg: #6c7481;
--cs-navbar-default-bg: #202225;
--cs-navbar-default-fg: #c3c3c3;
--cs-navbar-item-hovered-bg: #1a1c1f;
--cs-navbar-item-pressed-bg: #171717;
--cs-window-default-bg: #484b51;
--cs-window-default-fg: #c3c3c3;
--cs-window-header-default-fg: #aaaaaa;
--cs-window-header-grabbed-bg: #436a8a;
--cs-window-header-grabbed-fg: white;
--cs-window-closer-default-fg: #6c7481;
--cs-code-default-bg: #17191d;
--cs-code-default-fg: #aaaaaa;
--cs-code-comment-fg: #6c7481;
--cs-scroll-default-bg: #6c7481;
--cs-thumb-default-bg: #436a8a;
--cs-thumb-disabled-bg: #202225;
--cs-progress-default-bg: #171717;
--cs-progress-default-fg: white;
--cs-progress-bar-bg: #436a8a;
--cs-key-default-bg: #3b3e43;
--cs-key-default-fg: #c3c3c3;
--cs-key-hovered-bg: #2a2d31;
--cs-key-hovered-fg: white;
--cs-key-pressed-bg: #17191d;
--cs-key-pressed-fg: #6c7481;
--cs-key-holded-bg: #436a8a;
--cs-marker-fg: #5b90bb;
--cs-corner-bg: #5b90bb;
--shadow-micro: 1px 2px 4px 0 rgba(0, 0, 0, 0.4);
--shadow-small: 0 2px 4px 0 rgba(0, 0, 0, 0.2);
--shadow-big: 0 8px 16px 0 rgba(0, 0, 0, 0.4);
--shadow-navbar-item-pressed: 0 5px 0 #5b90bb inset;
--shadow-window-fullscreen-active: 0 0 0 2px #5b90bb inset;
--border-default-thin: thin solid #36393f;
--border-default-2px: 2px solid #36393f;
--border-hovered-2px: 2px solid #2a2d31;
--border-navbar-item-thin: thin solid black;
--border-control-thin: thin solid #17191d;
--border-key-thin: thin solid #202225;
--border-intensive-2px: 2px solid #5b90bb;
--border-intensive-thin: thin solid #5b90bb;
--border-window-default-2px: 2px solid #282a2e;
--border-window-active-2px: 2px solid #5b90bb;
--border-window-default-thin: thin solid #17191d;
--border-navbar-menu-default-2px: 2px solid black;
--border-navbar-menu-active-2px: 2px solid #5b90bb;
--border-menu-item-content-top-thin: thin solid #17191d;
}

View File

@@ -0,0 +1,176 @@
/*****************************************************************************
# #
# 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/>. #
# #
*****************************************************************************/
div.window {
visibility: hidden;
outline: none;
overflow: hidden;
position: fixed;
border: var(--border-window-default-2px);
border-radius: 8px;
box-sizing: border-box;
box-shadow: var(--shadow-big);
white-space: nowrap;
color: var(--cs-window-default-fg);
background-color: var(--cs-window-default-bg);
padding: 30px 9px 9px 9px;
}
div.window-resizable {
resize: both;
}
div.window-active {
border: var(--border-window-active-2px) !important;
}
div.window-resizable.window-active::after {
content: "";
width: 0;
height: 0;
border-style: solid;
border-width: 0 0 15px 15px;
border-color: transparent transparent var(--cs-corner-bg) transparent;
right: 0;
bottom: 0;
position: absolute;
}
div.window:fullscreen {
border: 0px;
border-radius: 0px;
resize: none !important;
position: absolute !important;
top: 0px !important;
left: 0px !important;
width: 100% !important;
height: 100% !important;
padding: 0px !important;
}
div.window:fullscreen::after {
display: none;
}
div.window:-webkit-full-screen {
border: 0px;
border-radius: 0px;
resize: none !important;
position: absolute !important;
top: 0px !important;
left: 0px !important;
width: 100% !important;
height: 100% !important;
padding: 0px !important;
}
div.window:-webkit-full-screen::after {
display: none;
}
div.window.window-full-tab {
border: 0px;
border-radius: 0px;
resize: none !important;
top: 0px !important;
left: 0px !important;
width: 100% !important;
height: 100% !important;
padding: 0px !important;
}
div.window div.window-header {
overflow: hidden;
top: 0;
left: 0;
position: absolute;
width: 100%;
padding: 0;
height: 20px;
font-size: 0.8em;
color: var(--cs-window-header-default-fg);
border-bottom: var(--border-default-thin);
}
div.window:fullscreen div.window-header {
display: none !important;
}
div.window:-webkit-full-screen div.window-header {
display: none !important;
}
div.window div.window-header div.window-grab {
overflow: hidden;
top: 0;
left: 0;
position: absolute;
width: 100%;
height: 20px;
cursor: move;
padding: 3px 0 2px 20px;
}
div.window div.window-header-grabbed {
color: var(--cs-window-header-grabbed-fg);
background-color: var(--cs-window-header-grabbed-bg);
border-bottom: var(--border-intensive-thin);
}
div.window div.window-header button.window-button-full-screen,
div.window div.window-header button.window-button-enter-full-tab,
div.window div.window-header button.window-button-original,
div.window div.window-header button.window-button-maximize,
div.window div.window-header button.window-button-close {
border: none;
position: absolute;
top: -2px;
width: 44px;
height: 24px;
padding-left: 0;
padding-right: 0;
color: var(--cs-window-closer-default-fg);
display: inline-block;
}
div.window div.window-header button.window-button-full-screen {
right: 180px;
}
div.window div.window-header button.window-button-enter-full-tab {
right: 135px;
}
div.window div.window-header button.window-button-original {
right: 90px;
}
div.window div.window-header button.window-button-maximize {
right: 45px;
}
div.window div.window-header button.window-button-close {
right: 0px;
}
div.window button.window-button-exit-full-tab {
width: 50px;
height: 10px;
left: calc(50% - 25px);
font-size: 8px;
position: absolute;
opacity: 0.5;
border-radius: 0px;
visibility: hidden;
z-index: 1;
}
div.window button.window-button-exit-full-tab:hover {
opacity: 1;
}
div.window.window-full-tab button.window-button-exit-full-tab {
visibility: visible !important;
}

View File

@@ -0,0 +1,88 @@
/*****************************************************************************
# #
# 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/>. #
# #
*****************************************************************************/
/* ===== main.css ===== */
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 {
color: var(--cs-control-hovered-fg);
background-color: var(--cs-control-hovered-bg);
}
button:active,
select:not([size]):active,
input[type=file]:active::-webkit-file-selector-button,
input[type=file]:active::file-selector-button {
color: var(--cs-control-pressed-fg) !important;
background-color: var(--cs-control-pressed-bg) !important;
}
button.key:active,
select.key:active {
box-shadow: none;
}
select:not([size]):enabled:hover {
background-image: url("../svg/select-arrow-intensive.svg") !important;
}
/* ===== radio.css ===== */
/* If we have a mouse cursor */
div.radio-box input[type=radio]:not(:checked):not(:disabled) + label:hover {
border: var(--border-hovered-2px);
color: var(--cs-control-hovered-fg) !important;
background-color: var(--cs-control-hovered-bg) !important;
}
/* ===== slider.css ===== */
/*div.switch-box label span.switch-inner:not(:disabled):hover::before {*/
input[type=range].slider: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 {
background-color: var(--cs-control-hovered-bg);
}
/* ===== switch.css ===== */
div.switch-box label span.switch-inner:not(:disabled):hover::before {
background-color: var(--cs-control-hovered-bg);
}
div.switch-box label span.switch-inner:not(:disabled):hover::after {
background-color: var(--cs-control-hovered-bg);
}
/* ===== tabs.css ===== */
div.tabs-box input[type="radio"]:not(:checked):hover + label {
color: var(--cs-control-hovered-fg);
}

View File

@@ -0,0 +1,121 @@
/*****************************************************************************
# #
# 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/>. #
# #
*****************************************************************************/
/* ===== main.css ===== */
button:active,
select:active,
input[type=file]:active::-webkit-file-selector-button,
input[type=file]:active::file-selector-button {
color: var(--cs-control-hovered-fg);
background-color: var(--cs-control-hovered-bg);
}
@media only screen and (orientation: portrait) {
button,
select,
input[type=text],
input[type=password],
input[type=file]::-webkit-file-selector-button,
input[type=file]::file-selector-button {
height: 45px !important;
}
}
/* ===== window.css ===== */
div.window {
padding-top: 45px !important;
}
div.window div.window-header {
height: 35px !important;
}
div.window div.window-header div.window-grab {
height: 35px !important;
}
div.window div.window-header button.window-button-full-screen,
div.window div.window-header button.window-button-enter-full-tab,
div.window div.window-header button.window-button-original,
div.window div.window-header button.window-button-maximize,
div.window div.window-header button.window-button-close {
height: 40px !important;
}
/* ===== modal.css ===== */
@media only screen and (orientation: portrait) {
div.modal-buttons button {
height: 50px !important;
}
}
/* ===== navbar.css ===== */
ul#navbar li a#logo:hover:not(.active),
ul#navbar li a.menu-button:hover:not(.active) {
background-color: var(--cs-navbar-default-bg);
}
/* ===== radio.css ===== */
/*@media only screen and (orientation: portrait) {
div.radio-box input[type=radio] + label {
height: 30px !important;
}
}*/
/* ===== slider.css ===== */
/*@media only screen and (orientation: portrait) {
@supports (-webkit-appearance: none) {
input[type=range].slider {
margin: 20px 0 20px 0 !important;
}
}
}*/
/* ===== tabs.css ===== */
@media only screen and (orientation: portrait) {
div.tabs-box label {
height: 30px !important;
}
}
/* ===== keypad.css ===== */
div.keypad {
zoom: 1.28 !important;
}
div.keypad div.key:hover,
div.keypad div.modifier:hover {
color: var(--cs-key-default-fg);
background-color: var(--cs-key-default-bg);
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.5 KiB

View File

@@ -0,0 +1,154 @@
{
"username": "Username:&nbsp;",
"password": "Password:&nbsp;",
"2fa_code": "2FA code:&nbsp;",
"if_enabled": "if enabled",
"login": "Login",
"select_language": "Select language:&nbsp;",
"chinese": "Simplified Chinese",
"english": "English",
"footer-left": "This site is actively using JavaScript.<br>It doesn't contain ads, but is blocked by some ad filters.<br>Please turn it off to continue and reload the page.",
"index": " One-KVM Index ",
"copyright": "Copyright &copy; 2018-2024 Maxim Devaev | Modified by SilentWind",
"index_text_1": "Please note that when you are working with a KVM session or another application that captures the keyboard,you can't use some keyboard shortcuts such as Ctrl+Alt+Del (which will be caught by your OS) or Ctrl+W (caught by your browser).",
"index_text_2": "To override this limitation you can use Google Chrome in application mode.",
"serve_name": "Server:",
"index_text_10":"PiKVM Project",
"index_text_11":"PiKVM Documentation",
"index_text_12":"One-KVM Project",
"index_text_13":"One-KVM Documentation",
"index_title":"The Open Source KVM over IP",
"vnc_text1": "This One-KVM device has running <b>kvmd-vnc</b> daemon and provides VNC access to the server.",
"vnc_text2": "<b>WARNING!</b> We strongly don't recommend you to use VNC in untrusted networks without enabled X.509 or TLS encryption. Otherwise your passwords are transmitted in a plain text over the network.",
"vnc_text3": "Your VNC client must support Tight JPEG compression and password authentication. <a href=\"https://tigervnc.org\">TigerVNC</a> is a good choice.On Linux, this client will most likely be available for installation from the repository. It can also be called vncviewer.",
"ipmi_text1":"This One-KVM device has running <b>kvmd-ipmi</b> daemon and provides IPMI 2.0 interface for some basic BMC operations like on/off/reset the server.",
"ipmi_text2":"<b>WARNING!</b> We strongly don't recommend you to use IPMI in untrusted networks because this protocol is completely unsafe by design. In short, the authentication process for IPMI mandates that the server send a salted SHA1 or MD5 hash of the requested user's password to the client, prior to the client authenticating.",
"ipmi_text3":"<b>NEVER</b> use the same passwords for KVMD and IPMI users. And even better not to use IPMI. Instead, you can directly use KVMD API via curl. Here some examples:",
"kvm_text1":"About",
"kvm_text2":"// These <a href=\"https://github.com/pikvm/pikvm?tab=readme-ov-file#special-thanks\">kind people</a> donated money to the PiKVM project and supported the work on it. <br>// If you also want to support this project, you can donate on <a target=\"_blank\" href=\"https://www.patreon.com/pikvm\"> Patreon</a> or <a target=\"_blank\" href=\"https://paypal.me/pikvm\"> Paypal</a>.<br><br>// These <a href=\"https://one-kvm.mofeng.run/thanks/#_2\">kind people</a> donated money to the One-KVM project and supported the work on it. <br>// If you also want to support this project, you can donate on <a target=\"_blank\" href=\"https://afdian.com/a/silentwind\"> afdian </a>.",
"kvm_text3":"System",
"kvm_text4":"Runtime settings &amp; tools",
"kvm_text5":"Term",
"kvm_text6":"About",
"kvm_text7":"Log",
"kvm_text8":"WoL",
"kvm_text9":"Resolution:",
"kvm_text10":"JPEG quality:",
"kvm_text11":"JPEG max fps:",
"kvm_text12":"H.264 kbps:",
"kvm_text13":"H.264 gop",
"kvm_text14":"Video mode",
"kvm_text15":"Virtual Keyboard",
"kvm_text16":"Terminal",
"kvm_text17":"Orientation:",
"kvm_text18":"Default",
"kvm_text19":"Audio volume:",
"kvm_text20":"&bull; Show stream",
"kvm_text21":"&bull; Screenshot",
"kvm_text22":"Reset stream",
"kvm_text23":"Keyboard mode:",
"kvm_text24":"Mouse mode:",
"kvm_text25":"Keyboard &amp; Mouse (HID) settings",
"kvm_text26":"Mouse polling:",
"kvm_text27":"Relative sensitivity:",
"kvm_text28":"Reverse scrolling:",
"kvm_text29":"Scroll rate:",
"kvm_text30":"&bull; Show keyboard",
"kvm_text31":"Reset HID",
"kvm_text32":"Macro",
"kvm_text33":"Record and play HID/ATX/GPIO actions<br>",
"kvm_text34":"For security reasons, the record will not be saved on the PiKVM",
"kvm_text35":"Rec",
"kvm_text36":"Stop",
"kvm_text37":"Play",
"kvm_text38":"Clear",
"kvm_text39":"Script time:",
"kvm_text40":"Scripted events:",
"kvm_text41":"include delays",
"kvm_text42":"Upload script",
"kvm_text43":"Download script",
"kvm_text44":"Text",
"kvm_text45":"Paste text as keypress sequence<br>",
"kvm_text46":"Please note that One-KVM cannot switch the keyboard layout",
"kvm_text47":"&bull; Paste",
"kvm_text48":"using host keymap",
"kvm_text49":"Text recognition <sup><i>&beta;</i></sup><br>",
"kvm_text50":"OCR works locally on One-KVM",
"kvm_text51":"&bull; Select area",
"kvm_text52":"for",
"kvm_text53":"text recognition",
"kvm_text54":"Press <b>Enter</b> to recognize and copy text to clipboard",
"kvm_text55":"Press <b>Esc</b> to cancel selection",
"kvm_text56":"Shortcuts",
"kvm_text57":"Quick keyboard shortcuts<br>",
"kvm_text58":"Also see <i>System &rarr; Show keyboard<i>",
"kvm_text59":"help",
"kvm_text60":"Drive",
"kvm_text61":"Mass Storage Drive:",
"kvm_text62":"Image:",
"kvm_text63":"Drive <a target=\"_blank\" href=\"https://docs.pikvm.org/msd\">mode</a>:",
"kvm_text64":"Select image to upload",
"kvm_text65":"Upload",
"kvm_text66":"Abort",
"kvm_text68":"Specify a local file:",
"kvm_text69":"<b>Or</b> paste a URL:",
"kvm_text70":"Upload partition:",
"kvm_text71":"Note:",
"kvm_text72":"&bull; Don't close the browser page until the upload is complete.",
"kvm_text73":"&bull; To speed up the upload, close the stream window.",
"kvm_text74":"New image:",
"kvm_text75":"Upload size:",
"kvm_text76":"Connect drive to Server",
"kvm_text77":"Disconnect",
"kvm_text78":"Reset",
"kvm_text79":"Video Record<br>",
"kvm_text80":"Record video using the browser API, and will be downloaded automatically",
"kvm_text81":"Start recording",
"kvm_text82":"End recording",
"kvm_text83":"Web UI settings",
"kvm_text84":"File display:",
"kvm_text85":"Quick file transfer:",
"kvm_text86":"&bull; Select NormalFiles tab to upload, package them and mount image",
"kvm_text87":"&bull; Disconnect MSD, unpackage it, select tab to download",
"kvm_text88":"Package files into image",
"kvm_text89":"Unpackage files from image",
"kvm_text90":"ImageFiles",
"kvm_text91":"NormalFiles",
"atx-ask-switch":"Ask click confirmation",
"hid-recorder-loop-switch":"Infinite loop playback",
"msd-rw-switch":"Writable",
"hid-sysrq-ask-switch":"Ask the magic confirmation",
"hid-keyboard-swap-cc-switch":"Swap Left Ctrl and Caps keys",
"hid-mouse-squash-switch":"Squash relative moves",
"hid-mouse-cumulative-scrolling-switch":"Cumulative scrolling",
"hid-mouse-dot-switch":"Show the blue dot",
"hid-connect-switch":"Connect HID to Server",
"hid-jiggler-switch":"<a href=\"https://docs.pikvm.org/mouse_jiggler\" target=\"_blank\">Mouse jiggler</a>",
"hid-mute-switch":"Mute HID input events",
"page-close-ask-switch":"Ask page close confirmation",
"hid-pak-ask-switch":"Ask paste confirmation",
"hid-pak-secure-switch":"Hide input text",
"meta":"Meta",
"hardware":"Hardware",
"version":"Version",
"thanks":"Thanks",
"stream-message-no-webrtc":"WebRTC is not supported by this browser",
"stream-message-no-h264":"H.264 is not supported by this browser",
"msd-message-offline":"Mass Storage Drive is offline",
"msd-message-image-broken":"Current image is broken!",
"msd-message-too-big-for-cdrom":"Current image is too big for CD-ROM!",
"msd-message-out-of-storage":"Current image is out of storage",
"msd-message-rw-enabled":"Read-write mode is enabled",
"msd-message-downloads":"The image is being downloaded from One-KVM",
"msd-message-another-user-uploads":"Another user uploads an image",
"page-full-tab-stream-switch":"Expand for the entire tab by default"
}

View File

@@ -0,0 +1,156 @@
{
"username": "用户名:&nbsp;",
"password": "密码:&nbsp;",
"2fa_code": "2FA 验证:&nbsp;",
"if_enabled": "如果有",
"login": "登录",
"select_language": "选择语言:&nbsp;",
"chinese": "简体中文",
"english": "英语",
"footer-left": "本网站需要使用 JavaScript 功能。<br>本网站不含广告,但仍有可能会被某些广告过滤器屏蔽。<br>如果网页响应异常请关闭它并重新加载页面以继续浏览。",
"index": " One-KVM 导航 ",
"copyright": "版权所有 &copy; 2018-2024 Maxim Devaev | 由 SilentWind 二次开发",
"index_text_1": "请注意,在使用 KVM 会话或其他可捕获键盘的应用程序时,您可能无法使用某些快捷键,如 Ctrl+Alt+Del这会被操作系统捕获或 Ctrl+W这会被浏览器捕获。",
"index_text_2": "要突破这一限制,您可以使用 Google Chrome 浏览器。",
"serve_name": "主机名:",
"index_text_10":"PiKVM 项目",
"index_text_11":"PiKVM 文档",
"index_text_12":"One-KVM 项目",
"index_text_13":"One-KVM 文档",
"index_title":"开源 KVM over IP",
"vnc_text1": "该 One-KVM 设备 <b>kvmd-vnc</b> 守护进程已运行,提供对服务器的 VNC 访问。",
"vnc_text2": "<b>不建议</b>在未启用 X.509 或 TLS 加密的网络中使用 VNC。否则您的密码将以纯文本形式在网络上传输。",
"vnc_text3": "VNC 客户端必须支持 Tight JPEG 压缩和密码验证。",
"ipmi_text1":"该 One-KVM 设备 <b>kvmd-ipmi</b> 守护进程已运行,并为一些基本的 BMC 操作(如打开/关闭/重置服务器)提供了 IPMI 2.0 接口。",
"ipmi_text2":"<b>不建议</b>在不受信任的网络中使用 IPMI因为该协议的设计完全不安全。<br>原因如下IPMI 的身份验证过程要求服务器在客户端进行身份验证之前,向客户端发送请求用户密码的加盐 SHA1 或 MD5 哈希值。",
"ipmi_text3":"<b>不建议</b>为 KVMD 和 IPMI 用户使用相同的密码,或者直接关闭 IPMI。如有需要可以通过 curl 直接使用 KVMD API。下面是一些示例",
"kvm_text1":"关于",
"kvm_text2":"//<a href=\"https://github.com/pikvm/pikvm?tab=readme-ov-file#special-thanks\">这些人</a>向 PiKVM 项目捐款并支持其工作,非常感谢他们的帮助。<br>//如果您也想支持 PiKVM ,可以在 <a target=\"_blank\" href=\"https://www.patreon.com/pikvm\"> Patreon</a> 或 <a target=\"_blank\" href=\"https://paypal.me/pikvm\"> PayPal 上捐款</a>。<br><br>//<a href=\"https://one-kvm.mofeng.run/thanks/#_2\">这些人</a>向 One-KVM 项目捐款并支持其工作,非常感谢他们的帮助。<br>//如果您也想支持 One-KVM ,可以在 <a target=\"_blank\" href=\"https://afdian.com/a/silentwind\"> 爱发电 </a>上捐款</a>。",
"kvm_text3":"系统",
"kvm_text4":"运行设置 &amp; 工具",
"kvm_text5":"终端",
"kvm_text6":"关于",
"kvm_text7":"日志",
"kvm_text8":"网络唤醒",
"kvm_text9":"分辨率:",
"kvm_text10":"JPEG 质量:",
"kvm_text11":"JPEG 最大FPS",
"kvm_text12":"H.264 比特率:",
"kvm_text13":"H.264 帧间隔:",
"kvm_text14":"视频模式",
"kvm_text15":"虚拟键盘",
"kvm_text16":"终端",
"kvm_text17":"方向:",
"kvm_text18":"默认",
"kvm_text19":"音量:",
"kvm_text20":"&bull; 显示视频流",
"kvm_text21":"&bull; 截屏",
"kvm_text22":"重置视频流",
"kvm_text23":"键盘模式:",
"kvm_text24":"鼠标模式:",
"kvm_text25":"键盘 &amp; 鼠标 (HID) 设置",
"kvm_text26":"鼠标轮询:",
"kvm_text27":"相对灵敏度:",
"kvm_text28":"反向滚动:",
"kvm_text29":"滚动速率:",
"kvm_text30":"显示虚拟键盘",
"kvm_text31":"重置 HID",
"kvm_text32":"宏",
"kvm_text33":"录制或重放 HID/ATX/GPIO 操作<br>",
"kvm_text34":"出于安全原因,录制的脚本默认不会保存到 One-KVM 主机上",
"kvm_text35":"录制",
"kvm_text36":"停止",
"kvm_text37":"重放",
"kvm_text38":"清除",
"kvm_text39":"脚本时间:",
"kvm_text40":"脚本事件:",
"kvm_text41":"包括延迟",
"kvm_text42":"上传脚本文件",
"kvm_text43":"下载脚本文件",
"kvm_text44":"文本",
"kvm_text45":"以按键序列方式粘贴文本<br>",
"kvm_text46":"请注意 One-KVM 无法自动切换被控机键盘布局",
"kvm_text47":"&bull; 粘贴",
"kvm_text48":"使用的键盘布局",
"kvm_text49":"文本识别<br>",
"kvm_text50":"文本识别仅在 One-KVM 主机上本地运行",
"kvm_text51":"&bull; 框选识别区域",
"kvm_text52":"进行",
"kvm_text53":"文本识别",
"kvm_text54":"按下 <b>Enter</b> 确认所选区域并将识别结果复制到粘贴板",
"kvm_text55":"按下 <b>Esc</b> 取消框选",
"kvm_text56":"快捷键",
"kvm_text57":"键盘快捷键<br>",
"kvm_text58":"也可以查看 <i>系统 &rarr; 显示键盘<i>",
"kvm_text59":"帮助",
"kvm_text60":"驱动器",
"kvm_text61":"虚拟存储驱动器",
"kvm_text62":"文件:",
"kvm_text63":"驱动<a target=\"_blank\" href=\"https://docs.pikvm.org/msd\">模式</a>:",
"kvm_text64":"选择要上传的文件",
"kvm_text65":"上传",
"kvm_text66":"中止",
"kvm_text68":"指定本地文件:",
"kvm_text69":"<b>或者</b>粘贴 URL:",
"kvm_text70":"上传分区:",
"kvm_text71":"注意:",
"kvm_text72":"&bull; 在上传完成之前,不要关闭浏览器页面。",
"kvm_text73":"&bull; 要加快上传速度,请关闭视频流窗口。",
"kvm_text74":"新镜像:",
"kvm_text75":"上传大小:",
"kvm_text76":"连接 MSD 到主机",
"kvm_text77":"断开连接",
"kvm_text78":"重置",
"kvm_text79":"视频录制<br>",
"kvm_text80":"使用浏览器 API 录制视频,结束录制后视频文件会自动下载",
"kvm_text81":"开始录制",
"kvm_text82":"结束录制",
"kvm_text83":"网页界面设置",
"kvm_text84":"文件显示:",
"kvm_text85":"快速文件互传:",
"kvm_text86":"&bull; 切换互传文件上传文件,打包生成镜像文件,挂载 NormalFiles 镜像",
"kvm_text87":"&bull; 断开 MSD 连接,选择从镜像文件解压,切换互传文件下载所需文件",
"kvm_text88":"从互传文件打包镜像文件",
"kvm_text89":"从镜像文件解压互传文件",
"kvm_text90":"镜像文件",
"kvm_text91":"互传文件",
"atx-ask-switch":"点击二次确认",
"hid-recorder-loop-switch":"无限循环重放",
"msd-rw-switch":"读写",
"hid-sysrq-ask-switch":"魔法键二次确认",
"hid-keyboard-swap-cc-switch":"交换左 Ctrl 键和大写字母键",
"hid-mouse-squash-switch":"减少相对移动",
"hid-mouse-cumulative-scrolling-switch":"累加滚动",
"hid-mouse-dot-switch":"显示蓝色圆点",
"hid-connect-switch":"连接 HID 到主机",
"hid-jiggler-switch":"<a href=\"https://docs.pikvm.org/mouse_jiggler\" target=\"_blank\">鼠标抖动器</a>",
"hid-mute-switch":"屏蔽 HID 输入事件",
"page-close-ask-switch":"页面关闭二次确认",
"hid-pak-ask-switch":"粘贴二次确认",
"hid-pak-secure-switch":"隐藏输入文本",
"meta":"元数据",
"hardware":"硬件",
"version":"版本",
"thanks":"鸣谢",
"stream-message-no-webrtc":"当前浏览器不支持 WebRTC",
"stream-message-no-h264":"当前浏览器不支持 H.264",
"msd-message-offline":"虚拟存储驱动器已离线",
"msd-message-image-broken":"当前镜像已损坏!",
"msd-message-too-big-for-cdrom":"当前 CD-ROM 格式镜像大小超出限制!",
"msd-message-out-of-storage":"当前镜像大小超出存储空间",
"msd-message-rw-enabled":"读写模式已启用",
"msd-message-downloads":"正在从 One-KVM 下载镜像",
"msd-message-another-user-uploads":"另一个用户正在上传镜像",
"page-full-tab-stream-switch":"自动全屏视频窗口"
}

View File

@@ -0,0 +1,159 @@
/*****************************************************************************
# #
# 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";
export var browser = new function() {
// https://stackoverflow.com/questions/9847580/how-to-detect-safari-chrome-ie-firefox-and-opera-browser/9851769
// https://github.com/fingerprintjs/fingerprintjs/discussions/641
// Opera 8.0+
let is_opera = (
(!!window.opr && !!opr.addons) // eslint-disable-line no-undef
|| !!window.opera
|| (navigator.userAgent.indexOf(" OPR/") >= 0)
);
// Firefox 1.0+
let is_firefox = (typeof mozInnerScreenX !== "undefined");
// Safari 3.0+ "[object HTMLElementConstructor]"
let is_safari = (function() {
if (/constructor/i.test(String(window["HTMLElement"]))) {
return true;
}
let push = null;
try {
push = window.top["safari"].pushNotification;
} catch {
try {
push = window["safari"].pushNotification;
} catch {
return false;
}
}
return String(push) === "[object SafariRemoteNotification]";
})();
// Chrome 1+
let is_chrome = !!window.chrome;
// Blink engine detection
let is_blink = ((is_chrome || is_opera) && !!window.CSS);
// Any browser on Mac
let is_mac = ((
window.navigator.oscpu
|| window.navigator.platform
|| window.navigator.appVersion
|| "Unknown"
).indexOf("Mac") !== -1);
// Any Windows
let is_win = (navigator && !!(/win/i).exec(navigator.platform));
// iOS browsers
// https://stackoverflow.com/questions/9038625/detect-if-device-is-ios
// https://github.com/lancedikson/bowser/issues/329
let is_ios = (!!navigator.platform && (
/iPad|iPhone|iPod/.test(navigator.platform)
|| (navigator.platform === "MacIntel" && navigator.maxTouchPoints > 1 && !window["MSStream"])
));
let is_android = /android/i.test(navigator.userAgent);
let flags = {
"is_opera": is_opera,
"is_firefox": is_firefox,
"is_safari": is_safari,
"is_chrome": is_chrome,
"is_blink": is_blink,
"is_mac": is_mac,
"is_win": is_win,
"is_ios": is_ios,
"is_android": is_android,
"is_mobile": (is_ios || is_android),
};
console.log("===== BB flags:", flags);
return flags;
};
export function checkBrowser(desktop_css, mobile_css) {
if (
!window.navigator
|| window.navigator.userAgent.indexOf("MSIE ") > 0
|| window.navigator.userAgent.indexOf("Trident/") > 0
|| window.navigator.userAgent.indexOf("Edge/") > 0
) {
let el_modal = document.createElement("div");
el_modal.className = "modal";
el_modal.style.visibility = "visible";
el_modal.innerHTML = `
<div class="modal-window">
<div class="modal-content">
Hello. You are using an incompatible or legacy browser.<br>
Please use one of the following browsers:
<hr>
<ul>
<li><a target="_blank" href="https://google.com/chrome">Google Chrome</a> <sup><i>recommended</i></sup></li>
<li><a target="_blank" href="https://chromium.org/Home">Chromium</a> <sup><i>recommended</i></sup></li>
<li><a target="_blank" href="https://mozilla.org/firefox">Mozilla Firefox</a></li>
<li><a target="_blank" href="https://apple.com/safari">Apple Safari</a></li>
<li><a target="_blank" href="https://opera.com">Opera</a></li>
<li><a target="_blank" href="https://vivaldi.com">Vivaldi</a></li>
</ul>
</div>
</div>
`;
document.body.appendChild(el_modal);
return false;
} else {
let force_desktop = (new URL(window.location.href)).searchParams.get("force_desktop");
let force_mobile = (new URL(window.location.href)).searchParams.get("force_mobile");
if ((force_desktop || !browser.is_mobile) && !force_mobile) {
__addCssLink("/share/css/x-desktop.css");
if (desktop_css) {
__addCssLink(desktop_css);
}
} else {
__addCssLink("/share/css/x-mobile.css");
if (mobile_css) {
__addCssLink(mobile_css);
}
}
return true;
}
}
function __addCssLink(path) {
console.log("===== Adding CSS:", path);
let el_head = document.getElementsByTagName("head")[0];
let el_link = document.createElement("link");
el_link.rel = "stylesheet";
el_link.type = "text/css";
el_link.href = path;
el_link.media = "all";
el_head.appendChild(el_link);
}

View File

@@ -0,0 +1,61 @@
function setCookie (name, value)
{
var expdate = new Date();
expdate.setTime(expdate.getTime() + 30 * 24 * 60 * 60 * 1000);
document.cookie = name + "=" + value + "; expires=" + expdate.toGMTString() + "; path=/" + ";SameSite=Lax";
}
function getCookie(name)
{
if (document.cookie.length > 0)
{
start = document.cookie.indexOf(name + "=")
if (start != -1)
{
start = start + name.length + 1
end = document.cookie.indexOf(";", start)
if (end == -1) end = document.cookie.length
return unescape(document.cookie.substring(start, end))
}
}
return ""
}
var i18nLanguage = "zh";
$(document).ready(function() {
if (getCookie('userLanguage')) {
i18nLanguage = getCookie('userLanguage');
if (i18nLanguage == "zh") {
no = 0;
}else if (i18nLanguage == "en") {
no = 1;
}
$("#selectLanguage").each(function(){
$(this).find("option").eq(no).prop("selected",true)
});
}
$("[i18n]").i18n({
defaultLang: i18nLanguage,
filePath: "/share/i18n/",
filePrefix: "i18n_",
fileSuffix: "",
forever: true,
callback: function() {
}
});
$("#selectLanguage").change(function() {
var selectOptionId = $(this).children("option:selected").attr("id");
console.log(selectOptionId);
$("[i18n]").i18n({
defaultLang: selectOptionId,
filePath: "/share/i18n/"
});
setCookie('userLanguage', selectOptionId)
});
});

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
(function($){$.fn.extend({i18n:function(options){var defaults={lang:"",defaultLang:"",filePath:"/i18n/",filePrefix:"i18n_",fileSuffix:"",forever:true,callback:function(){}};function getCookie(name){var arr=document.cookie.split('; ');for(var i=0;i<arr.length;i++){var arr1=arr[i].split('=');if(arr1[0]==name){return arr1[1]}}return''};function setCookie(name,value,myDay){var oDate=new Date();oDate.setDate(oDate.getDate()+myDay);document.cookie=name+'='+value+'; expires='+oDate};var options=$.extend(defaults,options);if(getCookie('i18n_lang')!=""&&getCookie('i18n_lang')!="undefined"&&getCookie('i18n_lang')!=null){defaults.defaultLang=getCookie('i18n_lang')}else if(options.lang==""&&defaults.defaultLang==""){throw"defaultLang must not be null !"};if(options.lang!=null&&options.lang!=""){if(options.forever){setCookie('i18n_lang',options.lang)}else{$.removeCookie("i18n_lang")}}else{options.lang=defaults.defaultLang};var i=this;$.getJSON(options.filePath+options.filePrefix+options.lang+options.fileSuffix+".json",function(data){var i18nLang={};if(data!=null){i18nLang=data}$(i).each(function(i){var i18nOnly=$(this).attr("i18n-only");if($(this).val()!=null&&$(this).val()!=""){if(i18nOnly==null||i18nOnly==undefined||i18nOnly==""||i18nOnly=="value"){$(this).val(i18nLang[$(this).attr("i18n")])}}if($(this).html()!=null&&$(this).html()!=""){if(i18nOnly==null||i18nOnly==undefined||i18nOnly==""||i18nOnly=="html"){$(this).html(i18nLang[$(this).attr("i18n")])}}if($(this).attr('placeholder')!=null&&$(this).attr('placeholder')!=""){if(i18nOnly==null||i18nOnly==undefined||i18nOnly==""||i18nOnly=="placeholder"){$(this).attr('placeholder',i18nLang[$(this).attr("i18n")])}}});options.callback()})}})})(jQuery);

View File

@@ -0,0 +1,118 @@
/*****************************************************************************
# #
# 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 {checkBrowser} from "../bb.js";
import {wm, initWindowManager} from "../wm.js";
export function main() {
initWindowManager();
if (checkBrowser(null, null)) {
__loadKvmdInfo();
}
}
function __loadKvmdInfo() {
tools.httpGet("/api/info", {"fields": "auth,meta,extras"}, function(http) {
if (http.status === 200) {
let info = JSON.parse(http.responseText).result;
let apps = [];
if (info.extras === null) {
wm.error("Not all applications in the menu can be displayed due an error.<br>See KVMD logs for details.");
} else {
apps = Object.values(info.extras).sort(function(a, b) {
if (a.place < b.place) {
return -1;
} else if (a.place > b.place) {
return 1;
} else {
return 0;
}
});
}
$("apps-box").innerHTML = "<ul id=\"apps\"></ul>";
// Don't use this option, it may be removed in any time
let hide_kvm_button = (
(info.meta !== null && info.meta.web && info.meta.web.hide_kvm_button)
|| tools.config.getBool("index--hide-kvm-button", false)
);
if (!hide_kvm_button) {
$("apps").innerHTML += __makeApp(null, "kvm", "share/svg/kvm.svg", "KVM");
}
for (let app of apps) {
if (app.place >= 0 && (app.enabled || app.started)) {
$("apps").innerHTML += __makeApp(null, app.path, app.icon, app.name);
}
}
if (info.auth.enabled) {
$("apps").innerHTML += __makeApp("logout-button", "#", "share/svg/logout.svg", "Logout");
tools.el.setOnClick($("logout-button"), __logout);
}
if (info.meta !== null && info.meta.server && info.meta.server.host) {
$("kvmd-meta-server-host").innerHTML = info.meta.server.host;
document.title = `PiKVM Index: ${info.meta.server.host}`;
} else {
$("kvmd-meta-server-host").innerHTML = "";
document.title = "PiKVM Index";
}
} else if (http.status === 401 || http.status === 403) {
document.location.href = "/login";
} else {
setTimeout(__loadKvmdInfo, 1000);
}
});
}
function __makeApp(id, path, icon, name) {
return `<li>
<div ${id ? "id=\"" + id + "\"" : ""} class="app">
<a href="${path}">
<div>
<img class="svg-gray" src="${icon}">
${tools.escape(name)}
</div>
</a>
</div>
</li>`;
}
function __logout() {
tools.httpPost("/api/auth/logout", null, function(http) {
if (http.status === 200 || http.status === 401 || http.status === 403) {
document.location.href = "/login";
} else {
wm.error("Logout error", http.responseText);
}
});
}

View File

@@ -0,0 +1,60 @@
/*****************************************************************************
# #
# 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 main() {
__loadKvmdInfo();
}
function __loadKvmdInfo() {
tools.httpGet("/api/info", null, function(http) {
if (http.status === 200) {
let ipmi_port = JSON.parse(http.responseText).result.extras.ipmi.port;
let make_item = (comment, ipmi, api) => `
<span class="code-comment"># ${comment}:<br>$</span>
ipmitool -I lanplus -U admin -P admin -H ${window.location.hostname} -p ${ipmi_port} ${ipmi}<br>
<span class="code-comment">$</span> curl -XPOST -HX-KVMD-User:admin -HX-KVMD-Passwd:admin -k \\<br>
&nbsp;&nbsp;&nbsp;&nbsp;${window.location.protocol}//${window.location.host}/api/atx${api}<br>
`;
$("ipmi-text").innerHTML = `
${make_item("Power on the server if it's off", "power on", "/power?action=on")}
<br>
${make_item("Soft power off the server if it's on", "power soft", "/power?action=off")}
<br>
${make_item("Hard power off the server if it's on", "power off", "/power?action=off_hard")}
<br>
${make_item("Hard reset the server if it's on", "power reset", "/power?action=reset_hard")}
<br>
${make_item("Check the power status", "power status", "")}
`;
} else if (http.status === 401 || http.status === 403) {
document.location.href = "/login";
} else {
setTimeout(__loadKvmdInfo, 1000);
}
});
}

View File

@@ -0,0 +1,251 @@
/*****************************************************************************
# #
# 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 Keypad(__keys_parent, __sendKey, __apply_fixes) {
var self = this;
/************************************************************************/
var __merged = {};
var __keys = {};
var __modifiers = {};
var __fix_mac_cmd = false;
var __fix_win_altgr = false;
var __altgr_ctrl_timer = null;
var __init__ = function() {
if (__apply_fixes) {
__fix_mac_cmd = tools.browser.is_mac;
if (__fix_mac_cmd) {
tools.info(`Keymap at ${__keys_parent}: enabled Fix-Mac-CMD`);
}
__fix_win_altgr = tools.browser.is_win;
if (__fix_win_altgr) {
tools.info(`Keymap at ${__keys_parent}: enabled Fix-Win-AltGr`);
}
}
for (let el_key of $$$(`${__keys_parent} div.key`)) {
let code = el_key.getAttribute("data-code");
tools.setDefault(__keys, code, []);
__keys[code].push(el_key);
tools.setDefault(__merged, code, []);
__merged[code].push(el_key);
tools.el.setOnDown(el_key, () => __clickHandler(el_key, true));
tools.el.setOnUp(el_key, () => __clickHandler(el_key, false));
el_key.onmouseout = function() {
if (__isPressed(el_key)) {
__clickHandler(el_key, false);
}
};
}
for (let el_key of $$$(`${__keys_parent} div.modifier`)) {
let code = el_key.getAttribute("data-code");
tools.setDefault(__modifiers, code, []);
__modifiers[code].push(el_key);
tools.setDefault(__merged, code, []);
__merged[code].push(el_key);
tools.el.setOnDown(el_key, () => __toggleModifierHandler(el_key));
}
};
/************************************************************************/
self.releaseAll = function() {
for (let dict of [__keys, __modifiers]) {
for (let code in dict) {
if (__isActive(dict[code][0])) {
self.emitByCode(code, false);
}
}
}
};
self.emitByKeyEvent = function(event, state) {
if (event.repeat) {
return;
}
let code = event.code;
if (__apply_fixes) {
// https://github.com/pikvm/pikvm/issues/819
if (code == "IntlBackslash" && ["`", "~"].includes(event.key)) {
code = "Backquote";
} else if (code == "Backquote" && ["§", "±"].includes(event.key)) {
code = "IntlBackslash";
}
}
self.emitByCode(code, state);
};
self.emitByCode = function(code, state, apply_fixes=true) {
if (code in __merged) {
if (__fix_win_altgr && apply_fixes) {
if (!__fixWinAltgr(code, state)) {
return;
}
}
if (__fix_mac_cmd && apply_fixes) {
__fixMacCmd(code, state);
}
__commonHandler(__merged[code][0], state, false);
__unholdModifiers();
}
};
var __fixMacCmd = function(code, state) {
if ((code == "MetaLeft" || code == "MetaRight") && !state) {
for (code in __keys) {
if (__isActive(__keys[code][0])) {
self.emitByCode(code, false, false);
}
}
}
};
var __fixWinAltgr = function(code, state) {
// https://github.com/pikvm/pikvm/issues/375
// https://github.com/novnc/noVNC/blob/84f102d6/core/input/keyboard.js
if (state) {
if (__altgr_ctrl_timer) {
clearTimeout(__altgr_ctrl_timer);
__altgr_ctrl_timer = null;
if (code !== "AltRight") {
self.emitByCode("ControlLeft", true, false);
}
}
if (code === "ControlLeft" && !__isActive(__modifiers["ControlLeft"][0])) {
__altgr_ctrl_timer = setTimeout(function() {
__altgr_ctrl_timer = null;
self.emitByCode("ControlLeft", true, false);
}, 50);
return false; // Stop handling
}
} else {
if (__altgr_ctrl_timer) {
clearTimeout(__altgr_ctrl_timer);
__altgr_ctrl_timer = null;
self.emitByCode("ControlLeft", true, false);
}
}
return true; // Continue handling
};
var __clickHandler = function(el_key, state) {
__commonHandler(el_key, state, false);
__unholdModifiers();
};
var __toggleModifierHandler = function(el_key) {
__commonHandler(el_key, !__isActive(el_key), true);
};
var __commonHandler = function(el_key, state, hold) {
if (state && !__isActive(el_key)) {
__deactivate(el_key);
__activate(el_key, (hold ? "holded" : "pressed"));
__process(el_key, true);
} else {
__deactivate(el_key);
__process(el_key, false);
}
};
var __unholdModifiers = function() {
for (let code in __modifiers) {
let el_key = __modifiers[code][0];
if (__isHolded(el_key)) {
__deactivate(el_key);
__process(el_key, false);
}
}
};
var __isPressed = function(el_key) {
let is_pressed = false;
let el_keys = __resolveKeys(el_key);
for (el_key of el_keys) {
is_pressed = (is_pressed || el_key.classList.contains("pressed"));
}
return is_pressed;
};
var __isHolded = function(el_key) {
let is_holded = false;
let el_keys = __resolveKeys(el_key);
for (el_key of el_keys) {
is_holded = (is_holded || el_key.classList.contains("holded"));
}
return is_holded;
};
var __isActive = function(el_key) {
let is_active = false;
let el_keys = __resolveKeys(el_key);
for (el_key of el_keys) {
is_active = (is_active || el_key.classList.contains("pressed") || el_key.classList.contains("holded"));
}
return is_active;
};
var __activate = function(el_key, cls) {
let el_keys = __resolveKeys(el_key);
for (el_key of el_keys) {
el_key.classList.add(cls);
}
};
var __deactivate = function(el_key) {
let el_keys = __resolveKeys(el_key);
for (el_key of el_keys) {
el_key.classList.remove("pressed");
el_key.classList.remove("holded");
}
};
var __resolveKeys = function(el_key) {
let code = el_key.getAttribute("data-code");
return __merged[code];
};
var __process = function(el_key, state) {
let code = el_key.getAttribute("data-code");
__sendKey(code, state);
};
__init__();
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,116 @@
/*****************************************************************************
# #
# 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 Atx(__recorder) {
var self = this;
/************************************************************************/
var __state = null;
var __init__ = function() {
$("atx-power-led").title = "Power Led";
$("atx-hdd-led").title = "Disk Activity Led";
tools.storage.bindSimpleSwitch($("atx-ask-switch"), "atx.ask", true);
tools.el.setOnClick($("atx-power-button"), () => __clickAtx("power"));
tools.el.setOnClick($("atx-power-button-long"), () => __clickAtx("power_long"));
tools.el.setOnClick($("atx-reset-button"), () => __clickAtx("reset"));
};
/************************************************************************/
self.setState = function(state) {
if (state) {
if (!__state) {
__state = {"leds": {}};
}
if (state.enabled !== undefined) {
__state.enabled = state.enabled;
tools.feature.setEnabled($("atx-dropdown"), __state.enabled);
}
if (__state.enabled !== undefined) {
if (state.busy !== undefined) {
__state.busy = state.busy;
__updateButtons(!__state.busy);
}
if (state.leds !== undefined) {
__state.leds = state.leds;
}
if (state.busy !== undefined || state.leds !== undefined) {
__updateLeds(__state.leds.power, __state.leds.hdd, __state.busy);
}
}
} else {
__state = null;
__updateLeds(false, false, false);
__updateButtons(false);
}
};
var __updateLeds = function(power, hdd, busy) {
$("atx-power-led").className = (busy ? "led-yellow" : (power ? "led-green" : "led-gray"));
$("atx-hdd-led").className = (hdd ? "led-red" : "led-gray");
};
var __updateButtons = function(enabled) {
for (let id of ["atx-power-button", "atx-power-button-long", "atx-reset-button"]) {
tools.el.setEnabled($(id), enabled);
}
};
var __clickAtx = function(button) {
let click_button = function() {
tools.httpPost("/api/atx/click", {"button": button}, function(http) {
if (http.status === 409) {
wm.error("Performing another ATX operation for other client.<br>Please try again later.");
} else if (http.status !== 200) {
wm.error("Click error", http.responseText);
}
});
__recorder.recordAtxButtonEvent(button);
};
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.
`).then(function(ok) {
if (ok) {
click_button();
}
});
} else {
click_button();
}
};
__init__();
}

View File

@@ -0,0 +1,246 @@
/*****************************************************************************
# #
# 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 Gpio(__recorder) {
var self = this;
/************************************************************************/
var __has_model = false;
/************************************************************************/
self.setState = function(state) {
if (state) {
if (state.model !== undefined) {
__has_model = true;
__updateModel(state.model);
}
if (__has_model && state.state !== undefined) {
if (state.state.inputs !== undefined) {
__updateInputs(state.state.inputs);
}
if (state.state.outputs !== undefined) {
__updateOutputs(state.state.outputs);
}
}
} else {
__has_model = false;
for (let el of $$("__gpio-led")) {
__setLedState(el, false);
}
for (let selector of ["__gpio-switch", "__gpio-button"]) {
for (let el of $$(selector)) {
tools.el.setEnabled(el, false);
}
}
}
};
var __updateInputs = function(inputs) {
for (let ch in inputs) {
for (let el of $$(`__gpio-led-${ch}`)) {
__setLedState(el, inputs[ch].state);
}
}
};
var __updateOutputs = function(outputs) {
for (let ch in outputs) {
for (let type of ["switch", "button"]) {
for (let el of $$(`__gpio-${type}-${ch}`)) {
tools.el.setEnabled(el, (outputs[ch].online && !outputs[ch].busy));
}
}
for (let el of $$(`__gpio-switch-${ch}`)) {
el.checked = outputs[ch].state;
}
}
};
var __updateModel = function(model) {
tools.feature.setEnabled($("gpio-dropdown"), model.view.table.length);
if (model.view.table.length) {
let title = [];
let last_is_label = false;
for (let item of model.view.header.title) {
if (last_is_label && item.type === "label") {
title.push("<span></span>");
}
last_is_label = (item.type === "label");
title.push(__createItem(item));
}
$("gpio-menu-button").innerHTML = title.join(" ");
}
let html = "<table class=\"kv\">";
for (let row of model.view.table) {
if (row === null) {
html += "</table><hr><table class=\"kv\">";
} else {
html += "<tr>";
for (let item of row) {
if (item.type === "output") {
item.scheme = model.scheme.outputs[item.channel];
}
html += `<td align="center">${__createItem(item)}</td>`;
}
html += "</tr>";
}
}
html += "</table>";
$("gpio-menu").innerHTML = html;
for (let ch in model.scheme.outputs) {
for (let el of $$(`__gpio-switch-${ch}`)) {
tools.el.setOnClick(el, tools.partial(__switchChannel, el));
}
for (let el of $$(`__gpio-button-${ch}`)) {
tools.el.setOnClick(el, tools.partial(__pulseChannel, el));
}
}
tools.feature.setEnabled($("v3-usb-breaker"), ("__v3_usb_breaker__" in model.scheme.outputs));
tools.feature.setEnabled($("v4-locator"), ("__v4_locator__" in model.scheme.outputs));
tools.feature.setEnabled($("system-tool-wol"), ("__wol__" in model.scheme.outputs));
};
var __createItem = function(item) {
if (item.type === "label") {
return item.text;
} else if (item.type === "input") {
return `
<img
class="__gpio-led __gpio-led-${item.channel} inline-lamp-big led-gray"
src="/share/svg/led-circle.svg"
data-color="${item.color}"
/>
`;
} else if (item.type === "output") {
let controls = [];
let confirm = (item.confirm ? "Are you sure you want to perform this action?" : "");
if (item.scheme["switch"]) {
let id = tools.makeId();
controls.push(`
<td><div class="switch-box">
<input
disabled
type="checkbox"
id="__gpio-switch-${id}"
class="__gpio-switch __gpio-switch-${item.channel}"
data-channel="${item.channel}"
data-confirm="${confirm}"
/>
<label for="__gpio-switch-${id}">
<span class="switch-inner"></span>
<span class="switch"></span>
</label>
</div></td>
`);
}
if (item.scheme.pulse.delay) {
controls.push(`
<td><button
disabled
class="__gpio-button __gpio-button-${item.channel}"
${item.hide ? "data-force-hide-menu" : ""}
data-channel="${item.channel}"
data-confirm="${confirm}"
>
${(item.hide ? "&bull; " : "") + item.text}
</button></td>
`);
}
return `<table><tr>${controls.join("<td>&nbsp;&nbsp;&nbsp;</td>")}</tr></table>`;
} else {
return "";
}
};
var __setLedState = function(el, on) {
let color = el.getAttribute("data-color");
if (on) {
el.classList.add(`led-${color}`);
el.classList.remove("led-gray");
} else {
el.classList.add("led-gray");
el.classList.remove(`led-${color}`);
}
};
var __switchChannel = function(el) {
let ch = el.getAttribute("data-channel");
let confirm = el.getAttribute("data-confirm");
let to = (el.checked ? "1" : "0");
if (to === "0" && el.hasAttribute("data-confirm-off")) {
confirm = el.getAttribute("data-confirm-off");
}
let act = () => {
__sendPost("/api/gpio/switch", {"channel": ch, "state": to});
__recorder.recordGpioSwitchEvent(ch, to);
};
if (confirm) {
wm.confirm(tools.escape(confirm)).then(function(ok) {
if (ok) {
act();
}
});
} else {
act();
}
};
var __pulseChannel = function(el) {
let ch = el.getAttribute("data-channel");
let confirm = el.getAttribute("data-confirm");
let act = () => {
__sendPost("/api/gpio/pulse", {"channel": ch});
__recorder.recordGpioPulseEvent(ch);
};
if (confirm) {
wm.confirm(tools.escape(confirm)).then(function(ok) {
if (ok) {
act();
}
});
} else {
act();
}
};
var __sendPost = function(url, params) {
tools.httpPost(url, params, function(http) {
if (http.status === 409) {
wm.error("Performing another operation for this GPIO channel.<br>Please try again later.");
} else if (http.status !== 200) {
wm.error("GPIO error", http.responseText);
}
});
};
}

View File

@@ -0,0 +1,316 @@
/*****************************************************************************
# #
# 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";
import {Keyboard} from "./keyboard.js";
import {Mouse} from "./mouse.js";
export function Hid(__getGeometry, __recorder) {
var self = this;
/************************************************************************/
var __state = null;
var __keyboard = null;
var __mouse = null;
var __init__ = function() {
__keyboard = new Keyboard(__recorder.recordWsEvent);
__mouse = new Mouse(__getGeometry, __recorder.recordWsEvent);
let hidden_attr = null;
let visibility_change_attr = null;
if (typeof document.hidden !== "undefined") {
hidden_attr = "hidden";
visibility_change_attr = "visibilitychange";
} else if (typeof document.webkitHidden !== "undefined") {
hidden_attr = "webkitHidden";
visibility_change_attr = "webkitvisibilitychange";
} else if (typeof document.mozHidden !== "undefined") {
hidden_attr = "mozHidden";
visibility_change_attr = "mozvisibilitychange";
}
if (visibility_change_attr) {
document.addEventListener(
visibility_change_attr,
function() {
if (document[hidden_attr]) {
__releaseAll();
}
},
false
);
}
window.addEventListener("pagehide", __releaseAll);
window.addEventListener("blur", __releaseAll);
tools.el.setOnClick($("hid-connect-switch"), __clickConnectSwitch);
tools.el.setOnClick($("hid-reset-button"), __clickResetButton);
for (let el_shortcut of $$$("[data-shortcut]")) {
tools.el.setOnClick(el_shortcut, function() {
let ask = false;
let confirm_id = el_shortcut.getAttribute("data-shortcut-confirm");
if (confirm_id) {
ask = $(confirm_id).checked;
}
let codes = el_shortcut.getAttribute("data-shortcut").split(" ");
if (ask) {
wm.confirm("Do you want to press this hotkey?", codes.join(" + ")).then(function(ok) {
if (ok) {
__emitShortcut(codes);
}
});
} else {
__emitShortcut(codes);
}
});
}
tools.storage.bindSimpleSwitch($("hid-sysrq-ask-switch"), "hid.sysrq.ask", true);
tools.el.setOnClick($("hid-jiggler-switch"), __clickJigglerSwitch);
};
/************************************************************************/
self.setSocket = function(ws) {
if (!ws) {
self.setState(null);
}
__keyboard.setSocket(ws);
__mouse.setSocket(ws);
};
self.setState = function(state) {
if (state) {
if (!__state) {
__state = {"keyboard": {}, "mouse": {}};
}
if (state.enabled !== undefined) {
__state.enabled = state.enabled; // Currently unused, always true
}
if (__state.enabled !== undefined) {
for (let key of ["online", "busy", "connected", "jiggler"]) {
if (state[key] !== undefined) {
__state[key] = state[key];
}
}
for (let hid of ["keyboard", "mouse"]) {
if (state[hid] === undefined) {
state[hid] = {}; // Add some stubs for processing
}
for (let key of ["online", "outputs", (hid === "keyboard" ? "leds" : "absolute")]) {
__state[hid][key] = state[hid][key];
}
}
if (state.connected !== undefined) {
tools.feature.setEnabled($("hid-connect"), (__state.connected !== null));
$("hid-connect-switch").checked = !!__state.connected;
}
if (state.jiggler !== undefined) {
tools.feature.setEnabled($("hid-jiggler"), __state.jiggler.enabled);
$("hid-jiggler-switch").checked = __state.jiggler.active;
}
if (state.keyboard.outputs !== undefined) {
__updateKeyboardOutputs(__state.keyboard.outputs);
}
if (state.mouse.outputs !== undefined) {
__updateMouseOutputs(__state.mouse.outputs, __state.mouse.absolute); // Follows together
}
if (
state.keyboard.online !== undefined || state.keyboard.leds !== undefined
|| state.online !== undefined || state.busy !== undefined
) {
__keyboard.setState(__state.keyboard.online, __state.keyboard.leds, __state.online, __state.busy);
}
if (
state.mouse.online !== undefined || state.mouse.absolute !== undefined
|| state.online !== undefined || state.busy !== undefined
) {
__mouse.setState(__state.mouse.online, __state.mouse.absolute, __state.online, __state.busy);
}
if (state.online !== undefined || state.busy !== undefined) {
tools.radio.setEnabled("hid-outputs-keyboard-radio", (__state.online && !__state.busy));
tools.radio.setEnabled("hid-outputs-mouse-radio", (__state.online && !__state.busy));
tools.el.setEnabled($("hid-connect-switch"), (__state.online && !__state.busy));
}
}
} else {
__state = null;
tools.radio.setEnabled("hid-outputs-keyboard-radio", false);
tools.radio.setEnabled("hid-outputs-mouse-radio", false);
tools.el.setEnabled($("hid-connect-switch"), false);
tools.el.setEnabled($("hid-mouse-squash-switch"), false);
tools.el.setEnabled($("hid-mouse-sens-slider"), false);
}
tools.el.setEnabled($("hid-reset-button"), __state);
tools.el.setEnabled($("hid-jiggler-switch"), __state);
};
var __updateKeyboardOutputs = function(outputs) {
let avail = outputs.available;
if (avail.length > 0) {
let el = $("hid-outputs-keyboard-box");
let avail_json = JSON.stringify(avail);
if (el.__avail_json !== avail_json) {
let html = "";
for (let pair of [
["USB", "usb"],
["PS/2", "ps2"],
["Off", "disabled"],
]) {
if (avail.includes(pair[1])) {
html += tools.radio.makeItem("hid-outputs-keyboard-radio", pair[0], pair[1]);
}
}
el.innerHTML = html;
tools.radio.setOnClick("hid-outputs-keyboard-radio", () => __clickOutputsRadio("keyboard"));
el.__avail_json = avail_json;
}
tools.radio.setValue("hid-outputs-keyboard-radio", outputs.active);
}
tools.feature.setEnabled($("hid-outputs-keyboard"), (avail.length > 0));
};
var __updateMouseOutputs = function(outputs, absolute) {
let has_relative = null;
let has_relative_squash = null;
let avail = outputs.available;
if (avail.length > 0) {
let el = $("hid-outputs-mouse-box");
let avail_json = JSON.stringify(avail);
if (el.__avail_json !== avail_json) {
has_relative = false;
let html = "";
for (let pair of [
["Absolute", "usb", false],
["Abs-Win98", "usb_win98", false],
["Relative", "usb_rel", true],
["PS/2", "ps2", true],
["Off", "disabled", false],
]) {
if (avail.includes(pair[1])) {
html += tools.radio.makeItem("hid-outputs-mouse-radio", pair[0], pair[1]);
has_relative = (has_relative || pair[2]);
}
}
el.innerHTML = html;
tools.radio.setOnClick("hid-outputs-mouse-radio", () => __clickOutputsRadio("mouse"));
el.__avail_json = avail_json;
}
tools.radio.setValue("hid-outputs-mouse-radio", outputs.active);
has_relative_squash = (["usb_rel", "ps2"].includes(outputs.active));
} else {
has_relative = !absolute;
has_relative_squash = has_relative;
}
if (has_relative !== null) {
tools.feature.setEnabled($("hid-mouse-squash"), has_relative);
tools.feature.setEnabled($("hid-mouse-sens"), has_relative);
}
tools.feature.setEnabled($("hid-outputs-mouse"), (avail.length > 0));
tools.el.setEnabled($("hid-mouse-squash-switch"), has_relative_squash);
tools.el.setEnabled($("hid-mouse-sens-slider"), has_relative_squash);
};
var __releaseAll = function() {
__keyboard.releaseAll();
__mouse.releaseAll();
};
var __emitShortcut = function(codes) {
return new Promise(function(resolve) {
tools.debug("HID: emitting keys:", codes);
let raw_events = [];
[[codes, true], [codes.slice().reverse(), false]].forEach(function(op) {
let [op_codes, state] = op;
for (let code of op_codes) {
raw_events.push({"code": code, "state": state});
}
});
let index = 0;
let iterate = () => setTimeout(function() {
__keyboard.emit(raw_events[index].code, raw_events[index].state);
++index;
if (index < raw_events.length) {
iterate();
} else {
resolve(null);
}
}, 100);
iterate();
});
};
var __clickOutputsRadio = function(hid) {
let output = tools.radio.getValue(`hid-outputs-${hid}-radio`);
tools.httpPost("/api/hid/set_params", {[`${hid}_output`]: output}, function(http) {
if (http.status !== 200) {
wm.error("Can't configure HID", http.responseText);
}
});
};
var __clickJigglerSwitch = function() {
let enabled = $("hid-jiggler-switch").checked;
tools.httpPost("/api/hid/set_params", {"jiggler": enabled}, function(http) {
if (http.status !== 200) {
wm.error(`Can't ${enabled ? "enabled" : "disable"} mouse jiggler`, http.responseText);
}
});
};
var __clickConnectSwitch = function() {
let connected = $("hid-connect-switch").checked;
tools.httpPost("/api/hid/set_connected", {"connected": connected}, function(http) {
if (http.status !== 200) {
wm.error(`Can't ${connected ? "connect" : "disconnect"} HID`, http.responseText);
}
});
};
var __clickResetButton = function() {
wm.confirm("Are you sure you want to reset HID (keyboard & mouse)?").then(function(ok) {
if (ok) {
tools.httpPost("/api/hid/reset", null, function(http) {
if (http.status !== 200) {
wm.error("HID reset error", http.responseText);
}
});
}
});
};
__init__();
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,152 @@
/*****************************************************************************
# #
# 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/>. #
# #
*****************************************************************************/
import {tools, $, $$$} from "../tools.js";
import {Keypad} from "../keypad.js";
export function Keyboard(__recordWsEvent) {
var self = this;
/************************************************************************/
var __ws = null;
var __online = true;
var __keypad = null;
var __init__ = function() {
__keypad = new Keypad("div#keyboard-window", __sendKey, true);
$("hid-keyboard-led").title = "Keyboard free";
$("keyboard-window").onkeydown = (event) => __keyboardHandler(event, true);
$("keyboard-window").onkeyup = (event) => __keyboardHandler(event, false);
$("keyboard-window").onfocus = __updateOnlineLeds;
$("keyboard-window").onblur = __updateOnlineLeds;
$("stream-window").onkeydown = (event) => __keyboardHandler(event, true);
$("stream-window").onkeyup = (event) => __keyboardHandler(event, false);
$("stream-window").onfocus = __updateOnlineLeds;
$("stream-window").onblur = __updateOnlineLeds;
window.addEventListener("focusin", __updateOnlineLeds);
window.addEventListener("focusout", __updateOnlineLeds);
tools.storage.bindSimpleSwitch($("hid-keyboard-swap-cc-switch"), "hid.keyboard.swap_cc", false);
};
/************************************************************************/
self.setSocket = function(ws) {
if (ws !== __ws) {
self.releaseAll();
__ws = ws;
}
__updateOnlineLeds();
};
self.setState = function(online, leds, hid_online, hid_busy) {
if (!hid_online) {
__online = null;
} else {
__online = (online && !hid_busy);
}
__updateOnlineLeds();
for (let led of ["caps", "scroll", "num"]) {
for (let el of $$$(`.hid-keyboard-${led}-led`)) {
if (leds[led]) {
el.classList.add("led-green");
el.classList.remove("led-gray");
} else {
el.classList.add("led-gray");
el.classList.remove("led-green");
}
}
}
};
self.releaseAll = function() {
__keypad.releaseAll();
};
self.emit = function(code, state) {
__keypad.emitByCode(code, state);
};
var __updateOnlineLeds = function() {
let is_captured = (
$("stream-window").classList.contains("window-active")
|| $("keyboard-window").classList.contains("window-active")
);
let led = "led-gray";
let title = "Keyboard free";
if (__ws) {
if (__online === null) {
led = "led-red";
title = (is_captured ? "Keyboard captured, HID offline" : "Keyboard free, HID offline");
} else if (__online) {
if (is_captured) {
led = "led-green";
title = "Keyboard captured";
}
} else {
led = "led-yellow";
title = (is_captured ? "Keyboard captured, inactive/busy" : "Keyboard free, inactive/busy");
}
} else {
if (is_captured) {
title = "Keyboard captured, PiKVM offline";
}
}
$("hid-keyboard-led").className = led;
$("hid-keyboard-led").title = title;
};
var __keyboardHandler = function(event, state) {
event.preventDefault();
__keypad.emitByKeyEvent(event, state);
};
var __sendKey = function(code, state) {
tools.debug("Keyboard: key", (state ? "pressed:" : "released:"), code);
if ($("hid-keyboard-swap-cc-switch").checked) {
if (code === "ControlLeft") {
code = "CapsLock";
} else if (code === "CapsLock") {
code = "ControlLeft";
}
}
let event = {
"event_type": "key",
"event": {"key": code, "state": state},
};
if (__ws && !$("hid-mute-switch").checked) {
__ws.sendHidEvent(event);
}
__recordWsEvent(event);
};
__init__();
}

View File

@@ -0,0 +1,65 @@
/*****************************************************************************
# #
# 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 {checkBrowser} from "../bb.js";
import {wm, initWindowManager} from "../wm.js";
import {Session} from "./session.js";
export function main() {
if (checkBrowser(null, "/share/css/kvm/x-mobile.css")) {
tools.storage.bindSimpleSwitch($("page-close-ask-switch"), "page.close.ask", true, function(value) {
if (value) {
window.onbeforeunload = function(event) {
let text = "Are you sure you want to close PiKVM session?";
if (event) {
event.returnValue = text;
}
return text;
};
} else {
window.onbeforeunload = null;
}
});
initWindowManager();
tools.el.setOnClick($("open-log-button"), () => window.open("/api/log?seek=3600&follow=1", "_blank"));
tools.storage.bindSimpleSwitch(
$("page-full-tab-stream-switch"),
"page.full_tab_stream",
tools.config.getBool("kvm--full-tab-stream", false));
if ($("page-full-tab-stream-switch").checked) {
wm.setFullTabWindow($("stream-window"), true);
}
wm.showWindow($("stream-window"));
new Session();
}
}

View File

@@ -0,0 +1,369 @@
/*****************************************************************************
# #
# 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 {Keypad} from "../keypad.js";
export function Mouse(__getGeometry, __recordWsEvent) {
var self = this;
/************************************************************************/
var __ws = null;
var __online = true;
var __absolute = true;
var __keypad = null;
var __timer = null;
var __planned_pos = {"x": 0, "y": 0};
var __sent_pos = {"x": 0, "y": 0};
var __relative_deltas = [];
var __relative_touch_pos = null;
var __relative_sens = 1.0;
var __scroll_rate = 5;
var __scroll_delta = {"x": 0, "y": 0};
var __stream_hovered = false;
var __init__ = function() {
__keypad = new Keypad("div#stream-mouse-buttons", __sendButton, false);
$("hid-mouse-led").title = "Mouse free";
document.onpointerlockchange = __relativeCapturedHandler; // Only for relative
document.onpointerlockerror = __relativeCapturedHandler;
$("stream-box").onmouseenter = () => __streamHoveredHandler(true);
$("stream-box").onmouseleave = () => __streamHoveredHandler(false);
$("stream-box").onmousedown = (event) => __streamButtonHandler(event, true);
$("stream-box").onmouseup = (event) => __streamButtonHandler(event, false);
$("stream-box").oncontextmenu = (event) => event.preventDefault();
$("stream-box").onmousemove = __streamMoveHandler;
$("stream-box").onwheel = __streamScrollHandler;
$("stream-box").ontouchstart = (event) => __streamTouchStartHandler(event);
$("stream-box").ontouchmove = (event) => __streamTouchMoveHandler(event);
$("stream-box").ontouchend = (event) => __streamTouchEndHandler(event);
tools.storage.bindSimpleSwitch($("hid-mouse-squash-switch"), "hid.mouse.squash", true);
tools.slider.setParams($("hid-mouse-sens-slider"), 0.1, 1.9, 0.1, tools.storage.get("hid.mouse.sens", 1.0), __updateRelativeSens);
tools.slider.setParams($("hid-mouse-rate-slider"), 10, 100, 10, tools.storage.get("hid.mouse.rate", 10), __updateRate); // set __timer
tools.storage.bindSimpleSwitch($("hid-mouse-reverse-scrolling-switch"), "hid.mouse.reverse_scrolling", false);
tools.storage.bindSimpleSwitch($("hid-mouse-reverse-panning-switch"), "hid.mouse.reverse_panning", false);
let cumulative_scrolling = !(tools.browser.is_firefox && !tools.browser.is_mac);
tools.storage.bindSimpleSwitch($("hid-mouse-cumulative-scrolling-switch"), "hid.mouse.cumulative_scrolling", cumulative_scrolling);
tools.slider.setParams($("hid-mouse-scroll-slider"), 1, 25, 1, tools.storage.get("hid.mouse.scroll_rate", 5), __updateScrollRate);
tools.storage.bindSimpleSwitch($("hid-mouse-dot-switch"), "hid.mouse.dot", true, __updateOnlineLeds);
};
/************************************************************************/
self.setSocket = function(ws) {
__ws = ws;
if (!__absolute && __isRelativeCaptured()) {
document.exitPointerLock();
}
__updateOnlineLeds();
};
self.setState = function(online, absolute, hid_online, hid_busy) {
if (!hid_online) {
__online = null;
} else {
__online = (online && !hid_busy);
}
if (!__absolute && absolute && __isRelativeCaptured()) {
document.exitPointerLock();
}
if (__absolute && !absolute) {
__relative_deltas = [];
__relative_touch_pos = null;
}
__absolute = absolute;
__updateOnlineLeds();
};
self.releaseAll = function() {
__keypad.releaseAll();
};
var __updateRate = function(value) {
$("hid-mouse-rate-value").innerHTML = value + " ms";
tools.storage.set("hid.mouse.rate", value);
if (__timer) {
clearInterval(__timer);
}
__timer = setInterval(__sendPlannedMove, value);
};
var __updateScrollRate = function(value) {
$("hid-mouse-scroll-value").innerHTML = value;
tools.storage.set("hid.mouse.scroll_rate", value);
__scroll_rate = value;
};
var __updateRelativeSens = function(value) {
$("hid-mouse-sens-value").innerHTML = value.toFixed(1);
tools.storage.set("hid.mouse.sens", value);
__relative_sens = value;
};
var __streamHoveredHandler = function(hovered) {
if (__absolute) {
__stream_hovered = hovered;
__updateOnlineLeds();
}
};
var __updateOnlineLeds = function() {
let is_captured;
if (__absolute) {
is_captured = (__stream_hovered || tools.browser.is_mobile);
} else {
is_captured = __isRelativeCaptured();
}
let led = "led-gray";
let title = "Mouse free";
if (__ws) {
if (__online === null) {
led = "led-red";
title = (is_captured ? "Mouse captured, HID offline" : "Mouse free, HID offline");
} else if (__online) {
if (is_captured) {
led = "led-green";
title = "Mouse captured";
}
} else {
led = "led-yellow";
title = (is_captured ? "Mouse captured, inactive/busy" : "Mouse free, inactive/busy");
}
} else {
if (is_captured) {
title = "Mouse captured, PiKVM offline";
}
}
$("hid-mouse-led").className = led;
$("hid-mouse-led").title = title;
if (__absolute && is_captured) {
let dot = $("hid-mouse-dot-switch").checked;
$("stream-box").classList.toggle("stream-box-mouse-dot", (dot && __ws));
$("stream-box").classList.toggle("stream-box-mouse-none", (!dot && __ws));
} else {
$("stream-box").classList.toggle("stream-box-mouse-dot", false);
$("stream-box").classList.toggle("stream-box-mouse-none", false);
}
};
var __isRelativeCaptured = function() {
return (document.pointerLockElement === $("stream-box"));
};
var __relativeCapturedHandler = function() {
tools.info("Relative mouse", (__isRelativeCaptured() ? "captured" : "released"), "by pointer lock");
__updateOnlineLeds();
};
var __streamButtonHandler = function(event, state) {
// https://www.w3schools.com/jsref/event_button.asp
event.preventDefault();
if (__absolute || __isRelativeCaptured()) {
switch (event.button) {
case 0: __keypad.emitByCode("left", state); break;
case 2: __keypad.emitByCode("right", state); break;
case 1: __keypad.emitByCode("middle", state); break;
case 3: __keypad.emitByCode("up", state); break;
case 4: __keypad.emitByCode("down", state); break;
}
} else if (!__absolute && !__isRelativeCaptured() && !state) {
$("stream-box").requestPointerLock();
}
};
var __streamTouchStartHandler = function(event) {
event.preventDefault();
if (event.touches.length === 1) {
if (__absolute) {
__planned_pos = __getTouchPosition(event, 0);
__sendPlannedMove();
} else {
__relative_touch_pos = __getTouchPosition(event, 0);
}
}
};
var __streamTouchMoveHandler = function(event) {
event.preventDefault();
if (event.touches.length === 1) {
if (__absolute) {
__planned_pos = __getTouchPosition(event, 0);
} else if (__relative_touch_pos === null) {
__relative_touch_pos = __getTouchPosition(event, 0);
} else {
let pos = __getTouchPosition(event, 0);
__sendOrPlanRelativeMove({
"x": (pos.x - __relative_touch_pos.x),
"y": (pos.y - __relative_touch_pos.y),
});
__relative_touch_pos = pos;
}
}
};
var __streamTouchEndHandler = function(event) {
event.preventDefault();
__sendPlannedMove();
};
var __getTouchPosition = function(event, index) {
if (event.touches[index].target && event.touches[index].target.getBoundingClientRect) {
let rect = event.touches[index].target.getBoundingClientRect();
return {
"x": Math.round(event.touches[index].clientX - rect.left),
"y": Math.round(event.touches[index].clientY - rect.top),
};
}
return null;
};
var __streamMoveHandler = function(event) {
if (__absolute) {
let rect = event.target.getBoundingClientRect();
__planned_pos = {
"x": Math.max(Math.round(event.clientX - rect.left), 0),
"y": Math.max(Math.round(event.clientY - rect.top), 0),
};
} else if (__isRelativeCaptured()) {
__sendOrPlanRelativeMove({
"x": event.movementX,
"y": event.movementY,
});
}
};
var __streamScrollHandler = function(event) {
// https://learn.javascript.ru/mousewheel
// https://stackoverflow.com/a/24595588
event.preventDefault();
if (!__absolute && !__isRelativeCaptured()) {
return;
}
let delta = {"x": 0, "y": 0};
if ($("hid-mouse-cumulative-scrolling-switch").checked) {
let factor = (tools.browser.is_mac ? 5 : 1);
__scroll_delta.x += event.deltaX * factor; // Horizontal scrolling
if (Math.abs(__scroll_delta.x) >= 100) {
delta.x = __scroll_delta.x / Math.abs(__scroll_delta.x) * (-__scroll_rate);
__scroll_delta.x = 0;
}
__scroll_delta.y += event.deltaY * factor; // Vertical scrolling
if (Math.abs(__scroll_delta.y) >= 100) {
delta.y = __scroll_delta.y / Math.abs(__scroll_delta.y) * (-__scroll_rate);
__scroll_delta.y = 0;
}
} else {
if (event.deltaX !== 0) {
delta.x = event.deltaX / Math.abs(event.deltaX) * (-__scroll_rate);
}
if (event.deltaY !== 0) {
delta.y = event.deltaY / Math.abs(event.deltaY) * (-__scroll_rate);
}
}
__sendScroll(delta);
};
var __sendOrPlanRelativeMove = function(delta) {
delta = {
"x": Math.min(Math.max(-127, Math.floor(delta.x * __relative_sens)), 127),
"y": Math.min(Math.max(-127, Math.floor(delta.y * __relative_sens)), 127),
};
if (delta.x || delta.y) {
if ($("hid-mouse-squash-switch").checked) {
__relative_deltas.push(delta);
} else {
tools.debug("Mouse: relative:", delta);
__sendEvent("mouse_relative", {"delta": delta});
}
}
};
var __sendScroll = function(delta) {
if (delta.x || delta.y) {
if ($("hid-mouse-reverse-scrolling-switch").checked) {
delta.y *= -1;
}
if ($("hid-mouse-reverse-panning-switch").checked) {
delta.x *= -1;
}
tools.debug("Mouse: scrolled:", delta);
__sendEvent("mouse_wheel", {"delta": delta});
}
};
var __sendPlannedMove = function() {
if (__absolute) {
let pos = __planned_pos;
if (pos.x !== __sent_pos.x || pos.y !== __sent_pos.y) {
let geo = __getGeometry();
let to = {
"x": tools.remap(pos.x, geo.x, geo.width, -32768, 32767),
"y": tools.remap(pos.y, geo.y, geo.height, -32768, 32767),
};
tools.debug("Mouse: moved:", to);
__sendEvent("mouse_move", {"to": to});
__sent_pos = pos;
}
} else if (__relative_deltas.length) {
tools.debug("Mouse: relative:", __relative_deltas);
__sendEvent("mouse_relative", {"delta": __relative_deltas, "squash": true});
__relative_deltas = [];
}
};
var __sendButton = function(button, state) {
tools.debug("Mouse: button", (state ? "pressed:" : "released:"), button);
__sendPlannedMove();
__sendEvent("mouse_button", {"button": button, "state": state});
};
var __sendEvent = function(event_type, event) {
event = {"event_type": event_type, "event": event};
if (__ws && !$("hid-mute-switch").checked) {
__ws.sendHidEvent(event);
}
__recordWsEvent(event);
};
__init__();
}

View File

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

View File

@@ -0,0 +1,202 @@
/*****************************************************************************
# #
# 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 Ocr(__getGeometry) {
var self = this;
/************************************************************************/
var __enabled = null;
var __start_pos = null;
var __end_pos = null;
var __sel = null;
var __init__ = function() {
tools.el.setOnClick($("stream-ocr-button"), function() {
__resetSelection();
wm.showWindow($("stream-window"));
wm.showWindow($("stream-ocr-window"));
});
$("stream-ocr-lang-selector").addEventListener("change", function() {
tools.storage.set("stream.ocr.lang", $("stream-ocr-lang-selector").value);
});
$("stream-ocr-window").addEventListener("blur", __resetSelection);
$("stream-ocr-window").addEventListener("resize", __resetSelection);
$("stream-ocr-window").close_hook = __resetSelection;
$("stream-ocr-window").onkeyup = function(event) {
event.preventDefault();
if (event.code === "Enter") {
if (__sel) {
__recognizeSelection();
wm.closeWindow($("stream-ocr-window"));
}
} else if (event.code === "Escape") {
wm.closeWindow($("stream-ocr-window"));
}
};
$("stream-ocr-window").onmousedown = __startSelection;
$("stream-ocr-window").onmousemove = __changeSelection;
$("stream-ocr-window").onmouseup = __endSelection;
};
/************************************************************************/
self.setState = function(state) {
if (state) {
if (state.enabled !== undefined) {
__enabled = (state.enabled && !tools.browser.is_mobile);
tools.feature.setEnabled($("stream-ocr"), __enabled);
$("stream-ocr-led").className = (__enabled ? "led-gray" : "hidden");
}
if (__enabled && state.langs !== undefined) {
__updateLangs(state.langs);
}
} else {
__enabled = false;
tools.feature.setEnabled($("stream-ocr"), false);
$("stream-ocr-led").className = "hidden";
}
};
var __updateLangs = function(langs) {
let el = $("stream-ocr-lang-selector");
el.options.length = 0;
for (let lang of langs.available) {
tools.selector.addOption(el, lang, lang);
}
el.value = tools.storage.get("stream.ocr.lang", langs["default"]);
};
var __startSelection = function(event) {
if (__start_pos === null) {
tools.hidden.setVisible($("stream-ocr-selection"), false);
__start_pos = __getGlobalPosition(event);
__end_pos = null;
}
};
var __changeSelection = function(event) {
if (__start_pos !== null) {
__end_pos = __getGlobalPosition(event);
let width = Math.abs(__start_pos.x - __end_pos.x);
let height = Math.abs(__start_pos.y - __end_pos.y);
let el = $("stream-ocr-selection");
el.style.left = Math.min(__start_pos.x, __end_pos.x) + "px";
el.style.top = Math.min(__start_pos.y, __end_pos.y) + "px";
el.style.width = width + "px";
el.style.height = height + "px";
tools.hidden.setVisible(el, (width > 1 || height > 1));
}
};
var __endSelection = function(event) {
__changeSelection(event);
let el = $("stream-ocr-selection");
let ok = (
el.offsetWidth > 1 && el.offsetHeight > 1
&& __start_pos !== null && __end_pos !== null
);
tools.hidden.setVisible(el, ok);
if (ok) {
let rect = $("stream-box").getBoundingClientRect();
let rel_left = Math.min(__start_pos.x, __end_pos.x) - rect.left;
let rel_right = Math.max(__start_pos.x, __end_pos.x) - rect.left;
let offset = __getNavbarOffset();
let rel_top = Math.min(__start_pos.y, __end_pos.y) - rect.top + offset;
let rel_bottom = Math.max(__start_pos.y, __end_pos.y) - rect.top + offset;
let geo = __getGeometry();
__sel = {
"left": tools.remap(rel_left, geo.x, geo.width, 0, geo.real_width),
"right": tools.remap(rel_right, geo.x, geo.width, 0, geo.real_width),
"top": tools.remap(rel_top, geo.y, geo.height, 0, geo.real_height),
"bottom": tools.remap(rel_bottom, geo.y, geo.height, 0, geo.real_height),
};
} else {
__sel = null;
}
__start_pos = null;
__end_pos = null;
};
var __getGlobalPosition = function(event) {
let rect = $("stream-box").getBoundingClientRect();
let geo = __getGeometry();
let offset = __getNavbarOffset();
return {
"x": Math.min(Math.max(event.clientX, rect.left + geo.x), rect.right - geo.x),
"y": Math.min(Math.max(event.clientY - offset, rect.top + geo.y - offset), rect.bottom - geo.y - offset),
};
};
var __getNavbarOffset = function() {
if (tools.browser.is_firefox) {
// На лисе наблюдается оффсет из-за навбара, хз почему
return wm.getViewGeometry().top;
}
return 0;
};
var __resetSelection = function() {
tools.hidden.setVisible($("stream-ocr-selection"), false);
__start_pos = null;
__end_pos = null;
__sel = null;
};
var __recognizeSelection = function() {
tools.el.setEnabled($("stream-ocr-button"), false);
tools.el.setEnabled($("stream-ocr-lang-selector"), false);
$("stream-ocr-led").className = "led-yellow-rotating-fast";
let params = {
"ocr": 1,
"ocr_langs": $("stream-ocr-lang-selector").value,
"ocr_left": __sel.left,
"ocr_top": __sel.top,
"ocr_right": __sel.right,
"orc_bottom": __sel.bottom,
};
tools.httpGet("/api/streamer/snapshot", params, function(http) {
if (http.status === 200) {
wm.copyTextToClipboard(http.responseText);
} else {
wm.error("OCR error:<br>", http.responseText);
}
tools.el.setEnabled($("stream-ocr-button"), true);
tools.el.setEnabled($("stream-ocr-lang-selector"), true);
$("stream-ocr-led").className = "led-gray";
}, null, null, 30000);
};
__init__();
}

View File

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

View File

@@ -0,0 +1,385 @@
/*****************************************************************************
# #
# 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 Recorder() {
var self = this;
/************************************************************************/
var __ws = null;
var __play_timer = null;
var __recording = false;
var __events = [];
var __events_time = 0;
var __last_event_ts = 0;
var __init__ = function() {
tools.el.setOnClick($("hid-recorder-record"), __startRecord);
tools.el.setOnClick($("hid-recorder-stop"), __stopProcess);
tools.el.setOnClick($("hid-recorder-play"), __playRecord);
tools.el.setOnClick($("hid-recorder-clear"), __clearRecord);
$("hid-recorder-new-script-file").onchange = __uploadScript;
tools.el.setOnClick($("hid-recorder-upload"), () => $("hid-recorder-new-script-file").click());
tools.el.setOnClick($("hid-recorder-download"), __downloadScript);
};
/************************************************************************/
self.setSocket = function(ws) {
if (ws !== __ws) {
__ws = ws;
}
if (__ws === null) {
__stopProcess();
}
__refresh();
};
self.recordWsEvent = function(event) {
__recordEvent(event);
};
self.recordPrintEvent = function(text, keymap) {
__recordEvent({"event_type": "print", "event": {"text": text, "keymap": keymap}});
};
self.recordAtxButtonEvent = function(button) {
__recordEvent({"event_type": "atx_button", "event": {"button": button}});
};
self.recordGpioSwitchEvent = function(channel, to) {
__recordEvent({"event_type": "gpio_switch", "event": {"channel": channel, "state": to}});
};
self.recordGpioPulseEvent = function(channel) {
__recordEvent({"event_type": "gpio_pulse", "event": {"channel": channel}});
};
var __recordEvent = function(event) {
if (__recording) {
let now = new Date().getTime();
if (__last_event_ts) {
let delay = now - __last_event_ts;
__events.push({"event_type": "delay", "event": {"millis": delay}});
__events_time += delay;
}
__last_event_ts = now;
__events.push(event);
__setCounters(__events.length, __events_time);
}
};
var __startRecord = function() {
__clearRecord();
__recording = true;
__refresh();
};
var __stopProcess = function() {
if (__play_timer) {
clearTimeout(__play_timer);
__play_timer = null;
}
if (__recording) {
__recording = false;
}
__refresh();
};
var __playRecord = function() {
__play_timer = setTimeout(() => __runEvents(0), 0);
__refresh();
};
var __clearRecord = function() {
__events = [];
__events_time = 0;
__last_event_ts = 0;
__refresh();
};
var __downloadScript = function() {
let blob = new Blob([JSON.stringify(__events, undefined, 4)], {"type": "application/json"});
let url = window.URL.createObjectURL(blob);
let el_anchor = document.createElement("a");
el_anchor.href = url;
el_anchor.download = "script.json";
el_anchor.click();
window.URL.revokeObjectURL(url);
};
var __uploadScript = function() {
let el_input = $("hid-recorder-new-script-file");
let script_file = (el_input.files.length ? el_input.files[0] : null);
if (script_file) {
let reader = new FileReader();
reader.onload = function () {
let events = [];
let events_time = 0;
try {
let raw_events = JSON.parse(reader.result);
__checkType(raw_events, "object", "Base of script is not an objects list");
for (let event of raw_events) {
__checkType(event, "object", "Non-dict event");
__checkType(event.event, "object", "Non-dict event");
if (event.event_type === "delay") {
__checkUnsigned(event.event.millis, "Non-unsigned delay");
events_time += event.event.millis;
} else if (event.event_type === "print") {
__checkType(event.event.text, "string", "Non-string print text");
if (event.event.keymap) {
__checkType(event.event.keymap, "string", "Non-string keymap");
}
} else if (event.event_type === "key") {
__checkType(event.event.key, "string", "Non-string key code");
__checkType(event.event.state, "boolean", "Non-bool key state");
} else if (event.event_type === "mouse_button") {
__checkType(event.event.button, "string", "Non-string mouse button code");
__checkType(event.event.state, "boolean", "Non-bool mouse button state");
} else if (event.event_type === "mouse_move") {
__checkType(event.event.to, "object", "Non-object mouse move target");
__checkInt(event.event.to.x, "Non-int mouse move X");
__checkInt(event.event.to.y, "Non-int mouse move Y");
} else if (event.event_type === "mouse_relative") {
__checkMouseRelativeDelta(event.event.delta);
__checkType(event.event.squash, "boolean", "Non-boolean squash");
} else if (event.event_type === "mouse_wheel") {
__checkType(event.event.delta, "object", "Non-object mouse wheel delta");
__checkInt(event.event.delta.x, "Non-int mouse delta X");
__checkInt(event.event.delta.y, "Non-int mouse delta Y");
} else if (event.event_type === "atx_button") {
__checkType(event.event.button, "string", "Non-string ATX button");
} else if (event.event_type === "gpio_switch") {
__checkType(event.event.channel, "string", "Non-string GPIO channel");
__checkType(event.event.state, "boolean", "Non-bool GPIO state");
} else if (event.event_type === "gpio_pulse") {
__checkType(event.event.channel, "string", "Non-string GPIO channel");
} else if (event.event_type === "delay_random") {
__checkType(event.event.range, "object", "Non-object random delay range");
__checkUnsigned(event.event.range.min, "Non-unsigned random delay range min");
__checkUnsigned(event.event.range.max, "Non-unsigned random delay range max");
__checkRangeMinMax(event.event.range, "Invalid random delay range");
events_time += event.event.range.max;
} else if (event.event_type === "mouse_move_random") { // Hack for pikvm/pikvm#1041
__checkType(event.event.range, "object", "Non-object random mouse move range");
__checkInt(event.event.range.min, "Non-int random mouse move range min");
__checkInt(event.event.range.max, "Non-int random mouse move range max");
__checkRangeMinMax(event.event.range, "Invalid random mouse move range");
} else {
throw `Unknown event type: ${event.event_type}`;
}
events.push(event);
}
__events = events;
__events_time = events_time;
} catch (ex) {
wm.error("Invalid script", `${ex}`);
}
el_input.value = "";
__refresh();
};
reader.readAsText(script_file, "UTF-8");
}
};
var __checkType = function(obj, type, msg) {
if (typeof obj !== type) {
throw msg;
}
};
var __checkInt = function(obj, msg) {
if (!Number.isInteger(obj)) {
throw msg;
}
};
var __checkUnsigned = function(obj, msg) {
__checkInt(obj, msg);
if (obj < 0) {
throw msg;
}
};
var __checkRangeMinMax = function(obj, msg) {
if (obj.min > obj.max) {
throw msg;
}
};
var __checkArray = function (obj, msg) {
if (!Array.isArray(obj)) {
throw msg;
}
};
var __checkMouseRelativeDelta = function(delta) {
__checkArray(delta, "Non-array relative mouse delta");
delta.forEach(element => {
__checkType(element, "object", "Non-object relative mouse delta element");
__checkInt(element.x, "Non-int mouse delta X");
__checkInt(element.y, "Non-int mouse delta Y");
});
};
var __runEvents = function(index, time=0) {
while (index < __events.length) {
__setCounters(__events.length - index + 1, __events_time - time);
let event = __events[index];
if (["delay", "delay_random"].includes(event.event_type)) {
let millis = (
event.event_type === "delay"
? event.event.millis
: tools.getRandomInt(event.event.range.min, event.event.range.max)
);
__play_timer = setTimeout(() => __runEvents(index + 1, time + millis), millis);
return;
} else if (event.event_type === "print") {
let params = {"limit": 0};
if (event.event.keymap) {
params["keymap"] = event.event.keymap;
}
tools.httpPost("/api/hid/print", params, function(http) {
if (http.status === 413) {
wm.error("Too many text for paste!");
__stopProcess();
} else if (http.status !== 200) {
wm.error("HID paste error", http.responseText);
__stopProcess();
} else if (http.status === 200) {
__play_timer = setTimeout(() => __runEvents(index + 1, time), 0);
}
}, event.event.text, "text/plain");
return;
} else if (event.event_type === "atx_button") {
tools.httpPost("/api/atx/click", {"button": event.event.button}, function(http) {
if (http.status !== 200) {
wm.error("ATX error", http.responseText);
__stopProcess();
} else if (http.status === 200) {
__play_timer = setTimeout(() => __runEvents(index + 1, time), 0);
}
});
return;
} else if (["gpio_switch", "gpio_pulse"].includes(event.event_type)) {
let path = "/api/gpio";
let params = {"channel": event.event.channel};
if (event.event_type === "gpio_switch") {
path += "/switch";
params["state"] = event.event.to;
} else { // gpio_pulse
path += "/pulse";
}
tools.httpPost(path, params, function(http) {
if (http.status !== 200) {
wm.error("GPIO error", http.responseText);
__stopProcess();
} else if (http.status === 200) {
__play_timer = setTimeout(() => __runEvents(index + 1, time), 0);
}
});
return;
} else if (["key", "mouse_button", "mouse_move", "mouse_wheel", "mouse_relative"].includes(event.event_type)) {
__ws.sendHidEvent(event);
} else if (event.event_type === "mouse_move_random") {
__ws.sendHidEvent({
"event_type": "mouse_move",
"event": {"to": {
"x": tools.getRandomInt(event.event.range.min, event.event.range.max),
"y": tools.getRandomInt(event.event.range.min, event.event.range.max),
}},
});
}
index += 1;
}
if ($("hid-recorder-loop-switch").checked) {
setTimeout(() => __runEvents(0));
} else {
__stopProcess();
}
};
var __refresh = function() {
if (__play_timer) {
$("hid-recorder-led").className = "led-yellow-rotating-fast";
$("hid-recorder-led").title = "Playing...";
} else if (__recording) {
$("hid-recorder-led").className = "led-red-rotating-fast";
$("hid-recorder-led").title = "Recording...";
} else {
$("hid-recorder-led").className = "led-gray";
$("hid-recorder-led").title = "";
}
tools.el.setEnabled($("hid-recorder-record"), (__ws && !__play_timer && !__recording));
tools.el.setEnabled($("hid-recorder-stop"), (__ws && (__play_timer || __recording)));
tools.el.setEnabled($("hid-recorder-play"), (__ws && !__recording && __events.length));
tools.el.setEnabled($("hid-recorder-clear"), (!__play_timer && !__recording && __events.length));
tools.el.setEnabled($("hid-recorder-loop-switch"), (__ws && !__recording));
tools.el.setEnabled($("hid-recorder-upload"), (!__play_timer && !__recording));
tools.el.setEnabled($("hid-recorder-download"), (!__play_timer && !__recording && __events.length));
__setCounters(__events.length, __events_time);
};
var __setCounters = function(events_count, events_time) {
$("hid-recorder-time").innerHTML = tools.formatDuration(events_time);
$("hid-recorder-events-count").innerHTML = events_count;
};
__init__();
}

View File

@@ -0,0 +1,425 @@
/*****************************************************************************
# #
# 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";
import {Recorder} from "./recorder.js";
import {Hid} from "./hid.js";
import {Paste} from "./paste.js";
import {Atx} from "./atx.js";
import {Msd} from "./msd.js";
import {Streamer} from "./stream.js";
import {Gpio} from "./gpio.js";
import {Ocr} from "./ocr.js";
export function Session() {
// var self = this;
/************************************************************************/
var __ws = null;
var __ping_timer = null;
var __missed_heartbeats = 0;
var __streamer = new Streamer();
var __recorder = new Recorder();
var __hid = new Hid(__streamer.getGeometry, __recorder);
var __paste = new Paste(__recorder);
var __atx = new Atx(__recorder);
var __msd = new Msd();
var __gpio = new Gpio(__recorder);
var __ocr = new Ocr(__streamer.getGeometry);
var __info_hw_state = null;
var __info_fan_state = null;
var __init__ = function() {
__streamer.ensureDeps(() => __startSession());
};
/************************************************************************/
var __setInfoState = function(state) {
for (let key of Object.keys(state)) {
switch (key) {
case "meta": __setInfoStateMeta(state.meta); break;
case "hw": __setInfoStateHw(state.hw); break;
case "fan": __setInfoStateFan(state.fan); break;
case "system": __setInfoStateSystem(state.system); break;
case "extras": __setInfoStateExtras(state.extras); break;
}
}
};
var __setInfoStateMeta = function(state) {
if (state !== null) {
$("kvmd-meta-json").innerText = JSON.stringify(state, undefined, 4);
if (state.server && state.server.host) {
$("kvmd-meta-server-host").innerText = `Server: ${state.server.host}`;
document.title = `PiKVM Session: ${state.server.host}`;
} else {
$("kvmd-meta-server-host").innerText = "";
document.title = "PiKVM Session";
}
if (state.tips && state.tips.left) {
$("kvmd-meta-tips-left").innerText = `${state.tips.left}`;
}
if (state.tips && state.tips.right) {
$("kvmd-meta-tips-right").innerText = `${state.tips.right}`;
}
// Don't use this option, it may be removed in any time
if (state.web && state.web.confirm_session_exit === false) {
window.onbeforeunload = null; // See main.js
}
}
};
var __setInfoStateHw = function(state) {
if (state.health.throttling !== null) {
let flags = state.health.throttling.parsed_flags;
let ignore_past = state.health.throttling.ignore_past;
let undervoltage = (flags.undervoltage.now || (flags.undervoltage.past && !ignore_past));
let freq_capped = (flags.freq_capped.now || (flags.freq_capped.past && !ignore_past));
tools.hidden.setVisible($("hw-health-dropdown"), (undervoltage || freq_capped));
$("hw-health-undervoltage-led").className = (undervoltage ? (flags.undervoltage.now ? "led-red" : "led-yellow") : "hidden");
$("hw-health-overheating-led").className = (freq_capped ? (flags.freq_capped.now ? "led-red" : "led-yellow") : "hidden");
tools.hidden.setVisible($("hw-health-message-undervoltage"), undervoltage);
tools.hidden.setVisible($("hw-health-message-overheating"), freq_capped);
}
__info_hw_state = state;
__renderAboutInfoHardware();
};
var __setInfoStateFan = function(state) {
let failed = false;
let failed_past = false;
if (state.monitored) {
if (state.state === null) {
failed = true;
} else {
if (!state.state.fan.ok) {
failed = true;
} else if (state.state.fan.last_fail_ts >= 0) {
failed = true;
failed_past = true;
}
}
}
tools.hidden.setVisible($("fan-health-dropdown"), failed);
$("fan-health-led").className = (failed ? (failed_past ? "led-yellow" : "led-red") : "hidden");
__info_fan_state = state;
__renderAboutInfoHardware();
};
var __renderAboutInfoHardware = function() {
let html = "";
if (__info_hw_state !== null) {
html += `
Platform:
${__formatMisc(__info_hw_state)}
<hr>
Temperature:
${__formatTemp(__info_hw_state.health.temp)}
<hr>
Throttling:
${__formatThrottling(__info_hw_state.health.throttling)}
`;
}
if (__info_fan_state !== null) {
if (html.length > 0) {
html += "<hr>";
}
html += `
Fan:
${__formatFan(__info_fan_state)}
`;
}
$("about-hardware").innerHTML = html;
};
var __formatMisc = function(state) {
return __formatUl([
["Base", state.platform.base],
["Serial", state.platform.serial],
["CPU", `${state.health.cpu.percent}%`],
["MEM", `${state.health.mem.percent}%`],
]);
};
var __formatFan = function(state) {
if (!state.monitored) {
return __formatUl([["Status", "Not monitored"]]);
} else if (state.state === null) {
return __formatUl([["Status", __colored("red", "Not available")]]);
} else {
state = state.state;
let pairs = [
["Status", (state.fan.ok ? __colored("green", "Ok") : __colored("red", "Failed"))],
["Desired speed", `${state.fan.speed}%`],
["PWM", `${state.fan.pwm}`],
];
if (state.hall.available) {
pairs.push(["RPM", __colored((state.fan.ok ? "green" : "red"), state.hall.rpm)]);
}
return __formatUl(pairs);
}
};
var __formatTemp = function(temp) {
let pairs = [];
for (let field of Object.keys(temp).sort()) {
pairs.push([field.toUpperCase(), `${temp[field]}&deg;C`]);
}
return __formatUl(pairs);
};
var __formatThrottling = function(throttling) {
if (throttling !== null) {
let pairs = [];
for (let field of Object.keys(throttling.parsed_flags).sort()) {
let flags = throttling.parsed_flags[field];
let key = tools.upperFirst(field).replace("_", " ");
let value = (flags["now"] ? __colored("red", "RIGHT NOW") : __colored("green", "No"));
if (!throttling.ignore_past) {
value += "; " + (flags["past"] ? __colored("red", "In the past") : __colored("green", "Never"));
}
pairs.push([key, value]);
}
return __formatUl(pairs);
} else {
return "NO DATA";
}
};
var __colored = function(color, html) {
return `<font color="${color}">${html}</font>`;
};
var __setInfoStateSystem = function(state) {
$("about-version").innerHTML = `
KVMD: <span class="code-comment">${state.kvmd.version}</span><br>
<hr>
Streamer: <span class="code-comment">${state.streamer.version} (${state.streamer.app})</span>
${__formatStreamerFeatures(state.streamer.features)}
<hr>
${state.kernel.system} kernel:
${__formatUname(state.kernel)}
`;
$("kvmd-version-kvmd").innerText = state.kvmd.version;
$("kvmd-version-streamer").innerText = state.streamer.version;
};
var __formatStreamerFeatures = function(features) {
let pairs = [];
for (let field of Object.keys(features).sort()) {
pairs.push([field, (features[field] ? "Yes" : "No")]);
}
return __formatUl(pairs);
};
var __formatUname = function(kernel) {
let pairs = [];
for (let field of Object.keys(kernel).sort()) {
if (field !== "system") {
pairs.push([tools.upperFirst(field), kernel[field]]);
}
}
return __formatUl(pairs);
};
var __formatUl = function(pairs) {
let html = "";
for (let pair of pairs) {
html += `<li>${pair[0]}: <span class="code-comment">${pair[1]}</span></li>`;
}
return `<ul>${html}</ul>`;
};
var __setInfoStateExtras = function(state) {
let show_hook = null;
let close_hook = null;
let has_webterm = (state.webterm && (state.webterm.enabled || state.webterm.started));
if (has_webterm) {
let path = "/" + state.webterm.path + "?disableLeaveAlert=true";
show_hook = function() {
tools.info("Terminal opened: ", path);
$("webterm-iframe").src = path;
};
close_hook = function() {
tools.info("Terminal closed");
$("webterm-iframe").src = "";
};
}
tools.feature.setEnabled($("system-tool-webterm"), has_webterm);
$("webterm-window").show_hook = show_hook;
$("webterm-window").close_hook = close_hook;
};
var __startSession = function() {
$("link-led").className = "led-yellow";
$("link-led").title = "Connecting...";
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.sendHidEvent = (event) => __sendHidEvent(__ws, event.event_type, event.event);
__ws.onopen = __wsOpenHandler;
__ws.onmessage = __wsMessageHandler;
__ws.onerror = __wsErrorHandler;
__ws.onclose = __wsCloseHandler;
} else if (http.status === 401 || http.status === 403) {
window.onbeforeunload = () => null;
wm.error("Unexpected logout occured, please login again").then(function() {
document.location.href = "/login";
});
} else {
__wsCloseHandler(null);
}
});
};
var __ascii_encoder = new TextEncoder("ascii");
var __sendHidEvent = function(ws, event_type, event) {
if (event_type == "key") {
let data = __ascii_encoder.encode("\x01\x00" + event.key);
data[1] = (event.state ? 1 : 0);
ws.send(data);
} else if (event_type == "mouse_button") {
let data = __ascii_encoder.encode("\x02\x00" + event.button);
data[1] = (event.state ? 1 : 0);
ws.send(data);
} else if (event_type == "mouse_move") {
let data = new Uint8Array([
3,
(event.to.x >> 8) & 0xFF, event.to.x & 0xFF,
(event.to.y >> 8) & 0xFF, event.to.y & 0xFF,
]);
ws.send(data);
} else if (event_type == "mouse_relative" || event_type == "mouse_wheel") {
let data;
if (Array.isArray(event.delta)) {
data = new Int8Array(2 + event.delta.length * 2);
let index = 0;
for (let delta of event.delta) {
data[index + 2] = delta["x"];
data[index + 3] = delta["y"];
index += 2;
}
} else {
data = new Int8Array([0, 0, event.delta.x, event.delta.y]);
}
data[0] = (event_type == "mouse_relative" ? 4 : 5);
data[1] = (event.squash ? 1 : 0);
ws.send(data);
}
};
var __wsOpenHandler = function(event) {
tools.debug("Session: socket opened:", event);
$("link-led").className = "led-green";
$("link-led").title = "Connected";
__recorder.setSocket(__ws);
__hid.setSocket(__ws);
__missed_heartbeats = 0;
__ping_timer = setInterval(__pingServer, 1000);
};
var __wsMessageHandler = function(event) {
// tools.debug("Session: received socket data:", event.data);
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;
}
};
var __wsErrorHandler = function(event) {
tools.error("Session: socket error:", event);
if (__ws) {
__ws.onclose = null;
__ws.close();
__wsCloseHandler(null);
}
};
var __wsCloseHandler = function(event) {
tools.debug("Session: socket closed:", event);
$("link-led").className = "led-gray";
if (__ping_timer) {
clearInterval(__ping_timer);
__ping_timer = null;
}
__gpio.setState(null);
__hid.setSocket(null); // auto setState(null);
__paste.setState(null);
__atx.setState(null);
__msd.setState(null);
__streamer.setState(null);
__ocr.setState(null);
__recorder.setSocket(null);
__ws = null;
setTimeout(function() {
$("link-led").className = "led-yellow";
setTimeout(__startSession, 500);
}, 500);
};
var __pingServer = function() {
try {
__missed_heartbeats += 1;
if (__missed_heartbeats >= 15) {
throw new Error("Too many missed heartbeats");
}
__ws.send("{\"event_type\": \"ping\", \"event\": {}}");
} catch (ex) {
__wsErrorHandler(ex.message);
}
};
__init__();
}

View File

@@ -0,0 +1,416 @@
/*****************************************************************************
# #
# 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";
import {JanusStreamer} from "./stream_janus.js";
import {MjpegStreamer} from "./stream_mjpeg.js";
export function Streamer() {
var self = this;
/************************************************************************/
var __janus_imported = null;
var __streamer = null;
var __state = null;
var __res = {"width": 640, "height": 480};
var __init__ = function() {
__streamer = new MjpegStreamer(__setActive, __setInactive, __setInfo);
$("stream-led").title = "Stream inactive";
tools.slider.setParams($("stream-quality-slider"), 5, 100, 5, 80, function(value) {
$("stream-quality-value").innerText = `${value}%`;
});
tools.slider.setOnUpDelayed($("stream-quality-slider"), 1000, (value) => __sendParam("quality", value));
tools.slider.setParams($("stream-h264-bitrate-slider"), 25, 20000, 25, 5000, function(value) {
$("stream-h264-bitrate-value").innerText = value;
});
tools.slider.setOnUpDelayed($("stream-h264-bitrate-slider"), 1000, (value) => __sendParam("h264_bitrate", value));
tools.slider.setParams($("stream-h264-gop-slider"), 0, 60, 1, 30, function(value) {
$("stream-h264-gop-value").innerText = value;
});
tools.slider.setOnUpDelayed($("stream-h264-gop-slider"), 1000, (value) => __sendParam("h264_gop", value));
tools.slider.setParams($("stream-desired-fps-slider"), 0, 120, 1, 0, function(value) {
$("stream-desired-fps-value").innerText = (value === 0 ? "Unlimited" : value);
});
tools.slider.setOnUpDelayed($("stream-desired-fps-slider"), 1000, (value) => __sendParam("desired_fps", value));
$("stream-resolution-selector").onchange = (() => __sendParam("resolution", $("stream-resolution-selector").value));
tools.radio.setOnClick("stream-mode-radio", __clickModeRadio, false);
// Not getInt() because of radio is a string container.
// Also don't reset Janus at class init.
tools.radio.clickValue("stream-orient-radio", tools.storage.get("stream.orient", 0));
tools.radio.setOnClick("stream-orient-radio", function() {
if (__streamer.getMode() === "janus") { // Right now it's working only for H.264
let orient = parseInt(tools.radio.getValue("stream-orient-radio"));
tools.storage.setInt("stream.orient", orient);
if (__streamer.getOrientation() != orient) {
__resetStream();
}
}
}, false);
tools.slider.setParams($("stream-audio-volume-slider"), 0, 100, 1, 0, function(value) {
$("stream-video").muted = !value;
$("stream-video").volume = value / 100;
$("stream-audio-volume-value").innerText = value + "%";
if (__streamer.getMode() === "janus") {
let allow_audio = !$("stream-video").muted;
if (__streamer.isAudioAllowed() !== allow_audio) {
__resetStream();
}
}
});
tools.el.setOnClick($("stream-screenshot-button"), __clickScreenshotButton);
tools.el.setOnClick($("stream-reset-button"), __clickResetButton);
tools.el.setOnClick($("stream-record-start-button"), __clickRecordStartButton);
tools.el.setOnClick($("stream-record-stop-button"), __clickRecordStopButton);
$("stream-window").show_hook = () => __applyState(__state);
$("stream-window").close_hook = () => __applyState(null);
//hidden stream-record-stop-button
document.getElementById('stream-record-stop-button').disabled = true;
};
/************************************************************************/
self.ensureDeps = function(callback) {
JanusStreamer.ensure_janus(function(avail) {
__janus_imported = avail;
callback();
});
};
self.getGeometry = function() {
// Первоначально обновление геометрии считалось через ResizeObserver.
// Но оно не ловило некоторые события, например в последовательности:
// - Находять в HD переходим в фулскрин
// - Меняем разрешение на маленькое
// - Убираем фулскрин
// - Переходим в HD
// - Видим нарушение пропорций
// Так что теперь используются быстре рассчеты через offset*
// вместо getBoundingClientRect().
let res = __streamer.getResolution();
let ratio = Math.min(res.view_width / res.real_width, res.view_height / res.real_height);
return {
"x": Math.round((res.view_width - ratio * res.real_width) / 2),
"y": Math.round((res.view_height - ratio * res.real_height) / 2),
"width": Math.round(ratio * res.real_width),
"height": Math.round(ratio * res.real_height),
"real_width": res.real_width,
"real_height": res.real_height,
};
};
self.setState = function(state) {
if (state) {
if (!__state) {
__state = {};
}
if (state.features !== undefined) {
__state.features = state.features;
__state.limits = state.limits; // Following together with features
}
if (__state.features !== undefined && state.streamer !== undefined) {
__state.streamer = state.streamer;
__setControlsEnabled(!!state.streamer);
}
} else {
__state = null;
__setControlsEnabled(false);
}
let visible = wm.isWindowVisible($("stream-window"));
__applyState((visible && __state && __state.features) ? state : null);
};
var __applyState = function(state) {
if (__janus_imported === null) {
alert("__janus_imported is null, please report");
return;
}
if (!state) {
__streamer.stopStream();
return;
}
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
tools.info(
`Stream: Janus WebRTC state: features.h264=${f.h264},`
+ ` webrtc=${has_webrtc}, h264=${has_h264}, janus_imported=${__janus_imported}`
);
tools.hidden.setVisible($("stream-message-no-webrtc"), __janus_imported && f.h264 && !has_webrtc);
tools.hidden.setVisible($("stream-message-no-h264"), __janus_imported && f.h264 && !has_h264);
tools.slider.setRange($("stream-desired-fps-slider"), l.desired_fps.min, l.desired_fps.max);
if (f.resolution) {
let el = $("stream-resolution-selector");
el.options.length = 0;
for (let res of l.available_resolutions) {
tools.selector.addOption(el, res, res);
}
} else {
$("stream-resolution-selector").options.length = 0;
}
if (has_janus) {
tools.slider.setRange($("stream-h264-bitrate-slider"), l.h264_bitrate.min, l.h264_bitrate.max);
tools.slider.setRange($("stream-h264-gop-slider"), l.h264_gop.min, l.h264_gop.max);
}
// tools.feature.setEnabled($("stream-quality"), f.quality); // Only on s.encoder.quality
tools.feature.setEnabled($("stream-resolution"), f.resolution);
tools.feature.setEnabled($("stream-h264-bitrate"), has_janus);
tools.feature.setEnabled($("stream-h264-gop"), has_janus);
tools.feature.setEnabled($("stream-mode"), has_janus);
if (!has_janus) {
tools.feature.setEnabled($("stream-audio"), false);
}
let mode = (has_janus ? tools.storage.get("stream.mode", "janus") : "mjpeg");
tools.radio.clickValue("stream-mode-radio", mode);
}
if (state.streamer) {
let s = state.streamer;
__res = s.source.resolution;
{
let res = `${__res.width}x${__res.height}`;
let el = $("stream-resolution-selector");
if (!tools.selector.hasValue(el, res)) {
tools.selector.addOption(el, res, res);
}
el.value = res;
}
tools.slider.setValue($("stream-quality-slider"), Math.max(s.encoder.quality, 1));
tools.slider.setValue($("stream-desired-fps-slider"), s.source.desired_fps);
if (s.h264 && s.h264.bitrate) {
tools.slider.setValue($("stream-h264-bitrate-slider"), s.h264.bitrate);
tools.slider.setValue($("stream-h264-gop-slider"), s.h264.gop); // Following together with gop
}
tools.feature.setEnabled($("stream-quality"), (s.encoder.quality > 0));
__streamer.ensureStream(s);
}
};
var __setActive = function() {
$("stream-led").className = "led-green";
$("stream-led").title = "Stream is active";
};
var __setInactive = function() {
$("stream-led").className = "led-gray";
$("stream-led").title = "Stream inactive";
};
var __setControlsEnabled = function(enabled) {
tools.el.setEnabled($("stream-quality-slider"), enabled);
tools.el.setEnabled($("stream-desired-fps-slider"), enabled);
tools.el.setEnabled($("stream-resolution-selector"), enabled);
tools.el.setEnabled($("stream-h264-bitrate-slider"), enabled);
tools.el.setEnabled($("stream-h264-gop-slider"), enabled);
};
var __setInfo = function(is_active, online, text) {
$("stream-box").classList.toggle("stream-box-offline", !online);
let el_grab = document.querySelector("#stream-window-header .window-grab");
let el_info = $("stream-info");
let title = `${__streamer.getName()} - `;
if (is_active) {
if (!online) {
title += "No signal / ";
}
title += `${__res.width}x${__res.height}`;
if (text.length > 0) {
title += " / " + text;
}
} else {
if (text.length > 0) {
title += text;
} else {
title += "Inactive";
}
}
el_grab.innerText = el_info.innerText = title;
};
var __resetStream = function(mode=null) {
if (mode === null) {
mode = __streamer.getMode();
}
__streamer.stopStream();
if (mode === "janus") {
__streamer = new JanusStreamer(__setActive, __setInactive, __setInfo,
tools.storage.getInt("stream.orient", 0), !$("stream-video").muted);
// 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);
tools.feature.setEnabled($("stream-orient"), false);
tools.feature.setEnabled($("stream-audio"), false); // Enabling in stream_janus.js
}
if (wm.isWindowVisible($("stream-window"))) {
__streamer.ensureStream((__state && __state.streamer !== undefined) ? __state.streamer : null);
}
};
var __clickModeRadio = function() {
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-video"), (mode === "janus"));
__resetStream(mode);
}
};
var __clickScreenshotButton = function() {
let el = document.createElement("a");
el.href = "/api/streamer/snapshot";
el.target = "_blank";
document.body.appendChild(el);
el.click();
setTimeout(() => document.body.removeChild(el), 0);
};
var __clickResetButton = function() {
wm.confirm("Are you sure you want to reset stream?").then(function(ok) {
if (ok) {
__resetStream();
tools.httpPost("/api/streamer/reset", null, function(http) {
if (http.status !== 200) {
wm.error("Can't reset stream", http.responseText);
}
});
}
});
};
var stream_mjpeg_refresh_img;
var stream_now_fps
let mediaRecorder;
var __clickRecordStartButton = function() {
wm.confirm("Are you sure you want to record stream?").then(function (ok) {
if (ok) {
stream_now_fps = tools.slider.getValue($("stream-desired-fps-slider"));
let recordedBlobs = [];
//"mjpeg" or "janus"
let stream_type = document.querySelector('input[name="stream-mode-radio"]:checked').value;
if ( stream_type == "mjpeg"){
var stream_mjpeg_img = document.getElementById('stream-image');
var stream_mjpeg_canvas = document.getElementById('stream-mjpeg-canvas');
var ctx = stream_mjpeg_canvas.getContext('2d');
stream_mjpeg_canvas.width = stream_mjpeg_img.width;
stream_mjpeg_canvas.height = stream_mjpeg_img.height;
const stream = stream_mjpeg_canvas.captureStream(stream_now_fps); // Capture FPS
mediaRecorder = new MediaRecorder(stream);
}else{
const stream = document.getElementById("stream-video")
stream.captureStream = stream.captureStream || stream.mozCaptureStream;
mediaRecorder = new MediaRecorder(stream.captureStream());
}
mediaRecorder.ondataavailable = function(event) {
if (event.data && event.data.size > 0) {
recordedBlobs.push(event.data);
}
};
mediaRecorder.onstop = function() {
const blob = new Blob(recordedBlobs, {type: 'video/webm'});
var url = URL.createObjectURL(blob);
const a = document.createElement('a');
document.body.appendChild(a);
const now = new Date();
const year = now.getFullYear();
const month = ('0' + (now.getMonth() + 1)).slice(-2);
const day = ('0' + now.getDate()).slice(-2);
const hours = ('0' + now.getHours()).slice(-2);
const minutes = ('0' + now.getMinutes()).slice(-2);
const seconds = ('0' + now.getSeconds()).slice(-2);//Get now time
a.style = "display: none";
a.href = url;
a.download = stream_type +"_"+ year + month + day + hours + minutes + seconds + ".webm";
a.click();
window.URL.revokeObjectURL(url);
};
mediaRecorder.start();
document.getElementById('stream-record-start-button').disabled = true;
document.getElementById('stream-record-stop-button').disabled = false;
if (stream_type == "mjpeg"){
stream_mjpeg_refresh_img = setInterval(function() {
ctx.drawImage(stream_mjpeg_img, 0, 0, stream_mjpeg_img.width, stream_mjpeg_img.height);
}, 1000 / stream_now_fps);
}
}
});
};
var __clickRecordStopButton = function() {
mediaRecorder.stop();
clearInterval(stream_mjpeg_refresh_img);
document.getElementById('stream-record-start-button').disabled = false;
document.getElementById('stream-record-stop-button').disabled = true;
};
var __sendParam = function(name, value) {
tools.httpPost("/api/streamer/set_params", {[name]: value}, function(http) {
if (http.status !== 200) {
wm.error("Can't configure stream", http.responseText);
}
});
};
__init__();
}

View File

@@ -0,0 +1,457 @@
/*****************************************************************************
# #
# 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";
var _Janus = null;
export function JanusStreamer(__setActive, __setInactive, __setInfo, __orient, __allow_audio) {
var self = this;
var __stop = false;
var __ensuring = false;
var __janus = null;
var __handle = null;
var __retry_ensure_timeout = null;
var __retry_emsg_timeout = null;
var __info_interval = null;
var __state = null;
var __frames = 0;
self.getOrientation = () => __orient;
self.isAudioAllowed = () => __allow_audio;
self.getName = () => (__allow_audio ? "H.264 + Audio" : "H.264");
self.getMode = () => "janus";
self.getResolution = function() {
let el = $("stream-video");
return {
// Разрешение видео или элемента
"real_width": (el.videoWidth || el.offsetWidth),
"real_height": (el.videoHeight || el.offsetHeight),
"view_width": el.offsetWidth,
"view_height": el.offsetHeight,
};
};
self.ensureStream = function(state) {
__state = state;
__stop = false;
__ensureJanus(false);
};
self.stopStream = function() {
__stop = true;
__destroyJanus();
};
var __ensureJanus = function(internal) {
if (__janus === null && !__stop && (!__ensuring || internal)) {
__setInactive();
__setInfo(false, false, "");
__ensuring = true;
__logInfo("Starting Janus ...");
__janus = new _Janus({
"server": `${tools.is_https ? "wss" : "ws"}://${location.host}/janus/ws`,
"ipv6": true,
"destroyOnUnload": false,
"success": __attachJanus,
"error": function(error) {
__logError(error);
__setInfo(false, false, error);
__finishJanus();
},
});
}
};
var __finishJanus = function() {
if (__stop) {
if (__retry_ensure_timeout !== null) {
clearTimeout(__retry_ensure_timeout);
__retry_ensure_timeout = null;
}
__ensuring = false;
} else {
if (__retry_ensure_timeout === null) {
__retry_ensure_timeout = setTimeout(function() {
__retry_ensure_timeout = null;
__ensureJanus(true);
}, 5000);
}
}
__stopRetryEmsgInterval();
__stopInfoInterval();
if (__handle) {
__logInfo("uStreamer detaching ...:", __handle.getPlugin(), __handle.getId());
__handle.detach();
__handle = null;
}
__janus = null;
__setInactive();
if (__stop) {
__setInfo(false, false, "");
}
};
var __destroyJanus = function() {
if (__janus !== null) {
__janus.destroy();
}
__finishJanus();
let stream = $("stream-video").srcObject;
if (stream) {
for (let track of stream.getTracks()) {
__removeTrack(track);
}
}
};
var __addTrack = function(track) {
let el = $("stream-video");
if (el.srcObject) {
for (let tr of el.srcObject.getTracks()) {
if (tr.kind === track.kind && tr.id !== track.id) {
__removeTrack(tr);
}
}
}
if (!el.srcObject) {
el.srcObject = new MediaStream();
}
el.srcObject.addTrack(track);
};
var __removeTrack = function(track) {
let el = $("stream-video");
if (!el.srcObject) {
return;
}
track.stop();
el.srcObject.removeTrack(track);
if (el.srcObject.getTracks().length === 0) {
// MediaStream should be destroyed to prevent old picture freezing
// on Janus reconnecting.
el.srcObject = null;
}
};
var __attachJanus = function() {
if (__janus === null) {
return;
}
__janus.attach({
"plugin": "janus.plugin.ustreamer",
"opaqueId": "oid-" + _Janus.randomString(12),
"success": function(handle) {
__handle = handle;
__logInfo("uStreamer attached:", handle.getPlugin(), handle.getId());
__sendWatch();
},
"error": function(error) {
__logError("Can't attach uStreamer: ", error);
__setInfo(false, false, error);
__destroyJanus();
},
"connectionState": function(state) {
__logInfo("Peer connection state changed to", state);
if (state === "failed") {
__destroyJanus();
}
},
"iceState": function(state) {
__logInfo("ICE state changed to", state);
},
"webrtcState": function(up) {
__logInfo("Janus says our WebRTC PeerConnection is", (up ? "up" : "down"), "now");
if (up) {
__sendKeyRequired();
}
},
"onmessage": function(msg, jsep) {
__stopRetryEmsgInterval();
if (msg.result) {
__logInfo("Got uStreamer result message:", msg.result.status); // starting, started, stopped
if (msg.result.status === "started") {
__setActive();
__setInfo(false, false, "");
} else if (msg.result.status === "stopped") {
__setInactive();
__setInfo(false, false, "");
} else if (msg.result.status === "features") {
tools.feature.setEnabled($("stream-audio"), msg.result.features.audio);
}
} else if (msg.error_code || msg.error) {
__logError("Got uStreamer error message:", msg.error_code, "-", msg.error);
__setInfo(false, false, msg.error);
if (__retry_emsg_timeout === null) {
__retry_emsg_timeout = setTimeout(function() {
if (!__stop) {
__sendStop();
__sendWatch();
}
__retry_emsg_timeout = null;
}, 2000);
}
return;
} else {
__logInfo("Got uStreamer other message:", msg);
}
if (jsep) {
__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});
}
__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
"customizeSdp": function(jsep) {
jsep.sdp = jsep.sdp.replace("useinbandfec=1", "useinbandfec=1;stereo=1");
},
"success": function(jsep) {
__logInfo("Got SDP:", jsep);
__sendStart(jsep);
},
"error": function(error) {
__logInfo("Error on SDP handling:", error);
__setInfo(false, false, error);
//__destroyJanus();
},
});
}
},
// Janus 1.x
"onremotetrack": function(track, id, added, meta) {
// 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
// reason == "mute" and "unmute".
let reason = (meta || {}).reason;
__logInfo("Got onremotetrack:", id, added, reason, track, meta);
if (added && reason === "created") {
__addTrack(track);
if (track.kind === "video") {
__sendKeyRequired();
__startInfoInterval();
}
} else if (!added && reason === "ended") {
__removeTrack(track);
}
},
// 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();
},
});
};
var __startInfoInterval = function() {
__stopInfoInterval();
__setActive();
__updateInfo();
__info_interval = setInterval(__updateInfo, 1000);
};
var __stopInfoInterval = function() {
if (__info_interval !== null) {
clearInterval(__info_interval);
}
__info_interval = null;
};
var __stopRetryEmsgInterval = function() {
if (__retry_emsg_timeout !== null) {
clearTimeout(__retry_emsg_timeout);
__retry_emsg_timeout = null;
}
};
var __updateInfo = function() {
if (__handle !== null) {
let info = "";
if (__handle !== null) {
// https://wiki.whatwg.org/wiki/Video_Metrics
let frames = null;
let el = $("stream-video");
if (el.webkitDecodedFrameCount !== undefined) {
frames = el.webkitDecodedFrameCount;
} else if (el.mozPaintedFrames !== undefined) {
frames = el.mozPaintedFrames;
}
info = `${__handle.getBitrate()}`.replace("kbits/sec", "kbps");
if (frames !== null) {
info += ` / ${Math.max(0, frames - __frames)} fps dynamic`;
__frames = frames;
}
}
__setInfo(true, __isOnline(), info);
}
};
var __isOnline = function() {
return !!(__state && __state.source.online);
};
var __sendWatch = function() {
if (__handle) {
__logInfo(`Sending WATCH(orient=${__orient}, audio=${__allow_audio}) + FEATURES ...`);
__handle.send({"message": {"request": "features"}});
__handle.send({"message": {"request": "watch", "params": {
"orientation": __orient,
"audio": __allow_audio,
}}});
}
};
var __sendStart = function(jsep) {
if (__handle) {
__logInfo("Sending START ...");
__handle.send({"message": {"request": "start"}, "jsep": jsep});
}
};
var __sendKeyRequired = function() {
/*if (__handle) {
// На этом шаге мы говорим что стрим пошел и надо запросить кейфрейм
__logInfo("Sending KEY_REQUIRED ...");
__handle.send({message: {request: "key_required"}});
}*/
};
var __sendStop = function() {
__stopInfoInterval();
if (__handle) {
__logInfo("Sending STOP ...");
__handle.send({"message": {"request": "stop"}});
__handle.hangup();
}
};
var __logInfo = (...args) => tools.info("Stream [Janus]:", ...args);
var __logError = (...args) => tools.error("Stream [Janus]:", ...args);
}
JanusStreamer.ensure_janus = function(callback) {
if (_Janus === null) {
import("./janus.js").then((module) => {
module.Janus.init({
"debug": "all",
"callback": function() {
_Janus = module.Janus;
callback(true);
},
});
}).catch((ex) => {
tools.error("Stream: Can't import Janus module:", ex);
callback(false);
});
} else {
callback(true);
}
};
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;
};

View File

@@ -0,0 +1,159 @@
/*****************************************************************************
# #
# 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 MjpegStreamer(__setActive, __setInactive, __setInfo) {
var self = this;
/************************************************************************/
var __key = tools.makeId();
var __id = "";
var __fps = -1;
var __state = null;
var __timer = null;
var __timer_retries = 0;
/************************************************************************/
self.getName = () => "MJPEG";
self.getMode = () => "mjpeg";
self.getResolution = function() {
let el = $("stream-image");
return {
"real_width": el.naturalWidth,
"real_height": el.naturalHeight,
"view_width": el.offsetWidth,
"view_height": el.offsetHeight,
};
};
self.ensureStream = function(state) {
if (state) {
__state = state;
__findId();
if (__id.length > 0 && __id in __state.stream.clients_stat) {
__setStreamActive();
__stopChecking();
} else {
__ensureChecking();
}
} else {
__stopChecking();
__setStreamInactive();
}
};
self.stopStream = function() {
self.ensureStream(null);
let blank = "/share/png/blank-stream.png";
if (!String.prototype.endsWith.call($("stream-image").src, blank)) {
$("stream-image").src = blank;
}
};
var __setStreamActive = function() {
let old_fps = __fps;
__fps = __state.stream.clients_stat[__id].fps;
if (old_fps < 0) {
__logInfo("Active");
__setActive();
}
__setInfo(true, __state.source.online, `${__fps} fps dynamic`);
};
var __setStreamInactive = function() {
let old_fps = __fps;
__key = tools.makeId();
__id = "";
__fps = -1;
__state = null;
if (old_fps >= 0) {
__logInfo("Inactive");
__setInactive();
__setInfo(false, false, "");
}
};
var __ensureChecking = function() {
if (!__timer) {
__timer_retries = 10;
__timer = setInterval(__checkStream, 100);
}
};
var __stopChecking = function() {
if (__timer) {
clearInterval(__timer);
}
__timer = null;
__timer_retries = 0;
};
var __findId = function() {
let sc = tools.cookies.get("stream_client");
console.log("stream_client", sc);
if (__id.length === 0 && sc && sc.startsWith(__key + "/")) {
__logInfo("Found acceptable stream_client cookie:", sc);
__id = sc.slice(sc.indexOf("/") + 1);
}
};
var __checkStream = function() {
__findId();
if (__id.legnth > 0 && __id in __state.stream.clients_stat) {
__setStreamActive();
__stopChecking();
} else if (__id.length > 0 && __timer_retries >= 0) {
__timer_retries -= 1;
} else {
__setStreamInactive();
__stopChecking();
let path = `/streamer/stream?key=${__key}`;
if (tools.browser.is_safari || tools.browser.is_ios) {
// uStreamer fix for WebKit
__logInfo("Using dual_final_frames=1 to fix WebKit bugs");
path += "&dual_final_frames=1";
} else if (tools.browser.is_chrome || tools.browser.is_blink) {
// uStreamer fix for Blink https://bugs.chromium.org/p/chromium/issues/detail?id=527446
__logInfo("Using advance_headers=1 to fix Blink bugs");
path += "&advance_headers=1";
}
__logInfo("Refreshing ...");
$("stream-image").src = path;
}
};
var __logInfo = (...args) => tools.info("Stream [MJPEG]:", ...args);
}

View File

@@ -0,0 +1,87 @@
/*****************************************************************************
# #
# 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 {checkBrowser} from "../bb.js";
import {wm, initWindowManager} from "../wm.js";
export function main() {
if (checkBrowser(null, null)) {
initWindowManager();
tools.el.setOnClick($("login-button"), __login);
$("user-input").onkeyup = $("passwd-input").onkeyup = $("code-input").onkeyup = function(event) {
if (event.code === "Enter") {
event.preventDefault();
$("login-button").click();
}
};
$("user-input").focus();
}
}
function __login() {
let user = $("user-input").value;
if (user.length === 0) {
$("user-input").focus();
} else {
let passwd = $("passwd-input").value + $("code-input").value;
let body = `user=${encodeURIComponent(user)}&passwd=${encodeURIComponent(passwd)}`;
tools.httpPost("/api/auth/login", null, function(http) {
if (http.status === 200) {
document.location.href = "/";
} else if (http.status === 403) {
wm.error("Invalid credentials").then(__tryAgain);
} else {
let error = "";
if (http.status === 400) {
try { error = JSON.parse(http.responseText)["result"]["error"]; } catch { /* Nah */ }
}
if (error === "ValidatorError") {
wm.error("Invalid characters in credentials").then(__tryAgain);
} else {
wm.error("Login error", http.responseText).then(__tryAgain);
}
}
}, body, "application/x-www-form-urlencoded");
__setEnabled(false);
}
}
function __setEnabled(enabled) {
tools.el.setEnabled($("user-input"), enabled);
tools.el.setEnabled($("passwd-input"), enabled);
tools.el.setEnabled($("code-input"), enabled);
tools.el.setEnabled($("login-button"), enabled);
}
function __tryAgain() {
__setEnabled(true);
let el = ($("code-input").value.length ? $("code-input") : $("passwd-input"));
el.focus();
el.select();
}

View File

@@ -0,0 +1,448 @@
/*****************************************************************************
# #
# 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 {browser} from "./bb.js";
export var tools = new function() {
var self = this;
/************************************************************************/
self.setDefault = function(dict, key, value) {
if (!(key in dict)) {
dict[key] = value;
}
};
/************************************************************************/
self.httpRequest = function(method, url, params, callback, body=null, content_type=null, timeout=15000) {
if (params) {
params = new URLSearchParams(params);
if (params) {
url += "?" + params;
}
}
let http = new XMLHttpRequest();
http.open(method, url, true);
if (content_type) {
http.setRequestHeader("Content-Type", content_type);
}
http.onreadystatechange = function() {
if (http.readyState === 4) {
callback(http);
}
};
http.timeout = timeout;
http.send(body);
};
self.httpGet = function(url, params, callback, body=null, content_type=null, timeout=15000) {
self.httpRequest("GET", url, params, callback, body, content_type, timeout);
};
self.httpPost = function(url, params, callback, body=null, content_type=null, timeout=15000) {
self.httpRequest("POST", url, params, callback, body, content_type, timeout);
};
/************************************************************************/
self.escape = function(text) {
return text.replace(
/[^0-9A-Za-z ]/g,
ch => "&#" + ch.charCodeAt(0) + ";"
);
};
self.partial = function(func, ...args) {
return () => func(...args);
};
self.upperFirst = function(text) {
return text[0].toUpperCase() + text.slice(1);
};
self.makeId = function() {
let chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
let id = "";
for (let count = 0; count < 16; ++count) {
id += chars.charAt(Math.floor(Math.random() * chars.length));
}
return id;
};
self.makeIdByText = function(text) {
return btoa(text).replace("=", "_");
};
self.getRandomInt = function(min, max) {
min = Math.ceil(min);
max = Math.floor(max);
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) );
return (size / Math.pow(1024, index)).toFixed(2) * 1 + " " + ["B", "KiB", "MiB", "GiB", "TiB"][index];
} else {
return 0;
}
};
self.formatDuration = function(duration) {
let millis = parseInt((duration % 1000) / 100);
let secs = Math.floor((duration / 1000) % 60);
let mins = Math.floor((duration / (1000 * 60)) % 60);
let hours = Math.floor((duration / (1000 * 60 * 60)) % 24);
hours = (hours < 10 ? "0" + hours : hours);
mins = (mins < 10 ? "0" + mins : mins);
secs = (secs < 10 ? "0" + secs : secs);
return `${hours}:${mins}:${secs}.${millis}`;
};
self.remap = function(x, a1, b1, a2, b2) {
let remapped = Math.round((x - a1) / b1 * (b2 - a2) + a2);
if (remapped < a2) {
return a2;
} else if (remapped > b2) {
return b2;
}
return remapped;
};
/************************************************************************/
self.el = new function() {
return {
"setOnClick": function(el, callback, prevent_default=true) {
el.onclick = el.ontouchend = function(event) {
if (prevent_default) {
event.preventDefault();
}
callback();
};
},
"setOnDown": function(el, callback, prevent_default=true) {
el.onmousedown = el.ontouchstart = function(event) {
if (prevent_default) {
event.preventDefault();
}
callback();
};
},
"setOnUp": function(el, callback, prevent_default=true) {
el.onmouseup = el.ontouchend = function(event) {
if (prevent_default) {
event.preventDefault();
}
callback();
};
},
"setEnabled": function(el, enabled) {
if (!enabled && document.activeElement === el) {
let el_to_focus = (
el.closest(".modal-window")
|| el.closest(".window")
|| el.closest(".menu")
);
if (el_to_focus) {
el_to_focus.focus();
}
}
el.disabled = !enabled;
},
};
};
self.slider = new function() {
return {
"setOnUpDelayed": function(el, delay, execute_callback) {
el.__execution_timer = null;
el.__pressed = false;
el.__postponed = null;
let clear_timer = function() {
if (el.__execution_timer) {
clearTimeout(el.__execution_timer);
el.__execution_timer = null;
}
};
el.onmousedown = el.ontouchstart = function() {
clear_timer();
el.__pressed = true;
};
el.onmouseup = el.ontouchend = function(event) {
let value = self.slider.getValue(el);
event.preventDefault();
clear_timer();
el.__execution_timer = setTimeout(function() {
el.__pressed = false;
if (el.__postponed !== null) {
self.slider.setValue(el, el.__postponed);
el.__postponed = null;
}
execute_callback(value);
}, delay);
};
},
"setParams": function(el, min, max, step, value, display_callback=null) {
el.min = min;
el.max = max;
el.step = step;
el.value = value;
if (display_callback) {
el.oninput = el.onchange = () => display_callback(self.slider.getValue(el));
display_callback(self.slider.getValue(el));
el.__display_callback = display_callback;
}
},
"setRange": function(el, min, max) {
let value = el.value;
el.min = min;
el.max = max;
if (el.value != value) {
self.slider.setValue(el, el.value, true);
}
},
"setValue": function(el, value, force=false) {
if (el.value != value || force) {
if (el.__pressed) {
el.__postponed = value;
} else {
el.value = value;
if (el.__display_callback) {
el.__display_callback(value);
}
}
}
},
"getValue": function(el) {
if (el.step % 1 === 0) {
return parseInt(el.value);
} else {
return parseFloat(el.value);
}
},
};
};
self.radio = new function() {
return {
"makeItem": function(name, title, value) {
return `
<input type="radio" id="${name}-${value}" name="${name}" value="${value}" />
<label for="${name}-${value}">${title}</label>
`;
},
"setOnClick": function(name, callback, prevent_default=true) {
for (let el of $$$(`input[type="radio"][name="${name}"]`)) {
self.el.setOnClick(el, callback, prevent_default);
}
},
"getValue": function(name) {
return document.querySelector(`input[type="radio"][name="${name}"]:checked`).value;
},
"setValue": function(name, value) {
for (let el of $$$(`input[type="radio"][name="${name}"]`)) {
el.checked = (el.value === value);
}
},
"clickValue": function(name, value) {
for (let el of $$$(`input[type="radio"][name="${name}"]`)) {
if (el.value === value) {
el.click();
return;
}
}
},
"setEnabled": function(name, enabled) {
for (let el of $$$(`input[type="radio"][name="${name}"]`)) {
self.el.setEnabled(el, enabled);
}
},
};
};
self.selector = new function() {
return {
"addOption": function(el, title, value, selected=false) {
el.add(new Option(title, value, selected, selected));
},
"addComment": function(el, title) {
let option = new Option(title, ".".repeat(30), false, false); // Kinda magic value
option.disabled = true;
option.className = "comment";
el.add(option);
},
"addSeparator": function(el, repeat=30) {
if (!self.browser.is_mobile) {
self.selector.addComment(el, "\u2500".repeat(repeat));
}
},
"hasValue": function(el, value) {
for (let el_op of el.options) {
if (el_op.value === value) {
return true;
}
}
return false;
},
};
};
self.progress = new function() {
return {
"setValue": function(el, title, percent) {
el.setAttribute("data-label", title);
el.querySelector(".progress-value").style.width = `${percent}%`;
},
"setPercentOf": function(el, max, value) {
let percent = Math.round(value * 100 / max);
self.progress.setValue(el, `${percent}%`, percent);
},
"setSizeOf": function(el, title, size, free) {
let size_str = self.formatSize(size);
let used = size - free;
let used_str = self.formatSize(used);
let percent = used / size * 100;
title = title.replace("%s", `${used_str} of ${size_str}`);
self.progress.setValue(el, title, percent);
},
};
};
self.input = new function() {
return {
"getFile": function(el) {
return (el.files.length ? el.files[0] : null);
},
};
};
self.hidden = new function() {
return {
"setVisible": function(el, visible) {
el.classList.toggle("hidden", !visible);
},
"isVisible": function(el) {
return !el.classList.contains("hidden");
},
};
};
self.feature = new function() {
return {
"setEnabled": function(el, enabled) {
el.classList.toggle("feature-disabled", !enabled);
},
};
};
/************************************************************************/
let __debug = (new URL(window.location.href)).searchParams.get("debug");
self.debug = function(...args) {
if (__debug) {
__log("DEBUG", ...args);
}
};
self.info = (...args) => __log("INFO", ...args);
self.error = (...args) => __log("ERROR", ...args);
let __log = function(label, ...args) {
let now = (new Date()).toISOString().split("T")[1].replace("Z", "");
console.log(`[${now}] LOG/${label} --`, ...args);
};
/************************************************************************/
self.is_https = (location.protocol === "https:");
self.cookies = new function() {
return {
"get": function(name) {
let matches = document.cookie.match(new RegExp(
"(?:^|; )" + name.replace(/([\.$?*|{}\(\)\[\]\\\/\+^])/g, "\\$1") + "=([^;]*)" // eslint-disable-line no-useless-escape
));
return (matches ? decodeURIComponent(matches[1]) : "");
},
};
};
self.storage = new function() {
return {
"get": function(key, default_value) {
let value = window.localStorage.getItem(key);
return (value !== null ? value : `${default_value}`);
},
"set": (key, value) => window.localStorage.setItem(key, value),
"getInt": (key, default_value) => parseInt(self.storage.get(key, default_value)),
"setInt": (key, value) => self.storage.set(key, value),
"getBool": (key, default_value) => !!parseInt(self.storage.get(key, (default_value ? "1" : "0"))),
"setBool": (key, value) => self.storage.set(key, (value ? "1" : "0")),
"bindSimpleSwitch": function(el, key, default_value, callback=null) {
let value = self.storage.getBool(key, default_value);
el.checked = value;
if (callback) {
callback(value);
}
self.el.setOnClick(el, function() {
if (callback) {
callback(el.checked);
}
self.storage.setBool(key, el.checked);
}, false);
},
};
};
self.config = new function() {
return {
"get": function(key, default_value) {
let value = window.getComputedStyle(document.documentElement).getPropertyValue(`--config-ui--${key}`);
return (value || default_value);
},
"getBool": (key, default_value) => !!parseInt(self.config.get(key, (default_value ? "1" : "0"))),
};
};
self.browser = browser;
};
export var $ = (id) => document.getElementById(id);
export var $$ = (cls) => [].slice.call(document.getElementsByClassName(cls));
export var $$$ = (selector) => document.querySelectorAll(selector);

View File

@@ -0,0 +1,47 @@
/*****************************************************************************
# #
# 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 main() {
__loadKvmdInfo();
}
function __loadKvmdInfo() {
tools.httpGet("/api/info", null, function(http) {
if (http.status === 200) {
let vnc_port = JSON.parse(http.responseText).result.extras.vnc.port;
$("vnc-text").innerHTML = `
<span class="code-comment"># How to connect using the Linux terminal:<br>
$</span> vncviewer ${window.location.hostname}::${vnc_port}
`;
} else if (http.status === 401 || http.status === 403) {
document.location.href = "/login";
} else {
setTimeout(__loadKvmdInfo, 1000);
}
});
}

View File

@@ -0,0 +1,675 @@
/*****************************************************************************
# #
# 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 var wm;
export function initWindowManager() {
wm = new __WindowManager();
}
function __WindowManager() {
var self = this;
/************************************************************************/
var __top_z_index = 0;
var __windows = [];
var __menu_buttons = [];
var __init__ = function() {
for (let el_button of $$$("button")) {
// XXX: Workaround for iOS Safari:
// https://stackoverflow.com/questions/3885018/active-pseudo-class-doesnt-work-in-mobile-safari
el_button.ontouchstart = function() {};
}
for (let el_button of $$("menu-button")) {
el_button.parentElement.querySelector(".menu").setAttribute("tabindex", "-1");
tools.el.setOnDown(el_button, () => __toggleMenu(el_button));
__menu_buttons.push(el_button);
}
if (!window.ResizeObserver) {
tools.error("ResizeObserver not supported");
}
for (let el_window of $$("window")) {
el_window.setAttribute("tabindex", "-1");
__makeWindowMovable(el_window);
__windows.push(el_window);
if (el_window.classList.contains("window-resizable") && window.ResizeObserver) {
new ResizeObserver(function() {
// При переполнении рабочей области сократить размер окна по высоте.
// По ширине оно настраивается само в CSS.
let view = self.getViewGeometry();
let rect = el_window.getBoundingClientRect();
if ((rect.bottom - rect.top) > (view.bottom - view.top)) {
let ratio = (rect.bottom - rect.top) / (view.bottom - view.top);
el_window.style.height = view.bottom - view.top + "px";
el_window.style.width = Math.round((rect.right - rect.left) / ratio) + "px";
}
if (el_window.hasAttribute("data-centered")) {
__centerWindow(el_window);
}
}).observe(el_window);
}
let el_close_button = el_window.querySelector(".window-header .window-button-close");
if (el_close_button) {
el_close_button.title = "Close window";
tools.el.setOnClick(el_close_button, () => self.closeWindow(el_window));
}
let el_maximize_button = el_window.querySelector(".window-header .window-button-maximize");
if (el_maximize_button) {
el_maximize_button.title = "Maximize window";
tools.el.setOnClick(el_maximize_button, function() {
__maximizeWindow(el_window);
__activateLastWindow(el_window);
});
}
let el_orig_button = el_window.querySelector(".window-header .window-button-original");
if (el_orig_button) {
el_orig_button.title = "Reduce window to its original size and center it";
tools.el.setOnClick(el_orig_button, function() {
el_window.style.width = "";
el_window.style.height = "";
__centerWindow(el_window);
__activateLastWindow(el_window);
});
}
let el_enter_full_tab_button = el_window.querySelector(".window-header .window-button-enter-full-tab");
let el_exit_full_tab_button = el_window.querySelector(".window-button-exit-full-tab");
if (el_enter_full_tab_button && el_exit_full_tab_button) {
el_enter_full_tab_button.title = "Stretch to the entire tab";
tools.el.setOnClick(el_enter_full_tab_button, () => self.setFullTabWindow(el_window, true));
tools.el.setOnClick(el_exit_full_tab_button, () => self.setFullTabWindow(el_window, false));
}
let el_full_screen_button = el_window.querySelector(".window-header .window-button-full-screen");
if (el_full_screen_button && __getFullScreenFunction(el_window)) {
el_full_screen_button.title = "Go to full-screen mode";
tools.el.setOnClick(el_full_screen_button, function() {
__fullScreenWindow(el_window);
el_window.focus(el_window); // Почему-то теряется фокус
__activateLastWindow(el_window);
});
}
}
for (let el_button of $$$("button[data-show-window]")) {
tools.el.setOnClick(el_button, () => self.showWindow($(el_button.getAttribute("data-show-window"))));
}
window.onmouseup = __globalMouseButtonHandler;
window.ontouchend = __globalMouseButtonHandler;
window.addEventListener("focusin", (event) => __focusInOut(event, true));
window.addEventListener("focusout", (event) => __focusInOut(event, false));
window.addEventListener("resize", __organizeWindowsOnBrowserResize);
window.addEventListener("orientationchange", __organizeWindowsOnBrowserResize);
document.onfullscreenchange = __onFullScreenChange;
};
/************************************************************************/
self.copyTextToClipboard = function(text) {
let workaround = function(ex) {
// https://stackoverflow.com/questions/60317969/document-execcommandcopy-not-working-even-though-the-dom-element-is-created
__modalDialog("Info", "Press OK to copy the text to the clipboard", true, false).then(function() {
tools.error("copyTextToClipboard(): navigator.clipboard.writeText() is not working:", ex);
tools.info("copyTextToClipboard(): Trying a workaround...");
let el = document.createElement("textarea");
el.readonly = true;
el.contentEditable = true;
el.style.position = "absolute";
el.style.top = "-1000px";
el.value = text;
document.body.appendChild(el);
// Select the content of the textarea
el.select(); // Ordinary browsers
el.setSelectionRange(0, el.value.length); // iOS
try {
ex = (document.execCommand("copy") ? null : "Unknown error");
} catch (ex) { // eslint-disable-line no-unused-vars
}
// Remove the added textarea again:
document.body.removeChild(el);
if (ex) {
tools.error("copyTextToClipboard(): Workaround failed:", ex);
self.error("Can't copy text to the clipboard", `${ex}`);
}
});
};
if (navigator.clipboard) {
navigator.clipboard.writeText(text).then(function() {
self.info("The text has been copied to the clipboard");
}, function(ex) {
workaround(ex);
});
} else {
workaround("navigator.clipboard is not available");
}
};
self.info = (html, ...args) => __modalCodeDialog("Info", html, args.join("\n"), true, false);
self.error = (html, ...args) => __modalCodeDialog("Error", html, args.join("\n"), true, false);
self.confirm = (html, ...args) => __modalCodeDialog("Question", html, args.join("\n"), true, true);
self.modal = (header, html, ok, cancel) => __modalDialog(header, html, ok, cancel);
var __modalCodeDialog = function(header, html, code, ok, cancel) {
let create_content = function(el_content) {
if (code) {
html += `<br><br><div class="code"><pre style="margin:0px">${tools.escape(code)}</pre></div>`;
}
el_content.innerHTML = html;
};
return __modalDialog(header, create_content, ok, cancel);
};
var __modalDialog = function(header, html, ok, cancel, parent=null) {
let el_active_menu = (document.activeElement && document.activeElement.closest(".menu"));
let el_modal = document.createElement("div");
el_modal.className = "modal";
el_modal.style.visibility = "visible";
let el_window = document.createElement("div");
el_window.className = "modal-window";
el_window.setAttribute("tabindex", "-1");
el_modal.appendChild(el_window);
let el_header = document.createElement("div");
el_header.className = "modal-header";
el_header.innerText = header;
el_window.appendChild(el_header);
let el_content = document.createElement("div");
el_content.className = "modal-content";
el_window.appendChild(el_content);
let el_buttons = document.createElement("div");
el_buttons.classList.add("modal-buttons", "buttons-row");
el_window.appendChild(el_buttons);
let el_cancel_button = null;
let el_ok_button = null;
if (cancel) {
el_cancel_button = document.createElement("button");
el_cancel_button.className = "row100";
el_cancel_button.innerText = "Cancel";
el_buttons.appendChild(el_cancel_button);
}
if (ok) {
el_ok_button = document.createElement("button");
el_ok_button.className = "row100";
el_ok_button.innerText = "OK";
el_buttons.appendChild(el_ok_button);
}
if (ok && cancel) {
el_ok_button.className = "row50";
el_cancel_button.className = "row50";
}
el_window.onkeyup = function(event) {
event.preventDefault();
if (ok && event.code === "Enter") {
el_ok_button.click();
} else if (cancel && event.code === "Escape") {
el_cancel_button.click();
}
};
let promise = null;
if (ok || cancel) {
promise = new Promise(function(resolve) {
function close(retval) {
__closeWindow(el_window);
let index = __windows.indexOf(el_modal);
if (index !== -1) {
__windows.splice(index, 1);
}
if (el_active_menu && el_active_menu.style.visibility === "visible") {
el_active_menu.focus();
} else {
__activateLastWindow(el_modal);
}
resolve(retval);
// Так как resolve() асинхронный, надо выполнить в эвентлупе после него
setTimeout(function() { el_modal.outerHTML = ""; }, 0);
}
if (cancel) {
tools.el.setOnClick(el_cancel_button, () => close(false));
}
if (ok) {
tools.el.setOnClick(el_ok_button, () => close(true));
}
});
}
__windows.push(el_modal);
(parent || document.fullscreenElement || document.body).appendChild(el_modal);
if (typeof html === "function") {
// Это должно быть здесь, потому что элемент должен иметь родителя чтобы существовать
html(el_content, el_ok_button);
} else {
el_content.innerHTML = html;
}
__activateWindow(el_modal);
return promise;
};
self.showWindow = function(el_window, activate=true, center=false) {
let showed = false;
if (!self.isWindowVisible(el_window)) {
center = true;
showed = true;
}
__organizeWindow(el_window, center);
el_window.style.visibility = "visible";
if (activate) {
__activateWindow(el_window);
}
if (el_window.show_hook) {
if (showed) {
el_window.show_hook();
}
}
};
self.isWindowVisible = function(el_window) {
return (window.getComputedStyle(el_window, null).visibility !== "hidden");
};
self.getViewGeometry = function() {
let el_navbar = $("navbar");
return {
"top": (el_navbar ? el_navbar.clientHeight : 0), // Navbar height
"bottom": Math.max(document.documentElement.clientHeight, window.innerHeight || 0),
"left": 0,
"right": Math.max(document.documentElement.clientWidth, window.innerWidth || 0),
};
};
self.closeWindow = function(el_window) {
__closeWindow(el_window);
__activateLastWindow(el_window);
};
self.setFullTabWindow = function(el_window, enabled) {
el_window.classList.toggle("window-full-tab", enabled);
__activateLastWindow(el_window);
let el_navbar = $("navbar");
if (el_navbar) {
tools.hidden.setVisible(el_navbar, !enabled);
}
};
var __closeWindow = function(el_window) {
el_window.focus();
el_window.blur();
el_window.style.visibility = "hidden";
if (el_window.close_hook) {
el_window.close_hook();
}
};
var __toggleMenu = function(el_a) {
let all_hidden = true;
for (let el_button of __menu_buttons) {
let el_menu = el_button.parentElement.querySelector(".menu");
if (el_button === el_a && window.getComputedStyle(el_menu, null).visibility === "hidden") {
let rect = el_menu.getBoundingClientRect();
let offset = self.getViewGeometry().right - (rect.left + el_menu.clientWidth + 2); // + 2 is ugly hack
if (offset < 0) {
el_menu.style.right = "0px";
} else {
el_menu.style.removeProperty("right");
}
el_button.classList.add("menu-button-pressed");
el_menu.style.visibility = "visible";
let el_focus = el_menu.querySelector("[data-focus]");
(el_focus !== null ? el_focus : el_menu).focus();
all_hidden &= false;
} else {
el_button.classList.remove("menu-button-pressed");
el_menu.style.visibility = "hidden";
el_menu.style.removeProperty("right");
}
}
if (all_hidden) {
document.onkeyup = null;
__activateLastWindow();
} else {
document.onkeyup = function(event) {
if (event.code === "Escape") {
event.preventDefault();
__closeAllMenues();
__activateLastWindow();
}
};
}
};
var __closeAllMenues = function() {
document.onkeyup = null;
for (let el_button of __menu_buttons) {
let el_menu = el_button.parentElement.querySelector(".menu");
el_button.classList.remove("menu-button-pressed");
el_menu.style.visibility = "hidden";
el_menu.style.removeProperty("right");
}
};
var __focusInOut = function(event, focus_in) {
let el_parent;
if ((el_parent = event.target.closest(".modal-window")) !== null) {
el_parent.classList.toggle("window-active", focus_in);
} else if ((el_parent = event.target.closest(".window")) !== null) {
el_parent.classList.toggle("window-active", focus_in);
} else if ((el_parent = event.target.closest(".menu")) !== null) {
el_parent.classList.toggle("menu-active", focus_in);
}
tools.debug(`UI: Focus ${focus_in ? "IN" : "OUT"}:`, el_parent);
};
var __globalMouseButtonHandler = function(event) {
if (
event.target.closest
&& !event.target.closest(".menu-button")
&& !event.target.closest(".modal")
) {
for (let el_item = event.target; el_item && el_item !== document; el_item = el_item.parentNode) {
if (el_item.classList.contains("menu")) {
return;
} else if (el_item.hasAttribute("data-force-hide-menu")) {
break;
}
}
__closeAllMenues();
__activateLastWindow();
}
};
var __organizeWindowsOnBrowserResize = function() {
for (let el_window of $$("window")) {
if (el_window.style.visibility === "visible") {
if (tools.browser.is_mobile && el_window.classList.contains("window-resizable")) {
// FIXME: При смене ориентации на мобильном браузере надо сбрасывать
// настройки окна стрима, поэтому тут стоит вот этот костыль
el_window.style.width = "";
el_window.style.height = "";
}
__organizeWindow(el_window);
}
}
};
var __organizeWindow = function(el_window, center=false) {
let view = self.getViewGeometry();
let rect = el_window.getBoundingClientRect();
if (el_window.classList.contains("window-resizable")) {
// При переполнении рабочей области сократить размер окна
if ((rect.bottom - rect.top) > (view.bottom - view.top)) {
let ratio = (rect.bottom - rect.top) / (view.bottom - view.top);
el_window.style.height = view.bottom - view.top + "px";
el_window.style.width = Math.round((rect.right - rect.left) / ratio) + "px";
}
if ((rect.right - rect.left) > (view.right - view.left)) {
el_window.style.width = view.right - view.left + "px";
}
rect = el_window.getBoundingClientRect();
}
if (el_window.hasAttribute("data-centered") || center) {
__centerWindow(el_window);
} else {
if (rect.top <= view.top) {
el_window.style.top = view.top + "px";
} else if (rect.bottom > view.bottom) {
el_window.style.top = view.bottom - rect.height + "px";
}
if (rect.left <= view.left) {
el_window.style.left = view.left + "px";
} else if (rect.right > view.right) {
el_window.style.left = view.right - rect.width + "px";
}
}
};
var __centerWindow = function(el_window) {
let view = self.getViewGeometry();
let rect = el_window.getBoundingClientRect();
el_window.style.top = Math.max(view.top, Math.round((view.bottom - rect.height) / 2)) + "px";
el_window.style.left = Math.round((view.right - rect.width) / 2) + "px";
el_window.setAttribute("data-centered", "");
};
var __activateLastWindow = function(el_except_window=null) {
let el_last_window = null;
if (document.activeElement) {
el_last_window = (document.activeElement.closest(".modal-window") || document.activeElement.closest(".window"));
if (el_last_window && window.getComputedStyle(el_last_window, null).visibility === "hidden") {
el_last_window = null;
}
}
if (!el_last_window || el_last_window === el_except_window) {
let max_z_index = 0;
for (let el_window of __windows) {
let z_index = parseInt(window.getComputedStyle(el_window, null).zIndex) || 0;
let visibility = window.getComputedStyle(el_window, null).visibility;
if (max_z_index < z_index && visibility !== "hidden" && el_window !== el_except_window) {
el_last_window = el_window;
max_z_index = z_index;
}
}
}
if (el_last_window) {
tools.debug("UI: Activating last window:", el_last_window);
__activateWindow(el_last_window);
} else {
tools.debug("UI: No last window to activation");
}
};
var __activateWindow = function(el_window) {
if (window.getComputedStyle(el_window, null).visibility !== "hidden") {
let el_to_focus;
let el_window_contains_focus;
if (el_window.className === "modal") {
el_to_focus = el_window.querySelector(".modal-window");
el_window_contains_focus = (document.activeElement && document.activeElement.closest(".modal-window"));
} else { // .window
el_to_focus = el_window;
el_window_contains_focus = (document.activeElement && document.activeElement.closest(".window"));
}
if (el_window.className !== "modal" && parseInt(el_window.style.zIndex) !== __top_z_index) {
__top_z_index += 1;
el_window.style.zIndex = __top_z_index;
tools.debug("UI: Activated window:", el_window);
}
if (el_window !== el_window_contains_focus) {
el_to_focus.focus();
tools.debug("UI: Focused window:", el_window);
}
}
};
var __makeWindowMovable = function(el_window) {
let el_header = el_window.querySelector(".window-header");
let el_grab = el_window.querySelector(".window-header .window-grab");
if (el_header === null || el_grab === null) {
// Для псевдоокна OCR
return;
}
let prev_pos = {"x": 0, "y": 0};
function startMoving(event) {
// При перетаскивании resizable-окна за правый кран экрана оно ужимается.
// Этот костыль фиксит это.
el_window.style.width = el_window.offsetWidth + "px";
__closeAllMenues();
__activateWindow(el_window);
event = (event || window.event);
event.preventDefault();
if (!event.touches || event.touches.length === 1) {
el_header.classList.add("window-header-grabbed");
prev_pos = getEventPosition(event);
document.onmousemove = doMoving;
document.onmouseup = stopMoving;
document.ontouchmove = doMoving;
document.ontouchend = stopMoving;
}
}
function doMoving(event) {
el_window.removeAttribute("data-centered");
event = (event || window.event);
event.preventDefault();
let event_pos = getEventPosition(event);
let x = prev_pos.x - event_pos.x;
let y = prev_pos.y - event_pos.y;
el_window.style.top = (el_window.offsetTop - y) + "px";
el_window.style.left = (el_window.offsetLeft - x) + "px";
prev_pos = event_pos;
}
function stopMoving() {
el_header.classList.remove("window-header-grabbed");
document.onmousemove = null;
document.onmouseup = null;
document.ontouchmove = null;
document.ontouchend = null;
}
function getEventPosition(event) {
if (event.touches) {
return {"x": event.touches[0].clientX, "y": event.touches[0].clientY};
} else {
return {"x": event.clientX, "y": event.clientY};
}
}
el_window.setAttribute("data-centered", "");
el_window.onmousedown = el_window.ontouchstart = () => __activateWindow(el_window);
el_grab.onmousedown = startMoving;
el_grab.ontouchstart = startMoving;
};
var __onFullScreenChange = function(event) {
let el_window = event.target;
if (!document.fullscreenElement) {
let rect = el_window.before_full_screen;
if (rect) {
el_window.style.width = rect.width + "px";
el_window.style.height = rect.height + "px";
el_window.style.top = rect.top + "px";
el_window.style.left = rect.left + "px";
}
}
};
var __fullScreenWindow = function(el_window) {
el_window.before_full_screen = el_window.getBoundingClientRect();
__getFullScreenFunction(el_window).call(el_window);
if (navigator.keyboard && navigator.keyboard.lock) {
navigator.keyboard.lock();
} else {
let msg = (
"Shortcuts like Alt+Tab, Ctrl+W, Ctrl+N might not be captured.<br>"
+ "For best keyboard handling use any browser with<br><a target=\"_blank\""
+ " href=\"https://developer.mozilla.org/en-US/docs/Web"
+ "/API/Keyboard_API#Browser_compatibility\">keyboard lock support from this list</a>.<br><br>"
+ "In Chrome use HTTPS and enable <i>system-keyboard-lock</i><br>"
+ "by putting at URL <i>chrome://flags/#system-keyboard-lock</i>"
);
__modalDialog("Keyboard lock is unsupported", msg, true, false, el_window);
}
};
var __maximizeWindow = function(el_window) {
let el_navbar = $("navbar");
let vertical_offset = (el_navbar ? el_navbar.offsetHeight : 0);
el_window.style.left = "0px";
el_window.style.top = vertical_offset + "px";
el_window.style.width = window.innerWidth + "px";
el_window.style.height = window.innerHeight - vertical_offset + "px";
};
var __getFullScreenFunction = function(el_window) {
if (el_window.requestFullscreen) {
return el_window.requestFullscreen;
} else if (el_window.webkitRequestFullscreen) {
return el_window.webkitRequestFullscreen;
} else if (el_window.mozRequestFullscreen) {
return el_window.mozRequestFullscreen;
}
return null;
};
__init__();
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 339 KiB

View File

@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Creator: CorelDRAW 2020 (64-Bit) -->
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" width="251.348mm" height="216.566mm" version="1.1" style="shape-rendering:geometricPrecision; text-rendering:geometricPrecision; image-rendering:optimizeQuality; fill-rule:evenodd; clip-rule:evenodd"
viewBox="0 0 25755.01 22190.97"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:xodm="http://www.corel.com/coreldraw/odm/2003">
<defs>
<style type="text/css">
<![CDATA[
.str0 {stroke:white;stroke-width:717.27;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:22.9256}
.fil0 {fill:black;fill-rule:nonzero}
]]>
</style>
</defs>
<g id="Layer_x0020_1">
<metadata id="CorelCorpID_0Corel-Layer"/>
<path class="fil0 str0" d="M24632.75 454.2c-173.5,97.6 -607.29,216.89 -889.24,249.43 -97.6,10.84 -238.57,25.3 -307.26,36.15 -896.48,119.29 -1883.32,133.75 -9543.13,148.21 -3867.85,3.62 -7128.42,18.08 -7247.7,28.92 -769.96,61.45 -791.64,65.07 -1247.11,130.13 -404.86,61.45 -394.02,57.83 -780.8,159.05 -1167.59,310.88 -2082.14,874.78 -2815.95,1738.73 -269.82,317.99 -597.52,833.51 -798.88,1229.03 -325.33,647.05 -560.3,1384.48 -614.52,1952 -36.15,365.1 -39.77,994.08 -7.23,1120.59 14.46,57.83 36.15,166.28 46.99,234.97 10.84,68.68 57.83,187.97 101.22,263.88 195.2,350.63 516.92,281.96 842.25,-184.36 224.12,-318.1 733.81,-820.57 1026.61,-1012.15 1105.15,-729.69 2578.47,-899.71 3885.93,-1004.94 473.54,-36.15 1476.2,-29.37 1572.44,-28.92 -21.68,694.56 -807.25,4104.96 -943.47,4623.35 -484.38,1825.49 -961.54,3141.27 -1630.29,4525.75 -596.44,1229.03 -1286.88,2100.21 -2197.81,2754.5 -495.24,357.87 -741.03,639.83 -950.7,1080.84 -253.65,531.06 -121.23,1611.48 133.75,2139.98 289.18,600.06 983.24,1030.23 1865.24,1153.13 916.71,130.97 1836.24,-47.23 2515.91,-712.12 741.03,-730.19 1482.08,-2436.39 1836.33,-4218.5 283.05,-1383.75 629.98,-2871.28 831.4,-4247.41 144.41,-914.48 323.41,-1831.43 469.93,-2747.27 300.64,-1403.49 465.29,-2909.15 704.89,-4337.77 7.24,-50.67 5643.66,-36.15 6430.75,-36.15 -204.41,490.58 -394.1,1023.02 -610.9,1500.15 -260.27,589.22 -1453.16,3799.17 -1670.04,4500.45 -86.76,271.11 -296.42,1026.61 -332.57,1189.27 -424.51,1867.72 -610.09,3620.83 -325.33,5516.22 168.28,1144.23 699.2,2131.08 1633.89,2823.17 886.03,661.31 1565.81,769.67 2631.59,791.64 1651.97,28.92 3058.14,-422.93 4410.08,-1413.39 621.75,-455.47 1449.54,-1398.94 1821.87,-2074.9 281.96,-513.3 534.99,-1044.69 704.89,-1471.24 169.9,-426.55 206.04,-556.68 209.66,-759.11 18.08,-610.9 -227.73,-867.56 -715.73,-751.89 -242.19,57.83 -368.71,140.97 -625.37,430.17 -704.89,788.04 -1290.49,1236.27 -1854.4,1424.24 -397.63,133.75 -509.69,155.43 -831.41,169.9 -332.57,14.46 -542.23,-32.53 -845.87,-180.74 -647.05,-318.1 -1163.97,-1156.74 -1384.48,-2259.26 -119.29,-589.22 -144.59,-1001.3 -130.13,-2223.11 10.84,-675.96 21.69,-1294.09 32.53,-1373.63 7.23,-79.52 25.3,-307.26 39.77,-506.08 10.84,-198.82 28.92,-412.09 36.15,-469.93 122.91,-860.47 183.05,-1649.91 361.49,-2512.3 86.94,-405.88 453.1,-2267.5 538.61,-2338.79 7.23,-7.23 328.95,-21.69 712.12,-28.92 657.9,-14.46 1344.71,-46.99 1474.84,-72.3 32.53,-3.62 130.13,-21.69 220.5,-36.15 892.86,-133.75 1348.33,-459.08 1850.79,-1312.18 347.03,-585.6 722.97,-1662.82 809.72,-2302.64 32.97,-307.88 72.29,-540.13 72.29,-860.33 -3.36,-319.17 -93.73,-720.19 -524.12,-437.39z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.4 KiB

View File

@@ -0,0 +1,15 @@
{
"name": "One-KVM",
"short_name": "One-KVM",
"start_url": "/",
"icons": [
{
"src": "/share/android-chrome-192x192.png",
"sizes": "192x192",
"type": "image/png"
}
],
"theme_color": "#ffffff",
"background_color": "#ffffff",
"display": "standalone"
}

View File

@@ -0,0 +1,20 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Creator: CorelDRAW 2020 (64-Bit) -->
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" width="251.348mm" height="216.566mm" version="1.1" style="shape-rendering:geometricPrecision; text-rendering:geometricPrecision; image-rendering:optimizeQuality; fill-rule:evenodd; clip-rule:evenodd"
viewBox="0 0 25755.01 22190.97"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:xodm="http://www.corel.com/coreldraw/odm/2003">
<defs>
<style type="text/css">
<![CDATA[
.str0 {stroke:white;stroke-width:717.27;stroke-linecap:round;stroke-linejoin:round;stroke-miterlimit:22.9256}
.fil0 {fill:black;fill-rule:nonzero}
]]>
</style>
</defs>
<g id="Layer_x0020_1">
<metadata id="CorelCorpID_0Corel-Layer"/>
<path class="fil0 str0" d="M24632.75 454.2c-173.5,97.6 -607.29,216.89 -889.24,249.43 -97.6,10.84 -238.57,25.3 -307.26,36.15 -896.48,119.29 -1883.32,133.75 -9543.13,148.21 -3867.85,3.62 -7128.42,18.08 -7247.7,28.92 -769.96,61.45 -791.64,65.07 -1247.11,130.13 -404.86,61.45 -394.02,57.83 -780.8,159.05 -1167.59,310.88 -2082.14,874.78 -2815.95,1738.73 -269.82,317.99 -597.52,833.51 -798.88,1229.03 -325.33,647.05 -560.3,1384.48 -614.52,1952 -36.15,365.1 -39.77,994.08 -7.23,1120.59 14.46,57.83 36.15,166.28 46.99,234.97 10.84,68.68 57.83,187.97 101.22,263.88 195.2,350.63 516.92,281.96 842.25,-184.36 224.12,-318.1 733.81,-820.57 1026.61,-1012.15 1105.15,-729.69 2578.47,-899.71 3885.93,-1004.94 473.54,-36.15 1476.2,-29.37 1572.44,-28.92 -21.68,694.56 -807.25,4104.96 -943.47,4623.35 -484.38,1825.49 -961.54,3141.27 -1630.29,4525.75 -596.44,1229.03 -1286.88,2100.21 -2197.81,2754.5 -495.24,357.87 -741.03,639.83 -950.7,1080.84 -253.65,531.06 -121.23,1611.48 133.75,2139.98 289.18,600.06 983.24,1030.23 1865.24,1153.13 916.71,130.97 1836.24,-47.23 2515.91,-712.12 741.03,-730.19 1482.08,-2436.39 1836.33,-4218.5 283.05,-1383.75 629.98,-2871.28 831.4,-4247.41 144.41,-914.48 323.41,-1831.43 469.93,-2747.27 300.64,-1403.49 465.29,-2909.15 704.89,-4337.77 7.24,-50.67 5643.66,-36.15 6430.75,-36.15 -204.41,490.58 -394.1,1023.02 -610.9,1500.15 -260.27,589.22 -1453.16,3799.17 -1670.04,4500.45 -86.76,271.11 -296.42,1026.61 -332.57,1189.27 -424.51,1867.72 -610.09,3620.83 -325.33,5516.22 168.28,1144.23 699.2,2131.08 1633.89,2823.17 886.03,661.31 1565.81,769.67 2631.59,791.64 1651.97,28.92 3058.14,-422.93 4410.08,-1413.39 621.75,-455.47 1449.54,-1398.94 1821.87,-2074.9 281.96,-513.3 534.99,-1044.69 704.89,-1471.24 169.9,-426.55 206.04,-556.68 209.66,-759.11 18.08,-610.9 -227.73,-867.56 -715.73,-751.89 -242.19,57.83 -368.71,140.97 -625.37,430.17 -704.89,788.04 -1290.49,1236.27 -1854.4,1424.24 -397.63,133.75 -509.69,155.43 -831.41,169.9 -332.57,14.46 -542.23,-32.53 -845.87,-180.74 -647.05,-318.1 -1163.97,-1156.74 -1384.48,-2259.26 -119.29,-589.22 -144.59,-1001.3 -130.13,-2223.11 10.84,-675.96 21.69,-1294.09 32.53,-1373.63 7.23,-79.52 25.3,-307.26 39.77,-506.08 10.84,-198.82 28.92,-412.09 36.15,-469.93 122.91,-860.47 183.05,-1649.91 361.49,-2512.3 86.94,-405.88 453.1,-2267.5 538.61,-2338.79 7.23,-7.23 328.95,-21.69 712.12,-28.92 657.9,-14.46 1344.71,-46.99 1474.84,-72.3 32.53,-3.62 130.13,-21.69 220.5,-36.15 892.86,-133.75 1348.33,-459.08 1850.79,-1312.18 347.03,-585.6 722.97,-1662.82 809.72,-2302.64 32.97,-307.88 72.29,-540.13 72.29,-860.33 -3.36,-319.17 -93.73,-720.19 -524.12,-437.39z"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.4 KiB

View File

@@ -0,0 +1,7 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48">
<circle cy="24" cx="24" r="24" fill="#36c"/>
<g fill="#fff">
<circle cx="24" cy="11.6" r="4.7"/>
<path d="m17.4 18.8v2.15h1.13c2.26 0 2.26 1.38 2.26 1.38v15.1s0 1.38-2.26 1.38h-1.13v2.08h14.2v-2.08h-1.13c-2.26 0-2.26-1.38-2.26-1.38v-18.6"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 328 B

View File

@@ -0,0 +1,227 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
version="1.1"
id="Layer_1"
x="0px"
y="0px"
viewBox="0 0 512.015 512.015"
style="enable-background:new 0 0 512.015 512.015;"
xml:space="preserve"
sodipodi:docname="server-svgrepo-com3.svg"
inkscape:version="0.92.4 (5da689c313, 2019-01-14)"><metadata
id="metadata3929"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
id="defs3927">
</defs><sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1920"
inkscape:window-height="1018"
id="namedview3925"
showgrid="false"
inkscape:zoom="1.6620606"
inkscape:cx="147.70821"
inkscape:cy="256.00751"
inkscape:window-x="-8"
inkscape:window-y="-8"
inkscape:window-maximized="1"
inkscape:current-layer="Layer_1" />
<g
id="g3826">
<g
id="g3824">
<circle
cx="69.818"
cy="69.826"
r="11.636"
id="circle3822"
style="stroke-width:20;stroke-miterlimit:4;stroke-dasharray:none;stroke:#000000;stroke-opacity:1;fill:#000000;fill-opacity:1" />
</g>
</g>
<g
id="g3832"
transform="translate(20)">
<g
id="g3830">
<circle
cx="116.364"
cy="69.825996"
r="11.636"
id="circle3828"
style="fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:20;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
</g>
</g>
<g
style="fill:#000000;fill-opacity:1"
id="g3836">
<path
inkscape:connector-curvature="0"
style="fill:#000000;fill-opacity:1"
id="path3834"
d="m 290.909,465.462 h -2.141 C 283.951,451.952 271.151,442.189 256,442.189 H 232.727 V 407.28 c 0,-6.423 -5.213,-11.636 -11.636,-11.636 -6.423,0 -11.636,5.213 -11.636,11.636 v 34.909 h -23.273 c -15.151,0 -27.951,9.763 -32.768,23.273 H 81.455 c -6.423,0 -11.636,5.213 -11.636,11.636 0,6.423 5.213,11.636 11.636,11.636 h 71.959 c 4.817,13.51 17.617,23.273 32.768,23.273 H 256 c 15.151,0 27.951,-9.763 32.768,-23.273 h 2.153 c 6.423,0 11.625,-5.213 11.625,-11.636 -10e-4,-6.423 -5.214,-11.636 -11.637,-11.636 z"
sodipodi:nodetypes="scscssscscssscsscscs" />
</g>
<g
id="g3844">
<g
id="g3842">
<path
d="M372.364,58.189H209.455c-6.423,0-11.636,5.213-11.636,11.636s5.213,11.636,11.636,11.636h162.909 c6.435,0,11.636-5.213,11.636-11.636S378.799,58.189,372.364,58.189z"
id="path3840" />
</g>
</g>
<g
id="g3850">
<g
id="g3848">
<circle
cx="69.818"
cy="302.553"
r="11.636"
id="circle3846"
style="stroke-width:20;stroke-miterlimit:4;stroke-dasharray:none;stroke:#000000;stroke-opacity:1;fill:#000000;fill-opacity:1" />
</g>
</g>
<g
id="g3856"
transform="translate(20)">
<g
id="g3854">
<circle
cx="116.364"
cy="302.55301"
r="11.636"
id="circle3852"
style="fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:20;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
</g>
</g>
<g
id="g3862">
<g
id="g3860">
<path
d="M325.818,290.917H209.455c-6.423,0-11.636,5.213-11.636,11.636s5.213,11.636,11.636,11.636h116.364 c6.435,0,11.636-5.213,11.636-11.636S332.253,290.917,325.818,290.917z"
id="path3858" />
</g>
</g>
<g
id="g3868">
<g
id="g3866">
<circle
cx="69.818"
cy="186.189"
r="11.636"
id="circle3864"
style="stroke-width:20;stroke-miterlimit:4;stroke-dasharray:none;stroke:#000000;stroke-opacity:1;fill:#000000;fill-opacity:1" />
</g>
</g>
<g
id="g3874"
style="stroke:#000000;stroke-opacity:1"
transform="translate(20)">
<g
id="g3872"
style="stroke:#000000;stroke-opacity:1">
<circle
cx="116.364"
cy="186.189"
r="11.636"
id="circle3870"
style="fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:20;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1" />
</g>
</g>
<g
id="g3880">
<g
id="g3878">
<path
d="M372.364,174.553H209.455c-6.423,0-11.636,5.213-11.636,11.636s5.213,11.636,11.636,11.636h162.909 c6.435,0,11.636-5.213,11.636-11.636S378.799,174.553,372.364,174.553z"
id="path3876" />
</g>
</g>
<g
id="g3886">
<g
id="g3884">
<path
d="M430.545,93.098c6.435,0,11.636-5.213,11.636-11.636V34.917c0-19.247-15.663-34.909-34.909-34.909H34.909 C15.663,0.007,0,15.67,0,34.917v93.091v0.012v69.807c0,6.423,5.213,11.636,11.636,11.636s11.636-5.213,11.636-11.636v-58.17 l395.636-0.012v93.091l-407.273,0.012C5.213,232.746,0,237.959,0,244.383v93.079c0,19.247,15.663,34.909,34.909,34.909h232.727 c6.423,0,11.636-5.213,11.636-11.636s-5.213-11.636-11.636-11.636H34.909c-6.412,0-11.636-5.225-11.636-11.636v-81.443 l395.636-0.012v46.545c0,6.423,5.201,11.636,11.636,11.636s11.636-5.213,11.636-11.636v-58.182V128.007 c0-3.084-1.222-6.051-3.409-8.227c-2.188-2.176-5.132-3.409-8.227-3.409l-407.273,0.012V34.917 c0-6.412,5.225-11.636,11.636-11.636h372.364c6.423,0,11.636,5.225,11.636,11.636v46.545 C418.909,87.885,424.111,93.098,430.545,93.098z"
id="path3882" />
</g>
</g>
<path
d="m 459.811,414.553 c 9.367,0 18.723,-3.561 25.856,-10.682 0.012,-0.012 0.023,-0.035 0.035,-0.058 0.012,-0.012 0.012,-0.012 0.023,-0.012 l 22.935,-23.273 c 4.515,-4.573 4.468,-11.939 -0.116,-16.454 -4.596,-4.515 -11.951,-4.468 -16.454,0.128 l -22.842,23.18 c -0.012,0.012 -0.023,0.012 -0.035,0.023 -5.178,5.178 -13.615,5.178 -18.793,0 -2.513,-2.502 -3.898,-5.841 -3.898,-9.391 0,-3.549 1.385,-6.889 3.898,-9.391 l 23.215,-22.877 c 4.585,-4.515 4.62,-11.881 0.116,-16.454 -4.492,-4.585 -11.857,-4.62 -16.454,-0.128 l -23.273,22.935 c -0.012,0.012 -0.012,0.023 -0.023,0.035 -0.012,0.012 -0.023,0.012 -0.035,0.023 -6.912,6.912 -10.717,16.093 -10.717,25.856 0,5.737 1.455,11.194 3.933,16.175 l -15.139,15.139 c -3.584,-1.28 -7.389,-1.827 -11.194,-1.676 l -23.668,-23.645 15.046,-15.046 c 4.55,-4.55 4.55,-11.904 0,-16.454 l -23.273,-23.273 c -4.55,-4.55 -11.904,-4.55 -16.454,0 l -46.545,46.545 c -3.119,3.119 -4.201,7.727 -2.804,11.904 l 11.636,34.909 c 1.268,3.828 4.433,6.714 8.367,7.645 0.884,0.209 1.78,0.314 2.665,0.314 3.049,0 6.028,-1.199 8.227,-3.409 l 26.682,-26.682 18.164,18.164 -44.719,44.719 c -5.388,5.388 -8.355,12.544 -8.355,20.154 0,7.61 2.967,14.778 8.355,20.154 5.388,5.388 12.544,8.355 20.154,8.355 7.61,0 14.778,-2.967 20.143,-8.355 l 44.73,-44.719 49.664,49.664 c 2.269,2.269 5.248,3.409 8.227,3.409 2.979,0 5.958,-1.14 8.227,-3.409 4.55,-4.55 4.55,-11.904 0,-16.454 l -54.912,-54.912 c 0.023,-0.489 0.14,-0.954 0.14,-1.455 0,-3.526 -0.698,-6.935 -1.92,-10.135 l 15.023,-15.023 c 5.096,2.529 10.6,3.937 16.162,3.937 z"
id="path3888"
inkscape:connector-curvature="0"
sodipodi:nodetypes="scccccccccscccccccsccccssssscccscccsscsccsssscsccs" />
<g
id="g3894">
</g>
<g
id="g3896">
</g>
<g
id="g3898">
</g>
<g
id="g3900">
</g>
<g
id="g3902">
</g>
<g
id="g3904">
</g>
<g
id="g3906">
</g>
<g
id="g3908">
</g>
<g
id="g3910">
</g>
<g
id="g3912">
</g>
<g
id="g3914">
</g>
<g
id="g3916">
</g>
<g
id="g3918">
</g>
<g
id="g3920">
</g>
<g
id="g3922">
</g>
<path
style="fill:#000000;fill-opacity:1;stroke-width:0.60166276"
d=""
id="path4738"
inkscape:connector-curvature="0" /></svg>

After

Width:  |  Height:  |  Size: 7.6 KiB

View File

@@ -0,0 +1,91 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 512.004 512.004" style="enable-background:new 0 0 512.004 512.004;" xml:space="preserve">
<g>
<g>
<path d="M444.326,0H67.754C50.346,0,36.19,14.16,36.19,31.56L36.106,284.5c0,17.404,14.156,31.5,31.556,31.5h124.336
c-0.008,0,0,0.18,0,0.212V348H168.27c-2.208,0-4.272,2.18-4.272,4.388V368c0,2.208,2.064,4,4.272,4h174.78
c2.212,0,4.948-1.792,4.948-4v-15.612c0-2.208-2.736-4.388-4.944-4.388h-23.056v-31.788c0-0.032-0.256-0.212-0.264-0.212h124.508
c17.408,0,31.564-14.096,31.564-31.496l0.092-252.972C475.898,14.128,461.734,0,444.326,0z M255.934,292.392
c-6.616,0-12-5.38-12-11.996c0-6.616,5.384-11.996,12-11.996c6.608,0,11.992,5.38,11.992,11.996
C267.926,287.012,262.546,292.392,255.934,292.392z M447.998,248h-384V28h328.008h38.296h17.696V248z"/>
</g>
</g>
<g>
<g>
<path d="M323.998,403.884c0-6.564-5.32-11.884-11.884-11.884H83.882c-6.564,0-11.884,5.32-11.884,11.884v96.236
c0,6.564,5.32,11.884,11.884,11.884h228.236c6.564,0,11.884-5.32,11.884-11.884v-96.236H323.998z M267.778,408h4.632
c4.42,0,8,3.58,8,8c0,4.416-3.58,8-8,8h-4.632c-4.42,0-8-3.584-8-8C259.778,411.58,263.358,408,267.778,408z M195.786,408h4.116
c4.42,0,8,3.58,8,8c0,4.416-3.58,8-8,8h-4.116c-4.42,0-8-3.584-8-8C187.786,411.58,191.366,408,195.786,408z M195.786,432h4.116
c4.42,0,8,3.584,8,8c0,4.42-3.58,8-8,8h-4.116c-4.42,0-8-3.58-8-8C187.786,435.584,191.366,432,195.786,432z M195.786,456h4.116
c4.42,0,8,3.584,8,8c0,4.416-3.58,8-8,8h-4.116c-4.42,0-8-3.584-8-8C187.786,459.584,191.366,456,195.786,456z M171.786,408h4.4
c4.416,0,8,3.58,8,8c0,4.416-3.584,8-8,8h-4.4c-4.416,0-8-3.584-8-8C163.786,411.58,167.374,408,171.786,408z M171.786,432h4.4
c4.416,0,8,3.584,8,8c0,4.42-3.584,8-8,8h-4.4c-4.416,0-8-3.58-8-8C163.786,435.584,167.374,432,171.786,432z M171.786,456h4.4
c4.416,0,8,3.584,8,8c0,4.416-3.584,8-8,8h-4.4c-4.416,0-8-3.584-8-8C163.786,459.584,167.374,456,171.786,456z M99.794,408h4.108
c4.416,0,8,3.58,8,8c0,4.416-3.584,8-8,8h-4.108c-4.416,0-8-3.584-8-8C91.794,411.58,95.382,408,99.794,408z M99.794,432h4.108
c4.416,0,8,3.584,8,8c0,4.42-3.584,8-8,8h-4.108c-4.416,0-8-3.58-8-8C91.794,435.584,95.382,432,99.794,432z M103.906,496h-4.108
c-4.416,0-8-3.584-8-8c0-4.416,3.584-8,8-8h4.108c4.416,0,8,3.584,8,8C111.906,492.416,108.318,496,103.906,496z M127.898,496
h-4.1c-4.42,0-8-3.584-8-8c0-4.416,3.58-8,8-8h4.1c4.42,0,8,3.584,8,8C135.898,492.416,132.318,496,127.898,496z M127.898,472
h-28.1c-4.416,0-8-3.584-8-8c0-4.416,3.584-8,8-8h28.1c4.42,0,8,3.584,8,8C135.898,468.416,132.318,472,127.898,472z M127.898,448
h-4.1c-4.42,0-8-3.58-8-8c0-4.416,3.58-8,8-8h4.1c4.42,0,8,3.584,8,8C135.898,444.42,132.318,448,127.898,448z M127.898,424h-4.1
c-4.42,0-8-3.584-8-8c0-4.42,3.58-8,8-8h4.1c4.42,0,8,3.58,8,8C135.898,420.416,132.318,424,127.898,424z M151.898,496h-4.1
c-4.42,0-8-3.584-8-8c0-4.416,3.58-8,8-8h4.1c4.416,0,8,3.584,8,8C159.898,492.416,156.31,496,151.898,496z M151.898,472h-4.1
c-4.42,0-8-3.584-8-8c0-4.416,3.58-8,8-8h4.1c4.416,0,8,3.584,8,8C159.898,468.416,156.31,472,151.898,472z M151.898,448h-4.1
c-4.42,0-8-3.58-8-8c0-4.416,3.58-8,8-8h4.1c4.416,0,8,3.584,8,8C159.898,444.42,156.31,448,151.898,448z M151.898,424h-4.1
c-4.42,0-8-3.584-8-8c0-4.42,3.58-8,8-8h4.1c4.416,0,8,3.58,8,8C159.898,420.416,156.31,424,151.898,424z M224.246,496h-52.46
c-4.416,0-8-3.584-8-8c0-4.416,3.584-8,8-8h52.46c4.416,0,8,3.584,8,8C232.246,492.416,228.662,496,224.246,496z M224.246,472
h-4.46c-4.42,0-8-3.584-8-8c0-4.416,3.58-8,8-8h4.46c4.416,0,8,3.584,8,8C232.246,468.416,228.662,472,224.246,472z M224.246,448
h-4.46c-4.42,0-8-3.58-8-8c0-4.416,3.58-8,8-8h4.46c4.416,0,8,3.584,8,8C232.246,444.42,228.662,448,224.246,448z M224.246,424
h-4.46c-4.42,0-8-3.584-8-8c0-4.42,3.58-8,8-8h4.46c4.416,0,8,3.58,8,8C232.246,420.416,228.662,424,224.246,424z M248.578,496
h-4.796c-4.416,0-8-3.584-8-8c0-4.416,3.584-8,8-8h4.796c4.42,0,8,3.584,8,8C256.578,492.416,252.998,496,248.578,496z
M248.578,472h-4.796c-4.416,0-8-3.584-8-8c0-4.416,3.584-8,8-8h4.796c4.42,0,8,3.584,8,8
C256.578,468.416,252.998,472,248.578,472z M248.578,448h-4.796c-4.416,0-8-3.58-8-8c0-4.416,3.584-8,8-8h4.796
c4.42,0,8,3.584,8,8C256.578,444.42,252.998,448,248.578,448z M248.578,424h-4.796c-4.416,0-8-3.584-8-8c0-4.42,3.584-8,8-8h4.796
c4.42,0,8,3.58,8,8C256.578,420.416,252.998,424,248.578,424z M267.778,432h4.632c4.42,0,8,3.584,8,8c0,4.42-3.58,8-8,8h-4.632
c-4.42,0-8-3.58-8-8C259.778,435.584,263.358,432,267.778,432z M272.414,496h-4.632c-4.42,0-8-3.584-8-8c0-4.416,3.58-8,8-8h4.632
c4.42,0,8,3.584,8,8C280.414,492.416,276.834,496,272.414,496z M295.882,496h-4.1c-4.42,0-8-3.584-8-8c0-4.416,3.58-8,8-8h4.1
c4.42,0,8,3.584,8,8C303.882,492.416,300.302,496,295.882,496z M295.882,472h-28.1c-4.42,0-8-3.584-8-8c0-4.42,3.58-8,8-8h28.1
c4.42,0,8,3.58,8,8C303.882,468.416,300.302,472,295.882,472z M296.622,448h-4.844c-4.42,0-8-3.58-8-8c0-4.416,3.58-8,8-8h4.844
c4.416,0,8,3.584,8,8C304.622,444.42,301.038,448,296.622,448z M296.622,424h-4.844c-4.42,0-8-3.584-8-8c0-4.42,3.58-8,8-8h4.844
c4.416,0,8,3.58,8,8C304.622,420.416,301.038,424,296.622,424z"/>
</g>
</g>
<g>
<g>
<path d="M394.442,400h-6.188c-18.14,0-32.256,15.26-32.256,34.092v44.316c0,18.836,14.116,33.588,32.256,33.588h6.188
c18.516,0,33.556-15.068,33.556-33.588v-44.316C427.998,415.576,412.958,400,394.442,400z M399.998,432.336c0,4.416-3.584,8-8,8
c-4.42,0-8-3.584-8-8v-8.668c0-4.42,3.58-8,8-8c4.416,0,8,3.58,8,8V432.336z"/>
</g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 5.8 KiB

View File

@@ -0,0 +1,82 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="16.933332mm"
height="16.933332mm"
viewBox="0 0 16.933332 16.933332"
version="1.1"
id="svg8"
sodipodi:docname="hdd.svg"
inkscape:version="0.92.2 2405546, 2018-03-11">
<defs
id="defs2" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="5.6"
inkscape:cx="13.28418"
inkscape:cy="14.886884"
inkscape:document-units="mm"
inkscape:current-layer="layer1"
showgrid="false"
fit-margin-top="0"
fit-margin-left="0"
fit-margin-right="0"
fit-margin-bottom="0"
inkscape:window-width="1920"
inkscape:window-height="1020"
inkscape:window-x="0"
inkscape:window-y="30"
inkscape:window-maximized="1" />
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-107.87666,-24.618942)">
<flowRoot
xml:space="preserve"
id="flowRoot5064"
style="fill:black;stroke:none;stroke-opacity:1;stroke-width:1px;stroke-linejoin:miter;stroke-linecap:butt;fill-opacity:1;font-family:Adobe Garamond Pro;font-style:normal;font-weight:bold;font-size:10px;line-height:125%;letter-spacing:0px;word-spacing:0px;-inkscape-font-specification:Adobe Garamond Pro Bold;font-stretch:normal;font-variant:normal;text-anchor:start;text-align:start;writing-mode:lr"><flowRegion
id="flowRegion5066"><rect
id="rect5068"
width="50.892857"
height="43.57143"
x="-72.321426"
y="5.4285674" /></flowRegion><flowPara
id="flowPara5070"></flowPara></flowRoot> <g
aria-label="🖴"
transform="matrix(1.8649046,0,0,1.8649046,248.12652,14.45492)"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:10px;line-height:125%;font-family:'Adobe Garamond Pro';-inkscape-font-specification:'Adobe Garamond Pro Bold';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.141875px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
id="flowRoot5072">
<path
d="m -75.064844,9.6551562 1.16,-2.35 q 0.07,-0.1299999 0.14,-0.24 0.08,-0.12 0.24,-0.12 h 5.72 q 0.16,0 0.23,0.12 0.08,0.1100001 0.15,0.24 l 1.16,2.35 z m 4.4,-0.4 q 0.62,0 1.12,-0.1399999 0.51,-0.14 0.8,-0.37 0.3,-0.23 0.3,-0.5100001 0,-0.28 -0.3,-0.5 -0.29,-0.23 -0.8,-0.36 -0.5,-0.14 -1.12,-0.14 -0.8,0 -1.41,0.23 -0.6,0.23 -0.77,0.5800001 l 1.32,0.01 -0.03,0.21 -1.31,0.11 q 0.07,0.24 0.37,0.44 0.31,0.2 0.78,0.32 0.48,0.1199999 1.05,0.1199999 z m 0,-0.7899999 q -0.24,0 -0.42,-0.08 -0.17,-0.09 -0.17,-0.2 0,-0.1100001 0.17,-0.1900001 0.18,-0.08 0.42,-0.08 0.25,0 0.43,0.08 0.18,0.08 0.18,0.1900001 0,0.11 -0.18,0.2 -0.18,0.08 -0.43,0.08 z m -4.02,4.5699997 q -0.22,0 -0.37,-0.15 -0.15,-0.16 -0.15,-0.37 V 9.9851562 h 9.08 v 2.5299998 q 0,0.21 -0.15,0.37 -0.14,0.15 -0.37,0.15 z m -0.05,-2.05 h 8.14 v -0.47 h -8.14 z m 7.8,1.08 q 0.13,0 0.21,-0.09 0.08,-0.09 0.08,-0.22 0,-0.11 -0.08,-0.2 -0.08,-0.09 -0.21,-0.09 -0.13,0 -0.22,0.09 -0.09,0.09 -0.09,0.2 0,0.13 0.09,0.22 0.09,0.09 0.22,0.09 z"
style="stroke-width:0.141875px"
id="path5080"
inkscape:connector-curvature="0" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 4.1 KiB

View File

@@ -0,0 +1,71 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="16.933332mm"
height="16.933332mm"
viewBox="0 0 16.933332 16.933332"
version="1.1"
id="svg8"
sodipodi:docname="power.svg"
inkscape:version="0.92.2 2405546, 2018-03-11">
<defs
id="defs2" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="5.6"
inkscape:cx="13.28418"
inkscape:cy="14.886884"
inkscape:document-units="mm"
inkscape:current-layer="layer1"
showgrid="false"
fit-margin-top="0"
fit-margin-left="0"
fit-margin-right="0"
fit-margin-bottom="0"
inkscape:window-width="1920"
inkscape:window-height="1020"
inkscape:window-x="0"
inkscape:window-y="30"
inkscape:window-maximized="1" />
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-107.87666,-24.618942)">
<g
aria-label="⏻"
transform="matrix(1.9608076,0,0,1.9608076,269.69365,18.4972)"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:10px;line-height:125%;font-family:'Adobe Garamond Pro';-inkscape-font-specification:'Adobe Garamond Pro Bold';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.21589744;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="flowRoot4958">
<path
d="M -78.747734,7.26 V 3.23 h 1.04 v 4.03 z m 0.54,4.39 q -0.87,0 -1.6,-0.3 -0.73,-0.29 -1.27,-0.82 -0.54,-0.53 -0.84,-1.24 -0.3,-0.72 -0.3,-1.57 0,-0.96 0.4,-1.84 0.4,-0.89 1.18,-1.45 l 0.54,0.7 q -0.43,0.29 -0.71,0.72 -0.27,0.43 -0.41,0.92 -0.13,0.49 -0.13,0.95 0,0.64 0.23,1.2 0.23,0.56 0.65,0.98 0.43,0.42 1,0.66 0.57,0.23 1.25,0.23 0.69,0 1.26,-0.23 0.58,-0.24 1,-0.66 0.42,-0.42 0.65,-0.98 0.24,-0.56 0.24,-1.2 0,-0.46 -0.14,-0.95 -0.13,-0.49 -0.41,-0.92 -0.27,-0.43 -0.7,-0.72 l 0.54,-0.7 q 0.78,0.56 1.18,1.45 0.4,0.88 0.4,1.84 0,0.85 -0.3,1.57 -0.3,0.71 -0.84,1.24 -0.54,0.53 -1.28,0.82 -0.73,0.3 -1.59,0.3 z"
style="stroke:#000000;stroke-width:0.21589744;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="path4998" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.1 KiB

View File

@@ -0,0 +1,128 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
height="481.1192"
width="481.1192"
inkscape:version="1.0 (4035a4fb49, 2020-05-01)"
sodipodi:docname="led-circle.svg"
xml:space="preserve"
viewBox="0 0 481.02523 481.1192"
y="0px"
x="0px"
id="Layer_1"
version="1.1"><metadata
id="metadata85"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
id="defs83" /><sodipodi:namedview
fit-margin-bottom="0"
fit-margin-right="0"
fit-margin-left="0"
fit-margin-top="0"
lock-margins="false"
inkscape:document-rotation="0"
inkscape:current-layer="Layer_1"
inkscape:window-maximized="1"
inkscape:window-y="28"
inkscape:window-x="0"
inkscape:cy="226.75611"
inkscape:cx="207.75746"
inkscape:zoom="0.921875"
showgrid="false"
id="namedview81"
inkscape:window-height="714"
inkscape:window-width="1366"
inkscape:pageshadow="2"
inkscape:pageopacity="0"
guidetolerance="10"
gridtolerance="10"
objecttolerance="10"
borderopacity="1"
bordercolor="#666666"
pagecolor="#ffffff" />
<g
transform="translate(-15.683731,-12.674362)"
id="g50">
</g>
<g
transform="translate(-15.683731,-12.674362)"
id="g52">
</g>
<g
transform="translate(-15.683731,-12.674362)"
id="g54">
</g>
<g
transform="translate(-15.683731,-12.674362)"
id="g56">
</g>
<g
transform="translate(-15.683731,-12.674362)"
id="g58">
</g>
<g
transform="translate(-15.683731,-12.674362)"
id="g60">
</g>
<g
transform="translate(-15.683731,-12.674362)"
id="g62">
</g>
<g
transform="translate(-15.683731,-12.674362)"
id="g64">
</g>
<g
transform="translate(-15.683731,-12.674362)"
id="g66">
</g>
<g
transform="translate(-15.683731,-12.674362)"
id="g68">
</g>
<g
transform="translate(-15.683731,-12.674362)"
id="g70">
</g>
<g
transform="translate(-15.683731,-12.674362)"
id="g72">
</g>
<g
transform="translate(-15.683731,-12.674362)"
id="g74">
</g>
<g
transform="translate(-15.683731,-12.674362)"
id="g76">
</g>
<g
transform="translate(-15.683731,-12.674362)"
id="g78">
</g>
<rect
y="45.901909"
x="10.350166"
height="351.45764"
width="478.37289"
id="rect4830"
style="opacity:1;fill:none;fill-opacity:1;paint-order:normal" /><circle
r="240.5596"
cy="240.5596"
cx="240.51262"
id="path857"
style="fill:#000000" /></svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

@@ -0,0 +1,119 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Generator: Adobe Illustrator 18.1.1, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
version="1.1"
id="Capa_1"
x="0px"
y="0px"
viewBox="0 0 63.999999 63.999714"
xml:space="preserve"
sodipodi:docname="fan.svg"
width="64"
height="63.999714"
inkscape:version="0.92.2 2405546, 2018-03-11"><metadata
id="metadata4808"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
id="defs4806">
</defs><sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1920"
inkscape:window-height="1020"
id="namedview4804"
showgrid="false"
fit-margin-top="0"
fit-margin-left="0"
fit-margin-right="0"
fit-margin-bottom="0"
inkscape:zoom="7.3853375"
inkscape:cx="69.858273"
inkscape:cy="30.334855"
inkscape:window-x="0"
inkscape:window-y="30"
inkscape:window-maximized="1"
inkscape:current-layer="Capa_1" />
<path
d="m 63.334922,25.549682 c -0.177154,-0.86073 -0.546488,-1.4749 -1.224053,-2.03456 -5.414798,-4.47493 -12.924109,-4.17414 -22.52822,0.90223 -0.626766,-0.62676 -1.30519,-1.15651 -2.065465,-1.6127 6.619247,-5.93839 12.653083,-8.43143 18.101079,-7.47955 1.606266,0.28061 2.718561,-1.5599 1.72332,-2.85136 -2.208992,-2.8669502 -4.723066,-5.2011502 -7.745565,-7.1919202 -0.734089,-0.48338 -1.429398,-0.65624 -2.304294,-0.5731 -6.993017,0.66469 -12.090282,6.1872302 -15.291795,16.5677902 -0.886487,0 -1.740778,0.10503 -2.600792,0.32011 0.481522,-8.87947 2.985008,-14.9088702 7.510599,-18.0882002 1.334239,-0.93729 0.819232,-3.02521996 -0.797623,-3.23499996 -3.589307,-0.46521 -7.017486,-0.33814 -10.562433,0.39166 -0.86073,0.17715 -1.474903,0.54648996 -2.034556,1.22404996 -4.474935,5.4148 -4.174145,12.9241102 0.902228,22.5282202 -0.626766,0.62677 -1.156512,1.30519 -1.612706,2.06547 -5.93839,-6.61925 -8.431429,-12.65309 -7.479547,-18.1010802 0.280614,-1.60627 -1.559902,-2.71856 -2.851354,-1.72347 -2.8669526,2.209 -5.201154,4.7230702 -7.1919221,7.7455702 -0.4833823,0.73409 -0.6562437,1.4294 -0.5731043,2.30429 0.6646865,6.99302 6.1872364,12.09029 16.5677934,15.2918 0,0.88649 0.105033,1.74078 0.320108,2.60079 -8.879467,-0.48152 -14.9088672,-2.98501 -18.0882006,-7.5106 -0.9372866,-1.33424 -3.02521772,-0.81923 -3.2349982,0.79762 -0.46520897,3.58931 -0.33813866,7.01749 0.391657,10.56244 0.17715433,0.86073 0.5464882,1.4749 1.2240534,2.03455 5.4147977,4.47494 12.9241094,4.17415 22.5282204,-0.90222 0.626766,0.62676 1.305189,1.15651 2.065465,1.6127 -6.619247,5.93839 -12.653083,8.43143 -18.1010797,7.47955 -1.6062659,-0.28062 -2.7185604,1.5599 -1.7233194,2.85135 2.2089913,2.86695 4.7230661,5.20116 7.7455651,7.19192 0.734088,0.48339 1.429398,0.65625 2.304294,0.57311 6.993016,-0.66469 12.090281,-6.18724 15.291795,-16.56779 0.886487,0 1.740777,-0.10504 2.600791,-0.32026 -0.481522,8.87947 -2.985007,14.90887 -7.510599,18.0882 -1.334238,0.93729 -0.819231,3.02522 0.797624,3.235 3.589307,0.46521 7.017486,0.33814 10.562433,-0.39165 0.860729,-0.17716 1.474903,-0.54649 2.034556,-1.22406 4.474935,-5.4148 4.174145,-12.92411 -0.902228,-22.52822 0.626766,-0.62676 1.156511,-1.30519 1.612705,-2.06546 5.938391,6.61924 8.43143,12.65308 7.479547,18.10108 -0.280613,1.60626 1.559903,2.71856 2.851355,1.72332 2.866952,-2.20899 5.201153,-4.72307 7.191922,-7.74557 0.483382,-0.73409 0.656243,-1.4294 0.573104,-2.30429 -0.664687,-6.99302 -6.187237,-12.09028 -16.567793,-15.2918 0,-0.88648 -0.105033,-1.74077 -0.320108,-2.60079 8.879467,0.48152 14.908867,2.98501 18.0882,7.5106 0.937287,1.33424 3.025218,0.81923 3.234998,-0.79762 0.465209,-3.58917 0.338139,-7.01706 -0.391657,-10.56215 z m -31.33485,15.38653 c -4.935279,0 -8.936277,-4.001 -8.936277,-8.93628 0,-4.93528 4.000998,-8.93628 8.936277,-8.93628 4.935279,0 8.936276,4.001 8.936276,8.93628 0,4.93528 -4.000997,8.93628 -8.936276,8.93628 z"
id="path4768"
inkscape:connector-curvature="0"
style="stroke-width:1" />
<g
id="g4773"
transform="translate(-82.375262,-465.62302)">
</g>
<g
id="g4775"
transform="translate(-82.375262,-465.62302)">
</g>
<g
id="g4777"
transform="translate(-82.375262,-465.62302)">
</g>
<g
id="g4779"
transform="translate(-82.375262,-465.62302)">
</g>
<g
id="g4781"
transform="translate(-82.375262,-465.62302)">
</g>
<g
id="g4783"
transform="translate(-82.375262,-465.62302)">
</g>
<g
id="g4785"
transform="translate(-82.375262,-465.62302)">
</g>
<g
id="g4787"
transform="translate(-82.375262,-465.62302)">
</g>
<g
id="g4789"
transform="translate(-82.375262,-465.62302)">
</g>
<g
id="g4791"
transform="translate(-82.375262,-465.62302)">
</g>
<g
id="g4793"
transform="translate(-82.375262,-465.62302)">
</g>
<g
id="g4795"
transform="translate(-82.375262,-465.62302)">
</g>
<g
id="g4797"
transform="translate(-82.375262,-465.62302)">
</g>
<g
id="g4799"
transform="translate(-82.375262,-465.62302)">
</g>
<g
id="g4801"
transform="translate(-82.375262,-465.62302)">
</g>
</svg>

After

Width:  |  Height:  |  Size: 5.6 KiB

View File

@@ -0,0 +1,151 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
version="1.1"
id="Layer_1"
x="0px"
y="0px"
viewBox="0 0 521.99903 521.99702"
xml:space="preserve"
sodipodi:docname="gear-led-2.svg"
inkscape:version="0.92.2 2405546, 2018-03-11"
width="521.99902"
height="521.99701"><metadata
id="metadata49"><rdf:RDF><cc:Work
rdf:about=""><dc:format>image/svg+xml</dc:format><dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" /><dc:title></dc:title></cc:Work></rdf:RDF></metadata><defs
id="defs47">
</defs><sodipodi:namedview
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1"
objecttolerance="10"
gridtolerance="10"
guidetolerance="10"
inkscape:pageopacity="0"
inkscape:pageshadow="2"
inkscape:window-width="1920"
inkscape:window-height="1020"
id="namedview45"
showgrid="false"
inkscape:zoom="0.92187681"
inkscape:cx="199.23524"
inkscape:cy="241.2828"
inkscape:window-x="0"
inkscape:window-y="30"
inkscape:window-maximized="1"
inkscape:current-layer="Layer_1"
fit-margin-top="0"
fit-margin-left="0"
fit-margin-right="0"
fit-margin-bottom="0" />
<g
id="g4810"><g
transform="translate(5,5)"
style="stroke:#000000;stroke-width:10;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="g4">
<path
style="stroke:#000000;stroke-width:10;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
d="m 489.175,206.556 c -9.629,-1.442 -19.514,-2.825 -29.379,-4.111 -1.195,-0.155 -2.165,-0.966 -2.467,-2.064 -4.553,-16.523 -11.16,-32.467 -19.636,-47.389 -0.57,-1.002 -0.463,-2.266 0.273,-3.223 6.067,-7.885 12.081,-15.856 17.876,-23.69 7.824,-10.578 6.688,-25.588 -2.64,-34.917 L 420.836,58.796 c -9.329,-9.328 -24.338,-10.464 -34.918,-2.638 -7.817,5.782 -15.787,11.796 -23.689,17.875 -0.954,0.736 -2.221,0.843 -3.223,0.274 -14.921,-8.476 -30.865,-15.083 -47.389,-19.637 -1.099,-0.301 -1.91,-1.271 -2.066,-2.469 -1.289,-9.88 -2.671,-19.764 -4.109,-29.376 C 303.495,9.812 292.079,0 278.886,0 h -45.773 c -13.194,0 -24.61,9.812 -26.554,22.824 -1.439,9.614 -2.821,19.497 -4.11,29.379 -0.157,1.197 -0.967,2.165 -2.067,2.467 -16.524,4.556 -32.469,11.162 -47.387,19.637 -1.003,0.569 -2.269,0.459 -3.225,-0.274 C 141.869,67.954 133.898,61.94 126.08,56.157 115.499,48.332 100.49,49.47 91.163,58.797 L 58.797,91.163 c -9.329,9.33 -10.464,24.341 -2.638,34.918 5.804,7.846 11.818,15.815 17.875,23.688 0.735,0.955 0.843,2.22 0.274,3.223 -8.478,14.925 -15.084,30.869 -19.637,47.389 -0.301,1.097 -1.271,1.908 -2.467,2.065 -9.86,1.287 -19.744,2.669 -29.378,4.111 C 9.812,208.502 0,219.92 0,233.112 v 45.774 c 0,13.193 9.812,24.61 22.824,26.556 9.634,1.442 19.519,2.824 29.379,4.11 1.197,0.157 2.165,0.967 2.467,2.066 4.553,16.521 11.16,32.465 19.637,47.389 0.569,1.003 0.461,2.268 -0.274,3.223 -6.072,7.892 -12.086,15.862 -17.875,23.689 -7.825,10.578 -6.691,25.589 2.638,34.918 l 32.366,32.366 c 9.33,9.329 24.341,10.465 34.918,2.638 7.817,-5.782 15.787,-11.796 23.689,-17.875 0.955,-0.736 2.221,-0.842 3.223,-0.274 14.92,8.476 30.863,15.081 47.389,19.637 1.099,0.302 1.91,1.271 2.066,2.467 1.289,9.88 2.672,19.765 4.11,29.376 1.946,13.013 13.362,22.825 26.556,22.825 h 45.773 c 13.193,0 24.61,-9.812 26.555,-22.827 1.439,-9.623 2.821,-19.507 4.109,-29.376 0.157,-1.197 0.967,-2.166 2.066,-2.469 16.524,-4.556 32.469,-11.162 47.388,-19.637 1.003,-0.567 2.268,-0.459 3.224,0.274 7.901,6.079 15.872,12.093 23.689,17.875 10.578,7.825 25.588,6.691 34.918,-2.638 l 32.366,-32.366 c 9.328,-9.329 10.464,-24.339 2.639,-34.918 -5.795,-7.831 -11.81,-15.802 -17.876,-23.689 -0.735,-0.955 -0.843,-2.22 -0.273,-3.223 8.477,-14.924 15.083,-30.868 19.636,-47.388 0.304,-1.1 1.272,-1.91 2.469,-2.067 9.863,-1.286 19.748,-2.669 29.378,-4.11 13.013,-1.945 22.825,-13.362 22.825,-26.555 v -45.774 c 0,-13.19 -9.812,-24.608 -22.824,-26.553 z m -1.084,72.332 c 0,1.45 -1.054,2.7 -2.453,2.911 -9.482,1.419 -19.216,2.779 -28.932,4.048 -10.758,1.402 -19.56,9.024 -22.426,19.42 -4.029,14.618 -9.875,28.727 -17.375,41.932 -5.333,9.389 -4.504,21.012 2.112,29.612 5.976,7.768 11.899,15.617 17.604,23.329 0.842,1.137 0.702,2.769 -0.323,3.794 L 403.931,436.3 c -1.026,1.026 -2.657,1.163 -3.793,0.324 -7.697,-5.695 -15.548,-11.618 -23.33,-17.605 -8.599,-6.617 -20.221,-7.446 -29.609,-2.114 -13.205,7.5 -27.314,13.347 -41.934,17.377 -10.394,2.865 -18.016,11.667 -19.421,22.426 -1.267,9.722 -2.629,19.456 -4.047,28.932 -0.209,1.399 -1.461,2.453 -2.911,2.453 h -45.773 c -1.45,0 -2.702,-1.054 -2.911,-2.454 -1.415,-9.465 -2.778,-19.199 -4.047,-28.93 -1.403,-10.759 -9.027,-19.561 -19.421,-22.426 -14.621,-4.03 -28.73,-9.877 -41.934,-17.378 -4.117,-2.337 -8.664,-3.491 -13.196,-3.491 -5.804,0 -11.585,1.89 -16.412,5.607 -7.783,5.987 -15.633,11.91 -23.33,17.605 -1.138,0.839 -2.767,0.702 -3.792,-0.324 L 75.703,403.936 c -1.026,-1.026 -1.166,-2.656 -0.324,-3.793 5.701,-7.707 11.623,-15.556 17.604,-23.33 6.615,-8.6 7.445,-20.221 2.114,-29.609 C 87.594,333.995 81.749,319.887 77.72,305.27 74.855,294.876 66.053,287.253 55.295,285.85 c -9.712,-1.267 -19.447,-2.63 -28.934,-4.048 -1.399,-0.21 -2.453,-1.461 -2.453,-2.911 v -45.774 c 0,-1.45 1.054,-2.701 2.453,-2.911 9.487,-1.419 19.221,-2.781 28.932,-4.048 10.759,-1.402 19.561,-9.025 22.426,-19.42 4.027,-14.616 9.874,-28.725 17.377,-41.934 5.332,-9.389 4.502,-21.011 -2.113,-29.609 -5.965,-7.756 -11.888,-15.604 -17.604,-23.33 -0.84,-1.137 -0.701,-2.769 0.324,-3.793 l 32.365,-32.367 c 1.024,-1.026 2.655,-1.163 3.792,-0.324 7.697,5.694 15.547,11.617 23.33,17.605 8.6,6.614 20.221,7.445 29.611,2.112 13.203,-7.5 27.312,-13.347 41.932,-17.377 10.395,-2.865 18.019,-11.667 19.422,-22.426 1.27,-9.731 2.631,-19.465 4.048,-28.933 0.209,-1.397 1.461,-2.452 2.911,-2.452 h 45.773 c 1.45,0 2.702,1.054 2.911,2.453 1.417,9.465 2.778,19.198 4.048,28.932 1.403,10.759 9.027,19.561 19.421,22.426 14.62,4.03 28.728,9.877 41.934,17.377 9.388,5.33 21.01,4.502 29.608,-2.114 7.783,-5.987 15.633,-11.91 23.329,-17.604 1.137,-0.842 2.769,-0.703 3.794,0.324 l 32.366,32.366 c 1.026,1.026 1.164,2.657 0.324,3.793 -5.705,7.714 -11.628,15.562 -17.604,23.33 -6.615,8.601 -7.445,20.223 -2.112,29.612 7.501,13.205 13.347,27.313 17.377,41.933 2.865,10.394 11.669,18.016 22.424,19.418 9.716,1.268 19.451,2.63 28.934,4.048 1.399,0.21 2.453,1.461 2.453,2.911 v 45.773 z"
id="path2"
inkscape:connector-curvature="0" />
</g><g
transform="translate(5,5)"
style="stroke:#000000;stroke-width:10;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="g12">
<g
style="stroke:#000000;stroke-width:10;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="g10">
<path
inkscape:connector-curvature="0"
style="stroke:#000000;stroke-width:10;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="path8"
d="m 256,144.866 c -61.28,0 -111.134,49.854 -111.134,111.134 0,61.28 49.854,111.134 111.134,111.134 61.28,0 111.134,-49.854 111.134,-111.134 0,-61.28 -49.854,-111.134 -111.134,-111.134 z m 0,198.359 c -48.097,0 -87.225,-39.129 -87.225,-87.225 0,-48.097 39.13,-87.225 87.225,-87.225 48.096,0 87.225,39.129 87.225,87.225 0,48.096 -39.128,87.225 -87.225,87.225 z" />
</g>
</g><g
transform="translate(5,5)"
style="stroke:#000000;stroke-width:10;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="g14">
</g><g
transform="translate(5,5)"
style="stroke:#000000;stroke-width:10;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="g16">
</g><g
transform="translate(5,5)"
style="stroke:#000000;stroke-width:10;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="g18">
</g><g
transform="translate(5,5)"
style="stroke:#000000;stroke-width:10;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="g20">
</g><g
transform="translate(5,5)"
style="stroke:#000000;stroke-width:10;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="g22">
</g><g
transform="translate(5,5)"
style="stroke:#000000;stroke-width:10;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="g24">
</g><g
transform="translate(5,5)"
style="stroke:#000000;stroke-width:10;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="g26">
</g><g
transform="translate(5,5)"
style="stroke:#000000;stroke-width:10;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="g28">
</g><g
transform="translate(5,5)"
style="stroke:#000000;stroke-width:10;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="g30">
</g><g
transform="translate(5,5)"
style="stroke:#000000;stroke-width:10;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="g32">
</g><g
transform="translate(5,5)"
style="stroke:#000000;stroke-width:10;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="g34">
</g><g
transform="translate(5,5)"
style="stroke:#000000;stroke-width:10;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="g36">
</g><g
transform="translate(5,5)"
style="stroke:#000000;stroke-width:10;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="g38">
</g><g
transform="translate(5,5)"
style="stroke:#000000;stroke-width:10;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="g40">
</g><g
transform="translate(5,5)"
style="stroke:#000000;stroke-width:10;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="g42">
</g></g>
</svg>

After

Width:  |  Height:  |  Size: 9.8 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 10 KiB

View File

@@ -0,0 +1,71 @@
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!-- Created with Inkscape (http://www.inkscape.org/) -->
<svg
xmlns:dc="http://purl.org/dc/elements/1.1/"
xmlns:cc="http://creativecommons.org/ns#"
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
xmlns:svg="http://www.w3.org/2000/svg"
xmlns="http://www.w3.org/2000/svg"
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
width="16.933332mm"
height="16.933332mm"
viewBox="0 0 16.933332 16.933332"
version="1.1"
id="svg8"
sodipodi:docname="mouse.svg"
inkscape:version="0.92.2 2405546, 2018-03-11">
<defs
id="defs2" />
<sodipodi:namedview
id="base"
pagecolor="#ffffff"
bordercolor="#666666"
borderopacity="1.0"
inkscape:pageopacity="0.0"
inkscape:pageshadow="2"
inkscape:zoom="5.6"
inkscape:cx="13.28418"
inkscape:cy="14.886884"
inkscape:document-units="mm"
inkscape:current-layer="layer1"
showgrid="false"
fit-margin-top="0"
fit-margin-left="0"
fit-margin-right="0"
fit-margin-bottom="0"
inkscape:window-width="1920"
inkscape:window-height="1020"
inkscape:window-x="0"
inkscape:window-y="30"
inkscape:window-maximized="1" />
<metadata
id="metadata5">
<rdf:RDF>
<cc:Work
rdf:about="">
<dc:format>image/svg+xml</dc:format>
<dc:type
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
<dc:title></dc:title>
</cc:Work>
</rdf:RDF>
</metadata>
<g
inkscape:label="Layer 1"
inkscape:groupmode="layer"
id="layer1"
transform="translate(-107.87666,-24.618942)">
<g
aria-label="🖰"
transform="matrix(2.0925222,0,0,2.0925222,225.46869,4.2347952)"
style="font-style:normal;font-variant:normal;font-weight:bold;font-stretch:normal;font-size:10px;line-height:125%;font-family:'Adobe Garamond Pro';-inkscape-font-specification:'Adobe Garamond Pro Bold';text-align:start;letter-spacing:0px;word-spacing:0px;writing-mode:lr-tb;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none;stroke-width:0.20230769;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="flowRoot4916">
<path
d="m -52.145156,17.732578 q -1.41,0 -2.15,-0.75 -0.73,-0.76 -0.73,-2.21 v -0.46 l 0.13,-2.72 q 0.04,-0.91 0.39,-1.33 0.36,-0.4199999 1.08,-0.4199999 0.2,0 0.44,0.03 l 2.48,0.2499999 q 0.55,0.05 0.79,0.35 0.25,0.29 0.28,0.9 l 0.16,3.14 v 0.29 q 0,1.42 -0.74,2.18 -0.73,0.75 -2.13,0.75 z m -2.48,-5.45 q 0.35,-0.04 0.71,-0.05 0.37,-0.02 0.73,-0.02 0.28,0 0.55,0.01 0.28,0.01 0.55,0.03 v -2 l -0.93,-0.09 q -0.11,-0.01 -0.22,-0.02 -0.1,-0.01 -0.19,-0.01 -0.59,0 -0.86,0.34 -0.27,0.33 -0.31,1.13 z m 4.96,0.39 -0.07,-1.29 q -0.03,-0.5 -0.21,-0.71 -0.17,-0.21 -0.6,-0.26 l -1.26,-0.13 v 2 q 0.56,0.05 1.1,0.16 0.55,0.1 1.04,0.23 z m -2.47,4.77 q 2.56,0 2.56,-2.64 v -0.27 l -0.08,-1.55 q -0.89,-0.24 -1.75,-0.35 -0.86,-0.12 -1.73,-0.12 -0.38,0 -0.75,0.02 -0.37,0.02 -0.76,0.06 l -0.08,1.71 v 0.47 q 0,2.67 2.59,2.67 z"
style="stroke:#000000;stroke-width:0.20230769;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
id="path5033" />
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.3 KiB

View File

@@ -0,0 +1,87 @@
<?xml version="1.0" encoding="iso-8859-1"?>
<!-- Generator: Adobe Illustrator 19.0.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
viewBox="0 0 512 512" style="enable-background:new 0 0 512 512;" xml:space="preserve">
<g>
<g>
<path d="M168,184.036c-6.632,0-12,5.376-12,12v23.752c0,6.628,5.368,12,12,12c6.624,0,12-5.372,12-12v-23.752
C180,189.412,174.624,184.036,168,184.036z"/>
</g>
</g>
<g>
<g>
<path d="M256,184.036c-6.632,0-12,5.376-12,12v23.752c0,6.628,5.368,12,12,12c6.624,0,12-5.372,12-12v-23.752
C268,189.412,262.624,184.036,256,184.036z"/>
</g>
</g>
<g>
<g>
<path d="M212,184.036c-6.632,0-12,5.376-12,12v23.752c0,6.628,5.368,12,12,12c6.624,0,12-5.372,12-12v-23.752
C224,189.412,218.624,184.036,212,184.036z"/>
</g>
</g>
<g>
<g>
<path d="M460,0H52C23.28,0,0,23.28,0,52v408c0,28.72,23.28,52,52,52h408c28.72,0,52-23.28,52-52V52C512,23.28,488.72,0,460,0z
M444,284h-88.024c-2.212,0-3.976,1.64-3.976,3.848V348H160v-60.152c0-2.208-1.616-3.848-3.828-3.848H68V68h44v92.184
c0,6.624,5.368,12,12,12c6.624,0,12-5.376,12-12V68h20v92.184c0,6.624,5.368,12,12,12c6.624,0,12-5.376,12-12V68h20v92.184
c0,6.624,5.368,12,12,12c6.624,0,12-5.376,12-12V68h20v92.184c0,6.624,5.368,12,12,12c6.624,0,12-5.376,12-12V68h20v92.184
c0,6.624,5.368,12,12,12c6.624,0,12-5.376,12-12V68h20v92.184c0,6.624,5.368,12,12,12c6.624,0,12-5.376,12-12V68h20v92.184
c0,6.624,5.368,12,12,12c6.624,0,12-5.376,12-12V68h44V284z"/>
</g>
</g>
<g>
<g>
<path d="M124,184.036c-6.632,0-12,5.376-12,12v23.752c0,6.628,5.368,12,12,12c6.624,0,12-5.372,12-12v-23.752
C136,189.412,130.624,184.036,124,184.036z"/>
</g>
</g>
<g>
<g>
<path d="M388,184.036c-6.632,0-12,5.376-12,12v23.752c0,6.628,5.368,12,12,12c6.624,0,12-5.372,12-12v-23.752
C400,189.412,394.624,184.036,388,184.036z"/>
</g>
</g>
<g>
<g>
<path d="M344,184.036c-6.632,0-12,5.376-12,12v23.752c0,6.628,5.368,12,12,12c6.624,0,12-5.372,12-12v-23.752
C356,189.412,350.624,184.036,344,184.036z"/>
</g>
</g>
<g>
<g>
<path d="M300,184.036c-6.632,0-12,5.376-12,12v23.752c0,6.628,5.368,12,12,12c6.624,0,12-5.372,12-12v-23.752
C312,189.412,306.624,184.036,300,184.036z"/>
</g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
<g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.5 KiB

Some files were not shown because too many files have changed in this diff Show More