soulseek: let a filtered row open into its real contents

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 <noreply@anthropic.com>
This commit is contained in:
2026-07-30 01:27:10 +00:00
co-authored by Claude Opus 4.8
parent 1159c0787d
commit 85b7337b68
2 changed files with 27 additions and 12 deletions
+4
View File
@@ -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)));
@@ -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 && (
<p
style={{ paddingLeft: 12 + (indent + 1) * 14 }}
className="flex items-center gap-1.5 py-1 text-xs text-zinc-500"
@@ -312,25 +322,26 @@ const TreeNode = ({ username, node, filtered, indent, initialOpen }: TreeNodePro
key={child.id}
username={username}
node={child}
filtered={filtered}
filtered={scoped}
indent={indent + 1}
initialOpen={!!filtered}
initialOpen={!!scoped}
/>
))}
{/* 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 && (
<button
type="button"
onClick={() => setLimit(Math.min(shown, LEVEL_LIMIT_MAX))}
onClick={() => (scoped ? setRevealed(true) : setLimit(Math.min(total, LEVEL_LIMIT_MAX)))}
style={{ paddingLeft: 12 + (indent + 1) * 14 }}
className="py-1 text-xs text-primary/80 transition hover:text-primary"
>
Show all {shown.toLocaleString()} folders
Show all {total.toLocaleString()} folders
</button>
)}
{filtered && node.childCount > children.length && (
{more && capped && (
<p style={{ paddingLeft: 12 + (indent + 1) * 14 }} className="py-1 text-xs text-zinc-600">
{(node.childCount - children.length).toLocaleString()} more folders here don't match the filter.
Showing {children.length.toLocaleString()} of {total.toLocaleString()} folders.
</p>
)}
{node.fileCount > 0 && <DirFiles username={username} dirId={node.id} indent={indent + 1} />}