Add CadSearchGui: WinForms app to index and search the CAD archive
Indicizza le cartelle __Accessori, __B.end, __Corpi, __Fondi in un DB SQLite locale ed espone una GUI di ricerca (codice, tipo, cartella, solo ultima versione) con aggiornamento incrementale dell'indice e stima del tempo rimanente. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
commit
82a69e0d20
8 files changed
+956
No files matched your search
+21
@@ -0,0 +1,21 @@
|
|||||||
|
# Build output
|
||||||
|
bin/
|
||||||
|
obj/
|
||||||
|
publish/
|
||||||
|
|
||||||
|
# Visual Studio / Rider
|
||||||
|
.vs/
|
||||||
|
*.user
|
||||||
|
*.suo
|
||||||
|
|
||||||
|
# Database generato dallo scan (rigenerabile con "Aggiorna indice")
|
||||||
|
*.db
|
||||||
|
*.db-journal
|
||||||
|
*.db-wal
|
||||||
|
*.db-shm
|
||||||
|
|
||||||
|
# Log
|
||||||
|
*.log
|
||||||
|
|
||||||
|
# Stato locale degli strumenti agentici
|
||||||
|
.claude/
|
||||||
@@ -0,0 +1,80 @@
|
|||||||
|
# CLAUDE.md
|
||||||
|
|
||||||
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||||
|
|
||||||
|
## Cos'è questo progetto
|
||||||
|
|
||||||
|
`CadSearchGui` è un'app Windows Forms (.NET 10, WinForms) che indicizza e permette di
|
||||||
|
cercare i file CAD Pro-ENGINEER/Creo contenuti in quattro cartelle di un disco/share
|
||||||
|
di rete (`__Accessori`, `__B.end`, `__Corpi`, `__Fondi` sotto una radice, di default
|
||||||
|
`X:\`), salvando i metadati in un DB SQLite locale (`cad_index.db`).
|
||||||
|
|
||||||
|
## Comandi
|
||||||
|
|
||||||
|
Tutti da eseguire dentro `CadSearchGui/`.
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
dotnet build -c Release
|
||||||
|
dotnet run -c Release
|
||||||
|
```
|
||||||
|
|
||||||
|
Pubblicazione come eseguibile standalone (single-file, self-contained, Windows x64):
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
dotnet publish -c Release -r win-x64 --self-contained true -p:PublishSingleFile=true -o publish
|
||||||
|
```
|
||||||
|
|
||||||
|
Non ci sono test automatizzati nel repo.
|
||||||
|
|
||||||
|
## Architettura
|
||||||
|
|
||||||
|
Il programma è tutto in `CadSearchGui/` (nessun altro progetto nella solution — in
|
||||||
|
passato esisteva anche una CLI separata e uno script Python di indicizzazione,
|
||||||
|
entrambi rimossi: ora l'unico programma è questo, con GUI e indicizzazione integrate).
|
||||||
|
|
||||||
|
- **Program.cs** — entry point WinForms standard, avvia `Form1`.
|
||||||
|
- **Form1.cs** / **Form1.Designer.cs** — finestra principale. La UI è costruita
|
||||||
|
interamente in codice in `Form1.BuildUi()` (nessun designer visuale usato); i
|
||||||
|
controlli sono campi privati (`_txtCerca`, `_cmbTipo`, `_cmbCartella`, `_chkUltima`,
|
||||||
|
`_grid`, ecc.). `Form1.Designer.cs` contiene solo lo scheletro minimo
|
||||||
|
(`InitializeComponent`/`Dispose`) generato dal template.
|
||||||
|
- **Indexer.cs** — logica di scansione del disco e scrittura/aggiornamento del DB
|
||||||
|
SQLite (`Indexer.ScanAsync`). Cammina le 4 cartelle radice (`Indexer.CartelleRadice`)
|
||||||
|
con un walk manuale a stack (`Walk`, più veloce di `EnumerateFiles(..., AllDirectories)`
|
||||||
|
su share di rete con centinaia di migliaia di file), estrae da ogni nome file il
|
||||||
|
pattern Pro-E `<codice>.<tipo>.<versione>` via regex, e fa un upsert incrementale
|
||||||
|
riga per riga (confronto per `percorso_completo` + dimensione + data modifica).
|
||||||
|
Riporta il progresso tramite `IProgress<ScanProgress>`, marshalato automaticamente
|
||||||
|
sul thread UI. Contiene anche lo schema SQL del DB (tabella `files` + vista
|
||||||
|
`files_ultima_versione`, che seleziona solo l'ultima versione per
|
||||||
|
`(cartella_radice, codice, tipo_file)` usando `ROW_NUMBER() OVER (...)` — non un
|
||||||
|
self-join con `GROUP BY`, che su ~200k righe degenera in tempi quasi quadratici).
|
||||||
|
- **ProgressForm.cs** — dialog modale mostrato durante l'aggiornamento indice: barra
|
||||||
|
di progresso, conteggi nuovi/aggiornati/invariati/rimossi, tempo trascorso e stima
|
||||||
|
del tempo rimanente (calcolata dal ritmo file/secondo rispetto al totale atteso,
|
||||||
|
preso dal conteggio della scansione precedente salvato nel DB).
|
||||||
|
|
||||||
|
### Schema del DB (`cad_index.db`)
|
||||||
|
|
||||||
|
Tabella `files`: un record per ogni file trovato, con `percorso_completo` (chiave
|
||||||
|
univoca), `cartella_radice`, `famiglia` (primo livello di sottocartella),
|
||||||
|
`codice`/`tipo_file`/`versione` estratti dal nome file, dimensione, data modifica e
|
||||||
|
timestamp dell'ultima scansione (`scansionato_il`, usato per rilevare ed eliminare i
|
||||||
|
file non più presenti su disco a fine scansione).
|
||||||
|
|
||||||
|
Vista `files_ultima_versione`: stessa struttura, filtrata alla sola versione più
|
||||||
|
recente per codice+tipo.
|
||||||
|
|
||||||
|
### Comportamento di ricerca di default
|
||||||
|
|
||||||
|
La ricerca di default (nessun filtro esplicito dall'utente) mostra solo i tipi
|
||||||
|
`prt`/`asm` e solo l'ultima versione — vedi `OpzionePrtAsm`/`TipiPredefiniti` in
|
||||||
|
`Form1.cs`. L'utente può comunque scegliere "Tutti i tipi" o un tipo specifico dal
|
||||||
|
combo, e togliere la spunta "Solo ultima versione".
|
||||||
|
|
||||||
|
### Percorso del DB
|
||||||
|
|
||||||
|
All'avvio la app cerca `cad_index.db` accanto all'eseguibile o nella working
|
||||||
|
directory corrente; se non lo trova, l'utente può selezionarlo con "Apri DB...".
|
||||||
|
Il bottone "Aggiorna indice" rilancia `Indexer.ScanAsync` sullo stesso path (radice
|
||||||
|
disco di default `X:\`, hardcoded in `Form1.RadiceDiscoDefault`).
|
||||||
@@ -0,0 +1,15 @@
|
|||||||
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<OutputType>WinExe</OutputType>
|
||||||
|
<TargetFramework>net10.0-windows</TargetFramework>
|
||||||
|
<Nullable>enable</Nullable>
|
||||||
|
<UseWindowsForms>true</UseWindowsForms>
|
||||||
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<PackageReference Include="Microsoft.Data.Sqlite" Version="10.0.12" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
</Project>
|
||||||
Generated
+30
@@ -0,0 +1,30 @@
|
|||||||
|
#nullable enable
|
||||||
|
|
||||||
|
namespace CadSearchGui;
|
||||||
|
|
||||||
|
partial class Form1
|
||||||
|
{
|
||||||
|
private System.ComponentModel.IContainer? components = null;
|
||||||
|
|
||||||
|
protected override void Dispose(bool disposing)
|
||||||
|
{
|
||||||
|
if (disposing && (components != null))
|
||||||
|
{
|
||||||
|
components.Dispose();
|
||||||
|
}
|
||||||
|
base.Dispose(disposing);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void InitializeComponent()
|
||||||
|
{
|
||||||
|
components = new System.ComponentModel.Container();
|
||||||
|
SuspendLayout();
|
||||||
|
AutoScaleDimensions = new SizeF(7F, 15F);
|
||||||
|
AutoScaleMode = AutoScaleMode.Font;
|
||||||
|
ClientSize = new Size(1000, 650);
|
||||||
|
MinimumSize = new Size(700, 400);
|
||||||
|
Name = "Form1";
|
||||||
|
Text = "CadSearch — ricerca archivio CAD";
|
||||||
|
ResumeLayout(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,407 @@
|
|||||||
|
using System.Diagnostics;
|
||||||
|
using Microsoft.Data.Sqlite;
|
||||||
|
|
||||||
|
namespace CadSearchGui;
|
||||||
|
|
||||||
|
public partial class Form1 : Form
|
||||||
|
{
|
||||||
|
private readonly TextBox _txtCerca = new();
|
||||||
|
private readonly ComboBox _cmbTipo = new();
|
||||||
|
private readonly ComboBox _cmbCartella = new();
|
||||||
|
private readonly CheckBox _chkUltima = new();
|
||||||
|
private readonly Button _btnCerca = new();
|
||||||
|
private readonly Button _btnApriDb = new();
|
||||||
|
private readonly Button _btnAggiorna = new();
|
||||||
|
private readonly DataGridView _grid = new();
|
||||||
|
private readonly Label _lblStatus = new();
|
||||||
|
private readonly Label _lblDb = new();
|
||||||
|
|
||||||
|
private const string OpzionePrtAsm = "Solo prt/asm (predefinito)";
|
||||||
|
private const string OpzioneTuttiTipi = "Tutti i tipi";
|
||||||
|
private static readonly string[] TipiPredefiniti = { "prt", "asm" };
|
||||||
|
private const string RadiceDiscoDefault = @"X:\";
|
||||||
|
|
||||||
|
private string? _dbPath;
|
||||||
|
|
||||||
|
public Form1()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
BuildUi();
|
||||||
|
Load += (_, _) => InizializzaDatabase();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void BuildUi()
|
||||||
|
{
|
||||||
|
var topPanel = new TableLayoutPanel
|
||||||
|
{
|
||||||
|
Dock = DockStyle.Top,
|
||||||
|
Height = 76,
|
||||||
|
ColumnCount = 7,
|
||||||
|
Padding = new Padding(8),
|
||||||
|
RowCount = 2,
|
||||||
|
};
|
||||||
|
topPanel.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 40));
|
||||||
|
topPanel.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 15));
|
||||||
|
topPanel.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 20));
|
||||||
|
topPanel.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 130));
|
||||||
|
topPanel.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 90));
|
||||||
|
topPanel.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 110));
|
||||||
|
topPanel.ColumnStyles.Add(new ColumnStyle(SizeType.Absolute, 130));
|
||||||
|
|
||||||
|
_txtCerca.Dock = DockStyle.Fill;
|
||||||
|
_txtCerca.Font = new Font(_txtCerca.Font!.FontFamily, 11f);
|
||||||
|
_txtCerca.PlaceholderText = "Cerca per codice o nome file (es. 9231000036)...";
|
||||||
|
_txtCerca.KeyDown += (_, e) =>
|
||||||
|
{
|
||||||
|
if (e.KeyCode == Keys.Enter)
|
||||||
|
{
|
||||||
|
e.SuppressKeyPress = true;
|
||||||
|
EseguiRicerca();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
_cmbTipo.Dock = DockStyle.Fill;
|
||||||
|
_cmbTipo.DropDownStyle = ComboBoxStyle.DropDownList;
|
||||||
|
_cmbTipo.Items.Add(OpzionePrtAsm);
|
||||||
|
_cmbTipo.Items.Add(OpzioneTuttiTipi);
|
||||||
|
|
||||||
|
_cmbCartella.Dock = DockStyle.Fill;
|
||||||
|
_cmbCartella.DropDownStyle = ComboBoxStyle.DropDownList;
|
||||||
|
_cmbCartella.Items.Add("Tutte le cartelle");
|
||||||
|
_cmbCartella.Items.AddRange(new object[] { "__Accessori", "__B.end", "__Corpi", "__Fondi" });
|
||||||
|
_cmbCartella.SelectedIndex = 0;
|
||||||
|
|
||||||
|
_chkUltima.Text = "Solo ultima versione";
|
||||||
|
_chkUltima.Dock = DockStyle.Fill;
|
||||||
|
_chkUltima.TextAlign = ContentAlignment.MiddleLeft;
|
||||||
|
_chkUltima.Checked = true;
|
||||||
|
|
||||||
|
_btnCerca.Text = "Cerca";
|
||||||
|
_btnCerca.Dock = DockStyle.Fill;
|
||||||
|
_btnCerca.Click += (_, _) => EseguiRicerca();
|
||||||
|
|
||||||
|
_btnApriDb.Text = "Apri DB...";
|
||||||
|
_btnApriDb.Dock = DockStyle.Fill;
|
||||||
|
_btnApriDb.Click += (_, _) => ScegliDatabase();
|
||||||
|
|
||||||
|
_btnAggiorna.Text = "Aggiorna indice";
|
||||||
|
_btnAggiorna.Dock = DockStyle.Fill;
|
||||||
|
_btnAggiorna.Click += (_, _) => AggiornaIndice();
|
||||||
|
|
||||||
|
topPanel.Controls.Add(_txtCerca, 0, 0);
|
||||||
|
topPanel.Controls.Add(_cmbTipo, 1, 0);
|
||||||
|
topPanel.Controls.Add(_cmbCartella, 2, 0);
|
||||||
|
topPanel.Controls.Add(_chkUltima, 3, 0);
|
||||||
|
topPanel.Controls.Add(_btnCerca, 4, 0);
|
||||||
|
topPanel.Controls.Add(_btnApriDb, 5, 0);
|
||||||
|
topPanel.Controls.Add(_btnAggiorna, 6, 0);
|
||||||
|
topPanel.SetColumnSpan(_lblDb, 7);
|
||||||
|
|
||||||
|
_lblDb.Dock = DockStyle.Fill;
|
||||||
|
_lblDb.AutoSize = false;
|
||||||
|
_lblDb.ForeColor = SystemColors.GrayText;
|
||||||
|
_lblDb.TextAlign = ContentAlignment.MiddleLeft;
|
||||||
|
topPanel.Controls.Add(_lblDb, 0, 1);
|
||||||
|
|
||||||
|
_grid.Dock = DockStyle.Fill;
|
||||||
|
_grid.ReadOnly = true;
|
||||||
|
_grid.AllowUserToAddRows = false;
|
||||||
|
_grid.AllowUserToDeleteRows = false;
|
||||||
|
_grid.AllowUserToResizeRows = false;
|
||||||
|
_grid.SelectionMode = DataGridViewSelectionMode.FullRowSelect;
|
||||||
|
_grid.MultiSelect = false;
|
||||||
|
_grid.AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.Fill;
|
||||||
|
_grid.CellDoubleClick += (_, e) => ApriCartellaFileSelezionato();
|
||||||
|
|
||||||
|
var statusPanel = new Panel { Dock = DockStyle.Bottom, Height = 28 };
|
||||||
|
_lblStatus.Dock = DockStyle.Fill;
|
||||||
|
_lblStatus.TextAlign = ContentAlignment.MiddleLeft;
|
||||||
|
_lblStatus.Padding = new Padding(8, 0, 0, 0);
|
||||||
|
_lblStatus.Text = "Pronto.";
|
||||||
|
statusPanel.Controls.Add(_lblStatus);
|
||||||
|
|
||||||
|
Controls.Add(_grid);
|
||||||
|
Controls.Add(statusPanel);
|
||||||
|
Controls.Add(topPanel);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void InizializzaDatabase()
|
||||||
|
{
|
||||||
|
var candidati = new[]
|
||||||
|
{
|
||||||
|
Path.Combine(AppContext.BaseDirectory, "cad_index.db"),
|
||||||
|
Path.Combine(Directory.GetCurrentDirectory(), "cad_index.db"),
|
||||||
|
};
|
||||||
|
_dbPath = candidati.FirstOrDefault(File.Exists);
|
||||||
|
|
||||||
|
if (_dbPath is null)
|
||||||
|
{
|
||||||
|
_lblStatus.Text = "Database non trovato. Usa 'Apri DB...' per selezionarlo.";
|
||||||
|
_lblDb.Text = "Nessun database caricato.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
CaricaDatabase(_dbPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ScegliDatabase()
|
||||||
|
{
|
||||||
|
using var dialog = new OpenFileDialog
|
||||||
|
{
|
||||||
|
Filter = "Database SQLite (*.db)|*.db|Tutti i file (*.*)|*.*",
|
||||||
|
Title = "Seleziona cad_index.db",
|
||||||
|
};
|
||||||
|
if (dialog.ShowDialog(this) == DialogResult.OK)
|
||||||
|
{
|
||||||
|
CaricaDatabase(dialog.FileName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CaricaDatabase(string path)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var connection = new SqliteConnection($"Data Source={path};Mode=ReadOnly");
|
||||||
|
connection.Open();
|
||||||
|
|
||||||
|
_cmbTipo.Items.Clear();
|
||||||
|
_cmbTipo.Items.Add(OpzionePrtAsm);
|
||||||
|
_cmbTipo.Items.Add(OpzioneTuttiTipi);
|
||||||
|
using (var cmd = connection.CreateCommand())
|
||||||
|
{
|
||||||
|
cmd.CommandText = "SELECT DISTINCT tipo_file FROM files ORDER BY tipo_file";
|
||||||
|
using var reader = cmd.ExecuteReader();
|
||||||
|
while (reader.Read())
|
||||||
|
{
|
||||||
|
_cmbTipo.Items.Add(reader.GetString(0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_cmbTipo.SelectedIndex = 0;
|
||||||
|
|
||||||
|
long totale = (long)(new SqliteCommand("SELECT COUNT(*) FROM files", connection).ExecuteScalar() ?? 0L);
|
||||||
|
|
||||||
|
_dbPath = path;
|
||||||
|
_lblDb.Text = $"Database: {path} ({totale:N0} file indicizzati)";
|
||||||
|
_lblStatus.Text = "Pronto.";
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
MessageBox.Show(this, $"Impossibile aprire il database:\n{ex.Message}", "Errore",
|
||||||
|
MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async void AggiornaIndice()
|
||||||
|
{
|
||||||
|
string dbPath = _dbPath ?? Path.Combine(AppContext.BaseDirectory, "cad_index.db");
|
||||||
|
|
||||||
|
if (!Directory.Exists(RadiceDiscoDefault))
|
||||||
|
{
|
||||||
|
MessageBox.Show(this, $"Disco non raggiungibile: {RadiceDiscoDefault}", "Errore",
|
||||||
|
MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_btnAggiorna.Enabled = false;
|
||||||
|
_btnCerca.Enabled = false;
|
||||||
|
_lblStatus.Text = "Aggiornamento indice in corso...";
|
||||||
|
|
||||||
|
using var cts = new CancellationTokenSource();
|
||||||
|
using var dialog = new ProgressForm();
|
||||||
|
var progress = new Progress<ScanProgress>(dialog.Aggiorna);
|
||||||
|
dialog.Annullato += (_, _) => cts.Cancel();
|
||||||
|
|
||||||
|
var scanTask = Indexer.ScanAsync(RadiceDiscoDefault, dbPath, progress, cts.Token);
|
||||||
|
|
||||||
|
// Chiude automaticamente il dialog quando lo scan termina (successo, errore o annullamento)
|
||||||
|
_ = scanTask.ContinueWith(_ =>
|
||||||
|
{
|
||||||
|
if (dialog.Visible)
|
||||||
|
{
|
||||||
|
dialog.BeginInvoke(dialog.Close);
|
||||||
|
}
|
||||||
|
}, TaskScheduler.Default);
|
||||||
|
|
||||||
|
dialog.ShowDialog(this);
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await scanTask;
|
||||||
|
_lblStatus.Text = "Indice aggiornato.";
|
||||||
|
CaricaDatabase(dbPath);
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
_lblStatus.Text = "Aggiornamento annullato.";
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
MessageBox.Show(this, $"Errore durante l'aggiornamento dell'indice:\n{ex.Message}", "Errore",
|
||||||
|
MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||||
|
_lblStatus.Text = "Aggiornamento fallito.";
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_btnAggiorna.Enabled = true;
|
||||||
|
_btnCerca.Enabled = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void EseguiRicerca()
|
||||||
|
{
|
||||||
|
if (_dbPath is null)
|
||||||
|
{
|
||||||
|
_lblStatus.Text = "Nessun database caricato. Usa 'Apri DB...'.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
string testo = _txtCerca.Text.Trim();
|
||||||
|
string? tipoSelezionato = (string?)_cmbTipo.SelectedItem;
|
||||||
|
string[]? tipi = tipoSelezionato switch
|
||||||
|
{
|
||||||
|
OpzionePrtAsm => TipiPredefiniti,
|
||||||
|
OpzioneTuttiTipi => null,
|
||||||
|
null => TipiPredefiniti,
|
||||||
|
_ => new[] { tipoSelezionato },
|
||||||
|
};
|
||||||
|
string? cartella = _cmbCartella.SelectedIndex > 0 ? (string)_cmbCartella.SelectedItem! : null;
|
||||||
|
bool soloUltima = _chkUltima.Checked;
|
||||||
|
|
||||||
|
if (testo.Length == 0 && tipi is null && cartella is null)
|
||||||
|
{
|
||||||
|
_lblStatus.Text = "Inserisci un termine di ricerca o imposta un filtro.";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Cursor = Cursors.WaitCursor;
|
||||||
|
_btnCerca.Enabled = false;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var (colonne, righe, troncato) = Cerca(_dbPath, testo, soloUltima, cartella, tipi);
|
||||||
|
PopolaGriglia(colonne, righe);
|
||||||
|
_lblStatus.Text = $"{righe.Count} risultat{(righe.Count == 1 ? "o" : "i")}"
|
||||||
|
+ (troncato ? " (troncato a 500, affina la ricerca)" : "");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
MessageBox.Show(this, $"Errore durante la ricerca:\n{ex.Message}", "Errore",
|
||||||
|
MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
Cursor = Cursors.Default;
|
||||||
|
_btnCerca.Enabled = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static (string[] colonne, List<string[]> righe, bool troncato) Cerca(
|
||||||
|
string dbPath, string testo, bool soloUltimaVersione, string? cartella, string[]? tipi)
|
||||||
|
{
|
||||||
|
using var connection = new SqliteConnection($"Data Source={dbPath};Mode=ReadOnly");
|
||||||
|
connection.Open();
|
||||||
|
|
||||||
|
string tabella = soloUltimaVersione ? "files_ultima_versione" : "files";
|
||||||
|
string filtroTipo = "1=1";
|
||||||
|
if (tipi is { Length: > 0 })
|
||||||
|
{
|
||||||
|
var nomiParametri = tipi.Select((_, i) => $"@tipo{i}");
|
||||||
|
filtroTipo = $"tipo_file IN ({string.Join(",", nomiParametri)})";
|
||||||
|
}
|
||||||
|
|
||||||
|
var sql = $"""
|
||||||
|
SELECT percorso_completo, cartella_radice, famiglia, codice, tipo_file, versione,
|
||||||
|
dimensione_byte, data_modifica
|
||||||
|
FROM {tabella}
|
||||||
|
WHERE (@testo = '' OR codice LIKE @like OR nome_file LIKE @like)
|
||||||
|
AND (@cartella IS NULL OR cartella_radice = @cartella)
|
||||||
|
AND ({filtroTipo})
|
||||||
|
ORDER BY codice, tipo_file, versione
|
||||||
|
LIMIT 500
|
||||||
|
""";
|
||||||
|
|
||||||
|
using var command = connection.CreateCommand();
|
||||||
|
command.CommandText = sql;
|
||||||
|
command.Parameters.AddWithValue("@testo", testo);
|
||||||
|
command.Parameters.AddWithValue("@like", $"%{testo}%");
|
||||||
|
command.Parameters.AddWithValue("@cartella", (object?)cartella ?? DBNull.Value);
|
||||||
|
if (tipi is { Length: > 0 })
|
||||||
|
{
|
||||||
|
for (int i = 0; i < tipi.Length; i++)
|
||||||
|
{
|
||||||
|
command.Parameters.AddWithValue($"@tipo{i}", tipi[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
using var reader = command.ExecuteReader();
|
||||||
|
var righe = new List<string[]>();
|
||||||
|
while (reader.Read())
|
||||||
|
{
|
||||||
|
righe.Add(new[]
|
||||||
|
{
|
||||||
|
reader.GetString(0),
|
||||||
|
reader.IsDBNull(2) ? "" : reader.GetString(2),
|
||||||
|
reader.IsDBNull(3) ? "" : reader.GetString(3),
|
||||||
|
reader.IsDBNull(4) ? "" : reader.GetString(4),
|
||||||
|
reader.IsDBNull(5) ? "" : reader.GetInt64(5).ToString(),
|
||||||
|
FormatSize(reader.IsDBNull(6) ? 0 : reader.GetInt64(6)),
|
||||||
|
reader.IsDBNull(7) ? "" : reader.GetString(7),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
var colonne = new[] { "percorso", "famiglia", "codice", "tipo", "ver", "dimensione", "data modifica" };
|
||||||
|
return (colonne, righe, righe.Count == 500);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string FormatSize(long bytes)
|
||||||
|
{
|
||||||
|
string[] unita = { "B", "KB", "MB", "GB" };
|
||||||
|
double value = bytes;
|
||||||
|
int i = 0;
|
||||||
|
while (value >= 1024 && i < unita.Length - 1)
|
||||||
|
{
|
||||||
|
value /= 1024;
|
||||||
|
i++;
|
||||||
|
}
|
||||||
|
return $"{value:0.#} {unita[i]}";
|
||||||
|
}
|
||||||
|
|
||||||
|
private void PopolaGriglia(string[] colonne, List<string[]> righe)
|
||||||
|
{
|
||||||
|
_grid.Rows.Clear();
|
||||||
|
_grid.Columns.Clear();
|
||||||
|
foreach (var nome in colonne)
|
||||||
|
{
|
||||||
|
_grid.Columns.Add(nome, nome);
|
||||||
|
}
|
||||||
|
foreach (var riga in righe)
|
||||||
|
{
|
||||||
|
_grid.Rows.Add(riga);
|
||||||
|
}
|
||||||
|
if (_grid.Columns.Count > 0)
|
||||||
|
{
|
||||||
|
_grid.Columns[0].FillWeight = 220;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ApriCartellaFileSelezionato()
|
||||||
|
{
|
||||||
|
if (_grid.CurrentRow is null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
string? percorso = _grid.CurrentRow.Cells[0].Value as string;
|
||||||
|
if (string.IsNullOrEmpty(percorso))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Process.Start("explorer.exe", $"/select,\"{percorso}\"");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
MessageBox.Show(this, $"Impossibile aprire Esplora risorse:\n{ex.Message}", "Errore",
|
||||||
|
MessageBoxButtons.OK, MessageBoxIcon.Error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,277 @@
|
|||||||
|
using System.Diagnostics;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using Microsoft.Data.Sqlite;
|
||||||
|
|
||||||
|
namespace CadSearchGui;
|
||||||
|
|
||||||
|
public sealed record ScanProgress(
|
||||||
|
string Cartella,
|
||||||
|
long FattiCartella,
|
||||||
|
long PrevistiCartella,
|
||||||
|
long FattiTotali,
|
||||||
|
long PrevistiTotali,
|
||||||
|
int Nuovi,
|
||||||
|
int Aggiornati,
|
||||||
|
int Invariati,
|
||||||
|
int Rimossi,
|
||||||
|
TimeSpan Trascorso);
|
||||||
|
|
||||||
|
public static class Indexer
|
||||||
|
{
|
||||||
|
public static readonly string[] CartelleRadice = { "__Accessori", "__B.end", "__Corpi", "__Fondi" };
|
||||||
|
|
||||||
|
private static readonly Regex FilenameRegex =
|
||||||
|
new(@"^(?<codice>.+)\.(?<tipo>[A-Za-z0-9]+)\.(?<versione>\d+)$", RegexOptions.Compiled);
|
||||||
|
|
||||||
|
private const string Schema = """
|
||||||
|
CREATE TABLE IF NOT EXISTS files (
|
||||||
|
id INTEGER PRIMARY KEY,
|
||||||
|
percorso_completo TEXT UNIQUE NOT NULL,
|
||||||
|
percorso_relativo TEXT,
|
||||||
|
nome_file TEXT,
|
||||||
|
cartella_radice TEXT,
|
||||||
|
famiglia TEXT,
|
||||||
|
codice TEXT,
|
||||||
|
tipo_file TEXT,
|
||||||
|
versione INTEGER,
|
||||||
|
dimensione_byte INTEGER,
|
||||||
|
data_modifica TEXT,
|
||||||
|
scansionato_il TEXT
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_files_codice ON files(codice);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_files_tipo ON files(tipo_file);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_files_cartella ON files(cartella_radice);
|
||||||
|
|
||||||
|
CREATE VIEW IF NOT EXISTS files_ultima_versione AS
|
||||||
|
SELECT percorso_completo, percorso_relativo, nome_file, cartella_radice, famiglia,
|
||||||
|
codice, tipo_file, versione, dimensione_byte, data_modifica, scansionato_il
|
||||||
|
FROM (
|
||||||
|
SELECT f.*, ROW_NUMBER() OVER (
|
||||||
|
PARTITION BY cartella_radice, codice, tipo_file
|
||||||
|
ORDER BY versione DESC
|
||||||
|
) AS rn
|
||||||
|
FROM files f
|
||||||
|
) WHERE rn = 1;
|
||||||
|
""";
|
||||||
|
|
||||||
|
public static Task ScanAsync(string root, string dbPath, IProgress<ScanProgress> progress, CancellationToken ct)
|
||||||
|
=> Task.Run(() => Scan(root, dbPath, progress, ct), ct);
|
||||||
|
|
||||||
|
private static void Scan(string root, string dbPath, IProgress<ScanProgress> progress, CancellationToken ct)
|
||||||
|
{
|
||||||
|
using var connection = new SqliteConnection($"Data Source={dbPath}");
|
||||||
|
connection.Open();
|
||||||
|
using (var schemaCmd = connection.CreateCommand())
|
||||||
|
{
|
||||||
|
schemaCmd.CommandText = Schema;
|
||||||
|
schemaCmd.ExecuteNonQuery();
|
||||||
|
}
|
||||||
|
|
||||||
|
var previsti = new Dictionary<string, long>();
|
||||||
|
foreach (var cartella in CartelleRadice)
|
||||||
|
{
|
||||||
|
using var cmd = connection.CreateCommand();
|
||||||
|
cmd.CommandText = "SELECT COUNT(*) FROM files WHERE cartella_radice = @c";
|
||||||
|
cmd.Parameters.AddWithValue("@c", cartella);
|
||||||
|
previsti[cartella] = (long)(cmd.ExecuteScalar() ?? 0L);
|
||||||
|
}
|
||||||
|
long previstiTotali = previsti.Values.Sum();
|
||||||
|
|
||||||
|
var sw = Stopwatch.StartNew();
|
||||||
|
long fattiTotali = 0;
|
||||||
|
|
||||||
|
foreach (var cartella in CartelleRadice)
|
||||||
|
{
|
||||||
|
ct.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
string rootPath = Path.Combine(root, cartella);
|
||||||
|
if (!Directory.Exists(rootPath))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
string now = DateTime.UtcNow.ToString("o");
|
||||||
|
int nuovi = 0, aggiornati = 0, invariati = 0;
|
||||||
|
long fattiCartella = 0;
|
||||||
|
|
||||||
|
using var transaction = connection.BeginTransaction();
|
||||||
|
|
||||||
|
using var cmdSelect = connection.CreateCommand();
|
||||||
|
cmdSelect.Transaction = transaction;
|
||||||
|
cmdSelect.CommandText = "SELECT dimensione_byte, data_modifica FROM files WHERE percorso_completo = @p";
|
||||||
|
var pSelect = cmdSelect.Parameters.Add("@p", Microsoft.Data.Sqlite.SqliteType.Text);
|
||||||
|
|
||||||
|
using var cmdInsert = connection.CreateCommand();
|
||||||
|
cmdInsert.Transaction = transaction;
|
||||||
|
cmdInsert.CommandText = """
|
||||||
|
INSERT INTO files
|
||||||
|
(percorso_completo, percorso_relativo, nome_file, cartella_radice, famiglia,
|
||||||
|
codice, tipo_file, versione, dimensione_byte, data_modifica, scansionato_il)
|
||||||
|
VALUES (@percorso, @relativo, @nome, @cartella, @famiglia, @codice, @tipo, @versione,
|
||||||
|
@dimensione, @modifica, @scansionato)
|
||||||
|
""";
|
||||||
|
AggiungiParametriRecord(cmdInsert);
|
||||||
|
cmdInsert.Parameters.AddWithValue("@scansionato", now);
|
||||||
|
|
||||||
|
using var cmdUpdate = connection.CreateCommand();
|
||||||
|
cmdUpdate.Transaction = transaction;
|
||||||
|
cmdUpdate.CommandText = """
|
||||||
|
UPDATE files SET percorso_relativo=@relativo, nome_file=@nome, cartella_radice=@cartella,
|
||||||
|
famiglia=@famiglia, codice=@codice, tipo_file=@tipo, versione=@versione,
|
||||||
|
dimensione_byte=@dimensione, data_modifica=@modifica, scansionato_il=@scansionato
|
||||||
|
WHERE percorso_completo=@percorso
|
||||||
|
""";
|
||||||
|
AggiungiParametriRecord(cmdUpdate);
|
||||||
|
cmdUpdate.Parameters.AddWithValue("@scansionato", now);
|
||||||
|
|
||||||
|
using var cmdTouch = connection.CreateCommand();
|
||||||
|
cmdTouch.Transaction = transaction;
|
||||||
|
cmdTouch.CommandText = "UPDATE files SET scansionato_il=@scansionato WHERE percorso_completo=@percorso";
|
||||||
|
cmdTouch.Parameters.AddWithValue("@scansionato", now);
|
||||||
|
var pTouchPercorso = cmdTouch.Parameters.Add("@percorso", Microsoft.Data.Sqlite.SqliteType.Text);
|
||||||
|
|
||||||
|
foreach (var file in Walk(rootPath))
|
||||||
|
{
|
||||||
|
ct.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
string percorsoCompleto = file.FullName;
|
||||||
|
string percorsoRelativo = Path.GetRelativePath(rootPath, percorsoCompleto);
|
||||||
|
string nomeFile = file.Name;
|
||||||
|
string[] parti = percorsoRelativo.Split(Path.DirectorySeparatorChar);
|
||||||
|
string? famiglia = parti.Length > 1 ? parti[0] : null;
|
||||||
|
var (codice, tipo, versione) = ParseFilename(nomeFile);
|
||||||
|
long dimensione = file.Length;
|
||||||
|
string modifica = file.LastWriteTimeUtc.ToString("o");
|
||||||
|
|
||||||
|
pSelect.Value = percorsoCompleto;
|
||||||
|
object? esistenteDim = null;
|
||||||
|
string? esistenteMod = null;
|
||||||
|
bool esiste = false;
|
||||||
|
using (var reader = cmdSelect.ExecuteReader())
|
||||||
|
{
|
||||||
|
if (reader.Read())
|
||||||
|
{
|
||||||
|
esiste = true;
|
||||||
|
esistenteDim = reader.IsDBNull(0) ? null : reader.GetInt64(0);
|
||||||
|
esistenteMod = reader.IsDBNull(1) ? null : reader.GetString(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!esiste)
|
||||||
|
{
|
||||||
|
ImpostaParametriRecord(cmdInsert, percorsoCompleto, percorsoRelativo, nomeFile, cartella,
|
||||||
|
famiglia, codice, tipo, versione, dimensione, modifica);
|
||||||
|
cmdInsert.ExecuteNonQuery();
|
||||||
|
nuovi++;
|
||||||
|
}
|
||||||
|
else if (!(esistenteDim is long d && d == dimensione && esistenteMod == modifica))
|
||||||
|
{
|
||||||
|
ImpostaParametriRecord(cmdUpdate, percorsoCompleto, percorsoRelativo, nomeFile, cartella,
|
||||||
|
famiglia, codice, tipo, versione, dimensione, modifica);
|
||||||
|
cmdUpdate.ExecuteNonQuery();
|
||||||
|
aggiornati++;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
pTouchPercorso.Value = percorsoCompleto;
|
||||||
|
cmdTouch.ExecuteNonQuery();
|
||||||
|
invariati++;
|
||||||
|
}
|
||||||
|
|
||||||
|
fattiCartella++;
|
||||||
|
fattiTotali++;
|
||||||
|
|
||||||
|
if (fattiTotali % 200 == 0)
|
||||||
|
{
|
||||||
|
progress.Report(new ScanProgress(cartella, fattiCartella, previsti[cartella],
|
||||||
|
fattiTotali, previstiTotali, nuovi, aggiornati, invariati, 0, sw.Elapsed));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
using (var cmdDelete = connection.CreateCommand())
|
||||||
|
{
|
||||||
|
cmdDelete.Transaction = transaction;
|
||||||
|
cmdDelete.CommandText = "DELETE FROM files WHERE cartella_radice = @c AND scansionato_il != @now";
|
||||||
|
cmdDelete.Parameters.AddWithValue("@c", cartella);
|
||||||
|
cmdDelete.Parameters.AddWithValue("@now", now);
|
||||||
|
int rimossi = cmdDelete.ExecuteNonQuery();
|
||||||
|
|
||||||
|
transaction.Commit();
|
||||||
|
|
||||||
|
progress.Report(new ScanProgress(cartella, fattiCartella, previsti[cartella],
|
||||||
|
fattiTotali, previstiTotali, nuovi, aggiornati, invariati, rimossi, sw.Elapsed));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void AggiungiParametriRecord(SqliteCommand cmd)
|
||||||
|
{
|
||||||
|
cmd.Parameters.Add("@percorso", Microsoft.Data.Sqlite.SqliteType.Text);
|
||||||
|
cmd.Parameters.Add("@relativo", Microsoft.Data.Sqlite.SqliteType.Text);
|
||||||
|
cmd.Parameters.Add("@nome", Microsoft.Data.Sqlite.SqliteType.Text);
|
||||||
|
cmd.Parameters.Add("@cartella", Microsoft.Data.Sqlite.SqliteType.Text);
|
||||||
|
cmd.Parameters.Add("@famiglia", Microsoft.Data.Sqlite.SqliteType.Text);
|
||||||
|
cmd.Parameters.Add("@codice", Microsoft.Data.Sqlite.SqliteType.Text);
|
||||||
|
cmd.Parameters.Add("@tipo", Microsoft.Data.Sqlite.SqliteType.Text);
|
||||||
|
cmd.Parameters.Add("@versione", Microsoft.Data.Sqlite.SqliteType.Integer);
|
||||||
|
cmd.Parameters.Add("@dimensione", Microsoft.Data.Sqlite.SqliteType.Integer);
|
||||||
|
cmd.Parameters.Add("@modifica", Microsoft.Data.Sqlite.SqliteType.Text);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ImpostaParametriRecord(SqliteCommand cmd, string percorso, string relativo, string nome,
|
||||||
|
string cartella, string? famiglia, string codice, string tipo, int? versione, long dimensione, string modifica)
|
||||||
|
{
|
||||||
|
cmd.Parameters["@percorso"].Value = percorso;
|
||||||
|
cmd.Parameters["@relativo"].Value = relativo;
|
||||||
|
cmd.Parameters["@nome"].Value = nome;
|
||||||
|
cmd.Parameters["@cartella"].Value = cartella;
|
||||||
|
cmd.Parameters["@famiglia"].Value = (object?)famiglia ?? DBNull.Value;
|
||||||
|
cmd.Parameters["@codice"].Value = codice;
|
||||||
|
cmd.Parameters["@tipo"].Value = tipo;
|
||||||
|
cmd.Parameters["@versione"].Value = (object?)versione ?? DBNull.Value;
|
||||||
|
cmd.Parameters["@dimensione"].Value = dimensione;
|
||||||
|
cmd.Parameters["@modifica"].Value = modifica;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static (string codice, string tipo, int? versione) ParseFilename(string nomeFile)
|
||||||
|
{
|
||||||
|
var match = FilenameRegex.Match(nomeFile);
|
||||||
|
if (match.Success)
|
||||||
|
{
|
||||||
|
return (match.Groups["codice"].Value, match.Groups["tipo"].Value,
|
||||||
|
int.Parse(match.Groups["versione"].Value));
|
||||||
|
}
|
||||||
|
return (nomeFile, "altro", null);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IEnumerable<FileInfo> Walk(string root)
|
||||||
|
{
|
||||||
|
var stack = new Stack<string>();
|
||||||
|
stack.Push(root);
|
||||||
|
while (stack.Count > 0)
|
||||||
|
{
|
||||||
|
string current = stack.Pop();
|
||||||
|
IEnumerable<FileSystemInfo> entries;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
entries = new DirectoryInfo(current).EnumerateFileSystemInfos();
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var entry in entries)
|
||||||
|
{
|
||||||
|
if (entry is DirectoryInfo dir)
|
||||||
|
{
|
||||||
|
stack.Push(dir.FullName);
|
||||||
|
}
|
||||||
|
else if (entry is FileInfo file)
|
||||||
|
{
|
||||||
|
yield return file;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
namespace CadSearchGui;
|
||||||
|
|
||||||
|
static class Program
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// The main entry point for the application.
|
||||||
|
/// </summary>
|
||||||
|
[STAThread]
|
||||||
|
static void Main()
|
||||||
|
{
|
||||||
|
// To customize application configuration such as set high DPI settings or default font,
|
||||||
|
// see https://aka.ms/applicationconfiguration.
|
||||||
|
ApplicationConfiguration.Initialize();
|
||||||
|
Application.Run(new Form1());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,110 @@
|
|||||||
|
namespace CadSearchGui;
|
||||||
|
|
||||||
|
public sealed class ProgressForm : Form
|
||||||
|
{
|
||||||
|
private readonly Label _lblCartella = new();
|
||||||
|
private readonly ProgressBar _progressBar = new();
|
||||||
|
private readonly Label _lblConteggio = new();
|
||||||
|
private readonly Label _lblDettaglio = new();
|
||||||
|
private readonly Label _lblEta = new();
|
||||||
|
private readonly Button _btnAnnulla = new();
|
||||||
|
|
||||||
|
public event EventHandler? Annullato;
|
||||||
|
|
||||||
|
public ProgressForm()
|
||||||
|
{
|
||||||
|
Text = "Aggiornamento indice in corso...";
|
||||||
|
FormBorderStyle = FormBorderStyle.FixedDialog;
|
||||||
|
StartPosition = FormStartPosition.CenterParent;
|
||||||
|
MinimizeBox = false;
|
||||||
|
MaximizeBox = false;
|
||||||
|
ControlBox = false;
|
||||||
|
ClientSize = new Size(460, 190);
|
||||||
|
|
||||||
|
_lblCartella.Text = "Preparazione...";
|
||||||
|
_lblCartella.Location = new Point(16, 16);
|
||||||
|
_lblCartella.AutoSize = true;
|
||||||
|
_lblCartella.Font = new Font(Font, FontStyle.Bold);
|
||||||
|
|
||||||
|
_progressBar.Location = new Point(16, 44);
|
||||||
|
_progressBar.Size = new Size(428, 24);
|
||||||
|
_progressBar.Minimum = 0;
|
||||||
|
_progressBar.Maximum = 1000;
|
||||||
|
_progressBar.Style = ProgressBarStyle.Marquee;
|
||||||
|
|
||||||
|
_lblConteggio.Location = new Point(16, 80);
|
||||||
|
_lblConteggio.AutoSize = true;
|
||||||
|
|
||||||
|
_lblDettaglio.Location = new Point(16, 104);
|
||||||
|
_lblDettaglio.AutoSize = true;
|
||||||
|
_lblDettaglio.ForeColor = SystemColors.GrayText;
|
||||||
|
|
||||||
|
_lblEta.Location = new Point(16, 128);
|
||||||
|
_lblEta.AutoSize = true;
|
||||||
|
_lblEta.Font = new Font(Font, FontStyle.Bold);
|
||||||
|
|
||||||
|
_btnAnnulla.Text = "Annulla";
|
||||||
|
_btnAnnulla.Location = new Point(354, 152);
|
||||||
|
_btnAnnulla.Size = new Size(90, 28);
|
||||||
|
_btnAnnulla.Click += (_, e) => Annullato?.Invoke(this, e);
|
||||||
|
|
||||||
|
Controls.AddRange(new Control[]
|
||||||
|
{
|
||||||
|
_lblCartella, _progressBar, _lblConteggio, _lblDettaglio, _lblEta, _btnAnnulla
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Aggiorna(ScanProgress p)
|
||||||
|
{
|
||||||
|
_lblCartella.Text = $"Scansione {p.Cartella}...";
|
||||||
|
|
||||||
|
if (p.PrevistiTotali > 0)
|
||||||
|
{
|
||||||
|
_progressBar.Style = ProgressBarStyle.Continuous;
|
||||||
|
double frazione = Math.Min(1.0, (double)p.FattiTotali / p.PrevistiTotali);
|
||||||
|
_progressBar.Value = (int)(frazione * _progressBar.Maximum);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_progressBar.Style = ProgressBarStyle.Marquee;
|
||||||
|
}
|
||||||
|
|
||||||
|
_lblConteggio.Text = p.PrevistiTotali > 0
|
||||||
|
? $"{p.FattiTotali:N0} / ~{p.PrevistiTotali:N0} file"
|
||||||
|
: $"{p.FattiTotali:N0} file processati";
|
||||||
|
|
||||||
|
_lblDettaglio.Text = $"nuovi: {p.Nuovi:N0} aggiornati: {p.Aggiornati:N0} invariati: {p.Invariati:N0}"
|
||||||
|
+ (p.Rimossi > 0 ? $" rimossi: {p.Rimossi:N0}" : "");
|
||||||
|
|
||||||
|
_lblEta.Text = $"Trascorso: {FormatTempo(p.Trascorso)} — {StimaRimanente(p)}";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string StimaRimanente(ScanProgress p)
|
||||||
|
{
|
||||||
|
if (p.PrevistiTotali <= 0 || p.FattiTotali <= 0)
|
||||||
|
{
|
||||||
|
return "stima non disponibile";
|
||||||
|
}
|
||||||
|
double filePerSecondo = p.FattiTotali / Math.Max(p.Trascorso.TotalSeconds, 0.001);
|
||||||
|
if (filePerSecondo <= 0)
|
||||||
|
{
|
||||||
|
return "stima non disponibile";
|
||||||
|
}
|
||||||
|
long rimanenti = Math.Max(p.PrevistiTotali - p.FattiTotali, 0);
|
||||||
|
var eta = TimeSpan.FromSeconds(rimanenti / filePerSecondo);
|
||||||
|
return $"tempo rimanente stimato: {FormatTempo(eta)}";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string FormatTempo(TimeSpan t)
|
||||||
|
{
|
||||||
|
if (t.TotalHours >= 1)
|
||||||
|
{
|
||||||
|
return $"{(int)t.TotalHours}h {t.Minutes}m {t.Seconds}s";
|
||||||
|
}
|
||||||
|
if (t.TotalMinutes >= 1)
|
||||||
|
{
|
||||||
|
return $"{t.Minutes}m {t.Seconds}s";
|
||||||
|
}
|
||||||
|
return $"{t.Seconds}s";
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user