3603 lines
116 KiB
JavaScript
3603 lines
116 KiB
JavaScript
//page js
|
|
var loc = false, locip;
|
|
var isOn = false, nlA = false, isLv = false, isInfo = false, isNodes = false, syncSend = false, syncTglRecv = true;
|
|
var hasWhite = false, hasRGB = false, hasCCT = false;
|
|
var nlDur = 60, nlTar = 0;
|
|
var nlMode = false;
|
|
var segLmax = 0; // size (in pixels) of largest selected segment
|
|
var selectedFx = 0;
|
|
var selectedPal = 0;
|
|
var csel = 0; // selected color slot (0-2)
|
|
var currentPreset = -1;
|
|
var lastUpdate = 0;
|
|
var segCount = 0, ledCount = 0, lowestUnused = 0, maxSeg = 0, lSeg = 0;
|
|
var pcMode = false, pcModeA = false, lastw = 0, wW;
|
|
var tr = 7;
|
|
var d = document;
|
|
var palettesData;
|
|
var fxdata = [];
|
|
var pJson = {}, eJson = {}, lJson = {};
|
|
var plJson = {}; // array of playlists
|
|
var pN = "", pI = 0, pNum = 0;
|
|
var pmt = 1, pmtLS = 0, pmtLast = 0;
|
|
var lastinfo = {};
|
|
var isM = false, mw = 0, mh=0;
|
|
var ws, cpick, ranges;
|
|
var cfg = {
|
|
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, seglen:false, segpwr:false, segexp:true,
|
|
css:true, hdays:false, fxdef:true} //WLEDMM segexp true as default
|
|
};
|
|
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
|
|
[2025,3,20,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"],
|
|
[0,6,4,1,"https://initiate.alphacoders.com/download/wallpaper/516792/images/jpg/510921363292536"], // 4th of July
|
|
[0,0,1,1,"https://initiate.alphacoders.com/download/wallpaper/1198800/images/jpg/2522807481585600"] // new year
|
|
];
|
|
var ctx = null; // WLEDMM
|
|
var ledmapNr = -1; //WLEDMM
|
|
var ledmapFileNames = []; //WLEDMM
|
|
let nodesData = []; //WLEDMM
|
|
|
|
function handleVisibilityChange() {if (!d.hidden && new Date () - lastUpdate > 3000) requestJson();}
|
|
function sCol(na, col) {d.documentElement.style.setProperty(na, col);}
|
|
function gId(c) {return d.getElementById(c);}
|
|
function gEBCN(c) {return d.getElementsByClassName(c);}
|
|
function isEmpty(o) {return Object.keys(o).length === 0;}
|
|
function isObj(i) {return (i && typeof i === 'object' && !Array.isArray(i));}
|
|
function isNumeric(n) {return !isNaN(parseFloat(n)) && isFinite(n);}
|
|
|
|
// returns true if dataset R, G & B values are 0
|
|
function isRgbBlack(a) {return (parseInt(a.r) == 0 && parseInt(a.g) == 0 && parseInt(a.b) == 0);}
|
|
|
|
// returns RGB color from a given dataset
|
|
function rgbStr(a) {return "rgb(" + a.r + "," + a.g + "," + a.b + ")";}
|
|
|
|
// brightness approximation for selecting white as text color if background bri < 127, and black if higher
|
|
function rgbBri(a) {return 0.2126*parseInt(a.r) + 0.7152*parseInt(a.g) + 0.0722*parseInt(a.b);}
|
|
|
|
// sets background of color slot selectors
|
|
function setCSL(cs)
|
|
{
|
|
let w = cs.dataset.w ? parseInt(cs.dataset.w) : 0;
|
|
let hasShadow = getComputedStyle(cs).textShadow !== "none";
|
|
if (hasRGB && !isRgbBlack(cs.dataset)) {
|
|
if (!hasShadow) cs.style.color = rgbBri(cs.dataset) > 127 ? "#000":"#fff"; // if text has no CSS "shadow"
|
|
cs.style.background = (hasWhite && w > 0) ? `linear-gradient(180deg, ${rgbStr(cs.dataset)} 30%, rgb(${w},${w},${w}))` : rgbStr(cs.dataset);
|
|
} else {
|
|
if (hasRGB && !hasWhite) w = 0;
|
|
cs.style.background = `rgb(${w},${w},${w})`;
|
|
if (!hasShadow) cs.style.color = w > 127 ? "#000":"#fff";
|
|
}
|
|
}
|
|
|
|
function applyCfg()
|
|
{
|
|
cTheme(cfg.theme.base === "light");
|
|
var bg = cfg.theme.color.bg;
|
|
if (bg) sCol('--c-1', bg);
|
|
var l = cfg.comp.labels;
|
|
sCol('--tbp', l ? "14px 14px 10px 14px":"10px 22px 4px 22px");
|
|
sCol('--bbp', l ? "9px 0 7px 0":"10px 0 4px 0");
|
|
sCol('--bhd', l ? "block":"none"); // show/hide labels
|
|
sCol('--bmt', l ? "0px":"5px");
|
|
sCol('--t-b', cfg.theme.alpha.tab);
|
|
sCol('--sgp', !cfg.comp.segpwr ? "block":"none"); // show/hide segment power
|
|
size();
|
|
localStorage.setItem('wledUiCfg', JSON.stringify(cfg));
|
|
if (lastinfo.leds) updateUI(); // update component visibility
|
|
}
|
|
|
|
function tglHex()
|
|
{
|
|
cfg.comp.colors.hex = !cfg.comp.colors.hex;
|
|
applyCfg();
|
|
}
|
|
|
|
function tglTheme()
|
|
{
|
|
cfg.theme.base = (cfg.theme.base === "light") ? "dark":"light";
|
|
applyCfg();
|
|
}
|
|
|
|
function tglLabels()
|
|
{
|
|
cfg.comp.labels = !cfg.comp.labels;
|
|
applyCfg();
|
|
}
|
|
|
|
function tglRgb()
|
|
{
|
|
cfg.comp.colors.rgb = !cfg.comp.colors.rgb;
|
|
applyCfg();
|
|
}
|
|
|
|
function cTheme(light) {
|
|
if (light) {
|
|
sCol('--c-1','#eee');
|
|
sCol('--c-f','#000');
|
|
sCol('--c-2','#ddd');
|
|
sCol('--c-3','#bbb');
|
|
sCol('--c-4','#aaa');
|
|
sCol('--c-5','#999');
|
|
sCol('--c-6','#999');
|
|
sCol('--c-8','#888');
|
|
sCol('--c-b','#444');
|
|
sCol('--c-c','#333');
|
|
sCol('--c-e','#111');
|
|
sCol('--c-d','#222');
|
|
sCol('--c-r','#a21');
|
|
sCol('--c-g','#2a1');
|
|
sCol('--c-l','#26c');
|
|
sCol('--c-o','rgba(204, 204, 204, 0.9)');
|
|
sCol('--c-sb','#0003'); sCol('--c-sbh','#0006');
|
|
sCol('--c-tb','rgba(204, 204, 204, var(--t-b))');
|
|
sCol('--c-tba','rgba(170, 170, 170, var(--t-b))');
|
|
sCol('--c-tbh','rgba(204, 204, 204, var(--t-b))');
|
|
gId('imgw').style.filter = "invert(0.8)";
|
|
} else {
|
|
sCol('--c-1','#111');
|
|
sCol('--c-f','#fff');
|
|
sCol('--c-2','#222');
|
|
sCol('--c-3','#333');
|
|
sCol('--c-4','#444');
|
|
sCol('--c-5','#555');
|
|
sCol('--c-6','#666');
|
|
sCol('--c-8','#888');
|
|
sCol('--c-b','#bbb');
|
|
sCol('--c-c','#ccc');
|
|
sCol('--c-e','#eee');
|
|
sCol('--c-d','#ddd');
|
|
sCol('--c-r','#e42');
|
|
sCol('--c-g','#4e2');
|
|
sCol('--c-l','#48a');
|
|
sCol('--c-o','rgba(34, 34, 34, 0.9)');
|
|
sCol('--c-sb','#fff3'); sCol('--c-sbh','#fff5');
|
|
sCol('--c-tb','rgba(34, 34, 34, var(--t-b))');
|
|
sCol('--c-tba','rgba(102, 102, 102, var(--t-b))');
|
|
sCol('--c-tbh','rgba(51, 51, 51, var(--t-b))');
|
|
gId('imgw').style.filter = "unset";
|
|
}
|
|
}
|
|
|
|
function loadBg(iUrl)
|
|
{
|
|
let bg = gId('bg');
|
|
let img = d.createElement("img");
|
|
img.src = iUrl;
|
|
if (iUrl == "" || iUrl==="https://picsum.photos/1920/1080") {
|
|
var today = new Date();
|
|
for (var h of (hol||[])) {
|
|
var yr = h[0]==0 ? today.getFullYear() : h[0];
|
|
var hs = new Date(yr,h[1],h[2]);
|
|
var he = new Date(hs);
|
|
he.setDate(he.getDate() + h[3]);
|
|
if (today>=hs && today<=he) img.src = h[4];
|
|
}
|
|
}
|
|
img.addEventListener('load', (e) => {
|
|
var a = parseFloat(cfg.theme.alpha.bg);
|
|
if (isNaN(a)) a = 0.6;
|
|
bg.style.opacity = a;
|
|
bg.style.backgroundImage = `url(${img.src})`;
|
|
img = null;
|
|
gId('namelabel').style.color = "var(--c-c)"; // improve namelabel legibility on background image
|
|
});
|
|
}
|
|
|
|
function loadSkinCSS(cId)
|
|
{
|
|
if (!gId(cId)) // check if element exists
|
|
{
|
|
var h = d.getElementsByTagName('head')[0];
|
|
var l = d.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()
|
|
{
|
|
if (window.location.protocol == "file:") {
|
|
loc = true;
|
|
locip = localStorage.getItem('locIp');
|
|
if (!locip) {
|
|
locip = prompt("File Mode. Please enter WLED IP!");
|
|
localStorage.setItem('locIp', locip);
|
|
}
|
|
}
|
|
var sett = localStorage.getItem('wledUiCfg');
|
|
if (sett) cfg = mergeDeep(cfg, JSON.parse(sett));
|
|
|
|
resetPUtil();
|
|
|
|
if (localStorage.getItem('pcm') == "true" || (!/Mobi/.test(navigator.userAgent) && localStorage.getItem('pcm') == null)) togglePcMode(true);
|
|
applyCfg();
|
|
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((e)=>{
|
|
console.log("No array of holidays in holidays.json. Defaults loaded.");
|
|
})
|
|
.finally(()=>{
|
|
loadBg(cfg.theme.bg.url);
|
|
});
|
|
} else
|
|
loadBg(cfg.theme.bg.url);
|
|
if (cfg.comp.css) loadSkinCSS('skinCss');
|
|
|
|
selectSlot(0);
|
|
updateTablinks(0);
|
|
pmtLS = localStorage.getItem('wledPmt');
|
|
|
|
// Load initial data
|
|
loadPalettes(()=>{
|
|
// fill effect extra data array
|
|
loadFXData(()=>{
|
|
// load and populate effects
|
|
loadFX(()=>{
|
|
setTimeout(()=>{ // ESP8266 can't handle quick requests
|
|
loadPalettesData(()=>{
|
|
requestJson();// will load presets and create WS
|
|
});
|
|
},100);
|
|
});
|
|
});
|
|
});
|
|
resetUtil();
|
|
|
|
d.addEventListener("visibilitychange", handleVisibilityChange, false);
|
|
//size();
|
|
gId("cv").style.opacity=0;
|
|
var sls = d.querySelectorAll('input[type="range"]');
|
|
for (var sl of sls) {
|
|
sl.addEventListener('touchstart', toggleBubble);
|
|
sl.addEventListener('touchend', toggleBubble);
|
|
}
|
|
}
|
|
|
|
function updateTablinks(tabI)
|
|
{
|
|
var tablinks = gEBCN("tablinks");
|
|
for (var i of tablinks) i.classList.remove('active');
|
|
if (pcMode) return;
|
|
tablinks[tabI].classList.add('active');
|
|
}
|
|
|
|
function openTab(tabI, force = false)
|
|
{
|
|
if (pcMode && !force) return;
|
|
iSlide = tabI;
|
|
_C.classList.toggle('smooth', false);
|
|
_C.style.setProperty('--i', iSlide);
|
|
updateTablinks(tabI);
|
|
}
|
|
|
|
var timeout;
|
|
function showToast(text, error = false)
|
|
{
|
|
if (error) gId('connind').style.backgroundColor = "var(--c-r)";
|
|
var x = gId('toast');
|
|
//if (error) text += '<i class="icons btn-icon" style="transform:rotate(45deg);position:absolute;top:10px;right:0px;" onclick="clearErrorToast(100);"></i>';
|
|
x.innerHTML = text;
|
|
x.classList.add(error ? 'error':'show');
|
|
clearTimeout(timeout);
|
|
x.style.animation = 'none';
|
|
timeout = setTimeout(()=>{ x.classList.remove('show'); }, 2900);
|
|
if (error) console.log(text);
|
|
}
|
|
|
|
function showErrorToast()
|
|
{
|
|
showToast('Connection to light failed!', true);
|
|
}
|
|
|
|
function clearErrorToast(n=5000)
|
|
{
|
|
var x = gId('toast');
|
|
if (x.classList.contains('error')) {
|
|
clearTimeout(timeout);
|
|
timeout = setTimeout(()=>{
|
|
x.classList.remove('show');
|
|
x.classList.remove('error');
|
|
}, n);
|
|
}
|
|
}
|
|
|
|
function getRuntimeStr(rt)
|
|
{
|
|
var t = parseInt(rt);
|
|
var days = Math.floor(t/86400);
|
|
var hrs = Math.floor((t - days*86400)/3600);
|
|
var mins = Math.floor((t - days*86400 - hrs*3600)/60);
|
|
var str = days ? (days + " " + (days == 1 ? "day" : "days") + ", ") : "";
|
|
str += (hrs || days) ? (hrs + " " + (hrs == 1 ? "hour" : "hours")) : "";
|
|
if (!days && hrs) str += ", ";
|
|
if (t > 59 && !days) str += mins + " min";
|
|
if (t < 3600 && t > 59) str += ", ";
|
|
if (t < 3600) str += (t - mins*60) + " sec";
|
|
return str;
|
|
}
|
|
|
|
function inforow(key, val, unit = "")
|
|
{
|
|
return `<tr><td class="keytd">${key}</td><td class="valtd">${val}${unit}</td></tr>`;
|
|
}
|
|
|
|
function getLowestUnusedP()
|
|
{
|
|
var l = 1;
|
|
for (var key in pJson) if (key == l) l++;
|
|
if (l > 250) l = 250;
|
|
return l;
|
|
}
|
|
|
|
function checkUsed(i)
|
|
{
|
|
var id = gId(`p${i}id`).value;
|
|
if (pJson[id] && (i == 0 || id != i))
|
|
gId(`p${i}warn`).innerHTML = `⚠ Overwriting ${pName(id)}!`;
|
|
else
|
|
gId(`p${i}warn`).innerHTML = id>250?"⚠ ID must be 250 or less.":"";
|
|
}
|
|
|
|
function pName(i)
|
|
{
|
|
var n = "Preset " + i;
|
|
if (pJson && pJson[i] && pJson[i].n) n = pJson[i].n;
|
|
return n;
|
|
}
|
|
|
|
function isPlaylist(i)
|
|
{
|
|
return pJson[i].playlist && pJson[i].playlist.ps;
|
|
}
|
|
|
|
function papiVal(i)
|
|
{
|
|
if (!pJson || !pJson[i]) return "";
|
|
var o = Object.assign({},pJson[i]);
|
|
if (o.win) return o.win;
|
|
delete o.n; delete o.p; delete o.ql;
|
|
return JSON.stringify(o);
|
|
}
|
|
|
|
function qlName(i)
|
|
{
|
|
if (!pJson || !pJson[i] || !pJson[i].ql) return "";
|
|
return pJson[i].ql;
|
|
}
|
|
|
|
function cpBck()
|
|
{
|
|
var copyText = gId("bck");
|
|
|
|
copyText.select();
|
|
copyText.setSelectionRange(0, 999999);
|
|
d.execCommand("copy");
|
|
showToast("Copied to clipboard!");
|
|
}
|
|
|
|
function presetError(empty)
|
|
{
|
|
var hasBackup = false; var bckstr = "";
|
|
try {
|
|
bckstr = localStorage.getItem("wledP");
|
|
if (bckstr.length > 10) hasBackup = true;
|
|
} catch (e) {}
|
|
|
|
var cn = `<div class="pres c" ${empty?'style="padding:8px;margin-top: 16px;"':'onclick="pmtLast=0;loadPresets();" style="cursor:pointer;padding:8px;margin-top: 16px;"'}>`;
|
|
if (empty)
|
|
cn += `You have no presets yet!`;
|
|
else
|
|
cn += `Sorry, there was an issue loading your presets!`;
|
|
|
|
if (hasBackup) {
|
|
cn += `<br><br>`;
|
|
if (empty)
|
|
cn += `However, there is backup preset data of a previous installation available.<br>
|
|
(Saving a preset will hide this and overwrite the backup)`;
|
|
else
|
|
cn += `Here is a backup of the last known good state:`;
|
|
cn += `<textarea id="bck"></textarea><br>
|
|
<button class="btn" onclick="cpBck()">Copy to clipboard</button>`;
|
|
}
|
|
cn += `</div>`;
|
|
gId('pcont').innerHTML = cn;
|
|
if (hasBackup) gId('bck').value = bckstr;
|
|
}
|
|
|
|
function loadPresets(callback = null)
|
|
{
|
|
// 1st boot (because there is a callback)
|
|
if (callback && pmt == pmtLS && pmt > 0) {
|
|
// we have a copy of the presets in local storage and don't need to fetch another one
|
|
populatePresets(true);
|
|
pmtLast = pmt;
|
|
callback();
|
|
return;
|
|
}
|
|
|
|
// afterwards
|
|
if (!callback && pmt == pmtLast) return;
|
|
|
|
var url = (loc?`http://${locip}`:'') + '/presets.json';
|
|
|
|
fetch(url, {
|
|
method: 'get'
|
|
})
|
|
.then(res => {
|
|
if (res.status=="404") return {"0":{}};
|
|
//if (!res.ok) showErrorToast();
|
|
return res.json();
|
|
})
|
|
.then(json => {
|
|
pJson = json;
|
|
pmtLast = pmt;
|
|
populatePresets();
|
|
})
|
|
.catch((e)=>{
|
|
//showToast(e, true);
|
|
presetError(false);
|
|
})
|
|
.finally(()=>{
|
|
if (callback) setTimeout(callback,99);
|
|
});
|
|
}
|
|
|
|
function loadPalettes(callback = null)
|
|
{
|
|
var url = (loc?`http://${locip}`:'') + '/json/palettes';
|
|
|
|
fetch(url, {
|
|
method: 'get'
|
|
})
|
|
.then((res)=>{
|
|
if (!res.ok) showErrorToast();
|
|
return res.json();
|
|
})
|
|
.then((json)=>{
|
|
lJson = Object.entries(json);
|
|
populatePalettes();
|
|
})
|
|
.catch((e)=>{
|
|
showToast(e, true);
|
|
})
|
|
.finally(()=>{
|
|
if (callback) callback();
|
|
updateUI();
|
|
});
|
|
}
|
|
|
|
function loadFX(callback = null)
|
|
{
|
|
var url = (loc?`http://${locip}`:'') + '/json/effects';
|
|
|
|
fetch(url, {
|
|
method: 'get'
|
|
})
|
|
.then((res)=>{
|
|
if (!res.ok) showErrorToast();
|
|
return res.json();
|
|
})
|
|
.then((json)=>{
|
|
eJson = Object.entries(json);
|
|
populateEffects();
|
|
})
|
|
.catch((e)=>{
|
|
showToast(e, true);
|
|
})
|
|
.finally(()=>{
|
|
if (callback) callback();
|
|
updateUI();
|
|
});
|
|
}
|
|
|
|
function loadFXData(callback = null)
|
|
{
|
|
var url = (loc?`http://${locip}`:'') + '/json/fxdata';
|
|
|
|
fetch(url, {
|
|
method: 'get'
|
|
})
|
|
.then((res)=>{
|
|
if (!res.ok) showErrorToast();
|
|
return res.json();
|
|
})
|
|
.then((json)=>{
|
|
fxdata = json||[];
|
|
// add default value for Solid
|
|
fxdata.shift()
|
|
fxdata.unshift(";!;");
|
|
})
|
|
.catch((e)=>{
|
|
fxdata = [];
|
|
showToast(e, true);
|
|
})
|
|
.finally(()=>{
|
|
if (callback) callback();
|
|
updateUI();
|
|
});
|
|
}
|
|
|
|
var pQL = [];
|
|
function populateQL()
|
|
{
|
|
var cn = "";
|
|
if (pQL.length > 0) {
|
|
pQL.sort((a,b) => (a[1]>b[1])); //WLEDMM do not sort on preset id but on ql name
|
|
cn += `<p class="labels hd">Quick load</p>`;
|
|
for (var key of (pQL||[])) {
|
|
cn += `<button class="btn btn-xs psts" id="p${key[0]}qlb" title="${key[2]?key[2]:''}" onclick="setPreset(${key[0]});">${key[1]}</button>`;
|
|
}
|
|
gId('pql').classList.add('expanded');
|
|
} else gId('pql').classList.remove('expanded');
|
|
gId('pql').innerHTML = cn;
|
|
}
|
|
|
|
function populatePresets(fromls)
|
|
{
|
|
if (fromls) pJson = JSON.parse(localStorage.getItem("wledP"));
|
|
if (!pJson) {setTimeout(loadPresets,250); return;}
|
|
delete pJson["0"];
|
|
var cn = "";
|
|
var arr = Object.entries(pJson);
|
|
arr.sort(cmpP);
|
|
pQL = [];
|
|
var is = [];
|
|
pNum = 0;
|
|
for (var key of (arr||[]))
|
|
{
|
|
if (!isObj(key[1])) continue;
|
|
let i = parseInt(key[0]);
|
|
var qll = key[1].ql;
|
|
if (qll) pQL.push([i, qll, pName(i)]);
|
|
is.push(i);
|
|
|
|
cn += `<div class="pres lstI" id="p${i}o">`;
|
|
if (cfg.comp.pid) cn += `<div class="pid">${i}</div>`;
|
|
//WLEDMM: show ql if defined
|
|
cn += `<div class="pname lstIname" onclick="setPreset(${i})">${isPlaylist(i)?"<i class='icons btn-icon'></i>":""}${(pJson[i].ql?pJson[i].ql+' ':'') + pName(i)}
|
|
<i class="icons edit-icon flr" id="p${i}nedit" onclick="tglSegn(${i+100})"></i></div>
|
|
<i class="icons e-icon flr" id="sege${i+100}" onclick="expand(${i+100})"></i>
|
|
<div class="presin lstIcontent" id="seg${i+100}"></div>
|
|
</div>`;
|
|
pNum++;
|
|
}
|
|
|
|
gId('pcont').innerHTML = cn;
|
|
if (pNum > 0) {
|
|
if (pmtLS != pmt && pmt != 0) {
|
|
localStorage.setItem("wledPmt", pmt);
|
|
pJson["0"] = {};
|
|
localStorage.setItem("wledP", JSON.stringify(pJson));
|
|
}
|
|
pmtLS = pmt;
|
|
} else { presetError(true); }
|
|
updatePA();
|
|
populateQL();
|
|
}
|
|
|
|
function parseInfo(i) {
|
|
lastinfo = i;
|
|
var name = i.name;
|
|
gId('namelabel').innerHTML = name;
|
|
if (!name.match(/[\u3040-\u30ff\u3400-\u4dbf\u4e00-\u9fff\uf900-\ufaff\uff66-\uff9f\u3131-\uD79D]/))
|
|
gId('namelabel').style.transform = "rotate(180deg)"; // rotate if no CJK characters
|
|
if (name === "Dinnerbone") d.documentElement.style.transform = "rotate(180deg)"; // Minecraft easter egg
|
|
if (i.live) name = "(Live) " + name;
|
|
if (loc) name = "(L) " + name;
|
|
d.title = name;
|
|
ledCount = i.leds.count;
|
|
syncTglRecv = i.str;
|
|
maxSeg = i.leds.maxseg;
|
|
pmt = i.fs.pmt;
|
|
gId('buttonNodes').style.display = lastinfo.ndc > 0 ? null:"none";
|
|
// do we have a matrix set-up
|
|
mw = i.leds.matrix ? i.leds.matrix.w : 0;
|
|
mh = i.leds.matrix ? i.leds.matrix.h : 0;
|
|
isM = mw>0 && mh>0;
|
|
if (!isM) {
|
|
gId("filter0D").classList.remove('hide');
|
|
gId("filter1D").classList.add('hide');
|
|
gId("filter2D").classList.add('hide');
|
|
} else {
|
|
gId("filter0D").classList.add('hide');
|
|
gId("filter1D").classList.remove('hide');
|
|
gId("filter2D").classList.remove('hide');
|
|
}
|
|
// if (i.noaudio) {
|
|
// gId("filterVol").classList.add("hide");
|
|
// gId("filterFreq").classList.add("hide");
|
|
// }
|
|
// if (!i.u || !i.u.AudioReactive) {
|
|
// gId("filterVol").classList.add("hide"); hideModes(" ♪"); // hide volume reactive effects
|
|
// gId("filterFreq").classList.add("hide"); hideModes(" ♫"); // hide frequency reactive effects
|
|
// }
|
|
}
|
|
|
|
//https://stackoverflow.com/questions/2592092/executing-script-elements-inserted-with-innerhtml
|
|
//var setInnerHTML = function(elm, html) {
|
|
// elm.innerHTML = html;
|
|
// Array.from(elm.querySelectorAll("script")).forEach( oldScript => {
|
|
// const newScript = document.createElement("script");
|
|
// Array.from(oldScript.attributes)
|
|
// .forEach( attr => newScript.setAttribute(attr.name, attr.value) );
|
|
// newScript.appendChild(document.createTextNode(oldScript.innerHTML));
|
|
// oldScript.parentNode.replaceChild(newScript, oldScript);
|
|
// });
|
|
//}
|
|
//setInnerHTML(obj, html);
|
|
|
|
function populateInfo(i)
|
|
{
|
|
var cn="";
|
|
var heap = i.freeheap/1000;
|
|
var heap = Math.round(i.freeheap/100)/10; // WLEDMM bugfix
|
|
var theap = (i.totalheap>0)?i.totalheap/1000:-1; //WLEDMM - total heap is not available on 8266
|
|
var flashsize = i.getflash/1000; //WLEDMM and Athom
|
|
flashsize = flashsize.toFixed(1); //WLEDMM and Athom
|
|
var pwr = i.leds.pwr;
|
|
var pwru = "Not calculated";
|
|
if (pwr > 1000) {pwr /= 1000; pwr = pwr.toFixed((pwr > 10) ? 0 : 1); pwru = pwr + " A";}
|
|
else if (pwr > 0) {pwr = 50 * Math.round(pwr/50); pwru = pwr + " mA";}
|
|
var urows="";
|
|
if (i.u) {
|
|
for (const [k, val] of Object.entries(i.u)) {
|
|
if (val[1])
|
|
urows += inforow(k,val[0],val[1]);
|
|
else
|
|
urows += inforow(k,val);
|
|
}
|
|
}
|
|
var vcn = "Kuuhaku";
|
|
if (i.ver.startsWith("0.14.")) vcn = "Hoshi";
|
|
// if (i.ver.includes("-bl")) vcn = "Supāku";
|
|
if (i.cn) vcn = i.cn;
|
|
|
|
//WLEDMM: add total heap and total PSRAM, and build number, add bin name
|
|
if (i.ver.includes("0.14.1")) vcn = "Sitting Ducks"; // easter egg
|
|
if (i.ver.includes("0.14.0")) vcn = "Lupo"; // check for MM versioning scheme
|
|
if (i.ver.includes("0.14.0-b15")) vcn = "Sitting Ducks"; // late easter egg
|
|
if (i.ver.includes("0.14.0-b2")) vcn = "This is the way"; // recently watched The Mandalorian? I have spoken ;-)
|
|
if (i.ver.includes("0.14.0-b15.22")) vcn = "Lupo";
|
|
cn += `v${i.ver} <i>"${vcn}"</i><p>(WLEDMM_${i.ver} ${i.rel}.bin)</p><p><em>build ${i.vid}</em></p><table>
|
|
${urows}
|
|
${urows===""?'':'<tr><td colspan=2><hr style="height:1px;border-width:0;color:SeaGreen;background-color:Seagreen"></td></tr>'}
|
|
${i.opt&0x100?inforow("Net Print ☾","<button class=\"btn btn-xs\" onclick=\"requestJson({'netDebug':"+(i.opt&0x0080?"false":"true")+"});\"><i class=\"icons "+(i.opt&0x0080?"on":"off")+"\"></i></button>"):''}
|
|
${i.serialOnline?inforow(i.serialOnline,"TX="+i.sTX,"; RX="+i.sRX):""}
|
|
${i.opt&0x100?'<tr><td colspan=2><hr style="height:1px;border-width:0;color:SeaGreen;background-color:SeaGreen"></td></tr>':''}
|
|
${inforow("Build",i.vid)}
|
|
${inforow("Estimated current",pwru)}
|
|
${inforow("Average FPS",i.leds.fps)}
|
|
${inforow("Signal strength",i.wifi.signal +"% ("+ i.wifi.rssi, " dBm)")}
|
|
${inforow("Uptime",getRuntimeStr(i.uptime))}
|
|
<!-- WLEDMM begin-->
|
|
<tr><td colspan=2><hr style="height:1px;border-width:0;color:SeaGreen;background-color:SeaGreen"></td></tr>
|
|
${inforow("Filesystem",i.fs.u + "/" + i.fs.t + " kB (" +Math.round(i.fs.u*100/i.fs.t) + "%)")}
|
|
${theap>0?inforow("Heap ☾",((i.totalheap-i.freeheap)/1000).toFixed(0)+"/"+theap.toFixed(0)+" kB"," ("+Math.round((i.totalheap-i.freeheap)/(10*theap))+"%)"):""}
|
|
${i.minfreeheap?inforow("Max used heap ☾",((i.totalheap-i.minfreeheap)/1000).toFixed(1)+" kB"," ("+Math.round((i.totalheap-i.minfreeheap)/(10*theap))+"%)"):""}
|
|
${inforow("Free heap",heap," kB")}
|
|
${inforow("Flash Size ☾",flashsize," kB")} <!--WLEDMM and Athom-->
|
|
${i.tpram?inforow("PSRAM ☾",(i.tpram/1024).toFixed(1)," kB"):""}
|
|
${i.psram?((i.tpram-i.psram)>16383?inforow("Used PSRAM ☾",((i.tpram-i.psram)/1024).toFixed(1)," kB"):inforow("Used PSRAM ☾",(i.tpram-i.psram)," B")):""}
|
|
${i.psusedram?((i.tpram-i.psusedram)>16383?inforow("Max used PSRAM ☾",((i.tpram-i.psusedram)/1024).toFixed(1)," kB"):inforow("Max used PSRAM ☾",(i.tpram-i.psusedram)," B")):""}
|
|
${i.psram?inforow("Free PSRAM",(i.psram/1024).toFixed(1)," kB"):""}
|
|
${inforow("MAC address",i.mac)}
|
|
${inforow("Environment",i.arch + " " + i.core + " (" + i.lwip + ")")}
|
|
<tr><td colspan=2><hr style="height:1px;border-width:0;color:SeaGreen;background-color:SeaGreen"></td></tr>
|
|
${i.e32model?inforow(i.e32model + " ☾",i.e32cores +" core(s)"," "+i.e32speed+" Mhz"):""}
|
|
${i.e32flash?inforow("Flash "+i.e32flash+"MB"+" mode "+i.e32flashmode+i.e32flashtext + " ☾",i.e32flashspeed," Mhz"):""}
|
|
${i.e32code?inforow("Last ESP Restart ☾",i.e32code+" "+i.e32text):""}
|
|
${i.e32core0code?inforow("Core0 rst reason ☾",i.e32core0code, " "+i.e32core0text):""}
|
|
${i.e32core1code?inforow("Core1 rst reason ☾",i.e32core1code, " "+i.e32core1text):""}
|
|
<!-- WLEDMM end-->
|
|
</table>`;
|
|
gId('kv').innerHTML = cn;
|
|
// update all sliders in Info
|
|
for (let sd of (gId('kv').getElementsByClassName('sliderdisplay')||[])) {
|
|
let s = sd.previousElementSibling;
|
|
if (s) updateTrail(s);
|
|
}
|
|
}
|
|
|
|
function populateSegments(s)
|
|
{
|
|
var cn = "";
|
|
let li = lastinfo;
|
|
segCount = 0; lowestUnused = 0; lSeg = 0;
|
|
|
|
ledmapNr = s.ledmap; //WLEDMM
|
|
ledmapFileNames = []; //WLEDMM
|
|
|
|
for (var inst of (s.seg||[])) {
|
|
segCount++;
|
|
|
|
let i = parseInt(inst.id);
|
|
if (i == lowestUnused) lowestUnused = i+1;
|
|
if (i > lSeg) lSeg = i;
|
|
|
|
let sg = gId(`seg${i}`);
|
|
let exp = sg ? (sg.classList.contains('expanded') || (i===0 && cfg.comp.segexp)) : false;
|
|
|
|
ledmapFileNames.push((inst.n?inst.n:"default") + ".json"); //WLEDMM
|
|
|
|
// segment set icon color
|
|
let cG = "var(--c-b)";
|
|
switch (inst.set) {
|
|
case 1: cG = "var(--c-r)"; break;
|
|
case 2: cG = "var(--c-g)"; break;
|
|
case 3: cG = "var(--c-l)"; break;
|
|
}
|
|
|
|
let segp = `<div id="segp${i}" class="sbs">`+
|
|
`<i class="icons slider-icon pwr ${inst.on ? "act":""}" id="seg${i}pwr" onclick="setSegPwr(${i})"></i>`+
|
|
`<div class="sliderwrap il">`+
|
|
`<input id="seg${i}bri" class="noslide" onchange="setSegBri(${i})" oninput="updateTrail(this)" max="255" min="1" type="range" value="${inst.bri}" />`+
|
|
`<div class="sliderdisplay"></div>`+
|
|
`</div>`+
|
|
`</div>`;
|
|
let staX = inst.start;
|
|
let stoX = inst.stop;
|
|
let staY = inst.startY;
|
|
let stoY = inst.stopY;
|
|
let isMSeg = isM && staX<mw*mh; // 2D matrix segment
|
|
let rvXck = `<label class="check revchkl">Reverse ${isM?'':'direction'}<input type="checkbox" id="seg${i}rev" onchange="setRev(${i})" ${inst.rev?"checked":""}><span class="checkmark"></span></label>`;
|
|
let miXck = `<label class="check revchkl">Mirror<input type="checkbox" id="seg${i}mi" onchange="setMi(${i})" ${inst.mi?"checked":""}><span class="checkmark"></span></label>`;
|
|
let rvYck = "", miYck ="";
|
|
if (isMSeg) {
|
|
rvYck = `<label class="check revchkl">Reverse<input type="checkbox" id="seg${i}rY" onchange="setRevY(${i})" ${inst.rY?"checked":""}><span class="checkmark"></span></label>`;
|
|
miYck = `<label class="check revchkl">Mirror<input type="checkbox" id="seg${i}mY" onchange="setMiY(${i})" ${inst.mY?"checked":""}><span class="checkmark"></span></label>`;
|
|
}
|
|
// WLEDMM: jMap
|
|
let map2D = `<div id="seg${i}map2D" data-map="map2D" class="lbl-s hide">Expand 1D FX<br>`+
|
|
`<div class="sel-p"><select class="sel-p" id="seg${i}m12" onchange="setM12(${i})">`+
|
|
`<option value="0" ${inst.m12==0?' selected':''}>Pixels</option>`+
|
|
`<option value="1" ${inst.m12==1?' selected':''}>Bar</option>`+
|
|
`<option value="2" ${inst.m12==2?' selected':''}>Arc</option>`+
|
|
`<option value="3" ${inst.m12==3?' selected':''}>Corner</option>`+
|
|
`<option value="4" ${inst.m12==4?' selected':''}>jMap ☾</option>`+
|
|
`<option value="5" ${inst.m12==5?' selected':''}>Circle ☾</option>`+
|
|
`<option value="6" ${inst.m12==6?' selected':''}>Block ☾</option>`+
|
|
`</select></div>`+
|
|
`</div>`;
|
|
let sndSim = `<div data-snd="si" class="lbl-s hide">Sound sim<br>`+
|
|
`<div class="sel-p"><select class="sel-p" id="seg${i}si" onchange="setSi(${i})">`+
|
|
`<option value="0" ${inst.si==0?' selected':''}>BeatSin</option>`+
|
|
`<option value="1" ${inst.si==1?' selected':''}>WeWillRockYou</option>`+
|
|
`</select></div>`+
|
|
`</div>`;
|
|
//WLEDMM ARTIFX
|
|
let fxName = eJson.find((o)=>{return o.id==selectedFx}).name;
|
|
let cusEff = `<button class="btn" onclick="toggleCEEditor('${inst.n?inst.n:"default"}', ${i})">ARTI-FX Editor ☾</button><br>`;
|
|
cn += `<div class="seg lstI ${i==s.mainseg ? 'selected' : ''} ${exp ? "expanded":""}" id="seg${i}" data-set="${inst.set}">`+
|
|
`<label class="check schkl">`+
|
|
`<input type="checkbox" id="seg${i}sel" onchange="selSeg(${i})" ${inst.sel ? "checked":""}>`+
|
|
`<span class="checkmark"></span>`+
|
|
`</label>`+
|
|
`<div class="segname" onclick="selSegEx(${i})">`+
|
|
`<i class="icons e-icon frz" id="seg${i}frz" onclick="event.preventDefault();tglFreeze(${i});">&#x${inst.frz ? (li.live && li.liveseg==i?'e410':'e0e8') : 'e325'};</i>`+
|
|
(inst.n ? inst.n : "Segment "+i) +
|
|
`<div class="pop hide" onclick="event.preventDefault();event.stopPropagation();">`+
|
|
`<i class="icons g-icon" style="color:${cG};" onclick="this.nextElementSibling.classList.toggle('hide');">ɸ${String.fromCharCode(inst.set+"A".charCodeAt(0))};</i>`+
|
|
`<div class="pop-c hide"><span style="color:var(--c-f);" onclick="setGrp(${i},0);">➊</span><span style="color:var(--c-r);" onclick="setGrp(${i},1);">➋</span><span style="color:var(--c-g);" onclick="setGrp(${i},2);">➌</span><span style="color:var(--c-l);" onclick="setGrp(${i},3);">➍</span></div>`+
|
|
`</div> `+
|
|
`<i class="icons edit-icon flr" id="seg${i}nedit" onclick="tglSegn(${i})"></i>`+
|
|
`</div>`+
|
|
`<i class="icons e-icon flr" id="sege${i}" onclick="expand(${i})"></i>`+
|
|
(cfg.comp.segpwr ? segp : '') +
|
|
`<div class="segin" id="seg${i}in">`+
|
|
`<input id="seg${i}fx" value="${inst.fx}" type="hidden"/>` + // <!--WLEDMM-->
|
|
`<input type="text" class="ptxt" id="seg${i}t" autocomplete="off" maxlength=32 value="${inst.n?inst.n:""}" placeholder="Enter name..."/>`+
|
|
`<table class="infot segt">`+
|
|
`<tr>`+
|
|
`<td>${isMSeg?'Start X':'Start LED'}</td>`+
|
|
`<td>${isMSeg?(cfg.comp.seglen?"Width":"Stop X"):(cfg.comp.seglen?"LED count":"Stop LED")}</td>`+
|
|
`<td>${isMSeg?'':'Offset'}</td>`+
|
|
`</tr>`+
|
|
`<tr>`+
|
|
`<td><input class="segn" id="seg${i}s" type="number" min="0" max="${(isMSeg?mw:ledCount)-1}" value="${staX}" oninput="updateLen(${i})" onkeydown="segEnter(${i})"></td>`+
|
|
`<td><input class="segn" id="seg${i}e" type="number" min="0" max="${(isMSeg?mw:ledCount)}" value="${stoX-(cfg.comp.seglen?staX:0)}" oninput="updateLen(${i})" onkeydown="segEnter(${i})"></td>`+
|
|
`<td ${isMSeg?'style="text-align:revert;"':''}>${isMSeg?miXck+'<br>'+rvXck:''}<input class="segn ${isMSeg?'hide':''}" id="seg${i}of" type="number" value="${inst.of}" oninput="updateLen(${i})"></td>`+
|
|
`</tr>`+
|
|
(isMSeg ? '<tr><td>Start Y</td><td>'+(cfg.comp.seglen?'Height':'Stop Y')+'</td><td></td></tr>'+
|
|
'<tr>'+
|
|
'<td><input class="segn" id="seg'+i+'sY" type="number" min="0" max="'+(mh-1)+'" value="'+staY+'" oninput="updateLen('+i+')" onkeydown="segEnter('+i+')"></td>'+
|
|
'<td><input class="segn" id="seg'+i+'eY" type="number" min="0" max="'+mh+'" value="'+(stoY-(cfg.comp.seglen?staY:0))+'" oninput="updateLen('+i+')" onkeydown="segEnter('+i+')"></td>'+
|
|
'<td style="text-align:revert;">'+miYck+'<br>'+rvYck+'</td>'+
|
|
'</tr>' : '') +
|
|
`<tr>`+
|
|
`<td>Grouping</td>`+
|
|
`<td>Spacing</td>`+
|
|
`<td></td>`+
|
|
`</tr>`+
|
|
`<tr>`+
|
|
`<td><input class="segn" id="seg${i}grp" type="number" min="1" max="255" value="${inst.grp}" oninput="updateLen(${i})" onkeydown="segEnter(${i})"></td>`+
|
|
`<td><input class="segn" id="seg${i}spc" type="number" min="0" max="255" value="${inst.spc}" oninput="updateLen(${i})" onkeydown="segEnter(${i})"></td>`+
|
|
`<td><button class="btn btn-xs" onclick="setSeg(${i})"><i class="icons btn-icon" id="segc${i}"></i></button></td>`+
|
|
`</tr>`+
|
|
`</table>`+
|
|
`<div class="h bp" id="seg${i}len"></div>`+
|
|
(!isMSeg ? rvXck : '') +
|
|
(isMSeg&&stoY-staY>1&&stoX-staX>1 ? map2D : '') +
|
|
(s.AudioReactive && s.AudioReactive.on ? "" : sndSim) +
|
|
(s.ARTIFX && s.ARTIFX.on && fxName.includes("ARTI-FX") ? cusEff : "") + // <!--WLEDMM-->
|
|
`<label class="check revchkl" id="seg${i}lbtm">`+
|
|
(isMSeg?'Transpose':'Mirror effect') + (isMSeg ?
|
|
'<input type="checkbox" id="seg'+i+'tp" onchange="setTp('+i+')" '+(inst.tp?"checked":"")+'>':
|
|
'<input type="checkbox" id="seg'+i+'mi" onchange="setMi('+i+')" '+(inst.mi?"checked":"")+'>') +
|
|
`<span class="checkmark"></span>`+
|
|
`</label>`+
|
|
`<div class="del">`+
|
|
`<button class="btn btn-xs" id="segr${i}" title="Repeat until end" onclick="rptSeg(${i})"><i class="icons btn-icon"></i></button>`+
|
|
`<button class="btn btn-xs" id="segd${i}" title="Delete" onclick="delSeg(${i})"><i class="icons btn-icon"></i></button>`+
|
|
`</div>`+
|
|
`</div>`+
|
|
(cfg.comp.segpwr ? '' : segp) +
|
|
`</div>`;
|
|
}
|
|
|
|
gId('segcont').innerHTML = cn;
|
|
let noNewSegs = (lowestUnused >= maxSeg);
|
|
resetUtil(noNewSegs);
|
|
if (gId('selall')) gId('selall').checked = true;
|
|
for (var i = 0; i <= lSeg; i++) {
|
|
updateLen(i, false); //WLEDMM: no draw
|
|
updateTrail(gId(`seg${i}bri`));
|
|
gId(`segr${i}`).classList.add("hide");
|
|
if (!gId(`seg${i}sel`).checked && gId('selall')) gId('selall').checked = false; // uncheck if at least one is unselected.
|
|
}
|
|
if (segCount < 2) {
|
|
gId(`segd${lSeg}`).classList.add("hide");
|
|
if (parseInt(gId("seg0bri").value)==255) gId(`segp0`).classList.add("hide");
|
|
}
|
|
if (!isM && !noNewSegs && (cfg.comp.seglen?parseInt(gId(`seg${lSeg}s`).value):0)+parseInt(gId(`seg${lSeg}e`).value)<ledCount) gId(`segr${lSeg}`).classList.remove("hide");
|
|
gId('segutil2').style.display = (segCount > 1) ? "block":"none"; // rsbtn parent
|
|
|
|
if (Array.isArray(li.maps) && li.maps.length>0) { //WLEDMM >0 instead of 1 to show also first ledmap. Attention: WLED AC has isM check, in MM Matrices are supported so do not check on isM
|
|
let cont = `Ledmap: <select class="sel-sg" onchange="requestJson({'ledmap':parseInt(this.value)})">`; //WLEDMM remove <option value="" selected>Unchanged</option>
|
|
for (const k of (li.maps||[])) cont += `<option value="${k.id}"${(i>0 && ledmapNr==k.id)?" selected":""}>${k.id==0?'Default':(k.id<10?'ledmap'+k.id+'.json':ledmapFileNames[k.id-10])}</option>`; //WLEDMM set ledmap selected, use ledmapFileNames
|
|
cont += "</select></div>";
|
|
gId("ledmap").innerHTML = cont;
|
|
gId("ledmap").classList.remove('hide');
|
|
} else {
|
|
gId("ledmap").classList.add('hide');
|
|
}
|
|
}
|
|
|
|
function populateEffects()
|
|
{
|
|
var effects = eJson;
|
|
var html = "";
|
|
|
|
effects.shift(); // temporary remove solid
|
|
for (let i = 0; i < effects.length; i++) {
|
|
effects[i] = {
|
|
id: effects[i][0],
|
|
name:effects[i][1]
|
|
};
|
|
}
|
|
effects.sort((a,b) => (a.name).localeCompare(b.name));
|
|
effects.unshift({
|
|
"id": 0,
|
|
"name": "Solid"
|
|
});
|
|
|
|
for (let ef of effects) {
|
|
// add slider and color control to setFX (used by requestjson)
|
|
let id = ef.id;
|
|
let nm = ef.name+" ";
|
|
let fd = "";
|
|
if (ef.name.indexOf("RSVD") < 0) {
|
|
if (Array.isArray(fxdata) && fxdata.length>id) {
|
|
if (fxdata[id].length==0) fd = ";;!;1"
|
|
else fd = fxdata[id];
|
|
let eP = (fd == '')?[]:fd.split(";"); // effect parameters
|
|
let p = (eP.length<3 || eP[2]==='')?[]:eP[2].split(","); // palette data
|
|
if (p.length>0 && (p[0] !== "" && !isNumeric(p[0]))) nm += "🎨"; // effects using palette
|
|
let m = (eP.length<4 || eP[3]==='')?'1':eP[3]; // flags
|
|
if (id == 0) m = ''; // solid has no flags
|
|
if (m.length>0) {
|
|
if (m.includes('0')) nm += "•"; // 0D effects (PWM & On/Off)
|
|
if (m.includes('1')) nm += "⋮"; // 1D effects
|
|
if (m.includes("1.5d")) nm += "⋮"; // WLEDMM: vStrips
|
|
if (m.includes('2')) nm += "▦"; // 2D effects
|
|
if (m.includes('v')) nm += "♪"; // volume effects
|
|
if (m.includes('f')) nm += "♫"; // frequency effects
|
|
}
|
|
}
|
|
html += generateListItemHtml('fx',id,nm,'setFX','',fd);
|
|
}
|
|
}
|
|
|
|
gId('fxlist').innerHTML=html;
|
|
}
|
|
|
|
function populatePalettes()
|
|
{
|
|
lJson.shift(); // temporary remove default
|
|
lJson.sort((a,b) => (a[1]).localeCompare(b[1]));
|
|
lJson.unshift([0,"Default"]);
|
|
|
|
var html = "";
|
|
for (let pa of lJson) {
|
|
html += generateListItemHtml(
|
|
'palette',
|
|
pa[0],
|
|
pa[1],
|
|
'setPalette',
|
|
`<div class="lstIprev" style="${genPalPrevCss(pa[0])}"></div>`
|
|
);
|
|
}
|
|
gId('pallist').innerHTML=html;
|
|
// append custom palettes (when loading for the 1st time)
|
|
if (!isEmpty(lastinfo) && lastinfo.cpalcount) {
|
|
for (let j = 0; j<lastinfo.cpalcount; j++) {
|
|
let div = d.createElement("div");
|
|
gId('pallist').appendChild(div);
|
|
div.outerHTML = generateListItemHtml(
|
|
'palette',
|
|
255-j,
|
|
'~ Custom '+j+' ~',
|
|
'setPalette',
|
|
`<div class="lstIprev" style="${genPalPrevCss(255-j)}"></div>`
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
function redrawPalPrev()
|
|
{
|
|
let palettes = d.querySelectorAll('#pallist .lstI');
|
|
for (var pal of (palettes||[])) {
|
|
let lP = pal.querySelector('.lstIprev');
|
|
if (lP) {
|
|
lP.style = genPalPrevCss(pal.dataset.id);
|
|
}
|
|
}
|
|
}
|
|
|
|
function genPalPrevCss(id)
|
|
{
|
|
if (!palettesData) return;
|
|
|
|
var paletteData = palettesData[id];
|
|
|
|
if (!paletteData) return 'display: none';
|
|
|
|
// We need at least two colors for a gradient
|
|
if (paletteData.length == 1) {
|
|
paletteData[1] = paletteData[0];
|
|
if (Array.isArray(paletteData[1])) {
|
|
paletteData[1][0] = 255;
|
|
}
|
|
}
|
|
|
|
var gradient = [];
|
|
for (let j = 0; j < paletteData.length; j++) {
|
|
const e = paletteData[j];
|
|
let r, g, b;
|
|
let index = false;
|
|
if (Array.isArray(e)) {
|
|
index = Math.round(e[0]/255*100);
|
|
r = e[1];
|
|
g = e[2];
|
|
b = e[3];
|
|
} else if (e == 'r') {
|
|
r = Math.random() * 255;
|
|
g = Math.random() * 255;
|
|
b = Math.random() * 255;
|
|
} else {
|
|
let i = e[1] - 1;
|
|
var cd = gId('csl').children;
|
|
r = parseInt(cd[i].dataset.r);
|
|
g = parseInt(cd[i].dataset.g);
|
|
b = parseInt(cd[i].dataset.b);
|
|
}
|
|
if (index === false) {
|
|
index = Math.round(j / paletteData.length * 100);
|
|
}
|
|
|
|
gradient.push(`rgb(${r},${g},${b}) ${index}%`);
|
|
}
|
|
|
|
return `background: linear-gradient(to right,${gradient.join()});`;
|
|
}
|
|
|
|
function generateListItemHtml(listName, id, name, clickAction, extraHtml = '', effectPar = '')
|
|
{
|
|
return `<div class="lstI${id==0?' sticky':''}" data-id="${id}" ${effectPar===''?'':'data-opt="'+effectPar+'" '}onClick="${clickAction}(${id})">`+
|
|
`<label title="(${id})" class="radio schkl" onclick="event.preventDefault()">`+ // (#1984)
|
|
`<input type="radio" value="${id}" name="${listName}">`+
|
|
`<span class="radiomark"></span>`+
|
|
`<div class="lstIcontent">`+
|
|
`<span class="lstIname">${name}</span>`+
|
|
`</div>`+
|
|
`</label>`+
|
|
extraHtml +
|
|
`</div>`;
|
|
}
|
|
|
|
function btype(b)
|
|
{
|
|
switch (b) {
|
|
case 32: return "ESP32";
|
|
case 33: return "ESP32-S2";
|
|
case 34: return "ESP32-S3";
|
|
case 35: return "ESP32-C3";
|
|
case 82: return "ESP8266";
|
|
}
|
|
return "?";
|
|
}
|
|
|
|
function bname(o)
|
|
{
|
|
if (o.name=="WLED") return o.ip;
|
|
return o.name;
|
|
}
|
|
|
|
//WLEDMM call a node with json api command
|
|
function callNode(ip, type, json) {
|
|
console.log("callNode", ip, type, json);
|
|
|
|
fetch('http://' + ip + '/json/' + type, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
//'Content-Type': 'text/html; charset=UTF-8'
|
|
},
|
|
body: JSON.stringify(json)
|
|
})
|
|
.then((res)=>{
|
|
console.log("then res", res);
|
|
loadNodes(); //reload nodes
|
|
})
|
|
.then((json)=>{
|
|
console.log("then json", json);
|
|
});
|
|
}
|
|
|
|
function ddpAll() {
|
|
if (!confirm('Press Yes/OK if you know what you are doing!')) return;
|
|
ins = [];
|
|
start = 0;
|
|
order = 0;
|
|
for (var node of nodesData) {
|
|
if (node.info.ip != lastinfo.ip) { //do not add to self
|
|
console.log(node);
|
|
output = {};
|
|
output.start = start; //increase with count
|
|
output.len = node.info.leds.count;
|
|
output.pin = node.info.ip.split(".");
|
|
output.order = order++;
|
|
output.rev = false;
|
|
output.skip = 0;
|
|
output.type = 80;
|
|
output.ref = false;
|
|
output.rgbm = 0;
|
|
// "ins":[{"start":0,"len":24,"pin":[2],"order":0,"rev":false,"skip":0,"type":22,"ref":false,"rgbwm":0},
|
|
// {"start":24,"len":241,"pin":[192,168,121,57],"order":1,"rev":false,"skip":0,"type":80,"ref":false,"rgbwm":0}]
|
|
ins.push(output);
|
|
start+=node.info.leds.count;
|
|
}
|
|
}
|
|
// console.log("ins", lastinfo.ip,JSON.stringify({"hw":{"led":{"ins":ins}}}));
|
|
|
|
//update own cfg.json
|
|
callNode(lastinfo.ip, "cfg", {"hw":{"led":{"ins":ins}}}); //self
|
|
}
|
|
|
|
//curl -s -F "update=@/Users/ewoudwijma/Developer/GitHub/MoonModules/WLED/build_output/release/WLEDMM_0.14.0-b27.31_esp32_4MB_M.bin" 192.168.8.105/update >nul &
|
|
|
|
//WLEDMM
|
|
function SuperSync() {
|
|
if (!confirm('Press Yes/OK if you know what you are doing!')) return;
|
|
|
|
for (i=0; i<nodesData.length; i++) {
|
|
if (nodesData[i].info.ip != lastinfo.ip) { //do not add to self
|
|
if (gId(`ssu${i}`).innerText == "yes") { //only update if needed (see SSync column)
|
|
callNode(nodesData[i].info.ip, "cfg", {"hw":{"led":nodesData[i].cfg.hw.led}});
|
|
callNode(nodesData[i].info.ip, "cfg", {"light":nodesData[i].cfg.light});
|
|
callNode(nodesData[i].info.ip, "state", {"rb":true}); //reboot
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
function populateNodes(i,n)
|
|
{
|
|
var cn="";
|
|
var urows="";
|
|
var nnodes = 0;
|
|
|
|
//WLEDMM starts here
|
|
nodesData = []; //WLEDMM reset nodes
|
|
|
|
function showPanel(panel) {
|
|
return "(" + panel.x + "," + panel.y + ") - " + panel.w + "x" + panel.h + " " + (panel.b?1:0) + (panel.r?1:0) + (panel.v?1:0) + (panel.s?1:0);
|
|
}
|
|
|
|
function checkNode(nodeNr) {
|
|
console.log("CheckNode", nodeNr, nodesData[nodeNr]);
|
|
let errFound = false;
|
|
|
|
//warnings
|
|
if (gId(`vid${nodeNr}`).innerText != lastinfo.vid) {
|
|
gId(`vid${nodeNr}`).style.color = "orange";
|
|
}
|
|
if (gId(`ver${nodeNr}`).innerText != lastinfo.ver) {
|
|
gId(`ver${nodeNr}`).style.color = "orange";
|
|
}
|
|
if (gId(`scale-bri${nodeNr}`).innerText != nodesData[nodeNr].cfg.light["scale-bri"]) {
|
|
gId(`scale-bri${nodeNr}`).style.color = "orange";
|
|
}
|
|
if (gId(`fps${nodeNr}`).innerText != nodesData[nodeNr].cfg.hw.led.fps) {
|
|
gId(`fps${nodeNr}`).style.color = "orange";
|
|
}
|
|
|
|
if (nodesData[nodeNr].cfg.hw.led.matrix) {
|
|
//if panel 0 the same and 1 or 2 panels (supersync always 1 or 2) and matrix size is this size then node is OK
|
|
if (nodesData[nodeNr].info.leds.countP != nodesData[nodeNr].cfg.hw.led.matrix.panels[0].w * nodesData[nodeNr].cfg.hw.led.matrix.panels[0].h) {
|
|
errFound = true;
|
|
gId(`lpc${nodeNr}`).style.color = "red";
|
|
}
|
|
if (gId(`mrx${nodeNr}`).innerText != lastinfo.leds.matrix.w + "x" + lastinfo.leds.matrix.h) {
|
|
errFound = true;
|
|
gId(`mrx${nodeNr}`).style.color = "red";
|
|
}
|
|
if (gId(`pnl0${nodeNr}`).innerText != gId(`pnlX${nodeNr}`).innerText) {
|
|
errFound = true;
|
|
gId(`pnl0${nodeNr}`).style.color = "red";
|
|
}
|
|
if (gId(`pnlC${nodeNr}`).innerText > 2) {
|
|
errFound = true;
|
|
gId(`pnlC${nodeNr}`).style.color = "red";
|
|
}
|
|
}
|
|
|
|
//set the SuperSync Update needed column
|
|
gId(`ssu${nodeNr}`).innerText = errFound?"yes":"no";
|
|
if (errFound) {
|
|
gId(`ssu${nodeNr}`).style.color = "red";
|
|
}
|
|
}
|
|
|
|
//fetch both cfg.json and info from a node and store in nodesData array
|
|
function fetchInfoAndCfg(ip, nodeNr, parms, callback) {
|
|
//add td placeholders
|
|
urows += `<tr>`;
|
|
for (let nm of ["ins", "pwr", "ip", "type", "rel", "ver", "vid", "fx", "scale-bri", "gcc", "fps", "fpsr", "lpc", "lvc", "mrx", "pnl0", "pnlC", "pnlX", "ssu"])
|
|
urows += `<td id="${nm}${nodeNr}"></td>`;
|
|
urows += `</tr>`;
|
|
|
|
//fetch info, state and effects
|
|
fetchAndExecute(`http://${ip}/`, "json", nodeNr, function(nodeNr, text) {
|
|
let info = JSON.parse(text)["info"];
|
|
let state = JSON.parse(text)["state"];
|
|
let effects = JSON.parse(text)["effects"];
|
|
|
|
//set values
|
|
gId(`pwr${nodeNr}`).innerHTML = "<button class=\"btn btn-xs\" onclick=\"callNode('"+info.ip+"','state',{'on':"+(state.on?"false":"true")+"});\"><i class=\"icons "+(state.on?"on":"off")+"\"></i></button>";
|
|
gId(`type${nodeNr}`).innerText = info.arch;
|
|
gId(`vid${nodeNr}`).innerText = info.vid;
|
|
gId(`ip${nodeNr}`).innerText = info.ip;
|
|
gId(`rel${nodeNr}`).innerText = info.rel;
|
|
gId(`ver${nodeNr}`).innerText = info.ver;
|
|
gId(`lvc${nodeNr}`).innerText = info.leds.count;
|
|
gId(`lpc${nodeNr}`).innerText = info.leds.countP;
|
|
gId(`fpsr${nodeNr}`).innerText = info.leds.fps;
|
|
gId(`fx${nodeNr}`).innerText = effects[state.seg[0].fx];
|
|
|
|
//store data
|
|
if (!nodesData[nodeNr]) nodesData[nodeNr] = {};
|
|
nodesData[nodeNr].info = info;
|
|
|
|
//if the node has a matrix, show matrix info
|
|
if (info.leds.matrix) {
|
|
gId(`mrx${nodeNr}`).innerText = info.leds.matrix.w + "x" + info.leds.matrix.h;
|
|
}
|
|
|
|
//fetch cfg.json
|
|
fetchAndExecute(`http://${ip}/`, "cfg.json", nodeNr, function(nodeNr,text) {
|
|
let cfg = JSON.parse(text);
|
|
|
|
//set values
|
|
let url = `<button class="btn" ${(ip == lastinfo.ip)?'style="background-color: red;"':''} title="${ip}" onclick="location.assign('http://${ip}');">${cfg.id.name}</button>`;
|
|
gId(`ins${nodeNr}`).innerHTML = url;
|
|
gId(`scale-bri${nodeNr}`).innerText = cfg.light["scale-bri"];
|
|
gId(`gcc${nodeNr}`).innerText = cfg.light.gc.col > 1;
|
|
gId(`fps${nodeNr}`).innerText = cfg.hw.led.fps;
|
|
|
|
//if the node has a matrix, show matrix info
|
|
if (cfg.hw.led.matrix) {
|
|
gId(`pnl0${nodeNr}`).innerText = showPanel(cfg.hw.led.matrix.panels[0]); //show the first panel
|
|
gId(`pnlC${nodeNr}`).innerText = cfg.hw.led.matrix.panels.length; //show nr of panels
|
|
|
|
//if self, assign it's matrix details to all others
|
|
if (ip == lastinfo.ip) {
|
|
let panelIndex = 0; //loop over panels
|
|
for (let i=0; i<nnodes; i++) { //loop over all nodes found
|
|
if (panelIndex < cfg.hw.led.matrix.panels.length && n.nodes[i].ip != lastinfo.ip) { //loop over panels of self: assign each panel to a different node
|
|
|
|
let panelX = cfg.hw.led.matrix.panels[panelIndex];
|
|
|
|
gId(`pnlX${i}`).innerText = showPanel(panelX);
|
|
|
|
//store data
|
|
//nodesData[i] does not exist if not all fetches done
|
|
if (!nodesData[i]) nodesData[i] = {};
|
|
|
|
nodesData[i].cfg = structuredClone(cfg); //structuredClone: by value, not by reference so we can make changex
|
|
|
|
//now modify led setup for the specific node
|
|
nodesData[i].cfg.hw.led.matrix.ba = true; //advanced settings mode
|
|
|
|
//set the first and second panel of the node
|
|
let widthOK = nodesData[nodeNr].info.leds.matrix.w == panelX.x + panelX.w;
|
|
let heightOK = nodesData[nodeNr].info.leds.matrix.h == panelX.y + panelX.h;
|
|
if (widthOK && heightOK) {
|
|
nodesData[i].cfg.hw.led.matrix.mpc = 1;
|
|
nodesData[i].cfg.hw.led.matrix.mph = 1;
|
|
nodesData[i].cfg.hw.led.matrix.mpv = 1;
|
|
nodesData[i].cfg.hw.led.matrix.panels = [panelX];
|
|
} else {
|
|
let dummyPanel = {"b": false,"r": false,"v": false,"s": false,
|
|
"x": nodesData[nodeNr].info.leds.matrix.w - 1,
|
|
"y": nodesData[nodeNr].info.leds.matrix.h - 1,
|
|
"h": 1, "w": 1};
|
|
nodesData[i].cfg.hw.led.matrix.mpc = 2;
|
|
nodesData[i].cfg.hw.led.matrix.mph = 1;
|
|
nodesData[i].cfg.hw.led.matrix.mpv = 2;
|
|
nodesData[i].cfg.hw.led.matrix.panels = [panelX, dummyPanel];
|
|
}
|
|
|
|
//only one led output, same as first master led output, length equal as panelX dimensions
|
|
nodesData[i].cfg.hw.led.ins = [cfg.hw.led.ins[0]];
|
|
nodesData[i].cfg.hw.led.ins[0].start = 0;
|
|
nodesData[i].cfg.hw.led.ins[0].len = panelX.w * panelX.h;
|
|
|
|
panelIndex++;
|
|
}
|
|
else
|
|
gId(`pnlX${i}`).innerText = "";
|
|
}
|
|
}
|
|
}
|
|
callback(nodeNr);
|
|
}, function(nodeNr, text) {console.log("cfg error", nodeNr, n.nodes[nodeNr].name, text);callback(nodeNr);}); //also callback on error
|
|
}, function(nodeNr, text) {
|
|
//to show nodes which failed in providing json info
|
|
let ip = n.nodes[nodeNr].ip;
|
|
let name = n.nodes[nodeNr].name;
|
|
let url = `<button class="btn" ${(ip == lastinfo.ip)?'style="background-color: red;"':''} title="${ip}" onclick="location.assign('http://${ip}');">${name}</button>`;
|
|
gId(`ins${nodeNr}`).innerHTML = url;
|
|
gId(`ip${nodeNr}`).innerText = ip;
|
|
console.log("json error", nodeNr, ip, name, text);
|
|
callback(nodeNr); //also callback on error
|
|
});
|
|
} //fetchInfoAndCfg
|
|
|
|
if (n.nodes) {
|
|
//WLEDMM add this node to nodes
|
|
let thisNode = {};
|
|
thisNode.name = i.name;
|
|
thisNode.ip = i.ip;
|
|
n.nodes.push(thisNode);
|
|
|
|
n.nodes.sort((a,b) => (a.name).localeCompare(b.name)); //alphabetic on name
|
|
// console.log("populateNodes",i,n);
|
|
|
|
//set table header
|
|
urows += `<tr>`;
|
|
for (let nm of ["Instance", "Power", "IP", "Type", "Release", "Version", "Build", "Effect", "Bri%", "Gamma", "FPS", "FPS Real", "LedsP#", "LedsV#", "Matrix", "Panel0", "Panels", "PanelX", "SSync"])
|
|
urows += `<th style="font-size:80%;">${nm}</th>`;
|
|
urows += `</tr>`;
|
|
|
|
//show other nodes e.g. {name: "MM 32 L", type: 32, ip: "192.168.121.249", age: 1, vid: 2305080}
|
|
var nodesDone = 0;
|
|
for (let o of n.nodes) {
|
|
if (o.name) {
|
|
if (o.ip) { //in ap mode no ip...
|
|
fetchInfoAndCfg(o.ip, nnodes, -1, function(nodeNr) {
|
|
nodesDone++;
|
|
console.log("nodesDone", nodesDone, nodeNr, n.nodes.length, nnodes, n.nodes[nodeNr].name);
|
|
//if all done
|
|
if (nodesDone == n.nodes.length ) {
|
|
for (let i=0; i<n.nodes.length; i++) {
|
|
if (n.nodes[i].ip != lastinfo.ip && nodesData[i] && nodesData[i].info && nodesData[i].cfg) //not self and data has been collected (no errors getting files)
|
|
checkNode(i);
|
|
}
|
|
}
|
|
});
|
|
nnodes++;
|
|
}
|
|
}
|
|
}
|
|
} //if n.nodes
|
|
|
|
if (i.ndc < 0) cn += `Instance List is disabled.`;
|
|
else if (!n.nodes) cn += `No other instances found.`;
|
|
cn += `<table>${urows}</table>`;
|
|
cn += `<button class="btn" onclick="ddpAll();">DDP all</button>`;
|
|
cn += `<button class="btn" onclick="SuperSync();">SuperSync</button>`;
|
|
gId('kn').innerHTML = cn;
|
|
// ${inforow("Current instance:",i.name)} //WLEDMM current instance is now also shown as node
|
|
}
|
|
|
|
function loadNodes()
|
|
{
|
|
var url = (loc?`http://${locip}`:'') + '/json/nodes';
|
|
fetch(url, {
|
|
method: 'get'
|
|
})
|
|
.then((res)=>{
|
|
if (!res.ok) showToast('Could not load Node list!', true);
|
|
return res.json();
|
|
})
|
|
.then((json)=>{
|
|
clearErrorToast(100);
|
|
populateNodes(lastinfo, json);
|
|
})
|
|
.catch((e)=>{
|
|
showToast(e, true);
|
|
});
|
|
}
|
|
|
|
// update the 'sliderdisplay' background div of a slider for a visual indication of slider position
|
|
function updateTrail(e)
|
|
{
|
|
if (e==null) return;
|
|
let sd = e.parentNode.getElementsByClassName('sliderdisplay')[0];
|
|
if (sd && getComputedStyle(sd).getPropertyValue("--bg") !== "none") {
|
|
var max = e.hasAttribute('max') ? e.attributes.max.value : 255;
|
|
var perc = Math.round(e.value * 100 / max);
|
|
if (perc < 50) perc += 2;
|
|
var val = `linear-gradient(90deg, var(--bg) ${perc}%, var(--c-6) ${perc}%)`;
|
|
sd.style.backgroundImage = val;
|
|
}
|
|
var b = e.parentNode.parentNode.getElementsByTagName('output')[0];
|
|
if (b) b.innerHTML = e.value;
|
|
}
|
|
|
|
// rangetouch slider function
|
|
function toggleBubble(e)
|
|
{
|
|
var b = e.target.parentNode.parentNode.getElementsByTagName('output')[0];
|
|
b.classList.toggle('sliderbubbleshow');
|
|
}
|
|
|
|
// updates segment length upon input of segment values
|
|
function updateLen(s, draw=true) //WLEDMM conditonally draw segment view
|
|
{
|
|
if (!gId(`seg${s}s`)) return;
|
|
var start = parseInt(gId(`seg${s}s`).value);
|
|
var stop = parseInt(gId(`seg${s}e`).value) + (cfg.comp.seglen?start:0);
|
|
var len = stop - start;
|
|
let sY = gId(`seg${s}sY`);
|
|
let eY = gId(`seg${s}eY`);
|
|
let sX = gId(`seg${s}s`);
|
|
let eX = gId(`seg${s}e`);
|
|
let of = gId(`seg${s}of`);
|
|
let mySH = gId("mkSYH");
|
|
let mySD = gId("mkSYD");
|
|
if (isM) {
|
|
// do we have 1D segment *after* the matrix?
|
|
if (start >= mw*mh) {
|
|
if (sY) { sY.value = 0; sY.max = 0; sY.min = 0; }
|
|
if (eY) { eY.value = 1; eY.max = 1; eY.min = 0; }
|
|
sX.min = mw*mh; sX.max = ledCount-1;
|
|
eX.min = mw*mh+1; eX.max = ledCount;
|
|
if (mySH) mySH.classList.add("hide");
|
|
if (mySD) mySD.classList.add("hide");
|
|
if (of) of.classList.remove("hide");
|
|
} else {
|
|
// matrix setup
|
|
if (mySH) mySH.classList.remove("hide");
|
|
if (mySD) mySD.classList.remove("hide");
|
|
if (of) of.classList.add("hide");
|
|
let startY = parseInt(sY.value);
|
|
let stopY = parseInt(eY.value) + (cfg.comp.seglen?startY:0);
|
|
len *= (stopY-startY);
|
|
let tPL = gId(`seg${s}lbtm`);
|
|
if (stop-start>1 && stopY-startY>1) {
|
|
// 2D segment
|
|
if (tPL) tPL.classList.remove('hide'); // unhide transpose checkbox
|
|
let sE = gId('fxlist').querySelector(`.lstI[data-id="${selectedFx}"]`);
|
|
if (sE) {
|
|
let sN = sE.querySelector(".lstIname").innerText;
|
|
let seg = gId(`seg${s}map2D`);
|
|
if (seg) {
|
|
if(sN.indexOf("\u25A6")<0) seg.classList.remove('hide'); // unhide mapping for 1D effects (| in name)
|
|
else seg.classList.add('hide'); // hide mapping otherwise
|
|
}
|
|
}
|
|
} else {
|
|
// 1D segment in 2D set-up
|
|
if (tPL) {
|
|
tPL.classList.add('hide'); // hide transpose checkbox
|
|
gId(`seg${s}tp`).checked = false; // and uncheck it
|
|
}
|
|
}
|
|
}
|
|
}
|
|
var out = "(delete)";
|
|
if (len > 1) {
|
|
out = `${len} LEDs`;
|
|
} else if (len == 1) {
|
|
out = "1 LED";
|
|
}
|
|
|
|
if (gId(`seg${s}grp`) != null)
|
|
{
|
|
var grp = parseInt(gId(`seg${s}grp`).value);
|
|
var spc = parseInt(gId(`seg${s}spc`).value);
|
|
if (grp == 0) grp = 1;
|
|
var virt = Math.ceil(len/(grp + spc));
|
|
if (!isNaN(virt) && (grp > 1 || spc > 0)) out += ` (${virt} virtual)`;
|
|
}
|
|
if (isM && start >= mw*mh) out += " [strip]";
|
|
|
|
gId(`seg${s}len`).innerHTML = out;
|
|
|
|
console.log("drawSegmentView","updateLen");
|
|
if (draw && isM) drawSegmentView(); //WLEDMM draw new segmentview if something changes in a segment
|
|
}
|
|
|
|
//WLEDMM
|
|
function drawSegmentView() {
|
|
|
|
var px, py, pw, ph;
|
|
var topLeftX, topLeftY;
|
|
|
|
function initSegmentVars(p) {
|
|
px = parseInt(gId("seg"+p+"s").value); //first led x
|
|
if (!gId("seg"+p+"sY")) return false; //no draw for 1D segments (yet)
|
|
py = parseInt(gId("seg"+p+"sY").value); //first led y
|
|
pw = parseInt(gId("seg"+p+"e").value - gId("seg"+p+"s").value); //width
|
|
ph = parseInt(gId("seg"+p+"eY").value - gId("seg"+p+"sY").value); //height
|
|
// console.log("sergment", p, px, py, pw, ph);
|
|
topLeftX = px*ppL;
|
|
topLeftY = py*ppL;
|
|
// console.log("rect", p, topLeftX, topLeftY, pw*ppL, ph*ppL);
|
|
return true;
|
|
}
|
|
|
|
//calc max height and width
|
|
var maxWidth = 0;
|
|
var maxHeight = 0;
|
|
for (let p=0; p<gId("segcont").children.length; p++) {
|
|
if (!initSegmentVars(p)) break;
|
|
maxWidth = Math.max(maxWidth, px + pw);
|
|
maxHeight = Math.max(maxHeight, py + ph);
|
|
}
|
|
|
|
canvasPeek = gId("canvasPeek");
|
|
if (!ctx) {
|
|
//WLEDMM: add canvas, initialize and set UI
|
|
var canvas = gId("canvasSegments");
|
|
ctx = canvas.getContext('2d');
|
|
}
|
|
|
|
let segments = gId("Segments");
|
|
let windowWidth = Math.min(window.innerWidth*0.98, maxWidth*30);
|
|
let windowWidthFactor = maxWidth > maxHeight?1:maxWidth/maxHeight;
|
|
ctx.canvas.width = (segments.offsetWidth > 800?windowWidth:300) * windowWidthFactor; //Mobile and non pc mode gets 300, pc 800
|
|
ctx.canvas.height = ctx.canvas.width / maxWidth * maxHeight;
|
|
canvasPeek.width = ctx.canvas.width;
|
|
canvasPeek.height = ctx.canvas.height;
|
|
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
|
|
|
|
var ppL = ctx.canvas.width / maxWidth; //pixels per led
|
|
// console.log("dim", ctx.canvas.width , maxWidth, ctx.canvas.height , maxHeight, ppL);
|
|
|
|
var colorArray = [[255,0,0], [0,255,0], [0,0,255], [255,0,255], [255,165,0], [255,255,0]];
|
|
// ["red", "green", "blue", "magenta", "orange", "yellow"];
|
|
|
|
for (let p=0; p<gId("segcont").children.length; p++) {
|
|
// console.log(gId("P"+p+"X").value, gId("P"+p+"Y").value, gId("P"+p+"W").value, gId("P"+p+"H").value, gId("P"+p+"B").value, gId("P"+p+"R").value, gId("P"+p+"V").value, gId("P"+p+"S").checked);
|
|
|
|
if (!initSegmentVars(p)) break;
|
|
|
|
if (gId("segcont").children.length > 1) { //Estetic: Don't draw surrounding box if only one segment
|
|
ctx.lineWidth = 3;
|
|
ctx.strokeStyle="white";
|
|
ctx.strokeRect(topLeftX, topLeftY, pw*ppL, ph*ppL);
|
|
}
|
|
|
|
var fx = parseInt(gId("seg"+p+"fx").value);
|
|
|
|
var grp = parseInt(gId("seg"+p+"grp").value); //reverseX
|
|
var spc = parseInt(gId("seg"+p+"spc").value); //reverseX
|
|
|
|
var rx = gId("seg"+p+"rev").checked; //reverseX
|
|
var ry = gId("seg"+p+"rY").checked; //reverseY
|
|
var mx = gId("seg"+p+"mi").checked; //mirrorX
|
|
var my = gId("seg"+p+"mY").checked; //mirrorY
|
|
var tp = gId("seg"+p+"tp").checked; //mirrorY
|
|
|
|
ctx.lineWidth = 1;
|
|
if (mx) {
|
|
ctx.beginPath();
|
|
ctx.moveTo(topLeftX + pw/2*ppL, topLeftY);
|
|
ctx.lineTo(topLeftX + pw/2*ppL, topLeftY + ph*ppL);
|
|
ctx.stroke();
|
|
}
|
|
if (my) {
|
|
ctx.beginPath();
|
|
ctx.moveTo(topLeftX, topLeftY + ph/2*ppL);
|
|
ctx.lineTo(topLeftX + pw*ppL, topLeftY + ph/2*ppL);
|
|
ctx.stroke();
|
|
}
|
|
if (ry) {
|
|
ctx.beginPath();
|
|
ctx.moveTo(topLeftX + pw/8*ppL+10, topLeftY+10);
|
|
ctx.lineTo(topLeftX + pw/8*ppL, topLeftY);
|
|
ctx.lineTo(topLeftX + pw/8*ppL, topLeftY + ph*ppL);
|
|
ctx.lineTo(topLeftX + pw/8*ppL-10, topLeftY + ph*ppL-10);
|
|
ctx.stroke();
|
|
}
|
|
if (rx) {
|
|
ctx.beginPath();
|
|
ctx.moveTo(topLeftX+10, topLeftY + ph/8*ppL+10);
|
|
ctx.lineTo(topLeftX, topLeftY + ph/8*ppL);
|
|
ctx.lineTo(topLeftX + pw*ppL, topLeftY + ph/8*ppL);
|
|
ctx.lineTo(topLeftX + pw*ppL-10, topLeftY + ph/8*ppL-10);
|
|
ctx.stroke();
|
|
}
|
|
if (tp) {
|
|
ctx.beginPath();
|
|
ctx.moveTo(topLeftX, topLeftY);
|
|
ctx.lineTo(topLeftX + pw*ppL, topLeftY + ph*ppL);
|
|
ctx.stroke();
|
|
}
|
|
|
|
let groupLength = grp+spc;
|
|
|
|
//make a string from [x,y,z] adjusted by brightness
|
|
function rgbToString(colorRGB, brightness) {
|
|
function colorAdjust(color) {return 55+150*color/255*brightness;}
|
|
return `rgb(${colorAdjust(colorRGB[0])},${colorAdjust(colorRGB[1])}, ${colorAdjust(colorRGB[2])})`;
|
|
}
|
|
|
|
//draw leds
|
|
var counter = 0;
|
|
for (let y=0; y<ph; y+=groupLength) {
|
|
for (let x=0; x<pw; x+=groupLength) {
|
|
for (let j = 0; j < grp; j++) { // grouping vertically
|
|
for (let g = 0; g < grp; g++) { // grouping horizontally
|
|
let xX = (x+g), yY = (y+j);
|
|
ctx.fillStyle = rgbToString(colorArray[p%colorArray.length], counter/ph/pw);
|
|
ctx.beginPath();
|
|
ctx.arc(topLeftX + ppL/2 + xX*ppL, topLeftY + ppL/2 + yY * ppL, ppL*0.4, 0, 2 * Math.PI);
|
|
ctx.fill();
|
|
counter++;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
} // for each segment
|
|
|
|
if (gId("segcont").children.length > 1) { //Only show this if more then one segment
|
|
gId("MD").innerHTML = "total W*H=LC: " + maxWidth + " x " + maxHeight + " = " + maxWidth * maxHeight;
|
|
}
|
|
gId("MD").style.display = gId("segcont").children.length > 1?"inline":"none"
|
|
|
|
function post() {
|
|
for (let p=0; p<gId("segcont").children.length; p++) {
|
|
if (!initSegmentVars(p)) break;
|
|
|
|
if (gId("segcont").children.length>1) { //only show number and name if more than one segment
|
|
ctx.font = '40px Arial';
|
|
ctx.fillStyle = "orange";
|
|
ctx.fillText(p, topLeftX + pw/2*ppL - 10, topLeftY + ph/2*ppL + 10);
|
|
|
|
//show name of fx
|
|
ctx.font = '20px Arial';
|
|
ctx.fillStyle = "white";
|
|
var name = eJson.find((o)=>{return o.id==fx}).name;
|
|
ctx.fillText(name, topLeftX+10, topLeftY + ph*ppL - 10);
|
|
}
|
|
}
|
|
}
|
|
|
|
//draw the ledmap
|
|
if (ledmapNr>=0 && ctx) { //WLEDMM: @Troy#2642 : include ledmap = 0 as default ledmap
|
|
var fileName;
|
|
if (ledmapNr==0)
|
|
fileName = "ledmap.json"; //0 is ledmap.json, not ledmap0.json
|
|
else if (ledmapNr<10)
|
|
fileName = "ledmap"+ledmapNr+".json";
|
|
else
|
|
fileName = ledmapFileNames[ledmapNr-10];
|
|
|
|
fetchAndExecute((loc?`http://${locip}`:'.') + "/", fileName, null, function(parms,text) {
|
|
var ledmapJson = JSON.parse(text);
|
|
var counter = 0;
|
|
var noMap = [];
|
|
for (let i=0;i<maxWidth * maxHeight;i++) noMap.push(i); //initially add all pixels in array
|
|
var colorArray = ["yellow", "green", "magenta", "orange"];
|
|
|
|
var customMappingTable = [];
|
|
for (let i=0;i<maxWidth * maxHeight;i++) customMappingTable.push(-1); //init with noshow
|
|
for (let i=0;i<maxWidth * maxHeight;i++)
|
|
if (ledmapJson["map"][i]>=0) customMappingTable[ledmapJson["map"][i]] = i;
|
|
|
|
for (let i=0;i<customMappingTable.length;i++) {
|
|
let mapIndex = customMappingTable[i];
|
|
if (mapIndex != -1) {
|
|
ctx.font = parseInt(ppL/3) + 'px Arial';
|
|
ctx.fillStyle = "white";
|
|
if (lastinfo.outputs!=null) {
|
|
var ledcount = 0;
|
|
for (let o=0; o<lastinfo.outputs.length;o++) {
|
|
ledcount+=lastinfo.outputs[o];
|
|
if (counter >= ledcount)
|
|
ctx.fillStyle = colorArray[o%colorArray.length];
|
|
}
|
|
}
|
|
x = mapIndex%maxWidth;
|
|
y = parseInt(mapIndex/maxWidth);
|
|
ctx.fillText(counter, topLeftX + ppL/2 + x*ppL-ppL*0.3, topLeftY + ppL/2 + y * ppL);
|
|
//remove the found pixels from noMap
|
|
const index = noMap.indexOf(mapIndex);
|
|
if (index > -1) noMap.splice(index, 1); // 2nd parameter means remove one item only
|
|
}
|
|
counter++;
|
|
}
|
|
//WLEDMM: make pixels not in ledmap black
|
|
for (let i=0;i<noMap.length;i++) {
|
|
x = noMap[i]%maxWidth;
|
|
y = parseInt(noMap[i]/maxWidth);
|
|
ctx.fillStyle = "black";
|
|
ctx.beginPath();
|
|
ctx.arc(topLeftX + ppL/2 + x*ppL, topLeftY + ppL/2 + y * ppL, ppL*0.4, 0, 2 * Math.PI);
|
|
ctx.fill();
|
|
}
|
|
post();
|
|
}, function(parms,error) { //error handling
|
|
console.log("drawledmap error fetching " + fileName +": ", error);
|
|
// downloadGHFile("LM", fileName, true, false); WLEDMM: remove as this has too much impact
|
|
post();
|
|
});
|
|
}
|
|
else
|
|
post();
|
|
}
|
|
|
|
// updates background color of currently selected preset
|
|
function updatePA()
|
|
{
|
|
let ps;
|
|
ps = gEBCN("pres"); for (let p of ps) p.classList.remove('selected');
|
|
ps = gEBCN("psts"); for (let p of ps) p.classList.remove('selected');
|
|
if (currentPreset > 0) {
|
|
var acv = gId(`p${currentPreset}o`);
|
|
if (acv /*&& !acv.classList.contains('expanded')*/) {
|
|
acv.classList.add('selected');
|
|
/*
|
|
// scroll selected preset into view (on WS refresh)
|
|
acv.scrollIntoView({
|
|
behavior: 'smooth',
|
|
block: 'center'
|
|
});
|
|
*/
|
|
}
|
|
acv = gId(`p${currentPreset}qlb`);
|
|
if (acv) acv.classList.add('selected');
|
|
}
|
|
}
|
|
|
|
function updateUI()
|
|
{
|
|
gId('buttonPower').className = (isOn) ? 'active':'';
|
|
gId('buttonNl').className = (nlA) ? 'active':'';
|
|
gId('buttonSync').className = (syncSend) ? 'active':'';
|
|
|
|
updateSelectedFx();
|
|
updateSelectedPalette(selectedPal); // must be after updateSelectedFx() to un-hide color slots for * palettes
|
|
|
|
updateTrail(gId('sliderBri'));
|
|
updateTrail(gId('sliderSpeed'));
|
|
updateTrail(gId('sliderIntensity'));
|
|
|
|
updateTrail(gId('sliderC1'));
|
|
updateTrail(gId('sliderC2'));
|
|
updateTrail(gId('sliderC3'));
|
|
|
|
if (hasRGB) {
|
|
updateTrail(gId('sliderR'));
|
|
updateTrail(gId('sliderG'));
|
|
updateTrail(gId('sliderB'));
|
|
}
|
|
if (hasWhite) updateTrail(gId('sliderW'));
|
|
|
|
var ccfg = cfg.comp.colors;
|
|
gId('wwrap').style.display = (hasWhite) ? "block":"none"; // white channel
|
|
gId('wbal').style.display = (hasCCT) ? "block":"none"; // white balance
|
|
gId('hexw').style.display = (ccfg.hex) ? "block":"none"; // HEX input
|
|
gId('picker').style.display = (hasRGB && ccfg.picker) ? "block":"none"; // color picker wheel
|
|
gId('hwrap').style.display = (hasRGB && !ccfg.picker) ? "block":"none"; // hue slider
|
|
gId('swrap').style.display = (hasRGB && !ccfg.picker) ? "block":"none"; // saturation slider
|
|
gId('vwrap').style.display = (hasRGB) ? "block":"none"; // brightness (value) slider
|
|
gId('kwrap').style.display = (hasRGB && !hasCCT) ? "block":"none"; // Kelvin slider
|
|
gId('rgbwrap').style.display = (hasRGB && ccfg.rgb) ? "block":"none"; // RGB sliders
|
|
gId('qcs-w').style.display = (hasRGB && ccfg.quick) ? "block":"none"; // quick selection
|
|
//gId('csl').style.display = (hasRGB || hasWhite) ? "block":"none"; // color selectors (hide for On/Off bus)
|
|
//gId('palw').style.display = (hasRGB) ? "inline-block":"none"; // palettes are shown/hidden in setEffectParameters()
|
|
|
|
updatePA();
|
|
updatePSliders();
|
|
}
|
|
|
|
function updateSelectedPalette(s)
|
|
{
|
|
var parent = gId('pallist');
|
|
var selPaletteInput = parent.querySelector(`input[name="palette"][value="${s}"]`);
|
|
if (selPaletteInput) selPaletteInput.checked = true;
|
|
|
|
var selElement = parent.querySelector('.selected');
|
|
if (selElement) selElement.classList.remove('selected');
|
|
|
|
var selectedPalette = parent.querySelector(`.lstI[data-id="${s}"]`);
|
|
if (selectedPalette) parent.querySelector(`.lstI[data-id="${s}"]`).classList.add('selected');
|
|
|
|
// in case of special palettes (* Colors...), force show color selectors (if hidden by effect data)
|
|
let cd = gId('csl').children; // color selectors
|
|
if (s > 1 && s < 6) {
|
|
cd[0].classList.remove('hide'); // * Color 1
|
|
if (s > 2) cd[1].classList.remove('hide'); // * Color 1 & 2
|
|
if (s > 3) cd[2].classList.remove('hide'); // all colors
|
|
} else {
|
|
for (let i of cd) if (i.dataset.hide == '1') i.classList.add('hide');
|
|
}
|
|
}
|
|
|
|
function updateSelectedFx()
|
|
{
|
|
var parent = gId('fxlist');
|
|
var selEffectInput = parent.querySelector(`input[name="fx"][value="${selectedFx}"]`);
|
|
if (selEffectInput) selEffectInput.checked = true;
|
|
|
|
var selElement = parent.querySelector('.selected');
|
|
if (selElement) {
|
|
selElement.classList.remove('selected');
|
|
selElement.style.bottom = null; // remove element style added in slider handling
|
|
}
|
|
|
|
var selectedEffect = parent.querySelector(`.lstI[data-id="${selectedFx}"]`);
|
|
if (selectedEffect) {
|
|
selectedEffect.classList.add('selected');
|
|
setEffectParameters(selectedFx);
|
|
// hide non-0D effects if segment only has 1 pixel (0D)
|
|
var fxs = parent.querySelectorAll('.lstI');
|
|
for (const fx of fxs) {
|
|
let opts = fx.dataset.opt.split(";");
|
|
if (fx.dataset.id>0) {
|
|
if (segLmax==0) fx.classList.add('hide'); // none of the segments selected (hide all effects)
|
|
else {
|
|
if ((segLmax==1 && (!opts[3] || opts[3].indexOf("0")<0)) || (!isM && opts[3] && ((opts[3].indexOf("2")>=0 && opts[3].indexOf("1")<0)))) fx.classList.add('hide');
|
|
else fx.classList.remove('hide');
|
|
}
|
|
}
|
|
}
|
|
// hide 2D mapping and/or sound simulation options
|
|
var selectedName = selectedEffect.querySelector(".lstIname").innerText;
|
|
var segs = gId("segcont").querySelectorAll(`div[data-map="map2D"]`);
|
|
for (const seg of segs) if (selectedName.indexOf("\u25A6")<0) seg.classList.remove('hide'); else seg.classList.add('hide');
|
|
var segs = gId("segcont").querySelectorAll(`div[data-snd="si"]`);
|
|
for (const seg of segs) if (selectedName.indexOf("\u266A")<0 && selectedName.indexOf("\u266B")<0) seg.classList.add('hide'); else seg.classList.remove('hide'); // also "♫ "?
|
|
}
|
|
}
|
|
|
|
function displayRover(i,s)
|
|
{
|
|
gId('rover').style.transform = (i.live && s.lor == 0 && i.liveseg<0) ? "translateY(0px)":"translateY(100%)";
|
|
var sour = i.lip ? i.lip:""; if (sour.length > 2) sour = " from " + sour;
|
|
gId('lv').innerHTML = `WLED is receiving live ${i.lm} data${sour}`;
|
|
gId('roverstar').style.display = (i.live && s.lor) ? "block":"none";
|
|
}
|
|
|
|
function cmpP(a, b)
|
|
{
|
|
//WLEDMM: simplify sort to better align with quick load labels (sort first) and ir remotes using id (sort after presets)
|
|
//WLEDMM a[0] = id, a[1] = Object(n, ql)
|
|
//WLEDMM sort first on ql alphabetically then playlists, then id numerical
|
|
|
|
function toString(a) {
|
|
return a[1].ql?String(a[1].ql).padEnd(2,' '):'zz' + (a[1].playlist ? '<' : '=') + String(a[0]).padStart(3, '0');
|
|
}
|
|
|
|
return toString(a).localeCompare(toString(b));
|
|
}
|
|
|
|
function makeWS() {
|
|
if (ws || lastinfo.ws < 0) return;
|
|
ws = new WebSocket((window.location.protocol == "https:"?"wss":"ws")+'://'+(loc?locip:window.location.hostname)+'/ws');
|
|
ws.binaryType = "arraybuffer";
|
|
ws.onmessage = (e)=>{
|
|
if (e.data instanceof ArrayBuffer) return; // liveview packet
|
|
var json = JSON.parse(e.data);
|
|
if (json.leds) return; // JSON liveview packet
|
|
clearTimeout(jsonTimeout);
|
|
jsonTimeout = null;
|
|
lastUpdate = new Date();
|
|
clearErrorToast();
|
|
gId('connind').style.backgroundColor = "var(--c-l)";
|
|
// json object should contain json.info AND json.state (but may not)
|
|
// console.log("makeWS onmessage", json); //WLEDMM Debug
|
|
var i = json.info;
|
|
if (i) {
|
|
parseInfo(i);
|
|
if (isInfo) populateInfo(i);
|
|
} else
|
|
i = lastinfo;
|
|
var s = json.state ? json.state : json;
|
|
displayRover(i, s);
|
|
readState(s);
|
|
console.log("drawSegmentView","websocket", json);
|
|
if (isM) drawSegmentView();
|
|
};
|
|
ws.onclose = (e)=>{
|
|
gId('connind').style.backgroundColor = "var(--c-r)";
|
|
setTimeout(makeWS,1500); // retry WS connection
|
|
ws = null;
|
|
}
|
|
ws.onopen = (e)=>{
|
|
//ws.send("{'v':true}"); // unnecessary (https://github.com/Aircoookie/WLED/blob/master/wled00/ws.cpp#L18)
|
|
reqsLegal = true;
|
|
}
|
|
}
|
|
|
|
function readState(s,command=false)
|
|
{
|
|
if (!s) return false;
|
|
if (s.success) return true; // no data to process
|
|
|
|
isOn = s.on;
|
|
gId('sliderBri').value = s.bri;
|
|
nlA = s.nl.on;
|
|
nlDur = s.nl.dur;
|
|
nlTar = s.nl.tbri;
|
|
nlFade = s.nl.fade;
|
|
syncSend = s.udpn.send;
|
|
if (s.pl<0) currentPreset = s.ps;
|
|
else currentPreset = s.pl;
|
|
|
|
tr = s.transition;
|
|
gId('tt').value = tr/10;
|
|
|
|
populateSegments(s);
|
|
var selc=0;
|
|
var sellvl=0; // 0: selc is invalid, 1: selc is mainseg, 2: selc is first selected
|
|
hasRGB = hasWhite = hasCCT = false;
|
|
segLmax = 0;
|
|
for (let i = 0; i < (s.seg||[]).length; i++)
|
|
{
|
|
if (sellvl == 0 && s.seg[i].id == s.mainseg) {
|
|
selc = i;
|
|
sellvl = 1;
|
|
}
|
|
if (s.seg[i].sel) {
|
|
if (sellvl < 2) selc = i; // get first selected segment
|
|
sellvl = 2;
|
|
var lc = lastinfo.leds.seglc[s.seg[i].id];
|
|
hasRGB |= !!(lc & 0x01);
|
|
hasWhite |= !!(lc & 0x02);
|
|
hasCCT |= !!(lc & 0x04);
|
|
let sLen = (s.seg[i].stop - s.seg[i].start)*(s.seg[i].stopY?(s.seg[i].stopY - s.seg[i].startY):1);
|
|
segLmax = segLmax < sLen ? sLen : segLmax;
|
|
}
|
|
}
|
|
var i=s.seg[selc];
|
|
if (sellvl == 1) {
|
|
var lc = lastinfo.leds.seglc[i.id];
|
|
hasRGB = !!(lc & 0x01);
|
|
hasWhite = !!(lc & 0x02);
|
|
hasCCT = !!(lc & 0x04);
|
|
}
|
|
if (!i) {
|
|
showToast('No Segments!', true);
|
|
updateUI();
|
|
return true;
|
|
}
|
|
|
|
if (s.seg.length>2) d.querySelectorAll(".pop").forEach((e)=>{e.classList.remove("hide");});
|
|
|
|
var cd = gId('csl').children;
|
|
for (let e = cd.length-1; e >= 0; e--) {
|
|
cd[e].dataset.r = i.col[e][0];
|
|
cd[e].dataset.g = i.col[e][1];
|
|
cd[e].dataset.b = i.col[e][2];
|
|
if (hasWhite || (!hasRGB && !hasWhite)) { cd[e].dataset.w = i.col[e][3]; }
|
|
setCSL(cd[e]);
|
|
}
|
|
selectSlot(csel);
|
|
if (i.cct != null && i.cct>=0) gId("sliderA").value = i.cct;
|
|
|
|
gId('sliderSpeed').value = i.sx;
|
|
gId('sliderIntensity').value = i.ix;
|
|
gId('sliderC1').value = i.c1 ? i.c1 : 0;
|
|
gId('sliderC2').value = i.c2 ? i.c2 : 0;
|
|
gId('sliderC3').value = i.c3 ? i.c3 : 0;
|
|
gId('checkO1').checked = !(!i.o1);
|
|
gId('checkO2').checked = !(!i.o2);
|
|
gId('checkO3').checked = !(!i.o3);
|
|
|
|
if (s.error && s.error != 0) {
|
|
var errstr = "";
|
|
switch (s.error) {
|
|
case 10:
|
|
errstr = "Could not mount filesystem!";
|
|
break;
|
|
case 11:
|
|
errstr = "Not enough space to save preset!";
|
|
break;
|
|
case 12:
|
|
errstr = "Preset not found.";
|
|
break;
|
|
case 13:
|
|
errstr = "Missing ir.json.";
|
|
break;
|
|
case 19:
|
|
errstr = "A filesystem error has occured.";
|
|
break;
|
|
}
|
|
showToast('Error ' + s.error + ": " + errstr, true);
|
|
}
|
|
|
|
selectedPal = i.pal;
|
|
selectedFx = i.fx;
|
|
redrawPalPrev(); // if any color changed (random palette did at least)
|
|
updateUI();
|
|
return true;
|
|
}
|
|
|
|
// control HTML elements for Slider and Color Control (original ported form WLED-SR)
|
|
// Technical notes
|
|
// ===============
|
|
// If an effect name is followed by an @, slider and color control is effective.
|
|
// If not effective then:
|
|
// - For AC effects (id<128) 2 sliders and 3 colors and the palette will be shown
|
|
// - For SR effects (id>128) 5 sliders and 3 colors and the palette will be shown
|
|
// If effective (@)
|
|
// - a ; seperates slider controls (left) from color controls (middle) and palette control (right)
|
|
// - if left, middle or right is empty no controls are shown
|
|
// - a , seperates slider controls (max 5) or color controls (max 3). Palette has only one value
|
|
// - a ! means that the default is used.
|
|
// - For sliders: Effect speeds, Effect intensity, Custom 1, Custom 2, Custom 3
|
|
// - For colors: Fx color, Background color, Custom
|
|
// - For palette: prompt for color palette OR palette ID if numeric (will hide palette selection)
|
|
//
|
|
// Note: If palette is on and no colors are specified 1,2 and 3 is shown in each color circle.
|
|
// If a color is specified, the 1,2 or 3 is replaced by that specification.
|
|
// Note: Effects can override default pattern behaviour
|
|
// - FadeToBlack can override the background setting
|
|
// - Defining SEGCOL(<i>) can override a specific palette using these values (e.g. Color Gradient)
|
|
function setEffectParameters(idx)
|
|
{
|
|
if (!(Array.isArray(fxdata) && fxdata.length>idx)) return;
|
|
var controlDefined = fxdata[idx].length;
|
|
var effectPar = fxdata[idx];
|
|
var effectPars = (effectPar == '')?[]:effectPar.split(";");
|
|
var slOnOff = (effectPars.length==0 || effectPars[0]=='')?[]:effectPars[0].split(",");
|
|
var coOnOff = (effectPars.length<2 || effectPars[1]=='')?[]:effectPars[1].split(",");
|
|
var paOnOff = (effectPars.length<3 || effectPars[2]=='')?[]:effectPars[2].split(",");
|
|
|
|
// set html slider items on/off
|
|
let nSliders = 5;
|
|
for (let i=0; i<nSliders; i++) {
|
|
var slider = gId("slider" + i);
|
|
var label = gId("sliderLabel" + i);
|
|
// if (not controlDefined and for AC speed or intensity and for SR all sliders) or slider has a value
|
|
if ((!controlDefined && i < ((idx<128)?2:nSliders)) || (slOnOff.length>i && slOnOff[i] != "")) {
|
|
if (slOnOff.length>i && slOnOff[i]!="!") label.innerHTML = slOnOff[i];
|
|
else if (i==0) label.innerHTML = "Effect speed";
|
|
else if (i==1) label.innerHTML = "Effect intensity";
|
|
else label.innerHTML = "Custom" + (i-1);
|
|
slider.classList.remove('hide');
|
|
} else {
|
|
slider.classList.add('hide');
|
|
}
|
|
}
|
|
if (slOnOff.length>5) { // up to 3 checkboxes
|
|
gId('fxopt').classList.remove('fade');
|
|
for (let i = 0; i<3; i++) {
|
|
if (5+i<slOnOff.length && slOnOff[5+i]!=='') {
|
|
gId('opt'+i).classList.remove('hide');
|
|
gId('optLabel'+i).innerHTML = slOnOff[5+i]=="!" ? 'Option' : slOnOff[5+i].substr(0,16);
|
|
} else
|
|
gId('opt'+i).classList.add('hide');
|
|
}
|
|
} else {
|
|
gId('fxopt').classList.add('fade');
|
|
}
|
|
|
|
// set the bottom position of selected effect (sticky) as the top of sliders div
|
|
setInterval(()=>{
|
|
let top = parseInt(getComputedStyle(gId("sliders")).height);
|
|
top += 5;
|
|
let sel = d.querySelector('#fxlist .selected');
|
|
if (sel) sel.style.bottom = top + "px"; // we will need to remove this when unselected (in setFX())
|
|
},750);
|
|
// set html color items on/off
|
|
var cslLabel = '';
|
|
var sep = '';
|
|
var cslCnt = 0, oCsel = csel;
|
|
for (let i=0; i<gId("csl").children.length; i++) {
|
|
var btn = gId("csl" + i);
|
|
// if no controlDefined or coOnOff has a value
|
|
if (coOnOff.length>i && coOnOff[i] != "") {
|
|
btn.classList.remove('hide');
|
|
btn.dataset.hide = 0;
|
|
if (coOnOff[i] != "!") {
|
|
var abbreviation = coOnOff[i].substr(0,2);
|
|
btn.innerHTML = abbreviation;
|
|
if (abbreviation != coOnOff[i]) {
|
|
cslLabel += sep + abbreviation + '=' + coOnOff[i];
|
|
sep = ', ';
|
|
}
|
|
}
|
|
else if (i==0) btn.innerHTML = "Fx";
|
|
else if (i==1) btn.innerHTML = "Bg";
|
|
else btn.innerHTML = "Cs";
|
|
if (!cslCnt || oCsel==i) selectSlot(i); // select 1st displayed slot or old one
|
|
cslCnt++;
|
|
} else if (!controlDefined) { // if no controls then all buttons should be shown for color 1..3
|
|
btn.classList.remove('hide');
|
|
btn.dataset.hide = 0;
|
|
btn.innerHTML = `${i+1}`;
|
|
if (!cslCnt || oCsel==i) selectSlot(i); // select 1st displayed slot or old one
|
|
cslCnt++;
|
|
} else {
|
|
btn.classList.add('hide');
|
|
btn.dataset.hide = 1;
|
|
btn.innerHTML = `${i+1}`; // name hidden buttons 1..3 for * palettes
|
|
}
|
|
}
|
|
gId("cslLabel").innerHTML = cslLabel;
|
|
|
|
// set palette on/off
|
|
var palw = gId("palw"); // wrapper
|
|
var pall = gId("pall"); // label
|
|
// if not controlDefined or palette has a value
|
|
if (hasRGB && ((!controlDefined) || (paOnOff.length>0 && paOnOff[0]!="" && isNaN(paOnOff[0])))) {
|
|
palw.style.display = "inline-block";
|
|
if (paOnOff.length>0 && paOnOff[0].indexOf("=")>0) {
|
|
// embeded default values
|
|
var dPos = paOnOff[0].indexOf("=");
|
|
var v = Math.max(0,Math.min(255,parseInt(paOnOff[0].substr(dPos+1))));
|
|
paOnOff[0] = paOnOff[0].substring(0,dPos);
|
|
}
|
|
if (paOnOff.length>0 && paOnOff[0] != "!") pall.innerHTML = paOnOff[0];
|
|
else pall.innerHTML = '<i class="icons sel-icon" onclick="tglHex()"></i> Color palette';
|
|
} else {
|
|
// disable palette list
|
|
pall.innerHTML = '<i class="icons sel-icon" onclick="tglHex()"></i> Color palette not used';
|
|
palw.style.display = "none";
|
|
}
|
|
// not all color selectors shown, hide palettes created from color selectors
|
|
// NOTE: this will disallow user to select "* Color ..." palettes which may be undesirable in some cases or for some users
|
|
//for (let e of (gId('pallist').querySelectorAll('.lstI')||[])) {
|
|
// let fltr = "* C";
|
|
// if (cslCnt==1 && csel==0) fltr = "* Colors";
|
|
// else if (cslCnt==2) fltr = "* Colors Only";
|
|
// if (cslCnt < 3 && e.querySelector('.lstIname').innerText.indexOf(fltr)>=0) e.classList.add('hide'); else e.classList.remove('hide');
|
|
//}
|
|
}
|
|
|
|
var jsonTimeout;
|
|
var reqsLegal = false;
|
|
|
|
function requestJson(command=null)
|
|
{
|
|
gId('connind').style.backgroundColor = "var(--c-y)";
|
|
if (command && !reqsLegal) return; // stop post requests from chrome onchange event on page restore
|
|
if (!jsonTimeout) jsonTimeout = setTimeout(()=>{if (ws) ws.close(); ws=null; showErrorToast()}, 3000);
|
|
var req = null;
|
|
var url = (loc?`http://${locip}`:'') + '/json/si';
|
|
var useWs = (ws && ws.readyState === WebSocket.OPEN);
|
|
var type = command ? 'post':'get';
|
|
if (command) {
|
|
command.v = true; // force complete /json/si API response
|
|
command.time = Math.floor(Date.now() / 1000);
|
|
var t = gId('tt');
|
|
if (t.validity.valid && command.transition==null) {
|
|
var tn = parseInt(t.value*10);
|
|
if (tn != tr) command.transition = tn;
|
|
}
|
|
req = JSON.stringify(command);
|
|
if (req.length > 1340) useWs = false; // do not send very long requests over websocket
|
|
if (req.length > 500 && lastinfo && lastinfo.arch == "esp8266") useWs = false; // esp8266 can only handle 500 bytes
|
|
};
|
|
|
|
if (useWs) {
|
|
// console.log("requestJson ws.send", command); //WLEDMM Debug
|
|
ws.send(req?req:'{"v":true}');
|
|
return;
|
|
}
|
|
|
|
// console.log("requestJson url fetch", url, type); //WLEDMM Debug
|
|
fetch(url, {
|
|
method: type,
|
|
headers: {
|
|
"Content-type": "application/json; charset=UTF-8"
|
|
},
|
|
body: req
|
|
})
|
|
.then(res => {
|
|
clearTimeout(jsonTimeout);
|
|
jsonTimeout = null;
|
|
if (!res.ok) showErrorToast();
|
|
return res.json();
|
|
})
|
|
.then(json => {
|
|
lastUpdate = new Date();
|
|
clearErrorToast(3000);
|
|
gId('connind').style.backgroundColor = "var(--c-g)";
|
|
if (!json) { showToast('Empty response', true); return; }
|
|
if (json.success) return;
|
|
// console.log("requestJson url return", json); //WLEDMM Debug
|
|
if (json.info) {
|
|
let i = json.info;
|
|
parseInfo(i);
|
|
populatePalettes(i);
|
|
if (isInfo) populateInfo(i);
|
|
}
|
|
var s = json.state ? json.state : json;
|
|
readState(s);
|
|
|
|
//WLEDMM init, gfx default on upon web page load
|
|
if (isM) {
|
|
// console.log("drawSegmentView","requestjson");
|
|
// drawSegmentView();
|
|
toggleLiveview();
|
|
}
|
|
|
|
//load presets and open websocket sequentially
|
|
if (!pJson || isEmpty(pJson)) setTimeout(()=>{
|
|
loadPresets(()=>{
|
|
if (!(ws && ws.readyState === WebSocket.OPEN)) makeWS();
|
|
});
|
|
},25);
|
|
reqsLegal = true;
|
|
})
|
|
.catch((e)=>{
|
|
showToast(e, true);
|
|
});
|
|
}
|
|
|
|
function togglePower()
|
|
{
|
|
isOn = !isOn;
|
|
var obj = {"on": isOn};
|
|
if (isOn && lastinfo && lastinfo.live && lastinfo.liveseg>=0) {
|
|
obj.live = false;
|
|
obj.seg = [];
|
|
obj.seg[0] = {"id": lastinfo.liveseg, "frz": false};
|
|
}
|
|
requestJson(obj);
|
|
}
|
|
|
|
function toggleNl()
|
|
{
|
|
nlA = !nlA;
|
|
if (nlA)
|
|
{
|
|
showToast(`Timer active. Your light will turn ${nlTar > 0 ? "on":"off"} ${nlMode ? "over":"after"} ${nlDur} minutes.`);
|
|
} else {
|
|
showToast('Timer deactivated.');
|
|
}
|
|
var obj = {"nl": {"on": nlA}};
|
|
requestJson(obj);
|
|
}
|
|
|
|
function toggleSync()
|
|
{
|
|
syncSend = !syncSend;
|
|
if (syncSend) showToast('Other lights in the network will now sync to this one.');
|
|
else showToast('This light and other lights in the network will no longer sync.');
|
|
var obj = {"udpn": {"send": syncSend}};
|
|
if (syncTglRecv) obj.udpn.recv = syncSend;
|
|
requestJson(obj);
|
|
}
|
|
|
|
function toggleLiveview()
|
|
{
|
|
if (isM) {
|
|
//WLEDMM adding liveview2D support on main ui
|
|
isLv = !isLv;
|
|
gId("colorGFX").style.display = isLv? "inline":"none"; //WLEDMM: set off if explicitly gfx pushed
|
|
gId("effectGFX").style.display = isLv? "inline":"none";
|
|
gId("segGFX").style.display = isLv? "inline":"none";
|
|
|
|
canvasPeek = gId("canvasPeek");
|
|
if (isLv) peek(canvasPeek); //W
|
|
} else {
|
|
//WLEDMM remove liveview2D support here
|
|
if (isInfo && isM) toggleInfo();
|
|
if (isNodes && isM) toggleNodes();
|
|
isLv = !isLv;
|
|
|
|
var lvID = "liveview";
|
|
|
|
gId(lvID).style.display = (isLv) ? "block":"none";
|
|
var url = (loc?`http://${locip}`:'') + "/" + lvID;
|
|
gId(lvID).src = (isLv) ? url:"about:blank";
|
|
size();
|
|
}
|
|
|
|
gId('buttonSr').className = (isLv) ? "active":"";
|
|
if (ws && ws.readyState === WebSocket.OPEN) ws.send(`{"lv":${isLv}}`);
|
|
}
|
|
|
|
//WLEDMM create and delete iFrame for peek (isLv is true if create)
|
|
function bigPeek(doCreate)
|
|
{
|
|
let lvID = "liveview2D"
|
|
if (doCreate) {
|
|
var cn = '<iframe id="liveview2D" src="about:blank" onload="this.contentWindow.document.body.onclick=function(){bigPeek(false);}"></iframe>';
|
|
gId('kliveview2D').innerHTML = cn;
|
|
}
|
|
|
|
gId('mliveview2D').style.transform = (doCreate) ? "translateY(0px)":"translateY(100%)";
|
|
|
|
gId(lvID).style.display = (doCreate) ? "block":"none";
|
|
var url = (loc?`http://${locip}`:'') + "/" + lvID;
|
|
gId(lvID).src = (doCreate) ? url:"about:blank";
|
|
}
|
|
|
|
function toggleInfo()
|
|
{
|
|
if (isNodes) toggleNodes();
|
|
// if (isLv && isM) toggleLiveview(); //WLEDMM: not for GFX
|
|
isInfo = !isInfo;
|
|
if (isInfo) requestJson();
|
|
gId('info').style.transform = (isInfo) ? "translateY(0px)":"translateY(100%)";
|
|
gId('buttonI').className = (isInfo) ? "active":"";
|
|
}
|
|
|
|
function toggleNodes()
|
|
{
|
|
if (isInfo) toggleInfo();
|
|
// if (isLv && isM) toggleLiveview(); //WLEDMM: not for GFX
|
|
isNodes = !isNodes;
|
|
if (isNodes) loadNodes();
|
|
gId('nodes').style.transform = (isNodes) ? "translateY(0px)":"translateY(100%)";
|
|
gId('buttonNodes').className = (isNodes) ? "active":"";
|
|
}
|
|
|
|
function makeSeg()
|
|
{
|
|
var ns = 0, ct = 0;
|
|
var lu = lowestUnused;
|
|
let li = lastinfo;
|
|
if (lu > 0) {
|
|
let xend = parseInt(gId(`seg${lu -1}e`).value,10) + (cfg.comp.seglen?parseInt(gId(`seg${lu -1}s`).value,10):0);
|
|
if (isM) {
|
|
ns = 0;
|
|
ct = mw;
|
|
} else {
|
|
if (xend < ledCount) ns = xend;
|
|
ct = ledCount-(cfg.comp.seglen?ns:0)
|
|
}
|
|
}
|
|
gId('segutil').scrollIntoView({
|
|
behavior: 'smooth',
|
|
block: 'start',
|
|
});
|
|
var cn = `<div class="seg lstI expanded">`+
|
|
`<div class="segin">`+
|
|
`<input type="text" id="seg${lu}t" autocomplete="off" maxlength=32 value="" placeholder="New segment ${lu}"/>`+
|
|
`<table class="segt">`+
|
|
`<tr>`+
|
|
`<td width="38%">${isM?'Start X':'Start LED'}</td>`+
|
|
`<td width="38%">${isM?(cfg.comp.seglen?"Width":"Stop X"):(cfg.comp.seglen?"LED count":"Stop LED")}</td>`+
|
|
`</tr>`+
|
|
`<tr>`+
|
|
`<td><input class="segn" id="seg${lu}s" type="number" min="0" max="${isM?mw-1:ledCount-1}" value="${ns}" oninput="updateLen(${lu})" onkeydown="segEnter(${lu})"></td>`+
|
|
`<td><input class="segn" id="seg${lu}e" type="number" min="0" max="${ct}" value="${ct}" oninput="updateLen(${lu})" onkeydown="segEnter(${lu})"></td>`+
|
|
`<td><button class="btn btn-xs" onclick="setSeg(${lu});"><i class="icons bth-icon" id="segc${lu}"></i></button></td>`+
|
|
`</tr>`+
|
|
`<tr id="mkSYH" class="${isM?"":"hide"}"><td>Start Y</td><td>${cfg.comp.seglen?'Height':'Stop Y'}</td></tr>`+
|
|
`<tr id="mkSYD" class="${isM?"":"hide"}">`+
|
|
`<td><input class="segn" id="seg${lu}sY" type="number" min="0" max="${mh-1}" value="0" oninput="updateLen(${lu})" onkeydown="segEnter(${lu})"></td>`+
|
|
`<td><input class="segn" id="seg${lu}eY" type="number" min="0" max="${mh}" value="${isM?mh:1}" oninput="updateLen(${lu})" onkeydown="segEnter(${lu})"></td>`+
|
|
`</tr>`+
|
|
`</table>`+
|
|
`<div class="h" id="seg${lu}len">${ledCount - ns} LEDs</div>`+
|
|
`<div class="c"><button class="btn btn-p" onclick="resetUtil()">Cancel</button></div>`+
|
|
`</div>`+
|
|
`</div>`;
|
|
gId('segutil').innerHTML = cn;
|
|
}
|
|
|
|
function resetUtil(off=false)
|
|
{
|
|
gId('segutil').innerHTML = `<div class="seg btn btn-s${off?' off':''}" style="padding:0;">`
|
|
+ '<label class="check schkl"><input type="checkbox" id="selall" onchange="selSegAll(this)"><span class="checkmark"></span></label>'
|
|
+ `<div class="segname" ${off?'':'onclick="makeSeg()"'}><i class="icons btn-icon"></i>Add segment</div>`
|
|
+ '<div class="pop hide" onclick="event.stopPropagation();">'
|
|
+ `<i class="icons g-icon" onclick="this.nextElementSibling.classList.toggle('hide');"></i>`
|
|
+ '<div class="pop-c hide"><span style="color:var(--c-f);" onclick="selGrp(0);">➊</span><span style="color:var(--c-r);" onclick="selGrp(1);">➋</span><span style="color:var(--c-g);" onclick="selGrp(2);">➌</span><span style="color:var(--c-l);" onclick="selGrp(3);">➍</span></div>'
|
|
+ '</div></div>';
|
|
}
|
|
|
|
function makePlSel(el, incPl=false)
|
|
{
|
|
var plSelContent = "";
|
|
delete pJson["0"]; // remove filler preset
|
|
var arr = Object.entries(pJson);
|
|
for (var a of arr) {
|
|
var n = a[1].n ? a[1].n : "Preset " + a[0];
|
|
if (!incPl && a[1].playlist && a[1].playlist.ps) continue; // remove playlists, sub-playlists not yet supported
|
|
plSelContent += `<option value="${a[0]}" ${a[0]==el?"selected":""}>${n}</option>`
|
|
}
|
|
return plSelContent;
|
|
}
|
|
|
|
function refreshPlE(p)
|
|
{
|
|
var plEDiv = gId(`ple${p}`);
|
|
if (!plEDiv) return;
|
|
var content = "<div class=\"first c\">Playlist entries</div>";
|
|
for (var i = 0; i < plJson[p].ps.length; i++) {
|
|
content += makePlEntry(p,i);
|
|
}
|
|
content += `<div class="hrz"></div>`;
|
|
plEDiv.innerHTML = content;
|
|
var dels = plEDiv.getElementsByClassName("btn-pl-del");
|
|
if (dels.length < 2) dels[0].style.display = "none";
|
|
|
|
var sels = gId(`seg${p+100}`).getElementsByClassName("sel");
|
|
for (var i of sels) {
|
|
if (i.dataset.val) {
|
|
if (parseInt(i.dataset.val) > 0) i.value = i.dataset.val;
|
|
else plJson[p].ps[i.dataset.index] = parseInt(i.value);
|
|
}
|
|
}
|
|
}
|
|
|
|
// p: preset ID, i: ps index
|
|
function addPl(p,i)
|
|
{
|
|
plJson[p].ps.splice(i+1,0,0);
|
|
plJson[p].dur.splice(i+1,0,plJson[p].dur[i]);
|
|
plJson[p].transition.splice(i+1,0,plJson[p].transition[i]);
|
|
refreshPlE(p);
|
|
}
|
|
|
|
function delPl(p,i)
|
|
{
|
|
if (plJson[p].ps.length < 2) return;
|
|
plJson[p].ps.splice(i,1);
|
|
plJson[p].dur.splice(i,1);
|
|
plJson[p].transition.splice(i,1);
|
|
refreshPlE(p);
|
|
}
|
|
|
|
function plePs(p,i,field)
|
|
{
|
|
plJson[p].ps[i] = parseInt(field.value);
|
|
}
|
|
|
|
function pleDur(p,i,field)
|
|
{
|
|
if (field.validity.valid)
|
|
plJson[p].dur[i] = Math.floor(field.value*10);
|
|
}
|
|
|
|
function pleTr(p,i,field)
|
|
{
|
|
if (field.validity.valid)
|
|
plJson[p].transition[i] = Math.floor(field.value*10);
|
|
}
|
|
|
|
function plR(p)
|
|
{
|
|
var pl = plJson[p];
|
|
pl.r = gId(`pl${p}rtgl`).checked;
|
|
if (gId(`pl${p}rptgl`).checked) { // infinite
|
|
pl.repeat = 0;
|
|
delete pl.end;
|
|
gId(`pl${p}o1`).style.display = "none";
|
|
} else {
|
|
pl.repeat = parseInt(gId(`pl${p}rp`).value);
|
|
pl.end = parseInt(gId(`pl${p}selEnd`).value);
|
|
gId(`pl${p}o1`).style.display = "block";
|
|
}
|
|
}
|
|
|
|
function makeP(i,pl)
|
|
{
|
|
var content = "";
|
|
if (pl) {
|
|
if (i===0) plJson[0] = {
|
|
ps: [1],
|
|
dur: [100],
|
|
transition: [tr],
|
|
repeat: 0,
|
|
r: false,
|
|
end: 0
|
|
};
|
|
var rep = plJson[i].repeat ? plJson[i].repeat : 0;
|
|
content =
|
|
`<div id="ple${i}" style="margin-top:10px;"></div><label class="check revchkl">Shuffle
|
|
<input type="checkbox" id="pl${i}rtgl" onchange="plR(${i})" ${plJson[i].r||rep<0?"checked":""}>
|
|
<span class="checkmark"></span>
|
|
</label>
|
|
<label class="check revchkl">Repeat indefinitely
|
|
<input type="checkbox" id="pl${i}rptgl" onchange="plR(${i})" ${rep>0?"":"checked"}>
|
|
<span class="checkmark"></span>
|
|
</label>
|
|
<div id="pl${i}o1" style="display:${rep>0?"block":"none"}">
|
|
<div class="c">Repeat <input type="number" id="pl${i}rp" oninput="plR(${i})" max=127 min=0 value=${rep>0?rep:1}> times</div>
|
|
<div class="sel">End preset:<br>
|
|
<div class="sel-p"><select class="sel-ple" id="pl${i}selEnd" onchange="plR(${i})" data-val=${plJson[i].end?plJson[i].end:0}>
|
|
<option value="0">None</option>
|
|
<option value="255">Restore preset</option>
|
|
${makePlSel(plJson[i].end?plJson[i].end:0, true)}
|
|
</select></div></div>
|
|
</div>
|
|
<div class="c"><button class="btn btn-p" onclick="testPl(${i}, this)"><i class='icons btn-icon'></i>Test</button></div>`;
|
|
} else {
|
|
content =
|
|
`<label class="check revchkl">
|
|
<span class="lstIname">
|
|
Include brightness
|
|
</span>
|
|
<input type="checkbox" id="p${i}ibtgl" checked>
|
|
<span class="checkmark"></span>
|
|
</label>
|
|
<label class="check revchkl">
|
|
<span class="lstIname">
|
|
Save segment bounds
|
|
</span>
|
|
<input type="checkbox" id="p${i}sbtgl" checked>
|
|
<span class="checkmark"></span>
|
|
</label>
|
|
<label class="check revchkl">
|
|
<span class="lstIname">
|
|
Checked segments only
|
|
</span>
|
|
<input type="checkbox" id="p${i}sbchk">
|
|
<span class="checkmark"></span>
|
|
</label>`;
|
|
if (Array.isArray(lastinfo.maps) && lastinfo.maps.length>0) { //WLEDMM >0 instead of 1 to show also first ledmap. Attention: WLED AC has isM check, in MM Matrices are supported so do not check on isM
|
|
content += `<div class="lbl-l">Ledmap: <div class="sel-p"><select class="sel-p" id="p${i}lmp"><option value="">Unchanged</option>`;
|
|
for (const k of (lastinfo.maps||[])) content += `<option value="${k.id}"${(i>0 && pJson[i].ledmap==k.id)?" selected":""}>${k.id==0?'Default':(k.id<10?'ledmap'+k.id+'.json':ledmapFileNames[k.id-10])}</option>`;
|
|
content += "</select></div></div>";
|
|
}
|
|
}
|
|
|
|
return `<input type="text" class="ptxt ${i==0?'show':''}" id="p${i}txt" autocomplete="off" maxlength=32 value="${(i>0)?pName(i):""}" placeholder="Enter name..."/>
|
|
<div class="c">Quick load label: <input type="text" class="stxt" maxlength=2 value="${qlName(i)}" id="p${i}ql" autocomplete="off"/></div>
|
|
<div class="h">(leave empty for no Quick load button)</div>
|
|
<div ${pl&&i==0?"style='display:none'":""}>
|
|
<label class="check revchkl">
|
|
<span class="lstIname">
|
|
${pl?"Show playlist editor":(i>0)?"Overwrite with state":"Use current state"}
|
|
</span>
|
|
<input type="checkbox" id="p${i}cstgl" onchange="tglCs(${i})" ${(i==0||pl)?"checked":""}>
|
|
<span class="checkmark"></span>
|
|
</label>
|
|
</div>
|
|
<div class="po2" id="p${i}o2">API command<br><textarea class="apitxt" id="p${i}api"></textarea></div>
|
|
<div class="po1" id="p${i}o1">${content}</div>
|
|
<div class="c m6">Save to ID <input id="p${i}id" type="number" oninput="checkUsed(${i})" max=250 min=1 value=${(i>0)?i:getLowestUnusedP()}></div>
|
|
<div class="c">
|
|
<button class="btn btn-p" onclick="saveP(${i},${pl})"><i class="icons btn-icon"></i>Save</button>
|
|
${(i>0)?'<button class="btn btn-p" id="p'+i+'del" onclick="delP('+i+')"><i class="icons btn-icon"></i>Delete':'<button class="btn btn-p" onclick="resetPUtil()">Cancel'}</button>
|
|
</div>
|
|
<div class="pwarn ${(i>0)?"bp":""} c" id="p${i}warn"></div>
|
|
${(i>0)? ('<div class="h">ID ' +i+ '</div>'):""}`;
|
|
}
|
|
|
|
function makePUtil()
|
|
{
|
|
let p = gId('putil');
|
|
p.classList.remove('staybot');
|
|
p.classList.add('pres');
|
|
p.innerHTML = `<div class="presin expanded">${makeP(0)}</div>`;
|
|
let pTx = gId('p0txt');
|
|
pTx.focus();
|
|
pTx.value = eJson.find((o)=>{return o.id==selectedFx}).name;
|
|
pTx.select();
|
|
p.scrollIntoView({
|
|
behavior: 'smooth',
|
|
block: 'center'
|
|
});
|
|
gId('psFind').classList.remove('staytop');
|
|
}
|
|
|
|
function makePlEntry(p,i)
|
|
{
|
|
return `<div class="plentry">
|
|
<div class="hrz"></div>
|
|
<table>
|
|
<tr>
|
|
<td width="80%" colspan=2>
|
|
<div class="sel-p"><select class="sel-pl" onchange="plePs(${p},${i},this)" data-val="${plJson[p].ps[i]}" data-index="${i}">
|
|
${makePlSel(plJson[p].ps[i])}
|
|
</select></div>
|
|
</td>
|
|
<td class="c"><button class="btn btn-pl-add" onclick="addPl(${p},${i})"><i class="icons btn-icon"></i></button></td>
|
|
</tr>
|
|
<tr>
|
|
<td class="c">Duration</td>
|
|
<td class="c">Transition</td>
|
|
<td class="c">#${i+1}</td>
|
|
</tr>
|
|
<tr>
|
|
<td class="c" width="40%"><input class="segn" type="number" placeholder="Duration" max=6553.0 min=0.2 step=0.1 oninput="pleDur(${p},${i},this)" value="${plJson[p].dur[i]/10.0}">s</td>
|
|
<td class="c" width="40%"><input class="segn" type="number" placeholder="Transition" max=65.0 min=0.0 step=0.1 oninput="pleTr(${p},${i},this)" value="${plJson[p].transition[i]/10.0}">s</td>
|
|
<td class="c"><button class="btn btn-pl-del" onclick="delPl(${p},${i})"><i class="icons btn-icon"></i></button></div></td>
|
|
</tr>
|
|
</table>
|
|
</div>`;
|
|
}
|
|
|
|
function makePlUtil()
|
|
{
|
|
if (pNum < 2) {
|
|
showToast("You need at least 2 presets to make a playlist!"); //return;
|
|
}
|
|
let p = gId('putil');
|
|
p.classList.remove('staybot');
|
|
p.classList.add('pres');
|
|
p.innerHTML = `<div class="presin expanded" id="seg100">${makeP(0,true)}</div></div>`;
|
|
refreshPlE(0);
|
|
gId('p0txt').focus();
|
|
p.scrollIntoView({
|
|
behavior: 'smooth',
|
|
block: 'center'
|
|
});
|
|
gId('psFind').classList.remove('staytop');
|
|
}
|
|
|
|
function resetPUtil()
|
|
{
|
|
gId('psFind').classList.add('staytop');
|
|
let p = gId('putil');
|
|
p.classList.add('staybot');
|
|
p.classList.remove('pres');
|
|
p.innerHTML = `<button class="btn btn-s" onclick="makePUtil()" style="float:left;"><i class="icons btn-icon"></i>Preset</button>`
|
|
+ `<button class="btn btn-s" onclick="makePlUtil()" style="float:right;"><i class="icons btn-icon"></i>Playlist</button>`;
|
|
}
|
|
|
|
function tglCs(i)
|
|
{
|
|
var pss = gId(`p${i}cstgl`).checked;
|
|
gId(`p${i}o1`).style.display = pss? "block" : "none";
|
|
gId(`p${i}o2`).style.display = !pss? "block" : "none";
|
|
}
|
|
|
|
function tglSegn(s)
|
|
{
|
|
let t = gId(s<100?`seg${s}t`:`p${s-100}txt`);
|
|
if (t) {
|
|
t.classList.toggle('show');
|
|
t.focus();
|
|
t.select();
|
|
}
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
}
|
|
|
|
function selSegAll(o)
|
|
{
|
|
var obj = {"seg":[]};
|
|
for (let i=0; i<=lSeg; i++) obj.seg.push({"id":i,"sel":o.checked});
|
|
requestJson(obj);
|
|
}
|
|
|
|
function selSegEx(s)
|
|
{
|
|
var obj = {"seg":[]};
|
|
for (let i=0; i<=lSeg; i++) obj.seg.push({"id":i,"sel":(i==s)});
|
|
obj.mainseg = s;
|
|
requestJson(obj);
|
|
}
|
|
|
|
function selSeg(s)
|
|
{
|
|
var sel = gId(`seg${s}sel`).checked;
|
|
var obj = {"seg": {"id": s, "sel": sel}};
|
|
requestJson(obj);
|
|
}
|
|
|
|
function selGrp(g)
|
|
{
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
var sel = gId(`segcont`).querySelectorAll(`div[data-set="${g}"]`);
|
|
var obj = {"seg":[]};
|
|
for (let i=0; i<=lSeg; i++) obj.seg.push({"id":i,"sel":false});
|
|
if (sel) for (let s of sel||[]) {
|
|
let i = parseInt(s.id.substring(3));
|
|
obj.seg[i] = {"id":i,"sel":true};
|
|
}
|
|
if (obj.seg.length) requestJson(obj);
|
|
}
|
|
|
|
function rptSeg(s)
|
|
{
|
|
//TODO: 2D support
|
|
var name = gId(`seg${s}t`).value;
|
|
var start = parseInt(gId(`seg${s}s`).value);
|
|
var stop = parseInt(gId(`seg${s}e`).value);
|
|
if (stop == 0) {return;}
|
|
var rev = gId(`seg${s}rev`).checked;
|
|
var mi = gId(`seg${s}mi`).checked;
|
|
var sel = gId(`seg${s}sel`).checked;
|
|
var pwr = gId(`seg${s}pwr`).classList.contains('act');
|
|
var obj = {"seg": {"id": s, "n": name, "start": start, "stop": (cfg.comp.seglen?start:0)+stop, "rev": rev, "mi": mi, "on": pwr, "bri": parseInt(gId(`seg${s}bri`).value), "sel": sel}};
|
|
if (gId(`seg${s}grp`)) {
|
|
var grp = parseInt(gId(`seg${s}grp`).value);
|
|
var spc = parseInt(gId(`seg${s}spc`).value);
|
|
var ofs = parseInt(gId(`seg${s}of` ).value);
|
|
obj.seg.grp = grp;
|
|
obj.seg.spc = spc;
|
|
obj.seg.of = ofs;
|
|
}
|
|
obj.seg.rpt = true;
|
|
expand(s);
|
|
requestJson(obj);
|
|
}
|
|
|
|
function setSeg(s)
|
|
{
|
|
var name = gId(`seg${s}t`).value;
|
|
let sX = gId(`seg${s}s`);
|
|
let eX = gId(`seg${s}e`);
|
|
var start = parseInt(sX.value);
|
|
var stop = parseInt(eX.value) + (cfg.comp.seglen?start:0);
|
|
if (start<sX.min || start>sX.max) {sX.value=sX.min; return;} // prevent out of bounds
|
|
if (stop<eX.min || stop-(cfg.comp.seglen?start:0)>eX.max) {eX.value=eX.max; return;} // prevent out of bounds
|
|
if ((cfg.comp.seglen && stop == 0) || (!cfg.comp.seglen && stop <= start)) {delSeg(s); return;}
|
|
var obj = {"seg": {"id": s, "n": name, "start": start, "stop": stop}};
|
|
if (isM && start<mw*mh) {
|
|
let sY = gId(`seg${s}sY`);
|
|
let eY = gId(`seg${s}eY`);
|
|
var startY = parseInt(sY.value);
|
|
var stopY = parseInt(eY.value) + (cfg.comp.seglen?startY:0);
|
|
if (startY<sY.min || startY>sY.max) {sY.value=sY.min; return;} // prevent out of bounds
|
|
if (stopY<eY.min || stopY>eY.max) {eY.value=eY.max; return;} // prevent out of bounds
|
|
obj.seg.startY = startY;
|
|
obj.seg.stopY = stopY;
|
|
}
|
|
let g = gId(`seg${s}grp`);
|
|
if (g) { // advanced options, not present in new segment dialog (makeSeg())
|
|
let grp = parseInt(g.value);
|
|
let spc = parseInt(gId(`seg${s}spc`).value);
|
|
let ofs = parseInt(gId(`seg${s}of` ).value);
|
|
obj.seg.grp = grp;
|
|
obj.seg.spc = spc;
|
|
obj.seg.of = ofs;
|
|
if (isM && gId(`seg${s}tp`)) obj.seg.tp = gId(`seg${s}tp`).checked;
|
|
}
|
|
resetUtil(); // close add segment dialog just in case
|
|
requestJson(obj);
|
|
}
|
|
|
|
function delSeg(s)
|
|
{
|
|
if (segCount < 2) {
|
|
showToast("You need to have multiple segments to delete one!");
|
|
return;
|
|
}
|
|
segCount--;
|
|
var obj = {"seg": {"id": s, "stop": 0}};
|
|
requestJson(obj);
|
|
}
|
|
|
|
function setRev(s)
|
|
{
|
|
var rev = gId(`seg${s}rev`).checked;
|
|
var obj = {"seg": {"id": s, "rev": rev}};
|
|
requestJson(obj);
|
|
}
|
|
|
|
function setRevY(s)
|
|
{
|
|
var rev = gId(`seg${s}rY`).checked;
|
|
var obj = {"seg": {"id": s, "rY": rev}};
|
|
requestJson(obj);
|
|
}
|
|
|
|
function setMi(s)
|
|
{
|
|
var mi = gId(`seg${s}mi`).checked;
|
|
var obj = {"seg": {"id": s, "mi": mi}};
|
|
requestJson(obj);
|
|
}
|
|
|
|
function setMiY(s)
|
|
{
|
|
var mi = gId(`seg${s}mY`).checked;
|
|
var obj = {"seg": {"id": s, "mY": mi}};
|
|
requestJson(obj);
|
|
}
|
|
|
|
function setM12(s)
|
|
{
|
|
var value = gId(`seg${s}m12`).selectedIndex;
|
|
var obj = {"seg": {"id": s, "m12": value}};
|
|
requestJson(obj);
|
|
}
|
|
|
|
function setSi(s)
|
|
{
|
|
var value = gId(`seg${s}si`).selectedIndex;
|
|
var obj = {"seg": {"id": s, "si": value}};
|
|
requestJson(obj);
|
|
}
|
|
|
|
function setTp(s)
|
|
{
|
|
var tp = gId(`seg${s}tp`).checked;
|
|
var obj = {"seg": {"id": s, "tp": tp}};
|
|
requestJson(obj);
|
|
}
|
|
|
|
function setGrp(s, g)
|
|
{
|
|
event.preventDefault();
|
|
event.stopPropagation();
|
|
var obj = {"seg": {"id": s, "set": g}};
|
|
requestJson(obj);
|
|
}
|
|
|
|
function setSegPwr(s)
|
|
{
|
|
var pwr = gId(`seg${s}pwr`).classList.contains('act');
|
|
var obj = {"seg": {"id": s, "on": !pwr}};
|
|
requestJson(obj);
|
|
}
|
|
|
|
function setSegBri(s)
|
|
{
|
|
var obj = {"seg": {"id": s, "bri": parseInt(gId(`seg${s}bri`).value)}};
|
|
requestJson(obj);
|
|
}
|
|
|
|
function tglFreeze(s=null)
|
|
{
|
|
var obj = {"seg": {"frz": "t"}}; // toggle
|
|
if (s!==null) {
|
|
obj.seg.id = s;
|
|
// if live segment, enter live override (which also unfreezes)
|
|
if (lastinfo && s==lastinfo.liveseg && lastinfo.live) obj = {"lor":1};
|
|
}
|
|
requestJson(obj);
|
|
}
|
|
|
|
function setFX(ind = null)
|
|
{
|
|
if (ind === null) {
|
|
ind = parseInt(d.querySelector('#fxlist input[name="fx"]:checked').value);
|
|
} else {
|
|
d.querySelector(`#fxlist input[name="fx"][value="${ind}"]`).checked = true;
|
|
}
|
|
var obj = {"seg": {"fx": parseInt(ind), "fxdef": cfg.comp.fxdef}}; // fxdef sets effect parameters to default values
|
|
requestJson(obj);
|
|
}
|
|
|
|
function setPalette(paletteId = null)
|
|
{
|
|
if (paletteId === null) {
|
|
paletteId = parseInt(d.querySelector('#pallist input[name="palette"]:checked').value);
|
|
} else {
|
|
d.querySelector(`#pallist input[name="palette"][value="${paletteId}"]`).checked = true;
|
|
}
|
|
|
|
var obj = {"seg": {"pal": paletteId}};
|
|
requestJson(obj);
|
|
}
|
|
|
|
function setBri()
|
|
{
|
|
var obj = {"bri": parseInt(gId('sliderBri').value)};
|
|
requestJson(obj);
|
|
}
|
|
|
|
function setSpeed()
|
|
{
|
|
var obj = {"seg": {"sx": parseInt(gId('sliderSpeed').value)}};
|
|
requestJson(obj);
|
|
}
|
|
|
|
function setIntensity()
|
|
{
|
|
var obj = {"seg": {"ix": parseInt(gId('sliderIntensity').value)}};
|
|
requestJson(obj);
|
|
}
|
|
|
|
function setCustom(i=1)
|
|
{
|
|
if (i<1 || i>3) return;
|
|
var obj = {"seg": {}};
|
|
var val = parseInt(gId(`sliderC${i}`).value);
|
|
if (i===3) obj.seg.c3 = val;
|
|
else if (i===2) obj.seg.c2 = val;
|
|
else obj.seg.c1 = val;
|
|
requestJson(obj);
|
|
}
|
|
|
|
function setOption(i=1, v=false)
|
|
{
|
|
if (i<1 || i>3) return;
|
|
var obj = {"seg": {}};
|
|
if (i===3) obj.seg.o3 = !(!v); //make sure it is bool
|
|
else if (i===2) obj.seg.o2 = !(!v); //make sure it is bool
|
|
else obj.seg.o1 = !(!v); //make sure it is bool
|
|
requestJson(obj);
|
|
}
|
|
|
|
function setLor(i)
|
|
{
|
|
var obj = {"lor": i};
|
|
requestJson(obj);
|
|
}
|
|
|
|
function setPreset(i)
|
|
{
|
|
var obj = {"ps":i};
|
|
if (!isPlaylist(i) && pJson && pJson[i] && (!pJson[i].win || pJson[i].win.indexOf("Please") <= 0)) {
|
|
// we will send the complete preset content as to avoid delay introduced by
|
|
// async nature of applyPreset() and having to read the preset from file system.
|
|
obj = {"pd":i}; // use "pd" instead of "ps" to indicate that we are sending the preset content directly
|
|
Object.assign(obj, pJson[i]);
|
|
delete obj.ql; // no need for quick load
|
|
delete obj.n; // no need for name
|
|
}
|
|
if (isPlaylist(i)) obj.on = true; // force on
|
|
showToast("Loading preset " + pName(i) +" (" + i + ")");
|
|
requestJson(obj);
|
|
}
|
|
|
|
function saveP(i,pl)
|
|
{
|
|
pI = parseInt(gId(`p${i}id`).value);
|
|
if (!pI || pI < 1) pI = (i>0) ? i : getLowestUnusedP();
|
|
if (pI > 250) {alert("Preset ID must be 250 or less."); return;}
|
|
pN = gId(`p${i}txt`).value;
|
|
if (pN == "") pN = (pl?"Playlist ":"Preset ") + pI;
|
|
var obj = {};
|
|
if (!gId(`p${i}cstgl`).checked) {
|
|
var raw = gId(`p${i}api`).value;
|
|
try {
|
|
obj = JSON.parse(raw);
|
|
} catch (e) {
|
|
obj.win = raw;
|
|
if (raw.length < 2) {
|
|
gId(`p${i}warn`).innerHTML = "⚠ Please enter your API command first";
|
|
return;
|
|
} else if (raw.indexOf('{') > -1) {
|
|
gId(`p${i}warn`).innerHTML = "⚠ Syntax error in custom JSON API command";
|
|
return;
|
|
} else if (raw.indexOf("Please") == 0) {
|
|
gId(`p${i}warn`).innerHTML = "⚠ Please refresh the page before modifying this preset";
|
|
return;
|
|
}
|
|
}
|
|
obj.o = true;
|
|
} else {
|
|
if (pl) {
|
|
obj.playlist = plJson[i];
|
|
obj.on = true;
|
|
obj.o = true;
|
|
} else {
|
|
obj.ib = gId(`p${i}ibtgl`).checked;
|
|
obj.sb = gId(`p${i}sbtgl`).checked;
|
|
obj.sc = gId(`p${i}sbchk`).checked;
|
|
if (gId(`p${i}lmp`) && gId(`p${i}lmp`).value!=="") obj.ledmap = parseInt(gId(`p${i}lmp`).value);
|
|
}
|
|
}
|
|
|
|
obj.psave = pI; obj.n = pN;
|
|
var pQN = gId(`p${i}ql`).value;
|
|
if (pQN.length > 0) obj.ql = pQN;
|
|
|
|
showToast("Saving " + pN +" (" + pI + ")");
|
|
requestJson(obj);
|
|
if (obj.o) {
|
|
pJson[pI] = obj;
|
|
delete pJson[pI].psave;
|
|
delete pJson[pI].o;
|
|
delete pJson[pI].v;
|
|
delete pJson[pI].time;
|
|
} else {
|
|
pJson[pI] = {"n":pN, "win":"Please refresh the page to see this newly saved command."};
|
|
if (obj.win) pJson[pI].win = obj.win;
|
|
if (obj.ql) pJson[pI].ql = obj.ql;
|
|
}
|
|
populatePresets();
|
|
resetPUtil();
|
|
setTimeout(()=>{pmtLast=0; loadPresets();}, 750); // force reloading of presets
|
|
}
|
|
|
|
function testPl(i,bt) {
|
|
if (bt.dataset.test == 1) {
|
|
bt.dataset.test = 0;
|
|
bt.innerHTML = "<i class='icons btn-icon'></i>Test";
|
|
stopPl();
|
|
return;
|
|
}
|
|
bt.dataset.test = 1;
|
|
bt.innerHTML = "<i class='icons btn-icon'></i>Stop";
|
|
var obj = {};
|
|
obj.playlist = plJson[i];
|
|
obj.on = true;
|
|
requestJson(obj);
|
|
}
|
|
|
|
function stopPl() {
|
|
requestJson({playlist:{}})
|
|
}
|
|
|
|
function delP(i) {
|
|
var bt = gId(`p${i}del`);
|
|
if (bt.dataset.cnf == 1) {
|
|
var obj = {"pdel": i};
|
|
requestJson(obj);
|
|
delete pJson[i];
|
|
populatePresets();
|
|
gId('putil').classList.add('staybot');
|
|
} else {
|
|
bt.style.color = "var(--c-r)";
|
|
bt.innerHTML = "<i class='icons btn-icon'></i>Delete!";
|
|
bt.dataset.cnf = 1;
|
|
}
|
|
}
|
|
|
|
function selectSlot(b)
|
|
{
|
|
csel = b;
|
|
var cd = gId('csl').children;
|
|
for (let i of cd) i.classList.remove('xxs-w');
|
|
cd[b].classList.add('xxs-w');
|
|
setPicker(rgbStr(cd[b].dataset));
|
|
// force slider update on initial load (picker "color:change" not fired if black)
|
|
if (cpick.color.value == 0) updatePSliders();
|
|
gId('sliderW').value = parseInt(cd[b].dataset.w);
|
|
updateTrail(gId('sliderW'));
|
|
redrawPalPrev();
|
|
}
|
|
|
|
// set the color from a hex string. Used by quick color selectors
|
|
var lasth = 0;
|
|
function pC(col)
|
|
{
|
|
if (col == "rnd") {
|
|
col = {h: 0, s: 0, v: 100};
|
|
col.s = Math.floor((Math.random() * 50) + 50);
|
|
do {
|
|
col.h = Math.floor(Math.random() * 360);
|
|
} while (Math.abs(col.h - lasth) < 50);
|
|
lasth = col.h;
|
|
}
|
|
setPicker(col);
|
|
setColor(0);
|
|
}
|
|
|
|
function updatePSliders() {
|
|
// update RGB sliders
|
|
var col = cpick.color.rgb;
|
|
gId('sliderR').value = col.r;
|
|
gId('sliderG').value = col.g;
|
|
gId('sliderB').value = col.b;
|
|
|
|
// update hex field
|
|
var str = cpick.color.hexString.substring(1);
|
|
var w = parseInt(gId("csl").children[csel].dataset.w);
|
|
if (w > 0) str += w.toString(16);
|
|
gId('hexc').value = str;
|
|
gId('hexcnf').style.backgroundColor = "var(--c-3)";
|
|
|
|
// update HSV sliders
|
|
var c;
|
|
let h = cpick.color.hue;
|
|
let s = cpick.color.saturation;
|
|
let v = cpick.color.value;
|
|
|
|
gId("sliderH").value = h;
|
|
gId("sliderS").value = s;
|
|
gId('sliderV').value = v;
|
|
|
|
c = iro.Color.hsvToRgb({"h":h,"s":100,"v":100});
|
|
gId("sliderS").nextElementSibling.style.backgroundImage = 'linear-gradient(90deg, #aaa -15%, rgb('+c.r+','+c.g+','+c.b+'))';
|
|
|
|
c = iro.Color.hsvToRgb({"h":h,"s":s,"v":100});
|
|
gId('sliderV').nextElementSibling.style.backgroundImage = 'linear-gradient(90deg, #000 -15%, rgb('+c.r+','+c.g+','+c.b+'))';
|
|
|
|
// update Kelvin slider
|
|
gId('sliderK').value = cpick.color.kelvin;
|
|
}
|
|
|
|
function hexEnter()
|
|
{
|
|
if(event.keyCode == 13) fromHex();
|
|
}
|
|
|
|
function segEnter(s) {
|
|
if(event.keyCode == 13) setSeg(s);
|
|
}
|
|
|
|
function fromHex()
|
|
{
|
|
var str = gId('hexc').value;
|
|
let w = parseInt(str.substring(6), 16);
|
|
try {
|
|
setPicker("#" + str.substring(0,6));
|
|
} catch (e) {
|
|
setPicker("#ffaa00");
|
|
}
|
|
gId("csl").children[csel].dataset.w = isNaN(w) ? 0 : w;
|
|
setColor(2);
|
|
}
|
|
|
|
function setPicker(rgb) {
|
|
var c = new iro.Color(rgb);
|
|
if (c.value > 0) cpick.color.set(c);
|
|
else cpick.color.setChannel('hsv', 'v', 0);
|
|
updateTrail(gId('sliderR'));
|
|
updateTrail(gId('sliderG'));
|
|
updateTrail(gId('sliderB'));
|
|
}
|
|
|
|
function fromH()
|
|
{
|
|
cpick.color.setChannel('hsv', 'h', gId('sliderH').value);
|
|
}
|
|
|
|
function fromS()
|
|
{
|
|
cpick.color.setChannel('hsv', 's', gId('sliderS').value);
|
|
}
|
|
|
|
function fromV()
|
|
{
|
|
cpick.color.setChannel('hsv', 'v', gId('sliderV').value);
|
|
}
|
|
|
|
function fromK()
|
|
{
|
|
cpick.color.set({ kelvin: gId('sliderK').value });
|
|
}
|
|
|
|
function fromRgb()
|
|
{
|
|
var r = gId('sliderR').value;
|
|
var g = gId('sliderG').value;
|
|
var b = gId('sliderB').value;
|
|
setPicker(`rgb(${r},${g},${b})`);
|
|
let cd = gId('csl').children; // color slots
|
|
cd[csel].dataset.r = r;
|
|
cd[csel].dataset.g = g;
|
|
cd[csel].dataset.b = b;
|
|
setCSL(cd[csel]);
|
|
}
|
|
|
|
function fromW()
|
|
{
|
|
let w = gId('sliderW');
|
|
let cd = gId('csl').children; // color slots
|
|
cd[csel].dataset.w = w.value;
|
|
setCSL(cd[csel]);
|
|
updateTrail(w);
|
|
}
|
|
|
|
// sr 0: from RGB sliders, 1: from picker, 2: from hex
|
|
function setColor(sr)
|
|
{
|
|
var cd = gId('csl').children; // color slots
|
|
let cdd = cd[csel].dataset;
|
|
let w = 0, r,g,b;
|
|
if (sr == 1 && isRgbBlack(cdd)) cpick.color.setChannel('hsv', 'v', 100);
|
|
if (sr != 2 && hasWhite) w = parseInt(gId('sliderW').value);
|
|
var col = cpick.color.rgb;
|
|
cdd.r = r = hasRGB ? col.r : w;
|
|
cdd.g = g = hasRGB ? col.g : w;
|
|
cdd.b = b = hasRGB ? col.b : w;
|
|
cdd.w = w;
|
|
setCSL(cd[csel]);
|
|
var obj = {"seg": {"col": [[],[],[]]}};
|
|
obj.seg.col[csel] = [r, g, b, w];
|
|
requestJson(obj);
|
|
}
|
|
|
|
function setBalance(b)
|
|
{
|
|
var obj = {"seg": {"cct": parseInt(b)}};
|
|
requestJson(obj);
|
|
}
|
|
|
|
var hc = 0;
|
|
setInterval(()=>{
|
|
if (!isInfo) return;
|
|
hc+=18;
|
|
if (hc>300) hc=0;
|
|
if (hc>200)hc=306;
|
|
if (hc==144) hc+=36;
|
|
if (hc==108) hc+=18;
|
|
gId('heart').style.color = `hsl(${hc}, 100%, 50%)`;
|
|
}, 910);
|
|
|
|
function openGH() { window.open("https://github.com/Aircoookie/WLED/wiki"); }
|
|
|
|
var cnfr = false;
|
|
function cnfReset()
|
|
{
|
|
if (!cnfr) {
|
|
var bt = gId('resetbtn');
|
|
bt.style.color = "var(--c-r)";
|
|
bt.innerHTML = "Confirm Reboot";
|
|
cnfr = true; return;
|
|
}
|
|
window.location.href = "/reset";
|
|
}
|
|
|
|
var cnfrS = false;
|
|
function rSegs()
|
|
{
|
|
var bt = gId('rsbtn');
|
|
if (!cnfrS) {
|
|
bt.style.color = "var(--c-r)";
|
|
bt.innerHTML = "Confirm reset";
|
|
cnfrS = true; return;
|
|
}
|
|
cnfrS = false;
|
|
bt.style.color = "var(--c-f)";
|
|
bt.innerHTML = "Reset segments";
|
|
var obj = {"seg":[{"start":0,"stop":ledCount,"sel":true}]};
|
|
if (isM) {
|
|
obj.seg[0].stop = mw;
|
|
obj.seg[0].startX = 0;
|
|
obj.seg[0].stopY = mh;
|
|
}
|
|
for (let i=1; i<=lSeg; i++) obj.seg.push({"stop":0});
|
|
requestJson(obj);
|
|
}
|
|
|
|
//WLEDMM generate presets.json file
|
|
function genPresets()
|
|
{
|
|
var result = "";
|
|
var sep = "{";
|
|
|
|
var effects = eJson;
|
|
var playlistPS = JSON.parse("{}");
|
|
var playlistSep = JSON.parse("{}");
|
|
var playlistDur = JSON.parse("{}");
|
|
var playlistTrans = JSON.parse("{}");
|
|
function addToPlaylist(m, id) {
|
|
if (!playlistPS[m]) playlistPS[m] = "";
|
|
if (!playlistDur[m]) playlistDur[m] = "";
|
|
if (!playlistTrans[m]) playlistTrans[m] = "";
|
|
if (!playlistSep[m]) playlistSep[m] = "";
|
|
playlistPS[m] += playlistSep[m] + `${id}`;
|
|
playlistDur[m] += playlistSep[m] + "100";
|
|
playlistTrans[m] += playlistSep[m] + "7";
|
|
playlistSep[m] = ",";
|
|
}
|
|
for (let ef of effects) {
|
|
if (ef.name.indexOf("RSVD") < 0) {
|
|
if (Array.isArray(fxdata) && fxdata.length>ef.id) {
|
|
let fd = fxdata[ef.id];
|
|
let eP = (fd == '')?[]:fd.split(";"); // effect parameters
|
|
let m = (eP.length<4 || eP[3]==='')?'1':eP[3]; // flags
|
|
// console.log(ef, eP);
|
|
//transform key values in json format
|
|
var defaultString = "";
|
|
//if key/values defined, convert them to json in defaultString
|
|
if (eP.length>4) {
|
|
let defaults = (eP[4] == '')?[]:eP[4].split(",");
|
|
for (let i=0; i<defaults.length;i++) {
|
|
let keyValue = (defaults[i] == '')?[]:defaults[i].split("=");
|
|
defaultString += `,"${keyValue[0]}":${keyValue[1]}`;
|
|
}
|
|
}
|
|
//if not defined set to default
|
|
if (!defaultString.includes("sx")) defaultString += ',"sx":128'; //Speed
|
|
if (!defaultString.includes("ix")) defaultString += ',"ix":128'; //Intensity
|
|
if (!defaultString.includes("c1")) defaultString += ',"c1":128'; //Custom 1
|
|
if (!defaultString.includes("c2")) defaultString += ',"c2":128'; //Custom 2
|
|
if (!defaultString.includes("c3")) defaultString += ',"c3":16'; //Custom 3
|
|
if (!defaultString.includes("o1")) defaultString += ',"o1":0'; //Check 1
|
|
if (!defaultString.includes("o2")) defaultString += ',"o2":0'; //Check 2
|
|
if (!defaultString.includes("o3")) defaultString += ',"o3":0'; //Check 3
|
|
if (!defaultString.includes("pal")) defaultString += ',"pal":11'; //Temporary for deterministic effects test: Set to 11/Raibow instead of 1/Random smooth palette (if not set different)
|
|
if (!defaultString.includes("m12") && m.includes("1") && !m.includes("1.5") && !m.includes("12"))
|
|
defaultString += ',"rev":true,"mi":true,"rY":true,"mY":true,"m12":2'; //Arc expansion
|
|
else {
|
|
if (!defaultString.includes("rev")) defaultString += ',"rev":false';
|
|
if (!defaultString.includes("mi")) defaultString += ',"mi":false';
|
|
if (!defaultString.includes("rY")) defaultString += ',"rY":false';
|
|
if (!defaultString.includes("mY")) defaultString += ',"mY":false';
|
|
}
|
|
result += `${sep}"${ef.id}":{"n":"${ef.name}","mainseg":0,"seg":[{"id":0,"fx":${ef.id}${defaultString}}]}`;
|
|
sep = "\n,";
|
|
addToPlaylist(m, ef.id);
|
|
addToPlaylist("All", ef.id);
|
|
if (m.includes("1")) addToPlaylist("All1", ef.id);
|
|
if (m.includes("2")) addToPlaylist("All2", ef.id);
|
|
} //fxData is array
|
|
} //not RSVD
|
|
} //all effects
|
|
|
|
var seq=230; //Playlist start here
|
|
// console.log(playlistPS, playlistDur, playlistTrans);
|
|
for (const m in playlistPS) {
|
|
let playListString = `\n,"${seq}":{"n":"${m}D Playlist","ql":"${seq}","on":true,"playlist":{"ps":[${playlistPS[m]}],"dur":[${playlistDur[m]}],"transition":[${playlistTrans[m]}],"repeat":0,"end":0,"r":1}}`;
|
|
// console.log(playListString);
|
|
result += playListString;
|
|
seq++;
|
|
}
|
|
|
|
result += "}";
|
|
|
|
//assign result and show text and save button
|
|
gId("genPresets").hidden = true;
|
|
gId("savePresetsGen").hidden = false;
|
|
gId("presetsGen").hidden = false;
|
|
gId("presetsGen").value = result;
|
|
// console.log(result);
|
|
|
|
}
|
|
|
|
//WLEDMM: utility function to load contents of file from FS (used in draw)
|
|
function fetchAndExecute(url, name, parms, callback, callError = null)
|
|
{
|
|
fetch
|
|
(url+name, {
|
|
method: 'get'
|
|
})
|
|
.then(res => {
|
|
if (!res.ok) {
|
|
callError("File " + name + " not found");
|
|
return "";
|
|
}
|
|
return res.text();
|
|
})
|
|
.then(text => {
|
|
callback(parms, text);
|
|
})
|
|
.catch(function (error) {
|
|
if (callError) callError(parms, "Error getting " + name);
|
|
console.log(error);
|
|
})
|
|
.finally(() => {
|
|
// if (callback) setTimeout(callback,99);
|
|
});
|
|
}
|
|
|
|
//WLEDMM: utility function to save file to FS (used in savePresetsGen)
|
|
function uploadFileWithText(name, text)
|
|
{
|
|
var req = new XMLHttpRequest();
|
|
req.addEventListener('load', function(){showToast(this.responseText,this.status >= 400)});
|
|
req.addEventListener('error', function(e){showToast(e.stack,true);});
|
|
req.open("POST", "/upload");
|
|
var formData = new FormData();
|
|
|
|
var blob = new Blob([text], {type : 'application/text'});
|
|
var fileOfBlob = new File([blob], name);
|
|
formData.append("upload", fileOfBlob);
|
|
|
|
req.send(formData);
|
|
}
|
|
|
|
//WLEDMM: save the presets.json to FS
|
|
function savePresetsGen()
|
|
{
|
|
if (!confirm('Are you sure to (over)write presets.json?')) return;
|
|
|
|
uploadFileWithText("/presets.json", gId("presetsGen").value);
|
|
}
|
|
|
|
function loadPalettesData(callback = null)
|
|
{
|
|
if (palettesData) return;
|
|
const lsKey = "wledPalx";
|
|
var lsPalData = localStorage.getItem(lsKey);
|
|
if (lsPalData) {
|
|
try {
|
|
var d = JSON.parse(lsPalData);
|
|
if (d && d.vid == d.vid) {
|
|
palettesData = d.p;
|
|
if (callback) callback();
|
|
return;
|
|
}
|
|
} catch (e) {}
|
|
}
|
|
|
|
palettesData = {};
|
|
getPalettesData(0, ()=>{
|
|
localStorage.setItem(lsKey, JSON.stringify({
|
|
p: palettesData,
|
|
vid: lastinfo.vid
|
|
}));
|
|
redrawPalPrev();
|
|
if (callback) setTimeout(callback, 99);
|
|
});
|
|
}
|
|
|
|
function getPalettesData(page, callback)
|
|
{
|
|
var url = (loc?`http://${locip}`:'') + `/json/palx?page=${page}`;
|
|
|
|
fetch(url, {
|
|
method: 'get',
|
|
headers: {
|
|
"Content-type": "application/json; charset=UTF-8"
|
|
}
|
|
})
|
|
.then(res => {
|
|
if (!res.ok) showErrorToast();
|
|
return res.json();
|
|
})
|
|
.then(json => {
|
|
palettesData = Object.assign({}, palettesData, json.p);
|
|
if (page < json.m) setTimeout(()=>{ getPalettesData(page + 1, callback); }, 50);
|
|
else callback();
|
|
})
|
|
.catch((error)=>{
|
|
showToast(error, true);
|
|
});
|
|
}
|
|
/*
|
|
function hideModes(txt)
|
|
{
|
|
for (let e of (gId('fxlist').querySelectorAll('.lstI')||[])) {
|
|
let iT = e.querySelector('.lstIname').innerText;
|
|
let f = false;
|
|
if (txt==="2D") f = iT.indexOf("\u25A6") >= 0 && iT.indexOf("\u22EE") < 0; // 2D && !1D
|
|
else f = iT.indexOf(txt) >= 0;
|
|
if (f) e.classList.add('hide'); //else e.classList.remove('hide');
|
|
}
|
|
}
|
|
*/
|
|
function search(f,l=null)
|
|
{
|
|
f.nextElementSibling.style.display=(f.value!=='')?'block':'none';
|
|
if (!l) return;
|
|
var el = gId(l).querySelectorAll('.lstI');
|
|
// filter list items but leave (Default & Solid) always visible
|
|
for (i = (l==='pcont'?0:1); i < el.length; i++) {
|
|
var it = el[i];
|
|
var itT = it.querySelector('.lstIname').innerText.toUpperCase();
|
|
it.style.display = (itT.indexOf(f.value.toUpperCase())<0) ? 'none' : '';
|
|
}
|
|
}
|
|
|
|
function clean(c)
|
|
{
|
|
c.style.display='none';
|
|
var i=c.previousElementSibling;
|
|
i.value='';
|
|
i.focus();
|
|
i.dispatchEvent(new Event('input'));
|
|
if (i.parentElement.id=='fxFind') {
|
|
gId("filters").querySelectorAll("input[type=checkbox]").forEach((e)=>{e.checked=false;});
|
|
}
|
|
}
|
|
|
|
function filterFx(o)
|
|
{
|
|
if (!o) return;
|
|
let i = gId('fxFind').children[0];
|
|
i.value=!o.checked?'':o.dataset.flt;
|
|
i.focus();
|
|
i.dispatchEvent(new Event('input'));
|
|
gId("filters").querySelectorAll("input[type=checkbox]").forEach((e)=>{if(e!==o)e.checked=false;});
|
|
}
|
|
|
|
// make sure "dur" and "transition" are arrays with at least the length of "ps"
|
|
function formatArr(pl) {
|
|
var l = pl.ps.length;
|
|
if (!Array.isArray(pl.dur)) {
|
|
var v = pl.dur;
|
|
if (isNaN(v)) v = 100;
|
|
pl.dur = [v];
|
|
}
|
|
var l2 = pl.dur.length;
|
|
if (l2 < l)
|
|
{
|
|
for (var i = 0; i < l - l2; i++)
|
|
pl.dur.push(pl.dur[l2-1]);
|
|
}
|
|
|
|
if (!Array.isArray(pl.transition)) {
|
|
var v = pl.transition;
|
|
if (isNaN(v)) v = tr;
|
|
pl.transition = [v];
|
|
}
|
|
var l2 = pl.transition.length;
|
|
if (l2 < l)
|
|
{
|
|
for (var i = 0; i < l - l2; i++)
|
|
pl.transition.push(pl.transition[l2-1]);
|
|
}
|
|
}
|
|
|
|
function expand(i)
|
|
{
|
|
var seg = i<100 ? gId('seg' +i) : gId(`p${i-100}o`);
|
|
let ps = gId("pcont").children; // preset wrapper
|
|
if (i>100) for (let p of ps) { p.classList.remove('selected'); if (p!==seg) p.classList.remove('expanded'); } // collapse all other presets & remove selected
|
|
|
|
seg.classList.toggle('expanded');
|
|
|
|
// presets
|
|
if (i >= 100) {
|
|
var p = i-100;
|
|
if (seg.classList.contains('expanded')) {
|
|
if (isPlaylist(p)) {
|
|
plJson[p] = pJson[p].playlist;
|
|
// make sure all keys are present in plJson[p]
|
|
formatArr(plJson[p]);
|
|
if (isNaN(plJson[p].repeat)) plJson[p].repeat = 0;
|
|
if (!plJson[p].r) plJson[p].r = false;
|
|
if (isNaN(plJson[p].end)) plJson[p].end = 0;
|
|
gId('seg' +i).innerHTML = makeP(p,true);
|
|
refreshPlE(p);
|
|
} else {
|
|
gId('seg' +i).innerHTML = makeP(p);
|
|
}
|
|
var papi = papiVal(p);
|
|
gId(`p${p}api`).value = papi;
|
|
if (papi.indexOf("Please") == 0) gId(`p${p}cstgl`).checked = false;
|
|
tglCs(p);
|
|
gId('putil').classList.remove('staybot');
|
|
} else {
|
|
updatePA();
|
|
gId('seg' +i).innerHTML = "";
|
|
gId('putil').classList.add('staybot');
|
|
}
|
|
}
|
|
|
|
seg.scrollIntoView({
|
|
behavior: 'smooth',
|
|
block: 'center'
|
|
});
|
|
}
|
|
|
|
function unfocusSliders()
|
|
{
|
|
gId("sliderBri").blur();
|
|
gId("sliderSpeed").blur();
|
|
gId("sliderIntensity").blur();
|
|
}
|
|
|
|
// sliding UI
|
|
const _C = d.querySelector('.container'), N = 4;
|
|
|
|
let iSlide = 0, x0 = null, scrollS = 0, locked = false;
|
|
|
|
function unify(e) { return e.changedTouches ? e.changedTouches[0] : e; }
|
|
|
|
function hasIroClass(classList)
|
|
{
|
|
for (var i = 0; i < classList.length; i++) {
|
|
var element = classList[i];
|
|
if (element.startsWith('Iro')) return true;
|
|
}
|
|
return false;
|
|
}
|
|
//required by rangetouch.js
|
|
function lock(e)
|
|
{
|
|
if (pcMode) return;
|
|
var l = e.target.classList;
|
|
var pl = e.target.parentElement.classList;
|
|
|
|
if (l.contains('noslide') || hasIroClass(l) || hasIroClass(pl)) return;
|
|
|
|
x0 = unify(e).clientX;
|
|
scrollS = gEBCN("tabcontent")[iSlide].scrollTop;
|
|
|
|
_C.classList.toggle('smooth', !(locked = true));
|
|
}
|
|
//required by rangetouch.js
|
|
function move(e)
|
|
{
|
|
if(!locked || pcMode) return;
|
|
var clientX = unify(e).clientX;
|
|
var dx = clientX - x0;
|
|
var s = Math.sign(dx);
|
|
var f = +(s*dx/wW).toFixed(2);
|
|
|
|
if((clientX != 0) &&
|
|
(iSlide > 0 || s < 0) && (iSlide < N - 1 || s > 0) &&
|
|
f > 0.12 &&
|
|
gEBCN("tabcontent")[iSlide].scrollTop == scrollS)
|
|
{
|
|
_C.style.setProperty('--i', iSlide -= s);
|
|
f = 1 - f;
|
|
updateTablinks(iSlide);
|
|
}
|
|
_C.style.setProperty('--f', f);
|
|
_C.classList.toggle('smooth', !(locked = false));
|
|
x0 = null;
|
|
}
|
|
|
|
function size()
|
|
{
|
|
wW = window.innerWidth;
|
|
var h = gId('top').clientHeight;
|
|
sCol('--th', h + "px");
|
|
sCol('--bh', gId('bot').clientHeight + "px");
|
|
if (isLv && !isM) h -= 4; //WLEDMM: no for matrices
|
|
sCol('--tp', h + "px");
|
|
togglePcMode();
|
|
lastw = wW;
|
|
}
|
|
|
|
function togglePcMode(fromB = false)
|
|
{
|
|
if (fromB) {
|
|
pcModeA = !pcModeA;
|
|
localStorage.setItem('pcm', pcModeA);
|
|
}
|
|
pcMode = (wW >= 1024) && pcModeA;
|
|
if (cpick) cpick.resize(pcMode && wW>1023 && wW<1250 ? 230 : 260); // for tablet in landscape
|
|
if (!fromB && ((wW < 1024 && lastw < 1024) || (wW >= 1024 && lastw >= 1024))) return; // no change in size and called from size()
|
|
openTab(0, true);
|
|
updateTablinks(0);
|
|
gId('buttonPcm').className = (pcMode) ? "active":"";
|
|
gId('bot').style.height = (pcMode && !cfg.comp.pcmbot) ? "0":"auto";
|
|
sCol('--bh', gId('bot').clientHeight + "px");
|
|
_C.style.width = (pcMode)?'100%':'400%';
|
|
//WLEDMM resize segmentview
|
|
console.log("drawSegmentView","togglePCMode");
|
|
if (isM) drawSegmentView();
|
|
}
|
|
|
|
function mergeDeep(target, ...sources)
|
|
{
|
|
if (!sources.length) return target;
|
|
const source = sources.shift();
|
|
|
|
if (isObj(target) && isObj(source)) {
|
|
for (const key in source) {
|
|
if (isObj(source[key])) {
|
|
if (!target[key]) Object.assign(target, { [key]: {} });
|
|
mergeDeep(target[key], source[key]);
|
|
} else {
|
|
Object.assign(target, { [key]: source[key] });
|
|
}
|
|
}
|
|
}
|
|
return mergeDeep(target, ...sources);
|
|
}
|
|
|
|
size();
|
|
_C.style.setProperty('--n', N);
|
|
|
|
window.addEventListener('resize', size, true);
|
|
|
|
_C.addEventListener('mousedown', lock, false);
|
|
_C.addEventListener('touchstart', lock, false);
|
|
|
|
_C.addEventListener('mouseout', move, false);
|
|
_C.addEventListener('mouseup', move, false);
|
|
_C.addEventListener('touchend', move, false);
|