1
0
mirror of https://github.com/Artemis-RGB/Artemis synced 2025-12-12 21:38:38 +00:00

Core - Prevent double-shutdowns which can cause crashes

Storage - Make the DbContext factory thread safe
Workshop - Replaced pagination with infinite scroll
Workshop - Added supported platforms and admin requirements to plugin details page
This commit is contained in:
RobertBeekman 2024-03-21 20:12:40 +01:00
parent 7e981a61d3
commit da3d47d7b8
27 changed files with 491 additions and 331 deletions

View File

@ -13,6 +13,8 @@ namespace Artemis.Core;
/// </summary> /// </summary>
public static class Utilities public static class Utilities
{ {
private static bool _shuttingDown;
/// <summary> /// <summary>
/// Call this before even initializing the Core to make sure the folders required for operation are in place /// Call this before even initializing the Core to make sure the folders required for operation are in place
/// </summary> /// </summary>
@ -33,7 +35,11 @@ public static class Utilities
/// </summary> /// </summary>
public static void Shutdown() public static void Shutdown()
{ {
if (_shuttingDown)
return;
// Request a graceful shutdown, whatever UI we're running can pick this up // Request a graceful shutdown, whatever UI we're running can pick this up
_shuttingDown = true;
OnShutdownRequested(); OnShutdownRequested();
} }
@ -45,9 +51,13 @@ public static class Utilities
/// <param name="extraArgs">A list of extra arguments to pass to Artemis when restarting</param> /// <param name="extraArgs">A list of extra arguments to pass to Artemis when restarting</param>
public static void Restart(bool elevate, TimeSpan delay, params string[] extraArgs) public static void Restart(bool elevate, TimeSpan delay, params string[] extraArgs)
{ {
if (_shuttingDown)
return;
if (!OperatingSystem.IsWindows() && elevate) if (!OperatingSystem.IsWindows() && elevate)
throw new ArtemisCoreException("Elevation on non-Windows platforms is not supported."); throw new ArtemisCoreException("Elevation on non-Windows platforms is not supported.");
_shuttingDown = true;
OnRestartRequested(new RestartEventArgs(elevate, delay, extraArgs.ToList())); OnRestartRequested(new RestartEventArgs(elevate, delay, extraArgs.ToList()));
} }
@ -106,12 +116,12 @@ public static class Utilities
/// Occurs when the core has requested an application shutdown /// Occurs when the core has requested an application shutdown
/// </summary> /// </summary>
public static event EventHandler? ShutdownRequested; public static event EventHandler? ShutdownRequested;
/// <summary> /// <summary>
/// Occurs when the core has requested an application restart /// Occurs when the core has requested an application restart
/// </summary> /// </summary>
public static event EventHandler<RestartEventArgs>? RestartRequested; public static event EventHandler<RestartEventArgs>? RestartRequested;
/// <summary> /// <summary>
/// Occurs when the core has requested a pending application update to be applied /// Occurs when the core has requested a pending application update to be applied
/// </summary> /// </summary>
@ -151,7 +161,7 @@ public static class Utilities
{ {
ShutdownRequested?.Invoke(null, EventArgs.Empty); ShutdownRequested?.Invoke(null, EventArgs.Empty);
} }
private static void OnUpdateRequested(UpdateEventArgs e) private static void OnUpdateRequested(UpdateEventArgs e)
{ {
UpdateRequested?.Invoke(null, e); UpdateRequested?.Invoke(null, e);

View File

@ -9,6 +9,7 @@ public static class StorageManager
{ {
private static bool _ranMigrations; private static bool _ranMigrations;
private static bool _inUse; private static bool _inUse;
private static object _factoryLock = new();
/// <summary> /// <summary>
/// Creates a backup of the database if the last backup is older than 10 minutes /// Creates a backup of the database if the last backup is older than 10 minutes
@ -39,19 +40,22 @@ public static class StorageManager
File.Copy(database, Path.Combine(backupFolder, $"artemis-{DateTime.Now:yyyy-dd-M--HH-mm-ss}.db")); File.Copy(database, Path.Combine(backupFolder, $"artemis-{DateTime.Now:yyyy-dd-M--HH-mm-ss}.db"));
} }
public static ArtemisDbContext CreateDbContext(string dataFolder) public static ArtemisDbContext CreateDbContext(string dataFolder)
{ {
_inUse = true; lock (_factoryLock)
{
_inUse = true;
ArtemisDbContext dbContext = new() {DataFolder = dataFolder};
if (_ranMigrations)
return dbContext;
dbContext.Database.Migrate();
dbContext.Database.ExecuteSqlRaw("PRAGMA optimize");
_ranMigrations = true;
ArtemisDbContext dbContext = new() {DataFolder = dataFolder};
if (_ranMigrations)
return dbContext; return dbContext;
}
dbContext.Database.Migrate();
dbContext.Database.ExecuteSqlRaw("PRAGMA optimize");
_ranMigrations = true;
return dbContext;
} }
} }

View File

@ -20,57 +20,64 @@ namespace Artemis.UI.Routing;
public static class Routes public static class Routes
{ {
public static List<IRouterRegistration> ArtemisRoutes = new() public static List<IRouterRegistration> ArtemisRoutes =
{ [
new RouteRegistration<BlankViewModel>("blank"), new RouteRegistration<BlankViewModel>("blank"),
new RouteRegistration<HomeViewModel>("home"), new RouteRegistration<HomeViewModel>("home"),
new RouteRegistration<WorkshopViewModel>("workshop") new RouteRegistration<WorkshopViewModel>("workshop")
{ {
Children = new List<IRouterRegistration> Children =
{ [
new RouteRegistration<WorkshopOfflineViewModel>("offline/{message:string}"), new RouteRegistration<WorkshopOfflineViewModel>("offline/{message:string}"),
new RouteRegistration<EntriesViewModel>("entries") new RouteRegistration<EntriesViewModel>("entries")
{ {
Children = new List<IRouterRegistration> Children =
{ [
new RouteRegistration<PluginListViewModel>("plugins/{page:int}"), new RouteRegistration<PluginListViewModel>("plugins")
new RouteRegistration<PluginDetailsViewModel>("plugins/details/{entryId:long}"), {
new RouteRegistration<ProfileListViewModel>("profiles/{page:int}"), Children = [new RouteRegistration<PluginDetailsViewModel>("details/{entryId:long}")]
new RouteRegistration<ProfileDetailsViewModel>("profiles/details/{entryId:long}"), },
new RouteRegistration<LayoutListViewModel>("layouts/{page:int}"), new RouteRegistration<ProfileListViewModel>("profiles")
new RouteRegistration<LayoutDetailsViewModel>("layouts/details/{entryId:long}"), {
} Children = [new RouteRegistration<ProfileDetailsViewModel>("details/{entryId:long}")]
},
new RouteRegistration<LayoutListViewModel>("layouts")
{
Children = [new RouteRegistration<LayoutDetailsViewModel>("details/{entryId:long}")]
},
]
}, },
new RouteRegistration<WorkshopLibraryViewModel>("library") new RouteRegistration<WorkshopLibraryViewModel>("library")
{ {
Children = new List<IRouterRegistration> Children =
{ [
new RouteRegistration<InstalledTabViewModel>("installed"), new RouteRegistration<InstalledTabViewModel>("installed"),
new RouteRegistration<SubmissionsTabViewModel>("submissions"), new RouteRegistration<SubmissionsTabViewModel>("submissions"),
new RouteRegistration<SubmissionDetailViewModel>("submissions/{entryId:long}"), new RouteRegistration<SubmissionDetailViewModel>("submissions/{entryId:long}")
} ]
} }
} ]
}, },
new RouteRegistration<SurfaceEditorViewModel>("surface-editor"), new RouteRegistration<SurfaceEditorViewModel>("surface-editor"),
new RouteRegistration<SettingsViewModel>("settings") new RouteRegistration<SettingsViewModel>("settings")
{ {
Children = new List<IRouterRegistration> Children =
{ [
new RouteRegistration<GeneralTabViewModel>("general"), new RouteRegistration<GeneralTabViewModel>("general"),
new RouteRegistration<PluginsTabViewModel>("plugins"), new RouteRegistration<PluginsTabViewModel>("plugins"),
new RouteRegistration<DevicesTabViewModel>("devices"), new RouteRegistration<DevicesTabViewModel>("devices"),
new RouteRegistration<ReleasesTabViewModel>("releases") new RouteRegistration<ReleasesTabViewModel>("releases")
{ {
Children = new List<IRouterRegistration> Children = [new RouteRegistration<ReleaseDetailsViewModel>("{releaseId:guid}")]
{
new RouteRegistration<ReleaseDetailsViewModel>("{releaseId:guid}")
}
}, },
new RouteRegistration<AccountTabViewModel>("account"), new RouteRegistration<AccountTabViewModel>("account"),
new RouteRegistration<AboutTabViewModel>("about") new RouteRegistration<AboutTabViewModel>("about")
} ]
}, },
new RouteRegistration<ProfileEditorViewModel>("profile-editor/{profileConfigurationId:guid}") new RouteRegistration<ProfileEditorViewModel>("profile-editor/{profileConfigurationId:guid}")
}; ];
} }

View File

@ -64,7 +64,7 @@ public partial class WorkshopLayoutViewModel : ActivatableViewModelBase, ILayout
if (!await _windowService.ShowConfirmContentDialog("Open workshop", "Do you want to close this window and view the workshop?")) if (!await _windowService.ShowConfirmContentDialog("Open workshop", "Do you want to close this window and view the workshop?"))
return false; return false;
await _router.Navigate("workshop/entries/layouts/1"); await _router.Navigate("workshop/entries/layouts");
return true; return true;
} }

View File

@ -41,9 +41,9 @@ public partial class SidebarViewModel : ActivatableViewModelBase
new(MaterialIconKind.HomeOutline, "Home", "home"), new(MaterialIconKind.HomeOutline, "Home", "home"),
new(MaterialIconKind.TestTube, "Workshop", "workshop", null, new ObservableCollection<SidebarScreenViewModel> new(MaterialIconKind.TestTube, "Workshop", "workshop", null, new ObservableCollection<SidebarScreenViewModel>
{ {
new(MaterialIconKind.FolderVideo, "Profiles", "workshop/entries/profiles/1", "workshop/entries/profiles"), new(MaterialIconKind.FolderVideo, "Profiles", "workshop/entries/profiles", "workshop/entries/profiles"),
new(MaterialIconKind.KeyboardVariant, "Layouts", "workshop/entries/layouts/1", "workshop/entries/layouts"), new(MaterialIconKind.KeyboardVariant, "Layouts", "workshop/entries/layouts", "workshop/entries/layouts"),
new(MaterialIconKind.Connection, "Plugins", "workshop/entries/plugins/1", "workshop/entries/plugins"), new(MaterialIconKind.Connection, "Plugins", "workshop/entries/plugins", "workshop/entries/plugins"),
new(MaterialIconKind.Bookshelf, "Library", "workshop/library"), new(MaterialIconKind.Bookshelf, "Library", "workshop/library"),
}), }),

View File

@ -24,9 +24,9 @@ public partial class EntriesViewModel : RoutableHostScreen<RoutableScreen>
Tabs = new ObservableCollection<RouteViewModel> Tabs = new ObservableCollection<RouteViewModel>
{ {
new("Profiles", "workshop/entries/profiles/1", "workshop/entries/profiles"), new("Profiles", "workshop/entries/profiles", "workshop/entries/profiles"),
new("Layouts", "workshop/entries/layouts/1", "workshop/entries/layouts"), new("Layouts", "workshop/entries/layouts", "workshop/entries/layouts"),
new("Plugins", "workshop/entries/plugins/1", "workshop/entries/plugins"), new("Plugins", "workshop/entries/plugins", "workshop/entries/plugins"),
}; };
this.WhenActivated(d => this.WhenActivated(d =>

View File

@ -2,8 +2,6 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:entries="clr-namespace:Artemis.UI.Screens.Workshop.Entries"
xmlns:system="clr-namespace:System;assembly=System.Runtime"
xmlns:list="clr-namespace:Artemis.UI.Screens.Workshop.Entries.List" xmlns:list="clr-namespace:Artemis.UI.Screens.Workshop.Entries.List"
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.List.EntryListInputView" x:Class="Artemis.UI.Screens.Workshop.Entries.List.EntryListInputView"
@ -25,15 +23,6 @@
<ComboBoxItem>Download count</ComboBoxItem> <ComboBoxItem>Download count</ComboBoxItem>
</ComboBox> </ComboBox>
</StackPanel> </StackPanel>
<StackPanel Grid.Column="2" Orientation="Horizontal" Spacing="5">
<TextBlock VerticalAlignment="Center">Show per page</TextBlock>
<ComboBox Width="65" SelectedItem="{CompiledBinding EntriesPerPage}">
<system:Int32>10</system:Int32>
<system:Int32>20</system:Int32>
<system:Int32>50</system:Int32>
<system:Int32>100</system:Int32>
</ComboBox>
</StackPanel>
<TextBlock Grid.Column="3" VerticalAlignment="Center" Margin="5 0 0 0" MinWidth="75" TextAlignment="Right"> <TextBlock Grid.Column="3" VerticalAlignment="Center" Margin="5 0 0 0" MinWidth="75" TextAlignment="Right">
<Run Text="{CompiledBinding TotalCount}"/> <Run Text="{CompiledBinding TotalCount}"/>
<Run Text="total"/> <Run Text="total"/>

View File

@ -9,7 +9,6 @@ namespace Artemis.UI.Screens.Workshop.Entries.List;
public partial class EntryListInputViewModel : ViewModelBase public partial class EntryListInputViewModel : ViewModelBase
{ {
private static string? _lastSearch; private static string? _lastSearch;
private readonly PluginSetting<int> _entriesPerPage;
private readonly PluginSetting<int> _sortBy; private readonly PluginSetting<int> _sortBy;
private string? _search; private string? _search;
[Notify] private string _searchWatermark = "Search"; [Notify] private string _searchWatermark = "Search";
@ -18,9 +17,7 @@ public partial class EntryListInputViewModel : ViewModelBase
public EntryListInputViewModel(ISettingsService settingsService) public EntryListInputViewModel(ISettingsService settingsService)
{ {
_search = _lastSearch; _search = _lastSearch;
_entriesPerPage = settingsService.GetSetting("Workshop.EntriesPerPage", 10);
_sortBy = settingsService.GetSetting("Workshop.SortBy", 10); _sortBy = settingsService.GetSetting("Workshop.SortBy", 10);
_entriesPerPage.AutoSave = true;
_sortBy.AutoSave = true; _sortBy.AutoSave = true;
} }
@ -33,17 +30,7 @@ public partial class EntryListInputViewModel : ViewModelBase
_lastSearch = value; _lastSearch = value;
} }
} }
public int EntriesPerPage
{
get => _entriesPerPage.Value;
set
{
_entriesPerPage.Value = value;
this.RaisePropertyChanged();
}
}
public int SortBy public int SortBy
{ {
get => _sortBy.Value; get => _sortBy.Value;

View File

@ -1,17 +1,16 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.Linq;
using System.Reactive.Disposables; 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.Screens.Workshop.Categories; using Artemis.UI.Screens.Workshop.Categories;
using Artemis.UI.Screens.Workshop.Parameters;
using Artemis.UI.Shared.Routing; using Artemis.UI.Shared.Routing;
using Artemis.UI.Shared.Services; using Artemis.UI.Shared.Services;
using Artemis.UI.Shared.Services.Builders; using Artemis.UI.Shared.Services.Builders;
using Artemis.WebClient.Workshop; using Artemis.WebClient.Workshop;
using Avalonia.Threading;
using DynamicData; using DynamicData;
using PropertyChanged.SourceGenerator; using PropertyChanged.SourceGenerator;
using ReactiveUI; using ReactiveUI;
@ -19,21 +18,20 @@ using StrawberryShake;
namespace Artemis.UI.Screens.Workshop.Entries.List; namespace Artemis.UI.Screens.Workshop.Entries.List;
public abstract partial class EntryListViewModel : RoutableScreen<WorkshopListParameters> public abstract partial class EntryListViewModel : RoutableHostScreen<RoutableScreen>
{ {
private readonly SourceList<IEntrySummary> _entries = new(); private readonly SourceList<IEntrySummary> _entries = new();
private readonly ObservableAsPropertyHelper<bool> _isLoading;
private readonly INotificationService _notificationService; private readonly INotificationService _notificationService;
private readonly string _route;
private readonly ObservableAsPropertyHelper<bool> _showPagination;
private readonly IWorkshopClient _workshopClient; private readonly IWorkshopClient _workshopClient;
[Notify] private int _page; private readonly string _route;
[Notify] private int _loadedPage = -1; private IGetEntriesv2_EntriesV2_PageInfo? _currentPageInfo;
[Notify] private int _totalPages = 1;
[Notify] private bool _initializing = true;
[Notify] private bool _fetchingMore;
[Notify] private int _entriesPerFetch;
protected EntryListViewModel(string route, protected EntryListViewModel(string route,
IWorkshopClient workshopClient, IWorkshopClient workshopClient,
IRouter router,
CategoriesViewModel categoriesViewModel, CategoriesViewModel categoriesViewModel,
EntryListInputViewModel entryListInputViewModel, EntryListInputViewModel entryListInputViewModel,
INotificationService notificationService, INotificationService notificationService,
@ -42,46 +40,37 @@ public abstract partial class EntryListViewModel : RoutableScreen<WorkshopListPa
_route = route; _route = route;
_workshopClient = workshopClient; _workshopClient = workshopClient;
_notificationService = notificationService; _notificationService = notificationService;
_showPagination = this.WhenAnyValue(vm => vm.TotalPages).Select(t => t > 1).ToProperty(this, vm => vm.ShowPagination);
_isLoading = this.WhenAnyValue(vm => vm.Page, vm => vm.LoadedPage, (p, c) => p != c).ToProperty(this, vm => vm.IsLoading);
CategoriesViewModel = categoriesViewModel; CategoriesViewModel = categoriesViewModel;
InputViewModel = entryListInputViewModel; InputViewModel = entryListInputViewModel;
_entries.Connect() _entries.Connect()
.ObserveOn(new AvaloniaSynchronizationContext(DispatcherPriority.SystemIdle))
.Transform(getEntryListViewModel) .Transform(getEntryListViewModel)
.Bind(out ReadOnlyObservableCollection<EntryListItemViewModel> entries) .Bind(out ReadOnlyObservableCollection<EntryListItemViewModel> entries)
.Subscribe(); .Subscribe();
Entries = entries; Entries = entries;
// Respond to page changes
this.WhenAnyValue<EntryListViewModel, int>(vm => vm.Page).Skip(1).Subscribe(p => Task.Run(() => router.Navigate($"{_route}/{p}")));
this.WhenActivated(d => this.WhenActivated(d =>
{ {
// Respond to filter query input changes // Respond to filter query input changes
InputViewModel.WhenAnyValue(vm => vm.Search).Skip(1).Throttle(TimeSpan.FromMilliseconds(200)).Subscribe(_ => RefreshToStart()).DisposeWith(d); InputViewModel.WhenAnyValue(vm => vm.Search).Skip(1).Throttle(TimeSpan.FromMilliseconds(200)).Subscribe(_ => Reset()).DisposeWith(d);
InputViewModel.WhenAnyValue(vm => vm.SortBy, vm => vm.EntriesPerPage).Skip(1).Subscribe(_ => RefreshToStart()).DisposeWith(d); CategoriesViewModel.WhenAnyValue(vm => vm.CategoryFilters).Skip(1).Subscribe(_ => Reset()).DisposeWith(d);
CategoriesViewModel.WhenAnyValue(vm => vm.CategoryFilters).Skip(1).Subscribe(_ => RefreshToStart()).DisposeWith(d);
}); });
} }
public bool ShowPagination => _showPagination.Value;
public bool IsLoading => _isLoading.Value;
public CategoriesViewModel CategoriesViewModel { get; } public CategoriesViewModel CategoriesViewModel { get; }
public EntryListInputViewModel InputViewModel { get; } public EntryListInputViewModel InputViewModel { get; }
public ReadOnlyObservableCollection<EntryListItemViewModel> Entries { get; } public ReadOnlyObservableCollection<EntryListItemViewModel> Entries { get; }
public override async Task OnNavigating(WorkshopListParameters parameters, NavigationArguments args, CancellationToken cancellationToken)
{
Page = Math.Max(1, parameters.Page);
await Task.Delay(200, cancellationToken); public override async Task OnNavigating(NavigationArguments args, CancellationToken cancellationToken)
if (!cancellationToken.IsCancellationRequested) {
await Query(cancellationToken); if (_entries.Count == 0)
{
await Task.Delay(250, cancellationToken);
await FetchMore(cancellationToken);
Initializing = false;
}
} }
public override Task OnClosing(NavigationArguments args) public override Task OnClosing(NavigationArguments args)
@ -92,6 +81,43 @@ public abstract partial class EntryListViewModel : RoutableScreen<WorkshopListPa
return base.OnClosing(args); return base.OnClosing(args);
} }
public async Task FetchMore(CancellationToken cancellationToken)
{
if (FetchingMore || _currentPageInfo != null && !_currentPageInfo.HasNextPage)
return;
FetchingMore = true;
int entriesPerFetch = _entries.Count == 0 ? _entriesPerFetch * 2 : _entriesPerFetch;
string? search = string.IsNullOrWhiteSpace(InputViewModel.Search) ? null : InputViewModel.Search;
EntryFilterInput filter = GetFilter();
IReadOnlyList<EntrySortInput> sort = GetSort();
try
{
IOperationResult<IGetEntriesv2Result> entries = await _workshopClient.GetEntriesv2.ExecuteAsync(search, filter, sort, entriesPerFetch, _currentPageInfo?.EndCursor, cancellationToken);
entries.EnsureNoErrors();
_currentPageInfo = entries.Data?.EntriesV2?.PageInfo;
if (entries.Data?.EntriesV2?.Edges != null)
_entries.Edit(e => e.AddRange(entries.Data.EntriesV2.Edges.Select(edge => edge.Node)));
InputViewModel.TotalCount = entries.Data?.EntriesV2?.TotalCount ?? 0;
}
catch (Exception e)
{
_notificationService.CreateNotification()
.WithTitle("Failed to load entries")
.WithMessage(e.Message)
.WithSeverity(NotificationSeverity.Error)
.Show();
}
finally
{
FetchingMore = false;
}
}
protected virtual EntryFilterInput GetFilter() protected virtual EntryFilterInput GetFilter()
{ {
@ -117,59 +143,10 @@ public abstract partial class EntryListViewModel : RoutableScreen<WorkshopListPa
}; };
} }
private void RefreshToStart() private void Reset()
{ {
// Reset to page one, will trigger a query _entries.Clear();
if (Page != 1) _currentPageInfo = null;
Page = 1; Task.Run(() => FetchMore(CancellationToken.None));
// If already at page one, force a query
else
Task.Run(() => Query(CancellationToken.None));
}
private async Task Query(CancellationToken cancellationToken)
{
try
{
string? search = string.IsNullOrWhiteSpace(InputViewModel.Search) ? null : InputViewModel.Search;
EntryFilterInput filter = GetFilter();
IReadOnlyList<EntrySortInput> sort = GetSort();
IOperationResult<IGetEntriesResult> entries = await _workshopClient.GetEntries.ExecuteAsync(
search,
filter,
InputViewModel.EntriesPerPage * (Page - 1),
InputViewModel.EntriesPerPage,
sort,
cancellationToken
);
entries.EnsureNoErrors();
if (entries.Data?.Entries?.Items != null)
{
TotalPages = (int) Math.Ceiling(entries.Data.Entries.TotalCount / (double) InputViewModel.EntriesPerPage);
InputViewModel.TotalCount = entries.Data.Entries.TotalCount;
_entries.Edit(e =>
{
e.Clear();
e.AddRange(entries.Data.Entries.Items);
});
}
else
{
TotalPages = 1;
}
}
catch (Exception e)
{
_notificationService.CreateNotification()
.WithTitle("Failed to load entries")
.WithMessage(e.Message)
.WithSeverity(NotificationSeverity.Error)
.Show();
}
finally
{
LoadedPage = Page;
}
} }
} }

View File

@ -2,8 +2,9 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:pagination="clr-namespace:Artemis.UI.Shared.Pagination;assembly=Artemis.UI.Shared"
xmlns:tabs="clr-namespace:Artemis.UI.Screens.Workshop.Entries.Tabs" 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" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Artemis.UI.Screens.Workshop.Entries.Tabs.LayoutListView" x:Class="Artemis.UI.Screens.Workshop.Entries.Tabs.LayoutListView"
x:DataType="tabs:LayoutListViewModel"> x:DataType="tabs:LayoutListViewModel">
@ -15,51 +16,52 @@
</Style> </Style>
</Styles> </Styles>
</UserControl.Styles> </UserControl.Styles>
<Grid ColumnDefinitions="300,*" RowDefinitions="Auto,*,Auto">
<StackPanel Grid.Column="0" Grid.RowSpan="3" Margin="0 0 10 0" VerticalAlignment="Top">
<Border Classes="card" VerticalAlignment="Stretch">
<StackPanel>
<TextBlock Theme="{StaticResource SubtitleTextBlockStyle}">Categories</TextBlock>
<Border Classes="card-separator" />
<ContentControl Content="{CompiledBinding CategoriesViewModel}"></ContentControl>
</StackPanel>
</Border>
</StackPanel>
<ProgressBar Grid.Column="1" Grid.Row="0" VerticalAlignment="Top" Margin="0 0 20 0" IsVisible="{CompiledBinding IsLoading}" IsIndeterminate="True" /> <Panel>
<ContentControl Grid.Column="1" Grid.Row="0" Margin="0 0 20 8" Content="{CompiledBinding InputViewModel}"/> <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">
<ScrollViewer Grid.Column="1" Grid.Row="1"> <Border Classes="card" VerticalAlignment="Stretch">
<ItemsControl ItemsSource="{CompiledBinding Entries}" Margin="0 0 20 0"> <StackPanel>
<ItemsControl.ItemsPanel> <TextBlock Theme="{StaticResource SubtitleTextBlockStyle}">Categories</TextBlock>
<ItemsPanelTemplate> <Border Classes="card-separator" />
<VirtualizingStackPanel /> <ContentControl Content="{CompiledBinding CategoriesViewModel}"></ContentControl>
</ItemsPanelTemplate> </StackPanel>
</ItemsControl.ItemsPanel> </Border>
<ItemsControl.ItemTemplate>
<DataTemplate>
<ContentControl Content="{CompiledBinding}" Margin="0 0 0 5"></ContentControl>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
<Panel Grid.Column="1" Grid.Row="1" IsVisible="{CompiledBinding !IsLoading}">
<StackPanel IsVisible="{CompiledBinding !Entries.Count}" Margin="0 50 0 0" Classes="empty-state">
<TextBlock Theme="{StaticResource TitleTextBlockStyle}">Looks like your current filters gave no results</TextBlock>
<TextBlock>
<Run>Modify or clear your filters to view other device layouts</Run>
</TextBlock>
<Lottie Path="/Assets/Animations/empty.json" RepeatCount="1" Width="350" Height="350"></Lottie>
</StackPanel> </StackPanel>
</Panel>
<pagination:Pagination Grid.Column="1" <ProgressBar Grid.Column="1" Grid.Row="0" VerticalAlignment="Top" Margin="0 0 20 0" IsVisible="{CompiledBinding FetchingMore}" IsIndeterminate="True" />
Grid.Row="2" <ContentControl Grid.Column="1" Grid.Row="0" Margin="0 0 20 8" Content="{CompiledBinding InputViewModel}" />
Margin="0 20 0 10"
IsVisible="{CompiledBinding ShowPagination}" <ScrollViewer Name="EntriesScrollViewer" Grid.Column="1" Grid.Row="1" ScrollChanged="ScrollViewer_OnScrollChanged">
Value="{CompiledBinding Page}" <ItemsControl ItemsSource="{CompiledBinding Entries}" Margin="0 0 20 0">
Maximum="{CompiledBinding TotalPages}" <ItemsControl.ItemsPanel>
HorizontalAlignment="Center" /> <ItemsPanelTemplate>
</Grid> <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> </UserControl>

View File

@ -1,4 +1,11 @@
using System;
using System.Reactive.Disposables;
using System.Threading;
using Artemis.UI.Shared.Routing;
using Avalonia.Controls;
using Avalonia.ReactiveUI; using Avalonia.ReactiveUI;
using Avalonia.Threading;
using ReactiveUI;
namespace Artemis.UI.Screens.Workshop.Entries.Tabs; namespace Artemis.UI.Screens.Workshop.Entries.Tabs;
@ -7,5 +14,30 @@ public partial class LayoutListView : ReactiveUserControl<LayoutListViewModel>
public LayoutListView() public LayoutListView()
{ {
InitializeComponent(); 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

@ -10,12 +10,11 @@ namespace Artemis.UI.Screens.Workshop.Entries.Tabs;
public class LayoutListViewModel : List.EntryListViewModel public class LayoutListViewModel : List.EntryListViewModel
{ {
public LayoutListViewModel(IWorkshopClient workshopClient, public LayoutListViewModel(IWorkshopClient workshopClient,
IRouter router,
CategoriesViewModel categoriesViewModel, CategoriesViewModel categoriesViewModel,
EntryListInputViewModel entryListInputViewModel, EntryListInputViewModel entryListInputViewModel,
INotificationService notificationService, INotificationService notificationService,
Func<IEntrySummary, EntryListItemViewModel> getEntryListViewModel) Func<IEntrySummary, EntryListItemViewModel> getEntryListViewModel)
: base("workshop/entries/layouts", workshopClient, router, categoriesViewModel, entryListInputViewModel, notificationService, getEntryListViewModel) : base("workshop/entries/layouts", workshopClient, categoriesViewModel, entryListInputViewModel, notificationService, getEntryListViewModel)
{ {
entryListInputViewModel.SearchWatermark = "Search layouts"; entryListInputViewModel.SearchWatermark = "Search layouts";
} }

View File

@ -3,11 +3,12 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:tabs="clr-namespace:Artemis.UI.Screens.Workshop.Entries.Tabs" xmlns:tabs="clr-namespace:Artemis.UI.Screens.Workshop.Entries.Tabs"
xmlns:pagination="clr-namespace:Artemis.UI.Shared.Pagination;assembly=Artemis.UI.Shared" xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
xmlns:ui="clr-namespace:Artemis.UI"
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.Tabs.PluginListView" x:Class="Artemis.UI.Screens.Workshop.Entries.Tabs.PluginListView"
x:DataType="tabs:PluginListViewModel"> x:DataType="tabs:PluginListViewModel">
<UserControl.Styles> <UserControl.Styles>
<Styles> <Styles>
<Style Selector="StackPanel.empty-state > TextBlock"> <Style Selector="StackPanel.empty-state > TextBlock">
<Setter Property="TextAlignment" Value="Center"></Setter> <Setter Property="TextAlignment" Value="Center"></Setter>
@ -15,51 +16,52 @@
</Style> </Style>
</Styles> </Styles>
</UserControl.Styles> </UserControl.Styles>
<Grid ColumnDefinitions="300,*" RowDefinitions="Auto,*,Auto">
<StackPanel Grid.Column="0" Grid.RowSpan="3" Margin="0 0 10 0" VerticalAlignment="Top"> <Panel>
<Border Classes="card" VerticalAlignment="Stretch"> <Grid ColumnDefinitions="300,*" RowDefinitions="Auto,*,Auto" IsVisible="{CompiledBinding Screen, Converter={x:Static ObjectConverters.IsNull}}">
<StackPanel> <StackPanel Grid.Column="0" Grid.RowSpan="3" Margin="0 0 10 0" VerticalAlignment="Top">
<TextBlock Theme="{StaticResource SubtitleTextBlockStyle}">Categories</TextBlock> <Border Classes="card" VerticalAlignment="Stretch">
<Border Classes="card-separator" /> <StackPanel>
<ContentControl Content="{CompiledBinding CategoriesViewModel}"></ContentControl> <TextBlock Theme="{StaticResource SubtitleTextBlockStyle}">Categories</TextBlock>
</StackPanel> <Border Classes="card-separator" />
</Border> <ContentControl Content="{CompiledBinding CategoriesViewModel}"></ContentControl>
</StackPanel> </StackPanel>
</Border>
<ProgressBar Grid.Column="1" Grid.Row="0" VerticalAlignment="Top" Margin="0 0 20 0" IsVisible="{CompiledBinding IsLoading}" IsIndeterminate="True" />
<ContentControl Grid.Column="1" Grid.Row="0" Margin="0 0 20 8" Content="{CompiledBinding InputViewModel}"/>
<ScrollViewer Grid.Column="1" Grid.Row="1">
<ItemsControl ItemsSource="{CompiledBinding Entries}" Margin="0 0 20 0">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<VirtualizingStackPanel />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<ContentControl Content="{CompiledBinding}" Margin="0 0 0 5"></ContentControl>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
<Panel Grid.Column="1" Grid.Row="1" IsVisible="{CompiledBinding !IsLoading}">
<StackPanel IsVisible="{CompiledBinding !Entries.Count}" Margin="0 50 0 0" Classes="empty-state">
<TextBlock Theme="{StaticResource TitleTextBlockStyle}">Looks like your current filters gave no results</TextBlock>
<TextBlock>
<Run>Modify or clear your filters to view other plugins</Run>
</TextBlock>
<Lottie Path="/Assets/Animations/empty.json" RepeatCount="1" Width="350" Height="350"></Lottie>
</StackPanel> </StackPanel>
</Panel>
<pagination:Pagination Grid.Column="1" <ProgressBar Grid.Column="1" Grid.Row="0" VerticalAlignment="Top" Margin="0 0 20 0" IsVisible="{CompiledBinding FetchingMore}" IsIndeterminate="True" />
Grid.Row="2" <ContentControl Grid.Column="1" Grid.Row="0" Margin="0 0 20 8" Content="{CompiledBinding InputViewModel}" />
Margin="0 20 0 10"
IsVisible="{CompiledBinding ShowPagination}" <ScrollViewer Name="EntriesScrollViewer" Grid.Column="1" Grid.Row="1" ScrollChanged="ScrollViewer_OnScrollChanged">
Value="{CompiledBinding Page}" <ItemsControl ItemsSource="{CompiledBinding Entries}" Margin="0 0 20 0">
Maximum="{CompiledBinding TotalPages}" <ItemsControl.ItemsPanel>
HorizontalAlignment="Center" /> <ItemsPanelTemplate>
</Grid> <VirtualizingStackPanel />
</UserControl> </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,7 +1,11 @@
using Avalonia; using System;
using System.Reactive.Disposables;
using System.Threading;
using Artemis.UI.Shared.Routing;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using Avalonia.ReactiveUI; using Avalonia.ReactiveUI;
using Avalonia.Threading;
using ReactiveUI;
namespace Artemis.UI.Screens.Workshop.Entries.Tabs; namespace Artemis.UI.Screens.Workshop.Entries.Tabs;
@ -10,5 +14,30 @@ public partial class PluginListView : ReactiveUserControl<PluginListViewModel>
public PluginListView() public PluginListView()
{ {
InitializeComponent(); 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

@ -10,12 +10,11 @@ namespace Artemis.UI.Screens.Workshop.Entries.Tabs;
public class PluginListViewModel : EntryListViewModel public class PluginListViewModel : EntryListViewModel
{ {
public PluginListViewModel(IWorkshopClient workshopClient, public PluginListViewModel(IWorkshopClient workshopClient,
IRouter router,
CategoriesViewModel categoriesViewModel, CategoriesViewModel categoriesViewModel,
EntryListInputViewModel entryListInputViewModel, EntryListInputViewModel entryListInputViewModel,
INotificationService notificationService, INotificationService notificationService,
Func<IEntrySummary, EntryListItemViewModel> getEntryListViewModel) Func<IEntrySummary, EntryListItemViewModel> getEntryListViewModel)
: base("workshop/entries/plugins", workshopClient, router, categoriesViewModel, entryListInputViewModel, notificationService, getEntryListViewModel) : base("workshop/entries/plugins", workshopClient, categoriesViewModel, entryListInputViewModel, notificationService, getEntryListViewModel)
{ {
entryListInputViewModel.SearchWatermark = "Search plugins"; entryListInputViewModel.SearchWatermark = "Search plugins";
} }

View File

@ -2,9 +2,10 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:pagination="clr-namespace:Artemis.UI.Shared.Pagination;assembly=Artemis.UI.Shared"
xmlns:tabs="clr-namespace:Artemis.UI.Screens.Workshop.Entries.Tabs" xmlns:tabs="clr-namespace:Artemis.UI.Screens.Workshop.Entries.Tabs"
mc:Ignorable="d" d:DesignWidth="1200" d:DesignHeight="450" 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:Class="Artemis.UI.Screens.Workshop.Entries.Tabs.ProfileListView"
x:DataType="tabs:ProfileListViewModel"> x:DataType="tabs:ProfileListViewModel">
<UserControl.Styles> <UserControl.Styles>
@ -15,51 +16,52 @@
</Style> </Style>
</Styles> </Styles>
</UserControl.Styles> </UserControl.Styles>
<Grid ColumnDefinitions="300,*" RowDefinitions="Auto,*,Auto">
<StackPanel Grid.Column="0" Grid.RowSpan="3" Margin="0 0 10 0" VerticalAlignment="Top">
<Border Classes="card" VerticalAlignment="Stretch">
<StackPanel>
<TextBlock Theme="{StaticResource SubtitleTextBlockStyle}">Categories</TextBlock>
<Border Classes="card-separator" />
<ContentControl Content="{CompiledBinding CategoriesViewModel}"></ContentControl>
</StackPanel>
</Border>
</StackPanel>
<ProgressBar Grid.Column="1" Grid.Row="0" VerticalAlignment="Top" Margin="0 0 20 0" IsVisible="{CompiledBinding IsLoading}" IsIndeterminate="True" /> <Panel>
<ContentControl Grid.Column="1" Grid.Row="0" Margin="0 0 20 8" Content="{CompiledBinding InputViewModel}"/> <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">
<ScrollViewer Grid.Column="1" Grid.Row="1"> <Border Classes="card" VerticalAlignment="Stretch">
<ItemsControl ItemsSource="{CompiledBinding Entries}" Margin="0 0 20 0"> <StackPanel>
<ItemsControl.ItemsPanel> <TextBlock Theme="{StaticResource SubtitleTextBlockStyle}">Categories</TextBlock>
<ItemsPanelTemplate> <Border Classes="card-separator" />
<VirtualizingStackPanel /> <ContentControl Content="{CompiledBinding CategoriesViewModel}"></ContentControl>
</ItemsPanelTemplate> </StackPanel>
</ItemsControl.ItemsPanel> </Border>
<ItemsControl.ItemTemplate>
<DataTemplate>
<ContentControl Content="{CompiledBinding}" Margin="0 0 0 5"></ContentControl>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
<Panel Grid.Column="1" Grid.Row="1" IsVisible="{CompiledBinding !IsLoading}">
<StackPanel IsVisible="{CompiledBinding !Entries.Count}" Margin="0 50 0 0" Classes="empty-state">
<TextBlock Theme="{StaticResource TitleTextBlockStyle}">Looks like your current filters gave no results</TextBlock>
<TextBlock>
<Run>Modify or clear your filters to view some awesome profiles</Run>
</TextBlock>
<Lottie Path="/Assets/Animations/empty.json" RepeatCount="1" Width="350" Height="350"></Lottie>
</StackPanel> </StackPanel>
</Panel>
<pagination:Pagination Grid.Column="1" <ProgressBar Grid.Column="1" Grid.Row="0" VerticalAlignment="Top" Margin="0 0 20 0" IsVisible="{CompiledBinding FetchingMore}" IsIndeterminate="True" />
Grid.Row="2" <ContentControl Grid.Column="1" Grid.Row="0" Margin="0 0 20 8" Content="{CompiledBinding InputViewModel}" />
Margin="0 20 0 10"
IsVisible="{CompiledBinding ShowPagination}" <ScrollViewer Name="EntriesScrollViewer" Grid.Column="1" Grid.Row="1" ScrollChanged="ScrollViewer_OnScrollChanged">
Value="{CompiledBinding Page}" <ItemsControl ItemsSource="{CompiledBinding Entries}" Margin="0 0 20 0">
Maximum="{CompiledBinding TotalPages}" <ItemsControl.ItemsPanel>
HorizontalAlignment="Center" /> <ItemsPanelTemplate>
</Grid> <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> </UserControl>

View File

@ -1,4 +1,11 @@
using System;
using System.Reactive.Disposables;
using System.Threading;
using Artemis.UI.Shared.Routing;
using Avalonia.Controls;
using Avalonia.ReactiveUI; using Avalonia.ReactiveUI;
using Avalonia.Threading;
using ReactiveUI;
namespace Artemis.UI.Screens.Workshop.Entries.Tabs; namespace Artemis.UI.Screens.Workshop.Entries.Tabs;
@ -7,5 +14,30 @@ public partial class ProfileListView : ReactiveUserControl<ProfileListViewModel>
public ProfileListView() public ProfileListView()
{ {
InitializeComponent(); 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

@ -10,12 +10,11 @@ namespace Artemis.UI.Screens.Workshop.Entries.Tabs;
public class ProfileListViewModel : List.EntryListViewModel public class ProfileListViewModel : List.EntryListViewModel
{ {
public ProfileListViewModel(IWorkshopClient workshopClient, public ProfileListViewModel(IWorkshopClient workshopClient,
IRouter router,
CategoriesViewModel categoriesViewModel, CategoriesViewModel categoriesViewModel,
EntryListInputViewModel entryListInputViewModel, EntryListInputViewModel entryListInputViewModel,
INotificationService notificationService, INotificationService notificationService,
Func<IEntrySummary, EntryListItemViewModel> getEntryListViewModel) Func<IEntrySummary, EntryListItemViewModel> getEntryListViewModel)
: base("workshop/entries/profiles", workshopClient, router, categoriesViewModel, entryListInputViewModel, notificationService, getEntryListViewModel) : base("workshop/entries/profiles", workshopClient, categoriesViewModel, entryListInputViewModel, notificationService, getEntryListViewModel)
{ {
entryListInputViewModel.SearchWatermark = "Search profiles"; entryListInputViewModel.SearchWatermark = "Search profiles";
} }

View File

@ -41,7 +41,7 @@
<StackPanel Margin="30 -75 30 0" Grid.Row="1"> <StackPanel Margin="30 -75 30 0" Grid.Row="1">
<StackPanel Spacing="10" Orientation="Horizontal" VerticalAlignment="Top"> <StackPanel Spacing="10" Orientation="Horizontal" VerticalAlignment="Top">
<Button Width="150" Height="180" Command="{CompiledBinding Navigate}" CommandParameter="workshop/entries/profiles/1" VerticalContentAlignment="Top"> <Button Width="150" Height="180" Command="{CompiledBinding Navigate}" CommandParameter="workshop/entries/profiles" VerticalContentAlignment="Top">
<StackPanel> <StackPanel>
<avalonia:MaterialIcon Kind="FolderVideo" HorizontalAlignment="Left" Width="60" Height="60" Margin="0 5" /> <avalonia:MaterialIcon Kind="FolderVideo" HorizontalAlignment="Left" Width="60" Height="60" Margin="0 5" />
<TextBlock TextWrapping="Wrap" FontSize="16" Margin="0 5">Profiles</TextBlock> <TextBlock TextWrapping="Wrap" FontSize="16" Margin="0 5">Profiles</TextBlock>
@ -49,7 +49,7 @@
</StackPanel> </StackPanel>
</Button> </Button>
<Button Width="150" Height="180" Command="{CompiledBinding Navigate}" CommandParameter="workshop/entries/layouts/1" VerticalContentAlignment="Top"> <Button Width="150" Height="180" Command="{CompiledBinding Navigate}" CommandParameter="workshop/entries/layouts" VerticalContentAlignment="Top">
<StackPanel> <StackPanel>
<avalonia:MaterialIcon Kind="KeyboardVariant" HorizontalAlignment="Left" Width="60" Height="60" Margin="0 5" /> <avalonia:MaterialIcon Kind="KeyboardVariant" HorizontalAlignment="Left" Width="60" Height="60" Margin="0 5" />
<TextBlock TextWrapping="Wrap" FontSize="16" Margin="0 5">Layouts</TextBlock> <TextBlock TextWrapping="Wrap" FontSize="16" Margin="0 5">Layouts</TextBlock>
@ -57,7 +57,7 @@
</StackPanel> </StackPanel>
</Button> </Button>
<Button Width="150" Height="180" Command="{CompiledBinding Navigate}" CommandParameter="workshop/entries/plugins/1" VerticalContentAlignment="Top"> <Button Width="150" Height="180" Command="{CompiledBinding Navigate}" CommandParameter="workshop/entries/plugins" VerticalContentAlignment="Top">
<StackPanel> <StackPanel>
<avalonia:MaterialIcon Kind="Connection" HorizontalAlignment="Left" Width="60" Height="60" Margin="0 5" /> <avalonia:MaterialIcon Kind="Connection" HorizontalAlignment="Left" Width="60" Height="60" Margin="0 5" />
<TextBlock TextWrapping="Wrap" FontSize="16" Margin="0 5">Plugins</TextBlock> <TextBlock TextWrapping="Wrap" FontSize="16" Margin="0 5">Plugins</TextBlock>

View File

@ -1,6 +0,0 @@
namespace Artemis.UI.Screens.Workshop.Parameters;
public class WorkshopListParameters
{
public int Page { get; set; }
}

View File

@ -4,6 +4,7 @@
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:mdxaml="https://github.com/whistyun/Markdown.Avalonia.Tight" xmlns:mdxaml="https://github.com/whistyun/Markdown.Avalonia.Tight"
xmlns:plugins="clr-namespace:Artemis.UI.Screens.Workshop.Plugins" xmlns:plugins="clr-namespace:Artemis.UI.Screens.Workshop.Plugins"
xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Artemis.UI.Screens.Workshop.Plugins.PluginDetailsView" x:Class="Artemis.UI.Screens.Workshop.Plugins.PluginDetailsView"
x:DataType="plugins:PluginDetailsViewModel"> x:DataType="plugins:PluginDetailsViewModel">
@ -12,13 +13,29 @@
<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.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}" />
</StackPanel>
</StackPanel>
</Border>
<Border Classes="card" VerticalAlignment="Top" IsVisible="{CompiledBinding Entry.LatestRelease, Converter={x:Static ObjectConverters.IsNotNull}}"> <Border Classes="card" VerticalAlignment="Top" IsVisible="{CompiledBinding Entry.LatestRelease, Converter={x:Static ObjectConverters.IsNotNull}}">
<ContentControl Content="{CompiledBinding EntryReleasesViewModel}" /> <ContentControl Content="{CompiledBinding EntryReleasesViewModel}" />
</Border> </Border>
</StackPanel> </StackPanel>
<ScrollViewer Grid.Row="1" Grid.Column="1"> <ScrollViewer Grid.Row="1" Grid.Column="1">
<StackPanel Margin="10 0" Spacing="10"> <StackPanel Margin="10 0" Spacing="10">
<Border Classes="card"> <Border Classes="card">
<mdxaml:MarkdownScrollViewer Markdown="{CompiledBinding Entry.Description}" MarkdownStyleName="FluentAvalonia"> <mdxaml:MarkdownScrollViewer Markdown="{CompiledBinding Entry.Description}" MarkdownStyleName="FluentAvalonia">
<mdxaml:MarkdownScrollViewer.Styles> <mdxaml:MarkdownScrollViewer.Styles>
@ -41,4 +58,4 @@
<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}" Content="{CompiledBinding EntryImagesViewModel}" />
</Grid> </Grid>
</UserControl> </UserControl>

View File

@ -28,7 +28,7 @@ public partial class PluginDetailsViewModel : RoutableScreen<WorkshopDetailParam
private readonly Func<IEntryDetails, EntryReleasesViewModel> _getEntryReleasesViewModel; private readonly Func<IEntryDetails, EntryReleasesViewModel> _getEntryReleasesViewModel;
private readonly Func<IEntryDetails, EntryImagesViewModel> _getEntryImagesViewModel; private readonly Func<IEntryDetails, EntryImagesViewModel> _getEntryImagesViewModel;
private readonly Func<IEntrySummary, EntryListItemViewModel> _getEntryListViewModel; private readonly Func<IEntrySummary, EntryListItemViewModel> _getEntryListViewModel;
[Notify] private IEntryDetails? _entry; [Notify] private IGetPluginEntryById_Entry? _entry;
[Notify] private EntryInfoViewModel? _entryInfoViewModel; [Notify] private EntryInfoViewModel? _entryInfoViewModel;
[Notify] private EntryReleasesViewModel? _entryReleasesViewModel; [Notify] private EntryReleasesViewModel? _entryReleasesViewModel;
[Notify] private EntryImagesViewModel? _entryImagesViewModel; [Notify] private EntryImagesViewModel? _entryImagesViewModel;
@ -58,7 +58,7 @@ public partial class PluginDetailsViewModel : RoutableScreen<WorkshopDetailParam
private async Task GetEntry(long entryId, CancellationToken cancellationToken) private async Task GetEntry(long entryId, CancellationToken cancellationToken)
{ {
IOperationResult<IGetEntryByIdResult> result = await _client.GetEntryById.ExecuteAsync(entryId, cancellationToken); IOperationResult<IGetPluginEntryByIdResult> result = await _client.GetPluginEntryById.ExecuteAsync(entryId, cancellationToken);
if (result.IsErrorResult()) if (result.IsErrorResult())
return; return;

View File

@ -68,4 +68,11 @@ fragment release on Release {
downloadSize downloadSize
md5Hash md5Hash
createdAt createdAt
} }
fragment pluginInfo on PluginInfo {
requiresAdmin
supportsWindows
supportsLinux
supportsOSX
}

View File

@ -5,4 +5,20 @@ query GetEntries($search: String $filter: EntryFilterInput $skip: Int $take: Int
...entrySummary ...entrySummary
} }
} }
}
query GetEntriesv2($search: String $filter: EntryFilterInput $order: [EntrySortInput!] $first: Int $after: String) {
entriesV2(search: $search where: $filter order: $order first: $first after: $after) {
totalCount
pageInfo {
hasNextPage
endCursor
}
edges {
cursor
node {
...entrySummary
}
}
}
} }

View File

@ -2,4 +2,22 @@ query GetEntryById($id: Long!) {
entry(id: $id) { entry(id: $id) {
...entryDetails ...entryDetails
} }
}
query GetPluginEntryById($id: Long!) {
entry(id: $id) {
...entryDetails
pluginInfo {
...pluginInfo
}
}
}
query GetLayoutEntryById($id: Long!) {
entry(id: $id) {
...entryDetails
layoutInfo {
...layoutInfo
}
}
} }

View File

@ -2,7 +2,7 @@ schema: schema.graphql
extensions: extensions:
endpoints: endpoints:
Default GraphQL Endpoint: Default GraphQL Endpoint:
url: https://workshop.artemis-rgb.com/graphql url: https://localhost:7281/graphql
headers: headers:
user-agent: JS GraphQL user-agent: JS GraphQL
introspect: true introspect: true

View File

@ -28,6 +28,26 @@ type EntriesCollectionSegment {
totalCount: Int! totalCount: Int!
} }
"A connection to a list of items."
type EntriesV2Connection {
"A list of edges."
edges: [EntriesV2Edge!]
"A flattened list of the nodes."
nodes: [Entry!]
"Information to aid in pagination."
pageInfo: PageInfo!
"Identifies the total count of items in the connection."
totalCount: Int!
}
"An edge in a connection."
type EntriesV2Edge {
"A cursor for use in pagination."
cursor: String!
"The item at the end of the edge."
node: Entry!
}
type Entry { type Entry {
author: String! author: String!
authorId: UUID! authorId: UUID!
@ -84,15 +104,29 @@ type Mutation {
updateEntryImage(input: UpdateEntryImageInput!): Image updateEntryImage(input: UpdateEntryImageInput!): Image
} }
"Information about pagination in a connection."
type PageInfo {
"When paginating forwards, the cursor to continue."
endCursor: String
"Indicates whether more edges exist following the set defined by the clients arguments."
hasNextPage: Boolean!
"Indicates whether more edges exist prior the set defined by the clients arguments."
hasPreviousPage: Boolean!
"When paginating backwards, the cursor to continue."
startCursor: String
}
type PluginInfo { type PluginInfo {
api: Int api: Int
entry: Entry! entry: Entry!
entryId: Long! entryId: Long!
helpPage: String helpPage: String
platforms: PluginPlatform
pluginGuid: UUID! pluginGuid: UUID!
repository: String repository: String
requiresAdmin: Boolean! requiresAdmin: Boolean!
supportsLinux: Boolean!
supportsOSX: Boolean!
supportsWindows: Boolean!
website: String website: String
} }
@ -108,6 +142,19 @@ type PluginInfosCollectionSegment {
type Query { type Query {
categories(order: [CategorySortInput!], where: CategoryFilterInput): [Category!]! categories(order: [CategorySortInput!], where: CategoryFilterInput): [Category!]!
entries(order: [EntrySortInput!], search: String, skip: Int, take: Int, where: EntryFilterInput): EntriesCollectionSegment entries(order: [EntrySortInput!], search: String, skip: Int, take: Int, where: EntryFilterInput): EntriesCollectionSegment
entriesV2(
"Returns the elements in the list that come after the specified cursor."
after: String,
"Returns the elements in the list that come before the specified cursor."
before: String,
"Returns the first _n_ elements from the list."
first: Int,
"Returns the last _n_ elements from the list."
last: Int,
order: [EntrySortInput!],
search: String,
where: EntryFilterInput
): EntriesV2Connection
entry(id: Long!): Entry entry(id: Long!): Entry
pluginInfo(pluginGuid: UUID!): PluginInfo pluginInfo(pluginGuid: UUID!): PluginInfo
pluginInfos(order: [PluginInfoSortInput!], skip: Int, take: Int, where: PluginInfoFilterInput): PluginInfosCollectionSegment pluginInfos(order: [PluginInfoSortInput!], skip: Int, take: Int, where: PluginInfoFilterInput): PluginInfosCollectionSegment
@ -155,12 +202,6 @@ enum KeyboardLayoutType {
UNKNOWN UNKNOWN
} }
enum PluginPlatform {
LINUX
OSX
WINDOWS
}
enum RGBDeviceType { enum RGBDeviceType {
ALL ALL
COOLER COOLER
@ -418,13 +459,6 @@ input NullableOfKeyboardLayoutTypeOperationFilterInput {
nin: [KeyboardLayoutType] nin: [KeyboardLayoutType]
} }
input NullableOfPluginPlatformOperationFilterInput {
eq: PluginPlatform
in: [PluginPlatform]
neq: PluginPlatform
nin: [PluginPlatform]
}
input PluginInfoFilterInput { input PluginInfoFilterInput {
and: [PluginInfoFilterInput!] and: [PluginInfoFilterInput!]
api: IntOperationFilterInput api: IntOperationFilterInput
@ -432,10 +466,12 @@ input PluginInfoFilterInput {
entryId: LongOperationFilterInput entryId: LongOperationFilterInput
helpPage: StringOperationFilterInput helpPage: StringOperationFilterInput
or: [PluginInfoFilterInput!] or: [PluginInfoFilterInput!]
platforms: NullableOfPluginPlatformOperationFilterInput
pluginGuid: UuidOperationFilterInput pluginGuid: UuidOperationFilterInput
repository: StringOperationFilterInput repository: StringOperationFilterInput
requiresAdmin: BooleanOperationFilterInput requiresAdmin: BooleanOperationFilterInput
supportsLinux: BooleanOperationFilterInput
supportsOSX: BooleanOperationFilterInput
supportsWindows: BooleanOperationFilterInput
website: StringOperationFilterInput website: StringOperationFilterInput
} }
@ -444,10 +480,12 @@ input PluginInfoSortInput {
entry: EntrySortInput entry: EntrySortInput
entryId: SortEnumType entryId: SortEnumType
helpPage: SortEnumType helpPage: SortEnumType
platforms: SortEnumType
pluginGuid: SortEnumType pluginGuid: SortEnumType
repository: SortEnumType repository: SortEnumType
requiresAdmin: SortEnumType requiresAdmin: SortEnumType
supportsLinux: SortEnumType
supportsOSX: SortEnumType
supportsWindows: SortEnumType
website: SortEnumType website: SortEnumType
} }