From 85b7337b6898bd9b0ea107c39d6429d2f6ea8086 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Padez?= Date: Thu, 30 Jul 2026 01:27:10 +0000 Subject: [PATCH] soulseek: let a filtered row open into its real contents MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit the tree search asked for no limit and clampInt read a missing param as 0 (Number(null) is 0, and 0 is finite), so it clamped to the minimum and ran with LIMIT 1: one match came back, its siblings looked like non-matches, and a folder with 25 matching albums showed none of them. on top of that, a filtered row was a dead end — it only ever showed the matches, with a note counting what it was hiding. now the note is the same "show all N folders" button browse mode already had: clicking it drops that row out of the filtered set, so from there down it is ordinary lazy browsing, which is usually why you searched for the folder in the first place. dropped the "don't match the filter" wording with it, since a truncated result makes that sentence false. Co-Authored-By: Claude Opus 4.8 --- src/servers/sidecar/slskd/officer.ts | 4 +++ .../src/apps/Soulseek/SharesBrowser.tsx | 35 ++++++++++++------- 2 files changed, 27 insertions(+), 12 deletions(-) diff --git a/src/servers/sidecar/slskd/officer.ts b/src/servers/sidecar/slskd/officer.ts index 1d60fb9d..701c4865 100644 --- a/src/servers/sidecar/slskd/officer.ts +++ b/src/servers/sidecar/slskd/officer.ts @@ -58,6 +58,10 @@ const DIR_PAGE_DEFAULT = 200; const SEARCH_MATCH_MAX = 1000; const SEARCH_MATCH_DEFAULT = 300; const clampInt = (raw: string | null, fallback: number, min: number, max: number): number => { + // An absent param has to be caught before Number(): `Number(null)` is 0, not NaN, so it passed the + // finite check and clamped to `min` — which ran the tree search with LIMIT 1 and returned a single + // match, making its siblings look like non-matches in the UI. + if (raw === null || raw.trim() === '') return fallback; const n = Number(raw); if (!Number.isFinite(n)) return fallback; return Math.min(max, Math.max(min, Math.trunc(n))); diff --git a/src/workspaces/officerdev/src/apps/Soulseek/SharesBrowser.tsx b/src/workspaces/officerdev/src/apps/Soulseek/SharesBrowser.tsx index 212f8f74..65e15b42 100644 --- a/src/workspaces/officerdev/src/apps/Soulseek/SharesBrowser.tsx +++ b/src/workspaces/officerdev/src/apps/Soulseek/SharesBrowser.tsx @@ -248,17 +248,27 @@ type TreeNodeProps = { const TreeNode = ({ username, node, filtered, indent, initialOpen }: TreeNodeProps) => { const [open, setOpen] = useState(initialOpen); const [limit, setLimit] = useState(LEVEL_LIMIT); + const [revealed, setRevealed] = useState(false); + + // Filtering stops at a revealed row: from here down it's ordinary lazy browsing, so one click gets you + // out of the filtered skeleton and into the real folder — which is usually why you searched. + const scoped = revealed ? null : filtered; const expandable = node.childCount > 0 || node.fileCount > 0; const level = useSoulseekBrowseLevel({ username, parent: node.name, limit, - enabled: !filtered && open && node.childCount > 0, + enabled: !scoped && open && node.childCount > 0, }); - const children = filtered ? (filtered.childrenOf.get(node.name) ?? []) : (level.data?.nodes ?? []); - const shown = filtered ? children.length : (level.data?.total ?? 0); + const children = scoped ? (scoped.childrenOf.get(node.name) ?? []) : (level.data?.nodes ?? []); + // In the filtered skeleton no level was fetched, so the row's own count is what says how many exist. + const total = scoped ? node.childCount : (level.data?.total ?? node.childCount); + const capped = !scoped && limit >= LEVEL_LIMIT_MAX; + // Not while a level is in flight: `total` falls back to the row's own count then, so every loading row + // would flash a "show all" button for children it's already fetching. + const more = open && total > children.length && (!!scoped || !level.isPending); const isMatch = filtered?.matched.has(node.name) ?? false; return ( @@ -299,7 +309,7 @@ const TreeNode = ({ username, node, filtered, indent, initialOpen }: TreeNodePro {open && ( <> - {!filtered && level.isPending && node.childCount > 0 && ( + {!scoped && level.isPending && node.childCount > 0 && (

))} - {/* Wide levels load in one go rather than paging — there's nothing to leaf through inside a folder. */} - {!filtered && shown > children.length && children.length > 0 && ( + {/* One button for both modes: in the filtered skeleton it drops this row out of the filter, in + browse mode it widens the level. Wide levels load in one go — there's nothing to page through. */} + {more && !capped && ( )} - {filtered && node.childCount > children.length && ( + {more && capped && (

- {(node.childCount - children.length).toLocaleString()} more folders here don't match the filter. + Showing {children.length.toLocaleString()} of {total.toLocaleString()} folders.

)} {node.fileCount > 0 && }