mirror of
https://github.com/Artemis-RGB/Artemis
synced 2025-12-13 05:48:35 +00:00
Merge branch 'development'
This commit is contained in:
commit
c61e3d289a
@ -12,6 +12,11 @@ namespace Artemis.Core.Services;
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public interface IPluginManagementService : IArtemisService, IDisposable
|
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>
|
/// <summary>
|
||||||
/// Indicates whether or not plugins are currently being loaded
|
/// Indicates whether or not plugins are currently being loaded
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@ -78,8 +78,11 @@ internal class PluginManagementService : IPluginManagementService
|
|||||||
File.Create(Path.Combine(pluginDirectory.FullName, "artemis.lock")).Close();
|
File.Create(Path.Combine(pluginDirectory.FullName, "artemis.lock")).Close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public List<DirectoryInfo> AdditionalPluginDirectories { get; } = new();
|
||||||
|
|
||||||
public bool LoadingPlugins { get; private set; }
|
public bool LoadingPlugins { get; private set; }
|
||||||
|
|
||||||
|
|
||||||
#region Built in plugins
|
#region Built in plugins
|
||||||
|
|
||||||
public void CopyBuiltInPlugins()
|
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
|
// ReSharper disable InconsistentlySynchronizedField - It's read-only, idc
|
||||||
_logger.Debug("Loaded {count} plugin(s)", _plugins.Count);
|
_logger.Debug("Loaded {count} plugin(s)", _plugins.Count);
|
||||||
|
|
||||||
@ -597,7 +612,7 @@ internal class PluginManagementService : IPluginManagementService
|
|||||||
using StreamReader reader = new(metaDataFileEntry.Open());
|
using StreamReader reader = new(metaDataFileEntry.Open());
|
||||||
PluginInfo pluginInfo = CoreJson.DeserializeObject<PluginInfo>(reader.ReadToEnd())!;
|
PluginInfo pluginInfo = CoreJson.DeserializeObject<PluginInfo>(reader.ReadToEnd())!;
|
||||||
if (!pluginInfo.Main.EndsWith(".dll"))
|
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);
|
Plugin? existing = _plugins.FirstOrDefault(p => p.Guid == pluginInfo.Guid);
|
||||||
if (existing != null)
|
if (existing != null)
|
||||||
|
|||||||
@ -35,4 +35,15 @@
|
|||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<AvaloniaResource Include="Assets\**" />
|
<AvaloniaResource Include="Assets\**" />
|
||||||
</ItemGroup>
|
</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>
|
</Project>
|
||||||
@ -13,6 +13,7 @@ using Artemis.UI.Shared.Services;
|
|||||||
using Artemis.VisualScripting.DryIoc;
|
using Artemis.VisualScripting.DryIoc;
|
||||||
using Artemis.WebClient.Updating.DryIoc;
|
using Artemis.WebClient.Updating.DryIoc;
|
||||||
using Artemis.WebClient.Workshop.DryIoc;
|
using Artemis.WebClient.Workshop.DryIoc;
|
||||||
|
using Artemis.WebClient.Workshop.Services;
|
||||||
using Avalonia;
|
using Avalonia;
|
||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
using Avalonia.Controls.ApplicationLifetimes;
|
using Avalonia.Controls.ApplicationLifetimes;
|
||||||
|
|||||||
@ -14,6 +14,7 @@ using Artemis.UI.Screens.Workshop.Library;
|
|||||||
using Artemis.UI.Screens.Workshop.Library.Tabs;
|
using Artemis.UI.Screens.Workshop.Library.Tabs;
|
||||||
using Artemis.UI.Screens.Workshop.Profile;
|
using Artemis.UI.Screens.Workshop.Profile;
|
||||||
using Artemis.UI.Shared.Routing;
|
using Artemis.UI.Shared.Routing;
|
||||||
|
using PluginDetailsViewModel = Artemis.UI.Screens.Workshop.Plugins.PluginDetailsViewModel;
|
||||||
|
|
||||||
namespace Artemis.UI.Routing;
|
namespace Artemis.UI.Routing;
|
||||||
|
|
||||||
@ -32,6 +33,8 @@ public static class Routes
|
|||||||
{
|
{
|
||||||
Children = new List<IRouterRegistration>
|
Children = new List<IRouterRegistration>
|
||||||
{
|
{
|
||||||
|
new RouteRegistration<PluginListViewModel>("plugins/{page:int}"),
|
||||||
|
new RouteRegistration<PluginDetailsViewModel>("plugins/details/{entryId:long}"),
|
||||||
new RouteRegistration<ProfileListViewModel>("profiles/{page:int}"),
|
new RouteRegistration<ProfileListViewModel>("profiles/{page:int}"),
|
||||||
new RouteRegistration<ProfileDetailsViewModel>("profiles/details/{entryId:long}"),
|
new RouteRegistration<ProfileDetailsViewModel>("profiles/details/{entryId:long}"),
|
||||||
new RouteRegistration<LayoutListViewModel>("layouts/{page:int}"),
|
new RouteRegistration<LayoutListViewModel>("layouts/{page:int}"),
|
||||||
|
|||||||
@ -24,7 +24,7 @@
|
|||||||
</Style>
|
</Style>
|
||||||
</Styles>
|
</Styles>
|
||||||
</UserControl.Styles>
|
</UserControl.Styles>
|
||||||
<Grid ColumnDefinitions="250,*">
|
<Grid ColumnDefinitions="350,*" Width="800">
|
||||||
<Grid.RowDefinitions>
|
<Grid.RowDefinitions>
|
||||||
<RowDefinition MinHeight="200" />
|
<RowDefinition MinHeight="200" />
|
||||||
</Grid.RowDefinitions>
|
</Grid.RowDefinitions>
|
||||||
@ -36,7 +36,7 @@
|
|||||||
IsHitTestVisible="False">
|
IsHitTestVisible="False">
|
||||||
<ListBox.ItemTemplate>
|
<ListBox.ItemTemplate>
|
||||||
<DataTemplate DataType="{x:Type prerequisites:PluginPrerequisiteViewModel}">
|
<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">
|
<Border Grid.Row="0" Grid.Column="0" Classes="status-border" IsVisible="{CompiledBinding !IsMet}" Background="#ff3838">
|
||||||
<avalonia:MaterialIcon Kind="Close" />
|
<avalonia:MaterialIcon Kind="Close" />
|
||||||
</Border>
|
</Border>
|
||||||
@ -45,8 +45,8 @@
|
|||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
<StackPanel Margin="8 0 0 0" Grid.Column="1" VerticalAlignment="Stretch">
|
<StackPanel Margin="8 0 0 0" Grid.Column="1" VerticalAlignment="Stretch">
|
||||||
<TextBlock FontWeight="Bold" Text="{CompiledBinding PluginPrerequisite.Name}" TextWrapping="Wrap" />
|
<TextBlock Text="{CompiledBinding PluginPrerequisite.Name}" TextWrapping="Wrap" />
|
||||||
<TextBlock Text="{CompiledBinding PluginPrerequisite.Description}" TextWrapping="Wrap" />
|
<TextBlock Text="{CompiledBinding PluginPrerequisite.Description}" TextWrapping="Wrap" Classes="subtitle"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Grid>
|
</Grid>
|
||||||
</DataTemplate>
|
</DataTemplate>
|
||||||
@ -74,12 +74,14 @@
|
|||||||
Grid.Column="1"
|
Grid.Column="1"
|
||||||
Margin="10 0"
|
Margin="10 0"
|
||||||
IsVisible="{CompiledBinding ShowFailed, Mode=OneWay}">
|
IsVisible="{CompiledBinding ShowFailed, Mode=OneWay}">
|
||||||
<StackPanel Orientation="Horizontal">
|
<TextBlock TextWrapping="Wrap" >
|
||||||
<TextBlock>Installing</TextBlock>
|
<Run>Installing</Run>
|
||||||
<TextBlock Text="{CompiledBinding ActivePrerequisite.PluginPrerequisite.Name, Mode=OneWay}" FontWeight="SemiBold" TextWrapping="Wrap" />
|
<Run Text="{CompiledBinding ActivePrerequisite.PluginPrerequisite.Name, Mode=OneWay}" FontWeight="SemiBold" />
|
||||||
<TextBlock>failed.</TextBlock>
|
<Run>failed.</Run>
|
||||||
</StackPanel>
|
<LineBreak/>
|
||||||
<TextBlock TextWrapping="Wrap">You may try again to see if that helps, otherwise install the prerequisite manually or contact the plugin developer.</TextBlock>
|
<LineBreak/>
|
||||||
|
<Run>You may try again to see if that helps, otherwise install the prerequisite manually or contact the plugin developer.</Run>
|
||||||
|
</TextBlock>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Grid>
|
</Grid>
|
||||||
</UserControl>
|
</UserControl>
|
||||||
@ -30,16 +30,21 @@ public partial class PluginPrerequisitesInstallDialogViewModel : ContentDialogVi
|
|||||||
[Notify] private bool _showIntro = true;
|
[Notify] private bool _showIntro = true;
|
||||||
[Notify] private bool _showProgress;
|
[Notify] private bool _showProgress;
|
||||||
|
|
||||||
|
private bool _finished;
|
||||||
|
|
||||||
public PluginPrerequisitesInstallDialogViewModel(List<IPrerequisitesSubject> subjects, IPrerequisitesVmFactory prerequisitesVmFactory)
|
public PluginPrerequisitesInstallDialogViewModel(List<IPrerequisitesSubject> subjects, IPrerequisitesVmFactory prerequisitesVmFactory)
|
||||||
{
|
{
|
||||||
Prerequisites = new ObservableCollection<PluginPrerequisiteViewModel>();
|
Prerequisites = new ObservableCollection<PluginPrerequisiteViewModel>();
|
||||||
foreach (PluginPrerequisite prerequisite in subjects.SelectMany(prerequisitesSubject => prerequisitesSubject.PlatformPrerequisites))
|
foreach (PluginPrerequisite prerequisite in subjects.SelectMany(prerequisitesSubject => prerequisitesSubject.PlatformPrerequisites))
|
||||||
Prerequisites.Add(prerequisitesVmFactory.PluginPrerequisiteViewModel(prerequisite, false));
|
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);
|
Dispatcher.UIThread.Post(() => CanInstall = Prerequisites.Any(p => !p.PluginPrerequisite.IsMet()), DispatcherPriority.Background);
|
||||||
this.WhenActivated(d =>
|
this.WhenActivated(d =>
|
||||||
{
|
{
|
||||||
|
if (ContentDialog != null)
|
||||||
|
ContentDialog.Closing += ContentDialogOnClosing;
|
||||||
|
|
||||||
Disposable.Create(() =>
|
Disposable.Create(() =>
|
||||||
{
|
{
|
||||||
_tokenSource?.Cancel();
|
_tokenSource?.Cancel();
|
||||||
@ -51,11 +56,12 @@ public partial class PluginPrerequisitesInstallDialogViewModel : ContentDialogVi
|
|||||||
|
|
||||||
public ReactiveCommand<Unit, Unit> Install { get; }
|
public ReactiveCommand<Unit, Unit> Install { get; }
|
||||||
public ObservableCollection<PluginPrerequisiteViewModel> Prerequisites { get; }
|
public ObservableCollection<PluginPrerequisiteViewModel> Prerequisites { get; }
|
||||||
|
|
||||||
public static async Task Show(IWindowService windowService, List<IPrerequisitesSubject> subjects)
|
public static async Task Show(IWindowService windowService, List<IPrerequisitesSubject> subjects)
|
||||||
{
|
{
|
||||||
await windowService.CreateContentDialog()
|
await windowService.CreateContentDialog()
|
||||||
.WithTitle("Plugin prerequisites")
|
.WithTitle("Plugin prerequisites")
|
||||||
|
.WithFullScreen()
|
||||||
.WithViewModel(out PluginPrerequisitesInstallDialogViewModel vm, subjects)
|
.WithViewModel(out PluginPrerequisitesInstallDialogViewModel vm, subjects)
|
||||||
.WithCloseButtonText("Cancel")
|
.WithCloseButtonText("Cancel")
|
||||||
.HavingPrimaryButton(b => b.WithText("Install").WithCommand(vm.Install))
|
.HavingPrimaryButton(b => b.WithText("Install").WithCommand(vm.Install))
|
||||||
@ -63,12 +69,8 @@ public partial class PluginPrerequisitesInstallDialogViewModel : ContentDialogVi
|
|||||||
.ShowAsync();
|
.ShowAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task ExecuteInstall()
|
private void ExecuteInstall()
|
||||||
{
|
{
|
||||||
Deferral? deferral = null;
|
|
||||||
if (ContentDialog != null)
|
|
||||||
ContentDialog.Closing += (_, args) => deferral = args.GetDeferral();
|
|
||||||
|
|
||||||
CanInstall = false;
|
CanInstall = false;
|
||||||
ShowFailed = false;
|
ShowFailed = false;
|
||||||
ShowIntro = false;
|
ShowIntro = false;
|
||||||
@ -77,6 +79,11 @@ public partial class PluginPrerequisitesInstallDialogViewModel : ContentDialogVi
|
|||||||
_tokenSource?.Dispose();
|
_tokenSource?.Dispose();
|
||||||
_tokenSource = new CancellationTokenSource();
|
_tokenSource = new CancellationTokenSource();
|
||||||
|
|
||||||
|
Dispatcher.UIThread.InvokeAsync(async () => await InstallPrerequisites(_tokenSource.Token));
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task InstallPrerequisites(CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
foreach (PluginPrerequisiteViewModel pluginPrerequisiteViewModel in Prerequisites)
|
foreach (PluginPrerequisiteViewModel pluginPrerequisiteViewModel in Prerequisites)
|
||||||
@ -86,7 +93,9 @@ public partial class PluginPrerequisitesInstallDialogViewModel : ContentDialogVi
|
|||||||
continue;
|
continue;
|
||||||
|
|
||||||
ActivePrerequisite = pluginPrerequisiteViewModel;
|
ActivePrerequisite = pluginPrerequisiteViewModel;
|
||||||
await ActivePrerequisite.Install(_tokenSource.Token);
|
await ActivePrerequisite.Install(cancellationToken);
|
||||||
|
|
||||||
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
if (!ActivePrerequisite.IsMet)
|
if (!ActivePrerequisite.IsMet)
|
||||||
{
|
{
|
||||||
@ -98,19 +107,33 @@ public partial class PluginPrerequisitesInstallDialogViewModel : ContentDialogVi
|
|||||||
|
|
||||||
// Wait after the task finished for the user to process what happened
|
// Wait after the task finished for the user to process what happened
|
||||||
if (pluginPrerequisiteViewModel != Prerequisites.Last())
|
if (pluginPrerequisiteViewModel != Prerequisites.Last())
|
||||||
await Task.Delay(250);
|
await Task.Delay(250, cancellationToken);
|
||||||
else
|
else
|
||||||
await Task.Delay(1000);
|
await Task.Delay(1000, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (deferral != null)
|
_finished = true;
|
||||||
deferral.Complete();
|
ContentDialog?.Hide(ContentDialogResult.Primary);
|
||||||
else
|
|
||||||
ContentDialog?.Hide(ContentDialogResult.Primary);
|
|
||||||
}
|
}
|
||||||
catch (OperationCanceledException)
|
catch (TaskCanceledException e)
|
||||||
{
|
{
|
||||||
// ignored
|
// 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -24,7 +24,7 @@
|
|||||||
</Style>
|
</Style>
|
||||||
</Styles>
|
</Styles>
|
||||||
</UserControl.Styles>
|
</UserControl.Styles>
|
||||||
<Grid ColumnDefinitions="250,*">
|
<Grid ColumnDefinitions="350,*" Width="800">
|
||||||
<Grid.RowDefinitions>
|
<Grid.RowDefinitions>
|
||||||
<RowDefinition MinHeight="200" />
|
<RowDefinition MinHeight="200" />
|
||||||
</Grid.RowDefinitions>
|
</Grid.RowDefinitions>
|
||||||
@ -37,8 +37,8 @@
|
|||||||
<ListBox.ItemTemplate>
|
<ListBox.ItemTemplate>
|
||||||
<DataTemplate DataType="{x:Type prerequisites:PluginPrerequisiteViewModel}">
|
<DataTemplate DataType="{x:Type prerequisites:PluginPrerequisiteViewModel}">
|
||||||
<StackPanel Margin="0 6" VerticalAlignment="Stretch">
|
<StackPanel Margin="0 6" VerticalAlignment="Stretch">
|
||||||
<TextBlock FontWeight="Bold" Text="{CompiledBinding PluginPrerequisite.Name}" TextWrapping="Wrap" />
|
<TextBlock Text="{CompiledBinding PluginPrerequisite.Name}" TextWrapping="Wrap" />
|
||||||
<TextBlock Text="{CompiledBinding PluginPrerequisite.Description}" TextWrapping="Wrap" />
|
<TextBlock Text="{CompiledBinding PluginPrerequisite.Description}" TextWrapping="Wrap" Classes="subtitle"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</DataTemplate>
|
</DataTemplate>
|
||||||
</ListBox.ItemTemplate>
|
</ListBox.ItemTemplate>
|
||||||
|
|||||||
@ -64,6 +64,7 @@ public partial class PluginPrerequisitesUninstallDialogViewModel : ContentDialog
|
|||||||
public static async Task Show(IWindowService windowService, List<IPrerequisitesSubject> subjects, string cancelLabel = "Cancel")
|
public static async Task Show(IWindowService windowService, List<IPrerequisitesSubject> subjects, string cancelLabel = "Cancel")
|
||||||
{
|
{
|
||||||
await windowService.CreateContentDialog()
|
await windowService.CreateContentDialog()
|
||||||
|
.WithFullScreen()
|
||||||
.WithTitle("Plugin prerequisites")
|
.WithTitle("Plugin prerequisites")
|
||||||
.WithViewModel(out PluginPrerequisitesUninstallDialogViewModel vm, subjects)
|
.WithViewModel(out PluginPrerequisitesUninstallDialogViewModel vm, subjects)
|
||||||
.WithCloseButtonText(cancelLabel)
|
.WithCloseButtonText(cancelLabel)
|
||||||
|
|||||||
@ -19,7 +19,10 @@
|
|||||||
Grid.RowSpan="3"
|
Grid.RowSpan="3"
|
||||||
VerticalAlignment="Top" />
|
VerticalAlignment="Top" />
|
||||||
|
|
||||||
<TextBlock Grid.Column="1" Grid.Row="0" Classes="h5 no-margin" Text="{CompiledBinding Plugin.Info.Name}" />
|
<TextBlock Grid.Column="1" Grid.Row="0" Classes="no-margin">
|
||||||
|
<Run Classes="h5" Text="{CompiledBinding Plugin.Info.Name}"/>
|
||||||
|
<Run Classes="subtitle" Text="{CompiledBinding Plugin.Info.Version}"/>
|
||||||
|
</TextBlock>
|
||||||
|
|
||||||
<ItemsControl Grid.Column="2" Grid.Row="0" IsVisible="{CompiledBinding Platforms.Count}" ItemsSource="{CompiledBinding Platforms}" HorizontalAlignment="Right">
|
<ItemsControl Grid.Column="2" Grid.Row="0" IsVisible="{CompiledBinding Platforms.Count}" ItemsSource="{CompiledBinding Platforms}" HorizontalAlignment="Right">
|
||||||
<ItemsControl.ItemsPanel>
|
<ItemsControl.ItemsPanel>
|
||||||
|
|||||||
@ -11,6 +11,8 @@ using Artemis.UI.Exceptions;
|
|||||||
using Artemis.UI.Shared;
|
using Artemis.UI.Shared;
|
||||||
using Artemis.UI.Shared.Services;
|
using Artemis.UI.Shared.Services;
|
||||||
using Artemis.UI.Shared.Services.Builders;
|
using Artemis.UI.Shared.Services.Builders;
|
||||||
|
using Artemis.WebClient.Workshop.Models;
|
||||||
|
using Artemis.WebClient.Workshop.Services;
|
||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
using Avalonia.Threading;
|
using Avalonia.Threading;
|
||||||
using Material.Icons;
|
using Material.Icons;
|
||||||
@ -24,6 +26,7 @@ public partial class PluginViewModel : ActivatableViewModelBase
|
|||||||
private readonly ICoreService _coreService;
|
private readonly ICoreService _coreService;
|
||||||
private readonly INotificationService _notificationService;
|
private readonly INotificationService _notificationService;
|
||||||
private readonly IPluginManagementService _pluginManagementService;
|
private readonly IPluginManagementService _pluginManagementService;
|
||||||
|
private readonly IWorkshopService _workshopService;
|
||||||
private readonly IWindowService _windowService;
|
private readonly IWindowService _windowService;
|
||||||
private Window? _settingsWindow;
|
private Window? _settingsWindow;
|
||||||
[Notify] private bool _canInstallPrerequisites;
|
[Notify] private bool _canInstallPrerequisites;
|
||||||
@ -36,13 +39,15 @@ public partial class PluginViewModel : ActivatableViewModelBase
|
|||||||
ICoreService coreService,
|
ICoreService coreService,
|
||||||
IWindowService windowService,
|
IWindowService windowService,
|
||||||
INotificationService notificationService,
|
INotificationService notificationService,
|
||||||
IPluginManagementService pluginManagementService)
|
IPluginManagementService pluginManagementService,
|
||||||
|
IWorkshopService workshopService)
|
||||||
{
|
{
|
||||||
_plugin = plugin;
|
_plugin = plugin;
|
||||||
_coreService = coreService;
|
_coreService = coreService;
|
||||||
_windowService = windowService;
|
_windowService = windowService;
|
||||||
_notificationService = notificationService;
|
_notificationService = notificationService;
|
||||||
_pluginManagementService = pluginManagementService;
|
_pluginManagementService = pluginManagementService;
|
||||||
|
_workshopService = workshopService;
|
||||||
|
|
||||||
Platforms = new ObservableCollection<PluginPlatformViewModel>();
|
Platforms = new ObservableCollection<PluginPlatformViewModel>();
|
||||||
if (Plugin.Info.Platforms != null)
|
if (Plugin.Info.Platforms != null)
|
||||||
@ -90,7 +95,7 @@ public partial class PluginViewModel : ActivatableViewModelBase
|
|||||||
public ObservableCollection<PluginPlatformViewModel> Platforms { get; }
|
public ObservableCollection<PluginPlatformViewModel> Platforms { get; }
|
||||||
public string Type => Plugin.GetType().BaseType?.Name ?? Plugin.GetType().Name;
|
public string Type => Plugin.GetType().BaseType?.Name ?? Plugin.GetType().Name;
|
||||||
public bool IsEnabled => Plugin.IsEnabled;
|
public bool IsEnabled => Plugin.IsEnabled;
|
||||||
|
|
||||||
public async Task UpdateEnabled(bool enable)
|
public async Task UpdateEnabled(bool enable)
|
||||||
{
|
{
|
||||||
if (Enabling)
|
if (Enabling)
|
||||||
@ -209,7 +214,7 @@ public partial class PluginViewModel : ActivatableViewModelBase
|
|||||||
await PluginPrerequisitesInstallDialogViewModel.Show(_windowService, subjects);
|
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};
|
List<IPrerequisitesSubject> subjects = new() {Plugin.Info};
|
||||||
subjects.AddRange(!forPluginRemoval ? Plugin.Features.Where(f => f.AlwaysEnabled) : Plugin.Features);
|
subjects.AddRange(!forPluginRemoval ? Plugin.Features.Where(f => f.AlwaysEnabled) : Plugin.Features);
|
||||||
@ -244,9 +249,6 @@ public partial class PluginViewModel : ActivatableViewModelBase
|
|||||||
return;
|
return;
|
||||||
|
|
||||||
// If the plugin or any of its features has uninstall actions, offer to run these
|
// 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);
|
await ExecuteRemovePrerequisites(true);
|
||||||
|
|
||||||
try
|
try
|
||||||
@ -259,6 +261,10 @@ public partial class PluginViewModel : ActivatableViewModelBase
|
|||||||
throw;
|
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();
|
_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);
|
_windowService.ShowExceptionDialog("Welp, we couldn\'t open the logs folder for you", e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task ShowUpdateEnableFailure(bool enable, Exception e)
|
private async Task ShowUpdateEnableFailure(bool enable, Exception e)
|
||||||
{
|
{
|
||||||
string action = enable ? "enable" : "disable";
|
string action = enable ? "enable" : "disable";
|
||||||
|
|||||||
@ -13,6 +13,7 @@ using Artemis.UI.Shared;
|
|||||||
using Artemis.UI.Shared.Routing;
|
using Artemis.UI.Shared.Routing;
|
||||||
using Artemis.UI.Shared.Services;
|
using Artemis.UI.Shared.Services;
|
||||||
using Artemis.UI.Shared.Services.MainWindow;
|
using Artemis.UI.Shared.Services.MainWindow;
|
||||||
|
using Artemis.WebClient.Workshop.Services;
|
||||||
using Avalonia;
|
using Avalonia;
|
||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
using Avalonia.Controls.ApplicationLifetimes;
|
using Avalonia.Controls.ApplicationLifetimes;
|
||||||
@ -41,6 +42,7 @@ public class RootViewModel : RoutableHostScreen<RoutableScreen>, IMainWindowProv
|
|||||||
IMainWindowService mainWindowService,
|
IMainWindowService mainWindowService,
|
||||||
IDebugService debugService,
|
IDebugService debugService,
|
||||||
IUpdateService updateService,
|
IUpdateService updateService,
|
||||||
|
IWorkshopService workshopService,
|
||||||
SidebarViewModel sidebarViewModel,
|
SidebarViewModel sidebarViewModel,
|
||||||
DefaultTitleBarViewModel defaultTitleBarViewModel)
|
DefaultTitleBarViewModel defaultTitleBarViewModel)
|
||||||
{
|
{
|
||||||
@ -64,7 +66,7 @@ public class RootViewModel : RoutableHostScreen<RoutableScreen>, IMainWindowProv
|
|||||||
OpenScreen = ReactiveCommand.Create<string?>(ExecuteOpenScreen);
|
OpenScreen = ReactiveCommand.Create<string?>(ExecuteOpenScreen);
|
||||||
OpenDebugger = ReactiveCommand.CreateFromTask(ExecuteOpenDebugger);
|
OpenDebugger = ReactiveCommand.CreateFromTask(ExecuteOpenDebugger);
|
||||||
Exit = ReactiveCommand.CreateFromTask(ExecuteExit);
|
Exit = ReactiveCommand.CreateFromTask(ExecuteExit);
|
||||||
|
|
||||||
_titleBarViewModel = this.WhenAnyValue(vm => vm.Screen)
|
_titleBarViewModel = this.WhenAnyValue(vm => vm.Screen)
|
||||||
.Select(s => s as IMainScreenViewModel)
|
.Select(s => s as IMainScreenViewModel)
|
||||||
.Select(s => s?.WhenAnyValue(svm => svm.TitleBarViewModel) ?? Observable.Never<ViewModelBase>())
|
.Select(s => s?.WhenAnyValue(svm => svm.TitleBarViewModel) ?? Observable.Never<ViewModelBase>())
|
||||||
@ -74,10 +76,15 @@ public class RootViewModel : RoutableHostScreen<RoutableScreen>, IMainWindowProv
|
|||||||
|
|
||||||
Task.Run(() =>
|
Task.Run(() =>
|
||||||
{
|
{
|
||||||
|
// Before doing heavy lifting, initialize the update service which may prompt a restart
|
||||||
if (_updateService.Initialize())
|
if (_updateService.Initialize())
|
||||||
return;
|
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();
|
coreService.Initialize();
|
||||||
|
|
||||||
registrationService.RegisterBuiltInDataModelDisplays();
|
registrationService.RegisterBuiltInDataModelDisplays();
|
||||||
registrationService.RegisterBuiltInDataModelInputs();
|
registrationService.RegisterBuiltInDataModelInputs();
|
||||||
registrationService.RegisterBuiltInPropertyEditors();
|
registrationService.RegisterBuiltInPropertyEditors();
|
||||||
@ -135,7 +142,7 @@ public class RootViewModel : RoutableHostScreen<RoutableScreen>, IMainWindowProv
|
|||||||
{
|
{
|
||||||
if (path != null)
|
if (path != null)
|
||||||
_router.ClearPreviousWindowRoute();
|
_router.ClearPreviousWindowRoute();
|
||||||
|
|
||||||
// The window will open on the UI thread at some point, respond to that to select the chosen screen
|
// The window will open on the UI thread at some point, respond to that to select the chosen screen
|
||||||
MainWindowOpened += OnEventHandler;
|
MainWindowOpened += OnEventHandler;
|
||||||
OpenMainWindow();
|
OpenMainWindow();
|
||||||
@ -184,7 +191,7 @@ public class RootViewModel : RoutableHostScreen<RoutableScreen>, IMainWindowProv
|
|||||||
_lifeTime.MainWindow.Activate();
|
_lifeTime.MainWindow.Activate();
|
||||||
if (_lifeTime.MainWindow.WindowState == WindowState.Minimized)
|
if (_lifeTime.MainWindow.WindowState == WindowState.Minimized)
|
||||||
_lifeTime.MainWindow.WindowState = WindowState.Normal;
|
_lifeTime.MainWindow.WindowState = WindowState.Normal;
|
||||||
|
|
||||||
OnMainWindowOpened();
|
OnMainWindowOpened();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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>
|
||||||
@ -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<CreatePersonalAccessTokenViewModel>
|
||||||
|
{
|
||||||
|
public CreatePersonalAccessTokenView()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
this.WhenActivated(_ => this.ClearAllDataValidationErrors());
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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<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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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>
|
||||||
@ -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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -5,10 +5,15 @@
|
|||||||
xmlns:settings="clr-namespace:Artemis.UI.Screens.Settings"
|
xmlns:settings="clr-namespace:Artemis.UI.Screens.Settings"
|
||||||
xmlns:asyncImageLoader="clr-namespace:AsyncImageLoader;assembly=AsyncImageLoader.Avalonia"
|
xmlns:asyncImageLoader="clr-namespace:AsyncImageLoader;assembly=AsyncImageLoader.Avalonia"
|
||||||
xmlns:loaders="clr-namespace:AsyncImageLoader.Loaders;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"
|
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="800"
|
||||||
x:Class="Artemis.UI.Screens.Settings.AccountTabView"
|
x:Class="Artemis.UI.Screens.Settings.AccountTabView"
|
||||||
x:DataType="settings:AccountTabViewModel">
|
x:DataType="settings:AccountTabViewModel">
|
||||||
|
<UserControl.Resources>
|
||||||
|
<converters:DateTimeConverter x:Key="DateTimeConverter" />
|
||||||
|
</UserControl.Resources>
|
||||||
<Panel>
|
<Panel>
|
||||||
<StackPanel IsVisible="{CompiledBinding !IsLoggedIn^}" Margin="0 50 0 0">
|
<StackPanel IsVisible="{CompiledBinding !IsLoggedIn^}" Margin="0 50 0 0">
|
||||||
<StackPanel.Styles>
|
<StackPanel.Styles>
|
||||||
@ -46,7 +51,7 @@
|
|||||||
</asyncImageLoader:AdvancedImage.Loader>
|
</asyncImageLoader:AdvancedImage.Loader>
|
||||||
</asyncImageLoader:AdvancedImage>
|
</asyncImageLoader:AdvancedImage>
|
||||||
</Border>
|
</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}" />
|
<TextBlock Classes="subtitle" Grid.Column="1" Grid.Row="1" Padding="0" VerticalAlignment="Top" Text="{CompiledBinding Email}" />
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
@ -66,8 +71,8 @@
|
|||||||
<Button Width="150" Content="Change password" Command="{CompiledBinding ChangePasswordAddress}" />
|
<Button Width="150" Content="Change password" Command="{CompiledBinding ChangePasswordAddress}" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Grid>
|
</Grid>
|
||||||
<Border Classes="card-separator" IsVisible="{CompiledBinding CanChangePassword}"/>
|
<Border Classes="card-separator" IsVisible="{CompiledBinding CanChangePassword}" />
|
||||||
|
|
||||||
<Grid RowDefinitions="*,*" ColumnDefinitions="*,Auto">
|
<Grid RowDefinitions="*,*" ColumnDefinitions="*,Auto">
|
||||||
<StackPanel Grid.Column="0" VerticalAlignment="Center">
|
<StackPanel Grid.Column="0" VerticalAlignment="Center">
|
||||||
<TextBlock>
|
<TextBlock>
|
||||||
@ -99,16 +104,55 @@
|
|||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
<!-- <TextBlock Classes="card-title"> -->
|
<TextBlock Classes="card-title">
|
||||||
<!-- Personal access tokens -->
|
Personal access tokens
|
||||||
<!-- </TextBlock> -->
|
</TextBlock>
|
||||||
<!-- <Border Classes="card" VerticalAlignment="Stretch" Margin="0,0,5,0"> -->
|
<Border Classes="card" VerticalAlignment="Stretch" Margin="0,0,5,0">
|
||||||
<!-- <StackPanel> -->
|
<StackPanel>
|
||||||
<!-- <TextBlock>TODO :)</TextBlock> -->
|
<DockPanel Margin="0 0 0 10">
|
||||||
<!-- </StackPanel> -->
|
<TextBlock VerticalAlignment="Center">
|
||||||
<!-- </Border> -->
|
Tokens be used to communicate with Artemis APIs without using a username and password
|
||||||
|
</TextBlock>
|
||||||
|
<Button HorizontalAlignment="Right" Command="{CompiledBinding GenerateToken}">Generate token</Button>
|
||||||
|
</DockPanel>
|
||||||
|
|
||||||
|
<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>
|
||||||
|
|
||||||
|
<TextBlock IsVisible="{CompiledBinding !PersonalAccessTokens.Count}" TextAlignment="Center" Classes="subtitle" Margin="0 10">
|
||||||
|
You have no active personal access tokens.
|
||||||
|
</TextBlock>
|
||||||
|
|
||||||
|
<ItemsControl ItemsSource="{CompiledBinding PersonalAccessTokens}" IsVisible="{CompiledBinding PersonalAccessTokens.Count}">
|
||||||
|
<ItemsControl.Styles>
|
||||||
|
<Style Selector="ContentPresenter:nth-child(odd) > 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>
|
</StackPanel>
|
||||||
</ScrollViewer>
|
</ScrollViewer>
|
||||||
</Panel>
|
</Panel>
|
||||||
|
|
||||||
</UserControl>
|
</UserControl>
|
||||||
@ -1,7 +1,10 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using System.Reactive;
|
||||||
using System.Reactive.Disposables;
|
using System.Reactive.Disposables;
|
||||||
using System.Reactive.Linq;
|
using System.Reactive.Linq;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
@ -12,6 +15,7 @@ using Artemis.UI.Shared.Routing;
|
|||||||
using Artemis.UI.Shared.Services;
|
using Artemis.UI.Shared.Services;
|
||||||
using Artemis.WebClient.Workshop;
|
using Artemis.WebClient.Workshop;
|
||||||
using Artemis.WebClient.Workshop.Handlers.UploadHandlers;
|
using Artemis.WebClient.Workshop.Handlers.UploadHandlers;
|
||||||
|
using Artemis.WebClient.Workshop.Models;
|
||||||
using Artemis.WebClient.Workshop.Services;
|
using Artemis.WebClient.Workshop.Services;
|
||||||
using IdentityModel;
|
using IdentityModel;
|
||||||
using PropertyChanged.SourceGenerator;
|
using PropertyChanged.SourceGenerator;
|
||||||
@ -29,6 +33,7 @@ public partial class AccountTabViewModel : RoutableScreen
|
|||||||
[Notify(Setter.Private)] private string? _name;
|
[Notify(Setter.Private)] private string? _name;
|
||||||
[Notify(Setter.Private)] private string? _email;
|
[Notify(Setter.Private)] private string? _email;
|
||||||
[Notify(Setter.Private)] private string? _avatarUrl;
|
[Notify(Setter.Private)] private string? _avatarUrl;
|
||||||
|
[Notify(Setter.Private)] private ObservableCollection<PersonalAccessToken> _personalAccessTokens = new();
|
||||||
|
|
||||||
public AccountTabViewModel(IWindowService windowService, IAuthenticationService authenticationService, IUserManagementService userManagementService)
|
public AccountTabViewModel(IWindowService windowService, IAuthenticationService authenticationService, IUserManagementService userManagementService)
|
||||||
{
|
{
|
||||||
@ -36,10 +41,11 @@ public partial class AccountTabViewModel : RoutableScreen
|
|||||||
_authenticationService = authenticationService;
|
_authenticationService = authenticationService;
|
||||||
_userManagementService = userManagementService;
|
_userManagementService = userManagementService;
|
||||||
_authenticationService.AutoLogin(true);
|
_authenticationService.AutoLogin(true);
|
||||||
|
|
||||||
DisplayName = "Account";
|
DisplayName = "Account";
|
||||||
IsLoggedIn = _authenticationService.IsLoggedIn;
|
IsLoggedIn = _authenticationService.IsLoggedIn;
|
||||||
|
DeleteToken = ReactiveCommand.CreateFromTask<PersonalAccessToken>(ExecuteDeleteToken);
|
||||||
|
|
||||||
this.WhenActivated(d =>
|
this.WhenActivated(d =>
|
||||||
{
|
{
|
||||||
_canChangePassword = _authenticationService.GetClaim(JwtClaimTypes.AuthenticationMethod).Select(c => c?.Value == "pwd").ToProperty(this, vm => vm.CanChangePassword);
|
_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 bool CanChangePassword => _canChangePassword?.Value ?? false;
|
||||||
public IObservable<bool> IsLoggedIn { get; }
|
public IObservable<bool> IsLoggedIn { get; }
|
||||||
|
public ReactiveCommand<PersonalAccessToken,Unit> DeleteToken { get; }
|
||||||
|
|
||||||
public async Task Login()
|
public async Task Login()
|
||||||
{
|
{
|
||||||
await _windowService.CreateContentDialog().WithViewModel(out WorkshopLoginViewModel _).WithTitle("Account login").ShowAsync();
|
await _windowService.CreateContentDialog().WithViewModel(out WorkshopLoginViewModel _).WithTitle("Account login").ShowAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task ChangeAvatar()
|
public async Task ChangeAvatar()
|
||||||
{
|
{
|
||||||
string[]? result = await _windowService.CreateOpenFileDialog().HavingFilter(f => f.WithBitmaps()).ShowAsync();
|
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))
|
.HavingPrimaryButton(b => b.WithText("Submit").WithCommand(vm.Submit))
|
||||||
.ShowAsync();
|
.ShowAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task ChangePasswordAddress()
|
public async Task ChangePasswordAddress()
|
||||||
{
|
{
|
||||||
await _windowService.CreateContentDialog().WithTitle("Change password")
|
await _windowService.CreateContentDialog().WithTitle("Change password")
|
||||||
@ -94,7 +101,7 @@ public partial class AccountTabViewModel : RoutableScreen
|
|||||||
.HavingPrimaryButton(b => b.WithText("Submit").WithCommand(vm.Submit))
|
.HavingPrimaryButton(b => b.WithText("Submit").WithCommand(vm.Submit))
|
||||||
.ShowAsync();
|
.ShowAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task RemoveAccount()
|
public async Task RemoveAccount()
|
||||||
{
|
{
|
||||||
await _windowService.CreateContentDialog().WithTitle("Remove account")
|
await _windowService.CreateContentDialog().WithTitle("Remove account")
|
||||||
@ -104,11 +111,36 @@ public partial class AccountTabViewModel : RoutableScreen
|
|||||||
.ShowAsync();
|
.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;
|
string? userId = _authenticationService.Claims.FirstOrDefault(c => c.Type == "sub")?.Value;
|
||||||
Name = _authenticationService.Claims.FirstOrDefault(c => c.Type == "name")?.Value;
|
Name = _authenticationService.Claims.FirstOrDefault(c => c.Type == "name")?.Value;
|
||||||
Email = _authenticationService.Claims.FirstOrDefault(c => c.Type == "email")?.Value;
|
Email = _authenticationService.Claims.FirstOrDefault(c => c.Type == "email")?.Value;
|
||||||
AvatarUrl = $"{WorkshopConstants.AUTHORITY_URL}/user/avatar/{userId}";
|
AvatarUrl = $"{WorkshopConstants.AUTHORITY_URL}/user/avatar/{userId}";
|
||||||
|
|
||||||
|
List<PersonalAccessToken> personalAccessTokens = await _userManagementService.GetPersonAccessTokens(CancellationToken.None);
|
||||||
|
PersonalAccessTokens = new ObservableCollection<PersonalAccessToken>(personalAccessTokens);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -43,6 +43,7 @@ public partial class SidebarViewModel : ActivatableViewModelBase
|
|||||||
{
|
{
|
||||||
new(MaterialIconKind.FolderVideo, "Profiles", "workshop/entries/profiles/1", "workshop/entries/profiles"),
|
new(MaterialIconKind.FolderVideo, "Profiles", "workshop/entries/profiles/1", "workshop/entries/profiles"),
|
||||||
new(MaterialIconKind.KeyboardVariant, "Layouts", "workshop/entries/layouts/1", "workshop/entries/layouts"),
|
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"),
|
new(MaterialIconKind.Bookshelf, "Library", "workshop/library"),
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
|||||||
@ -9,7 +9,6 @@ using Artemis.UI.Shared.Utilities;
|
|||||||
using Artemis.WebClient.Workshop;
|
using Artemis.WebClient.Workshop;
|
||||||
using Artemis.WebClient.Workshop.Handlers.InstallationHandlers;
|
using Artemis.WebClient.Workshop.Handlers.InstallationHandlers;
|
||||||
using Artemis.WebClient.Workshop.Models;
|
using Artemis.WebClient.Workshop.Models;
|
||||||
using Artemis.WebClient.Workshop.Services;
|
|
||||||
using Humanizer;
|
using Humanizer;
|
||||||
using ReactiveUI;
|
using ReactiveUI;
|
||||||
|
|
||||||
@ -29,11 +28,13 @@ public class EntryReleasesViewModel : ViewModelBase
|
|||||||
|
|
||||||
Entry = entry;
|
Entry = entry;
|
||||||
DownloadLatestRelease = ReactiveCommand.CreateFromTask(ExecuteDownloadLatestRelease);
|
DownloadLatestRelease = ReactiveCommand.CreateFromTask(ExecuteDownloadLatestRelease);
|
||||||
|
OnInstallationStarted = Confirm;
|
||||||
}
|
}
|
||||||
|
|
||||||
public IGetEntryById_Entry Entry { get; }
|
public IGetEntryById_Entry Entry { get; }
|
||||||
public ReactiveCommand<Unit, Unit> DownloadLatestRelease { get; }
|
public ReactiveCommand<Unit, Unit> DownloadLatestRelease { get; }
|
||||||
|
|
||||||
|
public Func<IEntryDetails, Task<bool>> OnInstallationStarted { get; set; }
|
||||||
public Func<InstalledEntry, Task>? OnInstallationFinished { get; set; }
|
public Func<InstalledEntry, Task>? OnInstallationFinished { get; set; }
|
||||||
|
|
||||||
private async Task ExecuteDownloadLatestRelease(CancellationToken cancellationToken)
|
private async Task ExecuteDownloadLatestRelease(CancellationToken cancellationToken)
|
||||||
@ -41,11 +42,7 @@ public class EntryReleasesViewModel : ViewModelBase
|
|||||||
if (Entry.LatestRelease == null)
|
if (Entry.LatestRelease == null)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
bool confirm = await _windowService.ShowConfirmContentDialog(
|
if (await OnInstallationStarted(Entry))
|
||||||
"Install latest release",
|
|
||||||
$"Are you sure you want to download and install version {Entry.LatestRelease.Version} of {Entry.Name}?"
|
|
||||||
);
|
|
||||||
if (!confirm)
|
|
||||||
return;
|
return;
|
||||||
|
|
||||||
IEntryInstallationHandler installationHandler = _factory.CreateHandler(Entry.EntryType);
|
IEntryInstallationHandler installationHandler = _factory.CreateHandler(Entry.EntryType);
|
||||||
@ -64,4 +61,14 @@ public class EntryReleasesViewModel : ViewModelBase
|
|||||||
.WithSeverity(NotificationSeverity.Error).Show();
|
.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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -25,7 +25,8 @@ public partial class EntriesViewModel : RoutableHostScreen<RoutableScreen>
|
|||||||
Tabs = new ObservableCollection<RouteViewModel>
|
Tabs = new ObservableCollection<RouteViewModel>
|
||||||
{
|
{
|
||||||
new("Profiles", "workshop/entries/profiles/1", "workshop/entries/profiles"),
|
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 =>
|
this.WhenActivated(d =>
|
||||||
|
|||||||
@ -34,6 +34,7 @@ public class EntryListItemViewModel : ActivatableViewModelBase
|
|||||||
await _router.Navigate($"workshop/entries/profiles/details/{Entry.Id}");
|
await _router.Navigate($"workshop/entries/profiles/details/{Entry.Id}");
|
||||||
break;
|
break;
|
||||||
case EntryType.Plugin:
|
case EntryType.Plugin:
|
||||||
|
await _router.Navigate($"workshop/entries/plugins/details/{Entry.Id}");
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
throw new ArgumentOutOfRangeException();
|
throw new ArgumentOutOfRangeException();
|
||||||
|
|||||||
@ -27,6 +27,7 @@ public class LayoutListViewModel : List.EntryListViewModel
|
|||||||
And = new[]
|
And = new[]
|
||||||
{
|
{
|
||||||
new EntryFilterInput {EntryType = new EntryTypeOperationFilterInput {Eq = EntryType.Layout}},
|
new EntryFilterInput {EntryType = new EntryTypeOperationFilterInput {Eq = EntryType.Layout}},
|
||||||
|
new EntryFilterInput(){LatestReleaseId = new LongOperationFilterInput {Gt = 0}},
|
||||||
base.GetFilter()
|
base.GetFilter()
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@ -0,0 +1,65 @@
|
|||||||
|
<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:tabs="clr-namespace:Artemis.UI.Screens.Workshop.Entries.Tabs"
|
||||||
|
xmlns:pagination="clr-namespace:Artemis.UI.Shared.Pagination;assembly=Artemis.UI.Shared"
|
||||||
|
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||||
|
x:Class="Artemis.UI.Screens.Workshop.Entries.Tabs.PluginListView"
|
||||||
|
x:DataType="tabs:PluginListViewModel">
|
||||||
|
<UserControl.Styles>
|
||||||
|
<Styles>
|
||||||
|
<Style Selector="StackPanel.empty-state > TextBlock">
|
||||||
|
<Setter Property="TextAlignment" Value="Center"></Setter>
|
||||||
|
<Setter Property="TextWrapping" Value="Wrap"></Setter>
|
||||||
|
</Style>
|
||||||
|
</Styles>
|
||||||
|
</UserControl.Styles>
|
||||||
|
<Grid ColumnDefinitions="300,*" RowDefinitions="Auto,*,Auto">
|
||||||
|
<StackPanel Grid.Column="0" Grid.RowSpan="3" Margin="0 0 10 0" VerticalAlignment="Top">
|
||||||
|
<Border Classes="card" VerticalAlignment="Stretch">
|
||||||
|
<StackPanel>
|
||||||
|
<TextBlock Theme="{StaticResource SubtitleTextBlockStyle}">Categories</TextBlock>
|
||||||
|
<Border Classes="card-separator" />
|
||||||
|
<ContentControl Content="{CompiledBinding CategoriesViewModel}"></ContentControl>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<ProgressBar Grid.Column="1" Grid.Row="0" VerticalAlignment="Top" Margin="0 0 20 0" IsVisible="{CompiledBinding IsLoading}" IsIndeterminate="True" />
|
||||||
|
<ContentControl Grid.Column="1" Grid.Row="0" Margin="0 0 20 8" Content="{CompiledBinding InputViewModel}"/>
|
||||||
|
|
||||||
|
<ScrollViewer Grid.Column="1" Grid.Row="1">
|
||||||
|
<ItemsControl ItemsSource="{CompiledBinding Entries}" Margin="0 0 20 0">
|
||||||
|
<ItemsControl.ItemsPanel>
|
||||||
|
<ItemsPanelTemplate>
|
||||||
|
<VirtualizingStackPanel />
|
||||||
|
</ItemsPanelTemplate>
|
||||||
|
</ItemsControl.ItemsPanel>
|
||||||
|
<ItemsControl.ItemTemplate>
|
||||||
|
<DataTemplate>
|
||||||
|
<ContentControl Content="{CompiledBinding}" Margin="0 0 0 5"></ContentControl>
|
||||||
|
</DataTemplate>
|
||||||
|
</ItemsControl.ItemTemplate>
|
||||||
|
</ItemsControl>
|
||||||
|
</ScrollViewer>
|
||||||
|
|
||||||
|
<Panel Grid.Column="1" Grid.Row="1" IsVisible="{CompiledBinding !IsLoading}">
|
||||||
|
<StackPanel IsVisible="{CompiledBinding !Entries.Count}" Margin="0 50 0 0" Classes="empty-state">
|
||||||
|
<TextBlock Theme="{StaticResource TitleTextBlockStyle}">Looks like your current filters gave no results</TextBlock>
|
||||||
|
<TextBlock>
|
||||||
|
<Run>Modify or clear your filters to view other plugins</Run>
|
||||||
|
</TextBlock>
|
||||||
|
<Lottie Path="/Assets/Animations/empty.json" RepeatCount="1" Width="350" Height="350"></Lottie>
|
||||||
|
</StackPanel>
|
||||||
|
</Panel>
|
||||||
|
|
||||||
|
<pagination:Pagination Grid.Column="1"
|
||||||
|
Grid.Row="2"
|
||||||
|
Margin="0 20 0 10"
|
||||||
|
IsVisible="{CompiledBinding ShowPagination}"
|
||||||
|
Value="{CompiledBinding Page}"
|
||||||
|
Maximum="{CompiledBinding TotalPages}"
|
||||||
|
HorizontalAlignment="Center" />
|
||||||
|
</Grid>
|
||||||
|
</UserControl>
|
||||||
@ -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<PluginListViewModel>
|
||||||
|
{
|
||||||
|
public PluginListView()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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<IGetEntries_Entries_Items, EntryListItemViewModel> 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()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -12,7 +12,7 @@ public class ProfileListViewModel : List.EntryListViewModel
|
|||||||
public ProfileListViewModel(IWorkshopClient workshopClient,
|
public ProfileListViewModel(IWorkshopClient workshopClient,
|
||||||
IRouter router,
|
IRouter router,
|
||||||
CategoriesViewModel categoriesViewModel,
|
CategoriesViewModel categoriesViewModel,
|
||||||
List.EntryListInputViewModel entryListInputViewModel,
|
EntryListInputViewModel entryListInputViewModel,
|
||||||
INotificationService notificationService,
|
INotificationService notificationService,
|
||||||
Func<IGetEntries_Entries_Items, EntryListItemViewModel> getEntryListViewModel)
|
Func<IGetEntries_Entries_Items, EntryListItemViewModel> getEntryListViewModel)
|
||||||
: base("workshop/entries/profiles", workshopClient, router, categoriesViewModel, entryListInputViewModel, notificationService, getEntryListViewModel)
|
: base("workshop/entries/profiles", workshopClient, router, categoriesViewModel, entryListInputViewModel, notificationService, getEntryListViewModel)
|
||||||
|
|||||||
@ -57,6 +57,14 @@
|
|||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
|
<Button Width="150" Height="180" Command="{CompiledBinding Navigate}" CommandParameter="workshop/entries/plugins/1" VerticalContentAlignment="Top">
|
||||||
|
<StackPanel>
|
||||||
|
<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>
|
||||||
|
</Button>
|
||||||
|
|
||||||
<Button Width="150" Height="180" Command="{CompiledBinding Navigate}" CommandParameter="workshop/library" VerticalContentAlignment="Top">
|
<Button Width="150" Height="180" Command="{CompiledBinding Navigate}" CommandParameter="workshop/library" VerticalContentAlignment="Top">
|
||||||
<StackPanel>
|
<StackPanel>
|
||||||
<avalonia:MaterialIcon Kind="Bookshelf" HorizontalAlignment="Left" Width="60" Height="60" Margin="0 5" />
|
<avalonia:MaterialIcon Kind="Bookshelf" HorizontalAlignment="Left" Width="60" Height="60" Margin="0 5" />
|
||||||
|
|||||||
@ -20,7 +20,7 @@
|
|||||||
HorizontalAlignment="Stretch"
|
HorizontalAlignment="Stretch"
|
||||||
HorizontalContentAlignment="Stretch"
|
HorizontalContentAlignment="Stretch"
|
||||||
Command="{CompiledBinding ViewWorkshopPage}">
|
Command="{CompiledBinding ViewWorkshopPage}">
|
||||||
<Grid ColumnDefinitions="Auto,*,*,*,Auto">
|
<Grid ColumnDefinitions="Auto,2*,*,*,*,Auto">
|
||||||
<Border Grid.Column="0"
|
<Border Grid.Column="0"
|
||||||
CornerRadius="6"
|
CornerRadius="6"
|
||||||
VerticalAlignment="Center"
|
VerticalAlignment="Center"
|
||||||
@ -42,12 +42,13 @@
|
|||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
<TextBlock Grid.Column="2" VerticalAlignment="Center" Text="{CompiledBinding InstalledEntry.EntryType}"></TextBlock>
|
<TextBlock Grid.Column="2" VerticalAlignment="Center" Text="{CompiledBinding InstalledEntry.EntryType}"></TextBlock>
|
||||||
<TextBlock Grid.Column="3" VerticalAlignment="Center">
|
<TextBlock Grid.Column="3" VerticalAlignment="Center" Text="{CompiledBinding InstalledEntry.ReleaseVersion}"></TextBlock>
|
||||||
|
<TextBlock Grid.Column="4" VerticalAlignment="Center">
|
||||||
<Run>Installed</Run>
|
<Run>Installed</Run>
|
||||||
<Run Text="{CompiledBinding InstalledEntry.InstalledAt, FallbackValue=01-01-1337, Mode=OneWay, Converter={StaticResource DateTimeConverter}}" />
|
<Run Text="{CompiledBinding InstalledEntry.InstalledAt, FallbackValue=01-01-1337, Mode=OneWay, Converter={StaticResource DateTimeConverter}}" />
|
||||||
</TextBlock>
|
</TextBlock>
|
||||||
|
|
||||||
<StackPanel Grid.Column="4" VerticalAlignment="Center" Orientation="Horizontal" Spacing="6">
|
<StackPanel Grid.Column="5" VerticalAlignment="Center" Orientation="Horizontal" Spacing="6">
|
||||||
<Button Command="{CompiledBinding ViewLocal}">Open</Button>
|
<Button Command="{CompiledBinding ViewLocal}">Open</Button>
|
||||||
<Button Command="{CompiledBinding Uninstall}" Theme="{StaticResource TransparentButton}" Height="32">
|
<Button Command="{CompiledBinding Uninstall}" Theme="{StaticResource TransparentButton}" Height="32">
|
||||||
<avalonia:MaterialIcon Kind="Trash"/>
|
<avalonia:MaterialIcon Kind="Trash"/>
|
||||||
|
|||||||
@ -1,7 +1,12 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Linq;
|
||||||
using System.Reactive;
|
using System.Reactive;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
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;
|
||||||
using Artemis.UI.Shared.Routing;
|
using Artemis.UI.Shared.Routing;
|
||||||
using Artemis.UI.Shared.Services;
|
using Artemis.UI.Shared.Services;
|
||||||
@ -20,14 +25,24 @@ public partial class InstalledTabItemViewModel : ViewModelBase
|
|||||||
private readonly IRouter _router;
|
private readonly IRouter _router;
|
||||||
private readonly EntryInstallationHandlerFactory _factory;
|
private readonly EntryInstallationHandlerFactory _factory;
|
||||||
private readonly IWindowService _windowService;
|
private readonly IWindowService _windowService;
|
||||||
|
private readonly IPluginManagementService _pluginManagementService;
|
||||||
|
private readonly ISettingsVmFactory _settingsVmFactory;
|
||||||
[Notify(Setter.Private)] private bool _isRemoved;
|
[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;
|
_workshopService = workshopService;
|
||||||
_router = router;
|
_router = router;
|
||||||
_factory = factory;
|
_factory = factory;
|
||||||
_windowService = windowService;
|
_windowService = windowService;
|
||||||
|
_pluginManagementService = pluginManagementService;
|
||||||
|
_settingsVmFactory = settingsVmFactory;
|
||||||
InstalledEntry = installedEntry;
|
InstalledEntry = installedEntry;
|
||||||
|
|
||||||
ViewWorkshopPage = ReactiveCommand.CreateFromTask(ExecuteViewWorkshopPage);
|
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.");
|
bool confirmed = await _windowService.ShowConfirmContentDialog("Do you want to uninstall this entry?", "Both the entry and its contents will be removed.");
|
||||||
if (!confirmed)
|
if (!confirmed)
|
||||||
return;
|
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);
|
IEntryInstallationHandler handler = _factory.CreateHandler(InstalledEntry.EntryType);
|
||||||
await handler.UninstallAsync(InstalledEntry, cancellationToken);
|
await handler.UninstallAsync(InstalledEntry, cancellationToken);
|
||||||
IsRemoved = true;
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -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>
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
using Avalonia.ReactiveUI;
|
||||||
|
|
||||||
|
namespace Artemis.UI.Screens.Workshop.Plugins.Dialogs;
|
||||||
|
|
||||||
|
public partial class PluginDialogView : ReactiveUserControl<PluginDialogViewModel>
|
||||||
|
{
|
||||||
|
public PluginDialogView()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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; }
|
||||||
|
}
|
||||||
@ -0,0 +1,30 @@
|
|||||||
|
<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: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.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">
|
||||||
|
<ContentControl Content="{CompiledBinding EntryInfoViewModel}" />
|
||||||
|
</Border>
|
||||||
|
<Border Classes="card" VerticalAlignment="Top" IsVisible="{CompiledBinding Entry.LatestRelease, Converter={x:Static ObjectConverters.IsNotNull}}">
|
||||||
|
<ContentControl Content="{CompiledBinding EntryReleasesViewModel}" />
|
||||||
|
</Border>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<Border Classes="card" Grid.Row="1" Grid.Column="1" Margin="10 0">
|
||||||
|
<mdxaml:MarkdownScrollViewer Markdown="{CompiledBinding Entry.Description}" MarkdownStyleName="FluentAvalonia">
|
||||||
|
<mdxaml:MarkdownScrollViewer.Styles>
|
||||||
|
<StyleInclude Source="/Styles/Markdown.axaml" />
|
||||||
|
</mdxaml:MarkdownScrollViewer.Styles>
|
||||||
|
</mdxaml:MarkdownScrollViewer>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<ContentControl Grid.Row="1" Grid.Column="2" IsVisible="{CompiledBinding Entry.Images.Count}" Content="{CompiledBinding EntryImagesViewModel}" />
|
||||||
|
</Grid>
|
||||||
|
</UserControl>
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
using Avalonia.ReactiveUI;
|
||||||
|
|
||||||
|
namespace Artemis.UI.Screens.Workshop.Plugins;
|
||||||
|
|
||||||
|
public partial class PluginDetailsView : ReactiveUserControl<PluginDetailsViewModel>
|
||||||
|
{
|
||||||
|
public PluginDetailsView()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,97 @@
|
|||||||
|
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.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;
|
||||||
|
[Notify] private IGetEntryById_Entry? _entry;
|
||||||
|
[Notify] private EntryInfoViewModel? _entryInfoViewModel;
|
||||||
|
[Notify] private EntryReleasesViewModel? _entryReleasesViewModel;
|
||||||
|
[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);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async Task GetEntry(long entryId, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
IOperationResult<IGetEntryByIdResult> result = await _client.GetEntryById.ExecuteAsync(entryId, cancellationToken);
|
||||||
|
if (result.IsErrorResult())
|
||||||
|
return;
|
||||||
|
|
||||||
|
Entry = result.Data?.Entry;
|
||||||
|
if (Entry == null)
|
||||||
|
{
|
||||||
|
EntryInfoViewModel = null;
|
||||||
|
EntryReleasesViewModel = null;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -2,6 +2,7 @@ using System;
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using Artemis.UI.Screens.Workshop.SubmissionWizard.Steps.Layout;
|
using Artemis.UI.Screens.Workshop.SubmissionWizard.Steps.Layout;
|
||||||
|
using Artemis.UI.Screens.Workshop.SubmissionWizard.Steps.Plugin;
|
||||||
using Artemis.UI.Screens.Workshop.SubmissionWizard.Steps.Profile;
|
using Artemis.UI.Screens.Workshop.SubmissionWizard.Steps.Profile;
|
||||||
using Artemis.UI.Shared.Services;
|
using Artemis.UI.Shared.Services;
|
||||||
using Artemis.WebClient.Workshop;
|
using Artemis.WebClient.Workshop;
|
||||||
@ -56,7 +57,9 @@ public class SubmissionWizardState : IDisposable
|
|||||||
|
|
||||||
public void StartForCurrentEntry()
|
public void StartForCurrentEntry()
|
||||||
{
|
{
|
||||||
if (EntryType == EntryType.Profile)
|
if (EntryType == EntryType.Plugin)
|
||||||
|
ChangeScreen<PluginSelectionStepViewModel>();
|
||||||
|
else if (EntryType == EntryType.Profile)
|
||||||
ChangeScreen<ProfileSelectionStepViewModel>();
|
ChangeScreen<ProfileSelectionStepViewModel>();
|
||||||
else if (EntryType == EntryType.Layout)
|
else if (EntryType == EntryType.Layout)
|
||||||
ChangeScreen<LayoutSelectionStepViewModel>();
|
ChangeScreen<LayoutSelectionStepViewModel>();
|
||||||
|
|||||||
@ -44,5 +44,14 @@
|
|||||||
</StackPanel>
|
</StackPanel>
|
||||||
</RadioButton.Content>
|
</RadioButton.Content>
|
||||||
</RadioButton>
|
</RadioButton>
|
||||||
|
<RadioButton GroupName="EntryType"
|
||||||
|
IsChecked="{CompiledBinding SelectedEntryType, Converter={StaticResource EnumBoolConverter}, ConverterParameter={x:Static workshop:EntryType.Plugin}}">
|
||||||
|
<RadioButton.Content>
|
||||||
|
<StackPanel>
|
||||||
|
<TextBlock>Plugin</TextBlock>
|
||||||
|
<TextBlock Classes="subtitle" TextWrapping="Wrap">A plugin that adds new features to Artemis.</TextBlock>
|
||||||
|
</StackPanel>
|
||||||
|
</RadioButton.Content>
|
||||||
|
</RadioButton>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</UserControl>
|
</UserControl>
|
||||||
|
|||||||
@ -0,0 +1,46 @@
|
|||||||
|
<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:plugin="clr-namespace:Artemis.UI.Screens.Workshop.SubmissionWizard.Steps.Plugin"
|
||||||
|
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||||
|
x:Class="Artemis.UI.Screens.Workshop.SubmissionWizard.Steps.Plugin.PluginSelectionStepView"
|
||||||
|
x:DataType="plugin:PluginSelectionStepViewModel">
|
||||||
|
<Grid RowDefinitions="Auto,Auto">
|
||||||
|
<StackPanel>
|
||||||
|
<StackPanel.Styles>
|
||||||
|
<Styles>
|
||||||
|
<Style Selector="TextBlock">
|
||||||
|
<Setter Property="TextWrapping" Value="Wrap"></Setter>
|
||||||
|
</Style>
|
||||||
|
</Styles>
|
||||||
|
</StackPanel.Styles>
|
||||||
|
<TextBlock Theme="{StaticResource TitleTextBlockStyle}" TextWrapping="Wrap">
|
||||||
|
Plugin selection
|
||||||
|
</TextBlock>
|
||||||
|
<TextBlock TextWrapping="Wrap">
|
||||||
|
Please select the plugin you want to share, a preview will be shown below.
|
||||||
|
</TextBlock>
|
||||||
|
<Button Command="{CompiledBinding Browse}" Margin="0 20">Browse file</Button>
|
||||||
|
</StackPanel>
|
||||||
|
<Border Grid.Row="1" Classes="card" ClipToBounds="True" IsVisible="{CompiledBinding SelectedPlugin, Converter={x:Static ObjectConverters.IsNotNull}}">
|
||||||
|
<Grid RowDefinitions="30,30,30,30,Auto" ColumnDefinitions="200,Auto">
|
||||||
|
<TextBlock Grid.Row="0" Grid.Column="0" FontWeight="SemiBold">Path</TextBlock>
|
||||||
|
<TextBlock Grid.Row="0" Grid.Column="1" Text="{CompiledBinding Path}"></TextBlock>
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="1" Grid.Column="0" FontWeight="SemiBold">Name</TextBlock>
|
||||||
|
<TextBlock Grid.Row="1" Grid.Column="1" Text="{CompiledBinding SelectedPlugin.Name}"></TextBlock>
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="2" Grid.Column="0" FontWeight="SemiBold">Description</TextBlock>
|
||||||
|
<TextBlock Grid.Row="2" Grid.Column="1" Text="{CompiledBinding SelectedPlugin.Description}"></TextBlock>
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="3" Grid.Column="0" FontWeight="SemiBold">Main entry point</TextBlock>
|
||||||
|
<TextBlock Grid.Row="3" Grid.Column="1" Text="{CompiledBinding SelectedPlugin.Main}"></TextBlock>
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="4" Grid.Column="0" FontWeight="SemiBold">Version</TextBlock>
|
||||||
|
<TextBlock Grid.Row="4" Grid.Column="1" Text="{CompiledBinding SelectedPlugin.Version}"></TextBlock>
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
</UserControl>
|
||||||
@ -0,0 +1,11 @@
|
|||||||
|
using Avalonia.ReactiveUI;
|
||||||
|
|
||||||
|
namespace Artemis.UI.Screens.Workshop.SubmissionWizard.Steps.Plugin;
|
||||||
|
|
||||||
|
public partial class PluginSelectionStepView : ReactiveUserControl<PluginSelectionStepViewModel>
|
||||||
|
{
|
||||||
|
public PluginSelectionStepView()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,83 @@
|
|||||||
|
using System.IO;
|
||||||
|
using System.IO.Compression;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Reactive;
|
||||||
|
using System.Reactive.Disposables;
|
||||||
|
using System.Reactive.Linq;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Artemis.Core;
|
||||||
|
using Artemis.UI.Screens.Workshop.SubmissionWizard.Steps.Profile;
|
||||||
|
using Artemis.UI.Shared.Services;
|
||||||
|
using Artemis.WebClient.Workshop.Handlers.UploadHandlers;
|
||||||
|
using PropertyChanged.SourceGenerator;
|
||||||
|
using ReactiveUI;
|
||||||
|
|
||||||
|
namespace Artemis.UI.Screens.Workshop.SubmissionWizard.Steps.Plugin;
|
||||||
|
|
||||||
|
public partial class PluginSelectionStepViewModel : SubmissionViewModel
|
||||||
|
{
|
||||||
|
private readonly IWindowService _windowService;
|
||||||
|
[Notify] private PluginInfo? _selectedPlugin;
|
||||||
|
[Notify] private string? _path;
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public PluginSelectionStepViewModel(IWindowService windowService)
|
||||||
|
{
|
||||||
|
_windowService = windowService;
|
||||||
|
|
||||||
|
GoBack = ReactiveCommand.Create(() => State.ChangeScreen<EntryTypeStepViewModel>());
|
||||||
|
Continue = ReactiveCommand.Create(ExecuteContinue, this.WhenAnyValue(vm => vm.SelectedPlugin).Select(p => p != null));
|
||||||
|
Browse = ReactiveCommand.CreateFromTask(ExecuteBrowse);
|
||||||
|
|
||||||
|
this.WhenActivated((CompositeDisposable _) =>
|
||||||
|
{
|
||||||
|
ShowGoBack = State.EntryId == null;
|
||||||
|
if (State.EntrySource is PluginEntrySource pluginEntrySource)
|
||||||
|
{
|
||||||
|
SelectedPlugin = pluginEntrySource.PluginInfo;
|
||||||
|
Path = pluginEntrySource.Path;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public ReactiveCommand<Unit, Unit> Browse { get; }
|
||||||
|
|
||||||
|
private async Task ExecuteBrowse()
|
||||||
|
{
|
||||||
|
string[]? files = await _windowService.CreateOpenFileDialog().HavingFilter(f => f.WithExtension("zip").WithName("ZIP files")).ShowAsync();
|
||||||
|
if (files == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
// Find the metadata file in the zip
|
||||||
|
using ZipArchive archive = ZipFile.OpenRead(files[0]);
|
||||||
|
ZipArchiveEntry? metaDataFileEntry = archive.Entries.FirstOrDefault(e => e.Name == "plugin.json");
|
||||||
|
if (metaDataFileEntry == null)
|
||||||
|
throw new ArtemisPluginException("Couldn't find a plugin.json in " + files[0]);
|
||||||
|
|
||||||
|
using StreamReader reader = new(metaDataFileEntry.Open());
|
||||||
|
PluginInfo pluginInfo = CoreJson.DeserializeObject<PluginInfo>(reader.ReadToEnd())!;
|
||||||
|
if (!pluginInfo.Main.EndsWith(".dll"))
|
||||||
|
throw new ArtemisPluginException("Main entry in plugin.json must point to a .dll file");
|
||||||
|
|
||||||
|
SelectedPlugin = pluginInfo;
|
||||||
|
Path = files[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ExecuteContinue()
|
||||||
|
{
|
||||||
|
if (SelectedPlugin == null || Path == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
State.EntrySource = new PluginEntrySource(SelectedPlugin, Path);
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(State.Name))
|
||||||
|
State.Name = SelectedPlugin.Name;
|
||||||
|
if (string.IsNullOrWhiteSpace(State.Summary))
|
||||||
|
State.Summary = SelectedPlugin.Description ?? "";
|
||||||
|
|
||||||
|
if (State.EntryId == null)
|
||||||
|
State.ChangeScreen<SpecificationsStepViewModel>();
|
||||||
|
else
|
||||||
|
State.ChangeScreen<UploadStepViewModel>();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -6,6 +6,7 @@ using System.Reactive.Disposables;
|
|||||||
using Artemis.UI.Extensions;
|
using Artemis.UI.Extensions;
|
||||||
using Artemis.UI.Screens.Workshop.Entries;
|
using Artemis.UI.Screens.Workshop.Entries;
|
||||||
using Artemis.UI.Screens.Workshop.SubmissionWizard.Steps.Layout;
|
using Artemis.UI.Screens.Workshop.SubmissionWizard.Steps.Layout;
|
||||||
|
using Artemis.UI.Screens.Workshop.SubmissionWizard.Steps.Plugin;
|
||||||
using Artemis.UI.Screens.Workshop.SubmissionWizard.Steps.Profile;
|
using Artemis.UI.Screens.Workshop.SubmissionWizard.Steps.Profile;
|
||||||
using Artemis.WebClient.Workshop;
|
using Artemis.WebClient.Workshop;
|
||||||
using DynamicData;
|
using DynamicData;
|
||||||
@ -37,19 +38,14 @@ public partial class SpecificationsStepViewModel : SubmissionViewModel
|
|||||||
// Apply what's there so far
|
// Apply what's there so far
|
||||||
ApplyToState();
|
ApplyToState();
|
||||||
|
|
||||||
switch (State.EntryType)
|
if (State.EntryType == EntryType.Layout)
|
||||||
{
|
State.ChangeScreen<LayoutInfoStepViewModel>();
|
||||||
case EntryType.Layout:
|
else if (State.EntryType == EntryType.Plugin)
|
||||||
State.ChangeScreen<LayoutInfoStepViewModel>();
|
State.ChangeScreen<PluginSelectionStepViewModel>();
|
||||||
break;
|
else if (State.EntryType == EntryType.Profile)
|
||||||
case EntryType.Plugin:
|
State.ChangeScreen<ProfileAdaptionHintsStepViewModel>();
|
||||||
break;
|
else
|
||||||
case EntryType.Profile:
|
throw new ArgumentOutOfRangeException();
|
||||||
State.ChangeScreen<ProfileAdaptionHintsStepViewModel>();
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
throw new ArgumentOutOfRangeException();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ExecuteContinue()
|
private void ExecuteContinue()
|
||||||
|
|||||||
@ -15,6 +15,7 @@ public class EntryInstallationHandlerFactory
|
|||||||
{
|
{
|
||||||
return entryType switch
|
return entryType switch
|
||||||
{
|
{
|
||||||
|
EntryType.Plugin => _container.Resolve<PluginEntryInstallationHandler>(),
|
||||||
EntryType.Profile => _container.Resolve<ProfileEntryInstallationHandler>(),
|
EntryType.Profile => _container.Resolve<ProfileEntryInstallationHandler>(),
|
||||||
EntryType.Layout => _container.Resolve<LayoutEntryInstallationHandler>(),
|
EntryType.Layout => _container.Resolve<LayoutEntryInstallationHandler>(),
|
||||||
_ => throw new NotSupportedException($"EntryType '{entryType}' is not supported.")
|
_ => throw new NotSupportedException($"EntryType '{entryType}' is not supported.")
|
||||||
|
|||||||
@ -14,11 +14,12 @@ public class EntryUninstallResult
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public static EntryUninstallResult FromSuccess()
|
public static EntryUninstallResult FromSuccess(string? message = null)
|
||||||
{
|
{
|
||||||
return new EntryUninstallResult
|
return new EntryUninstallResult
|
||||||
{
|
{
|
||||||
IsSuccess = true
|
IsSuccess = true,
|
||||||
|
Message = message
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -0,0 +1,130 @@
|
|||||||
|
using System.IO.Compression;
|
||||||
|
using Artemis.Core;
|
||||||
|
using Artemis.Core.Services;
|
||||||
|
using Artemis.UI.Shared.Extensions;
|
||||||
|
using Artemis.UI.Shared.Utilities;
|
||||||
|
using Artemis.WebClient.Workshop.Exceptions;
|
||||||
|
using Artemis.WebClient.Workshop.Models;
|
||||||
|
using Artemis.WebClient.Workshop.Services;
|
||||||
|
|
||||||
|
namespace Artemis.WebClient.Workshop.Handlers.InstallationHandlers;
|
||||||
|
|
||||||
|
public class PluginEntryInstallationHandler : IEntryInstallationHandler
|
||||||
|
{
|
||||||
|
private readonly IHttpClientFactory _httpClientFactory;
|
||||||
|
private readonly IWorkshopService _workshopService;
|
||||||
|
private readonly IPluginManagementService _pluginManagementService;
|
||||||
|
|
||||||
|
public PluginEntryInstallationHandler(IHttpClientFactory httpClientFactory, IWorkshopService workshopService, IPluginManagementService pluginManagementService)
|
||||||
|
{
|
||||||
|
_httpClientFactory = httpClientFactory;
|
||||||
|
_workshopService = workshopService;
|
||||||
|
_pluginManagementService = pluginManagementService;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<EntryInstallResult> InstallAsync(IEntryDetails entry, IRelease release, Progress<StreamProgress> progress, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
// Ensure there is an installed entry
|
||||||
|
InstalledEntry? installedEntry = _workshopService.GetInstalledEntry(entry);
|
||||||
|
if (installedEntry != null)
|
||||||
|
{
|
||||||
|
// If the folder already exists, we're not going to reinstall the plugin since files may be in use, consider our job done
|
||||||
|
if (installedEntry.GetReleaseDirectory(release).Exists)
|
||||||
|
return EntryInstallResult.FromSuccess(installedEntry);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// If none exists yet create one
|
||||||
|
installedEntry = new InstalledEntry(entry, release);
|
||||||
|
// Don't try to install a new plugin into an existing directory since files may be in use, consider our job screwed
|
||||||
|
if (installedEntry.GetReleaseDirectory(release).Exists)
|
||||||
|
return EntryInstallResult.FromFailure("Plugin is new but installation directory is not empty, try restarting Artemis");
|
||||||
|
}
|
||||||
|
|
||||||
|
using MemoryStream stream = new();
|
||||||
|
|
||||||
|
// Download the provided release
|
||||||
|
try
|
||||||
|
{
|
||||||
|
HttpClient client = _httpClientFactory.CreateClient(WorkshopConstants.WORKSHOP_CLIENT_NAME);
|
||||||
|
await client.DownloadDataAsync($"releases/download/{release.Id}", stream, progress, cancellationToken);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
return EntryInstallResult.FromFailure(e.Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create the release directory
|
||||||
|
DirectoryInfo releaseDirectory = installedEntry.GetReleaseDirectory(release);
|
||||||
|
releaseDirectory.Create();
|
||||||
|
|
||||||
|
// Extract the archive
|
||||||
|
stream.Seek(0, SeekOrigin.Begin);
|
||||||
|
using ZipArchive archive = new(stream);
|
||||||
|
archive.ExtractToDirectory(releaseDirectory.FullName);
|
||||||
|
|
||||||
|
// If there is already a version of the plugin installed, disable it
|
||||||
|
if (installedEntry.TryGetMetadata("PluginId", out Guid pluginId))
|
||||||
|
{
|
||||||
|
Plugin? currentVersion = _pluginManagementService.GetAllPlugins().FirstOrDefault(p => p.Guid == pluginId);
|
||||||
|
if (currentVersion != null)
|
||||||
|
_pluginManagementService.UnloadPlugin(currentVersion);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Load the plugin, next time during startup this will happen automatically
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Plugin? plugin = _pluginManagementService.LoadPlugin(releaseDirectory);
|
||||||
|
if (plugin == null)
|
||||||
|
throw new ArtemisWorkshopException("Failed to load plugin, it may be incompatible");
|
||||||
|
|
||||||
|
installedEntry.SetMetadata("PluginId", plugin.Guid);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
// If the plugin ended up being invalid yoink it out again, shoooo
|
||||||
|
try
|
||||||
|
{
|
||||||
|
releaseDirectory.Delete(true);
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
// ignored, will get cleaned up as an orphaned file
|
||||||
|
}
|
||||||
|
|
||||||
|
_workshopService.RemoveInstalledEntry(installedEntry);
|
||||||
|
return EntryInstallResult.FromFailure(e.Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
_workshopService.SaveInstalledEntry(installedEntry);
|
||||||
|
return EntryInstallResult.FromSuccess(installedEntry);
|
||||||
|
}
|
||||||
|
|
||||||
|
public Task<EntryUninstallResult> UninstallAsync(InstalledEntry installedEntry, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
// Disable the plugin
|
||||||
|
if (installedEntry.TryGetMetadata("PluginId", out Guid pluginId))
|
||||||
|
{
|
||||||
|
Plugin? plugin = _pluginManagementService.GetAllPlugins().FirstOrDefault(p => p.Guid == pluginId);
|
||||||
|
if (plugin != null)
|
||||||
|
_pluginManagementService.UnloadPlugin(plugin);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attempt to remove from filesystem
|
||||||
|
DirectoryInfo directory = installedEntry.GetDirectory();
|
||||||
|
string? message = null;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (directory.Exists)
|
||||||
|
directory.Delete(true);
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
message = "Failed to clean up files, you may need to restart Artemis";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove entry
|
||||||
|
_workshopService.RemoveInstalledEntry(installedEntry);
|
||||||
|
return Task.FromResult(EntryUninstallResult.FromSuccess(message));
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -15,6 +15,7 @@ public class EntryUploadHandlerFactory
|
|||||||
{
|
{
|
||||||
return entryType switch
|
return entryType switch
|
||||||
{
|
{
|
||||||
|
EntryType.Plugin => _container.Resolve<PluginEntryUploadHandler>(),
|
||||||
EntryType.Profile => _container.Resolve<ProfileEntryUploadHandler>(),
|
EntryType.Profile => _container.Resolve<ProfileEntryUploadHandler>(),
|
||||||
EntryType.Layout => _container.Resolve<LayoutEntryUploadHandler>(),
|
EntryType.Layout => _container.Resolve<LayoutEntryUploadHandler>(),
|
||||||
_ => throw new NotSupportedException($"EntryType '{entryType}' is not supported.")
|
_ => throw new NotSupportedException($"EntryType '{entryType}' is not supported.")
|
||||||
|
|||||||
@ -0,0 +1,15 @@
|
|||||||
|
using Artemis.Core;
|
||||||
|
|
||||||
|
namespace Artemis.WebClient.Workshop.Handlers.UploadHandlers;
|
||||||
|
|
||||||
|
public class PluginEntrySource : IEntrySource
|
||||||
|
{
|
||||||
|
public PluginEntrySource(PluginInfo pluginInfo, string path)
|
||||||
|
{
|
||||||
|
PluginInfo = pluginInfo;
|
||||||
|
Path = path;
|
||||||
|
}
|
||||||
|
|
||||||
|
public PluginInfo PluginInfo { get; set; }
|
||||||
|
public string Path { get; set; }
|
||||||
|
}
|
||||||
@ -0,0 +1,40 @@
|
|||||||
|
using System.Net.Http.Headers;
|
||||||
|
using Artemis.WebClient.Workshop.Entities;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
|
||||||
|
namespace Artemis.WebClient.Workshop.Handlers.UploadHandlers;
|
||||||
|
|
||||||
|
public class PluginEntryUploadHandler : IEntryUploadHandler
|
||||||
|
{
|
||||||
|
private readonly IHttpClientFactory _httpClientFactory;
|
||||||
|
|
||||||
|
public PluginEntryUploadHandler(IHttpClientFactory httpClientFactory)
|
||||||
|
{
|
||||||
|
_httpClientFactory = httpClientFactory;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public async Task<EntryUploadResult> CreateReleaseAsync(long entryId, IEntrySource entrySource, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
if (entrySource is not PluginEntrySource source)
|
||||||
|
throw new InvalidOperationException("Can only create releases for plugins");
|
||||||
|
|
||||||
|
// Submit the archive
|
||||||
|
HttpClient client = _httpClientFactory.CreateClient(WorkshopConstants.WORKSHOP_CLIENT_NAME);
|
||||||
|
|
||||||
|
// Construct the request
|
||||||
|
await using FileStream fileStream = File.Open(source.Path, FileMode.Open);
|
||||||
|
MultipartFormDataContent content = new();
|
||||||
|
StreamContent streamContent = new(fileStream);
|
||||||
|
streamContent.Headers.ContentType = new MediaTypeHeaderValue("application/zip");
|
||||||
|
content.Add(streamContent, "file", "file.zip");
|
||||||
|
|
||||||
|
// Submit
|
||||||
|
HttpResponseMessage response = await client.PostAsync("releases/upload/" + entryId, content, cancellationToken);
|
||||||
|
if (!response.IsSuccessStatusCode)
|
||||||
|
return EntryUploadResult.FromFailure($"{response.StatusCode} - {await response.Content.ReadAsStringAsync(cancellationToken)}");
|
||||||
|
|
||||||
|
Release? release = JsonConvert.DeserializeObject<Release>(await response.Content.ReadAsStringAsync(cancellationToken));
|
||||||
|
return release != null ? EntryUploadResult.FromSuccess(release) : EntryUploadResult.FromFailure("Failed to deserialize response");
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -97,7 +97,7 @@ public class InstalledEntry
|
|||||||
/// <param name="value">The value to set.</param>
|
/// <param name="value">The value to set.</param>
|
||||||
public void SetMetadata(string key, object value)
|
public void SetMetadata(string key, object value)
|
||||||
{
|
{
|
||||||
_metadata.Add(key, value);
|
_metadata[key] = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -118,7 +118,7 @@ public class InstalledEntry
|
|||||||
{
|
{
|
||||||
return new DirectoryInfo(Path.Combine(Constants.WorkshopFolder, $"{EntryId}-{StringUtilities.UrlFriendly(Name)}"));
|
return new DirectoryInfo(Path.Combine(Constants.WorkshopFolder, $"{EntryId}-{StringUtilities.UrlFriendly(Name)}"));
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Returns the directory info of a release of this entry, where any files would be stored if applicable.
|
/// Returns the directory info of a release of this entry, where any files would be stored if applicable.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@ -1,3 +1,9 @@
|
|||||||
namespace Artemis.WebClient.Workshop.Models;
|
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 DateTime CreationTime { get; init; }
|
||||||
|
public DateTime? Expiration { get; init; }
|
||||||
|
public string? Description { get; init; }
|
||||||
|
}
|
||||||
@ -10,7 +10,7 @@ public interface IUserManagementService : IProtectedArtemisService
|
|||||||
Task<ApiResult> ChangeEmailAddress(string emailAddress, CancellationToken cancellationToken);
|
Task<ApiResult> ChangeEmailAddress(string emailAddress, CancellationToken cancellationToken);
|
||||||
Task<ApiResult> ChangeAvatar(Stream avatar, CancellationToken cancellationToken);
|
Task<ApiResult> ChangeAvatar(Stream avatar, CancellationToken cancellationToken);
|
||||||
Task<ApiResult> RemoveAccount(CancellationToken cancellationToken);
|
Task<ApiResult> RemoveAccount(CancellationToken cancellationToken);
|
||||||
Task<string> CreatePersonAccessToken(string description, DateTimeOffset expirationDate, CancellationToken cancellationToken);
|
Task<string> CreatePersonAccessToken(string description, DateTime expirationDate, CancellationToken cancellationToken);
|
||||||
Task<ApiResult> DeletePersonAccessToken(PersonalAccessToken personalAccessToken, CancellationToken cancellationToken);
|
Task<ApiResult> DeletePersonAccessToken(PersonalAccessToken personalAccessToken, CancellationToken cancellationToken);
|
||||||
Task<List<PersonalAccessToken>> GetPersonAccessTokens(CancellationToken cancellationToken);
|
Task<List<PersonalAccessToken>> GetPersonAccessTokens(CancellationToken cancellationToken);
|
||||||
}
|
}
|
||||||
@ -17,6 +17,7 @@ public interface IWorkshopService
|
|||||||
InstalledEntry? GetInstalledEntry(IEntryDetails entry);
|
InstalledEntry? GetInstalledEntry(IEntryDetails entry);
|
||||||
void RemoveInstalledEntry(InstalledEntry installedEntry);
|
void RemoveInstalledEntry(InstalledEntry installedEntry);
|
||||||
void SaveInstalledEntry(InstalledEntry entry);
|
void SaveInstalledEntry(InstalledEntry entry);
|
||||||
|
void Initialize();
|
||||||
|
|
||||||
public record WorkshopStatus(bool IsReachable, string Message);
|
public record WorkshopStatus(bool IsReachable, string Message);
|
||||||
}
|
}
|
||||||
@ -66,13 +66,13 @@ internal class UserManagementService : IUserManagementService
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public async Task<string> CreatePersonAccessToken(string description, DateTimeOffset expirationDate, CancellationToken cancellationToken)
|
public async Task<string> CreatePersonAccessToken(string description, DateTime expirationDate, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
HttpClient client = _httpClientFactory.CreateClient(WorkshopConstants.IDENTITY_CLIENT_NAME);
|
HttpClient client = _httpClientFactory.CreateClient(WorkshopConstants.IDENTITY_CLIENT_NAME);
|
||||||
HttpResponseMessage response = await client.PostAsync("user/access-token", JsonContent.Create(new {Description = description, ExpirationDate = expirationDate}), cancellationToken);
|
HttpResponseMessage response = await client.PostAsync("user/access-token", JsonContent.Create(new {Description = description, ExpirationDate = expirationDate}), cancellationToken);
|
||||||
response.EnsureSuccessStatusCode();
|
response.EnsureSuccessStatusCode();
|
||||||
|
|
||||||
string? result = await response.Content.ReadFromJsonAsync<string>(cancellationToken: cancellationToken);
|
string? result = await response.Content.ReadAsStringAsync(cancellationToken);
|
||||||
if (result == null)
|
if (result == null)
|
||||||
throw new ArtemisWebClientException("Failed to deserialize access token");
|
throw new ArtemisWebClientException("Failed to deserialize access token");
|
||||||
return result;
|
return result;
|
||||||
|
|||||||
@ -1,23 +1,32 @@
|
|||||||
using System.Net.Http.Headers;
|
using System.Net.Http.Headers;
|
||||||
|
using Artemis.Core;
|
||||||
|
using Artemis.Core.Services;
|
||||||
using Artemis.Storage.Entities.Workshop;
|
using Artemis.Storage.Entities.Workshop;
|
||||||
using Artemis.Storage.Repositories.Interfaces;
|
using Artemis.Storage.Repositories.Interfaces;
|
||||||
using Artemis.UI.Shared.Routing;
|
using Artemis.UI.Shared.Routing;
|
||||||
|
using Artemis.WebClient.Workshop.Exceptions;
|
||||||
using Artemis.WebClient.Workshop.Handlers.UploadHandlers;
|
using Artemis.WebClient.Workshop.Handlers.UploadHandlers;
|
||||||
using Artemis.WebClient.Workshop.Models;
|
using Artemis.WebClient.Workshop.Models;
|
||||||
|
using Serilog;
|
||||||
|
|
||||||
namespace Artemis.WebClient.Workshop.Services;
|
namespace Artemis.WebClient.Workshop.Services;
|
||||||
|
|
||||||
public class WorkshopService : IWorkshopService
|
public class WorkshopService : IWorkshopService
|
||||||
{
|
{
|
||||||
|
private readonly ILogger _logger;
|
||||||
private readonly IHttpClientFactory _httpClientFactory;
|
private readonly IHttpClientFactory _httpClientFactory;
|
||||||
private readonly IRouter _router;
|
private readonly IRouter _router;
|
||||||
private readonly IEntryRepository _entryRepository;
|
private readonly IEntryRepository _entryRepository;
|
||||||
|
private readonly IPluginManagementService _pluginManagementService;
|
||||||
|
private bool _initialized;
|
||||||
|
|
||||||
public WorkshopService(IHttpClientFactory httpClientFactory, IRouter router, IEntryRepository entryRepository)
|
public WorkshopService(ILogger logger, IHttpClientFactory httpClientFactory, IRouter router, IEntryRepository entryRepository, IPluginManagementService pluginManagementService)
|
||||||
{
|
{
|
||||||
|
_logger = logger;
|
||||||
_httpClientFactory = httpClientFactory;
|
_httpClientFactory = httpClientFactory;
|
||||||
_router = router;
|
_router = router;
|
||||||
_entryRepository = entryRepository;
|
_entryRepository = entryRepository;
|
||||||
|
_pluginManagementService = pluginManagementService;
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<Stream?> GetEntryIcon(long entryId, CancellationToken cancellationToken)
|
public async Task<Stream?> GetEntryIcon(long entryId, CancellationToken cancellationToken)
|
||||||
@ -117,6 +126,7 @@ public class WorkshopService : IWorkshopService
|
|||||||
return status.IsReachable;
|
return status.IsReachable;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public async Task NavigateToEntry(long entryId, EntryType entryType)
|
public async Task NavigateToEntry(long entryId, EntryType entryType)
|
||||||
{
|
{
|
||||||
switch (entryType)
|
switch (entryType)
|
||||||
@ -135,6 +145,7 @@ public class WorkshopService : IWorkshopService
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
public List<InstalledEntry> GetInstalledEntries()
|
public List<InstalledEntry> GetInstalledEntries()
|
||||||
{
|
{
|
||||||
return _entryRepository.GetAll().Select(e => new InstalledEntry(e)).ToList();
|
return _entryRepository.GetAll().Select(e => new InstalledEntry(e)).ToList();
|
||||||
@ -150,12 +161,6 @@ public class WorkshopService : IWorkshopService
|
|||||||
return new InstalledEntry(entity);
|
return new InstalledEntry(entity);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public void AddOrUpdateInstalledEntry(InstalledEntry entry, IRelease release)
|
|
||||||
{
|
|
||||||
throw new NotImplementedException();
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public void RemoveInstalledEntry(InstalledEntry installedEntry)
|
public void RemoveInstalledEntry(InstalledEntry installedEntry)
|
||||||
{
|
{
|
||||||
@ -168,4 +173,48 @@ public class WorkshopService : IWorkshopService
|
|||||||
entry.Save();
|
entry.Save();
|
||||||
_entryRepository.Save(entry.Entity);
|
_entryRepository.Save(entry.Entity);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
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))
|
||||||
|
{
|
||||||
|
InstalledEntry? installedEntry = entries.FirstOrDefault(e => e.GetDirectory().FullName == directory);
|
||||||
|
if (installedEntry == null)
|
||||||
|
RemoveOrphanedDirectory(directory);
|
||||||
|
else
|
||||||
|
{
|
||||||
|
DirectoryInfo currentReleaseDirectory = installedEntry.GetReleaseDirectory();
|
||||||
|
foreach (string releaseDirectory in Directory.GetDirectories(directory))
|
||||||
|
{
|
||||||
|
if (releaseDirectory != currentReleaseDirectory.FullName)
|
||||||
|
RemoveOrphanedDirectory(releaseDirectory);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RemoveOrphanedDirectory(string directory)
|
||||||
|
{
|
||||||
|
_logger.Information("Removing orphaned workshop entry at {Directory}", directory);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Directory.Delete(directory, true);
|
||||||
|
}
|
||||||
|
catch (Exception e)
|
||||||
|
{
|
||||||
|
_logger.Warning(e, "Failed to remove orphaned workshop entry at {Directory}", directory);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user