From 0667d58ed8e31e4f9e4797f6223db95795b197cf Mon Sep 17 00:00:00 2001 From: RobertBeekman Date: Sun, 5 Nov 2023 21:27:41 +0100 Subject: [PATCH] Images WIP --- src/Artemis.UI/Artemis.UI.csproj | 15 +++ .../Screens/Debugger/DebugViewModel.cs | 5 +- .../Tabs/Workshop/WorkshopDebugView.axaml | 32 +++++ .../Tabs/Workshop/WorkshopDebugView.axaml.cs | 14 ++ .../Tabs/Workshop/WorkshopDebugViewModel.cs | 30 +++++ .../Entries/Details/EntryImageView.axaml | 24 ++++ .../Entries/Details/EntryImageView.axaml.cs | 13 ++ .../Entries/Details/EntryImageViewModel.cs | 17 +++ .../Entries/Details/EntryImagesView.axaml | 28 ++++ .../Entries/Details/EntryImagesView.axaml.cs | 13 ++ .../Entries/Details/EntryImagesViewModel.cs | 16 +++ .../Entries/Details/EntryInfoView.axaml | 83 ++++++++++++ .../Entries/Details/EntryInfoView.axaml.cs | 13 ++ .../Entries/Details/EntryInfoViewModel.cs | 28 ++++ .../Entries/Details/EntryReleasesView.axaml | 52 ++++++++ .../Details/EntryReleasesView.axaml.cs | 13 ++ .../Entries/Details/EntryReleasesViewModel.cs | 59 +++++++++ .../EntrySpecificationsView.axaml | 5 +- .../EntrySpecificationsView.axaml.cs | 10 +- .../EntrySpecificationsViewModel.cs | 10 +- .../Entries/EntryInstallationDialogView.axaml | 8 -- .../EntryInstallationDialogView.axaml.cs | 13 -- .../EntryInstallationDialogViewModel.cs | 8 -- .../{ => List}/EntryListInputView.axaml | 5 +- .../{ => List}/EntryListInputView.axaml.cs | 4 +- .../{ => List}/EntryListInputViewModel.cs | 2 +- .../{ => List}/EntryListItemView.axaml | 5 +- .../{ => List}/EntryListItemView.axaml.cs | 2 +- .../{ => List}/EntryListItemViewModel.cs | 7 +- .../Entries/{ => List}/EntryListViewModel.cs | 8 +- .../Entries/Tabs/LayoutListViewModel.cs | 5 +- .../Entries/Tabs/ProfileListViewModel.cs | 5 +- .../Workshop/Image/ImageSubmissionView.axaml | 43 +++--- .../Image/ImageSubmissionViewModel.cs | 26 ++-- .../Workshop/Layout/LayoutDetailsView.axaml | 119 ++--------------- .../Workshop/Layout/LayoutDetailsViewModel.cs | 52 ++++---- .../Workshop/Layout/LayoutInfoView.axaml | 3 +- .../Workshop/Layout/LayoutInfoViewModel.cs | 1 + .../Library/SubmissionDetailViewModel.cs | 1 + .../Workshop/Profile/ProfileDetailsView.axaml | 125 +----------------- .../Profile/ProfileDetailsViewModel.cs | 76 ++++------- .../Models/SubmissionWizardState.cs | 6 +- .../Steps/ImagesStepViewModel.cs | 30 +++-- .../Layout/LayoutSelectionStepViewModel.cs | 8 +- .../Steps/SpecificationsStepViewModel.cs | 1 + .../Steps/UploadStepViewModel.cs | 4 +- .../Artemis.WebClient.Workshop.csproj | 2 +- .../EntryInstallationHandlerFactory.cs | 1 + .../LayoutEntryInstallationHandler.cs | 45 +++++++ .../UploadHandlers/ImageUploadRequest.cs | 16 +++ .../LayoutEntryUploadHandler.cs | 35 ++++- .../Queries/Fragments.graphql | 30 +++++ .../Queries/GetEntryById.graphql | 19 +-- .../Services/Interfaces/IWorkshopService.cs | 2 +- .../Services/WorkshopService.cs | 8 +- src/Artemis.WebClient.Workshop/schema.graphql | 32 +++++ 56 files changed, 783 insertions(+), 454 deletions(-) create mode 100644 src/Artemis.UI/Screens/Debugger/Tabs/Workshop/WorkshopDebugView.axaml create mode 100644 src/Artemis.UI/Screens/Debugger/Tabs/Workshop/WorkshopDebugView.axaml.cs create mode 100644 src/Artemis.UI/Screens/Debugger/Tabs/Workshop/WorkshopDebugViewModel.cs create mode 100644 src/Artemis.UI/Screens/Workshop/Entries/Details/EntryImageView.axaml create mode 100644 src/Artemis.UI/Screens/Workshop/Entries/Details/EntryImageView.axaml.cs create mode 100644 src/Artemis.UI/Screens/Workshop/Entries/Details/EntryImageViewModel.cs create mode 100644 src/Artemis.UI/Screens/Workshop/Entries/Details/EntryImagesView.axaml create mode 100644 src/Artemis.UI/Screens/Workshop/Entries/Details/EntryImagesView.axaml.cs create mode 100644 src/Artemis.UI/Screens/Workshop/Entries/Details/EntryImagesViewModel.cs create mode 100644 src/Artemis.UI/Screens/Workshop/Entries/Details/EntryInfoView.axaml create mode 100644 src/Artemis.UI/Screens/Workshop/Entries/Details/EntryInfoView.axaml.cs create mode 100644 src/Artemis.UI/Screens/Workshop/Entries/Details/EntryInfoViewModel.cs create mode 100644 src/Artemis.UI/Screens/Workshop/Entries/Details/EntryReleasesView.axaml create mode 100644 src/Artemis.UI/Screens/Workshop/Entries/Details/EntryReleasesView.axaml.cs create mode 100644 src/Artemis.UI/Screens/Workshop/Entries/Details/EntryReleasesViewModel.cs rename src/Artemis.UI/Screens/Workshop/Entries/{ => Details}/EntrySpecificationsView.axaml (97%) rename src/Artemis.UI/Screens/Workshop/Entries/{ => Details}/EntrySpecificationsView.axaml.cs (87%) rename src/Artemis.UI/Screens/Workshop/Entries/{ => Details}/EntrySpecificationsViewModel.cs (87%) delete mode 100644 src/Artemis.UI/Screens/Workshop/Entries/EntryInstallationDialogView.axaml delete mode 100644 src/Artemis.UI/Screens/Workshop/Entries/EntryInstallationDialogView.axaml.cs delete mode 100644 src/Artemis.UI/Screens/Workshop/Entries/EntryInstallationDialogViewModel.cs rename src/Artemis.UI/Screens/Workshop/Entries/{ => List}/EntryListInputView.axaml (90%) rename src/Artemis.UI/Screens/Workshop/Entries/{ => List}/EntryListInputView.axaml.cs (63%) rename src/Artemis.UI/Screens/Workshop/Entries/{ => List}/EntryListInputViewModel.cs (96%) rename src/Artemis.UI/Screens/Workshop/Entries/{ => List}/EntryListItemView.axaml (94%) rename src/Artemis.UI/Screens/Workshop/Entries/{ => List}/EntryListItemView.axaml.cs (78%) rename src/Artemis.UI/Screens/Workshop/Entries/{ => List}/EntryListItemViewModel.cs (84%) rename src/Artemis.UI/Screens/Workshop/Entries/{ => List}/EntryListViewModel.cs (92%) create mode 100644 src/Artemis.WebClient.Workshop/Handlers/InstallationHandlers/Implementations/LayoutEntryInstallationHandler.cs create mode 100644 src/Artemis.WebClient.Workshop/Handlers/UploadHandlers/ImageUploadRequest.cs diff --git a/src/Artemis.UI/Artemis.UI.csproj b/src/Artemis.UI/Artemis.UI.csproj index e4a63012a..71d09529d 100644 --- a/src/Artemis.UI/Artemis.UI.csproj +++ b/src/Artemis.UI/Artemis.UI.csproj @@ -58,4 +58,19 @@ ..\..\..\RGB.NET\bin\net7.0\RGB.NET.Layout.dll + + + + EntryListInputView.axaml + Code + + + EntryListItemView.axaml + Code + + + EntrySpecificationsView.axaml + Code + + \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Debugger/DebugViewModel.cs b/src/Artemis.UI/Screens/Debugger/DebugViewModel.cs index 6ccd0cbb8..8cc677053 100644 --- a/src/Artemis.UI/Screens/Debugger/DebugViewModel.cs +++ b/src/Artemis.UI/Screens/Debugger/DebugViewModel.cs @@ -6,6 +6,7 @@ using Artemis.UI.Screens.Debugger.Logs; using Artemis.UI.Screens.Debugger.Performance; using Artemis.UI.Screens.Debugger.Render; using Artemis.UI.Screens.Debugger.Routing; +using Artemis.UI.Screens.Debugger.Workshop; using Artemis.UI.Services.Interfaces; using Artemis.UI.Shared; using PropertyChanged.SourceGenerator; @@ -17,9 +18,9 @@ public partial class DebugViewModel : ActivatableViewModelBase, IScreen { [Notify] private ViewModelBase _selectedItem; - public DebugViewModel(IDebugService debugService, RenderDebugViewModel render, DataModelDebugViewModel dataModel, PerformanceDebugViewModel performance, RoutingDebugViewModel routing, LogsDebugViewModel logs) + public DebugViewModel(IDebugService debugService, RenderDebugViewModel render, DataModelDebugViewModel dataModel, PerformanceDebugViewModel performance, RoutingDebugViewModel routing, WorkshopDebugViewModel workshop, LogsDebugViewModel logs) { - Items = new ObservableCollection {render, dataModel, performance, routing, logs}; + Items = new ObservableCollection {render, dataModel, performance, routing, workshop, logs}; _selectedItem = render; this.WhenActivated(d => Disposable.Create(debugService.ClearDebugger).DisposeWith(d)); diff --git a/src/Artemis.UI/Screens/Debugger/Tabs/Workshop/WorkshopDebugView.axaml b/src/Artemis.UI/Screens/Debugger/Tabs/Workshop/WorkshopDebugView.axaml new file mode 100644 index 000000000..647d30f06 --- /dev/null +++ b/src/Artemis.UI/Screens/Debugger/Tabs/Workshop/WorkshopDebugView.axaml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Debugger/Tabs/Workshop/WorkshopDebugView.axaml.cs b/src/Artemis.UI/Screens/Debugger/Tabs/Workshop/WorkshopDebugView.axaml.cs new file mode 100644 index 000000000..832b85df8 --- /dev/null +++ b/src/Artemis.UI/Screens/Debugger/Tabs/Workshop/WorkshopDebugView.axaml.cs @@ -0,0 +1,14 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; +using Avalonia.ReactiveUI; + +namespace Artemis.UI.Screens.Debugger.Workshop; + +public partial class WorkshopDebugView : ReactiveUserControl +{ + public WorkshopDebugView() + { + InitializeComponent(); + } +} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Debugger/Tabs/Workshop/WorkshopDebugViewModel.cs b/src/Artemis.UI/Screens/Debugger/Tabs/Workshop/WorkshopDebugViewModel.cs new file mode 100644 index 000000000..26a1e031f --- /dev/null +++ b/src/Artemis.UI/Screens/Debugger/Tabs/Workshop/WorkshopDebugViewModel.cs @@ -0,0 +1,30 @@ +using System.Threading; +using Artemis.UI.Extensions; +using Artemis.UI.Shared; +using Artemis.WebClient.Workshop.Services; +using Newtonsoft.Json; +using PropertyChanged.SourceGenerator; + +namespace Artemis.UI.Screens.Debugger.Workshop; + +public partial class WorkshopDebugViewModel : ActivatableViewModelBase +{ + + [Notify] private string? _token; + [Notify] private bool _emailVerified; + [Notify] private string? _claims; + [Notify] private IWorkshopService.WorkshopStatus? _workshopStatus; + + public WorkshopDebugViewModel(IWorkshopService workshopService, IAuthenticationService authenticationService) + { + DisplayName = "Workshop"; + + this.WhenActivatedAsync(async _ => + { + Token = await authenticationService.GetBearer(); + EmailVerified = authenticationService.GetIsEmailVerified(); + Claims = JsonConvert.SerializeObject(authenticationService.Claims, Formatting.Indented); + WorkshopStatus = await workshopService.GetWorkshopStatus(CancellationToken.None); + }); + } +} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Workshop/Entries/Details/EntryImageView.axaml b/src/Artemis.UI/Screens/Workshop/Entries/Details/EntryImageView.axaml new file mode 100644 index 000000000..4dd4f029f --- /dev/null +++ b/src/Artemis.UI/Screens/Workshop/Entries/Details/EntryImageView.axaml @@ -0,0 +1,24 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Workshop/Entries/Details/EntryImageView.axaml.cs b/src/Artemis.UI/Screens/Workshop/Entries/Details/EntryImageView.axaml.cs new file mode 100644 index 000000000..c7759c110 --- /dev/null +++ b/src/Artemis.UI/Screens/Workshop/Entries/Details/EntryImageView.axaml.cs @@ -0,0 +1,13 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; + +namespace Artemis.UI.Screens.Workshop.Entries.Details; + +public partial class EntryImageView : UserControl +{ + public EntryImageView() + { + InitializeComponent(); + } +} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Workshop/Entries/Details/EntryImageViewModel.cs b/src/Artemis.UI/Screens/Workshop/Entries/Details/EntryImageViewModel.cs new file mode 100644 index 000000000..5bfaa1052 --- /dev/null +++ b/src/Artemis.UI/Screens/Workshop/Entries/Details/EntryImageViewModel.cs @@ -0,0 +1,17 @@ +using Artemis.WebClient.Workshop; + +namespace Artemis.UI.Screens.Workshop.Entries.Details; + +public class EntryImageViewModel +{ + public EntryImageViewModel(IImage image) + { + Image = image; + Url = $"{WorkshopConstants.WORKSHOP_URL}/images/{image.Id}.png"; + ThumbnailUrl = $"{WorkshopConstants.WORKSHOP_URL}/images/{image.Id}-thumb.png"; + } + + public IImage Image { get; } + public string Url { get; } + public string ThumbnailUrl { get; } +} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Workshop/Entries/Details/EntryImagesView.axaml b/src/Artemis.UI/Screens/Workshop/Entries/Details/EntryImagesView.axaml new file mode 100644 index 000000000..3c7168cde --- /dev/null +++ b/src/Artemis.UI/Screens/Workshop/Entries/Details/EntryImagesView.axaml @@ -0,0 +1,28 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Workshop/Entries/Details/EntryImagesView.axaml.cs b/src/Artemis.UI/Screens/Workshop/Entries/Details/EntryImagesView.axaml.cs new file mode 100644 index 000000000..0d0672f94 --- /dev/null +++ b/src/Artemis.UI/Screens/Workshop/Entries/Details/EntryImagesView.axaml.cs @@ -0,0 +1,13 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; + +namespace Artemis.UI.Screens.Workshop.Entries.Details; + +public partial class EntryImagesView : UserControl +{ + public EntryImagesView() + { + InitializeComponent(); + } +} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Workshop/Entries/Details/EntryImagesViewModel.cs b/src/Artemis.UI/Screens/Workshop/Entries/Details/EntryImagesViewModel.cs new file mode 100644 index 000000000..c38cbb744 --- /dev/null +++ b/src/Artemis.UI/Screens/Workshop/Entries/Details/EntryImagesViewModel.cs @@ -0,0 +1,16 @@ +using System.Collections.ObjectModel; +using System.Linq; +using Artemis.UI.Shared; +using Artemis.WebClient.Workshop; + +namespace Artemis.UI.Screens.Workshop.Entries.Details; + +public class EntryImagesViewModel : ViewModelBase +{ + public ObservableCollection Images { get; } + + public EntryImagesViewModel(IEntryDetails entryDetails) + { + Images = new ObservableCollection(entryDetails.Images.Select(i => new EntryImageViewModel(i))); + } +} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Workshop/Entries/Details/EntryInfoView.axaml b/src/Artemis.UI/Screens/Workshop/Entries/Details/EntryInfoView.axaml new file mode 100644 index 000000000..2d0ecbffa --- /dev/null +++ b/src/Artemis.UI/Screens/Workshop/Entries/Details/EntryInfoView.axaml @@ -0,0 +1,83 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + downloads + + + + + Created + + + + + Updated + + + + \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Workshop/Entries/Details/EntryInfoView.axaml.cs b/src/Artemis.UI/Screens/Workshop/Entries/Details/EntryInfoView.axaml.cs new file mode 100644 index 000000000..5c6ec0a0c --- /dev/null +++ b/src/Artemis.UI/Screens/Workshop/Entries/Details/EntryInfoView.axaml.cs @@ -0,0 +1,13 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; + +namespace Artemis.UI.Screens.Workshop.Entries.Details; + +public partial class EntryInfoView : UserControl +{ + public EntryInfoView() + { + InitializeComponent(); + } +} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Workshop/Entries/Details/EntryInfoViewModel.cs b/src/Artemis.UI/Screens/Workshop/Entries/Details/EntryInfoViewModel.cs new file mode 100644 index 000000000..24bb74225 --- /dev/null +++ b/src/Artemis.UI/Screens/Workshop/Entries/Details/EntryInfoViewModel.cs @@ -0,0 +1,28 @@ +using System; +using System.Threading.Tasks; +using Artemis.Core; +using Artemis.UI.Shared; +using Artemis.UI.Shared.Services; +using Artemis.WebClient.Workshop; + +namespace Artemis.UI.Screens.Workshop.Entries.Details; + +public class EntryInfoViewModel : ViewModelBase +{ + private readonly INotificationService _notificationService; + public IGetEntryById_Entry Entry { get; } + public DateTimeOffset? UpdatedAt { get; } + + public EntryInfoViewModel(IGetEntryById_Entry entry, INotificationService notificationService) + { + _notificationService = notificationService; + Entry = entry; + UpdatedAt = Entry.LatestRelease?.CreatedAt ?? Entry.CreatedAt; + } + + public async Task CopyShareLink() + { + await Shared.UI.Clipboard.SetTextAsync($"{WorkshopConstants.WORKSHOP_URL}/entries/{Entry.Id}/{StringUtilities.UrlFriendly(Entry.Name)}"); + _notificationService.CreateNotification().WithTitle("Copied share link to clipboard.").Show(); + } +} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Workshop/Entries/Details/EntryReleasesView.axaml b/src/Artemis.UI/Screens/Workshop/Entries/Details/EntryReleasesView.axaml new file mode 100644 index 000000000..78944e621 --- /dev/null +++ b/src/Artemis.UI/Screens/Workshop/Entries/Details/EntryReleasesView.axaml @@ -0,0 +1,52 @@ + + + + + + + Latest release + + + + \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Workshop/Entries/Details/EntryReleasesView.axaml.cs b/src/Artemis.UI/Screens/Workshop/Entries/Details/EntryReleasesView.axaml.cs new file mode 100644 index 000000000..523edc7e5 --- /dev/null +++ b/src/Artemis.UI/Screens/Workshop/Entries/Details/EntryReleasesView.axaml.cs @@ -0,0 +1,13 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; + +namespace Artemis.UI.Screens.Workshop.Entries.Details; + +public partial class EntryReleasesView : UserControl +{ + public EntryReleasesView() + { + InitializeComponent(); + } +} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Workshop/Entries/Details/EntryReleasesViewModel.cs b/src/Artemis.UI/Screens/Workshop/Entries/Details/EntryReleasesViewModel.cs new file mode 100644 index 000000000..11608288a --- /dev/null +++ b/src/Artemis.UI/Screens/Workshop/Entries/Details/EntryReleasesViewModel.cs @@ -0,0 +1,59 @@ +using System; +using System.Reactive; +using System.Threading; +using System.Threading.Tasks; +using Artemis.UI.Shared; +using Artemis.UI.Shared.Services; +using Artemis.UI.Shared.Services.Builders; +using Artemis.UI.Shared.Utilities; +using Artemis.WebClient.Workshop; +using Artemis.WebClient.Workshop.Handlers.InstallationHandlers; +using Humanizer; +using ReactiveUI; + +namespace Artemis.UI.Screens.Workshop.Entries.Details; + +public class EntryReleasesViewModel : ViewModelBase +{ + private readonly EntryInstallationHandlerFactory _factory; + private readonly IWindowService _windowService; + private readonly INotificationService _notificationService; + + public EntryReleasesViewModel(IGetEntryById_Entry entry, EntryInstallationHandlerFactory factory, IWindowService windowService, INotificationService notificationService) + { + _factory = factory; + _windowService = windowService; + _notificationService = notificationService; + + Entry = entry; + DownloadLatestRelease = ReactiveCommand.CreateFromTask(ExecuteDownloadLatestRelease); + } + + public IGetEntryById_Entry Entry { get; } + public ReactiveCommand DownloadLatestRelease { get; } + + private async Task ExecuteDownloadLatestRelease(CancellationToken cancellationToken) + { + if (Entry.LatestRelease == null) + return; + + bool confirm = await _windowService.ShowConfirmContentDialog( + "Install latest release", + $"Are you sure you want to download and install version {Entry.LatestRelease.Version} of {Entry.Name}?" + ); + if (!confirm) + return; + + IEntryInstallationHandler installationHandler = _factory.CreateHandler(Entry.EntryType); + EntryInstallResult result = await installationHandler.InstallAsync(Entry, Entry.LatestRelease.Id, new Progress(), cancellationToken); + if (result.IsSuccess) + _notificationService.CreateNotification().WithTitle($"{Entry.EntryType.Humanize(LetterCasing.Sentence)} installed").WithSeverity(NotificationSeverity.Success).Show(); + else + { + _notificationService.CreateNotification() + .WithTitle($"Failed to install {Entry.EntryType.Humanize(LetterCasing.LowerCase)}") + .WithMessage(result.Message) + .WithSeverity(NotificationSeverity.Error).Show(); + } + } +} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Workshop/Entries/EntrySpecificationsView.axaml b/src/Artemis.UI/Screens/Workshop/Entries/Details/EntrySpecificationsView.axaml similarity index 97% rename from src/Artemis.UI/Screens/Workshop/Entries/EntrySpecificationsView.axaml rename to src/Artemis.UI/Screens/Workshop/Entries/Details/EntrySpecificationsView.axaml index 94bf49bb5..8b7fb56e6 100644 --- a/src/Artemis.UI/Screens/Workshop/Entries/EntrySpecificationsView.axaml +++ b/src/Artemis.UI/Screens/Workshop/Entries/Details/EntrySpecificationsView.axaml @@ -9,9 +9,10 @@ xmlns:avaloniaEdit="https://github.com/avaloniaui/avaloniaedit" xmlns:mdxaml="https://github.com/whistyun/Markdown.Avalonia.Tight" xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia" + xmlns:details="clr-namespace:Artemis.UI.Screens.Workshop.Entries.Details" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="800" - x:Class="Artemis.UI.Screens.Workshop.Entries.EntrySpecificationsView" - x:DataType="entries:EntrySpecificationsViewModel"> + x:Class="Artemis.UI.Screens.Workshop.Entries.Details.EntrySpecificationsView" + x:DataType="details:EntrySpecificationsViewModel"> diff --git a/src/Artemis.UI/Screens/Workshop/Entries/EntrySpecificationsView.axaml.cs b/src/Artemis.UI/Screens/Workshop/Entries/Details/EntrySpecificationsView.axaml.cs similarity index 87% rename from src/Artemis.UI/Screens/Workshop/Entries/EntrySpecificationsView.axaml.cs rename to src/Artemis.UI/Screens/Workshop/Entries/Details/EntrySpecificationsView.axaml.cs index b3289a3bf..37c85d118 100644 --- a/src/Artemis.UI/Screens/Workshop/Entries/EntrySpecificationsView.axaml.cs +++ b/src/Artemis.UI/Screens/Workshop/Entries/Details/EntrySpecificationsView.axaml.cs @@ -1,5 +1,4 @@ using System.Linq; -using Artemis.UI.Shared.Extensions; using Avalonia; using Avalonia.Controls; using Avalonia.Media; @@ -8,8 +7,9 @@ using Avalonia.ReactiveUI; using AvaloniaEdit.TextMate; using ReactiveUI; using TextMateSharp.Grammars; +using VisualExtensions = Artemis.UI.Shared.Extensions.VisualExtensions; -namespace Artemis.UI.Screens.Workshop.Entries; +namespace Artemis.UI.Screens.Workshop.Entries.Details; public partial class EntrySpecificationsView : ReactiveUserControl { @@ -23,7 +23,7 @@ public partial class EntrySpecificationsView : ReactiveUserControl().FirstOrDefault(); - _previewScrollViewer = DescriptionPreview.GetVisualChildrenOfType().FirstOrDefault(); + _editorScrollViewer = VisualExtensions.GetVisualChildrenOfType(DescriptionEditor).FirstOrDefault(); + _previewScrollViewer = VisualExtensions.GetVisualChildrenOfType(DescriptionPreview).FirstOrDefault(); if (_editorScrollViewer != null) _editorScrollViewer.PropertyChanged += EditorScrollViewerOnPropertyChanged; diff --git a/src/Artemis.UI/Screens/Workshop/Entries/EntrySpecificationsViewModel.cs b/src/Artemis.UI/Screens/Workshop/Entries/Details/EntrySpecificationsViewModel.cs similarity index 87% rename from src/Artemis.UI/Screens/Workshop/Entries/EntrySpecificationsViewModel.cs rename to src/Artemis.UI/Screens/Workshop/Entries/Details/EntrySpecificationsViewModel.cs index af6a2894e..f06262a3f 100644 --- a/src/Artemis.UI/Screens/Workshop/Entries/EntrySpecificationsViewModel.cs +++ b/src/Artemis.UI/Screens/Workshop/Entries/Details/EntrySpecificationsViewModel.cs @@ -22,7 +22,7 @@ using ReactiveUI.Validation.Extensions; using ReactiveUI.Validation.Helpers; using StrawberryShake; -namespace Artemis.UI.Screens.Workshop.Entries; +namespace Artemis.UI.Screens.Workshop.Entries.Details; public partial class EntrySpecificationsViewModel : ValidatableViewModelBase { @@ -52,12 +52,12 @@ public partial class EntrySpecificationsViewModel : ValidatableViewModelBase .Subscribe(); SelectedCategories = selectedCategories; - this.ValidationRule(vm => vm.Name, s => !string.IsNullOrWhiteSpace(s), "Name is required"); - this.ValidationRule(vm => vm.Summary, s => !string.IsNullOrWhiteSpace(s), "Summary is required"); - ValidationHelper descriptionRule = this.ValidationRule(vm => vm.Description, s => !string.IsNullOrWhiteSpace(s), "Description is required"); + this.ValidationRule(vm => vm.Name, s => !string.IsNullOrWhiteSpace(s), "Name is required"); + this.ValidationRule(vm => vm.Summary, s => !string.IsNullOrWhiteSpace(s), "Summary is required"); + ValidationHelper descriptionRule = this.ValidationRule(vm => vm.Description, s => !string.IsNullOrWhiteSpace(s), "Description is required"); // These don't use inputs that support validation messages, do so manually - ValidationHelper iconRule = this.ValidationRule(vm => vm.IconBitmap, s => s != null, "Icon required"); + ValidationHelper iconRule = this.ValidationRule(vm => vm.IconBitmap, s => s != null, "Icon required"); ValidationHelper categoriesRule = this.ValidationRule(vm => vm.Categories, Categories.ToObservableChangeSet().AutoRefresh(c => c.IsSelected).Filter(c => c.IsSelected).IsNotEmpty(), "At least one category must be selected" ); diff --git a/src/Artemis.UI/Screens/Workshop/Entries/EntryInstallationDialogView.axaml b/src/Artemis.UI/Screens/Workshop/Entries/EntryInstallationDialogView.axaml deleted file mode 100644 index af75201c2..000000000 --- a/src/Artemis.UI/Screens/Workshop/Entries/EntryInstallationDialogView.axaml +++ /dev/null @@ -1,8 +0,0 @@ - - Welcome to Avalonia! - diff --git a/src/Artemis.UI/Screens/Workshop/Entries/EntryInstallationDialogView.axaml.cs b/src/Artemis.UI/Screens/Workshop/Entries/EntryInstallationDialogView.axaml.cs deleted file mode 100644 index b228f4b0f..000000000 --- a/src/Artemis.UI/Screens/Workshop/Entries/EntryInstallationDialogView.axaml.cs +++ /dev/null @@ -1,13 +0,0 @@ -using Avalonia; -using Avalonia.Controls; -using Avalonia.Markup.Xaml; - -namespace Artemis.UI.Screens.Workshop.Entries; - -public partial class EntryInstallationDialogView : UserControl -{ - public EntryInstallationDialogView() - { - InitializeComponent(); - } -} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Workshop/Entries/EntryInstallationDialogViewModel.cs b/src/Artemis.UI/Screens/Workshop/Entries/EntryInstallationDialogViewModel.cs deleted file mode 100644 index 7b7cbca67..000000000 --- a/src/Artemis.UI/Screens/Workshop/Entries/EntryInstallationDialogViewModel.cs +++ /dev/null @@ -1,8 +0,0 @@ -using Artemis.UI.Shared; - -namespace Artemis.UI.Screens.Workshop.Entries; - -public class EntryInstallationDialogViewModel : ContentDialogViewModelBase -{ - -} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Workshop/Entries/EntryListInputView.axaml b/src/Artemis.UI/Screens/Workshop/Entries/List/EntryListInputView.axaml similarity index 90% rename from src/Artemis.UI/Screens/Workshop/Entries/EntryListInputView.axaml rename to src/Artemis.UI/Screens/Workshop/Entries/List/EntryListInputView.axaml index 0b01eb0e9..d5c3b34f9 100644 --- a/src/Artemis.UI/Screens/Workshop/Entries/EntryListInputView.axaml +++ b/src/Artemis.UI/Screens/Workshop/Entries/List/EntryListInputView.axaml @@ -4,9 +4,10 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:entries="clr-namespace:Artemis.UI.Screens.Workshop.Entries" xmlns:system="clr-namespace:System;assembly=System.Runtime" + xmlns:list="clr-namespace:Artemis.UI.Screens.Workshop.Entries.List" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" - x:Class="Artemis.UI.Screens.Workshop.Entries.EntryListInputView" - x:DataType="entries:EntryListInputViewModel"> + x:Class="Artemis.UI.Screens.Workshop.Entries.List.EntryListInputView" + x:DataType="list:EntryListInputViewModel"> diff --git a/src/Artemis.UI/Screens/Workshop/Entries/EntryListInputView.axaml.cs b/src/Artemis.UI/Screens/Workshop/Entries/List/EntryListInputView.axaml.cs similarity index 63% rename from src/Artemis.UI/Screens/Workshop/Entries/EntryListInputView.axaml.cs rename to src/Artemis.UI/Screens/Workshop/Entries/List/EntryListInputView.axaml.cs index 650d82eb9..3d2661690 100644 --- a/src/Artemis.UI/Screens/Workshop/Entries/EntryListInputView.axaml.cs +++ b/src/Artemis.UI/Screens/Workshop/Entries/List/EntryListInputView.axaml.cs @@ -1,8 +1,6 @@ -using Avalonia; using Avalonia.Controls; -using Avalonia.Markup.Xaml; -namespace Artemis.UI.Screens.Workshop.Entries; +namespace Artemis.UI.Screens.Workshop.Entries.List; public partial class EntryListInputView : UserControl { diff --git a/src/Artemis.UI/Screens/Workshop/Entries/EntryListInputViewModel.cs b/src/Artemis.UI/Screens/Workshop/Entries/List/EntryListInputViewModel.cs similarity index 96% rename from src/Artemis.UI/Screens/Workshop/Entries/EntryListInputViewModel.cs rename to src/Artemis.UI/Screens/Workshop/Entries/List/EntryListInputViewModel.cs index f9f72c092..f34348a8b 100644 --- a/src/Artemis.UI/Screens/Workshop/Entries/EntryListInputViewModel.cs +++ b/src/Artemis.UI/Screens/Workshop/Entries/List/EntryListInputViewModel.cs @@ -4,7 +4,7 @@ using Artemis.UI.Shared; using PropertyChanged.SourceGenerator; using ReactiveUI; -namespace Artemis.UI.Screens.Workshop.Entries; +namespace Artemis.UI.Screens.Workshop.Entries.List; public partial class EntryListInputViewModel : ViewModelBase { diff --git a/src/Artemis.UI/Screens/Workshop/Entries/EntryListItemView.axaml b/src/Artemis.UI/Screens/Workshop/Entries/List/EntryListItemView.axaml similarity index 94% rename from src/Artemis.UI/Screens/Workshop/Entries/EntryListItemView.axaml rename to src/Artemis.UI/Screens/Workshop/Entries/List/EntryListItemView.axaml index 2831515f6..f041d5655 100644 --- a/src/Artemis.UI/Screens/Workshop/Entries/EntryListItemView.axaml +++ b/src/Artemis.UI/Screens/Workshop/Entries/List/EntryListItemView.axaml @@ -6,9 +6,10 @@ xmlns:entries1="clr-namespace:Artemis.UI.Screens.Workshop.Entries" xmlns:il="clr-namespace:AsyncImageLoader;assembly=AsyncImageLoader.Avalonia" xmlns:converters="clr-namespace:Artemis.UI.Converters" + xmlns:list="clr-namespace:Artemis.UI.Screens.Workshop.Entries.List" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="110" - x:Class="Artemis.UI.Screens.Workshop.Entries.EntryListItemView" - x:DataType="entries1:EntryListItemViewModel"> + x:Class="Artemis.UI.Screens.Workshop.Entries.List.EntryListItemView" + x:DataType="list:EntryListItemViewModel"> diff --git a/src/Artemis.UI/Screens/Workshop/Entries/EntryListItemView.axaml.cs b/src/Artemis.UI/Screens/Workshop/Entries/List/EntryListItemView.axaml.cs similarity index 78% rename from src/Artemis.UI/Screens/Workshop/Entries/EntryListItemView.axaml.cs rename to src/Artemis.UI/Screens/Workshop/Entries/List/EntryListItemView.axaml.cs index 864057476..364af9252 100644 --- a/src/Artemis.UI/Screens/Workshop/Entries/EntryListItemView.axaml.cs +++ b/src/Artemis.UI/Screens/Workshop/Entries/List/EntryListItemView.axaml.cs @@ -1,6 +1,6 @@ using Avalonia.ReactiveUI; -namespace Artemis.UI.Screens.Workshop.Entries; +namespace Artemis.UI.Screens.Workshop.Entries.List; public partial class EntryListItemView : ReactiveUserControl { diff --git a/src/Artemis.UI/Screens/Workshop/Entries/EntryListItemViewModel.cs b/src/Artemis.UI/Screens/Workshop/Entries/List/EntryListItemViewModel.cs similarity index 84% rename from src/Artemis.UI/Screens/Workshop/Entries/EntryListItemViewModel.cs rename to src/Artemis.UI/Screens/Workshop/Entries/List/EntryListItemViewModel.cs index 4160048ff..be57fd898 100644 --- a/src/Artemis.UI/Screens/Workshop/Entries/EntryListItemViewModel.cs +++ b/src/Artemis.UI/Screens/Workshop/Entries/List/EntryListItemViewModel.cs @@ -1,17 +1,12 @@ using System; using System.Reactive; -using System.Reactive.Disposables; -using System.Reactive.Linq; -using System.Threading; using System.Threading.Tasks; using Artemis.UI.Shared; using Artemis.UI.Shared.Routing; using Artemis.WebClient.Workshop; -using Artemis.WebClient.Workshop.Services; -using Avalonia.Media.Imaging; using ReactiveUI; -namespace Artemis.UI.Screens.Workshop.Entries; +namespace Artemis.UI.Screens.Workshop.Entries.List; public class EntryListItemViewModel : ActivatableViewModelBase { diff --git a/src/Artemis.UI/Screens/Workshop/Entries/EntryListViewModel.cs b/src/Artemis.UI/Screens/Workshop/Entries/List/EntryListViewModel.cs similarity index 92% rename from src/Artemis.UI/Screens/Workshop/Entries/EntryListViewModel.cs rename to src/Artemis.UI/Screens/Workshop/Entries/List/EntryListViewModel.cs index 4e11a5568..7bbd75704 100644 --- a/src/Artemis.UI/Screens/Workshop/Entries/EntryListViewModel.cs +++ b/src/Artemis.UI/Screens/Workshop/Entries/List/EntryListViewModel.cs @@ -17,7 +17,7 @@ using PropertyChanged.SourceGenerator; using ReactiveUI; using StrawberryShake; -namespace Artemis.UI.Screens.Workshop.Entries; +namespace Artemis.UI.Screens.Workshop.Entries.List; public abstract partial class EntryListViewModel : RoutableScreen { @@ -42,8 +42,8 @@ public abstract partial class EntryListViewModel : RoutableScreen 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); + _showPagination = this.WhenAnyValue(vm => vm.TotalPages).Select(t => t > 1).ToProperty(this, vm => vm.ShowPagination); + _isLoading = this.WhenAnyValue(vm => vm.Page, vm => vm.LoadedPage, (p, c) => p != c).ToProperty(this, vm => vm.IsLoading); CategoriesViewModel = categoriesViewModel; InputViewModel = entryListInputViewModel; @@ -56,7 +56,7 @@ public abstract partial class EntryListViewModel : RoutableScreen vm.Page).Skip(1).Subscribe(p => Task.Run(() => router.Navigate($"{_route}/{p}"))); + this.WhenAnyValue(vm => vm.Page).Skip(1).Subscribe(p => Task.Run(() => router.Navigate($"{_route}/{p}"))); this.WhenActivated(d => { diff --git a/src/Artemis.UI/Screens/Workshop/Entries/Tabs/LayoutListViewModel.cs b/src/Artemis.UI/Screens/Workshop/Entries/Tabs/LayoutListViewModel.cs index 99308bd23..f4d5237e8 100644 --- a/src/Artemis.UI/Screens/Workshop/Entries/Tabs/LayoutListViewModel.cs +++ b/src/Artemis.UI/Screens/Workshop/Entries/Tabs/LayoutListViewModel.cs @@ -1,17 +1,18 @@ using System; using Artemis.UI.Screens.Workshop.Categories; +using Artemis.UI.Screens.Workshop.Entries.List; using Artemis.UI.Shared.Routing; using Artemis.UI.Shared.Services; using Artemis.WebClient.Workshop; namespace Artemis.UI.Screens.Workshop.Entries.Tabs; -public class LayoutListViewModel : EntryListViewModel +public class LayoutListViewModel : List.EntryListViewModel { public LayoutListViewModel(IWorkshopClient workshopClient, IRouter router, CategoriesViewModel categoriesViewModel, - EntryListInputViewModel entryListInputViewModel, + List.EntryListInputViewModel entryListInputViewModel, INotificationService notificationService, Func getEntryListViewModel) : base("workshop/entries/layout", workshopClient, router, categoriesViewModel, entryListInputViewModel, notificationService, getEntryListViewModel) diff --git a/src/Artemis.UI/Screens/Workshop/Entries/Tabs/ProfileListViewModel.cs b/src/Artemis.UI/Screens/Workshop/Entries/Tabs/ProfileListViewModel.cs index 0049fdb9e..c09a587a3 100644 --- a/src/Artemis.UI/Screens/Workshop/Entries/Tabs/ProfileListViewModel.cs +++ b/src/Artemis.UI/Screens/Workshop/Entries/Tabs/ProfileListViewModel.cs @@ -1,17 +1,18 @@ using System; using Artemis.UI.Screens.Workshop.Categories; +using Artemis.UI.Screens.Workshop.Entries.List; using Artemis.UI.Shared.Routing; using Artemis.UI.Shared.Services; using Artemis.WebClient.Workshop; namespace Artemis.UI.Screens.Workshop.Entries.Tabs; -public class ProfileListViewModel : EntryListViewModel +public class ProfileListViewModel : List.EntryListViewModel { public ProfileListViewModel(IWorkshopClient workshopClient, IRouter router, CategoriesViewModel categoriesViewModel, - EntryListInputViewModel entryListInputViewModel, + List.EntryListInputViewModel entryListInputViewModel, INotificationService notificationService, Func getEntryListViewModel) : base("workshop/entries/profiles", workshopClient, router, categoriesViewModel, entryListInputViewModel, notificationService, getEntryListViewModel) diff --git a/src/Artemis.UI/Screens/Workshop/Image/ImageSubmissionView.axaml b/src/Artemis.UI/Screens/Workshop/Image/ImageSubmissionView.axaml index ca6ec4cdd..c6b6db1d5 100644 --- a/src/Artemis.UI/Screens/Workshop/Image/ImageSubmissionView.axaml +++ b/src/Artemis.UI/Screens/Workshop/Image/ImageSubmissionView.axaml @@ -13,29 +13,34 @@ - - + + Source="{CompiledBinding Bitmap}" /> - - - - - + + + + + - + - - + + + + + + - + \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Workshop/Image/ImageSubmissionViewModel.cs b/src/Artemis.UI/Screens/Workshop/Image/ImageSubmissionViewModel.cs index c431bc2cf..c2928781a 100644 --- a/src/Artemis.UI/Screens/Workshop/Image/ImageSubmissionViewModel.cs +++ b/src/Artemis.UI/Screens/Workshop/Image/ImageSubmissionViewModel.cs @@ -2,39 +2,43 @@ using System.IO; using System.Reactive.Disposables; using System.Windows.Input; using Artemis.UI.Shared; +using Artemis.WebClient.Workshop.Handlers.UploadHandlers; using Avalonia.Media.Imaging; using Avalonia.Threading; using PropertyChanged.SourceGenerator; using ReactiveUI; +using ReactiveUI.Validation.Extensions; namespace Artemis.UI.Screens.Workshop.Image; -public partial class ImageSubmissionViewModel : ActivatableViewModelBase +public partial class ImageSubmissionViewModel : ValidatableViewModelBase { [Notify(Setter.Private)] private Bitmap? _bitmap; - [Notify(Setter.Private)] private string? _fileName; [Notify(Setter.Private)] private string? _imageDimensions; [Notify(Setter.Private)] private long _fileSize; + [Notify] private string? _name; + [Notify] private string? _description; [Notify] private ICommand? _remove; - public ImageSubmissionViewModel(Stream imageStream) + public ImageSubmissionViewModel(ImageUploadRequest image) { this.WhenActivated(d => { Dispatcher.UIThread.Invoke(() => { - imageStream.Seek(0, SeekOrigin.Begin); - Bitmap = new Bitmap(imageStream); - FileSize = imageStream.Length; + image.File.Seek(0, SeekOrigin.Begin); + Bitmap = new Bitmap(image.File); + FileSize = image.File.Length; ImageDimensions = Bitmap.Size.Width + "x" + Bitmap.Size.Height; - - if (imageStream is FileStream fileStream) - FileName = Path.GetFileName(fileStream.Name); - else - FileName = "Unnamed image"; + Name = image.Name; + Description = image.Description; Bitmap.DisposeWith(d); }, DispatcherPriority.Background); }); + + this.ValidationRule(vm => vm.Name, input => !string.IsNullOrWhiteSpace(input), "Name is required"); + this.ValidationRule(vm => vm.Name, input => input?.Length <= 50, "Name can be a maximum of 50 characters"); + this.ValidationRule(vm => vm.Description, input => input?.Length <= 150, "Description can be a maximum of 150 characters"); } } \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Workshop/Layout/LayoutDetailsView.axaml b/src/Artemis.UI/Screens/Workshop/Layout/LayoutDetailsView.axaml index 5e953bd10..3b38036a6 100644 --- a/src/Artemis.UI/Screens/Workshop/Layout/LayoutDetailsView.axaml +++ b/src/Artemis.UI/Screens/Workshop/Layout/LayoutDetailsView.axaml @@ -2,132 +2,31 @@ 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:Layout="clr-namespace:Artemis.UI.Screens.Workshop.Layout" - xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia" - xmlns:il="clr-namespace:AsyncImageLoader;assembly=AsyncImageLoader.Avalonia" - xmlns:converters="clr-namespace:Artemis.UI.Converters" + xmlns:layout="clr-namespace:Artemis.UI.Screens.Workshop.Layout" xmlns:mdxaml="https://github.com/whistyun/Markdown.Avalonia.Tight" - xmlns:mdsvg="https://github.com/whistyun/Markdown.Avalonia.Svg" - xmlns:ui="clr-namespace:Artemis.UI" - xmlns:converters1="clr-namespace:Artemis.UI.Shared.Converters;assembly=Artemis.UI.Shared" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="800" x:Class="Artemis.UI.Screens.Workshop.Layout.LayoutDetailsView" - x:DataType="Layout:LayoutDetailsViewModel"> - - - - - - + x:DataType="layout:LayoutDetailsViewModel"> + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - downloads - - - - - Created - - - - - Updated - - - + - - - Latest release - - - + - - + + + + \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Workshop/Layout/LayoutDetailsViewModel.cs b/src/Artemis.UI/Screens/Workshop/Layout/LayoutDetailsViewModel.cs index f1b3c9da3..de7fb32ea 100644 --- a/src/Artemis.UI/Screens/Workshop/Layout/LayoutDetailsViewModel.cs +++ b/src/Artemis.UI/Screens/Workshop/Layout/LayoutDetailsViewModel.cs @@ -1,17 +1,11 @@ using System; -using System.Reactive; -using System.Reactive.Linq; using System.Threading; using System.Threading.Tasks; -using Artemis.Core; +using Artemis.UI.Screens.Workshop.Entries.Details; using Artemis.UI.Screens.Workshop.Parameters; using Artemis.UI.Shared.Routing; -using Artemis.UI.Shared.Services; -using Artemis.UI.Shared.Services.Builders; -using Artemis.UI.Shared.Utilities; using Artemis.WebClient.Workshop; using PropertyChanged.SourceGenerator; -using ReactiveUI; using StrawberryShake; namespace Artemis.UI.Screens.Workshop.Layout; @@ -19,25 +13,25 @@ namespace Artemis.UI.Screens.Workshop.Layout; public partial class LayoutDetailsViewModel : RoutableScreen { private readonly IWorkshopClient _client; - private readonly INotificationService _notificationService; - private readonly IWindowService _windowService; - private readonly ObservableAsPropertyHelper _updatedAt; - [Notify(Setter.Private)] private IGetEntryById_Entry? _entry; + private readonly Func _getEntryInfoViewModel; + private readonly Func _getEntryReleasesViewModel; + private readonly Func _getEntryImagesViewModel; + [Notify] private IGetEntryById_Entry? _entry; + [Notify] private EntryInfoViewModel? _entryInfoViewModel; + [Notify] private EntryReleasesViewModel? _entryReleasesViewModel; + [Notify] private EntryImagesViewModel? _entryImagesViewModel; - public LayoutDetailsViewModel(IWorkshopClient client, INotificationService notificationService, IWindowService windowService) + public LayoutDetailsViewModel(IWorkshopClient client, + Func getEntryInfoViewModel, + Func getEntryReleasesViewModel, + Func getEntryImagesViewModel) { _client = client; - _notificationService = notificationService; - _windowService = windowService; - _updatedAt = this.WhenAnyValue(vm => vm.Entry).Select(e => e?.LatestRelease?.CreatedAt ?? e?.CreatedAt).ToProperty(this, vm => vm.UpdatedAt); - - DownloadLatestRelease = ReactiveCommand.CreateFromTask(ExecuteDownloadLatestRelease); + _getEntryInfoViewModel = getEntryInfoViewModel; + _getEntryReleasesViewModel = getEntryReleasesViewModel; + _getEntryImagesViewModel = getEntryImagesViewModel; } - public ReactiveCommand DownloadLatestRelease { get; } - - public DateTimeOffset? UpdatedAt => _updatedAt.Value; - public override async Task OnNavigating(WorkshopDetailParameters parameters, NavigationArguments args, CancellationToken cancellationToken) { await GetEntry(parameters.EntryId, cancellationToken); @@ -50,10 +44,16 @@ public partial class LayoutDetailsViewModel : RoutableScreen diff --git a/src/Artemis.UI/Screens/Workshop/Layout/LayoutInfoViewModel.cs b/src/Artemis.UI/Screens/Workshop/Layout/LayoutInfoViewModel.cs index b34228082..166652cad 100644 --- a/src/Artemis.UI/Screens/Workshop/Layout/LayoutInfoViewModel.cs +++ b/src/Artemis.UI/Screens/Workshop/Layout/LayoutInfoViewModel.cs @@ -51,6 +51,7 @@ public partial class LayoutInfoViewModel : ValidatableViewModelBase this.ValidationRule(vm => vm.Vendor, input => !string.IsNullOrWhiteSpace(input), "Device vendor is required"); this.ValidationRule(vm => vm.DeviceProviderIdInput, input => Guid.TryParse(input, out _), "Must be a valid GUID formatted as: 00000000-0000-0000-0000-000000000000"); this.ValidationRule(vm => vm.DeviceProviderIdInput, input => !string.IsNullOrWhiteSpace(input), "Device provider ID is required"); + this.ValidationRule(vm => vm.DeviceProviderIdInput, input => input != "00000000-0000-0000-0000-000000000000", "Device provider ID is required"); } public string? DeviceProviders => _deviceProviders.Value; diff --git a/src/Artemis.UI/Screens/Workshop/Library/SubmissionDetailViewModel.cs b/src/Artemis.UI/Screens/Workshop/Library/SubmissionDetailViewModel.cs index 379d3274f..e2acea559 100644 --- a/src/Artemis.UI/Screens/Workshop/Library/SubmissionDetailViewModel.cs +++ b/src/Artemis.UI/Screens/Workshop/Library/SubmissionDetailViewModel.cs @@ -21,6 +21,7 @@ using Avalonia.Media.Imaging; using PropertyChanged.SourceGenerator; using ReactiveUI; using StrawberryShake; +using EntrySpecificationsViewModel = Artemis.UI.Screens.Workshop.Entries.Details.EntrySpecificationsViewModel; namespace Artemis.UI.Screens.Workshop.Library; diff --git a/src/Artemis.UI/Screens/Workshop/Profile/ProfileDetailsView.axaml b/src/Artemis.UI/Screens/Workshop/Profile/ProfileDetailsView.axaml index 2ca2544a5..af516a4f0 100644 --- a/src/Artemis.UI/Screens/Workshop/Profile/ProfileDetailsView.axaml +++ b/src/Artemis.UI/Screens/Workshop/Profile/ProfileDetailsView.axaml @@ -3,141 +3,30 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:profile="clr-namespace:Artemis.UI.Screens.Workshop.Profile" - xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia" - xmlns:il="clr-namespace:AsyncImageLoader;assembly=AsyncImageLoader.Avalonia" - xmlns:converters="clr-namespace:Artemis.UI.Converters" xmlns:mdxaml="https://github.com/whistyun/Markdown.Avalonia.Tight" - xmlns:mdsvg="https://github.com/whistyun/Markdown.Avalonia.Svg" - xmlns:ui="clr-namespace:Artemis.UI" - xmlns:converters1="clr-namespace:Artemis.UI.Shared.Converters;assembly=Artemis.UI.Shared" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="800" x:Class="Artemis.UI.Screens.Workshop.Profile.ProfileDetailsView" x:DataType="profile:ProfileDetailsViewModel"> - - - - - - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - downloads - - - - - Created - - - - - Updated - - - + - - - Latest release - - - + - - + + + + \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Workshop/Profile/ProfileDetailsViewModel.cs b/src/Artemis.UI/Screens/Workshop/Profile/ProfileDetailsViewModel.cs index c413c9eee..6c65b78c4 100644 --- a/src/Artemis.UI/Screens/Workshop/Profile/ProfileDetailsViewModel.cs +++ b/src/Artemis.UI/Screens/Workshop/Profile/ProfileDetailsViewModel.cs @@ -1,18 +1,11 @@ using System; -using System.Reactive; -using System.Reactive.Linq; using System.Threading; using System.Threading.Tasks; -using Artemis.Core; +using Artemis.UI.Screens.Workshop.Entries.Details; using Artemis.UI.Screens.Workshop.Parameters; using Artemis.UI.Shared.Routing; -using Artemis.UI.Shared.Services; -using Artemis.UI.Shared.Services.Builders; -using Artemis.UI.Shared.Utilities; using Artemis.WebClient.Workshop; -using Artemis.WebClient.Workshop.Handlers.InstallationHandlers; using PropertyChanged.SourceGenerator; -using ReactiveUI; using StrawberryShake; namespace Artemis.UI.Screens.Workshop.Profile; @@ -20,30 +13,25 @@ namespace Artemis.UI.Screens.Workshop.Profile; public partial class ProfileDetailsViewModel : RoutableScreen { private readonly IWorkshopClient _client; - private readonly ProfileEntryInstallationHandler _installationHandler; - private readonly INotificationService _notificationService; - private readonly ObservableAsPropertyHelper _updatedAt; - private readonly IWindowService _windowService; - [Notify(Setter.Private)] private IGetEntryById_Entry? _entry; + private readonly Func _getEntryInfoViewModel; + private readonly Func _getEntryReleasesViewModel; + private readonly Func _getEntryImagesViewModel; + [Notify] private IGetEntryById_Entry? _entry; + [Notify] private EntryInfoViewModel? _entryInfoViewModel; + [Notify] private EntryReleasesViewModel? _entryReleasesViewModel; + [Notify] private EntryImagesViewModel? _entryImagesViewModel; - public ProfileDetailsViewModel(IWorkshopClient client, ProfileEntryInstallationHandler installationHandler, INotificationService notificationService, IWindowService windowService) + public ProfileDetailsViewModel(IWorkshopClient client, + Func getEntryInfoViewModel, + Func getEntryReleasesViewModel, + Func getEntryImagesViewModel) { _client = client; - _installationHandler = installationHandler; - _notificationService = notificationService; - _windowService = windowService; - _updatedAt = this.WhenAnyValue(vm => vm.Entry).Select(e => e?.LatestRelease?.CreatedAt ?? e?.CreatedAt).ToProperty(this, vm => vm.UpdatedAt); - - DownloadLatestRelease = ReactiveCommand.CreateFromTask(ExecuteDownloadLatestRelease); - CopyShareLink = ReactiveCommand.CreateFromTask(ExecuteCopyShareLink); + _getEntryInfoViewModel = getEntryInfoViewModel; + _getEntryReleasesViewModel = getEntryReleasesViewModel; + _getEntryImagesViewModel = getEntryImagesViewModel; } - public ReactiveCommand CopyShareLink { get; set; } - - public ReactiveCommand DownloadLatestRelease { get; } - - public DateTimeOffset? UpdatedAt => _updatedAt.Value; - public override async Task OnNavigating(WorkshopDetailParameters parameters, NavigationArguments args, CancellationToken cancellationToken) { await GetEntry(parameters.EntryId, cancellationToken); @@ -56,30 +44,16 @@ public partial class ProfileDetailsViewModel : RoutableScreen(), cancellationToken); - if (result.IsSuccess) - _notificationService.CreateNotification().WithTitle("Profile installed").WithSeverity(NotificationSeverity.Success).Show(); - else - _notificationService.CreateNotification().WithTitle("Failed to install profile").WithMessage(result.Message).WithSeverity(NotificationSeverity.Error).Show(); - } - - private async Task ExecuteCopyShareLink(CancellationToken arg) - { if (Entry == null) - return; - - await Shared.UI.Clipboard.SetTextAsync($"{WorkshopConstants.WORKSHOP_URL}/entries/{Entry.Id}/{StringUtilities.UrlFriendly(Entry.Name)}"); - _notificationService.CreateNotification().WithTitle("Copied share link to clipboard.").Show(); + { + EntryInfoViewModel = null; + EntryReleasesViewModel = null; + } + else + { + EntryInfoViewModel = _getEntryInfoViewModel(Entry); + EntryReleasesViewModel = _getEntryReleasesViewModel(Entry); + EntryImagesViewModel = _getEntryImagesViewModel(Entry); + } } } \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Models/SubmissionWizardState.cs b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Models/SubmissionWizardState.cs index 9b0c79503..89d2f4fa9 100644 --- a/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Models/SubmissionWizardState.cs +++ b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Models/SubmissionWizardState.cs @@ -33,7 +33,7 @@ public class SubmissionWizardState : IDisposable public List Categories { get; set; } = new(); public List Tags { get; set; } = new(); - public List Images { get; set; } = new(); + public List Images { get; set; } = new(); public IEntrySource? EntrySource { get; set; } @@ -67,7 +67,7 @@ public class SubmissionWizardState : IDisposable public void Dispose() { Icon?.Dispose(); - foreach (Stream stream in Images) - stream.Dispose(); + foreach (ImageUploadRequest image in Images) + image.File.Dispose(); } } \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/ImagesStepViewModel.cs b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/ImagesStepViewModel.cs index 411bf7fb3..2a430347f 100644 --- a/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/ImagesStepViewModel.cs +++ b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/ImagesStepViewModel.cs @@ -7,6 +7,7 @@ using System.Threading; using System.Threading.Tasks; using Artemis.UI.Screens.Workshop.Image; using Artemis.UI.Shared.Services; +using Artemis.WebClient.Workshop.Handlers.UploadHandlers; using DynamicData; using ReactiveUI; @@ -16,37 +17,39 @@ public class ImagesStepViewModel : SubmissionViewModel { private const long MAX_FILE_SIZE = 10 * 1024 * 1024; // 10 MB private readonly IWindowService _windowService; - private readonly SourceList _imageStreams; + private readonly Func _getImageSubmissionViewModel; + private readonly SourceList _stateImages; - public ImagesStepViewModel(IWindowService windowService, Func imageSubmissionViewModel) + public ImagesStepViewModel(IWindowService windowService, Func getImageSubmissionViewModel) { _windowService = windowService; + _getImageSubmissionViewModel = getImageSubmissionViewModel; Continue = ReactiveCommand.Create(() => State.ChangeScreen()); GoBack = ReactiveCommand.Create(() => State.ChangeScreen()); Secondary = ReactiveCommand.CreateFromTask(ExecuteAddImage); SecondaryText = "Add image"; - _imageStreams = new SourceList(); - _imageStreams.Connect() - .Transform(p => CreateImageSubmissionViewModel(imageSubmissionViewModel, p)) + _stateImages = new SourceList(); + _stateImages.Connect() + .Transform(p => CreateImageSubmissionViewModel(p)) .Bind(out ReadOnlyObservableCollection images) .Subscribe(); Images = images; this.WhenActivated((CompositeDisposable d) => { - _imageStreams.Clear(); - _imageStreams.AddRange(State.Images); + _stateImages.Clear(); + _stateImages.AddRange(State.Images); }); } public ReadOnlyObservableCollection Images { get; } - private ImageSubmissionViewModel CreateImageSubmissionViewModel(Func imageSubmissionViewModel, Stream stream) + private ImageSubmissionViewModel CreateImageSubmissionViewModel(ImageUploadRequest image) { - ImageSubmissionViewModel viewModel = imageSubmissionViewModel(stream); - viewModel.Remove = ReactiveCommand.Create(() => _imageStreams.Remove(stream)); + ImageSubmissionViewModel viewModel = _getImageSubmissionViewModel(image); + viewModel.Remove = ReactiveCommand.Create(() => _stateImages.Remove(image)); return viewModel; } @@ -58,7 +61,7 @@ public class ImagesStepViewModel : SubmissionViewModel foreach (string path in result) { - if (_imageStreams.Items.Any(i => i is FileStream fs && fs.Name == path)) + if (_stateImages.Items.Any(i => i.File is FileStream fs && fs.Name == path)) continue; FileStream stream = new(path, FileMode.Open, FileAccess.Read); @@ -69,8 +72,9 @@ public class ImagesStepViewModel : SubmissionViewModel continue; } - _imageStreams.Add(stream); - State.Images.Add(stream); + ImageUploadRequest request = new(stream, Path.GetFileName(path), string.Empty); + _stateImages.Add(request); + State.Images.Add(request); } } } \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/Layout/LayoutSelectionStepViewModel.cs b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/Layout/LayoutSelectionStepViewModel.cs index 69e4709c1..cdbdec029 100644 --- a/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/Layout/LayoutSelectionStepViewModel.cs +++ b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/Layout/LayoutSelectionStepViewModel.cs @@ -166,14 +166,14 @@ public partial class LayoutSelectionStepViewModel : SubmissionViewModel } State.Icon?.Dispose(); - foreach (Stream stateImage in State.Images) - stateImage.Dispose(); + foreach (ImageUploadRequest stateImage in State.Images) + stateImage.File.Dispose(); State.Images.Clear(); // Go through the hassle of resizing the image to 128x128 without losing aspect ratio, padding is added for this State.Icon = ResizeImage(deviceWithoutLeds, 128); - State.Images.Add(deviceWithoutLeds); - State.Images.Add(deviceWithLeds); + State.Images.Add(new ImageUploadRequest(deviceWithoutLeds, "Layout preview (no LEDs)", "A preview of the device without its LEDs")); + State.Images.Add(new ImageUploadRequest(deviceWithLeds, "Layout preview (with LEDs)", "A preview of the device with its LEDs")); } private Stream ResizeImage(Stream image, int size) diff --git a/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/SpecificationsStepViewModel.cs b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/SpecificationsStepViewModel.cs index 0f577e1fd..bf9ea0f6e 100644 --- a/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/SpecificationsStepViewModel.cs +++ b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/SpecificationsStepViewModel.cs @@ -11,6 +11,7 @@ using Artemis.WebClient.Workshop; using DynamicData; using PropertyChanged.SourceGenerator; using ReactiveUI; +using EntrySpecificationsViewModel = Artemis.UI.Screens.Workshop.Entries.Details.EntrySpecificationsViewModel; namespace Artemis.UI.Screens.Workshop.SubmissionWizard.Steps; diff --git a/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/UploadStepViewModel.cs b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/UploadStepViewModel.cs index 89814a7fa..bf6c4f395 100644 --- a/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/UploadStepViewModel.cs +++ b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/UploadStepViewModel.cs @@ -133,12 +133,12 @@ public partial class UploadStepViewModel : SubmissionViewModel return null; } - foreach (Stream image in State.Images.ToList()) + foreach (ImageUploadRequest image in State.Images.ToList()) { // Upload image try { - ImageUploadResult imageUploadResult = await _workshopService.UploadEntryImage(entryId.Value, _progress, image, cancellationToken); + ImageUploadResult imageUploadResult = await _workshopService.UploadEntryImage(entryId.Value, image, _progress, cancellationToken); if (!imageUploadResult.IsSuccess) throw new ArtemisWorkshopException(imageUploadResult.Message); State.Images.Remove(image); diff --git a/src/Artemis.WebClient.Workshop/Artemis.WebClient.Workshop.csproj b/src/Artemis.WebClient.Workshop/Artemis.WebClient.Workshop.csproj index 0f6acb5f9..a4f710127 100644 --- a/src/Artemis.WebClient.Workshop/Artemis.WebClient.Workshop.csproj +++ b/src/Artemis.WebClient.Workshop/Artemis.WebClient.Workshop.csproj @@ -15,7 +15,7 @@ - + diff --git a/src/Artemis.WebClient.Workshop/Handlers/InstallationHandlers/EntryInstallationHandlerFactory.cs b/src/Artemis.WebClient.Workshop/Handlers/InstallationHandlers/EntryInstallationHandlerFactory.cs index 9153d7c5a..c1e35a274 100644 --- a/src/Artemis.WebClient.Workshop/Handlers/InstallationHandlers/EntryInstallationHandlerFactory.cs +++ b/src/Artemis.WebClient.Workshop/Handlers/InstallationHandlers/EntryInstallationHandlerFactory.cs @@ -16,6 +16,7 @@ public class EntryInstallationHandlerFactory return entryType switch { EntryType.Profile => _container.Resolve(), + EntryType.Layout => _container.Resolve(), _ => throw new NotSupportedException($"EntryType '{entryType}' is not supported.") }; } diff --git a/src/Artemis.WebClient.Workshop/Handlers/InstallationHandlers/Implementations/LayoutEntryInstallationHandler.cs b/src/Artemis.WebClient.Workshop/Handlers/InstallationHandlers/Implementations/LayoutEntryInstallationHandler.cs new file mode 100644 index 000000000..575048c4f --- /dev/null +++ b/src/Artemis.WebClient.Workshop/Handlers/InstallationHandlers/Implementations/LayoutEntryInstallationHandler.cs @@ -0,0 +1,45 @@ +using Artemis.UI.Shared.Extensions; +using Artemis.UI.Shared.Utilities; +using Artemis.WebClient.Workshop.Services; + +namespace Artemis.WebClient.Workshop.Handlers.InstallationHandlers; + +public class LayoutEntryInstallationHandler : IEntryInstallationHandler +{ + private readonly IHttpClientFactory _httpClientFactory; + private readonly IWorkshopService _workshopService; + + public LayoutEntryInstallationHandler(IHttpClientFactory httpClientFactory, IWorkshopService workshopService) + { + _httpClientFactory = httpClientFactory; + _workshopService = workshopService; + } + + public async Task InstallAsync(IGetEntryById_Entry entry, long releaseId, Progress progress, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + + using MemoryStream stream = new(); + + // Download the provided release + try + { + HttpClient client = _httpClientFactory.CreateClient(WorkshopConstants.WORKSHOP_CLIENT_NAME); + await client.DownloadDataAsync($"releases/download/{releaseId}", stream, progress, cancellationToken); + } + catch (Exception e) + { + return EntryInstallResult.FromFailure(e.Message); + } + + // return EntryInstallResult.FromSuccess(); + } + + public async Task UninstallAsync(InstalledEntry installedEntry, CancellationToken cancellationToken) + { + throw new NotImplementedException(); + + if (!Guid.TryParse(installedEntry.LocalReference, out Guid profileId)) + return EntryUninstallResult.FromFailure("Local reference does not contain a GUID"); + } +} \ No newline at end of file diff --git a/src/Artemis.WebClient.Workshop/Handlers/UploadHandlers/ImageUploadRequest.cs b/src/Artemis.WebClient.Workshop/Handlers/UploadHandlers/ImageUploadRequest.cs new file mode 100644 index 000000000..0bb2e4774 --- /dev/null +++ b/src/Artemis.WebClient.Workshop/Handlers/UploadHandlers/ImageUploadRequest.cs @@ -0,0 +1,16 @@ +namespace Artemis.WebClient.Workshop.Handlers.UploadHandlers; + +public class ImageUploadRequest +{ + public ImageUploadRequest(Stream file, string name, string? description) + { + File = file; + Name = name.Length > 50 ? name.Substring(0, 50) : name; + if (description != null) + Description = description.Length > 150 ? description.Substring(0, 150) : description; + } + + public Stream File { get; set; } + public string Name { get; set; } + public string? Description { get; set; } +} \ No newline at end of file diff --git a/src/Artemis.WebClient.Workshop/Handlers/UploadHandlers/Implementations/LayoutEntryUploadHandler.cs b/src/Artemis.WebClient.Workshop/Handlers/UploadHandlers/Implementations/LayoutEntryUploadHandler.cs index d61a63472..2e16a085e 100644 --- a/src/Artemis.WebClient.Workshop/Handlers/UploadHandlers/Implementations/LayoutEntryUploadHandler.cs +++ b/src/Artemis.WebClient.Workshop/Handlers/UploadHandlers/Implementations/LayoutEntryUploadHandler.cs @@ -1,13 +1,23 @@ using System.IO.Compression; +using System.Net.Http.Headers; using Artemis.Core; using Artemis.UI.Shared.Utilities; +using Artemis.WebClient.Workshop.Entities; using Artemis.WebClient.Workshop.Exceptions; +using Newtonsoft.Json; using RGB.NET.Layout; namespace Artemis.WebClient.Workshop.Handlers.UploadHandlers; public class LayoutEntryUploadHandler : IEntryUploadHandler { + private readonly IHttpClientFactory _httpClientFactory; + + public LayoutEntryUploadHandler(IHttpClientFactory httpClientFactory) + { + _httpClientFactory = httpClientFactory; + } + /// public async Task CreateReleaseAsync(long entryId, IEntrySource entrySource, Progress progress, CancellationToken cancellationToken) { @@ -46,15 +56,28 @@ public class LayoutEntryUploadHandler : IEntryUploadHandler } archiveStream.Seek(0, SeekOrigin.Begin); - - string desktopPath = Environment.GetFolderPath(Environment.SpecialFolder.Desktop); - string filePath = Path.Combine(desktopPath, "layout-test.zip"); - await using (FileStream fileStream = new(filePath, FileMode.Create, FileAccess.Write)) + await using (FileStream fileStream = new(@"C:\Users\Robert\Desktop\layout-test.zip", FileMode.OpenOrCreate)) { - archiveStream.WriteTo(fileStream); + await archiveStream.CopyToAsync(fileStream, cancellationToken); } + archiveStream.Seek(0, SeekOrigin.Begin); + + // Submit the archive + HttpClient client = _httpClientFactory.CreateClient(WorkshopConstants.WORKSHOP_CLIENT_NAME); - return new EntryUploadResult(); + // Construct the request + MultipartFormDataContent content = new(); + ProgressableStreamContent streamContent = new(archiveStream, progress); + streamContent.Headers.ContentType = new MediaTypeHeaderValue("application/zip"); + content.Add(streamContent, "file", "file.zip"); + + // Submit + HttpResponseMessage response = await client.PostAsync("releases/upload/" + entryId, content, cancellationToken); + if (!response.IsSuccessStatusCode) + return EntryUploadResult.FromFailure($"{response.StatusCode} - {await response.Content.ReadAsStringAsync(cancellationToken)}"); + + Release? release = JsonConvert.DeserializeObject(await response.Content.ReadAsStringAsync(cancellationToken)); + return release != null ? EntryUploadResult.FromSuccess(release) : EntryUploadResult.FromFailure("Failed to deserialize response"); } private static void CopyImage(string layoutPath, string? imagePath, ZipArchive archive) diff --git a/src/Artemis.WebClient.Workshop/Queries/Fragments.graphql b/src/Artemis.WebClient.Workshop/Queries/Fragments.graphql index 38e33eb77..9dc7949ea 100644 --- a/src/Artemis.WebClient.Workshop/Queries/Fragments.graphql +++ b/src/Artemis.WebClient.Workshop/Queries/Fragments.graphql @@ -3,6 +3,12 @@ fragment category on Category { icon } +fragment image on Image { + id + name + description +} + fragment layoutInfo on LayoutInfo { id deviceProvider @@ -20,4 +26,28 @@ fragment submittedEntry on Entry { entryType downloads createdAt +} + +fragment entryDetails on Entry { + id + author + name + summary + entryType + downloads + createdAt + description + categories { + ...category + } + latestRelease { + id + version + downloadSize + md5Hash + createdAt + } + images { + ...image + } } \ No newline at end of file diff --git a/src/Artemis.WebClient.Workshop/Queries/GetEntryById.graphql b/src/Artemis.WebClient.Workshop/Queries/GetEntryById.graphql index 6467133ae..b71c19890 100644 --- a/src/Artemis.WebClient.Workshop/Queries/GetEntryById.graphql +++ b/src/Artemis.WebClient.Workshop/Queries/GetEntryById.graphql @@ -1,22 +1,5 @@ query GetEntryById($id: Long!) { entry(id: $id) { - id - author - name - summary - entryType - downloads - createdAt - description - categories { - ...category - } - latestRelease { - id - version - downloadSize - md5Hash - createdAt - } + ...entryDetails } } \ No newline at end of file diff --git a/src/Artemis.WebClient.Workshop/Services/Interfaces/IWorkshopService.cs b/src/Artemis.WebClient.Workshop/Services/Interfaces/IWorkshopService.cs index 57553b4ac..67dd4d310 100644 --- a/src/Artemis.WebClient.Workshop/Services/Interfaces/IWorkshopService.cs +++ b/src/Artemis.WebClient.Workshop/Services/Interfaces/IWorkshopService.cs @@ -7,7 +7,7 @@ public interface IWorkshopService { Task GetEntryIcon(long entryId, CancellationToken cancellationToken); Task SetEntryIcon(long entryId, Progress progress, Stream icon, CancellationToken cancellationToken); - Task UploadEntryImage(long entryId, Progress progress, Stream image, CancellationToken cancellationToken); + Task UploadEntryImage(long entryId, ImageUploadRequest request, Progress progress, CancellationToken cancellationToken); Task GetWorkshopStatus(CancellationToken cancellationToken); Task ValidateWorkshopStatus(CancellationToken cancellationToken); Task NavigateToEntry(long entryId, EntryType entryType); diff --git a/src/Artemis.WebClient.Workshop/Services/WorkshopService.cs b/src/Artemis.WebClient.Workshop/Services/WorkshopService.cs index c2b343981..0a5719b71 100644 --- a/src/Artemis.WebClient.Workshop/Services/WorkshopService.cs +++ b/src/Artemis.WebClient.Workshop/Services/WorkshopService.cs @@ -57,19 +57,19 @@ public class WorkshopService : IWorkshopService } /// - public async Task UploadEntryImage(long entryId, Progress progress, Stream image, CancellationToken cancellationToken) + public async Task UploadEntryImage(long entryId, ImageUploadRequest request, Progress progress, CancellationToken cancellationToken) { - image.Seek(0, SeekOrigin.Begin); + request.File.Seek(0, SeekOrigin.Begin); // Submit the archive HttpClient client = _httpClientFactory.CreateClient(WorkshopConstants.WORKSHOP_CLIENT_NAME); // Construct the request MultipartFormDataContent content = new(); - ProgressableStreamContent streamContent = new(image, progress); + ProgressableStreamContent streamContent = new(request.File, progress); streamContent.Headers.ContentType = new MediaTypeHeaderValue("image/png"); content.Add(streamContent, "file", "file.png"); - + // Submit HttpResponseMessage response = await client.PostAsync($"entries/{entryId}/image", content, cancellationToken); if (!response.IsSuccessStatusCode) diff --git a/src/Artemis.WebClient.Workshop/schema.graphql b/src/Artemis.WebClient.Workshop/schema.graphql index c79569c64..8639411bf 100644 --- a/src/Artemis.WebClient.Workshop/schema.graphql +++ b/src/Artemis.WebClient.Workshop/schema.graphql @@ -5,6 +5,8 @@ schema { mutation: Mutation } +directive @tag(name: String!) on SCHEMA | SCALAR | OBJECT | FIELD_DEFINITION | ARGUMENT_DEFINITION | INTERFACE | UNION | ENUM | ENUM_VALUE | INPUT_OBJECT | INPUT_FIELD_DEFINITION + type Category { icon: String! id: Long! @@ -50,8 +52,13 @@ type Entry { } type Image { + description: String + height: Int! id: UUID! mimeType: String! + name: String! + size: Long! + width: Int! } type LayoutInfo { @@ -252,14 +259,39 @@ input EntryTypeOperationFilterInput { input ImageFilterInput { and: [ImageFilterInput!] + description: StringOperationFilterInput + height: IntOperationFilterInput id: UuidOperationFilterInput mimeType: StringOperationFilterInput + name: StringOperationFilterInput or: [ImageFilterInput!] + size: LongOperationFilterInput + width: IntOperationFilterInput } input ImageSortInput { + description: SortEnumType + height: SortEnumType id: SortEnumType mimeType: SortEnumType + name: SortEnumType + size: SortEnumType + width: SortEnumType +} + +input IntOperationFilterInput { + eq: Int + gt: Int + gte: Int + in: [Int] + lt: Int + lte: Int + neq: Int + ngt: Int + ngte: Int + nin: [Int] + nlt: Int + nlte: Int } input LayoutInfoFilterInput {