mirror of
https://github.com/nimbold/Firelink.git
synced 2026-08-09 10:49:29 +00:00
feat(ui): modernize desktop interactions
This commit is contained in:
@@ -1,8 +1,8 @@
|
||||
import { create } from 'zustand';
|
||||
import { listen, UnlistenFn } from '@tauri-apps/api/event';
|
||||
import type { UnlistenFn } from '@tauri-apps/api/event';
|
||||
import type { DownloadProgressEvent } from '../bindings/DownloadProgressEvent';
|
||||
import type { DownloadStateEvent } from '../bindings/DownloadStateEvent';
|
||||
import type { DownloadStatus } from '../bindings/DownloadStatus';
|
||||
import { listenEvent as listen } from '../ipc';
|
||||
|
||||
interface DownloadProgressState {
|
||||
progressMap: Record<string, DownloadProgressEvent>;
|
||||
@@ -28,7 +28,7 @@ let unlistenTray: UnlistenFn | null = null;
|
||||
|
||||
export async function initDownloadListener() {
|
||||
if (unlistenProgress) return;
|
||||
unlistenProgress = await listen<DownloadProgressEvent>('download-progress', (event) => {
|
||||
unlistenProgress = await listen('download-progress', (event) => {
|
||||
const payload = event.payload;
|
||||
useDownloadProgressStore.getState().updateDownloadProgress(payload.id, payload);
|
||||
|
||||
@@ -46,7 +46,7 @@ export async function initDownloadListener() {
|
||||
});
|
||||
|
||||
if (!unlistenState) {
|
||||
unlistenState = await listen<DownloadStateEvent>('download-state', (event) => {
|
||||
unlistenState = await listen('download-state', (event) => {
|
||||
const payload = event.payload;
|
||||
const mainStore = useDownloadStore.getState();
|
||||
const current = mainStore.downloads.find(d => d.id === payload.id);
|
||||
@@ -77,18 +77,12 @@ export async function initDownloadListener() {
|
||||
}
|
||||
|
||||
if (!unlistenTray) {
|
||||
unlistenTray = await listen<string>('tray-action', (event) => {
|
||||
unlistenTray = await listen('tray-action', (event) => {
|
||||
const mainStore = useDownloadStore.getState();
|
||||
if (event.payload === 'pause-all') {
|
||||
const uniqueQueues = Array.from(new Set(
|
||||
mainStore.downloads.map(d => d.queueId).filter((id): id is string => Boolean(id))
|
||||
));
|
||||
uniqueQueues.forEach(qid => mainStore.pauseQueue(qid));
|
||||
void mainStore.pauseAll();
|
||||
} else if (event.payload === 'resume-all') {
|
||||
const uniqueQueues = Array.from(new Set(
|
||||
mainStore.downloads.map(d => d.queueId).filter((id): id is string => Boolean(id))
|
||||
));
|
||||
uniqueQueues.forEach(qid => mainStore.startQueue(qid));
|
||||
void mainStore.startAll();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -59,8 +59,8 @@ describe('useDownloadStore', () => {
|
||||
it('Start Queue dispatches exactly once for mixed dispatched/undispatched items', async () => {
|
||||
useDownloadStore.setState({
|
||||
downloads: [
|
||||
{ id: '1', url: 'http://test1', fileName: 'f1', status: 'queued', category: 'General', dateAdded: '', queueId: 'MAIN', hasBeenDispatched: true },
|
||||
{ id: '2', url: 'http://test2', fileName: 'f2', status: 'queued', category: 'General', dateAdded: '', queueId: 'MAIN', hasBeenDispatched: false },
|
||||
{ id: '1', url: 'http://test1', fileName: 'f1', destination: '/tmp', status: 'queued', category: 'Other', dateAdded: '', queueId: 'MAIN', hasBeenDispatched: true },
|
||||
{ id: '2', url: 'http://test2', fileName: 'f2', destination: '/tmp', status: 'queued', category: 'Other', dateAdded: '', queueId: 'MAIN', hasBeenDispatched: false },
|
||||
] as any[],
|
||||
backendRegisteredIds: new Set(['1']), // 1 is already registered, so it skips dispatch
|
||||
});
|
||||
@@ -82,7 +82,7 @@ describe('useDownloadStore', () => {
|
||||
it('resumeDownload unregisters ID and re-dispatches if un-resumable', async () => {
|
||||
useDownloadStore.setState({
|
||||
downloads: [
|
||||
{ id: '1', url: 'http://test1', fileName: 'f1', status: 'paused', category: 'General', dateAdded: '', queueId: 'MAIN', hasBeenDispatched: true },
|
||||
{ id: '1', url: 'http://test1', fileName: 'f1', destination: '/tmp', status: 'paused', category: 'Other', dateAdded: '', queueId: 'MAIN', hasBeenDispatched: true },
|
||||
] as any[],
|
||||
backendRegisteredIds: new Set(['1']),
|
||||
});
|
||||
@@ -102,7 +102,7 @@ describe('useDownloadStore', () => {
|
||||
expect(useDownloadStore.getState().backendRegisteredIds.has('1')).toBe(true); // Re-registered by dispatchItem
|
||||
});
|
||||
|
||||
it('adds to the list without assigning a queue or dispatching', async () => {
|
||||
it('adds to the list in the main queue without dispatching', async () => {
|
||||
await useDownloadStore.getState().addDownload({
|
||||
id: 'list-1',
|
||||
url: 'https://example.com/list.bin',
|
||||
@@ -113,7 +113,7 @@ describe('useDownloadStore', () => {
|
||||
|
||||
const item = useDownloadStore.getState().downloads[0];
|
||||
expect(item.status).toBe('ready');
|
||||
expect(item.queueId).toBeUndefined();
|
||||
expect(item.queueId).toBe('00000000-0000-0000-0000-000000000001');
|
||||
expect(ipc.invokeCommand).not.toHaveBeenCalledWith('enqueue_download', expect.anything());
|
||||
});
|
||||
|
||||
@@ -133,7 +133,7 @@ describe('useDownloadStore', () => {
|
||||
expect(ipc.invokeCommand).not.toHaveBeenCalledWith('enqueue_download', expect.anything());
|
||||
});
|
||||
|
||||
it('starts immediately without assigning a user queue', async () => {
|
||||
it('starts immediately in the main queue', async () => {
|
||||
vi.mocked(ipc.invokeCommand).mockImplementation(async (cmd: string) => {
|
||||
if (cmd === 'get_pending_order') return ['start-1'];
|
||||
return undefined;
|
||||
@@ -148,7 +148,7 @@ describe('useDownloadStore', () => {
|
||||
}, { type: 'start-now' });
|
||||
|
||||
const item = useDownloadStore.getState().downloads[0];
|
||||
expect(item.queueId).toBeUndefined();
|
||||
expect(item.queueId).toBe('00000000-0000-0000-0000-000000000001');
|
||||
expect(item.hasBeenDispatched).toBe(true);
|
||||
expect(ipc.invokeCommand).toHaveBeenCalledWith(
|
||||
'enqueue_download',
|
||||
@@ -158,6 +158,83 @@ describe('useDownloadStore', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('starts and pauses all items regardless of legacy missing queue ids', async () => {
|
||||
useDownloadStore.setState({
|
||||
downloads: [
|
||||
{ id: 'ready', url: 'http://ready', fileName: 'ready', status: 'ready', category: 'Other', dateAdded: '' },
|
||||
{ id: 'active', url: 'http://active', fileName: 'active', status: 'processing', category: 'Other', dateAdded: '' },
|
||||
] as any[],
|
||||
});
|
||||
|
||||
vi.mocked(ipc.invokeCommand).mockImplementation(async (cmd: string) => {
|
||||
if (cmd === 'get_pending_order') return ['ready'];
|
||||
return undefined;
|
||||
});
|
||||
|
||||
expect(await useDownloadStore.getState().startAll()).toBe(1);
|
||||
expect(await useDownloadStore.getState().pauseAll()).toBe(2);
|
||||
|
||||
const calls = vi.mocked(ipc.invokeCommand).mock.calls;
|
||||
expect(calls.some(call => call[0] === 'enqueue_download')).toBe(true);
|
||||
expect(calls.some(call => call[0] === 'pause_download' && (call[1] as any).id === 'active')).toBe(true);
|
||||
});
|
||||
|
||||
it('migrates legacy downloads without queue ids into the main queue', async () => {
|
||||
vi.mocked(ipc.invokeCommand).mockImplementation(async (cmd: string) => {
|
||||
if (cmd === 'db_get_all_queues') return [];
|
||||
if (cmd === 'db_get_all_downloads') {
|
||||
return [JSON.stringify({
|
||||
id: 'legacy',
|
||||
url: 'https://example.com/legacy.bin',
|
||||
fileName: 'legacy.bin',
|
||||
status: 'ready',
|
||||
category: 'Other',
|
||||
dateAdded: ''
|
||||
})];
|
||||
}
|
||||
return undefined;
|
||||
});
|
||||
|
||||
await useDownloadStore.getState().initDB();
|
||||
|
||||
expect(useDownloadStore.getState().downloads[0].queueId)
|
||||
.toBe('00000000-0000-0000-0000-000000000001');
|
||||
});
|
||||
|
||||
it('pauses queued, downloading, processing, and retrying queue items', async () => {
|
||||
useDownloadStore.setState({
|
||||
downloads: ['queued', 'downloading', 'processing', 'retrying'].map((status, index) => ({
|
||||
id: `${index}`,
|
||||
url: `https://example.com/${index}`,
|
||||
fileName: `${index}.bin`,
|
||||
status,
|
||||
category: 'Other',
|
||||
dateAdded: '',
|
||||
queueId: 'queue-a'
|
||||
})) as any[]
|
||||
});
|
||||
vi.mocked(ipc.invokeCommand).mockResolvedValue(undefined as never);
|
||||
|
||||
expect(await useDownloadStore.getState().pauseQueue('queue-a')).toBe(4);
|
||||
expect(
|
||||
vi.mocked(ipc.invokeCommand).mock.calls.filter(call => call[0] === 'pause_download')
|
||||
).toHaveLength(4);
|
||||
});
|
||||
|
||||
it('assigns selected unfinished downloads to a queue without moving completed items', () => {
|
||||
useDownloadStore.setState({
|
||||
downloads: [
|
||||
{ id: 'ready', status: 'ready', queueId: 'old' },
|
||||
{ id: 'done', status: 'completed', queueId: 'old' }
|
||||
] as any[]
|
||||
});
|
||||
|
||||
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('preserves extension request headers and cookies for the Add modal', () => {
|
||||
useDownloadStore.getState().handleExtensionDownload({
|
||||
urls: ['https://example.com/file.bin'],
|
||||
|
||||
@@ -1,20 +1,19 @@
|
||||
import { create } from 'zustand';
|
||||
import { info } from '@tauri-apps/plugin-log';
|
||||
import { homeDir } from '@tauri-apps/api/path';
|
||||
import { invokeCommand as invoke } from '../ipc';
|
||||
|
||||
import type { DownloadItem } from '../bindings/DownloadItem';
|
||||
import type { DownloadStatus } from '../bindings/DownloadStatus';
|
||||
import type { ExtensionDownload } from '../bindings/ExtensionDownload';
|
||||
import type { Queue } from '../bindings/Queue';
|
||||
import type { MediaMetadata } from '../bindings/MediaMetadata';
|
||||
import { useSettingsStore } from './useSettingsStore';
|
||||
import { isActiveDownloadStatus, normalizeSpeedLimitForBackend, redactDownloadForPersistence } from '../utils/downloads';
|
||||
import { fetchMediaMetadataDeduped } from '../utils/mediaMetadata';
|
||||
import {
|
||||
expandTilde,
|
||||
resolveCategoryDestination,
|
||||
resolveDownloadFilePath
|
||||
} from '../utils/downloadLocations';
|
||||
import { canPauseDownload, canStartDownload } from '../utils/downloadActions';
|
||||
|
||||
export type { DownloadCategory } from '../utils/downloads';
|
||||
|
||||
@@ -31,6 +30,8 @@ export async function dispatchItem(id: string): Promise<boolean> {
|
||||
if (state.backendRegisteredIds.has(id)) return true;
|
||||
|
||||
const settings = useSettingsStore.getState();
|
||||
const destination = item.destination ||
|
||||
await resolveCategoryDestination(settings, item.category);
|
||||
const login = getSiteLogin(item.url, settings);
|
||||
let keychainPassword = null;
|
||||
if (login) {
|
||||
@@ -42,7 +43,7 @@ export async function dispatchItem(id: string): Promise<boolean> {
|
||||
const enqueueItem = {
|
||||
id: item.id,
|
||||
url: item.url,
|
||||
destination: item.destination,
|
||||
destination,
|
||||
filename: item.fileName,
|
||||
connections: item.connections || settings.perServerConnections || null,
|
||||
speed_limit: item.speedLimit || normalizeSpeedLimitForBackend(settings.globalSpeedLimit),
|
||||
@@ -126,13 +127,7 @@ const syncSystemIntegrations = () => {
|
||||
};
|
||||
|
||||
const resolveDownloadPath = async (destination: string, fileName: string) => {
|
||||
let resolvedDestination = destination;
|
||||
if (destination.startsWith('~/')) {
|
||||
resolvedDestination = await resolveDownloadFilePath(await homeDir(), destination.slice(2));
|
||||
} else if (destination === '~') {
|
||||
resolvedDestination = await homeDir();
|
||||
}
|
||||
return resolveDownloadFilePath(resolvedDestination, fileName);
|
||||
return resolveDownloadFilePath(await expandTilde(destination), fileName);
|
||||
};
|
||||
|
||||
const effectiveDestinationForItem = async (
|
||||
@@ -165,8 +160,6 @@ interface DownloadState {
|
||||
backendRegisteredIds: Set<string>;
|
||||
registerBackendIds: (ids: string[]) => void;
|
||||
unregisterBackendIds: (ids: string[]) => void;
|
||||
activeDownloadId: string | null;
|
||||
setActiveDownloadId: (id: string | null) => void;
|
||||
applyProperties: (id: string, updates: Partial<DownloadItem>) => Promise<void>;
|
||||
moveInQueue: (id: string, direction: 'up' | 'down') => Promise<void>;
|
||||
removeFromQueue: (id: string) => Promise<void>;
|
||||
@@ -197,17 +190,14 @@ interface DownloadState {
|
||||
resumeDownload: (id: string) => Promise<void>;
|
||||
startQueue: (queueId: string) => Promise<number>;
|
||||
pauseQueue: (queueId: string) => Promise<number>;
|
||||
startAll: () => Promise<number>;
|
||||
pauseAll: () => Promise<number>;
|
||||
assignToQueue: (ids: string[], queueId: string) => void;
|
||||
addQueue: (name: string) => void;
|
||||
renameQueue: (id: string, name: string) => void;
|
||||
removeQueue: (id: string) => void;
|
||||
initDB: () => Promise<void>;
|
||||
|
||||
isParsing: boolean;
|
||||
activeMetadata: MediaMetadata | null;
|
||||
activeMetadataUrl: string | null;
|
||||
parsingError: string | null;
|
||||
fetchMetadataAction: (url: string) => Promise<void>;
|
||||
clearMetadata: () => void;
|
||||
}
|
||||
|
||||
export const useDownloadStore = create<DownloadState>((set, get) => ({
|
||||
@@ -215,8 +205,6 @@ export const useDownloadStore = create<DownloadState>((set, get) => ({
|
||||
queues: [{ id: MAIN_QUEUE_ID, name: 'Main Queue', isMain: true }],
|
||||
pendingOrder: [],
|
||||
setPendingOrder: (order) => set({ pendingOrder: order }),
|
||||
activeDownloadId: null,
|
||||
setActiveDownloadId: (id) => set({ activeDownloadId: id }),
|
||||
moveInQueue: async (id, direction) => {
|
||||
try {
|
||||
const order = await invoke('move_in_queue', { id, direction });
|
||||
@@ -253,10 +241,6 @@ export const useDownloadStore = create<DownloadState>((set, get) => ({
|
||||
pendingAddHeaders: '',
|
||||
pendingAddCookies: '',
|
||||
selectedPropertiesDownloadId: null,
|
||||
isParsing: false,
|
||||
activeMetadata: null,
|
||||
activeMetadataUrl: null,
|
||||
parsingError: null,
|
||||
deleteModalState: { isOpen: false },
|
||||
openDeleteModal: (downloadIds) => set({
|
||||
deleteModalState: {
|
||||
@@ -298,24 +282,6 @@ export const useDownloadStore = create<DownloadState>((set, get) => ({
|
||||
);
|
||||
},
|
||||
setSelectedPropertiesDownloadId: (id) => set({ selectedPropertiesDownloadId: id }),
|
||||
clearMetadata: () => set({ isParsing: false, activeMetadata: null, activeMetadataUrl: null, parsingError: null }),
|
||||
fetchMetadataAction: async (url) => {
|
||||
set({ isParsing: true, parsingError: null, activeMetadata: null, activeMetadataUrl: url });
|
||||
try {
|
||||
const settings = useSettingsStore.getState();
|
||||
const metadata = await fetchMediaMetadataDeduped({
|
||||
url,
|
||||
cookieBrowser: settings.mediaCookieSource === 'none' ? null : settings.mediaCookieSource,
|
||||
username: null,
|
||||
password: null
|
||||
});
|
||||
set({ isParsing: false, activeMetadata: metadata });
|
||||
info(`Media metadata parsed for ${url}: found ${metadata.formats.length} formats`);
|
||||
} catch (e) {
|
||||
set({ isParsing: false, parsingError: String(e) });
|
||||
info(`Media metadata parsing failed for ${url}: ${e}`);
|
||||
}
|
||||
},
|
||||
addDownload: async (item, action) => {
|
||||
const settings = useSettingsStore.getState();
|
||||
const destPath = await effectiveDestinationForItem(item, settings);
|
||||
@@ -323,7 +289,7 @@ export const useDownloadStore = create<DownloadState>((set, get) => ({
|
||||
...item,
|
||||
destination: destPath,
|
||||
status: action.type === 'add-to-queue' ? 'queued' : 'ready',
|
||||
queueId: action.type === 'add-to-queue' ? action.queueId : undefined,
|
||||
queueId: action.type === 'add-to-queue' ? action.queueId : MAIN_QUEUE_ID,
|
||||
hasBeenDispatched: false
|
||||
};
|
||||
set((state) => ({ downloads: [...state.downloads, ownedItem] }));
|
||||
@@ -427,7 +393,10 @@ export const useDownloadStore = create<DownloadState>((set, get) => ({
|
||||
|
||||
set((state) => ({
|
||||
downloads: state.downloads.filter(d => d.id !== id),
|
||||
pendingOrder: state.pendingOrder.filter(x => x !== id)
|
||||
pendingOrder: state.pendingOrder.filter(x => x !== id),
|
||||
backendRegisteredIds: new Set(
|
||||
Array.from(state.backendRegisteredIds).filter(registeredId => registeredId !== id)
|
||||
)
|
||||
}));
|
||||
info(`Download ${id} removed`);
|
||||
syncSystemIntegrations();
|
||||
@@ -483,7 +452,8 @@ export const useDownloadStore = create<DownloadState>((set, get) => ({
|
||||
destination: destPath,
|
||||
isMedia: targetItem.isMedia,
|
||||
mediaFormatSelector,
|
||||
queueId: targetItem.queueId
|
||||
queueId: targetItem.queueId || MAIN_QUEUE_ID,
|
||||
hasBeenDispatched: false
|
||||
};
|
||||
|
||||
set((state) => ({
|
||||
@@ -533,13 +503,13 @@ export const useDownloadStore = create<DownloadState>((set, get) => ({
|
||||
},
|
||||
startQueue: async (queueId) => {
|
||||
const runnable = get().downloads
|
||||
.filter(item => item.queueId === queueId && (item.status === 'queued' || item.status === 'paused' || item.status === 'failed'));
|
||||
.filter(item => item.queueId === queueId && (item.status === 'queued' || canStartDownload(item.status)));
|
||||
|
||||
if (runnable.length === 0) return 0;
|
||||
|
||||
let dispatchedCount = 0;
|
||||
const promises = runnable.map(async (item) => {
|
||||
if (item.status === 'failed' || !item.hasBeenDispatched) {
|
||||
if (item.status === 'ready' || item.status === 'failed' || !item.hasBeenDispatched) {
|
||||
if (await dispatchItem(item.id)) {
|
||||
get().updateDownload(item.id, { hasBeenDispatched: true, status: 'queued' });
|
||||
dispatchedCount++;
|
||||
@@ -561,7 +531,7 @@ export const useDownloadStore = create<DownloadState>((set, get) => ({
|
||||
},
|
||||
pauseQueue: async (queueId) => {
|
||||
const activeIds = get().downloads
|
||||
.filter(item => item.queueId === queueId && item.status === 'downloading')
|
||||
.filter(item => item.queueId === queueId && canPauseDownload(item.status))
|
||||
.map(item => item.id);
|
||||
|
||||
if (activeIds.length === 0) return 0;
|
||||
@@ -578,6 +548,43 @@ export const useDownloadStore = create<DownloadState>((set, get) => ({
|
||||
syncSystemIntegrations();
|
||||
return pausedCount;
|
||||
},
|
||||
startAll: async () => {
|
||||
set(state => ({
|
||||
downloads: state.downloads.map(item =>
|
||||
item.queueId ? item : { ...item, queueId: MAIN_QUEUE_ID }
|
||||
)
|
||||
}));
|
||||
const queueIds = new Set(
|
||||
get().downloads
|
||||
.filter(item => item.status === 'queued' || canStartDownload(item.status))
|
||||
.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);
|
||||
},
|
||||
pauseAll: async () => {
|
||||
const activeIds = get().downloads
|
||||
.filter(item => canPauseDownload(item.status))
|
||||
.map(item => item.id);
|
||||
if (activeIds.length === 0) return 0;
|
||||
|
||||
const results = await Promise.allSettled(
|
||||
activeIds.map(id => invoke('pause_download', { id }))
|
||||
);
|
||||
const pausedCount = results.filter(result => result.status === 'fulfilled').length;
|
||||
syncSystemIntegrations();
|
||||
return pausedCount;
|
||||
},
|
||||
assignToQueue: (ids, queueId) => {
|
||||
const selectedIds = new Set(ids);
|
||||
set(state => ({
|
||||
downloads: state.downloads.map(item =>
|
||||
selectedIds.has(item.id) && item.status !== 'completed'
|
||||
? { ...item, queueId }
|
||||
: item
|
||||
)
|
||||
}));
|
||||
},
|
||||
addQueue: (name) => {
|
||||
const id = crypto.randomUUID();
|
||||
const q = { id, name, isMain: false };
|
||||
@@ -615,7 +622,12 @@ export const useDownloadStore = create<DownloadState>((set, get) => ({
|
||||
|
||||
set(state => ({
|
||||
queues: queues.length > 0 ? queues : state.queues,
|
||||
downloads: downloads.length > 0 ? downloads : state.downloads
|
||||
downloads: downloads.length > 0
|
||||
? downloads.map(download => ({
|
||||
...download,
|
||||
queueId: download.queueId || MAIN_QUEUE_ID
|
||||
}))
|
||||
: state.downloads
|
||||
}));
|
||||
|
||||
// Reset interrupted active downloads to queued.
|
||||
@@ -666,9 +678,24 @@ export const useDownloadStore = create<DownloadState>((set, get) => ({
|
||||
is_media: item.isMedia || false
|
||||
});
|
||||
}
|
||||
await invoke('enqueue_many', { items: itemsToEnqueue });
|
||||
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');
|
||||
set({ pendingOrder: order });
|
||||
set(state => ({
|
||||
pendingOrder: order,
|
||||
backendRegisteredIds: new Set([
|
||||
...state.backendRegisteredIds,
|
||||
...registeredIds
|
||||
]),
|
||||
downloads: state.downloads.map(download =>
|
||||
failedIds.has(download.id)
|
||||
? { ...download, status: 'failed' as const }
|
||||
: registeredIds.includes(download.id)
|
||||
? { ...download, hasBeenDispatched: true }
|
||||
: download
|
||||
)
|
||||
}));
|
||||
} catch (e) {
|
||||
console.error("Failed to auto-resume active downloads:", e);
|
||||
}
|
||||
|
||||
@@ -137,7 +137,7 @@ export interface SettingsState {
|
||||
resetCategoryLocations: () => void;
|
||||
addSiteLogin: (login: SiteLogin) => void;
|
||||
removeSiteLogin: (id: string) => void;
|
||||
regeneratePairingToken: () => void;
|
||||
regeneratePairingToken: () => Promise<void>;
|
||||
setAutoCheckUpdates: (autoCheckUpdates: boolean) => void;
|
||||
hydratePairingToken: () => Promise<boolean>;
|
||||
}
|
||||
@@ -286,12 +286,10 @@ export const useSettingsStore = create<SettingsState>()(
|
||||
removeSiteLogin: (id) => set((state) => ({
|
||||
siteLogins: state.siteLogins.filter((login) => login.id !== id)
|
||||
})),
|
||||
regeneratePairingToken: () => {
|
||||
regeneratePairingToken: async () => {
|
||||
const token = generateSecureToken();
|
||||
await invoke('set_keychain_password', { id: PAIRING_TOKEN_KEYCHAIN_ID, password: token });
|
||||
set({ extensionPairingToken: token });
|
||||
invoke('set_keychain_password', { id: PAIRING_TOKEN_KEYCHAIN_ID, password: token }).catch(e => {
|
||||
console.error('Failed to persist regenerated extension pairing token to keychain:', e);
|
||||
});
|
||||
},
|
||||
hydratePairingToken: async () => {
|
||||
const result = await invoke('hydrate_extension_pairing_token');
|
||||
|
||||
Reference in New Issue
Block a user