Compare commits
8 Commits
7bb759e022
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 0ce501c58c | |||
| cf69e3b2fd | |||
| 923f4c761c | |||
| 0d3769db79 | |||
| 11b299548c | |||
| d6db312f73 | |||
| 51568df264 | |||
| 89138e2b8f |
@@ -414,3 +414,9 @@ FodyWeavers.xsd
|
||||
# Built Visual Studio Code Extensions
|
||||
*.vsix
|
||||
|
||||
|
||||
# Secrets / credentials - never commit
|
||||
appsettings.json
|
||||
appsettings.*.json
|
||||
!appsettings.template.json
|
||||
settings.ini
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
---
|
||||
name: my-agent
|
||||
description: Descrivi cosa fa questo agente personalizzato e quando usarlo.
|
||||
---
|
||||
|
||||
# my-agent
|
||||
|
||||
Definisci cosa fa questo agente personalizzato, includendo comportamento, funzionalità e istruzioni specifiche per il suo funzionamento.
|
||||
@@ -1,6 +1 @@
|
||||
<Application x:Class="HorseRacingPredictor.App"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
StartupUri="MainWindow.xaml">
|
||||
<Application.Resources />
|
||||
</Application>
|
||||
<Application x:Class="HorseRacingPredictor.App" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" StartupUri="MainWindow.xaml"><Application.Resources><ResourceDictionary><ResourceDictionary.MergedDictionaries><ResourceDictionary Source="/Styles/LightTheme.xaml"/><ResourceDictionary Source="/Styles/GlobalStyles.xaml"/></ResourceDictionary.MergedDictionaries></ResourceDictionary></Application.Resources></Application>
|
||||
@@ -1,8 +1,38 @@
|
||||
using System;
|
||||
using System.Windows;
|
||||
using System.Windows.Threading;
|
||||
|
||||
namespace HorseRacingPredictor
|
||||
{
|
||||
public partial class App : Application
|
||||
{
|
||||
protected override void OnStartup(StartupEventArgs e)
|
||||
{
|
||||
DispatcherUnhandledException += App_DispatcherUnhandledException;
|
||||
AppDomain.CurrentDomain.UnhandledException += CurrentDomain_UnhandledException;
|
||||
base.OnStartup(e);
|
||||
}
|
||||
|
||||
private void App_DispatcherUnhandledException(object sender, DispatcherUnhandledExceptionEventArgs e)
|
||||
{
|
||||
MessageBox.Show(
|
||||
$"Errore non gestito:\n\n{e.Exception.GetType().Name}: {e.Exception.Message}\n\n{e.Exception.StackTrace}",
|
||||
"Errore applicazione",
|
||||
MessageBoxButton.OK,
|
||||
MessageBoxImage.Error);
|
||||
e.Handled = true;
|
||||
}
|
||||
|
||||
private void CurrentDomain_UnhandledException(object sender, UnhandledExceptionEventArgs e)
|
||||
{
|
||||
if (e.ExceptionObject is Exception ex)
|
||||
{
|
||||
MessageBox.Show(
|
||||
$"Errore fatale:\n\n{ex.GetType().Name}: {ex.Message}\n\n{ex.StackTrace}",
|
||||
"Errore fatale",
|
||||
MessageBoxButton.OK,
|
||||
MessageBoxImage.Error);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@
|
||||
<OutputPath>bin\x64\Release\</OutputPath>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageReference Include="AngleSharp" Version="1.4.0" />
|
||||
<PackageReference Include="CsvHelper" Version="33.1.0" />
|
||||
<PackageReference Include="Microsoft.Bcl.AsyncInterfaces" Version="11.0.0-preview.2.26159.112" />
|
||||
<PackageReference Include="Microsoft.Bcl.HashCode" Version="6.0.0" />
|
||||
@@ -27,7 +28,6 @@
|
||||
<PackageReference Include="Microsoft.ML.CpuMath" Version="6.0.0-preview.26160.2" />
|
||||
<PackageReference Include="Microsoft.ML.DataView" Version="6.0.0-preview.26160.2" />
|
||||
<PackageReference Include="Microsoft.ML.FastTree" Version="6.0.0-preview.26160.2" />
|
||||
<PackageReference Include="Microsoft.Web.WebView2" Version="1.0.3908-prerelease" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.5-beta1" />
|
||||
<PackageReference Include="RestSharp" Version="114.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="10.0.5" />
|
||||
@@ -54,4 +54,13 @@
|
||||
<ItemGroup Label="EmbeddedResource items now included by globbing that were not in the original project file">
|
||||
<EmbeddedResource Remove="Main.resx" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Content Include="HorseRacingPredictor\appsettings.json" Link="appsettings.json">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Folder Include="Football\WebSearch\" />
|
||||
<Folder Include="HorseRacing\WebSearch\" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
+84
@@ -0,0 +1,84 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<script async src="https://www.googletagmanager.com/gtag/js?id=G-72D75QY30S"></script>
|
||||
<script>function gtag(){dataLayer.push(arguments)}window.dataLayer=window.dataLayer||[],gtag("js",new Date),gtag("config","G-72D75QY30S");</script>
|
||||
<meta charset="utf-8">
|
||||
<title>API-Football - Documentation</title>
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="description" content="All the documentation about API-FOOTBALL and how to use all endpoints like Timezone, Seasons, Countries, Leagues, Teams, Standings, Fixtures, Events..">
|
||||
<meta name="author" content="API FOOTBALL">
|
||||
<meta name="keywords" content="api, football, soccer, statistics, rest, rest-api, restful, api, results, footballresult, soccerresult, json Football API, Soccer Livescore API, Livescore Feed, Livescore API, Football Feed, Soccer API, Soccer Feed, odds, odds xml, odds feeds, free api, free plan, free data, open data soccer, transfers, transfers players, open data json, predictions, pronostics, top scorers">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1">
|
||||
<meta name="google-site-verification" content="SjoK3Uh8ZXPz41he_9gqcEQq4oBv-nk-AqMbAZJhPYk" />
|
||||
<meta property="og:url" content="https://www.api-football.com/documentation-v3" />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:title" content="API-Football - Documentation" />
|
||||
<meta property="og:description" content="All the documentation about API-FOOTBALL and how to use all endpoints like Timezone, Seasons, Countries, Leagues, Teams, Standings, Fixtures, Events.." />
|
||||
<meta property="og:image" content="https://www.api-football.com/public/img/home1/hero-banner.png" />
|
||||
<meta property="og:site_name" content="api-football" />
|
||||
|
||||
<meta name="twitter:card" content="summary" />
|
||||
<meta name="twitter:image" content="https://www.api-football.com/public/img/home1/hero-banner.png" />
|
||||
<meta name="twitter:title" content="API-Football - Documentation" />
|
||||
<meta name="twitter:description" content="All the documentation about API-FOOTBALL and how to use all endpoints like Timezone, Seasons, Countries, Leagues, Teams, Standings, Fixtures, Events.." />
|
||||
<link href="https://fonts.googleapis.com/css?family=Montserrat:300,400,700|Roboto:300,400,700" rel="stylesheet">
|
||||
<style>
|
||||
body {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
|
||||
<link rel="shortcut icon" type="image/png" href="https://www.api-football.com/public/img/favicon.ico">
|
||||
|
||||
<link rel="shortcut icon" href="https://www.api-football.com/public/img/favicon.ico">
|
||||
<link rel="apple-touch-icon" sizes="120x120" href="https://www.api-football.com/public/img/favicon-120.png">
|
||||
<link rel="apple-touch-icon" sizes="152x152" href="https://www.api-football.com/public/img/favicon-152.png">
|
||||
<!-- HTML5 Shim and Respond.js IE8 support of HTML5 elements and media queries -->
|
||||
<!-- WARNING: Respond.js doesn't work if you view the page via file:// -->
|
||||
<!--[if lt IE 9]>
|
||||
<script src="https://oss.maxcdn.com/libs/html5shiv/3.7.0/html5shiv.js"></script>
|
||||
<script src="https://oss.maxcdn.com/libs/respond.public/js/1.4.2/respond.min.js"></script>
|
||||
<![endif]-->
|
||||
</head>
|
||||
|
||||
<body data-spy="scroll" data-target="#scroll-menu" data-offset="65">
|
||||
<div id="loader-wrapper">
|
||||
<div id="loader"></div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<redoc spec-url='public/doc/openapi.yaml' hide-download-button></redoc>
|
||||
<script src="https://cdn.redoc.ly/redoc/v2.0.0/bundles/redoc.standalone.js"> </script>
|
||||
|
||||
|
||||
<script src="https://www.api-football.com/public/js/jquery.min.js"></script>
|
||||
<script src="https://www.api-football.com/public/js/jquery-ui.js"></script>
|
||||
<script src="https://www.api-football.com/public/js/owl.carousel.min.js"></script>
|
||||
<script src="https://www.api-football.com/public/js/isotope.pkgd.min.js"></script>
|
||||
<script src="https://www.api-football.com/public/js/imagesloaded.pkgd.min.js"></script>
|
||||
<script src="https://www.api-football.com/public/js/wow.min.js"></script>
|
||||
<script src="https://www.api-football.com/public/js/jquery.magnific-popup.min.js"></script>
|
||||
<script src="https://www.api-football.com/public/js/jquery.counterup.min.js"></script>
|
||||
<script src="https://www.api-football.com/public/js/countdown.js"></script>
|
||||
<script src="https://www.api-football.com/public/js/jquery.slicknav.min.js"></script>
|
||||
<script src="https://www.api-football.com/public/js/jquery.scrollUp.js"></script>
|
||||
<script src="https://www.api-football.com/public/js/jquery.waypoints.min.js"></script>
|
||||
<script src="https://www.api-football.com/public/js/popper.min.js"></script>
|
||||
<script src="https://www.api-football.com/public/js/bootstrap.min.js"></script>
|
||||
<script src="https://www.api-football.com/public/js/theme.js"></script>
|
||||
<script type="text/javascript" src="//www.freeprivacypolicy.com/public/cookie-consent/3.1.0/cookie-consent.js"></script>
|
||||
<script type="text/javascript">document.addEventListener('DOMContentLoaded', function () {setTimeout(function(){cookieconsent.run({"notice_banner_type":"simple","consent_type":"implied","palette":"light","language":"en","website_name":"www.api-football.com","cookies_policy_url":"https://www.api-football.com/privacy"});},1500);});</script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
+97668
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because one or more lines are too long
@@ -0,0 +1,26 @@
|
||||
using System;
|
||||
using RestSharp;
|
||||
|
||||
namespace HorseRacingPredictor.Football.API
|
||||
{
|
||||
/// <summary>
|
||||
/// Client per l'endpoint "coachs" dell'API-Football.
|
||||
/// Restituisce informazioni sugli allenatori.
|
||||
/// </summary>
|
||||
internal class Coaches : HorseRacingPredictor.Football.Manager.API
|
||||
{
|
||||
private const string Endpoint = "coachs";
|
||||
|
||||
public RestResponse GetByTeam(int teamId)
|
||||
{
|
||||
try
|
||||
{
|
||||
return ExecuteRequest($"{Endpoint}?team={teamId}", ApiTypes.Teams);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new Exception($"Errore recupero allenatore team {teamId}: {ex.Message}", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
using System;
|
||||
using RestSharp;
|
||||
|
||||
namespace HorseRacingPredictor.Football.API
|
||||
{
|
||||
/// <summary>
|
||||
/// Client per l'endpoint "fixtures/events" dell'API-Football.
|
||||
/// Restituisce gli eventi di una partita (gol, cartellini, sostituzioni, VAR).
|
||||
/// </summary>
|
||||
internal class Events : HorseRacingPredictor.Football.Manager.API
|
||||
{
|
||||
private const string Endpoint = "fixtures/events";
|
||||
|
||||
/// <summary>
|
||||
/// Ottiene tutti gli eventi per una partita
|
||||
/// </summary>
|
||||
public RestResponse GetEventsByFixture(int fixtureId)
|
||||
{
|
||||
try
|
||||
{
|
||||
return ExecuteRequest($"{Endpoint}?fixture={fixtureId}", ApiTypes.Fixtures);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new Exception($"Errore durante il recupero degli eventi per la partita {fixtureId}: {ex.Message}", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -41,6 +41,22 @@ namespace HorseRacingPredictor.Football.API
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ottiene le partite per una data e una lega specifica
|
||||
/// </summary>
|
||||
public RestResponse GetFixturesByDateAndLeague(DateTime date, int leagueId, int season)
|
||||
{
|
||||
try
|
||||
{
|
||||
string dateStr = date.ToString("yyyy-MM-dd");
|
||||
return ExecuteRequest($"{Endpoint}?date={dateStr}&league={leagueId}&season={season}", ApiTypes.Fixtures);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new Exception($"Errore durante il recupero delle partite per la data {date.ToShortDateString()} e lega {leagueId}: {ex.Message}", ex);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ottiene le partite per un ID specifico
|
||||
/// </summary>
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
using System;
|
||||
using RestSharp;
|
||||
|
||||
namespace HorseRacingPredictor.Football.API
|
||||
{
|
||||
/// <summary>
|
||||
/// Client per l'endpoint "fixtures/statistics" dell'API-Football.
|
||||
/// Restituisce le statistiche di una partita (possesso, tiri, falli, ecc.).
|
||||
/// </summary>
|
||||
internal class FixtureStatistics : HorseRacingPredictor.Football.Manager.API
|
||||
{
|
||||
private const string Endpoint = "fixtures/statistics";
|
||||
|
||||
/// <summary>
|
||||
/// Ottiene le statistiche per una partita
|
||||
/// </summary>
|
||||
public RestResponse GetStatisticsByFixture(int fixtureId)
|
||||
{
|
||||
try
|
||||
{
|
||||
return ExecuteRequest($"{Endpoint}?fixture={fixtureId}", ApiTypes.Fixtures);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new Exception($"Errore durante il recupero delle statistiche per la partita {fixtureId}: {ex.Message}", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
using System;
|
||||
using RestSharp;
|
||||
|
||||
namespace HorseRacingPredictor.Football.API
|
||||
{
|
||||
/// <summary>
|
||||
/// Client per l'endpoint "fixtures/headtohead" dell'API-Football.
|
||||
/// Restituisce lo storico degli scontri diretti tra due squadre.
|
||||
/// </summary>
|
||||
internal class HeadToHead : HorseRacingPredictor.Football.Manager.API
|
||||
{
|
||||
private const string Endpoint = "fixtures/headtohead";
|
||||
|
||||
/// <summary>
|
||||
/// Ottiene gli scontri diretti tra due squadre
|
||||
/// </summary>
|
||||
/// <param name="teamId1">ID della prima squadra</param>
|
||||
/// <param name="teamId2">ID della seconda squadra</param>
|
||||
/// <param name="last">Numero di ultimi scontri da recuperare (default 5)</param>
|
||||
public RestResponse GetH2H(int teamId1, int teamId2, int last = 5)
|
||||
{
|
||||
try
|
||||
{
|
||||
return ExecuteRequest($"{Endpoint}?h2h={teamId1}-{teamId2}&last={last}", ApiTypes.Fixtures);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new Exception($"Errore durante il recupero H2H per {teamId1} vs {teamId2}: {ex.Message}", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
using System;
|
||||
using RestSharp;
|
||||
|
||||
namespace HorseRacingPredictor.Football.API
|
||||
{
|
||||
/// <summary>
|
||||
/// Client per l'endpoint "injuries" dell'API-Football.
|
||||
/// Restituisce la lista di giocatori infortunati o squalificati.
|
||||
/// </summary>
|
||||
internal class Injuries : HorseRacingPredictor.Football.Manager.API
|
||||
{
|
||||
private const string Endpoint = "injuries";
|
||||
|
||||
/// <summary>
|
||||
/// Ottiene gli infortunati per una data specifica
|
||||
/// </summary>
|
||||
public RestResponse GetInjuriesByDate(DateTime date)
|
||||
{
|
||||
try
|
||||
{
|
||||
string dateStr = date.ToString("yyyy-MM-dd");
|
||||
return ExecuteRequest($"{Endpoint}?date={dateStr}", ApiTypes.Fixtures);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new Exception($"Errore durante il recupero degli infortunati per la data {date.ToShortDateString()}: {ex.Message}", ex);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ottiene gli infortunati per una partita specifica
|
||||
/// </summary>
|
||||
public RestResponse GetInjuriesByFixture(int fixtureId)
|
||||
{
|
||||
try
|
||||
{
|
||||
return ExecuteRequest($"{Endpoint}?fixture={fixtureId}", ApiTypes.Fixtures);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new Exception($"Errore durante il recupero degli infortunati per la partita {fixtureId}: {ex.Message}", ex);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ottiene gli infortunati per una lega e stagione
|
||||
/// </summary>
|
||||
public RestResponse GetInjuriesByLeague(int leagueId, int season)
|
||||
{
|
||||
try
|
||||
{
|
||||
return ExecuteRequest($"{Endpoint}?league={leagueId}&season={season}", ApiTypes.Fixtures);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new Exception($"Errore durante il recupero degli infortunati per lega {leagueId}, stagione {season}: {ex.Message}", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
using System;
|
||||
using RestSharp;
|
||||
|
||||
namespace HorseRacingPredictor.Football.API
|
||||
{
|
||||
/// <summary>
|
||||
/// Client per l'endpoint "fixtures/lineups" dell'API-Football.
|
||||
/// Restituisce formazioni, modulo e titolari di una partita.
|
||||
/// </summary>
|
||||
internal class Lineups : HorseRacingPredictor.Football.Manager.API
|
||||
{
|
||||
private const string Endpoint = "fixtures/lineups";
|
||||
|
||||
/// <summary>
|
||||
/// Ottiene le formazioni per una partita
|
||||
/// </summary>
|
||||
public RestResponse GetLineupsByFixture(int fixtureId)
|
||||
{
|
||||
try
|
||||
{
|
||||
return ExecuteRequest($"{Endpoint}?fixture={fixtureId}", ApiTypes.Fixtures);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new Exception($"Errore durante il recupero delle formazioni per la partita {fixtureId}: {ex.Message}", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,30 +9,36 @@ namespace HorseRacingPredictor.Football.API
|
||||
private const string Endpoint = "odds";
|
||||
|
||||
/// <summary>
|
||||
/// Ottiene le quote per una data specifica
|
||||
/// Ottiene le quote per una data specifica (tutti i bookmaker, nessun filtro).
|
||||
/// </summary>
|
||||
public RestResponse GetOddsByDate(DateTime date, int bookmaker = 8, int page = 1)
|
||||
public RestResponse GetOddsByDate(DateTime date, int page = 1)
|
||||
{
|
||||
try
|
||||
{
|
||||
string dateStr = date.ToString("yyyy-MM-dd");
|
||||
return ExecuteRequest($"{Endpoint}?date={dateStr}&bookmaker={bookmaker}&page={page}", ApiTypes.Odds);
|
||||
return ExecuteRequest($"{Endpoint}?date={dateStr}&page={page}", ApiTypes.Odds);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new Exception($"Errore durante il recupero delle quote per la data {date.ToShortDateString()}: {ex.Message}", ex);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Overload legacy: mantiene la compatibilità con codice che passa bookmaker.
|
||||
/// </summary>
|
||||
public RestResponse GetOddsByDate(DateTime date, int bookmaker, int page)
|
||||
=> GetOddsByDate(date, page);
|
||||
|
||||
/// <summary>
|
||||
/// Versione asincrona di GetOddsByDate
|
||||
/// Versione asincrona di GetOddsByDate (tutti i bookmaker).
|
||||
/// </summary>
|
||||
public async Task<RestResponse> GetOddsByDateAsync(DateTime date, int bookmaker = 8, int page = 1)
|
||||
public async Task<RestResponse> GetOddsByDateAsync(DateTime date, int page = 1)
|
||||
{
|
||||
try
|
||||
{
|
||||
string dateStr = date.ToString("yyyy-MM-dd");
|
||||
return await ExecuteRequestAsync($"{Endpoint}?date={dateStr}&bookmaker={bookmaker}&page={page}");
|
||||
return await ExecuteRequestAsync($"{Endpoint}?date={dateStr}&page={page}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
@@ -41,13 +47,13 @@ namespace HorseRacingPredictor.Football.API
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ottiene le quote per una partita specifica
|
||||
/// Ottiene le quote per una partita specifica (tutti i bookmaker).
|
||||
/// </summary>
|
||||
public RestResponse GetOddsByFixture(int fixtureId, int bookmaker = 8)
|
||||
public RestResponse GetOddsByFixture(int fixtureId)
|
||||
{
|
||||
try
|
||||
{
|
||||
return ExecuteRequest($"{Endpoint}?fixture={fixtureId}&bookmaker={bookmaker}", ApiTypes.Odds);
|
||||
return ExecuteRequest($"{Endpoint}?fixture={fixtureId}", ApiTypes.Odds);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
using System;
|
||||
using RestSharp;
|
||||
|
||||
namespace HorseRacingPredictor.Football.API
|
||||
{
|
||||
/// <summary>
|
||||
/// Client per l'endpoint "players" dell'API-Football.
|
||||
/// Restituisce statistiche dettagliate dei giocatori per squadra/lega/stagione.
|
||||
/// </summary>
|
||||
internal class PlayerStatistics : HorseRacingPredictor.Football.Manager.API
|
||||
{
|
||||
private const string Endpoint = "players";
|
||||
|
||||
/// <summary>
|
||||
/// Ottiene le statistiche dei giocatori per squadra e stagione (paginato, 20 per pagina).
|
||||
/// </summary>
|
||||
public RestResponse GetByTeamAndSeason(int teamId, int season, int page = 1)
|
||||
{
|
||||
try
|
||||
{
|
||||
return ExecuteRequest($"{Endpoint}?team={teamId}&season={season}&page={page}", ApiTypes.Teams);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new Exception($"Errore recupero statistiche giocatori team {teamId}, stagione {season}, pagina {page}: {ex.Message}", ex);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ottiene le statistiche di un singolo giocatore per stagione.
|
||||
/// </summary>
|
||||
public RestResponse GetByPlayerAndSeason(int playerId, int season)
|
||||
{
|
||||
try
|
||||
{
|
||||
return ExecuteRequest($"{Endpoint}?id={playerId}&season={season}", ApiTypes.Teams);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new Exception($"Errore recupero statistiche giocatore {playerId}, stagione {season}: {ex.Message}", ex);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ottiene le statistiche dei giocatori per lega e stagione (paginato, 20 per pagina).
|
||||
/// </summary>
|
||||
public RestResponse GetByLeagueAndSeason(int leagueId, int season, int page = 1)
|
||||
{
|
||||
try
|
||||
{
|
||||
return ExecuteRequest($"{Endpoint}?league={leagueId}&season={season}&page={page}", ApiTypes.Leagues);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new Exception($"Errore recupero statistiche giocatori lega {leagueId}, stagione {season}, pagina {page}: {ex.Message}", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
using System;
|
||||
using RestSharp;
|
||||
|
||||
namespace HorseRacingPredictor.Football.API
|
||||
{
|
||||
/// <summary>
|
||||
/// Client per l'endpoint "sidelined" dell'API-Football.
|
||||
/// Restituisce lo storico di indisponibilità/squalifiche di un giocatore o allenatore.
|
||||
/// </summary>
|
||||
internal class Sidelined : HorseRacingPredictor.Football.Manager.API
|
||||
{
|
||||
private const string Endpoint = "sidelined";
|
||||
|
||||
public RestResponse GetByPlayer(int playerId)
|
||||
{
|
||||
try
|
||||
{
|
||||
return ExecuteRequest($"{Endpoint}?player={playerId}", ApiTypes.Teams);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new Exception($"Errore recupero sidelined giocatore {playerId}: {ex.Message}", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public RestResponse GetByCoach(int coachId)
|
||||
{
|
||||
try
|
||||
{
|
||||
return ExecuteRequest($"{Endpoint}?coach={coachId}", ApiTypes.Teams);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new Exception($"Errore recupero sidelined allenatore {coachId}: {ex.Message}", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
using System;
|
||||
using RestSharp;
|
||||
|
||||
namespace HorseRacingPredictor.Football.API
|
||||
{
|
||||
/// <summary>
|
||||
/// Client per l'endpoint "players/squads" dell'API-Football.
|
||||
/// Restituisce la rosa corrente di una squadra.
|
||||
/// </summary>
|
||||
internal class Squads : HorseRacingPredictor.Football.Manager.API
|
||||
{
|
||||
private const string Endpoint = "players/squads";
|
||||
|
||||
public RestResponse GetSquad(int teamId)
|
||||
{
|
||||
try
|
||||
{
|
||||
return ExecuteRequest($"{Endpoint}?team={teamId}", ApiTypes.Teams);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new Exception($"Errore recupero rosa team {teamId}: {ex.Message}", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
using System;
|
||||
using RestSharp;
|
||||
|
||||
namespace HorseRacingPredictor.Football.API
|
||||
{
|
||||
/// <summary>
|
||||
/// Client per l'endpoint "standings" dell'API-Football.
|
||||
/// Restituisce la classifica di un campionato per una stagione.
|
||||
/// </summary>
|
||||
internal class Standings : HorseRacingPredictor.Football.Manager.API
|
||||
{
|
||||
private const string Endpoint = "standings";
|
||||
|
||||
/// <summary>
|
||||
/// Ottiene la classifica per una lega e una stagione
|
||||
/// </summary>
|
||||
public RestResponse GetStandings(int leagueId, int season)
|
||||
{
|
||||
try
|
||||
{
|
||||
return ExecuteRequest($"{Endpoint}?league={leagueId}&season={season}", ApiTypes.Leagues);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new Exception($"Errore durante il recupero della classifica per lega {leagueId}, stagione {season}: {ex.Message}", ex);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ottiene la classifica per un team e una stagione
|
||||
/// </summary>
|
||||
public RestResponse GetStandingsByTeam(int teamId, int season)
|
||||
{
|
||||
try
|
||||
{
|
||||
return ExecuteRequest($"{Endpoint}?team={teamId}&season={season}", ApiTypes.Teams);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new Exception($"Errore durante il recupero della classifica per team {teamId}, stagione {season}: {ex.Message}", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
using System;
|
||||
using RestSharp;
|
||||
|
||||
namespace HorseRacingPredictor.Football.API
|
||||
{
|
||||
/// <summary>
|
||||
/// Client per l'endpoint "status" dell'API-Football.
|
||||
/// Restituisce la quota residua giornaliera e le informazioni sull'account.
|
||||
/// Non conta come chiamata nella quota giornaliera.
|
||||
/// </summary>
|
||||
internal class Status : HorseRacingPredictor.Football.Manager.API
|
||||
{
|
||||
private const string Endpoint = "status";
|
||||
|
||||
/// <summary>
|
||||
/// Ottiene lo stato dell'account e la quota residua
|
||||
/// </summary>
|
||||
public RestResponse GetStatus()
|
||||
{
|
||||
try
|
||||
{
|
||||
return ExecuteRequest(Endpoint);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new Exception($"Errore durante il recupero dello stato API: {ex.Message}", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
using System;
|
||||
using RestSharp;
|
||||
|
||||
namespace HorseRacingPredictor.Football.API
|
||||
{
|
||||
/// <summary>
|
||||
/// Client per l'endpoint "teams/statistics" dell'API-Football.
|
||||
/// Restituisce statistiche aggregate di una squadra per campionato e stagione
|
||||
/// (forma, gol, clean sheet, penalty, cartellini, formazioni usate, ecc.).
|
||||
/// </summary>
|
||||
internal class TeamStatistics : HorseRacingPredictor.Football.Manager.API
|
||||
{
|
||||
private const string Endpoint = "teams/statistics";
|
||||
|
||||
/// <summary>
|
||||
/// Ottiene le statistiche di una squadra per lega, stagione e opzionalmente fino a una data.
|
||||
/// </summary>
|
||||
public RestResponse GetTeamStatistics(int teamId, int leagueId, int season, string dateTo = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
string query = $"{Endpoint}?team={teamId}&league={leagueId}&season={season}";
|
||||
if (!string.IsNullOrEmpty(dateTo))
|
||||
query += $"&date={dateTo}";
|
||||
return ExecuteRequest(query, ApiTypes.Teams);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new Exception($"Errore recupero statistiche team {teamId}, lega {leagueId}, stagione {season}: {ex.Message}", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
using System;
|
||||
using RestSharp;
|
||||
|
||||
namespace HorseRacingPredictor.Football.API
|
||||
{
|
||||
/// <summary>
|
||||
/// Client per l'endpoint "players/topassists" dell'API-Football.
|
||||
/// Restituisce i 20 migliori assistman di una lega/stagione.
|
||||
/// </summary>
|
||||
internal class TopAssists : HorseRacingPredictor.Football.Manager.API
|
||||
{
|
||||
private const string Endpoint = "players/topassists";
|
||||
|
||||
public RestResponse GetTopAssists(int leagueId, int season)
|
||||
{
|
||||
try
|
||||
{
|
||||
return ExecuteRequest($"{Endpoint}?league={leagueId}&season={season}", ApiTypes.Leagues);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new Exception($"Errore recupero top assistman lega {leagueId}, stagione {season}: {ex.Message}", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
using System;
|
||||
using RestSharp;
|
||||
|
||||
namespace HorseRacingPredictor.Football.API
|
||||
{
|
||||
/// <summary>
|
||||
/// Client per gli endpoint "players/topyellowcards" e "players/topredcards" dell'API-Football.
|
||||
/// Restituisce i 20 giocatori con più cartellini per lega/stagione.
|
||||
/// </summary>
|
||||
internal class TopCards : HorseRacingPredictor.Football.Manager.API
|
||||
{
|
||||
private const string YellowEndpoint = "players/topyellowcards";
|
||||
private const string RedEndpoint = "players/topredcards";
|
||||
|
||||
public RestResponse GetTopYellowCards(int leagueId, int season)
|
||||
{
|
||||
try
|
||||
{
|
||||
return ExecuteRequest($"{YellowEndpoint}?league={leagueId}&season={season}", ApiTypes.Leagues);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new Exception($"Errore recupero top cartellini gialli lega {leagueId}, stagione {season}: {ex.Message}", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public RestResponse GetTopRedCards(int leagueId, int season)
|
||||
{
|
||||
try
|
||||
{
|
||||
return ExecuteRequest($"{RedEndpoint}?league={leagueId}&season={season}", ApiTypes.Leagues);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new Exception($"Errore recupero top cartellini rossi lega {leagueId}, stagione {season}: {ex.Message}", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
using System;
|
||||
using RestSharp;
|
||||
|
||||
namespace HorseRacingPredictor.Football.API
|
||||
{
|
||||
/// <summary>
|
||||
/// Client per l'endpoint "players/topscorers" dell'API-Football.
|
||||
/// Restituisce i 20 migliori marcatori di una lega/stagione.
|
||||
/// </summary>
|
||||
internal class TopScorers : HorseRacingPredictor.Football.Manager.API
|
||||
{
|
||||
private const string Endpoint = "players/topscorers";
|
||||
|
||||
public RestResponse GetTopScorers(int leagueId, int season)
|
||||
{
|
||||
try
|
||||
{
|
||||
return ExecuteRequest($"{Endpoint}?league={leagueId}&season={season}", ApiTypes.Leagues);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new Exception($"Errore recupero capocannonieri lega {leagueId}, stagione {season}: {ex.Message}", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
using System;
|
||||
using RestSharp;
|
||||
|
||||
namespace HorseRacingPredictor.Football.API
|
||||
{
|
||||
/// <summary>
|
||||
/// Client per l'endpoint "transfers" dell'API-Football.
|
||||
/// Restituisce la cronologia trasferimenti di un giocatore o di una squadra.
|
||||
/// </summary>
|
||||
internal class Transfers : HorseRacingPredictor.Football.Manager.API
|
||||
{
|
||||
private const string Endpoint = "transfers";
|
||||
|
||||
public RestResponse GetByTeam(int teamId)
|
||||
{
|
||||
try
|
||||
{
|
||||
return ExecuteRequest($"{Endpoint}?team={teamId}", ApiTypes.Teams);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new Exception($"Errore recupero trasferimenti team {teamId}: {ex.Message}", ex);
|
||||
}
|
||||
}
|
||||
|
||||
public RestResponse GetByPlayer(int playerId)
|
||||
{
|
||||
try
|
||||
{
|
||||
return ExecuteRequest($"{Endpoint}?player={playerId}", ApiTypes.Teams);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
throw new Exception($"Errore recupero trasferimenti giocatore {playerId}: {ex.Message}", ex);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,424 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Data;
|
||||
using System.Globalization;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
|
||||
namespace HorseRacingPredictor.Football
|
||||
{
|
||||
/// <summary>
|
||||
/// Loads and joins ESPN Soccer CSV files (fixtures, teams, leagues, venues,
|
||||
/// teamStats, standings, status, players, teamRoster, keyEventDescription)
|
||||
/// for a given date, returning one enriched row per match sorted by kick-off time.
|
||||
/// </summary>
|
||||
public class EspnCsvLoader
|
||||
{
|
||||
// ── helpers ─────────────────────────────────────────────────────────────
|
||||
|
||||
private static string[] ParseCsvLine(string line)
|
||||
{
|
||||
var fields = new List<string>();
|
||||
bool inQuotes = false;
|
||||
var sb = new StringBuilder();
|
||||
foreach (char c in line)
|
||||
{
|
||||
if (inQuotes)
|
||||
{
|
||||
if (c == '"') 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();
|
||||
}
|
||||
|
||||
private static DataTable ReadCsv(string path)
|
||||
{
|
||||
var dt = new DataTable(Path.GetFileNameWithoutExtension(path));
|
||||
if (!File.Exists(path)) return dt;
|
||||
var lines = File.ReadAllLines(path, Encoding.UTF8);
|
||||
if (lines.Length == 0) return dt;
|
||||
var headers = ParseCsvLine(lines[0]);
|
||||
foreach (var h in headers)
|
||||
dt.Columns.Add(h.Trim(), typeof(string));
|
||||
for (int i = 1; i < lines.Length; i++)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(lines[i])) continue;
|
||||
var vals = ParseCsvLine(lines[i]);
|
||||
var row = dt.NewRow();
|
||||
for (int c = 0; c < headers.Length && c < vals.Length; c++)
|
||||
row[c] = vals[c]?.Trim() ?? "";
|
||||
dt.Rows.Add(row);
|
||||
}
|
||||
return dt;
|
||||
}
|
||||
|
||||
private static Dictionary<string, DataRow> IndexBy(DataTable dt, string key)
|
||||
{
|
||||
var d = new Dictionary<string, DataRow>(StringComparer.OrdinalIgnoreCase);
|
||||
if (!dt.Columns.Contains(key)) return d;
|
||||
foreach (DataRow r in dt.Rows)
|
||||
{
|
||||
var k = r[key]?.ToString() ?? "";
|
||||
if (!string.IsNullOrWhiteSpace(k) && !d.ContainsKey(k))
|
||||
d[k] = r;
|
||||
}
|
||||
return d;
|
||||
}
|
||||
|
||||
private static Dictionary<string, List<DataRow>> IndexByMulti(DataTable dt, string key)
|
||||
{
|
||||
var d = new Dictionary<string, List<DataRow>>(StringComparer.OrdinalIgnoreCase);
|
||||
if (!dt.Columns.Contains(key)) return d;
|
||||
foreach (DataRow r in dt.Rows)
|
||||
{
|
||||
var k = r[key]?.ToString() ?? "";
|
||||
if (string.IsNullOrWhiteSpace(k)) continue;
|
||||
if (!d.ContainsKey(k)) d[k] = new List<DataRow>();
|
||||
d[k].Add(r);
|
||||
}
|
||||
return d;
|
||||
}
|
||||
|
||||
private static string Get(DataRow r, string col)
|
||||
=> r?.Table?.Columns?.Contains(col) == true ? r[col]?.ToString() ?? "" : "";
|
||||
|
||||
private static void Set(DataRow dest, string col, string value)
|
||||
{
|
||||
if (dest.Table.Columns.Contains(col))
|
||||
dest[col] = value;
|
||||
}
|
||||
|
||||
// ── public entry point ───────────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Scans all *.csv files in <paramref name="folderPath"/>, identifies the known ESPN
|
||||
/// Soccer files by name, joins them by the relational keys shown in the schema diagram,
|
||||
/// and returns one <see cref="DataTable"/> row per match whose date matches
|
||||
/// <paramref name="targetDate"/>. Rows are sorted chronologically by kick-off time.
|
||||
/// </summary>
|
||||
public static (DataTable table, string message) Load(
|
||||
string folderPath, DateTime targetDate,
|
||||
IProgress<int> progress = null, IProgress<string> status = null)
|
||||
{
|
||||
var csvFiles = Directory.GetFiles(folderPath, "*.csv", SearchOption.AllDirectories)
|
||||
.ToDictionary(f => Path.GetFileNameWithoutExtension(f).ToLowerInvariant(), f => f);
|
||||
|
||||
DataTable TryRead(string name)
|
||||
{
|
||||
var key = csvFiles.Keys.FirstOrDefault(k => k.Contains(name));
|
||||
return key != null ? ReadCsv(csvFiles[key]) : new DataTable(name);
|
||||
}
|
||||
|
||||
status?.Report("Lettura file CSV…");
|
||||
progress?.Report(5);
|
||||
|
||||
var fixtures = TryRead("fixture");
|
||||
var teams = TryRead("team");
|
||||
var leagues = TryRead("league");
|
||||
var venues = TryRead("venue");
|
||||
var teamStats = TryRead("teamstat");
|
||||
var standings = TryRead("standing");
|
||||
var statusTbl = TryRead("status");
|
||||
var players = TryRead("player");
|
||||
var teamRoster = TryRead("teamroster");
|
||||
var keyEventDesc = TryRead("keyeventdescription");
|
||||
|
||||
status?.Report("Costruzione indici…");
|
||||
progress?.Report(20);
|
||||
|
||||
// Build lookup dictionaries
|
||||
var teamsById = IndexBy(teams, "teamId");
|
||||
var leaguesById = IndexBy(leagues, "leagueId");
|
||||
var venuesById = IndexBy(venues, "venueId");
|
||||
var statusById = IndexBy(statusTbl,"statusId");
|
||||
var keyEventsByTypeId = IndexBy(keyEventDesc, "keyEventTypeId");
|
||||
|
||||
// teamStats can have multiple rows per eventId+teamId – index by "eventId|teamId"
|
||||
var teamStatsByEventTeam = new Dictionary<string, DataRow>(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (DataRow r in teamStats.Rows)
|
||||
{
|
||||
var eid = Get(r, "eventId"); var tid = Get(r, "teamId");
|
||||
if (!string.IsNullOrEmpty(eid) && !string.IsNullOrEmpty(tid))
|
||||
teamStatsByEventTeam[$"{eid}|{tid}"] = r;
|
||||
}
|
||||
|
||||
// standings index by "leagueId|teamId" (latest record wins)
|
||||
var standingsByLeagueTeam = new Dictionary<string, DataRow>(StringComparer.OrdinalIgnoreCase);
|
||||
foreach (DataRow r in standings.Rows)
|
||||
{
|
||||
var lid = Get(r, "leagueId"); var tid = Get(r, "teamId");
|
||||
if (!string.IsNullOrEmpty(lid) && !string.IsNullOrEmpty(tid))
|
||||
standingsByLeagueTeam[$"{lid}|{tid}"] = r;
|
||||
}
|
||||
|
||||
// teamRoster multi-index by teamId
|
||||
var rosterByTeam = IndexByMulti(teamRoster, "teamId");
|
||||
// players index by athleteId
|
||||
var playersById = IndexBy(players, "athleteId");
|
||||
|
||||
status?.Report("Filtraggio partite per data…");
|
||||
progress?.Report(35);
|
||||
|
||||
// ── Filter fixtures by targetDate ────────────────────────────────────
|
||||
string targetDateStr = targetDate.ToString("yyyy-MM-dd");
|
||||
var dayFixtures = new List<DataRow>();
|
||||
foreach (DataRow r in fixtures.Rows)
|
||||
{
|
||||
var dateVal = Get(r, "date");
|
||||
if (dateVal.StartsWith(targetDateStr, StringComparison.Ordinal))
|
||||
dayFixtures.Add(r);
|
||||
}
|
||||
|
||||
if (dayFixtures.Count == 0)
|
||||
return (new DataTable(), $"Nessuna partita trovata per il {targetDateStr} nei CSV");
|
||||
|
||||
// Sort by kick-off time
|
||||
dayFixtures = dayFixtures
|
||||
.OrderBy(r => Get(r, "date"))
|
||||
.ToList();
|
||||
|
||||
// ── Build output schema ──────────────────────────────────────────────
|
||||
var dt = new DataTable();
|
||||
void AddCol(string name) { if (!dt.Columns.Contains(name)) dt.Columns.Add(name, typeof(string)); }
|
||||
|
||||
// Fixture core
|
||||
AddCol("eventId"); AddCol("Data/Ora"); AddCol("StatoPartita");
|
||||
AddCol("Lega"); AddCol("LeagueShortName"); AddCol("Stagione");
|
||||
AddCol("Stadio"); AddCol("Città"); AddCol("Paese Stadio"); AddCol("CapacitàStadio"); AddCol("Presenze");
|
||||
// Home team
|
||||
AddCol("SquadraCasa"); AddCol("NomeBreveCasa"); AddCol("ColoreCasa");
|
||||
AddCol("VincitoreCasa"); AddCol("GoalCasa"); AddCol("RigoreCasa");
|
||||
// Away team
|
||||
AddCol("SquadraOspite"); AddCol("NomeBreviOspite"); AddCol("ColoreOspite");
|
||||
AddCol("VincitoireOspite"); AddCol("GoalOspite"); AddCol("RigoreOspite");
|
||||
// Home stats
|
||||
AddCol("H_PossessoPct"); AddCol("H_Falli"); AddCol("H_GialleCasa"); AddCol("H_RosseCasa");
|
||||
AddCol("H_Fuorigioco"); AddCol("H_Angoli"); AddCol("H_Parate");
|
||||
AddCol("H_Tiri"); AddCol("H_TiriInPorta"); AddCol("H_TiriPct");
|
||||
AddCol("H_PassaggiAccurati"); AddCol("H_PassaggiTotali"); AddCol("H_PassaggiPct");
|
||||
AddCol("H_CrossAccurati"); AddCol("H_CrossTotali");
|
||||
AddCol("H_LongBallAccurati"); AddCol("H_LongBallTotali");
|
||||
AddCol("H_TiriBloccati"); AddCol("H_TacklePct"); AddCol("H_Intercetti");
|
||||
AddCol("H_Clearance"); AddCol("H_RigoriFatti"); AddCol("H_RigoriTentati");
|
||||
// Away stats
|
||||
AddCol("A_PossessoPct"); AddCol("A_Falli"); AddCol("A_GialleOspite"); AddCol("A_RosseOspite");
|
||||
AddCol("A_Fuorigioco"); AddCol("A_Angoli"); AddCol("A_Parate");
|
||||
AddCol("A_Tiri"); AddCol("A_TiriInPorta"); AddCol("A_TiriPct");
|
||||
AddCol("A_PassaggiAccurati"); AddCol("A_PassaggiTotali"); AddCol("A_PassaggiPct");
|
||||
AddCol("A_CrossAccurati"); AddCol("A_CrossTotali");
|
||||
AddCol("A_LongBallAccurati"); AddCol("A_LongBallTotali");
|
||||
AddCol("A_TiriBloccati"); AddCol("A_TacklePct"); AddCol("A_Intercetti");
|
||||
AddCol("A_Clearance"); AddCol("A_RigoriFatti"); AddCol("A_RigoriTentati");
|
||||
// Home standings
|
||||
AddCol("H_Posizione"); AddCol("H_PartiteGiocate"); AddCol("H_Vittorie");
|
||||
AddCol("H_Pareggi"); AddCol("H_Sconfitte"); AddCol("H_Punti");
|
||||
AddCol("H_GoalFatti"); AddCol("H_GoalSubiti"); AddCol("H_DiffReti");
|
||||
AddCol("H_PortaInviolata"); AddCol("H_Forma"); AddCol("H_ProssimoAvversario");
|
||||
// Away standings
|
||||
AddCol("A_Posizione"); AddCol("A_PartiteGiocate"); AddCol("A_Vittorie");
|
||||
AddCol("A_Pareggi"); AddCol("A_Sconfitte"); AddCol("A_Punti");
|
||||
AddCol("A_GoalFatti"); AddCol("A_GoalSubiti"); AddCol("A_DiffReti");
|
||||
AddCol("A_PortaInviolata"); AddCol("A_Forma"); AddCol("A_ProssimoAvversario");
|
||||
// Home roster summary
|
||||
AddCol("H_Portieri"); AddCol("H_Difensori"); AddCol("H_Centrocampisti"); AddCol("H_Attaccanti");
|
||||
AddCol("H_NumeroGiocatori");
|
||||
// Away roster summary
|
||||
AddCol("A_Portieri"); AddCol("A_Difensori"); AddCol("A_Centrocampisti"); AddCol("A_Attaccanti");
|
||||
AddCol("A_NumeroGiocatori");
|
||||
// Raw IDs (useful for AI)
|
||||
AddCol("leagueId"); AddCol("homeTeamId"); AddCol("awayTeamId"); AddCol("venueId"); AddCol("statusId");
|
||||
|
||||
status?.Report("Costruzione righe output…");
|
||||
progress?.Report(50);
|
||||
|
||||
int total = dayFixtures.Count;
|
||||
int done = 0;
|
||||
|
||||
foreach (var fx in dayFixtures)
|
||||
{
|
||||
var eventId = Get(fx, "eventId");
|
||||
var leagueId = Get(fx, "leagueId");
|
||||
var homeTeamId = Get(fx, "homeTeamId");
|
||||
var awayTeamId = Get(fx, "awayTeamId");
|
||||
var venueId = Get(fx, "venueId");
|
||||
var statusId = Get(fx, "statusId");
|
||||
var dateStr = Get(fx, "date");
|
||||
|
||||
teamsById.TryGetValue(homeTeamId, out var homeTeam);
|
||||
teamsById.TryGetValue(awayTeamId, out var awayTeam);
|
||||
leaguesById.TryGetValue(leagueId, out var league);
|
||||
venuesById.TryGetValue(venueId, out var venue);
|
||||
statusById.TryGetValue(statusId, out var stat);
|
||||
|
||||
teamStatsByEventTeam.TryGetValue($"{eventId}|{homeTeamId}", out var hStats);
|
||||
teamStatsByEventTeam.TryGetValue($"{eventId}|{awayTeamId}", out var aStats);
|
||||
|
||||
standingsByLeagueTeam.TryGetValue($"{leagueId}|{homeTeamId}", out var hStand);
|
||||
standingsByLeagueTeam.TryGetValue($"{leagueId}|{awayTeamId}", out var aStand);
|
||||
|
||||
// Roster summaries
|
||||
string RosterSummary(string teamId, string pos)
|
||||
{
|
||||
if (!rosterByTeam.TryGetValue(teamId, out var rosterRows)) return "";
|
||||
var names = new List<string>();
|
||||
foreach (var rr in rosterRows)
|
||||
{
|
||||
if (!Get(rr, "position").Contains(pos, StringComparison.OrdinalIgnoreCase)) continue;
|
||||
var aid = Get(rr, "athleteId");
|
||||
playersById.TryGetValue(aid, out var pl);
|
||||
var name = pl != null ? Get(pl, "displayName") : Get(rr, "playerDisplayName");
|
||||
if (!string.IsNullOrWhiteSpace(name)) names.Add(name);
|
||||
}
|
||||
return string.Join("; ", names);
|
||||
}
|
||||
|
||||
int RosterCount(string teamId)
|
||||
=> rosterByTeam.TryGetValue(teamId, out var rr) ? rr.Count : 0;
|
||||
|
||||
var row = dt.NewRow();
|
||||
|
||||
Set(row, "eventId", eventId);
|
||||
Set(row, "Data/Ora", dateStr);
|
||||
Set(row, "StatoPartita", Get(stat, "description"));
|
||||
Set(row, "Lega", Get(league, "leagueName"));
|
||||
Set(row, "LeagueShortName", Get(league, "leagueShortName"));
|
||||
Set(row, "Stagione", Get(league, "year"));
|
||||
Set(row, "Stadio", Get(venue, "fullName"));
|
||||
Set(row, "Città", Get(venue, "city"));
|
||||
Set(row, "Paese Stadio", Get(venue, "country"));
|
||||
Set(row, "CapacitàStadio", Get(venue, "capacity"));
|
||||
Set(row, "Presenze", Get(fx, "attendance"));
|
||||
|
||||
Set(row, "SquadraCasa", Get(homeTeam, "displayName"));
|
||||
Set(row, "NomeBreveCasa", Get(homeTeam, "shortDisplayName"));
|
||||
Set(row, "ColoreCasa", Get(homeTeam, "color"));
|
||||
Set(row, "VincitoreCasa", Get(fx, "homeTeamWinner"));
|
||||
Set(row, "GoalCasa", Get(fx, "homeTeamScore"));
|
||||
Set(row, "RigoreCasa", Get(fx, "homeTeamShootoutScore"));
|
||||
|
||||
Set(row, "SquadraOspite", Get(awayTeam, "displayName"));
|
||||
Set(row, "NomeBreviOspite", Get(awayTeam, "shortDisplayName"));
|
||||
Set(row, "ColoreOspite", Get(awayTeam, "color"));
|
||||
Set(row, "VincitoireOspite", Get(fx, "awayTeamWinner"));
|
||||
Set(row, "GoalOspite", Get(fx, "awayTeamScore"));
|
||||
Set(row, "RigoreOspite", Get(fx, "awayTeamShootoutScore"));
|
||||
|
||||
// Home team stats
|
||||
Set(row, "H_PossessoPct", Get(hStats, "possessionPct"));
|
||||
Set(row, "H_Falli", Get(hStats, "foulsCommitted"));
|
||||
Set(row, "H_GialleCasa", Get(hStats, "yellowCards"));
|
||||
Set(row, "H_RosseCasa", Get(hStats, "redCards"));
|
||||
Set(row, "H_Fuorigioco", Get(hStats, "offsides"));
|
||||
Set(row, "H_Angoli", Get(hStats, "wonCorners"));
|
||||
Set(row, "H_Parate", Get(hStats, "saves"));
|
||||
Set(row, "H_Tiri", Get(hStats, "totalShots"));
|
||||
Set(row, "H_TiriInPorta", Get(hStats, "shotsOnTarget"));
|
||||
Set(row, "H_TiriPct", Get(hStats, "shotPct"));
|
||||
Set(row, "H_PassaggiAccurati", Get(hStats, "accuratePasses"));
|
||||
Set(row, "H_PassaggiTotali", Get(hStats, "totalPasses"));
|
||||
Set(row, "H_PassaggiPct", Get(hStats, "passPct"));
|
||||
Set(row, "H_CrossAccurati", Get(hStats, "accurateCrosses"));
|
||||
Set(row, "H_CrossTotali", Get(hStats, "totalCrosses"));
|
||||
Set(row, "H_LongBallAccurati", Get(hStats, "accurateLongBalls"));
|
||||
Set(row, "H_LongBallTotali", Get(hStats, "totalLongBalls"));
|
||||
Set(row, "H_TiriBloccati", Get(hStats, "blockedShots"));
|
||||
Set(row, "H_TacklePct", Get(hStats, "tacklePct"));
|
||||
Set(row, "H_Intercetti", Get(hStats, "interceptions"));
|
||||
Set(row, "H_Clearance", Get(hStats, "totalClearance"));
|
||||
Set(row, "H_RigoriFatti", Get(hStats, "penaltyKickGoals"));
|
||||
Set(row, "H_RigoriTentati", Get(hStats, "penaltyKickShots"));
|
||||
|
||||
// Away team stats
|
||||
Set(row, "A_PossessoPct", Get(aStats, "possessionPct"));
|
||||
Set(row, "A_Falli", Get(aStats, "foulsCommitted"));
|
||||
Set(row, "A_GialleOspite", Get(aStats, "yellowCards"));
|
||||
Set(row, "A_RosseOspite", Get(aStats, "redCards"));
|
||||
Set(row, "A_Fuorigioco", Get(aStats, "offsides"));
|
||||
Set(row, "A_Angoli", Get(aStats, "wonCorners"));
|
||||
Set(row, "A_Parate", Get(aStats, "saves"));
|
||||
Set(row, "A_Tiri", Get(aStats, "totalShots"));
|
||||
Set(row, "A_TiriInPorta", Get(aStats, "shotsOnTarget"));
|
||||
Set(row, "A_TiriPct", Get(aStats, "shotPct"));
|
||||
Set(row, "A_PassaggiAccurati", Get(aStats, "accuratePasses"));
|
||||
Set(row, "A_PassaggiTotali", Get(aStats, "totalPasses"));
|
||||
Set(row, "A_PassaggiPct", Get(aStats, "passPct"));
|
||||
Set(row, "A_CrossAccurati", Get(aStats, "accurateCrosses"));
|
||||
Set(row, "A_CrossTotali", Get(aStats, "totalCrosses"));
|
||||
Set(row, "A_LongBallAccurati", Get(aStats, "accurateLongBalls"));
|
||||
Set(row, "A_LongBallTotali", Get(aStats, "totalLongBalls"));
|
||||
Set(row, "A_TiriBloccati", Get(aStats, "blockedShots"));
|
||||
Set(row, "A_TacklePct", Get(aStats, "tacklePct"));
|
||||
Set(row, "A_Intercetti", Get(aStats, "interceptions"));
|
||||
Set(row, "A_Clearance", Get(aStats, "totalClearance"));
|
||||
Set(row, "A_RigoriFatti", Get(aStats, "penaltyKickGoals"));
|
||||
Set(row, "A_RigoriTentati", Get(aStats, "penaltyKickShots"));
|
||||
|
||||
// Home standings
|
||||
Set(row, "H_Posizione", Get(hStand, "teamRank"));
|
||||
Set(row, "H_PartiteGiocate", Get(hStand, "gamesPlayed"));
|
||||
Set(row, "H_Vittorie", Get(hStand, "wins"));
|
||||
Set(row, "H_Pareggi", Get(hStand, "ties"));
|
||||
Set(row, "H_Sconfitte", Get(hStand, "losses"));
|
||||
Set(row, "H_Punti", Get(hStand, "points"));
|
||||
Set(row, "H_GoalFatti", Get(hStand, "gf"));
|
||||
Set(row, "H_GoalSubiti", Get(hStand, "ga"));
|
||||
Set(row, "H_DiffReti", Get(hStand, "gd"));
|
||||
Set(row, "H_PortaInviolata", Get(hStand, "clean_sheet"));
|
||||
Set(row, "H_Forma", Get(hStand, "form"));
|
||||
Set(row, "H_ProssimoAvversario", Get(hStand, "next_opponent"));
|
||||
|
||||
// Away standings
|
||||
Set(row, "A_Posizione", Get(aStand, "teamRank"));
|
||||
Set(row, "A_PartiteGiocate", Get(aStand, "gamesPlayed"));
|
||||
Set(row, "A_Vittorie", Get(aStand, "wins"));
|
||||
Set(row, "A_Pareggi", Get(aStand, "ties"));
|
||||
Set(row, "A_Sconfitte", Get(aStand, "losses"));
|
||||
Set(row, "A_Punti", Get(aStand, "points"));
|
||||
Set(row, "A_GoalFatti", Get(aStand, "gf"));
|
||||
Set(row, "A_GoalSubiti", Get(aStand, "ga"));
|
||||
Set(row, "A_DiffReti", Get(aStand, "gd"));
|
||||
Set(row, "A_PortaInviolata", Get(aStand, "clean_sheet"));
|
||||
Set(row, "A_Forma", Get(aStand, "form"));
|
||||
Set(row, "A_ProssimoAvversario", Get(aStand, "next_opponent"));
|
||||
|
||||
// Roster summaries
|
||||
Set(row, "H_Portieri", RosterSummary(homeTeamId, "Goalkeeper"));
|
||||
Set(row, "H_Difensori", RosterSummary(homeTeamId, "Defender"));
|
||||
Set(row, "H_Centrocampisti", RosterSummary(homeTeamId, "Midfielder"));
|
||||
Set(row, "H_Attaccanti", RosterSummary(homeTeamId, "Forward"));
|
||||
Set(row, "H_NumeroGiocatori", RosterCount(homeTeamId).ToString());
|
||||
|
||||
Set(row, "A_Portieri", RosterSummary(awayTeamId, "Goalkeeper"));
|
||||
Set(row, "A_Difensori", RosterSummary(awayTeamId, "Defender"));
|
||||
Set(row, "A_Centrocampisti", RosterSummary(awayTeamId, "Midfielder"));
|
||||
Set(row, "A_Attaccanti", RosterSummary(awayTeamId, "Forward"));
|
||||
Set(row, "A_NumeroGiocatori", RosterCount(awayTeamId).ToString());
|
||||
|
||||
// Raw IDs
|
||||
Set(row, "leagueId", leagueId);
|
||||
Set(row, "homeTeamId", homeTeamId);
|
||||
Set(row, "awayTeamId", awayTeamId);
|
||||
Set(row, "venueId", venueId);
|
||||
Set(row, "statusId", statusId);
|
||||
|
||||
dt.Rows.Add(row);
|
||||
done++;
|
||||
progress?.Report(50 + (int)((double)done / total * 45));
|
||||
status?.Report($"Elaborazione partite… {done}/{total}");
|
||||
}
|
||||
|
||||
progress?.Report(100);
|
||||
status?.Report($"Caricate {dt.Rows.Count} partite dal CSV ESPN Soccer");
|
||||
return (dt, null);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Globalization;
|
||||
using System.Windows;
|
||||
using System.Windows.Data;
|
||||
|
||||
namespace HorseRacingPredictor.Football
|
||||
{
|
||||
/// <summary>
|
||||
/// Converte bool → Visibility (True=Visible, False=Collapsed).
|
||||
/// </summary>
|
||||
[ValueConversion(typeof(bool), typeof(Visibility))]
|
||||
public class BoolToVisibilityConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
=> value is true ? Visibility.Visible : Visibility.Collapsed;
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
=> value is Visibility v && v == Visibility.Visible;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Converte ICollection → Visibility (Count>0 → Visible, altrimenti Collapsed).
|
||||
/// Usato per nascondere i tab di dettaglio quando la collezione è vuota.
|
||||
/// </summary>
|
||||
[ValueConversion(typeof(ICollection), typeof(Visibility))]
|
||||
public class CollectionToVisibilityConverter : IValueConverter
|
||||
{
|
||||
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
{
|
||||
if (value is ICollection col && col.Count > 0) return Visibility.Visible;
|
||||
return Visibility.Collapsed;
|
||||
}
|
||||
|
||||
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
|
||||
=> throw new NotSupportedException();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace HorseRacingPredictor.Football
|
||||
{
|
||||
// ?? Preset lega ??????????????????????????????????????????????????????????
|
||||
|
||||
/// <summary>
|
||||
/// Rappresenta una lega/competizione nota con ID API-Football.
|
||||
/// </summary>
|
||||
public class LeaguePreset
|
||||
{
|
||||
public int Id { get; init; }
|
||||
public string Name { get; init; }
|
||||
public string Country { get; init; }
|
||||
public string Category{ get; init; } // "Nazionale" | "Internazionale"
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parametri configurabili per controllare quali endpoint API-Football scaricare
|
||||
/// e come filtrare i dati risultanti.
|
||||
/// Sorgente unica: API-Football (https://www.api-football.com/).
|
||||
/// </summary>
|
||||
public class FootballDownloadOptions
|
||||
{
|
||||
// ?? Endpoint da scaricare ?????????????????????????????????????????????
|
||||
public bool DownloadFixtures { get; set; } = true;
|
||||
public bool DownloadOdds { get; set; } = true;
|
||||
public bool DownloadPredictions{ get; set; } = true;
|
||||
public bool DownloadStandings { get; set; } = false;
|
||||
public bool DownloadH2H { get; set; } = false;
|
||||
public bool DownloadEvents { get; set; } = false;
|
||||
public bool DownloadLineups { get; set; } = false;
|
||||
public bool DownloadStatistics { get; set; } = false;
|
||||
public bool DownloadInjuries { get; set; } = false;
|
||||
|
||||
// Supplementary league-level data
|
||||
public bool DownloadTopScorers { get; set; } = false;
|
||||
public bool DownloadTopAssists { get; set; } = false;
|
||||
public bool DownloadTopCards { get; set; } = false;
|
||||
public bool DownloadTeamStats { get; set; } = false;
|
||||
public bool DownloadPlayerStats { get; set; } = false;
|
||||
public bool DownloadSquads { get; set; } = false;
|
||||
public bool DownloadCoaches { get; set; } = false;
|
||||
public bool DownloadTransfers { get; set; } = false;
|
||||
|
||||
/// <summary>Returns true if at least one supplementary endpoint is enabled.</summary>
|
||||
public bool AnySupplementarySelected =>
|
||||
DownloadTopScorers || DownloadTopAssists || DownloadTopCards ||
|
||||
DownloadTeamStats || DownloadPlayerStats || DownloadSquads ||
|
||||
DownloadCoaches || DownloadTransfers;
|
||||
|
||||
// ?? Filtri
|
||||
/// <summary>
|
||||
/// Se non vuoto, scarica fixture solo per queste leghe (league IDs).
|
||||
/// Lista vuota = tutte le leghe.
|
||||
/// </summary>
|
||||
public List<int> LeagueIds { get; set; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Timezone IANA per le date delle fixture (es. "Europe/Rome").
|
||||
/// </summary>
|
||||
public string Timezone { get; set; } = "Europe/Rome";
|
||||
|
||||
/// <summary>
|
||||
/// Stagione corrente per le classifiche (calcolata automaticamente se 0).
|
||||
/// </summary>
|
||||
public int Season { get; set; } = 0;
|
||||
|
||||
/// <summary>
|
||||
/// Ritardo in millisecondi tra una chiamata API e l'altra per rispettare il rate limit.
|
||||
/// </summary>
|
||||
public int ApiDelayMs { get; set; } = 300;
|
||||
|
||||
/// <summary>
|
||||
/// Se true, controlla la quota residua prima di iniziare e si ferma quando insufficiente.
|
||||
/// </summary>
|
||||
public bool CheckQuota { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Numero minimo di richieste residue prima di fermare il download.
|
||||
/// </summary>
|
||||
public int MinRemainingQuota { get; set; } = 10;
|
||||
|
||||
/// <summary>
|
||||
/// Ora minima per filtrare le fixture (null = nessun limite inferiore).
|
||||
/// Il filtro viene applicato lato client dopo il download.
|
||||
/// </summary>
|
||||
public TimeSpan? TimeFrom { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Ora massima per filtrare le fixture (null = nessun limite superiore).
|
||||
/// Il filtro viene applicato lato client dopo il download.
|
||||
/// </summary>
|
||||
public TimeSpan? TimeTo { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Restituisce la stagione corrente calcolata (anno corrente o anno-1 se prima di luglio).
|
||||
/// </summary>
|
||||
public int GetEffectiveSeason()
|
||||
{
|
||||
if (Season > 0) return Season;
|
||||
var now = System.DateTime.Now;
|
||||
return now.Month >= 7 ? now.Year : now.Year - 1;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Conta il numero approssimativo di chiamate API previste per una data.
|
||||
/// Utile per mostrare all'utente una stima.
|
||||
/// </summary>
|
||||
public int EstimateApiCalls(int fixtureCount)
|
||||
{
|
||||
int calls = 0;
|
||||
if (DownloadFixtures) calls += 1;
|
||||
if (DownloadOdds) calls += 1;
|
||||
if (DownloadPredictions) calls += fixtureCount;
|
||||
if (DownloadStandings && LeagueIds.Count > 0) calls += LeagueIds.Count;
|
||||
else if (DownloadStandings) calls += 5;
|
||||
if (DownloadH2H) calls += fixtureCount;
|
||||
if (DownloadEvents) calls += fixtureCount;
|
||||
if (DownloadLineups) calls += fixtureCount;
|
||||
if (DownloadStatistics) calls += fixtureCount;
|
||||
if (DownloadInjuries) calls += 1;
|
||||
if (CheckQuota) calls += 1;
|
||||
return calls;
|
||||
}
|
||||
|
||||
// ?? Preset leghe conosciute ??????????????????????????????????????????
|
||||
|
||||
/// <summary>
|
||||
/// Lista predefinita delle principali leghe nazionali e competizioni internazionali europee.
|
||||
/// ID basati su API-Football v3.
|
||||
/// </summary>
|
||||
public static readonly IReadOnlyList<LeaguePreset> WellKnownLeagues = new List<LeaguePreset>
|
||||
{
|
||||
// ?? Nazionali ????????????????????????????????????????????????????
|
||||
new() { Id = 135, Name = "Serie A", Country = "Italy", Category = "Nazionale" },
|
||||
new() { Id = 136, Name = "Serie B", Country = "Italy", Category = "Nazionale" },
|
||||
new() { Id = 61, Name = "Ligue 1", Country = "France", Category = "Nazionale" },
|
||||
new() { Id = 62, Name = "Ligue 2", Country = "France", Category = "Nazionale" },
|
||||
new() { Id = 78, Name = "Bundesliga", Country = "Germany", Category = "Nazionale" },
|
||||
new() { Id = 79, Name = "2. Bundesliga", Country = "Germany", Category = "Nazionale" },
|
||||
new() { Id = 39, Name = "Premier League", Country = "England", Category = "Nazionale" },
|
||||
new() { Id = 40, Name = "Championship", Country = "England", Category = "Nazionale" },
|
||||
new() { Id = 140, Name = "La Liga", Country = "Spain", Category = "Nazionale" },
|
||||
new() { Id = 141, Name = "La Liga 2", Country = "Spain", Category = "Nazionale" },
|
||||
new() { Id = 88, Name = "Eredivisie", Country = "Netherlands", Category = "Nazionale" },
|
||||
new() { Id = 94, Name = "Primeira Liga", Country = "Portugal", Category = "Nazionale" },
|
||||
new() { Id = 144, Name = "Pro League", Country = "Belgium", Category = "Nazionale" },
|
||||
new() { Id = 179, Name = "Premiership", Country = "Scotland", Category = "Nazionale" },
|
||||
new() { Id = 106, Name = "Ekstraklasa", Country = "Poland", Category = "Nazionale" },
|
||||
new() { Id = 197, Name = "Super League", Country = "Greece", Category = "Nazionale" },
|
||||
new() { Id = 203, Name = "Süper Lig", Country = "Turkey", Category = "Nazionale" },
|
||||
new() { Id = 235, Name = "Premier League", Country = "Russia", Category = "Nazionale" },
|
||||
new() { Id = 98, Name = "Jupiler Pro League", Country = "Belgium", Category = "Nazionale" },
|
||||
new() { Id = 218, Name = "Bundesliga", Country = "Austria", Category = "Nazionale" },
|
||||
new() { Id = 207, Name = "Super League", Country = "Switzerland", Category = "Nazionale" },
|
||||
new() { Id = 244, Name = "Allsvenskan", Country = "Sweden", Category = "Nazionale" },
|
||||
new() { Id = 103, Name = "Eliteserien", Country = "Norway", Category = "Nazionale" },
|
||||
new() { Id = 119, Name = "Superliga", Country = "Denmark", Category = "Nazionale" },
|
||||
new() { Id = 113, Name = "Veikkausliiga", Country = "Finland", Category = "Nazionale" },
|
||||
new() { Id = 172, Name = "Premier League", Country = "Ukraine", Category = "Nazionale" },
|
||||
new() { Id = 333, Name = "Pro League", Country = "Saudi Arabia",Category = "Nazionale" },
|
||||
new() { Id = 128, Name = "Liga Profesional", Country = "Argentina", Category = "Nazionale" },
|
||||
new() { Id = 71, Name = "Brasileirão Série A", Country = "Brazil", Category = "Nazionale" },
|
||||
new() { Id = 262, Name = "Liga MX", Country = "Mexico", Category = "Nazionale" },
|
||||
|
||||
// ?? Internazionali UEFA ??????????????????????????????????????????
|
||||
new() { Id = 2, Name = "UEFA Champions League", Country = "World", Category = "Internazionale" },
|
||||
new() { Id = 3, Name = "UEFA Europa League", Country = "World", Category = "Internazionale" },
|
||||
new() { Id = 848, Name = "UEFA Europa Conference League", Country = "World", Category = "Internazionale" },
|
||||
new() { Id = 4, Name = "UEFA Super Cup", Country = "World", Category = "Internazionale" },
|
||||
new() { Id = 960, Name = "UEFA Nations League", Country = "World", Category = "Internazionale" },
|
||||
new() { Id = 1, Name = "World Cup", Country = "World", Category = "Internazionale" },
|
||||
new() { Id = 5, Name = "UEFA Euro", Country = "World", Category = "Internazionale" },
|
||||
new() { Id = 6, Name = "Africa Cup of Nations", Country = "World", Category = "Internazionale" },
|
||||
new() { Id = 7, Name = "Copa America", Country = "World", Category = "Internazionale" },
|
||||
new() { Id = 15, Name = "FIFA Club World Cup", Country = "World", Category = "Internazionale" },
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,334 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Text.Encodings.Web;
|
||||
using System.Text.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using System.Xml;
|
||||
|
||||
namespace HorseRacingPredictor.Football
|
||||
{
|
||||
/// <summary>
|
||||
/// Esporta una lista di <see cref="FootballMatchViewModel"/> in JSON o XML gerarchico.
|
||||
/// I dati 1:N (eventi, formazioni, H2H, statistiche, infortuni, quote) sono annidati
|
||||
/// all'interno del nodo/oggetto di ogni partita.
|
||||
/// </summary>
|
||||
public static class FootballExporter
|
||||
{
|
||||
// ── JSON ─────────────────────────────────────────────────────────────
|
||||
|
||||
public static void ExportJson(
|
||||
IReadOnlyList<FootballMatchViewModel> matches,
|
||||
string folder,
|
||||
string filename,
|
||||
Action<string> setStatus = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
Directory.CreateDirectory(folder);
|
||||
var path = Path.Combine(folder, filename.EndsWith(".json", StringComparison.OrdinalIgnoreCase)
|
||||
? filename : filename + ".json");
|
||||
|
||||
var opts = new JsonSerializerOptions
|
||||
{
|
||||
WriteIndented = true,
|
||||
Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping,
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
|
||||
};
|
||||
|
||||
// Costruisce struttura serializzabile
|
||||
var list = new List<object>();
|
||||
foreach (var m in matches)
|
||||
{
|
||||
var obj = new
|
||||
{
|
||||
fixture = new
|
||||
{
|
||||
id = m.FixtureId,
|
||||
dataOra = m.DataOra,
|
||||
stato = m.Stato,
|
||||
lega = m.Lega,
|
||||
nazione = m.Nazione,
|
||||
stagione = m.Stagione,
|
||||
squadraCasa = m.SquadraCasa,
|
||||
squadraOspite = m.SquadraOspite,
|
||||
risultato = m.Risultato,
|
||||
arbitro = m.Arbitro,
|
||||
stadio = m.Stadio,
|
||||
città = m.Città,
|
||||
},
|
||||
predizione = new
|
||||
{
|
||||
consiglio = m.PredizioneAdvice,
|
||||
finaleBase = m.PredizioneFinaleBase,
|
||||
percCasa = m.PredPercCasa,
|
||||
percPari = m.PredPercPari,
|
||||
percOspite = m.PredPercOspite,
|
||||
goalHome = m.PredGoalsHome,
|
||||
goalAway = m.PredGoalsAway,
|
||||
formCasa = m.FormCasa,
|
||||
formOspite = m.FormOspite,
|
||||
attaccoCasa = m.AttaccoCasa,
|
||||
difesaCasa = m.DifesaCasa,
|
||||
attaccoOspite = m.AttaccoOspite,
|
||||
difesaOspite = m.DifesaOspite,
|
||||
poissonHome = m.PoissonHome,
|
||||
poissonAway = m.PoissonAway,
|
||||
poissonDraw = m.PoissonDraw,
|
||||
under25 = m.Under25,
|
||||
over25 = m.Over25,
|
||||
goalGoal = m.GoalGoal,
|
||||
noGoal = m.NoGoal,
|
||||
},
|
||||
classificaCasa = new
|
||||
{
|
||||
posizione = m.ClassificaCasa,
|
||||
punti = m.PuntiCasa,
|
||||
vittorie = m.VittorieCasa,
|
||||
pareggi = m.PareggiCasa,
|
||||
sconfitte = m.SconfitteCasa,
|
||||
goalFatti = m.GoalFattiCasa,
|
||||
goalSubiti = m.GoalSubitiCasa,
|
||||
},
|
||||
classificaOspite = new
|
||||
{
|
||||
posizione = m.ClassificaOspite,
|
||||
punti = m.PuntiOspite,
|
||||
vittorie = m.VittorieOspite,
|
||||
pareggi = m.PareggiOspite,
|
||||
sconfitte = m.SconfitteOspite,
|
||||
goalFatti = m.GoalFattiOspite,
|
||||
goalSubiti = m.GoalSubitiOspite,
|
||||
},
|
||||
infoWeb = m.InfoWebPartita,
|
||||
eventi = m.Events,
|
||||
formazioni = m.Lineups,
|
||||
h2h = m.H2H,
|
||||
statistiche= m.Statistics,
|
||||
infortuni = m.Injuries,
|
||||
quote = m.Odds,
|
||||
};
|
||||
list.Add(obj);
|
||||
}
|
||||
|
||||
var json = JsonSerializer.Serialize(new { partite = list }, opts);
|
||||
File.WriteAllText(path, json, Encoding.UTF8);
|
||||
setStatus?.Invoke($"✅ JSON salvato: {path}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
setStatus?.Invoke($"❌ Errore export JSON: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
// ── XML ──────────────────────────────────────────────────────────────
|
||||
|
||||
public static void ExportXml(
|
||||
IReadOnlyList<FootballMatchViewModel> matches,
|
||||
string folder,
|
||||
string filename,
|
||||
Action<string> setStatus = null)
|
||||
{
|
||||
try
|
||||
{
|
||||
Directory.CreateDirectory(folder);
|
||||
var path = Path.Combine(folder, filename.EndsWith(".xml", StringComparison.OrdinalIgnoreCase)
|
||||
? filename : filename + ".xml");
|
||||
|
||||
var settings = new XmlWriterSettings
|
||||
{
|
||||
Indent = true,
|
||||
IndentChars = " ",
|
||||
Encoding = Encoding.UTF8
|
||||
};
|
||||
|
||||
using var writer = XmlWriter.Create(path, settings);
|
||||
writer.WriteStartDocument();
|
||||
writer.WriteStartElement("partite");
|
||||
|
||||
foreach (var m in matches)
|
||||
{
|
||||
writer.WriteStartElement("partita");
|
||||
|
||||
// ── Fixture ──────────────────────────────────────────────
|
||||
writer.WriteStartElement("fixture");
|
||||
WriteXmlEl(writer, "id", m.FixtureId);
|
||||
WriteXmlEl(writer, "dataOra", m.DataOra);
|
||||
WriteXmlEl(writer, "stato", m.Stato);
|
||||
WriteXmlEl(writer, "lega", m.Lega);
|
||||
WriteXmlEl(writer, "nazione", m.Nazione);
|
||||
WriteXmlEl(writer, "stagione", m.Stagione);
|
||||
WriteXmlEl(writer, "squadraCasa", m.SquadraCasa);
|
||||
WriteXmlEl(writer, "squadraOspite", m.SquadraOspite);
|
||||
WriteXmlEl(writer, "risultato", m.Risultato);
|
||||
WriteXmlEl(writer, "arbitro", m.Arbitro);
|
||||
WriteXmlEl(writer, "stadio", m.Stadio);
|
||||
WriteXmlEl(writer, "citta", m.Città);
|
||||
writer.WriteEndElement();
|
||||
|
||||
// ── Predizione ───────────────────────────────────────────
|
||||
writer.WriteStartElement("predizione");
|
||||
WriteXmlEl(writer, "consiglio", m.PredizioneAdvice);
|
||||
WriteXmlEl(writer, "finaleBase", m.PredizioneFinaleBase);
|
||||
WriteXmlEl(writer, "percCasa", m.PredPercCasa);
|
||||
WriteXmlEl(writer, "percPari", m.PredPercPari);
|
||||
WriteXmlEl(writer, "percOspite", m.PredPercOspite);
|
||||
WriteXmlEl(writer, "goalHome", m.PredGoalsHome);
|
||||
WriteXmlEl(writer, "goalAway", m.PredGoalsAway);
|
||||
WriteXmlEl(writer, "formCasa", m.FormCasa);
|
||||
WriteXmlEl(writer, "formOspite", m.FormOspite);
|
||||
WriteXmlEl(writer, "attaccoCasa", m.AttaccoCasa);
|
||||
WriteXmlEl(writer, "difesaCasa", m.DifesaCasa);
|
||||
WriteXmlEl(writer, "attaccoOspite", m.AttaccoOspite);
|
||||
WriteXmlEl(writer, "difesaOspite", m.DifesaOspite);
|
||||
WriteXmlEl(writer, "poissonHome", m.PoissonHome);
|
||||
WriteXmlEl(writer, "poissonAway", m.PoissonAway);
|
||||
WriteXmlEl(writer, "poissonDraw", m.PoissonDraw);
|
||||
WriteXmlEl(writer, "under25", m.Under25);
|
||||
WriteXmlEl(writer, "over25", m.Over25);
|
||||
WriteXmlEl(writer, "goalGoal", m.GoalGoal);
|
||||
WriteXmlEl(writer, "noGoal", m.NoGoal);
|
||||
writer.WriteEndElement();
|
||||
|
||||
// ── Classifiche ──────────────────────────────────────────
|
||||
WriteXmlStanding(writer, "classificaCasa", m.ClassificaCasa, m.PuntiCasa, m.VittorieCasa, m.PareggiCasa, m.SconfitteCasa, m.GoalFattiCasa, m.GoalSubitiCasa);
|
||||
WriteXmlStanding(writer, "classificaOspite", m.ClassificaOspite, m.PuntiOspite, m.VittorieOspite, m.PareggiOspite, m.SconfitteOspite, m.GoalFattiOspite, m.GoalSubitiOspite);
|
||||
|
||||
// ── Info Web ─────────────────────────────────────────────
|
||||
if (!string.IsNullOrWhiteSpace(m.InfoWebPartita))
|
||||
WriteXmlEl(writer, "infoWeb", m.InfoWebPartita);
|
||||
|
||||
// ── 1:N ──────────────────────────────────────────────────
|
||||
if (m.Events?.Count > 0)
|
||||
{
|
||||
writer.WriteStartElement("eventi");
|
||||
foreach (var ev in m.Events)
|
||||
{
|
||||
writer.WriteStartElement("evento");
|
||||
WriteXmlEl(writer, "minuto", ev.Minuto);
|
||||
WriteXmlEl(writer, "tipo", ev.Tipo);
|
||||
WriteXmlEl(writer, "dettaglio", ev.Dettaglio);
|
||||
WriteXmlEl(writer, "squadra", ev.Squadra);
|
||||
WriteXmlEl(writer, "giocatore", ev.Giocatore);
|
||||
WriteXmlEl(writer, "assistito", ev.Assistito);
|
||||
writer.WriteEndElement();
|
||||
}
|
||||
writer.WriteEndElement();
|
||||
}
|
||||
|
||||
if (m.Lineups?.Count > 0)
|
||||
{
|
||||
writer.WriteStartElement("formazioni");
|
||||
foreach (var l in m.Lineups)
|
||||
{
|
||||
writer.WriteStartElement("giocatore");
|
||||
WriteXmlEl(writer, "squadra", l.Squadra);
|
||||
WriteXmlEl(writer, "formazione", l.Formazione);
|
||||
WriteXmlEl(writer, "numero", l.Numero);
|
||||
WriteXmlEl(writer, "nome", l.Giocatore);
|
||||
WriteXmlEl(writer, "posizione", l.Posizione);
|
||||
WriteXmlEl(writer, "titolare", l.Titolare);
|
||||
writer.WriteEndElement();
|
||||
}
|
||||
writer.WriteEndElement();
|
||||
}
|
||||
|
||||
if (m.H2H?.Count > 0)
|
||||
{
|
||||
writer.WriteStartElement("h2h");
|
||||
foreach (var h in m.H2H)
|
||||
{
|
||||
writer.WriteStartElement("incontro");
|
||||
WriteXmlEl(writer, "data", h.Data);
|
||||
WriteXmlEl(writer, "lega", h.Lega);
|
||||
WriteXmlEl(writer, "casa", h.SquadraCasa);
|
||||
WriteXmlEl(writer, "ospite", h.SquadraOsp);
|
||||
WriteXmlEl(writer, "risultato", h.Risultato);
|
||||
WriteXmlEl(writer, "vincitore", h.Vincitore);
|
||||
writer.WriteEndElement();
|
||||
}
|
||||
writer.WriteEndElement();
|
||||
}
|
||||
|
||||
if (m.Statistics?.Count > 0)
|
||||
{
|
||||
writer.WriteStartElement("statistiche");
|
||||
foreach (var s in m.Statistics)
|
||||
{
|
||||
writer.WriteStartElement("statistica");
|
||||
WriteXmlEl(writer, "squadra", s.Squadra);
|
||||
WriteXmlEl(writer, "tipo", s.Tipo);
|
||||
WriteXmlEl(writer, "valore", s.Valore);
|
||||
writer.WriteEndElement();
|
||||
}
|
||||
writer.WriteEndElement();
|
||||
}
|
||||
|
||||
if (m.Injuries?.Count > 0)
|
||||
{
|
||||
writer.WriteStartElement("infortuni");
|
||||
foreach (var inj in m.Injuries)
|
||||
{
|
||||
writer.WriteStartElement("infortunio");
|
||||
WriteXmlEl(writer, "squadra", inj.Squadra);
|
||||
WriteXmlEl(writer, "giocatore", inj.Giocatore);
|
||||
WriteXmlEl(writer, "tipo", inj.Tipo);
|
||||
WriteXmlEl(writer, "gravita", inj.Gravità);
|
||||
WriteXmlEl(writer, "rientro", inj.DataRientro);
|
||||
writer.WriteEndElement();
|
||||
}
|
||||
writer.WriteEndElement();
|
||||
}
|
||||
|
||||
if (m.Odds?.Count > 0)
|
||||
{
|
||||
writer.WriteStartElement("quote");
|
||||
foreach (var o in m.Odds)
|
||||
{
|
||||
writer.WriteStartElement("quota");
|
||||
WriteXmlEl(writer, "bookmaker", o.Bookmaker);
|
||||
WriteXmlEl(writer, "mercato", o.Mercato);
|
||||
WriteXmlEl(writer, "esito", o.Esito);
|
||||
WriteXmlEl(writer, "valore", o.Quota);
|
||||
writer.WriteEndElement();
|
||||
}
|
||||
writer.WriteEndElement();
|
||||
}
|
||||
|
||||
writer.WriteEndElement(); // </partita>
|
||||
}
|
||||
|
||||
writer.WriteEndElement(); // </partite>
|
||||
writer.WriteEndDocument();
|
||||
setStatus?.Invoke($"✅ XML salvato: {path}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
setStatus?.Invoke($"❌ Errore export XML: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
// ── Helpers ──────────────────────────────────────────────────────────
|
||||
|
||||
private static void WriteXmlEl(XmlWriter w, string tag, string value)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(value)) return;
|
||||
w.WriteElementString(tag, value);
|
||||
}
|
||||
|
||||
private static void WriteXmlStanding(XmlWriter w, string tag,
|
||||
string pos, string punti, string vit, string par, string sco, string gf, string gs)
|
||||
{
|
||||
w.WriteStartElement(tag);
|
||||
WriteXmlEl(w, "posizione", pos);
|
||||
WriteXmlEl(w, "punti", punti);
|
||||
WriteXmlEl(w, "vittorie", vit);
|
||||
WriteXmlEl(w, "pareggi", par);
|
||||
WriteXmlEl(w, "sconfitte", sco);
|
||||
WriteXmlEl(w, "goalFatti", gf);
|
||||
WriteXmlEl(w, "goalSubiti",gs);
|
||||
w.WriteEndElement();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,376 @@
|
||||
<UserControl x:Class="HorseRacingPredictor.Football.FootballGridView"
|
||||
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:local="clr-namespace:HorseRacingPredictor.Football">
|
||||
|
||||
<UserControl.Resources>
|
||||
<local:BoolToVisibilityConverter x:Key="BoolVisConv"/>
|
||||
<local:CollectionToVisibilityConverter x:Key="ColVisConv"/>
|
||||
|
||||
<!-- ── TreeView global style ─────────────────────────────────────── -->
|
||||
<Style TargetType="TreeView">
|
||||
<Setter Property="Background" Value="{DynamicResource BrSurfaceContainerLow}"/>
|
||||
<Setter Property="BorderThickness" Value="0"/>
|
||||
<Setter Property="FontFamily" Value="Roboto, Segoe UI"/>
|
||||
<Setter Property="FontSize" Value="13"/>
|
||||
<Setter Property="ScrollViewer.HorizontalScrollBarVisibility" Value="Auto"/>
|
||||
</Style>
|
||||
|
||||
<!-- ── TreeViewItem strip all arrow decoration ───────────────────── -->
|
||||
<Style TargetType="TreeViewItem">
|
||||
<Setter Property="IsExpanded" Value="False"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource BrOnSurface}"/>
|
||||
<Setter Property="Background" Value="Transparent"/>
|
||||
<Setter Property="HorizontalContentAlignment" Value="Stretch"/>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="TreeViewItem">
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition/>
|
||||
</Grid.RowDefinitions>
|
||||
<Border x:Name="Bd" Grid.Row="0"
|
||||
Background="{TemplateBinding Background}"
|
||||
BorderThickness="0">
|
||||
<ContentPresenter x:Name="PART_Header"
|
||||
ContentSource="Header"
|
||||
HorizontalAlignment="Stretch"/>
|
||||
</Border>
|
||||
<ItemsPresenter x:Name="ItemsHost" Grid.Row="1"
|
||||
Visibility="Collapsed"
|
||||
Margin="0,0,0,0"/>
|
||||
</Grid>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsExpanded" Value="True">
|
||||
<Setter TargetName="ItemsHost" Property="Visibility" Value="Visible"/>
|
||||
</Trigger>
|
||||
<Trigger Property="IsSelected" Value="True">
|
||||
<Setter TargetName="Bd" Property="Background"
|
||||
Value="{DynamicResource BrSecondaryContainer}"/>
|
||||
</Trigger>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter TargetName="Bd" Property="Background"
|
||||
Value="{DynamicResource BrRowHover}"/>
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<!-- ── Shared sub-DataGrid style ─────────────────────────────────── -->
|
||||
<Style x:Key="SubDg" TargetType="DataGrid">
|
||||
<Setter Property="Background" Value="{DynamicResource BrSurfaceContainer}"/>
|
||||
<Setter Property="RowBackground" Value="{DynamicResource BrSurfaceContainer}"/>
|
||||
<Setter Property="AlternatingRowBackground" Value="{DynamicResource BrSurfaceContainer}"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource BrOnSurface}"/>
|
||||
<Setter Property="BorderThickness" Value="0"/>
|
||||
<Setter Property="GridLinesVisibility" Value="Horizontal"/>
|
||||
<Setter Property="HorizontalGridLinesBrush" Value="{DynamicResource BrOutlineVariant}"/>
|
||||
<Setter Property="HeadersVisibility" Value="Column"/>
|
||||
<Setter Property="RowHeaderWidth" Value="0"/>
|
||||
<Setter Property="AutoGenerateColumns" Value="False"/>
|
||||
<Setter Property="IsReadOnly" Value="True"/>
|
||||
<Setter Property="CanUserAddRows" Value="False"/>
|
||||
<Setter Property="CanUserDeleteRows" Value="False"/>
|
||||
<Setter Property="CanUserResizeRows" Value="False"/>
|
||||
<Setter Property="FontFamily" Value="Roboto, Segoe UI"/>
|
||||
<Setter Property="FontSize" Value="12"/>
|
||||
<Setter Property="RowStyle">
|
||||
<Setter.Value>
|
||||
<Style TargetType="DataGridRow">
|
||||
<Setter Property="Background" Value="{DynamicResource BrSurfaceContainer}"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource BrOnSurface}"/>
|
||||
<Style.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter Property="Background" Value="{DynamicResource BrRowHover}"/>
|
||||
</Trigger>
|
||||
<Trigger Property="IsSelected" Value="True">
|
||||
<Setter Property="Background" Value="{DynamicResource BrSecondaryContainer}"/>
|
||||
</Trigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
<Setter Property="CellStyle">
|
||||
<Setter.Value>
|
||||
<Style TargetType="DataGridCell">
|
||||
<Setter Property="Background" Value="Transparent"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource BrOnSurface}"/>
|
||||
<Setter Property="BorderThickness" Value="0"/>
|
||||
<Style.Triggers>
|
||||
<Trigger Property="IsSelected" Value="True">
|
||||
<Setter Property="Background" Value="Transparent"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource BrOnSurface}"/>
|
||||
</Trigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
<Setter Property="ColumnHeaderStyle">
|
||||
<Setter.Value>
|
||||
<Style TargetType="DataGridColumnHeader">
|
||||
<Setter Property="Background" Value="{DynamicResource BrSurfaceContainerHigh}"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource BrOnSurfaceVariant}"/>
|
||||
<Setter Property="FontWeight" Value="SemiBold"/>
|
||||
<Setter Property="Padding" Value="8,4"/>
|
||||
<Setter Property="BorderThickness" Value="0"/>
|
||||
</Style>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<!-- ── Section label style ───────────────────────────────────────── -->
|
||||
<Style x:Key="SectionLabel" TargetType="TextBlock">
|
||||
<Setter Property="FontSize" Value="11"/>
|
||||
<Setter Property="FontWeight" Value="SemiBold"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource BrOnSurfaceVariant}"/>
|
||||
<Setter Property="Margin" Value="16,8,0,2"/>
|
||||
</Style>
|
||||
|
||||
<!-- ════════════════════════════════════════════════════════════════
|
||||
CATEGORY CHILD NODE TEMPLATE – one expandable detail section
|
||||
════════════════════════════════════════════════════════════════ -->
|
||||
<DataTemplate x:Key="CategoryNodeTemplate" DataType="{x:Type local:DetailCategoryNode}">
|
||||
<Border Margin="40,0,4,2"
|
||||
Background="{DynamicResource BrSurfaceContainer}"
|
||||
CornerRadius="8" BorderBrush="{DynamicResource BrOutlineVariant}"
|
||||
BorderThickness="1">
|
||||
<StackPanel>
|
||||
<!-- Category header row with expand arrow -->
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<Button Grid.Column="0"
|
||||
Content="{Binding ExpandIcon}"
|
||||
FontFamily="Segoe UI Symbol" FontSize="13"
|
||||
Foreground="{DynamicResource BrPrimary}"
|
||||
Background="Transparent" BorderThickness="0"
|
||||
Cursor="Hand" Padding="10,6"
|
||||
VerticalAlignment="Center"
|
||||
Click="BtnCategoryExpand_Click"/>
|
||||
<TextBlock Grid.Column="1"
|
||||
Text="{Binding Summary}"
|
||||
FontSize="12" FontWeight="SemiBold"
|
||||
Foreground="{DynamicResource BrOnSurface}"
|
||||
VerticalAlignment="Center" Margin="0,0,12,0"/>
|
||||
</Grid>
|
||||
|
||||
<!-- Expanded content — only the populated DataGrid is visible -->
|
||||
<StackPanel Visibility="{Binding IsExpanded, Converter={StaticResource BoolVisConv}}">
|
||||
|
||||
<!-- Events -->
|
||||
<DataGrid Style="{StaticResource SubDg}"
|
||||
ItemsSource="{Binding Events}"
|
||||
Visibility="{Binding Events, Converter={StaticResource ColVisConv}}">
|
||||
<DataGrid.Columns>
|
||||
<DataGridTextColumn Header="Min" Binding="{Binding Minuto}" Width="50"/>
|
||||
<DataGridTextColumn Header="Tipo" Binding="{Binding Tipo}" Width="100"/>
|
||||
<DataGridTextColumn Header="Dettaglio" Binding="{Binding Dettaglio}" Width="140"/>
|
||||
<DataGridTextColumn Header="Squadra" Binding="{Binding Squadra}" Width="150"/>
|
||||
<DataGridTextColumn Header="Giocatore" Binding="{Binding Giocatore}" Width="180"/>
|
||||
<DataGridTextColumn Header="Assistito" Binding="{Binding Assistito}" Width="180"/>
|
||||
</DataGrid.Columns>
|
||||
</DataGrid>
|
||||
|
||||
<!-- Lineups -->
|
||||
<DataGrid Style="{StaticResource SubDg}"
|
||||
ItemsSource="{Binding Lineups}"
|
||||
Visibility="{Binding Lineups, Converter={StaticResource ColVisConv}}">
|
||||
<DataGrid.Columns>
|
||||
<DataGridTextColumn Header="Squadra" Binding="{Binding Squadra}" Width="150"/>
|
||||
<DataGridTextColumn Header="Modulo" Binding="{Binding Formazione}" Width="80"/>
|
||||
<DataGridTextColumn Header="N°" Binding="{Binding Numero}" Width="40"/>
|
||||
<DataGridTextColumn Header="Giocatore" Binding="{Binding Giocatore}" Width="180"/>
|
||||
<DataGridTextColumn Header="Posizione" Binding="{Binding Posizione}" Width="100"/>
|
||||
<DataGridTextColumn Header="Titolare" Binding="{Binding Titolare}" Width="70"/>
|
||||
</DataGrid.Columns>
|
||||
</DataGrid>
|
||||
|
||||
<!-- H2H -->
|
||||
<DataGrid Style="{StaticResource SubDg}"
|
||||
ItemsSource="{Binding H2H}"
|
||||
Visibility="{Binding H2H, Converter={StaticResource ColVisConv}}">
|
||||
<DataGrid.Columns>
|
||||
<DataGridTextColumn Header="Data" Binding="{Binding Data}" Width="90"/>
|
||||
<DataGridTextColumn Header="Lega" Binding="{Binding Lega}" Width="130"/>
|
||||
<DataGridTextColumn Header="Casa" Binding="{Binding SquadraCasa}" Width="150"/>
|
||||
<DataGridTextColumn Header="Ospite" Binding="{Binding SquadraOsp}" Width="150"/>
|
||||
<DataGridTextColumn Header="Risultato" Binding="{Binding Risultato}" Width="70"/>
|
||||
<DataGridTextColumn Header="Vincitore" Binding="{Binding Vincitore}" Width="110"/>
|
||||
</DataGrid.Columns>
|
||||
</DataGrid>
|
||||
|
||||
<!-- Statistics -->
|
||||
<DataGrid Style="{StaticResource SubDg}"
|
||||
ItemsSource="{Binding Statistics}"
|
||||
Visibility="{Binding Statistics, Converter={StaticResource ColVisConv}}">
|
||||
<DataGrid.Columns>
|
||||
<DataGridTextColumn Header="Squadra" Binding="{Binding Squadra}" Width="150"/>
|
||||
<DataGridTextColumn Header="Tipo" Binding="{Binding Tipo}" Width="220"/>
|
||||
<DataGridTextColumn Header="Valore" Binding="{Binding Valore}" Width="100"/>
|
||||
</DataGrid.Columns>
|
||||
</DataGrid>
|
||||
|
||||
<!-- Injuries -->
|
||||
<DataGrid Style="{StaticResource SubDg}"
|
||||
ItemsSource="{Binding Injuries}"
|
||||
Visibility="{Binding Injuries, Converter={StaticResource ColVisConv}}">
|
||||
<DataGrid.Columns>
|
||||
<DataGridTextColumn Header="Squadra" Binding="{Binding Squadra}" Width="150"/>
|
||||
<DataGridTextColumn Header="Giocatore" Binding="{Binding Giocatore}" Width="180"/>
|
||||
<DataGridTextColumn Header="Tipo" Binding="{Binding Tipo}" Width="130"/>
|
||||
<DataGridTextColumn Header="Gravità" Binding="{Binding Gravità}" Width="130"/>
|
||||
<DataGridTextColumn Header="Rientro" Binding="{Binding DataRientro}" Width="110"/>
|
||||
</DataGrid.Columns>
|
||||
</DataGrid>
|
||||
|
||||
<!-- Odds -->
|
||||
<DataGrid Style="{StaticResource SubDg}"
|
||||
ItemsSource="{Binding Odds}"
|
||||
Visibility="{Binding Odds, Converter={StaticResource ColVisConv}}">
|
||||
<DataGrid.Columns>
|
||||
<DataGridTextColumn Header="Bookmaker" Binding="{Binding Bookmaker}" Width="140"/>
|
||||
<DataGridTextColumn Header="Mercato" Binding="{Binding Mercato}" Width="180"/>
|
||||
<DataGridTextColumn Header="Esito" Binding="{Binding Esito}" Width="110"/>
|
||||
<DataGridTextColumn Header="Quota" Binding="{Binding Quota}" Width="80"/>
|
||||
</DataGrid.Columns>
|
||||
</DataGrid>
|
||||
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</DataTemplate>
|
||||
|
||||
<!-- ════════════════════════════════════════════════════════════════
|
||||
ROOT NODE TEMPLATE – one fixture / match
|
||||
════════════════════════════════════════════════════════════════ -->
|
||||
<HierarchicalDataTemplate x:Key="FixtureNodeTemplate"
|
||||
ItemsSource="{Binding Children}"
|
||||
ItemTemplate="{StaticResource CategoryNodeTemplate}">
|
||||
|
||||
<!-- ── Fixture header card ──────────────────────────────────── -->
|
||||
<Border Margin="0,4,0,0"
|
||||
Background="{DynamicResource BrSurfaceContainerLow}"
|
||||
CornerRadius="10" Padding="0,0,0,0"
|
||||
BorderBrush="{DynamicResource BrOutlineVariant}"
|
||||
BorderThickness="1">
|
||||
<Border.Effect>
|
||||
<DropShadowEffect BlurRadius="4" ShadowDepth="1" Opacity="0.08" Color="#000"/>
|
||||
</Border.Effect>
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<!-- Expand/collapse toggle -->
|
||||
<Button Grid.Column="0"
|
||||
Content="{Binding ExpandIcon}"
|
||||
FontFamily="Segoe UI Symbol" FontSize="13"
|
||||
Foreground="{DynamicResource BrPrimary}"
|
||||
Background="Transparent" BorderThickness="0"
|
||||
Cursor="Hand" Padding="10,0" VerticalAlignment="Center"
|
||||
Visibility="{Binding HasChildren, Converter={StaticResource BoolVisConv}}"
|
||||
Click="BtnExpand_Click"/>
|
||||
|
||||
<!-- Core match data -->
|
||||
<Grid Grid.Column="1" Margin="4,10,16,10">
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="58"/> <!-- Ora -->
|
||||
<ColumnDefinition Width="130"/> <!-- Lega -->
|
||||
<ColumnDefinition Width="*"/> <!-- Casa -->
|
||||
<ColumnDefinition Width="80"/> <!-- Risultato -->
|
||||
<ColumnDefinition Width="*"/> <!-- Ospite -->
|
||||
<ColumnDefinition Width="80"/> <!-- Stato -->
|
||||
<ColumnDefinition Width="90"/> <!-- Consiglio -->
|
||||
<ColumnDefinition Width="46"/> <!-- 1 -->
|
||||
<ColumnDefinition Width="46"/> <!-- X -->
|
||||
<ColumnDefinition Width="46"/> <!-- 2 -->
|
||||
<ColumnDefinition Width="60"/> <!-- Over2.5 -->
|
||||
<ColumnDefinition Width="60"/> <!-- G/G -->
|
||||
<ColumnDefinition Width="50"/> <!-- PosCasa -->
|
||||
<ColumnDefinition Width="50"/> <!-- PosOsp -->
|
||||
</Grid.ColumnDefinitions>
|
||||
|
||||
<TextBlock Grid.Column="0" Text="{Binding DataOra}" VerticalAlignment="Center" TextTrimming="CharacterEllipsis"/>
|
||||
<TextBlock Grid.Column="1" Text="{Binding Lega}" VerticalAlignment="Center" TextTrimming="CharacterEllipsis" Margin="4,0"/>
|
||||
<TextBlock Grid.Column="2" Text="{Binding SquadraCasa}" VerticalAlignment="Center" TextTrimming="CharacterEllipsis" FontWeight="Medium" HorizontalAlignment="Right" Margin="0,0,8,0"/>
|
||||
<TextBlock Grid.Column="3" Text="{Binding Risultato}" VerticalAlignment="Center" HorizontalAlignment="Center"
|
||||
FontWeight="Bold" Foreground="{DynamicResource BrPrimary}"/>
|
||||
<TextBlock Grid.Column="4" Text="{Binding SquadraOspite}" VerticalAlignment="Center" TextTrimming="CharacterEllipsis" Margin="8,0,0,0"/>
|
||||
<TextBlock Grid.Column="5" Text="{Binding Stato}" VerticalAlignment="Center" TextTrimming="CharacterEllipsis" FontSize="11"
|
||||
Foreground="{DynamicResource BrOnSurfaceVariant}" Margin="4,0"/>
|
||||
<TextBlock Grid.Column="6" Text="{Binding PredizioneAdvice}" VerticalAlignment="Center" TextTrimming="CharacterEllipsis" FontSize="11"
|
||||
Foreground="{DynamicResource BrSecondary}" Margin="4,0"/>
|
||||
<TextBlock Grid.Column="7" Text="{Binding PredPercCasa}" VerticalAlignment="Center" HorizontalAlignment="Center" FontSize="11"/>
|
||||
<TextBlock Grid.Column="8" Text="{Binding PredPercPari}" VerticalAlignment="Center" HorizontalAlignment="Center" FontSize="11"/>
|
||||
<TextBlock Grid.Column="9" Text="{Binding PredPercOspite}" VerticalAlignment="Center" HorizontalAlignment="Center" FontSize="11"/>
|
||||
<TextBlock Grid.Column="10" Text="{Binding Over25}" VerticalAlignment="Center" HorizontalAlignment="Center" FontSize="11"/>
|
||||
<TextBlock Grid.Column="11" Text="{Binding GoalGoal}" VerticalAlignment="Center" HorizontalAlignment="Center" FontSize="11"/>
|
||||
<TextBlock Grid.Column="12" Text="{Binding ClassificaCasa}" VerticalAlignment="Center" HorizontalAlignment="Center" FontSize="11"/>
|
||||
<TextBlock Grid.Column="13" Text="{Binding ClassificaOspite}" VerticalAlignment="Center" HorizontalAlignment="Center" FontSize="11"/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Border>
|
||||
</HierarchicalDataTemplate>
|
||||
|
||||
</UserControl.Resources>
|
||||
|
||||
<Grid>
|
||||
<Grid.RowDefinitions>
|
||||
<RowDefinition Height="Auto"/>
|
||||
<RowDefinition Height="*"/>
|
||||
</Grid.RowDefinitions>
|
||||
|
||||
<!-- ── Column header bar ────────────────────────────────────────── -->
|
||||
<Border Grid.Row="0"
|
||||
Background="{DynamicResource BrSurfaceContainerLow}"
|
||||
BorderBrush="{DynamicResource BrOutlineVariant}"
|
||||
BorderThickness="0,0,0,1"
|
||||
Padding="52,6,16,6">
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="58"/>
|
||||
<ColumnDefinition Width="130"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="80"/>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="80"/>
|
||||
<ColumnDefinition Width="90"/>
|
||||
<ColumnDefinition Width="46"/>
|
||||
<ColumnDefinition Width="46"/>
|
||||
<ColumnDefinition Width="46"/>
|
||||
<ColumnDefinition Width="60"/>
|
||||
<ColumnDefinition Width="60"/>
|
||||
<ColumnDefinition Width="50"/>
|
||||
<ColumnDefinition Width="50"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<TextBlock Grid.Column="0" Text="Ora" FontWeight="SemiBold" FontSize="12" Foreground="{DynamicResource BrOnSurfaceVariant}"/>
|
||||
<TextBlock Grid.Column="1" Text="Lega" FontWeight="SemiBold" FontSize="12" Foreground="{DynamicResource BrOnSurfaceVariant}" Margin="4,0"/>
|
||||
<TextBlock Grid.Column="2" Text="Casa" FontWeight="SemiBold" FontSize="12" Foreground="{DynamicResource BrOnSurfaceVariant}" HorizontalAlignment="Right" Margin="0,0,8,0"/>
|
||||
<TextBlock Grid.Column="3" Text="Risl." FontWeight="SemiBold" FontSize="12" Foreground="{DynamicResource BrOnSurfaceVariant}" HorizontalAlignment="Center"/>
|
||||
<TextBlock Grid.Column="4" Text="Ospite" FontWeight="SemiBold" FontSize="12" Foreground="{DynamicResource BrOnSurfaceVariant}" Margin="8,0,0,0"/>
|
||||
<TextBlock Grid.Column="5" Text="Stato" FontWeight="SemiBold" FontSize="12" Foreground="{DynamicResource BrOnSurfaceVariant}" Margin="4,0"/>
|
||||
<TextBlock Grid.Column="6" Text="Consiglio" FontWeight="SemiBold" FontSize="12" Foreground="{DynamicResource BrOnSurfaceVariant}" Margin="4,0"/>
|
||||
<TextBlock Grid.Column="7" Text="1" FontWeight="SemiBold" FontSize="12" Foreground="{DynamicResource BrOnSurfaceVariant}" HorizontalAlignment="Center"/>
|
||||
<TextBlock Grid.Column="8" Text="X" FontWeight="SemiBold" FontSize="12" Foreground="{DynamicResource BrOnSurfaceVariant}" HorizontalAlignment="Center"/>
|
||||
<TextBlock Grid.Column="9" Text="2" FontWeight="SemiBold" FontSize="12" Foreground="{DynamicResource BrOnSurfaceVariant}" HorizontalAlignment="Center"/>
|
||||
<TextBlock Grid.Column="10" Text="O2.5" FontWeight="SemiBold" FontSize="12" Foreground="{DynamicResource BrOnSurfaceVariant}" HorizontalAlignment="Center"/>
|
||||
<TextBlock Grid.Column="11" Text="G/G" FontWeight="SemiBold" FontSize="12" Foreground="{DynamicResource BrOnSurfaceVariant}" HorizontalAlignment="Center"/>
|
||||
<TextBlock Grid.Column="12" Text="#C" FontWeight="SemiBold" FontSize="12" Foreground="{DynamicResource BrOnSurfaceVariant}" HorizontalAlignment="Center"/>
|
||||
<TextBlock Grid.Column="13" Text="#O" FontWeight="SemiBold" FontSize="12" Foreground="{DynamicResource BrOnSurfaceVariant}" HorizontalAlignment="Center"/>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!-- ── Tree view ────────────────────────────────────────────────── -->
|
||||
<ScrollViewer Grid.Row="1" VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Auto">
|
||||
<TreeView x:Name="tvMatches"
|
||||
ItemTemplate="{StaticResource FixtureNodeTemplate}"
|
||||
Padding="8,4,8,8"/>
|
||||
</ScrollViewer>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
@@ -0,0 +1,181 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.ComponentModel;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
|
||||
namespace HorseRacingPredictor.Football
|
||||
{
|
||||
public partial class FootballGridView : UserControl
|
||||
{
|
||||
public FootballGridView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
// ── Public API ───────────────────────────────────────────────────────
|
||||
|
||||
public ObservableCollection<MatchTreeNode> Matches { get; }
|
||||
= new ObservableCollection<MatchTreeNode>();
|
||||
|
||||
public void Load(IEnumerable<FootballMatchViewModel> items)
|
||||
{
|
||||
Matches.Clear();
|
||||
if (items != null)
|
||||
foreach (var m in items)
|
||||
Matches.Add(new MatchTreeNode(m));
|
||||
|
||||
tvMatches.ItemsSource = Matches;
|
||||
}
|
||||
|
||||
public void Clear()
|
||||
{
|
||||
Matches.Clear();
|
||||
tvMatches.ItemsSource = null;
|
||||
}
|
||||
|
||||
public int MatchCount => Matches.Count;
|
||||
|
||||
// ── Expand toggle handler ────────────────────────────────────────────
|
||||
|
||||
private void BtnExpand_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (sender is not Button btn) return;
|
||||
|
||||
// Handle root match node expand
|
||||
if (btn.DataContext is MatchTreeNode node)
|
||||
{
|
||||
node.IsExpanded = !node.IsExpanded;
|
||||
if (tvMatches.ItemContainerGenerator.ContainerFromItem(node) is TreeViewItem tvi)
|
||||
tvi.IsExpanded = node.IsExpanded;
|
||||
}
|
||||
}
|
||||
|
||||
private void BtnCategoryExpand_Click(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (sender is not Button btn) return;
|
||||
if (btn.DataContext is not DetailCategoryNode cat) return;
|
||||
cat.IsExpanded = !cat.IsExpanded;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Root tree node: wraps a fixture ViewModel and exposes per-category expandable child nodes.
|
||||
/// </summary>
|
||||
public class MatchTreeNode : FootballMatchViewModel
|
||||
{
|
||||
public ObservableCollection<DetailCategoryNode> Children { get; } = new();
|
||||
|
||||
public bool HasChildren => Children.Count > 0;
|
||||
|
||||
public MatchTreeNode() { }
|
||||
|
||||
public MatchTreeNode(FootballMatchViewModel vm)
|
||||
{
|
||||
FixtureId = vm.FixtureId;
|
||||
LeagueId = vm.LeagueId;
|
||||
HomeTeamId = vm.HomeTeamId;
|
||||
AwayTeamId = vm.AwayTeamId;
|
||||
DataOra = vm.DataOra;
|
||||
Lega = vm.Lega;
|
||||
Nazione = vm.Nazione;
|
||||
Stagione = vm.Stagione;
|
||||
SquadraCasa = vm.SquadraCasa;
|
||||
SquadraOspite = vm.SquadraOspite;
|
||||
Risultato = vm.Risultato;
|
||||
Stato = vm.Stato;
|
||||
Arbitro = vm.Arbitro;
|
||||
Stadio = vm.Stadio;
|
||||
Città = vm.Città;
|
||||
|
||||
PredizioneFinaleBase= vm.PredizioneFinaleBase;
|
||||
PredizioneAdvice = vm.PredizioneAdvice;
|
||||
PredPercCasa = vm.PredPercCasa;
|
||||
PredPercPari = vm.PredPercPari;
|
||||
PredPercOspite = vm.PredPercOspite;
|
||||
PredGoalsHome = vm.PredGoalsHome;
|
||||
PredGoalsAway = vm.PredGoalsAway;
|
||||
FormCasa = vm.FormCasa;
|
||||
FormOspite = vm.FormOspite;
|
||||
AttaccoCasa = vm.AttaccoCasa;
|
||||
DifesaCasa = vm.DifesaCasa;
|
||||
AttaccoOspite = vm.AttaccoOspite;
|
||||
DifesaOspite = vm.DifesaOspite;
|
||||
PoissonHome = vm.PoissonHome;
|
||||
PoissonAway = vm.PoissonAway;
|
||||
PoissonDraw = vm.PoissonDraw;
|
||||
Under25 = vm.Under25;
|
||||
Over25 = vm.Over25;
|
||||
GoalGoal = vm.GoalGoal;
|
||||
NoGoal = vm.NoGoal;
|
||||
|
||||
ClassificaCasa = vm.ClassificaCasa;
|
||||
PuntiCasa = vm.PuntiCasa;
|
||||
VittorieCasa = vm.VittorieCasa;
|
||||
PareggiCasa = vm.PareggiCasa;
|
||||
SconfitteCasa = vm.SconfitteCasa;
|
||||
GoalFattiCasa = vm.GoalFattiCasa;
|
||||
GoalSubitiCasa = vm.GoalSubitiCasa;
|
||||
|
||||
ClassificaOspite = vm.ClassificaOspite;
|
||||
PuntiOspite = vm.PuntiOspite;
|
||||
VittorieOspite = vm.VittorieOspite;
|
||||
PareggiOspite = vm.PareggiOspite;
|
||||
SconfitteOspite = vm.SconfitteOspite;
|
||||
GoalFattiOspite = vm.GoalFattiOspite;
|
||||
GoalSubitiOspite = vm.GoalSubitiOspite;
|
||||
|
||||
// Build per-category child nodes
|
||||
if (vm.Events.Count > 0)
|
||||
Children.Add(new DetailCategoryNode("⚽ Eventi", vm.Events.Count) { Events = new(vm.Events) });
|
||||
if (vm.Lineups.Count > 0)
|
||||
Children.Add(new DetailCategoryNode("👥 Formazioni", vm.Lineups.Count) { Lineups = new(vm.Lineups) });
|
||||
if (vm.H2H.Count > 0)
|
||||
Children.Add(new DetailCategoryNode("🔄 Scontri H2H", vm.H2H.Count) { H2H = new(vm.H2H) });
|
||||
if (vm.Statistics.Count > 0)
|
||||
Children.Add(new DetailCategoryNode("📊 Statistiche", vm.Statistics.Count) { Statistics = new(vm.Statistics) });
|
||||
if (vm.Injuries.Count > 0)
|
||||
Children.Add(new DetailCategoryNode("🩹 Infortuni", vm.Injuries.Count) { Injuries = new(vm.Injuries) });
|
||||
if (vm.Odds.Count > 0)
|
||||
Children.Add(new DetailCategoryNode("💰 Quote", vm.Odds.Count) { Odds = new(vm.Odds) });
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A single expandable category child node shown beneath a match.
|
||||
/// Only one collection is populated (the one matching the category).
|
||||
/// </summary>
|
||||
public class DetailCategoryNode : INotifyPropertyChanged
|
||||
{
|
||||
public string Label { get; }
|
||||
public int Count { get; }
|
||||
public string Summary => $"{Label} ({Count})";
|
||||
|
||||
private bool _isExpanded;
|
||||
public bool IsExpanded
|
||||
{
|
||||
get => _isExpanded;
|
||||
set { _isExpanded = value; OnPropertyChanged(); OnPropertyChanged(nameof(ExpandIcon)); }
|
||||
}
|
||||
public string ExpandIcon => IsExpanded ? "▼" : "▶";
|
||||
|
||||
// Only one of these will be non-null per instance
|
||||
public ObservableCollection<MatchEventRow> Events { get; set; }
|
||||
public ObservableCollection<LineupRow> Lineups { get; set; }
|
||||
public ObservableCollection<H2HRow> H2H { get; set; }
|
||||
public ObservableCollection<StatisticRow> Statistics { get; set; }
|
||||
public ObservableCollection<InjuryRow> Injuries { get; set; }
|
||||
public ObservableCollection<OddsRow> Odds { get; set; }
|
||||
|
||||
public DetailCategoryNode(string label, int count)
|
||||
{
|
||||
Label = label;
|
||||
Count = count;
|
||||
}
|
||||
|
||||
public event PropertyChangedEventHandler PropertyChanged;
|
||||
private void OnPropertyChanged([CallerMemberName] string n = null)
|
||||
=> PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(n));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.ComponentModel;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace HorseRacingPredictor.Football
|
||||
{
|
||||
/// <summary>
|
||||
/// ViewModel per una singola partita. Contiene tutti i dati 1:1 della fixture
|
||||
/// e collezioni 1:N (eventi, formazioni, H2H, statistiche, infortuni, quote).
|
||||
/// Implementa INotifyPropertyChanged per binding WPF.
|
||||
/// </summary>
|
||||
public class FootballMatchViewModel : INotifyPropertyChanged
|
||||
{
|
||||
// ── Core fixture ─────────────────────────────────────────────────────
|
||||
public string FixtureId { get; set; }
|
||||
public string LeagueId { get; set; }
|
||||
public string HomeTeamId { get; set; }
|
||||
public string AwayTeamId { get; set; }
|
||||
public string DataOra { get; set; }
|
||||
public string Lega { get; set; }
|
||||
public string Nazione { get; set; }
|
||||
public string Stagione { get; set; }
|
||||
public string SquadraCasa { get; set; }
|
||||
public string SquadraOspite { get; set; }
|
||||
public string Risultato { get; set; } // "2 - 1"
|
||||
public string Stato { get; set; } // Scheduled / FT / Live...
|
||||
public string Arbitro { get; set; }
|
||||
public string Stadio { get; set; }
|
||||
public string Città { get; set; }
|
||||
|
||||
// ── Prediction ───────────────────────────────────────────────────────
|
||||
public string PredizioneFinaleBase { get; set; }
|
||||
public string PredizioneAdvice { get; set; }
|
||||
public string PredPercCasa { get; set; }
|
||||
public string PredPercPari { get; set; }
|
||||
public string PredPercOspite { get; set; }
|
||||
public string PredGoalsHome { get; set; }
|
||||
public string PredGoalsAway { get; set; }
|
||||
public string FormCasa { get; set; }
|
||||
public string FormOspite { get; set; }
|
||||
public string AttaccoCasa { get; set; }
|
||||
public string DifesaCasa { get; set; }
|
||||
public string AttaccoOspite { get; set; }
|
||||
public string DifesaOspite { get; set; }
|
||||
public string PoissonHome { get; set; }
|
||||
public string PoissonAway { get; set; }
|
||||
public string PoissonDraw { get; set; }
|
||||
public string Under25 { get; set; }
|
||||
public string Over25 { get; set; }
|
||||
public string GoalGoal { get; set; }
|
||||
public string NoGoal { get; set; }
|
||||
|
||||
// ── Standings casa ───────────────────────────────────────────────────
|
||||
public string ClassificaCasa { get; set; }
|
||||
public string PuntiCasa { get; set; }
|
||||
public string VittorieCasa { get; set; }
|
||||
public string PareggiCasa { get; set; }
|
||||
public string SconfitteCasa { get; set; }
|
||||
public string GoalFattiCasa { get; set; }
|
||||
public string GoalSubitiCasa { get; set; }
|
||||
|
||||
// ── Standings ospite ─────────────────────────────────────────────────
|
||||
public string ClassificaOspite { get; set; }
|
||||
public string PuntiOspite { get; set; }
|
||||
public string VittorieOspite { get; set; }
|
||||
public string PareggiOspite { get; set; }
|
||||
public string SconfitteOspite { get; set; }
|
||||
public string GoalFattiOspite { get; set; }
|
||||
public string GoalSubitiOspite { get; set; }
|
||||
|
||||
// ── Info web ─────────────────────────────────────────────────────────
|
||||
public string InfoWebPartita { get; set; }
|
||||
|
||||
// ── Expand/collapse ──────────────────────────────────────────────────
|
||||
private bool _isExpanded;
|
||||
public bool IsExpanded
|
||||
{
|
||||
get => _isExpanded;
|
||||
set { _isExpanded = value; OnPropertyChanged(); OnPropertyChanged(nameof(ExpandIcon)); }
|
||||
}
|
||||
public string ExpandIcon => IsExpanded ? "▼" : "▶";
|
||||
|
||||
/// <summary>True se almeno una collezione 1:N contiene dati.</summary>
|
||||
public bool HasDetails =>
|
||||
(Events?.Count > 0) ||
|
||||
(Lineups?.Count > 0) ||
|
||||
(H2H?.Count > 0) ||
|
||||
(Statistics?.Count > 0) ||
|
||||
(Injuries?.Count > 0) ||
|
||||
(Odds?.Count > 0);
|
||||
|
||||
// ── Collezioni 1:N ───────────────────────────────────────────────────
|
||||
public ObservableCollection<MatchEventRow> Events { get; set; } = new();
|
||||
public ObservableCollection<LineupRow> Lineups { get; set; } = new();
|
||||
public ObservableCollection<H2HRow> H2H { get; set; } = new();
|
||||
public ObservableCollection<StatisticRow> Statistics { get; set; } = new();
|
||||
public ObservableCollection<InjuryRow> Injuries { get; set; } = new();
|
||||
public ObservableCollection<OddsRow> Odds { get; set; } = new();
|
||||
|
||||
public event PropertyChangedEventHandler PropertyChanged;
|
||||
protected void OnPropertyChanged([CallerMemberName] string n = null)
|
||||
=> PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(n));
|
||||
}
|
||||
|
||||
// ── Sub-row types ────────────────────────────────────────────────────────
|
||||
|
||||
public class MatchEventRow
|
||||
{
|
||||
public string Minuto { get; set; }
|
||||
public string Tipo { get; set; }
|
||||
public string Dettaglio{ get; set; }
|
||||
public string Squadra { get; set; }
|
||||
public string Giocatore{ get; set; }
|
||||
public string Assistito{ get; set; }
|
||||
}
|
||||
|
||||
public class LineupRow
|
||||
{
|
||||
public string Squadra { get; set; }
|
||||
public string Formazione { get; set; }
|
||||
public string Numero { get; set; }
|
||||
public string Giocatore { get; set; }
|
||||
public string Posizione { get; set; }
|
||||
public string Titolare { get; set; }
|
||||
}
|
||||
|
||||
public class H2HRow
|
||||
{
|
||||
public string Data { get; set; }
|
||||
public string Lega { get; set; }
|
||||
public string SquadraCasa { get; set; }
|
||||
public string SquadraOsp { get; set; }
|
||||
public string Risultato { get; set; }
|
||||
public string Vincitore { get; set; }
|
||||
}
|
||||
|
||||
public class StatisticRow
|
||||
{
|
||||
public string Squadra { get; set; }
|
||||
public string Tipo { get; set; }
|
||||
public string Valore { get; set; }
|
||||
}
|
||||
|
||||
public class InjuryRow
|
||||
{
|
||||
public string Squadra { get; set; }
|
||||
public string Giocatore { get; set; }
|
||||
public string Tipo { get; set; }
|
||||
public string Gravità { get; set; }
|
||||
public string DataRientro { get; set; }
|
||||
}
|
||||
|
||||
public class OddsRow
|
||||
{
|
||||
public string Bookmaker { get; set; }
|
||||
public string Mercato { get; set; }
|
||||
public string Esito { get; set; }
|
||||
public string Quota { get; set; }
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,733 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Data;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace HorseRacingPredictor.Football
|
||||
{
|
||||
/// <summary>
|
||||
/// Scarica dati supplementari dall'API-Football e li esporta come CSV separati.
|
||||
/// Ogni tipo di dato produce un file CSV indipendente che l'IA potrà poi unire
|
||||
/// in fase di elaborazione usando le chiavi comuni (team_id, league_id, player_id).
|
||||
/// </summary>
|
||||
public class SupplementaryDataExporter
|
||||
{
|
||||
private readonly API.PlayerStatistics _playerStatsAPI = new();
|
||||
private readonly API.TeamStatistics _teamStatsAPI = new();
|
||||
private readonly API.TopScorers _topScorersAPI = new();
|
||||
private readonly API.TopAssists _topAssistsAPI = new();
|
||||
private readonly API.TopCards _topCardsAPI = new();
|
||||
private readonly API.Squads _squadsAPI = new();
|
||||
private readonly API.Coaches _coachesAPI = new();
|
||||
private readonly API.Transfers _transfersAPI = new();
|
||||
private readonly API.Status _statusAPI = new();
|
||||
private readonly Manager.Database _database = new();
|
||||
|
||||
/// <summary>
|
||||
/// Scarica tutti i dati supplementari selezionati e li salva come CSV nella cartella specificata.
|
||||
/// Restituisce la lista di file CSV generati.
|
||||
/// </summary>
|
||||
public List<string> DownloadAndExport(
|
||||
DateTime date,
|
||||
string exportFolder,
|
||||
FootballDownloadOptions options,
|
||||
HashSet<int> teamIds,
|
||||
HashSet<int> leagueIds,
|
||||
IProgress<int> progressCallback = null,
|
||||
IProgress<string> statusCallback = null)
|
||||
{
|
||||
var generatedFiles = new List<string>();
|
||||
int season = options.GetEffectiveSeason();
|
||||
string dateTag = date.ToString("yyyy-MM-dd");
|
||||
|
||||
// Calcola step totali per progress
|
||||
int totalSteps = CountSteps(options, teamIds.Count, leagueIds.Count);
|
||||
int currentStep = 0;
|
||||
|
||||
void Report(string msg)
|
||||
{
|
||||
currentStep++;
|
||||
int pct = totalSteps > 0 ? (int)((double)currentStep / totalSteps * 100) : 0;
|
||||
progressCallback?.Report(Math.Min(pct, 100));
|
||||
statusCallback?.Report(msg);
|
||||
}
|
||||
|
||||
// ?? Top Scorers ??
|
||||
if (options.DownloadTopScorers)
|
||||
{
|
||||
Report("Scaricamento capocannonieri...");
|
||||
var table = DownloadTopScorersTable(leagueIds, season, options.ApiDelayMs);
|
||||
if (table.Rows.Count > 0)
|
||||
{
|
||||
string path = SaveCsv(table, exportFolder, $"TopScorers_{dateTag}.csv");
|
||||
generatedFiles.Add(path);
|
||||
}
|
||||
}
|
||||
|
||||
// ?? Top Assists ??
|
||||
if (options.DownloadTopAssists)
|
||||
{
|
||||
Report("Scaricamento top assistman...");
|
||||
var table = DownloadTopAssistsTable(leagueIds, season, options.ApiDelayMs);
|
||||
if (table.Rows.Count > 0)
|
||||
{
|
||||
string path = SaveCsv(table, exportFolder, $"TopAssists_{dateTag}.csv");
|
||||
generatedFiles.Add(path);
|
||||
}
|
||||
}
|
||||
|
||||
// ?? Top Cards ??
|
||||
if (options.DownloadTopCards)
|
||||
{
|
||||
Report("Scaricamento top cartellini...");
|
||||
var table = DownloadTopCardsTable(leagueIds, season, options.ApiDelayMs);
|
||||
if (table.Rows.Count > 0)
|
||||
{
|
||||
string path = SaveCsv(table, exportFolder, $"TopCards_{dateTag}.csv");
|
||||
generatedFiles.Add(path);
|
||||
}
|
||||
}
|
||||
|
||||
// ?? Team Stats ??
|
||||
if (options.DownloadTeamStats)
|
||||
{
|
||||
var table = new DataTable();
|
||||
table.Columns.Add("TeamId", typeof(int));
|
||||
table.Columns.Add("LeagueId", typeof(int));
|
||||
table.Columns.Add("Season", typeof(int));
|
||||
table.Columns.Add("Forma", typeof(string));
|
||||
table.Columns.Add("Partite Totali", typeof(int));
|
||||
table.Columns.Add("Vittorie Casa", typeof(int));
|
||||
table.Columns.Add("Vittorie Trasf.", typeof(int));
|
||||
table.Columns.Add("Pareggi Casa", typeof(int));
|
||||
table.Columns.Add("Pareggi Trasf.", typeof(int));
|
||||
table.Columns.Add("Sconfitte Casa", typeof(int));
|
||||
table.Columns.Add("Sconfitte Trasf.", typeof(int));
|
||||
table.Columns.Add("Gol Fatti Casa", typeof(int));
|
||||
table.Columns.Add("Gol Fatti Trasf.", typeof(int));
|
||||
table.Columns.Add("Gol Subiti Casa", typeof(int));
|
||||
table.Columns.Add("Gol Subiti Trasf.", typeof(int));
|
||||
table.Columns.Add("Clean Sheet Tot.", typeof(int));
|
||||
table.Columns.Add("Penalty Segnati", typeof(int));
|
||||
table.Columns.Add("Penalty Falliti", typeof(int));
|
||||
|
||||
foreach (int teamId in teamIds)
|
||||
{
|
||||
foreach (int leagueId in leagueIds)
|
||||
{
|
||||
Report($"Statistiche team {teamId} in lega {leagueId}...");
|
||||
try
|
||||
{
|
||||
var resp = _teamStatsAPI.GetTeamStatistics(teamId, leagueId, season, dateTag);
|
||||
if (resp?.IsSuccessful == true && !string.IsNullOrEmpty(resp.Content))
|
||||
ParseTeamStatsRow(table, resp.Content, teamId, leagueId, season);
|
||||
}
|
||||
catch { /* continua */ }
|
||||
if (options.ApiDelayMs > 0) System.Threading.Thread.Sleep(options.ApiDelayMs);
|
||||
}
|
||||
}
|
||||
if (table.Rows.Count > 0)
|
||||
{
|
||||
string path = SaveCsv(table, exportFolder, $"TeamStats_{dateTag}.csv");
|
||||
generatedFiles.Add(path);
|
||||
}
|
||||
}
|
||||
|
||||
// ?? Player Stats ??
|
||||
if (options.DownloadPlayerStats)
|
||||
{
|
||||
Report("Scaricamento statistiche giocatori...");
|
||||
var table = DownloadPlayerStatsTable(teamIds, season, options.ApiDelayMs, statusCallback);
|
||||
if (table.Rows.Count > 0)
|
||||
{
|
||||
string path = SaveCsv(table, exportFolder, $"PlayerStats_{dateTag}.csv");
|
||||
generatedFiles.Add(path);
|
||||
}
|
||||
}
|
||||
|
||||
// ?? Squads ??
|
||||
if (options.DownloadSquads)
|
||||
{
|
||||
Report("Scaricamento rose squadre...");
|
||||
var table = DownloadSquadsTable(teamIds, options.ApiDelayMs, statusCallback);
|
||||
if (table.Rows.Count > 0)
|
||||
{
|
||||
string path = SaveCsv(table, exportFolder, $"Squads_{dateTag}.csv");
|
||||
generatedFiles.Add(path);
|
||||
}
|
||||
}
|
||||
|
||||
// ?? Coaches ??
|
||||
if (options.DownloadCoaches)
|
||||
{
|
||||
Report("Scaricamento allenatori...");
|
||||
var table = DownloadCoachesTable(teamIds, options.ApiDelayMs, statusCallback);
|
||||
if (table.Rows.Count > 0)
|
||||
{
|
||||
string path = SaveCsv(table, exportFolder, $"Coaches_{dateTag}.csv");
|
||||
generatedFiles.Add(path);
|
||||
}
|
||||
}
|
||||
|
||||
// ?? Transfers ??
|
||||
if (options.DownloadTransfers)
|
||||
{
|
||||
Report("Scaricamento trasferimenti...");
|
||||
var table = DownloadTransfersTable(teamIds, options.ApiDelayMs, statusCallback);
|
||||
if (table.Rows.Count > 0)
|
||||
{
|
||||
string path = SaveCsv(table, exportFolder, $"Transfers_{dateTag}.csv");
|
||||
generatedFiles.Add(path);
|
||||
}
|
||||
}
|
||||
|
||||
progressCallback?.Report(100);
|
||||
statusCallback?.Report($"Generati {generatedFiles.Count} CSV supplementari");
|
||||
return generatedFiles;
|
||||
}
|
||||
|
||||
#region Download & Parse Methods
|
||||
|
||||
private DataTable DownloadTopScorersTable(HashSet<int> leagueIds, int season, int delayMs)
|
||||
{
|
||||
var table = CreateTopPlayersTable("Gol");
|
||||
foreach (int leagueId in leagueIds)
|
||||
{
|
||||
try
|
||||
{
|
||||
var resp = _topScorersAPI.GetTopScorers(leagueId, season);
|
||||
if (resp?.IsSuccessful == true && !string.IsNullOrEmpty(resp.Content))
|
||||
ParseTopPlayersResponse(table, resp.Content, leagueId, season, "goals", "total");
|
||||
}
|
||||
catch { }
|
||||
if (delayMs > 0) System.Threading.Thread.Sleep(delayMs);
|
||||
}
|
||||
return table;
|
||||
}
|
||||
|
||||
private DataTable DownloadTopAssistsTable(HashSet<int> leagueIds, int season, int delayMs)
|
||||
{
|
||||
var table = CreateTopPlayersTable("Assist");
|
||||
foreach (int leagueId in leagueIds)
|
||||
{
|
||||
try
|
||||
{
|
||||
var resp = _topAssistsAPI.GetTopAssists(leagueId, season);
|
||||
if (resp?.IsSuccessful == true && !string.IsNullOrEmpty(resp.Content))
|
||||
ParseTopPlayersResponse(table, resp.Content, leagueId, season, "goals", "assists");
|
||||
}
|
||||
catch { }
|
||||
if (delayMs > 0) System.Threading.Thread.Sleep(delayMs);
|
||||
}
|
||||
return table;
|
||||
}
|
||||
|
||||
private DataTable DownloadTopCardsTable(HashSet<int> leagueIds, int season, int delayMs)
|
||||
{
|
||||
var table = new DataTable();
|
||||
table.Columns.Add("LeagueId", typeof(int));
|
||||
table.Columns.Add("Season", typeof(int));
|
||||
table.Columns.Add("Posizione", typeof(int));
|
||||
table.Columns.Add("PlayerId", typeof(int));
|
||||
table.Columns.Add("Giocatore", typeof(string));
|
||||
table.Columns.Add("TeamId", typeof(int));
|
||||
table.Columns.Add("Squadra", typeof(string));
|
||||
table.Columns.Add("Tipo", typeof(string));
|
||||
table.Columns.Add("Totale", typeof(int));
|
||||
|
||||
foreach (int leagueId in leagueIds)
|
||||
{
|
||||
try
|
||||
{
|
||||
var respY = _topCardsAPI.GetTopYellowCards(leagueId, season);
|
||||
if (respY?.IsSuccessful == true && !string.IsNullOrEmpty(respY.Content))
|
||||
ParseTopCardsResponse(table, respY.Content, leagueId, season, "Yellow");
|
||||
}
|
||||
catch { }
|
||||
if (delayMs > 0) System.Threading.Thread.Sleep(delayMs);
|
||||
|
||||
try
|
||||
{
|
||||
var respR = _topCardsAPI.GetTopRedCards(leagueId, season);
|
||||
if (respR?.IsSuccessful == true && !string.IsNullOrEmpty(respR.Content))
|
||||
ParseTopCardsResponse(table, respR.Content, leagueId, season, "Red");
|
||||
}
|
||||
catch { }
|
||||
if (delayMs > 0) System.Threading.Thread.Sleep(delayMs);
|
||||
}
|
||||
return table;
|
||||
}
|
||||
|
||||
private DataTable DownloadPlayerStatsTable(HashSet<int> teamIds, int season, int delayMs, IProgress<string> statusCallback)
|
||||
{
|
||||
var table = new DataTable();
|
||||
table.Columns.Add("PlayerId", typeof(int));
|
||||
table.Columns.Add("Giocatore", typeof(string));
|
||||
table.Columns.Add("Età", typeof(int));
|
||||
table.Columns.Add("Nazionalità", typeof(string));
|
||||
table.Columns.Add("TeamId", typeof(int));
|
||||
table.Columns.Add("Squadra", typeof(string));
|
||||
table.Columns.Add("LeagueId", typeof(int));
|
||||
table.Columns.Add("Campionato", typeof(string));
|
||||
table.Columns.Add("Presenze", typeof(int));
|
||||
table.Columns.Add("Minuti", typeof(int));
|
||||
table.Columns.Add("Rating", typeof(string));
|
||||
table.Columns.Add("Gol", typeof(int));
|
||||
table.Columns.Add("Assist", typeof(int));
|
||||
table.Columns.Add("Gialli", typeof(int));
|
||||
table.Columns.Add("Rossi", typeof(int));
|
||||
table.Columns.Add("Tiri Totali", typeof(int));
|
||||
table.Columns.Add("Tiri in Porta", typeof(int));
|
||||
table.Columns.Add("Passaggi Chiave", typeof(int));
|
||||
table.Columns.Add("Dribbling Riusciti", typeof(int));
|
||||
table.Columns.Add("Falli Commessi", typeof(int));
|
||||
table.Columns.Add("Falli Subiti", typeof(int));
|
||||
table.Columns.Add("Ruolo", typeof(string));
|
||||
|
||||
int count = 0;
|
||||
foreach (int teamId in teamIds)
|
||||
{
|
||||
count++;
|
||||
statusCallback?.Report($"Statistiche giocatori team {count}/{teamIds.Count}...");
|
||||
try
|
||||
{
|
||||
var resp = _playerStatsAPI.GetByTeamAndSeason(teamId, season);
|
||||
if (resp?.IsSuccessful == true && !string.IsNullOrEmpty(resp.Content))
|
||||
ParsePlayerStatsResponse(table, resp.Content);
|
||||
}
|
||||
catch { }
|
||||
if (delayMs > 0) System.Threading.Thread.Sleep(delayMs);
|
||||
}
|
||||
return table;
|
||||
}
|
||||
|
||||
private DataTable DownloadSquadsTable(HashSet<int> teamIds, int delayMs, IProgress<string> statusCallback)
|
||||
{
|
||||
var table = new DataTable();
|
||||
table.Columns.Add("TeamId", typeof(int));
|
||||
table.Columns.Add("Squadra", typeof(string));
|
||||
table.Columns.Add("PlayerId", typeof(int));
|
||||
table.Columns.Add("Giocatore", typeof(string));
|
||||
table.Columns.Add("Età", typeof(int));
|
||||
table.Columns.Add("Numero", typeof(int));
|
||||
table.Columns.Add("Ruolo", typeof(string));
|
||||
|
||||
int count = 0;
|
||||
foreach (int teamId in teamIds)
|
||||
{
|
||||
count++;
|
||||
statusCallback?.Report($"Rosa team {count}/{teamIds.Count}...");
|
||||
try
|
||||
{
|
||||
var resp = _squadsAPI.GetSquad(teamId);
|
||||
if (resp?.IsSuccessful == true && !string.IsNullOrEmpty(resp.Content))
|
||||
ParseSquadsResponse(table, resp.Content);
|
||||
}
|
||||
catch { }
|
||||
if (delayMs > 0) System.Threading.Thread.Sleep(delayMs);
|
||||
}
|
||||
return table;
|
||||
}
|
||||
|
||||
private DataTable DownloadCoachesTable(HashSet<int> teamIds, int delayMs, IProgress<string> statusCallback)
|
||||
{
|
||||
var table = new DataTable();
|
||||
table.Columns.Add("CoachId", typeof(int));
|
||||
table.Columns.Add("Nome", typeof(string));
|
||||
table.Columns.Add("Età", typeof(int));
|
||||
table.Columns.Add("Nazionalità", typeof(string));
|
||||
table.Columns.Add("TeamId", typeof(int));
|
||||
table.Columns.Add("Squadra", typeof(string));
|
||||
|
||||
int count = 0;
|
||||
foreach (int teamId in teamIds)
|
||||
{
|
||||
count++;
|
||||
statusCallback?.Report($"Allenatore team {count}/{teamIds.Count}...");
|
||||
try
|
||||
{
|
||||
var resp = _coachesAPI.GetByTeam(teamId);
|
||||
if (resp?.IsSuccessful == true && !string.IsNullOrEmpty(resp.Content))
|
||||
ParseCoachesResponse(table, resp.Content, teamId);
|
||||
}
|
||||
catch { }
|
||||
if (delayMs > 0) System.Threading.Thread.Sleep(delayMs);
|
||||
}
|
||||
return table;
|
||||
}
|
||||
|
||||
private DataTable DownloadTransfersTable(HashSet<int> teamIds, int delayMs, IProgress<string> statusCallback)
|
||||
{
|
||||
var table = new DataTable();
|
||||
table.Columns.Add("PlayerId", typeof(int));
|
||||
table.Columns.Add("Giocatore", typeof(string));
|
||||
table.Columns.Add("Data", typeof(string));
|
||||
table.Columns.Add("Tipo", typeof(string));
|
||||
table.Columns.Add("Da TeamId", typeof(int));
|
||||
table.Columns.Add("Da Squadra", typeof(string));
|
||||
table.Columns.Add("A TeamId", typeof(int));
|
||||
table.Columns.Add("A Squadra", typeof(string));
|
||||
|
||||
int count = 0;
|
||||
foreach (int teamId in teamIds)
|
||||
{
|
||||
count++;
|
||||
statusCallback?.Report($"Trasferimenti team {count}/{teamIds.Count}...");
|
||||
try
|
||||
{
|
||||
var resp = _transfersAPI.GetByTeam(teamId);
|
||||
if (resp?.IsSuccessful == true && !string.IsNullOrEmpty(resp.Content))
|
||||
ParseTransfersResponse(table, resp.Content);
|
||||
}
|
||||
catch { }
|
||||
if (delayMs > 0) System.Threading.Thread.Sleep(delayMs);
|
||||
}
|
||||
return table;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region JSON Parsing Helpers
|
||||
|
||||
private static DataTable CreateTopPlayersTable(string statName)
|
||||
{
|
||||
var table = new DataTable();
|
||||
table.Columns.Add("LeagueId", typeof(int));
|
||||
table.Columns.Add("Season", typeof(int));
|
||||
table.Columns.Add("Posizione", typeof(int));
|
||||
table.Columns.Add("PlayerId", typeof(int));
|
||||
table.Columns.Add("Giocatore", typeof(string));
|
||||
table.Columns.Add("TeamId", typeof(int));
|
||||
table.Columns.Add("Squadra", typeof(string));
|
||||
table.Columns.Add(statName, typeof(int));
|
||||
table.Columns.Add("Presenze", typeof(int));
|
||||
table.Columns.Add("Rating", typeof(string));
|
||||
return table;
|
||||
}
|
||||
|
||||
private static void ParseTopPlayersResponse(DataTable table, string json, int leagueId, int season, string statSection, string statField)
|
||||
{
|
||||
var doc = JsonDocument.Parse(json).RootElement;
|
||||
if (!doc.TryGetProperty("response", out var resp)) return;
|
||||
int pos = 0;
|
||||
foreach (var item in resp.EnumerateArray())
|
||||
{
|
||||
pos++;
|
||||
var row = table.NewRow();
|
||||
row["LeagueId"] = leagueId;
|
||||
row["Season"] = season;
|
||||
row["Posizione"] = pos;
|
||||
row["PlayerId"] = GetInt(item, "player", "id");
|
||||
row["Giocatore"] = GetStr(item, "player", "name");
|
||||
|
||||
// statistics[0]
|
||||
if (item.TryGetProperty("statistics", out var stats))
|
||||
{
|
||||
foreach (var s in stats.EnumerateArray())
|
||||
{
|
||||
row["TeamId"] = GetInt(s, "team", "id");
|
||||
row["Squadra"] = GetStr(s, "team", "name");
|
||||
row[table.Columns[7].ColumnName] = GetIntNested(s, statSection, statField);
|
||||
row["Presenze"] = GetIntNested(s, "games", "appearences");
|
||||
row["Rating"] = GetStrNested(s, "games", "rating");
|
||||
break;
|
||||
}
|
||||
}
|
||||
table.Rows.Add(row);
|
||||
}
|
||||
}
|
||||
|
||||
private static void ParseTopCardsResponse(DataTable table, string json, int leagueId, int season, string cardType)
|
||||
{
|
||||
var doc = JsonDocument.Parse(json).RootElement;
|
||||
if (!doc.TryGetProperty("response", out var resp)) return;
|
||||
int pos = 0;
|
||||
foreach (var item in resp.EnumerateArray())
|
||||
{
|
||||
pos++;
|
||||
var row = table.NewRow();
|
||||
row["LeagueId"] = leagueId;
|
||||
row["Season"] = season;
|
||||
row["Posizione"] = pos;
|
||||
row["PlayerId"] = GetInt(item, "player", "id");
|
||||
row["Giocatore"] = GetStr(item, "player", "name");
|
||||
row["Tipo"] = cardType;
|
||||
|
||||
if (item.TryGetProperty("statistics", out var stats))
|
||||
{
|
||||
foreach (var s in stats.EnumerateArray())
|
||||
{
|
||||
row["TeamId"] = GetInt(s, "team", "id");
|
||||
row["Squadra"] = GetStr(s, "team", "name");
|
||||
string section = cardType == "Yellow" ? "yellow" : "red";
|
||||
row["Totale"] = GetIntNested(s, "cards", section);
|
||||
break;
|
||||
}
|
||||
}
|
||||
table.Rows.Add(row);
|
||||
}
|
||||
}
|
||||
|
||||
private static void ParseTeamStatsRow(DataTable table, string json, int teamId, int leagueId, int season)
|
||||
{
|
||||
var doc = JsonDocument.Parse(json).RootElement;
|
||||
if (!doc.TryGetProperty("response", out var resp)) return;
|
||||
|
||||
var row = table.NewRow();
|
||||
row["TeamId"] = teamId;
|
||||
row["LeagueId"] = leagueId;
|
||||
row["Season"] = season;
|
||||
row["Forma"] = resp.TryGetProperty("form", out var f) ? f.GetString() ?? "" : "";
|
||||
row["Partite Totali"] = GetIntNested(resp, "fixtures", "played", "total");
|
||||
row["Vittorie Casa"] = GetIntNested(resp, "fixtures", "wins", "home");
|
||||
row["Vittorie Trasf."] = GetIntNested(resp, "fixtures", "wins", "away");
|
||||
row["Pareggi Casa"] = GetIntNested(resp, "fixtures", "draws", "home");
|
||||
row["Pareggi Trasf."] = GetIntNested(resp, "fixtures", "draws", "away");
|
||||
row["Sconfitte Casa"] = GetIntNested(resp, "fixtures", "loses", "home");
|
||||
row["Sconfitte Trasf."] = GetIntNested(resp, "fixtures", "loses", "away");
|
||||
row["Gol Fatti Casa"] = GetIntNested(resp, "goals", "for", "total", "home");
|
||||
row["Gol Fatti Trasf."] = GetIntNested(resp, "goals", "for", "total", "away");
|
||||
row["Gol Subiti Casa"] = GetIntNested(resp, "goals", "against", "total", "home");
|
||||
row["Gol Subiti Trasf."] = GetIntNested(resp, "goals", "against", "total", "away");
|
||||
row["Clean Sheet Tot."] = GetIntNested(resp, "clean_sheet", "total");
|
||||
row["Penalty Segnati"] = GetIntNested(resp, "penalty", "scored", "total");
|
||||
row["Penalty Falliti"] = GetIntNested(resp, "penalty", "missed", "total");
|
||||
table.Rows.Add(row);
|
||||
}
|
||||
|
||||
private static void ParsePlayerStatsResponse(DataTable table, string json)
|
||||
{
|
||||
var doc = JsonDocument.Parse(json).RootElement;
|
||||
if (!doc.TryGetProperty("response", out var resp)) return;
|
||||
|
||||
foreach (var item in resp.EnumerateArray())
|
||||
{
|
||||
int playerId = GetInt(item, "player", "id");
|
||||
string name = GetStr(item, "player", "name");
|
||||
int age = GetInt(item, "player", "age");
|
||||
string nationality = GetStr(item, "player", "nationality");
|
||||
|
||||
if (!item.TryGetProperty("statistics", out var stats)) continue;
|
||||
foreach (var s in stats.EnumerateArray())
|
||||
{
|
||||
var row = table.NewRow();
|
||||
row["PlayerId"] = playerId;
|
||||
row["Giocatore"] = name;
|
||||
row["Età"] = age;
|
||||
row["Nazionalità"] = nationality;
|
||||
row["TeamId"] = GetInt(s, "team", "id");
|
||||
row["Squadra"] = GetStr(s, "team", "name");
|
||||
row["LeagueId"] = GetInt(s, "league", "id");
|
||||
row["Campionato"] = GetStr(s, "league", "name");
|
||||
row["Presenze"] = GetIntNested(s, "games", "appearences");
|
||||
row["Minuti"] = GetIntNested(s, "games", "minutes");
|
||||
row["Rating"] = GetStrNested(s, "games", "rating");
|
||||
row["Gol"] = GetIntNested(s, "goals", "total");
|
||||
row["Assist"] = GetIntNested(s, "goals", "assists");
|
||||
row["Gialli"] = GetIntNested(s, "cards", "yellow");
|
||||
row["Rossi"] = GetIntNested(s, "cards", "red");
|
||||
row["Tiri Totali"] = GetIntNested(s, "shots", "total");
|
||||
row["Tiri in Porta"] = GetIntNested(s, "shots", "on");
|
||||
row["Passaggi Chiave"] = GetIntNested(s, "passes", "key");
|
||||
row["Dribbling Riusciti"] = GetIntNested(s, "dribbles", "success");
|
||||
row["Falli Commessi"] = GetIntNested(s, "fouls", "committed");
|
||||
row["Falli Subiti"] = GetIntNested(s, "fouls", "drawn");
|
||||
row["Ruolo"] = GetStrNested(s, "games", "position");
|
||||
table.Rows.Add(row);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void ParseSquadsResponse(DataTable table, string json)
|
||||
{
|
||||
var doc = JsonDocument.Parse(json).RootElement;
|
||||
if (!doc.TryGetProperty("response", out var resp)) return;
|
||||
|
||||
foreach (var teamEntry in resp.EnumerateArray())
|
||||
{
|
||||
int teamId = GetInt(teamEntry, "team", "id");
|
||||
string teamName = GetStr(teamEntry, "team", "name");
|
||||
|
||||
if (!teamEntry.TryGetProperty("players", out var players)) continue;
|
||||
foreach (var p in players.EnumerateArray())
|
||||
{
|
||||
var row = table.NewRow();
|
||||
row["TeamId"] = teamId;
|
||||
row["Squadra"] = teamName;
|
||||
row["PlayerId"] = p.TryGetProperty("id", out var id) && id.ValueKind == JsonValueKind.Number ? id.GetInt32() : 0;
|
||||
row["Giocatore"] = p.TryGetProperty("name", out var n) ? n.GetString() ?? "" : "";
|
||||
row["Età"] = p.TryGetProperty("age", out var a) && a.ValueKind == JsonValueKind.Number ? a.GetInt32() : 0;
|
||||
row["Numero"] = p.TryGetProperty("number", out var num) && num.ValueKind == JsonValueKind.Number ? num.GetInt32() : 0;
|
||||
row["Ruolo"] = p.TryGetProperty("position", out var pos) ? pos.GetString() ?? "" : "";
|
||||
table.Rows.Add(row);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void ParseCoachesResponse(DataTable table, string json, int teamId)
|
||||
{
|
||||
var doc = JsonDocument.Parse(json).RootElement;
|
||||
if (!doc.TryGetProperty("response", out var resp)) return;
|
||||
|
||||
foreach (var c in resp.EnumerateArray())
|
||||
{
|
||||
var row = table.NewRow();
|
||||
row["CoachId"] = c.TryGetProperty("id", out var id) && id.ValueKind == JsonValueKind.Number ? id.GetInt32() : 0;
|
||||
row["Nome"] = c.TryGetProperty("name", out var n) ? n.GetString() ?? "" : "";
|
||||
row["Età"] = c.TryGetProperty("age", out var a) && a.ValueKind == JsonValueKind.Number ? a.GetInt32() : 0;
|
||||
row["Nazionalità"] = c.TryGetProperty("nationality", out var nat) ? nat.GetString() ?? "" : "";
|
||||
row["TeamId"] = teamId;
|
||||
row["Squadra"] = c.TryGetProperty("team", out var t) && t.TryGetProperty("name", out var tn) ? tn.GetString() ?? "" : "";
|
||||
table.Rows.Add(row);
|
||||
}
|
||||
}
|
||||
|
||||
private static void ParseTransfersResponse(DataTable table, string json)
|
||||
{
|
||||
var doc = JsonDocument.Parse(json).RootElement;
|
||||
if (!doc.TryGetProperty("response", out var resp)) return;
|
||||
|
||||
foreach (var entry in resp.EnumerateArray())
|
||||
{
|
||||
int playerId = GetInt(entry, "player", "id");
|
||||
string playerName = GetStr(entry, "player", "name");
|
||||
|
||||
if (!entry.TryGetProperty("transfers", out var transfers)) continue;
|
||||
foreach (var t in transfers.EnumerateArray())
|
||||
{
|
||||
var row = table.NewRow();
|
||||
row["PlayerId"] = playerId;
|
||||
row["Giocatore"] = playerName;
|
||||
row["Data"] = t.TryGetProperty("date", out var d) ? d.GetString() ?? "" : "";
|
||||
row["Tipo"] = t.TryGetProperty("type", out var tp) ? tp.GetString() ?? "" : "";
|
||||
row["Da TeamId"] = GetInt(t, "teams", "out", "id");
|
||||
row["Da Squadra"] = GetStr(t, "teams", "out", "name");
|
||||
row["A TeamId"] = GetInt(t, "teams", "in", "id");
|
||||
row["A Squadra"] = GetStr(t, "teams", "in", "name");
|
||||
table.Rows.Add(row);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region JSON Utility Helpers
|
||||
|
||||
private static int GetInt(JsonElement el, string prop1, string prop2)
|
||||
{
|
||||
if (el.TryGetProperty(prop1, out var p1) && p1.TryGetProperty(prop2, out var p2) && p2.ValueKind == JsonValueKind.Number)
|
||||
return p2.GetInt32();
|
||||
return 0;
|
||||
}
|
||||
|
||||
private static int GetInt(JsonElement el, string prop1, string prop2, string prop3)
|
||||
{
|
||||
if (el.TryGetProperty(prop1, out var p1) && p1.TryGetProperty(prop2, out var p2) && p2.TryGetProperty(prop3, out var p3) && p3.ValueKind == JsonValueKind.Number)
|
||||
return p3.GetInt32();
|
||||
return 0;
|
||||
}
|
||||
|
||||
private static string GetStr(JsonElement el, string prop1, string prop2)
|
||||
{
|
||||
if (el.TryGetProperty(prop1, out var p1) && p1.TryGetProperty(prop2, out var p2))
|
||||
return p2.GetString() ?? "";
|
||||
return "";
|
||||
}
|
||||
|
||||
private static string GetStr(JsonElement el, string prop1, string prop2, string prop3)
|
||||
{
|
||||
if (el.TryGetProperty(prop1, out var p1) && p1.TryGetProperty(prop2, out var p2) && p2.TryGetProperty(prop3, out var p3))
|
||||
return p3.GetString() ?? "";
|
||||
return "";
|
||||
}
|
||||
|
||||
private static int GetIntNested(JsonElement el, string p1, string p2)
|
||||
{
|
||||
if (el.TryGetProperty(p1, out var v1) && v1.TryGetProperty(p2, out var v2) && v2.ValueKind == JsonValueKind.Number)
|
||||
return v2.GetInt32();
|
||||
return 0;
|
||||
}
|
||||
|
||||
private static int GetIntNested(JsonElement el, string p1, string p2, string p3)
|
||||
{
|
||||
if (el.TryGetProperty(p1, out var v1) && v1.TryGetProperty(p2, out var v2) && v2.TryGetProperty(p3, out var v3) && v3.ValueKind == JsonValueKind.Number)
|
||||
return v3.GetInt32();
|
||||
return 0;
|
||||
}
|
||||
|
||||
private static int GetIntNested(JsonElement el, string p1, string p2, string p3, string p4)
|
||||
{
|
||||
if (el.TryGetProperty(p1, out var v1) && v1.TryGetProperty(p2, out var v2) && v2.TryGetProperty(p3, out var v3) && v3.TryGetProperty(p4, out var v4) && v4.ValueKind == JsonValueKind.Number)
|
||||
return v4.GetInt32();
|
||||
return 0;
|
||||
}
|
||||
|
||||
private static string GetStrNested(JsonElement el, string p1, string p2)
|
||||
{
|
||||
if (el.TryGetProperty(p1, out var v1) && v1.TryGetProperty(p2, out var v2))
|
||||
return v2.GetString() ?? "";
|
||||
return "";
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region CSV & Utility
|
||||
|
||||
private static string SaveCsv(DataTable table, string folder, string fileName)
|
||||
{
|
||||
if (!Directory.Exists(folder))
|
||||
Directory.CreateDirectory(folder);
|
||||
|
||||
string path = Path.Combine(folder, fileName);
|
||||
var sb = new StringBuilder();
|
||||
|
||||
// Header
|
||||
for (int i = 0; i < table.Columns.Count; i++)
|
||||
{
|
||||
if (i > 0) sb.Append(',');
|
||||
sb.Append(EscapeCsvField(table.Columns[i].ColumnName));
|
||||
}
|
||||
sb.AppendLine();
|
||||
|
||||
// Rows
|
||||
foreach (DataRow row in table.Rows)
|
||||
{
|
||||
for (int i = 0; i < table.Columns.Count; i++)
|
||||
{
|
||||
if (i > 0) sb.Append(',');
|
||||
sb.Append(EscapeCsvField(row[i]?.ToString() ?? ""));
|
||||
}
|
||||
sb.AppendLine();
|
||||
}
|
||||
|
||||
File.WriteAllText(path, sb.ToString(), Encoding.UTF8);
|
||||
return path;
|
||||
}
|
||||
|
||||
private static string EscapeCsvField(string field)
|
||||
{
|
||||
if (field.Contains(',') || field.Contains('"') || field.Contains('\n'))
|
||||
return "\"" + field.Replace("\"", "\"\"") + "\"";
|
||||
return field;
|
||||
}
|
||||
|
||||
private static int CountSteps(FootballDownloadOptions options, int teamCount, int leagueCount)
|
||||
{
|
||||
int steps = 0;
|
||||
if (options.DownloadTopScorers) steps += leagueCount;
|
||||
if (options.DownloadTopAssists) steps += leagueCount;
|
||||
if (options.DownloadTopCards) steps += leagueCount * 2;
|
||||
if (options.DownloadTeamStats) steps += teamCount * leagueCount;
|
||||
if (options.DownloadPlayerStats) steps += teamCount;
|
||||
if (options.DownloadSquads) steps += teamCount;
|
||||
if (options.DownloadCoaches) steps += teamCount;
|
||||
if (options.DownloadTransfers) steps += teamCount;
|
||||
return Math.Max(steps, 1);
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -13,6 +13,9 @@ namespace HorseRacingPredictor
|
||||
{
|
||||
private static IConfiguration _configuration;
|
||||
|
||||
// Runtime override for the Football API key (set from UI / UserSettings)
|
||||
private static string _footballApiKeyOverride;
|
||||
|
||||
public static IConfiguration Configuration => _configuration ??= BuildConfiguration();
|
||||
|
||||
private static IConfiguration BuildConfiguration()
|
||||
@@ -38,12 +41,23 @@ namespace HorseRacingPredictor
|
||||
|
||||
// ?? API settings ????????????????????????????????????????
|
||||
public static string FootballApiKey =>
|
||||
Configuration["Api:FootballApiKey"] ?? string.Empty;
|
||||
!string.IsNullOrEmpty(_footballApiKeyOverride)
|
||||
? _footballApiKeyOverride
|
||||
: Configuration["Api:FootballApiKey"] ?? string.Empty;
|
||||
|
||||
public static string FootballApiKeyHeader =>
|
||||
Configuration["Api:FootballApiKeyHeader"] ?? "x-rapidapi-key";
|
||||
|
||||
public static string FootballApiHost =>
|
||||
Configuration["Api:FootballApiHost"] ?? "v3.football.api-sports.io";
|
||||
|
||||
/// <summary>
|
||||
/// Imposta la API key di Football da codice (es. dalla UI).
|
||||
/// Ha precedenza sul valore in appsettings.json.
|
||||
/// </summary>
|
||||
public static void SetFootballApiKey(string apiKey)
|
||||
{
|
||||
_footballApiKeyOverride = apiKey?.Trim();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+799
@@ -0,0 +1,799 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Data;
|
||||
using System.Linq;
|
||||
using System.Net;
|
||||
using System.Net.Http;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using AngleSharp;
|
||||
using AngleSharp.Dom;
|
||||
using AngleSharp.Html.Parser;
|
||||
|
||||
namespace HorseRacingPredictor.HorseRacing.Scraping
|
||||
{
|
||||
/// <summary>
|
||||
/// Scraper per Punters.com.au – estrae dati delle corse dei cavalli
|
||||
/// direttamente dal sito web (SSR HTML) senza scaricare file CSV.
|
||||
///
|
||||
/// Flusso:
|
||||
/// 1. Discovery: accede a /form-guide/ e scopre le corse del giorno
|
||||
/// per le nazioni richieste (es. GB, IE).
|
||||
/// 2. Extraction: per ogni corsa, accede alla pagina dettaglio ed estrae
|
||||
/// i dati dei corridori dalla tabella HTML renderizzata lato server.
|
||||
/// 3. Il risultato è un DataTable pronto per DataGrid ed esportazione.
|
||||
/// </summary>
|
||||
internal sealed class PuntersScraper
|
||||
{
|
||||
private const string BaseUrl = "https://www.punters.com.au";
|
||||
private const string FormGuideUrl = BaseUrl + "/form-guide/";
|
||||
|
||||
// Delay tra richieste per sembrare un utente reale (ms)
|
||||
private const int MinDelayMs = 1500;
|
||||
private const int MaxDelayMs = 3500;
|
||||
|
||||
private static readonly Random Rng = new();
|
||||
|
||||
private readonly HttpClient _http;
|
||||
private readonly HtmlParser _parser = new();
|
||||
|
||||
/// <summary>Codici paese da scaricare (es. "GB", "IE").</summary>
|
||||
public List<string> Countries { get; set; } = new() { "GB", "IE" };
|
||||
|
||||
public PuntersScraper()
|
||||
{
|
||||
var handler = new HttpClientHandler
|
||||
{
|
||||
AutomaticDecompression = DecompressionMethods.GZip
|
||||
| DecompressionMethods.Deflate
|
||||
| DecompressionMethods.Brotli,
|
||||
UseCookies = true,
|
||||
CookieContainer = new CookieContainer()
|
||||
};
|
||||
|
||||
_http = new HttpClient(handler)
|
||||
{
|
||||
Timeout = TimeSpan.FromSeconds(60)
|
||||
};
|
||||
|
||||
// Headers che mimano un browser reale (dal file HAR)
|
||||
_http.DefaultRequestHeaders.Add("User-Agent",
|
||||
"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 " +
|
||||
"(KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36");
|
||||
_http.DefaultRequestHeaders.Add("Accept",
|
||||
"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8");
|
||||
_http.DefaultRequestHeaders.Add("Accept-Language", "en-US,en;q=0.9");
|
||||
_http.DefaultRequestHeaders.Add("Accept-Encoding", "gzip, deflate, br");
|
||||
_http.DefaultRequestHeaders.Add("sec-ch-ua",
|
||||
"\"Chromium\";v=\"146\", \"Not-A.Brand\";v=\"24\", \"Google Chrome\";v=\"146\"");
|
||||
_http.DefaultRequestHeaders.Add("sec-ch-ua-mobile", "?0");
|
||||
_http.DefaultRequestHeaders.Add("sec-ch-ua-platform", "\"Windows\"");
|
||||
_http.DefaultRequestHeaders.Add("sec-fetch-dest", "document");
|
||||
_http.DefaultRequestHeaders.Add("sec-fetch-mode", "navigate");
|
||||
_http.DefaultRequestHeaders.Add("sec-fetch-site", "none");
|
||||
_http.DefaultRequestHeaders.Add("sec-fetch-user", "?1");
|
||||
_http.DefaultRequestHeaders.Add("upgrade-insecure-requests", "1");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Scarica tutti i dati delle corse per una data specifica.
|
||||
/// </summary>
|
||||
public async Task<DataTable> ScrapeAsync(DateTime date,
|
||||
IProgress<int> progress = null,
|
||||
IProgress<string> status = null,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
var dt = CreateTable();
|
||||
|
||||
try
|
||||
{
|
||||
// === FASE 1: Discovery – scarica la pagina form-guide ===
|
||||
status?.Report("Punters: Accesso al palinsesto...");
|
||||
progress?.Report(2);
|
||||
|
||||
var meetings = await DiscoverMeetingsAsync(date, ct);
|
||||
|
||||
if (meetings.Count == 0)
|
||||
{
|
||||
status?.Report("Punters: Nessun meeting trovato per le nazioni selezionate");
|
||||
progress?.Report(100);
|
||||
return dt;
|
||||
}
|
||||
|
||||
// Conta totale corse per progresso
|
||||
int totalRaces = meetings.Sum(m => m.Races.Count);
|
||||
status?.Report($"Punters: {meetings.Count} meeting, {totalRaces} corse trovate");
|
||||
progress?.Report(10);
|
||||
|
||||
// === FASE 2: Extraction – scarica ogni pagina corsa ===
|
||||
int completed = 0;
|
||||
int errors = 0;
|
||||
|
||||
foreach (var meeting in meetings)
|
||||
{
|
||||
foreach (var race in meeting.Races)
|
||||
{
|
||||
ct.ThrowIfCancellationRequested();
|
||||
|
||||
status?.Report($"Punters: {meeting.Track} R{race.Number}/{meeting.Races.Count} " +
|
||||
$"({completed + 1}/{totalRaces})");
|
||||
|
||||
try
|
||||
{
|
||||
await RandomDelayAsync(ct);
|
||||
await FetchRaceRunners(dt, race.Url, meeting, race, ct);
|
||||
}
|
||||
catch (OperationCanceledException) { throw; }
|
||||
catch (Exception ex)
|
||||
{
|
||||
errors++;
|
||||
System.Diagnostics.Debug.WriteLine(
|
||||
$"Errore Punters {meeting.Track} R{race.Number}: {ex.Message}");
|
||||
}
|
||||
|
||||
completed++;
|
||||
int pct = 10 + (int)((double)completed / Math.Max(totalRaces, 1) * 88);
|
||||
progress?.Report(Math.Min(pct, 98));
|
||||
}
|
||||
}
|
||||
|
||||
progress?.Report(100);
|
||||
string errMsg = errors > 0 ? $" ({errors} errori)" : "";
|
||||
string countriesStr = string.Join("+", Countries.Select(c => c.ToUpper()));
|
||||
status?.Report($"Punters [{countriesStr}]: {dt.Rows.Count} corridori in " +
|
||||
$"{meetings.Count} meeting{errMsg}");
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
status?.Report("Scraping annullato");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
status?.Report($"Errore Punters: {ex.Message}");
|
||||
}
|
||||
|
||||
return dt;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Scarica i dati delle corse restituendo un DataTable separato per ogni meeting.
|
||||
/// Il dizionario associa il nome del meeting (Track_Country) al relativo DataTable.
|
||||
/// </summary>
|
||||
public async Task<Dictionary<string, DataTable>> ScrapeByMeetingAsync(DateTime date,
|
||||
IProgress<int> progress = null,
|
||||
IProgress<string> status = null,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
var result = new Dictionary<string, DataTable>();
|
||||
|
||||
try
|
||||
{
|
||||
status?.Report("Punters: Accesso al palinsesto...");
|
||||
progress?.Report(2);
|
||||
|
||||
var meetings = await DiscoverMeetingsAsync(date, ct);
|
||||
|
||||
if (meetings.Count == 0)
|
||||
{
|
||||
status?.Report("Punters: Nessun meeting trovato per le nazioni selezionate");
|
||||
progress?.Report(100);
|
||||
return result;
|
||||
}
|
||||
|
||||
int totalRaces = meetings.Sum(m => m.Races.Count);
|
||||
status?.Report($"Punters: {meetings.Count} meeting, {totalRaces} corse trovate");
|
||||
progress?.Report(10);
|
||||
|
||||
int completed = 0;
|
||||
|
||||
foreach (var meeting in meetings)
|
||||
{
|
||||
var dt = CreateTable();
|
||||
string key = $"{meeting.Track}_{meeting.Country}";
|
||||
|
||||
foreach (var race in meeting.Races)
|
||||
{
|
||||
ct.ThrowIfCancellationRequested();
|
||||
|
||||
status?.Report($"Punters: {meeting.Track} R{race.Number}/{meeting.Races.Count} " +
|
||||
$"({completed + 1}/{totalRaces})");
|
||||
|
||||
try
|
||||
{
|
||||
await RandomDelayAsync(ct);
|
||||
await FetchRaceRunners(dt, race.Url, meeting, race, ct);
|
||||
}
|
||||
catch (OperationCanceledException) { throw; }
|
||||
catch (Exception ex)
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine(
|
||||
$"Errore Punters {meeting.Track} R{race.Number}: {ex.Message}");
|
||||
}
|
||||
|
||||
completed++;
|
||||
int pct = 10 + (int)((double)completed / Math.Max(totalRaces, 1) * 88);
|
||||
progress?.Report(Math.Min(pct, 98));
|
||||
}
|
||||
|
||||
if (dt.Rows.Count > 0)
|
||||
result[key] = dt;
|
||||
}
|
||||
|
||||
progress?.Report(100);
|
||||
status?.Report($"Punters: {result.Count} meeting scaricati separatamente");
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
status?.Report("Scraping annullato");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
status?.Report($"Errore Punters: {ex.Message}");
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
#region Discovery
|
||||
|
||||
/// <summary>
|
||||
/// Accede alla pagina form-guide e scopre i meeting per le nazioni selezionate.
|
||||
/// La pagina Punters ha sezioni raggruppate per paese con id="country-XX".
|
||||
/// </summary>
|
||||
private async Task<List<MeetingInfo>> DiscoverMeetingsAsync(DateTime date, CancellationToken ct)
|
||||
{
|
||||
var meetings = new List<MeetingInfo>();
|
||||
|
||||
// Punters mostra "Today" di default; la data è basata sul timezone australiano.
|
||||
// Per UK/IE con date specifiche, proviamo il percorso con data.
|
||||
string html = await FetchPageAsync(FormGuideUrl, ct);
|
||||
if (string.IsNullOrEmpty(html)) return meetings;
|
||||
|
||||
var doc = await ParseHtmlAsync(html);
|
||||
|
||||
// Cerca le sezioni per ogni nazione richiesta
|
||||
foreach (string country in Countries)
|
||||
{
|
||||
string countryUpper = country.ToUpper();
|
||||
string selectorId = $"country-{countryUpper}";
|
||||
|
||||
// Cerca la tabella con id="country-XX"
|
||||
var countrySection = doc.QuerySelector($"#{selectorId}");
|
||||
|
||||
if (countrySection == null)
|
||||
{
|
||||
// Prova a cercare in tutto il documento per sezioni paese
|
||||
// che contengono il codice nel testo
|
||||
var allMeetingStates = doc.QuerySelectorAll(
|
||||
".race-meetings-desktop__meeting-state");
|
||||
foreach (var stateEl in allMeetingStates)
|
||||
{
|
||||
if (string.Equals(stateEl.TextContent.Trim(), countryUpper,
|
||||
StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// Risali alla riga del meeting
|
||||
var row = stateEl.Closest("tr");
|
||||
if (row != null)
|
||||
{
|
||||
var meetingInfo = ParseMeetingRow(row, countryUpper);
|
||||
if (meetingInfo != null)
|
||||
meetings.Add(meetingInfo);
|
||||
}
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
// Parsa le righe della tabella meeting
|
||||
var rows = countrySection.QuerySelectorAll(
|
||||
"tbody tr.race-meetings-desktop__races-row");
|
||||
|
||||
foreach (var row in rows)
|
||||
{
|
||||
var meetingInfo = ParseMeetingRow(row, countryUpper);
|
||||
if (meetingInfo != null)
|
||||
meetings.Add(meetingInfo);
|
||||
}
|
||||
}
|
||||
|
||||
return meetings;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parsifica una riga di meeting nella tabella form-guide.
|
||||
/// Estrae nome track e URL di ogni singola corsa.
|
||||
/// </summary>
|
||||
private MeetingInfo ParseMeetingRow(IElement row, string country)
|
||||
{
|
||||
// Nome track
|
||||
var nameLink = row.QuerySelector(
|
||||
".race-meetings-desktop__meeting-name a");
|
||||
if (nameLink == null) return null;
|
||||
|
||||
string trackName = nameLink.TextContent.Trim();
|
||||
string trackUrl = nameLink.GetAttribute("href") ?? "";
|
||||
|
||||
// Condizioni pista
|
||||
string condition = "";
|
||||
var condEl = row.QuerySelector("[class*='track-condition']");
|
||||
if (condEl != null)
|
||||
condition = condEl.TextContent.Trim();
|
||||
|
||||
// Meteo
|
||||
string weather = "";
|
||||
var weatherImg = row.QuerySelector(".weather__icon img");
|
||||
if (weatherImg != null)
|
||||
weather = weatherImg.GetAttribute("alt")?.Replace("Icon ", "") ?? "";
|
||||
|
||||
// Raccogli i link delle corse individuali
|
||||
var raceCards = row.QuerySelectorAll("td.race-meetings-desktop__event");
|
||||
var races = new List<RaceRef>();
|
||||
int raceNum = 0;
|
||||
|
||||
foreach (var td in raceCards)
|
||||
{
|
||||
raceNum++;
|
||||
var link = td.QuerySelector("a.event-card");
|
||||
if (link == null) continue;
|
||||
|
||||
string href = link.GetAttribute("href");
|
||||
if (string.IsNullOrEmpty(href)) continue;
|
||||
|
||||
// Controlla se la corsa ha dati (non è "no-data")
|
||||
if (link.ClassList.Contains("event-card--no-data")) continue;
|
||||
|
||||
races.Add(new RaceRef
|
||||
{
|
||||
Number = raceNum,
|
||||
Url = href.StartsWith("http") ? href : BaseUrl + href
|
||||
});
|
||||
}
|
||||
|
||||
if (races.Count == 0) return null;
|
||||
|
||||
return new MeetingInfo
|
||||
{
|
||||
Track = trackName,
|
||||
TrackUrl = trackUrl.StartsWith("http") ? trackUrl : BaseUrl + trackUrl,
|
||||
Country = country,
|
||||
Condition = condition,
|
||||
Weather = weather,
|
||||
Races = races
|
||||
};
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Race Page Extraction
|
||||
|
||||
/// <summary>
|
||||
/// Scarica la pagina di una singola corsa ed estrae i dati dei corridori.
|
||||
/// La pagina Punters SSR contiene tabelle/liste con i dettagli di ogni cavallo.
|
||||
/// </summary>
|
||||
private async Task FetchRaceRunners(DataTable dt, string raceUrl,
|
||||
MeetingInfo meeting, RaceRef race, CancellationToken ct)
|
||||
{
|
||||
string html = await FetchPageAsync(raceUrl, ct);
|
||||
if (string.IsNullOrEmpty(html)) return;
|
||||
|
||||
var doc = await ParseHtmlAsync(html);
|
||||
|
||||
// Estrai nome corsa dall'header della pagina
|
||||
string raceName = ExtractRaceName(doc);
|
||||
string distance = ExtractMetadata(doc, "distance", "dist");
|
||||
string raceClass = ExtractMetadata(doc, "class", "grade");
|
||||
string prize = ExtractMetadata(doc, "prize", "prizemoney", "purse");
|
||||
string startTime = ExtractMetadata(doc, "time", "start");
|
||||
|
||||
// Cerca la tabella/lista dei corridori.
|
||||
// Punters usa diverse strutture: tabelle, card, liste.
|
||||
// Proviamo diverse strategie di parsing.
|
||||
var runners = ExtractRunners(doc);
|
||||
|
||||
if (runners.Count == 0)
|
||||
{
|
||||
// Fallback: cerca qualsiasi tabella con dati di cavalli
|
||||
runners = ExtractRunnersFromTable(doc);
|
||||
}
|
||||
|
||||
foreach (var runner in runners)
|
||||
{
|
||||
var row = dt.NewRow();
|
||||
row["Meeting"] = meeting.Track;
|
||||
row["Paese"] = meeting.Country;
|
||||
row["Corsa N."] = race.Number;
|
||||
row["Nome Corsa"] = raceName;
|
||||
row["Orario"] = startTime;
|
||||
row["Distanza"] = distance;
|
||||
row["Terreno"] = meeting.Condition;
|
||||
row["Classe"] = raceClass;
|
||||
row["Meteo"] = meeting.Weather;
|
||||
row["Premio"] = prize;
|
||||
row["N. Corridori"] = runners.Count;
|
||||
row["Num"] = runner.Number;
|
||||
row["Cavallo"] = runner.Name;
|
||||
row["Fantino"] = runner.Jockey;
|
||||
row["Allenatore"] = runner.Trainer;
|
||||
row["Peso"] = runner.Weight;
|
||||
row["Box"] = runner.Barrier;
|
||||
row["Forma"] = runner.Form;
|
||||
row["Eta'"] = runner.Age;
|
||||
row["Ritirato"] = runner.Scratched ? "Si" : "";
|
||||
dt.Rows.Add(row);
|
||||
}
|
||||
}
|
||||
|
||||
private string ExtractRaceName(IDocument doc)
|
||||
{
|
||||
// Prova vari selettori per il nome della corsa
|
||||
var candidates = new[]
|
||||
{
|
||||
"h1", ".race-header__name", ".race-name",
|
||||
"[data-test='race-name']", ".event-header__title"
|
||||
};
|
||||
|
||||
foreach (var sel in candidates)
|
||||
{
|
||||
var el = doc.QuerySelector(sel);
|
||||
if (el != null)
|
||||
{
|
||||
string text = el.TextContent.Trim();
|
||||
if (!string.IsNullOrEmpty(text) && text.Length < 200)
|
||||
return text;
|
||||
}
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Estrae un metadato della corsa cercando tra diversi selettori e pattern.
|
||||
/// </summary>
|
||||
private string ExtractMetadata(IDocument doc, params string[] keywords)
|
||||
{
|
||||
// Cerca nelle sezioni di info/dettaglio della corsa
|
||||
var infoElements = doc.QuerySelectorAll(
|
||||
".race-info dt, .race-info dd, .race-details span, " +
|
||||
".race-header__info span, .event-info span, " +
|
||||
"[class*='race-detail'], [class*='event-detail']");
|
||||
|
||||
foreach (var el in infoElements)
|
||||
{
|
||||
string cls = el.ClassName ?? "";
|
||||
string text = el.TextContent.Trim();
|
||||
|
||||
foreach (string kw in keywords)
|
||||
{
|
||||
if (cls.Contains(kw, StringComparison.OrdinalIgnoreCase) ||
|
||||
text.StartsWith(kw, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
// Se è un dt, cerca il successivo dd
|
||||
if (el.TagName.Equals("DT", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var dd = el.NextElementSibling;
|
||||
return dd?.TextContent.Trim() ?? "";
|
||||
}
|
||||
return text;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return "";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Estrae i corridori cercando strutture note del sito Punters.
|
||||
/// Il sito usa card/righe per ogni corridore con classi CSS riconoscibili.
|
||||
/// </summary>
|
||||
private List<RunnerData> ExtractRunners(IDocument doc)
|
||||
{
|
||||
var runners = new List<RunnerData>();
|
||||
|
||||
// Strategia 1: cerca elementi con classi runner-like
|
||||
var runnerSelectors = new[]
|
||||
{
|
||||
".runner-row", ".runner-card", ".runner",
|
||||
"[data-test*='runner']", ".form-guide-runner",
|
||||
".runner-list__item", "tr[class*='runner']",
|
||||
".horse-row", ".selection-row"
|
||||
};
|
||||
|
||||
IEnumerable<IElement> runnerElements = null;
|
||||
|
||||
foreach (var sel in runnerSelectors)
|
||||
{
|
||||
var elements = doc.QuerySelectorAll(sel);
|
||||
if (elements.Length > 0)
|
||||
{
|
||||
runnerElements = elements;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (runnerElements == null) return runners;
|
||||
|
||||
foreach (var el in runnerElements)
|
||||
{
|
||||
var runner = ParseRunnerElement(el);
|
||||
if (runner != null)
|
||||
runners.Add(runner);
|
||||
}
|
||||
|
||||
return runners;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Fallback: estrae corridori da una tabella HTML generica.
|
||||
/// </summary>
|
||||
private List<RunnerData> ExtractRunnersFromTable(IDocument doc)
|
||||
{
|
||||
var runners = new List<RunnerData>();
|
||||
|
||||
// Cerca tabelle che sembrano contenere dati di corse
|
||||
var tables = doc.QuerySelectorAll("table");
|
||||
|
||||
foreach (var table in tables)
|
||||
{
|
||||
var headers = table.QuerySelectorAll("th")
|
||||
.Select(th => th.TextContent.Trim().ToLower())
|
||||
.ToList();
|
||||
|
||||
// Identifica se la tabella contiene dati di corridori
|
||||
bool hasHorseColumn = headers.Any(h =>
|
||||
h.Contains("horse") || h.Contains("runner") ||
|
||||
h.Contains("name") || h.Contains("cavallo"));
|
||||
|
||||
if (!hasHorseColumn) continue;
|
||||
|
||||
int nameIdx = headers.FindIndex(h =>
|
||||
h.Contains("horse") || h.Contains("runner") || h.Contains("name"));
|
||||
int numIdx = headers.FindIndex(h =>
|
||||
h == "no" || h == "#" || h.Contains("number") || h.Contains("tab"));
|
||||
int jockeyIdx = headers.FindIndex(h =>
|
||||
h.Contains("jockey") || h.Contains("rider"));
|
||||
int trainerIdx = headers.FindIndex(h =>
|
||||
h.Contains("trainer"));
|
||||
int weightIdx = headers.FindIndex(h =>
|
||||
h.Contains("weight") || h.Contains("wgt"));
|
||||
int barrierIdx = headers.FindIndex(h =>
|
||||
h.Contains("barrier") || h.Contains("gate") || h.Contains("draw"));
|
||||
int formIdx = headers.FindIndex(h =>
|
||||
h.Contains("form") || h.Contains("last"));
|
||||
|
||||
var rows = table.QuerySelectorAll("tbody tr");
|
||||
foreach (var row in rows)
|
||||
{
|
||||
var cells = row.QuerySelectorAll("td").ToList();
|
||||
if (cells.Count < 2) continue;
|
||||
|
||||
var runner = new RunnerData();
|
||||
|
||||
if (nameIdx >= 0 && nameIdx < cells.Count)
|
||||
runner.Name = CleanText(cells[nameIdx].TextContent);
|
||||
if (numIdx >= 0 && numIdx < cells.Count)
|
||||
runner.Number = ParseInt(cells[numIdx].TextContent);
|
||||
if (jockeyIdx >= 0 && jockeyIdx < cells.Count)
|
||||
runner.Jockey = CleanText(cells[jockeyIdx].TextContent);
|
||||
if (trainerIdx >= 0 && trainerIdx < cells.Count)
|
||||
runner.Trainer = CleanText(cells[trainerIdx].TextContent);
|
||||
if (weightIdx >= 0 && weightIdx < cells.Count)
|
||||
runner.Weight = CleanText(cells[weightIdx].TextContent);
|
||||
if (barrierIdx >= 0 && barrierIdx < cells.Count)
|
||||
runner.Barrier = CleanText(cells[barrierIdx].TextContent);
|
||||
if (formIdx >= 0 && formIdx < cells.Count)
|
||||
runner.Form = CleanText(cells[formIdx].TextContent);
|
||||
|
||||
if (!string.IsNullOrEmpty(runner.Name))
|
||||
runners.Add(runner);
|
||||
}
|
||||
|
||||
if (runners.Count > 0) break; // Usa la prima tabella valida
|
||||
}
|
||||
|
||||
return runners;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parsifica un singolo elemento corridore (card, riga, div) estraendo
|
||||
/// i dati con selettori e pattern euristici.
|
||||
/// </summary>
|
||||
private RunnerData ParseRunnerElement(IElement el)
|
||||
{
|
||||
var runner = new RunnerData();
|
||||
|
||||
// Numero
|
||||
var numEl = el.QuerySelector(
|
||||
"[class*='number'], [class*='tab-no'], .runner-number, " +
|
||||
"[data-test*='number'], .saddle-cloth");
|
||||
if (numEl != null)
|
||||
runner.Number = ParseInt(numEl.TextContent);
|
||||
|
||||
// Nome cavallo
|
||||
var nameEl = el.QuerySelector(
|
||||
"[class*='horse-name'], [class*='runner-name'], .horse a, " +
|
||||
"[data-test*='horse-name'], [class*='selection-name'], h3 a, h4 a");
|
||||
if (nameEl != null)
|
||||
runner.Name = CleanText(nameEl.TextContent);
|
||||
else
|
||||
{
|
||||
// Fallback: il primo link o header
|
||||
var link = el.QuerySelector("a[href*='/horses/']");
|
||||
if (link != null)
|
||||
runner.Name = CleanText(link.TextContent);
|
||||
}
|
||||
|
||||
if (string.IsNullOrEmpty(runner.Name)) return null;
|
||||
|
||||
// Fantino
|
||||
var jockeyEl = el.QuerySelector(
|
||||
"[class*='jockey'], a[href*='/jockeys/'], [data-test*='jockey']");
|
||||
if (jockeyEl != null)
|
||||
runner.Jockey = CleanText(jockeyEl.TextContent);
|
||||
|
||||
// Allenatore
|
||||
var trainerEl = el.QuerySelector(
|
||||
"[class*='trainer'], a[href*='/trainers/'], [data-test*='trainer']");
|
||||
if (trainerEl != null)
|
||||
runner.Trainer = CleanText(trainerEl.TextContent);
|
||||
|
||||
// Peso
|
||||
var weightEl = el.QuerySelector(
|
||||
"[class*='weight'], [data-test*='weight']");
|
||||
if (weightEl != null)
|
||||
runner.Weight = CleanText(weightEl.TextContent);
|
||||
|
||||
// Barrier/Gate
|
||||
var barrierEl = el.QuerySelector(
|
||||
"[class*='barrier'], [class*='gate'], [class*='draw'], " +
|
||||
"[data-test*='barrier']");
|
||||
if (barrierEl != null)
|
||||
runner.Barrier = CleanText(barrierEl.TextContent);
|
||||
|
||||
// Forma
|
||||
var formEl = el.QuerySelector(
|
||||
"[class*='form'], [class*='last-starts'], [data-test*='form']");
|
||||
if (formEl != null)
|
||||
runner.Form = CleanText(formEl.TextContent);
|
||||
|
||||
// Età
|
||||
var ageEl = el.QuerySelector("[class*='age']");
|
||||
if (ageEl != null)
|
||||
runner.Age = CleanText(ageEl.TextContent);
|
||||
|
||||
// Ritirato (scratched)
|
||||
runner.Scratched = el.ClassList.Any(c =>
|
||||
c.Contains("scratched", StringComparison.OrdinalIgnoreCase)) ||
|
||||
el.QuerySelector("[class*='scratched']") != null;
|
||||
|
||||
return runner;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region HTTP & Parsing
|
||||
|
||||
private async Task<string> FetchPageAsync(string url, CancellationToken ct)
|
||||
{
|
||||
ct.ThrowIfCancellationRequested();
|
||||
|
||||
try
|
||||
{
|
||||
using var request = new HttpRequestMessage(HttpMethod.Get, url);
|
||||
request.Headers.Add("Referer", BaseUrl + "/form-guide/");
|
||||
|
||||
using var response = await _http.SendAsync(request, ct);
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine(
|
||||
$"[Punters] HTTP {(int)response.StatusCode} per {url}");
|
||||
return null;
|
||||
}
|
||||
|
||||
return await response.Content.ReadAsStringAsync(ct);
|
||||
}
|
||||
catch (OperationCanceledException) { throw; }
|
||||
catch (Exception ex)
|
||||
{
|
||||
System.Diagnostics.Debug.WriteLine($"[Punters] Errore fetch {url}: {ex.Message}");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private async Task<IDocument> ParseHtmlAsync(string html)
|
||||
{
|
||||
var config = Configuration.Default;
|
||||
var context = BrowsingContext.New(config);
|
||||
return await context.OpenAsync(req => req.Content(html));
|
||||
}
|
||||
|
||||
private static async Task RandomDelayAsync(CancellationToken ct)
|
||||
{
|
||||
int delay = Rng.Next(MinDelayMs, MaxDelayMs);
|
||||
await Task.Delay(delay, ct);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region DataTable
|
||||
|
||||
private DataTable CreateTable()
|
||||
{
|
||||
var dt = new DataTable();
|
||||
dt.Columns.Add("Meeting", typeof(string));
|
||||
dt.Columns.Add("Paese", typeof(string));
|
||||
dt.Columns.Add("Corsa N.", typeof(int));
|
||||
dt.Columns.Add("Nome Corsa", typeof(string));
|
||||
dt.Columns.Add("Orario", typeof(string));
|
||||
dt.Columns.Add("Distanza", typeof(string));
|
||||
dt.Columns.Add("Terreno", typeof(string));
|
||||
dt.Columns.Add("Classe", typeof(string));
|
||||
dt.Columns.Add("Meteo", typeof(string));
|
||||
dt.Columns.Add("Premio", typeof(string));
|
||||
dt.Columns.Add("N. Corridori", typeof(int));
|
||||
dt.Columns.Add("Num", typeof(int));
|
||||
dt.Columns.Add("Cavallo", typeof(string));
|
||||
dt.Columns.Add("Fantino", typeof(string));
|
||||
dt.Columns.Add("Allenatore", typeof(string));
|
||||
dt.Columns.Add("Peso", typeof(string));
|
||||
dt.Columns.Add("Box", typeof(string));
|
||||
dt.Columns.Add("Forma", typeof(string));
|
||||
dt.Columns.Add("Eta'", typeof(string));
|
||||
dt.Columns.Add("Ritirato", typeof(string));
|
||||
return dt;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Models
|
||||
|
||||
private class MeetingInfo
|
||||
{
|
||||
public string Track { get; set; }
|
||||
public string TrackUrl { get; set; }
|
||||
public string Country { get; set; }
|
||||
public string Condition { get; set; }
|
||||
public string Weather { get; set; }
|
||||
public List<RaceRef> Races { get; set; } = new();
|
||||
}
|
||||
|
||||
private class RaceRef
|
||||
{
|
||||
public int Number { get; set; }
|
||||
public string Url { get; set; }
|
||||
}
|
||||
|
||||
private class RunnerData
|
||||
{
|
||||
public int Number { get; set; }
|
||||
public string Name { get; set; }
|
||||
public string Jockey { get; set; }
|
||||
public string Trainer { get; set; }
|
||||
public string Weight { get; set; }
|
||||
public string Barrier { get; set; }
|
||||
public string Form { get; set; }
|
||||
public string Age { get; set; }
|
||||
public bool Scratched { get; set; }
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Helpers
|
||||
|
||||
private static string CleanText(string text)
|
||||
{
|
||||
if (string.IsNullOrEmpty(text)) return "";
|
||||
return text.Trim().Replace("\n", " ").Replace("\r", "")
|
||||
.Replace(" ", " ").Trim();
|
||||
}
|
||||
|
||||
private static int ParseInt(string text)
|
||||
{
|
||||
if (string.IsNullOrEmpty(text)) return 0;
|
||||
var cleaned = new string(text.Where(c => char.IsDigit(c)).ToArray());
|
||||
return int.TryParse(cleaned, out int val) ? val : 0;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||
|
||||
<!-- MD3 Dark Theme - Color Tokens -->
|
||||
|
||||
<SolidColorBrush x:Key="BrPrimary" Color="#D0BCFF"/>
|
||||
<SolidColorBrush x:Key="BrOnPrimary" Color="#381E72"/>
|
||||
<SolidColorBrush x:Key="BrPrimaryContainer" Color="#4F378B"/>
|
||||
<SolidColorBrush x:Key="BrOnPrimaryContainer" Color="#EADDFF"/>
|
||||
<SolidColorBrush x:Key="BrSecondary" Color="#CCC2DC"/>
|
||||
<SolidColorBrush x:Key="BrOnSecondary" Color="#332D41"/>
|
||||
<SolidColorBrush x:Key="BrSecondaryContainer" Color="#4A4458"/>
|
||||
<SolidColorBrush x:Key="BrOnSecondaryContainer" Color="#E8DEF8"/>
|
||||
<SolidColorBrush x:Key="BrTertiary" Color="#EFB8C8"/>
|
||||
<SolidColorBrush x:Key="BrTertiaryContainer" Color="#633B48"/>
|
||||
<SolidColorBrush x:Key="BrOnTertiaryContainer" Color="#FFD8E4"/>
|
||||
<SolidColorBrush x:Key="BrError" Color="#F2B8B5"/>
|
||||
<SolidColorBrush x:Key="BrSurface" Color="#141218"/>
|
||||
<SolidColorBrush x:Key="BrOnSurface" Color="#E6E0E9"/>
|
||||
<SolidColorBrush x:Key="BrSurfaceVariant" Color="#49454F"/>
|
||||
<SolidColorBrush x:Key="BrOnSurfaceVariant" Color="#CAC4D0"/>
|
||||
<SolidColorBrush x:Key="BrSurfaceContainer" Color="#211F26"/>
|
||||
<SolidColorBrush x:Key="BrSurfaceContainerHigh" Color="#2B2930"/>
|
||||
<SolidColorBrush x:Key="BrSurfaceContainerHighest" Color="#36343B"/>
|
||||
<SolidColorBrush x:Key="BrSurfaceContainerLow" Color="#1D1B20"/>
|
||||
<SolidColorBrush x:Key="BrBackground" Color="#141218"/>
|
||||
<SolidColorBrush x:Key="BrOutline" Color="#938F99"/>
|
||||
<SolidColorBrush x:Key="BrOutlineVariant" Color="#49454F"/>
|
||||
|
||||
<!-- Legacy aliases -->
|
||||
<SolidColorBrush x:Key="BrBase" Color="#141218"/>
|
||||
<SolidColorBrush x:Key="BrMantle" Color="#211F26"/>
|
||||
<SolidColorBrush x:Key="BrSurface0" Color="#1D1B20"/>
|
||||
<SolidColorBrush x:Key="BrSurface1" Color="#211F26"/>
|
||||
<SolidColorBrush x:Key="BrSurface2" Color="#2B2930"/>
|
||||
<SolidColorBrush x:Key="BrOverlay0" Color="#CAC4D0"/>
|
||||
<SolidColorBrush x:Key="BrText" Color="#E6E0E9"/>
|
||||
<SolidColorBrush x:Key="BrSubtext0" Color="#CAC4D0"/>
|
||||
<SolidColorBrush x:Key="BrBlue" Color="#D0BCFF"/>
|
||||
<SolidColorBrush x:Key="BrGreen" Color="#A8D5A2"/>
|
||||
<SolidColorBrush x:Key="BrRed" Color="#F2B8B5"/>
|
||||
<SolidColorBrush x:Key="BrPeach" Color="#CCC2DC"/>
|
||||
<SolidColorBrush x:Key="BrLavender" Color="#4F378B"/>
|
||||
<SolidColorBrush x:Key="BrBorder" Color="#49454F"/>
|
||||
|
||||
<!-- Row hover tint (semi-transparent primary) -->
|
||||
<SolidColorBrush x:Key="BrRowHover" Color="#1AD0BCFF"/>
|
||||
|
||||
</ResourceDictionary>
|
||||
@@ -0,0 +1,69 @@
|
||||
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||
|
||||
<!-- ================================================================
|
||||
MD3 Dark Theme – Catppuccin Mocha-inspired
|
||||
Background: #1E1E2E Surface: #181825 Primary: #CBA6F7
|
||||
================================================================ -->
|
||||
|
||||
<SolidColorBrush x:Key="BrPrimary" Color="#A67FF5"/>
|
||||
<SolidColorBrush x:Key="BrOnPrimary" Color="#1C1030"/>
|
||||
<SolidColorBrush x:Key="BrPrimaryContainer" Color="#4A3580"/>
|
||||
<SolidColorBrush x:Key="BrOnPrimaryContainer" Color="#E8D8FF"/>
|
||||
|
||||
<SolidColorBrush x:Key="BrSecondary" Color="#A6ADC8"/>
|
||||
<SolidColorBrush x:Key="BrOnSecondary" Color="#1E1E2E"/>
|
||||
<SolidColorBrush x:Key="BrSecondaryContainer" Color="#363651"/>
|
||||
<SolidColorBrush x:Key="BrOnSecondaryContainer" Color="#CDD6F4"/>
|
||||
|
||||
<SolidColorBrush x:Key="BrTertiary" Color="#74C7EC"/>
|
||||
<SolidColorBrush x:Key="BrTertiaryContainer" Color="#1A3D50"/>
|
||||
<SolidColorBrush x:Key="BrOnTertiaryContainer" Color="#C8EEFF"/>
|
||||
|
||||
<SolidColorBrush x:Key="BrError" Color="#F28B82"/>
|
||||
<SolidColorBrush x:Key="BrErrorContainer" Color="#601410"/>
|
||||
<SolidColorBrush x:Key="BrOnError" Color="#330F0C"/>
|
||||
|
||||
<!-- Surfaces -->
|
||||
<SolidColorBrush x:Key="BrBackground" Color="#1E1E2E"/>
|
||||
<SolidColorBrush x:Key="BrSurface" Color="#181825"/>
|
||||
<SolidColorBrush x:Key="BrSurfaceVariant" Color="#313244"/>
|
||||
<SolidColorBrush x:Key="BrSurfaceContainer" Color="#26263C"/>
|
||||
<SolidColorBrush x:Key="BrSurfaceContainerHigh" Color="#2E2E44"/>
|
||||
<SolidColorBrush x:Key="BrSurfaceContainerHighest" Color="#363650"/>
|
||||
<SolidColorBrush x:Key="BrSurfaceContainerLow" Color="#1F1F31"/>
|
||||
|
||||
<!-- On-surfaces -->
|
||||
<SolidColorBrush x:Key="BrOnSurface" Color="#CDD6F4"/>
|
||||
<SolidColorBrush x:Key="BrOnSurfaceVariant" Color="#7F849C"/>
|
||||
|
||||
<!-- Outline -->
|
||||
<SolidColorBrush x:Key="BrOutline" Color="#45475A"/>
|
||||
<SolidColorBrush x:Key="BrOutlineVariant" Color="#313244"/>
|
||||
|
||||
<!-- Semantic colors (status badges) -->
|
||||
<SolidColorBrush x:Key="BrSuccess" Color="#A6E3A1"/>
|
||||
<SolidColorBrush x:Key="BrWarn" Color="#FAB387"/>
|
||||
<SolidColorBrush x:Key="BrInfo" Color="#89B4FA"/>
|
||||
<SolidColorBrush x:Key="BrLogBg" Color="#14141E"/>
|
||||
|
||||
<!-- Legacy aliases (keep names stable) -->
|
||||
<SolidColorBrush x:Key="BrBase" Color="#1E1E2E"/>
|
||||
<SolidColorBrush x:Key="BrMantle" Color="#181825"/>
|
||||
<SolidColorBrush x:Key="BrSurface0" Color="#313244"/>
|
||||
<SolidColorBrush x:Key="BrSurface1" Color="#45475A"/>
|
||||
<SolidColorBrush x:Key="BrSurface2" Color="#585B70"/>
|
||||
<SolidColorBrush x:Key="BrOverlay0" Color="#6C7086"/>
|
||||
<SolidColorBrush x:Key="BrText" Color="#CDD6F4"/>
|
||||
<SolidColorBrush x:Key="BrSubtext0" Color="#A6ADC8"/>
|
||||
<SolidColorBrush x:Key="BrBlue" Color="#89B4FA"/>
|
||||
<SolidColorBrush x:Key="BrGreen" Color="#A6E3A1"/>
|
||||
<SolidColorBrush x:Key="BrRed" Color="#F38BA8"/>
|
||||
<SolidColorBrush x:Key="BrPeach" Color="#FAB387"/>
|
||||
<SolidColorBrush x:Key="BrLavender" Color="#B4BEFE"/>
|
||||
<SolidColorBrush x:Key="BrBorder" Color="#45475A"/>
|
||||
|
||||
<!-- Row hover tint -->
|
||||
<SolidColorBrush x:Key="BrRowHover" Color="#2A2A40"/>
|
||||
|
||||
</ResourceDictionary>
|
||||
@@ -21,8 +21,9 @@ namespace HorseRacingPredictor
|
||||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
|
||||
};
|
||||
|
||||
// ?? Football ????????????????????????????????????????????
|
||||
// ?? Football ??????????????????????????????????????????
|
||||
public string ApiKey { get; set; } = string.Empty;
|
||||
public string FbDataSource { get; set; } = "API - API-Football";
|
||||
public string FbExportPath { get; set; } = string.Empty;
|
||||
public string FbPrefix { get; set; } = string.Empty;
|
||||
public string FbSuffix { get; set; } = string.Empty;
|
||||
@@ -30,8 +31,38 @@ namespace HorseRacingPredictor
|
||||
public string FbDateFormat { get; set; } = "yyyy-MM-dd";
|
||||
public string FbFormat { get; set; } = "CSV";
|
||||
|
||||
// ?? Racing ??????????????????????????????????????????????
|
||||
// ?? Football Download Options ????????????????????????????
|
||||
public bool FbDownloadFixtures { get; set; } = true;
|
||||
public bool FbDownloadOdds { get; set; } = true;
|
||||
public bool FbDownloadPredictions { get; set; } = true;
|
||||
public bool FbDownloadStandings { get; set; } = false;
|
||||
public bool FbDownloadH2H { get; set; } = false;
|
||||
public bool FbDownloadEvents { get; set; } = false;
|
||||
public bool FbDownloadLineups { get; set; } = false;
|
||||
public bool FbDownloadStatistics { get; set; } = false;
|
||||
public bool FbDownloadInjuries { get; set; } = false;
|
||||
public List<int> FbLeagueIds { get; set; } = new();
|
||||
public string FbTimezone { get; set; } = "Europe/Rome";
|
||||
public int FbSeason { get; set; } = 0;
|
||||
public int FbApiDelayMs { get; set; } = 300;
|
||||
public bool FbCheckQuota { get; set; } = true;
|
||||
public int FbMinRemainingQuota { get; set; } = 10;
|
||||
public string FbTimeFrom { get; set; } = null;
|
||||
public string FbTimeTo { get; set; } = null;
|
||||
|
||||
// ?? Football Supplementary Downloads (CSV separati) ??????
|
||||
public bool FbDownloadPlayerStats { get; set; } = false;
|
||||
public bool FbDownloadTeamStats { get; set; } = false;
|
||||
public bool FbDownloadTopScorers { get; set; } = false;
|
||||
public bool FbDownloadTopAssists { get; set; } = false;
|
||||
public bool FbDownloadTopCards { get; set; } = false;
|
||||
public bool FbDownloadSquads { get; set; } = false;
|
||||
public bool FbDownloadCoaches { get; set; } = false;
|
||||
public bool FbDownloadTransfers { get; set; } = false;
|
||||
|
||||
// ?? Racing
|
||||
public string RacingApiKey { get; set; } = string.Empty;
|
||||
public string RcDataSource { get; set; } = "API - FormFav";
|
||||
public string RcExportPath { get; set; } = string.Empty;
|
||||
public string RcPrefix { get; set; } = string.Empty;
|
||||
public string RcSuffix { get; set; } = string.Empty;
|
||||
@@ -40,8 +71,48 @@ namespace HorseRacingPredictor
|
||||
public string RcFormat { get; set; } = "CSV";
|
||||
public string RcTimezone { get; set; } = "Australia/Sydney";
|
||||
public List<string> RcCountries { get; set; } = new() { "au", "nz" };
|
||||
public bool RcPuntersSplitCsv { get; set; } = false;
|
||||
|
||||
// ?? Persistence ?????????????????????????????????????????
|
||||
// ?? Aspetto ??????????????????????????????????????????
|
||||
public bool DarkMode { get; set; } = false;
|
||||
|
||||
// ?? Persistence ??????????????????????????????????????
|
||||
|
||||
/// <summary>
|
||||
/// Costruisce un FootballDownloadOptions a partire dalle impostazioni salvate.
|
||||
/// </summary>
|
||||
public Football.FootballDownloadOptions ToFootballDownloadOptions()
|
||||
{
|
||||
return new Football.FootballDownloadOptions
|
||||
{
|
||||
DownloadFixtures = FbDownloadFixtures,
|
||||
DownloadOdds = FbDownloadOdds,
|
||||
DownloadPredictions = FbDownloadPredictions,
|
||||
DownloadStandings = FbDownloadStandings,
|
||||
DownloadH2H = FbDownloadH2H,
|
||||
DownloadEvents = FbDownloadEvents,
|
||||
DownloadLineups = FbDownloadLineups,
|
||||
DownloadStatistics = FbDownloadStatistics,
|
||||
DownloadInjuries = FbDownloadInjuries,
|
||||
LeagueIds = new List<int>(FbLeagueIds),
|
||||
Timezone = FbTimezone,
|
||||
Season = FbSeason,
|
||||
ApiDelayMs = FbApiDelayMs,
|
||||
CheckQuota = FbCheckQuota,
|
||||
MinRemainingQuota = FbMinRemainingQuota,
|
||||
TimeFrom = TimeSpan.TryParse(FbTimeFrom, out var tf) ? tf : (TimeSpan?)null,
|
||||
TimeTo = TimeSpan.TryParse(FbTimeTo, out var tt) ? tt : (TimeSpan?)null,
|
||||
// Supplementari
|
||||
DownloadPlayerStats = FbDownloadPlayerStats,
|
||||
DownloadTeamStats = FbDownloadTeamStats,
|
||||
DownloadTopScorers = FbDownloadTopScorers,
|
||||
DownloadTopAssists = FbDownloadTopAssists,
|
||||
DownloadTopCards = FbDownloadTopCards,
|
||||
DownloadSquads = FbDownloadSquads,
|
||||
DownloadCoaches = FbDownloadCoaches,
|
||||
DownloadTransfers = FbDownloadTransfers,
|
||||
};
|
||||
}
|
||||
|
||||
public static UserSettings Load()
|
||||
{
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
{
|
||||
"ConnectionStrings": {
|
||||
"Football": "Server=DESKTOP-9O9JHFS;Database=TestBS_Football;User Id=sa;Password=Asti2019;TrustServerCertificate=True",
|
||||
"Horses": "Server=DESKTOP-9O9JHFS;Database=TestBS_Horses;User Id=sa;Password=Asti2019;TrustServerCertificate=True"
|
||||
},
|
||||
"Api": {
|
||||
"FootballApiKey": "f3795ccef056c5478d316162517d9970",
|
||||
"FootballApiKeyHeader": "x-rapidapi-key",
|
||||
"FootballApiHost": "v3.football.api-sports.io"
|
||||
}
|
||||
}
|
||||
+20
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"ConnectionStrings": {
|
||||
"Football": "Server=YOUR_SERVER;Database=YOUR_DB;User Id=YOUR_USER;Password=YOUR_PASSWORD;TrustServerCertificate=True",
|
||||
"Horses": "Server=YOUR_SERVER;Database=YOUR_DB;User Id=YOUR_USER;Password=YOUR_PASSWORD;TrustServerCertificate=True"
|
||||
},
|
||||
"Api": {
|
||||
"FootballApiKey": "",
|
||||
"FootballApiKeyHeader": "x-rapidapi-key",
|
||||
"FootballApiHost": "v3.football.api-sports.io"
|
||||
},
|
||||
"WebSearch": {
|
||||
"Provider": "SearXng",
|
||||
"ApiKey": "",
|
||||
"GoogleCx": "",
|
||||
"SearXNgUrl": "http://192.168.30.23:8082",
|
||||
"MaxResults": 10,
|
||||
"DelayMs": 300,
|
||||
"Language": "it"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,256 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
|
||||
namespace HorseRacingPredictor.Infrastructure
|
||||
{
|
||||
/// <summary>
|
||||
/// Log severity level.
|
||||
/// </summary>
|
||||
public enum LogLevel
|
||||
{
|
||||
Debug,
|
||||
Info,
|
||||
Warning,
|
||||
Error,
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// A single structured log entry exposed for in-memory/UI consumers.
|
||||
/// </summary>
|
||||
public sealed class LogEntry
|
||||
{
|
||||
public DateTime Timestamp { get; init; }
|
||||
public LogLevel Level { get; init; }
|
||||
public string Category { get; init; }
|
||||
public string Message { get; init; }
|
||||
public Exception Exception { get; init; }
|
||||
|
||||
public string LevelTag => Level switch
|
||||
{
|
||||
LogLevel.Debug => "DBG",
|
||||
LogLevel.Info => "INF",
|
||||
LogLevel.Warning => "WRN",
|
||||
LogLevel.Error => "ERR",
|
||||
_ => "???"
|
||||
};
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Thread-safe, file-based application logger.
|
||||
/// Writes to %AppData%\HorseRacingPredictor\logs\app-YYYY-MM-DD.log.
|
||||
/// A new file is created each day; old files are kept for 30 days.
|
||||
/// </summary>
|
||||
public sealed class AppLogger
|
||||
{
|
||||
// ?? Singleton ????????????????????????????????????????????????????????????
|
||||
|
||||
private static readonly Lazy<AppLogger> _instance =
|
||||
new(() => new AppLogger(), LazyThreadSafetyMode.ExecutionAndPublication);
|
||||
|
||||
public static AppLogger Instance => _instance.Value;
|
||||
|
||||
// ?? Configuration ????????????????????????????????????????????????????????
|
||||
|
||||
/// <summary>Minimum level written to file (default: Debug).</summary>
|
||||
public LogLevel MinLevel { get; set; } = LogLevel.Debug;
|
||||
|
||||
/// <summary>Also write to Debug output (default: true in DEBUG builds).</summary>
|
||||
#if DEBUG
|
||||
public bool WriteToDebugOutput { get; set; } = true;
|
||||
#else
|
||||
public bool WriteToDebugOutput { get; set; } = false;
|
||||
#endif
|
||||
|
||||
// ?? State ????????????????????????????????????????????????????????????????
|
||||
|
||||
private readonly string _logDir;
|
||||
private readonly SemaphoreSlim _lock = new(1, 1);
|
||||
private string _currentLogPath;
|
||||
private DateTime _currentDate;
|
||||
|
||||
// ?? In-memory buffer + live event ????????????????????????????????????????
|
||||
|
||||
private const int MaxBufferSize = 2000;
|
||||
private readonly ConcurrentQueue<LogEntry> _buffer = new();
|
||||
private int _bufferCount = 0;
|
||||
|
||||
/// <summary>
|
||||
/// Fired on the thread-pool whenever a new entry is logged.
|
||||
/// UI subscribers must marshal to the UI thread themselves.
|
||||
/// </summary>
|
||||
public event EventHandler<LogEntry> EntryLogged;
|
||||
|
||||
/// <summary>Returns a snapshot of all buffered entries (newest last).</summary>
|
||||
public IReadOnlyList<LogEntry> GetBufferedEntries()
|
||||
{
|
||||
return new List<LogEntry>(_buffer);
|
||||
}
|
||||
|
||||
// ?? Constructor ??????????????????????????????????????????????????????????
|
||||
|
||||
private AppLogger()
|
||||
{
|
||||
_logDir = Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
|
||||
"HorseRacingPredictor",
|
||||
"logs");
|
||||
|
||||
try { Directory.CreateDirectory(_logDir); }
|
||||
catch { /* if we can't create the dir, logging will silently fail */ }
|
||||
|
||||
RefreshLogPath(DateTime.Today);
|
||||
PurgeOldLogs(30);
|
||||
|
||||
Log(LogLevel.Info, "Logger", "=== AppLogger avviato ===");
|
||||
}
|
||||
|
||||
// ?? Public API ????????????????????????????????????????????????????????????
|
||||
|
||||
public static void Debug(string category, string message, Exception ex = null)
|
||||
=> Instance.Log(LogLevel.Debug, category, message, ex);
|
||||
|
||||
public static void Info(string category, string message, Exception ex = null)
|
||||
=> Instance.Log(LogLevel.Info, category, message, ex);
|
||||
|
||||
public static void Warn(string category, string message, Exception ex = null)
|
||||
=> Instance.Log(LogLevel.Warning, category, message, ex);
|
||||
|
||||
public static void Error(string category, string message, Exception ex = null)
|
||||
=> Instance.Log(LogLevel.Error, category, message, ex);
|
||||
|
||||
/// <summary>
|
||||
/// Log an outgoing API HTTP request.
|
||||
/// </summary>
|
||||
public static void LogApiRequest(string endpoint, string parameters = null)
|
||||
{
|
||||
var msg = string.IsNullOrWhiteSpace(parameters)
|
||||
? $"→ REQUEST {endpoint}"
|
||||
: $"→ REQUEST {endpoint} [{parameters}]";
|
||||
Instance.Log(LogLevel.Info, "API", msg);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Log an incoming API HTTP response.
|
||||
/// </summary>
|
||||
public static void LogApiResponse(string endpoint, int statusCode, string content, int maxLen = 2000)
|
||||
{
|
||||
var body = content ?? "";
|
||||
var truncated = body.Length > maxLen ? body.Substring(0, maxLen) + $"…(+{body.Length - maxLen} chars)" : body;
|
||||
var msg = $"← RESPONSE {endpoint} [{statusCode}] {truncated}";
|
||||
Instance.Log(LogLevel.Info, "API", msg);
|
||||
}
|
||||
|
||||
// ?? Core write ????????????????????????????????????????????????????????????
|
||||
|
||||
public void Log(LogLevel level, string category, string message, Exception ex = null)
|
||||
{
|
||||
if (level < MinLevel) return;
|
||||
|
||||
var now = DateTime.Now;
|
||||
var date = now.Date;
|
||||
var line = FormatLine(now, level, category, message, ex);
|
||||
|
||||
if (WriteToDebugOutput)
|
||||
System.Diagnostics.Debug.WriteLine(line);
|
||||
|
||||
// Rotate file if date changed
|
||||
if (date != _currentDate)
|
||||
{
|
||||
RefreshLogPath(date);
|
||||
PurgeOldLogs(30);
|
||||
}
|
||||
|
||||
WriteToFile(line);
|
||||
|
||||
// In-memory buffer for UI (capped at MaxBufferSize)
|
||||
var entry = new LogEntry { Timestamp = now, Level = level, Category = category, Message = message, Exception = ex };
|
||||
_buffer.Enqueue(entry);
|
||||
if (Interlocked.Increment(ref _bufferCount) > MaxBufferSize)
|
||||
{
|
||||
_buffer.TryDequeue(out _);
|
||||
Interlocked.Decrement(ref _bufferCount);
|
||||
}
|
||||
|
||||
// Fire event (non-blocking, thread-pool)
|
||||
var handler = EntryLogged;
|
||||
if (handler != null)
|
||||
System.Threading.ThreadPool.QueueUserWorkItem(_ => handler(this, entry));
|
||||
}
|
||||
|
||||
/// <summary>Returns the path of the current log file.</summary>
|
||||
public string CurrentLogPath => _currentLogPath;
|
||||
|
||||
// ?? Helpers ???????????????????????????????????????????????????????????????
|
||||
|
||||
private void RefreshLogPath(DateTime date)
|
||||
{
|
||||
_currentDate = date;
|
||||
_currentLogPath = Path.Combine(_logDir, $"app-{date:yyyy-MM-dd}.log");
|
||||
}
|
||||
|
||||
private void WriteToFile(string line)
|
||||
{
|
||||
if (string.IsNullOrEmpty(_currentLogPath)) return;
|
||||
|
||||
_lock.Wait();
|
||||
try
|
||||
{
|
||||
File.AppendAllText(_currentLogPath, line + Environment.NewLine, Encoding.UTF8);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Non-fatal: logging should never crash the app
|
||||
}
|
||||
finally
|
||||
{
|
||||
_lock.Release();
|
||||
}
|
||||
}
|
||||
|
||||
private static string FormatLine(DateTime ts, LogLevel level, string category, string message, Exception ex)
|
||||
{
|
||||
string levelTag = level switch
|
||||
{
|
||||
LogLevel.Debug => "DBG",
|
||||
LogLevel.Info => "INF",
|
||||
LogLevel.Warning => "WRN",
|
||||
LogLevel.Error => "ERR",
|
||||
_ => "???"
|
||||
};
|
||||
|
||||
var sb = new StringBuilder();
|
||||
sb.Append($"[{ts:yyyy-MM-dd HH:mm:ss.fff}] [{levelTag}] [{category}] {message}");
|
||||
|
||||
if (ex != null)
|
||||
{
|
||||
sb.AppendLine();
|
||||
sb.Append($" Exception: {ex.GetType().Name}: {ex.Message}");
|
||||
if (ex.InnerException != null)
|
||||
sb.Append($" | Inner: {ex.InnerException.Message}");
|
||||
sb.AppendLine();
|
||||
sb.Append($" StackTrace: {ex.StackTrace?.Replace(Environment.NewLine, " | ")}");
|
||||
}
|
||||
|
||||
return sb.ToString();
|
||||
}
|
||||
|
||||
private void PurgeOldLogs(int keepDays)
|
||||
{
|
||||
try
|
||||
{
|
||||
var cutoff = DateTime.Today.AddDays(-keepDays);
|
||||
foreach (var file in Directory.GetFiles(_logDir, "app-*.log"))
|
||||
{
|
||||
var name = Path.GetFileNameWithoutExtension(file); // "app-2024-01-01"
|
||||
if (DateTime.TryParse(name.Substring(4), out var fileDate) && fileDate < cutoff)
|
||||
File.Delete(file);
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
using BettingPredictor.UI;
|
||||
using BettingPredictor.UI;
|
||||
using System;
|
||||
using System.Data;
|
||||
using System.IO;
|
||||
@@ -16,8 +16,8 @@ namespace BettingPredictor
|
||||
private DataTable racingData;
|
||||
|
||||
// Credenziali predefinite Racing API
|
||||
private const string DefaultRacingUser = "qi1mHOHPquDY9KNDASAeGipy";
|
||||
private const string DefaultRacingPass = "RXNFU1YX27R9rTnk8Vop8ZfH";
|
||||
private const string DefaultRacingUser = "";
|
||||
private const string DefaultRacingPass = "";
|
||||
|
||||
// Pagine e nav gestiti come array per semplificare la navigazione
|
||||
private Panel[] pages;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -26,7 +26,11 @@ namespace HorseRacingPredictor.Manager
|
||||
var response = clientApi.Execute(request);
|
||||
if (!response.IsSuccessful)
|
||||
{
|
||||
throw new Exception($"Errore nella richiesta API: {response.ErrorMessage}");
|
||||
string errDetail = !string.IsNullOrEmpty(response.ErrorMessage)
|
||||
? response.ErrorMessage
|
||||
: $"HTTP {(int)response.StatusCode} {response.StatusCode}" +
|
||||
(!string.IsNullOrEmpty(response.Content) ? $" – {response.Content[..Math.Min(200, response.Content.Length)]}" : "");
|
||||
throw new Exception($"Errore nella richiesta API: {errDetail}");
|
||||
}
|
||||
|
||||
// Aggiungi una pausa tra una chiamata e l'altra
|
||||
|
||||
@@ -0,0 +1,49 @@
|
||||
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||
|
||||
<!-- MD3 Dark Theme - Color Tokens -->
|
||||
|
||||
<SolidColorBrush x:Key="BrPrimary" Color="#D0BCFF"/>
|
||||
<SolidColorBrush x:Key="BrOnPrimary" Color="#381E72"/>
|
||||
<SolidColorBrush x:Key="BrPrimaryContainer" Color="#4F378B"/>
|
||||
<SolidColorBrush x:Key="BrOnPrimaryContainer" Color="#EADDFF"/>
|
||||
<SolidColorBrush x:Key="BrSecondary" Color="#CCC2DC"/>
|
||||
<SolidColorBrush x:Key="BrOnSecondary" Color="#332D41"/>
|
||||
<SolidColorBrush x:Key="BrSecondaryContainer" Color="#4A4458"/>
|
||||
<SolidColorBrush x:Key="BrOnSecondaryContainer" Color="#E8DEF8"/>
|
||||
<SolidColorBrush x:Key="BrTertiary" Color="#EFB8C8"/>
|
||||
<SolidColorBrush x:Key="BrTertiaryContainer" Color="#633B48"/>
|
||||
<SolidColorBrush x:Key="BrOnTertiaryContainer" Color="#FFD8E4"/>
|
||||
<SolidColorBrush x:Key="BrError" Color="#F2B8B5"/>
|
||||
<SolidColorBrush x:Key="BrSurface" Color="#141218"/>
|
||||
<SolidColorBrush x:Key="BrOnSurface" Color="#E6E0E9"/>
|
||||
<SolidColorBrush x:Key="BrSurfaceVariant" Color="#49454F"/>
|
||||
<SolidColorBrush x:Key="BrOnSurfaceVariant" Color="#CAC4D0"/>
|
||||
<SolidColorBrush x:Key="BrSurfaceContainer" Color="#211F26"/>
|
||||
<SolidColorBrush x:Key="BrSurfaceContainerHigh" Color="#2B2930"/>
|
||||
<SolidColorBrush x:Key="BrSurfaceContainerHighest" Color="#36343B"/>
|
||||
<SolidColorBrush x:Key="BrSurfaceContainerLow" Color="#1D1B20"/>
|
||||
<SolidColorBrush x:Key="BrBackground" Color="#141218"/>
|
||||
<SolidColorBrush x:Key="BrOutline" Color="#938F99"/>
|
||||
<SolidColorBrush x:Key="BrOutlineVariant" Color="#49454F"/>
|
||||
|
||||
<!-- Legacy aliases -->
|
||||
<SolidColorBrush x:Key="BrBase" Color="#141218"/>
|
||||
<SolidColorBrush x:Key="BrMantle" Color="#211F26"/>
|
||||
<SolidColorBrush x:Key="BrSurface0" Color="#1D1B20"/>
|
||||
<SolidColorBrush x:Key="BrSurface1" Color="#211F26"/>
|
||||
<SolidColorBrush x:Key="BrSurface2" Color="#2B2930"/>
|
||||
<SolidColorBrush x:Key="BrOverlay0" Color="#CAC4D0"/>
|
||||
<SolidColorBrush x:Key="BrText" Color="#E6E0E9"/>
|
||||
<SolidColorBrush x:Key="BrSubtext0" Color="#CAC4D0"/>
|
||||
<SolidColorBrush x:Key="BrBlue" Color="#D0BCFF"/>
|
||||
<SolidColorBrush x:Key="BrGreen" Color="#A8D5A2"/>
|
||||
<SolidColorBrush x:Key="BrRed" Color="#F2B8B5"/>
|
||||
<SolidColorBrush x:Key="BrPeach" Color="#CCC2DC"/>
|
||||
<SolidColorBrush x:Key="BrLavender" Color="#4F378B"/>
|
||||
<SolidColorBrush x:Key="BrBorder" Color="#49454F"/>
|
||||
|
||||
<!-- Row hover tint (semi-transparent primary) -->
|
||||
<SolidColorBrush x:Key="BrRowHover" Color="#1AD0BCFF"/>
|
||||
|
||||
</ResourceDictionary>
|
||||
@@ -0,0 +1,470 @@
|
||||
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||
|
||||
<!-- Elevation drop shadows (MD3 tonal elevation complement) -->
|
||||
<DropShadowEffect x:Key="Elevation1" BlurRadius="3" ShadowDepth="1" Opacity="0.12" Color="#000000"/>
|
||||
<DropShadowEffect x:Key="Elevation2" BlurRadius="6" ShadowDepth="2" Opacity="0.14" Color="#000000"/>
|
||||
<DropShadowEffect x:Key="Elevation3" BlurRadius="12" ShadowDepth="4" Opacity="0.16" Color="#000000"/>
|
||||
<DropShadowEffect x:Key="SubtleDropShadow" BlurRadius="6" ShadowDepth="2" Opacity="0.12" Color="#000000"/>
|
||||
|
||||
<!-- =============================================================
|
||||
NAVIGATION RAIL BUTTON (MD3 � pill active indicator)
|
||||
============================================================= -->
|
||||
<Style x:Key="NavBtn" TargetType="RadioButton">
|
||||
<Setter Property="Width" Value="80"/>
|
||||
<Setter Property="Height" Value="72"/>
|
||||
<Setter Property="Margin" Value="0,2"/>
|
||||
<Setter Property="Cursor" Value="Hand"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource BrOnSurfaceVariant}"/>
|
||||
<Setter Property="Background" Value="Transparent"/>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="RadioButton">
|
||||
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center">
|
||||
<Grid HorizontalAlignment="Center" Width="56" Height="32">
|
||||
<!-- Active pill indicator -->
|
||||
<Border x:Name="Pill" CornerRadius="16"
|
||||
Background="Transparent"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Center"
|
||||
Width="56" Height="32"/>
|
||||
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||
</Grid>
|
||||
<TextBlock x:Name="NavLabel"
|
||||
Text="{TemplateBinding Tag}"
|
||||
FontFamily="Roboto, Segoe UI" FontSize="12" FontWeight="Medium"
|
||||
Foreground="{TemplateBinding Foreground}"
|
||||
HorizontalAlignment="Center" TextAlignment="Center"
|
||||
Margin="0,4,0,0"/>
|
||||
</StackPanel>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter TargetName="Pill" Property="Background" Value="{DynamicResource BrRowHover}"/>
|
||||
</Trigger>
|
||||
<Trigger Property="IsChecked" Value="True">
|
||||
<Setter TargetName="Pill" Property="Background" Value="{DynamicResource BrSecondaryContainer}"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource BrOnSecondaryContainer}"/>
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<!-- =============================================================
|
||||
FILLED BUTTON (MD3 � pill shape, height 40px)
|
||||
============================================================= -->
|
||||
<Style x:Key="AccentBtn" TargetType="Button">
|
||||
<Setter Property="Foreground" Value="{DynamicResource BrOnPrimary}"/>
|
||||
<Setter Property="Background" Value="{DynamicResource BrPrimary}"/>
|
||||
<Setter Property="FontFamily" Value="Roboto, Segoe UI"/>
|
||||
<Setter Property="FontWeight" Value="Medium"/>
|
||||
<Setter Property="FontSize" Value="14"/>
|
||||
<Setter Property="Height" Value="40"/>
|
||||
<Setter Property="Padding" Value="24,0"/>
|
||||
<Setter Property="Cursor" Value="Hand"/>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="Button">
|
||||
<Border x:Name="Bd" CornerRadius="4"
|
||||
Background="{TemplateBinding Background}"
|
||||
Padding="{TemplateBinding Padding}"
|
||||
Effect="{StaticResource Elevation1}">
|
||||
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter TargetName="Bd" Property="Opacity" Value="0.92"/>
|
||||
<Setter TargetName="Bd" Property="Effect" Value="{StaticResource Elevation2}"/>
|
||||
</Trigger>
|
||||
<Trigger Property="IsPressed" Value="True">
|
||||
<Setter TargetName="Bd" Property="Opacity" Value="0.88"/>
|
||||
</Trigger>
|
||||
<Trigger Property="IsEnabled" Value="False">
|
||||
<Setter TargetName="Bd" Property="Opacity" Value="0.38"/>
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<!-- Tonal button alias used by some pages -->
|
||||
<Style x:Key="ToolBtn" TargetType="Button">
|
||||
<Setter Property="Foreground" Value="{DynamicResource BrOnSecondaryContainer}"/>
|
||||
<Setter Property="Background" Value="{DynamicResource BrSecondaryContainer}"/>
|
||||
<Setter Property="FontFamily" Value="Roboto, Segoe UI"/>
|
||||
<Setter Property="FontWeight" Value="Medium"/>
|
||||
<Setter Property="FontSize" Value="14"/>
|
||||
<Setter Property="Height" Value="40"/>
|
||||
<Setter Property="Padding" Value="24,0"/>
|
||||
<Setter Property="Cursor" Value="Hand"/>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="Button">
|
||||
<Border x:Name="Bd" CornerRadius="4"
|
||||
Background="{TemplateBinding Background}"
|
||||
Padding="{TemplateBinding Padding}">
|
||||
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter TargetName="Bd" Property="Opacity" Value="0.92"/>
|
||||
</Trigger>
|
||||
<Trigger Property="IsEnabled" Value="False">
|
||||
<Setter TargetName="Bd" Property="Opacity" Value="0.38"/>
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<!-- =============================================================
|
||||
OUTLINED TEXT FIELD (MD3 � 4px corner, 2px focus border)
|
||||
============================================================= -->
|
||||
<Style x:Key="FlatTb" TargetType="TextBox">
|
||||
<Setter Property="Background" Value="Transparent"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource BrOnSurface}"/>
|
||||
<Setter Property="CaretBrush" Value="{DynamicResource BrPrimary}"/>
|
||||
<Setter Property="SelectionBrush" Value="{DynamicResource BrPrimaryContainer}"/>
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource BrOutline}"/>
|
||||
<Setter Property="BorderThickness" Value="1"/>
|
||||
<Setter Property="Height" Value="40"/>
|
||||
<Setter Property="Padding" Value="16,0"/>
|
||||
<Setter Property="VerticalContentAlignment" Value="Center"/>
|
||||
<Setter Property="FontFamily" Value="Roboto, Segoe UI"/>
|
||||
<Setter Property="FontSize" Value="14"/>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="TextBox">
|
||||
<Border x:Name="Bd" CornerRadius="4"
|
||||
Background="{TemplateBinding Background}"
|
||||
BorderBrush="{TemplateBinding BorderBrush}"
|
||||
BorderThickness="{TemplateBinding BorderThickness}"
|
||||
Padding="{TemplateBinding Padding}">
|
||||
<ScrollViewer x:Name="PART_ContentHost" VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsFocused" Value="True">
|
||||
<Setter TargetName="Bd" Property="BorderBrush" Value="{DynamicResource BrPrimary}"/>
|
||||
<Setter TargetName="Bd" Property="BorderThickness" Value="2"/>
|
||||
</Trigger>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter TargetName="Bd" Property="BorderBrush" Value="{DynamicResource BrOnSurface}"/>
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<!-- =============================================================
|
||||
PASSWORD BOX (MD3 outlined)
|
||||
============================================================= -->
|
||||
<Style x:Key="FlatPb" TargetType="PasswordBox">
|
||||
<Setter Property="Background" Value="Transparent"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource BrOnSurface}"/>
|
||||
<Setter Property="CaretBrush" Value="{DynamicResource BrPrimary}"/>
|
||||
<Setter Property="SelectionBrush" Value="{DynamicResource BrPrimaryContainer}"/>
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource BrOutline}"/>
|
||||
<Setter Property="BorderThickness" Value="1"/>
|
||||
<Setter Property="Height" Value="40"/>
|
||||
<Setter Property="Padding" Value="16,0"/>
|
||||
<Setter Property="VerticalContentAlignment" Value="Center"/>
|
||||
<Setter Property="FontFamily" Value="Roboto, Segoe UI"/>
|
||||
<Setter Property="FontSize" Value="14"/>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="PasswordBox">
|
||||
<Border x:Name="Bd" CornerRadius="4"
|
||||
Background="{TemplateBinding Background}"
|
||||
BorderBrush="{TemplateBinding BorderBrush}"
|
||||
BorderThickness="{TemplateBinding BorderThickness}"
|
||||
Padding="{TemplateBinding Padding}">
|
||||
<ScrollViewer x:Name="PART_ContentHost" VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsFocused" Value="True">
|
||||
<Setter TargetName="Bd" Property="BorderBrush" Value="{DynamicResource BrPrimary}"/>
|
||||
<Setter TargetName="Bd" Property="BorderThickness" Value="2"/>
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<!-- PROGRESS BAR -->
|
||||
<!-- =============================================================
|
||||
PROGRESS BAR (MD3 linear indicator)
|
||||
============================================================= -->
|
||||
<Style x:Key="ModernPb" TargetType="ProgressBar">
|
||||
<Setter Property="Height" Value="4"/>
|
||||
<Setter Property="Background" Value="{DynamicResource BrSurfaceVariant}"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource BrPrimary}"/>
|
||||
<Setter Property="BorderThickness" Value="0"/>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="ProgressBar">
|
||||
<Grid>
|
||||
<Border CornerRadius="2" Background="{TemplateBinding Background}"/>
|
||||
<Border x:Name="PART_Indicator" CornerRadius="2"
|
||||
Background="{TemplateBinding Foreground}"
|
||||
HorizontalAlignment="Left"/>
|
||||
<Border x:Name="PART_Track"/>
|
||||
</Grid>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<!-- =============================================================
|
||||
DATAGRID (MD3 surface tones)
|
||||
============================================================= -->
|
||||
<Style x:Key="ModernDg" TargetType="DataGrid">
|
||||
<Setter Property="Background" Value="{DynamicResource BrSurfaceContainerLow}"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource BrOnSurface}"/>
|
||||
<Setter Property="BorderThickness" Value="0"/>
|
||||
<Setter Property="RowBackground" Value="{DynamicResource BrSurfaceContainerLow}"/>
|
||||
<Setter Property="AlternatingRowBackground" Value="{DynamicResource BrSurfaceContainer}"/>
|
||||
<Setter Property="GridLinesVisibility" Value="Horizontal"/>
|
||||
<Setter Property="HorizontalGridLinesBrush" Value="{DynamicResource BrOutlineVariant}"/>
|
||||
<Setter Property="HeadersVisibility" Value="Column"/>
|
||||
<Setter Property="RowHeaderWidth" Value="0"/>
|
||||
<Setter Property="AutoGenerateColumns" Value="True"/>
|
||||
<Setter Property="IsReadOnly" Value="True"/>
|
||||
<Setter Property="SelectionMode" Value="Single"/>
|
||||
<Setter Property="CanUserAddRows" Value="False"/>
|
||||
<Setter Property="CanUserDeleteRows" Value="False"/>
|
||||
<Setter Property="CanUserResizeRows" Value="False"/>
|
||||
<Setter Property="FontFamily" Value="Roboto, Segoe UI"/>
|
||||
<Setter Property="FontSize" Value="14"/>
|
||||
</Style>
|
||||
|
||||
<Style TargetType="DataGridColumnHeader">
|
||||
<Setter Property="Background" Value="{DynamicResource BrSurfaceContainerHigh}"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource BrOnSurfaceVariant}"/>
|
||||
<Setter Property="FontFamily" Value="Roboto, Segoe UI"/>
|
||||
<Setter Property="FontWeight" Value="Medium"/>
|
||||
<Setter Property="FontSize" Value="12"/>
|
||||
<Setter Property="Padding" Value="16,12"/>
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource BrOutlineVariant}"/>
|
||||
<Setter Property="BorderThickness" Value="0,0,0,1"/>
|
||||
</Style>
|
||||
|
||||
<Style TargetType="DataGridCell">
|
||||
<Setter Property="Padding" Value="16,10"/>
|
||||
<Setter Property="BorderThickness" Value="0"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource BrOnSurface}"/>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="DataGridCell">
|
||||
<Border Padding="{TemplateBinding Padding}" Background="{TemplateBinding Background}">
|
||||
<ContentPresenter VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
<Style.Triggers>
|
||||
<Trigger Property="IsSelected" Value="True">
|
||||
<Setter Property="Background" Value="{DynamicResource BrSecondaryContainer}"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource BrOnSecondaryContainer}"/>
|
||||
</Trigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
|
||||
<Style TargetType="DataGridRow">
|
||||
<Setter Property="Margin" Value="0"/>
|
||||
<Style.Triggers>
|
||||
<Trigger Property="IsSelected" Value="True">
|
||||
<Setter Property="Background" Value="{DynamicResource BrSecondaryContainer}"/>
|
||||
</Trigger>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter Property="Background" Value="{DynamicResource BrRowHover}"/>
|
||||
</Trigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
|
||||
<!-- =============================================================
|
||||
CARD (MD3 � Surface Container, 12px radius, Elevation 1)
|
||||
============================================================= -->
|
||||
<Style x:Key="StatCard" TargetType="Border">
|
||||
<Setter Property="Background" Value="{DynamicResource BrSurfaceContainer}"/>
|
||||
<Setter Property="CornerRadius" Value="12"/>
|
||||
<Setter Property="Padding" Value="16"/>
|
||||
<Setter Property="BorderThickness" Value="0"/>
|
||||
<Setter Property="Effect" Value="{StaticResource Elevation1}"/>
|
||||
</Style>
|
||||
|
||||
<Style x:Key="ApiCard" TargetType="Border">
|
||||
<Setter Property="Background" Value="{DynamicResource BrSurfaceContainerHigh}"/>
|
||||
<Setter Property="CornerRadius" Value="12"/>
|
||||
<Setter Property="Padding" Value="20,16"/>
|
||||
<Setter Property="BorderThickness" Value="0"/>
|
||||
<Setter Property="Effect" Value="{StaticResource Elevation1}"/>
|
||||
<Setter Property="Margin" Value="0,0,0,16"/>
|
||||
</Style>
|
||||
|
||||
<Style x:Key="CardStyle" TargetType="Border">
|
||||
<Setter Property="Background" Value="{DynamicResource BrSurfaceContainer}"/>
|
||||
<Setter Property="CornerRadius" Value="12"/>
|
||||
<Setter Property="Padding" Value="16"/>
|
||||
<Setter Property="BorderThickness" Value="0"/>
|
||||
<Setter Property="Effect" Value="{StaticResource Elevation1}"/>
|
||||
<Setter Property="Margin" Value="0,0,0,8"/>
|
||||
</Style>
|
||||
|
||||
<!-- =============================================================
|
||||
SECTION TITLE (MD3 Title Medium)
|
||||
============================================================= -->
|
||||
<Style x:Key="SectionTitle" TargetType="TextBlock">
|
||||
<Setter Property="FontFamily" Value="Roboto, Segoe UI"/>
|
||||
<Setter Property="FontSize" Value="16"/>
|
||||
<Setter Property="FontWeight" Value="Medium"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource BrOnSurface}"/>
|
||||
<Setter Property="Margin" Value="0,0,0,8"/>
|
||||
</Style>
|
||||
|
||||
<!-- =============================================================
|
||||
SCROLLBAR (MD3 � slim, OutlineVariant thumb)
|
||||
============================================================= -->
|
||||
<Style TargetType="ScrollBar">
|
||||
<Setter Property="Width" Value="8"/>
|
||||
<Setter Property="Background" Value="Transparent"/>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="ScrollBar">
|
||||
<Grid>
|
||||
<Track x:Name="PART_Track" IsDirectionReversed="True">
|
||||
<Track.Thumb>
|
||||
<Thumb>
|
||||
<Thumb.Template>
|
||||
<ControlTemplate TargetType="Thumb">
|
||||
<Border CornerRadius="4" Margin="2"
|
||||
Background="{DynamicResource BrOutlineVariant}"/>
|
||||
</ControlTemplate>
|
||||
</Thumb.Template>
|
||||
</Thumb>
|
||||
</Track.Thumb>
|
||||
</Track>
|
||||
</Grid>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
<Style.Triggers>
|
||||
<Trigger Property="Orientation" Value="Horizontal">
|
||||
<Setter Property="Width" Value="Auto"/>
|
||||
<Setter Property="Height" Value="8"/>
|
||||
</Trigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
|
||||
<!-- =============================================================
|
||||
LISTBOX (transparent background)
|
||||
============================================================= -->
|
||||
<Style TargetType="ListBox">
|
||||
<Setter Property="Background" Value="Transparent"/>
|
||||
<Setter Property="BorderThickness" Value="0"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource BrOnSurface}"/>
|
||||
</Style>
|
||||
|
||||
<!-- =============================================================
|
||||
COMBOBOXITEM - foreground/background from active theme
|
||||
============================================================= -->
|
||||
<Style TargetType="ComboBoxItem">
|
||||
<Setter Property="Background" Value="{DynamicResource BrSurface}"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource BrOnSurface}"/>
|
||||
<Setter Property="Padding" Value="10,6"/>
|
||||
<Setter Property="FontFamily" Value="Roboto, Segoe UI"/>
|
||||
<Setter Property="FontSize" Value="14"/>
|
||||
<Style.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter Property="Background" Value="{DynamicResource BrSurfaceContainerHighest}"/>
|
||||
</Trigger>
|
||||
<Trigger Property="IsSelected" Value="True">
|
||||
<Setter Property="Background" Value="{DynamicResource BrPrimaryContainer}"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource BrOnPrimaryContainer}"/>
|
||||
</Trigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
|
||||
<!-- =============================================================
|
||||
COMBOBOX - themed box and popup
|
||||
============================================================= -->
|
||||
<Style TargetType="ComboBox">
|
||||
<Setter Property="Background" Value="{DynamicResource BrSurface}"/>
|
||||
<Setter Property="Foreground" Value="{DynamicResource BrOnSurface}"/>
|
||||
<Setter Property="BorderBrush" Value="{DynamicResource BrOutline}"/>
|
||||
<Setter Property="BorderThickness" Value="1"/>
|
||||
<Setter Property="Padding" Value="10,6"/>
|
||||
<Setter Property="FontFamily" Value="Roboto, Segoe UI"/>
|
||||
<Setter Property="FontSize" Value="14"/>
|
||||
<Setter Property="Height" Value="40"/>
|
||||
<Setter Property="VerticalContentAlignment" Value="Center"/>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="ComboBox">
|
||||
<Grid>
|
||||
<Border x:Name="Bd"
|
||||
Background="{TemplateBinding Background}"
|
||||
BorderBrush="{TemplateBinding BorderBrush}"
|
||||
BorderThickness="{TemplateBinding BorderThickness}"
|
||||
CornerRadius="4"/>
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="*"/>
|
||||
<ColumnDefinition Width="32"/>
|
||||
</Grid.ColumnDefinitions>
|
||||
<ContentPresenter Grid.Column="0"
|
||||
Content="{TemplateBinding SelectionBoxItem}"
|
||||
ContentTemplate="{TemplateBinding SelectionBoxItemTemplate}"
|
||||
Margin="{TemplateBinding Padding}"
|
||||
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
|
||||
HorizontalAlignment="Left"/>
|
||||
<Path Grid.Column="1"
|
||||
Data="M 0,0 L 4,4 L 8,0"
|
||||
Stroke="{DynamicResource BrOnSurfaceVariant}" StrokeThickness="1.5"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||
<ToggleButton Grid.Column="0" Grid.ColumnSpan="2"
|
||||
IsChecked="{Binding IsDropDownOpen, Mode=TwoWay, RelativeSource={RelativeSource TemplatedParent}}"
|
||||
Background="Transparent" BorderThickness="0" Opacity="0"
|
||||
Focusable="False"/>
|
||||
</Grid>
|
||||
<Popup x:Name="PART_Popup"
|
||||
IsOpen="{TemplateBinding IsDropDownOpen}"
|
||||
AllowsTransparency="True"
|
||||
Placement="Bottom"
|
||||
PlacementTarget="{Binding RelativeSource={RelativeSource TemplatedParent}}"
|
||||
Width="{Binding ActualWidth, RelativeSource={RelativeSource TemplatedParent}}">
|
||||
<Border Background="{DynamicResource BrSurface}"
|
||||
BorderBrush="{DynamicResource BrOutlineVariant}"
|
||||
BorderThickness="1" CornerRadius="4"
|
||||
Margin="0,2,0,0"
|
||||
MaxHeight="300">
|
||||
<Border.Effect>
|
||||
<DropShadowEffect BlurRadius="8" ShadowDepth="2" Opacity="0.15" Color="#000000"/>
|
||||
</Border.Effect>
|
||||
<ScrollViewer VerticalScrollBarVisibility="Auto">
|
||||
<ItemsPresenter/>
|
||||
</ScrollViewer>
|
||||
</Border>
|
||||
</Popup>
|
||||
</Grid>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter TargetName="Bd" Property="BorderBrush" Value="{DynamicResource BrOnSurface}"/>
|
||||
</Trigger>
|
||||
<Trigger Property="IsDropDownOpen" Value="True">
|
||||
<Setter TargetName="Bd" Property="BorderBrush" Value="{DynamicResource BrPrimary}"/>
|
||||
<Setter TargetName="Bd" Property="BorderThickness" Value="2"/>
|
||||
</Trigger>
|
||||
<Trigger Property="IsEnabled" Value="False">
|
||||
<Setter TargetName="Bd" Property="Opacity" Value="0.5"/>
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
</ResourceDictionary>
|
||||
@@ -0,0 +1,49 @@
|
||||
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||
|
||||
<!-- MD3 Light Theme - Color Tokens -->
|
||||
|
||||
<SolidColorBrush x:Key="BrPrimary" Color="#6750A4"/>
|
||||
<SolidColorBrush x:Key="BrOnPrimary" Color="#FFFFFF"/>
|
||||
<SolidColorBrush x:Key="BrPrimaryContainer" Color="#EADDFF"/>
|
||||
<SolidColorBrush x:Key="BrOnPrimaryContainer" Color="#21005D"/>
|
||||
<SolidColorBrush x:Key="BrSecondary" Color="#625B71"/>
|
||||
<SolidColorBrush x:Key="BrOnSecondary" Color="#FFFFFF"/>
|
||||
<SolidColorBrush x:Key="BrSecondaryContainer" Color="#E8DEF8"/>
|
||||
<SolidColorBrush x:Key="BrOnSecondaryContainer" Color="#1D192B"/>
|
||||
<SolidColorBrush x:Key="BrTertiary" Color="#7D5260"/>
|
||||
<SolidColorBrush x:Key="BrTertiaryContainer" Color="#FFD8E4"/>
|
||||
<SolidColorBrush x:Key="BrOnTertiaryContainer" Color="#31111D"/>
|
||||
<SolidColorBrush x:Key="BrError" Color="#B3261E"/>
|
||||
<SolidColorBrush x:Key="BrSurface" Color="#FEF7FF"/>
|
||||
<SolidColorBrush x:Key="BrOnSurface" Color="#1C1B1F"/>
|
||||
<SolidColorBrush x:Key="BrSurfaceVariant" Color="#E7E0EC"/>
|
||||
<SolidColorBrush x:Key="BrOnSurfaceVariant" Color="#49454F"/>
|
||||
<SolidColorBrush x:Key="BrSurfaceContainer" Color="#F3EDF7"/>
|
||||
<SolidColorBrush x:Key="BrSurfaceContainerHigh" Color="#ECE6F0"/>
|
||||
<SolidColorBrush x:Key="BrSurfaceContainerHighest" Color="#E6E0E9"/>
|
||||
<SolidColorBrush x:Key="BrSurfaceContainerLow" Color="#F7F2FA"/>
|
||||
<SolidColorBrush x:Key="BrBackground" Color="#FEF7FF"/>
|
||||
<SolidColorBrush x:Key="BrOutline" Color="#79747E"/>
|
||||
<SolidColorBrush x:Key="BrOutlineVariant" Color="#CAC4D0"/>
|
||||
|
||||
<!-- Legacy aliases -->
|
||||
<SolidColorBrush x:Key="BrBase" Color="#FEF7FF"/>
|
||||
<SolidColorBrush x:Key="BrMantle" Color="#F3EDF7"/>
|
||||
<SolidColorBrush x:Key="BrSurface0" Color="#F7F2FA"/>
|
||||
<SolidColorBrush x:Key="BrSurface1" Color="#F3EDF7"/>
|
||||
<SolidColorBrush x:Key="BrSurface2" Color="#ECE6F0"/>
|
||||
<SolidColorBrush x:Key="BrOverlay0" Color="#49454F"/>
|
||||
<SolidColorBrush x:Key="BrText" Color="#1C1B1F"/>
|
||||
<SolidColorBrush x:Key="BrSubtext0" Color="#49454F"/>
|
||||
<SolidColorBrush x:Key="BrBlue" Color="#6750A4"/>
|
||||
<SolidColorBrush x:Key="BrGreen" Color="#386A20"/>
|
||||
<SolidColorBrush x:Key="BrRed" Color="#B3261E"/>
|
||||
<SolidColorBrush x:Key="BrPeach" Color="#625B71"/>
|
||||
<SolidColorBrush x:Key="BrLavender" Color="#EADDFF"/>
|
||||
<SolidColorBrush x:Key="BrBorder" Color="#CAC4D0"/>
|
||||
|
||||
<!-- Row hover tint (semi-transparent primary) -->
|
||||
<SolidColorBrush x:Key="BrRowHover" Color="#1A6750A4"/>
|
||||
|
||||
</ResourceDictionary>
|
||||
@@ -0,0 +1,33 @@
|
||||
using System;
|
||||
using System.Windows;
|
||||
|
||||
namespace HorseRacingPredictor
|
||||
{
|
||||
/// <summary>
|
||||
/// Swaps the active MD3 theme (light / dark) at runtime by replacing
|
||||
/// the first merged dictionary in Application.Resources with either
|
||||
/// LightTheme.xaml or DarkTheme.xaml.
|
||||
/// </summary>
|
||||
internal static class ThemeManager
|
||||
{
|
||||
private static readonly Uri LightUri = new("/Styles/LightTheme.xaml", UriKind.Relative);
|
||||
private static readonly Uri DarkUri = new("/Styles/DarkTheme.xaml", UriKind.Relative);
|
||||
|
||||
public static bool IsDark { get; private set; }
|
||||
|
||||
/// <summary>Applies the requested theme. Call from the UI thread.</summary>
|
||||
public static void Apply(bool dark)
|
||||
{
|
||||
IsDark = dark;
|
||||
var mergedDicts = Application.Current.Resources.MergedDictionaries;
|
||||
|
||||
// The theme dictionary is always at index 0 (see App.xaml)
|
||||
var themeDict = new ResourceDictionary { Source = dark ? DarkUri : LightUri };
|
||||
|
||||
if (mergedDicts.Count > 0)
|
||||
mergedDicts[0] = themeDict;
|
||||
else
|
||||
mergedDicts.Insert(0, themeDict);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,30 +0,0 @@
|
||||
namespace HorseRacingPredictor.VirtualFootball
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a single virtual football match result displayed in the results panel.
|
||||
/// </summary>
|
||||
public class VirtualMatch
|
||||
{
|
||||
public string Time { get; set; }
|
||||
public string Home { get; set; }
|
||||
public int HomeGoals { get; set; }
|
||||
public int AwayGoals { get; set; }
|
||||
public string Away { get; set; }
|
||||
|
||||
public string Score => $"{HomeGoals} - {AwayGoals}";
|
||||
|
||||
/// <summary>1, X, or 2</summary>
|
||||
public string Outcome
|
||||
{
|
||||
get
|
||||
{
|
||||
if (HomeGoals > AwayGoals) return "1";
|
||||
if (HomeGoals < AwayGoals) return "2";
|
||||
return "X";
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Row background colour: green for draw, red for 1/2.</summary>
|
||||
public string RowColor => Outcome == "X" ? "#2A4A3A" : "#4A2A2A";
|
||||
}
|
||||
}
|
||||
@@ -1,67 +1,154 @@
|
||||
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||
|
||||
<!-- =========================================================
|
||||
CATPPUCCIN MOCHA PALETTE (shared)
|
||||
========================================================= -->
|
||||
<Color x:Key="CBase">#1E1E2E</Color>
|
||||
<Color x:Key="CMantle">#181825</Color>
|
||||
<Color x:Key="CCrust">#11111B</Color>
|
||||
<Color x:Key="CSurface0">#313244</Color>
|
||||
<Color x:Key="CSurface1">#45475A</Color>
|
||||
<Color x:Key="CSurface2">#585B70</Color>
|
||||
<Color x:Key="COverlay0">#6C7086</Color>
|
||||
<Color x:Key="CText">#CDD6F4</Color>
|
||||
<Color x:Key="CSubtext0">#A6ADC8</Color>
|
||||
<Color x:Key="CSubtext1">#BAC2DE</Color>
|
||||
<Color x:Key="CBlue">#89B4FA</Color>
|
||||
<Color x:Key="CGreen">#A6E3A1</Color>
|
||||
<Color x:Key="CRed">#F38BA8</Color>
|
||||
<Color x:Key="CPeach">#FAB387</Color>
|
||||
<Color x:Key="CLavender">#B4BEFE</Color>
|
||||
<!-- =============================================================
|
||||
MATERIAL DESIGN 3 � TOKEN SYSTEM (Light Mode)
|
||||
Reference: https://m3.material.io/styles/color/roles
|
||||
============================================================= -->
|
||||
|
||||
<SolidColorBrush x:Key="BrBase" Color="{StaticResource CBase}"/>
|
||||
<SolidColorBrush x:Key="BrMantle" Color="{StaticResource CMantle}"/>
|
||||
<SolidColorBrush x:Key="BrSurface0" Color="{StaticResource CSurface0}"/>
|
||||
<SolidColorBrush x:Key="BrSurface1" Color="{StaticResource CSurface1}"/>
|
||||
<SolidColorBrush x:Key="BrSurface2" Color="{StaticResource CSurface2}"/>
|
||||
<SolidColorBrush x:Key="BrOverlay0" Color="{StaticResource COverlay0}"/>
|
||||
<SolidColorBrush x:Key="BrText" Color="{StaticResource CText}"/>
|
||||
<SolidColorBrush x:Key="BrSubtext0" Color="{StaticResource CSubtext0}"/>
|
||||
<SolidColorBrush x:Key="BrBlue" Color="{StaticResource CBlue}"/>
|
||||
<SolidColorBrush x:Key="BrGreen" Color="{StaticResource CGreen}"/>
|
||||
<SolidColorBrush x:Key="BrRed" Color="{StaticResource CRed}"/>
|
||||
<SolidColorBrush x:Key="BrPeach" Color="{StaticResource CPeach}"/>
|
||||
<SolidColorBrush x:Key="BrLavender" Color="{StaticResource CLavender}"/>
|
||||
<!-- Primary palette -->
|
||||
<Color x:Key="CPrimary">#6750A4</Color>
|
||||
<Color x:Key="COnPrimary">#FFFFFF</Color>
|
||||
<Color x:Key="CPrimaryContainer">#EADDFF</Color>
|
||||
<Color x:Key="COnPrimaryContainer">#21005D</Color>
|
||||
|
||||
<!-- Acrylic-like background (semi-transparent fallback) -->
|
||||
<SolidColorBrush x:Key="AcrylicBackgroundBrush" Color="#0F000000"/>
|
||||
<!-- Secondary palette -->
|
||||
<Color x:Key="CSecondary">#625B71</Color>
|
||||
<Color x:Key="COnSecondary">#FFFFFF</Color>
|
||||
<Color x:Key="CSecondaryContainer">#E8DEF8</Color>
|
||||
<Color x:Key="COnSecondaryContainer">#1D192B</Color>
|
||||
|
||||
<!-- Subtle shadow effect for elevation -->
|
||||
<DropShadowEffect x:Key="SubtleDropShadow" BlurRadius="12" ShadowDepth="2" Color="#50000000"/>
|
||||
<!-- Tertiary palette -->
|
||||
<Color x:Key="CTertiary">#7D5260</Color>
|
||||
<Color x:Key="COnTertiary">#FFFFFF</Color>
|
||||
<Color x:Key="CTertiaryContainer">#FFD8E4</Color>
|
||||
<Color x:Key="COnTertiaryContainer">#31111D</Color>
|
||||
|
||||
<!-- NAV BUTTON STYLE (icon-only sidebar) -->
|
||||
<!-- Error -->
|
||||
<Color x:Key="CError">#B3261E</Color>
|
||||
<Color x:Key="COnError">#FFFFFF</Color>
|
||||
<Color x:Key="CErrorContainer">#F9DEDC</Color>
|
||||
|
||||
<!-- Surface / Background -->
|
||||
<Color x:Key="CSurface">#FEF7FF</Color>
|
||||
<Color x:Key="COnSurface">#1C1B1F</Color>
|
||||
<Color x:Key="CSurfaceVariant">#E7E0EC</Color>
|
||||
<Color x:Key="COnSurfaceVariant">#49454F</Color>
|
||||
<Color x:Key="CSurfaceContainer">#F3EDF7</Color>
|
||||
<Color x:Key="CSurfaceContainerHigh">#ECE6F0</Color>
|
||||
<Color x:Key="CSurfaceContainerHighest">#E6E0E9</Color>
|
||||
<Color x:Key="CSurfaceContainerLow">#F7F2FA</Color>
|
||||
<Color x:Key="CBackground">#FEF7FF</Color>
|
||||
<Color x:Key="COnBackground">#1C1B1F</Color>
|
||||
|
||||
<!-- Outline -->
|
||||
<Color x:Key="COutline">#79747E</Color>
|
||||
<Color x:Key="COutlineVariant">#CAC4D0</Color>
|
||||
|
||||
<!-- Misc -->
|
||||
<Color x:Key="CInversePrimary">#D0BCFF</Color>
|
||||
<Color x:Key="CShadow">#000000</Color>
|
||||
|
||||
<!-- Legacy color keys (kept for compatibility) -->
|
||||
<Color x:Key="CBase">#FEF7FF</Color>
|
||||
<Color x:Key="CMantle">#F3EDF7</Color>
|
||||
<Color x:Key="CCrust">#ECE6F0</Color>
|
||||
<Color x:Key="CSurface0">#F7F2FA</Color>
|
||||
<Color x:Key="CSurface1">#F3EDF7</Color>
|
||||
<Color x:Key="CSurface2">#ECE6F0</Color>
|
||||
<Color x:Key="COverlay0">#49454F</Color>
|
||||
<Color x:Key="CText">#1C1B1F</Color>
|
||||
<Color x:Key="CSubtext0">#49454F</Color>
|
||||
<Color x:Key="CSubtext1">#625B71</Color>
|
||||
<Color x:Key="CBlue">#6750A4</Color>
|
||||
<Color x:Key="CGreen">#7D5260</Color>
|
||||
<Color x:Key="CRed">#B3261E</Color>
|
||||
<Color x:Key="CPeach">#625B71</Color>
|
||||
<Color x:Key="CLavender">#EADDFF</Color>
|
||||
|
||||
<!-- MD3 Brushes -->
|
||||
<SolidColorBrush x:Key="BrPrimary" Color="{StaticResource CPrimary}"/>
|
||||
<SolidColorBrush x:Key="BrOnPrimary" Color="{StaticResource COnPrimary}"/>
|
||||
<SolidColorBrush x:Key="BrPrimaryContainer" Color="{StaticResource CPrimaryContainer}"/>
|
||||
<SolidColorBrush x:Key="BrOnPrimaryContainer" Color="{StaticResource COnPrimaryContainer}"/>
|
||||
<SolidColorBrush x:Key="BrSecondary" Color="{StaticResource CSecondary}"/>
|
||||
<SolidColorBrush x:Key="BrOnSecondary" Color="{StaticResource COnSecondary}"/>
|
||||
<SolidColorBrush x:Key="BrSecondaryContainer" Color="{StaticResource CSecondaryContainer}"/>
|
||||
<SolidColorBrush x:Key="BrOnSecondaryContainer" Color="{StaticResource COnSecondaryContainer}"/>
|
||||
<SolidColorBrush x:Key="BrTertiary" Color="{StaticResource CTertiary}"/>
|
||||
<SolidColorBrush x:Key="BrTertiaryContainer" Color="{StaticResource CTertiaryContainer}"/>
|
||||
<SolidColorBrush x:Key="BrOnTertiaryContainer" Color="{StaticResource COnTertiaryContainer}"/>
|
||||
<SolidColorBrush x:Key="BrError" Color="{StaticResource CError}"/>
|
||||
<SolidColorBrush x:Key="BrSurface" Color="{StaticResource CSurface}"/>
|
||||
<SolidColorBrush x:Key="BrOnSurface" Color="{StaticResource COnSurface}"/>
|
||||
<SolidColorBrush x:Key="BrSurfaceVariant" Color="{StaticResource CSurfaceVariant}"/>
|
||||
<SolidColorBrush x:Key="BrOnSurfaceVariant" Color="{StaticResource COnSurfaceVariant}"/>
|
||||
<SolidColorBrush x:Key="BrSurfaceContainer" Color="{StaticResource CSurfaceContainer}"/>
|
||||
<SolidColorBrush x:Key="BrSurfaceContainerHigh" Color="{StaticResource CSurfaceContainerHigh}"/>
|
||||
<SolidColorBrush x:Key="BrSurfaceContainerHighest" Color="{StaticResource CSurfaceContainerHighest}"/>
|
||||
<SolidColorBrush x:Key="BrSurfaceContainerLow" Color="{StaticResource CSurfaceContainerLow}"/>
|
||||
<SolidColorBrush x:Key="BrBackground" Color="{StaticResource CBackground}"/>
|
||||
<SolidColorBrush x:Key="BrOutline" Color="{StaticResource COutline}"/>
|
||||
<SolidColorBrush x:Key="BrOutlineVariant" Color="{StaticResource COutlineVariant}"/>
|
||||
|
||||
<!-- Legacy brush aliases used by existing XAML -->
|
||||
<SolidColorBrush x:Key="BrBase" Color="{StaticResource CBackground}"/>
|
||||
<SolidColorBrush x:Key="BrMantle" Color="{StaticResource CSurfaceContainer}"/>
|
||||
<SolidColorBrush x:Key="BrSurface0" Color="{StaticResource CSurfaceContainerLow}"/>
|
||||
<SolidColorBrush x:Key="BrSurface1" Color="{StaticResource CSurfaceContainer}"/>
|
||||
<SolidColorBrush x:Key="BrSurface2" Color="{StaticResource CSurfaceContainerHigh}"/>
|
||||
<SolidColorBrush x:Key="BrOverlay0" Color="{StaticResource COnSurfaceVariant}"/>
|
||||
<SolidColorBrush x:Key="BrText" Color="{StaticResource COnSurface}"/>
|
||||
<SolidColorBrush x:Key="BrSubtext0" Color="{StaticResource COnSurfaceVariant}"/>
|
||||
<SolidColorBrush x:Key="BrBlue" Color="{StaticResource CPrimary}"/>
|
||||
<SolidColorBrush x:Key="BrGreen" Color="{StaticResource CTertiary}"/>
|
||||
<SolidColorBrush x:Key="BrRed" Color="{StaticResource CError}"/>
|
||||
<SolidColorBrush x:Key="BrPeach" Color="{StaticResource CSecondary}"/>
|
||||
<SolidColorBrush x:Key="BrLavender" Color="{StaticResource CPrimaryContainer}"/>
|
||||
<SolidColorBrush x:Key="BrBorder" Color="{StaticResource COutlineVariant}"/>
|
||||
|
||||
<!-- Elevation drop shadows (MD3 tonal elevation complement) -->
|
||||
<DropShadowEffect x:Key="Elevation1" BlurRadius="3" ShadowDepth="1" Opacity="0.12" Color="{StaticResource CShadow}"/>
|
||||
<DropShadowEffect x:Key="Elevation2" BlurRadius="6" ShadowDepth="2" Opacity="0.14" Color="{StaticResource CShadow}"/>
|
||||
<DropShadowEffect x:Key="Elevation3" BlurRadius="12" ShadowDepth="4" Opacity="0.16" Color="{StaticResource CShadow}"/>
|
||||
<DropShadowEffect x:Key="SubtleDropShadow" BlurRadius="6" ShadowDepth="2" Opacity="0.12" Color="{StaticResource CShadow}"/>
|
||||
|
||||
<!-- =============================================================
|
||||
NAVIGATION RAIL BUTTON (MD3 � pill active indicator)
|
||||
============================================================= -->
|
||||
<Style x:Key="NavBtn" TargetType="RadioButton">
|
||||
<Setter Property="Width" Value="48"/>
|
||||
<Setter Property="Height" Value="48"/>
|
||||
<Setter Property="Margin" Value="6,4"/>
|
||||
<Setter Property="Width" Value="80"/>
|
||||
<Setter Property="Height" Value="72"/>
|
||||
<Setter Property="Margin" Value="0,2"/>
|
||||
<Setter Property="Cursor" Value="Hand"/>
|
||||
<Setter Property="Foreground" Value="{StaticResource BrSubtext0}"/>
|
||||
<Setter Property="Foreground" Value="{StaticResource BrOnSurfaceVariant}"/>
|
||||
<Setter Property="Background" Value="Transparent"/>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="RadioButton">
|
||||
<Grid>
|
||||
<Border x:Name="Bg" CornerRadius="10" Background="{TemplateBinding Background}"/>
|
||||
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||
</Grid>
|
||||
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center">
|
||||
<Grid HorizontalAlignment="Center" Width="56" Height="32">
|
||||
<!-- Active pill indicator -->
|
||||
<Border x:Name="Pill" CornerRadius="16"
|
||||
Background="Transparent"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Center"
|
||||
Width="56" Height="32"/>
|
||||
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||
</Grid>
|
||||
<TextBlock x:Name="NavLabel"
|
||||
Text="{TemplateBinding Tag}"
|
||||
FontFamily="Roboto, Segoe UI" FontSize="12" FontWeight="Medium"
|
||||
Foreground="{TemplateBinding Foreground}"
|
||||
HorizontalAlignment="Center" TextAlignment="Center"
|
||||
Margin="0,4,0,0"/>
|
||||
</StackPanel>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter TargetName="Bg" Property="Background" Value="#28283A"/>
|
||||
<Setter TargetName="Pill" Property="Background" Value="#1A6750A4"/>
|
||||
</Trigger>
|
||||
<Trigger Property="IsChecked" Value="True">
|
||||
<Setter TargetName="Bg" Property="Background" Value="{StaticResource BrBlue}"/>
|
||||
<Setter Property="Foreground" Value="#181825"/>
|
||||
<Setter TargetName="Pill" Property="Background" Value="{StaticResource BrSecondaryContainer}"/>
|
||||
<Setter Property="Foreground" Value="{StaticResource BrOnSecondaryContainer}"/>
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
@@ -69,27 +156,68 @@
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<!-- ACCENT BUTTON -->
|
||||
<!-- =============================================================
|
||||
FILLED BUTTON (MD3 � pill shape, height 40px)
|
||||
============================================================= -->
|
||||
<Style x:Key="AccentBtn" TargetType="Button">
|
||||
<Setter Property="Foreground" Value="#181825"/>
|
||||
<Setter Property="FontFamily" Value="Segoe UI Semibold"/>
|
||||
<Setter Property="FontSize" Value="13"/>
|
||||
<Setter Property="Padding" Value="18,7"/>
|
||||
<Setter Property="Foreground" Value="{StaticResource BrOnPrimary}"/>
|
||||
<Setter Property="Background" Value="{StaticResource BrPrimary}"/>
|
||||
<Setter Property="FontFamily" Value="Roboto, Segoe UI"/>
|
||||
<Setter Property="FontWeight" Value="Medium"/>
|
||||
<Setter Property="FontSize" Value="14"/>
|
||||
<Setter Property="Height" Value="40"/>
|
||||
<Setter Property="Padding" Value="24,0"/>
|
||||
<Setter Property="Cursor" Value="Hand"/>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="Button">
|
||||
<Border x:Name="Bd" CornerRadius="8"
|
||||
<Border x:Name="Bd" CornerRadius="100"
|
||||
Background="{TemplateBinding Background}"
|
||||
Padding="{TemplateBinding Padding}"
|
||||
Effect="{StaticResource Elevation1}">
|
||||
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter TargetName="Bd" Property="Opacity" Value="0.92"/>
|
||||
<Setter TargetName="Bd" Property="Effect" Value="{StaticResource Elevation2}"/>
|
||||
</Trigger>
|
||||
<Trigger Property="IsPressed" Value="True">
|
||||
<Setter TargetName="Bd" Property="Opacity" Value="0.88"/>
|
||||
</Trigger>
|
||||
<Trigger Property="IsEnabled" Value="False">
|
||||
<Setter TargetName="Bd" Property="Opacity" Value="0.38"/>
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<!-- Tonal button alias used by some pages -->
|
||||
<Style x:Key="ToolBtn" TargetType="Button">
|
||||
<Setter Property="Foreground" Value="{StaticResource BrOnSecondaryContainer}"/>
|
||||
<Setter Property="Background" Value="{StaticResource BrSecondaryContainer}"/>
|
||||
<Setter Property="FontFamily" Value="Roboto, Segoe UI"/>
|
||||
<Setter Property="FontWeight" Value="Medium"/>
|
||||
<Setter Property="FontSize" Value="14"/>
|
||||
<Setter Property="Height" Value="40"/>
|
||||
<Setter Property="Padding" Value="24,0"/>
|
||||
<Setter Property="Cursor" Value="Hand"/>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="Button">
|
||||
<Border x:Name="Bd" CornerRadius="100"
|
||||
Background="{TemplateBinding Background}"
|
||||
Padding="{TemplateBinding Padding}">
|
||||
<ContentPresenter HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter TargetName="Bd" Property="Opacity" Value="0.85"/>
|
||||
<Setter TargetName="Bd" Property="Opacity" Value="0.92"/>
|
||||
</Trigger>
|
||||
<Trigger Property="IsEnabled" Value="False">
|
||||
<Setter TargetName="Bd" Property="Opacity" Value="0.40"/>
|
||||
<Setter TargetName="Bd" Property="Opacity" Value="0.38"/>
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
@@ -97,29 +225,38 @@
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<!-- FLAT TEXTBOX -->
|
||||
<!-- =============================================================
|
||||
OUTLINED TEXT FIELD (MD3 � 4px corner, 2px focus border)
|
||||
============================================================= -->
|
||||
<Style x:Key="FlatTb" TargetType="TextBox">
|
||||
<Setter Property="Background" Value="{StaticResource BrSurface0}"/>
|
||||
<Setter Property="Foreground" Value="{StaticResource BrText}"/>
|
||||
<Setter Property="CaretBrush" Value="{StaticResource BrText}"/>
|
||||
<Setter Property="BorderBrush" Value="{StaticResource BrSurface1}"/>
|
||||
<Setter Property="Background" Value="Transparent"/>
|
||||
<Setter Property="Foreground" Value="{StaticResource BrOnSurface}"/>
|
||||
<Setter Property="CaretBrush" Value="{StaticResource BrPrimary}"/>
|
||||
<Setter Property="SelectionBrush" Value="{StaticResource BrPrimaryContainer}"/>
|
||||
<Setter Property="BorderBrush" Value="{StaticResource BrOutline}"/>
|
||||
<Setter Property="BorderThickness" Value="1"/>
|
||||
<Setter Property="Padding" Value="10,7"/>
|
||||
<Setter Property="FontSize" Value="13"/>
|
||||
<Setter Property="FontFamily" Value="Segoe UI"/>
|
||||
<Setter Property="Height" Value="40"/>
|
||||
<Setter Property="Padding" Value="16,0"/>
|
||||
<Setter Property="VerticalContentAlignment" Value="Center"/>
|
||||
<Setter Property="FontFamily" Value="Roboto, Segoe UI"/>
|
||||
<Setter Property="FontSize" Value="14"/>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="TextBox">
|
||||
<Border x:Name="Bd" CornerRadius="6"
|
||||
<Border x:Name="Bd" CornerRadius="4"
|
||||
Background="{TemplateBinding Background}"
|
||||
BorderBrush="{TemplateBinding BorderBrush}"
|
||||
BorderThickness="{TemplateBinding BorderThickness}"
|
||||
Padding="{TemplateBinding Padding}">
|
||||
<ScrollViewer x:Name="PART_ContentHost"/>
|
||||
<ScrollViewer x:Name="PART_ContentHost" VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsFocused" Value="True">
|
||||
<Setter TargetName="Bd" Property="BorderBrush" Value="{StaticResource BrBlue}"/>
|
||||
<Setter TargetName="Bd" Property="BorderBrush" Value="{StaticResource BrPrimary}"/>
|
||||
<Setter TargetName="Bd" Property="BorderThickness" Value="2"/>
|
||||
</Trigger>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter TargetName="Bd" Property="BorderBrush" Value="{StaticResource BrOnSurface}"/>
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
@@ -127,29 +264,35 @@
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<!-- PASSWORD BOX -->
|
||||
<!-- =============================================================
|
||||
PASSWORD BOX (MD3 outlined)
|
||||
============================================================= -->
|
||||
<Style x:Key="FlatPb" TargetType="PasswordBox">
|
||||
<Setter Property="Background" Value="{StaticResource BrSurface0}"/>
|
||||
<Setter Property="Foreground" Value="{StaticResource BrText}"/>
|
||||
<Setter Property="CaretBrush" Value="{StaticResource BrText}"/>
|
||||
<Setter Property="BorderBrush" Value="{StaticResource BrSurface1}"/>
|
||||
<Setter Property="Background" Value="Transparent"/>
|
||||
<Setter Property="Foreground" Value="{StaticResource BrOnSurface}"/>
|
||||
<Setter Property="CaretBrush" Value="{StaticResource BrPrimary}"/>
|
||||
<Setter Property="SelectionBrush" Value="{StaticResource BrPrimaryContainer}"/>
|
||||
<Setter Property="BorderBrush" Value="{StaticResource BrOutline}"/>
|
||||
<Setter Property="BorderThickness" Value="1"/>
|
||||
<Setter Property="Padding" Value="10,7"/>
|
||||
<Setter Property="FontSize" Value="13"/>
|
||||
<Setter Property="FontFamily" Value="Segoe UI"/>
|
||||
<Setter Property="Height" Value="40"/>
|
||||
<Setter Property="Padding" Value="16,0"/>
|
||||
<Setter Property="VerticalContentAlignment" Value="Center"/>
|
||||
<Setter Property="FontFamily" Value="Roboto, Segoe UI"/>
|
||||
<Setter Property="FontSize" Value="14"/>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="PasswordBox">
|
||||
<Border x:Name="Bd" CornerRadius="6"
|
||||
<Border x:Name="Bd" CornerRadius="4"
|
||||
Background="{TemplateBinding Background}"
|
||||
BorderBrush="{TemplateBinding BorderBrush}"
|
||||
BorderThickness="{TemplateBinding BorderThickness}"
|
||||
Padding="{TemplateBinding Padding}">
|
||||
<ScrollViewer x:Name="PART_ContentHost"/>
|
||||
<ScrollViewer x:Name="PART_ContentHost" VerticalAlignment="Center"/>
|
||||
</Border>
|
||||
<ControlTemplate.Triggers>
|
||||
<Trigger Property="IsFocused" Value="True">
|
||||
<Setter TargetName="Bd" Property="BorderBrush" Value="{StaticResource BrBlue}"/>
|
||||
<Setter TargetName="Bd" Property="BorderBrush" Value="{StaticResource BrPrimary}"/>
|
||||
<Setter TargetName="Bd" Property="BorderThickness" Value="2"/>
|
||||
</Trigger>
|
||||
</ControlTemplate.Triggers>
|
||||
</ControlTemplate>
|
||||
@@ -158,10 +301,13 @@
|
||||
</Style>
|
||||
|
||||
<!-- PROGRESS BAR -->
|
||||
<!-- =============================================================
|
||||
PROGRESS BAR (MD3 linear indicator)
|
||||
============================================================= -->
|
||||
<Style x:Key="ModernPb" TargetType="ProgressBar">
|
||||
<Setter Property="Height" Value="4"/>
|
||||
<Setter Property="Background" Value="{StaticResource BrSurface0}"/>
|
||||
<Setter Property="Foreground" Value="{StaticResource BrBlue}"/>
|
||||
<Setter Property="Background" Value="{StaticResource BrSurfaceVariant}"/>
|
||||
<Setter Property="Foreground" Value="{StaticResource BrPrimary}"/>
|
||||
<Setter Property="BorderThickness" Value="0"/>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
@@ -178,15 +324,17 @@
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<!-- DATAGRID -->
|
||||
<!-- =============================================================
|
||||
DATAGRID (MD3 surface tones)
|
||||
============================================================= -->
|
||||
<Style x:Key="ModernDg" TargetType="DataGrid">
|
||||
<Setter Property="Background" Value="#282A3A"/>
|
||||
<Setter Property="Foreground" Value="{StaticResource BrText}"/>
|
||||
<Setter Property="Background" Value="{StaticResource BrSurfaceContainerLow}"/>
|
||||
<Setter Property="Foreground" Value="{StaticResource BrOnSurface}"/>
|
||||
<Setter Property="BorderThickness" Value="0"/>
|
||||
<Setter Property="RowBackground" Value="#282A3A"/>
|
||||
<Setter Property="AlternatingRowBackground" Value="#2D2F42"/>
|
||||
<Setter Property="RowBackground" Value="{StaticResource BrSurfaceContainerLow}"/>
|
||||
<Setter Property="AlternatingRowBackground" Value="{StaticResource BrSurfaceContainer}"/>
|
||||
<Setter Property="GridLinesVisibility" Value="Horizontal"/>
|
||||
<Setter Property="HorizontalGridLinesBrush" Value="#37394E"/>
|
||||
<Setter Property="HorizontalGridLinesBrush" Value="{StaticResource BrOutlineVariant}"/>
|
||||
<Setter Property="HeadersVisibility" Value="Column"/>
|
||||
<Setter Property="RowHeaderWidth" Value="0"/>
|
||||
<Setter Property="AutoGenerateColumns" Value="True"/>
|
||||
@@ -195,23 +343,25 @@
|
||||
<Setter Property="CanUserAddRows" Value="False"/>
|
||||
<Setter Property="CanUserDeleteRows" Value="False"/>
|
||||
<Setter Property="CanUserResizeRows" Value="False"/>
|
||||
<Setter Property="FontFamily" Value="Segoe UI"/>
|
||||
<Setter Property="FontSize" Value="13"/>
|
||||
<Setter Property="FontFamily" Value="Roboto, Segoe UI"/>
|
||||
<Setter Property="FontSize" Value="14"/>
|
||||
</Style>
|
||||
|
||||
<Style TargetType="DataGridColumnHeader">
|
||||
<Setter Property="Background" Value="#23243A"/>
|
||||
<Setter Property="Foreground" Value="{StaticResource BrBlue}"/>
|
||||
<Setter Property="FontFamily" Value="Segoe UI Semibold"/>
|
||||
<Setter Property="FontSize" Value="13"/>
|
||||
<Setter Property="Padding" Value="10,8"/>
|
||||
<Setter Property="BorderBrush" Value="#37394E"/>
|
||||
<Setter Property="Background" Value="{StaticResource BrSurfaceContainerHigh}"/>
|
||||
<Setter Property="Foreground" Value="{StaticResource BrOnSurfaceVariant}"/>
|
||||
<Setter Property="FontFamily" Value="Roboto, Segoe UI"/>
|
||||
<Setter Property="FontWeight" Value="Medium"/>
|
||||
<Setter Property="FontSize" Value="12"/>
|
||||
<Setter Property="Padding" Value="16,12"/>
|
||||
<Setter Property="BorderBrush" Value="{StaticResource BrOutlineVariant}"/>
|
||||
<Setter Property="BorderThickness" Value="0,0,0,1"/>
|
||||
</Style>
|
||||
|
||||
<Style TargetType="DataGridCell">
|
||||
<Setter Property="Padding" Value="8,6"/>
|
||||
<Setter Property="Padding" Value="16,10"/>
|
||||
<Setter Property="BorderThickness" Value="0"/>
|
||||
<Setter Property="Foreground" Value="{StaticResource BrOnSurface}"/>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="DataGridCell">
|
||||
@@ -223,8 +373,8 @@
|
||||
</Setter>
|
||||
<Style.Triggers>
|
||||
<Trigger Property="IsSelected" Value="True">
|
||||
<Setter Property="Background" Value="#3C3F58"/>
|
||||
<Setter Property="Foreground" Value="{StaticResource BrBlue}"/>
|
||||
<Setter Property="Background" Value="{StaticResource BrSecondaryContainer}"/>
|
||||
<Setter Property="Foreground" Value="{StaticResource BrOnSecondaryContainer}"/>
|
||||
</Trigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
@@ -233,17 +383,95 @@
|
||||
<Setter Property="Margin" Value="0"/>
|
||||
<Style.Triggers>
|
||||
<Trigger Property="IsSelected" Value="True">
|
||||
<Setter Property="Background" Value="#3C3F58"/>
|
||||
<Setter Property="Background" Value="{StaticResource BrSecondaryContainer}"/>
|
||||
</Trigger>
|
||||
<Trigger Property="IsMouseOver" Value="True">
|
||||
<Setter Property="Background" Value="#1A6750A4"/>
|
||||
</Trigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
|
||||
<!-- Card style for lists / rows -->
|
||||
<!-- =============================================================
|
||||
CARD (MD3 � Surface Container, 12px radius, Elevation 1)
|
||||
============================================================= -->
|
||||
<Style x:Key="StatCard" TargetType="Border">
|
||||
<Setter Property="Background" Value="{StaticResource BrSurfaceContainer}"/>
|
||||
<Setter Property="CornerRadius" Value="12"/>
|
||||
<Setter Property="Padding" Value="16"/>
|
||||
<Setter Property="BorderThickness" Value="0"/>
|
||||
<Setter Property="Effect" Value="{StaticResource Elevation1}"/>
|
||||
</Style>
|
||||
|
||||
<Style x:Key="ApiCard" TargetType="Border">
|
||||
<Setter Property="Background" Value="{StaticResource BrSurfaceContainerHigh}"/>
|
||||
<Setter Property="CornerRadius" Value="12"/>
|
||||
<Setter Property="Padding" Value="20,16"/>
|
||||
<Setter Property="BorderThickness" Value="0"/>
|
||||
<Setter Property="Effect" Value="{StaticResource Elevation1}"/>
|
||||
<Setter Property="Margin" Value="0,0,0,16"/>
|
||||
</Style>
|
||||
|
||||
<Style x:Key="CardStyle" TargetType="Border">
|
||||
<Setter Property="CornerRadius" Value="8"/>
|
||||
<Setter Property="Background" Value="#23232A"/>
|
||||
<Setter Property="Padding" Value="12"/>
|
||||
<Setter Property="Margin" Value="6"/>
|
||||
<Setter Property="Background" Value="{StaticResource BrSurfaceContainer}"/>
|
||||
<Setter Property="CornerRadius" Value="12"/>
|
||||
<Setter Property="Padding" Value="16"/>
|
||||
<Setter Property="BorderThickness" Value="0"/>
|
||||
<Setter Property="Effect" Value="{StaticResource Elevation1}"/>
|
||||
<Setter Property="Margin" Value="0,0,0,8"/>
|
||||
</Style>
|
||||
|
||||
<!-- =============================================================
|
||||
SECTION TITLE (MD3 Title Medium)
|
||||
============================================================= -->
|
||||
<Style x:Key="SectionTitle" TargetType="TextBlock">
|
||||
<Setter Property="FontFamily" Value="Roboto, Segoe UI"/>
|
||||
<Setter Property="FontSize" Value="16"/>
|
||||
<Setter Property="FontWeight" Value="Medium"/>
|
||||
<Setter Property="Foreground" Value="{StaticResource BrOnSurface}"/>
|
||||
<Setter Property="Margin" Value="0,0,0,8"/>
|
||||
</Style>
|
||||
|
||||
<!-- =============================================================
|
||||
SCROLLBAR (MD3 � slim, OutlineVariant thumb)
|
||||
============================================================= -->
|
||||
<Style TargetType="ScrollBar">
|
||||
<Setter Property="Width" Value="8"/>
|
||||
<Setter Property="Background" Value="Transparent"/>
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate TargetType="ScrollBar">
|
||||
<Grid>
|
||||
<Track x:Name="PART_Track" IsDirectionReversed="True">
|
||||
<Track.Thumb>
|
||||
<Thumb>
|
||||
<Thumb.Template>
|
||||
<ControlTemplate TargetType="Thumb">
|
||||
<Border CornerRadius="4" Margin="2"
|
||||
Background="{StaticResource BrOutlineVariant}"/>
|
||||
</ControlTemplate>
|
||||
</Thumb.Template>
|
||||
</Thumb>
|
||||
</Track.Thumb>
|
||||
</Track>
|
||||
</Grid>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
<Style.Triggers>
|
||||
<Trigger Property="Orientation" Value="Horizontal">
|
||||
<Setter Property="Width" Value="Auto"/>
|
||||
<Setter Property="Height" Value="8"/>
|
||||
</Trigger>
|
||||
</Style.Triggers>
|
||||
</Style>
|
||||
|
||||
<!-- =============================================================
|
||||
LISTBOX (transparent background)
|
||||
============================================================= -->
|
||||
<Style TargetType="ListBox">
|
||||
<Setter Property="Background" Value="Transparent"/>
|
||||
<Setter Property="BorderThickness" Value="0"/>
|
||||
<Setter Property="Foreground" Value="{StaticResource BrOnSurface}"/>
|
||||
</Style>
|
||||
|
||||
</ResourceDictionary>
|
||||
|
||||
Reference in New Issue
Block a user