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-01-16 08:39:51 +01:00
commit ff6efe9456
273 changed files with 4428 additions and 3050 deletions

View File

@ -1,6 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\Artemis.props" />
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<PreserveCompilationContext>false</PreserveCompilationContext>
@ -38,27 +36,20 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="DryIoc.dll" Version="5.4.1" />
<PackageReference Include="DryIoc.dll" Version="5.4.3" />
<PackageReference Include="EmbedIO" Version="3.5.2" />
<PackageReference Include="HidSharp" Version="2.1.0" />
<PackageReference Include="Humanizer.Core" Version="2.14.1" />
<PackageReference Include="JetBrains.Annotations" Version="2023.2.0" />
<PackageReference Include="LiteDB" Version="5.0.17" />
<PackageReference Include="JetBrains.Annotations" Version="2023.3.0" />
<PackageReference Include="McMaster.NETCore.Plugins" Version="1.4.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
<PackageReference Include="RGB.NET.Core" Version="$(RGBDotNetVersion)" />
<PackageReference Include="RGB.NET.Layout" Version="$(RGBDotNetVersion)" />
<PackageReference Include="RGB.NET.Presets" Version="$(RGBDotNetVersion)" />
<PackageReference Include="Serilog" Version="3.0.1" />
<PackageReference Include="Serilog.Sinks.Console" Version="4.1.0" />
<PackageReference Include="RGB.NET.Core" Version="2.0.4-prerelease.1" />
<PackageReference Include="RGB.NET.Layout" Version="2.0.4-prerelease.1" />
<PackageReference Include="RGB.NET.Presets" Version="2.0.4-prerelease.1" />
<PackageReference Include="Serilog.Sinks.Console" Version="5.0.1" />
<PackageReference Include="Serilog.Sinks.Debug" Version="2.0.0" />
<PackageReference Include="Serilog.Sinks.File" Version="5.0.0" />
<PackageReference Include="SkiaSharp" Version="$(SkiaSharpVersion)" />
<PackageReference Include="System.Buffers" Version="4.5.1" />
<PackageReference Include="System.IO.FileSystem.AccessControl" Version="5.0.0" />
<PackageReference Include="System.Numerics.Vectors" Version="4.5.0" />
<PackageReference Include="System.ValueTuple" Version="4.5.0" />
<PackageReference Include="Unosquare.Swan.Lite" Version="3.1.0" />
<PackageReference Include="SkiaSharp" Version="2.88.7" />
</ItemGroup>
<ItemGroup>

View File

@ -47,6 +47,7 @@ public static class Constants
/// The full path to the Artemis logs folder
/// </summary>
public static readonly string LogsFolder = Path.Combine(DataFolder, "Logs");
/// <summary>
/// The full path to the Artemis logs folder
/// </summary>
@ -61,6 +62,11 @@ public static class Constants
/// The full path to the Artemis user layouts folder
/// </summary>
public static readonly string LayoutsFolder = Path.Combine(DataFolder, "User Layouts");
/// <summary>
/// The full path to the Artemis user layouts folder
/// </summary>
public static readonly string WorkshopFolder = Path.Combine(DataFolder, "workshop");
/// <summary>
/// The current API version for plugins
@ -71,9 +77,9 @@ public static class Constants
/// <summary>
/// The current version of the application
/// </summary>
public static readonly string CurrentVersion = CoreAssembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>()!.InformationalVersion != "1.0.0"
? CoreAssembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>()!.InformationalVersion
: "local";
public static readonly string CurrentVersion = CoreAssembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>()!.InformationalVersion.StartsWith("1.0.0")
? "local"
: CoreAssembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>()!.InformationalVersion;
/// <summary>
/// The plugin info used by core components of Artemis
@ -151,8 +157,8 @@ public static class Constants
public static ReadOnlyCollection<string> StartupArguments { get; set; } = null!;
/// <summary>
/// Gets the graphics context to be used for rendering by SkiaSharp. Can be set via
/// <see cref="IRgbService.UpdateGraphicsContext" />.
/// Gets the graphics context to be used for rendering by SkiaSharp.
/// </summary>
public static IManagedGraphicsContext? ManagedGraphicsContext { get; internal set; }
}

View File

@ -2,6 +2,7 @@ using System;
using System.Linq;
using System.Reflection;
using Artemis.Core.DryIoc.Factories;
using Artemis.Core.Providers;
using Artemis.Core.Services;
using Artemis.Storage;
using Artemis.Storage.Migrations.Interfaces;
@ -35,7 +36,8 @@ public static class ContainerExtensions
// Bind migrations
container.RegisterMany(storageAssembly, type => type.IsAssignableTo<IStorageMigration>(), Reuse.Singleton, nonPublicServiceTypes: true);
container.RegisterMany(coreAssembly, type => type.IsAssignableTo<ILayoutProvider>(), Reuse.Singleton);
container.Register<IPluginSettingsFactory, PluginSettingsFactory>(Reuse.Singleton);
container.Register(Made.Of(_ => ServiceInfo.Of<IPluginSettingsFactory>(), f => f.CreatePluginSettings(Arg.Index<Type>(0)), r => r.Parent.ImplementationType));
container.Register<ILoggerFactory, LoggerFactory>(Reuse.Singleton);

View File

@ -1,31 +0,0 @@
using System.IO;
namespace Artemis.Core;
internal static class DirectoryInfoExtensions
{
public static void CopyFilesRecursively(this DirectoryInfo source, DirectoryInfo target)
{
foreach (DirectoryInfo dir in source.GetDirectories())
CopyFilesRecursively(dir, target.CreateSubdirectory(dir.Name));
foreach (FileInfo file in source.GetFiles())
file.CopyTo(Path.Combine(target.FullName, file.Name));
}
public static void DeleteRecursively(this DirectoryInfo baseDir)
{
if (!baseDir.Exists)
return;
foreach (DirectoryInfo dir in baseDir.EnumerateDirectories())
DeleteRecursively(dir);
FileInfo[] files = baseDir.GetFiles();
foreach (FileInfo file in files)
{
file.IsReadOnly = false;
file.Delete();
}
baseDir.Delete();
}
}

View File

@ -23,7 +23,6 @@ public class ProfileConfiguration : BreakableModel, IStorageModel, IDisposable
private bool _disposed;
private Hotkey? _enableHotkey;
private ProfileConfigurationHotkeyMode _hotkeyMode;
private bool _isBeingEdited;
private bool _isMissingModule;
private bool _isSuspended;
private bool _fadeInAndOut;

View File

@ -2,9 +2,10 @@
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Diagnostics;
using System.Linq;
using Artemis.Core.DeviceProviders;
using Artemis.Core.Providers;
using Artemis.Core.Services;
using Artemis.Storage.Entities.Surface;
using RGB.NET.Core;
using SkiaSharp;
@ -45,6 +46,7 @@ public class ArtemisDevice : CorePropertyChanged
InputIdentifiers = new List<ArtemisDeviceInputIdentifier>();
InputMappings = new Dictionary<ArtemisLed, ArtemisLed>();
Categories = new HashSet<DeviceCategory>();
LayoutSelection = new LayoutSelection {Type = DefaultLayoutProvider.LayoutType};
RgbDevice.ColorCorrections.Clear();
RgbDevice.ColorCorrections.Add(new ScaleColorCorrection(this));
@ -73,6 +75,7 @@ public class ArtemisDevice : CorePropertyChanged
InputIdentifiers = new List<ArtemisDeviceInputIdentifier>();
InputMappings = new Dictionary<ArtemisLed, ArtemisLed>();
Categories = new HashSet<DeviceCategory>();
LayoutSelection = new LayoutSelection {Type = DefaultLayoutProvider.LayoutType};
foreach (DeviceInputIdentifierEntity identifierEntity in DeviceEntity.InputIdentifiers)
InputIdentifiers.Add(new ArtemisDeviceInputIdentifier(identifierEntity.InputProvider, identifierEntity.Identifier));
@ -152,6 +155,8 @@ public class ArtemisDevice : CorePropertyChanged
/// </summary>
public HashSet<DeviceCategory> Categories { get; }
public LayoutSelection LayoutSelection { get; }
/// <summary>
/// Gets or sets the X-position of the device
/// </summary>
@ -266,7 +271,7 @@ public class ArtemisDevice : CorePropertyChanged
/// <summary>
/// Gets a boolean indicating whether this devices is enabled or not
/// <para>Note: To enable/disable a device use the methods provided by <see cref="IRgbService" /></para>
/// <para>Note: To enable/disable a device use the methods provided by <see cref="IDeviceService" /></para>
/// </summary>
public bool IsEnabled
{
@ -292,19 +297,6 @@ public class ArtemisDevice : CorePropertyChanged
}
}
/// <summary>
/// Gets or sets a boolean indicating whether falling back to default layouts is enabled or not
/// </summary>
public bool DisableDefaultLayout
{
get => DeviceEntity.DisableDefaultLayout;
set
{
DeviceEntity.DisableDefaultLayout = value;
OnPropertyChanged(nameof(DisableDefaultLayout));
}
}
/// <summary>
/// Gets or sets the logical layout of the device e.g. DE, UK or US.
/// <para>Only applicable to keyboards</para>
@ -319,20 +311,6 @@ public class ArtemisDevice : CorePropertyChanged
}
}
/// <summary>
/// Gets or sets the path of the custom layout to load when calling <see cref="IRgbService.ApplyBestDeviceLayout" />
/// for this device
/// </summary>
public string? CustomLayoutPath
{
get => DeviceEntity.CustomLayoutPath;
set
{
DeviceEntity.CustomLayoutPath = value;
OnPropertyChanged(nameof(CustomLayoutPath));
}
}
/// <summary>
/// Gets the layout of the device expanded with Artemis-specific data
/// </summary>
@ -380,40 +358,6 @@ public class ArtemisDevice : CorePropertyChanged
return artemisLed;
}
/// <summary>
/// Returns the most preferred device layout for this device.
/// </summary>
/// <returns>The most preferred device layout for this device.</returns>
public ArtemisLayout? GetBestDeviceLayout()
{
ArtemisLayout? layout;
// Configured layout path takes precedence over all other options
if (CustomLayoutPath != null)
{
layout = new ArtemisLayout(CustomLayoutPath, LayoutSource.Configured);
if (layout.IsValid)
return layout;
}
// Look for a layout provided by the user
layout = DeviceProvider.LoadUserLayout(this);
if (layout.IsValid)
return layout;
if (DisableDefaultLayout)
return null;
// Look for a layout provided by the plugin
layout = DeviceProvider.LoadLayout(this);
if (layout.IsValid)
return layout;
// Finally fall back to a default layout
layout = ArtemisLayout.GetDefaultLayout(this);
return layout;
}
/// <summary>
/// Occurs when the underlying RGB.NET device was updated
/// </summary>
@ -454,14 +398,6 @@ public class ArtemisDevice : CorePropertyChanged
}
}
/// <summary>
/// Invokes the <see cref="DeviceUpdated" /> event
/// </summary>
protected virtual void OnDeviceUpdated()
{
DeviceUpdated?.Invoke(this, EventArgs.Empty);
}
/// <summary>
/// Applies the provided layout to the device
/// </summary>
@ -474,13 +410,13 @@ public class ArtemisDevice : CorePropertyChanged
/// A boolean indicating whether to remove excess LEDs present in the device but missing
/// in the layout
/// </param>
internal void ApplyLayout(ArtemisLayout? layout, bool createMissingLeds, bool removeExcessiveLeds)
public void ApplyLayout(ArtemisLayout? layout, bool createMissingLeds, bool removeExcessiveLeds)
{
if (layout != null && layout.IsValid && createMissingLeds && !DeviceProvider.CreateMissingLedsSupported)
throw new ArtemisCoreException($"Cannot apply layout with {nameof(createMissingLeds)} set to true because the device provider does not support it");
if (layout != null && layout.IsValid && removeExcessiveLeds && !DeviceProvider.RemoveExcessiveLedsSupported)
throw new ArtemisCoreException($"Cannot apply layout with {nameof(removeExcessiveLeds)} set to true because the device provider does not support it");
// Always clear the current layout
ClearLayout();
@ -501,6 +437,14 @@ public class ArtemisDevice : CorePropertyChanged
CalculateRenderProperties();
}
/// <summary>
/// Invokes the <see cref="DeviceUpdated" /> event
/// </summary>
protected virtual void OnDeviceUpdated()
{
DeviceUpdated?.Invoke(this, EventArgs.Empty);
}
private void ClearLayout()
{
if (Layout == null)
@ -533,6 +477,9 @@ public class ArtemisDevice : CorePropertyChanged
DeviceEntity.Categories.Clear();
foreach (DeviceCategory deviceCategory in Categories)
DeviceEntity.Categories.Add((int) deviceCategory);
DeviceEntity.LayoutType = LayoutSelection.Type;
DeviceEntity.LayoutParameter = LayoutSelection.Parameter;
}
internal void Load()
@ -547,6 +494,9 @@ public class ArtemisDevice : CorePropertyChanged
if (!Categories.Any())
ApplyDefaultCategories();
LayoutSelection.Type = DeviceEntity.LayoutType;
LayoutSelection.Parameter = DeviceEntity.LayoutParameter;
LoadInputMappings();
}
@ -572,7 +522,7 @@ public class ArtemisDevice : CorePropertyChanged
{
Leds = RgbDevice.Select(l => new ArtemisLed(l, this)).ToList().AsReadOnly();
LedIds = new ReadOnlyDictionary<LedId, ArtemisLed>(Leds.ToDictionary(l => l.RgbLed.Id, l => l));
if (loadInputMappings)
LoadInputMappings();
}

View File

@ -12,17 +12,18 @@ namespace Artemis.Core;
/// </summary>
public class ArtemisLayout
{
private static readonly string DefaultLayoutPath = Path.Combine(Constants.ApplicationFolder, "DefaultLayouts", "Artemis");
/// <summary>
/// Creates a new instance of the <see cref="ArtemisLayout" /> class
/// </summary>
/// <param name="filePath">The path of the layout XML file</param>
/// <param name="source">The source from where this layout is being loaded</param>
public ArtemisLayout(string filePath, LayoutSource source)
public ArtemisLayout(string filePath)
{
FilePath = filePath;
Source = source;
Leds = new List<ArtemisLedLayout>();
IsDefaultLayout = filePath.StartsWith(DefaultLayoutPath);
LoadLayout();
}
@ -31,11 +32,6 @@ public class ArtemisLayout
/// </summary>
public string FilePath { get; }
/// <summary>
/// Gets the source from where this layout was loaded
/// </summary>
public LayoutSource Source { get; }
/// <summary>
/// Gets a boolean indicating whether a valid layout was loaded
/// </summary>
@ -61,6 +57,8 @@ public class ArtemisLayout
/// </summary>
public LayoutCustomDeviceData LayoutCustomDeviceData { get; private set; } = null!;
public bool IsDefaultLayout { get; private set; }
/// <summary>
/// Applies the layout to the provided device
/// </summary>
@ -121,29 +119,28 @@ public class ArtemisLayout
internal static ArtemisLayout? GetDefaultLayout(ArtemisDevice device)
{
string layoutFolder = Path.Combine(Constants.ApplicationFolder, "DefaultLayouts", "Artemis");
if (device.DeviceType == RGBDeviceType.Keyboard)
{
// XL layout is defined by its programmable macro keys
if (device.Leds.Any(l => l.RgbLed.Id >= LedId.Keyboard_Programmable1 && l.RgbLed.Id <= LedId.Keyboard_Programmable32))
{
if (device.PhysicalLayout == KeyboardLayoutType.ANSI)
return new ArtemisLayout(Path.Combine(layoutFolder, "Keyboard", "Artemis XL keyboard-ANSI.xml"), LayoutSource.Default);
return new ArtemisLayout(Path.Combine(layoutFolder, "Keyboard", "Artemis XL keyboard-ISO.xml"), LayoutSource.Default);
return new ArtemisLayout(Path.Combine(DefaultLayoutPath, "Keyboard", "Artemis XL keyboard-ANSI.xml"));
return new ArtemisLayout(Path.Combine(DefaultLayoutPath, "Keyboard", "Artemis XL keyboard-ISO.xml"));
}
// L layout is defined by its numpad
if (device.Leds.Any(l => l.RgbLed.Id >= LedId.Keyboard_NumLock && l.RgbLed.Id <= LedId.Keyboard_NumPeriodAndDelete))
{
if (device.PhysicalLayout == KeyboardLayoutType.ANSI)
return new ArtemisLayout(Path.Combine(layoutFolder + "Keyboard", "Artemis L keyboard-ANSI.xml"), LayoutSource.Default);
return new ArtemisLayout(Path.Combine(layoutFolder + "Keyboard", "Artemis L keyboard-ISO.xml"), LayoutSource.Default);
return new ArtemisLayout(Path.Combine(DefaultLayoutPath, "Keyboard", "Artemis L keyboard-ANSI.xml"));
return new ArtemisLayout(Path.Combine(DefaultLayoutPath, "Keyboard", "Artemis L keyboard-ISO.xml"));
}
// No numpad will result in TKL
if (device.PhysicalLayout == KeyboardLayoutType.ANSI)
return new ArtemisLayout(Path.Combine(layoutFolder + "Keyboard", "Artemis TKL keyboard-ANSI.xml"), LayoutSource.Default);
return new ArtemisLayout(Path.Combine(layoutFolder + "Keyboard", "Artemis TKL keyboard-ISO.xml"), LayoutSource.Default);
return new ArtemisLayout(Path.Combine(DefaultLayoutPath, "Keyboard", "Artemis TKL keyboard-ANSI.xml"));
return new ArtemisLayout(Path.Combine(DefaultLayoutPath, "Keyboard", "Artemis TKL keyboard-ISO.xml"));
}
// if (device.DeviceType == RGBDeviceType.Mouse)
@ -151,21 +148,21 @@ public class ArtemisLayout
// if (device.Leds.Count == 1)
// {
// if (device.Leds.Any(l => l.RgbLed.Id == LedId.Logo))
// return new ArtemisLayout(Path.Combine(layoutFolder + "Mouse", "1 LED mouse logo.xml"), LayoutSource.Default);
// return new ArtemisLayout(Path.Combine(layoutFolder + "Mouse", "1 LED mouse.xml"), LayoutSource.Default);
// return new ArtemisLayout(Path.Combine(DefaultLayoutPath, "Mouse", "1 LED mouse logo.xml"), LayoutSource.Default);
// return new ArtemisLayout(Path.Combine(DefaultLayoutPath, "Mouse", "1 LED mouse.xml"), LayoutSource.Default);
// }
// if (device.Leds.Any(l => l.RgbLed.Id == LedId.Logo))
// return new ArtemisLayout(Path.Combine(layoutFolder + "Mouse", "4 LED mouse logo.xml"), LayoutSource.Default);
// return new ArtemisLayout(Path.Combine(layoutFolder + "Mouse", "4 LED mouse.xml"), LayoutSource.Default);
// return new ArtemisLayout(Path.Combine(DefaultLayoutPath, "Mouse", "4 LED mouse logo.xml"), LayoutSource.Default);
// return new ArtemisLayout(Path.Combine(DefaultLayoutPath, "Mouse", "4 LED mouse.xml"), LayoutSource.Default);
// }
if (device.DeviceType == RGBDeviceType.Headset)
{
if (device.Leds.Count == 1)
return new ArtemisLayout(Path.Combine(layoutFolder + "Headset", "Artemis 1 LED headset.xml"), LayoutSource.Default);
return new ArtemisLayout(Path.Combine(DefaultLayoutPath, "Headset", "Artemis 1 LED headset.xml"));
if (device.Leds.Count == 2)
return new ArtemisLayout(Path.Combine(layoutFolder + "Headset", "Artemis 2 LED headset.xml"), LayoutSource.Default);
return new ArtemisLayout(Path.Combine(layoutFolder + "Headset", "Artemis 4 LED headset.xml"), LayoutSource.Default);
return new ArtemisLayout(Path.Combine(DefaultLayoutPath, "Headset", "Artemis 2 LED headset.xml"));
return new ArtemisLayout(Path.Combine(DefaultLayoutPath, "Headset", "Artemis 4 LED headset.xml"));
}
return null;
@ -206,30 +203,10 @@ public class ArtemisLayout
else
Image = null;
}
}
/// <summary>
/// Represents a source from where a layout came
/// </summary>
public enum LayoutSource
{
/// <summary>
/// A layout loaded from config
/// </summary>
Configured,
/// <summary>
/// A layout loaded from the user layout folder
/// </summary>
User,
/// <summary>
/// A layout loaded from the plugin folder
/// </summary>
Plugin,
/// <summary>
/// A default layout loaded as a fallback option
/// </summary>
Default
/// <inheritdoc />
public override string ToString()
{
return FilePath;
}
}

View File

@ -15,6 +15,11 @@ 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)
ApplyLogicalLayout(defaultLogicalLayout);
}
/// <summary>
@ -54,7 +59,17 @@ public class ArtemisLedLayout
.ThenBy(l => l.Name == null)
.First();
ApplyLogicalLayout(logicalLayout);
}
private void ApplyLogicalLayout(LayoutCustomLedDataLogicalLayout logicalLayout)
{
string? layoutDirectory = Path.GetDirectoryName(DeviceLayout.FilePath);
LogicalName = logicalLayout.Name;
Image = new Uri(Path.Combine(Path.GetDirectoryName(DeviceLayout.FilePath)!, logicalLayout.Image!), UriKind.Absolute);
if (layoutDirectory != null && logicalLayout.Image != null)
Image = new Uri(Path.Combine(layoutDirectory, logicalLayout.Image!), UriKind.Absolute);
else
Image = null;
}
}

View File

@ -0,0 +1,28 @@
namespace Artemis.Core;
/// <summary>
/// Represents a reference to a layout for a device.
/// </summary>
public class LayoutSelection : CorePropertyChanged
{
private string? _type;
private string? _parameter;
/// <summary>
/// Gets or sets what kind of layout reference this is.
/// </summary>
public string? Type
{
get => _type;
set => SetAndNotify(ref _type, value);
}
/// <summary>
/// Gets or sets the parameter of the layout reference, such as a file path of workshop entry ID.
/// </summary>
public string? Parameter
{
get => _parameter;
set => SetAndNotify(ref _parameter, value);
}
}

View File

@ -62,7 +62,7 @@ public abstract class DeviceProvider : PluginFeature
device.DeviceType.ToString(),
GetDeviceLayoutName(device)
);
return new ArtemisLayout(filePath, LayoutSource.Plugin);
return new ArtemisLayout(filePath);
}
/// <summary>
@ -79,7 +79,7 @@ public abstract class DeviceProvider : PluginFeature
device.DeviceType.ToString(),
GetDeviceLayoutName(device)
);
return new ArtemisLayout(filePath, LayoutSource.User);
return new ArtemisLayout(filePath);
}
/// <summary>

View File

@ -0,0 +1,37 @@
namespace Artemis.Core.Providers;
public class CustomPathLayoutProvider : ILayoutProvider
{
public static string LayoutType = "CustomPath";
/// <inheritdoc />
public ArtemisLayout? GetDeviceLayout(ArtemisDevice device)
{
if (device.LayoutSelection.Parameter == null)
return null;
return new ArtemisLayout(device.LayoutSelection.Parameter);
}
/// <inheritdoc />
public void ApplyLayout(ArtemisDevice device, ArtemisLayout layout)
{
device.ApplyLayout(layout, device.DeviceProvider.CreateMissingLedsSupported, device.DeviceProvider.RemoveExcessiveLedsSupported);
}
/// <inheritdoc />
public bool IsMatch(ArtemisDevice device)
{
return device.LayoutSelection.Type == LayoutType;
}
/// <summary>
/// Configures the provided device to use this layout provider.
/// </summary>
/// <param name="device">The device to apply the provider to.</param>
/// <param name="path">The path to the custom layout.</param>
public void ConfigureDevice(ArtemisDevice device, string? path)
{
device.LayoutSelection.Type = LayoutType;
device.LayoutSelection.Parameter = path;
}
}

View File

@ -0,0 +1,41 @@
namespace Artemis.Core.Providers;
public class DefaultLayoutProvider : ILayoutProvider
{
public static string LayoutType = "Default";
/// <inheritdoc />
public ArtemisLayout? GetDeviceLayout(ArtemisDevice device)
{
// Look for a layout provided by the plugin
ArtemisLayout layout = device.DeviceProvider.LoadLayout(device);
// Finally fall back to a default layout
return layout.IsValid ? layout : ArtemisLayout.GetDefaultLayout(device);
}
/// <inheritdoc />
public void ApplyLayout(ArtemisDevice device, ArtemisLayout layout)
{
if (layout.IsDefaultLayout)
device.ApplyLayout(layout, false, false);
else
device.ApplyLayout(layout, device.DeviceProvider.CreateMissingLedsSupported, device.DeviceProvider.RemoveExcessiveLedsSupported);
}
/// <inheritdoc />
public bool IsMatch(ArtemisDevice device)
{
return device.LayoutSelection.Type == LayoutType;
}
/// <summary>
/// Configures the provided device to use this layout provider.
/// </summary>
/// <param name="device">The device to apply the provider to.</param>
public void ConfigureDevice(ArtemisDevice device)
{
device.LayoutSelection.Type = LayoutType;
device.LayoutSelection.Parameter = null;
}
}

View File

@ -0,0 +1,17 @@
namespace Artemis.Core.Providers;
/// <summary>
/// Represents a class that can provide Artemis layouts for devices.
/// </summary>
public interface ILayoutProvider
{
/// <summary>
/// If available, loads an Artemis layout for the provided device.
/// </summary>
/// <param name="device">The device to load the layout for.</param>
/// <returns>The resulting layout if one was available; otherwise <see langword="null" />.</returns>
ArtemisLayout? GetDeviceLayout(ArtemisDevice device);
void ApplyLayout(ArtemisDevice device, ArtemisLayout layout);
bool IsMatch(ArtemisDevice device);
}

View File

@ -0,0 +1,34 @@
namespace Artemis.Core.Providers;
public class NoneLayoutProvider : ILayoutProvider
{
public static string LayoutType = "None";
/// <inheritdoc />
public ArtemisLayout? GetDeviceLayout(ArtemisDevice device)
{
return null;
}
/// <inheritdoc />
public void ApplyLayout(ArtemisDevice device, ArtemisLayout layout)
{
device.ApplyLayout(null, false, false);
}
/// <inheritdoc />
public bool IsMatch(ArtemisDevice device)
{
return device.LayoutSelection.Type == LayoutType;
}
/// <summary>
/// Configures the provided device to use this layout provider.
/// </summary>
/// <param name="device">The device to apply the provider to.</param>
public void ConfigureDevice(ArtemisDevice device)
{
device.LayoutSelection.Type = LayoutType;
device.LayoutSelection.Parameter = null;
}
}

View File

@ -4,9 +4,11 @@ using System.Collections.ObjectModel;
using System.Linq;
using System.Threading.Tasks;
using Artemis.Core.DeviceProviders;
using Artemis.Core.Providers;
using Artemis.Core.Services.Models;
using Artemis.Storage.Entities.Surface;
using Artemis.Storage.Repositories.Interfaces;
using DryIoc;
using RGB.NET.Core;
using Serilog;
@ -18,15 +20,21 @@ internal class DeviceService : IDeviceService
private readonly IPluginManagementService _pluginManagementService;
private readonly IDeviceRepository _deviceRepository;
private readonly Lazy<IRenderService> _renderService;
private readonly Func<List<ILayoutProvider>> _getLayoutProviders;
private readonly List<ArtemisDevice> _enabledDevices = new();
private readonly List<ArtemisDevice> _devices = new();
public DeviceService(ILogger logger, IPluginManagementService pluginManagementService, IDeviceRepository deviceRepository, Lazy<IRenderService> renderService)
public DeviceService(ILogger logger,
IPluginManagementService pluginManagementService,
IDeviceRepository deviceRepository,
Lazy<IRenderService> renderService,
Func<List<ILayoutProvider>> getLayoutProviders)
{
_logger = logger;
_pluginManagementService = pluginManagementService;
_deviceRepository = deviceRepository;
_renderService = renderService;
_getLayoutProviders = getLayoutProviders;
EnabledDevices = new ReadOnlyCollection<ArtemisDevice>(_enabledDevices);
Devices = new ReadOnlyCollection<ArtemisDevice>(_devices);
@ -157,12 +165,30 @@ internal class DeviceService : IDeviceService
}
/// <inheritdoc />
public void ApplyDeviceLayout(ArtemisDevice device, ArtemisLayout? layout)
public void LoadDeviceLayout(ArtemisDevice device)
{
if (layout == null || layout.Source == LayoutSource.Default)
device.ApplyLayout(layout, false, false);
else
device.ApplyLayout(layout, device.DeviceProvider.CreateMissingLedsSupported, device.DeviceProvider.RemoveExcessiveLedsSupported);
ILayoutProvider? provider = _getLayoutProviders().FirstOrDefault(p => p.IsMatch(device));
if (provider == null)
_logger.Warning("Could not find a layout provider for type {LayoutType} of device {Device}", device.LayoutSelection.Type, device);
ArtemisLayout? layout = provider?.GetDeviceLayout(device);
if (layout != null && !layout.IsValid)
{
_logger.Warning("Got an invalid layout {Layout} from {LayoutProvider}", layout, provider!.GetType().FullName);
layout = null;
}
try
{
if (layout == null)
device.ApplyLayout(null, false, false);
else
provider?.ApplyLayout(device, layout);
}
catch (Exception e)
{
_logger.Error(e, "Failed to apply device layout");
}
UpdateLeds();
}
@ -230,7 +256,7 @@ internal class DeviceService : IDeviceService
device = new ArtemisDevice(rgbDevice, deviceProvider);
}
ApplyDeviceLayout(device, device.GetBestDeviceLayout());
LoadDeviceLayout(device);
return device;
}

View File

@ -43,11 +43,10 @@ public interface IDeviceService : IArtemisService
void AutoArrangeDevices();
/// <summary>
/// Apples the provided <see cref="ArtemisLayout" /> to the provided <see cref="ArtemisDevice" />
/// Apples the best available to the provided <see cref="ArtemisDevice" />
/// </summary>
/// <param name="device"></param>
/// <param name="layout"></param>
void ApplyDeviceLayout(ArtemisDevice device, ArtemisLayout? layout);
void LoadDeviceLayout(ArtemisDevice device);
/// <summary>
/// Enables the provided device

View File

@ -31,7 +31,7 @@ internal class PluginManagementService : IPluginManagementService
private readonly IPluginRepository _pluginRepository;
private readonly List<Plugin> _plugins;
private readonly IQueuedActionRepository _queuedActionRepository;
private FileSystemWatcher _hotReloadWatcher;
private FileSystemWatcher? _hotReloadWatcher;
private bool _disposed;
private bool _isElevated;
@ -57,7 +57,7 @@ internal class PluginManagementService : IPluginManagementService
// Remove the old directory if it exists
if (Directory.Exists(pluginDirectory.FullName))
pluginDirectory.DeleteRecursively();
pluginDirectory.Delete(true);
// Extract everything in the same archive directory to the unique plugin directory
Utilities.CreateAccessibleDirectory(pluginDirectory.FullName);

View File

@ -1,24 +1,49 @@
namespace Artemis.Core.Services;
/// <summary>
/// This readonly struct provides information about a process.
/// </summary>
public readonly struct ProcessInfo
{
#region Properties & Fields
/// <summary>
/// Gets the Identifier for the process.
/// </summary>
public readonly int ProcessId;
/// <summary>
/// Gets the name of the process.
/// </summary>
public readonly string ProcessName;
public readonly string ImageName; //TODO DarthAffe 01.09.2023: Do we need this if we can't get it through Process.GetProcesses()?
/// <summary>
/// Gets the Image Name of the Process.
/// </summary>
public readonly string ImageName; // TODO DarthAffe 01.09.2023: Do we need this if we can't get it through Process.GetProcesses()?
/// <summary>
/// Gets the Executable associated with the Process.
/// </summary>
public readonly string Executable;
#endregion
#region Constructors
/// <summary>
/// Initializes a new instance of the <see cref="ProcessInfo"/> struct.
/// </summary>
/// <param name="processId">The identifier for the process.</param>
/// <param name="processName">The name of the process.</param>
/// <param name="imageName">The Image Name of the process.</param>
/// <param name="executable">The executable associated with the process.</param>
public ProcessInfo(int processId, string processName, string imageName, string executable)
{
this.ProcessId = processId;
this.ProcessName = processName;
this.ImageName = imageName;
this.Executable = executable;
ProcessId = processId;
ProcessName = processName;
ImageName = imageName;
Executable = executable;
}
#endregion

View File

@ -75,8 +75,8 @@ internal class RenderService : IRenderService, IRenderer, IDisposable
_frameStopWatch.Restart();
try
{
OnFrameRendering(new FrameRenderingEventArgs(canvas, delta, _surfaceManager.Surface));
_coreRenderer.Render(canvas, delta);
OnFrameRendering(new FrameRenderingEventArgs(canvas, delta, _surfaceManager.Surface));
}
catch (Exception e)
{

View File

@ -1,6 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\Artemis.props" />
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<PreserveCompilationContext>false</PreserveCompilationContext>
@ -9,6 +7,6 @@
<ItemGroup>
<PackageReference Include="LiteDB" Version="5.0.17" />
<PackageReference Include="Serilog" Version="3.0.1" />
<PackageReference Include="Serilog" Version="3.1.1" />
</ItemGroup>
</Project>

View File

@ -22,11 +22,11 @@ public class DeviceEntity
public float GreenScale { get; set; }
public float BlueScale { get; set; }
public bool IsEnabled { get; set; }
public bool DisableDefaultLayout { get; set; }
public int PhysicalLayout { get; set; }
public string LogicalLayout { get; set; }
public string CustomLayoutPath { get; set; }
public string LayoutType { get; set; }
public string LayoutParameter { get; set; }
public List<DeviceInputIdentifierEntity> InputIdentifiers { get; set; }
public List<InputMappingEntity> InputMappings { get; set; }

View File

@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
namespace Artemis.Storage.Entities.Workshop;
@ -16,6 +17,6 @@ public class EntryEntity
public long ReleaseId { get; set; }
public string ReleaseVersion { get; set; }
public DateTimeOffset InstalledAt { get; set; }
public string LocalReference { get; set; }
public Dictionary<string,object> Metadata { get; set; }
}

View File

@ -0,0 +1,35 @@
using System.Collections.Generic;
using Artemis.Storage.Migrations.Interfaces;
using LiteDB;
namespace Artemis.Storage.Migrations;
public class M0023LayoutProviders : IStorageMigration
{
public int UserVersion => 23;
public void Apply(LiteRepository repository)
{
ILiteCollection<BsonDocument> deviceEntities = repository.Database.GetCollection("DeviceEntity");
List<BsonDocument> toUpdate = new();
foreach (BsonDocument bsonDocument in deviceEntities.FindAll())
{
if (bsonDocument.TryGetValue("CustomLayoutPath", out BsonValue customLayoutPath) && customLayoutPath.IsString && !string.IsNullOrEmpty(customLayoutPath.AsString))
{
bsonDocument.Add("LayoutType", new BsonValue("CustomPath"));
bsonDocument.Add("LayoutParameter", new BsonValue(customLayoutPath.AsString));
}
else if (bsonDocument.TryGetValue("DisableDefaultLayout", out BsonValue disableDefaultLayout) && disableDefaultLayout.AsBoolean)
bsonDocument.Add("LayoutType", new BsonValue("None"));
else
bsonDocument.Add("LayoutType", new BsonValue("Default"));
bsonDocument.Remove("CustomLayoutPath");
bsonDocument.Remove("DisableDefaultLayout");
toUpdate.Add(bsonDocument);
}
deviceEntities.Update(toUpdate);
}
}

View File

@ -1,6 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\Artemis.props" />
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net7.0</TargetFramework>
@ -17,14 +15,6 @@
<AvaloniaResource Include="Assets\**" />
<None Remove=".gitignore" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Avalonia" Version="$(AvaloniaVersion)" />
<PackageReference Include="Avalonia.Desktop" Version="$(AvaloniaVersion)" />
<!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.-->
<PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="$(AvaloniaVersion)" />
<PackageReference Include="Avalonia.ReactiveUI" Version="$(AvaloniaVersion)" />
<PackageReference Include="ReactiveUI" Version="19.4.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Artemis.Core\Artemis.Core.csproj" />
<ProjectReference Include="..\Artemis.UI\Artemis.UI.csproj" />

View File

@ -1,6 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\Artemis.props" />
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net7.0</TargetFramework>
@ -16,14 +14,6 @@
<AvaloniaResource Include="Assets\**" />
<None Remove=".gitignore" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Avalonia" Version="$(AvaloniaVersion)" />
<PackageReference Include="Avalonia.Desktop" Version="$(AvaloniaVersion)" />
<!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.-->
<PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="$(AvaloniaVersion)" />
<PackageReference Include="Avalonia.ReactiveUI" Version="$(AvaloniaVersion)" />
<PackageReference Include="ReactiveUI" Version="19.4.1" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Artemis.Core\Artemis.Core.csproj" />
<ProjectReference Include="..\Artemis.UI\Artemis.UI.csproj" />

View File

@ -1,6 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\Artemis.props" />
<PropertyGroup>
<OutputType>Library</OutputType>
<TargetFramework>net7.0</TargetFramework>
@ -12,19 +10,17 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Avalonia" Version="$(AvaloniaVersion)" />
<PackageReference Include="Avalonia" Version="11.0.6" />
<!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.-->
<PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="$(AvaloniaVersion)" />
<PackageReference Include="Avalonia.Controls.ItemsRepeater" Version="$(AvaloniaVersion)" />
<PackageReference Include="Avalonia.ReactiveUI" Version="$(AvaloniaVersion)" />
<PackageReference Include="Avalonia.Xaml.Behaviors" Version="$(AvaloniaBehavioursVersion)" />
<PackageReference Include="DynamicData" Version="7.14.2" />
<PackageReference Include="FluentAvaloniaUI" Version="$(FluentAvaloniaVersion)" />
<PackageReference Include="Material.Icons.Avalonia" Version="2.0.1" />
<PackageReference Include="ReactiveUI" Version="19.4.1" />
<PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="11.0.6" />
<PackageReference Include="Avalonia.Controls.ItemsRepeater" Version="11.0.6" />
<PackageReference Include="Avalonia.ReactiveUI" Version="11.0.6" />
<PackageReference Include="Avalonia.Xaml.Behaviors" Version="11.0.6" />
<PackageReference Include="DynamicData" Version="8.3.27" />
<PackageReference Include="FluentAvaloniaUI" Version="2.0.5" />
<PackageReference Include="Material.Icons.Avalonia" Version="2.1.0" />
<PackageReference Include="ReactiveUI" Version="19.5.39" />
<PackageReference Include="ReactiveUI.Validation" Version="3.1.7" />
<PackageReference Include="RGB.NET.Core" Version="$(RGBDotNetVersion)" />
<PackageReference Include="SkiaSharp" Version="$(SkiaSharpVersion)" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Artemis.Core\Artemis.Core.csproj" />

View File

@ -15,7 +15,7 @@ public class LostFocusTextBoxBindingBehavior : Behavior<TextBox>
/// <summary>
/// Gets or sets the value of the binding.
/// </summary>
public static readonly StyledProperty<string> TextProperty = AvaloniaProperty.Register<LostFocusTextBoxBindingBehavior, string>(
public static readonly StyledProperty<string?> TextProperty = AvaloniaProperty.Register<LostFocusTextBoxBindingBehavior, string?>(
"Text", defaultBindingMode: BindingMode.TwoWay);
static LostFocusTextBoxBindingBehavior()
@ -26,7 +26,7 @@ public class LostFocusTextBoxBindingBehavior : Behavior<TextBox>
/// <summary>
/// Gets or sets the value of the binding.
/// </summary>
public string Text
public string? Text
{
get => GetValue(TextProperty);
set => SetValue(TextProperty, value);

View File

@ -1,12 +1,11 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Artemis.Core;
using Artemis.Core.Services;
using Artemis.UI.Shared.Events;
using Artemis.UI.Shared.Extensions;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Input;
@ -36,7 +35,7 @@ public class DeviceVisualizer : Control
private ArtemisDevice? _oldDevice;
private bool _loading;
private Color[] _previousState = Array.Empty<Color>();
/// <inheritdoc />
public DeviceVisualizer()
{
@ -69,11 +68,7 @@ public class DeviceVisualizer : Control
// Render device and LED images
if (_deviceImage != null)
drawingContext.DrawImage(
_deviceImage,
new Rect(_deviceImage.Size),
new Rect(0, 0, Device.RgbDevice.ActualSize.Width, Device.RgbDevice.ActualSize.Height)
);
drawingContext.DrawImage(_deviceImage, new Rect(_deviceImage.Size), new Rect(0, 0, Device.RgbDevice.ActualSize.Width, Device.RgbDevice.ActualSize.Height));
if (!ShowColors)
return;
@ -163,7 +158,7 @@ public class DeviceVisualizer : Control
{
if (Device == null || float.IsNaN(Device.RgbDevice.ActualSize.Width) || float.IsNaN(Device.RgbDevice.ActualSize.Height))
return new Rect();
Rect deviceRect = new(0, 0, Device.RgbDevice.ActualSize.Width, Device.RgbDevice.ActualSize.Height);
Geometry geometry = new RectangleGeometry(deviceRect);
geometry.Transform = new RotateTransform(Device.Rotation);
@ -305,7 +300,7 @@ public class DeviceVisualizer : Control
{
_deviceImage = await Task.Run(() => GetDeviceImage(device));
}
catch (Exception e)
catch (Exception)
{
// ignored
}
@ -316,34 +311,15 @@ public class DeviceVisualizer : Control
private RenderTargetBitmap? GetDeviceImage(ArtemisDevice device)
{
string? path = device.Layout?.Image?.LocalPath;
if (path == null)
ArtemisLayout? layout = device.Layout;
if (layout == null)
return null;
if (BitmapCache.TryGetValue(path, out RenderTargetBitmap? existingBitmap))
if (BitmapCache.TryGetValue(layout.FilePath, out RenderTargetBitmap? existingBitmap))
return existingBitmap;
if (!File.Exists(path))
{
BitmapCache[path] = null;
return null;
}
// Create a bitmap that'll be used to render the device and LED images just once
// Render 4 times the actual size of the device to make sure things look sharp when zoomed in
RenderTargetBitmap renderTargetBitmap = new(new PixelSize((int) device.RgbDevice.ActualSize.Width * 2, (int) device.RgbDevice.ActualSize.Height * 2));
using DrawingContext context = renderTargetBitmap.CreateDrawingContext();
using Bitmap bitmap = new(path);
using Bitmap scaledBitmap = bitmap.CreateScaledBitmap(renderTargetBitmap.PixelSize);
context.DrawImage(scaledBitmap, new Rect(scaledBitmap.Size));
lock (_deviceVisualizerLeds)
{
foreach (DeviceVisualizerLed deviceVisualizerLed in _deviceVisualizerLeds)
deviceVisualizerLed.DrawBitmap(context, 2 * device.Scale);
}
// BitmapCache[path] = renderTargetBitmap;
RenderTargetBitmap renderTargetBitmap = layout.RenderLayout(false);
BitmapCache[layout.FilePath] = renderTargetBitmap;
return renderTargetBitmap;
}

View File

@ -1,9 +1,7 @@
using System;
using System.IO;
using Artemis.Core;
using Avalonia;
using Avalonia.Media;
using Avalonia.Media.Imaging;
using RGB.NET.Core;
using Color = Avalonia.Media.Color;
using Point = Avalonia.Point;
@ -30,27 +28,7 @@ internal class DeviceVisualizerLed
public ArtemisLed Led { get; }
public Geometry? DisplayGeometry { get; private set; }
public void DrawBitmap(DrawingContext drawingContext, double scale)
{
if (Led.Layout?.Image == null || !File.Exists(Led.Layout.Image.LocalPath))
return;
try
{
using Bitmap bitmap = new(Led.Layout.Image.LocalPath);
using Bitmap scaledBitmap = bitmap.CreateScaledBitmap(new PixelSize((Led.RgbLed.Size.Width * scale).RoundToInt(), (Led.RgbLed.Size.Height * scale).RoundToInt()));
drawingContext.DrawImage(
scaledBitmap,
new Rect(Led.RgbLed.Location.X * scale, Led.RgbLed.Location.Y * scale, scaledBitmap.Size.Width, scaledBitmap.Size.Height)
);
}
catch
{
// ignored
}
}
public void RenderGeometry(DrawingContext drawingContext)
{
if (DisplayGeometry == null)

View File

@ -19,32 +19,32 @@ namespace Artemis.UI.Shared.Pagination;
[TemplatePart("PART_PagesView", typeof(StackPanel))]
public partial class Pagination : TemplatedControl
{
private Button? _previousButton;
private Button? _nextButton;
private StackPanel? _pagesView;
/// <inheritdoc />
public Pagination()
{
PropertyChanged += OnPropertyChanged;
}
public Button? PreviousButton { get; set; }
public Button? NextButton { get; set; }
public StackPanel? PagesView { get; set; }
/// <inheritdoc />
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
if (PreviousButton != null)
PreviousButton.Click -= PreviousButtonOnClick;
if (NextButton != null)
NextButton.Click -= NextButtonOnClick;
if (_previousButton != null)
_previousButton.Click -= PreviousButtonOnClick;
if (_nextButton != null)
_nextButton.Click -= NextButtonOnClick;
PreviousButton = e.NameScope.Find<Button>("PART_PreviousButton");
NextButton = e.NameScope.Find<Button>("PART_NextButton");
PagesView = e.NameScope.Find<StackPanel>("PART_PagesView");
_previousButton = e.NameScope.Find<Button>("PART_PreviousButton");
_nextButton = e.NameScope.Find<Button>("PART_NextButton");
_pagesView = e.NameScope.Find<StackPanel>("PART_PagesView");
if (PreviousButton != null)
PreviousButton.Click += PreviousButtonOnClick;
if (NextButton != null)
NextButton.Click += NextButtonOnClick;
if (_previousButton != null)
_previousButton.Click += PreviousButtonOnClick;
if (_nextButton != null)
_nextButton.Click += NextButtonOnClick;
Update();
}
@ -69,40 +69,40 @@ public partial class Pagination : TemplatedControl
private void Update()
{
if (PagesView == null)
if (_pagesView == null)
return;
List<int> pages = GetPages(Value, Maximum);
// Remove extra children
while (PagesView.Children.Count > pages.Count)
while (_pagesView.Children.Count > pages.Count)
{
PagesView.Children.RemoveAt(PagesView.Children.Count - 1);
_pagesView.Children.RemoveAt(_pagesView.Children.Count - 1);
}
if (PagesView.Children.Count > pages.Count)
PagesView.Children.RemoveRange(0, PagesView.Children.Count - pages.Count);
if (_pagesView.Children.Count > pages.Count)
_pagesView.Children.RemoveRange(0, _pagesView.Children.Count - pages.Count);
// Add/modify children
for (int i = 0; i < pages.Count; i++)
{
int page = pages[i];
// -1 indicates an ellipsis (...)
if (page == -1)
{
if (PagesView.Children.ElementAtOrDefault(i) is not PaginationEllipsis)
if (_pagesView.Children.ElementAtOrDefault(i) is not PaginationEllipsis)
{
if (PagesView.Children.Count - 1 >= i)
PagesView.Children[i] = new PaginationEllipsis();
if (_pagesView.Children.Count - 1 >= i)
_pagesView.Children[i] = new PaginationEllipsis();
else
PagesView.Children.Add(new PaginationEllipsis());
_pagesView.Children.Add(new PaginationEllipsis());
}
}
// Anything else indicates a regular page
else
{
if (PagesView.Children.ElementAtOrDefault(i) is PaginationPage paginationPage)
if (_pagesView.Children.ElementAtOrDefault(i) is PaginationPage paginationPage)
{
paginationPage.Page = page;
paginationPage.Command = ReactiveCommand.Create(() => Value = page);
@ -110,14 +110,14 @@ public partial class Pagination : TemplatedControl
}
paginationPage = new PaginationPage {Page = page, Command = ReactiveCommand.Create(() => Value = page)};
if (PagesView.Children.Count - 1 >= i)
PagesView.Children[i] = paginationPage;
if (_pagesView.Children.Count - 1 >= i)
_pagesView.Children[i] = paginationPage;
else
PagesView.Children.Add(paginationPage);
_pagesView.Children.Add(paginationPage);
}
}
foreach (Control child in PagesView.Children)
foreach (Control child in _pagesView.Children)
{
if (child is PaginationPage paginationPage)
((IPseudoClasses) paginationPage.Classes).Set(":selected", paginationPage.Page == Value);

View File

@ -8,10 +8,17 @@ using ReactiveUI;
namespace Artemis.UI.Shared.TagsInput;
/// <summary>
/// Represents an input for tags.
/// </summary>
[TemplatePart("PART_TagInputBox", typeof(TextBox))]
public partial class TagsInput : TemplatedControl
{
public TextBox? TagInputBox { get; set; }
private TextBox? _tagInputBox;
/// <summary>
/// Gets the command that is to be called when removing a tag
/// </summary>
public ICommand RemoveTag { get; }
/// <inheritdoc />
@ -23,18 +30,18 @@ public partial class TagsInput : TemplatedControl
/// <inheritdoc />
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
if (TagInputBox != null)
if (_tagInputBox != null)
{
TagInputBox.KeyDown -= TagInputBoxOnKeyDown;
TagInputBox.TextChanging -= TagInputBoxOnTextChanging;
_tagInputBox.KeyDown -= TagInputBoxOnKeyDown;
_tagInputBox.TextChanging -= TagInputBoxOnTextChanging;
}
TagInputBox = e.NameScope.Find<TextBox>("PART_TagInputBox");
_tagInputBox = e.NameScope.Find<TextBox>("PART_TagInputBox");
if (TagInputBox != null)
if (_tagInputBox != null)
{
TagInputBox.KeyDown += TagInputBoxOnKeyDown;
TagInputBox.TextChanging += TagInputBoxOnTextChanging;
_tagInputBox.KeyDown += TagInputBoxOnKeyDown;
_tagInputBox.TextChanging += TagInputBoxOnTextChanging;
}
}
@ -42,21 +49,21 @@ public partial class TagsInput : TemplatedControl
{
Tags.Remove(t);
if (TagInputBox != null)
TagInputBox.IsEnabled = Tags.Count < MaxLength;
if (_tagInputBox != null)
_tagInputBox.IsEnabled = Tags.Count < MaxLength;
}
private void TagInputBoxOnTextChanging(object? sender, TextChangingEventArgs e)
{
if (TagInputBox?.Text == null)
if (_tagInputBox?.Text == null)
return;
TagInputBox.Text = CleanTagRegex().Replace(TagInputBox.Text.ToLower(), "");
_tagInputBox.Text = CleanTagRegex().Replace(_tagInputBox.Text.ToLower(), "");
}
private void TagInputBoxOnKeyDown(object? sender, KeyEventArgs e)
{
if (TagInputBox == null)
if (_tagInputBox == null)
return;
if (e.Key == Key.Space)
@ -64,13 +71,13 @@ public partial class TagsInput : TemplatedControl
if (e.Key != Key.Enter)
return;
if (string.IsNullOrWhiteSpace(TagInputBox.Text) || Tags.Contains(TagInputBox.Text) || Tags.Count >= MaxLength)
if (string.IsNullOrWhiteSpace(_tagInputBox.Text) || Tags.Contains(_tagInputBox.Text) || Tags.Count >= MaxLength)
return;
Tags.Add(CleanTagRegex().Replace(TagInputBox.Text.ToLower(), ""));
Tags.Add(CleanTagRegex().Replace(_tagInputBox.Text.ToLower(), ""));
TagInputBox.Text = "";
TagInputBox.IsEnabled = Tags.Count < MaxLength;
_tagInputBox.Text = "";
_tagInputBox.IsEnabled = Tags.Count < MaxLength;
}
[GeneratedRegex("[\\s\\-]+")]

View File

@ -2,6 +2,9 @@ using System;
namespace Artemis.UI.Shared;
/// <summary>
/// Represents errors that occur within the Artemis router.
/// </summary>
public class ArtemisRoutingException : Exception
{
/// <inheritdoc />

View File

@ -0,0 +1,139 @@
using System;
using System.IO;
using Artemis.Core;
using Avalonia;
using Avalonia.Media;
using Avalonia.Media.Imaging;
using RGB.NET.Core;
using Color = Avalonia.Media.Color;
using SolidColorBrush = Avalonia.Media.SolidColorBrush;
namespace Artemis.UI.Shared.Extensions;
/// <summary>
/// Provides extension methods for the <see cref="ArtemisLayout" /> type.
/// </summary>
public static class ArtemisLayoutExtensions
{
/// <summary>
/// Renders the layout to a bitmap.
/// </summary>
/// <param name="layout">The layout to render</param>
/// <returns>The resulting bitmap.</returns>
public static RenderTargetBitmap RenderLayout(this ArtemisLayout layout, bool previewLeds)
{
string? path = layout.Image?.LocalPath;
// Create a bitmap that'll be used to render the device and LED images just once
// Render 4 times the actual size of the device to make sure things look sharp when zoomed in
RenderTargetBitmap renderTargetBitmap = new(new PixelSize((int) layout.RgbLayout.Width * 2, (int) layout.RgbLayout.Height * 2));
using DrawingContext context = renderTargetBitmap.CreateDrawingContext();
// Draw device background
if (path != null && File.Exists(path))
{
using Bitmap bitmap = new(path);
using Bitmap scaledBitmap = bitmap.CreateScaledBitmap(renderTargetBitmap.PixelSize);
context.DrawImage(scaledBitmap, new Rect(scaledBitmap.Size));
}
// Draw LED images
foreach (ArtemisLedLayout led in layout.Leds)
{
string? ledPath = led.Image?.LocalPath;
if (ledPath == null || !File.Exists(ledPath))
continue;
using Bitmap bitmap = new(ledPath);
using Bitmap scaledBitmap = bitmap.CreateScaledBitmap(new PixelSize((led.RgbLayout.Width * 2).RoundToInt(), (led.RgbLayout.Height * 2).RoundToInt()));
context.DrawImage(scaledBitmap, new Rect(led.RgbLayout.X * 2, led.RgbLayout.Y * 2, scaledBitmap.Size.Width, scaledBitmap.Size.Height));
}
if (!previewLeds)
return renderTargetBitmap;
// Draw LED geometry using a rainbow gradient
ColorGradient colors = ColorGradient.GetUnicornBarf();
colors.ToggleSeamless();
context.PushTransform(Matrix.CreateScale(2, 2));
foreach (ArtemisLedLayout led in layout.Leds)
{
Geometry? geometry = CreateLedGeometry(led);
if (geometry == null)
continue;
Color color = colors.GetColor((led.RgbLayout.X + led.RgbLayout.Width / 2) / layout.RgbLayout.Width).ToColor();
SolidColorBrush fillBrush = new() {Color = color, Opacity = 0.4};
SolidColorBrush penBrush = new() {Color = color};
Pen pen = new(penBrush) {LineJoin = PenLineJoin.Round};
context.DrawGeometry(fillBrush, pen, geometry);
}
return renderTargetBitmap;
}
private static Geometry? CreateLedGeometry(ArtemisLedLayout led)
{
// The minimum required size for geometry to be created
if (led.RgbLayout.Width < 2 || led.RgbLayout.Height < 2)
return null;
switch (led.RgbLayout.Shape)
{
case Shape.Custom:
if (led.DeviceLayout.RgbLayout.Type is RGBDeviceType.Keyboard or RGBDeviceType.Keypad)
return CreateCustomGeometry(led, 2.0);
return CreateCustomGeometry(led, 1.0);
case Shape.Rectangle:
if (led.DeviceLayout.RgbLayout.Type is RGBDeviceType.Keyboard or RGBDeviceType.Keypad)
return CreateKeyCapGeometry(led);
return CreateRectangleGeometry(led);
case Shape.Circle:
return CreateCircleGeometry(led);
default:
throw new ArgumentOutOfRangeException();
}
}
private static RectangleGeometry CreateRectangleGeometry(ArtemisLedLayout led)
{
return new RectangleGeometry(new Rect(led.RgbLayout.X + 0.5, led.RgbLayout.Y + 0.5, led.RgbLayout.Width - 1, led.RgbLayout.Height - 1));
}
private static EllipseGeometry CreateCircleGeometry(ArtemisLedLayout led)
{
return new EllipseGeometry(new Rect(led.RgbLayout.X + 0.5, led.RgbLayout.Y + 0.5, led.RgbLayout.Width - 1, led.RgbLayout.Height - 1));
}
private static RectangleGeometry CreateKeyCapGeometry(ArtemisLedLayout led)
{
return new RectangleGeometry(new Rect(led.RgbLayout.X + 1, led.RgbLayout.Y + 1, led.RgbLayout.Width - 2, led.RgbLayout.Height - 2));
}
private static Geometry? CreateCustomGeometry(ArtemisLedLayout led, double deflateAmount)
{
try
{
if (led.RgbLayout.ShapeData == null)
return null;
double width = led.RgbLayout.Width - deflateAmount;
double height = led.RgbLayout.Height - deflateAmount;
Geometry geometry = Geometry.Parse(led.RgbLayout.ShapeData);
geometry.Transform = new TransformGroup
{
Children = new Transforms
{
new ScaleTransform(width, height),
new TranslateTransform(led.RgbLayout.X + deflateAmount / 2, led.RgbLayout.Y + deflateAmount / 2)
}
};
return geometry;
}
catch (Exception)
{
return CreateRectangleGeometry(led);
}
}
}

View File

@ -5,50 +5,60 @@ using System.Threading;
using System.Threading.Tasks;
using Artemis.UI.Shared.Utilities;
namespace Artemis.UI.Shared.Extensions
namespace Artemis.UI.Shared.Extensions;
/// <summary>
/// Provides extension methods for the <see cref="HttpClient" /> type.
/// </summary>
public static class HttpClientProgressExtensions
{
public static class HttpClientProgressExtensions
/// <summary>
/// Send a GET request to the specified Uri with an HTTP completion option and a cancellation token as an asynchronous operation.
/// </summary>
/// <param name="client">The HTTP client to use.</param>
/// <param name="requestUrl">The Uri the request is sent to.</param>
/// <param name="destination">The destination stream.</param>
/// <param name="progress">The progress instance to use for progress indication.</param>
/// <param name="cancellationToken">A cancellation token that can be used by other objects or threads to receive notice of cancellation.</param>
public static async Task DownloadDataAsync(this HttpClient client, string requestUrl, Stream destination, IProgress<StreamProgress>? progress, CancellationToken cancellationToken)
{
public static async Task DownloadDataAsync(this HttpClient client, string requestUrl, Stream destination, IProgress<StreamProgress>? progress, CancellationToken cancellationToken)
using HttpResponseMessage response = await client.GetAsync(requestUrl, HttpCompletionOption.ResponseHeadersRead, cancellationToken);
response.EnsureSuccessStatusCode();
long? contentLength = response.Content.Headers.ContentLength;
await using Stream download = await response.Content.ReadAsStreamAsync(cancellationToken);
// no progress... no contentLength... very sad
if (progress is null || !contentLength.HasValue)
{
using HttpResponseMessage response = await client.GetAsync(requestUrl, HttpCompletionOption.ResponseHeadersRead, cancellationToken);
response.EnsureSuccessStatusCode();
long? contentLength = response.Content.Headers.ContentLength;
await using Stream download = await response.Content.ReadAsStreamAsync(cancellationToken);
// no progress... no contentLength... very sad
if (progress is null || !contentLength.HasValue)
{
await download.CopyToAsync(destination, cancellationToken);
return;
}
// Such progress and contentLength much reporting Wow!
await download.CopyToAsync(destination, 81920, progress, contentLength, cancellationToken);
await download.CopyToAsync(destination, cancellationToken);
return;
}
static async Task CopyToAsync(this Stream source, Stream destination, int bufferSize, IProgress<StreamProgress> progress, long? contentLength, CancellationToken cancellationToken)
{
if (bufferSize < 0)
throw new ArgumentOutOfRangeException(nameof(bufferSize));
if (source is null)
throw new ArgumentNullException(nameof(source));
if (!source.CanRead)
throw new InvalidOperationException($"'{nameof(source)}' is not readable.");
if (destination == null)
throw new ArgumentNullException(nameof(destination));
if (!destination.CanWrite)
throw new InvalidOperationException($"'{nameof(destination)}' is not writable.");
// Such progress and contentLength much reporting Wow!
await download.CopyToAsync(destination, 81920, progress, contentLength, cancellationToken);
}
byte[] buffer = new byte[bufferSize];
long totalBytesRead = 0;
int bytesRead;
while ((bytesRead = await source.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false)) != 0)
{
await destination.WriteAsync(buffer, 0, bytesRead, cancellationToken).ConfigureAwait(false);
totalBytesRead += bytesRead;
progress?.Report(new StreamProgress(totalBytesRead, contentLength ?? totalBytesRead));
}
private static async Task CopyToAsync(this Stream source, Stream destination, int bufferSize, IProgress<StreamProgress> progress, long? contentLength, CancellationToken cancellationToken)
{
if (bufferSize < 0)
throw new ArgumentOutOfRangeException(nameof(bufferSize));
if (source is null)
throw new ArgumentNullException(nameof(source));
if (!source.CanRead)
throw new InvalidOperationException($"'{nameof(source)}' is not readable.");
if (destination == null)
throw new ArgumentNullException(nameof(destination));
if (!destination.CanWrite)
throw new InvalidOperationException($"'{nameof(destination)}' is not writable.");
byte[] buffer = new byte[bufferSize];
long totalBytesRead = 0;
int bytesRead;
while ((bytesRead = await source.ReadAsync(buffer, 0, buffer.Length, cancellationToken).ConfigureAwait(false)) != 0)
{
await destination.WriteAsync(buffer, 0, bytesRead, cancellationToken).ConfigureAwait(false);
totalBytesRead += bytesRead;
progress?.Report(new StreamProgress(totalBytesRead, contentLength ?? totalBytesRead));
}
}
}

View File

@ -107,7 +107,7 @@ public class ContentDialogBuilder
_contentDialog.IsSecondaryButtonEnabled = builder.Command.CanExecute(builder.CommandParameter);
builder.Command.CanExecuteChanged += (_, _) => _contentDialog.IsSecondaryButtonEnabled = builder.Command.CanExecute(builder.CommandParameter);
}
return this;
}
@ -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

@ -61,7 +61,10 @@ internal class WindowService : IWindowService
public async Task ShowDialogAsync(object viewModel)
{
Window? parent = GetCurrentWindow();
if (parent == null)
throw new ArtemisSharedUIException("Failed to get the current window.");
string name = viewModel.GetType().FullName!.Split('`')[0].Replace("ViewModel", "View");
Type? type = viewModel.GetType().Assembly.GetType(name);
@ -100,6 +103,9 @@ internal class WindowService : IWindowService
public async Task<TResult> ShowDialogAsync<TResult>(DialogViewModelBase<TResult> viewModel)
{
Window? parent = GetCurrentWindow();
if (parent == null)
throw new ArtemisSharedUIException("Failed to get the current window.");
string name = viewModel.GetType().FullName!.Split('`')[0].Replace("ViewModel", "View");
Type? type = viewModel.GetType().Assembly.GetType(name);

View File

@ -4,7 +4,7 @@
<StackPanel>
<Border Classes="card" Margin="20">
<TextBlock>I'm in a panel yo!</TextBlock>
</Border>
</Border>
<Border Classes="card" Margin="20">
<StackPanel>
<TextBlock>I'm in a panel yo!</TextBlock>

View File

@ -27,22 +27,7 @@
<Setter Property="Maximum" Value="{x:Static system:Double.MaxValue}"></Setter>
<Setter Property="Minimum" Value="{x:Static system:Double.MinValue}"></Setter>
</Style>
<Style Selector="controls|NumberBox /template/ TextBox.NumberBoxTextBoxStyle /template/ TextBlock#PART_Prefix">
<Setter Property="Foreground" Value="{DynamicResource TextControlForegroundDisabled}"></Setter>
<Setter Property="Margin" Value="-4 0 -12 0"></Setter>
</Style>
<Style Selector="controls|NumberBox /template/ TextBox.NumberBoxTextBoxStyle /template/ TextBlock#PART_Suffix">
<Setter Property="Foreground" Value="{DynamicResource TextControlForegroundDisabled}"></Setter>
<Setter Property="Margin" Value="-12 0 0 0"></Setter>
</Style>
<Style Selector="controls|NumberBox.condensed /template/ TextBox.NumberBoxTextBoxStyle /template/ TextBlock#PART_Prefix">
<Setter Property="Margin" Value="0 0 -4 0"></Setter>
</Style>
<Style Selector="controls|NumberBox.condensed /template/ TextBox.NumberBoxTextBoxStyle /template/ TextBlock#PART_Suffix">
<Setter Property="Margin" Value="-4 0 0 0"></Setter>
</Style>
<Style Selector="controls|NumberBox /template/ TextBox.NumberBoxTextBoxStyle">
<Style Selector="controls|NumberBox /template/ TextBox#InputBox">
<Setter Property="attachedProperties:TextBoxAssist.PrefixText" Value="{TemplateBinding attachedProperties:NumberBoxAssist.PrefixText}"></Setter>
<Setter Property="attachedProperties:TextBoxAssist.SuffixText" Value="{TemplateBinding attachedProperties:NumberBoxAssist.SuffixText}"></Setter>
</Style>

View File

@ -5,7 +5,21 @@
<Design.PreviewWith>
<Border Padding="20">
<StackPanel Spacing="20">
<TextBox Width="200" Height="150" AcceptsReturn="True" xml:space="preserve">asdasdas asd
asdasd
asd
asd
asd
as
fdsfsdf
sdg
sdg
sdg
</TextBox>
<TextBox Width="200" Height="150" AcceptsReturn="True" xml:space="preserve">asdasdas asd
asdasd
asd
as</TextBox>
<!-- Add Controls for Previewer Here -->
<TextBox Text="99999999"
@ -46,12 +60,10 @@
<Border Margin="{TemplateBinding BorderThickness}">
<Grid ColumnDefinitions="Auto,*,Auto">
<ContentPresenter Grid.Column="0"
Grid.ColumnSpan="1"
Content="{TemplateBinding InnerLeftContent}" />
<ContentPresenter Grid.Column="0" Content="{TemplateBinding InnerLeftContent}" />
<Grid x:Name="PART_InnerGrid"
Grid.Column="1"
RowDefinitions="Auto,Auto"
RowDefinitions="Auto,*"
ColumnDefinitions="Auto,*,Auto"
Cursor="IBeam"
Margin="{TemplateBinding Padding}">

View File

@ -18,6 +18,9 @@ public static class UI
{
private static readonly BehaviorSubject<bool> MicaEnabledSubject = new(false);
/// <summary>
/// Gets the background event loop scheduler.
/// </summary>
public static EventLoopScheduler BackgroundScheduler = new(ts => new Thread(ts));
static UI()

View File

@ -1,119 +0,0 @@
// Heavily based on:
// SkyClip
// - ProgressableStreamContent.cs
// --------------------------------------------------------------------
// Author: Jeff Hansen <jeff@jeffijoe.com>
// Copyright (C) Jeff Hansen 2015. All rights reserved.
using System;
using System.IO;
using System.Net;
using System.Net.Http;
using System.Threading;
using System.Threading.Tasks;
namespace Artemis.UI.Shared.Utilities;
/// <summary>
/// Provides HTTP content based on a stream with support for IProgress.
/// </summary>
public class ProgressableStreamContent : StreamContent
{
private const int DEFAULT_BUFFER_SIZE = 4096;
private readonly int _bufferSize;
private readonly IProgress<StreamProgress> _progress;
private readonly Stream _streamToWrite;
private bool _contentConsumed;
/// <summary>
/// Initializes a new instance of the <see cref="ProgressableStreamContent" /> class.
/// </summary>
/// <param name="streamToWrite">The stream to write.</param>
/// <param name="progress">The downloader.</param>
public ProgressableStreamContent(Stream streamToWrite, IProgress<StreamProgress> progress) : this(streamToWrite, DEFAULT_BUFFER_SIZE, progress)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="ProgressableStreamContent" /> class.
/// </summary>
/// <param name="streamToWrite">The stream to write.</param>
/// <param name="bufferSize">The buffer size.</param>
/// <param name="progress">The downloader.</param>
public ProgressableStreamContent(Stream streamToWrite, int bufferSize, IProgress<StreamProgress> progress) : base(streamToWrite, bufferSize)
{
if (bufferSize <= 0)
throw new ArgumentOutOfRangeException(nameof(bufferSize));
_streamToWrite = streamToWrite;
_bufferSize = bufferSize;
_progress = progress;
}
/// <inheritdoc />
protected override void Dispose(bool disposing)
{
if (disposing)
_streamToWrite.Dispose();
base.Dispose(disposing);
}
/// <inheritdoc />
protected override async Task SerializeToStreamAsync(Stream stream, TransportContext? context)
{
await SerializeToStreamAsync(stream, context, CancellationToken.None);
}
/// <inheritdoc />
protected override async Task SerializeToStreamAsync(Stream stream, TransportContext? context, CancellationToken cancellationToken)
{
PrepareContent();
byte[] buffer = new byte[_bufferSize];
long size = _streamToWrite.Length;
int uploaded = 0;
await using (_streamToWrite)
{
while (!cancellationToken.IsCancellationRequested)
{
int length = await _streamToWrite.ReadAsync(buffer, cancellationToken);
if (length <= 0)
break;
uploaded += length;
_progress.Report(new StreamProgress(uploaded, size));
await stream.WriteAsync(buffer, 0, length, cancellationToken);
}
}
}
/// <inheritdoc />
protected override bool TryComputeLength(out long length)
{
length = _streamToWrite.Length;
return true;
}
/// <summary>
/// Prepares the content.
/// </summary>
/// <exception cref="System.InvalidOperationException">The stream has already been read.</exception>
private void PrepareContent()
{
if (_contentConsumed)
{
// If the content needs to be written to a target stream a 2nd time, then the stream must support
// seeking (e.g. a FileStream), otherwise the stream can't be copied a second time to a target
// stream (e.g. a NetworkStream).
if (_streamToWrite.CanSeek)
_streamToWrite.Position = 0;
else
throw new InvalidOperationException("The stream has already been read.");
}
_contentConsumed = true;
}
}

View File

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using Artemis.UI.Shared.Events;
using FluentAvalonia.UI.Controls;
@ -100,6 +101,24 @@ public abstract class ValidatableViewModelBase : ReactiveValidationObject, IActi
/// <inheritdoc />
public ViewModelActivator Activator { get; } = new();
/// <summary>
/// Raises the property changed event for the provided property.
/// </summary>
/// <param name="args">The event arguments containing the name of the property that changed.</param>
protected virtual void OnPropertyChanged(PropertyChangedEventArgs args)
{
this.RaisePropertyChanged(args.PropertyName);
}
/// <summary>
/// Raises the property changing event for the provided property.
/// </summary>
/// <param name="args">The event arguments containing the name of the property that is changing.</param>
protected virtual void OnPropertyChanging(PropertyChangingEventArgs args)
{
this.RaisePropertyChanging(args.PropertyName);
}
}
/// <summary>
@ -153,4 +172,22 @@ public abstract class ViewModelBase : ReactiveObject
this.RaisePropertyChanged(propertyName);
return newValue;
}
/// <summary>
/// Raises the property changed event for the provided property.
/// </summary>
/// <param name="args">The event arguments containing the name of the property that changed.</param>
protected virtual void OnPropertyChanged(PropertyChangedEventArgs args)
{
this.RaisePropertyChanged(args.PropertyName);
}
/// <summary>
/// Raises the property changing event for the provided property.
/// </summary>
/// <param name="args">The event arguments containing the name of the property that is changing.</param>
protected virtual void OnPropertyChanging(PropertyChangingEventArgs args)
{
this.RaisePropertyChanging(args.PropertyName);
}
}

View File

@ -1,6 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\Artemis.props" />
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFramework>net7.0-windows10.0.17763.0</TargetFramework>
@ -23,18 +21,13 @@
</None>
</ItemGroup>
<ItemGroup>
<PackageReference Include="Avalonia" Version="$(AvaloniaVersion)" />
<PackageReference Include="Avalonia.Desktop" Version="$(AvaloniaVersion)" />
<!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.-->
<PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="$(AvaloniaVersion)" />
<PackageReference Include="Avalonia.ReactiveUI" Version="$(AvaloniaVersion)" />
<PackageReference Include="Avalonia.Win32" Version="$(AvaloniaVersion)" />
<PackageReference Include="Avalonia.Win32" Version="11.0.6" />
<PackageReference Include="Microsoft.Toolkit.Uwp.Notifications" Version="7.1.3" />
<PackageReference Include="Microsoft.Win32" Version="2.0.1" />
<!-- Note: Do NOT upgrade this compatibility package to 8.X before updating to net8, it WILL break -->
<PackageReference Include="Microsoft.Windows.Compatibility" Version="7.0.5" />
<PackageReference Include="RawInput.Sharp" Version="0.1.1" />
<PackageReference Include="ReactiveUI" Version="19.4.1" />
<PackageReference Include="SkiaSharp.Vulkan.SharpVk" Version="$(SkiaSharpVersion)" />
<PackageReference Include="RawInput.Sharp" Version="0.1.3" />
<PackageReference Include="SkiaSharp.Vulkan.SharpVk" Version="2.88.7" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\Artemis.Core\Artemis.Core.csproj" />

View File

@ -9,17 +9,19 @@ namespace Artemis.UI.Windows.Providers;
public class ProtocolProvider : IProtocolProvider
{
/// <inheritdoc />
public async Task AssociateWithProtocol(string protocol)
public Task AssociateWithProtocol(string protocol)
{
string key = $"HKEY_CURRENT_USER\\Software\\Classes\\{protocol}";
Registry.SetValue($"{key}", null, "URL:artemis protocol");
Registry.SetValue($"{key}", "URL Protocol", "");
Registry.SetValue($"{key}\\DefaultIcon", null, $"\"{Constants.ExecutablePath}\",1");
Registry.SetValue($"{key}\\shell\\open\\command", null, $"\"{Constants.ExecutablePath}\", \"--route=%1\"");
return Task.CompletedTask;
}
/// <inheritdoc />
public async Task DisassociateWithProtocol(string protocol)
public Task DisassociateWithProtocol(string protocol)
{
try
{
@ -30,5 +32,7 @@ public class ProtocolProvider : IProtocolProvider
{
// Ignore errors (which means that the protocol wasn't associated before)
}
return Task.CompletedTask;
}
}

View File

@ -1,6 +1,4 @@
<Project Sdk="Microsoft.NET.Sdk">
<Import Project="..\Artemis.props" />
<PropertyGroup>
<OutputType>Library</OutputType>
<TargetFramework>net7.0</TargetFramework>
@ -18,31 +16,19 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="AsyncImageLoader.Avalonia" Version="3.2.0" />
<PackageReference Include="Avalonia" Version="$(AvaloniaVersion)" />
<PackageReference Include="Avalonia.AvaloniaEdit" Version="11.0.1" />
<PackageReference Include="Avalonia.Controls.PanAndZoom" Version="11.0.0" />
<PackageReference Include="Avalonia.Desktop" Version="$(AvaloniaVersion)" />
<!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.-->
<PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="$(AvaloniaVersion)" />
<PackageReference Include="Avalonia.Controls.ItemsRepeater" Version="$(AvaloniaVersion)" />
<PackageReference Include="Avalonia.ReactiveUI" Version="$(AvaloniaVersion)" />
<PackageReference Include="Avalonia.Xaml.Behaviors" Version="$(AvaloniaBehavioursVersion)" />
<PackageReference Include="AsyncImageLoader.Avalonia" Version="3.2.1" />
<PackageReference Include="Avalonia.AvaloniaEdit" Version="11.0.6" />
<PackageReference Include="Avalonia.Controls.PanAndZoom" Version="11.0.0.2" />
<PackageReference Include="Avalonia.Desktop" Version="11.0.6" />
<PackageReference Include="Avalonia.Skia.Lottie" Version="11.0.0" />
<PackageReference Include="AvaloniaEdit.TextMate" Version="11.0.1" />
<PackageReference Include="DryIoc.dll" Version="5.4.1" />
<PackageReference Include="DynamicData" Version="7.14.2" />
<PackageReference Include="FluentAvaloniaUI" Version="$(FluentAvaloniaVersion)" />
<PackageReference Include="Flurl.Http" Version="3.2.4" />
<PackageReference Include="AvaloniaEdit.TextMate" Version="11.0.6" />
<PackageReference Include="Markdown.Avalonia.Tight" Version="11.0.2" />
<PackageReference Include="Material.Icons.Avalonia" Version="2.0.1" />
<PackageReference Include="Octopus.Octodiff" Version="2.0.326" />
<PackageReference Include="ReactiveUI" Version="19.4.1" />
<PackageReference Include="ReactiveUI.Validation" Version="3.1.7" />
<PackageReference Include="RGB.NET.Core" Version="$(RGBDotNetVersion)" />
<PackageReference Include="RGB.NET.Layout" Version="$(RGBDotNetVersion)" />
<PackageReference Include="SkiaSharp" Version="$(SkiaSharpVersion)" />
<PackageReference Include="Splat.DryIoc" Version="14.7.1" />
<PackageReference Include="Octopus.Octodiff" Version="2.0.544" />
<PackageReference Include="PropertyChanged.SourceGenerator" Version="1.1.0">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Splat.DryIoc" Version="14.8.12" />
<PackageReference Include="TextMateSharp.Grammars" Version="1.0.56" />
</ItemGroup>

View File

@ -33,9 +33,9 @@ public static class ArtemisBootstrapper
{
if (_application != null || _container != null)
throw new ArtemisUIException("UI already bootstrapped");
Utilities.PrepareFirstLaunch();
application.RequestedThemeVariant = ThemeVariant.Dark;
_application = application;
_container = new Container(rules => rules
@ -52,7 +52,7 @@ public static class ArtemisBootstrapper
configureServices?.Invoke(_container);
_container.UseDryIocDependencyResolver();
Logger.Sink = _container.Resolve<SerilogAvaloniaSink>();
return _container;
}
@ -64,7 +64,7 @@ public static class ArtemisBootstrapper
if (_application.ApplicationLifetime is not IClassicDesktopStyleApplicationLifetime desktop)
return;
Constants.StartupArguments = new ReadOnlyCollection<string>(new List<string>(desktop.Args));
Constants.StartupArguments = new ReadOnlyCollection<string>(desktop.Args != null ? new List<string>(desktop.Args) : new List<string>());
// Don't shut down when the last window closes, we might still be active in the tray
desktop.ShutdownMode = ShutdownMode.OnExplicitShutdown;

File diff suppressed because one or more lines are too long

View File

@ -1,6 +1,7 @@
using System.Reflection;
using Artemis.UI.DryIoc.Factories;
using Artemis.UI.DryIoc.InstanceProviders;
using Artemis.UI.Screens.Device.Layout.LayoutProviders;
using Artemis.UI.Screens.VisualScripting;
using Artemis.UI.Services.Interfaces;
using Artemis.UI.Services.Updating;
@ -26,6 +27,7 @@ public static class ContainerExtensions
container.RegisterMany(thisAssembly, type => type.IsAssignableTo<ViewModelBase>(), setup: Setup.With(preventDisposal: true));
container.RegisterMany(thisAssembly, type => type.IsAssignableTo<IToolViewModel>() && type.IsInterface, setup: Setup.With(preventDisposal: true));
container.RegisterMany(thisAssembly, type => type.IsAssignableTo<ILayoutProviderViewModel>() && type.IsInterface, setup: Setup.With(preventDisposal: true));
container.RegisterMany(thisAssembly, type => type.IsAssignableTo<IVmFactory>() && type != typeof(PropertyVmFactory));
container.Register<NodeScriptWindowViewModelBase, NodeScriptWindowViewModel>(Reuse.Singleton);

View File

@ -1,13 +1,14 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.ObjectModel;
using System.Reactive;
using Artemis.Core;
using Artemis.Core.LayerBrushes;
using Artemis.Core.LayerEffects;
using Artemis.Core.ScriptingProviders;
using Artemis.UI.Routing;
using Artemis.UI.Screens.Device;
using Artemis.UI.Screens.Device.General;
using Artemis.UI.Screens.Device.InputMappings;
using Artemis.UI.Screens.Device.Layout;
using Artemis.UI.Screens.Device.Leds;
using Artemis.UI.Screens.Plugins;
using Artemis.UI.Screens.Plugins.Features;
using Artemis.UI.Screens.Plugins.Prerequisites;
@ -27,12 +28,8 @@ using Artemis.UI.Screens.Sidebar;
using Artemis.UI.Screens.SurfaceEditor;
using Artemis.UI.Screens.VisualScripting;
using Artemis.UI.Screens.VisualScripting.Pins;
using Artemis.UI.Shared;
using Artemis.UI.Shared.Routing;
using Artemis.WebClient.Updating;
using DryIoc;
using DynamicData;
using Material.Icons;
using ReactiveUI;
namespace Artemis.UI.DryIoc.Factories;
@ -51,48 +48,49 @@ public interface IDeviceVmFactory : IVmFactory
InputMappingsTabViewModel InputMappingsTabViewModel(ArtemisDevice device, ObservableCollection<ArtemisLed> selectedLeds);
DeviceGeneralTabViewModel DeviceGeneralTabViewModel(ArtemisDevice device);
}
public class DeviceFactory : IDeviceVmFactory
{
private readonly IContainer _container;
public DeviceFactory(IContainer container)
{
_container = container;
}
public DevicePropertiesViewModel DevicePropertiesViewModel(ArtemisDevice device)
{
return _container.Resolve<DevicePropertiesViewModel>(new object[] { device });
return _container.Resolve<DevicePropertiesViewModel>(new object[] {device});
}
public DeviceSettingsViewModel DeviceSettingsViewModel(ArtemisDevice device, DevicesTabViewModel devicesTabViewModel)
{
return _container.Resolve<DeviceSettingsViewModel>(new object[] { device, devicesTabViewModel });
return _container.Resolve<DeviceSettingsViewModel>(new object[] {device, devicesTabViewModel});
}
public DeviceDetectInputViewModel DeviceDetectInputViewModel(ArtemisDevice device)
{
return _container.Resolve<DeviceDetectInputViewModel>(new object[] { device });
return _container.Resolve<DeviceDetectInputViewModel>(new object[] {device});
}
public DeviceLayoutTabViewModel DeviceLayoutTabViewModel(ArtemisDevice device)
{
return _container.Resolve<DeviceLayoutTabViewModel>(new object[] { device });
return _container.Resolve<DeviceLayoutTabViewModel>(new object[] {device});
}
public DeviceLedsTabViewModel DeviceLedsTabViewModel(ArtemisDevice device, ObservableCollection<ArtemisLed> selectedLeds)
{
return _container.Resolve<DeviceLedsTabViewModel>(new object[] { device, selectedLeds });
return _container.Resolve<DeviceLedsTabViewModel>(new object[] {device, selectedLeds});
}
public InputMappingsTabViewModel InputMappingsTabViewModel(ArtemisDevice device, ObservableCollection<ArtemisLed> selectedLeds)
{
return _container.Resolve<InputMappingsTabViewModel>(new object[] { device, selectedLeds });
return _container.Resolve<InputMappingsTabViewModel>(new object[] {device, selectedLeds});
}
public DeviceGeneralTabViewModel DeviceGeneralTabViewModel(ArtemisDevice device)
{
return _container.Resolve<DeviceGeneralTabViewModel>(new object[] { device });
return _container.Resolve<DeviceGeneralTabViewModel>(new object[] {device});
}
}
@ -102,28 +100,29 @@ public interface ISettingsVmFactory : IVmFactory
PluginViewModel PluginViewModel(Plugin plugin, ReactiveCommand<Unit, Unit>? reload);
PluginFeatureViewModel PluginFeatureViewModel(PluginFeatureInfo pluginFeatureInfo, bool showShield);
}
public class SettingsVmFactory : ISettingsVmFactory
{
private readonly IContainer _container;
public SettingsVmFactory(IContainer container)
{
_container = container;
}
public PluginSettingsViewModel PluginSettingsViewModel(Plugin plugin)
{
return _container.Resolve<PluginSettingsViewModel>(new object[] { plugin });
return _container.Resolve<PluginSettingsViewModel>(new object[] {plugin});
}
public PluginViewModel PluginViewModel(Plugin plugin, ReactiveCommand<Unit, Unit>? reload)
{
return _container.Resolve<PluginViewModel>(new object?[] { plugin, reload });
return _container.Resolve<PluginViewModel>(new object?[] {plugin, reload});
}
public PluginFeatureViewModel PluginFeatureViewModel(PluginFeatureInfo pluginFeatureInfo, bool showShield)
{
return _container.Resolve<PluginFeatureViewModel>(new object[] { pluginFeatureInfo, showShield });
return _container.Resolve<PluginFeatureViewModel>(new object[] {pluginFeatureInfo, showShield});
}
}
@ -132,31 +131,33 @@ public interface ISidebarVmFactory : IVmFactory
SidebarCategoryViewModel SidebarCategoryViewModel(ProfileCategory profileCategory);
SidebarProfileConfigurationViewModel SidebarProfileConfigurationViewModel(ProfileConfiguration profileConfiguration);
}
public class SidebarVmFactory : ISidebarVmFactory
{
private readonly IContainer _container;
public SidebarVmFactory(IContainer container)
{
_container = container;
}
public SidebarCategoryViewModel SidebarCategoryViewModel(ProfileCategory profileCategory)
{
return _container.Resolve<SidebarCategoryViewModel>(new object[] { profileCategory });
return _container.Resolve<SidebarCategoryViewModel>(new object[] {profileCategory});
}
public SidebarProfileConfigurationViewModel SidebarProfileConfigurationViewModel(ProfileConfiguration profileConfiguration)
{
return _container.Resolve<SidebarProfileConfigurationViewModel>(new object[] { profileConfiguration });
return _container.Resolve<SidebarProfileConfigurationViewModel>(new object[] {profileConfiguration});
}
}
public interface ISurfaceVmFactory : IVmFactory
{
SurfaceDeviceViewModel SurfaceDeviceViewModel(ArtemisDevice device, SurfaceEditorViewModel surfaceEditorViewModel);
ListDeviceViewModel ListDeviceViewModel(ArtemisDevice device, SurfaceEditorViewModel surfaceEditorViewModel);
ListDeviceViewModel ListDeviceViewModel(ArtemisDevice device);
}
public class SurfaceVmFactory : ISurfaceVmFactory
{
private readonly IContainer _container;
@ -168,12 +169,12 @@ public class SurfaceVmFactory : ISurfaceVmFactory
public SurfaceDeviceViewModel SurfaceDeviceViewModel(ArtemisDevice device, SurfaceEditorViewModel surfaceEditorViewModel)
{
return _container.Resolve<SurfaceDeviceViewModel>(new object[] { device, surfaceEditorViewModel });
return _container.Resolve<SurfaceDeviceViewModel>(new object[] {device, surfaceEditorViewModel});
}
public ListDeviceViewModel ListDeviceViewModel(ArtemisDevice device, SurfaceEditorViewModel surfaceEditorViewModel)
public ListDeviceViewModel ListDeviceViewModel(ArtemisDevice device)
{
return _container.Resolve<ListDeviceViewModel>(new object[] { device, surfaceEditorViewModel });
return _container.Resolve<ListDeviceViewModel>(new object[] {device});
}
}
@ -181,6 +182,7 @@ public interface IPrerequisitesVmFactory : IVmFactory
{
PluginPrerequisiteViewModel PluginPrerequisiteViewModel(PluginPrerequisite pluginPrerequisite, bool uninstall);
}
public class PrerequisitesVmFactory : IPrerequisitesVmFactory
{
private readonly IContainer _container;
@ -192,7 +194,7 @@ public class PrerequisitesVmFactory : IPrerequisitesVmFactory
public PluginPrerequisiteViewModel PluginPrerequisiteViewModel(PluginPrerequisite pluginPrerequisite, bool uninstall)
{
return _container.Resolve<PluginPrerequisiteViewModel>(new object[] { pluginPrerequisite, uninstall });
return _container.Resolve<PluginPrerequisiteViewModel>(new object[] {pluginPrerequisite, uninstall});
}
}
@ -204,6 +206,7 @@ public interface IProfileEditorVmFactory : IVmFactory
LayerShapeVisualizerViewModel LayerShapeVisualizerViewModel(Layer layer);
LayerVisualizerViewModel LayerVisualizerViewModel(Layer layer);
}
public class ProfileEditorVmFactory : IProfileEditorVmFactory
{
private readonly IContainer _container;
@ -215,27 +218,27 @@ public class ProfileEditorVmFactory : IProfileEditorVmFactory
public FolderTreeItemViewModel FolderTreeItemViewModel(TreeItemViewModel? parent, Folder folder)
{
return _container.Resolve<FolderTreeItemViewModel>(new object?[] { parent, folder });
return _container.Resolve<FolderTreeItemViewModel>(new object?[] {parent, folder});
}
public LayerShapeVisualizerViewModel LayerShapeVisualizerViewModel(Layer layer)
{
return _container.Resolve<LayerShapeVisualizerViewModel>(new object[] { layer });
return _container.Resolve<LayerShapeVisualizerViewModel>(new object[] {layer});
}
public LayerTreeItemViewModel LayerTreeItemViewModel(TreeItemViewModel? parent, Layer layer)
{
return _container.Resolve<LayerTreeItemViewModel>(new object?[] { parent, layer });
return _container.Resolve<LayerTreeItemViewModel>(new object?[] {parent, layer});
}
public LayerVisualizerViewModel LayerVisualizerViewModel(Layer layer)
{
return _container.Resolve<LayerVisualizerViewModel>(new object[] { layer });
return _container.Resolve<LayerVisualizerViewModel>(new object[] {layer});
}
public ProfileEditorViewModel ProfileEditorViewModel(IScreen hostScreen)
{
return _container.Resolve<ProfileEditorViewModel>(new object[] { hostScreen });
return _container.Resolve<ProfileEditorViewModel>(new object[] {hostScreen});
}
}
@ -251,6 +254,7 @@ public interface ILayerPropertyVmFactory : IVmFactory
TimelineViewModel TimelineViewModel(ObservableCollection<PropertyGroupViewModel> propertyGroupViewModels);
TimelineGroupViewModel TimelineGroupViewModel(PropertyGroupViewModel propertyGroupViewModel);
}
public class LayerPropertyVmFactory : ILayerPropertyVmFactory
{
private readonly IContainer _container;
@ -262,37 +266,37 @@ public class LayerPropertyVmFactory : ILayerPropertyVmFactory
public PropertyViewModel PropertyViewModel(ILayerProperty layerProperty)
{
return _container.Resolve<PropertyViewModel>(new object[] { layerProperty });
return _container.Resolve<PropertyViewModel>(new object[] {layerProperty});
}
public PropertyGroupViewModel PropertyGroupViewModel(LayerPropertyGroup layerPropertyGroup)
{
return _container.Resolve<PropertyGroupViewModel>(new object[] { layerPropertyGroup });
return _container.Resolve<PropertyGroupViewModel>(new object[] {layerPropertyGroup});
}
public PropertyGroupViewModel PropertyGroupViewModel(LayerPropertyGroup layerPropertyGroup, BaseLayerBrush layerBrush)
{
return _container.Resolve<PropertyGroupViewModel>(new object[] { layerPropertyGroup, layerBrush });
return _container.Resolve<PropertyGroupViewModel>(new object[] {layerPropertyGroup, layerBrush});
}
public PropertyGroupViewModel PropertyGroupViewModel(LayerPropertyGroup layerPropertyGroup, BaseLayerEffect layerEffect)
{
return _container.Resolve<PropertyGroupViewModel>(new object[] { layerPropertyGroup, layerEffect });
return _container.Resolve<PropertyGroupViewModel>(new object[] {layerPropertyGroup, layerEffect});
}
public TreeGroupViewModel TreeGroupViewModel(PropertyGroupViewModel propertyGroupViewModel)
{
return _container.Resolve<TreeGroupViewModel>(new object[] { propertyGroupViewModel });
return _container.Resolve<TreeGroupViewModel>(new object[] {propertyGroupViewModel});
}
public TimelineViewModel TimelineViewModel(ObservableCollection<PropertyGroupViewModel> propertyGroupViewModels)
{
return _container.Resolve<TimelineViewModel>(new object[] { propertyGroupViewModels });
return _container.Resolve<TimelineViewModel>(new object[] {propertyGroupViewModels});
}
public TimelineGroupViewModel TimelineGroupViewModel(PropertyGroupViewModel propertyGroupViewModel)
{
return _container.Resolve<TimelineGroupViewModel>(new object[] { propertyGroupViewModel });
return _container.Resolve<TimelineGroupViewModel>(new object[] {propertyGroupViewModel});
}
}
@ -300,6 +304,7 @@ public interface IDataBindingVmFactory : IVmFactory
{
DataBindingViewModel DataBindingViewModel();
}
public class DataBindingVmFactory : IDataBindingVmFactory
{
private readonly IContainer _container;
@ -333,6 +338,7 @@ public interface INodeVmFactory : IVmFactory
InputPinCollectionViewModel InputPinCollectionViewModel(IPinCollection inputPinCollection, NodeScriptViewModel nodeScriptViewModel);
OutputPinCollectionViewModel OutputPinCollectionViewModel(IPinCollection outputPinCollection, NodeScriptViewModel nodeScriptViewModel);
}
public class NodeVmFactory : INodeVmFactory
{
private readonly IContainer _container;
@ -344,47 +350,47 @@ public class NodeVmFactory : INodeVmFactory
public NodeScriptViewModel NodeScriptViewModel(NodeScript nodeScript, bool isPreview)
{
return _container.Resolve<NodeScriptViewModel>(new object[] { nodeScript, isPreview });
return _container.Resolve<NodeScriptViewModel>(new object[] {nodeScript, isPreview});
}
public NodePickerViewModel NodePickerViewModel(NodeScript nodeScript)
{
return _container.Resolve<NodePickerViewModel>(new object[] { nodeScript });
return _container.Resolve<NodePickerViewModel>(new object[] {nodeScript});
}
public NodeViewModel NodeViewModel(NodeScriptViewModel nodeScriptViewModel, INode node)
{
return _container.Resolve<NodeViewModel>(new object[] { nodeScriptViewModel, node });
return _container.Resolve<NodeViewModel>(new object[] {nodeScriptViewModel, node});
}
public CableViewModel CableViewModel(NodeScriptViewModel nodeScriptViewModel, IPin from, IPin to)
{
return _container.Resolve<CableViewModel>(new object[] { nodeScriptViewModel, from, to });
return _container.Resolve<CableViewModel>(new object[] {nodeScriptViewModel, from, to});
}
public DragCableViewModel DragCableViewModel(PinViewModel pinViewModel)
{
return _container.Resolve<DragCableViewModel>(new object[] { pinViewModel });
return _container.Resolve<DragCableViewModel>(new object[] {pinViewModel});
}
public InputPinViewModel InputPinViewModel(IPin inputPin, NodeScriptViewModel nodeScriptViewModel)
{
return _container.Resolve<InputPinViewModel>(new object[] { inputPin, nodeScriptViewModel });
return _container.Resolve<InputPinViewModel>(new object[] {inputPin, nodeScriptViewModel});
}
public OutputPinViewModel OutputPinViewModel(IPin outputPin, NodeScriptViewModel nodeScriptViewModel)
{
return _container.Resolve<OutputPinViewModel>(new object[] { outputPin, nodeScriptViewModel });
return _container.Resolve<OutputPinViewModel>(new object[] {outputPin, nodeScriptViewModel});
}
public InputPinCollectionViewModel InputPinCollectionViewModel(IPinCollection inputPinCollection, NodeScriptViewModel nodeScriptViewModel)
{
return _container.Resolve<InputPinCollectionViewModel>(new object[] { inputPinCollection, nodeScriptViewModel });
return _container.Resolve<InputPinCollectionViewModel>(new object[] {inputPinCollection, nodeScriptViewModel});
}
public OutputPinCollectionViewModel OutputPinCollectionViewModel(IPinCollection outputPinCollection, NodeScriptViewModel nodeScriptViewModel)
{
return _container.Resolve<OutputPinCollectionViewModel>(new object[] { outputPinCollection, nodeScriptViewModel });
return _container.Resolve<OutputPinCollectionViewModel>(new object[] {outputPinCollection, nodeScriptViewModel});
}
}
@ -395,6 +401,7 @@ public interface IConditionVmFactory : IVmFactory
StaticConditionViewModel StaticConditionViewModel(StaticCondition staticCondition);
EventConditionViewModel EventConditionViewModel(EventCondition eventCondition);
}
public class ConditionVmFactory : IConditionVmFactory
{
private readonly IContainer _container;
@ -406,22 +413,22 @@ public class ConditionVmFactory : IConditionVmFactory
public AlwaysOnConditionViewModel AlwaysOnConditionViewModel(AlwaysOnCondition alwaysOnCondition)
{
return _container.Resolve<AlwaysOnConditionViewModel>(new object[] { alwaysOnCondition });
return _container.Resolve<AlwaysOnConditionViewModel>(new object[] {alwaysOnCondition});
}
public PlayOnceConditionViewModel PlayOnceConditionViewModel(PlayOnceCondition playOnceCondition)
{
return _container.Resolve<PlayOnceConditionViewModel>(new object[] { playOnceCondition });
return _container.Resolve<PlayOnceConditionViewModel>(new object[] {playOnceCondition});
}
public StaticConditionViewModel StaticConditionViewModel(StaticCondition staticCondition)
{
return _container.Resolve<StaticConditionViewModel>(new object[] { staticCondition });
return _container.Resolve<StaticConditionViewModel>(new object[] {staticCondition});
}
public EventConditionViewModel EventConditionViewModel(EventCondition eventCondition)
{
return _container.Resolve<EventConditionViewModel>(new object[] { eventCondition });
return _container.Resolve<EventConditionViewModel>(new object[] {eventCondition});
}
}
@ -432,6 +439,7 @@ public interface ILayerHintVmFactory : IVmFactory
KeyboardSectionAdaptionHintViewModel KeyboardSectionAdaptionHintViewModel(Layer layer, KeyboardSectionAdaptionHint adaptionHint);
SingleLedAdaptionHintViewModel SingleLedAdaptionHintViewModel(Layer layer, SingleLedAdaptionHint adaptionHint);
}
public class LayerHintVmFactory : ILayerHintVmFactory
{
private readonly IContainer _container;
@ -443,22 +451,22 @@ public class LayerHintVmFactory : ILayerHintVmFactory
public CategoryAdaptionHintViewModel CategoryAdaptionHintViewModel(Layer layer, CategoryAdaptionHint adaptionHint)
{
return _container.Resolve<CategoryAdaptionHintViewModel>(new object[] { layer, adaptionHint });
return _container.Resolve<CategoryAdaptionHintViewModel>(new object[] {layer, adaptionHint});
}
public DeviceAdaptionHintViewModel DeviceAdaptionHintViewModel(Layer layer, DeviceAdaptionHint adaptionHint)
{
return _container.Resolve<DeviceAdaptionHintViewModel>(new object[] { layer, adaptionHint });
return _container.Resolve<DeviceAdaptionHintViewModel>(new object[] {layer, adaptionHint});
}
public KeyboardSectionAdaptionHintViewModel KeyboardSectionAdaptionHintViewModel(Layer layer, KeyboardSectionAdaptionHint adaptionHint)
{
return _container.Resolve<KeyboardSectionAdaptionHintViewModel>(new object[] { layer, adaptionHint });
return _container.Resolve<KeyboardSectionAdaptionHintViewModel>(new object[] {layer, adaptionHint});
}
public SingleLedAdaptionHintViewModel SingleLedAdaptionHintViewModel(Layer layer, SingleLedAdaptionHint adaptionHint)
{
return _container.Resolve<SingleLedAdaptionHintViewModel>(new object[] { layer, adaptionHint });
return _container.Resolve<SingleLedAdaptionHintViewModel>(new object[] {layer, adaptionHint});
}
}
@ -467,6 +475,7 @@ public interface IScriptVmFactory : IVmFactory
ScriptConfigurationViewModel ScriptConfigurationViewModel(ScriptConfiguration scriptConfiguration);
ScriptConfigurationViewModel ScriptConfigurationViewModel(Profile profile, ScriptConfiguration scriptConfiguration);
}
public class ScriptVmFactory : IScriptVmFactory
{
private readonly IContainer _container;
@ -478,12 +487,12 @@ public class ScriptVmFactory : IScriptVmFactory
public ScriptConfigurationViewModel ScriptConfigurationViewModel(ScriptConfiguration scriptConfiguration)
{
return _container.Resolve<ScriptConfigurationViewModel>(new object[] { scriptConfiguration });
return _container.Resolve<ScriptConfigurationViewModel>(new object[] {scriptConfiguration});
}
public ScriptConfigurationViewModel ScriptConfigurationViewModel(Profile profile, ScriptConfiguration scriptConfiguration)
{
return _container.Resolve<ScriptConfigurationViewModel>(new object[] { profile, scriptConfiguration });
return _container.Resolve<ScriptConfigurationViewModel>(new object[] {profile, scriptConfiguration});
}
}
@ -491,6 +500,7 @@ public interface IReleaseVmFactory : IVmFactory
{
ReleaseViewModel ReleaseListViewModel(IGetReleases_PublishedReleases_Nodes release);
}
public class ReleaseVmFactory : IReleaseVmFactory
{
private readonly IContainer _container;
@ -499,9 +509,9 @@ public class ReleaseVmFactory : IReleaseVmFactory
{
_container = container;
}
public ReleaseViewModel ReleaseListViewModel(IGetReleases_PublishedReleases_Nodes release)
{
return _container.Resolve<ReleaseViewModel>(new object[] { release });
return _container.Resolve<ReleaseViewModel>(new object[] {release});
}
}

View File

@ -6,32 +6,28 @@ using Artemis.UI.Screens.Debugger.Logs;
using Artemis.UI.Screens.Debugger.Performance;
using Artemis.UI.Screens.Debugger.Render;
using Artemis.UI.Screens.Debugger.Routing;
using Artemis.UI.Screens.Debugger.Workshop;
using Artemis.UI.Services.Interfaces;
using Artemis.UI.Shared;
using PropertyChanged.SourceGenerator;
using ReactiveUI;
namespace Artemis.UI.Screens.Debugger;
public class DebugViewModel : ActivatableViewModelBase, IScreen
public partial class DebugViewModel : ActivatableViewModelBase, IScreen
{
private ViewModelBase _selectedItem;
[Notify] private ViewModelBase _selectedItem;
public DebugViewModel(IDebugService debugService, RenderDebugViewModel render, DataModelDebugViewModel dataModel, PerformanceDebugViewModel performance, RoutingDebugViewModel routing, LogsDebugViewModel logs)
public DebugViewModel(IDebugService debugService, RenderDebugViewModel render, DataModelDebugViewModel dataModel, PerformanceDebugViewModel performance, RoutingDebugViewModel routing, WorkshopDebugViewModel workshop, LogsDebugViewModel logs)
{
Items = new ObservableCollection<ViewModelBase> {render, dataModel, performance, routing, logs};
Items = new ObservableCollection<ViewModelBase> {render, dataModel, performance, routing, workshop, logs};
_selectedItem = render;
this.WhenActivated(d => Disposable.Create(debugService.ClearDebugger).DisposeWith(d));
}
public ObservableCollection<ViewModelBase> Items { get; }
public ViewModelBase SelectedItem
{
get => _selectedItem;
set => RaiseAndSetIfChanged(ref _selectedItem, value);
}
public void Activate()
{
OnActivationRequested();

View File

@ -12,21 +12,21 @@ using Artemis.UI.Shared.DataModelVisualization.Shared;
using Artemis.UI.Shared.Services;
using Avalonia.Threading;
using DynamicData;
using PropertyChanged.SourceGenerator;
using ReactiveUI;
namespace Artemis.UI.Screens.Debugger.DataModel;
public class DataModelDebugViewModel : ActivatableViewModelBase
public partial class DataModelDebugViewModel : ActivatableViewModelBase
{
private readonly IDataModelUIService _dataModelUIService;
private readonly IPluginManagementService _pluginManagementService;
private readonly DispatcherTimer _updateTimer;
private bool _isModuleFilterEnabled;
private DataModelPropertiesViewModel? _mainDataModel;
private string? _propertySearch;
private Module? _selectedModule;
private bool _slowUpdates;
[Notify] private DataModelPropertiesViewModel? _mainDataModel;
[Notify] private string? _propertySearch;
public DataModelDebugViewModel(IDataModelUIService dataModelUIService, IPluginManagementService pluginManagementService)
{
@ -57,18 +57,6 @@ public class DataModelDebugViewModel : ActivatableViewModelBase
});
}
public DataModelPropertiesViewModel? MainDataModel
{
get => _mainDataModel;
set => RaiseAndSetIfChanged(ref _mainDataModel, value);
}
public string? PropertySearch
{
get => _propertySearch;
set => RaiseAndSetIfChanged(ref _propertySearch, value);
}
public bool SlowUpdates
{
get => _slowUpdates;

View File

@ -1,16 +1,17 @@
using Artemis.Core;
using Artemis.UI.Shared;
using PropertyChanged.SourceGenerator;
namespace Artemis.UI.Screens.Debugger.Performance;
public class PerformanceDebugMeasurementViewModel : ViewModelBase
public partial class PerformanceDebugMeasurementViewModel : ViewModelBase
{
private string? _average;
private string? _last;
private string? _max;
private string? _min;
private string? _percentile;
private string? _count;
[Notify] private string? _average;
[Notify] private string? _last;
[Notify] private string? _max;
[Notify] private string? _min;
[Notify] private string? _percentile;
[Notify] private string? _count;
public PerformanceDebugMeasurementViewModel(ProfilingMeasurement measurement)
{
@ -19,42 +20,6 @@ public class PerformanceDebugMeasurementViewModel : ViewModelBase
public ProfilingMeasurement Measurement { get; }
public string? Last
{
get => _last;
set => RaiseAndSetIfChanged(ref _last, value);
}
public string? Average
{
get => _average;
set => RaiseAndSetIfChanged(ref _average, value);
}
public string? Min
{
get => _min;
set => RaiseAndSetIfChanged(ref _min, value);
}
public string? Max
{
get => _max;
set => RaiseAndSetIfChanged(ref _max, value);
}
public string? Percentile
{
get => _percentile;
set => RaiseAndSetIfChanged(ref _percentile, value);
}
public string? Count
{
get => _count;
set => RaiseAndSetIfChanged(ref _count, value);
}
public void Update()
{
Last = Measurement.GetLast().TotalMilliseconds + " ms";

View File

@ -7,20 +7,21 @@ using Artemis.Core;
using Artemis.Core.Services;
using Artemis.UI.Shared;
using Avalonia.Threading;
using PropertyChanged.SourceGenerator;
using ReactiveUI;
using SkiaSharp;
namespace Artemis.UI.Screens.Debugger.Performance;
public class PerformanceDebugViewModel : ActivatableViewModelBase
public partial class PerformanceDebugViewModel : ActivatableViewModelBase
{
private readonly IRenderService _renderService;
private readonly IPluginManagementService _pluginManagementService;
private readonly DispatcherTimer _updateTimer;
private double _currentFps;
private string? _renderer;
private int _renderHeight;
private int _renderWidth;
[Notify] private double _currentFps;
[Notify] private string? _renderer;
[Notify] private int _renderHeight;
[Notify] private int _renderWidth;
public PerformanceDebugViewModel(IRenderService renderService, IPluginManagementService pluginManagementService)
{
@ -59,31 +60,7 @@ public class PerformanceDebugViewModel : ActivatableViewModelBase
}
public ObservableCollection<PerformanceDebugPluginViewModel> Items { get; } = new();
public double CurrentFps
{
get => _currentFps;
set => RaiseAndSetIfChanged(ref _currentFps, value);
}
public int RenderWidth
{
get => _renderWidth;
set => RaiseAndSetIfChanged(ref _renderWidth, value);
}
public int RenderHeight
{
get => _renderHeight;
set => RaiseAndSetIfChanged(ref _renderHeight, value);
}
public string? Renderer
{
get => _renderer;
set => RaiseAndSetIfChanged(ref _renderer, value);
}
private void HandleActivation()
{
Renderer = Constants.ManagedGraphicsContext != null ? Constants.ManagedGraphicsContext.GetType().Name : "Software";

View File

@ -4,21 +4,21 @@ using Artemis.Core;
using Artemis.Core.Services;
using Artemis.UI.Shared;
using Avalonia.Media.Imaging;
using PropertyChanged.SourceGenerator;
using ReactiveUI;
using SkiaSharp;
namespace Artemis.UI.Screens.Debugger.Render;
public class RenderDebugViewModel : ActivatableViewModelBase
public partial class RenderDebugViewModel : ActivatableViewModelBase
{
private readonly IRenderService _renderService;
private double _currentFps;
private Bitmap? _currentFrame;
private string? _frameTargetPath;
private string? _renderer;
private int _renderHeight;
private int _renderWidth;
[Notify] private double _currentFps;
[Notify] private Bitmap? _currentFrame;
[Notify] private string? _renderer;
[Notify] private int _renderHeight;
[Notify] private int _renderWidth;
public RenderDebugViewModel(IRenderService renderService)
{
@ -33,36 +33,6 @@ public class RenderDebugViewModel : ActivatableViewModelBase
});
}
public Bitmap? CurrentFrame
{
get => _currentFrame;
set => RaiseAndSetIfChanged(ref _currentFrame, value);
}
public double CurrentFps
{
get => _currentFps;
set => RaiseAndSetIfChanged(ref _currentFps, value);
}
public int RenderWidth
{
get => _renderWidth;
set => RaiseAndSetIfChanged(ref _renderWidth, value);
}
public int RenderHeight
{
get => _renderHeight;
set => RaiseAndSetIfChanged(ref _renderHeight, value);
}
public string? Renderer
{
get => _renderer;
set => RaiseAndSetIfChanged(ref _renderer, value);
}
private void HandleActivation()
{
Renderer = Constants.ManagedGraphicsContext != null ? Constants.ManagedGraphicsContext.GetType().Name : "Software";

View File

@ -11,18 +11,19 @@ using Artemis.UI.Shared.Routing;
using Avalonia.Controls.Documents;
using Avalonia.Media;
using Avalonia.Threading;
using PropertyChanged.SourceGenerator;
using ReactiveUI;
using Serilog.Events;
using Serilog.Formatting.Display;
namespace Artemis.UI.Screens.Debugger.Routing;
public class RoutingDebugViewModel : ActivatableViewModelBase
public partial class RoutingDebugViewModel : ActivatableViewModelBase
{
private readonly IRouter _router;
private const int MAX_ENTRIES = 1000;
private readonly MessageTemplateTextFormatter _formatter;
private string? _route;
[Notify] private string? _route;
public RoutingDebugViewModel(IRouter router)
{
@ -49,12 +50,6 @@ public class RoutingDebugViewModel : ActivatableViewModelBase
public InlineCollection Lines { get; } = new();
public ReactiveCommand<Unit, Unit> Navigate { get; }
public string? Route
{
get => _route;
set => RaiseAndSetIfChanged(ref _route, value);
}
private void OnLogEventAdded(object? sender, LogEventEventArgs e)
{
Dispatcher.UIThread.Post(() => AddLogEvent(e.LogEvent));

View File

@ -0,0 +1,32 @@
<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:workshop="clr-namespace:Artemis.UI.Screens.Debugger.Workshop"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Artemis.UI.Screens.Debugger.Workshop.WorkshopDebugView"
x:DataType="workshop:WorkshopDebugViewModel">
<ScrollViewer Classes="with-padding">
<StackPanel>
<Label>Workshop Status</Label>
<Border Classes="card-condensed">
<SelectableTextBlock Text="{CompiledBinding WorkshopStatus}" FontFamily="{StaticResource RobotoMono}" FontSize="13" />
</Border>
<Label Margin="0 10 0 0">Auth token (DO NOT SHARE)</Label>
<Border Classes="card-condensed">
<SelectableTextBlock Text="{CompiledBinding Token}" FontFamily="{StaticResource RobotoMono}" FontSize="13" TextWrapping="Wrap" />
</Border>
<Label Margin="0 10 0 0">Email verified</Label>
<Border Classes="card-condensed">
<SelectableTextBlock Text="{CompiledBinding EmailVerified}" FontFamily="{StaticResource RobotoMono}" FontSize="13" />
</Border>
<Label Margin="0 10 0 0">Claims</Label>
<Border Classes="card-condensed">
<SelectableTextBlock Text="{CompiledBinding Claims}" FontFamily="{StaticResource RobotoMono}" FontSize="13" />
</Border>
</StackPanel>
</ScrollViewer>
</UserControl>

View File

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

View File

@ -0,0 +1,30 @@
using System.Threading;
using Artemis.UI.Extensions;
using Artemis.UI.Shared;
using Artemis.WebClient.Workshop.Services;
using Newtonsoft.Json;
using PropertyChanged.SourceGenerator;
namespace Artemis.UI.Screens.Debugger.Workshop;
public partial class WorkshopDebugViewModel : ActivatableViewModelBase
{
[Notify] private string? _token;
[Notify] private bool _emailVerified;
[Notify] private string? _claims;
[Notify] private IWorkshopService.WorkshopStatus? _workshopStatus;
public WorkshopDebugViewModel(IWorkshopService workshopService, IAuthenticationService authenticationService)
{
DisplayName = "Workshop";
this.WhenActivatedAsync(async _ =>
{
Token = await authenticationService.GetBearer();
EmailVerified = authenticationService.GetIsEmailVerified();
Claims = JsonConvert.SerializeObject(authenticationService.Claims, Formatting.Indented);
WorkshopStatus = await workshopService.GetWorkshopStatus(CancellationToken.None);
});
}
}

View File

@ -6,16 +6,17 @@ using Artemis.Core;
using Artemis.Core.Services;
using Artemis.UI.DryIoc.Factories;
using Artemis.UI.Shared;
using PropertyChanged.SourceGenerator;
using ReactiveUI;
using RGB.NET.Core;
using SkiaSharp;
namespace Artemis.UI.Screens.Device;
public class DevicePropertiesViewModel : DialogViewModelBase<object>
public partial class DevicePropertiesViewModel : DialogViewModelBase<object>
{
private readonly IDeviceVmFactory _deviceVmFactory;
private ArtemisDevice _device;
[Notify] private ArtemisDevice _device;
public DevicePropertiesViewModel(ArtemisDevice device, IRenderService renderService, IDeviceService deviceService, IDeviceVmFactory deviceVmFactory)
{
@ -42,12 +43,6 @@ public class DevicePropertiesViewModel : DialogViewModelBase<object>
ClearSelectedLeds = ReactiveCommand.Create(ExecuteClearSelectedLeds);
}
public ArtemisDevice Device
{
get => _device;
set => RaiseAndSetIfChanged(ref _device, value);
}
public ObservableCollection<ArtemisLed> SelectedLeds { get; }
public ObservableCollection<ActivatableViewModelBase> Tabs { get; }
public ReactiveCommand<Unit, Unit> ClearSelectedLeds { get; }

View File

@ -8,18 +8,19 @@ using Artemis.UI.Shared;
using Artemis.UI.Shared.Services;
using Avalonia.Threading;
using Humanizer;
using PropertyChanged.SourceGenerator;
using ReactiveUI;
using RGB.NET.Core;
namespace Artemis.UI.Screens.Device;
public class DeviceSettingsViewModel : ActivatableViewModelBase
public partial class DeviceSettingsViewModel : ActivatableViewModelBase
{
private readonly IDeviceService _deviceService;
private readonly DevicesTabViewModel _devicesTabViewModel;
private readonly IDeviceVmFactory _deviceVmFactory;
private readonly IWindowService _windowService;
private bool _togglingDevice;
[Notify] private bool _togglingDevice;
public DeviceSettingsViewModel(ArtemisDevice device, DevicesTabViewModel devicesTabViewModel, IDeviceService deviceService, IWindowService windowService, IDeviceVmFactory deviceVmFactory)
{
@ -51,12 +52,6 @@ public class DeviceSettingsViewModel : ActivatableViewModelBase
set => Dispatcher.UIThread.InvokeAsync(async () => await UpdateIsDeviceEnabled(value));
}
public bool TogglingDevice
{
get => _togglingDevice;
set => RaiseAndSetIfChanged(ref _togglingDevice, value);
}
public void IdentifyDevice()
{
_deviceService.IdentifyDevice(Device);

View File

@ -5,9 +5,10 @@
xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
xmlns:converters="clr-namespace:Artemis.UI.Shared.Converters;assembly=Artemis.UI.Shared"
xmlns:device="clr-namespace:Artemis.UI.Screens.Device"
xmlns:general="clr-namespace:Artemis.UI.Screens.Device.General"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="650"
x:Class="Artemis.UI.Screens.Device.DeviceGeneralTabView"
x:DataType="device:DeviceGeneralTabViewModel">
x:Class="Artemis.UI.Screens.Device.General.DeviceGeneralTabView"
x:DataType="general:DeviceGeneralTabViewModel">
<UserControl.Resources>
<converters:SKColorToColorConverter x:Key="SKColorToColorConverter" />
</UserControl.Resources>

View File

@ -1,7 +1,6 @@
using Avalonia.Controls;
using Avalonia.ReactiveUI;
namespace Artemis.UI.Screens.Device;
namespace Artemis.UI.Screens.Device.General;
public partial class DeviceGeneralTabView : ReactiveUserControl<DeviceGeneralTabViewModel>
{
public DeviceGeneralTabView()

View File

@ -1,48 +1,41 @@
using Artemis.UI.Shared;
using Artemis.UI.Shared;
using System;
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Reactive.Disposables;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Artemis.Core;
using Artemis.Core.Services;
using Artemis.UI.Screens.Device.Layout;
using Artemis.UI.Shared;
using Artemis.UI.Shared.Services;
using PropertyChanged.SourceGenerator;
using ReactiveUI;
using RGB.NET.Core;
using SkiaSharp;
namespace Artemis.UI.Screens.Device;
namespace Artemis.UI.Screens.Device.General;
public class DeviceGeneralTabViewModel : ActivatableViewModelBase
public partial class DeviceGeneralTabViewModel : ActivatableViewModelBase
{
private readonly ICoreService _coreService;
private readonly IDeviceService _deviceService;
private readonly IRenderService _renderService;
private readonly IWindowService _windowService;
private readonly List<DeviceCategory> _categories;
private readonly float _initialBlueScale;
private readonly float _initialGreenScale;
private readonly float _initialRedScale;
private int _rotation;
private float _scale;
private int _x;
private int _y;
[Notify] private int _rotation;
[Notify] private float _scale;
[Notify] private int _x;
[Notify] private int _y;
[Notify] private float _redScale;
[Notify] private float _greenScale;
[Notify] private float _blueScale;
[Notify] private SKColor _currentColor;
[Notify] private bool _displayOnDevices;
private float _redScale;
private float _greenScale;
private float _blueScale;
private SKColor _currentColor;
private bool _displayOnDevices;
public DeviceGeneralTabViewModel(ArtemisDevice device, ICoreService coreService, IDeviceService deviceService, IRenderService renderService, IWindowService windowService)
public DeviceGeneralTabViewModel(ArtemisDevice device, IDeviceService deviceService, IRenderService renderService, IWindowService windowService)
{
_coreService = coreService;
_deviceService = deviceService;
_renderService = renderService;
_windowService = windowService;
@ -50,10 +43,10 @@ public class DeviceGeneralTabViewModel : ActivatableViewModelBase
Device = device;
DisplayName = "General";
X = (int)Device.X;
Y = (int)Device.Y;
X = (int) Device.X;
Y = (int) Device.Y;
Scale = Device.Scale;
Rotation = (int)Device.Rotation;
Rotation = (int) Device.Rotation;
RedScale = Device.RedScale * 100f;
GreenScale = Device.GreenScale * 100f;
BlueScale = Device.BlueScale * 100f;
@ -64,7 +57,7 @@ public class DeviceGeneralTabViewModel : ActivatableViewModelBase
_initialGreenScale = Device.GreenScale;
_initialBlueScale = Device.BlueScale;
this.WhenAnyValue(x => x.RedScale, x => x.GreenScale, x => x.BlueScale).Subscribe(_ => ApplyScaling());
this.WhenAnyValue<DeviceGeneralTabViewModel, float, float, float>(x => x.RedScale, x => x.GreenScale, x => x.BlueScale).Subscribe(_ => ApplyScaling());
this.WhenActivated(d =>
{
@ -81,31 +74,7 @@ public class DeviceGeneralTabViewModel : ActivatableViewModelBase
public bool RequiresManualSetup => !Device.DeviceProvider.CanDetectPhysicalLayout || !Device.DeviceProvider.CanDetectLogicalLayout;
public ArtemisDevice Device { get; }
public int X
{
get => _x;
set => RaiseAndSetIfChanged(ref _x, value);
}
public int Y
{
get => _y;
set => RaiseAndSetIfChanged(ref _y, value);
}
public float Scale
{
get => _scale;
set => RaiseAndSetIfChanged(ref _scale, value);
}
public int Rotation
{
get => _rotation;
set => RaiseAndSetIfChanged(ref _rotation, value);
}
public bool IsKeyboard => Device.DeviceType == RGBDeviceType.Keyboard;
public bool HasDeskCategory
@ -137,37 +106,7 @@ public class DeviceGeneralTabViewModel : ActivatableViewModelBase
get => GetCategory(DeviceCategory.Peripherals);
set => SetCategory(DeviceCategory.Peripherals, value);
}
public float RedScale
{
get => _redScale;
set => RaiseAndSetIfChanged(ref _redScale, value);
}
public float GreenScale
{
get => _greenScale;
set => RaiseAndSetIfChanged(ref _greenScale, value);
}
public float BlueScale
{
get => _blueScale;
set => RaiseAndSetIfChanged(ref _blueScale, value);
}
public SKColor CurrentColor
{
get => _currentColor;
set => RaiseAndSetIfChanged(ref _currentColor, value);
}
public bool DisplayOnDevices
{
get => _displayOnDevices;
set => RaiseAndSetIfChanged(ref _displayOnDevices, value);
}
private bool GetCategory(DeviceCategory category)
{
return _categories.Contains(category);
@ -189,12 +128,12 @@ public class DeviceGeneralTabViewModel : ActivatableViewModelBase
return;
if (!Device.DeviceProvider.CanDetectPhysicalLayout && !await DevicePhysicalLayoutDialogViewModel.SelectPhysicalLayout(_windowService, Device))
return;
if (!Device.DeviceProvider.CanDetectLogicalLayout && !await DeviceLogicalLayoutDialogViewModel.SelectLogicalLayout(_windowService, Device))
if (!Device.DeviceProvider.CanDetectLogicalLayout && !await Layout.DeviceLogicalLayoutDialogViewModel.SelectLogicalLayout(_windowService, Device))
return;
await Task.Delay(400);
_deviceService.SaveDevice(Device);
_deviceService.ApplyDeviceLayout(Device, Device.GetBestDeviceLayout());
_deviceService.LoadDeviceLayout(Device);
}
private void Apply()
@ -234,7 +173,7 @@ public class DeviceGeneralTabViewModel : ActivatableViewModelBase
if (!DisplayOnDevices)
return;
using SKPaint overlayPaint = new() { Color = CurrentColor };
using SKPaint overlayPaint = new() {Color = CurrentColor};
e.Canvas.DrawRect(0, 0, e.Canvas.LocalClipBounds.Width, e.Canvas.LocalClipBounds.Height, overlayPaint);
}
}

View File

@ -4,9 +4,10 @@
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:device="clr-namespace:Artemis.UI.Screens.Device"
xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
xmlns:inputMappings="clr-namespace:Artemis.UI.Screens.Device.InputMappings"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Artemis.UI.Screens.Device.InputMappingsTabView"
x:DataType="device:InputMappingsTabViewModel">
x:Class="Artemis.UI.Screens.Device.InputMappings.InputMappingsTabView"
x:DataType="inputMappings:InputMappingsTabViewModel">
<ScrollViewer HorizontalScrollBarVisibility="Disabled" VerticalScrollBarVisibility="Auto">
<Grid RowDefinitions="Auto,Auto,*">
<StackPanel Grid.Row="0">

View File

@ -1,7 +1,6 @@
using Avalonia.Markup.Xaml;
using Avalonia.ReactiveUI;
namespace Artemis.UI.Screens.Device;
namespace Artemis.UI.Screens.Device.InputMappings;
public partial class InputMappingsTabView : ReactiveUserControl<InputMappingsTabViewModel>
{

View File

@ -6,19 +6,19 @@ using Artemis.Core;
using Artemis.Core.Services;
using Artemis.UI.Exceptions;
using Artemis.UI.Shared;
using HidSharp.Reports.Units;
using PropertyChanged.SourceGenerator;
using ReactiveUI;
using RGB.NET.Core;
using Unit = System.Reactive.Unit;
namespace Artemis.UI.Screens.Device;
namespace Artemis.UI.Screens.Device.InputMappings;
public class InputMappingsTabViewModel : ActivatableViewModelBase
public partial class InputMappingsTabViewModel : ActivatableViewModelBase
{
private readonly IInputService _inputService;
private readonly IDeviceService _deviceService;
private readonly ObservableCollection<ArtemisLed> _selectedLeds;
private ArtemisLed? _selectedLed;
[Notify] private ArtemisLed? _selectedLed;
public InputMappingsTabViewModel(ArtemisDevice device, ObservableCollection<ArtemisLed> selectedLeds, IInputService inputService, IDeviceService deviceService)
{
@ -49,15 +49,7 @@ public class InputMappingsTabViewModel : ActivatableViewModelBase
}
public ReactiveCommand<ArtemisInputMapping, Unit> DeleteMapping { get; }
public ArtemisDevice Device { get; }
public ArtemisLed? SelectedLed
{
get => _selectedLed;
set => RaiseAndSetIfChanged(ref _selectedLed, value);
}
public ObservableCollection<ArtemisInputMapping> InputMappings { get; }
private void ExecuteDeleteMapping(ArtemisInputMapping inputMapping)

View File

@ -4,10 +4,11 @@
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
xmlns:device="clr-namespace:Artemis.UI.Screens.Device"
xmlns:layout="clr-namespace:Artemis.UI.Screens.Device.Layout"
xmlns:layoutProviders="clr-namespace:Artemis.UI.Screens.Device.Layout.LayoutProviders"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="800"
x:Class="Artemis.UI.Screens.Device.DeviceLayoutTabView"
x:DataType="device:DeviceLayoutTabViewModel">
x:Class="Artemis.UI.Screens.Device.Layout.DeviceLayoutTabView"
x:DataType="layout:DeviceLayoutTabViewModel">
<ScrollViewer HorizontalScrollBarVisibility="Disabled" VerticalScrollBarVisibility="Auto">
<Border Classes="card" Margin="5">
<Grid RowDefinitions="*,Auto">
@ -28,6 +29,7 @@
</Button>
</StackPanel>
</Grid>
<Border Classes="card-separator" />
<Grid RowDefinitions="*,*" ColumnDefinitions="*,Auto">
<StackPanel Grid.Row="1" Grid.Column="0">
@ -45,28 +47,43 @@
</Button>
</StackPanel>
</Grid>
<Border Classes="card-separator" />
<Grid RowDefinitions="*,*" ColumnDefinitions="*,Auto">
<Grid RowDefinitions="*,*,*" ColumnDefinitions="*,Auto">
<StackPanel Grid.Row="1" Grid.Column="0">
<TextBlock Text="Disable default layout" />
<TextBlock Classes="subtitle" FontSize="12" Text="With this checked, Artemis will not load a layout for this device unless you specifically provide one." />
<TextBlock Text="Layout provider" />
<TextBlock Classes="subtitle" FontSize="12" Text="Choose between different ways to load a layout for this device." />
</StackPanel>
<StackPanel Grid.Row="1" Grid.Column="1" VerticalAlignment="Center">
<CheckBox HorizontalAlignment="Right" Margin="0,0,-10,0" IsChecked="{CompiledBinding Device.DisableDefaultLayout}"/>
</StackPanel>
</Grid>
<Border Classes="card-separator" />
<Grid RowDefinitions="*,*" ColumnDefinitions="*,Auto">
<StackPanel Grid.Row="1" Grid.Column="0">
<TextBlock Text="Custom layout path" />
<TextBlock Classes="subtitle" FontSize="12" Text="{CompiledBinding CustomLayoutPath, TargetNullValue=None}"/>
</StackPanel>
<StackPanel Grid.Row="1" Grid.Column="1" VerticalAlignment="Center" Orientation="Horizontal">
<Button Content="Clear" Command="{CompiledBinding ClearCustomLayout}" IsEnabled="{CompiledBinding HasCustomLayout}" />
<!-- 5 pixels of margin between the buttons -->
<Button Margin="5,0,0,0" Content="Browse" Command="{CompiledBinding BrowseCustomLayout}" />
<StackPanel.Styles>
<Style Selector="ComboBox.layoutProvider /template/ ContentControl#ContentPresenter">
<Setter Property="ContentTemplate">
<Setter.Value>
<DataTemplate x:DataType="layoutProviders:ILayoutProviderViewModel">
<TextBlock Text="{CompiledBinding Name}" TextWrapping="Wrap" MaxWidth="350" />
</DataTemplate>
</Setter.Value>
</Setter>
</Style>
</StackPanel.Styles>
<ComboBox Classes="layoutProvider"
Width="150"
SelectedItem="{CompiledBinding SelectedLayoutProvider}"
ItemsSource="{CompiledBinding LayoutProviders}">
<ComboBox.ItemTemplate>
<DataTemplate x:DataType="layoutProviders:ILayoutProviderViewModel">
<StackPanel>
<TextBlock Text="{CompiledBinding Name}" TextWrapping="Wrap" MaxWidth="350" />
<TextBlock Classes="subtitle" Text="{CompiledBinding Description}" TextWrapping="Wrap" MaxWidth="350" />
</StackPanel>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
</StackPanel>
</Grid>
<ContentControl Content="{CompiledBinding SelectedLayoutProvider}" ClipToBounds="False" />
<Border Classes="card-separator" />
<Grid RowDefinitions="*,*" ColumnDefinitions="*,Auto">
<StackPanel Grid.Row="1" Grid.Column="0">

View File

@ -1,11 +1,8 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Controls;
using Avalonia.Interactivity;
using Avalonia.Markup.Xaml;
using Avalonia.ReactiveUI;
namespace Artemis.UI.Screens.Device;
namespace Artemis.UI.Screens.Device.Layout;
public partial class DeviceLayoutTabView : ReactiveUserControl<DeviceLayoutTabViewModel>
{
@ -18,17 +15,17 @@ public partial class DeviceLayoutTabView : ReactiveUserControl<DeviceLayoutTabVi
{
if (ViewModel?.DefaultLayoutPath is null)
return;
TopLevel.GetTopLevel(this).Clipboard.SetTextAsync(ViewModel.DefaultLayoutPath);
TopLevel.GetTopLevel(this)?.Clipboard?.SetTextAsync(ViewModel.DefaultLayoutPath);
ViewModel.ShowCopiedNotification();
}
private void ImagePathButton_OnClick(object? sender, RoutedEventArgs e)
{
if (ViewModel?.Device?.Layout?.Image?.LocalPath is null)
if (ViewModel?.Device.Layout?.Image?.LocalPath is null)
return;
TopLevel.GetTopLevel(this).Clipboard.SetTextAsync(ViewModel.Device.Layout.Image.LocalPath);
TopLevel.GetTopLevel(this)?.Clipboard?.SetTextAsync(ViewModel.Device.Layout.Image.LocalPath);
ViewModel.ShowCopiedNotification();
}
}

View File

@ -1,81 +1,60 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using System.Reactive.Disposables;
using System.Threading.Tasks;
using System.Xml.Serialization;
using Artemis.Core;
using Artemis.Core.Services;
using Artemis.UI.Screens.Device.Layout.LayoutProviders;
using Artemis.UI.Shared;
using Artemis.UI.Shared.Services;
using Artemis.UI.Shared.Services.Builders;
using Avalonia.Controls;
using PropertyChanged.SourceGenerator;
using ReactiveUI;
using RGB.NET.Layout;
using SkiaSharp;
namespace Artemis.UI.Screens.Device;
namespace Artemis.UI.Screens.Device.Layout;
public class DeviceLayoutTabViewModel : ActivatableViewModelBase
public partial class DeviceLayoutTabViewModel : ActivatableViewModelBase
{
[Notify] private ILayoutProviderViewModel? _selectedLayoutProvider;
private readonly IWindowService _windowService;
private readonly INotificationService _notificationService;
private readonly IDeviceService _deviceService;
public DeviceLayoutTabViewModel(IWindowService windowService, INotificationService notificationService, IDeviceService deviceService, ArtemisDevice device)
public DeviceLayoutTabViewModel(IWindowService windowService, INotificationService notificationService, ArtemisDevice device, List<ILayoutProviderViewModel> layoutProviders)
{
_windowService = windowService;
_notificationService = notificationService;
_deviceService = deviceService;
Device = device;
DisplayName = "Layout";
DefaultLayoutPath = Device.DeviceProvider.LoadLayout(Device).FilePath;
this.WhenActivated(d =>
LayoutProviders = new ObservableCollection<ILayoutProviderViewModel>(layoutProviders);
foreach (ILayoutProviderViewModel layoutProviderViewModel in layoutProviders)
{
Device.PropertyChanged += DeviceOnPropertyChanged;
Disposable.Create(() => Device.PropertyChanged -= DeviceOnPropertyChanged).DisposeWith(d);
layoutProviderViewModel.Device = Device;
if (layoutProviderViewModel.Provider.IsMatch(Device))
SelectedLayoutProvider = layoutProviderViewModel;
}
// When changing device provider to one that isn't currently on the device, apply it to the device immediately
this.WhenAnyValue(vm => vm.SelectedLayoutProvider).Subscribe(l =>
{
if (l != null && !l.Provider.IsMatch(Device))
l.Apply();
});
}
public ArtemisDevice Device { get; }
public ObservableCollection<ILayoutProviderViewModel> LayoutProviders { get; set; }
public string DefaultLayoutPath { get; }
public string? ImagePath => Device.Layout?.Image?.LocalPath;
public string? CustomLayoutPath => Device.CustomLayoutPath;
public bool HasCustomLayout => Device.CustomLayoutPath != null;
public void ClearCustomLayout()
{
Device.CustomLayoutPath = null;
_notificationService.CreateNotification()
.WithMessage("Cleared imported layout.")
.WithSeverity(NotificationSeverity.Informational);
}
public async Task BrowseCustomLayout()
{
string[]? files = await _windowService.CreateOpenFileDialog()
.WithTitle("Select device layout file")
.HavingFilter(f => f.WithName("Layout files").WithExtension("xml"))
.ShowAsync();
if (files?.Length > 0)
{
Device.CustomLayoutPath = files[0];
_notificationService.CreateNotification()
.WithTitle("Imported layout")
.WithMessage($"File loaded from {files[0]}")
.WithSeverity(NotificationSeverity.Informational);
}
}
public async Task ExportLayout()
{
string fileName = Device.DeviceProvider.GetDeviceLayoutName(Device);
@ -106,13 +85,13 @@ public class DeviceLayoutTabViewModel : ActivatableViewModelBase
}
else
{
List<LedLayout> ledLayouts = Device.Leds.Select(x => new LedLayout()
List<LedLayout> ledLayouts = Device.Leds.Select(x => new LedLayout
{
Id = x.RgbLed.Id.ToString(),
DescriptiveX = x.Rectangle.Left.ToString(),
DescriptiveY = x.Rectangle.Top.ToString(),
DescriptiveWidth = $"{x.Rectangle.Width}mm",
DescriptiveHeight = $"{x.Rectangle.Height}mm",
DescriptiveHeight = $"{x.Rectangle.Height}mm"
}).ToList();
DeviceLayout emptyLayout = new()
@ -123,7 +102,7 @@ public class DeviceLayoutTabViewModel : ActivatableViewModelBase
Model = Device.RgbDevice.DeviceInfo.Model,
Width = Device.Rectangle.Width,
Height = Device.Rectangle.Height,
InternalLeds = ledLayouts,
InternalLeds = ledLayouts
};
XmlSerializer serializer = new(typeof(DeviceLayout));
@ -145,19 +124,4 @@ public class DeviceLayoutTabViewModel : ActivatableViewModelBase
.WithSeverity(NotificationSeverity.Informational)
.Show();
}
private void DeviceOnPropertyChanged(object? sender, PropertyChangedEventArgs e)
{
if (e.PropertyName is nameof(Device.CustomLayoutPath) or nameof(Device.DisableDefaultLayout))
{
Task.Run(() =>
{
_deviceService.ApplyDeviceLayout(Device, Device.GetBestDeviceLayout());
_deviceService.SaveDevice(Device);
});
this.RaisePropertyChanged(nameof(CustomLayoutPath));
this.RaisePropertyChanged(nameof(HasCustomLayout));
}
}
}

View File

@ -2,21 +2,21 @@
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:device="clr-namespace:Artemis.UI.Screens.Device"
xmlns:globalization="clr-namespace:System.Globalization;assembly=System.Runtime"
xmlns:layout="clr-namespace:Artemis.UI.Screens.Device.Layout"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Artemis.UI.Screens.Device.DeviceLogicalLayoutDialogView"
x:DataType="device:DeviceLogicalLayoutDialogViewModel">
x:Class="Artemis.UI.Screens.Device.Layout.DeviceLogicalLayoutDialogView"
x:DataType="layout:DeviceLogicalLayoutDialogViewModel">
<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,11 @@
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 Text="(" /><Run FontWeight="SemiBold" Text="{CompiledBinding TwoLetterISORegionName}" />
<Run Text=")" />
</TextBlock>
</DataTemplate>
</AutoCompleteBox.ItemTemplate>
</AutoCompleteBox>

View File

@ -1,19 +1,17 @@
using System;
using System.Globalization;
using System.Threading.Tasks;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using Avalonia.ReactiveUI;
using Avalonia.Threading;
namespace Artemis.UI.Screens.Device;
namespace Artemis.UI.Screens.Device.Layout;
public partial class DeviceLogicalLayoutDialogView : ReactiveUserControl<DeviceLogicalLayoutDialogViewModel>
{
public DeviceLogicalLayoutDialogView()
{
InitializeComponent();
RegionsAutoCompleteBox.ItemFilter += SearchRegions;
Dispatcher.UIThread.InvokeAsync(DelayedAutoFocus);
}
@ -25,8 +23,10 @@ public partial class DeviceLogicalLayoutDialogView : ReactiveUserControl<DeviceL
RegionsAutoCompleteBox.PopulateComplete();
}
private bool SearchRegions(string search, object item)
private bool SearchRegions(string? search, object? item)
{
if (search == null)
return true;
if (item is not RegionInfo regionInfo)
return false;
@ -34,5 +34,4 @@ public partial class DeviceLogicalLayoutDialogView : ReactiveUserControl<DeviceL
regionInfo.NativeName.Contains(search, StringComparison.OrdinalIgnoreCase) ||
regionInfo.TwoLetterISORegionName.Contains(search, StringComparison.OrdinalIgnoreCase);
}
}

View File

@ -8,23 +8,23 @@ using Artemis.Core;
using Artemis.UI.Shared;
using Artemis.UI.Shared.Services;
using FluentAvalonia.UI.Controls;
using PropertyChanged.SourceGenerator;
using ReactiveUI;
using ContentDialogButton = Artemis.UI.Shared.Services.Builders.ContentDialogButton;
namespace Artemis.UI.Screens.Device;
namespace Artemis.UI.Screens.Device.Layout;
public class DeviceLogicalLayoutDialogViewModel : ContentDialogViewModelBase
public partial class DeviceLogicalLayoutDialogViewModel : ContentDialogViewModelBase
{
private const int LOCALE_NEUTRAL = 0x0000;
private const int LOCALE_CUSTOM_DEFAULT = 0x0c00;
private const int LOCALE_INVARIANT = 0x007F;
private RegionInfo? _selectedRegion;
[Notify] private RegionInfo? _selectedRegion;
public DeviceLogicalLayoutDialogViewModel(ArtemisDevice device)
{
Device = device;
ApplyLogicalLayout = ReactiveCommand.Create(ExecuteApplyLogicalLayout, this.WhenAnyValue(vm => vm.SelectedRegion).Select(r => r != null));
ApplyLogicalLayout = ReactiveCommand.Create(ExecuteApplyLogicalLayout, this.WhenAnyValue<DeviceLogicalLayoutDialogViewModel, RegionInfo>(vm => vm.SelectedRegion).Select(r => r != null));
Regions = new ObservableCollection<RegionInfo>(CultureInfo.GetCultures(CultureTypes.SpecificCultures)
.Where(c => c.LCID != LOCALE_INVARIANT &&
c.LCID != LOCALE_NEUTRAL &&
@ -44,12 +44,6 @@ public class DeviceLogicalLayoutDialogViewModel : ContentDialogViewModelBase
public ObservableCollection<RegionInfo> Regions { get; }
public bool Applied { get; set; }
public RegionInfo? SelectedRegion
{
get => _selectedRegion;
set => RaiseAndSetIfChanged(ref _selectedRegion, value);
}
public static async Task<bool> SelectLogicalLayout(IWindowService windowService, ArtemisDevice device)
{
await windowService.CreateContentDialog()

View File

@ -2,10 +2,10 @@
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:device="clr-namespace:Artemis.UI.Screens.Device"
xmlns:layout="clr-namespace:Artemis.UI.Screens.Device.Layout"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Artemis.UI.Screens.Device.DevicePhysicalLayoutDialogView"
x:DataType="device:DevicePhysicalLayoutDialogViewModel">
x:Class="Artemis.UI.Screens.Device.Layout.DevicePhysicalLayoutDialogView"
x:DataType="layout:DevicePhysicalLayoutDialogViewModel">
<Grid RowDefinitions="Auto,*">
<StackPanel Grid.Row="0">
<StackPanel Orientation="Horizontal">

View File

@ -1,7 +1,6 @@
using Avalonia.Markup.Xaml;
using Avalonia.ReactiveUI;
using Avalonia.ReactiveUI;
namespace Artemis.UI.Screens.Device;
namespace Artemis.UI.Screens.Device.Layout;
public partial class DevicePhysicalLayoutDialogView : ReactiveUserControl<DevicePhysicalLayoutDialogViewModel>
{
@ -9,5 +8,4 @@ public partial class DevicePhysicalLayoutDialogView : ReactiveUserControl<Device
{
InitializeComponent();
}
}

View File

@ -7,7 +7,7 @@ using Artemis.UI.Shared.Services;
using FluentAvalonia.UI.Controls;
using ReactiveUI;
namespace Artemis.UI.Screens.Device;
namespace Artemis.UI.Screens.Device.Layout;
public class DevicePhysicalLayoutDialogViewModel : ContentDialogViewModelBase
{

View File

@ -0,0 +1,24 @@
<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:layoutProviders="clr-namespace:Artemis.UI.Screens.Device.Layout.LayoutProviders"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Artemis.UI.Screens.Device.Layout.LayoutProviders.CustomLayoutView"
x:DataType="layoutProviders:CustomLayoutViewModel"
ClipToBounds="False">
<StackPanel ClipToBounds="False">
<Border Classes="card-separator" />
<Grid RowDefinitions="*,*" ColumnDefinitions="*,Auto">
<StackPanel Grid.Row="1" Grid.Column="0">
<TextBlock Text="Current layout" />
<TextBlock Classes="subtitle" FontSize="12" Text="{CompiledBinding Device.LayoutSelection.Parameter, TargetNullValue=None}" TextWrapping="Wrap" />
</StackPanel>
<StackPanel Grid.Row="1" Grid.Column="1" VerticalAlignment="Center" Orientation="Horizontal">
<Button Content="Clear" Command="{CompiledBinding ClearCustomLayout}" IsEnabled="{CompiledBinding !!Device.LayoutSelection.Parameter}" />
<!-- 5 pixels of margin between the buttons -->
<Button Margin="5,0,0,0" Content="Browse" Command="{CompiledBinding BrowseCustomLayout}" />
</StackPanel>
</Grid>
</StackPanel>
</UserControl>

View File

@ -0,0 +1,11 @@
using Avalonia.Controls;
namespace Artemis.UI.Screens.Device.Layout.LayoutProviders;
public partial class CustomLayoutView : UserControl
{
public CustomLayoutView()
{
InitializeComponent();
}
}

View File

@ -0,0 +1,80 @@
using System.Threading.Tasks;
using Artemis.Core;
using Artemis.Core.Providers;
using Artemis.Core.Services;
using Artemis.UI.Shared;
using Artemis.UI.Shared.Services;
using Artemis.UI.Shared.Services.Builders;
namespace Artemis.UI.Screens.Device.Layout.LayoutProviders;
public class CustomLayoutViewModel : ViewModelBase, ILayoutProviderViewModel
{
private readonly CustomPathLayoutProvider _layoutProvider;
public CustomLayoutViewModel(IWindowService windowService, INotificationService notificationService, IDeviceService deviceService, CustomPathLayoutProvider layoutProvider)
{
_layoutProvider = layoutProvider;
_windowService = windowService;
_notificationService = notificationService;
_deviceService = deviceService;
}
/// <inheritdoc />
public string Name => "Custom";
/// <inheritdoc />
public string Description => "Select a layout file from a folder on your computer";
/// <inheritdoc />
public ILayoutProvider Provider => _layoutProvider;
public ArtemisDevice Device { get; set; } = null!;
private readonly IWindowService _windowService;
private readonly INotificationService _notificationService;
private readonly IDeviceService _deviceService;
public void ClearCustomLayout()
{
_layoutProvider.ConfigureDevice(Device, null);
Save();
_notificationService.CreateNotification()
.WithMessage("Cleared imported layout.")
.WithSeverity(NotificationSeverity.Informational);
}
public async Task BrowseCustomLayout()
{
string[]? files = await _windowService.CreateOpenFileDialog()
.WithTitle("Select device layout file")
.HavingFilter(f => f.WithName("Layout files").WithExtension("xml"))
.ShowAsync();
if (files?.Length > 0)
{
_layoutProvider.ConfigureDevice(Device, files[0]);
Save();
_notificationService.CreateNotification()
.WithTitle("Imported layout")
.WithMessage($"File loaded from {files[0]}")
.WithSeverity(NotificationSeverity.Informational);
}
}
/// <inheritdoc />
public void Apply()
{
_layoutProvider.ConfigureDevice(Device, null);
Save();
}
private void Save()
{
_deviceService.SaveDevice(Device);
_deviceService.LoadDeviceLayout(Device);
}
}

View File

@ -0,0 +1,14 @@
<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"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Artemis.UI.Screens.Device.Layout.LayoutProviders.DefaultLayoutView">
<StackPanel ClipToBounds="False">
<Border Classes="card-separator" />
<StackPanel>
<TextBlock Text="Current layout" />
<TextBlock Classes="subtitle" FontSize="12" Text="Loading the default layout from the plugin or User Layouts folder." TextWrapping="Wrap" />
</StackPanel>
</StackPanel>
</UserControl>

View File

@ -0,0 +1,11 @@
using Avalonia.Controls;
namespace Artemis.UI.Screens.Device.Layout.LayoutProviders;
public partial class DefaultLayoutView : UserControl
{
public DefaultLayoutView()
{
InitializeComponent();
}
}

View File

@ -0,0 +1,42 @@
using Artemis.Core;
using Artemis.Core.Providers;
using Artemis.Core.Services;
using Artemis.UI.Shared;
namespace Artemis.UI.Screens.Device.Layout.LayoutProviders;
public class DefaultLayoutViewModel : ViewModelBase, ILayoutProviderViewModel
{
private readonly DefaultLayoutProvider _layoutProvider;
private readonly IDeviceService _deviceService;
public DefaultLayoutViewModel(DefaultLayoutProvider layoutProvider, IDeviceService deviceService)
{
_layoutProvider = layoutProvider;
_deviceService = deviceService;
}
/// <inheritdoc />
public ILayoutProvider Provider => _layoutProvider;
public ArtemisDevice Device { get; set; } = null!;
/// <inheritdoc />
public string Name => "Default";
/// <inheritdoc />
public string Description => "Attempts to load a layout from the default paths";
/// <inheritdoc />
public void Apply()
{
_layoutProvider.ConfigureDevice(Device);
Save();
}
private void Save()
{
_deviceService.SaveDevice(Device);
_deviceService.LoadDeviceLayout(Device);
}
}

View File

@ -0,0 +1,17 @@
using Artemis.Core;
using Artemis.Core.Providers;
namespace Artemis.UI.Screens.Device.Layout.LayoutProviders;
public interface ILayoutProviderViewModel
{
string Name { get; }
string Description { get; }
ILayoutProvider Provider { get; }
ArtemisDevice Device { get; set; }
void Apply();
}

View File

@ -0,0 +1,14 @@
<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"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Artemis.UI.Screens.Device.Layout.LayoutProviders.NoneLayoutView">
<StackPanel ClipToBounds="False">
<Border Classes="card-separator" />
<StackPanel>
<TextBlock Text="Current layout" />
<TextBlock Classes="subtitle" FontSize="12" Text="Not loading any layout, leaving LEDs to be positioned by the device provider." TextWrapping="Wrap" />
</StackPanel>
</StackPanel>
</UserControl>

View File

@ -0,0 +1,11 @@
using Avalonia.Controls;
namespace Artemis.UI.Screens.Device.Layout.LayoutProviders;
public partial class NoneLayoutView : UserControl
{
public NoneLayoutView()
{
InitializeComponent();
}
}

View File

@ -0,0 +1,42 @@
using Artemis.Core;
using Artemis.Core.Providers;
using Artemis.Core.Services;
using Artemis.UI.Shared;
namespace Artemis.UI.Screens.Device.Layout.LayoutProviders;
public class NoneLayoutViewModel : ViewModelBase, ILayoutProviderViewModel
{
private readonly NoneLayoutProvider _layoutProvider;
private readonly IDeviceService _deviceService;
public NoneLayoutViewModel(NoneLayoutProvider layoutProvider, IDeviceService deviceService)
{
_layoutProvider = layoutProvider;
_deviceService = deviceService;
}
/// <inheritdoc />
public ILayoutProvider Provider => _layoutProvider;
public ArtemisDevice Device { get; set; } = null!;
/// <inheritdoc />
public string Name => "None";
/// <inheritdoc />
public string Description => "Do not load any layout";
/// <inheritdoc />
public void Apply()
{
_layoutProvider.ConfigureDevice(Device);
Save();
}
private void Save()
{
_deviceService.SaveDevice(Device);
_deviceService.LoadDeviceLayout(Device);
}
}

View File

@ -0,0 +1,47 @@
<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:services="clr-namespace:Artemis.WebClient.Workshop.Services;assembly=Artemis.WebClient.Workshop"
xmlns:layoutProviders="clr-namespace:Artemis.UI.Screens.Device.Layout.LayoutProviders"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Artemis.UI.Screens.Device.Layout.LayoutProviders.WorkshopLayoutView"
x:DataType="layoutProviders:WorkshopLayoutViewModel">
<StackPanel ClipToBounds="False">
<Border Classes="card-separator" />
<Grid RowDefinitions="*,*" ColumnDefinitions="*,Auto">
<StackPanel Grid.Row="1" Grid.Column="0">
<TextBlock Text="Current layout" />
<TextBlock Classes="subtitle" FontSize="12" Text="Loading the layout from a workshop entry" TextWrapping="Wrap" />
</StackPanel>
<StackPanel Grid.Row="1" Grid.Column="1" VerticalAlignment="Center" Spacing="5">
<StackPanel.Styles>
<Style Selector="ComboBox.layoutProvider /template/ ContentControl#ContentPresenter">
<Setter Property="ContentTemplate">
<Setter.Value>
<DataTemplate x:DataType="services:InstalledEntry">
<TextBlock Text="{CompiledBinding Name}" TextWrapping="Wrap" MaxWidth="350" />
</DataTemplate>
</Setter.Value>
</Setter>
</Style>
</StackPanel.Styles>
<ComboBox Classes="layoutProvider"
Width="350"
SelectedItem="{CompiledBinding SelectedEntry}"
ItemsSource="{CompiledBinding Entries}"
PlaceholderText="Select an installed layout">
<ComboBox.ItemTemplate>
<DataTemplate x:DataType="services:InstalledEntry">
<StackPanel>
<TextBlock Text="{CompiledBinding Name}" TextWrapping="Wrap" MaxWidth="350" />
<TextBlock Classes="subtitle" Text="{CompiledBinding Author}" TextWrapping="Wrap" MaxWidth="350" />
</StackPanel>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
<Button HorizontalAlignment="Right" Click="Button_OnClick">Browse workshop layouts</Button>
</StackPanel>
</Grid>
</StackPanel>
</UserControl>

View File

@ -0,0 +1,20 @@
using System.Threading.Tasks;
using Avalonia.Controls;
using Avalonia.Interactivity;
using Avalonia.ReactiveUI;
namespace Artemis.UI.Screens.Device.Layout.LayoutProviders;
public partial class WorkshopLayoutView : ReactiveUserControl<WorkshopLayoutViewModel>
{
public WorkshopLayoutView()
{
InitializeComponent();
}
private async void Button_OnClick(object? sender, RoutedEventArgs e)
{
if (ViewModel != null && await ViewModel.BrowseLayouts())
(VisualRoot as Window)?.Close();
}
}

View File

@ -0,0 +1,83 @@
using System.Collections.ObjectModel;
using System;
using System.Linq;
using System.Reactive.Disposables;
using System.Threading.Tasks;
using Artemis.Core;
using Artemis.Core.Providers;
using Artemis.Core.Services;
using Artemis.UI.Shared;
using Artemis.UI.Shared.Routing;
using Artemis.UI.Shared.Services;
using Artemis.WebClient.Workshop;
using Artemis.WebClient.Workshop.Providers;
using Artemis.WebClient.Workshop.Services;
using PropertyChanged.SourceGenerator;
using ReactiveUI;
namespace Artemis.UI.Screens.Device.Layout.LayoutProviders;
public partial class WorkshopLayoutViewModel : ActivatableViewModelBase, ILayoutProviderViewModel
{
[Notify] private InstalledEntry? _selectedEntry;
private readonly WorkshopLayoutProvider _layoutProvider;
private readonly IDeviceService _deviceService;
private readonly IWindowService _windowService;
private readonly IRouter _router;
public WorkshopLayoutViewModel(WorkshopLayoutProvider layoutProvider, IWorkshopService workshopService, IDeviceService deviceService, IWindowService windowService, IRouter router)
{
_layoutProvider = layoutProvider;
_deviceService = deviceService;
_windowService = windowService;
_router = router;
Entries = new ObservableCollection<InstalledEntry>(workshopService.GetInstalledEntries().Where(e => e.EntryType == EntryType.Layout));
this.WhenAnyValue(vm => vm.SelectedEntry).Subscribe(ApplyEntry);
this.WhenActivated((CompositeDisposable _) => SelectedEntry = Entries.FirstOrDefault(e => e.EntryId.ToString() == Device.LayoutSelection.Parameter));
}
/// <inheritdoc />
public ILayoutProvider Provider => _layoutProvider;
public ArtemisDevice Device { get; set; } = null!;
public ObservableCollection<InstalledEntry> Entries { get; }
/// <inheritdoc />
public string Name => "Workshop";
/// <inheritdoc />
public string Description => "Load a layout from the workshop";
/// <inheritdoc />
public void Apply()
{
_layoutProvider.ConfigureDevice(Device, null);
Save();
}
public async Task<bool> BrowseLayouts()
{
if (!await _windowService.ShowConfirmContentDialog("Open workshop", "Do you want to close this window and view the workshop?"))
return false;
await _router.Navigate("workshop/entries/layouts/1");
return true;
}
private void ApplyEntry(InstalledEntry? entry)
{
if (entry == null || Device.LayoutSelection.Parameter == entry.EntryId.ToString())
return;
_layoutProvider.ConfigureDevice(Device, entry);
Save();
}
private void Save()
{
_deviceService.SaveDevice(Device);
_deviceService.LoadDeviceLayout(Device);
}
}

View File

@ -2,7 +2,7 @@
using Artemis.Core;
using Artemis.UI.Shared;
namespace Artemis.UI.Screens.Device;
namespace Artemis.UI.Screens.Device.Leds;
public class DeviceLedsTabLedViewModel : ViewModelBase
{

View File

@ -5,9 +5,10 @@
xmlns:device="clr-namespace:Artemis.UI.Screens.Device"
xmlns:converters="clr-namespace:Artemis.UI.Converters"
xmlns:shared="clr-namespace:Artemis.UI.Shared.Converters;assembly=Artemis.UI.Shared"
xmlns:leds="clr-namespace:Artemis.UI.Screens.Device.Leds"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Artemis.UI.Screens.Device.DeviceLedsTabView"
x:DataType="device:DeviceLedsTabViewModel">
x:Class="Artemis.UI.Screens.Device.Leds.DeviceLedsTabView"
x:DataType="leds:DeviceLedsTabViewModel">
<UserControl.Resources>
<converters:UriToFileNameConverter x:Key="UriToFileNameConverter" />
<converters:LedIdToStringConverter x:Key="LedIdToStringConverter" />

View File

@ -1,7 +1,6 @@
using Avalonia.Markup.Xaml;
using Avalonia.ReactiveUI;
namespace Artemis.UI.Screens.Device;
namespace Artemis.UI.Screens.Device.Leds;
public partial class DeviceLedsTabView : ReactiveUserControl<DeviceLedsTabViewModel>
{

View File

@ -8,7 +8,7 @@ using Artemis.UI.Shared;
using DynamicData.Binding;
using ReactiveUI;
namespace Artemis.UI.Screens.Device;
namespace Artemis.UI.Screens.Device.Leds;
public class DeviceLedsTabViewModel : ActivatableViewModelBase
{

View File

@ -14,20 +14,21 @@ using Artemis.UI.Shared.Services;
using Avalonia.Threading;
using FluentAvalonia.Core;
using FluentAvalonia.UI.Controls;
using PropertyChanged.SourceGenerator;
using ReactiveUI;
using ContentDialogButton = Artemis.UI.Shared.Services.Builders.ContentDialogButton;
namespace Artemis.UI.Screens.Plugins;
public class PluginPrerequisitesInstallDialogViewModel : ContentDialogViewModelBase
public partial class PluginPrerequisitesInstallDialogViewModel : ContentDialogViewModelBase
{
private PluginPrerequisiteViewModel? _activePrerequisite;
private bool _canInstall;
private bool _showFailed;
private bool _showInstall = true;
private bool _showIntro = true;
private bool _showProgress;
private CancellationTokenSource? _tokenSource;
[Notify] private PluginPrerequisiteViewModel? _activePrerequisite;
[Notify] private bool _canInstall;
[Notify] private bool _showFailed;
[Notify] private bool _showInstall = true;
[Notify] private bool _showIntro = true;
[Notify] private bool _showProgress;
public PluginPrerequisitesInstallDialogViewModel(List<IPrerequisitesSubject> subjects, IPrerequisitesVmFactory prerequisitesVmFactory)
{
@ -50,43 +51,7 @@ public class PluginPrerequisitesInstallDialogViewModel : ContentDialogViewModelB
public ReactiveCommand<Unit, Unit> Install { get; }
public ObservableCollection<PluginPrerequisiteViewModel> Prerequisites { get; }
public PluginPrerequisiteViewModel? ActivePrerequisite
{
get => _activePrerequisite;
set => RaiseAndSetIfChanged(ref _activePrerequisite, value);
}
public bool ShowProgress
{
get => _showProgress;
set => RaiseAndSetIfChanged(ref _showProgress, value);
}
public bool ShowIntro
{
get => _showIntro;
set => RaiseAndSetIfChanged(ref _showIntro, value);
}
public bool ShowFailed
{
get => _showFailed;
set => RaiseAndSetIfChanged(ref _showFailed, value);
}
public bool ShowInstall
{
get => _showInstall;
set => RaiseAndSetIfChanged(ref _showInstall, value);
}
public bool CanInstall
{
get => _canInstall;
set => RaiseAndSetIfChanged(ref _canInstall, value);
}
public static async Task Show(IWindowService windowService, List<IPrerequisitesSubject> subjects)
{
await windowService.CreateContentDialog()

View File

@ -15,19 +15,20 @@ using Artemis.UI.Shared.Services;
using Avalonia.Threading;
using FluentAvalonia.Core;
using FluentAvalonia.UI.Controls;
using PropertyChanged.SourceGenerator;
using ReactiveUI;
using ContentDialogButton = Artemis.UI.Shared.Services.Builders.ContentDialogButton;
namespace Artemis.UI.Screens.Plugins;
public class PluginPrerequisitesUninstallDialogViewModel : ContentDialogViewModelBase
public partial class PluginPrerequisitesUninstallDialogViewModel : ContentDialogViewModelBase
{
private readonly IPluginManagementService _pluginManagementService;
private readonly List<IPrerequisitesSubject> _subjects;
private readonly IWindowService _windowService;
private PluginPrerequisiteViewModel? _activePrerequisite;
private bool _canUninstall;
private CancellationTokenSource? _tokenSource;
[Notify] private PluginPrerequisiteViewModel? _activePrerequisite;
[Notify] private bool _canUninstall;
public PluginPrerequisitesUninstallDialogViewModel(List<IPrerequisitesSubject> subjects,
IPrerequisitesVmFactory prerequisitesVmFactory,
@ -59,19 +60,7 @@ public class PluginPrerequisitesUninstallDialogViewModel : ContentDialogViewMode
public ReactiveCommand<Unit, Unit> Uninstall { get; }
public ObservableCollection<PluginPrerequisiteViewModel> Prerequisites { get; }
public PluginPrerequisiteViewModel? ActivePrerequisite
{
get => _activePrerequisite;
set => RaiseAndSetIfChanged(ref _activePrerequisite, value);
}
public bool CanUninstall
{
get => _canUninstall;
set => RaiseAndSetIfChanged(ref _canUninstall, value);
}
public static async Task Show(IWindowService windowService, List<IPrerequisitesSubject> subjects, string cancelLabel = "Cancel")
{
await windowService.CreateContentDialog()

View File

@ -16,16 +16,17 @@ using Artemis.UI.Shared.Services;
using Artemis.UI.Shared.Services.Builders;
using Avalonia.Threading;
using Material.Icons;
using PropertyChanged.SourceGenerator;
using ReactiveUI;
namespace Artemis.UI.Screens.Plugins.Features;
public class PluginFeatureViewModel : ActivatableViewModelBase
public partial class PluginFeatureViewModel : ActivatableViewModelBase
{
private readonly ICoreService _coreService;
private readonly IPluginManagementService _pluginManagementService;
private readonly IWindowService _windowService;
private bool _enabling;
[Notify] private bool _enabling;
public PluginFeatureViewModel(PluginFeatureInfo pluginFeatureInfo,
bool showShield,
@ -73,15 +74,8 @@ public class PluginFeatureViewModel : ActivatableViewModelBase
public PluginFeatureInfo FeatureInfo { get; }
public Exception? LoadException => FeatureInfo.LoadException;
public bool ShowShield { get; }
public bool Enabling
{
get => _enabling;
set => RaiseAndSetIfChanged(ref _enabling, value);
}
public bool IsEnabled
{
get => FeatureInfo.AlwaysEnabled || (FeatureInfo.Instance != null && FeatureInfo.Instance.IsEnabled);

View File

@ -14,21 +14,22 @@ using Artemis.UI.Shared.Services.Builders;
using Avalonia.Controls;
using Avalonia.Threading;
using Material.Icons;
using PropertyChanged.SourceGenerator;
using ReactiveUI;
namespace Artemis.UI.Screens.Plugins;
public class PluginViewModel : ActivatableViewModelBase
public partial class PluginViewModel : ActivatableViewModelBase
{
private readonly ICoreService _coreService;
private readonly INotificationService _notificationService;
private readonly IPluginManagementService _pluginManagementService;
private readonly IWindowService _windowService;
private bool _canInstallPrerequisites;
private bool _canRemovePrerequisites;
private bool _enabling;
private Plugin _plugin;
private Window? _settingsWindow;
[Notify] private bool _canInstallPrerequisites;
[Notify] private bool _canRemovePrerequisites;
[Notify] private bool _enabling;
[Notify] private Plugin _plugin;
public PluginViewModel(Plugin plugin,
ReactiveCommand<Unit, Unit>? reload,
@ -87,34 +88,9 @@ public class PluginViewModel : ActivatableViewModelBase
public ReactiveCommand<Unit, Unit> OpenPluginDirectory { get; }
public ObservableCollection<PluginPlatformViewModel> Platforms { get; }
public Plugin Plugin
{
get => _plugin;
set => RaiseAndSetIfChanged(ref _plugin, value);
}
public bool Enabling
{
get => _enabling;
set => RaiseAndSetIfChanged(ref _enabling, value);
}
public string Type => Plugin.GetType().BaseType?.Name ?? Plugin.GetType().Name;
public bool IsEnabled => Plugin.IsEnabled;
public bool CanInstallPrerequisites
{
get => _canInstallPrerequisites;
set => RaiseAndSetIfChanged(ref _canInstallPrerequisites, value);
}
public bool CanRemovePrerequisites
{
get => _canRemovePrerequisites;
set => RaiseAndSetIfChanged(ref _canRemovePrerequisites, value);
}
public async Task UpdateEnabled(bool enable)
{
if (Enabling)

View File

@ -6,21 +6,20 @@ using System.Threading;
using System.Threading.Tasks;
using Artemis.Core;
using Artemis.UI.Shared;
using PropertyChanged.SourceGenerator;
using ReactiveUI;
namespace Artemis.UI.Screens.Plugins.Prerequisites;
public class PluginPrerequisiteViewModel : ActivatableViewModelBase
public partial class PluginPrerequisiteViewModel : ActivatableViewModelBase
{
private readonly ObservableAsPropertyHelper<int> _activeStepNumber;
private readonly ObservableAsPropertyHelper<bool> _busy;
private readonly bool _uninstall;
private PluginPrerequisiteActionViewModel? _activeAction;
private bool _installing;
private bool _isMet;
private bool _uninstalling;
[Notify] private PluginPrerequisiteActionViewModel? _activeAction;
[Notify] private bool _installing;
[Notify] private bool _isMet;
[Notify] private bool _uninstalling;
public PluginPrerequisiteViewModel(PluginPrerequisite pluginPrerequisite, bool uninstall)
{
@ -45,33 +44,7 @@ public class PluginPrerequisiteViewModel : ActivatableViewModelBase
}
public ObservableCollection<PluginPrerequisiteActionViewModel> Actions { get; }
public PluginPrerequisiteActionViewModel? ActiveAction
{
get => _activeAction;
set => RaiseAndSetIfChanged(ref _activeAction, value);
}
public PluginPrerequisite PluginPrerequisite { get; }
public bool Installing
{
get => _installing;
set => RaiseAndSetIfChanged(ref _installing, value);
}
public bool Uninstalling
{
get => _uninstalling;
set => RaiseAndSetIfChanged(ref _uninstalling, value);
}
public bool IsMet
{
get => _isMet;
set => RaiseAndSetIfChanged(ref _isMet, value);
}
public bool Busy => _busy.Value;
public int ActiveStepNumber => _activeStepNumber.Value;

View File

@ -15,11 +15,12 @@ using Artemis.UI.Shared.Routing;
using Artemis.UI.Shared.Services;
using Artemis.UI.Shared.Services.ProfileEditor;
using Newtonsoft.Json;
using PropertyChanged.SourceGenerator;
using ReactiveUI;
namespace Artemis.UI.Screens.ProfileEditor.MenuBar;
public class MenuBarViewModel : ActivatableViewModelBase
public partial class MenuBarViewModel : ActivatableViewModelBase
{
private readonly IRouter _router;
private readonly IProfileEditorService _profileEditorService;
@ -29,12 +30,12 @@ public class MenuBarViewModel : ActivatableViewModelBase
private ObservableAsPropertyHelper<bool>? _focusFolder;
private ObservableAsPropertyHelper<bool>? _focusNone;
private ObservableAsPropertyHelper<bool>? _focusSelection;
private ProfileEditorHistory? _history;
private ObservableAsPropertyHelper<bool>? _isSuspended;
private ObservableAsPropertyHelper<bool>? _keyBindingsEnabled;
private ObservableAsPropertyHelper<ProfileConfiguration?>? _profileConfiguration;
private ObservableAsPropertyHelper<RenderProfileElement?>? _profileElement;
private ObservableAsPropertyHelper<bool>? _suspendedEditing;
[Notify] private ProfileEditorHistory? _history;
public MenuBarViewModel(IRouter router, IProfileEditorService profileEditorService, IProfileService profileService, ISettingsService settingsService, IWindowService windowService)
{
@ -108,12 +109,6 @@ public class MenuBarViewModel : ActivatableViewModelBase
public PluginSetting<bool> AlwaysApplyDataBindings => _settingsService.GetSetting("ProfileEditor.AlwaysApplyDataBindings", false);
public PluginSetting<ProfileEditorFocusMode> FocusMode => _settingsService.GetSetting("ProfileEditor.FocusMode", ProfileEditorFocusMode.Folder);
public ProfileEditorHistory? History
{
get => _history;
set => RaiseAndSetIfChanged(ref _history, value);
}
private void ExecuteAddFolder()
{
if (ProfileConfiguration?.Profile == null)

View File

@ -8,11 +8,12 @@ using Artemis.Core.Services;
using Artemis.UI.Shared;
using Artemis.UI.Shared.Services.ProfileEditor;
using Avalonia.Threading;
using PropertyChanged.SourceGenerator;
using ReactiveUI;
namespace Artemis.UI.Screens.ProfileEditor.Playback;
public class PlaybackViewModel : ActivatableViewModelBase
public partial class PlaybackViewModel : ActivatableViewModelBase
{
private readonly IProfileEditorService _profileEditorService;
private readonly ISettingsService _settingsService;
@ -22,9 +23,9 @@ public class PlaybackViewModel : ActivatableViewModelBase
private DateTime _lastUpdate;
private ObservableAsPropertyHelper<bool>? _playing;
private RenderProfileElement? _profileElement;
private bool _repeating;
private bool _repeatSegment;
private bool _repeatTimeline;
[Notify] private bool _repeating;
[Notify] private bool _repeatSegment;
[Notify] private bool _repeatTimeline;
public PlaybackViewModel(IProfileEditorService profileEditorService, ISettingsService settingsService)
{
@ -84,24 +85,6 @@ public class PlaybackViewModel : ActivatableViewModelBase
public bool Playing => _playing?.Value ?? false;
public bool KeyBindingsEnabled => _keyBindingsEnabled?.Value ?? false;
public bool Repeating
{
get => _repeating;
set => RaiseAndSetIfChanged(ref _repeating, value);
}
public bool RepeatTimeline
{
get => _repeatTimeline;
set => RaiseAndSetIfChanged(ref _repeatTimeline, value);
}
public bool RepeatSegment
{
get => _repeatSegment;
set => RaiseAndSetIfChanged(ref _repeatSegment, value);
}
public ReactiveCommand<Unit, Unit> PlayFromStart { get; }
public ReactiveCommand<Unit, Unit> TogglePlay { get; }
public ReactiveCommand<Unit, Unit> GoToStart { get; }

Some files were not shown because too many files have changed in this diff Show More