diff --git a/src/Artemis.UI.Shared/Services/Builders/ContentDialogBuilder.cs b/src/Artemis.UI.Shared/Services/Builders/ContentDialogBuilder.cs
index 92e681433..c5a9f3e34 100644
--- a/src/Artemis.UI.Shared/Services/Builders/ContentDialogBuilder.cs
+++ b/src/Artemis.UI.Shared/Services/Builders/ContentDialogBuilder.cs
@@ -148,6 +148,16 @@ public class ContentDialogBuilder
_contentDialog.FullSizeDesired = true;
return this;
}
+
+ ///
+ /// Changes the dialog to be full screen.
+ ///
+ /// The builder that can be used to further build the dialog.
+ public ContentDialogBuilder WithFullScreen()
+ {
+ _contentDialog.Classes.Add("fullscreen");
+ return this;
+ }
///
/// Asynchronously shows the content dialog.
diff --git a/src/Artemis.UI.Shared/Services/Builders/FileDialogFilterBuilder.cs b/src/Artemis.UI.Shared/Services/Builders/FileDialogFilterBuilder.cs
index e3c946804..e0b6532fc 100644
--- a/src/Artemis.UI.Shared/Services/Builders/FileDialogFilterBuilder.cs
+++ b/src/Artemis.UI.Shared/Services/Builders/FileDialogFilterBuilder.cs
@@ -2,6 +2,7 @@
using System.Linq;
using Avalonia.Controls;
using Avalonia.Platform.Storage;
+using SkiaSharp;
namespace Artemis.UI.Shared.Services.Builders;
@@ -37,6 +38,29 @@ public class FileDialogFilterBuilder
return this;
}
+ ///
+ /// Adds all supported bitmap types to the filter.
+ ///
+ public FileDialogFilterBuilder WithBitmaps()
+ {
+ // Formats from SKEncodedImageFormat
+ return WithExtension("astc")
+ .WithExtension("avif")
+ .WithExtension("bmp")
+ .WithExtension("dng")
+ .WithExtension("gif")
+ .WithExtension("heif")
+ .WithExtension("ico")
+ .WithExtension("jpg")
+ .WithExtension("jpeg")
+ .WithExtension("ktx")
+ .WithExtension("pkm")
+ .WithExtension("png")
+ .WithExtension("wbmp")
+ .WithExtension("webp")
+ .WithName("Bitmap image");
+ }
+
internal FilePickerFileType Build()
{
return new FilePickerFileType(_name)
diff --git a/src/Artemis.UI/Screens/Device/Tabs/DeviceLogicalLayoutDialogView.axaml b/src/Artemis.UI/Screens/Device/Tabs/DeviceLogicalLayoutDialogView.axaml
index 213762d55..4a86455ad 100644
--- a/src/Artemis.UI/Screens/Device/Tabs/DeviceLogicalLayoutDialogView.axaml
+++ b/src/Artemis.UI/Screens/Device/Tabs/DeviceLogicalLayoutDialogView.axaml
@@ -10,13 +10,13 @@
Artemis couldn't automatically determine the logical layout of your
-
+
While not as important as the physical layout, setting the correct logical layout will allow Artemis to show the right keycaps (if a matching layout file is present)
-
-
-
-
-
-
-
+
+
+
+
diff --git a/src/Artemis.UI/Screens/Sidebar/Dialogs/ProfileConfigurationEditViewModel.cs b/src/Artemis.UI/Screens/Sidebar/Dialogs/ProfileConfigurationEditViewModel.cs
index 4edaa16a0..a7ba504ee 100644
--- a/src/Artemis.UI/Screens/Sidebar/Dialogs/ProfileConfigurationEditViewModel.cs
+++ b/src/Artemis.UI/Screens/Sidebar/Dialogs/ProfileConfigurationEditViewModel.cs
@@ -181,7 +181,7 @@ public partial class ProfileConfigurationEditViewModel : DialogViewModelBase f.WithExtension("png").WithExtension("jpg").WithExtension("bmp").WithName("Bitmap image"))
+ .HavingFilter(f => f.WithBitmaps())
.ShowAsync();
if (result == null)
diff --git a/src/Artemis.UI/Screens/Workshop/Image/ImageSubmissionView.axaml b/src/Artemis.UI/Screens/Workshop/Image/ImageSubmissionView.axaml
new file mode 100644
index 000000000..ca6ec4cdd
--- /dev/null
+++ b/src/Artemis.UI/Screens/Workshop/Image/ImageSubmissionView.axaml
@@ -0,0 +1,41 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Artemis.UI/Screens/Workshop/Image/ImageSubmissionView.axaml.cs b/src/Artemis.UI/Screens/Workshop/Image/ImageSubmissionView.axaml.cs
new file mode 100644
index 000000000..5fa82a6c2
--- /dev/null
+++ b/src/Artemis.UI/Screens/Workshop/Image/ImageSubmissionView.axaml.cs
@@ -0,0 +1,11 @@
+using Avalonia.ReactiveUI;
+
+namespace Artemis.UI.Screens.Workshop.Image;
+
+public partial class ImageSubmissionView : ReactiveUserControl
+{
+ public ImageSubmissionView()
+ {
+ InitializeComponent();
+ }
+}
\ No newline at end of file
diff --git a/src/Artemis.UI/Screens/Workshop/Image/ImageSubmissionViewModel.cs b/src/Artemis.UI/Screens/Workshop/Image/ImageSubmissionViewModel.cs
new file mode 100644
index 000000000..5b0c03cbe
--- /dev/null
+++ b/src/Artemis.UI/Screens/Workshop/Image/ImageSubmissionViewModel.cs
@@ -0,0 +1,42 @@
+using System.IO;
+using System.Reactive.Disposables;
+using System.Threading.Tasks;
+using System.Windows.Input;
+using Artemis.UI.Shared;
+using Artemis.UI.Shared.Services;
+using Avalonia.Media.Imaging;
+using Avalonia.Threading;
+using PropertyChanged.SourceGenerator;
+using ReactiveUI;
+
+namespace Artemis.UI.Screens.Workshop.Image;
+
+public partial class ImageSubmissionViewModel : ActivatableViewModelBase
+{
+ [Notify(Setter.Private)] private Bitmap? _bitmap;
+ [Notify(Setter.Private)] private string? _fileName;
+ [Notify(Setter.Private)] private string? _imageDimensions;
+ [Notify(Setter.Private)] private long _fileSize;
+ [Notify] private ICommand? _remove;
+
+ public ImageSubmissionViewModel(Stream imageStream)
+ {
+ this.WhenActivated(d =>
+ {
+ Dispatcher.UIThread.Invoke(() =>
+ {
+ imageStream.Seek(0, SeekOrigin.Begin);
+ Bitmap = new Bitmap(imageStream);
+ FileSize = imageStream.Length;
+ ImageDimensions = Bitmap.Size.Width + "x" + Bitmap.Size.Height;
+
+ if (imageStream is FileStream fileStream)
+ FileName = Path.GetFileName(fileStream.Name);
+ else
+ FileName = "Unnamed image";
+
+ Bitmap.DisposeWith(d);
+ }, DispatcherPriority.Background);
+ });
+ }
+}
\ No newline at end of file
diff --git a/src/Artemis.UI/Screens/Workshop/Layout/Dialogs/DeviceProviderPickerDialogView.axaml b/src/Artemis.UI/Screens/Workshop/Layout/Dialogs/DeviceProviderPickerDialogView.axaml
new file mode 100644
index 000000000..57a7d0fb4
--- /dev/null
+++ b/src/Artemis.UI/Screens/Workshop/Layout/Dialogs/DeviceProviderPickerDialogView.axaml
@@ -0,0 +1,62 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ You do not have any device providers enabled
+
+
+
+
+
diff --git a/src/Artemis.UI/Screens/Workshop/Layout/Dialogs/DeviceProviderPickerDialogView.axaml.cs b/src/Artemis.UI/Screens/Workshop/Layout/Dialogs/DeviceProviderPickerDialogView.axaml.cs
new file mode 100644
index 000000000..7758ce344
--- /dev/null
+++ b/src/Artemis.UI/Screens/Workshop/Layout/Dialogs/DeviceProviderPickerDialogView.axaml.cs
@@ -0,0 +1,24 @@
+using Artemis.Core.DeviceProviders;
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Input;
+using Avalonia.Markup.Xaml;
+using Avalonia.ReactiveUI;
+
+namespace Artemis.UI.Screens.Workshop.Layout.Dialogs;
+
+public partial class DeviceProviderPickerDialogView : ReactiveUserControl
+{
+ public DeviceProviderPickerDialogView()
+ {
+ InitializeComponent();
+ }
+
+ private void InputElement_OnPointerReleased(object? sender, PointerReleasedEventArgs e)
+ {
+ if (sender is not IDataContextProvider {DataContext: DeviceProvider deviceProvider} || ViewModel == null)
+ return;
+
+ ViewModel?.SelectDeviceProvider(deviceProvider);
+ }
+}
\ No newline at end of file
diff --git a/src/Artemis.UI/Screens/Workshop/Layout/Dialogs/DeviceProviderPickerDialogViewModel.cs b/src/Artemis.UI/Screens/Workshop/Layout/Dialogs/DeviceProviderPickerDialogViewModel.cs
new file mode 100644
index 000000000..6bc61fbb7
--- /dev/null
+++ b/src/Artemis.UI/Screens/Workshop/Layout/Dialogs/DeviceProviderPickerDialogViewModel.cs
@@ -0,0 +1,24 @@
+using System.Collections.ObjectModel;
+using Artemis.Core.DeviceProviders;
+using Artemis.Core.Services;
+using Artemis.UI.Shared;
+
+namespace Artemis.UI.Screens.Workshop.Layout.Dialogs;
+
+public class DeviceProviderPickerDialogViewModel : ContentDialogViewModelBase
+{
+ public ObservableCollection DeviceProviders { get; }
+
+ public DeviceProviderPickerDialogViewModel(IPluginManagementService pluginManagementService)
+ {
+ DeviceProviders = new ObservableCollection(pluginManagementService.GetFeaturesOfType());
+ }
+
+ public DeviceProvider? DeviceProvider { get; set; }
+
+ public void SelectDeviceProvider(DeviceProvider deviceProvider)
+ {
+ DeviceProvider = deviceProvider;
+ ContentDialog?.Hide();
+ }
+}
\ No newline at end of file
diff --git a/src/Artemis.UI/Screens/Workshop/Layout/LayoutInfoView.axaml b/src/Artemis.UI/Screens/Workshop/Layout/LayoutInfoView.axaml
index 92d02d58f..8a505a1f8 100644
--- a/src/Artemis.UI/Screens/Workshop/Layout/LayoutInfoView.axaml
+++ b/src/Artemis.UI/Screens/Workshop/Layout/LayoutInfoView.axaml
@@ -3,8 +3,55 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:layout="clr-namespace:Artemis.UI.Screens.Workshop.Layout"
+ xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Artemis.UI.Screens.Workshop.Layout.LayoutInfoView"
x:DataType="layout:LayoutInfoViewModel">
- Welcome to Avalonia!
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Artemis.UI/Screens/Workshop/Layout/LayoutInfoView.axaml.cs b/src/Artemis.UI/Screens/Workshop/Layout/LayoutInfoView.axaml.cs
index b7371d6fe..57092f2aa 100644
--- a/src/Artemis.UI/Screens/Workshop/Layout/LayoutInfoView.axaml.cs
+++ b/src/Artemis.UI/Screens/Workshop/Layout/LayoutInfoView.axaml.cs
@@ -1,10 +1,8 @@
-using Avalonia;
-using Avalonia.Controls;
-using Avalonia.Markup.Xaml;
+using Avalonia.ReactiveUI;
namespace Artemis.UI.Screens.Workshop.Layout;
-public partial class LayoutInfoView : UserControl
+public partial class LayoutInfoView : ReactiveUserControl
{
public LayoutInfoView()
{
diff --git a/src/Artemis.UI/Screens/Workshop/Layout/LayoutInfoViewModel.cs b/src/Artemis.UI/Screens/Workshop/Layout/LayoutInfoViewModel.cs
index eea0f305b..22ee6a750 100644
--- a/src/Artemis.UI/Screens/Workshop/Layout/LayoutInfoViewModel.cs
+++ b/src/Artemis.UI/Screens/Workshop/Layout/LayoutInfoViewModel.cs
@@ -1,32 +1,68 @@
using System;
+using System.Linq;
+using System.Reactive.Linq;
+using System.Threading.Tasks;
+using System.Windows.Input;
using Artemis.Core;
-using Artemis.UI.Screens.Workshop.SubmissionWizard.Models;
+using Artemis.Core.DeviceProviders;
+using Artemis.Core.Services;
+using Artemis.UI.Screens.Workshop.Layout.Dialogs;
using Artemis.UI.Shared;
+using Artemis.UI.Shared.Services;
using PropertyChanged.SourceGenerator;
-using RGB.NET.Core;
-using KeyboardLayoutType = Artemis.Core.KeyboardLayoutType;
+using ReactiveUI;
+using ReactiveUI.Validation.Extensions;
namespace Artemis.UI.Screens.Workshop.Layout;
-public partial class LayoutInfoViewModel : ViewModelBase
+public partial class LayoutInfoViewModel : ValidatableViewModelBase
{
- [Notify] private Guid _deviceProvider;
+ private readonly IWindowService _windowService;
+ private readonly ObservableAsPropertyHelper _deviceProviders;
[Notify] private string? _vendor;
[Notify] private string? _model;
- [Notify] private KeyboardLayoutType? _physicalLayout;
- [Notify] private string? _logicalLayout;
-
- ///
- public LayoutInfoViewModel(ArtemisLayout layout)
+ [Notify] private Guid _deviceProviderId;
+ [Notify] private string? _deviceProviderIdInput;
+ [Notify] private ICommand? _remove;
+
+ public LayoutInfoViewModel(ArtemisLayout layout,
+ IDeviceService deviceService,
+ IWindowService windowService,
+ IPluginManagementService pluginManagementService)
{
- DisplayKeyboardLayout = layout.RgbLayout.Type == RGBDeviceType.Keyboard;
+ _windowService = windowService;
+ _vendor = layout.RgbLayout.Vendor;
+ _model = layout.RgbLayout.Model;
+
+ DeviceProvider? deviceProvider = deviceService.Devices.FirstOrDefault(d => d.Layout == layout)?.DeviceProvider;
+ if (deviceProvider != null)
+ _deviceProviderId = deviceProvider.Plugin.Guid;
+
+ _deviceProviders = this.WhenAnyValue(vm => vm.DeviceProviderId)
+ .Select(id => pluginManagementService.GetAllPlugins().FirstOrDefault(p => p.Guid == id)?.Features.Select(f => f.Name))
+ .Select(names => names != null ? string.Join(", ", names) : "")
+ .ToProperty(this, vm => vm.DeviceProviders);
+
+ this.WhenAnyValue(vm => vm.DeviceProviderId).Subscribe(g => DeviceProviderIdInput = g.ToString());
+ this.WhenAnyValue(vm => vm.DeviceProviderIdInput).Where(i => Guid.TryParse(i, out _)).Subscribe(i => DeviceProviderId = Guid.Parse(i!));
+
+ this.ValidationRule(vm => vm.Model, input => !string.IsNullOrWhiteSpace(input), "Device model is required");
+ this.ValidationRule(vm => vm.Vendor, input => !string.IsNullOrWhiteSpace(input), "Device vendor is required");
+ this.ValidationRule(vm => vm.DeviceProviderIdInput, input => Guid.TryParse(input, out _), "Must be a valid GUID formatted as: 00000000-0000-0000-0000-000000000000");
+ this.ValidationRule(vm => vm.DeviceProviderIdInput, input => !string.IsNullOrWhiteSpace(input), "Device provider ID is required");
}
- public LayoutInfoViewModel(ArtemisLayout layout, LayoutInfo layoutInfo)
- {
- DisplayKeyboardLayout = layout.RgbLayout.Type == RGBDeviceType.Keyboard;
-
- }
+ public string? DeviceProviders => _deviceProviders.Value;
- public bool DisplayKeyboardLayout { get; }
+ public async Task BrowseDeviceProvider()
+ {
+ await _windowService.CreateContentDialog()
+ .WithTitle("Select device provider")
+ .WithViewModel(out DeviceProviderPickerDialogViewModel vm)
+ .ShowAsync();
+
+ DeviceProvider? deviceProvider = vm.DeviceProvider;
+ if (deviceProvider != null)
+ DeviceProviderId = deviceProvider.Plugin.Guid;
+ }
}
\ No newline at end of file
diff --git a/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Models/LayoutEntrySource.cs b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Models/LayoutEntrySource.cs
index 5445b34ab..c795a5b6b 100644
--- a/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Models/LayoutEntrySource.cs
+++ b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Models/LayoutEntrySource.cs
@@ -1,8 +1,8 @@
-using System;
-using System.Collections.Generic;
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Linq;
using Artemis.Core;
-using Artemis.WebClient.Workshop;
-using KeyboardLayoutType = Artemis.WebClient.Workshop.KeyboardLayoutType;
+using Artemis.UI.Screens.Workshop.Layout;
namespace Artemis.UI.Screens.Workshop.SubmissionWizard.Models;
@@ -14,15 +14,16 @@ public class LayoutEntrySource : IEntrySource
}
public ArtemisLayout Layout { get; set; }
- public List LayoutInfo { get; } = new();
-}
+ public ObservableCollection LayoutInfo { get; } = new();
+ public KeyboardLayoutType PhysicalLayout { get; set; }
-public class LayoutInfo
-{
- public Guid DeviceProvider { get; set; }
- public RGBDeviceType DeviceType { get; set; }
- public string Model { get; set; }
- public string Vendor { get; set; }
- public string? LogicalLayout { get; set; }
- public KeyboardLayoutType? PhysicalLayout { get; set; }
+ private List GetLogicalLayouts()
+ {
+ return Layout.Leds
+ .Where(l => l.LayoutCustomLedData.LogicalLayouts != null)
+ .SelectMany(l => l.LayoutCustomLedData.LogicalLayouts!)
+ .Where(l => !string.IsNullOrWhiteSpace(l.Name))
+ .DistinctBy(l => l.Name)
+ .ToList();
+ }
}
\ No newline at end of file
diff --git a/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Models/SubmissionWizardState.cs b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Models/SubmissionWizardState.cs
index 15c6dca1d..c980a0895 100644
--- a/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Models/SubmissionWizardState.cs
+++ b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Models/SubmissionWizardState.cs
@@ -9,7 +9,7 @@ using DryIoc;
namespace Artemis.UI.Screens.Workshop.SubmissionWizard.Models;
-public class SubmissionWizardState
+public class SubmissionWizardState : IDisposable
{
private readonly IContainer _container;
private readonly IWindowService _windowService;
@@ -62,4 +62,11 @@ public class SubmissionWizardState
else
throw new NotImplementedException();
}
+
+ public void Dispose()
+ {
+ Icon?.Dispose();
+ foreach (Stream stream in Images)
+ stream.Dispose();
+ }
}
\ No newline at end of file
diff --git a/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/ImagesStepView.axaml b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/ImagesStepView.axaml
new file mode 100644
index 000000000..d563f760e
--- /dev/null
+++ b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/ImagesStepView.axaml
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+
+
+ Images
+
+ Optionally provide some images of your submission.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/ImagesStepView.axaml.cs b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/ImagesStepView.axaml.cs
new file mode 100644
index 000000000..f8c499a2d
--- /dev/null
+++ b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/ImagesStepView.axaml.cs
@@ -0,0 +1,14 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Markup.Xaml;
+using Avalonia.ReactiveUI;
+
+namespace Artemis.UI.Screens.Workshop.SubmissionWizard.Steps;
+
+public partial class ImagesStepView : ReactiveUserControl
+{
+ public ImagesStepView()
+ {
+ InitializeComponent();
+ }
+}
\ No newline at end of file
diff --git a/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/ImagesStepViewModel.cs b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/ImagesStepViewModel.cs
new file mode 100644
index 000000000..4e0d1e7c5
--- /dev/null
+++ b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/ImagesStepViewModel.cs
@@ -0,0 +1,68 @@
+using System;
+using System.Collections.ObjectModel;
+using System.IO;
+using System.Linq;
+using System.Reactive.Disposables;
+using System.Threading;
+using System.Threading.Tasks;
+using Artemis.UI.Screens.Workshop.Image;
+using Artemis.UI.Shared.Services;
+using DynamicData;
+using ReactiveUI;
+
+namespace Artemis.UI.Screens.Workshop.SubmissionWizard.Steps;
+
+public class ImagesStepViewModel : SubmissionViewModel
+{
+ private readonly IWindowService _windowService;
+ private readonly SourceList _imageStreams;
+
+ public ImagesStepViewModel(IWindowService windowService, Func imageSubmissionViewModel)
+ {
+ _windowService = windowService;
+
+ Continue = ReactiveCommand.Create(() => State.ChangeScreen());
+ GoBack = ReactiveCommand.Create(() => State.ChangeScreen());
+ Secondary = ReactiveCommand.CreateFromTask(ExecuteAddImage);
+ SecondaryText = "Add image";
+
+ _imageStreams = new SourceList();
+ _imageStreams.Connect()
+ .Transform(p => CreateImageSubmissionViewModel(imageSubmissionViewModel, p))
+ .Bind(out ReadOnlyObservableCollection images)
+ .Subscribe();
+ Images = images;
+
+ this.WhenActivated((CompositeDisposable d) =>
+ {
+ _imageStreams.Clear();
+ _imageStreams.AddRange(State.Images);
+ });
+ }
+
+ public ReadOnlyObservableCollection Images { get; }
+
+ private ImageSubmissionViewModel CreateImageSubmissionViewModel(Func imageSubmissionViewModel, Stream stream)
+ {
+ ImageSubmissionViewModel viewModel = imageSubmissionViewModel(stream);
+ viewModel.Remove = ReactiveCommand.Create(() => _imageStreams.Remove(stream));
+ return viewModel;
+ }
+
+ 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)
+ {
+ if (_imageStreams.Items.Any(i => i is FileStream fs && fs.Name == path))
+ continue;
+
+ FileStream stream = new(path, FileMode.Open);
+ _imageStreams.Add(stream);
+ State.Images.Add(stream);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/Layout/LayoutInfoStepView.axaml b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/Layout/LayoutInfoStepView.axaml
index 62d3b1563..5b621ec63 100644
--- a/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/Layout/LayoutInfoStepView.axaml
+++ b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/Layout/LayoutInfoStepView.axaml
@@ -3,10 +3,12 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:layout="clr-namespace:Artemis.UI.Screens.Workshop.SubmissionWizard.Steps.Layout"
+ xmlns:shared="clr-namespace:Artemis.UI.Shared;assembly=Artemis.UI.Shared"
+ xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Artemis.UI.Screens.Workshop.SubmissionWizard.Steps.Layout.LayoutInfoStepView"
x:DataType="layout:LayoutInfoStepViewModel">
-
+
@@ -24,7 +26,25 @@
-
+
+
+
+
+
+ Learn about physical layouts
+
+
+
-
+
+
+
+
diff --git a/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/Layout/LayoutInfoStepViewModel.cs b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/Layout/LayoutInfoStepViewModel.cs
index 370000363..585e2f8b4 100644
--- a/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/Layout/LayoutInfoStepViewModel.cs
+++ b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/Layout/LayoutInfoStepViewModel.cs
@@ -1,25 +1,117 @@
+using System;
+using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Reactive.Disposables;
+using System.Reactive.Linq;
+using Artemis.Core;
using Artemis.UI.Screens.Workshop.Layout;
using Artemis.UI.Screens.Workshop.SubmissionWizard.Models;
-using DynamicData;
+using PropertyChanged.SourceGenerator;
using ReactiveUI;
+using ReactiveUI.Validation.Extensions;
+using RGB.NET.Core;
+using KeyboardLayoutType = Artemis.Core.KeyboardLayoutType;
namespace Artemis.UI.Screens.Workshop.SubmissionWizard.Steps.Layout;
-public class LayoutInfoStepViewModel : SubmissionViewModel
+public partial class LayoutInfoStepViewModel : SubmissionViewModel
{
- public LayoutInfoStepViewModel()
+ private readonly Func _getLayoutInfoViewModel;
+ private ArtemisLayout? _layout;
+ [Notify(Setter.Private)] private bool _isKeyboardLayout;
+ [Notify] private ObservableCollection _layoutInfo = new();
+ [Notify] private KeyboardLayoutType _physicalLayout;
+
+ public LayoutInfoStepViewModel(Func getLayoutInfoViewModel)
{
+ _getLayoutInfoViewModel = getLayoutInfoViewModel;
+
GoBack = ReactiveCommand.Create(() => State.ChangeScreen());
- this.WhenActivated((CompositeDisposable _) =>
+ Continue = ReactiveCommand.Create(ExecuteContinue, ValidationContext.Valid);
+ Secondary = ReactiveCommand.Create(ExecuteAddLayoutInfo);
+ SecondaryText = "Add layout info";
+
+ this.WhenActivated(d =>
{
- LayoutInfo.Clear();
- if (State.EntrySource is LayoutEntrySource layoutEntrySource)
- LayoutInfo.AddRange(layoutEntrySource.LayoutInfo.Select(i => new LayoutInfoViewModel(layoutEntrySource.Layout, i)));
+ if (State.EntrySource is not LayoutEntrySource layoutEntrySource)
+ return;
+
+ _layout = layoutEntrySource.Layout;
+ IsKeyboardLayout = _layout.RgbLayout.Type == RGBDeviceType.Keyboard;
+ PhysicalLayout = layoutEntrySource.PhysicalLayout;
+ LayoutInfo = layoutEntrySource.LayoutInfo;
+
+ if (!LayoutInfo.Any())
+ ExecuteAddLayoutInfo();
+
+ this.ValidationRule(
+ vm => vm.PhysicalLayout,
+ this.WhenAnyValue(vm => vm.IsKeyboardLayout, vm => vm.PhysicalLayout, (isKeyboard, layout) => !isKeyboard || layout != KeyboardLayoutType.Unknown),
+ "A keyboard layout is required"
+ ).DisposeWith(d);
+ this.ValidationRule(
+ vm => vm.LayoutInfo,
+ this.WhenAnyValue(vm => vm.LayoutInfo.Count).Select(c => c != 0),
+ "At least one layout info is required"
+ ).DisposeWith(d);
});
}
- public ObservableCollection LayoutInfo { get; } = new();
+ private void ExecuteAddLayoutInfo()
+ {
+ if (_layout == null)
+ return;
+
+ LayoutInfoViewModel layoutInfo = _getLayoutInfoViewModel(_layout);
+ layoutInfo.Remove = ReactiveCommand.Create(() => LayoutInfo.Remove(layoutInfo));
+ LayoutInfo.Add(layoutInfo);
+ }
+
+ private void ExecuteContinue()
+ {
+ if (State.EntrySource is not LayoutEntrySource layoutEntrySource)
+ return;
+
+ layoutEntrySource.PhysicalLayout = PhysicalLayout;
+
+ if (string.IsNullOrWhiteSpace(State.Name))
+ State.Name = layoutEntrySource.Layout.RgbLayout.Name ?? "";
+ if (string.IsNullOrWhiteSpace(State.Summary))
+ {
+ State.Summary = !string.IsNullOrWhiteSpace(layoutEntrySource.Layout.RgbLayout.Vendor)
+ ? $"{layoutEntrySource.Layout.RgbLayout.Vendor} {layoutEntrySource.Layout.RgbLayout.Type} device layout"
+ : $"{layoutEntrySource.Layout.RgbLayout.Type} device layout";
+ }
+
+ if (string.IsNullOrWhiteSpace(State.Description))
+ {
+ State.Description = $@"### Layout properties
+**Name**
+{layoutEntrySource.Layout.RgbLayout.Name ?? "N/A"}
+**Description**
+{layoutEntrySource.Layout.RgbLayout.Description ?? "N/A"}
+**Author**
+{layoutEntrySource.Layout.RgbLayout.Author ?? "N/A"}
+**Type**
+{layoutEntrySource.Layout.RgbLayout.Type}
+**Vendor**
+{layoutEntrySource.Layout.RgbLayout.Vendor ?? "N/A"}
+**Model**
+{layoutEntrySource.Layout.RgbLayout.Model ?? "N/A"}
+**Shape**
+{layoutEntrySource.Layout.RgbLayout.Shape}
+**Width**
+{layoutEntrySource.Layout.RgbLayout.Width}mm
+**Height**
+{layoutEntrySource.Layout.RgbLayout.Height}mm";
+ }
+
+ State.Categories = new List {8}; // Device category, yes this could change but why would it
+
+ if (State.EntryId == null)
+ State.ChangeScreen();
+ else
+ State.ChangeScreen();
+ }
}
\ No newline at end of file
diff --git a/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/Layout/LayoutSelectionStepViewModel.cs b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/Layout/LayoutSelectionStepViewModel.cs
index 5c365fe6a..83c788e68 100644
--- a/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/Layout/LayoutSelectionStepViewModel.cs
+++ b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/Layout/LayoutSelectionStepViewModel.cs
@@ -5,7 +5,6 @@ using Artemis.Core.Services;
using PropertyChanged.SourceGenerator;
using ReactiveUI;
using System;
-using System.Collections.Generic;
using System.IO;
using System.Reactive.Disposables;
using System.Reactive.Linq;
@@ -14,6 +13,7 @@ using Artemis.UI.Screens.Workshop.SubmissionWizard.Models;
using Artemis.UI.Shared.Extensions;
using Artemis.UI.Shared.Services;
using Avalonia.Media.Imaging;
+using Avalonia.Threading;
using SkiaSharp;
namespace Artemis.UI.Screens.Workshop.SubmissionWizard.Steps.Layout;
@@ -36,11 +36,11 @@ public partial class LayoutSelectionStepViewModel : SubmissionViewModel
);
GoBack = ReactiveCommand.Create(() => State.ChangeScreen());
- Continue = ReactiveCommand.Create(ExecuteContinue, this.WhenAnyValue(vm => vm.Layout).Select(p => p != null));
+ Continue = ReactiveCommand.CreateFromTask(ExecuteContinue, this.WhenAnyValue(vm => vm.Layout).Select(p => p != null));
this.WhenAnyValue(vm => vm.SelectedDevice).WhereNotNull().Subscribe(d => Layout = d.Layout);
this.WhenAnyValue(vm => vm.Layout).Subscribe(CreatePreviewDevice);
-
+
this.WhenActivated((CompositeDisposable _) =>
{
ShowGoBack = State.EntryId == null;
@@ -82,44 +82,64 @@ public partial class LayoutSelectionStepViewModel : SubmissionViewModel
Layout = layout;
}
- private void ExecuteContinue()
+ private async Task ExecuteContinue()
{
if (Layout == null)
return;
State.EntrySource = new LayoutEntrySource(Layout);
- State.Name = Layout.RgbLayout.Name ?? "";
- State.Summary = !string.IsNullOrWhiteSpace(Layout.RgbLayout.Vendor)
- ? $"{Layout.RgbLayout.Vendor} {Layout.RgbLayout.Type} device layout"
- : $"{Layout.RgbLayout.Type} device layout";
-
- State.Categories = new List {8}; // Device category, yes this could change but why would it
-
- State.Icon?.Dispose();
- State.Icon = GetDeviceIcon();
-
+ await Dispatcher.UIThread.InvokeAsync(SetDeviceImages, DispatcherPriority.Background);
State.ChangeScreen();
}
- private Stream GetDeviceIcon()
+ private void SetDeviceImages()
{
- // Go through the hassle of resizing the image to 128x128 without losing aspect ratio, padding is added for this
- using RenderTargetBitmap image = Layout.RenderLayout(false);
- using MemoryStream stream = new();
- image.Save(stream);
- stream.Seek(0, SeekOrigin.Begin);
+ if (Layout == null)
+ return;
+
+ MemoryStream deviceWithoutLeds = new();
+ MemoryStream deviceWithLeds = new();
+
+ using (RenderTargetBitmap image = Layout.RenderLayout(false))
+ {
+ image.Save(deviceWithoutLeds);
+ deviceWithoutLeds.Seek(0, SeekOrigin.Begin);
+ }
+ using (RenderTargetBitmap image = Layout.RenderLayout(true))
+ {
+ image.Save(deviceWithLeds);
+ deviceWithLeds.Seek(0, SeekOrigin.Begin);
+ }
+ State.Icon?.Dispose();
+ foreach (Stream stateImage in State.Images)
+ stateImage.Dispose();
+ State.Images.Clear();
+
+ // Go through the hassle of resizing the image to 128x128 without losing aspect ratio, padding is added for this
+ State.Icon = ResizeImage(deviceWithoutLeds, 128);
+ State.Images.Add(deviceWithoutLeds);
+ State.Images.Add(deviceWithLeds);
+ }
+
+ private Stream ResizeImage(Stream image, int size)
+ {
MemoryStream output = new();
- using SKBitmap? sourceBitmap = SKBitmap.Decode(stream);
+ using MemoryStream input = new();
+
+ image.CopyTo(input);
+ input.Seek(0, SeekOrigin.Begin);
+
+ using SKBitmap? sourceBitmap = SKBitmap.Decode(input);
int sourceWidth = sourceBitmap.Width;
int sourceHeight = sourceBitmap.Height;
- float scale = Math.Min((float) 128 / sourceWidth, (float) 128 / sourceHeight);
+ float scale = Math.Min((float) size / sourceWidth, (float) size / sourceHeight);
SKSizeI scaledDimensions = new((int) Math.Floor(sourceWidth * scale), (int) Math.Floor(sourceHeight * scale));
- SKPointI offset = new((128 - scaledDimensions.Width) / 2, (128 - scaledDimensions.Height) / 2);
+ SKPointI offset = new((size - scaledDimensions.Width) / 2, (size - scaledDimensions.Height) / 2);
using SKBitmap? scaleBitmap = sourceBitmap.Resize(scaledDimensions, SKFilterQuality.High);
- using SKBitmap targetBitmap = new(128, 128);
+ using SKBitmap targetBitmap = new(size, size);
using SKCanvas canvas = new(targetBitmap);
canvas.Clear(SKColors.Transparent);
canvas.DrawBitmap(scaleBitmap, offset.X, offset.Y);
diff --git a/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/SpecificationsStepViewModel.cs b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/SpecificationsStepViewModel.cs
index 729f5b352..0f577e1fd 100644
--- a/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/SpecificationsStepViewModel.cs
+++ b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/SpecificationsStepViewModel.cs
@@ -57,7 +57,7 @@ public partial class SpecificationsStepViewModel : SubmissionViewModel
return;
ApplyToState();
- State.ChangeScreen();
+ State.ChangeScreen();
}
private void ApplyFromState()
diff --git a/src/Artemis.UI/Screens/Workshop/SubmissionWizard/SubmissionViewModel.cs b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/SubmissionViewModel.cs
index 5a118bae9..cbd398049 100644
--- a/src/Artemis.UI/Screens/Workshop/SubmissionWizard/SubmissionViewModel.cs
+++ b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/SubmissionViewModel.cs
@@ -8,9 +8,11 @@ namespace Artemis.UI.Screens.Workshop.SubmissionWizard;
public abstract partial class SubmissionViewModel : ValidatableViewModelBase
{
+ [Notify] private ReactiveCommand? _secondary;
[Notify] private ReactiveCommand? _continue;
[Notify] private ReactiveCommand? _goBack;
[Notify] private string _continueText = "Continue";
+ [Notify] private string? _secondaryText;
[Notify] private bool _showFinish;
[Notify] private bool _showGoBack = true;
[Notify] private bool _showHeader = true;
diff --git a/src/Artemis.UI/Screens/Workshop/SubmissionWizard/SubmissionWizardView.axaml b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/SubmissionWizardView.axaml
index d64ed0199..448e4420f 100644
--- a/src/Artemis.UI/Screens/Workshop/SubmissionWizard/SubmissionWizardView.axaml
+++ b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/SubmissionWizardView.axaml
@@ -16,9 +16,10 @@
WindowStartupLocation="CenterOwner">
-
-
-
+
+
+
@@ -36,16 +37,23 @@
-
+
+
+
-
+
diff --git a/src/Artemis.UI/Screens/Workshop/SubmissionWizard/SubmissionWizardViewModel.cs b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/SubmissionWizardViewModel.cs
index 787e9bdfc..efb87b34d 100644
--- a/src/Artemis.UI/Screens/Workshop/SubmissionWizard/SubmissionWizardViewModel.cs
+++ b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/SubmissionWizardViewModel.cs
@@ -1,10 +1,12 @@
-using Artemis.UI.Screens.Workshop.CurrentUser;
+using System.Reactive.Disposables;
+using Artemis.UI.Screens.Workshop.CurrentUser;
using Artemis.UI.Screens.Workshop.SubmissionWizard.Models;
using Artemis.UI.Screens.Workshop.SubmissionWizard.Steps;
using Artemis.UI.Shared;
using Artemis.UI.Shared.Services;
using DryIoc;
using PropertyChanged.SourceGenerator;
+using ReactiveUI;
namespace Artemis.UI.Screens.Workshop.SubmissionWizard;
@@ -26,6 +28,8 @@ public partial class SubmissionWizardViewModel : ActivatableViewModelBase, IWork
WindowService = windowService;
CurrentUserViewModel = currentUserViewModel;
CurrentUserViewModel.AllowLogout = false;
+
+ this.WhenActivated(d => _state.DisposeWith(d));
}
public IWindowService WindowService { get; }