1
0
mirror of https://github.com/Artemis-RGB/Artemis synced 2025-12-13 05:48:35 +00:00

Workshop Library - Finished basic implementation of installed tab

This commit is contained in:
Robert 2023-09-08 17:06:48 +02:00
parent 876465cfdb
commit 3d1e53e395
32 changed files with 407 additions and 191 deletions

View File

@ -160,10 +160,10 @@ public class ProfileEditorViewModel : RoutableScreen<ProfileEditorViewModelParam
{ {
ProfileConfiguration? profileConfiguration = _profileService.ProfileConfigurations.FirstOrDefault(c => c.ProfileId == parameters.ProfileId); ProfileConfiguration? profileConfiguration = _profileService.ProfileConfigurations.FirstOrDefault(c => c.ProfileId == parameters.ProfileId);
// If the profile doesn't exist, navigate home for lack of some kind of 404 :p // If the profile doesn't exist, cancel navigation
if (profileConfiguration == null) if (profileConfiguration == null)
{ {
await args.Router.Navigate("home"); args.Cancel();
return; return;
} }

View File

@ -10,7 +10,6 @@ using Artemis.UI.Shared.Services;
using Artemis.UI.Shared.Services.Builders; using Artemis.UI.Shared.Services.Builders;
using Artemis.UI.Shared.Utilities; using Artemis.UI.Shared.Utilities;
using Artemis.WebClient.Workshop; using Artemis.WebClient.Workshop;
using Artemis.WebClient.Workshop.DownloadHandlers;
using ReactiveUI; using ReactiveUI;
using StrawberryShake; using StrawberryShake;

View File

@ -15,8 +15,8 @@ using Artemis.UI.Shared.Services;
using Artemis.UI.Shared.Utilities; using Artemis.UI.Shared.Utilities;
using Artemis.WebClient.Workshop; using Artemis.WebClient.Workshop;
using Artemis.WebClient.Workshop.Exceptions; using Artemis.WebClient.Workshop.Exceptions;
using Artemis.WebClient.Workshop.Handlers.UploadHandlers;
using Artemis.WebClient.Workshop.Services; using Artemis.WebClient.Workshop.Services;
using Artemis.WebClient.Workshop.UploadHandlers;
using Avalonia.Media.Imaging; using Avalonia.Media.Imaging;
using ReactiveUI; using ReactiveUI;
using StrawberryShake; using StrawberryShake;

View File

@ -5,6 +5,7 @@
xmlns:tabs="clr-namespace:Artemis.UI.Screens.Workshop.Library.Tabs" xmlns:tabs="clr-namespace:Artemis.UI.Screens.Workshop.Library.Tabs"
xmlns:asyncImageLoader="clr-namespace:AsyncImageLoader;assembly=AsyncImageLoader.Avalonia" xmlns:asyncImageLoader="clr-namespace:AsyncImageLoader;assembly=AsyncImageLoader.Avalonia"
xmlns:converters="clr-namespace:Artemis.UI.Converters" xmlns:converters="clr-namespace:Artemis.UI.Converters"
xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Artemis.UI.Screens.Workshop.Library.Tabs.InstalledTabItemView" x:Class="Artemis.UI.Screens.Workshop.Library.Tabs.InstalledTabItemView"
x:DataType="tabs:InstalledTabItemViewModel"> x:DataType="tabs:InstalledTabItemViewModel">
@ -12,14 +13,14 @@
<converters:EntryIconUriConverter x:Key="EntryIconUriConverter" /> <converters:EntryIconUriConverter x:Key="EntryIconUriConverter" />
<converters:DateTimeConverter x:Key="DateTimeConverter" /> <converters:DateTimeConverter x:Key="DateTimeConverter" />
</UserControl.Resources> </UserControl.Resources>
<Border Classes="card-condensed" <Button MinHeight="65"
MinHeight="80"
MaxHeight="110" MaxHeight="110"
Padding="12 6" Padding="6"
Margin="0 0 0 5" Margin="0 0 0 5"
HorizontalAlignment="Stretch"> HorizontalAlignment="Stretch"
<Grid ColumnDefinitions="Auto,*,Auto"> HorizontalContentAlignment="Stretch"
<!-- Icon --> Command="{CompiledBinding ViewWorkshopPage}">
<Grid ColumnDefinitions="Auto,*,*,*,Auto">
<Border Grid.Column="0" <Border Grid.Column="0"
CornerRadius="6" CornerRadius="6"
VerticalAlignment="Center" VerticalAlignment="Center"
@ -30,24 +31,28 @@
<Image Stretch="UniformToFill" asyncImageLoader:ImageLoader.Source="{CompiledBinding InstalledEntry.EntryId, Converter={StaticResource EntryIconUriConverter}, Mode=OneWay}" /> <Image Stretch="UniformToFill" asyncImageLoader:ImageLoader.Source="{CompiledBinding InstalledEntry.EntryId, Converter={StaticResource EntryIconUriConverter}, Mode=OneWay}" />
</Border> </Border>
<!-- Body --> <StackPanel Grid.Column="1" VerticalAlignment="Center">
<Grid Grid.Column="1" VerticalAlignment="Stretch" RowDefinitions="Auto,*,Auto"> <TextBlock TextTrimming="CharacterEllipsis"
<TextBlock Grid.Row="0"
Classes="h5 no-margin"
TextTrimming="CharacterEllipsis"
Text="{CompiledBinding InstalledEntry.Name, FallbackValue=Title}" /> Text="{CompiledBinding InstalledEntry.Name, FallbackValue=Title}" />
<TextBlock Grid.Row="1" <TextBlock Classes="subtitle"
Classes="subtitle"
TextWrapping="Wrap" TextWrapping="Wrap"
TextTrimming="CharacterEllipsis" TextTrimming="CharacterEllipsis"
Text="{CompiledBinding InstalledEntry.Summary, FallbackValue=Summary}"> Text="{CompiledBinding InstalledEntry.Author, FallbackValue=Summary}">
</TextBlock> </TextBlock>
</Grid> </StackPanel>
<!-- Info --> <TextBlock Grid.Column="2" VerticalAlignment="Center" Text="{CompiledBinding InstalledEntry.EntryType}"></TextBlock>
<StackPanel Grid.Column="2" Margin="0 0 4 0"> <TextBlock Grid.Column="3" VerticalAlignment="Center">
<TextBlock TextAlignment="Right" Text="{CompiledBinding InstalledEntry.InstalledAt, FallbackValue=01-01-1337, Converter={StaticResource DateTimeConverter}}" /> <Run>Installed</Run>
<Run Text="{CompiledBinding InstalledEntry.InstalledAt, FallbackValue=01-01-1337, Converter={StaticResource DateTimeConverter}}" />
</TextBlock>
<StackPanel Grid.Column="4" VerticalAlignment="Center" Orientation="Horizontal" Spacing="6">
<Button Command="{CompiledBinding ViewLocal}">Open</Button>
<Button Command="{CompiledBinding Uninstall}" Theme="{StaticResource TransparentButton}" Height="32">
<avalonia:MaterialIcon Kind="Trash"/>
</Button>
</StackPanel> </StackPanel>
</Grid> </Grid>
</Border> </Button>
</UserControl> </UserControl>

View File

@ -1,14 +1,70 @@
using System;
using System.Reactive;
using System.Threading;
using System.Threading.Tasks;
using Artemis.UI.Shared; 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.Services; using Artemis.WebClient.Workshop.Services;
using ReactiveUI;
namespace Artemis.UI.Screens.Workshop.Library.Tabs; namespace Artemis.UI.Screens.Workshop.Library.Tabs;
public class InstalledTabItemViewModel : ViewModelBase public class InstalledTabItemViewModel : ViewModelBase
{ {
public InstalledTabItemViewModel(InstalledEntry installedEntry) private readonly IWorkshopService _workshopService;
private readonly IRouter _router;
private readonly EntryInstallationHandlerFactory _factory;
private readonly IWindowService _windowService;
private bool _isRemoved;
public InstalledTabItemViewModel(InstalledEntry installedEntry, IWorkshopService workshopService, IRouter router, EntryInstallationHandlerFactory factory, IWindowService windowService)
{ {
_workshopService = workshopService;
_router = router;
_factory = factory;
_windowService = windowService;
InstalledEntry = installedEntry; InstalledEntry = installedEntry;
ViewWorkshopPage = ReactiveCommand.CreateFromTask(ExecuteViewWorkshopPage);
ViewLocal = ReactiveCommand.CreateFromTask(ExecuteViewLocal);
Uninstall = ReactiveCommand.CreateFromTask(ExecuteUninstall);
}
public bool IsRemoved
{
get => _isRemoved;
private set => RaiseAndSetIfChanged(ref _isRemoved, value);
} }
public InstalledEntry InstalledEntry { get; } public InstalledEntry InstalledEntry { get; }
public ReactiveCommand<Unit, Unit> ViewWorkshopPage { get; }
public ReactiveCommand<Unit,Unit> ViewLocal { get; }
public ReactiveCommand<Unit, Unit> Uninstall { get; }
private async Task ExecuteViewWorkshopPage()
{
await _workshopService.NavigateToEntry(InstalledEntry.EntryId, InstalledEntry.EntryType);
}
private async Task ExecuteViewLocal(CancellationToken cancellationToken)
{
if (InstalledEntry.EntryType == EntryType.Profile && Guid.TryParse(InstalledEntry.LocalReference, out Guid profileId))
{
await _router.Navigate($"profile-editor/{profileId}");
}
}
private async Task ExecuteUninstall(CancellationToken cancellationToken)
{
bool confirmed = await _windowService.ShowConfirmContentDialog("Do you want to uninstall this entry?", "Both the entry and its contents will be removed.");
if (!confirmed)
return;
IEntryInstallationHandler handler = _factory.CreateHandler(InstalledEntry.EntryType);
await handler.UninstallAsync(InstalledEntry, cancellationToken);
IsRemoved = true;
}
} }

View File

@ -6,13 +6,34 @@
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Artemis.UI.Screens.Workshop.Library.Tabs.InstalledTabView" x:Class="Artemis.UI.Screens.Workshop.Library.Tabs.InstalledTabView"
x:DataType="tabs:InstalledTabViewModel"> x:DataType="tabs:InstalledTabViewModel">
<ScrollViewer> <UserControl.Styles>
<ItemsRepeater ItemsSource="{CompiledBinding InstalledEntries}"> <Styles>
<ItemsRepeater.ItemTemplate> <Style Selector="StackPanel.empty-state > TextBlock">
<DataTemplate> <Setter Property="TextAlignment" Value="Center"></Setter>
<ContentControl Content="{CompiledBinding}"></ContentControl> <Setter Property="TextWrapping" Value="Wrap"></Setter>
</DataTemplate> </Style>
</ItemsRepeater.ItemTemplate> </Styles>
</ItemsRepeater> </UserControl.Styles>
</ScrollViewer>
<Panel>
<StackPanel IsVisible="{CompiledBinding Empty}" Margin="0 50 0 0" Classes="empty-state">
<TextBlock Theme="{StaticResource TitleTextBlockStyle}">Not much here yet, huh!</TextBlock>
<TextBlock>
<Run>Any entries you download from the workshop you can later manage here</Run>
</TextBlock>
<Lottie Path="/Assets/Animations/empty.json" RepeatCount="1" Width="350" Height="350"></Lottie>
<Button HorizontalAlignment="Center" Command="{CompiledBinding OpenWorkshop}">Browse the Workshop</Button>
</StackPanel>
<ScrollViewer IsVisible="{CompiledBinding !Empty}">
<ItemsRepeater ItemsSource="{CompiledBinding InstalledEntries}">
<ItemsRepeater.ItemTemplate>
<DataTemplate>
<ContentControl Content="{CompiledBinding}"/>
</DataTemplate>
</ItemsRepeater.ItemTemplate>
</ItemsRepeater>
</ScrollViewer>
</Panel>
</UserControl> </UserControl>

View File

@ -1,10 +1,10 @@
using System; using System;
using System.Collections.Generic;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.Reactive.Disposables; using System.Reactive;
using System.Reactive.Linq; using System.Reactive.Linq;
using Artemis.UI.Shared.Routing; using Artemis.UI.Shared.Routing;
using Artemis.WebClient.Workshop.Services; using Artemis.WebClient.Workshop.Services;
using Avalonia.ReactiveUI;
using DynamicData; using DynamicData;
using DynamicData.Binding; using DynamicData.Binding;
using ReactiveUI; using ReactiveUI;
@ -15,27 +15,32 @@ public class InstalledTabViewModel : RoutableScreen
{ {
private string? _searchEntryInput; private string? _searchEntryInput;
public InstalledTabViewModel(IWorkshopService workshopService, Func<InstalledEntry, InstalledTabItemViewModel> getInstalledTabItemViewModel) public InstalledTabViewModel(IWorkshopService workshopService, IRouter router, Func<InstalledEntry, InstalledTabItemViewModel> getInstalledTabItemViewModel)
{ {
SourceList<InstalledEntry> installedEntries = new(); SourceList<InstalledEntry> installedEntries = new();
IObservable<Func<InstalledEntry, bool>> pluginFilter = this.WhenAnyValue(vm => vm.SearchEntryInput).Throttle(TimeSpan.FromMilliseconds(100)).Select(CreatePredicate); IObservable<Func<InstalledEntry, bool>> pluginFilter = this.WhenAnyValue(vm => vm.SearchEntryInput).Throttle(TimeSpan.FromMilliseconds(100)).Select(CreatePredicate);
installedEntries.Connect() installedEntries.Connect()
.Filter(pluginFilter) .Filter(pluginFilter)
.Sort(SortExpressionComparer<InstalledEntry>.Ascending(p => p.Name)) .Sort(SortExpressionComparer<InstalledEntry>.Descending(p => p.InstalledAt))
.Transform(getInstalledTabItemViewModel) .Transform(getInstalledTabItemViewModel)
.ObserveOn(AvaloniaScheduler.Instance) .AutoRefresh(vm => vm.IsRemoved)
.Filter(vm => !vm.IsRemoved)
.Bind(out ReadOnlyObservableCollection<InstalledTabItemViewModel> installedEntryViewModels) .Bind(out ReadOnlyObservableCollection<InstalledTabItemViewModel> installedEntryViewModels)
.Subscribe(); .Subscribe();
List<InstalledEntry> entries = workshopService.GetInstalledEntries();
installedEntries.AddRange(entries);
Empty = entries.Count == 0;
InstalledEntries = installedEntryViewModels; InstalledEntries = installedEntryViewModels;
this.WhenActivated(d => OpenWorkshop = ReactiveCommand.CreateFromTask(async () => await router.Navigate("workshop"));
{
installedEntries.AddRange(workshopService.GetInstalledEntries());
Disposable.Create(installedEntries, e => e.Clear()).DisposeWith(d);
});
} }
public bool Empty { get; }
public ReactiveCommand<Unit, Unit> OpenWorkshop { get; }
public ReadOnlyObservableCollection<InstalledTabItemViewModel> InstalledEntries { get; } public ReadOnlyObservableCollection<InstalledTabItemViewModel> InstalledEntries { get; }
public string? SearchEntryInput public string? SearchEntryInput
@ -49,7 +54,6 @@ public class InstalledTabViewModel : RoutableScreen
if (string.IsNullOrWhiteSpace(text)) if (string.IsNullOrWhiteSpace(text))
return _ => true; return _ => true;
return data => data.Name.Contains(text, StringComparison.InvariantCultureIgnoreCase) || return data => data.Name.Contains(text, StringComparison.InvariantCultureIgnoreCase);
data.Summary.Contains(text, StringComparison.InvariantCultureIgnoreCase);
} }
} }

View File

@ -0,0 +1,76 @@
<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.Library.Tabs"
xmlns:asyncImageLoader="clr-namespace:AsyncImageLoader;assembly=AsyncImageLoader.Avalonia"
xmlns:avalonia1="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
xmlns:converters="clr-namespace:Artemis.UI.Converters"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Artemis.UI.Screens.Workshop.Library.Tabs.SubmissionsTabItemView"
x:DataType="tabs:SubmissionsTabItemViewModel">
<UserControl.Resources>
<converters:EntryIconUriConverter x:Key="EntryIconUriConverter" />
<converters:DateTimeConverter x:Key="DateTimeConverter" />
</UserControl.Resources>
<Button MinHeight="80"
MaxHeight="110"
Padding="12 6"
Margin="0 0 0 5"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Stretch"
Command="{CompiledBinding NavigateToEntry}">
<Grid ColumnDefinitions="Auto,*,Auto">
<!-- Icon -->
<Border Grid.Column="0"
CornerRadius="6"
VerticalAlignment="Center"
Margin="0 0 10 0"
Width="50"
Height="50"
ClipToBounds="True">
<Image Stretch="UniformToFill" asyncImageLoader:ImageLoader.Source="{CompiledBinding Entry.Id, Converter={StaticResource EntryIconUriConverter}, Mode=OneWay}" />
</Border>
<!-- Body -->
<Grid Grid.Column="1" VerticalAlignment="Stretch" RowDefinitions="Auto,*,Auto">
<TextBlock Grid.Row="0"
Classes="h5 no-margin"
TextTrimming="CharacterEllipsis"
Text="{CompiledBinding Entry.Name, FallbackValue=Title}" />
<TextBlock Grid.Row="1"
Classes="subtitle"
TextWrapping="Wrap"
TextTrimming="CharacterEllipsis"
Text="{CompiledBinding Entry.Summary, FallbackValue=Summary}">
</TextBlock>
<ItemsControl Grid.Row="2" ItemsSource="{CompiledBinding Entry.Categories}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal" Spacing="8"></StackPanel>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<avalonia1:MaterialIcon Kind="{CompiledBinding Icon}" Margin="0 0 3 0"></avalonia1:MaterialIcon>
<TextBlock Text="{CompiledBinding Name}" TextTrimming="CharacterEllipsis" />
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Grid>
<!-- Info -->
<StackPanel Grid.Column="2" Margin="0 0 4 0">
<TextBlock TextAlignment="Right" Text="{CompiledBinding Entry.CreatedAt, FallbackValue=01-01-1337, Converter={StaticResource DateTimeConverter}}" />
<TextBlock TextAlignment="Right">
<avalonia1:MaterialIcon Kind="Downloads" />
<Run Classes="h5" Text="{CompiledBinding Entry.Downloads, FallbackValue=0}" />
<Run>downloads</Run>
</TextBlock>
</StackPanel>
</Grid>
</Button>
</UserControl>

View File

@ -0,0 +1,13 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
namespace Artemis.UI.Screens.Workshop.Library.Tabs;
public partial class SubmissionsTabItemView : UserControl
{
public SubmissionsTabItemView()
{
InitializeComponent();
}
}

View File

@ -0,0 +1,30 @@
using System.Reactive;
using System.Threading;
using System.Threading.Tasks;
using Artemis.UI.Shared;
using Artemis.UI.Shared.Routing;
using Artemis.WebClient.Workshop;
using ReactiveUI;
namespace Artemis.UI.Screens.Workshop.Library.Tabs;
public class SubmissionsTabItemViewModel : ViewModelBase
{
private readonly IRouter _router;
public SubmissionsTabItemViewModel(IGetSubmittedEntries_SubmittedEntries entry, IRouter router)
{
_router = router;
Entry = entry;
NavigateToEntry = ReactiveCommand.CreateFromTask(ExecuteNavigateToEntry);
}
public IGetSubmittedEntries_SubmittedEntries Entry { get; }
public ReactiveCommand<Unit, Unit> NavigateToEntry { get; }
private async Task ExecuteNavigateToEntry(CancellationToken cancellationToken)
{
await _router.Navigate($"workshop/library/submissions/{Entry.Id}");
}
}

View File

@ -3,17 +3,9 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:tabs="clr-namespace:Artemis.UI.Screens.Workshop.Library.Tabs" xmlns:tabs="clr-namespace:Artemis.UI.Screens.Workshop.Library.Tabs"
xmlns:workshop="clr-namespace:Artemis.WebClient.Workshop;assembly=Artemis.WebClient.Workshop"
xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
xmlns:asyncImageLoader="clr-namespace:AsyncImageLoader;assembly=AsyncImageLoader.Avalonia"
xmlns:converters="clr-namespace:Artemis.UI.Converters"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="650" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="650"
x:Class="Artemis.UI.Screens.Workshop.Library.Tabs.SubmissionsTabView" x:Class="Artemis.UI.Screens.Workshop.Library.Tabs.SubmissionsTabView"
x:DataType="tabs:SubmissionsTabViewModel"> x:DataType="tabs:SubmissionsTabViewModel">
<UserControl.Resources>
<converters:EntryIconUriConverter x:Key="EntryIconUriConverter" />
<converters:DateTimeConverter x:Key="DateTimeConverter" />
</UserControl.Resources>
<UserControl.Styles> <UserControl.Styles>
<Styles> <Styles>
@ -44,71 +36,11 @@
<Button HorizontalAlignment="Center" Command="{CompiledBinding AddSubmission}">Submit new entry</Button> <Button HorizontalAlignment="Center" Command="{CompiledBinding AddSubmission}">Submit new entry</Button>
</StackPanel> </StackPanel>
<ScrollViewer> <ScrollViewer IsVisible="{CompiledBinding Entries.Count}">
<ItemsRepeater IsVisible="{CompiledBinding Entries.Count}" ItemsSource="{CompiledBinding Entries}"> <ItemsRepeater ItemsSource="{CompiledBinding Entries}">
<ItemsRepeater.ItemTemplate> <ItemsRepeater.ItemTemplate>
<DataTemplate DataType="workshop:IGetSubmittedEntries_SubmittedEntries"> <DataTemplate>
<Button MinHeight="80" <ContentControl Content="{CompiledBinding}"/>
MaxHeight="110"
Padding="12 6"
Margin="0 0 0 5"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Stretch"
Command="{Binding $parent[tabs:SubmissionsTabView].DataContext.NavigateToEntry}"
CommandParameter="{CompiledBinding}">
<Grid ColumnDefinitions="Auto,*,Auto">
<!-- Icon -->
<Border Grid.Column="0"
CornerRadius="6"
VerticalAlignment="Center"
Margin="0 0 10 0"
Width="50"
Height="50"
ClipToBounds="True">
<Image Stretch="UniformToFill" asyncImageLoader:ImageLoader.Source="{CompiledBinding Id, Converter={StaticResource EntryIconUriConverter}, Mode=OneWay}" />
</Border>
<!-- Body -->
<Grid Grid.Column="1" VerticalAlignment="Stretch" RowDefinitions="Auto,*,Auto">
<TextBlock Grid.Row="0"
Classes="h5 no-margin"
TextTrimming="CharacterEllipsis"
Text="{CompiledBinding Name, FallbackValue=Title}"/>
<TextBlock Grid.Row="1"
Classes="subtitle"
TextWrapping="Wrap"
TextTrimming="CharacterEllipsis"
Text="{CompiledBinding Summary, FallbackValue=Summary}">
</TextBlock>
<ItemsControl Grid.Row="2" ItemsSource="{CompiledBinding Categories}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal" Spacing="8"></StackPanel>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<avalonia:MaterialIcon Kind="{CompiledBinding Icon}" Margin="0 0 3 0"></avalonia:MaterialIcon>
<TextBlock Text="{CompiledBinding Name}" TextTrimming="CharacterEllipsis" />
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Grid>
<!-- Info -->
<StackPanel Grid.Column="2" Margin="0 0 4 0">
<TextBlock TextAlignment="Right" Text="{CompiledBinding CreatedAt, FallbackValue=01-01-1337, Converter={StaticResource DateTimeConverter}}" />
<TextBlock TextAlignment="Right">
<avalonia:MaterialIcon Kind="Downloads" />
<Run Classes="h5" Text="{CompiledBinding Downloads, FallbackValue=0}" />
<Run>downloads</Run>
</TextBlock>
</StackPanel>
</Grid>
</Button>
</DataTemplate> </DataTemplate>
</ItemsRepeater.ItemTemplate> </ItemsRepeater.ItemTemplate>
</ItemsRepeater> </ItemsRepeater>

View File

@ -21,21 +21,25 @@ public class SubmissionsTabViewModel : RoutableScreen
private readonly IWorkshopClient _client; private readonly IWorkshopClient _client;
private readonly SourceCache<IGetSubmittedEntries_SubmittedEntries, Guid> _entries; private readonly SourceCache<IGetSubmittedEntries_SubmittedEntries, Guid> _entries;
private readonly IWindowService _windowService; private readonly IWindowService _windowService;
private readonly IRouter _router;
private bool _isLoading = true; private bool _isLoading = true;
private bool _workshopReachable; private bool _workshopReachable;
public SubmissionsTabViewModel(IWorkshopClient client, IAuthenticationService authenticationService, IWindowService windowService, IWorkshopService workshopService, IRouter router) public SubmissionsTabViewModel(IWorkshopClient client,
IAuthenticationService authenticationService,
IWindowService windowService,
IWorkshopService workshopService,
Func<IGetSubmittedEntries_SubmittedEntries, SubmissionsTabItemViewModel> getSubmissionsTabItemViewModel)
{ {
_client = client; _client = client;
_windowService = windowService; _windowService = windowService;
_router = router;
_entries = new SourceCache<IGetSubmittedEntries_SubmittedEntries, Guid>(e => e.Id); _entries = new SourceCache<IGetSubmittedEntries_SubmittedEntries, Guid>(e => e.Id);
_entries.Connect().Bind(out ReadOnlyObservableCollection<IGetSubmittedEntries_SubmittedEntries> entries).Subscribe(); _entries.Connect()
.Transform(getSubmissionsTabItemViewModel)
.Bind(out ReadOnlyObservableCollection<SubmissionsTabItemViewModel> entries)
.Subscribe();
AddSubmission = ReactiveCommand.CreateFromTask(ExecuteAddSubmission, this.WhenAnyValue(vm => vm.WorkshopReachable)); AddSubmission = ReactiveCommand.CreateFromTask(ExecuteAddSubmission, this.WhenAnyValue(vm => vm.WorkshopReachable));
Login = ReactiveCommand.CreateFromTask(ExecuteLogin, this.WhenAnyValue(vm => vm.WorkshopReachable)); Login = ReactiveCommand.CreateFromTask(ExecuteLogin, this.WhenAnyValue(vm => vm.WorkshopReachable));
NavigateToEntry = ReactiveCommand.CreateFromTask<IGetSubmittedEntries_SubmittedEntries>(ExecuteNavigateToEntry);
IsLoggedIn = authenticationService.IsLoggedIn; IsLoggedIn = authenticationService.IsLoggedIn;
Entries = entries; Entries = entries;
@ -53,7 +57,7 @@ public class SubmissionsTabViewModel : RoutableScreen
public ReactiveCommand<IGetSubmittedEntries_SubmittedEntries, Unit> NavigateToEntry { get; } public ReactiveCommand<IGetSubmittedEntries_SubmittedEntries, Unit> NavigateToEntry { get; }
public IObservable<bool> IsLoggedIn { get; } public IObservable<bool> IsLoggedIn { get; }
public ReadOnlyObservableCollection<IGetSubmittedEntries_SubmittedEntries> Entries { get; } public ReadOnlyObservableCollection<SubmissionsTabItemViewModel> Entries { get; }
public bool WorkshopReachable public bool WorkshopReachable
{ {
@ -78,11 +82,6 @@ public class SubmissionsTabViewModel : RoutableScreen
await _windowService.ShowDialogAsync<SubmissionWizardViewModel>(); await _windowService.ShowDialogAsync<SubmissionWizardViewModel>();
} }
private async Task ExecuteNavigateToEntry(IGetSubmittedEntries_SubmittedEntries entry, CancellationToken cancellationToken)
{
await _router.Navigate($"workshop/library/submissions/{entry.Id}");
}
private async Task GetEntries(CancellationToken ct) private async Task GetEntries(CancellationToken ct)
{ {
IsLoading = true; IsLoading = true;

View File

@ -10,8 +10,8 @@ using Artemis.UI.Shared.Services;
using Artemis.UI.Shared.Services.Builders; using Artemis.UI.Shared.Services.Builders;
using Artemis.UI.Shared.Utilities; using Artemis.UI.Shared.Utilities;
using Artemis.WebClient.Workshop; using Artemis.WebClient.Workshop;
using Artemis.WebClient.Workshop.DownloadHandlers; using Artemis.WebClient.Workshop.Handlers.InstallationHandlers;
using Artemis.WebClient.Workshop.DownloadHandlers.Implementations; using Artemis.WebClient.Workshop.Handlers.InstallationHandlers.Implementations;
using ReactiveUI; using ReactiveUI;
using StrawberryShake; using StrawberryShake;
@ -70,7 +70,7 @@ public class ProfileDetailsViewModel : RoutableScreen<WorkshopDetailParameters>
if (!confirm) if (!confirm)
return; return;
EntryInstallResult<ProfileConfiguration> result = await _installationHandler.InstallProfileAsync(Entry, Entry.LatestRelease.Id, new Progress<StreamProgress>(), cancellationToken); EntryInstallResult result = await _installationHandler.InstallAsync(Entry, Entry.LatestRelease.Id, new Progress<StreamProgress>(), cancellationToken);
if (result.IsSuccess) if (result.IsSuccess)
_notificationService.CreateNotification().WithTitle("Profile installed").WithSeverity(NotificationSeverity.Success).Show(); _notificationService.CreateNotification().WithTitle("Profile installed").WithSeverity(NotificationSeverity.Success).Show();
else else

View File

@ -8,8 +8,8 @@ using Artemis.UI.Shared.Services;
using Artemis.UI.Shared.Utilities; using Artemis.UI.Shared.Utilities;
using Artemis.WebClient.Workshop; using Artemis.WebClient.Workshop;
using Artemis.WebClient.Workshop.Exceptions; using Artemis.WebClient.Workshop.Exceptions;
using Artemis.WebClient.Workshop.Handlers.UploadHandlers;
using Artemis.WebClient.Workshop.Services; using Artemis.WebClient.Workshop.Services;
using Artemis.WebClient.Workshop.UploadHandlers;
using ReactiveUI; using ReactiveUI;
using StrawberryShake; using StrawberryShake;

View File

@ -48,4 +48,8 @@
<Generator>MSBuild:GenerateGraphQLCode</Generator> <Generator>MSBuild:GenerateGraphQLCode</Generator>
</GraphQL> </GraphQL>
</ItemGroup> </ItemGroup>
<ItemGroup>
<Folder Include="Handlers\" />
</ItemGroup>
</Project> </Project>

View File

@ -1,28 +0,0 @@
using Artemis.Web.Workshop.Entities;
namespace Artemis.WebClient.Workshop.DownloadHandlers;
public class EntryInstallResult<T>
{
public bool IsSuccess { get; set; }
public string? Message { get; set; }
public T? Result { get; set; }
public static EntryInstallResult<T> FromFailure(string? message)
{
return new EntryInstallResult<T>
{
IsSuccess = false,
Message = message
};
}
public static EntryInstallResult<T> FromSuccess(T installationResult)
{
return new EntryInstallResult<T>
{
IsSuccess = true,
Result = installationResult
};
}
}

View File

@ -1,7 +0,0 @@
using Artemis.UI.Shared.Utilities;
namespace Artemis.WebClient.Workshop.DownloadHandlers;
public interface IEntryInstallationHandler
{
}

View File

@ -1,10 +1,10 @@
using System.Reflection; using System.Reflection;
using Artemis.WebClient.Workshop.DownloadHandlers;
using Artemis.WebClient.Workshop.Extensions; using Artemis.WebClient.Workshop.Extensions;
using Artemis.WebClient.Workshop.Handlers.InstallationHandlers;
using Artemis.WebClient.Workshop.Handlers.UploadHandlers;
using Artemis.WebClient.Workshop.Repositories; using Artemis.WebClient.Workshop.Repositories;
using Artemis.WebClient.Workshop.Services; using Artemis.WebClient.Workshop.Services;
using Artemis.WebClient.Workshop.State; using Artemis.WebClient.Workshop.State;
using Artemis.WebClient.Workshop.UploadHandlers;
using DryIoc; using DryIoc;
using DryIoc.Microsoft.DependencyInjection; using DryIoc.Microsoft.DependencyInjection;
using IdentityModel.Client; using IdentityModel.Client;

View File

@ -0,0 +1,26 @@
namespace Artemis.WebClient.Workshop.Handlers.InstallationHandlers;
public class EntryInstallResult
{
public bool IsSuccess { get; set; }
public string? Message { get; set; }
public object? Result { get; set; }
public static EntryInstallResult FromFailure(string? message)
{
return new EntryInstallResult
{
IsSuccess = false,
Message = message
};
}
public static EntryInstallResult FromSuccess(object installationResult)
{
return new EntryInstallResult
{
IsSuccess = true,
Result = installationResult
};
}
}

View File

@ -0,0 +1,23 @@
using Artemis.WebClient.Workshop.Handlers.InstallationHandlers.Implementations;
using DryIoc;
namespace Artemis.WebClient.Workshop.Handlers.InstallationHandlers;
public class EntryInstallationHandlerFactory
{
private readonly IContainer _container;
public EntryInstallationHandlerFactory(IContainer container)
{
_container = container;
}
public IEntryInstallationHandler CreateHandler(EntryType entryType)
{
return entryType switch
{
EntryType.Profile => _container.Resolve<ProfileEntryInstallationHandler>(),
_ => throw new NotSupportedException($"EntryType '{entryType}' is not supported.")
};
}
}

View File

@ -0,0 +1,24 @@
namespace Artemis.WebClient.Workshop.Handlers.InstallationHandlers;
public class EntryUninstallResult
{
public bool IsSuccess { get; set; }
public string? Message { get; set; }
public static EntryUninstallResult FromFailure(string? message)
{
return new EntryUninstallResult
{
IsSuccess = false,
Message = message
};
}
public static EntryUninstallResult FromSuccess()
{
return new EntryUninstallResult
{
IsSuccess = true
};
}
}

View File

@ -0,0 +1,10 @@
using Artemis.UI.Shared.Utilities;
using Artemis.WebClient.Workshop.Services;
namespace Artemis.WebClient.Workshop.Handlers.InstallationHandlers;
public interface IEntryInstallationHandler
{
Task<EntryInstallResult> InstallAsync(IGetEntryById_Entry entry, Guid releaseId, Progress<StreamProgress> progress, CancellationToken cancellationToken);
Task<EntryUninstallResult> UninstallAsync(InstalledEntry installedEntry, CancellationToken cancellationToken);
}

View File

@ -4,7 +4,7 @@ using Artemis.UI.Shared.Extensions;
using Artemis.UI.Shared.Utilities; using Artemis.UI.Shared.Utilities;
using Artemis.WebClient.Workshop.Services; using Artemis.WebClient.Workshop.Services;
namespace Artemis.WebClient.Workshop.DownloadHandlers.Implementations; namespace Artemis.WebClient.Workshop.Handlers.InstallationHandlers.Implementations;
public class ProfileEntryInstallationHandler : IEntryInstallationHandler public class ProfileEntryInstallationHandler : IEntryInstallationHandler
{ {
@ -19,7 +19,7 @@ public class ProfileEntryInstallationHandler : IEntryInstallationHandler
_workshopService = workshopService; _workshopService = workshopService;
} }
public async Task<EntryInstallResult<ProfileConfiguration>> InstallProfileAsync(IGetEntryById_Entry entry, Guid releaseId, Progress<StreamProgress> progress, CancellationToken cancellationToken) public async Task<EntryInstallResult> InstallAsync(IGetEntryById_Entry entry, Guid releaseId, Progress<StreamProgress> progress, CancellationToken cancellationToken)
{ {
using MemoryStream stream = new(); using MemoryStream stream = new();
@ -31,7 +31,7 @@ public class ProfileEntryInstallationHandler : IEntryInstallationHandler
} }
catch (Exception e) catch (Exception e)
{ {
return EntryInstallResult<ProfileConfiguration>.FromFailure(e.Message); return EntryInstallResult.FromFailure(e.Message);
} }
// Find existing installation to potentially replace the profile // Find existing installation to potentially replace the profile
@ -46,7 +46,7 @@ public class ProfileEntryInstallationHandler : IEntryInstallationHandler
// Update the release and return the profile configuration // Update the release and return the profile configuration
UpdateRelease(releaseId, installedEntry); UpdateRelease(releaseId, installedEntry);
return EntryInstallResult<ProfileConfiguration>.FromSuccess(overwritten); return EntryInstallResult.FromSuccess(overwritten);
} }
} }
@ -60,7 +60,33 @@ public class ProfileEntryInstallationHandler : IEntryInstallationHandler
// Update the release and return the profile configuration // Update the release and return the profile configuration
UpdateRelease(releaseId, installedEntry); UpdateRelease(releaseId, installedEntry);
return EntryInstallResult<ProfileConfiguration>.FromSuccess(imported); return EntryInstallResult.FromSuccess(imported);
}
public async Task<EntryUninstallResult> UninstallAsync(InstalledEntry installedEntry, CancellationToken cancellationToken)
{
if (!Guid.TryParse(installedEntry.LocalReference, out Guid profileId))
return EntryUninstallResult.FromFailure("Local reference does not contain a GUID");
return await Task.Run(() =>
{
try
{
// Find the profile if still there
ProfileConfiguration? profile = _profileService.ProfileConfigurations.FirstOrDefault(c => c.ProfileId == profileId);
if (profile != null)
_profileService.DeleteProfile(profile);
// Remove the release
_workshopService.RemoveInstalledEntry(installedEntry);
}
catch (Exception e)
{
return EntryUninstallResult.FromFailure(e.Message);
}
return EntryUninstallResult.FromSuccess();
}, cancellationToken);
} }
private void UpdateRelease(Guid releaseId, InstalledEntry installedEntry) private void UpdateRelease(Guid releaseId, InstalledEntry installedEntry)

View File

@ -1,7 +1,7 @@
using Artemis.WebClient.Workshop.UploadHandlers.Implementations; using Artemis.WebClient.Workshop.Handlers.UploadHandlers.Implementations;
using DryIoc; using DryIoc;
namespace Artemis.WebClient.Workshop.UploadHandlers; namespace Artemis.WebClient.Workshop.Handlers.UploadHandlers;
public class EntryUploadHandlerFactory public class EntryUploadHandlerFactory
{ {

View File

@ -1,6 +1,6 @@
using Artemis.Web.Workshop.Entities; using Artemis.Web.Workshop.Entities;
namespace Artemis.WebClient.Workshop.UploadHandlers; namespace Artemis.WebClient.Workshop.Handlers.UploadHandlers;
public class EntryUploadResult public class EntryUploadResult
{ {

View File

@ -1,6 +1,6 @@
using Artemis.UI.Shared.Utilities; using Artemis.UI.Shared.Utilities;
namespace Artemis.WebClient.Workshop.UploadHandlers; namespace Artemis.WebClient.Workshop.Handlers.UploadHandlers;
public interface IEntryUploadHandler public interface IEntryUploadHandler
{ {

View File

@ -1,4 +1,4 @@
namespace Artemis.WebClient.Workshop.UploadHandlers; namespace Artemis.WebClient.Workshop.Handlers.UploadHandlers;
public class ImageUploadResult public class ImageUploadResult
{ {

View File

@ -1,6 +1,6 @@
using Artemis.UI.Shared.Utilities; using Artemis.UI.Shared.Utilities;
namespace Artemis.WebClient.Workshop.UploadHandlers.Implementations; namespace Artemis.WebClient.Workshop.Handlers.UploadHandlers.Implementations;
public class LayoutEntryUploadHandler : IEntryUploadHandler public class LayoutEntryUploadHandler : IEntryUploadHandler
{ {

View File

@ -1,12 +1,11 @@
using System.Net.Http.Headers; using System.Net.Http.Headers;
using Artemis.Core; using Artemis.Core;
using Artemis.Core.Services; using Artemis.Core.Services;
using Artemis.Storage.Repositories.Interfaces;
using Artemis.UI.Shared.Utilities; using Artemis.UI.Shared.Utilities;
using Artemis.Web.Workshop.Entities; using Artemis.Web.Workshop.Entities;
using Newtonsoft.Json; using Newtonsoft.Json;
namespace Artemis.WebClient.Workshop.UploadHandlers.Implementations; namespace Artemis.WebClient.Workshop.Handlers.UploadHandlers.Implementations;
public class ProfileEntryUploadHandler : IEntryUploadHandler public class ProfileEntryUploadHandler : IEntryUploadHandler
{ {

View File

@ -20,7 +20,6 @@ public class InstalledEntry
Author = entry.Author; Author = entry.Author;
Name = entry.Name; Name = entry.Name;
Summary = entry.Summary;
} }
public Guid EntryId { get; set; } public Guid EntryId { get; set; }
@ -28,7 +27,6 @@ public class InstalledEntry
public string Author { get; set; } = string.Empty; public string Author { get; set; } = string.Empty;
public string Name { get; set; } = string.Empty; public string Name { get; set; } = string.Empty;
public string Summary { get; set; } = string.Empty;
public Guid ReleaseId { get; set; } public Guid ReleaseId { get; set; }
public string ReleaseVersion { get; set; } = string.Empty; public string ReleaseVersion { get; set; } = string.Empty;
@ -45,7 +43,6 @@ public class InstalledEntry
Author = Entity.Author; Author = Entity.Author;
Name = Entity.Name; Name = Entity.Name;
Summary = Entity.Summary;
ReleaseId = Entity.ReleaseId; ReleaseId = Entity.ReleaseId;
ReleaseVersion = Entity.ReleaseVersion; ReleaseVersion = Entity.ReleaseVersion;
@ -61,7 +58,6 @@ public class InstalledEntry
Entity.Author = Author; Entity.Author = Author;
Entity.Name = Name; Entity.Name = Name;
Entity.Summary = Summary;
Entity.ReleaseId = ReleaseId; Entity.ReleaseId = ReleaseId;
Entity.ReleaseVersion = ReleaseVersion; Entity.ReleaseVersion = ReleaseVersion;

View File

@ -1,5 +1,5 @@
using Artemis.UI.Shared.Utilities; using Artemis.UI.Shared.Utilities;
using Artemis.WebClient.Workshop.UploadHandlers; using Artemis.WebClient.Workshop.Handlers.UploadHandlers;
namespace Artemis.WebClient.Workshop.Services; namespace Artemis.WebClient.Workshop.Services;
@ -14,7 +14,9 @@ public interface IWorkshopService
List<InstalledEntry> GetInstalledEntries(); List<InstalledEntry> GetInstalledEntries();
InstalledEntry? GetInstalledEntry(IGetEntryById_Entry entry); InstalledEntry? GetInstalledEntry(IGetEntryById_Entry entry);
InstalledEntry CreateInstalledEntry(IGetEntryById_Entry entry); InstalledEntry CreateInstalledEntry(IGetEntryById_Entry entry);
void RemoveInstalledEntry(InstalledEntry installedEntry);
void SaveInstalledEntry(InstalledEntry entry); void SaveInstalledEntry(InstalledEntry entry);
public record WorkshopStatus(bool IsReachable, string Message); public record WorkshopStatus(bool IsReachable, string Message);
} }

View File

@ -3,7 +3,7 @@ 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.UI.Shared.Utilities; using Artemis.UI.Shared.Utilities;
using Artemis.WebClient.Workshop.UploadHandlers; using Artemis.WebClient.Workshop.Handlers.UploadHandlers;
namespace Artemis.WebClient.Workshop.Services; namespace Artemis.WebClient.Workshop.Services;
@ -124,6 +124,12 @@ public class WorkshopService : IWorkshopService
return new InstalledEntry(entry); return new InstalledEntry(entry);
} }
/// <inheritdoc />
public void RemoveInstalledEntry(InstalledEntry installedEntry)
{
_entryRepository.Remove(installedEntry.Entity);
}
/// <inheritdoc /> /// <inheritdoc />
public void SaveInstalledEntry(InstalledEntry entry) public void SaveInstalledEntry(InstalledEntry entry)
{ {