slskd: /soulseek search + downloads on the Workspace/Panel framework

Two panels (search + transfers) via WorkspaceView, coordinating over the
soulseek:refresh channel. Adds the page-title rule and documents the
route conventions in CLAUDE.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-29 17:21:29 +00:00
co-authored by Claude Opus 4.8
parent c023082975
commit 2a509646e7
15 changed files with 577 additions and 4 deletions
@@ -0,0 +1,45 @@
import { useEffect, useMemo } from 'react';
import type { LayoutNode } from 'officerdev';
import { WorkspaceView } from 'officerdev';
import { useDashboardState } from 'state/useDashboardState';
import { defaultLayout } from './defaultLayout';
// /soulseek uses the Workspace/Panel system (like /chat and /music): two panels — the search browser
// (soulseek-search) and the download queue (soulseek-transfers) — coordinating via the 'soulseek:refresh'
// panel channel. Both talk to slskd through the /api/slskd auth proxy.
const ALLOWED_APP_TYPES = new Set<string | null>(['soulseek-search', 'soulseek-transfers', null]);
function normalizeLayout(node: LayoutNode): LayoutNode {
if (node.type === 'panel') {
return ALLOWED_APP_TYPES.has(node.appType) ? node : { ...node, appType: 'soulseek-search' };
}
const children = node.children.map((c) => {
const fixed = normalizeLayout(c.node);
return fixed === c.node ? c : { ...c, node: fixed };
});
const changed = children.some((c, i) => c !== node.children[i]);
return changed ? { ...node, children } : node;
}
export const SoulseekScreen = () => {
const rawWorkspace = useDashboardState<LayoutNode>('screens/soulseek', defaultLayout);
const workspace = useMemo(() => {
const fixed = normalizeLayout(rawWorkspace.value);
if (fixed === rawWorkspace.value) return rawWorkspace;
return { ...rawWorkspace, value: fixed };
}, [rawWorkspace]);
useEffect(() => {
if (rawWorkspace.isLoaded && workspace.value !== rawWorkspace.value) {
rawWorkspace.setValue(workspace.value);
}
}, [rawWorkspace.isLoaded, workspace.value, rawWorkspace.value]);
return (
<div className="h-full w-full pt-2">
<WorkspaceView workspace={workspace} locked />
</div>
);
};
@@ -0,0 +1,11 @@
import type { LayoutNode } from 'officerdev';
export const defaultLayout: LayoutNode = {
type: 'group',
id: 'soulseek-root',
direction: 'horizontal',
children: [
{ node: { type: 'panel', id: 'soulseek-search', appType: 'soulseek-search' }, size: 55 },
{ node: { type: 'panel', id: 'soulseek-transfers', appType: 'soulseek-transfers' }, size: 45 },
],
};
@@ -0,0 +1 @@
export * from './SoulseekScreen';