diff --git a/HorseRacingPredictor/HorseRacingPredictor/Football/Database/Fixture.cs b/HorseRacingPredictor/HorseRacingPredictor/Football/Database/Fixture.cs
index 9d18089..fe9d610 100644
--- a/HorseRacingPredictor/HorseRacingPredictor/Football/Database/Fixture.cs
+++ b/HorseRacingPredictor/HorseRacingPredictor/Football/Database/Fixture.cs
@@ -153,6 +153,7 @@ namespace HorseRacingPredictor.Football.Database
l.country AS Paese,
l.name AS Campionato,
f.date AS [Data / Ora],
+ f.timestamp AS unix_ts,
f.status AS Stato,
th.name AS Casa,
ta.name AS Trasferta,
diff --git a/HorseRacingPredictor/HorseRacingPredictor/MainWindow.xaml b/HorseRacingPredictor/HorseRacingPredictor/MainWindow.xaml
index 965ea0c..e2fc556 100644
--- a/HorseRacingPredictor/HorseRacingPredictor/MainWindow.xaml
+++ b/HorseRacingPredictor/HorseRacingPredictor/MainWindow.xaml
@@ -8,9 +8,8 @@
Loaded="Window_Loaded">
-
+
+
#1E1E2E
#181825
#11111B
@@ -41,9 +40,21 @@
-
+
+
+
+
+
+
+
+
+
+
+
-
+
-
+
-
+
-
+
-
+
+
+
+
+
+
+
@@ -313,28 +320,29 @@
-
+
-
+
+ FontSize="13" VerticalContentAlignment="Center" IsHitTestVisible="True"/>
+ Click="btnDownloadFb_Click" IsHitTestVisible="True"/>
+ Click="btnExportFbCsv_Click" IsHitTestVisible="True"/>
+
@@ -352,21 +360,35 @@
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -375,15 +397,13 @@
FontSize="12" Foreground="{StaticResource BrSubtext0}"/>
-
+
-
-
@@ -391,6 +411,39 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Includi data
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -409,12 +462,45 @@
+ Foreground="{StaticResource BrBlue}" Margin="0,32,0,8"/>
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Includi data
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -435,7 +521,7 @@
-
-
-
-
-
-
-
-
- Applicazione per lo scaricamento e l'analisi di dati sportivi
- tramite API esterne. Supporta calcio e corse dei cavalli
- con esportazione in CSV.
-
-
-
-
-
-
-
-
+
-
+
\ No newline at end of file
diff --git a/HorseRacingPredictor/HorseRacingPredictor/MainWindow.xaml.cs b/HorseRacingPredictor/HorseRacingPredictor/MainWindow.xaml.cs
index 1039488..4288566 100644
--- a/HorseRacingPredictor/HorseRacingPredictor/MainWindow.xaml.cs
+++ b/HorseRacingPredictor/HorseRacingPredictor/MainWindow.xaml.cs
@@ -1,7 +1,10 @@
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.Tasks;
using System.Windows;
using System.Windows.Controls;
@@ -23,6 +26,139 @@ namespace HorseRacingPredictor
InitializeComponent();
_footballManager = new Football.Main();
_racingManager = new HorseRacing.Main(DefaultRacingUser, DefaultRacingPass);
+ // 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 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>();
+ foreach (DataRow r in data.Rows)
+ {
+ var dict = new System.Collections.Generic.Dictionary();
+ 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 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 ????????????????????
@@ -44,7 +180,6 @@ namespace HorseRacingPredictor
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 (pageInfo != null) pageInfo.Visibility = name == "info" ? Visibility.Visible : Visibility.Collapsed;
// Update title if available
if (lblTitle != null)
@@ -54,7 +189,6 @@ namespace HorseRacingPredictor
case "football": lblTitle.Text = "Calcio"; break;
case "racing": lblTitle.Text = "Corse Cavalli"; break;
case "settings": lblTitle.Text = "Impostazioni"; break;
- case "info": lblTitle.Text = "Informazioni"; break;
}
}
}
@@ -62,7 +196,6 @@ namespace HorseRacingPredictor
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 void navInfo_Checked(object sender, RoutedEventArgs e) => ShowPage("info");
// ???????????????????? FOOTBALL ????????????????????
@@ -89,6 +222,10 @@ namespace HorseRacingPredictor
_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)
@@ -117,18 +254,224 @@ namespace HorseRacingPredictor
private void btnExportFbCsv_Click(object sender, RoutedEventArgs e)
{
- ExportToCsv(_footballData, txtFbExportPath.Text,
- $"Partite_{dpFootball.SelectedDate:yyyy-MM-dd}.csv",
- s => lblStatusFb.Text = s);
+ 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 void rbRcSource_Checked(object sender, RoutedEventArgs e)
+ {
+ // Toggle visibility of API vs CSV controls
+ if (cmbDay == null || btnDownloadRc == null || btnBrowseCsvRc == null) return;
+ bool isApi = rbRcApi.IsChecked == true;
+ cmbDay.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;
+ }
+ }
+ }
+
+ ///
+ /// Parses a single CSV line respecting quoted fields (comma delimiter).
+ ///
+ private static string[] ParseCsvLine(string line)
+ {
+ var fields = new List();
+ 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();
+ }
+
+ ///
+ /// Adds a "No" row-number column as the first column in the DataTable.
+ ///
+ 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()
{
try
@@ -148,6 +491,10 @@ namespace HorseRacingPredictor
_racingManager.GetRacecards(day, progress, status));
_racingData = table;
+
+ // Add only row numbers for racing (do not add an "Inizio" column — meeting name already contains time)
+ InjectRomeStartTimeColumn(_racingData, null);
+
dgRacing.ItemsSource = _racingData?.DefaultView;
if (_racingData != null && _racingData.Rows.Count > 0)
@@ -177,9 +524,27 @@ namespace HorseRacingPredictor
private void btnExportRcCsv_Click(object sender, RoutedEventArgs e)
{
string dayLabel = cmbDay.SelectedIndex == 0 ? "oggi" : "domani";
- ExportToCsv(_racingData, txtRcExportPath.Text,
- $"Corse_{dayLabel}_{DateTime.Now:yyyy-MM-dd}.csv",
- s => lblStatusRc.Text = s);
+ var format = (cmbRcFormat?.SelectedItem as ComboBoxItem)?.Content?.ToString() ?? "CSV";
+ var defaultName = $"Corse_{dayLabel}_{DateTime.Now:yyyy-MM-dd}.{format.ToLower()}";
+ var filename = BuildFilename(txtRcPrefix?.Text, chkRcIncludeDate?.IsChecked == true ? GetSelectedDateString(cmbRcDateFormat, DateTime.Now) : 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 ????????????????????
@@ -193,6 +558,8 @@ namespace HorseRacingPredictor
return;
}
+ // Ensure filename has .csv extension
+ defaultName = EnsureFileExtension(defaultName, ".csv");
string filePath;
if (!string.IsNullOrEmpty(folder) && Directory.Exists(folder))
{
@@ -203,7 +570,8 @@ namespace HorseRacingPredictor
var dlg = new Microsoft.Win32.SaveFileDialog
{
Filter = "File CSV|*.csv",
- FileName = defaultName
+ FileName = defaultName,
+ AddExtension = true
};
if (dlg.ShowDialog() != true) return;
filePath = dlg.FileName;
@@ -240,6 +608,10 @@ namespace HorseRacingPredictor
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 ????????????????????
@@ -248,12 +620,14 @@ namespace HorseRacingPredictor
{
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)
@@ -289,16 +663,251 @@ namespace HorseRacingPredictor
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 == "RacingUser") txtRacingUser.Text = val;
else if (key == "RacingPass") txtRacingPass.Password = val;
}
+ // Update preview UI after loading values
+ UpdateFbPreview();
+ UpdateRcPreview();
+
_racingManager.UpdateCredentials(txtRacingUser.Text, txtRacingPass.Password);
}
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 datePart = chkRcIncludeDate?.IsChecked == true ? GetSelectedDateString(cmbRcDateFormat, DateTime.Now) : null;
+ var defaultName = $"Corse_{(cmbDay.SelectedIndex==0?"oggi":"domani")}_{DateTime.Now: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 { }
+ }
+
+ ///
+ /// 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.
+ ///
+ 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 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
@@ -306,11 +915,25 @@ namespace HorseRacingPredictor
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($"RacingUser={txtRacingUser.Text.Trim()}");
sb.AppendLine($"RacingPass={txtRacingPass.Password.Trim()}");
File.WriteAllText(SettingsFilePath, sb.ToString(), Encoding.UTF8);
+ // update previews after save
+ UpdateFbPreview();
+ UpdateRcPreview();
+
_racingManager.UpdateCredentials(txtRacingUser.Text.Trim(), txtRacingPass.Password.Trim());
MessageBox.Show("Impostazioni salvate con successo.",
diff --git a/HorseRacingPredictor/Styles/GlobalStyles.xaml b/HorseRacingPredictor/Styles/GlobalStyles.xaml
new file mode 100644
index 0000000..72589cc
--- /dev/null
+++ b/HorseRacingPredictor/Styles/GlobalStyles.xaml
@@ -0,0 +1,249 @@
+
+
+
+ #1E1E2E
+ #181825
+ #11111B
+ #313244
+ #45475A
+ #585B70
+ #6C7086
+ #CDD6F4
+ #A6ADC8
+ #BAC2DE
+ #89B4FA
+ #A6E3A1
+ #F38BA8
+ #FAB387
+ #B4BEFE
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+