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:
parent
7e981a61d3
commit
da3d47d7b8
@ -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);
|
||||||
|
|||||||
@ -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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -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}")
|
||||||
};
|
];
|
||||||
}
|
}
|
||||||
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -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"),
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
|||||||
@ -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 =>
|
||||||
|
|||||||
@ -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"/>
|
||||||
|
|||||||
@ -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;
|
||||||
|
|||||||
@ -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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -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>
|
||||||
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -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";
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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>
|
||||||
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -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";
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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>
|
||||||
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -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";
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -1,6 +0,0 @@
|
|||||||
namespace Artemis.UI.Screens.Workshop.Parameters;
|
|
||||||
|
|
||||||
public class WorkshopListParameters
|
|
||||||
{
|
|
||||||
public int Page { get; set; }
|
|
||||||
}
|
|
||||||
@ -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>
|
||||||
@ -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;
|
||||||
|
|
||||||
|
|||||||
@ -68,4 +68,11 @@ fragment release on Release {
|
|||||||
downloadSize
|
downloadSize
|
||||||
md5Hash
|
md5Hash
|
||||||
createdAt
|
createdAt
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fragment pluginInfo on PluginInfo {
|
||||||
|
requiresAdmin
|
||||||
|
supportsWindows
|
||||||
|
supportsLinux
|
||||||
|
supportsOSX
|
||||||
|
}
|
||||||
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -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
|
||||||
|
|||||||
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user