Aggiunta applicazione WPF "AutoBidder"
* Aggiunti `App.xaml` e `App.xaml.cs` per configurare l'app. * Creati `MainWindow.xaml` e `MainWindow.xaml.cs` per UI e logica. * Implementata automazione con `WebView2` per interazioni web. * Aggiunti stili personalizzati per pulsanti e controlli. * Configurato progetto WPF in `.NET 8.0` con supporto WebView2. * Aggiunto file soluzione `Mimante.sln` con configurazioni di build. * Migliorata gestione di timer, prezzo e clic con script JS. * Aggiunta gestione visiva per pulsanti di avvio/arresto. * Creata icona dinamica per la finestra principale.
This commit is contained in:
9
Mimante/App.xaml
Normal file
9
Mimante/App.xaml
Normal file
@@ -0,0 +1,9 @@
|
||||
<Application x:Class="Mimante.App"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:local="clr-namespace:Mimante"
|
||||
StartupUri="MainWindow.xaml">
|
||||
<Application.Resources>
|
||||
|
||||
</Application.Resources>
|
||||
</Application>
|
||||
14
Mimante/App.xaml.cs
Normal file
14
Mimante/App.xaml.cs
Normal file
@@ -0,0 +1,14 @@
|
||||
using System.Configuration;
|
||||
using System.Data;
|
||||
using System.Windows;
|
||||
|
||||
namespace Mimante
|
||||
{
|
||||
/// <summary>
|
||||
/// Interaction logic for App.xaml
|
||||
/// </summary>
|
||||
public partial class App : Application
|
||||
{
|
||||
}
|
||||
|
||||
}
|
||||
10
Mimante/AssemblyInfo.cs
Normal file
10
Mimante/AssemblyInfo.cs
Normal file
@@ -0,0 +1,10 @@
|
||||
using System.Windows;
|
||||
|
||||
[assembly: ThemeInfo(
|
||||
ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located
|
||||
//(used if a resource is not found in the page,
|
||||
// or application resource dictionaries)
|
||||
ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located
|
||||
//(used if a resource is not found in the page,
|
||||
// app, or any theme specific resource dictionaries)
|
||||
)]
|
||||
155
Mimante/MainWindow.xaml
Normal file
155
Mimante/MainWindow.xaml
Normal file
@@ -0,0 +1,155 @@
|
||||
<Window x:Class="AutoBidder.MainWindow"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:local="clr-namespace:AutoBidder"
|
||||
xmlns:wv2="clr-namespace:Microsoft.Web.WebView2.Wpf;assembly=Microsoft.Web.WebView2.Wpf"
|
||||
mc:Ignorable="d"
|
||||
Title="AutoBidder - Bidoo Auto Bid" Height="700" Width="1100" Background="#101219" Foreground="#E6EDF3">
|
||||
<Window.Resources>
|
||||
<Style x:Key="PrimaryButtonStyle" TargetType="Button">
|
||||
<Setter Property="Foreground" Value="White" />
|
||||
<Setter Property="FontWeight" Value="SemiBold" />
|
||||
<Setter Property="Padding" Value="10,6" />
|
||||
<Setter Property="Margin" Value="0,6,0,0" />
|
||||
<Setter Property="BorderThickness" Value="0" />
|
||||
<Setter Property="Height" Value="40" />
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="Button">
|
||||
<Border Background="{TemplateBinding Background}" CornerRadius="8" SnapsToDevicePixels="True">
|
||||
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center" RecognizesAccessKey="True" />
|
||||
</Border>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<Style x:Key="StartButtonStyle" TargetType="Button" BasedOn="{StaticResource PrimaryButtonStyle}">
|
||||
<Setter Property="Background" Value="#16A34A" /> <!-- green -->
|
||||
</Style>
|
||||
<Style x:Key="StopButtonStyle" TargetType="Button" BasedOn="{StaticResource PrimaryButtonStyle}">
|
||||
<Setter Property="Background" Value="#DC2626" /> <!-- red -->
|
||||
<Setter Property="Opacity" Value="0.6" />
|
||||
</Style>
|
||||
<Style x:Key="RefreshButtonStyle" TargetType="Button" BasedOn="{StaticResource PrimaryButtonStyle}">
|
||||
<Setter Property="Background" Value="#0EA5E9" /> <!-- blue -->
|
||||
<Setter Property="Height" Value="30" />
|
||||
<Setter Property="Padding" Value="8,4" />
|
||||
</Style>
|
||||
|
||||
<Style TargetType="TextBlock">
|
||||
<Setter Property="Foreground" Value="#E6EDF3" />
|
||||
</Style>
|
||||
|
||||
<Style x:Key="LogBoxStyle" TargetType="TextBox">
|
||||
<Setter Property="Background" Value="#0F1720" />
|
||||
<Setter Property="Foreground" Value="#E6EDF3" />
|
||||
<Setter Property="BorderBrush" Value="#263143" />
|
||||
<Setter Property="BorderThickness" Value="1" />
|
||||
<Setter Property="Padding" Value="8" />
|
||||
<Setter Property="FontFamily" Value="Consolas" />
|
||||
<Setter Property="FontSize" Value="12" />
|
||||
</Style>
|
||||
|
||||
<Style x:Key="NumericBoxStyle" 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="6" />
|
||||
<Setter Property="Width" Value="80" />
|
||||
<Setter Property="HorizontalContentAlignment" Value="Center" />
|
||||
<Setter Property="FontWeight" Value="SemiBold" />
|
||||
</Style>
|
||||
</Window.Resources>
|
||||
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="360" MinWidth="220" />
|
||||
<ColumnDefinition Width="6" />
|
||||
<ColumnDefinition Width="*" />
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<!-- Left panel: fixed height, resizable width -->
|
||||
<Border Grid.Column="0" Margin="12" Background="#0B1015" CornerRadius="6">
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto" /> <!-- Start -->
|
||||
<RowDefinition Height="Auto" /> <!-- Stop -->
|
||||
<RowDefinition Height="Auto" /> <!-- Back/Refresh -->
|
||||
<RowDefinition Height="Auto" /> <!-- Site -->
|
||||
<RowDefinition Height="Auto" /> <!-- Stats -->
|
||||
<RowDefinition Height="Auto" /> <!-- Settings -->
|
||||
<RowDefinition Height="Auto" /> <!-- Price -->
|
||||
<RowDefinition Height="Auto" /> <!-- Log label -->
|
||||
<RowDefinition Height="*" /> <!-- Log box expands -->
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<Button x:Name="StartButton" Grid.Row="0" Style="{StaticResource StartButtonStyle}" Click="StartButton_Click" Margin="12,12,12,0">Avvia</Button>
|
||||
<Button x:Name="StopButton" Grid.Row="1" Style="{StaticResource StopButtonStyle}" Click="StopButton_Click" IsEnabled="False" Margin="12,8,12,0">Stop</Button>
|
||||
|
||||
<StackPanel Grid.Row="2" Orientation="Horizontal" Margin="12,8,12,0">
|
||||
<Button x:Name="BackButton" Style="{StaticResource RefreshButtonStyle}" Click="BackButton_Click" Width="80" Height="30">Indietro</Button>
|
||||
<Button x:Name="RefreshButton" Style="{StaticResource RefreshButtonStyle}" Click="RefreshButton_Click" Width="80" Height="30" Margin="8,0,0,0">Aggiorna</Button>
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Grid.Row="3" Orientation="Vertical" Margin="12,12,12,0">
|
||||
<TextBlock Text="Sito:" />
|
||||
<TextBlock x:Name="SiteLinkText" Text="https://it.bidoo.com" TextWrapping="Wrap" FontWeight="Bold" />
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Grid.Row="4" Orientation="Horizontal" Margin="12,8,12,0">
|
||||
<TextBlock Text="Auto-click:" Margin="0,0,8,0" />
|
||||
<TextBlock x:Name="ClickCountText" Text="0" FontWeight="Bold" Margin="0,0,12,0" />
|
||||
<TextBlock Text="Resets:" Margin="0,0,8,0" />
|
||||
<TextBlock x:Name="ResetCountText" Text="0" FontWeight="Bold" />
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Grid.Row="5" Orientation="Horizontal" Margin="12,8,12,0">
|
||||
<StackPanel Orientation="Vertical">
|
||||
<TextBlock>Max Clicks</TextBlock>
|
||||
<TextBox x:Name="MaxClicksBox" Style="{StaticResource NumericBoxStyle}" Text="0" ToolTip="0 = nessun limite" />
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Orientation="Vertical" Margin="12,0,0,0">
|
||||
<TextBlock>Max Resets</TextBlock>
|
||||
<TextBox x:Name="MaxResetsBox" Style="{StaticResource NumericBoxStyle}" Text="0" ToolTip="0 = nessun limite" />
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Grid.Row="6" Orientation="Horizontal" Margin="12,8,12,0">
|
||||
<StackPanel Orientation="Vertical">
|
||||
<TextBlock>Min Price</TextBlock>
|
||||
<TextBox x:Name="MinPriceBox" Style="{StaticResource NumericBoxStyle}" Text="0" ToolTip="0 = nessun limite" />
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Orientation="Vertical" Margin="12,0,0,0">
|
||||
<TextBlock>Max Price</TextBlock>
|
||||
<TextBox x:Name="MaxPriceBox" Style="{StaticResource NumericBoxStyle}" Text="0" ToolTip="0 = nessun limite" />
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Orientation="Vertical" Margin="12,0,0,0">
|
||||
<TextBlock>Prezzo corrente</TextBlock>
|
||||
<TextBlock x:Name="CurrentPriceText" Text="-" FontWeight="Bold" />
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
|
||||
<TextBlock Grid.Row="7" Margin="12,12,12,6" Text="Log operazioni:" />
|
||||
|
||||
<TextBox x:Name="LogBox" Grid.Row="8" Margin="12" IsReadOnly="True" VerticalScrollBarVisibility="Auto" TextWrapping="Wrap" AcceptsReturn="True" Style="{StaticResource LogBoxStyle}" />
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!-- GridSplitter to allow resizing the left panel (only width) -->
|
||||
<GridSplitter Grid.Column="1" Width="6" HorizontalAlignment="Center" VerticalAlignment="Stretch" Background="#1f2937" ShowsPreview="True" ResizeDirection="Columns" ResizeBehavior="PreviousAndNext" Cursor="SizeWE" />
|
||||
|
||||
<!-- Right: webview -->
|
||||
<Grid Grid.Column="2">
|
||||
<Border Margin="12" CornerRadius="8" Background="#0B1015">
|
||||
<wv2:WebView2 x:Name="webView" Source="https://it.bidoo.com" Margin="2" />
|
||||
</Border>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Window>
|
||||
636
Mimante/MainWindow.xaml.cs
Normal file
636
Mimante/MainWindow.xaml.cs
Normal file
@@ -0,0 +1,636 @@
|
||||
using System;
|
||||
using System.Text.Json;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows;
|
||||
using System.Windows.Media;
|
||||
using System.Windows.Media.Imaging;
|
||||
using Microsoft.Web.WebView2.Core;
|
||||
|
||||
namespace AutoBidder
|
||||
{
|
||||
/// <summary>
|
||||
/// Interaction logic for MainWindow.xaml
|
||||
/// </summary>
|
||||
public partial class MainWindow : Window
|
||||
{
|
||||
private CancellationTokenSource? _cts;
|
||||
private Task? _automationTask;
|
||||
|
||||
public MainWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
// create a simple programmatic icon (circle with AB) and assign to window
|
||||
try
|
||||
{
|
||||
const int size = 64;
|
||||
var dv = new DrawingVisual();
|
||||
using (var dc = dv.RenderOpen())
|
||||
{
|
||||
dc.DrawRoundedRectangle(new SolidColorBrush(Color.FromRgb(16,163,74)), null, new Rect(0, 0, size, size), 8, 8);
|
||||
var ft = new FormattedText("AB", System.Globalization.CultureInfo.InvariantCulture, FlowDirection.LeftToRight,
|
||||
new Typeface("Segoe UI"), 28, Brushes.White, 1.0);
|
||||
var pt = new Point((size - ft.Width) / 2, (size - ft.Height) / 2 - 2);
|
||||
dc.DrawText(ft, pt);
|
||||
}
|
||||
var rtb = new RenderTargetBitmap(size, size, 96, 96, PixelFormats.Pbgra32);
|
||||
rtb.Render(dv);
|
||||
this.Icon = rtb; // set window and taskbar icon
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignore icon errors
|
||||
}
|
||||
|
||||
// initial visual states: start is primary action
|
||||
StartButton.IsEnabled = true;
|
||||
StartButton.Opacity = 1.0;
|
||||
StopButton.IsEnabled = false;
|
||||
StopButton.Opacity = 0.5;
|
||||
BackButton.IsEnabled = false;
|
||||
|
||||
// navigation completed -> update back button state
|
||||
webView.NavigationCompleted += WebView_NavigationCompleted;
|
||||
}
|
||||
|
||||
private void WebView_NavigationCompleted(object? sender, CoreWebView2NavigationCompletedEventArgs e)
|
||||
{
|
||||
UpdateBackButton();
|
||||
}
|
||||
|
||||
private void UpdateBackButton()
|
||||
{
|
||||
try
|
||||
{
|
||||
Dispatcher.Invoke(() =>
|
||||
{
|
||||
BackButton.IsEnabled = webView.CoreWebView2 != null && webView.CoreWebView2.CanGoBack;
|
||||
try
|
||||
{
|
||||
string? url = null;
|
||||
if (webView.CoreWebView2 != null)
|
||||
{
|
||||
url = webView.CoreWebView2.Source;
|
||||
}
|
||||
else if (webView.Source != null)
|
||||
{
|
||||
url = webView.Source.ToString();
|
||||
}
|
||||
|
||||
if (!string.IsNullOrEmpty(url)) SiteLinkText.Text = url;
|
||||
}
|
||||
catch { }
|
||||
});
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
private async void StartButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
StartButton.IsEnabled = false;
|
||||
StopButton.IsEnabled = true;
|
||||
// visual feedback: running -> stop is primary
|
||||
StartButton.Opacity = 0.5;
|
||||
StopButton.Opacity = 1.0;
|
||||
|
||||
Log("Inizializzazione web...");
|
||||
|
||||
try
|
||||
{
|
||||
// Ensure WebView2 is initialized
|
||||
if (webView.CoreWebView2 == null)
|
||||
{
|
||||
await webView.EnsureCoreWebView2Async();
|
||||
}
|
||||
UpdateBackButton();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
var msg = "Errore inizializzazione WebView2: " + ex.Message;
|
||||
Log(msg);
|
||||
StartButton.IsEnabled = true;
|
||||
StopButton.IsEnabled = false;
|
||||
StartButton.Opacity = 1.0;
|
||||
StopButton.Opacity = 0.5;
|
||||
return;
|
||||
}
|
||||
|
||||
Log("Avviato: WebView inizializzato. Avvio monitoraggio pagina.");
|
||||
|
||||
_cts = new CancellationTokenSource();
|
||||
var token = _cts.Token;
|
||||
_automationTask = Task.Run(async () => await AutomationLoop(token), token);
|
||||
}
|
||||
|
||||
private void StopButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
// visual feedback: stopped -> start is primary
|
||||
StartButton.Opacity = 1.0;
|
||||
StopButton.Opacity = 0.5;
|
||||
StopAutomation("Arrestato dall'utente");
|
||||
}
|
||||
|
||||
private void BackButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (webView.CoreWebView2 == null)
|
||||
{
|
||||
// if not initialized, try ensuring
|
||||
_ = webView.EnsureCoreWebView2Async();
|
||||
}
|
||||
|
||||
if (webView.CoreWebView2 != null && webView.CoreWebView2.CanGoBack)
|
||||
{
|
||||
webView.CoreWebView2.GoBack();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log("Errore navigazione indietro: " + ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
private async void RefreshButton_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (webView.CoreWebView2 == null)
|
||||
{
|
||||
await webView.EnsureCoreWebView2Async();
|
||||
}
|
||||
webView.CoreWebView2?.Reload();
|
||||
Log("Pagina aggiornata");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log("Errore aggiornamento pagina: " + ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
private void StopAutomation(string reason)
|
||||
{
|
||||
try
|
||||
{
|
||||
_cts?.Cancel();
|
||||
}
|
||||
catch { }
|
||||
|
||||
StartButton.Dispatcher.Invoke(() =>
|
||||
{
|
||||
StartButton.IsEnabled = true;
|
||||
StopButton.IsEnabled = false;
|
||||
// reset visual states: start primary
|
||||
StartButton.Opacity = 1.0;
|
||||
StopButton.Opacity = 0.5;
|
||||
});
|
||||
|
||||
Log("STOP: " + reason);
|
||||
}
|
||||
|
||||
private void Log(string message)
|
||||
{
|
||||
var entry = $"{DateTime.Now:HH:mm:ss} - {message}{Environment.NewLine}";
|
||||
try
|
||||
{
|
||||
Dispatcher.Invoke(() =>
|
||||
{
|
||||
if (LogBox != null)
|
||||
{
|
||||
LogBox.AppendText(entry);
|
||||
LogBox.ScrollToEnd();
|
||||
}
|
||||
});
|
||||
}
|
||||
catch
|
||||
{
|
||||
// ignore logging errors
|
||||
}
|
||||
}
|
||||
|
||||
private async Task AutomationLoop(CancellationToken token)
|
||||
{
|
||||
int clickCount = 0;
|
||||
int resetCount = 0;
|
||||
string? previousTimer = null;
|
||||
|
||||
// read limits from UI
|
||||
int maxClicks = 0;
|
||||
int maxResets = 0;
|
||||
double minPrice = 0.0;
|
||||
double maxPrice = double.MaxValue;
|
||||
try
|
||||
{
|
||||
Dispatcher.Invoke(() =>
|
||||
{
|
||||
int.TryParse(MaxClicksBox.Text, out maxClicks);
|
||||
int.TryParse(MaxResetsBox.Text, out maxResets);
|
||||
if (maxClicks <= 0) maxClicks = int.MaxValue;
|
||||
if (maxResets <= 0) maxResets = int.MaxValue;
|
||||
|
||||
// price bounds: 0 = no limit
|
||||
if (!double.TryParse(MinPriceBox.Text.Replace(',', '.'), System.Globalization.NumberStyles.Any, System.Globalization.CultureInfo.InvariantCulture, out minPrice)) minPrice = 0.0;
|
||||
if (!double.TryParse(MaxPriceBox.Text.Replace(',', '.'), System.Globalization.NumberStyles.Any, System.Globalization.CultureInfo.InvariantCulture, out var tmpMax)) tmpMax = 0.0;
|
||||
if (tmpMax <= 0) maxPrice = double.MaxValue; else maxPrice = tmpMax;
|
||||
});
|
||||
}
|
||||
catch { maxClicks = int.MaxValue; maxResets = int.MaxValue; minPrice = 0.0; maxPrice = double.MaxValue; }
|
||||
|
||||
// post-click delay to avoid duplicate actions
|
||||
// increased delay to click as late as possible (milliseconds)
|
||||
const int postClickDelayMs = 1200;
|
||||
|
||||
// Improved JS snippet: find PUNTA anchor/button by class or text, find nearby numeric timer elements (including svg text) and price
|
||||
const string findScript = @"(function(){
|
||||
function isVisible(el){ if(!el) return false; try{ var r=el.getBoundingClientRect(); var s=window.getComputedStyle(el); return r.width>0 && r.height>0 && s.visibility!=='hidden' && s.display!=='none'; }catch(e){ return false; } }
|
||||
|
||||
// find price
|
||||
var priceText = '';
|
||||
try{ var pEl = document.querySelector('.auction-action-price strong, .auction-action-price'); if(pEl) priceText = (pEl.textContent||pEl.innerText||'').trim(); }catch(e){}
|
||||
var priceVal = null;
|
||||
try{ if(priceText){ var p = priceText.replace('€','').replace(/\./g,'').replace(',','.').match(/\d+(?:\.\d+)?/); if(p) priceVal = p[0]; } }catch(e){}
|
||||
|
||||
// Try to find a bid button that might say PUNTA or INIZIA
|
||||
var btn = document.querySelector('a.auction-btn-bid, a.bid-button, .auction-btn-bid');
|
||||
if(btn && isVisible(btn)){
|
||||
var txt = (btn.textContent||btn.innerText||'').trim();
|
||||
if(/\bINIZIA\b/i.test(txt)){
|
||||
return JSON.stringify({status:'soon', debug: txt, price: priceVal});
|
||||
}
|
||||
if(/\bPUNTA\b/i.test(txt)){
|
||||
// continue to find timer normally
|
||||
}
|
||||
}
|
||||
|
||||
// Prefer direct countdown element by known class
|
||||
try{
|
||||
var direct = document.querySelector('.text-countdown-progressbar');
|
||||
if(direct && isVisible(direct)){
|
||||
var t = (direct.textContent||'').trim();
|
||||
var m = t.match(/\d+/);
|
||||
if(m) return JSON.stringify({status:'found', timer:m[0], price: priceVal, debug: direct.outerHTML});
|
||||
// if no digits, continue to fallback
|
||||
}
|
||||
}catch(err){ /* ignore */ }
|
||||
|
||||
if(!btn || !isVisible(btn)){
|
||||
var candidates = Array.from(document.querySelectorAll('a, button, div, span')).filter(e=> e && (e.innerText||e.textContent) && /\bPUNTA\b/i.test((e.innerText||e.textContent)) && isVisible(e));
|
||||
if(candidates.length>0) btn = candidates[0];
|
||||
}
|
||||
if(!btn) return JSON.stringify({status:'no-button', price: priceVal});
|
||||
|
||||
var btnRect; try{ btnRect = btn.getBoundingClientRect(); }catch(e){ btnRect = {top:0,left:0,right:0,bottom:0,width:0,height:0}; }
|
||||
|
||||
// collect possible numeric-containing elements, including svg text
|
||||
var nodeList = Array.from(document.querySelectorAll('div, span, p, strong, b, i, em, label, small, a, svg text'));
|
||||
var nums = nodeList.map(e=>{ try{return {el:e, text:(e.textContent||'').trim(), rect:e.getBoundingClientRect(), html:(e.outerHTML||'')}; }catch(err){ return null; }}).filter(x=> x && /\d+/.test(x.text)).map(x=>({el:x.el, text:x.text, rect:x.rect, html:x.html}));
|
||||
|
||||
if(nums.length==0) return JSON.stringify({status:'no-timer', price: priceVal});
|
||||
|
||||
// prefer elements that are visually near and above the button
|
||||
function distanceRect(a,b){ var ax=(a.left+a.right)/2, ay=(a.top+a.bottom)/2; var bx=(b.left+b.right)/2, by=(b.top+b.bottom)/2; return Math.hypot(ax-bx, ay-by); }
|
||||
|
||||
nums.sort(function(a,b){
|
||||
var da = distanceRect(a.rect, btnRect);
|
||||
var db = distanceRect(b.rect, btnRect);
|
||||
var ya = btnRect.top - a.rect.bottom;
|
||||
var yb = btnRect.top - b.rect.bottom;
|
||||
var pref = (ya>=0?0:200) - (yb>=0?0:200);
|
||||
return (da - db) + pref;
|
||||
});
|
||||
|
||||
var best = nums[0];
|
||||
var m = (best.text||'').match(/\d+/);
|
||||
if(!m) return JSON.stringify({status:'no-timer-extracted', debug: best.html, price: priceVal});
|
||||
return JSON.stringify({status:'found', timer:m[0], price: priceVal, debug: best.html});
|
||||
})();";
|
||||
|
||||
const string clickScript = @"(function(){
|
||||
var btn = document.querySelector('a.auction-btn-bid, a.bid-button, .auction-btn-bid');
|
||||
if(!btn){ var candidates = Array.from(document.querySelectorAll('a, button, div, span')).filter(e=> e && (e.innerText||e.textContent) && /\bPUNTA\b/i.test((e.innerText||e.textContent))); if(candidates.length>0) btn=candidates[0]; }
|
||||
if(!btn) return 'no-button';
|
||||
try{ btn.click(); return 'clicked'; }catch(e){ try{ var evt = document.createEvent('MouseEvents'); evt.initEvent('click', true, true); btn.dispatchEvent(evt); return 'dispatched'; }catch(ex){ return 'error:'+ (ex && ex.message?ex.message:ex); } }
|
||||
})();";
|
||||
|
||||
try
|
||||
{
|
||||
while (!token.IsCancellationRequested)
|
||||
{
|
||||
string? result = null;
|
||||
try
|
||||
{
|
||||
var op = Dispatcher.InvokeAsync(() => webView.ExecuteScriptAsync(findScript));
|
||||
var innerTask = await op.Task.ConfigureAwait(false);
|
||||
result = await innerTask.ConfigureAwait(false);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
break;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log("Errore JS/interop: " + ex.Message);
|
||||
StopAutomation("Errore JS/interop: " + ex.Message);
|
||||
return;
|
||||
}
|
||||
|
||||
string json = result ?? "";
|
||||
if (json.Length >= 2 && json[0] == '"' && json[^1] == '"')
|
||||
{
|
||||
json = System.Text.Json.JsonSerializer.Deserialize<string>(result) ?? json;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
using var doc = JsonDocument.Parse(json);
|
||||
var root = doc.RootElement;
|
||||
var status = root.GetProperty("status").GetString();
|
||||
|
||||
// price may be present
|
||||
string? priceStr = null;
|
||||
double? priceVal = null;
|
||||
if (root.TryGetProperty("price", out var priceEl) && priceEl.ValueKind != JsonValueKind.Null)
|
||||
{
|
||||
priceStr = priceEl.GetString();
|
||||
if (!string.IsNullOrEmpty(priceStr))
|
||||
{
|
||||
if (double.TryParse(priceStr.Replace(',', '.').Trim(), System.Globalization.NumberStyles.Any, System.Globalization.CultureInfo.InvariantCulture, out var pval))
|
||||
{
|
||||
priceVal = pval;
|
||||
Dispatcher.Invoke(() => CurrentPriceText.Text = pval.ToString("0.##") + " €");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (status == "soon")
|
||||
{
|
||||
Log("Stato: INIZIA TRA POCO. In attesa che appaia il pulsante PUNTA...");
|
||||
// poll until PUNTA button appears (or cancellation)
|
||||
while (!token.IsCancellationRequested)
|
||||
{
|
||||
string? chk = null;
|
||||
try
|
||||
{
|
||||
var op2 = Dispatcher.InvokeAsync(() => webView.ExecuteScriptAsync("(function(){ var b=document.querySelector('a.auction-btn-bid, a.bid-button'); if(!b) return 'no'; var t=(b.textContent||b.innerText||'').trim(); return /PUNTA/i.test(t)?'yes':'no'; })();"));
|
||||
var inner2 = await op2.Task.ConfigureAwait(false);
|
||||
chk = await inner2.ConfigureAwait(false);
|
||||
}
|
||||
catch { chk = null; }
|
||||
|
||||
if (!string.IsNullOrEmpty(chk))
|
||||
{
|
||||
if (chk.Length >= 2 && chk[0] == '"' && chk[^1] == '"') chk = JsonSerializer.Deserialize<string>(chk) ?? chk;
|
||||
if (chk == "yes")
|
||||
{
|
||||
Log("Pulsante PUNTA trovato. Riprendo esecuzione.");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
await Task.Delay(700, token).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
continue; // next main loop iteration
|
||||
}
|
||||
|
||||
if (status == "no-button")
|
||||
{
|
||||
Log("Nessun pulsante PUNTA trovato; arresto");
|
||||
StopAutomation("Nessun pulsante PUNTA trovato; arresto");
|
||||
return;
|
||||
}
|
||||
|
||||
if (status == "no-timer" || status == "no-timer-extracted")
|
||||
{
|
||||
// Pause: wait until timer reappears
|
||||
Log("Timer non trovato: pausa in corso, verrà ripreso quando il timer ricompare");
|
||||
bool resumed = false;
|
||||
while (!token.IsCancellationRequested)
|
||||
{
|
||||
string? pollResult = null;
|
||||
try
|
||||
{
|
||||
var op3 = Dispatcher.InvokeAsync(() => webView.ExecuteScriptAsync(findScript));
|
||||
var inner3 = await op3.Task.ConfigureAwait(false);
|
||||
pollResult = await inner3.ConfigureAwait(false);
|
||||
}
|
||||
catch { pollResult = null; }
|
||||
|
||||
if (!string.IsNullOrEmpty(pollResult))
|
||||
{
|
||||
if (pollResult.Length >= 2 && pollResult[0] == '"' && pollResult[^1] == '"') pollResult = JsonSerializer.Deserialize<string>(pollResult) ?? pollResult;
|
||||
try
|
||||
{
|
||||
using var doc2 = JsonDocument.Parse(pollResult);
|
||||
var st2 = doc2.RootElement.GetProperty("status").GetString();
|
||||
if (st2 == "found" || st2 == "soon")
|
||||
{
|
||||
Log("Timer ricomparso, ripresa.");
|
||||
resumed = true;
|
||||
break;
|
||||
}
|
||||
if (st2 == "no-button")
|
||||
{
|
||||
Log("Nessun pulsante durante pausa; arresto");
|
||||
StopAutomation("Nessun pulsante durante pausa");
|
||||
return;
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
await Task.Delay(800, token).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
if (!resumed) break;
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (status == "found")
|
||||
{
|
||||
var timerValue = root.GetProperty("timer").GetString();
|
||||
|
||||
// only log when timer value actually changes
|
||||
if (timerValue != previousTimer)
|
||||
{
|
||||
Log("Timer rilevato: " + timerValue);
|
||||
|
||||
// detect resets: if price available, enforce min/max bounds
|
||||
try
|
||||
{
|
||||
if (previousTimer != null && int.TryParse(previousTimer, out var prev) && int.TryParse(timerValue, out var curr))
|
||||
{
|
||||
if (curr > prev)
|
||||
{
|
||||
resetCount++;
|
||||
Dispatcher.Invoke(() => ResetCountText.Text = resetCount.ToString());
|
||||
Log("Timer resettato (contatore): " + resetCount);
|
||||
// stop if reached limit
|
||||
if (resetCount >= maxResets)
|
||||
{
|
||||
StopAutomation($"Limite reset raggiunto: {resetCount}");
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
|
||||
previousTimer = timerValue;
|
||||
}
|
||||
|
||||
// price check: if price available, enforce min/max bounds
|
||||
if (priceVal.HasValue)
|
||||
{
|
||||
if (priceVal.Value < minPrice)
|
||||
{
|
||||
Log($"Prezzo {priceVal.Value:0.##}€ sotto limite minimo ({minPrice:0.##}). Pausa.");
|
||||
// wait until price >= minPrice
|
||||
while (!token.IsCancellationRequested)
|
||||
{
|
||||
// poll price only
|
||||
string? pricePoll = null;
|
||||
try
|
||||
{
|
||||
var opP = Dispatcher.InvokeAsync(() => webView.ExecuteScriptAsync(@"(function(){ var p=document.querySelector('.auction-action-price strong, .auction-action-price'); if(!p) return null; var t=(p.textContent||p.innerText||'').trim(); var num = t.replace('€','').replace(/\./g,'').replace(',','.').match(/\d+(?:\.\d+)?/); return num?num[0]:null; })();"));
|
||||
var innerP = await opP.Task.ConfigureAwait(false);
|
||||
pricePoll = await innerP.ConfigureAwait(false);
|
||||
}
|
||||
catch { pricePoll = null; }
|
||||
|
||||
if (!string.IsNullOrEmpty(pricePoll))
|
||||
{
|
||||
if (pricePoll.Length >= 2 && pricePoll[0] == '"' && pricePoll[^1] == '"') pricePoll = JsonSerializer.Deserialize<string>(pricePoll) ?? pricePoll;
|
||||
if (double.TryParse(pricePoll.Replace(',', '.'), System.Globalization.NumberStyles.Any, System.Globalization.CultureInfo.InvariantCulture, out var pp))
|
||||
{
|
||||
Dispatcher.Invoke(() => CurrentPriceText.Text = pp.ToString("0.##") + " €");
|
||||
if (pp >= minPrice)
|
||||
{
|
||||
Log("Prezzo salito sopra il minimo; ripresa esecuzione.");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await Task.Delay(700, token).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
|
||||
if (priceVal.Value > maxPrice)
|
||||
{
|
||||
Log($"Prezzo {priceVal.Value:0.##}€ sopra limite massimo ({maxPrice:0.##}). Pausa.");
|
||||
while (!token.IsCancellationRequested)
|
||||
{
|
||||
string? pricePoll = null;
|
||||
try
|
||||
{
|
||||
var opP = Dispatcher.InvokeAsync(() => webView.ExecuteScriptAsync(@"(function(){ var p=document.querySelector('.auction-action-price strong, .auction-action-price'); if(!p) return null; var t=(p.textContent||p.innerText||'').trim(); var num = t.replace('€','').replace(/\./g,'').replace(',','.').match(/\d+(?:\.\d+)?/); return num?num[0]:null; })();"));
|
||||
var innerP = await opP.Task.ConfigureAwait(false);
|
||||
pricePoll = await innerP.ConfigureAwait(false);
|
||||
}
|
||||
catch { pricePoll = null; }
|
||||
|
||||
if (!string.IsNullOrEmpty(pricePoll))
|
||||
{
|
||||
if (pricePoll.Length >= 2 && pricePoll[0] == '"' && pricePoll[^1] == '"') pricePoll = JsonSerializer.Deserialize<string>(pricePoll) ?? pricePoll;
|
||||
if (double.TryParse(pricePoll.Replace(',', '.'), System.Globalization.NumberStyles.Any, System.Globalization.CultureInfo.InvariantCulture, out var pp))
|
||||
{
|
||||
Dispatcher.Invoke(() => CurrentPriceText.Text = pp.ToString("0.##") + " €");
|
||||
if (pp <= maxPrice)
|
||||
{
|
||||
Log("Prezzo sceso sotto il massimo; ripresa esecuzione.");
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await Task.Delay(700, token).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (timerValue == "0")
|
||||
{
|
||||
// immediate click when timer reaches 0 (extreme test)
|
||||
string clickResult;
|
||||
try
|
||||
{
|
||||
var op2 = Dispatcher.InvokeAsync(() => webView.ExecuteScriptAsync(clickScript));
|
||||
var inner2 = await op2.Task.ConfigureAwait(false);
|
||||
clickResult = await inner2.ConfigureAwait(false);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
break;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log("Errore durante click JS: " + ex.Message);
|
||||
StopAutomation("Errore durante click JS: " + ex.Message);
|
||||
return;
|
||||
}
|
||||
|
||||
if (clickResult.Length >= 2 && clickResult[0] == '"' && clickResult[^1] == '"')
|
||||
{
|
||||
clickResult = JsonSerializer.Deserialize<string>(clickResult) ?? clickResult;
|
||||
}
|
||||
|
||||
// increment click counter and update UI
|
||||
clickCount++;
|
||||
Dispatcher.Invoke(() => ClickCountText.Text = clickCount.ToString());
|
||||
|
||||
Log("Click eseguito: " + clickResult + " (totale: " + clickCount + ")");
|
||||
|
||||
// stop if reached max clicks
|
||||
if (clickCount >= maxClicks)
|
||||
{
|
||||
StopAutomation($"Limite click raggiunto: {clickCount}");
|
||||
return;
|
||||
}
|
||||
|
||||
await Task.Delay(postClickDelayMs, token).ConfigureAwait(false);
|
||||
continue;
|
||||
}
|
||||
|
||||
await Task.Delay(200, token).ConfigureAwait(false);
|
||||
}
|
||||
|
||||
await Task.Delay(200, token).ConfigureAwait(false);
|
||||
}
|
||||
catch (JsonException ex)
|
||||
{
|
||||
Log("Errore parsing JSON: " + ex.Message);
|
||||
await Task.Delay(300, token).ConfigureAwait(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log("Errore loop: " + ex.Message);
|
||||
StopAutomation("Errore loop: " + ex.Message);
|
||||
}
|
||||
finally
|
||||
{
|
||||
StartButton.Dispatcher.Invoke(() =>
|
||||
{
|
||||
StartButton.IsEnabled = true;
|
||||
StopButton.IsEnabled = false;
|
||||
StartButton.Opacity = 1.0;
|
||||
StopButton.Opacity = 0.5;
|
||||
});
|
||||
|
||||
Log("Automazione terminata");
|
||||
}
|
||||
}
|
||||
|
||||
protected override void OnClosed(EventArgs e)
|
||||
{
|
||||
try { _cts?.Cancel(); } catch { }
|
||||
base.OnClosed(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
17
Mimante/Mimante.csproj
Normal file
17
Mimante/Mimante.csproj
Normal file
@@ -0,0 +1,17 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>WinExe</OutputType>
|
||||
<TargetFramework>net8.0-windows</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<UseWPF>true</UseWPF>
|
||||
<AssemblyName>AutoBidder</AssemblyName>
|
||||
<RootNamespace>AutoBidder</RootNamespace>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Web.WebView2" Version="1.0.3530-prerelease" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
25
Mimante/Mimante.sln
Normal file
25
Mimante/Mimante.sln
Normal file
@@ -0,0 +1,25 @@
|
||||
|
||||
Microsoft Visual Studio Solution File, Format Version 12.00
|
||||
# Visual Studio Version 17
|
||||
VisualStudioVersion = 17.14.36511.14 d17.14
|
||||
MinimumVisualStudioVersion = 10.0.40219.1
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Mimante", "Mimante.csproj", "{9BBAEF93-DF66-432C-9349-459E272D6538}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
Release|Any CPU = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||
{9BBAEF93-DF66-432C-9349-459E272D6538}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{9BBAEF93-DF66-432C-9349-459E272D6538}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{9BBAEF93-DF66-432C-9349-459E272D6538}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{9BBAEF93-DF66-432C-9349-459E272D6538}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
EndGlobalSection
|
||||
GlobalSection(ExtensibilityGlobals) = postSolution
|
||||
SolutionGuid = {1C55CA56-D270-4D9A-91DA-410BF131E905}
|
||||
EndGlobalSection
|
||||
EndGlobal
|
||||
Reference in New Issue
Block a user