mirror of
https://github.com/noctalia-dev/noctalia-shell.git
synced 2026-05-11 17:08:27 +08:00
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:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user