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 @@ + + 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 @@