mirror of
https://github.com/mofeng-git/One-KVM.git
synced 2026-01-29 00:51:53 +08:00
feat: merge upstream master - version 4.94
Merge upstream PiKVM master branch updates: - Bump version from 4.93 to 4.94 - HID: improved jiggler pattern for better compatibility - Streamer: major refactoring for improved performance and maintainability - Prometheus: tidying GPIO channel name formatting - Web: added __gpio-label class for custom styling - HID: customizable /api/hid/print delay configuration - ATX: independent power/reset regions for better control - OLED: added --fill option for display testing - Web: improved keyboard handling in modal dialogs - Web: enhanced login error messages - Switch: added heartbeat functionality - Web: mouse touch code simplification and refactoring - Configs: use systemd-networkd-wait-online --any by default - PKGBUILD: use cp -r to install systemd units properly - Various bug fixes and performance improvements
This commit is contained in:
40
web/base.pug
40
web/base.pug
@@ -23,34 +23,42 @@ doctype html
|
||||
# #
|
||||
==============================================================================
|
||||
|
||||
- var css_dir = "/share/css"
|
||||
- var js_dir = "/share/js"
|
||||
- var svg_dir = "/share/svg"
|
||||
- var png_dir = "/share/png"
|
||||
-
|
||||
var root_prefix = "./"
|
||||
|
||||
title = ""
|
||||
main_js = ""
|
||||
body_class = ""
|
||||
css_list = []
|
||||
|
||||
- var title = ""
|
||||
- var main_js = ""
|
||||
- var body_class = ""
|
||||
- var css_list = ["vars", "main"]
|
||||
|
||||
block vars
|
||||
|
||||
|
||||
block _vars_dynamic
|
||||
-
|
||||
share_dir = `${root_prefix}share`
|
||||
css_dir = `${share_dir}/css`
|
||||
js_dir = `${share_dir}/js`
|
||||
svg_dir = `${share_dir}/svg`
|
||||
png_dir = `${share_dir}/png`
|
||||
|
||||
|
||||
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")
|
||||
link(rel="apple-touch-icon" sizes="180x180" href=`${share_dir}/apple-touch-icon.png`)
|
||||
link(rel="icon" type="image/png" sizes="32x32" href=`${share_dir}/favicon-32x32.png`)
|
||||
link(rel="icon" type="image/png" sizes="16x16" href=`${share_dir}/favicon-16x16.png`)
|
||||
link(rel="manifest" href=`${share_dir}/site.webmanifest`)
|
||||
link(rel="mask-icon" href=`${share_dir}/safari-pinned-tab.svg` color="#5bbad5")
|
||||
meta(name="msapplication-TileColor" content="#2b5797")
|
||||
meta(name="theme-color" content="#ffffff")
|
||||
|
||||
each name in css_list
|
||||
each name in ["vars", "main"].concat(css_list).concat(["user"])
|
||||
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`)
|
||||
@@ -58,6 +66,8 @@ html(lang="en")
|
||||
|
||||
if main_js
|
||||
script(type="module")
|
||||
| import {setRootPrefix} from "#{js_dir}/vars.js";
|
||||
| setRootPrefix("#{root_prefix}");
|
||||
| import {main} from "#{js_dir}/#{main_js}.js";
|
||||
| main();
|
||||
|
||||
|
||||
@@ -27,24 +27,26 @@
|
||||
<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">
|
||||
<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";
|
||||
<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 {setRootPrefix} from "./share/js/vars.js";
|
||||
setRootPrefix("./");
|
||||
import {main} from "./share/js/index/main.js";
|
||||
main();
|
||||
</script>
|
||||
</head>
|
||||
@@ -53,31 +55,32 @@
|
||||
<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 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 © 2018-2024 Maxim Devaev | Modified by SilentWind</td>
|
||||
<td class="copyright" colspan="2" i18n="copyright">Copyright © 2018-2025 Maxim Devaev | Modified by SilentWind</td>
|
||||
</tr>
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<hr>
|
||||
<table>
|
||||
<td class="server">
|
||||
<td i18n="serve_name">Name:</td>
|
||||
<td><a id="kvmd-meta-server-host" target="_blank" href="./api/info"></a></td>
|
||||
</td>
|
||||
</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">
|
||||
<hr>
|
||||
<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).
|
||||
@@ -85,6 +88,7 @@
|
||||
<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 class="code" id="app-text"></div>
|
||||
</div>
|
||||
<hr>
|
||||
<p class="text credits"><a target="_blank" href="https://github.com/mofeng-git/One-KVM" i18n="index_text_12">One-KVM Project</a> | <a target="_blank" href="https://one-kvm.mofeng.run" i18n="index_text_13">One-KVM Documentation</a></p>
|
||||
|
||||
@@ -1,44 +1,45 @@
|
||||
extends start.pug
|
||||
|
||||
|
||||
append vars
|
||||
- title = "One-KVM Index"
|
||||
- main_js = "index/main"
|
||||
- css_list = css_list.concat(["window", "modal", "index/index"])
|
||||
- css_list.push("window", "modal", "index/index")
|
||||
|
||||
block start
|
||||
table
|
||||
tr
|
||||
td(class="logo")
|
||||
td.logo
|
||||
a(href="https://pikvm.org" target="_blank")
|
||||
img(class="svg-gray" src=`${svg_dir}/logo.svg` alt="PiKVM" height="40")
|
||||
img.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.title(colspan="2" i18n="index_title") The Open Source KVM over IP]
|
||||
tr
|
||||
td(colspan="2" class="copyright" i18n="copyright")
|
||||
| Copyright © 2018-2024 Maxim Devaev | Modified by SilentWind
|
||||
|
||||
hr
|
||||
|
||||
div(id="apps-box")
|
||||
h4 Loading ...
|
||||
td.copyright(colspan="2" i18n="copyright")
|
||||
| Copyright © 2018-2025 Maxim Devaev | Modified by SilentWind
|
||||
|
||||
hr
|
||||
table
|
||||
td(class="server")
|
||||
td(i18n="serve_name") Server:
|
||||
td #[a(id="kvmd-meta-server-host" target="_blank" href="/api/info")]
|
||||
td(i18n="serve_name") Name:
|
||||
td #[a#kvmd-meta-server-host(target="_blank" href=`${root_prefix}api/info`)]
|
||||
hr
|
||||
#apps-box
|
||||
h4 Loading ...
|
||||
|
||||
div(id="app-keyboard-warning")
|
||||
p(class="text" i18n="index_text_1")
|
||||
#app-keyboard-warning
|
||||
hr
|
||||
p.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")
|
||||
p.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.
|
||||
.code#app-text
|
||||
|
||||
hr
|
||||
p(class="text credits")
|
||||
p.text.credits
|
||||
a(target="_blank" href="https://github.com/mofeng-git/One-KVM" i18n="index_text_12") One-KVM Project
|
||||
| |
|
||||
a(target="_blank" href="https://one-kvm.mofeng.run" i18n="index_text_13") One-KVM Documentation
|
||||
@@ -27,27 +27,29 @@
|
||||
<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">
|
||||
<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";
|
||||
<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 {setRootPrefix} from "../share/js/vars.js";
|
||||
setRootPrefix("../");
|
||||
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"> ← [ One-KVM Index ]</a>
|
||||
<div class="start"><a style="display:inline-block; margin-top:4px; color:#5c90bc; text-decoration:none" href="../" i18n="index"> ← [ 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.
|
||||
@@ -57,7 +59,7 @@
|
||||
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.
|
||||
<p class="tetx" 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>
|
||||
|
||||
@@ -1,20 +1,23 @@
|
||||
extends ../start.pug
|
||||
|
||||
|
||||
append vars
|
||||
- title = "One-KVM IPMI Info"
|
||||
- main_js = "ipmi/main"
|
||||
- index_link = true
|
||||
-
|
||||
root_prefix = "../"
|
||||
title = "One-KVM IPMI Info"
|
||||
main_js = "ipmi/main"
|
||||
index_link = true
|
||||
|
||||
block start
|
||||
p(class="text" i18n="ipmi_text1")
|
||||
p.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")
|
||||
p.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")
|
||||
p.tetx(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")
|
||||
.code#ipmi-text(style="max-height:200px")
|
||||
|
||||
1382
web/kvm/index.html
1382
web/kvm/index.html
File diff suppressed because it is too large
Load Diff
@@ -1,30 +1,39 @@
|
||||
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"])
|
||||
-
|
||||
root_prefix = "../"
|
||||
title = "One-KVM Session"
|
||||
main_js = "kvm/main"
|
||||
body_class = "body-no-select"
|
||||
css_list.push(
|
||||
"navbar", "window", "modal", "led", "slider",
|
||||
"switch", "radio", "progress", "keypad", "tabs",
|
||||
"kvm/stream", "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.navbar-bg-tips
|
||||
li.left
|
||||
pre#kvmd-meta-tips-left
|
||||
li.right
|
||||
pre#kvmd-meta-tips-right
|
||||
|
||||
ul(class="footer")
|
||||
li(class="left")
|
||||
span(id="kvmd-meta-server-host" title="Server name (see System/About)")
|
||||
ul.footer
|
||||
li.left
|
||||
span#kvmd-meta-server-host(title="KVM/Server name (see System/About)")
|
||||
| |
|
||||
span(id="kvmd-version-kvmd" title="KVMD version")
|
||||
span#kvmd-info-platform(title="PiKVM Platform")
|
||||
| |
|
||||
span(id="kvmd-version-streamer" title="Streamer version")
|
||||
li(class="right")
|
||||
span#kvmd-version-kvmd(title="KVMD version")
|
||||
| |
|
||||
span#kvmd-version-streamer(title="Streamer version")
|
||||
li.right
|
||||
a(target="_blank" href="https://github.com/mofeng-git/One-KVM" i18n="index_text_12") One-KVM Project
|
||||
| |
|
||||
a(target="_blank" href="https://one-kvm.mofeng.run" i18n="index_text_13") One-KVM Documentation
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
li(id="atx-dropdown" class="right feature-disabled")
|
||||
a(class="menu-button" href="#")
|
||||
li.right.feature-disabled#atx-dropdown
|
||||
a.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")
|
||||
|
||||
.menu
|
||||
.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")
|
||||
+menu_switch_table("atx-ask-switch", true, true) Ask click confirmation:
|
||||
hr
|
||||
div(class="buttons")
|
||||
button(disabled data-force-hide-menu id="atx-power-button") • Click Power #[sup #[i short]]
|
||||
button(disabled data-force-hide-menu id="atx-power-button-long") • Click Power #[sup #[i long]]
|
||||
.buttons
|
||||
button#atx-power-button(disabled data-force-hide-menu) • Click Power #[sup #[i short]]
|
||||
button#atx-power-button-long(disabled data-force-hide-menu) • Click Power #[sup #[i long]]
|
||||
hr
|
||||
button(disabled data-force-hide-menu id="atx-reset-button") • Click Reset
|
||||
button#atx-reset-button(disabled data-force-hide-menu) • Click Reset
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
li(id="gpio-dropdown" class="right feature-disabled")
|
||||
a(class="menu-button" id="gpio-menu-button" href="#")
|
||||
li.right.feature-disabled#gpio-dropdown
|
||||
a.menu-button#gpio-menu-button(href="#")
|
||||
span GPIO
|
||||
div(id="gpio-menu" class="menu")
|
||||
|
||||
.menu#gpio-menu
|
||||
|
||||
@@ -1,32 +1,34 @@
|
||||
div(id="hw-health-dropdown" class="hidden")
|
||||
li(class="left")
|
||||
a(class="menu-button" href="#")
|
||||
.hidden#hw-health-dropdown
|
||||
li.left
|
||||
a.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
|
||||
+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")
|
||||
.hidden#hw-health-message-undervoltage
|
||||
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")
|
||||
.hidden#hw-health-message-overheating
|
||||
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="#")
|
||||
.hidden#fan-health-dropdown
|
||||
li.left
|
||||
a.menu-button(href="#")
|
||||
+navbar_led("fan-health-led", "led-fan", "hidden")
|
||||
div(class="menu")
|
||||
|
||||
.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")
|
||||
#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]
|
||||
| A fan error occured, please #[a(href=`${root_prefix}api/log?seek=3600&follow=1` target="_blank") check the log]
|
||||
|
||||
@@ -1,30 +1,31 @@
|
||||
li(id="macro-dropdown" class="right")
|
||||
a(class="menu-button" href="#")
|
||||
li.right#macro-dropdown
|
||||
a.menu-button(href="#")
|
||||
+navbar_led("hid-recorder-led", "led-gear")
|
||||
span(i18n="kvm_text32") Macro
|
||||
div(class="menu")
|
||||
div(class="text")
|
||||
.menu
|
||||
.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") • 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
|
||||
.buttons.buttons-row
|
||||
button.row25#hid-recorder-record(disabled data-force-hide-menu i18n="kvm_text35") • Rec
|
||||
button.row25#hid-recorder-stop(disabled i18n="kvm_text36") Stop
|
||||
button.row25#hid-recorder-play(disabled i18n="kvm_text37") Play
|
||||
button.row25#hid-recorder-clear(disabled i18n="kvm_text38") Clear
|
||||
hr
|
||||
table(class="kv")
|
||||
|
||||
table.kv
|
||||
tr
|
||||
td(i18n="kvm_text39") Script time:
|
||||
td(colspan="2" id="hid-recorder-time" class="value") 00:00:00.0
|
||||
td.value#hid-recorder-time(colspan="2") 00:00:00.0
|
||||
tr
|
||||
td(i18n="kvm_text40") Scripted events:
|
||||
td(id="hid-recorder-events-count" class="value") 0
|
||||
tdvalue#hid-recorder-events-count 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")
|
||||
+menu_switch_table("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
|
||||
input#hid-recorder-new-script-file(type="file" style="display: none")
|
||||
.buttons.buttons-row
|
||||
button.row50#hid-recorder-upload(disabled i18n="kvm_text42") Upload script
|
||||
button.row50#hid-recorder-download(disabled i18n="kvm_text43") Download script
|
||||
|
||||
@@ -1,115 +1,118 @@
|
||||
li(id="msd-dropdown" class="right feature-disabled")
|
||||
a(class="menu-button" href="#")
|
||||
li.right.feature-disabled#msd-dropdown
|
||||
a.menu-button(href="#")
|
||||
+navbar_led("msd-led", "led-msd")
|
||||
span(i18n="kvm_text60") Drive
|
||||
div(id="msd-menu" class="menu")
|
||||
div(class="text")
|
||||
.menu#msd-menu
|
||||
.text
|
||||
b(i18n="kvm_text61") Mass Storage Drive:
|
||||
span(id="msd-status")
|
||||
span#msd-status
|
||||
br
|
||||
hr
|
||||
div(id="msd-message-offline" class="hidden")
|
||||
.hidden#msd-message-offline
|
||||
+menu_message("warning", "Mass Storage Drive is offline", "msd-message-offline")
|
||||
hr
|
||||
div(id="msd-message-image-broken" class="hidden")
|
||||
.hidden#msd-message-image-broken
|
||||
+menu_message("warning", "Current image is broken!", "msd-message-image-broken")
|
||||
| Perhaps uploading was interrupted#[br]
|
||||
hr
|
||||
div(id="msd-message-too-big-for-dvd" class="hidden")
|
||||
.hidden#msd-message-too-big-for-dvd
|
||||
+menu_message("warning", "Current image is too big for DVD!", "msd-message-too-big-for-dvd")
|
||||
| The maximum is 31.6GiB. Please switch to the Flash mode.
|
||||
hr
|
||||
div(id="msd-message-out-of-storage" class="hidden")
|
||||
.hidden#msd-message-out-of-storage
|
||||
+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")
|
||||
.hidden#msd-message-rw-enabled
|
||||
+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")
|
||||
.hidden#msd-message-downloads
|
||||
+menu_message("info", "The image is being downloaded from One-KVM", "msd-message-downloads")
|
||||
| Please wait
|
||||
hr
|
||||
table(class="kv")
|
||||
|
||||
table.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 ⇩ ]]
|
||||
td #[button(disabled id="msd-remove-button" title="Remove image") #[b × ]]
|
||||
table(class="kv")
|
||||
td(width="100%") #[select#msd-image-selector(disabled)]
|
||||
td #[button#msd-download-button(disabled title="Download image") #[b ⇩ ]]
|
||||
td #[button#msd-remove-button(disabled title="Remove image") #[b × ]]
|
||||
table.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/DVD
|
||||
input(type="radio" id="msd-mode-radio-flash" name="msd-mode-radio" value="0")
|
||||
label(for="msd-mode-radio-flash") Flash
|
||||
+menu_radio_td2("msd-mode-radio", [
|
||||
{title: "CD/DVD", value: "1", checked: true},
|
||||
{title: "Flash", value: "0"},
|
||||
]) Drive #[a(target="_blank" href="https://docs.pikvm.org/msd") mode]:
|
||||
td
|
||||
+menu_switch_notable("msd-rw-switch", "Writable", false, false, "msd-rw-switch")
|
||||
+menu_switch_td2("msd-rw-switch", false, false) Writable:
|
||||
tr
|
||||
td(i18n="kvm_text84") 文件内容:
|
||||
td(i18n="kvm_text84") Files:
|
||||
td
|
||||
div(class="radio-box")
|
||||
input(checked type="radio" id="msd-mode-radio-image" name="file-mode-radio" value="1")
|
||||
div.radio-box()
|
||||
input#msd-mode-radio-image(checked type="radio" 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")
|
||||
input#msd-mode-radio-file(type="radio" 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
|
||||
|
||||
#msd-storages
|
||||
hr
|
||||
.buttons.buttons-row
|
||||
button.row50#msd-select-new-button(disabled i18n="kvm_text64") Select image to upload
|
||||
button.row25#msd-upload-new-button(disabled i18n="kvm_text65") Upload
|
||||
button.row25#msd-abort-new-button(disabled i18n="kvm_text66") Abort
|
||||
.hidden#msd-message-another-user-uploads
|
||||
+menu_message("info", "Another user uploads an image", "msd-message-another-user-uploads")
|
||||
div(id="msd-new-sub" class="hidden")
|
||||
hr
|
||||
table(class="kv")
|
||||
|
||||
.hidden#msd-new-sub
|
||||
table.kv
|
||||
tr
|
||||
td(i18n="kvm_text68") Specify a local file:
|
||||
td #[input(type="file" id="msd-new-file")]
|
||||
td #[input#msd-new-file(type="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 #[input#msd-new-url(type="text" style="width: 100%")]
|
||||
tr.hidden#msd-new-part
|
||||
td(i18n="kvm_text70") Upload partition:
|
||||
td(width="100%") #[select(id="msd-new-part-selector")]
|
||||
td(width="100%") #[select#msd-new-part-selector]
|
||||
div(id="msd-uploading-sub" class="hidden")
|
||||
hr
|
||||
table(class="kv")
|
||||
|
||||
.hidden#msd-uploading-sub
|
||||
table.kv
|
||||
tr
|
||||
td(i18n="kvm_text74") New image:
|
||||
td(id="msd-uploading-name" class="value")
|
||||
td.value#msd-uploading-name
|
||||
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")
|
||||
td.value#msd-uploading-size
|
||||
.text
|
||||
.progress#msd-uploading-progress
|
||||
span.progress-value#msd-uploading-progress-value
|
||||
hr
|
||||
table(class="kv")
|
||||
|
||||
.hidden#msd-new-tips
|
||||
table.kv
|
||||
tr
|
||||
td(class="value" i18n="kvm_text71") Note:
|
||||
td.value(i18n="kvm_text71") Note:
|
||||
td(i18n="kvm_text72") • Don't close the browser page until the upload is complete.
|
||||
tr
|
||||
td
|
||||
td(i18n="kvm_text73") • 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
|
||||
|
||||
|
||||
.buttons.buttons-row
|
||||
button.row50#msd-connect-button(disabled i18n="kvm_text76") Connect drive to Server
|
||||
button.row25#msd-disconnect-button(disabled i18n="kvm_text77") Disconnect
|
||||
button.row25#msd-reset-button(disabled i18n="kvm_text78") Reset
|
||||
|
||||
hr
|
||||
div(class="text")
|
||||
|
||||
.text
|
||||
b(i18n="kvm_text85") Quick file transfer:
|
||||
br
|
||||
sub(i18n="kvm_text86") • Select NormalFiles tab to upload, package them and mount image
|
||||
@@ -117,8 +120,9 @@ li(id="msd-dropdown" class="right feature-disabled")
|
||||
sub(i18n="kvm_text87") • 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
|
||||
button.row50#msd-file-image-update-button(i18n="kvm_text88") Package files into image
|
||||
button.row50#msd-file-image-unzip-button(i18n="kvm_text89") Unpackage files from image
|
||||
hr
|
||||
|
||||
|
||||
|
||||
@@ -1,47 +1,51 @@
|
||||
li(id="shortcuts-dropdown" class="right")
|
||||
a(class="menu-button" href="#" i18n="kvm_text56") Shortcuts
|
||||
div(id="shortcuts-menu" class="menu")
|
||||
div(class="text")
|
||||
li.right#shortcuts-dropdown
|
||||
a.menu-button(href="#" i18n="kvm_text56") Shortcuts
|
||||
|
||||
.menu#shortcuts-menu
|
||||
.text
|
||||
b(i18n="kvm_text57") Quick keyboard shortcuts#[br]
|
||||
sub(i18n="kvm_text58") Also see #[i System → Show keyboard]
|
||||
hr
|
||||
div(class="buttons")
|
||||
div(class="buttons-row")
|
||||
button(data-force-hide-menu data-shortcut="CapsLock" class="row50")
|
||||
|
||||
.buttons
|
||||
.buttons-row
|
||||
button.row50(data-force-hide-menu data-shortcut="CapsLock")
|
||||
| • Caps Lock
|
||||
img(class="inline-lamp-small hid-keyboard-caps-led led-gray" src=`${svg_dir}/led-square.svg`)
|
||||
button(data-force-hide-menu data-shortcut="MetaLeft" class="row50") • Left Win
|
||||
img.inline-lamp-small.hid-keyboard-caps-led.led-gray(src=`${svg_dir}/led-square.svg`)
|
||||
button.row50(data-force-hide-menu data-shortcut="MetaLeft") • Left Win
|
||||
hr
|
||||
div(class="buttons-row")
|
||||
button(data-force-hide-menu data-shortcut="AltLeft ShiftLeft" class="row50") • Alt+Shift
|
||||
button(data-force-hide-menu data-shortcut="ControlLeft KeyW" class="row50") • Ctrl+W
|
||||
div(class="buttons-row")
|
||||
button(data-force-hide-menu data-shortcut="ControlLeft ShiftLeft" class="row50") • Ctrl+Shift
|
||||
button(data-force-hide-menu data-shortcut="AltLeft Tab" class="row50") • Alt+Tab
|
||||
div(class="buttons-row")
|
||||
button(data-force-hide-menu data-shortcut="ShiftLeft ShiftRight" class="row50") • Shift+Shift
|
||||
button(data-force-hide-menu data-shortcut="AltLeft Enter" class="row50") • Alt+Enter
|
||||
div(class="buttons-row")
|
||||
button(data-force-hide-menu data-shortcut="MetaLeft Space" class="row50") • Win+Space
|
||||
button(data-force-hide-menu data-shortcut="AltLeft F4" class="row50") • Alt+F4
|
||||
.buttons-row
|
||||
button.row50(data-force-hide-menu data-shortcut="AltLeft ShiftLeft") • Alt+Shift
|
||||
button.row50(data-force-hide-menu data-shortcut="ControlLeft KeyW") • Ctrl+W
|
||||
.buttons-row
|
||||
button.row50(data-force-hide-menu data-shortcut="ControlLeft ShiftLeft") • Ctrl+Shift
|
||||
button.row50(data-force-hide-menu data-shortcut="AltLeft Tab") • Alt+Tab
|
||||
.buttons-row
|
||||
button.row50(data-force-hide-menu data-shortcut="ShiftLeft ShiftRight") • Shift+Shift
|
||||
button.row50(data-force-hide-menu data-shortcut="AltLeft Enter") • Alt+Enter
|
||||
.buttons-row
|
||||
button.row50(data-force-hide-menu data-shortcut="MetaLeft Space") • Win+Space
|
||||
button.row50(data-force-hide-menu data-shortcut="AltLeft F4") • Alt+F4
|
||||
hr
|
||||
div(class="buttons-row")
|
||||
button(data-force-hide-menu data-shortcut="ControlLeft AltLeft F1" class="row50") • Ctrl+Alt+F1
|
||||
button(data-force-hide-menu data-shortcut="MetaLeft KeyL" class="row50") • Win+L
|
||||
div(class="buttons-row")
|
||||
button(data-force-hide-menu data-shortcut="ControlLeft AltLeft F2" class="row50") • Ctrl+Alt+F2
|
||||
button(data-force-hide-menu data-shortcut="PrintScreen" class="row50") • Print Screen
|
||||
.buttons-row
|
||||
button.row50(data-force-hide-menu data-shortcut="ControlLeft AltLeft F1") • Ctrl+Alt+F1
|
||||
button.row50(data-force-hide-menu data-shortcut="MetaLeft KeyL") • Win+L
|
||||
.buttons-row
|
||||
button.row50(data-force-hide-menu data-shortcut="ControlLeft AltLeft F2") • Ctrl+Alt+F2
|
||||
button.row50(data-force-hide-menu data-shortcut="PrintScreen") • Print Screen
|
||||
hr
|
||||
div(class="buttons-row")
|
||||
button(data-force-hide-menu data-shortcut="ControlLeft AltLeft Delete" class="row50") • Ctrl+Alt+Del
|
||||
button(data-force-hide-menu data-shortcut="Power" class="row50") • Power
|
||||
.buttons-row
|
||||
button.row50(data-force-hide-menu data-shortcut="ControlLeft AltLeft Delete") • Ctrl+Alt+Del
|
||||
button.row50(data-force-hide-menu data-shortcut="Power") • Power
|
||||
hr
|
||||
div(class="text")
|
||||
|
||||
.text
|
||||
| ↓ • 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")
|
||||
|
||||
.buttons
|
||||
.buttons-row
|
||||
-
|
||||
let sysrq = {
|
||||
"F": "Call the OOM killer to kill a memory hog process",
|
||||
@@ -50,9 +54,14 @@ li(id="shortcuts-dropdown" class="right")
|
||||
"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}
|
||||
button.row25(
|
||||
data-shortcut=`AltLeft PrintScreen Key${key}`
|
||||
data-shortcut-confirm="hid-sysrq-ask-switch"
|
||||
style="text-align: center"
|
||||
title=`${title}`
|
||||
) #{key}
|
||||
hr
|
||||
div(class="buttons-row")
|
||||
.buttons-row
|
||||
-
|
||||
sysrq = {
|
||||
"R": "Turn off keyboard raw mode, set it to XLATE",
|
||||
@@ -63,6 +72,11 @@ li(id="shortcuts-dropdown" class="right")
|
||||
"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}
|
||||
button.row16(
|
||||
data-shortcut=`AltLeft PrintScreen Key${key}`
|
||||
data-shortcut-confirm="hid-sysrq-ask-switch"
|
||||
style="text-align: center"
|
||||
title=`${title}`
|
||||
) #{key}
|
||||
hr
|
||||
+menu_switch("hid-sysrq-ask-switch", "Ask the magic confirmation", true, true,"hid-sysrq-ask-switch")
|
||||
+menu_switch_table("hid-sysrq-ask-switch", "Ask the magic confirmation", true, true,"hid-sysrq-ask-switch")
|
||||
|
||||
@@ -1,23 +1,32 @@
|
||||
li(id="switch-dropdown" class="right feature-disabled")
|
||||
a(class="menu-button" id="switch-menu-button" href="#")
|
||||
li.right.feature-disabled#switch-dropdown
|
||||
a.menu-button#switch-menu-button(href="#")
|
||||
+navbar_led("switch-atx-power-led", "led-atx-power")
|
||||
+navbar_led("switch-atx-hdd-led", "led-atx-hdd")
|
||||
span Switch #[i #[sub(id="switch-active-port") ]]
|
||||
div(id="switch-menu" class="menu")
|
||||
table(style="border-spacing: 0px;")
|
||||
span Switch #[i #[sub#switch-active-port]]
|
||||
|
||||
.menu#switch-menu
|
||||
table(style="border-spacing: 0px")
|
||||
tr
|
||||
td
|
||||
div(class="text")
|
||||
.text
|
||||
b #[a(target="_blank" href="https://docs.pikvm.org/switch") PiKVM Switch] is attached#[br]
|
||||
sub Select a port or perform any available action like ATX click
|
||||
td
|
||||
div(class="text")
|
||||
button(data-force-hide-menu data-show-window="switch-window" class="small") • Settings
|
||||
.text
|
||||
button.small(data-force-hide-menu data-show-window="switch-window") • Settings
|
||||
hr
|
||||
div(id="switch-message-update" class="hidden")
|
||||
|
||||
.hidden#switch-message-update
|
||||
+menu_message("info", "Good news! Your switch is ready to get the firmware update")
|
||||
| Please #[a(target="_blank" href="https://docs.pikvm.org/switch/#firmware-updating") follow the instructions] when you decide to install it.
|
||||
| Please #[a(target="_blank" href="https://docs.pikvm.org/switch/#firmware-updating") follow the instructions]
|
||||
| when you decide to install it.
|
||||
hr
|
||||
+menu_switch("switch-atx-ask-switch", "Ask ATX click confirmation", true, true)
|
||||
|
||||
table.kv
|
||||
tr
|
||||
+menu_switch_td2("switch-atx-ask-switch", true, true) Ask ATX click confirmation:
|
||||
tr
|
||||
+menu_switch_td2("switch-msd-ask-switch", true, true) Confirm switching when the Drive is connected:
|
||||
hr
|
||||
table(id="switch-chain" class="kv")
|
||||
|
||||
table.kv#switch-chain
|
||||
|
||||
@@ -1,154 +1,149 @@
|
||||
li(id="system-dropdown" class="right")
|
||||
a(class="menu-button" href="#")
|
||||
li.right#system-dropdown
|
||||
a.menu-button(href="#")
|
||||
+navbar_led("link-led", "led-link")
|
||||
+navbar_led("stream-led", "led-video")
|
||||
+navbar_led("hid-keyboard-led", "led-hid-keyboard")
|
||||
+navbar_led("hid-mouse-led", "led-hid-mouse")
|
||||
span(i18n="kvm_text3") System
|
||||
div(id="system-menu" class="menu")
|
||||
table(class="kv")
|
||||
|
||||
.menu#system-menu
|
||||
table.kv
|
||||
tr
|
||||
td(class="value" i18n="kvm_text4") Runtime settings & tools
|
||||
td(id="system-tool-webterm" class="feature-disabled") #[button(data-force-hide-menu data-show-window="webterm-window" class="small" i18n="kvm_text5") • Term]
|
||||
td(id="system-tool-about") #[button(data-force-hide-menu data-show-window="about-window" class="small" i18n="kvm_text6") • About]
|
||||
td(id="system-tool-log") #[button(data-force-hide-menu id="open-log-button" class="small" i18n="kvm_text7") • 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") • WoL
|
||||
td.value(i18n="kvm_text4")
|
||||
| Runtime settings & tools
|
||||
td.feature-disabled#system-tool-webterm
|
||||
button.small(data-force-hide-menu data-show-window="webterm-window" i18n="kvm_text5") • Term
|
||||
td#system-tool-about
|
||||
button.small(data-force-hide-menu data-show-window="about-window" i18n="kvm_text6") • About
|
||||
td#system-tool-log
|
||||
button.small#open-log-button(data-force-hide-menu i18n="kvm_text7") • Log
|
||||
td.feature-disabled#system-tool-wol
|
||||
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"
|
||||
) • WoL
|
||||
hr
|
||||
div(id="stream-message-no-webrtc" class="hidden")
|
||||
+menu_message("warning", "WebRTC is not supported by this browser")
|
||||
|
||||
.hidden#stream-message-no-webrtc
|
||||
+menu_message("warning", "WebRTC is not supported by this browser", "", "stream-message-no-support-webrtc")
|
||||
hr
|
||||
div(id="stream-message-no-vd" class="hidden")
|
||||
+menu_message("warning", "Direct HTTP H.264 streaming is not supported")
|
||||
.hidden#stream-message-no-vd
|
||||
+menu_message("warning", "Direct H.264 streaming is not supported", "", "stream-message-no-support-drict-h264")
|
||||
hr
|
||||
div(id="stream-message-no-h264" class="hidden")
|
||||
+menu_message("warning", "H.264 is not supported by this browser", "stream-message-no-h264")
|
||||
.hidden#stream-message-no-h264
|
||||
+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")
|
||||
|
||||
table.kv
|
||||
tr.feature-disabled#stream-resolution
|
||||
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")
|
||||
td #[select#stream-resolution-selector(disabled)]
|
||||
tr.feature-disabled#stream-quality
|
||||
+menu_slider_td3("stream-quality-slider", "stream-quality-value", false, "kvm_text10") JPEG quality:
|
||||
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") Legacy<br>MJPEG
|
||||
input(type="radio" id="stream-mode-radio-media" name="stream-mode-radio" value="media")
|
||||
label(for="stream-mode-radio-media") Direct<br>H.264
|
||||
input(type="radio" id="stream-mode-radio-janus" name="stream-mode-radio" value="janus")
|
||||
label(for="stream-mode-radio-janus") WebRTC<br>H.264
|
||||
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°
|
||||
input(type="radio" id="stream-orient-radio-180" name="stream-orient-radio" value="180")
|
||||
label(for="stream-orient-radio-180") 180°
|
||||
input(type="radio" id="stream-orient-radio-270" name="stream-orient-radio" value="270")
|
||||
label(for="stream-orient-radio-270") 270°
|
||||
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")
|
||||
tr(id="stream-mic" class="feature-disabled")
|
||||
+menu_switch_notable("stream-mic-switch", "Microphone", false, false)
|
||||
+menu_slider_td3("stream-desired-fps-slider", "stream-desired-fps-value", false, "kvm_text11") JPEG max fps:
|
||||
tr.feature-disabled#stream-h264-bitrate
|
||||
+menu_slider_td3("stream-h264-bitrate-slider", "stream-h264-bitrate-value", false, "kvm_text12") H.264 kbps:
|
||||
tr.feature-disabled#stream-h264-gop
|
||||
+menu_slider_td3("stream-h264-gop-slider", "stream-h264-gop-value", false, "kvm_text13") H.264 gop:
|
||||
tr.feature-disabled#stream-mode
|
||||
+menu_radio_td2("stream-mode-radio", [
|
||||
{title: "WebRTC<br>H.264", value: "janus"},
|
||||
{title: "Direct<br>H.264", value: "media"},
|
||||
{title: "Legacy<br>MJPEG", value: "mjpeg", checked: true},
|
||||
], "kvm_text14") #[a(target="_blank" href="https://docs.pikvm.org/webrtc") Video mode]:
|
||||
tr.feature-disabled#stream-orient
|
||||
+menu_radio_td2("stream-orient-radio", [
|
||||
{title: "Default", value: "0", checked: true},
|
||||
{title: "90°", value: "90"},
|
||||
{title: "180°", value: "180"},
|
||||
{title: "270°", value: "270"},
|
||||
], "kvm_text17") Orientation:
|
||||
tr.feature-disabled#stream-audio
|
||||
+menu_slider_td3("stream-audio-volume-slider", "stream-audio-volume-value", false, "kvm_text19") Audio volume:
|
||||
tr.feature-disabled#stream-mic
|
||||
+menu_switch_td2("stream-mic-switch", false, "stream-mic-switch") Microphone:
|
||||
hr
|
||||
div(class="buttons buttons-row")
|
||||
button(data-force-hide-menu data-show-window="stream-window" class="row33" i18n="kvm_text20") • Show stream
|
||||
button(data-force-hide-menu id="stream-screenshot-button" class="row33" i18n="kvm_text21") • Screenshot
|
||||
button(id="stream-reset-button" class="row33" i18n="kvm_text22") Reset stream
|
||||
div(class="text")
|
||||
|
||||
.buttons.buttons-row
|
||||
button.row33(data-force-hide-menu data-show-window="stream-window" i18n="kvm_text20") • Show stream
|
||||
button.row33#stream-screenshot-button(data-force-hide-menu i18n="kvm_text21") • Screenshot
|
||||
button.row33#stream-reset-button( i18n="kvm_text22") Reset stream
|
||||
hr
|
||||
|
||||
|
||||
.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") • Start recording
|
||||
button(data-force-hide-menu id="stream-record-stop-button" class="row50" i18n="kvm_text82") • End recording
|
||||
.buttons.buttons-row
|
||||
button.row50#stream-record-start-button(data-force-hide-menu i18n="kvm_text81") • Start recording
|
||||
button.row50#stream-record-stop-button(data-force-hide-menu i18n="kvm_text82") • End recording
|
||||
hr
|
||||
table(class="kv")
|
||||
tr(id="hid-outputs-keyboard", class="feature-disabled")
|
||||
|
||||
table.kv
|
||||
tr.feature-disabled#hid-outputs-keyboard
|
||||
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 & 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
|
||||
+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")
|
||||
td #[div.radio-box#hid-outputs-keyboard-box]
|
||||
tr.feature-disabled#hid-outputs-mouse
|
||||
td #[a(target="_blank" href="https://docs.pikvm.org/mouse" i18n="kvm_text24") Mouse mode]:
|
||||
td #[div.radio-box#hid-outputs-mouse-box]
|
||||
|
||||
+menu_spoiler("Keyboard & mouse (HID) settings", "kvm_text25")
|
||||
+menu_switch_table("hid-keyboard-swap-cc-switch", true, false, "hid-keyboard-swap-cc-switch") Swap Left Ctrl and Caps keys:
|
||||
hr
|
||||
table(class="kv")
|
||||
tr
|
||||
+menu_slider_td3("hid-mouse-rate-slider", "hid-mouse-rate-value", false, "kvm_text26") Mouse polling:
|
||||
tr.feature-disabled#hid-mouse-sens
|
||||
+menu_slider_td3("hid-mouse-sens-slider", "hid-mouse-sens-value", false, "kvm_text27") Relative sensitivity:
|
||||
tr(id="hid-mouse-squash" class="feature-disabled")
|
||||
+menu_switch_td2("hid-mouse-squash-switch", false, "hid-mouse-squash-switch") Squash relative moves:
|
||||
tr
|
||||
td Reverse scrolling:
|
||||
td
|
||||
table
|
||||
tr
|
||||
+menu_switch_td2("hid-mouse-reverse-scrolling-switch", true, false, "hid-mouse-reverse-scrolling-switch") Y:
|
||||
td
|
||||
+menu_switch_td2("hid-mouse-reverse-panning-switch", true, false, "hid-mouse-reverse-panning-switch") X:
|
||||
tr
|
||||
+menu_switch_td2("hid-mouse-cumulative-scrolling-switch", true, false, "hid-mouse-cumulative-scrolling-switch") Cumulative scrolling:
|
||||
tr
|
||||
+menu_slider_td3("hid-mouse-scroll-slider", "hid-mouse-scroll-value", true, "kvm_text29") Scroll rate:
|
||||
tr
|
||||
+menu_switch_td2("hid-mouse-dot-switch", true, true, "hid-mouse-dot-switch") Show the blue dot:
|
||||
|
||||
+menu_spoiler("Web UI settings")
|
||||
table.kv
|
||||
tr
|
||||
+menu_switch_td2("page-close-ask-switch", true, true, "page-close-ask-switch") Ask page close confirmation:
|
||||
tr
|
||||
+menu_switch_td2("page-full-tab-stream-switch", true, false, "page-full-tab-stream-switch") Expand for the entire tab by default:
|
||||
tr
|
||||
+menu_switch_td2("stream-suspend-switch", true, false, "stream-suspend-switch") Suspend stream when tab is not active:
|
||||
|
||||
table.kv
|
||||
tr
|
||||
+menu_switch_notable("hid-keyboard-bad-link-switch", "Bad link mode (release keys immediately)", true, false,"hid-keyboard-bad-link-switch")
|
||||
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")
|
||||
+menu_switch_td2("hid-keyboard-bad-link-switch", true, false, "hid-keyboard-bad-link-switch") Bad link mode (release keys immediately):
|
||||
tr.feature-disabled#hid-connect
|
||||
+menu_switch_td2("hid-connect-switch", true, true, "hid-connect-switch") Connect HID to Server:
|
||||
tr.feature-disabled#hid-jiggler
|
||||
+menu_switch_td2("hid-jiggler-switch", false, false, "hid-jiggler-switch")
|
||||
| #[a(target="_blank" href="https://docs.pikvm.org/mouse_jiggler") Mouse jiggler]:
|
||||
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")
|
||||
+menu_switch_td2("hid-mute-switch", true, false, "hid-mute-switch") Mute all input HID events:
|
||||
tr.feature-disabled#v3-usb-breaker
|
||||
+menu_switch_td2_gpio(
|
||||
"__v3_usb_breaker__",
|
||||
"Turning off this switch will disconnect the main USB from the server. Are you sure you want to continue?"
|
||||
) Connect main USB to Server:
|
||||
tr.feature-disabled#v4-locator
|
||||
+menu_switch_td2_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") • Show keyboard
|
||||
button(disabled id="hid-reset-button" class="row50" i18n="kvm_text31") Reset HID
|
||||
|
||||
.buttons.buttons-row
|
||||
button.row50(data-force-hide-menu data-show-window="keyboard-window" i18n="kvm_text30") • Show keyboard
|
||||
button.row50#hid-reset-button(disabled i18n="kvm_text31") Reset HID
|
||||
|
||||
@@ -1,52 +1,46 @@
|
||||
li(id="text-dropdown" class="right")
|
||||
a(class="menu-button" href="#")
|
||||
li.right#text-dropdown
|
||||
a.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")
|
||||
.menu#text-menu(style="width: 360px")
|
||||
.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")
|
||||
.text(style="margin-right: 20px")
|
||||
textarea#hid-pak-text(data-focus placeholder="Enter your text here")
|
||||
table.kv
|
||||
tr
|
||||
td
|
||||
button(disabled data-force-hide-menu id="hid-pak-button" i18n="kvm_text47") • Paste
|
||||
button#hid-pak-button(disabled data-force-hide-menu title="Ctrl+Enter when the text is focused" i18n="kvm_text47") • Paste
|
||||
td(i18n="kvm_text48") using host keymap
|
||||
td
|
||||
select(id="hid-pak-keymap-selector")
|
||||
table(class="kv")
|
||||
td #[select#hid-pak-keymap-selector]
|
||||
table.kv
|
||||
tr
|
||||
+menu_switch_notable("hid-pak-slow-switch", "Slow typing", true, false)
|
||||
+menu_slider_td3("hid-pak-delay-slider", "hid-pak-delay-value", true, false, "hid-pak-slow-switch") Delay between keys:
|
||||
tr
|
||||
+menu_switch_notable("hid-pak-secure-switch", "Hide input text", true, false)
|
||||
+menu_switch_td2("hid-pak-secure-switch", true, false, "hid-pak-secure-switch") Hide input text:
|
||||
tr
|
||||
+menu_switch_notable("hid-pak-slow-switch", "Slow typing", true, false, "hid-pak-slow-switch")
|
||||
tr
|
||||
+menu_switch_notable("hid-pak-secure-switch", "Hide input text", true, false, "hid-pak-secure-switch")
|
||||
tr
|
||||
+menu_switch_notable("hid-pak-ask-switch", "Ask paste confirmation", true, true, "hid-pak-ask-switch")
|
||||
div(id="stream-ocr" class="feature-disabled")
|
||||
+menu_switch_td2("hid-pak-ask-switch", true, true, "hid-pak-ask-switch") Ask paste confirmation:
|
||||
|
||||
.feature-disabled#stream-ocr
|
||||
hr
|
||||
br
|
||||
hr
|
||||
div(class="text")
|
||||
.text
|
||||
b(i18n="kvm_text49") Text recognition <sup><i>β</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")
|
||||
table.kv
|
||||
tr
|
||||
td
|
||||
button(data-force-hide-menu id="stream-ocr-button" i18n="kvm_text51") • Select area
|
||||
td #[button#stream-ocr-button(data-force-hide-menu i18n="kvm_text51") • Select area]
|
||||
td(i18n="kvm_text52") for
|
||||
td
|
||||
select(id="stream-ocr-lang-selector")
|
||||
td #[select#stream-ocr-lang-selector]
|
||||
td(i18n="kvm_text53") text recognition
|
||||
table(class="kv")
|
||||
table.kv
|
||||
tr
|
||||
td(colspan="4" i18n="kvm_text54") • Press #[b Enter] to recognize and copy text to clipboard
|
||||
td(colspan="4" i18n="kvm_text54") • Next hit the #[b Enter] to recognize and copy text to clipboard...
|
||||
tr
|
||||
td(colspan="4" i18n="kvm_text55") • Press #[b Esc] to cancel selection
|
||||
td(colspan="4" i18n="kvm_text55") • Or hit the #[b Escape] to cancel a selection
|
||||
tr
|
||||
td
|
||||
|
||||
@@ -2,7 +2,7 @@ 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")
|
||||
.text
|
||||
table
|
||||
tr
|
||||
td(rowspan="2") #[img(class=`sign ${classes}` src=`${svg_dir}/${icon}.svg`)]
|
||||
@@ -13,34 +13,76 @@ mixin menu_message(icon, short, classes="", i18nid)
|
||||
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}:
|
||||
mixin menu_switch_td2_gpio(channel, confirm_off="")
|
||||
td
|
||||
block
|
||||
td(align="right")
|
||||
div(class="switch-box")
|
||||
.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.switch-inner
|
||||
span.switch
|
||||
|
||||
|
||||
mixin menu_switch_td2(id, enabled, checked, i18nid)
|
||||
td
|
||||
block
|
||||
td(align="right")
|
||||
.switch-box
|
||||
input(checked=checked disabled=!enabled type="checkbox" id=id)
|
||||
label(for=id)
|
||||
span(class="switch-inner")
|
||||
span(class="switch")
|
||||
span.switch-inner
|
||||
span.switch
|
||||
|
||||
mixin menu_switch(id, title, enabled, checked, i18nid)
|
||||
table(class="kv")
|
||||
mixin menu_switch_table(id, title, enabled, checked, i18nid)
|
||||
table.kv
|
||||
tr
|
||||
+menu_switch_notable(id, title, enabled, checked, i18nid)
|
||||
+menu_switch_td2(id, title, enabled, checked, i18nid)
|
||||
block
|
||||
|
||||
ul(id="navbar")
|
||||
li(class="left")
|
||||
a(id="logo" href="/") ←
|
||||
img(class="svg-gray" src=`${svg_dir}/logo.svg` alt="π-kvm")
|
||||
|
||||
mixin menu_radio_td2(name, items, i18nid)
|
||||
td
|
||||
block
|
||||
td
|
||||
.radio-box
|
||||
each item in items
|
||||
-
|
||||
let id = `${name}-${item["value"]}`
|
||||
let checked = (item["checked"] || false)
|
||||
input(type="radio" id=id name=name value=item["value"] checked=checked)
|
||||
label(for=id) !{item["title"]}
|
||||
|
||||
|
||||
mixin menu_slider_td3(slider_id, value_id, enabled, i18nid)
|
||||
-
|
||||
enabled = (enabled || true)
|
||||
td
|
||||
block
|
||||
td.value-slider
|
||||
input(type="range" id=slider_id disabled=!enabled)
|
||||
td.value-number(id=value_id)
|
||||
|
||||
|
||||
mixin menu_spoiler(title, i18nid)
|
||||
details
|
||||
summary !{title}
|
||||
div(class="spoiler")
|
||||
block
|
||||
|
||||
|
||||
ul#navbar
|
||||
li.left
|
||||
a(id="logo" href=root_prefix) ←
|
||||
img.svg-gray(src=`${svg_dir}/logo.svg` alt="π-kvm")
|
||||
|
||||
include navbar-health.pug
|
||||
|
||||
|
||||
@@ -2,46 +2,52 @@ 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")
|
||||
.tab
|
||||
.code(id=`about-${name}`)
|
||||
if block
|
||||
block
|
||||
else
|
||||
span(class="code-comment") No data
|
||||
span.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 ×]
|
||||
div(id="about")
|
||||
|
||||
.window#about-window(data-show-centered)
|
||||
.window-header
|
||||
.window-grab(i18n="kvm_text1") About
|
||||
.window-buttons
|
||||
button.window-button-original •
|
||||
button.window-button-close #[b ×]
|
||||
|
||||
#about
|
||||
table
|
||||
tr
|
||||
td(class="logo")
|
||||
td.logo
|
||||
a(href="https://pikvm.org" target="_blank")
|
||||
img(class="svg-gray" src=`${svg_dir}/logo.svg` alt="PiKVM" height="40")
|
||||
img.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")
|
||||
td.title(colspan="2" i18n="index_title")
|
||||
| The Open Source KVM over IP
|
||||
tr
|
||||
td.copyright(colspan="2" i18n="copyright")
|
||||
| Copyright © 2018-2024 #[a(target="_blank" href="mailto:mdevaev@gmail.com") Maxim Devaev]
|
||||
br
|
||||
div(class="tabs-box")
|
||||
+about_tab("meta", "Meta", "meta", true)
|
||||
.tabs-box
|
||||
+about_tab("version", "Version", "version", true)
|
||||
+about_tab("hardware", "Hardware", "hardware")
|
||||
|
||||
+about_tab("meta", "Meta", "meta")
|
||||
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]
|
||||
span.code-comment
|
||||
| // You can get this JSON using handle
|
||||
| #[a(target="_blank" href=`${root_prefix}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")
|
||||
pre#kvmd-meta-json No data
|
||||
|
||||
+about_tab("thanks", "Thanks", "thanks")
|
||||
span(class="code-comment" i18n="kvm_text2")
|
||||
span.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]
|
||||
@@ -50,7 +56,7 @@ div(id="about-window" class="window")
|
||||
| // you can donate on #[a(target="_blank" href="https://www.patreon.com/pikvm") Patreon]
|
||||
| or #[a(target="_blank" href="https://paypal.me/pikvm") Paypal].
|
||||
br
|
||||
p(class="text credits")
|
||||
p.text.credits
|
||||
a(target="_blank" href="https://github.com/mofeng-git/One-KVM" i18n="index_text_12") One-KVM Project
|
||||
| |
|
||||
a(target="_blank" href="https://one-kvm.mofeng.run" i18n="index_text_13") One-KVM Documentation
|
||||
|
||||
@@ -1,41 +1,45 @@
|
||||
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 spacer(sp)
|
||||
if sp == 1
|
||||
.spacer
|
||||
else if sp == 2
|
||||
.spacer-fixed
|
||||
|
||||
mixin modifier(spacer, code, classes="", width=0)
|
||||
div(data-code=code class=`modifier ${classes}` style=(width ? `width:${width}px` : ""))
|
||||
div(class="label")
|
||||
|
||||
mixin key(sp, code, classes="", width=0)
|
||||
div(data-code=code, class=`key ${classes}`, style=(width ? `width: ${width}px` : ""))
|
||||
.label
|
||||
block
|
||||
+spacer(sp)
|
||||
|
||||
|
||||
mixin modifier(sp, code, classes="", width=0)
|
||||
div(data-code=code, data-allow-autohold, class=`key ${classes}`, style=(width ? `width: ${width}px` : ""))
|
||||
.label
|
||||
| #[b •]#[br]
|
||||
block
|
||||
if spacer == 1
|
||||
div(class="spacer")
|
||||
else if spacer == 2
|
||||
div(class="spacer-fixed")
|
||||
+spacer(sp)
|
||||
|
||||
|
||||
mixin empty(sp, classes="", width=0)
|
||||
div(class=`empty ${classes}`, style=(width ? `width:${width}px` : ""))
|
||||
.label
|
||||
+spacer(sp)
|
||||
|
||||
mixin empty(spacer, classes="", width=0)
|
||||
div(class=`empty ${classes}` style=(width ? `width:${width}px` : ""))
|
||||
div(class="label")
|
||||
if spacer == 1
|
||||
div(class="spacer")
|
||||
else if spacer == 2
|
||||
div(class="spacer-fixed")
|
||||
|
||||
mixin lamp(cls)
|
||||
img(class=`inline-lamp-small ${cls} led-gray` src=`${svg_dir}/led-square.svg`)
|
||||
|
||||
div(id="keyboard-window" class="window")
|
||||
div(id="keyboard-window-header" class="window-header")
|
||||
div(class="window-grab" i18n="kvm_text15") Virtual Keyboard
|
||||
button(class="window-button-close") #[b ×]
|
||||
|
||||
div(id="keyboard-desktop" class="keypad" align="center")
|
||||
div(class="keypad-block")
|
||||
div(class="keypad-row")
|
||||
.window#keyboard-window(data-show-centered)
|
||||
.window-header#keyboard-window-header
|
||||
.window-grab(i18n="kvm_text15") Virtual Keyboard
|
||||
.window-buttons
|
||||
button.window-button-original •
|
||||
button.window-button-close #[b ×]
|
||||
|
||||
.keypad#keyboard-desktop(align="center")
|
||||
.keypad-block
|
||||
.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"]
|
||||
@@ -43,7 +47,7 @@ div(id="keyboard-window" class="window")
|
||||
if key == "F4" || key == "F8"
|
||||
+empty(1, "", 10)
|
||||
hr
|
||||
div(class="keypad-row")
|
||||
.keypad-row
|
||||
+key(1, "Backquote") ~#[br]`
|
||||
each key, index in ["!", "@", "#", "$", "%", "^", "&", "*", "("]
|
||||
+key(1, `Digit${index + 1}`) #{key}#[br]#{index + 1}
|
||||
@@ -51,14 +55,14 @@ div(id="keyboard-window" class="window")
|
||||
+key(1, "Minus") _#[br]-
|
||||
+key(1, "Equal") +#[br]=
|
||||
+key(0, "Backspace", "wide-1 right") ↤
|
||||
div(class="keypad-row")
|
||||
.keypad-row
|
||||
+key(1, "Tab", "wide-1 left") ⇤#[br]⇥
|
||||
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]\
|
||||
div(class="keypad-row")
|
||||
.keypad-row
|
||||
+key(1, "CapsLock", "wide-2 left small")
|
||||
+lamp("hid-keyboard-caps-led")
|
||||
| #[br] Caps Lock
|
||||
@@ -67,7 +71,7 @@ div(id="keyboard-window" class="window")
|
||||
+key(1, "Semicolon") :#[br];
|
||||
+key(1, "Quote") "#[br]'
|
||||
+key(0, "Enter", "wide-2 right small") Enter#[br]↵
|
||||
div(class="keypad-row")
|
||||
.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}
|
||||
@@ -75,7 +79,7 @@ div(id="keyboard-window" class="window")
|
||||
+key(1, "Period") >#[br].
|
||||
+key(1, "Slash") ?#[br]/
|
||||
+modifier(0, "ShiftRight", "wide-3 right small") Shift
|
||||
div(class="keypad-row")
|
||||
.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
|
||||
@@ -84,83 +88,83 @@ div(id="keyboard-window" class="window")
|
||||
+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")
|
||||
.keypad-block
|
||||
.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")
|
||||
.keypad-row
|
||||
+key(2, "Insert", "small") Ins
|
||||
+key(2, "Home", "small") Home
|
||||
+key(0, "PageUp", "small") PgUp
|
||||
div(class="keypad-row")
|
||||
.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")
|
||||
.keypad-row
|
||||
.keypad-row
|
||||
+empty(1, "")
|
||||
+key(2, "ArrowUp") ↑
|
||||
+empty(0, "")
|
||||
div(class="keypad-row")
|
||||
.keypad-row
|
||||
+key(2, "ArrowLeft") ←
|
||||
+key(2, "ArrowDown") ↓
|
||||
+key(0, "ArrowRight") →
|
||||
div(class="keypad-block")
|
||||
div(class="keypad-row")
|
||||
.keypad-block
|
||||
.keypad-row
|
||||
+empty(2, "small")
|
||||
+empty(2, "small")
|
||||
+empty(2, "small")
|
||||
+key(0, "Power", "small") PWR
|
||||
hr
|
||||
div(class="keypad-row")
|
||||
.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")
|
||||
.keypad-row
|
||||
+key(2, "Numpad7", "small") 7#[br]Home
|
||||
+key(2, "Numpad8", "small") 8#[br]↑
|
||||
+key(2, "Numpad9", "small") 9#[br]PgUp
|
||||
+empty(0, "")
|
||||
div(class="keypad-row")
|
||||
.keypad-row
|
||||
+key(2, "Numpad4", "small") 4#[br]←
|
||||
+key(2, "Numpad5", "small") 5#[br]#[br]
|
||||
+key(2, "Numpad6", "small") 6#[br]→
|
||||
+key(0, "NumpadAdd") +
|
||||
div(class="keypad-row")
|
||||
.keypad-row
|
||||
+key(2, "Numpad1", "small") 1#[br]End
|
||||
+key(2, "Numpad2", "small") 2#[br]↓
|
||||
+key(2, "Numpad3", "small") 3#[br]PgDn
|
||||
+empty(0, "")
|
||||
div(class="keypad-row")
|
||||
.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")
|
||||
.keypad-block
|
||||
.keypad-row
|
||||
+key(0, "IntlBackslash", "small") \#[br]|
|
||||
hr
|
||||
div(class="keypad-row")
|
||||
.keypad-row
|
||||
+key(0, "IntlYen", "small") ¥#[br]_
|
||||
div(class="keypad-row")
|
||||
.keypad-row
|
||||
+key(0, "IntlRo", "small") \#[br]ろ
|
||||
div(class="keypad-row")
|
||||
.keypad-row
|
||||
+modifier(0, "KanaMode", "small") Kana
|
||||
div(class="keypad-row")
|
||||
.keypad-row
|
||||
+modifier(0, "NonConvert", "small") N/Cnv
|
||||
div(class="keypad-row")
|
||||
.keypad-row
|
||||
+modifier(0, "Convert", "small") Cnv
|
||||
|
||||
div(id="keyboard-mobile" class="keypad" align="center")
|
||||
div(class="keypad-block")
|
||||
div(class="keypad-row")
|
||||
.keypad#keyboard-mobile(align="center")
|
||||
.keypad-block
|
||||
.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
|
||||
@@ -183,7 +187,7 @@ div(id="keyboard-window" class="window")
|
||||
+key(1, "Home", "small") Home
|
||||
+key(1, "End", "small") End
|
||||
+key(0, "Delete", "small") Del
|
||||
div(class="keypad-row")
|
||||
.keypad-row
|
||||
+key(1, "Backquote") ~#[br]`
|
||||
each key, index in ["!", "@", "#", "$", "%", "^", "&", "*", "("]
|
||||
+key(1, `Digit${index + 1}`) #{key}#[br]#{index + 1}
|
||||
@@ -191,14 +195,14 @@ div(id="keyboard-window" class="window")
|
||||
+key(1, "Minus") _#[br]-
|
||||
+key(1, "Equal") +#[br]=
|
||||
+key(0, "Backspace", "wide-2 right", 101) ↤
|
||||
div(class="keypad-row")
|
||||
.keypad-row
|
||||
+key(1, "Tab", "wide-1 left") ⇤<br>⇥
|
||||
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]\
|
||||
div(class="keypad-row")
|
||||
.keypad-row
|
||||
+key(1, "CapsLock", "wide-2 left small")
|
||||
+lamp("hid-keyboard-caps-led")
|
||||
| #[br] Caps Lock
|
||||
@@ -207,7 +211,7 @@ div(id="keyboard-window" class="window")
|
||||
+key(1, "Semicolon") :#[br];
|
||||
+key(1, "Quote") `#[br]'
|
||||
+key(0, "Enter", "wide-3 right small", 116) Enter#[br]↵
|
||||
div(class="keypad-row")
|
||||
.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}
|
||||
@@ -217,7 +221,7 @@ div(id="keyboard-window" class="window")
|
||||
+key(2, "PageUp", "small") PgUp
|
||||
+key(2, "ArrowUp") ↑
|
||||
+key(0, "PageDown", "small") PgDn
|
||||
div(class="keypad-row")
|
||||
.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
|
||||
|
||||
@@ -1,50 +1,39 @@
|
||||
div(id="stream-ocr-window" class="window")
|
||||
div(id="stream-ocr-selection" class="hidden")
|
||||
.window#stream-ocr-window
|
||||
.hidden#stream-ocr-selection
|
||||
|
||||
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 ×]
|
||||
button(class="window-button-maximize") ☐
|
||||
button(class="window-button-original") •
|
||||
button(class="window-button-enter-full-tab") ▲
|
||||
button(class="window-button-full-screen") ⤢
|
||||
.window.window-resizable#stream-window(data-show-maximized data-organize-hook)
|
||||
.window-header#stream-window-header
|
||||
.window-grab MJPEG
|
||||
.window-buttons
|
||||
button.window-button-full-screen ⤢
|
||||
button.window-button-enter-full-tab ▲
|
||||
button.window-button-original •
|
||||
button.window-button-maximize ☐
|
||||
button.window-button-close #[b ×]
|
||||
|
||||
div(id="stream-info")
|
||||
#stream-info
|
||||
|
||||
button(class="window-button-exit-full-tab") ▼
|
||||
div(id="stream-box" class="stream-box-offline")
|
||||
img(id="stream-image" src=`${png_dir}/blank-stream.png`)
|
||||
video(id="stream-video" class="hidden" disablePictureInPicture="true" autoplay playsinline muted)
|
||||
canvas(id="stream-canvas" class="hidden")
|
||||
div(id="stream-fullscreen-active")
|
||||
button.window-button-exit-full-tab ▼
|
||||
|
||||
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 •]#[br]Hold
|
||||
.stream-box-offline#stream-box
|
||||
img#stream-image(src=`${png_dir}/blank-stream.png`)
|
||||
video.hidden#stream-video(disablePictureInPicture="true" autoplay playsinline muted)
|
||||
canvas.hidden#stream-canvas
|
||||
#stream-fullscreen-active
|
||||
|
||||
div(class="empty" style="width:15px")
|
||||
.keypad#stream-mouse-buttons(align="center")
|
||||
.keypad-block
|
||||
.keypad-row
|
||||
.key.wide-3.left.rounded-left(data-code="left" data-allow-autohold)
|
||||
.label #[b •]#[br]Left
|
||||
.key.wide-1.rounded-none(data-code="middle" data-allow-autohold)
|
||||
.label #[b •]#[br]Mid
|
||||
.key.wide-3.right.rounded-right(data-code="right" data-allow-autohold)
|
||||
.label #[b •]#[br]Right
|
||||
|
||||
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 •]#[br]Hold
|
||||
.empty(style="width: 30px")
|
||||
|
||||
div(class="empty" style="width:15px")
|
||||
|
||||
div(data-code="right" class="modifier right small rounded-left")
|
||||
div(class="label") #[b •]#[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")
|
||||
.key.small.rounded-left(data-code="up" data-allow-autohold)
|
||||
.label #[b •]#[br]Up
|
||||
.key.small.rounded-right(data-code="down" data-allow-autohold)
|
||||
.label #[b •]#[br]Down
|
||||
|
||||
@@ -1,95 +1,73 @@
|
||||
mixin switch_tab(name, title, checked=false)
|
||||
- let button_id = `switch-tab-${name}-button`
|
||||
input(checked=checked type="radio" name="switch-tab-button", id=button_id)
|
||||
input(checked=checked type="radio" name="switch-tab-button" id=button_id)
|
||||
label(for=button_id) #{title}
|
||||
div(class="tab")
|
||||
.tab
|
||||
block
|
||||
|
||||
div(id="switch-window" class="window" style="width:min-content")
|
||||
div(class="window-header")
|
||||
div(class="window-grab") Switch settings
|
||||
button(class="window-button-close") #[b ×]
|
||||
|
||||
div(class="tabs-box")
|
||||
mixin color_slider_tr(name, title)
|
||||
tr
|
||||
td(style="white-space: nowrap") #{title}:
|
||||
td #[input(type="color" id=`switch-color-${name}-input`)]
|
||||
td #[input(type="range" id=`switch-color-${name}-brightness-slider` style="min-width: 150px")]
|
||||
td
|
||||
td #[button(id=`switch-color-${name}-default-button` class="small" title="Reset default") ↻]
|
||||
|
||||
|
||||
.window#switch-window(data-show-centered style="width: min-content")
|
||||
.window-header
|
||||
.window-grab Switch settings
|
||||
.window-buttons
|
||||
button.window-button-original •
|
||||
button.window-button-close #[b ×]
|
||||
|
||||
.tabs-box
|
||||
+switch_tab("edid", "EDIDs collection", true)
|
||||
table
|
||||
tr
|
||||
td(colspan="2")
|
||||
select(id="switch-edid-selector" size="8")
|
||||
td(rowspan="2" style="vertical-align:top")
|
||||
table(class="kv")
|
||||
select#switch-edid-selector(
|
||||
size="8"
|
||||
style="width: -webkit-fill-available; width: -moz-available;"
|
||||
)
|
||||
td(rowspan="2" style="vertical-align: top")
|
||||
table.kv
|
||||
tr
|
||||
td Manufacturer:
|
||||
td(id="switch-edid-info-mfc-id" class="value")
|
||||
td.value#switch-edid-info-mfc-id
|
||||
tr
|
||||
td Product ID:
|
||||
td(id="switch-edid-info-product-id" class="value")
|
||||
td.value#switch-edid-info-product-id
|
||||
tr
|
||||
td Serial:
|
||||
td(id="switch-edid-info-serial" class="value")
|
||||
td.value#switch-edid-info-serial
|
||||
tr
|
||||
td Monitor name:
|
||||
td(id="switch-edid-info-monitor-name" class="value")
|
||||
td.value#switch-edid-info-monitor-name
|
||||
tr
|
||||
td Extra serial:
|
||||
td(id="switch-edid-info-monitor-serial" class="value")
|
||||
td.value#switch-edid-info-monitor-serial
|
||||
tr
|
||||
td Audio enabled:
|
||||
td(id="switch-edid-info-audio" class="value")
|
||||
td.value#switch-edid-info-audio
|
||||
tr
|
||||
td Data:
|
||||
td #[button(disabled id="switch-edid-copy-data-button" class="small") Copy]
|
||||
td #[button.small#switch-edid-copy-data-button(disabled) Copy]
|
||||
tr
|
||||
td #[button(id="switch-edid-add-button") Add new]
|
||||
td(style="float:right") #[button(disabled id="switch-edid-remove-button") Remove]
|
||||
td #[button#switch-edid-add-button Add new]
|
||||
td(style="float: right") #[button#switch-edid-remove-button(disabled) Remove]
|
||||
|
||||
+switch_tab("colors", "Color scheme")
|
||||
table
|
||||
//tr
|
||||
td Role
|
||||
td Color
|
||||
td Brightness
|
||||
td
|
||||
td Reset
|
||||
//tr
|
||||
td #[hr]
|
||||
td #[hr]
|
||||
td #[hr]
|
||||
td
|
||||
td #[hr]
|
||||
tr
|
||||
td(style="white-space: nowrap") Selected port:
|
||||
td #[input(type="color" id="switch-color-active-input")]
|
||||
td #[input(type="range" id="switch-color-active-brightness-slider" style="min-width:150px")]
|
||||
td
|
||||
td #[button(id="switch-color-active-default-button" class="small" title="Reset default") ↻]
|
||||
tr
|
||||
td(style="white-space: nowrap") Inactive port:
|
||||
td #[input(type="color" id="switch-color-inactive-input")]
|
||||
td #[input(type="range" id="switch-color-inactive-brightness-slider" style="min-width:150px")]
|
||||
td
|
||||
td #[button(id="switch-color-inactive-default-button" class="small" title="Reset default") ↻]
|
||||
tr
|
||||
td(style="white-space: nowrap") Blinking beacon:
|
||||
td #[input(type="color" id="switch-color-beacon-input")]
|
||||
td #[input(type="range" id="switch-color-beacon-brightness-slider" style="min-width:150px")]
|
||||
td
|
||||
td #[button(id="switch-color-beacon-default-button" class="small" title="Reset default") ↻]
|
||||
+color_slider_tr("active", "Selected port")
|
||||
+color_slider_tr("inactive", "Inactive port")
|
||||
+color_slider_tr("beacon", "Blinking beacon")
|
||||
tr
|
||||
td #[hr]
|
||||
td #[hr]
|
||||
td #[hr]
|
||||
td
|
||||
td #[hr]
|
||||
tr
|
||||
td(style="white-space: nowrap") Flashing downlink:
|
||||
td #[input(type="color" id="switch-color-flashing-input")]
|
||||
td #[input(type="range" id="switch-color-flashing-brightness-slider" style="min-width:150px")]
|
||||
td
|
||||
td #[button(id="switch-color-flashing-default-button" class="small" title="Reset default") ↻]
|
||||
tr
|
||||
td(style="white-space: nowrap") Bootloader mode:
|
||||
td #[input(type="color" id="switch-color-bootloader-input")]
|
||||
td #[input(type="range" id="switch-color-bootloader-brightness-slider" style="min-width:150px")]
|
||||
td
|
||||
td #[button(id="switch-color-bootloader-default-button" class="small" title="Reset default") ↻]
|
||||
+color_slider_tr("flashing", "Flashing downlink")
|
||||
+color_slider_tr("bootloader", "Bootloader mode")
|
||||
|
||||
@@ -1,8 +1,11 @@
|
||||
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 ×]
|
||||
button(class="window-button-maximize") ☐
|
||||
// Терминал глючит из-за зажимаемой клавиши ESC для выхода
|
||||
// button(class="window-button-full-screen") ⤢
|
||||
iframe(id="webterm-iframe" src="" style="width: 100%; height: 100%")
|
||||
.window.window-resizable#webterm-window(data-show-centered style="display: flex; min-width: 720px; min-height: 480px")
|
||||
.window-header
|
||||
.window-grab(i18n="kvm_text16") Terminal
|
||||
.window-buttons
|
||||
button.window-button-original •
|
||||
button.window-button-maximize ☐
|
||||
button.window-button-close #[b ×]
|
||||
// Терминал глючит из-за зажимаемой клавиши ESC для выхода
|
||||
// button(class="window-button-full-screen") ⤢
|
||||
|
||||
iframe#webterm-iframe(src="" style="width: 100%; flex: 1")
|
||||
|
||||
@@ -27,23 +27,26 @@
|
||||
<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">
|
||||
<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";
|
||||
<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/radio.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 {setRootPrefix} from "../share/js/vars.js";
|
||||
setRootPrefix("../");
|
||||
import {main} from "../share/js/login/main.js";
|
||||
main();
|
||||
</script>
|
||||
</head>
|
||||
@@ -52,22 +55,49 @@
|
||||
<div id="login-box">
|
||||
<div id="login">
|
||||
<table>
|
||||
<tr>
|
||||
<td><img class="svg-gray" id="login-logo" src="../share/svg/logo.svg" alt="&pi;-kvm"></td>
|
||||
<td></td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2">
|
||||
<hr>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td i18n="username">Username: </td>
|
||||
<td>
|
||||
<input type="text" id="user-input" autocapitalize="off">
|
||||
<input id="user-input" type="text" autocapitalize="off">
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td i18n="password">Password: </td>
|
||||
<td>
|
||||
<input type="password" id="passwd-input" autocapitalize="off">
|
||||
<input id="passwd-input" type="password" autocapitalize="off">
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td i18n="2fa_code">2FA code: </td>
|
||||
<td>
|
||||
<input type="text" id="code-input" placeholder="if enabled" i18n="if_enabled">
|
||||
<input id="code-input" type="text" placeholder="if enabled" autocomplete="off" i18n="if_enabled">
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="2">
|
||||
<hr>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><a target="_blank" href="https://docs.pikvm.org/auth/#session-expiration">Remember me</a>: </td>
|
||||
<td>
|
||||
<div class="radio-box">
|
||||
<input type="radio" id="expire-radio-3600" name="expire-radio" value="3600"/>
|
||||
<label for="expire-radio-3600">1h</label>
|
||||
<input type="radio" id="expire-radio-43200" name="expire-radio" value="43200"/>
|
||||
<label for="expire-radio-43200">12h</label>
|
||||
<input type="radio" id="expire-radio-0" name="expire-radio" value="0" checked="checked"/>
|
||||
<label for="expire-radio-0">Forever</label>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
|
||||
@@ -1,39 +1,67 @@
|
||||
extends ../base.pug
|
||||
|
||||
|
||||
mixin radio(name, items)
|
||||
.radio-box
|
||||
each item in items
|
||||
-
|
||||
let id = `${name}-${item["value"]}`
|
||||
let checked = (item["checked"] || false)
|
||||
input(type="radio" id=id name=name value=item["value"] checked=checked)
|
||||
label(for=id) !{item["title"]}
|
||||
|
||||
|
||||
append vars
|
||||
- title = "One-KVM Login"
|
||||
- main_js = "login/main"
|
||||
- css_list = css_list.concat(["window", "modal", "login/login"])
|
||||
-
|
||||
root_prefix = "../"
|
||||
title = "One-KVM Login"
|
||||
main_js = "login/main"
|
||||
css_list.push("window", "modal", "radio", "login/login")
|
||||
|
||||
|
||||
block body
|
||||
form(action="javascript:void(0)")
|
||||
div(id="login-box")
|
||||
div(id="login")
|
||||
#login-box
|
||||
#login
|
||||
table
|
||||
tr
|
||||
td #[img.svg-gray#login-logo(src=`${svg_dir}/logo.svg` alt="π-kvm")]
|
||||
td
|
||||
tr
|
||||
td(colspan=2) #[hr]
|
||||
tr
|
||||
td(i18n="username") Username:
|
||||
td #[input(type="text" id="user-input" autocapitalize="off")]
|
||||
td #[input#user-input(type="text" autocapitalize="off")]
|
||||
tr
|
||||
td(i18n="password") Password:
|
||||
td #[input(type="password" id="passwd-input" autocapitalize="off")]
|
||||
td #[input#passwd-input(type="password" autocapitalize="off")]
|
||||
tr
|
||||
td(i18n="2fa_code") 2FA code:
|
||||
td #[input(type="text" id="code-input" placeholder="if enabled" i18n="if_enabled")]
|
||||
td #[input#code-input(type="text" placeholder="if enabled" autocomplete="off" i18n="if_enabled")]
|
||||
tr
|
||||
td(colspan=2)
|
||||
hr
|
||||
td(colspan=2) #[hr]
|
||||
tr
|
||||
td #[a(target="_blank" href="https://docs.pikvm.org/auth/#session-expiration") Remember me]:
|
||||
td
|
||||
+radio("expire-radio", [
|
||||
{"title": "1h", "value": "3600"},
|
||||
{"title": "12h", "value": "43200"},
|
||||
{"title": "Forever", "value": "0", "checked": true},
|
||||
])
|
||||
tr
|
||||
td(colspan=2) #[hr]
|
||||
tr
|
||||
td(i18n="select_language") Select language:
|
||||
td
|
||||
select(id="selectLanguage" style="width:100%")
|
||||
option(id='zh', selected="selected" i18n="chinese") Simplified Chinese
|
||||
option(id='en' i18n="english") English
|
||||
select#selectLanguage(style="width:100%")
|
||||
option#zh(selected="selected" i18n="chinese") Simplified Chinese
|
||||
option#en(i18n="english") English
|
||||
tr
|
||||
td
|
||||
td #[button(id="login-button" class="key" style="width:100%" i18n="login") Login]
|
||||
td #[button.key#login-button(style="width:100%" i18n="login") Login]
|
||||
|
||||
ul(class="footer")
|
||||
li(class="left" i18n="footer-left")
|
||||
ul.footer
|
||||
li.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.
|
||||
|
||||
@@ -34,7 +34,7 @@ div.keypad div.keypad-block:not(:first-child) {
|
||||
|
||||
div.keypad div.keypad-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
flex-wrap: nowrap;
|
||||
white-space: nowrap;
|
||||
height: 40px;
|
||||
margin-bottom: 5px;
|
||||
@@ -52,7 +52,6 @@ div.keypad div.keypad-row div.spacer-fixed {
|
||||
}
|
||||
|
||||
div.keypad div.key,
|
||||
div.keypad div.modifier,
|
||||
div.keypad div.empty {
|
||||
vertical-align: top;
|
||||
font-size: 0.9em;
|
||||
@@ -65,8 +64,7 @@ div.keypad div.empty {
|
||||
div.keypad div.empty {
|
||||
border: thin solid transparent;
|
||||
}
|
||||
div.keypad div.key,
|
||||
div.keypad div.modifier {
|
||||
div.keypad div.key {
|
||||
box-shadow: var(--shadow-micro);
|
||||
border: var(--border-key-thin);
|
||||
border-radius: 6px;
|
||||
@@ -74,33 +72,57 @@ div.keypad div.modifier {
|
||||
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);
|
||||
@media (hover: hover) {
|
||||
div.keypad div.key:not(div.holded):not(div.locked):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;
|
||||
border-radius: 6px 0px 0px 6px;
|
||||
}
|
||||
div.keypad div.rounded-right {
|
||||
border-radius: 0px 6px 6px 0px !important;
|
||||
border-radius: 0px 6px 6px 0px;
|
||||
}
|
||||
div.keypad div.rounded-none {
|
||||
border-radius: 0px !important;
|
||||
border-radius: 0px;
|
||||
}
|
||||
|
||||
div.keypad div.pressed {
|
||||
box-shadow: none;
|
||||
color: var(--cs-key-pressed-fg) !important;
|
||||
background-color: var(--cs-key-pressed-bg) !important;
|
||||
color: var(--cs-key-pressed-fg);
|
||||
background-color: var(--cs-key-pressed-bg);
|
||||
}
|
||||
|
||||
div.keypad div.pressed:not(div.holded):not(div.locked):hover[data-allow-autohold] {
|
||||
/* :active is not working on Firefox and iOS */
|
||||
background: linear-gradient(to top, var(--cs-key-holded-bg) 50%, var(--cs-key-pressed-bg) 0);
|
||||
background-size: 100% 200%;
|
||||
background-position: top;
|
||||
animation: keypad-animate-holding 0.2s 0.3s forwards;
|
||||
}
|
||||
@keyframes keypad-animate-holding {
|
||||
100% {
|
||||
background-position: bottom;
|
||||
}
|
||||
}
|
||||
|
||||
div.keypad div.holded {
|
||||
box-shadow: none;
|
||||
color: var(--cs-key-default-fg) !important;
|
||||
/* Override animation end on iOS with !important */
|
||||
box-shadow: none !important;
|
||||
color: var(--cs-key-holded-fg) !important;
|
||||
background-color: var(--cs-key-holded-bg) !important;
|
||||
}
|
||||
|
||||
div.keypad div.locked {
|
||||
box-shadow: none;
|
||||
color: var(--cs-key-locked-fg);
|
||||
background-color: var(--cs-key-locked-bg);
|
||||
}
|
||||
|
||||
div.keypad div.key:last-child,
|
||||
div.keypad div.empty:last-child,
|
||||
div.keypad div.modifier:last-child {
|
||||
div.keypad div.empty:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
div.keypad div.wide-0 {
|
||||
@@ -120,12 +142,12 @@ div.keypad div.wide-4 {
|
||||
width: 288px;
|
||||
}
|
||||
div.keypad div.left {
|
||||
text-align: left !important;
|
||||
padding-left: 6px !important;
|
||||
text-align: left;
|
||||
padding-left: 6px;
|
||||
}
|
||||
div.keypad div.right {
|
||||
text-align: right !important;
|
||||
padding-right: 6px !important;
|
||||
text-align: right;
|
||||
padding-right: 6px;
|
||||
}
|
||||
div.keypad div.small {
|
||||
font-size: 0.7em;
|
||||
@@ -142,3 +164,6 @@ div.keypad div.label {
|
||||
div.keypad b {
|
||||
color: var(--cs-key-holded-bg);
|
||||
}
|
||||
div.keypad div.locked b {
|
||||
color: var(--cs-key-locked-bg);
|
||||
}
|
||||
|
||||
@@ -104,7 +104,7 @@ div#stream-window.window-active:fullscreen div#stream-box div#stream-fullscreen-
|
||||
top: 0;
|
||||
left: 0;
|
||||
border: 0;
|
||||
box-shadow: var(--shadow-window-fullscreen-active);
|
||||
/*box-shadow: var(--shadow-window-fullscreen-active);*/
|
||||
}
|
||||
|
||||
div#stream-mouse-buttons {
|
||||
|
||||
@@ -34,6 +34,11 @@ div#stream-window {
|
||||
width: 100% !important;
|
||||
-webkit-transform: translateX(-50%) !important;
|
||||
transform: translateX(-50%) !important;
|
||||
|
||||
/* Ignore stream's resize_hook() */
|
||||
aspect-ratio: unset !important;
|
||||
max-width: unset !important;
|
||||
max-height: unset !important;
|
||||
}
|
||||
|
||||
div#stream-window::after {
|
||||
|
||||
@@ -42,6 +42,14 @@ div#login {
|
||||
padding: 15px;
|
||||
}
|
||||
|
||||
img#login-logo {
|
||||
height: 30px;
|
||||
}
|
||||
|
||||
button#login-button {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
input[type="text"]#user-input,
|
||||
input[type="password"]#passwd-input,
|
||||
input[type="text"]#code-input {
|
||||
|
||||
@@ -56,8 +56,8 @@ div.modal div.modal-window div.modal-header {
|
||||
}
|
||||
|
||||
div.modal div.modal-window div.modal-content {
|
||||
max-width: 500px;
|
||||
max-height: 500px;
|
||||
max-width: 600px;
|
||||
max-height: 600px;
|
||||
padding: 16px 9px 16px 9px;
|
||||
}
|
||||
|
||||
|
||||
@@ -133,7 +133,7 @@ ul#navbar li div.menu::-webkit-scrollbar-thumb {
|
||||
}
|
||||
@-moz-document url-prefix() {
|
||||
ul#navbar li div.menu {
|
||||
scrollbar-width: 8px;
|
||||
/* scrollbar-width: 8px; px is not supported */
|
||||
scrollbar-color: var(--cs-scroll-default-bg) var(--cs-code-default-bg);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,26 +20,23 @@
|
||||
*****************************************************************************/
|
||||
|
||||
|
||||
@supports (-webkit-appearance:none) {
|
||||
input[type=range] {
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
width: 100%;
|
||||
box-shadow: none;
|
||||
background: transparent;
|
||||
}
|
||||
@supports (-webkit-locale: auto) { /* WebKit only */
|
||||
input[type=range] {
|
||||
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) {
|
||||
@supports not (-webkit-locale: auto) { /* Firefox */
|
||||
input[type=range] {
|
||||
cursor: pointer;
|
||||
outline: none;
|
||||
width: 100%;
|
||||
box-shadow: none;
|
||||
margin-left: 0;
|
||||
margin-right: 0;
|
||||
margin: 1px 0 1px 0;
|
||||
}
|
||||
}
|
||||
input[type=range]:disabled {
|
||||
@@ -81,8 +78,8 @@ input[type=range]:disabled::-moz-range-track {
|
||||
|
||||
input[type=range]::-moz-range-thumb {
|
||||
border: var(--border-intensive-2px);
|
||||
height: 18px;
|
||||
width: 18px;
|
||||
height: 14px;
|
||||
width: 14px;
|
||||
border-radius: 25px;
|
||||
background: var(--cs-thumb-default-bg);
|
||||
}
|
||||
|
||||
@@ -45,6 +45,7 @@
|
||||
--cs-window-header-grabbed-bg: #436a8a;
|
||||
--cs-window-header-grabbed-fg: white;
|
||||
--cs-window-closer-default-fg: #6c7481;
|
||||
--cs-window-button-special-fg: #009b48;
|
||||
|
||||
--cs-code-default-bg: #17191d;
|
||||
--cs-code-default-fg: #aaaaaa;
|
||||
@@ -65,6 +66,9 @@
|
||||
--cs-key-pressed-bg: #17191d;
|
||||
--cs-key-pressed-fg: #6c7481;
|
||||
--cs-key-holded-bg: #436a8a;
|
||||
--cs-key-holded-fg: white;
|
||||
--cs-key-locked-bg: #a80000;
|
||||
--cs-key-locked-fg: white;
|
||||
|
||||
--cs-marker-fg: #5b90bb;
|
||||
--cs-corner-bg: #5b90bb;
|
||||
@@ -73,12 +77,12 @@
|
||||
--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;
|
||||
/*--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-navbar-item-thin: thin solid #101010;
|
||||
--border-control-thin: thin solid #17191d;
|
||||
--border-key-thin: thin solid #202225;
|
||||
--border-intensive-2px: 2px solid #5b90bb;
|
||||
|
||||
@@ -40,7 +40,7 @@ div.window-resizable {
|
||||
div.window-active {
|
||||
border: var(--border-window-active-2px) !important;
|
||||
}
|
||||
div.window-resizable.window-active::after {
|
||||
div.window-resizable.window-active:not(.window-full-tab)::after {
|
||||
content: "";
|
||||
width: 0;
|
||||
height: 0;
|
||||
@@ -61,6 +61,11 @@ div.window:fullscreen {
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
padding: 0px !important;
|
||||
|
||||
/* Ignore stream's resize_hook() */
|
||||
aspect-ratio: unset !important;
|
||||
max-width: unset !important;
|
||||
max-height: unset !important;
|
||||
}
|
||||
div.window:fullscreen::after {
|
||||
display: none;
|
||||
@@ -75,6 +80,11 @@ div.window:-webkit-full-screen {
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
padding: 0px !important;
|
||||
|
||||
/* Ignore stream's resize_hook() */
|
||||
aspect-ratio: unset !important;
|
||||
max-width: unset !important;
|
||||
max-height: unset !important;
|
||||
}
|
||||
div.window:-webkit-full-screen::after {
|
||||
display: none;
|
||||
@@ -88,6 +98,11 @@ div.window.window-full-tab {
|
||||
width: 100% !important;
|
||||
height: 100% !important;
|
||||
padding: 0px !important;
|
||||
|
||||
/* Ignore stream's resize_hook() */
|
||||
aspect-ratio: unset !important;
|
||||
max-width: unset !important;
|
||||
max-height: unset !important;
|
||||
}
|
||||
|
||||
div.window div.window-header {
|
||||
@@ -126,35 +141,35 @@ div.window div.window-header-grabbed {
|
||||
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;
|
||||
div.window div.window-header div.window-buttons {
|
||||
position: absolute;
|
||||
top: -2px;
|
||||
right: 0;
|
||||
font-size: 0px;
|
||||
background-color: var(--cs-window-default-bg);
|
||||
}
|
||||
|
||||
div.window div.window-header div.window-buttons button.window-button-full-screen,
|
||||
div.window div.window-header div.window-buttons button.window-button-enter-full-tab,
|
||||
div.window div.window-header div.window-buttons button.window-button-original,
|
||||
div.window div.window-header div.window-buttons button.window-button-maximize,
|
||||
div.window div.window-header div.window-buttons button.window-button-close {
|
||||
line-height: 1px;
|
||||
border: none;
|
||||
border-radius: 0px;
|
||||
width: 44px;
|
||||
height: 24px;
|
||||
padding-left: 0;
|
||||
padding-right: 0;
|
||||
margin-left: 1px;
|
||||
color: var(--cs-window-closer-default-fg);
|
||||
display: inline-block;
|
||||
}
|
||||
div.window div.window-header button.window-button-full-screen {
|
||||
right: 180px;
|
||||
div.window[data-centered] div.window-header div.window-buttons button.window-button-original {
|
||||
color: var(--cs-window-button-special-fg);
|
||||
}
|
||||
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[data-maximized] div.window-header div.window-buttons button.window-button-maximize {
|
||||
color: var(--cs-window-button-special-fg);
|
||||
}
|
||||
|
||||
div.window button.window-button-exit-full-tab {
|
||||
|
||||
@@ -91,7 +91,7 @@ ul#navbar li a.menu-button:hover:not(.active) {
|
||||
/* ===== slider.css ===== */
|
||||
|
||||
/*@media only screen and (orientation: portrait) {
|
||||
@supports (-webkit-appearance: none) {
|
||||
@supports (-webkit-locale: auto) {
|
||||
input[type=range] {
|
||||
margin: 20px 0 20px 0 !important;
|
||||
}
|
||||
@@ -113,9 +113,3 @@ ul#navbar li a.menu-button:hover:not(.active) {
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -23,6 +23,9 @@
|
||||
"use strict";
|
||||
|
||||
|
||||
import {ROOT_PREFIX} from "./vars.js";
|
||||
|
||||
|
||||
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
|
||||
@@ -133,12 +136,12 @@ export function checkBrowser(desktop_css, mobile_css) {
|
||||
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");
|
||||
__addCssLink("x-desktop.css");
|
||||
if (desktop_css) {
|
||||
__addCssLink(desktop_css);
|
||||
}
|
||||
} else {
|
||||
__addCssLink("/share/css/x-mobile.css");
|
||||
__addCssLink("x-mobile.css");
|
||||
if (mobile_css) {
|
||||
__addCssLink(mobile_css);
|
||||
}
|
||||
@@ -148,6 +151,7 @@ export function checkBrowser(desktop_css, mobile_css) {
|
||||
}
|
||||
|
||||
function __addCssLink(path) {
|
||||
path = `${ROOT_PREFIX}share/css/${path}`;
|
||||
console.log("===== Adding CSS:", path);
|
||||
let el_head = document.getElementsByTagName("head")[0];
|
||||
let el_link = document.createElement("link");
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
"use strict";
|
||||
|
||||
|
||||
import {ROOT_PREFIX} from "../vars.js";
|
||||
import {tools, $} from "../tools.js";
|
||||
import {checkBrowser} from "../bb.js";
|
||||
import {wm, initWindowManager} from "../wm.js";
|
||||
@@ -39,68 +40,86 @@ export function main() {
|
||||
|
||||
|
||||
function __loadKvmdInfo() {
|
||||
tools.httpGet("/api/info", {"fields": "auth,meta,extras"}, function(http) {
|
||||
if (http.status === 200) {
|
||||
let info = JSON.parse(http.responseText).result;
|
||||
tools.httpGet("api/info", {"fields": "auth,meta,extras"}, function(http) {
|
||||
switch (http.status) {
|
||||
case 200:
|
||||
__showKvmdInfo(JSON.parse(http.responseText).result);
|
||||
break;
|
||||
|
||||
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;
|
||||
}
|
||||
});
|
||||
}
|
||||
case 401:
|
||||
case 403:
|
||||
tools.currentOpen("login");
|
||||
break;
|
||||
|
||||
$("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 = `One-KVM Index: ${info.meta.server.host}`;
|
||||
} else {
|
||||
$("kvmd-meta-server-host").innerHTML = "";
|
||||
document.title = "One-KVM Index";
|
||||
}
|
||||
} else if (http.status === 401 || http.status === 403) {
|
||||
document.location.href = "/login";
|
||||
} else {
|
||||
setTimeout(__loadKvmdInfo, 1000);
|
||||
default:
|
||||
setTimeout(__loadKvmdInfo, 1000);
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function __showKvmdInfo(info) {
|
||||
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;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
let html = "";
|
||||
|
||||
// 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) {
|
||||
html += __makeApp(null, "kvm", "share/svg/kvm.svg", "KVM");
|
||||
}
|
||||
|
||||
for (let app of apps) {
|
||||
if (app.place >= 0 && (app.enabled || app.started)) {
|
||||
html += __makeApp(null, app.path, app.icon, app.name);
|
||||
}
|
||||
}
|
||||
|
||||
if (info.auth.enabled) {
|
||||
html += __makeApp("logout-button", "#", "share/svg/logout.svg", "Logout");
|
||||
}
|
||||
|
||||
$("apps-box").innerHTML = `<ul id="apps">${html}</ul>`;
|
||||
|
||||
if (info.auth.enabled) {
|
||||
tools.el.setOnClick($("logout-button"), __logout);
|
||||
}
|
||||
|
||||
if (info.meta !== null && info.meta.server && info.meta.server.host) {
|
||||
$("kvmd-meta-server-host").innerText = info.meta.server.host;
|
||||
document.title = `${info.meta.server.host} | PiKVM Index`;
|
||||
} else {
|
||||
$("kvmd-meta-server-host").innerHTML = "<i>Invalid meta</i>";
|
||||
document.title = "PiKVM Index";
|
||||
}
|
||||
}
|
||||
|
||||
function __makeApp(id, path, icon, name) {
|
||||
// Tailing slash in href is added to avoid Nginx 301 redirect
|
||||
// when the location doesn't have tailing slash: "foo -> foo/".
|
||||
// Reverse proxy over PiKVM can be misconfigured to handle this.
|
||||
let e_add_id = (id ? `id="${tools.escape(id)}"` : "");
|
||||
return `<li>
|
||||
<div ${id ? "id=\"" + id + "\"" : ""} class="app">
|
||||
<a href="${path}">
|
||||
<div ${e_add_id} class="app">
|
||||
<a href="${tools.escape(ROOT_PREFIX + path)}/">
|
||||
<div>
|
||||
<img class="svg-gray" src="${icon}">
|
||||
<img class="svg-gray" src="${tools.escape(ROOT_PREFIX + icon)}">
|
||||
${tools.escape(name)}
|
||||
</div>
|
||||
</a>
|
||||
@@ -109,11 +128,17 @@ function __makeApp(id, path, icon, name) {
|
||||
}
|
||||
|
||||
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);
|
||||
tools.httpPost("api/auth/logout", null, function(http) {
|
||||
switch (http.status) {
|
||||
case 200:
|
||||
case 401:
|
||||
case 403:
|
||||
tools.currentOpen("login");
|
||||
break;
|
||||
|
||||
default:
|
||||
wm.error("Logout error", http.responseText);
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -31,30 +31,44 @@ export function main() {
|
||||
}
|
||||
|
||||
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>
|
||||
${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);
|
||||
tools.httpGet("api/info", null, function(http) {
|
||||
switch (http.status) {
|
||||
case 200:
|
||||
__showKvmdInfo(JSON.parse(http.responseText).result);
|
||||
break;
|
||||
|
||||
case 401:
|
||||
case 403:
|
||||
tools.currentOpen("login");
|
||||
break;
|
||||
|
||||
default:
|
||||
setTimeout(__loadKvmdInfo, 1000);
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function __showKvmdInfo(info) {
|
||||
let make_item = function (comment, cmd, api) {
|
||||
return `
|
||||
<span class="code-comment">
|
||||
# ${tools.escape(comment)}:<br>$
|
||||
</span>
|
||||
ipmitool -I lanplus -U admin -P admin
|
||||
-H ${tools.escape(window.location.hostname)}
|
||||
-p ${tools.escape(info.extras.ipmi.port)} ${tools.escape(cmd)}
|
||||
<br>
|
||||
<span class="code-comment">$</span>
|
||||
curl -XPOST -HX-KVMD-User:admin -HX-KVMD-Passwd:admin -k \\<br>
|
||||
${tools.escape(window.location.protocol + "//" + window.location.host + "/api/atx" + api)}
|
||||
`;
|
||||
};
|
||||
$("ipmi-text").innerHTML = [
|
||||
make_item("Power on the server if it's off", "power on", "/power?action=on"),
|
||||
make_item("Soft power off the server if it's on", "power soft", "/power?action=off"),
|
||||
make_item("Hard power off the server if it's on", "power off", "/power?action=off_hard"),
|
||||
make_item("Hard reset the server if it's on", "power reset", "/power?action=reset_hard"),
|
||||
make_item("Check the power status", "power status", ""),
|
||||
].join("<br><br>");
|
||||
}
|
||||
|
||||
@@ -23,88 +23,82 @@
|
||||
"use strict";
|
||||
|
||||
|
||||
import {tools, $$$} from "./tools.js";
|
||||
import {tools} from "./tools.js";
|
||||
|
||||
|
||||
export function Keypad(__keys_parent, __sendKey, __apply_fixes) {
|
||||
export function Keypad(__el_keypad, __sendKey, __apply_fixes) {
|
||||
var self = this;
|
||||
|
||||
/************************************************************************/
|
||||
|
||||
var __merged = {};
|
||||
var __keys = {};
|
||||
var __modifiers = {};
|
||||
var __hold_timers = {};
|
||||
|
||||
var __fix_mac_cmd = false;
|
||||
var __fix_win_altgr = false;
|
||||
var __altgr_ctrl_timer = null;
|
||||
|
||||
var __init__ = function() {
|
||||
__el_keypad.addEventListener("contextmenu", (ev) => ev.preventDefault());
|
||||
|
||||
if (__apply_fixes) {
|
||||
__fix_mac_cmd = tools.browser.is_mac;
|
||||
if (__fix_mac_cmd) {
|
||||
tools.info(`Keymap at ${__keys_parent}: enabled Fix-Mac-CMD`);
|
||||
tools.info(`Keymap at ${__el_keypad.id}: 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`);
|
||||
tools.info(`Keymap at ${__el_keypad.id}: enabled Fix-Win-AltGr`);
|
||||
}
|
||||
}
|
||||
|
||||
for (let el_key of $$$(`${__keys_parent} div.key`)) {
|
||||
for (let el_key of [].slice.call(__el_keypad.getElementsByClassName("key"))) {
|
||||
if (el_key.hasAttribute("data-allow-autohold")) {
|
||||
el_key.title = "Long left click or short right click for hold, middle for lock";
|
||||
} else {
|
||||
el_key.title = "Right click for hold, middle for lock";
|
||||
}
|
||||
|
||||
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));
|
||||
tools.el.setOnDown(el_key, (ev) => __clickHandler(el_key, ev));
|
||||
tools.el.setOnUp(el_key, () => __clickHandler(el_key, null));
|
||||
el_key.onmouseout = function() {
|
||||
if (__isPressed(el_key)) {
|
||||
__clickHandler(el_key, false);
|
||||
if (
|
||||
__isActive(el_key, "pressed")
|
||||
&& !__isActive(el_key, "holded")
|
||||
&& !__isActive(el_key, "locked")
|
||||
) {
|
||||
__clickHandler(el_key, null);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
for (let code in __keys) {
|
||||
if (__isActive(__keys[code][0])) {
|
||||
self.emitByCode(code, false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
self.emitByKeyEvent = function(event, state) {
|
||||
if (event.repeat) {
|
||||
self.emitByKeyEvent = function(ev, state) {
|
||||
if (ev.repeat) {
|
||||
return;
|
||||
}
|
||||
|
||||
let code = event.code;
|
||||
let code = ev.code;
|
||||
if (__apply_fixes) {
|
||||
// https://github.com/pikvm/pikvm/issues/819
|
||||
if (code == "IntlBackslash" && ["`", "~"].includes(event.key)) {
|
||||
if (code === "IntlBackslash" && ["`", "~"].includes(ev.key)) {
|
||||
code = "Backquote";
|
||||
} else if (code == "Backquote" && ["§", "±"].includes(event.key)) {
|
||||
} else if (code === "Backquote" && ["§", "±"].includes(ev.key)) {
|
||||
code = "IntlBackslash";
|
||||
}
|
||||
}
|
||||
@@ -113,7 +107,10 @@ export function Keypad(__keys_parent, __sendKey, __apply_fixes) {
|
||||
};
|
||||
|
||||
self.emitByCode = function(code, state, apply_fixes=true) {
|
||||
if (code in __merged) {
|
||||
if (code in __keys) {
|
||||
let el_key = __keys[code][0];
|
||||
__stopHoldTimer(el_key);
|
||||
|
||||
if (__fix_win_altgr && apply_fixes) {
|
||||
if (!__fixWinAltgr(code, state)) {
|
||||
return;
|
||||
@@ -122,13 +119,21 @@ export function Keypad(__keys_parent, __sendKey, __apply_fixes) {
|
||||
if (__fix_mac_cmd && apply_fixes) {
|
||||
__fixMacCmd(code, state);
|
||||
}
|
||||
__commonHandler(__merged[code][0], state, false);
|
||||
__unholdModifiers();
|
||||
}
|
||||
|
||||
if (state && !__isActive(el_key)) {
|
||||
__deactivate(el_key);
|
||||
__activate(el_key, "pressed");
|
||||
__process(el_key, true);
|
||||
} else {
|
||||
__deactivate(el_key);
|
||||
__process(el_key, false);
|
||||
}
|
||||
__unholdAll();
|
||||
};
|
||||
};
|
||||
|
||||
var __fixMacCmd = function(code, state) {
|
||||
if ((code == "MetaLeft" || code == "MetaRight") && !state) {
|
||||
if ((code === "MetaLeft" || code === "MetaRight") && !state) {
|
||||
for (code in __keys) {
|
||||
if (__isActive(__keys[code][0])) {
|
||||
self.emitByCode(code, false, false);
|
||||
@@ -148,7 +153,7 @@ export function Keypad(__keys_parent, __sendKey, __apply_fixes) {
|
||||
self.emitByCode("ControlLeft", true, false);
|
||||
}
|
||||
}
|
||||
if (code === "ControlLeft" && !__isActive(__modifiers["ControlLeft"][0])) {
|
||||
if (code === "ControlLeft" && !__isActive(__keys["ControlLeft"][0])) {
|
||||
__altgr_ctrl_timer = setTimeout(function() {
|
||||
__altgr_ctrl_timer = null;
|
||||
self.emitByCode("ControlLeft", true, false);
|
||||
@@ -165,61 +170,93 @@ export function Keypad(__keys_parent, __sendKey, __apply_fixes) {
|
||||
return true; // Continue handling
|
||||
};
|
||||
|
||||
var __clickHandler = function(el_key, state) {
|
||||
__commonHandler(el_key, state, false);
|
||||
__unholdModifiers();
|
||||
};
|
||||
var __clickHandler = function(el_key, ev) {
|
||||
let state = false;
|
||||
let act = "pressed";
|
||||
if (ev) {
|
||||
state = (ev.type === "mousedown" || ev.type === "touchstart");
|
||||
if (ev.type === "mousedown") {
|
||||
if (ev.button === 1) {
|
||||
act = "locked";
|
||||
} else if (ev.button === 2) {
|
||||
act = "holded";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var __toggleModifierHandler = function(el_key) {
|
||||
__commonHandler(el_key, !__isActive(el_key), true);
|
||||
};
|
||||
|
||||
var __commonHandler = function(el_key, state, hold) {
|
||||
if (state && !__isActive(el_key)) {
|
||||
__stopHoldTimer(el_key);
|
||||
__deactivate(el_key);
|
||||
__activate(el_key, (hold ? "holded" : "pressed"));
|
||||
__activate(el_key, act);
|
||||
__process(el_key, true);
|
||||
__startHoldTimer(el_key);
|
||||
} else {
|
||||
__deactivate(el_key);
|
||||
__process(el_key, false);
|
||||
let fixed = (__isActive(el_key, "holded") || __isActive(el_key, "locked"));
|
||||
if (!state && fixed && __stopHoldTimer(el_key)) {
|
||||
return; // Игнорировать первое отжатие сразу после нажатия
|
||||
}
|
||||
if (!state) {
|
||||
__stopHoldTimer(el_key);
|
||||
__deactivate(el_key);
|
||||
__process(el_key, false);
|
||||
if (!fixed) {
|
||||
__unholdAll();
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var __unholdModifiers = function() {
|
||||
for (let code in __modifiers) {
|
||||
let el_key = __modifiers[code][0];
|
||||
if (__isHolded(el_key)) {
|
||||
var __startHoldTimer = function(el_key) {
|
||||
__stopHoldTimer(el_key);
|
||||
let code = el_key.getAttribute("data-code");
|
||||
__hold_timers[code] = setTimeout(function() {
|
||||
// Помимо прямой функции, hold timer используется для детектирования факта
|
||||
// нажатия в рамках одной сессии press/release, чтобы не отпустить сразу же
|
||||
// зажатую или заблокированную клавишу. Поэтому таймер инициализируется всегда,
|
||||
// но основную функцию выполняет только если у него есть атрибут data-allow-autohold.
|
||||
if (el_key.hasAttribute("data-allow-autohold")) {
|
||||
__deactivate(el_key);
|
||||
__activate(el_key, "holded");
|
||||
}
|
||||
}, 500); // Check keypad.css for the animation
|
||||
};
|
||||
|
||||
var __stopHoldTimer = function(el_key) {
|
||||
let code = el_key.getAttribute("data-code");
|
||||
if (!__hold_timers[code]) {
|
||||
return false;
|
||||
}
|
||||
clearTimeout(__hold_timers[code]);
|
||||
__hold_timers[code] = null;
|
||||
return true;
|
||||
};
|
||||
|
||||
var __unholdAll = function() {
|
||||
for (let el_key of [].slice.call(__el_keypad.getElementsByClassName("key"))) {
|
||||
__stopHoldTimer(el_key);
|
||||
if (__isActive(el_key, "holded") && !__isActive(el_key, "locked")) { // Skip duplicating keys
|
||||
__deactivate(el_key);
|
||||
__process(el_key, false);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var __isPressed = function(el_key) {
|
||||
let is_pressed = false;
|
||||
var __isActive = function(el_key, cls=null) {
|
||||
let el_keys = __resolveKeys(el_key);
|
||||
for (el_key of el_keys) {
|
||||
is_pressed = (is_pressed || el_key.classList.contains("pressed"));
|
||||
if (cls) {
|
||||
if (el_key.classList.contains(cls)) {
|
||||
return true;
|
||||
}
|
||||
} else if (
|
||||
el_key.classList.contains("pressed")
|
||||
|| el_key.classList.contains("holded")
|
||||
|| el_key.classList.contains("locked")
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
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;
|
||||
return false;
|
||||
};
|
||||
|
||||
var __activate = function(el_key, cls) {
|
||||
@@ -234,12 +271,13 @@ export function Keypad(__keys_parent, __sendKey, __apply_fixes) {
|
||||
for (el_key of el_keys) {
|
||||
el_key.classList.remove("pressed");
|
||||
el_key.classList.remove("holded");
|
||||
el_key.classList.remove("locked");
|
||||
}
|
||||
};
|
||||
|
||||
var __resolveKeys = function(el_key) {
|
||||
let code = el_key.getAttribute("data-code");
|
||||
return __merged[code];
|
||||
return __keys[code];
|
||||
};
|
||||
|
||||
var __process = function(el_key, state) {
|
||||
|
||||
@@ -94,7 +94,7 @@ export function Atx(__recorder) {
|
||||
|
||||
var __clickAtx = function(button) {
|
||||
let click_button = function() {
|
||||
tools.httpPost("/api/atx/click", {"button": button}, function(http) {
|
||||
tools.httpPost("api/atx/click", {"button": button}, function(http) {
|
||||
if (http.status === 409) {
|
||||
wm.error("Performing another ATX operation for other client.<br>Please try again later.");
|
||||
} else if (http.status !== 200) {
|
||||
@@ -106,7 +106,7 @@ export function Atx(__recorder) {
|
||||
|
||||
if ($("atx-ask-switch").checked) {
|
||||
wm.confirm(`
|
||||
Are you sure you want to press the <b>${button}</b> button?<br>
|
||||
Are you sure you want to press the <b>${tools.escape(button)}</b> button?<br>
|
||||
Warning! This could cause data loss on the server.
|
||||
`).then(function(ok) {
|
||||
if (ok) {
|
||||
|
||||
78
web/share/js/kvm/clipboard.js
Normal file
78
web/share/js/kvm/clipboard.js
Normal file
@@ -0,0 +1,78 @@
|
||||
/*****************************************************************************
|
||||
# #
|
||||
# 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 var clipboard = new function() {
|
||||
var self = this;
|
||||
|
||||
/************************************************************************/
|
||||
|
||||
self.setText = function(text) {
|
||||
let workaround = function(ex) {
|
||||
// https://stackoverflow.com/questions/60317969/document-execcommandcopy-not-working-even-though-the-dom-element-is-created
|
||||
wm.info("Press OK to copy the text to the clipboard").then(function() {
|
||||
tools.error("clipboard.setText(): navigator.clipboard.writeText() is not working:", ex);
|
||||
tools.info("clipboard.setText(): 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("clipboard.setText(): Workaround failed:", ex);
|
||||
wm.error("Can't copy text to the clipboard", `${ex}`);
|
||||
}
|
||||
});
|
||||
};
|
||||
if (navigator.clipboard) {
|
||||
navigator.clipboard.writeText(text).then(function() {
|
||||
wm.info("The text has been copied to the clipboard");
|
||||
}, function(ex) {
|
||||
workaround(ex);
|
||||
});
|
||||
} else {
|
||||
workaround("navigator.clipboard is not available");
|
||||
}
|
||||
};
|
||||
};
|
||||
@@ -23,6 +23,7 @@
|
||||
"use strict";
|
||||
|
||||
|
||||
import {ROOT_PREFIX} from "../vars.js";
|
||||
import {tools, $, $$} from "../tools.js";
|
||||
import {wm} from "../wm.js";
|
||||
|
||||
@@ -133,31 +134,38 @@ export function Gpio(__recorder) {
|
||||
|
||||
var __createItem = function(item) {
|
||||
if (item.type === "label") {
|
||||
return item.text;
|
||||
// User may want to use HTML in the text so we don't perform escaping here.
|
||||
return `<span class="__gpio-label">${item.text}</span>`;
|
||||
|
||||
} else if (item.type === "input") {
|
||||
let e_ch_class = tools.escape(`__gpio-led-${item.channel}`);
|
||||
let e_icon = tools.escape(`${ROOT_PREFIX}share/svg/led-circle.svg`);
|
||||
return `
|
||||
<img
|
||||
class="__gpio-led __gpio-led-${item.channel} inline-lamp-big led-gray"
|
||||
src="/share/svg/led-circle.svg"
|
||||
data-color="${item.color}"
|
||||
class="__gpio-led ${e_ch_class} inline-lamp-big led-gray"
|
||||
src="${e_icon}"
|
||||
data-color="${tools.escape(item.color)}"
|
||||
/>
|
||||
`;
|
||||
|
||||
} else if (item.type === "output") {
|
||||
let controls = [];
|
||||
let confirm = (item.confirm ? "Are you sure you want to perform this action?" : "");
|
||||
let e_ch = tools.escape(item.channel);
|
||||
let e_confirm = (item.confirm ? tools.escape("Are you sure you want to perform this action?") : "");
|
||||
if (item.scheme["switch"]) {
|
||||
let id = tools.makeId();
|
||||
let e_id = tools.escape(`__gpio-switch-${tools.makeRandomId()}`);
|
||||
let e_ch_class = tools.escape(`__gpio-switch-${item.channel}`);
|
||||
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}"
|
||||
id="${e_id}"
|
||||
class="__gpio-switch ${e_ch_class}"
|
||||
data-channel="${e_ch}"
|
||||
data-confirm="${e_confirm}"
|
||||
/>
|
||||
<label for="__gpio-switch-${id}">
|
||||
<label for="${e_id}">
|
||||
<span class="switch-inner"></span>
|
||||
<span class="switch"></span>
|
||||
</label>
|
||||
@@ -165,22 +173,23 @@ export function Gpio(__recorder) {
|
||||
`);
|
||||
}
|
||||
if (item.scheme.pulse.delay) {
|
||||
let e_ch_class = tools.escape(`__gpio-button-${item.channel}`);
|
||||
controls.push(`
|
||||
<td><button
|
||||
disabled
|
||||
class="__gpio-button __gpio-button-${item.channel}"
|
||||
class="__gpio-button ${e_ch_class}"
|
||||
${item.hide ? "data-force-hide-menu" : ""}
|
||||
data-channel="${item.channel}"
|
||||
data-confirm="${confirm}"
|
||||
data-channel="${e_ch}"
|
||||
data-confirm="${e_confirm}"
|
||||
>
|
||||
${(item.hide ? "• " : "") + item.text}
|
||||
${(item.hide ? "• " : "") + tools.escape(item.text)}
|
||||
</button></td>
|
||||
`);
|
||||
}
|
||||
return `<table><tr>${controls.join("<td> </td>")}</tr></table>`;
|
||||
} else {
|
||||
return "";
|
||||
}
|
||||
|
||||
return "";
|
||||
};
|
||||
|
||||
var __setLedState = function(el, on) {
|
||||
@@ -202,7 +211,7 @@ export function Gpio(__recorder) {
|
||||
confirm = el.getAttribute("data-confirm-off");
|
||||
}
|
||||
let act = () => {
|
||||
__sendPost("/api/gpio/switch", {"channel": ch, "state": to});
|
||||
__sendPost("api/gpio/switch", {"channel": ch, "state": to});
|
||||
__recorder.recordGpioSwitchEvent(ch, to);
|
||||
};
|
||||
if (confirm) {
|
||||
@@ -220,7 +229,7 @@ export function Gpio(__recorder) {
|
||||
let ch = el.getAttribute("data-channel");
|
||||
let confirm = el.getAttribute("data-confirm");
|
||||
let act = () => {
|
||||
__sendPost("/api/gpio/pulse", {"channel": ch});
|
||||
__sendPost("api/gpio/pulse", {"channel": ch});
|
||||
__recorder.recordGpioPulseEvent(ch);
|
||||
};
|
||||
if (confirm) {
|
||||
|
||||
@@ -43,31 +43,11 @@ export function Hid(__getGeometry, __recorder) {
|
||||
__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
|
||||
);
|
||||
}
|
||||
document.addEventListener("visibilitychange", function() {
|
||||
if (document.visibilityState === "hidden") {
|
||||
__releaseAll();
|
||||
}
|
||||
}, false);
|
||||
|
||||
window.addEventListener("pagehide", __releaseAll);
|
||||
window.addEventListener("blur", __releaseAll);
|
||||
@@ -183,13 +163,13 @@ export function Hid(__getGeometry, __recorder) {
|
||||
let avail_json = JSON.stringify(avail);
|
||||
if (el.__avail_json !== avail_json) {
|
||||
let html = "";
|
||||
for (let pair of [
|
||||
for (let kv 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]);
|
||||
if (avail.includes(kv[1])) {
|
||||
html += tools.radio.makeItem("hid-outputs-keyboard-radio", kv[0], kv[1]);
|
||||
}
|
||||
}
|
||||
el.innerHTML = html;
|
||||
@@ -211,16 +191,16 @@ export function Hid(__getGeometry, __recorder) {
|
||||
if (el.__avail_json !== avail_json) {
|
||||
has_relative = false;
|
||||
let html = "";
|
||||
for (let pair of [
|
||||
for (let kv 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]);
|
||||
if (avail.includes(kv[1])) {
|
||||
html += tools.radio.makeItem("hid-outputs-mouse-radio", kv[0], kv[1]);
|
||||
has_relative = (has_relative || kv[2]);
|
||||
}
|
||||
}
|
||||
el.innerHTML = html;
|
||||
@@ -275,7 +255,7 @@ export function Hid(__getGeometry, __recorder) {
|
||||
|
||||
var __clickOutputsRadio = function(hid) {
|
||||
let output = tools.radio.getValue(`hid-outputs-${hid}-radio`);
|
||||
tools.httpPost("/api/hid/set_params", {[`${hid}_output`]: output}, function(http) {
|
||||
tools.httpPost("api/hid/set_params", {[`${hid}_output`]: output}, function(http) {
|
||||
if (http.status !== 200) {
|
||||
wm.error("Can't configure HID", http.responseText);
|
||||
}
|
||||
@@ -284,7 +264,7 @@ export function Hid(__getGeometry, __recorder) {
|
||||
|
||||
var __clickJigglerSwitch = function() {
|
||||
let enabled = $("hid-jiggler-switch").checked;
|
||||
tools.httpPost("/api/hid/set_params", {"jiggler": enabled}, function(http) {
|
||||
tools.httpPost("api/hid/set_params", {"jiggler": enabled}, function(http) {
|
||||
if (http.status !== 200) {
|
||||
wm.error(`Can't ${enabled ? "enabled" : "disable"} mouse jiggler`, http.responseText);
|
||||
}
|
||||
@@ -293,7 +273,7 @@ export function Hid(__getGeometry, __recorder) {
|
||||
|
||||
var __clickConnectSwitch = function() {
|
||||
let connected = $("hid-connect-switch").checked;
|
||||
tools.httpPost("/api/hid/set_connected", {"connected": connected}, function(http) {
|
||||
tools.httpPost("api/hid/set_connected", {"connected": connected}, function(http) {
|
||||
if (http.status !== 200) {
|
||||
wm.error(`Can't ${connected ? "connect" : "disconnect"} HID`, http.responseText);
|
||||
}
|
||||
@@ -303,7 +283,7 @@ export function Hid(__getGeometry, __recorder) {
|
||||
var __clickResetButton = function() {
|
||||
wm.confirm("Are you sure you want to reset HID (keyboard & mouse)?").then(function(ok) {
|
||||
if (ok) {
|
||||
tools.httpPost("/api/hid/reset", null, function(http) {
|
||||
tools.httpPost("api/hid/reset", null, function(http) {
|
||||
if (http.status !== 200) {
|
||||
wm.error("HID reset error", http.responseText);
|
||||
}
|
||||
|
||||
275
web/share/js/kvm/info.js
Normal file
275
web/share/js/kvm/info.js
Normal file
@@ -0,0 +1,275 @@
|
||||
/*****************************************************************************
|
||||
# #
|
||||
# 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 {ROOT_PREFIX} from "../vars.js";
|
||||
import {tools, $} from "../tools.js";
|
||||
|
||||
|
||||
export function Info() {
|
||||
var self = this;
|
||||
|
||||
/************************************************************************/
|
||||
|
||||
var __health_state = null;
|
||||
var __fan_state = null;
|
||||
|
||||
var __init__ = function() {
|
||||
};
|
||||
|
||||
/************************************************************************/
|
||||
|
||||
self.setState = function(state) {
|
||||
for (let key of Object.keys(state)) {
|
||||
switch (key) {
|
||||
case "meta": __setStateMeta(state.meta); break;
|
||||
case "health": __setStateHealth(state.health); break;
|
||||
case "fan": __setStateFan(state.fan); break;
|
||||
case "system": __setStateSystem(state.system); break;
|
||||
case "extras": __setStateExtras(state.extras); break;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var __setStateMeta = 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 = state.server.host;
|
||||
document.title = `${state.server.host} | PiKVM Session`;
|
||||
} else {
|
||||
$("kvmd-meta-server-host").innerText = "";
|
||||
document.title = "PiKVM Session";
|
||||
}
|
||||
|
||||
for (let place of ["left", "right"]) {
|
||||
if (state.tips && state.tips[place]) {
|
||||
$(`kvmd-meta-tips-${place}`).innerText = state.tips[place];
|
||||
}
|
||||
}
|
||||
|
||||
// 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 __setStateHealth = function(state) {
|
||||
if (state.throttling !== null) {
|
||||
let flags = state.throttling.parsed_flags;
|
||||
let ignore_past = state.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);
|
||||
}
|
||||
__health_state = state;
|
||||
__renderAboutHardware();
|
||||
};
|
||||
|
||||
var __setStateFan = 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");
|
||||
|
||||
__fan_state = state;
|
||||
__renderAboutHardware();
|
||||
};
|
||||
|
||||
var __renderAboutHardware = function() {
|
||||
let parts = [];
|
||||
if (__health_state !== null) {
|
||||
parts = [
|
||||
"Resources:" + __formatMisc(__health_state),
|
||||
"Temperature:" + __formatTemp(__health_state.temp),
|
||||
"Throttling:" + __formatThrottling(__health_state.throttling),
|
||||
];
|
||||
}
|
||||
if (__fan_state !== null) {
|
||||
parts.push("Fan:" + __formatFan(__fan_state));
|
||||
}
|
||||
$("about-hardware").innerHTML = parts.join("<hr>");
|
||||
};
|
||||
|
||||
var __formatMisc = function(state) {
|
||||
return __formatUl([
|
||||
["CPU", tools.escape(`${state.cpu.percent}%`)],
|
||||
["MEM", tools.escape(`${state.mem.percent}%`)],
|
||||
]);
|
||||
};
|
||||
|
||||
var __formatFan = function(state) {
|
||||
if (!state.monitored) {
|
||||
return __formatUl([["Status", "Not monitored"]]);
|
||||
} else if (state.state === null) {
|
||||
return __formatUl([["Status", __red("Not available")]]);
|
||||
} else {
|
||||
state = state.state;
|
||||
let kvs = [
|
||||
["Status", (state.fan.ok ? __green("Ok") : __red("Failed"))],
|
||||
["Desired speed", tools.escape(`${state.fan.speed}%`)],
|
||||
["PWM", tools.escape(`${state.fan.pwm}`)],
|
||||
];
|
||||
if (state.hall.available) {
|
||||
kvs.push(["RPM", __colored(state.fan.ok, tools.escape(`${state.hall.rpm}`))]);
|
||||
}
|
||||
return __formatUl(kvs);
|
||||
}
|
||||
};
|
||||
|
||||
var __formatTemp = function(temp) {
|
||||
let kvs = [];
|
||||
for (let field of Object.keys(temp).sort()) {
|
||||
kvs.push([
|
||||
tools.escape(field.toUpperCase()),
|
||||
tools.escape(`${temp[field]}`) + "°C",
|
||||
]);
|
||||
}
|
||||
return __formatUl(kvs);
|
||||
};
|
||||
|
||||
var __formatThrottling = function(throttling) {
|
||||
if (throttling !== null) {
|
||||
let kvs = [];
|
||||
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"] ? __red("RIGHT NOW") : __green("No"));
|
||||
if (!throttling.ignore_past) {
|
||||
value += "; " + (flags["past"] ? __red("In the past") : __green("Never"));
|
||||
}
|
||||
kvs.push([tools.escape(key), value]);
|
||||
}
|
||||
return __formatUl(kvs);
|
||||
} else {
|
||||
return "NO DATA";
|
||||
}
|
||||
};
|
||||
|
||||
var __setStateSystem = function(state) {
|
||||
let p = state.platform;
|
||||
let s = state.streamer;
|
||||
$("about-version").innerHTML = `
|
||||
Base: ${__commented(tools.escape(p.base))}
|
||||
<hr>
|
||||
Platform: ${__commented(tools.escape(p.model + "-" + p.video + "-" + p.board))}
|
||||
<hr>
|
||||
Serial: ${__commented(tools.escape(p.serial))}
|
||||
<hr>
|
||||
KVMD: ${__commented(tools.escape(state.kvmd.version))}
|
||||
<hr>
|
||||
Streamer: ${__commented(tools.escape(s.version + " (" + s.app + ")"))}
|
||||
${__formatStreamerFeatures(s.features)}
|
||||
<hr>
|
||||
${tools.escape(state.kernel.system)} kernel:
|
||||
${__formatUname(state.kernel)}
|
||||
`;
|
||||
$("kvmd-info-platform").innerText = p.model;
|
||||
$("kvmd-version-kvmd").innerText = state.kvmd.version;
|
||||
$("kvmd-version-streamer").innerText = s.version;
|
||||
};
|
||||
|
||||
var __formatStreamerFeatures = function(features) {
|
||||
let kvs = [];
|
||||
for (let field of Object.keys(features).sort()) {
|
||||
kvs.push([
|
||||
tools.escape(field),
|
||||
(features[field] ? "Yes" : "No"),
|
||||
]);
|
||||
}
|
||||
return __formatUl(kvs);
|
||||
};
|
||||
|
||||
var __formatUname = function(kernel) {
|
||||
let kvs = [];
|
||||
for (let field of Object.keys(kernel).sort()) {
|
||||
if (field !== "system") {
|
||||
kvs.push([
|
||||
tools.escape(tools.upperFirst(field)),
|
||||
tools.escape(kernel[field]),
|
||||
]);
|
||||
}
|
||||
}
|
||||
return __formatUl(kvs);
|
||||
};
|
||||
|
||||
var __formatUl = function(kvs) {
|
||||
let html = "";
|
||||
for (let kv of kvs) {
|
||||
html += `<li>${kv[0]}: ${__commented(kv[1])}</li>`;
|
||||
}
|
||||
return `<ul>${html}</ul>`;
|
||||
};
|
||||
|
||||
var __green = (html) => __colored(true, html);
|
||||
var __red = (html) => __colored(false, html);
|
||||
var __colored = (ok, html) => `<font color="${ok ? "green" : "red"}">${html}</font>`;
|
||||
var __commented = (html) => `<span class="code-comment">${html}</span>`;
|
||||
|
||||
var __setStateExtras = 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 loc = window.location;
|
||||
let base = `${loc.protocol}//${loc.host}${loc.pathname}${ROOT_PREFIX}`;
|
||||
// Tailing slash after state.webterm.path is added to avoid Nginx 301 redirect
|
||||
// when the location doesn't have tailing slash: "foo -> foo/".
|
||||
// Reverse proxy over PiKVM can be misconfigured to handle this.
|
||||
let url = base + state.webterm.path + "/?disableLeaveAlert=true";
|
||||
show_hook = function() {
|
||||
tools.info("Terminal opened: ", url);
|
||||
$("webterm-iframe").src = url;
|
||||
};
|
||||
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;
|
||||
};
|
||||
|
||||
__init__();
|
||||
}
|
||||
@@ -35,17 +35,17 @@ export function Keyboard(__recordWsEvent) {
|
||||
var __keypad = null;
|
||||
|
||||
var __init__ = function() {
|
||||
__keypad = new Keypad("div#keyboard-window", __sendKey, true);
|
||||
__keypad = new Keypad($("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").onkeydown = (ev) => __keyboardHandler(ev, true);
|
||||
$("keyboard-window").onkeyup = (ev) => __keyboardHandler(ev, 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").onkeydown = (ev) => __keyboardHandler(ev, true);
|
||||
$("stream-window").onkeyup = (ev) => __keyboardHandler(ev, false);
|
||||
$("stream-window").onfocus = __updateOnlineLeds;
|
||||
$("stream-window").onblur = __updateOnlineLeds;
|
||||
|
||||
@@ -125,9 +125,9 @@ export function Keyboard(__recordWsEvent) {
|
||||
$("hid-keyboard-led").title = title;
|
||||
};
|
||||
|
||||
var __keyboardHandler = function(event, state) {
|
||||
event.preventDefault();
|
||||
__keypad.emitByKeyEvent(event, state);
|
||||
var __keyboardHandler = function(ev, state) {
|
||||
ev.preventDefault();
|
||||
__keypad.emitByKeyEvent(ev, state);
|
||||
};
|
||||
|
||||
var __sendKey = function(code, state) {
|
||||
@@ -139,7 +139,7 @@ export function Keyboard(__recordWsEvent) {
|
||||
code = "ControlLeft";
|
||||
}
|
||||
}
|
||||
let event = {
|
||||
let ev = {
|
||||
"event_type": "key",
|
||||
"event": {
|
||||
"key": code,
|
||||
@@ -148,10 +148,10 @@ export function Keyboard(__recordWsEvent) {
|
||||
},
|
||||
};
|
||||
if (__ws && !$("hid-mute-switch").checked) {
|
||||
__ws.sendHidEvent(event);
|
||||
__ws.sendHidEvent(ev);
|
||||
}
|
||||
delete event.event.finish;
|
||||
__recordWsEvent(event);
|
||||
delete ev.event.finish;
|
||||
__recordWsEvent(ev);
|
||||
};
|
||||
|
||||
__init__();
|
||||
|
||||
@@ -32,7 +32,7 @@ import {Session} from "./session.js";
|
||||
|
||||
|
||||
export function main() {
|
||||
if (checkBrowser(null, "/share/css/kvm/x-mobile.css")) {
|
||||
if (checkBrowser(null, "kvm/x-mobile.css")) {
|
||||
tools.storage.bindSimpleSwitch($("page-close-ask-switch"), "page.close.ask", true, function(value) {
|
||||
if (value) {
|
||||
window.onbeforeunload = function(event) {
|
||||
@@ -49,7 +49,7 @@ export function main() {
|
||||
|
||||
initWindowManager();
|
||||
|
||||
tools.el.setOnClick($("open-log-button"), () => window.open("/api/log?seek=3600&follow=1", "_blank"));
|
||||
tools.el.setOnClick($("open-log-button"), () => tools.windowOpen("api/log?seek=3600&follow=1"));
|
||||
|
||||
tools.storage.bindSimpleSwitch(
|
||||
$("page-full-tab-stream-switch"),
|
||||
|
||||
@@ -34,38 +34,43 @@ export function Mouse(__getGeometry, __recordWsEvent) {
|
||||
|
||||
var __ws = null;
|
||||
var __online = true;
|
||||
var __absolute = true;
|
||||
var __abs = 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 __touch_pos = null;
|
||||
|
||||
var __abs_pos = null;
|
||||
|
||||
var __rel_sens = 1.0;
|
||||
var __rel_deltas = [];
|
||||
|
||||
var __scroll_rate = 5;
|
||||
var __scroll_fix = (tools.browser.is_mac ? 5 : 1);
|
||||
var __scroll_delta = {"x": 0, "y": 0};
|
||||
|
||||
var __stream_hovered = false;
|
||||
|
||||
var __init__ = function() {
|
||||
__keypad = new Keypad("div#stream-mouse-buttons", __sendButton, false);
|
||||
__keypad = new Keypad($("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);
|
||||
document.addEventListener("pointerlockchange", __relativeCapturedHandler); // Only for relative
|
||||
document.addEventListener("pointerlockerror", __relativeCapturedHandler);
|
||||
|
||||
$("stream-box").addEventListener("contextmenu", (ev) => ev.preventDefault());
|
||||
$("stream-box").addEventListener("mouseenter", () => __streamHoveredHandler(true));
|
||||
$("stream-box").addEventListener("mouseleave", () => __streamHoveredHandler(false));
|
||||
$("stream-box").addEventListener("mousedown", (ev) => __streamButtonHandler(ev, true));
|
||||
$("stream-box").addEventListener("mouseup", (ev) => __streamButtonHandler(ev, false));
|
||||
$("stream-box").addEventListener("mousemove", __streamMoveHandler);
|
||||
$("stream-box").addEventListener("wheel", __streamScrollHandler);
|
||||
|
||||
$("stream-box").addEventListener("touchstart", __streamTouchStartHandler);
|
||||
$("stream-box").addEventListener("touchmove", __streamTouchMoveHandler);
|
||||
$("stream-box").addEventListener("touchend", __streamTouchEndHandler);
|
||||
|
||||
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);
|
||||
@@ -84,26 +89,26 @@ export function Mouse(__getGeometry, __recordWsEvent) {
|
||||
|
||||
self.setSocket = function(ws) {
|
||||
__ws = ws;
|
||||
if (!__absolute && __isRelativeCaptured()) {
|
||||
if (!__abs && __isRelativeCaptured()) {
|
||||
document.exitPointerLock();
|
||||
}
|
||||
__updateOnlineLeds();
|
||||
};
|
||||
|
||||
self.setState = function(online, absolute, hid_online, hid_busy) {
|
||||
self.setState = function(online, abs, hid_online, hid_busy) {
|
||||
if (!hid_online) {
|
||||
__online = null;
|
||||
} else {
|
||||
__online = (online && !hid_busy);
|
||||
}
|
||||
if (!__absolute && absolute && __isRelativeCaptured()) {
|
||||
if (!__abs && abs && __isRelativeCaptured()) {
|
||||
document.exitPointerLock();
|
||||
}
|
||||
if (__absolute && !absolute) {
|
||||
__relative_deltas = [];
|
||||
__relative_touch_pos = null;
|
||||
if (__abs && !abs) {
|
||||
__touch_pos = null;
|
||||
__rel_deltas = [];
|
||||
}
|
||||
__absolute = absolute;
|
||||
__abs = abs;
|
||||
__updateOnlineLeds();
|
||||
};
|
||||
|
||||
@@ -112,7 +117,7 @@ export function Mouse(__getGeometry, __recordWsEvent) {
|
||||
};
|
||||
|
||||
var __updateRate = function(value) {
|
||||
$("hid-mouse-rate-value").innerHTML = value + " ms";
|
||||
$("hid-mouse-rate-value").innerText = value + " ms";
|
||||
tools.storage.set("hid.mouse.rate", value);
|
||||
if (__timer) {
|
||||
clearInterval(__timer);
|
||||
@@ -121,19 +126,19 @@ export function Mouse(__getGeometry, __recordWsEvent) {
|
||||
};
|
||||
|
||||
var __updateScrollRate = function(value) {
|
||||
$("hid-mouse-scroll-value").innerHTML = value;
|
||||
$("hid-mouse-scroll-value").innerText = value;
|
||||
tools.storage.set("hid.mouse.scroll_rate", value);
|
||||
__scroll_rate = value;
|
||||
};
|
||||
|
||||
var __updateRelativeSens = function(value) {
|
||||
$("hid-mouse-sens-value").innerHTML = value.toFixed(1);
|
||||
$("hid-mouse-sens-value").innerText = value.toFixed(1);
|
||||
tools.storage.set("hid.mouse.sens", value);
|
||||
__relative_sens = value;
|
||||
__rel_sens = value;
|
||||
};
|
||||
|
||||
var __streamHoveredHandler = function(hovered) {
|
||||
if (__absolute) {
|
||||
if (__abs) {
|
||||
__stream_hovered = hovered;
|
||||
__updateOnlineLeds();
|
||||
}
|
||||
@@ -141,7 +146,7 @@ export function Mouse(__getGeometry, __recordWsEvent) {
|
||||
|
||||
var __updateOnlineLeds = function() {
|
||||
let is_captured;
|
||||
if (__absolute) {
|
||||
if (__abs) {
|
||||
is_captured = (__stream_hovered || tools.browser.is_mobile);
|
||||
} else {
|
||||
is_captured = __isRelativeCaptured();
|
||||
@@ -170,7 +175,7 @@ export function Mouse(__getGeometry, __recordWsEvent) {
|
||||
$("hid-mouse-led").className = led;
|
||||
$("hid-mouse-led").title = title;
|
||||
|
||||
if (__absolute && is_captured) {
|
||||
if (__abs && 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));
|
||||
@@ -189,128 +194,149 @@ export function Mouse(__getGeometry, __recordWsEvent) {
|
||||
__updateOnlineLeds();
|
||||
};
|
||||
|
||||
var __streamButtonHandler = function(event, state) {
|
||||
var __streamButtonHandler = function(ev, state) {
|
||||
// https://www.w3schools.com/jsref/event_button.asp
|
||||
event.preventDefault();
|
||||
if (__absolute || __isRelativeCaptured()) {
|
||||
switch (event.button) {
|
||||
ev.preventDefault();
|
||||
if (__abs || __isRelativeCaptured()) {
|
||||
switch (ev.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) {
|
||||
} else if (!__abs && !__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 __streamTouchStartHandler = function(ev) {
|
||||
ev.preventDefault();
|
||||
let pos = __getTouchPosition(ev, 0);
|
||||
if (__abs && ev.touches.length === 1) {
|
||||
__abs_pos = pos;
|
||||
__sendPlannedMove();
|
||||
} else if (!__abs) {
|
||||
__touch_pos = pos;
|
||||
__abs_pos = null;
|
||||
}
|
||||
};
|
||||
|
||||
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);
|
||||
var __streamTouchMoveHandler = function(ev) {
|
||||
ev.preventDefault();
|
||||
let pos = __getTouchPosition(ev, 0);
|
||||
if (ev.touches.length === 1) {
|
||||
if (__abs) {
|
||||
__abs_pos = pos;
|
||||
} else if (__touch_pos !== null) {
|
||||
__sendOrPlanRelativeMove({
|
||||
"x": (pos.x - __relative_touch_pos.x),
|
||||
"y": (pos.y - __relative_touch_pos.y),
|
||||
"x": (pos.x - __touch_pos.x),
|
||||
"y": (pos.y - __touch_pos.y),
|
||||
});
|
||||
__relative_touch_pos = pos;
|
||||
__touch_pos = pos;
|
||||
}
|
||||
} else if (ev.touches.length >= 2) {
|
||||
if (__touch_pos === null) {
|
||||
__touch_pos = pos;
|
||||
} else {
|
||||
let dx = __touch_pos.x - pos.x;
|
||||
let dy = __touch_pos.y - pos.y;
|
||||
if (Math.abs(dx) < 15) {
|
||||
dx = 0;
|
||||
}
|
||||
if (Math.abs(dy) < 15) {
|
||||
dy = 0;
|
||||
}
|
||||
if (dx || dy) {
|
||||
__sendScroll({"x": dx, "y": dy});
|
||||
__touch_pos = null;
|
||||
}
|
||||
}
|
||||
__abs_pos = null;
|
||||
}
|
||||
};
|
||||
|
||||
var __streamTouchEndHandler = function(event) {
|
||||
event.preventDefault();
|
||||
var __streamTouchEndHandler = function(ev) {
|
||||
ev.preventDefault();
|
||||
__sendPlannedMove();
|
||||
__touch_pos = null;
|
||||
__abs_pos = null;
|
||||
};
|
||||
|
||||
var __getTouchPosition = function(event, index) {
|
||||
if (event.touches[index].target && event.touches[index].target.getBoundingClientRect) {
|
||||
let rect = event.touches[index].target.getBoundingClientRect();
|
||||
var __getTouchPosition = function(ev, index) {
|
||||
if (ev.touches[index].target && ev.touches[index].target.getBoundingClientRect) {
|
||||
let rect = ev.touches[index].target.getBoundingClientRect();
|
||||
return {
|
||||
"x": Math.round(event.touches[index].clientX - rect.left),
|
||||
"y": Math.round(event.touches[index].clientY - rect.top),
|
||||
"x": Math.round(ev.touches[index].clientX - rect.left),
|
||||
"y": Math.round(ev.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),
|
||||
var __streamMoveHandler = function(ev) {
|
||||
if (__abs) {
|
||||
let rect = ev.target.getBoundingClientRect();
|
||||
__abs_pos = {
|
||||
"x": Math.max(Math.round(ev.clientX - rect.left), 0),
|
||||
"y": Math.max(Math.round(ev.clientY - rect.top), 0),
|
||||
};
|
||||
} else if (__isRelativeCaptured()) {
|
||||
__sendOrPlanRelativeMove({
|
||||
"x": event.movementX,
|
||||
"y": event.movementY,
|
||||
"x": ev.movementX,
|
||||
"y": ev.movementY,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
var __streamScrollHandler = function(event) {
|
||||
var __streamScrollHandler = function(ev) {
|
||||
// https://learn.javascript.ru/mousewheel
|
||||
// https://stackoverflow.com/a/24595588
|
||||
|
||||
event.preventDefault();
|
||||
ev.preventDefault();
|
||||
|
||||
if (!__absolute && !__isRelativeCaptured()) {
|
||||
if (!__abs && !__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);
|
||||
if (__scroll_delta.x && Math.sign(__scroll_delta.x) !== Math.sign(ev.deltaX)) {
|
||||
delta.x = __scroll_delta.x;
|
||||
__scroll_delta.x = 0;
|
||||
} else {
|
||||
__scroll_delta.x += ev.deltaX * __scroll_fix;
|
||||
if (Math.abs(__scroll_delta.x) >= 100) {
|
||||
delta.x = __scroll_delta.x;
|
||||
__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);
|
||||
if (__scroll_delta.y && Math.sign(__scroll_delta.y) !== Math.sign(ev.deltaY)) {
|
||||
delta.y = __scroll_delta.y;
|
||||
__scroll_delta.y = 0;
|
||||
} else {
|
||||
__scroll_delta.y += ev.deltaY * __scroll_fix;
|
||||
if (Math.abs(__scroll_delta.y) >= 100) {
|
||||
delta.y = __scroll_delta.y;
|
||||
__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);
|
||||
}
|
||||
delta.x = ev.deltaX;
|
||||
delta.y = ev.deltaY;
|
||||
}
|
||||
|
||||
__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),
|
||||
"x": Math.min(Math.max(-127, Math.floor(delta.x * __rel_sens)), 127),
|
||||
"y": Math.min(Math.max(-127, Math.floor(delta.y * __rel_sens)), 127),
|
||||
};
|
||||
if (delta.x || delta.y) {
|
||||
if ($("hid-mouse-squash-switch").checked) {
|
||||
__relative_deltas.push(delta);
|
||||
__rel_deltas.push(delta);
|
||||
} else {
|
||||
tools.debug("Mouse: relative:", delta);
|
||||
__sendEvent("mouse_relative", {"delta": delta});
|
||||
@@ -319,35 +345,41 @@ export function Mouse(__getGeometry, __recordWsEvent) {
|
||||
};
|
||||
|
||||
var __sendScroll = function(delta) {
|
||||
if (delta.x || delta.y) {
|
||||
if ($("hid-mouse-reverse-scrolling-switch").checked) {
|
||||
delta.y *= -1;
|
||||
}
|
||||
// Send a single scroll step defined by rate
|
||||
if (delta.x) {
|
||||
delta.x = Math.sign(delta.x) * (-__scroll_rate);
|
||||
if ($("hid-mouse-reverse-panning-switch").checked) {
|
||||
delta.x *= -1;
|
||||
}
|
||||
}
|
||||
if (delta.y) {
|
||||
delta.y = Math.sign(delta.y) * (-__scroll_rate);
|
||||
if ($("hid-mouse-reverse-scrolling-switch").checked) {
|
||||
delta.y *= -1;
|
||||
}
|
||||
}
|
||||
if (delta.x || delta.y) {
|
||||
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) {
|
||||
if (__abs) {
|
||||
if (__abs_pos !== null) {
|
||||
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),
|
||||
"x": tools.remap(__abs_pos.x - geo.x, 0, geo.width - 1, -32768, 32767),
|
||||
"y": tools.remap(__abs_pos.y - geo.y, 0, geo.height - 1, -32768, 32767),
|
||||
};
|
||||
tools.debug("Mouse: moved:", to);
|
||||
tools.debug("Mouse: abs:", to);
|
||||
__sendEvent("mouse_move", {"to": to});
|
||||
__sent_pos = pos;
|
||||
__abs_pos = null;
|
||||
}
|
||||
} else if (__relative_deltas.length) {
|
||||
tools.debug("Mouse: relative:", __relative_deltas);
|
||||
__sendEvent("mouse_relative", {"delta": __relative_deltas, "squash": true});
|
||||
__relative_deltas = [];
|
||||
} else if (__rel_deltas.length) {
|
||||
tools.debug("Mouse: relative:", __rel_deltas);
|
||||
__sendEvent("mouse_relative", {"delta": __rel_deltas, "squash": true});
|
||||
__rel_deltas = [];
|
||||
}
|
||||
};
|
||||
|
||||
@@ -357,12 +389,12 @@ export function Mouse(__getGeometry, __recordWsEvent) {
|
||||
__sendEvent("mouse_button", {"button": button, "state": state});
|
||||
};
|
||||
|
||||
var __sendEvent = function(event_type, event) {
|
||||
event = {"event_type": event_type, "event": event};
|
||||
var __sendEvent = function(ev_type, ev) {
|
||||
ev = {"event_type": ev_type, "event": ev};
|
||||
if (__ws && !$("hid-mute-switch").checked) {
|
||||
__ws.sendHidEvent(event);
|
||||
__ws.sendHidEvent(ev);
|
||||
}
|
||||
__recordWsEvent(event);
|
||||
__recordWsEvent(ev);
|
||||
};
|
||||
|
||||
__init__();
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
"use strict";
|
||||
|
||||
|
||||
import {ROOT_PREFIX} from "../vars.js";
|
||||
import {tools, $} from "../tools.js";
|
||||
import {wm} from "../wm.js";
|
||||
|
||||
@@ -226,7 +227,7 @@ export function Msd() {
|
||||
if (el.__names_json !== names_json) {
|
||||
el.innerHTML = names.map(name => `
|
||||
<div class="text">
|
||||
<div id="__msd-storage-${tools.makeIdByText(name)}-progress" class="progress">
|
||||
<div id="__msd-storage-${tools.makeTextId(name)}-progress" class="progress">
|
||||
<span class="progress-value"></span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -241,7 +242,7 @@ export function Msd() {
|
||||
? `${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`;
|
||||
let id = `__msd-storage-${tools.makeTextId(name)}-progress`;
|
||||
tools.progress.setSizeOf($(id), title, part.size, part.free);
|
||||
}
|
||||
};
|
||||
@@ -291,15 +292,15 @@ export function Msd() {
|
||||
};
|
||||
|
||||
var __clickDownloadButton = function() {
|
||||
let image = encodeURIComponent($("msd-image-selector").value);
|
||||
window.open(`/api/msd/read?image=${image}`);
|
||||
let e_image = encodeURIComponent($("msd-image-selector").value);
|
||||
tools.windowOpen(`api/msd/read?image=${e_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) {
|
||||
tools.httpPost("api/msd/remove", {"image": name}, function(http) {
|
||||
if (http.status !== 200) {
|
||||
wm.error("Can't remove image", http.responseText);
|
||||
}
|
||||
@@ -309,7 +310,7 @@ export function Msd() {
|
||||
};
|
||||
|
||||
var __sendParam = function(name, value) {
|
||||
tools.httpPost("/api/msd/set_params", {[name]: value}, function(http) {
|
||||
tools.httpPost("api/msd/set_params", {[name]: value}, function(http) {
|
||||
if (http.status !== 200) {
|
||||
wm.error("Can't configure Mass Storage", http.responseText);
|
||||
}
|
||||
@@ -335,11 +336,11 @@ export function Msd() {
|
||||
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);
|
||||
let e_image = encodeURIComponent(file.name);
|
||||
__http.open("POST", `${ROOT_PREFIX}api/msd/write?prefix=${e_prefix}&image=${e_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);
|
||||
let e_url = encodeURIComponent($("msd-new-url").value);
|
||||
__http.open("POST", `${ROOT_PREFIX}api/msd/write_remote?prefix=${e_prefix}&url=${e_url}&remove_incomplete=1`, true);
|
||||
}
|
||||
__http.upload.timeout = 7 * 24 * 3600;
|
||||
__http.onreadystatechange = __uploadStateChange;
|
||||
@@ -395,7 +396,7 @@ export function Msd() {
|
||||
};
|
||||
|
||||
var __clickConnectButton = function(connected) {
|
||||
tools.httpPost("/api/msd/set_connected", {"connected": connected}, function(http) {
|
||||
tools.httpPost("api/msd/set_connected", {"connected": connected}, function(http) {
|
||||
if (http.status !== 200) {
|
||||
wm.error("Can't switch Mass Storage", http.responseText);
|
||||
}
|
||||
@@ -423,7 +424,7 @@ export function Msd() {
|
||||
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) {
|
||||
tools.httpPost("api/msd/reset", null, function(http) {
|
||||
if (http.status !== 200) {
|
||||
wm.error("Mass Storage reset error", http.responseText);
|
||||
}
|
||||
@@ -451,7 +452,8 @@ export function Msd() {
|
||||
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)}`);
|
||||
let e_size = tools.escape(tools.formatSize(part.size));
|
||||
wm.error(`The new image is too big for the Mass Storage partition.<br>Maximum: ${e_size}`);
|
||||
el.value = "";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,6 +25,7 @@
|
||||
|
||||
import {tools, $} from "../tools.js";
|
||||
import {wm} from "../wm.js";
|
||||
import {clipboard} from "./clipboard.js";
|
||||
|
||||
|
||||
export function Ocr(__getGeometry) {
|
||||
@@ -53,14 +54,14 @@ export function Ocr(__getGeometry) {
|
||||
$("stream-ocr-window").addEventListener("resize", __resetSelection);
|
||||
$("stream-ocr-window").close_hook = __resetSelection;
|
||||
|
||||
$("stream-ocr-window").onkeyup = function(event) {
|
||||
event.preventDefault();
|
||||
if (event.code === "Enter") {
|
||||
$("stream-ocr-window").onkeyup = function(ev) {
|
||||
ev.preventDefault();
|
||||
if (ev.code === "Enter") {
|
||||
if (__sel) {
|
||||
__recognizeSelection();
|
||||
wm.closeWindow($("stream-ocr-window"));
|
||||
}
|
||||
} else if (event.code === "Escape") {
|
||||
} else if (ev.code === "Escape") {
|
||||
wm.closeWindow($("stream-ocr-window"));
|
||||
}
|
||||
};
|
||||
@@ -98,17 +99,17 @@ export function Ocr(__getGeometry) {
|
||||
el.value = tools.storage.get("stream.ocr.lang", langs["default"]);
|
||||
};
|
||||
|
||||
var __startSelection = function(event) {
|
||||
var __startSelection = function(ev) {
|
||||
if (__start_pos === null) {
|
||||
tools.hidden.setVisible($("stream-ocr-selection"), false);
|
||||
__start_pos = __getGlobalPosition(event);
|
||||
__start_pos = __getGlobalPosition(ev);
|
||||
__end_pos = null;
|
||||
}
|
||||
};
|
||||
|
||||
var __changeSelection = function(event) {
|
||||
var __changeSelection = function(ev) {
|
||||
if (__start_pos !== null) {
|
||||
__end_pos = __getGlobalPosition(event);
|
||||
__end_pos = __getGlobalPosition(ev);
|
||||
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");
|
||||
@@ -120,8 +121,8 @@ export function Ocr(__getGeometry) {
|
||||
}
|
||||
};
|
||||
|
||||
var __endSelection = function(event) {
|
||||
__changeSelection(event);
|
||||
var __endSelection = function(ev) {
|
||||
__changeSelection(ev);
|
||||
let el = $("stream-ocr-selection");
|
||||
let ok = (
|
||||
el.offsetWidth > 1 && el.offsetHeight > 1
|
||||
@@ -137,10 +138,10 @@ export function Ocr(__getGeometry) {
|
||||
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),
|
||||
"left": tools.remap(rel_left - geo.x, 0, geo.width, 0, geo.real_width),
|
||||
"right": tools.remap(rel_right - geo.x, 0, geo.width, 0, geo.real_width),
|
||||
"top": tools.remap(rel_top - geo.y, 0, geo.height, 0, geo.real_height),
|
||||
"bottom": tools.remap(rel_bottom - geo.y, 0, geo.height, 0, geo.real_height),
|
||||
};
|
||||
} else {
|
||||
__sel = null;
|
||||
@@ -149,13 +150,13 @@ export function Ocr(__getGeometry) {
|
||||
__end_pos = null;
|
||||
};
|
||||
|
||||
var __getGlobalPosition = function(event) {
|
||||
var __getGlobalPosition = function(ev) {
|
||||
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),
|
||||
"x": Math.min(Math.max(ev.clientX, rect.left + geo.x), rect.right - geo.x),
|
||||
"y": Math.min(Math.max(ev.clientY - offset, rect.top + geo.y - offset), rect.bottom - geo.y - offset),
|
||||
};
|
||||
};
|
||||
|
||||
@@ -179,6 +180,7 @@ export function Ocr(__getGeometry) {
|
||||
tools.el.setEnabled($("stream-ocr-lang-selector"), false);
|
||||
$("stream-ocr-led").className = "led-yellow-rotating-fast";
|
||||
let params = {
|
||||
"allow_offline": 1,
|
||||
"ocr": 1,
|
||||
"ocr_langs": $("stream-ocr-lang-selector").value,
|
||||
"ocr_left": __sel.left,
|
||||
@@ -186,9 +188,9 @@ export function Ocr(__getGeometry) {
|
||||
"ocr_right": __sel.right,
|
||||
"ocr_bottom": __sel.bottom,
|
||||
};
|
||||
tools.httpGet("/api/streamer/snapshot", params, function(http) {
|
||||
tools.httpGet("api/streamer/snapshot", params, function(http) {
|
||||
if (http.status === 200) {
|
||||
wm.copyTextToClipboard(http.responseText);
|
||||
clipboard.setText(http.responseText);
|
||||
} else {
|
||||
wm.error("OCR error:<br>", http.responseText);
|
||||
}
|
||||
|
||||
@@ -33,14 +33,27 @@ export function Paste(__recorder) {
|
||||
/************************************************************************/
|
||||
|
||||
var __init__ = function() {
|
||||
$("hid-pak-text").addEventListener("keyup", function(ev) {
|
||||
if (ev.ctrlKey && ev.code == "Enter") {
|
||||
$("hid-pak-button").click();
|
||||
}
|
||||
});
|
||||
|
||||
tools.storage.bindSimpleSwitch($("hid-pak-ask-switch"), "hid.pak.ask", true);
|
||||
tools.storage.bindSimpleSwitch($("hid-pak-slow-switch"), "hid.pak.slow", false);
|
||||
|
||||
tools.storage.bindSimpleSwitch($("hid-pak-secure-switch"), "hid.pak.secure", false, function(value) {
|
||||
$("hid-pak-text").style.setProperty("-webkit-text-security", (value ? "disc" : "none"));
|
||||
});
|
||||
|
||||
tools.slider.setParams($("hid-pak-delay-slider"), 0, 200, 20, tools.storage.getInt("hid.pak.delay", 20), function (value) {
|
||||
$("hid-pak-delay-value").innerText = value + " ms";
|
||||
tools.storage.setInt("hid.pak.delay", value);
|
||||
});
|
||||
|
||||
$("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);
|
||||
};
|
||||
|
||||
@@ -68,11 +81,11 @@ export function Paste(__recorder) {
|
||||
tools.el.setEnabled($("hid-pak-keymap-selector"), false);
|
||||
|
||||
let keymap = $("hid-pak-keymap-selector").value;
|
||||
let slow = $("hid-pak-slow-switch").checked;
|
||||
let delay = $("hid-pak-delay-slider").value;
|
||||
|
||||
tools.debug(`HID: paste-as-keys ${keymap}: ${text}`);
|
||||
|
||||
tools.httpPost("/api/hid/print", {"limit": 0, "keymap": keymap, "slow": slow}, function(http) {
|
||||
tools.httpPost("api/hid/print", {"limit": 0, "keymap": keymap, "delay": delay / 1000}, function(http) {
|
||||
tools.el.setEnabled($("hid-pak-text"), true);
|
||||
tools.el.setEnabled($("hid-pak-button"), true);
|
||||
tools.el.setEnabled($("hid-pak-keymap-selector"), true);
|
||||
@@ -82,9 +95,9 @@ export function Paste(__recorder) {
|
||||
} else if (http.status !== 200) {
|
||||
wm.error("HID paste error", http.responseText);
|
||||
} else if (http.status === 200) {
|
||||
__recorder.recordPrintEvent(text, keymap, slow);
|
||||
__recorder.recordPrintEvent(text, keymap, delay);
|
||||
}
|
||||
}, text, "text/plain");
|
||||
}, text, "text/plain", 7 * 24 * 3600);
|
||||
};
|
||||
|
||||
if ($("hid-pak-ask-switch").checked) {
|
||||
|
||||
@@ -63,12 +63,12 @@ export function Recorder() {
|
||||
__refresh();
|
||||
};
|
||||
|
||||
self.recordWsEvent = function(event) {
|
||||
__recordEvent(event);
|
||||
self.recordWsEvent = function(ev) {
|
||||
__recordEvent(ev);
|
||||
};
|
||||
|
||||
self.recordPrintEvent = function(text, keymap, slow) {
|
||||
__recordEvent({"event_type": "print", "event": {"text": text, "keymap": keymap, "slow": slow}});
|
||||
self.recordPrintEvent = function(text, keymap, delay) {
|
||||
__recordEvent({"event_type": "print", "event": {"text": text, "keymap": keymap, "delay": delay}});
|
||||
};
|
||||
|
||||
self.recordAtxButtonEvent = function(button) {
|
||||
@@ -83,7 +83,7 @@ export function Recorder() {
|
||||
__recordEvent({"event_type": "gpio_pulse", "event": {"channel": channel}});
|
||||
};
|
||||
|
||||
var __recordEvent = function(event) {
|
||||
var __recordEvent = function(ev) {
|
||||
if (__recording) {
|
||||
let now = new Date().getTime();
|
||||
if (__last_event_ts) {
|
||||
@@ -92,7 +92,7 @@ export function Recorder() {
|
||||
__events_time += delay;
|
||||
}
|
||||
__last_event_ts = now;
|
||||
__events.push(event);
|
||||
__events.push(ev);
|
||||
__setCounters(__events.length, __events_time);
|
||||
}
|
||||
};
|
||||
@@ -149,73 +149,76 @@ export function Recorder() {
|
||||
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");
|
||||
for (let ev of raw_events) {
|
||||
__checkType(ev, "object", "Non-dict event");
|
||||
__checkType(ev.event, "object", "Non-dict event");
|
||||
|
||||
if (event.event_type === "delay") {
|
||||
__checkUnsigned(event.event.millis, "Non-unsigned delay");
|
||||
events_time += event.event.millis;
|
||||
if (ev.event_type === "delay") {
|
||||
__checkUnsigned(ev.event.millis, "Non-unsigned delay");
|
||||
events_time += ev.event.millis;
|
||||
|
||||
} else if (event.event_type === "print") {
|
||||
__checkType(event.event.text, "string", "Non-string print text");
|
||||
if (event.event.keymap !== undefined) {
|
||||
__checkType(event.event.keymap, "string", "Non-string keymap");
|
||||
} else if (ev.event_type === "print") {
|
||||
__checkType(ev.event.text, "string", "Non-string print text");
|
||||
if (ev.event.keymap !== undefined) {
|
||||
__checkType(ev.event.keymap, "string", "Non-string keymap");
|
||||
}
|
||||
if (event.event.slow !== undefined) {
|
||||
__checkType(event.event.slow, "boolean", "Non-bool slow");
|
||||
if (ev.event.slow !== undefined) {
|
||||
__checkType(ev.event.slow, "boolean", "Non-bool slow");
|
||||
}
|
||||
if (ev.event.delay !== undefined) {
|
||||
__checkInt(ev.event.delay, "Non-int delay");
|
||||
}
|
||||
|
||||
} 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 (ev.event_type === "key") {
|
||||
__checkType(ev.event.key, "string", "Non-string key code");
|
||||
__checkType(ev.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 (ev.event_type === "mouse_button") {
|
||||
__checkType(ev.event.button, "string", "Non-string mouse button code");
|
||||
__checkType(ev.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 (ev.event_type === "mouse_move") {
|
||||
__checkType(ev.event.to, "object", "Non-object mouse move target");
|
||||
__checkInt(ev.event.to.x, "Non-int mouse move X");
|
||||
__checkInt(ev.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 (ev.event_type === "mouse_relative") {
|
||||
__checkMouseRelativeDelta(ev.event.delta);
|
||||
__checkType(ev.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 (ev.event_type === "mouse_wheel") {
|
||||
__checkType(ev.event.delta, "object", "Non-object mouse wheel delta");
|
||||
__checkInt(ev.event.delta.x, "Non-int mouse delta X");
|
||||
__checkInt(ev.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 (ev.event_type === "atx_button") {
|
||||
__checkType(ev.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 (ev.event_type === "gpio_switch") {
|
||||
__checkType(ev.event.channel, "string", "Non-string GPIO channel");
|
||||
__checkType(ev.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 (ev.event_type === "gpio_pulse") {
|
||||
__checkType(ev.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 (ev.event_type === "delay_random") {
|
||||
__checkType(ev.event.range, "object", "Non-object random delay range");
|
||||
__checkUnsigned(ev.event.range.min, "Non-unsigned random delay range min");
|
||||
__checkUnsigned(ev.event.range.max, "Non-unsigned random delay range max");
|
||||
__checkRangeMinMax(ev.event.range, "Invalid random delay range");
|
||||
events_time += ev.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 if (ev.event_type === "mouse_move_random") { // Hack for pikvm/pikvm#1041
|
||||
__checkType(ev.event.range, "object", "Non-object random mouse move range");
|
||||
__checkInt(ev.event.range.min, "Non-int random mouse move range min");
|
||||
__checkInt(ev.event.range.max, "Non-int random mouse move range max");
|
||||
__checkRangeMinMax(ev.event.range, "Invalid random mouse move range");
|
||||
|
||||
} else {
|
||||
throw `Unknown event type: ${event.event_type}`;
|
||||
throw `Unknown event type: ${ev.event_type}`;
|
||||
}
|
||||
|
||||
events.push(event);
|
||||
events.push(ev);
|
||||
}
|
||||
|
||||
__events = events;
|
||||
@@ -274,26 +277,29 @@ export function Recorder() {
|
||||
var __runEvents = function(index, time=0) {
|
||||
while (index < __events.length) {
|
||||
__setCounters(__events.length - index + 1, __events_time - time);
|
||||
let event = __events[index];
|
||||
let ev = __events[index];
|
||||
|
||||
if (["delay", "delay_random"].includes(event.event_type)) {
|
||||
if (["delay", "delay_random"].includes(ev.event_type)) {
|
||||
let millis = (
|
||||
event.event_type === "delay"
|
||||
? event.event.millis
|
||||
: tools.getRandomInt(event.event.range.min, event.event.range.max)
|
||||
ev.event_type === "delay"
|
||||
? ev.event.millis
|
||||
: tools.getRandomInt(ev.event.range.min, ev.event.range.max)
|
||||
);
|
||||
__play_timer = setTimeout(() => __runEvents(index + 1, time + millis), millis);
|
||||
return;
|
||||
|
||||
} else if (event.event_type === "print") {
|
||||
} else if (ev.event_type === "print") {
|
||||
let params = {"limit": 0};
|
||||
if (event.event.keymap !== undefined) {
|
||||
params["keymap"] = event.event.keymap;
|
||||
if (ev.event.keymap !== undefined) {
|
||||
params["keymap"] = ev.event.keymap;
|
||||
}
|
||||
if (event.event.slow !== undefined) {
|
||||
params["slow"] = event.event.slow;
|
||||
if (ev.event.slow !== undefined) {
|
||||
params["slow"] = ev.event.slow;
|
||||
}
|
||||
tools.httpPost("/api/hid/print", params, function(http) {
|
||||
if (ev.event.delay !== undefined) {
|
||||
params["delay"] = ev.event.delay / 1000;
|
||||
}
|
||||
tools.httpPost("api/hid/print", params, function(http) {
|
||||
if (http.status === 413) {
|
||||
wm.error("Too many text for paste!");
|
||||
__stopProcess();
|
||||
@@ -303,11 +309,11 @@ export function Recorder() {
|
||||
} else if (http.status === 200) {
|
||||
__play_timer = setTimeout(() => __runEvents(index + 1, time), 0);
|
||||
}
|
||||
}, event.event.text, "text/plain");
|
||||
}, ev.event.text, "text/plain");
|
||||
return;
|
||||
|
||||
} else if (event.event_type === "atx_button") {
|
||||
tools.httpPost("/api/atx/click", {"button": event.event.button}, function(http) {
|
||||
} else if (ev.event_type === "atx_button") {
|
||||
tools.httpPost("api/atx/click", {"button": ev.event.button}, function(http) {
|
||||
if (http.status !== 200) {
|
||||
wm.error("ATX error", http.responseText);
|
||||
__stopProcess();
|
||||
@@ -317,12 +323,12 @@ export function Recorder() {
|
||||
});
|
||||
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") {
|
||||
} else if (["gpio_switch", "gpio_pulse"].includes(ev.event_type)) {
|
||||
let path = "api/gpio";
|
||||
let params = {"channel": ev.event.channel};
|
||||
if (ev.event_type === "gpio_switch") {
|
||||
path += "/switch";
|
||||
params["state"] = event.event.to;
|
||||
params["state"] = ev.event.to;
|
||||
} else { // gpio_pulse
|
||||
path += "/pulse";
|
||||
}
|
||||
@@ -336,19 +342,19 @@ export function Recorder() {
|
||||
});
|
||||
return;
|
||||
|
||||
} else if (event.event_type === "key") {
|
||||
event.event.finish = $("hid-keyboard-bad-link-switch").checked;
|
||||
__ws.sendHidEvent(event);
|
||||
} else if (ev.event_type === "key") {
|
||||
ev.event.finish = $("hid-keyboard-bad-link-switch").checked;
|
||||
__ws.sendHidEvent(ev);
|
||||
|
||||
} else if (["mouse_button", "mouse_move", "mouse_wheel", "mouse_relative"].includes(event.event_type)) {
|
||||
__ws.sendHidEvent(event);
|
||||
} else if (["mouse_button", "mouse_move", "mouse_wheel", "mouse_relative"].includes(ev.event_type)) {
|
||||
__ws.sendHidEvent(ev);
|
||||
|
||||
} else if (event.event_type === "mouse_move_random") {
|
||||
} else if (ev.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),
|
||||
"x": tools.getRandomInt(ev.event.range.min, ev.event.range.max),
|
||||
"y": tools.getRandomInt(ev.event.range.min, ev.event.range.max),
|
||||
}},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
import {tools, $} from "../tools.js";
|
||||
import {wm} from "../wm.js";
|
||||
|
||||
import {Info} from "./info.js";
|
||||
import {Recorder} from "./recorder.js";
|
||||
import {Hid} from "./hid.js";
|
||||
import {Paste} from "./paste.js";
|
||||
@@ -48,6 +49,7 @@ export function Session() {
|
||||
var __ping_timer = null;
|
||||
var __missed_heartbeats = 0;
|
||||
|
||||
var __info = new Info();
|
||||
var __streamer = new Streamer();
|
||||
var __recorder = new Recorder();
|
||||
var __hid = new Hid(__streamer.getGeometry, __recorder);
|
||||
@@ -58,252 +60,36 @@ export function Session() {
|
||||
var __ocr = new Ocr(__streamer.getGeometry);
|
||||
var __switch = new Switch();
|
||||
|
||||
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 = `One-KVM Session: ${state.server.host}`;
|
||||
} else {
|
||||
$("kvmd-meta-server-host").innerText = "";
|
||||
document.title = "One-KVM 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]}°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) {
|
||||
tools.httpGet("api/auth/check", null, function(http) {
|
||||
if (http.status === 200) {
|
||||
__ws = new WebSocket(`${tools.is_https ? "wss" : "ws"}://${location.host}/api/ws`);
|
||||
__ws.sendHidEvent = (event) => __sendHidEvent(__ws, event.event_type, event.event);
|
||||
__ws = new WebSocket(tools.makeWsUrl("api/ws"));
|
||||
__ws.sendHidEvent = (ev) => __sendHidEvent(__ws, ev.event_type, ev.event);
|
||||
__ws.binaryType = "arraybuffer";
|
||||
__ws.onopen = __wsOpenHandler;
|
||||
__ws.onmessage = __wsMessageHandler;
|
||||
__ws.onmessage = async (ev) => {
|
||||
if (typeof ev.data === "string") {
|
||||
ev = JSON.parse(ev.data);
|
||||
__wsJsonHandler(ev.event_type, ev.event);
|
||||
} else { // Binary
|
||||
__wsBinHandler(ev.data);
|
||||
}
|
||||
};
|
||||
__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";
|
||||
tools.currentOpen("login");
|
||||
});
|
||||
} else {
|
||||
__wsCloseHandler(null);
|
||||
@@ -311,51 +97,8 @@ export function Session() {
|
||||
});
|
||||
};
|
||||
|
||||
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);
|
||||
if (event.finish === true) { // Optional
|
||||
data[1] |= 0x02;
|
||||
}
|
||||
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);
|
||||
var __wsOpenHandler = function(ev) {
|
||||
tools.debug("Session: socket opened:", ev);
|
||||
$("link-led").className = "led-green";
|
||||
$("link-led").title = "Connected";
|
||||
__recorder.setSocket(__ws);
|
||||
@@ -364,39 +107,43 @@ export function Session() {
|
||||
__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": __setInfoState(data.event); break;
|
||||
case "gpio": __gpio.setState(data.event); break;
|
||||
case "hid": __hid.setState(data.event); break;
|
||||
case "hid_keymaps": __paste.setState(data.event); break;
|
||||
case "atx": __atx.setState(data.event); break;
|
||||
case "streamer": __streamer.setState(data.event); break;
|
||||
case "ocr": __ocr.setState(data.event); break;
|
||||
var __wsBinHandler = function(data) {
|
||||
data = new Uint8Array(data);
|
||||
if (data[0] === 255) { // Pong
|
||||
__missed_heartbeats = 0;
|
||||
}
|
||||
};
|
||||
|
||||
var __wsJsonHandler = function(ev_type, ev) {
|
||||
switch (ev_type) {
|
||||
case "info": __info.setState(ev); break;
|
||||
case "gpio": __gpio.setState(ev); break;
|
||||
case "hid": __hid.setState(ev); break;
|
||||
case "hid_keymaps": __paste.setState(ev); break;
|
||||
case "atx": __atx.setState(ev); break;
|
||||
case "streamer": __streamer.setState(ev); break;
|
||||
case "ocr": __ocr.setState(ev); break;
|
||||
|
||||
case "msd":
|
||||
if (data.event.online === false) {
|
||||
if (ev.online === false) {
|
||||
__switch.setMsdConnected(false);
|
||||
} else if (data.event.drive !== undefined) {
|
||||
__switch.setMsdConnected(data.event.drive.connected);
|
||||
} else if (ev.drive !== undefined) {
|
||||
__switch.setMsdConnected(ev.drive.connected);
|
||||
}
|
||||
__msd.setState(data.event);
|
||||
__msd.setState(ev);
|
||||
break;
|
||||
|
||||
case "switch":
|
||||
if (data.event.model) {
|
||||
__atx.setHasSwitch(data.event.model.ports.length > 0);
|
||||
if (ev.model) {
|
||||
__atx.setHasSwitch(ev.model.ports.length > 0);
|
||||
}
|
||||
__switch.setState(data.event);
|
||||
__switch.setState(ev);
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
var __wsErrorHandler = function(event) {
|
||||
tools.error("Session: socket error:", event);
|
||||
var __wsErrorHandler = function(ev) {
|
||||
tools.error("Session: socket error:", ev);
|
||||
if (__ws) {
|
||||
__ws.onclose = null;
|
||||
__ws.close();
|
||||
@@ -404,9 +151,8 @@ export function Session() {
|
||||
}
|
||||
};
|
||||
|
||||
var __wsCloseHandler = function(event) {
|
||||
tools.debug("Session: socket closed:", event);
|
||||
|
||||
var __wsCloseHandler = function(ev) {
|
||||
tools.debug("Session: socket closed:", ev);
|
||||
$("link-led").className = "led-gray";
|
||||
|
||||
if (__ping_timer) {
|
||||
@@ -437,11 +183,54 @@ export function Session() {
|
||||
if (__missed_heartbeats >= 15) {
|
||||
throw new Error("Too many missed heartbeats");
|
||||
}
|
||||
__ws.send("{\"event_type\": \"ping\", \"event\": {}}");
|
||||
__ws.send(new Uint8Array([0]));
|
||||
} catch (ex) {
|
||||
__wsErrorHandler(ex.message);
|
||||
}
|
||||
};
|
||||
|
||||
var __ascii_encoder = new TextEncoder("ascii");
|
||||
|
||||
var __sendHidEvent = function(ws, ev_type, ev) {
|
||||
if (ev_type === "key") {
|
||||
let data = __ascii_encoder.encode("\x01\x00" + ev.key);
|
||||
data[1] = (ev.state ? 1 : 0);
|
||||
if (ev.finish === true) { // Optional
|
||||
data[1] |= 0x02;
|
||||
}
|
||||
ws.send(data);
|
||||
|
||||
} else if (ev_type === "mouse_button") {
|
||||
let data = __ascii_encoder.encode("\x02\x00" + ev.button);
|
||||
data[1] = (ev.state ? 1 : 0);
|
||||
ws.send(data);
|
||||
|
||||
} else if (ev_type === "mouse_move") {
|
||||
let data = new Uint8Array([
|
||||
3,
|
||||
(ev.to.x >> 8) & 0xFF, ev.to.x & 0xFF,
|
||||
(ev.to.y >> 8) & 0xFF, ev.to.y & 0xFF,
|
||||
]);
|
||||
ws.send(data);
|
||||
|
||||
} else if (ev_type === "mouse_relative" || ev_type === "mouse_wheel") {
|
||||
let data;
|
||||
if (Array.isArray(ev.delta)) {
|
||||
data = new Int8Array(2 + ev.delta.length * 2);
|
||||
let index = 0;
|
||||
for (let delta of ev.delta) {
|
||||
data[index + 2] = delta["x"];
|
||||
data[index + 3] = delta["y"];
|
||||
index += 2;
|
||||
}
|
||||
} else {
|
||||
data = new Int8Array([0, 0, ev.delta.x, ev.delta.y]);
|
||||
}
|
||||
data[0] = (ev_type === "mouse_relative" ? 4 : 5);
|
||||
data[1] = (ev.squash ? 1 : 0);
|
||||
ws.send(data);
|
||||
}
|
||||
};
|
||||
|
||||
__init__();
|
||||
}
|
||||
|
||||
@@ -44,9 +44,9 @@ export function Streamer() {
|
||||
var __res = {"width": 640, "height": 480};
|
||||
|
||||
var __init__ = function() {
|
||||
__streamer = new MjpegStreamer(__setActive, __setInactive, __setInfo);
|
||||
__streamer = new MjpegStreamer(__setActive, __setInactive, __setInfo, __organizeHook);
|
||||
|
||||
$("stream-led").title = "Stream inactive";
|
||||
$("stream-led").title = "No stream from PiKVM";
|
||||
|
||||
tools.slider.setParams($("stream-quality-slider"), 5, 100, 5, 80, function(value) {
|
||||
$("stream-quality-value").innerText = `${value}%`;
|
||||
@@ -73,13 +73,13 @@ export function Streamer() {
|
||||
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.
|
||||
// Also don't reset Streamer 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
|
||||
if (["janus", "media"].includes(__streamer.getMode())) {
|
||||
let orient = parseInt(tools.radio.getValue("stream-orient-radio"));
|
||||
tools.storage.setInt("stream.orient", orient);
|
||||
if (__streamer.getOrientation() != orient) {
|
||||
if (__streamer.getOrientation() !== orient) {
|
||||
__resetStream();
|
||||
}
|
||||
}
|
||||
@@ -112,15 +112,37 @@ export function Streamer() {
|
||||
tools.el.setOnClick($("stream-record-stop-button"), __clickRecordStopButton);
|
||||
|
||||
|
||||
$("stream-window").show_hook = () => __applyState(__state);
|
||||
$("stream-window").close_hook = () => __applyState(null);
|
||||
tools.storage.bindSimpleSwitch($("stream-suspend-switch"), "stream.suspend", false, __visibilityHook);
|
||||
|
||||
//hidden stream-record-stop-button
|
||||
document.getElementById('stream-record-stop-button').disabled = true;
|
||||
$("stream-window").show_hook = __visibilityHook;
|
||||
$("stream-window").close_hook = __visibilityHook;
|
||||
$("stream-window").organize_hook = __organizeHook;
|
||||
|
||||
document.addEventListener("visibilitychange", __visibilityHook);
|
||||
};
|
||||
|
||||
/************************************************************************/
|
||||
|
||||
var __isStreamRequired = function() {
|
||||
return (
|
||||
wm.isWindowVisible($("stream-window"))
|
||||
&& (
|
||||
!$("stream-suspend-switch").checked
|
||||
|| (document.visibilityState === "visible")
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
var __visibilityHook = function() {
|
||||
let req = __isStreamRequired();
|
||||
__applyState(req ? __state : null);
|
||||
};
|
||||
|
||||
var __organizeHook = function() {
|
||||
let geo = self.getGeometry();
|
||||
wm.setAspectRatio($("stream-window"), geo.width, geo.height);
|
||||
};
|
||||
|
||||
self.ensureDeps = function(callback) {
|
||||
JanusStreamer.ensure_janus(function(avail) {
|
||||
__janus_imported = avail;
|
||||
@@ -167,13 +189,13 @@ export function Streamer() {
|
||||
__state = null;
|
||||
__setControlsEnabled(false);
|
||||
}
|
||||
let visible = wm.isWindowVisible($("stream-window"));
|
||||
__applyState((visible && __state && __state.features) ? state : null);
|
||||
__applyState((__isStreamRequired() && __state && __state.features) ? state : null);
|
||||
};
|
||||
|
||||
var __applyState = function(state) {
|
||||
if (__janus_imported === null) {
|
||||
alert("__janus_imported is null, please report");
|
||||
// XXX: This warning is triggered by visibilitychange event via the __visibilityHook()
|
||||
// alert("__janus_imported is null, please report");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -267,7 +289,7 @@ export function Streamer() {
|
||||
|
||||
var __setInactive = function() {
|
||||
$("stream-led").className = "led-gray";
|
||||
$("stream-led").title = "Stream inactive";
|
||||
$("stream-led").title = "No stream from PiKVM";
|
||||
};
|
||||
|
||||
var __setControlsEnabled = function(enabled) {
|
||||
@@ -285,7 +307,7 @@ export function Streamer() {
|
||||
let title = `${__streamer.getName()} - `;
|
||||
if (is_active) {
|
||||
if (!online) {
|
||||
title += "No signal / ";
|
||||
title += "No video from host / ";
|
||||
}
|
||||
title += `${__res.width}x${__res.height}`;
|
||||
if (text.length > 0) {
|
||||
@@ -295,7 +317,7 @@ export function Streamer() {
|
||||
if (text.length > 0) {
|
||||
title += text;
|
||||
} else {
|
||||
title += "Inactive";
|
||||
title += "No stream from PiKVM";
|
||||
}
|
||||
}
|
||||
el_grab.innerText = el_info.innerText = title;
|
||||
@@ -306,27 +328,26 @@ export function Streamer() {
|
||||
mode = __streamer.getMode();
|
||||
}
|
||||
__streamer.stopStream();
|
||||
if (mode === "mjpeg") {
|
||||
// For mjpeg mode, create an instance of MjpegStreamer
|
||||
__streamer = new MjpegStreamer(__setActive, __setInactive, __setInfo);
|
||||
tools.feature.setEnabled($("stream-orient"), false);
|
||||
tools.feature.setEnabled($("stream-audio"), false); // Enabling in stream_janus.js
|
||||
tools.feature.setEnabled($("stream-mic"), false); // Ditto
|
||||
} else if (mode === "media") {
|
||||
// For media mode, create an instance of MediaStreamer
|
||||
__streamer = new MediaStreamer(__setActive, __setInactive, __setInfo);
|
||||
tools.feature.setEnabled($("stream-orient"), false);
|
||||
tools.feature.setEnabled($("stream-audio"), false); // Assuming this should be disabled for MediaStreamer as well
|
||||
tools.feature.setEnabled($("stream-mic"), false); // Ditto
|
||||
} else { // janus
|
||||
// For janus mode, create an instance of JanusStreamer with specific settings
|
||||
__streamer = new JanusStreamer(__setActive, __setInactive, __setInfo,
|
||||
tools.storage.getInt("stream.orient", 0), !$("stream-video").muted, $("stream-mic-switch").checked);
|
||||
let orient = tools.storage.getInt("stream.orient", 0);
|
||||
if (mode === "janus") {
|
||||
let allow_audio = !$("stream-video").muted;
|
||||
let allow_mic = $("stream-mic-switch").checked;
|
||||
__streamer = new JanusStreamer(__setActive, __setInactive, __setInfo, __organizeHook, orient, allow_audio, allow_mic);
|
||||
// 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 {
|
||||
if (mode === "media") {
|
||||
__streamer = new MediaStreamer(__setActive, __setInactive, __setInfo, __organizeHook, orient);
|
||||
tools.feature.setEnabled($("stream-orient"), true);
|
||||
} else { // mjpeg
|
||||
__streamer = new MjpegStreamer(__setActive, __setInactive, __setInfo, __organizeHook);
|
||||
tools.feature.setEnabled($("stream-orient"), false);
|
||||
}
|
||||
tools.feature.setEnabled($("stream-audio"), false); // Enabling in stream_janus.js
|
||||
tools.feature.setEnabled($("stream-mic"), false); // Ditto
|
||||
}
|
||||
if (wm.isWindowVisible($("stream-window"))) {
|
||||
if (__isStreamRequired()) {
|
||||
__streamer.ensureStream((__state && __state.streamer !== undefined) ? __state.streamer : null);
|
||||
}
|
||||
};
|
||||
@@ -343,19 +364,14 @@ export function Streamer() {
|
||||
};
|
||||
|
||||
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);
|
||||
tools.windowOpen("api/streamer/snapshot");
|
||||
};
|
||||
|
||||
var __clickResetButton = function() {
|
||||
wm.confirm("Are you sure you want to reset stream?").then(function(ok) {
|
||||
wm.confirm("Are you sure you want to reset the stream?").then(function(ok) {
|
||||
if (ok) {
|
||||
__resetStream();
|
||||
tools.httpPost("/api/streamer/reset", null, function(http) {
|
||||
tools.httpPost("api/streamer/reset", null, function(http) {
|
||||
if (http.status !== 200) {
|
||||
wm.error("Can't reset stream", http.responseText);
|
||||
}
|
||||
@@ -440,7 +456,7 @@ export function Streamer() {
|
||||
};
|
||||
|
||||
var __sendParam = function(name, value) {
|
||||
tools.httpPost("/api/streamer/set_params", {[name]: value}, function(http) {
|
||||
tools.httpPost("api/streamer/set_params", {[name]: value}, function(http) {
|
||||
if (http.status !== 200) {
|
||||
wm.error("Can't configure stream", http.responseText);
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ import {tools, $} from "../tools.js";
|
||||
var _Janus = null;
|
||||
|
||||
|
||||
export function JanusStreamer(__setActive, __setInactive, __setInfo, __orient, __allow_audio, __allow_mic) {
|
||||
export function JanusStreamer(__setActive, __setInactive, __setInfo, __organizeHook, __orient, __allow_audio, __allow_mic) {
|
||||
var self = this;
|
||||
|
||||
/************************************************************************/
|
||||
@@ -49,6 +49,10 @@ export function JanusStreamer(__setActive, __setInactive, __setInfo, __orient, _
|
||||
|
||||
var __state = null;
|
||||
var __frames = 0;
|
||||
var __res = {"width": -1, "height": -1};
|
||||
var __resize_listener_installed = false;
|
||||
|
||||
var __ice = null;
|
||||
|
||||
/************************************************************************/
|
||||
|
||||
@@ -83,11 +87,28 @@ export function JanusStreamer(__setActive, __setInactive, __setInfo, __orient, _
|
||||
__state = state;
|
||||
__stop = false;
|
||||
__ensureJanus(false);
|
||||
if (!__resize_listener_installed) {
|
||||
$("stream-video").addEventListener("resize", __videoResizeHandler);
|
||||
__resize_listener_installed = true;
|
||||
}
|
||||
};
|
||||
|
||||
self.stopStream = function() {
|
||||
__stop = true;
|
||||
__destroyJanus();
|
||||
if (__resize_listener_installed) {
|
||||
$("stream-video").removeEventListener("resize", __videoResizeHandler);
|
||||
__resize_listener_installed = false;
|
||||
}
|
||||
};
|
||||
|
||||
var __videoResizeHandler = function(ev) {
|
||||
let el = ev.target;
|
||||
if (__res.width !== el.videoWidth || __res.height !== el.videoHeight) {
|
||||
__res.width = el.videoWidth;
|
||||
__res.height = el.videoHeight;
|
||||
__organizeHook();
|
||||
}
|
||||
};
|
||||
|
||||
var __ensureJanus = function(internal) {
|
||||
@@ -97,9 +118,10 @@ export function JanusStreamer(__setActive, __setInactive, __setInfo, __orient, _
|
||||
__setInfo(false, false, "");
|
||||
__logInfo("Starting Janus ...");
|
||||
__janus = new _Janus({
|
||||
"server": `${tools.is_https ? "wss" : "ws"}://${location.host}/janus/ws`,
|
||||
"server": tools.makeWsUrl("janus/ws"),
|
||||
"ipv6": true,
|
||||
"destroyOnUnload": false,
|
||||
"iceServers": () => __getIceServers(),
|
||||
"success": __attachJanus,
|
||||
"error": function(error) {
|
||||
__logError(error);
|
||||
@@ -110,6 +132,15 @@ export function JanusStreamer(__setActive, __setInactive, __setInfo, __orient, _
|
||||
}
|
||||
};
|
||||
|
||||
var __getIceServers = function() {
|
||||
if (__ice !== null && __ice.url) {
|
||||
__logInfo("Using the custom ICE Server got from uStreamer:", __ice);
|
||||
return [{"urls": __ice.url}];
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
};
|
||||
|
||||
var __finishJanus = function() {
|
||||
if (__stop) {
|
||||
if (__retry_ensure_timeout !== null) {
|
||||
@@ -202,7 +233,8 @@ export function JanusStreamer(__setActive, __setInactive, __setInfo, __orient, _
|
||||
"success": function(handle) {
|
||||
__handle = handle;
|
||||
__logInfo("uStreamer attached:", handle.getPlugin(), handle.getId());
|
||||
__sendWatch();
|
||||
__logInfo("Sending FEATURES ...");
|
||||
__handle.send({"message": {"request": "features"}});
|
||||
},
|
||||
|
||||
"error": function(error) {
|
||||
@@ -233,7 +265,7 @@ export function JanusStreamer(__setActive, __setInactive, __setInfo, __orient, _
|
||||
__stopRetryEmsgInterval();
|
||||
|
||||
if (msg.result) {
|
||||
__logInfo("Got uStreamer result message:", msg.result.status); // starting, started, stopped
|
||||
__logInfo("Got uStreamer result message:", msg.result); // starting, started, stopped
|
||||
if (msg.result.status === "started") {
|
||||
__setActive();
|
||||
__setInfo(false, false, "");
|
||||
@@ -243,6 +275,8 @@ export function JanusStreamer(__setActive, __setInactive, __setInfo, __orient, _
|
||||
} else if (msg.result.status === "features") {
|
||||
tools.feature.setEnabled($("stream-audio"), msg.result.features.audio);
|
||||
tools.feature.setEnabled($("stream-mic"), msg.result.features.mic);
|
||||
__ice = msg.result.features.ice;
|
||||
__sendWatch();
|
||||
}
|
||||
} else if (msg.error_code || msg.error) {
|
||||
__logError("Got uStreamer error message:", msg.error_code, "-", msg.error);
|
||||
@@ -298,7 +332,7 @@ export function JanusStreamer(__setActive, __setInactive, __setInfo, __orient, _
|
||||
// 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".
|
||||
// reason === "mute" and "unmute".
|
||||
let reason = (meta || {}).reason;
|
||||
__logInfo("Got onremotetrack:", id, added, reason, track, meta);
|
||||
if (added && reason === "created") {
|
||||
@@ -412,8 +446,7 @@ export function JanusStreamer(__setActive, __setInactive, __setInfo, __orient, _
|
||||
|
||||
var __sendWatch = function() {
|
||||
if (__handle) {
|
||||
__logInfo(`Sending WATCH(orient=${__orient}, audio=${__allow_audio}, mic=${__allow_mic}) + FEATURES ...`);
|
||||
__handle.send({"message": {"request": "features"}});
|
||||
__logInfo(`Sending WATCH(orient=${__orient}, audio=${__allow_audio}, mic=${__allow_mic}) ...`);
|
||||
__handle.send({"message": {"request": "watch", "params": {
|
||||
"orientation": __orient,
|
||||
"audio": __allow_audio,
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
import {tools, $} from "../tools.js";
|
||||
|
||||
|
||||
export function MediaStreamer(__setActive, __setInactive, __setInfo) {
|
||||
export function MediaStreamer(__setActive, __setInactive, __setInfo, __organizeHook, __orient) {
|
||||
var self = this;
|
||||
|
||||
/************************************************************************/
|
||||
@@ -37,17 +37,20 @@ export function MediaStreamer(__setActive, __setInactive, __setInfo) {
|
||||
var __ws = null;
|
||||
var __ping_timer = null;
|
||||
var __missed_heartbeats = 0;
|
||||
var __decoder = null;
|
||||
|
||||
var __codec = "";
|
||||
var __decoder = null;
|
||||
var __frame = null;
|
||||
var __canvas = $("stream-canvas");
|
||||
var __ctx = __canvas.getContext("2d");
|
||||
|
||||
var __state = null;
|
||||
var __frames = 0;
|
||||
var __fps_accum = 0;
|
||||
|
||||
/************************************************************************/
|
||||
|
||||
self.getName = () => "HTTP H.264";
|
||||
self.getOrientation = () => __orient;
|
||||
self.getName = () => "Direct H.264";
|
||||
self.getMode = () => "media";
|
||||
|
||||
self.getResolution = function() {
|
||||
@@ -79,23 +82,28 @@ export function MediaStreamer(__setActive, __setInactive, __setInfo) {
|
||||
__setInactive();
|
||||
__setInfo(false, false, "");
|
||||
__logInfo("Starting Media ...");
|
||||
__ws = new WebSocket(`${tools.is_https ? "wss" : "ws"}://${location.host}/api/media/ws`);
|
||||
__ws = new WebSocket(tools.makeWsUrl("api/media/ws"));
|
||||
__ws.binaryType = "arraybuffer";
|
||||
__ws.onopen = __wsOpenHandler;
|
||||
__ws.onerror = __wsErrorHandler;
|
||||
__ws.onclose = __wsCloseHandler;
|
||||
__ws.onmessage = async (event) => {
|
||||
if (typeof event.data === "string") {
|
||||
__wsJsonHandler(JSON.parse(event.data));
|
||||
} else { // Binary
|
||||
await __wsBinHandler(event.data);
|
||||
__ws.onmessage = async (ev) => {
|
||||
try {
|
||||
if (typeof ev.data === "string") {
|
||||
ev = JSON.parse(ev.data);
|
||||
__wsJsonHandler(ev.event_type, ev.event);
|
||||
} else { // Binary
|
||||
await __wsBinHandler(ev.data);
|
||||
}
|
||||
} catch (ex) {
|
||||
__wsErrorHandler(ex);
|
||||
}
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
var __wsOpenHandler = function(event) {
|
||||
__logInfo("Socket opened:", event);
|
||||
var __wsOpenHandler = function(ev) {
|
||||
__logInfo("Socket opened:", ev);
|
||||
__missed_heartbeats = 0;
|
||||
__ping_timer = setInterval(__ping, 1000);
|
||||
};
|
||||
@@ -110,8 +118,8 @@ export function MediaStreamer(__setActive, __setInactive, __setInfo) {
|
||||
|
||||
if (__decoder && __decoder.state === "configured") {
|
||||
let online = !!(__state && __state.source.online);
|
||||
let info = `${__frames} fps dynamic`;
|
||||
__frames = 0;
|
||||
let info = `${__fps_accum} fps dynamic`;
|
||||
__fps_accum = 0;
|
||||
__setInfo(true, online, info);
|
||||
}
|
||||
} catch (ex) {
|
||||
@@ -128,64 +136,35 @@ export function MediaStreamer(__setActive, __setInactive, __setInfo) {
|
||||
__setInactive();
|
||||
};
|
||||
|
||||
var __wsErrorHandler = function(event) {
|
||||
__logInfo("Socket error:", event);
|
||||
__setInfo(false, false, event);
|
||||
var __wsErrorHandler = function(ev) {
|
||||
__logInfo("Socket error:", ev);
|
||||
__setInfo(false, false, ev);
|
||||
__wsForceClose();
|
||||
};
|
||||
|
||||
var __wsCloseHandler = function(event) {
|
||||
__logInfo("Socket closed:", event);
|
||||
var __wsCloseHandler = function(ev) {
|
||||
__logInfo("Socket closed:", ev);
|
||||
if (__ping_timer) {
|
||||
clearInterval(__ping_timer);
|
||||
__ping_timer = null;
|
||||
}
|
||||
if (__decoder) {
|
||||
__decoder.close();
|
||||
__decoder = null;
|
||||
}
|
||||
__closeDecoder();
|
||||
__missed_heartbeats = 0;
|
||||
__frames = 0;
|
||||
__fps_accum = 0;
|
||||
__ws = null;
|
||||
if (!__stop) {
|
||||
setTimeout(() => __ensureMedia(true), 1000);
|
||||
}
|
||||
};
|
||||
|
||||
var __wsJsonHandler = function(event) {
|
||||
if (event.event_type === "media") {
|
||||
__decoderCreate(event.event.video);
|
||||
var __wsJsonHandler = function(ev_type, ev) {
|
||||
if (ev_type === "media") {
|
||||
__setupCodec(ev.video);
|
||||
}
|
||||
};
|
||||
|
||||
var __wsBinHandler = async (data) => {
|
||||
let header = new Uint8Array(data.slice(0, 2));
|
||||
|
||||
if (header[0] === 255) { // Pong
|
||||
__missed_heartbeats = 0;
|
||||
|
||||
} else if (header[0] === 1 && __decoder !== null) { // Video frame
|
||||
let key = !!header[1];
|
||||
if (__decoder.state !== "configured") {
|
||||
if (!key) {
|
||||
return;
|
||||
}
|
||||
await __decoder.configure({"codec": __codec, "optimizeForLatency": true});
|
||||
__setActive();
|
||||
}
|
||||
|
||||
let chunk = new EncodedVideoChunk({ // eslint-disable-line no-undef
|
||||
"timestamp": (performance.now() + performance.timeOrigin) * 1000,
|
||||
"type": (key ? "key" : "delta"),
|
||||
"data": data.slice(2),
|
||||
});
|
||||
await __decoder.decode(chunk);
|
||||
}
|
||||
};
|
||||
|
||||
var __decoderCreate = function(formats) {
|
||||
__decoderDestroy();
|
||||
|
||||
var __setupCodec = function(formats) {
|
||||
__closeDecoder();
|
||||
if (formats.h264 === undefined) {
|
||||
let msg = "No H.264 stream available on PiKVM";
|
||||
__setInfo(false, false, msg);
|
||||
@@ -201,35 +180,144 @@ export function MediaStreamer(__setActive, __setInactive, __setInfo) {
|
||||
__logInfo(msg);
|
||||
return;
|
||||
}
|
||||
|
||||
__decoder = new VideoDecoder({ // eslint-disable-line no-undef
|
||||
"output": (frame) => {
|
||||
try {
|
||||
if (__canvas.width !== frame.displayWidth || __canvas.height !== frame.displayHeight) {
|
||||
__canvas.width = frame.displayWidth;
|
||||
__canvas.height = frame.displayHeight;
|
||||
}
|
||||
__ctx.drawImage(frame, 0, 0);
|
||||
__frames += 1;
|
||||
} finally {
|
||||
frame.close();
|
||||
}
|
||||
},
|
||||
"error": (err) => __logInfo(err.message),
|
||||
});
|
||||
__codec = `avc1.${formats.h264.profile_level_id}`;
|
||||
|
||||
__ws.send(JSON.stringify({
|
||||
"event_type": "start",
|
||||
"event": {"type": "video", "format": "h264"},
|
||||
}));
|
||||
};
|
||||
|
||||
var __decoderDestroy = function() {
|
||||
var __wsBinHandler = async (data) => {
|
||||
let header = new Uint8Array(data.slice(0, 2));
|
||||
if (header[0] === 255) { // Pong
|
||||
__missed_heartbeats = 0;
|
||||
} else if (header[0] === 1) { // Video frame
|
||||
let key = !!header[1];
|
||||
if (await __ensureDecoder(key)) {
|
||||
await __processFrame(key, data.slice(2));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var __ensureDecoder = async (key) => {
|
||||
if (__codec === "") {
|
||||
return false;
|
||||
}
|
||||
if (__decoder === null || __decoder.state === "closed") {
|
||||
let started = (__codec !== "");
|
||||
let codec = __codec;
|
||||
__closeDecoder();
|
||||
__codec = codec;
|
||||
__decoder = new VideoDecoder({ // eslint-disable-line no-undef
|
||||
"output": __renderFrame,
|
||||
"error": (err) => __logInfo(err.message),
|
||||
});
|
||||
if (started) {
|
||||
__ws.send(new Uint8Array([0]));
|
||||
}
|
||||
}
|
||||
if (__decoder.state !== "configured") {
|
||||
if (!key) {
|
||||
return false;
|
||||
}
|
||||
await __decoder.configure({"codec": __codec, "optimizeForLatency": true});
|
||||
}
|
||||
if (__decoder.state === "configured") {
|
||||
__setActive();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
var __processFrame = async (key, raw) => {
|
||||
let chunk = new EncodedVideoChunk({ // eslint-disable-line no-undef
|
||||
"timestamp": (performance.now() + performance.timeOrigin) * 1000,
|
||||
"type": (key ? "key" : "delta"),
|
||||
"data": raw,
|
||||
});
|
||||
await __decoder.decode(chunk);
|
||||
};
|
||||
|
||||
var __closeDecoder = function() {
|
||||
if (__decoder !== null) {
|
||||
__decoder.close();
|
||||
__decoder = null;
|
||||
__codec = "";
|
||||
try {
|
||||
__decoder.close();
|
||||
} finally {
|
||||
__codec = "";
|
||||
__decoder = null;
|
||||
if (__frame !== null) {
|
||||
try {
|
||||
__closeFrame(__frame);
|
||||
} finally {
|
||||
__frame = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
var __renderFrame = function(frame) {
|
||||
if (__frame === null) {
|
||||
__frame = frame;
|
||||
window.requestAnimationFrame(__drawPendingFrame, __canvas);
|
||||
} else {
|
||||
__closeFrame(frame);
|
||||
}
|
||||
};
|
||||
|
||||
var __drawPendingFrame = function() {
|
||||
if (__frame === null) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
let width = __frame.displayWidth;
|
||||
let height = __frame.displayHeight;
|
||||
switch (__orient) {
|
||||
case 90:
|
||||
case 270:
|
||||
width = __frame.displayHeight;
|
||||
height = __frame.displayWidth;
|
||||
}
|
||||
|
||||
if (__canvas.width !== width || __canvas.height !== height) {
|
||||
__canvas.width = width;
|
||||
__canvas.height = height;
|
||||
__organizeHook();
|
||||
}
|
||||
|
||||
if (__orient === 0) {
|
||||
__ctx.drawImage(__frame, 0, 0);
|
||||
} else {
|
||||
__ctx.save();
|
||||
try {
|
||||
switch(__orient) {
|
||||
case 90: __ctx.translate(0, height); __ctx.rotate(-Math.PI / 2); break;
|
||||
case 180: __ctx.translate(width, height); __ctx.rotate(-Math.PI); break;
|
||||
case 270: __ctx.translate(width, 0); __ctx.rotate(Math.PI / 2); break;
|
||||
}
|
||||
__ctx.drawImage(__frame, 0, 0);
|
||||
} finally {
|
||||
__ctx.restore();
|
||||
}
|
||||
}
|
||||
__fps_accum += 1;
|
||||
} finally {
|
||||
__closeFrame(__frame);
|
||||
__frame = null;
|
||||
}
|
||||
};
|
||||
|
||||
var __closeFrame = function(frame) {
|
||||
if (!tools.browser.is_firefox) {
|
||||
// FIXME: On Firefox, image is flickering when we're closing the frame for some reason.
|
||||
// So we're just not performing the close() and it seems there is no problems here
|
||||
// because Firefox is implementing some auto-closing logic. With auto-close,
|
||||
// no flickering observed.
|
||||
// - https://github.com/mozilla/gecko-dev/blob/82333a9/dom/media/webcodecs/VideoFrame.cpp
|
||||
// Note at 2025.05.13:
|
||||
// - The problem is not observed on nightly Firefox 140.
|
||||
// - It's also not observed with hardware accelleration on 138.
|
||||
frame.close();
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -23,15 +23,16 @@
|
||||
"use strict";
|
||||
|
||||
|
||||
import {ROOT_PREFIX} from "../vars.js";
|
||||
import {tools, $} from "../tools.js";
|
||||
|
||||
|
||||
export function MjpegStreamer(__setActive, __setInactive, __setInfo) {
|
||||
export function MjpegStreamer(__setActive, __setInactive, __setInfo, __organizeHook) {
|
||||
var self = this;
|
||||
|
||||
/************************************************************************/
|
||||
|
||||
var __key = tools.makeId();
|
||||
var __key = tools.makeRandomId();
|
||||
var __id = "";
|
||||
var __fps = -1;
|
||||
var __state = null;
|
||||
@@ -61,6 +62,7 @@ export function MjpegStreamer(__setActive, __setInactive, __setInfo) {
|
||||
if (__id.length > 0 && __id in __state.stream.clients_stat) {
|
||||
__setStreamActive();
|
||||
__stopChecking();
|
||||
__organizeHook();
|
||||
} else {
|
||||
__ensureChecking();
|
||||
}
|
||||
@@ -72,7 +74,7 @@ export function MjpegStreamer(__setActive, __setInactive, __setInfo) {
|
||||
|
||||
self.stopStream = function() {
|
||||
self.ensureStream(null);
|
||||
let blank = "/share/png/blank-stream.png";
|
||||
let blank = `${ROOT_PREFIX}share/png/blank-stream.png`;
|
||||
if (!String.prototype.endsWith.call($("stream-image").src, blank)) {
|
||||
$("stream-image").src = blank;
|
||||
}
|
||||
@@ -90,7 +92,7 @@ export function MjpegStreamer(__setActive, __setInactive, __setInfo) {
|
||||
|
||||
var __setStreamInactive = function() {
|
||||
let old_fps = __fps;
|
||||
__key = tools.makeId();
|
||||
__key = tools.makeRandomId();
|
||||
__id = "";
|
||||
__fps = -1;
|
||||
__state = null;
|
||||
@@ -127,7 +129,7 @@ export function MjpegStreamer(__setActive, __setInactive, __setInfo) {
|
||||
var __checkStream = function() {
|
||||
__findId();
|
||||
|
||||
if (__id.legnth > 0 && __id in __state.stream.clients_stat) {
|
||||
if (__id.length > 0 && __id in __state.stream.clients_stat) {
|
||||
__setStreamActive();
|
||||
__stopChecking();
|
||||
|
||||
@@ -138,7 +140,7 @@ export function MjpegStreamer(__setActive, __setInactive, __setInfo) {
|
||||
__setStreamInactive();
|
||||
__stopChecking();
|
||||
|
||||
let path = `/streamer/stream?key=${__key}`;
|
||||
let path = `${ROOT_PREFIX}streamer/stream?key=${encodeURIComponent(__key)}`;
|
||||
if (tools.browser.is_safari || tools.browser.is_ios) {
|
||||
// uStreamer fix for WebKit
|
||||
__logInfo("Using dual_final_frames=1 to fix WebKit bugs");
|
||||
|
||||
@@ -23,8 +23,10 @@
|
||||
"use strict";
|
||||
|
||||
|
||||
import {ROOT_PREFIX} from "../vars.js";
|
||||
import {tools, $} from "../tools.js";
|
||||
import {wm} from "../wm.js";
|
||||
import {clipboard} from "./clipboard.js";
|
||||
|
||||
|
||||
export function Switch() {
|
||||
@@ -44,6 +46,7 @@ export function Switch() {
|
||||
tools.el.setOnClick($("switch-edid-copy-data-button"), __clickCopyEdidDataButton);
|
||||
|
||||
tools.storage.bindSimpleSwitch($("switch-atx-ask-switch"), "switch.atx.ask", true);
|
||||
tools.storage.bindSimpleSwitch($("switch-msd-ask-switch"), "switch.msd.ask", true);
|
||||
|
||||
for (let role of ["inactive", "active", "flashing", "beacon", "bootloader"]) {
|
||||
let el_brightness = $(`switch-color-${role}-brightness-slider`);
|
||||
@@ -125,7 +128,7 @@ export function Switch() {
|
||||
+ ":" + brightness.toString(16).padStart(2, "0")
|
||||
+ ":" + color.blink_ms.toString(16).padStart(4, "0")
|
||||
);
|
||||
__sendPost("/api/switch/set_colors", {[role]: rgbx}, function() {
|
||||
__sendPost("api/switch/set_colors", {[role]: rgbx}, function() {
|
||||
el_color.value = (
|
||||
"#"
|
||||
+ color.red.toString(16).padStart(2, "0")
|
||||
@@ -137,7 +140,7 @@ export function Switch() {
|
||||
};
|
||||
|
||||
var __clickSetDefaultColorButton = function(role) {
|
||||
__sendPost("/api/switch/set_colors", {[role]: "default"});
|
||||
__sendPost("api/switch/set_colors", {[role]: "default"});
|
||||
};
|
||||
|
||||
var __applyEdids = function(edids) {
|
||||
@@ -177,8 +180,8 @@ export function Switch() {
|
||||
};
|
||||
|
||||
var __clickAddEdidButton = function() {
|
||||
let create_content = function(el_parent, el_ok_button) {
|
||||
tools.el.setEnabled(el_ok_button, false);
|
||||
let create_content = function(el_parent, el_ok_bt) {
|
||||
tools.el.setEnabled(el_ok_bt, false);
|
||||
el_parent.innerHTML = `
|
||||
<table>
|
||||
<tr>
|
||||
@@ -202,7 +205,7 @@ export function Switch() {
|
||||
el_name.oninput = el_data.oninput = function() {
|
||||
let name = el_name.value.replace(/\s+/g, "");
|
||||
let data = el_data.value.replace(/\s+/g, "");
|
||||
tools.el.setEnabled(el_ok_button, ((name.length > 0) && /[0-9a-fA-F]{512}/.test(data)));
|
||||
tools.el.setEnabled(el_ok_bt, ((name.length > 0) && /[0-9a-fA-F]{512}/.test(data)));
|
||||
};
|
||||
};
|
||||
|
||||
@@ -210,7 +213,7 @@ export function Switch() {
|
||||
if (ok) {
|
||||
let name = $("__switch-edid-new-name-input").value;
|
||||
let data = $("__switch-edid-new-data-text").value;
|
||||
__sendPost("/api/switch/edids/create", {"name": name, "data": data});
|
||||
__sendPost("api/switch/edids/create", {"name": name, "data": data});
|
||||
}
|
||||
});
|
||||
};
|
||||
@@ -222,7 +225,7 @@ export function Switch() {
|
||||
let html = "Are you sure to remove this EDID?<br>Ports that used it will change it to the default.";
|
||||
wm.confirm(html, name).then(function(ok) {
|
||||
if (ok) {
|
||||
__sendPost("/api/switch/edids/remove", {"id": edid_id});
|
||||
__sendPost("api/switch/edids/remove", {"id": edid_id});
|
||||
}
|
||||
});
|
||||
}
|
||||
@@ -233,7 +236,7 @@ export function Switch() {
|
||||
if (edid_id && __state && __state.edids) {
|
||||
let data = __state.edids.all[edid_id].data;
|
||||
data = data.replace(/(.{32})/g, "$1\n");
|
||||
wm.copyTextToClipboard(data);
|
||||
clipboard.setText(data);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -305,7 +308,7 @@ export function Switch() {
|
||||
if (active < 0 || active >= __state.model.ports.length) {
|
||||
$("switch-active-port").innerText = "N/A";
|
||||
} else {
|
||||
$("switch-active-port").innerText = "p" + __formatPort(__state.model, active);
|
||||
$("switch-active-port").innerText = "p" + summary.active_id;
|
||||
}
|
||||
for (let port = 0; port < __state.model.ports.length; ++port) {
|
||||
__setLedState($(`__switch-port-led-p${port}`), "green", (port === active));
|
||||
@@ -333,7 +336,7 @@ export function Switch() {
|
||||
let content = "";
|
||||
let unit = -1;
|
||||
for (let port = 0; port < model.ports.length; ++port) {
|
||||
let pa = model.ports[port]; // pa == port attrs
|
||||
let pa = model.ports[port]; // pa === port attrs
|
||||
if (unit !== pa.unit) {
|
||||
unit = pa.unit;
|
||||
content += `${unit > 0 ? "<tr><td colspan=100><hr></td></tr>" : ""}
|
||||
@@ -344,11 +347,11 @@ export function Switch() {
|
||||
<td colspan=100>
|
||||
<div class="buttons-row">
|
||||
<button id="__switch-beacon-button-u${unit}" class="small" title="Toggle uplink Beacon Led">
|
||||
<img id="__switch-beacon-led-u${unit}" class="inline-lamp led-gray" src="/share/svg/led-beacon.svg"/>
|
||||
<img id="__switch-beacon-led-u${unit}" class="inline-lamp led-gray" src="${ROOT_PREFIX}share/svg/led-beacon.svg"/>
|
||||
Uplink
|
||||
</button>
|
||||
<button id="__switch-beacon-button-d${unit}" class="small" title="Toggle downlink Beacon Led">
|
||||
<img id="__switch-beacon-led-d${unit}" class="inline-lamp led-gray" src="/share/svg/led-beacon.svg"/>
|
||||
<img id="__switch-beacon-led-d${unit}" class="inline-lamp led-gray" src="${ROOT_PREFIX}share/svg/led-beacon.svg"/>
|
||||
Downlink
|
||||
</button>
|
||||
</div>
|
||||
@@ -360,15 +363,15 @@ export function Switch() {
|
||||
content += `
|
||||
<tr>
|
||||
<td>Port:</td>
|
||||
<td class="value">${__formatPort(model, port)}</td>
|
||||
<td class="value">${pa.id}</td>
|
||||
<td> </td>
|
||||
<td>
|
||||
<div class="buttons-row">
|
||||
<button id="__switch-port-button-p${port}" title="Activate this port">
|
||||
<img id="__switch-port-led-p${port}" class="inline-lamp led-gray" src="/share/svg/led-circle.svg"/>
|
||||
<img id="__switch-port-led-p${port}" class="inline-lamp led-gray" src="${ROOT_PREFIX}share/svg/led-circle.svg"/>
|
||||
</button>
|
||||
<button id="__switch-params-button-p${port}" title="Configure this port">
|
||||
<img id="__switch-params-led-p${port}" class="inline-lamp led-gray" src="/share/svg/led-gear.svg"/>
|
||||
<img id="__switch-params-led-p${port}" class="inline-lamp led-gray" src="${ROOT_PREFIX}share/svg/led-gear.svg"/>
|
||||
</button>
|
||||
</div>
|
||||
</td>
|
||||
@@ -385,14 +388,14 @@ export function Switch() {
|
||||
</td>
|
||||
<td style="font-size:1em">
|
||||
<button id="__switch-beacon-button-p${port}" class="small" title="Toggle Beacon Led on this port">
|
||||
<img id="__switch-beacon-led-p${port}" class="inline-lamp led-gray" src="/share/svg/led-beacon.svg"/>
|
||||
<img id="__switch-beacon-led-p${port}" class="inline-lamp led-gray" src="${ROOT_PREFIX}share/svg/led-beacon.svg"/>
|
||||
</button>
|
||||
</td>
|
||||
<td>
|
||||
<img id="__switch-video-led-p${port}" class="inline-lamp led-gray" src="/share/svg/led-video.svg" title="Video Link"/>
|
||||
<img id="__switch-usb-led-p${port}" class="inline-lamp led-gray" src="/share/svg/led-usb.svg" title="USB Link"/>
|
||||
<img id="__switch-atx-power-led-p${port}" class="inline-lamp led-gray" src="/share/svg/led-atx-power.svg" title="Power Led"/>
|
||||
<img id="__switch-atx-hdd-led-p${port}" class="inline-lamp led-gray" src="/share/svg/led-atx-hdd.svg" title="HDD Led"/>
|
||||
<img id="__switch-video-led-p${port}" class="inline-lamp led-gray" src="${ROOT_PREFIX}share/svg/led-video.svg" title="Video Link"/>
|
||||
<img id="__switch-usb-led-p${port}" class="inline-lamp led-gray" src="${ROOT_PREFIX}share/svg/led-usb.svg" title="USB Link"/>
|
||||
<img id="__switch-atx-power-led-p${port}" class="inline-lamp led-gray" src="${ROOT_PREFIX}share/svg/led-atx-power.svg" title="Power Led"/>
|
||||
<img id="__switch-atx-hdd-led-p${port}" class="inline-lamp led-gray" src="${ROOT_PREFIX}share/svg/led-atx-hdd.svg" title="HDD Led"/>
|
||||
</td>
|
||||
<td>
|
||||
<div class="buttons-row">
|
||||
@@ -407,7 +410,8 @@ export function Switch() {
|
||||
$("switch-chain").innerHTML = content;
|
||||
|
||||
if (model.units.length > 0) {
|
||||
tools.hidden.setVisible($("switch-message-update"), (model.firmware.version > model.units[0].firmware.version));
|
||||
let fw = model.units[0].firmware;
|
||||
tools.hidden.setVisible($("switch-message-update"), (fw.devbuild || fw.version < model.firmware.version));
|
||||
}
|
||||
|
||||
for (let unit = 0; unit < model.units.length; ++unit) {
|
||||
@@ -437,6 +441,7 @@ export function Switch() {
|
||||
|
||||
let model = __state.model;
|
||||
let edids = __state.edids;
|
||||
let pa = model.ports[port]; // Port attrs
|
||||
|
||||
let atx_actions = {
|
||||
"power": "ATX power click",
|
||||
@@ -457,12 +462,12 @@ export function Switch() {
|
||||
|
||||
let create_content = function(el_parent) {
|
||||
let html = `
|
||||
<table>
|
||||
<table style="width: 100%">
|
||||
<tr>
|
||||
<td>Port name:</td>
|
||||
<td><input
|
||||
type="text" autocomplete="off" id="__switch-port-name-input"
|
||||
value="${tools.escape(model.ports[port].name)}" placeholder="Host ${port + 1}"
|
||||
value="${tools.escape(pa.name)}" placeholder="Host ${port + 1}"
|
||||
style="width:100%"
|
||||
/></td>
|
||||
</tr>
|
||||
@@ -471,9 +476,22 @@ export function Switch() {
|
||||
<td><select id="__switch-port-edid-selector" style="width: 100%"></select></td>
|
||||
</tr>
|
||||
</table>
|
||||
<hr>
|
||||
<table>
|
||||
`;
|
||||
|
||||
let fw = model.units[pa.unit].firmware;
|
||||
if (fw.devbuild || fw.version >= 8) {
|
||||
html += `
|
||||
<hr>
|
||||
<table style="width: 100%">
|
||||
<tr>
|
||||
<td>Simulate display on inactive port:</td>
|
||||
<td align="right">${tools.sw.makeItem("__switch-port-dummy-switch", pa.video.dummy)}</td>
|
||||
</tr>
|
||||
</table>
|
||||
`;
|
||||
}
|
||||
|
||||
html += "<hr><table style=\"width: 100%\">";
|
||||
for (let kv of Object.entries(atx_actions)) {
|
||||
html += `
|
||||
<tr>
|
||||
@@ -489,6 +507,7 @@ export function Switch() {
|
||||
`;
|
||||
}
|
||||
html += "</table>";
|
||||
|
||||
el_parent.innerHTML = html;
|
||||
|
||||
let el_selector = $("__switch-port-edid-selector");
|
||||
@@ -509,34 +528,30 @@ export function Switch() {
|
||||
let reset_default = tools.partial(function(el_slider, limits) {
|
||||
tools.slider.setValue(el_slider, limits["default"]);
|
||||
}, el_slider, limits);
|
||||
tools.slider.setParams(el_slider, limits.min, limits.max, 0.5, model.ports[port].atx.click_delays[action], display_value);
|
||||
tools.slider.setParams(el_slider, limits.min, limits.max, 0.5, pa.atx.click_delays[action], display_value);
|
||||
tools.el.setOnClick($(`__switch-port-atx-click-${action}-delay-default-button`), reset_default);
|
||||
}
|
||||
};
|
||||
|
||||
wm.modal(`Port ${__formatPort(__state.model, port)} settings`, create_content, true, true).then(function(ok) {
|
||||
wm.modal(`Port ${pa.id} settings`, create_content, true, true).then(function(ok) {
|
||||
if (ok) {
|
||||
let params = {
|
||||
"port": port,
|
||||
"edid_id": $("__switch-port-edid-selector").value,
|
||||
"name": $("__switch-port-name-input").value,
|
||||
};
|
||||
let el_dummy_switch = $("__switch-port-dummy-switch");
|
||||
if (el_dummy_switch) { // Only for devbuild or firmware >= 8
|
||||
params["dummy"] = $("__switch-port-dummy-switch").checked;
|
||||
}
|
||||
for (let action of Object.keys(atx_actions)) {
|
||||
params[`atx_click_${action}_delay`] = tools.slider.getValue($(`__switch-port-atx-click-${action}-delay-slider`));
|
||||
};
|
||||
__sendPost("/api/switch/set_port_params", params);
|
||||
__sendPost("api/switch/set_port_params", params);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
var __formatPort = function(model, port) {
|
||||
if (model.units.length > 1) {
|
||||
return `${model.ports[port].unit + 1}.${model.ports[port].channel + 1}`;
|
||||
} else {
|
||||
return `${port + 1}`;
|
||||
}
|
||||
};
|
||||
|
||||
var __setLedState = function(el, color, on) {
|
||||
el.classList.toggle(`led-${color}`, on);
|
||||
el.classList.toggle("led-gray", !on);
|
||||
@@ -549,41 +564,50 @@ export function Switch() {
|
||||
};
|
||||
|
||||
var __switchActivePort = function(port) {
|
||||
let switch_port = () => __sendPost("api/switch/set_active", {"port": port});
|
||||
if (__msd_connected) {
|
||||
wm.error(`
|
||||
Oops! Before port switching, please disconnect an active Mass Storage Drive image first.
|
||||
Otherwise, it will break a current USB operation (OS installation, Live CD, or whatever).
|
||||
`);
|
||||
if ($("switch-msd-ask-switch").checked) {
|
||||
wm.confirm(`
|
||||
The Mass Storage Drive is active.<br><br>
|
||||
If you switch the port now, it will break a current USB disk operation<br>
|
||||
(OS installation, Live CD, or whatever).<br><br>
|
||||
Are you sure you want to continue a port switching?
|
||||
`).then(function(ok) {
|
||||
if (ok) {
|
||||
switch_port();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
switch_port();
|
||||
}
|
||||
} else {
|
||||
__sendPost("/api/switch/set_active", {"port": port});
|
||||
switch_port();
|
||||
}
|
||||
};
|
||||
|
||||
var __switchUplinkBeacon = function(unit) {
|
||||
let state = false;
|
||||
try { state = !__state.beacons.uplinks[unit]; } catch {}; // eslint-disable-line no-empty
|
||||
__sendPost("/api/switch/set_beacon", {"uplink": unit, "state": state});
|
||||
__sendPost("api/switch/set_beacon", {"uplink": unit, "state": state});
|
||||
};
|
||||
|
||||
var __switchDownlinkBeacon = function(unit) {
|
||||
let state = false;
|
||||
try { state = !__state.beacons.downlinks[unit]; } catch {}; // eslint-disable-line no-empty
|
||||
__sendPost("/api/switch/set_beacon", {"downlink": unit, "state": state});
|
||||
__sendPost("api/switch/set_beacon", {"downlink": unit, "state": state});
|
||||
};
|
||||
|
||||
var __switchPortBeacon = function(port) {
|
||||
let state = false;
|
||||
try { state = !__state.beacons.ports[port]; } catch {}; // eslint-disable-line no-empty
|
||||
__sendPost("/api/switch/set_beacon", {"port": port, "state": state});
|
||||
__sendPost("api/switch/set_beacon", {"port": port, "state": state});
|
||||
};
|
||||
|
||||
var __atxClick = function(port, button) {
|
||||
let click_button = function() {
|
||||
__sendPost("/api/switch/atx/click", {"port": port, "button": button});
|
||||
};
|
||||
let click_button = () => __sendPost("api/switch/atx/click", {"port": port, "button": button});
|
||||
if ($("switch-atx-ask-switch").checked) {
|
||||
wm.confirm(`
|
||||
Are you sure you want to press the <b>${button}</b> button?<br>
|
||||
Are you sure you want to press the <b>${tools.escape(button)}</b> button?<br>
|
||||
Warning! This could cause data loss on the server.
|
||||
`).then(function(ok) {
|
||||
if (ok) {
|
||||
|
||||
@@ -32,10 +32,17 @@ export function main() {
|
||||
if (checkBrowser(null, null)) {
|
||||
initWindowManager();
|
||||
|
||||
// Radio is a string container
|
||||
tools.radio.clickValue("expire-radio", tools.storage.get("login.expire", 0));
|
||||
tools.radio.setOnClick("expire-radio", function() {
|
||||
let expire = parseInt(tools.radio.getValue("expire-radio"));
|
||||
tools.storage.setInt("login.expire", expire);
|
||||
}, false);
|
||||
|
||||
tools.el.setOnClick($("login-button"), __login);
|
||||
$("user-input").onkeyup = $("passwd-input").onkeyup = $("code-input").onkeyup = function(event) {
|
||||
if (event.code === "Enter") {
|
||||
event.preventDefault();
|
||||
$("user-input").onkeyup = $("passwd-input").onkeyup = $("code-input").onkeyup = function(ev) {
|
||||
if (ev.code === "Enter") {
|
||||
ev.preventDefault();
|
||||
$("login-button").click();
|
||||
}
|
||||
};
|
||||
@@ -45,31 +52,43 @@ export function main() {
|
||||
}
|
||||
|
||||
function __login() {
|
||||
let user = $("user-input").value;
|
||||
if (user.length === 0) {
|
||||
let e_user = encodeURIComponent($("user-input").value);
|
||||
if (e_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 {
|
||||
return;
|
||||
}
|
||||
|
||||
let e_passwd = encodeURIComponent($("passwd-input").value + $("code-input").value);
|
||||
let e_expire = encodeURIComponent(tools.radio.getValue("expire-radio"));
|
||||
let body = `user=${e_user}&passwd=${e_passwd}&expire=${e_expire}`;
|
||||
|
||||
tools.httpPost("api/auth/login", null, function(http) {
|
||||
switch (http.status) {
|
||||
case 200:
|
||||
tools.currentOpen("");
|
||||
break;
|
||||
|
||||
case 403:
|
||||
wm.error("Invalid username, password, or OTP").then(__tryAgain);
|
||||
break;
|
||||
|
||||
default: {
|
||||
let error = "";
|
||||
if (http.status === 400) {
|
||||
try { error = JSON.parse(http.responseText)["result"]["error"]; } catch { /* Nah */ }
|
||||
try {
|
||||
error = JSON.parse(http.responseText)["result"]["error"];
|
||||
} catch { /* Nah */ }
|
||||
}
|
||||
if (error === "ValidatorError") {
|
||||
wm.error("Invalid characters in credentials").then(__tryAgain);
|
||||
} else {
|
||||
wm.error("Login error", http.responseText).then(__tryAgain);
|
||||
wm.error("Unexpected login error:", http.responseText).then(__tryAgain);
|
||||
}
|
||||
}
|
||||
}, body, "application/x-www-form-urlencoded");
|
||||
__setEnabled(false);
|
||||
}
|
||||
} break;
|
||||
}
|
||||
}, body, "application/x-www-form-urlencoded");
|
||||
|
||||
__setEnabled(false);
|
||||
}
|
||||
|
||||
function __setEnabled(enabled) {
|
||||
|
||||
@@ -23,6 +23,7 @@
|
||||
"use strict";
|
||||
|
||||
|
||||
import {ROOT_PREFIX} from "./vars.js";
|
||||
import {browser} from "./bb.js";
|
||||
|
||||
|
||||
@@ -39,7 +40,16 @@ export var tools = new function() {
|
||||
|
||||
/************************************************************************/
|
||||
|
||||
self.currentOpen = function(url) {
|
||||
window.location.href = ROOT_PREFIX + url;
|
||||
};
|
||||
|
||||
self.windowOpen = function(url) {
|
||||
window.open(ROOT_PREFIX + url, "_blank");
|
||||
};
|
||||
|
||||
self.httpRequest = function(method, url, params, callback, body=null, content_type=null, timeout=15000) {
|
||||
url = ROOT_PREFIX + url;
|
||||
if (params) {
|
||||
params = new URLSearchParams(params);
|
||||
if (params) {
|
||||
@@ -68,11 +78,19 @@ export var tools = new function() {
|
||||
self.httpRequest("POST", url, params, callback, body, content_type, timeout);
|
||||
};
|
||||
|
||||
self.makeWsUrl = function(url) {
|
||||
let proto = (self.is_https ? "wss://" : "ws://");
|
||||
return proto + window.location.host + window.location.pathname + ROOT_PREFIX + url;
|
||||
};
|
||||
|
||||
/************************************************************************/
|
||||
|
||||
self.escape = function(text) {
|
||||
if (typeof text !== "string") {
|
||||
text = "" + text;
|
||||
}
|
||||
return text.replace(
|
||||
/[^0-9A-Za-z ]/g,
|
||||
/[^-_0-9A-Za-z ]/g,
|
||||
ch => "&#" + ch.charCodeAt(0) + ";"
|
||||
);
|
||||
};
|
||||
@@ -85,7 +103,7 @@ export var tools = new function() {
|
||||
return text[0].toUpperCase() + text.slice(1);
|
||||
};
|
||||
|
||||
self.makeId = function() {
|
||||
self.makeRandomId = function() {
|
||||
let chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||
let id = "";
|
||||
for (let count = 0; count < 16; ++count) {
|
||||
@@ -94,16 +112,10 @@ export var tools = new function() {
|
||||
return id;
|
||||
};
|
||||
|
||||
self.makeIdByText = function(text) {
|
||||
self.makeTextId = 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.formatSize = function(size) {
|
||||
if (size > 0) {
|
||||
let index = Math.floor( Math.log(size) / Math.log(1024) );
|
||||
@@ -124,14 +136,15 @@ export var tools = new function() {
|
||||
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.remap = function(value, in_min, in_max, out_min, out_max) {
|
||||
let result = Math.round((value - in_min) * (out_max - out_min) / ((in_max - in_min) || 1) + out_min);
|
||||
return Math.min(Math.max(result, out_min), out_max);
|
||||
};
|
||||
|
||||
self.getRandomInt = function(min, max) {
|
||||
min = Math.ceil(min);
|
||||
max = Math.floor(max);
|
||||
return Math.floor(Math.random() * (max - min + 1)) + min;
|
||||
};
|
||||
|
||||
/************************************************************************/
|
||||
@@ -139,25 +152,25 @@ export var tools = new function() {
|
||||
self.el = new function() {
|
||||
return {
|
||||
"setOnClick": function(el, callback, prevent_default=true) {
|
||||
el.onclick = el.ontouchend = function(event) {
|
||||
el.onclick = el.ontouchend = function(ev) {
|
||||
if (prevent_default) {
|
||||
event.preventDefault();
|
||||
ev.preventDefault();
|
||||
}
|
||||
callback();
|
||||
};
|
||||
},
|
||||
"setOnDown": function(el, callback, prevent_default=true) {
|
||||
el.onmousedown = el.ontouchstart = function(event) {
|
||||
el.onmousedown = el.ontouchstart = function(ev) {
|
||||
if (prevent_default) {
|
||||
event.preventDefault();
|
||||
ev.preventDefault();
|
||||
}
|
||||
callback();
|
||||
callback(ev);
|
||||
};
|
||||
},
|
||||
"setOnUp": function(el, callback, prevent_default=true) {
|
||||
el.onmouseup = el.ontouchend = function(event) {
|
||||
el.onmouseup = el.ontouchend = function(ev) {
|
||||
if (prevent_default) {
|
||||
event.preventDefault();
|
||||
ev.preventDefault();
|
||||
}
|
||||
callback();
|
||||
};
|
||||
@@ -197,9 +210,9 @@ export var tools = new function() {
|
||||
el.__pressed = true;
|
||||
};
|
||||
|
||||
el.onmouseup = el.ontouchend = function(event) {
|
||||
el.onmouseup = el.ontouchend = function(ev) {
|
||||
let value = self.slider.getValue(el);
|
||||
event.preventDefault();
|
||||
ev.preventDefault();
|
||||
clear_timer();
|
||||
el.__execution_timer = setTimeout(function() {
|
||||
el.__pressed = false;
|
||||
@@ -252,29 +265,57 @@ export var tools = new function() {
|
||||
};
|
||||
};
|
||||
|
||||
self.sw = new function() {
|
||||
return {
|
||||
"makeItem": function(id, checked) {
|
||||
id = tools.escape(id);
|
||||
return `
|
||||
<div class="switch-box">
|
||||
<input
|
||||
type="checkbox" id="${id}"
|
||||
${checked ? "checked" : ""}
|
||||
/>
|
||||
<label for="${id}">
|
||||
<span class="switch-inner"></span>
|
||||
<span class="switch"></span>
|
||||
</label>
|
||||
</div>
|
||||
`;
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
self.radio = new function() {
|
||||
return {
|
||||
"makeItem": function(name, title, value) {
|
||||
let e_id = self.escape(name) + self.makeTextId(value);
|
||||
return `
|
||||
<input type="radio" id="${name}-${value}" name="${name}" value="${value}" />
|
||||
<label for="${name}-${value}">${title}</label>
|
||||
<input
|
||||
type="radio"
|
||||
id="${e_id}"
|
||||
name="${tools.escape(name)}"
|
||||
value="${tools.escape(value)}"
|
||||
/>
|
||||
<label for="${e_id}">
|
||||
${tools.escape(title)}
|
||||
</label>
|
||||
`;
|
||||
},
|
||||
"setOnClick": function(name, callback, prevent_default=true) {
|
||||
for (let el of $$$(`input[type="radio"][name="${name}"]`)) {
|
||||
for (let el of $$$(`input[type="radio"][name="${CSS.escape(name)}"]`)) {
|
||||
self.el.setOnClick(el, callback, prevent_default);
|
||||
}
|
||||
},
|
||||
"getValue": function(name) {
|
||||
return document.querySelector(`input[type="radio"][name="${name}"]:checked`).value;
|
||||
return document.querySelector(`input[type="radio"][name="${CSS.escape(name)}"]:checked`).value;
|
||||
},
|
||||
"setValue": function(name, value) {
|
||||
for (let el of $$$(`input[type="radio"][name="${name}"]`)) {
|
||||
for (let el of $$$(`input[type="radio"][name="${CSS.escape(name)}"]`)) {
|
||||
el.checked = (el.value === value);
|
||||
}
|
||||
},
|
||||
"clickValue": function(name, value) {
|
||||
for (let el of $$$(`input[type="radio"][name="${name}"]`)) {
|
||||
for (let el of $$$(`input[type="radio"][name="${CSS.escape(name)}"]`)) {
|
||||
if (el.value === value) {
|
||||
el.click();
|
||||
return;
|
||||
@@ -282,7 +323,7 @@ export var tools = new function() {
|
||||
}
|
||||
},
|
||||
"setEnabled": function(name, enabled) {
|
||||
for (let el of $$$(`input[type="radio"][name="${name}"]`)) {
|
||||
for (let el of $$$(`input[type="radio"][name="${CSS.escape(name)}"]`)) {
|
||||
self.el.setEnabled(el, enabled);
|
||||
}
|
||||
},
|
||||
@@ -359,7 +400,9 @@ export var tools = new function() {
|
||||
self.feature = new function() {
|
||||
return {
|
||||
"setEnabled": function(el, enabled) {
|
||||
el.classList.toggle("feature-disabled", !enabled);
|
||||
if (el) {
|
||||
el.classList.toggle("feature-disabled", !enabled);
|
||||
}
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -383,7 +426,7 @@ export var tools = new function() {
|
||||
|
||||
/************************************************************************/
|
||||
|
||||
self.is_https = (location.protocol === "https:");
|
||||
self.is_https = (window.location.protocol === "https:");
|
||||
|
||||
self.cookies = new function() {
|
||||
return {
|
||||
|
||||
@@ -20,10 +20,12 @@
|
||||
*****************************************************************************/
|
||||
|
||||
|
||||
div#text-menu {
|
||||
width: 340px;
|
||||
}
|
||||
"use strict";
|
||||
|
||||
input#hid-recorder-new-script-file {
|
||||
display: none;
|
||||
|
||||
export var ROOT_PREFIX = "./";
|
||||
|
||||
|
||||
export function setRootPrefix(prefix) {
|
||||
ROOT_PREFIX = prefix;
|
||||
}
|
||||
@@ -31,17 +31,27 @@ export function main() {
|
||||
}
|
||||
|
||||
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);
|
||||
tools.httpGet("api/info", null, function(http) {
|
||||
switch (http.status) {
|
||||
case 200:
|
||||
__showKvmdInfo(JSON.parse(http.responseText).result);
|
||||
break;
|
||||
|
||||
case 401:
|
||||
case 403:
|
||||
tools.currentOpen("login");
|
||||
break;
|
||||
|
||||
default:
|
||||
setTimeout(__loadKvmdInfo, 1000);
|
||||
break;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function __showKvmdInfo(info) {
|
||||
$("vnc-text").innerHTML = `
|
||||
<span class="code-comment"># How to connect using the Linux terminal:<br>
|
||||
$</span> vncviewer ${tools.escape(window.location.hostname + "::" + info.extras.vnc.port)}
|
||||
`;
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -4,7 +4,7 @@
|
||||
"start_url": "/",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/share/android-chrome-192x192.png",
|
||||
"src": "android-chrome-192x192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png"
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@ block body
|
||||
div(class="start-box")
|
||||
div(class="start")
|
||||
if index_link
|
||||
a(style="display:inline-block; margin-top:4px; color:#5c90bc; text-decoration:none" href="/" i18n="index")
|
||||
a(style="display:inline-block; margin-top:4px; color:#5c90bc; text-decoration:none" href=root_prefix i18n="index")
|
||||
| ← [ One-KVM Index ]
|
||||
hr
|
||||
block start
|
||||
|
||||
@@ -27,27 +27,29 @@
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>One-KVM VNC 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">
|
||||
<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/vnc/main.js";
|
||||
<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 {setRootPrefix} from "../share/js/vars.js";
|
||||
setRootPrefix("../");
|
||||
import {main} from "../share/js/vnc/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"> ← [ One-KVM Index ]</a>
|
||||
<div class="start"><a style="display:inline-block; margin-top:4px; color:#5c90bc; text-decoration:none" href="../" i18n="index"> ← [ One-KVM Index ]</a>
|
||||
<hr>
|
||||
<p class="text" i18n="vnc_text1">This PiKVM device has running <b>kvmd-vnc</b> daemon and provides VNC access to the server.</p>
|
||||
<p class="text" i18n="vnc_text2"><b>WARNING!</b> We strongly don't recommend you to use VNC in untrusted networks without
|
||||
|
||||
@@ -1,20 +1,23 @@
|
||||
extends ../start.pug
|
||||
|
||||
|
||||
append vars
|
||||
- title = "One-KVM VNC info"
|
||||
- main_js = "vnc/main"
|
||||
- index_link = true
|
||||
-
|
||||
root_prefix = "../"
|
||||
title = "One-KVM VNC info"
|
||||
main_js = "vnc/main"
|
||||
index_link = true
|
||||
|
||||
block start
|
||||
p(class="text" i18n="vnc_text1")
|
||||
p.text(i18n="vnc_text1")
|
||||
| This PiKVM device has running #[b kvmd-vnc] daemon and provides VNC access to the server.
|
||||
p(class="text" i18n="vnc_text2")
|
||||
p.text(i18n="vnc_text2")
|
||||
| #[b WARNING!] 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.
|
||||
p(class="text" i18n="vnc_text3")
|
||||
p.text(i18n="vnc_text3")
|
||||
| Your VNC client must support Tight JPEG compression and password authentication.
|
||||
| #[a(href="https://tigervnc.org") TigerVNC] is a good choice.
|
||||
| On Linux, this client will most likely be available for installation from the repository.
|
||||
| It can also be called vncviewer.
|
||||
div(id="vnc-text" class="code" style="max-height:200px")
|
||||
.code#vnc-text(style="max-height:200px")
|
||||
|
||||
Reference in New Issue
Block a user