From b0bcea2f10ff00f8b117141bdd0d4653b5928c5a Mon Sep 17 00:00:00 2001 From: Davide Grilli Date: Mon, 16 Mar 2026 14:54:23 +0100 Subject: [PATCH] feat(ui): aggiunge barra di progresso durante lo smistamento MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mostra una progress bar con messaggio contestuale nelle 4 fasi: scansione sorgente, analisi cartella destinazione (con contatore cartelle), ricerca duplicati (con contatore file CAD), smistamento (N / totale). La barra รจ indeterminata nelle prime tre fasi e determinata durante la copia. Il canale IPC progress viene aperto all'inizio e chiuso al termine dell'operazione --- main.js | 18 ++++++++----- preload.js | 2 ++ renderer/index.html | 52 ++++++++++++++++++++++++++++++++++++ renderer/renderer.js | 34 +++++++++++++++++++++++ services/destinationIndex.js | 7 ++++- services/duplicateIndex.js | 6 ++++- services/folderProcessor.js | 14 +++++++--- services/zipProcessor.js | 19 ++++++++----- 8 files changed, 133 insertions(+), 19 deletions(-) diff --git a/main.js b/main.js index ef35659..c693662 100644 --- a/main.js +++ b/main.js @@ -142,7 +142,7 @@ app.on('window-all-closed', () => { if (process.platform !== 'darwin') app.quit(); }); -ipcMain.handle('select-folder', async () => { +ipcMain.handle('select-folder', async (event) => { const result = await dialog.showOpenDialog({ properties: ['openDirectory'], }); @@ -151,11 +151,12 @@ ipcMain.handle('select-folder', async () => { return { canceled: true }; } - const routingResult = await processFolder(result.filePaths[0], config); + const onProgress = (data) => event.sender.send('progress', data); + const routingResult = await processFolder(result.filePaths[0], config, onProgress); return { canceled: false, ...routingResult }; }); -ipcMain.handle('select-zip', async () => { +ipcMain.handle('select-zip', async (event) => { const result = await dialog.showOpenDialog({ properties: ['openFile'], filters: [{ name: 'Zip', extensions: ['zip'] }], @@ -165,11 +166,12 @@ ipcMain.handle('select-zip', async () => { return { canceled: true }; } - const routingResult = await processZip(result.filePaths[0], config); + const onProgress = (data) => event.sender.send('progress', data); + const routingResult = await processZip(result.filePaths[0], config, onProgress); return { canceled: false, ...routingResult }; }); -ipcMain.handle('process-dropped-path', async (_event, payload) => { +ipcMain.handle('process-dropped-path', async (event, payload) => { const droppedPath = String(payload?.path || '').trim(); if (!droppedPath) { throw new Error('Percorso non valido'); @@ -180,13 +182,15 @@ ipcMain.handle('process-dropped-path', async (_event, payload) => { throw new Error('Percorso non trovato'); } + const onProgress = (data) => event.sender.send('progress', data); + if (stats.isDirectory()) { - const routingResult = await processFolder(droppedPath, config); + const routingResult = await processFolder(droppedPath, config, onProgress); return { canceled: false, sourceType: 'folder', ...routingResult }; } if (stats.isFile() && path.extname(droppedPath).toLowerCase() === '.zip') { - const routingResult = await processZip(droppedPath, config); + const routingResult = await processZip(droppedPath, config, onProgress); return { canceled: false, sourceType: 'zip', ...routingResult }; } diff --git a/preload.js b/preload.js index 8bf2cd8..bd951a5 100644 --- a/preload.js +++ b/preload.js @@ -15,4 +15,6 @@ contextBridge.exposeInMainWorld('api', { clearUnroutedFiles: () => ipcRenderer.invoke('clear-unrouted-files'), clearDuplicatesFiles: () => ipcRenderer.invoke('clear-duplicates-files'), clearSkippedFiles: () => ipcRenderer.invoke('clear-skipped-files'), + onProgress: (cb) => ipcRenderer.on('progress', (_e, data) => cb(data)), + offProgress: () => ipcRenderer.removeAllListeners('progress'), }); diff --git a/renderer/index.html b/renderer/index.html index 498a0c7..5ba05b4 100644 --- a/renderer/index.html +++ b/renderer/index.html @@ -254,6 +254,53 @@ overflow: auto; min-height: 180px; } + + .progress-container { + margin-top: 18px; + display: none; + } + + .progress-label { + font-size: 13px; + color: var(--muted); + margin-bottom: 6px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + } + + progress { + width: 100%; + height: 6px; + border-radius: 4px; + border: none; + appearance: none; + -webkit-appearance: none; + background: var(--border); + overflow: hidden; + } + + progress::-webkit-progress-bar { + background: var(--border); + border-radius: 4px; + } + + progress::-webkit-progress-value { + background: var(--accent); + border-radius: 4px; + transition: width 80ms ease; + } + + progress:indeterminate { + background: linear-gradient(90deg, var(--border) 25%, var(--accent) 50%, var(--border) 75%); + background-size: 200% 100%; + animation: progress-indeterminate 1.2s linear infinite; + } + + @keyframes progress-indeterminate { + 0% { background-position: 100% 0; } + 100% { background-position: -100% 0; } + } @@ -270,6 +317,11 @@ Trascina qui una cartella o un file .zip +
+
In corso...
+ +
+

Destinazione file CAD

diff --git a/renderer/renderer.js b/renderer/renderer.js index 3708a4d..81cab9b 100644 --- a/renderer/renderer.js +++ b/renderer/renderer.js @@ -15,10 +15,40 @@ const clearPreviewBtn = document.getElementById('clearPreviewBtn'); const previewTitle = document.getElementById('previewTitle'); const previewMeta = document.getElementById('previewMeta'); const previewList = document.getElementById('previewList'); +const progressContainer = document.getElementById('progressContainer'); +const progressBar = document.getElementById('progressBar'); +const progressLabel = document.getElementById('progressLabel'); const defaultDropZoneText = 'Trascina qui una cartella o un file .zip'; let isProcessing = false; let currentPreviewKind = null; +function showProgress() { + progressContainer.style.display = 'block'; + progressBar.removeAttribute('value'); + progressLabel.textContent = 'In corso...'; +} + +function hideProgress() { + progressContainer.style.display = 'none'; +} + +function updateProgress({ phase, current, total, file, scanned, folder }) { + if (phase === 'scan') { + progressLabel.textContent = 'Scansione file sorgente...'; + progressBar.removeAttribute('value'); + } else if (phase === 'index-dest') { + progressLabel.textContent = `Analisi destinazione: ${scanned} cartelle scansionate${folder ? ` โ€” ${folder}` : ''}`; + progressBar.removeAttribute('value'); + } else if (phase === 'index-dup') { + progressLabel.textContent = `Ricerca duplicati: ${scanned} file CAD trovati${file ? ` โ€” ${file}` : ''}`; + progressBar.removeAttribute('value'); + } else if (phase === 'copy') { + progressBar.max = total; + progressBar.value = current; + progressLabel.textContent = `Smistamento: ${current} / ${total}${file ? ` โ€” ${file}` : ''}`; + } +} + function setLoading(isLoading) { folderBtn.disabled = isLoading; zipBtn.disabled = isLoading; @@ -101,6 +131,8 @@ async function handleAction(actionName, actionFn) { isProcessing = true; setLoading(true); + showProgress(); + window.api.onProgress(updateProgress); output.textContent = `${actionName} in corso...`; try { @@ -115,6 +147,8 @@ async function handleAction(actionName, actionFn) { } catch (error) { output.textContent = `${actionName} fallito:\n${error.message}`; } finally { + window.api.offProgress(); + hideProgress(); setLoading(false); isProcessing = false; } diff --git a/services/destinationIndex.js b/services/destinationIndex.js index f1eb6d5..b6ed712 100644 --- a/services/destinationIndex.js +++ b/services/destinationIndex.js @@ -4,13 +4,15 @@ const path = require('path'); const CODE_FOLDER_REGEX = /^\d{3}$/; const MAX_SCAN_DEPTH = 6; -async function buildDestinationIndex(destinationRoot) { +async function buildDestinationIndex(destinationRoot, onProgress) { const index = new Map(); if (!destinationRoot || !(await fs.pathExists(destinationRoot))) { return index; } + let scanned = 0; + async function walk(currentDir, depth) { if (depth > MAX_SCAN_DEPTH) { return; @@ -29,6 +31,9 @@ async function buildDestinationIndex(destinationRoot) { } const fullPath = path.join(currentDir, entry.name); + scanned += 1; + onProgress?.({ phase: 'index-dest', scanned, folder: entry.name }); + if (CODE_FOLDER_REGEX.test(entry.name)) { const rows = index.get(entry.name) || []; rows.push(fullPath); diff --git a/services/duplicateIndex.js b/services/duplicateIndex.js index 4ad10b6..0d36389 100644 --- a/services/duplicateIndex.js +++ b/services/duplicateIndex.js @@ -8,13 +8,15 @@ function toCadKey(cadInfo) { return String(cadInfo?.key || '').toLowerCase(); } -async function buildExistingCadKeyIndex(destinationRoot) { +async function buildExistingCadKeyIndex(destinationRoot, onProgress) { const keys = new Set(); if (!destinationRoot || !(await fs.pathExists(destinationRoot))) { return keys; } + let scanned = 0; + async function walk(currentDir, depth) { if (depth > MAX_SCAN_DEPTH) { return; @@ -44,6 +46,8 @@ async function buildExistingCadKeyIndex(destinationRoot) { continue; } + scanned += 1; + onProgress?.({ phase: 'index-dup', scanned, file: entry.name }); keys.add(toCadKey(cadInfo)); } } diff --git a/services/folderProcessor.js b/services/folderProcessor.js index 79d5471..5d7cc56 100644 --- a/services/folderProcessor.js +++ b/services/folderProcessor.js @@ -66,10 +66,12 @@ async function collectFilesRecursively(rootDir) { return files; } -async function processFolder(folder, config) { +async function processFolder(folder, config, onProgress) { + onProgress?.({ phase: 'scan' }); const files = await collectFilesRecursively(folder); - const destinationIndex = await buildDestinationIndex(config?.destination); - const existingCadKeys = await buildExistingCadKeyIndex(config?.destination); + + const destinationIndex = await buildDestinationIndex(config?.destination, onProgress); + const existingCadKeys = await buildExistingCadKeyIndex(config?.destination, onProgress); const sourceMaxVersions = buildHighestNumericVersionByCadKey(files); const result = { scanned: 0, @@ -80,9 +82,13 @@ async function processFolder(folder, config) { details: [], }; - for (const { fullPath, fileName } of files) { + const total = files.length; + + for (let i = 0; i < files.length; i++) { + const { fullPath, fileName } = files[i]; const file = fileName; result.scanned += 1; + onProgress?.({ phase: 'copy', current: i + 1, total, file }); const cadInfo = getCadInfo(file); if (!cadInfo) { diff --git a/services/zipProcessor.js b/services/zipProcessor.js index 9047c51..217ea55 100644 --- a/services/zipProcessor.js +++ b/services/zipProcessor.js @@ -27,7 +27,7 @@ async function buildZipHighestNumericVersionByCadKey(zipPath) { try { directory = await unzipper.Open.file(zipPath); } catch { - return index; + return { index, total: 0 }; } for (const row of directory.files || []) { @@ -53,14 +53,17 @@ async function buildZipHighestNumericVersionByCadKey(zipPath) { } } - return index; + return { index, total: (directory.files || []).filter((f) => f.type === 'File').length }; } -async function processZip(zipPath, config) { +async function processZip(zipPath, config, onProgress) { + onProgress?.({ phase: 'scan' }); + const { index: sourceMaxVersions, total } = await buildZipHighestNumericVersionByCadKey(zipPath); + + const destinationIndex = await buildDestinationIndex(config?.destination, onProgress); + const existingCadKeys = await buildExistingCadKeyIndex(config?.destination, onProgress); + const stream = fs.createReadStream(zipPath).pipe(unzipper.Parse({ forceStream: true })); - const destinationIndex = await buildDestinationIndex(config?.destination); - const existingCadKeys = await buildExistingCadKeyIndex(config?.destination); - const sourceMaxVersions = await buildZipHighestNumericVersionByCadKey(zipPath); const result = { scanned: 0, copied: 0, @@ -70,6 +73,8 @@ async function processZip(zipPath, config) { details: [], }; + let current = 0; + for await (const entry of stream) { if (entry.type !== 'File') { entry.autodrain(); @@ -78,7 +83,9 @@ async function processZip(zipPath, config) { const file = entry.path; const baseName = path.basename(file); + current += 1; result.scanned += 1; + onProgress?.({ phase: 'copy', current, total, file: baseName }); const cadInfo = getCadInfo(baseName); if (!cadInfo) {