diff --git a/renderer/app.js b/renderer/app.js index 74ad8fa..cab8573 100644 --- a/renderer/app.js +++ b/renderer/app.js @@ -71,6 +71,7 @@ let _sessionUploadedBytes = 0; // Bytes fully uploaded this session (done jobs) const _sessionTrackedJobs = new Set(); // Job IDs already counted for totalBytes const _sessionDoneJobs = new Set(); // Job IDs already counted for uploadedBytes const _completedUploadKeys = new Set(); // 'filepath|hoster' keys for done uploads (survives removeFromQueueOnDone) +const _suppressedPreviewKeys = new Set(); const _deletedJobIds = new Set(); // IDs of jobs explicitly deleted by user (prevents re-creation from stale progress callbacks) // Coalesce removeFromQueueOnDone removals into one filter pass per microtask // to avoid O(N²) behaviour when a burst of jobs finish at once. Logic now @@ -215,6 +216,7 @@ async function init() { } if (newFiles.length > 0) { const newPaths = new Set(newFiles.map(f => f.path)); + clearDedupKeysForPaths(newPaths); selectedFiles.push(...newFiles); buildQueuePreview(); updateUploadView(); @@ -671,12 +673,7 @@ function applyHosterSelection() { selectedFiles.push(..._pendingFiles); _pendingFiles = []; } - if (pendingPaths.size > 0) { - for (const key of [..._completedUploadKeys]) { - const sep = key.lastIndexOf('|'); - if (sep > 0 && pendingPaths.has(key.slice(0, sep))) _completedUploadKeys.delete(key); - } - } + clearDedupKeysForPaths(pendingPaths); renderHosterSummary(); // During an active upload, build preview jobs for the new files and inject @@ -725,6 +722,12 @@ function restoreQueueStateFromConfig() { } } + if (Array.isArray(pending.suppressedKeys)) { + for (const k of pending.suppressedKeys) { + if (typeof k === 'string' && k) _suppressedPreviewKeys.add(k); + } + } + selectedUploadHosters = Array.isArray(pending.selectedUploadHosters) ? pending.selectedUploadHosters.filter(Boolean) : selectedUploadHosters; @@ -802,11 +805,17 @@ function buildPersistedQueueState() { const sep = k.lastIndexOf('|'); if (sep > 0 && selectedFileMap.has(k.slice(0, sep))) completedKeys.push(k); } + const suppressedKeys = []; + for (const k of _suppressedPreviewKeys) { + const sep = k.lastIndexOf('|'); + if (sep > 0 && selectedFileMap.has(k.slice(0, sep))) suppressedKeys.push(k); + } return { savedAt: Date.now(), selectedUploadHosters: getSelectedHosters(), selectedFiles: Array.from(selectedFileMap.values()), completedKeys, + suppressedKeys, queueJobs: queueJobs.map(job => { const isTerminal = TERMINAL.has(job.status); return { @@ -1062,6 +1071,25 @@ function updateQueueActionButtons() { if (moveBottomBtn) moveBottomBtn.disabled = !hasMovableSelection; } +function clearDedupKeysForPaths(pathSet) { + if (!pathSet || pathSet.size === 0) return; + for (const set of [_completedUploadKeys, _suppressedPreviewKeys]) { + for (const key of [...set]) { + const sep = key.lastIndexOf('|'); + if (sep > 0 && pathSet.has(key.slice(0, sep))) set.delete(key); + } + } +} + +function suppressPreviewKeysStillSelected(keys) { + if (!keys || keys.length === 0) return; + const stillSelected = new Set(selectedFiles.map(f => f.path)); + for (const key of keys) { + const sep = key.lastIndexOf('|'); + if (sep > 0 && stillSelected.has(key.slice(0, sep))) _suppressedPreviewKeys.add(key); + } +} + // Build preview jobs from selected files x selected hosters (before upload starts) function buildQueuePreview() { const hosters = getSelectedHosters(); @@ -1084,7 +1112,7 @@ function buildQueuePreview() { for (const file of selectedFiles) { for (const hoster of hosters) { const key = `${file.path}|${hoster}`; - if (!existingKeys.has(key) && !_completedUploadKeys.has(key)) { + if (!existingKeys.has(key) && !_completedUploadKeys.has(key) && !_suppressedPreviewKeys.has(key)) { const job = { id: `preview-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`, file: file.path, fileName: file.name, hoster, @@ -1870,12 +1898,18 @@ document.addEventListener('keydown', (e) => { return j && (j.status === 'uploading' || j.status === 'queued' || j.status === 'retrying' || j.status === 'getting-server'); }); if (activeIds.length > 0) window.api.cancelSelectedJobs(activeIds); + const _deletedKeys = []; queueJobs = queueJobs.filter(j => { - if (selectedJobIds.has(j.id)) { removeJobFromIndex(j); return false; } + if (selectedJobIds.has(j.id)) { + if (j.file && j.hoster && j.status !== 'done') _deletedKeys.push(`${j.file}|${j.hoster}`); + removeJobFromIndex(j); + return false; + } return true; }); selectedJobIds.clear(); syncSelectedFilesFromQueue(); + suppressPreviewKeysStillSelected(_deletedKeys); renderQueueTable(); if (queueJobs.length === 0) { selectedFiles = []; updateUploadView(); } updateStatusBar(); @@ -1911,8 +1945,10 @@ async function handleContextAction(action) { return j && (j.status === 'uploading' || j.status === 'queued' || j.status === 'retrying' || j.status === 'getting-server'); }); if (activeIds.length > 0) window.api.cancelSelectedJobs(activeIds); + const _deletedKeys = []; queueJobs = queueJobs.filter(j => { if (selectedJobIds.has(j.id)) { + if (j.file && j.hoster && j.status !== 'done') _deletedKeys.push(`${j.file}|${j.hoster}`); removeJobFromIndex(j); return false; } @@ -1920,6 +1956,7 @@ async function handleContextAction(action) { }); selectedJobIds.clear(); syncSelectedFilesFromQueue(); + suppressPreviewKeysStillSelected(_deletedKeys); renderQueueTable(); if (queueJobs.length === 0) { selectedFiles = []; updateUploadView(); } updateStatusBar(); @@ -2604,6 +2641,12 @@ async function retrySelectedJobs() { } }); if (retryJobs.length === 0) return; + for (const j of retryJobs) { + if (j.file && j.hoster) { + _completedUploadKeys.delete(`${j.file}|${j.hoster}`); + _suppressedPreviewKeys.delete(`${j.file}|${j.hoster}`); + } + } // Select the retry jobs and start them immediately. // No renderQueueTable / updateQueueActionButtons / updateStatusBar here: