1258 lines
54 KiB
C#
1258 lines
54 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Data;
|
|
using System.IO;
|
|
using System.Linq;
|
|
using System.Text;
|
|
using System.Text.RegularExpressions;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using System.Windows;
|
|
using System.Collections.ObjectModel;
|
|
using System.Windows.Controls;
|
|
using Microsoft.Web.WebView2.Core;
|
|
|
|
namespace HorseRacingPredictor
|
|
{
|
|
public partial class MainWindow : Window
|
|
{
|
|
private readonly Football.Main _footballManager;
|
|
private HorseRacing.Main _racingManager;
|
|
private DataTable _footballData;
|
|
private DataTable _racingData;
|
|
private CancellationTokenSource _racingCts;
|
|
|
|
// Virtual Football
|
|
private readonly ObservableCollection<VirtualFootball.VirtualMatch> _vfbResults = new ObservableCollection<VirtualFootball.VirtualMatch>();
|
|
|
|
private const string DefaultRacingApiKey = "";
|
|
|
|
public MainWindow()
|
|
{
|
|
InitializeComponent();
|
|
_footballManager = new Football.Main();
|
|
_racingManager = new HorseRacing.Main(DefaultRacingApiKey);
|
|
BuildCountryCheckboxes();
|
|
// Wire preview update events
|
|
txtFbPrefix.TextChanged += (s, e) => UpdateFbPreview();
|
|
txtFbSuffix.TextChanged += (s, e) => UpdateFbPreview();
|
|
chkFbIncludeDate.Checked += (s, e) => UpdateFbPreview();
|
|
chkFbIncludeDate.Unchecked += (s, e) => UpdateFbPreview();
|
|
cmbFbDateFormat.SelectionChanged += (s, e) => UpdateFbPreview();
|
|
cmbFbFormat.SelectionChanged += (s, e) => UpdateFbPreview();
|
|
dpFootball.SelectedDateChanged += (s, e) => UpdateFbPreview();
|
|
|
|
txtRcPrefix.TextChanged += (s, e) => UpdateRcPreview();
|
|
txtRcSuffix.TextChanged += (s, e) => UpdateRcPreview();
|
|
chkRcIncludeDate.Checked += (s, e) => UpdateRcPreview();
|
|
chkRcIncludeDate.Unchecked += (s, e) => UpdateRcPreview();
|
|
cmbRcDateFormat.SelectionChanged += (s, e) => UpdateRcPreview();
|
|
cmbRcFormat.SelectionChanged += (s, e) => UpdateRcPreview();
|
|
}
|
|
|
|
private void dgRacing_AutoGeneratingColumn(object sender, DataGridAutoGeneratingColumnEventArgs e)
|
|
{
|
|
// Hide the "Inizio" column in the racing grid if it appears
|
|
if (e.PropertyName.Equals("Inizio", StringComparison.OrdinalIgnoreCase) || e.Column.Header?.ToString() == "Inizio")
|
|
{
|
|
e.Cancel = true;
|
|
return;
|
|
}
|
|
|
|
// Ensure the row number 'No' column is visible and placed first
|
|
if (e.PropertyName.Equals("No", StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
// make header readable
|
|
e.Column.Header = "No";
|
|
// set width small
|
|
e.Column.Width = 60;
|
|
}
|
|
}
|
|
|
|
private void ExportToJson(DataTable data, string folder, string defaultName, Action<string> setStatus)
|
|
{
|
|
if (data == null || data.Rows.Count == 0) { MessageBox.Show("Nessun dato da esportare.", "Nessun dato", MessageBoxButton.OK, MessageBoxImage.Warning); return; }
|
|
// ensure file name has .json
|
|
defaultName = EnsureFileExtension(string.IsNullOrWhiteSpace(defaultName) ? "export.json" : defaultName, ".json");
|
|
string filePath;
|
|
if (!string.IsNullOrEmpty(folder) && Directory.Exists(folder)) filePath = Path.Combine(folder, defaultName);
|
|
else
|
|
{
|
|
var dlg = new Microsoft.Win32.SaveFileDialog { Filter = "File JSON|*.json", FileName = defaultName, AddExtension = true };
|
|
if (dlg.ShowDialog() != true) return;
|
|
filePath = dlg.FileName;
|
|
}
|
|
|
|
try
|
|
{
|
|
var list = new System.Collections.Generic.List<System.Collections.Generic.Dictionary<string, object>>();
|
|
foreach (DataRow r in data.Rows)
|
|
{
|
|
var dict = new System.Collections.Generic.Dictionary<string, object>();
|
|
foreach (DataColumn c in data.Columns)
|
|
dict[c.ColumnName] = r[c] == DBNull.Value ? null : r[c];
|
|
list.Add(dict);
|
|
}
|
|
var json = System.Text.Json.JsonSerializer.Serialize(list, new System.Text.Json.JsonSerializerOptions { WriteIndented = true });
|
|
File.WriteAllText(filePath, json, Encoding.UTF8);
|
|
setStatus?.Invoke($"JSON esportato: {Path.GetFileName(filePath)}");
|
|
MessageBox.Show($"Esportate {data.Rows.Count} righe in:\n{filePath}", "Esportazione completata", MessageBoxButton.OK, MessageBoxImage.Information);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
MessageBox.Show($"Errore durante l'esportazione JSON:\n{ex.Message}", "Errore", MessageBoxButton.OK, MessageBoxImage.Error);
|
|
}
|
|
finally
|
|
{
|
|
// display count somewhere: update status label if provided
|
|
setStatus?.Invoke($"Esportate {data.Rows.Count} righe");
|
|
}
|
|
}
|
|
|
|
private void ExportToXml(DataTable data, string folder, string defaultName, Action<string> setStatus)
|
|
{
|
|
if (data == null || data.Rows.Count == 0) { MessageBox.Show("Nessun dato da esportare.", "Nessun dato", MessageBoxButton.OK, MessageBoxImage.Warning); return; }
|
|
// ensure file name has .xml
|
|
defaultName = EnsureFileExtension(string.IsNullOrWhiteSpace(defaultName) ? "export.xml" : defaultName, ".xml");
|
|
string filePath;
|
|
if (!string.IsNullOrEmpty(folder) && Directory.Exists(folder)) filePath = Path.Combine(folder, defaultName);
|
|
else
|
|
{
|
|
var dlg = new Microsoft.Win32.SaveFileDialog { Filter = "File XML|*.xml", FileName = defaultName, AddExtension = true };
|
|
if (dlg.ShowDialog() != true) return;
|
|
filePath = dlg.FileName;
|
|
}
|
|
|
|
try
|
|
{
|
|
// Build a simple XML without schema to avoid assembly/type resolution issues
|
|
var doc = new System.Xml.Linq.XDocument();
|
|
var root = new System.Xml.Linq.XElement("Rows");
|
|
foreach (DataRow r in data.Rows)
|
|
{
|
|
var rowEl = new System.Xml.Linq.XElement("Row");
|
|
foreach (DataColumn c in data.Columns)
|
|
{
|
|
var val = r[c] == DBNull.Value ? string.Empty : r[c].ToString();
|
|
rowEl.Add(new System.Xml.Linq.XElement(XmlConvertName(c.ColumnName), val));
|
|
}
|
|
root.Add(rowEl);
|
|
}
|
|
doc.Add(root);
|
|
doc.Save(filePath);
|
|
|
|
setStatus?.Invoke($"XML esportato: {Path.GetFileName(filePath)}");
|
|
MessageBox.Show($"Esportate {data.Rows.Count} righe in:\n{filePath}", "Esportazione completata", MessageBoxButton.OK, MessageBoxImage.Information);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
MessageBox.Show($"Errore durante l'esportazione XML:\n{ex.Message}", "Errore", MessageBoxButton.OK, MessageBoxImage.Error);
|
|
}
|
|
finally
|
|
{
|
|
setStatus?.Invoke($"Esportate {data.Rows.Count} righe");
|
|
}
|
|
}
|
|
|
|
private string XmlConvertName(string name)
|
|
{
|
|
// Ensure XML element name is valid: replace spaces and illegal chars with underscore
|
|
if (string.IsNullOrEmpty(name)) return "Column";
|
|
var sb = new StringBuilder();
|
|
foreach (var ch in name)
|
|
{
|
|
if (char.IsLetterOrDigit(ch) || ch == '_' || ch == '-') sb.Append(ch); else sb.Append('_');
|
|
}
|
|
// cannot start with digit
|
|
if (char.IsDigit(sb[0])) return "C_" + sb.ToString();
|
|
return sb.ToString();
|
|
}
|
|
|
|
// ???????????????????? LIFECYCLE ????????????????????
|
|
|
|
private void Window_Loaded(object sender, RoutedEventArgs e)
|
|
{
|
|
dpFootball.SelectedDate = DateTime.Today;
|
|
dpRacing.SelectedDate = DateTime.Today;
|
|
LoadSettings();
|
|
}
|
|
|
|
// ???????????????????? NAVIGATION ????????????????????
|
|
|
|
private void ShowPage(string name)
|
|
{
|
|
// Guard against UI elements not being initialized (possible when called early)
|
|
if (pageFootball != null) pageFootball.Visibility = name == "football" ? Visibility.Visible : Visibility.Collapsed;
|
|
if (pageRacing != null) pageRacing.Visibility = name == "racing" ? Visibility.Visible : Visibility.Collapsed;
|
|
if (pageSettings != null) pageSettings.Visibility = name == "settings" ? Visibility.Visible : Visibility.Collapsed;
|
|
if (pageVirtualFb != null) pageVirtualFb.Visibility = name == "virtualfb" ? Visibility.Visible : Visibility.Collapsed;
|
|
|
|
// Update title if available
|
|
if (lblTitle != null)
|
|
{
|
|
switch (name)
|
|
{
|
|
case "football": lblTitle.Text = "Calcio"; break;
|
|
case "racing": lblTitle.Text = "Corse Cavalli"; break;
|
|
case "settings": lblTitle.Text = "Impostazioni"; break;
|
|
case "virtualfb": lblTitle.Text = "Calcio Virtuale"; break;
|
|
}
|
|
}
|
|
}
|
|
|
|
private void navFootball_Checked(object sender, RoutedEventArgs e) => ShowPage("football");
|
|
private void navRacing_Checked(object sender, RoutedEventArgs e) => ShowPage("racing");
|
|
private void navSettings_Checked(object sender, RoutedEventArgs e) => ShowPage("settings");
|
|
private bool _vfbInitialized;
|
|
private async void navVirtualFb_Checked(object sender, RoutedEventArgs e)
|
|
{
|
|
ShowPage("virtualfb");
|
|
// Bind results list once
|
|
if (lbVfbResults.ItemsSource == null)
|
|
lbVfbResults.ItemsSource = _vfbResults;
|
|
|
|
if (!_vfbInitialized)
|
|
{
|
|
_vfbInitialized = true;
|
|
try
|
|
{
|
|
// Persistent user-data folder so cookies, localStorage and session survive
|
|
var userDataFolder = Path.Combine(
|
|
Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
|
|
"BettingPredictor", "WebView2Data");
|
|
|
|
var env = await CoreWebView2Environment.CreateAsync(
|
|
browserExecutableFolder: null,
|
|
userDataFolder: userDataFolder,
|
|
options: new CoreWebView2EnvironmentOptions());
|
|
|
|
await wbVirtualFb.EnsureCoreWebView2Async(env);
|
|
|
|
// Match the real Chrome User-Agent from the HAR capture
|
|
wbVirtualFb.CoreWebView2.Settings.UserAgent =
|
|
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " +
|
|
"(KHTML, like Gecko) Chrome/145.0.0.0 Safari/537.36";
|
|
|
|
// Allow everything the SPA needs
|
|
wbVirtualFb.CoreWebView2.Settings.IsScriptEnabled = true;
|
|
wbVirtualFb.CoreWebView2.Settings.IsWebMessageEnabled = true;
|
|
wbVirtualFb.CoreWebView2.Settings.AreDefaultScriptDialogsEnabled = true;
|
|
wbVirtualFb.CoreWebView2.Settings.IsStatusBarEnabled = false;
|
|
|
|
wbVirtualFb.CoreWebView2.Navigate(txtVfbUrl.Text);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
System.Diagnostics.Debug.WriteLine($"[VFB] WebView2 init error: {ex.Message}");
|
|
MessageBox.Show(
|
|
$"Impossibile inizializzare WebView2.\n\n" +
|
|
$"Assicurati che il Microsoft Edge WebView2 Runtime sia installato.\n\n{ex.Message}",
|
|
"Errore WebView2", MessageBoxButton.OK, MessageBoxImage.Warning);
|
|
_vfbInitialized = false;
|
|
}
|
|
}
|
|
}
|
|
|
|
// ???????????????????? FOOTBALL ????????????????????
|
|
|
|
private async void btnDownloadFb_Click(object sender, RoutedEventArgs e)
|
|
{
|
|
var date = dpFootball.SelectedDate ?? DateTime.Today;
|
|
await DownloadFootballAsync(date);
|
|
}
|
|
|
|
private async Task DownloadFootballAsync(DateTime date)
|
|
{
|
|
try
|
|
{
|
|
pbFootball.Value = 0;
|
|
lblStatusFb.Text = "Scaricamento elenco partite…";
|
|
btnDownloadFb.IsEnabled = false;
|
|
dpFootball.IsEnabled = false;
|
|
btnExportFbCsv.IsEnabled = false;
|
|
|
|
var progress = new Progress<int>(v => pbFootball.Value = v);
|
|
var status = new Progress<string>(s => lblStatusFb.Text = s);
|
|
|
|
var table = await Task.Run(() =>
|
|
_footballManager.GetTodayFixtures(date, progress, status));
|
|
|
|
_footballData = table;
|
|
|
|
// Ensure the start time column exists and populate it (no timezone label)
|
|
InjectRomeStartTimeColumn(_footballData, "Inizio");
|
|
|
|
dgFootball.ItemsSource = _footballData?.DefaultView;
|
|
|
|
if (_footballData != null && _footballData.Rows.Count > 0)
|
|
{
|
|
btnExportFbCsv.IsEnabled = true;
|
|
lblStatusFb.Text = $"Scaricate {_footballData.Rows.Count} partite";
|
|
}
|
|
else
|
|
{
|
|
lblStatusFb.Text = "Nessuna partita trovata per la data selezionata";
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
MessageBox.Show($"Errore durante lo scaricamento:\n{ex.Message}",
|
|
"Errore", MessageBoxButton.OK, MessageBoxImage.Error);
|
|
lblStatusFb.Text = "Errore nello scaricamento";
|
|
pbFootball.Value = 0;
|
|
}
|
|
finally
|
|
{
|
|
btnDownloadFb.IsEnabled = true;
|
|
dpFootball.IsEnabled = true;
|
|
}
|
|
}
|
|
|
|
private void btnExportFbCsv_Click(object sender, RoutedEventArgs e)
|
|
{
|
|
var format = (cmbFbFormat?.SelectedItem as ComboBoxItem)?.Content?.ToString() ?? "CSV";
|
|
var filename = BuildFilename(txtFbPrefix?.Text, chkFbIncludeDate?.IsChecked == true ? GetSelectedDateString(cmbFbDateFormat, dpFootball.SelectedDate ?? DateTime.Today) : null, txtFbSuffix?.Text, null, $"Partite_{dpFootball.SelectedDate:yyyy-MM-dd}.{format.ToLower()}");
|
|
filename = EnsureFileExtension(SanitizeFileName(filename), "." + format.ToLower());
|
|
|
|
switch (format.ToUpper())
|
|
{
|
|
case "CSV":
|
|
ExportToCsv(_footballData, txtFbExportPath.Text, filename, s => lblStatusFb.Text = s);
|
|
break;
|
|
case "JSON":
|
|
ExportToJson(_footballData, txtFbExportPath.Text, filename, s => lblStatusFb.Text = s);
|
|
break;
|
|
case "XML":
|
|
ExportToXml(_footballData, txtFbExportPath.Text, filename, s => lblStatusFb.Text = s);
|
|
break;
|
|
default:
|
|
ExportToCsv(_footballData, txtFbExportPath.Text, filename, s => lblStatusFb.Text = s);
|
|
break;
|
|
}
|
|
// show total rows extracted
|
|
lblStatusFb.Text = _footballData == null ? "Nessuna riga" : $"Righe estratte: {_footballData.Rows.Count}";
|
|
}
|
|
|
|
// ???????????????????? HORSE RACING ????????????????????
|
|
|
|
private readonly Dictionary<string, CheckBox> _countryCheckboxes = new Dictionary<string, CheckBox>();
|
|
|
|
private void BuildCountryCheckboxes()
|
|
{
|
|
if (pnlRcCountries == null) return;
|
|
pnlRcCountries.Children.Clear();
|
|
_countryCheckboxes.Clear();
|
|
|
|
var supported = new HashSet<string>(HorseRacing.Main.SupportedCountries);
|
|
|
|
// Header: nazioni con dati
|
|
pnlRcCountries.Children.Add(new TextBlock
|
|
{
|
|
Text = "Con dati disponibili",
|
|
FontSize = 10,
|
|
FontFamily = new System.Windows.Media.FontFamily("Segoe UI Semibold"),
|
|
Foreground = FindResource("BrBlue") as System.Windows.Media.Brush,
|
|
Margin = new Thickness(6, 2, 0, 4)
|
|
});
|
|
|
|
foreach (var code in HorseRacing.Main.SupportedCountries)
|
|
AddCountryCheckbox(code, supported, true);
|
|
|
|
// Separator
|
|
pnlRcCountries.Children.Add(new Border
|
|
{
|
|
Height = 1,
|
|
Background = FindResource("BrBorder") as System.Windows.Media.Brush,
|
|
Margin = new Thickness(4, 6, 4, 6)
|
|
});
|
|
|
|
// Header: catalogo
|
|
pnlRcCountries.Children.Add(new TextBlock
|
|
{
|
|
Text = "Solo catalogo (nessun dato di forma)",
|
|
FontSize = 10,
|
|
Foreground = FindResource("BrOverlay0") as System.Windows.Media.Brush,
|
|
Margin = new Thickness(6, 2, 0, 4)
|
|
});
|
|
|
|
foreach (var code in HorseRacing.Main.AllCountries)
|
|
{
|
|
if (supported.Contains(code)) continue;
|
|
AddCountryCheckbox(code, supported, false);
|
|
}
|
|
}
|
|
|
|
private void AddCountryCheckbox(string code, HashSet<string> supported, bool isSupported)
|
|
{
|
|
string label = HorseRacing.Main.CountryNames.TryGetValue(code, out var name)
|
|
? $"{name} ({code.ToUpper()})"
|
|
: code.ToUpper();
|
|
|
|
var cb = new CheckBox
|
|
{
|
|
Content = label,
|
|
Tag = code,
|
|
IsChecked = false,
|
|
Margin = new Thickness(4, 2, 4, 2),
|
|
FontSize = 12,
|
|
Foreground = isSupported
|
|
? FindResource("BrText") as System.Windows.Media.Brush
|
|
: FindResource("BrOverlay0") as System.Windows.Media.Brush,
|
|
Opacity = isSupported ? 1.0 : 0.7
|
|
};
|
|
cb.Checked += (s, e) => UpdateCountriesSummary();
|
|
cb.Unchecked += (s, e) => UpdateCountriesSummary();
|
|
|
|
_countryCheckboxes[code] = cb;
|
|
pnlRcCountries.Children.Add(cb);
|
|
}
|
|
|
|
private List<string> GetSelectedCountries()
|
|
{
|
|
return _countryCheckboxes
|
|
.Where(kv => kv.Value.IsChecked == true)
|
|
.Select(kv => kv.Key)
|
|
.ToList();
|
|
}
|
|
|
|
private void SetSelectedCountries(IEnumerable<string> codes)
|
|
{
|
|
var set = new HashSet<string>(codes.Select(c => c.Trim().ToLowerInvariant()));
|
|
foreach (var kv in _countryCheckboxes)
|
|
kv.Value.IsChecked = set.Contains(kv.Key);
|
|
UpdateCountriesSummary();
|
|
}
|
|
|
|
private void UpdateCountriesSummary()
|
|
{
|
|
var selected = GetSelectedCountries();
|
|
if (lblRcCountriesSummary != null)
|
|
{
|
|
lblRcCountriesSummary.Text = selected.Count > 0
|
|
? string.Join(", ", selected.Select(c => c.ToUpper()))
|
|
: "Nessuna";
|
|
}
|
|
}
|
|
|
|
private void rbRcSource_Checked(object sender, RoutedEventArgs e)
|
|
{
|
|
// Toggle visibility of API vs CSV controls
|
|
if (dpRacing == null || btnDownloadRc == null || btnBrowseCsvRc == null) return;
|
|
bool isApi = rbRcApi.IsChecked == true;
|
|
dpRacing.Visibility = isApi ? Visibility.Visible : Visibility.Collapsed;
|
|
btnDownloadRc.Visibility = isApi ? Visibility.Visible : Visibility.Collapsed;
|
|
btnBrowseCsvRc.Visibility = isApi ? Visibility.Collapsed : Visibility.Visible;
|
|
}
|
|
|
|
private async void btnDownloadRc_Click(object sender, RoutedEventArgs e)
|
|
{
|
|
await DownloadRacecardsAsync();
|
|
}
|
|
|
|
private void btnBrowseCsvRc_Click(object sender, RoutedEventArgs e)
|
|
{
|
|
using (var dlg = new System.Windows.Forms.FolderBrowserDialog())
|
|
{
|
|
dlg.Description = "Seleziona la cartella con i file CSV Punters";
|
|
if (dlg.ShowDialog() != System.Windows.Forms.DialogResult.OK) return;
|
|
|
|
try
|
|
{
|
|
lblStatusRc.Text = "Caricamento file CSV…";
|
|
pbRacing.Value = 0;
|
|
btnExportRcCsv.IsEnabled = false;
|
|
|
|
var csvFiles = Directory.GetFiles(dlg.SelectedPath, "*.csv", SearchOption.AllDirectories)
|
|
.OrderBy(f => f)
|
|
.ToList();
|
|
|
|
if (csvFiles.Count == 0)
|
|
{
|
|
lblStatusRc.Text = "Nessun file CSV trovato nella cartella selezionata";
|
|
return;
|
|
}
|
|
|
|
// Merge all CSV files into a single DataTable preserving all original columns
|
|
var table = new DataTable();
|
|
table.Columns.Add("Meeting", typeof(string));
|
|
table.Columns.Add("Race", typeof(int));
|
|
|
|
int processed = 0;
|
|
foreach (var file in csvFiles)
|
|
{
|
|
try
|
|
{
|
|
// Extract meeting name and race number from filename pattern YYYYMMDD-meeting-rXX.csv
|
|
string fileName = Path.GetFileNameWithoutExtension(file);
|
|
string meetingName = fileName;
|
|
int raceNumber = 0;
|
|
var m = Regex.Match(Path.GetFileName(file), @"^\d{8}-(.+)-r(\d+)\.csv$", RegexOptions.IgnoreCase);
|
|
if (m.Success)
|
|
{
|
|
meetingName = string.Join(" ", m.Groups[1].Value.Split('-')
|
|
.Select(s => s.Length > 0 ? char.ToUpper(s[0]) + s.Substring(1).ToLower() : s));
|
|
int.TryParse(m.Groups[2].Value, out raceNumber);
|
|
}
|
|
|
|
// Read CSV with simple parser (comma-delimited, first row = header)
|
|
var lines = File.ReadAllLines(file, Encoding.UTF8);
|
|
if (lines.Length < 2) { processed++; continue; }
|
|
|
|
var headers = ParseCsvLine(lines[0]);
|
|
|
|
// Ensure all columns exist in the merged DataTable
|
|
foreach (var h in headers)
|
|
{
|
|
string colName = h.Trim();
|
|
if (string.IsNullOrWhiteSpace(colName)) continue;
|
|
if (!table.Columns.Contains(colName))
|
|
table.Columns.Add(colName, typeof(string));
|
|
}
|
|
|
|
// Parse data rows
|
|
for (int i = 1; i < lines.Length; i++)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(lines[i])) continue;
|
|
var values = ParseCsvLine(lines[i]);
|
|
var row = table.NewRow();
|
|
row["Meeting"] = meetingName;
|
|
row["Race"] = raceNumber;
|
|
for (int c = 0; c < headers.Length && c < values.Length; c++)
|
|
{
|
|
string colName = headers[c].Trim();
|
|
if (string.IsNullOrWhiteSpace(colName)) continue;
|
|
if (table.Columns.Contains(colName))
|
|
row[colName] = values[c]?.Trim() ?? "";
|
|
}
|
|
table.Rows.Add(row);
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
System.Diagnostics.Debug.WriteLine($"Errore CSV {file}: {ex.Message}");
|
|
}
|
|
processed++;
|
|
pbRacing.Value = (int)((double)processed / csvFiles.Count * 100);
|
|
}
|
|
|
|
// Add row numbers
|
|
InjectRowNumbers(table);
|
|
|
|
_racingData = table;
|
|
dgRacing.ItemsSource = _racingData?.DefaultView;
|
|
|
|
if (_racingData.Rows.Count > 0)
|
|
{
|
|
btnExportRcCsv.IsEnabled = true;
|
|
lblStatusRc.Text = $"Caricati {_racingData.Rows.Count} cavalli da {csvFiles.Count} file CSV";
|
|
}
|
|
else
|
|
{
|
|
lblStatusRc.Text = "Nessun cavallo trovato nei file CSV";
|
|
}
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
MessageBox.Show($"Errore durante il caricamento CSV:\n{ex.Message}",
|
|
"Errore", MessageBoxButton.OK, MessageBoxImage.Error);
|
|
lblStatusRc.Text = "Errore nel caricamento CSV";
|
|
pbRacing.Value = 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Parses a single CSV line respecting quoted fields (comma delimiter).
|
|
/// </summary>
|
|
private static string[] ParseCsvLine(string line)
|
|
{
|
|
var fields = new List<string>();
|
|
bool inQuotes = false;
|
|
var sb = new StringBuilder();
|
|
for (int i = 0; i < line.Length; i++)
|
|
{
|
|
char c = line[i];
|
|
if (inQuotes)
|
|
{
|
|
if (c == '"')
|
|
{
|
|
if (i + 1 < line.Length && line[i + 1] == '"')
|
|
{
|
|
sb.Append('"');
|
|
i++; // skip escaped quote
|
|
}
|
|
else
|
|
{
|
|
inQuotes = false;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
sb.Append(c);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (c == '"')
|
|
{
|
|
inQuotes = true;
|
|
}
|
|
else if (c == ',')
|
|
{
|
|
fields.Add(sb.ToString());
|
|
sb.Clear();
|
|
}
|
|
else
|
|
{
|
|
sb.Append(c);
|
|
}
|
|
}
|
|
}
|
|
fields.Add(sb.ToString());
|
|
return fields.ToArray();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Adds a "No" row-number column as the first column in the DataTable.
|
|
/// </summary>
|
|
private static void InjectRowNumbers(DataTable table)
|
|
{
|
|
if (table == null || table.Rows.Count == 0) return;
|
|
if (table.Columns.Contains("No")) table.Columns.Remove("No");
|
|
var col = new DataColumn("No", typeof(int));
|
|
table.Columns.Add(col);
|
|
col.SetOrdinal(0);
|
|
int n = 1;
|
|
foreach (DataRow r in table.Rows)
|
|
r[col] = n++;
|
|
}
|
|
|
|
private async Task DownloadRacecardsAsync()
|
|
{
|
|
// Se e' gia' in corso, annulla
|
|
if (_racingCts != null)
|
|
{
|
|
_racingCts.Cancel();
|
|
_racingCts = null;
|
|
btnDownloadRc.Content = "Scarica Corse";
|
|
lblStatusRc.Text = "Annullato";
|
|
return;
|
|
}
|
|
|
|
_racingCts = new CancellationTokenSource();
|
|
var ct = _racingCts.Token;
|
|
|
|
try
|
|
{
|
|
pbRacing.Value = 0;
|
|
lblStatusRc.Text = "Scaricamento corse da FormFav...";
|
|
btnDownloadRc.Content = "Annulla";
|
|
dpRacing.IsEnabled = false;
|
|
btnExportRcCsv.IsEnabled = false;
|
|
|
|
// Applica impostazioni correnti al manager
|
|
ApplyRacingSettings();
|
|
|
|
var progress = new Progress<int>(v => pbRacing.Value = v);
|
|
var status = new Progress<string>(s => lblStatusRc.Text = s);
|
|
|
|
var date = dpRacing.SelectedDate ?? DateTime.Today;
|
|
|
|
var table = await Task.Run(() =>
|
|
_racingManager.GetAllRacesForDate(date, progress, status, ct), ct);
|
|
|
|
_racingData = table;
|
|
|
|
// Add row numbers
|
|
InjectRowNumbers(_racingData);
|
|
|
|
dgRacing.ItemsSource = _racingData?.DefaultView;
|
|
|
|
if (_racingData != null && _racingData.Rows.Count > 0)
|
|
{
|
|
btnExportRcCsv.IsEnabled = true;
|
|
lblStatusRc.Text = $"Trovati {_racingData.Rows.Count} corridori";
|
|
}
|
|
else
|
|
{
|
|
lblStatusRc.Text = "Nessuna corsa trovata per la data selezionata";
|
|
}
|
|
}
|
|
catch (OperationCanceledException)
|
|
{
|
|
lblStatusRc.Text = "Scaricamento annullato";
|
|
pbRacing.Value = 0;
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
MessageBox.Show($"Errore durante lo scaricamento:\n{ex.Message}",
|
|
"Errore", MessageBoxButton.OK, MessageBoxImage.Error);
|
|
lblStatusRc.Text = "Errore nello scaricamento";
|
|
pbRacing.Value = 0;
|
|
}
|
|
finally
|
|
{
|
|
_racingCts = null;
|
|
btnDownloadRc.Content = "Scarica Corse";
|
|
dpRacing.IsEnabled = true;
|
|
}
|
|
}
|
|
|
|
private void ApplyRacingSettings()
|
|
{
|
|
_racingManager.UpdateApiKey(txtRacingApiKey.Text.Trim());
|
|
|
|
var tz = txtRcTimezone?.Text?.Trim();
|
|
if (!string.IsNullOrEmpty(tz))
|
|
_racingManager.Timezone = tz;
|
|
|
|
var selected = GetSelectedCountries();
|
|
if (selected.Count > 0)
|
|
_racingManager.Countries = selected;
|
|
}
|
|
|
|
private void btnExportRcCsv_Click(object sender, RoutedEventArgs e)
|
|
{
|
|
var rcDate = dpRacing.SelectedDate ?? DateTime.Today;
|
|
var format = (cmbRcFormat?.SelectedItem as ComboBoxItem)?.Content?.ToString() ?? "CSV";
|
|
var defaultName = $"Corse_{rcDate:yyyy-MM-dd}.{format.ToLower()}";
|
|
var filename = BuildFilename(txtRcPrefix?.Text, chkRcIncludeDate?.IsChecked == true ? GetSelectedDateString(cmbRcDateFormat, rcDate) : null, txtRcSuffix?.Text, null, defaultName);
|
|
filename = EnsureFileExtension(SanitizeFileName(filename), "." + format.ToLower());
|
|
|
|
switch (format.ToUpper())
|
|
{
|
|
case "CSV":
|
|
ExportToCsv(_racingData, txtRcExportPath.Text, filename, s => lblStatusRc.Text = s);
|
|
break;
|
|
case "JSON":
|
|
ExportToJson(_racingData, txtRcExportPath.Text, filename, s => lblStatusRc.Text = s);
|
|
break;
|
|
case "XML":
|
|
ExportToXml(_racingData, txtRcExportPath.Text, filename, s => lblStatusRc.Text = s);
|
|
break;
|
|
default:
|
|
ExportToCsv(_racingData, txtRcExportPath.Text, filename, s => lblStatusRc.Text = s);
|
|
break;
|
|
}
|
|
lblStatusRc.Text = _racingData == null ? "Nessuna riga" : $"Righe estratte: {_racingData.Rows.Count}";
|
|
}
|
|
|
|
// ???????????????????? SHARED CSV EXPORT ????????????????????
|
|
|
|
private void ExportToCsv(DataTable data, string folder, string defaultName, Action<string> setStatus)
|
|
{
|
|
if (data == null || data.Rows.Count == 0)
|
|
{
|
|
MessageBox.Show("Nessun dato da esportare.",
|
|
"Nessun dato", MessageBoxButton.OK, MessageBoxImage.Warning);
|
|
return;
|
|
}
|
|
|
|
// Ensure filename has .csv extension
|
|
defaultName = EnsureFileExtension(defaultName, ".csv");
|
|
string filePath;
|
|
if (!string.IsNullOrEmpty(folder) && Directory.Exists(folder))
|
|
{
|
|
filePath = Path.Combine(folder, defaultName);
|
|
}
|
|
else
|
|
{
|
|
var dlg = new Microsoft.Win32.SaveFileDialog
|
|
{
|
|
Filter = "File CSV|*.csv",
|
|
FileName = defaultName,
|
|
AddExtension = true
|
|
};
|
|
if (dlg.ShowDialog() != true) return;
|
|
filePath = dlg.FileName;
|
|
}
|
|
|
|
try
|
|
{
|
|
var sb = new StringBuilder();
|
|
var headers = new string[data.Columns.Count];
|
|
for (int i = 0; i < data.Columns.Count; i++)
|
|
headers[i] = data.Columns[i].ColumnName;
|
|
sb.AppendLine(string.Join(";", headers));
|
|
|
|
foreach (DataRow row in data.Rows)
|
|
{
|
|
var vals = new string[data.Columns.Count];
|
|
for (int i = 0; i < data.Columns.Count; i++)
|
|
{
|
|
var v = row[i]?.ToString() ?? "";
|
|
if (v.Contains(";") || v.Contains("\""))
|
|
v = "\"" + v.Replace("\"", "\"\"") + "\"";
|
|
vals[i] = v;
|
|
}
|
|
sb.AppendLine(string.Join(";", vals));
|
|
}
|
|
|
|
File.WriteAllText(filePath, sb.ToString(), Encoding.UTF8);
|
|
setStatus?.Invoke($"CSV esportato: {Path.GetFileName(filePath)}");
|
|
MessageBox.Show($"Esportate {data.Rows.Count} righe in:\n{filePath}",
|
|
"Esportazione completata", MessageBoxButton.OK, MessageBoxImage.Information);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
MessageBox.Show($"Errore durante l'esportazione CSV:\n{ex.Message}",
|
|
"Errore", MessageBoxButton.OK, MessageBoxImage.Error);
|
|
}
|
|
finally
|
|
{
|
|
setStatus?.Invoke($"Esportate {data.Rows.Count} righe");
|
|
}
|
|
}
|
|
|
|
// ???????????????????? FOLDER BROWSE ????????????????????
|
|
|
|
private void btnBrowseFbExport_Click(object sender, RoutedEventArgs e)
|
|
{
|
|
var path = BrowseFolder("Seleziona la cartella di esportazione per Calcio");
|
|
if (path != null) txtFbExportPath.Text = path;
|
|
UpdateFbPreview();
|
|
}
|
|
|
|
private void btnBrowseRcExport_Click(object sender, RoutedEventArgs e)
|
|
{
|
|
var path = BrowseFolder("Seleziona la cartella di esportazione per Corse Cavalli");
|
|
if (path != null) txtRcExportPath.Text = path;
|
|
UpdateRcPreview();
|
|
}
|
|
|
|
private static string BrowseFolder(string description)
|
|
{
|
|
using (var dlg = new System.Windows.Forms.FolderBrowserDialog())
|
|
{
|
|
dlg.Description = description;
|
|
if (dlg.ShowDialog() == System.Windows.Forms.DialogResult.OK)
|
|
return dlg.SelectedPath;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
// ???????????????????? SETTINGS ????????????????????
|
|
|
|
private string SettingsFilePath =>
|
|
Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "settings.ini");
|
|
|
|
private void LoadSettings()
|
|
{
|
|
try
|
|
{
|
|
txtRacingApiKey.Text = DefaultRacingApiKey;
|
|
|
|
// Default countries
|
|
SetSelectedCountries(new[] { "au", "nz" });
|
|
|
|
if (!File.Exists(SettingsFilePath)) { ApplyRacingSettings(); return; }
|
|
foreach (var line in File.ReadAllLines(SettingsFilePath))
|
|
{
|
|
var idx = line.IndexOf('=');
|
|
if (idx < 0) continue;
|
|
var key = line.Substring(0, idx).Trim();
|
|
var val = line.Substring(idx + 1).Trim();
|
|
|
|
if (key == "ApiKey") txtApiKey.Text = val;
|
|
else if (key == "FbExportPath") txtFbExportPath.Text = val;
|
|
else if (key == "FbPrefix") txtFbPrefix.Text = val;
|
|
else if (key == "FbSuffix") txtFbSuffix.Text = val;
|
|
else if (key == "FbIncludeDate") chkFbIncludeDate.IsChecked = val == "1" || val.Equals("true", StringComparison.OrdinalIgnoreCase);
|
|
else if (key == "FbDateFormat") { try { SetComboBoxSelectionByContent(cmbFbDateFormat, val); } catch { } }
|
|
else if (key == "FbFormat") { try { SetComboBoxSelectionByContent(cmbFbFormat, val); } catch { } }
|
|
else if (key == "RcExportPath") txtRcExportPath.Text = val;
|
|
else if (key == "RcPrefix") txtRcPrefix.Text = val;
|
|
else if (key == "RcSuffix") txtRcSuffix.Text = val;
|
|
else if (key == "RcIncludeDate") chkRcIncludeDate.IsChecked = val == "1" || val.Equals("true", StringComparison.OrdinalIgnoreCase);
|
|
else if (key == "RcDateFormat") { try { SetComboBoxSelectionByContent(cmbRcDateFormat, val); } catch { } }
|
|
else if (key == "RcFormat") { try { SetComboBoxSelectionByContent(cmbRcFormat, val); } catch { } }
|
|
else if (key == "RacingApiKey") txtRacingApiKey.Text = val;
|
|
else if (key == "RcTimezone" && txtRcTimezone != null) txtRcTimezone.Text = val;
|
|
else if (key == "RcCountries")
|
|
{
|
|
var codes = val.Split(new[] { ',', ';', ' ' }, StringSplitOptions.RemoveEmptyEntries);
|
|
SetSelectedCountries(codes);
|
|
}
|
|
}
|
|
|
|
// Update preview UI after loading values
|
|
UpdateFbPreview();
|
|
UpdateRcPreview();
|
|
|
|
ApplyRacingSettings();
|
|
}
|
|
catch { }
|
|
}
|
|
|
|
private void SetComboBoxSelectionByContent(ComboBox combo, string content)
|
|
{
|
|
if (combo == null) return;
|
|
for (int i = 0; i < combo.Items.Count; i++)
|
|
{
|
|
var item = combo.Items[i] as ComboBoxItem;
|
|
if (item != null && string.Equals(item.Content?.ToString(), content, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
combo.SelectedIndex = i;
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
private static string EnsureFileExtension(string fileName, string extension)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(fileName)) return "";
|
|
if (!extension.StartsWith(".")) extension = "." + extension;
|
|
if (fileName.EndsWith(extension, StringComparison.OrdinalIgnoreCase)) return fileName;
|
|
return fileName + extension;
|
|
}
|
|
|
|
private static string BuildFilename(string prefix, string datePart, string suffix, string explicitName, string fallback)
|
|
{
|
|
// If user provided explicit full filename, use it
|
|
if (!string.IsNullOrWhiteSpace(explicitName)) return SanitizeFileName(explicitName.Trim());
|
|
|
|
prefix = prefix ?? "";
|
|
suffix = suffix ?? "";
|
|
// The user may include underscores in prefix/suffix as desired
|
|
|
|
string middle = string.IsNullOrWhiteSpace(datePart) ? "" : datePart;
|
|
|
|
string name = (prefix ?? string.Empty) + middle + (suffix ?? string.Empty);
|
|
if (string.IsNullOrWhiteSpace(name)) return fallback;
|
|
return SanitizeFileName(name);
|
|
}
|
|
|
|
private static string GetSelectedDateString(ComboBox combo, DateTime? date)
|
|
{
|
|
if (date == null) return null;
|
|
var fmt = (combo?.SelectedItem as ComboBoxItem)?.Content?.ToString();
|
|
if (string.IsNullOrWhiteSpace(fmt)) fmt = "yyyy-MM-dd";
|
|
try { return date.Value.ToString(fmt); } catch { return date.Value.ToString("yyyy-MM-dd"); }
|
|
}
|
|
|
|
private static string SanitizeFileName(string name)
|
|
{
|
|
if (string.IsNullOrWhiteSpace(name)) return name;
|
|
var invalid = Path.GetInvalidFileNameChars();
|
|
var sb = new StringBuilder();
|
|
foreach (var ch in name)
|
|
{
|
|
if (Array.IndexOf(invalid, ch) >= 0)
|
|
continue; // remove invalid characters
|
|
sb.Append(ch);
|
|
}
|
|
// trim whitespace
|
|
return sb.ToString().Trim();
|
|
}
|
|
|
|
private void UpdateFbPreview()
|
|
{
|
|
try
|
|
{
|
|
var format = (cmbFbFormat?.SelectedItem as ComboBoxItem)?.Content?.ToString() ?? "CSV";
|
|
var datePart = chkFbIncludeDate?.IsChecked == true ? GetSelectedDateString(cmbFbDateFormat, dpFootball.SelectedDate ?? DateTime.Today) : null;
|
|
var name = BuildFilename(txtFbPrefix?.Text, datePart, txtFbSuffix?.Text, null, $"Partite_{(dpFootball.SelectedDate ?? DateTime.Today):yyyy-MM-dd}.{format.ToLower()}");
|
|
name = SanitizeFileName(name);
|
|
name = EnsureFileExtension(name, "." + format.ToLower());
|
|
if (txtFbPreview != null) txtFbPreview.Text = name;
|
|
}
|
|
catch { }
|
|
}
|
|
|
|
private void UpdateRcPreview()
|
|
{
|
|
try
|
|
{
|
|
var format = (cmbRcFormat?.SelectedItem as ComboBoxItem)?.Content?.ToString() ?? "CSV";
|
|
var rcDate = dpRacing?.SelectedDate ?? DateTime.Today;
|
|
var datePart = chkRcIncludeDate?.IsChecked == true ? GetSelectedDateString(cmbRcDateFormat, rcDate) : null;
|
|
var defaultName = $"Corse_{rcDate:yyyy-MM-dd}.{format.ToLower()}";
|
|
var name = BuildFilename(txtRcPrefix?.Text, datePart, txtRcSuffix?.Text, null, defaultName);
|
|
name = SanitizeFileName(name);
|
|
name = EnsureFileExtension(name, "." + format.ToLower());
|
|
if (txtRcPreview != null) txtRcPreview.Text = name;
|
|
}
|
|
catch { }
|
|
}
|
|
|
|
/// <summary>
|
|
/// Ensure a visible column with the fixture/race start time converted to Rome timezone exists in the DataTable.
|
|
/// It attempts to use a unix timestamp column (unix_ts) or a date column (Data / Ora / date) and adds a formatted column.
|
|
/// </summary>
|
|
private void InjectRomeStartTimeColumn(DataTable table, string columnName)
|
|
{
|
|
if (table == null) return;
|
|
|
|
try
|
|
{
|
|
// If no columnName specified, only add/populate row number column and return
|
|
if (string.IsNullOrWhiteSpace(columnName))
|
|
{
|
|
if (table.Columns.Contains("No")) table.Columns.Remove("No");
|
|
var rowOnly = new DataColumn("No", typeof(int));
|
|
table.Columns.Add(rowOnly);
|
|
rowOnly.SetOrdinal(0);
|
|
int rnOnly = 1;
|
|
foreach (DataRow r in table.Rows)
|
|
{
|
|
try { r[rowOnly] = rnOnly++; } catch { }
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Remove any existing columns with the same names to avoid duplicates
|
|
if (table.Columns.Contains(columnName)) table.Columns.Remove(columnName);
|
|
if (table.Columns.Contains("No")) table.Columns.Remove("No");
|
|
|
|
// Add row number column as first column
|
|
var rowNoCol = new DataColumn("No", typeof(int));
|
|
table.Columns.Add(rowNoCol);
|
|
rowNoCol.SetOrdinal(0);
|
|
|
|
// Add new column as string for formatted display and place it after 'No'
|
|
var col = new DataColumn(columnName, typeof(string));
|
|
table.Columns.Add(col);
|
|
col.SetOrdinal(1);
|
|
|
|
// Populate row numbers immediately
|
|
int rn = 1;
|
|
foreach (DataRow r in table.Rows)
|
|
{
|
|
try { r[rowNoCol] = rn++; } catch { }
|
|
}
|
|
|
|
// Determine timezone for Rome (Windows TZ id). Fallback to UTC+1 offset if not found.
|
|
TimeZoneInfo romeTz = null;
|
|
try
|
|
{
|
|
romeTz = TimeZoneInfo.FindSystemTimeZoneById("W. Europe Standard Time");
|
|
}
|
|
catch
|
|
{
|
|
try { romeTz = TimeZoneInfo.FindSystemTimeZoneById("Central Europe Standard Time"); } catch { romeTz = null; }
|
|
}
|
|
|
|
// Helper to convert DateTimeOffset to Rome local time and format
|
|
Func<DateTimeOffset, string> fmt = dto =>
|
|
{
|
|
DateTimeOffset rome;
|
|
if (romeTz != null)
|
|
rome = TimeZoneInfo.ConvertTime(dto, romeTz);
|
|
else
|
|
rome = dto.ToOffset(TimeSpan.FromHours(1)); // fallback UTC+1
|
|
|
|
return rome.ToString("yyyy-MM-dd HH:mm");
|
|
};
|
|
|
|
// Try unix timestamp first
|
|
if (table.Columns.Contains("unix_ts"))
|
|
{
|
|
foreach (DataRow r in table.Rows)
|
|
{
|
|
try
|
|
{
|
|
var obj = r["unix_ts"];
|
|
if (obj == DBNull.Value) { r[col] = string.Empty; continue; }
|
|
long ts = 0;
|
|
if (obj is long) ts = (long)obj;
|
|
else if (obj is int) ts = Convert.ToInt64(obj);
|
|
else ts = Convert.ToInt64(obj);
|
|
|
|
var dto = DateTimeOffset.FromUnixTimeSeconds(ts);
|
|
r[col] = fmt(dto);
|
|
}
|
|
catch { r[col] = string.Empty; }
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Otherwise try common date columns
|
|
string[] candidates = new[] { "Data / Ora", "date", "Date", "start", "kickoff" };
|
|
foreach (var c in candidates)
|
|
{
|
|
if (!table.Columns.Contains(c)) continue;
|
|
foreach (DataRow r in table.Rows)
|
|
{
|
|
try
|
|
{
|
|
var v = r[c];
|
|
if (v == DBNull.Value) { r[col] = string.Empty; continue; }
|
|
|
|
DateTimeOffset dto;
|
|
if (v is DateTime dt)
|
|
{
|
|
// treat as UTC if unspecified
|
|
dto = new DateTimeOffset(DateTime.SpecifyKind(dt, DateTimeKind.Utc));
|
|
}
|
|
else
|
|
{
|
|
// parse string including offset
|
|
dto = DateTimeOffset.Parse(v.ToString());
|
|
}
|
|
|
|
r[col] = fmt(dto);
|
|
}
|
|
catch { r[col] = string.Empty; }
|
|
}
|
|
return;
|
|
}
|
|
|
|
// If no source found, leave column empty
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
// Non-fatal: log to debug output
|
|
System.Diagnostics.Debug.WriteLine("InjectRomeStartTimeColumn error: " + ex.Message);
|
|
}
|
|
}
|
|
|
|
private void btnSaveSettings_Click(object sender, RoutedEventArgs e)
|
|
{
|
|
try
|
|
{
|
|
var sb = new StringBuilder();
|
|
sb.AppendLine($"ApiKey={txtApiKey.Text.Trim()}");
|
|
sb.AppendLine($"FbExportPath={txtFbExportPath.Text.Trim()}");
|
|
sb.AppendLine($"FbPrefix={txtFbPrefix.Text.Trim()}");
|
|
sb.AppendLine($"FbSuffix={txtFbSuffix.Text.Trim()}");
|
|
sb.AppendLine($"FbIncludeDate={(chkFbIncludeDate.IsChecked==true?"1":"0")}");
|
|
sb.AppendLine($"FbDateFormat={(cmbFbDateFormat?.SelectedItem as ComboBoxItem)?.Content?.ToString() ?? "yyyy-MM-dd"}");
|
|
sb.AppendLine($"FbFormat={(cmbFbFormat?.SelectedItem as ComboBoxItem)?.Content?.ToString() ?? "CSV"}");
|
|
sb.AppendLine($"RcExportPath={txtRcExportPath.Text.Trim()}");
|
|
sb.AppendLine($"RcPrefix={txtRcPrefix.Text.Trim()}");
|
|
sb.AppendLine($"RcSuffix={txtRcSuffix.Text.Trim()}");
|
|
sb.AppendLine($"RcIncludeDate={(chkRcIncludeDate.IsChecked==true?"1":"0")}");
|
|
sb.AppendLine($"RcDateFormat={(cmbRcDateFormat?.SelectedItem as ComboBoxItem)?.Content?.ToString() ?? "yyyy-MM-dd"}");
|
|
sb.AppendLine($"RcFormat={(cmbRcFormat?.SelectedItem as ComboBoxItem)?.Content?.ToString() ?? "CSV"}");
|
|
sb.AppendLine($"RacingApiKey={txtRacingApiKey.Text.Trim()}");
|
|
sb.AppendLine($"RcTimezone={txtRcTimezone?.Text?.Trim() ?? "Australia/Sydney"}");
|
|
sb.AppendLine($"RcCountries={string.Join(",", GetSelectedCountries())}");
|
|
File.WriteAllText(SettingsFilePath, sb.ToString(), Encoding.UTF8);
|
|
|
|
// update previews after save
|
|
UpdateFbPreview();
|
|
UpdateRcPreview();
|
|
|
|
ApplyRacingSettings();
|
|
|
|
MessageBox.Show("Impostazioni salvate con successo.",
|
|
"Salvato", MessageBoxButton.OK, MessageBoxImage.Information);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
MessageBox.Show($"Errore nel salvataggio:\n{ex.Message}",
|
|
"Errore", MessageBoxButton.OK, MessageBoxImage.Error);
|
|
}
|
|
}
|
|
|
|
// ???????????????????????? VIRTUAL FOOTBALL ????????????????????????
|
|
|
|
private void btnVfbNavigate_Click(object sender, RoutedEventArgs e)
|
|
{
|
|
try
|
|
{
|
|
var url = txtVfbUrl.Text?.Trim();
|
|
if (!string.IsNullOrEmpty(url) && wbVirtualFb.CoreWebView2 != null)
|
|
wbVirtualFb.CoreWebView2.Navigate(url);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
System.Diagnostics.Debug.WriteLine($"[VFB] Navigate error: {ex.Message}");
|
|
}
|
|
}
|
|
|
|
private void btnVfbRefresh_Click(object sender, RoutedEventArgs e)
|
|
{
|
|
try { wbVirtualFb.CoreWebView2?.Reload(); }
|
|
catch (Exception ex)
|
|
{
|
|
System.Diagnostics.Debug.WriteLine($"[VFB] Refresh error: {ex.Message}");
|
|
}
|
|
}
|
|
|
|
private void btnVfbAddResult_Click(object sender, RoutedEventArgs e)
|
|
{
|
|
if (!int.TryParse(txtVfbHomeGoals.Text, out int hg)) hg = 0;
|
|
if (!int.TryParse(txtVfbAwayGoals.Text, out int ag)) ag = 0;
|
|
|
|
var match = new VirtualFootball.VirtualMatch
|
|
{
|
|
Time = DateTime.Now.ToString("HH:mm"),
|
|
Home = string.IsNullOrWhiteSpace(txtVfbHome.Text) ? "Casa" : txtVfbHome.Text.Trim(),
|
|
HomeGoals = hg,
|
|
AwayGoals = ag,
|
|
Away = string.IsNullOrWhiteSpace(txtVfbAway.Text) ? "Ospite" : txtVfbAway.Text.Trim()
|
|
};
|
|
|
|
_vfbResults.Insert(0, match);
|
|
|
|
// Reset input
|
|
txtVfbHomeGoals.Text = "0";
|
|
txtVfbAwayGoals.Text = "0";
|
|
|
|
UpdateVfbStats();
|
|
UpdateVfbSuggestion();
|
|
}
|
|
|
|
private void UpdateVfbStats()
|
|
{
|
|
if (_vfbResults.Count == 0) { lblVfbStats.Text = "Nessun dato"; return; }
|
|
|
|
int total = _vfbResults.Count;
|
|
int draws = _vfbResults.Count(m => m.Outcome == "X");
|
|
int home = _vfbResults.Count(m => m.Outcome == "1");
|
|
int away = _vfbResults.Count(m => m.Outcome == "2");
|
|
double drawPct = (double)draws / total * 100;
|
|
double homePct = (double)home / total * 100;
|
|
double awayPct = (double)away / total * 100;
|
|
int totalGoals = _vfbResults.Sum(m => m.HomeGoals + m.AwayGoals);
|
|
double avgGoals = (double)totalGoals / total;
|
|
int over25 = _vfbResults.Count(m => m.HomeGoals + m.AwayGoals > 2);
|
|
|
|
lblVfbStats.Text = $"Partite: {total}\n" +
|
|
$"1: {home} ({homePct:F1}%) X: {draws} ({drawPct:F1}%) 2: {away} ({awayPct:F1}%)\n" +
|
|
$"Media gol: {avgGoals:F1} Over 2.5: {over25} ({(double)over25 / total * 100:F1}%)";
|
|
}
|
|
|
|
private void UpdateVfbSuggestion()
|
|
{
|
|
if (_vfbResults.Count < 5)
|
|
{
|
|
lblVfbSuggestion.Text = "Inserisci almeno 5 risultati";
|
|
return;
|
|
}
|
|
|
|
// Count consecutive non-draw results (most recent first)
|
|
int streak = 0;
|
|
foreach (var m in _vfbResults)
|
|
{
|
|
if (m.Outcome != "X") streak++;
|
|
else break;
|
|
}
|
|
|
|
// Simple strategy: after a long streak without draws, suggest betting on draw
|
|
if (streak >= 8)
|
|
{
|
|
lblVfbSuggestion.Text = $"\u26A0 PUNTA X (pareggio) \u2014 {streak} partite consecutive senza pareggio!\n" +
|
|
"Puntata alta consigliata.";
|
|
}
|
|
else if (streak >= 5)
|
|
{
|
|
lblVfbSuggestion.Text = $"\u2705 Punta X (pareggio) \u2014 {streak} partite senza pareggio.\n" +
|
|
"Puntata media consigliata.";
|
|
}
|
|
else if (streak >= 3)
|
|
{
|
|
lblVfbSuggestion.Text = $"\u23F3 Possibile X \u2014 {streak} partite senza pareggio.\n" +
|
|
"Puntata bassa o attendi.";
|
|
}
|
|
else
|
|
{
|
|
double drawPct = (double)_vfbResults.Count(m => m.Outcome == "X") / _vfbResults.Count * 100;
|
|
lblVfbSuggestion.Text = $"Pareggio recente \u2014 attendi una serie senza X.\n" +
|
|
$"Frequenza X attuale: {drawPct:F1}%";
|
|
}
|
|
}
|
|
}
|
|
}
|