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

Workshop - Improve child navigation performance

This commit is contained in:
Robert 2024-04-12 22:53:40 +02:00
parent cac44d748d
commit 62057d657a
27 changed files with 309 additions and 212 deletions

View File

@ -9,5 +9,6 @@ internal interface IRoutableHostScreen : IRoutableScreen
{ {
bool RecycleScreen { get; } bool RecycleScreen { get; }
IRoutableScreen? InternalScreen { get; } IRoutableScreen? InternalScreen { get; }
IRoutableScreen? InternalDefaultScreen { get; }
void InternalChangeScreen(IRoutableScreen? screen); void InternalChangeScreen(IRoutableScreen? screen);
} }

View File

@ -25,7 +25,13 @@ public abstract class RoutableHostScreen<TScreen> : RoutableScreen, IRoutableHos
protected set => RaiseAndSetIfChanged(ref _recycleScreen, value); protected set => RaiseAndSetIfChanged(ref _recycleScreen, value);
} }
/// <summary>
/// Gets the screen to show when no other screen is active.
/// </summary>
public virtual TScreen? DefaultScreen { get; }
IRoutableScreen? IRoutableHostScreen.InternalScreen => Screen; IRoutableScreen? IRoutableHostScreen.InternalScreen => Screen;
IRoutableScreen? IRoutableHostScreen.InternalDefaultScreen => DefaultScreen;
void IRoutableHostScreen.InternalChangeScreen(IRoutableScreen? screen) void IRoutableHostScreen.InternalChangeScreen(IRoutableScreen? screen)
{ {

View File

@ -27,7 +27,13 @@ public abstract class RoutableHostScreen<TScreen, TParam> : RoutableScreen<TPara
protected set => RaiseAndSetIfChanged(ref _recycleScreen, value); protected set => RaiseAndSetIfChanged(ref _recycleScreen, value);
} }
/// <summary>
/// Gets the screen to show when no other screen is active.
/// </summary>
public virtual TScreen? DefaultScreen { get; }
IRoutableScreen? IRoutableHostScreen.InternalScreen => Screen; IRoutableScreen? IRoutableHostScreen.InternalScreen => Screen;
IRoutableScreen? IRoutableHostScreen.InternalDefaultScreen => DefaultScreen;
void IRoutableHostScreen.InternalChangeScreen(IRoutableScreen? screen) void IRoutableHostScreen.InternalChangeScreen(IRoutableScreen? screen)
{ {

View File

@ -109,12 +109,11 @@ internal class Navigation
// Navigate the child too // Navigate the child too
if (resolution.Child != null) if (resolution.Child != null)
await NavigateResolution(resolution.Child, args, childScreen); await NavigateResolution(resolution.Child, args, childScreen);
// Make sure there is no child // Without a resolution, navigate to the default screen (which may be null)
else if (childScreen.InternalScreen != null) else if (childScreen.InternalScreen != childScreen.InternalDefaultScreen)
childScreen.InternalChangeScreen(null); childScreen.InternalChangeScreen(childScreen.InternalDefaultScreen);
} }
Completed = true; Completed = true;
} }

View File

@ -8,6 +8,7 @@
</Grid.RowDefinitions> </Grid.RowDefinitions>
<Grid Margin="20" Grid.Column="0"> <Grid Margin="20" Grid.Column="0">
<StackPanel> <StackPanel>
<TextBlock Theme="{StaticResource TitleTextBlockStyle}">TitleTextBlockStyle</TextBlock>
<TextBlock Classes="h1">This is heading 1</TextBlock> <TextBlock Classes="h1">This is heading 1</TextBlock>
<TextBlock Classes="h2">This is heading 2</TextBlock> <TextBlock Classes="h2">This is heading 2</TextBlock>
<TextBlock Classes="h3">This is heading 3</TextBlock> <TextBlock Classes="h3">This is heading 3</TextBlock>
@ -22,6 +23,7 @@
<Grid Margin="20" Grid.Column="1"> <Grid Margin="20" Grid.Column="1">
<StackPanel> <StackPanel>
<Border Width="400" Classes="skeleton-text title"></Border>
<Border Width="400" Classes="skeleton-text h1"></Border> <Border Width="400" Classes="skeleton-text h1"></Border>
<Border Width="400" Classes="skeleton-text h2"></Border> <Border Width="400" Classes="skeleton-text h2"></Border>
<Border Width="400" Classes="skeleton-text h3"></Border> <Border Width="400" Classes="skeleton-text h3"></Border>
@ -39,6 +41,7 @@
<Setter Property="Background" Value="#55ff0000"></Setter> <Setter Property="Background" Value="#55ff0000"></Setter>
</Style> </Style>
</StackPanel.Styles> </StackPanel.Styles>
<TextBlock Theme="{StaticResource TitleTextBlockStyle}">TitleTextBlockStyle</TextBlock>
<TextBlock Classes="h1">This is heading 1</TextBlock> <TextBlock Classes="h1">This is heading 1</TextBlock>
<TextBlock Classes="h2">This is heading 2</TextBlock> <TextBlock Classes="h2">This is heading 2</TextBlock>
<TextBlock Classes="h3">This is heading 3</TextBlock> <TextBlock Classes="h3">This is heading 3</TextBlock>
@ -51,6 +54,7 @@
<Grid Margin="20" Grid.Column="0" Row="1"> <Grid Margin="20" Grid.Column="0" Row="1">
<StackPanel Spacing="2"> <StackPanel Spacing="2">
<Border Width="400" Classes="skeleton-text title no-margin"></Border>
<Border Width="400" Classes="skeleton-text h1 no-margin"></Border> <Border Width="400" Classes="skeleton-text h1 no-margin"></Border>
<Border Width="400" Classes="skeleton-text h2 no-margin"></Border> <Border Width="400" Classes="skeleton-text h2 no-margin"></Border>
<Border Width="400" Classes="skeleton-text h3 no-margin"></Border> <Border Width="400" Classes="skeleton-text h3 no-margin"></Border>
@ -68,6 +72,7 @@
<Setter Property="Background" Value="#55ff0000"></Setter> <Setter Property="Background" Value="#55ff0000"></Setter>
</Style> </Style>
</StackPanel.Styles> </StackPanel.Styles>
<TextBlock Theme="{StaticResource TitleTextBlockStyle}">TitleTextBlockStyle</TextBlock>
<TextBlock Classes="h1 no-margin">This is heading 1</TextBlock> <TextBlock Classes="h1 no-margin">This is heading 1</TextBlock>
<TextBlock Classes="h2 no-margin">This is heading 2</TextBlock> <TextBlock Classes="h2 no-margin">This is heading 2</TextBlock>
<TextBlock Classes="h3 no-margin">This is heading 3</TextBlock> <TextBlock Classes="h3 no-margin">This is heading 3</TextBlock>
@ -125,6 +130,11 @@
</Style.Animations> </Style.Animations>
</Style> </Style>
<Style Selector="Border.skeleton-text.title">
<Setter Property="Height" Value="28" />
<Setter Property="Margin" Value="0 5 0 5" />
<Setter Property="CornerRadius" Value="8" />
</Style>
<Style Selector="Border.skeleton-text.h1"> <Style Selector="Border.skeleton-text.h1">
<Setter Property="Height" Value="65" /> <Setter Property="Height" Value="65" />
<Setter Property="Margin" Value="0 10 0 20" /> <Setter Property="Margin" Value="0 10 0 20" />

View File

@ -13,74 +13,90 @@
<converters:EntryIconUriConverter x:Key="EntryIconUriConverter" /> <converters:EntryIconUriConverter x:Key="EntryIconUriConverter" />
<converters:DateTimeConverter x:Key="DateTimeConverter" /> <converters:DateTimeConverter x:Key="DateTimeConverter" />
</UserControl.Resources> </UserControl.Resources>
<StackPanel> <Panel>
<Panel> <StackPanel IsVisible="{CompiledBinding Entry, Converter={x:Static ObjectConverters.IsNull}}">
<Border CornerRadius="6" <Border Classes="skeleton-text" Margin="0 0 10 0" Width="80" Height="80"></Border>
HorizontalAlignment="Left" <Border Classes="skeleton-text title" HorizontalAlignment="Stretch"/>
Margin="0 0 10 0" <Border Classes="skeleton-text" Width="120"/>
Width="80" <Border Classes="skeleton-text" Width="140" Margin="0 8"/>
Height="80" <Border Classes="skeleton-text" Width="80"/>
ClipToBounds="True"> <Border Classes="card-separator" Margin="0 15 0 17"></Border>
<Image Stretch="UniformToFill" il:ImageLoader.Source="{CompiledBinding Entry.Id, Converter={StaticResource EntryIconUriConverter}, Mode=OneWay}" /> <Border Classes="skeleton-text" Width="120"/>
</Border> <StackPanel Margin="0 10 0 0">
<Button Classes="icon-button" <Border Classes="skeleton-text" Width="160"/>
VerticalAlignment="Top" <Border Classes="skeleton-text" Width="160"/>
HorizontalAlignment="Right" </StackPanel>
Command="{CompiledBinding CopyShareLink}" <Border Classes="skeleton-button"></Border>
ToolTip.Tip="Copy share link"> </StackPanel>
<avalonia:MaterialIcon Kind="ShareVariant" /> <StackPanel IsVisible="{CompiledBinding Entry, Converter={x:Static ObjectConverters.IsNotNull}}">
<Panel>
<Border CornerRadius="6"
HorizontalAlignment="Left"
Margin="0 0 10 0"
Width="80"
Height="80"
ClipToBounds="True">
<Image Stretch="UniformToFill" il:ImageLoader.Source="{CompiledBinding Entry.Id, Converter={StaticResource EntryIconUriConverter}, Mode=OneWay}" />
</Border>
<Button Classes="icon-button"
VerticalAlignment="Top"
HorizontalAlignment="Right"
Command="{CompiledBinding CopyShareLink}"
ToolTip.Tip="Copy share link">
<avalonia:MaterialIcon Kind="ShareVariant" />
</Button>
</Panel>
<TextBlock Theme="{StaticResource TitleTextBlockStyle}"
MaxLines="3"
TextTrimming="CharacterEllipsis"
Text="{CompiledBinding Entry.Name, FallbackValue=Title}"/>
<TextBlock Classes="subtitle" TextTrimming="CharacterEllipsis" Text="{CompiledBinding Entry.Author, FallbackValue=Author}" />
<TextBlock Margin="0 8" TextWrapping="Wrap" Text="{CompiledBinding Entry.Summary, FallbackValue=Summary}" />
<!-- Categories -->
<ItemsControl ItemsSource="{CompiledBinding Entry.Categories}" Margin="0 0 -8 0">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel Orientation="Horizontal"></WrapPanel>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal" Margin="0 0 8 0">
<avalonia:MaterialIcon Kind="{CompiledBinding Icon}" Margin="0 0 3 0"></avalonia:MaterialIcon>
<TextBlock Text="{CompiledBinding Name}" TextTrimming="CharacterEllipsis" />
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<Border Classes="card-separator"></Border>
<TextBlock Margin="0 0 0 8">
<avalonia:MaterialIcon Kind="Downloads" />
<Run Classes="h5" Text="{CompiledBinding Entry.Downloads, FallbackValue=0}" />
<Run>downloads</Run>
</TextBlock>
<TextBlock Classes="subtitle"
ToolTip.Tip="{CompiledBinding Entry.CreatedAt, Converter={StaticResource DateTimeConverter}}">
<avalonia:MaterialIcon Kind="Calendar" />
<Run>Created</Run>
<Run Text="{CompiledBinding Entry.CreatedAt, Converter={StaticResource DateTimeConverter}, ConverterParameter='humanize'}"></Run>
</TextBlock>
<TextBlock Classes="subtitle"
ToolTip.Tip="{CompiledBinding UpdatedAt, Converter={StaticResource DateTimeConverter}}">
<avalonia:MaterialIcon Kind="Update" />
<Run>Updated</Run>
<Run Text="{CompiledBinding UpdatedAt, Converter={StaticResource DateTimeConverter}, ConverterParameter='humanize'}"></Run>
</TextBlock>
<Button IsVisible="{CompiledBinding CanBeManaged}" Command="{CompiledBinding GoToManage}" Margin="0 10 0 0" HorizontalAlignment="Stretch">
Manage installation
</Button> </Button>
</Panel> </StackPanel>
</Panel>
<TextBlock Theme="{StaticResource TitleTextBlockStyle}"
MaxLines="3"
TextTrimming="CharacterEllipsis"
Text="{CompiledBinding Entry.Name, FallbackValue=Title }" />
<TextBlock Classes="subtitle" TextTrimming="CharacterEllipsis" Text="{CompiledBinding Entry.Author, FallbackValue=Author}" />
<TextBlock Margin="0 8" TextWrapping="Wrap" Text="{CompiledBinding Entry.Summary, FallbackValue=Summary}" />
<!-- Categories -->
<ItemsControl ItemsSource="{CompiledBinding Entry.Categories}" Margin="0 0 -8 0">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel Orientation="Horizontal"></WrapPanel>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal" Margin="0 0 8 0">
<avalonia:MaterialIcon Kind="{CompiledBinding Icon}" Margin="0 0 3 0"></avalonia:MaterialIcon>
<TextBlock Text="{CompiledBinding Name}" TextTrimming="CharacterEllipsis" />
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<Border Classes="card-separator"></Border>
<TextBlock Margin="0 0 0 8">
<avalonia:MaterialIcon Kind="Downloads" />
<Run Classes="h5" Text="{CompiledBinding Entry.Downloads, FallbackValue=0}" />
<Run>downloads</Run>
</TextBlock>
<TextBlock Classes="subtitle"
ToolTip.Tip="{CompiledBinding Entry.CreatedAt, Converter={StaticResource DateTimeConverter}}">
<avalonia:MaterialIcon Kind="Calendar" />
<Run>Created</Run>
<Run Text="{CompiledBinding Entry.CreatedAt, Converter={StaticResource DateTimeConverter}, ConverterParameter='humanize'}"></Run>
</TextBlock>
<TextBlock Classes="subtitle"
ToolTip.Tip="{CompiledBinding UpdatedAt, Converter={StaticResource DateTimeConverter}}">
<avalonia:MaterialIcon Kind="Update" />
<Run>Updated</Run>
<Run Text="{CompiledBinding UpdatedAt, Converter={StaticResource DateTimeConverter}, ConverterParameter='humanize'}"></Run>
</TextBlock>
<Button IsVisible="{CompiledBinding CanBeManaged}" Command="{CompiledBinding GoToManage}" Margin="0 10 0 0" HorizontalAlignment="Stretch">
Manage installation
</Button>
</StackPanel>
</UserControl> </UserControl>

View File

@ -8,6 +8,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.WebClient.Workshop; using Artemis.WebClient.Workshop;
using Artemis.WebClient.Workshop.Extensions;
using Artemis.WebClient.Workshop.Models; using Artemis.WebClient.Workshop.Models;
using Artemis.WebClient.Workshop.Services; using Artemis.WebClient.Workshop.Services;
using PropertyChanged.SourceGenerator; using PropertyChanged.SourceGenerator;
@ -19,36 +20,47 @@ public partial class EntryInfoViewModel : ActivatableViewModelBase
{ {
private readonly IRouter _router; private readonly IRouter _router;
private readonly INotificationService _notificationService; private readonly INotificationService _notificationService;
private readonly IWorkshopService _workshopService;
[Notify] private IEntryDetails? _entry;
[Notify] private DateTimeOffset? _updatedAt;
[Notify] private bool _canBeManaged; [Notify] private bool _canBeManaged;
public EntryInfoViewModel(IEntryDetails entry, IRouter router, INotificationService notificationService, IWorkshopService workshopService) public EntryInfoViewModel(IRouter router, INotificationService notificationService, IWorkshopService workshopService)
{ {
_router = router; _router = router;
_notificationService = notificationService; _notificationService = notificationService;
Entry = entry; _workshopService = workshopService;
UpdatedAt = Entry.Releases.Any() ? Entry.Releases.Max(r => r.CreatedAt) : Entry.CreatedAt;
CanBeManaged = Entry.EntryType != EntryType.Profile && workshopService.GetInstalledEntry(entry.Id) != null;
this.WhenActivated(d => this.WhenActivated(d =>
{ {
Observable.FromEventPattern<InstalledEntry>(x => workshopService.OnInstalledEntrySaved += x, x => workshopService.OnInstalledEntrySaved -= x) Observable.FromEventPattern<InstalledEntry>(x => workshopService.OnInstalledEntrySaved += x, x => workshopService.OnInstalledEntrySaved -= x)
.StartWith([]) .StartWith([])
.Subscribe(_ => CanBeManaged = Entry.EntryType != EntryType.Profile && workshopService.GetInstalledEntry(entry.Id) != null) .Subscribe(_ => CanBeManaged = Entry != null && Entry.EntryType != EntryType.Profile && workshopService.GetInstalledEntry(Entry.Id) != null)
.DisposeWith(d); .DisposeWith(d);
}); });
} }
public IEntryDetails Entry { get; } public void SetEntry(IEntryDetails? entry)
public DateTimeOffset? UpdatedAt { get; } {
Entry = entry;
UpdatedAt = Entry != null && Entry.Releases.Any() ? Entry.Releases.Max(r => r.CreatedAt) : Entry?.CreatedAt;
CanBeManaged = Entry != null && Entry.EntryType != EntryType.Profile && _workshopService.GetInstalledEntry(Entry.Id) != null;
}
public async Task CopyShareLink() public async Task CopyShareLink()
{ {
if (Entry == null)
return;
await Shared.UI.Clipboard.SetTextAsync($"{WorkshopConstants.WORKSHOP_URL}/entries/{Entry.Id}/{StringUtilities.UrlFriendly(Entry.Name)}"); await Shared.UI.Clipboard.SetTextAsync($"{WorkshopConstants.WORKSHOP_URL}/entries/{Entry.Id}/{StringUtilities.UrlFriendly(Entry.Name)}");
_notificationService.CreateNotification().WithTitle("Copied share link to clipboard.").Show(); _notificationService.CreateNotification().WithTitle("Copied share link to clipboard.").Show();
} }
public async Task GoToManage() public async Task GoToManage()
{ {
await _router.Navigate("/manage"); if (Entry == null)
return;
await _router.Navigate($"{Entry.GetEntryPath()}/manage");
} }
} }

View File

@ -6,6 +6,7 @@ using System.Threading.Tasks;
using Artemis.UI.Shared; using Artemis.UI.Shared;
using Artemis.UI.Shared.Routing; using Artemis.UI.Shared.Routing;
using Artemis.WebClient.Workshop; using Artemis.WebClient.Workshop;
using Artemis.WebClient.Workshop.Extensions;
using PropertyChanged.SourceGenerator; using PropertyChanged.SourceGenerator;
using ReactiveUI; using ReactiveUI;
@ -25,14 +26,15 @@ public partial class EntryReleasesViewModel : ActivatableViewModelBase
this.WhenActivated(d => this.WhenActivated(d =>
{ {
router.CurrentPath.Subscribe(p => SelectedRelease = p != null && p.Contains("releases") && float.TryParse(p.Split('/').Last(), out float releaseId) router.CurrentPath.Subscribe(p =>
? Releases.FirstOrDefault(r => r.Release.Id == releaseId) SelectedRelease = p != null && p.StartsWith(Entry.GetEntryPath()) && float.TryParse(p.Split('/').Last(), out float releaseId)
: null) ? Releases.FirstOrDefault(r => r.Release.Id == releaseId)
: null)
.DisposeWith(d); .DisposeWith(d);
this.WhenAnyValue(vm => vm.SelectedRelease) this.WhenAnyValue(vm => vm.SelectedRelease)
.WhereNotNull() .WhereNotNull()
.Subscribe(s => _router.Navigate($"/releases/{s.Release.Id}")) .Subscribe(s => _router.Navigate($"{Entry.GetEntryPath()}/releases/{s.Release.Id}"))
.DisposeWith(d); .DisposeWith(d);
}); });
} }

View File

@ -3,7 +3,6 @@
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:layout="clr-namespace:Artemis.UI.Screens.Workshop.Layout" xmlns:layout="clr-namespace:Artemis.UI.Screens.Workshop.Layout"
xmlns:mdxaml="https://github.com/whistyun/Markdown.Avalonia.Tight"
xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia" xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
xmlns:ui="clr-namespace:Artemis.UI" xmlns:ui="clr-namespace:Artemis.UI"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="800" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="800"
@ -14,7 +13,7 @@
<Border Classes="card" VerticalAlignment="Top"> <Border Classes="card" VerticalAlignment="Top">
<ContentControl Content="{CompiledBinding EntryInfoViewModel}" /> <ContentControl Content="{CompiledBinding EntryInfoViewModel}" />
</Border> </Border>
<Border Classes="card" VerticalAlignment="Top" IsVisible="{CompiledBinding Entry.Releases.Count}"> <Border Classes="card" VerticalAlignment="Top" IsVisible="{CompiledBinding Entry.Releases.Count, FallbackValue=False}">
<ContentControl Content="{CompiledBinding EntryReleasesViewModel}" /> <ContentControl Content="{CompiledBinding EntryReleasesViewModel}" />
</Border> </Border>
</StackPanel> </StackPanel>

View File

@ -11,7 +11,8 @@ public partial class LayoutDetailsView : ReactiveUserControl<LayoutDetailsViewMo
{ {
InitializeComponent(); InitializeComponent();
this.WhenActivated(d => ViewModel.WhenAnyValue(vm => vm.Screen) this.WhenActivated(d => ViewModel.WhenAnyValue(vm => vm.Screen)
.Subscribe(screen => RouterFrame.NavigateFromObject(screen ?? ViewModel?.LayoutDescriptionViewModel)) .WhereNotNull()
.Subscribe(screen => RouterFrame.NavigateFromObject(screen))
.DisposeWith(d)); .DisposeWith(d));
} }
} }

View File

@ -14,30 +14,30 @@ namespace Artemis.UI.Screens.Workshop.Layout;
public partial class LayoutDetailsViewModel : RoutableHostScreen<RoutableScreen, WorkshopDetailParameters> public partial class LayoutDetailsViewModel : RoutableHostScreen<RoutableScreen, WorkshopDetailParameters>
{ {
private readonly IWorkshopClient _client; private readonly IWorkshopClient _client;
private readonly Func<IEntryDetails, EntryInfoViewModel> _getEntryInfoViewModel; private readonly LayoutDescriptionViewModel _layoutDescriptionViewModel;
private readonly Func<IEntryDetails, EntryReleasesViewModel> _getEntryReleasesViewModel; private readonly Func<IEntryDetails, EntryReleasesViewModel> _getEntryReleasesViewModel;
private readonly Func<IEntryDetails, EntryImagesViewModel> _getEntryImagesViewModel; private readonly Func<IEntryDetails, EntryImagesViewModel> _getEntryImagesViewModel;
[Notify] private IEntryDetails? _entry; [Notify] private IEntryDetails? _entry;
[Notify] private EntryInfoViewModel? _entryInfoViewModel;
[Notify] private EntryReleasesViewModel? _entryReleasesViewModel; [Notify] private EntryReleasesViewModel? _entryReleasesViewModel;
[Notify] private EntryImagesViewModel? _entryImagesViewModel; [Notify] private EntryImagesViewModel? _entryImagesViewModel;
public LayoutDetailsViewModel(IWorkshopClient client, public LayoutDetailsViewModel(IWorkshopClient client,
LayoutDescriptionViewModel layoutDescriptionViewModel, LayoutDescriptionViewModel layoutDescriptionViewModel,
Func<IEntryDetails, EntryInfoViewModel> getEntryInfoViewModel, EntryInfoViewModel entryInfoViewModel,
Func<IEntryDetails, EntryReleasesViewModel> getEntryReleasesViewModel, Func<IEntryDetails, EntryReleasesViewModel> getEntryReleasesViewModel,
Func<IEntryDetails, EntryImagesViewModel> getEntryImagesViewModel) Func<IEntryDetails, EntryImagesViewModel> getEntryImagesViewModel)
{ {
_client = client; _client = client;
_getEntryInfoViewModel = getEntryInfoViewModel; _layoutDescriptionViewModel = layoutDescriptionViewModel;
_getEntryReleasesViewModel = getEntryReleasesViewModel; _getEntryReleasesViewModel = getEntryReleasesViewModel;
_getEntryImagesViewModel = getEntryImagesViewModel; _getEntryImagesViewModel = getEntryImagesViewModel;
LayoutDescriptionViewModel = layoutDescriptionViewModel;
RecycleScreen = false; RecycleScreen = false;
EntryInfoViewModel = entryInfoViewModel;
} }
public LayoutDescriptionViewModel LayoutDescriptionViewModel { get; } public override RoutableScreen DefaultScreen => _layoutDescriptionViewModel;
public EntryInfoViewModel EntryInfoViewModel { get; }
public override async Task OnNavigating(WorkshopDetailParameters parameters, NavigationArguments args, CancellationToken cancellationToken) public override async Task OnNavigating(WorkshopDetailParameters parameters, NavigationArguments args, CancellationToken cancellationToken)
{ {
@ -47,14 +47,18 @@ public partial class LayoutDetailsViewModel : RoutableHostScreen<RoutableScreen,
private async Task GetEntry(long entryId, CancellationToken cancellationToken) private async Task GetEntry(long entryId, CancellationToken cancellationToken)
{ {
Task grace = Task.Delay(300, cancellationToken);
IOperationResult<IGetEntryByIdResult> result = await _client.GetEntryById.ExecuteAsync(entryId, cancellationToken); IOperationResult<IGetEntryByIdResult> result = await _client.GetEntryById.ExecuteAsync(entryId, cancellationToken);
if (result.IsErrorResult()) if (result.IsErrorResult())
return; return;
// Let the UI settle to avoid lag when deep linking
await grace;
Entry = result.Data?.Entry; Entry = result.Data?.Entry;
EntryInfoViewModel = Entry != null ? _getEntryInfoViewModel(Entry) : null; EntryInfoViewModel.SetEntry(Entry);
EntryReleasesViewModel = Entry != null ? _getEntryReleasesViewModel(Entry) : null; EntryReleasesViewModel = Entry != null ? _getEntryReleasesViewModel(Entry) : null;
EntryImagesViewModel = Entry != null ? _getEntryImagesViewModel(Entry) : null; EntryImagesViewModel = Entry != null ? _getEntryImagesViewModel(Entry) : null;
LayoutDescriptionViewModel.Entry = Entry; _layoutDescriptionViewModel.Entry = Entry;
} }
} }

View File

@ -13,7 +13,8 @@ public partial class LayoutListView : ReactiveUserControl<LayoutListViewModel>
{ {
InitializeComponent(); InitializeComponent();
this.WhenActivated(d => ViewModel.WhenAnyValue(vm => vm.Screen) this.WhenActivated(d => ViewModel.WhenAnyValue(vm => vm.Screen)
.Subscribe(screen => RouterFrame.NavigateFromObject(screen ?? ViewModel?.EntryListViewModel)) .WhereNotNull()
.Subscribe(screen => RouterFrame.NavigateFromObject(screen))
.DisposeWith(d)); .DisposeWith(d));
} }
} }

View File

@ -6,11 +6,12 @@ namespace Artemis.UI.Screens.Workshop.Layout;
public class LayoutListViewModel : RoutableHostScreen<RoutableScreen> public class LayoutListViewModel : RoutableHostScreen<RoutableScreen>
{ {
public EntryListViewModel EntryListViewModel { get; } private readonly EntryListViewModel _entryListViewModel;
public override RoutableScreen DefaultScreen => _entryListViewModel;
public LayoutListViewModel(EntryListViewModel entryListViewModel) public LayoutListViewModel(EntryListViewModel entryListViewModel)
{ {
EntryListViewModel = entryListViewModel; _entryListViewModel = entryListViewModel;
EntryListViewModel.EntryType = EntryType.Layout; _entryListViewModel.EntryType = EntryType.Layout;
} }
} }

View File

@ -13,68 +13,73 @@
<UserControl.Resources> <UserControl.Resources>
<converters:DateTimeConverter x:Key="DateTimeConverter" /> <converters:DateTimeConverter x:Key="DateTimeConverter" />
</UserControl.Resources> </UserControl.Resources>
<Grid ColumnDefinitions="300,*" RowDefinitions="*, Auto"> <Panel>
<StackPanel Grid.Column="0" Grid.Row="0" Spacing="10" Margin="0 0 10 0"> <ProgressBar HorizontalAlignment="Stretch"
<Border Classes="card" VerticalAlignment="Top"> VerticalAlignment="Top"
<StackPanel> IsVisible="{CompiledBinding Entry, Converter={x:Static ObjectConverters.IsNull}}"
<TextBlock Theme="{StaticResource SubtitleTextBlockStyle}">Management</TextBlock> IsIndeterminate="True" />
<Border Classes="card-separator" /> <Grid ColumnDefinitions="300,*" RowDefinitions="*, Auto" IsVisible="{CompiledBinding Entry, Converter={x:Static ObjectConverters.IsNotNull}}">
<StackPanel Grid.Column="0" Grid.Row="0" Spacing="10" Margin="0 0 10 0">
<Border Classes="card" VerticalAlignment="Top">
<StackPanel>
<TextBlock Theme="{StaticResource SubtitleTextBlockStyle}">Management</TextBlock>
<Border Classes="card-separator" />
<TextBlock Margin="0 0 0 8"> <TextBlock Margin="0 0 0 8">
<avalonia:MaterialIcon Kind="Downloads" /> <avalonia:MaterialIcon Kind="Downloads" />
<Run Classes="h5" Text="{CompiledBinding Entry.Downloads, FallbackValue=0}" /> <Run Classes="h5" Text="{CompiledBinding Entry.Downloads, FallbackValue=0}" />
<Run>downloads</Run> <Run>downloads</Run>
</TextBlock> </TextBlock>
<TextBlock Classes="subtitle" ToolTip.Tip="{CompiledBinding Entry.CreatedAt, Converter={StaticResource DateTimeConverter}}"> <TextBlock Classes="subtitle" ToolTip.Tip="{CompiledBinding Entry.CreatedAt, Converter={StaticResource DateTimeConverter}}">
<avalonia:MaterialIcon Kind="Calendar" /> <avalonia:MaterialIcon Kind="Calendar" />
<Run>Created</Run> <Run>Created</Run>
<Run Text="{CompiledBinding Entry.CreatedAt, Converter={StaticResource DateTimeConverter}, ConverterParameter='humanize'}"></Run> <Run Text="{CompiledBinding Entry.CreatedAt, Converter={StaticResource DateTimeConverter}, ConverterParameter='humanize'}"></Run>
</TextBlock> </TextBlock>
<Border Classes="card-separator" /> <Border Classes="card-separator" />
<StackPanel Spacing="5"> <StackPanel Spacing="5">
<Button HorizontalAlignment="Stretch" Command="{CompiledBinding CreateRelease}"> <Button HorizontalAlignment="Stretch" Command="{CompiledBinding CreateRelease}">
Create new release Create new release
</Button> </Button>
<Button Classes="danger" HorizontalAlignment="Stretch" Command="{CompiledBinding DeleteSubmission}"> <Button Classes="danger" HorizontalAlignment="Stretch" Command="{CompiledBinding DeleteSubmission}">
Delete submission Delete submission
</Button> </Button>
</StackPanel>
</StackPanel> </StackPanel>
</StackPanel> </Border>
</Border> <Border Classes="card" IsVisible="{CompiledBinding Releases.Count}">
<Border Classes="card" IsVisible="{CompiledBinding Releases.Count}"> <StackPanel>
<StackPanel> <TextBlock Theme="{StaticResource SubtitleTextBlockStyle}">Releases</TextBlock>
<TextBlock Theme="{StaticResource SubtitleTextBlockStyle}">Releases</TextBlock> <Border Classes="card-separator" />
<Border Classes="card-separator" />
<ListBox ItemsSource="{CompiledBinding Releases}" SelectedItem="{CompiledBinding SelectedRelease}"> <ListBox ItemsSource="{CompiledBinding Releases}" SelectedItem="{CompiledBinding SelectedRelease}">
<ListBox.ItemTemplate> <ListBox.ItemTemplate>
<DataTemplate> <DataTemplate>
<StackPanel Margin="0 5"> <StackPanel Margin="0 5">
<TextBlock Text="{CompiledBinding Version}"></TextBlock> <TextBlock Text="{CompiledBinding Version}"></TextBlock>
<TextBlock Classes="subtitle" ToolTip.Tip="{CompiledBinding CreatedAt, Converter={StaticResource DateTimeConverter}}"> <TextBlock Classes="subtitle" ToolTip.Tip="{CompiledBinding CreatedAt, Converter={StaticResource DateTimeConverter}}">
<avalonia:MaterialIcon Kind="Calendar" /> <avalonia:MaterialIcon Kind="Calendar" />
<Run>Created</Run> <Run>Created</Run>
<Run Text="{CompiledBinding CreatedAt, Converter={StaticResource DateTimeConverter}, ConverterParameter='humanize'}"></Run> <Run Text="{CompiledBinding CreatedAt, Converter={StaticResource DateTimeConverter}, ConverterParameter='humanize'}"></Run>
</TextBlock> </TextBlock>
</StackPanel> </StackPanel>
</DataTemplate> </DataTemplate>
</ListBox.ItemTemplate> </ListBox.ItemTemplate>
</ListBox> </ListBox>
</StackPanel> </StackPanel>
</Border> </Border>
<controls:HyperlinkButton Command="{CompiledBinding ViewWorkshopPage}" HorizontalAlignment="Center"> <controls:HyperlinkButton Command="{CompiledBinding ViewWorkshopPage}" HorizontalAlignment="Center">
View workshop page View workshop page
</controls:HyperlinkButton> </controls:HyperlinkButton>
</StackPanel> </StackPanel>
<controls:Frame Grid.Column="1" Grid.Row="0" Name="RouterFrame" IsNavigationStackEnabled="False" CacheSize="0">
<controls:Frame.NavigationPageFactory>
<ui:PageFactory />
</controls:Frame.NavigationPageFactory>
</controls:Frame>
</Grid>
<controls:Frame Grid.Column="1" Grid.Row="0" Name="RouterFrame" IsNavigationStackEnabled="False" CacheSize="0">
<controls:Frame.NavigationPageFactory>
<ui:PageFactory />
</controls:Frame.NavigationPageFactory>
</controls:Frame>
</Grid>
</Panel>
</UserControl> </UserControl>

View File

@ -11,7 +11,8 @@ public partial class SubmissionManagementView : ReactiveUserControl<SubmissionMa
{ {
InitializeComponent(); InitializeComponent();
this.WhenActivated(d => ViewModel.WhenAnyValue(vm => vm.Screen) this.WhenActivated(d => ViewModel.WhenAnyValue(vm => vm.Screen)
.Subscribe(screen => RouterFrame.NavigateFromObject(screen ?? ViewModel?.DetailsViewModel)) .WhereNotNull()
.Subscribe(screen => RouterFrame.NavigateFromObject(screen))
.DisposeWith(d)); .DisposeWith(d));
} }
} }

View File

@ -22,6 +22,7 @@ public partial class SubmissionManagementViewModel : RoutableHostScreen<Routable
private readonly IWindowService _windowService; private readonly IWindowService _windowService;
private readonly IRouter _router; private readonly IRouter _router;
private readonly IWorkshopService _workshopService; private readonly IWorkshopService _workshopService;
private readonly SubmissionDetailsViewModel _detailsViewModel;
[Notify] private IGetSubmittedEntryById_Entry? _entry; [Notify] private IGetSubmittedEntryById_Entry? _entry;
[Notify] private List<IGetSubmittedEntryById_Entry_Releases>? _releases; [Notify] private List<IGetSubmittedEntryById_Entry_Releases>? _releases;
@ -29,7 +30,7 @@ public partial class SubmissionManagementViewModel : RoutableHostScreen<Routable
public SubmissionManagementViewModel(IWorkshopClient client, IRouter router, IWindowService windowService, IWorkshopService workshopService, SubmissionDetailsViewModel detailsViewModel) public SubmissionManagementViewModel(IWorkshopClient client, IRouter router, IWindowService windowService, IWorkshopService workshopService, SubmissionDetailsViewModel detailsViewModel)
{ {
DetailsViewModel = detailsViewModel; _detailsViewModel = detailsViewModel;
_client = client; _client = client;
_router = router; _router = router;
_windowService = windowService; _windowService = windowService;
@ -39,12 +40,12 @@ public partial class SubmissionManagementViewModel : RoutableHostScreen<Routable
{ {
this.WhenAnyValue(vm => vm.SelectedRelease) this.WhenAnyValue(vm => vm.SelectedRelease)
.WhereNotNull() .WhereNotNull()
.Subscribe(r => _router.Navigate($"/releases/{r.Id}")) .Subscribe(r => _router.Navigate($"workshop/library/submissions/{Entry?.Id}/releases/{r.Id}"))
.DisposeWith(d); .DisposeWith(d);
}); });
} }
public SubmissionDetailsViewModel DetailsViewModel { get; } public override RoutableScreen DefaultScreen => _detailsViewModel;
public async Task ViewWorkshopPage() public async Task ViewWorkshopPage()
{ {
@ -87,11 +88,11 @@ public partial class SubmissionManagementViewModel : RoutableHostScreen<Routable
Entry = result.Data?.Entry; Entry = result.Data?.Entry;
Releases = Entry?.Releases.OrderByDescending(r => r.CreatedAt).ToList(); Releases = Entry?.Releases.OrderByDescending(r => r.CreatedAt).ToList();
await DetailsViewModel.SetEntry(Entry, cancellationToken); await _detailsViewModel.SetEntry(Entry, cancellationToken);
} }
public override async Task OnClosing(NavigationArguments args) public override async Task OnClosing(NavigationArguments args)
{ {
await DetailsViewModel.OnClosing(args); await _detailsViewModel.OnClosing(args);
} }
} }

View File

@ -2,7 +2,6 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
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:mdxaml="https://github.com/whistyun/Markdown.Avalonia.Tight"
xmlns:plugins="clr-namespace:Artemis.UI.Screens.Workshop.Plugins" xmlns:plugins="clr-namespace:Artemis.UI.Screens.Workshop.Plugins"
xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia" xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia" xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
@ -17,21 +16,32 @@
</Border> </Border>
<Border Classes="card" VerticalAlignment="Top" IsVisible="{CompiledBinding Entry.PluginInfo, Converter={x:Static ObjectConverters.IsNotNull}}"> <Border Classes="card" VerticalAlignment="Top" IsVisible="{CompiledBinding Entry.PluginInfo, Converter={x:Static ObjectConverters.IsNotNull}}">
<StackPanel> <Panel>
<TextBlock>Admin required</TextBlock> <StackPanel IsVisible="{CompiledBinding Entry, Converter={x:Static ObjectConverters.IsNull}}">
<TextBlock Text="Yes" IsVisible="{CompiledBinding Entry.PluginInfo.RequiresAdmin}" /> <Border Width="110" Classes="skeleton-text"></Border>
<TextBlock Text="No" IsVisible="{CompiledBinding !Entry.PluginInfo.RequiresAdmin}" /> <Border Width="35" Classes="skeleton-text"></Border>
<TextBlock Margin="0 15 0 5">Supported platforms</TextBlock> <Border Margin="0 16 0 3" Width="130" Classes="skeleton-text"></Border>
<StackPanel Orientation="Horizontal" Spacing="10"> <Border Width="60" Classes="skeleton-text"></Border>
<avalonia:MaterialIcon Kind="MicrosoftWindows" IsVisible="{CompiledBinding Entry.PluginInfo.SupportsWindows}" />
<avalonia:MaterialIcon Kind="Linux" IsVisible="{CompiledBinding Entry.PluginInfo.SupportsLinux}" />
<avalonia:MaterialIcon Kind="Apple" IsVisible="{CompiledBinding Entry.PluginInfo.SupportsOSX}" />
</StackPanel> </StackPanel>
</StackPanel>
<StackPanel IsVisible="{CompiledBinding Entry, Converter={x:Static ObjectConverters.IsNotNull}}">
<TextBlock>Admin required</TextBlock>
<TextBlock Text="Yes" IsVisible="{CompiledBinding Entry.PluginInfo.RequiresAdmin}" />
<TextBlock Text="No" IsVisible="{CompiledBinding !Entry.PluginInfo.RequiresAdmin}" />
<TextBlock Margin="0 15 0 5">Supported platforms</TextBlock>
<StackPanel Orientation="Horizontal" Spacing="10">
<avalonia:MaterialIcon Kind="MicrosoftWindows" IsVisible="{CompiledBinding Entry.PluginInfo.SupportsWindows}" />
<avalonia:MaterialIcon Kind="Linux" IsVisible="{CompiledBinding Entry.PluginInfo.SupportsLinux}" />
<avalonia:MaterialIcon Kind="Apple" IsVisible="{CompiledBinding Entry.PluginInfo.SupportsOSX}" />
</StackPanel>
</StackPanel>
</Panel>
</Border> </Border>
<Border Classes="card" VerticalAlignment="Top" IsVisible="{CompiledBinding Entry.Releases.Count}"> <Border Classes="card" VerticalAlignment="Top" IsVisible="{CompiledBinding Entry.Releases.Count, FallbackValue=False}">
<ContentControl Content="{CompiledBinding EntryReleasesViewModel}" /> <ContentControl Content="{CompiledBinding EntryReleasesViewModel}" />
</Border> </Border>
</StackPanel> </StackPanel>
@ -44,6 +54,6 @@
</controls:Frame> </controls:Frame>
</ScrollViewer> </ScrollViewer>
<ContentControl Grid.Row="1" Grid.Column="2" IsVisible="{CompiledBinding Entry.Images.Count}" Content="{CompiledBinding EntryImagesViewModel}" /> <ContentControl Grid.Row="1" Grid.Column="2" IsVisible="{CompiledBinding Entry.Images.Count, FallbackValue=False}" Content="{CompiledBinding EntryImagesViewModel}" />
</Grid> </Grid>
</UserControl> </UserControl>

View File

@ -11,7 +11,8 @@ public partial class PluginDetailsView : ReactiveUserControl<PluginDetailsViewMo
{ {
InitializeComponent(); InitializeComponent();
this.WhenActivated(d => ViewModel.WhenAnyValue(vm => vm.Screen) this.WhenActivated(d => ViewModel.WhenAnyValue(vm => vm.Screen)
.Subscribe(screen => RouterFrame.NavigateFromObject(screen ?? ViewModel?.PluginDescriptionViewModel)) .WhereNotNull()
.Subscribe(screen => RouterFrame.NavigateFromObject(screen))
.DisposeWith(d)); .DisposeWith(d));
} }
} }

View File

@ -16,31 +16,31 @@ namespace Artemis.UI.Screens.Workshop.Plugins;
public partial class PluginDetailsViewModel : RoutableHostScreen<RoutableScreen, WorkshopDetailParameters> public partial class PluginDetailsViewModel : RoutableHostScreen<RoutableScreen, WorkshopDetailParameters>
{ {
private readonly IWorkshopClient _client; private readonly IWorkshopClient _client;
private readonly Func<IEntryDetails, EntryInfoViewModel> _getEntryInfoViewModel; private readonly PluginDescriptionViewModel _pluginDescriptionViewModel;
private readonly Func<IEntryDetails, EntryReleasesViewModel> _getEntryReleasesViewModel; private readonly Func<IEntryDetails, EntryReleasesViewModel> _getEntryReleasesViewModel;
private readonly Func<IEntryDetails, EntryImagesViewModel> _getEntryImagesViewModel; private readonly Func<IEntryDetails, EntryImagesViewModel> _getEntryImagesViewModel;
[Notify] private IGetPluginEntryById_Entry? _entry; [Notify] private IGetPluginEntryById_Entry? _entry;
[Notify] private EntryInfoViewModel? _entryInfoViewModel;
[Notify] private EntryReleasesViewModel? _entryReleasesViewModel; [Notify] private EntryReleasesViewModel? _entryReleasesViewModel;
[Notify] private EntryImagesViewModel? _entryImagesViewModel; [Notify] private EntryImagesViewModel? _entryImagesViewModel;
[Notify] private ReadOnlyObservableCollection<EntryListItemViewModel>? _dependants; [Notify] private ReadOnlyObservableCollection<EntryListItemViewModel>? _dependants;
public PluginDetailsViewModel(IWorkshopClient client, public PluginDetailsViewModel(IWorkshopClient client,
PluginDescriptionViewModel pluginDescriptionViewModel, PluginDescriptionViewModel pluginDescriptionViewModel,
Func<IEntryDetails, EntryInfoViewModel> getEntryInfoViewModel, EntryInfoViewModel entryInfoViewModel,
Func<IEntryDetails, EntryReleasesViewModel> getEntryReleasesViewModel, Func<IEntryDetails, EntryReleasesViewModel> getEntryReleasesViewModel,
Func<IEntryDetails, EntryImagesViewModel> getEntryImagesViewModel) Func<IEntryDetails, EntryImagesViewModel> getEntryImagesViewModel)
{ {
_client = client; _client = client;
_getEntryInfoViewModel = getEntryInfoViewModel; _pluginDescriptionViewModel = pluginDescriptionViewModel;
_getEntryReleasesViewModel = getEntryReleasesViewModel; _getEntryReleasesViewModel = getEntryReleasesViewModel;
_getEntryImagesViewModel = getEntryImagesViewModel; _getEntryImagesViewModel = getEntryImagesViewModel;
PluginDescriptionViewModel = pluginDescriptionViewModel; EntryInfoViewModel = entryInfoViewModel;
RecycleScreen = false; RecycleScreen = false;
} }
public PluginDescriptionViewModel PluginDescriptionViewModel { get; } public override RoutableScreen DefaultScreen => _pluginDescriptionViewModel;
public EntryInfoViewModel EntryInfoViewModel { get; }
public override async Task OnNavigating(WorkshopDetailParameters parameters, NavigationArguments args, CancellationToken cancellationToken) public override async Task OnNavigating(WorkshopDetailParameters parameters, NavigationArguments args, CancellationToken cancellationToken)
{ {
@ -50,15 +50,19 @@ public partial class PluginDetailsViewModel : RoutableHostScreen<RoutableScreen,
private async Task GetEntry(long entryId, CancellationToken cancellationToken) private async Task GetEntry(long entryId, CancellationToken cancellationToken)
{ {
Task grace = Task.Delay(300, cancellationToken);
IOperationResult<IGetPluginEntryByIdResult> result = await _client.GetPluginEntryById.ExecuteAsync(entryId, cancellationToken); IOperationResult<IGetPluginEntryByIdResult> result = await _client.GetPluginEntryById.ExecuteAsync(entryId, cancellationToken);
if (result.IsErrorResult()) if (result.IsErrorResult())
return; return;
// Let the UI settle to avoid lag when deep linking
await grace;
Entry = result.Data?.Entry; Entry = result.Data?.Entry;
EntryInfoViewModel = Entry != null ? _getEntryInfoViewModel(Entry) : null; EntryInfoViewModel.SetEntry(Entry);
EntryReleasesViewModel = Entry != null ? _getEntryReleasesViewModel(Entry) : null; EntryReleasesViewModel = Entry != null ? _getEntryReleasesViewModel(Entry) : null;
EntryImagesViewModel = Entry != null ? _getEntryImagesViewModel(Entry) : null; EntryImagesViewModel = Entry != null ? _getEntryImagesViewModel(Entry) : null;
await PluginDescriptionViewModel.SetEntry(Entry, cancellationToken); await _pluginDescriptionViewModel.SetEntry(Entry, cancellationToken);
} }
} }

View File

@ -11,7 +11,8 @@ public partial class PluginListView : ReactiveUserControl<PluginListViewModel>
{ {
InitializeComponent(); InitializeComponent();
this.WhenActivated(d => ViewModel.WhenAnyValue(vm => vm.Screen) this.WhenActivated(d => ViewModel.WhenAnyValue(vm => vm.Screen)
.Subscribe(screen => RouterFrame.NavigateFromObject(screen ?? ViewModel?.EntryListViewModel)) .WhereNotNull()
.Subscribe(screen => RouterFrame.NavigateFromObject(screen))
.DisposeWith(d)); .DisposeWith(d));
} }
} }

View File

@ -6,11 +6,12 @@ namespace Artemis.UI.Screens.Workshop.Plugins;
public class PluginListViewModel : RoutableHostScreen<RoutableScreen> public class PluginListViewModel : RoutableHostScreen<RoutableScreen>
{ {
public EntryListViewModel EntryListViewModel { get; } private readonly EntryListViewModel _entryListViewModel;
public override RoutableScreen DefaultScreen => _entryListViewModel;
public PluginListViewModel(EntryListViewModel entryListViewModel) public PluginListViewModel(EntryListViewModel entryListViewModel)
{ {
EntryListViewModel = entryListViewModel; _entryListViewModel = entryListViewModel;
EntryListViewModel.EntryType = EntryType.Plugin; _entryListViewModel.EntryType = EntryType.Plugin;
} }
} }

View File

@ -3,7 +3,6 @@
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:profile="clr-namespace:Artemis.UI.Screens.Workshop.Profile" xmlns:profile="clr-namespace:Artemis.UI.Screens.Workshop.Profile"
xmlns:mdxaml="https://github.com/whistyun/Markdown.Avalonia.Tight"
xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia" xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
xmlns:ui="clr-namespace:Artemis.UI" xmlns:ui="clr-namespace:Artemis.UI"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="800" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="800"
@ -14,7 +13,7 @@
<Border Classes="card" VerticalAlignment="Top"> <Border Classes="card" VerticalAlignment="Top">
<ContentControl Content="{CompiledBinding EntryInfoViewModel}" /> <ContentControl Content="{CompiledBinding EntryInfoViewModel}" />
</Border> </Border>
<Border Classes="card" VerticalAlignment="Top" IsVisible="{CompiledBinding Entry.Releases.Count}"> <Border Classes="card" VerticalAlignment="Top" IsVisible="{CompiledBinding Entry.Releases.Count, FallbackValue=False}">
<ContentControl Content="{CompiledBinding EntryReleasesViewModel}" /> <ContentControl Content="{CompiledBinding EntryReleasesViewModel}" />
</Border> </Border>
</StackPanel> </StackPanel>
@ -27,6 +26,6 @@
</controls:Frame> </controls:Frame>
</ScrollViewer> </ScrollViewer>
<ContentControl Grid.Row="1" Grid.Column="2" IsVisible="{CompiledBinding Entry.Images.Count}" Content="{CompiledBinding EntryImagesViewModel}" /> <ContentControl Grid.Row="1" Grid.Column="2" IsVisible="{CompiledBinding Entry.Images.Count, FallbackValue=False}" Content="{CompiledBinding EntryImagesViewModel}" />
</Grid> </Grid>
</UserControl> </UserControl>

View File

@ -11,7 +11,8 @@ public partial class ProfileDetailsView : ReactiveUserControl<ProfileDetailsView
{ {
InitializeComponent(); InitializeComponent();
this.WhenActivated(d => ViewModel.WhenAnyValue(vm => vm.Screen) this.WhenActivated(d => ViewModel.WhenAnyValue(vm => vm.Screen)
.Subscribe(screen => RouterFrame.NavigateFromObject(screen ?? ViewModel?.ProfileDescriptionViewModel)) .WhereNotNull()
.Subscribe(screen => RouterFrame.NavigateFromObject(screen))
.DisposeWith(d)); .DisposeWith(d));
} }
} }

View File

@ -17,31 +17,31 @@ namespace Artemis.UI.Screens.Workshop.Profile;
public partial class ProfileDetailsViewModel : RoutableHostScreen<RoutableScreen, WorkshopDetailParameters> public partial class ProfileDetailsViewModel : RoutableHostScreen<RoutableScreen, WorkshopDetailParameters>
{ {
private readonly IWorkshopClient _client; private readonly IWorkshopClient _client;
private readonly Func<IEntryDetails, EntryInfoViewModel> _getEntryInfoViewModel; private readonly ProfileDescriptionViewModel _profileDescriptionViewModel;
private readonly Func<IEntryDetails, EntryReleasesViewModel> _getEntryReleasesViewModel; private readonly Func<IEntryDetails, EntryReleasesViewModel> _getEntryReleasesViewModel;
private readonly Func<IEntryDetails, EntryImagesViewModel> _getEntryImagesViewModel; private readonly Func<IEntryDetails, EntryImagesViewModel> _getEntryImagesViewModel;
[Notify] private IEntryDetails? _entry; [Notify] private IEntryDetails? _entry;
[Notify] private EntryInfoViewModel? _entryInfoViewModel;
[Notify] private EntryReleasesViewModel? _entryReleasesViewModel; [Notify] private EntryReleasesViewModel? _entryReleasesViewModel;
[Notify] private EntryImagesViewModel? _entryImagesViewModel; [Notify] private EntryImagesViewModel? _entryImagesViewModel;
public ProfileDetailsViewModel(IWorkshopClient client, public ProfileDetailsViewModel(IWorkshopClient client,
ProfileDescriptionViewModel profileDescriptionViewModel, ProfileDescriptionViewModel profileDescriptionViewModel,
Func<IEntryDetails, EntryInfoViewModel> getEntryInfoViewModel, EntryInfoViewModel entryInfoViewModel,
Func<IEntryDetails, EntryReleasesViewModel> getEntryReleasesViewModel, Func<IEntryDetails, EntryReleasesViewModel> getEntryReleasesViewModel,
Func<IEntryDetails, EntryImagesViewModel> getEntryImagesViewModel) Func<IEntryDetails, EntryImagesViewModel> getEntryImagesViewModel)
{ {
_client = client; _client = client;
_getEntryInfoViewModel = getEntryInfoViewModel; _profileDescriptionViewModel = profileDescriptionViewModel;
_getEntryReleasesViewModel = getEntryReleasesViewModel; _getEntryReleasesViewModel = getEntryReleasesViewModel;
_getEntryImagesViewModel = getEntryImagesViewModel; _getEntryImagesViewModel = getEntryImagesViewModel;
ProfileDescriptionViewModel = profileDescriptionViewModel; EntryInfoViewModel = entryInfoViewModel;
RecycleScreen = false; RecycleScreen = false;
} }
public ProfileDescriptionViewModel ProfileDescriptionViewModel { get; } public override RoutableScreen DefaultScreen => _profileDescriptionViewModel;
public EntryInfoViewModel EntryInfoViewModel { get; }
public override async Task OnNavigating(WorkshopDetailParameters parameters, NavigationArguments args, CancellationToken cancellationToken) public override async Task OnNavigating(WorkshopDetailParameters parameters, NavigationArguments args, CancellationToken cancellationToken)
{ {
@ -51,15 +51,19 @@ public partial class ProfileDetailsViewModel : RoutableHostScreen<RoutableScreen
private async Task GetEntry(long entryId, CancellationToken cancellationToken) private async Task GetEntry(long entryId, CancellationToken cancellationToken)
{ {
Task grace = Task.Delay(300, cancellationToken);
IOperationResult<IGetEntryByIdResult> result = await _client.GetEntryById.ExecuteAsync(entryId, cancellationToken); IOperationResult<IGetEntryByIdResult> result = await _client.GetEntryById.ExecuteAsync(entryId, cancellationToken);
if (result.IsErrorResult()) if (result.IsErrorResult())
return; return;
// Let the UI settle to avoid lag when deep linking
await grace;
Entry = result.Data?.Entry; Entry = result.Data?.Entry;
EntryInfoViewModel = Entry != null ? _getEntryInfoViewModel(Entry) : null; EntryInfoViewModel.SetEntry(Entry);
EntryReleasesViewModel = Entry != null ? _getEntryReleasesViewModel(Entry) : null; EntryReleasesViewModel = Entry != null ? _getEntryReleasesViewModel(Entry) : null;
EntryImagesViewModel = Entry != null ? _getEntryImagesViewModel(Entry) : null; EntryImagesViewModel = Entry != null ? _getEntryImagesViewModel(Entry) : null;
await ProfileDescriptionViewModel.SetEntry(Entry, cancellationToken); await _profileDescriptionViewModel.SetEntry(Entry, cancellationToken);
} }
} }

View File

@ -11,7 +11,8 @@ public partial class ProfileListView : ReactiveUserControl<ProfileListViewModel>
{ {
InitializeComponent(); InitializeComponent();
this.WhenActivated(d => ViewModel.WhenAnyValue(vm => vm.Screen) this.WhenActivated(d => ViewModel.WhenAnyValue(vm => vm.Screen)
.Subscribe(screen => RouterFrame.NavigateFromObject(screen ?? ViewModel?.EntryListViewModel)) .WhereNotNull()
.Subscribe(screen => RouterFrame.NavigateFromObject(screen))
.DisposeWith(d)); .DisposeWith(d));
} }
} }

View File

@ -6,11 +6,12 @@ namespace Artemis.UI.Screens.Workshop.Profile;
public class ProfileListViewModel : RoutableHostScreen<RoutableScreen> public class ProfileListViewModel : RoutableHostScreen<RoutableScreen>
{ {
public EntryListViewModel EntryListViewModel { get; } private readonly EntryListViewModel _entryListViewModel;
public override RoutableScreen DefaultScreen => _entryListViewModel;
public ProfileListViewModel(EntryListViewModel entryListViewModel) public ProfileListViewModel(EntryListViewModel entryListViewModel)
{ {
EntryListViewModel = entryListViewModel; _entryListViewModel = entryListViewModel;
EntryListViewModel.EntryType = EntryType.Profile; _entryListViewModel.EntryType = EntryType.Profile;
} }
} }

View File

@ -0,0 +1,9 @@
namespace Artemis.WebClient.Workshop.Extensions;
public static class EntryExtensions
{
public static string GetEntryPath(this IEntryDetails entry)
{
return $"workshop/entries/{entry.EntryType.ToString().ToLower()}s/details/{entry.Id}";
}
}