fix: scan button visual consistency and responsive cancel
All checks were successful
CI / test (push) Successful in 23s
All checks were successful
CI / test (push) Successful in 23s
- Unified toast variant (info) for scan start/cancel - Added minWidth to scan buttons to prevent layout shift - Simplified button text (Stop Scan vs Stop Scan (X Found)) - Optimistic cancel via clearScan() for instant button response - Shows Starting… state instead of disabled during scan init
This commit is contained in:
parent
3e21acfe2b
commit
b633a23cfb
3 changed files with 28 additions and 10 deletions
|
|
@ -19,7 +19,16 @@
|
||||||
"Bash(gh repo:*)",
|
"Bash(gh repo:*)",
|
||||||
"Bash(git remote:*)",
|
"Bash(git remote:*)",
|
||||||
"Bash(git add:*)",
|
"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/**)"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -161,6 +161,8 @@ interface DownloadProgressContextValue {
|
||||||
scanStoreSubscribe: (listener: () => void) => () => void;
|
scanStoreSubscribe: (listener: () => void) => () => void;
|
||||||
/** Get scan store snapshot */
|
/** Get scan store snapshot */
|
||||||
scanStoreGetSnapshot: () => Map<number, ScanProgress>;
|
scanStoreGetSnapshot: () => Map<number, ScanProgress>;
|
||||||
|
/** Clear scan state for a channel (optimistic update) */
|
||||||
|
clearScan: (channelId: number) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
const DownloadProgressContext = createContext<DownloadProgressContextValue | null>(null);
|
const DownloadProgressContext = createContext<DownloadProgressContextValue | null>(null);
|
||||||
|
|
@ -252,6 +254,7 @@ export function DownloadProgressProvider({ children }: { children: ReactNode })
|
||||||
isConnected,
|
isConnected,
|
||||||
scanStoreSubscribe: scanStore.subscribe,
|
scanStoreSubscribe: scanStore.subscribe,
|
||||||
scanStoreGetSnapshot: scanStore.getSnapshot,
|
scanStoreGetSnapshot: scanStore.getSnapshot,
|
||||||
|
clearScan: scanStore.clearScan.bind(scanStore),
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{children}
|
{children}
|
||||||
|
|
@ -288,7 +291,7 @@ export function useDownloadProgressConnection(): boolean {
|
||||||
* Returns `{ scanning, newItemCount }` from the scan store via useSyncExternalStore.
|
* Returns `{ scanning, newItemCount }` from the scan store via useSyncExternalStore.
|
||||||
* Only re-renders components that use this hook when the scan store changes.
|
* 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);
|
const context = useContext(DownloadProgressContext);
|
||||||
if (!context) {
|
if (!context) {
|
||||||
throw new Error('useScanProgress must be used within a DownloadProgressProvider');
|
throw new Error('useScanProgress must be used within a DownloadProgressProvider');
|
||||||
|
|
@ -297,7 +300,11 @@ export function useScanProgress(channelId: number): ScanProgress {
|
||||||
context.scanStoreSubscribe,
|
context.scanStoreSubscribe,
|
||||||
context.scanStoreGetSnapshot,
|
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 ──
|
// ── Cache Injection Helper ──
|
||||||
|
|
|
||||||
|
|
@ -162,7 +162,7 @@ export function ChannelDetail() {
|
||||||
const updateContentRating = useUpdateContentRating(channelId);
|
const updateContentRating = useUpdateContentRating(channelId);
|
||||||
|
|
||||||
// ── Scan state (WebSocket-driven) ──
|
// ── Scan state (WebSocket-driven) ──
|
||||||
const { scanning: scanInProgress, newItemCount: scanNewItemCount } = useScanProgress(channelId);
|
const { scanning: scanInProgress, newItemCount: scanNewItemCount, clearScan } = useScanProgress(channelId);
|
||||||
|
|
||||||
// ── Local state ──
|
// ── Local state ──
|
||||||
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
|
const [showDeleteConfirm, setShowDeleteConfirm] = useState(false);
|
||||||
|
|
@ -299,7 +299,7 @@ export function ChannelDetail() {
|
||||||
if (result.status === 'already_running') {
|
if (result.status === 'already_running') {
|
||||||
toast('Scan already in progress', 'info');
|
toast('Scan already in progress', 'info');
|
||||||
} else if (result.status === 'started') {
|
} else if (result.status === 'started') {
|
||||||
toast('Scan started — items will appear as they\'re found', 'success');
|
toast('Scan started', 'info');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onError: (err) => {
|
onError: (err) => {
|
||||||
|
|
@ -309,6 +309,8 @@ export function ChannelDetail() {
|
||||||
}, [scanChannel, toast]);
|
}, [scanChannel, toast]);
|
||||||
|
|
||||||
const handleCancelScan = useCallback(() => {
|
const handleCancelScan = useCallback(() => {
|
||||||
|
// Optimistic update: clear scan state immediately
|
||||||
|
clearScan();
|
||||||
cancelScan.mutate(undefined, {
|
cancelScan.mutate(undefined, {
|
||||||
onSuccess: (result) => {
|
onSuccess: (result) => {
|
||||||
if (result.cancelled) {
|
if (result.cancelled) {
|
||||||
|
|
@ -321,7 +323,7 @@ export function ChannelDetail() {
|
||||||
toast(err instanceof Error ? err.message : 'Failed to cancel scan', 'error');
|
toast(err instanceof Error ? err.message : 'Failed to cancel scan', 'error');
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}, [cancelScan, toast]);
|
}, [cancelScan, toast, clearScan]);
|
||||||
|
|
||||||
const handleCollect = useCallback(() => {
|
const handleCollect = useCallback(() => {
|
||||||
collectMonitored.mutate(undefined, {
|
collectMonitored.mutate(undefined, {
|
||||||
|
|
@ -1034,7 +1036,7 @@ export function ChannelDetail() {
|
||||||
disabled={scanChannel.isPending || cancelScan.isPending}
|
disabled={scanChannel.isPending || cancelScan.isPending}
|
||||||
title={scanInProgress ? 'Cancel scan' : 'Refresh & Scan'}
|
title={scanInProgress ? 'Cancel scan' : 'Refresh & Scan'}
|
||||||
className={scanInProgress ? 'btn btn-danger' : 'btn btn-ghost'}
|
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 ? (
|
{scanInProgress ? (
|
||||||
<Square size={12} />
|
<Square size={12} />
|
||||||
|
|
@ -1043,7 +1045,7 @@ export function ChannelDetail() {
|
||||||
) : (
|
) : (
|
||||||
<RefreshCw size={12} />
|
<RefreshCw size={12} />
|
||||||
)}
|
)}
|
||||||
{scanInProgress ? `Stop (${scanNewItemCount})` : 'Scan'}
|
{scanInProgress ? 'Stop Scan' : scanChannel.isPending ? 'Starting…' : 'Scan'}
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
|
|
@ -1359,7 +1361,7 @@ export function ChannelDetail() {
|
||||||
disabled={scanChannel.isPending || cancelScan.isPending}
|
disabled={scanChannel.isPending || cancelScan.isPending}
|
||||||
title={scanInProgress ? 'Cancel scan' : 'Refresh & Scan'}
|
title={scanInProgress ? 'Cancel scan' : 'Refresh & Scan'}
|
||||||
className={scanInProgress ? 'btn btn-danger' : 'btn btn-ghost'}
|
className={scanInProgress ? 'btn btn-danger' : 'btn btn-ghost'}
|
||||||
style={{ opacity: (scanChannel.isPending || cancelScan.isPending) ? 0.6 : 1 }}
|
style={{ opacity: (scanChannel.isPending || cancelScan.isPending) ? 0.6 : 1, minWidth: 140 }}
|
||||||
>
|
>
|
||||||
{scanInProgress ? (
|
{scanInProgress ? (
|
||||||
<Square size={14} />
|
<Square size={14} />
|
||||||
|
|
@ -1368,7 +1370,7 @@ export function ChannelDetail() {
|
||||||
) : (
|
) : (
|
||||||
<RefreshCw size={14} />
|
<RefreshCw size={14} />
|
||||||
)}
|
)}
|
||||||
{scanInProgress ? `Stop Scan (${scanNewItemCount} found)` : 'Scan'}
|
{scanInProgress ? 'Stop Scan' : scanChannel.isPending ? 'Starting…' : 'Scan'}
|
||||||
</button>
|
</button>
|
||||||
<button
|
<button
|
||||||
onClick={handleCollect}
|
onClick={handleCollect}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue