diff --git a/.claude/settings.local.json b/.claude/settings.local.json index ed09d09..8d0aa47 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -19,7 +19,16 @@ "Bash(gh repo:*)", "Bash(git remote:*)", "Bash(git add:*)", - "Bash(git commit:*)" + "Bash(git commit:*)", + "Bash(npm list:*)", + "Bash(npm show:*)", + "Bash(gsd --version)", + "Bash(gsd version:*)", + "Bash(python3 -c \"import sys,json; d=json.load\\(sys.stdin\\); print\\(d.get\\('version', 'no version field'\\)\\)\")", + "Bash(python3 -c \"import sys,json; d=json.load\\(sys.stdin\\); print\\(json.dumps\\(d, indent=2\\)\\)\")", + "Bash(gsd)", + "Read(//home/aux/.config/**)", + "Read(//home/aux/**)" ] } } diff --git a/src/frontend/src/contexts/DownloadProgressContext.tsx b/src/frontend/src/contexts/DownloadProgressContext.tsx index 8862030..d0df508 100644 --- a/src/frontend/src/contexts/DownloadProgressContext.tsx +++ b/src/frontend/src/contexts/DownloadProgressContext.tsx @@ -161,6 +161,8 @@ interface DownloadProgressContextValue { scanStoreSubscribe: (listener: () => void) => () => void; /** Get scan store snapshot */ scanStoreGetSnapshot: () => Map; + /** Clear scan state for a channel (optimistic update) */ + clearScan: (channelId: number) => void; } const DownloadProgressContext = createContext(null); @@ -252,6 +254,7 @@ export function DownloadProgressProvider({ children }: { children: ReactNode }) isConnected, scanStoreSubscribe: scanStore.subscribe, scanStoreGetSnapshot: scanStore.getSnapshot, + clearScan: scanStore.clearScan.bind(scanStore), }} > {children} @@ -288,7 +291,7 @@ export function useDownloadProgressConnection(): boolean { * Returns `{ scanning, newItemCount }` from the scan store via useSyncExternalStore. * Only re-renders components that use this hook when the scan store changes. */ -export function useScanProgress(channelId: number): ScanProgress { +export function useScanProgress(channelId: number): ScanProgress & { clearScan: () => void } { const context = useContext(DownloadProgressContext); if (!context) { throw new Error('useScanProgress must be used within a DownloadProgressProvider'); @@ -297,7 +300,11 @@ export function useScanProgress(channelId: number): ScanProgress { context.scanStoreSubscribe, context.scanStoreGetSnapshot, ); - return scanMap.get(channelId) ?? { scanning: false, newItemCount: 0 }; + return { + scanning: scanMap.get(channelId)?.scanning ?? false, + newItemCount: scanMap.get(channelId)?.newItemCount ?? 0, + clearScan: () => context.clearScan(channelId), + }; } // ── Cache Injection Helper ── diff --git a/src/frontend/src/pages/ChannelDetail.tsx b/src/frontend/src/pages/ChannelDetail.tsx index 1bbf8a9..c55df0f 100644 --- a/src/frontend/src/pages/ChannelDetail.tsx +++ b/src/frontend/src/pages/ChannelDetail.tsx @@ -162,7 +162,7 @@ export function ChannelDetail() { const updateContentRating = useUpdateContentRating(channelId); // ── Scan state (WebSocket-driven) ── - const { scanning: scanInProgress, newItemCount: scanNewItemCount } = useScanProgress(channelId); + const { scanning: scanInProgress, newItemCount: scanNewItemCount, clearScan } = useScanProgress(channelId); // ── Local state ── const [showDeleteConfirm, setShowDeleteConfirm] = useState(false); @@ -299,7 +299,7 @@ export function ChannelDetail() { if (result.status === 'already_running') { toast('Scan already in progress', 'info'); } else if (result.status === 'started') { - toast('Scan started — items will appear as they\'re found', 'success'); + toast('Scan started', 'info'); } }, onError: (err) => { @@ -309,6 +309,8 @@ export function ChannelDetail() { }, [scanChannel, toast]); const handleCancelScan = useCallback(() => { + // Optimistic update: clear scan state immediately + clearScan(); cancelScan.mutate(undefined, { onSuccess: (result) => { if (result.cancelled) { @@ -321,7 +323,7 @@ export function ChannelDetail() { toast(err instanceof Error ? err.message : 'Failed to cancel scan', 'error'); }, }); - }, [cancelScan, toast]); + }, [cancelScan, toast, clearScan]); const handleCollect = useCallback(() => { collectMonitored.mutate(undefined, { @@ -1034,7 +1036,7 @@ export function ChannelDetail() { disabled={scanChannel.isPending || cancelScan.isPending} title={scanInProgress ? 'Cancel scan' : 'Refresh & Scan'} className={scanInProgress ? 'btn btn-danger' : 'btn btn-ghost'} - style={{ padding: '4px 10px', fontSize: 'var(--font-size-xs)', opacity: (scanChannel.isPending || cancelScan.isPending) ? 0.6 : 1 }} + style={{ padding: '4px 10px', fontSize: 'var(--font-size-xs)', minWidth: 120, opacity: (scanChannel.isPending || cancelScan.isPending) ? 0.6 : 1 }} > {scanInProgress ? ( @@ -1043,7 +1045,7 @@ export function ChannelDetail() { ) : ( )} - {scanInProgress ? `Stop (${scanNewItemCount})` : 'Scan'} + {scanInProgress ? 'Stop Scan' : scanChannel.isPending ? 'Starting…' : 'Scan'}