mirror of
https://github.com/Psychotoxical/psysonic.git
synced 2026-07-22 15:25:46 +00:00
fix(offline): cancellable downloads + stable sidebar progress toast (#694)
* fix(sidebar): keep offline-download toast from squishing in a short window The toast lives in the sidebar nav flex column; without flex-shrink: 0 the column compressed it vertically when the main window was small. The label now also ellipsis-truncates instead of overflowing on a narrow sidebar. * fix(offline): make offline downloads cancellable down to the Rust transfer A running offline download could not be stopped — the sidebar X button only dropped not-yet-started tracks between batches of 8, and the Rust transfer had no cancellation path at all, so in-flight HTTP streams always ran to completion. Add an offline_cancel_flags() registry (mirroring sync_cancel_flags for the device-sync side) plus additive cancel_offline_downloads / clear_offline_cancel commands. download_track_offline takes an optional download_id, checks the flag right after acquiring its semaphore slot, and threads it through finalize_streamed_download / stream_to_file so an in-flight stream aborts at the next chunk — the partial .part file is cleaned up by the existing error path. * fix(offline): cancel per-track and clear the sidebar toast immediately downloadAlbum tags each run with a downloadId, checks for cancellation before every track instead of once per 8-track batch (which never re-ran for albums of 8 or fewer tracks), and persists tracks that finished before the cancel so they are not orphaned on disk. cancelDownload / cancelAllDownloads drop every job for the album and call cancel_offline_downloads so Rust aborts the in-flight transfers — the toast disappears at once instead of lingering on stuck rows. Adds offlineJobStore cancellation tests. * docs(changelog): offline download cancel button + toast sizing fixes
This commit is contained in:
committed by
GitHub
parent
946528350c
commit
b4c8ed4b65
@@ -0,0 +1,84 @@
|
||||
import { beforeEach, describe, expect, it } from 'vitest';
|
||||
import { onInvoke } from '@/test/mocks/tauri';
|
||||
import { useOfflineJobStore, cancelledDownloads, type DownloadJob } from './offlineJobStore';
|
||||
|
||||
function job(over: Partial<DownloadJob>): DownloadJob {
|
||||
return {
|
||||
trackId: 't',
|
||||
albumId: 'a',
|
||||
albumName: 'A',
|
||||
trackTitle: 'T',
|
||||
trackIndex: 0,
|
||||
totalTracks: 1,
|
||||
status: 'queued',
|
||||
downloadId: 'a-1',
|
||||
...over,
|
||||
};
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
useOfflineJobStore.setState({ jobs: [], bulkProgress: {} });
|
||||
cancelledDownloads.clear();
|
||||
});
|
||||
|
||||
describe('offlineJobStore cancellation', () => {
|
||||
it('cancelAllDownloads drops queued + downloading jobs but keeps settled ones', () => {
|
||||
const calls: string[][] = [];
|
||||
onInvoke('cancel_offline_downloads', (a: unknown) => {
|
||||
calls.push((a as { downloadIds: string[] }).downloadIds);
|
||||
});
|
||||
useOfflineJobStore.setState({
|
||||
jobs: [
|
||||
job({ trackId: 'q', status: 'queued' }),
|
||||
job({ trackId: 'd', status: 'downloading' }),
|
||||
job({ trackId: 'done', status: 'done' }),
|
||||
job({ trackId: 'err', status: 'error' }),
|
||||
],
|
||||
bulkProgress: {},
|
||||
});
|
||||
|
||||
useOfflineJobStore.getState().cancelAllDownloads();
|
||||
|
||||
// Only settled jobs survive → the sidebar toast clears.
|
||||
expect(useOfflineJobStore.getState().jobs.map(j => j.status).sort()).toEqual(['done', 'error']);
|
||||
expect(cancelledDownloads.has('a')).toBe(true);
|
||||
// Rust is told to abort the in-flight transfers for this download id.
|
||||
expect(calls).toEqual([['a-1']]);
|
||||
});
|
||||
|
||||
it('cancelDownload drops every job for one album and leaves others running', () => {
|
||||
const calls: string[][] = [];
|
||||
onInvoke('cancel_offline_downloads', (a: unknown) => {
|
||||
calls.push((a as { downloadIds: string[] }).downloadIds);
|
||||
});
|
||||
useOfflineJobStore.setState({
|
||||
jobs: [
|
||||
job({ trackId: 't1', albumId: 'a', status: 'downloading', downloadId: 'a-1' }),
|
||||
job({ trackId: 't2', albumId: 'b', status: 'downloading', downloadId: 'b-1' }),
|
||||
],
|
||||
bulkProgress: {},
|
||||
});
|
||||
|
||||
useOfflineJobStore.getState().cancelDownload('a');
|
||||
|
||||
expect(useOfflineJobStore.getState().jobs.map(j => j.albumId)).toEqual(['b']);
|
||||
expect(cancelledDownloads.has('a')).toBe(true);
|
||||
expect(calls).toEqual([['a-1']]);
|
||||
});
|
||||
|
||||
it('cancelAllDownloads with nothing active does not call into Rust', () => {
|
||||
let called = false;
|
||||
onInvoke('cancel_offline_downloads', () => {
|
||||
called = true;
|
||||
});
|
||||
useOfflineJobStore.setState({
|
||||
jobs: [job({ status: 'done' }), job({ status: 'error' })],
|
||||
bulkProgress: {},
|
||||
});
|
||||
|
||||
useOfflineJobStore.getState().cancelAllDownloads();
|
||||
|
||||
expect(called).toBe(false);
|
||||
expect(useOfflineJobStore.getState().jobs).toHaveLength(2);
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user