diff options
author | Devaev Maxim <[email protected]> | 2020-06-20 11:29:06 +0300 |
---|---|---|
committer | Devaev Maxim <[email protected]> | 2020-06-20 11:29:06 +0300 |
commit | cf2f763d1bd23170cb82e0df9eff363931cc66dc (patch) | |
tree | 541f6cd3b498d2a6ccde3e0921639a588535fdba | |
parent | 490e5b352ef569ef5b3f5211523d21bbd5ed0e1c (diff) |
user macro
-rw-r--r-- | web/kvm/index.html | 37 | ||||
-rw-r--r-- | web/share/css/kvm/hid.css | 4 | ||||
-rw-r--r-- | web/share/css/led.css | 17 | ||||
-rw-r--r-- | web/share/js/kvm/hid.js | 13 | ||||
-rw-r--r-- | web/share/js/kvm/keyboard.js | 14 | ||||
-rw-r--r-- | web/share/js/kvm/mouse.js | 8 | ||||
-rw-r--r-- | web/share/js/kvm/recorder.js | 258 | ||||
-rw-r--r-- | web/share/js/tools.js | 11 |
8 files changed, 341 insertions, 21 deletions
diff --git a/web/kvm/index.html b/web/kvm/index.html index 118bdd5c..8c576db5 100644 --- a/web/kvm/index.html +++ b/web/kvm/index.html @@ -302,6 +302,43 @@ <li class="menu-right-items"> <a class="menu-item" href="#"> + <img data-dont-hide-menu id="hid-recorder-led" class="led-gray" src="../share/svg/led-gear.svg" /> + Macro ↴ + </a> + <div data-dont-hide-menu class="menu-item-content"> + <div class="menu-item-content-text"> + <b>Record and play keyboard & mouse actions</b><br> + <sub>For security reasons, the record will not saved on Pi-KVM</sub> + </div> + <div class="menu-item-content-buttons buttons-row"> + <button disabled data-force-hide-menu id="hid-recorder-record" class="row25">• Rec</button> + <button disabled id="hid-recorder-stop" class="row25">Stop</button> + <button disabled id="hid-recorder-play" class="row25">Play</button> + <button disabled id="hid-recorder-clear" class="row25">Clear</button> + </div> + <hr> + <table class="menu-item-content-kv"> + <tr> + <td>Script time:</td> + <td colspan="2" id="hid-recorder-time" class="value">00:00:00.0</td> + </tr> + <tr> + <td>Scripted events:</td> + <td id="hid-recorder-events-count" class="value">0</td> + <td><sup><i>include delays</i></sup></td> + </tr> + </table> + <hr> + <input type="file" id="hid-recorder-new-script-file" /> + <div class="menu-item-content-buttons buttons-row"> + <button disabled id="hid-recorder-upload" class="row50">Upload script</button> + <button disabled id="hid-recorder-download" class="row50">Download script</button> + </div> + </div> + </li> + + <li class="menu-right-items"> + <a class="menu-item" href="#"> Shortcuts ↴ </a> <div data-dont-hide-menu class="menu-item-content"> diff --git a/web/share/css/kvm/hid.css b/web/share/css/kvm/hid.css index 24862f33..fd095b18 100644 --- a/web/share/css/kvm/hid.css +++ b/web/share/css/kvm/hid.css @@ -41,3 +41,7 @@ textarea#hid-pak-text::-webkit-input-placeholder { line-height: 40px; text-align: center; } + +input#hid-recorder-new-script-file { + display: none; +} diff --git a/web/share/css/led.css b/web/share/css/led.css index 491659cb..dab35fa4 100644 --- a/web/share/css/led.css +++ b/web/share/css/led.css @@ -63,18 +63,11 @@ img.led-yellow { filter: var(--led-filter-yellow); } -img.led-green-rotating-medium { - -webkit-filter: var(--led-filter-green); - filter: var(--led-filter-green); - -webkit-animation: var(--led-spin-medium); - animation: var(--led-spin-medium); -} - -img.led-yellow-rotating-slow { - -webkit-filter: var(--led-filter-yellow); - filter: var(--led-filter-yellow); - -webkit-animation: var(--led-spin-slow); - animation: var(--led-spin-slow); +img.led-red-rotating-fast { + -webkit-filter: var(--led-filter-red); + filter: var(--led-filter-red); + -webkit-animation: var(--led-spin-fast); + animation: var(--led-spin-fast); } img.led-yellow-rotating-fast { diff --git a/web/share/js/kvm/hid.js b/web/share/js/kvm/hid.js index 8efb72c2..6ec64fea 100644 --- a/web/share/js/kvm/hid.js +++ b/web/share/js/kvm/hid.js @@ -26,6 +26,7 @@ import {tools, $, $$$} from "../tools.js"; import {wm} from "../wm.js"; +import {Recorder} from "./recorder.js"; import {Keyboard} from "./keyboard.js"; import {Mouse} from "./mouse.js"; @@ -35,10 +36,15 @@ export function Hid() { /************************************************************************/ - var __keyboard = new Keyboard(); - var __mouse = new Mouse(); + var __recorder = null; + var __keyboard = null; + var __mouse = null; var __init__ = function() { + __recorder = new Recorder(); + __keyboard = new Keyboard(__recorder.recordWsEvent); + __mouse = new Mouse(__recorder.recordWsEvent); + let hidden_attr = null; let visibility_change_attr = null; @@ -82,6 +88,7 @@ export function Hid() { wm.switchEnabled($("hid-pak-text"), ws); wm.switchEnabled($("hid-pak-button"), ws); wm.switchEnabled($("hid-reset-button"), ws); + __recorder.setSocket(ws); __keyboard.setSocket(ws); __mouse.setSocket(ws); }; @@ -146,6 +153,8 @@ export function Hid() { 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"); diff --git a/web/share/js/kvm/keyboard.js b/web/share/js/kvm/keyboard.js index 10c6ccc5..e919a840 100644 --- a/web/share/js/kvm/keyboard.js +++ b/web/share/js/kvm/keyboard.js @@ -24,11 +24,13 @@ import {tools, $, $$$} from "../tools.js"; import {Keypad} from "../keypad.js"; -export function Keyboard() { +export function Keyboard(record_callback) { var self = this; /************************************************************************/ + var __record_callback = record_callback; + var __ws = null; var __online = true; @@ -136,12 +138,14 @@ export function Keyboard() { var __sendKey = function(code, state) { tools.debug("Keyboard: key", (state ? "pressed:" : "released:"), code); + let event = { + "event_type": "key", + "event": {"key": code, "state": state}, + }; if (__ws) { - __ws.send(JSON.stringify({ - "event_type": "key", - "event": {"key": code, "state": state}, - })); + __ws.send(JSON.stringify(event)); } + __record_callback(event); }; __init__(); diff --git a/web/share/js/kvm/mouse.js b/web/share/js/kvm/mouse.js index dca2f4c5..9ac1fd0c 100644 --- a/web/share/js/kvm/mouse.js +++ b/web/share/js/kvm/mouse.js @@ -27,11 +27,13 @@ import {tools, $} from "../tools.js"; import {Keypad} from "../keypad.js"; -export function Mouse() { +export function Mouse(record_callback) { var self = this; /************************************************************************/ + var __record_callback = record_callback; + var __ws = null; var __online = true; @@ -194,9 +196,11 @@ export function Mouse() { }; var __sendEvent = function(event_type, event) { + event = {"event_type": event_type, "event": event}; if (__ws) { - __ws.send(JSON.stringify({"event_type": event_type, "event": event})); + __ws.send(JSON.stringify(event)); } + __record_callback(event); }; __init__(); diff --git a/web/share/js/kvm/recorder.js b/web/share/js/kvm/recorder.js new file mode 100644 index 00000000..ea5b956b --- /dev/null +++ b/web/share/js/kvm/recorder.js @@ -0,0 +1,258 @@ +/***************************************************************************** +# # +# KVMD - The main Pi-KVM daemon. # +# # +# Copyright (C) 2018 Maxim Devaev <[email protected]> # +# # +# This program is free software: you can redistribute it and/or modify # +# it under the terms of the GNU General Public License as published by # +# the Free Software Foundation, either version 3 of the License, or # +# (at your option) any later version. # +# # +# This program is distributed in the hope that it will be useful, # +# but WITHOUT ANY WARRANTY; without even the implied warranty of # +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # +# GNU General Public License for more details. # +# # +# You should have received a copy of the GNU General Public License # +# along with this program. If not, see <https://www.gnu.org/licenses/>. # +# # +*****************************************************************************/ + + +"use strict"; + + +import {tools, $} from "../tools.js"; +import {wm} from "../wm.js"; + + +export function Recorder() { + var self = this; + + /************************************************************************/ + + var __ws = null; + + var __play_timer = null; + var __recording = false; + var __record = []; + var __record_time = 0; + var __last_event_ts = 0; + + var __init__ = function() { + tools.setOnClick($("hid-recorder-record"), __startRecord); + tools.setOnClick($("hid-recorder-stop"), __stopProcess); + tools.setOnClick($("hid-recorder-play"), __playRecord); + tools.setOnClick($("hid-recorder-clear"), __clearRecord); + + $("hid-recorder-new-script-file").onchange = __uploadScript; + tools.setOnClick($("hid-recorder-upload"), () => $("hid-recorder-new-script-file").click()); + tools.setOnClick($("hid-recorder-download"), __downloadScript); + }; + + /************************************************************************/ + + self.setSocket = function(ws) { + if (ws !== __ws) { + __ws = ws; + } + if (__ws === null) { + __stopProcess(); + } + __refresh(); + }; + + self.recordWsEvent = function(event) { + __recordEvent(event); + }; + + self.recordPrintEvent = function(text) { + __recordEvent({"event_type": "print", "event": {"text": text}}); + }; + + var __recordEvent = function(event) { + if (__recording) { + let now = new Date().getTime(); + if (__last_event_ts) { + let delay = now - __last_event_ts; + __record.push({"event_type": "delay", "event": {"millis": delay}}); + __record_time += delay; + } + __last_event_ts = now; + __record.push(event); + __setCounters(__record.length, __record_time); + } + }; + + var __startRecord = function() { + __clearRecord(); + __recording = true; + __refresh(); + }; + + var __stopProcess = function() { + if (__play_timer) { + clearTimeout(__play_timer); + __play_timer = null; + } + if (__recording) { + __recording = false; + } + __refresh(); + }; + + var __playRecord = function() { + __play_timer = setTimeout(() => __runEvents(0), 0); + __refresh(); + }; + + var __clearRecord = function() { + __record = []; + __record_time = 0; + __last_event_ts = 0; + __refresh(); + }; + + var __downloadScript = function() { + let blob = new Blob([JSON.stringify(__record, undefined, 4)], {"type": "application/json"}); + let url = window.URL.createObjectURL(blob); + let el_anchor = document.createElement("a"); + el_anchor.href = url; + el_anchor.download = "script.json"; + el_anchor.click(); + window.URL.revokeObjectURL(url); + }; + + var __uploadScript = function() { + let el_input = $("hid-recorder-new-script-file"); + let script_file = (el_input.files.length ? el_input.files[0] : null); + if (script_file) { + let reader = new FileReader(); + reader.onload = function () { + let record = []; + let record_time = 0; + + try { + let raw_record = JSON.parse(reader.result); + console.log(typeof raw_record); + console.log(raw_record); + __checkType(raw_record, "object", "Base of script is not an objects list"); + + for (let event of raw_record) { + __checkType(event, "object", "Non-dict event"); + __checkType(event.event, "object", "Non-dict event"); + + if (event.event_type === "delay") { + __checkInt(event.event.millis, "Non-integer delay"); + if (event.event.millis < 0) { + throw "Negative delay"; + } + record_time += event.event.millis; + } else if (event.event_type === "print") { + __checkType(event.event.text, "string", "Non-string print text"); + } else if (event.event_type === "key") { + __checkType(event.event.key, "string", "Non-string key code"); + __checkType(event.event.state, "boolean", "Non-bool key state"); + } else if (event.event_type === "mouse_button") { + __checkType(event.event.button, "string", "Non-string mouse button code"); + __checkType(event.event.state, "boolean", "Non-bool mouse button state"); + } else if (event.event_type === "mouse_move") { + __checkType(event.event.to, "object", "Non-object mouse move target"); + __checkInt(event.event.to.x, "Non-int mouse move X"); + __checkInt(event.event.to.y, "Non-int mouse move Y"); + } else if (event.event_type === "mouse_wheel") { + __checkType(event.event.delta, "object", "Non-object mouse wheel delta"); + __checkInt(event.event.delta.x, "Non-int mouse delta X"); + __checkInt(event.event.delta.y, "Non-int mouse delta Y"); + } else { + throw "Unknown event type"; + } + + record.push(event); + } + + __record = record; + __record_time = record_time; + } catch (err) { + wm.error(`Invalid script: ${err}`); + } + + el_input.value = ""; + __refresh(); + }; + reader.readAsText(script_file, "UTF-8"); + } + }; + + var __checkType = function(obj, type, msg) { + if (typeof obj !== type) { + throw msg; + } + }; + + var __checkInt = function(obj, msg) { + if (!Number.isInteger(obj)) { + throw msg; + } + }; + + var __runEvents = function(index, time=0) { + while (index < __record.length) { + __setCounters(__record.length - index + 1, __record_time - time); + let event = __record[index]; + if (event.event_type === "delay") { + __play_timer = setTimeout(() => __runEvents(index + 1, time + event.event.millis), event.event.millis); + return; + } else if (event.event_type === "print") { + let http = tools.makeRequest("POST", "/api/hid/print?limit=0", function() { + if (http.readyState === 4) { + 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); + __stopProcess(); + } else if (http.status === 200) { + __play_timer = setTimeout(() => __runEvents(index + 1, time), 0); + } + } + }, event.event.text, "text/plain"); + return; + } else if (event.event_type in ["key", "mouse_button", "mouse_move", "mouse_wheel"]) { + __ws.send(JSON.stringify(event)); + } + index += 1; + } + __stopProcess(); + }; + + var __refresh = function() { + if (__play_timer) { + $("hid-recorder-led").className = "led-yellow-rotating-fast"; + $("hid-recorder-led").title = "Playing..."; + } else if (__recording) { + $("hid-recorder-led").className = "led-red-rotating-fast"; + $("hid-recorder-led").title = "Recording..."; + } else { + $("hid-recorder-led").className = "led-gray"; + $("hid-recorder-led").title = ""; + } + + wm.switchEnabled($("hid-recorder-record"), (__ws && !__play_timer && !__recording)); + wm.switchEnabled($("hid-recorder-stop"), (__ws && (__play_timer || __recording))); + wm.switchEnabled($("hid-recorder-play"), (__ws && !__recording && __record.length)); + wm.switchEnabled($("hid-recorder-clear"), (!__play_timer && !__recording && __record.length)); + wm.switchEnabled($("hid-recorder-upload"), (!__play_timer && !__recording)); + wm.switchEnabled($("hid-recorder-download"), (!__play_timer && !__recording && __record.length)); + + __setCounters(__record.length, __record_time); + }; + + var __setCounters = function(events_count, time) { + $("hid-recorder-time").innerHTML = tools.formatDuration(time); + $("hid-recorder-events-count").innerHTML = events_count; + }; + + __init__(); +} diff --git a/web/share/js/tools.js b/web/share/js/tools.js index 8212b62f..e3fa3892 100644 --- a/web/share/js/tools.js +++ b/web/share/js/tools.js @@ -64,6 +64,17 @@ export var tools = new function() { } }; + this.formatDuration = function(duration) { + let millis = parseInt((duration % 1000) / 100); + let secs = Math.floor((duration / 1000) % 60); + let mins = Math.floor((duration / (1000 * 60)) % 60); + let hours = Math.floor((duration / (1000 * 60 * 60)) % 24); + hours = (hours < 10 ? "0" + hours : hours); + mins = (mins < 10 ? "0" + mins : mins); + secs = (secs < 10 ? "0" + secs : secs); + return `${hours}:${mins}:${secs}.${millis}`; + }; + /************************************************************************/ this.getCookie = function(name) { |