From 3004293051613daefedb413d141d515698822446 Mon Sep 17 00:00:00 2001 From: Robert Date: Fri, 23 Jan 2026 21:15:58 +0100 Subject: [PATCH] Initial setup of the models and view models --- .../ConfigurationBooleanItem.cs | 25 ++++ .../ConfigurationDropdownValue.cs | 26 ++++ .../ConfigurationInputItem.cs | 53 ++++++++ .../ConfigurationNumericItem.cs | 8 ++ .../ConfigurationSKColorItem.cs | 10 ++ .../ConfigurationSection.cs | 32 +++++ .../ConfigurationStringItem.cs | 8 ++ .../ConfigurationTextItem.cs | 16 +++ .../IConfigurationItem.cs | 8 ++ .../ProfileConfiguration.cs | 113 +++++++++++------- .../Services/Input/InputService.cs | 6 + .../Input/Interfaces/IInputService.cs | 10 ++ .../Routing/Router/NavigationArguments.cs | 6 + src/Artemis.UI/Artemis.UI.csproj | 11 ++ src/Artemis.UI/Routing/Routes.cs | 2 + .../Preview/PreviewView.axaml | 71 +++++++++++ .../Preview/PreviewView.axaml.cs | 98 +++++++++++++++ .../Preview/PreviewViewModel.cs | 36 ++++++ .../Section/ConfigurationSectionView.axaml | 18 +++ .../Section/ConfigurationSectionView.axaml.cs | 14 +++ .../Section/ConfigurationSectionViewModel.cs | 14 +++ .../ConfigurationPanels/Slot/SlotView.axaml | 10 ++ .../Slot/SlotView.axaml.cs | 13 ++ .../ConfigurationPanels/Slot/SlotViewModel.cs | 8 ++ .../ProfileEditor/ConfigureProfileView.axaml | 18 +++ .../ConfigureProfileView.axaml.cs | 11 ++ .../ConfigureProfileViewModel.cs | 91 ++++++++++++++ .../ProfileEditor/DesignProfileView.axaml | 14 +++ .../ProfileEditor/DesignProfileView.axaml.cs | 11 ++ .../ProfileEditor/DesignProfileViewModel.cs | 65 ++++++++++ .../VisualEditor/VisualEditorView.axaml.cs | 2 +- .../Screens/ProfileEditor/ProfileViewModel.cs | 24 ++-- .../Sidebar/SidebarCategoryViewModel.cs | 2 +- .../SurfaceEditor/SurfaceEditorView.axaml.cs | 2 +- .../VisualScripting/NodeScriptView.axaml.cs | 2 +- .../Profile/ProfilePreviewView.axaml.cs | 2 +- .../ProfileAdaptionHintsStepViewModel.cs | 2 +- .../WorkshopConstants.cs | 8 +- 38 files changed, 808 insertions(+), 62 deletions(-) create mode 100644 src/Artemis.Core/Models/ProfileConfiguration/ConfigurationBooleanItem.cs create mode 100644 src/Artemis.Core/Models/ProfileConfiguration/ConfigurationDropdownValue.cs create mode 100644 src/Artemis.Core/Models/ProfileConfiguration/ConfigurationInputItem.cs create mode 100644 src/Artemis.Core/Models/ProfileConfiguration/ConfigurationNumericItem.cs create mode 100644 src/Artemis.Core/Models/ProfileConfiguration/ConfigurationSKColorItem.cs create mode 100644 src/Artemis.Core/Models/ProfileConfiguration/ConfigurationSection.cs create mode 100644 src/Artemis.Core/Models/ProfileConfiguration/ConfigurationStringItem.cs create mode 100644 src/Artemis.Core/Models/ProfileConfiguration/ConfigurationTextItem.cs create mode 100644 src/Artemis.Core/Models/ProfileConfiguration/IConfigurationItem.cs create mode 100644 src/Artemis.UI/Screens/ProfileEditor/ConfigurationPanels/Preview/PreviewView.axaml create mode 100644 src/Artemis.UI/Screens/ProfileEditor/ConfigurationPanels/Preview/PreviewView.axaml.cs create mode 100644 src/Artemis.UI/Screens/ProfileEditor/ConfigurationPanels/Preview/PreviewViewModel.cs create mode 100644 src/Artemis.UI/Screens/ProfileEditor/ConfigurationPanels/Section/ConfigurationSectionView.axaml create mode 100644 src/Artemis.UI/Screens/ProfileEditor/ConfigurationPanels/Section/ConfigurationSectionView.axaml.cs create mode 100644 src/Artemis.UI/Screens/ProfileEditor/ConfigurationPanels/Section/ConfigurationSectionViewModel.cs create mode 100644 src/Artemis.UI/Screens/ProfileEditor/ConfigurationPanels/Slot/SlotView.axaml create mode 100644 src/Artemis.UI/Screens/ProfileEditor/ConfigurationPanels/Slot/SlotView.axaml.cs create mode 100644 src/Artemis.UI/Screens/ProfileEditor/ConfigurationPanels/Slot/SlotViewModel.cs create mode 100644 src/Artemis.UI/Screens/ProfileEditor/ConfigureProfileView.axaml create mode 100644 src/Artemis.UI/Screens/ProfileEditor/ConfigureProfileView.axaml.cs create mode 100644 src/Artemis.UI/Screens/ProfileEditor/ConfigureProfileViewModel.cs create mode 100644 src/Artemis.UI/Screens/ProfileEditor/DesignProfileView.axaml create mode 100644 src/Artemis.UI/Screens/ProfileEditor/DesignProfileView.axaml.cs create mode 100644 src/Artemis.UI/Screens/ProfileEditor/DesignProfileViewModel.cs 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";