feat(settings): improved search index to support visibility conditions, prevent showing results that would be invisible to the user. Fix #2113

This commit is contained in:
Lemmy
2026-03-11 10:07:30 -04:00
parent 8005958b8e
commit d35f399f53
3 changed files with 451 additions and 61 deletions
+142 -1
View File
@@ -25,6 +25,8 @@ import qs.Modules.Panels.Settings.Tabs.SessionMenu
import qs.Modules.Panels.Settings.Tabs.SystemMonitor
import qs.Modules.Panels.Settings.Tabs.UserInterface
import qs.Modules.Panels.Settings.Tabs.Wallpaper
import qs.Services.Compositor
import qs.Services.Power
import qs.Services.System
import qs.Services.UI
import qs.Widgets
@@ -90,6 +92,143 @@ Item {
}
}
// Visibility condition evaluation for search filtering.
// Resolves a dotted property path on a known root object.
function _resolveValue(path) {
const roots = {
"CompositorService": CompositorService,
"Settings": Settings,
"Quickshell": Quickshell,
"IdleService": IdleService,
"SystemStatService": SystemStatService,
"SoundService": SoundService
};
const parts = path.split(".");
const rootObj = roots[parts[0]];
if (rootObj === undefined)
return undefined;
let obj = rootObj;
for (let i = 1; i < parts.length; i++) {
if (obj === undefined || obj === null)
return undefined;
// Strip optional chaining marker (e.g. "sounds?" -> "sounds")
let key = parts[i];
if (key.endsWith("?"))
key = key.slice(0, -1);
obj = obj[key];
}
return obj;
}
// Split an expression on top-level && (respecting parentheses).
function _splitAnd(expr) {
const parts = [];
let depth = 0;
let current = "";
for (let i = 0; i < expr.length; i++) {
const ch = expr[i];
if (ch === "(")
depth++;
else if (ch === ")")
depth--;
else if (depth === 0 && ch === "&" && i + 1 < expr.length && expr[i + 1] === "&") {
parts.push(current);
current = "";
i++; // skip second &
continue;
}
current += ch;
}
parts.push(current);
return parts;
}
// Evaluate a single visibility condition expression.
// Returns true if the condition is met (item should be visible).
// Falls back to true for unrecognized expressions.
function _evalCondition(expr) {
expr = expr.trim();
// Strip outer parentheses
if (expr.startsWith("(") && expr.endsWith(")")) {
let depth = 0;
let allWrapped = true;
for (let i = 0; i < expr.length - 1; i++) {
if (expr[i] === "(")
depth++;
else if (expr[i] === ")")
depth--;
if (depth === 0) {
allWrapped = false;
break;
}
}
if (allWrapped)
return _evalCondition(expr.slice(1, -1));
}
// AND: all parts must be true
if (expr.includes("&&")) {
const parts = _splitAnd(expr);
if (parts.length > 1) {
for (let i = 0; i < parts.length; i++) {
if (!_evalCondition(parts[i]))
return false;
}
return true;
}
}
// Negation
if (expr.startsWith("!"))
return !_evalCondition(expr.slice(1).trim());
// Literal false
if (expr === "false")
return false;
// Strip nullish coalescing fallback (e.g. "expr ?? false")
const nullishMatch = expr.match(/^(.+?)\s*\?\?\s*(?:false|true)\s*$/);
if (nullishMatch)
return _evalCondition(nullishMatch[1]);
// === comparison
let m = expr.match(/^(.+?)\s*===\s*"([^"]*)"\s*$/);
if (m)
return _resolveValue(m[1].trim()) === m[2];
// !== comparison
m = expr.match(/^(.+?)\s*!==\s*"([^"]*)"\s*$/);
if (m)
return _resolveValue(m[1].trim()) !== m[2];
// > comparison
m = expr.match(/^(.+?)\s*>\s*(\d+)\s*$/);
if (m)
return _resolveValue(m[1].trim()) > parseInt(m[2]);
// Simple property path — resolve and return truthiness
const val = _resolveValue(expr);
if (val !== undefined)
return !!val;
// Unrecognized expression — assume visible
return true;
}
// Check if a search index entry is currently visible.
function _isEntryVisible(entry) {
if (!entry.visibleWhen || entry.visibleWhen.length === 0)
return true;
for (let i = 0; i < entry.visibleWhen.length; i++) {
if (!_evalCondition(entry.visibleWhen[i]))
return false;
}
return true;
}
// Search function
onSearchTextChanged: {
if (searchText.trim() === "") {
@@ -113,10 +252,12 @@ Item {
if (searchIndex.length === 0)
return;
// Build searchable items with resolved translations
// Build searchable items with resolved translations, filtering out invisible entries
let items = [];
for (let j = 0; j < searchIndex.length; j++) {
const entry = searchIndex[j];
if (!_isEntryVisible(entry))
continue;
items.push({
"labelKey": entry.labelKey,
"descriptionKey": entry.descriptionKey,