Skip to content

Commit b7bf2ea

Browse files
author
Alexander Phillips
committed
Debounce folder regex typing path
1 parent f661f82 commit b7bf2ea

6 files changed

Lines changed: 57 additions & 16 deletions

File tree

archive/folderview.plus-2026.04.14.01.txz.sha256

Lines changed: 0 additions & 1 deletion
This file was deleted.
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
757be4e0eff2276fd286894c788f01a6056e1a20da57b253f2f5c8fc5948f391 folderview.plus-2026.04.15.11.txz

folderview.plus.plg

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,18 @@
66
<!ENTITY launch "Settings/FolderViewPlus">
77
<!ENTITY plugdir "/usr/local/emhttp/plugins/&name;">
88
<!ENTITY pluginURL "https://raw.githubusercontent.com/&github;/dev/folderview.plus.plg">
9-
<!ENTITY version "2026.04.15.10">
10-
<!ENTITY md5 "7cb604ebba883c89e58430b61f88972a">
9+
<!ENTITY version "2026.04.15.11">
10+
<!ENTITY md5 "d01fe1451050c8cb7d527a719108af02">
1111
]>
1212

1313
<PLUGIN name="&name;" author="&author;" version="&version;" launch="&launch;" pluginURL="&pluginURL;" icon="folder-icon.png" support="https://forums.unraid.net/topic/197631-plugin-folderview-plus/" min="7.0.0">
1414
<CHANGES>
1515

16+
###2026.04.15.11
17+
- Perf: Debounce legacy folder-regex matching while typing and stop running the duplicate editor recalculation path on every regex keystroke.
18+
- Perf: Avoid the extra pre-worker member-table redraw so large regex edits render once per settled pattern instead of twice.
19+
20+
1621
###2026.04.15.10
1722
- Fix: Settings privacy mode no longer blurs the Folder Defaults source picker or saved-default summary.
1823
- UX: Folder Defaults action buttons now use the same FolderView Plus settings button styling as the rest of the plugin.

src/folderview.plus/usr/local/emhttp/plugins/folderview.plus/scripts/folder.js

Lines changed: 43 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -301,6 +301,7 @@ const CUSTOM_ICON_SEARCH_DEBOUNCE_MS = 150;
301301
const THIRD_PARTY_ICON_SEARCH_DEBOUNCE_MS = 140;
302302
const EDITOR_INPUT_RECALC_DEBOUNCE_MS = 90;
303303
const NAME_REGEX_SYNC_DEBOUNCE_MS = 150;
304+
const REGEX_INPUT_SYNC_DEBOUNCE_MS = 120;
304305
const MEMBER_LIST_RENDER_CHUNK_SIZE = 140;
305306
const REGEX_WORKER_MIN_ITEMS = 180;
306307
const THIRD_PARTY_RECENT_LIMIT = 36;
@@ -382,6 +383,7 @@ let isFormInitialized = false;
382383
let suppressUnloadPrompt = false;
383384
let editorRecalcTimer = null;
384385
let nameRegexSyncTimer = null;
386+
let regexInputSyncTimer = null;
385387
let lastNameRegexSyncValue = '';
386388
let memberListRenderToken = 0;
387389
let regexWorker = null;
@@ -1508,7 +1510,7 @@ const runNameDrivenRegexSync = () => {
15081510
return;
15091511
}
15101512
lastNameRegexSyncValue = nextName;
1511-
updateRegex(form.regex);
1513+
updateRegex(form.regex, { immediate: true });
15121514
};
15131515

15141516
const scheduleNameDrivenRegexSync = (mode = 'debounced') => {
@@ -2569,7 +2571,7 @@ const suggestDefaultsFromMembers = () => {
25692571
}
25702572
});
25712573
updateIcon(form.icon);
2572-
updateRegex(form.regex);
2574+
updateRegex(form.regex, { immediate: true });
25732575
updateForm();
25742576
validateForm();
25752577
updateLiveSummary();
@@ -3390,7 +3392,7 @@ const startFolderEditorRuntime = async () => {
33903392

33913393
// make the ui respond to the previus changes
33923394
updateForm();
3393-
updateRegex(getFormField(form, 'regex'));
3395+
updateRegex(getFormField(form, 'regex'), { immediate: true });
33943396
updateIcon(getFormField(form, 'icon'));
33953397
setParentDefaultsNote('');
33963398
}
@@ -3467,6 +3469,14 @@ const startFolderEditorRuntime = async () => {
34673469
}
34683470
scheduleNameDrivenRegexSync('immediate');
34693471
}
3472+
if (fieldName === 'regex') {
3473+
if (event.type === 'input') {
3474+
markUnsavedIndicatorDirty();
3475+
return;
3476+
}
3477+
updateRegex(form.regex, { immediate: true });
3478+
return;
3479+
}
34703480
if (fieldName === 'dropdown_style' || fieldName === 'dropdown_color' || fieldName === 'dropdown_hover_color'
34713481
|| fieldName === 'preview_border' || fieldName === 'preview_border_color' || fieldName === 'preview_border_width'
34723482
|| fieldName === 'folder_accent_enabled' || fieldName === 'folder_accent_color') {
@@ -3619,21 +3629,23 @@ const mergeMembersByName = (baseMembers, candidateMembers) => {
36193629
return ordered;
36203630
};
36213631

3622-
/**
3623-
* Update the regex selection when editing the respective field
3624-
* @param {*} e the element
3625-
*/
3626-
const updateRegex = (e) => {
3632+
const evaluateRegexSelection = (e) => {
3633+
regexInputSyncTimer = null;
3634+
const regexField = e || getFormField(getForm(), 'regex');
3635+
if (!regexField) {
3636+
return false;
3637+
}
3638+
const requestId = ++latestRegexEvaluationRequestId;
36273639
syncMemberArraysFromTable();
36283640
choose = choose.concat(selectedRegex);
36293641
const fldName = ($('[name="name"]')[0].value || '').trim();
36303642
if (fldName) {
3631-
selectedRegex = choose.filter(el => el.Label === fldName);
3632-
choose = choose.filter(el => el.Label !== fldName);
3643+
selectedRegex = choose.filter((el) => el.Label === fldName);
3644+
choose = choose.filter((el) => el.Label !== fldName);
36333645
} else {
36343646
selectedRegex = [];
36353647
}
3636-
const regexSource = String(e?.value || '').trim();
3648+
const regexSource = String(regexField.value || '').trim();
36373649
if (!regexSource) {
36383650
updateList();
36393651
updateRegexSimulator();
@@ -3648,7 +3660,6 @@ const updateRegex = (e) => {
36483660
}
36493661

36503662
const baseChoose = choose.slice();
3651-
const requestId = ++latestRegexEvaluationRequestId;
36523663
const applyMatchResult = (matchNames) => {
36533664
if (requestId !== latestRegexEvaluationRequestId) {
36543665
return;
@@ -3700,11 +3711,30 @@ const updateRegex = (e) => {
37003711
applyMatchResult(matchedNames);
37013712
});
37023713

3703-
updateList();
37043714
updateRegexSimulator();
37053715
return true;
37063716
};
37073717

3718+
/**
3719+
* Update the regex selection when editing the respective field
3720+
* @param {*} e the element
3721+
*/
3722+
const updateRegex = (e, options = {}) => {
3723+
const immediate = options && typeof options === 'object' && options.immediate === true;
3724+
if (regexInputSyncTimer) {
3725+
clearTimeout(regexInputSyncTimer);
3726+
regexInputSyncTimer = null;
3727+
}
3728+
latestRegexEvaluationRequestId += 1;
3729+
if (immediate || !isFormInitialized) {
3730+
return evaluateRegexSelection(e);
3731+
}
3732+
regexInputSyncTimer = setTimeout(() => {
3733+
evaluateRegexSelection(e);
3734+
}, REGEX_INPUT_SYNC_DEBOUNCE_MS);
3735+
return true;
3736+
};
3737+
37083738
/**
37093739
* Update the setting visibility according to the preview setting
37103740
* @param {*} e the element

tests/performance-optimizations.test.mjs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -325,11 +325,17 @@ test('settings/runtime scripts use batched localStorage writes', () => {
325325
test('folder editor avoids synchronous large-list stalls via chunking and worker-backed regex matching', () => {
326326
assert.match(folderEditorJs, /const MEMBER_LIST_RENDER_CHUNK_SIZE = \d+;/);
327327
assert.match(folderEditorJs, /const REGEX_WORKER_MIN_ITEMS = \d+;/);
328+
assert.match(folderEditorJs, /const REGEX_INPUT_SYNC_DEBOUNCE_MS = \d+;/);
328329
assert.match(folderEditorJs, /const getRegexWorker = \(\) =>/);
329330
assert.match(folderEditorJs, /const runRegexMatch = async \(pattern,\s*names\) =>/);
331+
assert.match(folderEditorJs, /const evaluateRegexSelection = \(e\) =>/);
332+
assert.match(folderEditorJs, /const updateRegex = \(e,\s*options = \{\}\) =>/);
330333
assert.match(folderEditorJs, /const mergeMembersByName = \(baseMembers,\s*candidateMembers\) =>/);
331334
assert.match(folderEditorJs, /if \(rows\.length <= MEMBER_LIST_RENDER_CHUNK_SIZE\) \{/);
332335
assert.match(folderEditorJs, /scheduleAnimationFrameTask\(appendChunk\)/);
336+
assert.match(folderEditorJs, /if \(fieldName === 'regex'\) \{\s*if \(event\.type === 'input'\) \{\s*markUnsavedIndicatorDirty\(\);\s*return;\s*\}\s*updateRegex\(form\.regex,\s*\{\s*immediate:\s*true\s*\}\);\s*return;\s*\}/);
337+
assert.match(folderEditorJs, /regexInputSyncTimer = setTimeout\(\(\) => \{\s*evaluateRegexSelection\(e\);\s*\}, REGEX_INPUT_SYNC_DEBOUNCE_MS\);/);
338+
assert.doesNotMatch(folderEditorJs, /runRegexMatch\(regexSource,\s*baseChoose\.map\(\(member\) => member\.Name\)\)\s*[\s\S]*updateList\(\);\s*updateRegexSimulator\(\);\s*return true;/);
333339
});
334340

335341
test('folder editor save queues docker order sync off the submit critical path in both runtimes', () => {

0 commit comments

Comments
 (0)