1
0
mirror of https://github.com/Artemis-RGB/Artemis synced 2025-12-12 13:28:33 +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; }
IRoutableScreen? InternalScreen { get; }
IRoutableScreen? InternalDefaultScreen { get; }
void InternalChangeScreen(IRoutableScreen? screen);
}

View File

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

View File

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

View File

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

View File

@ -13,74 +13,90 @@
<converters:EntryIconUriConverter x:Key="EntryIconUriConverter" />
<converters:DateTimeConverter x:Key="DateTimeConverter" />
</UserControl.Resources>
<StackPanel>
<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" />
<Panel>
<StackPanel IsVisible="{CompiledBinding Entry, Converter={x:Static ObjectConverters.IsNull}}">
<Border Classes="skeleton-text" Margin="0 0 10 0" Width="80" Height="80"></Border>
<Border Classes="skeleton-text title" HorizontalAlignment="Stretch"/>
<Border Classes="skeleton-text" Width="120"/>
<Border Classes="skeleton-text" Width="140" Margin="0 8"/>
<Border Classes="skeleton-text" Width="80"/>
<Border Classes="card-separator" Margin="0 15 0 17"></Border>
<Border Classes="skeleton-text" Width="120"/>
<StackPanel Margin="0 10 0 0">
<Border Classes="skeleton-text" Width="160"/>
<Border Classes="skeleton-text" Width="160"/>
</StackPanel>
<Border Classes="skeleton-button"></Border>
</StackPanel>
<StackPanel IsVisible="{CompiledBinding Entry, Converter={x:Static ObjectConverters.IsNotNull}}">
<Panel>
<Border CornerRadius="6"
HorizontalAlignment="Left"
Margin="0 0 10 0"
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>
</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>
</StackPanel>
</Panel>
</UserControl>

View File

@ -8,6 +8,7 @@ using Artemis.UI.Shared;
using Artemis.UI.Shared.Routing;
using Artemis.UI.Shared.Services;
using Artemis.WebClient.Workshop;
using Artemis.WebClient.Workshop.Extensions;
using Artemis.WebClient.Workshop.Models;
using Artemis.WebClient.Workshop.Services;
using PropertyChanged.SourceGenerator;
@ -19,36 +20,47 @@ public partial class EntryInfoViewModel : ActivatableViewModelBase
{
private readonly IRouter _router;
private readonly INotificationService _notificationService;
private readonly IWorkshopService _workshopService;
[Notify] private IEntryDetails? _entry;
[Notify] private DateTimeOffset? _updatedAt;
[Notify] private bool _canBeManaged;
public EntryInfoViewModel(IEntryDetails entry, IRouter router, INotificationService notificationService, IWorkshopService workshopService)
public EntryInfoViewModel(IRouter router, INotificationService notificationService, IWorkshopService workshopService)
{
_router = router;
_notificationService = notificationService;
Entry = entry;
UpdatedAt = Entry.Releases.Any() ? Entry.Releases.Max(r => r.CreatedAt) : Entry.CreatedAt;
CanBeManaged = Entry.EntryType != EntryType.Profile && workshopService.GetInstalledEntry(entry.Id) != null;
_workshopService = workshopService;
this.WhenActivated(d =>
{
Observable.FromEventPattern<InstalledEntry>(x => workshopService.OnInstalledEntrySaved += x, x => workshopService.OnInstalledEntrySaved -= x)
.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);
});
}
public IEntryDetails Entry { get; }
public DateTimeOffset? UpdatedAt { get; }
public void SetEntry(IEntryDetails? entry)
{
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()
{
if (Entry == null)
return;
await Shared.UI.Clipboard.SetTextAsync($"{WorkshopConstants.WORKSHOP_URL}/entries/{Entry.Id}/{StringUtilities.UrlFriendly(Entry.Name)}");
_notificationService.CreateNotification().WithTitle("Copied share link to clipboard.").Show();
}
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.Routing;
using Artemis.WebClient.Workshop;
using Artemis.WebClient.Workshop.Extensions;
using PropertyChanged.SourceGenerator;
using ReactiveUI;
@ -25,14 +26,15 @@ public partial class EntryReleasesViewModel : ActivatableViewModelBase
this.WhenActivated(d =>
{
router.CurrentPath.Subscribe(p => SelectedRelease = p != null && p.Contains("releases") && float.TryParse(p.Split('/').Last(), out float releaseId)
? Releases.FirstOrDefault(r => r.Release.Id == releaseId)
: null)
router.CurrentPath.Subscribe(p =>
SelectedRelease = p != null && p.StartsWith(Entry.GetEntryPath()) && float.TryParse(p.Split('/').Last(), out float releaseId)
? Releases.FirstOrDefault(r => r.Release.Id == releaseId)
: null)
.DisposeWith(d);
this.WhenAnyValue(vm => vm.SelectedRelease)
.WhereNotNull()
.Subscribe(s => _router.Navigate($"/releases/{s.Release.Id}"))
.Subscribe(s => _router.Navigate($"{Entry.GetEntryPath()}/releases/{s.Release.Id}"))
.DisposeWith(d);
});
}

View File

@ -3,7 +3,6 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
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:ui="clr-namespace:Artemis.UI"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="800"
@ -14,7 +13,7 @@
<Border Classes="card" VerticalAlignment="Top">
<ContentControl Content="{CompiledBinding EntryInfoViewModel}" />
</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}" />
</Border>
</StackPanel>

View File

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

View File

@ -14,31 +14,31 @@ namespace Artemis.UI.Screens.Workshop.Layout;
public partial class LayoutDetailsViewModel : RoutableHostScreen<RoutableScreen, WorkshopDetailParameters>
{
private readonly IWorkshopClient _client;
private readonly Func<IEntryDetails, EntryInfoViewModel> _getEntryInfoViewModel;
private readonly LayoutDescriptionViewModel _layoutDescriptionViewModel;
private readonly Func<IEntryDetails, EntryReleasesViewModel> _getEntryReleasesViewModel;
private readonly Func<IEntryDetails, EntryImagesViewModel> _getEntryImagesViewModel;
[Notify] private IEntryDetails? _entry;
[Notify] private EntryInfoViewModel? _entryInfoViewModel;
[Notify] private EntryReleasesViewModel? _entryReleasesViewModel;
[Notify] private EntryImagesViewModel? _entryImagesViewModel;
public LayoutDetailsViewModel(IWorkshopClient client,
LayoutDescriptionViewModel layoutDescriptionViewModel,
Func<IEntryDetails, EntryInfoViewModel> getEntryInfoViewModel,
EntryInfoViewModel entryInfoViewModel,
Func<IEntryDetails, EntryReleasesViewModel> getEntryReleasesViewModel,
Func<IEntryDetails, EntryImagesViewModel> getEntryImagesViewModel)
{
_client = client;
_getEntryInfoViewModel = getEntryInfoViewModel;
_layoutDescriptionViewModel = layoutDescriptionViewModel;
_getEntryReleasesViewModel = getEntryReleasesViewModel;
_getEntryImagesViewModel = getEntryImagesViewModel;
LayoutDescriptionViewModel = layoutDescriptionViewModel;
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)
{
if (Entry?.Id != parameters.EntryId)
@ -47,14 +47,18 @@ public partial class LayoutDetailsViewModel : RoutableHostScreen<RoutableScreen,
private async Task GetEntry(long entryId, CancellationToken cancellationToken)
{
Task grace = Task.Delay(300, cancellationToken);
IOperationResult<IGetEntryByIdResult> result = await _client.GetEntryById.ExecuteAsync(entryId, cancellationToken);
if (result.IsErrorResult())
return;
// Let the UI settle to avoid lag when deep linking
await grace;
Entry = result.Data?.Entry;
EntryInfoViewModel = Entry != null ? _getEntryInfoViewModel(Entry) : null;
EntryInfoViewModel.SetEntry(Entry);
EntryReleasesViewModel = Entry != null ? _getEntryReleasesViewModel(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();
this.WhenActivated(d => ViewModel.WhenAnyValue(vm => vm.Screen)
.Subscribe(screen => RouterFrame.NavigateFromObject(screen ?? ViewModel?.EntryListViewModel))
.WhereNotNull()
.Subscribe(screen => RouterFrame.NavigateFromObject(screen))
.DisposeWith(d));
}
}

View File

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

View File

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

View File

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

View File

@ -22,6 +22,7 @@ public partial class SubmissionManagementViewModel : RoutableHostScreen<Routable
private readonly IWindowService _windowService;
private readonly IRouter _router;
private readonly IWorkshopService _workshopService;
private readonly SubmissionDetailsViewModel _detailsViewModel;
[Notify] private IGetSubmittedEntryById_Entry? _entry;
[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)
{
DetailsViewModel = detailsViewModel;
_detailsViewModel = detailsViewModel;
_client = client;
_router = router;
_windowService = windowService;
@ -39,12 +40,12 @@ public partial class SubmissionManagementViewModel : RoutableHostScreen<Routable
{
this.WhenAnyValue(vm => vm.SelectedRelease)
.WhereNotNull()
.Subscribe(r => _router.Navigate($"/releases/{r.Id}"))
.Subscribe(r => _router.Navigate($"workshop/library/submissions/{Entry?.Id}/releases/{r.Id}"))
.DisposeWith(d);
});
}
public SubmissionDetailsViewModel DetailsViewModel { get; }
public override RoutableScreen DefaultScreen => _detailsViewModel;
public async Task ViewWorkshopPage()
{
@ -79,7 +80,7 @@ public partial class SubmissionManagementViewModel : RoutableHostScreen<Routable
{
// If there is a 2nd parameter, it's a release ID
SelectedRelease = args.RouteParameters.Length > 1 ? Releases?.FirstOrDefault(r => r.Id == (long) args.RouteParameters[1]) : null;
IOperationResult<IGetSubmittedEntryByIdResult> result = await _client.GetSubmittedEntryById.ExecuteAsync(parameters.EntryId, cancellationToken);
if (result.IsErrorResult())
return;
@ -87,11 +88,11 @@ public partial class SubmissionManagementViewModel : RoutableHostScreen<Routable
Entry = result.Data?.Entry;
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)
{
await DetailsViewModel.OnClosing(args);
await _detailsViewModel.OnClosing(args);
}
}

View File

@ -2,7 +2,6 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:mdxaml="https://github.com/whistyun/Markdown.Avalonia.Tight"
xmlns:plugins="clr-namespace:Artemis.UI.Screens.Workshop.Plugins"
xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
@ -17,21 +16,32 @@
</Border>
<Border Classes="card" VerticalAlignment="Top" IsVisible="{CompiledBinding Entry.PluginInfo, Converter={x:Static ObjectConverters.IsNotNull}}">
<StackPanel>
<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}" />
<Panel>
<StackPanel IsVisible="{CompiledBinding Entry, Converter={x:Static ObjectConverters.IsNull}}">
<Border Width="110" Classes="skeleton-text"></Border>
<Border Width="35" Classes="skeleton-text"></Border>
<Border Margin="0 16 0 3" Width="130" Classes="skeleton-text"></Border>
<Border Width="60" Classes="skeleton-text"></Border>
</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 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}" />
</Border>
</StackPanel>
@ -44,6 +54,6 @@
</controls:Frame>
</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>
</UserControl>

View File

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

View File

@ -16,32 +16,32 @@ namespace Artemis.UI.Screens.Workshop.Plugins;
public partial class PluginDetailsViewModel : RoutableHostScreen<RoutableScreen, WorkshopDetailParameters>
{
private readonly IWorkshopClient _client;
private readonly Func<IEntryDetails, EntryInfoViewModel> _getEntryInfoViewModel;
private readonly PluginDescriptionViewModel _pluginDescriptionViewModel;
private readonly Func<IEntryDetails, EntryReleasesViewModel> _getEntryReleasesViewModel;
private readonly Func<IEntryDetails, EntryImagesViewModel> _getEntryImagesViewModel;
[Notify] private IGetPluginEntryById_Entry? _entry;
[Notify] private EntryInfoViewModel? _entryInfoViewModel;
[Notify] private EntryReleasesViewModel? _entryReleasesViewModel;
[Notify] private EntryImagesViewModel? _entryImagesViewModel;
[Notify] private ReadOnlyObservableCollection<EntryListItemViewModel>? _dependants;
public PluginDetailsViewModel(IWorkshopClient client,
PluginDescriptionViewModel pluginDescriptionViewModel,
Func<IEntryDetails, EntryInfoViewModel> getEntryInfoViewModel,
EntryInfoViewModel entryInfoViewModel,
Func<IEntryDetails, EntryReleasesViewModel> getEntryReleasesViewModel,
Func<IEntryDetails, EntryImagesViewModel> getEntryImagesViewModel)
{
_client = client;
_getEntryInfoViewModel = getEntryInfoViewModel;
_pluginDescriptionViewModel = pluginDescriptionViewModel;
_getEntryReleasesViewModel = getEntryReleasesViewModel;
_getEntryImagesViewModel = getEntryImagesViewModel;
PluginDescriptionViewModel = pluginDescriptionViewModel;
EntryInfoViewModel = entryInfoViewModel;
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)
{
if (Entry?.Id != parameters.EntryId)
@ -50,15 +50,19 @@ public partial class PluginDetailsViewModel : RoutableHostScreen<RoutableScreen,
private async Task GetEntry(long entryId, CancellationToken cancellationToken)
{
Task grace = Task.Delay(300, cancellationToken);
IOperationResult<IGetPluginEntryByIdResult> result = await _client.GetPluginEntryById.ExecuteAsync(entryId, cancellationToken);
if (result.IsErrorResult())
return;
// Let the UI settle to avoid lag when deep linking
await grace;
Entry = result.Data?.Entry;
EntryInfoViewModel = Entry != null ? _getEntryInfoViewModel(Entry) : null;
EntryInfoViewModel.SetEntry(Entry);
EntryReleasesViewModel = Entry != null ? _getEntryReleasesViewModel(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();
this.WhenActivated(d => ViewModel.WhenAnyValue(vm => vm.Screen)
.Subscribe(screen => RouterFrame.NavigateFromObject(screen ?? ViewModel?.EntryListViewModel))
.WhereNotNull()
.Subscribe(screen => RouterFrame.NavigateFromObject(screen))
.DisposeWith(d));
}
}

View File

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

View File

@ -3,7 +3,6 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
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:ui="clr-namespace:Artemis.UI"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="800"
@ -14,7 +13,7 @@
<Border Classes="card" VerticalAlignment="Top">
<ContentControl Content="{CompiledBinding EntryInfoViewModel}" />
</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}" />
</Border>
</StackPanel>
@ -27,6 +26,6 @@
</controls:Frame>
</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>
</UserControl>

View File

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

View File

@ -17,32 +17,32 @@ namespace Artemis.UI.Screens.Workshop.Profile;
public partial class ProfileDetailsViewModel : RoutableHostScreen<RoutableScreen, WorkshopDetailParameters>
{
private readonly IWorkshopClient _client;
private readonly Func<IEntryDetails, EntryInfoViewModel> _getEntryInfoViewModel;
private readonly ProfileDescriptionViewModel _profileDescriptionViewModel;
private readonly Func<IEntryDetails, EntryReleasesViewModel> _getEntryReleasesViewModel;
private readonly Func<IEntryDetails, EntryImagesViewModel> _getEntryImagesViewModel;
[Notify] private IEntryDetails? _entry;
[Notify] private EntryInfoViewModel? _entryInfoViewModel;
[Notify] private EntryReleasesViewModel? _entryReleasesViewModel;
[Notify] private EntryImagesViewModel? _entryImagesViewModel;
public ProfileDetailsViewModel(IWorkshopClient client,
ProfileDescriptionViewModel profileDescriptionViewModel,
Func<IEntryDetails, EntryInfoViewModel> getEntryInfoViewModel,
EntryInfoViewModel entryInfoViewModel,
Func<IEntryDetails, EntryReleasesViewModel> getEntryReleasesViewModel,
Func<IEntryDetails, EntryImagesViewModel> getEntryImagesViewModel)
{
_client = client;
_getEntryInfoViewModel = getEntryInfoViewModel;
_profileDescriptionViewModel = profileDescriptionViewModel;
_getEntryReleasesViewModel = getEntryReleasesViewModel;
_getEntryImagesViewModel = getEntryImagesViewModel;
ProfileDescriptionViewModel = profileDescriptionViewModel;
EntryInfoViewModel = entryInfoViewModel;
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)
{
if (Entry?.Id != parameters.EntryId)
@ -51,15 +51,19 @@ public partial class ProfileDetailsViewModel : RoutableHostScreen<RoutableScreen
private async Task GetEntry(long entryId, CancellationToken cancellationToken)
{
Task grace = Task.Delay(300, cancellationToken);
IOperationResult<IGetEntryByIdResult> result = await _client.GetEntryById.ExecuteAsync(entryId, cancellationToken);
if (result.IsErrorResult())
return;
// Let the UI settle to avoid lag when deep linking
await grace;
Entry = result.Data?.Entry;
EntryInfoViewModel = Entry != null ? _getEntryInfoViewModel(Entry) : null;
EntryInfoViewModel.SetEntry(Entry);
EntryReleasesViewModel = Entry != null ? _getEntryReleasesViewModel(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();
this.WhenActivated(d => ViewModel.WhenAnyValue(vm => vm.Screen)
.Subscribe(screen => RouterFrame.NavigateFromObject(screen ?? ViewModel?.EntryListViewModel))
.WhereNotNull()
.Subscribe(screen => RouterFrame.NavigateFromObject(screen))
.DisposeWith(d));
}
}

View File

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