- add: Auth service

This commit is contained in:
2024-12-22 21:38:19 +03:00
parent 4d64c995f1
commit fd347a4fc8
30 changed files with 894 additions and 211 deletions

View File

@@ -1 +0,0 @@
Nebula

View File

@@ -57,4 +57,16 @@
<Setter Property="Padding" Value="8" />
<Setter Property="Background" Value="#00000000" />
</Style>
<Style Selector="ListBox.AccountSelector > ListBoxItem">
<Setter Property="CornerRadius" Value="0" />
<Setter Property="Margin" Value="0,0,0,0" />
<Setter Property="Padding" Value="0" />
<Setter Property="Background" Value="#00000000" />
<Setter Property="Focusable" Value="False" />
</Style>
<Style Selector="ListBox.AccountSelector > ListBoxItem:selected">
<Setter Property="Background" Value="#00000000" />
</Style>
</Styles>

View File

@@ -17,6 +17,6 @@ public static class CurrentConVar
"https://hub.spacestation14.com/api/servers"
]);
public static readonly ConVar AuthServers = ConVar.Build<string[]>("launcher.authServers", [
"https://auth.spacestation14.com/api/auth/authenticate"
"https://auth.spacestation14.com/api/auth"
]);
}

View File

@@ -1,5 +1,6 @@
using System.Collections.Generic;
using System.IO;
using Nebula.Launcher.Models;
using Nebula.Launcher.Utils;
using Robust.LoaderApi;

View File

@@ -0,0 +1,10 @@
using System;
namespace Nebula.Launcher.Models.Auth;
public sealed record AuthenticateRequest(string? Username, Guid? UserId, string Password, string? TfaCode = null)
{
public AuthenticateRequest(string username, string password) : this(username, null, password)
{
}
}

View File

@@ -0,0 +1,5 @@
using System;
namespace Nebula.Launcher.Models.Auth;
public sealed record AuthenticateResponse(string Token, string Username, Guid UserId, DateTimeOffset ExpireTime);

View File

@@ -0,0 +1,15 @@
using System;
namespace Nebula.Launcher.Models.Auth;
public class LoginInfo
{
public Guid UserId { get; set; }
public string Username { get; set; } = default!;
public LoginToken Token { get; set; }
public override string ToString()
{
return $"{Username}/{UserId}";
}
}

View File

@@ -0,0 +1,15 @@
using System;
namespace Nebula.Launcher.Models.Auth;
public readonly struct LoginToken
{
public readonly string Token;
public readonly DateTimeOffset ExpireTime;
public LoginToken(string token, DateTimeOffset expireTime)
{
Token = token;
ExpireTime = expireTime;
}
}

View File

@@ -0,0 +1,12 @@
namespace Nebula.Launcher.Models;
public enum ContentCompressionScheme
{
None = 0,
Deflate = 1,
/// <summary>
/// ZStandard compression. In the future may use SS14 specific dictionary IDs in the frame header.
/// </summary>
ZStd = 2
}

View File

@@ -0,0 +1,20 @@
using System.Collections.Generic;
using Robust.LoaderApi;
namespace Nebula.Launcher.Models;
public sealed class MainArgs : IMainArgs
{
public MainArgs(string[] args, IFileApi fileApi, IRedialApi? redialApi, IEnumerable<ApiMount>? apiMounts)
{
Args = args;
FileApi = fileApi;
RedialApi = redialApi;
ApiMounts = apiMounts;
}
public string[] Args { get; }
public IFileApi FileApi { get; }
public IRedialApi? RedialApi { get; }
public IEnumerable<ApiMount>? ApiMounts { get; }
}

View File

@@ -1,5 +1,5 @@
using System;
namespace Nebula.Launcher.Utils;
namespace Nebula.Launcher.Models;
public record struct RobustManifestInfo(Uri ManifestUri, Uri DownloadUri, string Hash);

View File

@@ -1,3 +1,3 @@
namespace Nebula.Launcher.Utils;
namespace Nebula.Launcher.Models;
public record struct RobustManifestItem(string Hash, string Path, int Id);

View File

@@ -4,9 +4,9 @@ using System.Text.Json.Serialization;
namespace Nebula.Launcher.Models;
public sealed record Auth(string Mode, string PublicKey);
public sealed record AuthInfo(string Mode, string PublicKey);
public sealed record Build(
public sealed record BuildInfo(
string EngineVersion,
string ForkId,
string Version,
@@ -16,38 +16,16 @@ public sealed record Build(
string Hash,
string ManifestHash);
public sealed record Link(string Name, string Icon, string Url);
public sealed record Info(string ConnectAddress, Auth Auth, Build Build, string Desc, List<Link> Links);
public sealed record ServerLink(string Name, string Icon, string Url);
public sealed record ServerInfo(string ConnectAddress, AuthInfo Auth, BuildInfo Build, string Desc, List<ServerLink> Links);
public sealed record Status(
string Name,
int Players,
List<object> Tags,
string Map,
int RoundId,
int SoftMaxPlayer,
bool PanicBunker,
int RunLevel,
string Preset);
public enum ContentCompressionScheme
{
None = 0,
Deflate = 1,
/// <summary>
/// ZStandard compression. In the future may use SS14 specific dictionary IDs in the frame header.
/// </summary>
ZStd = 2
}
public sealed record VersionInfo(
public sealed record EngineVersionInfo(
bool Insecure,
[property: JsonPropertyName("redirect")]
string? RedirectVersion,
Dictionary<string, BuildInfo> Platforms);
Dictionary<string, EngineBuildInfo> Platforms);
public sealed class BuildInfo
public sealed class EngineBuildInfo
{
[JsonInclude] [JsonPropertyName("sha256")]
public string Sha256 = default!;
@@ -59,9 +37,9 @@ public sealed class BuildInfo
public string Url = default!;
}
public sealed record ServerInfo(string Address, StatusData StatusData, List<string> InferredTags);
public sealed record ServerHubInfo(string Address, ServerStatus StatusData, List<string> InferredTags);
public sealed record StatusData(
public sealed record ServerStatus(
string Map,
string Name,
List<string> Tags,
@@ -70,11 +48,11 @@ public sealed record StatusData(
int RoundId,
int RunLevel,
bool PanicBunker,
DateTime RoundStartTime,
int SoftMaxPlayer);
DateTime? RoundStartTime,
int SoftMaxPlayers);
public sealed record ModulesInfo(Dictionary<string, Module> Modules);
public sealed record Module(Dictionary<string, ModuleVersionInfo> Versions);
public sealed record ModuleVersionInfo(Dictionary<string, BuildInfo> Platforms);
public sealed record ModuleVersionInfo(Dictionary<string, EngineBuildInfo> Platforms);

View File

@@ -6,6 +6,7 @@
<BuiltInComInteropSupport>true</BuiltInComInteropSupport>
<ApplicationManifest>app.manifest</ApplicationManifest>
<AvaloniaUseCompiledBindingsByDefault>true</AvaloniaUseCompiledBindingsByDefault>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
<ItemGroup>
@@ -24,14 +25,12 @@
<PrivateAssets Condition="'$(Configuration)' != 'Debug'">All</PrivateAssets>
</PackageReference>
<PackageReference Include="CommunityToolkit.Mvvm" Version="8.2.1" />
<PackageReference Include="libsodium" Version="1.0.20" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="9.0.0" />
<PackageReference Include="SharpZstd.Interop" Version="1.5.6" />
</ItemGroup>
<ItemGroup>
<Compile Update="Views\Controls\ServerContainerControl.axaml.cs">
<DependentUpon>ServerContainerControl.axaml</DependentUpon>
<SubType>Code</SubType>
</Compile>
<Compile Update="Views\Tabs\ServerListTab.axaml.cs">
<DependentUpon>ServerListTab.axaml</DependentUpon>
<SubType>Code</SubType>

View File

@@ -44,7 +44,7 @@ public static class ServiceCollectionExtensions
foreach (var (viewModel, view) in GetTypesWithHelpAttribute(Assembly.GetExecutingAssembly()))
{
services.AddTransient(viewModel);
services.AddSingleton(viewModel);
services.AddTransient(view);
}
@@ -60,14 +60,6 @@ public static class ServiceCollectionExtensions
}
}
}
private static void AddView<TView, TViewModel>(this IServiceCollection services)
where TView : class
where TViewModel : class
{
services.AddTransient<TViewModel>();
services.AddTransient<TView>();
}
private static IEnumerable<(Type,Type)> GetTypesWithHelpAttribute(Assembly assembly) {
foreach(Type type in assembly.GetTypes())

View File

@@ -0,0 +1,65 @@
using System;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Input;
using Nebula.Launcher.Models.Auth;
namespace Nebula.Launcher.Services;
[ServiceRegister]
public class AuthService
{
private readonly HttpClient _httpClient = new();
private readonly RestService _restService;
private readonly DebugService _debugService;
public CurrentAuthInfo? SelectedAuth;
public AuthService(RestService restService, DebugService debugService)
{
_restService = restService;
_debugService = debugService;
}
public async Task<bool> Auth(AuthLoginPassword authLoginPassword)
{
var authServer = authLoginPassword.AuthServer;
var login = authLoginPassword.Login;
var password = authLoginPassword.Password;
_debugService.Debug($"Auth to {authServer}/authenticate {login}");
var authUrl = new Uri($"{authServer}/authenticate");
var result =
await _restService.PostAsync<AuthenticateResponse, AuthenticateRequest>(
new AuthenticateRequest(login, password), authUrl, CancellationToken.None);
_debugService.Debug("RESULT " + result.Value);
if (result.Value is null) return false;
SelectedAuth = new CurrentAuthInfo(result.Value.UserId, result.Value.Username,
new LoginToken(result.Value.Token, result.Value.ExpireTime), authServer);
return true;
}
public async Task<bool> EnsureToken()
{
if (SelectedAuth is null) return false;
var authUrl = new Uri($"{SelectedAuth.AuthServer}/ping");
using var requestMessage = new HttpRequestMessage(HttpMethod.Get, authUrl);
requestMessage.Headers.Authorization = new AuthenticationHeaderValue("SS14Auth", SelectedAuth.Token.Token);
using var resp = await _httpClient.SendAsync(requestMessage);
if (!resp.IsSuccessStatusCode) SelectedAuth = null;
return resp.IsSuccessStatusCode;
}
}
public sealed record CurrentAuthInfo(Guid UserId, string Username, LoginToken Token, string AuthServer);
public record AuthLoginPassword(string Login, string Password, string AuthServer);

View File

@@ -54,7 +54,7 @@ public class ConfigurationService
return value != null;
}
public void SetValue(ConVar conVar, object value)
public void SetConfigValue(ConVar conVar, object value)
{
if(conVar.Type != value.GetType())
return;

View File

@@ -5,6 +5,7 @@ using System.IO.Compression;
using System.Runtime.InteropServices;
using Nebula.Launcher.FileApis;
using Nebula.Launcher.FileApis.Interfaces;
using Nebula.Launcher.Models;
using Nebula.Launcher.Utils;
using Robust.LoaderApi;

View File

@@ -16,7 +16,7 @@ public class HubService
public readonly ObservableCollection<string> HubList = new();
private readonly Dictionary<string, List<ServerInfo>> _servers = new();
private readonly Dictionary<string, List<ServerHubInfo>> _servers = new();
public HubService(ConfigurationService configurationService, RestService restService)
@@ -37,7 +37,7 @@ public class HubService
foreach (var hubUri in e.NewItems)
{
var urlStr = (string)hubUri;
var servers = await _restService.GetAsyncDefault<List<ServerInfo>>(new Uri(urlStr), [], CancellationToken.None);
var servers = await _restService.GetAsyncDefault<List<ServerHubInfo>>(new Uri(urlStr), [], CancellationToken.None);
_servers[urlStr] = servers;
HubServerChangedEventArgs?.Invoke(new HubServerChangedEventArgs(servers, HubServerChangeAction.Add));
}
@@ -61,9 +61,9 @@ public class HubService
public class HubServerChangedEventArgs : EventArgs
{
public HubServerChangeAction Action;
public List<ServerInfo> Items;
public List<ServerHubInfo> Items;
public HubServerChangedEventArgs(List<ServerInfo> items, HubServerChangeAction action)
public HubServerChangedEventArgs(List<ServerHubInfo> items, HubServerChangeAction action)
{
Items = items;
Action = action;

View File

@@ -1,6 +1,7 @@
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Text;
using Nebula.Launcher.Models;
namespace Nebula.Launcher.Utils;

View File

@@ -0,0 +1,494 @@
using System;
using System.Buffers;
using System.IO;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Threading;
using System.Threading.Tasks;
using SharpZstd.Interop;
using static SharpZstd.Interop.Zstd;
namespace Nebula.Launcher.Utils;
public static class ZStd
{
public static int CompressBound(int length)
{
return (int)ZSTD_compressBound((nuint)length);
}
[ModuleInitializer]
public static void InitZStd()
{
return;
NativeLibrary.SetDllImportResolver(
typeof(Zstd).Assembly,
ResolveZstd
);
}
private static IntPtr ResolveZstd(string name, Assembly assembly, DllImportSearchPath? path)
{
if (name == "zstd" && OperatingSystem.IsLinux())
{
if (NativeLibrary.TryLoad("zstd.so", assembly, path, out var handle))
return handle;
// Try some extra paths too worst case.
if (NativeLibrary.TryLoad("libzstd.so.1", assembly, path, out handle))
return handle;
if (NativeLibrary.TryLoad("libzstd.so", assembly, path, out handle))
return handle;
}
return IntPtr.Zero;
}
}
public sealed unsafe class ZStdCCtx : IDisposable
{
public ZStdCCtx()
{
Context = ZSTD_createCCtx();
}
public ZSTD_CCtx* Context { get; private set; }
private bool Disposed => Context == null;
public void Dispose()
{
if (Disposed)
return;
ZSTD_freeCCtx(Context);
Context = null;
GC.SuppressFinalize(this);
}
public void SetParameter(ZSTD_cParameter parameter, int value)
{
CheckDisposed();
ZSTD_CCtx_setParameter(Context, parameter, value);
}
public int Compress(Span<byte> destination, Span<byte> source, int compressionLevel = ZSTD_CLEVEL_DEFAULT)
{
CheckDisposed();
fixed (byte* dst = destination)
fixed (byte* src = source)
{
var ret = ZSTD_compressCCtx(
Context,
dst, (nuint)destination.Length,
src, (nuint)source.Length,
compressionLevel);
ZStdException.ThrowIfError(ret);
return (int)ret;
}
}
~ZStdCCtx()
{
Dispose();
}
private void CheckDisposed()
{
if (Disposed)
throw new ObjectDisposedException(nameof(ZStdCCtx));
}
}
public sealed unsafe class ZStdDCtx : IDisposable
{
public ZStdDCtx()
{
Context = ZSTD_createDCtx();
}
public ZSTD_DCtx* Context { get; private set; }
private bool Disposed => Context == null;
public void Dispose()
{
if (Disposed)
return;
ZSTD_freeDCtx(Context);
Context = null;
GC.SuppressFinalize(this);
}
public void SetParameter(ZSTD_dParameter parameter, int value)
{
CheckDisposed();
ZSTD_DCtx_setParameter(Context, parameter, value);
}
public int Decompress(Span<byte> destination, Span<byte> source)
{
CheckDisposed();
fixed (byte* dst = destination)
fixed (byte* src = source)
{
var ret = ZSTD_decompressDCtx(Context, dst, (nuint)destination.Length, src, (nuint)source.Length);
ZStdException.ThrowIfError(ret);
return (int)ret;
}
}
~ZStdDCtx()
{
Dispose();
}
private void CheckDisposed()
{
if (Disposed)
throw new ObjectDisposedException(nameof(ZStdDCtx));
}
}
[Serializable]
public class ZStdException : Exception
{
public ZStdException()
{
}
public ZStdException(string message) : base(message)
{
}
public ZStdException(string message, Exception inner) : base(message, inner)
{
}
public static unsafe ZStdException FromCode(nuint code)
{
return new ZStdException(Marshal.PtrToStringUTF8((IntPtr)ZSTD_getErrorName(code))!);
}
public static void ThrowIfError(nuint code)
{
if (ZSTD_isError(code) != 0)
throw FromCode(code);
}
}
public sealed class ZStdDecompressStream : Stream
{
private readonly Stream _baseStream;
private readonly byte[] _buffer;
private readonly unsafe ZSTD_DCtx* _ctx;
private readonly bool _ownStream;
private int _bufferPos;
private int _bufferSize;
private bool _disposed;
public unsafe ZStdDecompressStream(Stream baseStream, bool ownStream = true)
{
_baseStream = baseStream;
_ownStream = ownStream;
_ctx = ZSTD_createDCtx();
_buffer = ArrayPool<byte>.Shared.Rent((int)ZSTD_DStreamInSize());
}
public override bool CanRead => true;
public override bool CanSeek => false;
public override bool CanWrite => false;
public override long Length => throw new NotSupportedException();
public override long Position
{
get => throw new NotSupportedException();
set => throw new NotSupportedException();
}
protected override unsafe void Dispose(bool disposing)
{
if (_disposed)
return;
_disposed = true;
ZSTD_freeDCtx(_ctx);
if (disposing)
{
if (_ownStream)
_baseStream.Dispose();
ArrayPool<byte>.Shared.Return(_buffer);
}
}
public override void Flush()
{
ThrowIfDisposed();
_baseStream.Flush();
}
public override int Read(byte[] buffer, int offset, int count)
{
return Read(buffer.AsSpan(offset, count));
}
public override int ReadByte()
{
Span<byte> buf = stackalloc byte[1];
return Read(buf) == 0 ? -1 : buf[0];
}
public override unsafe int Read(Span<byte> buffer)
{
ThrowIfDisposed();
do
{
if (_bufferSize == 0 || _bufferPos == _bufferSize)
{
_bufferPos = 0;
_bufferSize = _baseStream.Read(_buffer);
if (_bufferSize == 0)
return 0;
}
fixed (byte* inputPtr = _buffer)
fixed (byte* outputPtr = buffer)
{
var outputBuf = new ZSTD_outBuffer { dst = outputPtr, pos = 0, size = (nuint)buffer.Length };
var inputBuf = new ZSTD_inBuffer { src = inputPtr, pos = (nuint)_bufferPos, size = (nuint)_bufferSize };
var ret = ZSTD_decompressStream(_ctx, &outputBuf, &inputBuf);
_bufferPos = (int)inputBuf.pos;
ZStdException.ThrowIfError(ret);
if (outputBuf.pos > 0)
return (int)outputBuf.pos;
}
} while (true);
}
public override async ValueTask<int> ReadAsync(
Memory<byte> buffer,
CancellationToken cancellationToken = default)
{
ThrowIfDisposed();
do
{
if (_bufferSize == 0 || _bufferPos == _bufferSize)
{
_bufferPos = 0;
_bufferSize = await _baseStream.ReadAsync(_buffer, cancellationToken);
if (_bufferSize == 0)
return 0;
}
var ret = DecompressChunk(this, buffer.Span);
if (ret > 0)
return (int)ret;
} while (true);
static unsafe nuint DecompressChunk(ZStdDecompressStream stream, Span<byte> buffer)
{
fixed (byte* inputPtr = stream._buffer)
fixed (byte* outputPtr = buffer)
{
ZSTD_outBuffer outputBuf = default;
outputBuf.dst = outputPtr;
outputBuf.pos = 0;
outputBuf.size = (nuint)buffer.Length;
ZSTD_inBuffer inputBuf = default;
inputBuf.src = inputPtr;
inputBuf.pos = (nuint)stream._bufferPos;
inputBuf.size = (nuint)stream._bufferSize;
var ret = ZSTD_decompressStream(stream._ctx, &outputBuf, &inputBuf);
stream._bufferPos = (int)inputBuf.pos;
ZStdException.ThrowIfError(ret);
return outputBuf.pos;
}
}
}
public override long Seek(long offset, SeekOrigin origin)
{
throw new NotSupportedException();
}
public override void SetLength(long value)
{
throw new NotSupportedException();
}
public override void Write(byte[] buffer, int offset, int count)
{
throw new NotSupportedException();
}
private void ThrowIfDisposed()
{
if (_disposed)
throw new ObjectDisposedException(nameof(ZStdDecompressStream));
}
}
public sealed class ZStdCompressStream : Stream
{
private readonly Stream _baseStream;
private readonly byte[] _buffer;
private readonly unsafe ZSTD_CCtx* _ctx;
private readonly bool _ownStream;
private int _bufferPos;
private bool _disposed;
public unsafe ZStdCompressStream(Stream baseStream, bool ownStream = true)
{
_ctx = ZSTD_createCCtx();
_baseStream = baseStream;
_ownStream = ownStream;
_buffer = ArrayPool<byte>.Shared.Rent((int)ZSTD_CStreamOutSize());
}
public override bool CanRead => false;
public override bool CanSeek => false;
public override bool CanWrite => true;
public override long Length => throw new NotSupportedException();
public override long Position
{
get => throw new NotSupportedException();
set => throw new NotSupportedException();
}
public override void Flush()
{
FlushInternal(ZSTD_EndDirective.ZSTD_e_flush);
}
public void FlushEnd()
{
FlushInternal(ZSTD_EndDirective.ZSTD_e_end);
}
private unsafe void FlushInternal(ZSTD_EndDirective directive)
{
fixed (byte* outPtr = _buffer)
{
ZSTD_outBuffer outBuf = default;
outBuf.size = (nuint)_buffer.Length;
outBuf.pos = (nuint)_bufferPos;
outBuf.dst = outPtr;
ZSTD_inBuffer inBuf = default;
while (true)
{
var err = ZSTD_compressStream2(_ctx, &outBuf, &inBuf, directive);
ZStdException.ThrowIfError(err);
_bufferPos = (int)outBuf.pos;
_baseStream.Write(_buffer.AsSpan(0, (int)outBuf.pos));
_bufferPos = 0;
outBuf.pos = 0;
if (err == 0)
break;
}
}
_baseStream.Flush();
}
public override int Read(byte[] buffer, int offset, int count)
{
throw new NotSupportedException();
}
public override long Seek(long offset, SeekOrigin origin)
{
throw new NotSupportedException();
}
public override void SetLength(long value)
{
throw new NotSupportedException();
}
public override void Write(byte[] buffer, int offset, int count)
{
Write(buffer.AsSpan(offset, count));
}
public override unsafe void Write(ReadOnlySpan<byte> buffer)
{
ThrowIfDisposed();
fixed (byte* outPtr = _buffer)
fixed (byte* inPtr = buffer)
{
ZSTD_outBuffer outBuf = default;
outBuf.size = (nuint)_buffer.Length;
outBuf.pos = (nuint)_bufferPos;
outBuf.dst = outPtr;
ZSTD_inBuffer inBuf = default;
inBuf.pos = 0;
inBuf.size = (nuint)buffer.Length;
inBuf.src = inPtr;
while (true)
{
var err = ZSTD_compressStream2(_ctx, &outBuf, &inBuf, ZSTD_EndDirective.ZSTD_e_continue);
ZStdException.ThrowIfError(err);
_bufferPos = (int)outBuf.pos;
if (inBuf.pos >= inBuf.size)
break;
// Not all input data consumed. Flush output buffer and continue.
_baseStream.Write(_buffer.AsSpan(0, (int)outBuf.pos));
_bufferPos = 0;
outBuf.pos = 0;
}
}
}
protected override unsafe void Dispose(bool disposing)
{
base.Dispose(disposing);
if (_disposed)
return;
_disposed = true;
ZSTD_freeCCtx(_ctx);
if (disposing)
{
if (_ownStream)
_baseStream.Dispose();
ArrayPool<byte>.Shared.Return(_buffer);
}
}
private void ThrowIfDisposed()
{
if (_disposed)
throw new ObjectDisposedException(nameof(ZStdCompressStream));
}
}

View File

@@ -1,13 +1,117 @@
using System;
using System.Collections.ObjectModel;
using System.Windows.Input;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using Nebula.Launcher.Services;
using Nebula.Launcher.ViewHelper;
using Nebula.Launcher.Views.Pages;
namespace Nebula.Launcher.ViewModels;
[ViewRegister(typeof(AccountInfoView))]
public class AccountInfoViewModel : ViewModelBase
public partial class AccountInfoViewModel : ViewModelBase
{
public AccountInfoViewModel(IServiceProvider serviceProvider) : base(serviceProvider)
private readonly AuthService _authService;
public ObservableCollection<AuthLoginPasswordModel> Accounts { get; } = new();
[ObservableProperty]
private string _currentLogin = String.Empty;
[ObservableProperty]
private string _currentPassword = String.Empty;
[ObservableProperty]
private string _currentAuthServer = String.Empty;
[ObservableProperty] private bool _pageEnabled = true;
public AuthLoginPassword CurrentALP => new AuthLoginPassword(CurrentLogin, CurrentPassword, CurrentAuthServer);
//Design think
public AccountInfoViewModel()
{
AddAccount(new AuthLoginPassword("Binka","12341",""));
}
//Real think
public AccountInfoViewModel(IServiceProvider serviceProvider, AuthService authService) : base(serviceProvider)
{
_authService = authService;
}
public void AuthByALP(AuthLoginPassword authLoginPassword)
{
CurrentLogin = authLoginPassword.Login;
CurrentPassword = authLoginPassword.Password;
CurrentAuthServer = authLoginPassword.AuthServer;
DoAuth();
}
public async void DoAuth()
{
PageEnabled = false;
if(await _authService.Auth(CurrentALP))
Console.WriteLine("Hello, " + _authService.SelectedAuth!.Username);
else
Console.WriteLine("Shit!");
PageEnabled = true;
}
private void AddAccount(AuthLoginPassword authLoginPassword)
{
var onDelete = new DelegateCommand<AuthLoginPasswordModel>(a => Accounts.Remove(a));
var onSelect = new DelegateCommand<AuthLoginPasswordModel>(AuthByALP);
var alpm = new AuthLoginPasswordModel(
authLoginPassword.Login,
authLoginPassword.Password,
authLoginPassword.AuthServer,
onSelect,
onDelete);
onDelete.TRef.Value = alpm;
onSelect.TRef.Value = alpm;
Accounts.Add(alpm);
}
[RelayCommand]
public void OnSaveProfile()
{
AddAccount(CurrentALP);
}
}
internal class Ref<T>
{
public T Value = default!;
}
internal class DelegateCommand<T> : ICommand
{
private readonly Action<T> _func;
public readonly Ref<T> TRef = new();
public DelegateCommand(Action<T> func)
{
_func = func;
}
public bool CanExecute(object? parameter)
{
return true;
}
public void Execute(object? parameter)
{
_func(TRef.Value);
}
public event EventHandler? CanExecuteChanged;
}
public record AuthLoginPasswordModel(string Login, string Password, string AuthServer, ICommand OnSelect = default!, ICommand OnDelete = default!)
: AuthLoginPassword(Login, Password, AuthServer);

View File

@@ -11,18 +11,22 @@ namespace Nebula.Launcher.ViewModels;
[ViewRegister(typeof(ServerListView))]
public partial class ServerListViewModel : ViewModelBase
{
public ObservableCollection<ServerInfo> ServerInfos { get; }
public ObservableCollection<ServerHubInfo> ServerInfos { get; }
[ObservableProperty]
private ServerInfo? _selectedListItem;
private ServerHubInfo? _selectedListItem;
//Design think
public ServerListViewModel()
{
ServerInfos = new ObservableCollection<ServerInfo>();
ServerInfos = new ObservableCollection<ServerHubInfo>();
ServerInfos.Add(new ServerHubInfo("ss14://localhost",new ServerStatus("","TestCraft", [], "super", 12,55,1,false,DateTime.Now, 20),[]));
}
//real think
public ServerListViewModel(IServiceProvider serviceProvider, HubService hubService) : base(serviceProvider)
{
ServerInfos = new ObservableCollection<ServerInfo>();
ServerInfos = new ObservableCollection<ServerHubInfo>();
hubService.HubServerChangedEventArgs += HubServerChangedEventArgs;
}

View File

@@ -1,37 +0,0 @@
<UserControl
d:DesignHeight="450"
d:DesignWidth="800"
mc:Ignorable="d"
x:Class="Nebula.Launcher.Views.Controls.PlayerContainerControl"
xmlns="https://github.com/avaloniaui"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Border
BorderThickness="0,0,1,0"
CornerRadius="0,10,0,10"
Margin="5,4,5,0"
VerticalAlignment="Center">
<Panel>
<StackPanel Margin="10,5,5,5" Orientation="Horizontal">
<Label Margin="0,0,2,0">Name:</Label>
<Label>Ni Higgers</Label>
</StackPanel>
<StackPanel HorizontalAlignment="Right" Orientation="Horizontal">
<Button CornerRadius="0,0,0,10" Padding="5">
<Label>
Select
</Label>
</Button>
<Button
BorderThickness="2,0,0,0"
CornerRadius="0,10,0,0"
Padding="5">
<Label>
Delete
</Label>
</Button>
</StackPanel>
</Panel>
</Border>
</UserControl>

View File

@@ -1,13 +0,0 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
namespace Nebula.Launcher.Views.Controls;
public partial class PlayerContainerControl : UserControl
{
public PlayerContainerControl()
{
InitializeComponent();
}
}

View File

@@ -1,57 +0,0 @@
<UserControl
d:DesignHeight="100"
d:DesignWidth="800"
mc:Ignorable="d"
x:Class="Nebula.Launcher.Views.Controls.ServerContainerControl"
xmlns="https://github.com/avaloniaui"
xmlns:asyncImageLoader="clr-namespace:AsyncImageLoader;assembly=AsyncImageLoader.Avalonia"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Grid
ColumnDefinitions="*,70"
Margin="0,5,0,5"
RowDefinitions="40,*">
<Border
BorderThickness="2,0,0,0"
Grid.Column="0"
Grid.Row="0"
Padding="10">
<TextBlock x:Name="ServerNameLabel">Server name</TextBlock>
</Border>
<Border
BorderThickness="2,0,0,0"
CornerRadius="0"
Grid.Column="1"
Grid.Row="0"
Padding="5">
<Label HorizontalAlignment="Center" VerticalAlignment="Center">15/15</Label>
</Border>
<Border
BorderThickness="2,0,0,0"
CornerRadius="0,0,10,10"
Grid.Column="0"
Grid.ColumnSpan="2"
Grid.Row="1"
MinHeight="50">
<Border.Background>
<ImageBrush Stretch="UniformToFill" asyncImageLoader:ImageBrushLoader.Source="https://t4.ftcdn.net/jpg/00/81/55/69/360_F_81556974_8sF8cKszJaRfBGd5sDt1RXE2QbzDtQqs.jpg" />
</Border.Background>
</Border>
<Panel Grid.Column="1" Grid.Row="1">
<Border Classes="ButtonBack" CornerRadius="0,0,10,0">
<Button
CornerRadius="0,0,10,0"
HorizontalAlignment="Stretch"
Padding="0"
VerticalAlignment="Stretch">
<Label HorizontalAlignment="Center" VerticalAlignment="Center">
Play
</Label>
</Button>
</Border>
</Panel>
</Grid>
</UserControl>

View File

@@ -1,25 +0,0 @@
using Avalonia;
using Avalonia.Controls;
namespace Nebula.Launcher.Views.Controls;
public partial class ServerContainerControl : UserControl
{
public static readonly StyledProperty<string> ServerNameProperty
= AvaloniaProperty.Register<ServerContainerControl, string>(nameof (ServerName));
public string ServerName
{
get => GetValue(ServerNameProperty);
set
{
SetValue(ServerNameProperty, value);
ServerNameLabel.Text = value;
}
}
public ServerContainerControl()
{
InitializeComponent();
}
}

View File

@@ -5,11 +5,13 @@
x:Class="Nebula.Launcher.Views.Pages.AccountInfoView"
x:DataType="viewModels:AccountInfoViewModel"
xmlns="https://github.com/avaloniaui"
xmlns:controls="clr-namespace:Nebula.Launcher.Views.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:viewModels="clr-namespace:Nebula.Launcher.ViewModels">
xmlns:viewModels="clr-namespace:Nebula.Launcher.ViewModels" IsEnabled="{Binding PageEnabled}">
<Design.DataContext>
<viewModels:AccountInfoViewModel />
</Design.DataContext>
<Grid ColumnDefinitions="*,1.5*" RowDefinitions="*">
<StackPanel Grid.Column="0" Grid.Row="0">
<Border
@@ -23,21 +25,28 @@
Source="/Assets/account.png" />
<Grid ColumnDefinitions="120, 100" RowDefinitions="Auto, Auto, Auto">
<Label Grid.Column="0" Grid.Row="0">Login:</Label>
<TextBox Grid.Column="1" Grid.Row="0" />
<TextBox
Grid.Column="1"
Grid.Row="0"
Text="{Binding CurrentLogin}"/>
<Label Grid.Column="0" Grid.Row="1">Password:</Label>
<TextBox
Grid.Column="1"
Grid.Row="1"
PasswordChar="#"
Text="Sas" />
Text="{Binding CurrentPassword}" />
<Label Grid.Column="0" Grid.Row="2">Auth server:</Label>
<TextBox Grid.Column="1" Grid.Row="2" />
<TextBox
Grid.Column="1"
Grid.Row="2"
Text="{Binding CurrentAuthServer}"/>
</Grid>
<StackPanel
HorizontalAlignment="Center"
Margin="5"
Margin="5" Spacing="5"
Orientation="Horizontal">
<Button>Save profile</Button>
<Button Command="{Binding OnSaveProfile}"><Label>Save profile</Label></Button>
<Button Command="{Binding DoAuth}"><Label>Auth</Label></Button>
</StackPanel>
</StackPanel>
</Border>
@@ -51,10 +60,40 @@
<Label HorizontalAlignment="Center">Profiles:</Label>
</Border>
<ScrollViewer HorizontalScrollBarVisibility="Disabled" VerticalScrollBarVisibility="Auto">
<StackPanel>
<controls:PlayerContainerControl />
<controls:PlayerContainerControl />
</StackPanel>
<ListBox
Background="#00000000"
ItemsSource="{Binding Accounts}"
Padding="0" Classes="AccountSelector">
<ListBox.ItemTemplate>
<DataTemplate DataType="{x:Type viewModels:AuthLoginPasswordModel}">
<Border Margin="5,5,5,0"
CornerRadius="0,10,0,10"
VerticalAlignment="Center">
<Panel>
<StackPanel Margin="10,5,5,5" Orientation="Horizontal">
<Label>Name:</Label>
<Label><TextBlock Text="{Binding Login}"/></Label>
</StackPanel>
<StackPanel HorizontalAlignment="Right" Orientation="Horizontal">
<Button CornerRadius="0,0,0,10" Padding="5" Command="{Binding OnSelect}">
<Label>
Select
</Label>
</Button>
<Button
BorderThickness="2,0,0,0"
CornerRadius="0,10,0,0"
Padding="5" Command="{Binding OnDelete}">
<Label>
Delete
</Label>
</Button>
</StackPanel>
</Panel>
</Border>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</ScrollViewer>
</StackPanel>
</Grid>

View File

@@ -11,7 +11,7 @@ public partial class AccountInfoView : UserControl
InitializeComponent();
}
public AccountInfoView(ViewModels.AccountInfoViewModel viewModel)
public AccountInfoView(AccountInfoViewModel viewModel)
: this()
{
DataContext = viewModel;

View File

@@ -5,12 +5,12 @@
x:Class="Nebula.Launcher.Views.Pages.ServerListView"
x:DataType="viewModels:ServerListViewModel"
xmlns="https://github.com/avaloniaui"
xmlns:controls="clr-namespace:Nebula.Launcher.Views.Controls"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:models="clr-namespace:Nebula.Launcher.Models"
xmlns:viewModels="clr-namespace:Nebula.Launcher.ViewModels"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:asyncImageLoader="clr-namespace:AsyncImageLoader;assembly=AsyncImageLoader.Avalonia">
<Design.DataContext>
<viewModels:ServerListViewModel />
@@ -23,8 +23,57 @@
ItemsSource="{Binding ServerInfos}"
Padding="0">
<ListBox.ItemTemplate>
<DataTemplate DataType="{x:Type models:ServerInfo}">
<controls:ServerContainerControl ServerName="{Binding StatusData.Name}" />
<DataTemplate DataType="{x:Type models:ServerHubInfo}">
<Grid
ColumnDefinitions="*,70"
Margin="0,5,0,5"
RowDefinitions="40,*">
<Border
BorderThickness="2,0,0,0"
Grid.Column="0"
Grid.Row="0"
Padding="10">
<TextBlock Text="{Binding StatusData.Name}"/>
</Border>
<Border
BorderThickness="2,0,0,0"
CornerRadius="0"
Grid.Column="1"
Grid.Row="0"
Padding="5">
<StackPanel Orientation="Horizontal" HorizontalAlignment="Center" VerticalAlignment="Center">
<TextBlock Text="{Binding StatusData.Players}"/>
<TextBlock>/</TextBlock>
<TextBlock Text="{Binding StatusData.SoftMaxPlayers}"/>
</StackPanel>
</Border>
<Border
BorderThickness="2,0,0,0"
CornerRadius="0,0,10,10"
Grid.Column="0"
Grid.ColumnSpan="2"
Grid.Row="1"
MinHeight="50">
<Border.Background>
<ImageBrush Stretch="UniformToFill" asyncImageLoader:ImageBrushLoader.Source="https://t4.ftcdn.net/jpg/00/81/55/69/360_F_81556974_8sF8cKszJaRfBGd5sDt1RXE2QbzDtQqs.jpg" />
</Border.Background>
</Border>
<Panel Grid.Column="1" Grid.Row="1">
<Border Classes="ButtonBack" CornerRadius="0,0,10,0">
<Button
CornerRadius="0,0,10,0"
HorizontalAlignment="Stretch"
Padding="0"
VerticalAlignment="Stretch">
<Label HorizontalAlignment="Center" VerticalAlignment="Center">
Play
</Label>
</Button>
</Border>
</Panel>
</Grid>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>