Core - Added API for retrieving current suspended device providers

Core - Added events for plugin removal, entry installlation/uninstallation
Workshop - Remove the related workshop entry when manually removing a plugin or profile
Workshop - Prevent installing profiles with missing plugins and show a dialog with which plugins are missing
This commit is contained in:
Robert committed 2024-06-30 09:42:41 +02:00
1 parent 86f78940b1
commit 648b7765ef
20 files changed
+440 -106

No files matched your search

@@ -11,8 +11,6 @@ using Artemis.UI.Exceptions;
using Artemis.UI.Shared;
using Artemis.UI.Shared.Services;
using Artemis.UI.Shared.Services.Builders;
using Artemis.WebClient.Workshop.Models;
using Artemis.WebClient.Workshop.Services;
using Avalonia.Controls;
using Avalonia.Threading;
using Material.Icons;
@@ -26,7 +24,6 @@ public partial class PluginViewModel : ActivatableViewModelBase
private readonly ICoreService _coreService;
private readonly INotificationService _notificationService;
private readonly IPluginManagementService _pluginManagementService;
private readonly IWorkshopService _workshopService;
private readonly IWindowService _windowService;
private Window? _settingsWindow;
[Notify] private bool _canInstallPrerequisites;
@@ -39,15 +36,13 @@ public partial class PluginViewModel : ActivatableViewModelBase
ICoreService coreService,
IWindowService windowService,
INotificationService notificationService,
IPluginManagementService pluginManagementService,
IWorkshopService workshopService)
IPluginManagementService pluginManagementService)
{
_plugin = plugin;
_coreService = coreService;
_windowService = windowService;
_notificationService = notificationService;
_pluginManagementService = pluginManagementService;
_workshopService = workshopService;
Platforms = new ObservableCollection<PluginPlatformViewModel>();
if (Plugin.Info.Platforms != null)
@@ -260,11 +255,7 @@ public partial class PluginViewModel : ActivatableViewModelBase
_windowService.ShowExceptionDialog("Failed to remove plugin", e);
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();
}
@@ -15,24 +15,29 @@
</UserControl.Resources>
<Panel>
<StackPanel IsVisible="{CompiledBinding Entry, Converter={x:Static ObjectConverters.IsNull}}">
<Border Classes="skeleton-text" Margin="0 0 10 0" Width="80" Height="80"></Border>
<Border Classes="skeleton-text title" HorizontalAlignment="Stretch"/>
<Border Classes="skeleton-text" Width="120"/>
<Border Classes="skeleton-text" Width="140" Margin="0 8"/>
<Border Classes="skeleton-text" Width="80"/>
<Border Classes="skeleton-text"
HorizontalAlignment="Center"
Margin="30 30 30 0"
Width="80"
Height="80">
</Border>
<Border Classes="skeleton-text title" HorizontalAlignment="Stretch" Margin="0 20" />
<Border Classes="skeleton-text" Width="120" />
<Border Classes="skeleton-text" Width="140" Margin="0 8" />
<Border Classes="skeleton-text" Width="80" />
<Border Classes="card-separator" Margin="0 15 0 17"></Border>
<Border Classes="skeleton-text" Width="120"/>
<Border Classes="skeleton-text" Width="120" />
<StackPanel Margin="0 10 0 0">
<Border Classes="skeleton-text" Width="160"/>
<Border Classes="skeleton-text" Width="160"/>
<Border Classes="skeleton-text" Width="160" />
<Border Classes="skeleton-text" Width="160" />
</StackPanel>
<Border Classes="skeleton-button"></Border>
</StackPanel>
<StackPanel IsVisible="{CompiledBinding Entry, Converter={x:Static ObjectConverters.IsNotNull}}">
<Panel>
<Border CornerRadius="6"
HorizontalAlignment="Left"
Margin="0 0 10 0"
HorizontalAlignment="Center"
Margin="30 30 30 0"
Width="80"
Height="80"
ClipToBounds="True">
@@ -46,11 +51,13 @@
<avalonia:MaterialIcon Kind="ShareVariant" />
</Button>
</Panel>
<TextBlock Theme="{StaticResource TitleTextBlockStyle}"
MaxLines="3"
TextTrimming="CharacterEllipsis"
Text="{CompiledBinding Entry.Name, FallbackValue=Title}"/>
TextAlignment="Center"
Text="{CompiledBinding Entry.Name, FallbackValue=Title}"
Margin="0 15" />
<TextBlock Classes="subtitle" TextTrimming="CharacterEllipsis" Text="{CompiledBinding Entry.Author, FallbackValue=Author}" />
@@ -0,0 +1,44 @@
<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.EntryReleases.Dialogs"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Artemis.UI.Screens.Workshop.EntryReleases.Dialogs.DependenciesDialogView"
x:DataType="dialogs:DependenciesDialogViewModel">
<StackPanel>
<TextBlock IsVisible="{CompiledBinding Multiple}">
<Run Text="Some or all of the required" />
<Run Text="{CompiledBinding EntryTypePlural}" />
<Run Text="are not installed. This" />
<Run Text="{CompiledBinding DependantType}" />
<Run Text="will not work properly without them. The missing" />
<Run Text="{CompiledBinding EntryTypePlural}" />
<Run Text="are listed below and you can click on them to view them" />
</TextBlock>
<TextBlock IsVisible="{CompiledBinding !Multiple}">
<Run Text="A required" />
<Run Text="{CompiledBinding EntryType}" />
<Run Text="is not installed. This" />
<Run Text="{CompiledBinding DependantType}" />
<Run Text="will not work properly without it. The missing" />
<Run Text="{CompiledBinding EntryType}" />
<Run Text="is listed below and you can click on it to view it" />
</TextBlock>
<ScrollViewer MaxHeight="500" Margin="0 30 0 0">
<ItemsControl ItemsSource="{CompiledBinding Dependencies}" >
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<VirtualizingStackPanel />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<ContentControl Content="{CompiledBinding}" Margin="0 0 0 5"></ContentControl>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
</StackPanel>
</UserControl>
@@ -0,0 +1,14 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using Avalonia.ReactiveUI;
namespace Artemis.UI.Screens.Workshop.EntryReleases.Dialogs;
public partial class DependenciesDialogView : ReactiveUserControl<DependenciesDialogViewModel>
{
public DependenciesDialogView()
{
InitializeComponent();
}
}
@@ -0,0 +1,36 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using Artemis.UI.Screens.Workshop.Entries.List;
using Artemis.UI.Shared;
using Artemis.UI.Shared.Routing;
using Artemis.WebClient.Workshop;
using Humanizer;
using ReactiveUI;
namespace Artemis.UI.Screens.Workshop.EntryReleases.Dialogs;
public class DependenciesDialogViewModel : ContentDialogViewModelBase
{
public DependenciesDialogViewModel(IEntrySummary dependant, List<IEntrySummary> dependencies, Func<IEntrySummary, EntryListItemViewModel> getEntryListItemViewModel, IRouter router)
{
Dependant = dependant;
DependantType = dependant.EntryType.Humanize(LetterCasing.LowerCase);
EntryType = dependencies.First().EntryType.Humanize(LetterCasing.LowerCase);
EntryTypePlural = dependencies.First().EntryType.Humanize(LetterCasing.LowerCase).Pluralize();
Dependencies = new ObservableCollection<EntryListItemViewModel>(dependencies.Select(getEntryListItemViewModel));
this.WhenActivated(d => router.CurrentPath.Skip(1).Subscribe(s => ContentDialog?.Hide()).DisposeWith(d));
}
public string DependantType { get; }
public string EntryType { get; }
public string EntryTypePlural { get; }
public bool Multiple => Dependencies.Count > 1;
public IEntrySummary Dependant { get; }
public ObservableCollection<EntryListItemViewModel> Dependencies { get; }
}
@@ -1,13 +1,14 @@
using System;
using System.ComponentModel;
using System.Collections.Generic;
using System.Linq;
using System.Reactive.Linq;
using System.Reactive.Disposables;
using System.Threading;
using System.Threading.Tasks;
using Artemis.Core;
using Artemis.Core.Services;
using Artemis.UI.DryIoc.Factories;
using Artemis.UI.Screens.Plugins;
using Artemis.UI.Screens.Workshop.EntryReleases.Dialogs;
using Artemis.UI.Shared;
using Artemis.UI.Shared.Routing;
using Artemis.UI.Shared.Services;
@@ -17,7 +18,6 @@ using Artemis.WebClient.Workshop;
using Artemis.WebClient.Workshop.Handlers.InstallationHandlers;
using Artemis.WebClient.Workshop.Models;
using Artemis.WebClient.Workshop.Services;
using Humanizer;
using PropertyChanged.SourceGenerator;
using ReactiveUI;
@@ -30,13 +30,12 @@ public partial class EntryReleaseInfoViewModel : ActivatableViewModelBase
private readonly IWindowService _windowService;
private readonly IWorkshopService _workshopService;
private readonly IPluginManagementService _pluginManagementService;
private readonly EntryInstallationHandlerFactory _factory;
private readonly ISettingsVmFactory _settingsVmFactory;
private readonly Progress<StreamProgress> _progress = new();
private readonly ObservableAsPropertyHelper<bool> _isCurrentVersion;
[Notify] private IReleaseDetails? _release;
[Notify] private float _installProgress;
[Notify] private bool _isCurrentVersion;
[Notify] private bool _installationInProgress;
[Notify] private bool _inDetailsScreen;
@@ -47,7 +46,6 @@ public partial class EntryReleaseInfoViewModel : ActivatableViewModelBase
IWindowService windowService,
IWorkshopService workshopService,
IPluginManagementService pluginManagementService,
EntryInstallationHandlerFactory factory,
ISettingsVmFactory settingsVmFactory)
{
_router = router;
@@ -55,18 +53,31 @@ public partial class EntryReleaseInfoViewModel : ActivatableViewModelBase
_windowService = windowService;
_workshopService = workshopService;
_pluginManagementService = pluginManagementService;
_factory = factory;
_settingsVmFactory = settingsVmFactory;
_progress.ProgressChanged += (_, f) => InstallProgress = f.ProgressPercentage;
_isCurrentVersion = this.WhenAnyValue(vm => vm.Release, vm => vm.InstallationInProgress, (release, _) => release)
.Select(r => r != null && _workshopService.GetInstalledEntry(r.Entry.Id)?.ReleaseId == r.Id)
.ToProperty(this, vm => vm.IsCurrentVersion);
this.WhenActivated(d =>
{
_workshopService.OnEntryInstalled += WorkshopServiceOnOnEntryInstalled;
_workshopService.OnEntryUninstalled += WorkshopServiceOnOnEntryInstalled;
Disposable.Create(() =>
{
_workshopService.OnEntryInstalled -= WorkshopServiceOnOnEntryInstalled;
_workshopService.OnEntryUninstalled -= WorkshopServiceOnOnEntryInstalled;
}).DisposeWith(d);
IsCurrentVersion = Release != null && _workshopService.GetInstalledEntry(Release.Entry.Id)?.ReleaseId == Release.Id;
});
this.WhenAnyValue(vm => vm.Release).Subscribe(r => IsCurrentVersion = r != null && _workshopService.GetInstalledEntry(r.Entry.Id)?.ReleaseId == r.Id);
InDetailsScreen = true;
}
public bool IsCurrentVersion => _isCurrentVersion.Value;
private void WorkshopServiceOnOnEntryInstalled(object? sender, InstalledEntry e)
{
IsCurrentVersion = Release != null && _workshopService.GetInstalledEntry(Release.Entry.Id)?.ReleaseId == Release.Id;
}
public async Task Close()
{
@@ -79,15 +90,15 @@ public partial class EntryReleaseInfoViewModel : ActivatableViewModelBase
return;
// If the entry has missing dependencies, show a dialog
foreach (IGetEntryById_Entry_LatestRelease_Dependencies dependency in Release.Dependencies)
List<IEntrySummary> missing = Release.Dependencies.Where(d => _workshopService.GetInstalledEntry(d.Id) == null).Cast<IEntrySummary>().ToList();
if (missing.Count > 0)
{
if (_workshopService.GetInstalledEntry(dependency.Id) == null)
{
if (await _windowService.ShowConfirmContentDialog("Missing dependencies",
$"One or more dependencies are missing, this {Release.Entry.EntryType.Humanize(LetterCasing.LowerCase)} won't work without them", "View dependencies"))
await _router.GoUp();
return;
}
await _windowService.CreateContentDialog()
.WithTitle("Requirements missing")
.WithViewModel(out DependenciesDialogViewModel _, Release.Entry, missing)
.WithCloseButtonText("Cancel installation")
.ShowAsync();
return;
}
_cts = new CancellationTokenSource();
@@ -95,8 +106,7 @@ public partial class EntryReleaseInfoViewModel : ActivatableViewModelBase
InstallationInProgress = true;
try
{
IEntryInstallationHandler handler = _factory.CreateHandler(Release.Entry.EntryType);
EntryInstallResult result = await handler.InstallAsync(Release.Entry, Release, _progress, _cts.Token);
EntryInstallResult result = await _workshopService.InstallEntry(Release.Entry, Release, _progress, _cts.Token);
if (result.IsSuccess)
{
_notificationService.CreateNotification().WithTitle("Installation succeeded").WithSeverity(NotificationSeverity.Success).Show();
@@ -145,8 +155,9 @@ public partial class EntryReleaseInfoViewModel : ActivatableViewModelBase
if (installedEntry.EntryType == EntryType.Plugin)
await UninstallPluginPrerequisites(installedEntry);
IEntryInstallationHandler handler = _factory.CreateHandler(installedEntry.EntryType);
await handler.UninstallAsync(installedEntry, CancellationToken.None);
await _workshopService.UninstallEntry(installedEntry, CancellationToken.None);
_notificationService.CreateNotification().WithTitle("Entry uninstalled").WithSeverity(NotificationSeverity.Success).Show();
}
finally
{
@@ -1,6 +1,4 @@
using System;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using System.Reactive.Disposables;
using Artemis.UI.Shared;
using Artemis.WebClient.Workshop;
using Artemis.WebClient.Workshop.Models;
@@ -22,19 +20,24 @@ public partial class EntryReleaseItemViewModel : ActivatableViewModelBase
_entry = entry;
Release = release;
UpdateIsCurrentVersion();
this.WhenActivated(d =>
{
Observable.FromEventPattern<InstalledEntry>(x => _workshopService.OnInstalledEntrySaved += x, x => _workshopService.OnInstalledEntrySaved -= x)
.Subscribe(_ => UpdateIsCurrentVersion())
.DisposeWith(d);
_workshopService.OnEntryInstalled += WorkshopServiceOnOnEntryInstalled;
_workshopService.OnEntryUninstalled += WorkshopServiceOnOnEntryInstalled;
Disposable.Create(() =>
{
_workshopService.OnEntryInstalled -= WorkshopServiceOnOnEntryInstalled;
_workshopService.OnEntryUninstalled -= WorkshopServiceOnOnEntryInstalled;
}).DisposeWith(d);
IsCurrentVersion = _workshopService.GetInstalledEntry(_entry.Id)?.ReleaseId == Release.Id;
});
}
public IRelease Release { get; }
private void UpdateIsCurrentVersion()
private void WorkshopServiceOnOnEntryInstalled(object? sender, InstalledEntry e)
{
IsCurrentVersion = _workshopService.GetInstalledEntry(_entry.Id)?.ReleaseId == Release.Id;
}
@@ -11,7 +11,6 @@ using Artemis.WebClient.Workshop.Models;
using Artemis.WebClient.Workshop.Providers;
using Artemis.WebClient.Workshop.Services;
using Material.Icons;
using Material.Icons.Avalonia;
using PropertyChanged.SourceGenerator;
using StrawberryShake;
@@ -23,7 +22,6 @@ public partial class LayoutFinderDeviceViewModel : ViewModelBase
private readonly IDeviceService _deviceService;
private readonly IWorkshopService _workshopService;
private readonly WorkshopLayoutProvider _layoutProvider;
private readonly EntryInstallationHandlerFactory _factory;
[Notify] private bool _searching;
[Notify] private bool _hasLayout;
@@ -33,18 +31,12 @@ public partial class LayoutFinderDeviceViewModel : ViewModelBase
[Notify] private string? _logicalLayout;
[Notify] private string? _physicalLayout;
public LayoutFinderDeviceViewModel(ArtemisDevice device,
IWorkshopClient client,
IDeviceService deviceService,
IWorkshopService workshopService,
WorkshopLayoutProvider layoutProvider,
EntryInstallationHandlerFactory factory)
public LayoutFinderDeviceViewModel(ArtemisDevice device, IWorkshopClient client, IDeviceService deviceService, IWorkshopService workshopService, WorkshopLayoutProvider layoutProvider)
{
_client = client;
_deviceService = deviceService;
_workshopService = workshopService;
_layoutProvider = layoutProvider;
_factory = factory;
Device = device;
DeviceIcon = DetermineDeviceIcon();
@@ -116,8 +108,7 @@ public partial class LayoutFinderDeviceViewModel : ViewModelBase
InstalledEntry? installedEntry = _workshopService.GetInstalledEntry(entry.Id);
if (installedEntry == null)
{
IEntryInstallationHandler installationHandler = _factory.CreateHandler(EntryType.Layout);
EntryInstallResult result = await installationHandler.InstallAsync(entry, release, new Progress<StreamProgress>(), CancellationToken.None);
EntryInstallResult result = await _workshopService.InstallEntry(entry, release, new Progress<StreamProgress>(), CancellationToken.None);
installedEntry = result.Entry;
}
@@ -11,7 +11,6 @@ using Artemis.UI.Shared;
using Artemis.UI.Shared.Routing;
using Artemis.UI.Shared.Services;
using Artemis.WebClient.Workshop;
using Artemis.WebClient.Workshop.Handlers.InstallationHandlers;
using Artemis.WebClient.Workshop.Models;
using Artemis.WebClient.Workshop.Services;
using PropertyChanged.SourceGenerator;
@@ -23,23 +22,19 @@ public partial class InstalledTabItemViewModel : ViewModelBase
{
private readonly IWorkshopService _workshopService;
private readonly IRouter _router;
private readonly EntryInstallationHandlerFactory _factory;
private readonly IWindowService _windowService;
private readonly IPluginManagementService _pluginManagementService;
private readonly ISettingsVmFactory _settingsVmFactory;
[Notify(Setter.Private)] private bool _isRemoved;
public InstalledTabItemViewModel(InstalledEntry installedEntry,
IWorkshopService workshopService,
IRouter router,
EntryInstallationHandlerFactory factory,
IWindowService windowService,
IPluginManagementService pluginManagementService,
ISettingsVmFactory settingsVmFactory)
{
_workshopService = workshopService;
_router = router;
_factory = factory;
_windowService = windowService;
_pluginManagementService = pluginManagementService;
_settingsVmFactory = settingsVmFactory;
@@ -78,9 +73,7 @@ public partial class InstalledTabItemViewModel : ViewModelBase
if (InstalledEntry.EntryType == EntryType.Plugin)
await UninstallPluginPrerequisites();
IEntryInstallationHandler handler = _factory.CreateHandler(InstalledEntry.EntryType);
await handler.UninstallAsync(InstalledEntry, cancellationToken);
IsRemoved = true;
await _workshopService.UninstallEntry(InstalledEntry, cancellationToken);
}
private async Task UninstallPluginPrerequisites()
@@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Reactive;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using Artemis.UI.Shared.Routing;
using Artemis.WebClient.Workshop.Models;
@@ -15,31 +16,41 @@ namespace Artemis.UI.Screens.Workshop.Library.Tabs;
public partial class InstalledTabViewModel : RoutableScreen
{
private SourceList<InstalledEntry> _installedEntries = new();
[Notify] private string? _searchEntryInput;
public InstalledTabViewModel(IWorkshopService workshopService, IRouter router, Func<InstalledEntry, InstalledTabItemViewModel> getInstalledTabItemViewModel)
{
SourceList<InstalledEntry> installedEntries = new();
IObservable<Func<InstalledEntry, bool>> pluginFilter = this.WhenAnyValue(vm => vm.SearchEntryInput).Throttle(TimeSpan.FromMilliseconds(100)).Select(CreatePredicate);
installedEntries.Connect()
_installedEntries.Connect()
.Filter(pluginFilter)
.Sort(SortExpressionComparer<InstalledEntry>.Descending(p => p.InstalledAt))
.Transform(getInstalledTabItemViewModel)
.AutoRefresh(vm => vm.IsRemoved)
.Filter(vm => !vm.IsRemoved)
.Bind(out ReadOnlyObservableCollection<InstalledTabItemViewModel> installedEntryViewModels)
.Subscribe();
List<InstalledEntry> entries = workshopService.GetInstalledEntries();
installedEntries.AddRange(entries);
_installedEntries.AddRange(entries);
Empty = entries.Count == 0;
InstalledEntries = installedEntryViewModels;
this.WhenActivated(d =>
{
workshopService.OnEntryUninstalled += WorkshopServiceOnOnEntryUninstalled;
Disposable.Create(() => workshopService.OnEntryUninstalled -= WorkshopServiceOnOnEntryUninstalled).DisposeWith(d);
});
OpenWorkshop = ReactiveCommand.CreateFromTask(async () => await router.Navigate("workshop"));
}
private void WorkshopServiceOnOnEntryUninstalled(object? sender, InstalledEntry e)
{
_installedEntries.Remove(e);
}
public bool Empty { get; }
public ReactiveCommand<Unit, Unit> OpenWorkshop { get; }
public ReadOnlyObservableCollection<InstalledTabItemViewModel> InstalledEntries { get; }