Use ECMAScript modules

This commit is contained in:
Unrud 2023-04-25 01:38:50 +02:00
parent e5369af219
commit b239ca7160
12 changed files with 879 additions and 664 deletions

76
webdata/app/compat.mjs Normal file
View file

@ -0,0 +1,76 @@
/*
* Copyright (c) 2018-2019, 2023 Unrud <unrud@outlook.com>
*
* This file is part of Remote-Touchpad.
*
* Remote-Touchpad 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.
*
* Remote-Touchpad 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 Remote-Touchpad. If not, see <http://www.gnu.org/licenses/>.
*/
export const fullscreenEnabled = () => {
return (document.fullscreenEnabled ||
document.webkitFullscreenEnabled ||
false);
};
export const requestFullscreen = (element, options) => {
if (element.requestFullscreen) {
element.requestFullscreen(options);
} else if (element.webkitRequestFullscreen) {
element.webkitRequestFullscreen(options);
}
};
export const exitFullscreen = () => {
if (document.exitFullscreen) {
document.exitFullscreen();
} else if (document.webkitExitFullscreen) {
document.webkitExitFullscreen();
}
};
export const fullscreenElement = () => {
return (document.fullscreenElement ||
document.webkitFullscreenElement ||
null);
};
export const addFullscreenchangeEventListener = (listener) => {
if ("onfullscreenchange" in document) {
document.addEventListener("fullscreenchange", listener);
} else if ("onwebkitfullscreenchange" in document) {
document.addEventListener("webkitfullscreenchange", listener);
}
};
export const requestPointerLock = (element) => {
if (element.requestPointerLock) {
element.requestPointerLock();
}
};
export const exitPointerLock = () => {
if (document.exitPointerLock) {
document.exitPointerLock();
}
};
export const pointerLockElement = () => {
return document.pointerLockElement || null;
};
export const addPointerlockchangeEventListener = (listener) => {
if ("onpointerlockchange" in document) {
document.addEventListener("pointerlockchange", listener);
}
};

View file

@ -0,0 +1,121 @@
/*
* Copyright (c) 2018-2019, 2023 Unrud <unrud@outlook.com>
*
* This file is part of Remote-Touchpad.
*
* Remote-Touchpad 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.
*
* Remote-Touchpad 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 Remote-Touchpad. If not, see <http://www.gnu.org/licenses/>.
*/
export const POINTER_BUTTON_LEFT = 0;
export const POINTER_BUTTON_RIGHT = 1;
export const POINTER_BUTTON_MIDDLE = 2;
export const KEY_VOLUME_MUTE = 0;
export const KEY_VOLUME_DOWN = 1;
export const KEY_VOLUME_UP = 2;
export const KEY_MEDIA_PLAY_PAUSE = 3;
export const KEY_MEDIA_PREV_TRACK = 4;
export const KEY_MEDIA_NEXT_TRACK = 5;
export const KEY_BROWSER_BACK = 6;
export const KEY_BROWSER_FORWARD = 7;
export const KEY_SUPER = 8;
export const KEY_LEFT = 9;
export const KEY_RIGHT = 10;
export const KEY_UP = 11;
export const KEY_DOWN = 12;
export const KEY_HOME = 13;
export const KEY_END = 14;
export const KEY_BACK_SPACE = 15;
export const KEY_DELETE = 16;
export const KEY_RETURN = 17;
export default class InputController {
#updateRate = 0;
#moveXSum = 0;
#moveYSum = 0;
#scrollHSum = 0;
#scrollVSum = 0;
#scrolling = false;
#scrollFinish = false;
#updateTimeoutActive = false;
#socket;
constructor(socket) {
this.#socket = socket;
}
configure(config) {
this.#updateRate = config.updateRate;
}
#startUpdate(fromTimeout) {
if (this.#updateTimeoutActive && !fromTimeout) {
return;
}
this.#updateTimeoutActive = false;
let finished = true;
const xInt = Math.trunc(this.#moveXSum);
const yInt = Math.trunc(this.#moveYSum);
if (xInt != 0 || yInt != 0) {
this.#socket.send("m" + xInt + ";" + yInt);
this.#moveXSum -= xInt;
this.#moveYSum -= yInt;
finished = false;
}
const hInt = Math.trunc(this.#scrollHSum);
const vInt = Math.trunc(this.#scrollVSum);
if (hInt != 0 || vInt != 0) {
this.#socket.send((this.#scrollFinish ? "S" : "s") + hInt + ";" + vInt);
this.#scrollHSum -= hInt;
this.#scrollVSum -= vInt;
this.#scrolling = !this.#scrollFinish;
this.#scrollFinish = false;
finished = false;
} else if (this.#scrollFinish && this.#scrolling) {
this.#socket.send("S");
this.#scrolling = false;
this.#scrollFinish = false;
}
this.#updateTimeoutActive = !finished && this.#updateRate > 0;
if (this.#updateTimeoutActive) {
setTimeout(this.#startUpdate.bind(this), 1000 / this.#updateRate, true);
}
}
pointerMove(deltaX, deltaY) {
this.#moveXSum += deltaX;
this.#moveYSum += deltaY;
this.#startUpdate();
}
pointerScroll(deltaHorizontal, deltaVertical, finish) {
this.#scrollHSum += deltaHorizontal;
this.#scrollVSum += deltaVertical;
this.#scrollFinish |= finish;
this.#startUpdate();
};
pointerButton(button, press) {
this.#socket.send("b" + button + ";" + (press ? 1 : 0));
}
keyboardKey(key) {
this.#socket.send("k" + key);
}
keyboardText(text) {
this.#socket.send("t" + text);
}
}

74
webdata/app/keyboard.mjs Normal file
View file

@ -0,0 +1,74 @@
/*
* Copyright (c) 2018-2019, 2023 Unrud <unrud@outlook.com>
*
* This file is part of Remote-Touchpad.
*
* Remote-Touchpad 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.
*
* Remote-Touchpad 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 Remote-Touchpad. If not, see <http://www.gnu.org/licenses/>.
*/
import {
KEY_SUPER, KEY_BACK_SPACE, KEY_RETURN, KEY_DELETE, KEY_HOME, KEY_END,
KEY_LEFT, KEY_RIGHT, KEY_UP, KEY_DOWN,
} from "./inputcontroller.mjs";
export default class Keyboard {
#inputController;
#checkAllowedCallback;
constructor(inputController, checkAllowedCallback) {
this.#inputController = inputController;
this.#checkAllowedCallback = checkAllowedCallback;
document.addEventListener("keydown", this.#handleKeydown.bind(this));
}
configure() {}
#handleKeydown(event) {
if (!this.#checkAllowedCallback() ||
event.ctrlKey || event.altKey || event.isComposing) {
return;
}
let key = null;
if (event.key == "OS" || event.key == "Super" || event.key == "Meta") {
key = KEY_SUPER;
} else if (event.key == "Backspace") {
key = KEY_BACK_SPACE;
} else if (event.key == "Enter") {
key = KEY_RETURN;
} else if (event.key == "Delete") {
key = KEY_DELETE;
} else if (event.key == "Home") {
key = KEY_HOME;
} else if (event.key == "End") {
key = KEY_END;
} else if (event.key == "Left" || event.key == "ArrowLeft") {
key = KEY_LEFT;
} else if (event.key == "Right" || event.key == "ArrowRight") {
key = KEY_RIGHT;
} else if (event.key == "Up" || event.key == "ArrowUp") {
key = KEY_UP;
} else if (event.key == "Down" || event.key == "ArrowDown") {
key = KEY_DOWN;
}
if (key != null) {
if (!event.shiftKey) {
event.preventDefault();
this.#inputController.keyboardKey(key);
}
} else if (event.key.length == 1) {
event.preventDefault();
this.#inputController.keyboardText(event.key);
}
}
}

53
webdata/app/main.mjs Normal file
View file

@ -0,0 +1,53 @@
/*
* Copyright (c) 2018-2019, 2023 Unrud <unrud@outlook.com>
*
* This file is part of Remote-Touchpad.
*
* Remote-Touchpad 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.
*
* Remote-Touchpad 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 Remote-Touchpad. If not, see <http://www.gnu.org/licenses/>.
*/
import InputController, * as inputcontrollerModule from "./inputcontroller.mjs";
import Socket from "./socket.mjs";
import UI from "./ui.mjs";
const url = new URL("ws", location.href);
url.protocol = url.protocol == "http:" ? "ws:" : "wss:";
const socket = new Socket(url, window.location.hash.substr(1));
const inputController = new InputController(socket);
const ui = new UI(inputController);
socket.addEventListener("config", (event) => {
const config = event.detail;
inputController.configure(config);
ui.configure(config);
});
socket.addEventListener("close", () => {
ui.close();
});
window.app = {
key: inputController.keyboardKey.bind(inputController),
text: inputController.keyboardText.bind(inputController),
toggleFullscreen: ui.toggleFullscreen.bind(ui),
showTextInput: ui.showTextInput.bind(ui),
showKeys: ui.showKeys.bind(ui),
setKeysPage: ui.setKeysPage.bind(ui),
};
for (const name in inputcontrollerModule) {
if (name.startsWith("KEY_")) {
window.app[name] = inputcontrollerModule[name];
}
}

74
webdata/app/mouse.mjs Normal file
View file

@ -0,0 +1,74 @@
/*
* Copyright (c) 2018-2019, 2023 Unrud <unrud@outlook.com>
*
* This file is part of Remote-Touchpad.
*
* Remote-Touchpad 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.
*
* Remote-Touchpad 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 Remote-Touchpad. If not, see <http://www.gnu.org/licenses/>.
*/
export default class Mouse {
#moveSpeed = 1;
#scrollSpeed = 1;
#buttons = 0;
#inputController;
constructor (inputController, element) {
this.#inputController = inputController;
for (const type of ["touchstart", "touchend", "touchcancel", "touchmove"]) {
element.addEventListener(type, (event) => {
event.preventDefault();
});
}
element.addEventListener("mousedown", this.#handleMouseDownAndUp.bind(this));
element.addEventListener("mouseup", this.#handleMouseDownAndUp.bind(this));
element.addEventListener("mousemove", this.#handleMousemove.bind(this));
element.addEventListener("wheel", this.#handleWheel.bind(this));
}
configure(config) {
this.#moveSpeed = config.mouseMoveSpeed;
this.#scrollSpeed = config.mouseScrollSpeed;
}
#updateButtons(newButtons) {
for (let button = 0; button < 3; button += 1) {
const flag = 1 << button;
if ((newButtons&flag) != (this.#buttons & flag)) {
this.#inputController.pointerButton(button, newButtons & flag);
}
}
this.#buttons = newButtons;
}
#handleMouseDownAndUp(event) {
this.#updateButtons(event.buttons);
}
#handleMousemove(event) {
this.#inputController.pointerMove(
event.movementX * this.#moveSpeed, event.movementY * this.#moveSpeed);
}
#handleWheel(event) {
if (event.deltaMode == WheelEvent.DOM_DELTA_PIXEL) {
this.#inputController.pointerScroll(
event.deltaX * this.#scrollSpeed, event.deltaY * this.#scrollSpeed, true);
} else if (event.deltaMode == WheelEvent.DOM_DELTA_LINE) {
this.#inputController.pointerScroll(
event.deltaX * 20 * this.#scrollSpeed, event.deltaY * 20 * this.#scrollSpeed,
true);
}
}
}

9
webdata/app/sha256.mjs Normal file

File diff suppressed because one or more lines are too long

66
webdata/app/socket.mjs Normal file
View file

@ -0,0 +1,66 @@
/*
* Copyright (c) 2018-2019, 2023 Unrud <unrud@outlook.com>
*
* This file is part of Remote-Touchpad.
*
* Remote-Touchpad 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.
*
* Remote-Touchpad 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 Remote-Touchpad. If not, see <http://www.gnu.org/licenses/>.
*/
import jsSHA from "./sha256.mjs"
const challengeResponse = (message, secret) => {
const shaObj = new jsSHA("SHA-256", "TEXT");
shaObj.setHMACKey(message, "TEXT");
shaObj.update(secret);
return btoa(shaObj.getHMAC("BYTES"));
};
export default class Socket extends EventTarget {
#secret;
#authenticated;
#ws;
constructor(url, secret) {
super();
this.#secret = secret;
this.#authenticated = false;
this.#ws = new WebSocket(url);
this.#ws.addEventListener("message", this.#handle_ws_message.bind(this));
this.#ws.addEventListener("close", this.#handle_ws_close.bind(this));
}
#handle_ws_message(event) {
if (!this.#authenticated) {
this.#ws.send(challengeResponse(event.data, this.#secret));
this.#authenticated = true;
return;
}
let config;
try {
config = JSON.parse(event.data);
} catch (e) {
this.#ws.close();
throw (e);
}
this.dispatchEvent(new CustomEvent("config", {detail: config}));
}
#handle_ws_close() {
this.dispatchEvent(new CustomEvent("close"));
}
send(message) {
this.#ws.send(message);
}
}

232
webdata/app/touchpad.mjs Normal file
View file

@ -0,0 +1,232 @@
/*
* Copyright (c) 2018-2019, 2023 Unrud <unrud@outlook.com>
*
* This file is part of Remote-Touchpad.
*
* Remote-Touchpad 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.
*
* Remote-Touchpad 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 Remote-Touchpad. If not, see <http://www.gnu.org/licenses/>.
*/
import {POINTER_BUTTON_LEFT, POINTER_BUTTON_MIDDLE, POINTER_BUTTON_RIGHT} from "./inputcontroller.mjs";
// [1 Touch, 2 Touches, 3 Touches] (as pixel)
const TOUCH_MOVE_THRESHOLD = [10, 15, 15];
// Max time between consecutive touches for clicking or dragging (as milliseconds)
const TOUCH_TIMEOUT = 250;
// [[pixel/second, multiplicator], ...]
const POINTER_ACCELERATION = [
[0, 0],
[87, 1],
[173, 1],
[553, 2],
];
const copyTouch = (touch, timeStamp) => ({
identifier: touch.identifier,
pageX: touch.pageX,
pageXStart: touch.pageX,
pageY: touch.pageY,
pageYStart: touch.pageY,
timeStamp: timeStamp,
});
const calculateAccelerationMult = (speed) => {
for (let i = 0; i < POINTER_ACCELERATION.length; i += 1) {
const s2 = POINTER_ACCELERATION[i][0];
const a2 = POINTER_ACCELERATION[i][1];
if (s2 <= speed) {
continue;
}
if (i == 0) {
return a2;
}
const s1 = POINTER_ACCELERATION[i - 1][0];
const a1 = POINTER_ACCELERATION[i - 1][1];
return ((speed - s1) / (s2 - s1)) * (a2 - a1) + a1;
}
if (POINTER_ACCELERATION.length > 0) {
return POINTER_ACCELERATION[POINTER_ACCELERATION.length - 1][1];
}
return 1;
};
export default class Touchpad {
#moveSpeed = 1;
#scrollSpeed = 1;
#moved = false;
#startTimeStamp = 0;
#lastEndTimeStamp = 0;
#releasedCount = 0;
#ongoingTouches = [];
#dragging = false;
#draggingTimeout = null;
#inputController;
#checkAllowedCallback;
constructor(inputController, checkAllowedCallback) {
this.#inputController = inputController;
this.#checkAllowedCallback = checkAllowedCallback;
document.addEventListener("touchstart", this.#handleTouchstart.bind(this));
document.addEventListener("touchend", this.#handleTouchend.bind(this));
document.addEventListener("touchcancel", this.#handleTouchend.bind(this));
document.addEventListener("touchmove", this.#handleTouchmove.bind(this));
}
configure(config) {
this.#moveSpeed = config.moveSpeed;
this.#scrollSpeed = config.scrollSpeed;
}
#ongoingTouchIndexById(idToFind) {
for (let i = 0; i < this.#ongoingTouches.length; i += 1) {
if (this.#ongoingTouches[i].identifier == idToFind) {
return i;
}
}
return -1;
}
#handleDraggingTimeout() {
this.#draggingTimeout = null;
this.#inputController.pointerButton(POINTER_BUTTON_LEFT, false);
}
#handleTouchstart(event) {
// Might get called multiple times for the same touches
if (this.#ongoingTouches.length == 0) {
this.#startTimeStamp = event.timeStamp;
this.#moved = false;
}
const touches = event.changedTouches;
let foundTouch = false;
for (let i = 0; i < touches.length; i += 1) {
if (this.#ongoingTouches.length == 0 &&
!this.#checkAllowedCallback(touches[i].target)) {
continue;
}
foundTouch = true;
const touch = copyTouch(touches[i], event.timeStamp);
const idx = this.#ongoingTouchIndexById(touch.identifier);
if (idx < 0) {
this.#ongoingTouches.push(touch);
} else {
this.#ongoingTouches[idx] = touch;
}
}
if (!foundTouch) {
return;
}
event.preventDefault();
this.#lastEndTimeStamp = 0;
if (this.#draggingTimeout != null) {
clearTimeout(this.#draggingTimeout);
this.#draggingTimeout = null;
this.#dragging = true;
}
this.#inputController.pointerScroll(0, 0, true);
}
#handleTouchend(event) {
const touches = event.changedTouches;
let foundTouch = false;
for (let i = 0; i < touches.length; i += 1) {
const idx = this.#ongoingTouchIndexById(touches[i].identifier);
if (idx < 0) {
continue;
}
foundTouch = true;
this.#ongoingTouches.splice(idx, 1);
this.#releasedCount += 1;
}
if (!foundTouch) {
return;
}
event.preventDefault();
this.#lastEndTimeStamp = event.timeStamp;
this.#inputController.pointerScroll(0, 0, true);
if (this.#releasedCount > TOUCH_MOVE_THRESHOLD.length) {
this.#moved = true;
}
if (this.#ongoingTouches.length == 0 && this.#releasedCount >= 1) {
if (this.#dragging) {
this.#dragging = false;
this.#inputController.pointerButton(POINTER_BUTTON_LEFT, false);
}
if (!this.#moved && event.timeStamp - this.#startTimeStamp < TOUCH_TIMEOUT) {
let button = 0;
if (this.#releasedCount == 1) {
button = POINTER_BUTTON_LEFT;
} else if (this.#releasedCount == 2) {
button = POINTER_BUTTON_RIGHT;
} else if (this.#releasedCount == 3) {
button = POINTER_BUTTON_MIDDLE;
}
this.#inputController.pointerButton(button, true);
if (button == POINTER_BUTTON_LEFT) {
this.#draggingTimeout = setTimeout(
this.#handleDraggingTimeout.bind(this), TOUCH_TIMEOUT);
} else {
this.#inputController.pointerButton(button, false);
}
}
this.#releasedCount = 0;
}
}
#handleTouchmove(event) {
let sumX = 0;
let sumY = 0;
const touches = event.changedTouches;
let foundTouch = false;
for (let i = 0; i < touches.length; i += 1) {
const idx = this.#ongoingTouchIndexById(touches[i].identifier);
if (idx < 0) {
continue;
}
foundTouch = true;
if (!this.#moved) {
const dist = Math.sqrt(
Math.pow(touches[i].pageX - this.#ongoingTouches[idx].pageXStart, 2) +
Math.pow(touches[i].pageY - this.#ongoingTouches[idx].pageYStart, 2)
);
if (this.#ongoingTouches.length > TOUCH_MOVE_THRESHOLD.length ||
dist > TOUCH_MOVE_THRESHOLD[this.#ongoingTouches.length - 1] ||
event.timeStamp - this.#startTimeStamp >= TOUCH_TIMEOUT) {
this.#moved = true;
}
}
const dx = touches[i].pageX - this.#ongoingTouches[idx].pageX;
const dy = touches[i].pageY - this.#ongoingTouches[idx].pageY;
const timeDelta = event.timeStamp - this.#ongoingTouches[idx].timeStamp;
sumX += dx * calculateAccelerationMult(Math.abs(dx) / timeDelta * 1000);
sumY += dy * calculateAccelerationMult(Math.abs(dy) / timeDelta * 1000);
this.#ongoingTouches[idx].pageX = touches[i].pageX;
this.#ongoingTouches[idx].pageY = touches[i].pageY;
this.#ongoingTouches[idx].timeStamp = event.timeStamp;
}
if (!foundTouch) {
return;
}
event.preventDefault();
if (this.#moved && event.timeStamp - this.#lastEndTimeStamp >= TOUCH_TIMEOUT) {
if (this.#ongoingTouches.length == 1 || this.#dragging) {
this.#inputController.pointerMove(
sumX * this.#moveSpeed, sumY * this.#moveSpeed);
} else if (this.#ongoingTouches.length == 2) {
this.#inputController.pointerScroll(
-sumX * this.#scrollSpeed, -sumY * this.#scrollSpeed, false);
}
}
}
}

173
webdata/app/ui.mjs Normal file
View file

@ -0,0 +1,173 @@
/*
* Copyright (c) 2018-2019, 2023 Unrud <unrud@outlook.com>
*
* This file is part of Remote-Touchpad.
*
* Remote-Touchpad 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.
*
* Remote-Touchpad 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 Remote-Touchpad. If not, see <http://www.gnu.org/licenses/>.
*/
import Keyboard from "./keyboard.mjs";
import Mouse from "./mouse.mjs";
import Touchpad from "./touchpad.mjs";
import * as compat from "./compat.mjs";
const scenes = document.querySelectorAll("body > .scene");
const openingScene = document.getElementById("opening");
const closedScene = document.getElementById("closed");
const padScene = document.getElementById("pad");
const keysScene = document.getElementById("keys");
const keysPages = keysScene.querySelectorAll(":scope > .page");
const textInputScene = document.getElementById("text-input");
const textInput = textInputScene.querySelector("textarea");
const mouseScene = document.getElementById("mouse");
const sendText = document.getElementById("send-text");
export default class UI {
#activeScene = null;
#keysActiveName = "";
#ready = false;
#closed = false;
#inputController;
#mouse;
#keyboard;
#touchpad;
constructor(inputController) {
this.#inputController = inputController;
this.#mouse = new Mouse(inputController, mouseScene);
this.#keyboard = new Keyboard(inputController,
() => this.#activeScene?.classList.contains("keyboard-input"));
this.#touchpad = new Touchpad(inputController,
(target) => target.classList.contains("touch-input"));
document.addEventListener("mousedown", this.#handleMousedown.bind(this));
textInput.addEventListener("input", this.#handleTextInput.bind(this));
sendText.addEventListener("click", this.#handleSendText.bind(this));
window.addEventListener("popstate", () => { this.#update(); });
compat.addFullscreenchangeEventListener(() => { this.#update(); });
compat.addPointerlockchangeEventListener(() => { this.#update(); });
this.#update();
}
configure(config) {
this.#mouse.configure(config);
this.#keyboard.configure(config);
this.#touchpad.configure(config);
if (!this.#closed) {
this.#ready = true;
}
this.#update();
}
close() {
this.#ready = false;
this.#closed = true;
this.#update();
}
#handleMousedown(event) {
if (this.#activeScene != mouseScene && event.buttons == 1 &&
event.target.classList.contains("mouse-input")) {
compat.requestPointerLock(mouseScene);
}
}
#handleTextInput() {
sessionStorage.setItem("text-input", textInput.value);
}
#handleSendText() {
if (textInput.value) {
// normalize line endings
const text = textInput.value.replace(/\r\n?/g, "\n");
this.#inputController.keyboardText(text);
textInput.value = "";
this.#handleTextInput();
}
history.back();
}
#showScene(scene) {
this.#activeScene = scene;
if (compat.fullscreenElement() && !scene.classList.contains("allow-fullscreen")) {
compat.exitFullscreen();
}
if (compat.pointerLockElement() && this.#activeScene != mouseScene) {
compat.exitPointerLock();
}
textInput.value = "";
for (const otherScene of scenes) {
otherScene.classList.toggle("hidden", otherScene != scene);
}
}
setKeysPage(index, relative = false) {
if (relative) {
for (let i = 0; i < keysPages.length && keysPages[i].classList.contains("hidden");
i += 1, index += 1);
}
index = ((index % keysPages.length) + keysPages.length) % keysPages.length;
sessionStorage.setItem(this.#keysActiveName, index);
for (let i = 0; i < keysPages.length; i += 1) {
keysPages[i].classList.toggle("hidden", i != index);
}
}
showKeys(name = "", defaultPageIndex = 0) {
this.#showScene(keysScene);
this.#keysActiveName = "keys" + (name ? ":" + name : "");
let pageIndex = parseInt(sessionStorage.getItem(this.#keysActiveName));
if (isNaN(pageIndex)) {
pageIndex = defaultPageIndex;
}
this.setKeysPage(pageIndex);
if (history.state != this.#keysActiveName) {
history.pushState(this.#keysActiveName, "");
}
}
showTextInput() {
this.#showScene(textInputScene);
textInput.value = sessionStorage.getItem("text-input") || "";
textInput.focus();
if (history.state != "text-input") {
history.pushState("text-input", "");
}
}
toggleFullscreen() {
if (compat.fullscreenElement()) {
compat.exitFullscreen();
} else {
compat.requestFullscreen(document.documentElement, {navigationUI: "hide"});
}
}
#update() {
const fullscreenEnabled = compat.fullscreenEnabled();
for (const element of document.querySelectorAll(".visble-if-fullscreen-enabled")) {
element.classList.toggle("hidden", !fullscreenEnabled);
}
if (!this.#ready) {
this.#showScene(this.#closed ? closedScene : openingScene);
} else if (compat.pointerLockElement()) {
this.#showScene(mouseScene);
} else if ((history.state || "").split(":")[0] == "keys") {
this.showKeys(history.state.substr("keys:".length));
} else if (history.state == "text-input") {
this.showTextInput();
} else {
this.#showScene(padScene);
}
}
}

View file

@ -1,635 +0,0 @@
"use strict";
/*
* Copyright (c) 2018-2019, 2023 Unrud <unrud@outlook.com>
*
* This file is part of Remote-Touchpad.
*
* Remote-Touchpad 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.
*
* Remote-Touchpad 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 Remote-Touchpad. If not, see <http://www.gnu.org/licenses/>.
*/
(() => {
// [1 Touch, 2 Touches, 3 Touches] (as pixel)
const TOUCH_MOVE_THRESHOLD = [10, 15, 15];
// Max time between consecutive touches for clicking or dragging (as milliseconds)
const TOUCH_TIMEOUT = 250;
// [[pixel/second, multiplicator], ...]
const POINTER_ACCELERATION = [
[0, 0],
[87, 1],
[173, 1],
[553, 2],
];
const POINTER_BUTTON_LEFT = 0;
const POINTER_BUTTON_RIGHT = 1;
const POINTER_BUTTON_MIDDLE = 2;
const KEY_VOLUME_MUTE = 0;
const KEY_VOLUME_DOWN = 1;
const KEY_VOLUME_UP = 2;
const KEY_MEDIA_PLAY_PAUSE = 3;
const KEY_MEDIA_PREV_TRACK = 4;
const KEY_MEDIA_NEXT_TRACK = 5;
const KEY_BROWSER_BACK = 6;
const KEY_BROWSER_FORWARD = 7;
const KEY_SUPER = 8;
const KEY_LEFT = 9;
const KEY_RIGHT = 10;
const KEY_UP = 11;
const KEY_DOWN = 12;
const KEY_HOME = 13;
const KEY_END = 14;
const KEY_BACK_SPACE = 15;
const KEY_DELETE = 16;
const KEY_RETURN = 17;
const wsURL = new URL("ws", location.href);
wsURL.protocol = wsURL.protocol == "http:" ? "ws:" : "wss:";
const ws = new WebSocket(wsURL);
let config = null;
const compat = (() => {
const compat = {};
compat.fullscreenEnabled = () => {
return (document.fullscreenEnabled ||
document.webkitFullscreenEnabled ||
false);
};
compat.requestFullscreen = (element, options) => {
if (element.requestFullscreen) {
element.requestFullscreen(options);
} else if (element.webkitRequestFullscreen) {
element.webkitRequestFullscreen(options);
}
};
compat.exitFullscreen = () => {
if (document.exitFullscreen) {
document.exitFullscreen();
} else if (document.webkitExitFullscreen) {
document.webkitExitFullscreen();
}
};
compat.fullscreenElement = () => {
return (document.fullscreenElement ||
document.webkitFullscreenElement ||
null);
};
compat.addFullscreenchangeEventListener = (listener) => {
if ("onfullscreenchange" in document) {
document.addEventListener("fullscreenchange", listener);
} else if ("onwebkitfullscreenchange" in document) {
document.addEventListener("webkitfullscreenchange", listener);
}
};
compat.requestPointerLock = (element) => {
if (element.requestPointerLock) {
element.requestPointerLock();
}
};
compat.exitPointerLock = () => {
if (document.exitPointerLock) {
document.exitPointerLock();
}
};
compat.pointerLockElement = () => {
return document.pointerLockElement || null;
};
compat.addPointerlockchangeEventListener = (listener) => {
if ("onpointerlockchange" in document) {
document.addEventListener("pointerlockchange", listener);
}
};
return compat;
})();
const controller = (() => {
const controller = {};
let moveXSum = 0;
let moveYSum = 0;
let scrollHSum = 0;
let scrollVSum = 0;
let scrolling = false;
let scrollFinish = false;
let updateTimeoutActive = false;
const startUpdate = (fromTimeout) => {
if (updateTimeoutActive && !fromTimeout) {
return;
}
updateTimeoutActive = false;
let finished = true;
const xInt = Math.trunc(moveXSum);
const yInt = Math.trunc(moveYSum);
if (xInt != 0 || yInt != 0) {
ws.send("m" + xInt + ";" + yInt);
moveXSum -= xInt;
moveYSum -= yInt;
finished = false;
}
const hInt = Math.trunc(scrollHSum);
const vInt = Math.trunc(scrollVSum);
if (hInt != 0 || vInt != 0) {
ws.send((scrollFinish ? "S" : "s") + hInt + ";" + vInt);
scrollHSum -= hInt;
scrollVSum -= vInt;
scrolling = !scrollFinish;
scrollFinish = false;
finished = false;
} else if (scrollFinish && scrolling) {
ws.send("S");
scrolling = false;
scrollFinish = false;
}
updateTimeoutActive = !finished && config.updateRate > 0;
if (updateTimeoutActive) {
setTimeout(startUpdate, 1000/config.updateRate, true);
}
};
controller.pointerMove = (deltaX, deltaY) => {
moveXSum += deltaX;
moveYSum += deltaY;
startUpdate();
};
controller.pointerScroll = (deltaHorizontal, deltaVertical, finish) => {
scrollHSum += deltaHorizontal;
scrollVSum += deltaVertical;
scrollFinish |= finish;
startUpdate();
};
controller.pointerButton = (button, press) => {
ws.send("b" + button + ";" + (press ? 1 : 0));
};
controller.keyboardKey = (key) => {
ws.send("k" + key);
};
controller.keyboardText = (text) => {
ws.send("t" + text);
};
return controller;
})();
const touchpad = (() => {
const touchpad = {};
let moved = false;
let startTimeStamp = 0;
let lastEndTimeStamp = 0;
let releasedCount = 0;
let ongoingTouches = [];
let dragging = false;
let draggingTimeout = null;
const copyTouch = (touch, timeStamp) => {
return {
identifier: touch.identifier,
pageX: touch.pageX,
pageXStart: touch.pageX,
pageY: touch.pageY,
pageYStart: touch.pageY,
timeStamp: timeStamp,
};
};
const ongoingTouchIndexById = (idToFind) => {
for (let i = 0; i < ongoingTouches.length; i += 1) {
if (ongoingTouches[i].identifier == idToFind) {
return i;
}
}
return -1;
};
const calculateAccelerationMult = (speed) => {
for (let i = 0; i < POINTER_ACCELERATION.length; i += 1) {
const s2 = POINTER_ACCELERATION[i][0];
const a2 = POINTER_ACCELERATION[i][1];
if (s2 <= speed) {
continue;
}
if (i == 0) {
return a2;
}
const s1 = POINTER_ACCELERATION[i - 1][0];
const a1 = POINTER_ACCELERATION[i - 1][1];
return ((speed - s1) / (s2 - s1)) * (a2 - a1) + a1;
}
if (POINTER_ACCELERATION.length > 0) {
return POINTER_ACCELERATION[POINTER_ACCELERATION.length - 1][1];
}
return 1;
};
const onDraggingTimeout = () => {
draggingTimeout = null;
controller.pointerButton(POINTER_BUTTON_LEFT, false);
};
touchpad.handleTouchstart = (evt) => {
// Might get called multiple times for the same touches
if (ongoingTouches.length == 0) {
startTimeStamp = evt.timeStamp;
moved = false;
}
const touches = evt.changedTouches;
let foundTouch = false;
for (let i = 0; i < touches.length; i += 1) {
if (ongoingTouches.length == 0 && !touches[i].target.classList.contains("touch-input")) {
continue;
}
foundTouch = true;
const touch = copyTouch(touches[i], evt.timeStamp);
const idx = ongoingTouchIndexById(touch.identifier);
if (idx < 0) {
ongoingTouches.push(touch);
} else {
ongoingTouches[idx] = touch;
}
}
if (!foundTouch) {
return;
}
evt.preventDefault();
lastEndTimeStamp = 0;
if (draggingTimeout != null) {
clearTimeout(draggingTimeout);
draggingTimeout = null;
dragging = true;
}
controller.pointerScroll(0, 0, true);
};
touchpad.handleTouchend = touchpad.handleTouchcancel = (evt) => {
const touches = evt.changedTouches;
let foundTouch = false;
for (let i = 0; i < touches.length; i += 1) {
const idx = ongoingTouchIndexById(touches[i].identifier);
if (idx < 0) {
continue;
}
foundTouch = true;
ongoingTouches.splice(idx, 1);
releasedCount += 1;
}
if (!foundTouch) {
return;
}
evt.preventDefault();
lastEndTimeStamp = evt.timeStamp;
controller.pointerScroll(0, 0, true);
if (releasedCount > TOUCH_MOVE_THRESHOLD.length) {
moved = true;
}
if (ongoingTouches.length == 0 && releasedCount >= 1) {
if (dragging) {
dragging = false;
controller.pointerButton(POINTER_BUTTON_LEFT, false);
}
if (!moved && evt.timeStamp - startTimeStamp < TOUCH_TIMEOUT) {
let button = 0;
if (releasedCount == 1) {
button = POINTER_BUTTON_LEFT;
} else if (releasedCount == 2) {
button = POINTER_BUTTON_RIGHT;
} else if (releasedCount == 3) {
button = POINTER_BUTTON_MIDDLE;
}
controller.pointerButton(button, true);
if (button == POINTER_BUTTON_LEFT) {
draggingTimeout = setTimeout(onDraggingTimeout, TOUCH_TIMEOUT);
} else {
controller.pointerButton(button, false);
}
}
releasedCount = 0;
}
};
touchpad.handleTouchmove = (evt) => {
let sumX = 0;
let sumY = 0;
const touches = evt.changedTouches;
let foundTouch = false;
for (let i = 0; i < touches.length; i += 1) {
const idx = ongoingTouchIndexById(touches[i].identifier);
if (idx < 0) {
continue;
}
foundTouch = true;
if (!moved) {
const dist = Math.sqrt(Math.pow(touches[i].pageX - ongoingTouches[idx].pageXStart, 2) +
Math.pow(touches[i].pageY - ongoingTouches[idx].pageYStart, 2));
if (ongoingTouches.length > TOUCH_MOVE_THRESHOLD.length ||
dist > TOUCH_MOVE_THRESHOLD[ongoingTouches.length - 1] ||
evt.timeStamp - startTimeStamp >= TOUCH_TIMEOUT) {
moved = true;
}
}
const dx = touches[i].pageX - ongoingTouches[idx].pageX;
const dy = touches[i].pageY - ongoingTouches[idx].pageY;
const timeDelta = evt.timeStamp - ongoingTouches[idx].timeStamp;
sumX += dx * calculateAccelerationMult(Math.abs(dx) / timeDelta * 1000);
sumY += dy * calculateAccelerationMult(Math.abs(dy) / timeDelta * 1000);
ongoingTouches[idx].pageX = touches[i].pageX;
ongoingTouches[idx].pageY = touches[i].pageY;
ongoingTouches[idx].timeStamp = evt.timeStamp;
}
if (!foundTouch) {
return;
}
evt.preventDefault();
if (moved && evt.timeStamp - lastEndTimeStamp >= TOUCH_TIMEOUT) {
if (ongoingTouches.length == 1 || dragging) {
controller.pointerMove(sumX*config.moveSpeed, sumY*config.moveSpeed);
} else if (ongoingTouches.length == 2) {
controller.pointerScroll(-sumX*config.scrollSpeed, -sumY*config.scrollSpeed, false);
}
}
};
return touchpad;
})();
const keyboard = (() => {
const keyboard = {};
keyboard.handleKeydown = (evt) => {
if (evt.ctrlKey || evt.altKey || evt.isComposing) {
return;
}
let key = null;
if (evt.key == "OS" || evt.key == "Super" || evt.key == "Meta") {
key = KEY_SUPER;
} else if (evt.key == "Backspace") {
key = KEY_BACK_SPACE;
} else if (evt.key == "Enter") {
key = KEY_RETURN;
} else if (evt.key == "Delete") {
key = KEY_DELETE;
} else if (evt.key == "Home") {
key = KEY_HOME;
} else if (evt.key == "End") {
key = KEY_END;
} else if (evt.key == "Left" || evt.key == "ArrowLeft") {
key = KEY_LEFT;
} else if (evt.key == "Right" || evt.key == "ArrowRight") {
key = KEY_RIGHT;
} else if (evt.key == "Up" || evt.key == "ArrowUp") {
key = KEY_UP;
} else if (evt.key == "Down" || evt.key == "ArrowDown") {
key = KEY_DOWN;
}
if (key != null) {
if (!evt.shiftKey) {
evt.preventDefault();
controller.keyboardKey(key);
}
} else if (evt.key.length == 1) {
evt.preventDefault();
controller.keyboardText(evt.key);
}
};
return keyboard;
})();
const mouse = (() => {
const mouse = {};
let buttons = 0;
const updateButtons = (newButtons) => {
for (let button = 0; button < 3; button += 1) {
const flag = 1 << button;
if ((newButtons&flag) != (buttons&flag)) {
controller.pointerButton(button, newButtons&flag);
}
}
buttons = newButtons;
};
mouse.handleMousedown = mouse.handleMouseup = (evt) => {
updateButtons(evt.buttons);
};
mouse.handleMousemove = (evt) => {
controller.pointerMove(evt.movementX*config.mouseMoveSpeed, evt.movementY*config.mouseMoveSpeed);
};
mouse.handleWheel = (evt) => {
if (evt.deltaMode == WheelEvent.DOM_DELTA_PIXEL) {
controller.pointerScroll(evt.deltaX*config.mouseScrollSpeed, evt.deltaY*config.mouseScrollSpeed, true);
} else if (evt.deltaMode == WheelEvent.DOM_DELTA_LINE) {
controller.pointerScroll(evt.deltaX*20*config.mouseScrollSpeed, evt.deltaY*20*config.mouseScrollSpeed, true);
}
};
return mouse;
})();
const challengeResponse = (message) => {
const shaObj = new window.jsSHA("SHA-256", "TEXT");
shaObj.setHMACKey(message, "TEXT");
shaObj.update(window.location.hash.substr(1));
return btoa(shaObj.getHMAC("BYTES"));
};
const scenes = document.querySelectorAll("body > .scene");
const openingScene = document.getElementById("opening");
const closedScene = document.getElementById("closed");
const padScene = document.getElementById("pad");
const keysScene = document.getElementById("keys");
const keysPages = keysScene.querySelectorAll(":scope > .page");
const textInputScene = document.getElementById("text-input");
const textInput = textInputScene.querySelector("textarea");
const mouseScene = document.getElementById("mouse");
let ready = false;
let closed = false;
let activeScene = null;
let keysActiveName = "";
const showScene = (scene) => {
activeScene = scene;
if (compat.fullscreenElement() && !scene.classList.contains("allow-fullscreen")) {
compat.exitFullscreen();
}
if (compat.pointerLockElement() && activeScene != mouseScene) {
compat.exitPointerLock();
}
textInput.value = "";
for (const otherScene of scenes) {
otherScene.classList.toggle("hidden", otherScene != scene);
}
};
const setKeysPage = (index, relative=false) => {
if (relative) {
for (let i = 0; i < keysPages.length && keysPages[i].classList.contains("hidden"); i += 1, index += 1);
}
index = ((index % keysPages.length) + keysPages.length) % keysPages.length;
sessionStorage.setItem(keysActiveName, index);
for (let i = 0; i < keysPages.length; i += 1) {
keysPages[i].classList.toggle("hidden", i != index);
}
};
const showKeys = (name = "", defaultIndex = 0) => {
showScene(keysScene);
keysActiveName = "keys" + (name ? ":" + name : "");
let keysIndex = parseInt(sessionStorage.getItem(keysActiveName));
if (isNaN(keysIndex)) {
keysIndex = defaultIndex;
}
setKeysPage(keysIndex);
if (history.state != keysActiveName) {
history.pushState(keysActiveName, "");
}
};
const showTextInput = () => {
showScene(textInputScene);
textInput.value = sessionStorage.getItem("text-input") || "";
textInput.focus();
if (history.state != "text-input") {
history.pushState("text-input", "");
}
};
textInput.oninput = () => {
sessionStorage.setItem("text-input", textInput.value);
};
const updateUI = () => {
if (!ready) {
showScene(closed ? closedScene : openingScene);
} else if (compat.pointerLockElement()) {
showScene(mouseScene);
} else if ((history.state || "").split(":")[0] == "keys") {
showKeys(history.state.substr("keys:".length));
} else if (history.state == "text-input") {
showTextInput();
} else {
showScene(padScene);
}
};
let authenticated = false;
ws.onmessage = (evt) => {
if (!authenticated) {
ws.send(challengeResponse(evt.data));
authenticated = true;
return;
}
try {
config = JSON.parse(evt.data);
} catch (e) {
ws.close();
throw (e);
}
ready = true;
updateUI();
};
ws.onclose = () => {
ready = false;
closed = true;
updateUI();
};
compat.addFullscreenchangeEventListener(() => {
updateUI();
});
for (const element of document.querySelectorAll(".visble-if-fullscreen-enabled")) {
element.classList.toggle("hidden", !compat.fullscreenEnabled());
}
const toggleFullscreen = () => {
if (compat.fullscreenElement()) {
compat.exitFullscreen();
} else {
compat.requestFullscreen(document.documentElement, {navigationUI: "hide"});
}
};
document.getElementById("send-text").addEventListener("click", () => {
if (textInput.value) {
// normalize line endings
controller.keyboardText(textInput.value.replace(/\r\n?/g, "\n"));
textInput.value = "";
textInput.oninput();
}
history.back();
});
window.addEventListener("popstate", () => {
updateUI();
});
document.addEventListener("touchstart", touchpad.handleTouchstart);
document.addEventListener("touchend", touchpad.handleTouchend);
document.addEventListener("touchcancel", touchpad.handleTouchcancel);
document.addEventListener("touchmove", touchpad.handleTouchmove);
document.addEventListener("keydown", (evt) => {
if (activeScene && activeScene.classList.contains("keyboard-input")) {
keyboard.handleKeydown(evt);
}
});
compat.addPointerlockchangeEventListener(() => {
updateUI();
});
document.addEventListener("mousedown", (event) => {
if (activeScene != mouseScene && event.buttons == 1 && event.target.classList.contains("mouse-input")) {
compat.requestPointerLock(mouseScene);
}
});
for (const type of ["touchstart", "touchend", "touchcancel", "touchmove"]) {
mouseScene.addEventListener(type, (evt) => {
evt.preventDefault();
});
}
mouseScene.addEventListener("mousedown", mouse.handleMousedown);
mouseScene.addEventListener("mouseup", mouse.handleMouseup);
mouseScene.addEventListener("mousemove", mouse.handleMousemove);
mouseScene.addEventListener("wheel", mouse.handleWheel);
window.app = {
KEY_VOLUME_MUTE, KEY_VOLUME_DOWN, KEY_VOLUME_UP, KEY_MEDIA_PLAY_PAUSE,
KEY_MEDIA_PREV_TRACK, KEY_MEDIA_NEXT_TRACK, KEY_BROWSER_BACK, KEY_BROWSER_FORWARD,
KEY_SUPER, KEY_LEFT, KEY_RIGHT, KEY_UP, KEY_DOWN, KEY_HOME, KEY_END, KEY_BACK_SPACE,
KEY_DELETE, KEY_RETURN,
showKeys, showTextInput, setKeysPage, toggleFullscreen,
key: controller.keyboardKey,
text: controller.keyboardText,
};
updateUI();
})();

View file

@ -2,8 +2,7 @@
<html lang="en">
<meta name="viewport" content="width=device-width, initial-scale=1, user-scalable=0">
<script src="fn.js" defer></script>
<script src="sha256.js"></script>
<script src="app/main.mjs" type="module"></script>
<title>Remote Touchpad</title>
<link href="main.css" media="screen" rel="stylesheet">
<link href="icon.png" type="image/png" rel="icon">

View file

@ -1,27 +0,0 @@
/*
A JavaScript implementation of the SHA family of hashes, as
defined in FIPS PUB 180-4 and FIPS PUB 202, as well as the corresponding
HMAC implementation as defined in FIPS PUB 198a
Copyright 2008-2018 Brian Turek, 1998-2009 Paul Johnston & Contributors
Distributed under the BSD License
See http://caligatio.github.com/jsSHA/ for more information
*/
'use strict';(function(I){function w(c,a,d){var l=0,b=[],g=0,f,n,k,e,h,q,y,p,m=!1,t=[],r=[],u,z=!1;d=d||{};f=d.encoding||"UTF8";u=d.numRounds||1;if(u!==parseInt(u,10)||1>u)throw Error("numRounds must a integer >= 1");if(0===c.lastIndexOf("SHA-",0))if(q=function(b,a){return A(b,a,c)},y=function(b,a,l,f){var g,e;if("SHA-224"===c||"SHA-256"===c)g=(a+65>>>9<<4)+15,e=16;else throw Error("Unexpected error in SHA-2 implementation");for(;b.length<=g;)b.push(0);b[a>>>5]|=128<<24-a%32;a=a+l;b[g]=a&4294967295;
b[g-1]=a/4294967296|0;l=b.length;for(a=0;a<l;a+=e)f=A(b.slice(a,a+e),f,c);if("SHA-224"===c)b=[f[0],f[1],f[2],f[3],f[4],f[5],f[6]];else if("SHA-256"===c)b=f;else throw Error("Unexpected error in SHA-2 implementation");return b},p=function(b){return b.slice()},"SHA-224"===c)h=512,e=224;else if("SHA-256"===c)h=512,e=256;else throw Error("Chosen SHA variant is not supported");else throw Error("Chosen SHA variant is not supported");k=B(a,f);n=x(c);this.setHMACKey=function(b,a,g){var e;if(!0===m)throw Error("HMAC key already set");
if(!0===z)throw Error("Cannot set HMAC key after calling update");f=(g||{}).encoding||"UTF8";a=B(a,f)(b);b=a.binLen;a=a.value;e=h>>>3;g=e/4-1;if(e<b/8){for(a=y(a,b,0,x(c));a.length<=g;)a.push(0);a[g]&=4294967040}else if(e>b/8){for(;a.length<=g;)a.push(0);a[g]&=4294967040}for(b=0;b<=g;b+=1)t[b]=a[b]^909522486,r[b]=a[b]^1549556828;n=q(t,n);l=h;m=!0};this.update=function(a){var c,f,e,d=0,p=h>>>5;c=k(a,b,g);a=c.binLen;f=c.value;c=a>>>5;for(e=0;e<c;e+=p)d+h<=a&&(n=q(f.slice(e,e+p),n),d+=h);l+=d;b=f.slice(d>>>
5);g=a%h;z=!0};this.getHash=function(a,f){var d,h,k,q;if(!0===m)throw Error("Cannot call getHash after setting HMAC key");k=C(f);switch(a){case "HEX":d=function(a){return D(a,e,k)};break;case "B64":d=function(a){return E(a,e,k)};break;case "BYTES":d=function(a){return F(a,e)};break;case "ARRAYBUFFER":try{h=new ArrayBuffer(0)}catch(v){throw Error("ARRAYBUFFER not supported by this environment");}d=function(a){return G(a,e)};break;default:throw Error("format must be HEX, B64, BYTES, or ARRAYBUFFER");
}q=y(b.slice(),g,l,p(n));for(h=1;h<u;h+=1)q=y(q,e,0,x(c));return d(q)};this.getHMAC=function(a,f){var d,k,t,u;if(!1===m)throw Error("Cannot call getHMAC without first setting HMAC key");t=C(f);switch(a){case "HEX":d=function(a){return D(a,e,t)};break;case "B64":d=function(a){return E(a,e,t)};break;case "BYTES":d=function(a){return F(a,e)};break;case "ARRAYBUFFER":try{d=new ArrayBuffer(0)}catch(v){throw Error("ARRAYBUFFER not supported by this environment");}d=function(a){return G(a,e)};break;default:throw Error("outputFormat must be HEX, B64, BYTES, or ARRAYBUFFER");
}k=y(b.slice(),g,l,p(n));u=q(r,x(c));u=y(k,e,h,u);return d(u)}}function m(){}function D(c,a,d){var l="";a/=8;var b,g;for(b=0;b<a;b+=1)g=c[b>>>2]>>>8*(3+b%4*-1),l+="0123456789abcdef".charAt(g>>>4&15)+"0123456789abcdef".charAt(g&15);return d.outputUpper?l.toUpperCase():l}function E(c,a,d){var l="",b=a/8,g,f,n;for(g=0;g<b;g+=3)for(f=g+1<b?c[g+1>>>2]:0,n=g+2<b?c[g+2>>>2]:0,n=(c[g>>>2]>>>8*(3+g%4*-1)&255)<<16|(f>>>8*(3+(g+1)%4*-1)&255)<<8|n>>>8*(3+(g+2)%4*-1)&255,f=0;4>f;f+=1)8*g+6*f<=a?l+="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/".charAt(n>>>
6*(3-f)&63):l+=d.b64Pad;return l}function F(c,a){var d="",l=a/8,b,g;for(b=0;b<l;b+=1)g=c[b>>>2]>>>8*(3+b%4*-1)&255,d+=String.fromCharCode(g);return d}function G(c,a){var d=a/8,l,b=new ArrayBuffer(d),g;g=new Uint8Array(b);for(l=0;l<d;l+=1)g[l]=c[l>>>2]>>>8*(3+l%4*-1)&255;return b}function C(c){var a={outputUpper:!1,b64Pad:"=",shakeLen:-1};c=c||{};a.outputUpper=c.outputUpper||!1;!0===c.hasOwnProperty("b64Pad")&&(a.b64Pad=c.b64Pad);if("boolean"!==typeof a.outputUpper)throw Error("Invalid outputUpper formatting option");
if("string"!==typeof a.b64Pad)throw Error("Invalid b64Pad formatting option");return a}function B(c,a){var d;switch(a){case "UTF8":case "UTF16BE":case "UTF16LE":break;default:throw Error("encoding must be UTF8, UTF16BE, or UTF16LE");}switch(c){case "HEX":d=function(a,b,c){var f=a.length,d,k,e,h,q;if(0!==f%2)throw Error("String of HEX type must be in byte increments");b=b||[0];c=c||0;q=c>>>3;for(d=0;d<f;d+=2){k=parseInt(a.substr(d,2),16);if(isNaN(k))throw Error("String of HEX type contains invalid characters");
h=(d>>>1)+q;for(e=h>>>2;b.length<=e;)b.push(0);b[e]|=k<<8*(3+h%4*-1)}return{value:b,binLen:4*f+c}};break;case "TEXT":d=function(c,b,d){var f,n,k=0,e,h,q,m,p,r;b=b||[0];d=d||0;q=d>>>3;if("UTF8"===a)for(r=3,e=0;e<c.length;e+=1)for(f=c.charCodeAt(e),n=[],128>f?n.push(f):2048>f?(n.push(192|f>>>6),n.push(128|f&63)):55296>f||57344<=f?n.push(224|f>>>12,128|f>>>6&63,128|f&63):(e+=1,f=65536+((f&1023)<<10|c.charCodeAt(e)&1023),n.push(240|f>>>18,128|f>>>12&63,128|f>>>6&63,128|f&63)),h=0;h<n.length;h+=1){p=k+
q;for(m=p>>>2;b.length<=m;)b.push(0);b[m]|=n[h]<<8*(r+p%4*-1);k+=1}else if("UTF16BE"===a||"UTF16LE"===a)for(r=2,n="UTF16LE"===a&&!0||"UTF16LE"!==a&&!1,e=0;e<c.length;e+=1){f=c.charCodeAt(e);!0===n&&(h=f&255,f=h<<8|f>>>8);p=k+q;for(m=p>>>2;b.length<=m;)b.push(0);b[m]|=f<<8*(r+p%4*-1);k+=2}return{value:b,binLen:8*k+d}};break;case "B64":d=function(a,b,c){var f=0,d,k,e,h,q,m,p;if(-1===a.search(/^[a-zA-Z0-9=+\/]+$/))throw Error("Invalid character in base-64 string");k=a.indexOf("=");a=a.replace(/\=/g,
"");if(-1!==k&&k<a.length)throw Error("Invalid '=' found in base-64 string");b=b||[0];c=c||0;m=c>>>3;for(k=0;k<a.length;k+=4){q=a.substr(k,4);for(e=h=0;e<q.length;e+=1)d="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/".indexOf(q[e]),h|=d<<18-6*e;for(e=0;e<q.length-1;e+=1){p=f+m;for(d=p>>>2;b.length<=d;)b.push(0);b[d]|=(h>>>16-8*e&255)<<8*(3+p%4*-1);f+=1}}return{value:b,binLen:8*f+c}};break;case "BYTES":d=function(a,b,c){var d,n,k,e,h;b=b||[0];c=c||0;k=c>>>3;for(n=0;n<a.length;n+=
1)d=a.charCodeAt(n),h=n+k,e=h>>>2,b.length<=e&&b.push(0),b[e]|=d<<8*(3+h%4*-1);return{value:b,binLen:8*a.length+c}};break;case "ARRAYBUFFER":try{d=new ArrayBuffer(0)}catch(l){throw Error("ARRAYBUFFER not supported by this environment");}d=function(a,b,c){var d,n,k,e,h;b=b||[0];c=c||0;n=c>>>3;h=new Uint8Array(a);for(d=0;d<a.byteLength;d+=1)e=d+n,k=e>>>2,b.length<=k&&b.push(0),b[k]|=h[d]<<8*(3+e%4*-1);return{value:b,binLen:8*a.byteLength+c}};break;default:throw Error("format must be HEX, TEXT, B64, BYTES, or ARRAYBUFFER");
}return d}function r(c,a){return c>>>a|c<<32-a}function J(c,a,d){return c&a^~c&d}function K(c,a,d){return c&a^c&d^a&d}function L(c){return r(c,2)^r(c,13)^r(c,22)}function M(c){return r(c,6)^r(c,11)^r(c,25)}function N(c){return r(c,7)^r(c,18)^c>>>3}function O(c){return r(c,17)^r(c,19)^c>>>10}function P(c,a){var d=(c&65535)+(a&65535);return((c>>>16)+(a>>>16)+(d>>>16)&65535)<<16|d&65535}function Q(c,a,d,l){var b=(c&65535)+(a&65535)+(d&65535)+(l&65535);return((c>>>16)+(a>>>16)+(d>>>16)+(l>>>16)+(b>>>
16)&65535)<<16|b&65535}function R(c,a,d,l,b){var g=(c&65535)+(a&65535)+(d&65535)+(l&65535)+(b&65535);return((c>>>16)+(a>>>16)+(d>>>16)+(l>>>16)+(b>>>16)+(g>>>16)&65535)<<16|g&65535}function x(c){var a=[],d;if(0===c.lastIndexOf("SHA-",0))switch(a=[3238371032,914150663,812702999,4144912697,4290775857,1750603025,1694076839,3204075428],d=[1779033703,3144134277,1013904242,2773480762,1359893119,2600822924,528734635,1541459225],c){case "SHA-224":break;case "SHA-256":a=d;break;case "SHA-384":a=[new m,new m,
new m,new m,new m,new m,new m,new m];break;case "SHA-512":a=[new m,new m,new m,new m,new m,new m,new m,new m];break;default:throw Error("Unknown SHA variant");}else throw Error("No SHA variants supported");return a}function A(c,a,d){var l,b,g,f,n,k,e,h,m,r,p,w,t,x,u,z,A,B,C,D,E,F,v=[],G;if("SHA-224"===d||"SHA-256"===d)r=64,w=1,F=Number,t=P,x=Q,u=R,z=N,A=O,B=L,C=M,E=K,D=J,G=H;else throw Error("Unexpected error in SHA-2 implementation");d=a[0];l=a[1];b=a[2];g=a[3];f=a[4];n=a[5];k=a[6];e=a[7];for(p=
0;p<r;p+=1)16>p?(m=p*w,h=c.length<=m?0:c[m],m=c.length<=m+1?0:c[m+1],v[p]=new F(h,m)):v[p]=x(A(v[p-2]),v[p-7],z(v[p-15]),v[p-16]),h=u(e,C(f),D(f,n,k),G[p],v[p]),m=t(B(d),E(d,l,b)),e=k,k=n,n=f,f=t(g,h),g=b,b=l,l=d,d=t(h,m);a[0]=t(d,a[0]);a[1]=t(l,a[1]);a[2]=t(b,a[2]);a[3]=t(g,a[3]);a[4]=t(f,a[4]);a[5]=t(n,a[5]);a[6]=t(k,a[6]);a[7]=t(e,a[7]);return a}var H;H=[1116352408,1899447441,3049323471,3921009573,961987163,1508970993,2453635748,2870763221,3624381080,310598401,607225278,1426881987,1925078388,2162078206,
2614888103,3248222580,3835390401,4022224774,264347078,604807628,770255983,1249150122,1555081692,1996064986,2554220882,2821834349,2952996808,3210313671,3336571891,3584528711,113926993,338241895,666307205,773529912,1294757372,1396182291,1695183700,1986661051,2177026350,2456956037,2730485921,2820302411,3259730800,3345764771,3516065817,3600352804,4094571909,275423344,430227734,506948616,659060556,883997877,958139571,1322822218,1537002063,1747873779,1955562222,2024104815,2227730452,2361852424,2428436474,
2756734187,3204031479,3329325298];"function"===typeof define&&define.amd?define(function(){return w}):"undefined"!==typeof exports?("undefined"!==typeof module&&module.exports&&(module.exports=w),exports=w):I.jsSHA=w})(this);