diff --git a/src/Artemis.UI.Shared/Extensions/ArtemisLayoutExtensions.cs b/src/Artemis.UI.Shared/Extensions/ArtemisLayoutExtensions.cs
index 7e0f3853a..b356e8c24 100644
--- a/src/Artemis.UI.Shared/Extensions/ArtemisLayoutExtensions.cs
+++ b/src/Artemis.UI.Shared/Extensions/ArtemisLayoutExtensions.cs
@@ -19,14 +19,16 @@ public static class ArtemisLayoutExtensions
/// Renders the layout to a bitmap.
///
/// The layout to render
+ /// A value indicating whether or not to draw LEDs on the image.
+ /// The scale at which to draw the layout.
/// The resulting bitmap.
- public static RenderTargetBitmap RenderLayout(this ArtemisLayout layout, bool previewLeds)
+ public static RenderTargetBitmap RenderLayout(this ArtemisLayout layout, bool previewLeds, int scale = 2)
{
string? path = layout.Image?.LocalPath;
// Create a bitmap that'll be used to render the device and LED images just once
// Render 4 times the actual size of the device to make sure things look sharp when zoomed in
- RenderTargetBitmap renderTargetBitmap = new(new PixelSize((int) layout.RgbLayout.Width * 2, (int) layout.RgbLayout.Height * 2));
+ RenderTargetBitmap renderTargetBitmap = new(new PixelSize((int) layout.RgbLayout.Width * scale, (int) layout.RgbLayout.Height * scale));
using DrawingContext context = renderTargetBitmap.CreateDrawingContext();
@@ -45,8 +47,8 @@ public static RenderTargetBitmap RenderLayout(this ArtemisLayout layout, bool pr
if (ledPath == null || !File.Exists(ledPath))
continue;
using Bitmap bitmap = new(ledPath);
- using Bitmap scaledBitmap = bitmap.CreateScaledBitmap(new PixelSize((led.RgbLayout.Width * 2).RoundToInt(), (led.RgbLayout.Height * 2).RoundToInt()));
- context.DrawImage(scaledBitmap, new Rect(led.RgbLayout.X * 2, led.RgbLayout.Y * 2, scaledBitmap.Size.Width, scaledBitmap.Size.Height));
+ using Bitmap scaledBitmap = bitmap.CreateScaledBitmap(new PixelSize((led.RgbLayout.Width * scale).RoundToInt(), (led.RgbLayout.Height * scale).RoundToInt()));
+ context.DrawImage(scaledBitmap, new Rect(led.RgbLayout.X * scale, led.RgbLayout.Y * scale, scaledBitmap.Size.Width, scaledBitmap.Size.Height));
}
if (!previewLeds)
@@ -55,14 +57,14 @@ public static RenderTargetBitmap RenderLayout(this ArtemisLayout layout, bool pr
// Draw LED geometry using a rainbow gradient
ColorGradient colors = ColorGradient.GetUnicornBarf();
colors.ToggleSeamless();
- context.PushTransform(Matrix.CreateScale(2, 2));
+ context.PushTransform(Matrix.CreateScale(scale, scale));
foreach (ArtemisLedLayout led in layout.Leds)
{
Geometry? geometry = CreateLedGeometry(led);
if (geometry == null)
continue;
- Color color = colors.GetColor((led.RgbLayout.X + led.RgbLayout.Width / 2) / layout.RgbLayout.Width).ToColor();
+ Color color = colors.GetColor((led.RgbLayout.X + led.RgbLayout.Width / scale) / layout.RgbLayout.Width).ToColor();
SolidColorBrush fillBrush = new() {Color = color, Opacity = 0.4};
SolidColorBrush penBrush = new() {Color = color};
Pen pen = new(penBrush) {LineJoin = PenLineJoin.Round};
diff --git a/src/Artemis.UI.Shared/Routing/Router/IRouter.cs b/src/Artemis.UI.Shared/Routing/Router/IRouter.cs
index cfbd0eb89..e23be6eae 100644
--- a/src/Artemis.UI.Shared/Routing/Router/IRouter.cs
+++ b/src/Artemis.UI.Shared/Routing/Router/IRouter.cs
@@ -27,6 +27,12 @@ public interface IRouter
/// Optional navigation options used to control navigation behaviour.
/// A task representing the operation
Task Navigate(string path, RouterNavigationOptions? options = null);
+
+ ///
+ /// Asynchronously reloads the current route
+ ///
+ /// A task representing the operation
+ Task Reload();
///
/// Asynchronously navigates back to the previous active route.
diff --git a/src/Artemis.UI.Shared/Routing/Router/Router.cs b/src/Artemis.UI.Shared/Routing/Router/Router.cs
index a4afe117d..2f84eb145 100644
--- a/src/Artemis.UI.Shared/Routing/Router/Router.cs
+++ b/src/Artemis.UI.Shared/Routing/Router/Router.cs
@@ -79,6 +79,19 @@ public async Task Navigate(string path, RouterNavigationOptions? options = null)
await Dispatcher.UIThread.InvokeAsync(() => InternalNavigate(path, options));
}
+ ///
+ public async Task Reload()
+ {
+ string path = _currentRouteSubject.Value ?? "blank";
+
+ // Routing takes place on the UI thread with processing heavy tasks offloaded by the router itself
+ await Dispatcher.UIThread.InvokeAsync(async () =>
+ {
+ await InternalNavigate("blank", new RouterNavigationOptions {AddToHistory = false, RecycleScreens = false, EnableLogging = false});
+ await InternalNavigate(path, new RouterNavigationOptions {AddToHistory = false, RecycleScreens = false});
+ });
+ }
+
private async Task InternalNavigate(string path, RouterNavigationOptions options)
{
if (_root == null)
diff --git a/src/Artemis.UI/Screens/Debugger/Tabs/Routing/RoutingDebugView.axaml b/src/Artemis.UI/Screens/Debugger/Tabs/Routing/RoutingDebugView.axaml
index 68230f577..cdefb0ec2 100644
--- a/src/Artemis.UI/Screens/Debugger/Tabs/Routing/RoutingDebugView.axaml
+++ b/src/Artemis.UI/Screens/Debugger/Tabs/Routing/RoutingDebugView.axaml
@@ -7,16 +7,17 @@
x:Class="Artemis.UI.Screens.Debugger.Routing.RoutingDebugView"
x:DataType="routing:RoutingDebugViewModel">
-
+
-
+
+
- Navigation logs
-
+ Navigation logs
+
vm.Route).Select(r => !string.IsNullOrWhiteSpace(r)));
_formatter = new MessageTemplateTextFormatter(
@@ -48,6 +49,7 @@ public RoutingDebugViewModel(IRouter router)
}
public InlineCollection Lines { get; } = new();
+ public ReactiveCommand Reload { get; }
public ReactiveCommand Navigate { get; }
private void OnLogEventAdded(object? sender, LogEventEventArgs e)
@@ -87,6 +89,18 @@ private void LimitLines()
if (Lines.Count > MAX_ENTRIES)
Lines.RemoveRange(0, Lines.Count - MAX_ENTRIES);
}
+
+ private async Task ExecutReload(CancellationToken arg)
+ {
+ try
+ {
+ await _router.Reload();
+ }
+ catch (Exception)
+ {
+ // ignored
+ }
+ }
private async Task ExecuteNavigate(CancellationToken arg)
{
diff --git a/src/Artemis.UI/Screens/Root/BlankView.axaml.cs b/src/Artemis.UI/Screens/Root/BlankView.axaml.cs
index c4c8b84be..e145f1b7f 100644
--- a/src/Artemis.UI/Screens/Root/BlankView.axaml.cs
+++ b/src/Artemis.UI/Screens/Root/BlankView.axaml.cs
@@ -1,10 +1,11 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
+using Avalonia.ReactiveUI;
namespace Artemis.UI.Screens.Root;
-public partial class BlankView : UserControl
+public partial class BlankView : ReactiveUserControl
{
public BlankView()
{
diff --git a/src/Artemis.UI/Screens/Workshop/Entries/Tabs/LayoutListViewModel.cs b/src/Artemis.UI/Screens/Workshop/Entries/Tabs/LayoutListViewModel.cs
index f4d5237e8..6cab02f27 100644
--- a/src/Artemis.UI/Screens/Workshop/Entries/Tabs/LayoutListViewModel.cs
+++ b/src/Artemis.UI/Screens/Workshop/Entries/Tabs/LayoutListViewModel.cs
@@ -12,10 +12,10 @@ public class LayoutListViewModel : List.EntryListViewModel
public LayoutListViewModel(IWorkshopClient workshopClient,
IRouter router,
CategoriesViewModel categoriesViewModel,
- List.EntryListInputViewModel entryListInputViewModel,
+ EntryListInputViewModel entryListInputViewModel,
INotificationService notificationService,
Func getEntryListViewModel)
- : base("workshop/entries/layout", workshopClient, router, categoriesViewModel, entryListInputViewModel, notificationService, getEntryListViewModel)
+ : base("workshop/entries/layouts", workshopClient, router, categoriesViewModel, entryListInputViewModel, notificationService, getEntryListViewModel)
{
entryListInputViewModel.SearchWatermark = "Search layouts";
}
diff --git a/src/Artemis.UI/Screens/Workshop/Image/ImagePropertiesDialogViewModel.cs b/src/Artemis.UI/Screens/Workshop/Image/ImagePropertiesDialogViewModel.cs
index 476feed1d..9074035b8 100644
--- a/src/Artemis.UI/Screens/Workshop/Image/ImagePropertiesDialogViewModel.cs
+++ b/src/Artemis.UI/Screens/Workshop/Image/ImagePropertiesDialogViewModel.cs
@@ -10,33 +10,27 @@ namespace Artemis.UI.Screens.Workshop.Image;
public partial class ImagePropertiesDialogViewModel : ContentDialogViewModelBase
{
- private readonly ImageUploadRequest _image;
[Notify] private string? _name;
[Notify] private string? _description;
- public ImagePropertiesDialogViewModel(ImageUploadRequest image)
+ public ImagePropertiesDialogViewModel(string name, string description)
{
- _image = image;
- _name = image.Name;
- _description = image.Description;
-
+ _name = string.IsNullOrWhiteSpace(name) ? null : name;
+ _description = string.IsNullOrWhiteSpace(description) ? null : description;
Confirm = ReactiveCommand.Create(ExecuteConfirm, ValidationContext.Valid);
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");
+ this.ValidationRule(vm => vm.Description, input => input == null || input.Length <= 150, "Description can be a maximum of 150 characters");
}
public ReactiveCommand Confirm { get; }
private void ExecuteConfirm()
{
- if (string.IsNullOrWhiteSpace(Name))
+ if (!ValidationContext.IsValid)
return;
- _image.Name = Name;
- _image.Description = string.IsNullOrWhiteSpace(Description) ? null : Description;
-
ContentDialog?.Hide(ContentDialogResult.Primary);
}
}
\ No newline at end of file
diff --git a/src/Artemis.UI/Screens/Workshop/Image/ImageSubmissionView.axaml b/src/Artemis.UI/Screens/Workshop/Image/ImageSubmissionView.axaml
index 04d4b10de..4227ef203 100644
--- a/src/Artemis.UI/Screens/Workshop/Image/ImageSubmissionView.axaml
+++ b/src/Artemis.UI/Screens/Workshop/Image/ImageSubmissionView.axaml
@@ -11,7 +11,7 @@
-
+
{
Dispatcher.UIThread.Invoke(() =>
{
- _image.File.Seek(0, SeekOrigin.Begin);
- Bitmap = new Bitmap(_image.File);
+ imageUploadRequest.File.Seek(0, SeekOrigin.Begin);
+ Bitmap = new Bitmap(imageUploadRequest.File);
ImageDimensions = Bitmap.Size.Width + "x" + Bitmap.Size.Height;
Bitmap.DisposeWith(d);
}, DispatcherPriority.Background);
});
}
+ public ImageSubmissionViewModel(IImage existingImage, IWindowService windowService, IHttpClientFactory httpClientFactory)
+ {
+ _windowService = windowService;
+
+ Id = existingImage.Id;
+ Name = existingImage.Name;
+ Description = existingImage.Description;
+
+ // Download the image
+ this.WhenActivated(d =>
+ {
+ Dispatcher.UIThread.Invoke(async () =>
+ {
+ HttpClient client = httpClientFactory.CreateClient(WorkshopConstants.WORKSHOP_CLIENT_NAME);
+ byte[] bytes = await client.GetByteArrayAsync($"/images/{existingImage.Id}.png");
+ MemoryStream stream = new(bytes);
+
+ Bitmap = new Bitmap(stream);
+ FileSize = stream.Length;
+ ImageDimensions = Bitmap.Size.Width + "x" + Bitmap.Size.Height;
+ Bitmap.DisposeWith(d);
+ }, DispatcherPriority.Background);
+ });
+
+ PropertyChanged += (_, args) => HasChanges = HasChanges || args.PropertyName == nameof(Name) || args.PropertyName == nameof(Description);
+ }
+
+ public ImageUploadRequest? ImageUploadRequest { get; }
+ public Guid? Id { get; }
+
public async Task Edit()
{
ContentDialogResult result = await _windowService.CreateContentDialog()
.WithTitle("Edit image properties")
- .WithViewModel(out ImagePropertiesDialogViewModel vm, _image)
+ .WithViewModel(out ImagePropertiesDialogViewModel vm, Name ?? string.Empty, Description ?? string.Empty)
.HavingPrimaryButton(b => b.WithText("Confirm").WithCommand(vm.Confirm))
.WithCloseButtonText("Cancel")
.WithDefaultButton(ContentDialogButton.Primary)
.ShowAsync();
-
- Name = _image.Name;
- Description = _image.Description;
+
+ Name = vm.Name;
+ Description = vm.Description;
return result;
}
diff --git a/src/Artemis.UI/Screens/Workshop/Library/SubmissionDetailView.axaml b/src/Artemis.UI/Screens/Workshop/Library/SubmissionDetailView.axaml
index 99365cb52..dbb99d606 100644
--- a/src/Artemis.UI/Screens/Workshop/Library/SubmissionDetailView.axaml
+++ b/src/Artemis.UI/Screens/Workshop/Library/SubmissionDetailView.axaml
@@ -12,7 +12,7 @@
-
+
@@ -48,8 +48,35 @@
View workshop page
+
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Artemis.UI/Screens/Workshop/Library/SubmissionDetailViewModel.cs b/src/Artemis.UI/Screens/Workshop/Library/SubmissionDetailViewModel.cs
index a70e014a0..e617170f5 100644
--- a/src/Artemis.UI/Screens/Workshop/Library/SubmissionDetailViewModel.cs
+++ b/src/Artemis.UI/Screens/Workshop/Library/SubmissionDetailViewModel.cs
@@ -1,23 +1,23 @@
using System;
using System.Collections.Generic;
+using System.Collections.ObjectModel;
using System.Collections.Specialized;
-using System.ComponentModel;
using System.IO;
using System.Linq;
using System.Reactive;
using System.Threading;
using System.Threading.Tasks;
-using Artemis.UI.Screens.Workshop.Entries;
+using Artemis.UI.Screens.Workshop.Image;
using Artemis.UI.Screens.Workshop.Parameters;
using Artemis.UI.Screens.Workshop.SubmissionWizard;
using Artemis.UI.Shared.Routing;
using Artemis.UI.Shared.Services;
-using Artemis.UI.Shared.Utilities;
using Artemis.WebClient.Workshop;
using Artemis.WebClient.Workshop.Exceptions;
using Artemis.WebClient.Workshop.Handlers.UploadHandlers;
using Artemis.WebClient.Workshop.Services;
using Avalonia.Media.Imaging;
+using FluentAvalonia.UI.Controls;
using PropertyChanged.SourceGenerator;
using ReactiveUI;
using StrawberryShake;
@@ -28,34 +28,50 @@ namespace Artemis.UI.Screens.Workshop.Library;
public partial class SubmissionDetailViewModel : RoutableScreen
{
private readonly IWorkshopClient _client;
- private readonly Func _getGetSpecificationsVm;
- private readonly IRouter _router;
private readonly IWindowService _windowService;
private readonly IWorkshopService _workshopService;
+ private readonly IRouter _router;
+ private readonly Func _getGetSpecificationsViewModel;
+ private readonly Func _getExistingImageSubmissionViewModel;
+ private readonly Func _getImageSubmissionViewModel;
+ private readonly List _removedImages = new();
+
[Notify] private IGetSubmittedEntryById_Entry? _entry;
[Notify] private EntrySpecificationsViewModel? _entrySpecificationsViewModel;
[Notify(Setter.Private)] private bool _hasChanges;
- public SubmissionDetailViewModel(IWorkshopClient client, IWindowService windowService, IWorkshopService workshopService, IRouter router, Func getSpecificationsVm) {
+ public SubmissionDetailViewModel(IWorkshopClient client,
+ IWindowService windowService,
+ IWorkshopService workshopService,
+ IRouter router,
+ Func getSpecificationsViewModel,
+ Func getExistingImageSubmissionViewModel,
+ Func getImageSubmissionViewModel)
+ {
_client = client;
_windowService = windowService;
_workshopService = workshopService;
_router = router;
- _getGetSpecificationsVm = getSpecificationsVm;
+ _getGetSpecificationsViewModel = getSpecificationsViewModel;
+ _getExistingImageSubmissionViewModel = getExistingImageSubmissionViewModel;
+ _getImageSubmissionViewModel = getImageSubmissionViewModel;
CreateRelease = ReactiveCommand.CreateFromTask(ExecuteCreateRelease);
DeleteSubmission = ReactiveCommand.CreateFromTask(ExecuteDeleteSubmission);
ViewWorkshopPage = ReactiveCommand.CreateFromTask(ExecuteViewWorkshopPage);
+ AddImage = ReactiveCommand.CreateFromTask(ExecuteAddImage);
DiscardChanges = ReactiveCommand.CreateFromTask(ExecuteDiscardChanges, this.WhenAnyValue(vm => vm.HasChanges));
SaveChanges = ReactiveCommand.CreateFromTask(ExecuteSaveChanges, this.WhenAnyValue(vm => vm.HasChanges));
}
+ public ObservableCollection Images { get; } = new();
public ReactiveCommand CreateRelease { get; }
public ReactiveCommand DeleteSubmission { get; }
public ReactiveCommand ViewWorkshopPage { get; }
+ public ReactiveCommand AddImage { get; }
public ReactiveCommand SaveChanges { get; }
public ReactiveCommand DiscardChanges { get; }
-
+
public override async Task OnNavigating(WorkshopDetailParameters parameters, NavigationArguments args, CancellationToken cancellationToken)
{
IOperationResult result = await _client.GetSubmittedEntryById.ExecuteAsync(parameters.EntryId, cancellationToken);
@@ -63,7 +79,8 @@ public override async Task OnNavigating(WorkshopDetailParameters parameters, Nav
return;
Entry = result.Data?.Entry;
- await ApplyFromEntry(cancellationToken);
+ await ApplyDetailsFromEntry(cancellationToken);
+ ApplyImagesFromEntry();
}
public override async Task OnClosing(NavigationArguments args)
@@ -76,34 +93,69 @@ public override async Task OnClosing(NavigationArguments args)
args.Cancel();
}
- private async Task ApplyFromEntry(CancellationToken cancellationToken)
+ private async Task ApplyDetailsFromEntry(CancellationToken cancellationToken)
{
- if (Entry == null)
- return;
-
+ // Clean up event handlers
if (EntrySpecificationsViewModel != null)
{
- EntrySpecificationsViewModel.PropertyChanged -= EntrySpecificationsViewModelOnPropertyChanged;
- ((INotifyCollectionChanged) EntrySpecificationsViewModel.SelectedCategories).CollectionChanged -= SelectedCategoriesOnCollectionChanged;
- EntrySpecificationsViewModel.Tags.CollectionChanged -= TagsOnCollectionChanged;
+ EntrySpecificationsViewModel.PropertyChanged -= InputChanged;
+ ((INotifyCollectionChanged) EntrySpecificationsViewModel.SelectedCategories).CollectionChanged -= InputChanged;
+ EntrySpecificationsViewModel.Tags.CollectionChanged -= InputChanged;
}
- EntrySpecificationsViewModel viewModel = _getGetSpecificationsVm();
+ if (Entry == null)
+ {
+ EntrySpecificationsViewModel = null;
+ return;
+ }
- viewModel.IconBitmap = await GetEntryIcon(cancellationToken);
- viewModel.Name = Entry.Name;
- viewModel.Summary = Entry.Summary;
- viewModel.Description = Entry.Description;
- viewModel.PreselectedCategories = Entry.Categories.Select(c => c.Id).ToList();
+ EntrySpecificationsViewModel specificationsViewModel = _getGetSpecificationsViewModel();
+ specificationsViewModel.IconBitmap = await GetEntryIcon(cancellationToken);
+ specificationsViewModel.Name = Entry.Name;
+ specificationsViewModel.Summary = Entry.Summary;
+ specificationsViewModel.Description = Entry.Description;
+ specificationsViewModel.PreselectedCategories = Entry.Categories.Select(c => c.Id).ToList();
- viewModel.Tags.Clear();
+ specificationsViewModel.Tags.Clear();
foreach (string tag in Entry.Tags.Select(c => c.Name))
- viewModel.Tags.Add(tag);
+ specificationsViewModel.Tags.Add(tag);
+
+ EntrySpecificationsViewModel = specificationsViewModel;
+ EntrySpecificationsViewModel.PropertyChanged += InputChanged;
+ ((INotifyCollectionChanged) EntrySpecificationsViewModel.SelectedCategories).CollectionChanged += InputChanged;
+ EntrySpecificationsViewModel.Tags.CollectionChanged += InputChanged;
+
+ ApplyImagesFromEntry();
+ }
+
+ private void ApplyImagesFromEntry()
+ {
+ foreach (ImageSubmissionViewModel imageSubmissionViewModel in Images)
+ imageSubmissionViewModel.PropertyChanged -= InputChanged;
+
+ Images.Clear();
+ _removedImages.Clear();
+
+ if (Entry == null)
+ return;
+
+ foreach (IImage image in Entry.Images)
+ AddImageViewModel(_getExistingImageSubmissionViewModel(image));
+ }
+
+ private void AddImageViewModel(ImageSubmissionViewModel viewModel)
+ {
+ viewModel.PropertyChanged += InputChanged;
+ viewModel.Remove = ReactiveCommand.Create(() =>
+ {
+ // _removedImages is a list of images that are to be deleted, images without an ID never existed in the first place so only add those with an ID
+ if (viewModel.Id != null)
+ _removedImages.Add(viewModel);
- EntrySpecificationsViewModel = viewModel;
- EntrySpecificationsViewModel.PropertyChanged += EntrySpecificationsViewModelOnPropertyChanged;
- ((INotifyCollectionChanged) EntrySpecificationsViewModel.SelectedCategories).CollectionChanged += SelectedCategoriesOnCollectionChanged;
- EntrySpecificationsViewModel.Tags.CollectionChanged += TagsOnCollectionChanged;
+ Images.Remove(viewModel);
+ UpdateHasChanges();
+ });
+ Images.Add(viewModel);
}
private async Task GetEntryIcon(CancellationToken cancellationToken)
@@ -128,19 +180,22 @@ private void UpdateHasChanges()
EntrySpecificationsViewModel.Summary != Entry.Summary ||
EntrySpecificationsViewModel.IconChanged ||
!tags.SequenceEqual(Entry.Tags.Select(t => t.Name).OrderBy(t => t)) ||
- !categories.SequenceEqual(Entry.Categories.Select(c => c.Id).OrderBy(c => c));
+ !categories.SequenceEqual(Entry.Categories.Select(c => c.Id).OrderBy(c => c)) ||
+ Images.Any(i => i.HasChanges) ||
+ _removedImages.Any();
}
private async Task ExecuteDiscardChanges()
{
- await ApplyFromEntry(CancellationToken.None);
+ await ApplyDetailsFromEntry(CancellationToken.None);
+ ApplyImagesFromEntry();
}
private async Task ExecuteSaveChanges(CancellationToken cancellationToken)
{
if (Entry == null || EntrySpecificationsViewModel == null || !EntrySpecificationsViewModel.ValidationContext.GetIsValid())
return;
-
+
UpdateEntryInput input = new()
{
Id = Entry.Id,
@@ -163,7 +218,30 @@ private async Task ExecuteSaveChanges(CancellationToken cancellationToken)
throw new ArtemisWorkshopException("Failed to upload image. " + imageResult.Message);
}
+ foreach (ImageSubmissionViewModel imageViewModel in Images)
+ {
+ // Upload new images
+ if (imageViewModel.ImageUploadRequest != null)
+ {
+ await _workshopService.UploadEntryImage(Entry.Id, imageViewModel.ImageUploadRequest, cancellationToken);
+ }
+ // Update existing images
+ else if (imageViewModel.HasChanges && imageViewModel.Id != null)
+ {
+ if (imageViewModel.Name != null)
+ await _client.UpdateEntryImage.ExecuteAsync(imageViewModel.Id.Value, imageViewModel.Name, imageViewModel.Description, cancellationToken);
+ }
+ }
+
+ // Delete old images
+ foreach (ImageSubmissionViewModel imageViewModel in _removedImages)
+ {
+ if (imageViewModel.Id != null)
+ await _workshopService.DeleteEntryImage(imageViewModel.Id.Value, cancellationToken);
+ }
+
HasChanges = false;
+ await _router.Reload();
}
private async Task ExecuteCreateRelease(CancellationToken cancellationToken)
@@ -189,23 +267,43 @@ private async Task ExecuteDeleteSubmission(CancellationToken cancellationToken)
await _router.Navigate("workshop/library/submissions");
}
- private async Task ExecuteViewWorkshopPage()
- {
- if (Entry != null)
- await _workshopService.NavigateToEntry(Entry.Id, Entry.EntryType);
- }
-
- private void TagsOnCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
+ private async Task ExecuteAddImage(CancellationToken arg)
{
- UpdateHasChanges();
+ string[]? result = await _windowService.CreateOpenFileDialog().WithAllowMultiple().HavingFilter(f => f.WithBitmaps()).ShowAsync();
+ if (result == null)
+ return;
+
+ foreach (string path in result)
+ {
+ FileStream stream = new(path, FileMode.Open, FileAccess.Read);
+ if (stream.Length > ImageUploadRequest.MAX_FILE_SIZE)
+ {
+ await _windowService.ShowConfirmContentDialog("File too big", $"File {path} exceeds maximum file size of 10 MB", "Skip file", null);
+ await stream.DisposeAsync();
+ continue;
+ }
+
+ ImageUploadRequest request = new(stream, Path.GetFileName(path), string.Empty);
+ ImageSubmissionViewModel viewModel = _getImageSubmissionViewModel(request);
+
+ // Show the dialog to give the image a name and description
+ if (await viewModel.Edit() != ContentDialogResult.Primary)
+ {
+ await stream.DisposeAsync();
+ continue;
+ }
+
+ AddImageViewModel(viewModel);
+ }
}
- private void SelectedCategoriesOnCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
+ private async Task ExecuteViewWorkshopPage()
{
- UpdateHasChanges();
+ if (Entry != null)
+ await _workshopService.NavigateToEntry(Entry.Id, Entry.EntryType);
}
- private void EntrySpecificationsViewModelOnPropertyChanged(object? sender, PropertyChangedEventArgs e)
+ private void InputChanged(object? sender, EventArgs e)
{
UpdateHasChanges();
}
diff --git a/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/ImagesStepView.axaml b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/ImagesStepView.axaml
index d563f760e..399f6cdb5 100644
--- a/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/ImagesStepView.axaml
+++ b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/ImagesStepView.axaml
@@ -23,6 +23,11 @@
+
+
+
+
+
diff --git a/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/ImagesStepViewModel.cs b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/ImagesStepViewModel.cs
index 3efdb1259..4462c8167 100644
--- a/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/ImagesStepViewModel.cs
+++ b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/ImagesStepViewModel.cs
@@ -16,7 +16,6 @@ namespace Artemis.UI.Screens.Workshop.SubmissionWizard.Steps;
public class ImagesStepViewModel : SubmissionViewModel
{
- private const long MAX_FILE_SIZE = 10 * 1024 * 1024; // 10 MB
private readonly IWindowService _windowService;
private readonly Func _getImageSubmissionViewModel;
private readonly SourceList _stateImages;
@@ -66,7 +65,7 @@ private async Task ExecuteAddImage(CancellationToken arg)
continue;
FileStream stream = new(path, FileMode.Open, FileAccess.Read);
- if (stream.Length > MAX_FILE_SIZE)
+ if (stream.Length > ImageUploadRequest.MAX_FILE_SIZE)
{
await _windowService.ShowConfirmContentDialog("File too big", $"File {path} exceeds maximum file size of 10 MB", "Skip file", null);
await stream.DisposeAsync();
diff --git a/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/Layout/LayoutSelectionStepView.axaml b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/Layout/LayoutSelectionStepView.axaml
index 794174b2c..e9837b889 100644
--- a/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/Layout/LayoutSelectionStepView.axaml
+++ b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/Layout/LayoutSelectionStepView.axaml
@@ -7,7 +7,7 @@
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Artemis.UI.Screens.Workshop.SubmissionWizard.Steps.Layout.LayoutSelectionStepView"
x:DataType="layout:LayoutSelectionStepViewModel">
-
+
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 a2396ae0e..d38a9b5b4 100644
--- a/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/Layout/LayoutSelectionStepViewModel.cs
+++ b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/Layout/LayoutSelectionStepViewModel.cs
@@ -145,7 +145,7 @@ await _windowService.ShowConfirmContentDialog(
return true;
}
- private void SetDeviceImages()
+ private async Task SetDeviceImages()
{
if (Layout == null)
return;
@@ -153,15 +153,15 @@ private void SetDeviceImages()
MemoryStream deviceWithoutLeds = new();
MemoryStream deviceWithLeds = new();
- using (RenderTargetBitmap image = Layout.RenderLayout(false))
+ using (RenderTargetBitmap image = Layout.RenderLayout(false, 4))
{
- image.Save(deviceWithoutLeds);
+ await Task.Run(() => image.Save(deviceWithoutLeds));
deviceWithoutLeds.Seek(0, SeekOrigin.Begin);
}
- using (RenderTargetBitmap image = Layout.RenderLayout(true))
+ using (RenderTargetBitmap image = Layout.RenderLayout(true, 4))
{
- image.Save(deviceWithLeds);
+ await Task.Run(() => image.Save(deviceWithLeds));
deviceWithLeds.Seek(0, SeekOrigin.Begin);
}
diff --git a/src/Artemis.WebClient.Workshop/Artemis.WebClient.Workshop.csproj b/src/Artemis.WebClient.Workshop/Artemis.WebClient.Workshop.csproj
index b81e3f37d..6b3873c41 100644
--- a/src/Artemis.WebClient.Workshop/Artemis.WebClient.Workshop.csproj
+++ b/src/Artemis.WebClient.Workshop/Artemis.WebClient.Workshop.csproj
@@ -39,7 +39,10 @@
MSBuild:GenerateGraphQLCode
-
+
+ MSBuild:GenerateGraphQLCode
+
+
MSBuild:GenerateGraphQLCode
diff --git a/src/Artemis.WebClient.Workshop/Handlers/UploadHandlers/ImageUploadRequest.cs b/src/Artemis.WebClient.Workshop/Handlers/UploadHandlers/ImageUploadRequest.cs
index 0bb2e4774..97aa46b58 100644
--- a/src/Artemis.WebClient.Workshop/Handlers/UploadHandlers/ImageUploadRequest.cs
+++ b/src/Artemis.WebClient.Workshop/Handlers/UploadHandlers/ImageUploadRequest.cs
@@ -2,6 +2,8 @@
public class ImageUploadRequest
{
+ public const long MAX_FILE_SIZE = 10 * 1024 * 1024; // 10 MB
+
public ImageUploadRequest(Stream file, string name, string? description)
{
File = file;
diff --git a/src/Artemis.WebClient.Workshop/Queries/UpdateEntry.graphql b/src/Artemis.WebClient.Workshop/Mutations/UpdateEntry.graphql
similarity index 100%
rename from src/Artemis.WebClient.Workshop/Queries/UpdateEntry.graphql
rename to src/Artemis.WebClient.Workshop/Mutations/UpdateEntry.graphql
diff --git a/src/Artemis.WebClient.Workshop/Mutations/UpdateEntryImage.graphql b/src/Artemis.WebClient.Workshop/Mutations/UpdateEntryImage.graphql
new file mode 100644
index 000000000..382871b54
--- /dev/null
+++ b/src/Artemis.WebClient.Workshop/Mutations/UpdateEntryImage.graphql
@@ -0,0 +1,5 @@
+mutation UpdateEntryImage ($id: UUID! $name: String! $description: String) {
+ updateEntryImage(input: {id: $id, name: $name description: $description}) {
+ id
+ }
+}
diff --git a/src/Artemis.WebClient.Workshop/Queries/GetSubmittedEntryById.graphql b/src/Artemis.WebClient.Workshop/Queries/GetSubmittedEntryById.graphql
index 85106a433..1bcd98fd6 100644
--- a/src/Artemis.WebClient.Workshop/Queries/GetSubmittedEntryById.graphql
+++ b/src/Artemis.WebClient.Workshop/Queries/GetSubmittedEntryById.graphql
@@ -11,5 +11,8 @@ query GetSubmittedEntryById($id: Long!) {
layoutInfo {
...layoutInfo
}
+ images {
+ ...image
+ }
}
}
\ 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 3b36c6b2d..976e37805 100644
--- a/src/Artemis.WebClient.Workshop/Services/Interfaces/IWorkshopService.cs
+++ b/src/Artemis.WebClient.Workshop/Services/Interfaces/IWorkshopService.cs
@@ -7,6 +7,7 @@ public interface IWorkshopService
Task GetEntryIcon(long entryId, CancellationToken cancellationToken);
Task SetEntryIcon(long entryId, Stream icon, CancellationToken cancellationToken);
Task UploadEntryImage(long entryId, ImageUploadRequest request, CancellationToken cancellationToken);
+ Task DeleteEntryImage(Guid id, 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 920d29f8e..97ba4299f 100644
--- a/src/Artemis.WebClient.Workshop/Services/WorkshopService.cs
+++ b/src/Artemis.WebClient.Workshop/Services/WorkshopService.cs
@@ -80,6 +80,14 @@ public async Task UploadEntryImage(long entryId, ImageUploadR
return ImageUploadResult.FromSuccess();
}
+ ///
+ public async Task DeleteEntryImage(Guid id, CancellationToken cancellationToken)
+ {
+ HttpClient client = _httpClientFactory.CreateClient(WorkshopConstants.WORKSHOP_CLIENT_NAME);
+ HttpResponseMessage response = await client.DeleteAsync($"images/{id}", cancellationToken);
+ response.EnsureSuccessStatusCode();
+ }
+
///
public async Task GetWorkshopStatus(CancellationToken cancellationToken)
{
diff --git a/src/Artemis.WebClient.Workshop/schema.graphql b/src/Artemis.WebClient.Workshop/schema.graphql
index 8639411bf..959e11204 100644
--- a/src/Artemis.WebClient.Workshop/schema.graphql
+++ b/src/Artemis.WebClient.Workshop/schema.graphql
@@ -53,6 +53,8 @@ type Entry {
type Image {
description: String
+ entry: Entry
+ entryId: Long
height: Int!
id: UUID!
mimeType: String!
@@ -79,6 +81,7 @@ type Mutation {
removeEntry(id: Long!): Entry
removeLayoutInfo(id: Long!): LayoutInfo!
updateEntry(input: UpdateEntryInput!): Entry
+ updateEntryImage(input: UpdateEntryImageInput!): Image
}
type Query {
@@ -260,6 +263,8 @@ input EntryTypeOperationFilterInput {
input ImageFilterInput {
and: [ImageFilterInput!]
description: StringOperationFilterInput
+ entry: EntryFilterInput
+ entryId: LongOperationFilterInput
height: IntOperationFilterInput
id: UuidOperationFilterInput
mimeType: StringOperationFilterInput
@@ -271,6 +276,8 @@ input ImageFilterInput {
input ImageSortInput {
description: SortEnumType
+ entry: EntrySortInput
+ entryId: SortEnumType
height: SortEnumType
id: SortEnumType
mimeType: SortEnumType
@@ -418,6 +425,12 @@ input TagFilterInput {
or: [TagFilterInput!]
}
+input UpdateEntryImageInput {
+ description: String
+ id: UUID!
+ name: String!
+}
+
input UpdateEntryInput {
categories: [Long!]!
description: String!