namespace PrepStep; public sealed class ProgressForm : Form { private readonly Label _lblTitolo = new(); private readonly Label _lblCartella = new(); private readonly Panel _barraTraccia = new(); private readonly Panel _barraRiempimento = new(); private readonly Label _lblPercentuale = new(); private readonly Label _lblConteggio = new(); private readonly Label _lblDettaglio = new(); private readonly Label _lblEta = new(); private readonly Button _btnAnnulla = new(); private readonly Panel _separatore = new(); private readonly Panel _piePagina = new(); private readonly ToolTip _tipCartella = new(); private const int Margine = 24; // SystemColors.GrayText (~3.9:1 su bianco) è sotto la soglia WCAG AA 4.5:1 per testo // normale: usiamo un grigio più scuro, leggibile ma comunque secondario. private static readonly Color GrigioTesto = Color.FromArgb(90, 90, 90); public event EventHandler? Annullato; public ProgressForm() { Text = "Aggiornamento indice"; FormBorderStyle = FormBorderStyle.FixedDialog; StartPosition = FormStartPosition.CenterParent; MinimizeBox = false; MaximizeBox = false; ControlBox = false; BackColor = Color.White; ClientSize = new Size(480, 270); _lblTitolo.Text = "Aggiornamento indice in corso..."; _lblTitolo.Location = new Point(Margine, 22); _lblTitolo.AutoSize = true; _lblTitolo.Font = new Font(Font.FontFamily, 11f, FontStyle.Bold); _lblCartella.Text = "Preparazione..."; _lblCartella.Location = new Point(Margine, 52); // AutoSize=false + AutoEllipsis: un percorso lungo (share di rete) va troncato con "..." // invece di sforare il bordo del dialog, che è FixedDialog e non si può allargare. // Il testo completo resta disponibile nel tooltip (stesso pattern di Form1._lblImportStatus). _lblCartella.AutoSize = false; _lblCartella.AutoEllipsis = true; _lblCartella.Size = new Size(ClientSize.Width - Margine * 2, 20); _lblCartella.ForeColor = GrigioTesto; // Barra piatta disegnata a mano (traccia grigia + riempimento) invece della ProgressBar // nativa: quella di Windows è verde e in stile "a blocchi", stona con il blu del resto // dell'app (UiStile.Primario). Larghezza = margine destro reale meno lo spazio per // "100%" in grassetto, così barra+percentuale rispettano lo stesso margine destro del // resto del contenuto (prima finivano ~25px prima, sbilanciate rispetto al margine // sinistro di 24px). _barraTraccia.Location = new Point(Margine, 90); _barraTraccia.Size = new Size(ClientSize.Width - Margine * 2 - 46, 10); _barraTraccia.BackColor = Color.FromArgb(230, 230, 230); _barraRiempimento.Location = Point.Empty; _barraRiempimento.Size = new Size(0, _barraTraccia.Height); _barraRiempimento.BackColor = UiStile.ColorePrimario; _barraTraccia.Controls.Add(_barraRiempimento); _lblPercentuale.AutoSize = true; _lblPercentuale.Font = new Font(Font, FontStyle.Bold); _lblPercentuale.TextAlign = ContentAlignment.MiddleRight; _lblConteggio.Location = new Point(Margine, 122); _lblConteggio.AutoSize = true; _lblDettaglio.Location = new Point(Margine, 146); _lblDettaglio.AutoSize = true; _lblDettaglio.ForeColor = GrigioTesto; _lblEta.Location = new Point(Margine, 174); _lblEta.AutoSize = true; // Footer separato dal contenuto (linea + sfondo leggermente diverso) invece del // bottone appiccicato al bordo inferiore del dialog: ancorato in basso, quindi la // spaziatura sopra/sotto resta corretta anche se la ClientSize cambia. Altezza footer // 60px e ClientSize 270 scelti per lasciare sotto l'ultima riga (eta, Bottom≈189) lo // stesso margine (~20px) usato sopra il titolo, invece dei ~7px sbilanciati di prima. _separatore.BackColor = Color.FromArgb(230, 230, 230); _separatore.Size = new Size(ClientSize.Width, 1); _separatore.Location = new Point(0, ClientSize.Height - 60); _separatore.Anchor = AnchorStyles.Left | AnchorStyles.Right | AnchorStyles.Bottom; _piePagina.BackColor = Color.FromArgb(247, 247, 248); _piePagina.Size = new Size(ClientSize.Width, 59); _piePagina.Location = new Point(0, ClientSize.Height - 59); _piePagina.Anchor = AnchorStyles.Left | AnchorStyles.Right | AnchorStyles.Bottom; _btnAnnulla.Text = "Annulla"; UiStile.Secondario(_btnAnnulla); UiStile.Arrotonda(_btnAnnulla); _btnAnnulla.Click += (_, e) => Annullato?.Invoke(this, e); _piePagina.Controls.Add(_btnAnnulla); Controls.AddRange(new Control[] { _lblTitolo, _lblCartella, _barraTraccia, _lblPercentuale, _lblConteggio, _lblDettaglio, _lblEta, _separatore, _piePagina }); // Posizionato dopo l'aggiunta ai Controls: con AutoSize il Width vero si conosce // solo a layout avvenuto, non subito dopo la creazione del bottone. _btnAnnulla.Location = new Point(_piePagina.Width - Margine - _btnAnnulla.Width, (_piePagina.Height - _btnAnnulla.Height) / 2); _btnAnnulla.Anchor = AnchorStyles.Right | AnchorStyles.Top; } public void Aggiorna(ScanProgress p) { _lblCartella.Text = $"Scansione: {p.Cartella}"; _tipCartella.SetToolTip(_lblCartella, _lblCartella.Text); if (p.PrevistiTotali > 0) { double frazione = Math.Min(1.0, (double)p.FattiTotali / p.PrevistiTotali); _barraRiempimento.Width = (int)(frazione * _barraTraccia.Width); _lblPercentuale.Text = $"{frazione * 100:0}%"; } else { // ponytail: prima del conteggio totale (poco probabile: richiede la scansione // precedente salvata nel DB) la barra resta vuota invece di un'animazione // indeterminata — se serve, aggiungere un Timer che pulsa _barraRiempimento. _barraRiempimento.Width = 0; _lblPercentuale.Text = ""; } // Riallineato al margine destro reale ogni volta: AutoSize aggiorna Width non appena // Text cambia, quindi la Location va ricalcolata qui (non solo una volta nel costruttore). _lblPercentuale.Location = new Point( ClientSize.Width - Margine - _lblPercentuale.Width, _barraTraccia.Top - 3); _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"; } }