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
+
+
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) {