diff --git a/src/Artemis.Core/Models/ProfileConfiguration/ProfileConfigurationIcon.cs b/src/Artemis.Core/Models/ProfileConfiguration/ProfileConfigurationIcon.cs index 058e95687..ed9646e7a 100644 --- a/src/Artemis.Core/Models/ProfileConfiguration/ProfileConfigurationIcon.cs +++ b/src/Artemis.Core/Models/ProfileConfiguration/ProfileConfigurationIcon.cs @@ -40,15 +40,6 @@ public class ProfileConfigurationIcon : CorePropertyChanged, IStorageModel private set => SetAndNotify(ref _iconName, value); } - /// - /// Gets the original file name of the icon (if applicable) - /// - public string? OriginalFileName - { - get => _originalFileName; - private set => SetAndNotify(ref _originalFileName, value); - } - /// /// Gets or sets a boolean indicating whether or not this icon should be filled. /// @@ -69,7 +60,6 @@ public class ProfileConfigurationIcon : CorePropertyChanged, IStorageModel _iconStream?.Dispose(); IconName = iconName; - OriginalFileName = null; IconType = ProfileConfigurationIconType.MaterialIcon; OnIconUpdated(); @@ -78,21 +68,19 @@ public class ProfileConfigurationIcon : CorePropertyChanged, IStorageModel /// /// 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) + public void SetIconByStream(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); + if (stream.CanSeek) + stream.Seek(0, SeekOrigin.Begin); stream.CopyTo(_iconStream); _iconStream.Seek(0, SeekOrigin.Begin); IconName = null; - OriginalFileName = originalFileName; IconType = ProfileConfigurationIconType.BitmapImage; OnIconUpdated(); } diff --git a/src/Artemis.Core/Services/ModuleService.cs b/src/Artemis.Core/Services/ModuleService.cs index e3d6cbaef..1050484c0 100644 --- a/src/Artemis.Core/Services/ModuleService.cs +++ b/src/Artemis.Core/Services/ModuleService.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Threading.Tasks; using System.Timers; using Artemis.Core.Modules; using Newtonsoft.Json; @@ -31,8 +32,8 @@ internal class ModuleService : IModuleService pluginManagementService.PluginFeatureEnabled += PluginManagementServiceOnPluginFeatureEnabled; pluginManagementService.PluginFeatureDisabled += PluginManagementServiceOnPluginFeatureDisabled; _modules = pluginManagementService.GetFeaturesOfType().ToList(); - foreach (Module module in _modules) - ImportDefaultProfiles(module); + + Task.Run(ImportDefaultProfiles); } protected virtual void OnModuleActivated(ModuleEventArgs e) @@ -96,7 +97,7 @@ internal class ModuleService : IModuleService { if (e.PluginFeature is Module module && !_modules.Contains(module)) { - ImportDefaultProfiles(module); + Task.Run(() => ImportDefaultProfiles(module)); _modules.Add(module); } } @@ -111,24 +112,21 @@ internal class ModuleService : IModuleService } } - private void ImportDefaultProfiles(Module module) + private async Task ImportDefaultProfiles() + { + foreach (Module module in _modules) + await ImportDefaultProfiles(module); + } + + private async Task ImportDefaultProfiles(Module module) { try { - List profileConfigurations = _profileService.ProfileCategories.SelectMany(c => c.ProfileConfigurations).ToList(); foreach ((DefaultCategoryName categoryName, string profilePath) in module.DefaultProfilePaths) { - ProfileConfigurationExportModel? profileConfigurationExportModel = - JsonConvert.DeserializeObject(File.ReadAllText(profilePath), IProfileService.ExportSettings); - if (profileConfigurationExportModel?.ProfileEntity == null) - throw new ArtemisCoreException($"Default profile at path {profilePath} contains no valid profile data"); - if (profileConfigurations.Any(p => p.Entity.ProfileId == profileConfigurationExportModel.ProfileEntity.Id)) - continue; - - ProfileCategory category = _profileService.ProfileCategories.FirstOrDefault(c => c.Name == categoryName.ToString()) ?? - _profileService.CreateProfileCategory(categoryName.ToString()); - - _profileService.ImportProfile(category, profileConfigurationExportModel, false, true, null); + ProfileCategory category = _profileService.ProfileCategories.FirstOrDefault(c => c.Name == categoryName.ToString()) ?? _profileService.CreateProfileCategory(categoryName.ToString()); + await using FileStream fileStream = File.OpenRead(profilePath); + await _profileService.ImportProfile(fileStream, category, false, true, null); } } catch (Exception e) diff --git a/src/Artemis.Core/Services/Storage/Interfaces/IProfileService.cs b/src/Artemis.Core/Services/Storage/Interfaces/IProfileService.cs index 98e0bc322..099138b92 100644 --- a/src/Artemis.Core/Services/Storage/Interfaces/IProfileService.cs +++ b/src/Artemis.Core/Services/Storage/Interfaces/IProfileService.cs @@ -1,5 +1,7 @@ using System; using System.Collections.ObjectModel; +using System.IO; +using System.Threading.Tasks; using Newtonsoft.Json; using SkiaSharp; @@ -110,17 +112,17 @@ public interface IProfileService : IArtemisService void SaveProfile(Profile profile, bool includeChildren); /// - /// Exports the profile described in the given into an export model. + /// Exports the profile described in the given into a zip archive. /// /// The profile configuration of the profile to export. - /// The resulting export model. - ProfileConfigurationExportModel ExportProfile(ProfileConfiguration profileConfiguration); + /// The resulting zip archive. + Task ExportProfile(ProfileConfiguration profileConfiguration); /// /// Imports the provided base64 encoded GZIPed JSON as a profile configuration. /// + /// The zip archive containing the profile to import. /// The in which to import the profile. - /// The model containing the profile to import. /// Whether or not to give the profile a new GUID, making it unique. /// /// Whether or not to mark the profile as a fresh import, causing it to be adapted until @@ -128,8 +130,7 @@ public interface IProfileService : IArtemisService /// /// Text to add after the name of the profile (separated by a dash). /// The resulting profile configuration. - ProfileConfiguration ImportProfile(ProfileCategory category, ProfileConfigurationExportModel exportModel, bool makeUnique = true, bool markAsFreshImport = true, - string? nameAffix = "imported"); + Task ImportProfile(Stream archiveStream, ProfileCategory category, bool makeUnique, bool markAsFreshImport, string? nameAffix = "imported"); /// /// Adapts a given profile to the currently active devices. diff --git a/src/Artemis.Core/Services/Storage/ProfileService.cs b/src/Artemis.Core/Services/Storage/ProfileService.cs index ba1f496e7..b01e6bc56 100644 --- a/src/Artemis.Core/Services/Storage/ProfileService.cs +++ b/src/Artemis.Core/Services/Storage/ProfileService.cs @@ -2,7 +2,11 @@ using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.IO; +using System.IO.Compression; using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; using Artemis.Core.Modules; using Artemis.Storage.Entities.Profile; using Artemis.Storage.Repositories.Interfaces; @@ -311,7 +315,7 @@ internal class ProfileService : IProfileService using Stream? stream = _profileCategoryRepository.GetProfileIconStream(profileConfiguration.Entity.FileIconId); if (stream != null) - profileConfiguration.Icon.SetIconByStream(profileConfiguration.Entity.IconOriginalFileName, stream); + profileConfiguration.Icon.SetIconByStream(stream); } public void SaveProfileConfigurationIcon(ProfileConfiguration profileConfiguration) @@ -320,9 +324,8 @@ internal class ProfileService : IProfileService return; using Stream? stream = profileConfiguration.Icon.GetIconStream(); - if (stream != null && profileConfiguration.Icon.OriginalFileName != null) + if (stream != null) { - profileConfiguration.Entity.IconOriginalFileName = profileConfiguration.Icon.OriginalFileName; _profileCategoryRepository.SaveProfileIconStream(profileConfiguration.Entity, stream); } } @@ -378,7 +381,7 @@ internal class ProfileService : IProfileService OnProfileDeactivated(new ProfileConfigurationEventArgs(profileConfiguration)); } - public void RequestDeactivation(ProfileConfiguration profileConfiguration) + private void RequestDeactivation(ProfileConfiguration profileConfiguration) { if (profileConfiguration.IsBeingEdited) throw new ArtemisCoreException("Cannot disable a profile that is being edited, that's rude"); @@ -480,34 +483,83 @@ internal class ProfileService : IProfileService _profileRepository.Save(profile.ProfileEntity); } - public ProfileConfigurationExportModel ExportProfile(ProfileConfiguration profileConfiguration) + /// + public async Task ExportProfile(ProfileConfiguration profileConfiguration) { - // The profile may not be active and in that case lets activate it real quick - Profile profile = profileConfiguration.Profile ?? ActivateProfile(profileConfiguration); + ProfileEntity? profileEntity = _profileRepository.Get(profileConfiguration.Entity.ProfileId); + if (profileEntity == null) + throw new ArtemisCoreException("Could not locate profile entity"); - return new ProfileConfigurationExportModel + string configurationJson = JsonConvert.SerializeObject(profileConfiguration.Entity, IProfileService.ExportSettings); + string profileJson = JsonConvert.SerializeObject(profileEntity, IProfileService.ExportSettings); + + MemoryStream archiveStream = new(); + + // Create a ZIP archive + using (ZipArchive archive = new(archiveStream, ZipArchiveMode.Create, true)) { - ProfileConfigurationEntity = profileConfiguration.Entity, - ProfileEntity = profile.ProfileEntity, - ProfileImage = profileConfiguration.Icon.GetIconStream() - }; + ZipArchiveEntry configurationEntry = archive.CreateEntry("configuration.json"); + await using (Stream entryStream = configurationEntry.Open()) + { + await entryStream.WriteAsync(Encoding.Default.GetBytes(configurationJson)); + } + + ZipArchiveEntry profileEntry = archive.CreateEntry("profile.json"); + await using (Stream entryStream = profileEntry.Open()) + { + await entryStream.WriteAsync(Encoding.Default.GetBytes(profileJson)); + } + + await using Stream? iconStream = profileConfiguration.Icon.GetIconStream(); + if (iconStream != null) + { + ZipArchiveEntry iconEntry = archive.CreateEntry("icon.png"); + await using Stream entryStream = iconEntry.Open(); + await iconStream.CopyToAsync(entryStream); + } + } + + archiveStream.Seek(0, SeekOrigin.Begin); + return archiveStream; } - public ProfileConfiguration ImportProfile(ProfileCategory category, ProfileConfigurationExportModel exportModel, - bool makeUnique, bool markAsFreshImport, string? nameAffix) + /// + public async Task ImportProfile(Stream archiveStream, ProfileCategory category, bool makeUnique, bool markAsFreshImport, string? nameAffix) { - if (exportModel.ProfileEntity == null) - throw new ArtemisCoreException("Cannot import a profile without any data"); + using ZipArchive archive = new(archiveStream, ZipArchiveMode.Read, true); - // Create a copy of the entity because we'll be using it from now on - ProfileEntity profileEntity = JsonConvert.DeserializeObject( - JsonConvert.SerializeObject(exportModel.ProfileEntity, IProfileService.ExportSettings), - IProfileService.ExportSettings - )!; + // There should be a configuration.json and profile.json + ZipArchiveEntry? configurationEntry = archive.Entries.FirstOrDefault(e => e.Name.EndsWith("configuration.json")); + ZipArchiveEntry? profileEntry = archive.Entries.FirstOrDefault(e => e.Name.EndsWith("profile.json")); + ZipArchiveEntry? iconEntry = archive.Entries.FirstOrDefault(e => e.Name.EndsWith("icon.png")); + + if (configurationEntry == null) + throw new ArtemisCoreException("Could not import profile, configuration.json missing"); + if (profileEntry == null) + throw new ArtemisCoreException("Could not import profile, profile.json missing"); + + await using Stream configurationStream = configurationEntry.Open(); + using StreamReader configurationReader = new(configurationStream); + ProfileConfigurationEntity? configurationEntity = JsonConvert.DeserializeObject(await configurationReader.ReadToEndAsync(), IProfileService.ExportSettings); + if (configurationEntity == null) + throw new ArtemisCoreException("Could not import profile, failed to deserialize configuration.json"); + + await using Stream profileStream = profileEntry.Open(); + using StreamReader profileReader = new(profileStream); + ProfileEntity? profileEntity = JsonConvert.DeserializeObject(await profileReader.ReadToEndAsync(), IProfileService.ExportSettings); + if (profileEntity == null) + throw new ArtemisCoreException("Could not import profile, failed to deserialize profile.json"); // Assign a new GUID to make sure it is unique in case of a previous import of the same content if (makeUnique) profileEntity.UpdateGuid(Guid.NewGuid()); + else + { + // If the profile already exists and this one is not to be made unique, return the existing profile + ProfileConfiguration? existing = ProfileCategories.SelectMany(c => c.ProfileConfigurations).FirstOrDefault(p => p.ProfileId == profileEntity.Id); + if (existing != null) + return existing; + } if (nameAffix != null) profileEntity.Name = $"{profileEntity.Name} - {nameAffix}"; @@ -519,25 +571,19 @@ internal class ProfileService : IProfileService else throw new ArtemisCoreException($"Cannot import this profile without {nameof(makeUnique)} being true"); - ProfileConfiguration profileConfiguration; - if (exportModel.ProfileConfigurationEntity != null) - { - ProfileConfigurationEntity profileConfigurationEntity = JsonConvert.DeserializeObject( - JsonConvert.SerializeObject(exportModel.ProfileConfigurationEntity, IProfileService.ExportSettings), IProfileService.ExportSettings - )!; - // A new GUID will be given on save - profileConfigurationEntity.FileIconId = Guid.Empty; - profileConfiguration = new ProfileConfiguration(category, profileConfigurationEntity); - if (nameAffix != null) - profileConfiguration.Name = $"{profileConfiguration.Name} - {nameAffix}"; - } - else - { - profileConfiguration = new ProfileConfiguration(category, profileEntity.Name, "Import"); - } + // A new GUID will be given on save + configurationEntity.FileIconId = Guid.Empty; + ProfileConfiguration profileConfiguration = new(category, configurationEntity); + if (nameAffix != null) + profileConfiguration.Name = $"{profileConfiguration.Name} - {nameAffix}"; - if (exportModel.ProfileImage != null && exportModel.ProfileConfigurationEntity?.IconOriginalFileName != null) - profileConfiguration.Icon.SetIconByStream(exportModel.ProfileConfigurationEntity.IconOriginalFileName, exportModel.ProfileImage); + // If an icon was provided, import that as well + if (iconEntry != null) + { + await using Stream iconStream = iconEntry.Open(); + profileConfiguration.Icon.SetIconByStream(iconStream); + SaveProfileConfigurationIcon(profileConfiguration); + } 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 e91ddcc96..276b6ec8d 100644 --- a/src/Artemis.Storage/Entities/Profile/ProfileConfigurationEntity.cs +++ b/src/Artemis.Storage/Entities/Profile/ProfileConfigurationEntity.cs @@ -7,7 +7,6 @@ public class ProfileConfigurationEntity { 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 bool IconFill { get; set; } diff --git a/src/Artemis.Storage/Repositories/ProfileCategoryRepository.cs b/src/Artemis.Storage/Repositories/ProfileCategoryRepository.cs index 573b50a19..71517929f 100644 --- a/src/Artemis.Storage/Repositories/ProfileCategoryRepository.cs +++ b/src/Artemis.Storage/Repositories/ProfileCategoryRepository.cs @@ -70,6 +70,6 @@ internal class ProfileCategoryRepository : IProfileCategoryRepository if (stream == null && _profileIcons.Exists(profileConfigurationEntity.FileIconId)) _profileIcons.Delete(profileConfigurationEntity.FileIconId); - _profileIcons.Upload(profileConfigurationEntity.FileIconId, profileConfigurationEntity.IconOriginalFileName, stream); + _profileIcons.Upload(profileConfigurationEntity.FileIconId, profileConfigurationEntity.FileIconId + ".png", stream); } } \ No newline at end of file diff --git a/src/Artemis.UI/Screens/ProfileEditor/Panels/MenuBar/MenuBarViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/Panels/MenuBar/MenuBarViewModel.cs index 7578952da..98b248c5d 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/Panels/MenuBar/MenuBarViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/Panels/MenuBar/MenuBarViewModel.cs @@ -70,7 +70,7 @@ public class MenuBarViewModel : ActivatableViewModelBase ToggleSuspended = ReactiveCommand.Create(ExecuteToggleSuspended, this.WhenAnyValue(vm => vm.ProfileConfiguration).Select(c => c != null)); DeleteProfile = ReactiveCommand.CreateFromTask(ExecuteDeleteProfile, this.WhenAnyValue(vm => vm.ProfileConfiguration).Select(c => c != null)); ExportProfile = ReactiveCommand.CreateFromTask(ExecuteExportProfile, this.WhenAnyValue(vm => vm.ProfileConfiguration).Select(c => c != null)); - DuplicateProfile = ReactiveCommand.Create(ExecuteDuplicateProfile, this.WhenAnyValue(vm => vm.ProfileConfiguration).Select(c => c != null)); + DuplicateProfile = ReactiveCommand.CreateFromTask(ExecuteDuplicateProfile, this.WhenAnyValue(vm => vm.ProfileConfiguration).Select(c => c != null)); ToggleSuspendedEditing = ReactiveCommand.Create(ExecuteToggleSuspendedEditing); OpenUri = ReactiveCommand.Create(s => Process.Start(new ProcessStartInfo(s) {UseShellExecute = true, Verb = "open"})); ToggleBooleanSetting = ReactiveCommand.Create>(ExecuteToggleBooleanSetting); @@ -195,32 +195,31 @@ public class MenuBarViewModel : ActivatableViewModelBase // Might not cover everything but then the dialog will complain and that's good enough string fileName = Path.GetInvalidFileNameChars().Aggregate(ProfileConfiguration.Name, (current, c) => current.Replace(c, '-')); string? result = await _windowService.CreateSaveFileDialog() - .HavingFilter(f => f.WithExtension("json").WithName("Artemis profile")) + .HavingFilter(f => f.WithExtension("zip").WithName("Artemis profile")) .WithInitialFileName(fileName) .ShowAsync(); if (result == null) return; - ProfileConfigurationExportModel export = _profileService.ExportProfile(ProfileConfiguration); - string json = JsonConvert.SerializeObject(export, IProfileService.ExportSettings); try { - await File.WriteAllTextAsync(result, json); + await using Stream stream = await _profileService.ExportProfile(ProfileConfiguration); + await using FileStream fileStream = File.OpenWrite(result); + await stream.CopyToAsync(fileStream); } catch (Exception e) { _windowService.ShowExceptionDialog("Failed to export profile", e); } } - - private void ExecuteDuplicateProfile() + + private async Task ExecuteDuplicateProfile() { if (ProfileConfiguration == null) return; - - ProfileConfigurationExportModel export = _profileService.ExportProfile(ProfileConfiguration); - _profileService.ImportProfile(ProfileConfiguration.Category, export, true, false, "copy"); + await using Stream export = await _profileService.ExportProfile(ProfileConfiguration); + await _profileService.ImportProfile(export, ProfileConfiguration.Category, true, false, "copy"); } private void ExecuteToggleSuspendedEditing() diff --git a/src/Artemis.UI/Screens/Sidebar/Dialogs/ProfileConfigurationEditViewModel.cs b/src/Artemis.UI/Screens/Sidebar/Dialogs/ProfileConfigurationEditViewModel.cs index c0d130ce6..0e5e5e8d4 100644 --- a/src/Artemis.UI/Screens/Sidebar/Dialogs/ProfileConfigurationEditViewModel.cs +++ b/src/Artemis.UI/Screens/Sidebar/Dialogs/ProfileConfigurationEditViewModel.cs @@ -231,7 +231,7 @@ public class ProfileConfigurationEditViewModel : DialogViewModelBase(x => profileCategory.ProfileConfigurationRemoved += x, x => profileCategory.ProfileConfigurationRemoved -= x) .Subscribe(e => profileConfigurations.RemoveMany(profileConfigurations.Items.Where(c => c == e.EventArgs.ProfileConfiguration))) .DisposeWith(d); - + + profileConfigurations.Edit(updater => + { + updater.Clear(); + updater.AddRange(profileCategory.ProfileConfigurations); + }); + _isCollapsed = ProfileCategory.WhenAnyValue(vm => vm.IsCollapsed).ToProperty(this, vm => vm.IsCollapsed).DisposeWith(d); _isSuspended = ProfileCategory.WhenAnyValue(vm => vm.IsSuspended).ToProperty(this, vm => vm.IsSuspended).DisposeWith(d); }); @@ -155,32 +163,32 @@ public class SidebarCategoryViewModel : ActivatableViewModelBase private async Task ExecuteImportProfile() { string[]? result = await _windowService.CreateOpenFileDialog() - .HavingFilter(f => f.WithExtension("json").WithName("Artemis profile")) + .HavingFilter(f => f.WithExtension("zip").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); - } + // Removing this at some point in the future + if (result[0].EndsWith("json")) + { + ProfileConfigurationExportModel? exportModel = JsonConvert.DeserializeObject(await File.ReadAllTextAsync(result[0]), IProfileService.ExportSettings); + if (exportModel == null) + { + await _windowService.ShowConfirmContentDialog("Import profile", "Failed to import this profile, make sure it is a valid Artemis profile.", "Confirm", null); + return; + } - if (profileConfigurationExportModel == null) - { - await _windowService.ShowConfirmContentDialog("Import profile", "Failed to import this profile, make sure it is a valid Artemis profile.", "Confirm", null); - return; - } - - try - { - _profileService.ImportProfile(ProfileCategory, profileConfigurationExportModel); + await using Stream convertedFileStream = await ConvertLegacyExport(exportModel); + await _profileService.ImportProfile(convertedFileStream, ProfileCategory, true, true); + } + else + { + await using FileStream fileStream = File.OpenRead(result[0]); + await _profileService.ImportProfile(fileStream, ProfileCategory, true, true); + } } catch (Exception e) { @@ -234,4 +242,38 @@ public class SidebarCategoryViewModel : ActivatableViewModelBase _profileService.SaveProfileCategory(categories[i]); } } + + private async Task ConvertLegacyExport(ProfileConfigurationExportModel exportModel) + { + MemoryStream archiveStream = new(); + + string configurationJson = JsonConvert.SerializeObject(exportModel.ProfileConfigurationEntity, IProfileService.ExportSettings); + string profileJson = JsonConvert.SerializeObject(exportModel.ProfileEntity, IProfileService.ExportSettings); + + // Create a ZIP archive + using (ZipArchive archive = new(archiveStream, ZipArchiveMode.Create, true)) + { + ZipArchiveEntry configurationEntry = archive.CreateEntry("configuration.json"); + await using (Stream entryStream = configurationEntry.Open()) + { + await entryStream.WriteAsync(Encoding.Default.GetBytes(configurationJson)); + } + + ZipArchiveEntry profileEntry = archive.CreateEntry("profile.json"); + await using (Stream entryStream = profileEntry.Open()) + { + await entryStream.WriteAsync(Encoding.Default.GetBytes(profileJson)); + } + + if (exportModel.ProfileImage != null) + { + ZipArchiveEntry iconEntry = archive.CreateEntry("icon.png"); + await using Stream entryStream = iconEntry.Open(); + await exportModel.ProfileImage.CopyToAsync(entryStream); + } + } + + archiveStream.Seek(0, SeekOrigin.Begin); + return archiveStream; + } } \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Sidebar/SidebarProfileConfigurationViewModel.cs b/src/Artemis.UI/Screens/Sidebar/SidebarProfileConfigurationViewModel.cs index 4fa5728a5..2d7a7712e 100644 --- a/src/Artemis.UI/Screens/Sidebar/SidebarProfileConfigurationViewModel.cs +++ b/src/Artemis.UI/Screens/Sidebar/SidebarProfileConfigurationViewModel.cs @@ -10,8 +10,6 @@ using Artemis.Core.Services; using Artemis.UI.Shared; using Artemis.UI.Shared.Routing; using Artemis.UI.Shared.Services; -using Artemis.UI.Shared.Services.ProfileEditor; -using Newtonsoft.Json; using ReactiveUI; namespace Artemis.UI.Screens.Sidebar; @@ -36,7 +34,7 @@ public class SidebarProfileConfigurationViewModel : ActivatableViewModelBase SuspendAll = ReactiveCommand.Create(ExecuteSuspendAll); DeleteProfile = ReactiveCommand.CreateFromTask(ExecuteDeleteProfile); ExportProfile = ReactiveCommand.CreateFromTask(ExecuteExportProfile); - DuplicateProfile = ReactiveCommand.Create(ExecuteDuplicateProfile); + DuplicateProfile = ReactiveCommand.CreateFromTask(ExecuteDuplicateProfile); this.WhenActivated(d => _isDisabled = ProfileConfiguration.WhenAnyValue(c => c.Profile) .Select(p => p == null) @@ -116,18 +114,18 @@ public class SidebarProfileConfigurationViewModel : ActivatableViewModelBase // Might not cover everything but then the dialog will complain and that's good enough string fileName = Path.GetInvalidFileNameChars().Aggregate(ProfileConfiguration.Name, (current, c) => current.Replace(c, '-')); string? result = await _windowService.CreateSaveFileDialog() - .HavingFilter(f => f.WithExtension("json").WithName("Artemis profile")) + .HavingFilter(f => f.WithExtension("zip").WithName("Artemis profile")) .WithInitialFileName(fileName) .ShowAsync(); if (result == null) return; - ProfileConfigurationExportModel export = _profileService.ExportProfile(ProfileConfiguration); - string json = JsonConvert.SerializeObject(export, IProfileService.ExportSettings); try { - await File.WriteAllTextAsync(result, json); + await using Stream stream = await _profileService.ExportProfile(ProfileConfiguration); + await using FileStream fileStream = File.OpenWrite(result); + await stream.CopyToAsync(fileStream); } catch (Exception e) { @@ -135,10 +133,10 @@ public class SidebarProfileConfigurationViewModel : ActivatableViewModelBase } } - private void ExecuteDuplicateProfile() + private async Task ExecuteDuplicateProfile() { - ProfileConfigurationExportModel export = _profileService.ExportProfile(ProfileConfiguration); - _profileService.ImportProfile(ProfileConfiguration.Category, export, true, false, "copy"); + await using Stream export = await _profileService.ExportProfile(ProfileConfiguration); + await _profileService.ImportProfile(export, ProfileConfiguration.Category, true, false, "copy"); } public bool Matches(string s)