diff --git a/src/Artemis.UI.Shared/Styles/Border.axaml b/src/Artemis.UI.Shared/Styles/Border.axaml
index d753809a0..e3997e5ec 100644
--- a/src/Artemis.UI.Shared/Styles/Border.axaml
+++ b/src/Artemis.UI.Shared/Styles/Border.axaml
@@ -23,15 +23,11 @@
-
-
- 8
-
-
+
+
+
+
+
+
+ You are not logged in
+
+ In order to manage your submissions you must be logged in.
+
+
+
+
+
+
+
+ Oh boy, it's empty here 🤔
+
+ Any entries you submit to the workshop you can later manage here
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Artemis.UI/Screens/Workshop/Library/Tabs/LibrarySubmissionsViewModel.cs b/src/Artemis.UI/Screens/Workshop/Library/Tabs/LibrarySubmissionsViewModel.cs
index cbb98d615..3421b6aff 100644
--- a/src/Artemis.UI/Screens/Workshop/Library/Tabs/LibrarySubmissionsViewModel.cs
+++ b/src/Artemis.UI/Screens/Workshop/Library/Tabs/LibrarySubmissionsViewModel.cs
@@ -1,8 +1,108 @@
+using System;
+using System.Collections.ObjectModel;
+using System.Reactive;
+using System.Threading;
+using System.Threading.Tasks;
+using Artemis.UI.Extensions;
+using Artemis.UI.Screens.Workshop.CurrentUser;
+using Artemis.UI.Screens.Workshop.SubmissionWizard;
using Artemis.UI.Shared;
+using Artemis.UI.Shared.Routing;
+using Artemis.UI.Shared.Services;
+using Artemis.WebClient.Workshop;
+using Artemis.WebClient.Workshop.Services;
+using DynamicData;
+using ReactiveUI;
+using StrawberryShake;
namespace Artemis.UI.Screens.Workshop.Library.Tabs;
-public class LibrarySubmissionsViewModel : ActivatableViewModelBase
+public class LibrarySubmissionsViewModel : ActivatableViewModelBase, IWorkshopViewModel
{
-
+ private readonly IWorkshopClient _client;
+ private readonly SourceCache _entries;
+ private readonly IWindowService _windowService;
+ private bool _isLoading = true;
+ private bool _workshopReachable;
+
+ public LibrarySubmissionsViewModel(IWorkshopClient client, IAuthenticationService authenticationService, IWindowService windowService, IWorkshopService workshopService, IRouter router)
+ {
+ _client = client;
+ _windowService = windowService;
+ _entries = new SourceCache(e => e.Id);
+ _entries.Connect().Bind(out ReadOnlyObservableCollection entries).Subscribe();
+
+ AddSubmission = ReactiveCommand.CreateFromTask(ExecuteAddSubmission, this.WhenAnyValue(vm => vm.WorkshopReachable));
+ Login = ReactiveCommand.CreateFromTask(ExecuteLogin, this.WhenAnyValue(vm => vm.WorkshopReachable));
+ NavigateToEntry = ReactiveCommand.CreateFromTask(ExecuteNavigateToEntry);
+
+ IsLoggedIn = authenticationService.IsLoggedIn;
+ Entries = entries;
+
+ this.WhenActivatedAsync(async d =>
+ {
+ WorkshopReachable = await workshopService.ValidateWorkshopStatus(d.AsCancellationToken());
+ if (WorkshopReachable)
+ await GetEntries(d.AsCancellationToken());
+ });
+ }
+
+ public ReactiveCommand Login { get; }
+ public ReactiveCommand AddSubmission { get; }
+ public ReactiveCommand NavigateToEntry { get; }
+
+ public IObservable IsLoggedIn { get; }
+ public ReadOnlyObservableCollection Entries { get; }
+
+ public bool WorkshopReachable
+ {
+ get => _workshopReachable;
+ set => RaiseAndSetIfChanged(ref _workshopReachable, value);
+ }
+
+ public bool IsLoading
+ {
+ get => _isLoading;
+ set => RaiseAndSetIfChanged(ref _isLoading, value);
+ }
+
+ private async Task ExecuteLogin(CancellationToken ct)
+ {
+ await _windowService.CreateContentDialog().WithViewModel(out WorkshopLoginViewModel _).WithTitle("Workshop login").ShowAsync();
+ }
+
+ private async Task ExecuteAddSubmission(CancellationToken arg)
+ {
+ await _windowService.ShowDialogAsync();
+ }
+
+ private Task ExecuteNavigateToEntry(IGetSubmittedEntries_SubmittedEntries entry, CancellationToken cancellationToken)
+ {
+ return Task.CompletedTask;
+ }
+
+ private async Task GetEntries(CancellationToken ct)
+ {
+ IsLoading = true;
+
+ try
+ {
+ IOperationResult result = await _client.GetSubmittedEntries.ExecuteAsync(null, ct);
+
+ if (result.Data?.SubmittedEntries == null)
+ _entries.Clear();
+ else
+ _entries.Edit(e =>
+ {
+ e.Clear();
+ e.AddOrUpdate(result.Data.SubmittedEntries);
+ });
+ }
+ finally
+ {
+ IsLoading = false;
+ }
+ }
+
+ public EntryType? EntryType => null;
}
\ No newline at end of file
diff --git a/src/Artemis.UI/Screens/Workshop/Library/WorkshopLibraryVIew.axaml b/src/Artemis.UI/Screens/Workshop/Library/WorkshopLibraryVIew.axaml
index f6e2f5740..1dafa387c 100644
--- a/src/Artemis.UI/Screens/Workshop/Library/WorkshopLibraryVIew.axaml
+++ b/src/Artemis.UI/Screens/Workshop/Library/WorkshopLibraryVIew.axaml
@@ -2,26 +2,25 @@
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:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
+ xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
xmlns:library="clr-namespace:Artemis.UI.Screens.Workshop.Library"
- xmlns:routing="clr-namespace:Artemis.UI.Routing"
- xmlns:ui1="clr-namespace:Artemis.UI"
+ xmlns:ui="clr-namespace:Artemis.UI"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Artemis.UI.Screens.Workshop.Library.WorkshopLibraryView"
x:DataType="library:WorkshopLibraryViewModel">
-
-
+
+
-
-
+
-
-
-
-
-
-
+
+
+
+
+
+
diff --git a/src/Artemis.UI/Screens/Workshop/Library/WorkshopLibraryVIew.axaml.cs b/src/Artemis.UI/Screens/Workshop/Library/WorkshopLibraryVIew.axaml.cs
index f2faa7e5a..5057296ac 100644
--- a/src/Artemis.UI/Screens/Workshop/Library/WorkshopLibraryVIew.axaml.cs
+++ b/src/Artemis.UI/Screens/Workshop/Library/WorkshopLibraryVIew.axaml.cs
@@ -21,17 +21,18 @@ public partial class WorkshopLibraryView : ReactiveUserControl
- {
- if (ViewModel == null)
- return;
-
- SlideNavigationTransitionInfo transitionInfo = new()
- {
- Effect = ViewModel.Tabs.IndexOf(ViewModel.SelectedTab) > _lastIndex ? SlideNavigationTransitionEffect.FromRight : SlideNavigationTransitionEffect.FromLeft
- };
- TabFrame.NavigateFromObject(viewModel, new FrameNavigationOptions {TransitionInfoOverride = transitionInfo});
- _lastIndex = ViewModel.Tabs.IndexOf(ViewModel.SelectedTab);
- });
+ Dispatcher.UIThread.Invoke(() => TabFrame.NavigateFromObject(viewModel, new FrameNavigationOptions {TransitionInfoOverride = GetTransitionInfo()}));
+ }
+
+ private SlideNavigationTransitionInfo GetTransitionInfo()
+ {
+ if (ViewModel?.SelectedTab == null)
+ return new SlideNavigationTransitionInfo();
+
+ SlideNavigationTransitionEffect effect = ViewModel.Tabs.IndexOf(ViewModel.SelectedTab) > _lastIndex ? SlideNavigationTransitionEffect.FromRight : SlideNavigationTransitionEffect.FromLeft;
+ SlideNavigationTransitionInfo info = new() {Effect = effect};
+ _lastIndex = ViewModel.Tabs.IndexOf(ViewModel.SelectedTab);
+
+ return info;
}
}
\ No newline at end of file
diff --git a/src/Artemis.UI/Screens/Workshop/Profile/ProfileDetailsView.axaml b/src/Artemis.UI/Screens/Workshop/Profile/ProfileDetailsView.axaml
index 00ffc76c9..f41c65768 100644
--- a/src/Artemis.UI/Screens/Workshop/Profile/ProfileDetailsView.axaml
+++ b/src/Artemis.UI/Screens/Workshop/Profile/ProfileDetailsView.axaml
@@ -23,8 +23,7 @@
-
-
- Categories
-
-
-
+
+
+ Categories
+
+
+
-
-
+
+
+
diff --git a/src/Artemis.UI/Screens/Workshop/Profile/ProfileListViewModel.cs b/src/Artemis.UI/Screens/Workshop/Profile/ProfileListViewModel.cs
index 23ddddfa2..521c9a351 100644
--- a/src/Artemis.UI/Screens/Workshop/Profile/ProfileListViewModel.cs
+++ b/src/Artemis.UI/Screens/Workshop/Profile/ProfileListViewModel.cs
@@ -1,165 +1,44 @@
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.Entries;
-using Artemis.UI.Screens.Workshop.Parameters;
-using Artemis.UI.Shared;
using Artemis.UI.Shared.Routing;
using Artemis.UI.Shared.Services;
-using Artemis.UI.Shared.Services.Builders;
using Artemis.WebClient.Workshop;
-using Avalonia.Threading;
-using DryIoc.ImTools;
-using DynamicData;
-using ReactiveUI;
-using StrawberryShake;
namespace Artemis.UI.Screens.Workshop.Profile;
-public class ProfileListViewModel : RoutableScreen, IWorkshopViewModel
+public class ProfileListViewModel : EntryListBaseViewModel
{
- private readonly INotificationService _notificationService;
- private readonly Func _getEntryListViewModel;
- private readonly IWorkshopClient _workshopClient;
- private readonly ObservableAsPropertyHelper _showPagination;
- private readonly ObservableAsPropertyHelper _isLoading;
- private SourceList _entries = new();
- private int _page;
- private int _loadedPage = -1;
- private int _totalPages = 1;
- private int _entriesPerPage = 10;
-
+ ///
public ProfileListViewModel(IWorkshopClient workshopClient,
- IRouter router,
- CategoriesViewModel categoriesViewModel,
+ IRouter router,
+ CategoriesViewModel categoriesViewModel,
INotificationService notificationService,
- Func getEntryListViewModel)
+ Func getEntryListViewModel)
+ : base(workshopClient, router, categoriesViewModel, notificationService, getEntryListViewModel)
{
- _workshopClient = workshopClient;
- _notificationService = notificationService;
- _getEntryListViewModel = getEntryListViewModel;
- _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;
-
- _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($"workshop/profiles/{p}")));
-
- // Respond to filter changes
- this.WhenActivated(d => CategoriesViewModel.WhenAnyValue(vm => vm.CategoryFilters).Skip(1).Subscribe(_ =>
+ #region Overrides of EntryListBaseViewModel
+
+ ///
+ protected override string GetPagePath(int page)
+ {
+ return $"workshop/profiles/{page}";
+ }
+
+ ///
+ protected override EntryFilterInput GetFilter()
+ {
+ return new EntryFilterInput
{
- // 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));
- }).DisposeWith(d));
- }
-
- public bool ShowPagination => _showPagination.Value;
- public bool IsLoading => _isLoading.Value;
-
- public CategoriesViewModel CategoriesViewModel { get; }
-
- public ReadOnlyObservableCollection Entries { get; }
-
- public int Page
- {
- get => _page;
- set => RaiseAndSetIfChanged(ref _page, value);
- }
-
- public int LoadedPage
- {
- get => _loadedPage;
- set => RaiseAndSetIfChanged(ref _loadedPage, value);
- }
-
- public int TotalPages
- {
- get => _totalPages;
- set => RaiseAndSetIfChanged(ref _totalPages, value);
- }
-
- public int EntriesPerPage
- {
- get => _entriesPerPage;
- set => RaiseAndSetIfChanged(ref _entriesPerPage, value);
- }
-
- public override async Task OnNavigating(WorkshopListParameters parameters, NavigationArguments args, CancellationToken cancellationToken)
- {
- Page = Math.Max(1, parameters.Page);
-
- // Throttle page changes, wait longer for the first one to keep UI smooth
- // if (Entries == null)
- // await Task.Delay(400, cancellationToken);
- // else
- await Task.Delay(200, cancellationToken);
-
- if (!cancellationToken.IsCancellationRequested)
- await Query(cancellationToken);
- }
-
- private async Task Query(CancellationToken cancellationToken)
- {
- try
- {
- EntryFilterInput filter = GetFilter();
- IOperationResult entries = await _workshopClient.GetEntries.ExecuteAsync(filter, EntriesPerPage * (Page - 1), EntriesPerPage, cancellationToken);
- entries.EnsureNoErrors();
-
- if (entries.Data?.Entries?.Items != null)
+ And = new[]
{
- TotalPages = (int) Math.Ceiling(entries.Data.Entries.TotalCount / (double) EntriesPerPage);
- _entries.Edit(e =>
- {
- e.Clear();
- e.AddRange(entries.Data.Entries.Items);
- });
+ new EntryFilterInput {EntryType = new EntryTypeOperationFilterInput {Eq = WebClient.Workshop.EntryType.Profile}},
+ base.GetFilter()
}
- else
- TotalPages = 1;
- }
- catch (Exception e)
- {
- _notificationService.CreateNotification()
- .WithTitle("Failed to load entries")
- .WithMessage(e.Message)
- .WithSeverity(NotificationSeverity.Error)
- .Show();
- }
- finally
- {
- LoadedPage = Page;
- }
- }
-
- private EntryFilterInput GetFilter()
- {
- EntryFilterInput filter = new()
- {
- EntryType = new EntryTypeOperationFilterInput {Eq = WebClient.Workshop.EntryType.Profile},
- And = CategoriesViewModel.CategoryFilters
};
-
- return filter;
}
- public EntryType? EntryType => null;
+ #endregion
}
\ No newline at end of file
diff --git a/src/Artemis.WebClient.Workshop/Artemis.WebClient.Workshop.csproj b/src/Artemis.WebClient.Workshop/Artemis.WebClient.Workshop.csproj
index d1c6d5490..d2838936b 100644
--- a/src/Artemis.WebClient.Workshop/Artemis.WebClient.Workshop.csproj
+++ b/src/Artemis.WebClient.Workshop/Artemis.WebClient.Workshop.csproj
@@ -35,5 +35,8 @@
MSBuild:GenerateGraphQLCode
+
+ MSBuild:GenerateGraphQLCode
+
diff --git a/src/Artemis.WebClient.Workshop/Queries/GetSubmittedEntries.graphql b/src/Artemis.WebClient.Workshop/Queries/GetSubmittedEntries.graphql
new file mode 100644
index 000000000..422fcaf90
--- /dev/null
+++ b/src/Artemis.WebClient.Workshop/Queries/GetSubmittedEntries.graphql
@@ -0,0 +1,10 @@
+query GetSubmittedEntries($filter: EntryFilterInput) {
+ submittedEntries(where: $filter order: {createdAt: DESC}) {
+ id
+ name
+ summary
+ entryType
+ downloads
+ createdAt
+ }
+}
\ No newline at end of file
diff --git a/src/Artemis.WebClient.Workshop/Services/IWorkshopService.cs b/src/Artemis.WebClient.Workshop/Services/IWorkshopService.cs
index c79bde1e1..6b18e7d12 100644
--- a/src/Artemis.WebClient.Workshop/Services/IWorkshopService.cs
+++ b/src/Artemis.WebClient.Workshop/Services/IWorkshopService.cs
@@ -1,30 +1,19 @@
-using System.Net;
using System.Net.Http.Headers;
using Artemis.UI.Shared.Routing;
-using Artemis.UI.Shared.Services.MainWindow;
using Artemis.UI.Shared.Utilities;
using Artemis.WebClient.Workshop.UploadHandlers;
-using Avalonia.Media.Imaging;
-using Avalonia.Threading;
namespace Artemis.WebClient.Workshop.Services;
public class WorkshopService : IWorkshopService
{
- private readonly Dictionary _entryIconCache = new();
private readonly IHttpClientFactory _httpClientFactory;
private readonly IRouter _router;
- private readonly SemaphoreSlim _iconCacheLock = new(1);
- public WorkshopService(IHttpClientFactory httpClientFactory, IMainWindowService mainWindowService, IRouter router)
+ public WorkshopService(IHttpClientFactory httpClientFactory, IRouter router)
{
_httpClientFactory = httpClientFactory;
_router = router;
- mainWindowService.MainWindowClosed += (_, _) => Dispatcher.UIThread.InvokeAsync(async () =>
- {
- await Task.Delay(1000);
- ClearCache();
- });
}
public async Task SetEntryIcon(Guid entryId, Progress progress, Stream icon, CancellationToken cancellationToken)
@@ -48,52 +37,41 @@ public class WorkshopService : IWorkshopService
}
///
- public async Task GetWorkshopStatus()
+ public async Task GetWorkshopStatus(CancellationToken cancellationToken)
{
try
{
// Don't use the workshop client which adds auth headers
HttpClient client = _httpClientFactory.CreateClient();
- HttpResponseMessage response = await client.SendAsync(new HttpRequestMessage(HttpMethod.Head, WorkshopConstants.WORKSHOP_URL + "/status"));
+ HttpResponseMessage response = await client.SendAsync(new HttpRequestMessage(HttpMethod.Head, WorkshopConstants.WORKSHOP_URL + "/status"), cancellationToken);
return new IWorkshopService.WorkshopStatus(response.IsSuccessStatusCode, response.StatusCode.ToString());
}
+ catch (OperationCanceledException e)
+ {
+ return new IWorkshopService.WorkshopStatus(false, e.Message);
+ }
catch (HttpRequestException e)
{
return new IWorkshopService.WorkshopStatus(false, e.Message);
}
}
+ ///
///
- public async Task ValidateWorkshopStatus()
+ public async Task ValidateWorkshopStatus(CancellationToken cancellationToken)
{
- IWorkshopService.WorkshopStatus status = await GetWorkshopStatus();
+ IWorkshopService.WorkshopStatus status = await GetWorkshopStatus(cancellationToken);
if (!status.IsReachable)
await _router.Navigate($"workshop/offline/{status.Message}");
return status.IsReachable;
}
-
- private void ClearCache()
- {
- try
- {
- List values = _entryIconCache.Values.ToList();
- _entryIconCache.Clear();
- foreach (Stream bitmap in values)
- bitmap.Dispose();
- }
- catch (Exception e)
- {
- Console.WriteLine(e);
- throw;
- }
- }
}
public interface IWorkshopService
{
Task SetEntryIcon(Guid entryId, Progress progress, Stream icon, CancellationToken cancellationToken);
- Task GetWorkshopStatus();
- Task ValidateWorkshopStatus();
+ Task GetWorkshopStatus(CancellationToken cancellationToken);
+ Task ValidateWorkshopStatus(CancellationToken cancellationToken);
public record WorkshopStatus(bool IsReachable, string Message);
}
\ No newline at end of file
diff --git a/src/Artemis.WebClient.Workshop/graphql.config.yml b/src/Artemis.WebClient.Workshop/graphql.config.yml
index 9662a514f..a8ba99703 100644
--- a/src/Artemis.WebClient.Workshop/graphql.config.yml
+++ b/src/Artemis.WebClient.Workshop/graphql.config.yml
@@ -2,7 +2,7 @@ schema: schema.graphql
extensions:
endpoints:
Default GraphQL Endpoint:
- url: https://workshop.artemis-rgb.com/graphql
+ url: https://localhost:7281/graphql
headers:
user-agent: JS GraphQL
introspect: true
diff --git a/src/Artemis.WebClient.Workshop/schema.graphql b/src/Artemis.WebClient.Workshop/schema.graphql
index 8b99d85ea..bed72a4a2 100644
--- a/src/Artemis.WebClient.Workshop/schema.graphql
+++ b/src/Artemis.WebClient.Workshop/schema.graphql
@@ -41,6 +41,7 @@ type Entry {
id: UUID!
images: [Image!]!
latestRelease: Release
+ latestReleaseId: UUID
name: String!
releases: [Release!]!
summary: String!
@@ -62,6 +63,7 @@ type Query {
entries(order: [EntrySortInput!], skip: Int, take: Int, where: EntryFilterInput): EntriesCollectionSegment
entry(id: UUID!): Entry
searchEntries(input: String!, order: [EntrySortInput!], type: EntryType, where: EntryFilterInput): [Entry!]!
+ submittedEntries(order: [EntrySortInput!], where: EntryFilterInput): [Entry!]!
}
type Release {
@@ -156,6 +158,8 @@ input EntryFilterInput {
iconId: UuidOperationFilterInput
id: UuidOperationFilterInput
images: ListFilterInputTypeOfImageFilterInput
+ latestRelease: ReleaseFilterInput
+ latestReleaseId: UuidOperationFilterInput
name: StringOperationFilterInput
or: [EntryFilterInput!]
releases: ListFilterInputTypeOfReleaseFilterInput
@@ -173,6 +177,8 @@ input EntrySortInput {
icon: ImageSortInput
iconId: SortEnumType
id: SortEnumType
+ latestRelease: ReleaseSortInput
+ latestReleaseId: SortEnumType
name: SortEnumType
summary: SortEnumType
}
@@ -267,6 +273,17 @@ input ReleaseFilterInput {
version: StringOperationFilterInput
}
+input ReleaseSortInput {
+ createdAt: SortEnumType
+ downloadSize: SortEnumType
+ downloads: SortEnumType
+ entry: EntrySortInput
+ entryId: SortEnumType
+ id: SortEnumType
+ md5Hash: SortEnumType
+ version: SortEnumType
+}
+
input StringOperationFilterInput {
and: [StringOperationFilterInput!]
contains: String