mirror of
https://github.com/Artemis-RGB/Artemis
synced 2025-12-13 05:48:35 +00:00
Color properties - Redesigned input
This commit is contained in:
parent
2a6ed8cc7f
commit
09e7bb5168
@ -58,7 +58,7 @@ namespace Artemis.UI.Shared.Controls
|
||||
|
||||
private void OnSelectionChanged(object? sender, SelectionChangedEventArgs e)
|
||||
{
|
||||
if (_enumComboBox == null)
|
||||
if (_enumComboBox == null || _enumComboBox.SelectedIndex == -1)
|
||||
return;
|
||||
|
||||
(Enum enumValue, _) = _currentValues[_enumComboBox.SelectedIndex];
|
||||
|
||||
@ -17,10 +17,8 @@ namespace Artemis.UI.Shared.Services.PropertyInput;
|
||||
/// <typeparam name="T">The type of property this input view model supports</typeparam>
|
||||
public abstract class PropertyInputViewModel<T> : PropertyInputViewModel
|
||||
{
|
||||
private T _dragStartValue;
|
||||
private bool _inputDragging;
|
||||
|
||||
[AllowNull] private T _inputValue;
|
||||
private LayerPropertyPreview<T>? _preview;
|
||||
|
||||
private TimeSpan _time;
|
||||
private bool _updating;
|
||||
@ -35,7 +33,6 @@ public abstract class PropertyInputViewModel<T> : PropertyInputViewModel
|
||||
PropertyInputService = propertyInputService;
|
||||
|
||||
_inputValue = default!;
|
||||
_dragStartValue = default!;
|
||||
|
||||
this.WhenActivated(d =>
|
||||
{
|
||||
@ -55,8 +52,6 @@ public abstract class PropertyInputViewModel<T> : PropertyInputViewModel
|
||||
.Subscribe(_ => UpdateDataBinding())
|
||||
.DisposeWith(d);
|
||||
});
|
||||
|
||||
ValidationContext.ValidationStatusChange.Subscribe(s => Console.WriteLine(s));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -80,17 +75,13 @@ public abstract class PropertyInputViewModel<T> : PropertyInputViewModel
|
||||
public IPropertyInputService PropertyInputService { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a boolean indicating whether the input is currently being dragged
|
||||
/// Gets or boolean indicating whether the current input is being previewed, the value won't be applied until
|
||||
/// <para>
|
||||
/// Only applicable when using something like a <see cref="DraggableFloat" />, see
|
||||
/// <see cref="InputDragStarted" /> and <see cref="InputDragEnded" />
|
||||
/// <see cref="StartPreview" /> and <see cref="ApplyPreview" />
|
||||
/// </para>
|
||||
/// </summary>
|
||||
public bool InputDragging
|
||||
{
|
||||
get => _inputDragging;
|
||||
private set => this.RaiseAndSetIfChanged(ref _inputDragging, value);
|
||||
}
|
||||
public bool IsPreviewing => _preview != null;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the input value
|
||||
@ -119,27 +110,37 @@ public abstract class PropertyInputViewModel<T> : PropertyInputViewModel
|
||||
internal override object InternalGuard { get; } = new();
|
||||
|
||||
/// <summary>
|
||||
/// Called by the view input drag has started
|
||||
/// <para>
|
||||
/// To use, add the following to DraggableFloat in your xaml: <c>DragStarted="{s:Action InputDragStarted}"</c>
|
||||
/// </para>
|
||||
/// Starts the preview of the current property, allowing updates without causing real changes to the property.
|
||||
/// </summary>
|
||||
public void InputDragStarted(object sender, EventArgs e)
|
||||
public void StartPreview()
|
||||
{
|
||||
InputDragging = true;
|
||||
_dragStartValue = GetDragStartValue();
|
||||
_preview?.DiscardPreview();
|
||||
_preview = new LayerPropertyPreview<T>(LayerProperty, _time);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called by the view when input drag has ended
|
||||
/// <para>
|
||||
/// To use, add the following to DraggableFloat in your xaml: <c>DragEnded="{s:Action InputDragEnded}"</c>
|
||||
/// </para>
|
||||
/// Applies the current preview to the property.
|
||||
/// </summary>
|
||||
public void InputDragEnded(object sender, EventArgs e)
|
||||
public void ApplyPreview()
|
||||
{
|
||||
InputDragging = false;
|
||||
ProfileEditorService.ExecuteCommand(new UpdateLayerProperty<T>(LayerProperty, _inputValue, _dragStartValue, _time));
|
||||
if (_preview == null)
|
||||
return;
|
||||
|
||||
if (_preview.DiscardPreview() && _preview.PreviewValue != null)
|
||||
ProfileEditorService.ExecuteCommand(new UpdateLayerProperty<T>(LayerProperty, _inputValue, _preview.PreviewValue, _time));
|
||||
_preview = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Discard the preview of the property.
|
||||
/// </summary>
|
||||
public void DiscardPreview()
|
||||
{
|
||||
if (_preview == null)
|
||||
return;
|
||||
|
||||
_preview.DiscardPreview();
|
||||
_preview = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
@ -157,37 +158,31 @@ public abstract class PropertyInputViewModel<T> : PropertyInputViewModel
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Called when dragging starts to get the initial value before dragging begun
|
||||
/// </summary>
|
||||
/// <returns>The initial value before dragging begun</returns>
|
||||
protected virtual T? GetDragStartValue()
|
||||
{
|
||||
return InputValue;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Applies the input value to the layer property using an <see cref="IProfileEditorCommand" />.
|
||||
/// Applies the input value to the layer property or the currently active preview.
|
||||
/// </summary>
|
||||
protected virtual void ApplyInputValue()
|
||||
{
|
||||
if (_updating)
|
||||
// Avoid reapplying the latest value by checking if we're currently updating
|
||||
if (_updating || !ValidationContext.IsValid)
|
||||
return;
|
||||
|
||||
if (InputDragging)
|
||||
ProfileEditorService.ChangeTime(_time);
|
||||
else if (ValidationContext.IsValid)
|
||||
if (_preview != null)
|
||||
_preview.Preview(_inputValue);
|
||||
else
|
||||
ProfileEditorService.ExecuteCommand(new UpdateLayerProperty<T>(LayerProperty, _inputValue, _time));
|
||||
}
|
||||
|
||||
private void UpdateInputValue()
|
||||
{
|
||||
// Always run this on the UI thread to avoid race conditions with ApplyInputValue
|
||||
Dispatcher.UIThread.Post(() =>
|
||||
{
|
||||
try
|
||||
{
|
||||
_updating = true;
|
||||
|
||||
// Avoid unnecessary UI updates and validator cycles
|
||||
if (_inputValue != null && _inputValue.Equals(LayerProperty.CurrentValue) || _inputValue == null && LayerProperty.CurrentValue == null)
|
||||
if (Equals(_inputValue, LayerProperty.CurrentValue))
|
||||
return;
|
||||
|
||||
// Override the input value
|
||||
@ -202,7 +197,6 @@ public abstract class PropertyInputViewModel<T> : PropertyInputViewModel
|
||||
_updating = false;
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
private void UpdateDataBinding()
|
||||
|
||||
@ -5,25 +5,56 @@
|
||||
xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
|
||||
xmlns:converters="clr-namespace:Artemis.UI.Converters"
|
||||
xmlns:propertyInput="clr-namespace:Artemis.UI.DefaultTypes.PropertyInput"
|
||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||
mc:Ignorable="d" d:DesignWidth="200" d:DesignHeight="450"
|
||||
x:Class="Artemis.UI.DefaultTypes.PropertyInput.SKColorPropertyInputView"
|
||||
x:DataType="propertyInput:SKColorPropertyInputViewModel">
|
||||
<UserControl.Styles>
|
||||
<Style Selector="controls|ColorPickerButton.contained-color-picker-button">
|
||||
<Setter Property="Padding" Value="0" />
|
||||
<Setter Property="Margin" Value="4 0" />
|
||||
<Setter Property="Background" Value="Transparent" />
|
||||
<Setter Property="MinHeight" Value="00" />
|
||||
<Setter Property="MinWidth" Value="0" />
|
||||
<Setter Property="VerticalAlignment" Value="Center" />
|
||||
<Setter Property="HorizontalAlignment" Value="Right" />
|
||||
<Setter Property="Cursor" Value="Hand" />
|
||||
<Setter Property="Template">
|
||||
<Setter.Value>
|
||||
<ControlTemplate>
|
||||
<controls:Button Name="MainButton"
|
||||
Padding="0"
|
||||
BorderThickness="0"
|
||||
CornerRadius="{TemplateBinding CornerRadius}">
|
||||
<Border BorderBrush="{DynamicResource ColorPickerButtonOutline}"
|
||||
BorderThickness="1"
|
||||
HorizontalAlignment="Stretch" VerticalAlignment="Stretch"
|
||||
MinWidth="18" MinHeight="18"
|
||||
Background="{TemplateBinding Color, Converter={StaticResource ColorBrushConv}}"
|
||||
CornerRadius="{TemplateBinding CornerRadius}">
|
||||
<controls:ColorPicker Name="ColorPicker" IsVisible="False" />
|
||||
</Border>
|
||||
</controls:Button>
|
||||
</ControlTemplate>
|
||||
</Setter.Value>
|
||||
</Setter>
|
||||
</Style>
|
||||
|
||||
<Style Selector="controls|ColorPickerButton.contained-color-picker-button /template/ Viewbox">
|
||||
<Setter Property="IsVisible" Value="False"></Setter>
|
||||
</Style>
|
||||
</UserControl.Styles>
|
||||
<UserControl.Resources>
|
||||
<converters:SKColorToStringConverter x:Key="SKColorToStringConverter" />
|
||||
<converters:SKColorToColor2Converter x:Key="SKColorToColor2Converter" />
|
||||
</UserControl.Resources>
|
||||
<Grid Height="24" ColumnDefinitions="*,Auto">
|
||||
<Grid Height="24" ColumnDefinitions="*">
|
||||
<TextBox Classes="condensed"
|
||||
Grid.Row="0"
|
||||
Grid.Column="0"
|
||||
Text="{CompiledBinding InputValue, Converter={StaticResource SKColorToStringConverter}}" />
|
||||
<controls:ColorPickerButton Grid.Row="0"
|
||||
Grid.Column="1"
|
||||
Text="{CompiledBinding InputValue, Converter={StaticResource SKColorToStringConverter}}"
|
||||
Padding="2 2 30 2">
|
||||
</TextBox>
|
||||
<controls:ColorPickerButton Classes="contained-color-picker-button"
|
||||
Color="{CompiledBinding InputValue, Converter={StaticResource SKColorToColor2Converter}}"
|
||||
Classes="condensed"
|
||||
VerticalAlignment="Center"
|
||||
HorizontalAlignment="Right"
|
||||
Margin="-6 0 -8 0" />
|
||||
ShowAcceptDismissButtons="False" />
|
||||
</Grid>
|
||||
|
||||
</UserControl>
|
||||
@ -1,5 +1,6 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Input;
|
||||
using Avalonia.Markup.Xaml;
|
||||
using Avalonia.ReactiveUI;
|
||||
|
||||
@ -17,4 +18,4 @@ namespace Artemis.UI.DefaultTypes.PropertyInput
|
||||
AvaloniaXamlLoader.Load(this);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -3,8 +3,10 @@
|
||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||
xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
|
||||
xmlns:menuBar="clr-namespace:Artemis.UI.Screens.ProfileEditor.MenuBar"
|
||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||
x:Class="Artemis.UI.Screens.ProfileEditor.MenuBar.MenuBarView">
|
||||
x:Class="Artemis.UI.Screens.ProfileEditor.MenuBar.MenuBarView"
|
||||
x:DataType="menuBar:MenuBarViewModel">
|
||||
<Menu VerticalAlignment="Top">
|
||||
<MenuItem Header="_File">
|
||||
<MenuItem Header="New">
|
||||
@ -38,11 +40,11 @@
|
||||
<avalonia:MaterialIcon Kind="Magic" />
|
||||
</MenuItem.Icon>
|
||||
</MenuItem>
|
||||
<MenuItem Header="Suspend Profile">
|
||||
<MenuItem Header="Suspend Profile" IsSelected="{CompiledBinding IsSuspended}">
|
||||
<MenuItem.Icon>
|
||||
<CheckBox BorderThickness="0"
|
||||
IsHitTestVisible="False"
|
||||
IsChecked="{Binding ProfileConfiguration.IsSuspended}" />
|
||||
IsChecked="{CompiledBinding IsSuspended}"/>
|
||||
</MenuItem.Icon>
|
||||
</MenuItem>
|
||||
<Separator />
|
||||
|
||||
@ -1,5 +1,8 @@
|
||||
using System;
|
||||
using System.Reactive.Disposables;
|
||||
using System.Reactive.Linq;
|
||||
using Artemis.Core;
|
||||
using Artemis.Core.Services;
|
||||
using Artemis.UI.Shared;
|
||||
using Artemis.UI.Shared.Services.ProfileEditor;
|
||||
using ReactiveUI;
|
||||
@ -8,11 +11,24 @@ namespace Artemis.UI.Screens.ProfileEditor.MenuBar;
|
||||
|
||||
public class MenuBarViewModel : ActivatableViewModelBase
|
||||
{
|
||||
private readonly IProfileService _profileService;
|
||||
private ProfileEditorHistory? _history;
|
||||
private ObservableAsPropertyHelper<ProfileConfiguration?>? _profileConfiguration;
|
||||
private ObservableAsPropertyHelper<bool>? _isSuspended;
|
||||
|
||||
public MenuBarViewModel(IProfileEditorService profileEditorService)
|
||||
public MenuBarViewModel(IProfileEditorService profileEditorService, IProfileService profileService)
|
||||
{
|
||||
this.WhenActivated(d => profileEditorService.History.Subscribe(history => History = history).DisposeWith(d));
|
||||
_profileService = profileService;
|
||||
this.WhenActivated(d =>
|
||||
{
|
||||
profileEditorService.History.Subscribe(history => History = history).DisposeWith(d);
|
||||
_profileConfiguration = profileEditorService.ProfileConfiguration.ToProperty(this, vm => vm.ProfileConfiguration);
|
||||
_isSuspended = profileEditorService.ProfileConfiguration
|
||||
.Select(p => p?.WhenAnyValue(c => c.IsSuspended) ?? Observable.Never<bool>())
|
||||
.Switch()
|
||||
.ToProperty(this, vm => vm.IsSuspended)
|
||||
.DisposeWith(d);
|
||||
});
|
||||
}
|
||||
|
||||
public ProfileEditorHistory? History
|
||||
@ -20,4 +36,19 @@ public class MenuBarViewModel : ActivatableViewModelBase
|
||||
get => _history;
|
||||
set => RaiseAndSetIfChanged(ref _history, value);
|
||||
}
|
||||
|
||||
public ProfileConfiguration? ProfileConfiguration => _profileConfiguration?.Value;
|
||||
|
||||
public bool IsSuspended
|
||||
{
|
||||
get => _isSuspended?.Value ?? false;
|
||||
set
|
||||
{
|
||||
if (ProfileConfiguration == null)
|
||||
return;
|
||||
|
||||
ProfileConfiguration.IsSuspended = value;
|
||||
_profileService.SaveProfileCategory(ProfileConfiguration.Category);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -52,15 +52,17 @@ public class VisualEditorViewModel : ActivatableViewModelBase
|
||||
Tools = tools;
|
||||
|
||||
this.WhenAnyValue(vm => vm.ProfileConfiguration)
|
||||
.Select(p => p?.Profile != null
|
||||
? Observable.FromEventPattern<ProfileElementEventArgs>(x => p.Profile.DescendentAdded += x, x => p.Profile.DescendentAdded -= x)
|
||||
.Select(p => p?.Profile)
|
||||
.Select(p => p != null
|
||||
? Observable.FromEventPattern<ProfileElementEventArgs>(x => p.DescendentAdded += x, x => p.DescendentAdded -= x)
|
||||
: Observable.Never<EventPattern<ProfileElementEventArgs>>())
|
||||
.Switch()
|
||||
.Subscribe(AddElement)
|
||||
.DisposeWith(d);
|
||||
this.WhenAnyValue(vm => vm.ProfileConfiguration)
|
||||
.Select(p => p?.Profile != null
|
||||
? Observable.FromEventPattern<ProfileElementEventArgs>(x => p.Profile.DescendentRemoved += x, x => p.Profile.DescendentRemoved -= x)
|
||||
.Select(p => p?.Profile)
|
||||
.Select(p => p != null
|
||||
? Observable.FromEventPattern<ProfileElementEventArgs>(x => p.DescendentRemoved += x, x => p.DescendentRemoved -= x)
|
||||
: Observable.Never<EventPattern<ProfileElementEventArgs>>())
|
||||
.Switch()
|
||||
.Subscribe(RemoveElement)
|
||||
|
||||
@ -14,7 +14,7 @@
|
||||
ExtendClientAreaToDecorationsHint="True"
|
||||
ExtendClientAreaTitleBarHeightHint="450">
|
||||
<Grid RowDefinitions="250,50,Auto,*" IsHitTestVisible="False">
|
||||
<Image Grid.Column="0" Stretch="Uniform">
|
||||
<Image Grid.Column="0" Stretch="Uniform" Width="200">
|
||||
<Image.Source>
|
||||
<svg:SvgImage Source="/Assets/Images/Logo/bow.svg" />
|
||||
</Image.Source>
|
||||
@ -22,7 +22,6 @@
|
||||
<TextBlock Grid.Row="1"
|
||||
HorizontalAlignment="Center"
|
||||
VerticalAlignment="Bottom"
|
||||
FontSize="16"
|
||||
TextWrapping="Wrap">
|
||||
Artemis is initializing...
|
||||
</TextBlock>
|
||||
|
||||
@ -83,7 +83,7 @@
|
||||
</Button>
|
||||
<ToggleButton Classes="category-button icon-button icon-button-small"
|
||||
Grid.Column="3"
|
||||
ToolTip.Tip="Suspend profile"
|
||||
ToolTip.Tip="Suspend category"
|
||||
Margin="5 0"
|
||||
IsChecked="{Binding IsSuspended}">
|
||||
<avalonia:MaterialIcon Kind="Pause" />
|
||||
|
||||
@ -5,8 +5,10 @@
|
||||
xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
|
||||
xmlns:converters="clr-namespace:Artemis.UI.Converters"
|
||||
xmlns:controls="clr-namespace:Artemis.UI.Shared.Controls;assembly=Artemis.UI.Shared"
|
||||
xmlns:sidebar="clr-namespace:Artemis.UI.Screens.Sidebar"
|
||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||
x:Class="Artemis.UI.Screens.Sidebar.SidebarProfileConfigurationView">
|
||||
x:Class="Artemis.UI.Screens.Sidebar.SidebarProfileConfigurationView"
|
||||
x:DataType="sidebar:SidebarProfileConfigurationViewModel">
|
||||
<UserControl.Resources>
|
||||
<converters:ValuesAdditionConverter x:Key="ValuesAddition" />
|
||||
</UserControl.Resources>
|
||||
@ -65,17 +67,11 @@
|
||||
</MenuItem>
|
||||
</ContextMenu>
|
||||
</UserControl.ContextMenu>
|
||||
<Grid>
|
||||
<Grid.ColumnDefinitions>
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="*" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
<ColumnDefinition Width="Auto" />
|
||||
</Grid.ColumnDefinitions>
|
||||
<Grid ColumnDefinitions="Auto,*,Auto,Auto">
|
||||
<controls:ProfileConfigurationIcon Grid.Column="0"
|
||||
x:Name="ProfileIcon"
|
||||
VerticalAlignment="Center"
|
||||
ConfigurationIcon="{Binding ProfileConfiguration.Icon}"
|
||||
ConfigurationIcon="{CompiledBinding ProfileConfiguration.Icon}"
|
||||
Width="20"
|
||||
Height="20" />
|
||||
|
||||
@ -85,7 +81,7 @@
|
||||
Margin="10 0 0 0"
|
||||
VerticalAlignment="Center"
|
||||
HorizontalAlignment="Left"
|
||||
Text="{Binding ProfileConfiguration.Name}"
|
||||
Text="{CompiledBinding ProfileConfiguration.Name}"
|
||||
TextTrimming="CharacterEllipsis" />
|
||||
|
||||
<Border Grid.Column="0"
|
||||
@ -117,7 +113,7 @@
|
||||
Grid.Column="3"
|
||||
ToolTip.Tip="Suspend profile"
|
||||
Margin="2 0 0 0"
|
||||
IsChecked="{Binding IsSuspended}">
|
||||
IsChecked="{CompiledBinding IsSuspended}">
|
||||
<avalonia:MaterialIcon Kind="Pause" />
|
||||
</ToggleButton>
|
||||
</Grid>
|
||||
|
||||
@ -1,16 +1,20 @@
|
||||
using System.Threading.Tasks;
|
||||
using System.Reactive.Disposables;
|
||||
using System.Reactive.Linq;
|
||||
using System.Threading.Tasks;
|
||||
using Artemis.Core;
|
||||
using Artemis.Core.Services;
|
||||
using Artemis.UI.Shared;
|
||||
using Artemis.UI.Shared.Services.Interfaces;
|
||||
using ReactiveUI;
|
||||
|
||||
namespace Artemis.UI.Screens.Sidebar
|
||||
{
|
||||
public class SidebarProfileConfigurationViewModel : ViewModelBase
|
||||
public class SidebarProfileConfigurationViewModel : ActivatableViewModelBase
|
||||
{
|
||||
private readonly SidebarViewModel _sidebarViewModel;
|
||||
private readonly IProfileService _profileService;
|
||||
private readonly IWindowService _windowService;
|
||||
private ObservableAsPropertyHelper<bool>? _isSuspended;
|
||||
public ProfileConfiguration ProfileConfiguration { get; }
|
||||
|
||||
public SidebarProfileConfigurationViewModel(SidebarViewModel sidebarViewModel, ProfileConfiguration profileConfiguration, IProfileService profileService, IWindowService windowService)
|
||||
@ -18,8 +22,15 @@ namespace Artemis.UI.Screens.Sidebar
|
||||
_sidebarViewModel = sidebarViewModel;
|
||||
_profileService = profileService;
|
||||
_windowService = windowService;
|
||||
|
||||
ProfileConfiguration = profileConfiguration;
|
||||
|
||||
this.WhenActivated(d =>
|
||||
{
|
||||
_isSuspended = ProfileConfiguration.WhenAnyValue(c => c.IsSuspended)
|
||||
.ToProperty(this, vm => vm.IsSuspended)
|
||||
.DisposeWith(d);
|
||||
});
|
||||
_profileService.LoadProfileConfigurationIcon(ProfileConfiguration);
|
||||
}
|
||||
|
||||
@ -27,7 +38,7 @@ namespace Artemis.UI.Screens.Sidebar
|
||||
|
||||
public bool IsSuspended
|
||||
{
|
||||
get => ProfileConfiguration.IsSuspended;
|
||||
get => _isSuspended?.Value ?? false;
|
||||
set
|
||||
{
|
||||
ProfileConfiguration.IsSuspended = value;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user