mirror of
https://github.com/Artemis-RGB/Artemis
synced 2026-11-11 11:26:27 +00:00
Merge branch 'development'
This commit is contained in:
commit
bbadef7a9a
262 files changed
+7395
-1968
No files matched your search
@@ -17,13 +17,12 @@ public sealed class Profile : ProfileElement
|
||||
private readonly ObservableCollection<ScriptConfiguration> _scriptConfigurations;
|
||||
private readonly ObservableCollection<ProfileScript> _scripts;
|
||||
private bool _isFreshImport;
|
||||
private ProfileElement? _lastSelectedProfileElement;
|
||||
|
||||
internal Profile(ProfileConfiguration configuration, ProfileEntity profileEntity) : base(null!)
|
||||
{
|
||||
_scripts = new ObservableCollection<ProfileScript>();
|
||||
_scriptConfigurations = new ObservableCollection<ScriptConfiguration>();
|
||||
|
||||
|
||||
Opacity = 0d;
|
||||
ShouldDisplay = true;
|
||||
Configuration = configuration;
|
||||
@@ -67,15 +66,6 @@ public sealed class Profile : ProfileElement
|
||||
set => SetAndNotify(ref _isFreshImport, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the last selected profile element of this profile
|
||||
/// </summary>
|
||||
public ProfileElement? LastSelectedProfileElement
|
||||
{
|
||||
get => _lastSelectedProfileElement;
|
||||
set => SetAndNotify(ref _lastSelectedProfileElement, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the profile entity this profile uses for persistent storage
|
||||
/// </summary>
|
||||
@@ -105,7 +95,7 @@ public sealed class Profile : ProfileElement
|
||||
profileScript.OnProfileUpdated(deltaTime);
|
||||
|
||||
const double OPACITY_PER_SECOND = 1;
|
||||
|
||||
|
||||
if (ShouldDisplay && Opacity < 1)
|
||||
Opacity = Math.Clamp(Opacity + OPACITY_PER_SECOND * deltaTime, 0d, 1d);
|
||||
if (!ShouldDisplay && Opacity > 0)
|
||||
@@ -123,14 +113,14 @@ public sealed class Profile : ProfileElement
|
||||
|
||||
foreach (ProfileScript profileScript in Scripts)
|
||||
profileScript.OnProfileRendering(canvas, canvas.LocalClipBounds);
|
||||
|
||||
|
||||
SKPaint? opacityPaint = null;
|
||||
bool applyOpacityLayer = Configuration.FadeInAndOut && Opacity < 1;
|
||||
|
||||
|
||||
if (applyOpacityLayer)
|
||||
{
|
||||
opacityPaint = new SKPaint();
|
||||
opacityPaint.Color = new SKColor(0, 0, 0, (byte)(255d * Easings.CubicEaseInOut(Opacity)));
|
||||
opacityPaint.Color = new SKColor(0, 0, 0, (byte) (255d * Easings.CubicEaseInOut(Opacity)));
|
||||
canvas.SaveLayer(opacityPaint);
|
||||
}
|
||||
|
||||
@@ -242,20 +232,13 @@ public sealed class Profile : ProfileElement
|
||||
AddChild(new Folder(this, this, rootFolder));
|
||||
}
|
||||
|
||||
List<RenderProfileElement> renderElements = GetAllRenderElements();
|
||||
|
||||
if (ProfileEntity.LastSelectedProfileElement != Guid.Empty)
|
||||
LastSelectedProfileElement = renderElements.FirstOrDefault(f => f.EntityId == ProfileEntity.LastSelectedProfileElement);
|
||||
else
|
||||
LastSelectedProfileElement = null;
|
||||
|
||||
while (_scriptConfigurations.Any())
|
||||
RemoveScriptConfiguration(_scriptConfigurations[0]);
|
||||
foreach (ScriptConfiguration scriptConfiguration in ProfileEntity.ScriptConfigurations.Select(e => new ScriptConfiguration(e)))
|
||||
AddScriptConfiguration(scriptConfiguration);
|
||||
|
||||
// Load node scripts last since they may rely on the profile structure being in place
|
||||
foreach (RenderProfileElement renderProfileElement in renderElements)
|
||||
foreach (RenderProfileElement renderProfileElement in GetAllRenderElements())
|
||||
renderProfileElement.LoadNodeScript();
|
||||
}
|
||||
|
||||
@@ -312,7 +295,6 @@ public sealed class Profile : ProfileElement
|
||||
ProfileEntity.Id = EntityId;
|
||||
ProfileEntity.Name = Configuration.Name;
|
||||
ProfileEntity.IsFreshImport = IsFreshImport;
|
||||
ProfileEntity.LastSelectedProfileElement = LastSelectedProfileElement?.EntityId ?? Guid.Empty;
|
||||
|
||||
foreach (ProfileElement profileElement in Children)
|
||||
profileElement.Save();
|
||||
|
||||
@@ -98,6 +98,7 @@ public class ProfileCategory : CorePropertyChanged, IStorageModel
|
||||
/// </summary>
|
||||
public void AddProfileConfiguration(ProfileConfiguration configuration, int? targetIndex)
|
||||
{
|
||||
// TODO: Look into this, it doesn't seem to make sense
|
||||
// Removing the original will shift every item in the list forwards, keep that in mind with the target index
|
||||
if (configuration.Category == this && targetIndex != null && targetIndex.Value > _profileConfigurations.IndexOf(configuration))
|
||||
targetIndex -= 1;
|
||||
|
||||
@@ -147,16 +147,7 @@ public class ProfileConfiguration : BreakableModel, IStorageModel, IDisposable
|
||||
get => _activationConditionMet;
|
||||
private set => SetAndNotify(ref _activationConditionMet, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a boolean indicating whether this profile configuration is being edited
|
||||
/// </summary>
|
||||
public bool IsBeingEdited
|
||||
{
|
||||
get => _isBeingEdited;
|
||||
set => SetAndNotify(ref _isBeingEdited, value);
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Gets the profile of this profile configuration
|
||||
/// </summary>
|
||||
@@ -243,8 +234,6 @@ public class ProfileConfiguration : BreakableModel, IStorageModel, IDisposable
|
||||
{
|
||||
if (_disposed)
|
||||
throw new ObjectDisposedException("ProfileConfiguration");
|
||||
if (IsBeingEdited)
|
||||
return true;
|
||||
if (Category.IsSuspended || IsSuspended || IsMissingModule)
|
||||
return false;
|
||||
|
||||
|
||||
@@ -15,7 +15,6 @@ public class ProfileConfigurationIcon : CorePropertyChanged, IStorageModel
|
||||
private string? _iconName;
|
||||
private Stream? _iconStream;
|
||||
private ProfileConfigurationIconType _iconType;
|
||||
private string? _originalFileName;
|
||||
|
||||
internal ProfileConfigurationIcon(ProfileConfigurationEntity entity)
|
||||
{
|
||||
|
||||
@@ -28,19 +28,26 @@ public interface IProfileService : IArtemisService
|
||||
ReadOnlyCollection<ProfileConfiguration> ProfileConfigurations { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a boolean indicating whether hotkeys are enabled.
|
||||
/// Gets or sets the focused profile configuration which is rendered exclusively.
|
||||
/// </summary>
|
||||
bool HotkeysEnabled { get; set; }
|
||||
ProfileConfiguration? FocusProfile { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a boolean indicating whether rendering should only be done for profiles being edited.
|
||||
/// Gets or sets the profile element which is rendered exclusively.
|
||||
/// </summary>
|
||||
bool RenderForEditor { get; set; }
|
||||
ProfileElement? FocusProfileElement { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the profile element to focus on while rendering for the editor.
|
||||
/// Gets or sets a value indicating whether the currently focused profile should receive updates.
|
||||
/// </summary>
|
||||
ProfileElement? EditorFocus { get; set; }
|
||||
bool UpdateFocusProfile { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Creates a copy of the provided profile configuration.
|
||||
/// </summary>
|
||||
/// <param name="profileConfiguration">The profile configuration to clone.</param>
|
||||
/// <returns>The resulting clone.</returns>
|
||||
ProfileConfiguration CloneProfileConfiguration(ProfileConfiguration profileConfiguration);
|
||||
|
||||
/// <summary>
|
||||
/// Activates the profile of the given <see cref="ProfileConfiguration" /> with the currently active surface.
|
||||
@@ -71,8 +78,9 @@ public interface IProfileService : IArtemisService
|
||||
/// Creates a new profile category and saves it to persistent storage.
|
||||
/// </summary>
|
||||
/// <param name="name">The name of the new profile category, must be unique.</param>
|
||||
/// <param name="addToTop">A boolean indicating whether or not to add the category to the top.</param>
|
||||
/// <returns>The newly created profile category.</returns>
|
||||
ProfileCategory CreateProfileCategory(string name);
|
||||
ProfileCategory CreateProfileCategory(string name, bool addToTop = false);
|
||||
|
||||
/// <summary>
|
||||
/// Permanently deletes the provided profile category.
|
||||
@@ -119,7 +127,7 @@ public interface IProfileService : IArtemisService
|
||||
Task<Stream> ExportProfile(ProfileConfiguration profileConfiguration);
|
||||
|
||||
/// <summary>
|
||||
/// Imports the provided base64 encoded GZIPed JSON as a profile configuration.
|
||||
/// Imports the provided ZIP archive stream as a profile configuration.
|
||||
/// </summary>
|
||||
/// <param name="archiveStream">The zip archive containing the profile to import.</param>
|
||||
/// <param name="category">The <see cref="ProfileCategory" /> in which to import the profile.</param>
|
||||
@@ -129,8 +137,17 @@ public interface IProfileService : IArtemisService
|
||||
/// any changes are made to it.
|
||||
/// </param>
|
||||
/// <param name="nameAffix">Text to add after the name of the profile (separated by a dash).</param>
|
||||
/// <param name="targetIndex">The index at which to import the profile into the category.</param>
|
||||
/// <returns>The resulting profile configuration.</returns>
|
||||
Task<ProfileConfiguration> ImportProfile(Stream archiveStream, ProfileCategory category, bool makeUnique, bool markAsFreshImport, string? nameAffix = "imported");
|
||||
Task<ProfileConfiguration> ImportProfile(Stream archiveStream, ProfileCategory category, bool makeUnique, bool markAsFreshImport, string? nameAffix = "imported", int targetIndex = 0);
|
||||
|
||||
/// <summary>
|
||||
/// Imports the provided ZIP archive stream into the provided profile configuration
|
||||
/// </summary>
|
||||
/// <param name="archiveStream">The zip archive containing the profile to import.</param>
|
||||
/// <param name="profileConfiguration">The profile configuration to overwrite.</param>
|
||||
/// <returns>The resulting profile configuration.</returns>
|
||||
Task<ProfileConfiguration> OverwriteProfile(MemoryStream archiveStream, ProfileConfiguration profileConfiguration);
|
||||
|
||||
/// <summary>
|
||||
/// Adapts a given profile to the currently active devices.
|
||||
@@ -168,4 +185,5 @@ public interface IProfileService : IArtemisService
|
||||
/// Occurs whenever a profile category is removed.
|
||||
/// </summary>
|
||||
public event EventHandler<ProfileCategoryEventArgs>? ProfileCategoryRemoved;
|
||||
|
||||
}
|
||||
@@ -19,14 +19,14 @@ namespace Artemis.Core.Services;
|
||||
internal class ProfileService : IProfileService
|
||||
{
|
||||
private readonly ILogger _logger;
|
||||
private readonly IRgbService _rgbService;
|
||||
private readonly IProfileCategoryRepository _profileCategoryRepository;
|
||||
private readonly IPluginManagementService _pluginManagementService;
|
||||
|
||||
private readonly List<ArtemisKeyboardKeyEventArgs> _pendingKeyboardEvents = new();
|
||||
private readonly IPluginManagementService _pluginManagementService;
|
||||
private readonly List<ProfileCategory> _profileCategories;
|
||||
private readonly IProfileCategoryRepository _profileCategoryRepository;
|
||||
private readonly IProfileRepository _profileRepository;
|
||||
private readonly List<Exception> _renderExceptions = new();
|
||||
private readonly IRgbService _rgbService;
|
||||
private readonly List<Exception> _updateExceptions = new();
|
||||
private DateTime _lastRenderExceptionLog;
|
||||
private DateTime _lastUpdateExceptionLog;
|
||||
@@ -49,7 +49,6 @@ internal class ProfileService : IProfileService
|
||||
_pluginManagementService.PluginFeatureEnabled += PluginManagementServiceOnPluginFeatureToggled;
|
||||
_pluginManagementService.PluginFeatureDisabled += PluginManagementServiceOnPluginFeatureToggled;
|
||||
|
||||
HotkeysEnabled = true;
|
||||
inputService.KeyboardKeyUp += InputServiceOnKeyboardKeyUp;
|
||||
|
||||
if (!_profileCategories.Any())
|
||||
@@ -57,11 +56,488 @@ internal class ProfileService : IProfileService
|
||||
UpdateModules();
|
||||
}
|
||||
|
||||
private void InputServiceOnKeyboardKeyUp(object? sender, ArtemisKeyboardKeyEventArgs e)
|
||||
public ProfileConfiguration? FocusProfile { get; set; }
|
||||
public ProfileElement? FocusProfileElement { get; set; }
|
||||
public bool UpdateFocusProfile { get; set; }
|
||||
|
||||
/// <inheritdoc />
|
||||
public void UpdateProfiles(double deltaTime)
|
||||
{
|
||||
if (!HotkeysEnabled)
|
||||
// If there is a focus profile update only that, and only if UpdateFocusProfile is true
|
||||
if (FocusProfile != null)
|
||||
{
|
||||
if (UpdateFocusProfile)
|
||||
FocusProfile.Profile?.Update(deltaTime);
|
||||
return;
|
||||
}
|
||||
|
||||
lock (_profileCategories)
|
||||
{
|
||||
// Iterate the children in reverse because the first category must be rendered last to end up on top
|
||||
for (int i = _profileCategories.Count - 1; i > -1; i--)
|
||||
{
|
||||
ProfileCategory profileCategory = _profileCategories[i];
|
||||
for (int j = profileCategory.ProfileConfigurations.Count - 1; j > -1; j--)
|
||||
{
|
||||
ProfileConfiguration profileConfiguration = profileCategory.ProfileConfigurations[j];
|
||||
|
||||
// Process hotkeys that where pressed since this profile last updated
|
||||
ProcessPendingKeyEvents(profileConfiguration);
|
||||
|
||||
bool shouldBeActive = profileConfiguration.ShouldBeActive(false);
|
||||
if (shouldBeActive)
|
||||
{
|
||||
profileConfiguration.Update();
|
||||
shouldBeActive = profileConfiguration.ActivationConditionMet;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Make sure the profile is active or inactive according to the parameters above
|
||||
if (shouldBeActive && profileConfiguration.Profile == null && profileConfiguration.BrokenState != "Failed to activate profile")
|
||||
profileConfiguration.TryOrBreak(() => ActivateProfile(profileConfiguration), "Failed to activate profile");
|
||||
if (shouldBeActive && profileConfiguration.Profile != null && !profileConfiguration.Profile.ShouldDisplay)
|
||||
profileConfiguration.Profile.ShouldDisplay = true;
|
||||
else if (!shouldBeActive && profileConfiguration.Profile != null)
|
||||
{
|
||||
if (!profileConfiguration.FadeInAndOut)
|
||||
DeactivateProfile(profileConfiguration);
|
||||
else if (!profileConfiguration.Profile.ShouldDisplay && profileConfiguration.Profile.Opacity <= 0)
|
||||
DeactivateProfile(profileConfiguration);
|
||||
else if (profileConfiguration.Profile.Opacity > 0)
|
||||
RequestDeactivation(profileConfiguration);
|
||||
}
|
||||
|
||||
profileConfiguration.Profile?.Update(deltaTime);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_updateExceptions.Add(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LogProfileUpdateExceptions();
|
||||
_pendingKeyboardEvents.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void RenderProfiles(SKCanvas canvas)
|
||||
{
|
||||
// If there is a focus profile, render only that
|
||||
if (FocusProfile != null)
|
||||
{
|
||||
FocusProfile.Profile?.Render(canvas, SKPointI.Empty, FocusProfileElement);
|
||||
return;
|
||||
}
|
||||
|
||||
lock (_profileCategories)
|
||||
{
|
||||
// Iterate the children in reverse because the first category must be rendered last to end up on top
|
||||
for (int i = _profileCategories.Count - 1; i > -1; i--)
|
||||
{
|
||||
ProfileCategory profileCategory = _profileCategories[i];
|
||||
for (int j = profileCategory.ProfileConfigurations.Count - 1; j > -1; j--)
|
||||
{
|
||||
try
|
||||
{
|
||||
ProfileConfiguration profileConfiguration = profileCategory.ProfileConfigurations[j];
|
||||
// Ensure all criteria are met before rendering
|
||||
bool fadingOut = profileConfiguration.Profile?.ShouldDisplay == false && profileConfiguration.Profile?.Opacity > 0;
|
||||
if (!profileConfiguration.IsSuspended && !profileConfiguration.IsMissingModule && (profileConfiguration.ActivationConditionMet || fadingOut))
|
||||
profileConfiguration.Profile?.Render(canvas, SKPointI.Empty, null);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_renderExceptions.Add(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LogProfileRenderExceptions();
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public ReadOnlyCollection<ProfileCategory> ProfileCategories
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_profileRepository)
|
||||
{
|
||||
return _profileCategories.AsReadOnly();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public ReadOnlyCollection<ProfileConfiguration> ProfileConfigurations
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_profileRepository)
|
||||
{
|
||||
return _profileCategories.SelectMany(c => c.ProfileConfigurations).ToList().AsReadOnly();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void LoadProfileConfigurationIcon(ProfileConfiguration profileConfiguration)
|
||||
{
|
||||
if (profileConfiguration.Icon.IconType == ProfileConfigurationIconType.MaterialIcon)
|
||||
return;
|
||||
|
||||
using Stream? stream = _profileCategoryRepository.GetProfileIconStream(profileConfiguration.Entity.FileIconId);
|
||||
if (stream != null)
|
||||
profileConfiguration.Icon.SetIconByStream(stream);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void SaveProfileConfigurationIcon(ProfileConfiguration profileConfiguration)
|
||||
{
|
||||
if (profileConfiguration.Icon.IconType == ProfileConfigurationIconType.MaterialIcon)
|
||||
return;
|
||||
|
||||
using Stream? stream = profileConfiguration.Icon.GetIconStream();
|
||||
if (stream != null)
|
||||
_profileCategoryRepository.SaveProfileIconStream(profileConfiguration.Entity, stream);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public ProfileConfiguration CloneProfileConfiguration(ProfileConfiguration profileConfiguration)
|
||||
{
|
||||
return new ProfileConfiguration(profileConfiguration.Category, profileConfiguration.Entity);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public Profile ActivateProfile(ProfileConfiguration profileConfiguration)
|
||||
{
|
||||
if (profileConfiguration.Profile != null)
|
||||
{
|
||||
profileConfiguration.Profile.ShouldDisplay = true;
|
||||
return profileConfiguration.Profile;
|
||||
}
|
||||
|
||||
ProfileEntity profileEntity;
|
||||
try
|
||||
{
|
||||
profileEntity = _profileRepository.Get(profileConfiguration.Entity.ProfileId);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
profileConfiguration.SetBrokenState("Failed to activate profile", e);
|
||||
throw;
|
||||
}
|
||||
|
||||
if (profileEntity == null)
|
||||
throw new ArtemisCoreException($"Cannot find profile named: {profileConfiguration.Name} ID: {profileConfiguration.Entity.ProfileId}");
|
||||
|
||||
Profile profile = new(profileConfiguration, profileEntity);
|
||||
profile.PopulateLeds(_rgbService.EnabledDevices);
|
||||
|
||||
if (profile.IsFreshImport)
|
||||
{
|
||||
_logger.Debug("Profile is a fresh import, adapting to surface - {profile}", profile);
|
||||
AdaptProfile(profile);
|
||||
}
|
||||
|
||||
profileConfiguration.Profile = profile;
|
||||
|
||||
OnProfileActivated(new ProfileConfigurationEventArgs(profileConfiguration));
|
||||
return profile;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void DeactivateProfile(ProfileConfiguration profileConfiguration)
|
||||
{
|
||||
if (FocusProfile == profileConfiguration)
|
||||
throw new ArtemisCoreException("Cannot disable a profile that is being edited, that's rude");
|
||||
if (profileConfiguration.Profile == null)
|
||||
return;
|
||||
|
||||
Profile profile = profileConfiguration.Profile;
|
||||
profileConfiguration.Profile = null;
|
||||
profile.Dispose();
|
||||
|
||||
OnProfileDeactivated(new ProfileConfigurationEventArgs(profileConfiguration));
|
||||
}
|
||||
|
||||
private void RequestDeactivation(ProfileConfiguration profileConfiguration)
|
||||
{
|
||||
if (FocusProfile == profileConfiguration)
|
||||
throw new ArtemisCoreException("Cannot disable a profile that is being edited, that's rude");
|
||||
if (profileConfiguration.Profile == null)
|
||||
return;
|
||||
|
||||
profileConfiguration.Profile.ShouldDisplay = false;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void DeleteProfile(ProfileConfiguration profileConfiguration)
|
||||
{
|
||||
DeactivateProfile(profileConfiguration);
|
||||
|
||||
ProfileEntity profileEntity = _profileRepository.Get(profileConfiguration.Entity.ProfileId);
|
||||
if (profileEntity == null)
|
||||
return;
|
||||
|
||||
profileConfiguration.Category.RemoveProfileConfiguration(profileConfiguration);
|
||||
_profileRepository.Remove(profileEntity);
|
||||
SaveProfileCategory(profileConfiguration.Category);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public ProfileCategory CreateProfileCategory(string name, bool addToTop = false)
|
||||
{
|
||||
ProfileCategory profileCategory;
|
||||
lock (_profileRepository)
|
||||
{
|
||||
if (addToTop)
|
||||
{
|
||||
profileCategory = new ProfileCategory(name, 1);
|
||||
foreach (ProfileCategory category in _profileCategories)
|
||||
{
|
||||
category.Order++;
|
||||
category.Save();
|
||||
_profileCategoryRepository.Save(category.Entity);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
profileCategory = new ProfileCategory(name, _profileCategories.Count + 1);
|
||||
}
|
||||
|
||||
_profileCategories.Add(profileCategory);
|
||||
SaveProfileCategory(profileCategory);
|
||||
}
|
||||
|
||||
OnProfileCategoryAdded(new ProfileCategoryEventArgs(profileCategory));
|
||||
return profileCategory;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void DeleteProfileCategory(ProfileCategory profileCategory)
|
||||
{
|
||||
List<ProfileConfiguration> profileConfigurations = profileCategory.ProfileConfigurations.ToList();
|
||||
foreach (ProfileConfiguration profileConfiguration in profileConfigurations)
|
||||
RemoveProfileConfiguration(profileConfiguration);
|
||||
|
||||
lock (_profileRepository)
|
||||
{
|
||||
_profileCategories.Remove(profileCategory);
|
||||
_profileCategoryRepository.Remove(profileCategory.Entity);
|
||||
}
|
||||
|
||||
OnProfileCategoryRemoved(new ProfileCategoryEventArgs(profileCategory));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public ProfileConfiguration CreateProfileConfiguration(ProfileCategory category, string name, string icon)
|
||||
{
|
||||
ProfileConfiguration configuration = new(category, name, icon);
|
||||
ProfileEntity entity = new();
|
||||
_profileRepository.Add(entity);
|
||||
|
||||
configuration.Entity.ProfileId = entity.Id;
|
||||
category.AddProfileConfiguration(configuration, 0);
|
||||
return configuration;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void RemoveProfileConfiguration(ProfileConfiguration profileConfiguration)
|
||||
{
|
||||
profileConfiguration.Category.RemoveProfileConfiguration(profileConfiguration);
|
||||
|
||||
DeactivateProfile(profileConfiguration);
|
||||
SaveProfileCategory(profileConfiguration.Category);
|
||||
ProfileEntity profileEntity = _profileRepository.Get(profileConfiguration.Entity.ProfileId);
|
||||
if (profileEntity != null)
|
||||
_profileRepository.Remove(profileEntity);
|
||||
|
||||
profileConfiguration.Dispose();
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void SaveProfileCategory(ProfileCategory profileCategory)
|
||||
{
|
||||
profileCategory.Save();
|
||||
_profileCategoryRepository.Save(profileCategory.Entity);
|
||||
|
||||
lock (_profileCategories)
|
||||
{
|
||||
_profileCategories.Sort((a, b) => a.Order - b.Order);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void SaveProfile(Profile profile, bool includeChildren)
|
||||
{
|
||||
_logger.Debug("Updating profile - Saving {Profile}", profile);
|
||||
profile.Save();
|
||||
if (includeChildren)
|
||||
{
|
||||
foreach (RenderProfileElement child in profile.GetAllRenderElements())
|
||||
child.Save();
|
||||
}
|
||||
|
||||
// At this point the user made actual changes, save that
|
||||
profile.IsFreshImport = false;
|
||||
profile.ProfileEntity.IsFreshImport = false;
|
||||
|
||||
_profileRepository.Save(profile.ProfileEntity);
|
||||
|
||||
// If the provided profile is external (cloned or from the workshop?) but it is loaded locally too, reload the local instance
|
||||
// A bit dodge but it ensures local instances always represent the latest stored version
|
||||
ProfileConfiguration? localInstance = ProfileConfigurations.FirstOrDefault(p => p.Profile != null && p.Profile != profile && p.ProfileId == profile.ProfileEntity.Id);
|
||||
if (localInstance == null)
|
||||
return;
|
||||
DeactivateProfile(localInstance);
|
||||
ActivateProfile(localInstance);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<Stream> ExportProfile(ProfileConfiguration profileConfiguration)
|
||||
{
|
||||
ProfileEntity? profileEntity = _profileRepository.Get(profileConfiguration.Entity.ProfileId);
|
||||
if (profileEntity == null)
|
||||
throw new ArtemisCoreException("Could not locate profile entity");
|
||||
|
||||
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))
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<ProfileConfiguration> ImportProfile(Stream archiveStream, ProfileCategory category, bool makeUnique, bool markAsFreshImport, string? nameAffix, int targetIndex = 0)
|
||||
{
|
||||
using ZipArchive archive = new(archiveStream, ZipArchiveMode.Read, true);
|
||||
|
||||
// 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<ProfileConfigurationEntity>(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<ProfileEntity>(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}";
|
||||
if (markAsFreshImport)
|
||||
profileEntity.IsFreshImport = true;
|
||||
|
||||
if (_profileRepository.Get(profileEntity.Id) == null)
|
||||
_profileRepository.Add(profileEntity);
|
||||
else
|
||||
throw new ArtemisCoreException($"Cannot import this profile without {nameof(makeUnique)} being true");
|
||||
|
||||
// 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 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, targetIndex);
|
||||
|
||||
List<Module> modules = _pluginManagementService.GetFeaturesOfType<Module>();
|
||||
profileConfiguration.LoadModules(modules);
|
||||
SaveProfileCategory(category);
|
||||
|
||||
return profileConfiguration;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<ProfileConfiguration> OverwriteProfile(MemoryStream archiveStream, ProfileConfiguration profileConfiguration)
|
||||
{
|
||||
ProfileConfiguration imported = await ImportProfile(archiveStream, profileConfiguration.Category, true, true, null, profileConfiguration.Order + 1);
|
||||
|
||||
DeleteProfile(profileConfiguration);
|
||||
SaveProfileCategory(imported.Category);
|
||||
|
||||
return imported;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void AdaptProfile(Profile profile)
|
||||
{
|
||||
List<ArtemisDevice> devices = _rgbService.EnabledDevices.ToList();
|
||||
foreach (Layer layer in profile.GetAllLayers())
|
||||
layer.Adapter.Adapt(devices);
|
||||
|
||||
profile.Save();
|
||||
|
||||
foreach (RenderProfileElement renderProfileElement in profile.GetAllRenderElements())
|
||||
renderProfileElement.Save();
|
||||
|
||||
_logger.Debug("Adapt profile - Saving " + profile);
|
||||
_profileRepository.Save(profile.ProfileEntity);
|
||||
}
|
||||
|
||||
private void InputServiceOnKeyboardKeyUp(object? sender, ArtemisKeyboardKeyEventArgs e)
|
||||
{
|
||||
lock (_profileCategories)
|
||||
{
|
||||
_pendingKeyboardEvents.Add(e);
|
||||
@@ -129,7 +605,7 @@ internal class ProfileService : IProfileService
|
||||
profileConfiguration.IsSuspended = true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// If suspension was changed, save the category
|
||||
if (before != profileConfiguration.IsSuspended)
|
||||
SaveProfileCategory(profileConfiguration.Category);
|
||||
@@ -185,432 +661,6 @@ internal class ProfileService : IProfileService
|
||||
_renderExceptions.Clear();
|
||||
}
|
||||
|
||||
public bool HotkeysEnabled { get; set; }
|
||||
public bool RenderForEditor { get; set; }
|
||||
public ProfileElement? EditorFocus { get; set; }
|
||||
|
||||
public void UpdateProfiles(double deltaTime)
|
||||
{
|
||||
lock (_profileCategories)
|
||||
{
|
||||
// Iterate the children in reverse because the first category must be rendered last to end up on top
|
||||
for (int i = _profileCategories.Count - 1; i > -1; i--)
|
||||
{
|
||||
ProfileCategory profileCategory = _profileCategories[i];
|
||||
for (int j = profileCategory.ProfileConfigurations.Count - 1; j > -1; j--)
|
||||
{
|
||||
ProfileConfiguration profileConfiguration = profileCategory.ProfileConfigurations[j];
|
||||
|
||||
// Process hotkeys that where pressed since this profile last updated
|
||||
ProcessPendingKeyEvents(profileConfiguration);
|
||||
|
||||
// Profiles being edited are updated at their own leisure
|
||||
if (profileConfiguration.IsBeingEdited && RenderForEditor)
|
||||
continue;
|
||||
|
||||
bool shouldBeActive = profileConfiguration.ShouldBeActive(false);
|
||||
if (shouldBeActive)
|
||||
{
|
||||
profileConfiguration.Update();
|
||||
if (!profileConfiguration.IsBeingEdited)
|
||||
shouldBeActive = profileConfiguration.ActivationConditionMet;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
// Make sure the profile is active or inactive according to the parameters above
|
||||
if (shouldBeActive && profileConfiguration.Profile == null && profileConfiguration.BrokenState != "Failed to activate profile")
|
||||
profileConfiguration.TryOrBreak(() => ActivateProfile(profileConfiguration), "Failed to activate profile");
|
||||
if (shouldBeActive && profileConfiguration.Profile != null && !profileConfiguration.Profile.ShouldDisplay)
|
||||
profileConfiguration.Profile.ShouldDisplay = true;
|
||||
else if (!shouldBeActive && profileConfiguration.Profile != null)
|
||||
{
|
||||
if (!profileConfiguration.FadeInAndOut)
|
||||
DeactivateProfile(profileConfiguration);
|
||||
else if (!profileConfiguration.Profile.ShouldDisplay && profileConfiguration.Profile.Opacity <= 0)
|
||||
DeactivateProfile(profileConfiguration);
|
||||
else if (profileConfiguration.Profile.Opacity > 0)
|
||||
RequestDeactivation(profileConfiguration);
|
||||
}
|
||||
|
||||
profileConfiguration.Profile?.Update(deltaTime);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_updateExceptions.Add(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LogProfileUpdateExceptions();
|
||||
_pendingKeyboardEvents.Clear();
|
||||
}
|
||||
}
|
||||
|
||||
public void RenderProfiles(SKCanvas canvas)
|
||||
{
|
||||
lock (_profileCategories)
|
||||
{
|
||||
ProfileConfiguration? editedProfileConfiguration = _profileCategories.SelectMany(c => c.ProfileConfigurations).FirstOrDefault(p => p.IsBeingEdited);
|
||||
if (editedProfileConfiguration != null)
|
||||
{
|
||||
editedProfileConfiguration.Profile?.Render(canvas, SKPointI.Empty, RenderForEditor ? EditorFocus : null);
|
||||
return;
|
||||
}
|
||||
|
||||
if (RenderForEditor)
|
||||
return;
|
||||
|
||||
// Iterate the children in reverse because the first category must be rendered last to end up on top
|
||||
for (int i = _profileCategories.Count - 1; i > -1; i--)
|
||||
{
|
||||
ProfileCategory profileCategory = _profileCategories[i];
|
||||
for (int j = profileCategory.ProfileConfigurations.Count - 1; j > -1; j--)
|
||||
{
|
||||
try
|
||||
{
|
||||
ProfileConfiguration profileConfiguration = profileCategory.ProfileConfigurations[j];
|
||||
// Ensure all criteria are met before rendering
|
||||
bool fadingOut = profileConfiguration.Profile?.ShouldDisplay == false && profileConfiguration.Profile?.Opacity > 0;
|
||||
if (!profileConfiguration.IsSuspended && !profileConfiguration.IsMissingModule && (profileConfiguration.ActivationConditionMet || fadingOut))
|
||||
profileConfiguration.Profile?.Render(canvas, SKPointI.Empty, null);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
_renderExceptions.Add(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
LogProfileRenderExceptions();
|
||||
}
|
||||
}
|
||||
|
||||
public ReadOnlyCollection<ProfileCategory> ProfileCategories
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_profileRepository)
|
||||
{
|
||||
return _profileCategories.AsReadOnly();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public ReadOnlyCollection<ProfileConfiguration> ProfileConfigurations
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_profileRepository)
|
||||
{
|
||||
return _profileCategories.SelectMany(c => c.ProfileConfigurations).ToList().AsReadOnly();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void LoadProfileConfigurationIcon(ProfileConfiguration profileConfiguration)
|
||||
{
|
||||
if (profileConfiguration.Icon.IconType == ProfileConfigurationIconType.MaterialIcon)
|
||||
return;
|
||||
|
||||
using Stream? stream = _profileCategoryRepository.GetProfileIconStream(profileConfiguration.Entity.FileIconId);
|
||||
if (stream != null)
|
||||
profileConfiguration.Icon.SetIconByStream(stream);
|
||||
}
|
||||
|
||||
public void SaveProfileConfigurationIcon(ProfileConfiguration profileConfiguration)
|
||||
{
|
||||
if (profileConfiguration.Icon.IconType == ProfileConfigurationIconType.MaterialIcon)
|
||||
return;
|
||||
|
||||
using Stream? stream = profileConfiguration.Icon.GetIconStream();
|
||||
if (stream != null)
|
||||
{
|
||||
_profileCategoryRepository.SaveProfileIconStream(profileConfiguration.Entity, stream);
|
||||
}
|
||||
}
|
||||
|
||||
public Profile ActivateProfile(ProfileConfiguration profileConfiguration)
|
||||
{
|
||||
if (profileConfiguration.Profile != null)
|
||||
{
|
||||
profileConfiguration.Profile.ShouldDisplay = true;
|
||||
return profileConfiguration.Profile;
|
||||
}
|
||||
|
||||
ProfileEntity profileEntity;
|
||||
try
|
||||
{
|
||||
profileEntity = _profileRepository.Get(profileConfiguration.Entity.ProfileId);
|
||||
}
|
||||
catch (Exception e)
|
||||
{
|
||||
profileConfiguration.SetBrokenState("Failed to activate profile", e);
|
||||
throw;
|
||||
}
|
||||
|
||||
if (profileEntity == null)
|
||||
throw new ArtemisCoreException($"Cannot find profile named: {profileConfiguration.Name} ID: {profileConfiguration.Entity.ProfileId}");
|
||||
|
||||
Profile profile = new(profileConfiguration, profileEntity);
|
||||
profile.PopulateLeds(_rgbService.EnabledDevices);
|
||||
|
||||
if (profile.IsFreshImport)
|
||||
{
|
||||
_logger.Debug("Profile is a fresh import, adapting to surface - {profile}", profile);
|
||||
AdaptProfile(profile);
|
||||
}
|
||||
|
||||
profileConfiguration.Profile = profile;
|
||||
|
||||
OnProfileActivated(new ProfileConfigurationEventArgs(profileConfiguration));
|
||||
return profile;
|
||||
}
|
||||
|
||||
public void DeactivateProfile(ProfileConfiguration profileConfiguration)
|
||||
{
|
||||
if (profileConfiguration.IsBeingEdited)
|
||||
throw new ArtemisCoreException("Cannot disable a profile that is being edited, that's rude");
|
||||
if (profileConfiguration.Profile == null)
|
||||
return;
|
||||
|
||||
Profile profile = profileConfiguration.Profile;
|
||||
profileConfiguration.Profile = null;
|
||||
profile.Dispose();
|
||||
|
||||
OnProfileDeactivated(new ProfileConfigurationEventArgs(profileConfiguration));
|
||||
}
|
||||
|
||||
private void RequestDeactivation(ProfileConfiguration profileConfiguration)
|
||||
{
|
||||
if (profileConfiguration.IsBeingEdited)
|
||||
throw new ArtemisCoreException("Cannot disable a profile that is being edited, that's rude");
|
||||
if (profileConfiguration.Profile == null)
|
||||
return;
|
||||
|
||||
profileConfiguration.Profile.ShouldDisplay = false;
|
||||
}
|
||||
|
||||
public void DeleteProfile(ProfileConfiguration profileConfiguration)
|
||||
{
|
||||
DeactivateProfile(profileConfiguration);
|
||||
|
||||
ProfileEntity profileEntity = _profileRepository.Get(profileConfiguration.Entity.ProfileId);
|
||||
if (profileEntity == null)
|
||||
return;
|
||||
|
||||
profileConfiguration.Category.RemoveProfileConfiguration(profileConfiguration);
|
||||
_profileRepository.Remove(profileEntity);
|
||||
SaveProfileCategory(profileConfiguration.Category);
|
||||
}
|
||||
|
||||
public ProfileCategory CreateProfileCategory(string name)
|
||||
{
|
||||
ProfileCategory profileCategory;
|
||||
lock (_profileRepository)
|
||||
{
|
||||
profileCategory = new ProfileCategory(name, _profileCategories.Count + 1);
|
||||
_profileCategories.Add(profileCategory);
|
||||
SaveProfileCategory(profileCategory);
|
||||
}
|
||||
|
||||
OnProfileCategoryAdded(new ProfileCategoryEventArgs(profileCategory));
|
||||
return profileCategory;
|
||||
}
|
||||
|
||||
public void DeleteProfileCategory(ProfileCategory profileCategory)
|
||||
{
|
||||
List<ProfileConfiguration> profileConfigurations = profileCategory.ProfileConfigurations.ToList();
|
||||
foreach (ProfileConfiguration profileConfiguration in profileConfigurations)
|
||||
RemoveProfileConfiguration(profileConfiguration);
|
||||
|
||||
lock (_profileRepository)
|
||||
{
|
||||
_profileCategories.Remove(profileCategory);
|
||||
_profileCategoryRepository.Remove(profileCategory.Entity);
|
||||
}
|
||||
|
||||
OnProfileCategoryRemoved(new ProfileCategoryEventArgs(profileCategory));
|
||||
}
|
||||
|
||||
public ProfileConfiguration CreateProfileConfiguration(ProfileCategory category, string name, string icon)
|
||||
{
|
||||
ProfileConfiguration configuration = new(category, name, icon);
|
||||
ProfileEntity entity = new();
|
||||
_profileRepository.Add(entity);
|
||||
|
||||
configuration.Entity.ProfileId = entity.Id;
|
||||
category.AddProfileConfiguration(configuration, 0);
|
||||
return configuration;
|
||||
}
|
||||
|
||||
public void RemoveProfileConfiguration(ProfileConfiguration profileConfiguration)
|
||||
{
|
||||
profileConfiguration.Category.RemoveProfileConfiguration(profileConfiguration);
|
||||
|
||||
DeactivateProfile(profileConfiguration);
|
||||
SaveProfileCategory(profileConfiguration.Category);
|
||||
ProfileEntity profileEntity = _profileRepository.Get(profileConfiguration.Entity.ProfileId);
|
||||
if (profileEntity != null)
|
||||
_profileRepository.Remove(profileEntity);
|
||||
|
||||
profileConfiguration.Dispose();
|
||||
}
|
||||
|
||||
public void SaveProfileCategory(ProfileCategory profileCategory)
|
||||
{
|
||||
profileCategory.Save();
|
||||
_profileCategoryRepository.Save(profileCategory.Entity);
|
||||
|
||||
lock (_profileCategories)
|
||||
{
|
||||
_profileCategories.Sort((a, b) => a.Order - b.Order);
|
||||
}
|
||||
}
|
||||
|
||||
public void SaveProfile(Profile profile, bool includeChildren)
|
||||
{
|
||||
_logger.Debug("Updating profile - Saving {Profile}", profile);
|
||||
profile.Save();
|
||||
if (includeChildren)
|
||||
foreach (RenderProfileElement child in profile.GetAllRenderElements())
|
||||
child.Save();
|
||||
|
||||
// At this point the user made actual changes, save that
|
||||
profile.IsFreshImport = false;
|
||||
profile.ProfileEntity.IsFreshImport = false;
|
||||
|
||||
_profileRepository.Save(profile.ProfileEntity);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<Stream> ExportProfile(ProfileConfiguration profileConfiguration)
|
||||
{
|
||||
ProfileEntity? profileEntity = _profileRepository.Get(profileConfiguration.Entity.ProfileId);
|
||||
if (profileEntity == null)
|
||||
throw new ArtemisCoreException("Could not locate profile entity");
|
||||
|
||||
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))
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public async Task<ProfileConfiguration> ImportProfile(Stream archiveStream, ProfileCategory category, bool makeUnique, bool markAsFreshImport, string? nameAffix)
|
||||
{
|
||||
using ZipArchive archive = new(archiveStream, ZipArchiveMode.Read, true);
|
||||
|
||||
// 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<ProfileConfigurationEntity>(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<ProfileEntity>(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}";
|
||||
if (markAsFreshImport)
|
||||
profileEntity.IsFreshImport = true;
|
||||
|
||||
if (_profileRepository.Get(profileEntity.Id) == null)
|
||||
_profileRepository.Add(profileEntity);
|
||||
else
|
||||
throw new ArtemisCoreException($"Cannot import this profile without {nameof(makeUnique)} being true");
|
||||
|
||||
// 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 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);
|
||||
|
||||
List<Module> modules = _pluginManagementService.GetFeaturesOfType<Module>();
|
||||
profileConfiguration.LoadModules(modules);
|
||||
SaveProfileCategory(category);
|
||||
|
||||
return profileConfiguration;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public void AdaptProfile(Profile profile)
|
||||
{
|
||||
List<ArtemisDevice> devices = _rgbService.EnabledDevices.ToList();
|
||||
foreach (Layer layer in profile.GetAllLayers())
|
||||
layer.Adapter.Adapt(devices);
|
||||
|
||||
profile.Save();
|
||||
|
||||
foreach (RenderProfileElement renderProfileElement in profile.GetAllRenderElements())
|
||||
renderProfileElement.Save();
|
||||
|
||||
_logger.Debug("Adapt profile - Saving " + profile);
|
||||
_profileRepository.Save(profile.ProfileEntity);
|
||||
}
|
||||
|
||||
#region Events
|
||||
|
||||
public event EventHandler<ProfileConfigurationEventArgs>? ProfileActivated;
|
||||
|
||||
Reference in new issue
Block a user