-
-
Notifications
You must be signed in to change notification settings - Fork 4.2k
Add settings page to hide unwanted effects from the picker #5585
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,185 @@ | ||
| <!DOCTYPE html> | ||
| <html lang="en"> | ||
| <head> | ||
| <meta charset="utf-8"> | ||
| <meta content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" name="viewport"> | ||
| <title>Effect Visibility</title> | ||
| <style> html { visibility: hidden; } </style> | ||
| <style> | ||
| .fxcols { display: flex; flex-direction: column; gap: 6px; } | ||
| .fxcol { display: flex; flex-direction: column; } | ||
| .fxcol h3 { margin: 0 0 4px 0; font-size: 0.95em; display: flex; align-items: center; gap: 6px; } | ||
| .fxcol h3 .selbtns { margin-left: auto; display: inline-flex; gap: 4px; } | ||
| .fxcol h3 .selbtns button { | ||
| font-size: 0.8em; padding: 2px 8px; border-radius: 3px; | ||
| background: #444; color: #fff; border: 0; cursor: pointer; | ||
| margin: 0; min-width: 0; | ||
| } | ||
| .fxcol h3 .selbtns button:hover { background: #555; } | ||
| .fxlist { | ||
| border: 1px solid #555; | ||
| background: #111; | ||
| min-height: 200px; max-height: 50vh; overflow-y: auto; | ||
| padding: 2px; border-radius: 4px; | ||
| text-align: left; | ||
| } | ||
| .fxitem { | ||
| padding: 4px 6px; margin: 1px 0; border-radius: 3px; | ||
| cursor: pointer; user-select: none; | ||
| white-space: nowrap; overflow: hidden; text-overflow: ellipsis; | ||
| font-size: 0.9em; | ||
| } | ||
| .fxitem:hover { background: #333; } | ||
| .fxitem.sel { background: #888; color: #111; } | ||
| .fxitem.solid { opacity: 0.55; cursor: not-allowed; font-style: italic; } | ||
| .fxbtns { display: flex; flex-direction: row; justify-content: center; gap: 6px; } | ||
| .fxbtns button { padding: 6px 10px; margin: 4px 0; min-width: 80px; font-size: 0.95em; } | ||
| .counts { font-size: 0.8em; opacity: 0.7; } | ||
| </style> | ||
| <script> | ||
| // load common.js with retry on error | ||
| (function loadFiles() { | ||
| const l = document.createElement('script'); | ||
| l.src = 'common.js'; | ||
| l.onload = () => loadResources(['style.css'], S); | ||
| l.onerror = () => setTimeout(loadFiles, 100); | ||
| document.head.appendChild(l); | ||
| })(); | ||
|
|
||
| var allEffects = []; // [{id,name}], id 0 always first | ||
| var hiddenSet = new Set(); // ids currently hidden | ||
| var dirty = false; | ||
|
|
||
| function S() { | ||
| getLoc(); | ||
| if (loc) d.Sf.action = getURL('/settings/fx'); | ||
| // ?all=1 returns real names for user-hidden effects so the Hidden list is populated | ||
| Promise.all([ | ||
| fetch(getURL('/json/effects?all=1')).then(r=>r.json()), | ||
| fetch(getURL('/json/cfg')).then(r=>r.json()) | ||
| ]).then(([effs, cfg]) => { | ||
| allEffects = effs.map((name, id) => ({id, name})); | ||
| let h = (cfg && cfg.fx && cfg.fx.hidden) || []; | ||
| hiddenSet = new Set(h.filter(id => id !== 0)); | ||
| render(); | ||
| d.documentElement.style.visibility='visible'; | ||
| }).catch(e => { | ||
| alert('Load failed: '+e); | ||
| d.documentElement.style.visibility='visible'; | ||
| }); | ||
| } | ||
|
|
||
| // build-disabled or deprecated slot, not user-toggleable | ||
| function isReserved(name) { return name && name.indexOf('RSVD') >= 0; } | ||
|
|
||
| function render() { | ||
| const visList = gId('visList'); | ||
| const hidList = gId('hidList'); | ||
| visList.innerHTML = ''; | ||
| hidList.innerHTML = ''; | ||
| let visN = 0, hidN = 0; | ||
| for (const e of allEffects) { | ||
| if (isReserved(e.name)) continue; // never show RSVD slots in either list | ||
| const inHidden = hiddenSet.has(e.id); | ||
| const li = d.createElement('div'); | ||
| li.className = 'fxitem'; | ||
| li.dataset.id = e.id; | ||
| li.textContent = e.name; | ||
| if (e.id === 0) { | ||
| li.classList.add('solid'); | ||
| li.title = 'Solid cannot be hidden'; | ||
| } else { | ||
| li.onclick = () => toggleSel(li); | ||
| } | ||
|
softhack007 marked this conversation as resolved.
|
||
| if (inHidden && e.id !== 0) { hidList.appendChild(li); hidN++; } | ||
| else { visList.appendChild(li); visN++; } | ||
| } | ||
| gId('visCount').textContent = visN; | ||
| gId('hidCount').textContent = hidN; | ||
| updateBtns(); | ||
| } | ||
|
|
||
| function toggleSel(li) { | ||
| li.classList.toggle('sel'); | ||
| updateBtns(); | ||
| } | ||
|
|
||
| function selectAll(listId, sel) { | ||
| // skip Solid (non-selectable, has .solid class) | ||
| const items = d.querySelectorAll('#'+listId+' .fxitem:not(.solid)'); | ||
| for (const li of items) li.classList.toggle('sel', sel); | ||
| updateBtns(); | ||
| } | ||
|
|
||
| function updateBtns() { | ||
| gId('toHideBtn').disabled = !d.querySelectorAll('#visList .sel').length; | ||
| gId('toShowBtn').disabled = !d.querySelectorAll('#hidList .sel').length; | ||
| } | ||
|
|
||
| function moveSel(fromId, hide) { | ||
| const sel = d.querySelectorAll('#'+fromId+' .sel'); | ||
| if (!sel.length) return; | ||
| for (const li of sel) { | ||
| const id = parseInt(li.dataset.id); | ||
| if (id === 0) continue; // safety: Solid never moves | ||
| if (hide) hiddenSet.add(id); else hiddenSet.delete(id); | ||
| } | ||
| dirty = true; | ||
| render(); | ||
| } | ||
|
|
||
| function Save() { | ||
| gId('FXH').value = Array.from(hiddenSet).sort((a,b)=>a-b).join(','); | ||
| dirty = false; | ||
| d.Sf.submit(); | ||
| } | ||
|
|
||
| window.addEventListener('beforeunload', e => { | ||
| if (dirty) { e.preventDefault(); e.returnValue = ''; } | ||
| }); | ||
| </script> | ||
| </head> | ||
| <body> | ||
| <form id="form_s" name="Sf" method="post"> | ||
| <div class="toprow"> | ||
| <div class="helpB"><button type="button" onclick="H('features/effects/#effect-visibility')">?</button></div> | ||
| <button type="button" onclick="B()">Back</button><button type="button" onclick="Save()">Save</button><hr> | ||
| </div> | ||
| <h2>Effect Visibility</h2> | ||
| <div class="sec"> | ||
| <i>Hide effects you don't use so they don't clutter the main UI's effect picker. | ||
| Hidden effects are reported as <code>RSVD</code> on the JSON API.</i><br><br> | ||
| <i>Presets, playlists, or external API calls that reference a hidden effect will fall | ||
| through to the next visible effect — un-hide to restore the original mapping.</i> | ||
|
|
||
| <div class="fxcols"> | ||
| <div class="fxcol"> | ||
| <h3>Visible <span class="counts">(<span id="visCount">0</span>)</span> | ||
| <span class="selbtns"> | ||
| <button type="button" onclick="selectAll('visList', true)">All</button> | ||
| <button type="button" onclick="selectAll('visList', false)">None</button> | ||
| </span> | ||
| </h3> | ||
| <div id="visList" class="fxlist"></div> | ||
| </div> | ||
| <div class="fxbtns"> | ||
| <button id="toHideBtn" type="button" onclick="moveSel('visList', true)" disabled>Hide ↓</button> | ||
| <button id="toShowBtn" type="button" onclick="moveSel('hidList', false)" disabled>↑ Show</button> | ||
| </div> | ||
| <div class="fxcol"> | ||
| <h3>Hidden <span class="counts">(<span id="hidCount">0</span>)</span> | ||
| <span class="selbtns"> | ||
| <button type="button" onclick="selectAll('hidList', true)">All</button> | ||
| <button type="button" onclick="selectAll('hidList', false)">None</button> | ||
| </span> | ||
| </h3> | ||
| <div id="hidList" class="fxlist"></div> | ||
| </div> | ||
| </div> | ||
|
|
||
| <input type="hidden" name="FXH" id="FXH" value=""> | ||
| </div> | ||
| <hr><button type="button" onclick="B()">Back</button><button type="button" onclick="Save()">Save</button> | ||
| </form> | ||
| </body> | ||
| </html> | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -1188,10 +1188,15 @@ void serializePins(JsonObject root) | |||||||||||||
|
|
||||||||||||||
| // deserializes mode names string into JsonArray | ||||||||||||||
| // also removes effect data extensions (@...) from deserialised names | ||||||||||||||
| void serializeModeNames(JsonArray arr) | ||||||||||||||
| // bypassHide=true returns real names for user-hidden effects (used by /settings/fx) | ||||||||||||||
| void serializeModeNames(JsonArray arr, bool bypassHide) | ||||||||||||||
| { | ||||||||||||||
| char lineBuffer[256]; | ||||||||||||||
| for (size_t i = 0; i < strip.getModeCount(); i++) { | ||||||||||||||
| if (!bypassHide && isFxHidden((uint8_t)i)) { | ||||||||||||||
| arr.add("RSVD"); | ||||||||||||||
| continue; | ||||||||||||||
| } | ||||||||||||||
| strncpy_P(lineBuffer, strip.getModeData(i), sizeof(lineBuffer)/sizeof(char)-1); | ||||||||||||||
| lineBuffer[sizeof(lineBuffer)/sizeof(char)-1] = '\0'; // terminate string | ||||||||||||||
| if (lineBuffer[0] != 0) { | ||||||||||||||
|
|
@@ -1352,7 +1357,7 @@ void serveJson(AsyncWebServerRequest* request) | |||||||||||||
| case json_target::palettes: | ||||||||||||||
| serializePalettes(lDoc, request->hasParam(F("page")) ? request->getParam(F("page"))->value().toInt() : 0); break; | ||||||||||||||
| case json_target::effects: | ||||||||||||||
| serializeModeNames(lDoc); break; | ||||||||||||||
| serializeModeNames(lDoc, request->hasParam(F("all"))); break; | ||||||||||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Parse Line 1360 currently enables bypass for any Suggested fix- serializeModeNames(lDoc, request->hasParam(F("all"))); break;
+ bool bypassHide = false;
+ if (request->hasParam(F("all"))) {
+ bypassHide = request->getParam(F("all"))->value().toInt() == 1;
+ }
+ serializeModeNames(lDoc, bypassHide); break;📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||
| case json_target::networks: | ||||||||||||||
| serializeNetworks(lDoc); break; | ||||||||||||||
| case json_target::config: | ||||||||||||||
|
|
||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -13,8 +13,8 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage) | |
| return; | ||
| } | ||
|
|
||
| //0: menu 1: wifi 2: leds 3: ui 4: sync 5: time 6: sec 7: DMX 8: usermods 9: N/A 10: 2D | ||
| if (subPage < 1 || subPage > 10 || !correctPIN) return; | ||
| // see SUBPAGE_* in const.h | ||
| if (subPage < 1 || subPage > SUBPAGE_LAST || !correctPIN) return; | ||
|
|
||
| //WIFI SETTINGS | ||
| if (subPage == SUBPAGE_WIFI) | ||
|
|
@@ -877,6 +877,20 @@ void handleSettingsSet(AsyncWebServerRequest *request, byte subPage) | |
| } | ||
| #endif | ||
|
|
||
| if (subPage == SUBPAGE_FX) | ||
| { | ||
| for (size_t i = 0; i < 8; i++) hiddenFxMask[i] = 0; | ||
| String s = request->arg(F("FXH")); | ||
| int idx = 0; | ||
| while (idx < (int)s.length()) { | ||
| int comma = s.indexOf(',', idx); | ||
| int end = (comma < 0) ? s.length() : comma; | ||
| int id = s.substring(idx, end).toInt(); | ||
| if (id > 0 && id < 256) setFxHidden((uint8_t)id, true); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Clamp hidden effect IDs to active mode range. Line 889 accepts any ID Suggested fix- if (id > 0 && id < 256) setFxHidden((uint8_t)id, true);
+ if (id > 0 && id < 256 && (size_t)id < strip.getModeCount()) {
+ setFxHidden((uint8_t)id, true);
+ }🤖 Prompt for AI Agents |
||
| idx = (comma < 0) ? s.length() : comma + 1; | ||
| } | ||
| } | ||
|
|
||
| lastEditTime = millis(); | ||
| // do not save if factory reset or LED settings (which are saved after LED re-init) | ||
| configNeedsWrite = subPage != SUBPAGE_LEDS && !(subPage == SUBPAGE_SEC && doReboot); | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -752,6 +752,7 @@ void serveSettings(AsyncWebServerRequest* request, bool post) { | |||||
| else if (url.indexOf( "2D") > 0) subPage = SUBPAGE_2D; | ||||||
| #endif | ||||||
| else if (url.indexOf(F("pins")) > 0) subPage = SUBPAGE_PINS; | ||||||
| else if (url.indexOf(F("fx")) > 0) subPage = SUBPAGE_FX; | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Use a more specific URL pattern to avoid false matches. The pattern 🔍 Proposed refinement- else if (url.indexOf(F("fx")) > 0) subPage = SUBPAGE_FX;
+ else if (url.indexOf(F("/fx")) > 0) subPage = SUBPAGE_FX;Alternatively, if you want to be even more strict, check for - else if (url.indexOf(F("fx")) > 0) subPage = SUBPAGE_FX;
+ else if (url.indexOf(F("settings/fx")) >= 0) subPage = SUBPAGE_FX;📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||
| else if (url.indexOf(F("lock")) > 0) subPage = SUBPAGE_LOCK; | ||||||
| } | ||||||
| else if (url.indexOf("/update") >= 0) subPage = SUBPAGE_UPDATE; // update page, for PIN check | ||||||
|
|
@@ -796,6 +797,7 @@ void serveSettings(AsyncWebServerRequest* request, bool post) { | |||||
| case SUBPAGE_2D : strcpy_P(s, PSTR("2D")); break; | ||||||
| #endif | ||||||
| case SUBPAGE_PINREQ : strcpy_P(s, correctPIN ? PSTR("PIN accepted") : PSTR("PIN rejected")); break; | ||||||
| case SUBPAGE_FX : strcpy_P(s, PSTR("Effect Visibility")); break; | ||||||
| } | ||||||
|
|
||||||
| if (subPage != SUBPAGE_PINREQ) strcat_P(s, PSTR(" settings saved.")); | ||||||
|
|
@@ -846,6 +848,7 @@ void serveSettings(AsyncWebServerRequest* request, bool post) { | |||||
| case SUBPAGE_2D : content = PAGE_settings_2D; len = PAGE_settings_2D_length; break; | ||||||
| #endif | ||||||
| case SUBPAGE_PINS : content = PAGE_settings_pininfo; len = PAGE_settings_pininfo_length; break; | ||||||
| case SUBPAGE_FX : content = PAGE_settings_fx; len = PAGE_settings_fx_length; break; | ||||||
| case SUBPAGE_LOCK : { | ||||||
| correctPIN = !strlen(settingsPIN); // lock if a pin is set | ||||||
| serveMessage(request, 200, strlen(settingsPIN) > 0 ? PSTR("Settings locked") : PSTR("No PIN set"), FPSTR(s_redirecting), 1); | ||||||
|
|
||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Guard
fx.hiddenIDs before narrowing touint8_t.v.as<uint8_t>()can wrap out-of-range JSON values (e.g.,300 -> 44), hiding unintended effects.Suggested fix
🤖 Prompt for AI Agents