Files
UnrealEngine/Engine/Source/Programs/UnrealToolbox/GeneralSettingsViewModel.cs
2025-05-18 13:04:45 +08:00

259 lines
6.9 KiB
C#

// Copyright Epic Games, Inc. All Rights Reserved.
using System.Diagnostics;
using Avalonia.Controls;
using Avalonia.Threading;
using CommunityToolkit.Mvvm.ComponentModel;
using EpicGames.Core;
using EpicGames.Horde;
using FluentAvalonia.UI.Controls;
using Microsoft.Extensions.DependencyInjection;
namespace UnrealToolbox
{
partial class GeneralSettingsViewModel : ObservableObject, IDisposable
{
record class ConnectionState(string ServerUrl, string ServerStatus)
{
public static ConnectionState NoServerConfigured { get; } = new ConnectionState("No client configured", "Status unavailable");
}
readonly SettingsContext _context;
readonly IHordeClientProvider _hordeClientProvider;
readonly IToolCatalog? _toolCatalog;
[ObservableProperty]
string _serverUrl = String.Empty;
[ObservableProperty]
string _serverStatus = String.Empty;
[ObservableProperty]
bool _isConnecting;
bool _requestRefresh;
BackgroundTask<ConnectionState>? _connectTask;
public ToolCatalogViewModel ToolCatalog { get; }
public GeneralSettingsViewModel()
: this(SettingsContext.Default)
{
}
public GeneralSettingsViewModel(SettingsContext context)
{
_context = context;
_hordeClientProvider = context.ServiceProvider.GetRequiredService<IHordeClientProvider>();
_hordeClientProvider.OnStateChanged += StartRefresh;
// _hordeClientProvider.OnAccessTokenStateChanged += StartRefresh;
_toolCatalog = context.ServiceProvider.GetService<IToolCatalog>();
ToolCatalog = new ToolCatalogViewModel(_toolCatalog);
ServerUrl = HordeOptions.GetDefaultServerUrl()?.ToString() ?? "Unknown";
ServerStatus = "Unknown";
StartRefresh();
}
public async void Dispose()
{
_hordeClientProvider.OnStateChanged -= StartRefresh;
// _hordeClientProvider.OnAccessTokenStateChanged -= StartRefresh;
if (_connectTask != null)
{
await _connectTask.DisposeAsync();
_connectTask = null;
}
}
void RunConnectionTask(Func<CancellationToken, Task<ConnectionState>> innerTask)
{
BackgroundTask<ConnectionState>? prevConnectTask = _connectTask;
_connectTask = BackgroundTask.StartNew<ConnectionState>(async cancellationToken =>
{
if (prevConnectTask != null)
{
try
{
await prevConnectTask.DisposeAsync();
}
catch (OperationCanceledException)
{
// Ignore
}
}
return await innerTask(cancellationToken);
});
_connectTask.Task?.ContinueWith(_ => Dispatcher.UIThread.Post(() => UpdateConnectionState()), TaskScheduler.Default);
ServerStatus = "Connecting...";
IsConnecting = true;
}
void UpdateConnectionState()
{
if (_connectTask != null && (_connectTask.Task?.IsCompleted ?? true))
{
ConnectionState? connectionState = null;
if (_connectTask.Task != null)
{
try
{
_connectTask.Task.TryGetResult(out connectionState);
}
catch (Exception ex)
{
connectionState = new ConnectionState(ServerUrl, $"Unable to connect: {ex.Message}");
}
}
if(connectionState != null)
{
ServerUrl = connectionState.ServerUrl;
ServerStatus = connectionState.ServerStatus;
}
IsConnecting = false;
_ = _connectTask.DisposeAsync().AsTask();
_connectTask = null;
}
}
public void OpenServer()
=> OpenBrowser(ServerUrl);
public void StartRefresh()
{
if (!_requestRefresh)
{
_requestRefresh = true;
Dispatcher.UIThread.Post(() => UpdateRefresh());
}
}
public void CancelRefresh()
=> RunConnectionTask(_ => Task.FromResult<ConnectionState>(new ConnectionState(ServerUrl, "Cancelled")));
void UpdateRefresh()
{
if (_requestRefresh)
{
_requestRefresh = false;
RunConnectionTask(HandleRefreshAsync);
}
}
async Task<ConnectionState> HandleRefreshAsync(CancellationToken cancellationToken)
{
using IHordeClientRef? hordeClientRef = _hordeClientProvider.GetClientRef();
if (hordeClientRef == null)
{
return ConnectionState.NoServerConfigured;
}
IHordeClient client = hordeClientRef.Client;
try
{
bool result = await client.LoginAsync(true, cancellationToken);
if (!result)
{
ToolboxNotificationManager.PostNotification($"Connection Error: {client.ServerUrl.ToString()}", "Login Failed");
return new ConnectionState(client.ServerUrl.ToString(), "Login failed");
}
else if (!client.HasValidAccessToken())
{
ToolboxNotificationManager.PostNotification($"Connection Error: {client.ServerUrl.ToString()}", "Session expired");
return new ConnectionState(client.ServerUrl.ToString(), "Session expired");
}
else
{
_toolCatalog?.RequestUpdate();
return new ConnectionState(client.ServerUrl.ToString(), "Authenticated");
}
}
catch (Exception ex)
{
string message = ex.Message;
message = message.Length > 100 ? message.Substring(0, 100) : message;
// Make some connection errors more friendly
string serverUrl = HordeOptions.GetDefaultServerUrl()?.ToString() ?? "Not Conigured";
if (message.Contains("party did not properly respond", StringComparison.OrdinalIgnoreCase) || message.Contains("actively refused it", StringComparison.OrdinalIgnoreCase))
{
message = $"Unable to reach the Horde Server: {serverUrl}";
}
ToolboxNotificationManager.PostNotification($"Connection Error", $"{message}");
return new ConnectionState(hordeClientRef.Client.ServerUrl.ToString(), $"Connection failed: {ex.Message}");
}
}
public async Task ConfigureServerAsync()
{
string serverUrl = ServerUrl;
TextBox serverUrlTextBox = new TextBox();
serverUrlTextBox.Text = serverUrl;
serverUrlTextBox.SelectionStart = serverUrl.Length;
serverUrlTextBox.SelectionEnd = serverUrl.Length;
ContentDialog dialog = new ContentDialog()
{
Title = "Connect to Server",
Content = serverUrlTextBox,
PrimaryButtonText = "Connect",
CloseButtonText = "Cancel"
};
ContentDialogResult result = await dialog.ShowAsync(_context.SettingsWindow);
if (result == ContentDialogResult.None)
{
return;
}
serverUrl = serverUrlTextBox.Text;
try
{
Uri normalizedServerUrl = new Uri(serverUrl.Trim());
HordeOptions.SetDefaultServerUrl(normalizedServerUrl);
ServerUrl = normalizedServerUrl.ToString();
ServerStatus = "Connecting...";
_hordeClientProvider.Reset(); // Will trigger a call to StartRefresh()
}
catch (Exception ex)
{
ServerStatus = $"Error: {ex.Message}";
RunConnectionTask(_ => Task.FromResult<ConnectionState>(new ConnectionState(ServerUrl, ServerStatus)));
}
}
static void OpenBrowser(string url)
{
if (OperatingSystem.IsWindows())
{
string escapedUrl = url.Replace("&", "^&", StringComparison.Ordinal);
Process.Start(new ProcessStartInfo("cmd", $"/c start {escapedUrl}") { CreateNoWindow = true });
}
else if (OperatingSystem.IsLinux())
{
Process.Start("xdg-open", url);
}
else if (OperatingSystem.IsMacOS())
{
Process.Start("open", url);
}
}
}
}