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 && }