mirror of
https://github.com/Artemis-RGB/Artemis
synced 2025-12-12 13:28:33 +00:00
Merge branch 'development'
This commit is contained in:
commit
ab64d1332b
@ -19,14 +19,16 @@ public static class ArtemisLayoutExtensions
|
||||
/// Renders the layout to a bitmap.
|
||||
/// </summary>
|
||||
/// <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>
|
||||
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 class ArtemisLayoutExtensions
|
||||
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 class ArtemisLayoutExtensions
|
||||
// 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};
|
||||
|
||||
@ -27,6 +27,12 @@ public interface IRouter
|
||||
/// <param name="options">Optional navigation options used to control navigation behaviour.</param>
|
||||
/// <returns>A task representing the operation</returns>
|
||||
Task Navigate(string path, RouterNavigationOptions? options = null);
|
||||
|
||||
/// <summary>
|
||||
/// Asynchronously reloads the current route
|
||||
/// </summary>
|
||||
/// <returns>A task representing the operation</returns>
|
||||
Task Reload();
|
||||
|
||||
/// <summary>
|
||||
/// Asynchronously navigates back to the previous active route.
|
||||
|
||||
@ -79,6 +79,19 @@ internal class Router : CorePropertyChanged, IRouter, IDisposable
|
||||
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)
|
||||
{
|
||||
if (_root == null)
|
||||
|
||||
@ -7,16 +7,17 @@
|
||||
x:Class="Artemis.UI.Screens.Debugger.Routing.RoutingDebugView"
|
||||
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.KeyBindings>
|
||||
<KeyBinding Gesture="Enter" Command="{CompiledBinding Navigate}"></KeyBinding>
|
||||
</TextBox.KeyBindings>
|
||||
</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>
|
||||
<ScrollViewer Grid.Row="2" Grid.Column="0" Grid.ColumnSpan="2" Name="LogsScrollViewer" VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Auto">
|
||||
<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="3" Name="LogsScrollViewer" VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Auto">
|
||||
<SelectableTextBlock
|
||||
Inlines="{CompiledBinding Lines}"
|
||||
FontFamily="{StaticResource RobotoMono}"
|
||||
|
||||
@ -29,6 +29,7 @@ public partial class RoutingDebugViewModel : ActivatableViewModelBase
|
||||
{
|
||||
_router = router;
|
||||
DisplayName = "Routing";
|
||||
Reload = ReactiveCommand.CreateFromTask(ExecutReload);
|
||||
Navigate = ReactiveCommand.CreateFromTask(ExecuteNavigate, this.WhenAnyValue(vm => vm.Route).Select(r => !string.IsNullOrWhiteSpace(r)));
|
||||
|
||||
_formatter = new MessageTemplateTextFormatter(
|
||||
@ -48,6 +49,7 @@ public partial class RoutingDebugViewModel : ActivatableViewModelBase
|
||||
}
|
||||
|
||||
public InlineCollection Lines { get; } = new();
|
||||
public ReactiveCommand<Unit, Unit> Reload { get; }
|
||||
public ReactiveCommand<Unit, Unit> Navigate { get; }
|
||||
|
||||
private void OnLogEventAdded(object? sender, LogEventEventArgs e)
|
||||
@ -87,6 +89,18 @@ public partial class RoutingDebugViewModel : ActivatableViewModelBase
|
||||
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)
|
||||
{
|
||||
|
||||
@ -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<BlankViewModel>
|
||||
{
|
||||
public BlankView()
|
||||
{
|
||||
|
||||
@ -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<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";
|
||||
}
|
||||
|
||||
@ -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<Unit, Unit> 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);
|
||||
}
|
||||
}
|
||||
@ -11,7 +11,7 @@
|
||||
<UserControl.Resources>
|
||||
<converters:BytesToStringConverter x:Key="BytesToStringConverter" />
|
||||
</UserControl.Resources>
|
||||
<Border Classes="card" Padding="0" Width="300" ClipToBounds="True" Margin="5">
|
||||
<Border Classes="card" Padding="0" ClipToBounds="True">
|
||||
<Grid RowDefinitions="230,*">
|
||||
<Rectangle Grid.Row="0" Fill="{DynamicResource CheckerboardBrush}" />
|
||||
<Image Grid.Row="0"
|
||||
|
||||
@ -1,64 +1,97 @@
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Net.Http;
|
||||
using System.Reactive.Disposables;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Input;
|
||||
using Artemis.UI.Shared;
|
||||
using Artemis.UI.Shared.Services;
|
||||
using Artemis.WebClient.Workshop;
|
||||
using Artemis.WebClient.Workshop.Handlers.UploadHandlers;
|
||||
using Avalonia.Media.Imaging;
|
||||
using Avalonia.Threading;
|
||||
using FluentAvalonia.UI.Controls;
|
||||
using PropertyChanged.SourceGenerator;
|
||||
using ReactiveUI;
|
||||
using ReactiveUI.Validation.Extensions;
|
||||
using ContentDialogButton = Artemis.UI.Shared.Services.Builders.ContentDialogButton;
|
||||
|
||||
namespace Artemis.UI.Screens.Workshop.Image;
|
||||
|
||||
public partial class ImageSubmissionViewModel : ValidatableViewModelBase
|
||||
{
|
||||
private readonly ImageUploadRequest _image;
|
||||
private readonly IWindowService _windowService;
|
||||
[Notify(Setter.Private)] private Bitmap? _bitmap;
|
||||
[Notify(Setter.Private)] private string? _imageDimensions;
|
||||
[Notify(Setter.Private)] private long _fileSize;
|
||||
[Notify] private string? _name;
|
||||
[Notify] private string? _description;
|
||||
[Notify] private bool _hasChanges;
|
||||
[Notify] private ICommand? _remove;
|
||||
|
||||
public ImageSubmissionViewModel(ImageUploadRequest image, IWindowService windowService)
|
||||
public ImageSubmissionViewModel(ImageUploadRequest imageUploadRequest, IWindowService windowService)
|
||||
{
|
||||
_image = image;
|
||||
ImageUploadRequest = imageUploadRequest;
|
||||
_windowService = windowService;
|
||||
|
||||
FileSize = _image.File.Length;
|
||||
Name = _image.Name;
|
||||
Description = _image.Description;
|
||||
|
||||
|
||||
FileSize = imageUploadRequest.File.Length;
|
||||
Name = imageUploadRequest.Name;
|
||||
Description = imageUploadRequest.Description;
|
||||
HasChanges = true;
|
||||
|
||||
this.WhenActivated(d =>
|
||||
{
|
||||
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<ContentDialogResult> 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;
|
||||
}
|
||||
|
||||
@ -12,7 +12,7 @@
|
||||
<UserControl.Resources>
|
||||
<converters:DateTimeConverter x:Key="DateTimeConverter" />
|
||||
</UserControl.Resources>
|
||||
<Grid ColumnDefinitions="300,*" RowDefinitions="*, Auto">
|
||||
<Grid ColumnDefinitions="300,*,300" RowDefinitions="*, Auto">
|
||||
<StackPanel Grid.Column="0" Grid.RowSpan="2" Spacing="10">
|
||||
<Border Classes="card" VerticalAlignment="Top" Margin="0 0 10 0">
|
||||
<StackPanel>
|
||||
@ -48,8 +48,35 @@
|
||||
View workshop page
|
||||
</controls:HyperlinkButton>
|
||||
</StackPanel>
|
||||
|
||||
<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 SaveChanges}">Save</Button>
|
||||
</StackPanel>
|
||||
|
||||
@ -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<WorkshopDetailParameters>
|
||||
{
|
||||
private readonly IWorkshopClient _client;
|
||||
private readonly Func<EntrySpecificationsViewModel> _getGetSpecificationsVm;
|
||||
private readonly IRouter _router;
|
||||
private readonly IWindowService _windowService;
|
||||
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 EntrySpecificationsViewModel? _entrySpecificationsViewModel;
|
||||
[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;
|
||||
_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<ImageSubmissionViewModel> Images { get; } = new();
|
||||
public ReactiveCommand<Unit, Unit> CreateRelease { get; }
|
||||
public ReactiveCommand<Unit, Unit> DeleteSubmission { get; }
|
||||
public ReactiveCommand<Unit, Unit> ViewWorkshopPage { get; }
|
||||
public ReactiveCommand<Unit, Unit> AddImage { get; }
|
||||
public ReactiveCommand<Unit, Unit> SaveChanges { get; }
|
||||
public ReactiveCommand<Unit, Unit> DiscardChanges { get; }
|
||||
|
||||
|
||||
public override async Task OnNavigating(WorkshopDetailParameters parameters, NavigationArguments args, CancellationToken cancellationToken)
|
||||
{
|
||||
IOperationResult<IGetSubmittedEntryByIdResult> result = await _client.GetSubmittedEntryById.ExecuteAsync(parameters.EntryId, cancellationToken);
|
||||
@ -63,7 +79,8 @@ public partial class SubmissionDetailViewModel : RoutableScreen<WorkshopDetailPa
|
||||
return;
|
||||
|
||||
Entry = result.Data?.Entry;
|
||||
await ApplyFromEntry(cancellationToken);
|
||||
await ApplyDetailsFromEntry(cancellationToken);
|
||||
ApplyImagesFromEntry();
|
||||
}
|
||||
|
||||
public override async Task OnClosing(NavigationArguments args)
|
||||
@ -76,34 +93,69 @@ public partial class SubmissionDetailViewModel : RoutableScreen<WorkshopDetailPa
|
||||
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)
|
||||
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;
|
||||
((INotifyCollectionChanged) EntrySpecificationsViewModel.SelectedCategories).CollectionChanged -= SelectedCategoriesOnCollectionChanged;
|
||||
EntrySpecificationsViewModel.Tags.CollectionChanged -= TagsOnCollectionChanged;
|
||||
}
|
||||
// _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 = _getGetSpecificationsVm();
|
||||
|
||||
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();
|
||||
|
||||
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;
|
||||
Images.Remove(viewModel);
|
||||
UpdateHasChanges();
|
||||
});
|
||||
Images.Add(viewModel);
|
||||
}
|
||||
|
||||
private async Task<Bitmap?> GetEntryIcon(CancellationToken cancellationToken)
|
||||
@ -128,19 +180,22 @@ public partial class SubmissionDetailViewModel : RoutableScreen<WorkshopDetailPa
|
||||
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 @@ public partial class SubmissionDetailViewModel : RoutableScreen<WorkshopDetailPa
|
||||
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 @@ public partial class SubmissionDetailViewModel : RoutableScreen<WorkshopDetailPa
|
||||
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()
|
||||
{
|
||||
if (Entry != null)
|
||||
await _workshopService.NavigateToEntry(Entry.Id, Entry.EntryType);
|
||||
}
|
||||
|
||||
private void TagsOnCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
|
||||
{
|
||||
UpdateHasChanges();
|
||||
}
|
||||
|
||||
private void SelectedCategoriesOnCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
|
||||
{
|
||||
UpdateHasChanges();
|
||||
}
|
||||
|
||||
private void EntrySpecificationsViewModelOnPropertyChanged(object? sender, PropertyChangedEventArgs e)
|
||||
private void InputChanged(object? sender, EventArgs e)
|
||||
{
|
||||
UpdateHasChanges();
|
||||
}
|
||||
|
||||
@ -23,6 +23,11 @@
|
||||
|
||||
<ScrollViewer Grid.Row="1" Margin="0 20 0 0">
|
||||
<ItemsControl ItemsSource="{CompiledBinding Images}">
|
||||
<ItemsControl.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<ContentControl Content="{CompiledBinding}" Margin="0 0 5 0" Width="300"></ContentControl>
|
||||
</DataTemplate>
|
||||
</ItemsControl.ItemTemplate>
|
||||
<ItemsControl.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<WrapPanel />
|
||||
|
||||
@ -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<ImageUploadRequest, ImageSubmissionViewModel> _getImageSubmissionViewModel;
|
||||
private readonly SourceList<ImageUploadRequest> _stateImages;
|
||||
@ -66,7 +65,7 @@ public class ImagesStepViewModel : SubmissionViewModel
|
||||
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();
|
||||
|
||||
@ -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">
|
||||
<Grid RowDefinitions="Auto,*">
|
||||
<Grid RowDefinitions="Auto,*, *">
|
||||
<StackPanel>
|
||||
<StackPanel.Styles>
|
||||
<Styles>
|
||||
|
||||
@ -145,7 +145,7 @@ public partial class LayoutSelectionStepViewModel : SubmissionViewModel
|
||||
return true;
|
||||
}
|
||||
|
||||
private void SetDeviceImages()
|
||||
private async Task SetDeviceImages()
|
||||
{
|
||||
if (Layout == null)
|
||||
return;
|
||||
@ -153,15 +153,15 @@ public partial class LayoutSelectionStepViewModel : SubmissionViewModel
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
@ -39,7 +39,10 @@
|
||||
<GraphQL Update="Queries\RemoveEntry.graphql">
|
||||
<Generator>MSBuild:GenerateGraphQLCode</Generator>
|
||||
</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>
|
||||
</GraphQL>
|
||||
</ItemGroup>
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -0,0 +1,5 @@
|
||||
mutation UpdateEntryImage ($id: UUID! $name: String! $description: String) {
|
||||
updateEntryImage(input: {id: $id, name: $name description: $description}) {
|
||||
id
|
||||
}
|
||||
}
|
||||
@ -11,5 +11,8 @@ query GetSubmittedEntryById($id: Long!) {
|
||||
layoutInfo {
|
||||
...layoutInfo
|
||||
}
|
||||
images {
|
||||
...image
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -7,6 +7,7 @@ public interface IWorkshopService
|
||||
Task<Stream?> GetEntryIcon(long entryId, CancellationToken cancellationToken);
|
||||
Task<ImageUploadResult> SetEntryIcon(long entryId, Stream icon, CancellationToken cancellationToken);
|
||||
Task<ImageUploadResult> UploadEntryImage(long entryId, ImageUploadRequest request, CancellationToken cancellationToken);
|
||||
Task DeleteEntryImage(Guid id, CancellationToken cancellationToken);
|
||||
Task<WorkshopStatus> GetWorkshopStatus(CancellationToken cancellationToken);
|
||||
Task<bool> ValidateWorkshopStatus(CancellationToken cancellationToken);
|
||||
Task NavigateToEntry(long entryId, EntryType entryType);
|
||||
|
||||
@ -80,6 +80,14 @@ public class WorkshopService : IWorkshopService
|
||||
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 />
|
||||
public async Task<IWorkshopService.WorkshopStatus> GetWorkshopStatus(CancellationToken cancellationToken)
|
||||
{
|
||||
|
||||
@ -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!
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user