1
0
mirror of https://github.com/Artemis-RGB/Artemis synced 2025-12-12 13:28:33 +00:00

Merge branch 'development'

This commit is contained in:
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;
}
#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
}

View File

@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using RGB.NET.Layout;
@ -15,7 +16,7 @@ public class ArtemisLedLayout
DeviceLayout = deviceLayout;
RgbLayout = led;
LayoutCustomLedData = (LayoutCustomLedData?) led.CustomData ?? new LayoutCustomLedData();
// Default to the first logical layout for images
LayoutCustomLedDataLogicalLayout? defaultLogicalLayout = LayoutCustomLedData.LogicalLayouts?.FirstOrDefault();
if (defaultLogicalLayout != null)
@ -46,7 +47,14 @@ public class ArtemisLedLayout
/// Gets the custom layout data embedded in the RGB.NET layout
/// </summary>
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)
{
@ -61,11 +69,11 @@ public class ArtemisLedLayout
ApplyLogicalLayout(logicalLayout);
}
private void ApplyLogicalLayout(LayoutCustomLedDataLogicalLayout logicalLayout)
{
string? layoutDirectory = Path.GetDirectoryName(DeviceLayout.FilePath);
LogicalName = logicalLayout.Name;
if (layoutDirectory != null && logicalLayout.Image != null)
Image = new Uri(Path.Combine(layoutDirectory, logicalLayout.Image!), UriKind.Absolute);

View File

@ -23,8 +23,7 @@ public class Profiler
/// Gets the name of this profiler
/// </summary>
public string Name { get; }
/// <summary>
/// Gets a dictionary containing measurements by their identifiers
/// </summary>

View File

@ -21,6 +21,7 @@
<PackageReference Include="Avalonia.Desktop" />
<PackageReference Include="Avalonia.Skia.Lottie" />
<PackageReference Include="AvaloniaEdit.TextMate" />
<PackageReference Include="FluentAvalonia.ProgressRing" />
<PackageReference Include="Markdown.Avalonia.Tight" />
<PackageReference Include="Octopus.Octodiff" />
<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);
// Update timer
Timer updateTimer = new(TimeSpan.FromMilliseconds(60.0 / 1000));
Timer updateTimer = new(TimeSpan.FromMilliseconds(16));
updateTimer.Elapsed += (_, _) => Update();
updateTimer.DisposeWith(d);
_profileEditorService.Playing.Subscribe(_ => _lastUpdate = DateTime.Now).DisposeWith(d);

View File

@ -51,7 +51,7 @@ public class DataBindingViewModel : ActivatableViewModelBase
.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));
updateTimer.Elapsed += (_, _) => Update();

View File

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

View File

@ -9,6 +9,7 @@ using Artemis.Core.DeviceProviders;
using Artemis.Core.Services;
using Artemis.UI.DryIoc.Factories;
using Artemis.UI.Screens.Plugins;
using Artemis.UI.Screens.Workshop.LayoutFinder;
using Artemis.UI.Shared;
using Artemis.UI.Shared.Providers;
using Artemis.UI.Shared.Services;
@ -35,7 +36,8 @@ public partial class StartupWizardViewModel : DialogViewModelBase<bool>
IPluginManagementService pluginManagementService,
IWindowService windowService,
IDeviceService deviceService,
ISettingsVmFactory settingsVmFactory)
ISettingsVmFactory settingsVmFactory,
LayoutFinderViewModel layoutFinderViewModel)
{
_settingsService = settingsService;
_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))))
.OrderBy(p => p.Info.Name)
.Select(p => settingsVmFactory.PluginViewModel(p, ReactiveCommand.Create(() => new Unit()))));
LayoutFinderViewModel = layoutFinderViewModel;
CurrentStep = 1;
SetupButtons();
@ -82,7 +85,8 @@ public partial class StartupWizardViewModel : DialogViewModelBase<bool>
public string Version { get; }
public ObservableCollection<PluginViewModel> DeviceProviders { get; }
public LayoutFinderViewModel LayoutFinderViewModel { get; }
public bool IsAutoRunSupported => _autoRunProvider != null;
public PluginSetting<bool> UIAutoRun => _settingsService.GetSetting("UI.AutoRun", false);
@ -98,7 +102,7 @@ public partial class StartupWizardViewModel : DialogViewModelBase<bool>
CurrentStep--;
// Skip the settings step if none of it's contents are supported
if (CurrentStep == 4 && !IsAutoRunSupported)
if (CurrentStep == 5 && !IsAutoRunSupported)
CurrentStep--;
SetupButtons();
@ -106,11 +110,11 @@ public partial class StartupWizardViewModel : DialogViewModelBase<bool>
private void ExecuteContinue()
{
if (CurrentStep < 5)
if (CurrentStep < 6)
CurrentStep++;
// Skip the settings step if none of it's contents are supported
if (CurrentStep == 4 && !IsAutoRunSupported)
if (CurrentStep == 5 && !IsAutoRunSupported)
CurrentStep++;
SetupButtons();
@ -118,9 +122,9 @@ public partial class StartupWizardViewModel : DialogViewModelBase<bool>
private void SetupButtons()
{
ShowContinue = CurrentStep != 3 && CurrentStep < 5;
ShowContinue = CurrentStep != 4 && CurrentStep < 6;
ShowGoBack = CurrentStep > 1;
ShowFinish = CurrentStep == 5;
ShowFinish = CurrentStep == 6;
}
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:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
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">
<Grid RowDefinitions="Auto,*,Auto,Auto" ColumnDefinitions="*,*">

View File

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

View File

@ -66,7 +66,7 @@ public class NodeScriptWindowViewModel : NodeScriptWindowViewModelBase
{
_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));
updateTimer.Elapsed += (_, _) => Update();

View File

@ -42,16 +42,7 @@
</Ellipse>
<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>
<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
IsVisible="{CompiledBinding AllowLogout}"
Grid.Column="1"
@ -61,6 +52,15 @@
Click="Manage_OnClick">
Manage account
</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>
</Flyout>
</Ellipse.ContextFlyout>

View File

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

View File

@ -54,6 +54,7 @@ public partial class EntryListViewModel : RoutableScreen
this.WhenActivated(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);
});
@ -71,6 +72,7 @@ public partial class EntryListViewModel : RoutableScreen
public CategoriesViewModel CategoriesViewModel { get; }
public EntryListInputViewModel InputViewModel { get; }
public bool ShowCategoryFilter { get; set; } = true;
public EntryType? EntryType { get; set; }
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,
IPluginManagementService pluginManagementService)
{
ArtemisDevice? device = deviceService.Devices.FirstOrDefault(d => d.Layout == layout);
_windowService = windowService;
_vendor = layout.RgbLayout.Vendor;
_model = layout.RgbLayout.Model;
_vendor = device?.RgbDevice.DeviceInfo.Manufacturer ?? layout.RgbLayout.Vendor;
_model = device?.RgbDevice.DeviceInfo.Model ?? layout.RgbLayout.Model;
DeviceProvider? deviceProvider = deviceService.Devices.FirstOrDefault(d => d.Layout == layout)?.DeviceProvider;
if (deviceProvider != null)
_deviceProviderId = deviceProvider.Plugin.Guid;
if (device != null)
_deviceProviderId = device.DeviceProvider.Plugin.Guid;
_deviceProviders = this.WhenAnyValue(vm => vm.DeviceProviderId)
.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>
{
private readonly EntryListViewModel _entryListViewModel;
public override RoutableScreen DefaultScreen => _entryListViewModel;
public override RoutableScreen DefaultScreen { get; }
public LayoutListViewModel(EntryListViewModel entryListViewModel)
public LayoutListViewModel(LayoutListDefaultViewModel defaultViewModel)
{
_entryListViewModel = entryListViewModel;
_entryListViewModel.EntryType = EntryType.Layout;
DefaultScreen = defaultViewModel;
}
}

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.
Some layouts can be shared across different devices and here you have a chance to set that up.
</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 Grid.Row="1"

View File

@ -7,12 +7,14 @@
xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia">
<!-- Third party styles -->
<styling:FluentAvaloniaTheme PreferSystemTheme="False" PreferUserAccentColor="True"/>
<avalonia:MaterialIconStyles />
<!-- <FluentTheme Mode="Dark"></FluentTheme> -->
<StyleInclude Source="avares://Artemis.UI.Shared/Styles/Artemis.axaml" />
<StyleInclude Source="avares://AsyncImageLoader.Avalonia/AdvancedImage.axaml" />
<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 -->
<Style Selector="aedit|TextEditor /template/ ScrollViewer ScrollContentPresenter">

View File

@ -1,6 +1,5 @@
using Artemis.UI.Shared.Utilities;
using Artemis.WebClient.Workshop.Models;
using Artemis.WebClient.Workshop.Services;
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
stream.Seek(0, SeekOrigin.Begin);
using ZipArchive archive = new(stream);
archive.ExtractToDirectory(releaseDirectory.FullName);
archive.ExtractToDirectory(releaseDirectory.FullName, true);
ArtemisLayout layout = new(Path.Combine(releaseDirectory.FullName, "layout.xml"));
if (layout.IsValid)

View File

@ -11,12 +11,14 @@ namespace Artemis.WebClient.Workshop.Handlers.UploadHandlers;
public class LayoutEntryUploadHandler : IEntryUploadHandler
{
private readonly IHttpClientFactory _httpClientFactory;
private readonly IWorkshopClient _workshopClient;
public LayoutEntryUploadHandler(IHttpClientFactory httpClientFactory)
public LayoutEntryUploadHandler(IHttpClientFactory httpClientFactory, IWorkshopClient workshopClient)
{
_httpClientFactory = httpClientFactory;
_workshopClient = workshopClient;
}
/// <inheritdoc />
public async Task<EntryUploadResult> CreateReleaseAsync(long entryId, IEntrySource entrySource, string? changelog, CancellationToken cancellationToken)
{
@ -41,8 +43,10 @@ public class LayoutEntryUploadHandler : IEntryUploadHandler
await using (Stream layoutArchiveStream = archiveEntry.Open())
await layoutStream.CopyToAsync(layoutArchiveStream, cancellationToken);
List<string> imagePaths = [];
// 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
foreach (ArtemisLedLayout ledLayout in source.Layout.Leds)
@ -50,11 +54,12 @@ public class LayoutEntryUploadHandler : IEntryUploadHandler
if (ledLayout.LayoutCustomLedData.LogicalLayouts == null)
continue;
foreach (LayoutCustomLedDataLogicalLayout customData in ledLayout.LayoutCustomLedData.LogicalLayouts)
CopyImage(layoutPath, customData.Image, archive);
CopyImage(layoutPath, customData.Image, archive, imagePaths);
}
}
archiveStream.Seek(0, SeekOrigin.Begin);
// Submit the archive
HttpClient client = _httpClientFactory.CreateClient(WorkshopConstants.WORKSHOP_CLIENT_NAME);
@ -71,16 +76,53 @@ public class LayoutEntryUploadHandler : IEntryUploadHandler
if (!response.IsSuccessStatusCode)
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);
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;
string fullPath = Path.Combine(layoutPath, 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
removeLayoutInfo(id: Long!): LayoutInfo!
removeRelease(id: Long!): Release!
setLayoutInfo(input: SetLayoutInfoInput!): [LayoutInfo!]!
updateEntry(input: UpdateEntryInput!): Entry
updateEntryImage(input: UpdateEntryImageInput!): Image
updateRelease(input: UpdateReleaseInput!): Release
@ -399,6 +400,15 @@ input LayoutInfoFilterInput {
vendor: StringOperationFilterInput
}
input LayoutInfoInput {
deviceProvider: UUID!
deviceType: RGBDeviceType!
logicalLayout: String
model: String!
physicalLayout: KeyboardLayoutType
vendor: String!
}
input ListFilterInputTypeOfCategoryFilterInput {
all: CategoryFilterInput
any: Boolean
@ -527,6 +537,11 @@ input ReleaseSortInput {
version: SortEnumType
}
input SetLayoutInfoInput {
entryId: Long!
layoutInfo: [LayoutInfoInput!]!
}
input StringOperationFilterInput {
and: [StringOperationFilterInput!]
contains: String

View File

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