Compare commits

...

2 Commits

Author SHA1 Message Date
Alberto Balbo
8717a3b6ef Aggiornamento alla versione 4.0.0
- Aggiunta dipendenza per WebView2 per integrare un browser.
- Introdotto layout a schede (TabControl) per organizzare le funzionalità.
- Aggiunto browser WebView2 per navigazione e aggiunta aste.
- Implementata gestione delle impostazioni di esportazione (CSV, JSON, XML).
- Aggiunta funzionalità di caricamento e analisi delle aste chiuse.
- Introdotta gestione dei cookie di sessione tramite la scheda "Impostazioni".
- Creato controllo personalizzato `SimpleToolbar` per layout modulare.
- Migliorata gestione dello stato utente e fallback per dati mancanti.
- Rimossi stili e animazioni obsolete per semplificare il codice.
- Salvate le impostazioni utente in un file JSON locale.
- Correzioni di bug e miglioramenti di leggibilità del codice.
2025-11-04 23:05:49 +01:00
Alberto Balbo
967005b96a Supporto per aste chiuse e miglioramenti UI
- Aggiornamento alla versione Microsoft.EntityFrameworkCore.Sqlite 8.0.0.
- Aggiornamento alla versione Microsoft.Windows.SDK.BuildTools 10.0.26100.6584.
- Migliorata l'interfaccia per l'inserimento di più URL/ID di aste.
- Aggiunti pulsanti per "Aste Chiuse" e "Esporta" in MainWindow.
- Creata finestra "Aste Chiuse" per visualizzare e gestire aste chiuse.
- Implementato scraper per estrarre dati da aste chiuse.
- Aggiunto supporto per esportazione dati in CSV, JSON e XML.
- Introdotto contesto Entity Framework per statistiche delle aste.
- Aggiunto servizio per calcolo e gestione delle statistiche.
- Gestite preferenze di esportazione con salvataggio in file JSON.
2025-11-03 14:24:19 +01:00
16 changed files with 2366 additions and 543 deletions

View File

@@ -22,6 +22,8 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.EntityFrameworkCore.Sqlite" Version="8.0.0" />
<PackageReference Include="Microsoft.Web.WebView2" Version="1.0.1343.22" />
<PackageReference Include="Microsoft.Windows.SDK.BuildTools" Version="10.0.26100.6584" />
</ItemGroup>

View File

@@ -0,0 +1,16 @@
<UserControl x:Class="AutoBidder.Controls.SimpleToolbar"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
mc:Ignorable="d"
d:DesignHeight="48" d:DesignWidth="800">
<Grid Background="Transparent">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<ContentPresenter Content="{Binding LeftContent, RelativeSource={RelativeSource AncestorType=UserControl}}" VerticalAlignment="Center" />
<ContentPresenter Grid.Column="1" Content="{Binding RightContent, RelativeSource={RelativeSource AncestorType=UserControl}}" VerticalAlignment="Center" />
</Grid>
</UserControl>

View File

@@ -0,0 +1,28 @@
using System.Windows;
using System.Windows.Controls;
namespace AutoBidder.Controls
{
public partial class SimpleToolbar : UserControl
{
public static readonly DependencyProperty LeftContentProperty = DependencyProperty.Register("LeftContent", typeof(object), typeof(SimpleToolbar));
public static readonly DependencyProperty RightContentProperty = DependencyProperty.Register("RightContent", typeof(object), typeof(SimpleToolbar));
public object? LeftContent
{
get => GetValue(LeftContentProperty);
set => SetValue(LeftContentProperty, value);
}
public object? RightContent
{
get => GetValue(RightContentProperty);
set => SetValue(RightContentProperty, value);
}
public SimpleToolbar()
{
InitializeComponent();
}
}
}

View File

@@ -0,0 +1,23 @@
using Microsoft.EntityFrameworkCore;
using AutoBidder.Models;
namespace AutoBidder.Data
{
public class StatisticsContext : DbContext
{
public DbSet<ProductStat> ProductStats { get; set; }
public StatisticsContext(DbContextOptions<StatisticsContext> options) : base(options)
{
}
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity<ProductStat>()
.HasIndex(p => p.ProductKey)
.IsUnique(false);
base.OnModelCreating(modelBuilder);
}
}
}

View File

@@ -1,28 +1,40 @@
<?xml version="1.0" encoding="utf-8"?>
<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>
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="clr-namespace:AutoBidder.Controls"
Title="Aggiungi Asta" Height="320" 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="*" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<TextBlock Text="Inserire URL dell'asta (uno o pi&#x00F9;)" Foreground="#CCCCCC" FontSize="14" Margin="0,0,0,6" />
<TextBlock Grid.Row="1" Text="Puoi aggiungere pi&#x00F9; link separandoli con 'a capo', 'spazio' o ';'" Foreground="#999999" FontSize="12" Margin="0,0,0,10" />
<TextBox x:Name="AuctionUrlBox" Grid.Row="2" MinWidth="560" Margin="0,0,0,8"
Background="#181818" Foreground="#00CCFF" BorderBrush="#444" BorderThickness="1"
Padding="8" FontSize="13" ToolTip="Inserisci uno o pi&#x00F9; URL/ID dell'asta. Separali con a capo, spazio o ';'"
AcceptsReturn="True" TextWrapping="Wrap" VerticalScrollBarVisibility="Auto" Height="160" />
<controls:SimpleToolbar Grid.Row="3" Margin="0,8,0,0">
<controls:SimpleToolbar.RightContent>
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right">
<Button Content="OK" Width="110" Margin="6" Padding="10,8"
Style="{StaticResource SmallButtonStyle}" Background="#00CC66" Foreground="White" Click="OkButton_Click" />
<Button Content="Annulla" Width="110" Margin="6" Padding="10,8"
Style="{StaticResource SmallButtonStyle}" Background="#666" Foreground="White" Click="CancelButton_Click" />
</StackPanel>
</controls:SimpleToolbar.RightContent>
</controls:SimpleToolbar>
</Grid>
</Border>
</Window>

View File

@@ -13,13 +13,15 @@ namespace AutoBidder.Dialogs
private void OkButton_Click(object sender, RoutedEventArgs e)
{
var text = AuctionUrlBox.Text?.Trim() ?? string.Empty;
if (string.IsNullOrWhiteSpace(text) || !text.StartsWith("http"))
var text = AuctionUrlBox.Text ?? string.Empty;
if (string.IsNullOrWhiteSpace(text))
{
MessageBox.Show("Inserisci un URL valido dell'asta.", "Errore", MessageBoxButton.OK, MessageBoxImage.Warning);
MessageBox.Show("Inserisci almeno un URL o ID dell'asta.", "Errore", MessageBoxButton.OK, MessageBoxImage.Warning);
return;
}
AuctionId = text;
// Return the raw text (may contain multiple entries); caller will parse it
AuctionId = text.Trim();
DialogResult = true;
Close();
}

View File

@@ -0,0 +1,89 @@
<Window x:Class="AutoBidder.Dialogs.ClosedAuctionsWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
Title="Aste Chiuse - Estrazione" Height="600" Width="1000"
Background="#0a0a0a" Foreground="#FFFFFF" WindowStartupLocation="CenterOwner">
<Grid Margin="10">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="8" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<Border Background="#1a1a1a" Padding="10" CornerRadius="6" Grid.Row="0" BorderBrush="#333333" BorderThickness="1">
<DockPanel>
<TextBlock Text="Estrazione Aste Chiuse" FontSize="16" FontWeight="Bold" Foreground="#00CC66" VerticalAlignment="Center" DockPanel.Dock="Left" />
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right" DockPanel.Dock="Right">
<Button x:Name="StartExtractButton" Content="Avvia Estrazione" Click="StartExtractButton_Click" Width="140" Height="36" Margin="8,0,0,0" Background="#00CC66" Style="{StaticResource SmallButtonStyle}"/>
<Button x:Name="ExportStatsButton" Content="Esporta Statistiche" Click="ExportStatsButton_Click" Width="160" Height="36" Margin="8,0,0,0" Background="#8B5CF6" Style="{StaticResource SmallButtonStyle}"/>
<Button x:Name="CloseButton" Content="Chiudi" Click="CloseButton_Click" Width="80" Height="36" Margin="8,0,0,0" Background="#666" Style="{StaticResource SmallButtonStyle}"/>
</StackPanel>
</DockPanel>
</Border>
<Border Grid.Row="2" Background="#1a1a1a" Padding="8" CornerRadius="6" BorderBrush="#333333" BorderThickness="1">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="2*" />
<ColumnDefinition Width="8" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<!-- Products grid: styled like MainWindow -->
<DataGrid x:Name="ProductsGrid" AutoGenerateColumns="False" Grid.Column="0" Background="#1a1a1a" Foreground="#FFFFFF" BorderBrush="#333333" BorderThickness="1"
RowBackground="#1a1a1a" AlternatingRowBackground="#222222" GridLinesVisibility="Horizontal" HorizontalGridLinesBrush="#333333"
IsReadOnly="True" SelectionUnit="CellOrRowHeader" SelectionMode="Extended" ClipboardCopyMode="IncludeHeader" CanUserAddRows="False" CanUserDeleteRows="False">
<DataGrid.Resources>
<Style TargetType="DataGridColumnHeader">
<Setter Property="Background" Value="#2a2a2a" />
<Setter Property="Foreground" Value="#FFFFFF" />
<Setter Property="FontWeight" Value="Bold" />
<Setter Property="Padding" Value="10,8" />
<Setter Property="BorderThickness" Value="0,0,0,2" />
<Setter Property="BorderBrush" Value="#00CC66" />
</Style>
<Style TargetType="DataGridRow">
<Setter Property="Height" Value="36" />
<Style.Triggers>
<Trigger Property="IsSelected" Value="True">
<Setter Property="Background" Value="#0099FF" />
<Setter Property="Foreground" Value="White" />
</Trigger>
</Style.Triggers>
</Style>
<Style TargetType="DataGridCell">
<Setter Property="BorderThickness" Value="0" />
<Setter Property="Padding" Value="10,6" />
</Style>
</DataGrid.Resources>
<DataGrid.ContextMenu>
<ContextMenu>
<MenuItem Command="ApplicationCommands.Copy" Header="Copia" />
</ContextMenu>
</DataGrid.ContextMenu>
<DataGrid.Columns>
<DataGridTextColumn Header="Asta URL" Binding="{Binding AuctionUrl}" Width="2*" />
<DataGridTextColumn Header="Nome" Binding="{Binding ProductName}" Width="3*"/>
<DataGridTextColumn Header="Prezzo" Binding="{Binding FinalPrice}" Width="80"/>
<DataGridTextColumn Header="Vincitore" Binding="{Binding Winner}" Width="120"/>
<DataGridTextColumn Header="Puntate Usate" Binding="{Binding BidsUsed}" Width="100"/>
<DataGridTextColumn Header="Scraped At" Binding="{Binding ScrapedAt}" Width="140"/>
</DataGrid.Columns>
</DataGrid>
<GridSplitter Grid.Column="1" Width="8" />
<!-- Log area styled like main window log -->
<Border Grid.Column="2" Background="#0f0f0f" Padding="8" CornerRadius="6" BorderBrush="#333333" BorderThickness="1">
<DockPanel>
<TextBlock Text="Log Operazioni" FontWeight="Bold" Foreground="#00CC66" DockPanel.Dock="Top" Margin="0,0,0,8" />
<RichTextBox x:Name="ExtractLogBox" IsReadOnly="True" VerticalScrollBarVisibility="Auto" FontFamily="Consolas" FontSize="11" Background="#0f0f0f" Foreground="#CCC" BorderBrush="#333333" BorderThickness="1" />
</DockPanel>
</Border>
</Grid>
</Border>
</Grid>
</Window>

View File

@@ -0,0 +1,140 @@
using System;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Documents;
using Microsoft.Win32;
using AutoBidder.Models;
using AutoBidder.Services;
using AutoBidder.Data;
using Microsoft.EntityFrameworkCore;
namespace AutoBidder.Dialogs
{
public partial class ClosedAuctionsWindow : Window
{
private ObservableCollection<ClosedAuctionRecord> _products = new();
private bool _isRunning = false;
// StatsService using local DB. Create context with default sqlite file in app folder
private readonly StatsService _statsService;
public ClosedAuctionsWindow()
{
InitializeComponent();
ProductsGrid.ItemsSource = _products;
var optionsBuilder = new DbContextOptionsBuilder<StatisticsContext>();
var dbPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "stats.db");
optionsBuilder.UseSqlite($"Data Source={dbPath}");
var ctx = new StatisticsContext(optionsBuilder.Options);
_statsService = new StatsService(ctx);
Log("Finestra pronta");
}
private void Log(string message)
{
try
{
var para = new Paragraph(new Run($"{DateTime.Now:HH:mm} - {message}"));
ExtractLogBox.Document.Blocks.Add(para);
// keep size manageable
while (ExtractLogBox.Document.Blocks.Count > 500)
ExtractLogBox.Document.Blocks.Remove(ExtractLogBox.Document.Blocks.FirstBlock);
ExtractLogBox.ScrollToEnd();
}
catch { }
}
private async void StartExtractButton_Click(object sender, RoutedEventArgs e)
{
if (_isRunning)
{
Log("Estrazione già in corso");
return;
}
_isRunning = true;
StartExtractButton.IsEnabled = false;
Log("Avvio procedura di estrazione da closed_auctions.php...");
try
{
var scraper = new ClosedAuctionsScraper(null, _statsService, Log);
var closedUrl = "https://it.bidoo.com/closed_auctions.php";
Log($"Scarico: {closedUrl}");
int count = 0;
await foreach (var rec in scraper.ScrapeYieldAsync(closedUrl))
{
// Filter out records without bids info (user requested)
if (!rec.BidsUsed.HasValue)
{
Log($"Scartata asta (mancano puntate): {rec.AuctionUrl} - '{rec.ProductName ?? "?"}'");
continue;
}
// Add and log incrementally so user sees progress
_products.Add(rec);
count++;
Log($"[{count}] {rec.ProductName} | Prezzo: {(rec.FinalPrice.HasValue?rec.FinalPrice.Value.ToString("F2")+"":"--")} | Vincitore: {rec.Winner ?? "--"} | Puntate: {rec.BidsUsed.Value} | URL: {rec.AuctionUrl}");
}
Log($"Estrazione completata: {count} record aggiunti.");
}
catch (Exception ex)
{
Log($"[ERRORE] Estrattore: {ex.Message}");
}
finally
{
_isRunning = false;
StartExtractButton.IsEnabled = true;
}
}
private void CloseButton_Click(object sender, RoutedEventArgs e)
{
this.Close();
}
private async void ExportStatsButton_Click(object sender, RoutedEventArgs e)
{
try
{
Log("Preparazione esportazione statistiche...");
var stats = await _statsService.GetAllStatsAsync();
if (stats == null || stats.Count == 0)
{
Log("Nessuna statistica disponibile da esportare.");
MessageBox.Show(this, "Nessuna statistica disponibile.", "Esporta Statistiche", MessageBoxButton.OK, MessageBoxImage.Information);
return;
}
var dlg = new SaveFileDialog() { Filter = "CSV files|*.csv|All files|*.*", FileName = "auction_stats.csv" };
if (dlg.ShowDialog(this) != true) return;
using var sw = new StreamWriter(dlg.FileName, false, System.Text.Encoding.UTF8);
sw.WriteLine("ProductKey,ProductName,TotalAuctions,AverageBidsUsed,AverageFinalPrice,LastSeen");
foreach (var s in stats)
{
var line = $"\"{s.ProductKey}\",\"{s.ProductName}\",{s.TotalAuctions},{s.AverageBidsUsed:F2},{s.AverageFinalPrice:F2},{s.LastSeen:O}";
sw.WriteLine(line);
}
Log($"Statistiche esportate su: {dlg.FileName}");
MessageBox.Show(this, "Statistiche esportate con successo.", "Esporta Statistiche", MessageBoxButton.OK, MessageBoxImage.Information);
}
catch (Exception ex)
{
Log($"[ERRORE] Esporta: {ex.Message}");
MessageBox.Show(this, "Errore durante esportazione: " + ex.Message, "Esporta Statistiche", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,8 +1,17 @@
using System.Collections.ObjectModel;
using System;
using System.IO;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using System.Text.Json;
using System.Collections.Generic;
using System.Text;
using System.Net;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Xml.Linq;
using System.Globalization;
using AutoBidder.Models;
using AutoBidder.Services;
using AutoBidder.ViewModels;
@@ -17,9 +26,9 @@ namespace AutoBidder
/// </summary>
public partial class MainWindow : Window
{
// SERVIZI CORE
// SERVIZI CORE
private readonly AuctionMonitor _auctionMonitor;
private readonly ObservableCollection<AuctionViewModel> _auctionViewModels = new();
private readonly System.Collections.ObjectModel.ObservableCollection<AuctionViewModel> _auctionViewModels = new System.Collections.ObjectModel.ObservableCollection<AuctionViewModel>();
// UI State
private AuctionViewModel? _selectedAuction;
private bool _isAutomationActive = false;
@@ -38,6 +47,9 @@ namespace AutoBidder
private System.Windows.Threading.DispatcherTimer _userBannerTimer;
private System.Windows.Threading.DispatcherTimer _userHtmlTimer;
// export cancellation
private CancellationTokenSource? _exportCts;
public MainWindow()
{
InitializeComponent();
@@ -69,6 +81,9 @@ namespace AutoBidder
// Carica aste salvate
LoadSavedAuctions();
// Load export/settings UI
LoadExportSettings();
// Ensure initial global button states (pause/stop disabled until starting)
UpdateGlobalControlButtons();
@@ -82,7 +97,7 @@ namespace AutoBidder
_userBannerTimer.Start();
_ = UpdateUserBannerInfoAsync();
// Timer per aggiornamento dati utente da HTML ogni 3 minuti
// Timer per aggiornamento dati utente da HTML ogni3 minuti
_userHtmlTimer = new System.Windows.Threading.DispatcherTimer();
_userHtmlTimer.Interval = TimeSpan.FromMinutes(3);
_userHtmlTimer.Tick += UserHtmlTimer_Tick;
@@ -90,6 +105,21 @@ namespace AutoBidder
_ = UpdateUserHtmlInfoAsync();
}
// New button handler to open ClosedAuctionsWindow
private void OpenClosedAuctionsButton_Click(object sender, RoutedEventArgs e)
{
try
{
var win = new ClosedAuctionsWindow();
win.Owner = this;
win.ShowDialog();
}
catch (Exception ex)
{
Log($"[ERRORE] Apri Aste Chiuse: {ex.Message}");
}
}
// Command implementations
private void ExecuteStartAll()
{
@@ -349,7 +379,62 @@ namespace AutoBidder
var dialog = new AddAuctionSimpleDialog();
if (dialog.ShowDialog() == true)
{
await AddAuctionById(dialog.AuctionId);
var raw = dialog.AuctionId ?? string.Empty;
// Split by newline, semicolon or whitespace sequences
var parts = System.Text.RegularExpressions.Regex.Split(raw, "[\r\n;]+|\\s+")
.Select(p => p.Trim())
.Where(p => !string.IsNullOrWhiteSpace(p))
.ToList();
if (parts.Count ==0)
{
MessageBox.Show("Nessun URL/ID valido trovato.", "Errore", MessageBoxButton.OK, MessageBoxImage.Warning);
return;
}
var added =0;
var skipped = new List<string>();
foreach (var part in parts)
{
try
{
// If part looks like a plain numeric ID, pass as-is; otherwise pass full URL
var input = part;
// Avoid duplicates: extract auction id if possible
string? aid = null;
if (input.Contains("bidoo.com") || input.StartsWith("http", StringComparison.OrdinalIgnoreCase))
{
aid = ExtractAuctionId(input);
}
else
{
// treat as ID
aid = input;
}
if (!string.IsNullOrEmpty(aid) && _auctionViewModels.Any(a => a.AuctionId == aid))
{
skipped.Add(part + " (duplicato)");
continue;
}
await AddAuctionById(input);
added++;
}
catch (Exception ex)
{
skipped.Add(part + " (errore: " + ex.Message + ")");
}
}
UpdateGlobalControlButtons();
var summary = $"Aggiunte: {added}. Skipped: {skipped.Count}.";
if (skipped.Count >0)
summary += "\nDettagli: " + string.Join("; ", skipped.Take(10));
MessageBox.Show(summary, "Aggiunta aste", MessageBoxButton.OK, MessageBoxImage.Information);
}
}
@@ -410,7 +495,7 @@ namespace AutoBidder
{
if (_selectedAuction != null && sender is TextBox tb)
{
if (int.TryParse(tb.Text, out var value) && value >= 0 && value <= 8)
if (int.TryParse(tb.Text, out var value) && value >=0 && value <=8)
{
_selectedAuction.TimerClick = value;
}
@@ -421,7 +506,7 @@ namespace AutoBidder
{
if (_selectedAuction != null && sender is TextBox tb)
{
if (int.TryParse(tb.Text, out var value) && value >= 0)
if (int.TryParse(tb.Text, out var value) && value >=0)
{
_selectedAuction.AuctionInfo.DelayMs = value;
}
@@ -456,7 +541,7 @@ namespace AutoBidder
{
if (_selectedAuction != null && sender is TextBox tb)
{
if (int.TryParse(tb.Text, out var value) && value >= 0)
if (int.TryParse(tb.Text, out var value) && value >=0)
{
_selectedAuction.AuctionInfo.MinResets = value;
}
@@ -467,7 +552,7 @@ namespace AutoBidder
{
if (_selectedAuction != null && sender is TextBox tb)
{
if (int.TryParse(tb.Text, out var value) && value >= 0)
if (int.TryParse(tb.Text, out var value) && value >=0)
{
_selectedAuction.AuctionInfo.MaxResets = value;
}
@@ -478,7 +563,7 @@ namespace AutoBidder
{
if (_selectedAuction != null && sender is TextBox tb)
{
if (int.TryParse(tb.Text, out var value) && value >= 0)
if (int.TryParse(tb.Text, out var value) && value >=0)
{
_selectedAuction.MaxClicks = value;
SaveAuctions(); // Persist change immediately
@@ -498,12 +583,12 @@ namespace AutoBidder
if (result == MessageBoxResult.Yes)
{
_selectedAuction.TimerClick = 0;
_selectedAuction.AuctionInfo.DelayMs = 50;
_selectedAuction.MinPrice = 0;
_selectedAuction.MaxPrice = 0;
_selectedAuction.AuctionInfo.MinResets = 0;
_selectedAuction.AuctionInfo.MaxResets = 0;
_selectedAuction.TimerClick =0;
_selectedAuction.AuctionInfo.DelayMs =50;
_selectedAuction.MinPrice =0;
_selectedAuction.MaxPrice =0;
_selectedAuction.AuctionInfo.MinResets =0;
_selectedAuction.AuctionInfo.MaxResets =0;
UpdateSelectedAuctionDetails(_selectedAuction);
Log($"Reset impostazioni: {_selectedAuction.Name}");
@@ -1040,8 +1125,198 @@ namespace AutoBidder
Log($"[ERRORE] Errore salvataggio: {ex.Message}");
}
}
private enum LogLevel { Info, Warn, Error }
// Add LoadExportSettings to initialize UI from saved settings
private void LoadExportSettings()
{
try
{
var s = Utilities.SettingsManager.Load();
if (s != null)
{
ExportPathTextBox.Text = s.ExportPath ?? string.Empty;
if (!string.IsNullOrEmpty(s.LastExportExt))
{
var ext = s.LastExportExt.ToLowerInvariant();
if (ext == ".json") ExtJson.IsChecked = true;
else if (ext == ".xml") ExtXml.IsChecked = true;
else ExtCsv.IsChecked = true;
}
else
{
ExtCsv.IsChecked = true;
}
switch (s.ExportScope)
{
case "Closed": ExportScopeCombo.SelectedIndex =1; break;
case "Unknown": ExportScopeCombo.SelectedIndex =2; break;
default: ExportScopeCombo.SelectedIndex =0; break;
}
IncludeOnlyUsedBids.IsChecked = s.IncludeOnlyUsedBids;
IncludeLogs.IsChecked = s.IncludeLogs;
IncludeUserBids.IsChecked = s.IncludeUserBids;
}
}
catch { }
finally
{
try { ExportProgressBar.Visibility = Visibility.Collapsed; ExportProgressText.Visibility = Visibility.Collapsed; } catch { }
}
}
// Export all (simple async wrapper)
private async void ExportAllButton_Click(object sender, RoutedEventArgs e)
{
try
{
var settings = Utilities.SettingsManager.Load();
string ext = ExtJson.IsChecked == true ? ".json" : ExtXml.IsChecked == true ? ".xml" : ".csv";
var dlg = new Microsoft.Win32.SaveFileDialog() { FileName = "auctions_export" + ext, Filter = "CSV files|*.csv|JSON files|*.json|XML files|*.xml|All files|*.*" };
if (dlg.ShowDialog(this) != true) return;
var path = dlg.FileName;
var all = _auctionMonitor.GetAuctions();
var selection = all.AsEnumerable();
var scope = settings.ExportScope ?? "All";
if (scope == "Closed") selection = selection.Where(a => !a.IsActive);
else if (scope == "Unknown") selection = selection.Where(a => (a.BidHistory == null || a.BidHistory.Count ==0) && (a.BidderStats == null || a.BidderStats.Count ==0));
var list = selection.ToList();
if (list.Count ==0)
{
MessageBox.Show(this, "Nessuna asta da esportare.", "Esporta Aste", MessageBoxButton.OK, MessageBoxImage.Information);
return;
}
ExportProgressBar.Visibility = Visibility.Visible;
ExportProgressText.Visibility = Visibility.Visible;
ExportProgressText.Text = "Esportazione in corso...";
await Task.Run(() =>
{
if (path.EndsWith(".json", StringComparison.OrdinalIgnoreCase))
{
var json = System.Text.Json.JsonSerializer.Serialize(list, new System.Text.Json.JsonSerializerOptions { WriteIndented = true });
System.IO.File.WriteAllText(path, json, System.Text.Encoding.UTF8);
}
else if (path.EndsWith(".xml", StringComparison.OrdinalIgnoreCase))
{
var doc = new XDocument(new XElement("Auctions",
from a in list
select new XElement("Auction",
new XElement("AuctionId", a.AuctionId),
new XElement("Name", a.Name),
new XElement("OriginalUrl", a.OriginalUrl ?? string.Empty)
)
));
doc.Save(path);
}
else
{
// Use existing exporter
CsvExporter.ExportAllAuctions(list, path);
}
});
try { ExportPreferences.SaveLastExportExtension(System.IO.Path.GetExtension(path)); } catch { }
MessageBox.Show(this, "Esportazione completata.", "Esporta Aste", MessageBoxButton.OK, MessageBoxImage.Information);
Log($"[EXPORT] Aste esportate -> {path}");
}
catch (Exception ex)
{
Log($"[ERRORE] Esportazione massiva: {ex.Message}");
MessageBox.Show(this, "Errore durante esportazione: " + ex.Message, "Esporta Aste", MessageBoxButton.OK, MessageBoxImage.Error);
}
finally
{
ExportProgressBar.Visibility = Visibility.Collapsed;
ExportProgressText.Visibility = Visibility.Collapsed;
}
}
// Browser handlers (simple)
private void BrowserBackButton_Click(object sender, RoutedEventArgs e)
{
try { if (EmbeddedWebView?.CoreWebView2 != null && EmbeddedWebView.CoreWebView2.CanGoBack) EmbeddedWebView.CoreWebView2.GoBack(); } catch { }
}
private void BrowserForwardButton_Click(object sender, RoutedEventArgs e)
{
try { if (EmbeddedWebView?.CoreWebView2 != null && EmbeddedWebView.CoreWebView2.CanGoForward) EmbeddedWebView.CoreWebView2.GoForward(); } catch { }
}
private void BrowserRefreshButton_Click(object sender, RoutedEventArgs e)
{
try { EmbeddedWebView?.Reload(); } catch { }
}
private void BrowserHomeButton_Click(object sender, RoutedEventArgs e)
{
try { EmbeddedWebView?.CoreWebView2?.Navigate("https://it.bidoo.com/"); BrowserAddress.Text = "https://it.bidoo.com/"; } catch { }
}
private void BrowserGoButton_Click(object sender, RoutedEventArgs e)
{
try
{
var url = BrowserAddress.Text?.Trim(); if (string.IsNullOrEmpty(url)) return; if (!url.StartsWith("http", StringComparison.OrdinalIgnoreCase)) url = "https://" + url;
EmbeddedWebView?.CoreWebView2?.Navigate(url);
}
catch { }
}
private void BrowserAddAuctionButton_Click(object sender, RoutedEventArgs e)
{
try { var url = BrowserAddress.Text?.Trim() ?? EmbeddedWebView?.Source?.ToString(); if (!string.IsNullOrEmpty(url)) _ = AddAuctionFromUrl(url); } catch { }
}
private void BrowserContext_AddAuction_Click(object sender, RoutedEventArgs e)
{
try { var url = EmbeddedWebView?.Source?.ToString() ?? BrowserAddress.Text; if (!string.IsNullOrEmpty(url)) _ = AddAuctionFromUrl(url); } catch { }
}
private void EmbeddedWebView_NavigationStarting(object sender, Microsoft.Web.WebView2.Core.CoreWebView2NavigationStartingEventArgs e)
{
try { BrowserAddress.Text = e.Uri ?? string.Empty; BrowserAddAuctionButton.IsEnabled = IsValidAuctionUrl(e.Uri ?? string.Empty); } catch { }
}
private void EmbeddedWebView_NavigationCompleted(object sender, Microsoft.Web.WebView2.Core.CoreWebView2NavigationCompletedEventArgs e)
{
try { var uri = EmbeddedWebView?.Source?.ToString() ?? BrowserAddress.Text; BrowserAddress.Text = uri; BrowserAddAuctionButton.IsEnabled = IsValidAuctionUrl(uri); } catch { }
}
private void ExportBrowseButton_Click(object sender, RoutedEventArgs e)
{
var dlg = new Microsoft.Win32.SaveFileDialog() { FileName = "export.csv", Filter = "CSV files|*.csv|All files|*.*" };
if (dlg.ShowDialog(this) == true)
{
ExportPathTextBox.Text = System.IO.Path.GetDirectoryName(dlg.FileName) ?? string.Empty;
}
}
private void SaveSettingsButton_Click(object sender, RoutedEventArgs e)
{
try
{
var s = new Utilities.AppSettings()
{
ExportPath = ExportPathTextBox.Text,
LastExportExt = ExtJson.IsChecked == true ? ".json" : ExtXml.IsChecked == true ? ".xml" : ".csv",
ExportScope = ExportScopeCombo.SelectedIndex ==1 ? "Closed" : ExportScopeCombo.SelectedIndex ==2 ? "Unknown" : "All",
IncludeOnlyUsedBids = IncludeOnlyUsedBids.IsChecked == true,
IncludeLogs = IncludeLogs.IsChecked == true,
IncludeUserBids = IncludeUserBids.IsChecked == true
};
Utilities.SettingsManager.Save(s);
ExportPreferences.SaveLastExportExtension(s.LastExportExt);
MessageBox.Show(this, "Impostazioni salvate.", "Salva", MessageBoxButton.OK, MessageBoxImage.Information);
}
catch (Exception ex)
{
MessageBox.Show(this, "Errore salvataggio impostazioni: " + ex.Message, "Errore", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
private void CancelSettingsButton_Click(object sender, RoutedEventArgs e)
{
LoadExportSettings();
}
private enum LogLevel { Info, Warn, Error }
private void Log(string message, LogLevel level = LogLevel.Info)
{
@@ -1098,6 +1373,8 @@ namespace AutoBidder
base.OnClosed(e);
}
// NOTE: Window_Loaded and ToggleTabsButton handlers were removed because they forced a fixed
// width on `MainTabControl` at runtime, causing the UI to render incorrectly compared to the designer.
// === HANDLER BOTTONI GRIGLIA ===
private void GridOpenAuction_Click(object sender, RoutedEventArgs e)
@@ -1176,7 +1453,7 @@ namespace AutoBidder
private void PauseAllButton_Click(object sender, RoutedEventArgs e)
{
int paused = 0;
int paused =0;
foreach (var vm in _auctionViewModels)
{
if (vm.IsActive && !vm.IsPaused)
@@ -1211,31 +1488,31 @@ namespace AutoBidder
// According to rule: darken if all in same state that matches the button meaning
if (allActive)
{
StartButton.IsEnabled = false; StartButton.Opacity = 0.5;
StartButton.IsEnabled = false; StartButton.Opacity =0.5;
}
else
{
StartButton.IsEnabled = true; StartButton.Opacity = 1.0;
StartButton.IsEnabled = true; StartButton.Opacity =1.0;
}
// PauseAll button: darken if allPaused or allStopped
if (allPaused || allStopped)
{
PauseAllButton.IsEnabled = false; PauseAllButton.Opacity = 0.5;
PauseAllButton.IsEnabled = false; PauseAllButton.Opacity =0.5;
}
else
{
PauseAllButton.IsEnabled = true; PauseAllButton.Opacity = 1.0;
PauseAllButton.IsEnabled = true; PauseAllButton.Opacity =1.0;
}
// Stop button: darken if allStopped
if (allStopped)
{
StopButton.IsEnabled = false; StopButton.Opacity = 0.5;
StopButton.IsEnabled = false; StopButton.Opacity =0.5;
}
else
{
StopButton.IsEnabled = true; StopButton.Opacity = 1.0;
StopButton.IsEnabled = true; StopButton.Opacity =1.0;
}
}
@@ -1249,13 +1526,32 @@ namespace AutoBidder
var info = await _auctionMonitor.GetUserBannerInfoAsync();
if (info != null)
{
BannerAsteDaRiscattare.Text = $"{info.nAsteVinte - info.nAsteConfermate}";
// BannerPuntateBonus.Text = $"Bonus: {info.nPuntateBonus}"; // RIMOSSO
// Map banner info to available UI fields
try
{
// Use nPuntateDaRiscattare if available as remaining bids proxy
if (info.nPuntateDaRiscattare >0)
{
RemainingBidsText.Text = info.nPuntateDaRiscattare.ToString();
}
else if (info.nPuntateBonus >0)
{
RemainingBidsText.Text = info.nPuntateBonus.ToString();
}
else
{
// fallback: show total won minus confirmed if meaningful
if (info.nAsteVinte >= info.nAsteConfermate)
RemainingBidsText.Text = (info.nAsteVinte - info.nAsteConfermate).ToString();
else
RemainingBidsText.Text = "--";
}
}
catch { RemainingBidsText.Text = "--"; }
}
else
{
BannerAsteDaRiscattare.Text = "--";
// BannerPuntateBonus.Text = "Bonus: --"; // RIMOSSO
RemainingBidsText.Text = "--";
}
}
@@ -1286,11 +1582,348 @@ namespace AutoBidder
}
else
{
// Rimuovi newline e taglia a max 20 caratteri
// Rimuovi newline e taglia a max20 caratteri
var clean = username.Replace("\r", "").Replace("\n", "");
UsernameText.Text = clean.Length > 20 ? clean.Substring(0, 20) + "..." : clean;
UsernameText.Text = clean.Length >20 ? clean.Substring(0,20) + "..." : clean;
}
RemainingBidsText.Text = remainingBids.ToString();
}
private void ExportSelectedAuction_Click(object sender, RoutedEventArgs e)
{
if (_selectedAuction == null)
{
MessageBox.Show("Seleziona un'asta da esportare", "Nessuna selezione", MessageBoxButton.OK, MessageBoxImage.Information);
return;
}
try
{
var dlg = new Microsoft.Win32.SaveFileDialog()
{
Filter = "CSV files|*.csv|JSON files|*.json|All files|*.*",
FileName = $"auction_{_selectedAuction.AuctionId}.csv"
};
// Preselect filter based on last saved preference
try
{
var last = ExportPreferences.LoadLastExportExtension();
if (!string.IsNullOrEmpty(last))
{
switch (last.ToLowerInvariant())
{
case ".csv":
dlg.FilterIndex =1; dlg.FileName = $"auction_{_selectedAuction.AuctionId}.csv"; break;
case ".json":
dlg.FilterIndex =2; dlg.FileName = $"auction_{_selectedAuction.AuctionId}.json"; break;
case ".xml":
dlg.FilterIndex =3; dlg.FileName = $"auction_{_selectedAuction.AuctionId}.xml"; break;
default:
dlg.FilterIndex =1; break;
}
}
}
catch { /* ignore preference load errors */ }
if (dlg.ShowDialog(this) != true) return;
var path = dlg.FileName;
if (path.EndsWith(".json", StringComparison.OrdinalIgnoreCase))
{
// Export as JSON
var obj = new
{
AuctionId = _selectedAuction.AuctionId,
Name = _selectedAuction.Name,
OriginalUrl = _selectedAuction.AuctionInfo.OriginalUrl,
MinPrice = _selectedAuction.MinPrice,
MaxPrice = _selectedAuction.MaxPrice,
TimerClick = _selectedAuction.TimerClick,
DelayMs = _selectedAuction.AuctionInfo.DelayMs,
IsActive = _selectedAuction.IsActive,
IsPaused = _selectedAuction.IsPaused,
BidHistory = _selectedAuction.AuctionInfo.BidHistory,
Bidders = _selectedAuction.AuctionInfo.BidderStats.Values.ToList(),
AuctionLog = _selectedAuction.AuctionInfo.AuctionLog.ToList()
};
var json = System.Text.Json.JsonSerializer.Serialize(obj, new System.Text.Json.JsonSerializerOptions { WriteIndented = true });
System.IO.File.WriteAllText(path, json, System.Text.Encoding.UTF8);
}
else if (path.EndsWith(".xml", StringComparison.OrdinalIgnoreCase))
{
// Export as XML
var auctionVm = _selectedAuction;
var ai = auctionVm.AuctionInfo;
var doc = new XDocument(
new XElement("AuctionExport",
new XElement("Metadata",
new XElement("AuctionId", auctionVm.AuctionId),
new XElement("Name", auctionVm.Name),
new XElement("OriginalUrl", ai.OriginalUrl ?? string.Empty),
new XElement("MinPrice", auctionVm.MinPrice),
new XElement("MaxPrice", auctionVm.MaxPrice),
new XElement("TimerClick", auctionVm.TimerClick),
new XElement("DelayMs", ai.DelayMs),
new XElement("IsActive", auctionVm.IsActive),
new XElement("IsPaused", auctionVm.IsPaused)
),
new XElement("FinalPrice", auctionVm.PriceDisplay ?? string.Empty),
new XElement("TotalBids", ai.BidHistory?.Count ??0),
new XElement("Bidders",
from b in ai.BidderStats.Values.Where(x => x.BidCount >0)
select new XElement("Bidder",
new XAttribute("Username", b.Username ?? string.Empty),
new XAttribute("BidCount", b.BidCount),
new XElement("LastBidTime", b.LastBidTimeDisplay ?? string.Empty)
)
),
new XElement("AuctionLog",
from l in ai.AuctionLog
select new XElement("Entry", l)
),
new XElement("BidHistory",
from bh in ai.BidHistory
select new XElement("Entry",
new XElement("Timestamp", bh.Timestamp.ToString("o")),
new XElement("EventType", bh.EventType),
new XElement("Bidder", bh.Bidder),
new XElement("Price", bh.Price.ToString("F2", CultureInfo.InvariantCulture)),
new XElement("Timer", bh.Timer.ToString("F2", CultureInfo.InvariantCulture)),
new XElement("LatencyMs", bh.LatencyMs),
new XElement("Success", bh.Success),
new XElement("Notes", bh.Notes)
)
)
)
);
doc.Save(path);
}
else
{
// Export as CSV (simple flat CSV with key-value pairs and sections)
using var sw = new System.IO.StreamWriter(path, false, System.Text.Encoding.UTF8);
sw.WriteLine("Field,Value");
sw.WriteLine($"AuctionId,{_selectedAuction.AuctionId}");
sw.WriteLine($"Name,\"{EscapeCsv(_selectedAuction.Name)}\"");
sw.WriteLine($"OriginalUrl,\"{EscapeCsv(_selectedAuction.AuctionInfo.OriginalUrl)}\"");
sw.WriteLine($"MinPrice,{_selectedAuction.MinPrice}");
sw.WriteLine($"MaxPrice,{_selectedAuction.MaxPrice}");
sw.WriteLine($"TimerClick,{_selectedAuction.TimerClick}");
sw.WriteLine($"DelayMs,{_selectedAuction.AuctionInfo.DelayMs}");
sw.WriteLine($"IsActive,{_selectedAuction.IsActive}");
sw.WriteLine($"IsPaused,{_selectedAuction.IsPaused}");
sw.WriteLine();
sw.WriteLine("--Auction Log--");
sw.WriteLine("Timestamp,Message");
foreach (var l in _selectedAuction.AuctionInfo.AuctionLog)
{
sw.WriteLine($"\"\",\"{EscapeCsv(l)}\"");
}
sw.WriteLine();
sw.WriteLine("--Bidders--");
sw.WriteLine("Username,BidCount,LastBidTime");
foreach (var b in _selectedAuction.AuctionInfo.BidderStats.Values)
{
sw.WriteLine($"\"{EscapeCsv(b.Username)}\",{b.BidCount},\"{EscapeCsv(b.LastBidTimeDisplay)}\"");
}
sw.WriteLine();
sw.WriteLine("--BidHistory--");
sw.WriteLine("Timestamp,EventType,Bidder,Price,Timer,LatencyMs,Success,Notes");
foreach (var bh in _selectedAuction.AuctionInfo.BidHistory)
{
sw.WriteLine($"\"{EscapeCsv(bh.Timestamp.ToString("o"))}\",{bh.EventType},\"{EscapeCsv(bh.Bidder)}\",{bh.Price:F2},{bh.Timer:F2},{bh.LatencyMs},{bh.Success},\"{EscapeCsv(bh.Notes)}\"");
}
}
// Persist last chosen extension
try
{
var ext = System.IO.Path.GetExtension(path);
if (!string.IsNullOrEmpty(ext)) ExportPreferences.SaveLastExportExtension(ext);
}
catch { }
MessageBox.Show(this, "Esportazione completata.", "Esporta Asta", MessageBoxButton.OK, MessageBoxImage.Information);
Log($"[EXPORT] Asta esportata: {_selectedAuction.Name} -> {dlg.FileName}");
}
catch (Exception ex)
{
Log($"[ERRORE] Esporta asta: {ex.Message}");
MessageBox.Show(this, "Errore durante esportazione: " + ex.Message, "Esporta Asta", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
private static string EscapeCsv(string? v)
{
if (v == null) return string.Empty;
return v.Replace("\"", "\"\"");
}
private void OpenFreeBids_Click(object sender, RoutedEventArgs e)
{
try { MainTabControl.SelectedIndex =1; } catch { }
}
private void ExportSelectionButton_Click(object sender, RoutedEventArgs e)
{
ExportSelectedAuction_Click(sender, e);
}
// New handlers for Statistics tab
private async void LoadClosedAuctionsButton_Click(object sender, RoutedEventArgs e)
{
try
{
StatsStatusText.Text = "Caricamento aste chiuse in corso...";
Log("[STATS] Avvio caricamento aste chiuse...");
// Use the existing ClosedAuctionsScraper service
var scraper = new Services.ClosedAuctionsScraper(null, null, (msg) => Log($"[SCRAPER] {msg}"));
var closedUrl = "https://it.bidoo.com/closed_auctions.php";
var results = new System.Collections.ObjectModel.ObservableCollection<Models.ClosedAuctionRecord>();
await foreach (var rec in scraper.ScrapeYieldAsync(closedUrl))
{
// Filter out records without bids info
if (!rec.BidsUsed.HasValue)
{
continue;
}
results.Add(rec);
StatsStatusText.Text = $"Caricate {results.Count} aste...";
}
StatsDataGrid.ItemsSource = results;
StatsStatusText.Text = $"Totale: {results.Count} aste caricate";
Log($"[STATS] Caricamento completato: {results.Count} aste");
}
catch (Exception ex)
{
Log($"[ERRORE] Caricamento statistiche: {ex.Message}", LogLevel.Error);
StatsStatusText.Text = "Errore nel caricamento";
MessageBox.Show($"Errore: {ex.Message}", "Errore Caricamento", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
private async void ExportStatsButton_Click(object sender, RoutedEventArgs e)
{
try
{
var items = StatsDataGrid.ItemsSource as System.Collections.ObjectModel.ObservableCollection<Models.ClosedAuctionRecord>;
if (items == null || items.Count ==0)
{
MessageBox.Show("Nessuna statistica da esportare. Carica prima le aste chiuse.", "Info", MessageBoxButton.OK, MessageBoxImage.Information);
return;
}
var dlg = new Microsoft.Win32.SaveFileDialog()
{
Filter = "CSV files|*.csv|JSON files|*.json|All files|*.*",
FileName = "closed_auctions_stats.csv"
};
if (dlg.ShowDialog(this) != true) return;
var path = dlg.FileName;
await Task.Run(() =>
{
if (path.EndsWith(".json", StringComparison.OrdinalIgnoreCase))
{
var json = System.Text.Json.JsonSerializer.Serialize(items, new System.Text.Json.JsonSerializerOptions { WriteIndented = true });
System.IO.File.WriteAllText(path, json, System.Text.Encoding.UTF8);
}
else
{
// CSV export
using var sw = new System.IO.StreamWriter(path, false, System.Text.Encoding.UTF8);
sw.WriteLine("ProductName,FinalPrice,Winner,BidsUsed,AuctionUrl");
foreach (var item in items)
{
sw.WriteLine($"\"{EscapeCsv(item.ProductName)}\",{item.FinalPrice:F2},\"{EscapeCsv(item.Winner)}\",{item.BidsUsed},\"{EscapeCsv(item.AuctionUrl)}\"");
}
}
});
MessageBox.Show("Esportazione completata.", "Esporta Statistiche", MessageBoxButton.OK, MessageBoxImage.Information);
Log($"[EXPORT] Statistiche esportate -> {path}");
}
catch (Exception ex)
{
Log($"[ERRORE] Esportazione statistiche: {ex.Message}", LogLevel.Error);
MessageBox.Show($"Errore: {ex.Message}", "Errore Esportazione", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
// New handler for Settings tab
private void SaveCookieButton_Click(object sender, RoutedEventArgs e)
{
try
{
var cookieValue = SettingsCookieTextBox.Text?.Trim();
if (string.IsNullOrWhiteSpace(cookieValue))
{
MessageBox.Show("Inserisci un valore valido per il cookie.", "Errore", MessageBoxButton.OK, MessageBoxImage.Warning);
return;
}
// Initialize session with the cookie
var cookieString = $"__stattrb={cookieValue}";
_auctionMonitor.InitializeSessionWithCookie(cookieString, "");
StartButton.IsEnabled = true;
// Save session securely
var session = _auctionMonitor.GetSession();
SessionManager.SaveSession(session);
Log("Sessione configurata da impostazioni");
Log("Cookie salvato in modo sicuro");
// Update user info
Task.Run(async () =>
{
var userData = await _auctionMonitor.GetUserDataAsync();
if (userData != null)
{
Dispatcher.Invoke(() =>
{
SetUserBanner(userData.Username, userData.RemainingBids);
Log($"[OK] Utente: {userData.Username}, Puntate residue: {userData.RemainingBids}");
});
}
});
MessageBox.Show("Cookie salvato con successo!", "Salva Cookie", MessageBoxButton.OK, MessageBoxImage.Information);
}
catch (Exception ex)
{
Log($"[ERRORE] Salvataggio cookie: {ex.Message}", LogLevel.Error);
MessageBox.Show($"Errore: {ex.Message}", "Errore", MessageBoxButton.OK, MessageBoxImage.Error);
}
}
// Add empty handlers required by XAML (no-op to avoid forcing runtime width changes)
private void Window_Loaded(object sender, RoutedEventArgs e)
{
// Intentionally left empty to allow designer sizing to take precedence at runtime.
}
private void ToggleTabsButton_Checked(object sender, RoutedEventArgs e)
{
// No runtime width adjustments to avoid layout mismatch with designer.
}
private void ToggleTabsButton_Unchecked(object sender, RoutedEventArgs e)
{
// No runtime width adjustments to avoid layout mismatch with designer.
}
}
}

View File

@@ -0,0 +1,15 @@
using System;
namespace AutoBidder.Models
{
public class ClosedAuctionRecord
{
public string? AuctionUrl { get; set; }
public string? ProductName { get; set; }
public double? FinalPrice { get; set; }
public string? Winner { get; set; }
public int? BidsUsed { get; set; }
public DateTime ScrapedAt { get; set; }
public string? Notes { get; set; }
}
}

View File

@@ -0,0 +1,21 @@
using System;
namespace AutoBidder.Models
{
public class ProductStat
{
public int Id { get; set; }
public string ProductKey { get; set; } = string.Empty;
public string ProductName { get; set; } = string.Empty;
public int TotalAuctions { get; set; }
public long TotalBidsUsed { get; set; }
public long TotalFinalPriceCents { get; set; }
public DateTime LastSeen { get; set; }
// Helper properties (not mapped)
public double AverageBidsUsed => TotalAuctions > 0 ? (double)TotalBidsUsed / TotalAuctions : 0.0;
public double AverageFinalPrice => TotalAuctions > 0 ? (double)TotalFinalPriceCents / 100.0 / TotalAuctions : 0.0;
}
}

View File

@@ -0,0 +1,370 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Http;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using AutoBidder.Models;
namespace AutoBidder.Services
{
/// <summary>
/// Semplice scraper che scarica la pagina delle "closed auctions" di Bidoo,
/// estrae i link alle singole aste, visita ciascuna pagina e prova ad estrarre
/// informazioni utili (nome prodotto, prezzo finale, vincitore, puntate usate).
/// Risultato salvato in CSV per analisi statistiche esterne.
///
/// Nota: il parsing è basato su euristiche (regex) per resistere a vari formati HTML.
/// </summary>
public class ClosedAuctionsScraper
{
private readonly HttpClient _http;
private readonly StatsService? _statsService;
private readonly Action<string>? _log;
public ClosedAuctionsScraper(HttpMessageHandler? handler = null, StatsService? statsService = null, Action<string>? log = null)
{
var h = handler ?? new HttpClientHandler
{
AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate | DecompressionMethods.Brotli
};
_http = new HttpClient(h)
{
Timeout = TimeSpan.FromSeconds(10)
};
// Default headers user-like
_http.DefaultRequestHeaders.TryAddWithoutValidation("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/141.0.0.0 Safari/537.36");
_http.DefaultRequestHeaders.TryAddWithoutValidation("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8");
_http.DefaultRequestHeaders.TryAddWithoutValidation("Accept-Language", "it-IT,it;q=0.9,en-US;q=0.8,en;q=0.7");
_statsService = statsService;
_log = log;
}
/// <summary>
/// Scarica la pagina di aste chiuse, estrae i link ed esegue scraping per ogni asta.
/// Salva il risultato in CSV.
/// </summary>
public async Task ScrapeAndSaveCsvAsync(string closedAuctionsUrl, string outputCsvPath)
{
var results = await ScrapeAsync(closedAuctionsUrl);
SaveCsv(results, outputCsvPath);
}
/// <summary>
/// Scarica la pagina di aste chiuse, estrae i link ed esegue scraping per ogni asta.
/// Ritorna la lista dei record (non salva su disco).
/// </summary>
public async Task<List<ClosedAuctionRecord>> ScrapeAsync(string closedAuctionsUrl)
{
var list = new List<ClosedAuctionRecord>();
await foreach (var rec in ScrapeYieldAsync(closedAuctionsUrl))
{
list.Add(rec);
}
return list;
}
/// <summary>
/// Scarica la pagina di aste chiuse e produce i record uno per uno (yield) per permettere aggiornamenti UI incrementali.
/// </summary>
public async IAsyncEnumerable<ClosedAuctionRecord> ScrapeYieldAsync(string closedAuctionsUrl)
{
if (string.IsNullOrWhiteSpace(closedAuctionsUrl)) throw new ArgumentNullException(nameof(closedAuctionsUrl));
var baseUri = new Uri("https://it.bidoo.com/");
_log?.Invoke($"[scraper] Downloading closed auctions page: {closedAuctionsUrl}");
var html = await GetStringAsync(closedAuctionsUrl);
if (html == null)
{
_log?.Invoke("[scraper] ERROR: unable to download closed auctions page");
throw new InvalidOperationException("Impossibile scaricare la pagina delle aste chiuse.");
}
var auctionUrls = ExtractAuctionLinks(html, baseUri).Distinct().ToList();
_log?.Invoke($"[scraper] Found {auctionUrls.Count} auction links on closed auctions page.");
foreach (var auctionUrl in auctionUrls)
{
ClosedAuctionRecord record;
try
{
_log?.Invoke($"[scraper] Fetching auction page: {auctionUrl}");
var contextInfo = ExtractSummaryInfoForUrl(html, auctionUrl);
var auctionHtml = await GetStringAsync(auctionUrl);
if (auctionHtml == null)
{
_log?.Invoke($"[scraper] WARNING: failed to download auction page: {auctionUrl}");
throw new InvalidOperationException("Download auction page failed");
}
var productName = contextInfo?.ProductName ?? ExtractProductNameFromAuctionHtml(auctionHtml);
var finalPrice = contextInfo?.FinalPrice ?? ExtractFinalPriceFromAuctionHtml(auctionHtml);
var winner = contextInfo?.Winner ?? ExtractWinnerFromAuctionHtml(auctionHtml);
var bidsUsed = ExtractBidsUsedFromAuctionHtml(auctionHtml);
_log?.Invoke($"[scraper] Parsed: Name='{productName}', FinalPrice={(finalPrice.HasValue? finalPrice.Value.ToString("F2", CultureInfo.InvariantCulture):"null")}, Winner='{winner}', BidsUsed={(bidsUsed.HasValue?bidsUsed.Value.ToString():"null")}" );
// Ensure HTML entities decoded already by helper methods
record = new ClosedAuctionRecord
{
AuctionUrl = auctionUrl,
ProductName = productName,
FinalPrice = finalPrice,
Winner = winner,
BidsUsed = bidsUsed,
ScrapedAt = DateTime.UtcNow,
Notes = string.Empty
};
// Record stats if service provided (fire-and-forget)
if (_statsService != null)
{
#pragma warning disable CS4014
_statsService.RecordClosedAuctionAsync(record);
#pragma warning restore CS4014
}
}
catch (Exception ex)
{
_log?.Invoke($"[scraper] ERROR parsing auction {auctionUrl}: {ex.Message}");
record = new ClosedAuctionRecord
{
AuctionUrl = auctionUrl,
ProductName = "(parse error)",
FinalPrice = null,
Winner = null,
BidsUsed = null,
ScrapedAt = DateTime.UtcNow,
Notes = ex.Message
};
}
yield return record;
}
}
private async Task<string?> GetStringAsync(string url)
{
try
{
var uri = new Uri(url, UriKind.RelativeOrAbsolute);
if (!uri.IsAbsoluteUri)
{
uri = new Uri(new Uri("https://it.bidoo.com"), url);
}
var req = new HttpRequestMessage(HttpMethod.Get, uri);
req.Headers.TryAddWithoutValidation("Referer", "https://it.bidoo.com/");
var resp = await _http.SendAsync(req);
resp.EnsureSuccessStatusCode();
var txt = await resp.Content.ReadAsStringAsync();
_log?.Invoke($"[scraper] HTTP {resp.StatusCode} {uri}");
return txt;
}
catch (Exception ex)
{
_log?.Invoke($"[scraper] HTTP ERROR fetching {url}: {ex.Message}");
return null;
}
}
private IEnumerable<string> ExtractAuctionLinks(string closedHtml, Uri baseUri)
{
var urls = new List<string>();
// Cerca attributi data-href
var mh = Regex.Matches(closedHtml, "data-href\\s*=\\s*\\\"(?<u>[^\\\"]+)\\\"", RegexOptions.IgnoreCase);
foreach (Match m in mh)
{
var u = m.Groups["u"].Value.Trim();
if (!string.IsNullOrEmpty(u)) urls.Add(ToAbsolute(u, baseUri));
}
// fallback: cerca link a auction.php?a=
var mh2 = Regex.Matches(closedHtml, "href\\s*=\\s*\\\"(?<u>[^\\\"]*auction.php\\?a=[^\\\"]+)\\\"", RegexOptions.IgnoreCase);
foreach (Match m in mh2)
{
var u = m.Groups["u"].Value.Trim();
urls.Add(ToAbsolute(u, baseUri));
}
return urls.Where(u => !string.IsNullOrWhiteSpace(u));
}
private string ToAbsolute(string url, Uri baseUri)
{
try
{
if (url.StartsWith("http://", StringComparison.OrdinalIgnoreCase) || url.StartsWith("https://", StringComparison.OrdinalIgnoreCase))
return url;
if (url.StartsWith("//"))
return "https:" + url;
if (url.StartsWith("/"))
return new Uri(baseUri, url).ToString();
return new Uri(baseUri, "/" + url).ToString();
}
catch
{
return url;
}
}
private (string? ProductName, double? FinalPrice, string? Winner)? ExtractSummaryInfoForUrl(string closedHtml, string auctionUrl)
{
try
{
var idx = closedHtml.IndexOf(auctionUrl, StringComparison.OrdinalIgnoreCase);
if (idx < 0) return null;
var start = Math.Max(0, idx - 800);
var len = Math.Min(2500, closedHtml.Length - start);
var seg = closedHtml.Substring(start, len);
var namePattern1 = "<b[^>]*class=\\\"media-heading\\\"[^>]*>\\s*<a[^>]*>(?<name>[^<]+)</a>";
var namePattern2 = "<span[^>]*class=\\\"media-heading[^\\\"]*\\\"[^>]*>\\s*<a[^>]*>(?<name>[^<]+)</a>";
var nameMatch = Regex.Match(seg, namePattern1, RegexOptions.IgnoreCase);
if (!nameMatch.Success)
{
nameMatch = Regex.Match(seg, namePattern2, RegexOptions.IgnoreCase);
}
var product = nameMatch.Success ? WebUtility.HtmlDecode(nameMatch.Groups["name"].Value).Trim() : null;
var priceMatch = Regex.Match(seg, "<span[^>]*class=\\\"price\\\"[^>]*>(?<p>[0-9.,]+)\\s*€", RegexOptions.IgnoreCase);
double? price = null;
if (priceMatch.Success) price = ParseEuro(priceMatch.Groups["p"].Value);
var winnerPattern1 = "<span[^>]*class=\\\"username\\\"[^>]*>.*?<span[^>]*class=\\\"offer\\\"[^>]*>(?<w>[^<]+)</span>";
var winnerPattern2 = "<span[^>]*class=\\\"mobile_offerer offer\\\"[^>]*>(?<w>[^<]+)</span>";
var winnerMatch = Regex.Match(seg, winnerPattern1, RegexOptions.IgnoreCase | RegexOptions.Singleline);
if (!winnerMatch.Success)
{
winnerMatch = Regex.Match(seg, winnerPattern2, RegexOptions.IgnoreCase);
}
var winner = winnerMatch.Success ? WebUtility.HtmlDecode(winnerMatch.Groups["w"].Value).Trim() : null;
return (product, price, winner);
}
catch
{
return null;
}
}
private string? ExtractProductNameFromAuctionHtml(string? auctionHtml)
{
if (string.IsNullOrEmpty(auctionHtml)) return null;
var content = auctionHtml ?? string.Empty;
var m = Regex.Match(content, "<h1[^>]*>(?<n>.*?)</h1>", RegexOptions.IgnoreCase | RegexOptions.Singleline);
if (m.Success) return WebUtility.HtmlDecode(StripTags(m.Groups["n"].Value)).Trim();
m = Regex.Match(content, "<title[^>]*>(?<t>.*?)</title>", RegexOptions.IgnoreCase | RegexOptions.Singleline);
if (m.Success) return WebUtility.HtmlDecode(StripTags(m.Groups["t"].Value)).Trim();
m = Regex.Match(content, "<b[^>]*class=\\\"media-heading\\\"[^>]*>\\s*<a[^>]*>(?<name>[^<]+)</a>", RegexOptions.IgnoreCase);
if (m.Success) return WebUtility.HtmlDecode(m.Groups["name"].Value).Trim();
return null;
}
private double? ExtractFinalPriceFromAuctionHtml(string? auctionHtml)
{
if (string.IsNullOrEmpty(auctionHtml)) return null;
var content = auctionHtml ?? string.Empty;
var m = Regex.Match(content, "<span[^>]*class=\\\"price\\\"[^>]*>(?<p>[0-9.,]+)\\s*€", RegexOptions.IgnoreCase);
if (m.Success) return ParseEuro(m.Groups["p"].Value);
m = Regex.Match(content, "prez[zo]?[\\\"\\']?[^0-9]{0,30}(?<p>[0-9.,]+)\\s*€", RegexOptions.IgnoreCase);
if (m.Success) return ParseEuro(m.Groups["p"].Value);
m = Regex.Match(content, "([0-9]{1,3}(?:[.,][0-9]{2}))\\s*€", RegexOptions.IgnoreCase);
if (m.Success) return ParseEuro(m.Groups[1].Value);
return null;
}
private string? ExtractWinnerFromAuctionHtml(string? auctionHtml)
{
if (string.IsNullOrEmpty(auctionHtml)) return null;
var content = auctionHtml ?? string.Empty;
var m = Regex.Match(content, "Vincitore[:\\s\\\"]+<[^>]*>(?<w>[^<]+)</", RegexOptions.IgnoreCase);
if (m.Success) return WebUtility.HtmlDecode(m.Groups["w"].Value).Trim();
m = Regex.Match(content, "<span[^>]*class=\\\"username\\\"[^>]*>.*?<span[^>]*class=\\\"offer\\\"[^>]*>(?<w>[^<]+)</span>", RegexOptions.IgnoreCase | RegexOptions.Singleline);
if (m.Success) return WebUtility.HtmlDecode(m.Groups["w"].Value).Trim();
m = Regex.Match(content, "mobile_offerer offer\\\"[^>]*>(?<w>[^<]+)<", RegexOptions.IgnoreCase);
if (m.Success) return WebUtility.HtmlDecode(m.Groups["w"].Value).Trim();
return null;
}
private int? ExtractBidsUsedFromAuctionHtml(string? auctionHtml)
{
if (string.IsNullOrEmpty(auctionHtml)) return null;
var content = auctionHtml ?? string.Empty;
// 1) Look for the explicit bids-used span: <p ...><span>628</span> Puntate utilizzate</p>
var m = Regex.Match(content, "class=\\\"bids-used\\\"[^>]*>[^<]*<span[^>]*>(?<n>[0-9]{1,7})</span>", RegexOptions.IgnoreCase | RegexOptions.Singleline);
if (m.Success && int.TryParse(m.Groups["n"].Value, out var val)) return val;
// 2) Look for numeric followed by 'Puntate utilizzate' or similar
m = Regex.Match(content, "(?<n>[0-9]{1,7})\\s*(?:Puntate utilizzate|Puntate usate|puntate utilizzate|puntate usate|puntate)\\b", RegexOptions.IgnoreCase);
if (m.Success && int.TryParse(m.Groups["n"].Value, out val)) return val;
// 3) Fallbacks used previously
m = Regex.Match(content, "(?<n>[0-9]+)\\s*(?:puntate|Puntate|puntate usate|puntate_usate|pt\\.?|pts)\\b", RegexOptions.IgnoreCase);
if (m.Success && int.TryParse(m.Groups["n"].Value, out val)) return val;
m = Regex.Match(content, "usato[sx]?\\s*(?<n>[0-9]{1,6})\\s*(?:puntate|pts|pt)\\b", RegexOptions.IgnoreCase);
if (m.Success && int.TryParse(m.Groups["n"].Value, out val)) return val;
m = Regex.Match(content, "(Puntate\\s*(?:usate|vinte)?)[^0-9]{0,10}(?<n>[0-9]{1,6})", RegexOptions.IgnoreCase);
if (m.Success && int.TryParse(m.Groups["n"].Value, out val)) return val;
m = Regex.Match(content, "<[^>]*>\\s*(?<n>[0-9]{1,6})\\s*(puntate)\\s*<", RegexOptions.IgnoreCase);
if (m.Success && int.TryParse(m.Groups["n"].Value, out val)) return val;
return null;
}
private double? ParseEuro(string s)
{
if (string.IsNullOrWhiteSpace(s)) return null;
s = s.Trim();
s = s.Replace(".", "").Replace(',', '.');
if (double.TryParse(s, NumberStyles.Any, CultureInfo.InvariantCulture, out var d)) return d;
return null;
}
private string StripTags(string input)
{
return Regex.Replace(input ?? string.Empty, "<.*?>", string.Empty);
}
private void SaveCsv(IEnumerable<Models.ClosedAuctionRecord> data, string filePath)
{
var sb = new StringBuilder();
sb.AppendLine("AuctionUrl,ProductName,FinalPrice,Winner,BidsUsed,ScrapedAt,Notes");
foreach (var r in data)
{
// Escape quotes
string Escape(string? v) => (v ?? string.Empty).Replace("\"", "\"\"");
var finalPrice = r.FinalPrice.HasValue ? r.FinalPrice.Value.ToString("F2", CultureInfo.InvariantCulture) : string.Empty;
var bidsUsed = r.BidsUsed.HasValue ? r.BidsUsed.Value.ToString() : string.Empty;
var line = $"\"{Escape(r.AuctionUrl)}\",\"{Escape(r.ProductName)}\",{finalPrice},\"{Escape(r.Winner)}\",{bidsUsed},\"{r.ScrapedAt:O}\",\"{Escape(r.Notes)}\"";
sb.AppendLine(line);
}
File.WriteAllText(filePath, sb.ToString(), Encoding.UTF8);
}
}
}

View File

@@ -0,0 +1,109 @@
using System;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.EntityFrameworkCore;
using AutoBidder.Data;
using AutoBidder.Models;
using System.Collections.Generic;
namespace AutoBidder.Services
{
public class StatsService
{
private readonly StatisticsContext _ctx;
public StatsService(StatisticsContext ctx)
{
_ctx = ctx;
// Ensure DB created
_ctx.Database.Migrate();
}
private static string NormalizeKey(string? productName, string? auctionUrl)
{
// Prefer auctionUrl numeric id if present
try
{
if (!string.IsNullOrWhiteSpace(auctionUrl))
{
var uri = new Uri(auctionUrl);
// Try regex to find trailing numeric ID
var m = System.Text.RegularExpressions.Regex.Match(uri.AbsolutePath + uri.Query, @"(\d{6,})");
if (m.Success) return m.Groups[1].Value;
}
}
catch { }
if (!string.IsNullOrWhiteSpace(productName))
{
var key = productName.Trim().ToLowerInvariant();
key = System.Text.RegularExpressions.Regex.Replace(key, "[^a-z0-9]+", "_");
return key;
}
return string.Empty;
}
public async Task RecordClosedAuctionAsync(ClosedAuctionRecord rec)
{
if (rec == null) return;
var key = NormalizeKey(rec.ProductName, rec.AuctionUrl);
if (string.IsNullOrWhiteSpace(key)) return;
var stat = await _ctx.ProductStats.FirstOrDefaultAsync(p => p.ProductKey == key);
if (stat == null)
{
stat = new ProductStat
{
ProductKey = key,
ProductName = rec.ProductName ?? "",
TotalAuctions = 0,
TotalBidsUsed = 0,
TotalFinalPriceCents = 0,
LastSeen = DateTime.UtcNow
};
_ctx.ProductStats.Add(stat);
}
stat.TotalAuctions += 1;
stat.TotalBidsUsed += rec.BidsUsed ?? 0;
if (rec.FinalPrice.HasValue)
stat.TotalFinalPriceCents += (long)Math.Round(rec.FinalPrice.Value * 100.0);
stat.LastSeen = DateTime.UtcNow;
await _ctx.SaveChangesAsync();
}
public async Task<ProductStat?> GetStatsForKeyAsync(string productName, string auctionUrl)
{
var key = NormalizeKey(productName, auctionUrl);
if (string.IsNullOrWhiteSpace(key)) return null;
return await _ctx.ProductStats.FirstOrDefaultAsync(p => p.ProductKey == key);
}
public async Task<(int recommendedBids, double recommendedMaxPrice)> GetRecommendationAsync(string productName, string auctionUrl, double userRiskFactor = 1.0)
{
var stat = await GetStatsForKeyAsync(productName, auctionUrl);
if (stat == null || stat.TotalAuctions < 3)
{
return (1, 1.0); // conservative defaults
}
int recBids = (int)Math.Ceiling(stat.AverageBidsUsed * userRiskFactor);
if (recBids < 1) recBids = 1;
// recommended max price: avg * (1 + min(0.2, 1/sqrt(n)))
double factor = 1.0 + Math.Min(0.2, 1.0 / Math.Sqrt(Math.Max(1, stat.TotalAuctions)));
double recPrice = stat.AverageFinalPrice * factor;
return (recBids, recPrice);
}
// New: return all stats for export
public async Task<List<ProductStat>> GetAllStatsAsync()
{
return await _ctx.ProductStats
.OrderByDescending(p => p.LastSeen)
.ToListAsync();
}
}
}

View File

@@ -0,0 +1,41 @@
using System;
using System.IO;
using System.Text.Json;
namespace AutoBidder.Utilities
{
internal static class ExportPreferences
{
private class Prefs { public string? LastExportExt { get; set; } }
private static readonly string _folder = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "AutoBidder");
private static readonly string _file = Path.Combine(_folder, "exportprefs.json");
public static string? LoadLastExportExtension()
{
try
{
if (!File.Exists(_file)) return null;
var txt = File.ReadAllText(_file);
var p = JsonSerializer.Deserialize<Prefs>(txt);
if (p == null || string.IsNullOrEmpty(p.LastExportExt)) return null;
return p.LastExportExt;
}
catch
{
return null;
}
}
public static void SaveLastExportExtension(string? ext)
{
try
{
if (!Directory.Exists(_folder)) Directory.CreateDirectory(_folder);
var p = new Prefs { LastExportExt = ext };
var txt = JsonSerializer.Serialize(p);
File.WriteAllText(_file, txt);
}
catch { }
}
}
}

View File

@@ -0,0 +1,46 @@
using System;
using System.IO;
using System.Text.Json;
namespace AutoBidder.Utilities
{
internal class AppSettings
{
public string? ExportPath { get; set; }
public string? LastExportExt { get; set; }
public string ExportScope { get; set; } = "All"; // All, Closed, Unknown
public bool IncludeOnlyUsedBids { get; set; } = true;
public bool IncludeLogs { get; set; } = false;
public bool IncludeUserBids { get; set; } = false;
}
internal static class SettingsManager
{
private static readonly string _folder = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData), "AutoBidder");
private static readonly string _file = Path.Combine(_folder, "settings.json");
public static AppSettings Load()
{
try
{
if (!File.Exists(_file)) return new AppSettings();
var txt = File.ReadAllText(_file);
var s = JsonSerializer.Deserialize<AppSettings>(txt);
if (s == null) return new AppSettings();
return s;
}
catch { return new AppSettings(); }
}
public static void Save(AppSettings settings)
{
try
{
if (!Directory.Exists(_folder)) Directory.CreateDirectory(_folder);
var txt = JsonSerializer.Serialize(settings, new JsonSerializerOptions { WriteIndented = true });
File.WriteAllText(_file, txt);
}
catch { }
}
}
}