Files
prep-step/PrepStep/Form1.cs
T
davideandClaude Sonnet 5 13798ad47c Add explicit working folder; rename project from CadSearchGui to PrepStep
Working folder (phase 1): CSV/raw/step/step_finali all used to live split
across two different implicit locations (CSV in the "repo root" found by
walking up for CLAUDE.md, raw/step next to the input distinta file) with
neither shown to the user. That repo-root lookup also silently breaks once
the app runs as a standalone published exe on a machine with no repo/
CLAUDE.md at all. Replaced both with one explicit "cartella di lavoro",
auto-suggested (last used, remembered the same way as the DB path) and
changeable from phase 1, with no dependency on any repo marker file.

Rename: CadSearchGui was the working name from before the app had a
product identity; renamed folders, csproj files, namespace and the
AppData settings folder to PrepStep throughout, matching the exe name
already in use. Product-facing name stays "Cevolani StepPrep".

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-21 11:18:43 +02:00

1290 lines
52 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System.Diagnostics;
using Microsoft.Data.Sqlite;
namespace PrepStep;
public partial class Form1 : Form
{
private readonly Button _btnApriDb = new();
private readonly Button _btnAggiorna = new();
private readonly Button _btnCreaRaw = new();
private readonly Button _btnTrovaPrt = new();
private readonly Button _btnReport = new();
private readonly Button _btnVerifica = new();
private readonly Button _btnCompletezza = new();
private readonly ListView _lstAnteprimaAsm = new();
private readonly Button _btnPulisci = new();
private readonly Label _lblStatus = new();
private readonly Label _lblDb = new();
private readonly Label _lblUltimoAggiornamento = new();
private readonly Label _lblCartellaLavoro = new();
private readonly Button _btnCambiaCartella = new();
private readonly DataGridView _gridImport = new();
private readonly DataGridView _gridRaw = new();
private readonly Label _lblImportStatus = new();
private readonly ToolTip _tipImportStatus = new();
// Wizard: una fase alla volta occupa tutta la finestra, si avanza solo quando quella
// corrente è "pronta" (vedi AvantiConsentito). NomiFasi/_panelFasi sono paralleli per indice.
private static readonly string[] NomiFasi =
{
"1. Database",
"2. Importa Distinta Base",
"3. Crea cartella raw",
"4. Trova componenti referenziati",
"5. Verifica completezza e STEP",
};
private readonly Panel[] _panelFasi = new Panel[NomiFasi.Length];
private readonly Panel _faseContainer = new() { Dock = DockStyle.Fill };
private readonly Label _lblFase = new();
private readonly Button _btnIndietro = new();
private readonly Button _btnAvanti = new();
private int _faseAttuale;
private static readonly string[] TipiPredefiniti = { "prt", "asm" };
private const string RadiceDiscoDefault = @"X:\";
private string? _dbPath;
private string? _cartellaLavoro;
private string? _percorsoInputCorrente;
private List<(string Componente, string Descrizione)>? _righeCorrenti;
private Dictionary<string, List<string>>? _ultimaVerifica;
private string? _ultimaCartellaRaw;
public Form1()
{
InitializeComponent();
BuildUi();
Load += (_, _) =>
{
InizializzaCartellaLavoro();
InizializzaDatabase();
};
}
// Palette: blu primario per l'azione principale del flusso ("Crea cartella raw"),
// il resto resta ai colori di sistema per restare coerente col tema Windows dell'utente.
private static readonly Color ColorePrimario = Color.FromArgb(0, 99, 177);
private void BuildUi()
{
AllowDrop = true;
// Versione letta dall'assembly (<Version> nel csproj) invece che duplicata qui a mano.
var versione = typeof(Form1).Assembly.GetName().Version;
Text = $"Cevolani StepPrep {versione?.ToString(3)}";
MinimumSize = new Size(760, 480);
Font = new Font("Segoe UI", 9.5f);
// Stessa icona di ApplicationIcon nel csproj: la si rilegge dall'exe compilato invece
// di duplicare il file .ico come Content, funziona identica in Debug/Release/publish.
try
{
Icon = Icon.ExtractAssociatedIcon(Application.ExecutablePath);
}
catch (Exception ex) when (ex is IOException or ArgumentException)
{
// ponytail: nessuna icona nell'exe (es. lanciato da dotnet run) -> resta quella di default
}
var headerPanel = new Panel { Dock = DockStyle.Top, Height = 40, BackColor = SystemColors.ControlLight };
_lblFase.Dock = DockStyle.Fill;
_lblFase.TextAlign = ContentAlignment.MiddleLeft;
_lblFase.Padding = new Padding(12, 0, 0, 0);
_lblFase.Font = new Font(Font, FontStyle.Bold);
headerPanel.Controls.Add(_lblFase);
var navPanel = new FlowLayoutPanel
{
Dock = DockStyle.Bottom,
// I bottoni si auto-dimensionano al testo (UiStile): l'altezza qui deve stare
// comoda sopra al loro MinimumSize (32) più il padding, altrimenti vengono
// tagliati sul fondo della barra invece di restarci dentro.
Height = 56,
FlowDirection = FlowDirection.RightToLeft,
Padding = new Padding(12, 10, 12, 10),
};
_btnAvanti.Text = "Avanti ›";
UiStile.Primario(_btnAvanti);
UiStile.Arrotonda(_btnAvanti);
_btnAvanti.Click += (_, _) => VaiAllaFase(_faseAttuale + 1);
navPanel.Controls.Add(_btnAvanti);
_btnIndietro.Text = "‹ Indietro";
_btnIndietro.Margin = new Padding(8, 0, 0, 0);
UiStile.Secondario(_btnIndietro);
UiStile.Arrotonda(_btnIndietro);
_btnIndietro.Click += (_, _) => VaiAllaFase(_faseAttuale - 1);
navPanel.Controls.Add(_btnIndietro);
var statusPanel = new Panel { Dock = DockStyle.Bottom, Height = 28, BackColor = SystemColors.ControlLight };
_lblStatus.Dock = DockStyle.Fill;
_lblStatus.TextAlign = ContentAlignment.MiddleLeft;
_lblStatus.Padding = new Padding(12, 0, 0, 0);
_lblStatus.Text = "Pronto.";
statusPanel.Controls.Add(_lblStatus);
_panelFasi[0] = BuildFaseDatabase();
_panelFasi[1] = BuildFaseImporta();
_panelFasi[2] = BuildFaseCreaRaw();
_panelFasi[3] = BuildFaseTrovaComponenti();
_panelFasi[4] = BuildFaseVerifica();
foreach (var panel in _panelFasi)
{
panel.Dock = DockStyle.Fill;
panel.Visible = false;
_faseContainer.Controls.Add(panel);
}
Controls.Add(_faseContainer);
Controls.Add(navPanel);
Controls.Add(statusPanel);
Controls.Add(headerPanel);
DragEnter += (_, e) =>
e.Effect = e.Data?.GetDataPresent(DataFormats.FileDrop) == true
? DragDropEffects.Copy
: DragDropEffects.None;
DragDrop += (_, e) =>
{
if (e.Data?.GetData(DataFormats.FileDrop) is string[] { Length: > 0 } file)
ImportaDistinta(file[0]);
};
VaiAllaFase(0);
}
/// <summary>Nasconde la fase corrente e mostra quella richiesta (se nei limiti e se la fase
/// corrente permette di avanzare); aggiorna titolo e stato dei pulsanti Indietro/Avanti.</summary>
private void VaiAllaFase(int fase)
{
if (fase < 0 || fase >= NomiFasi.Length)
return;
if (fase > _faseAttuale && !AvantiConsentito(_faseAttuale))
return;
_panelFasi[_faseAttuale].Visible = false;
_faseAttuale = fase;
_panelFasi[_faseAttuale].Visible = true;
_lblFase.Text = NomiFasi[_faseAttuale];
_btnIndietro.Visible = _faseAttuale > 0;
_btnAvanti.Visible = _faseAttuale < NomiFasi.Length - 1;
AggiornaStatoAvanti();
if (_faseAttuale == 4)
AggiornaAnteprimaAsm();
}
/// <summary>Popola le icone trascinabili (drag&amp;drop) dei file .asm della distinta importata
/// (non i sotto-assiemi di fase 4, che in "raw" servono solo a completarla) già copiati in
/// "raw" (non riletti dal DB/share: sono già in locale), mostrate in fase 5 prima ancora di
/// avviare la verifica.</summary>
private void AggiornaAnteprimaAsm()
{
_lstAnteprimaAsm.Items.Clear();
_lstAnteprimaAsm.LargeImageList!.Images.Clear();
if (_ultimaCartellaRaw is null || !Directory.Exists(_ultimaCartellaRaw) || _righeCorrenti is null)
return;
var codiciDistinta = _righeCorrenti.Select(r => r.Componente).ToHashSet(StringComparer.OrdinalIgnoreCase);
var percorsiAsm = Directory.GetFiles(_ultimaCartellaRaw)
.Where(p =>
{
var (codice, tipo, _) = Indexer.ParseFilename(Path.GetFileName(p));
return tipo.Equals("asm", StringComparison.OrdinalIgnoreCase) && codiciDistinta.Contains(codice);
});
foreach (var percorso in percorsiAsm)
{
Icon? icona = null;
try
{
icona = Icon.ExtractAssociatedIcon(percorso);
}
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException or ArgumentException)
{
// ponytail: file irraggiungibile (share offline) -> icona generica, non blocca gli altri
}
int indice = _lstAnteprimaAsm.LargeImageList.Images.Count;
_lstAnteprimaAsm.LargeImageList.Images.Add(icona?.ToBitmap() ?? SystemIcons.WinLogo.ToBitmap());
icona?.Dispose();
_lstAnteprimaAsm.Items.Add(new ListViewItem(Path.GetFileName(percorso), indice) { Tag = percorso });
}
}
/// <summary>Condizione per cui la fase indicata è "conclusa" e si può passare alla successiva.</summary>
private bool AvantiConsentito(int fase) => fase switch
{
0 => _dbPath is not null && _cartellaLavoro is not null,
1 => _righeCorrenti is { Count: > 0 },
2 => _ultimaCartellaRaw is not null,
3 => true, // risoluzione ricorsiva opzionale: non tutti gli assiemi hanno sotto-componenti
_ => true,
};
private void AggiornaStatoAvanti() => _btnAvanti.Enabled = AvantiConsentito(_faseAttuale);
private Panel BuildFaseDatabase()
{
var contenuto = new TableLayoutPanel
{
Dock = DockStyle.Top,
AutoSize = true,
AutoSizeMode = AutoSizeMode.GrowAndShrink,
ColumnCount = 1,
Padding = new Padding(16),
};
// Colonna al 100%: le label sotto (Dock=Top) vanno a capo sulla larghezza reale della
// finestra invece che su un limite fisso in pixel, e si riadattano se viene ridimensionata.
contenuto.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100));
var lblIstruzioni = new Label
{
Dock = DockStyle.Top,
AutoSize = true,
ForeColor = SystemColors.GrayText,
Margin = new Padding(0, 0, 0, 12),
Text = "L'indice CAD viene caricato automaticamente all'avvio, se già presente. "
+ "Usa 'Apri DB...' per selezionarne un altro, oppure 'Aggiorna indice' per "
+ "scansionare di nuovo il disco (X:\\).",
};
contenuto.Controls.Add(lblIstruzioni);
_lblDb.Dock = DockStyle.Top;
_lblDb.AutoSize = true;
_lblDb.ForeColor = SystemColors.GrayText;
_lblDb.Margin = new Padding(0, 0, 0, 4);
contenuto.Controls.Add(_lblDb);
_lblUltimoAggiornamento.AutoSize = true;
_lblUltimoAggiornamento.ForeColor = SystemColors.GrayText;
_lblUltimoAggiornamento.Margin = new Padding(0, 0, 0, 12);
contenuto.Controls.Add(_lblUltimoAggiornamento);
var bottoni = new FlowLayoutPanel { AutoSize = true, FlowDirection = FlowDirection.LeftToRight };
_btnApriDb.Text = "Apri DB...";
_btnApriDb.Margin = new Padding(0, 0, 8, 0);
UiStile.Secondario(_btnApriDb);
_btnApriDb.Click += (_, _) => ScegliDatabase();
bottoni.Controls.Add(_btnApriDb);
_btnAggiorna.Text = "Aggiorna indice";
UiStile.Secondario(_btnAggiorna);
_btnAggiorna.Click += (_, _) => AggiornaIndice();
bottoni.Controls.Add(_btnAggiorna);
contenuto.Controls.Add(bottoni);
var lblIstruzioniCartella = new Label
{
Dock = DockStyle.Top,
AutoSize = true,
ForeColor = SystemColors.GrayText,
Margin = new Padding(0, 20, 0, 4),
Text = "Cartella di lavoro: qui vengono scritti il CSV della distinta e le "
+ "cartelle 'raw', 'step' e 'step_finali'. Proposta automaticamente "
+ "(ricordata da un avvio all'altro), modificabile con 'Cambia cartella...'.",
};
contenuto.Controls.Add(lblIstruzioniCartella);
_lblCartellaLavoro.Dock = DockStyle.Top;
_lblCartellaLavoro.AutoSize = true;
_lblCartellaLavoro.ForeColor = SystemColors.GrayText;
_lblCartellaLavoro.Margin = new Padding(0, 0, 0, 4);
contenuto.Controls.Add(_lblCartellaLavoro);
_btnCambiaCartella.Text = "Cambia cartella...";
UiStile.Secondario(_btnCambiaCartella);
_btnCambiaCartella.Click += (_, _) => ScegliCartellaLavoro();
contenuto.Controls.Add(_btnCambiaCartella);
var lblManutenzione = new Label
{
Dock = DockStyle.Top,
AutoSize = true,
ForeColor = SystemColors.GrayText,
Margin = new Padding(0, 20, 0, 8),
Text = "Manutenzione: svuota le cartelle 'raw' e 'step' nella cartella di lavoro "
+ "prima di un nuovo giro (non tocca l'indice, solo le copie locali).",
};
contenuto.Controls.Add(lblManutenzione);
_btnPulisci.Text = "Pulisci raw e step";
UiStile.Secondario(_btnPulisci);
_btnPulisci.Click += (_, _) => PulisciRawEStep();
contenuto.Controls.Add(_btnPulisci);
var panel = new Panel();
panel.Controls.Add(contenuto);
return panel;
}
private Panel BuildFaseImporta()
{
var contenuto = new TableLayoutPanel
{
Dock = DockStyle.Top,
AutoSize = true,
AutoSizeMode = AutoSizeMode.GrowAndShrink,
ColumnCount = 1,
Padding = new Padding(16),
};
contenuto.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100));
var lblIstruzioni = new Label
{
Dock = DockStyle.Top,
AutoSize = true,
ForeColor = SystemColors.GrayText,
Margin = new Padding(0, 0, 0, 8),
Text = "Trascina qui il file .xls/.xml esportato dal gestionale (foglio 'Distinta "
+ "Base'), oppure scegli il file col pulsante sotto: genera un CSV con Componente "
+ "e Descrizione, in anteprima nella griglia.",
};
contenuto.Controls.Add(lblIstruzioni);
var btnSfoglia = new Button
{
Text = "Scegli file distinta...",
Margin = new Padding(0, 0, 0, 8),
};
UiStile.Secondario(btnSfoglia);
btnSfoglia.Click += (_, _) => ScegliFileDistinta();
contenuto.Controls.Add(btnSfoglia);
_btnVerifica.Text = "Verifica presenza";
_btnVerifica.Margin = new Padding(0, 0, 0, 8);
UiStile.Secondario(_btnVerifica);
_btnVerifica.Click += (_, _) => VerificaCodiciNelDb();
contenuto.Controls.Add(_btnVerifica);
_lblImportStatus.Dock = DockStyle.Top;
_lblImportStatus.AutoSize = false;
_lblImportStatus.AutoEllipsis = true;
_lblImportStatus.Height = 24;
_lblImportStatus.ForeColor = SystemColors.GrayText;
contenuto.Controls.Add(_lblImportStatus);
_gridImport.Dock = DockStyle.Fill;
_gridImport.ReadOnly = true;
_gridImport.BorderStyle = BorderStyle.None;
_gridImport.BackgroundColor = SystemColors.Window;
_gridImport.RowHeadersVisible = false;
_gridImport.AllowUserToAddRows = false;
_gridImport.AllowUserToDeleteRows = false;
_gridImport.AllowUserToResizeRows = false;
_gridImport.SelectionMode = DataGridViewSelectionMode.FullRowSelect;
_gridImport.MultiSelect = false;
_gridImport.AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.Fill;
_gridImport.ColumnHeadersHeight = 32;
_gridImport.ColumnHeadersDefaultCellStyle.Font = new Font(Font, FontStyle.Bold);
_gridImport.ColumnHeadersDefaultCellStyle.Alignment = DataGridViewContentAlignment.MiddleLeft;
_gridImport.RowTemplate.Height = 26;
_gridImport.AlternatingRowsDefaultCellStyle.BackColor = Color.FromArgb(245, 247, 250);
_gridImport.DefaultCellStyle.SelectionBackColor = ColorePrimario;
_gridImport.DefaultCellStyle.Padding = new Padding(4, 0, 4, 0);
_gridImport.Columns.Add("componente", "Componente");
_gridImport.Columns.Add("descrizione", "Descrizione");
_gridImport.Columns["componente"]!.FillWeight = 30;
_gridImport.Columns["descrizione"]!.FillWeight = 70;
var panel = new Panel();
panel.Controls.Add(_gridImport);
panel.Controls.Add(contenuto);
return panel;
}
private Panel BuildFaseCreaRaw()
{
var contenuto = new TableLayoutPanel
{
Dock = DockStyle.Top,
AutoSize = true,
AutoSizeMode = AutoSizeMode.GrowAndShrink,
ColumnCount = 1,
Padding = new Padding(16),
};
contenuto.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100));
var lblIstruzioni = new Label
{
Dock = DockStyle.Top,
AutoSize = true,
ForeColor = SystemColors.GrayText,
Margin = new Padding(0, 0, 0, 8),
Text = "Cerca nell'indice il percorso CAD di ogni componente importato e copia i "
+ "file trovati in una cartella 'raw' dentro la cartella di lavoro (fase 1).",
};
contenuto.Controls.Add(lblIstruzioni);
_btnCreaRaw.Text = "Crea cartella raw";
_btnCreaRaw.Margin = new Padding(0, 0, 0, 12);
UiStile.Primario(_btnCreaRaw);
_btnCreaRaw.Click += (_, _) => CreaCartellaRaw();
contenuto.Controls.Add(_btnCreaRaw);
_gridRaw.Dock = DockStyle.Fill;
_gridRaw.ReadOnly = true;
_gridRaw.BorderStyle = BorderStyle.None;
_gridRaw.BackgroundColor = SystemColors.Window;
_gridRaw.RowHeadersVisible = false;
_gridRaw.AllowUserToAddRows = false;
_gridRaw.AllowUserToDeleteRows = false;
_gridRaw.AllowUserToResizeRows = false;
_gridRaw.SelectionMode = DataGridViewSelectionMode.FullRowSelect;
_gridRaw.MultiSelect = false;
_gridRaw.AutoSizeColumnsMode = DataGridViewAutoSizeColumnsMode.Fill;
_gridRaw.ColumnHeadersHeight = 32;
_gridRaw.ColumnHeadersDefaultCellStyle.Font = new Font(Font, FontStyle.Bold);
_gridRaw.RowTemplate.Height = 26;
_gridRaw.AlternatingRowsDefaultCellStyle.BackColor = Color.FromArgb(245, 247, 250);
_gridRaw.DefaultCellStyle.SelectionBackColor = ColorePrimario;
_gridRaw.DefaultCellStyle.Padding = new Padding(4, 0, 4, 0);
_gridRaw.Columns.Add("componente", "Componente");
_gridRaw.Columns.Add("percorso", "Percorso CAD");
_gridRaw.Columns["componente"]!.FillWeight = 25;
_gridRaw.Columns["percorso"]!.FillWeight = 75;
_gridRaw.CellDoubleClick += (_, _) => ApriCartellaFileSelezionato();
var panel = new Panel();
panel.Controls.Add(_gridRaw);
panel.Controls.Add(contenuto);
return panel;
}
private Panel BuildFaseTrovaComponenti()
{
var contenuto = new TableLayoutPanel
{
Dock = DockStyle.Top,
AutoSize = true,
AutoSizeMode = AutoSizeMode.GrowAndShrink,
ColumnCount = 1,
Padding = new Padding(16),
};
contenuto.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100));
var lblIstruzioni = new Label
{
Dock = DockStyle.Top,
AutoSize = true,
ForeColor = SystemColors.GrayText,
Margin = new Padding(0, 0, 0, 12),
Text = "Gli assiemi referenziano altri PRT e sotto-assiemi al loro interno: premi "
+ "qui per risolvere l'albero componenti (ricorsivamente) e copiare tutto ciò che "
+ "viene trovato nell'indice dentro 'raw'. Passo opzionale: se la distinta contiene "
+ "solo parti singole puoi anche passare direttamente alla fase successiva.",
};
contenuto.Controls.Add(lblIstruzioni);
_btnTrovaPrt.Text = "Trova componenti referenziati";
UiStile.Secondario(_btnTrovaPrt);
_btnTrovaPrt.Click += (_, _) => TrovaPrtReferenziati();
contenuto.Controls.Add(_btnTrovaPrt);
var panel = new Panel();
panel.Controls.Add(contenuto);
return panel;
}
private Panel BuildFaseVerifica()
{
// Padding qui invece che su ogni sotto-pannello: si applica uniformemente a top/fill/bottom
// (il layout docked usa il DisplayRectangle del contenitore, già ridotto dal Padding).
var panel = new Panel { Padding = new Padding(16) };
var top = new TableLayoutPanel
{
Dock = DockStyle.Top,
AutoSize = true,
AutoSizeMode = AutoSizeMode.GrowAndShrink,
ColumnCount = 1,
};
top.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100));
var lblIstruzioniCompletezza = new Label
{
Dock = DockStyle.Top,
AutoSize = true,
ForeColor = SystemColors.GrayText,
Margin = new Padding(0, 0, 0, 8),
Text = "Per ogni assieme della distinta, verifica se tutti i suoi componenti "
+ "(ricorsivamente fino ai PRT) sono nell'indice. Nota: la scansione dei .asm "
+ "è euristica, quindi qualche falso 'incompleto' è possibile.",
};
top.Controls.Add(lblIstruzioniCompletezza);
_btnCompletezza.Text = "Verifica completezza assiemi";
_btnCompletezza.Margin = new Padding(0, 0, 0, 12);
UiStile.Secondario(_btnCompletezza);
_btnCompletezza.Click += (_, _) => VerificaCompletezzaAssiemi();
top.Controls.Add(_btnCompletezza);
var lblAnteprimaAsm = new Label
{
AutoSize = true,
ForeColor = SystemColors.GrayText,
Margin = new Padding(0, 0, 0, 4),
Text = "File .asm della distinta trovati nell'indice (trascinali su Creo/Esplora risorse):",
};
top.Controls.Add(lblAnteprimaAsm);
var bottom = new TableLayoutPanel
{
Dock = DockStyle.Bottom,
AutoSize = true,
AutoSizeMode = AutoSizeMode.GrowAndShrink,
ColumnCount = 1,
};
bottom.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100));
var lblIstruzioniStep = new Label
{
Dock = DockStyle.Top,
AutoSize = true,
ForeColor = SystemColors.GrayText,
Margin = new Padding(12, 0, 0, 8),
Text = "Dopo aver esportato gli STEP con Creo, premi qui per confrontare i componenti "
+ "della distinta (e i sotto-componenti trovati in fase 4) con i file .stp presenti "
+ "in 'step', copiare quelli trovati in 'step_finali' e vedere quali mancano ancora.",
};
bottom.Controls.Add(lblIstruzioniStep);
_btnReport.Text = "Crea cartella step_finali";
UiStile.Secondario(_btnReport);
_btnReport.Click += (_, _) => GeneraReportStep();
bottom.Controls.Add(_btnReport);
// Dock=Fill invece di una Size fissa: la lista occupa tutto lo spazio verticale
// rimasto tra le istruzioni sopra e sotto, così si vedono più file .asm senza scroll
// anche a finestra ingrandita, invece dei ~160px fissi di prima.
_lstAnteprimaAsm.View = View.LargeIcon;
_lstAnteprimaAsm.MultiSelect = true;
_lstAnteprimaAsm.LargeImageList = new ImageList { ImageSize = new Size(32, 32) };
_lstAnteprimaAsm.Dock = DockStyle.Fill;
_lstAnteprimaAsm.Margin = new Padding(0, 4, 0, 12);
_lstAnteprimaAsm.BorderStyle = BorderStyle.FixedSingle;
_lstAnteprimaAsm.ItemDrag += (_, e) =>
{
var percorsi = _lstAnteprimaAsm.SelectedItems.Cast<ListViewItem>()
.Select(item => (string)item.Tag!)
.ToArray();
if (percorsi.Length > 0)
_lstAnteprimaAsm.DoDragDrop(new DataObject(DataFormats.FileDrop, percorsi), DragDropEffects.Copy);
};
panel.Controls.Add(_lstAnteprimaAsm);
panel.Controls.Add(bottom);
panel.Controls.Add(top);
return panel;
}
private void ScegliFileDistinta()
{
using var dialog = new OpenFileDialog
{
Filter = "File distinta base (*.xls;*.xml)|*.xls;*.xml|Tutti i file|*.*",
Title = "Seleziona il file Distinta Base",
};
if (dialog.ShowDialog(this) == DialogResult.OK)
ImportaDistinta(dialog.FileName);
}
private void ImportaDistinta(string percorso)
{
try
{
var (csv, righe) = DistintaBaseImporter.ConvertiInCsv(percorso, _cartellaLavoro!);
_percorsoInputCorrente = percorso;
_righeCorrenti = righe;
_gridImport.Rows.Clear();
foreach (var (componente, descrizione) in righe)
{
_gridImport.Rows.Add(componente, descrizione);
}
_gridRaw.Rows.Clear();
_ultimaCartellaRaw = null;
_ultimaVerifica = null;
ImpostaStatoImport($"CSV generato ({righe.Count} righe): {csv}", Color.DarkGreen);
}
catch (Exception ex)
{
_percorsoInputCorrente = null;
_righeCorrenti = null;
_gridImport.Rows.Clear();
ImpostaStatoImport($"Errore durante la conversione: {ex.Message}", Color.DarkRed);
}
AggiornaStatoAvanti();
}
/// <summary>Imposta testo/colore dello stato import e il tooltip con il testo completo
/// (la label tronca con "..." quando il percorso non ci sta in una riga).</summary>
private void ImpostaStatoImport(string testo, Color colore)
{
_lblImportStatus.ForeColor = colore;
_lblImportStatus.Text = testo;
_tipImportStatus.SetToolTip(_lblImportStatus, testo);
}
private void VerificaCodiciNelDb()
{
if (_righeCorrenti is null)
return;
var trovati = TrovaPercorsiCad(_righeCorrenti.Select(r => r.Componente));
_ultimaVerifica = trovati;
foreach (DataGridViewRow riga in _gridImport.Rows)
{
string componente = (string)riga.Cells["componente"].Value!;
riga.DefaultCellStyle.BackColor = trovati.ContainsKey(componente)
? Color.FromArgb(198, 239, 206)
: Color.FromArgb(255, 199, 206);
}
}
private void CreaCartellaRaw()
{
if (_percorsoInputCorrente is null || _righeCorrenti is null)
return;
// L'assieme generale è già una riga di _righeCorrenti (aggiunta da
// DistintaBaseImporter.ConvertiInCsv leggendo il nome del file distinta), quindi viene
// trovato e copiato in raw come qualsiasi altro componente, senza bisogno di un caso speciale.
var percorsiTrovati = _ultimaVerifica ?? TrovaPercorsiCad(_righeCorrenti.Select(r => r.Componente));
_gridRaw.Rows.Clear();
foreach (var (componente, _) in _righeCorrenti)
{
string percorsoCad = percorsiTrovati.TryGetValue(componente, out var lista)
? string.Join("; ", lista)
: "";
_gridRaw.Rows.Add(componente, percorsoCad);
}
var cartellaRaw = Path.Combine(_cartellaLavoro!, "raw");
int copiati = CopiaFileTrovati(percorsiTrovati.Values.SelectMany(p => p).Distinct(), cartellaRaw);
_ultimaCartellaRaw = cartellaRaw;
ImpostaStatoImport(
$"{percorsiTrovati.Count} componenti con percorso CAD trovato su {_righeCorrenti.Count}. "
+ $"{copiati} file copiati in {cartellaRaw}",
Color.DarkGreen);
AggiornaStatoAvanti();
}
/// <summary>Confronta i componenti della distinta importata con i file .stp presenti nella
/// cartella "step" (esportati manualmente con Creo Distributed Batch), per dire quali sono
/// già stati realizzati e quali mancano ancora.</summary>
private void GeneraReportStep()
{
if (_righeCorrenti is null)
return;
string radice = _cartellaLavoro!;
var cartellaStep = Path.Combine(radice, "step");
if (!Directory.Exists(cartellaStep))
{
ImpostaStatoImport($"Cartella 'step' non trovata in {cartellaStep}.", Color.DarkRed);
return;
}
var descrizioni = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
foreach (var r in _righeCorrenti)
descrizioni[r.Componente] = r.Descrizione;
// Solo i componenti della distinta importata dal CSV: i sotto-componenti trovati in
// fase 4 (_componentiRisolti) servono a completare "raw", non entrano nello STEP finale.
var tuttiICodici = _righeCorrenti.Select(r => r.Componente)
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToList();
// Per i codici senza STEP, distingue "il CAD c'è ma lo STEP non è stato esportato" da
// "il componente non è nemmeno nell'indice CAD" (riusa la verifica già fatta in fase 2/3
// se disponibile, altrimenti la rifà).
var cadTrovati = _ultimaVerifica ?? TrovaPercorsiCad(tuttiICodici);
var cartellaStepFinali = Path.Combine(radice, "step_finali");
Directory.CreateDirectory(cartellaStepFinali);
var righeReport = new List<(string Componente, string Descrizione, EsitoStep Esito)>();
foreach (var codice in tuttiICodici)
{
var percorsoStp = Path.Combine(cartellaStep, codice + ".stp");
EsitoStep esito;
if (File.Exists(percorsoStp))
{
File.Copy(percorsoStp, Path.Combine(cartellaStepFinali, codice + ".stp"), overwrite: true);
esito = EsitoStep.Creato;
}
else
{
esito = cadTrovati.ContainsKey(codice) ? EsitoStep.StepMancante : EsitoStep.ComponenteNonTrovato;
}
righeReport.Add((codice, descrizioni.GetValueOrDefault(codice, ""), esito));
}
int creati = righeReport.Count(r => r.Esito == EsitoStep.Creato);
int stepMancanti = righeReport.Count(r => r.Esito == EsitoStep.StepMancante);
int nonTrovati = righeReport.Count(r => r.Esito == EsitoStep.ComponenteNonTrovato);
ImpostaStatoImport(
$"STEP finali: {creati} copiati, {stepMancanti} da esportare (CAD presente), "
+ $"{nonTrovati} componenti non nell'indice, su {righeReport.Count} totali. "
+ $"Cartella: {cartellaStepFinali}.",
Color.DarkGreen);
using var dialog = new ReportStepForm(righeReport, cartellaStepFinali);
dialog.ShowDialog(this);
}
/// <summary>Svuota in un colpo solo le cartelle "raw", "step" e "step_finali" nella cartella
/// di lavoro, previa un'unica conferma. Non tocca l'indice CAD: solo le copie locali
/// generate dalla pipeline.</summary>
private void PulisciRawEStep()
{
string radice = _cartellaLavoro!;
var cartelle = new[] { Path.Combine(radice, "raw"), Path.Combine(radice, "step"), Path.Combine(radice, "step_finali") }
.Where(Directory.Exists)
.ToList();
if (cartelle.Count == 0)
{
ImpostaStatoImport($"Nessuna cartella 'raw', 'step' o 'step_finali' trovata in {radice}.", Color.DarkRed);
return;
}
var conferma = MessageBox.Show(this,
$"Eliminare tutti i file in:\n{string.Join("\n", cartelle)}\n\nL'operazione non è reversibile.",
"Pulisci raw e step", MessageBoxButtons.YesNo, MessageBoxIcon.Warning);
if (conferma != DialogResult.Yes)
return;
int eliminati = 0;
foreach (var cartella in cartelle)
{
foreach (var file in Directory.GetFiles(cartella))
{
try
{
File.Delete(file);
eliminati++;
}
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException)
{
// ponytail: file in uso (es. aperto in Creo) -> si salta, non blocca gli altri
}
}
}
ImpostaStatoImport($"{eliminati} file eliminati da {string.Join(" e ", cartelle)}.", Color.DarkGreen);
}
/// <summary>Tutti i codici prt/asm noti all'indice, con i relativi percorsi (ultima
/// versione, un solo file per tipo - stesso criterio di dedup per data_modifica DESC
/// usato ovunque in questa classe). Caricato in un'unica query invece che una per
/// codice: serve sia a risolvere i percorsi dei componenti trovati, sia a
/// AssemblyComponentScanner per validare i token generici (parti commerciali).</summary>
private static Dictionary<string, List<(string Tipo, string Percorso)>> CaricaCodiciNoti(string dbPath)
{
var codiciNoti = new Dictionary<string, List<(string Tipo, string Percorso)>>(StringComparer.OrdinalIgnoreCase);
using var connection = new SqliteConnection($"Data Source={dbPath};Mode=ReadOnly");
connection.Open();
using var command = connection.CreateCommand();
command.CommandText = "SELECT codice, tipo_file, percorso_completo FROM files_ultima_versione "
+ "WHERE LOWER(tipo_file) IN ('prt', 'asm') ORDER BY codice, tipo_file, data_modifica DESC";
using var reader = command.ExecuteReader();
while (reader.Read())
{
string codice = reader.GetString(0);
string tipo = reader.GetString(1).ToLowerInvariant();
var lista = codiciNoti.TryGetValue(codice, out var l) ? l : codiciNoti[codice] = new();
if (!lista.Any(x => x.Tipo == tipo))
lista.Add((tipo, reader.GetString(2)));
}
return codiciNoti;
}
/// <summary>Risolve ricorsivamente l'albero dei componenti a partire dai file .asm indicati
/// (già copiati in "raw"): per ogni assieme legge i codici referenziati al suo interno, li
/// cerca nell'indice, copia in "raw" quelli trovati e, se sono a loro volta assiemi, li
/// accoda per essere scansionati finché non restano solo PRT (foglie) o codici non presenti
/// nell'indice. Usata dal bottone "Trova componenti referenziati".</summary>
private (List<(string Codice, string Tipo, string Percorso)> Trovati, List<string> NonTrovati) RisolviAlberoComponenti(
IEnumerable<string> asmIniziali, IProgress<(int Fatti, int Totale)>? progress = null)
{
var daScansionare = new Queue<string>(asmIniziali);
var codiciIniziali = daScansionare.Select(f => Path.GetFileName(f).Split('.')[0]);
var codiciVisti = new HashSet<string>(
codiciIniziali.Concat(codiciIniziali.Select(AssemblyComponentScanner.CodiceRadice)),
StringComparer.OrdinalIgnoreCase);
var trovati = new List<(string Codice, string Tipo, string Percorso)>();
var codiciNonTrovati = new List<string>();
if (daScansionare.Count == 0 || _dbPath is null)
return (trovati, codiciNonTrovati);
var codiciNoti = CaricaCodiciNoti(_dbPath);
int processati = 0;
while (daScansionare.Count > 0)
{
var asm = daScansionare.Dequeue();
processati++;
foreach (var codice in AssemblyComponentScanner.EstraiCodiciReferenziati(asm, codiciNoti.Keys))
{
if (!codiciVisti.Add(codice))
continue;
if (!codiciNoti.TryGetValue(codice, out var componenti))
{
codiciNonTrovati.Add(codice);
continue;
}
foreach (var (tipo, percorso) in componenti)
{
CopiaFileTrovati(new[] { percorso }, _ultimaCartellaRaw!);
trovati.Add((codice, tipo, percorso));
if (tipo == "asm")
{
daScansionare.Enqueue(Path.Combine(_ultimaCartellaRaw!, Path.GetFileName(percorso)));
}
}
}
progress?.Report((processati, processati + daScansionare.Count));
}
return (trovati, codiciNonTrovati);
}
private async void TrovaPrtReferenziati()
{
if (_ultimaCartellaRaw is null || !Directory.Exists(_ultimaCartellaRaw))
return;
if (_dbPath is null)
{
ImpostaStatoImport("Nessun database caricato: impossibile risolvere i componenti referenziati.", Color.DarkRed);
return;
}
// L'assieme generale è già in raw come qualsiasi altro assieme importato (vedi
// CreaCartellaRaw), quindi rientra qui senza bisogno di un caso speciale.
var asmIniziali = Directory.GetFiles(_ultimaCartellaRaw).Where(f => Path.GetFileName(f).Contains(".asm.")).ToList();
using var progressForm = new ProgressoComponentiForm();
var progress = new Progress<(int Fatti, int Totale)>(p => progressForm.Aggiorna(p.Fatti, p.Totale));
var task = Task.Run(() => RisolviAlberoComponenti(asmIniziali, progress));
progressForm.Shown += async (_, _) =>
{
await task;
progressForm.Close();
};
progressForm.ShowDialog(this);
var (trovati, codiciNonTrovati) = await task;
int numPrt = trovati.Count(r => r.Tipo == "prt");
int numAsm = trovati.Count(r => r.Tipo == "asm");
ImpostaStatoImport(
$"Componenti referenziati: {numPrt} PRT e {numAsm} sotto-assiemi trovati e copiati in raw, "
+ $"{codiciNonTrovati.Count} codici referenziati non trovati nell'indice.",
Color.DarkGreen);
using var dialog = new PrtMancantiForm(trovati, codiciNonTrovati, _ultimaCartellaRaw);
dialog.ShowDialog(this);
}
/// <summary>Per ogni assieme della distinta, verifica ricorsivamente (leggendo i .asm dai
/// percorsi originali sulla share) se tutti i componenti fino ai PRT sono nell'indice.
/// I risultati di ogni file/codice vengono in cache e condivisi tra gli assiemi controllati
/// nella stessa passata, per non rileggere più volte gli stessi sotto-componenti.</summary>
private void VerificaCompletezzaAssiemi()
{
if (_righeCorrenti is null)
return;
if (_dbPath is null)
{
ImpostaStatoImport("Nessun database caricato: impossibile verificare la completezza.", Color.DarkRed);
return;
}
var codiciNoti = CaricaCodiciNoti(_dbPath);
var cacheCodiciFile = new Dictionary<string, HashSet<string>>();
List<(string Tipo, string Percorso)> CercaComponenti(string codice) =>
codiciNoti.TryGetValue(codice, out var trovati) ? trovati : new List<(string, string)>();
bool AssiemeCompleto(string percorsoAsmIniziale)
{
var daVisitare = new Queue<string>();
daVisitare.Enqueue(percorsoAsmIniziale);
var fileVisitati = new HashSet<string> { percorsoAsmIniziale };
var codiceIniziale = Path.GetFileName(percorsoAsmIniziale).Split('.')[0];
var codiciVisti = new HashSet<string>
{
codiceIniziale,
AssemblyComponentScanner.CodiceRadice(codiceIniziale),
};
while (daVisitare.Count > 0)
{
var file = daVisitare.Dequeue();
if (!cacheCodiciFile.TryGetValue(file, out var codiciReferenziati))
{
try
{
codiciReferenziati = AssemblyComponentScanner.EstraiCodiciReferenziati(file, codiciNoti.Keys);
}
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException)
{
return false; // file irraggiungibile (share offline) -> non verificabile, per prudenza "incompleto"
}
cacheCodiciFile[file] = codiciReferenziati;
}
foreach (var codice in codiciReferenziati)
{
if (!codiciVisti.Add(codice))
continue;
var componenti = CercaComponenti(codice);
if (componenti.Count == 0)
return false;
foreach (var (tipo, percorso) in componenti)
{
if (tipo == "asm" && fileVisitati.Add(percorso))
{
daVisitare.Enqueue(percorso);
}
}
}
}
return true;
}
// L'assieme generale è già una riga di _righeCorrenti (vedi DistintaBaseImporter), quindi
// la sua completezza viene verificata insieme a quella di tutti gli altri componenti.
var righeReport = new List<(string Componente, string Descrizione, bool? Completo)>();
foreach (var (componente, descrizione) in _righeCorrenti)
{
var componenti = CercaComponenti(componente);
string? percorsoAsm = componenti.Where(c => c.Tipo == "asm").Select(c => c.Percorso).FirstOrDefault();
bool? completo = componenti.Count == 0 ? null
: percorsoAsm is null ? true // solo prt trovato: nessuna sotto-struttura da verificare
: AssiemeCompleto(percorsoAsm);
righeReport.Add((componente, descrizione, completo));
}
int completi = righeReport.Count(r => r.Completo == true);
int incompleti = righeReport.Count(r => r.Completo == false);
int nonTrovati = righeReport.Count(r => r.Completo is null);
ImpostaStatoImport(
$"Completezza assiemi: {completi} completi, {incompleti} incompleti, "
+ $"{nonTrovati} non trovati nell'indice.",
Color.DarkGreen);
using var dialogCompletezza = new ReportCompletezzaForm(righeReport);
dialogCompletezza.ShowDialog(this);
}
/// <summary>Copia i file trovati nella cartella "raw" indicata, creandola se necessario.
/// Ritorna il numero di file copiati con successo. Parallelizzata (grado limitato): il
/// collo di bottiglia è la latenza di rete verso la share, non la CPU, quindi poche copie
/// concorrenti bastano a sovrapporre le attese senza saturare la share.</summary>
internal static int CopiaFileTrovati(IEnumerable<string> percorsi, string cartellaRaw)
{
Directory.CreateDirectory(cartellaRaw);
int copiati = 0;
Parallel.ForEach(percorsi, new ParallelOptions { MaxDegreeOfParallelism = 6 }, percorso =>
{
try
{
File.Copy(percorso, Path.Combine(cartellaRaw, Path.GetFileName(percorso)), overwrite: true);
Interlocked.Increment(ref copiati);
}
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException)
{
// ponytail: file non raggiungibile (share offline, permessi) -> lo si salta, non blocca gli altri
}
});
return copiati;
}
/// <summary>Cerca, per ogni codice componente, i percorsi dei file CAD (ultima versione,
/// solo prt/asm) corrispondenti nell'indice.</summary>
private Dictionary<string, List<string>> TrovaPercorsiCad(IEnumerable<string> codici)
{
var risultato = new Dictionary<string, List<string>>();
if (_dbPath is null)
return risultato;
using var connection = new SqliteConnection($"Data Source={_dbPath};Mode=ReadOnly");
connection.Open();
var nomiParametri = TipiPredefiniti.Select((_, i) => $"@tipo{i}");
using var command = connection.CreateCommand();
command.CommandText = $"""
SELECT tipo_file, percorso_completo FROM files_ultima_versione
WHERE LOWER(codice) = LOWER(@codice) AND LOWER(tipo_file) IN ({string.Join(",", nomiParametri)})
ORDER BY tipo_file, data_modifica DESC
""";
var pCodice = command.Parameters.Add("@codice", Microsoft.Data.Sqlite.SqliteType.Text);
for (int i = 0; i < TipiPredefiniti.Length; i++)
{
command.Parameters.AddWithValue($"@tipo{i}", TipiPredefiniti[i]);
}
foreach (var codice in codici.Distinct())
{
pCodice.Value = codice;
using var reader = command.ExecuteReader();
var percorsi = new List<string>();
// Lo stesso codice+tipo può esistere in più cartelle radice (duplicati sulla share):
// essendo ordinato per data_modifica DESC, il primo per tipo è il più recente, gli altri si scartano.
var tipiVisti = new HashSet<string>();
while (reader.Read())
{
if (tipiVisti.Add(reader.GetString(0).ToLowerInvariant()))
{
percorsi.Add(reader.GetString(1));
}
}
if (percorsi.Count > 0)
risultato[codice] = percorsi;
}
return risultato;
}
private static readonly string CartellaImpostazioni = Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData), "PrepStep");
private static readonly string PercorsoImpostazioni = Path.Combine(CartellaImpostazioni, "ultimo_db.txt");
private static readonly string PercorsoImpostazioniCartella = Path.Combine(CartellaImpostazioni, "ultima_cartella_lavoro.txt");
/// <summary>Propone l'ultima cartella di lavoro usata (se ancora esiste su disco) o, la prima
/// volta, la cartella dell'eseguibile: non richiede mai un CLAUDE.md/repo (a differenza del
/// vecchio TrovaRadiceRepo), quindi funziona identica anche sull'exe pubblicato standalone
/// copiato su un'altra macchina. Sempre modificabile con "Cambia cartella..." in fase 1.</summary>
private void InizializzaCartellaLavoro()
{
var ultima = LeggiUltimaCartellaLavoro();
_cartellaLavoro = ultima is not null && Directory.Exists(ultima) ? ultima : AppContext.BaseDirectory;
AggiornaLabelCartellaLavoro();
}
private void ScegliCartellaLavoro()
{
using var dialog = new FolderBrowserDialog
{
Description = "Seleziona la cartella di lavoro (CSV, raw, step, step_finali)",
SelectedPath = _cartellaLavoro ?? string.Empty,
};
if (dialog.ShowDialog(this) != DialogResult.OK)
return;
string percorso = dialog.SelectedPath;
_cartellaLavoro = percorso;
AggiornaLabelCartellaLavoro();
SalvaUltimaCartellaLavoro(percorso);
AggiornaStatoAvanti();
}
private void AggiornaLabelCartellaLavoro() => _lblCartellaLavoro.Text = $"Cartella di lavoro: {_cartellaLavoro}";
private static string? LeggiUltimaCartellaLavoro()
{
try
{
return File.Exists(PercorsoImpostazioniCartella) ? File.ReadAllText(PercorsoImpostazioniCartella).Trim() : null;
}
catch (IOException)
{
return null;
}
}
private static void SalvaUltimaCartellaLavoro(string path)
{
try
{
Directory.CreateDirectory(CartellaImpostazioni);
File.WriteAllText(PercorsoImpostazioniCartella, path);
}
catch (IOException)
{
// ponytail: non critico se non si riesce a salvare, si ripropone il default al prossimo avvio
}
}
private void InizializzaDatabase()
{
var candidati = new[]
{
LeggiUltimoDbUsato(),
Path.Combine(AppContext.BaseDirectory, "cad_index.db"),
Path.Combine(Directory.GetCurrentDirectory(), "cad_index.db"),
};
_dbPath = candidati.FirstOrDefault(p => p is not null && File.Exists(p));
if (_dbPath is null)
{
_lblStatus.Text = "Database non trovato. Usa 'Apri DB...' per selezionarlo oppure 'Aggiorna indice' per crearlo.";
_lblDb.Text = "Nessun database caricato. Premi 'Aggiorna indice' per crearlo da X:\\, oppure 'Apri DB...' se ne hai gi\u00e0 uno.";
_lblUltimoAggiornamento.Text = "";
AggiornaStatoAvanti();
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();
long totale = (long)(new SqliteCommand("SELECT COUNT(*) FROM files", connection).ExecuteScalar() ?? 0L);
object? ultimaScansioneRaw = new SqliteCommand("SELECT MAX(scansionato_il) FROM files", connection).ExecuteScalar();
_dbPath = path;
_lblDb.Text = $"Database: {path} ({totale:N0} file indicizzati)";
_lblUltimoAggiornamento.Text = ultimaScansioneRaw is string testoData
&& DateTime.TryParse(testoData, out var ultimaScansione)
? $"Ultimo aggiornamento indice: {ultimaScansione.ToLocalTime():dd/MM/yyyy HH:mm}"
: "Ultimo aggiornamento indice: sconosciuto";
_lblStatus.Text = "Pronto.";
SalvaUltimoDbUsato(path);
}
catch (Exception ex)
{
MessageBox.Show(this, $"Impossibile aprire il database:\n{ex.Message}", "Errore",
MessageBoxButtons.OK, MessageBoxIcon.Error);
}
AggiornaStatoAvanti();
}
/// <summary>Ricorda l'ultimo DB caricato con successo, così al prossimo avvio si
/// ricarica automaticamente anche se l'eseguibile gira da una cartella diversa.</summary>
private static string? LeggiUltimoDbUsato()
{
try
{
return File.Exists(PercorsoImpostazioni) ? File.ReadAllText(PercorsoImpostazioni).Trim() : null;
}
catch (IOException)
{
return null;
}
}
private static void SalvaUltimoDbUsato(string path)
{
try
{
Directory.CreateDirectory(CartellaImpostazioni);
File.WriteAllText(PercorsoImpostazioni, path);
}
catch (IOException)
{
// ponytail: non critico se non si riesce a salvare, si torna a chiedere "Apri DB..." la prossima volta
}
}
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;
_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;
}
}
private void ApriCartellaFileSelezionato()
{
if (_gridRaw.CurrentRow is null)
{
return;
}
string? percorsi = _gridRaw.CurrentRow.Cells[1].Value as string;
if (string.IsNullOrEmpty(percorsi))
{
return;
}
string primoPercorso = percorsi.Split("; ", 2)[0];
try
{
Process.Start("explorer.exe", $"/select,\"{primoPercorso}\"");
}
catch (Exception ex)
{
MessageBox.Show(this, $"Impossibile aprire Esplora risorse:\n{ex.Message}", "Errore",
MessageBoxButtons.OK, MessageBoxIcon.Error);
}
}
}