- 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.
421 lines
17 KiB
C#
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);
|
|
}
|
|
}
|
|
}
|
|
}
|