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:
parent
b058fb8db4
commit
2e9bd477d9
@ -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)"></i></div>`;
|
<i class="icons search-cancel-icon" onclick="cancelSearch(this)"></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);
|
||||||
|
@ -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}')"> ×</span><br>`;
|
c += `<span style="cursor: pointer;" onclick="off('${bt}')"> ×</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()"> <select name="IT">
|
IR pin: <input type="number" class="xs" min="-1" max="40" name="IR" onchange="UI()"> <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')"> ×</span><br>
|
</select><span style="cursor: pointer;" onclick="off('IR')"> ×</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')"> ×</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')"> ×</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>
|
||||||
|
@ -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;">⚠ 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>
|
@ -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>🥺<br></span>
|
<span id="idonthateyou" style="display:none"><i>Why would you? </i>🥺<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>
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
@ -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
1615
wled00/html_ui.h
1615
wled00/html_ui.h
File diff suppressed because it is too large
Load Diff
@ -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
|
||||||
|
@ -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 {
|
||||||
@ -137,6 +158,11 @@ void initServer()
|
|||||||
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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user