Upload files & skinning (#2084)

* Skinning WLED & uploading files.
Backup & restore configuration & presets.
External holidays.json

* Option for segment count instead of stop.

* Small fixes and improvements

* Further improvements

* Enable custom CSS by default

Co-authored-by: Christian Schwinne <dev.aircoookie@gmail.com>
This commit is contained in:
Blaž Kristan 2021-07-26 00:10:36 +02:00 committed by GitHub
parent b058fb8db4
commit 2e9bd477d9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 1107 additions and 881 deletions

View File

@ -26,8 +26,16 @@ var ws;
var fxlist = d.getElementById('fxlist'), pallist = d.getElementById('pallist'); var fxlist = d.getElementById('fxlist'), pallist = d.getElementById('pallist');
var cfg = { var cfg = {
theme:{base:"dark", bg:{url:""}, alpha:{bg:0.6,tab:0.8}, color:{bg:""}}, theme:{base:"dark", bg:{url:""}, alpha:{bg:0.6,tab:0.8}, color:{bg:""}},
comp :{colors:{picker: true, rgb: false, quick: true, hex: false}, labels:true, pcmbot:false, pid:true} comp :{colors:{picker: true, rgb: false, quick: true, hex: false},
labels:true, pcmbot:false, pid:true, seglen:false, css:true, hdays:false}
}; };
var hol = [
[0,11,24,4,"https://aircoookie.github.io/xmas.png"], // christmas
[0,2,17,1,"https://images.alphacoders.com/491/491123.jpg"], // st. Patrick's day
[2022,3,17,2,"https://aircoookie.github.io/easter.png"],
[2023,3,9,2,"https://aircoookie.github.io/easter.png"],
[2024,2,31,2,"https://aircoookie.github.io/easter.png"]
];
var cpick = new iro.ColorPicker("#picker", { var cpick = new iro.ColorPicker("#picker", {
width: 260, width: 260,
@ -158,13 +166,6 @@ function loadBg(iUrl) {
img.src = iUrl; img.src = iUrl;
if (iUrl == "") { if (iUrl == "") {
var today = new Date(); var today = new Date();
var hol = [
[0,11,24,4,"https://aircoookie.github.io/xmas.png"], // christmas
[0,2,17,1,"https://images.alphacoders.com/491/491123.jpg"], // st. Patrick's day
[2022,3,17,2,"https://aircoookie.github.io/easter.png"],
[2023,3,9,2,"https://aircoookie.github.io/easter.png"],
[2024,2,31,2,"https://aircoookie.github.io/easter.png"]
];
for (var i=0; i<hol.length; i++) { for (var i=0; i<hol.length; i++) {
var yr = hol[i][0]==0 ? today.getFullYear() : hol[i][0]; var yr = hol[i][0]==0 ? today.getFullYear() : hol[i][0];
var hs = new Date(yr,hol[i][1],hol[i][2]); var hs = new Date(yr,hol[i][1],hol[i][2]);
@ -182,6 +183,21 @@ function loadBg(iUrl) {
}); });
} }
function loadSkinCSS(cId)
{
if (!d.getElementById(cId)) // check if element exists
{
var h = document.getElementsByTagName('head')[0];
var l = document.createElement('link');
l.id = cId;
l.rel = 'stylesheet';
l.type = 'text/css';
l.href = (loc?`http://${locip}`:'.') + '/skin.css';
l.media = 'all';
h.appendChild(l);
}
}
function onLoad() { function onLoad() {
if (window.location.protocol == "file:") { if (window.location.protocol == "file:") {
loc = true; loc = true;
@ -198,7 +214,27 @@ function onLoad() {
resetPUtil(); resetPUtil();
applyCfg(); applyCfg();
loadBg(cfg.theme.bg.url); if (cfg.comp.hdays) { //load custom holiday list
fetch((loc?`http://${locip}`:'.') + "/holidays.json", { // may be loaded from external source
method: 'get'
})
.then(res => {
//if (!res.ok) showErrorToast();
return res.json();
})
.then(json => {
if (Array.isArray(json)) hol = json;
//TODO: do some parsing first
})
.catch(function (error) {
console.log("holidays.json does not contain array of holidays. Defaults loaded.");
})
.finally(function(){
loadBg(cfg.theme.bg.url);
});
} else
loadBg(cfg.theme.bg.url);
if (cfg.comp.css) loadSkinCSS('skinCss');
var cd = d.getElementById('csl').children; var cd = d.getElementById('csl').children;
for (var i = 0; i < cd.length; i++) { for (var i = 0; i < cd.length; i++) {
@ -211,7 +247,7 @@ function onLoad() {
setColor(1); setColor(1);
}); });
pmtLS = localStorage.getItem('wledPmt'); pmtLS = localStorage.getItem('wledPmt');
setTimeout(function(){requestJson(null, false);}, 25); setTimeout(function(){requestJson(null, false);}, 50);
d.addEventListener("visibilitychange", handleVisibilityChange, false); d.addEventListener("visibilitychange", handleVisibilityChange, false);
size(); size();
d.getElementById("cv").style.opacity=0; d.getElementById("cv").style.opacity=0;
@ -557,12 +593,12 @@ function populateSegments(s)
<table class="infot"> <table class="infot">
<tr> <tr>
<td class="segtd">Start LED</td> <td class="segtd">Start LED</td>
<td class="segtd">Stop LED</td> <td class="segtd">${cfg.comp.seglen?"Length":"Stop LED"}</td>
<td class="segtd">Offset</td> <td class="segtd">Offset</td>
</tr> </tr>
<tr> <tr>
<td class="segtd"><input class="noslide segn" id="seg${i}s" type="number" min="0" max="${ledCount-1}" value="${inst.start}" oninput="updateLen(${i})"></td> <td class="segtd"><input class="noslide segn" id="seg${i}s" type="number" min="0" max="${ledCount-1}" value="${inst.start}" oninput="updateLen(${i})"></td>
<td class="segtd"><input class="noslide segn" id="seg${i}e" type="number" min="0" max="${ledCount}" value="${inst.stop}" oninput="updateLen(${i})"></td> <td class="segtd"><input class="noslide segn" id="seg${i}e" type="number" min="0" max="${ledCount-(cfg.comp.seglen?inst.start:0)}" value="${inst.stop-(cfg.comp.seglen?inst.start:0)}" oninput="updateLen(${i})"></td>
<td class="segtd"><input class="noslide segn" id="seg${i}of" type="number" value="${inst.of}" oninput="updateLen(${i})"></td> <td class="segtd"><input class="noslide segn" id="seg${i}of" type="number" value="${inst.of}" oninput="updateLen(${i})"></td>
</tr> </tr>
</table> </table>
@ -661,13 +697,12 @@ function populatePalettes(palettes)
var html = `<div class="searchbar"><input type="text" class="search" placeholder="Search" oninput="search(this)" /> var html = `<div class="searchbar"><input type="text" class="search" placeholder="Search" oninput="search(this)" />
<i class="icons search-cancel-icon" onclick="cancelSearch(this)">&#xe38f;</i></div>`; <i class="icons search-cancel-icon" onclick="cancelSearch(this)">&#xe38f;</i></div>`;
for (let i = 0; i < palettes.length; i++) { for (let i = 0; i < palettes.length; i++) {
let previewCss = genPalPrevCss(palettes[i].id);
html += generateListItemHtml( html += generateListItemHtml(
'palette', 'palette',
palettes[i].id, palettes[i].id,
palettes[i].name, palettes[i].name,
'setPalette', 'setPalette',
`<div class="lstIprev" style="${previewCss}"></div>`, `<div class="lstIprev" style="${genPalPrevCss(palettes[i].id)}"></div>`,
palettes[i].class, palettes[i].class,
); );
} }
@ -693,7 +728,6 @@ function genPalPrevCss(id)
return; return;
} }
var paletteData = palettesData[id]; var paletteData = palettesData[id];
var previewCss = "";
if (!paletteData) { if (!paletteData) {
return 'display: none'; return 'display: none';
@ -855,7 +889,7 @@ function updateLen(s)
if (!d.getElementById(`seg${s}s`)) return; if (!d.getElementById(`seg${s}s`)) return;
var start = parseInt(d.getElementById(`seg${s}s`).value); var start = parseInt(d.getElementById(`seg${s}s`).value);
var stop = parseInt(d.getElementById(`seg${s}e`).value); var stop = parseInt(d.getElementById(`seg${s}e`).value);
var len = stop - start; var len = stop - (cfg.comp.seglen?0:start);
var out = "(delete)"; var out = "(delete)";
if (len > 1) { if (len > 1) {
out = `${len} LEDs`; out = `${len} LEDs`;
@ -1218,7 +1252,7 @@ function toggleNodes() {
function makeSeg() { function makeSeg() {
var ns = 0; var ns = 0;
if (lowestUnused > 0) { if (lowestUnused > 0) {
var pend = d.getElementById(`seg${lowestUnused -1}e`).value; var pend = parseInt(d.getElementById(`seg${lowestUnused -1}e`).value,10) + (cfg.comp.seglen?parseInt(d.getElementById(`seg${lowestUnused -1}s`).value,10):0);
if (pend < ledCount) ns = pend; if (pend < ledCount) ns = pend;
} }
var cn = `<div class="seg"> var cn = `<div class="seg">
@ -1230,11 +1264,11 @@ function makeSeg() {
<table class="segt"> <table class="segt">
<tr> <tr>
<td class="segtd">Start LED</td> <td class="segtd">Start LED</td>
<td class="segtd">Stop LED</td> <td class="segtd">${cfg.comp.seglen?"Length":"Stop LED"}</td>
</tr> </tr>
<tr> <tr>
<td class="segtd"><input class="noslide segn" id="seg${lowestUnused}s" type="number" min="0" max="${ledCount-1}" value="${ns}" oninput="updateLen(${lowestUnused})"></td> <td class="segtd"><input class="noslide segn" id="seg${lowestUnused}s" type="number" min="0" max="${ledCount-1}" value="${ns}" oninput="updateLen(${lowestUnused})"></td>
<td class="segtd"><input class="noslide segn" id="seg${lowestUnused}e" type="number" min="0" max="${ledCount}" value="${ledCount}" oninput="updateLen(${lowestUnused})"></td> <td class="segtd"><input class="noslide segn" id="seg${lowestUnused}e" type="number" min="0" max="${ledCount-(cfg.comp.seglen?ns:0)}" value="${ledCount-(cfg.comp.seglen?ns:0)}" oninput="updateLen(${lowestUnused})"></td>
</tr> </tr>
</table> </table>
<div class="h" id="seg${lowestUnused}len">${ledCount - ns} LED${ledCount - ns >1 ? "s":""}</div> <div class="h" id="seg${lowestUnused}len">${ledCount - ns} LED${ledCount - ns >1 ? "s":""}</div>
@ -1467,7 +1501,7 @@ function setSeg(s){
var start = parseInt(d.getElementById(`seg${s}s`).value); var start = parseInt(d.getElementById(`seg${s}s`).value);
var stop = parseInt(d.getElementById(`seg${s}e`).value); var stop = parseInt(d.getElementById(`seg${s}e`).value);
if (stop <= start) {delSeg(s); return;} if (stop <= start) {delSeg(s); return;}
var obj = {"seg": {"id": s, "start": start, "stop": stop}}; var obj = {"seg": {"id": s, "start": start, "stop": (cfg.comp.seglen?start:0)+stop}};
if (d.getElementById(`seg${s}grp`)) if (d.getElementById(`seg${s}grp`))
{ {
var grp = parseInt(d.getElementById(`seg${s}grp`).value); var grp = parseInt(d.getElementById(`seg${s}grp`).value);

View File

@ -18,6 +18,16 @@
function off(n){ function off(n){
d.getElementsByName(n)[0].value = -1; d.getElementsByName(n)[0].value = -1;
} }
var timeout;
function showToast(text, error = false)
{
var x = gId("toast");
x.innerHTML = text;
x.className = error ? "error":"show";
clearTimeout(timeout);
x.style.animation = 'none';
timeout = setTimeout(function(){ x.className = x.className.replace("show", ""); }, 2900);
}
function bLimits(b,p,m) { function bLimits(b,p,m) {
maxB = b; maxM = m; maxPB = p; maxB = b; maxM = m; maxPB = p;
} }
@ -204,6 +214,7 @@
s2 += "A is enough)<br>"; s2 += "A is enough)<br>";
gId('psu').innerHTML = s; gId('psu').innerHTML = s;
gId('psu2').innerHTML = isWS2815 ? "" : s2; gId('psu2').innerHTML = isWS2815 ? "" : s2;
gId("json").style.display = d.Sf.IT.value==8 ? "" : "none";
} }
function lastEnd(i) { function lastEnd(i) {
if (i<1) return 0; if (i<1) return 0;
@ -293,6 +304,17 @@ Reverse (rotated 180°): <input type="checkbox" name="CV${i}">
c += `</select>`; c += `</select>`;
c += `<span style="cursor: pointer;" onclick="off('${bt}')">&nbsp;&#215;</span><br>`; c += `<span style="cursor: pointer;" onclick="off('${bt}')">&nbsp;&#215;</span><br>`;
gId("btns").innerHTML = c; gId("btns").innerHTML = c;
}
function uploadFile(name) {
var req = new XMLHttpRequest();
req.addEventListener('load', function(){showToast(this.responseText)});
req.addEventListener('error', function(e){showToast(e.stack,true);});
req.open("POST", "/upload");
var formData = new FormData();
formData.append("data", d.Sf.data.files[0], name);
req.send(formData);
d.Sf.data.value = '';
return false;
} }
function GetV() function GetV()
{ {
@ -350,7 +372,7 @@ Reverse (rotated 180°): <input type="checkbox" name="CV${i}">
</div><hr style="width:260px"> </div><hr style="width:260px">
<div id="btns"></div> <div id="btns"></div>
Touch threshold: <input type="number" class="s" min="0" max="100" name="TT" required><br> Touch threshold: <input type="number" class="s" min="0" max="100" name="TT" required><br>
IR pin: <input type="number" class="xs" min="-1" max="40" name="IR" onchange="UI()">&nbsp;<select name="IT"> IR pin: <input type="number" class="xs" min="-1" max="40" name="IR" onchange="UI()">&nbsp;<select name="IT" onchange="UI()">
<option value=0>Remote disabled</option> <option value=0>Remote disabled</option>
<option value=1>24-key RGB</option> <option value=1>24-key RGB</option>
<option value=2>24-key with CT</option> <option value=2>24-key with CT</option>
@ -361,6 +383,8 @@ Reverse (rotated 180°): <input type="checkbox" name="CV${i}">
<option value=7>9-key red</option> <option value=7>9-key red</option>
<option value=8>JSON remote</option> <option value=8>JSON remote</option>
</select><span style="cursor: pointer;" onclick="off('IR')">&nbsp;&#215;</span><br> </select><span style="cursor: pointer;" onclick="off('IR')">&nbsp;&#215;</span><br>
<div id="json" style="display:none;">JSON file: <input type="file" name="data" accept=".json"> <input type="button" value="Upload" onclick="uploadFile('/ir.json');"><br></div>
<div id="toast"></div>
<a href="https://github.com/Aircoookie/WLED/wiki/Infrared-Control" target="_blank">IR info</a><br> <a href="https://github.com/Aircoookie/WLED/wiki/Infrared-Control" target="_blank">IR info</a><br>
Relay pin: <input type="number" class="xs" min="-1" max="33" name="RL" onchange="UI()"> Invert <input type="checkbox" name="RM"><span style="cursor: pointer;" onclick="off('RL')">&nbsp;&#215;</span><br> Relay pin: <input type="number" class="xs" min="-1" max="33" name="RL" onchange="UI()"> Invert <input type="checkbox" name="RM"><span style="cursor: pointer;" onclick="off('RL')">&nbsp;&#215;</span><br>
<hr style="width:260px"> <hr style="width:260px">
@ -404,7 +428,7 @@ Reverse (rotated 180°): <input type="checkbox" name="CV${i}">
<option value=4>Legacy</option> <option value=4>Legacy</option>
</select> </select>
<br></span><hr> <br></span><hr>
<button type="button" onclick="B()">Back</button><button type="submit">Save</button> <button type="button" onclick="B()">Back</button><button type="submit">Save</button>
</form> </form>
</body> </body>
</html> </html>

View File

@ -5,6 +5,7 @@
<meta charset="utf-8"> <meta charset="utf-8">
<title>Misc Settings</title> <title>Misc Settings</title>
<script> <script>
var d = document;
function H() function H()
{ {
window.open("https://github.com/Aircoookie/WLED/wiki/Settings#security-settings"); window.open("https://github.com/Aircoookie/WLED/wiki/Settings#security-settings");
@ -17,6 +18,34 @@
{ {
window.open("/update","_self"); window.open("/update","_self");
} }
function gId(s)
{
return d.getElementById(s);
}
function isObject(item) {
return (item && typeof item === 'object' && !Array.isArray(item));
}
var timeout;
function showToast(text, error = false)
{
var x = gId("toast");
x.innerHTML = text;
x.className = error ? "error":"show";
clearTimeout(timeout);
x.style.animation = 'none';
timeout = setTimeout(function(){ x.className = x.className.replace("show", ""); }, 2900);
}
function uploadFile(fO,name) {
var req = new XMLHttpRequest();
req.addEventListener('load', function(){showToast(this.responseText)});
req.addEventListener('error', function(e){showToast(e.stack,true);});
req.open("POST", "/upload");
var formData = new FormData();
formData.append("data", fO.files[0], name);
req.send(formData);
fO.value = '';
return false;
}
function GetV() function GetV()
{ {
//values injected by server while sending HTML //values injected by server while sending HTML
@ -44,6 +73,14 @@
<h3>Software Update</h3> <h3>Software Update</h3>
<button type="button" onclick="U()">Manual OTA Update</button><br> <button type="button" onclick="U()">Manual OTA Update</button><br>
Enable ArduinoOTA: <input type="checkbox" name="AO"><br> Enable ArduinoOTA: <input type="checkbox" name="AO"><br>
<h3>Backup & Restore</h3>
<a class="btn lnk" href="/presets.json?download" target="download-frame">Backup presets</a><br>
<div>Restore presets<br><input type="file" name="data" accept=".json"> <input type="button" value="Upload" onclick="uploadFile(d.Sf.data,'/presets.json');"><br></div><br>
<a class="btn lnk" href="/cfg.json?download" target="download-frame">Backup configuration</a><br>
<div>Restore configuration<br><input type="file" name="data2" accept=".json"> <input type="button" value="Upload" onclick="uploadFile(d.Sf.data2,'/cfg.json');"><br></div>
<div style="color: #fa0;">&#9888; Restoring presets/configuration will OVERWRITE your current presets/configuration.<br>
Incorrect configuration may require a factory reset or re-flashing of your ESP.</div>
For security reasons, passwords are not backed up.
<h3>About</h3> <h3>About</h3>
<a href="https://github.com/Aircoookie/WLED/" target="_blank">WLED</a> version ##VERSION##<!-- Autoreplaced from package.json --><br><br> <a href="https://github.com/Aircoookie/WLED/" target="_blank">WLED</a> version ##VERSION##<!-- Autoreplaced from package.json --><br><br>
<a href="https://github.com/Aircoookie/WLED/wiki/Contributors-and-credits" target="_blank">Contributors, dependencies and special thanks</a><br> <a href="https://github.com/Aircoookie/WLED/wiki/Contributors-and-credits" target="_blank">Contributors, dependencies and special thanks</a><br>
@ -51,7 +88,9 @@
(c) 2016-2021 Christian Schwinne <br> (c) 2016-2021 Christian Schwinne <br>
<i>Licensed under the <a href="https://github.com/Aircoookie/WLED/blob/master/LICENSE" target="_blank">MIT license</a></i><br><br> <i>Licensed under the <a href="https://github.com/Aircoookie/WLED/blob/master/LICENSE" target="_blank">MIT license</a></i><br><br>
Server message: <span class="sip"> Response error! </span><hr> Server message: <span class="sip"> Response error! </span><hr>
<div id="toast"></div>
<button type="button" onclick="B()">Back</button><button type="submit">Save & Reboot</button> <button type="button" onclick="B()">Back</button><button type="submit">Save & Reboot</button>
</form> </form>
<iframe name=download-frame style='display:none;'></iframe>
</body> </body>
</html> </html>

View File

@ -1,7 +1,7 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head lang="en"> <head lang="en">
<meta charset="utf-8"> <meta charset="utf-8">
<meta name="viewport" content="width=500"> <meta name="viewport" content="width=500">
<title>UI Settings</title> <title>UI Settings</title>
<script> <script>
@ -10,16 +10,19 @@
var sett = null; var sett = null;
var l = { var l = {
"comp":{ "comp":{
"labels":"Show button labels", "labels":"Show button labels",
"colors":{ "colors":{
"LABEL":"Color selection methods", "LABEL":"Color selection methods",
"picker": "Color Wheel", "picker": "Color Wheel",
"rgb": "RGB sliders", "rgb": "RGB sliders",
"quick": "Quick color selectors", "quick": "Quick color selectors",
"hex": "HEX color input" "hex": "HEX color input"
}, },
"pcmbot": "Show bottom tab bar in PC mode", "pcmbot": "Show bottom tab bar in PC mode",
"pid": "Show preset IDs" "pid": "Show preset IDs",
"seglen": "Set segment length instead of stop LED",
"css": "Enable custom CSS",
"hdays": "Enable custom Holidays list"
}, },
"theme":{ "theme":{
"alpha": { "alpha": {
@ -34,7 +37,6 @@
"bg":"BG HEX color" "bg":"BG HEX color"
} }
} }
}; };
function gId(s) function gId(s)
{ {
@ -52,10 +54,18 @@
if( !tar[elem] ) tar[elem] = {} if( !tar[elem] ) tar[elem] = {}
tar = tar[elem]; tar = tar[elem];
} }
tar[pList[len-1]] = val; tar[pList[len-1]] = val;
} }
var timeout;
function showToast(text, error = false)
{
var x = gId("toast");
x.innerHTML = text;
x.className = error ? "error":"show";
clearTimeout(timeout);
x.style.animation = 'none';
timeout = setTimeout(function(){ x.className = x.className.replace("show", ""); }, 2900);
}
function addRec(s, path = "", label = null) function addRec(s, path = "", label = null)
{ {
var str = ""; var str = "";
@ -181,12 +191,20 @@
gId("theme_bg_random").checked = false; gId("theme_bg_random").checked = false;
} }
} }
function uploadFile(fO,name) {
var req = new XMLHttpRequest();
req.addEventListener('load', function(){showToast(this.responseText)});
req.addEventListener('error', function(e){showToast(e.stack,true);});
req.open("POST", "/upload");
var formData = new FormData();
formData.append("data", fO.files[0], name);
req.send(formData);
fO.value = '';
return false;
}
function GetV(){var d=document;} function GetV(){var d=document;}
</script> </script>
<style> <style>@import url("style.css");</style>
@import url("style.css");
</style>
</head> </head>
<body onload="S()"> <body onload="S()">
<form id="form_s" name="Sf" method="post"> <form id="form_s" name="Sf" method="post">
@ -198,7 +216,7 @@
</div> </div>
<h2>Web Setup</h2> <h2>Web Setup</h2>
Server description: <input name="DS" maxlength="32"><br> Server description: <input name="DS" maxlength="32"><br>
Sync button toggles both send and receive: <input type="checkbox" name="ST"><br> Sync button toggles both send and receive: <input type="checkbox" name="ST"><br>
<i>The following UI customization settings are unique both to the WLED device and this browser.<br> <i>The following UI customization settings are unique both to the WLED device and this browser.<br>
You will need to set them again if using a different browser, device or WLED IP address.<br> You will need to set them again if using a different browser, device or WLED IP address.<br>
Refresh the main UI to apply changes.</i><br> Refresh the main UI to apply changes.</i><br>
@ -207,8 +225,9 @@
<h3>UI Appearance</h3> <h3>UI Appearance</h3>
<span class="l"></span>: <input type="checkbox" id="comp_labels" class="agi cb"><br> <span class="l"></span>: <input type="checkbox" id="comp_labels" class="agi cb"><br>
<span class="l"></span>: <input type="checkbox" id="comp_pcmbot" class="agi cb"><br> <span class="l"></span>: <input type="checkbox" id="comp_pcmbot" class="agi cb"><br>
<span class="l"></span>: <input type="checkbox" id="comp_pid" class="agi cb"><br> <span class="l"></span>: <input type="checkbox" id="comp_pid" class="agi cb"><br>
<span class="l"></span>: <input type="checkbox" id="comp_seglen" class="agi cb"><br>
I hate dark mode: <input type="checkbox" id="dm" onchange="UI()"><br> I hate dark mode: <input type="checkbox" id="dm" onchange="UI()"><br>
<span id="idonthateyou" style="display:none"><i>Why would you? </i>&#x1F97A;<br></span> <span id="idonthateyou" style="display:none"><i>Why would you? </i>&#x1F97A;<br></span>
<span class="l"></span>: <input type="number" min=0.0 max=1.0 step=0.01 id="theme_alpha_tab" class="agi"><br> <span class="l"></span>: <input type="number" min=0.0 max=1.0 step=0.01 id="theme_alpha_tab" class="agi"><br>
@ -217,6 +236,11 @@
<span class="l">BG image URL</span>: <input id="theme_bg_url" class="agi" oninput="checkRandomBg()"><br> <span class="l">BG image URL</span>: <input id="theme_bg_url" class="agi" oninput="checkRandomBg()"><br>
<span class="l">Random BG image</span>: <input type="checkbox" id="theme_bg_random" class="agi cb" onchange="setRandomBg()"><br> <span class="l">Random BG image</span>: <input type="checkbox" id="theme_bg_random" class="agi cb" onchange="setRandomBg()"><br>
<input id="theme_base" class="agi" style="display:none"> <input id="theme_base" class="agi" style="display:none">
<span class="l"></span>: <input type="checkbox" id="comp_css" class="agi cb"><br>
<div id="skin">Custom CSS: <input type="file" name="data" accept=".css"> <input type="button" value="Upload" onclick="uploadFile(d.Sf.data,'/skin.css');"><br></div>
<span class="l"></span>: <input type="checkbox" id="comp_hdays" class="agi cb"><br>
<div id="holidays">Holidays: <input type="file" name="data2" accept=".json"> <input type="button" value="Upload" onclick="uploadFile(d.Sf.data2,'/holidays.json');"><br></div>
<div id="toast"></div>
<hr><button type="button" onclick="B()">Back</button><button type="button" onclick="Save()">Save</button> <hr><button type="button" onclick="B()">Back</button><button type="button" onclick="Save()">Save</button>
</form> </form>
</body> </body>

View File

@ -9,16 +9,20 @@ body {
hr { hr {
border-color: #666; border-color: #666;
} }
button { button, .btn {
background: #333; background: #333;
color: #fff; color: #fff;
font-family: Verdana, sans-serif; font-family: Verdana, sans-serif;
border: 0.3ch solid #333; border: 0.3ch solid #333;
display: inline-block; display: inline-block;
font-size: 20px; font-size: 20px;
margin: 8px; margin: 12px 8px 8px;
margin-top: 12px; padding: 1px 6px;
cursor: pointer; cursor: pointer;
text-decoration: none;
}
.lnk {
border: 0;
} }
.helpB { .helpB {
text-align: left; text-align: left;
@ -42,16 +46,16 @@ input[type="number"].xl {
width: 85px; width: 85px;
} }
input[type="number"].l { input[type="number"].l {
width: 60px; width: 63px;
} }
input[type="number"].m { input[type="number"].m {
width: 55px; width: 56px;
} }
input[type="number"].s { input[type="number"].s {
width: 42px; width: 49px;
} }
input[type="number"].xs { input[type="number"].xs {
width: 35px; width: 42px;
} }
input[type="checkbox"] { input[type="checkbox"] {
transform: scale(1.5); transform: scale(1.5);
@ -69,3 +73,32 @@ td {
.d5 { .d5 {
width: 4.5em !important; width: 4.5em !important;
} }
#toast {
opacity: 0;
background-color: #444;
border-radius: 5px;
bottom: 64px;
color: #fff;
font-size: 17px;
padding: 16px;
pointer-events: none;
position: fixed;
text-align: center;
z-index: 5;
transform: translateX(-50%%); /* %% because of AsyncWebServer */
max-width: 90%%; /* %% because of AsyncWebServer */
left: 50%%; /* %% because of AsyncWebServer */
}
#toast.show {
opacity: 1;
background-color: #264;
animation: fadein 0.5s, fadein 0.5s 2.5s reverse;
}
#toast.error {
opacity: 1;
background-color: #b21;
animation: fadein 0.5s;
}

View File

@ -379,7 +379,7 @@ String getContentType(AsyncWebServerRequest* request, String filename){
if(request->hasArg("download")) return "application/octet-stream"; if(request->hasArg("download")) return "application/octet-stream";
else if(filename.endsWith(".htm")) return "text/html"; else if(filename.endsWith(".htm")) return "text/html";
else if(filename.endsWith(".html")) return "text/html"; else if(filename.endsWith(".html")) return "text/html";
// else if(filename.endsWith(".css")) return "text/css"; else if(filename.endsWith(".css")) return "text/css";
// else if(filename.endsWith(".js")) return "application/javascript"; // else if(filename.endsWith(".js")) return "application/javascript";
else if(filename.endsWith(".json")) return "application/json"; else if(filename.endsWith(".json")) return "application/json";
else if(filename.endsWith(".png")) return "image/png"; else if(filename.endsWith(".png")) return "image/png";

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load Diff

View File

@ -8,7 +8,7 @@
*/ */
// version code in format yymmddb (b = daily build) // version code in format yymmddb (b = daily build)
#define VERSION 2107100 #define VERSION 2107230
//uncomment this if you have a "my_config.h" file you'd like to use //uncomment this if you have a "my_config.h" file you'd like to use
//#define WLED_USE_MY_CONFIG //#define WLED_USE_MY_CONFIG

View File

@ -15,6 +15,22 @@ bool isIp(String str) {
return true; return true;
} }
void handleUpload(AsyncWebServerRequest *request, const String& filename, size_t index, uint8_t *data, size_t len, bool final){
if(!index){
request->_tempFile = WLED_FS.open(filename, "w");
DEBUG_PRINT("Uploading ");
DEBUG_PRINTLN(filename);
if (filename == "/presets.json") presetsModifiedTime = toki.second();
}
if (len) {
request->_tempFile.write(data,len);
}
if(final){
request->_tempFile.close();
request->send(200, "text/plain", F("File Uploaded!"));
}
}
bool captivePortal(AsyncWebServerRequest *request) bool captivePortal(AsyncWebServerRequest *request)
{ {
if (ON_STA_FILTER(request)) return false; //only serve captive in AP mode if (ON_STA_FILTER(request)) return false; //only serve captive in AP mode
@ -95,7 +111,12 @@ void initServer()
const String& url = request->url(); const String& url = request->url();
isConfig = url.indexOf("cfg") > -1; isConfig = url.indexOf("cfg") > -1;
if (!isConfig) { if (!isConfig) {
fileDoc = &jsonBuffer; #ifdef WLED_DEBUG
DEBUG_PRINTLN(F("Serialized HTTP"));
serializeJson(root,Serial);
DEBUG_PRINTLN();
#endif
fileDoc = &jsonBuffer; // used for applying presets (presets.cpp)
verboseResponse = deserializeState(root); verboseResponse = deserializeState(root);
fileDoc = nullptr; fileDoc = nullptr;
} else { } else {
@ -136,7 +157,12 @@ void initServer()
server.on("/teapot", HTTP_GET, [](AsyncWebServerRequest *request){ server.on("/teapot", HTTP_GET, [](AsyncWebServerRequest *request){
serveMessage(request, 418, F("418. I'm a teapot."), F("(Tangible Embedded Advanced Project Of Twinkling)"), 254); serveMessage(request, 418, F("418. I'm a teapot."), F("(Tangible Embedded Advanced Project Of Twinkling)"), 254);
}); });
server.on("/upload", HTTP_POST, [](AsyncWebServerRequest *request) {},
[](AsyncWebServerRequest *request, const String& filename, size_t index, uint8_t *data,
size_t len, bool final) {handleUpload(request, filename, index, data, len, final);}
);
//if OTA is allowed //if OTA is allowed
if (!otaLock){ if (!otaLock){
#ifdef WLED_ENABLE_FS_EDITOR #ifdef WLED_ENABLE_FS_EDITOR
@ -197,15 +223,15 @@ void initServer()
} }
#ifdef WLED_ENABLE_DMX #ifdef WLED_ENABLE_DMX
server.on("/dmxmap", HTTP_GET, [](AsyncWebServerRequest *request){ server.on("/dmxmap", HTTP_GET, [](AsyncWebServerRequest *request){
request->send_P(200, "text/html", PAGE_dmxmap , dmxProcessor); request->send_P(200, "text/html", PAGE_dmxmap , dmxProcessor);
}); });
#else #else
server.on("/dmxmap", HTTP_GET, [](AsyncWebServerRequest *request){ server.on("/dmxmap", HTTP_GET, [](AsyncWebServerRequest *request){
serveMessage(request, 501, "Not implemented", F("DMX support is not enabled in this build."), 254); serveMessage(request, 501, "Not implemented", F("DMX support is not enabled in this build."), 254);
}); });
#endif #endif
server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){ server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){
if (captivePortal(request)) return; if (captivePortal(request)) return;
serveIndexOrWelcome(request); serveIndexOrWelcome(request);
@ -261,7 +287,13 @@ bool handleIfNoneMatchCacheHeader(AsyncWebServerRequest* request)
void setStaticContentCacheHeaders(AsyncWebServerResponse *response) void setStaticContentCacheHeaders(AsyncWebServerResponse *response)
{ {
#ifndef WLED_DEBUG
//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
response->addHeader(F("Cache-Control"),"no-cache"); response->addHeader(F("Cache-Control"),"no-cache");
#else
response->addHeader(F("Cache-Control"),"no-store,max-age=0"); // prevent caching if debug build
#endif
response->addHeader(F("ETag"), String(VERSION)); response->addHeader(F("ETag"), String(VERSION));
} }
@ -275,7 +307,6 @@ void serveIndex(AsyncWebServerRequest* request)
response->addHeader(F("Content-Encoding"),"gzip"); response->addHeader(F("Content-Encoding"),"gzip");
setStaticContentCacheHeaders(response); setStaticContentCacheHeaders(response);
request->send(response); request->send(response);
} }