Files
Mimante/Mimante/Pages/AuctionBrowser.razor
Alberto Balbo 77eb9943d0 Gestione avanzata database e rimozione MaxClicks
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.
2026-01-24 01:30:49 +01:00

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();
}
}