mirror of
https://github.com/nimbold/Firelink.git
synced 2026-08-18 23:26:18 +00:00
fix(core): harden download lifecycle and scheduling
This commit is contained in:
@@ -71,7 +71,7 @@ describe('useDownloadStore', () => {
|
||||
});
|
||||
|
||||
const dispatched = await useDownloadStore.getState().startQueue('MAIN');
|
||||
expect(dispatched).toBe(2); // Both items counted as dispatched/handled
|
||||
expect(dispatched).toEqual(['1', '2']);
|
||||
|
||||
const calls = vi.mocked(ipc.invokeCommand).mock.calls;
|
||||
const enqueues = calls.filter(c => c[0] === 'enqueue_download');
|
||||
@@ -79,6 +79,34 @@ describe('useDownloadStore', () => {
|
||||
expect((enqueues[0] as any)[1].item.id).toBe('2');
|
||||
});
|
||||
|
||||
it('does not overwrite a downloading event received while starting a queue', async () => {
|
||||
useDownloadStore.setState({
|
||||
downloads: [
|
||||
{ id: '1', url: 'http://test1', fileName: 'f1', destination: '/tmp', status: 'queued', category: 'Other', dateAdded: '', queueId: 'MAIN', hasBeenDispatched: false },
|
||||
] as any[],
|
||||
});
|
||||
|
||||
vi.mocked(ipc.invokeCommand).mockImplementation(async (cmd: string) => {
|
||||
if (cmd === 'enqueue_download') {
|
||||
useDownloadStore.getState().updateDownload('1', {
|
||||
status: 'downloading',
|
||||
speed: '1 MB/s',
|
||||
eta: '10s'
|
||||
});
|
||||
}
|
||||
if (cmd === 'get_pending_order') return ['1'];
|
||||
return undefined;
|
||||
});
|
||||
|
||||
expect(await useDownloadStore.getState().startQueue('MAIN')).toEqual(['1']);
|
||||
expect(useDownloadStore.getState().downloads[0]).toMatchObject({
|
||||
status: 'downloading',
|
||||
speed: '1 MB/s',
|
||||
eta: '10s',
|
||||
hasBeenDispatched: true
|
||||
});
|
||||
});
|
||||
|
||||
it('resumeDownload unregisters ID and re-dispatches if un-resumable', async () => {
|
||||
useDownloadStore.setState({
|
||||
downloads: [
|
||||
@@ -113,9 +141,9 @@ describe('useDownloadStore', () => {
|
||||
}, { type: 'add-to-queue', queueId: 'queue-b' });
|
||||
|
||||
const item = useDownloadStore.getState().downloads[0];
|
||||
expect(item.status).toBe('queued');
|
||||
expect(item.status).toBe('staged');
|
||||
expect(item.queueId).toBe('queue-b');
|
||||
expect(useDownloadStore.getState().pendingOrder).toContain('queue-1');
|
||||
expect(item.queuePosition).toBe(0);
|
||||
expect(ipc.invokeCommand).not.toHaveBeenCalledWith('enqueue_download', expect.anything());
|
||||
});
|
||||
|
||||
@@ -207,7 +235,7 @@ describe('useDownloadStore', () => {
|
||||
).toHaveLength(4);
|
||||
});
|
||||
|
||||
it('assigns selected unfinished downloads to a queue without moving completed items', () => {
|
||||
it('assigns selected unfinished downloads to a queue without moving completed items', async () => {
|
||||
useDownloadStore.setState({
|
||||
downloads: [
|
||||
{ id: 'ready', status: 'ready', queueId: 'old' },
|
||||
@@ -215,12 +243,50 @@ describe('useDownloadStore', () => {
|
||||
] as any[]
|
||||
});
|
||||
|
||||
useDownloadStore.getState().assignToQueue(['ready', 'done'], 'new');
|
||||
await useDownloadStore.getState().assignToQueue(['ready', 'done'], 'new');
|
||||
|
||||
expect(useDownloadStore.getState().downloads.find(item => item.id === 'ready')?.queueId).toBe('new');
|
||||
expect(useDownloadStore.getState().downloads.find(item => item.id === 'done')?.queueId).toBe('old');
|
||||
});
|
||||
|
||||
it('retains the UI item when backend removal fails', async () => {
|
||||
useDownloadStore.setState({
|
||||
downloads: [
|
||||
{ id: 'active', url: 'https://example.com/file', fileName: 'file', status: 'downloading', category: 'Other', dateAdded: '', queueId: 'main' }
|
||||
] as any[]
|
||||
});
|
||||
vi.mocked(ipc.invokeCommand).mockRejectedValueOnce(new Error('writer did not stop'));
|
||||
|
||||
await expect(useDownloadStore.getState().removeDownload('active', true))
|
||||
.rejects.toThrow('writer did not stop');
|
||||
expect(useDownloadStore.getState().downloads.map(download => download.id))
|
||||
.toEqual(['active']);
|
||||
});
|
||||
|
||||
it('starts staged queue items in their persisted queue order', async () => {
|
||||
useDownloadStore.setState({
|
||||
downloads: [
|
||||
{ id: 'later', url: 'https://example.com/later', fileName: 'later', status: 'staged', category: 'Other', dateAdded: '', queueId: 'queue-a', queuePosition: 1 },
|
||||
{ id: 'first', url: 'https://example.com/first', fileName: 'first', status: 'staged', category: 'Other', dateAdded: '', queueId: 'queue-a', queuePosition: 0 }
|
||||
] as any[]
|
||||
});
|
||||
vi.mocked(ipc.invokeCommand).mockImplementation(async (command: string, args?: unknown) => {
|
||||
if (command === 'get_pending_order') {
|
||||
return [(args as { queueId: string }).queueId === 'queue-a' ? 'first' : 'later'];
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
|
||||
expect(await useDownloadStore.getState().startQueue('queue-a')).toEqual(['first', 'later']);
|
||||
const enqueuedIds = vi.mocked(ipc.invokeCommand).mock.calls
|
||||
.filter(call => call[0] === 'enqueue_download')
|
||||
.map(call => (call[1] as any).item.id);
|
||||
expect(enqueuedIds).toEqual(['first', 'later']);
|
||||
expect((vi.mocked(ipc.invokeCommand).mock.calls.find(call =>
|
||||
call[0] === 'enqueue_download'
|
||||
)?.[1] as any).item.queue_id).toBe('queue-a');
|
||||
});
|
||||
|
||||
it('preserves extension request headers and cookies for the Add modal', () => {
|
||||
useDownloadStore.getState().handleExtensionDownload({
|
||||
urls: ['https://example.com/file.bin'],
|
||||
|
||||
+130
-61
@@ -7,11 +7,9 @@ import type { DownloadStatus } from '../bindings/DownloadStatus';
|
||||
import type { ExtensionDownload } from '../bindings/ExtensionDownload';
|
||||
import type { Queue } from '../bindings/Queue';
|
||||
import { useSettingsStore } from './useSettingsStore';
|
||||
import { isActiveDownloadStatus, normalizeSpeedLimitForBackend, redactDownloadForPersistence } from '../utils/downloads';
|
||||
import { categoryForFileName, isActiveDownloadStatus, normalizeSpeedLimitForBackend, redactDownloadForPersistence } from '../utils/downloads';
|
||||
import {
|
||||
expandTilde,
|
||||
resolveCategoryDestination,
|
||||
resolveDownloadFilePath
|
||||
resolveCategoryDestination
|
||||
} from '../utils/downloadLocations';
|
||||
import { canPauseDownload, canStartDownload } from '../utils/downloadActions';
|
||||
|
||||
@@ -42,6 +40,7 @@ export async function dispatchItem(id: string): Promise<boolean> {
|
||||
|
||||
const enqueueItem = {
|
||||
id: item.id,
|
||||
queue_id: item.queueId || MAIN_QUEUE_ID,
|
||||
url: item.url,
|
||||
destination,
|
||||
filename: item.fileName,
|
||||
@@ -61,8 +60,15 @@ export async function dispatchItem(id: string): Promise<boolean> {
|
||||
is_media: item.isMedia || false
|
||||
};
|
||||
|
||||
await invoke('enqueue_download', { item: enqueueItem });
|
||||
const order = await invoke('get_pending_order');
|
||||
const accepted = await invoke('enqueue_download', { item: enqueueItem });
|
||||
const acceptedFilename = accepted?.filename || item.fileName;
|
||||
if (acceptedFilename !== item.fileName) {
|
||||
useDownloadStore.getState().updateDownload(id, {
|
||||
fileName: acceptedFilename,
|
||||
category: categoryForFileName(acceptedFilename)
|
||||
});
|
||||
}
|
||||
const order = await invoke('get_pending_order', { queueId: item.queueId || MAIN_QUEUE_ID });
|
||||
useDownloadStore.getState().setPendingOrder(order);
|
||||
useDownloadStore.getState().registerBackendIds([id]);
|
||||
return true;
|
||||
@@ -126,16 +132,26 @@ const syncSystemIntegrations = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const resolveDownloadPath = async (destination: string, fileName: string) => {
|
||||
return resolveDownloadFilePath(await expandTilde(destination), fileName);
|
||||
};
|
||||
|
||||
const effectiveDestinationForItem = async (
|
||||
item: Pick<DownloadItem, 'destination' | 'category'>,
|
||||
settings: ReturnType<typeof useSettingsStore.getState>
|
||||
): Promise<string> =>
|
||||
item.destination || resolveCategoryDestination(settings, item.category);
|
||||
|
||||
const normalizeQueuePositions = (downloads: DownloadItem[]): DownloadItem[] => {
|
||||
const nextPosition = new Map<string, number>();
|
||||
return downloads.map(download => {
|
||||
const queueId = download.queueId || MAIN_QUEUE_ID;
|
||||
const position = nextPosition.get(queueId) || 0;
|
||||
nextPosition.set(queueId, position + 1);
|
||||
return {
|
||||
...download,
|
||||
queueId,
|
||||
queuePosition: download.queuePosition ?? position
|
||||
};
|
||||
});
|
||||
};
|
||||
|
||||
export type { DownloadStatus };
|
||||
export const MAIN_QUEUE_ID = '00000000-0000-0000-0000-000000000001';
|
||||
|
||||
@@ -186,15 +202,15 @@ interface DownloadState {
|
||||
updateDownload: (id: string, updates: Partial<DownloadItem>) => void;
|
||||
removeDownload: (id: string, deleteFile?: boolean) => Promise<void>;
|
||||
redownload: (id: string) => Promise<void>;
|
||||
resumeDownload: (id: string) => Promise<void>;
|
||||
startQueue: (queueId: string) => Promise<number>;
|
||||
resumeDownload: (id: string) => Promise<boolean>;
|
||||
startQueue: (queueId: string) => Promise<string[]>;
|
||||
pauseQueue: (queueId: string) => Promise<number>;
|
||||
startAll: () => Promise<number>;
|
||||
pauseAll: () => Promise<number>;
|
||||
assignToQueue: (ids: string[], queueId: string) => void;
|
||||
assignToQueue: (ids: string[], queueId: string) => Promise<void>;
|
||||
addQueue: (name: string) => void;
|
||||
renameQueue: (id: string, name: string) => void;
|
||||
removeQueue: (id: string) => void;
|
||||
removeQueue: (id: string) => Promise<void>;
|
||||
initDB: () => Promise<void>;
|
||||
|
||||
}
|
||||
@@ -205,8 +221,27 @@ export const useDownloadStore = create<DownloadState>((set, get) => ({
|
||||
pendingOrder: [],
|
||||
setPendingOrder: (order) => set({ pendingOrder: order }),
|
||||
moveInQueue: async (id, direction) => {
|
||||
const item = get().downloads.find(download => download.id === id);
|
||||
if (!item) return;
|
||||
const queueId = item.queueId || MAIN_QUEUE_ID;
|
||||
const queueItems = get().downloads
|
||||
.filter(download => (download.queueId || MAIN_QUEUE_ID) === queueId && download.status !== 'completed')
|
||||
.sort((left, right) => (left.queuePosition ?? 0) - (right.queuePosition ?? 0));
|
||||
const index = queueItems.findIndex(download => download.id === id);
|
||||
const target = direction === 'up' ? index - 1 : index + 1;
|
||||
if (index < 0 || target < 0 || target >= queueItems.length) return;
|
||||
const reordered = [...queueItems];
|
||||
[reordered[index], reordered[target]] = [reordered[target], reordered[index]];
|
||||
const positions = new Map(reordered.map((download, position) => [download.id, position]));
|
||||
set(state => ({
|
||||
downloads: state.downloads.map(download => positions.has(download.id)
|
||||
? { ...download, queuePosition: positions.get(download.id) }
|
||||
: download)
|
||||
}));
|
||||
|
||||
if (!get().backendRegisteredIds.has(id)) return;
|
||||
try {
|
||||
const order = await invoke('move_in_queue', { id, direction });
|
||||
const order = await invoke('move_in_queue', { id, queueId, direction });
|
||||
set({ pendingOrder: order });
|
||||
} catch (e) {
|
||||
console.error("Failed to move item in queue:", e);
|
||||
@@ -284,20 +319,21 @@ export const useDownloadStore = create<DownloadState>((set, get) => ({
|
||||
addDownload: async (item, action) => {
|
||||
const settings = useSettingsStore.getState();
|
||||
const destPath = await effectiveDestinationForItem(item, settings);
|
||||
const queueId = action.type === 'add-to-queue' ? action.queueId : MAIN_QUEUE_ID;
|
||||
const queuePosition = get().downloads.filter(download =>
|
||||
(download.queueId || MAIN_QUEUE_ID) === queueId && download.status !== 'completed'
|
||||
).length;
|
||||
const ownedItem: DownloadItem = {
|
||||
...item,
|
||||
destination: destPath,
|
||||
status: action.type === 'add-to-queue' ? 'queued' : 'ready',
|
||||
queueId: action.type === 'add-to-queue' ? action.queueId : MAIN_QUEUE_ID,
|
||||
status: action.type === 'add-to-queue' ? 'staged' : 'ready',
|
||||
queueId,
|
||||
queuePosition,
|
||||
hasBeenDispatched: false
|
||||
};
|
||||
set((state) => ({ downloads: [...state.downloads, ownedItem] }));
|
||||
|
||||
if (action.type === 'add-to-queue') {
|
||||
const order = useDownloadStore.getState().pendingOrder;
|
||||
if (!order.includes(item.id)) {
|
||||
useDownloadStore.getState().setPendingOrder([...order, item.id]);
|
||||
}
|
||||
info(`Download ${item.id} added to queue ${action.queueId}`);
|
||||
} else if (action.type === 'start-now') {
|
||||
if (await dispatchItem(item.id)) {
|
||||
@@ -315,7 +351,7 @@ export const useDownloadStore = create<DownloadState>((set, get) => ({
|
||||
throw new Error("Cannot change properties while transfer is active. Pause it first.");
|
||||
}
|
||||
|
||||
if (item.status === 'ready' || item.status === 'completed' || item.status === 'failed') {
|
||||
if (item.status === 'ready' || item.status === 'staged' || item.status === 'completed' || item.status === 'failed') {
|
||||
state.updateDownload(id, updates);
|
||||
return;
|
||||
}
|
||||
@@ -373,18 +409,8 @@ export const useDownloadStore = create<DownloadState>((set, get) => ({
|
||||
removeDownload: async (id, deleteFile = false) => {
|
||||
const item = get().downloads.find(d => d.id === id);
|
||||
|
||||
if (item && deleteFile) {
|
||||
const filepath = await resolveDownloadPath(item.destination || '~/Downloads', item.fileName);
|
||||
const partialPaths = [`${filepath}.aria2`, `${filepath}.part`];
|
||||
await invoke('trash_download_assets', { path: filepath, partialPaths });
|
||||
}
|
||||
|
||||
if (item) {
|
||||
try {
|
||||
await invoke('remove_download', { id, filepath: null });
|
||||
} catch (e) {
|
||||
console.error("Failed to terminate download on backend during deletion, but will still remove from UI:", e);
|
||||
}
|
||||
await invoke('remove_download', { id, deleteAssets: deleteFile });
|
||||
}
|
||||
|
||||
set((state) => ({
|
||||
@@ -464,19 +490,20 @@ export const useDownloadStore = create<DownloadState>((set, get) => ({
|
||||
},
|
||||
resumeDownload: async (id) => {
|
||||
const targetItem = get().downloads.find(d => d.id === id);
|
||||
if (!targetItem) return;
|
||||
if (!targetItem) return false;
|
||||
|
||||
try {
|
||||
if (targetItem.status === 'ready') {
|
||||
if (targetItem.status === 'ready' || targetItem.status === 'staged') {
|
||||
if (await dispatchItem(id)) {
|
||||
get().updateDownload(id, { hasBeenDispatched: true });
|
||||
return true;
|
||||
}
|
||||
return;
|
||||
return false;
|
||||
}
|
||||
|
||||
const resumedExisting = await invoke('resume_download', { id });
|
||||
if (resumedExisting) {
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
|
||||
get().unregisterBackendIds([id]);
|
||||
@@ -492,38 +519,50 @@ export const useDownloadStore = create<DownloadState>((set, get) => ({
|
||||
|
||||
if (!await dispatchItem(id)) {
|
||||
console.error("Failed to re-enqueue for resume");
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
} catch (e) {
|
||||
console.error("Failed to resume download:", e);
|
||||
return false;
|
||||
}
|
||||
},
|
||||
startQueue: async (queueId) => {
|
||||
const runnable = get().downloads
|
||||
.filter(item => item.queueId === queueId && (item.status === 'queued' || canStartDownload(item.status)));
|
||||
.filter(item => item.queueId === queueId && (item.status === 'queued' || canStartDownload(item.status)))
|
||||
.sort((left, right) => (left.queuePosition ?? 0) - (right.queuePosition ?? 0));
|
||||
|
||||
if (runnable.length === 0) return 0;
|
||||
if (runnable.length === 0) return [];
|
||||
|
||||
let dispatchedCount = 0;
|
||||
const promises = runnable.map(async (item) => {
|
||||
if (item.status === 'ready' || item.status === 'failed' || !item.hasBeenDispatched) {
|
||||
const acceptedIds: string[] = [];
|
||||
for (const item of runnable) {
|
||||
if (
|
||||
item.status === 'ready' ||
|
||||
item.status === 'staged' ||
|
||||
item.status === 'failed' ||
|
||||
!item.hasBeenDispatched ||
|
||||
!get().backendRegisteredIds.has(item.id)
|
||||
) {
|
||||
if (await dispatchItem(item.id)) {
|
||||
get().updateDownload(item.id, { hasBeenDispatched: true, status: 'queued' });
|
||||
dispatchedCount++;
|
||||
const current = get().downloads.find(download => download.id === item.id);
|
||||
get().updateDownload(item.id, {
|
||||
hasBeenDispatched: true,
|
||||
...(current?.status === item.status ? { status: 'queued' as const } : {})
|
||||
});
|
||||
acceptedIds.push(item.id);
|
||||
}
|
||||
} else if (item.status === 'paused' || item.status === 'queued') {
|
||||
// If it's queued but already dispatched, it might be waiting.
|
||||
// If it's paused, we resume it.
|
||||
if (item.status === 'paused') {
|
||||
await get().resumeDownload(item.id);
|
||||
if (!await get().resumeDownload(item.id)) continue;
|
||||
}
|
||||
dispatchedCount++;
|
||||
acceptedIds.push(item.id);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
await Promise.all(promises);
|
||||
|
||||
info(`Queue ${queueId} started, ${dispatchedCount} items dispatched/resumed`);
|
||||
return dispatchedCount;
|
||||
info(`Queue ${queueId} started, ${acceptedIds.length} items dispatched/resumed`);
|
||||
return acceptedIds;
|
||||
},
|
||||
pauseQueue: async (queueId) => {
|
||||
const activeIds = get().downloads
|
||||
@@ -556,7 +595,7 @@ export const useDownloadStore = create<DownloadState>((set, get) => ({
|
||||
.map(item => item.queueId || MAIN_QUEUE_ID)
|
||||
);
|
||||
const results = await Promise.all(Array.from(queueIds, queueId => get().startQueue(queueId)));
|
||||
return results.reduce((total, count) => total + count, 0);
|
||||
return results.reduce((total, ids) => total + ids.length, 0);
|
||||
},
|
||||
pauseAll: async () => {
|
||||
const activeIds = get().downloads
|
||||
@@ -571,12 +610,39 @@ export const useDownloadStore = create<DownloadState>((set, get) => ({
|
||||
syncSystemIntegrations();
|
||||
return pausedCount;
|
||||
},
|
||||
assignToQueue: (ids, queueId) => {
|
||||
assignToQueue: async (ids, queueId) => {
|
||||
const selectedIds = new Set(ids);
|
||||
const selected = get().downloads.filter(item => selectedIds.has(item.id));
|
||||
const locked = selected.find(item => isActiveDownloadStatus(item.status) && item.status !== 'queued');
|
||||
if (locked) {
|
||||
throw new Error(`Pause ${locked.fileName} before moving it to another queue.`);
|
||||
}
|
||||
|
||||
for (const item of selected) {
|
||||
if (!get().backendRegisteredIds.has(item.id)) continue;
|
||||
if (item.status === 'queued') {
|
||||
await invoke('remove_from_queue', { id: item.id });
|
||||
} else if (item.status === 'paused') {
|
||||
await invoke('detach_download_for_reconfigure', { id: item.id });
|
||||
}
|
||||
get().unregisterBackendIds([item.id]);
|
||||
}
|
||||
|
||||
const nextPosition = get().downloads.filter(item =>
|
||||
!selectedIds.has(item.id) &&
|
||||
(item.queueId || MAIN_QUEUE_ID) === queueId &&
|
||||
item.status !== 'completed'
|
||||
).length;
|
||||
set(state => ({
|
||||
downloads: state.downloads.map(item =>
|
||||
selectedIds.has(item.id) && item.status !== 'completed'
|
||||
? { ...item, queueId }
|
||||
? {
|
||||
...item,
|
||||
queueId,
|
||||
queuePosition: nextPosition + selected.findIndex(selectedItem => selectedItem.id === item.id),
|
||||
status: 'staged' as const,
|
||||
hasBeenDispatched: false
|
||||
}
|
||||
: item
|
||||
)
|
||||
}));
|
||||
@@ -599,9 +665,14 @@ export const useDownloadStore = create<DownloadState>((set, get) => ({
|
||||
})
|
||||
}));
|
||||
},
|
||||
removeQueue: (id) => {
|
||||
removeQueue: async (id) => {
|
||||
if (id === MAIN_QUEUE_ID) return;
|
||||
|
||||
const unfinishedIds = get().downloads
|
||||
.filter(download => download.queueId === id && download.status !== 'completed')
|
||||
.map(download => download.id);
|
||||
if (unfinishedIds.length > 0) {
|
||||
await get().assignToQueue(unfinishedIds, MAIN_QUEUE_ID);
|
||||
}
|
||||
set((state) => ({
|
||||
queues: state.queues.filter(q => q.id !== id),
|
||||
downloads: state.downloads.map(d =>
|
||||
@@ -619,10 +690,7 @@ export const useDownloadStore = create<DownloadState>((set, get) => ({
|
||||
set(state => ({
|
||||
queues: queues.length > 0 ? queues : state.queues,
|
||||
downloads: downloads.length > 0
|
||||
? downloads.map(download => ({
|
||||
...download,
|
||||
queueId: download.queueId || MAIN_QUEUE_ID
|
||||
}))
|
||||
? normalizeQueuePositions(downloads)
|
||||
: state.downloads
|
||||
}));
|
||||
|
||||
@@ -655,6 +723,7 @@ export const useDownloadStore = create<DownloadState>((set, get) => ({
|
||||
await resolveCategoryDestination(settings, item.category);
|
||||
itemsToEnqueue.push({
|
||||
id: item.id,
|
||||
queue_id: item.queueId || MAIN_QUEUE_ID,
|
||||
url: item.url,
|
||||
destination: destPath,
|
||||
filename: item.fileName,
|
||||
@@ -677,7 +746,7 @@ export const useDownloadStore = create<DownloadState>((set, get) => ({
|
||||
const results = await invoke('enqueue_many', { items: itemsToEnqueue });
|
||||
const registeredIds = results.filter(result => result.success).map(result => result.id);
|
||||
const failedIds = new Set(results.filter(result => !result.success).map(result => result.id));
|
||||
const order = await invoke('get_pending_order');
|
||||
const order = await invoke('get_pending_order', { queueId: null });
|
||||
set(state => ({
|
||||
pendingOrder: order,
|
||||
backendRegisteredIds: new Set([
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
} from '../utils/downloadLocations';
|
||||
|
||||
let settingsSave = Promise.resolve();
|
||||
const DEFAULT_SCHEDULER_QUEUE_ID = '00000000-0000-0000-0000-000000000001';
|
||||
|
||||
const tauriStorage: StateStorage = {
|
||||
getItem: async (name: string): Promise<string | null> => {
|
||||
@@ -80,6 +81,7 @@ export interface SettingsState {
|
||||
activeSettingsTab: SettingsTab;
|
||||
scheduler: SchedulerSettings;
|
||||
schedulerRunning: boolean;
|
||||
schedulerActiveDownloadIds: string[];
|
||||
schedulerLastStartKey: string;
|
||||
schedulerLastStopKey: string;
|
||||
lastCustomSpeedLimitKiB: number;
|
||||
@@ -112,6 +114,7 @@ export interface SettingsState {
|
||||
setActiveSettingsTab: (tab: SettingsTab) => void;
|
||||
setScheduler: (settings: SchedulerSettings) => void;
|
||||
setSchedulerRunning: (running: boolean) => void;
|
||||
setSchedulerActiveDownloadIds: (ids: string[]) => void;
|
||||
setSchedulerLastStartKey: (key: string) => void;
|
||||
setSchedulerLastStopKey: (key: string) => void;
|
||||
setLastCustomSpeedLimitKiB: (limit: number) => void;
|
||||
@@ -187,9 +190,11 @@ export const useSettingsStore = create<SettingsState>()(
|
||||
stopTime: '08:00',
|
||||
everyday: true,
|
||||
selectedDays: [0, 1, 2, 3, 4, 5, 6],
|
||||
selectedQueueIds: [DEFAULT_SCHEDULER_QUEUE_ID],
|
||||
postQueueAction: 'none'
|
||||
},
|
||||
schedulerRunning: false,
|
||||
schedulerActiveDownloadIds: [],
|
||||
schedulerLastStartKey: '',
|
||||
schedulerLastStopKey: '',
|
||||
lastCustomSpeedLimitKiB: 1024,
|
||||
@@ -231,6 +236,7 @@ export const useSettingsStore = create<SettingsState>()(
|
||||
setActiveSettingsTab: (activeSettingsTab) => set({ activeSettingsTab }),
|
||||
setScheduler: (scheduler) => set({ scheduler }),
|
||||
setSchedulerRunning: (schedulerRunning) => set({ schedulerRunning }),
|
||||
setSchedulerActiveDownloadIds: (schedulerActiveDownloadIds) => set({ schedulerActiveDownloadIds }),
|
||||
setSchedulerLastStartKey: (schedulerLastStartKey) => set({ schedulerLastStartKey }),
|
||||
setSchedulerLastStopKey: (schedulerLastStopKey) => set({ schedulerLastStopKey }),
|
||||
setLastCustomSpeedLimitKiB: (lastCustomSpeedLimitKiB) => set({ lastCustomSpeedLimitKiB }),
|
||||
@@ -301,7 +307,7 @@ export const useSettingsStore = create<SettingsState>()(
|
||||
{
|
||||
name: 'firelink-settings',
|
||||
storage: createJSONStorage(() => tauriStorage),
|
||||
version: 2,
|
||||
version: 3,
|
||||
migrate: (persistedState) => {
|
||||
if (!persistedState || typeof persistedState !== 'object') {
|
||||
return persistedState as SettingsState;
|
||||
@@ -316,6 +322,15 @@ export const useSettingsStore = create<SettingsState>()(
|
||||
return {
|
||||
...persisted,
|
||||
...locations,
|
||||
scheduler: persisted.scheduler
|
||||
? {
|
||||
...persisted.scheduler,
|
||||
selectedQueueIds: Array.isArray(persisted.scheduler.selectedQueueIds)
|
||||
&& persisted.scheduler.selectedQueueIds.length > 0
|
||||
? persisted.scheduler.selectedQueueIds
|
||||
: [DEFAULT_SCHEDULER_QUEUE_ID]
|
||||
}
|
||||
: persisted.scheduler,
|
||||
siteLogins: Array.isArray(persisted.siteLogins) ? persisted.siteLogins : []
|
||||
} as SettingsState;
|
||||
},
|
||||
@@ -360,6 +375,14 @@ export const useSettingsStore = create<SettingsState>()(
|
||||
...currentState,
|
||||
...persisted,
|
||||
...locations,
|
||||
scheduler: {
|
||||
...currentState.scheduler,
|
||||
...persisted.scheduler,
|
||||
selectedQueueIds: Array.isArray(persisted.scheduler?.selectedQueueIds)
|
||||
&& persisted.scheduler.selectedQueueIds.length > 0
|
||||
? persisted.scheduler.selectedQueueIds
|
||||
: currentState.scheduler.selectedQueueIds
|
||||
},
|
||||
appFontSize: persisted.appFontSize || currentState.appFontSize,
|
||||
listRowDensity: persisted.listRowDensity || currentState.listRowDensity,
|
||||
siteLogins: Array.isArray(persisted.siteLogins)
|
||||
|
||||
Reference in New Issue
Block a user