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

Merge branch 'development'

This commit is contained in:
Robert 2024-05-03 21:08:48 +02:00
commit d9a3a238af
39 changed files with 601 additions and 156 deletions

View File

@ -385,31 +385,5 @@ public class DataModelPath : IStorageModel, IDisposable, IPluginFeatureDependent
Entity.Type = pathType.FullName; Entity.Type = pathType.FullName;
} }
#region Equality members
/// <inheritdoc cref="Equals(object)" />
/// >
protected bool Equals(DataModelPath other)
{
return ReferenceEquals(Target, other.Target) && Path == other.Path;
}
/// <inheritdoc />
public override bool Equals(object? obj)
{
if (ReferenceEquals(null, obj)) return false;
if (ReferenceEquals(this, obj)) return true;
if (obj.GetType() != GetType()) return false;
return Equals((DataModelPath) obj);
}
/// <inheritdoc />
public override int GetHashCode()
{
return HashCode.Combine(Target, Path);
}
#endregion
#endregion #endregion
} }

View File

@ -1,4 +1,5 @@
using System; using System;
using System.Collections.Generic;
using System.IO; using System.IO;
using System.Linq; using System.Linq;
using RGB.NET.Layout; using RGB.NET.Layout;
@ -47,6 +48,13 @@ public class ArtemisLedLayout
/// </summary> /// </summary>
public LayoutCustomLedData LayoutCustomLedData { get; } public LayoutCustomLedData LayoutCustomLedData { get; }
/// <summary>
/// Gets the logical layout names available for this LED
/// </summary>
public IEnumerable<string> GetLogicalLayoutNames()
{
return LayoutCustomLedData.LogicalLayouts?.Where(l => l.Name != null).Select(l => l.Name!) ?? [];
}
internal void ApplyCustomLedData(ArtemisDevice artemisDevice) internal void ApplyCustomLedData(ArtemisDevice artemisDevice)
{ {

View File

@ -24,7 +24,6 @@ public class Profiler
/// </summary> /// </summary>
public string Name { get; } public string Name { get; }
/// <summary> /// <summary>
/// Gets a dictionary containing measurements by their identifiers /// Gets a dictionary containing measurements by their identifiers
/// </summary> /// </summary>

View File

@ -21,6 +21,7 @@
<PackageReference Include="Avalonia.Desktop" /> <PackageReference Include="Avalonia.Desktop" />
<PackageReference Include="Avalonia.Skia.Lottie" /> <PackageReference Include="Avalonia.Skia.Lottie" />
<PackageReference Include="AvaloniaEdit.TextMate" /> <PackageReference Include="AvaloniaEdit.TextMate" />
<PackageReference Include="FluentAvalonia.ProgressRing" />
<PackageReference Include="Markdown.Avalonia.Tight" /> <PackageReference Include="Markdown.Avalonia.Tight" />
<PackageReference Include="Octopus.Octodiff" /> <PackageReference Include="Octopus.Octodiff" />
<PackageReference Include="PropertyChanged.SourceGenerator"> <PackageReference Include="PropertyChanged.SourceGenerator">

View File

@ -57,7 +57,7 @@ public partial class PlaybackViewModel : ActivatableViewModelBase
_keyBindingsEnabled = Shared.UI.CurrentKeyBindingsEnabled.ToProperty(this, vm => vm.KeyBindingsEnabled).DisposeWith(d); _keyBindingsEnabled = Shared.UI.CurrentKeyBindingsEnabled.ToProperty(this, vm => vm.KeyBindingsEnabled).DisposeWith(d);
// Update timer // Update timer
Timer updateTimer = new(TimeSpan.FromMilliseconds(60.0 / 1000)); Timer updateTimer = new(TimeSpan.FromMilliseconds(16));
updateTimer.Elapsed += (_, _) => Update(); updateTimer.Elapsed += (_, _) => Update();
updateTimer.DisposeWith(d); updateTimer.DisposeWith(d);
_profileEditorService.Playing.Subscribe(_ => _lastUpdate = DateTime.Now).DisposeWith(d); _profileEditorService.Playing.Subscribe(_ => _lastUpdate = DateTime.Now).DisposeWith(d);

View File

@ -51,7 +51,7 @@ public class DataBindingViewModel : ActivatableViewModelBase
.DisposeWith(d); .DisposeWith(d);
_profileEditorService.Playing.CombineLatest(_profileEditorService.SuspendedEditing).Subscribe(tuple => _playing = tuple.First || tuple.Second).DisposeWith(d); _profileEditorService.Playing.CombineLatest(_profileEditorService.SuspendedEditing).Subscribe(tuple => _playing = tuple.First || tuple.Second).DisposeWith(d);
Timer updateTimer = new(TimeSpan.FromMilliseconds(25.0 / 1000)); Timer updateTimer = new(TimeSpan.FromMilliseconds(25));
Timer saveTimer = new(TimeSpan.FromMinutes(2)); Timer saveTimer = new(TimeSpan.FromMinutes(2));
updateTimer.Elapsed += (_, _) => Update(); updateTimer.Elapsed += (_, _) => Update();

View File

@ -29,10 +29,12 @@ public partial class StartupWizardView : ReactiveAppWindow<StartupWizardViewMode
else if (step == 2) else if (step == 2)
Frame.NavigateToType(typeof(DevicesStep), null, new FrameNavigationOptions()); Frame.NavigateToType(typeof(DevicesStep), null, new FrameNavigationOptions());
else if (step == 3) else if (step == 3)
Frame.NavigateToType(typeof(LayoutStep), null, new FrameNavigationOptions()); Frame.NavigateToType(typeof(LayoutsStep), null, new FrameNavigationOptions());
else if (step == 4) else if (step == 4)
Frame.NavigateToType(typeof(SettingsStep), null, new FrameNavigationOptions()); Frame.NavigateToType(typeof(SurfaceStep), null, new FrameNavigationOptions());
else if (step == 5) else if (step == 5)
Frame.NavigateToType(typeof(SettingsStep), null, new FrameNavigationOptions());
else if (step == 6)
Frame.NavigateToType(typeof(FinishStep), null, new FrameNavigationOptions()); Frame.NavigateToType(typeof(FinishStep), null, new FrameNavigationOptions());
} }
} }

View File

@ -9,6 +9,7 @@ using Artemis.Core.DeviceProviders;
using Artemis.Core.Services; using Artemis.Core.Services;
using Artemis.UI.DryIoc.Factories; using Artemis.UI.DryIoc.Factories;
using Artemis.UI.Screens.Plugins; using Artemis.UI.Screens.Plugins;
using Artemis.UI.Screens.Workshop.LayoutFinder;
using Artemis.UI.Shared; using Artemis.UI.Shared;
using Artemis.UI.Shared.Providers; using Artemis.UI.Shared.Providers;
using Artemis.UI.Shared.Services; using Artemis.UI.Shared.Services;
@ -35,7 +36,8 @@ public partial class StartupWizardViewModel : DialogViewModelBase<bool>
IPluginManagementService pluginManagementService, IPluginManagementService pluginManagementService,
IWindowService windowService, IWindowService windowService,
IDeviceService deviceService, IDeviceService deviceService,
ISettingsVmFactory settingsVmFactory) ISettingsVmFactory settingsVmFactory,
LayoutFinderViewModel layoutFinderViewModel)
{ {
_settingsService = settingsService; _settingsService = settingsService;
_windowService = windowService; _windowService = windowService;
@ -54,6 +56,7 @@ public partial class StartupWizardViewModel : DialogViewModelBase<bool>
.Where(p => p.Info.IsCompatible && p.Features.Any(f => f.AlwaysEnabled && f.FeatureType.IsAssignableTo(typeof(DeviceProvider)))) .Where(p => p.Info.IsCompatible && p.Features.Any(f => f.AlwaysEnabled && f.FeatureType.IsAssignableTo(typeof(DeviceProvider))))
.OrderBy(p => p.Info.Name) .OrderBy(p => p.Info.Name)
.Select(p => settingsVmFactory.PluginViewModel(p, ReactiveCommand.Create(() => new Unit())))); .Select(p => settingsVmFactory.PluginViewModel(p, ReactiveCommand.Create(() => new Unit()))));
LayoutFinderViewModel = layoutFinderViewModel;
CurrentStep = 1; CurrentStep = 1;
SetupButtons(); SetupButtons();
@ -82,6 +85,7 @@ public partial class StartupWizardViewModel : DialogViewModelBase<bool>
public string Version { get; } public string Version { get; }
public ObservableCollection<PluginViewModel> DeviceProviders { get; } public ObservableCollection<PluginViewModel> DeviceProviders { get; }
public LayoutFinderViewModel LayoutFinderViewModel { get; }
public bool IsAutoRunSupported => _autoRunProvider != null; public bool IsAutoRunSupported => _autoRunProvider != null;
@ -98,7 +102,7 @@ public partial class StartupWizardViewModel : DialogViewModelBase<bool>
CurrentStep--; CurrentStep--;
// Skip the settings step if none of it's contents are supported // Skip the settings step if none of it's contents are supported
if (CurrentStep == 4 && !IsAutoRunSupported) if (CurrentStep == 5 && !IsAutoRunSupported)
CurrentStep--; CurrentStep--;
SetupButtons(); SetupButtons();
@ -106,11 +110,11 @@ public partial class StartupWizardViewModel : DialogViewModelBase<bool>
private void ExecuteContinue() private void ExecuteContinue()
{ {
if (CurrentStep < 5) if (CurrentStep < 6)
CurrentStep++; CurrentStep++;
// Skip the settings step if none of it's contents are supported // Skip the settings step if none of it's contents are supported
if (CurrentStep == 4 && !IsAutoRunSupported) if (CurrentStep == 5 && !IsAutoRunSupported)
CurrentStep++; CurrentStep++;
SetupButtons(); SetupButtons();
@ -118,9 +122,9 @@ public partial class StartupWizardViewModel : DialogViewModelBase<bool>
private void SetupButtons() private void SetupButtons()
{ {
ShowContinue = CurrentStep != 3 && CurrentStep < 5; ShowContinue = CurrentStep != 4 && CurrentStep < 6;
ShowGoBack = CurrentStep > 1; ShowGoBack = CurrentStep > 1;
ShowFinish = CurrentStep == 5; ShowFinish = CurrentStep == 6;
} }
private void ExecuteSkipOrFinishWizard() private void ExecuteSkipOrFinishWizard()

View File

@ -0,0 +1,31 @@
<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:startupWizard="clr-namespace:Artemis.UI.Screens.StartupWizard"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Artemis.UI.Screens.StartupWizard.Steps.LayoutsStep"
x:DataType="startupWizard:StartupWizardViewModel">
<Border Classes="card">
<Grid RowDefinitions="Auto,Auto,Auto">
<StackPanel Grid.Row="0">
<TextBlock TextWrapping="Wrap">
Device layouts provide Artemis with an image of your devices and exact LED positions. <LineBreak />
While not strictly necessary, this helps to create effects that are perfectly aligned with your hardware.
</TextBlock>
<TextBlock TextWrapping="Wrap" Margin="0 10">
Below you can automatically search the Artemis Workshop for device layouts of your devices.
</TextBlock>
</StackPanel>
<Button Grid.Row="1"
Content="Auto-install layouts"
Command="{CompiledBinding LayoutFinderViewModel.SearchAll}"
ToolTip.Tip="Search layouts and if found install them automatically"
HorizontalAlignment="Right"/>
<ScrollViewer Grid.Row="2" Margin="0 15">
<ContentControl Content="{CompiledBinding LayoutFinderViewModel}"></ContentControl>
</ScrollViewer>
</Grid>
</Border>
</UserControl>

View File

@ -0,0 +1,13 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
namespace Artemis.UI.Screens.StartupWizard.Steps;
public partial class LayoutsStep : UserControl
{
public LayoutsStep()
{
InitializeComponent();
}
}

View File

@ -5,7 +5,7 @@
xmlns:startupWizard="clr-namespace:Artemis.UI.Screens.StartupWizard" xmlns:startupWizard="clr-namespace:Artemis.UI.Screens.StartupWizard"
xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia" xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Artemis.UI.Screens.StartupWizard.Steps.LayoutStep" x:Class="Artemis.UI.Screens.StartupWizard.Steps.SurfaceStep"
x:DataType="startupWizard:StartupWizardViewModel"> x:DataType="startupWizard:StartupWizardViewModel">
<Grid RowDefinitions="Auto,*,Auto,Auto" ColumnDefinitions="*,*"> <Grid RowDefinitions="Auto,*,Auto,Auto" ColumnDefinitions="*,*">

View File

@ -3,9 +3,9 @@ using Avalonia.Markup.Xaml;
namespace Artemis.UI.Screens.StartupWizard.Steps; namespace Artemis.UI.Screens.StartupWizard.Steps;
public partial class LayoutStep : UserControl public partial class SurfaceStep : UserControl
{ {
public LayoutStep() public SurfaceStep()
{ {
InitializeComponent(); InitializeComponent();
} }

View File

@ -66,7 +66,7 @@ public class NodeScriptWindowViewModel : NodeScriptWindowViewModelBase
{ {
_keyBindingsEnabled = Shared.UI.CurrentKeyBindingsEnabled.ToProperty(this, vm => vm.KeyBindingsEnabled).DisposeWith(d); _keyBindingsEnabled = Shared.UI.CurrentKeyBindingsEnabled.ToProperty(this, vm => vm.KeyBindingsEnabled).DisposeWith(d);
Timer updateTimer = new(TimeSpan.FromMilliseconds(25.0 / 1000)); Timer updateTimer = new(TimeSpan.FromMilliseconds(25));
Timer saveTimer = new(TimeSpan.FromMinutes(2)); Timer saveTimer = new(TimeSpan.FromMinutes(2));
updateTimer.Elapsed += (_, _) => Update(); updateTimer.Elapsed += (_, _) => Update();

View File

@ -42,15 +42,6 @@
</Ellipse> </Ellipse>
<TextBlock Grid.Column="1" Grid.Row="0" Text="{CompiledBinding Name}" Margin="0 4 0 0"></TextBlock> <TextBlock Grid.Column="1" Grid.Row="0" Text="{CompiledBinding Name}" Margin="0 4 0 0"></TextBlock>
<TextBlock Grid.Column="1" Grid.Row="1" Text="{CompiledBinding Email}"></TextBlock> <TextBlock Grid.Column="1" Grid.Row="1" Text="{CompiledBinding Email}"></TextBlock>
<controls:HyperlinkButton
IsVisible="{CompiledBinding AllowLogout}"
Grid.Column="1"
Grid.Row="2"
Margin="-8 4 0 0"
Padding="6 4"
Click="Signout_OnClick">
Sign out
</controls:HyperlinkButton>
<controls:HyperlinkButton <controls:HyperlinkButton
IsVisible="{CompiledBinding AllowLogout}" IsVisible="{CompiledBinding AllowLogout}"
@ -61,6 +52,15 @@
Click="Manage_OnClick"> Click="Manage_OnClick">
Manage account Manage account
</controls:HyperlinkButton> </controls:HyperlinkButton>
<controls:HyperlinkButton
IsVisible="{CompiledBinding AllowLogout}"
Grid.Column="1"
Grid.Row="2"
Margin="-8 4 0 0"
Padding="6 4"
Click="Signout_OnClick">
Sign out
</controls:HyperlinkButton>
</Grid> </Grid>
</Flyout> </Flyout>
</Ellipse.ContextFlyout> </Ellipse.ContextFlyout>

View File

@ -15,8 +15,8 @@
</Styles> </Styles>
</UserControl.Styles> </UserControl.Styles>
<Grid ColumnDefinitions="300,*" RowDefinitions="Auto,*,Auto"> <Grid ColumnDefinitions="Auto,*" RowDefinitions="Auto,*,Auto">
<StackPanel Grid.Column="0" Grid.RowSpan="3" Margin="0 0 10 0" VerticalAlignment="Top"> <StackPanel Grid.Column="0" Grid.RowSpan="3" Margin="0 0 10 0" VerticalAlignment="Top" Width="300" IsVisible="{CompiledBinding ShowCategoryFilter}">
<Border Classes="card" VerticalAlignment="Stretch"> <Border Classes="card" VerticalAlignment="Stretch">
<StackPanel> <StackPanel>
<TextBlock Theme="{StaticResource SubtitleTextBlockStyle}">Categories</TextBlock> <TextBlock Theme="{StaticResource SubtitleTextBlockStyle}">Categories</TextBlock>

View File

@ -54,6 +54,7 @@ public partial class EntryListViewModel : RoutableScreen
this.WhenActivated(d => this.WhenActivated(d =>
{ {
InputViewModel.WhenAnyValue(vm => vm.Search).Skip(1).Throttle(TimeSpan.FromMilliseconds(200)).Subscribe(_ => Reset()).DisposeWith(d); InputViewModel.WhenAnyValue(vm => vm.Search).Skip(1).Throttle(TimeSpan.FromMilliseconds(200)).Subscribe(_ => Reset()).DisposeWith(d);
InputViewModel.WhenAnyValue(vm => vm.SortBy).Skip(1).Throttle(TimeSpan.FromMilliseconds(200)).Subscribe(_ => Reset()).DisposeWith(d);
CategoriesViewModel.WhenAnyValue(vm => vm.CategoryFilters).Skip(1).Subscribe(_ => Reset()).DisposeWith(d); CategoriesViewModel.WhenAnyValue(vm => vm.CategoryFilters).Skip(1).Subscribe(_ => Reset()).DisposeWith(d);
}); });
@ -71,6 +72,7 @@ public partial class EntryListViewModel : RoutableScreen
public CategoriesViewModel CategoriesViewModel { get; } public CategoriesViewModel CategoriesViewModel { get; }
public EntryListInputViewModel InputViewModel { get; } public EntryListInputViewModel InputViewModel { get; }
public bool ShowCategoryFilter { get; set; } = true;
public EntryType? EntryType { get; set; } public EntryType? EntryType { get; set; }
public ReadOnlyObservableCollection<EntryListItemViewModel> Entries { get; } public ReadOnlyObservableCollection<EntryListItemViewModel> Entries { get; }

View File

@ -1,24 +0,0 @@
<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:surfaceEditor="clr-namespace:Artemis.UI.Screens.SurfaceEditor"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Artemis.UI.Screens.Workshop.Layout.Dialogs.DeviceSelectionDialogView"
x:DataType="dialogs:DeviceSelectionDialogViewModel">
<StackPanel>
<TextBlock>
Select the devices on which you would like to apply the downloaded layout.
</TextBlock>
<ItemsControl Name="EffectDescriptorsList" ItemsSource="{CompiledBinding Devices}" Margin="0 10 0 0">
<ItemsControl.DataTemplates>
<DataTemplate DataType="{x:Type surfaceEditor:ListDeviceViewModel}">
<CheckBox IsChecked="{CompiledBinding IsSelected}">
<TextBlock Text="{CompiledBinding Device.RgbDevice.DeviceInfo.DeviceName}"></TextBlock>
</CheckBox>
</DataTemplate>
</ItemsControl.DataTemplates>
</ItemsControl>
</StackPanel>
</UserControl>

View File

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

View File

@ -1,44 +0,0 @@
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Reactive;
using Artemis.Core;
using Artemis.Core.Services;
using Artemis.UI.DryIoc.Factories;
using Artemis.UI.Screens.SurfaceEditor;
using Artemis.UI.Shared;
using Artemis.WebClient.Workshop.Models;
using Artemis.WebClient.Workshop.Providers;
using Artemis.WebClient.Workshop.Services;
using ReactiveUI;
namespace Artemis.UI.Screens.Workshop.Layout.Dialogs;
public class DeviceSelectionDialogViewModel : ContentDialogViewModelBase
{
private readonly IDeviceService _deviceService;
private readonly WorkshopLayoutProvider _layoutProvider;
public DeviceSelectionDialogViewModel(List<ArtemisDevice> devices, InstalledEntry entry, ISurfaceVmFactory surfaceVmFactory, IDeviceService deviceService, WorkshopLayoutProvider layoutProvider)
{
_deviceService = deviceService;
_layoutProvider = layoutProvider;
Entry = entry;
Devices = new ObservableCollection<ListDeviceViewModel>(devices.Select(surfaceVmFactory.ListDeviceViewModel));
Apply = ReactiveCommand.Create(ExecuteApply);
}
public InstalledEntry Entry { get; }
public ObservableCollection<ListDeviceViewModel> Devices { get; }
public ReactiveCommand<Unit, Unit> Apply { get; }
private void ExecuteApply()
{
foreach (ListDeviceViewModel listDeviceViewModel in Devices.Where(d => d.IsSelected))
{
_layoutProvider.ConfigureDevice(listDeviceViewModel.Device, Entry);
_deviceService.SaveDevice(listDeviceViewModel.Device);
_deviceService.LoadDeviceLayout(listDeviceViewModel.Device);
}
}
}

View File

@ -31,13 +31,14 @@ public partial class LayoutInfoViewModel : ValidatableViewModelBase
IWindowService windowService, IWindowService windowService,
IPluginManagementService pluginManagementService) IPluginManagementService pluginManagementService)
{ {
_windowService = windowService; ArtemisDevice? device = deviceService.Devices.FirstOrDefault(d => d.Layout == layout);
_vendor = layout.RgbLayout.Vendor;
_model = layout.RgbLayout.Model;
DeviceProvider? deviceProvider = deviceService.Devices.FirstOrDefault(d => d.Layout == layout)?.DeviceProvider; _windowService = windowService;
if (deviceProvider != null) _vendor = device?.RgbDevice.DeviceInfo.Manufacturer ?? layout.RgbLayout.Vendor;
_deviceProviderId = deviceProvider.Plugin.Guid; _model = device?.RgbDevice.DeviceInfo.Model ?? layout.RgbLayout.Model;
if (device != null)
_deviceProviderId = device.DeviceProvider.Plugin.Guid;
_deviceProviders = this.WhenAnyValue(vm => vm.DeviceProviderId) _deviceProviders = this.WhenAnyValue(vm => vm.DeviceProviderId)
.Select(id => pluginManagementService.GetAllPlugins().FirstOrDefault(p => p.Guid == id)?.Features.Select(f => f.Name)) .Select(id => pluginManagementService.GetAllPlugins().FirstOrDefault(p => p.Guid == id)?.Features.Select(f => f.Name))

View File

@ -0,0 +1,25 @@
<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:layout="clr-namespace:Artemis.UI.Screens.Workshop.Layout"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Artemis.UI.Screens.Workshop.Layout.LayoutListDefaultView"
x:DataType="layout:LayoutListDefaultViewModel">
<Grid ColumnDefinitions="400,*">
<Border Grid.Column="0" Classes="card" Margin="0 0 10 0" VerticalAlignment="Top">
<StackPanel>
<DockPanel>
<Button DockPanel.Dock="Right"
Content="Auto-install layouts"
Command="{CompiledBinding LayoutFinderViewModel.SearchAll}"
ToolTip.Tip="Search layouts and if found install them automatically"/>
<TextBlock Theme="{StaticResource SubtitleTextBlockStyle}">Detected devices</TextBlock>
</DockPanel>
<Border Classes="card-separator" />
<ContentControl Content="{CompiledBinding LayoutFinderViewModel}"></ContentControl>
</StackPanel>
</Border>
<ContentControl Grid.Column="1" Content="{CompiledBinding EntryListViewModel}"></ContentControl>
</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.Layout;
public partial class LayoutListDefaultView : ReactiveUserControl<LayoutListDefaultViewModel>
{
public LayoutListDefaultView()
{
InitializeComponent();
}
}

View File

@ -0,0 +1,20 @@
using Artemis.UI.Screens.Workshop.Entries.List;
using Artemis.UI.Screens.Workshop.LayoutFinder;
using Artemis.UI.Shared.Routing;
using Artemis.WebClient.Workshop;
namespace Artemis.UI.Screens.Workshop.Layout;
public class LayoutListDefaultViewModel : RoutableScreen
{
public LayoutListDefaultViewModel(LayoutFinderViewModel layoutFinderViewModel, EntryListViewModel entryListViewModel)
{
LayoutFinderViewModel = layoutFinderViewModel;
EntryListViewModel = entryListViewModel;
EntryListViewModel.EntryType = EntryType.Layout;
EntryListViewModel.ShowCategoryFilter = false;
}
public LayoutFinderViewModel LayoutFinderViewModel { get; }
public EntryListViewModel EntryListViewModel { get; }
}

View File

@ -7,11 +7,10 @@ namespace Artemis.UI.Screens.Workshop.Layout;
public class LayoutListViewModel : RoutableHostScreen<RoutableScreen> public class LayoutListViewModel : RoutableHostScreen<RoutableScreen>
{ {
private readonly EntryListViewModel _entryListViewModel; private readonly EntryListViewModel _entryListViewModel;
public override RoutableScreen DefaultScreen => _entryListViewModel; public override RoutableScreen DefaultScreen { get; }
public LayoutListViewModel(EntryListViewModel entryListViewModel) public LayoutListViewModel(LayoutListDefaultViewModel defaultViewModel)
{ {
_entryListViewModel = entryListViewModel; DefaultScreen = defaultViewModel;
_entryListViewModel.EntryType = EntryType.Layout;
} }
} }

View File

@ -0,0 +1,73 @@
<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:shared="clr-namespace:Artemis.UI.Shared;assembly=Artemis.UI.Shared"
xmlns:layoutFinder="clr-namespace:Artemis.UI.Screens.Workshop.LayoutFinder"
xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia.ProgressRing"
mc:Ignorable="d" d:DesignWidth="400" d:DesignHeight="55"
x:Class="Artemis.UI.Screens.Workshop.LayoutFinder.LayoutFinderDeviceView"
x:DataType="layoutFinder:LayoutFinderDeviceViewModel">
<Grid RowDefinitions="Auto,Auto" ColumnDefinitions="55,*,Auto">
<Border Grid.Column="0" Grid.RowSpan="2" Width="50" Height="50" Margin="5 0 10 5" IsVisible="{CompiledBinding HasLayout}" >
<shared:DeviceVisualizer Device="{CompiledBinding Device}"
VerticalAlignment="Center"
HorizontalAlignment="Center"
RenderOptions.BitmapInterpolationMode="MediumQuality"/>
</Border>
<avalonia:MaterialIcon Grid.Column="0"
Grid.Row="0"
Grid.RowSpan="2"
IsVisible="{CompiledBinding !HasLayout}"
Kind="{CompiledBinding DeviceIcon}"
Width="50"
Height="40"
Margin="5 5 10 10"
HorizontalAlignment="Center"
VerticalAlignment="Center"/>
<TextBlock Grid.Column="1" Grid.Row="0" Text="{CompiledBinding Device.RgbDevice.DeviceInfo.Model}" VerticalAlignment="Bottom" />
<TextBlock Grid.Column="1" Grid.Row="1" Classes="subtitle" VerticalAlignment="Top">
<Run Text="{CompiledBinding Device.RgbDevice.DeviceInfo.Manufacturer}"></Run>
<Run>-</Run>
<Run Text="{CompiledBinding Device.RgbDevice.DeviceInfo.DeviceType}"></Run>
</TextBlock>
<Panel Grid.Column="2" Grid.Row="0" Grid.RowSpan="2" IsVisible="{CompiledBinding !HasLayout}">
<controls:ProgressRing IsVisible="{CompiledBinding Searching}"
IsIndeterminate="True"
BorderThickness="2"
Width="25"
Height="25"
PreserveAspect="False"
Margin="0 0 0 0"
VerticalAlignment="Center"/>
<Panel IsVisible="{CompiledBinding !Searching}">
<avalonia:MaterialIcon IsVisible="{CompiledBinding Entry, Converter={x:Static ObjectConverters.IsNull}}"
Width="28"
Height="28"
Kind="MultiplyCircle"
Foreground="#D64848"
ToolTip.Tip="No layout found"/>
<avalonia:MaterialIcon IsVisible="{CompiledBinding Entry, Converter={x:Static ObjectConverters.IsNotNull}}"
Width="28"
Height="28"
Kind="CloudCheck"
Foreground="{DynamicResource SystemAccentColorLight1}"
ToolTip.Tip="Layout found and installed"/>
</Panel>
</Panel>
<avalonia:MaterialIcon Grid.Column="2"
Grid.Row="0"
Grid.RowSpan="2"
IsVisible="{CompiledBinding HasLayout}"
Width="28"
Height="28"
Kind="CheckCircle"
Foreground="{DynamicResource SystemAccentColorLight1}"
ToolTip.Tip="Using existing layout"/>
</Grid>
</UserControl>

View File

@ -0,0 +1,13 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
namespace Artemis.UI.Screens.Workshop.LayoutFinder;
public partial class LayoutFinderDeviceView : UserControl
{
public LayoutFinderDeviceView()
{
InitializeComponent();
}
}

View File

@ -0,0 +1,159 @@
using System;
using System.Threading;
using System.Threading.Tasks;
using Artemis.Core;
using Artemis.Core.Services;
using Artemis.UI.Shared;
using Artemis.UI.Shared.Utilities;
using Artemis.WebClient.Workshop;
using Artemis.WebClient.Workshop.Handlers.InstallationHandlers;
using Artemis.WebClient.Workshop.Models;
using Artemis.WebClient.Workshop.Providers;
using Artemis.WebClient.Workshop.Services;
using Material.Icons;
using Material.Icons.Avalonia;
using PropertyChanged.SourceGenerator;
using StrawberryShake;
namespace Artemis.UI.Screens.Workshop.LayoutFinder;
public partial class LayoutFinderDeviceViewModel : ViewModelBase
{
private readonly IWorkshopClient _client;
private readonly IDeviceService _deviceService;
private readonly IWorkshopService _workshopService;
private readonly WorkshopLayoutProvider _layoutProvider;
private readonly EntryInstallationHandlerFactory _factory;
[Notify] private bool _searching;
[Notify] private bool _hasLayout;
[Notify] private IEntrySummary? _entry;
[Notify] private IRelease? _release;
[Notify] private string? _logicalLayout;
[Notify] private string? _physicalLayout;
public LayoutFinderDeviceViewModel(ArtemisDevice device,
IWorkshopClient client,
IDeviceService deviceService,
IWorkshopService workshopService,
WorkshopLayoutProvider layoutProvider,
EntryInstallationHandlerFactory factory)
{
_client = client;
_deviceService = deviceService;
_workshopService = workshopService;
_layoutProvider = layoutProvider;
_factory = factory;
Device = device;
DeviceIcon = DetermineDeviceIcon();
HasLayout = Device.Layout != null && !Device.Layout.IsDefaultLayout;
}
public ArtemisDevice Device { get; }
public MaterialIconKind DeviceIcon { get; }
public async Task Search()
{
if (HasLayout)
return;
try
{
Searching = true;
Task delayTask = Task.Delay(400);
if (Device.DeviceType == RGB.NET.Core.RGBDeviceType.Keyboard)
await SearchKeyboardLayout();
else
await SearchLayout();
if (Entry != null && Release != null)
await InstallAndApplyEntry(Entry, Release);
await delayTask;
}
finally
{
Searching = false;
HasLayout = Device.Layout != null && !Device.Layout.IsDefaultLayout;
}
}
private async Task SearchKeyboardLayout()
{
IOperationResult<ISearchKeyboardLayoutResult> result = await _client.SearchKeyboardLayout.ExecuteAsync(
Device.DeviceProvider.Plugin.Guid,
Device.RgbDevice.DeviceInfo.Model,
Device.RgbDevice.DeviceInfo.Manufacturer,
Device.LogicalLayout,
Enum.Parse<Artemis.WebClient.Workshop.KeyboardLayoutType>(Device.PhysicalLayout.ToString(), true));
Entry = result.Data?.SearchKeyboardLayout?.Entry;
Release = result.Data?.SearchKeyboardLayout?.Entry.LatestRelease;
LogicalLayout = result.Data?.SearchKeyboardLayout?.LogicalLayout;
PhysicalLayout = result.Data?.SearchKeyboardLayout?.PhysicalLayout.ToString();
}
private async Task SearchLayout()
{
IOperationResult<ISearchLayoutResult> result = await _client.SearchLayout.ExecuteAsync(
Enum.Parse<RGBDeviceType>(Device.DeviceType.ToString(), true),
Device.DeviceProvider.Plugin.Guid,
Device.RgbDevice.DeviceInfo.Model,
Device.RgbDevice.DeviceInfo.Manufacturer);
Entry = result.Data?.SearchLayout?.Entry;
Release = result.Data?.SearchLayout?.Entry.LatestRelease;
LogicalLayout = null;
PhysicalLayout = null;
}
private async Task InstallAndApplyEntry(IEntrySummary entry, IRelease release)
{
// Try a local install first
InstalledEntry? installedEntry = _workshopService.GetInstalledEntry(entry.Id);
if (installedEntry == null)
{
IEntryInstallationHandler installationHandler = _factory.CreateHandler(EntryType.Layout);
EntryInstallResult result = await installationHandler.InstallAsync(entry, release, new Progress<StreamProgress>(), CancellationToken.None);
installedEntry = result.Entry;
}
if (installedEntry != null)
{
_layoutProvider.ConfigureDevice(Device, installedEntry);
_deviceService.SaveDevice(Device);
_deviceService.LoadDeviceLayout(Device);
}
}
private MaterialIconKind DetermineDeviceIcon()
{
return Device.DeviceType switch
{
RGB.NET.Core.RGBDeviceType.None => MaterialIconKind.QuestionMarkCircle,
RGB.NET.Core.RGBDeviceType.Keyboard => MaterialIconKind.Keyboard,
RGB.NET.Core.RGBDeviceType.Mouse => MaterialIconKind.Mouse,
RGB.NET.Core.RGBDeviceType.Headset => MaterialIconKind.Headset,
RGB.NET.Core.RGBDeviceType.Mousepad => MaterialIconKind.TextureBox,
RGB.NET.Core.RGBDeviceType.LedStripe => MaterialIconKind.LightStrip,
RGB.NET.Core.RGBDeviceType.LedMatrix => MaterialIconKind.DrawingBox,
RGB.NET.Core.RGBDeviceType.Mainboard => MaterialIconKind.Chip,
RGB.NET.Core.RGBDeviceType.GraphicsCard => MaterialIconKind.GraphicsProcessingUnit,
RGB.NET.Core.RGBDeviceType.DRAM => MaterialIconKind.Memory,
RGB.NET.Core.RGBDeviceType.HeadsetStand => MaterialIconKind.HeadsetDock,
RGB.NET.Core.RGBDeviceType.Keypad => MaterialIconKind.Keypad,
RGB.NET.Core.RGBDeviceType.Fan => MaterialIconKind.Fan,
RGB.NET.Core.RGBDeviceType.Speaker => MaterialIconKind.Speaker,
RGB.NET.Core.RGBDeviceType.Cooler => MaterialIconKind.FreezingPoint,
RGB.NET.Core.RGBDeviceType.Monitor => MaterialIconKind.DesktopWindows,
RGB.NET.Core.RGBDeviceType.LedController => MaterialIconKind.LedStripVariant,
RGB.NET.Core.RGBDeviceType.GameController => MaterialIconKind.MicrosoftXboxController,
RGB.NET.Core.RGBDeviceType.Unknown => MaterialIconKind.QuestionMarkCircle,
RGB.NET.Core.RGBDeviceType.All => MaterialIconKind.QuestionMarkCircle,
_ => MaterialIconKind.QuestionMarkCircle
};
}
}

View File

@ -0,0 +1,27 @@
<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:layoutFinder="clr-namespace:Artemis.UI.Screens.Workshop.LayoutFinder"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Artemis.UI.Screens.Workshop.LayoutFinder.LayoutFinderView"
x:DataType="layoutFinder:LayoutFinderViewModel">
<ItemsControl ItemsSource="{CompiledBinding DeviceViewModels}">
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="layoutFinder:LayoutFinderDeviceViewModel">
<StackPanel Classes="device-view-model-container">
<!-- Your existing item template goes here -->
<ContentControl Content="{CompiledBinding}"/>
<Border Classes="card-separator" Name="Separator" />
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
<ItemsControl.Styles>
<Styles>
<Style Selector="ContentPresenter:nth-last-child(1) Border#Separator">
<Setter Property="IsVisible" Value="False" />
</Style>
</Styles>
</ItemsControl.Styles>
</ItemsControl>
</UserControl>

View File

@ -0,0 +1,14 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using Avalonia.ReactiveUI;
namespace Artemis.UI.Screens.Workshop.LayoutFinder;
public partial class LayoutFinderView : ReactiveUserControl<LayoutFinderViewModel>
{
public LayoutFinderView()
{
InitializeComponent();
}
}

View File

@ -0,0 +1,53 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Reactive;
using System.Reactive.Disposables;
using System.Threading.Tasks;
using Artemis.Core;
using Artemis.Core.Services;
using Artemis.UI.Shared;
using DynamicData;
using PropertyChanged.SourceGenerator;
using ReactiveUI;
using RGB.NET.Core;
using Serilog;
namespace Artemis.UI.Screens.Workshop.LayoutFinder;
public partial class LayoutFinderViewModel : ActivatableViewModelBase
{
private readonly ILogger _logger;
private readonly SourceList<IRGBDeviceInfo> _devices;
[Notify] private ReadOnlyObservableCollection<LayoutFinderDeviceViewModel> _deviceViewModels;
public LayoutFinderViewModel(ILogger logger, IDeviceService deviceService, Func<ArtemisDevice, LayoutFinderDeviceViewModel> getDeviceViewModel)
{
_logger = logger;
SearchAll = ReactiveCommand.CreateFromTask(ExecuteSearchAll);
this.WhenActivated((CompositeDisposable _) =>
{
IEnumerable<LayoutFinderDeviceViewModel> deviceGroups = deviceService.EnabledDevices.Select(getDeviceViewModel);
DeviceViewModels = new ReadOnlyObservableCollection<LayoutFinderDeviceViewModel>(new ObservableCollection<LayoutFinderDeviceViewModel>(deviceGroups));
});
}
public ReactiveCommand<Unit, Unit> SearchAll { get; }
private async Task ExecuteSearchAll()
{
foreach (LayoutFinderDeviceViewModel deviceViewModel in DeviceViewModels)
{
try
{
await deviceViewModel.Search();
}
catch (Exception e)
{
_logger.Error(e, "Failed to search for layout on device {Device}", deviceViewModel.Device);
}
}
}
}

View File

@ -24,6 +24,11 @@
The information below is used by Artemis to automatically find your layout. The information below is used by Artemis to automatically find your layout.
Some layouts can be shared across different devices and here you have a chance to set that up. Some layouts can be shared across different devices and here you have a chance to set that up.
</TextBlock> </TextBlock>
<TextBlock TextWrapping="Wrap" Classes="warning" Margin="0 10 0 0">
<Run>Ensure you enter the model and vendor</Run>
<Run FontWeight="Bold">exactly</Run>
<Run>as detected on the device by Artemis.</Run>
</TextBlock>
</StackPanel> </StackPanel>
<StackPanel Grid.Row="1" <StackPanel Grid.Row="1"

View File

@ -7,12 +7,14 @@
xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"> xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia">
<!-- Third party styles --> <!-- Third party styles -->
<styling:FluentAvaloniaTheme PreferSystemTheme="False" PreferUserAccentColor="True"/> <styling:FluentAvaloniaTheme PreferSystemTheme="False" PreferUserAccentColor="True"/>
<avalonia:MaterialIconStyles /> <avalonia:MaterialIconStyles />
<!-- <FluentTheme Mode="Dark"></FluentTheme> --> <!-- <FluentTheme Mode="Dark"></FluentTheme> -->
<StyleInclude Source="avares://Artemis.UI.Shared/Styles/Artemis.axaml" /> <StyleInclude Source="avares://Artemis.UI.Shared/Styles/Artemis.axaml" />
<StyleInclude Source="avares://AsyncImageLoader.Avalonia/AdvancedImage.axaml" /> <StyleInclude Source="avares://AsyncImageLoader.Avalonia/AdvancedImage.axaml" />
<StyleInclude Source="avares://AvaloniaEdit/Themes/Fluent/AvaloniaEdit.xaml" /> <StyleInclude Source="avares://AvaloniaEdit/Themes/Fluent/AvaloniaEdit.xaml" />
<StyleInclude Source="avares://FluentAvalonia.ProgressRing/Styling/Controls/ProgressRing.axaml" />
<!-- Adjust the ScrollViewer padding in AvaloniaEdit so scrollbar doesn't overlap text --> <!-- Adjust the ScrollViewer padding in AvaloniaEdit so scrollbar doesn't overlap text -->
<Style Selector="aedit|TextEditor /template/ ScrollViewer ScrollContentPresenter"> <Style Selector="aedit|TextEditor /template/ ScrollViewer ScrollContentPresenter">

View File

@ -1,6 +1,5 @@
using Artemis.UI.Shared.Utilities; using Artemis.UI.Shared.Utilities;
using Artemis.WebClient.Workshop.Models; using Artemis.WebClient.Workshop.Models;
using Artemis.WebClient.Workshop.Services;
namespace Artemis.WebClient.Workshop.Handlers.InstallationHandlers; namespace Artemis.WebClient.Workshop.Handlers.InstallationHandlers;

View File

@ -52,7 +52,7 @@ public class LayoutEntryInstallationHandler : IEntryInstallationHandler
// Extract the archive, we could go through the hoops of keeping track of progress but this should be so quick it doesn't matter // Extract the archive, we could go through the hoops of keeping track of progress but this should be so quick it doesn't matter
stream.Seek(0, SeekOrigin.Begin); stream.Seek(0, SeekOrigin.Begin);
using ZipArchive archive = new(stream); using ZipArchive archive = new(stream);
archive.ExtractToDirectory(releaseDirectory.FullName); archive.ExtractToDirectory(releaseDirectory.FullName, true);
ArtemisLayout layout = new(Path.Combine(releaseDirectory.FullName, "layout.xml")); ArtemisLayout layout = new(Path.Combine(releaseDirectory.FullName, "layout.xml"));
if (layout.IsValid) if (layout.IsValid)

View File

@ -11,10 +11,12 @@ namespace Artemis.WebClient.Workshop.Handlers.UploadHandlers;
public class LayoutEntryUploadHandler : IEntryUploadHandler public class LayoutEntryUploadHandler : IEntryUploadHandler
{ {
private readonly IHttpClientFactory _httpClientFactory; private readonly IHttpClientFactory _httpClientFactory;
private readonly IWorkshopClient _workshopClient;
public LayoutEntryUploadHandler(IHttpClientFactory httpClientFactory) public LayoutEntryUploadHandler(IHttpClientFactory httpClientFactory, IWorkshopClient workshopClient)
{ {
_httpClientFactory = httpClientFactory; _httpClientFactory = httpClientFactory;
_workshopClient = workshopClient;
} }
/// <inheritdoc /> /// <inheritdoc />
@ -41,8 +43,10 @@ public class LayoutEntryUploadHandler : IEntryUploadHandler
await using (Stream layoutArchiveStream = archiveEntry.Open()) await using (Stream layoutArchiveStream = archiveEntry.Open())
await layoutStream.CopyToAsync(layoutArchiveStream, cancellationToken); await layoutStream.CopyToAsync(layoutArchiveStream, cancellationToken);
List<string> imagePaths = [];
// Add the layout image to the archive // Add the layout image to the archive
CopyImage(layoutPath, source.Layout.LayoutCustomDeviceData.DeviceImage, archive); CopyImage(layoutPath, source.Layout.LayoutCustomDeviceData.DeviceImage, archive, imagePaths);
// Add the LED images to the archive // Add the LED images to the archive
foreach (ArtemisLedLayout ledLayout in source.Layout.Leds) foreach (ArtemisLedLayout ledLayout in source.Layout.Leds)
@ -50,9 +54,10 @@ public class LayoutEntryUploadHandler : IEntryUploadHandler
if (ledLayout.LayoutCustomLedData.LogicalLayouts == null) if (ledLayout.LayoutCustomLedData.LogicalLayouts == null)
continue; continue;
foreach (LayoutCustomLedDataLogicalLayout customData in ledLayout.LayoutCustomLedData.LogicalLayouts) foreach (LayoutCustomLedDataLogicalLayout customData in ledLayout.LayoutCustomLedData.LogicalLayouts)
CopyImage(layoutPath, customData.Image, archive); CopyImage(layoutPath, customData.Image, archive, imagePaths);
} }
} }
archiveStream.Seek(0, SeekOrigin.Begin); archiveStream.Seek(0, SeekOrigin.Begin);
// Submit the archive // Submit the archive
@ -71,16 +76,53 @@ public class LayoutEntryUploadHandler : IEntryUploadHandler
if (!response.IsSuccessStatusCode) if (!response.IsSuccessStatusCode)
return EntryUploadResult.FromFailure($"{response.StatusCode} - {await response.Content.ReadAsStringAsync(cancellationToken)}"); return EntryUploadResult.FromFailure($"{response.StatusCode} - {await response.Content.ReadAsStringAsync(cancellationToken)}");
// Determine layout info, here we're combining user supplied data with what we can infer from the layout
List<LayoutInfoInput> layoutInfo = GetLayoutInfoInput(source);
// Submit layout info, overwriting the existing layout info
await _workshopClient.SetLayoutInfo.ExecuteAsync(new SetLayoutInfoInput {EntryId = entryId, LayoutInfo = layoutInfo}, cancellationToken);
Release? release = await response.Content.ReadFromJsonAsync<Release>(cancellationToken); Release? release = await response.Content.ReadFromJsonAsync<Release>(cancellationToken);
return release != null ? EntryUploadResult.FromSuccess(release) : EntryUploadResult.FromFailure("Failed to deserialize response"); return release != null ? EntryUploadResult.FromSuccess(release) : EntryUploadResult.FromFailure("Failed to deserialize response");
} }
private static void CopyImage(string layoutPath, string? imagePath, ZipArchive archive) private static List<LayoutInfoInput> GetLayoutInfoInput(LayoutEntrySource source)
{ {
if (imagePath == null) RGBDeviceType deviceType = Enum.Parse<RGBDeviceType>(source.Layout.RgbLayout.Type.ToString(), true);
KeyboardLayoutType physicalLayout = Enum.Parse<KeyboardLayoutType>(source.PhysicalLayout.ToString(), true);
List<string> logicalLayouts = source.Layout.Leds.SelectMany(l => l.GetLogicalLayoutNames()).Distinct().ToList();
if (logicalLayouts.Any())
{
return logicalLayouts.SelectMany(logicalLayout => source.LayoutInfo.Select(i => new LayoutInfoInput
{
PhysicalLayout = deviceType == RGBDeviceType.Keyboard ? physicalLayout : null,
LogicalLayout = logicalLayout,
Model = i.Model,
Vendor = i.Vendor,
DeviceType = deviceType,
DeviceProvider = i.DeviceProviderId
})).ToList();
}
return source.LayoutInfo.Select(i => new LayoutInfoInput
{
PhysicalLayout = deviceType == RGBDeviceType.Keyboard ? physicalLayout : null,
LogicalLayout = null,
Model = i.Model,
Vendor = i.Vendor,
DeviceType = deviceType,
DeviceProvider = i.DeviceProviderId
}).ToList();
}
private static void CopyImage(string layoutPath, string? imagePath, ZipArchive archive, List<string> imagePaths)
{
if (imagePath == null || imagePaths.Contains(imagePath))
return; return;
string fullPath = Path.Combine(layoutPath, imagePath); string fullPath = Path.Combine(layoutPath, imagePath);
archive.CreateEntryFromFile(fullPath, imagePath); archive.CreateEntryFromFile(fullPath, imagePath);
imagePaths.Add(imagePath);
} }
} }

View File

@ -0,0 +1,5 @@
mutation SetLayoutInfo ($input: SetLayoutInfoInput!) {
setLayoutInfo(input: $input) {
id
}
}

View File

@ -0,0 +1,23 @@
query SearchLayout($deviceType: RGBDeviceType!, $deviceProvider: UUID!, $model: String!, $vendor: String!) {
searchLayout(deviceProvider: $deviceProvider, deviceType: $deviceType, model: $model, vendor: $vendor) {
entry {
...entrySummary
latestRelease {
...release
}
}
}
}
query SearchKeyboardLayout($deviceProvider: UUID!, $model: String!, $vendor: String!, $logicalLayout: String, $physicalLayout: KeyboardLayoutType!) {
searchKeyboardLayout(deviceProvider: $deviceProvider, logicalLayout: $logicalLayout, model: $model, physicalLayout: $physicalLayout, vendor: $vendor) {
entry {
...entrySummary
latestRelease {
...release
}
}
logicalLayout
physicalLayout
}
}

View File

@ -101,6 +101,7 @@ type Mutation {
removeEntry(id: Long!): Entry removeEntry(id: Long!): Entry
removeLayoutInfo(id: Long!): LayoutInfo! removeLayoutInfo(id: Long!): LayoutInfo!
removeRelease(id: Long!): Release! removeRelease(id: Long!): Release!
setLayoutInfo(input: SetLayoutInfoInput!): [LayoutInfo!]!
updateEntry(input: UpdateEntryInput!): Entry updateEntry(input: UpdateEntryInput!): Entry
updateEntryImage(input: UpdateEntryImageInput!): Image updateEntryImage(input: UpdateEntryImageInput!): Image
updateRelease(input: UpdateReleaseInput!): Release updateRelease(input: UpdateReleaseInput!): Release
@ -399,6 +400,15 @@ input LayoutInfoFilterInput {
vendor: StringOperationFilterInput vendor: StringOperationFilterInput
} }
input LayoutInfoInput {
deviceProvider: UUID!
deviceType: RGBDeviceType!
logicalLayout: String
model: String!
physicalLayout: KeyboardLayoutType
vendor: String!
}
input ListFilterInputTypeOfCategoryFilterInput { input ListFilterInputTypeOfCategoryFilterInput {
all: CategoryFilterInput all: CategoryFilterInput
any: Boolean any: Boolean
@ -527,6 +537,11 @@ input ReleaseSortInput {
version: SortEnumType version: SortEnumType
} }
input SetLayoutInfoInput {
entryId: Long!
layoutInfo: [LayoutInfoInput!]!
}
input StringOperationFilterInput { input StringOperationFilterInput {
and: [StringOperationFilterInput!] and: [StringOperationFilterInput!]
contains: String contains: String

View File

@ -20,6 +20,7 @@
<PackageVersion Include="DryIoc.dll" Version="5.4.3" /> <PackageVersion Include="DryIoc.dll" Version="5.4.3" />
<PackageVersion Include="DynamicData" Version="8.3.27" /> <PackageVersion Include="DynamicData" Version="8.3.27" />
<PackageVersion Include="EmbedIO" Version="3.5.2" /> <PackageVersion Include="EmbedIO" Version="3.5.2" />
<PackageVersion Include="FluentAvalonia.ProgressRing" Version="1.69.2" />
<PackageVersion Include="FluentAvaloniaUI" Version="2.0.5" /> <PackageVersion Include="FluentAvaloniaUI" Version="2.0.5" />
<PackageVersion Include="HidSharp" Version="2.1.0" /> <PackageVersion Include="HidSharp" Version="2.1.0" />
<PackageVersion Include="Humanizer.Core" Version="2.14.1" /> <PackageVersion Include="Humanizer.Core" Version="2.14.1" />