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 8194cef44..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);
@@ -597,7 +612,7 @@ internal class PluginManagementService : IPluginManagementService
using StreamReader reader = new(metaDataFileEntry.Open());
PluginInfo pluginInfo = CoreJson.DeserializeObject(reader.ReadToEnd())!;
if (!pluginInfo.Main.EndsWith(".dll"))
- throw new ArtemisPluginException("Main entry in plugin.json must point to a .dll file" + fileName);
+ throw new ArtemisPluginException("Main entry in plugin.json must point to a .dll file");
Plugin? existing = _plugins.FirstOrDefault(p => p.Guid == pluginInfo.Guid);
if (existing != null)
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/ArtemisBootstrapper.cs b/src/Artemis.UI/ArtemisBootstrapper.cs
index be365cd9f..f869bb8e3 100644
--- a/src/Artemis.UI/ArtemisBootstrapper.cs
+++ b/src/Artemis.UI/ArtemisBootstrapper.cs
@@ -13,6 +13,7 @@ using Artemis.UI.Shared.Services;
using Artemis.VisualScripting.DryIoc;
using Artemis.WebClient.Updating.DryIoc;
using Artemis.WebClient.Workshop.DryIoc;
+using Artemis.WebClient.Workshop.Services;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
diff --git a/src/Artemis.UI/Routing/Routes.cs b/src/Artemis.UI/Routing/Routes.cs
index 1d4862f44..4353a4eef 100644
--- a/src/Artemis.UI/Routing/Routes.cs
+++ b/src/Artemis.UI/Routing/Routes.cs
@@ -14,6 +14,7 @@ using Artemis.UI.Screens.Workshop.Library;
using Artemis.UI.Screens.Workshop.Library.Tabs;
using Artemis.UI.Screens.Workshop.Profile;
using Artemis.UI.Shared.Routing;
+using PluginDetailsViewModel = Artemis.UI.Screens.Workshop.Plugins.PluginDetailsViewModel;
namespace Artemis.UI.Routing;
@@ -32,6 +33,8 @@ public static class Routes
{
Children = new List
{
+ new RouteRegistration("plugins/{page:int}"),
+ new RouteRegistration("plugins/details/{entryId:long}"),
new RouteRegistration("profiles/{page:int}"),
new RouteRegistration("profiles/details/{entryId:long}"),
new RouteRegistration("layouts/{page:int}"),
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/PluginView.axaml b/src/Artemis.UI/Screens/Plugins/PluginView.axaml
index b9dc83c55..64ef0e88c 100644
--- a/src/Artemis.UI/Screens/Plugins/PluginView.axaml
+++ b/src/Artemis.UI/Screens/Plugins/PluginView.axaml
@@ -19,7 +19,10 @@
Grid.RowSpan="3"
VerticalAlignment="Top" />
-
+
+
+
+
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 e1fb3eeb9..f97399a7e 100644
--- a/src/Artemis.UI/Screens/Root/RootViewModel.cs
+++ b/src/Artemis.UI/Screens/Root/RootViewModel.cs
@@ -13,6 +13,7 @@ using Artemis.UI.Shared;
using Artemis.UI.Shared.Routing;
using Artemis.UI.Shared.Services;
using Artemis.UI.Shared.Services.MainWindow;
+using Artemis.WebClient.Workshop.Services;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
@@ -41,6 +42,7 @@ public class RootViewModel : RoutableHostScreen, IMainWindowProv
IMainWindowService mainWindowService,
IDebugService debugService,
IUpdateService updateService,
+ IWorkshopService workshopService,
SidebarViewModel sidebarViewModel,
DefaultTitleBarViewModel defaultTitleBarViewModel)
{
@@ -64,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())
@@ -74,10 +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;
+ // 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();
@@ -135,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();
@@ -184,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..368c39351
--- /dev/null
+++ b/src/Artemis.UI/Screens/Settings/Account/CreatePersonalAccessTokenView.axaml.cs
@@ -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 CreatePersonalAccessTokenView : ReactiveUserControl
+{
+ public CreatePersonalAccessTokenView()
+ {
+ InitializeComponent();
+ this.WhenActivated(_ => this.ClearAllDataValidationErrors());
+ }
+}
\ 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..dba94f3c6
--- /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.UtcNow.Date.AddDays(181);
+
+ 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 >= 5, "You must specify a description of at least 5 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.UtcNow.Date.AddDays(1), "Expiration date must be at least 24 hours from now");
+ }
+
+ public DateTime StartDate => DateTime.UtcNow.Date.AddDays(1);
+ public DateTime EndDate => DateTime.UtcNow.Date.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..eeb650355 100644
--- a/src/Artemis.UI/Screens/Settings/Tabs/AccountTabView.axaml
+++ b/src/Artemis.UI/Screens/Settings/Tabs/AccountTabView.axaml
@@ -5,10 +5,15 @@
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">
+
+
+
@@ -46,7 +51,7 @@
-
+
@@ -66,8 +71,8 @@
-
-
+
+
@@ -99,16 +104,55 @@
-
-
-
-
-
-
-
-
+
+ Personal access tokens
+
+
+
+
+
+ Tokens be used to communicate with Artemis APIs without using a username and password
+
+
+
+
+
+ Description
+ Created at
+ Expires at
+
+
+
+ You have no active personal access tokens.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
\ 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 ae9de0d2c..3b7de1c0c 100644
--- a/src/Artemis.UI/Screens/Sidebar/SidebarViewModel.cs
+++ b/src/Artemis.UI/Screens/Sidebar/SidebarViewModel.cs
@@ -43,6 +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.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/Entries/EntriesViewModel.cs b/src/Artemis.UI/Screens/Workshop/Entries/EntriesViewModel.cs
index 4ee34fee4..904348bc6 100644
--- a/src/Artemis.UI/Screens/Workshop/Entries/EntriesViewModel.cs
+++ b/src/Artemis.UI/Screens/Workshop/Entries/EntriesViewModel.cs
@@ -25,7 +25,8 @@ public partial class EntriesViewModel : RoutableHostScreen
Tabs = new ObservableCollection
{
new("Profiles", "workshop/entries/profiles/1", "workshop/entries/profiles"),
- new("Layouts", "workshop/entries/layouts/1", "workshop/entries/layouts")
+ new("Layouts", "workshop/entries/layouts/1", "workshop/entries/layouts"),
+ new("Plugins", "workshop/entries/plugins/1", "workshop/entries/plugins"),
};
this.WhenActivated(d =>
diff --git a/src/Artemis.UI/Screens/Workshop/Entries/List/EntryListItemViewModel.cs b/src/Artemis.UI/Screens/Workshop/Entries/List/EntryListItemViewModel.cs
index be57fd898..757396c72 100644
--- a/src/Artemis.UI/Screens/Workshop/Entries/List/EntryListItemViewModel.cs
+++ b/src/Artemis.UI/Screens/Workshop/Entries/List/EntryListItemViewModel.cs
@@ -34,6 +34,7 @@ public class EntryListItemViewModel : ActivatableViewModelBase
await _router.Navigate($"workshop/entries/profiles/details/{Entry.Id}");
break;
case EntryType.Plugin:
+ await _router.Navigate($"workshop/entries/plugins/details/{Entry.Id}");
break;
default:
throw new ArgumentOutOfRangeException();
diff --git a/src/Artemis.UI/Screens/Workshop/Entries/Tabs/LayoutListViewModel.cs b/src/Artemis.UI/Screens/Workshop/Entries/Tabs/LayoutListViewModel.cs
index 6cab02f27..ed2fa328b 100644
--- a/src/Artemis.UI/Screens/Workshop/Entries/Tabs/LayoutListViewModel.cs
+++ b/src/Artemis.UI/Screens/Workshop/Entries/Tabs/LayoutListViewModel.cs
@@ -27,6 +27,7 @@ public class LayoutListViewModel : List.EntryListViewModel
And = new[]
{
new EntryFilterInput {EntryType = new EntryTypeOperationFilterInput {Eq = EntryType.Layout}},
+ new EntryFilterInput(){LatestReleaseId = new LongOperationFilterInput {Gt = 0}},
base.GetFilter()
}
};
diff --git a/src/Artemis.UI/Screens/Workshop/Entries/Tabs/PluginListView.axaml b/src/Artemis.UI/Screens/Workshop/Entries/Tabs/PluginListView.axaml
new file mode 100644
index 000000000..e52c30641
--- /dev/null
+++ b/src/Artemis.UI/Screens/Workshop/Entries/Tabs/PluginListView.axaml
@@ -0,0 +1,65 @@
+
+
+
+
+
+
+
+
+
+
+ Categories
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Looks like your current filters gave no results
+
+ Modify or clear your filters to view other plugins
+
+
+
+
+
+
+
+
diff --git a/src/Artemis.UI/Screens/Workshop/Entries/Tabs/PluginListView.axaml.cs b/src/Artemis.UI/Screens/Workshop/Entries/Tabs/PluginListView.axaml.cs
new file mode 100644
index 000000000..2e32eea93
--- /dev/null
+++ b/src/Artemis.UI/Screens/Workshop/Entries/Tabs/PluginListView.axaml.cs
@@ -0,0 +1,14 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Markup.Xaml;
+using Avalonia.ReactiveUI;
+
+namespace Artemis.UI.Screens.Workshop.Entries.Tabs;
+
+public partial class PluginListView : ReactiveUserControl
+{
+ public PluginListView()
+ {
+ InitializeComponent();
+ }
+}
\ No newline at end of file
diff --git a/src/Artemis.UI/Screens/Workshop/Entries/Tabs/PluginListViewModel.cs b/src/Artemis.UI/Screens/Workshop/Entries/Tabs/PluginListViewModel.cs
new file mode 100644
index 000000000..206697490
--- /dev/null
+++ b/src/Artemis.UI/Screens/Workshop/Entries/Tabs/PluginListViewModel.cs
@@ -0,0 +1,34 @@
+using System;
+using Artemis.UI.Screens.Workshop.Categories;
+using Artemis.UI.Screens.Workshop.Entries.List;
+using Artemis.UI.Shared.Routing;
+using Artemis.UI.Shared.Services;
+using Artemis.WebClient.Workshop;
+
+namespace Artemis.UI.Screens.Workshop.Entries.Tabs;
+
+public class PluginListViewModel : EntryListViewModel
+{
+ public PluginListViewModel(IWorkshopClient workshopClient,
+ IRouter router,
+ CategoriesViewModel categoriesViewModel,
+ EntryListInputViewModel entryListInputViewModel,
+ INotificationService notificationService,
+ Func getEntryListViewModel)
+ : base("workshop/entries/plugins", workshopClient, router, categoriesViewModel, entryListInputViewModel, notificationService, getEntryListViewModel)
+ {
+ entryListInputViewModel.SearchWatermark = "Search plugins";
+ }
+
+ protected override EntryFilterInput GetFilter()
+ {
+ return new EntryFilterInput
+ {
+ And = new[]
+ {
+ new EntryFilterInput {EntryType = new EntryTypeOperationFilterInput {Eq = EntryType.Plugin}},
+ base.GetFilter()
+ }
+ };
+ }
+}
\ No newline at end of file
diff --git a/src/Artemis.UI/Screens/Workshop/Entries/Tabs/ProfileListViewModel.cs b/src/Artemis.UI/Screens/Workshop/Entries/Tabs/ProfileListViewModel.cs
index c09a587a3..f251ee10d 100644
--- a/src/Artemis.UI/Screens/Workshop/Entries/Tabs/ProfileListViewModel.cs
+++ b/src/Artemis.UI/Screens/Workshop/Entries/Tabs/ProfileListViewModel.cs
@@ -12,7 +12,7 @@ public class ProfileListViewModel : List.EntryListViewModel
public ProfileListViewModel(IWorkshopClient workshopClient,
IRouter router,
CategoriesViewModel categoriesViewModel,
- List.EntryListInputViewModel entryListInputViewModel,
+ EntryListInputViewModel entryListInputViewModel,
INotificationService notificationService,
Func getEntryListViewModel)
: base("workshop/entries/profiles", workshopClient, router, categoriesViewModel, entryListInputViewModel, notificationService, getEntryListViewModel)
diff --git a/src/Artemis.UI/Screens/Workshop/Home/WorkshopHomeView.axaml b/src/Artemis.UI/Screens/Workshop/Home/WorkshopHomeView.axaml
index 433f87c50..c2ed7ebb8 100644
--- a/src/Artemis.UI/Screens/Workshop/Home/WorkshopHomeView.axaml
+++ b/src/Artemis.UI/Screens/Workshop/Home/WorkshopHomeView.axaml
@@ -57,6 +57,14 @@
+
+