New file editor (#4956)
- no mendatory external JS dependency, works in offline mode - optional external dependency is used for highlighting JSON, plain text edit is used if not available - WLED styling (dark mode only) - JSON files are displayed "prettyfied" and saved "minified" - JSON color highlighting (if available) - JSON verification during edit and on saving both in online and offline mode - special treatment for ledmap files: displayed in aligned columns (2D) or as lines (1D), saved as minified json: no more white-space problems - displays file size and total flash usage
This commit is contained in:
@@ -26,7 +26,7 @@ const packageJson = require("../package.json");
|
|||||||
// Export functions for testing
|
// Export functions for testing
|
||||||
module.exports = { isFileNewerThan, isAnyFileInFolderNewerThan };
|
module.exports = { isFileNewerThan, isAnyFileInFolderNewerThan };
|
||||||
|
|
||||||
const output = ["wled00/html_ui.h", "wled00/html_pixart.h", "wled00/html_cpal.h", "wled00/html_pxmagic.h", "wled00/html_settings.h", "wled00/html_other.h"]
|
const output = ["wled00/html_ui.h", "wled00/html_pixart.h", "wled00/html_cpal.h", "wled00/html_edit.h", "wled00/html_pxmagic.h", "wled00/html_settings.h", "wled00/html_other.h"]
|
||||||
|
|
||||||
// \x1b[34m is blue, \x1b[36m is cyan, \x1b[0m is reset
|
// \x1b[34m is blue, \x1b[36m is cyan, \x1b[0m is reset
|
||||||
const wledBanner = `
|
const wledBanner = `
|
||||||
@@ -246,6 +246,21 @@ writeHtmlGzipped("wled00/data/index.htm", "wled00/html_ui.h", 'index');
|
|||||||
writeHtmlGzipped("wled00/data/pixart/pixart.htm", "wled00/html_pixart.h", 'pixart');
|
writeHtmlGzipped("wled00/data/pixart/pixart.htm", "wled00/html_pixart.h", 'pixart');
|
||||||
//writeHtmlGzipped("wled00/data/cpal/cpal.htm", "wled00/html_cpal.h", 'cpal');
|
//writeHtmlGzipped("wled00/data/cpal/cpal.htm", "wled00/html_cpal.h", 'cpal');
|
||||||
writeHtmlGzipped("wled00/data/pxmagic/pxmagic.htm", "wled00/html_pxmagic.h", 'pxmagic');
|
writeHtmlGzipped("wled00/data/pxmagic/pxmagic.htm", "wled00/html_pxmagic.h", 'pxmagic');
|
||||||
|
//writeHtmlGzipped("wled00/data/edit.htm", "wled00/html_edit.h", 'edit');
|
||||||
|
|
||||||
|
|
||||||
|
writeChunks(
|
||||||
|
"wled00/data",
|
||||||
|
[
|
||||||
|
{
|
||||||
|
file: "edit.htm",
|
||||||
|
name: "PAGE_edit",
|
||||||
|
method: "gzip",
|
||||||
|
filter: "html-minify"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"wled00/html_edit.h"
|
||||||
|
);
|
||||||
|
|
||||||
writeChunks(
|
writeChunks(
|
||||||
"wled00/data/cpal",
|
"wled00/data/cpal",
|
||||||
|
|||||||
1051
wled00/data/edit.htm
1051
wled00/data/edit.htm
File diff suppressed because it is too large
Load Diff
@@ -672,7 +672,6 @@ function parseInfo(i) {
|
|||||||
//syncTglRecv = i.str;
|
//syncTglRecv = i.str;
|
||||||
maxSeg = i.leds.maxseg;
|
maxSeg = i.leds.maxseg;
|
||||||
pmt = i.fs.pmt;
|
pmt = i.fs.pmt;
|
||||||
if (pcMode && !i.wifi.ap) gId('edit').classList.remove("hide"); else gId('edit').classList.add("hide");
|
|
||||||
gId('buttonNodes').style.display = lastinfo.ndc > 0 ? null:"none";
|
gId('buttonNodes').style.display = lastinfo.ndc > 0 ? null:"none";
|
||||||
// do we have a matrix set-up
|
// do we have a matrix set-up
|
||||||
mw = i.leds.matrix ? i.leds.matrix.w : 0;
|
mw = i.leds.matrix ? i.leds.matrix.w : 0;
|
||||||
@@ -3151,7 +3150,6 @@ function togglePcMode(fromB = false)
|
|||||||
if (!fromB && ((wW < 1024 && lastw < 1024) || (wW >= 1024 && lastw >= 1024))) return; // no change in size and called from size()
|
if (!fromB && ((wW < 1024 && lastw < 1024) || (wW >= 1024 && lastw >= 1024))) return; // no change in size and called from size()
|
||||||
if (pcMode) openTab(0, true);
|
if (pcMode) openTab(0, true);
|
||||||
gId('buttonPcm').className = (pcMode) ? "active":"";
|
gId('buttonPcm').className = (pcMode) ? "active":"";
|
||||||
if (pcMode && !ap) gId('edit').classList.remove("hide"); else gId('edit').classList.add("hide");
|
|
||||||
gId('bot').style.height = (pcMode && !cfg.comp.pcmbot) ? "0":"auto";
|
gId('bot').style.height = (pcMode && !cfg.comp.pcmbot) ? "0":"auto";
|
||||||
sCol('--bh', gId('bot').clientHeight + "px");
|
sCol('--bh', gId('bot').clientHeight + "px");
|
||||||
_C.style.width = (pcMode || simplifiedUI)?'100%':'400%';
|
_C.style.width = (pcMode || simplifiedUI)?'100%':'400%';
|
||||||
|
|||||||
@@ -541,7 +541,6 @@ void handleSerial();
|
|||||||
void updateBaudRate(uint32_t rate);
|
void updateBaudRate(uint32_t rate);
|
||||||
|
|
||||||
//wled_server.cpp
|
//wled_server.cpp
|
||||||
void createEditHandler(bool enable);
|
|
||||||
void initServer();
|
void initServer();
|
||||||
void serveMessage(AsyncWebServerRequest* request, uint16_t code, const String& headl, const String& subl="", byte optionT=255);
|
void serveMessage(AsyncWebServerRequest* request, uint16_t code, const String& headl, const String& subl="", byte optionT=255);
|
||||||
void serveJsonError(AsyncWebServerRequest* request, uint16_t code, uint16_t error);
|
void serveJsonError(AsyncWebServerRequest* request, uint16_t code, uint16_t error);
|
||||||
|
|||||||
@@ -613,7 +613,6 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage)
|
|||||||
#ifndef WLED_DISABLE_OTA
|
#ifndef WLED_DISABLE_OTA
|
||||||
aOtaEnabled = request->hasArg(F("AO"));
|
aOtaEnabled = request->hasArg(F("AO"));
|
||||||
#endif
|
#endif
|
||||||
//createEditHandler(correctPIN && !otaLock);
|
|
||||||
otaSameSubnet = request->hasArg(F("SU"));
|
otaSameSubnet = request->hasArg(F("SU"));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -369,7 +369,6 @@ void checkSettingsPIN(const char* pin) {
|
|||||||
if (!correctPIN && millis() - lastEditTime < PIN_RETRY_COOLDOWN) return; // guard against PIN brute force
|
if (!correctPIN && millis() - lastEditTime < PIN_RETRY_COOLDOWN) return; // guard against PIN brute force
|
||||||
bool correctBefore = correctPIN;
|
bool correctBefore = correctPIN;
|
||||||
correctPIN = (strlen(settingsPIN) == 0 || strncmp(settingsPIN, pin, 4) == 0);
|
correctPIN = (strlen(settingsPIN) == 0 || strncmp(settingsPIN, pin, 4) == 0);
|
||||||
if (correctBefore != correctPIN) createEditHandler(correctPIN);
|
|
||||||
lastEditTime = millis();
|
lastEditTime = millis();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -167,7 +167,6 @@ void WLED::loop()
|
|||||||
// 15min PIN time-out
|
// 15min PIN time-out
|
||||||
if (strlen(settingsPIN)>0 && correctPIN && millis() - lastEditTime > PIN_TIMEOUT) {
|
if (strlen(settingsPIN)>0 && correctPIN && millis() - lastEditTime > PIN_TIMEOUT) {
|
||||||
correctPIN = false;
|
correctPIN = false;
|
||||||
createEditHandler(false);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// reconnect WiFi to clear stale allocations if heap gets too low
|
// reconnect WiFi to clear stale allocations if heap gets too low
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
#include "html_pxmagic.h"
|
#include "html_pxmagic.h"
|
||||||
#endif
|
#endif
|
||||||
#include "html_cpal.h"
|
#include "html_cpal.h"
|
||||||
|
#include "html_edit.h"
|
||||||
|
|
||||||
// define flash strings once (saves flash memory)
|
// define flash strings once (saves flash memory)
|
||||||
static const char s_redirecting[] PROGMEM = "Redirecting...";
|
static const char s_redirecting[] PROGMEM = "Redirecting...";
|
||||||
@@ -22,6 +23,13 @@ static const char s_unlock_cfg [] PROGMEM = "Please unlock settings using PIN co
|
|||||||
static const char s_rebooting [] PROGMEM = "Rebooting now...";
|
static const char s_rebooting [] PROGMEM = "Rebooting now...";
|
||||||
static const char s_notimplemented[] PROGMEM = "Not implemented";
|
static const char s_notimplemented[] PROGMEM = "Not implemented";
|
||||||
static const char s_accessdenied[] PROGMEM = "Access Denied";
|
static const char s_accessdenied[] PROGMEM = "Access Denied";
|
||||||
|
static const char s_not_found[] PROGMEM = "Not found";
|
||||||
|
static const char s_wsec[] PROGMEM = "wsec.json";
|
||||||
|
static const char s_func[] PROGMEM = "func";
|
||||||
|
static const char s_path[] PROGMEM = "path";
|
||||||
|
static const char s_cache_control[] PROGMEM = "Cache-Control";
|
||||||
|
static const char s_no_store[] PROGMEM = "no-store";
|
||||||
|
static const char s_expires[] PROGMEM = "Expires";
|
||||||
static const char _common_js[] PROGMEM = "/common.js";
|
static const char _common_js[] PROGMEM = "/common.js";
|
||||||
|
|
||||||
//Is this an IP?
|
//Is this an IP?
|
||||||
@@ -67,9 +75,9 @@ static void setStaticContentCacheHeaders(AsyncWebServerResponse *response, int c
|
|||||||
#ifndef WLED_DEBUG
|
#ifndef WLED_DEBUG
|
||||||
// this header name is misleading, "no-cache" will not disable cache,
|
// this header name is misleading, "no-cache" will not disable cache,
|
||||||
// it just revalidates on every load using the "If-None-Match" header with the last ETag value
|
// it just revalidates on every load using the "If-None-Match" header with the last ETag value
|
||||||
response->addHeader(F("Cache-Control"), F("no-cache"));
|
response->addHeader(FPSTR(s_cache_control), F("no-cache"));
|
||||||
#else
|
#else
|
||||||
response->addHeader(F("Cache-Control"), F("no-store,max-age=0")); // prevent caching if debug build
|
response->addHeader(FPSTR(s_cache_control), F("no-store,max-age=0")); // prevent caching if debug build
|
||||||
#endif
|
#endif
|
||||||
char etag[32];
|
char etag[32];
|
||||||
generateEtag(etag, eTagSuffix);
|
generateEtag(etag, eTagSuffix);
|
||||||
@@ -194,7 +202,7 @@ static void handleUpload(AsyncWebServerRequest *request, const String& filename,
|
|||||||
request->_tempFile.close();
|
request->_tempFile.close();
|
||||||
if (filename.indexOf(F("cfg.json")) >= 0) { // check for filename with or without slash
|
if (filename.indexOf(F("cfg.json")) >= 0) { // check for filename with or without slash
|
||||||
doReboot = true;
|
doReboot = true;
|
||||||
request->send(200, FPSTR(CONTENT_TYPE_PLAIN), F("Configuration restore successful.\nRebooting..."));
|
request->send(200, FPSTR(CONTENT_TYPE_PLAIN), F("Config restore ok.\nRebooting..."));
|
||||||
} else {
|
} else {
|
||||||
if (filename.indexOf(F("palette")) >= 0 && filename.indexOf(F(".json")) >= 0) loadCustomPalettes();
|
if (filename.indexOf(F("palette")) >= 0 && filename.indexOf(F(".json")) >= 0) loadCustomPalettes();
|
||||||
request->send(200, FPSTR(CONTENT_TYPE_PLAIN), F("File Uploaded!"));
|
request->send(200, FPSTR(CONTENT_TYPE_PLAIN), F("File Uploaded!"));
|
||||||
@@ -203,25 +211,94 @@ static void handleUpload(AsyncWebServerRequest *request, const String& filename,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void createEditHandler(bool enable) {
|
static const char _edit_htm[] PROGMEM = "/edit.htm";
|
||||||
|
|
||||||
|
void createEditHandler() {
|
||||||
if (editHandler != nullptr) server.removeHandler(editHandler);
|
if (editHandler != nullptr) server.removeHandler(editHandler);
|
||||||
if (enable) {
|
|
||||||
#ifdef WLED_ENABLE_FS_EDITOR
|
editHandler = &server.on(F("/edit"), static_cast<WebRequestMethod>(HTTP_GET), [](AsyncWebServerRequest *request) {
|
||||||
#ifdef ARDUINO_ARCH_ESP32
|
// PIN check for GET/DELETE, for POST it is done in handleUpload()
|
||||||
editHandler = &server.addHandler(new SPIFFSEditor(WLED_FS));//http_username,http_password));
|
if (!correctPIN) {
|
||||||
#else
|
|
||||||
editHandler = &server.addHandler(new SPIFFSEditor("","",WLED_FS));//http_username,http_password));
|
|
||||||
#endif
|
|
||||||
#else
|
|
||||||
editHandler = &server.on(F("/edit"), HTTP_GET, [](AsyncWebServerRequest *request){
|
|
||||||
serveMessage(request, 501, FPSTR(s_notimplemented), F("The FS editor is disabled in this build."), 254);
|
|
||||||
});
|
|
||||||
#endif
|
|
||||||
} else {
|
|
||||||
editHandler = &server.on(F("/edit"), HTTP_ANY, [](AsyncWebServerRequest *request){
|
|
||||||
serveMessage(request, 401, FPSTR(s_accessdenied), FPSTR(s_unlock_cfg), 254);
|
serveMessage(request, 401, FPSTR(s_accessdenied), FPSTR(s_unlock_cfg), 254);
|
||||||
});
|
return;
|
||||||
}
|
}
|
||||||
|
const String& func = request->arg(FPSTR(s_func));
|
||||||
|
|
||||||
|
if(func.length() == 0) {
|
||||||
|
// default: serve the editor page
|
||||||
|
handleStaticContent(request, FPSTR(_edit_htm), 200, FPSTR(CONTENT_TYPE_HTML), PAGE_edit, PAGE_edit_length);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (func == "list") {
|
||||||
|
bool first = true;
|
||||||
|
AsyncResponseStream* response = request->beginResponseStream(FPSTR(CONTENT_TYPE_JSON));
|
||||||
|
response->addHeader(FPSTR(s_cache_control), FPSTR(s_no_store));
|
||||||
|
response->addHeader(FPSTR(s_expires), F("0"));
|
||||||
|
response->write('[');
|
||||||
|
|
||||||
|
File rootdir = WLED_FS.open("/", "r");
|
||||||
|
File rootfile = rootdir.openNextFile();
|
||||||
|
while (rootfile) {
|
||||||
|
String name = rootfile.name();
|
||||||
|
if (name.indexOf(FPSTR(s_wsec)) >= 0) {
|
||||||
|
rootfile = rootdir.openNextFile(); // skip wsec.json
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (!first) response->write(',');
|
||||||
|
first = false;
|
||||||
|
response->printf_P(PSTR("{\"name\":\"%s\",\"type\":\"file\",\"size\":%u}"), name.c_str(), rootfile.size());
|
||||||
|
rootfile = rootdir.openNextFile();
|
||||||
|
}
|
||||||
|
rootfile.close();
|
||||||
|
rootdir.close();
|
||||||
|
response->write(']');
|
||||||
|
request->send(response);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
String path = request->arg(FPSTR(s_path)); // remaining functions expect a path
|
||||||
|
|
||||||
|
if (path.length() == 0) {
|
||||||
|
request->send(400, FPSTR(CONTENT_TYPE_PLAIN), F("Missing path"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (path.charAt(0) != '/') {
|
||||||
|
path = '/' + path; // prepend slash if missing
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!WLED_FS.exists(path)) {
|
||||||
|
request->send(404, FPSTR(CONTENT_TYPE_PLAIN), FPSTR(s_not_found));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (path.indexOf(FPSTR(s_wsec)) >= 0) {
|
||||||
|
request->send(403, FPSTR(CONTENT_TYPE_PLAIN), FPSTR(s_accessdenied)); // skip wsec.json
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (func == "edit") {
|
||||||
|
request->send(WLED_FS, path);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (func == "download") {
|
||||||
|
request->send(WLED_FS, path, String(), true);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (func == "delete") {
|
||||||
|
if (!WLED_FS.remove(path))
|
||||||
|
request->send(500, FPSTR(CONTENT_TYPE_PLAIN), F("Delete failed"));
|
||||||
|
else
|
||||||
|
request->send(200, FPSTR(CONTENT_TYPE_PLAIN), F("File deleted"));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// unrecognized func
|
||||||
|
request->send(400, FPSTR(CONTENT_TYPE_PLAIN), F("Invalid function"));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool captivePortal(AsyncWebServerRequest *request)
|
static bool captivePortal(AsyncWebServerRequest *request)
|
||||||
@@ -387,7 +464,7 @@ void initServer()
|
|||||||
size_t len, bool isFinal) {handleUpload(request, filename, index, data, len, isFinal);}
|
size_t len, bool isFinal) {handleUpload(request, filename, index, data, len, isFinal);}
|
||||||
);
|
);
|
||||||
|
|
||||||
createEditHandler(correctPIN);
|
createEditHandler(); // initialize "/edit" handler, access is protected by "correctPIN"
|
||||||
|
|
||||||
static const char _update[] PROGMEM = "/update";
|
static const char _update[] PROGMEM = "/update";
|
||||||
#ifndef WLED_DISABLE_OTA
|
#ifndef WLED_DISABLE_OTA
|
||||||
@@ -553,8 +630,8 @@ void serveSettingsJS(AsyncWebServerRequest* request)
|
|||||||
}
|
}
|
||||||
|
|
||||||
AsyncResponseStream *response = request->beginResponseStream(FPSTR(CONTENT_TYPE_JAVASCRIPT));
|
AsyncResponseStream *response = request->beginResponseStream(FPSTR(CONTENT_TYPE_JAVASCRIPT));
|
||||||
response->addHeader(F("Cache-Control"), F("no-store"));
|
response->addHeader(FPSTR(s_cache_control), FPSTR(s_no_store));
|
||||||
response->addHeader(F("Expires"), F("0"));
|
response->addHeader(FPSTR(s_expires), F("0"));
|
||||||
|
|
||||||
response->print(F("function GetV(){var d=document;"));
|
response->print(F("function GetV(){var d=document;"));
|
||||||
getSettingsJS(subPage, *response);
|
getSettingsJS(subPage, *response);
|
||||||
@@ -678,7 +755,6 @@ void serveSettings(AsyncWebServerRequest* request, bool post) {
|
|||||||
#endif
|
#endif
|
||||||
case SUBPAGE_LOCK : {
|
case SUBPAGE_LOCK : {
|
||||||
correctPIN = !strlen(settingsPIN); // lock if a pin is set
|
correctPIN = !strlen(settingsPIN); // lock if a pin is set
|
||||||
createEditHandler(correctPIN);
|
|
||||||
serveMessage(request, 200, strlen(settingsPIN) > 0 ? PSTR("Settings locked") : PSTR("No PIN set"), FPSTR(s_redirecting), 1);
|
serveMessage(request, 200, strlen(settingsPIN) > 0 ? PSTR("Settings locked") : PSTR("No PIN set"), FPSTR(s_redirecting), 1);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user