Files
Mimante/Mimante/Core/MainWindow.Catalog.cs
T
Alby96 7ca504a70a Add utility classes for theme management, waiting, watched products, and notifications
- Implement ThemeManager for dynamic light/dark theme switching in the application.
- Create Wait class for cancellable delays without exceptions for smoother user experience.
- Introduce WatchedProductsStore to manage and persist watched products in JSON format.
- Add WindowsNotifier for system notifications to inform users of important events.
- Develop ProductViewModel to encapsulate product data and manage UI interactions effectively.
2026-08-04 21:49:53 +02:00

406 lines
16 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using AutoBidder.Models;
using AutoBidder.Services;
using AutoBidder.Utilities;
namespace AutoBidder
{
/// <summary>
/// Catalogo nativo della scheda Esplora.
///
/// Affianca il browser integrato senza sostituirlo: il browser resta l'unico modo per
/// fare il login (da cui il cookie viene importato da solo), questa e' la via veloce
/// per confrontare molte aste senza caricare pagine.
/// </summary>
public partial class MainWindow
{
private BidooCatalogClient? _catalogClient;
private readonly List<CatalogCategory> _catalogCategories = new();
/// <summary>Aste della categoria corrente, prima dei filtri.</summary>
private List<CatalogAuction> _catalogAuctions = new();
/// <summary>
/// Le aste effettivamente mostrate, nell'ordine in cui compaiono. È su queste che
/// lavora l'aggiornamento prezzi: aggiornare anche quelle nascoste dai filtri
/// costerebbe richieste per numeri che nessuno sta guardando.
/// </summary>
private List<CatalogAuction> _catalogVisible = new();
private CatalogCategory? _catalogCategory;
private CancellationTokenSource? _catalogCts;
private CancellationTokenSource? _catalogRefreshCts;
/// <summary>Comandi usati dai pulsanti sulle singole schede prodotto.</summary>
public RelayCommand? CatalogAddCommand { get; private set; }
public RelayCommand? CatalogOpenCommand { get; private set; }
public RelayCommand? CatalogWatchCommand { get; private set; }
public RelayCommand? CatalogConfigureCommand { get; private set; }
private BidooCatalogClient CatalogClient
{
get
{
if (_catalogClient == null)
{
_catalogClient = new BidooCatalogClient(_auctionMonitor.GetApiClient().Transport)
{
Diagnostic = msg => Dispatcher.Invoke(() => Log(msg, LogLevel.Info))
};
}
return _catalogClient;
}
}
private void InitializeCatalogCommands()
{
CatalogAddCommand = new RelayCommand(async p => await AddFromCatalogAsync(p as CatalogAuction));
CatalogOpenCommand = new RelayCommand(p => OpenCatalogAuction(p as CatalogAuction));
CatalogWatchCommand = new RelayCommand(p => ToggleWatchedProduct(p as CatalogAuction));
CatalogConfigureCommand = new RelayCommand(p => ConfigureCatalogProduct(p as CatalogAuction));
}
/// <summary>Carica l'elenco categorie la prima volta che si apre la scheda Esplora.</summary>
private async Task EnsureCatalogLoadedAsync()
{
if (_catalogCategories.Count > 0) return;
// L'interruttore riflette l'impostazione salvata.
Browser.SetAutoRefresh(SettingsManager.Load().CatalogAutoRefresh);
try
{
Browser.SetCatalogMessage("Caricamento categorie…");
var categories = await CatalogClient.GetCategoriesAsync(false, CancellationToken.None);
_catalogCategories.Clear();
_catalogCategories.AddRange(categories);
Browser.SetCategories(_catalogCategories);
var first = _catalogCategories.FirstOrDefault();
if (first != null)
{
// Prima si registra la categoria corrente, poi si spunta il pulsante:
// spuntarlo scatena CategoryChanged, e senza questo ordine partirebbero
// due caricamenti concorrenti che si annullano a vicenda.
_catalogCategory = first;
first.IsSelected = true;
await LoadCategoryAsync(first);
}
else
{
Browser.SetCatalogMessage("Nessuna categoria disponibile.");
}
}
catch (Exception ex)
{
Browser.SetCatalogMessage($"Impossibile caricare le categorie: {ex.Message}");
Log($"[CATALOGO] Errore categorie: {ex.Message}", LogLevel.Error);
}
}
private async Task LoadCategoryAsync(CatalogCategory category)
{
// Una richiesta per volta: cambiare categoria durante un caricamento deve
// annullare il precedente, non accodarne un altro.
var previous = _catalogCts;
var cts = new CancellationTokenSource();
_catalogCts = cts;
previous?.Cancel();
previous?.Dispose();
_catalogCategory = category;
StopCatalogAutoRefresh();
try
{
Browser.SetCatalogMessage($"Carico \"{category.DisplayName}\"…");
var settings = SettingsManager.Load();
var max = Math.Max(20, settings.CatalogMaxAuctions);
var auctions = await CatalogClient.GetAllAuctionsAsync(
category, max, cts.Token, settings.CatalogCacheSeconds);
if (cts.Token.IsCancellationRequested) return;
// La pagina HTML da sola darebbe sempre 0,01 € e il timer di partenza:
// un secondo giro su data.php porta prezzi e scadenze reali.
if (auctions.Count > 0)
await CatalogClient.UpdateStatesAsync(auctions, cts.Token);
if (cts.Token.IsCancellationRequested) return;
MarkWatchedProducts(auctions);
_catalogAuctions = auctions;
RebuildCatalogView();
StartCatalogAutoRefresh();
}
catch (OperationCanceledException)
{
// Sostituita da una richiesta piu' recente: nessun messaggio, e' normale.
}
catch (Exception ex)
{
Browser.SetCatalogMessage($"Errore: {ex.Message}");
Log($"[CATALOGO] {ex.Message}", LogLevel.Error);
}
finally
{
if (ReferenceEquals(_catalogCts, cts))
{
_catalogCts = null;
cts.Dispose();
}
}
}
/// <summary>
/// Segna quali schede appartengono a un prodotto gia' in elenco, distinguendo
/// "seguito" (stellina) da "solo configurato" (ingranaggio acceso).
/// </summary>
private static void MarkWatchedProducts(IEnumerable<CatalogAuction> auctions)
{
var products = WatchedProductsStore.GetAll();
foreach (var auction in auctions)
{
var rule = products.FirstOrDefault(p => p.Matches(auction));
auction.IsWatched = rule?.IsWatched == true;
auction.IsListed = rule != null;
}
}
/// <summary>
/// Ricostruisce (e riordina) l'elenco mostrato. Si chiama SOLO al caricamento o
/// quando cambiano i filtri: riordinare mentre si legge farebbe saltare le schede
/// da una posizione all'altra sotto il cursore.
/// </summary>
private void RebuildCatalogView()
{
var filter = Browser.SearchText;
var hideManual = Browser.HideManualAuctions;
var view = _catalogAuctions
.Where(a => !hideManual || !a.IsManualOnly)
.Where(a => filter.Length == 0 || a.Name.Contains(filter, StringComparison.OrdinalIgnoreCase))
// Le aste che stanno per chiudere sono quelle su cui si decide: prima.
.OrderBy(a => a.RemainingSeconds <= 0 ? int.MaxValue : a.RemainingSeconds)
.ToList();
_catalogVisible = view;
Browser.SetCatalogItems(view, _catalogAuctions.Count);
}
// ── Aggiornamento prezzi sul posto ───────────────────────────────
/// <summary>
/// Quante aste tenere aggiornate mentre si guarda la griglia. Il listato si sfoglia
/// ormai a migliaia, e <c>data.php</c> ne accetta una sessantina per chiamata: senza
/// un tetto, un catalogo grande significherebbe decine di richieste ogni due secondi
/// per aggiornare aste che chiudono fra ore.
/// </summary>
private const int CatalogRefreshBudget = 300;
private void StartCatalogAutoRefresh()
{
StopCatalogAutoRefresh();
if (!Browser.AutoRefreshEnabled || _catalogAuctions.Count == 0) return;
var cts = new CancellationTokenSource();
_catalogRefreshCts = cts;
_ = CatalogRefreshLoopAsync(cts.Token);
}
private void StopCatalogAutoRefresh()
{
var cts = _catalogRefreshCts;
_catalogRefreshCts = null;
cts?.Cancel();
cts?.Dispose();
}
/// <summary>
/// Aggiorna prezzi e timer delle aste in vista. I valori cambiano <i>sul posto</i>:
/// nessun riordino e nessuna ricostruzione della lista, altrimenti le schede
/// ballerebbero mentre le si guarda.
///
/// <para>Si aggiornano le prime <see cref="CatalogRefreshBudget"/> della griglia,
/// che essendo ordinata per scadenza sono quelle che stanno per chiudere: sono le
/// uniche i cui numeri cambiano da un momento all'altro.</para>
/// </summary>
private async Task CatalogRefreshLoopAsync(CancellationToken ct)
{
while (!ct.IsCancellationRequested)
{
if (!await Wait.DelayAsync(2000, ct).ConfigureAwait(false)) return;
var visible = _catalogVisible;
if (visible.Count == 0) continue;
var snapshot = visible.Count > CatalogRefreshBudget
? visible.GetRange(0, CatalogRefreshBudget)
: visible;
try
{
await CatalogClient.UpdateStatesAsync(snapshot, ct).ConfigureAwait(false);
}
catch (OperationCanceledException)
{
return;
}
catch
{
// Un aggiornamento saltato non compromette la pagina.
continue;
}
if (ct.IsCancellationRequested) return;
await Dispatcher.InvokeAsync(() =>
{
foreach (var auction in snapshot) auction.NotifyStateChanged();
});
}
}
// ── Azioni sulle schede ──────────────────────────────────────────
private async Task AddFromCatalogAsync(CatalogAuction? auction)
{
if (auction == null) return;
if (_auctionViewModels.Any(a => a.AuctionId == auction.AuctionId))
{
Log($"[CATALOGO] {auction.Name} è già nel monitor", LogLevel.Info);
return;
}
await AddAuctionById(auction.Url);
WatchedProductsStore.MarkHandled(auction.AuctionId);
Log($"[CATALOGO] Aggiunta al monitor: {auction.Name}", LogLevel.Info);
}
private void OpenCatalogAuction(CatalogAuction? auction)
{
if (auction == null) return;
try
{
// Passa alla scheda Browser: TabBrowser_Checked mostra il pannello e il
// browser integrato, poi si naviga all'asta.
TabBrowser.IsChecked = true;
Browser.ShowBrowser();
Browser.EmbeddedWebView?.CoreWebView2?.Navigate(auction.Url);
Browser.BrowserAddress.Text = auction.Url;
}
catch (Exception ex)
{
Log($"[CATALOGO] Apertura nel browser fallita: {ex.Message}", LogLevel.Error);
}
}
/// <summary>
/// Attiva o disattiva la sorveglianza del prodotto (stellina). Da qui in poi le aste
/// nuove dello stesso articolo entrano nel monitor da sole.
/// </summary>
private void ToggleWatchedProduct(CatalogAuction? auction)
{
if (auction == null) return;
try
{
if (auction.IsWatched)
{
// Spegnere la stellina non butta via i limiti scritti a mano: la scheda
// resta fra i Prodotti, solo senza aggiunta automatica.
WatchedProductsStore.SetWatched(auction, false);
Log($"[SEGUITI] Non seguo più: {auction.Name}", LogLevel.Info);
}
else
{
WatchedProductsStore.SetWatched(auction, true);
var settings = SettingsManager.Load();
if (settings.AutoAddProductsEnabled)
{
Log($"[SEGUITI] Ora seguo: {auction.Name} — le aste nuove entreranno in stato {StateLabel(settings.AutoAddNewAuctionState)}",
LogLevel.Success);
_ = _productWatcher?.ScanNowAsync();
}
else
{
Log($"[SEGUITI] Ora seguo: {auction.Name} — l'aggiunta automatica è però disattivata in Impostazioni",
LogLevel.Warning);
}
}
// Tutte le schede dello stesso prodotto cambiano stella insieme.
MarkWatchedProducts(_catalogAuctions);
}
catch (Exception ex)
{
Log($"[SEGUITI] Errore: {ex.Message}", LogLevel.Error);
}
}
private static string StateLabel(string state) => state switch
{
"Active" => "Attiva",
"Stopped" => "Ferma",
_ => "Osserva"
};
// ── Handler degli eventi del controllo ───────────────────────────
private async void Browser_CatalogCategoryChanged(object sender, RoutedEventArgs e)
{
var selected = _catalogCategories.FirstOrDefault(c => c.IsSelected);
if (selected == null || ReferenceEquals(selected, _catalogCategory)) return;
await LoadCategoryAsync(selected);
}
private async void Browser_CatalogRefreshClicked(object sender, RoutedEventArgs e)
{
// Chi preme "Aggiorna" vuole i dati dal server, non quelli in cache.
CatalogClient.InvalidateCache();
if (_catalogCategory == null)
{
_catalogCategories.Clear();
await EnsureCatalogLoadedAsync();
return;
}
await LoadCategoryAsync(_catalogCategory);
}
private void Browser_CatalogSearchChanged(object sender, RoutedEventArgs e)
{
// Filtri cambiati: qui il riordino ci sta, è l'utente ad averlo chiesto.
RebuildCatalogView();
}
private void Browser_CatalogAutoRefreshChanged(object sender, RoutedEventArgs e)
{
var settings = SettingsManager.Load();
settings.CatalogAutoRefresh = Browser.AutoRefreshEnabled;
SettingsManager.Save(settings);
if (Browser.AutoRefreshEnabled) StartCatalogAutoRefresh();
else StopCatalogAutoRefresh();
}
}
}