feat: merge upstream master - version 4.94

Merge upstream PiKVM master branch updates:

- Bump version from 4.93 to 4.94
- HID: improved jiggler pattern for better compatibility
- Streamer: major refactoring for improved performance and maintainability
- Prometheus: tidying GPIO channel name formatting
- Web: added __gpio-label class for custom styling
- HID: customizable /api/hid/print delay configuration
- ATX: independent power/reset regions for better control
- OLED: added --fill option for display testing
- Web: improved keyboard handling in modal dialogs
- Web: enhanced login error messages
- Switch: added heartbeat functionality
- Web: mouse touch code simplification and refactoring
- Configs: use systemd-networkd-wait-online --any by default
- PKGBUILD: use cp -r to install systemd units properly
- Various bug fixes and performance improvements
This commit is contained in:
mofeng-git
2025-08-21 11:21:41 +08:00
205 changed files with 9359 additions and 4653 deletions

View File

@@ -23,6 +23,7 @@
"use strict";
import {ROOT_PREFIX} from "./vars.js";
import {browser} from "./bb.js";
@@ -39,7 +40,16 @@ export var tools = new function() {
/************************************************************************/
self.currentOpen = function(url) {
window.location.href = ROOT_PREFIX + url;
};
self.windowOpen = function(url) {
window.open(ROOT_PREFIX + url, "_blank");
};
self.httpRequest = function(method, url, params, callback, body=null, content_type=null, timeout=15000) {
url = ROOT_PREFIX + url;
if (params) {
params = new URLSearchParams(params);
if (params) {
@@ -68,11 +78,19 @@ export var tools = new function() {
self.httpRequest("POST", url, params, callback, body, content_type, timeout);
};
self.makeWsUrl = function(url) {
let proto = (self.is_https ? "wss://" : "ws://");
return proto + window.location.host + window.location.pathname + ROOT_PREFIX + url;
};
/************************************************************************/
self.escape = function(text) {
if (typeof text !== "string") {
text = "" + text;
}
return text.replace(
/[^0-9A-Za-z ]/g,
/[^-_0-9A-Za-z ]/g,
ch => "&#" + ch.charCodeAt(0) + ";"
);
};
@@ -85,7 +103,7 @@ export var tools = new function() {
return text[0].toUpperCase() + text.slice(1);
};
self.makeId = function() {
self.makeRandomId = function() {
let chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
let id = "";
for (let count = 0; count < 16; ++count) {
@@ -94,16 +112,10 @@ export var tools = new function() {
return id;
};
self.makeIdByText = function(text) {
self.makeTextId = function(text) {
return btoa(text).replace("=", "_");
};
self.getRandomInt = function(min, max) {
min = Math.ceil(min);
max = Math.floor(max);
return Math.floor(Math.random() * (max - min + 1)) + min;
};
self.formatSize = function(size) {
if (size > 0) {
let index = Math.floor( Math.log(size) / Math.log(1024) );
@@ -124,14 +136,15 @@ export var tools = new function() {
return `${hours}:${mins}:${secs}.${millis}`;
};
self.remap = function(x, a1, b1, a2, b2) {
let remapped = Math.round((x - a1) / b1 * (b2 - a2) + a2);
if (remapped < a2) {
return a2;
} else if (remapped > b2) {
return b2;
}
return remapped;
self.remap = function(value, in_min, in_max, out_min, out_max) {
let result = Math.round((value - in_min) * (out_max - out_min) / ((in_max - in_min) || 1) + out_min);
return Math.min(Math.max(result, out_min), out_max);
};
self.getRandomInt = function(min, max) {
min = Math.ceil(min);
max = Math.floor(max);
return Math.floor(Math.random() * (max - min + 1)) + min;
};
/************************************************************************/
@@ -139,25 +152,25 @@ export var tools = new function() {
self.el = new function() {
return {
"setOnClick": function(el, callback, prevent_default=true) {
el.onclick = el.ontouchend = function(event) {
el.onclick = el.ontouchend = function(ev) {
if (prevent_default) {
event.preventDefault();
ev.preventDefault();
}
callback();
};
},
"setOnDown": function(el, callback, prevent_default=true) {
el.onmousedown = el.ontouchstart = function(event) {
el.onmousedown = el.ontouchstart = function(ev) {
if (prevent_default) {
event.preventDefault();
ev.preventDefault();
}
callback();
callback(ev);
};
},
"setOnUp": function(el, callback, prevent_default=true) {
el.onmouseup = el.ontouchend = function(event) {
el.onmouseup = el.ontouchend = function(ev) {
if (prevent_default) {
event.preventDefault();
ev.preventDefault();
}
callback();
};
@@ -197,9 +210,9 @@ export var tools = new function() {
el.__pressed = true;
};
el.onmouseup = el.ontouchend = function(event) {
el.onmouseup = el.ontouchend = function(ev) {
let value = self.slider.getValue(el);
event.preventDefault();
ev.preventDefault();
clear_timer();
el.__execution_timer = setTimeout(function() {
el.__pressed = false;
@@ -252,29 +265,57 @@ export var tools = new function() {
};
};
self.sw = new function() {
return {
"makeItem": function(id, checked) {
id = tools.escape(id);
return `
<div class="switch-box">
<input
type="checkbox" id="${id}"
${checked ? "checked" : ""}
/>
<label for="${id}">
<span class="switch-inner"></span>
<span class="switch"></span>
</label>
</div>
`;
},
};
};
self.radio = new function() {
return {
"makeItem": function(name, title, value) {
let e_id = self.escape(name) + self.makeTextId(value);
return `
<input type="radio" id="${name}-${value}" name="${name}" value="${value}" />
<label for="${name}-${value}">${title}</label>
<input
type="radio"
id="${e_id}"
name="${tools.escape(name)}"
value="${tools.escape(value)}"
/>
<label for="${e_id}">
${tools.escape(title)}
</label>
`;
},
"setOnClick": function(name, callback, prevent_default=true) {
for (let el of $$$(`input[type="radio"][name="${name}"]`)) {
for (let el of $$$(`input[type="radio"][name="${CSS.escape(name)}"]`)) {
self.el.setOnClick(el, callback, prevent_default);
}
},
"getValue": function(name) {
return document.querySelector(`input[type="radio"][name="${name}"]:checked`).value;
return document.querySelector(`input[type="radio"][name="${CSS.escape(name)}"]:checked`).value;
},
"setValue": function(name, value) {
for (let el of $$$(`input[type="radio"][name="${name}"]`)) {
for (let el of $$$(`input[type="radio"][name="${CSS.escape(name)}"]`)) {
el.checked = (el.value === value);
}
},
"clickValue": function(name, value) {
for (let el of $$$(`input[type="radio"][name="${name}"]`)) {
for (let el of $$$(`input[type="radio"][name="${CSS.escape(name)}"]`)) {
if (el.value === value) {
el.click();
return;
@@ -282,7 +323,7 @@ export var tools = new function() {
}
},
"setEnabled": function(name, enabled) {
for (let el of $$$(`input[type="radio"][name="${name}"]`)) {
for (let el of $$$(`input[type="radio"][name="${CSS.escape(name)}"]`)) {
self.el.setEnabled(el, enabled);
}
},
@@ -359,7 +400,9 @@ export var tools = new function() {
self.feature = new function() {
return {
"setEnabled": function(el, enabled) {
el.classList.toggle("feature-disabled", !enabled);
if (el) {
el.classList.toggle("feature-disabled", !enabled);
}
},
};
};
@@ -383,7 +426,7 @@ export var tools = new function() {
/************************************************************************/
self.is_https = (location.protocol === "https:");
self.is_https = (window.location.protocol === "https:");
self.cookies = new function() {
return {