Add DTFluxRaceResult Module. A Tab to manage RaceResult with a WebConsole interface

This commit is contained in:
2025-07-10 20:20:53 +02:00
parent 03eb1132ef
commit d419681172
8 changed files with 687 additions and 0 deletions

View File

@ -0,0 +1,34 @@
using UnrealBuildTool;
public class DTFluxRaceResult : ModuleRules
{
public DTFluxRaceResult(ReadOnlyTargetRules Target) : base(Target)
{
PCHUsage = ModuleRules.PCHUsageMode.UseExplicitOrSharedPCHs;
PublicDependencyModuleNames.AddRange(
new string[]
{
"Core"
}
);
PrivateDependencyModuleNames.AddRange(
new string[]
{
"CoreUObject",
"Engine",
"Slate",
"SlateCore",
"DTFluxProjectSettings",
"UMG",
"WebBrowser",
"Projects",
"ToolMenus",
"HTTP",
"JsonUtilities",
"Json"
}
);
}
}

View File

@ -0,0 +1,161 @@
#include "DTFluxRaceResultModule.h"
#include "LevelEditor.h"
#include "Widget/SDTFluxRaceResultWidget.h"
#include "Widget/Style/DTFluxRaceResultStyle.h"
#define LOCTEXT_NAMESPACE "FDTFluxRaceResultModule"
DEFINE_LOG_CATEGORY(logDTFluxRaceResult)
FName DTFLUXRACERESULT_API FDTFluxRaceResult::RaceResultTabId = "DTFluxRaceResult";
FText DTFLUXRACERESULT_API FDTFluxRaceResult::RaceResultTabDisplayName = FText::FromString(TEXT("DTFlux RaceResult"));
void DTFLUXRACERESULT_API FDTFluxRaceResult::StartupModule()
{
UE_LOG(logDTFluxRaceResult, Warning, TEXT("DTFluxRaceResult Module Started"))
FDTFluxRaceResultStyle::RegisterStyle();
InitMenuExtension();
RegisterRaceResultTab();
}
#pragma region MenuExtension
void DTFLUXRACERESULT_API FDTFluxRaceResult::InitMenuExtension()
{
UToolMenus::RegisterStartupCallback(
FSimpleMulticastDelegate::FDelegate::CreateRaw(this,
&FDTFluxRaceResult::RegisterMenuExtensions));
}
void DTFLUXRACERESULT_API FDTFluxRaceResult::RegisterMenuExtensions()
{
// Étendre le menu enregistré
if (UToolMenu* DTFluxMenu = UToolMenus::Get()->ExtendMenu("DTFlux.MainMenu"))
{
FToolMenuSection& ToolsSection = DTFluxMenu->FindOrAddSection("Tools");
ToolsSection.AddMenuEntry(
"DTFluxRaceResult",
FText::FromString("RaceResult"),
FText::FromString("Launch DTFlux RaceResult Control Panel"),
FSlateIcon(FDTFluxRaceResultStyle::GetStyleSetName(), "LevelEditor.Tab.IconRaceResult"),
FUIAction(FExecuteAction::CreateRaw(this, &FDTFluxRaceResult::OnButtonClicked))
);
}
}
void FDTFluxRaceResult::CreateSubmenu(UToolMenu* Menu)
{
// Section 2 : Tools
FToolMenuSection& ToolsSection = Menu->FindOrAddSection("Tools");
ToolsSection.Label = FText::FromString("Tools");
ToolsSection.AddMenuEntry(
"DTFluxRaceResult",
FText::FromString("RaceResult"),
FText::FromString("Launch Race Result WebAdmin Console"),
FSlateIcon(FDTFluxRaceResultStyle::GetStyleSetName(), "LevelEditor.Tab.IconRaceResult"),
// Adaptez selon votre icône
FUIAction(FExecuteAction::CreateRaw(this, &FDTFluxRaceResult::OnButtonClicked))
);
}
void DTFLUXRACERESULT_API FDTFluxRaceResult::OnButtonClicked()
{
FGlobalTabmanager::Get()->TryInvokeTab(RaceResultTabId);
UE_LOG(LogTemp, Log, TEXT("Race Result Launched"))
}
#pragma endregion EditorTab
#pragma region
void DTFLUXRACERESULT_API FDTFluxRaceResult::RegisterRaceResultTab()
{
FTabSpawnerEntry& SpawnerEntry =
FGlobalTabmanager::Get()->RegisterNomadTabSpawner(
RaceResultTabId,
FOnSpawnTab::CreateRaw(this, &FDTFluxRaceResult::OnSpawnTab)
)
.SetDisplayName(RaceResultTabDisplayName)
.SetTooltipText(FText::FromString(TEXT("Race Result Control Panel")));
}
TSharedRef<SDockTab> DTFLUXRACERESULT_API FDTFluxRaceResult::OnSpawnTab(const FSpawnTabArgs& SpawnTabArgs)
{
return
SNew(
SDockTab
)
.TabRole(ETabRole::NomadTab)
// .ShouldAutosize(true)
[
SNew(SDTFluxRaceResultWidget)
];
}
#pragma endregion
void DTFLUXRACERESULT_API FDTFluxRaceResult::ShutdownModule()
{
FGlobalTabmanager::Get()->UnregisterNomadTabSpawner(RaceResultTabId);
FDTFluxRaceResultStyle::UnregisterStyle();
}
//
// // Dans votre code de debug
// void DTFLUXRACERESULT_API FDTFluxRaceResult::DebugMenus()
// {
// UToolMenus* ToolMenus = UToolMenus::Get();
// if (ToolMenus)
// {
// TArray<FName> MenuNames;
// ToolMenus->GetAllMenuNames(MenuNames);
//
// UE_LOG(logDTFluxRaceResult, Warning, TEXT("=== ALL AVAILABLE MENUS ==="));
// for (const FName& MenuName : MenuNames)
// {
// UE_LOG(logDTFluxRaceResult, Warning, TEXT("Menu: %s"), *MenuName.ToString());
//
// // Obtenir les sections de chaque menu
// UToolMenu* Menu = ToolMenus->FindMenu(MenuName);
// if (Menu)
// {
// for (const FToolMenuSection& Section : Menu->Sections)
// {
// UE_LOG(logDTFluxRaceResult, Warning, TEXT(" Section: %s"), *Section.Name.ToString());
// }
// }
// }
// UE_LOG(logDTFluxRaceResult, Warning, TEXT("=== END MENU LIST ==="));
// }
// }
// void DTFLUXRACERESULT_API FDTFluxRaceResult::AddMenu(FMenuBarBuilder& MenuBarBuilder)
// {
// MenuBarBuilder.AddPullDownMenu(
// FText::FromString("DTFlux"),
// FText::FromString("DTFlux API Tools"),
// FNewMenuDelegate::CreateRaw(this, &FDTFluxRaceResult::FillMenu)
// );
// }
// void DTFLUXRACERESULT_API FDTFluxRaceResult::FillMenu(FMenuBuilder& MenuBuilder)
// {
// MenuBuilder.AddMenuEntry(
// FText::FromString("RaceResult ControlPanel"),
// FText::FromString("Launch RaceResult Control Panel"),
// FSlateIcon(FDTFluxRaceResultStyle::GetStyleSetName(), "LevelEditor.Tab.IconRaceResult"),
// FExecuteAction::CreateRaw(this, &FDTFluxRaceResult::OnButtonClicked)
// );
// MenuBuilder.EndSection();
// }
#undef LOCTEXT_NAMESPACE
IMPLEMENT_MODULE(FDTFluxRaceResult, DTFluxRaceResult)

View File

@ -0,0 +1,283 @@
// Fill out your copyright notice in the Description page of Project Settings.
#include "Widget/SDTFluxRaceResultWidget.h"
#include "DTFluxRaceResultModule.h"
#include "HttpModule.h"
#include "IWebBrowserCookieManager.h"
#include "IWebBrowserWindow.h"
#include "SlateOptMacros.h"
#include "WebBrowserModule.h"
#include "Interfaces/IHttpRequest.h"
#include "Interfaces/IHttpResponse.h"
BEGIN_SLATE_FUNCTION_BUILD_OPTIMIZATION
void SDTFluxRaceResultWidget::Construct(const FArguments& InArgs)
{
FWebBrowserInitSettings browserInitSettings = FWebBrowserInitSettings();
IWebBrowserModule::Get().CustomInitialize(browserInitSettings);
WindowSettings.InitialURL = TEXT("about:blank");
WindowSettings.BrowserFrameRate = 25;
if (IWebBrowserModule::IsAvailable() && IWebBrowserModule::Get().IsWebModuleAvailable())
{
IWebBrowserSingleton* WebBrowserSingleton = IWebBrowserModule::Get().GetSingleton();
Browser = WebBrowserSingleton->CreateBrowserWindow(WindowSettings);
// Browser->OnLoadUrl().BindRaw(this, &SDTFluxRaceResultWidget::OnLoadOverride);
}
ChildSlot
[
SNew(SBox)
.Padding(5.0f)
[
SNew(SBorder)
.BorderImage(FCoreStyle::Get().GetBrush("ToolPanel.GroupBorder"))
[
SNew(SVerticalBox)
+ SVerticalBox::Slot()
[
SAssignNew(WebBrowser, SWebBrowser, Browser)
.ShowControls(true)
.SupportsTransparency(true)
.OnUrlChanged(this, &SDTFluxRaceResultWidget::OnUrlChanged)
.OnBeforeNavigation(this, &SDTFluxRaceResultWidget::OnBeforeNavigation)
.OnLoadCompleted(FSimpleDelegate::CreateRaw(this, &SDTFluxRaceResultWidget::OnLoadCompleted))
.OnLoadError(FSimpleDelegate::CreateRaw(this, &SDTFluxRaceResultWidget::OnLoadError))
.OnLoadStarted(FSimpleDelegate::CreateRaw(this, &SDTFluxRaceResultWidget::OnLoadStarted))
.ShowErrorMessage(true)
.ShowAddressBar(true)
]
]
]
];
}
void SDTFluxRaceResultWidget::OnCookieSet(bool bSuccess)
{
}
void SDTFluxRaceResultWidget::LoadContentViaHTTP()
{
UE_LOG(logDTFluxRaceResult, Log, TEXT("Loading initial content via HTTP: %s"), *RaceResultUrl);
LoadSpecificURL(RaceResultUrl);
}
void SDTFluxRaceResultWidget::LoadSpecificURL(const FString& Url)
{
UE_LOG(logDTFluxRaceResult, Log, TEXT("Loading via HTTP: %s"), *Url);
// Créer la requête HTTP
TSharedRef<IHttpRequest, ESPMode::ThreadSafe> Request = FHttpModule::Get().CreateRequest();
// Ajouter l'authentification Basic pour TOUTES les requêtes
FString Credentials = Username + TEXT(":") + Password;
FString EncodedCredentials = FBase64::Encode(Credentials);
Request->SetURL(Url);
Request->SetVerb("GET");
Request->SetHeader("Authorization", "Basic " + EncodedCredentials);
Request->SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36");
Request->SetHeader("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8");
Request->SetHeader("Accept-Language", "en-US,en;q=0.5");
Request->SetHeader("Accept-Encoding", "gzip, deflate");
Request->SetHeader("Connection", "keep-alive");
Request->OnProcessRequestComplete().BindRaw(this, &SDTFluxRaceResultWidget::OnHTTPContentLoaded);
Request->ProcessRequest();
UE_LOG(logDTFluxRaceResult, Log, TEXT("HTTP request sent with Basic Auth"));
}
void SDTFluxRaceResultWidget::OnHTTPContentLoaded(TSharedPtr<IHttpRequest> Request,
TSharedPtr<IHttpResponse> HttpResponse, bool bWasSuccessful)
{
if (bWasSuccessful && HttpResponse.IsValid())
{
int32 ResponseCode = HttpResponse->GetResponseCode();
UE_LOG(logDTFluxRaceResult, Log, TEXT("HTTP Response Code: %d"), ResponseCode);
if (ResponseCode == 200)
{
FString Content = HttpResponse->GetContentAsString();
UE_LOG(logDTFluxRaceResult, Log, TEXT("Content loaded successfully, size: %d characters"), Content.Len());
// Traiter le contenu HTML
FString ProcessedContent = ProcessHTMLContent(Content, Request->GetURL());
// Charger le contenu dans le browser via LoadString
if (Browser.IsValid())
{
Browser->LoadString(ProcessedContent, Request->GetURL());
UE_LOG(logDTFluxRaceResult, Log, TEXT("Content loaded into browser"));
}
}
else if (ResponseCode == 401)
{
UE_LOG(logDTFluxRaceResult, Error, TEXT("Authentication failed - 401 Unauthorized. Check your credentials."));
}
else if (ResponseCode == 403)
{
UE_LOG(logDTFluxRaceResult, Error, TEXT("Access forbidden - 403 Forbidden"));
}
else if (ResponseCode == 404)
{
UE_LOG(logDTFluxRaceResult, Error, TEXT("Page not found - 404 Not Found"));
}
else
{
UE_LOG(logDTFluxRaceResult, Error, TEXT("HTTP request failed with code: %d"), ResponseCode);
}
}
else
{
UE_LOG(logDTFluxRaceResult, Error, TEXT("HTTP request failed completely"));
}
}
void SDTFluxRaceResultWidget::OnLoadOverride()
{
}
void SDTFluxRaceResultWidget::OnUrlChanged(const FText& NewUrl)
{
UE_LOG(logDTFluxRaceResult, Log, TEXT("URL changed to: %s"), *NewUrl.ToString());
}
FString SDTFluxRaceResultWidget::ProcessHTMLContent(const FString& Content, const FString& BaseUrl)
{
FString ProcessedContent = Content;
// Extraire le domaine de base
FString BaseDomain = ExtractDomain(BaseUrl);
UE_LOG(logDTFluxRaceResult, Log, TEXT("Processing HTML content, base domain: %s"), *BaseDomain);
// Convertir tous les liens relatifs en liens absolus
ProcessedContent = ProcessedContent.Replace(TEXT("src=\"/"), *FString::Printf(TEXT("src=\"%s/"), *BaseDomain));
ProcessedContent = ProcessedContent.Replace(TEXT("href=\"/"), *FString::Printf(TEXT("href=\"%s/"), *BaseDomain));
ProcessedContent = ProcessedContent.Replace(TEXT("src='/"), *FString::Printf(TEXT("src='%s/"), *BaseDomain));
ProcessedContent = ProcessedContent.Replace(TEXT("href='/"), *FString::Printf(TEXT("href='%s/"), *BaseDomain));
// Gérer les liens relatifs avec ./
ProcessedContent = ProcessedContent.Replace(TEXT("src=\"./"), *FString::Printf(TEXT("src=\"%s/"), *BaseDomain));
ProcessedContent = ProcessedContent.Replace(TEXT("href=\"./"), *FString::Printf(TEXT("href=\"%s/"), *BaseDomain));
// Ajouter une balise base pour aider avec les liens relatifs restants
FString BaseTag = FString::Printf(TEXT("<base href=\"%s/\">"), *BaseDomain);
if (ProcessedContent.Contains(TEXT("<head>")))
{
FString HeadReplacement = TEXT("<head>") + BaseTag;
ProcessedContent = ProcessedContent.Replace(TEXT("<head>"), *HeadReplacement);
}
else if (ProcessedContent.Contains(TEXT("<HEAD>")))
{
FString HeadReplacement = TEXT("<HEAD>") + BaseTag;
ProcessedContent = ProcessedContent.Replace(TEXT("<HEAD>"), *HeadReplacement);
}
UE_LOG(logDTFluxRaceResult, Log, TEXT("HTML content processed"));
return ProcessedContent;
}
FString SDTFluxRaceResultWidget::ExtractDomain(const FString& Url)
{
FString Domain = Url;
// Enlever le protocole
if (Url.StartsWith(TEXT("https://")))
{
Domain = Url.Mid(8);
}
else if (Url.StartsWith(TEXT("http://")))
{
Domain = Url.Mid(7);
}
// Trouver le premier slash (début du path)
int32 SlashIndex;
if (Domain.FindChar(TEXT('/'), SlashIndex))
{
Domain = Url.Left(Url.Len() - (Domain.Len() - SlashIndex));
}
// Enlever le port si présent
FString DomainPart = Domain;
if (Domain.StartsWith(TEXT("https://")))
{
DomainPart = Domain.Mid(8);
}
else if (Domain.StartsWith(TEXT("http://")))
{
DomainPart = Domain.Mid(7);
}
int32 ColonIndex;
if (DomainPart.FindChar(TEXT(':'), ColonIndex))
{
Domain = Domain.Left(Domain.Len() - (DomainPart.Len() - ColonIndex));
}
return Domain;
}
bool SDTFluxRaceResultWidget::OnBeforeNavigation(const FString& Url, const FWebNavigationRequest& Request)
{
UE_LOG(logDTFluxRaceResult, Log, TEXT("Loading via HTTP: %s"), *Url);
TSharedRef<IHttpRequest, ESPMode::ThreadSafe> HTTPRequest = FHttpModule::Get().CreateRequest();
// Authentification pour toutes les requêtes
FString Credentials = Username + TEXT(":") + Password;
FString EncodedCredentials = FBase64::Encode(Credentials);
HTTPRequest->SetURL(RaceResultUrl);
HTTPRequest->SetVerb("GET");
HTTPRequest->SetHeader("Authorization", "Basic " + EncodedCredentials);
HTTPRequest->SetHeader("User-Agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36");
HTTPRequest->SetHeader("Accept", "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8");
HTTPRequest->OnProcessRequestComplete().BindRaw(this, &SDTFluxRaceResultWidget::OnHTTPContentLoaded);
HTTPRequest->ProcessRequest();
return true;
}
void SDTFluxRaceResultWidget::OnLoadCompleted()
{
UE_LOG(logDTFluxRaceResult, Log, TEXT("Load Completed"));
}
void SDTFluxRaceResultWidget::OnLoadStarted()
{
UE_LOG(logDTFluxRaceResult, Log, TEXT("Load Started"));
}
void SDTFluxRaceResultWidget::OnLoadError()
{
UE_LOG(logDTFluxRaceResult, Log, TEXT("Load Error"));
}
void SDTFluxRaceResultWidget::OnBeforeResourceLoad(FString Url, FString ResourceType, FContextRequestHeaders& AdditionalHeaders, const bool AllowUserCredentials)
{
}
void SDTFluxRaceResultWidget::SetupBasicAuth()
{
}
END_SLATE_FUNCTION_BUILD_OPTIMIZATION

View File

@ -0,0 +1,47 @@
// Fill out your copyright notice in the Description page of Project Settings.
#include "Widget/Style/DTFluxRaceResultStyle.h"
#include "Interfaces/IPluginManager.h"
#include "Styling/SlateStyleRegistry.h"
#include "Styling/SlateStyleMacros.h"
#define RootToContentDir Style->RootToContentDir
TSharedPtr<ISlateStyle> FDTFluxRaceResultStyle::StyleSet = nullptr;
void FDTFluxRaceResultStyle::RegisterStyle()
{
if(StyleSet.IsValid()) return;
StyleSet = Create();
FSlateStyleRegistry::RegisterSlateStyle(*StyleSet);
}
void FDTFluxRaceResultStyle::UnregisterStyle()
{
if(StyleSet.IsValid())
{
FSlateStyleRegistry::UnRegisterSlateStyle(*StyleSet);
ensure(StyleSet.IsUnique());
StyleSet.Reset();
}
}
void FDTFluxRaceResultStyle::ReloadTextures()
{
}
TSharedPtr<ISlateStyle> FDTFluxRaceResultStyle::Create()
{
TSharedPtr<FSlateStyleSet> Style = MakeShareable(new FSlateStyleSet("DTFluxRaceResultStyle"));
Style->SetContentRoot(IPluginManager::Get().FindPlugin("DTFluxAPI")->GetBaseDir()/TEXT("Resources"));
Style->Set("LevelEditor.Tab.IconRaceResult", new IMAGE_BRUSH_SVG("DTFluxRaceResult16x16", FVector2d(16)) );
return Style;
}

View File

@ -0,0 +1,33 @@
#pragma once
#include "CoreMinimal.h"
#include "Modules/ModuleManager.h"
DTFLUXRACERESULT_API DECLARE_LOG_CATEGORY_EXTERN(logDTFluxRaceResult, All, All);
class DTFLUXRACERESULT_API FDTFluxRaceResult : public IModuleInterface
{
public:
virtual void StartupModule() override;
virtual void ShutdownModule() override;
#pragma region MenuExtention
void RegisterMenuExtensions();
void InitMenuExtension();
void CreateSubmenu(UToolMenu* Menu);
// void AddMenu(FMenuBarBuilder& MenuBarBuilder);
// void FillMenu(FMenuBuilder& MenuBuilder);
void OnButtonClicked();
#pragma endregion
#pragma region EditorTab
void RegisterRaceResultTab();
TSharedRef<SDockTab> OnSpawnTab(const FSpawnTabArgs& SpawnTabArgs);
private:
// static void DebugMenus();
static FName RaceResultTabId;
static FText RaceResultTabDisplayName;
TSharedPtr<class SDTFluxRaceResultWidget> RaceResultWidget;
#pragma endregion
};

View File

@ -0,0 +1,54 @@
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "SWebBrowser.h"
#include "Widgets/SCompoundWidget.h"
class IHttpResponse;
class IHttpRequest;
/**
*
*/
class DTFLUXRACERESULT_API SDTFluxRaceResultWidget : public SCompoundWidget
{
public:
SLATE_BEGIN_ARGS(SDTFluxRaceResultWidget)
{
}
SLATE_END_ARGS()
/** Constructs this widget with InArgs */
void Construct(const FArguments& InArgs);
void OnCookieSet(bool bSuccess);
void LoadSpecificURL(const FString& Url);
void LoadContentViaHTTP();
private:
TSharedPtr<SWebBrowser> WebBrowser;
TSharedPtr<IWebBrowserWindow> Browser;
TSharedPtr<IWebBrowserAdapter> BrowserAdapter;
FCreateBrowserWindowSettings WindowSettings;
void OnUrlChanged(const FText& NewUrl);
FString ProcessHTMLContent(const FString& Content, const FString& BaseUrl);
FString ExtractDomain(const FString& Url);
void OnHTTPContentLoaded(TSharedPtr<IHttpRequest> Request, TSharedPtr<IHttpResponse> HttpResponse, bool bWasSuccessful);
bool OnBeforeNavigation(const FString& Url, const FWebNavigationRequest& Request);
void OnLoadCompleted();
void OnLoadStarted();
void OnLoadError();
void OnLoadOverride();
void OnBeforeResourceLoad(FString Url, FString ResourceType, FContextRequestHeaders& AdditionalHeaders, const bool AllowUserCredentials);
FString RaceResultUrl = "https://raceresult.tds-france.com";
FString Username = "sporkrono";
FString Password = "Notre 3ème décennie d'action pour le climat";
void SetupBasicAuth();
};

View File

@ -0,0 +1,34 @@
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "Styling/ISlateStyle.h"
/**
*
*/
class DTFLUXRACERESULT_API FDTFluxRaceResultStyle
{
public:
static void RegisterStyle();
static void UnregisterStyle();
static void ReloadTextures();
static const ISlateStyle& Get()
{
return *StyleSet;
}
static const FName& GetStyleSetName()
{
return StyleSet->GetStyleSetName();
}
private:
static TSharedPtr<ISlateStyle> Create();
static TSharedPtr<ISlateStyle> StyleSet;
};