1
0
mirror of https://github.com/Artemis-RGB/Artemis synced 2025-12-13 05:48:35 +00:00

Router - Added reload

Workshop - Added image management to existing entries
Workshop - Fixed pagination in layouts page
This commit is contained in:
RobertBeekman 2024-01-28 22:43:20 +01:00 committed by Robert
parent 19082481c3
commit 78b96deeab
25 changed files with 328 additions and 100 deletions

View File

@ -19,14 +19,16 @@ public static class ArtemisLayoutExtensions
/// Renders the layout to a bitmap. /// Renders the layout to a bitmap.
/// </summary> /// </summary>
/// <param name="layout">The layout to render</param> /// <param name="layout">The layout to render</param>
/// <param name="previewLeds">A value indicating whether or not to draw LEDs on the image.</param>
/// <param name="scale">The scale at which to draw the layout.</param>
/// <returns>The resulting bitmap.</returns> /// <returns>The resulting bitmap.</returns>
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; string? path = layout.Image?.LocalPath;
// Create a bitmap that'll be used to render the device and LED images just once // 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 // 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(); using DrawingContext context = renderTargetBitmap.CreateDrawingContext();
@ -45,8 +47,8 @@ public static class ArtemisLayoutExtensions
if (ledPath == null || !File.Exists(ledPath)) if (ledPath == null || !File.Exists(ledPath))
continue; continue;
using Bitmap bitmap = new(ledPath); using Bitmap bitmap = new(ledPath);
using Bitmap scaledBitmap = bitmap.CreateScaledBitmap(new PixelSize((led.RgbLayout.Width * 2).RoundToInt(), (led.RgbLayout.Height * 2).RoundToInt())); 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 * 2, led.RgbLayout.Y * 2, scaledBitmap.Size.Width, scaledBitmap.Size.Height)); context.DrawImage(scaledBitmap, new Rect(led.RgbLayout.X * scale, led.RgbLayout.Y * scale, scaledBitmap.Size.Width, scaledBitmap.Size.Height));
} }
if (!previewLeds) if (!previewLeds)
@ -55,14 +57,14 @@ public static class ArtemisLayoutExtensions
// Draw LED geometry using a rainbow gradient // Draw LED geometry using a rainbow gradient
ColorGradient colors = ColorGradient.GetUnicornBarf(); ColorGradient colors = ColorGradient.GetUnicornBarf();
colors.ToggleSeamless(); colors.ToggleSeamless();
context.PushTransform(Matrix.CreateScale(2, 2)); context.PushTransform(Matrix.CreateScale(scale, scale));
foreach (ArtemisLedLayout led in layout.Leds) foreach (ArtemisLedLayout led in layout.Leds)
{ {
Geometry? geometry = CreateLedGeometry(led); Geometry? geometry = CreateLedGeometry(led);
if (geometry == null) if (geometry == null)
continue; 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 fillBrush = new() {Color = color, Opacity = 0.4};
SolidColorBrush penBrush = new() {Color = color}; SolidColorBrush penBrush = new() {Color = color};
Pen pen = new(penBrush) {LineJoin = PenLineJoin.Round}; Pen pen = new(penBrush) {LineJoin = PenLineJoin.Round};

View File

@ -27,6 +27,12 @@ public interface IRouter
/// <param name="options">Optional navigation options used to control navigation behaviour.</param> /// <param name="options">Optional navigation options used to control navigation behaviour.</param>
/// <returns>A task representing the operation</returns> /// <returns>A task representing the operation</returns>
Task Navigate(string path, RouterNavigationOptions? options = null); Task Navigate(string path, RouterNavigationOptions? options = null);
/// <summary>
/// Asynchronously reloads the current route
/// </summary>
/// <returns>A task representing the operation</returns>
Task Reload();
/// <summary> /// <summary>
/// Asynchronously navigates back to the previous active route. /// Asynchronously navigates back to the previous active route.

View File

@ -79,6 +79,19 @@ internal class Router : CorePropertyChanged, IRouter, IDisposable
await Dispatcher.UIThread.InvokeAsync(() => InternalNavigate(path, options)); await Dispatcher.UIThread.InvokeAsync(() => InternalNavigate(path, options));
} }
/// <inheritdoc />
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) private async Task InternalNavigate(string path, RouterNavigationOptions options)
{ {
if (_root == null) if (_root == null)

View File

@ -7,16 +7,17 @@
x:Class="Artemis.UI.Screens.Debugger.Routing.RoutingDebugView" x:Class="Artemis.UI.Screens.Debugger.Routing.RoutingDebugView"
x:DataType="routing:RoutingDebugViewModel"> x:DataType="routing:RoutingDebugViewModel">
<Grid RowDefinitions="Auto,Auto,*" ColumnDefinitions="*,Auto"> <Grid RowDefinitions="Auto,Auto,*" ColumnDefinitions="*,Auto,Auto">
<TextBox Grid.Row="0" Grid.Column="0" Watermark="Enter a route to navigate to" Text="{CompiledBinding Route}"> <TextBox Grid.Row="0" Grid.Column="0" Watermark="Enter a route to navigate to" Text="{CompiledBinding Route}">
<TextBox.KeyBindings> <TextBox.KeyBindings>
<KeyBinding Gesture="Enter" Command="{CompiledBinding Navigate}"></KeyBinding> <KeyBinding Gesture="Enter" Command="{CompiledBinding Navigate}"></KeyBinding>
</TextBox.KeyBindings> </TextBox.KeyBindings>
</TextBox> </TextBox>
<Button Grid.Row="0" Grid.Column="1" Margin="5 0 0 0" Command="{CompiledBinding Navigate}">Navigate</Button> <Button Grid.Row="0" Grid.Column="1" Margin="5 0 0 0" Command="{CompiledBinding Reload}">Reload</Button>
<Button Grid.Row="0" Grid.Column="2" Margin="5 0 0 0" Command="{CompiledBinding Navigate}">Navigate</Button>
<TextBlock Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="2" Margin="0 15">Navigation logs</TextBlock> <TextBlock Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="3" Margin="0 15">Navigation logs</TextBlock>
<ScrollViewer Grid.Row="2" Grid.Column="0" Grid.ColumnSpan="2" Name="LogsScrollViewer" VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Auto"> <ScrollViewer Grid.Row="2" Grid.Column="0" Grid.ColumnSpan="3" Name="LogsScrollViewer" VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Auto">
<SelectableTextBlock <SelectableTextBlock
Inlines="{CompiledBinding Lines}" Inlines="{CompiledBinding Lines}"
FontFamily="{StaticResource RobotoMono}" FontFamily="{StaticResource RobotoMono}"

View File

@ -29,6 +29,7 @@ public partial class RoutingDebugViewModel : ActivatableViewModelBase
{ {
_router = router; _router = router;
DisplayName = "Routing"; DisplayName = "Routing";
Reload = ReactiveCommand.CreateFromTask(ExecutReload);
Navigate = ReactiveCommand.CreateFromTask(ExecuteNavigate, this.WhenAnyValue(vm => vm.Route).Select(r => !string.IsNullOrWhiteSpace(r))); Navigate = ReactiveCommand.CreateFromTask(ExecuteNavigate, this.WhenAnyValue(vm => vm.Route).Select(r => !string.IsNullOrWhiteSpace(r)));
_formatter = new MessageTemplateTextFormatter( _formatter = new MessageTemplateTextFormatter(
@ -48,6 +49,7 @@ public partial class RoutingDebugViewModel : ActivatableViewModelBase
} }
public InlineCollection Lines { get; } = new(); public InlineCollection Lines { get; } = new();
public ReactiveCommand<Unit, Unit> Reload { get; }
public ReactiveCommand<Unit, Unit> Navigate { get; } public ReactiveCommand<Unit, Unit> Navigate { get; }
private void OnLogEventAdded(object? sender, LogEventEventArgs e) private void OnLogEventAdded(object? sender, LogEventEventArgs e)
@ -87,6 +89,18 @@ public partial class RoutingDebugViewModel : ActivatableViewModelBase
if (Lines.Count > MAX_ENTRIES) if (Lines.Count > MAX_ENTRIES)
Lines.RemoveRange(0, 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) private async Task ExecuteNavigate(CancellationToken arg)
{ {

View File

@ -1,10 +1,11 @@
using Avalonia; using Avalonia;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Markup.Xaml; using Avalonia.Markup.Xaml;
using Avalonia.ReactiveUI;
namespace Artemis.UI.Screens.Root; namespace Artemis.UI.Screens.Root;
public partial class BlankView : UserControl public partial class BlankView : ReactiveUserControl<BlankViewModel>
{ {
public BlankView() public BlankView()
{ {

View File

@ -12,10 +12,10 @@ public class LayoutListViewModel : List.EntryListViewModel
public LayoutListViewModel(IWorkshopClient workshopClient, public LayoutListViewModel(IWorkshopClient workshopClient,
IRouter router, IRouter router,
CategoriesViewModel categoriesViewModel, CategoriesViewModel categoriesViewModel,
List.EntryListInputViewModel entryListInputViewModel, EntryListInputViewModel entryListInputViewModel,
INotificationService notificationService, INotificationService notificationService,
Func<IGetEntries_Entries_Items, EntryListItemViewModel> getEntryListViewModel) Func<IGetEntries_Entries_Items, EntryListItemViewModel> 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"; entryListInputViewModel.SearchWatermark = "Search layouts";
} }

View File

@ -10,33 +10,27 @@ namespace Artemis.UI.Screens.Workshop.Image;
public partial class ImagePropertiesDialogViewModel : ContentDialogViewModelBase public partial class ImagePropertiesDialogViewModel : ContentDialogViewModelBase
{ {
private readonly ImageUploadRequest _image;
[Notify] private string? _name; [Notify] private string? _name;
[Notify] private string? _description; [Notify] private string? _description;
public ImagePropertiesDialogViewModel(ImageUploadRequest image) public ImagePropertiesDialogViewModel(string name, string description)
{ {
_image = image; _name = string.IsNullOrWhiteSpace(name) ? null : name;
_name = image.Name; _description = string.IsNullOrWhiteSpace(description) ? null : description;
_description = image.Description;
Confirm = ReactiveCommand.Create(ExecuteConfirm, ValidationContext.Valid); Confirm = ReactiveCommand.Create(ExecuteConfirm, ValidationContext.Valid);
this.ValidationRule(vm => vm.Name, input => !string.IsNullOrWhiteSpace(input), "Name is required"); 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.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<Unit, Unit> Confirm { get; } public ReactiveCommand<Unit, Unit> Confirm { get; }
private void ExecuteConfirm() private void ExecuteConfirm()
{ {
if (string.IsNullOrWhiteSpace(Name)) if (!ValidationContext.IsValid)
return; return;
_image.Name = Name;
_image.Description = string.IsNullOrWhiteSpace(Description) ? null : Description;
ContentDialog?.Hide(ContentDialogResult.Primary); ContentDialog?.Hide(ContentDialogResult.Primary);
} }
} }

View File

@ -11,7 +11,7 @@
<UserControl.Resources> <UserControl.Resources>
<converters:BytesToStringConverter x:Key="BytesToStringConverter" /> <converters:BytesToStringConverter x:Key="BytesToStringConverter" />
</UserControl.Resources> </UserControl.Resources>
<Border Classes="card" Padding="0" Width="300" ClipToBounds="True" Margin="5"> <Border Classes="card" Padding="0" ClipToBounds="True">
<Grid RowDefinitions="230,*"> <Grid RowDefinitions="230,*">
<Rectangle Grid.Row="0" Fill="{DynamicResource CheckerboardBrush}" /> <Rectangle Grid.Row="0" Fill="{DynamicResource CheckerboardBrush}" />
<Image Grid.Row="0" <Image Grid.Row="0"

View File

@ -1,64 +1,97 @@
using System;
using System.IO; using System.IO;
using System.Net.Http;
using System.Reactive.Disposables; using System.Reactive.Disposables;
using System.Threading.Tasks; using System.Threading.Tasks;
using System.Windows.Input; using System.Windows.Input;
using Artemis.UI.Shared; using Artemis.UI.Shared;
using Artemis.UI.Shared.Services; using Artemis.UI.Shared.Services;
using Artemis.WebClient.Workshop;
using Artemis.WebClient.Workshop.Handlers.UploadHandlers; using Artemis.WebClient.Workshop.Handlers.UploadHandlers;
using Avalonia.Media.Imaging; using Avalonia.Media.Imaging;
using Avalonia.Threading; using Avalonia.Threading;
using FluentAvalonia.UI.Controls; using FluentAvalonia.UI.Controls;
using PropertyChanged.SourceGenerator; using PropertyChanged.SourceGenerator;
using ReactiveUI; using ReactiveUI;
using ReactiveUI.Validation.Extensions;
using ContentDialogButton = Artemis.UI.Shared.Services.Builders.ContentDialogButton; using ContentDialogButton = Artemis.UI.Shared.Services.Builders.ContentDialogButton;
namespace Artemis.UI.Screens.Workshop.Image; namespace Artemis.UI.Screens.Workshop.Image;
public partial class ImageSubmissionViewModel : ValidatableViewModelBase public partial class ImageSubmissionViewModel : ValidatableViewModelBase
{ {
private readonly ImageUploadRequest _image;
private readonly IWindowService _windowService; private readonly IWindowService _windowService;
[Notify(Setter.Private)] private Bitmap? _bitmap; [Notify(Setter.Private)] private Bitmap? _bitmap;
[Notify(Setter.Private)] private string? _imageDimensions; [Notify(Setter.Private)] private string? _imageDimensions;
[Notify(Setter.Private)] private long _fileSize; [Notify(Setter.Private)] private long _fileSize;
[Notify] private string? _name; [Notify] private string? _name;
[Notify] private string? _description; [Notify] private string? _description;
[Notify] private bool _hasChanges;
[Notify] private ICommand? _remove; [Notify] private ICommand? _remove;
public ImageSubmissionViewModel(ImageUploadRequest image, IWindowService windowService) public ImageSubmissionViewModel(ImageUploadRequest imageUploadRequest, IWindowService windowService)
{ {
_image = image; ImageUploadRequest = imageUploadRequest;
_windowService = windowService; _windowService = windowService;
FileSize = _image.File.Length; FileSize = imageUploadRequest.File.Length;
Name = _image.Name; Name = imageUploadRequest.Name;
Description = _image.Description; Description = imageUploadRequest.Description;
HasChanges = true;
this.WhenActivated(d => this.WhenActivated(d =>
{ {
Dispatcher.UIThread.Invoke(() => Dispatcher.UIThread.Invoke(() =>
{ {
_image.File.Seek(0, SeekOrigin.Begin); imageUploadRequest.File.Seek(0, SeekOrigin.Begin);
Bitmap = new Bitmap(_image.File); Bitmap = new Bitmap(imageUploadRequest.File);
ImageDimensions = Bitmap.Size.Width + "x" + Bitmap.Size.Height; ImageDimensions = Bitmap.Size.Width + "x" + Bitmap.Size.Height;
Bitmap.DisposeWith(d); Bitmap.DisposeWith(d);
}, DispatcherPriority.Background); }, 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<ContentDialogResult> Edit() public async Task<ContentDialogResult> Edit()
{ {
ContentDialogResult result = await _windowService.CreateContentDialog() ContentDialogResult result = await _windowService.CreateContentDialog()
.WithTitle("Edit image properties") .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)) .HavingPrimaryButton(b => b.WithText("Confirm").WithCommand(vm.Confirm))
.WithCloseButtonText("Cancel") .WithCloseButtonText("Cancel")
.WithDefaultButton(ContentDialogButton.Primary) .WithDefaultButton(ContentDialogButton.Primary)
.ShowAsync(); .ShowAsync();
Name = _image.Name; Name = vm.Name;
Description = _image.Description; Description = vm.Description;
return result; return result;
} }

View File

@ -12,7 +12,7 @@
<UserControl.Resources> <UserControl.Resources>
<converters:DateTimeConverter x:Key="DateTimeConverter" /> <converters:DateTimeConverter x:Key="DateTimeConverter" />
</UserControl.Resources> </UserControl.Resources>
<Grid ColumnDefinitions="300,*" RowDefinitions="*, Auto"> <Grid ColumnDefinitions="300,*,300" RowDefinitions="*, Auto">
<StackPanel Grid.Column="0" Grid.RowSpan="2" Spacing="10"> <StackPanel Grid.Column="0" Grid.RowSpan="2" Spacing="10">
<Border Classes="card" VerticalAlignment="Top" Margin="0 0 10 0"> <Border Classes="card" VerticalAlignment="Top" Margin="0 0 10 0">
<StackPanel> <StackPanel>
@ -48,8 +48,35 @@
View workshop page View workshop page
</controls:HyperlinkButton> </controls:HyperlinkButton>
</StackPanel> </StackPanel>
<ContentControl Grid.Column="1" Grid.Row="0" Content="{CompiledBinding EntrySpecificationsViewModel}"></ContentControl> <ContentControl Grid.Column="1" Grid.Row="0" Content="{CompiledBinding EntrySpecificationsViewModel}"></ContentControl>
<StackPanel Grid.Column="1" Grid.Row="1" HorizontalAlignment="Right" Spacing="5" Orientation="Horizontal" Margin="0 10 0 0">
<Border Grid.Column="2" Grid.Row="0" Classes="card" Margin="10 0 0 0">
<Grid RowDefinitions="*,Auto">
<ScrollViewer Grid.Row="0" Classes="with-padding" HorizontalScrollBarVisibility="Disabled" VerticalScrollBarVisibility="Auto">
<ItemsControl ItemsSource="{CompiledBinding Images}">
<ItemsControl.Styles>
<Styles>
<Style Selector="ItemsControl > ContentPresenter">
<Setter Property="Margin" Value="0 0 0 10"></Setter>
</Style>
<Style Selector="ItemsControl > ContentPresenter:nth-last-child(1)">
<Setter Property="Margin" Value="0 0 0 0"></Setter>
</Style>
</Styles>
</ItemsControl.Styles>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<VirtualizingStackPanel />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
</ScrollViewer>
<Button Grid.Row="1" HorizontalAlignment="Stretch" Command="{CompiledBinding AddImage}">Add image</Button>
</Grid>
</Border>
<StackPanel Grid.Column="0" Grid.ColumnSpan="3" Grid.Row="1" HorizontalAlignment="Right" Spacing="5" Orientation="Horizontal" Margin="0 10 0 0">
<Button Command="{CompiledBinding DiscardChanges}">Discard changes</Button> <Button Command="{CompiledBinding DiscardChanges}">Discard changes</Button>
<Button Command="{CompiledBinding SaveChanges}">Save</Button> <Button Command="{CompiledBinding SaveChanges}">Save</Button>
</StackPanel> </StackPanel>

View File

@ -1,23 +1,23 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized; using System.Collections.Specialized;
using System.ComponentModel;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using System.Reactive; using System.Reactive;
using System.Threading; using System.Threading;
using System.Threading.Tasks; 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.Parameters;
using Artemis.UI.Screens.Workshop.SubmissionWizard; using Artemis.UI.Screens.Workshop.SubmissionWizard;
using Artemis.UI.Shared.Routing; using Artemis.UI.Shared.Routing;
using Artemis.UI.Shared.Services; using Artemis.UI.Shared.Services;
using Artemis.UI.Shared.Utilities;
using Artemis.WebClient.Workshop; using Artemis.WebClient.Workshop;
using Artemis.WebClient.Workshop.Exceptions; using Artemis.WebClient.Workshop.Exceptions;
using Artemis.WebClient.Workshop.Handlers.UploadHandlers; using Artemis.WebClient.Workshop.Handlers.UploadHandlers;
using Artemis.WebClient.Workshop.Services; using Artemis.WebClient.Workshop.Services;
using Avalonia.Media.Imaging; using Avalonia.Media.Imaging;
using FluentAvalonia.UI.Controls;
using PropertyChanged.SourceGenerator; using PropertyChanged.SourceGenerator;
using ReactiveUI; using ReactiveUI;
using StrawberryShake; using StrawberryShake;
@ -28,34 +28,50 @@ namespace Artemis.UI.Screens.Workshop.Library;
public partial class SubmissionDetailViewModel : RoutableScreen<WorkshopDetailParameters> public partial class SubmissionDetailViewModel : RoutableScreen<WorkshopDetailParameters>
{ {
private readonly IWorkshopClient _client; private readonly IWorkshopClient _client;
private readonly Func<EntrySpecificationsViewModel> _getGetSpecificationsVm;
private readonly IRouter _router;
private readonly IWindowService _windowService; private readonly IWindowService _windowService;
private readonly IWorkshopService _workshopService; private readonly IWorkshopService _workshopService;
private readonly IRouter _router;
private readonly Func<EntrySpecificationsViewModel> _getGetSpecificationsViewModel;
private readonly Func<IImage, ImageSubmissionViewModel> _getExistingImageSubmissionViewModel;
private readonly Func<ImageUploadRequest, ImageSubmissionViewModel> _getImageSubmissionViewModel;
private readonly List<ImageSubmissionViewModel> _removedImages = new();
[Notify] private IGetSubmittedEntryById_Entry? _entry; [Notify] private IGetSubmittedEntryById_Entry? _entry;
[Notify] private EntrySpecificationsViewModel? _entrySpecificationsViewModel; [Notify] private EntrySpecificationsViewModel? _entrySpecificationsViewModel;
[Notify(Setter.Private)] private bool _hasChanges; [Notify(Setter.Private)] private bool _hasChanges;
public SubmissionDetailViewModel(IWorkshopClient client, IWindowService windowService, IWorkshopService workshopService, IRouter router, Func<EntrySpecificationsViewModel> getSpecificationsVm) { public SubmissionDetailViewModel(IWorkshopClient client,
IWindowService windowService,
IWorkshopService workshopService,
IRouter router,
Func<EntrySpecificationsViewModel> getSpecificationsViewModel,
Func<IImage, ImageSubmissionViewModel> getExistingImageSubmissionViewModel,
Func<ImageUploadRequest, ImageSubmissionViewModel> getImageSubmissionViewModel)
{
_client = client; _client = client;
_windowService = windowService; _windowService = windowService;
_workshopService = workshopService; _workshopService = workshopService;
_router = router; _router = router;
_getGetSpecificationsVm = getSpecificationsVm; _getGetSpecificationsViewModel = getSpecificationsViewModel;
_getExistingImageSubmissionViewModel = getExistingImageSubmissionViewModel;
_getImageSubmissionViewModel = getImageSubmissionViewModel;
CreateRelease = ReactiveCommand.CreateFromTask(ExecuteCreateRelease); CreateRelease = ReactiveCommand.CreateFromTask(ExecuteCreateRelease);
DeleteSubmission = ReactiveCommand.CreateFromTask(ExecuteDeleteSubmission); DeleteSubmission = ReactiveCommand.CreateFromTask(ExecuteDeleteSubmission);
ViewWorkshopPage = ReactiveCommand.CreateFromTask(ExecuteViewWorkshopPage); ViewWorkshopPage = ReactiveCommand.CreateFromTask(ExecuteViewWorkshopPage);
AddImage = ReactiveCommand.CreateFromTask(ExecuteAddImage);
DiscardChanges = ReactiveCommand.CreateFromTask(ExecuteDiscardChanges, this.WhenAnyValue(vm => vm.HasChanges)); DiscardChanges = ReactiveCommand.CreateFromTask(ExecuteDiscardChanges, this.WhenAnyValue(vm => vm.HasChanges));
SaveChanges = ReactiveCommand.CreateFromTask(ExecuteSaveChanges, this.WhenAnyValue(vm => vm.HasChanges)); SaveChanges = ReactiveCommand.CreateFromTask(ExecuteSaveChanges, this.WhenAnyValue(vm => vm.HasChanges));
} }
public ObservableCollection<ImageSubmissionViewModel> Images { get; } = new();
public ReactiveCommand<Unit, Unit> CreateRelease { get; } public ReactiveCommand<Unit, Unit> CreateRelease { get; }
public ReactiveCommand<Unit, Unit> DeleteSubmission { get; } public ReactiveCommand<Unit, Unit> DeleteSubmission { get; }
public ReactiveCommand<Unit, Unit> ViewWorkshopPage { get; } public ReactiveCommand<Unit, Unit> ViewWorkshopPage { get; }
public ReactiveCommand<Unit, Unit> AddImage { get; }
public ReactiveCommand<Unit, Unit> SaveChanges { get; } public ReactiveCommand<Unit, Unit> SaveChanges { get; }
public ReactiveCommand<Unit, Unit> DiscardChanges { get; } public ReactiveCommand<Unit, Unit> DiscardChanges { get; }
public override async Task OnNavigating(WorkshopDetailParameters parameters, NavigationArguments args, CancellationToken cancellationToken) public override async Task OnNavigating(WorkshopDetailParameters parameters, NavigationArguments args, CancellationToken cancellationToken)
{ {
IOperationResult<IGetSubmittedEntryByIdResult> result = await _client.GetSubmittedEntryById.ExecuteAsync(parameters.EntryId, cancellationToken); IOperationResult<IGetSubmittedEntryByIdResult> result = await _client.GetSubmittedEntryById.ExecuteAsync(parameters.EntryId, cancellationToken);
@ -63,7 +79,8 @@ public partial class SubmissionDetailViewModel : RoutableScreen<WorkshopDetailPa
return; return;
Entry = result.Data?.Entry; Entry = result.Data?.Entry;
await ApplyFromEntry(cancellationToken); await ApplyDetailsFromEntry(cancellationToken);
ApplyImagesFromEntry();
} }
public override async Task OnClosing(NavigationArguments args) public override async Task OnClosing(NavigationArguments args)
@ -76,34 +93,69 @@ public partial class SubmissionDetailViewModel : RoutableScreen<WorkshopDetailPa
args.Cancel(); args.Cancel();
} }
private async Task ApplyFromEntry(CancellationToken cancellationToken) private async Task ApplyDetailsFromEntry(CancellationToken cancellationToken)
{ {
// Clean up event handlers
if (EntrySpecificationsViewModel != null)
{
EntrySpecificationsViewModel.PropertyChanged -= InputChanged;
((INotifyCollectionChanged) EntrySpecificationsViewModel.SelectedCategories).CollectionChanged -= InputChanged;
EntrySpecificationsViewModel.Tags.CollectionChanged -= InputChanged;
}
if (Entry == null)
{
EntrySpecificationsViewModel = null;
return;
}
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();
specificationsViewModel.Tags.Clear();
foreach (string tag in Entry.Tags.Select(c => c.Name))
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) if (Entry == null)
return; return;
if (EntrySpecificationsViewModel != null) foreach (IImage image in Entry.Images)
AddImageViewModel(_getExistingImageSubmissionViewModel(image));
}
private void AddImageViewModel(ImageSubmissionViewModel viewModel)
{
viewModel.PropertyChanged += InputChanged;
viewModel.Remove = ReactiveCommand.Create(() =>
{ {
EntrySpecificationsViewModel.PropertyChanged -= EntrySpecificationsViewModelOnPropertyChanged; // _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
((INotifyCollectionChanged) EntrySpecificationsViewModel.SelectedCategories).CollectionChanged -= SelectedCategoriesOnCollectionChanged; if (viewModel.Id != null)
EntrySpecificationsViewModel.Tags.CollectionChanged -= TagsOnCollectionChanged; _removedImages.Add(viewModel);
}
EntrySpecificationsViewModel viewModel = _getGetSpecificationsVm(); Images.Remove(viewModel);
UpdateHasChanges();
viewModel.IconBitmap = await GetEntryIcon(cancellationToken); });
viewModel.Name = Entry.Name; Images.Add(viewModel);
viewModel.Summary = Entry.Summary;
viewModel.Description = Entry.Description;
viewModel.PreselectedCategories = Entry.Categories.Select(c => c.Id).ToList();
viewModel.Tags.Clear();
foreach (string tag in Entry.Tags.Select(c => c.Name))
viewModel.Tags.Add(tag);
EntrySpecificationsViewModel = viewModel;
EntrySpecificationsViewModel.PropertyChanged += EntrySpecificationsViewModelOnPropertyChanged;
((INotifyCollectionChanged) EntrySpecificationsViewModel.SelectedCategories).CollectionChanged += SelectedCategoriesOnCollectionChanged;
EntrySpecificationsViewModel.Tags.CollectionChanged += TagsOnCollectionChanged;
} }
private async Task<Bitmap?> GetEntryIcon(CancellationToken cancellationToken) private async Task<Bitmap?> GetEntryIcon(CancellationToken cancellationToken)
@ -128,19 +180,22 @@ public partial class SubmissionDetailViewModel : RoutableScreen<WorkshopDetailPa
EntrySpecificationsViewModel.Summary != Entry.Summary || EntrySpecificationsViewModel.Summary != Entry.Summary ||
EntrySpecificationsViewModel.IconChanged || EntrySpecificationsViewModel.IconChanged ||
!tags.SequenceEqual(Entry.Tags.Select(t => t.Name).OrderBy(t => t)) || !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() private async Task ExecuteDiscardChanges()
{ {
await ApplyFromEntry(CancellationToken.None); await ApplyDetailsFromEntry(CancellationToken.None);
ApplyImagesFromEntry();
} }
private async Task ExecuteSaveChanges(CancellationToken cancellationToken) private async Task ExecuteSaveChanges(CancellationToken cancellationToken)
{ {
if (Entry == null || EntrySpecificationsViewModel == null || !EntrySpecificationsViewModel.ValidationContext.GetIsValid()) if (Entry == null || EntrySpecificationsViewModel == null || !EntrySpecificationsViewModel.ValidationContext.GetIsValid())
return; return;
UpdateEntryInput input = new() UpdateEntryInput input = new()
{ {
Id = Entry.Id, Id = Entry.Id,
@ -163,7 +218,30 @@ public partial class SubmissionDetailViewModel : RoutableScreen<WorkshopDetailPa
throw new ArtemisWorkshopException("Failed to upload image. " + imageResult.Message); 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 && imageViewModel.Description != 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; HasChanges = false;
await _router.Reload();
} }
private async Task ExecuteCreateRelease(CancellationToken cancellationToken) private async Task ExecuteCreateRelease(CancellationToken cancellationToken)
@ -189,23 +267,43 @@ public partial class SubmissionDetailViewModel : RoutableScreen<WorkshopDetailPa
await _router.Navigate("workshop/library/submissions"); await _router.Navigate("workshop/library/submissions");
} }
private async Task ExecuteAddImage(CancellationToken arg)
{
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 async Task ExecuteViewWorkshopPage() private async Task ExecuteViewWorkshopPage()
{ {
if (Entry != null) if (Entry != null)
await _workshopService.NavigateToEntry(Entry.Id, Entry.EntryType); await _workshopService.NavigateToEntry(Entry.Id, Entry.EntryType);
} }
private void TagsOnCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
{
UpdateHasChanges();
}
private void SelectedCategoriesOnCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e) private void InputChanged(object? sender, EventArgs e)
{
UpdateHasChanges();
}
private void EntrySpecificationsViewModelOnPropertyChanged(object? sender, PropertyChangedEventArgs e)
{ {
UpdateHasChanges(); UpdateHasChanges();
} }

View File

@ -23,6 +23,11 @@
<ScrollViewer Grid.Row="1" Margin="0 20 0 0"> <ScrollViewer Grid.Row="1" Margin="0 20 0 0">
<ItemsControl ItemsSource="{CompiledBinding Images}"> <ItemsControl ItemsSource="{CompiledBinding Images}">
<ItemsControl.ItemTemplate>
<DataTemplate>
<ContentControl Content="{CompiledBinding}" Margin="0 0 5 0" Width="300"></ContentControl>
</DataTemplate>
</ItemsControl.ItemTemplate>
<ItemsControl.ItemsPanel> <ItemsControl.ItemsPanel>
<ItemsPanelTemplate> <ItemsPanelTemplate>
<WrapPanel /> <WrapPanel />

View File

@ -16,7 +16,6 @@ namespace Artemis.UI.Screens.Workshop.SubmissionWizard.Steps;
public class ImagesStepViewModel : SubmissionViewModel public class ImagesStepViewModel : SubmissionViewModel
{ {
private const long MAX_FILE_SIZE = 10 * 1024 * 1024; // 10 MB
private readonly IWindowService _windowService; private readonly IWindowService _windowService;
private readonly Func<ImageUploadRequest, ImageSubmissionViewModel> _getImageSubmissionViewModel; private readonly Func<ImageUploadRequest, ImageSubmissionViewModel> _getImageSubmissionViewModel;
private readonly SourceList<ImageUploadRequest> _stateImages; private readonly SourceList<ImageUploadRequest> _stateImages;
@ -66,7 +65,7 @@ public class ImagesStepViewModel : SubmissionViewModel
continue; continue;
FileStream stream = new(path, FileMode.Open, FileAccess.Read); 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 _windowService.ShowConfirmContentDialog("File too big", $"File {path} exceeds maximum file size of 10 MB", "Skip file", null);
await stream.DisposeAsync(); await stream.DisposeAsync();

View File

@ -7,7 +7,7 @@
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Artemis.UI.Screens.Workshop.SubmissionWizard.Steps.Layout.LayoutSelectionStepView" x:Class="Artemis.UI.Screens.Workshop.SubmissionWizard.Steps.Layout.LayoutSelectionStepView"
x:DataType="layout:LayoutSelectionStepViewModel"> x:DataType="layout:LayoutSelectionStepViewModel">
<Grid RowDefinitions="Auto,*"> <Grid RowDefinitions="Auto,*, *">
<StackPanel> <StackPanel>
<StackPanel.Styles> <StackPanel.Styles>
<Styles> <Styles>

View File

@ -145,7 +145,7 @@ public partial class LayoutSelectionStepViewModel : SubmissionViewModel
return true; return true;
} }
private void SetDeviceImages() private async Task SetDeviceImages()
{ {
if (Layout == null) if (Layout == null)
return; return;
@ -153,15 +153,15 @@ public partial class LayoutSelectionStepViewModel : SubmissionViewModel
MemoryStream deviceWithoutLeds = new(); MemoryStream deviceWithoutLeds = new();
MemoryStream deviceWithLeds = 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); 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); deviceWithLeds.Seek(0, SeekOrigin.Begin);
} }

View File

@ -39,7 +39,10 @@
<GraphQL Update="Queries\RemoveEntry.graphql"> <GraphQL Update="Queries\RemoveEntry.graphql">
<Generator>MSBuild:GenerateGraphQLCode</Generator> <Generator>MSBuild:GenerateGraphQLCode</Generator>
</GraphQL> </GraphQL>
<GraphQL Update="Queries\UpdateEntry.graphql"> <GraphQL Update="Mutations\UpdateEntry.graphql">
<Generator>MSBuild:GenerateGraphQLCode</Generator>
</GraphQL>
<GraphQL Update="Mutations\UpdateEntryImage.graphql">
<Generator>MSBuild:GenerateGraphQLCode</Generator> <Generator>MSBuild:GenerateGraphQLCode</Generator>
</GraphQL> </GraphQL>
</ItemGroup> </ItemGroup>

View File

@ -2,6 +2,8 @@
public class ImageUploadRequest public class ImageUploadRequest
{ {
public const long MAX_FILE_SIZE = 10 * 1024 * 1024; // 10 MB
public ImageUploadRequest(Stream file, string name, string? description) public ImageUploadRequest(Stream file, string name, string? description)
{ {
File = file; File = file;

View File

@ -0,0 +1,5 @@
mutation UpdateEntryImage ($id: UUID! $name: String! $description: String!) {
updateEntryImage(input: {id: $id, name: $name description: $description}) {
id
}
}

View File

@ -11,5 +11,8 @@ query GetSubmittedEntryById($id: Long!) {
layoutInfo { layoutInfo {
...layoutInfo ...layoutInfo
} }
images {
...image
}
} }
} }

View File

@ -7,6 +7,7 @@ public interface IWorkshopService
Task<Stream?> GetEntryIcon(long entryId, CancellationToken cancellationToken); Task<Stream?> GetEntryIcon(long entryId, CancellationToken cancellationToken);
Task<ImageUploadResult> SetEntryIcon(long entryId, Stream icon, CancellationToken cancellationToken); Task<ImageUploadResult> SetEntryIcon(long entryId, Stream icon, CancellationToken cancellationToken);
Task<ImageUploadResult> UploadEntryImage(long entryId, ImageUploadRequest request, CancellationToken cancellationToken); Task<ImageUploadResult> UploadEntryImage(long entryId, ImageUploadRequest request, CancellationToken cancellationToken);
Task DeleteEntryImage(Guid id, CancellationToken cancellationToken);
Task<WorkshopStatus> GetWorkshopStatus(CancellationToken cancellationToken); Task<WorkshopStatus> GetWorkshopStatus(CancellationToken cancellationToken);
Task<bool> ValidateWorkshopStatus(CancellationToken cancellationToken); Task<bool> ValidateWorkshopStatus(CancellationToken cancellationToken);
Task NavigateToEntry(long entryId, EntryType entryType); Task NavigateToEntry(long entryId, EntryType entryType);

View File

@ -80,6 +80,14 @@ public class WorkshopService : IWorkshopService
return ImageUploadResult.FromSuccess(); return ImageUploadResult.FromSuccess();
} }
/// <inheritdoc />
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();
}
/// <inheritdoc /> /// <inheritdoc />
public async Task<IWorkshopService.WorkshopStatus> GetWorkshopStatus(CancellationToken cancellationToken) public async Task<IWorkshopService.WorkshopStatus> GetWorkshopStatus(CancellationToken cancellationToken)
{ {

View File

@ -2,9 +2,9 @@ namespace Artemis.WebClient.Workshop;
public static class WorkshopConstants public static class WorkshopConstants
{ {
// public const string AUTHORITY_URL = "https://localhost:5001"; public const string AUTHORITY_URL = "https://localhost:5001";
// public const string WORKSHOP_URL = "https://localhost:7281"; public const string WORKSHOP_URL = "https://localhost:7281";
public const string AUTHORITY_URL = "https://identity.artemis-rgb.com"; // public const string AUTHORITY_URL = "https://identity.artemis-rgb.com";
public const string WORKSHOP_URL = "https://workshop.artemis-rgb.com"; // public const string WORKSHOP_URL = "https://workshop.artemis-rgb.com";
public const string WORKSHOP_CLIENT_NAME = "WorkshopApiClient"; public const string WORKSHOP_CLIENT_NAME = "WorkshopApiClient";
} }

View File

@ -53,6 +53,8 @@ type Entry {
type Image { type Image {
description: String description: String
entry: Entry
entryId: Long
height: Int! height: Int!
id: UUID! id: UUID!
mimeType: String! mimeType: String!
@ -79,6 +81,7 @@ type Mutation {
removeEntry(id: Long!): Entry removeEntry(id: Long!): Entry
removeLayoutInfo(id: Long!): LayoutInfo! removeLayoutInfo(id: Long!): LayoutInfo!
updateEntry(input: UpdateEntryInput!): Entry updateEntry(input: UpdateEntryInput!): Entry
updateEntryImage(input: UpdateEntryImageInput!): Image
} }
type Query { type Query {
@ -260,6 +263,8 @@ input EntryTypeOperationFilterInput {
input ImageFilterInput { input ImageFilterInput {
and: [ImageFilterInput!] and: [ImageFilterInput!]
description: StringOperationFilterInput description: StringOperationFilterInput
entry: EntryFilterInput
entryId: LongOperationFilterInput
height: IntOperationFilterInput height: IntOperationFilterInput
id: UuidOperationFilterInput id: UuidOperationFilterInput
mimeType: StringOperationFilterInput mimeType: StringOperationFilterInput
@ -271,6 +276,8 @@ input ImageFilterInput {
input ImageSortInput { input ImageSortInput {
description: SortEnumType description: SortEnumType
entry: EntrySortInput
entryId: SortEnumType
height: SortEnumType height: SortEnumType
id: SortEnumType id: SortEnumType
mimeType: SortEnumType mimeType: SortEnumType
@ -418,6 +425,12 @@ input TagFilterInput {
or: [TagFilterInput!] or: [TagFilterInput!]
} }
input UpdateEntryImageInput {
description: String!
id: UUID!
name: String!
}
input UpdateEntryInput { input UpdateEntryInput {
categories: [Long!]! categories: [Long!]!
description: String! description: String!