remove webpack and improve performance

This commit is contained in:
hippoz 2022-08-26 22:09:08 +03:00
parent c05a23912f
commit ca9d562ed5
No known key found for this signature in database
GPG key ID: 7C52899193467641
16 changed files with 193 additions and 1718 deletions

View file

@ -20,7 +20,7 @@ control_message_parser.add_handler("0", {
"auth_string": "str" "auth_string": "str"
}) })
app.static("app", "frontend/dist/", resource_type="dir") app.static("app", "frontend/", resource_type="dir")
@app.get("/") @app.get("/")
async def home(req): async def home(req):

View file

@ -1,31 +1,53 @@
import Banner from "./Banner"; import Banner from "./Banner.js";
import Connection from "./Connection"; import connection, { ConnectionEvent } from "./connection.js";
import KeyboardController from "./Keyboard"; import KeyboardController from "./Keyboard.js";
import { getAuth, setAuth } from "./LocalConfiguration"; import { setItem } from "./storage.js";
import LoginPrompt from "./LoginPrompt"; import LoginPrompt from "./LoginPrompt.js";
import TouchpadController from "./Touchpad"; import TouchpadController from "./Touchpad.js";
class App { class App {
constructor(mountElement) { constructor() {
this.mountElement = mountElement; this.connection = connection;
this.connection = new Connection(`ws://${location.host}/gateway`);
this.touchpad = new TouchpadController(this.connection); this.touchpad = new TouchpadController(this.connection);
this.keyboard = new KeyboardController(this.connection); this.keyboard = new KeyboardController(this.connection);
this.loginPromptComponent = null; this.bannerComponent = new Banner();
this.loginPromptComponent = new LoginPrompt(this.connection);
this.connectionHandlers = [];
}
this.connection.connect(getAuth()); mountOn(mount, keepMountChildren=false) {
this.connection.ws.addEventListener("close", ({code}) => { this.mountElement = mount;
if (!keepMountChildren) {
while (mount.firstChild) {
mount.removeChild(mount.lastChild);
}
}
this.connectionHandlers.push([ConnectionEvent.Closed, this.connection.subscribe(ConnectionEvent.Closed, (code) => {
this.unmountApp(); this.unmountApp();
if (code === 4001) { // 4001 - code for bad auth if (code === 4001) { // 4001 - code for bad auth
this.transitionTo("login"); this.transitionTo("login");
} else { } else {
this.transitionTo("reconnectingBanner"); this.transitionTo("reconnectingBanner");
} }
}); })]);
this.connection.onHandshakeCompleted = () => {
this.connectionHandlers.push([ConnectionEvent.Ready, this.connection.subscribe(ConnectionEvent.Ready, () => {
this.transitionTo("app"); this.transitionTo("app");
}; })]);
}
unmount() {
if (this.mountElement) {
this.unmountApp();
this.unmountBannerComponent();
this.unmountLoginComponent();
}
this.connectionHandlers.forEach(([ event, handler ]) => {
this.connection.unsubscribe(event, handler);
});
} }
transitionTo(type) { transitionTo(type) {
@ -51,27 +73,20 @@ class App {
} }
mountBannerComponent(title, text) { mountBannerComponent(title, text) {
if (!this.bannerComponent)
this.bannerComponent = new Banner();
this.bannerComponent.mountOn(this.mountElement); this.bannerComponent.mountOn(this.mountElement);
this.bannerComponent.updateTitle(title); this.bannerComponent.updateTitle(title);
this.bannerComponent.updateText(text); this.bannerComponent.updateText(text);
} }
unmountBannerComponent() { unmountBannerComponent() {
if (this.bannerComponent)
this.bannerComponent.unmount(); this.bannerComponent.unmount();
} }
mountLoginComponent() { mountLoginComponent() {
if (!this.loginPromptComponent)
this.loginPromptComponent = new LoginPrompt(this.connection);
this.loginPromptComponent.mountOn(this.mountElement); this.loginPromptComponent.mountOn(this.mountElement);
this.loginPromptComponent.onPasswordSubmitted = (p) => { this.loginPromptComponent.onPasswordSubmitted = (p) => {
setAuth(p); setItem("auth:token", p);
this.connection.connect(p); this.connection.connect();
}; };
} }

View file

@ -38,8 +38,10 @@ class KeyboardController {
this.container.removeChild(this.container.lastChild); this.container.removeChild(this.container.lastChild);
} }
const rowsElement = document.createElement("div"); const rowsParent = document.createElement("div");
rowsElement.classList.add("Keyboard-rows"); rowsParent.classList.add("Keyboard-rows");
const rowsFragment = document.createDocumentFragment();
for (let row = 0; row < layout.length; row++) { for (let row = 0; row < layout.length; row++) {
const rowElement = document.createElement("div"); const rowElement = document.createElement("div");
@ -49,17 +51,18 @@ class KeyboardController {
const colElement = document.createElement("button"); const colElement = document.createElement("button");
colElement.classList.add("Keyboard-button"); colElement.classList.add("Keyboard-button");
colElement.innerHTML = key; colElement.innerText = key;
colElement.addEventListener("click", (event) => { colElement.addEventListener("click", () => {
this.handleKeypress(key); this.handleKeypress(key);
}); });
rowElement.appendChild(colElement); rowElement.appendChild(colElement);
} }
rowsElement.appendChild(rowElement); rowsFragment.appendChild(rowElement);
} }
this.container.appendChild(rowsElement); rowsParent.appendChild(rowsFragment);
this.container.appendChild(rowsParent);
}); });
} }

View file

@ -8,16 +8,11 @@ class LoginPrompt {
return; // Already mounted return; // Already mounted
this.element = document.createRange().createContextualFragment(` this.element = document.createRange().createContextualFragment(`
<div class="Card Card-ui-bottom Card-centered-text"> <div class="Card Card-ui-bottom LoginPrompt">
<h2>Login</h2> <h2>Login</h2>
<p>You need to enter the login code before you can start controlling your device.</p>
<br>
<div class="full-width">
<input type="password" id="code-input" class="input full-width" placeholder="Code"> <input type="password" id="code-input" class="input full-width" placeholder="Code">
<br>
<button id="continue-button" class="button-default full-width">Continue</button> <button id="continue-button" class="button-default full-width">Continue</button>
</div> </div>
</div>
`).children[0]; `).children[0];
this.element.querySelector("#continue-button").addEventListener("click", () => { this.element.querySelector("#continue-button").addEventListener("click", () => {

27
frontend/app.html Normal file
View file

@ -0,0 +1,27 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
<title>Capybara</title>
<link rel="stylesheet" href="./main.css">
<script type="module">
import App from "./App.js";
import connection from "./connection.js";
window.__capybara = {};
window.__capybara.app = new App();
window.__capybara.app.mountOn(document.body);
connection.connect();
</script>
</head>
<body>
<div class="Card Card-ui-bottom Card-centered-text">
<h2>Loading...</h2>
</div>
</body>
</html>

View file

@ -1,38 +1,45 @@
import Logger from "./common/Logger"; import logger from "./logger.js";
import { getItem } from "./storage.js";
class Connection { export const ConnectionEvent = {
constructor(url) { Open: 0,
this.ws = null; Closed: 1,
this.log = Logger(["Connection"], ["log"]).log; Ready: 2,
this.messageLog = Logger(["Connection", "Message"], ["log"]).log; };
this.url = url;
this.isReady = false; export default {
this.reconnectTimeout = 0; ws: null,
} log: logger(["Connection"], ["log"]).log,
messageLog: logger(["Connection", "Message"], ["log"]).log,
url: getItem("server:gatewayBase"),
isReady: false,
reconnectTimeout: 0,
handlers: new Map(),
formatAuthString(password) { formatAuthString(password) {
return `%auth%${btoa(password)}`; return `%auth%${btoa(password)}`;
} },
connect(password) { connect(password=getItem("auth:token")) {
this.ws = new WebSocket(this.url); this.ws = new WebSocket(this.url);
this.ws.onerror = (e) => this.log("Error", e); this.ws.onerror = (e) => this.log("Error", e);
this.ws.onopen = () => { this.ws.onopen = () => {
this.log("Open"); this.log("Open");
this.dispatch(ConnectionEvent.Open, 1);
this.reconnectTimeout = 0; this.reconnectTimeout = 0;
this.log("Sending authentication packet"); this.log("Sending authentication packet");
this.ws.send(`0${this.formatAuthString(password)}`); // send auth packet this.ws.send(`0${this.formatAuthString(password)}`); // send auth packet
}; };
this.ws.onmessage = ({ data }) => { this.ws.onmessage = ({ data }) => {
if (data === "1") { if (data === "1") {
if (this.onHandshakeCompleted)
this.onHandshakeCompleted();
this.isReady = true; this.isReady = true;
this.dispatch(ConnectionEvent.Ready, 1);
this.log("Handshake complete"); this.log("Handshake complete");
} }
}; };
this.ws.onclose = ({ code }) => { this.ws.onclose = ({ code }) => {
this.dispatch(ConnectionEvent.Closed, code);
if (code === 4001) {// code for bad auth if (code === 4001) {// code for bad auth
this.log("Closed due to bad auth - skipping reconnect"); this.log("Closed due to bad auth - skipping reconnect");
return; return;
@ -44,7 +51,7 @@ class Connection {
this.log(`Closed - will reconnect in ${this.reconnectTimeout}ms`); this.log(`Closed - will reconnect in ${this.reconnectTimeout}ms`);
setTimeout(() => this.connect(password), this.reconnectTimeout); setTimeout(() => this.connect(password), this.reconnectTimeout);
} }
} },
sendMessage(code, params=[]) { sendMessage(code, params=[]) {
let message = code; let message = code;
@ -57,11 +64,38 @@ class Connection {
} }
this.ws.send(message); this.ws.send(message);
} },
disconnect() { disconnect() {
this.ws.close(); this.ws.close();
} },
dispatch(event, data) {
const eventHandlers = this.handlers.get(event);
if (!eventHandlers)
return;
eventHandlers.forEach(e => e(data));
},
subscribe(event, handler) {
if (!this.handlers.get(event)) {
this.handlers.set(event, new Set());
} }
export default Connection; this.handlers.get(event).add(handler);
return handler; // can later be used for unsubscribe()
},
unsubscribe(event, handler) {
const eventHandlers = this.handlers.get(event);
if (!eventHandlers)
return;
eventHandlers.delete(handler);
if (eventHandlers.size < 1) {
this.handlers.delete(event);
}
},
};

View file

@ -35,7 +35,7 @@ const loggerOfType = (components, type='log') => (...args) => {
} }
}; };
function Logger(components, types=['warn', 'error', 'log']) { function logger(components, types=['warn', 'error', 'log']) {
const loggerObj = {}; const loggerObj = {};
for (const type of types) { for (const type of types) {
@ -50,4 +50,4 @@ export function domLog(message) {
document.body.appendChild(document.createTextNode(message)); document.body.appendChild(document.createTextNode(message));
} }
export default Logger; export default logger;

View file

@ -108,6 +108,15 @@ body {
padding: 12px; padding: 12px;
} }
/* LoginPrompt */
.LoginPrompt {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
/* Touchpad */ /* Touchpad */
.Touchpad { .Touchpad {

View file

@ -1,19 +0,0 @@
{
"name": "capybara-frontend",
"version": "1.0.0",
"private": true,
"license": "MIT",
"scripts": {
"watch": "webpack --watch",
"build": "webpack"
},
"devDependencies": {
"css-loader": "^6.5.0",
"css-minimizer-webpack-plugin": "^3.1.1",
"html-webpack-plugin": "^5.5.0",
"mini-css-extract-plugin": "^2.4.3",
"webpack": "^5.61.0",
"webpack-cli": "^4.9.1"
},
"dependencies": {}
}

View file

@ -1,7 +0,0 @@
export function getAuth() {
return localStorage.getItem("$auth");
}
export function setAuth(newValue) {
return localStorage.setItem("$auth", newValue);
}

View file

@ -1,4 +0,0 @@
import "./styles/main.css";
import App from "./App";
window.__app = new App(document.body);

47
frontend/storage.js Normal file
View file

@ -0,0 +1,47 @@
const defaults = {
"server:gatewayBase": `${location.protocol === "https:" ? "wss" : "ws"}://${location.host}/gateway`,
"auth:token": "",
};
const store = new Map(Object.entries(defaults));
const persistentProvider = localStorage;
let didCacheProvider = false;
export function setItem(key, value) {
store.set(key, value);
if (persistentProvider) {
persistentProvider.setItem(key, typeof value === "string" ? value : JSON.stringify(value));
}
}
export function getItem(key) {
if (!didCacheProvider) {
init();
}
return store.get(key);
}
export function removeItem(key) {
store.delete(key);
if (persistentProvider) {
persistentProvider.removeItem(key);
}
}
export function init() {
if (!persistentProvider)
return;
store.forEach((defaultValue, key) => {
const override = persistentProvider.getItem(key);
if (override !== null) {
try {
store.set(key, typeof defaultValue === "string" ? override : JSON.parse(override));
} catch (o_O) {
console.warn("[Storage]", `init(): An exception was thrown while parsing the value of key "${key}" from persistentProvider. The key "${key}" will be removed from persistentProvider.`, o_O);
persistentProvider.removeItem(key);
}
}
});
didCacheProvider = true;
}

View file

@ -1,52 +0,0 @@
const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const CssMinimizerPlugin = require("css-minimizer-webpack-plugin");
module.exports = {
mode: "development",
entry: './src/index.js',
output: {
filename: '[name].[contenthash].js',
path: path.resolve(__dirname, 'dist'),
clean: true
},
plugins: [
new HtmlWebpackPlugin({
title: "Capybara",
filename: "app.html",
meta: {
"viewport": "width=device-width,height=device-height,initial-scale=1,minimum-scale=1,user-scalable=no,viewport-fit=contain",
"application-name": "Capybara"
}
}),
new MiniCssExtractPlugin({
filename: "[name].[contenthash].css"
}),
],
module: {
rules: [
{
test: /\.css$/i,
use: [MiniCssExtractPlugin.loader, "css-loader"],
},
],
},
optimization: {
moduleIds: 'deterministic',
runtimeChunk: 'single',
splitChunks: {
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
chunks: 'all',
},
},
},
minimizer: [
`...`,
new CssMinimizerPlugin(),
]
},
};

File diff suppressed because it is too large Load diff