diff --git a/src/Artemis.Core/Services/Interfaces/IPluginManagementService.cs b/src/Artemis.Core/Services/Interfaces/IPluginManagementService.cs
index 8e0faf564..bbc4afe7a 100644
--- a/src/Artemis.Core/Services/Interfaces/IPluginManagementService.cs
+++ b/src/Artemis.Core/Services/Interfaces/IPluginManagementService.cs
@@ -12,6 +12,11 @@ namespace Artemis.Core.Services;
///
public interface IPluginManagementService : IArtemisService, IDisposable
{
+ ///
+ /// Gets a list containing additional directories in which plugins are located, used while loading plugins.
+ ///
+ List AdditionalPluginDirectories { get; }
+
///
/// Indicates whether or not plugins are currently being loaded
///
diff --git a/src/Artemis.Core/Services/PluginManagementService.cs b/src/Artemis.Core/Services/PluginManagementService.cs
index 03a4a1269..75cd31cb6 100644
--- a/src/Artemis.Core/Services/PluginManagementService.cs
+++ b/src/Artemis.Core/Services/PluginManagementService.cs
@@ -78,8 +78,11 @@ internal class PluginManagementService : IPluginManagementService
File.Create(Path.Combine(pluginDirectory.FullName, "artemis.lock")).Close();
}
+ public List AdditionalPluginDirectories { get; } = new();
+
public bool LoadingPlugins { get; private set; }
+
#region Built in plugins
public void CopyBuiltInPlugins()
@@ -276,6 +279,18 @@ internal class PluginManagementService : IPluginManagementService
}
}
+ foreach (DirectoryInfo directory in AdditionalPluginDirectories)
+ {
+ try
+ {
+ LoadPlugin(directory);
+ }
+ catch (Exception e)
+ {
+ _logger.Warning(new ArtemisPluginException($"Failed to load plugin at {directory}", e), "Plugin exception");
+ }
+ }
+
// ReSharper disable InconsistentlySynchronizedField - It's read-only, idc
_logger.Debug("Loaded {count} plugin(s)", _plugins.Count);
diff --git a/src/Artemis.UI/Artemis.UI.csproj b/src/Artemis.UI/Artemis.UI.csproj
index 0048cffb7..ec42bd185 100644
--- a/src/Artemis.UI/Artemis.UI.csproj
+++ b/src/Artemis.UI/Artemis.UI.csproj
@@ -35,4 +35,15 @@
+
+
+
+ DeviceProviderPickerDialogView.axaml
+ Code
+
+
+ DeviceSelectionDialogView.axaml
+ Code
+
+
\ No newline at end of file
diff --git a/src/Artemis.UI/Routing/Routes.cs b/src/Artemis.UI/Routing/Routes.cs
index 8e26f8d3b..4353a4eef 100644
--- a/src/Artemis.UI/Routing/Routes.cs
+++ b/src/Artemis.UI/Routing/Routes.cs
@@ -12,9 +12,9 @@ using Artemis.UI.Screens.Workshop.Home;
using Artemis.UI.Screens.Workshop.Layout;
using Artemis.UI.Screens.Workshop.Library;
using Artemis.UI.Screens.Workshop.Library.Tabs;
-using Artemis.UI.Screens.Workshop.Plugin;
using Artemis.UI.Screens.Workshop.Profile;
using Artemis.UI.Shared.Routing;
+using PluginDetailsViewModel = Artemis.UI.Screens.Workshop.Plugins.PluginDetailsViewModel;
namespace Artemis.UI.Routing;
diff --git a/src/Artemis.UI/Screens/Plugins/Dialogs/PluginPrerequisitesInstallDialogView.axaml b/src/Artemis.UI/Screens/Plugins/Dialogs/PluginPrerequisitesInstallDialogView.axaml
index 06fe3c015..efa8af53e 100644
--- a/src/Artemis.UI/Screens/Plugins/Dialogs/PluginPrerequisitesInstallDialogView.axaml
+++ b/src/Artemis.UI/Screens/Plugins/Dialogs/PluginPrerequisitesInstallDialogView.axaml
@@ -24,7 +24,7 @@
-
+
@@ -36,7 +36,7 @@
IsHitTestVisible="False">
-
+
@@ -45,8 +45,8 @@
-
-
+
+
@@ -74,12 +74,14 @@
Grid.Column="1"
Margin="10 0"
IsVisible="{CompiledBinding ShowFailed, Mode=OneWay}">
-
- Installing
-
- failed.
-
- You may try again to see if that helps, otherwise install the prerequisite manually or contact the plugin developer.
+
+ Installing
+
+ failed.
+
+
+ You may try again to see if that helps, otherwise install the prerequisite manually or contact the plugin developer.
+
\ No newline at end of file
diff --git a/src/Artemis.UI/Screens/Plugins/Dialogs/PluginPrerequisitesInstallDialogViewModel.cs b/src/Artemis.UI/Screens/Plugins/Dialogs/PluginPrerequisitesInstallDialogViewModel.cs
index c01733e92..30db350ca 100644
--- a/src/Artemis.UI/Screens/Plugins/Dialogs/PluginPrerequisitesInstallDialogViewModel.cs
+++ b/src/Artemis.UI/Screens/Plugins/Dialogs/PluginPrerequisitesInstallDialogViewModel.cs
@@ -30,16 +30,21 @@ public partial class PluginPrerequisitesInstallDialogViewModel : ContentDialogVi
[Notify] private bool _showIntro = true;
[Notify] private bool _showProgress;
+ private bool _finished;
+
public PluginPrerequisitesInstallDialogViewModel(List subjects, IPrerequisitesVmFactory prerequisitesVmFactory)
{
Prerequisites = new ObservableCollection();
foreach (PluginPrerequisite prerequisite in subjects.SelectMany(prerequisitesSubject => prerequisitesSubject.PlatformPrerequisites))
Prerequisites.Add(prerequisitesVmFactory.PluginPrerequisiteViewModel(prerequisite, false));
- Install = ReactiveCommand.CreateFromTask(ExecuteInstall, this.WhenAnyValue(vm => vm.CanInstall));
+ Install = ReactiveCommand.Create(ExecuteInstall, this.WhenAnyValue(vm => vm.CanInstall));
Dispatcher.UIThread.Post(() => CanInstall = Prerequisites.Any(p => !p.PluginPrerequisite.IsMet()), DispatcherPriority.Background);
this.WhenActivated(d =>
{
+ if (ContentDialog != null)
+ ContentDialog.Closing += ContentDialogOnClosing;
+
Disposable.Create(() =>
{
_tokenSource?.Cancel();
@@ -51,11 +56,12 @@ public partial class PluginPrerequisitesInstallDialogViewModel : ContentDialogVi
public ReactiveCommand Install { get; }
public ObservableCollection Prerequisites { get; }
-
+
public static async Task Show(IWindowService windowService, List subjects)
{
await windowService.CreateContentDialog()
.WithTitle("Plugin prerequisites")
+ .WithFullScreen()
.WithViewModel(out PluginPrerequisitesInstallDialogViewModel vm, subjects)
.WithCloseButtonText("Cancel")
.HavingPrimaryButton(b => b.WithText("Install").WithCommand(vm.Install))
@@ -63,12 +69,8 @@ public partial class PluginPrerequisitesInstallDialogViewModel : ContentDialogVi
.ShowAsync();
}
- private async Task ExecuteInstall()
+ private void ExecuteInstall()
{
- Deferral? deferral = null;
- if (ContentDialog != null)
- ContentDialog.Closing += (_, args) => deferral = args.GetDeferral();
-
CanInstall = false;
ShowFailed = false;
ShowIntro = false;
@@ -77,6 +79,11 @@ public partial class PluginPrerequisitesInstallDialogViewModel : ContentDialogVi
_tokenSource?.Dispose();
_tokenSource = new CancellationTokenSource();
+ Dispatcher.UIThread.InvokeAsync(async () => await InstallPrerequisites(_tokenSource.Token));
+ }
+
+ private async Task InstallPrerequisites(CancellationToken cancellationToken)
+ {
try
{
foreach (PluginPrerequisiteViewModel pluginPrerequisiteViewModel in Prerequisites)
@@ -86,7 +93,9 @@ public partial class PluginPrerequisitesInstallDialogViewModel : ContentDialogVi
continue;
ActivePrerequisite = pluginPrerequisiteViewModel;
- await ActivePrerequisite.Install(_tokenSource.Token);
+ await ActivePrerequisite.Install(cancellationToken);
+
+ cancellationToken.ThrowIfCancellationRequested();
if (!ActivePrerequisite.IsMet)
{
@@ -98,19 +107,33 @@ public partial class PluginPrerequisitesInstallDialogViewModel : ContentDialogVi
// Wait after the task finished for the user to process what happened
if (pluginPrerequisiteViewModel != Prerequisites.Last())
- await Task.Delay(250);
+ await Task.Delay(250, cancellationToken);
else
- await Task.Delay(1000);
+ await Task.Delay(1000, cancellationToken);
}
- if (deferral != null)
- deferral.Complete();
- else
- ContentDialog?.Hide(ContentDialogResult.Primary);
+ _finished = true;
+ ContentDialog?.Hide(ContentDialogResult.Primary);
}
- catch (OperationCanceledException)
+ catch (TaskCanceledException e)
{
// ignored
}
}
+
+ private void ContentDialogOnClosing(ContentDialog sender, ContentDialogClosingEventArgs args)
+ {
+ // Cancel button is allowed to close
+ if (args.Result == ContentDialogResult.None)
+ {
+ _tokenSource?.Cancel();
+ _tokenSource?.Dispose();
+ _tokenSource = null;
+ }
+ else
+ {
+ // Keep dialog open until either ready
+ args.Cancel = !_finished;
+ }
+ }
}
\ No newline at end of file
diff --git a/src/Artemis.UI/Screens/Plugins/Dialogs/PluginPrerequisitesUninstallDialogView.axaml b/src/Artemis.UI/Screens/Plugins/Dialogs/PluginPrerequisitesUninstallDialogView.axaml
index b25fbc844..3ba9bec0c 100644
--- a/src/Artemis.UI/Screens/Plugins/Dialogs/PluginPrerequisitesUninstallDialogView.axaml
+++ b/src/Artemis.UI/Screens/Plugins/Dialogs/PluginPrerequisitesUninstallDialogView.axaml
@@ -24,7 +24,7 @@
-
+
@@ -37,8 +37,8 @@
-
-
+
+
diff --git a/src/Artemis.UI/Screens/Plugins/Dialogs/PluginPrerequisitesUninstallDialogViewModel.cs b/src/Artemis.UI/Screens/Plugins/Dialogs/PluginPrerequisitesUninstallDialogViewModel.cs
index 60264ae54..fc6a88aff 100644
--- a/src/Artemis.UI/Screens/Plugins/Dialogs/PluginPrerequisitesUninstallDialogViewModel.cs
+++ b/src/Artemis.UI/Screens/Plugins/Dialogs/PluginPrerequisitesUninstallDialogViewModel.cs
@@ -64,6 +64,7 @@ public partial class PluginPrerequisitesUninstallDialogViewModel : ContentDialog
public static async Task Show(IWindowService windowService, List subjects, string cancelLabel = "Cancel")
{
await windowService.CreateContentDialog()
+ .WithFullScreen()
.WithTitle("Plugin prerequisites")
.WithViewModel(out PluginPrerequisitesUninstallDialogViewModel vm, subjects)
.WithCloseButtonText(cancelLabel)
diff --git a/src/Artemis.UI/Screens/Plugins/PluginViewModel.cs b/src/Artemis.UI/Screens/Plugins/PluginViewModel.cs
index c01e2dfc2..1a0333aca 100644
--- a/src/Artemis.UI/Screens/Plugins/PluginViewModel.cs
+++ b/src/Artemis.UI/Screens/Plugins/PluginViewModel.cs
@@ -11,6 +11,8 @@ using Artemis.UI.Exceptions;
using Artemis.UI.Shared;
using Artemis.UI.Shared.Services;
using Artemis.UI.Shared.Services.Builders;
+using Artemis.WebClient.Workshop.Models;
+using Artemis.WebClient.Workshop.Services;
using Avalonia.Controls;
using Avalonia.Threading;
using Material.Icons;
@@ -24,6 +26,7 @@ public partial class PluginViewModel : ActivatableViewModelBase
private readonly ICoreService _coreService;
private readonly INotificationService _notificationService;
private readonly IPluginManagementService _pluginManagementService;
+ private readonly IWorkshopService _workshopService;
private readonly IWindowService _windowService;
private Window? _settingsWindow;
[Notify] private bool _canInstallPrerequisites;
@@ -36,13 +39,15 @@ public partial class PluginViewModel : ActivatableViewModelBase
ICoreService coreService,
IWindowService windowService,
INotificationService notificationService,
- IPluginManagementService pluginManagementService)
+ IPluginManagementService pluginManagementService,
+ IWorkshopService workshopService)
{
_plugin = plugin;
_coreService = coreService;
_windowService = windowService;
_notificationService = notificationService;
_pluginManagementService = pluginManagementService;
+ _workshopService = workshopService;
Platforms = new ObservableCollection();
if (Plugin.Info.Platforms != null)
@@ -90,7 +95,7 @@ public partial class PluginViewModel : ActivatableViewModelBase
public ObservableCollection Platforms { get; }
public string Type => Plugin.GetType().BaseType?.Name ?? Plugin.GetType().Name;
public bool IsEnabled => Plugin.IsEnabled;
-
+
public async Task UpdateEnabled(bool enable)
{
if (Enabling)
@@ -209,7 +214,7 @@ public partial class PluginViewModel : ActivatableViewModelBase
await PluginPrerequisitesInstallDialogViewModel.Show(_windowService, subjects);
}
- private async Task ExecuteRemovePrerequisites(bool forPluginRemoval = false)
+ public async Task ExecuteRemovePrerequisites(bool forPluginRemoval = false)
{
List subjects = new() {Plugin.Info};
subjects.AddRange(!forPluginRemoval ? Plugin.Features.Where(f => f.AlwaysEnabled) : Plugin.Features);
@@ -244,9 +249,6 @@ public partial class PluginViewModel : ActivatableViewModelBase
return;
// If the plugin or any of its features has uninstall actions, offer to run these
- List subjects = new() {Plugin.Info};
- subjects.AddRange(Plugin.Features);
- if (subjects.Any(s => s.PlatformPrerequisites.Any(p => p.UninstallActions.Any())))
await ExecuteRemovePrerequisites(true);
try
@@ -259,6 +261,10 @@ public partial class PluginViewModel : ActivatableViewModelBase
throw;
}
+ InstalledEntry? entry = _workshopService.GetInstalledEntries().FirstOrDefault(e => e.TryGetMetadata("PluginId", out Guid pluginId) && pluginId == Plugin.Guid);
+ if (entry != null)
+ _workshopService.RemoveInstalledEntry(entry);
+
_notificationService.CreateNotification().WithTitle("Removed plugin.").Show();
}
@@ -273,7 +279,7 @@ public partial class PluginViewModel : ActivatableViewModelBase
_windowService.ShowExceptionDialog("Welp, we couldn\'t open the logs folder for you", e);
}
}
-
+
private async Task ShowUpdateEnableFailure(bool enable, Exception e)
{
string action = enable ? "enable" : "disable";
diff --git a/src/Artemis.UI/Screens/Root/RootViewModel.cs b/src/Artemis.UI/Screens/Root/RootViewModel.cs
index ce83f0742..f97399a7e 100644
--- a/src/Artemis.UI/Screens/Root/RootViewModel.cs
+++ b/src/Artemis.UI/Screens/Root/RootViewModel.cs
@@ -66,7 +66,7 @@ public class RootViewModel : RoutableHostScreen, IMainWindowProv
OpenScreen = ReactiveCommand.Create(ExecuteOpenScreen);
OpenDebugger = ReactiveCommand.CreateFromTask(ExecuteOpenDebugger);
Exit = ReactiveCommand.CreateFromTask(ExecuteExit);
-
+
_titleBarViewModel = this.WhenAnyValue(vm => vm.Screen)
.Select(s => s as IMainScreenViewModel)
.Select(s => s?.WhenAnyValue(svm => svm.TitleBarViewModel) ?? Observable.Never())
@@ -76,13 +76,15 @@ public class RootViewModel : RoutableHostScreen, IMainWindowProv
Task.Run(() =>
{
+ // Before doing heavy lifting, initialize the update service which may prompt a restart
if (_updateService.Initialize())
return;
-
- // Before initializing the core and files become in use, clean up orphaned files
- workshopService.RemoveOrphanedFiles();
+ // Workshop service goes first so it has a chance to clean up old workshop entries and introduce new ones
+ workshopService.Initialize();
+ // Core is initialized now that everything is ready to go
coreService.Initialize();
+
registrationService.RegisterBuiltInDataModelDisplays();
registrationService.RegisterBuiltInDataModelInputs();
registrationService.RegisterBuiltInPropertyEditors();
@@ -140,7 +142,7 @@ public class RootViewModel : RoutableHostScreen, IMainWindowProv
{
if (path != null)
_router.ClearPreviousWindowRoute();
-
+
// The window will open on the UI thread at some point, respond to that to select the chosen screen
MainWindowOpened += OnEventHandler;
OpenMainWindow();
@@ -189,7 +191,7 @@ public class RootViewModel : RoutableHostScreen, IMainWindowProv
_lifeTime.MainWindow.Activate();
if (_lifeTime.MainWindow.WindowState == WindowState.Minimized)
_lifeTime.MainWindow.WindowState = WindowState.Normal;
-
+
OnMainWindowOpened();
}
diff --git a/src/Artemis.UI/Screens/Settings/Account/CreatePersonalAccessTokenView.axaml b/src/Artemis.UI/Screens/Settings/Account/CreatePersonalAccessTokenView.axaml
new file mode 100644
index 000000000..184242179
--- /dev/null
+++ b/src/Artemis.UI/Screens/Settings/Account/CreatePersonalAccessTokenView.axaml
@@ -0,0 +1,19 @@
+
+
+
+
+
+
+
+
+
diff --git a/src/Artemis.UI/Screens/Settings/Account/CreatePersonalAccessTokenView.axaml.cs b/src/Artemis.UI/Screens/Settings/Account/CreatePersonalAccessTokenView.axaml.cs
new file mode 100644
index 000000000..3b359d1bd
--- /dev/null
+++ b/src/Artemis.UI/Screens/Settings/Account/CreatePersonalAccessTokenView.axaml.cs
@@ -0,0 +1,14 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Markup.Xaml;
+using Avalonia.ReactiveUI;
+
+namespace Artemis.UI.Screens.Settings.Account;
+
+public partial class CreatePersonalAccessTokenView : ReactiveUserControl
+{
+ public CreatePersonalAccessTokenView()
+ {
+ InitializeComponent();
+ }
+}
\ No newline at end of file
diff --git a/src/Artemis.UI/Screens/Settings/Account/CreatePersonalAccessTokenViewModel.cs b/src/Artemis.UI/Screens/Settings/Account/CreatePersonalAccessTokenViewModel.cs
new file mode 100644
index 000000000..a09ae4a21
--- /dev/null
+++ b/src/Artemis.UI/Screens/Settings/Account/CreatePersonalAccessTokenViewModel.cs
@@ -0,0 +1,46 @@
+using System;
+using System.Reactive;
+using System.Threading;
+using System.Threading.Tasks;
+using Artemis.UI.Shared;
+using Artemis.UI.Shared.Services;
+using Artemis.WebClient.Workshop.Services;
+using PropertyChanged.SourceGenerator;
+using ReactiveUI;
+using ReactiveUI.Validation.Extensions;
+
+namespace Artemis.UI.Screens.Settings.Account;
+
+public partial class CreatePersonalAccessTokenViewModel : ContentDialogViewModelBase
+{
+ private readonly IUserManagementService _userManagementService;
+ private readonly IWindowService _windowService;
+ [Notify] private string _description = string.Empty;
+ [Notify] private DateTime _expirationDate = DateTime.Today.AddDays(180);
+
+ public CreatePersonalAccessTokenViewModel(IUserManagementService userManagementService, IWindowService windowService)
+ {
+ _userManagementService = userManagementService;
+ _windowService = windowService;
+ Submit = ReactiveCommand.CreateFromTask(ExecuteSubmit, ValidationContext.Valid);
+
+ this.ValidationRule(vm => vm.Description, e => e != null, "You must specify a description");
+ this.ValidationRule(vm => vm.Description, e => e == null || e.Length >= 10, "You must specify a description of at least 10 characters");
+ this.ValidationRule(vm => vm.Description, e => e == null || e.Length <= 150, "You must specify a description of less than 150 characters");
+ this.ValidationRule(vm => vm.ExpirationDate, e => e >= DateTime.Today.AddDays(1), "Expiration date must be at least 24 hours from now");
+ }
+
+ public DateTime StartDate => DateTime.Today.AddDays(1);
+ public DateTime EndDate => DateTime.Today.AddDays(365);
+ public ReactiveCommand Submit { get; }
+
+ private async Task ExecuteSubmit(CancellationToken cts)
+ {
+ string result = await _userManagementService.CreatePersonAccessToken(Description, ExpirationDate, cts);
+ await _windowService.CreateContentDialog()
+ .WithTitle("Personal Access Token")
+ .WithViewModel(out PersonalAccessTokenViewModel _, result)
+ .WithFullScreen()
+ .ShowAsync();
+ }
+}
\ No newline at end of file
diff --git a/src/Artemis.UI/Screens/Settings/Account/PersonalAccessTokenView.axaml b/src/Artemis.UI/Screens/Settings/Account/PersonalAccessTokenView.axaml
new file mode 100644
index 000000000..c68e4e024
--- /dev/null
+++ b/src/Artemis.UI/Screens/Settings/Account/PersonalAccessTokenView.axaml
@@ -0,0 +1,19 @@
+
+
+
+ Your token has been created, please copy it now as you cannot view it again later.
+
+
+
+
diff --git a/src/Artemis.UI/Screens/Settings/Account/PersonalAccessTokenView.axaml.cs b/src/Artemis.UI/Screens/Settings/Account/PersonalAccessTokenView.axaml.cs
new file mode 100644
index 000000000..d9110ec53
--- /dev/null
+++ b/src/Artemis.UI/Screens/Settings/Account/PersonalAccessTokenView.axaml.cs
@@ -0,0 +1,14 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Markup.Xaml;
+using Avalonia.ReactiveUI;
+
+namespace Artemis.UI.Screens.Settings.Account;
+
+public partial class PersonalAccessTokenView : ReactiveUserControl
+{
+ public PersonalAccessTokenView()
+ {
+ InitializeComponent();
+ }
+}
\ No newline at end of file
diff --git a/src/Artemis.UI/Screens/Settings/Account/PersonalAccessTokenViewModel.cs b/src/Artemis.UI/Screens/Settings/Account/PersonalAccessTokenViewModel.cs
new file mode 100644
index 000000000..4ae480037
--- /dev/null
+++ b/src/Artemis.UI/Screens/Settings/Account/PersonalAccessTokenViewModel.cs
@@ -0,0 +1,13 @@
+using Artemis.UI.Shared;
+
+namespace Artemis.UI.Screens.Settings.Account;
+
+public class PersonalAccessTokenViewModel : ContentDialogViewModelBase
+{
+ public string Token { get; }
+
+ public PersonalAccessTokenViewModel(string token)
+ {
+ Token = token;
+ }
+}
\ No newline at end of file
diff --git a/src/Artemis.UI/Screens/Settings/Tabs/AccountTabView.axaml b/src/Artemis.UI/Screens/Settings/Tabs/AccountTabView.axaml
index c27ce59bc..3e54e59dc 100644
--- a/src/Artemis.UI/Screens/Settings/Tabs/AccountTabView.axaml
+++ b/src/Artemis.UI/Screens/Settings/Tabs/AccountTabView.axaml
@@ -5,36 +5,41 @@
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"
+ xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
+ xmlns:models="clr-namespace:Artemis.WebClient.Workshop.Models;assembly=Artemis.WebClient.Workshop"
+ xmlns:converters="clr-namespace:Artemis.UI.Converters"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="800"
x:Class="Artemis.UI.Screens.Settings.AccountTabView"
x:DataType="settings:AccountTabViewModel">
+
+
+
-
-
-
-
-
-
- You are not logged in
-
- In order to manage your account you must be logged in.
-
- Creating an account is free and we'll not bother you with a newsletter or crap like that.
-
-
-
-
-
- Click Log In below to (create an account) and log in.
-
- You'll also be able to log in with Google or Discord.
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
@@ -46,7 +51,7 @@
-
+
@@ -66,8 +71,8 @@
-
-
+
+
@@ -99,16 +104,45 @@
-
-
-
-
-
-
-
-
+
+ Personal access tokens
+
+
+
+
+
+ Description
+ Created at
+ Expires at
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
\ No newline at end of file
diff --git a/src/Artemis.UI/Screens/Settings/Tabs/AccountTabViewModel.cs b/src/Artemis.UI/Screens/Settings/Tabs/AccountTabViewModel.cs
index 0e361deb3..afab57079 100644
--- a/src/Artemis.UI/Screens/Settings/Tabs/AccountTabViewModel.cs
+++ b/src/Artemis.UI/Screens/Settings/Tabs/AccountTabViewModel.cs
@@ -1,7 +1,10 @@
using System;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
using System.ComponentModel;
using System.IO;
using System.Linq;
+using System.Reactive;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using System.Threading;
@@ -12,6 +15,7 @@ using Artemis.UI.Shared.Routing;
using Artemis.UI.Shared.Services;
using Artemis.WebClient.Workshop;
using Artemis.WebClient.Workshop.Handlers.UploadHandlers;
+using Artemis.WebClient.Workshop.Models;
using Artemis.WebClient.Workshop.Services;
using IdentityModel;
using PropertyChanged.SourceGenerator;
@@ -29,6 +33,7 @@ public partial class AccountTabViewModel : RoutableScreen
[Notify(Setter.Private)] private string? _name;
[Notify(Setter.Private)] private string? _email;
[Notify(Setter.Private)] private string? _avatarUrl;
+ [Notify(Setter.Private)] private ObservableCollection _personalAccessTokens = new();
public AccountTabViewModel(IWindowService windowService, IAuthenticationService authenticationService, IUserManagementService userManagementService)
{
@@ -36,10 +41,11 @@ public partial class AccountTabViewModel : RoutableScreen
_authenticationService = authenticationService;
_userManagementService = userManagementService;
_authenticationService.AutoLogin(true);
-
+
DisplayName = "Account";
IsLoggedIn = _authenticationService.IsLoggedIn;
-
+ DeleteToken = ReactiveCommand.CreateFromTask(ExecuteDeleteToken);
+
this.WhenActivated(d =>
{
_canChangePassword = _authenticationService.GetClaim(JwtClaimTypes.AuthenticationMethod).Select(c => c?.Value == "pwd").ToProperty(this, vm => vm.CanChangePassword);
@@ -50,12 +56,13 @@ public partial class AccountTabViewModel : RoutableScreen
public bool CanChangePassword => _canChangePassword?.Value ?? false;
public IObservable IsLoggedIn { get; }
+ public ReactiveCommand DeleteToken { 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();
@@ -85,7 +92,7 @@ public partial class AccountTabViewModel : RoutableScreen
.HavingPrimaryButton(b => b.WithText("Submit").WithCommand(vm.Submit))
.ShowAsync();
}
-
+
public async Task ChangePasswordAddress()
{
await _windowService.CreateContentDialog().WithTitle("Change password")
@@ -94,7 +101,7 @@ public partial class AccountTabViewModel : RoutableScreen
.HavingPrimaryButton(b => b.WithText("Submit").WithCommand(vm.Submit))
.ShowAsync();
}
-
+
public async Task RemoveAccount()
{
await _windowService.CreateContentDialog().WithTitle("Remove account")
@@ -104,11 +111,36 @@ public partial class AccountTabViewModel : RoutableScreen
.ShowAsync();
}
- private void LoadCurrentUser()
+ public async Task GenerateToken()
+ {
+ await _windowService.CreateContentDialog().WithTitle("Create Personal Access Token")
+ .WithViewModel(out CreatePersonalAccessTokenViewModel vm)
+ .WithCloseButtonText("Cancel")
+ .HavingPrimaryButton(b => b.WithText("Submit").WithCommand(vm.Submit))
+ .ShowAsync();
+
+ List personalAccessTokens = await _userManagementService.GetPersonAccessTokens(CancellationToken.None);
+ PersonalAccessTokens = new ObservableCollection(personalAccessTokens);
+ }
+
+ private async Task ExecuteDeleteToken(PersonalAccessToken token)
+ {
+ bool confirm = await _windowService.ShowConfirmContentDialog("Delete Personal Access Token", "Are you sure you want to delete this token? Any services using it will stop working");
+ if (!confirm)
+ return;
+
+ await _userManagementService.DeletePersonAccessToken(token, CancellationToken.None);
+ PersonalAccessTokens.Remove(token);
+ }
+
+ private async Task 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}";
+
+ List personalAccessTokens = await _userManagementService.GetPersonAccessTokens(CancellationToken.None);
+ PersonalAccessTokens = new ObservableCollection(personalAccessTokens);
}
}
\ No newline at end of file
diff --git a/src/Artemis.UI/Screens/Sidebar/SidebarViewModel.cs b/src/Artemis.UI/Screens/Sidebar/SidebarViewModel.cs
index bb2d56f95..3b7de1c0c 100644
--- a/src/Artemis.UI/Screens/Sidebar/SidebarViewModel.cs
+++ b/src/Artemis.UI/Screens/Sidebar/SidebarViewModel.cs
@@ -43,7 +43,7 @@ public partial class SidebarViewModel : ActivatableViewModelBase
{
new(MaterialIconKind.FolderVideo, "Profiles", "workshop/entries/profiles/1", "workshop/entries/profiles"),
new(MaterialIconKind.KeyboardVariant, "Layouts", "workshop/entries/layouts/1", "workshop/entries/layouts"),
- new(MaterialIconKind.Plugin, "Plugins", "workshop/entries/plugins/1", "workshop/entries/plugins"),
+ new(MaterialIconKind.Connection, "Plugins", "workshop/entries/plugins/1", "workshop/entries/plugins"),
new(MaterialIconKind.Bookshelf, "Library", "workshop/library"),
}),
diff --git a/src/Artemis.UI/Screens/Workshop/Entries/Details/EntryReleasesViewModel.cs b/src/Artemis.UI/Screens/Workshop/Entries/Details/EntryReleasesViewModel.cs
index 311164269..21be13b28 100644
--- a/src/Artemis.UI/Screens/Workshop/Entries/Details/EntryReleasesViewModel.cs
+++ b/src/Artemis.UI/Screens/Workshop/Entries/Details/EntryReleasesViewModel.cs
@@ -9,7 +9,6 @@ using Artemis.UI.Shared.Utilities;
using Artemis.WebClient.Workshop;
using Artemis.WebClient.Workshop.Handlers.InstallationHandlers;
using Artemis.WebClient.Workshop.Models;
-using Artemis.WebClient.Workshop.Services;
using Humanizer;
using ReactiveUI;
@@ -29,11 +28,13 @@ public class EntryReleasesViewModel : ViewModelBase
Entry = entry;
DownloadLatestRelease = ReactiveCommand.CreateFromTask(ExecuteDownloadLatestRelease);
+ OnInstallationStarted = Confirm;
}
public IGetEntryById_Entry Entry { get; }
public ReactiveCommand DownloadLatestRelease { get; }
+ public Func> OnInstallationStarted { get; set; }
public Func? OnInstallationFinished { get; set; }
private async Task ExecuteDownloadLatestRelease(CancellationToken cancellationToken)
@@ -41,11 +42,7 @@ public class EntryReleasesViewModel : ViewModelBase
if (Entry.LatestRelease == null)
return;
- bool confirm = await _windowService.ShowConfirmContentDialog(
- "Install latest release",
- $"Are you sure you want to download and install version {Entry.LatestRelease.Version} of {Entry.Name}?"
- );
- if (!confirm)
+ if (await OnInstallationStarted(Entry))
return;
IEntryInstallationHandler installationHandler = _factory.CreateHandler(Entry.EntryType);
@@ -64,4 +61,14 @@ public class EntryReleasesViewModel : ViewModelBase
.WithSeverity(NotificationSeverity.Error).Show();
}
}
+
+ private async Task Confirm(IEntryDetails entryDetails)
+ {
+ bool confirm = await _windowService.ShowConfirmContentDialog(
+ "Install latest release",
+ $"Are you sure you want to download and install version {entryDetails.LatestRelease?.Version} of {entryDetails.Name}?"
+ );
+
+ return !confirm;
+ }
}
\ No newline at end of file
diff --git a/src/Artemis.UI/Screens/Workshop/Home/WorkshopHomeView.axaml b/src/Artemis.UI/Screens/Workshop/Home/WorkshopHomeView.axaml
index 4bc7bd2a4..c2ed7ebb8 100644
--- a/src/Artemis.UI/Screens/Workshop/Home/WorkshopHomeView.axaml
+++ b/src/Artemis.UI/Screens/Workshop/Home/WorkshopHomeView.axaml
@@ -59,7 +59,7 @@