Aggiornamento alla versione 3.0.0

- Rimosse funzionalità legacy legate a WebView2.
- Introdotto uno stile globale per i pulsanti.
- Semplificata l'interfaccia con gestione tramite griglia unica.
- Aggiunti comandi per avviare, mettere in pausa e fermare aste.
- Introdotta gestione manuale dei cookie tramite dialog.
- Aggiunti dialog per configurare sessione e aggiungere aste.
- Migliorata la persistenza con salvataggio sicuro (DPAPI).
- Rifattorizzate statistiche per utilizzare `BidHistory` e `BidderStats`.
- Ottimizzato il polling per ridurre il carico di sistema.
- Aggiornata esportazione CSV con dati più dettagliati.
- Introdotti nuovi modelli dati per utente e banner aste.
- Rimossi file di test manuale e codice obsoleto.
- Aggiornata documentazione per riflettere le modifiche.
- Aggiunta nuova icona dell'applicazione.
- Migliorata la sicurezza eliminando il salvataggio in chiaro dei cookie.
This commit is contained in:
Alberto Balbo
2025-10-28 12:45:08 +01:00
parent fef7b909e7
commit 717dc44b3b
32 changed files with 1039 additions and 1471 deletions

View File

@@ -5,9 +5,25 @@
StartupUri="MainWindow.xaml">
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="Converters/Converters.xaml" />
</ResourceDictionary.MergedDictionaries>
<!-- Stile pulsanti globale -->
<Style x:Key="SmallButtonStyle" TargetType="Button">
<Setter Property="Foreground" Value="White" />
<Setter Property="FontWeight" Value="SemiBold" />
<Setter Property="FontSize" Value="12" />
<Setter Property="BorderThickness" Value="0" />
<Setter Property="Cursor" Value="Hand" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Border Background="{TemplateBinding Background}"
CornerRadius="12"
Padding="{TemplateBinding Padding}">
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center" />
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>
</Application.Resources>
</Application>

View File

@@ -1,2 +0,0 @@
[Binary ICO placeholder removed in this environment]

View File

@@ -11,13 +11,22 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Web.WebView2" Version="1.0.3530-prerelease" />
<Compile Remove=".github\**" />
<EmbeddedResource Remove=".github\**" />
<None Remove=".github\**" />
<Page Remove=".github\**" />
</ItemGroup>
<ItemGroup>
<None Remove="Icon\favicon.ico" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.26100.6584" />
</ItemGroup>
<ItemGroup>
<!-- Ensure the application icon is included as a WPF Resource so Icon="Assets/app.ico" resolves -->
<Resource Include="Assets\app.ico" />
<Resource Include="Icon\favicon.ico" />
</ItemGroup>
</Project>

View File

@@ -1,93 +0,0 @@
<Window x:Class="AutoBidder.BrowserWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:wv2="clr-namespace:Microsoft.Web.WebView2.Wpf;assembly=Microsoft.Web.WebView2.Wpf"
Title="Browser - Bidoo"
Height="800" Width="1200"
Background="#101219" Foreground="#E6EDF3">
<Window.Resources>
<Style x:Key="AddressBarStyle" TargetType="TextBox">
<Setter Property="Background" Value="#0B1220" />
<Setter Property="Foreground" Value="#E6EDF3" />
<Setter Property="BorderBrush" Value="#263143" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="Padding" Value="8,6" />
<Setter Property="FontSize" Value="13" />
</Style>
</Window.Resources>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<!-- Barra navigazione -->
<Grid Grid.Row="0" Margin="12,12,12,6" Background="#0B1015">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Button x:Name="BackButton" Grid.Column="0" Content="?" Click="BackButton_Click"
Background="#374151" Foreground="White" Padding="12,8" Margin="8,8,6,8"
BorderThickness="0" FontWeight="Bold" FontSize="14" MinWidth="40" Height="38" IsEnabled="False">
<Button.Template>
<ControlTemplate TargetType="Button">
<Border Background="{TemplateBinding Background}" CornerRadius="6">
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center" />
</Border>
</ControlTemplate>
</Button.Template>
</Button>
<Button x:Name="RefreshButton" Grid.Column="1" Content="?" Click="RefreshButton_Click"
Background="#0EA5E9" Foreground="White" Padding="12,8" Margin="0,8,8,8"
BorderThickness="0" FontWeight="Bold" FontSize="16" MinWidth="40" Height="38">
<Button.Template>
<ControlTemplate TargetType="Button">
<Border Background="{TemplateBinding Background}" CornerRadius="6">
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center" />
</Border>
</ControlTemplate>
</Button.Template>
</Button>
<TextBox x:Name="AddressBar" Grid.Column="2" Style="{StaticResource AddressBarStyle}"
Text="https://it.bidoo.com" KeyDown="AddressBar_KeyDown" Margin="0,8,8,8" />
<Button x:Name="NavigateButton" Grid.Column="3" Content="Vai" Click="NavigateButton_Click"
Background="#16A34A" Foreground="White" Padding="20,8" Margin="0,8,8,8"
BorderThickness="0" FontWeight="Bold" FontSize="14" Height="38">
<Button.Template>
<ControlTemplate TargetType="Button">
<Border Background="{TemplateBinding Background}" CornerRadius="6">
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center" />
</Border>
</ControlTemplate>
</Button.Template>
</Button>
<Button x:Name="AddCurrentPageButton" Grid.Column="4" Content="? Aggiungi Asta" Click="AddCurrentPageButton_Click"
Background="#8B5CF6" Foreground="White" Padding="16,8" Margin="0,8,8,8"
BorderThickness="0" FontWeight="SemiBold" FontSize="13" Height="38">
<Button.Template>
<ControlTemplate TargetType="Button">
<Border Background="{TemplateBinding Background}" CornerRadius="6">
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center" />
</Border>
</ControlTemplate>
</Button.Template>
</Button>
</Grid>
<!-- WebView -->
<Border Grid.Row="1" Margin="12,0,12,12" CornerRadius="8" Background="#0B1015">
<wv2:WebView2 x:Name="webView" Source="https://it.bidoo.com" Margin="2" />
</Border>
</Grid>
</Window>

View File

@@ -1,204 +0,0 @@
using System;
using System.Windows;
using System.Windows.Input;
using Microsoft.Web.WebView2.Core;
namespace AutoBidder
{
/// <summary>
/// Finestra browser separata per navigazione Bidoo
/// </summary>
public partial class BrowserWindow : Window
{
public event Action<string>? OnAddAuction;
public BrowserWindow()
{
InitializeComponent();
Loaded += BrowserWindow_Loaded;
}
private async void BrowserWindow_Loaded(object sender, RoutedEventArgs e)
{
try
{
if (webView.CoreWebView2 == null)
{
await webView.EnsureCoreWebView2Async();
}
webView.NavigationCompleted += WebView_NavigationCompleted;
webView.NavigationStarting += WebView_NavigationStarting;
// Context menu per aggiungere asta
if (webView.CoreWebView2 != null)
{
webView.CoreWebView2.ContextMenuRequested += CoreWebView2_ContextMenuRequested;
webView.CoreWebView2.Navigate("https://it.bidoo.com");
}
}
catch (Exception ex)
{
MessageBox.Show($"Errore inizializzazione: {ex.Message}", "Errore", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
private void CoreWebView2_ContextMenuRequested(object? sender, CoreWebView2ContextMenuRequestedEventArgs e)
{
try
{
var currentUrl = webView.CoreWebView2?.Source ?? "";
if (IsValidAuctionUrl(currentUrl) && webView.CoreWebView2 != null)
{
// Aggiungi voce menu contestuale
var menuItem = webView.CoreWebView2.Environment.CreateContextMenuItem(
"Aggiungi asta al monitoraggio",
null,
CoreWebView2ContextMenuItemKind.Command);
menuItem.CustomItemSelected += (s, args) =>
{
OnAddAuction?.Invoke(currentUrl);
};
e.MenuItems.Insert(0, menuItem);
}
}
catch { }
}
private void WebView_NavigationStarting(object? sender, CoreWebView2NavigationStartingEventArgs e)
{
if (!string.IsNullOrEmpty(e.Uri) && !IsBidooUrl(e.Uri))
{
e.Cancel = true;
MessageBox.Show("Solo domini Bidoo consentiti!", "Navigazione Bloccata", MessageBoxButton.OK, MessageBoxImage.Warning);
}
}
private void WebView_NavigationCompleted(object? sender, CoreWebView2NavigationCompletedEventArgs e)
{
try
{
Dispatcher.BeginInvoke(() =>
{
BackButton.IsEnabled = webView.CoreWebView2?.CanGoBack ?? false;
var url = webView.CoreWebView2?.Source ?? "";
if (!string.IsNullOrEmpty(url))
{
AddressBar.Text = url;
}
});
}
catch { }
}
private bool IsBidooUrl(string url)
{
if (string.IsNullOrWhiteSpace(url)) return false;
try
{
var uri = new Uri(url);
var host = uri.Host.ToLowerInvariant();
return host.Contains("bidoo.com") || host.Contains("bidoo.it") ||
host.Contains("bidoo.fr") || host.Contains("bidoo.es") ||
host.Contains("bidoo.de");
}
catch
{
return false;
}
}
private bool IsValidAuctionUrl(string url)
{
if (!IsBidooUrl(url)) return false;
try
{
var uri = new Uri(url);
return uri.AbsolutePath.Contains("/asta/") || uri.Query.Contains("?a=");
}
catch
{
return false;
}
}
private void BackButton_Click(object sender, RoutedEventArgs e)
{
webView.CoreWebView2?.GoBack();
}
private void RefreshButton_Click(object sender, RoutedEventArgs e)
{
webView.CoreWebView2?.Reload();
}
private void NavigateButton_Click(object sender, RoutedEventArgs e)
{
NavigateToAddress();
}
private void AddressBar_KeyDown(object sender, KeyEventArgs e)
{
if (e.Key == Key.Enter)
{
NavigateToAddress();
}
}
private void NavigateToAddress()
{
try
{
var url = AddressBar.Text.Trim();
if (string.IsNullOrEmpty(url)) return;
if (!url.StartsWith("http"))
{
url = "https://" + url;
}
if (!IsBidooUrl(url))
{
MessageBox.Show("Solo URL Bidoo consentiti!", "URL Non Valido", MessageBoxButton.OK, MessageBoxImage.Warning);
return;
}
webView.CoreWebView2?.Navigate(url);
}
catch (Exception ex)
{
MessageBox.Show($"Errore navigazione: {ex.Message}", "Errore", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
private void AddCurrentPageButton_Click(object sender, RoutedEventArgs e)
{
try
{
var currentUrl = webView.CoreWebView2?.Source ?? "";
if (string.IsNullOrEmpty(currentUrl) || !IsValidAuctionUrl(currentUrl))
{
MessageBox.Show("La pagina corrente non <20> un'asta valida!", "URL Non Valido", MessageBoxButton.OK, MessageBoxImage.Warning);
return;
}
OnAddAuction?.Invoke(currentUrl);
MessageBox.Show("Asta aggiunta al monitoraggio!", "Successo", MessageBoxButton.OK, MessageBoxImage.Information);
}
catch (Exception ex)
{
MessageBox.Show($"Errore: {ex.Message}", "Errore", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
}
}

View File

@@ -1,23 +0,0 @@
using System;
using System.Globalization;
using System.Windows.Data;
namespace AutoBidder.Converters
{
public class AndNotPausedConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
if (values.Length == 2 && values[0] is bool isActive && values[1] is bool isPaused)
{
return isActive && !isPaused;
}
return false;
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
}

View File

@@ -1,46 +0,0 @@
using System;
using System.Globalization;
using System.Windows.Data;
namespace AutoBidder.Converters
{
// Converte bool in Opacity: true -> 0.5 (disabilitato), false -> 1.0 (abilitato)
public class BoolToOpacityConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
bool inverse = parameter?.ToString() == "Inverse";
if (value is bool b)
{
if (inverse)
return b ? 0.5 : 1.0;
else
return b ? 1.0 : 0.5;
}
return 1.0;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
// Converte (IsActive, IsPaused) in Opacity per il pulsante Pausa
public class PauseButtonOpacityConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
if (values.Length == 2 && values[0] is bool isActive && values[1] is bool isPaused)
{
return (isActive && !isPaused) ? 1.0 : 0.5;
}
return 0.5;
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
}

View File

@@ -1,10 +0,0 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:AutoBidder.Converters">
<local:InverseBoolConverter x:Key="InverseBoolConverter"/>
<local:AndNotPausedConverter x:Key="AndNotPausedConverter"/>
<local:StartResumeConverter x:Key="StartResumeConverter"/>
<local:BoolToOpacityConverter x:Key="BoolToOpacityConverter"/>
<local:PauseButtonOpacityConverter x:Key="PauseButtonOpacityConverter"/>
<local:StartButtonOpacityConverter x:Key="StartButtonOpacityConverter"/>
</ResourceDictionary>

View File

@@ -1,23 +0,0 @@
using System;
using System.Globalization;
using System.Windows.Data;
namespace AutoBidder.Converters
{
public class InverseBoolConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is bool b)
return !b;
return true;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is bool b)
return !b;
return true;
}
}
}

View File

@@ -1,24 +0,0 @@
using System;
using System.Globalization;
using System.Windows.Data;
namespace AutoBidder.Converters
{
public class StartButtonOpacityConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
if (values.Length == 2 && values[0] is bool isActive && values[1] is bool isPaused)
{
// Bright (1.0) when not active (can start) or when paused (can resume)
return (!isActive || isPaused) ? 1.0 : 0.5;
}
return 1.0;
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
}

View File

@@ -1,25 +0,0 @@
using System;
using System.Globalization;
using System.Windows.Data;
namespace AutoBidder.Converters
{
// Returns true when the Start/Resume button for a row should be enabled:
// Enabled when NOT active (stopped) OR when paused (to resume).
public class StartResumeConverter : IMultiValueConverter
{
public object Convert(object[] values, Type targetType, object parameter, CultureInfo culture)
{
if (values.Length >= 2 && values[0] is bool isActive && values[1] is bool isPaused)
{
return (!isActive) || isPaused;
}
return false;
}
public object[] ConvertBack(object value, Type[] targetTypes, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
}

View File

@@ -0,0 +1,28 @@
<Window x:Class="AutoBidder.Dialogs.AddAuctionSimpleDialog"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Aggiungi Asta" Height="220" Width="600"
Background="#0a0a0a" Foreground="#FFFFFF"
WindowStartupLocation="CenterOwner"
Icon="pack://application:,,,/Icon/favicon.ico"
ResizeMode="NoResize">
<Border Background="#1a1a1a" CornerRadius="8" Padding="16" Margin="8">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<TextBlock Text="Inserire URL dell'asta" Foreground="#CCCCCC" FontSize="14" Margin="0,0,0,10" />
<TextBox x:Name="AuctionUrlBox" Grid.Row="1" MinWidth="320" Margin="0,0,0,8"
Background="#181818" Foreground="#00CCFF" BorderBrush="#444" BorderThickness="1"
Padding="8" FontSize="13" ToolTip="Inserisci l'URL completo dell'asta" />
<StackPanel Grid.Row="2" Orientation="Horizontal" HorizontalAlignment="Right" Margin="0,8,0,0">
<Button x:Name="OkButton" Content="OK" Width="110" Margin="6" Padding="10,8"
Style="{StaticResource SmallButtonStyle}" Background="#00CC66" Foreground="White" Click="OkButton_Click" />
<Button x:Name="CancelButton" Content="Annulla" Width="110" Margin="6" Padding="10,8"
Style="{StaticResource SmallButtonStyle}" Background="#666" Foreground="White" Click="CancelButton_Click" />
</StackPanel>
</Grid>
</Border>
</Window>

View File

@@ -0,0 +1,33 @@
using System.Windows;
namespace AutoBidder.Dialogs
{
public partial class AddAuctionSimpleDialog : Window
{
public string AuctionId { get; private set; } = string.Empty;
public AddAuctionSimpleDialog()
{
InitializeComponent();
}
private void OkButton_Click(object sender, RoutedEventArgs e)
{
var text = AuctionUrlBox.Text?.Trim() ?? string.Empty;
if (string.IsNullOrWhiteSpace(text) || !text.StartsWith("http"))
{
MessageBox.Show("Inserisci un URL valido dell'asta.", "Errore", MessageBoxButton.OK, MessageBoxImage.Warning);
return;
}
AuctionId = text;
DialogResult = true;
Close();
}
private void CancelButton_Click(object sender, RoutedEventArgs e)
{
DialogResult = false;
Close();
}
}
}

View File

@@ -0,0 +1,41 @@
<Window x:Class="AutoBidder.Dialogs.SessionDialog"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Configura Sessione" Height="440" Width="700"
Background="#0a0a0a" Foreground="#FFFFFF"
WindowStartupLocation="CenterOwner"
Icon="pack://application:,,,/Icon/favicon.ico"
ResizeMode="NoResize">
<Border Background="#1a1a1a" CornerRadius="8" Padding="16" Margin="8">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<TextBlock Text="Inserisci Cookie di sessione" FontSize="16" FontWeight="SemiBold" Foreground="#00CC66" Margin="0,0,0,12" />
<TextBlock Grid.Row="1" Text="Cookie di sessione (copia dal menu delle Impostazioni/Applicazioni di Chrome)" Foreground="#CCCCCC" FontSize="13" />
<TextBlock Grid.Row="2" Foreground="#888" FontSize="12" TextWrapping="Wrap" Margin="0,4,0,10">
<Run Text="Come trovare i cookie di sessione in Chrome:" />
<LineBreak/>
<Run Text="1. Apri Chrome e premi F12 (Windows) o Cmd+Option+I (Mac) per aprire gli Strumenti per sviluppatori." />
<LineBreak/>
<Run Text="2. Vai alla scheda 'Application'." />
<LineBreak/>
<Run Text="3. Nel pannello a sinistra, espandi 'Storage' e seleziona 'Cookies'." />
<LineBreak/>
<Run Text="4. Scegli il sito desiderato e copia il valore del cookie di sessione." />
</TextBlock>
<TextBox x:Name="CookieBox" Grid.Row="3" MinWidth="320" MinHeight="120" MaxHeight="220" VerticalScrollBarVisibility="Auto"
Background="#181818" Foreground="#00CCFF" BorderBrush="#444" BorderThickness="1" Padding="8" FontSize="14" AcceptsReturn="True" TextWrapping="Wrap" />
<StackPanel Grid.Row="4" Orientation="Horizontal" HorizontalAlignment="Right" Margin="0,12,0,0">
<Button x:Name="OkButton" Content="OK" Width="110" Margin="6" Padding="10,8"
Style="{StaticResource SmallButtonStyle}" Background="#00CC66" Foreground="White" Click="OkButton_Click" />
<Button x:Name="CancelButton" Content="Annulla" Width="110" Margin="6" Padding="10,8"
Style="{StaticResource SmallButtonStyle}" Background="#666" Foreground="White" Click="CancelButton_Click" />
</StackPanel>
</Grid>
</Border>
</Window>

View File

@@ -0,0 +1,33 @@
using System.Windows;
namespace AutoBidder.Dialogs
{
public partial class SessionDialog : Window
{
public string AuthToken { get; private set; } = string.Empty;
public SessionDialog(string existingToken = "", string existingUsername = "")
{
InitializeComponent();
CookieBox.Text = existingToken ?? string.Empty;
}
private void OkButton_Click(object sender, RoutedEventArgs e)
{
AuthToken = CookieBox.Text?.Trim() ?? string.Empty;
if (string.IsNullOrEmpty(AuthToken))
{
MessageBox.Show("Cookie obbligatorio", "Errore", MessageBoxButton.OK, MessageBoxImage.Warning);
return;
}
DialogResult = true;
Close();
}
private void CancelButton_Click(object sender, RoutedEventArgs e)
{
DialogResult = false;
Close();
}
}
}

View File

@@ -1,247 +0,0 @@
using System.Windows;
using System.Windows.Controls;
namespace AutoBidder
{
/// <summary>
/// Dialog per inizializzare sessione Bidoo
/// Richiede: Auth Token + Username
/// </summary>
public class SessionDialog : Window
{
private readonly TextBox _tokenTextBox;
private readonly TextBox _usernameTextBox;
public string AuthToken => _tokenTextBox.Text.Trim();
public string Username => _usernameTextBox.Text.Trim();
public SessionDialog(string existingToken = "", string existingUsername = "")
{
Title = "Configura Sessione Bidoo";
Width = 680;
Height = 420;
WindowStartupLocation = WindowStartupLocation.CenterOwner;
ResizeMode = ResizeMode.NoResize;
Background = System.Windows.Media.Brushes.Black;
Foreground = System.Windows.Media.Brushes.White;
var grid = new Grid { Margin = new Thickness(20) };
grid.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto });
grid.RowDefinitions.Add(new RowDefinition { Height = new GridLength(10) });
grid.RowDefinitions.Add(new RowDefinition { Height = new GridLength(140) });
grid.RowDefinitions.Add(new RowDefinition { Height = new GridLength(15) });
grid.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto });
grid.RowDefinitions.Add(new RowDefinition { Height = new GridLength(10) });
grid.RowDefinitions.Add(new RowDefinition { Height = new GridLength(40) });
grid.RowDefinitions.Add(new RowDefinition { Height = new GridLength(20) });
grid.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto });
var label1 = new TextBlock
{
Text = "Cookie __stattrb (F12 > Application > Cookies > __stattrb):",
FontWeight = FontWeights.SemiBold,
Foreground = System.Windows.Media.Brushes.White,
TextWrapping = TextWrapping.Wrap
};
Grid.SetRow(label1, 0);
_tokenTextBox = new TextBox
{
Text = existingToken,
Padding = new Thickness(10),
TextWrapping = TextWrapping.Wrap,
AcceptsReturn = false,
VerticalScrollBarVisibility = ScrollBarVisibility.Auto,
VerticalContentAlignment = VerticalAlignment.Top,
Background = new System.Windows.Media.SolidColorBrush(System.Windows.Media.Color.FromRgb(15, 15, 15)),
Foreground = System.Windows.Media.Brushes.LightGray,
FontFamily = new System.Windows.Media.FontFamily("Consolas"),
FontSize = 11,
ToolTip = "Esempio: eyJVU0VSSUQiOiI2NzA3NjY0Ii..."
};
Grid.SetRow(_tokenTextBox, 2);
var label2 = new TextBlock
{
Text = "Username Bidoo:",
FontWeight = FontWeights.SemiBold,
Foreground = System.Windows.Media.Brushes.White
};
Grid.SetRow(label2, 4);
_usernameTextBox = new TextBox
{
Text = existingUsername,
Padding = new Thickness(10),
FontSize = 14,
VerticalContentAlignment = VerticalAlignment.Center,
Background = new System.Windows.Media.SolidColorBrush(System.Windows.Media.Color.FromRgb(15, 15, 15)),
Foreground = System.Windows.Media.Brushes.LightGray
};
Grid.SetRow(_usernameTextBox, 6);
var buttonPanel = new StackPanel
{
Orientation = Orientation.Horizontal,
HorizontalAlignment = HorizontalAlignment.Right
};
Grid.SetRow(buttonPanel, 8);
var okButton = new Button
{
Content = "Conferma",
Width = 120,
Height = 40,
Margin = new Thickness(0, 0, 10, 0),
Background = new System.Windows.Media.SolidColorBrush(System.Windows.Media.Color.FromRgb(0, 204, 102)),
Foreground = System.Windows.Media.Brushes.White,
FontWeight = FontWeights.Bold,
FontSize = 14
};
var cancelButton = new Button
{
Content = "Annulla",
Width = 120,
Height = 40,
Background = new System.Windows.Media.SolidColorBrush(System.Windows.Media.Color.FromRgb(204, 0, 0)),
Foreground = System.Windows.Media.Brushes.White,
FontWeight = FontWeights.Bold,
FontSize = 14
};
okButton.Click += (s, e) =>
{
if (string.IsNullOrWhiteSpace(AuthToken) || string.IsNullOrWhiteSpace(Username))
{
MessageBox.Show("Inserisci Token e Username!", "Errore", MessageBoxButton.OK, MessageBoxImage.Warning);
return;
}
DialogResult = true;
Close();
};
cancelButton.Click += (s, e) => { DialogResult = false; Close(); };
buttonPanel.Children.Add(okButton);
buttonPanel.Children.Add(cancelButton);
grid.Children.Add(label1);
grid.Children.Add(_tokenTextBox);
grid.Children.Add(label2);
grid.Children.Add(_usernameTextBox);
grid.Children.Add(buttonPanel);
Content = grid;
Loaded += (s, e) => _tokenTextBox.Focus();
}
}
/// <summary>
/// Dialog semplificato per aggiungere asta (ID o URL completo)
/// </summary>
public class AddAuctionSimpleDialog : Window
{
private readonly TextBox _auctionIdTextBox;
public string AuctionId => _auctionIdTextBox.Text.Trim();
public AddAuctionSimpleDialog()
{
Title = "Aggiungi Asta";
Width = 680;
Height = 280;
WindowStartupLocation = WindowStartupLocation.CenterOwner;
ResizeMode = ResizeMode.NoResize;
Background = System.Windows.Media.Brushes.Black;
Foreground = System.Windows.Media.Brushes.White;
var grid = new Grid { Margin = new Thickness(20) };
grid.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto });
grid.RowDefinitions.Add(new RowDefinition { Height = new GridLength(10) });
grid.RowDefinitions.Add(new RowDefinition { Height = new GridLength(50) });
grid.RowDefinitions.Add(new RowDefinition { Height = new GridLength(10) });
grid.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto });
grid.RowDefinitions.Add(new RowDefinition { Height = new GridLength(20) });
grid.RowDefinitions.Add(new RowDefinition { Height = new GridLength(50) });
var label = new TextBlock
{
Text = "URL Asta o ID:",
FontWeight = FontWeights.SemiBold,
Foreground = System.Windows.Media.Brushes.White
};
Grid.SetRow(label, 0);
_auctionIdTextBox = new TextBox
{
Text = "",
Padding = new Thickness(10),
FontSize = 13,
Background = new System.Windows.Media.SolidColorBrush(System.Windows.Media.Color.FromRgb(15, 15, 15)),
Foreground = System.Windows.Media.Brushes.LightGray,
VerticalContentAlignment = VerticalAlignment.Center
};
Grid.SetRow(_auctionIdTextBox, 2);
var hintLabel = new TextBlock
{
Text = "Esempio: https://it.bidoo.com/auction.php?a=Galaxy_S25_Ultra_256GB_81204324\nOppure: 81204324",
FontSize = 11,
Foreground = System.Windows.Media.Brushes.Gray,
TextWrapping = TextWrapping.Wrap
};
Grid.SetRow(hintLabel, 4);
var buttonPanel = new StackPanel
{
Orientation = Orientation.Horizontal,
HorizontalAlignment = HorizontalAlignment.Right
};
Grid.SetRow(buttonPanel, 6);
var okButton = new Button
{
Content = "Aggiungi",
Width = 120,
Height = 40,
Margin = new Thickness(0, 0, 10, 0),
Background = new System.Windows.Media.SolidColorBrush(System.Windows.Media.Color.FromRgb(0, 204, 102)),
Foreground = System.Windows.Media.Brushes.White,
FontWeight = FontWeights.Bold,
FontSize = 14
};
var cancelButton = new Button
{
Content = "Annulla",
Width = 120,
Height = 40,
Background = new System.Windows.Media.SolidColorBrush(System.Windows.Media.Color.FromRgb(204, 0, 0)),
Foreground = System.Windows.Media.Brushes.White,
FontWeight = FontWeights.Bold,
FontSize = 14
};
okButton.Click += (s, e) => { DialogResult = true; Close(); };
cancelButton.Click += (s, e) => { DialogResult = false; Close(); };
_auctionIdTextBox.KeyDown += (s, e) =>
{
if (e.Key == System.Windows.Input.Key.Enter)
{
DialogResult = true;
Close();
}
};
buttonPanel.Children.Add(okButton);
buttonPanel.Children.Add(cancelButton);
grid.Children.Add(label);
grid.Children.Add(_auctionIdTextBox);
grid.Children.Add(hintLabel);
grid.Children.Add(buttonPanel);
Content = grid;
Loaded += (s, e) => _auctionIdTextBox.Focus();
}
}
}

BIN
Mimante/Icon/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

View File

@@ -1,11 +1,10 @@
<Window x:Class="AutoBidder.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:AutoBidder.Converters"
Title="AutoBidder v3.0" Height="800" Width="1400"
Background="#0a0a0a" Foreground="#FFFFFF"
WindowStartupLocation="CenterScreen">
WindowStartupLocation="CenterScreen"
Icon="pack://application:,,,/Icon/favicon.ico">
<Window.Resources>
<!-- Stile pulsanti stile vecchia versione -->
<Style x:Key="MainButtonStyle" TargetType="Button">
@@ -27,24 +26,127 @@
</Setter>
</Style>
<Style x:Key="SmallButtonStyle" TargetType="Button">
<Setter Property="Foreground" Value="White" />
<Setter Property="FontWeight" Value="SemiBold" />
<Setter Property="FontSize" Value="12" />
<Setter Property="BorderThickness" Value="0" />
<Setter Property="Cursor" Value="Hand" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="Button">
<Border Background="{TemplateBinding Background}"
CornerRadius="6"
Padding="{TemplateBinding Padding}">
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center" />
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
<!-- Grid action button styles (enable/opacity based on auction state) -->
<Style x:Key="GridStartButtonStyle" TargetType="Button" BasedOn="{StaticResource SmallButtonStyle}">
<Setter Property="IsEnabled" Value="True" />
<Setter Property="Opacity" Value="1" />
<Style.Triggers>
<MultiDataTrigger>
<MultiDataTrigger.Conditions>
<Condition Binding="{Binding IsActive}" Value="True" />
<Condition Binding="{Binding IsPaused}" Value="False" />
</MultiDataTrigger.Conditions>
<Setter Property="IsEnabled" Value="False" />
<Setter Property="Opacity" Value="0.5" />
</MultiDataTrigger>
<Trigger Property="IsEnabled" Value="False">
<Trigger.EnterActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="Opacity" To="0.5" Duration="0:0:0.15" />
</Storyboard>
</BeginStoryboard>
</Trigger.EnterActions>
<Trigger.ExitActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="Opacity" To="1" Duration="0:0:0.15" />
</Storyboard>
</BeginStoryboard>
</Trigger.ExitActions>
</Trigger>
</Style.Triggers>
</Style>
<Style x:Key="GridPauseButtonStyle" TargetType="Button" BasedOn="{StaticResource SmallButtonStyle}">
<Setter Property="IsEnabled" Value="False" />
<Setter Property="Opacity" Value="0.5" />
<Style.Triggers>
<MultiDataTrigger>
<MultiDataTrigger.Conditions>
<Condition Binding="{Binding IsActive}" Value="True" />
<Condition Binding="{Binding IsPaused}" Value="False" />
</MultiDataTrigger.Conditions>
<Setter Property="IsEnabled" Value="True" />
<Setter Property="Opacity" Value="1" />
</MultiDataTrigger>
<Trigger Property="IsEnabled" Value="False">
<Trigger.EnterActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="Opacity" To="0.5" Duration="0:0:0.15" />
</Storyboard>
</BeginStoryboard>
</Trigger.EnterActions>
<Trigger.ExitActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="Opacity" To="1" Duration="0:0:0.15" />
</Storyboard>
</BeginStoryboard>
</Trigger.ExitActions>
</Trigger>
</Style.Triggers>
</Style>
<Style x:Key="GridStopButtonStyle" TargetType="Button" BasedOn="{StaticResource SmallButtonStyle}">
<Setter Property="IsEnabled" Value="False" />
<Setter Property="Opacity" Value="0.5" />
<Style.Triggers>
<DataTrigger Binding="{Binding IsActive}" Value="True">
<Setter Property="IsEnabled" Value="True" />
<Setter Property="Opacity" Value="1" />
</DataTrigger>
<Trigger Property="IsEnabled" Value="False">
<Trigger.EnterActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="Opacity" To="0.5" Duration="0:0:0.15" />
</Storyboard>
</BeginStoryboard>
</Trigger.EnterActions>
<Trigger.ExitActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="Opacity" To="1" Duration="0:0:0.15" />
</Storyboard>
</BeginStoryboard>
</Trigger.ExitActions>
</Trigger>
</Style.Triggers>
</Style>
<Style x:Key="GridManualBidButtonStyle" TargetType="Button" BasedOn="{StaticResource SmallButtonStyle}">
<Setter Property="IsEnabled" Value="False" />
<Setter Property="Opacity" Value="0.5" />
<Style.Triggers>
<MultiDataTrigger>
<MultiDataTrigger.Conditions>
<Condition Binding="{Binding IsActive}" Value="True" />
<Condition Binding="{Binding IsPaused}" Value="False" />
</MultiDataTrigger.Conditions>
<Setter Property="IsEnabled" Value="True" />
<Setter Property="Opacity" Value="1" />
</MultiDataTrigger>
<Trigger Property="IsEnabled" Value="False">
<Trigger.EnterActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="Opacity" To="0.5" Duration="0:0:0.15" />
</Storyboard>
</BeginStoryboard>
</Trigger.EnterActions>
<Trigger.ExitActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="Opacity" To="1" Duration="0:0:0.15" />
</Storyboard>
</BeginStoryboard>
</Trigger.ExitActions>
</Trigger>
</Style.Triggers>
</Style>
</Window.Resources>
<Grid Margin="12" Background="#0a0a0a">
@@ -60,42 +162,39 @@
<Border Grid.Row="0" Background="#1a1a1a" Padding="12" CornerRadius="8" Margin="0,0,0,12">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<!-- Info Utente -->
<StackPanel Grid.Column="0" Orientation="Horizontal" VerticalAlignment="Center" Margin="0,0,20,0">
<TextBlock Text="User" FontSize="16" Margin="0,0,8,0" Foreground="#00CC66" FontWeight="Bold" />
<TextBlock x:Name="UsernameText" Text="Non configurato" FontSize="13" FontWeight="Bold" Foreground="#00CC66" VerticalAlignment="Center" />
<TextBlock Text=" | Bids " FontSize="13" Foreground="#666" Margin="12,0,8,0" />
<TextBlock x:Name="RemainingBidsText" Text="--" FontSize="13" FontWeight="Bold" Foreground="#0099FF" VerticalAlignment="Center" />
<!-- Sinistra: Utente, Puntate, Aste da confermare -->
<StackPanel Grid.Column="0" VerticalAlignment="Center" HorizontalAlignment="Left">
<StackPanel Orientation="Horizontal" Margin="0,0,0,2">
<TextBlock Text="Utente: " FontSize="14" Foreground="#00CC66" FontWeight="Bold" />
<TextBlock x:Name="UsernameText" Text="Non configurato" FontSize="14" FontWeight="Bold" Foreground="#00CC66" Width="160" TextTrimming="CharacterEllipsis" />
</StackPanel>
<StackPanel Orientation="Horizontal" Margin="0,0,0,2">
<TextBlock Text="Puntate: " FontSize="14" Foreground="#666" FontWeight="Bold" />
<TextBlock x:Name="RemainingBidsText" Text="--" FontSize="14" FontWeight="Bold" Foreground="#0099FF" Width="80" TextTrimming="CharacterEllipsis" />
</StackPanel>
<StackPanel Orientation="Horizontal">
<TextBlock Text="Aste vinte da confermare: " FontSize="14" Foreground="#FF9933" FontWeight="Bold" />
<TextBlock x:Name="BannerAsteDaRiscattare" Text="--" FontSize="14" Foreground="#FF9933" FontWeight="Bold" Width="40" TextTrimming="CharacterEllipsis" />
</StackPanel>
</StackPanel>
<!-- Pulsanti Centrali -->
<StackPanel Grid.Column="1" Orientation="Horizontal" HorizontalAlignment="Center" VerticalAlignment="Center">
<!-- Destra: Pulsanti globali -->
<StackPanel Grid.Column="1" Orientation="Horizontal" HorizontalAlignment="Right" VerticalAlignment="Center">
<Button x:Name="ConfigSessionButton" Content="Configura" Click="ConfigSessionButton_Click"
Style="{StaticResource SmallButtonStyle}"
Background="#8B5CF6" Padding="20,10" Margin="0,0,6,0" Height="40" MinWidth="110" />
<Button x:Name="StartButton" Content="Avvia Tutti" Click="StartButton_Click" IsEnabled="False"
<Button x:Name="StartButton" Content="Avvia Tutti" Command="{Binding StartAllCommand}" IsEnabled="False"
Style="{StaticResource SmallButtonStyle}"
Background="#00CC66" Padding="20,10" Margin="0,0,6,0" Height="40" MinWidth="110" />
<Button x:Name="PauseAllButton" Content="Pausa Tutti" Click="PauseAllButton_Click" IsEnabled="True"
<Button x:Name="PauseAllButton" Content="Pausa Tutti" Command="{Binding PauseAllCommand}" IsEnabled="True"
Style="{StaticResource SmallButtonStyle}"
Background="#FF9933" Padding="20,10" Margin="0,0,6,0" Height="40" MinWidth="110" />
<Button x:Name="StopButton" Content="Ferma Tutti" Click="StopButton_Click" IsEnabled="False"
<Button x:Name="StopButton" Content="Ferma Tutti" Command="{Binding StopAllCommand}" IsEnabled="False"
Style="{StaticResource SmallButtonStyle}"
Background="#CC0000" Padding="20,10" Opacity="0.5" Height="40" MinWidth="110" />
</StackPanel>
<!-- Contatore Aste -->
<Border Grid.Column="2" Background="#0f0f0f" CornerRadius="6" Padding="16,8" VerticalAlignment="Center">
<StackPanel Orientation="Horizontal">
<TextBlock Text="Aste:" FontSize="12" Foreground="#999" Margin="0,0,8,0" VerticalAlignment="Center" />
<TextBlock x:Name="TotalAuctionsText" Text="0" FontWeight="Bold" FontSize="18" Foreground="#00CC66" VerticalAlignment="Center" />
</StackPanel>
</Border>
</Grid>
</Border>
@@ -120,7 +219,7 @@
<!-- Header con titolo e pulsanti -->
<Grid Grid.Row="0" Margin="0,0,0,12">
<TextBlock Text="Aste Monitorate" FontSize="14" FontWeight="Bold" Foreground="#00CC66" VerticalAlignment="Center" />
<TextBlock x:Name="MonitorateTitle" Text="Aste monitorate: 0" FontSize="14" FontWeight="Bold" Foreground="#00CC66" VerticalAlignment="Center" />
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right">
<Button x:Name="AddUrlButton" Content="+ Aggiungi" Click="AddUrlButton_Click"
Style="{StaticResource SmallButtonStyle}"
@@ -182,6 +281,7 @@
</DataGrid.Resources>
<DataGrid.Columns>
<DataGridTextColumn Header="Asta" Binding="{Binding Name, Mode=OneWay}" Width="2*" IsReadOnly="True" />
<DataGridTextColumn Header="Latenza (ms)" Binding="{Binding AuctionInfo.PollingLatencyMs, Mode=OneWay}" Width="80" IsReadOnly="True" />
<DataGridTextColumn Header="Stato" Binding="{Binding StatusDisplay, Mode=OneWay}" Width="90" IsReadOnly="True" />
<DataGridTextColumn Header="Timer" Binding="{Binding TimerDisplay, Mode=OneWay}" Width="70" IsReadOnly="True" />
<DataGridTextColumn Header="Prezzo" Binding="{Binding PriceDisplay, Mode=OneWay}" Width="85" IsReadOnly="True" />
@@ -192,38 +292,10 @@
<DataGridTemplateColumn.CellTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center">
<Button Content="Avvia" CommandParameter="{Binding}" Click="GridStartAuction_Click" Style="{StaticResource SmallButtonStyle}" Background="#00CC66" Padding="6,2" MinWidth="40" Margin="0,0,4,0">
<Button.Opacity>
<MultiBinding Converter="{StaticResource StartButtonOpacityConverter}">
<Binding Path="IsActive" />
<Binding Path="IsPaused" />
</MultiBinding>
</Button.Opacity>
<Button.IsEnabled>
<MultiBinding Converter="{StaticResource StartResumeConverter}">
<Binding Path="IsActive" />
<Binding Path="IsPaused" />
</MultiBinding>
</Button.IsEnabled>
</Button>
<Button Content="Pausa" CommandParameter="{Binding}" Click="GridPauseAuction_Click" Style="{StaticResource SmallButtonStyle}" Background="#FF9933" Padding="6,2" MinWidth="40" Margin="0,0,4,0">
<Button.IsEnabled>
<MultiBinding Converter="{StaticResource AndNotPausedConverter}">
<Binding Path="IsActive" />
<Binding Path="IsPaused" />
</MultiBinding>
</Button.IsEnabled>
<Button.Opacity>
<MultiBinding Converter="{StaticResource PauseButtonOpacityConverter}">
<Binding Path="IsActive" />
<Binding Path="IsPaused" />
</MultiBinding>
</Button.Opacity>
</Button>
<Button Content="Ferma" CommandParameter="{Binding}" Click="GridStopAuction_Click" Style="{StaticResource SmallButtonStyle}" Background="#CC0000" Padding="6,2" MinWidth="40" Margin="0,0,4,0"
IsEnabled="{Binding IsActive}"
Opacity="{Binding IsActive, Converter={StaticResource BoolToOpacityConverter}}" />
<Button Content="Punta" CommandParameter="{Binding}" Click="GridManualBid_Click" Style="{StaticResource SmallButtonStyle}" Background="#8B5CF6" Padding="6,2" MinWidth="40" />
<Button Content="Avvia" Command="{Binding DataContext.GridStartCommand, RelativeSource={RelativeSource AncestorType=Window}}" CommandParameter="{Binding}" Style="{StaticResource GridStartButtonStyle}" Background="#00CC66" Padding="6,2" MinWidth="40" Margin="0,0,4,0" />
<Button Content="Pausa" Command="{Binding DataContext.GridPauseCommand, RelativeSource={RelativeSource AncestorType=Window}}" CommandParameter="{Binding}" Style="{StaticResource GridPauseButtonStyle}" Background="#FF9933" Padding="6,2" MinWidth="40" Margin="0,0,4,0" />
<Button Content="Ferma" Command="{Binding DataContext.GridStopCommand, RelativeSource={RelativeSource AncestorType=Window}}" CommandParameter="{Binding}" Style="{StaticResource GridStopButtonStyle}" Background="#CC0000" Padding="6,2" MinWidth="40" Margin="0,0,4,0" />
<Button Content="Punta" Command="{Binding DataContext.GridBidCommand, RelativeSource={RelativeSource AncestorType=Window}}" CommandParameter="{Binding}" Style="{StaticResource GridManualBidButtonStyle}" Background="#8B5CF6" Padding="6,2" MinWidth="40" />
</StackPanel>
</DataTemplate>
</DataGridTemplateColumn.CellTemplate>
@@ -295,7 +367,7 @@
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<TextBox x:Name="SelectedAuctionUrl" Text="" IsReadOnly="True" MinWidth="220" Margin="0,0,8,0" VerticalAlignment="Center" Background="#181818" Foreground="#00CCFF" BorderBrush="#333" BorderThickness="1" FontSize="11" Grid.Column="0" HorizontalAlignment="Stretch" />
<Button Content="Apri" x:Name="OpenAuctionButton" Click="OpenAuctionButton_Click" Style="{StaticResource SmallButtonStyle}" Background="#0099FF" Padding="10,6" Margin="0,0,4,0" Height="28" MinWidth="60" FontSize="11" Grid.Column="1" />
<Button Content="Apri" x:Name="OpenAuctionButton" Click="GridOpenAuction_Click" Style="{StaticResource SmallButtonStyle}" Background="#0099FF" Padding="10,6" Margin="0,0,4,0" Height="28" MinWidth="60" FontSize="11" Grid.Column="1" />
<Button Content="Copia" x:Name="CopyAuctionUrlButton" Click="CopyAuctionUrlButton_Click" Style="{StaticResource SmallButtonStyle}" Background="#666" Padding="10,6" Height="28" MinWidth="60" FontSize="11" Grid.Column="2" />
</Grid>
@@ -341,13 +413,9 @@
<UniformGrid Columns="2" Margin="0,0,0,12">
<StackPanel Margin="0,0,4,0">
<TextBlock Text="Max Clicks (0=inf)" FontSize="10" Margin="0,0,0,4" Foreground="#999" />
<TextBox x:Name="SelectedMaxClicks" Text="0" TextChanged="SelectedMaxClicks_TextChanged"
Background="#1a1a1a" Foreground="#FFF" Padding="8" FontSize="12" BorderBrush="#444" BorderThickness="1" />
</StackPanel>
<StackPanel Margin="4,0,0,0">
<TextBlock Text="(Opzionale)" FontSize="10" Margin="0,0,0,4" Foreground="#999" />
<TextBlock Text="Limita il numero di puntate del bot per questa asta" FontSize="10" Foreground="#AAA" TextWrapping="Wrap" />
<TextBox x:Name="SelectedMaxClicks" Text="{Binding MaxClicks, Mode=TwoWay}" Background="#1a1a1a" Foreground="#FFF" Padding="8" FontSize="12" BorderBrush="#444" BorderThickness="1" />
</StackPanel>
<StackPanel />
</UniformGrid>
</StackPanel>
</ScrollViewer>

View File

@@ -1,17 +1,13 @@
using System;
using System.Collections.ObjectModel;
using System.Linq;
using System.Collections.ObjectModel;
using System.Net;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Threading.Tasks;
using Microsoft.Web.WebView2.Core;
using AutoBidder.Models;
using AutoBidder.Services;
using AutoBidder.ViewModels;
using AutoBidder.Utilities;
using Microsoft.Win32;
using AutoBidder.Dialogs;
namespace AutoBidder
{
@@ -24,11 +20,23 @@ namespace AutoBidder
// SERVIZI CORE
private readonly AuctionMonitor _auctionMonitor;
private readonly ObservableCollection<AuctionViewModel> _auctionViewModels = new();
// UI State
private AuctionViewModel? _selectedAuction;
private bool _isAutomationActive = false;
private BrowserWindow? _browserWindow;
// Commands
public RelayCommand? StartAllCommand { get; private set; }
public RelayCommand? StopAllCommand { get; private set; }
public RelayCommand? PauseAllCommand { get; private set; }
// Grid item commands (parameter = AuctionViewModel)
public RelayCommand? GridStartCommand { get; private set; }
public RelayCommand? GridPauseCommand { get; private set; }
public RelayCommand? GridStopCommand { get; private set; }
public RelayCommand? GridBidCommand { get; private set; }
private System.Windows.Threading.DispatcherTimer _userBannerTimer;
private System.Windows.Threading.DispatcherTimer _userHtmlTimer;
public MainWindow()
{
@@ -37,6 +45,18 @@ namespace AutoBidder
// Inizializza servizi
_auctionMonitor = new AuctionMonitor();
// Commands
StartAllCommand = new RelayCommand(_ => ExecuteStartAll());
StopAllCommand = new RelayCommand(_ => ExecuteStopAll());
PauseAllCommand = new RelayCommand(_ => ExecutePauseAll());
GridStartCommand = new RelayCommand(param => ExecuteGridStart(param as ViewModels.AuctionViewModel));
GridPauseCommand = new RelayCommand(param => ExecuteGridPause(param as ViewModels.AuctionViewModel));
GridStopCommand = new RelayCommand(param => ExecuteGridStop(param as ViewModels.AuctionViewModel));
GridBidCommand = new RelayCommand(async param => await ExecuteGridBidAsync(param as ViewModels.AuctionViewModel));
this.DataContext = this;
// Event handlers
_auctionMonitor.OnAuctionUpdated += AuctionMonitor_OnAuctionUpdated;
_auctionMonitor.OnBidExecuted += AuctionMonitor_OnBidExecuted;
@@ -53,7 +73,68 @@ namespace AutoBidder
UpdateGlobalControlButtons();
Log("[OK] AutoBidder v4.0 avviato (API-based)");
Log("[INFO] Usa 'Configura Sessione' per inserire PHPSESSID");
Log("[INFO] Usa 'Configura Sessione' per inserire cookie dal menu Applicazioni di Chrome");
// Timer per aggiornamento banner utente ogni minuto
_userBannerTimer = new System.Windows.Threading.DispatcherTimer();
_userBannerTimer.Interval = TimeSpan.FromMinutes(1);
_userBannerTimer.Tick += UserBannerTimer_Tick;
_userBannerTimer.Start();
_ = UpdateUserBannerInfoAsync();
// Timer per aggiornamento dati utente da HTML ogni 3 minuti
_userHtmlTimer = new System.Windows.Threading.DispatcherTimer();
_userHtmlTimer.Interval = TimeSpan.FromMinutes(3);
_userHtmlTimer.Tick += UserHtmlTimer_Tick;
_userHtmlTimer.Start();
_ = UpdateUserHtmlInfoAsync();
}
// Command implementations
private void ExecuteStartAll()
{
StartButton_Click(null, null);
}
private void ExecuteStopAll()
{
StopButton_Click(null, null);
}
private void ExecutePauseAll()
{
PauseAllButton_Click(null, null);
}
private void ExecuteGridStart(ViewModels.AuctionViewModel? vm)
{
if (vm == null) return;
vm.IsActive = true; vm.IsPaused = false; Log($"[START] Asta avviata: {vm.Name}"); UpdateGlobalControlButtons();
}
private void ExecuteGridPause(ViewModels.AuctionViewModel? vm)
{
if (vm == null) return;
vm.IsPaused = true; Log($"[PAUSA] Asta in pausa: {vm.Name}"); UpdateGlobalControlButtons();
}
private void ExecuteGridStop(ViewModels.AuctionViewModel? vm)
{
if (vm == null) return;
vm.IsActive = false; Log($"[STOP] Asta fermata: {vm.Name}"); UpdateGlobalControlButtons();
}
private async Task ExecuteGridBidAsync(ViewModels.AuctionViewModel? vm)
{
if (vm == null) return;
try
{
Log($"[BID] Puntata manuale richiesta su: {vm.Name}");
var result = await _auctionMonitor.PlaceManualBidAsync(vm.AuctionInfo);
if (result.Success)
Log($"[OK] Puntata manuale su {vm.Name}: {result.LatencyMs}ms");
else
Log($"[FAIL] Puntata manuale su {vm.Name}: {result.Error}");
}
catch (Exception ex)
{
Log($"[ERRORE] Puntata manuale: {ex.Message}");
}
}
private void AuctionMonitor_OnAuctionUpdated(AuctionState state)
@@ -116,8 +197,7 @@ namespace AutoBidder
{
// Carica sessione esistente (se presente)
var currentSession = _auctionMonitor.GetSession();
string existingToken = "";
string existingUsername = "";
string existingToken = string.Empty;
if (currentSession != null)
{
@@ -130,11 +210,9 @@ namespace AutoBidder
{
existingToken = currentSession.AuthToken;
}
existingUsername = currentSession.Username ?? "";
}
var dialog = new SessionDialog(existingToken, existingUsername);
var dialog = new SessionDialog(existingToken);
if (dialog.ShowDialog() == true)
{
try
@@ -142,32 +220,43 @@ namespace AutoBidder
// Usa InitializeSessionWithCookie invece di InitializeSession
// perché il token è __stattrb (non __stattr)
var cookieString = $"__stattrb={dialog.AuthToken}";
_auctionMonitor.InitializeSessionWithCookie(cookieString, dialog.Username);
_auctionMonitor.InitializeSessionWithCookie(cookieString, ""); // Username non più passato
UsernameText.Text = dialog.Username ?? string.Empty;
StartButton.IsEnabled = true;
// Salva sessione in modo sicuro (crittografata)
var session = _auctionMonitor.GetSession();
SessionManager.SaveSession(session);
Log($"Sessione configurata per: {dialog.Username}");
Log($"Sessione configurata");
Log($"Cookie salvato in modo sicuro");
// Aggiorna info utente
Task.Run(() =>
// Aggiorna info utente (nome e puntate) tramite chiamata automatica
Task.Run(async () =>
{
_auctionMonitor.UpdateUserInfoAsync().GetAwaiter().GetResult();
var updatedSession = _auctionMonitor.GetSession();
Dispatcher.Invoke(() =>
var userData = await _auctionMonitor.GetUserDataAsync();
if (userData != null)
{
RemainingBidsText.Text = updatedSession.RemainingBids.ToString();
});
Dispatcher.Invoke(() =>
{
SetUserBanner(userData.Username, userData.RemainingBids);
Log($"[OK] Utente: {userData.Username}, Puntate residue: {userData.RemainingBids}");
});
}
else
{
Dispatcher.Invoke(() =>
{
UsernameText.Text = "";
RemainingBidsText.Text = "0";
Log($"[WARN] Impossibile ricavare dati utente");
});
}
});
}
catch (Exception ex)
{
Log($"Errore configurazione sessione: {ex.Message}");
Log($"[ERRORE] Configurazione sessione: {ex.Message}");
MessageBox.Show($"Errore: {ex.Message}", "Errore", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
@@ -248,24 +337,6 @@ namespace AutoBidder
MessageBox.Show("Usa i pulsanti Pausa/Riprendi per asta", "Info", MessageBoxButton.OK, MessageBoxImage.Information);
}
private void OpenBrowserButton_Click(object sender, RoutedEventArgs e)
{
if (_browserWindow == null || !_browserWindow.IsVisible)
{
_browserWindow = new BrowserWindow();
_browserWindow.OnAddAuction += async (url) =>
{
await AddAuctionFromUrl(url);
};
_browserWindow.Show();
Log("[BROWSER] Browser aperto");
}
else
{
_browserWindow.Activate();
}
}
private async void AddUrlButton_Click(object sender, RoutedEventArgs e)
{
var dialog = new AddAuctionSimpleDialog();
@@ -402,7 +473,8 @@ namespace AutoBidder
{
if (int.TryParse(tb.Text, out var value) && value >= 0)
{
_selectedAuction.AuctionInfo.MaxClicks = value;
_selectedAuction.MaxClicks = value;
SaveAuctions(); // Persist change immediately
}
}
}
@@ -511,13 +583,13 @@ namespace AutoBidder
return Task.CompletedTask;
}
productName = ExtractProductName(originalUrl);
productName = ExtractProductName(originalUrl) ?? string.Empty;
}
else
{
// È solo un ID numerico - costruisci URL generico
auctionId = input.Trim();
productName = null;
productName = string.Empty;
originalUrl = $"https://it.bidoo.com/auction.php?a=asta_{auctionId}";
}
@@ -593,7 +665,8 @@ namespace AutoBidder
var name = $"Asta {auctionId}";
try
{
var html = await HttpClientProvider.GetStringAsync(url);
using var httpClient = new System.Net.Http.HttpClient();
var html = await httpClient.GetStringAsync(url);
var match = System.Text.RegularExpressions.Regex.Match(html, @"<title>([^<]+)</title>");
if (match.Success)
{
@@ -753,18 +826,16 @@ namespace AutoBidder
SelectedMaxPrice.Text = auction.MaxPrice.ToString();
SelectedMinResets.Text = auction.AuctionInfo.MinResets.ToString();
SelectedMaxResets.Text = auction.AuctionInfo.MaxResets.ToString();
SelectedMaxClicks.Text = auction.MaxClicks.ToString();
// Mostra il link dell'asta selezionata
var url = auction.AuctionInfo.OriginalUrl;
if (string.IsNullOrEmpty(url))
url = $"https://it.bidoo.com/auction.php?a=asta_{auction.AuctionId}";
SelectedAuctionUrl.Text = url;
// Abilita solo i pulsanti rimasti
ResetSettingsButton.IsEnabled = true;
ClearBiddersButton.IsEnabled = true;
ClearLogButton.IsEnabled = true;
// Aggiorna log asta
UpdateAuctionLog(auction);
// Aggiorna bidders con metodo dedicato
@@ -773,43 +844,14 @@ namespace AutoBidder
catch { }
}
private void OpenAuctionButton_Click(object sender, RoutedEventArgs e)
{
if (_selectedAuction == null) return;
try
{
// Usa l'URL originale salvato nell'asta
var url = _selectedAuction.AuctionInfo.OriginalUrl;
if (string.IsNullOrEmpty(url))
{
// Fallback: costruisci URL generico
url = $"https://it.bidoo.com/auction.php?a=asta_{_selectedAuction.AuctionId}";
}
System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo
{
FileName = url,
UseShellExecute = true
});
Log($"[LINK] Apertura asta: {_selectedAuction.Name}");
}
catch (Exception ex)
{
Log($"[ERRORE] Errore apertura asta: {ex.Message}");
MessageBox.Show($"Errore: {ex.Message}", "Errore", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
private void UpdateAuctionLog(AuctionViewModel auction)
{
try
{
var auctionInfo = auction.AuctionInfo;
SelectedAuctionLog.Document.Blocks.Clear();
var logBox = SelectedAuctionLog;
var doc = logBox.Document;
doc.Blocks.Clear();
foreach (var entry in auctionInfo.AuctionLog)
{
var upper = entry.ToUpperInvariant();
@@ -818,14 +860,21 @@ namespace AutoBidder
color = System.Windows.Media.Brushes.IndianRed;
else if (upper.Contains("[WARN]") || upper.Contains("WARN"))
color = System.Windows.Media.Brushes.Orange;
var p = new System.Windows.Documents.Paragraph { Margin = new Thickness(0) };
var r = new System.Windows.Documents.Run(entry) { Foreground = color };
p.Inlines.Add(r);
SelectedAuctionLog.Document.Blocks.Add(p);
doc.Blocks.Add(p);
}
SelectedAuctionLog.ScrollToEnd();
// Scroll only if user is already near the bottom
var viewer = logBox;
var scroll = viewer.VerticalScrollBarVisibility;
var vpos = viewer.VerticalOffset;
var vmax = viewer.ExtentHeight - viewer.ViewportHeight;
if (vmax - vpos < 40) // threshold: 40px from bottom
{
viewer.ScrollToEnd();
}
// else: preserve scroll position
}
catch { }
}
@@ -840,7 +889,7 @@ namespace AutoBidder
SelectedAuctionBiddersGrid.ItemsSource = null; // Force refresh
SelectedAuctionBiddersGrid.ItemsSource = bidders;
SelectedAuctionBiddersCount.Text = bidders.Count.ToString();
SelectedAuctionBiddersCount.Text = bidders?.Count.ToString() ?? "0";
}
catch { }
}
@@ -853,7 +902,7 @@ namespace AutoBidder
private void UpdateTotalCount()
{
TotalAuctionsText.Text = _auctionViewModels.Count.ToString();
MonitorateTitle.Text = $"Aste monitorate: {_auctionViewModels.Count}";
}
private void LoadSavedAuctions()
@@ -861,28 +910,25 @@ namespace AutoBidder
try
{
var auctions = PersistenceManager.LoadAuctions();
foreach (var auction in auctions)
{
// Protezione: rimuovi eventuali BidHistory null
auction.BidHistory = auction.BidHistory?.Where(b => b != null).ToList() ?? new List<BidHistory>();
_auctionMonitor.AddAuction(auction);
var vm = new AuctionViewModel(auction);
_auctionViewModels.Add(vm);
}
// On startup treat persisted auctions as stopped (user expectation)
foreach (var vm in _auctionViewModels)
{
vm.IsActive = false;
vm.IsPaused = false;
}
UpdateTotalCount();
if (auctions.Count > 0)
{
Log($"[OK] Caricate {auctions.Count} aste salvate");
}
// Carica sessione salvata (se esiste)
LoadSavedSession();
}
@@ -994,7 +1040,6 @@ namespace AutoBidder
else
level = LogLevel.Info;
}
Dispatcher.BeginInvoke(() =>
{
var para = new System.Windows.Documents.Paragraph();
@@ -1015,6 +1060,11 @@ namespace AutoBidder
}
para.Inlines.Add(run);
LogBox.Document.Blocks.Add(para);
// Mantieni solo ultimi 500 log
while (LogBox.Document.Blocks.Count > 500)
{
LogBox.Document.Blocks.Remove(LogBox.Document.Blocks.FirstBlock);
}
LogBox.ScrollToEnd();
});
}
@@ -1057,7 +1107,7 @@ namespace AutoBidder
private void GridStartAuction_Click(object sender, RoutedEventArgs e)
{
if (sender is Button btn && btn.CommandParameter is AuctionViewModel vm)
{
{
vm.IsActive = true;
vm.IsPaused = false;
Log($"[START] Asta avviata: {vm.Name}");
@@ -1169,67 +1219,59 @@ namespace AutoBidder
StopButton.IsEnabled = true; StopButton.Opacity = 1.0;
}
}
}
// ===== DIALOG HELPER =====
public class AddAuctionDialog : Window
{
private readonly TextBox _urlTextBox;
public string AuctionUrl => _urlTextBox.Text.Trim();
public AddAuctionDialog()
private async void UserBannerTimer_Tick(object? sender, EventArgs e)
{
Title = "Aggiungi Asta";
Width = 500;
Height = 150;
WindowStartupLocation = WindowStartupLocation.CenterOwner;
ResizeMode = ResizeMode.NoResize;
var grid = new Grid { Margin = new Thickness(20) };
grid.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto });
grid.RowDefinitions.Add(new RowDefinition { Height = new GridLength(10) });
grid.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto });
grid.RowDefinitions.Add(new RowDefinition { Height = new GridLength(15) });
grid.RowDefinitions.Add(new RowDefinition { Height = GridLength.Auto });
var label = new TextBlock { Text = "URL Asta:", FontWeight = FontWeights.SemiBold };
Grid.SetRow(label, 0);
_urlTextBox = new TextBox { Text = "https://it.bidoo.com/asta/", Height = 30, Padding = new Thickness(5) };
Grid.SetRow(_urlTextBox, 2);
var buttonPanel = new StackPanel { Orientation = Orientation.Horizontal, HorizontalAlignment = HorizontalAlignment.Right };
Grid.SetRow(buttonPanel, 4);
var okButton = new Button { Content = "Aggiungi", Width = 100, Height = 32, Margin = new Thickness(0, 0, 10, 0) };
var cancelButton = new Button { Content = "Annulla", Width = 100, Height = 32 };
okButton.Click += (s, e) => { DialogResult = true; Close(); };
cancelButton.Click += (s, e) => { DialogResult = false; Close(); };
_urlTextBox.KeyDown += (s, e) => { if (e.Key == Key.Enter) { DialogResult = true; Close(); } };
buttonPanel.Children.Add(okButton);
buttonPanel.Children.Add(cancelButton);
grid.Children.Add(label);
grid.Children.Add(_urlTextBox);
grid.Children.Add(buttonPanel);
Content = grid;
Loaded += (s, e) => _urlTextBox.Focus();
await UpdateUserBannerInfoAsync();
}
}
// ===== HttpClient Helper =====
internal static class HttpClientProvider
{
private static readonly System.Net.Http.HttpClient _httpClient = new();
public static async Task<string> GetStringAsync(string url)
private async Task UpdateUserBannerInfoAsync()
{
return await _httpClient.GetStringAsync(url);
var info = await _auctionMonitor.GetUserBannerInfoAsync();
if (info != null)
{
BannerAsteDaRiscattare.Text = $"{info.nAsteVinte - info.nAsteConfermate}";
// BannerPuntateBonus.Text = $"Bonus: {info.nPuntateBonus}"; // RIMOSSO
}
else
{
BannerAsteDaRiscattare.Text = "--";
// BannerPuntateBonus.Text = "Bonus: --"; // RIMOSSO
}
}
private async void UserHtmlTimer_Tick(object? sender, EventArgs e)
{
await UpdateUserHtmlInfoAsync();
}
private async Task UpdateUserHtmlInfoAsync()
{
var userData = await _auctionMonitor.GetUserDataFromHtmlAsync();
if (userData != null)
{
Dispatcher.Invoke(() =>
{
SetUserBanner(userData.Username, userData.RemainingBids);
Log($"[HTML] Utente: {userData.Username}, Puntate residue: {userData.RemainingBids}");
});
}
}
private void SetUserBanner(string username, int remainingBids)
{
// Filtra valori non validi
if (string.IsNullOrWhiteSpace(username) || username.Contains("<!DOCTYPE") || username.Contains("<html") || username.Contains("<head") || username.Contains("<title"))
{
UsernameText.Text = "Non configurato";
}
else
{
// Rimuovi newline e taglia a max 20 caratteri
var clean = username.Replace("\r", "").Replace("\n", "");
UsernameText.Text = clean.Length > 20 ? clean.Substring(0, 20) + "..." : clean;
}
RemainingBidsText.Text = remainingBids.ToString();
}
}
}

View File

@@ -1,10 +1,13 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.Json.Serialization;
namespace AutoBidder.Models
{
/// <summary>
/// Informazioni base di un'asta monitorata
/// Solo HTTP, nessuna modalità, browser o multi-click
/// </summary>
public class AuctionInfo
{
@@ -19,15 +22,14 @@ namespace AutoBidder.Models
public double MaxPrice { get; set; } = 0;
public int MinResets { get; set; } = 0; // Numero minimo reset prima di puntare
public int MaxResets { get; set; } = 0; // Numero massimo reset (0 = illimitati)
// Numero massimo di click/puntate che il bot può eseguire per questa asta (0 = illimitato)
public int MaxClicks { get; set; } = 0;
[JsonPropertyName("MaxClicks")]
public int MaxClicks { get; set; } = 0; // Numero massimo di puntate consentite (0 = illimitato)
// Stato asta
public bool IsActive { get; set; } = true;
public bool IsPaused { get; set; } = false;
// Contatori
public int MyClicks { get; set; } = 0;
public int ResetCount { get; set; } = 0;
// Timestamp
@@ -35,12 +37,10 @@ namespace AutoBidder.Models
public DateTime? LastClickAt { get; set; }
// Storico
public List<BidHistory> BidHistory { get; set; } = new();
public List<BidHistory> BidHistory { get; set; } = new List<BidHistory>();
public Dictionary<string, BidderInfo> BidderStats { get; set; } = new(StringComparer.OrdinalIgnoreCase);
// Legacy (deprecato, usa BidderStats)
[System.Text.Json.Serialization.JsonIgnore]
public Dictionary<string, int> Bidders { get; set; } = new(StringComparer.OrdinalIgnoreCase);
// Legacy (deprecato) - removed `Bidders` dictionary; use `BidderStats` instead
// Log per-asta (non serializzato)
[System.Text.Json.Serialization.JsonIgnore]
@@ -54,11 +54,13 @@ namespace AutoBidder.Models
var entry = $"{DateTime.Now:HH:mm:ss} - {message}";
AuctionLog.Add(entry);
// Mantieni solo ultimi 100 log
if (AuctionLog.Count > 100)
// Mantieni solo ultimi 500 log
if (AuctionLog.Count > 500)
{
AuctionLog.RemoveAt(0);
}
}
public int PollingLatencyMs { get; set; } = 0; // Ultima latenza polling ms
}
}

View File

@@ -61,10 +61,12 @@ namespace AutoBidder.Models
Name = auction.Name,
MonitoringStarted = auction.AddedAt,
MonitoringDuration = DateTime.UtcNow - auction.AddedAt,
MyBids = auction.MyClicks,
// MyBids calcolato dalle entry in BidHistory
MyBids = auction.BidHistory.Count(h => h.EventType == BidEventType.MyBid),
Resets = auction.ResetCount,
UniqueBidders = auction.Bidders.Count,
BidderRanking = auction.Bidders
UniqueBidders = auction.BidderStats?.Count ?? 0,
BidderRanking = (auction.BidderStats ?? new Dictionary<string, BidderInfo>())
.ToDictionary(k => k.Key, v => v.Value.BidCount)
};
if (auction.BidHistory.Any())
@@ -113,11 +115,11 @@ namespace AutoBidder.Models
}
}
if (auction.Bidders.Any())
if (auction.BidderStats != null && auction.BidderStats.Any())
{
var topBidder = auction.Bidders.OrderByDescending(b => b.Value).First();
stats.MostActiveBidder = topBidder.Key;
stats.MostActiveBidderCount = topBidder.Value;
var top = auction.BidderStats.OrderByDescending(b => b.Value.BidCount).First();
stats.MostActiveBidder = top.Key;
stats.MostActiveBidderCount = top.Value.BidCount;
}
return stats;

View File

@@ -0,0 +1,19 @@
using System.Collections.Generic;
namespace AutoBidder.Models
{
public class UserBannerInfo
{
public int nAsteConfermate { get; set; }
public int limiteAsteVinte { get; set; }
public int nAsteVinte { get; set; }
public int nPuntateRiscattate { get; set; }
public int nPuntateBonus { get; set; }
public int nPuntateDaRiscattare { get; set; }
public int nAstePerBonus { get; set; }
public long validUntil { get; set; }
public int percentualeBonus { get; set; }
public int viewSlot { get; set; }
public List<object> extraSlots { get; set; } = new();
}
}

View File

@@ -0,0 +1,10 @@
namespace AutoBidder.Models
{
public class UserData
{
public string? Username { get; set; }
public int RemainingBids { get; set; }
public double? CashBalance { get; set; }
public int? UserId { get; set; }
}
}

33
Mimante/README.md Normal file
View File

@@ -0,0 +1,33 @@
# AutoBidder
AutoBidder è una semplice applicazione WPF per monitorare aste Bidoo via API (HTTP-only).
Questa versione è stata ripulita da funzionalità legacy (WebView2/browser integrato, multi-click, modalità legacy) e si basa esclusivamente su chiamate HTTP verso le API del sito.
Features principali:
- Monitoraggio parallelo di più aste
- Polling adattivo basato su timer aste
- Invio puntate via HTTP
- Visualizzazione log globale e log per asta
- Esportazione CSV di cronologia e statistiche
- Salvataggio sessione (cookie) in modo sicuro (DPAPI)
Pulizia effettuata:
- Rimosse classi XAML/Converters non più utilizzate
- Rimosso file di test manuale
- Rifattorizzate statistiche per usare `BidHistory` e `BidderStats`
Come usare:
1. Avvia l'app
2. Configura la sessione con il cookie `__stattrb=...` tramite `Configura`
3. Aggiungi aste (ID o URL) con `+ Aggiungi`
4. Avvia il monitoraggio con `Avvia Tutti`
Sicurezza:
- La sessione viene salvata criptata in `%APPDATA%/AutoBidder/session.dat` usando DPAPI per l'utente corrente.
Note per sviluppatori:
- Progetto .NET 8 (WPF)
- File principali: `Services/BidooApiClient.cs`, `Services/AuctionMonitor.cs`, `MainWindow.xaml.cs`.
- Per ulteriori pulizie o refactor, eseguire una ricerca per `legacy` o `RIMOSSO`.

View File

@@ -8,8 +8,8 @@ using AutoBidder.Models;
namespace AutoBidder.Services
{
/// <summary>
/// Servizio centrale per monitoraggio multi-asta
/// Gestisce polling API Bidoo e trigger dei click
/// Servizio centrale per monitoraggio aste
/// Solo HTTP, nessuna modalità, browser o multi-click
/// </summary>
public class AuctionMonitor
{
@@ -79,6 +79,30 @@ namespace AutoBidder.Services
return _apiClient.GetSession();
}
/// <summary>
/// Ottiene dati utente (nome, puntate residue, saldo, id) tramite chiamata AJAX leggera
/// </summary>
public async Task<UserData?> GetUserDataAsync()
{
return await _apiClient.GetUserDataAsync();
}
/// <summary>
/// Ottiene info banner utente (aste vinte, bonus, ecc.) tramite chiamata AJAX
/// </summary>
public async Task<UserBannerInfo?> GetUserBannerInfoAsync()
{
return await _apiClient.GetUserBannerInfoAsync();
}
/// <summary>
/// Estrae nome utente e puntate residue dall'HTML di bids_history.php
/// </summary>
public async Task<UserData?> GetUserDataFromHtmlAsync()
{
return await _apiClient.GetUserDataFromHtmlAsync();
}
public void AddAuction(AuctionInfo auction)
{
lock (_auctions)
@@ -112,12 +136,6 @@ namespace AutoBidder.Services
}
}
public Task<bool> InitializeCookies(Microsoft.Web.WebView2.Wpf.WebView2 webView)
{
// Non più utilizzato - usa InitializeSession invece
return Task.FromResult(false);
}
public void Start()
{
if (_monitoringTask != null && !_monitoringTask.IsCompleted)
@@ -259,6 +277,9 @@ namespace AutoBidder.Services
return;
}
// Aggiorna la latenza per la DataGrid
auction.PollingLatencyMs = state.PollingLatencyMs;
// Se l'asta è terminata, segnala e disattiva polling
if (state.Status == AuctionStatus.EndedWon ||
state.Status == AuctionStatus.EndedLost ||
@@ -305,63 +326,86 @@ namespace AutoBidder.Services
// Aggiorna storico e bidders
UpdateAuctionHistory(auction, state);
// Verifica se puntare (solo se asta Running, NON se in pausa)
if (state.Status == AuctionStatus.Running && ShouldBid(auction, state) && !auction.IsPaused)
{
auction.AddLog($"[TRIGGER] CONDIZIONI OK - Timer {state.Timer:F2}s <= {auction.TimerClick}s");
auction.AddLog($"[BID] Invio puntata...");
OnLog?.Invoke($"[BID] [{auction.AuctionId}] PUNTATA a {state.Timer:F2}s!");
// Attendi ritardo configurato
if (auction.DelayMs > 0)
// OTTIMIZZAZIONE PUNTATA
if (state.Status == AuctionStatus.Running && !auction.IsPaused && ShouldBid(auction, state))
{
// Se timer è esattamente 0
if (Math.Abs(state.Timer) < 0.001)
{
await Task.Delay(auction.DelayMs, token);
}
// Esegui puntata API
var result = await _apiClient.PlaceBidAsync(auction.AuctionId);
// Aggiorna contatori
if (result.Success)
{
auction.MyClicks++;
auction.LastClickAt = DateTime.UtcNow;
}
// Notifica risultato
OnBidExecuted?.Invoke(auction, result);
// Log
if (result.Success)
{
auction.AddLog($"[OK] PUNTATA OK: {result.LatencyMs}ms -> EUR{result.NewPrice:F2}");
OnLog?.Invoke($"[OK] [{auction.AuctionId}] Puntata riuscita {result.LatencyMs}ms");
auction.AddLog($"[TRIGGER] Timer 0, attendo delay {auction.DelayMs}ms...");
OnLog?.Invoke($"[BID] [{auction.AuctionId}] Timer 0, attendo delay...");
if (auction.DelayMs > 0)
await Task.Delay(auction.DelayMs, token);
// Rileggi stato asta
var stateAfterDelay = await _apiClient.PollAuctionStateAsync(auction.AuctionId, auction.OriginalUrl, token);
if (stateAfterDelay != null && Math.Abs(stateAfterDelay.Timer) < 0.001 && stateAfterDelay.LastBidder == state.LastBidder)
{
auction.AddLog($"[BID] Condizioni OK dopo delay, invio puntata...");
OnLog?.Invoke($"[BID] [{auction.AuctionId}] PUNTATA dopo delay!");
var result = await _apiClient.PlaceBidAsync(auction.AuctionId);
auction.LastClickAt = DateTime.UtcNow;
OnBidExecuted?.Invoke(auction, result);
if (result.Success)
{
auction.AddLog($"[OK] PUNTATA OK: {result.LatencyMs}ms -> EUR{result.NewPrice:F2}");
OnLog?.Invoke($"[OK] [{auction.AuctionId}] Puntata riuscita {result.LatencyMs}ms");
}
else
{
auction.AddLog($"[FAIL] PUNTATA FALLITA: {result.Error}");
OnLog?.Invoke($"[FAIL] [{auction.AuctionId}] ERRORE: {result.Error}");
}
auction.BidHistory.Add(new BidHistory
{
Timestamp = result.Timestamp,
EventType = result.Success ? BidEventType.MyBid : BidEventType.OpponentBid,
Bidder = "Tu",
Price = stateAfterDelay.Price,
Timer = stateAfterDelay.Timer,
LatencyMs = result.LatencyMs,
Success = result.Success,
Notes = result.Success ? $"EUR{result.NewPrice:F2}" : result.Error
});
}
else
{
auction.AddLog($"[SKIP] Puntata non inviata: timer/utente cambiato dopo delay");
}
}
else
{
auction.AddLog($"[FAIL] PUNTATA FALLITA: {result.Error}");
OnLog?.Invoke($"[FAIL] [{auction.AuctionId}] ERRORE: {result.Error}");
}
// Storico
auction.BidHistory.Add(new BidHistory
{
Timestamp = result.Timestamp,
EventType = result.Success ? BidEventType.MyBid : BidEventType.OpponentBid,
Bidder = "Tu",
Price = state.Price,
Timer = state.Timer,
LatencyMs = result.LatencyMs,
Success = result.Success,
Notes = result.Success ? $"EUR{result.NewPrice:F2}" : result.Error
});
// Se abbiamo raggiunto il numero massimo di click per l'asta, metti in pausa le puntate (ma continua il monitor)
if (auction.MaxClicks > 0 && auction.MyClicks >= auction.MaxClicks)
{
auction.IsPaused = true;
auction.AddLog($"[PAUSA] Massimo click ({auction.MaxClicks}) raggiunto - Puntate disabilitate");
OnLog?.Invoke($"[PAUSA] [{auction.AuctionId}] MaxClicks raggiunti");
// Logica normale per timer > 0
auction.AddLog($"[TRIGGER] CONDIZIONI OK - Timer {state.Timer:F2}s <= {auction.TimerClick}s");
auction.AddLog($"[BID] Invio puntata...");
OnLog?.Invoke($"[BID] [{auction.AuctionId}] PUNTATA a {state.Timer:F2}s!");
if (auction.DelayMs > 0)
{
await Task.Delay(auction.DelayMs, token);
}
var result = await _apiClient.PlaceBidAsync(auction.AuctionId);
auction.LastClickAt = DateTime.UtcNow;
OnBidExecuted?.Invoke(auction, result);
if (result.Success)
{
auction.AddLog($"[OK] PUNTATA OK: {result.LatencyMs}ms -> EUR{result.NewPrice:F2}");
OnLog?.Invoke($"[OK] [{auction.AuctionId}] Puntata riuscita {result.LatencyMs}ms");
}
else
{
auction.AddLog($"[FAIL] PUNTATA FALLITA: {result.Error}");
OnLog?.Invoke($"[FAIL] [{auction.AuctionId}] ERRORE: {result.Error}");
}
auction.BidHistory.Add(new BidHistory
{
Timestamp = result.Timestamp,
EventType = result.Success ? BidEventType.MyBid : BidEventType.OpponentBid,
Bidder = "Tu",
Price = state.Price,
Timer = state.Timer,
LatencyMs = result.LatencyMs,
Success = result.Success,
Notes = result.Success ? $"EUR{result.NewPrice:F2}" : result.Error
});
}
}
}
@@ -393,10 +437,6 @@ namespace AutoBidder.Services
return false;
}
// Max clicks per auction
if (auction.MaxClicks > 0 && auction.MyClicks >= auction.MaxClicks)
return false;
return true;
}

View File

@@ -12,28 +12,7 @@ namespace AutoBidder.Services
{
/// <summary>
/// Servizio completo API Bidoo (polling, puntate, info utente)
/// 100% API-based con simulazione completa del comportamento browser
///
/// FLUSSO DI AUTENTICAZIONE:
/// - Cookie principale: __stattr (non PHPSESSID)
/// - Il cookie deve essere estratto dal browser dopo login manuale
/// - Tutti i request devono includere: Cookie + X-Requested-With: XMLHttpRequest
///
/// CHIAMATE GET (SOLO LETTURA - No CSRF Token):
/// 1. Polling asta: GET data.php?ALL={id}&LISTID=0
/// 2. Info utente: GET ajax/get_auction_bids_info_banner.php
///
/// CHIAMATE POST (AZIONI - Richiedono CSRF Token):
/// 1. Piazza puntata: POST bid.php
/// - Step 1: GET pagina asta HTML per estrarre bid_token
/// - Step 2: POST con payload: a={id}&bid_type=manual&bid_token={token}&time_sent={ts}
/// - Step 3: Analizza risposta: "ok|..." o "error|..."
///
/// SIMULAZIONE BROWSER:
/// - User-Agent: Chrome su Windows
/// - Headers CORS: Sec-Fetch-*
/// - Referer: URL pagina asta
/// - Content-Type: application/x-www-form-urlencoded (per POST)
/// Solo HTTP, nessuna modalità, browser o multi-click
/// </summary>
public class BidooApiClient
{
@@ -91,14 +70,13 @@ namespace AutoBidder.Services
}
/// <summary>
/// Inizializza sessione con cookie string (fallback)
/// Inizializza sessione con cookie string (manuale)
/// </summary>
public void InitializeSessionWithCookie(string cookieString, string username)
{
_session.CookieString = cookieString;
_session.Username = username;
Log($"[SESSION] Cookie impostato ({cookieString.Length} chars)");
Log($"[SESSION] Cookie impostato manualmente ({cookieString.Length} chars)");
Log($"[SESSION] Username: {username}");
}
@@ -108,20 +86,12 @@ namespace AutoBidder.Services
/// </summary>
private void AddAuthHeaders(HttpRequestMessage request, string? referer = null, string? auctionId = null)
{
// 1. AUTENTICAZIONE (priorità: CookieString completa, poi Token singolo)
// Il cookie principale per Bidoo è __stattr (non PHPSESSID)
// 1. AUTENTICAZIONE (solo cookie manuale)
if (!string.IsNullOrWhiteSpace(_session.CookieString))
{
// Usa la stringa cookie completa (es: "__stattr=eyJyZWZ...")
request.Headers.Add("Cookie", _session.CookieString);
Log("[AUTH] Using full cookie string", auctionId);
}
else if (!string.IsNullOrWhiteSpace(_session.AuthToken))
{
// Fallback: se abbiamo solo il token, assumiamo sia __stattr
request.Headers.Add("Cookie", $"__stattr={_session.AuthToken}");
Log("[AUTH] Using __stattr token", auctionId);
}
else
{
Log("[AUTH WARN] No authentication method available!", auctionId);
@@ -244,40 +214,28 @@ namespace AutoBidder.Services
{
var startTime = DateTime.UtcNow;
var url = $"https://it.bidoo.com/data.php?ALL={auctionId}&LISTID=0";
Log($"[API REQUEST] GET {url}", auctionId);
var request = new HttpRequestMessage(HttpMethod.Get, url);
var referer = !string.IsNullOrEmpty(auctionUrl)
? auctionUrl
: $"https://it.bidoo.com/auction.php?a=asta_{auctionId}";
Log($"[API REQUEST] Using referer: {referer}", auctionId);
AddAuthHeaders(request, referer, auctionId);
var response = await _httpClient.SendAsync(request, token);
var latency = (int)(DateTime.UtcNow - startTime).TotalMilliseconds;
Log($"[API RESPONSE] Status: {(int)response.StatusCode} {response.StatusCode}", auctionId);
Log($"[API RESPONSE] Latency: {latency}ms", auctionId);
Log($"[API RESPONSE] Content-Type: {response.Content.Headers.ContentType}", auctionId);
var responseText = await response.Content.ReadAsStringAsync();
Log($"[API RESPONSE] Body ({responseText.Length} bytes): {responseText}", auctionId);
if (!response.IsSuccessStatusCode)
{
Log($"[API ERROR] Non-success status code: {response.StatusCode}", auctionId);
string reason = response.StatusCode == System.Net.HttpStatusCode.RequestTimeout ? "timeout" : "errore HTTP";
Log($"[ERRORE] [{auctionId}] API non ha risposto (motivo: {reason})", null); // globale
Log($"API non ha risposto: {response.StatusCode} ({reason})", auctionId); // asta
return null;
}
return ParsePollingResponse(auctionId, responseText, latency);
var state = ParsePollingResponse(auctionId, responseText, latency);
return state;
}
catch (Exception ex)
{
Log($"[API EXCEPTION] Polling {auctionId}: {ex.GetType().Name} - {ex.Message}", auctionId);
Log($"[API EXCEPTION] StackTrace: {ex.StackTrace}", auctionId);
Log($"[ERRORE] [{auctionId}] API non ha risposto (motivo: eccezione)", null); // globale
Log($"API non ha risposto: {ex.GetType().Name} - {ex.Message}", auctionId); // asta
return null;
}
}
@@ -286,156 +244,71 @@ namespace AutoBidder.Services
{
try
{
Log($"[PARSE] Starting parse for auction {auctionId}", auctionId);
Log($"[PARSE] Raw response: {response}", auctionId);
// Step 1: Estrai Server Timestamp (può avere formato "timestamp*..." o "timestamp|flag*")
string serverTimestamp;
string mainData;
// Cerca il separatore '*' che divide timestamp da dati
var starIndex = response.IndexOf('*');
if (starIndex == -1)
{
Log("[PARSE ERROR] No '*' separator found in response", auctionId);
return null;
}
var timestampPart = response.Substring(0, starIndex);
mainData = response.Substring(starIndex + 1);
// Il timestamp può contenere '|' (es: "1761120002|1")
// Prendiamo solo la prima parte numerica
if (timestampPart.Contains('|'))
{
serverTimestamp = timestampPart.Split('|')[0];
Log($"[PARSE] Extended format detected: {timestampPart}", auctionId);
}
else
{
serverTimestamp = timestampPart;
}
Log($"[PARSE] Server Timestamp: {serverTimestamp}", auctionId);
Log($"[PARSE] Main Data: {mainData}", auctionId);
// Step 2: Estrai dati asta tra parentesi quadre [...]
var bracketStart = mainData.IndexOf('[');
var bracketEnd = mainData.IndexOf(']');
if (bracketStart == -1 || bracketEnd == -1)
{
Log("[PARSE ERROR] Missing brackets in auction data", auctionId);
return null;
}
var auctionData = mainData.Substring(bracketStart + 1, bracketEnd - bracketStart - 1);
Log($"[PARSE] Auction Data extracted: {auctionData}", auctionId);
// Step 3: Split per ';' per ottenere i campi principali
// Nota: la stringa può contenere '|' per separare bid history, quindi ci fermiamo al primo '|' o ','
var firstSeparator = auctionData.IndexOfAny(new[] { '|', ',' });
var coreData = firstSeparator > 0 ? auctionData.Substring(0, firstSeparator) : auctionData;
var fields = coreData.Split(';');
Log($"[PARSE] Core fields count: {fields.Length}", auctionId);
for (int i = 0; i < Math.Min(fields.Length, 10); i++)
{
Log($"[PARSE] Field[{i}] = '{fields[i]}'", auctionId);
}
if (fields.Length < 5)
{
Log($"[PARSE ERROR] Expected at least 5 core fields, got {fields.Length}", auctionId);
return null;
}
var state = new AuctionState
{
AuctionId = auctionId,
SnapshotTime = DateTime.UtcNow,
PollingLatencyMs = latency
};
// Step 4: Parse campi principali
// Field 0: AuctionID (verifica)
Log($"[PARSE] Auction ID from response: {fields[0]} (expected: {auctionId})", auctionId);
// Field 1: Status (ON/OFF)
var status = fields[1].Trim().ToUpperInvariant();
// Determiniamo lo stato in base a: Status API + LastBidder + Timer
// Parsing di Field 4 (LastBidder) anticipato per logica stato
string lastBidder = fields[4].Trim();
bool hasWinner = !string.IsNullOrEmpty(lastBidder);
bool iAmWinner = hasWinner && lastBidder.Equals(_session.Username, StringComparison.OrdinalIgnoreCase);
state.Status = DetermineAuctionStatus(status, hasWinner, iAmWinner, ref state);
Log($"[PARSE] Status: {status} -> {state.Status}", auctionId);
// Field 2: Expiry Timestamp (CRITICO per timing)
// IMPORTANTE: Usa il ServerTimestamp dalla risposta, NON il tempo locale!
// Formato: ServerTimestamp*[..;ExpiryTimestamp;..]
// Timer = ExpiryTimestamp - ServerTimestamp
if (long.TryParse(serverTimestamp, out var serverTs) && long.TryParse(fields[2], out var expiryTs))
{
// Calcolo corretto usando il tempo del server
var timerSeconds = (double)(expiryTs - serverTs);
state.Timer = Math.Max(0, timerSeconds);
Log($"[PARSE] Server Timestamp: {serverTs}", auctionId);
Log($"[PARSE] Expiry Timestamp: {expiryTs}", auctionId);
Log($"[PARSE] Timer (Expiry - Server): {expiryTs} - {serverTs} = {timerSeconds:F2}s", auctionId);
Log($"[PARSE] ⏱️ Final Timer: {state.Timer:F2}s", auctionId);
// DEBUG: Verifica se il timer sembra sbagliato
if (state.Timer > 60)
{
Log("[PARSE WARN] Timer unusually high! Check data", auctionId);
}
}
else
{
Log($"[PARSE WARN] Failed to parse timestamps - Server: {serverTimestamp}, Expiry: {fields[2]}", auctionId);
}
// Field 3: Price Index (0.01 EUR per index)
if (int.TryParse(fields[3], out var priceIndex))
{
state.Price = priceIndex * 0.01;
Log($"[PARSE] Price Index: {priceIndex} -> €{state.Price:F2}", auctionId);
}
else
{
Log($"[PARSE WARN] Failed to parse price index: {fields[3]}", auctionId);
}
// Field 4: Last Bidder
state.LastBidder = fields[4].Trim();
state.IsMyBid = !string.IsNullOrEmpty(_session.Username) &&
state.LastBidder.Equals(_session.Username, StringComparison.OrdinalIgnoreCase);
Log($"[PARSE] 👤 Last Bidder: {state.LastBidder}", auctionId);
Log($"[PARSE] Is My Bid: {state.IsMyBid} (my username: {_session.Username})", auctionId);
// Field 5 (optional): Additional flags or data
if (fields.Length > 5)
{
Log($"[PARSE] Additional data fields present: {fields.Length - 5}", auctionId);
}
state.ParsingSuccess = true;
Log($"[PARSE SUCCESS] Timer: {state.Timer:F2}s, Price: €{state.Price:F2}, Bidder: {state.LastBidder}, Status: {state.Status}", auctionId);
// Log only summary on success
Log($"[PARSE SUCCESS] Timer: {state.Timer:F2}s, Price: €{state.Price:F2}, Bidder: {state.LastBidder}, Status: {state.Status}", auctionId);
return state;
}
catch (Exception ex)
{
Log($"[PARSE EXCEPTION] {ex.GetType().Name}: {ex.Message}", auctionId);
Log($"[PARSE EXCEPTION] StackTrace: {ex.StackTrace}", auctionId);
return null;
}
}
@@ -459,7 +332,7 @@ namespace AutoBidder.Services
Log($"[USER INFO RESPONSE] Latency: {latency}ms");
var responseText = await response.Content.ReadAsStringAsync();
Log($"[USER INFO RESPONSE] Body: {responseText}");
Log($"[USER INFO RESPONSE] Body length: {responseText.Length}");
if (!response.IsSuccessStatusCode)
{
@@ -484,39 +357,33 @@ namespace AutoBidder.Services
AuctionId = auctionId,
Timestamp = DateTime.UtcNow
};
try
{
Log($"[BID] Placing bid via direct GET to bid.php (no token)", auctionId);
Log($"[BID] Placing bid via direct GET to bid.php", auctionId);
var url = "https://it.bidoo.com/bid.php";
var payload = $"AID={WebUtility.UrlEncode(auctionId)}&sup=0&shock=0";
Log($"[BID REQUEST] GET {url}?{payload}", auctionId);
var getUrl = url + "?" + payload;
var request = new HttpRequestMessage(HttpMethod.Get, getUrl);
var referer = !string.IsNullOrEmpty(auctionUrl) ? auctionUrl : $"https://it.bidoo.com/asta/nome-prodotto-{auctionId}";
AddAuthHeaders(request, referer, auctionId);
if (!request.Headers.Contains("Origin"))
{
request.Headers.Add("Origin", "https://it.bidoo.com");
}
var startTime = DateTime.UtcNow;
var response = await _httpClient.SendAsync(request);
result.LatencyMs = (int)(DateTime.UtcNow - startTime).TotalMilliseconds;
Log($"[BID RESPONSE] Status: {(int)response.StatusCode} {response.StatusCode}", auctionId);
Log($"[BID RESPONSE] Latency: {result.LatencyMs}ms", auctionId);
var responseText = await response.Content.ReadAsStringAsync();
result.Response = responseText;
Log($"[BID RESPONSE] Body ({responseText.Length} bytes): {responseText}", auctionId);
Log($"[BID RESPONSE] Body length: {responseText.Length} bytes", auctionId);
if (!string.IsNullOrEmpty(responseText))
{
var preview = responseText.Length > 80 ? responseText.Substring(0, 80) + "..." : responseText;
Log($"[BID RESPONSE] Preview: {preview}", auctionId);
}
if (responseText.StartsWith("ok", StringComparison.OrdinalIgnoreCase))
{
result.Success = true;
@@ -525,8 +392,7 @@ namespace AutoBidder.Services
{
result.NewPrice = priceIndex * 0.01;
}
Log("[BID SUCCESS] ✓ Bid placed successfully via GET", auctionId);
Log("[BID SUCCESS] ✓ Bid placed successfully", auctionId);
}
else if (responseText.StartsWith("error", StringComparison.OrdinalIgnoreCase) || responseText.StartsWith("no|", StringComparison.OrdinalIgnoreCase))
{
@@ -547,7 +413,6 @@ namespace AutoBidder.Services
result.Error = string.IsNullOrEmpty(responseText) ? $"HTTP {(int)response.StatusCode}" : responseText;
Log($"[BID ERROR] Unexpected response format: {result.Error}", auctionId);
}
return result;
}
catch (Exception ex)
@@ -555,7 +420,7 @@ namespace AutoBidder.Services
result.Success = false;
result.Error = ex.Message;
Log($"[BID EXCEPTION] {ex.GetType().Name}: {ex.Message}", auctionId);
Log($"[BID EXCEPTION] StackTrace: {ex.StackTrace}", auctionId);
Log($"[BID EXCEPTION] StackTrace available in debug logs", auctionId);
return result;
}
}
@@ -623,5 +488,136 @@ namespace AutoBidder.Services
{
_httpClient?.Dispose();
}
/// <summary>
/// Ottiene dati utente (nome, puntate residue, saldo, id) tramite chiamata AJAX leggera
/// </summary>
public async Task<UserData?> GetUserDataAsync()
{
try
{
var url = "https://it.bidoo.com/update_credits_status.php?submit=1";
Log($"[USER STATUS REQUEST] GET {url}");
var request = new HttpRequestMessage(HttpMethod.Get, url);
AddAuthHeaders(request, "https://it.bidoo.com/");
var response = await _httpClient.SendAsync(request);
var responseString = await response.Content.ReadAsStringAsync();
Log($"[USER STATUS RESPONSE] Body: {responseString}");
var userData = new UserData();
var trimmed = responseString.Trim();
// Caso: solo numero
if (int.TryParse(trimmed, out int bids))
{
userData.RemainingBids = bids;
return userData;
}
// Caso: HTML <span id="bids_count">125</span>
if (trimmed.StartsWith("<span") && trimmed.Contains("id=\"bids_count\""))
{
var start = trimmed.IndexOf('>');
var end = trimmed.IndexOf("</span>");
if (start >= 0 && end > start)
{
var value = trimmed.Substring(start + 1, end - start - 1);
if (int.TryParse(value, out bids))
{
userData.RemainingBids = bids;
return userData;
}
}
}
// Caso: stringa delimitata da pipe
var parts = trimmed.Split('|');
if (parts.Length >= 2)
{
userData.Username = parts[0];
if (int.TryParse(parts[1], out bids))
{
userData.RemainingBids = bids;
}
if (parts.Length >= 3 && double.TryParse(parts[2], out double cash))
{
userData.CashBalance = cash;
}
if (parts.Length >= 4 && int.TryParse(parts[3], out int userId))
{
userData.UserId = userId;
}
return userData;
}
// Se non riconosciuto
Log($"[USER STATUS PARSE ERROR] Formato non riconosciuto: {responseString}");
return null;
}
catch (Exception ex)
{
Log($"[USER STATUS EXCEPTION] {ex.GetType().Name}: {ex.Message}");
return null;
}
}
/// <summary>
/// Ottiene info banner utente (aste vinte, bonus, ecc.) tramite chiamata AJAX
/// </summary>
public async Task<UserBannerInfo?> GetUserBannerInfoAsync()
{
try
{
var url = "https://it.bidoo.com/ajax/get_auction_bids_info_banner.php";
Log($"[USER BANNER REQUEST] GET {url}");
var request = new HttpRequestMessage(HttpMethod.Get, url);
AddAuthHeaders(request, "https://it.bidoo.com/");
var response = await _httpClient.SendAsync(request);
var responseString = await response.Content.ReadAsStringAsync();
Log($"[USER BANNER RESPONSE] Body: {responseString}");
if (!response.IsSuccessStatusCode || string.IsNullOrWhiteSpace(responseString))
return null;
var info = System.Text.Json.JsonSerializer.Deserialize<UserBannerInfo>(responseString);
return info;
}
catch (Exception ex)
{
Log($"[USER BANNER EXCEPTION] {ex.GetType().Name}: {ex.Message}");
return null;
}
}
/// <summary>
/// Estrae nome utente e puntate residue dall'HTML di bids_history.php
/// </summary>
public async Task<UserData?> GetUserDataFromHtmlAsync()
{
try
{
var url = "https://it.bidoo.com/bids_history.php";
Log($"[USER HTML REQUEST] GET {url}");
var request = new HttpRequestMessage(HttpMethod.Get, url);
AddAuthHeaders(request, "https://it.bidoo.com/");
var response = await _httpClient.SendAsync(request);
var html = await response.Content.ReadAsStringAsync();
Log($"[USER HTML RESPONSE] Body length: {html.Length}");
var userData = new UserData();
// Estrai nome utente
var userMatch = System.Text.RegularExpressions.Regex.Match(html, @"<a class=""pers_lnk""[^>]*>([^<]+)</a>");
if (userMatch.Success)
{
userData.Username = userMatch.Groups[1].Value.Trim();
}
// Estrai puntate residue
var bidsMatch = System.Text.RegularExpressions.Regex.Match(html, @"<span id=""divSaldoBidBottom""[^>]*>(\d+)</span>");
if (bidsMatch.Success && int.TryParse(bidsMatch.Groups[1].Value, out int bids))
{
userData.RemainingBids = bids;
}
if (!string.IsNullOrEmpty(userData.Username) && userData.RemainingBids > 0)
return userData;
return null;
}
catch (Exception ex)
{
Log($"[USER HTML EXCEPTION] {ex.GetType().Name}: {ex.Message}");
return null;
}
}
}
}

View File

@@ -10,6 +10,7 @@ namespace AutoBidder.Services
/// <summary>
/// Gestore persistenza sessione Bidoo
/// Salva in modo sicuro il cookie di autenticazione per riutilizzo
/// Il cookie deve essere inserito manualmente dall'utente (copiato dal browser)
/// </summary>
public class SessionManager
{

View File

@@ -1,154 +0,0 @@
using System;
using System.Net.Http;
using System.Threading.Tasks;
namespace AutoBidder.Tests
{
/// <summary>
/// Test manuale delle API Bidoo
/// Compilare e eseguire per testare le chiamate
/// </summary>
public class TestBidooApi
{
public static async Task RunAsync(string[] args)
{
Console.WriteLine("=== TEST MANUALE API BIDOO ===\n");
// CONFIGURAZIONE
Console.Write("Inserisci il cookie __stattr: ");
string? cookie = Console.ReadLine();
Console.Write("Inserisci l'ID asta (es: 81417915): ");
string? auctionId = Console.ReadLine();
if (string.IsNullOrWhiteSpace(cookie) || string.IsNullOrWhiteSpace(auctionId))
{
Console.WriteLine("? Cookie o Auction ID mancanti!");
return;
}
Console.WriteLine("\n--- Test 1: Polling Stato Asta ---");
await TestPollingAsync(cookie, auctionId);
Console.WriteLine("\n--- Test 2: Info Utente ---");
await TestUserInfoAsync(cookie);
Console.WriteLine("? Test completati! Premi un tasto per uscire...");
Console.ReadKey();
}
/// <summary>
/// Test chiamata polling asta
/// </summary>
private static async Task TestPollingAsync(string cookie, string auctionId)
{
try
{
var handler = new HttpClientHandler { UseCookies = false };
using var client = new HttpClient(handler) { Timeout = TimeSpan.FromSeconds(5) };
var url = $"https://it.bidoo.com/data.php?ALL={auctionId}&LISTID=0";
Console.WriteLine($"?? GET {url}");
var request = new HttpRequestMessage(HttpMethod.Get, url);
// Headers
request.Headers.Add("Cookie", $"__stattr={cookie}");
request.Headers.Add("X-Requested-With", "XMLHttpRequest");
request.Headers.Add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36");
request.Headers.Add("Referer", $"https://it.bidoo.com/asta/nome-prodotto-{auctionId}");
var startTime = DateTime.UtcNow;
var response = await client.SendAsync(request);
var latency = (int)(DateTime.UtcNow - startTime).TotalMilliseconds;
Console.WriteLine($"?? Status: {(int)response.StatusCode} {response.StatusCode}");
Console.WriteLine($"?? Latency: {latency}ms");
var responseText = await response.Content.ReadAsStringAsync();
Console.WriteLine($"?? Response ({responseText.Length} bytes):");
Console.WriteLine(responseText);
// Parse semplice
if (response.IsSuccessStatusCode && responseText.Contains("*"))
{
Console.WriteLine("\n?? Parsing:");
var parts = responseText.Split('*');
Console.WriteLine($" Server Time: {parts[0]}");
if (parts.Length > 1)
{
var data = parts[1].Replace("[", "").Replace("]", "").Split(';');
if (data.Length >= 5)
{
Console.WriteLine($" Auction ID: {data[0]}");
Console.WriteLine($" Status: {data[1]}");
Console.WriteLine($" Expiry: {data[2]}");
if (int.TryParse(data[3], out var priceIndex))
{
Console.WriteLine($" Price: {priceIndex} ? €{(priceIndex * 0.01):F2}");
}
Console.WriteLine($" Last Bidder: {data[4]}");
// Calcola timer
if (long.TryParse(data[2], out var expiryTs))
{
var expiryTime = DateTimeOffset.FromUnixTimeSeconds(expiryTs);
var timer = (expiryTime - DateTimeOffset.UtcNow).TotalSeconds;
Console.WriteLine($" Timer: {Math.Max(0, timer):F2}s");
}
}
}
}
else
{
Console.WriteLine("?? Risposta non valida o errore");
}
}
catch (Exception ex)
{
Console.WriteLine($"? Errore: {ex.GetType().Name} - {ex.Message}");
}
}
/// <summary>
/// Test chiamata info utente
/// </summary>
private static async Task TestUserInfoAsync(string cookie)
{
try
{
var handler = new HttpClientHandler { UseCookies = false };
using var client = new HttpClient(handler) { Timeout = TimeSpan.FromSeconds(5) };
var url = "https://it.bidoo.com/ajax/get_auction_bids_info_banner.php";
Console.WriteLine($"?? GET {url}");
var request = new HttpRequestMessage(HttpMethod.Get, url);
// Headers
request.Headers.Add("Cookie", $"__stattr={cookie}");
request.Headers.Add("X-Requested-With", "XMLHttpRequest");
request.Headers.Add("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36");
request.Headers.Add("Referer", "https://it.bidoo.com/");
var startTime = DateTime.UtcNow;
var response = await client.SendAsync(request);
var latency = (int)(DateTime.UtcNow - startTime).TotalMilliseconds;
Console.WriteLine($"?? Status: {(int)response.StatusCode} {response.StatusCode}");
Console.WriteLine($"?? Latency: {latency}ms");
var responseText = await response.Content.ReadAsStringAsync();
Console.WriteLine($"?? Response ({responseText.Length} bytes):");
Console.WriteLine(responseText);
}
catch (Exception ex)
{
Console.WriteLine($"? Errore: {ex.GetType().Name} - {ex.Message}");
}
}
}
}

View File

@@ -101,22 +101,23 @@ namespace AutoBidder.Utilities
var csv = new StringBuilder();
// Header
csv.AppendLine("Auction ID,Name,Timer Click,Delay (ms),Min Price,Max Price," +
"My Clicks,Resets,Total Bidders,Active,Paused," +
"Added At,Last Click At");
csv.AppendLine("Auction ID,Name,Timer Click,Delay (ms),Min Price,Max Price,My Bids,Resets,Total Bidders,Active,Paused,Added At,Last Click At");
// Data
foreach (var auction in auctions)
{
var totalBidders = auction.BidderStats?.Count ?? 0;
var myBids = auction.BidHistory.Count(h => h.EventType == BidEventType.MyBid);
csv.AppendLine($"{auction.AuctionId}," +
$"\"{auction.Name}\"," +
$"{auction.TimerClick}," +
$"{auction.DelayMs}," +
$"{auction.MinPrice:F2}," +
$"{auction.MaxPrice:F2}," +
$"{auction.MyClicks}," +
$"{myBids}," +
$"{auction.ResetCount}," +
$"{auction.Bidders.Count}," +
$"{totalBidders}," +
$"{auction.IsActive}," +
$"{auction.IsPaused}," +
$"{auction.AddedAt:yyyy-MM-dd HH:mm:ss}," +

View File

@@ -0,0 +1,34 @@
using System;
using System.Windows.Input;
namespace AutoBidder.Utilities
{
public class RelayCommand : ICommand
{
private readonly Action<object?> _execute;
private readonly Func<object?, bool>? _canExecute;
public RelayCommand(Action<object?> execute, Func<object?, bool>? canExecute = null)
{
_execute = execute ?? throw new ArgumentNullException(nameof(execute));
_canExecute = canExecute;
}
public bool CanExecute(object? parameter)
{
return _canExecute?.Invoke(parameter) ?? true;
}
public void Execute(object? parameter)
{
_execute(parameter);
}
public event EventHandler? CanExecuteChanged;
public void RaiseCanExecuteChanged()
{
CanExecuteChanged?.Invoke(this, EventArgs.Empty);
}
}
}

View File

@@ -1,11 +1,13 @@
using System;
using System.ComponentModel;
using System.Linq;
using AutoBidder.Models;
namespace AutoBidder.ViewModels
{
/// <summary>
/// ViewModel per una riga della griglia aste (DataBinding)
/// Solo HTTP, nessuna modalità, browser o multi-click
/// </summary>
public class AuctionViewModel : INotifyPropertyChanged
{
@@ -64,6 +66,16 @@ namespace AutoBidder.ViewModels
}
}
public int MaxClicks
{
get => _auctionInfo.MaxClicks;
set
{
_auctionInfo.MaxClicks = value;
OnPropertyChanged(nameof(MaxClicks));
}
}
// Stato
public bool IsActive
{
@@ -74,7 +86,6 @@ namespace AutoBidder.ViewModels
OnPropertyChanged(nameof(IsActive));
}
}
public bool IsPaused
{
get => _auctionInfo.IsPaused;
@@ -84,11 +95,24 @@ namespace AutoBidder.ViewModels
OnPropertyChanged(nameof(IsPaused));
}
}
// Contatori
public int MyClicks => _auctionInfo.MyClicks;
public int ResetCount => _auctionInfo.ResetCount;
// My clicks computed from history (thread-safe, null-safe)
public int MyClicks
{
get
{
var history = _auctionInfo.BidHistory;
if (history == null) return 0;
BidHistory[] snapshot;
lock (history)
{
snapshot = history.ToArray();
}
return snapshot.Count(h => h != null && h.EventType == BidEventType.MyBid);
}
}
// Dati live (da ultimo polling)
public string TimerDisplay
{
@@ -192,7 +216,6 @@ namespace AutoBidder.ViewModels
public void UpdateState(AuctionState state)
{
_lastState = state;
// Notifica tutte le proprietà dipendenti dallo stato
OnPropertyChanged(nameof(TimerDisplay));
OnPropertyChanged(nameof(PriceDisplay));
@@ -200,6 +223,8 @@ namespace AutoBidder.ViewModels
OnPropertyChanged(nameof(IsMyBid));
OnPropertyChanged(nameof(StatusDisplay));
OnPropertyChanged(nameof(Strategy));
OnPropertyChanged(nameof(MyClicks));
OnPropertyChanged(nameof(AuctionInfo)); // For PollingLatencyMs
}
/// <summary>
@@ -207,8 +232,9 @@ namespace AutoBidder.ViewModels
/// </summary>
public void RefreshCounters()
{
OnPropertyChanged(nameof(MyClicks));
// RIMOSSO: OnPropertyChanged(nameof(MyClicks));
OnPropertyChanged(nameof(ResetCount));
OnPropertyChanged(nameof(MyClicks));
}
public event PropertyChangedEventHandler? PropertyChanged;

101
README.md
View File

@@ -2,7 +2,7 @@
> AutoBidder è uno strumento desktop per Windows (WPF, .NET 8) pensato per automatizzare le offerte sul sito Bidoo.com. Questa guida descrive caratteristiche, installazione, configurazione, modalità operative, strategie avanzate, dettagli tecnici e risoluzione dei problemi.
![Version](https://img.shields.io/badge/version-2.10-blue)
![Version](https://img.shields.io/badge/version-3.0-blue)
![.NET](https://img.shields.io/badge/.NET-8.0-purple)
---
@@ -12,9 +12,9 @@ Sommario
- Requisiti di sistema
- Installazione e avvio
- Guida rapida (primi passi)
- Modalità operative: Asta Singola e Multi-Asta
- Gestione aste tramite griglia
- Strategie e consigli pratici
- Dettagli tecnici: Polling, Click HTTP e sincronizzazione cookie
- Dettagli tecnici: Polling e Click HTTP
- Impostazioni e persistenza
- Esportazione e diagnostica
- FAQ e risoluzione problemi
@@ -23,10 +23,9 @@ Sommario
---
## Caratteristiche principali
- Monitoraggio in tempo reale di singole aste o molte aste contemporaneamente
- Due modalità operative: `Asta Singola` (massima precisione) e `Multi-Asta` (monitoraggio e auto-switch)
- Polling adattivo (HTTP / WebView2 / Active) per ridurre uso di CPU e RAM
- Click HTTP diretto (reverse engineered) con sincronizzazione dei cookie dal WebView2
- Monitoraggio in tempo reale di tutte le aste tramite griglia unica
- Polling adattivo solo HTTP per ridurre uso di CPU e RAM
- Click HTTP diretto (reverse engineered) con cookie manuale
- Persistenza della lista aste (`auctions.json`) e esportazione CSV delle statistiche
- UI dark theme moderna con griglia in tempo reale, dettagli asta, log e contatori
@@ -44,59 +43,47 @@ Sommario
- `dotnet restore`
- `dotnet build --configuration Release`
4. Eseguire l'app:
- `dotnet run` (dalla cartella del progetto) oppure avviare `AutoBidder.exe` in `bin\\Release\\net8.0-windows`
- `dotnet run` (dalla cartella del progetto) oppure avviare `AutoBidder.exe` in `bin\Release\net8.0-windows`
## Guida rapida (primi passi)
1. Avvia l'app: l'interfaccia principale mostra due pannelli (controlli a sinistra, browser WebView2 a destra).
2. Login: accedi a `bidoo.com` tramite il browser integrato (se vuoi usare il Click HTTP diretto devi essere loggato).
3. Scegli modalità:
- `Asta Singola` per concentrarti su un solo oggetto
- `Multi-Asta` per monitorare più aste e lasciare che l'app esegua auto-switch
4. Aggiungi aste (Multi-Asta):
- Metodo Automatico: vai sulla pagina Preferiti e lascia che l'app rilevi le aste
- Metodo Manuale: clicca `+ URL` o `Pagina` e incolla/aggiungi l'URL dell'asta
5. Configura impostazioni globali o per-asta (Timer Click, Min/Max Price, Max Clicks, Max Resets, Ritardo, Multi-Click)
6. Premi `Avvia` per attivare il Click Loop; il background polling rimane sempre attivo anche senza Avvia.
1. Avvia l'app: l'interfaccia principale mostra la griglia delle aste monitorate.
2. Configura la sessione: copia il cookie `__stattrb` dal browser (F12 > Application > Cookies) e inseriscilo tramite il dialog di configurazione.
3. Aggiungi aste: usa il pulsante `+ Aggiungi` per inserire URL o ID delle aste da monitorare.
4. Configura impostazioni per ogni asta (Timer Click, Min/Max Price, Max Clicks, Max Resets, Ritardo).
5. Premi `Avvia Tutti` per attivare il monitoraggio e le puntate automatiche.
## Modalità operative
### Asta Singola
- Ideale per oggetti di valore dove la precisione è critica.
- Monitoraggio intensivo del DOM tramite WebView2, contatori dettagliati, lista utenti e log dedicato.
- Supporta `Multi-Click` (click multipli paralleli) per aumentare probabilità in connessione instabile.
- Impostazioni consigliate per asta singola: `Timer Click` 0-1, `Multi-Click` ON, `Ritardo` 0ms.
### Multi-Asta
- Monitoraggio simultaneo di molte aste.
- L'app punta solo sull'asta con `timer` più basso (auto-switch) per evitare concorrere contro se stessa.
- Per ogni asta è possibile impostare opzioni indipendenti (Timer Click, Min/Max Price, Pausa/Riprendi).
- Metodo manuale (URL diretto) è raccomandato per monitoraggi massivi (consumo risorse minimo).
## Gestione aste tramite griglia
- Tutte le aste sono gestite tramite una griglia unica.
- Puoi avviare, mettere in pausa, fermare o puntare manualmente su ogni asta direttamente dalla griglia.
- Non esistono più modalità "Asta Singola" o "Multi-Asta": tutto è gestito in modo uniforme.
## Strategie consigliate
- Monitoraggio massivo (50+ aste): usa `URL Manuale`, `Timer Click` 0, limiti di prezzo bassi.
- Asta singola ad alto valore: `Timer Click` 0-1, `Multi-Click` ON, `Ritardo` 0ms.
- Caccia all'affare: `Max Price` molto basso, monitora molte aste con Timer Click 0.
- Monitoraggio massivo: aggiungi molte aste tramite URL o ID, imposta Timer Click basso e limiti di prezzo.
- Asta ad alto valore: Timer Click 0-1, Ritardo 0ms.
- Caccia all'affare: Max Price basso, monitora molte aste con Timer Click 0.
## Approfondimento tecnico
### Polling adattivo (Dual-Track)
- Track 1 — Background Polling: esegue richieste HTTP ogni ~5s per aggiornare timer, prezzo e ultimo bidder. Mantiene uso CPU/RAM minimo.
- Track 2 — Click Loop: attivo solo dopo `Avvia`. Polling dinamico 20-400ms per aste critiche e invio dei click (HTTP diretto o WebView2 fallback).
### Polling adattivo
- Il monitoraggio avviene solo tramite chiamate HTTP dirette alle API di Bidoo.
- Nessun WebView2 viene utilizzato, nemmeno come fallback.
- Il polling si adatta al timer dell'asta per ottimizzare la frequenza delle richieste.
### Click HTTP diretto
- Al momento dell'azione, il client costruisce una GET verso l'endpoint reverse engineered di Bidoo tipo:
`GET https://it.bidoo.com/bid.php?AID=81204347&sup=0&shock=0`
- Richiede i cookie di sessione (PHPSESSID, user_token, ecc.) che vengono sincronizzati dal WebView2.
- Latenza tipica: 10-30ms (molto più veloce del click via WebView2 che può impiegare 50-100ms)
- Se il Click HTTP fallisce, l'app esegue fallback automatico con `ExecuteScript` su WebView2.
- Richiede i cookie di sessione (`__stattrb`) che vengono copiati manualmente dal browser.
- Latenza tipica: 10-30ms.
- Non esiste più alcun fallback WebView2: tutte le puntate sono gestite solo via HTTP.
### Sincronizzazione cookie
- Al primo `Avvia` viene letto il `CookieManager` di `CoreWebView2`, copiato in un `CookieContainer` per l'`HttpClient` utilizzato dai Click HTTP.
- I cookie sono conservati solo in memoria, mai su disco. Devono essere rinfrescati ri-effettuando il login nel WebView2 se scadono.
- I cookie vengono inseriti manualmente dall'utente tramite dialog.
- Non vengono mai salvati su disco in chiaro.
- Se il cookie scade, è necessario copiarlo nuovamente dal browser.
## Persistenza e file locali
- Lista aste manuali salvata in:
`%AppData%\\AutoBidder\\auctions.json`
`%AppData%\AutoBidder\auctions.json`
- La lista viene ricaricata automaticamente all'avvio dell'app.
- Esportazione CSV: la funzionalità `Export CSV` genera un file contenente statistiche per ogni asta (nome, ID, URL, timer, prezzo, strategy, click/resets, impostazioni per-asta).
@@ -105,13 +92,10 @@ Sommario
- `Max Clicks` / `Max Resets`: limiti operativi (0 = illimitato)
- `Min/Max Price`: evita puntate fuori dal range desiderato
- `Ritardo (ms)`: delay aggiuntivo prima di inviare il click
- `Multi-Click`: invia più click paralleli (utile su connessioni con jitter)
## Interfaccia e controlli
- Pannello sinistro: controlli principali (modalità, Avvia, Pausa, Stop, aggiungi/rimuovi aste, esporta CSV)
- Pannello destro: browser integrato (WebView2) + dettagli asta quando selezionata
- Griglia aste (multi-asta): mostra Timer, Prezzo, Strategia, Clicks, Resets, Ultimo bidder
- Log in tempo reale per ogni asta con dettagli di latenza, risposta server e fallimenti
- Griglia centrale: mostra tutte le aste monitorate con controlli per avviare, mettere in pausa, fermare e puntare manualmente.
- Log in tempo reale per ogni asta con dettagli di latenza, risposta server e fallimenti.
## Diagnostica ed esportazione
- Abilita log dettagliato prima di aprire issue o per analisi locali
@@ -119,17 +103,18 @@ Sommario
- Mantieni screenshot del pannello log per report più chiari
## FAQ e risoluzione dei problemi
- "Non vedo Click HTTP riuscito": assicurati di essere loggato nel WebView2 e che la sincronizzazione cookie sia avvenuta (vedi log "Cookie sincronizzati (X cookie)"). Premi `Avvia` dopo il login.
- "Aste non rilevate": assicurati di essere nella pagina Preferiti per il metodo automatico oppure aggiungi gli URL manualmente.
- "Non vedo Click HTTP riuscito": assicurati di aver inserito correttamente il cookie dal browser e che la sessione sia valida.
- "Aste non rilevate": aggiungi manualmente gli URL o ID delle aste.
- "Il programma non si avvia": verifica .NET 8.0 Runtime installato e che il build sia andato a buon fine.
- "Click non funzionano": verifica Timer Click, limiti di prezzo e connessione internet. Controlla se il fallback WebView è attivo nei log.
- "Click non funzionano": verifica Timer Click, limiti di prezzo, connessione internet e validità del cookie.
## Avvisi e responsabilità
- L'automazione potrebbe violare i Termini di Servizio di Bidoo. L'uso è a rischio e responsabilità dell'utente.
- Non salvare credenziali su disco: l'app non memorizza login, usa il WebView2 per la sessione.
- Non salvare credenziali su disco: l'app non memorizza login, usa solo il cookie manuale.
- Cookie e dati di sessione rimangono in memoria e vengono rimossi alla chiusura dell'app.
## Changelog sintetico (ultime versioni)
- v3.0: Solo HTTP, nessun WebView2, cookie manuale, interfaccia unica su griglia
- v2.10: Click HTTP diretto, sincronizzazione cookie automatica, miglioramenti prestazionali
- v2.9: Persistenza automatica, UI improvements, export CSV
- v2.8: Polling adattivo e strategie ibride
@@ -141,13 +126,13 @@ Sommario
---
Note tecniche per sviluppatori
- Progetto target: `.NET 8.0` (WPF + WebView2)
- Progetto target: `.NET 8.0` (WPF)
- Aree chiave del codice:
- `Services\\BidooApiClient.cs` — gestione Click HTTP e parsing risposte
- `Services\\AuctionMonitor.cs` — loop di polling e logica auto-switch
- `Services\\SessionManager.cs`sincronizzazione cookie e HttpClient creation
- `Utilities\\PersistenceManager.cs` — salvataggio/ricaricamento `auctions.json`
- `ViewModels\\AuctionViewModel.cs` + XAML corrispondenti — visualizzazione e binding UI
- `Services\BidooApiClient.cs` — gestione Click HTTP e parsing risposte
- `Services\AuctionMonitor.cs` — loop di polling
- `Services\SessionManager.cs`gestione cookie manuale
- `Utilities\PersistenceManager.cs` — salvataggio/ricaricamento `auctions.json`
- `ViewModels\AuctionViewModel.cs` + XAML corrispondenti — visualizzazione e binding UI
## Contributi
- Questo repository è privato. Per contribuire, aprire PR verso branch `main` e seguire le convenzioni del progetto.