1
0
mirror of https://github.com/Artemis-RGB/Artemis synced 2025-12-12 21:38:38 +00:00

Workshop - Layout info and images WIP

This commit is contained in:
Robert 2023-10-29 20:43:30 +01:00
parent f4b9b67f1a
commit c1e0dadce8
25 changed files with 680 additions and 90 deletions

View File

@ -148,6 +148,16 @@ public class ContentDialogBuilder
_contentDialog.FullSizeDesired = true;
return this;
}
/// <summary>
/// Changes the dialog to be full screen.
/// </summary>
/// <returns>The builder that can be used to further build the dialog.</returns>
public ContentDialogBuilder WithFullScreen()
{
_contentDialog.Classes.Add("fullscreen");
return this;
}
/// <summary>
/// Asynchronously shows the content dialog.

View File

@ -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;
}
/// <summary>
/// Adds all supported bitmap types to the filter.
/// </summary>
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)

View File

@ -10,13 +10,13 @@
<StackPanel>
<TextBlock TextWrapping="Wrap">Artemis couldn't automatically determine the logical layout of your</TextBlock>
<TextBlock TextWrapping="Wrap" Text="{CompiledBinding Device.RgbDevice.DeviceInfo.DeviceName, Mode=OneWay}" />
<TextBlock Margin="0 10" TextWrapping="Wrap">
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)
</TextBlock>
<AutoCompleteBox HorizontalAlignment="Stretch"
ItemsSource="{CompiledBinding Regions}"
<AutoCompleteBox HorizontalAlignment="Stretch"
ItemsSource="{CompiledBinding Regions}"
SelectedItem="{CompiledBinding SelectedRegion}"
ValueMemberBinding="{CompiledBinding EnglishName, DataType=globalization:RegionInfo}"
Watermark="Enter keyboard country name"
@ -26,12 +26,10 @@
Name="RegionsAutoCompleteBox">
<AutoCompleteBox.ItemTemplate>
<DataTemplate DataType="{x:Type globalization:RegionInfo}">
<StackPanel Orientation="Horizontal">
<TextBlock Text="{CompiledBinding EnglishName}"></TextBlock>
<TextBlock Text=" ("/>
<TextBlock FontWeight="SemiBold" Text="{CompiledBinding TwoLetterISORegionName}"></TextBlock>
<TextBlock Text=")"/>
</StackPanel>
<TextBlock>
<Run Text="{CompiledBinding EnglishName}"></Run>
<Run Text="(" /><Run FontWeight="SemiBold" Text="{CompiledBinding TwoLetterISORegionName}"></Run><Run Text=")" />
</TextBlock>
</DataTemplate>
</AutoCompleteBox.ItemTemplate>
</AutoCompleteBox>

View File

@ -181,7 +181,7 @@ public partial class ProfileConfigurationEditViewModel : DialogViewModelBase<Pro
private async Task ExecuteBrowseBitmapFile()
{
string[]? result = await _windowService.CreateOpenFileDialog()
.HavingFilter(f => f.WithExtension("png").WithExtension("jpg").WithExtension("bmp").WithName("Bitmap image"))
.HavingFilter(f => f.WithBitmaps())
.ShowAsync();
if (result == null)

View File

@ -0,0 +1,41 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:image="clr-namespace:Artemis.UI.Screens.Workshop.Image"
xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
xmlns:converters="clr-namespace:Artemis.UI.Shared.Converters;assembly=Artemis.UI.Shared"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Artemis.UI.Screens.Workshop.Image.ImageSubmissionView"
x:DataType="image:ImageSubmissionViewModel">
<UserControl.Resources>
<converters:BytesToStringConverter x:Key="BytesToStringConverter" />
</UserControl.Resources>
<Border Classes="card" Padding="0" Width="300" ClipToBounds="True" Margin="5">
<Grid RowDefinitions="230,*">
<Rectangle Grid.Row="0" Fill="{DynamicResource CheckerboardBrush}"/>
<Image Grid.Row="0"
VerticalAlignment="Center"
HorizontalAlignment="Center"
RenderOptions.BitmapInterpolationMode="HighQuality"
Source="{CompiledBinding Bitmap}"/>
<StackPanel Grid.Row="1" Margin="12">
<TextBlock Text="{CompiledBinding FileName, FallbackValue=Unnamed image}" TextTrimming="CharacterEllipsis" />
<StackPanel>
<TextBlock TextWrapping="Wrap" Classes="subtitle" Text="{CompiledBinding ImageDimensions, Mode=OneWay}" />
<TextBlock TextWrapping="Wrap" Classes="subtitle" Text="{CompiledBinding FileSize, Converter={StaticResource BytesToStringConverter}, Mode=OneWay}" />
</StackPanel>
</StackPanel>
<Button Grid.Row="1"
VerticalAlignment="Bottom"
HorizontalAlignment="Right"
Margin="6"
Classes="icon-button"
Command="{CompiledBinding Remove}"
ToolTip.Tip="Remove">
<avalonia:MaterialIcon Kind="Trash" />
</Button>
</Grid>
</Border>
</UserControl>

View File

@ -0,0 +1,11 @@
using Avalonia.ReactiveUI;
namespace Artemis.UI.Screens.Workshop.Image;
public partial class ImageSubmissionView : ReactiveUserControl<ImageSubmissionViewModel>
{
public ImageSubmissionView()
{
InitializeComponent();
}
}

View File

@ -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);
});
}
}

View File

@ -0,0 +1,62 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:dialogs="clr-namespace:Artemis.UI.Screens.Workshop.Layout.Dialogs"
xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
xmlns:deviceProviders="clr-namespace:Artemis.Core.DeviceProviders;assembly=Artemis.Core"
xmlns:shared="clr-namespace:Artemis.UI.Shared;assembly=Artemis.UI.Shared"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Artemis.UI.Screens.Workshop.Layout.Dialogs.DeviceProviderPickerDialogView"
x:DataType="dialogs:DeviceProviderPickerDialogViewModel">
<Grid RowDefinitions="Auto,*">
<ListBox Name="EffectDescriptorsList"
Grid.Row="1"
ItemsSource="{CompiledBinding DeviceProviders}"
IsVisible="{CompiledBinding DeviceProviders.Count}"
Height="300">
<ListBox.DataTemplates>
<DataTemplate DataType="{x:Type deviceProviders:DeviceProvider}">
<Grid RowDefinitions="Auto,*"
ColumnDefinitions="Auto,Auto"
Background="Transparent"
PointerReleased="InputElement_OnPointerReleased"
Margin="0 4"
VerticalAlignment="Center">
<shared:ArtemisIcon Grid.Column="0"
Grid.RowSpan="2"
Icon="{CompiledBinding Plugin.Info.Icon}"
Width="24"
Height="24"
VerticalAlignment="Center"
Margin="0 0 15 0" />
<TextBlock Grid.Column="1"
Grid.Row="0"
Classes="BodyStrongTextBlockStyle"
Text="{CompiledBinding Info.Name}"
VerticalAlignment="Bottom"
Width="450"
TextWrapping="Wrap" />
<TextBlock Grid.Column="1"
Grid.Row="1"
Foreground="{DynamicResource TextFillColorSecondary}"
Text="{CompiledBinding Plugin.Info.Name}"
VerticalAlignment="Top"
Width="450"
TextWrapping="Wrap" />
</Grid>
</DataTemplate>
</ListBox.DataTemplates>
</ListBox>
<Grid Grid.Row="1" Height="300">
<StackPanel VerticalAlignment="Center"
Spacing="20"
IsVisible="{CompiledBinding !DeviceProviders.Count}">
<avalonia:MaterialIcon Kind="CloseCircle" Width="32" Height="32" />
<TextBlock Classes="h5" TextAlignment="Center">You do not have any device providers enabled</TextBlock>
</StackPanel>
</Grid>
</Grid>
</UserControl>

View File

@ -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<DeviceProviderPickerDialogViewModel>
{
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);
}
}

View File

@ -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<DeviceProvider> DeviceProviders { get; }
public DeviceProviderPickerDialogViewModel(IPluginManagementService pluginManagementService)
{
DeviceProviders = new ObservableCollection<DeviceProvider>(pluginManagementService.GetFeaturesOfType<DeviceProvider>());
}
public DeviceProvider? DeviceProvider { get; set; }
public void SelectDeviceProvider(DeviceProvider deviceProvider)
{
DeviceProvider = deviceProvider;
ContentDialog?.Hide();
}
}

View File

@ -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!
</UserControl>
<Grid RowDefinitions="Auto,Auto,Auto" ColumnDefinitions="*,*">
<StackPanel Grid.Row="0" Grid.Column="0" Margin="0 0 4 0">
<Label>Model</Label>
<TextBox Text="{CompiledBinding Model}"></TextBox>
</StackPanel>
<Grid Grid.Row="0" Grid.Column="1" Margin="4 0 0 4" ColumnDefinitions="*,*" RowDefinitions="*,*">
<Label Grid.Row="0" Grid.Column="0" VerticalAlignment="Bottom">Vendor</Label>
<TextBox Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="2" Text="{CompiledBinding Vendor}"></TextBox>
</Grid>
<StackPanel Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="2">
<Label>Device provider ID</Label>
<TextBox Text="{CompiledBinding DeviceProviderIdInput}"></TextBox>
</StackPanel>
<Button Grid.Row="1"
Grid.Column="0"
Grid.ColumnSpan="2"
HorizontalAlignment="Right"
VerticalAlignment="Bottom"
Classes="AppBarButton"
Command="{CompiledBinding BrowseDeviceProvider}"
ToolTip.Tip="Browse">
...
</Button>
<TextBlock Grid.Row="2"
Grid.Column="0"
Grid.ColumnSpan="2"
Classes="subtitle"
Margin="0 2"
Text="{CompiledBinding DeviceProviders}"
VerticalAlignment="Top">
</TextBlock>
<Button Grid.Row="2"
Grid.Column="0"
Grid.ColumnSpan="2"
Margin="0 5"
HorizontalAlignment="Right"
VerticalAlignment="Top"
Command="{CompiledBinding Remove}"
Classes="icon-button">
<avalonia:MaterialIcon Kind="Trash" />
</Button>
</Grid>
</UserControl>

View File

@ -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<LayoutInfoViewModel>
{
public LayoutInfoView()
{

View File

@ -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<string?> _deviceProviders;
[Notify] private string? _vendor;
[Notify] private string? _model;
[Notify] private KeyboardLayoutType? _physicalLayout;
[Notify] private string? _logicalLayout;
/// <inheritdoc />
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;
}
}

View File

@ -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> LayoutInfo { get; } = new();
}
public ObservableCollection<LayoutInfoViewModel> 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<LayoutCustomLedDataLogicalLayout> 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();
}
}

View File

@ -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();
}
}

View File

@ -0,0 +1,34 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:steps="clr-namespace:Artemis.UI.Screens.Workshop.SubmissionWizard.Steps"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Artemis.UI.Screens.Workshop.SubmissionWizard.Steps.ImagesStepView"
x:DataType="steps:ImagesStepViewModel">
<Grid RowDefinitions="Auto,*">
<StackPanel Grid.Row="0">
<StackPanel.Styles>
<Styles>
<Style Selector="TextBlock">
<Setter Property="TextWrapping" Value="Wrap"></Setter>
</Style>
</Styles>
</StackPanel.Styles>
<TextBlock Theme="{StaticResource TitleTextBlockStyle}" TextWrapping="Wrap">Images</TextBlock>
<TextBlock TextWrapping="Wrap">
Optionally provide some images of your submission.
</TextBlock>
</StackPanel>
<ScrollViewer Grid.Row="1" Margin="0 20 0 0">
<ItemsControl ItemsSource="{CompiledBinding Images}">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<WrapPanel />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
</ScrollViewer>
</Grid>
</UserControl>

View File

@ -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<ImagesStepViewModel>
{
public ImagesStepView()
{
InitializeComponent();
}
}

View File

@ -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<Stream> _imageStreams;
public ImagesStepViewModel(IWindowService windowService, Func<Stream, ImageSubmissionViewModel> imageSubmissionViewModel)
{
_windowService = windowService;
Continue = ReactiveCommand.Create(() => State.ChangeScreen<UploadStepViewModel>());
GoBack = ReactiveCommand.Create(() => State.ChangeScreen<SpecificationsStepViewModel>());
Secondary = ReactiveCommand.CreateFromTask(ExecuteAddImage);
SecondaryText = "Add image";
_imageStreams = new SourceList<Stream>();
_imageStreams.Connect()
.Transform(p => CreateImageSubmissionViewModel(imageSubmissionViewModel, p))
.Bind(out ReadOnlyObservableCollection<ImageSubmissionViewModel> images)
.Subscribe();
Images = images;
this.WhenActivated((CompositeDisposable d) =>
{
_imageStreams.Clear();
_imageStreams.AddRange(State.Images);
});
}
public ReadOnlyObservableCollection<ImageSubmissionViewModel> Images { get; }
private ImageSubmissionViewModel CreateImageSubmissionViewModel(Func<Stream, ImageSubmissionViewModel> 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);
}
}
}

View File

@ -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">
<Grid RowDefinitions="Auto,*">
<Grid RowDefinitions="Auto,Auto,*">
<StackPanel>
<StackPanel.Styles>
<Styles>
@ -24,7 +26,25 @@
</TextBlock>
</StackPanel>
<ScrollViewer Grid.Row="1"
<StackPanel Grid.Row="1"
Grid.Column="0"
IsVisible="{CompiledBinding IsKeyboardLayout}"
Margin="0 20 0 0">
<Label>Physical layout</Label>
<shared:EnumComboBox Value="{CompiledBinding PhysicalLayout}"></shared:EnumComboBox>
</StackPanel>
<controls:HyperlinkButton Grid.Row="1"
Grid.Column="0"
IsVisible="{CompiledBinding IsKeyboardLayout}"
Margin="0 10 0 0"
VerticalAlignment="Top"
HorizontalAlignment="Right"
NavigateUri="https://wiki.artemis-rgb.com/en/guides/developer/layouts/keyboard-layouts?mtm_campaign=artemis&amp;mtm_kwd=workshop-wizard">
Learn about physical layouts
</controls:HyperlinkButton>
<ScrollViewer Grid.Row="2"
Grid.Column="0"
Margin="0 10 0 0"
Classes="with-padding"
@ -34,7 +54,10 @@
<ItemsRepeater ItemsSource="{CompiledBinding LayoutInfo}">
<ItemsRepeater.ItemTemplate>
<DataTemplate>
<ContentControl Content="{CompiledBinding}" />
<StackPanel>
<Border Classes="card-separator" />
<ContentControl Content="{CompiledBinding}" />
</StackPanel>
</DataTemplate>
</ItemsRepeater.ItemTemplate>
</ItemsRepeater>

View File

@ -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<ArtemisLayout, LayoutInfoViewModel> _getLayoutInfoViewModel;
private ArtemisLayout? _layout;
[Notify(Setter.Private)] private bool _isKeyboardLayout;
[Notify] private ObservableCollection<LayoutInfoViewModel> _layoutInfo = new();
[Notify] private KeyboardLayoutType _physicalLayout;
public LayoutInfoStepViewModel(Func<ArtemisLayout, LayoutInfoViewModel> getLayoutInfoViewModel)
{
_getLayoutInfoViewModel = getLayoutInfoViewModel;
GoBack = ReactiveCommand.Create(() => State.ChangeScreen<LayoutSelectionStepViewModel>());
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<LayoutInfoViewModel> 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<long> {8}; // Device category, yes this could change but why would it
if (State.EntryId == null)
State.ChangeScreen<SpecificationsStepViewModel>();
else
State.ChangeScreen<UploadStepViewModel>();
}
}

View File

@ -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<EntryTypeStepViewModel>());
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<long> {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<LayoutInfoStepViewModel>();
}
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);

View File

@ -57,7 +57,7 @@ public partial class SpecificationsStepViewModel : SubmissionViewModel
return;
ApplyToState();
State.ChangeScreen<SubmitStepViewModel>();
State.ChangeScreen<ImagesStepViewModel>();
}
private void ApplyFromState()

View File

@ -8,9 +8,11 @@ namespace Artemis.UI.Screens.Workshop.SubmissionWizard;
public abstract partial class SubmissionViewModel : ValidatableViewModelBase
{
[Notify] private ReactiveCommand<Unit, Unit>? _secondary;
[Notify] private ReactiveCommand<Unit, Unit>? _continue;
[Notify] private ReactiveCommand<Unit, Unit>? _goBack;
[Notify] private string _continueText = "Continue";
[Notify] private string? _secondaryText;
[Notify] private bool _showFinish;
[Notify] private bool _showGoBack = true;
[Notify] private bool _showHeader = true;

View File

@ -16,9 +16,10 @@
WindowStartupLocation="CenterOwner">
<Grid Margin="15" RowDefinitions="Auto,*,Auto">
<Grid RowDefinitions="*,*" ColumnDefinitions="Auto,*,Auto" Margin="0 0 0 15">
<ContentControl Grid.Column="0" Grid.RowSpan="2" Width="65" Height="65" VerticalAlignment="Center" Margin="0 0 20 0" Content="{CompiledBinding CurrentUserViewModel}"/>
<TextBlock Grid.Row="0" Grid.Column="1" FontSize="36" VerticalAlignment="Bottom" Text="{CompiledBinding CurrentUserViewModel.Name}" IsVisible="{CompiledBinding !CurrentUserViewModel.IsAnonymous}"/>
<TextBlock Grid.Row="0" Grid.Column="1" FontSize="36" VerticalAlignment="Bottom" Text="Not logged in" IsVisible="{CompiledBinding CurrentUserViewModel.IsAnonymous}"/>
<ContentControl Grid.Column="0" Grid.RowSpan="2" Width="65" Height="65" VerticalAlignment="Center" Margin="0 0 20 0" Content="{CompiledBinding CurrentUserViewModel}" />
<TextBlock Grid.Row="0" Grid.Column="1" FontSize="36" VerticalAlignment="Bottom" Text="{CompiledBinding CurrentUserViewModel.Name}"
IsVisible="{CompiledBinding !CurrentUserViewModel.IsAnonymous}" />
<TextBlock Grid.Row="0" Grid.Column="1" FontSize="36" VerticalAlignment="Bottom" Text="Not logged in" IsVisible="{CompiledBinding CurrentUserViewModel.IsAnonymous}" />
<StackPanel Grid.Row="0" Grid.Column="2" HorizontalAlignment="Right" VerticalAlignment="Bottom" Orientation="Horizontal">
<controls:HyperlinkButton Classes="icon-button" ToolTip.Tip="View Wiki" NavigateUri="https://wiki.artemis-rgb.com?mtm_campaign=artemis&amp;mtm_kwd=workshop-wizard">
@ -36,16 +37,23 @@
<Border Classes="card" Grid.Row="1" Grid.Column="0">
<controls:Frame Name="Frame" IsNavigationStackEnabled="False" CacheSize="0">
<controls:Frame.NavigationPageFactory>
<ui:PageFactory/>
<ui:PageFactory />
</controls:Frame.NavigationPageFactory>
</controls:Frame>
</Border>
<Button Grid.Row="2"
Grid.Column="0"
IsVisible="{CompiledBinding Screen.Secondary, Converter={x:Static ObjectConverters.IsNotNull}}"
Margin="0 15 0 0"
Content="{CompiledBinding Screen.SecondaryText}"
Command="{CompiledBinding Screen.Secondary}"/>
<StackPanel Grid.Row="2" Grid.Column="0" HorizontalAlignment="Right" Orientation="Horizontal" Spacing="5" Margin="0 15 0 0">
<Button Command="{CompiledBinding Screen.GoBack}" IsVisible="{CompiledBinding Screen.ShowGoBack}">
Back
</Button>
<Button Command="{CompiledBinding Screen.Continue}" IsVisible="{CompiledBinding !Screen.ShowFinish}" Width="80" Content="{CompiledBinding Screen.ContinueText}"/>
<Button Command="{CompiledBinding Screen.Continue}" IsVisible="{CompiledBinding !Screen.ShowFinish}" Width="80" Content="{CompiledBinding Screen.ContinueText}" />
<Button Command="{CompiledBinding Screen.Continue}" IsVisible="{CompiledBinding Screen.ShowFinish}" Width="80">
Finish
</Button>

View File

@ -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; }