Merge remote-tracking branch 'upstream/master'

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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