LANCommander/LANCommander.SDK/Providers/ServerConfigurationProvider.cs
Aaron Powell 8cf2139ccf feat: Add Avalonia launcher PoC with cross-platform UI
- Create LANCommander.Launcher.Avalonia project using Avalonia MVVM
- Implement server selection, login, and game library views
- Add library sidebar showing user's games
- Add depot/games list view with game details
- Integrate with LANCommander.Launcher.Services for:
  - DepotService, LibraryService, ImportService, MediaService
  - SDK authentication and connection clients
- Fix async initialization pattern to prevent UI thread deadlocks
- Add ConfigureAwait(false) to ServerConfigurationProvider.RefreshAsync
- Add Microsoft.Extensions.Http package for HttpClient DI

Key features:
- Server discovery and connection
- User authentication with credential persistence
- Library import from server to local SQLite cache
- Game icons and banners via MediaService
- Navigation between library, depot, and game details
2026-01-19 14:24:49 +11:00

112 lines
No EOL
3.6 KiB
C#

#nullable enable
using System;
using System.Collections.Generic;
using System.Net.Http;
using System.Text.Json.Nodes;
using System.Threading;
using System.Threading.Tasks;
using LANCommander.SDK.Extensions;
using LANCommander.SDK.Models;
using Microsoft.Extensions.Configuration;
namespace LANCommander.SDK.Providers;
public interface IServerConfigurationRefresher
{
Task RefreshAsync(CancellationToken cancellationToken = default);
}
public sealed class ServerConfigurationSource : IConfigurationSource
{
public required IConfiguration Configuration { get; set; }
internal ServerConfigurationProvider? Provider { get; set; }
public IConfigurationProvider Build(IConfigurationBuilder builder) => Provider = new ServerConfigurationProvider(this);
}
public sealed class ServerConfigurationProvider : ConfigurationProvider
{
private readonly ServerConfigurationSource _source;
private readonly HttpClient _httpClient;
public ServerConfigurationProvider(ServerConfigurationSource source)
{
var settings = new Settings();
_source = source;
_source.Configuration.Bind(settings);
_httpClient = new HttpClient
{
BaseAddress = settings.Authentication.ServerAddress
};
}
public override void Load() => RefreshAsync().GetAwaiter().GetResult();
/// <summary>
/// Repopulates configuration with values grabbed from the LANCommander server
/// </summary>
/// <param name="cancellationToken"></param>
public async Task RefreshAsync(CancellationToken cancellationToken = default)
{
try
{
var settings = new Settings();
_source.Configuration.Bind(settings);
if (settings.Authentication.ServerAddress is null)
{
return;
}
var request = new HttpRequestMessage(HttpMethod.Get, settings.Authentication.ServerAddress.Join("/api/Settings"));
if (settings.Authentication.Token?.AccessToken is not null)
{
request.Headers.Add("Authorization", $"Bearer {settings.Authentication.Token.AccessToken}");
}
// Use ConfigureAwait(false) to prevent deadlocks when called from UI threads
var response = await _httpClient.SendAsync(request, cancellationToken).ConfigureAwait(false);
response.EnsureSuccessStatusCode();
await using var stream = await response.Content.ReadAsStreamAsync(cancellationToken).ConfigureAwait(false);
var payload = await JsonNode.ParseAsync(stream, cancellationToken: cancellationToken).ConfigureAwait(false) ?? new JsonObject();
var prefix = "";
var data = new Dictionary<string, string?>(StringComparer.OrdinalIgnoreCase);
void Walk(JsonNode? node, string keyPrefix)
{
switch (node)
{
case JsonObject obj:
foreach (var kvp in obj)
Walk(kvp.Value, keyPrefix + kvp.Key + ":");
break;
case JsonArray array:
for (int i = 0; i < array.Count; i++)
Walk(array[i], keyPrefix + i + ":");
break;
default:
data[keyPrefix.TrimEnd(':')] = node?.ToString();
break;
}
}
Walk(payload, prefix);
Data = data;
OnReload();
}
catch (Exception ex)
{
}
}
}