1
0
mirror of https://github.com/Artemis-RGB/Artemis synced 2025-12-13 05:48:35 +00:00

Core - Rewrote profile configuration icons

WPF UI - Adjusted for the above rewrite 
Avalonia UI - Implemented profile icons
This commit is contained in:
Robert 2021-12-04 12:02:17 +01:00
parent 4c5c785aa6
commit a6f1b05c19
31 changed files with 659 additions and 105 deletions

View File

@ -27,7 +27,8 @@ namespace Artemis.Core
_category = category;
Entity = new ProfileConfigurationEntity();
Icon = new ProfileConfigurationIcon(Entity) {MaterialIcon = icon};
Icon = new ProfileConfigurationIcon(Entity);
Icon.SetIconByName(icon);
ActivationCondition = new NodeScript<bool>("Activate profile", "Whether or not the profile should be active", this);
}

View File

@ -1,4 +1,5 @@
using System.IO;
using System;
using System.IO;
using Artemis.Core.JsonConverters;
using Artemis.Storage.Entities.Profile;
using Newtonsoft.Json;
@ -8,7 +9,7 @@ namespace Artemis.Core
/// <summary>
/// A model that can be used to serialize a profile configuration, it's profile and it's icon
/// </summary>
public class ProfileConfigurationExportModel
public class ProfileConfigurationExportModel : IDisposable
{
/// <summary>
/// Gets or sets the storage entity of the profile configuration
@ -26,5 +27,11 @@ namespace Artemis.Core
/// </summary>
[JsonConverter(typeof(StreamConverter))]
public Stream? ProfileImage { get; set; }
/// <inheritdoc />
public void Dispose()
{
ProfileImage?.Dispose();
}
}
}

View File

@ -1,4 +1,5 @@
using System.ComponentModel;
using System;
using System.ComponentModel;
using System.IO;
using Artemis.Storage.Entities.Profile;
@ -10,9 +11,10 @@ namespace Artemis.Core
public class ProfileConfigurationIcon : CorePropertyChanged, IStorageModel
{
private readonly ProfileConfigurationEntity _entity;
private Stream? _fileIcon;
private string? _iconName;
private Stream? _iconStream;
private ProfileConfigurationIconType _iconType;
private string? _materialIcon;
private string? _originalFileName;
internal ProfileConfigurationIcon(ProfileConfigurationEntity entity)
{
@ -20,31 +22,82 @@ namespace Artemis.Core
}
/// <summary>
/// Gets or sets the type of icon this profile configuration uses
/// Gets the type of icon this profile configuration uses
/// </summary>
public ProfileConfigurationIconType IconType
{
get => _iconType;
set => SetAndNotify(ref _iconType, value);
private set => SetAndNotify(ref _iconType, value);
}
/// <summary>
/// Gets or sets the icon if it is a Material icon
/// Gets the name of the icon if <see cref="IconType" /> is <see cref="ProfileConfigurationIconType.MaterialIcon" />
/// </summary>
public string? MaterialIcon
public string? IconName
{
get => _materialIcon;
set => SetAndNotify(ref _materialIcon, value);
get => _iconName;
private set => SetAndNotify(ref _iconName, value);
}
/// <summary>
/// Gets or sets a stream containing the icon if it is bitmap or SVG
/// Gets the original file name of the icon (if applicable)
/// </summary>
/// <returns></returns>
public Stream? FileIcon
public string? OriginalFileName
{
get => _fileIcon;
set => SetAndNotify(ref _fileIcon, value);
get => _originalFileName;
private set => SetAndNotify(ref _originalFileName, value);
}
/// <summary>
/// Updates the <see cref="IconName" /> to the provided value and changes the <see cref="IconType" /> is
/// <see cref="ProfileConfigurationIconType.MaterialIcon" />
/// </summary>
/// <param name="iconName">The name of the icon</param>
public void SetIconByName(string iconName)
{
IconName = iconName ?? throw new ArgumentNullException(nameof(iconName));
OriginalFileName = null;
IconType = ProfileConfigurationIconType.MaterialIcon;
_iconStream?.Dispose();
}
/// <summary>
/// Updates the stream returned by <see cref="GetIconStream" /> to the provided stream
/// </summary>
/// <param name="originalFileName">The original file name backing the stream, should include the extension</param>
/// <param name="stream">The stream to copy</param>
public void SetIconByStream(string originalFileName, Stream stream)
{
if (originalFileName == null) throw new ArgumentNullException(nameof(originalFileName));
if (stream == null) throw new ArgumentNullException(nameof(stream));
_iconStream?.Dispose();
_iconStream = new MemoryStream();
stream.Seek(0, SeekOrigin.Begin);
stream.CopyTo(_iconStream);
_iconStream.Seek(0, SeekOrigin.Begin);
IconName = null;
OriginalFileName = originalFileName;
IconType = OriginalFileName.EndsWith(".svg") ? ProfileConfigurationIconType.SvgImage : ProfileConfigurationIconType.BitmapImage;
}
/// <summary>
/// Creates a copy of the stream containing the icon
/// </summary>
/// <returns>A stream containing the icon</returns>
public Stream? GetIconStream()
{
if (_iconStream == null)
return null;
MemoryStream stream = new();
_iconStream.CopyTo(stream);
stream.Seek(0, SeekOrigin.Begin);
_iconStream.Seek(0, SeekOrigin.Begin);
return stream;
}
#region Implementation of IStorageModel
@ -53,14 +106,15 @@ namespace Artemis.Core
public void Load()
{
IconType = (ProfileConfigurationIconType) _entity.IconType;
MaterialIcon = _entity.MaterialIcon;
if (IconType == ProfileConfigurationIconType.MaterialIcon)
IconName = _entity.MaterialIcon;
}
/// <inheritdoc />
public void Save()
{
_entity.IconType = (int) IconType;
_entity.MaterialIcon = MaterialIcon;
_entity.MaterialIcon = IconType == ProfileConfigurationIconType.MaterialIcon ? IconName : null;
}
#endregion

View File

@ -304,10 +304,13 @@ namespace Artemis.Core.Services
{
if (profileConfiguration.Icon.IconType == ProfileConfigurationIconType.MaterialIcon)
return;
if (profileConfiguration.Icon.FileIcon != null)
return;
profileConfiguration.Icon.FileIcon = _profileCategoryRepository.GetProfileIconStream(profileConfiguration.Entity.FileIconId);
// This can happen if the icon was saved before the original file name was stored (pre-Avalonia)
profileConfiguration.Entity.IconOriginalFileName ??= profileConfiguration.Icon.IconType == ProfileConfigurationIconType.BitmapImage ? "icon.png" : "icon.svg";
using Stream? stream = _profileCategoryRepository.GetProfileIconStream(profileConfiguration.Entity.FileIconId);
if (stream != null)
profileConfiguration.Icon.SetIconByStream(profileConfiguration.Entity.IconOriginalFileName, stream);
}
public void SaveProfileConfigurationIcon(ProfileConfiguration profileConfiguration)
@ -315,10 +318,11 @@ namespace Artemis.Core.Services
if (profileConfiguration.Icon.IconType == ProfileConfigurationIconType.MaterialIcon)
return;
if (profileConfiguration.Icon.FileIcon != null)
using Stream? stream = profileConfiguration.Icon.GetIconStream();
if (stream != null && profileConfiguration.Icon.OriginalFileName != null)
{
profileConfiguration.Icon.FileIcon.Position = 0;
_profileCategoryRepository.SaveProfileIconStream(profileConfiguration.Entity, profileConfiguration.Icon.FileIcon);
profileConfiguration.Entity.IconOriginalFileName = profileConfiguration.Icon.OriginalFileName;
_profileCategoryRepository.SaveProfileIconStream(profileConfiguration.Entity, stream);
}
}
@ -532,7 +536,7 @@ namespace Artemis.Core.Services
{
ProfileConfigurationEntity = profileConfiguration.Entity,
ProfileEntity = profile.ProfileEntity,
ProfileImage = profileConfiguration.Icon.FileIcon
ProfileImage = profileConfiguration.Icon.GetIconStream()
};
}
@ -579,12 +583,8 @@ namespace Artemis.Core.Services
profileConfiguration = new ProfileConfiguration(category, profileEntity.Name, "Import");
}
if (exportModel.ProfileImage != null)
{
profileConfiguration.Icon.FileIcon = new MemoryStream();
exportModel.ProfileImage.Position = 0;
exportModel.ProfileImage.CopyTo(profileConfiguration.Icon.FileIcon);
}
if (exportModel.ProfileImage != null && exportModel.ProfileConfigurationEntity?.IconOriginalFileName != null)
profileConfiguration.Icon.SetIconByStream(exportModel.ProfileConfigurationEntity.IconOriginalFileName, exportModel.ProfileImage);
profileConfiguration.Entity.ProfileId = profileEntity.Id;
category.AddProfileConfiguration(profileConfiguration, 0);

View File

@ -8,6 +8,7 @@ namespace Artemis.Storage.Entities.Profile
{
public string Name { get; set; }
public string MaterialIcon { get; set; }
public string IconOriginalFileName { get; set; }
public Guid FileIconId { get; set; }
public int IconType { get; set; }
public int Order { get; set; }

View File

@ -70,7 +70,7 @@ namespace Artemis.Storage.Repositories
if (stream == null && _profileIcons.Exists(profileConfigurationEntity.FileIconId))
_profileIcons.Delete(profileConfigurationEntity.FileIconId);
_profileIcons.Upload(profileConfigurationEntity.FileIconId, "image", stream);
_profileIcons.Upload(profileConfigurationEntity.FileIconId, profileConfigurationEntity.IconOriginalFileName, stream);
}
}
}

View File

@ -166,6 +166,17 @@
<member name="M:Artemis.UI.Shared.Converters.ColorToSKColorConverter.ConvertBack(System.Object,System.Type,System.Object,System.Globalization.CultureInfo)">
<inheritdoc />
</member>
<member name="T:Artemis.UI.Shared.Converters.EnumToBooleanConverter">
<summary>
Converts an enum into a boolean.
</summary>
</member>
<member name="M:Artemis.UI.Shared.Converters.EnumToBooleanConverter.Convert(System.Object,System.Type,System.Object,System.Globalization.CultureInfo)">
<inheritdoc />
</member>
<member name="M:Artemis.UI.Shared.Converters.EnumToBooleanConverter.ConvertBack(System.Object,System.Type,System.Object,System.Globalization.CultureInfo)">
<inheritdoc />
</member>
<member name="T:Artemis.UI.Shared.Converters.SKColorToColorConverter">
<summary>
Converts <see cref="T:SkiaSharp.SKColor" /> into <see cref="T:Avalonia.Media.Color" />.

View File

@ -22,7 +22,7 @@
<Setter Property="ContentTemplate">
<Setter.Value>
<DataTemplate>
<materialDesign:PackIcon Kind="{Binding ConfigurationIcon.MaterialIcon, RelativeSource={RelativeSource AncestorType={x:Type UserControl}}}"
<materialDesign:PackIcon Kind="{Binding ConfigurationIcon.IconName, RelativeSource={RelativeSource AncestorType={x:Type UserControl}}}"
Width="Auto"
Height="Auto" />
</DataTemplate>

View File

@ -10,7 +10,7 @@
xmlns:profileEdit="clr-namespace:Artemis.UI.Screens.Sidebar.Dialogs.ProfileEdit"
xmlns:core="clr-namespace:Artemis.Core;assembly=Artemis.Core"
mc:Ignorable="d"
d:DesignHeight="450" d:DesignWidth="280"
d:DesignHeight="450" d:DesignWidth="600"
d:DataContext="{d:DesignInstance {x:Type profileEdit:ProfileEditViewModel}}"
Width="800">
<UserControl.Resources>

View File

@ -28,6 +28,7 @@ namespace Artemis.UI.Screens.Sidebar.Dialogs.ProfileEdit
private ProfileConfigurationIconType _selectedIconType;
private Stream _selectedImage;
private ProfileModuleViewModel _selectedModule;
private string _selectedIconPath;
public ProfileEditViewModel(ProfileConfiguration profileConfiguration, bool isNew,
IProfileService profileService,
@ -60,7 +61,7 @@ namespace Artemis.UI.Screens.Sidebar.Dialogs.ProfileEdit
_profileName = ProfileConfiguration.Name;
_selectedModule = Modules.FirstOrDefault(m => m.Module == ProfileConfiguration.Module);
_selectedIconType = ProfileConfiguration.Icon.IconType;
_selectedImage = ProfileConfiguration.Icon.FileIcon;
_selectedImage = ProfileConfiguration.Icon.GetIconStream();
Task.Run(() =>
{
@ -71,7 +72,7 @@ namespace Artemis.UI.Screens.Sidebar.Dialogs.ProfileEdit
if (IsNew)
SelectedIcon = Icons[new Random().Next(0, Icons.Count - 1)];
else
SelectedIcon = Icons.FirstOrDefault(i => i.Icon.ToString() == ProfileConfiguration.Icon.MaterialIcon);
SelectedIcon = Icons.FirstOrDefault(i => i.Icon.ToString() == ProfileConfiguration.Icon.IconName);
Initializing = false;
});
}
@ -167,18 +168,18 @@ namespace Artemis.UI.Screens.Sidebar.Dialogs.ProfileEdit
return;
ProfileConfiguration.Name = ProfileName;
ProfileConfiguration.Icon.IconType = SelectedIconType;
ProfileConfiguration.Icon.MaterialIcon = SelectedIcon?.Icon.ToString();
ProfileConfiguration.Icon.FileIcon = SelectedImage;
if (SelectedIconType == ProfileConfigurationIconType.MaterialIcon)
ProfileConfiguration.Icon.SetIconByName(SelectedIcon?.Icon.ToString());
else if (_selectedIconPath != null)
{
await using FileStream fileStream = File.OpenRead(_selectedIconPath);
ProfileConfiguration.Icon.SetIconByStream(Path.GetFileName(_selectedIconPath), fileStream);
}
ProfileConfiguration.Module = SelectedModule?.Module;
if (_changedImage)
{
ProfileConfiguration.Icon.FileIcon = SelectedImage;
_profileService.SaveProfileConfigurationIcon(ProfileConfiguration);
}
_profileService.SaveProfileCategory(ProfileConfiguration.Category);
Session.Close(nameof(Accept));
@ -199,6 +200,7 @@ namespace Artemis.UI.Screens.Sidebar.Dialogs.ProfileEdit
// TODO: Scale down to 100x100-ish
SelectedImage = File.OpenRead(dialog.FileName);
_selectedIconPath = dialog.FileName;
}
public void SelectSvgFile()
@ -214,6 +216,7 @@ namespace Artemis.UI.Screens.Sidebar.Dialogs.ProfileEdit
_changedImage = true;
SelectedImage = File.OpenRead(dialog.FileName);
_selectedIconPath = dialog.FileName;
}
#region Overrides of Screen

View File

@ -1,5 +1,6 @@
using System;
using System.ComponentModel;
using System.IO;
using Artemis.Core;
using Avalonia;
using Avalonia.Controls;
@ -48,20 +49,29 @@ namespace Artemis.UI.Shared.Controls
try
{
if (ConfigurationIcon.IconType == ProfileConfigurationIconType.SvgImage && ConfigurationIcon.FileIcon != null)
if (ConfigurationIcon.IconType == ProfileConfigurationIconType.MaterialIcon)
{
SvgSource source = new();
source.Load(ConfigurationIcon.FileIcon);
Content = new SvgImage {Source = source};
}
else if (ConfigurationIcon.IconType == ProfileConfigurationIconType.MaterialIcon && ConfigurationIcon.MaterialIcon != null)
{
Content = Enum.TryParse(ConfigurationIcon.MaterialIcon, true, out MaterialIconKind parsedIcon)
Content = Enum.TryParse(ConfigurationIcon.IconName, true, out MaterialIconKind parsedIcon)
? new MaterialIcon {Kind = parsedIcon!}
: new MaterialIcon {Kind = MaterialIconKind.QuestionMark};
return;
}
else if (ConfigurationIcon.IconType == ProfileConfigurationIconType.BitmapImage && ConfigurationIcon.FileIcon != null)
Content = new Image {Source = new Bitmap(ConfigurationIcon.FileIcon)};
Stream? stream = ConfigurationIcon.GetIconStream();
if (stream == null)
{
Content = new MaterialIcon {Kind = MaterialIconKind.QuestionMark};
return;
}
if (ConfigurationIcon.IconType == ProfileConfigurationIconType.SvgImage)
{
SvgSource source = new();
source.Load(stream);
Content = new Image {Source = new SvgImage {Source = source}};
}
else if (ConfigurationIcon.IconType == ProfileConfigurationIconType.BitmapImage)
Content = new Image {Source = new Bitmap(ConfigurationIcon.GetIconStream())};
else
Content = new MaterialIcon {Kind = MaterialIconKind.QuestionMark};
}
@ -83,10 +93,8 @@ namespace Artemis.UI.Shared.Controls
if (ConfigurationIcon != null)
ConfigurationIcon.PropertyChanged -= IconOnPropertyChanged;
if (Content is SvgImage svgImage)
svgImage.Source?.Dispose();
else if (Content is Image image)
((Bitmap) image.Source).Dispose();
if (Content is Image image && image.Source is IDisposable disposable)
disposable.Dispose();
}
private void OnPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e)

View File

@ -0,0 +1,25 @@
using System;
using System.Globalization;
using Avalonia.Data;
using Avalonia.Data.Converters;
namespace Artemis.UI.Shared.Converters
{
/// <summary>
/// Converts an enum into a boolean.
/// </summary>
public class EnumToBooleanConverter : IValueConverter
{
/// <inheritdoc />
public object Convert(object? value, Type targetType, object parameter, CultureInfo culture)
{
return Equals(value, parameter);
}
/// <inheritdoc />
public object ConvertBack(object? value, Type targetType, object parameter, CultureInfo culture)
{
return value?.Equals(true) == true ? parameter : BindingOperations.DoNothing;
}
}
}

View File

@ -41,7 +41,7 @@ namespace Artemis.UI.Shared.Services.Interfaces
/// <typeparam name="TViewModel">The view model type</typeparam>
/// <typeparam name="TResult">The return type</typeparam>
/// <returns>A task containing the return value of type <typeparamref name="TResult" /></returns>
Task<TResult> ShowDialogAsync<TViewModel, TResult>(params (string name, object value)[] parameters) where TViewModel : DialogViewModelBase<TResult>;
Task<TResult> ShowDialogAsync<TViewModel, TResult>(params (string name, object? value)[] parameters) where TViewModel : DialogViewModelBase<TResult>;
/// <summary>
/// Shows a content dialog asking the user to confirm an action

View File

@ -56,7 +56,7 @@ namespace Artemis.UI.Shared.Services
window.Show();
}
public async Task<TResult> ShowDialogAsync<TViewModel, TResult>(params (string name, object value)[] parameters) where TViewModel : DialogViewModelBase<TResult>
public async Task<TResult> ShowDialogAsync<TViewModel, TResult>(params (string name, object? value)[] parameters) where TViewModel : DialogViewModelBase<TResult>
{
IParameter[] paramsArray = parameters.Select(kv => new ConstructorArgument(kv.name, kv.value)).Cast<IParameter>().ToArray();
TViewModel viewModel = _kernel.Get<TViewModel>(paramsArray)!;

View File

@ -1,5 +1,17 @@
<Styles xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<Styles.Resources>
<VisualBrush x:Key="CheckerboardBrush" TileMode="Tile" Stretch="Uniform" DestinationRect="0,0,15,15">
<VisualBrush.Visual>
<Grid Width="15" Height="15" RowDefinitions="*,*" ColumnDefinitions="*,*">
<Rectangle Grid.Row="0" Grid.Column="0" Fill="Black" Opacity="0.15" />
<Rectangle Grid.Row="0" Grid.Column="1" />
<Rectangle Grid.Row="1" Grid.Column="0" />
<Rectangle Grid.Row="1" Grid.Column="1" Fill="Black" Opacity="0.15" />
</Grid>
</VisualBrush.Visual>
</VisualBrush>
</Styles.Resources>
<StyleInclude Source="/Styles/Border.axaml" />
<StyleInclude Source="/Styles/Button.axaml" />
<StyleInclude Source="/Styles/TextBlock.axaml" />

View File

@ -15,6 +15,10 @@
</StackPanel>
</Design.PreviewWith>
<Styles.Resources>
<CornerRadius x:Key="CardCornerRadius">8</CornerRadius>
</Styles.Resources>
<!-- Add Styles Here -->
<Style Selector="Border.router-container">
<Setter Property="Background" Value="{DynamicResource SolidBackgroundFillColorTertiary}" />
@ -25,13 +29,13 @@
<Style Selector="Border.card">
<Setter Property="Padding" Value="25" />
<Setter Property="Background" Value="{DynamicResource ControlFillColorDefaultBrush}" />
<Setter Property="CornerRadius" Value="10" />
<Setter Property="CornerRadius" Value="{DynamicResource CardCornerRadius}" />
</Style>
<Style Selector="Border.card-condensed">
<Setter Property="Padding" Value="15" />
<Setter Property="Background" Value="{DynamicResource ControlFillColorDefaultBrush}" />
<Setter Property="CornerRadius" Value="10" />
<Setter Property="CornerRadius" Value="{DynamicResource CardCornerRadius}" />
</Style>
<Style Selector="Separator.card-separator">

View File

@ -51,5 +51,11 @@
<Compile Update="Screens\Debugger\Tabs\Settings\DebugSettingsView.axaml.cs">
<DependentUpon>DebugSettingsView.axaml</DependentUpon>
</Compile>
<Compile Update="Screens\Root\Sidebar\ContentDialogs\SidebarCategoryEditView.axaml.cs">
<DependentUpon>SidebarCategoryEditView.axaml</DependentUpon>
</Compile>
<Compile Update="Screens\Root\Sidebar\Dialogs\ProfileConfigurationEditView.axaml.cs">
<DependentUpon>ProfileConfigurationEditView.axaml</DependentUpon>
</Compile>
</ItemGroup>
</Project>

View File

@ -9,20 +9,7 @@
x:Class="Artemis.UI.Screens.Device.DeviceSettingsView">
<Border Classes="card" Padding="0" Width="200" ClipToBounds="True" Margin="5">
<Grid RowDefinitions="140,*,Auto">
<Rectangle Grid.Row="0">
<Rectangle.Fill>
<VisualBrush TileMode="Tile" Stretch="Uniform" DestinationRect="0,0,15,15">
<VisualBrush.Visual>
<Grid Width="15" Height="15" RowDefinitions="*,*" ColumnDefinitions="*,*">
<Rectangle Grid.Row="0" Grid.Column="0" Fill="Black" Opacity="0.15" />
<Rectangle Grid.Row="0" Grid.Column="1" />
<Rectangle Grid.Row="1" Grid.Column="0" />
<Rectangle Grid.Row="1" Grid.Column="1" Fill="Black" Opacity="0.15" />
</Grid>
</VisualBrush.Visual>
</VisualBrush>
</Rectangle.Fill>
</Rectangle>
<Rectangle Grid.Row="0" Fill="{DynamicResource CheckerboardBrush}"/>
<controls1:DeviceVisualizer VerticalAlignment="Center"
HorizontalAlignment="Center"
Margin="5"

View File

@ -3,7 +3,7 @@
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Artemis.UI.Screens.Root.Sidebar.Dialogs.SidebarCategoryCreateView">
x:Class="Artemis.UI.Screens.Root.Sidebar.Dialogs.SidebarCategoryEditView">
<StackPanel>
<StackPanel.KeyBindings>
<KeyBinding Gesture="Enter" Command="{Binding Confirm}" />

View File

@ -5,9 +5,9 @@ using ReactiveUI;
namespace Artemis.UI.Screens.Root.Sidebar.Dialogs
{
public class SidebarCategoryCreateView : ReactiveUserControl<SidebarCategoryCreateViewModel>
public class SidebarCategoryEditView : ReactiveUserControl<SidebarCategoryEditViewModel>
{
public SidebarCategoryCreateView()
public SidebarCategoryEditView()
{
InitializeComponent();
this.WhenActivated(_ => this.ClearAllDataValidationErrors());

View File

@ -8,13 +8,13 @@ using ReactiveUI.Validation.Extensions;
namespace Artemis.UI.Screens.Root.Sidebar.Dialogs
{
public class SidebarCategoryCreateViewModel : ContentDialogViewModelBase
public class SidebarCategoryEditViewModel : ContentDialogViewModelBase
{
private readonly IProfileService _profileService;
private readonly ProfileCategory? _category;
private string? _categoryName;
public SidebarCategoryCreateViewModel(IProfileService profileService, ProfileCategory? category)
public SidebarCategoryEditViewModel(IProfileService profileService, ProfileCategory? category)
{
_profileService = profileService;
_category = category;

View File

@ -0,0 +1,150 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:core="clr-namespace:Artemis.Core;assembly=Artemis.Core"
xmlns:controls="clr-namespace:Artemis.UI.Shared.Controls;assembly=Artemis.UI.Shared"
xmlns:local="clr-namespace:Artemis.UI.Screens.Root.Sidebar.Dialogs"
xmlns:converters="clr-namespace:Artemis.UI.Shared.Converters;assembly=Artemis.UI.Shared"
xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
xmlns:svg="clr-namespace:Avalonia.Svg.Skia;assembly=Avalonia.Svg.Skia"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="850"
x:Class="Artemis.UI.Screens.Root.Sidebar.Dialogs.ProfileConfigurationEditView"
Title="{Binding DisplayName}"
Icon="/Assets/Images/Logo/bow.ico"
Width="800"
Height="850">
<Window.Resources>
<converters:EnumToBooleanConverter x:Key="EnumBoolConverter" />
</Window.Resources>
<Grid Margin="16" RowDefinitions="*,Auto">
<ScrollViewer>
<StackPanel>
<StackPanel.Styles>
<Style Selector="TextBlock.label">
<Setter Property="Margin" Value="0 10 0 5" />
</Style>
</StackPanel.Styles>
<Grid>
<TextBlock Classes="h4" IsVisible="{Binding IsNew}">Add a new profile</TextBlock>
<TextBlock Classes="h4" IsVisible="{Binding !IsNew}" Text="{Binding ProfileConfiguration.Name}" />
</Grid>
<TextBlock Classes="h5">General</TextBlock>
<Border Classes="card" Margin="0 0 0 15">
<StackPanel>
<TextBlock Classes="label">Profile name</TextBlock>
<TextBox Text="{Binding ProfileName}" />
<TextBlock Classes="label">Module</TextBlock>
<ComboBox SelectedItem="{Binding SelectedModule}" IsEnabled="{Binding Modules.Count}" Items="{Binding Modules}" HorizontalAlignment="Stretch">
<ComboBox.ItemsPanel>
<ItemsPanelTemplate>
<VirtualizingStackPanel />
</ItemsPanelTemplate>
</ComboBox.ItemsPanel>
<ComboBox.ItemTemplate>
<DataTemplate DataType="{x:Type local:ProfileModuleViewModel}">
<StackPanel Orientation="Horizontal">
<controls:ArtemisIcon Icon="{Binding Icon}" Width="16" Height="16" Margin="0 0 5 0" />
<TextBlock Text="{Binding Name}" />
</StackPanel>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
<Grid>
<TextBlock Classes="subtitle" IsVisible="{Binding Modules.Count}">Optional and binds the profile to the selected module, making module data available</TextBlock>
<TextBlock Classes="subtitle" IsVisible="{Binding !Modules.Count}">No available modules were found</TextBlock>
</Grid>
<TextBlock Classes="label">Icon type</TextBlock>
<WrapPanel Orientation="Horizontal">
<RadioButton Content="Material Icon"
IsChecked="{Binding IconType, Converter={StaticResource EnumBoolConverter}, ConverterParameter={x:Static core:ProfileConfigurationIconType.MaterialIcon}}" />
<RadioButton Content="Bitmap image"
IsChecked="{Binding IconType, Converter={StaticResource EnumBoolConverter}, ConverterParameter={x:Static core:ProfileConfigurationIconType.BitmapImage}}" />
<RadioButton Content="SVG image"
IsChecked="{Binding IconType, Converter={StaticResource EnumBoolConverter}, ConverterParameter={x:Static core:ProfileConfigurationIconType.SvgImage}}" />
</WrapPanel>
<TextBlock Classes="label">Icon</TextBlock>
<StackPanel Orientation="Horizontal"
IsVisible="{Binding IconType, Converter={StaticResource EnumBoolConverter}, ConverterParameter={x:Static core:ProfileConfigurationIconType.BitmapImage}}">
<Border Background="{DynamicResource CheckerboardBrush}" CornerRadius="{DynamicResource CardCornerRadius}" Width="98" Height="98">
<Image Source="{Binding SelectedBitmapSource}" Margin="10"/>
</Border>
<Button Command="{Binding BrowseBitmapFile}"
VerticalAlignment="Bottom"
Margin="10 0"
IsVisible="{Binding IconType, Converter={StaticResource EnumBoolConverter}, ConverterParameter={x:Static core:ProfileConfigurationIconType.BitmapImage}}">
Browse bitmap file
</Button>
</StackPanel>
<StackPanel Orientation="Horizontal"
IsVisible="{Binding IconType, Converter={StaticResource EnumBoolConverter}, ConverterParameter={x:Static core:ProfileConfigurationIconType.SvgImage}}">
<Border Background="{DynamicResource CheckerboardBrush}" CornerRadius="{DynamicResource CardCornerRadius}" Width="98" Height="98">
<Image Margin="10" Source="{Binding SelectedSvgSource}">
</Image>
</Border>
<Button Command="{Binding BrowseSvgFile}"
VerticalAlignment="Bottom"
Margin="10 0"
IsVisible="{Binding IconType, Converter={StaticResource EnumBoolConverter}, ConverterParameter={x:Static core:ProfileConfigurationIconType.SvgImage}}">
Browse SVG file
</Button>
</StackPanel>
<StackPanel Orientation="Horizontal"
IsVisible="{Binding IconType, Converter={StaticResource EnumBoolConverter}, ConverterParameter={x:Static core:ProfileConfigurationIconType.MaterialIcon}}">
<Border Background="{DynamicResource CheckerboardBrush}" CornerRadius="{DynamicResource CardCornerRadius}" Width="98" Height="98">
<avalonia:MaterialIcon Kind="{Binding SelectedMaterialIcon.Icon}" Width="65" Height="65" />
</Border>
<ComboBox Items="{Binding MaterialIcons}"
SelectedItem="{Binding SelectedMaterialIcon}"
VirtualizationMode="Simple"
VerticalAlignment="Bottom"
IsTextSearchEnabled="True"
Margin="10 0"
Width="250"
IsVisible="{Binding IconType, Converter={StaticResource EnumBoolConverter}, ConverterParameter={x:Static core:ProfileConfigurationIconType.MaterialIcon}}">
<ComboBox.ItemsPanel>
<ItemsPanelTemplate>
<VirtualizingStackPanel />
</ItemsPanelTemplate>
</ComboBox.ItemsPanel>
<ComboBox.ItemTemplate>
<DataTemplate DataType="local:ProfileIconViewModel">
<StackPanel Orientation="Horizontal">
<avalonia:MaterialIcon Kind="{Binding Icon}" Margin="0 0 5 0" />
<TextBlock Text="{Binding DisplayName}" />
</StackPanel>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
</StackPanel>
</StackPanel>
</Border>
<TextBlock Classes="h5">Keybindings</TextBlock>
<TextBlock Classes="subtitle">You may set up hotkeys to activate/deactivate the profile</TextBlock>
<Border Classes="card" Margin="0 5 0 15">
<TextBlock>TODO</TextBlock>
</Border>
<TextBlock Classes="h5">Activation conditions</TextBlock>
<TextBlock Classes="subtitle">If you only want this profile to be active under certain conditions, configure those conditions below</TextBlock>
<Border Classes="card" Margin="0 5 0 15">
<TextBlock>TODO</TextBlock>
</Border>
</StackPanel>
</ScrollViewer>
<Grid Grid.Row="1" ColumnDefinitions="*,Auto,Auto">
<Button Grid.Column="0" Command="{Binding Import}">Import profile</Button>
<Button Grid.Column="1" Margin="10" Classes="accent" Command="{Binding Confirm}">Confirm</Button>
<Button Grid.Column="2" Command="{Binding Cancel}">Cancel</Button>
</Grid>
</Grid>
</Window>

View File

@ -0,0 +1,23 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using Avalonia.ReactiveUI;
namespace Artemis.UI.Screens.Root.Sidebar.Dialogs
{
public partial class ProfileConfigurationEditView : ReactiveWindow<ProfileConfigurationEditViewModel>
{
public ProfileConfigurationEditView()
{
InitializeComponent();
#if DEBUG
this.AttachDevTools();
#endif
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
}
}

View File

@ -0,0 +1,225 @@
using System;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Artemis.Core;
using Artemis.Core.Modules;
using Artemis.Core.Services;
using Artemis.UI.Shared;
using Artemis.UI.Shared.Services.Interfaces;
using Avalonia.Media.Imaging;
using Avalonia.Svg.Skia;
using Avalonia.Threading;
using Castle.Core.Resource;
using Material.Icons;
using Newtonsoft.Json;
using ReactiveUI;
namespace Artemis.UI.Screens.Root.Sidebar.Dialogs
{
public class ProfileConfigurationEditViewModel : DialogViewModelBase<bool>
{
private readonly ProfileCategory _profileCategory;
private readonly IProfileService _profileService;
private readonly IWindowService _windowService;
private ProfileConfigurationIconType _iconType;
private ObservableCollection<ProfileIconViewModel>? _materialIcons;
private ProfileConfiguration _profileConfiguration;
private string _profileName;
private Bitmap? _selectedBitmapSource;
private ProfileIconViewModel? _selectedMaterialIcon;
private ProfileModuleViewModel? _selectedModule;
private string? _selectedIconPath;
private SvgImage? _selectedSvgSource;
public ProfileConfigurationEditViewModel(ProfileCategory profileCategory, ProfileConfiguration? profileConfiguration, IWindowService windowService,
IProfileService profileService, IPluginManagementService pluginManagementService)
{
_profileCategory = profileCategory;
_windowService = windowService;
_profileService = profileService;
_profileConfiguration = profileConfiguration ?? profileService.CreateProfileConfiguration(profileCategory, "New profile", Enum.GetValues<MaterialIconKind>().First().ToString());
_profileName = _profileConfiguration.Name;
_iconType = _profileConfiguration.Icon.IconType;
IsNew = profileConfiguration == null;
DisplayName = IsNew ? "Artemis | Add profile" : "Artemis | Edit profile";
Modules = new ObservableCollection<ProfileModuleViewModel>(
pluginManagementService.GetFeaturesOfType<Module>().Where(m => !m.IsAlwaysAvailable).Select(m => new ProfileModuleViewModel(m))
);
Dispatcher.UIThread.Post(LoadIcon, DispatcherPriority.Background);
}
public bool IsNew { get; }
public ProfileConfiguration ProfileConfiguration
{
get => _profileConfiguration;
set => this.RaiseAndSetIfChanged(ref _profileConfiguration, value);
}
public string ProfileName
{
get => _profileName;
set => this.RaiseAndSetIfChanged(ref _profileName, value);
}
public ObservableCollection<ProfileModuleViewModel> Modules { get; }
public ProfileModuleViewModel? SelectedModule
{
get => _selectedModule;
set => this.RaiseAndSetIfChanged(ref _selectedModule, value);
}
public async Task Import()
{
string[]? result = await _windowService.CreateOpenFileDialog()
.HavingFilter(f => f.WithExtension("json").WithName("Artemis profile"))
.ShowAsync();
if (result == null)
return;
string json = await File.ReadAllTextAsync(result[0]);
ProfileConfigurationExportModel? profileConfigurationExportModel = null;
try
{
profileConfigurationExportModel = JsonConvert.DeserializeObject<ProfileConfigurationExportModel>(json, IProfileService.ExportSettings);
}
catch (JsonException e)
{
_windowService.ShowExceptionDialog("Import profile failed", e);
}
if (profileConfigurationExportModel == null)
{
await _windowService.ShowConfirmContentDialog("Import profile", "Failed to import this profile, make sure it is a valid Artemis profile.", "Confirm", null);
return;
}
_profileService.ImportProfile(_profileCategory, profileConfigurationExportModel);
Close(true);
}
public async Task Confirm()
{
ProfileConfiguration.Name = ProfileName;
ProfileConfiguration.Module = SelectedModule?.Module;
await SaveIcon();
_profileService.SaveProfileConfigurationIcon(ProfileConfiguration);
_profileService.SaveProfileCategory(_profileCategory);
Close(true);
}
public void Cancel()
{
if (IsNew)
_profileService.RemoveProfileConfiguration(_profileConfiguration);
Close(false);
}
#region Icon
public ProfileConfigurationIconType IconType
{
get => _iconType;
set => this.RaiseAndSetIfChanged(ref _iconType, value);
}
public ObservableCollection<ProfileIconViewModel>? MaterialIcons
{
get => _materialIcons;
set => this.RaiseAndSetIfChanged(ref _materialIcons, value);
}
public ProfileIconViewModel? SelectedMaterialIcon
{
get => _selectedMaterialIcon;
set => this.RaiseAndSetIfChanged(ref _selectedMaterialIcon, value);
}
public Bitmap? SelectedBitmapSource
{
get => _selectedBitmapSource;
set => this.RaiseAndSetIfChanged(ref _selectedBitmapSource, value);
}
public SvgImage? SelectedSvgSource
{
get => _selectedSvgSource;
set => this.RaiseAndSetIfChanged(ref _selectedSvgSource, value);
}
private void LoadIcon()
{
// Preselect the icon based on streams if needed
if (_profileConfiguration.Icon.IconType == ProfileConfigurationIconType.BitmapImage)
{
SelectedBitmapSource = new Bitmap(_profileConfiguration.Icon.GetIconStream());
}
else if (_profileConfiguration.Icon.IconType == ProfileConfigurationIconType.SvgImage)
{
SvgSource newSource = new();
newSource.Load(_profileConfiguration.Icon.GetIconStream());
SelectedSvgSource = new SvgImage {Source = newSource};
}
// Prepare the contents of the dropdown box, it should be virtualized so no need to wait with this
MaterialIcons = new ObservableCollection<ProfileIconViewModel>(Enum.GetValues<MaterialIconKind>()
.Select(kind => new ProfileIconViewModel(kind))
.DistinctBy(vm => vm.DisplayName)
.OrderBy(vm => vm.DisplayName));
// Preselect the icon or fall back to a random one
SelectedMaterialIcon = !IsNew && Enum.TryParse(_profileConfiguration.Icon.IconName, out MaterialIconKind enumValue)
? MaterialIcons.FirstOrDefault(m => m.Icon == enumValue)
: MaterialIcons.ElementAt(new Random().Next(0, MaterialIcons.Count - 1));
}
private async Task SaveIcon()
{
if (IconType == ProfileConfigurationIconType.MaterialIcon && SelectedMaterialIcon != null)
ProfileConfiguration.Icon.SetIconByName(SelectedMaterialIcon.Icon.ToString());
else if (_selectedIconPath != null)
{
await using FileStream fileStream = File.OpenRead(_selectedIconPath);
ProfileConfiguration.Icon.SetIconByStream(Path.GetFileName(_selectedIconPath), fileStream);
}
}
public async Task BrowseBitmapFile()
{
string[]? result = await _windowService.CreateOpenFileDialog()
.HavingFilter(f => f.WithExtension("png").WithExtension("jpg").WithExtension("bmp").WithName("Bitmap image"))
.ShowAsync();
if (result == null)
return;
SelectedBitmapSource = new Bitmap(result[0]);
_selectedIconPath = result[0];
}
public async Task BrowseSvgFile()
{
string[]? result = await _windowService.CreateOpenFileDialog()
.HavingFilter(f => f.WithExtension("svg").WithName("SVG image"))
.ShowAsync();
if (result == null)
return;
SvgSource newSource = new();
newSource.Load(result[0]);
SelectedSvgSource = new SvgImage {Source = newSource};
_selectedIconPath = result[0];
}
#endregion
}
}

View File

@ -0,0 +1,16 @@
using Artemis.UI.Shared;
using Material.Icons;
namespace Artemis.UI.Screens.Root.Sidebar.Dialogs
{
public class ProfileIconViewModel : ViewModelBase
{
public ProfileIconViewModel(MaterialIconKind icon)
{
Icon = icon;
DisplayName = icon.ToString();
}
public MaterialIconKind Icon { get; }
}
}

View File

@ -0,0 +1,23 @@
using Artemis.Core.Modules;
using Artemis.UI.Shared;
using Material.Icons;
namespace Artemis.UI.Screens.Root.Sidebar.Dialogs
{
public class ProfileModuleViewModel : ViewModelBase
{
public ProfileModuleViewModel(Module module)
{
Module = module;
Name = module.Info.Name;
Icon = module.Info.ResolvedIcon ?? MaterialIconKind.QuestionMark.ToString();
Description = module.Info.Description;
}
public string Icon { get; }
public string Name { get; }
public string? Description { get; }
public Module Module { get; }
}
}

View File

@ -29,7 +29,7 @@
</Style>
</UserControl.Styles>
<Grid x:Name="ContainerGrid" Margin="0 8 0 0" RowDefinitions="Auto,*">
<Grid Grid.Row="0" Background="Transparent" ColumnDefinitions="Auto,Auto,*,Auto,Auto">
<Grid Grid.Row="0" Background="Transparent" ColumnDefinitions="Auto,Auto,*,Auto,Auto,Auto">
<avalonia:MaterialIcon Classes.chevron-collapsed="{Binding ShowItems}"
Kind="ChevronUp"
@ -84,11 +84,17 @@
<ToggleButton Classes="category-button icon-button icon-button-small"
Grid.Column="3"
ToolTip.Tip="Suspend profile"
Margin="2 0 0 0"
Margin="5 0"
IsChecked="{Binding IsSuspended}">
<avalonia:MaterialIcon Kind="Pause" />
</ToggleButton>
<Button Classes="category-button icon-button icon-button-small"
Grid.Column="4"
ToolTip.Tip="Add profile"
HorizontalAlignment="Right"
Command="{Binding AddProfile}">
<avalonia:MaterialIcon Kind="Plus" />
</Button>
</Grid>
<Border Grid.Row="1">

View File

@ -74,15 +74,21 @@ namespace Artemis.UI.Screens.Root.Sidebar
{
await _windowService.CreateContentDialog()
.WithTitle("Edit category")
.WithViewModel<SidebarCategoryCreateViewModel>(out var vm, ("category", ProfileCategory))
.WithViewModel<SidebarCategoryEditViewModel>(out var vm, ("category", ProfileCategory))
.HavingPrimaryButton(b => b.WithText("Confirm").WithCommand(vm.Confirm))
.HavingSecondaryButton(b => b.WithText("Delete").WithCommand(vm.Delete))
.WithCloseButtonText("Cancel")
.WithDefaultButton(ContentDialogButton.Primary)
.ShowAsync();
_sidebarViewModel.UpdateProfileCategories();
}
public async Task AddProfile()
{
await _windowService.ShowDialogAsync<ProfileConfigurationEditViewModel, bool>(("profileCategory", ProfileCategory), ("profileConfiguration", null));
}
private void CreateProfileViewModels()
{
ProfileConfigurations.Clear();

View File

@ -7,22 +7,8 @@
xmlns:svg="clr-namespace:Avalonia.Svg.Skia;assembly=Avalonia.Svg.Skia"
mc:Ignorable="d" d:DesignWidth="240" d:DesignHeight="450"
x:Class="Artemis.UI.Screens.Root.Sidebar.SidebarView">
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="60" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Grid Grid.Row="0" IsHitTestVisible="False">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" />
<ColumnDefinition Width="*" />
</Grid.ColumnDefinitions>
<Grid RowDefinitions="60,Auto,Auto,*,Auto,Auto">
<Grid Grid.Row="0" IsHitTestVisible="False" ColumnDefinitions="Auto,*">
<Image Grid.Column="0">
<Image.Source>
<svg:SvgImage Source="/Assets/Images/Logo/bow.svg" />

View File

@ -90,8 +90,9 @@ namespace Artemis.UI.Screens.Root.Sidebar
{
await _windowService.CreateContentDialog()
.WithTitle("Add new category")
.WithViewModel<SidebarCategoryCreateViewModel>(out var vm, ("category", null))
.WithViewModel<SidebarCategoryEditViewModel>(out var vm, ("category", null))
.HavingPrimaryButton(b => b.WithText("Confirm").WithCommand(vm.Confirm))
.WithCloseButtonText("Cancel")
.WithDefaultButton(ContentDialogButton.Primary)
.ShowAsync();

View File

@ -6,7 +6,6 @@
<ResourceDictionary.MergedDictionaries>
<ResourceInclude Source="/ArtemisTrayIcon.axaml"/>
</ResourceDictionary.MergedDictionaries>
<SolidColorBrush x:Key="Warning">Yellow</SolidColorBrush>
</ResourceDictionary>
</Styles.Resources>
<!-- Third party styles -->