Skip to content
2 changes: 2 additions & 0 deletions wled00/const.h
Original file line number Diff line number Diff line change
Expand Up @@ -448,6 +448,8 @@ static_assert(WLED_MAX_BUSSES <= 32, "WLED_MAX_BUSSES exceeds hard limit");
//Playlist option byte
#define PL_OPTION_SHUFFLE 0x01
#define PL_OPTION_RESTORE 0x02
#define PL_OPTION_DETERMINISTIC_SHUFFLE 0x04
#define PL_OPTION_CLOCK_SYNC 0x08

// Segment capability byte
#define SEG_CAPABILITY_RGB 0x01
Expand Down
80 changes: 61 additions & 19 deletions wled00/data/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2032,10 +2032,46 @@ function pleTr(p,i,field)
plJson[p].transition[i] = Math.floor(field.value*10);
}

function plR(p)
function plUpdateOptions(p, changed)
{
var pl = plJson[p];
pl.r = gId(`pl${p}rtgl`).checked;

const shuffle = gId(`pl${p}rtgl`).checked;
const deterministic = gId(`pl${p}deterministic`);
const clockSync = gId(`pl${p}clockSync`);
const manual = gId(`pl${p}manual`);
let manualChanged = changed == "manual";

// Ensure clock sync and manual advance can't be checked at the same time
if (clockSync.checked && manual.checked) {
if (changed == "clockSync") {
manual.checked = false;
manualChanged = true;
} else {
clockSync.checked = false;
}
}

// Reset & hide deterministic flag when not shuffling
if (!shuffle) deterministic.checked = false;
gId(`pl${p}deterministicRow`).style.display = shuffle ? "block" : "none";

pl.r = shuffle;
pl.deterministic = deterministic.checked;
pl.clockSync = clockSync.checked;

// Normalize durations when manual advance changes
if (manualChanged) {
plJson[p].dur.forEach((_,i)=>{
const e = manual.checked ? 0 : 100;
const d = gId(`pl${p}du${i}`);
plJson[p].dur[i] = e;
d.value = e/10; // 10s default
d.readOnly = manual.checked;
});
}

// Update repeat/end-preset controls
if (gId(`pl${p}rptgl`).checked) { // infinite
pl.repeat = 0;
delete pl.end;
Expand All @@ -2047,17 +2083,6 @@ function plR(p)
}
}

function plM(p)
{
const man = gId(`pl${p}manual`).checked;
plJson[p].dur.forEach((e,i)=>{
const d = gId(`pl${p}du${i}`);
plJson[p].dur[i] = e = man ? 0 : 100;
d.value = e/10; // 10s default
d.readOnly = man;
});
}

function makeP(i,pl)
{
var content = "";
Expand All @@ -2069,27 +2094,39 @@ function makeP(i,pl)
transition: [tr],
repeat: 0,
r: false,
deterministic: false,
clockSync: false,
end: 0
};
const clockSync = !!plJson[i].clockSync;
if (!plJson[i].r) plJson[i].deterministic = false;
const rep = plJson[i].repeat ? plJson[i].repeat : 0;
const man = plJson[i].dur == 0;
const man = !clockSync && plJson[i].dur.every(d => d == 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":""}>
<input type="checkbox" id="pl${i}rtgl" onchange="plUpdateOptions(${i}, 'shuffle')" ${plJson[i].r||rep<0?"checked":""}>
<span class="checkmark"></span>
</label>
<label class="check revchkl" id="pl${i}deterministicRow" style="display:${plJson[i].r||rep<0?"block":"none"};margin-left:1.2em" title="Uses a repeatable shuffle order">Deterministic
<input type="checkbox" id="pl${i}deterministic" onchange="plUpdateOptions(${i}, 'deterministic')" ${plJson[i].deterministic?"checked":""}>
<span class="checkmark"></span>
</label>
<label class="check revchkl" title="Derives playlist position from wall time">Clock sync
<input type="checkbox" id="pl${i}clockSync" onchange="plUpdateOptions(${i}, 'clockSync')" ${clockSync?"checked":""}>
<span class="checkmark"></span>
</label>
<label class="check revchkl">Manual advance
<input type="checkbox" id="pl${i}manual" onchange="plM(${i})" ${man?"checked":""}>
<input type="checkbox" id="pl${i}manual" onchange="plUpdateOptions(${i}, 'manual')" ${man?"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"}>
<input type="checkbox" id="pl${i}rptgl" onchange="plUpdateOptions(${i}, 'repeat')" ${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="c">Repeat <input type="number" id="pl${i}rp" oninput="plUpdateOptions(${i}, 'repeat')" 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}>
<div class="sel-p"><select class="sel-ple" id="pl${i}selEnd" onchange="plUpdateOptions(${i}, 'repeat')" data-val=${plJson[i].end?plJson[i].end:0}>
<option value="0">None</option>
<option value="255" ${plJson[i].end && plJson[i].end==255?"selected":""}>Restore preset</option>
${makePlSel(i, plJson[i].end?plJson[i].end:0)}
Expand Down Expand Up @@ -2152,6 +2189,7 @@ function makePUtil()
p.classList.remove('staybot');
p.classList.add('pres');
p.innerHTML = `<div class="presin expanded">${makeP(0)}</div>`;
tooltip("#putil");
let pTx = gId('p0txt');
pTx.focus();
pTx.value = eJson.find((o)=>{return o.id==selectedFx}).name;
Expand Down Expand Up @@ -2201,6 +2239,7 @@ function makePlUtil()
p.classList.add('pres');
p.innerHTML = `<div class="presin expanded" id="seg100">${makeP(0,true)}</div></div>`;
refreshPlE(0);
tooltip("#putil");
gId('p0txt').focus();
p.scrollIntoView({
behavior: 'smooth',
Expand Down Expand Up @@ -3071,12 +3110,15 @@ function expand(i)
formatArr(plJson[p]);
if (isNaN(plJson[p].repeat)) plJson[p].repeat = 0;
if (!plJson[p].r) plJson[p].r = false;
if (!plJson[p].deterministic) plJson[p].deterministic = false;
if (!plJson[p].clockSync) plJson[p].clockSync = false;
if (isNaN(plJson[p].end)) plJson[p].end = 0;
gId('seg' +i).innerHTML = makeP(p,true);
refreshPlE(p);
Comment on lines +3113 to 3117

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

Normalize persisted clock-sync playlists before rendering.

The PR explicitly says clock-sync playlists do not support zero-duration/manual-advance entries, but this path only backfills keys and then renders the editor. If a playlist is loaded from JSON with clockSync: true and all dur[] == 0, wled00/playlist.cpp will reject clock-sync because playlistTotalDur == 0, while the UI still shows Clock sync as enabled until the user toggles an option. Normalize that combination here (or clear clockSync) before makeP()/refreshPlE().

🧰 Tools
🪛 ast-grep (0.44.0)

[warning] 3115-3115: Avoid assigning untrusted data to innerHTML/outerHTML or document.write
Context: gId('seg' +i).innerHTML = makeP(p,true)
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation ('Cross-site Scripting').

(inner-outer-html)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@wled00/data/index.js` around lines 3113 - 3117, The playlist editor path in
makeP()/refreshPlE() only backfills missing fields and can leave persisted
clock-sync playlists in an invalid state. Update the JSON load normalization so
that when a playlist has clockSync enabled but all durations are zero, clockSync
is cleared or otherwise normalized before rendering the segment UI. Make this
adjustment in the existing playlist parsing flow around the makeP(p, true) and
refreshPlE(p) calls so the editor reflects the same constraints enforced by
playlist.cpp.

} else {
gId('seg' +i).innerHTML = makeP(p);
}
tooltip('#seg' + i);
var papi = papiVal(p);
gId(`p${p}api`).value = papi;
if (papi.indexOf("Please") == 0) gId(`p${p}cstgl`).checked = false;
Expand Down
1 change: 0 additions & 1 deletion wled00/fcn_declare.h
Original file line number Diff line number Diff line change
Expand Up @@ -256,7 +256,6 @@ void handleOverlayDraw();
// void _overlayAnalogClock(); // local function, only used in overlay.cpp

//playlist.cpp
void shufflePlaylist();
void unloadPlaylist();
int16_t loadPlaylist(JsonObject playlistObject, byte presetId = 0);
void handlePlaylist();
Expand Down
Loading
Loading