mirror of
https://github.com/Artemis-RGB/Artemis
synced 2025-12-12 21:38:38 +00:00
Merge branch 'development'
This commit is contained in:
commit
5184d53d97
@ -155,10 +155,4 @@ public static class Constants
|
|||||||
/// Gets the startup arguments provided to the application
|
/// Gets the startup arguments provided to the application
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public static ReadOnlyCollection<string> StartupArguments { get; set; } = null!;
|
public static ReadOnlyCollection<string> StartupArguments { get; set; } = null!;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the graphics context to be used for rendering by SkiaSharp.
|
|
||||||
/// </summary>
|
|
||||||
public static IManagedGraphicsContext? ManagedGraphicsContext { get; internal set; }
|
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -735,6 +735,9 @@ public sealed class Layer : RenderProfileElement
|
|||||||
if (Disposed)
|
if (Disposed)
|
||||||
throw new ObjectDisposedException("Layer");
|
throw new ObjectDisposedException("Layer");
|
||||||
|
|
||||||
|
if (_leds.Contains(led))
|
||||||
|
return;
|
||||||
|
|
||||||
_leds.Add(led);
|
_leds.Add(led);
|
||||||
CalculateRenderProperties();
|
CalculateRenderProperties();
|
||||||
}
|
}
|
||||||
@ -761,7 +764,9 @@ public sealed class Layer : RenderProfileElement
|
|||||||
if (Disposed)
|
if (Disposed)
|
||||||
throw new ObjectDisposedException("Layer");
|
throw new ObjectDisposedException("Layer");
|
||||||
|
|
||||||
_leds.Remove(led);
|
if (!_leds.Remove(led))
|
||||||
|
return;
|
||||||
|
|
||||||
CalculateRenderProperties();
|
CalculateRenderProperties();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -773,6 +778,9 @@ public sealed class Layer : RenderProfileElement
|
|||||||
if (Disposed)
|
if (Disposed)
|
||||||
throw new ObjectDisposedException("Layer");
|
throw new ObjectDisposedException("Layer");
|
||||||
|
|
||||||
|
if (!_leds.Any())
|
||||||
|
return;
|
||||||
|
|
||||||
_leds.Clear();
|
_leds.Clear();
|
||||||
CalculateRenderProperties();
|
CalculateRenderProperties();
|
||||||
}
|
}
|
||||||
@ -790,7 +798,7 @@ public sealed class Layer : RenderProfileElement
|
|||||||
{
|
{
|
||||||
ArtemisLed? match = availableLeds.FirstOrDefault(a => a.Device.Identifier == ledEntity.DeviceIdentifier &&
|
ArtemisLed? match = availableLeds.FirstOrDefault(a => a.Device.Identifier == ledEntity.DeviceIdentifier &&
|
||||||
a.RgbLed.Id.ToString() == ledEntity.LedName);
|
a.RgbLed.Id.ToString() == ledEntity.LedName);
|
||||||
if (match != null)
|
if (match != null && !leds.Contains(match))
|
||||||
leds.Add(match);
|
leds.Add(match);
|
||||||
else
|
else
|
||||||
_missingLeds.Add(ledEntity);
|
_missingLeds.Add(ledEntity);
|
||||||
|
|||||||
@ -1,117 +0,0 @@
|
|||||||
using System;
|
|
||||||
using SkiaSharp;
|
|
||||||
|
|
||||||
namespace Artemis.Core;
|
|
||||||
|
|
||||||
internal class Renderer : IDisposable
|
|
||||||
{
|
|
||||||
private bool _disposed;
|
|
||||||
private SKRect _lastBounds;
|
|
||||||
private GRContext? _lastGraphicsContext;
|
|
||||||
private SKRect _lastParentBounds;
|
|
||||||
private bool _valid;
|
|
||||||
public SKSurface? Surface { get; private set; }
|
|
||||||
public SKPaint? Paint { get; private set; }
|
|
||||||
public SKPath? Path { get; private set; }
|
|
||||||
public SKPoint TargetLocation { get; private set; }
|
|
||||||
|
|
||||||
public bool IsOpen { get; private set; }
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Opens the render context using the dimensions of the provided path
|
|
||||||
/// </summary>
|
|
||||||
public void Open(SKPath path, Folder? parent)
|
|
||||||
{
|
|
||||||
if (_disposed)
|
|
||||||
throw new ObjectDisposedException("Renderer");
|
|
||||||
|
|
||||||
if (IsOpen)
|
|
||||||
throw new ArtemisCoreException("Cannot open render context because it is already open");
|
|
||||||
|
|
||||||
if (path.Bounds != _lastBounds || (parent != null && parent.Bounds != _lastParentBounds) || _lastGraphicsContext != Constants.ManagedGraphicsContext?.GraphicsContext)
|
|
||||||
Invalidate();
|
|
||||||
|
|
||||||
if (!_valid || Surface == null)
|
|
||||||
{
|
|
||||||
SKRect pathBounds = path.Bounds;
|
|
||||||
int width = (int) pathBounds.Width;
|
|
||||||
int height = (int) pathBounds.Height;
|
|
||||||
|
|
||||||
SKImageInfo imageInfo = new(width, height);
|
|
||||||
if (Constants.ManagedGraphicsContext?.GraphicsContext == null)
|
|
||||||
Surface = SKSurface.Create(imageInfo);
|
|
||||||
else
|
|
||||||
Surface = SKSurface.Create(Constants.ManagedGraphicsContext.GraphicsContext, true, imageInfo);
|
|
||||||
|
|
||||||
Path = new SKPath(path);
|
|
||||||
Path.Transform(SKMatrix.CreateTranslation(pathBounds.Left * -1, pathBounds.Top * -1));
|
|
||||||
|
|
||||||
TargetLocation = new SKPoint(pathBounds.Location.X, pathBounds.Location.Y);
|
|
||||||
if (parent != null)
|
|
||||||
TargetLocation -= parent.Bounds.Location;
|
|
||||||
|
|
||||||
Surface.Canvas.ClipPath(Path);
|
|
||||||
|
|
||||||
_lastParentBounds = parent?.Bounds ?? new SKRect();
|
|
||||||
_lastBounds = path.Bounds;
|
|
||||||
_lastGraphicsContext = Constants.ManagedGraphicsContext?.GraphicsContext;
|
|
||||||
_valid = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
Paint = new SKPaint();
|
|
||||||
|
|
||||||
Surface.Canvas.Clear();
|
|
||||||
Surface.Canvas.Save();
|
|
||||||
|
|
||||||
IsOpen = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Close()
|
|
||||||
{
|
|
||||||
if (_disposed)
|
|
||||||
throw new ObjectDisposedException("Renderer");
|
|
||||||
|
|
||||||
Surface?.Canvas.Restore();
|
|
||||||
|
|
||||||
// Looks like every part of the paint needs to be disposed :(
|
|
||||||
Paint?.ColorFilter?.Dispose();
|
|
||||||
Paint?.ImageFilter?.Dispose();
|
|
||||||
Paint?.MaskFilter?.Dispose();
|
|
||||||
Paint?.PathEffect?.Dispose();
|
|
||||||
Paint?.Dispose();
|
|
||||||
|
|
||||||
Paint = null;
|
|
||||||
|
|
||||||
IsOpen = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Invalidate()
|
|
||||||
{
|
|
||||||
if (_disposed)
|
|
||||||
throw new ObjectDisposedException("Renderer");
|
|
||||||
|
|
||||||
_valid = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
~Renderer()
|
|
||||||
{
|
|
||||||
if (IsOpen)
|
|
||||||
Close();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
if (IsOpen)
|
|
||||||
Close();
|
|
||||||
|
|
||||||
Surface?.Dispose();
|
|
||||||
Paint?.Dispose();
|
|
||||||
Path?.Dispose();
|
|
||||||
|
|
||||||
Surface = null;
|
|
||||||
Paint = null;
|
|
||||||
Path = null;
|
|
||||||
|
|
||||||
_disposed = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -123,6 +123,7 @@ internal class RenderService : IRenderService, IRenderer, IDisposable
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_logger.Information("Applying {Name} graphics context", _preferredGraphicsContext.Value);
|
||||||
if (_preferredGraphicsContext.Value == "Software")
|
if (_preferredGraphicsContext.Value == "Software")
|
||||||
{
|
{
|
||||||
GraphicsContext = null;
|
GraphicsContext = null;
|
||||||
|
|||||||
@ -60,10 +60,10 @@ public class WindowsUpdateNotificationProvider : IUpdateNotificationProvider
|
|||||||
Dispatcher.UIThread.Invoke(async () =>
|
Dispatcher.UIThread.Invoke(async () =>
|
||||||
{
|
{
|
||||||
_mainWindowService.OpenMainWindow();
|
_mainWindowService.OpenMainWindow();
|
||||||
if (releaseId != null)
|
if (releaseId != null && releaseId.Value != Guid.Empty)
|
||||||
await _router.Navigate($"settings/releases/{releaseId}");
|
await _router.Navigate($"settings/releases/{releaseId}");
|
||||||
else
|
else
|
||||||
await _router.Navigate($"settings/releases");
|
await _router.Navigate("settings/releases");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -64,6 +64,7 @@ public static class Routes
|
|||||||
new RouteRegistration<ReleaseDetailsViewModel>("{releaseId:guid}")
|
new RouteRegistration<ReleaseDetailsViewModel>("{releaseId:guid}")
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
new RouteRegistration<AccountTabViewModel>("account"),
|
||||||
new RouteRegistration<AboutTabViewModel>("about")
|
new RouteRegistration<AboutTabViewModel>("about")
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@ -63,7 +63,7 @@ public partial class PerformanceDebugViewModel : ActivatableViewModelBase
|
|||||||
|
|
||||||
private void HandleActivation()
|
private void HandleActivation()
|
||||||
{
|
{
|
||||||
Renderer = Constants.ManagedGraphicsContext != null ? Constants.ManagedGraphicsContext.GetType().Name : "Software";
|
Renderer = _renderService.GraphicsContext?.GetType().Name ?? "Software";
|
||||||
_renderService.FrameRendered += RenderServiceOnFrameRendered;
|
_renderService.FrameRendered += RenderServiceOnFrameRendered;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -35,7 +35,7 @@ public partial class RenderDebugViewModel : ActivatableViewModelBase
|
|||||||
|
|
||||||
private void HandleActivation()
|
private void HandleActivation()
|
||||||
{
|
{
|
||||||
Renderer = Constants.ManagedGraphicsContext != null ? Constants.ManagedGraphicsContext.GetType().Name : "Software";
|
Renderer = _renderService.GraphicsContext?.GetType().Name ?? "Software";
|
||||||
_renderService.FrameRendered += RenderServiceOnFrameRendered;
|
_renderService.FrameRendered += RenderServiceOnFrameRendered;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -4,6 +4,7 @@
|
|||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
xmlns:services="clr-namespace:Artemis.WebClient.Workshop.Services;assembly=Artemis.WebClient.Workshop"
|
xmlns:services="clr-namespace:Artemis.WebClient.Workshop.Services;assembly=Artemis.WebClient.Workshop"
|
||||||
xmlns:layoutProviders="clr-namespace:Artemis.UI.Screens.Device.Layout.LayoutProviders"
|
xmlns:layoutProviders="clr-namespace:Artemis.UI.Screens.Device.Layout.LayoutProviders"
|
||||||
|
xmlns:models="clr-namespace:Artemis.WebClient.Workshop.Models;assembly=Artemis.WebClient.Workshop"
|
||||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||||
x:Class="Artemis.UI.Screens.Device.Layout.LayoutProviders.WorkshopLayoutView"
|
x:Class="Artemis.UI.Screens.Device.Layout.LayoutProviders.WorkshopLayoutView"
|
||||||
x:DataType="layoutProviders:WorkshopLayoutViewModel">
|
x:DataType="layoutProviders:WorkshopLayoutViewModel">
|
||||||
@ -19,7 +20,7 @@
|
|||||||
<Style Selector="ComboBox.layoutProvider /template/ ContentControl#ContentPresenter">
|
<Style Selector="ComboBox.layoutProvider /template/ ContentControl#ContentPresenter">
|
||||||
<Setter Property="ContentTemplate">
|
<Setter Property="ContentTemplate">
|
||||||
<Setter.Value>
|
<Setter.Value>
|
||||||
<DataTemplate x:DataType="services:InstalledEntry">
|
<DataTemplate x:DataType="models:InstalledEntry">
|
||||||
<TextBlock Text="{CompiledBinding Name}" TextWrapping="Wrap" MaxWidth="350" />
|
<TextBlock Text="{CompiledBinding Name}" TextWrapping="Wrap" MaxWidth="350" />
|
||||||
</DataTemplate>
|
</DataTemplate>
|
||||||
</Setter.Value>
|
</Setter.Value>
|
||||||
@ -32,7 +33,7 @@
|
|||||||
ItemsSource="{CompiledBinding Entries}"
|
ItemsSource="{CompiledBinding Entries}"
|
||||||
PlaceholderText="Select an installed layout">
|
PlaceholderText="Select an installed layout">
|
||||||
<ComboBox.ItemTemplate>
|
<ComboBox.ItemTemplate>
|
||||||
<DataTemplate x:DataType="services:InstalledEntry">
|
<DataTemplate x:DataType="models:InstalledEntry">
|
||||||
<StackPanel>
|
<StackPanel>
|
||||||
<TextBlock Text="{CompiledBinding Name}" TextWrapping="Wrap" MaxWidth="350" />
|
<TextBlock Text="{CompiledBinding Name}" TextWrapping="Wrap" MaxWidth="350" />
|
||||||
<TextBlock Classes="subtitle" Text="{CompiledBinding Author}" TextWrapping="Wrap" MaxWidth="350" />
|
<TextBlock Classes="subtitle" Text="{CompiledBinding Author}" TextWrapping="Wrap" MaxWidth="350" />
|
||||||
|
|||||||
@ -10,6 +10,7 @@ using Artemis.UI.Shared;
|
|||||||
using Artemis.UI.Shared.Routing;
|
using Artemis.UI.Shared.Routing;
|
||||||
using Artemis.UI.Shared.Services;
|
using Artemis.UI.Shared.Services;
|
||||||
using Artemis.WebClient.Workshop;
|
using Artemis.WebClient.Workshop;
|
||||||
|
using Artemis.WebClient.Workshop.Models;
|
||||||
using Artemis.WebClient.Workshop.Providers;
|
using Artemis.WebClient.Workshop.Providers;
|
||||||
using Artemis.WebClient.Workshop.Services;
|
using Artemis.WebClient.Workshop.Services;
|
||||||
using PropertyChanged.SourceGenerator;
|
using PropertyChanged.SourceGenerator;
|
||||||
|
|||||||
@ -0,0 +1,14 @@
|
|||||||
|
<UserControl xmlns="https://github.com/avaloniaui"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
|
xmlns:account="clr-namespace:Artemis.UI.Screens.Settings.Account"
|
||||||
|
mc:Ignorable="d" d:DesignWidth="320" d:DesignHeight="450"
|
||||||
|
x:Class="Artemis.UI.Screens.Settings.Account.ChangeEmailAddressView"
|
||||||
|
x:DataType="account:ChangeEmailAddressViewModel">
|
||||||
|
<StackPanel Spacing="5" Width="300">
|
||||||
|
<Label>New email address</Label>
|
||||||
|
<TextBox Text="{CompiledBinding EmailAddress}" />
|
||||||
|
<TextBlock Classes="subtitle" Margin="0 5 0 0" TextWrapping="Wrap">An email will be sent to the new address to confirm the change.</TextBlock>
|
||||||
|
</StackPanel>
|
||||||
|
</UserControl>
|
||||||
@ -0,0 +1,16 @@
|
|||||||
|
using Artemis.UI.Shared.Extensions;
|
||||||
|
using Avalonia.ReactiveUI;
|
||||||
|
using Avalonia.Threading;
|
||||||
|
using ReactiveUI;
|
||||||
|
|
||||||
|
namespace Artemis.UI.Screens.Settings.Account;
|
||||||
|
|
||||||
|
public partial class ChangeEmailAddressView : ReactiveUserControl<ChangeEmailAddressViewModel>
|
||||||
|
{
|
||||||
|
public ChangeEmailAddressView()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
this.WhenActivated(_ => this.ClearAllDataValidationErrors());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,50 @@
|
|||||||
|
using System;
|
||||||
|
using System.ComponentModel.DataAnnotations;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Reactive;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Artemis.UI.Shared;
|
||||||
|
using Artemis.UI.Shared.Services;
|
||||||
|
using Artemis.WebClient.Workshop.Handlers.UploadHandlers;
|
||||||
|
using Artemis.WebClient.Workshop.Services;
|
||||||
|
using IdentityModel;
|
||||||
|
using PropertyChanged.SourceGenerator;
|
||||||
|
using ReactiveUI;
|
||||||
|
using ReactiveUI.Validation.Extensions;
|
||||||
|
|
||||||
|
namespace Artemis.UI.Screens.Settings.Account;
|
||||||
|
|
||||||
|
public partial class ChangeEmailAddressViewModel : ContentDialogViewModelBase
|
||||||
|
{
|
||||||
|
private readonly IUserManagementService _userManagementService;
|
||||||
|
private readonly IWindowService _windowService;
|
||||||
|
[Notify] private string _emailAddress = string.Empty;
|
||||||
|
|
||||||
|
public ChangeEmailAddressViewModel(IUserManagementService userManagementService, IAuthenticationService authenticationService, IWindowService windowService)
|
||||||
|
{
|
||||||
|
_userManagementService = userManagementService;
|
||||||
|
_windowService = windowService;
|
||||||
|
Submit = ReactiveCommand.CreateFromTask(ExecuteSubmit, ValidationContext.Valid);
|
||||||
|
|
||||||
|
string? currentEmail = authenticationService.Claims.FirstOrDefault(c => c.Type == JwtClaimTypes.Email)?.Value;
|
||||||
|
this.ValidationRule(vm => vm.EmailAddress, e => !string.IsNullOrWhiteSpace(e),
|
||||||
|
"You must specify a new email address");
|
||||||
|
this.ValidationRule(vm => vm.EmailAddress, e => string.IsNullOrWhiteSpace(e) || new EmailAddressAttribute().IsValid(e),
|
||||||
|
"You must specify a valid email address");
|
||||||
|
this.ValidationRule(vm => vm.EmailAddress,
|
||||||
|
e => string.IsNullOrWhiteSpace(e) || currentEmail == null || !string.Equals(e, currentEmail, StringComparison.InvariantCultureIgnoreCase),
|
||||||
|
"New email address must be different from the old one");
|
||||||
|
}
|
||||||
|
|
||||||
|
public ReactiveCommand<Unit, Unit> Submit { get; }
|
||||||
|
|
||||||
|
private async Task ExecuteSubmit(CancellationToken cts)
|
||||||
|
{
|
||||||
|
ApiResult result = await _userManagementService.ChangeEmailAddress(EmailAddress, cts);
|
||||||
|
if (result.IsSuccess)
|
||||||
|
await _windowService.ShowConfirmContentDialog("Confirmation required", "Before being applied, you must confirm your new email address. Please check your inbox.", cancel: null);
|
||||||
|
else
|
||||||
|
await _windowService.ShowConfirmContentDialog("Failed to update email address", result.Message ?? "An unexpected error occured", cancel: null);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,17 @@
|
|||||||
|
<UserControl xmlns="https://github.com/avaloniaui"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
|
xmlns:account="clr-namespace:Artemis.UI.Screens.Settings.Account"
|
||||||
|
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||||
|
x:Class="Artemis.UI.Screens.Settings.Account.ChangePasswordView"
|
||||||
|
x:DataType="account:ChangePasswordViewModel">
|
||||||
|
<StackPanel Spacing="5" Width="300">
|
||||||
|
<Label>Current password</Label>
|
||||||
|
<TextBox Text="{CompiledBinding CurrentPassword}" PasswordChar="●"/>
|
||||||
|
<Label>New password</Label>
|
||||||
|
<TextBox Text="{CompiledBinding NewPassword}" PasswordChar="●"/>
|
||||||
|
<Label>New password confirmation</Label>
|
||||||
|
<TextBox Text="{CompiledBinding NewPasswordConfirmation}" PasswordChar="●"/>
|
||||||
|
</StackPanel>
|
||||||
|
</UserControl>
|
||||||
@ -0,0 +1,17 @@
|
|||||||
|
using Artemis.UI.Shared.Extensions;
|
||||||
|
using Avalonia;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Markup.Xaml;
|
||||||
|
using Avalonia.ReactiveUI;
|
||||||
|
using ReactiveUI;
|
||||||
|
|
||||||
|
namespace Artemis.UI.Screens.Settings.Account;
|
||||||
|
|
||||||
|
public partial class ChangePasswordView : ReactiveUserControl<ChangePasswordViewModel>
|
||||||
|
{
|
||||||
|
public ChangePasswordView()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
this.WhenActivated(_ => this.ClearAllDataValidationErrors());
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,47 @@
|
|||||||
|
using System.Reactive;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Artemis.UI.Shared;
|
||||||
|
using Artemis.UI.Shared.Services;
|
||||||
|
using Artemis.WebClient.Workshop.Handlers.UploadHandlers;
|
||||||
|
using Artemis.WebClient.Workshop.Services;
|
||||||
|
using PropertyChanged.SourceGenerator;
|
||||||
|
using ReactiveUI;
|
||||||
|
using ReactiveUI.Validation.Extensions;
|
||||||
|
|
||||||
|
namespace Artemis.UI.Screens.Settings.Account;
|
||||||
|
|
||||||
|
public partial class ChangePasswordViewModel : ContentDialogViewModelBase
|
||||||
|
{
|
||||||
|
private readonly IUserManagementService _userManagementService;
|
||||||
|
private readonly IWindowService _windowService;
|
||||||
|
[Notify] private string _currentPassword = string.Empty;
|
||||||
|
[Notify] private string _newPassword = string.Empty;
|
||||||
|
[Notify] private string _newPasswordConfirmation = string.Empty;
|
||||||
|
|
||||||
|
public ChangePasswordViewModel(IUserManagementService userManagementService, IWindowService windowService)
|
||||||
|
{
|
||||||
|
_userManagementService = userManagementService;
|
||||||
|
_windowService = windowService;
|
||||||
|
Submit = ReactiveCommand.CreateFromTask(ExecuteSubmit, ValidationContext.Valid);
|
||||||
|
|
||||||
|
this.ValidationRule(vm => vm.CurrentPassword, e => !string.IsNullOrWhiteSpace(e), "You must specify your current password");
|
||||||
|
this.ValidationRule(vm => vm.NewPassword, e => !string.IsNullOrWhiteSpace(e), "You must specify a new password");
|
||||||
|
this.ValidationRule(
|
||||||
|
vm => vm.NewPasswordConfirmation,
|
||||||
|
this.WhenAnyValue(vm => vm.NewPassword, vm => vm.NewPasswordConfirmation, (password, confirmation) => password == confirmation),
|
||||||
|
"The passwords must match"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public ReactiveCommand<Unit, Unit> Submit { get; }
|
||||||
|
|
||||||
|
private async Task ExecuteSubmit(CancellationToken cts)
|
||||||
|
{
|
||||||
|
ApiResult result = await _userManagementService.ChangePassword(CurrentPassword, NewPassword, cts);
|
||||||
|
if (result.IsSuccess)
|
||||||
|
await _windowService.ShowConfirmContentDialog("Password changed", "Your password has been changed", cancel: null);
|
||||||
|
else
|
||||||
|
await _windowService.ShowConfirmContentDialog("Failed to change password", result.Message ?? "An unexpected error occured", cancel: null);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,14 @@
|
|||||||
|
<UserControl xmlns="https://github.com/avaloniaui"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
|
xmlns:account="clr-namespace:Artemis.UI.Screens.Settings.Account"
|
||||||
|
mc:Ignorable="d" d:DesignWidth="320" d:DesignHeight="450"
|
||||||
|
x:Class="Artemis.UI.Screens.Settings.Account.RemoveAccountView"
|
||||||
|
x:DataType="account:RemoveAccountViewModel">
|
||||||
|
<StackPanel Spacing="5" Width="350">
|
||||||
|
<Label>Please enter your email address to confirm</Label>
|
||||||
|
<TextBox Text="{CompiledBinding EmailAddress}"/>
|
||||||
|
<TextBlock Classes="danger" Margin="0 5 0 0" TextWrapping="Wrap">This is a destructive action that cannot be undone.</TextBlock>
|
||||||
|
</StackPanel>
|
||||||
|
</UserControl>
|
||||||
@ -0,0 +1,16 @@
|
|||||||
|
using Artemis.UI.Shared.Extensions;
|
||||||
|
using Avalonia.ReactiveUI;
|
||||||
|
using Avalonia.Threading;
|
||||||
|
using ReactiveUI;
|
||||||
|
|
||||||
|
namespace Artemis.UI.Screens.Settings.Account;
|
||||||
|
|
||||||
|
public partial class RemoveAccountView : ReactiveUserControl<RemoveAccountViewModel>
|
||||||
|
{
|
||||||
|
public RemoveAccountView()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
this.WhenActivated(_ => this.ClearAllDataValidationErrors());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@ -0,0 +1,52 @@
|
|||||||
|
using System;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Reactive;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Artemis.UI.Shared;
|
||||||
|
using Artemis.UI.Shared.Services;
|
||||||
|
using Artemis.WebClient.Workshop.Handlers.UploadHandlers;
|
||||||
|
using Artemis.WebClient.Workshop.Services;
|
||||||
|
using IdentityModel;
|
||||||
|
using PropertyChanged.SourceGenerator;
|
||||||
|
using ReactiveUI;
|
||||||
|
using ReactiveUI.Validation.Extensions;
|
||||||
|
|
||||||
|
namespace Artemis.UI.Screens.Settings.Account;
|
||||||
|
|
||||||
|
public partial class RemoveAccountViewModel : ContentDialogViewModelBase
|
||||||
|
{
|
||||||
|
private readonly IUserManagementService _userManagementService;
|
||||||
|
private readonly IAuthenticationService _authenticationService;
|
||||||
|
private readonly IWindowService _windowService;
|
||||||
|
[Notify] private string _emailAddress = string.Empty;
|
||||||
|
|
||||||
|
public RemoveAccountViewModel(IUserManagementService userManagementService, IAuthenticationService authenticationService, IWindowService windowService)
|
||||||
|
{
|
||||||
|
_userManagementService = userManagementService;
|
||||||
|
_authenticationService = authenticationService;
|
||||||
|
_windowService = windowService;
|
||||||
|
|
||||||
|
Submit = ReactiveCommand.CreateFromTask(ExecuteSubmit, ValidationContext.Valid);
|
||||||
|
string? currentEmail = authenticationService.Claims.FirstOrDefault(c => c.Type == JwtClaimTypes.Email)?.Value;
|
||||||
|
|
||||||
|
this.ValidationRule(vm => vm.EmailAddress, e => !string.IsNullOrWhiteSpace(e), "You must enter your email address");
|
||||||
|
this.ValidationRule(vm => vm.EmailAddress,
|
||||||
|
e => string.IsNullOrWhiteSpace(e) || string.Equals(e, currentEmail, StringComparison.InvariantCultureIgnoreCase),
|
||||||
|
"The entered email address is not correct");
|
||||||
|
}
|
||||||
|
|
||||||
|
public ReactiveCommand<Unit, Unit> Submit { get; }
|
||||||
|
|
||||||
|
private async Task ExecuteSubmit(CancellationToken cts)
|
||||||
|
{
|
||||||
|
ApiResult result = await _userManagementService.RemoveAccount(cts);
|
||||||
|
if (result.IsSuccess)
|
||||||
|
{
|
||||||
|
await _windowService.ShowConfirmContentDialog("Account removed", "Hopefully we'll see you again!", cancel: null);
|
||||||
|
_authenticationService.Logout();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
await _windowService.ShowConfirmContentDialog("Failed to remove account", result.Message ?? "An unexpected error occured", cancel: null);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -26,6 +26,7 @@ public partial class SettingsViewModel : RoutableHostScreen<RoutableScreen>, IMa
|
|||||||
new("Plugins", "settings/plugins"),
|
new("Plugins", "settings/plugins"),
|
||||||
new("Devices", "settings/devices"),
|
new("Devices", "settings/devices"),
|
||||||
new("Releases", "settings/releases"),
|
new("Releases", "settings/releases"),
|
||||||
|
new("Account", "settings/account"),
|
||||||
new("About", "settings/about"),
|
new("About", "settings/about"),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
114
src/Artemis.UI/Screens/Settings/Tabs/AccountTabView.axaml
Normal file
114
src/Artemis.UI/Screens/Settings/Tabs/AccountTabView.axaml
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
<UserControl xmlns="https://github.com/avaloniaui"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
|
xmlns:settings="clr-namespace:Artemis.UI.Screens.Settings"
|
||||||
|
xmlns:asyncImageLoader="clr-namespace:AsyncImageLoader;assembly=AsyncImageLoader.Avalonia"
|
||||||
|
xmlns:loaders="clr-namespace:AsyncImageLoader.Loaders;assembly=AsyncImageLoader.Avalonia"
|
||||||
|
xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
|
||||||
|
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="800"
|
||||||
|
x:Class="Artemis.UI.Screens.Settings.AccountTabView"
|
||||||
|
x:DataType="settings:AccountTabViewModel">
|
||||||
|
<Panel>
|
||||||
|
<StackPanel IsVisible="{CompiledBinding !IsLoggedIn^}" Margin="0 50 0 0">
|
||||||
|
<StackPanel.Styles>
|
||||||
|
<Styles>
|
||||||
|
<Style Selector="TextBlock">
|
||||||
|
<Setter Property="TextAlignment" Value="Center"></Setter>
|
||||||
|
<Setter Property="TextWrapping" Value="Wrap"></Setter>
|
||||||
|
</Style>
|
||||||
|
</Styles>
|
||||||
|
</StackPanel.Styles>
|
||||||
|
<TextBlock Theme="{StaticResource TitleTextBlockStyle}">You are not logged in</TextBlock>
|
||||||
|
<TextBlock>
|
||||||
|
<Run>In order to manage your account you must be logged in.</Run>
|
||||||
|
<LineBreak />
|
||||||
|
<Run>Creating an account is free and we'll not bother you with a newsletter or crap like that.</Run>
|
||||||
|
</TextBlock>
|
||||||
|
|
||||||
|
<Lottie Path="/Assets/Animations/login-pending.json" RepeatCount="1" Width="350" Height="350"></Lottie>
|
||||||
|
|
||||||
|
<TextBlock>
|
||||||
|
<Run>Click Log In below to (create an account) and log in.</Run>
|
||||||
|
<LineBreak />
|
||||||
|
<Run>You'll also be able to log in with Google or Discord.</Run>
|
||||||
|
</TextBlock>
|
||||||
|
<Button HorizontalAlignment="Center" Command="{CompiledBinding Login}" Margin="0 25 0 0">Login</Button>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<ScrollViewer IsVisible="{CompiledBinding IsLoggedIn^}" HorizontalScrollBarVisibility="Disabled" VerticalScrollBarVisibility="Auto">
|
||||||
|
<StackPanel Margin="15" MaxWidth="1000">
|
||||||
|
<Grid RowDefinitions="*,*" ColumnDefinitions="Auto,*" VerticalAlignment="Top">
|
||||||
|
<Border Grid.Row="0" Grid.Column="0" Grid.RowSpan="2" VerticalAlignment="Top" CornerRadius="92" Width="92" Height="92" Margin="0 0 15 0" ClipToBounds="True">
|
||||||
|
<asyncImageLoader:AdvancedImage Source="{CompiledBinding AvatarUrl}">
|
||||||
|
<asyncImageLoader:AdvancedImage.Loader>
|
||||||
|
<loaders:BaseWebImageLoader />
|
||||||
|
</asyncImageLoader:AdvancedImage.Loader>
|
||||||
|
</asyncImageLoader:AdvancedImage>
|
||||||
|
</Border>
|
||||||
|
<TextBlock Grid.Row="0" Grid.Column="1" Padding="0" VerticalAlignment="Bottom" Text="{CompiledBinding Name}" Classes="h3 no-margin"/>
|
||||||
|
<TextBlock Classes="subtitle" Grid.Column="1" Grid.Row="1" Padding="0" VerticalAlignment="Top" Text="{CompiledBinding Email}" />
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<TextBlock Classes="card-title">
|
||||||
|
Account management
|
||||||
|
</TextBlock>
|
||||||
|
<Border Classes="card" VerticalAlignment="Stretch" Margin="0,0,5,0">
|
||||||
|
<StackPanel>
|
||||||
|
<Grid RowDefinitions="*,*" ColumnDefinitions="*,Auto" IsVisible="{CompiledBinding CanChangePassword}">
|
||||||
|
<StackPanel Grid.Column="0" VerticalAlignment="Top">
|
||||||
|
<TextBlock>
|
||||||
|
Credentials
|
||||||
|
</TextBlock>
|
||||||
|
</StackPanel>
|
||||||
|
<StackPanel Grid.Row="0" Grid.Column="1" VerticalAlignment="Center" Spacing="10">
|
||||||
|
<Button Width="150" Content="Change email" Command="{CompiledBinding ChangeEmailAddress}" />
|
||||||
|
<Button Width="150" Content="Change password" Command="{CompiledBinding ChangePasswordAddress}" />
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
<Border Classes="card-separator" IsVisible="{CompiledBinding CanChangePassword}"/>
|
||||||
|
|
||||||
|
<Grid RowDefinitions="*,*" ColumnDefinitions="*,Auto">
|
||||||
|
<StackPanel Grid.Column="0" VerticalAlignment="Center">
|
||||||
|
<TextBlock>
|
||||||
|
Change avatar
|
||||||
|
</TextBlock>
|
||||||
|
<TextBlock Classes="subtitle">
|
||||||
|
Quite pointless currently, but in the future your avatar will be visible in the workshop.
|
||||||
|
</TextBlock>
|
||||||
|
</StackPanel>
|
||||||
|
<StackPanel Grid.Row="0" Grid.Column="1" VerticalAlignment="Center">
|
||||||
|
<Button Width="150" Content="Choose image" Command="{CompiledBinding ChangeAvatar}" />
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
<Border Classes="card-separator" />
|
||||||
|
|
||||||
|
<Grid RowDefinitions="*,*" ColumnDefinitions="*,Auto">
|
||||||
|
<StackPanel Grid.Column="0" VerticalAlignment="Center">
|
||||||
|
<TextBlock>
|
||||||
|
Remove account
|
||||||
|
</TextBlock>
|
||||||
|
<TextBlock Classes="subtitle">
|
||||||
|
Permanently remove your account, this cannot be undone.
|
||||||
|
</TextBlock>
|
||||||
|
</StackPanel>
|
||||||
|
<StackPanel Grid.Row="0" Grid.Column="1" VerticalAlignment="Center">
|
||||||
|
<Button Classes="danger" Width="150" Content="Remove account" Command="{CompiledBinding RemoveAccount}" />
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<!-- <TextBlock Classes="card-title"> -->
|
||||||
|
<!-- Personal access tokens -->
|
||||||
|
<!-- </TextBlock> -->
|
||||||
|
<!-- <Border Classes="card" VerticalAlignment="Stretch" Margin="0,0,5,0"> -->
|
||||||
|
<!-- <StackPanel> -->
|
||||||
|
<!-- <TextBlock>TODO :)</TextBlock> -->
|
||||||
|
<!-- </StackPanel> -->
|
||||||
|
<!-- </Border> -->
|
||||||
|
</StackPanel>
|
||||||
|
</ScrollViewer>
|
||||||
|
</Panel>
|
||||||
|
|
||||||
|
</UserControl>
|
||||||
14
src/Artemis.UI/Screens/Settings/Tabs/AccountTabView.axaml.cs
Normal file
14
src/Artemis.UI/Screens/Settings/Tabs/AccountTabView.axaml.cs
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
using Avalonia;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Markup.Xaml;
|
||||||
|
using Avalonia.ReactiveUI;
|
||||||
|
|
||||||
|
namespace Artemis.UI.Screens.Settings;
|
||||||
|
|
||||||
|
public partial class AccountTabView : ReactiveUserControl<AccountTabViewModel>
|
||||||
|
{
|
||||||
|
public AccountTabView()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
}
|
||||||
114
src/Artemis.UI/Screens/Settings/Tabs/AccountTabViewModel.cs
Normal file
114
src/Artemis.UI/Screens/Settings/Tabs/AccountTabViewModel.cs
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
using System;
|
||||||
|
using System.ComponentModel;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Reactive.Disposables;
|
||||||
|
using System.Reactive.Linq;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Artemis.UI.Screens.Settings.Account;
|
||||||
|
using Artemis.UI.Screens.Workshop.CurrentUser;
|
||||||
|
using Artemis.UI.Shared.Routing;
|
||||||
|
using Artemis.UI.Shared.Services;
|
||||||
|
using Artemis.WebClient.Workshop;
|
||||||
|
using Artemis.WebClient.Workshop.Handlers.UploadHandlers;
|
||||||
|
using Artemis.WebClient.Workshop.Services;
|
||||||
|
using IdentityModel;
|
||||||
|
using PropertyChanged.SourceGenerator;
|
||||||
|
using ReactiveUI;
|
||||||
|
|
||||||
|
namespace Artemis.UI.Screens.Settings;
|
||||||
|
|
||||||
|
public partial class AccountTabViewModel : RoutableScreen
|
||||||
|
{
|
||||||
|
private readonly IWindowService _windowService;
|
||||||
|
private readonly IAuthenticationService _authenticationService;
|
||||||
|
private readonly IUserManagementService _userManagementService;
|
||||||
|
private ObservableAsPropertyHelper<bool>? _canChangePassword;
|
||||||
|
|
||||||
|
[Notify(Setter.Private)] private string? _name;
|
||||||
|
[Notify(Setter.Private)] private string? _email;
|
||||||
|
[Notify(Setter.Private)] private string? _avatarUrl;
|
||||||
|
|
||||||
|
public AccountTabViewModel(IWindowService windowService, IAuthenticationService authenticationService, IUserManagementService userManagementService)
|
||||||
|
{
|
||||||
|
_windowService = windowService;
|
||||||
|
_authenticationService = authenticationService;
|
||||||
|
_userManagementService = userManagementService;
|
||||||
|
_authenticationService.AutoLogin(true);
|
||||||
|
|
||||||
|
DisplayName = "Account";
|
||||||
|
IsLoggedIn = _authenticationService.IsLoggedIn;
|
||||||
|
|
||||||
|
this.WhenActivated(d =>
|
||||||
|
{
|
||||||
|
_canChangePassword = _authenticationService.GetClaim(JwtClaimTypes.AuthenticationMethod).Select(c => c?.Value == "pwd").ToProperty(this, vm => vm.CanChangePassword);
|
||||||
|
_canChangePassword.DisposeWith(d);
|
||||||
|
});
|
||||||
|
this.WhenActivated(d => _authenticationService.IsLoggedIn.Subscribe(_ => LoadCurrentUser()).DisposeWith(d));
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool CanChangePassword => _canChangePassword?.Value ?? false;
|
||||||
|
public IObservable<bool> IsLoggedIn { get; }
|
||||||
|
|
||||||
|
public async Task Login()
|
||||||
|
{
|
||||||
|
await _windowService.CreateContentDialog().WithViewModel(out WorkshopLoginViewModel _).WithTitle("Account login").ShowAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task ChangeAvatar()
|
||||||
|
{
|
||||||
|
string[]? result = await _windowService.CreateOpenFileDialog().HavingFilter(f => f.WithBitmaps()).ShowAsync();
|
||||||
|
if (result == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
AvatarUrl = $"{WorkshopConstants.AUTHORITY_URL}/user/avatar/{Guid.Empty}";
|
||||||
|
await using FileStream fileStream = new(result.First(), FileMode.Open);
|
||||||
|
ApiResult changeResult = await _userManagementService.ChangeAvatar(fileStream, CancellationToken.None);
|
||||||
|
if (!changeResult.IsSuccess)
|
||||||
|
await _windowService.ShowConfirmContentDialog("Failed to change image", changeResult.Message ?? "An unexpected error occured", cancel: null);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
string? userId = _authenticationService.Claims.FirstOrDefault(c => c.Type == "sub")?.Value;
|
||||||
|
AvatarUrl = $"{WorkshopConstants.AUTHORITY_URL}/user/avatar/{userId}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task ChangeEmailAddress()
|
||||||
|
{
|
||||||
|
await _windowService.CreateContentDialog().WithTitle("Change email address")
|
||||||
|
.WithViewModel(out ChangeEmailAddressViewModel vm)
|
||||||
|
.WithCloseButtonText("Cancel")
|
||||||
|
.HavingPrimaryButton(b => b.WithText("Submit").WithCommand(vm.Submit))
|
||||||
|
.ShowAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task ChangePasswordAddress()
|
||||||
|
{
|
||||||
|
await _windowService.CreateContentDialog().WithTitle("Change password")
|
||||||
|
.WithViewModel(out ChangePasswordViewModel vm)
|
||||||
|
.WithCloseButtonText("Cancel")
|
||||||
|
.HavingPrimaryButton(b => b.WithText("Submit").WithCommand(vm.Submit))
|
||||||
|
.ShowAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task RemoveAccount()
|
||||||
|
{
|
||||||
|
await _windowService.CreateContentDialog().WithTitle("Remove account")
|
||||||
|
.WithViewModel(out RemoveAccountViewModel vm)
|
||||||
|
.WithCloseButtonText("Cancel")
|
||||||
|
.HavingPrimaryButton(b => b.WithText("Permanently remove account").WithCommand(vm.Submit))
|
||||||
|
.ShowAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void LoadCurrentUser()
|
||||||
|
{
|
||||||
|
string? userId = _authenticationService.Claims.FirstOrDefault(c => c.Type == "sub")?.Value;
|
||||||
|
Name = _authenticationService.Claims.FirstOrDefault(c => c.Type == "name")?.Value;
|
||||||
|
Email = _authenticationService.Claims.FirstOrDefault(c => c.Type == "email")?.Value;
|
||||||
|
AvatarUrl = $"{WorkshopConstants.AUTHORITY_URL}/user/avatar/{userId}";
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -34,8 +34,8 @@
|
|||||||
Name="UserMenu">
|
Name="UserMenu">
|
||||||
<Ellipse.ContextFlyout>
|
<Ellipse.ContextFlyout>
|
||||||
<Flyout>
|
<Flyout>
|
||||||
<Grid ColumnDefinitions="Auto,*" RowDefinitions="*,*,*" MinWidth="300">
|
<Grid ColumnDefinitions="Auto,*" RowDefinitions="*,*,*,*" MinWidth="300">
|
||||||
<Ellipse Grid.Column="0" Grid.RowSpan="3" Height="50" Width="50" Margin="0 0 8 0" VerticalAlignment="Top">
|
<Ellipse Grid.Column="0" Grid.RowSpan="4" Height="50" Width="50" Margin="0 0 8 0" VerticalAlignment="Top">
|
||||||
<Ellipse.Fill>
|
<Ellipse.Fill>
|
||||||
<ImageBrush asyncImageLoader:ImageBrushLoader.Source="{CompiledBinding AvatarUrl}" />
|
<ImageBrush asyncImageLoader:ImageBrushLoader.Source="{CompiledBinding AvatarUrl}" />
|
||||||
</Ellipse.Fill>
|
</Ellipse.Fill>
|
||||||
@ -46,10 +46,20 @@
|
|||||||
IsVisible="{CompiledBinding AllowLogout}"
|
IsVisible="{CompiledBinding AllowLogout}"
|
||||||
Grid.Column="1"
|
Grid.Column="1"
|
||||||
Grid.Row="2"
|
Grid.Row="2"
|
||||||
|
Margin="-8 4 0 0"
|
||||||
|
Padding="6 4"
|
||||||
|
Click="Signout_OnClick">
|
||||||
|
Sign out
|
||||||
|
</controls:HyperlinkButton>
|
||||||
|
|
||||||
|
<controls:HyperlinkButton
|
||||||
|
IsVisible="{CompiledBinding AllowLogout}"
|
||||||
|
Grid.Column="1"
|
||||||
|
Grid.Row="3"
|
||||||
Margin="-8 0 0 0"
|
Margin="-8 0 0 0"
|
||||||
Padding="6 4"
|
Padding="6 4"
|
||||||
Click="Button_OnClick">
|
Click="Manage_OnClick">
|
||||||
Sign out
|
Manage account
|
||||||
</controls:HyperlinkButton>
|
</controls:HyperlinkButton>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Flyout>
|
</Flyout>
|
||||||
|
|||||||
@ -13,9 +13,15 @@ public partial class CurrentUserView : ReactiveUserControl<CurrentUserViewModel>
|
|||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void Button_OnClick(object? sender, RoutedEventArgs e)
|
private void Signout_OnClick(object? sender, RoutedEventArgs e)
|
||||||
{
|
{
|
||||||
UserMenu.ContextFlyout?.Hide();
|
UserMenu.ContextFlyout?.Hide();
|
||||||
ViewModel?.Logout();
|
ViewModel?.Logout();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void Manage_OnClick(object? sender, RoutedEventArgs e)
|
||||||
|
{
|
||||||
|
UserMenu.ContextFlyout?.Hide();
|
||||||
|
ViewModel?.ManageAccount();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -6,6 +6,7 @@ using System.Reactive.Disposables;
|
|||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Artemis.UI.Shared;
|
using Artemis.UI.Shared;
|
||||||
|
using Artemis.UI.Shared.Routing;
|
||||||
using Artemis.UI.Shared.Services;
|
using Artemis.UI.Shared.Services;
|
||||||
using Artemis.WebClient.Workshop;
|
using Artemis.WebClient.Workshop;
|
||||||
using Artemis.WebClient.Workshop.Services;
|
using Artemis.WebClient.Workshop.Services;
|
||||||
@ -23,6 +24,7 @@ public partial class CurrentUserViewModel : ActivatableViewModelBase
|
|||||||
private readonly ObservableAsPropertyHelper<bool> _isAnonymous;
|
private readonly ObservableAsPropertyHelper<bool> _isAnonymous;
|
||||||
private readonly ILogger _logger;
|
private readonly ILogger _logger;
|
||||||
private readonly IWindowService _windowService;
|
private readonly IWindowService _windowService;
|
||||||
|
private readonly IRouter _router;
|
||||||
[Notify] private bool _allowLogout = true;
|
[Notify] private bool _allowLogout = true;
|
||||||
[Notify(Setter.Private)] private Bitmap? _avatar;
|
[Notify(Setter.Private)] private Bitmap? _avatar;
|
||||||
[Notify(Setter.Private)] private string? _email;
|
[Notify(Setter.Private)] private string? _email;
|
||||||
@ -31,11 +33,12 @@ public partial class CurrentUserViewModel : ActivatableViewModelBase
|
|||||||
[Notify(Setter.Private)] private string? _userId;
|
[Notify(Setter.Private)] private string? _userId;
|
||||||
[Notify(Setter.Private)] private string? _avatarUrl;
|
[Notify(Setter.Private)] private string? _avatarUrl;
|
||||||
|
|
||||||
public CurrentUserViewModel(ILogger logger, IAuthenticationService authenticationService, IWindowService windowService)
|
public CurrentUserViewModel(ILogger logger, IAuthenticationService authenticationService, IWindowService windowService, IRouter router)
|
||||||
{
|
{
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_authenticationService = authenticationService;
|
_authenticationService = authenticationService;
|
||||||
_windowService = windowService;
|
_windowService = windowService;
|
||||||
|
_router = router;
|
||||||
Login = ReactiveCommand.CreateFromTask(ExecuteLogin);
|
Login = ReactiveCommand.CreateFromTask(ExecuteLogin);
|
||||||
|
|
||||||
_isAnonymous = this.WhenAnyValue(vm => vm.Loading, vm => vm.Name, (l, n) => l || n == null).ToProperty(this, vm => vm.IsAnonymous);
|
_isAnonymous = this.WhenAnyValue(vm => vm.Loading, vm => vm.Name, (l, n) => l || n == null).ToProperty(this, vm => vm.IsAnonymous);
|
||||||
@ -92,4 +95,9 @@ public partial class CurrentUserViewModel : ActivatableViewModelBase
|
|||||||
Email = _authenticationService.Claims.FirstOrDefault(c => c.Type == "email")?.Value;
|
Email = _authenticationService.Claims.FirstOrDefault(c => c.Type == "email")?.Value;
|
||||||
AvatarUrl = $"{WorkshopConstants.AUTHORITY_URL}/user/avatar/{UserId}";
|
AvatarUrl = $"{WorkshopConstants.AUTHORITY_URL}/user/avatar/{UserId}";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void ManageAccount()
|
||||||
|
{
|
||||||
|
_router.Navigate("settings/account");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -8,6 +8,7 @@ using Artemis.UI.Shared.Services.Builders;
|
|||||||
using Artemis.UI.Shared.Utilities;
|
using Artemis.UI.Shared.Utilities;
|
||||||
using Artemis.WebClient.Workshop;
|
using Artemis.WebClient.Workshop;
|
||||||
using Artemis.WebClient.Workshop.Handlers.InstallationHandlers;
|
using Artemis.WebClient.Workshop.Handlers.InstallationHandlers;
|
||||||
|
using Artemis.WebClient.Workshop.Models;
|
||||||
using Artemis.WebClient.Workshop.Services;
|
using Artemis.WebClient.Workshop.Services;
|
||||||
using Humanizer;
|
using Humanizer;
|
||||||
using ReactiveUI;
|
using ReactiveUI;
|
||||||
|
|||||||
@ -7,6 +7,7 @@ using Artemis.Core.Services;
|
|||||||
using Artemis.UI.DryIoc.Factories;
|
using Artemis.UI.DryIoc.Factories;
|
||||||
using Artemis.UI.Screens.SurfaceEditor;
|
using Artemis.UI.Screens.SurfaceEditor;
|
||||||
using Artemis.UI.Shared;
|
using Artemis.UI.Shared;
|
||||||
|
using Artemis.WebClient.Workshop.Models;
|
||||||
using Artemis.WebClient.Workshop.Providers;
|
using Artemis.WebClient.Workshop.Providers;
|
||||||
using Artemis.WebClient.Workshop.Services;
|
using Artemis.WebClient.Workshop.Services;
|
||||||
using ReactiveUI;
|
using ReactiveUI;
|
||||||
|
|||||||
@ -12,6 +12,7 @@ using Artemis.UI.Screens.Workshop.Parameters;
|
|||||||
using Artemis.UI.Shared.Routing;
|
using Artemis.UI.Shared.Routing;
|
||||||
using Artemis.UI.Shared.Services;
|
using Artemis.UI.Shared.Services;
|
||||||
using Artemis.WebClient.Workshop;
|
using Artemis.WebClient.Workshop;
|
||||||
|
using Artemis.WebClient.Workshop.Models;
|
||||||
using Artemis.WebClient.Workshop.Services;
|
using Artemis.WebClient.Workshop.Services;
|
||||||
using PropertyChanged.SourceGenerator;
|
using PropertyChanged.SourceGenerator;
|
||||||
using StrawberryShake;
|
using StrawberryShake;
|
||||||
|
|||||||
@ -213,7 +213,7 @@ public partial class SubmissionDetailViewModel : RoutableScreen<WorkshopDetailPa
|
|||||||
{
|
{
|
||||||
using MemoryStream stream = new();
|
using MemoryStream stream = new();
|
||||||
EntrySpecificationsViewModel.IconBitmap.Save(stream);
|
EntrySpecificationsViewModel.IconBitmap.Save(stream);
|
||||||
ImageUploadResult imageResult = await _workshopService.SetEntryIcon(Entry.Id, stream, cancellationToken);
|
ApiResult imageResult = await _workshopService.SetEntryIcon(Entry.Id, stream, cancellationToken);
|
||||||
if (!imageResult.IsSuccess)
|
if (!imageResult.IsSuccess)
|
||||||
throw new ArtemisWorkshopException("Failed to upload image. " + imageResult.Message);
|
throw new ArtemisWorkshopException("Failed to upload image. " + imageResult.Message);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,6 +7,7 @@ using Artemis.UI.Shared.Routing;
|
|||||||
using Artemis.UI.Shared.Services;
|
using Artemis.UI.Shared.Services;
|
||||||
using Artemis.WebClient.Workshop;
|
using Artemis.WebClient.Workshop;
|
||||||
using Artemis.WebClient.Workshop.Handlers.InstallationHandlers;
|
using Artemis.WebClient.Workshop.Handlers.InstallationHandlers;
|
||||||
|
using Artemis.WebClient.Workshop.Models;
|
||||||
using Artemis.WebClient.Workshop.Services;
|
using Artemis.WebClient.Workshop.Services;
|
||||||
using PropertyChanged.SourceGenerator;
|
using PropertyChanged.SourceGenerator;
|
||||||
using ReactiveUI;
|
using ReactiveUI;
|
||||||
|
|||||||
@ -4,6 +4,7 @@ using System.Collections.ObjectModel;
|
|||||||
using System.Reactive;
|
using System.Reactive;
|
||||||
using System.Reactive.Linq;
|
using System.Reactive.Linq;
|
||||||
using Artemis.UI.Shared.Routing;
|
using Artemis.UI.Shared.Routing;
|
||||||
|
using Artemis.WebClient.Workshop.Models;
|
||||||
using Artemis.WebClient.Workshop.Services;
|
using Artemis.WebClient.Workshop.Services;
|
||||||
using DynamicData;
|
using DynamicData;
|
||||||
using DynamicData.Binding;
|
using DynamicData.Binding;
|
||||||
|
|||||||
@ -82,8 +82,8 @@ public partial class UploadStepViewModel : SubmissionViewModel
|
|||||||
FailureMessage = e.Message;
|
FailureMessage = e.Message;
|
||||||
Failed = true;
|
Failed = true;
|
||||||
|
|
||||||
// If something went wrong halfway through, delete the entry
|
// If something went wrong halfway through, delete the entry if it was new
|
||||||
if (_entryId != null)
|
if (State.EntryId == null && _entryId != null)
|
||||||
await _workshopClient.RemoveEntry.ExecuteAsync(_entryId.Value, CancellationToken.None);
|
await _workshopClient.RemoveEntry.ExecuteAsync(_entryId.Value, CancellationToken.None);
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
@ -129,11 +129,11 @@ public partial class UploadStepViewModel : SubmissionViewModel
|
|||||||
return entryId;
|
return entryId;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task TryImageUpload(Func<Task<ImageUploadResult>> action)
|
private async Task TryImageUpload(Func<Task<ApiResult>> action)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
ImageUploadResult result = await action();
|
ApiResult result = await action();
|
||||||
if (!result.IsSuccess)
|
if (!result.IsSuccess)
|
||||||
throw new ArtemisWorkshopException(result.Message);
|
throw new ArtemisWorkshopException(result.Message);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,6 +11,7 @@ public class DataModelEventCycleNode : Node<DataModelPathEntity, DataModelEventC
|
|||||||
private int _currentIndex;
|
private int _currentIndex;
|
||||||
private Type _currentType;
|
private Type _currentType;
|
||||||
private DataModelPath? _dataModelPath;
|
private DataModelPath? _dataModelPath;
|
||||||
|
private IDataModelEvent? _subscribedEvent;
|
||||||
private object? _lastPathValue;
|
private object? _lastPathValue;
|
||||||
private bool _updating;
|
private bool _updating;
|
||||||
|
|
||||||
@ -76,13 +77,19 @@ public class DataModelEventCycleNode : Node<DataModelPathEntity, DataModelEventC
|
|||||||
{
|
{
|
||||||
DataModelPath? old = _dataModelPath;
|
DataModelPath? old = _dataModelPath;
|
||||||
|
|
||||||
if (old?.GetValue() is IDataModelEvent oldEvent)
|
if (_subscribedEvent != null)
|
||||||
oldEvent.EventTriggered -= OnEventTriggered;
|
{
|
||||||
|
_subscribedEvent.EventTriggered -= OnEventTriggered;
|
||||||
|
_subscribedEvent = null;
|
||||||
|
}
|
||||||
|
|
||||||
_dataModelPath = Storage != null ? new DataModelPath(Storage) : null;
|
_dataModelPath = Storage != null ? new DataModelPath(Storage) : null;
|
||||||
|
|
||||||
if (_dataModelPath?.GetValue() is IDataModelEvent newEvent)
|
if (_dataModelPath?.GetValue() is IDataModelEvent newEvent)
|
||||||
newEvent.EventTriggered += OnEventTriggered;
|
{
|
||||||
|
_subscribedEvent = newEvent;
|
||||||
|
_subscribedEvent.EventTriggered += OnEventTriggered;
|
||||||
|
}
|
||||||
|
|
||||||
old?.Dispose();
|
old?.Dispose();
|
||||||
}
|
}
|
||||||
@ -153,8 +160,12 @@ public class DataModelEventCycleNode : Node<DataModelPathEntity, DataModelEventC
|
|||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
if (_dataModelPath?.GetValue() is IDataModelEvent newEvent)
|
if (_subscribedEvent != null)
|
||||||
newEvent.EventTriggered -= OnEventTriggered;
|
{
|
||||||
|
_subscribedEvent.EventTriggered -= OnEventTriggered;
|
||||||
|
_subscribedEvent = null;
|
||||||
|
}
|
||||||
|
|
||||||
_dataModelPath?.Dispose();
|
_dataModelPath?.Dispose();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -33,6 +33,11 @@ public static class ContainerExtensions
|
|||||||
.AddWorkshopClient()
|
.AddWorkshopClient()
|
||||||
.AddHttpMessageHandler<WorkshopClientStoreAccessor, AuthenticationDelegatingHandler>()
|
.AddHttpMessageHandler<WorkshopClientStoreAccessor, AuthenticationDelegatingHandler>()
|
||||||
.ConfigureHttpClient(client => client.BaseAddress = new Uri(WorkshopConstants.WORKSHOP_URL + "/graphql"));
|
.ConfigureHttpClient(client => client.BaseAddress = new Uri(WorkshopConstants.WORKSHOP_URL + "/graphql"));
|
||||||
|
|
||||||
|
serviceCollection.AddHttpClient(WorkshopConstants.IDENTITY_CLIENT_NAME)
|
||||||
|
.AddHttpMessageHandler<AuthenticationDelegatingHandler>()
|
||||||
|
.ConfigureHttpClient(client => client.BaseAddress = new Uri(WorkshopConstants.AUTHORITY_URL));
|
||||||
|
|
||||||
serviceCollection.AddHttpClient(WorkshopConstants.WORKSHOP_CLIENT_NAME)
|
serviceCollection.AddHttpClient(WorkshopConstants.WORKSHOP_CLIENT_NAME)
|
||||||
.AddHttpMessageHandler<AuthenticationDelegatingHandler>()
|
.AddHttpMessageHandler<AuthenticationDelegatingHandler>()
|
||||||
.ConfigureHttpClient(client => client.BaseAddress = new Uri(WorkshopConstants.WORKSHOP_URL));
|
.ConfigureHttpClient(client => client.BaseAddress = new Uri(WorkshopConstants.WORKSHOP_URL));
|
||||||
@ -49,6 +54,7 @@ public static class ContainerExtensions
|
|||||||
container.Register<IAuthenticationService, AuthenticationService>(Reuse.Singleton);
|
container.Register<IAuthenticationService, AuthenticationService>(Reuse.Singleton);
|
||||||
container.Register<IWorkshopService, WorkshopService>(Reuse.Singleton);
|
container.Register<IWorkshopService, WorkshopService>(Reuse.Singleton);
|
||||||
container.Register<ILayoutProvider, WorkshopLayoutProvider>(Reuse.Singleton);
|
container.Register<ILayoutProvider, WorkshopLayoutProvider>(Reuse.Singleton);
|
||||||
|
container.Register<IUserManagementService, UserManagementService>();
|
||||||
|
|
||||||
container.Register<EntryUploadHandlerFactory>(Reuse.Transient);
|
container.Register<EntryUploadHandlerFactory>(Reuse.Transient);
|
||||||
container.RegisterMany(workshopAssembly, type => type.IsAssignableTo<IEntryUploadHandler>(), Reuse.Transient);
|
container.RegisterMany(workshopAssembly, type => type.IsAssignableTo<IEntryUploadHandler>(), Reuse.Transient);
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
using Artemis.WebClient.Workshop.Services;
|
using Artemis.WebClient.Workshop.Models;
|
||||||
|
using Artemis.WebClient.Workshop.Services;
|
||||||
|
|
||||||
namespace Artemis.WebClient.Workshop.Handlers.InstallationHandlers;
|
namespace Artemis.WebClient.Workshop.Handlers.InstallationHandlers;
|
||||||
|
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
using Artemis.UI.Shared.Utilities;
|
using Artemis.UI.Shared.Utilities;
|
||||||
|
using Artemis.WebClient.Workshop.Models;
|
||||||
using Artemis.WebClient.Workshop.Services;
|
using Artemis.WebClient.Workshop.Services;
|
||||||
|
|
||||||
namespace Artemis.WebClient.Workshop.Handlers.InstallationHandlers;
|
namespace Artemis.WebClient.Workshop.Handlers.InstallationHandlers;
|
||||||
|
|||||||
@ -4,6 +4,7 @@ using Artemis.Core.Providers;
|
|||||||
using Artemis.Core.Services;
|
using Artemis.Core.Services;
|
||||||
using Artemis.UI.Shared.Extensions;
|
using Artemis.UI.Shared.Extensions;
|
||||||
using Artemis.UI.Shared.Utilities;
|
using Artemis.UI.Shared.Utilities;
|
||||||
|
using Artemis.WebClient.Workshop.Models;
|
||||||
using Artemis.WebClient.Workshop.Providers;
|
using Artemis.WebClient.Workshop.Providers;
|
||||||
using Artemis.WebClient.Workshop.Services;
|
using Artemis.WebClient.Workshop.Services;
|
||||||
|
|
||||||
|
|||||||
@ -2,6 +2,7 @@
|
|||||||
using Artemis.Core.Services;
|
using Artemis.Core.Services;
|
||||||
using Artemis.UI.Shared.Extensions;
|
using Artemis.UI.Shared.Extensions;
|
||||||
using Artemis.UI.Shared.Utilities;
|
using Artemis.UI.Shared.Utilities;
|
||||||
|
using Artemis.WebClient.Workshop.Models;
|
||||||
using Artemis.WebClient.Workshop.Services;
|
using Artemis.WebClient.Workshop.Services;
|
||||||
|
|
||||||
namespace Artemis.WebClient.Workshop.Handlers.InstallationHandlers;
|
namespace Artemis.WebClient.Workshop.Handlers.InstallationHandlers;
|
||||||
|
|||||||
@ -1,21 +1,21 @@
|
|||||||
namespace Artemis.WebClient.Workshop.Handlers.UploadHandlers;
|
namespace Artemis.WebClient.Workshop.Handlers.UploadHandlers;
|
||||||
|
|
||||||
public class ImageUploadResult
|
public class ApiResult
|
||||||
{
|
{
|
||||||
public bool IsSuccess { get; set; }
|
public bool IsSuccess { get; set; }
|
||||||
public string? Message { get; set; }
|
public string? Message { get; set; }
|
||||||
|
|
||||||
public static ImageUploadResult FromFailure(string? message)
|
public static ApiResult FromFailure(string? message)
|
||||||
{
|
{
|
||||||
return new ImageUploadResult
|
return new ApiResult
|
||||||
{
|
{
|
||||||
IsSuccess = false,
|
IsSuccess = false,
|
||||||
Message = message
|
Message = message
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public static ImageUploadResult FromSuccess()
|
public static ApiResult FromSuccess()
|
||||||
{
|
{
|
||||||
return new ImageUploadResult {IsSuccess = true};
|
return new ApiResult {IsSuccess = true};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1,8 +1,7 @@
|
|||||||
using Artemis.Core;
|
|
||||||
using Artemis.WebClient.Workshop.Exceptions;
|
using Artemis.WebClient.Workshop.Exceptions;
|
||||||
using IdentityModel.Client;
|
using IdentityModel.Client;
|
||||||
|
|
||||||
namespace Artemis.WebClient.Workshop.Services;
|
namespace Artemis.WebClient.Workshop.Models;
|
||||||
|
|
||||||
internal class AuthenticationToken
|
internal class AuthenticationToken
|
||||||
{
|
{
|
||||||
@ -2,7 +2,7 @@
|
|||||||
using Artemis.Core;
|
using Artemis.Core;
|
||||||
using Artemis.Storage.Entities.Workshop;
|
using Artemis.Storage.Entities.Workshop;
|
||||||
|
|
||||||
namespace Artemis.WebClient.Workshop.Services;
|
namespace Artemis.WebClient.Workshop.Models;
|
||||||
|
|
||||||
public class InstalledEntry
|
public class InstalledEntry
|
||||||
{
|
{
|
||||||
@ -0,0 +1,3 @@
|
|||||||
|
namespace Artemis.WebClient.Workshop.Models;
|
||||||
|
|
||||||
|
public record PersonalAccessToken(string Key, DateTime CreationTime, DateTime? Expiration, string? Description);
|
||||||
@ -1,5 +1,6 @@
|
|||||||
using Artemis.Core;
|
using Artemis.Core;
|
||||||
using Artemis.Core.Providers;
|
using Artemis.Core.Providers;
|
||||||
|
using Artemis.WebClient.Workshop.Models;
|
||||||
using Artemis.WebClient.Workshop.Services;
|
using Artemis.WebClient.Workshop.Services;
|
||||||
|
|
||||||
namespace Artemis.WebClient.Workshop.Providers;
|
namespace Artemis.WebClient.Workshop.Providers;
|
||||||
|
|||||||
@ -8,6 +8,7 @@ using System.Security.Cryptography;
|
|||||||
using System.Text;
|
using System.Text;
|
||||||
using Artemis.Core;
|
using Artemis.Core;
|
||||||
using Artemis.WebClient.Workshop.Exceptions;
|
using Artemis.WebClient.Workshop.Exceptions;
|
||||||
|
using Artemis.WebClient.Workshop.Models;
|
||||||
using Artemis.WebClient.Workshop.Repositories;
|
using Artemis.WebClient.Workshop.Repositories;
|
||||||
using DynamicData;
|
using DynamicData;
|
||||||
using IdentityModel;
|
using IdentityModel;
|
||||||
@ -122,7 +123,7 @@ internal class AuthenticationService : CorePropertyChanged, IAuthenticationServi
|
|||||||
public IObservable<Claim?> GetClaim(string type)
|
public IObservable<Claim?> GetClaim(string type)
|
||||||
{
|
{
|
||||||
return _claims.Connect()
|
return _claims.Connect()
|
||||||
.Filter(c => c.Type == JwtClaimTypes.Email)
|
.Filter(c => c.Type == type)
|
||||||
.ToCollection()
|
.ToCollection()
|
||||||
.Select(f => f.FirstOrDefault());
|
.Select(f => f.FirstOrDefault());
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,16 @@
|
|||||||
|
using Artemis.Core.Services;
|
||||||
|
using Artemis.WebClient.Workshop.Handlers.UploadHandlers;
|
||||||
|
using Artemis.WebClient.Workshop.Models;
|
||||||
|
|
||||||
|
namespace Artemis.WebClient.Workshop.Services;
|
||||||
|
|
||||||
|
public interface IUserManagementService : IProtectedArtemisService
|
||||||
|
{
|
||||||
|
Task<ApiResult> ChangePassword(string currentPassword, string newPassword, CancellationToken cancellationToken);
|
||||||
|
Task<ApiResult> ChangeEmailAddress(string emailAddress, CancellationToken cancellationToken);
|
||||||
|
Task<ApiResult> ChangeAvatar(Stream avatar, CancellationToken cancellationToken);
|
||||||
|
Task<ApiResult> RemoveAccount(CancellationToken cancellationToken);
|
||||||
|
Task<string> CreatePersonAccessToken(string description, DateTimeOffset expirationDate, CancellationToken cancellationToken);
|
||||||
|
Task<ApiResult> DeletePersonAccessToken(PersonalAccessToken personalAccessToken, CancellationToken cancellationToken);
|
||||||
|
Task<List<PersonalAccessToken>> GetPersonAccessTokens(CancellationToken cancellationToken);
|
||||||
|
}
|
||||||
@ -1,12 +1,13 @@
|
|||||||
using Artemis.WebClient.Workshop.Handlers.UploadHandlers;
|
using Artemis.WebClient.Workshop.Handlers.UploadHandlers;
|
||||||
|
using Artemis.WebClient.Workshop.Models;
|
||||||
|
|
||||||
namespace Artemis.WebClient.Workshop.Services;
|
namespace Artemis.WebClient.Workshop.Services;
|
||||||
|
|
||||||
public interface IWorkshopService
|
public interface IWorkshopService
|
||||||
{
|
{
|
||||||
Task<Stream?> GetEntryIcon(long entryId, CancellationToken cancellationToken);
|
Task<Stream?> GetEntryIcon(long entryId, CancellationToken cancellationToken);
|
||||||
Task<ImageUploadResult> SetEntryIcon(long entryId, Stream icon, CancellationToken cancellationToken);
|
Task<ApiResult> SetEntryIcon(long entryId, Stream icon, CancellationToken cancellationToken);
|
||||||
Task<ImageUploadResult> UploadEntryImage(long entryId, ImageUploadRequest request, CancellationToken cancellationToken);
|
Task<ApiResult> UploadEntryImage(long entryId, ImageUploadRequest request, CancellationToken cancellationToken);
|
||||||
Task DeleteEntryImage(Guid id, CancellationToken cancellationToken);
|
Task DeleteEntryImage(Guid id, CancellationToken cancellationToken);
|
||||||
Task<WorkshopStatus> GetWorkshopStatus(CancellationToken cancellationToken);
|
Task<WorkshopStatus> GetWorkshopStatus(CancellationToken cancellationToken);
|
||||||
Task<bool> ValidateWorkshopStatus(CancellationToken cancellationToken);
|
Task<bool> ValidateWorkshopStatus(CancellationToken cancellationToken);
|
||||||
|
|||||||
103
src/Artemis.WebClient.Workshop/Services/UserManagementService.cs
Normal file
103
src/Artemis.WebClient.Workshop/Services/UserManagementService.cs
Normal file
@ -0,0 +1,103 @@
|
|||||||
|
using System.Net.Http.Headers;
|
||||||
|
using System.Net.Http.Json;
|
||||||
|
using Artemis.WebClient.Workshop.Exceptions;
|
||||||
|
using Artemis.WebClient.Workshop.Handlers.UploadHandlers;
|
||||||
|
using Artemis.WebClient.Workshop.Models;
|
||||||
|
|
||||||
|
namespace Artemis.WebClient.Workshop.Services;
|
||||||
|
|
||||||
|
internal class UserManagementService : IUserManagementService
|
||||||
|
{
|
||||||
|
private readonly IHttpClientFactory _httpClientFactory;
|
||||||
|
|
||||||
|
public UserManagementService(IHttpClientFactory httpClientFactory)
|
||||||
|
{
|
||||||
|
_httpClientFactory = httpClientFactory;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<ApiResult> ChangePassword(string currentPassword, string newPassword, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
HttpClient client = _httpClientFactory.CreateClient(WorkshopConstants.IDENTITY_CLIENT_NAME);
|
||||||
|
HttpResponseMessage response = await client.PostAsync("user/credentials", JsonContent.Create(new {CurrentPassword = currentPassword, NewPassword = newPassword}), cancellationToken);
|
||||||
|
return response.IsSuccessStatusCode
|
||||||
|
? ApiResult.FromSuccess()
|
||||||
|
: ApiResult.FromFailure(await response.Content.ReadAsStringAsync(cancellationToken));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<ApiResult> ChangeEmailAddress(string emailAddress, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
HttpClient client = _httpClientFactory.CreateClient(WorkshopConstants.IDENTITY_CLIENT_NAME);
|
||||||
|
HttpResponseMessage response = await client.PostAsync("user/email", JsonContent.Create(new {EmailAddress = emailAddress}), cancellationToken);
|
||||||
|
return response.IsSuccessStatusCode
|
||||||
|
? ApiResult.FromSuccess()
|
||||||
|
: ApiResult.FromFailure(await response.Content.ReadAsStringAsync(cancellationToken));
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<ApiResult> ChangeAvatar(Stream avatar, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
avatar.Seek(0, SeekOrigin.Begin);
|
||||||
|
|
||||||
|
// Submit the archive
|
||||||
|
HttpClient client = _httpClientFactory.CreateClient(WorkshopConstants.IDENTITY_CLIENT_NAME);
|
||||||
|
|
||||||
|
// Construct the request
|
||||||
|
MultipartFormDataContent content = new();
|
||||||
|
StreamContent streamContent = new(avatar);
|
||||||
|
streamContent.Headers.ContentType = new MediaTypeHeaderValue("image/png");
|
||||||
|
content.Add(streamContent, "file", "file.png");
|
||||||
|
|
||||||
|
// Submit
|
||||||
|
HttpResponseMessage response = await client.PutAsync($"user/avatar", content, cancellationToken);
|
||||||
|
return response.IsSuccessStatusCode
|
||||||
|
? ApiResult.FromSuccess()
|
||||||
|
: ApiResult.FromFailure(await response.Content.ReadAsStringAsync(cancellationToken));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<ApiResult> RemoveAccount(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
HttpClient client = _httpClientFactory.CreateClient(WorkshopConstants.IDENTITY_CLIENT_NAME);
|
||||||
|
HttpResponseMessage response = await client.DeleteAsync("user", cancellationToken);
|
||||||
|
return response.IsSuccessStatusCode
|
||||||
|
? ApiResult.FromSuccess()
|
||||||
|
: ApiResult.FromFailure(await response.Content.ReadAsStringAsync(cancellationToken));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<string> CreatePersonAccessToken(string description, DateTimeOffset expirationDate, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
HttpClient client = _httpClientFactory.CreateClient(WorkshopConstants.IDENTITY_CLIENT_NAME);
|
||||||
|
HttpResponseMessage response = await client.PostAsync("user/access-token", JsonContent.Create(new {Description = description, ExpirationDate = expirationDate}), cancellationToken);
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
|
||||||
|
string? result = await response.Content.ReadFromJsonAsync<string>(cancellationToken: cancellationToken);
|
||||||
|
if (result == null)
|
||||||
|
throw new ArtemisWebClientException("Failed to deserialize access token");
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<ApiResult> DeletePersonAccessToken(PersonalAccessToken personalAccessToken, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
HttpClient client = _httpClientFactory.CreateClient(WorkshopConstants.IDENTITY_CLIENT_NAME);
|
||||||
|
HttpResponseMessage response = await client.DeleteAsync($"user/access-token/{personalAccessToken.Key}", cancellationToken);
|
||||||
|
return response.IsSuccessStatusCode
|
||||||
|
? ApiResult.FromSuccess()
|
||||||
|
: ApiResult.FromFailure(await response.Content.ReadAsStringAsync(cancellationToken));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<List<PersonalAccessToken>> GetPersonAccessTokens(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
HttpClient client = _httpClientFactory.CreateClient(WorkshopConstants.IDENTITY_CLIENT_NAME);
|
||||||
|
HttpResponseMessage response = await client.GetAsync("user/access-token", cancellationToken);
|
||||||
|
response.EnsureSuccessStatusCode();
|
||||||
|
|
||||||
|
List<PersonalAccessToken>? result = await response.Content.ReadFromJsonAsync<List<PersonalAccessToken>>(cancellationToken: cancellationToken);
|
||||||
|
if (result == null)
|
||||||
|
throw new ArtemisWebClientException("Failed to deserialize access tokens");
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -2,8 +2,8 @@ using System.Net.Http.Headers;
|
|||||||
using Artemis.Storage.Entities.Workshop;
|
using Artemis.Storage.Entities.Workshop;
|
||||||
using Artemis.Storage.Repositories.Interfaces;
|
using Artemis.Storage.Repositories.Interfaces;
|
||||||
using Artemis.UI.Shared.Routing;
|
using Artemis.UI.Shared.Routing;
|
||||||
using Artemis.UI.Shared.Utilities;
|
|
||||||
using Artemis.WebClient.Workshop.Handlers.UploadHandlers;
|
using Artemis.WebClient.Workshop.Handlers.UploadHandlers;
|
||||||
|
using Artemis.WebClient.Workshop.Models;
|
||||||
|
|
||||||
namespace Artemis.WebClient.Workshop.Services;
|
namespace Artemis.WebClient.Workshop.Services;
|
||||||
|
|
||||||
@ -36,7 +36,7 @@ public class WorkshopService : IWorkshopService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<ImageUploadResult> SetEntryIcon(long entryId, Stream icon, CancellationToken cancellationToken)
|
public async Task<ApiResult> SetEntryIcon(long entryId, Stream icon, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
icon.Seek(0, SeekOrigin.Begin);
|
icon.Seek(0, SeekOrigin.Begin);
|
||||||
|
|
||||||
@ -52,12 +52,12 @@ public class WorkshopService : IWorkshopService
|
|||||||
// Submit
|
// Submit
|
||||||
HttpResponseMessage response = await client.PostAsync($"entries/{entryId}/icon", content, cancellationToken);
|
HttpResponseMessage response = await client.PostAsync($"entries/{entryId}/icon", content, cancellationToken);
|
||||||
if (!response.IsSuccessStatusCode)
|
if (!response.IsSuccessStatusCode)
|
||||||
return ImageUploadResult.FromFailure($"{response.StatusCode} - {await response.Content.ReadAsStringAsync(cancellationToken)}");
|
return ApiResult.FromFailure($"{response.StatusCode} - {await response.Content.ReadAsStringAsync(cancellationToken)}");
|
||||||
return ImageUploadResult.FromSuccess();
|
return ApiResult.FromSuccess();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task<ImageUploadResult> UploadEntryImage(long entryId, ImageUploadRequest request, CancellationToken cancellationToken)
|
public async Task<ApiResult> UploadEntryImage(long entryId, ImageUploadRequest request, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
request.File.Seek(0, SeekOrigin.Begin);
|
request.File.Seek(0, SeekOrigin.Begin);
|
||||||
|
|
||||||
@ -76,8 +76,8 @@ public class WorkshopService : IWorkshopService
|
|||||||
// Submit
|
// Submit
|
||||||
HttpResponseMessage response = await client.PostAsync($"entries/{entryId}/image", content, cancellationToken);
|
HttpResponseMessage response = await client.PostAsync($"entries/{entryId}/image", content, cancellationToken);
|
||||||
if (!response.IsSuccessStatusCode)
|
if (!response.IsSuccessStatusCode)
|
||||||
return ImageUploadResult.FromFailure($"{response.StatusCode} - {await response.Content.ReadAsStringAsync(cancellationToken)}");
|
return ApiResult.FromFailure($"{response.StatusCode} - {await response.Content.ReadAsStringAsync(cancellationToken)}");
|
||||||
return ImageUploadResult.FromSuccess();
|
return ApiResult.FromSuccess();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
|
|||||||
@ -6,5 +6,6 @@ public static class WorkshopConstants
|
|||||||
// public const string WORKSHOP_URL = "https://localhost:7281";
|
// public const string WORKSHOP_URL = "https://localhost:7281";
|
||||||
public const string AUTHORITY_URL = "https://identity.artemis-rgb.com";
|
public const string AUTHORITY_URL = "https://identity.artemis-rgb.com";
|
||||||
public const string WORKSHOP_URL = "https://workshop.artemis-rgb.com";
|
public const string WORKSHOP_URL = "https://workshop.artemis-rgb.com";
|
||||||
|
public const string IDENTITY_CLIENT_NAME = "IdentityApiClient";
|
||||||
public const string WORKSHOP_CLIENT_NAME = "WorkshopApiClient";
|
public const string WORKSHOP_CLIENT_NAME = "WorkshopApiClient";
|
||||||
}
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user