feat(ui): aggiunge barra di progresso durante lo smistamento

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
This commit is contained in:
2026-03-16 14:54:23 +01:00
parent 57f8b230b2
commit b0bcea2f10
8 changed files with 133 additions and 19 deletions

18
main.js
View File

@@ -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 };
}

View File

@@ -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'),
});

View File

@@ -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; }
}
</style>
</head>
<body>
@@ -270,6 +317,11 @@
Trascina qui una cartella o un file .zip
</section>
<div class="progress-container" id="progressContainer">
<div class="progress-label" id="progressLabel">In corso...</div>
<progress id="progressBar"></progress>
</div>
<section class="rules">
<h2>Destinazione file CAD</h2>
<div class="rule-row single-row">

View File

@@ -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;
}

View File

@@ -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);

View File

@@ -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));
}
}

View File

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

View File

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