Aggiunta sezione impostazioni per manutenzione database (auto-salvataggio, pulizia duplicati/incompleti, retention, ottimizzazione). Implementati metodi asincroni in DatabaseService per pulizia e statistiche. Pulizia automatica all’avvio secondo impostazioni. Rimossa la proprietà MaxClicks da modello, UI e logica. Migliorata la sicurezza thread-safe e la trasparenza nella gestione dati. Spostato il badge versione nelle info applicazione.
628 lines
22 KiB
Plaintext
628 lines
22 KiB
Plaintext
@page "/browser"
|
|
@attribute [Microsoft.AspNetCore.Authorization.Authorize]
|
|
@using AutoBidder.Models
|
|
@using AutoBidder.Services
|
|
@inject BidooBrowserService BrowserService
|
|
@inject ApplicationStateService AppState
|
|
@inject AuctionMonitor AuctionMonitor
|
|
@inject IJSRuntime JSRuntime
|
|
@implements IDisposable
|
|
|
|
<PageTitle>Esplora Aste - AutoBidder</PageTitle>
|
|
|
|
<div class="browser-container animate-fade-in p-4">
|
|
<!-- Header -->
|
|
<div class="d-flex align-items-center justify-content-between mb-4 flex-wrap gap-3">
|
|
<div class="d-flex align-items-center animate-fade-in-down">
|
|
<i class="bi bi-search text-primary me-3" style="font-size: 2rem;"></i>
|
|
<div>
|
|
<h2 class="mb-0 fw-bold">Esplora Aste</h2>
|
|
<small class="text-muted">Naviga le aste pubbliche di Bidoo senza login</small>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="d-flex gap-2 flex-wrap">
|
|
<button class="btn btn-outline-secondary" @onclick="RefreshAll" disabled="@isLoading">
|
|
<i class="bi @(isLoading ? "bi-arrow-clockwise spin" : "bi-arrow-clockwise")"></i>
|
|
Aggiorna
|
|
</button>
|
|
@if (auctions.Count > 0)
|
|
{
|
|
<button class="btn btn-outline-danger" @onclick="ClearAllAuctions" title="Rimuove tutte le aste caricate e ferma il polling">
|
|
<i class="bi bi-trash"></i>
|
|
Pulisci Tutto
|
|
</button>
|
|
}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Category Selector -->
|
|
<div class="card mb-4 shadow-sm">
|
|
<div class="card-body">
|
|
<div class="row g-3 align-items-end">
|
|
<div class="col-md-6">
|
|
<label class="form-label fw-semibold">
|
|
<i class="bi bi-tag me-2"></i>Categoria
|
|
</label>
|
|
<select class="form-select form-select-lg" @bind="selectedCategoryIndex" @bind:after="OnCategoryChanged">
|
|
@if (categories.Count == 0)
|
|
{
|
|
<option value="-1">Caricamento categorie...</option>
|
|
}
|
|
else
|
|
{
|
|
@for (int i = 0; i < categories.Count; i++)
|
|
{
|
|
<option value="@i">
|
|
@if (!string.IsNullOrEmpty(categories[i].Icon))
|
|
{
|
|
@categories[i].DisplayName
|
|
}
|
|
else
|
|
{
|
|
@categories[i].DisplayName
|
|
}
|
|
</option>
|
|
}
|
|
}
|
|
</select>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<div class="stats-mini">
|
|
<span class="text-muted">Aste caricate:</span>
|
|
<span class="fw-bold text-primary ms-2">@auctions.Count</span>
|
|
</div>
|
|
</div>
|
|
<div class="col-md-3">
|
|
<div class="stats-mini">
|
|
<span class="text-muted">Monitorate:</span>
|
|
<span class="fw-bold text-success ms-2">@auctions.Count(a => a.IsMonitored)</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- ? NUOVO: Search Bar -->
|
|
<div class="card mb-4 shadow-sm">
|
|
<div class="card-body">
|
|
<div class="row g-3 align-items-center">
|
|
<div class="col-md-8">
|
|
<div class="input-group input-group-lg">
|
|
<span class="input-group-text bg-primary text-white border-0">
|
|
<i class="bi bi-search"></i>
|
|
</span>
|
|
<input type="text"
|
|
class="form-control form-control-lg border-0"
|
|
placeholder="Cerca per nome asta, prezzo, vincitore..."
|
|
@bind="searchQuery"
|
|
@bind:event="oninput"
|
|
@bind:after="OnSearchChanged" />
|
|
@if (!string.IsNullOrEmpty(searchQuery))
|
|
{
|
|
<button class="btn btn-outline-secondary border-0"
|
|
@onclick="ClearSearch"
|
|
title="Cancella ricerca">
|
|
<i class="bi bi-x-lg"></i>
|
|
</button>
|
|
}
|
|
</div>
|
|
</div>
|
|
<div class="col-md-4">
|
|
<div class="stats-mini">
|
|
<span class="text-muted">Risultati filtrati:</span>
|
|
<span class="fw-bold text-info ms-2">@filteredAuctions.Count</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Loading State -->
|
|
@if (isLoading)
|
|
{
|
|
<div class="text-center py-5 animate-fade-in">
|
|
<div class="spinner-border text-primary mb-3" role="status" style="width: 3rem; height: 3rem;">
|
|
<span class="visually-hidden">Caricamento...</span>
|
|
</div>
|
|
<p class="text-muted">Caricamento aste in corso...</p>
|
|
</div>
|
|
}
|
|
else if (errorMessage != null)
|
|
{
|
|
<div class="alert alert-warning d-flex align-items-center animate-scale-in">
|
|
<i class="bi bi-exclamation-triangle-fill me-3" style="font-size: 1.5rem;"></i>
|
|
<div>
|
|
<strong>Attenzione</strong><br />
|
|
@errorMessage
|
|
</div>
|
|
</div>
|
|
}
|
|
else if (filteredAuctions.Count == 0 && !string.IsNullOrEmpty(searchQuery))
|
|
{
|
|
<div class="text-center py-5 animate-fade-in">
|
|
<i class="bi bi-search text-muted" style="font-size: 4rem;"></i>
|
|
<p class="text-muted mt-3">Nessuna asta trovata per "<strong>@searchQuery</strong>"</p>
|
|
<button class="btn btn-outline-secondary" @onclick="ClearSearch">
|
|
<i class="bi bi-x-circle me-2"></i>Cancella Ricerca
|
|
</button>
|
|
</div>
|
|
}
|
|
else if (auctions.Count == 0)
|
|
{
|
|
<div class="text-center py-5 animate-fade-in">
|
|
<i class="bi bi-inbox text-muted" style="font-size: 4rem;"></i>
|
|
<p class="text-muted mt-3">Nessuna asta trovata in questa categoria</p>
|
|
<button class="btn btn-primary" @onclick="LoadAuctions">
|
|
<i class="bi bi-arrow-clockwise me-2"></i>Ricarica
|
|
</button>
|
|
</div>
|
|
}
|
|
else
|
|
{
|
|
<!-- Auctions Grid -->
|
|
<div class="auction-grid animate-fade-in">
|
|
@foreach (var auction in filteredAuctions)
|
|
{
|
|
<div class="auction-card @(auction.IsMonitored ? "monitored" : "") @(auction.IsSold ? "sold" : "")">
|
|
<!-- Image -->
|
|
<div class="auction-image">
|
|
@if (!string.IsNullOrEmpty(auction.ImageUrl))
|
|
{
|
|
<img src="@auction.ImageUrl" alt="@auction.Name" loading="lazy" />
|
|
}
|
|
else
|
|
{
|
|
<div class="placeholder-image">
|
|
<i class="bi bi-image"></i>
|
|
</div>
|
|
}
|
|
|
|
<!-- Badges -->
|
|
<div class="auction-badges">
|
|
@if (auction.IsCreditAuction)
|
|
{
|
|
<span class="badge bg-warning text-dark">
|
|
<i class="bi bi-coin"></i> @auction.CreditValue
|
|
</span>
|
|
}
|
|
@if (auction.IsManualOnly)
|
|
{
|
|
<span class="badge bg-info">
|
|
<i class="bi bi-hand-index"></i> Manuale
|
|
</span>
|
|
}
|
|
@if (auction.IsTurbo)
|
|
{
|
|
<span class="badge bg-danger">
|
|
<i class="bi bi-lightning"></i> @auction.TimerFrequency s
|
|
</span>
|
|
}
|
|
</div>
|
|
|
|
@if (auction.IsSold)
|
|
{
|
|
<div class="sold-overlay">
|
|
<span>VENDUTO</span>
|
|
</div>
|
|
}
|
|
|
|
@if (auction.IsMonitored)
|
|
{
|
|
<div class="monitored-badge">
|
|
<i class="bi bi-check-circle-fill"></i>
|
|
</div>
|
|
}
|
|
</div>
|
|
|
|
<!-- Info -->
|
|
<div class="auction-info">
|
|
<h6 class="auction-name" title="@auction.Name">@auction.Name</h6>
|
|
|
|
<div class="auction-price">
|
|
<span class="current-price">@auction.CurrentPrice.ToString("0.00") €</span>
|
|
@if (auction.BuyNowPrice > 0)
|
|
{
|
|
<span class="buynow-price text-muted">
|
|
<small>Compra: @auction.BuyNowPrice.ToString("0.00") €</small>
|
|
</span>
|
|
}
|
|
</div>
|
|
|
|
<div class="auction-bidder">
|
|
<i class="bi bi-person-fill text-muted me-1"></i>
|
|
<span>@(string.IsNullOrEmpty(auction.LastBidder) ? "—" : auction.LastBidder)</span>
|
|
</div>
|
|
|
|
<div class="auction-timer @(auction.RemainingSeconds <= 3 ? "urgent" : "")">
|
|
<i class="bi bi-clock me-1"></i>
|
|
@auction.TimerDisplay
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Actions -->
|
|
<div class="auction-actions">
|
|
<div class="d-flex gap-1 mb-1">
|
|
<button class="btn btn-outline-secondary btn-sm flex-grow-1"
|
|
@onclick="() => CopyAuctionLink(auction)"
|
|
title="Copia link">
|
|
<i class="bi bi-clipboard"></i>
|
|
</button>
|
|
<button class="btn btn-outline-secondary btn-sm flex-grow-1"
|
|
@onclick="() => OpenAuctionInNewTab(auction)"
|
|
title="Apri in nuova scheda">
|
|
<i class="bi bi-box-arrow-up-right"></i>
|
|
</button>
|
|
</div>
|
|
@if (auction.IsMonitored)
|
|
{
|
|
<button class="btn btn-warning btn-sm w-100" @onclick="() => RemoveFromMonitor(auction)">
|
|
<i class="bi bi-dash-lg me-1"></i>Togli dal Monitor
|
|
</button>
|
|
}
|
|
else
|
|
{
|
|
<button class="btn btn-primary btn-sm w-100" @onclick="() => AddToMonitor(auction)">
|
|
<i class="bi bi-plus-lg me-1"></i>Aggiungi al Monitor
|
|
</button>
|
|
}
|
|
</div>
|
|
</div>
|
|
}
|
|
</div>
|
|
|
|
<!-- Load More -->
|
|
@if (canLoadMore)
|
|
{
|
|
<div class="text-center mt-4">
|
|
<button class="btn btn-outline-primary btn-lg" @onclick="LoadMoreAuctions" disabled="@isLoadingMore">
|
|
@if (isLoadingMore)
|
|
{
|
|
<span class="spinner-border spinner-border-sm me-2"></span>
|
|
}
|
|
else
|
|
{
|
|
<i class="bi bi-plus-circle me-2"></i>
|
|
}
|
|
Carica Altre Aste
|
|
</button>
|
|
</div>
|
|
}
|
|
}
|
|
</div>
|
|
|
|
@code {
|
|
private List<BidooCategoryInfo> categories = new();
|
|
private List<BidooBrowserAuction> auctions = new();
|
|
private List<BidooBrowserAuction> filteredAuctions = new();
|
|
private int selectedCategoryIndex = 0;
|
|
private int currentPage = 0;
|
|
|
|
private bool isLoading = false;
|
|
private bool isLoadingMore = false;
|
|
private bool canLoadMore = true;
|
|
private string? errorMessage = null;
|
|
|
|
// ? NUOVO: Ricerca
|
|
private string searchQuery = "";
|
|
|
|
private System.Threading.Timer? stateUpdateTimer;
|
|
private CancellationTokenSource? cts;
|
|
private bool isUpdatingInBackground = false;
|
|
|
|
protected override async Task OnInitializedAsync()
|
|
{
|
|
await LoadCategories();
|
|
|
|
if (categories.Count > 0)
|
|
{
|
|
await LoadAuctions();
|
|
}
|
|
|
|
// Auto-update states every 500ms for real-time price updates
|
|
stateUpdateTimer = new System.Threading.Timer(async _ =>
|
|
{
|
|
if (auctions.Count > 0 && !isUpdatingInBackground)
|
|
{
|
|
await UpdateAuctionStatesBackground();
|
|
}
|
|
}, null, TimeSpan.FromMilliseconds(500), TimeSpan.FromMilliseconds(500));
|
|
}
|
|
|
|
private async Task LoadCategories()
|
|
{
|
|
try
|
|
{
|
|
categories = await BrowserService.GetCategoriesAsync();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Console.WriteLine($"[Browser] Error loading categories: {ex.Message}");
|
|
errorMessage = "Errore nel caricamento delle categorie";
|
|
}
|
|
}
|
|
|
|
private async Task OnCategoryChanged()
|
|
{
|
|
currentPage = 0;
|
|
canLoadMore = true;
|
|
auctions.Clear();
|
|
await LoadAuctions();
|
|
}
|
|
|
|
private async Task LoadAuctions()
|
|
{
|
|
if (categories.Count == 0 || selectedCategoryIndex < 0 || selectedCategoryIndex >= categories.Count)
|
|
return;
|
|
|
|
isLoading = true;
|
|
errorMessage = null;
|
|
cts?.Cancel();
|
|
cts = new CancellationTokenSource();
|
|
|
|
try
|
|
{
|
|
var category = categories[selectedCategoryIndex];
|
|
var newAuctions = await BrowserService.GetAuctionsAsync(category, currentPage, cts.Token);
|
|
|
|
auctions = newAuctions;
|
|
canLoadMore = newAuctions.Count >= 20; // Assume pagination at 20
|
|
|
|
// Mark already monitored auctions
|
|
UpdateMonitoredStatus();
|
|
|
|
// Get initial states
|
|
if (auctions.Count > 0)
|
|
{
|
|
await BrowserService.UpdateAuctionStatesAsync(auctions, cts.Token);
|
|
}
|
|
|
|
// ? NUOVO: Applica filtro ricerca
|
|
ApplySearchFilter();
|
|
}
|
|
catch (OperationCanceledException)
|
|
{
|
|
// Ignore cancellation
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Console.WriteLine($"[Browser] Error loading auctions: {ex.Message}");
|
|
errorMessage = "Errore nel caricamento delle aste";
|
|
}
|
|
finally
|
|
{
|
|
isLoading = false;
|
|
StateHasChanged();
|
|
}
|
|
}
|
|
|
|
// ? NUOVO: Metodo per applicare il filtro di ricerca
|
|
private void ApplySearchFilter()
|
|
{
|
|
if (string.IsNullOrWhiteSpace(searchQuery))
|
|
{
|
|
filteredAuctions = auctions.ToList();
|
|
return;
|
|
}
|
|
|
|
var query = searchQuery.ToLowerInvariant().Trim();
|
|
|
|
filteredAuctions = auctions.Where(a =>
|
|
// Cerca nel nome
|
|
a.Name.ToLowerInvariant().Contains(query) ||
|
|
// Cerca nel prezzo corrente
|
|
a.CurrentPrice.ToString("F2").Contains(query) ||
|
|
// Cerca nel prezzo buy-now
|
|
(a.BuyNowPrice > 0 && a.BuyNowPrice.ToString("F2").Contains(query)) ||
|
|
// Cerca nel nome dell'ultimo puntatore
|
|
(!string.IsNullOrEmpty(a.LastBidder) && a.LastBidder.ToLowerInvariant().Contains(query)) ||
|
|
// Cerca nell'ID asta
|
|
a.AuctionId.Contains(query)
|
|
).ToList();
|
|
}
|
|
|
|
// ? NUOVO: Callback quando cambia la ricerca
|
|
private void OnSearchChanged()
|
|
{
|
|
ApplySearchFilter();
|
|
StateHasChanged();
|
|
}
|
|
|
|
// ? NUOVO: Pulisce la ricerca
|
|
private void ClearSearch()
|
|
{
|
|
searchQuery = "";
|
|
ApplySearchFilter();
|
|
StateHasChanged();
|
|
}
|
|
|
|
private async Task LoadMoreAuctions()
|
|
{
|
|
if (categories.Count == 0 || selectedCategoryIndex < 0 || auctions.Count == 0)
|
|
return;
|
|
|
|
isLoadingMore = true;
|
|
cts?.Cancel();
|
|
cts = new CancellationTokenSource();
|
|
|
|
try
|
|
{
|
|
var category = categories[selectedCategoryIndex];
|
|
var existingIds = auctions.Select(a => a.AuctionId).ToList();
|
|
|
|
// Usa GetMoreAuctionsAsync che evita duplicati
|
|
var newAuctions = await BrowserService.GetMoreAuctionsAsync(category, existingIds, cts.Token);
|
|
|
|
if (newAuctions.Count == 0)
|
|
{
|
|
canLoadMore = false;
|
|
}
|
|
else
|
|
{
|
|
auctions.AddRange(newAuctions);
|
|
UpdateMonitoredStatus();
|
|
|
|
// Aggiorna stati delle nuove aste
|
|
await BrowserService.UpdateAuctionStatesAsync(newAuctions, cts.Token);
|
|
|
|
// ? NUOVO: Riapplica filtro dopo caricamento
|
|
ApplySearchFilter();
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Console.WriteLine($"[Browser] Error loading more auctions: {ex.Message}");
|
|
}
|
|
finally
|
|
{
|
|
isLoadingMore = false;
|
|
StateHasChanged();
|
|
}
|
|
}
|
|
|
|
private async Task UpdateAuctionStatesBackground()
|
|
{
|
|
if (isUpdatingInBackground) return;
|
|
|
|
isUpdatingInBackground = true;
|
|
try
|
|
{
|
|
await BrowserService.UpdateAuctionStatesAsync(auctions);
|
|
UpdateMonitoredStatus();
|
|
await InvokeAsync(StateHasChanged);
|
|
}
|
|
catch
|
|
{
|
|
// Ignore background errors
|
|
}
|
|
finally
|
|
{
|
|
isUpdatingInBackground = false;
|
|
}
|
|
}
|
|
|
|
private async Task RefreshAll()
|
|
{
|
|
await LoadCategories();
|
|
currentPage = 0;
|
|
canLoadMore = true;
|
|
auctions.Clear();
|
|
await LoadAuctions();
|
|
}
|
|
|
|
private void ClearAllAuctions()
|
|
{
|
|
// Cancella le aste e ferma il timer
|
|
cts?.Cancel();
|
|
auctions.Clear();
|
|
filteredAuctions.Clear();
|
|
StateHasChanged();
|
|
}
|
|
|
|
private void UpdateMonitoredStatus()
|
|
{
|
|
var monitoredIds = AppState.Auctions.Select(a => a.AuctionId).ToHashSet();
|
|
foreach (var auction in auctions)
|
|
{
|
|
auction.IsMonitored = monitoredIds.Contains(auction.AuctionId);
|
|
}
|
|
}
|
|
|
|
private void AddToMonitor(BidooBrowserAuction browserAuction)
|
|
{
|
|
if (browserAuction.IsMonitored) return;
|
|
|
|
// ?? Carica impostazioni di default
|
|
var settings = AutoBidder.Utilities.SettingsManager.Load();
|
|
|
|
var auctionInfo = new AuctionInfo
|
|
{
|
|
AuctionId = browserAuction.AuctionId,
|
|
Name = browserAuction.Name,
|
|
OriginalUrl = browserAuction.Url,
|
|
BuyNowPrice = (double)browserAuction.BuyNowPrice,
|
|
|
|
// ?? FIX: Applica valori dalle impostazioni
|
|
BidBeforeDeadlineMs = settings.DefaultBidBeforeDeadlineMs,
|
|
CheckAuctionOpenBeforeBid = settings.DefaultCheckAuctionOpenBeforeBid,
|
|
MinPrice = settings.DefaultMinPrice,
|
|
MaxPrice = settings.DefaultMaxPrice,
|
|
MinResets = settings.DefaultMinResets,
|
|
MaxResets = settings.DefaultMaxResets,
|
|
|
|
IsActive = true,
|
|
IsPaused = true, // Start paused
|
|
AddedAt = DateTime.UtcNow
|
|
};
|
|
|
|
AppState.AddAuction(auctionInfo);
|
|
|
|
// ?? FIX CRITICO: Registra l'asta nel monitor!
|
|
AuctionMonitor.AddAuction(auctionInfo);
|
|
|
|
browserAuction.IsMonitored = true;
|
|
|
|
// Save to disk
|
|
AutoBidder.Utilities.PersistenceManager.SaveAuctions(AppState.Auctions.ToList());
|
|
|
|
// ?? FIX CRITICO: Avvia il monitor se non è già attivo
|
|
if (!AppState.IsMonitoringActive)
|
|
{
|
|
AuctionMonitor.Start();
|
|
AppState.IsMonitoringActive = true;
|
|
Console.WriteLine($"[AuctionBrowser] Monitor auto-started for paused auction: {auctionInfo.Name}");
|
|
}
|
|
|
|
StateHasChanged();
|
|
}
|
|
|
|
private void RemoveFromMonitor(BidooBrowserAuction browserAuction)
|
|
{
|
|
if (!browserAuction.IsMonitored) return;
|
|
|
|
// Trova l'asta nel monitor
|
|
var auctionToRemove = AppState.Auctions.FirstOrDefault(a => a.AuctionId == browserAuction.AuctionId);
|
|
if (auctionToRemove != null)
|
|
{
|
|
AppState.RemoveAuction(auctionToRemove);
|
|
browserAuction.IsMonitored = false;
|
|
|
|
// Save to disk
|
|
AutoBidder.Utilities.PersistenceManager.SaveAuctions(AppState.Auctions.ToList());
|
|
}
|
|
|
|
StateHasChanged();
|
|
}
|
|
|
|
private async Task CopyAuctionLink(BidooBrowserAuction auction)
|
|
{
|
|
try
|
|
{
|
|
await JSRuntime.InvokeVoidAsync("navigator.clipboard.writeText", auction.Url);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Console.WriteLine($"[Browser] Error copying link: {ex.Message}");
|
|
}
|
|
}
|
|
|
|
private async Task OpenAuctionInNewTab(BidooBrowserAuction auction)
|
|
{
|
|
try
|
|
{
|
|
await JSRuntime.InvokeVoidAsync("window.open", auction.Url, "_blank");
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Console.WriteLine($"[Browser] Error opening link: {ex.Message}");
|
|
}
|
|
}
|
|
|
|
public void Dispose()
|
|
{
|
|
stateUpdateTimer?.Dispose();
|
|
cts?.Cancel();
|
|
cts?.Dispose();
|
|
}
|
|
}
|