From da3d47d7b80669242310d219fe84ecf02622c0b6 Mon Sep 17 00:00:00 2001 From: RobertBeekman Date: Thu, 21 Mar 2024 20:12:40 +0100 Subject: [PATCH] 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 --- src/Artemis.Core/Utilities/Utilities.cs | 16 +- src/Artemis.Storage/StorageManager.cs | 22 +-- src/Artemis.UI/Routing/Routes.cs | 59 ++++---- .../WorkshopLayoutViewModel.cs | 2 +- .../Screens/Sidebar/SidebarViewModel.cs | 6 +- .../Workshop/Entries/EntriesViewModel.cs | 6 +- .../Entries/List/EntryListInputView.axaml | 11 -- .../Entries/List/EntryListInputViewModel.cs | 15 +- .../Entries/List/EntryListViewModel.cs | 141 ++++++++---------- .../Entries/Tabs/LayoutListView.axaml | 92 ++++++------ .../Entries/Tabs/LayoutListView.axaml.cs | 32 ++++ .../Entries/Tabs/LayoutListViewModel.cs | 3 +- .../Entries/Tabs/PluginListView.axaml | 98 ++++++------ .../Entries/Tabs/PluginListView.axaml.cs | 33 +++- .../Entries/Tabs/PluginListViewModel.cs | 3 +- .../Entries/Tabs/ProfileListView.axaml | 94 ++++++------ .../Entries/Tabs/ProfileListView.axaml.cs | 32 ++++ .../Entries/Tabs/ProfileListViewModel.cs | 3 +- .../Workshop/Home/WorkshopHomeView.axaml | 6 +- .../Parameters/WorkshopListParameters.cs | 6 - .../Workshop/Plugins/PluginDetailsView.axaml | 21 ++- .../Plugins/PluginDetailsViewModel.cs | 4 +- .../Queries/Fragments.graphql | 9 +- .../Queries/GetEntries.graphql | 16 ++ .../Queries/GetEntryById.graphql | 18 +++ .../graphql.config.yml | 2 +- src/Artemis.WebClient.Workshop/schema.graphql | 70 +++++++-- 27 files changed, 490 insertions(+), 330 deletions(-) delete mode 100644 src/Artemis.UI/Screens/Workshop/Parameters/WorkshopListParameters.cs diff --git a/src/Artemis.Core/Utilities/Utilities.cs b/src/Artemis.Core/Utilities/Utilities.cs index ed0ded1d7..8c9422400 100644 --- a/src/Artemis.Core/Utilities/Utilities.cs +++ b/src/Artemis.Core/Utilities/Utilities.cs @@ -13,6 +13,8 @@ namespace Artemis.Core; /// public static class Utilities { + private static bool _shuttingDown; + /// /// Call this before even initializing the Core to make sure the folders required for operation are in place /// @@ -33,7 +35,11 @@ public static void PrepareFirstLaunch() /// public static void Shutdown() { + if (_shuttingDown) + return; + // Request a graceful shutdown, whatever UI we're running can pick this up + _shuttingDown = true; OnShutdownRequested(); } @@ -45,9 +51,13 @@ public static void Shutdown() /// A list of extra arguments to pass to Artemis when restarting public static void Restart(bool elevate, TimeSpan delay, params string[] extraArgs) { + if (_shuttingDown) + return; + if (!OperatingSystem.IsWindows() && elevate) throw new ArtemisCoreException("Elevation on non-Windows platforms is not supported."); + _shuttingDown = true; OnRestartRequested(new RestartEventArgs(elevate, delay, extraArgs.ToList())); } @@ -106,12 +116,12 @@ public static void CreateAccessibleDirectory(string path) /// Occurs when the core has requested an application shutdown /// public static event EventHandler? ShutdownRequested; - + /// /// Occurs when the core has requested an application restart /// public static event EventHandler? RestartRequested; - + /// /// Occurs when the core has requested a pending application update to be applied /// @@ -151,7 +161,7 @@ private static void OnShutdownRequested() { ShutdownRequested?.Invoke(null, EventArgs.Empty); } - + private static void OnUpdateRequested(UpdateEventArgs e) { UpdateRequested?.Invoke(null, e); diff --git a/src/Artemis.Storage/StorageManager.cs b/src/Artemis.Storage/StorageManager.cs index c87a86be1..3b787549c 100644 --- a/src/Artemis.Storage/StorageManager.cs +++ b/src/Artemis.Storage/StorageManager.cs @@ -9,6 +9,7 @@ public static class StorageManager { private static bool _ranMigrations; private static bool _inUse; + private static object _factoryLock = new(); /// /// Creates a backup of the database if the last backup is older than 10 minutes @@ -39,19 +40,22 @@ public static void CreateBackup(string dataFolder) File.Copy(database, Path.Combine(backupFolder, $"artemis-{DateTime.Now:yyyy-dd-M--HH-mm-ss}.db")); } - + public static ArtemisDbContext CreateDbContext(string dataFolder) { - _inUse = true; + lock (_factoryLock) + { + _inUse = true; - ArtemisDbContext dbContext = new() {DataFolder = dataFolder}; - if (_ranMigrations) - return dbContext; + ArtemisDbContext dbContext = new() {DataFolder = dataFolder}; + if (_ranMigrations) + return dbContext; - dbContext.Database.Migrate(); - dbContext.Database.ExecuteSqlRaw("PRAGMA optimize"); - _ranMigrations = true; + dbContext.Database.Migrate(); + dbContext.Database.ExecuteSqlRaw("PRAGMA optimize"); + _ranMigrations = true; - return dbContext; + return dbContext; + } } } \ No newline at end of file diff --git a/src/Artemis.UI/Routing/Routes.cs b/src/Artemis.UI/Routing/Routes.cs index 4353a4eef..8973793b0 100644 --- a/src/Artemis.UI/Routing/Routes.cs +++ b/src/Artemis.UI/Routing/Routes.cs @@ -20,57 +20,64 @@ namespace Artemis.UI.Routing; public static class Routes { - public static List ArtemisRoutes = new() - { + public static List ArtemisRoutes = + [ new RouteRegistration("blank"), new RouteRegistration("home"), new RouteRegistration("workshop") { - Children = new List - { + Children = + [ new RouteRegistration("offline/{message:string}"), new RouteRegistration("entries") { - Children = new List - { - new RouteRegistration("plugins/{page:int}"), - new RouteRegistration("plugins/details/{entryId:long}"), - new RouteRegistration("profiles/{page:int}"), - new RouteRegistration("profiles/details/{entryId:long}"), - new RouteRegistration("layouts/{page:int}"), - new RouteRegistration("layouts/details/{entryId:long}"), - } + Children = + [ + new RouteRegistration("plugins") + { + Children = [new RouteRegistration("details/{entryId:long}")] + }, + new RouteRegistration("profiles") + { + Children = [new RouteRegistration("details/{entryId:long}")] + }, + new RouteRegistration("layouts") + { + Children = [new RouteRegistration("details/{entryId:long}")] + }, + ] }, + new RouteRegistration("library") { - Children = new List - { + Children = + [ new RouteRegistration("installed"), new RouteRegistration("submissions"), - new RouteRegistration("submissions/{entryId:long}"), - } + new RouteRegistration("submissions/{entryId:long}") + ] } - } + ] }, + new RouteRegistration("surface-editor"), new RouteRegistration("settings") { - Children = new List - { + Children = + [ new RouteRegistration("general"), new RouteRegistration("plugins"), new RouteRegistration("devices"), new RouteRegistration("releases") { - Children = new List - { - new RouteRegistration("{releaseId:guid}") - } + Children = [new RouteRegistration("{releaseId:guid}")] }, + new RouteRegistration("account"), new RouteRegistration("about") - } + ] }, + new RouteRegistration("profile-editor/{profileConfigurationId:guid}") - }; + ]; } \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Device/Tabs/Layout/LayoutProviders/WorkshopLayoutViewModel.cs b/src/Artemis.UI/Screens/Device/Tabs/Layout/LayoutProviders/WorkshopLayoutViewModel.cs index 65c9c8d93..231093e79 100644 --- a/src/Artemis.UI/Screens/Device/Tabs/Layout/LayoutProviders/WorkshopLayoutViewModel.cs +++ b/src/Artemis.UI/Screens/Device/Tabs/Layout/LayoutProviders/WorkshopLayoutViewModel.cs @@ -64,7 +64,7 @@ public async Task BrowseLayouts() if (!await _windowService.ShowConfirmContentDialog("Open workshop", "Do you want to close this window and view the workshop?")) return false; - await _router.Navigate("workshop/entries/layouts/1"); + await _router.Navigate("workshop/entries/layouts"); return true; } diff --git a/src/Artemis.UI/Screens/Sidebar/SidebarViewModel.cs b/src/Artemis.UI/Screens/Sidebar/SidebarViewModel.cs index 3b7de1c0c..d2f8e9a23 100644 --- a/src/Artemis.UI/Screens/Sidebar/SidebarViewModel.cs +++ b/src/Artemis.UI/Screens/Sidebar/SidebarViewModel.cs @@ -41,9 +41,9 @@ public SidebarViewModel(IRouter router, IProfileService profileService, IWindowS new(MaterialIconKind.HomeOutline, "Home", "home"), new(MaterialIconKind.TestTube, "Workshop", "workshop", null, new ObservableCollection { - new(MaterialIconKind.FolderVideo, "Profiles", "workshop/entries/profiles/1", "workshop/entries/profiles"), - new(MaterialIconKind.KeyboardVariant, "Layouts", "workshop/entries/layouts/1", "workshop/entries/layouts"), - new(MaterialIconKind.Connection, "Plugins", "workshop/entries/plugins/1", "workshop/entries/plugins"), + new(MaterialIconKind.FolderVideo, "Profiles", "workshop/entries/profiles", "workshop/entries/profiles"), + new(MaterialIconKind.KeyboardVariant, "Layouts", "workshop/entries/layouts", "workshop/entries/layouts"), + new(MaterialIconKind.Connection, "Plugins", "workshop/entries/plugins", "workshop/entries/plugins"), new(MaterialIconKind.Bookshelf, "Library", "workshop/library"), }), diff --git a/src/Artemis.UI/Screens/Workshop/Entries/EntriesViewModel.cs b/src/Artemis.UI/Screens/Workshop/Entries/EntriesViewModel.cs index 904348bc6..e76014c34 100644 --- a/src/Artemis.UI/Screens/Workshop/Entries/EntriesViewModel.cs +++ b/src/Artemis.UI/Screens/Workshop/Entries/EntriesViewModel.cs @@ -24,9 +24,9 @@ public EntriesViewModel(IRouter router) Tabs = new ObservableCollection { - new("Profiles", "workshop/entries/profiles/1", "workshop/entries/profiles"), - new("Layouts", "workshop/entries/layouts/1", "workshop/entries/layouts"), - new("Plugins", "workshop/entries/plugins/1", "workshop/entries/plugins"), + new("Profiles", "workshop/entries/profiles", "workshop/entries/profiles"), + new("Layouts", "workshop/entries/layouts", "workshop/entries/layouts"), + new("Plugins", "workshop/entries/plugins", "workshop/entries/plugins"), }; this.WhenActivated(d => diff --git a/src/Artemis.UI/Screens/Workshop/Entries/List/EntryListInputView.axaml b/src/Artemis.UI/Screens/Workshop/Entries/List/EntryListInputView.axaml index d5c3b34f9..8fdc91d29 100644 --- a/src/Artemis.UI/Screens/Workshop/Entries/List/EntryListInputView.axaml +++ b/src/Artemis.UI/Screens/Workshop/Entries/List/EntryListInputView.axaml @@ -2,8 +2,6 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" - xmlns: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" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="Artemis.UI.Screens.Workshop.Entries.List.EntryListInputView" @@ -25,15 +23,6 @@ Download count - - Show per page - - 10 - 20 - 50 - 100 - - diff --git a/src/Artemis.UI/Screens/Workshop/Entries/List/EntryListInputViewModel.cs b/src/Artemis.UI/Screens/Workshop/Entries/List/EntryListInputViewModel.cs index f34348a8b..c327e3eb2 100644 --- a/src/Artemis.UI/Screens/Workshop/Entries/List/EntryListInputViewModel.cs +++ b/src/Artemis.UI/Screens/Workshop/Entries/List/EntryListInputViewModel.cs @@ -9,7 +9,6 @@ namespace Artemis.UI.Screens.Workshop.Entries.List; public partial class EntryListInputViewModel : ViewModelBase { private static string? _lastSearch; - private readonly PluginSetting _entriesPerPage; private readonly PluginSetting _sortBy; private string? _search; [Notify] private string _searchWatermark = "Search"; @@ -18,9 +17,7 @@ public partial class EntryListInputViewModel : ViewModelBase public EntryListInputViewModel(ISettingsService settingsService) { _search = _lastSearch; - _entriesPerPage = settingsService.GetSetting("Workshop.EntriesPerPage", 10); _sortBy = settingsService.GetSetting("Workshop.SortBy", 10); - _entriesPerPage.AutoSave = true; _sortBy.AutoSave = true; } @@ -33,17 +30,7 @@ public string? Search _lastSearch = value; } } - - public int EntriesPerPage - { - get => _entriesPerPage.Value; - set - { - _entriesPerPage.Value = value; - this.RaisePropertyChanged(); - } - } - + public int SortBy { get => _sortBy.Value; diff --git a/src/Artemis.UI/Screens/Workshop/Entries/List/EntryListViewModel.cs b/src/Artemis.UI/Screens/Workshop/Entries/List/EntryListViewModel.cs index 4d831ba29..92322920d 100644 --- a/src/Artemis.UI/Screens/Workshop/Entries/List/EntryListViewModel.cs +++ b/src/Artemis.UI/Screens/Workshop/Entries/List/EntryListViewModel.cs @@ -1,17 +1,16 @@ using System; using System.Collections.Generic; using System.Collections.ObjectModel; +using System.Linq; using System.Reactive.Disposables; using System.Reactive.Linq; using System.Threading; using System.Threading.Tasks; using Artemis.UI.Screens.Workshop.Categories; -using Artemis.UI.Screens.Workshop.Parameters; using Artemis.UI.Shared.Routing; using Artemis.UI.Shared.Services; using Artemis.UI.Shared.Services.Builders; using Artemis.WebClient.Workshop; -using Avalonia.Threading; using DynamicData; using PropertyChanged.SourceGenerator; using ReactiveUI; @@ -19,21 +18,20 @@ namespace Artemis.UI.Screens.Workshop.Entries.List; -public abstract partial class EntryListViewModel : RoutableScreen +public abstract partial class EntryListViewModel : RoutableHostScreen { private readonly SourceList _entries = new(); - private readonly ObservableAsPropertyHelper _isLoading; private readonly INotificationService _notificationService; - private readonly string _route; - private readonly ObservableAsPropertyHelper _showPagination; private readonly IWorkshopClient _workshopClient; - [Notify] private int _page; - [Notify] private int _loadedPage = -1; - [Notify] private int _totalPages = 1; + private readonly string _route; + private IGetEntriesv2_EntriesV2_PageInfo? _currentPageInfo; + + [Notify] private bool _initializing = true; + [Notify] private bool _fetchingMore; + [Notify] private int _entriesPerFetch; protected EntryListViewModel(string route, IWorkshopClient workshopClient, - IRouter router, CategoriesViewModel categoriesViewModel, EntryListInputViewModel entryListInputViewModel, INotificationService notificationService, @@ -42,46 +40,37 @@ protected EntryListViewModel(string route, _route = route; _workshopClient = workshopClient; _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; InputViewModel = entryListInputViewModel; _entries.Connect() - .ObserveOn(new AvaloniaSynchronizationContext(DispatcherPriority.SystemIdle)) .Transform(getEntryListViewModel) .Bind(out ReadOnlyObservableCollection entries) .Subscribe(); Entries = entries; - // Respond to page changes - this.WhenAnyValue(vm => vm.Page).Skip(1).Subscribe(p => Task.Run(() => router.Navigate($"{_route}/{p}"))); - this.WhenActivated(d => { // 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.SortBy, vm => vm.EntriesPerPage).Skip(1).Subscribe(_ => RefreshToStart()).DisposeWith(d); - CategoriesViewModel.WhenAnyValue(vm => vm.CategoryFilters).Skip(1).Subscribe(_ => RefreshToStart()).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); }); } - public bool ShowPagination => _showPagination.Value; - public bool IsLoading => _isLoading.Value; - public CategoriesViewModel CategoriesViewModel { get; } public EntryListInputViewModel InputViewModel { get; } public ReadOnlyObservableCollection Entries { get; } - - public override async Task OnNavigating(WorkshopListParameters parameters, NavigationArguments args, CancellationToken cancellationToken) - { - Page = Math.Max(1, parameters.Page); - await Task.Delay(200, cancellationToken); - if (!cancellationToken.IsCancellationRequested) - await Query(cancellationToken); + 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) @@ -92,6 +81,43 @@ public override Task OnClosing(NavigationArguments 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 sort = GetSort(); + + try + { + IOperationResult 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() { @@ -117,59 +143,10 @@ protected virtual IReadOnlyList GetSort() }; } - private void RefreshToStart() + private void Reset() { - // Reset to page one, will trigger a query - if (Page != 1) - Page = 1; - // 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 sort = GetSort(); - IOperationResult 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; - } + _entries.Clear(); + _currentPageInfo = null; + Task.Run(() => FetchMore(CancellationToken.None)); } } \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Workshop/Entries/Tabs/LayoutListView.axaml b/src/Artemis.UI/Screens/Workshop/Entries/Tabs/LayoutListView.axaml index 3f8aa658a..1c0c4182c 100644 --- a/src/Artemis.UI/Screens/Workshop/Entries/Tabs/LayoutListView.axaml +++ b/src/Artemis.UI/Screens/Workshop/Entries/Tabs/LayoutListView.axaml @@ -2,8 +2,9 @@ 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:pagination="clr-namespace:Artemis.UI.Shared.Pagination;assembly=Artemis.UI.Shared" 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"> @@ -15,51 +16,52 @@ - - - - - Categories - - - - - - - - - - - - - - - - - - - - - - - - - - Looks like your current filters gave no results - - Modify or clear your filters to view other device layouts - - + + + + + + Categories + + + + - - - + + + + + + + + + + + + + + + + + + + + + Looks like your current filters gave no results + + Modify or clear your filters to view other device layouts + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Workshop/Entries/Tabs/LayoutListView.axaml.cs b/src/Artemis.UI/Screens/Workshop/Entries/Tabs/LayoutListView.axaml.cs index 6b574bb29..a003f47bb 100644 --- a/src/Artemis.UI/Screens/Workshop/Entries/Tabs/LayoutListView.axaml.cs +++ b/src/Artemis.UI/Screens/Workshop/Entries/Tabs/LayoutListView.axaml.cs @@ -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.Threading; +using ReactiveUI; namespace Artemis.UI.Screens.Workshop.Entries.Tabs; @@ -7,5 +14,30 @@ public partial class LayoutListView : ReactiveUserControl public LayoutListView() { 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); } } \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Workshop/Entries/Tabs/LayoutListViewModel.cs b/src/Artemis.UI/Screens/Workshop/Entries/Tabs/LayoutListViewModel.cs index ca0cdbb99..11c97846c 100644 --- a/src/Artemis.UI/Screens/Workshop/Entries/Tabs/LayoutListViewModel.cs +++ b/src/Artemis.UI/Screens/Workshop/Entries/Tabs/LayoutListViewModel.cs @@ -10,12 +10,11 @@ namespace Artemis.UI.Screens.Workshop.Entries.Tabs; public class LayoutListViewModel : List.EntryListViewModel { public LayoutListViewModel(IWorkshopClient workshopClient, - IRouter router, CategoriesViewModel categoriesViewModel, EntryListInputViewModel entryListInputViewModel, INotificationService notificationService, Func getEntryListViewModel) - : base("workshop/entries/layouts", workshopClient, router, categoriesViewModel, entryListInputViewModel, notificationService, getEntryListViewModel) + : base("workshop/entries/layouts", workshopClient, categoriesViewModel, entryListInputViewModel, notificationService, getEntryListViewModel) { entryListInputViewModel.SearchWatermark = "Search layouts"; } diff --git a/src/Artemis.UI/Screens/Workshop/Entries/Tabs/PluginListView.axaml b/src/Artemis.UI/Screens/Workshop/Entries/Tabs/PluginListView.axaml index e52c30641..7b6cb1f0c 100644 --- a/src/Artemis.UI/Screens/Workshop/Entries/Tabs/PluginListView.axaml +++ b/src/Artemis.UI/Screens/Workshop/Entries/Tabs/PluginListView.axaml @@ -3,11 +3,12 @@ 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: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" x:Class="Artemis.UI.Screens.Workshop.Entries.Tabs.PluginListView" x:DataType="tabs:PluginListViewModel"> - + - - - - - Categories - - - - - - - - - - - - - - - - - - - - - - - - - - - Looks like your current filters gave no results - - Modify or clear your filters to view other plugins - - + + + + + + + Categories + + + + - - - - + + + + + + + + + + + + + + + + + + + + + Looks like your current filters gave no results + + Modify or clear your filters to view other plugins + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Workshop/Entries/Tabs/PluginListView.axaml.cs b/src/Artemis.UI/Screens/Workshop/Entries/Tabs/PluginListView.axaml.cs index 2e32eea93..8cfaa1696 100644 --- a/src/Artemis.UI/Screens/Workshop/Entries/Tabs/PluginListView.axaml.cs +++ b/src/Artemis.UI/Screens/Workshop/Entries/Tabs/PluginListView.axaml.cs @@ -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.Markup.Xaml; using Avalonia.ReactiveUI; +using Avalonia.Threading; +using ReactiveUI; namespace Artemis.UI.Screens.Workshop.Entries.Tabs; @@ -10,5 +14,30 @@ public partial class PluginListView : ReactiveUserControl 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); } } \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Workshop/Entries/Tabs/PluginListViewModel.cs b/src/Artemis.UI/Screens/Workshop/Entries/Tabs/PluginListViewModel.cs index 7af13b4e4..c7ea484a6 100644 --- a/src/Artemis.UI/Screens/Workshop/Entries/Tabs/PluginListViewModel.cs +++ b/src/Artemis.UI/Screens/Workshop/Entries/Tabs/PluginListViewModel.cs @@ -10,12 +10,11 @@ namespace Artemis.UI.Screens.Workshop.Entries.Tabs; public class PluginListViewModel : EntryListViewModel { public PluginListViewModel(IWorkshopClient workshopClient, - IRouter router, CategoriesViewModel categoriesViewModel, EntryListInputViewModel entryListInputViewModel, INotificationService notificationService, Func getEntryListViewModel) - : base("workshop/entries/plugins", workshopClient, router, categoriesViewModel, entryListInputViewModel, notificationService, getEntryListViewModel) + : base("workshop/entries/plugins", workshopClient, categoriesViewModel, entryListInputViewModel, notificationService, getEntryListViewModel) { entryListInputViewModel.SearchWatermark = "Search plugins"; } diff --git a/src/Artemis.UI/Screens/Workshop/Entries/Tabs/ProfileListView.axaml b/src/Artemis.UI/Screens/Workshop/Entries/Tabs/ProfileListView.axaml index cd6e6641b..03028d4b8 100644 --- a/src/Artemis.UI/Screens/Workshop/Entries/Tabs/ProfileListView.axaml +++ b/src/Artemis.UI/Screens/Workshop/Entries/Tabs/ProfileListView.axaml @@ -2,9 +2,10 @@ 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:pagination="clr-namespace:Artemis.UI.Shared.Pagination;assembly=Artemis.UI.Shared" 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:DataType="tabs:ProfileListViewModel"> @@ -15,51 +16,52 @@ - - - - - Categories - - - - - - - - - - - - - - - - - - - - - - - - - - Looks like your current filters gave no results - - Modify or clear your filters to view some awesome profiles - - + + + + + + Categories + + + + - - - + + + + + + + + + + + + + + + + + + + + + Looks like your current filters gave no results + + Modify or clear your filters to view some awesome profiles + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Workshop/Entries/Tabs/ProfileListView.axaml.cs b/src/Artemis.UI/Screens/Workshop/Entries/Tabs/ProfileListView.axaml.cs index 62adad88b..b25ba45f3 100644 --- a/src/Artemis.UI/Screens/Workshop/Entries/Tabs/ProfileListView.axaml.cs +++ b/src/Artemis.UI/Screens/Workshop/Entries/Tabs/ProfileListView.axaml.cs @@ -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.Threading; +using ReactiveUI; namespace Artemis.UI.Screens.Workshop.Entries.Tabs; @@ -7,5 +14,30 @@ public partial class ProfileListView : ReactiveUserControl 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); } } \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Workshop/Entries/Tabs/ProfileListViewModel.cs b/src/Artemis.UI/Screens/Workshop/Entries/Tabs/ProfileListViewModel.cs index d8ae07a17..09ed5410b 100644 --- a/src/Artemis.UI/Screens/Workshop/Entries/Tabs/ProfileListViewModel.cs +++ b/src/Artemis.UI/Screens/Workshop/Entries/Tabs/ProfileListViewModel.cs @@ -10,12 +10,11 @@ namespace Artemis.UI.Screens.Workshop.Entries.Tabs; public class ProfileListViewModel : List.EntryListViewModel { public ProfileListViewModel(IWorkshopClient workshopClient, - IRouter router, CategoriesViewModel categoriesViewModel, EntryListInputViewModel entryListInputViewModel, INotificationService notificationService, Func getEntryListViewModel) - : base("workshop/entries/profiles", workshopClient, router, categoriesViewModel, entryListInputViewModel, notificationService, getEntryListViewModel) + : base("workshop/entries/profiles", workshopClient, categoriesViewModel, entryListInputViewModel, notificationService, getEntryListViewModel) { entryListInputViewModel.SearchWatermark = "Search profiles"; } diff --git a/src/Artemis.UI/Screens/Workshop/Home/WorkshopHomeView.axaml b/src/Artemis.UI/Screens/Workshop/Home/WorkshopHomeView.axaml index c2ed7ebb8..4d535f0ba 100644 --- a/src/Artemis.UI/Screens/Workshop/Home/WorkshopHomeView.axaml +++ b/src/Artemis.UI/Screens/Workshop/Home/WorkshopHomeView.axaml @@ -41,7 +41,7 @@ - - -