mirror of
https://github.com/Artemis-RGB/Artemis
synced 2025-12-13 05:48:35 +00:00
Workshop - Implemented plugin browsing, installation and removal
This commit is contained in:
parent
d9df443970
commit
21b8112de5
@ -35,24 +35,4 @@
|
|||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<AvaloniaResource Include="Assets\**" />
|
<AvaloniaResource Include="Assets\**" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<Compile Update="Screens\Workshop\SubmissionWizard\Steps\Plugin\PluginSelectionStepView.axaml.cs">
|
|
||||||
<DependentUpon>PluginSelectionStepView.axaml</DependentUpon>
|
|
||||||
<SubType>Code</SubType>
|
|
||||||
</Compile>
|
|
||||||
<Compile Update="Screens\Workshop\SubmissionWizard\Steps\Plugin\PluginAdaptionHintsStepView.axaml.cs">
|
|
||||||
<DependentUpon>ProfileAdaptionHintsStepView.axaml</DependentUpon>
|
|
||||||
<SubType>Code</SubType>
|
|
||||||
</Compile>
|
|
||||||
<Compile Update="Screens\Workshop\SubmissionWizard\Steps\Plugin\PluginSelectionStepView.axaml.cs">
|
|
||||||
<DependentUpon>ProfileSelectionStepView.axaml</DependentUpon>
|
|
||||||
<SubType>Code</SubType>
|
|
||||||
</Compile>
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<AdditionalFiles Include="Screens\Workshop\SubmissionWizard\Steps\Plugin\PluginAdaptionHintsStepView.axaml" />
|
|
||||||
<AdditionalFiles Include="Screens\Workshop\SubmissionWizard\Steps\Plugin\PluginSelectionStepView.axaml" />
|
|
||||||
</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;
|
||||||
|
|||||||
@ -12,6 +12,7 @@ using Artemis.UI.Screens.Workshop.Home;
|
|||||||
using Artemis.UI.Screens.Workshop.Layout;
|
using Artemis.UI.Screens.Workshop.Layout;
|
||||||
using Artemis.UI.Screens.Workshop.Library;
|
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.Plugin;
|
||||||
using Artemis.UI.Screens.Workshop.Profile;
|
using Artemis.UI.Screens.Workshop.Profile;
|
||||||
using Artemis.UI.Shared.Routing;
|
using Artemis.UI.Shared.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}"),
|
||||||
|
|||||||
@ -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)
|
||||||
{
|
{
|
||||||
@ -77,6 +79,9 @@ public class RootViewModel : RoutableHostScreen<RoutableScreen>, IMainWindowProv
|
|||||||
if (_updateService.Initialize())
|
if (_updateService.Initialize())
|
||||||
return;
|
return;
|
||||||
|
|
||||||
|
// Before initializing the core and files become in use, clean up orphaned files
|
||||||
|
workshopService.RemoveOrphanedFiles();
|
||||||
|
|
||||||
coreService.Initialize();
|
coreService.Initialize();
|
||||||
registrationService.RegisterBuiltInDataModelDisplays();
|
registrationService.RegisterBuiltInDataModelDisplays();
|
||||||
registrationService.RegisterBuiltInDataModelInputs();
|
registrationService.RegisterBuiltInDataModelInputs();
|
||||||
|
|||||||
@ -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.Plugin, "Plugins", "workshop/entries/plugins/1", "workshop/entries/plugins"),
|
||||||
new(MaterialIconKind.Bookshelf, "Library", "workshop/library"),
|
new(MaterialIconKind.Bookshelf, "Library", "workshop/library"),
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
|||||||
@ -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();
|
||||||
|
|||||||
@ -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="Plugin" 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" />
|
||||||
|
|||||||
@ -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:plugin="clr-namespace:Artemis.UI.Screens.Workshop.Plugin"
|
||||||
|
xmlns:mdxaml="https://github.com/whistyun/Markdown.Avalonia.Tight"
|
||||||
|
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||||
|
x:Class="Artemis.UI.Screens.Workshop.Plugin.PluginDetailsView"
|
||||||
|
x:DataType="plugin: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,14 @@
|
|||||||
|
using Avalonia;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Markup.Xaml;
|
||||||
|
using Avalonia.ReactiveUI;
|
||||||
|
|
||||||
|
namespace Artemis.UI.Screens.Workshop.Plugin;
|
||||||
|
|
||||||
|
public partial class PluginDetailsView : ReactiveUserControl<PluginDetailsViewModel>
|
||||||
|
{
|
||||||
|
public PluginDetailsView()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,59 @@
|
|||||||
|
using System;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Artemis.UI.Screens.Workshop.Entries.Details;
|
||||||
|
using Artemis.UI.Screens.Workshop.Parameters;
|
||||||
|
using Artemis.UI.Shared.Routing;
|
||||||
|
using Artemis.WebClient.Workshop;
|
||||||
|
using PropertyChanged.SourceGenerator;
|
||||||
|
using StrawberryShake;
|
||||||
|
|
||||||
|
namespace Artemis.UI.Screens.Workshop.Plugin;
|
||||||
|
|
||||||
|
public partial class PluginDetailsViewModel : RoutableScreen<WorkshopDetailParameters>
|
||||||
|
{
|
||||||
|
private readonly IWorkshopClient _client;
|
||||||
|
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,
|
||||||
|
Func<IGetEntryById_Entry, EntryInfoViewModel> getEntryInfoViewModel,
|
||||||
|
Func<IGetEntryById_Entry, EntryReleasesViewModel> getEntryReleasesViewModel,
|
||||||
|
Func<IGetEntryById_Entry, EntryImagesViewModel> getEntryImagesViewModel)
|
||||||
|
{
|
||||||
|
_client = client;
|
||||||
|
_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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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,122 @@
|
|||||||
|
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);
|
||||||
|
|
||||||
|
// 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));
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -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 RemoveOrphanedFiles();
|
||||||
|
|
||||||
public record WorkshopStatus(bool IsReachable, string Message);
|
public record WorkshopStatus(bool IsReachable, string Message);
|
||||||
}
|
}
|
||||||
@ -1,20 +1,24 @@
|
|||||||
using System.Net.Http.Headers;
|
using System.Net.Http.Headers;
|
||||||
|
using Artemis.Core;
|
||||||
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.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;
|
||||||
|
|
||||||
public WorkshopService(IHttpClientFactory httpClientFactory, IRouter router, IEntryRepository entryRepository)
|
public WorkshopService(ILogger logger, IHttpClientFactory httpClientFactory, IRouter router, IEntryRepository entryRepository)
|
||||||
{
|
{
|
||||||
|
_logger = logger;
|
||||||
_httpClientFactory = httpClientFactory;
|
_httpClientFactory = httpClientFactory;
|
||||||
_router = router;
|
_router = router;
|
||||||
_entryRepository = entryRepository;
|
_entryRepository = entryRepository;
|
||||||
@ -117,6 +121,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 +140,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 +156,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 +168,38 @@ public class WorkshopService : IWorkshopService
|
|||||||
entry.Save();
|
entry.Save();
|
||||||
_entryRepository.Save(entry.Entity);
|
_entryRepository.Save(entry.Entity);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -2,10 +2,10 @@ namespace Artemis.WebClient.Workshop;
|
|||||||
|
|
||||||
public static class WorkshopConstants
|
public static class WorkshopConstants
|
||||||
{
|
{
|
||||||
// public const string AUTHORITY_URL = "https://localhost:5001";
|
public const string AUTHORITY_URL = "https://localhost:5001";
|
||||||
// public const string WORKSHOP_URL = "https://localhost:7281";
|
public const string WORKSHOP_URL = "https://localhost:7281";
|
||||||
public const string AUTHORITY_URL = "https://identity.artemis-rgb.com";
|
// public const string AUTHORITY_URL = "https://identity.artemis-rgb.com";
|
||||||
public const string WORKSHOP_URL = "https://workshop.artemis-rgb.com";
|
// public const string WORKSHOP_URL = "https://workshop.artemis-rgb.com";
|
||||||
public const string IDENTITY_CLIENT_NAME = "IdentityApiClient";
|
public const string IDENTITY_CLIENT_NAME = "IdentityApiClient";
|
||||||
public const string WORKSHOP_CLIENT_NAME = "WorkshopApiClient";
|
public const string WORKSHOP_CLIENT_NAME = "WorkshopApiClient";
|
||||||
}
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user