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:
18
main.js
18
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 };
|
||||
}
|
||||
|
||||
|
||||
@@ -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'),
|
||||
});
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user