1
0
mirror of https://github.com/Artemis-RGB/Artemis synced 2025-12-12 13:28:33 +00:00

Plugins - Fixed cancelling prerequisite installation

Plugins - Tweaked prerequisite installation popup size
Account settings - Added personal access token management
Workshop - Added plugin loading, installing and removal
This commit is contained in:
RobertBeekman 2024-02-16 23:04:24 +01:00
parent 21b8112de5
commit f2c8de1746
33 changed files with 516 additions and 113 deletions

View File

@ -12,6 +12,11 @@ namespace Artemis.Core.Services;
/// </summary>
public interface IPluginManagementService : IArtemisService, IDisposable
{
/// <summary>
/// Gets a list containing additional directories in which plugins are located, used while loading plugins.
/// </summary>
List<DirectoryInfo> AdditionalPluginDirectories { get; }
/// <summary>
/// Indicates whether or not plugins are currently being loaded
/// </summary>

View File

@ -78,8 +78,11 @@ internal class PluginManagementService : IPluginManagementService
File.Create(Path.Combine(pluginDirectory.FullName, "artemis.lock")).Close();
}
public List<DirectoryInfo> 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);

View File

@ -35,4 +35,15 @@
<ItemGroup>
<AvaloniaResource Include="Assets\**" />
</ItemGroup>
<ItemGroup>
<Compile Update="Screens\Workshop\Plugin\Dialogs\DeviceProviderPickerDialogView.axaml.cs">
<DependentUpon>DeviceProviderPickerDialogView.axaml</DependentUpon>
<SubType>Code</SubType>
</Compile>
<Compile Update="Screens\Workshop\Plugin\Dialogs\DeviceSelectionDialogView.axaml.cs">
<DependentUpon>DeviceSelectionDialogView.axaml</DependentUpon>
<SubType>Code</SubType>
</Compile>
</ItemGroup>
</Project>

View File

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

View File

@ -24,7 +24,7 @@
</Style>
</Styles>
</UserControl.Styles>
<Grid ColumnDefinitions="250,*">
<Grid ColumnDefinitions="350,*" Width="800">
<Grid.RowDefinitions>
<RowDefinition MinHeight="200" />
</Grid.RowDefinitions>
@ -36,7 +36,7 @@
IsHitTestVisible="False">
<ListBox.ItemTemplate>
<DataTemplate DataType="{x:Type prerequisites:PluginPrerequisiteViewModel}">
<Grid ColumnDefinitions="Auto,*" Margin="0 6">
<Grid ColumnDefinitions="Auto,*" Margin="-6 0 20 0">
<Border Grid.Row="0" Grid.Column="0" Classes="status-border" IsVisible="{CompiledBinding !IsMet}" Background="#ff3838">
<avalonia:MaterialIcon Kind="Close" />
</Border>
@ -45,8 +45,8 @@
</Border>
<StackPanel Margin="8 0 0 0" Grid.Column="1" VerticalAlignment="Stretch">
<TextBlock FontWeight="Bold" Text="{CompiledBinding PluginPrerequisite.Name}" TextWrapping="Wrap" />
<TextBlock Text="{CompiledBinding PluginPrerequisite.Description}" TextWrapping="Wrap" />
<TextBlock Text="{CompiledBinding PluginPrerequisite.Name}" TextWrapping="Wrap" />
<TextBlock Text="{CompiledBinding PluginPrerequisite.Description}" TextWrapping="Wrap" Classes="subtitle"/>
</StackPanel>
</Grid>
</DataTemplate>
@ -74,12 +74,14 @@
Grid.Column="1"
Margin="10 0"
IsVisible="{CompiledBinding ShowFailed, Mode=OneWay}">
<StackPanel Orientation="Horizontal">
<TextBlock>Installing</TextBlock>
<TextBlock Text="{CompiledBinding ActivePrerequisite.PluginPrerequisite.Name, Mode=OneWay}" FontWeight="SemiBold" TextWrapping="Wrap" />
<TextBlock>failed.</TextBlock>
</StackPanel>
<TextBlock TextWrapping="Wrap">You may try again to see if that helps, otherwise install the prerequisite manually or contact the plugin developer.</TextBlock>
<TextBlock TextWrapping="Wrap" >
<Run>Installing</Run>
<Run Text="{CompiledBinding ActivePrerequisite.PluginPrerequisite.Name, Mode=OneWay}" FontWeight="SemiBold" />
<Run>failed.</Run>
<LineBreak/>
<LineBreak/>
<Run>You may try again to see if that helps, otherwise install the prerequisite manually or contact the plugin developer.</Run>
</TextBlock>
</StackPanel>
</Grid>
</UserControl>

View File

@ -30,16 +30,21 @@ public partial class PluginPrerequisitesInstallDialogViewModel : ContentDialogVi
[Notify] private bool _showIntro = true;
[Notify] private bool _showProgress;
private bool _finished;
public PluginPrerequisitesInstallDialogViewModel(List<IPrerequisitesSubject> subjects, IPrerequisitesVmFactory prerequisitesVmFactory)
{
Prerequisites = new ObservableCollection<PluginPrerequisiteViewModel>();
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<Unit, Unit> Install { get; }
public ObservableCollection<PluginPrerequisiteViewModel> Prerequisites { get; }
public static async Task Show(IWindowService windowService, List<IPrerequisitesSubject> 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;
}
}
}

View File

@ -24,7 +24,7 @@
</Style>
</Styles>
</UserControl.Styles>
<Grid ColumnDefinitions="250,*">
<Grid ColumnDefinitions="350,*" Width="800">
<Grid.RowDefinitions>
<RowDefinition MinHeight="200" />
</Grid.RowDefinitions>
@ -37,8 +37,8 @@
<ListBox.ItemTemplate>
<DataTemplate DataType="{x:Type prerequisites:PluginPrerequisiteViewModel}">
<StackPanel Margin="0 6" VerticalAlignment="Stretch">
<TextBlock FontWeight="Bold" Text="{CompiledBinding PluginPrerequisite.Name}" TextWrapping="Wrap" />
<TextBlock Text="{CompiledBinding PluginPrerequisite.Description}" TextWrapping="Wrap" />
<TextBlock Text="{CompiledBinding PluginPrerequisite.Name}" TextWrapping="Wrap" />
<TextBlock Text="{CompiledBinding PluginPrerequisite.Description}" TextWrapping="Wrap" Classes="subtitle"/>
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>

View File

@ -64,6 +64,7 @@ public partial class PluginPrerequisitesUninstallDialogViewModel : ContentDialog
public static async Task Show(IWindowService windowService, List<IPrerequisitesSubject> subjects, string cancelLabel = "Cancel")
{
await windowService.CreateContentDialog()
.WithFullScreen()
.WithTitle("Plugin prerequisites")
.WithViewModel(out PluginPrerequisitesUninstallDialogViewModel vm, subjects)
.WithCloseButtonText(cancelLabel)

View File

@ -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<PluginPlatformViewModel>();
if (Plugin.Info.Platforms != null)
@ -90,7 +95,7 @@ public partial class PluginViewModel : ActivatableViewModelBase
public ObservableCollection<PluginPlatformViewModel> 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<IPrerequisitesSubject> 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<IPrerequisitesSubject> 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";

View File

@ -66,7 +66,7 @@ public class RootViewModel : RoutableHostScreen<RoutableScreen>, IMainWindowProv
OpenScreen = ReactiveCommand.Create<string?>(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<ViewModelBase>())
@ -76,13 +76,15 @@ public class RootViewModel : RoutableHostScreen<RoutableScreen>, 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<RoutableScreen>, 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<RoutableScreen>, IMainWindowProv
_lifeTime.MainWindow.Activate();
if (_lifeTime.MainWindow.WindowState == WindowState.Minimized)
_lifeTime.MainWindow.WindowState = WindowState.Normal;
OnMainWindowOpened();
}

View File

@ -0,0 +1,19 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:account="clr-namespace:Artemis.UI.Screens.Settings.Account"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Artemis.UI.Screens.Settings.Account.CreatePersonalAccessTokenView"
x:DataType="account:CreatePersonalAccessTokenViewModel">
<StackPanel Spacing="5" Width="300">
<Label>Description</Label>
<TextBox Text="{CompiledBinding Description}"/>
<Label>Expiration date</Label>
<CalendarDatePicker SelectedDate="{CompiledBinding ExpirationDate}"
DisplayDateStart="{CompiledBinding StartDate}"
DisplayDateEnd="{CompiledBinding EndDate}"
HorizontalAlignment="Stretch"/>
</StackPanel>
</UserControl>

View File

@ -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<CreatePersonalAccessTokenViewModel>
{
public CreatePersonalAccessTokenView()
{
InitializeComponent();
}
}

View File

@ -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<Unit, Unit> 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();
}
}

View File

@ -0,0 +1,19 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:account="clr-namespace:Artemis.UI.Screens.Settings.Account"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Artemis.UI.Screens.Settings.Account.PersonalAccessTokenView"
x:DataType="account:PersonalAccessTokenViewModel">
<StackPanel>
<TextBlock>
Your token has been created, please copy it now as you cannot view it again later.
</TextBlock>
<TextBox Margin="0 10 0 0"
VerticalAlignment="Top"
TextWrapping="Wrap"
IsReadOnly="True"
Text="{CompiledBinding Token, Mode=OneWay}" />
</StackPanel>
</UserControl>

View File

@ -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<PersonalAccessTokenViewModel>
{
public PersonalAccessTokenView()
{
InitializeComponent();
}
}

View File

@ -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;
}
}

View File

@ -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">
<UserControl.Resources>
<converters:DateTimeConverter x:Key="DateTimeConverter" />
</UserControl.Resources>
<Panel>
<StackPanel IsVisible="{CompiledBinding !IsLoggedIn^}" Margin="0 50 0 0">
<StackPanel.Styles>
<Styles>
<Style Selector="TextBlock">
<Setter Property="TextAlignment" Value="Center"></Setter>
<Setter Property="TextWrapping" Value="Wrap"></Setter>
</Style>
</Styles>
</StackPanel.Styles>
<TextBlock Theme="{StaticResource TitleTextBlockStyle}">You are not logged in</TextBlock>
<TextBlock>
<Run>In order to manage your account you must be logged in.</Run>
<LineBreak />
<Run>Creating an account is free and we'll not bother you with a newsletter or crap like that.</Run>
</TextBlock>
<Lottie Path="/Assets/Animations/login-pending.json" RepeatCount="1" Width="350" Height="350"></Lottie>
<TextBlock>
<Run>Click Log In below to (create an account) and log in.</Run>
<LineBreak />
<Run>You'll also be able to log in with Google or Discord.</Run>
</TextBlock>
<Button HorizontalAlignment="Center" Command="{CompiledBinding Login}" Margin="0 25 0 0">Login</Button>
</StackPanel>
<!-- <StackPanel IsVisible="{CompiledBinding !IsLoggedIn^}" Margin="0 50 0 0"> -->
<!-- <StackPanel.Styles> -->
<!-- <Styles> -->
<!-- <Style Selector="TextBlock"> -->
<!-- <Setter Property="TextAlignment" Value="Center"></Setter> -->
<!-- <Setter Property="TextWrapping" Value="Wrap"></Setter> -->
<!-- </Style> -->
<!-- </Styles> -->
<!-- </StackPanel.Styles> -->
<!-- <TextBlock Theme="{StaticResource TitleTextBlockStyle}">You are not logged in</TextBlock> -->
<!-- <TextBlock> -->
<!-- <Run>In order to manage your account you must be logged in.</Run> -->
<!-- <LineBreak /> -->
<!-- <Run>Creating an account is free and we'll not bother you with a newsletter or crap like that.</Run> -->
<!-- </TextBlock> -->
<!-- -->
<!-- <Lottie Path="/Assets/Animations/login-pending.json" RepeatCount="1" Width="350" Height="350"></Lottie> -->
<!-- -->
<!-- <TextBlock> -->
<!-- <Run>Click Log In below to (create an account) and log in.</Run> -->
<!-- <LineBreak /> -->
<!-- <Run>You'll also be able to log in with Google or Discord.</Run> -->
<!-- </TextBlock> -->
<!-- <Button HorizontalAlignment="Center" Command="{CompiledBinding Login}" Margin="0 25 0 0">Login</Button> -->
<!-- </StackPanel> -->
<ScrollViewer IsVisible="{CompiledBinding IsLoggedIn^}" HorizontalScrollBarVisibility="Disabled" VerticalScrollBarVisibility="Auto">
<StackPanel Margin="15" MaxWidth="1000">
@ -46,7 +51,7 @@
</asyncImageLoader:AdvancedImage.Loader>
</asyncImageLoader:AdvancedImage>
</Border>
<TextBlock Grid.Row="0" Grid.Column="1" Padding="0" VerticalAlignment="Bottom" Text="{CompiledBinding Name}" Classes="h3 no-margin"/>
<TextBlock Grid.Row="0" Grid.Column="1" Padding="0" VerticalAlignment="Bottom" Text="{CompiledBinding Name}" Classes="h3 no-margin" />
<TextBlock Classes="subtitle" Grid.Column="1" Grid.Row="1" Padding="0" VerticalAlignment="Top" Text="{CompiledBinding Email}" />
</Grid>
@ -66,8 +71,8 @@
<Button Width="150" Content="Change password" Command="{CompiledBinding ChangePasswordAddress}" />
</StackPanel>
</Grid>
<Border Classes="card-separator" IsVisible="{CompiledBinding CanChangePassword}"/>
<Border Classes="card-separator" IsVisible="{CompiledBinding CanChangePassword}" />
<Grid RowDefinitions="*,*" ColumnDefinitions="*,Auto">
<StackPanel Grid.Column="0" VerticalAlignment="Center">
<TextBlock>
@ -99,16 +104,45 @@
</StackPanel>
</Border>
<!-- <TextBlock Classes="card-title"> -->
<!-- Personal access tokens -->
<!-- </TextBlock> -->
<!-- <Border Classes="card" VerticalAlignment="Stretch" Margin="0,0,5,0"> -->
<!-- <StackPanel> -->
<!-- <TextBlock>TODO :)</TextBlock> -->
<!-- </StackPanel> -->
<!-- </Border> -->
<TextBlock Classes="card-title">
Personal access tokens
</TextBlock>
<Border Classes="card" VerticalAlignment="Stretch" Margin="0,0,5,0">
<StackPanel>
<Button HorizontalAlignment="Right" Command="{CompiledBinding GenerateToken}">Generate token</Button>
<Grid ColumnDefinitions="2*,*,*,*" Margin="12 6">
<TextBlock Grid.Column="0">Description</TextBlock>
<TextBlock Grid.Column="1">Created at</TextBlock>
<TextBlock Grid.Column="2" Grid.ColumnSpan="2">Expires at</TextBlock>
</Grid>
<ItemsControl ItemsSource="{CompiledBinding PersonalAccessTokens}">
<ItemsControl.Styles>
<Style Selector="ContentPresenter:nth-child(even) > Border">
<Setter Property="Background" Value="{StaticResource ControlStrokeColorOnAccentDefault}"></Setter>
</Style>
</ItemsControl.Styles>
<ItemsControl.DataTemplates>
<DataTemplate DataType="models:PersonalAccessToken">
<Border CornerRadius="4" Padding="12 6">
<Grid ColumnDefinitions="2*,*,*,*">
<TextBlock Grid.Column="0" VerticalAlignment="Center" Text="{CompiledBinding Description}" />
<TextBlock Grid.Column="1" VerticalAlignment="Center" Text="{CompiledBinding CreationTime, Converter={StaticResource DateTimeConverter}}" />
<TextBlock Grid.Column="2" VerticalAlignment="Center" Text="{CompiledBinding Expiration, Converter={StaticResource DateTimeConverter}}" />
<Button Grid.Column="3" HorizontalAlignment="Right"
Classes="icon-button"
Command="{Binding $parent[settings:AccountTabView].DataContext.DeleteToken}"
CommandParameter="{CompiledBinding}">
<avalonia:MaterialIcon Kind="Trash" />
</Button>
</Grid>
</Border>
</DataTemplate>
</ItemsControl.DataTemplates>
</ItemsControl>
</StackPanel>
</Border>
</StackPanel>
</ScrollViewer>
</Panel>
</UserControl>

View File

@ -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<PersonalAccessToken> _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<PersonalAccessToken>(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<bool> IsLoggedIn { get; }
public ReactiveCommand<PersonalAccessToken,Unit> 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<PersonalAccessToken> personalAccessTokens = await _userManagementService.GetPersonAccessTokens(CancellationToken.None);
PersonalAccessTokens = new ObservableCollection<PersonalAccessToken>(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<PersonalAccessToken> personalAccessTokens = await _userManagementService.GetPersonAccessTokens(CancellationToken.None);
PersonalAccessTokens = new ObservableCollection<PersonalAccessToken>(personalAccessTokens);
}
}

View File

@ -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"),
}),

View File

@ -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<Unit, Unit> DownloadLatestRelease { get; }
public Func<IEntryDetails, Task<bool>> OnInstallationStarted { get; set; }
public Func<InstalledEntry, Task>? 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<bool> 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;
}
}

View File

@ -59,7 +59,7 @@
<Button Width="150" Height="180" Command="{CompiledBinding Navigate}" CommandParameter="workshop/entries/plugins/1" VerticalContentAlignment="Top">
<StackPanel>
<avalonia:MaterialIcon Kind="Plugin" HorizontalAlignment="Left" Width="60" Height="60" Margin="0 5" />
<avalonia:MaterialIcon Kind="Connection" HorizontalAlignment="Left" Width="60" Height="60" Margin="0 5" />
<TextBlock TextWrapping="Wrap" FontSize="16" Margin="0 5">Plugins</TextBlock>
<TextBlock TextWrapping="Wrap" FontSize="12" Opacity="0.8">Plugins add new functionality to Artemis.</TextBlock>
</StackPanel>

View File

@ -1,7 +1,12 @@
using System;
using System.Linq;
using System.Reactive;
using System.Threading;
using System.Threading.Tasks;
using Artemis.Core;
using Artemis.Core.Services;
using Artemis.UI.DryIoc.Factories;
using Artemis.UI.Screens.Plugins;
using Artemis.UI.Shared;
using Artemis.UI.Shared.Routing;
using Artemis.UI.Shared.Services;
@ -20,14 +25,24 @@ public partial class InstalledTabItemViewModel : ViewModelBase
private readonly IRouter _router;
private readonly EntryInstallationHandlerFactory _factory;
private readonly IWindowService _windowService;
private readonly IPluginManagementService _pluginManagementService;
private readonly ISettingsVmFactory _settingsVmFactory;
[Notify(Setter.Private)] private bool _isRemoved;
public InstalledTabItemViewModel(InstalledEntry installedEntry, IWorkshopService workshopService, IRouter router, EntryInstallationHandlerFactory factory, IWindowService windowService)
public InstalledTabItemViewModel(InstalledEntry installedEntry,
IWorkshopService workshopService,
IRouter router,
EntryInstallationHandlerFactory factory,
IWindowService windowService,
IPluginManagementService pluginManagementService,
ISettingsVmFactory settingsVmFactory)
{
_workshopService = workshopService;
_router = router;
_factory = factory;
_windowService = windowService;
_pluginManagementService = pluginManagementService;
_settingsVmFactory = settingsVmFactory;
InstalledEntry = installedEntry;
ViewWorkshopPage = ReactiveCommand.CreateFromTask(ExecuteViewWorkshopPage);
@ -58,9 +73,25 @@ public partial class InstalledTabItemViewModel : ViewModelBase
bool confirmed = await _windowService.ShowConfirmContentDialog("Do you want to uninstall this entry?", "Both the entry and its contents will be removed.");
if (!confirmed)
return;
// Ideally the installation handler does this but it doesn't have access to the required view models
if (InstalledEntry.EntryType == EntryType.Plugin)
await UninstallPluginPrerequisites();
IEntryInstallationHandler handler = _factory.CreateHandler(InstalledEntry.EntryType);
await handler.UninstallAsync(InstalledEntry, cancellationToken);
IsRemoved = true;
}
private async Task UninstallPluginPrerequisites()
{
if (!InstalledEntry.TryGetMetadata("PluginId", out Guid pluginId))
return;
Plugin? plugin = _pluginManagementService.GetAllPlugins().FirstOrDefault(p => p.Guid == pluginId);
if (plugin == null)
return;
PluginViewModel pluginViewModel = _settingsVmFactory.PluginViewModel(plugin, ReactiveCommand.Create(() => { }));
await pluginViewModel.ExecuteRemovePrerequisites(true);
}
}

View File

@ -0,0 +1,19 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:dialogs="clr-namespace:Artemis.UI.Screens.Workshop.Plugins.Dialogs"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Artemis.UI.Screens.Workshop.Plugins.Dialogs.PluginDialogView"
x:DataType="dialogs:PluginDialogViewModel">
<Grid ColumnDefinitions="4*,5*" Width="800" Height="160">
<ContentControl Grid.Column="0" Content="{CompiledBinding PluginViewModel}" />
<Border Grid.Column="1" BorderBrush="{DynamicResource ButtonBorderBrush}" BorderThickness="1 0 0 0" Margin="10 0 0 0" Padding="10 0 0 0">
<Grid RowDefinitions="Auto,*">
<TextBlock Classes="h5">Plugin features</TextBlock>
<ListBox Grid.Row="1" MaxHeight="135" ItemsSource="{CompiledBinding PluginFeatures}" />
</Grid>
</Border>
</Grid>
</UserControl>

View File

@ -0,0 +1,11 @@
using Avalonia.ReactiveUI;
namespace Artemis.UI.Screens.Workshop.Plugins.Dialogs;
public partial class PluginDialogView : ReactiveUserControl<PluginDialogViewModel>
{
public PluginDialogView()
{
InitializeComponent();
}
}

View File

@ -0,0 +1,23 @@
using System.Collections.ObjectModel;
using System.Linq;
using System.Reactive.Linq;
using Artemis.Core;
using Artemis.UI.DryIoc.Factories;
using Artemis.UI.Screens.Plugins;
using Artemis.UI.Screens.Plugins.Features;
using Artemis.UI.Shared;
using ReactiveUI;
namespace Artemis.UI.Screens.Workshop.Plugins.Dialogs;
public class PluginDialogViewModel : ContentDialogViewModelBase
{
public PluginDialogViewModel(Plugin plugin, ISettingsVmFactory settingsVmFactory)
{
PluginViewModel = settingsVmFactory.PluginViewModel(plugin, ReactiveCommand.Create(() => {}, Observable.Empty<bool>()));
PluginFeatures = new ObservableCollection<PluginFeatureViewModel>(plugin.Features.Select(f => settingsVmFactory.PluginFeatureViewModel(f, false)));
}
public PluginViewModel PluginViewModel { get; }
public ObservableCollection<PluginFeatureViewModel> PluginFeatures { get; }
}

View File

@ -2,11 +2,11 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:plugin="clr-namespace:Artemis.UI.Screens.Workshop.Plugin"
xmlns:mdxaml="https://github.com/whistyun/Markdown.Avalonia.Tight"
xmlns:plugins="clr-namespace:Artemis.UI.Screens.Workshop.Plugins"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Artemis.UI.Screens.Workshop.Plugin.PluginDetailsView"
x:DataType="plugin:PluginDetailsViewModel">
x:Class="Artemis.UI.Screens.Workshop.Plugins.PluginDetailsView"
x:DataType="plugins:PluginDetailsViewModel">
<Grid ColumnDefinitions="300,*, 300" RowDefinitions="Auto,*">
<StackPanel Grid.Row="1" Grid.Column="0" Spacing="10">
<Border Classes="card" VerticalAlignment="Top">

View File

@ -1,9 +1,6 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using Avalonia.ReactiveUI;
namespace Artemis.UI.Screens.Workshop.Plugin;
namespace Artemis.UI.Screens.Workshop.Plugins;
public partial class PluginDetailsView : ReactiveUserControl<PluginDetailsViewModel>
{

View File

@ -1,18 +1,26 @@
using System;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Artemis.Core;
using Artemis.Core.Services;
using Artemis.UI.Screens.Workshop.Entries.Details;
using Artemis.UI.Screens.Workshop.Parameters;
using Artemis.UI.Screens.Workshop.Plugins.Dialogs;
using Artemis.UI.Shared.Routing;
using Artemis.UI.Shared.Services;
using Artemis.WebClient.Workshop;
using Artemis.WebClient.Workshop.Models;
using PropertyChanged.SourceGenerator;
using StrawberryShake;
namespace Artemis.UI.Screens.Workshop.Plugin;
namespace Artemis.UI.Screens.Workshop.Plugins;
public partial class PluginDetailsViewModel : RoutableScreen<WorkshopDetailParameters>
{
private readonly IWorkshopClient _client;
private readonly IWindowService _windowService;
private readonly IPluginManagementService _pluginManagementService;
private readonly Func<IGetEntryById_Entry, EntryInfoViewModel> _getEntryInfoViewModel;
private readonly Func<IGetEntryById_Entry, EntryReleasesViewModel> _getEntryReleasesViewModel;
private readonly Func<IGetEntryById_Entry, EntryImagesViewModel> _getEntryImagesViewModel;
@ -22,16 +30,20 @@ public partial class PluginDetailsViewModel : RoutableScreen<WorkshopDetailParam
[Notify] private EntryImagesViewModel? _entryImagesViewModel;
public PluginDetailsViewModel(IWorkshopClient client,
IWindowService windowService,
IPluginManagementService pluginManagementService,
Func<IGetEntryById_Entry, EntryInfoViewModel> getEntryInfoViewModel,
Func<IGetEntryById_Entry, EntryReleasesViewModel> getEntryReleasesViewModel,
Func<IGetEntryById_Entry, EntryImagesViewModel> getEntryImagesViewModel)
{
_client = client;
_windowService = windowService;
_pluginManagementService = pluginManagementService;
_getEntryInfoViewModel = getEntryInfoViewModel;
_getEntryReleasesViewModel = getEntryReleasesViewModel;
_getEntryImagesViewModel = getEntryImagesViewModel;
}
public override async Task OnNavigating(WorkshopDetailParameters parameters, NavigationArguments args, CancellationToken cancellationToken)
{
await GetEntry(parameters.EntryId, cancellationToken);
@ -54,6 +66,32 @@ public partial class PluginDetailsViewModel : RoutableScreen<WorkshopDetailParam
EntryInfoViewModel = _getEntryInfoViewModel(Entry);
EntryReleasesViewModel = _getEntryReleasesViewModel(Entry);
EntryImagesViewModel = _getEntryImagesViewModel(Entry);
EntryReleasesViewModel.OnInstallationStarted = OnInstallationStarted;
EntryReleasesViewModel.OnInstallationFinished = OnInstallationFinished;
}
}
private async Task<bool> OnInstallationStarted(IEntryDetails entryDetails)
{
bool confirm = await _windowService.ShowConfirmContentDialog(
"Installing plugin",
$"You are about to install version {entryDetails.LatestRelease?.Version} of {entryDetails.Name}. \r\n\r\n" +
"Plugins are NOT verified by Artemis and could harm your PC, if you have doubts about a plugin please ask on Discord!",
"I trust this plugin, install it"
);
return !confirm;
}
private async Task OnInstallationFinished(InstalledEntry installedEntry)
{
if (!installedEntry.TryGetMetadata("PluginId", out Guid pluginId))
return;
Plugin? plugin = _pluginManagementService.GetAllPlugins().FirstOrDefault(p => p.Guid == pluginId);
if (plugin == null)
return;
await _windowService.CreateContentDialog().WithTitle("Manage plugin").WithViewModel(out PluginDialogViewModel _, plugin).WithFullScreen().ShowAsync();
}
}

View File

@ -1,3 +1,9 @@
namespace Artemis.WebClient.Workshop.Models;
public record PersonalAccessToken(string Key, DateTime CreationTime, DateTime? Expiration, string? Description);
public class PersonalAccessToken
{
public string Key { get; init; }
public DateTimeOffset CreationTime { get; init; }
public DateTimeOffset? Expiration { get; init; }
public string? Description { get; init; }
}

View File

@ -17,7 +17,7 @@ public interface IWorkshopService
InstalledEntry? GetInstalledEntry(IEntryDetails entry);
void RemoveInstalledEntry(InstalledEntry installedEntry);
void SaveInstalledEntry(InstalledEntry entry);
void RemoveOrphanedFiles();
void Initialize();
public record WorkshopStatus(bool IsReachable, string Message);
}

View File

@ -72,7 +72,7 @@ internal class UserManagementService : IUserManagementService
HttpResponseMessage response = await client.PostAsync("user/access-token", JsonContent.Create(new {Description = description, ExpirationDate = expirationDate}), cancellationToken);
response.EnsureSuccessStatusCode();
string? result = await response.Content.ReadFromJsonAsync<string>(cancellationToken: cancellationToken);
string? result = await response.Content.ReadAsStringAsync(cancellationToken);
if (result == null)
throw new ArtemisWebClientException("Failed to deserialize access token");
return result;

View File

@ -1,8 +1,10 @@
using System.Net.Http.Headers;
using Artemis.Core;
using Artemis.Core.Services;
using Artemis.Storage.Entities.Workshop;
using Artemis.Storage.Repositories.Interfaces;
using Artemis.UI.Shared.Routing;
using Artemis.WebClient.Workshop.Exceptions;
using Artemis.WebClient.Workshop.Handlers.UploadHandlers;
using Artemis.WebClient.Workshop.Models;
using Serilog;
@ -15,13 +17,16 @@ public class WorkshopService : IWorkshopService
private readonly IHttpClientFactory _httpClientFactory;
private readonly IRouter _router;
private readonly IEntryRepository _entryRepository;
private readonly IPluginManagementService _pluginManagementService;
private bool _initialized;
public WorkshopService(ILogger logger, IHttpClientFactory httpClientFactory, IRouter router, IEntryRepository entryRepository)
public WorkshopService(ILogger logger, IHttpClientFactory httpClientFactory, IRouter router, IEntryRepository entryRepository, IPluginManagementService pluginManagementService)
{
_logger = logger;
_httpClientFactory = httpClientFactory;
_router = router;
_entryRepository = entryRepository;
_pluginManagementService = pluginManagementService;
}
public async Task<Stream?> GetEntryIcon(long entryId, CancellationToken cancellationToken)
@ -170,7 +175,17 @@ public class WorkshopService : IWorkshopService
}
/// <inheritdoc />
public void RemoveOrphanedFiles()
public void Initialize()
{
if (_initialized)
throw new ArtemisWorkshopException("Workshop service is already initialized");
RemoveOrphanedFiles();
_pluginManagementService.AdditionalPluginDirectories.AddRange(GetInstalledEntries().Where(e => e.EntryType == EntryType.Plugin).Select(e => e.GetReleaseDirectory()));
_initialized = true;
}
private void RemoveOrphanedFiles()
{
List<InstalledEntry> entries = GetInstalledEntries();
foreach (string directory in Directory.GetDirectories(Constants.WorkshopFolder))

View File

@ -2,10 +2,10 @@ namespace Artemis.WebClient.Workshop;
public static class WorkshopConstants
{
public const string AUTHORITY_URL = "https://localhost:5001";
public const string WORKSHOP_URL = "https://localhost:7281";
// public const string AUTHORITY_URL = "https://identity.artemis-rgb.com";
// public const string WORKSHOP_URL = "https://workshop.artemis-rgb.com";
// public const string AUTHORITY_URL = "https://localhost:5001";
// public const string WORKSHOP_URL = "https://localhost:7281";
public const string AUTHORITY_URL = "https://identity.artemis-rgb.com";
public const string WORKSHOP_URL = "https://workshop.artemis-rgb.com";
public const string IDENTITY_CLIENT_NAME = "IdentityApiClient";
public const string WORKSHOP_CLIENT_NAME = "WorkshopApiClient";
}