Files
Mimante/Mimante/Core/MainWindow.FreeBids.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

421 lines
17 KiB
C#

using System;
using System.Diagnostics;
using System.Threading;
using System.Threading.Tasks;
using System.Windows;
using AutoBidder.Models;
using AutoBidder.Services;
using AutoBidder.Utilities;
namespace AutoBidder
{
/// <summary>
/// Scheda "Puntate": riscatto automatico delle ricompense, contatori e registro.
///
/// <para>Il lavoro vero sta in <see cref="FreeBidsAutoClaimService"/> e
/// <see cref="BidooFreeBidsClaimer"/>: qui c'è solo il collegamento fra quei servizi e
/// l'interfaccia, compreso il passaggio sul thread dell'interfaccia — il ciclo gira su
/// un thread di lavoro e non deve toccare i controlli.</para>
///
/// <para>Resta invece manuale la parte consegnata per notifica push (OneSignal) e per
/// email: quei premi sono agganciati all'abbonamento del browser, non all'account, e
/// non esiste una chiamata da fare con il cookie di sessione.</para>
/// </summary>
public partial class MainWindow
{
private FreeBidsAutoClaimService? _freeBidsService;
private BidooFreeBidsClaimer? _freeBidsClaimer;
private BidooPromoRedeemer? _promoRedeemer;
/// <summary>Esito dell'ultima raccolta, da mostrare nella riga di stato.</summary>
private string? _lastHarvestOutcome;
/// <summary>
/// Indirizzi presi dai riferimenti configurati, non scritti qui: sono gli stessi che
/// usa il riscatto automatico, e devono restare gli stessi anche quando cambiano —
/// altrimenti «Apri su Bidoo» porterebbe a una pagina diversa da quella che
/// l'applicazione sta davvero interrogando.
/// </summary>
private static FreeBidsSiteConfig FreeBidsConfig => FreeBidsConfigStore.Current;
private static string ChestsUrl => FreeBidsConfig.Url(FreeBidsConfig.Endpoints.Rewards);
private void StartFreeBidsService()
{
try
{
// Alla prima esecuzione il file dei riferimenti non c'è: crearlo subito è il
// modo per farne sapere l'esistenza a chi dovrà correggerlo.
if (FreeBidsConfigStore.EnsureFileExists())
Log($"[PUNTATE] Riferimenti del riscatto creati in {FreeBidsConfigStore.FilePath}", LogLevel.Info);
// Il file va letto davvero prima di poter dire se ha qualcosa che non va:
// un problema non ancora incontrato non è un problema che si può segnalare.
FreeBidsConfigStore.Reload();
if (FreeBidsConfigStore.LastProblem is { } problem)
Log($"[PUNTATE] {problem}", LogLevel.Warning);
var transport = _auctionMonitor.GetApiClient().Transport;
_freeBidsClaimer = new BidooFreeBidsClaimer(transport)
{
Diagnostic = msg => Dispatcher.BeginInvoke(() => Log($"[PUNTATE] {msg}", LogLevel.Info))
};
_promoRedeemer = new BidooPromoRedeemer(transport)
{
Diagnostic = msg => Dispatcher.BeginInvoke(() =>
{
Log($"[PUNTATE] {msg}", LogLevel.Info);
FreeBids.AppendActivity(msg);
})
};
_freeBidsService = new FreeBidsAutoClaimService(_freeBidsClaimer, ReadBalanceAsync, _promoRedeemer);
_freeBidsService.OnLog += (message, isProblem) => Dispatcher.BeginInvoke(() =>
{
Log($"[PUNTATE] {message}", isProblem ? LogLevel.Warning : LogLevel.Success);
FreeBids.AppendActivity(message, isProblem);
});
_freeBidsService.OnCycleCompleted += _ => Dispatcher.BeginInvoke(() =>
{
if (_freeBidsService?.LastHarvest is { } harvest) _lastHarvestOutcome = harvest.Message;
RefreshFreeBidsPanel();
});
var settings = SettingsManager.Load();
FreeBids.SetOptions(settings.FreeBidsAutoClaimEnabled, settings.FreeBidsCheckMinutes);
if (settings.FreeBidsAutoClaimEnabled) _freeBidsService.Start();
RefreshFreeBidsPanel();
}
catch (Exception ex)
{
// Il riscatto è un accessorio: se non parte, il resto dell'applicazione
// deve continuare a funzionare senza accorgersene.
Log($"[PUNTATE] Riscatto automatico non avviato: {ex.Message}", LogLevel.Error);
}
}
/// <summary>
/// Rilegge il saldo dal sito. Serve al servizio per misurare quante puntate ha
/// portato davvero un riscatto, invece di fidarsi del messaggio di risposta.
/// </summary>
private async Task<int?> ReadBalanceAsync(CancellationToken ct)
{
await _auctionMonitor.UpdateUserInfoAsync().ConfigureAwait(false);
return _auctionMonitor.GetSession()?.RemainingBids;
}
private void RefreshFreeBidsPanel()
{
try
{
var session = _auctionMonitor.GetSession();
FreeBids.SetBalance(session?.RemainingBids);
FreeBids.SetSchedule(
running: _freeBidsService?.IsRunning == true,
lastCheck: _freeBidsService?.LastCheckAt,
nextCheck: _freeBidsService?.NextCheckAt);
FreeBids.SetCounters(FreeBidsStats.Read());
FreeBids.SetConfigPath(FreeBidsConfigStore.FilePath, FreeBidsConfigStore.LastProblem);
FreeBids.SetHarvestStatus(
FreeBidsConfig.PromoHarvest.SourceUrl,
ClaimedPromoStore.ClaimedCount,
_lastHarvestOutcome);
}
catch { }
}
// ── Eventi della scheda ──────────────────────────────────────────
private async void FreeBids_RefreshBalanceClicked(object sender, RoutedEventArgs e)
{
try
{
await _auctionMonitor.UpdateUserInfoAsync();
RefreshFreeBidsPanel();
UpdateRemainingBidsDisplay();
Log("[PUNTATE] Saldo aggiornato", LogLevel.Info);
}
catch (Exception ex)
{
Log($"[PUNTATE] Aggiornamento saldo non riuscito: {ex.Message}", LogLevel.Error);
}
}
private async void FreeBids_CheckNowClicked(object sender, RoutedEventArgs e)
{
if (_freeBidsService == null) return;
try
{
FreeBids.AppendActivity("Controllo richiesto a mano…");
var result = await _freeBidsService.CheckNowAsync();
if (result.IsSuccess && result.ClaimedCount == 0)
FreeBids.AppendActivity(result.Message);
RefreshFreeBidsPanel();
UpdateRemainingBidsDisplay();
}
catch (Exception ex)
{
Log($"[PUNTATE] Controllo non riuscito: {ex.Message}", LogLevel.Error);
FreeBids.AppendActivity($"controllo non riuscito: {ex.Message}", isProblem: true);
}
}
/// <summary>Apre la pagina nel browser predefinito di Windows, fuori dall'applicazione.</summary>
private void FreeBids_OpenExternalClicked(object sender, RoutedEventArgs e)
{
try
{
Process.Start(new ProcessStartInfo(ChestsUrl) { UseShellExecute = true });
}
catch (Exception ex)
{
Log($"[PUNTATE] Apertura nel browser esterno non riuscita: {ex.Message}", LogLevel.Error);
}
}
private void FreeBids_OpenInternalClicked(object sender, RoutedEventArgs e)
=> OpenInEmbeddedBrowser(ChestsUrl);
private void FreeBids_OpenVouchersClicked(object sender, RoutedEventArgs e)
=> OpenInEmbeddedBrowser(FreeBidsConfig.Url(FreeBidsConfig.Endpoints.Vouchers));
private void FreeBids_OpenBuyBidsClicked(object sender, RoutedEventArgs e)
=> OpenInEmbeddedBrowser(FreeBidsConfig.Url(FreeBidsConfig.Endpoints.BuyBids));
/// <summary>
/// Riscatta il codice o il collegamento incollato dall'utente.
///
/// <para>Il saldo viene riletto in ogni caso: il messaggio del sito dice quello che
/// vuole, la differenza di puntate sul conto no.</para>
/// </summary>
private async void FreeBids_ClaimPromoClicked(object sender, RoutedEventArgs e)
{
if (_freeBidsClaimer == null) return;
var code = FreeBids.PromoCode;
if (code.Length == 0)
{
FreeBids.AppendActivity("manca il codice o il collegamento da riscattare", isProblem: true);
return;
}
try
{
FreeBids.AppendActivity("Riscatto del codice richiesto a mano…");
var before = _auctionMonitor.GetSession()?.RemainingBids;
var result = await _freeBidsClaimer.ClaimPromoAsync(code);
if (!result.IsSuccess)
{
FreeBids.AppendActivity(result.Message, isProblem: true);
Log($"[PUNTATE] Riscatto codice non riuscito: {result.Message}", LogLevel.Warning);
return;
}
await _auctionMonitor.UpdateUserInfoAsync();
var after = _auctionMonitor.GetSession()?.RemainingBids;
var gained = before.HasValue && after.HasValue ? Math.Max(0, after.Value - before.Value) : 0;
if (gained > 0) FreeBidsStats.RecordClaim(claimedCount: 1, bidsGained: gained);
FreeBids.AppendActivity(gained > 0
? $"codice riscattato: +{gained} puntate"
: $"codice accettato ({result.Message}), saldo invariato");
FreeBids.ClearPromoCode();
RefreshFreeBidsPanel();
UpdateRemainingBidsDisplay();
}
catch (Exception ex)
{
Log($"[PUNTATE] Riscatto codice non riuscito: {ex.Message}", LogLevel.Error);
FreeBids.AppendActivity($"riscatto non riuscito: {ex.Message}", isProblem: true);
}
}
/// <summary>
/// Esegue subito una raccolta dei collegamenti pubblicati, senza aspettare il giro.
///
/// <para>Le puntate ottenute si misurano sul saldo prima e dopo: la pagina di Bidoo
/// risponde allo stesso modo per un codice appena riscosso e per uno già usato.</para>
/// </summary>
private async void FreeBids_HarvestClicked(object sender, RoutedEventArgs e)
{
if (_promoRedeemer == null) return;
try
{
FreeBids.AppendActivity("Raccolta dei collegamenti richiesta a mano…");
var before = _auctionMonitor.GetSession()?.RemainingBids;
var report = await _promoRedeemer.HarvestAndRedeemAsync();
_lastHarvestOutcome = report.Message;
if (!report.SourceReadable)
{
FreeBids.AppendActivity(report.Message, isProblem: true);
Log($"[PUNTATE] Raccolta non riuscita: {report.Message}", LogLevel.Warning);
RefreshFreeBidsPanel();
return;
}
if (report.Claimed > 0)
{
await _auctionMonitor.UpdateUserInfoAsync();
var after = _auctionMonitor.GetSession()?.RemainingBids;
var gained = before.HasValue && after.HasValue ? Math.Max(0, after.Value - before.Value) : 0;
FreeBidsStats.RecordClaim(report.Claimed, gained);
FreeBids.AppendActivity(gained > 0
? $"{report.Message}: +{gained} puntate"
: $"{report.Message} (saldo invariato)");
UpdateRemainingBidsDisplay();
}
else
{
FreeBids.AppendActivity(report.Message);
}
RefreshFreeBidsPanel();
}
catch (Exception ex)
{
Log($"[PUNTATE] Raccolta non riuscita: {ex.Message}", LogLevel.Error);
FreeBids.AppendActivity($"raccolta non riuscita: {ex.Message}", isProblem: true);
}
}
private void FreeBids_OpenPromoSourceClicked(object sender, RoutedEventArgs e)
=> OpenInEmbeddedBrowser(FreeBidsConfig.PromoHarvest.SourceUrl);
/// <summary>
/// Azzera la memoria dei collegamenti già aperti. Serve dopo un cambio di account:
/// i codici presi da un utente non dicono nulla su quelli disponibili per un altro.
/// </summary>
private void FreeBids_ForgetPromosClicked(object sender, RoutedEventArgs e)
{
var answer = MessageBox.Show(this,
$"Vuoi dimenticare i {ClaimedPromoStore.ClaimedCount} codici già presi?\n\n" +
"Alla raccolta successiva verranno riaperti tutti i collegamenti pubblicati, " +
"compresi quelli già usati: serve dopo un cambio di account, non nell'uso normale.",
"Puntate", MessageBoxButton.YesNo, MessageBoxImage.Warning);
if (answer != MessageBoxResult.Yes) return;
ClaimedPromoStore.Clear();
_lastHarvestOutcome = null;
RefreshFreeBidsPanel();
Log("[PUNTATE] Memoria dei collegamenti azzerata", LogLevel.Info);
}
/// <summary>
/// Apre <c>site-config.json</c> con l'editor predefinito. Il file viene creato se
/// manca: aprire il vuoto non direbbe a nessuno quali valori si possono cambiare.
/// </summary>
private void FreeBids_OpenConfigClicked(object sender, RoutedEventArgs e)
{
try
{
FreeBidsConfigStore.EnsureFileExists();
Process.Start(new ProcessStartInfo(FreeBidsConfigStore.FilePath) { UseShellExecute = true });
FreeBids.AppendActivity(
"Riferimenti aperti: salva il file e premi «Controlla adesso», " +
"le modifiche valgono subito.");
}
catch (Exception ex)
{
Log($"[PUNTATE] Apertura dei riferimenti non riuscita: {ex.Message}", LogLevel.Error);
}
}
private void FreeBids_ResetCountersClicked(object sender, RoutedEventArgs e)
{
var answer = MessageBox.Show(this,
"Vuoi azzerare i contatori dei riscatti?\n\n" +
"Si perde il totale storico delle puntate raccolte. Le puntate sul conto non vengono toccate.",
"Puntate", MessageBoxButton.YesNo, MessageBoxImage.Warning);
if (answer != MessageBoxResult.Yes) return;
FreeBidsStats.Clear();
RefreshFreeBidsPanel();
Log("[PUNTATE] Contatori azzerati", LogLevel.Info);
}
private void FreeBids_OptionsChanged(object sender, RoutedEventArgs e)
{
try
{
var settings = SettingsManager.Load();
settings.FreeBidsAutoClaimEnabled = FreeBids.AutoClaimEnabled;
settings.FreeBidsCheckMinutes = FreeBids.CheckMinutes;
SettingsManager.Save(settings);
if (_freeBidsService != null)
{
if (settings.FreeBidsAutoClaimEnabled && !_freeBidsService.IsRunning)
{
_freeBidsService.Start();
Log($"[PUNTATE] Riscatto automatico attivo, ogni {settings.FreeBidsCheckMinutes} minuti", LogLevel.Success);
}
else if (!settings.FreeBidsAutoClaimEnabled && _freeBidsService.IsRunning)
{
_freeBidsService.Stop();
Log("[PUNTATE] Riscatto automatico spento", LogLevel.Info);
}
}
RefreshFreeBidsPanel();
}
catch (Exception ex)
{
Log($"[PUNTATE] Impostazioni non salvate: {ex.Message}", LogLevel.Error);
}
}
/// <summary>
/// Apre una pagina nel browser integrato. Deve essere quello, non il browser di
/// sistema: è lì che la sessione è attiva ed è lì che vanno concesse le notifiche.
/// </summary>
private void OpenInEmbeddedBrowser(string url)
{
try
{
TabBrowser.IsChecked = true;
Browser.ShowBrowser();
Browser.EmbeddedWebView?.CoreWebView2?.Navigate(url);
Browser.BrowserAddress.Text = url;
}
catch (Exception ex)
{
Log($"[PUNTATE] Apertura pagina non riuscita: {ex.Message}", LogLevel.Error);
}
}
}
}