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

Show multiple releases and improve workshop routing

This commit is contained in:
Robert 2024-03-24 21:29:48 +01:00
parent da3d47d7b8
commit 9c04932afa
39 changed files with 419 additions and 498 deletions

View File

@ -44,5 +44,22 @@
<DependentUpon>DeviceSelectionDialogView.axaml</DependentUpon> <DependentUpon>DeviceSelectionDialogView.axaml</DependentUpon>
<SubType>Code</SubType> <SubType>Code</SubType>
</Compile> </Compile>
<Compile Update="Screens\Workshop\Layout\LayoutListView.axaml.cs">
<DependentUpon>LayoutListView.axaml</DependentUpon>
<SubType>Code</SubType>
</Compile>
<Compile Update="Screens\Workshop\Plugins\PluginListView.axaml.cs">
<DependentUpon>LayoutListView.axaml</DependentUpon>
<SubType>Code</SubType>
</Compile>
<Compile Update="Screens\Workshop\Profile\ProfileListView.axaml.cs">
<DependentUpon>LayoutListView.axaml</DependentUpon>
<SubType>Code</SubType>
</Compile>
</ItemGroup>
<ItemGroup>
<UpToDateCheckInput Remove="Screens\Workshop\Entries\Tabs\PluginListView.axaml" />
<UpToDateCheckInput Remove="Screens\Workshop\Entries\Tabs\ProfileListView.axaml" />
</ItemGroup> </ItemGroup>
</Project> </Project>

View File

@ -7,11 +7,11 @@ using Artemis.UI.Screens.Settings.Updating;
using Artemis.UI.Screens.SurfaceEditor; using Artemis.UI.Screens.SurfaceEditor;
using Artemis.UI.Screens.Workshop; using Artemis.UI.Screens.Workshop;
using Artemis.UI.Screens.Workshop.Entries; using Artemis.UI.Screens.Workshop.Entries;
using Artemis.UI.Screens.Workshop.Entries.Tabs;
using Artemis.UI.Screens.Workshop.Home; using Artemis.UI.Screens.Workshop.Home;
using Artemis.UI.Screens.Workshop.Layout; using Artemis.UI.Screens.Workshop.Layout;
using Artemis.UI.Screens.Workshop.Library; using Artemis.UI.Screens.Workshop.Library;
using Artemis.UI.Screens.Workshop.Library.Tabs; using Artemis.UI.Screens.Workshop.Library.Tabs;
using Artemis.UI.Screens.Workshop.Plugins;
using Artemis.UI.Screens.Workshop.Profile; using Artemis.UI.Screens.Workshop.Profile;
using Artemis.UI.Shared.Routing; using Artemis.UI.Shared.Routing;
using PluginDetailsViewModel = Artemis.UI.Screens.Workshop.Plugins.PluginDetailsViewModel; using PluginDetailsViewModel = Artemis.UI.Screens.Workshop.Plugins.PluginDetailsViewModel;

View File

@ -19,7 +19,7 @@ public partial class RootView : ReactiveUserControl<RootViewModel>
{ {
try try
{ {
Dispatcher.UIThread.Invoke(() => RootFrame.NavigateFromObject(viewModel)); RootFrame.NavigateFromObject(viewModel);
} }
catch (Exception) catch (Exception)
{ {

View File

@ -18,7 +18,7 @@ public partial class SettingsView : ReactiveUserControl<SettingsViewModel>
private void Navigate(ViewModelBase viewModel) private void Navigate(ViewModelBase viewModel)
{ {
Dispatcher.UIThread.Invoke(() => TabFrame.NavigateFromObject(viewModel)); TabFrame.NavigateFromObject(viewModel);
} }
private void NavigationView_OnBackRequested(object? sender, NavigationViewBackRequestedEventArgs e) private void NavigationView_OnBackRequested(object? sender, NavigationViewBackRequestedEventArgs e)

View File

@ -17,17 +17,13 @@ public partial class ReleasesTabView : ReactiveUserControl<ReleasesTabViewModel>
private void Navigate(ViewModelBase viewModel) private void Navigate(ViewModelBase viewModel)
{ {
Dispatcher.UIThread.Invoke(() => try
{ {
try ReleaseFrame.NavigateFromObject(viewModel);
{ }
ReleaseFrame.NavigateFromObject(viewModel); catch (Exception)
} {
catch (Exception) // ignored
{ }
// ignored
}
});
} }
} }

View File

@ -1,4 +1,5 @@
using System; using System;
using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Artemis.Core; using Artemis.Core;
using Artemis.UI.Shared; using Artemis.UI.Shared;
@ -12,12 +13,12 @@ public class EntryInfoViewModel : ViewModelBase
private readonly INotificationService _notificationService; private readonly INotificationService _notificationService;
public IEntryDetails Entry { get; } public IEntryDetails Entry { get; }
public DateTimeOffset? UpdatedAt { get; } public DateTimeOffset? UpdatedAt { get; }
public EntryInfoViewModel(IEntryDetails entry, INotificationService notificationService) public EntryInfoViewModel(IEntryDetails entry, INotificationService notificationService)
{ {
_notificationService = notificationService; _notificationService = notificationService;
Entry = entry; Entry = entry;
UpdatedAt = Entry.LatestRelease?.CreatedAt ?? Entry.CreatedAt; UpdatedAt = Entry.Releases.Any() ? Entry.Releases.Max(r => r.CreatedAt) : Entry.CreatedAt;
} }
public async Task CopyShareLink() public async Task CopyShareLink()

View File

@ -6,6 +6,7 @@
xmlns:converters="clr-namespace:Artemis.UI.Converters" xmlns:converters="clr-namespace:Artemis.UI.Converters"
xmlns:sharedConverters="clr-namespace:Artemis.UI.Shared.Converters;assembly=Artemis.UI.Shared" xmlns:sharedConverters="clr-namespace:Artemis.UI.Shared.Converters;assembly=Artemis.UI.Shared"
xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia" xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
xmlns:workshop="clr-namespace:Artemis.WebClient.Workshop;assembly=Artemis.WebClient.Workshop"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Artemis.UI.Screens.Workshop.Entries.Details.EntryReleasesView" x:Class="Artemis.UI.Screens.Workshop.Entries.Details.EntryReleasesView"
x:DataType="details:EntryReleasesViewModel"> x:DataType="details:EntryReleasesViewModel">
@ -14,39 +15,74 @@
<sharedConverters:BytesToStringConverter x:Key="BytesToStringConverter" /> <sharedConverters:BytesToStringConverter x:Key="BytesToStringConverter" />
</UserControl.Resources> </UserControl.Resources>
<StackPanel> <StackPanel>
<TextBlock Theme="{StaticResource SubtitleTextBlockStyle}">Latest release</TextBlock> <TextBlock Theme="{StaticResource SubtitleTextBlockStyle}">Releases</TextBlock>
<Border Classes="card-separator" /> <Border Classes="card-separator" />
<Button HorizontalAlignment="Stretch"
<Button Margin="0 0 0 5"
HorizontalAlignment="Stretch"
HorizontalContentAlignment="Stretch" HorizontalContentAlignment="Stretch"
Command="{CompiledBinding DownloadLatestRelease}"> Command="{CompiledBinding NavigateToRelease}"
CommandParameter="{CompiledBinding LatestRelease}">
<Grid ColumnDefinitions="Auto,*"> <Grid ColumnDefinitions="Auto,*">
<!-- Icon --> <!-- Icon -->
<Border Grid.Column="0" <avalonia:MaterialIcon Grid.Column="0"
CornerRadius="4" Margin="0 6"
Background="{StaticResource SystemAccentColor}" Width="50"
VerticalAlignment="Center" Height="50"
Margin="0 6" Kind="BoxStar">
Width="50" </avalonia:MaterialIcon>
Height="50"
ClipToBounds="True">
<avalonia:MaterialIcon Kind="Download"></avalonia:MaterialIcon>
</Border>
<!-- Body --> <!-- Body -->
<StackPanel Grid.Column="1" Margin="10 0" VerticalAlignment="Center"> <StackPanel Grid.Column="1" Margin="10 0" VerticalAlignment="Center">
<TextBlock Text="{CompiledBinding Entry.LatestRelease.Version, FallbackValue=Version}"></TextBlock> <TextBlock Text="{CompiledBinding LatestRelease.Version, FallbackValue=Version}"></TextBlock>
<TextBlock Classes="subtitle"> <TextBlock Classes="subtitle">
<avalonia:MaterialIcon Kind="BoxOutline" /> <avalonia:MaterialIcon Kind="File" />
<Run Text="{CompiledBinding Entry.LatestRelease.DownloadSize, Converter={StaticResource BytesToStringConverter}, Mode=OneWay}"></Run> <Run Text="{CompiledBinding LatestRelease.DownloadSize, Converter={StaticResource BytesToStringConverter}, Mode=OneWay}"></Run>
</TextBlock> </TextBlock>
<TextBlock Classes="subtitle" <TextBlock Classes="subtitle"
ToolTip.Tip="{CompiledBinding Entry.LatestRelease.CreatedAt, Converter={StaticResource DateTimeConverter}}"> ToolTip.Tip="{CompiledBinding LatestRelease.CreatedAt, Converter={StaticResource DateTimeConverter}}">
<avalonia:MaterialIcon Kind="Calendar" /> <avalonia:MaterialIcon Kind="Calendar" />
<Run>Created</Run> <Run>Created</Run>
<Run Text="{CompiledBinding Entry.LatestRelease.CreatedAt, Converter={StaticResource DateTimeConverter}, ConverterParameter='humanize'}"></Run> <Run Text="{CompiledBinding LatestRelease.CreatedAt, Converter={StaticResource DateTimeConverter}, ConverterParameter='humanize'}"></Run>
</TextBlock> </TextBlock>
</StackPanel> </StackPanel>
</Grid> </Grid>
</Button> </Button>
<ItemsControl ItemsSource="{CompiledBinding OtherReleases}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Spacing="5" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="workshop:IRelease">
<Button HorizontalAlignment="Stretch"
HorizontalContentAlignment="Stretch"
Command="{Binding $parent[details:EntryReleasesView].DataContext.NavigateToRelease}"
CommandParameter="{CompiledBinding}">
<Grid ColumnDefinitions="Auto,*">
<avalonia:MaterialIcon Grid.Column="0"
Margin="0 6"
Width="25"
Height="25"
Kind="Archive">
</avalonia:MaterialIcon>
<!-- Body -->
<StackPanel Grid.Column="1" Margin="10 0" VerticalAlignment="Center">
<TextBlock Text="{CompiledBinding Version, FallbackValue=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>
</Grid>
</Button>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel> </StackPanel>
</UserControl> </UserControl>

View File

@ -1,8 +1,11 @@
using System; using System;
using System.Collections.Generic;
using System.Linq;
using System.Reactive; using System.Reactive;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Artemis.UI.Shared; using Artemis.UI.Shared;
using Artemis.UI.Shared.Routing;
using Artemis.UI.Shared.Services; 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;
@ -19,34 +22,49 @@ public class EntryReleasesViewModel : ViewModelBase
private readonly EntryInstallationHandlerFactory _factory; private readonly EntryInstallationHandlerFactory _factory;
private readonly IWindowService _windowService; private readonly IWindowService _windowService;
private readonly INotificationService _notificationService; private readonly INotificationService _notificationService;
private readonly IRouter _router;
public EntryReleasesViewModel(IEntryDetails entry, EntryInstallationHandlerFactory factory, IWindowService windowService, INotificationService notificationService) public EntryReleasesViewModel(IEntryDetails entry, EntryInstallationHandlerFactory factory, IWindowService windowService, INotificationService notificationService, IRouter router)
{ {
_factory = factory; _factory = factory;
_windowService = windowService; _windowService = windowService;
_notificationService = notificationService; _notificationService = notificationService;
_router = router;
Entry = entry; Entry = entry;
LatestRelease = Entry.Releases.MaxBy(r => r.CreatedAt);
OtherReleases = Entry.Releases.OrderByDescending(r => r.CreatedAt).Skip(1).Take(4).Cast<IRelease>().ToList();
DownloadLatestRelease = ReactiveCommand.CreateFromTask(ExecuteDownloadLatestRelease); DownloadLatestRelease = ReactiveCommand.CreateFromTask(ExecuteDownloadLatestRelease);
OnInstallationStarted = Confirm; OnInstallationStarted = Confirm;
NavigateToRelease = ReactiveCommand.CreateFromTask<IRelease>(ExecuteNavigateToRelease);
} }
public IEntryDetails Entry { get; } public IEntryDetails Entry { get; }
public ReactiveCommand<Unit, Unit> DownloadLatestRelease { get; } public IRelease? LatestRelease { get; }
public List<IRelease> OtherReleases { get; }
public Func<IEntryDetails, Task<bool>> OnInstallationStarted { get; set; } public ReactiveCommand<Unit, Unit> DownloadLatestRelease { get; }
public ReactiveCommand<IRelease, Unit> NavigateToRelease { get; }
public Func<IEntryDetails, IRelease, Task<bool>> OnInstallationStarted { get; set; }
public Func<InstalledEntry, Task>? OnInstallationFinished { get; set; } public Func<InstalledEntry, Task>? OnInstallationFinished { get; set; }
private async Task ExecuteNavigateToRelease(IRelease release)
{
await _router.Navigate($"workshop/entries/{Entry.Id}/releases/{release.Id}");
}
private async Task ExecuteDownloadLatestRelease(CancellationToken cancellationToken) private async Task ExecuteDownloadLatestRelease(CancellationToken cancellationToken)
{ {
if (Entry.LatestRelease == null) if (LatestRelease == null)
return; return;
if (await OnInstallationStarted(Entry)) if (await OnInstallationStarted(Entry, LatestRelease))
return; return;
IEntryInstallationHandler installationHandler = _factory.CreateHandler(Entry.EntryType); IEntryInstallationHandler installationHandler = _factory.CreateHandler(Entry.EntryType);
EntryInstallResult result = await installationHandler.InstallAsync(Entry, Entry.LatestRelease, new Progress<StreamProgress>(), cancellationToken); EntryInstallResult result = await installationHandler.InstallAsync(Entry, LatestRelease, new Progress<StreamProgress>(), cancellationToken);
if (result.IsSuccess && result.Entry != null) if (result.IsSuccess && result.Entry != null)
{ {
if (OnInstallationFinished != null) if (OnInstallationFinished != null)
@ -62,13 +80,13 @@ public class EntryReleasesViewModel : ViewModelBase
} }
} }
private async Task<bool> Confirm(IEntryDetails entryDetails) private async Task<bool> Confirm(IEntryDetails entryDetails, IRelease release)
{ {
bool confirm = await _windowService.ShowConfirmContentDialog( bool confirm = await _windowService.ShowConfirmContentDialog(
"Install latest release", "Install latest release",
$"Are you sure you want to download and install version {entryDetails.LatestRelease?.Version} of {entryDetails.Name}?" $"Are you sure you want to download and install version {release.Version} of {entryDetails.Name}?"
); );
return !confirm; return !confirm;
} }
} }

View File

@ -18,7 +18,7 @@ public partial class EntriesView : ReactiveUserControl<EntriesViewModel>
private void Navigate(ViewModelBase viewModel) private void Navigate(ViewModelBase viewModel)
{ {
Dispatcher.UIThread.Invoke(() => TabFrame.NavigateFromObject(viewModel)); TabFrame.NavigateFromObject(viewModel);
} }
private void NavigationView_OnBackRequested(object? sender, NavigationViewBackRequestedEventArgs e) private void NavigationView_OnBackRequested(object? sender, NavigationViewBackRequestedEventArgs e)

View File

@ -53,8 +53,8 @@ public partial class EntriesViewModel : RoutableHostScreen<RoutableScreen>
public void GoBack() public void GoBack()
{ {
if (ViewingDetails) if (ViewingDetails && SelectedTab != null)
_router.GoBack(); _router.Navigate(SelectedTab.Path);
else else
_router.Navigate("workshop"); _router.Navigate("workshop");
} }

View File

@ -0,0 +1,57 @@
<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:list="clr-namespace:Artemis.UI.Screens.Workshop.Entries.List"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Artemis.UI.Screens.Workshop.Entries.List.EntryListView"
x:DataType="list:EntryListViewModel">
<UserControl.Styles>
<Styles>
<Style Selector="StackPanel.empty-state > TextBlock">
<Setter Property="TextAlignment" Value="Center"></Setter>
<Setter Property="TextWrapping" Value="Wrap"></Setter>
</Style>
</Styles>
</UserControl.Styles>
<Grid ColumnDefinitions="300,*" RowDefinitions="Auto,*,Auto">
<StackPanel Grid.Column="0" Grid.RowSpan="3" Margin="0 0 10 0" VerticalAlignment="Top">
<Border Classes="card" VerticalAlignment="Stretch">
<StackPanel>
<TextBlock Theme="{StaticResource SubtitleTextBlockStyle}">Categories</TextBlock>
<Border Classes="card-separator" />
<ContentControl Content="{CompiledBinding CategoriesViewModel}"></ContentControl>
</StackPanel>
</Border>
</StackPanel>
<ProgressBar Grid.Column="1" Grid.Row="0" VerticalAlignment="Top" Margin="0 0 20 0" IsVisible="{CompiledBinding FetchingMore}" IsIndeterminate="True" />
<ContentControl Grid.Column="1" Grid.Row="0" Margin="0 0 20 8" Content="{CompiledBinding InputViewModel}" />
<ScrollViewer Name="EntriesScrollViewer" Grid.Column="1" Grid.Row="1" ScrollChanged="ScrollViewer_OnScrollChanged" Offset="{CompiledBinding ScrollOffset}">
<ItemsControl ItemsSource="{CompiledBinding Entries}" Margin="0 0 20 0">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<VirtualizingStackPanel />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<ContentControl Content="{CompiledBinding}" Margin="0 0 0 5"></ContentControl>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
<Panel Grid.Column="1" Grid.Row="1" IsVisible="{CompiledBinding !Initializing}">
<StackPanel IsVisible="{CompiledBinding !Entries.Count}" Margin="0 50 0 0" Classes="empty-state">
<TextBlock Theme="{StaticResource TitleTextBlockStyle}">Looks like your current filters gave no results</TextBlock>
<TextBlock>
<Run>Modify or clear your filters to view other entries</Run>
</TextBlock>
<Lottie Path="/Assets/Animations/empty.json" RepeatCount="1" Width="350" Height="350"></Lottie>
</StackPanel>
</Panel>
</Grid>
</UserControl>

View File

@ -1,38 +1,30 @@
using System;
using System.Reactive.Disposables;
using System.Threading; using System.Threading;
using Artemis.UI.Shared.Routing;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.ReactiveUI; using Avalonia.ReactiveUI;
using Avalonia.Threading;
using ReactiveUI; using ReactiveUI;
namespace Artemis.UI.Screens.Workshop.Entries.Tabs; namespace Artemis.UI.Screens.Workshop.Entries.List;
public partial class LayoutListView : ReactiveUserControl<LayoutListViewModel> public partial class EntryListView : ReactiveUserControl<EntryListViewModel>
{ {
public LayoutListView() public EntryListView()
{ {
InitializeComponent(); InitializeComponent();
EntriesScrollViewer.SizeChanged += (_, _) => UpdateEntriesPerFetch(); EntriesScrollViewer.SizeChanged += (_, _) => UpdateEntriesPerFetch();
this.WhenActivated(d => this.WhenActivated(_ => UpdateEntriesPerFetch());
{
UpdateEntriesPerFetch();
ViewModel.WhenAnyValue(vm => vm.Screen).WhereNotNull().Subscribe(Navigate).DisposeWith(d);
});
} }
private void ScrollViewer_OnScrollChanged(object? sender, ScrollChangedEventArgs e) private void ScrollViewer_OnScrollChanged(object? sender, ScrollChangedEventArgs e)
{ {
if (ViewModel == null)
return;
// When near the bottom of EntriesScrollViewer, call FetchMore on the view model // When near the bottom of EntriesScrollViewer, call FetchMore on the view model
if (EntriesScrollViewer.Offset.Y != 0 && EntriesScrollViewer.Extent.Height - (EntriesScrollViewer.Viewport.Height + EntriesScrollViewer.Offset.Y) < 100) if (EntriesScrollViewer.Offset.Y != 0 && EntriesScrollViewer.Extent.Height - (EntriesScrollViewer.Viewport.Height + EntriesScrollViewer.Offset.Y) < 100)
ViewModel?.FetchMore(CancellationToken.None); ViewModel.FetchMore(CancellationToken.None);
}
private void Navigate(RoutableScreen viewModel) ViewModel.ScrollOffset = EntriesScrollViewer.Offset;
{
Dispatcher.UIThread.Invoke(() => RouterFrame.NavigateFromObject(viewModel), DispatcherPriority.ApplicationIdle);
} }
private void UpdateEntriesPerFetch() private void UpdateEntriesPerFetch()

View File

@ -6,6 +6,7 @@ using System.Reactive.Disposables;
using System.Reactive.Linq; using System.Reactive.Linq;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Artemis.UI.Extensions;
using Artemis.UI.Screens.Workshop.Categories; using Artemis.UI.Screens.Workshop.Categories;
using Artemis.UI.Shared.Routing; using Artemis.UI.Shared.Routing;
using Artemis.UI.Shared.Services; using Artemis.UI.Shared.Services;
@ -15,29 +16,28 @@ using DynamicData;
using PropertyChanged.SourceGenerator; using PropertyChanged.SourceGenerator;
using ReactiveUI; using ReactiveUI;
using StrawberryShake; using StrawberryShake;
using Vector = Avalonia.Vector;
namespace Artemis.UI.Screens.Workshop.Entries.List; namespace Artemis.UI.Screens.Workshop.Entries.List;
public abstract partial class EntryListViewModel : RoutableHostScreen<RoutableScreen> public partial class EntryListViewModel : RoutableScreen
{ {
private readonly SourceList<IEntrySummary> _entries = new(); private readonly SourceList<IEntrySummary> _entries = new();
private readonly INotificationService _notificationService; private readonly INotificationService _notificationService;
private readonly IWorkshopClient _workshopClient; private readonly IWorkshopClient _workshopClient;
private readonly string _route;
private IGetEntriesv2_EntriesV2_PageInfo? _currentPageInfo; private IGetEntriesv2_EntriesV2_PageInfo? _currentPageInfo;
[Notify] private bool _initializing = true; [Notify] private bool _initializing = true;
[Notify] private bool _fetchingMore; [Notify] private bool _fetchingMore;
[Notify] private int _entriesPerFetch; [Notify] private int _entriesPerFetch;
[Notify] private Vector _scrollOffset;
protected EntryListViewModel(string route, protected EntryListViewModel(IWorkshopClient workshopClient,
IWorkshopClient workshopClient,
CategoriesViewModel categoriesViewModel, CategoriesViewModel categoriesViewModel,
EntryListInputViewModel entryListInputViewModel, EntryListInputViewModel entryListInputViewModel,
INotificationService notificationService, INotificationService notificationService,
Func<IEntrySummary, EntryListItemViewModel> getEntryListViewModel) Func<IEntrySummary, EntryListItemViewModel> getEntryListViewModel)
{ {
_route = route;
_workshopClient = workshopClient; _workshopClient = workshopClient;
_notificationService = notificationService; _notificationService = notificationService;
@ -50,37 +50,31 @@ public abstract partial class EntryListViewModel : RoutableHostScreen<RoutableSc
.Subscribe(); .Subscribe();
Entries = entries; Entries = entries;
// Respond to filter query input changes
this.WhenActivated(d => this.WhenActivated(d =>
{ {
// Respond to filter query input changes
InputViewModel.WhenAnyValue(vm => vm.Search).Skip(1).Throttle(TimeSpan.FromMilliseconds(200)).Subscribe(_ => Reset()).DisposeWith(d); InputViewModel.WhenAnyValue(vm => vm.Search).Skip(1).Throttle(TimeSpan.FromMilliseconds(200)).Subscribe(_ => Reset()).DisposeWith(d);
CategoriesViewModel.WhenAnyValue(vm => vm.CategoryFilters).Skip(1).Subscribe(_ => Reset()).DisposeWith(d); CategoriesViewModel.WhenAnyValue(vm => vm.CategoryFilters).Skip(1).Subscribe(_ => Reset()).DisposeWith(d);
}); });
// Load entries when the view model is first activated
this.WhenActivatedAsync(async _ =>
{
if (_entries.Count == 0)
{
await Task.Delay(250);
await FetchMore(CancellationToken.None);
Initializing = false;
}
});
} }
public CategoriesViewModel CategoriesViewModel { get; } public CategoriesViewModel CategoriesViewModel { get; }
public EntryListInputViewModel InputViewModel { get; } public EntryListInputViewModel InputViewModel { get; }
public EntryType? EntryType { get; set; }
public ReadOnlyObservableCollection<EntryListItemViewModel> Entries { get; } public ReadOnlyObservableCollection<EntryListItemViewModel> Entries { get; }
public override async Task OnNavigating(NavigationArguments args, CancellationToken cancellationToken)
{
if (_entries.Count == 0)
{
await Task.Delay(250, cancellationToken);
await FetchMore(cancellationToken);
Initializing = false;
}
}
public override Task OnClosing(NavigationArguments args)
{
// Clear search if not navigating to a child
if (!args.Path.StartsWith(_route))
InputViewModel.ClearLastSearch();
return base.OnClosing(args);
}
public async Task FetchMore(CancellationToken cancellationToken) public async Task FetchMore(CancellationToken cancellationToken)
{ {
if (FetchingMore || _currentPageInfo != null && !_currentPageInfo.HasNextPage) if (FetchingMore || _currentPageInfo != null && !_currentPageInfo.HasNextPage)
@ -119,12 +113,19 @@ public abstract partial class EntryListViewModel : RoutableHostScreen<RoutableSc
} }
} }
protected virtual EntryFilterInput GetFilter() private EntryFilterInput GetFilter()
{ {
return new EntryFilterInput {And = CategoriesViewModel.CategoryFilters}; return new EntryFilterInput
{
And =
[
new EntryFilterInput {EntryType = new EntryTypeOperationFilterInput {Eq = EntryType}},
..CategoriesViewModel.CategoryFilters ?? []
]
};
} }
protected virtual IReadOnlyList<EntrySortInput> GetSort() private IReadOnlyList<EntrySortInput> GetSort()
{ {
// Sort by created at // Sort by created at
if (InputViewModel.SortBy == 1) if (InputViewModel.SortBy == 1)

View File

@ -1,67 +0,0 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:tabs="clr-namespace:Artemis.UI.Screens.Workshop.Entries.Tabs"
xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
xmlns:ui="clr-namespace:Artemis.UI"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Artemis.UI.Screens.Workshop.Entries.Tabs.LayoutListView"
x:DataType="tabs:LayoutListViewModel">
<UserControl.Styles>
<Styles>
<Style Selector="StackPanel.empty-state > TextBlock">
<Setter Property="TextAlignment" Value="Center"></Setter>
<Setter Property="TextWrapping" Value="Wrap"></Setter>
</Style>
</Styles>
</UserControl.Styles>
<Panel>
<Grid ColumnDefinitions="300,*" RowDefinitions="Auto,*,Auto" IsVisible="{CompiledBinding Screen, Converter={x:Static ObjectConverters.IsNull}}">
<StackPanel Grid.Column="0" Grid.RowSpan="3" Margin="0 0 10 0" VerticalAlignment="Top">
<Border Classes="card" VerticalAlignment="Stretch">
<StackPanel>
<TextBlock Theme="{StaticResource SubtitleTextBlockStyle}">Categories</TextBlock>
<Border Classes="card-separator" />
<ContentControl Content="{CompiledBinding CategoriesViewModel}"></ContentControl>
</StackPanel>
</Border>
</StackPanel>
<ProgressBar Grid.Column="1" Grid.Row="0" VerticalAlignment="Top" Margin="0 0 20 0" IsVisible="{CompiledBinding FetchingMore}" IsIndeterminate="True" />
<ContentControl Grid.Column="1" Grid.Row="0" Margin="0 0 20 8" Content="{CompiledBinding InputViewModel}" />
<ScrollViewer Name="EntriesScrollViewer" Grid.Column="1" Grid.Row="1" ScrollChanged="ScrollViewer_OnScrollChanged">
<ItemsControl ItemsSource="{CompiledBinding Entries}" Margin="0 0 20 0">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<VirtualizingStackPanel />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<ContentControl Content="{CompiledBinding}" Margin="0 0 0 5"></ContentControl>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
<Panel Grid.Column="1" Grid.Row="1" IsVisible="{CompiledBinding !Initializing}">
<StackPanel IsVisible="{CompiledBinding !Entries.Count}" Margin="0 50 0 0" Classes="empty-state">
<TextBlock Theme="{StaticResource TitleTextBlockStyle}">Looks like your current filters gave no results</TextBlock>
<TextBlock>
<Run>Modify or clear your filters to view other device layouts</Run>
</TextBlock>
<Lottie Path="/Assets/Animations/empty.json" RepeatCount="1" Width="350" Height="350"></Lottie>
</StackPanel>
</Panel>
</Grid>
<controls:Frame Name="RouterFrame" IsNavigationStackEnabled="False" CacheSize="0" IsVisible="{CompiledBinding Screen, Converter={x:Static ObjectConverters.IsNotNull}}">
<controls:Frame.NavigationPageFactory>
<ui:PageFactory />
</controls:Frame.NavigationPageFactory>
</controls:Frame>
</Panel>
</UserControl>

View File

@ -1,34 +0,0 @@
using System;
using Artemis.UI.Screens.Workshop.Categories;
using Artemis.UI.Screens.Workshop.Entries.List;
using Artemis.UI.Shared.Routing;
using Artemis.UI.Shared.Services;
using Artemis.WebClient.Workshop;
namespace Artemis.UI.Screens.Workshop.Entries.Tabs;
public class LayoutListViewModel : List.EntryListViewModel
{
public LayoutListViewModel(IWorkshopClient workshopClient,
CategoriesViewModel categoriesViewModel,
EntryListInputViewModel entryListInputViewModel,
INotificationService notificationService,
Func<IEntrySummary, EntryListItemViewModel> getEntryListViewModel)
: base("workshop/entries/layouts", workshopClient, categoriesViewModel, entryListInputViewModel, notificationService, getEntryListViewModel)
{
entryListInputViewModel.SearchWatermark = "Search layouts";
}
protected override EntryFilterInput GetFilter()
{
return new EntryFilterInput
{
And = new[]
{
new EntryFilterInput {EntryType = new EntryTypeOperationFilterInput {Eq = EntryType.Layout}},
new EntryFilterInput(){LatestReleaseId = new LongOperationFilterInput {Gt = 0}},
base.GetFilter()
}
};
}
}

View File

@ -1,67 +0,0 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:tabs="clr-namespace:Artemis.UI.Screens.Workshop.Entries.Tabs"
xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
xmlns:ui="clr-namespace:Artemis.UI"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Artemis.UI.Screens.Workshop.Entries.Tabs.PluginListView"
x:DataType="tabs:PluginListViewModel">
<UserControl.Styles>
<Styles>
<Style Selector="StackPanel.empty-state > TextBlock">
<Setter Property="TextAlignment" Value="Center"></Setter>
<Setter Property="TextWrapping" Value="Wrap"></Setter>
</Style>
</Styles>
</UserControl.Styles>
<Panel>
<Grid ColumnDefinitions="300,*" RowDefinitions="Auto,*,Auto" IsVisible="{CompiledBinding Screen, Converter={x:Static ObjectConverters.IsNull}}">
<StackPanel Grid.Column="0" Grid.RowSpan="3" Margin="0 0 10 0" VerticalAlignment="Top">
<Border Classes="card" VerticalAlignment="Stretch">
<StackPanel>
<TextBlock Theme="{StaticResource SubtitleTextBlockStyle}">Categories</TextBlock>
<Border Classes="card-separator" />
<ContentControl Content="{CompiledBinding CategoriesViewModel}"></ContentControl>
</StackPanel>
</Border>
</StackPanel>
<ProgressBar Grid.Column="1" Grid.Row="0" VerticalAlignment="Top" Margin="0 0 20 0" IsVisible="{CompiledBinding FetchingMore}" IsIndeterminate="True" />
<ContentControl Grid.Column="1" Grid.Row="0" Margin="0 0 20 8" Content="{CompiledBinding InputViewModel}" />
<ScrollViewer Name="EntriesScrollViewer" Grid.Column="1" Grid.Row="1" ScrollChanged="ScrollViewer_OnScrollChanged">
<ItemsControl ItemsSource="{CompiledBinding Entries}" Margin="0 0 20 0">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<VirtualizingStackPanel />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<ContentControl Content="{CompiledBinding}" Margin="0 0 0 5"></ContentControl>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
<Panel Grid.Column="1" Grid.Row="1" IsVisible="{CompiledBinding !Initializing}">
<StackPanel IsVisible="{CompiledBinding !Entries.Count}" Margin="0 50 0 0" Classes="empty-state">
<TextBlock Theme="{StaticResource TitleTextBlockStyle}">Looks like your current filters gave no results</TextBlock>
<TextBlock>
<Run>Modify or clear your filters to view other plugins</Run>
</TextBlock>
<Lottie Path="/Assets/Animations/empty.json" RepeatCount="1" Width="350" Height="350"></Lottie>
</StackPanel>
</Panel>
</Grid>
<controls:Frame Name="RouterFrame" IsNavigationStackEnabled="False" CacheSize="0" IsVisible="{CompiledBinding Screen, Converter={x:Static ObjectConverters.IsNotNull}}">
<controls:Frame.NavigationPageFactory>
<ui:PageFactory />
</controls:Frame.NavigationPageFactory>
</controls:Frame>
</Panel>
</UserControl>

View File

@ -1,43 +0,0 @@
using System;
using System.Reactive.Disposables;
using System.Threading;
using Artemis.UI.Shared.Routing;
using Avalonia.Controls;
using Avalonia.ReactiveUI;
using Avalonia.Threading;
using ReactiveUI;
namespace Artemis.UI.Screens.Workshop.Entries.Tabs;
public partial class PluginListView : ReactiveUserControl<PluginListViewModel>
{
public PluginListView()
{
InitializeComponent();
EntriesScrollViewer.SizeChanged += (_, _) => UpdateEntriesPerFetch();
this.WhenActivated(d =>
{
UpdateEntriesPerFetch();
ViewModel.WhenAnyValue(vm => vm.Screen).WhereNotNull().Subscribe(Navigate).DisposeWith(d);
});
}
private void ScrollViewer_OnScrollChanged(object? sender, ScrollChangedEventArgs e)
{
// When near the bottom of EntriesScrollViewer, call FetchMore on the view model
if (EntriesScrollViewer.Offset.Y != 0 && EntriesScrollViewer.Extent.Height - (EntriesScrollViewer.Viewport.Height + EntriesScrollViewer.Offset.Y) < 100)
ViewModel?.FetchMore(CancellationToken.None);
}
private void Navigate(RoutableScreen viewModel)
{
Dispatcher.UIThread.Invoke(() => RouterFrame.NavigateFromObject(viewModel), DispatcherPriority.ApplicationIdle);
}
private void UpdateEntriesPerFetch()
{
if (ViewModel != null)
ViewModel.EntriesPerFetch = (int) (EntriesScrollViewer.Viewport.Height / 120);
}
}

View File

@ -1,33 +0,0 @@
using System;
using Artemis.UI.Screens.Workshop.Categories;
using Artemis.UI.Screens.Workshop.Entries.List;
using Artemis.UI.Shared.Routing;
using Artemis.UI.Shared.Services;
using Artemis.WebClient.Workshop;
namespace Artemis.UI.Screens.Workshop.Entries.Tabs;
public class PluginListViewModel : EntryListViewModel
{
public PluginListViewModel(IWorkshopClient workshopClient,
CategoriesViewModel categoriesViewModel,
EntryListInputViewModel entryListInputViewModel,
INotificationService notificationService,
Func<IEntrySummary, EntryListItemViewModel> getEntryListViewModel)
: base("workshop/entries/plugins", workshopClient, categoriesViewModel, entryListInputViewModel, notificationService, getEntryListViewModel)
{
entryListInputViewModel.SearchWatermark = "Search plugins";
}
protected override EntryFilterInput GetFilter()
{
return new EntryFilterInput
{
And = new[]
{
new EntryFilterInput {EntryType = new EntryTypeOperationFilterInput {Eq = EntryType.Plugin}},
base.GetFilter()
}
};
}
}

View File

@ -1,67 +0,0 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:tabs="clr-namespace:Artemis.UI.Screens.Workshop.Entries.Tabs"
xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
xmlns:ui="clr-namespace:Artemis.UI"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Artemis.UI.Screens.Workshop.Entries.Tabs.ProfileListView"
x:DataType="tabs:ProfileListViewModel">
<UserControl.Styles>
<Styles>
<Style Selector="StackPanel.empty-state > TextBlock">
<Setter Property="TextAlignment" Value="Center"></Setter>
<Setter Property="TextWrapping" Value="Wrap"></Setter>
</Style>
</Styles>
</UserControl.Styles>
<Panel>
<Grid ColumnDefinitions="300,*" RowDefinitions="Auto,*,Auto" IsVisible="{CompiledBinding Screen, Converter={x:Static ObjectConverters.IsNull}}">
<StackPanel Grid.Column="0" Grid.RowSpan="3" Margin="0 0 10 0" VerticalAlignment="Top">
<Border Classes="card" VerticalAlignment="Stretch">
<StackPanel>
<TextBlock Theme="{StaticResource SubtitleTextBlockStyle}">Categories</TextBlock>
<Border Classes="card-separator" />
<ContentControl Content="{CompiledBinding CategoriesViewModel}"></ContentControl>
</StackPanel>
</Border>
</StackPanel>
<ProgressBar Grid.Column="1" Grid.Row="0" VerticalAlignment="Top" Margin="0 0 20 0" IsVisible="{CompiledBinding FetchingMore}" IsIndeterminate="True" />
<ContentControl Grid.Column="1" Grid.Row="0" Margin="0 0 20 8" Content="{CompiledBinding InputViewModel}" />
<ScrollViewer Name="EntriesScrollViewer" Grid.Column="1" Grid.Row="1" ScrollChanged="ScrollViewer_OnScrollChanged">
<ItemsControl ItemsSource="{CompiledBinding Entries}" Margin="0 0 20 0">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<VirtualizingStackPanel />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<ContentControl Content="{CompiledBinding}" Margin="0 0 0 5"></ContentControl>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
<Panel Grid.Column="1" Grid.Row="1" IsVisible="{CompiledBinding !Initializing}">
<StackPanel IsVisible="{CompiledBinding !Entries.Count}" Margin="0 50 0 0" Classes="empty-state">
<TextBlock Theme="{StaticResource TitleTextBlockStyle}">Looks like your current filters gave no results</TextBlock>
<TextBlock>
<Run>Modify or clear your filters to view some awesome profiles</Run>
</TextBlock>
<Lottie Path="/Assets/Animations/empty.json" RepeatCount="1" Width="350" Height="350"></Lottie>
</StackPanel>
</Panel>
</Grid>
<controls:Frame Name="RouterFrame" IsNavigationStackEnabled="False" CacheSize="0" IsVisible="{CompiledBinding Screen, Converter={x:Static ObjectConverters.IsNotNull}}">
<controls:Frame.NavigationPageFactory>
<ui:PageFactory />
</controls:Frame.NavigationPageFactory>
</controls:Frame>
</Panel>
</UserControl>

View File

@ -1,43 +0,0 @@
using System;
using System.Reactive.Disposables;
using System.Threading;
using Artemis.UI.Shared.Routing;
using Avalonia.Controls;
using Avalonia.ReactiveUI;
using Avalonia.Threading;
using ReactiveUI;
namespace Artemis.UI.Screens.Workshop.Entries.Tabs;
public partial class ProfileListView : ReactiveUserControl<ProfileListViewModel>
{
public ProfileListView()
{
InitializeComponent();
EntriesScrollViewer.SizeChanged += (_, _) => UpdateEntriesPerFetch();
this.WhenActivated(d =>
{
UpdateEntriesPerFetch();
ViewModel.WhenAnyValue(vm => vm.Screen).WhereNotNull().Subscribe(Navigate).DisposeWith(d);
});
}
private void ScrollViewer_OnScrollChanged(object? sender, ScrollChangedEventArgs e)
{
// When near the bottom of EntriesScrollViewer, call FetchMore on the view model
if (EntriesScrollViewer.Offset.Y != 0 && EntriesScrollViewer.Extent.Height - (EntriesScrollViewer.Viewport.Height + EntriesScrollViewer.Offset.Y) < 100)
ViewModel?.FetchMore(CancellationToken.None);
}
private void Navigate(RoutableScreen viewModel)
{
Dispatcher.UIThread.Invoke(() => RouterFrame.NavigateFromObject(viewModel), DispatcherPriority.ApplicationIdle);
}
private void UpdateEntriesPerFetch()
{
if (ViewModel != null)
ViewModel.EntriesPerFetch = (int) (EntriesScrollViewer.Viewport.Height / 120);
}
}

View File

@ -1,33 +0,0 @@
using System;
using Artemis.UI.Screens.Workshop.Categories;
using Artemis.UI.Screens.Workshop.Entries.List;
using Artemis.UI.Shared.Routing;
using Artemis.UI.Shared.Services;
using Artemis.WebClient.Workshop;
namespace Artemis.UI.Screens.Workshop.Entries.Tabs;
public class ProfileListViewModel : List.EntryListViewModel
{
public ProfileListViewModel(IWorkshopClient workshopClient,
CategoriesViewModel categoriesViewModel,
EntryListInputViewModel entryListInputViewModel,
INotificationService notificationService,
Func<IEntrySummary, EntryListItemViewModel> getEntryListViewModel)
: base("workshop/entries/profiles", workshopClient, categoriesViewModel, entryListInputViewModel, notificationService, getEntryListViewModel)
{
entryListInputViewModel.SearchWatermark = "Search profiles";
}
protected override EntryFilterInput GetFilter()
{
return new EntryFilterInput
{
And = new[]
{
new EntryFilterInput {EntryType = new EntryTypeOperationFilterInput {Eq = EntryType.Profile}},
base.GetFilter()
}
};
}
}

View File

@ -12,7 +12,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.LatestRelease, Converter={x:Static ObjectConverters.IsNotNull}}"> <Border Classes="card" VerticalAlignment="Top" IsVisible="{CompiledBinding Entry.Releases.Count}">
<ContentControl Content="{CompiledBinding EntryReleasesViewModel}" /> <ContentControl Content="{CompiledBinding EntryReleasesViewModel}" />
</Border> </Border>
</StackPanel> </StackPanel>

View File

@ -0,0 +1,16 @@
<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:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
xmlns:ui="clr-namespace:Artemis.UI"
xmlns:layout="clr-namespace:Artemis.UI.Screens.Workshop.Layout"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Artemis.UI.Screens.Workshop.Layout.LayoutListView"
x:DataType="layout:LayoutListViewModel">
<controls:Frame Name="RouterFrame" IsNavigationStackEnabled="False" CacheSize="0">
<controls:Frame.NavigationPageFactory>
<ui:PageFactory />
</controls:Frame.NavigationPageFactory>
</controls:Frame>
</UserControl>

View File

@ -0,0 +1,25 @@
using System;
using System.Reactive.Disposables;
using Artemis.UI.Shared.Routing;
using Avalonia.ReactiveUI;
using Avalonia.Threading;
using ReactiveUI;
namespace Artemis.UI.Screens.Workshop.Layout;
public partial class LayoutListView : ReactiveUserControl<LayoutListViewModel>
{
public LayoutListView()
{
InitializeComponent();
this.WhenActivated(d =>
{
ViewModel.WhenAnyValue(vm => vm.Screen).Subscribe(Navigate).DisposeWith(d);
});
}
private void Navigate(RoutableScreen? viewModel)
{
RouterFrame.NavigateFromObject(viewModel ?? ViewModel?.EntryListViewModel);
}
}

View File

@ -0,0 +1,16 @@
using Artemis.UI.Screens.Workshop.Entries.List;
using Artemis.UI.Shared.Routing;
using Artemis.WebClient.Workshop;
namespace Artemis.UI.Screens.Workshop.Layout;
public class LayoutListViewModel : RoutableHostScreen<RoutableScreen>
{
public EntryListViewModel EntryListViewModel { get; }
public LayoutListViewModel(EntryListViewModel entryListViewModel)
{
EntryListViewModel = entryListViewModel;
EntryListViewModel.EntryType = EntryType.Layout;
}
}

View File

@ -18,9 +18,9 @@ public partial class WorkshopLibraryView : ReactiveUserControl<WorkshopLibraryVi
private void Navigate(ViewModelBase viewModel) private void Navigate(ViewModelBase viewModel)
{ {
Dispatcher.UIThread.Invoke(() => TabFrame.NavigateFromObject(viewModel)); TabFrame.NavigateFromObject(viewModel);
} }
private void NavigationView_OnBackRequested(object? sender, NavigationViewBackRequestedEventArgs e) private void NavigationView_OnBackRequested(object? sender, NavigationViewBackRequestedEventArgs e)
{ {
ViewModel?.GoBack(); ViewModel?.GoBack();

View File

@ -29,7 +29,7 @@
</StackPanel> </StackPanel>
</Border> </Border>
<Border Classes="card" VerticalAlignment="Top" IsVisible="{CompiledBinding Entry.LatestRelease, Converter={x:Static ObjectConverters.IsNotNull}}"> <Border Classes="card" VerticalAlignment="Top" IsVisible="{CompiledBinding Entry.Releases.Count}">
<ContentControl Content="{CompiledBinding EntryReleasesViewModel}" /> <ContentControl Content="{CompiledBinding EntryReleasesViewModel}" />
</Border> </Border>
</StackPanel> </StackPanel>
@ -49,7 +49,13 @@
<TextBlock Theme="{StaticResource SubtitleTextBlockStyle}">Used by these profiles</TextBlock> <TextBlock Theme="{StaticResource SubtitleTextBlockStyle}">Used by these profiles</TextBlock>
<Border Classes="card-separator" /> <Border Classes="card-separator" />
<ScrollViewer> <ScrollViewer>
<ItemsControl ItemsSource="{CompiledBinding Dependants}"></ItemsControl> <ItemsControl ItemsSource="{CompiledBinding Dependants}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Spacing="5"></StackPanel>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
</ScrollViewer> </ScrollViewer>
</StackPanel> </StackPanel>
</Border> </Border>

View File

@ -33,7 +33,7 @@ public partial class PluginDetailsViewModel : RoutableScreen<WorkshopDetailParam
[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,
IWindowService windowService, IWindowService windowService,
IPluginManagementService pluginManagementService, IPluginManagementService pluginManagementService,
@ -72,18 +72,21 @@ public partial class PluginDetailsViewModel : RoutableScreen<WorkshopDetailParam
EntryReleasesViewModel.OnInstallationStarted = OnInstallationStarted; EntryReleasesViewModel.OnInstallationStarted = OnInstallationStarted;
EntryReleasesViewModel.OnInstallationFinished = OnInstallationFinished; EntryReleasesViewModel.OnInstallationFinished = OnInstallationFinished;
} }
IReadOnlyList<IEntrySummary>? dependants = (await _client.GetDependantEntries.ExecuteAsync(entryId, 0, 25, cancellationToken)).Data?.Entries?.Items; IReadOnlyList<IEntrySummary>? dependants = (await _client.GetDependantEntries.ExecuteAsync(entryId, 0, 25, cancellationToken)).Data?.Entries?.Items;
Dependants = dependants != null && dependants.Any() Dependants = dependants != null && dependants.Any()
? new ReadOnlyObservableCollection<EntryListItemViewModel>(new ObservableCollection<EntryListItemViewModel>(dependants.Select(_getEntryListViewModel))) ? new ReadOnlyObservableCollection<EntryListItemViewModel>(new ObservableCollection<EntryListItemViewModel>(dependants
.Select(_getEntryListViewModel)
.OrderByDescending(d => d.Entry.Downloads)
.Take(10)))
: null; : null;
} }
private async Task<bool> OnInstallationStarted(IEntryDetails entryDetails) private async Task<bool> OnInstallationStarted(IEntryDetails entryDetails, IRelease release)
{ {
bool confirm = await _windowService.ShowConfirmContentDialog( bool confirm = await _windowService.ShowConfirmContentDialog(
"Installing plugin", "Installing plugin",
$"You are about to install version {entryDetails.LatestRelease?.Version} of {entryDetails.Name}. \r\n\r\n" + $"You are about to install version {release.Version} of {entryDetails.Name}. \r\n\r\n" +
"Plugins are NOT verified by Artemis and could harm your PC, if you have doubts about a plugin please ask on Discord!", "Plugins are NOT verified by Artemis and could harm your PC, if you have doubts about a plugin please ask on Discord!",
"I trust this plugin, install it" "I trust this plugin, install it"
); );

View File

@ -0,0 +1,16 @@
<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:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
xmlns:ui="clr-namespace:Artemis.UI"
xmlns:plugins="clr-namespace:Artemis.UI.Screens.Workshop.Plugins"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Artemis.UI.Screens.Workshop.Plugins.PluginListView"
x:DataType="plugins:PluginListViewModel">
<controls:Frame Name="RouterFrame" IsNavigationStackEnabled="False" CacheSize="0">
<controls:Frame.NavigationPageFactory>
<ui:PageFactory />
</controls:Frame.NavigationPageFactory>
</controls:Frame>
</UserControl>

View File

@ -0,0 +1,24 @@
using System;
using System.Reactive.Disposables;
using Artemis.UI.Shared.Routing;
using Avalonia.ReactiveUI;
using ReactiveUI;
namespace Artemis.UI.Screens.Workshop.Plugins;
public partial class PluginListView : ReactiveUserControl<PluginListViewModel>
{
public PluginListView()
{
InitializeComponent();
this.WhenActivated(d =>
{
ViewModel.WhenAnyValue(vm => vm.Screen).Subscribe(Navigate).DisposeWith(d);
});
}
private void Navigate(RoutableScreen? viewModel)
{
RouterFrame.NavigateFromObject(viewModel ?? ViewModel?.EntryListViewModel);
}
}

View File

@ -0,0 +1,16 @@
using Artemis.UI.Screens.Workshop.Entries.List;
using Artemis.UI.Shared.Routing;
using Artemis.WebClient.Workshop;
namespace Artemis.UI.Screens.Workshop.Plugins;
public class PluginListViewModel : RoutableHostScreen<RoutableScreen>
{
public EntryListViewModel EntryListViewModel { get; }
public PluginListViewModel(EntryListViewModel entryListViewModel)
{
EntryListViewModel = entryListViewModel;
EntryListViewModel.EntryType = EntryType.Plugin;
}
}

View File

@ -12,7 +12,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.LatestRelease, Converter={x:Static ObjectConverters.IsNotNull}}"> <Border Classes="card" VerticalAlignment="Top" IsVisible="{CompiledBinding Entry.Releases.Count}">
<ContentControl Content="{CompiledBinding EntryReleasesViewModel}" /> <ContentControl Content="{CompiledBinding EntryReleasesViewModel}" />
</Border> </Border>
</StackPanel> </StackPanel>
@ -32,7 +32,13 @@
<TextBlock Theme="{StaticResource SubtitleTextBlockStyle}">Required plugins</TextBlock> <TextBlock Theme="{StaticResource SubtitleTextBlockStyle}">Required plugins</TextBlock>
<Border Classes="card-separator" /> <Border Classes="card-separator" />
<ScrollViewer> <ScrollViewer>
<ItemsControl ItemsSource="{CompiledBinding Dependencies}"></ItemsControl> <ItemsControl ItemsSource="{CompiledBinding Dependencies}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Spacing="5"></StackPanel>
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
</ScrollViewer> </ScrollViewer>
</StackPanel> </StackPanel>
</Border> </Border>

View File

@ -0,0 +1,16 @@
<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:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
xmlns:ui="clr-namespace:Artemis.UI"
xmlns:profile="clr-namespace:Artemis.UI.Screens.Workshop.Profile"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Artemis.UI.Screens.Workshop.Profile.ProfileListView"
x:DataType="profile:ProfileListViewModel">
<controls:Frame Name="RouterFrame" IsNavigationStackEnabled="False" CacheSize="0">
<controls:Frame.NavigationPageFactory>
<ui:PageFactory />
</controls:Frame.NavigationPageFactory>
</controls:Frame>
</UserControl>

View File

@ -0,0 +1,25 @@
using System;
using System.Reactive.Disposables;
using Artemis.UI.Shared.Routing;
using Avalonia.ReactiveUI;
using Avalonia.Threading;
using ReactiveUI;
namespace Artemis.UI.Screens.Workshop.Profile;
public partial class ProfileListView : ReactiveUserControl<ProfileListViewModel>
{
public ProfileListView()
{
InitializeComponent();
this.WhenActivated(d =>
{
ViewModel.WhenAnyValue(vm => vm.Screen).Subscribe(Navigate).DisposeWith(d);
});
}
private void Navigate(RoutableScreen? viewModel)
{
RouterFrame.NavigateFromObject(viewModel ?? ViewModel?.EntryListViewModel);
}
}

View File

@ -0,0 +1,16 @@
using Artemis.UI.Screens.Workshop.Entries.List;
using Artemis.UI.Shared.Routing;
using Artemis.WebClient.Workshop;
namespace Artemis.UI.Screens.Workshop.Profile;
public class ProfileListViewModel : RoutableHostScreen<RoutableScreen>
{
public EntryListViewModel EntryListViewModel { get; }
public ProfileListViewModel(EntryListViewModel entryListViewModel)
{
EntryListViewModel = entryListViewModel;
EntryListViewModel.EntryType = EntryType.Profile;
}
}

View File

@ -8,7 +8,7 @@ using ReactiveUI;
namespace Artemis.UI.Screens.Workshop.SubmissionWizard; namespace Artemis.UI.Screens.Workshop.SubmissionWizard;
public partial class ReleaseWizardView: ReactiveAppWindow<ReleaseWizardViewModel> public partial class ReleaseWizardView : ReactiveAppWindow<ReleaseWizardViewModel>
{ {
public ReleaseWizardView() public ReleaseWizardView()
{ {
@ -25,7 +25,7 @@ public partial class ReleaseWizardView: ReactiveAppWindow<ReleaseWizardViewModel
{ {
try try
{ {
Dispatcher.UIThread.Invoke(() => Frame.NavigateFromObject(viewModel)); Frame.NavigateFromObject(viewModel);
} }
catch (Exception e) catch (Exception e)
{ {

View File

@ -25,7 +25,7 @@ public partial class SubmissionWizardView : ReactiveAppWindow<SubmissionWizardVi
{ {
try try
{ {
Dispatcher.UIThread.Invoke(() => Frame.NavigateFromObject(viewModel)); Frame.NavigateFromObject(viewModel);
} }
catch (Exception e) catch (Exception e)
{ {

View File

@ -2,6 +2,11 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:avalonia="clr-namespace:Markdown.Avalonia;assembly=Markdown.Avalonia" xmlns:avalonia="clr-namespace:Markdown.Avalonia;assembly=Markdown.Avalonia"
xmlns:ctxt="clr-namespace:ColorTextBlock.Avalonia;assembly=ColorTextBlock.Avalonia"> xmlns:ctxt="clr-namespace:ColorTextBlock.Avalonia;assembly=ColorTextBlock.Avalonia">
<Design.PreviewWith>
<avalonia:MarkdownScrollViewer>
Test
</avalonia:MarkdownScrollViewer>
</Design.PreviewWith>
<Style Selector="ScrollViewer > StackPanel"> <Style Selector="ScrollViewer > StackPanel">
<Setter Property="Margin" Value="0 0 15 0"></Setter> <Setter Property="Margin" Value="0 0 15 0"></Setter>
</Style> </Style>

View File

@ -54,12 +54,12 @@ fragment entryDetails on Entry {
categories { categories {
...category ...category
} }
latestRelease {
...release
}
images { images {
...image ...image
} }
releases {
...release
}
} }
fragment release on Release { fragment release on Release {