From 59025e317e84c3484163a37e0b411befc1f6cd1e Mon Sep 17 00:00:00 2001 From: Christian Schwinne Date: Mon, 5 Jun 2023 16:33:51 +0200 Subject: [PATCH] New settings es6 (merge from pbolduc fork) (#3231) * Initial new settings page test commit * quick poc to add chibi style helper and start to change files to es6 * add ready and loaded functions * added missing pageloaded variable to track if loaded already called. * More POC to dynamically add content, minimal DOM lib based on chibi * Add simple translation for any element that has class=l10n * simply self executing function --------- Co-authored-by: Phil Bolduc --- tools/wled.js | 122 +++++++++++++++++++++++++++++++++++ wled00/data/cfg.css | 129 +++++++++++++++++++++++++++++++++++++ wled00/data/cfg.htm | 71 ++++++++++++++++++++ wled00/data/cfg.js | 30 +++++++++ wled00/data/cfg_lang.js | 10 +++ wled00/data/dom.mjs | 126 ++++++++++++++++++++++++++++++++++++ wled00/data/translator.mjs | 27 ++++++++ 7 files changed, 515 insertions(+) create mode 100644 tools/wled.js create mode 100644 wled00/data/cfg.css create mode 100644 wled00/data/cfg.htm create mode 100644 wled00/data/cfg.js create mode 100644 wled00/data/cfg_lang.js create mode 100644 wled00/data/dom.mjs create mode 100644 wled00/data/translator.mjs diff --git a/tools/wled.js b/tools/wled.js new file mode 100644 index 000000000..432ce9ec0 --- /dev/null +++ b/tools/wled.js @@ -0,0 +1,122 @@ + +const express = require("express"); +const { createProxyMiddleware } = require('http-proxy-middleware'); +const path = require("path"); +const nopt = require("nopt"); +const app = express(); + +var knownOpts = { + "help": Boolean, + "port": Number, + "settings": [path], + "host": String, + "verbose": Boolean +}; +var shortHands = { + "?":["--help"], + "p":["--port"], + "s":["--settings"], + "h":["--host"], + "v":["--verbose"] +}; + +nopt.invalidHandler = function(k,v,t) { + // TODO: console.log(k,v,t); +} + +var parsedArgs = nopt(knownOpts,shortHands,process.argv,2); + +if (parsedArgs.help) { + console.log("WLED Dev Server"); + console.log("Usage: wled [-v] [-?] [--settings settings.js] [--userDir DIR]"); + console.log(" [--port PORT] [--host HOST]"); + console.log(""); + console.log("Options:"); + console.log(" -p, --port PORT port to listen on"); + console.log(" -s, --settings FILE use specified settings file"); + console.log(" --host HOST WLED instance for dynamic content"); + console.log(" -v, --verbose enable verbose output"); + console.log(" -?, --help show this help"); + console.log(""); + process.exit(); +} + +// WLED_HOME - the root directory where the html files are +process.env.WLED_HOME = process.env.WLED_HOME || path.resolve(__dirname,'..','wled00','data'); + +parsedArgs.port = parsedArgs.port || 8080; +parsedArgs.host = parsedArgs.host || "0.0.0.0"; + +// get the static file reference +function static(page) { + return express.static(process.env.WLED_HOME, {index: page}); +} + +// add routes for each setting page +function useSettingsRoutes() { + app.use(`/settings`, static(`settings.htm`)); + + const settings = ['wifi', 'leds', 'ui', 'sync','time','um', 'sec']; + settings.forEach(function(setting) { + app.use(`/settings/${setting}`, static(`settings_${setting}.htm`)); + }); +} + +// dynamic content that is proxied to real WLED instance +function useWebProxyRoutes(host) { + const httpProxy = createProxyMiddleware({ target: `http://${host}`, changeOrigin: true, logLevel: 'warn' }); + const proxyRoutes = ['json', 'presets.json', 'skins.css', 'liveview']; + proxyRoutes.forEach(function(route) { + app.use(`/${route}`, httpProxy); + }); +} + +// proxy data to a real WLED instance +function useWebSocketProxy(host, server) { + const wsProxy = createProxyMiddleware(`ws://${host}`, { changeOrigin: true, logLevel: 'warn' }); + app.use(wsProxy); + server.on('upgrade', wsProxy.upgrade); +} + +// first matching route +app.use('/', static("index.htm")); +useSettingsRoutes(); // map the setting pages +useWebProxyRoutes(parsedArgs.host); + +// use static files like style sheets etc +app.use(express.static(process.env.WLED_HOME)); + +const server = app.listen(parsedArgs.port, () => { + if (parsedArgs.verbose) { + console.log(`WLED UI listening at http://localhost:${parsedArgs.port}`) + } +}); + +useWebSocketProxy(parsedArgs.host, server); + +var stopping = false; +function exitWhenStopped() { + if (!stopping) { + stopping = true; + server.close(function() { + console.log('WLED UI closed'); + process.exit(); + }); + } +} + +process.on('uncaughtException',function(err) { + util.log('[WLED] Uncaught Exception:'); + if (err.stack) { + console.log(err.stack); + } else { + console.log(err); + } + process.exit(1); +}); + +process.on('SIGINT', exitWhenStopped); +process.on('SIGTERM', exitWhenStopped); +process.on('SIGHUP', exitWhenStopped); +process.on('SIGUSR2', exitWhenStopped); // for nodemon restart +process.on('SIGBREAK', exitWhenStopped); // for windows ctrl-break \ No newline at end of file diff --git a/wled00/data/cfg.css b/wled00/data/cfg.css new file mode 100644 index 000000000..6411daa22 --- /dev/null +++ b/wled00/data/cfg.css @@ -0,0 +1,129 @@ +@font-face { + font-family: "CIcons"; + src: url(data:application/font-woff;charset=utf-8;base64,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) format('woff'); +} + +:root { + --c-1: #111; + --c-f: #fff; + --c-2: #222; + --c-3: #333; + --c-4: #444; + --c-5: #555; + --c-6: #666; + --c-8: #888; + --c-b: #bbb; + --c-c: #ccc; + --c-e: #eee; + --c-d: #ddd; + --c-r: #831; +} + +html { + touch-action: manipulation; +} + +body { + margin: 0; + background-color: var(--c-2); + font-family: Helvetica, Verdana, sans-serif; + font-size: 17px; + color: var(--c-f); + -webkit-touch-callout: none; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; + -webkit-tap-highlight-color: transparent; + scrollbar-width: 6px; + scrollbar-color: var(--c-sb) transparent; +} + +html, +body { + height: 100%; + width: 100%; + position: fixed; + overscroll-behavior: none; +} + +.icons { + font-family: "CIcons"; + font-style: normal; + font-size: 24px; + line-height: 1; + display: inline-block; +} + +#header { + width: 100%; + background-color: var(--c-3); + height: 45px; +} + +#menu { + height: calc(100% - 45px); + background-color: var(--c-3); + width: 180px; +} + +.entry { + height: 45px; + color: var(--c-b); +} + +.entry:hover { + color: var(--c-f); + background-color: var(--c-4); +} + +.e-icon { + padding: 10px; + display: inline-block; + width: 45px; + height: 100%; + box-sizing: border-box; +} + +.e-label { + display: inline-block; + height: 45px; + vertical-align: top; + padding: 14px 0; + box-sizing: border-box; +} + +.btn { + padding: 9px; + color: var(--c-b); + box-sizing: border-box; +} + +.btn:hover { + color: var(--c-f); + background-color: var(--c-4); +} + +.save { + float: right; + height: 100%; +} + +.b-icon { + display: inline-block; +} + +.b-label { + display: inline-block; + vertical-align: top; + padding: 4px; +} + +@media (max-width: 500px) { + #menu { + width: 45px; + } + .e-label { + display: none; + } +} \ No newline at end of file diff --git a/wled00/data/cfg.htm b/wled00/data/cfg.htm new file mode 100644 index 000000000..57adc79e9 --- /dev/null +++ b/wled00/data/cfg.htm @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + +
+ +
+

Network

+
+
+

Hardware

+
+
+

Customization

+
+
+

Interfaces

+
+
+

Schedules

+
+
+

Sound Reactive

+
+
+

Usermods

+
+
+

DMX Out

+
+
+

About

+
+
+

Update

+
+
+

Reboot

+
+
+ + + + \ No newline at end of file diff --git a/wled00/data/cfg.js b/wled00/data/cfg.js new file mode 100644 index 000000000..854549b1f --- /dev/null +++ b/wled00/data/cfg.js @@ -0,0 +1,30 @@ +import $ from './dom.mjs' +import translate from './translator.mjs' + +/* Dynamically create the menu +const menuItems = [ + { "icon": "", "id": "e-nw", "text": "Network" }, + { "icon": "", "id": "e-hw", "text": "Hardware" }, + { "icon": "", "id": "e-ui", "text": "Customization" }, + { "icon": "", "id": "e-if", "text": "Interfaces" }, + { "icon": "", "id": "e-tm", "text": "Schedules" }, + { "icon": "", "id": "e-dx", "text": "DMX Out" }, + { "icon": "", "id": "e-sr", "text": "Sound Reactive" }, + { "icon": "", "id": "e-um", "text": "Usermods" }, + { "icon": "", "id": "e-ab", "text": "About" } +]; + +$().ready(function() { + const menu = $('#menu'); + menuItems.map(item => + menu.append(`
${item.icon}
${item.text}
`) + ); +}); +*/ + +// populate labels when to dom is ready but before it is rendered +$().ready(function() { + // https://www.w3.org/International/questions/qa-i18n + // Localization is sometimes written in English as l10n + translate('.l10n'); +}); \ No newline at end of file diff --git a/wled00/data/cfg_lang.js b/wled00/data/cfg_lang.js new file mode 100644 index 000000000..ae41a2a41 --- /dev/null +++ b/wled00/data/cfg_lang.js @@ -0,0 +1,10 @@ +(function() { + // self executing function to ensure translations is set on page load + // so we dont have to wait for fetch/xhr request + window.translations = { + "About": "Über", + "Save": "Speichern", + "Schedules": "Zeitpläne", + "Sound Reactive": "Tonreaktiv" + }; +}()); \ No newline at end of file diff --git a/wled00/data/dom.mjs b/wled00/data/dom.mjs new file mode 100644 index 000000000..209147361 --- /dev/null +++ b/wled00/data/dom.mjs @@ -0,0 +1,126 @@ +/* + ** DOM module - base on https://github.com/kylebarrow/chibi with IE hacks removed + ** + */ +var readyfn = [], + loadedfn = [], + domready = false, + pageloaded = false, + d = document, + w = window; + +// Fire any function calls on ready event +function fireReady() { + var i; + domready = true; + for (i = 0; i < readyfn.length; i += 1) { + readyfn[i](); + } + readyfn = []; +} + +// Fire any function calls on loaded event +function fireLoaded() { + var i; + pageloaded = true; + for (i = 0; i < loadedfn.length; i += 1) { + loadedfn[i](); + } + loadedfn = []; +} + +// Check DOM ready, page loaded +if (d.addEventListener) { + // Standards + d.addEventListener('DOMContentLoaded', fireReady, false); + w.addEventListener('load', fireLoaded, false); +} + +// Loop through node array +function nodeLoop(fn, nodes) { + var i; + // Good idea to walk up the DOM + for (i = nodes.length - 1; i >= 0; i -= 1) { + fn(nodes[i]); + } +} + +function dom(selector) { + var self, nodes = [], + json = false, + nodelist; + + if (selector) { + + // Element node + if (selector instanceof HTMLElement) { + nodes = [selector]; // return element as node list + } else if (selector instanceof NodeList) { + // JSON, document object or node list + json = (typeof selector.length !== 'number'); + nodes = selector; + } else if (typeof selector === 'string') { + nodelist = d.querySelectorAll(selector); + nodes = Array.from(nodelist); + } + } + + // Only attach nodes if not JSON + self = json ? {} : nodes; + + // Public functions + + // Fire on DOM ready + self.ready = function(fn) { + if (fn) { + if (domready) { + fn(); + return self; + } else { + readyfn.push(fn); + } + } + }; + // Fire on page loaded + self.loaded = function(fn) { + if (fn) { + if (pageloaded) { + fn(); + return self; + } else { + loadedfn.push(fn); + } + } + }; + + // Executes a function on nodes + self.each = function(fn) { + if (typeof fn === 'function') { + nodeLoop(fn, nodes); + } + return self; + }; + + self.first = function() { + return dom(nodes.shift()); + }; + + // Find last + self.last = function() { + return dom(nodes.pop()); + }; + + // append html before end of the of the tag + self.append = function(value) { + if (value) { + nodeLoop(function(elm) { + elm.insertAdjacentHTML('beforeend', value); + }, nodes); + } + return self; + }; + + return self; +} + +export default dom; \ No newline at end of file diff --git a/wled00/data/translator.mjs b/wled00/data/translator.mjs new file mode 100644 index 000000000..25d3c143f --- /dev/null +++ b/wled00/data/translator.mjs @@ -0,0 +1,27 @@ +import $ from './dom.mjs' + +var w = window; + +function getText(elm) { + return elm.textContent; // or innerText ? +} + +function setText(elm, value) { + if (value) { + elm.textContent = value; // or innerText ? + } +} + +// perform simple translation +function translateElement(elm) { + const text = getText(elm); + setText(elm, w.translations[text]); +} + +export default function translate(selector) { + if (w.translations) { + $(selector).each(translateElement) + } else { + console.info("no translations"); + } +}