diff --git a/src/Artemis.Core/Models/ProfileConfiguration/ConfigurationBooleanItem.cs b/src/Artemis.Core/Models/ProfileConfiguration/ConfigurationBooleanItem.cs
new file mode 100644
index 000000000..30073885d
--- /dev/null
+++ b/src/Artemis.Core/Models/ProfileConfiguration/ConfigurationBooleanItem.cs
@@ -0,0 +1,25 @@
+namespace Artemis.Core;
+
+///
+/// Represents a configuration item that accepts boolean input from the user.
+///
+public class ConfigurationBooleanItem : ConfigurationInputItem
+{
+ ///
+ /// Gets or sets the display text shown when the boolean value is true.
+ ///
+ public required string TrueText
+ {
+ get;
+ set => SetAndNotify(ref field, value);
+ }
+
+ ///
+ /// Gets or sets the display text shown when the boolean value is false.
+ ///
+ public required string FalseText
+ {
+ get;
+ set => SetAndNotify(ref field, value);
+ }
+}
\ No newline at end of file
diff --git a/src/Artemis.Core/Models/ProfileConfiguration/ConfigurationDropdownValue.cs b/src/Artemis.Core/Models/ProfileConfiguration/ConfigurationDropdownValue.cs
new file mode 100644
index 000000000..59136d34b
--- /dev/null
+++ b/src/Artemis.Core/Models/ProfileConfiguration/ConfigurationDropdownValue.cs
@@ -0,0 +1,26 @@
+namespace Artemis.Core;
+
+///
+/// Represents a single option in a configuration dropdown list.
+///
+/// The type of the value that this dropdown option represents.
+public class ConfigurationDropdownValue : CorePropertyChanged
+{
+ ///
+ /// Gets or sets the display name shown to the user for this dropdown option.
+ ///
+ public required string DisplayName
+ {
+ get;
+ set => SetAndNotify(ref field, value);
+ }
+
+ ///
+ /// Gets or sets the actual value associated with this dropdown option.
+ ///
+ public required T Value
+ {
+ get;
+ set => SetAndNotify(ref field, value);
+ }
+}
\ No newline at end of file
diff --git a/src/Artemis.Core/Models/ProfileConfiguration/ConfigurationInputItem.cs b/src/Artemis.Core/Models/ProfileConfiguration/ConfigurationInputItem.cs
new file mode 100644
index 000000000..b92439e5d
--- /dev/null
+++ b/src/Artemis.Core/Models/ProfileConfiguration/ConfigurationInputItem.cs
@@ -0,0 +1,53 @@
+using System.Collections.ObjectModel;
+
+namespace Artemis.Core;
+
+///
+/// Represents a generic base class for configuration items that accept user input.
+///
+/// The type of the value that this configuration item holds.
+public class ConfigurationInputItem : CorePropertyChanged, IConfigurationItem
+{
+ ///
+ /// Gets or sets the display name of the configuration item.
+ ///
+ public required string Name
+ {
+ get;
+ set => SetAndNotify(ref field, value);
+ }
+
+ ///
+ /// Gets or sets the description text that explains the purpose of this configuration item.
+ ///
+ public string? Description
+ {
+ get;
+ set => SetAndNotify(ref field, value);
+ }
+
+ ///
+ /// Gets or sets the current value of the configuration item.
+ ///
+ public T? Value
+ {
+ get;
+ set => SetAndNotify(ref field, value);
+ }
+
+ ///
+ /// Gets or sets the collection of dropdown values for this configuration item.
+ /// When populated, the configuration item will be rendered as a dropdown/combo box.
+ ///
+ public ObservableCollection>? DropdownValues
+ {
+ get;
+ set
+ {
+ if (Equals(value, field))
+ return;
+ field = value;
+ OnPropertyChanged();
+ }
+ } = [];
+}
\ No newline at end of file
diff --git a/src/Artemis.Core/Models/ProfileConfiguration/ConfigurationNumericItem.cs b/src/Artemis.Core/Models/ProfileConfiguration/ConfigurationNumericItem.cs
new file mode 100644
index 000000000..ebfe1a3dd
--- /dev/null
+++ b/src/Artemis.Core/Models/ProfileConfiguration/ConfigurationNumericItem.cs
@@ -0,0 +1,8 @@
+namespace Artemis.Core;
+
+///
+/// Represents a configuration item that accepts numeric input from the user.
+///
+public class ConfigurationNumericItem : ConfigurationInputItem
+{
+}
\ No newline at end of file
diff --git a/src/Artemis.Core/Models/ProfileConfiguration/ConfigurationSKColorItem.cs b/src/Artemis.Core/Models/ProfileConfiguration/ConfigurationSKColorItem.cs
new file mode 100644
index 000000000..5ab7757ae
--- /dev/null
+++ b/src/Artemis.Core/Models/ProfileConfiguration/ConfigurationSKColorItem.cs
@@ -0,0 +1,10 @@
+using SkiaSharp;
+
+namespace Artemis.Core;
+
+///
+/// Represents a configuration item that accepts SKColor input from the user.
+///
+public class ConfigurationSKColorItem : ConfigurationInputItem
+{
+}
\ No newline at end of file
diff --git a/src/Artemis.Core/Models/ProfileConfiguration/ConfigurationSection.cs b/src/Artemis.Core/Models/ProfileConfiguration/ConfigurationSection.cs
new file mode 100644
index 000000000..aa7cd072c
--- /dev/null
+++ b/src/Artemis.Core/Models/ProfileConfiguration/ConfigurationSection.cs
@@ -0,0 +1,32 @@
+using System.Collections.ObjectModel;
+
+namespace Artemis.Core;
+
+///
+/// Represents a configuration section that contains a collection of configuration items.
+///
+public class ConfigurationSection : CorePropertyChanged
+{
+ ///
+ /// Gets or sets the name of the configuration section.
+ ///
+ public required string Name
+ {
+ get;
+ set => SetAndNotify(ref field, value);
+ }
+
+ ///
+ /// Gets or sets the slot number of the configuration section.
+ ///
+ public int Slot
+ {
+ get;
+ set => SetAndNotify(ref field, value);
+ }
+
+ ///
+ /// Gets the collection of configuration items in this section.
+ ///
+ public ObservableCollection Items { get; } = [];
+}
\ No newline at end of file
diff --git a/src/Artemis.Core/Models/ProfileConfiguration/ConfigurationStringItem.cs b/src/Artemis.Core/Models/ProfileConfiguration/ConfigurationStringItem.cs
new file mode 100644
index 000000000..11740f04d
--- /dev/null
+++ b/src/Artemis.Core/Models/ProfileConfiguration/ConfigurationStringItem.cs
@@ -0,0 +1,8 @@
+namespace Artemis.Core;
+
+///
+/// Represents a configuration item that accepts string input from the user.
+///
+public class ConfigurationStringItem : ConfigurationInputItem
+{
+}
\ No newline at end of file
diff --git a/src/Artemis.Core/Models/ProfileConfiguration/ConfigurationTextItem.cs b/src/Artemis.Core/Models/ProfileConfiguration/ConfigurationTextItem.cs
new file mode 100644
index 000000000..403cd5b5c
--- /dev/null
+++ b/src/Artemis.Core/Models/ProfileConfiguration/ConfigurationTextItem.cs
@@ -0,0 +1,16 @@
+namespace Artemis.Core;
+
+///
+/// Represents a configuration item that displays static text.
+///
+public class ConfigurationTextItem : CorePropertyChanged, IConfigurationItem
+{
+ ///
+ /// Gets or sets the text content to display.
+ ///
+ public required string Text
+ {
+ get;
+ set => SetAndNotify(ref field, value);
+ }
+}
\ No newline at end of file
diff --git a/src/Artemis.Core/Models/ProfileConfiguration/IConfigurationItem.cs b/src/Artemis.Core/Models/ProfileConfiguration/IConfigurationItem.cs
new file mode 100644
index 000000000..2aa299640
--- /dev/null
+++ b/src/Artemis.Core/Models/ProfileConfiguration/IConfigurationItem.cs
@@ -0,0 +1,8 @@
+namespace Artemis.Core;
+
+///
+/// Defines a contract for configuration items that can be added to a configuration section.
+///
+public interface IConfigurationItem
+{
+}
\ No newline at end of file
diff --git a/src/Artemis.Core/Models/ProfileConfiguration/ProfileConfiguration.cs b/src/Artemis.Core/Models/ProfileConfiguration/ProfileConfiguration.cs
index 06e0dd205..5a7cffbec 100644
--- a/src/Artemis.Core/Models/ProfileConfiguration/ProfileConfiguration.cs
+++ b/src/Artemis.Core/Models/ProfileConfiguration/ProfileConfiguration.cs
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
+using System.Collections.ObjectModel;
using System.Linq;
using Artemis.Core.Modules;
using Artemis.Storage.Entities.Profile;
@@ -16,26 +17,12 @@ public class ProfileConfiguration : BreakableModel, IStorageModel, IDisposable,
///
public static readonly ProfileConfiguration Empty = new(ProfileCategory.Empty, "Empty", "Empty");
- private ActivationBehaviour _activationBehaviour;
- private bool _activationConditionMet;
- private ProfileCategory _category;
- private Hotkey? _disableHotkey;
private bool _disposed;
- private Hotkey? _enableHotkey;
- private ProfileConfigurationHotkeyMode _hotkeyMode;
- private bool _isMissingModule;
- private bool _isSuspended;
- private bool _fadeInAndOut;
- private Module? _module;
-
- private string _name;
- private int _order;
- private Profile? _profile;
internal ProfileConfiguration(ProfileCategory category, string name, string icon)
{
- _name = name;
- _category = category;
+ Name = name;
+ Category = category;
Entity = new ProfileContainerEntity();
Icon = new ProfileConfigurationIcon(Entity);
@@ -49,8 +36,8 @@ public class ProfileConfiguration : BreakableModel, IStorageModel, IDisposable,
internal ProfileConfiguration(ProfileCategory category, ProfileContainerEntity entity)
{
// Will be loaded from the entity
- _name = null!;
- _category = category;
+ Name = null!;
+ Category = category;
Entity = entity;
Icon = new ProfileConfigurationIcon(Entity);
@@ -64,8 +51,8 @@ public class ProfileConfiguration : BreakableModel, IStorageModel, IDisposable,
///
public string Name
{
- get => _name;
- set => SetAndNotify(ref _name, value);
+ get;
+ set => SetAndNotify(ref field, value);
}
///
@@ -73,8 +60,8 @@ public class ProfileConfiguration : BreakableModel, IStorageModel, IDisposable,
///
public int Order
{
- get => _order;
- set => SetAndNotify(ref _order, value);
+ get;
+ set => SetAndNotify(ref field, value);
}
///
@@ -83,8 +70,8 @@ public class ProfileConfiguration : BreakableModel, IStorageModel, IDisposable,
///
public bool IsSuspended
{
- get => _isSuspended;
- set => SetAndNotify(ref _isSuspended, value);
+ get;
+ set => SetAndNotify(ref field, value);
}
///
@@ -92,8 +79,8 @@ public class ProfileConfiguration : BreakableModel, IStorageModel, IDisposable,
///
public bool IsMissingModule
{
- get => _isMissingModule;
- private set => SetAndNotify(ref _isMissingModule, value);
+ get;
+ private set => SetAndNotify(ref field, value);
}
///
@@ -101,8 +88,8 @@ public class ProfileConfiguration : BreakableModel, IStorageModel, IDisposable,
///
public ProfileCategory Category
{
- get => _category;
- internal set => SetAndNotify(ref _category, value);
+ get;
+ internal set => SetAndNotify(ref field, value);
}
///
@@ -110,8 +97,8 @@ public class ProfileConfiguration : BreakableModel, IStorageModel, IDisposable,
///
public ProfileConfigurationHotkeyMode HotkeyMode
{
- get => _hotkeyMode;
- set => SetAndNotify(ref _hotkeyMode, value);
+ get;
+ set => SetAndNotify(ref field, value);
}
///
@@ -119,8 +106,8 @@ public class ProfileConfiguration : BreakableModel, IStorageModel, IDisposable,
///
public Hotkey? EnableHotkey
{
- get => _enableHotkey;
- set => SetAndNotify(ref _enableHotkey, value);
+ get;
+ set => SetAndNotify(ref field, value);
}
///
@@ -128,8 +115,8 @@ public class ProfileConfiguration : BreakableModel, IStorageModel, IDisposable,
///
public Hotkey? DisableHotkey
{
- get => _disableHotkey;
- set => SetAndNotify(ref _disableHotkey, value);
+ get;
+ set => SetAndNotify(ref field, value);
}
///
@@ -137,8 +124,8 @@ public class ProfileConfiguration : BreakableModel, IStorageModel, IDisposable,
///
public ActivationBehaviour ActivationBehaviour
{
- get => _activationBehaviour;
- set => SetAndNotify(ref _activationBehaviour, value);
+ get;
+ set => SetAndNotify(ref field, value);
}
///
@@ -146,8 +133,8 @@ public class ProfileConfiguration : BreakableModel, IStorageModel, IDisposable,
///
public bool ActivationConditionMet
{
- get => _activationConditionMet;
- private set => SetAndNotify(ref _activationConditionMet, value);
+ get;
+ private set => SetAndNotify(ref field, value);
}
///
@@ -155,8 +142,8 @@ public class ProfileConfiguration : BreakableModel, IStorageModel, IDisposable,
///
public Profile? Profile
{
- get => _profile;
- internal set => SetAndNotify(ref _profile, value);
+ get;
+ internal set => SetAndNotify(ref field, value);
}
///
@@ -164,8 +151,17 @@ public class ProfileConfiguration : BreakableModel, IStorageModel, IDisposable,
///
public bool FadeInAndOut
{
- get => _fadeInAndOut;
- set => SetAndNotify(ref _fadeInAndOut, value);
+ get;
+ set => SetAndNotify(ref field, value);
+ }
+
+ ///
+ /// Gets or sets a boolean indicating whether this profile is configurable via its .
+ ///
+ public bool IsConfigurable
+ {
+ get;
+ set => SetAndNotify(ref field, value);
}
///
@@ -173,14 +169,19 @@ public class ProfileConfiguration : BreakableModel, IStorageModel, IDisposable,
///
public Module? Module
{
- get => _module;
+ get;
set
{
- SetAndNotify(ref _module, value);
+ SetAndNotify(ref field, value);
IsMissingModule = false;
}
}
+ ///
+ /// Gets the configuration sections of this profile configuration.
+ ///
+ public ObservableCollection ConfigurationSections { get; } = [];
+
///
/// Gets the icon configuration
///
@@ -301,6 +302,30 @@ public class ProfileConfiguration : BreakableModel, IStorageModel, IDisposable,
EnableHotkey = Entity.ProfileConfiguration.EnableHotkey != null ? new Hotkey(Entity.ProfileConfiguration.EnableHotkey) : null;
DisableHotkey = Entity.ProfileConfiguration.DisableHotkey != null ? new Hotkey(Entity.ProfileConfiguration.DisableHotkey) : null;
+
+ // Placeholder configuration sections
+ ConfigurationSections.Clear();
+ ConfigurationSections.Add(new ConfigurationSection()
+ {
+ Name = "General (slot 0)",
+ Slot = 0,
+ });
+ ConfigurationSections.Add(new ConfigurationSection()
+ {
+ Name = "Other (slot 1)",
+ Slot = 1
+ });
+ ConfigurationSections.Add(new ConfigurationSection()
+ {
+ Name = "Something else (slot 2)",
+ Slot = 2
+ });
+ ConfigurationSections[0].Items.Add(new ConfigurationTextItem() {Text = "This is a placeholder text item in the General section."});
+ ConfigurationSections[0].Items.Add(new ConfigurationNumericItem() {Name = "Numeric item"});
+ ConfigurationSections[0].Items.Add(new ConfigurationBooleanItem() {Name = "Do the thing?", TrueText = "Absolutely", FalseText = "Nope"});
+ ConfigurationSections[1].Items.Add(new ConfigurationTextItem() {Text = "This is a placeholder text item in the Other section."});
+ ConfigurationSections[2].Items.Add(new ConfigurationTextItem() {Text = "This is a placeholder text item in the Something else section."});
+ ConfigurationSections[2].Items.Add(new ConfigurationTextItem() {Text = "This is another placeholder text item in the Something else section."});
}
///
diff --git a/src/Artemis.Core/Services/Input/InputService.cs b/src/Artemis.Core/Services/Input/InputService.cs
index abf0487d9..f855e566e 100644
--- a/src/Artemis.Core/Services/Input/InputService.cs
+++ b/src/Artemis.Core/Services/Input/InputService.cs
@@ -24,6 +24,9 @@ internal class InputService : IInputService
_deviceService.DeviceRemoved += DeviceServiceOnDevicesModified;
BustIdentifierCache();
}
+
+ public int CursorX { get; private set; }
+ public int CursorY { get; private set; }
protected virtual void OnKeyboardKeyUpDown(ArtemisKeyboardKeyUpDownEventArgs e)
{
@@ -426,6 +429,9 @@ internal class InputService : IInputService
private void InputProviderOnMouseMoveDataReceived(object? sender, InputProviderMouseMoveEventArgs e)
{
+ CursorX = e.CursorX;
+ CursorY = e.CursorY;
+
OnMouseMove(new ArtemisMouseMoveEventArgs(e.Device, e.CursorX, e.CursorY, e.DeltaX, e.DeltaY));
// _logger.Verbose("Mouse move data: XY: {X},{Y} - delta XY: {deltaX},{deltaY} - device: {device} ", e.CursorX, e.CursorY, e.DeltaX, e.DeltaY, e.Device);
}
diff --git a/src/Artemis.Core/Services/Input/Interfaces/IInputService.cs b/src/Artemis.Core/Services/Input/Interfaces/IInputService.cs
index 5344edf2d..6d393aa40 100644
--- a/src/Artemis.Core/Services/Input/Interfaces/IInputService.cs
+++ b/src/Artemis.Core/Services/Input/Interfaces/IInputService.cs
@@ -12,6 +12,16 @@ public interface IInputService : IArtemisService, IDisposable
///
KeyboardToggleStatus KeyboardToggleStatus { get; }
+ ///
+ /// Gets the last reported cursor X position
+ ///
+ public int CursorX { get; }
+
+ ///
+ /// Gets the last reported cursor Y position
+ ///
+ public int CursorY { get; }
+
///
/// Adds an input provided
///
diff --git a/src/Artemis.UI.Shared/Routing/Router/NavigationArguments.cs b/src/Artemis.UI.Shared/Routing/Router/NavigationArguments.cs
index a064e3f69..74e65e7fe 100644
--- a/src/Artemis.UI.Shared/Routing/Router/NavigationArguments.cs
+++ b/src/Artemis.UI.Shared/Routing/Router/NavigationArguments.cs
@@ -12,6 +12,7 @@ public class NavigationArguments
Router = router;
Options = options;
Path = path;
+ PathSegments = path.Split('/', StringSplitOptions.RemoveEmptyEntries);
RouteParameters = routeParameters;
SegmentParameters = [];
}
@@ -31,6 +32,11 @@ public class NavigationArguments
///
public string Path { get; }
+ ///
+ /// Gets the segments of the path that is being navigated to.
+ ///
+ public string[] PathSegments { get; }
+
///
/// GEts an array of all parameters provided to this route.
///
diff --git a/src/Artemis.UI/Artemis.UI.csproj b/src/Artemis.UI/Artemis.UI.csproj
index b750e0ae3..e310d146d 100644
--- a/src/Artemis.UI/Artemis.UI.csproj
+++ b/src/Artemis.UI/Artemis.UI.csproj
@@ -35,4 +35,15 @@
+
+
+
+
+
+
+
+ DesignProfileView.axaml
+ Code
+
+
\ No newline at end of file
diff --git a/src/Artemis.UI/Routing/Routes.cs b/src/Artemis.UI/Routing/Routes.cs
index d0f043e2c..e89dfe4eb 100644
--- a/src/Artemis.UI/Routing/Routes.cs
+++ b/src/Artemis.UI/Routing/Routes.cs
@@ -67,6 +67,8 @@ namespace Artemis.UI.Routing
]),
new RouteRegistration("profile/{profileConfigurationId:guid}", [
new RouteRegistration("editor"),
+ new RouteRegistration("configure"),
+ new RouteRegistration("design"),
new RouteRegistration("workshop")
]),
];
diff --git a/src/Artemis.UI/Screens/ProfileEditor/ConfigurationPanels/Preview/PreviewView.axaml b/src/Artemis.UI/Screens/ProfileEditor/ConfigurationPanels/Preview/PreviewView.axaml
new file mode 100644
index 000000000..d700793f9
--- /dev/null
+++ b/src/Artemis.UI/Screens/ProfileEditor/ConfigurationPanels/Preview/PreviewView.axaml
@@ -0,0 +1,71 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Artemis.UI/Screens/ProfileEditor/ConfigurationPanels/Preview/PreviewView.axaml.cs b/src/Artemis.UI/Screens/ProfileEditor/ConfigurationPanels/Preview/PreviewView.axaml.cs
new file mode 100644
index 000000000..c7a1ede60
--- /dev/null
+++ b/src/Artemis.UI/Screens/ProfileEditor/ConfigurationPanels/Preview/PreviewView.axaml.cs
@@ -0,0 +1,98 @@
+using System;
+using System.Linq;
+using System.Reactive.Disposables;
+using System.Reactive.Disposables.Fluent;
+using System.Reactive.Linq;
+using Artemis.UI.Screens.ProfileEditor.VisualEditor;
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Controls.PanAndZoom;
+using Avalonia.Input;
+using Avalonia.Markup.Xaml;
+using Avalonia.Media;
+using Avalonia.Threading;
+using ReactiveUI;
+using ReactiveUI.Avalonia;
+
+namespace Artemis.UI.Screens.ProfileEditor.ConfigurationPanels.Preview;
+
+public partial class PreviewView : ReactiveUserControl
+{
+ private bool _movedByUser;
+
+ public PreviewView()
+ {
+ InitializeComponent();
+
+ ZoomBorder.PropertyChanged += ZoomBorderOnPropertyChanged;
+ ZoomBorder.PointerMoved += ZoomBorderOnPointerMoved;
+ ZoomBorder.PointerWheelChanged += ZoomBorderOnPointerWheelChanged;
+ UpdateZoomBorderBackground();
+
+ this.WhenActivated(d =>
+ {
+ PreviewViewModel vm = ViewModel!;
+ vm.AutoFitRequested += ViewModelOnAutoFitRequested;
+ Disposable.Create(() => vm.AutoFitRequested -= ViewModelOnAutoFitRequested).DisposeWith(d);
+ });
+
+ this.WhenAnyValue(v => v.Bounds).Where(_ => !_movedByUser).Subscribe(_ => AutoFit(true));
+ }
+
+ private void ZoomBorderOnPointerWheelChanged(object? sender, PointerWheelEventArgs e)
+ {
+ _movedByUser = true;
+ }
+
+ private void ZoomBorderOnPointerMoved(object? sender, PointerEventArgs e)
+ {
+ if (e.GetCurrentPoint(ZoomBorder).Properties.IsMiddleButtonPressed)
+ _movedByUser = true;
+ }
+
+ private void ViewModelOnAutoFitRequested(object? sender, EventArgs e)
+ {
+ Dispatcher.UIThread.Post(() => AutoFit(false));
+ }
+
+ private void ZoomBorderOnPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e)
+ {
+ if (e.Property.Name == nameof(ZoomBorder.Background))
+ UpdateZoomBorderBackground();
+ }
+
+ private void UpdateZoomBorderBackground()
+ {
+ if (ZoomBorder.Background is VisualBrush visualBrush)
+ visualBrush.DestinationRect = new RelativeRect(ZoomBorder.OffsetX, ZoomBorder.OffsetY, 20, 20, RelativeUnit.Absolute);
+ }
+
+
+ private void ZoomBorder_OnZoomChanged(object sender, ZoomChangedEventArgs e)
+ {
+ UpdateZoomBorderBackground();
+ }
+
+ private void AutoFit(bool skipTransitions)
+ {
+ if (ViewModel == null || !ViewModel.Devices.Any())
+ return;
+
+ double left = ViewModel.Devices.Select(d => d.Rectangle.Left).Min();
+ double top = ViewModel.Devices.Select(d => d.Rectangle.Top).Min();
+ double bottom = ViewModel.Devices.Select(d => d.Rectangle.Bottom).Max();
+ double right = ViewModel.Devices.Select(d => d.Rectangle.Right).Max();
+
+ // Add a 10 pixel margin around the rect
+ Rect scriptRect = new(new Point(left - 10, top - 10), new Point(right + 10, bottom + 10));
+
+ // The scale depends on the available space
+ double scale = Math.Min(3, Math.Min(Bounds.Width / scriptRect.Width, Bounds.Height / scriptRect.Height));
+
+ // Pan and zoom to make the script fit
+ ZoomBorder.Zoom(scale, 0, 0, skipTransitions);
+ ZoomBorder.Pan(Bounds.Center.X - scriptRect.Center.X * scale, Bounds.Center.Y - scriptRect.Center.Y * scale, skipTransitions);
+
+ _movedByUser = false;
+ }
+}
\ No newline at end of file
diff --git a/src/Artemis.UI/Screens/ProfileEditor/ConfigurationPanels/Preview/PreviewViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/ConfigurationPanels/Preview/PreviewViewModel.cs
new file mode 100644
index 000000000..0ee5048a0
--- /dev/null
+++ b/src/Artemis.UI/Screens/ProfileEditor/ConfigurationPanels/Preview/PreviewViewModel.cs
@@ -0,0 +1,36 @@
+using System;
+using System.Collections.ObjectModel;
+using System.Linq;
+using System.Reactive.Disposables.Fluent;
+using Artemis.Core;
+using Artemis.Core.Services;
+using Artemis.UI.Shared;
+using Artemis.UI.Shared.Services.ProfileEditor;
+using ReactiveUI;
+
+namespace Artemis.UI.Screens.ProfileEditor.ConfigurationPanels.Preview;
+
+public class PreviewViewModel : ActivatableViewModelBase
+{
+ private ObservableAsPropertyHelper? _profileConfiguration;
+
+ public PreviewViewModel(IProfileEditorService profileEditorService, IDeviceService deviceService)
+ {
+ Devices = new ObservableCollection(deviceService.EnabledDevices.OrderBy(d => d.ZIndex));
+
+ this.WhenActivated(d =>
+ {
+ _profileConfiguration = profileEditorService.ProfileConfiguration.ToProperty(this, vm => vm.ProfileConfiguration).DisposeWith(d);
+ });
+ }
+
+ public ProfileConfiguration? ProfileConfiguration => _profileConfiguration?.Value;
+ public ObservableCollection Devices { get; }
+
+ public void RequestAutoFit()
+ {
+ AutoFitRequested?.Invoke(this, EventArgs.Empty);
+ }
+
+ public event EventHandler? AutoFitRequested;
+}
\ No newline at end of file
diff --git a/src/Artemis.UI/Screens/ProfileEditor/ConfigurationPanels/Section/ConfigurationSectionView.axaml b/src/Artemis.UI/Screens/ProfileEditor/ConfigurationPanels/Section/ConfigurationSectionView.axaml
new file mode 100644
index 000000000..407fdd53e
--- /dev/null
+++ b/src/Artemis.UI/Screens/ProfileEditor/ConfigurationPanels/Section/ConfigurationSectionView.axaml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+ Test
+
+
+
+
diff --git a/src/Artemis.UI/Screens/ProfileEditor/ConfigurationPanels/Section/ConfigurationSectionView.axaml.cs b/src/Artemis.UI/Screens/ProfileEditor/ConfigurationPanels/Section/ConfigurationSectionView.axaml.cs
new file mode 100644
index 000000000..a7ba7cf2b
--- /dev/null
+++ b/src/Artemis.UI/Screens/ProfileEditor/ConfigurationPanels/Section/ConfigurationSectionView.axaml.cs
@@ -0,0 +1,14 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Markup.Xaml;
+using ReactiveUI.Avalonia;
+
+namespace Artemis.UI.Screens.ProfileEditor.ConfigurationPanels.Section;
+
+public partial class ConfigurationSectionView: ReactiveUserControl
+{
+ public ConfigurationSectionView()
+ {
+ InitializeComponent();
+ }
+}
\ No newline at end of file
diff --git a/src/Artemis.UI/Screens/ProfileEditor/ConfigurationPanels/Section/ConfigurationSectionViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/ConfigurationPanels/Section/ConfigurationSectionViewModel.cs
new file mode 100644
index 000000000..70083013c
--- /dev/null
+++ b/src/Artemis.UI/Screens/ProfileEditor/ConfigurationPanels/Section/ConfigurationSectionViewModel.cs
@@ -0,0 +1,14 @@
+using Artemis.Core;
+using Artemis.UI.Shared;
+
+namespace Artemis.UI.Screens.ProfileEditor.ConfigurationPanels.Section;
+
+public class ConfigurationSectionViewModel : ActivatableViewModelBase
+{
+ public ConfigurationSection ConfigurationSection { get; }
+
+ public ConfigurationSectionViewModel(ConfigurationSection configurationSection)
+ {
+ ConfigurationSection = configurationSection;
+ }
+}
\ No newline at end of file
diff --git a/src/Artemis.UI/Screens/ProfileEditor/ConfigurationPanels/Slot/SlotView.axaml b/src/Artemis.UI/Screens/ProfileEditor/ConfigurationPanels/Slot/SlotView.axaml
new file mode 100644
index 000000000..05ecf5753
--- /dev/null
+++ b/src/Artemis.UI/Screens/ProfileEditor/ConfigurationPanels/Slot/SlotView.axaml
@@ -0,0 +1,10 @@
+
+ Welcome to Avalonia!
+
diff --git a/src/Artemis.UI/Screens/ProfileEditor/ConfigurationPanels/Slot/SlotView.axaml.cs b/src/Artemis.UI/Screens/ProfileEditor/ConfigurationPanels/Slot/SlotView.axaml.cs
new file mode 100644
index 000000000..f0513e279
--- /dev/null
+++ b/src/Artemis.UI/Screens/ProfileEditor/ConfigurationPanels/Slot/SlotView.axaml.cs
@@ -0,0 +1,13 @@
+using Avalonia;
+using Avalonia.Controls;
+using Avalonia.Markup.Xaml;
+
+namespace Artemis.UI.Screens.ProfileEditor.ConfigurationPanels.Slot;
+
+public partial class SlotView : UserControl
+{
+ public SlotView()
+ {
+ InitializeComponent();
+ }
+}
\ No newline at end of file
diff --git a/src/Artemis.UI/Screens/ProfileEditor/ConfigurationPanels/Slot/SlotViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/ConfigurationPanels/Slot/SlotViewModel.cs
new file mode 100644
index 000000000..d24258f52
--- /dev/null
+++ b/src/Artemis.UI/Screens/ProfileEditor/ConfigurationPanels/Slot/SlotViewModel.cs
@@ -0,0 +1,8 @@
+using Artemis.UI.Shared;
+
+namespace Artemis.UI.Screens.ProfileEditor.ConfigurationPanels.Slot;
+
+public class SlotViewModel : ActivatableViewModelBase
+{
+
+}
\ No newline at end of file
diff --git a/src/Artemis.UI/Screens/ProfileEditor/ConfigureProfileView.axaml b/src/Artemis.UI/Screens/ProfileEditor/ConfigureProfileView.axaml
new file mode 100644
index 000000000..0ce41a1ce
--- /dev/null
+++ b/src/Artemis.UI/Screens/ProfileEditor/ConfigureProfileView.axaml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Artemis.UI/Screens/ProfileEditor/ConfigureProfileView.axaml.cs b/src/Artemis.UI/Screens/ProfileEditor/ConfigureProfileView.axaml.cs
new file mode 100644
index 000000000..e412b9a6e
--- /dev/null
+++ b/src/Artemis.UI/Screens/ProfileEditor/ConfigureProfileView.axaml.cs
@@ -0,0 +1,11 @@
+using ReactiveUI.Avalonia;
+
+namespace Artemis.UI.Screens.ProfileEditor;
+
+public partial class ConfigureProfileView : ReactiveUserControl
+{
+ public ConfigureProfileView()
+ {
+ InitializeComponent();
+ }
+}
\ No newline at end of file
diff --git a/src/Artemis.UI/Screens/ProfileEditor/ConfigureProfileViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/ConfigureProfileViewModel.cs
new file mode 100644
index 000000000..10ee0492f
--- /dev/null
+++ b/src/Artemis.UI/Screens/ProfileEditor/ConfigureProfileViewModel.cs
@@ -0,0 +1,91 @@
+using System;
+using System.Collections.ObjectModel;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using Artemis.Core;
+using Artemis.Core.Services;
+using Artemis.UI.Screens.ProfileEditor.ConfigurationPanels.Preview;
+using Artemis.UI.Screens.ProfileEditor.ConfigurationPanels.Section;
+using Artemis.UI.Shared.Routing;
+using Artemis.UI.Shared.Services.ProfileEditor;
+using DynamicData;
+using PropertyChanged.SourceGenerator;
+
+namespace Artemis.UI.Screens.ProfileEditor;
+
+public partial class ConfigureProfileViewModel : RoutableScreen
+{
+ private readonly IProfileService _profileService;
+ private readonly IProfileEditorService _profileEditorService;
+ private readonly SourceList _configurationSections;
+
+ [Notify] private ProfileConfiguration? _profileConfiguration;
+
+ public ConfigureProfileViewModel(IProfileService profileService, IProfileEditorService profileEditorService, PreviewViewModel previewViewModel,
+ Func getConfigurationSectionViewModel)
+ {
+ _profileService = profileService;
+ _profileEditorService = profileEditorService;
+ ParameterSource = ParameterSource.Route;
+
+ PreviewViewModel = previewViewModel;
+
+ _configurationSections = new SourceList();
+ _configurationSections.Connect()
+ .Filter(s => s.Slot == 0)
+ .Transform(getConfigurationSectionViewModel)
+ .Bind(out ReadOnlyObservableCollection bottomLeftSections)
+ .Subscribe();
+ _configurationSections.Connect()
+ .Filter(s => s.Slot == 1)
+ .Transform(getConfigurationSectionViewModel)
+ .Bind(out ReadOnlyObservableCollection bottomRightSections)
+ .Subscribe();
+ _configurationSections.Connect()
+ .Filter(s => s.Slot == 2)
+ .Transform(getConfigurationSectionViewModel)
+ .Bind(out ReadOnlyObservableCollection sideSections)
+ .Subscribe();
+
+ BottomLeftSections = bottomLeftSections;
+ BottomRightSections = bottomRightSections;
+ SideSections = sideSections;
+ }
+
+ public PreviewViewModel PreviewViewModel { get; }
+ public ReadOnlyObservableCollection BottomLeftSections { get; private set; }
+ public ReadOnlyObservableCollection BottomRightSections { get; private set; }
+ public ReadOnlyObservableCollection SideSections { get; private set; }
+
+ ///
+ public override async Task OnNavigating(ProfileViewModelParameters parameters, NavigationArguments args, CancellationToken cancellationToken)
+ {
+ ProfileConfiguration? profileConfiguration = _profileService.ProfileCategories.SelectMany(c => c.ProfileConfigurations).FirstOrDefault(c => c.ProfileId == parameters.ProfileId);
+
+ // If the profile doesn't exist, cancel navigation
+ if (profileConfiguration == null)
+ {
+ args.Cancel();
+ return;
+ }
+
+ await _profileEditorService.ChangeCurrentProfileConfiguration(profileConfiguration);
+ ProfileConfiguration = profileConfiguration;
+ _configurationSections.Edit(editableSections =>
+ {
+ editableSections.Clear();
+ editableSections.AddRange(profileConfiguration.ConfigurationSections);
+ });
+ }
+
+ ///
+ public override async Task OnClosing(NavigationArguments args)
+ {
+ if (!args.Path.StartsWith("profile"))
+ {
+ ProfileConfiguration = null;
+ await _profileEditorService.ChangeCurrentProfileConfiguration(null);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Artemis.UI/Screens/ProfileEditor/DesignProfileView.axaml b/src/Artemis.UI/Screens/ProfileEditor/DesignProfileView.axaml
new file mode 100644
index 000000000..4cd8ad867
--- /dev/null
+++ b/src/Artemis.UI/Screens/ProfileEditor/DesignProfileView.axaml
@@ -0,0 +1,14 @@
+
+
+
+ Design
+
+
+
\ No newline at end of file
diff --git a/src/Artemis.UI/Screens/ProfileEditor/DesignProfileView.axaml.cs b/src/Artemis.UI/Screens/ProfileEditor/DesignProfileView.axaml.cs
new file mode 100644
index 000000000..d291cfc51
--- /dev/null
+++ b/src/Artemis.UI/Screens/ProfileEditor/DesignProfileView.axaml.cs
@@ -0,0 +1,11 @@
+using ReactiveUI.Avalonia;
+
+namespace Artemis.UI.Screens.ProfileEditor;
+
+public partial class DesignProfileView : ReactiveUserControl
+{
+ public DesignProfileView()
+ {
+ InitializeComponent();
+ }
+}
\ No newline at end of file
diff --git a/src/Artemis.UI/Screens/ProfileEditor/DesignProfileViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/DesignProfileViewModel.cs
new file mode 100644
index 000000000..0cecd7f7c
--- /dev/null
+++ b/src/Artemis.UI/Screens/ProfileEditor/DesignProfileViewModel.cs
@@ -0,0 +1,65 @@
+using System;
+using System.Linq;
+using System.Threading;
+using System.Threading.Tasks;
+using Artemis.Core;
+using Artemis.Core.Services;
+using Artemis.UI.Screens.Workshop.Library.Tabs;
+using Artemis.UI.Shared.Routing;
+using Artemis.WebClient.Workshop.Models;
+using Artemis.WebClient.Workshop.Services;
+using PropertyChanged.SourceGenerator;
+
+namespace Artemis.UI.Screens.ProfileEditor;
+
+public partial class DesignProfileViewModel : RoutableScreen
+{
+ private readonly IProfileService _profileService;
+ private readonly IWorkshopService _workshopService;
+ private readonly IRouter _router;
+ private readonly Func _getInstalledTabItemViewModel;
+
+ [Notify] private ProfileConfiguration? _profileConfiguration;
+ [Notify] private InstalledEntry? _workshopEntry;
+ [Notify] private InstalledTabItemViewModel? _entryViewModel;
+
+ public DesignProfileViewModel(IProfileService profileService, IWorkshopService workshopService, IRouter router, Func getInstalledTabItemViewModel)
+ {
+ _profileService = profileService;
+ _workshopService = workshopService;
+ _router = router;
+ _getInstalledTabItemViewModel = getInstalledTabItemViewModel;
+ ParameterSource = ParameterSource.Route;
+ }
+
+ public async Task DisableAutoUpdate()
+ {
+ if (WorkshopEntry != null)
+ {
+ _workshopService.SetAutoUpdate(WorkshopEntry, false);
+ }
+
+ if (ProfileConfiguration != null)
+ {
+ await _router.Navigate($"profile/{ProfileConfiguration.ProfileId}/editor");
+ }
+ }
+
+ public override Task OnNavigating(ProfileViewModelParameters parameters, NavigationArguments args, CancellationToken cancellationToken)
+ {
+ ProfileConfiguration = _profileService.ProfileCategories.SelectMany(c => c.ProfileConfigurations).FirstOrDefault(c => c.ProfileId == parameters.ProfileId);
+
+ // If the profile doesn't exist, cancel navigation
+ if (ProfileConfiguration == null)
+ {
+ args.Cancel();
+ return Task.CompletedTask;
+ }
+
+ WorkshopEntry = _workshopService.GetInstalledEntryByProfile(ProfileConfiguration);
+ EntryViewModel = WorkshopEntry != null ? _getInstalledTabItemViewModel(WorkshopEntry) : null;
+ if (EntryViewModel != null)
+ EntryViewModel.DisplayManagement = false;
+ return Task.CompletedTask;
+ }
+}
\ No newline at end of file
diff --git a/src/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/VisualEditorView.axaml.cs b/src/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/VisualEditorView.axaml.cs
index 016859e94..11e1a5c0e 100644
--- a/src/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/VisualEditorView.axaml.cs
+++ b/src/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/VisualEditorView.axaml.cs
@@ -61,7 +61,7 @@ public partial class VisualEditorView : ReactiveUserControl= 3)
+ return;
- // If the profile is from the workshop, redirect to the workshop page
- InstalledEntry? workshopEntry = _workshopService.GetInstalledEntryByProfile(profileConfiguration);
- if (workshopEntry != null && workshopEntry.AutoUpdate)
+ // If the profile is configurable, go to the configuration page
+ if (profileConfiguration.IsConfigurable)
{
- if (!args.Path.EndsWith("workshop"))
- await args.Router.Navigate($"profile/{parameters.ProfileId}/workshop");
+ await args.Router.Navigate($"profile/{parameters.ProfileId}/configure");
+ }
+ // Otherwise either the workshop notice or the editor
+ else
+ {
+ InstalledEntry? workshopEntry = _workshopService.GetInstalledEntryByProfile(profileConfiguration);
+ if (workshopEntry != null && workshopEntry.AutoUpdate)
+ await args.Router.Navigate($"profile/{parameters.ProfileId}/workshop");
+ else
+ await args.Router.Navigate($"profile/{parameters.ProfileId}/editor");
}
- // Otherwise, show the profile editor if not already on the editor page
- else if (!args.Path.EndsWith("editor"))
- await args.Router.Navigate($"profile/{parameters.ProfileId}/editor");
}
}
diff --git a/src/Artemis.UI/Screens/Sidebar/SidebarCategoryViewModel.cs b/src/Artemis.UI/Screens/Sidebar/SidebarCategoryViewModel.cs
index eec34a018..9e4ecdbbb 100644
--- a/src/Artemis.UI/Screens/Sidebar/SidebarCategoryViewModel.cs
+++ b/src/Artemis.UI/Screens/Sidebar/SidebarCategoryViewModel.cs
@@ -65,7 +65,7 @@ public partial class SidebarCategoryViewModel : ActivatableViewModelBase
// Navigate on selection change
this.WhenAnyValue(vm => vm.SelectedProfileConfiguration)
.WhereNotNull()
- .Subscribe(s => _router.Navigate($"profile/{s.ProfileConfiguration.ProfileId}/editor", new RouterNavigationOptions {IgnoreOnPartialMatch = true, RecycleScreens = false}))
+ .Subscribe(s => _router.Navigate($"profile/{s.ProfileConfiguration.ProfileId}", new RouterNavigationOptions {IgnoreOnPartialMatch = true, RecycleScreens = false}))
.DisposeWith(d);
_router.CurrentPath.WhereNotNull().Subscribe(r => SelectedProfileConfiguration = ProfileConfigurations.FirstOrDefault(c => c.Matches(r))).DisposeWith(d);
diff --git a/src/Artemis.UI/Screens/SurfaceEditor/SurfaceEditorView.axaml.cs b/src/Artemis.UI/Screens/SurfaceEditor/SurfaceEditorView.axaml.cs
index 01e5ce71b..6b16e2aff 100644
--- a/src/Artemis.UI/Screens/SurfaceEditor/SurfaceEditorView.axaml.cs
+++ b/src/Artemis.UI/Screens/SurfaceEditor/SurfaceEditorView.axaml.cs
@@ -48,6 +48,6 @@ public partial class SurfaceEditorView : ReactiveUserControl
private void UpdateZoomBorderBackground()
{
if (NodeScriptZoomBorder.Background is VisualBrush visualBrush)
- visualBrush.DestinationRect = new RelativeRect(NodeScriptZoomBorder.OffsetX * -1, NodeScriptZoomBorder.OffsetY * -1, 20, 20, RelativeUnit.Absolute);
+ visualBrush.DestinationRect = new RelativeRect(NodeScriptZoomBorder.OffsetX, NodeScriptZoomBorder.OffsetY, 20, 20, RelativeUnit.Absolute);
}
diff --git a/src/Artemis.UI/Screens/Workshop/Profile/ProfilePreviewView.axaml.cs b/src/Artemis.UI/Screens/Workshop/Profile/ProfilePreviewView.axaml.cs
index 37dc44f8a..49438576a 100644
--- a/src/Artemis.UI/Screens/Workshop/Profile/ProfilePreviewView.axaml.cs
+++ b/src/Artemis.UI/Screens/Workshop/Profile/ProfilePreviewView.axaml.cs
@@ -46,7 +46,7 @@ public partial class ProfilePreviewView : ReactiveUserControl layers).Subscribe();
GoBack = ReactiveCommand.Create(() => State.ChangeScreen());
- Continue = ReactiveCommand.Create(ExecuteContinue, _layers.Connect().AutoRefresh(l => l.AdaptionHintCount).Filter(l => l.AdaptionHintCount == 0).IsEmpty());
+ Continue = ReactiveCommand.Create(ExecuteContinue);
EditAdaptionHints = ReactiveCommand.CreateFromTask(ExecuteEditAdaptionHints);
Layers = layers;
diff --git a/src/Artemis.WebClient.Workshop/WorkshopConstants.cs b/src/Artemis.WebClient.Workshop/WorkshopConstants.cs
index 73a6f14dd..9899d65a7 100644
--- a/src/Artemis.WebClient.Workshop/WorkshopConstants.cs
+++ b/src/Artemis.WebClient.Workshop/WorkshopConstants.cs
@@ -4,10 +4,10 @@ public static class WorkshopConstants
{
// This is so I can never accidentally release with localhost
#if DEBUG
- public const string AUTHORITY_URL = "https://localhost:5001";
- public const string WORKSHOP_URL = "https://localhost:7281";
- // public const string AUTHORITY_URL = "https://identity.artemis-rgb.com";
- // public const string WORKSHOP_URL = "https://workshop.artemis-rgb.com";
+ // public const string AUTHORITY_URL = "https://localhost:5001";
+ // public const string WORKSHOP_URL = "https://localhost:7281";
+ public const string AUTHORITY_URL = "https://identity.artemis-rgb.com";
+ public const string WORKSHOP_URL = "https://workshop.artemis-rgb.com";
#else
public const string AUTHORITY_URL = "https://identity.artemis-rgb.com";
public const string WORKSHOP_URL = "https://workshop.artemis-rgb.com";