- 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.
406 lines
16 KiB
C#
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();
|
|
}
|
|
}
|
|
}
|