diff --git a/src/Artemis.Core/Models/ProfileConfiguration/ProfileConfiguration.cs b/src/Artemis.Core/Models/ProfileConfiguration/ProfileConfiguration.cs index 7ddc0f5f5..a278443cd 100644 --- a/src/Artemis.Core/Models/ProfileConfiguration/ProfileConfiguration.cs +++ b/src/Artemis.Core/Models/ProfileConfiguration/ProfileConfiguration.cs @@ -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("Activate profile", "Whether or not the profile should be active", this); } diff --git a/src/Artemis.Core/Models/ProfileConfiguration/ProfileConfigurationExportModel.cs b/src/Artemis.Core/Models/ProfileConfiguration/ProfileConfigurationExportModel.cs index c899cdc01..20ea8f088 100644 --- a/src/Artemis.Core/Models/ProfileConfiguration/ProfileConfigurationExportModel.cs +++ b/src/Artemis.Core/Models/ProfileConfiguration/ProfileConfigurationExportModel.cs @@ -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 /// /// A model that can be used to serialize a profile configuration, it's profile and it's icon /// - public class ProfileConfigurationExportModel + public class ProfileConfigurationExportModel : IDisposable { /// /// Gets or sets the storage entity of the profile configuration @@ -26,5 +27,11 @@ namespace Artemis.Core /// [JsonConverter(typeof(StreamConverter))] public Stream? ProfileImage { get; set; } + + /// + public void Dispose() + { + ProfileImage?.Dispose(); + } } } \ No newline at end of file diff --git a/src/Artemis.Core/Models/ProfileConfiguration/ProfileConfigurationIcon.cs b/src/Artemis.Core/Models/ProfileConfiguration/ProfileConfigurationIcon.cs index b188852bf..ff205f93d 100644 --- a/src/Artemis.Core/Models/ProfileConfiguration/ProfileConfigurationIcon.cs +++ b/src/Artemis.Core/Models/ProfileConfiguration/ProfileConfigurationIcon.cs @@ -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 } /// - /// Gets or sets the type of icon this profile configuration uses + /// Gets the type of icon this profile configuration uses /// public ProfileConfigurationIconType IconType { get => _iconType; - set => SetAndNotify(ref _iconType, value); + private set => SetAndNotify(ref _iconType, value); } /// - /// Gets or sets the icon if it is a Material icon + /// Gets the name of the icon if is /// - public string? MaterialIcon + public string? IconName { - get => _materialIcon; - set => SetAndNotify(ref _materialIcon, value); + get => _iconName; + private set => SetAndNotify(ref _iconName, value); } /// - /// Gets or sets a stream containing the icon if it is bitmap or SVG + /// Gets the original file name of the icon (if applicable) /// - /// - public Stream? FileIcon + public string? OriginalFileName { - get => _fileIcon; - set => SetAndNotify(ref _fileIcon, value); + get => _originalFileName; + private set => SetAndNotify(ref _originalFileName, value); + } + + /// + /// Updates the to the provided value and changes the is + /// + /// + /// The name of the icon + public void SetIconByName(string iconName) + { + IconName = iconName ?? throw new ArgumentNullException(nameof(iconName)); + OriginalFileName = null; + IconType = ProfileConfigurationIconType.MaterialIcon; + + _iconStream?.Dispose(); + } + + /// + /// Updates the stream returned by to the provided stream + /// + /// The original file name backing the stream, should include the extension + /// The stream to copy + 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; + } + + /// + /// Creates a copy of the stream containing the icon + /// + /// A stream containing the icon + 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; } /// public void Save() { _entity.IconType = (int) IconType; - _entity.MaterialIcon = MaterialIcon; + _entity.MaterialIcon = IconType == ProfileConfigurationIconType.MaterialIcon ? IconName : null; } #endregion diff --git a/src/Artemis.Core/Services/Storage/ProfileService.cs b/src/Artemis.Core/Services/Storage/ProfileService.cs index 13674ddfd..455037aa5 100644 --- a/src/Artemis.Core/Services/Storage/ProfileService.cs +++ b/src/Artemis.Core/Services/Storage/ProfileService.cs @@ -304,10 +304,13 @@ namespace Artemis.Core.Services { if (profileConfiguration.Icon.IconType == ProfileConfigurationIconType.MaterialIcon) return; - if (profileConfiguration.Icon.FileIcon != null) - return; + + // 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"; - profileConfiguration.Icon.FileIcon = _profileCategoryRepository.GetProfileIconStream(profileConfiguration.Entity.FileIconId); + 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); diff --git a/src/Artemis.Storage/Entities/Profile/ProfileConfigurationEntity.cs b/src/Artemis.Storage/Entities/Profile/ProfileConfigurationEntity.cs index 8c98eb726..6c9fc79c2 100644 --- a/src/Artemis.Storage/Entities/Profile/ProfileConfigurationEntity.cs +++ b/src/Artemis.Storage/Entities/Profile/ProfileConfigurationEntity.cs @@ -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; } diff --git a/src/Artemis.Storage/Repositories/ProfileCategoryRepository.cs b/src/Artemis.Storage/Repositories/ProfileCategoryRepository.cs index 81aa22772..8bf25498c 100644 --- a/src/Artemis.Storage/Repositories/ProfileCategoryRepository.cs +++ b/src/Artemis.Storage/Repositories/ProfileCategoryRepository.cs @@ -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); } } } \ No newline at end of file diff --git a/src/Artemis.UI.Avalonia.Shared/Artemis.UI.Avalonia.Shared.xml b/src/Artemis.UI.Avalonia.Shared/Artemis.UI.Avalonia.Shared.xml index 94a337097..44de98edf 100644 --- a/src/Artemis.UI.Avalonia.Shared/Artemis.UI.Avalonia.Shared.xml +++ b/src/Artemis.UI.Avalonia.Shared/Artemis.UI.Avalonia.Shared.xml @@ -166,6 +166,17 @@ + + + Converts an enum into a boolean. + + + + + + + + Converts into . diff --git a/src/Artemis.UI.Shared/Controls/ProfileConfigurationIcon.xaml b/src/Artemis.UI.Shared/Controls/ProfileConfigurationIcon.xaml index 3611f5bef..1572fc3d9 100644 --- a/src/Artemis.UI.Shared/Controls/ProfileConfigurationIcon.xaml +++ b/src/Artemis.UI.Shared/Controls/ProfileConfigurationIcon.xaml @@ -22,7 +22,7 @@ - diff --git a/src/Artemis.UI/Screens/Sidebar/Dialogs/ProfileEdit/ProfileEditView.xaml b/src/Artemis.UI/Screens/Sidebar/Dialogs/ProfileEdit/ProfileEditView.xaml index d56908000..c36ea8a74 100644 --- a/src/Artemis.UI/Screens/Sidebar/Dialogs/ProfileEdit/ProfileEditView.xaml +++ b/src/Artemis.UI/Screens/Sidebar/Dialogs/ProfileEdit/ProfileEditView.xaml @@ -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"> diff --git a/src/Artemis.UI/Screens/Sidebar/Dialogs/ProfileEdit/ProfileEditViewModel.cs b/src/Artemis.UI/Screens/Sidebar/Dialogs/ProfileEdit/ProfileEditViewModel.cs index f8926d273..306c37767 100644 --- a/src/Artemis.UI/Screens/Sidebar/Dialogs/ProfileEdit/ProfileEditViewModel.cs +++ b/src/Artemis.UI/Screens/Sidebar/Dialogs/ProfileEdit/ProfileEditViewModel.cs @@ -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, @@ -48,7 +49,7 @@ namespace Artemis.UI.Screens.Sidebar.Dialogs.ProfileEdit pluginManagementService.GetFeaturesOfType().Where(m => !m.IsAlwaysAvailable).Select(m => new ProfileModuleViewModel(m)) ); Initializing = true; - + ModuleActivationRequirementsViewModel = new ModuleActivationRequirementsViewModel(sidebarVmFactory); ModuleActivationRequirementsViewModel.ConductWith(this); ModuleActivationRequirementsViewModel.SetModule(ProfileConfiguration.Module); @@ -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,11 +72,11 @@ 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; }); } - + public ModuleActivationRequirementsViewModel ModuleActivationRequirementsViewModel { get; } public ProfileConfigurationHotkeyViewModel EnableHotkeyViewModel { get; } public ProfileConfigurationHotkeyViewModel DisableHotkeyViewModel { get; } @@ -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 diff --git a/src/Avalonia/Artemis.UI.Shared/Controls/ProfileConfigurationIcon.axaml.cs b/src/Avalonia/Artemis.UI.Shared/Controls/ProfileConfigurationIcon.axaml.cs index 21cd7173e..6cbd39a0a 100644 --- a/src/Avalonia/Artemis.UI.Shared/Controls/ProfileConfigurationIcon.axaml.cs +++ b/src/Avalonia/Artemis.UI.Shared/Controls/ProfileConfigurationIcon.axaml.cs @@ -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) diff --git a/src/Avalonia/Artemis.UI.Shared/Converters/EnumToBooleanConverter.cs b/src/Avalonia/Artemis.UI.Shared/Converters/EnumToBooleanConverter.cs new file mode 100644 index 000000000..3c7b00239 --- /dev/null +++ b/src/Avalonia/Artemis.UI.Shared/Converters/EnumToBooleanConverter.cs @@ -0,0 +1,25 @@ +using System; +using System.Globalization; +using Avalonia.Data; +using Avalonia.Data.Converters; + +namespace Artemis.UI.Shared.Converters +{ + /// + /// Converts an enum into a boolean. + /// + public class EnumToBooleanConverter : IValueConverter + { + /// + public object Convert(object? value, Type targetType, object parameter, CultureInfo culture) + { + return Equals(value, parameter); + } + + /// + public object ConvertBack(object? value, Type targetType, object parameter, CultureInfo culture) + { + return value?.Equals(true) == true ? parameter : BindingOperations.DoNothing; + } + } +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI.Shared/Services/Interfaces/IWindowService.cs b/src/Avalonia/Artemis.UI.Shared/Services/Interfaces/IWindowService.cs index a844a7c9c..9dcf4ce8a 100644 --- a/src/Avalonia/Artemis.UI.Shared/Services/Interfaces/IWindowService.cs +++ b/src/Avalonia/Artemis.UI.Shared/Services/Interfaces/IWindowService.cs @@ -41,7 +41,7 @@ namespace Artemis.UI.Shared.Services.Interfaces /// The view model type /// The return type /// A task containing the return value of type - Task ShowDialogAsync(params (string name, object value)[] parameters) where TViewModel : DialogViewModelBase; + Task ShowDialogAsync(params (string name, object? value)[] parameters) where TViewModel : DialogViewModelBase; /// /// Shows a content dialog asking the user to confirm an action diff --git a/src/Avalonia/Artemis.UI.Shared/Services/WindowService/WindowService.cs b/src/Avalonia/Artemis.UI.Shared/Services/WindowService/WindowService.cs index 7d6df0d16..691eca3cf 100644 --- a/src/Avalonia/Artemis.UI.Shared/Services/WindowService/WindowService.cs +++ b/src/Avalonia/Artemis.UI.Shared/Services/WindowService/WindowService.cs @@ -56,7 +56,7 @@ namespace Artemis.UI.Shared.Services window.Show(); } - public async Task ShowDialogAsync(params (string name, object value)[] parameters) where TViewModel : DialogViewModelBase + public async Task ShowDialogAsync(params (string name, object? value)[] parameters) where TViewModel : DialogViewModelBase { IParameter[] paramsArray = parameters.Select(kv => new ConstructorArgument(kv.name, kv.value)).Cast().ToArray(); TViewModel viewModel = _kernel.Get(paramsArray)!; diff --git a/src/Avalonia/Artemis.UI.Shared/Styles/Artemis.axaml b/src/Avalonia/Artemis.UI.Shared/Styles/Artemis.axaml index 008cf7654..806683bee 100644 --- a/src/Avalonia/Artemis.UI.Shared/Styles/Artemis.axaml +++ b/src/Avalonia/Artemis.UI.Shared/Styles/Artemis.axaml @@ -1,5 +1,17 @@  + + + + + + + + + + + + diff --git a/src/Avalonia/Artemis.UI.Shared/Styles/Border.axaml b/src/Avalonia/Artemis.UI.Shared/Styles/Border.axaml index bc9ce6bdb..3cf4ad494 100644 --- a/src/Avalonia/Artemis.UI.Shared/Styles/Border.axaml +++ b/src/Avalonia/Artemis.UI.Shared/Styles/Border.axaml @@ -15,6 +15,10 @@ + + 8 + + + + + + Add a new profile + + + + General + + + + Profile name + + Module + + + + + + + + + + + + + + + + + Optional and binds the profile to the selected module, making module data available + No available modules were found + + + Icon type + + + + + + + Icon + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Keybindings + You may set up hotkeys to activate/deactivate the profile + + TODO + + + Activation conditions + If you only want this profile to be active under certain conditions, configure those conditions below + + TODO + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/Root/Sidebar/Dialogs/ProfileConfigurationEditView.axaml.cs b/src/Avalonia/Artemis.UI/Screens/Root/Sidebar/Dialogs/ProfileConfigurationEditView.axaml.cs new file mode 100644 index 000000000..6c822bb91 --- /dev/null +++ b/src/Avalonia/Artemis.UI/Screens/Root/Sidebar/Dialogs/ProfileConfigurationEditView.axaml.cs @@ -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 + { + public ProfileConfigurationEditView() + { + InitializeComponent(); +#if DEBUG + this.AttachDevTools(); +#endif + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + } +} diff --git a/src/Avalonia/Artemis.UI/Screens/Root/Sidebar/Dialogs/ProfileConfigurationEditViewModel.cs b/src/Avalonia/Artemis.UI/Screens/Root/Sidebar/Dialogs/ProfileConfigurationEditViewModel.cs new file mode 100644 index 000000000..4e0261214 --- /dev/null +++ b/src/Avalonia/Artemis.UI/Screens/Root/Sidebar/Dialogs/ProfileConfigurationEditViewModel.cs @@ -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 + { + private readonly ProfileCategory _profileCategory; + private readonly IProfileService _profileService; + private readonly IWindowService _windowService; + private ProfileConfigurationIconType _iconType; + private ObservableCollection? _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().First().ToString()); + _profileName = _profileConfiguration.Name; + _iconType = _profileConfiguration.Icon.IconType; + + IsNew = profileConfiguration == null; + DisplayName = IsNew ? "Artemis | Add profile" : "Artemis | Edit profile"; + Modules = new ObservableCollection( + pluginManagementService.GetFeaturesOfType().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 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(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? 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(Enum.GetValues() + .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 + } +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/Root/Sidebar/Dialogs/ProfileIconViewModel.cs b/src/Avalonia/Artemis.UI/Screens/Root/Sidebar/Dialogs/ProfileIconViewModel.cs new file mode 100644 index 000000000..11417df61 --- /dev/null +++ b/src/Avalonia/Artemis.UI/Screens/Root/Sidebar/Dialogs/ProfileIconViewModel.cs @@ -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; } + } +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/Root/Sidebar/Dialogs/ProfileModuleViewModel.cs b/src/Avalonia/Artemis.UI/Screens/Root/Sidebar/Dialogs/ProfileModuleViewModel.cs new file mode 100644 index 000000000..4452babb6 --- /dev/null +++ b/src/Avalonia/Artemis.UI/Screens/Root/Sidebar/Dialogs/ProfileModuleViewModel.cs @@ -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; } + } +} \ No newline at end of file diff --git a/src/Avalonia/Artemis.UI/Screens/Root/Sidebar/SidebarCategoryView.axaml b/src/Avalonia/Artemis.UI/Screens/Root/Sidebar/SidebarCategoryView.axaml index da722c486..7c4ea26be 100644 --- a/src/Avalonia/Artemis.UI/Screens/Root/Sidebar/SidebarCategoryView.axaml +++ b/src/Avalonia/Artemis.UI/Screens/Root/Sidebar/SidebarCategoryView.axaml @@ -29,7 +29,7 @@ - + - + diff --git a/src/Avalonia/Artemis.UI/Screens/Root/Sidebar/SidebarCategoryViewModel.cs b/src/Avalonia/Artemis.UI/Screens/Root/Sidebar/SidebarCategoryViewModel.cs index c8b1ffbbb..3093c95cf 100644 --- a/src/Avalonia/Artemis.UI/Screens/Root/Sidebar/SidebarCategoryViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/Root/Sidebar/SidebarCategoryViewModel.cs @@ -74,15 +74,21 @@ namespace Artemis.UI.Screens.Root.Sidebar { await _windowService.CreateContentDialog() .WithTitle("Edit category") - .WithViewModel(out var vm, ("category", ProfileCategory)) + .WithViewModel(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(("profileCategory", ProfileCategory), ("profileConfiguration", null)); + } + private void CreateProfileViewModels() { ProfileConfigurations.Clear(); diff --git a/src/Avalonia/Artemis.UI/Screens/Root/Sidebar/SidebarView.axaml b/src/Avalonia/Artemis.UI/Screens/Root/Sidebar/SidebarView.axaml index e9cc753c5..e98beae43 100644 --- a/src/Avalonia/Artemis.UI/Screens/Root/Sidebar/SidebarView.axaml +++ b/src/Avalonia/Artemis.UI/Screens/Root/Sidebar/SidebarView.axaml @@ -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"> - - - - - - - - - - - - - - - - + + diff --git a/src/Avalonia/Artemis.UI/Screens/Root/Sidebar/SidebarViewModel.cs b/src/Avalonia/Artemis.UI/Screens/Root/Sidebar/SidebarViewModel.cs index fa14d3652..8cabe8409 100644 --- a/src/Avalonia/Artemis.UI/Screens/Root/Sidebar/SidebarViewModel.cs +++ b/src/Avalonia/Artemis.UI/Screens/Root/Sidebar/SidebarViewModel.cs @@ -90,8 +90,9 @@ namespace Artemis.UI.Screens.Root.Sidebar { await _windowService.CreateContentDialog() .WithTitle("Add new category") - .WithViewModel(out var vm, ("category", null)) + .WithViewModel(out var vm, ("category", null)) .HavingPrimaryButton(b => b.WithText("Confirm").WithCommand(vm.Confirm)) + .WithCloseButtonText("Cancel") .WithDefaultButton(ContentDialogButton.Primary) .ShowAsync(); diff --git a/src/Avalonia/Artemis.UI/Styles/Artemis.axaml b/src/Avalonia/Artemis.UI/Styles/Artemis.axaml index c40b34e96..7c51cbf0f 100644 --- a/src/Avalonia/Artemis.UI/Styles/Artemis.axaml +++ b/src/Avalonia/Artemis.UI/Styles/Artemis.axaml @@ -6,7 +6,6 @@ - Yellow