mirror of
https://github.com/Artemis-RGB/Artemis
synced 2025-12-13 05:48:35 +00:00
Merge branch 'development'
This commit is contained in:
commit
bbadef7a9a
@ -17,7 +17,6 @@ public sealed class Profile : ProfileElement
|
|||||||
private readonly ObservableCollection<ScriptConfiguration> _scriptConfigurations;
|
private readonly ObservableCollection<ScriptConfiguration> _scriptConfigurations;
|
||||||
private readonly ObservableCollection<ProfileScript> _scripts;
|
private readonly ObservableCollection<ProfileScript> _scripts;
|
||||||
private bool _isFreshImport;
|
private bool _isFreshImport;
|
||||||
private ProfileElement? _lastSelectedProfileElement;
|
|
||||||
|
|
||||||
internal Profile(ProfileConfiguration configuration, ProfileEntity profileEntity) : base(null!)
|
internal Profile(ProfileConfiguration configuration, ProfileEntity profileEntity) : base(null!)
|
||||||
{
|
{
|
||||||
@ -67,15 +66,6 @@ public sealed class Profile : ProfileElement
|
|||||||
set => SetAndNotify(ref _isFreshImport, value);
|
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>
|
/// <summary>
|
||||||
/// Gets the profile entity this profile uses for persistent storage
|
/// Gets the profile entity this profile uses for persistent storage
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -130,7 +120,7 @@ public sealed class Profile : ProfileElement
|
|||||||
if (applyOpacityLayer)
|
if (applyOpacityLayer)
|
||||||
{
|
{
|
||||||
opacityPaint = new SKPaint();
|
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);
|
canvas.SaveLayer(opacityPaint);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -242,20 +232,13 @@ public sealed class Profile : ProfileElement
|
|||||||
AddChild(new Folder(this, this, rootFolder));
|
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())
|
while (_scriptConfigurations.Any())
|
||||||
RemoveScriptConfiguration(_scriptConfigurations[0]);
|
RemoveScriptConfiguration(_scriptConfigurations[0]);
|
||||||
foreach (ScriptConfiguration scriptConfiguration in ProfileEntity.ScriptConfigurations.Select(e => new ScriptConfiguration(e)))
|
foreach (ScriptConfiguration scriptConfiguration in ProfileEntity.ScriptConfigurations.Select(e => new ScriptConfiguration(e)))
|
||||||
AddScriptConfiguration(scriptConfiguration);
|
AddScriptConfiguration(scriptConfiguration);
|
||||||
|
|
||||||
// Load node scripts last since they may rely on the profile structure being in place
|
// 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();
|
renderProfileElement.LoadNodeScript();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -312,7 +295,6 @@ public sealed class Profile : ProfileElement
|
|||||||
ProfileEntity.Id = EntityId;
|
ProfileEntity.Id = EntityId;
|
||||||
ProfileEntity.Name = Configuration.Name;
|
ProfileEntity.Name = Configuration.Name;
|
||||||
ProfileEntity.IsFreshImport = IsFreshImport;
|
ProfileEntity.IsFreshImport = IsFreshImport;
|
||||||
ProfileEntity.LastSelectedProfileElement = LastSelectedProfileElement?.EntityId ?? Guid.Empty;
|
|
||||||
|
|
||||||
foreach (ProfileElement profileElement in Children)
|
foreach (ProfileElement profileElement in Children)
|
||||||
profileElement.Save();
|
profileElement.Save();
|
||||||
|
|||||||
@ -98,6 +98,7 @@ public class ProfileCategory : CorePropertyChanged, IStorageModel
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
public void AddProfileConfiguration(ProfileConfiguration configuration, int? targetIndex)
|
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
|
// 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))
|
if (configuration.Category == this && targetIndex != null && targetIndex.Value > _profileConfigurations.IndexOf(configuration))
|
||||||
targetIndex -= 1;
|
targetIndex -= 1;
|
||||||
|
|||||||
@ -148,15 +148,6 @@ public class ProfileConfiguration : BreakableModel, IStorageModel, IDisposable
|
|||||||
private set => SetAndNotify(ref _activationConditionMet, value);
|
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>
|
/// <summary>
|
||||||
/// Gets the profile of this profile configuration
|
/// Gets the profile of this profile configuration
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -243,8 +234,6 @@ public class ProfileConfiguration : BreakableModel, IStorageModel, IDisposable
|
|||||||
{
|
{
|
||||||
if (_disposed)
|
if (_disposed)
|
||||||
throw new ObjectDisposedException("ProfileConfiguration");
|
throw new ObjectDisposedException("ProfileConfiguration");
|
||||||
if (IsBeingEdited)
|
|
||||||
return true;
|
|
||||||
if (Category.IsSuspended || IsSuspended || IsMissingModule)
|
if (Category.IsSuspended || IsSuspended || IsMissingModule)
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
|
|||||||
@ -15,7 +15,6 @@ public class ProfileConfigurationIcon : CorePropertyChanged, IStorageModel
|
|||||||
private string? _iconName;
|
private string? _iconName;
|
||||||
private Stream? _iconStream;
|
private Stream? _iconStream;
|
||||||
private ProfileConfigurationIconType _iconType;
|
private ProfileConfigurationIconType _iconType;
|
||||||
private string? _originalFileName;
|
|
||||||
|
|
||||||
internal ProfileConfigurationIcon(ProfileConfigurationEntity entity)
|
internal ProfileConfigurationIcon(ProfileConfigurationEntity entity)
|
||||||
{
|
{
|
||||||
|
|||||||
@ -28,19 +28,26 @@ public interface IProfileService : IArtemisService
|
|||||||
ReadOnlyCollection<ProfileConfiguration> ProfileConfigurations { get; }
|
ReadOnlyCollection<ProfileConfiguration> ProfileConfigurations { get; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets a boolean indicating whether hotkeys are enabled.
|
/// Gets or sets the focused profile configuration which is rendered exclusively.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
bool HotkeysEnabled { get; set; }
|
ProfileConfiguration? FocusProfile { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <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>
|
/// </summary>
|
||||||
bool RenderForEditor { get; set; }
|
ProfileElement? FocusProfileElement { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <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>
|
/// </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>
|
/// <summary>
|
||||||
/// Activates the profile of the given <see cref="ProfileConfiguration" /> with the currently active surface.
|
/// 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.
|
/// Creates a new profile category and saves it to persistent storage.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="name">The name of the new profile category, must be unique.</param>
|
/// <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>
|
/// <returns>The newly created profile category.</returns>
|
||||||
ProfileCategory CreateProfileCategory(string name);
|
ProfileCategory CreateProfileCategory(string name, bool addToTop = false);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Permanently deletes the provided profile category.
|
/// Permanently deletes the provided profile category.
|
||||||
@ -119,7 +127,7 @@ public interface IProfileService : IArtemisService
|
|||||||
Task<Stream> ExportProfile(ProfileConfiguration profileConfiguration);
|
Task<Stream> ExportProfile(ProfileConfiguration profileConfiguration);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Imports the provided base64 encoded GZIPed JSON as a profile configuration.
|
/// Imports the provided ZIP archive stream as a profile configuration.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="archiveStream">The zip archive containing the profile to import.</param>
|
/// <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>
|
/// <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.
|
/// any changes are made to it.
|
||||||
/// </param>
|
/// </param>
|
||||||
/// <param name="nameAffix">Text to add after the name of the profile (separated by a dash).</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>
|
/// <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>
|
/// <summary>
|
||||||
/// Adapts a given profile to the currently active devices.
|
/// Adapts a given profile to the currently active devices.
|
||||||
@ -168,4 +185,5 @@ public interface IProfileService : IArtemisService
|
|||||||
/// Occurs whenever a profile category is removed.
|
/// Occurs whenever a profile category is removed.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public event EventHandler<ProfileCategoryEventArgs>? ProfileCategoryRemoved;
|
public event EventHandler<ProfileCategoryEventArgs>? ProfileCategoryRemoved;
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -19,14 +19,14 @@ namespace Artemis.Core.Services;
|
|||||||
internal class ProfileService : IProfileService
|
internal class ProfileService : IProfileService
|
||||||
{
|
{
|
||||||
private readonly ILogger _logger;
|
private readonly ILogger _logger;
|
||||||
|
private readonly IRgbService _rgbService;
|
||||||
|
private readonly IProfileCategoryRepository _profileCategoryRepository;
|
||||||
|
private readonly IPluginManagementService _pluginManagementService;
|
||||||
|
|
||||||
private readonly List<ArtemisKeyboardKeyEventArgs> _pendingKeyboardEvents = new();
|
private readonly List<ArtemisKeyboardKeyEventArgs> _pendingKeyboardEvents = new();
|
||||||
private readonly IPluginManagementService _pluginManagementService;
|
|
||||||
private readonly List<ProfileCategory> _profileCategories;
|
private readonly List<ProfileCategory> _profileCategories;
|
||||||
private readonly IProfileCategoryRepository _profileCategoryRepository;
|
|
||||||
private readonly IProfileRepository _profileRepository;
|
private readonly IProfileRepository _profileRepository;
|
||||||
private readonly List<Exception> _renderExceptions = new();
|
private readonly List<Exception> _renderExceptions = new();
|
||||||
private readonly IRgbService _rgbService;
|
|
||||||
private readonly List<Exception> _updateExceptions = new();
|
private readonly List<Exception> _updateExceptions = new();
|
||||||
private DateTime _lastRenderExceptionLog;
|
private DateTime _lastRenderExceptionLog;
|
||||||
private DateTime _lastUpdateExceptionLog;
|
private DateTime _lastUpdateExceptionLog;
|
||||||
@ -49,7 +49,6 @@ internal class ProfileService : IProfileService
|
|||||||
_pluginManagementService.PluginFeatureEnabled += PluginManagementServiceOnPluginFeatureToggled;
|
_pluginManagementService.PluginFeatureEnabled += PluginManagementServiceOnPluginFeatureToggled;
|
||||||
_pluginManagementService.PluginFeatureDisabled += PluginManagementServiceOnPluginFeatureToggled;
|
_pluginManagementService.PluginFeatureDisabled += PluginManagementServiceOnPluginFeatureToggled;
|
||||||
|
|
||||||
HotkeysEnabled = true;
|
|
||||||
inputService.KeyboardKeyUp += InputServiceOnKeyboardKeyUp;
|
inputService.KeyboardKeyUp += InputServiceOnKeyboardKeyUp;
|
||||||
|
|
||||||
if (!_profileCategories.Any())
|
if (!_profileCategories.Any())
|
||||||
@ -57,11 +56,488 @@ internal class ProfileService : IProfileService
|
|||||||
UpdateModules();
|
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;
|
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)
|
lock (_profileCategories)
|
||||||
{
|
{
|
||||||
_pendingKeyboardEvents.Add(e);
|
_pendingKeyboardEvents.Add(e);
|
||||||
@ -185,432 +661,6 @@ internal class ProfileService : IProfileService
|
|||||||
_renderExceptions.Clear();
|
_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
|
#region Events
|
||||||
|
|
||||||
public event EventHandler<ProfileConfigurationEventArgs>? ProfileActivated;
|
public event EventHandler<ProfileConfigurationEventArgs>? ProfileActivated;
|
||||||
|
|||||||
@ -18,7 +18,6 @@ public class ProfileEntity
|
|||||||
|
|
||||||
public string Name { get; set; }
|
public string Name { get; set; }
|
||||||
public bool IsFreshImport { get; set; }
|
public bool IsFreshImport { get; set; }
|
||||||
public Guid LastSelectedProfileElement { get; set; }
|
|
||||||
|
|
||||||
public List<FolderEntity> Folders { get; set; }
|
public List<FolderEntity> Folders { get; set; }
|
||||||
public List<LayerEntity> Layers { get; set; }
|
public List<LayerEntity> Layers { get; set; }
|
||||||
|
|||||||
21
src/Artemis.Storage/Entities/Workshop/EntryEntity.cs
Normal file
21
src/Artemis.Storage/Entities/Workshop/EntryEntity.cs
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Artemis.Storage.Entities.Workshop;
|
||||||
|
|
||||||
|
public class EntryEntity
|
||||||
|
{
|
||||||
|
public Guid Id { get; set; }
|
||||||
|
|
||||||
|
public long EntryId { get; set; }
|
||||||
|
public int EntryType { get; set; }
|
||||||
|
|
||||||
|
public string Author { get; set; }
|
||||||
|
public string Name { get; set; } = string.Empty;
|
||||||
|
public string Summary { get; set; } = string.Empty;
|
||||||
|
|
||||||
|
public long ReleaseId { get; set; }
|
||||||
|
public string ReleaseVersion { get; set; }
|
||||||
|
public DateTimeOffset InstalledAt { get; set; }
|
||||||
|
|
||||||
|
public string LocalReference { get; set; }
|
||||||
|
}
|
||||||
54
src/Artemis.Storage/Repositories/EntryRepository.cs
Normal file
54
src/Artemis.Storage/Repositories/EntryRepository.cs
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using Artemis.Storage.Entities.Workshop;
|
||||||
|
using Artemis.Storage.Repositories.Interfaces;
|
||||||
|
using LiteDB;
|
||||||
|
|
||||||
|
namespace Artemis.Storage.Repositories;
|
||||||
|
|
||||||
|
internal class EntryRepository : IEntryRepository
|
||||||
|
{
|
||||||
|
private readonly LiteRepository _repository;
|
||||||
|
|
||||||
|
public EntryRepository(LiteRepository repository)
|
||||||
|
{
|
||||||
|
_repository = repository;
|
||||||
|
_repository.Database.GetCollection<EntryEntity>().EnsureIndex(s => s.Id);
|
||||||
|
_repository.Database.GetCollection<EntryEntity>().EnsureIndex(s => s.EntryId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Add(EntryEntity entryEntity)
|
||||||
|
{
|
||||||
|
_repository.Insert(entryEntity);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Remove(EntryEntity entryEntity)
|
||||||
|
{
|
||||||
|
_repository.Delete<EntryEntity>(entryEntity.Id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public EntryEntity Get(Guid id)
|
||||||
|
{
|
||||||
|
return _repository.FirstOrDefault<EntryEntity>(s => s.Id == id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public EntryEntity GetByEntryId(long entryId)
|
||||||
|
{
|
||||||
|
return _repository.FirstOrDefault<EntryEntity>(s => s.EntryId == entryId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<EntryEntity> GetAll()
|
||||||
|
{
|
||||||
|
return _repository.Query<EntryEntity>().ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Save(EntryEntity entryEntity)
|
||||||
|
{
|
||||||
|
_repository.Upsert(entryEntity);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Save(IEnumerable<EntryEntity> entryEntities)
|
||||||
|
{
|
||||||
|
_repository.Upsert(entryEntities);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,16 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using Artemis.Storage.Entities.Workshop;
|
||||||
|
|
||||||
|
namespace Artemis.Storage.Repositories.Interfaces;
|
||||||
|
|
||||||
|
public interface IEntryRepository : IRepository
|
||||||
|
{
|
||||||
|
void Add(EntryEntity entryEntity);
|
||||||
|
void Remove(EntryEntity entryEntity);
|
||||||
|
EntryEntity Get(Guid id);
|
||||||
|
EntryEntity GetByEntryId(long entryId);
|
||||||
|
List<EntryEntity> GetAll();
|
||||||
|
void Save(EntryEntity entryEntity);
|
||||||
|
void Save(IEnumerable<EntryEntity> entryEntities);
|
||||||
|
}
|
||||||
@ -18,7 +18,7 @@
|
|||||||
<TrayIcon.Menu>
|
<TrayIcon.Menu>
|
||||||
<NativeMenu>
|
<NativeMenu>
|
||||||
<NativeMenuItem Header="Home" Command="{CompiledBinding OpenScreen}" CommandParameter="home" />
|
<NativeMenuItem Header="Home" Command="{CompiledBinding OpenScreen}" CommandParameter="home" />
|
||||||
<!-- <NativeMenuItem Header="Workshop" Command="{CompiledBinding OpenScreen}" CommandParameter="workshop" /> -->
|
<NativeMenuItem Header="Workshop" Command="{CompiledBinding OpenScreen}" CommandParameter="workshop" />
|
||||||
<NativeMenuItem Header="Surface Editor" Command="{CompiledBinding OpenScreen}" CommandParameter="surface-editor" />
|
<NativeMenuItem Header="Surface Editor" Command="{CompiledBinding OpenScreen}" CommandParameter="surface-editor" />
|
||||||
<NativeMenuItem Header="Settings" Command="{CompiledBinding OpenScreen}" CommandParameter="settings" />
|
<NativeMenuItem Header="Settings" Command="{CompiledBinding OpenScreen}" CommandParameter="settings" />
|
||||||
<NativeMenuItemSeparator />
|
<NativeMenuItemSeparator />
|
||||||
|
|||||||
@ -18,7 +18,7 @@
|
|||||||
<TrayIcon.Menu>
|
<TrayIcon.Menu>
|
||||||
<NativeMenu>
|
<NativeMenu>
|
||||||
<NativeMenuItem Header="Home" Command="{CompiledBinding OpenScreen}" CommandParameter="home" />
|
<NativeMenuItem Header="Home" Command="{CompiledBinding OpenScreen}" CommandParameter="home" />
|
||||||
<!-- <NativeMenuItem Header="Workshop" Command="{CompiledBinding OpenScreen}" CommandParameter="workshop" /> -->
|
<NativeMenuItem Header="Workshop" Command="{CompiledBinding OpenScreen}" CommandParameter="workshop" />
|
||||||
<NativeMenuItem Header="Surface Editor" Command="{CompiledBinding OpenScreen}" CommandParameter="surface-editor" />
|
<NativeMenuItem Header="Surface Editor" Command="{CompiledBinding OpenScreen}" CommandParameter="surface-editor" />
|
||||||
<NativeMenuItem Header="Settings" Command="{CompiledBinding OpenScreen}" CommandParameter="settings" />
|
<NativeMenuItem Header="Settings" Command="{CompiledBinding OpenScreen}" CommandParameter="settings" />
|
||||||
<NativeMenuItemSeparator />
|
<NativeMenuItemSeparator />
|
||||||
|
|||||||
@ -15,6 +15,7 @@
|
|||||||
<PackageReference Include="Avalonia" Version="$(AvaloniaVersion)" />
|
<PackageReference Include="Avalonia" Version="$(AvaloniaVersion)" />
|
||||||
<!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.-->
|
<!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.-->
|
||||||
<PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="$(AvaloniaVersion)" />
|
<PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="$(AvaloniaVersion)" />
|
||||||
|
<PackageReference Include="Avalonia.Controls.ItemsRepeater" Version="$(AvaloniaVersion)" />
|
||||||
<PackageReference Include="Avalonia.ReactiveUI" Version="$(AvaloniaVersion)" />
|
<PackageReference Include="Avalonia.ReactiveUI" Version="$(AvaloniaVersion)" />
|
||||||
<PackageReference Include="Avalonia.Xaml.Behaviors" Version="$(AvaloniaBehavioursVersion)" />
|
<PackageReference Include="Avalonia.Xaml.Behaviors" Version="$(AvaloniaBehavioursVersion)" />
|
||||||
<PackageReference Include="DynamicData" Version="7.13.1" />
|
<PackageReference Include="DynamicData" Version="7.13.1" />
|
||||||
|
|||||||
54
src/Artemis.UI.Shared/Controls/NotificationHost.cs
Normal file
54
src/Artemis.UI.Shared/Controls/NotificationHost.cs
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
using System;
|
||||||
|
using Avalonia;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Controls.Primitives;
|
||||||
|
using Avalonia.Layout;
|
||||||
|
|
||||||
|
namespace Artemis.UI.Shared;
|
||||||
|
|
||||||
|
internal class NotificationHost : ContentControl
|
||||||
|
{
|
||||||
|
private IDisposable? _rootBoundsWatcher;
|
||||||
|
|
||||||
|
public NotificationHost()
|
||||||
|
{
|
||||||
|
Background = null;
|
||||||
|
HorizontalAlignment = HorizontalAlignment.Center;
|
||||||
|
VerticalAlignment = VerticalAlignment.Center;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override Type StyleKeyOverride => typeof(OverlayPopupHost);
|
||||||
|
|
||||||
|
protected override Size MeasureOverride(Size availableSize)
|
||||||
|
{
|
||||||
|
_ = base.MeasureOverride(availableSize);
|
||||||
|
|
||||||
|
if (VisualRoot is TopLevel tl)
|
||||||
|
return tl.ClientSize;
|
||||||
|
if (VisualRoot is Control c)
|
||||||
|
return c.Bounds.Size;
|
||||||
|
|
||||||
|
return default;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
|
||||||
|
{
|
||||||
|
base.OnAttachedToVisualTree(e);
|
||||||
|
if (e.Root is Control wb)
|
||||||
|
// OverlayLayer is a Canvas, so we won't get a signal to resize if the window
|
||||||
|
// bounds change. Subscribe to force update
|
||||||
|
_rootBoundsWatcher = wb.GetObservable(BoundsProperty).Subscribe(_ => OnRootBoundsChanged());
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
|
||||||
|
{
|
||||||
|
base.OnDetachedFromVisualTree(e);
|
||||||
|
_rootBoundsWatcher?.Dispose();
|
||||||
|
_rootBoundsWatcher = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnRootBoundsChanged()
|
||||||
|
{
|
||||||
|
InvalidateMeasure();
|
||||||
|
}
|
||||||
|
}
|
||||||
78
src/Artemis.UI.Shared/Controls/TagsInput/TagsInput.cs
Normal file
78
src/Artemis.UI.Shared/Controls/TagsInput/TagsInput.cs
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
using System.Text.RegularExpressions;
|
||||||
|
using System.Windows.Input;
|
||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Controls.Metadata;
|
||||||
|
using Avalonia.Controls.Primitives;
|
||||||
|
using Avalonia.Input;
|
||||||
|
using ReactiveUI;
|
||||||
|
|
||||||
|
namespace Artemis.UI.Shared.TagsInput;
|
||||||
|
|
||||||
|
[TemplatePart("PART_TagInputBox", typeof(TextBox))]
|
||||||
|
public partial class TagsInput : TemplatedControl
|
||||||
|
{
|
||||||
|
public TextBox? TagInputBox { get; set; }
|
||||||
|
public ICommand RemoveTag { get; }
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public TagsInput()
|
||||||
|
{
|
||||||
|
RemoveTag = ReactiveCommand.Create<string>(ExecuteRemoveTag);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
|
||||||
|
{
|
||||||
|
if (TagInputBox != null)
|
||||||
|
{
|
||||||
|
TagInputBox.KeyDown -= TagInputBoxOnKeyDown;
|
||||||
|
TagInputBox.TextChanging -= TagInputBoxOnTextChanging;
|
||||||
|
}
|
||||||
|
|
||||||
|
TagInputBox = e.NameScope.Find<TextBox>("PART_TagInputBox");
|
||||||
|
|
||||||
|
if (TagInputBox != null)
|
||||||
|
{
|
||||||
|
TagInputBox.KeyDown += TagInputBoxOnKeyDown;
|
||||||
|
TagInputBox.TextChanging += TagInputBoxOnTextChanging;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ExecuteRemoveTag(string t)
|
||||||
|
{
|
||||||
|
Tags.Remove(t);
|
||||||
|
|
||||||
|
if (TagInputBox != null)
|
||||||
|
TagInputBox.IsEnabled = Tags.Count < MaxLength;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void TagInputBoxOnTextChanging(object? sender, TextChangingEventArgs e)
|
||||||
|
{
|
||||||
|
if (TagInputBox?.Text == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
TagInputBox.Text = CleanTagRegex().Replace(TagInputBox.Text.ToLower(), "");
|
||||||
|
}
|
||||||
|
|
||||||
|
private void TagInputBoxOnKeyDown(object? sender, KeyEventArgs e)
|
||||||
|
{
|
||||||
|
if (TagInputBox == null)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (e.Key == Key.Space)
|
||||||
|
e.Handled = true;
|
||||||
|
if (e.Key != Key.Enter)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(TagInputBox.Text) || Tags.Contains(TagInputBox.Text) || Tags.Count >= MaxLength)
|
||||||
|
return;
|
||||||
|
|
||||||
|
Tags.Add(CleanTagRegex().Replace(TagInputBox.Text.ToLower(), ""));
|
||||||
|
|
||||||
|
TagInputBox.Text = "";
|
||||||
|
TagInputBox.IsEnabled = Tags.Count < MaxLength;
|
||||||
|
}
|
||||||
|
|
||||||
|
[GeneratedRegex("[\\s\\-]+")]
|
||||||
|
private static partial Regex CleanTagRegex();
|
||||||
|
}
|
||||||
@ -0,0 +1,54 @@
|
|||||||
|
using System.Collections.ObjectModel;
|
||||||
|
using Avalonia;
|
||||||
|
using Avalonia.Controls.Primitives;
|
||||||
|
using Avalonia.Data;
|
||||||
|
|
||||||
|
namespace Artemis.UI.Shared.TagsInput;
|
||||||
|
|
||||||
|
public partial class TagsInput : TemplatedControl
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Defines the <see cref="Tags" /> property
|
||||||
|
/// </summary>
|
||||||
|
public static readonly StyledProperty<ObservableCollection<string>> TagsProperty =
|
||||||
|
AvaloniaProperty.Register<TagsInput, ObservableCollection<string>>(nameof(Tags), new ObservableCollection<string>());
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the selected tags.
|
||||||
|
/// </summary>
|
||||||
|
public ObservableCollection<string> Tags
|
||||||
|
{
|
||||||
|
get => GetValue(TagsProperty);
|
||||||
|
set => SetValue(TagsProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Defines the <see cref="MaxLength" /> property
|
||||||
|
/// </summary>
|
||||||
|
public static readonly StyledProperty<int> MaxLengthProperty =
|
||||||
|
AvaloniaProperty.Register<TagsInput, int>(nameof(MaxLength), 20);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the max length of each tag
|
||||||
|
/// </summary>
|
||||||
|
public int MaxLength
|
||||||
|
{
|
||||||
|
get => GetValue(MaxLengthProperty);
|
||||||
|
set => SetValue(MaxLengthProperty, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Defines the <see cref="MaxTags" /> property
|
||||||
|
/// </summary>
|
||||||
|
public static readonly StyledProperty<int> MaxTagsProperty =
|
||||||
|
AvaloniaProperty.Register<TagsInput, int>(nameof(MaxTags), 20);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the max amount of tags to be added
|
||||||
|
/// </summary>
|
||||||
|
public int MaxTags
|
||||||
|
{
|
||||||
|
get => GetValue(MaxTagsProperty);
|
||||||
|
set => SetValue(MaxTagsProperty, value);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,49 @@
|
|||||||
|
<ResourceDictionary xmlns="https://github.com/avaloniaui"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:ui="using:FluentAvalonia.UI.Controls"
|
||||||
|
xmlns:tagsInput="clr-namespace:Artemis.UI.Shared.TagsInput"
|
||||||
|
xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
|
||||||
|
x:CompileBindings="True">
|
||||||
|
<Design.PreviewWith>
|
||||||
|
<Border Padding="30" Width="400">
|
||||||
|
<StackPanel Spacing="20">
|
||||||
|
<tagsInput:TagsInput Name="TagsInput"/>
|
||||||
|
<ItemsControl ItemsSource="{CompiledBinding Path=Tags, ElementName=TagsInput}"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
</Design.PreviewWith>
|
||||||
|
|
||||||
|
<ControlTheme x:Key="{x:Type tagsInput:TagsInput}" TargetType="tagsInput:TagsInput">
|
||||||
|
<Setter Property="Template">
|
||||||
|
<ControlTemplate>
|
||||||
|
<StackPanel>
|
||||||
|
<TextBox Watermark="Enter tags" Name="PART_TagInputBox" MaxLines="1" MaxLength="{TemplateBinding MaxLength}">
|
||||||
|
<TextBox.InnerLeftContent>
|
||||||
|
<avalonia:MaterialIcon Kind="Tags" Margin="8 0 -2 0"></avalonia:MaterialIcon>
|
||||||
|
</TextBox.InnerLeftContent>
|
||||||
|
</TextBox>
|
||||||
|
|
||||||
|
<ItemsControl ItemsSource="{CompiledBinding Tags, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type tagsInput:TagsInput}}}">
|
||||||
|
<ItemsControl.ItemsPanel>
|
||||||
|
<ItemsPanelTemplate>
|
||||||
|
<WrapPanel/>
|
||||||
|
</ItemsPanelTemplate>
|
||||||
|
</ItemsControl.ItemsPanel>
|
||||||
|
<ItemsControl.DataTemplates>
|
||||||
|
<DataTemplate DataType="x:String">
|
||||||
|
<Button Margin="0 5 5 0"
|
||||||
|
Command="{CompiledBinding RemoveTag, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type tagsInput:TagsInput}}}"
|
||||||
|
CommandParameter="{CompiledBinding}">
|
||||||
|
<StackPanel Orientation="Horizontal" Spacing="5">
|
||||||
|
<avalonia:MaterialIcon Kind="Close" Margin="-5 0 0 0" Foreground="Gray" />
|
||||||
|
<TextBlock Text="{CompiledBinding}"></TextBlock>
|
||||||
|
</StackPanel>
|
||||||
|
</Button>
|
||||||
|
</DataTemplate>
|
||||||
|
</ItemsControl.DataTemplates>
|
||||||
|
</ItemsControl>
|
||||||
|
</StackPanel>
|
||||||
|
</ControlTemplate>
|
||||||
|
</Setter>
|
||||||
|
</ControlTheme>
|
||||||
|
</ResourceDictionary>
|
||||||
@ -3,12 +3,13 @@ using System.IO;
|
|||||||
using System.Net.Http;
|
using System.Net.Http;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using Artemis.UI.Shared.Utilities;
|
||||||
|
|
||||||
namespace Artemis.UI.Extensions
|
namespace Artemis.UI.Shared.Extensions
|
||||||
{
|
{
|
||||||
public static class HttpClientProgressExtensions
|
public static class HttpClientProgressExtensions
|
||||||
{
|
{
|
||||||
public static async Task DownloadDataAsync(this HttpClient client, string requestUrl, Stream destination, IProgress<float>? progress, CancellationToken cancellationToken)
|
public static async Task DownloadDataAsync(this HttpClient client, string requestUrl, Stream destination, IProgress<StreamProgress>? progress, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
using HttpResponseMessage response = await client.GetAsync(requestUrl, HttpCompletionOption.ResponseHeadersRead, cancellationToken);
|
using HttpResponseMessage response = await client.GetAsync(requestUrl, HttpCompletionOption.ResponseHeadersRead, cancellationToken);
|
||||||
response.EnsureSuccessStatusCode();
|
response.EnsureSuccessStatusCode();
|
||||||
@ -23,13 +24,10 @@ namespace Artemis.UI.Extensions
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Such progress and contentLength much reporting Wow!
|
// Such progress and contentLength much reporting Wow!
|
||||||
Progress<long> progressWrapper = new(totalBytes => progress.Report(GetProgressPercentage(totalBytes, contentLength.Value)));
|
await download.CopyToAsync(destination, 81920, progress, contentLength, cancellationToken);
|
||||||
await download.CopyToAsync(destination, 81920, progressWrapper, cancellationToken);
|
|
||||||
|
|
||||||
float GetProgressPercentage(float totalBytes, float currentBytes) => (totalBytes / currentBytes) * 100f;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static async Task CopyToAsync(this Stream source, Stream destination, int bufferSize, IProgress<long> progress, CancellationToken cancellationToken)
|
static async Task CopyToAsync(this Stream source, Stream destination, int bufferSize, IProgress<StreamProgress> progress, long? contentLength, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
if (bufferSize < 0)
|
if (bufferSize < 0)
|
||||||
throw new ArgumentOutOfRangeException(nameof(bufferSize));
|
throw new ArgumentOutOfRangeException(nameof(bufferSize));
|
||||||
@ -49,7 +47,7 @@ namespace Artemis.UI.Extensions
|
|||||||
{
|
{
|
||||||
await destination.WriteAsync(buffer, 0, bytesRead, cancellationToken).ConfigureAwait(false);
|
await destination.WriteAsync(buffer, 0, bytesRead, cancellationToken).ConfigureAwait(false);
|
||||||
totalBytesRead += bytesRead;
|
totalBytesRead += bytesRead;
|
||||||
progress?.Report(totalBytesRead);
|
progress?.Report(new StreamProgress(totalBytesRead, contentLength ?? totalBytesRead));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -0,0 +1,13 @@
|
|||||||
|
namespace Artemis.UI.Shared.Routing;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// For internal use.
|
||||||
|
/// </summary>
|
||||||
|
/// <seealso cref="RoutableHostScreen{TScreen}" />
|
||||||
|
/// <seealso cref="RoutableHostScreen{TScreen,TParam}" />
|
||||||
|
internal interface IRoutableHostScreen : IRoutableScreen
|
||||||
|
{
|
||||||
|
bool RecycleScreen { get; }
|
||||||
|
IRoutableScreen? InternalScreen { get; }
|
||||||
|
void InternalChangeScreen(IRoutableScreen? screen);
|
||||||
|
}
|
||||||
14
src/Artemis.UI.Shared/Routing/Routable/IRoutableScreen.cs
Normal file
14
src/Artemis.UI.Shared/Routing/Routable/IRoutableScreen.cs
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using ReactiveUI;
|
||||||
|
|
||||||
|
namespace Artemis.UI.Shared.Routing;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// For internal use.
|
||||||
|
/// </summary>
|
||||||
|
internal interface IRoutableScreen : IActivatableViewModel
|
||||||
|
{
|
||||||
|
Task InternalOnNavigating(NavigationArguments args, CancellationToken cancellationToken);
|
||||||
|
Task InternalOnClosing(NavigationArguments args);
|
||||||
|
}
|
||||||
@ -0,0 +1,42 @@
|
|||||||
|
namespace Artemis.UI.Shared.Routing;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a view model to which routing can take place and which in turn can host another view model.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="TScreen">The type of view model the screen can host.</typeparam>
|
||||||
|
public abstract class RoutableHostScreen<TScreen> : RoutableScreen, IRoutableHostScreen where TScreen : RoutableScreen
|
||||||
|
{
|
||||||
|
private bool _recycleScreen = true;
|
||||||
|
private TScreen? _screen;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the currently active child screen.
|
||||||
|
/// </summary>
|
||||||
|
public TScreen? Screen
|
||||||
|
{
|
||||||
|
get => _screen;
|
||||||
|
private set => RaiseAndSetIfChanged(ref _screen, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public bool RecycleScreen
|
||||||
|
{
|
||||||
|
get => _recycleScreen;
|
||||||
|
protected set => RaiseAndSetIfChanged(ref _recycleScreen, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
IRoutableScreen? IRoutableHostScreen.InternalScreen => Screen;
|
||||||
|
|
||||||
|
void IRoutableHostScreen.InternalChangeScreen(IRoutableScreen? screen)
|
||||||
|
{
|
||||||
|
if (screen == null)
|
||||||
|
{
|
||||||
|
Screen = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (screen is not TScreen typedScreen)
|
||||||
|
throw new ArtemisRoutingException($"Screen cannot be hosted, {screen.GetType().Name} is not assignable to {typeof(TScreen).Name}.");
|
||||||
|
Screen = typedScreen;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -0,0 +1,44 @@
|
|||||||
|
namespace Artemis.UI.Shared.Routing;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Represents a view model to which routing with parameters can take place and which in turn can host another view
|
||||||
|
/// model.
|
||||||
|
/// </summary>
|
||||||
|
/// <typeparam name="TScreen">The type of view model the screen can host.</typeparam>
|
||||||
|
/// <typeparam name="TParam">The type of parameters the screen expects. It must have a parameterless constructor.</typeparam>
|
||||||
|
public abstract class RoutableHostScreen<TScreen, TParam> : RoutableScreen<TParam>, IRoutableHostScreen where TScreen : RoutableScreen where TParam : new()
|
||||||
|
{
|
||||||
|
private bool _recycleScreen = true;
|
||||||
|
private TScreen? _screen;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the currently active child screen.
|
||||||
|
/// </summary>
|
||||||
|
public TScreen? Screen
|
||||||
|
{
|
||||||
|
get => _screen;
|
||||||
|
private set => RaiseAndSetIfChanged(ref _screen, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public bool RecycleScreen
|
||||||
|
{
|
||||||
|
get => _recycleScreen;
|
||||||
|
protected set => RaiseAndSetIfChanged(ref _recycleScreen, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
IRoutableScreen? IRoutableHostScreen.InternalScreen => Screen;
|
||||||
|
|
||||||
|
void IRoutableHostScreen.InternalChangeScreen(IRoutableScreen? screen)
|
||||||
|
{
|
||||||
|
if (screen == null)
|
||||||
|
{
|
||||||
|
Screen = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (screen is not TScreen typedScreen)
|
||||||
|
throw new ArtemisRoutingException($"Screen cannot be hosted, {screen.GetType().Name} is not assignable to {typeof(TScreen).Name}.");
|
||||||
|
Screen = typedScreen;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,24 +1,55 @@
|
|||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using ReactiveUI;
|
|
||||||
|
|
||||||
namespace Artemis.UI.Shared.Routing;
|
namespace Artemis.UI.Shared.Routing;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// For internal use.
|
/// Represents a view model to which routing can take place.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <seealso cref="RoutableScreen{TScreen}"/>
|
public abstract class RoutableScreen : ActivatableViewModelBase, IRoutableScreen
|
||||||
/// <seealso cref="RoutableScreen{TScreen, TParam}"/>
|
|
||||||
internal interface IRoutableScreen : IActivatableViewModel
|
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets a value indicating whether or not to reuse the child screen instance if the type has not changed.
|
/// Called before navigating to this screen.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <remarks>Defaults to <see langword="true"/>.</remarks>
|
/// <param name="args">Navigation arguments containing information about the navigation action.</param>
|
||||||
bool RecycleScreen { get; }
|
public virtual Task BeforeNavigating(NavigationArguments args)
|
||||||
|
{
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
object? InternalScreen { get; }
|
/// <summary>
|
||||||
void InternalChangeScreen(object? screen);
|
/// Called while navigating to this screen.
|
||||||
Task InternalOnNavigating(NavigationArguments args, CancellationToken cancellationToken);
|
/// </summary>
|
||||||
Task InternalOnClosing(NavigationArguments args);
|
/// <param name="args">Navigation arguments containing information about the navigation action.</param>
|
||||||
|
/// <param name="cancellationToken">
|
||||||
|
/// A cancellation token that can be used by other objects or threads to receive notice of
|
||||||
|
/// cancellation.
|
||||||
|
/// </param>
|
||||||
|
public virtual Task OnNavigating(NavigationArguments args, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Called before navigating away from this screen.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="args">Navigation arguments containing information about the navigation action.</param>
|
||||||
|
public virtual Task OnClosing(NavigationArguments args)
|
||||||
|
{
|
||||||
|
return Task.CompletedTask;
|
||||||
|
}
|
||||||
|
|
||||||
|
#region Overrides of RoutableScreen
|
||||||
|
|
||||||
|
async Task IRoutableScreen.InternalOnNavigating(NavigationArguments args, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
await OnNavigating(args, cancellationToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
async Task IRoutableScreen.InternalOnClosing(NavigationArguments args)
|
||||||
|
{
|
||||||
|
await OnClosing(args);
|
||||||
|
}
|
||||||
|
|
||||||
|
#endregion
|
||||||
}
|
}
|
||||||
@ -4,39 +4,15 @@ using System.Linq;
|
|||||||
using System.Linq.Expressions;
|
using System.Linq.Expressions;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using Avalonia.Platform;
|
|
||||||
|
|
||||||
namespace Artemis.UI.Shared.Routing;
|
namespace Artemis.UI.Shared.Routing;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Represents a view model to which routing with parameters can take place and which in turn can host another view
|
/// Represents a view model to which routing with parameters can take place.
|
||||||
/// model.
|
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <typeparam name="TScreen">The type of view model the screen can host.</typeparam>
|
|
||||||
/// <typeparam name="TParam">The type of parameters the screen expects. It must have a parameterless constructor.</typeparam>
|
/// <typeparam name="TParam">The type of parameters the screen expects. It must have a parameterless constructor.</typeparam>
|
||||||
public abstract class RoutableScreen<TScreen, TParam> : ActivatableViewModelBase, IRoutableScreen where TScreen : class where TParam : new()
|
public abstract class RoutableScreen<TParam> : RoutableScreen, IRoutableScreen where TParam : new()
|
||||||
{
|
{
|
||||||
private bool _recycleScreen = true;
|
|
||||||
private TScreen? _screen;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the currently active child screen.
|
|
||||||
/// </summary>
|
|
||||||
public TScreen? Screen
|
|
||||||
{
|
|
||||||
get => _screen;
|
|
||||||
private set => RaiseAndSetIfChanged(ref _screen, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Called before navigating to this screen.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="args">Navigation arguments containing information about the navigation action.</param>
|
|
||||||
public virtual Task BeforeNavigating(NavigationArguments args)
|
|
||||||
{
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Called while navigating to this screen.
|
/// Called while navigating to this screen.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@ -51,39 +27,6 @@ public abstract class RoutableScreen<TScreen, TParam> : ActivatableViewModelBase
|
|||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Called before navigating away from this screen.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="args">Navigation arguments containing information about the navigation action.</param>
|
|
||||||
public virtual Task OnClosing(NavigationArguments args)
|
|
||||||
{
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public bool RecycleScreen
|
|
||||||
{
|
|
||||||
get => _recycleScreen;
|
|
||||||
protected set => RaiseAndSetIfChanged(ref _recycleScreen, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
#region Overrides of RoutableScreen
|
|
||||||
|
|
||||||
object? IRoutableScreen.InternalScreen => Screen;
|
|
||||||
|
|
||||||
void IRoutableScreen.InternalChangeScreen(object? screen)
|
|
||||||
{
|
|
||||||
if (screen == null)
|
|
||||||
{
|
|
||||||
Screen = null;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (screen is not TScreen typedScreen)
|
|
||||||
throw new ArtemisRoutingException($"Provided screen is not assignable to {typeof(TScreen).FullName}");
|
|
||||||
Screen = typedScreen;
|
|
||||||
}
|
|
||||||
|
|
||||||
async Task IRoutableScreen.InternalOnNavigating(NavigationArguments args, CancellationToken cancellationToken)
|
async Task IRoutableScreen.InternalOnNavigating(NavigationArguments args, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
Func<object[], TParam> activator = GetParameterActivator();
|
Func<object[], TParam> activator = GetParameterActivator();
|
||||||
@ -92,6 +35,7 @@ public abstract class RoutableScreen<TScreen, TParam> : ActivatableViewModelBase
|
|||||||
throw new ArtemisRoutingException($"Did not retrieve the required amount of parameters, expects {_parameterPropertyCount}, got {args.SegmentParameters.Length}.");
|
throw new ArtemisRoutingException($"Did not retrieve the required amount of parameters, expects {_parameterPropertyCount}, got {args.SegmentParameters.Length}.");
|
||||||
|
|
||||||
TParam parameters = activator(args.SegmentParameters);
|
TParam parameters = activator(args.SegmentParameters);
|
||||||
|
await OnNavigating(args, cancellationToken);
|
||||||
await OnNavigating(parameters, args, cancellationToken);
|
await OnNavigating(parameters, args, cancellationToken);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -100,8 +44,6 @@ public abstract class RoutableScreen<TScreen, TParam> : ActivatableViewModelBase
|
|||||||
await OnClosing(args);
|
await OnClosing(args);
|
||||||
}
|
}
|
||||||
|
|
||||||
#endregion
|
|
||||||
|
|
||||||
#region Parameter generation
|
#region Parameter generation
|
||||||
|
|
||||||
// ReSharper disable once StaticMemberInGenericType - That's intentional, each kind of TParam should have its own property count
|
// ReSharper disable once StaticMemberInGenericType - That's intentional, each kind of TParam should have its own property count
|
||||||
@ -1,87 +0,0 @@
|
|||||||
using System.Threading;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace Artemis.UI.Shared.Routing;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Represents a view model to which routing can take place and which in turn can host another view model.
|
|
||||||
/// </summary>
|
|
||||||
/// <typeparam name="TScreen">The type of view model the screen can host.</typeparam>
|
|
||||||
public abstract class RoutableScreen<TScreen> : ActivatableViewModelBase, IRoutableScreen where TScreen : class
|
|
||||||
{
|
|
||||||
private TScreen? _screen;
|
|
||||||
private bool _recycleScreen = true;
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Gets the currently active child screen.
|
|
||||||
/// </summary>
|
|
||||||
public TScreen? Screen
|
|
||||||
{
|
|
||||||
get => _screen;
|
|
||||||
private set => RaiseAndSetIfChanged(ref _screen, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <inheritdoc />
|
|
||||||
public bool RecycleScreen
|
|
||||||
{
|
|
||||||
get => _recycleScreen;
|
|
||||||
protected set => RaiseAndSetIfChanged(ref _recycleScreen, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Called before navigating to this screen.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="args">Navigation arguments containing information about the navigation action.</param>
|
|
||||||
public virtual Task BeforeNavigating(NavigationArguments args)
|
|
||||||
{
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Called while navigating to this screen.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="args">Navigation arguments containing information about the navigation action.</param>
|
|
||||||
/// <param name="cancellationToken">A cancellation token that can be used by other objects or threads to receive notice of cancellation.</param>
|
|
||||||
public virtual Task OnNavigating(NavigationArguments args, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Called before navigating away from this screen.
|
|
||||||
/// </summary>
|
|
||||||
/// <param name="args">Navigation arguments containing information about the navigation action.</param>
|
|
||||||
public virtual Task OnClosing(NavigationArguments args)
|
|
||||||
{
|
|
||||||
return Task.CompletedTask;
|
|
||||||
}
|
|
||||||
|
|
||||||
#region Overrides of RoutableScreen
|
|
||||||
|
|
||||||
object? IRoutableScreen.InternalScreen => Screen;
|
|
||||||
|
|
||||||
void IRoutableScreen.InternalChangeScreen(object? screen)
|
|
||||||
{
|
|
||||||
if (screen == null)
|
|
||||||
{
|
|
||||||
Screen = null;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (screen is not TScreen typedScreen)
|
|
||||||
throw new ArtemisRoutingException($"Screen cannot be hosted, {screen.GetType().Name} is not assignable to {typeof(TScreen).Name}.");
|
|
||||||
Screen = typedScreen;
|
|
||||||
}
|
|
||||||
|
|
||||||
async Task IRoutableScreen.InternalOnNavigating(NavigationArguments args, CancellationToken cancellationToken)
|
|
||||||
{
|
|
||||||
await OnNavigating(args, cancellationToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
async Task IRoutableScreen.InternalOnClosing(NavigationArguments args)
|
|
||||||
{
|
|
||||||
await OnClosing(args);
|
|
||||||
}
|
|
||||||
|
|
||||||
#endregion
|
|
||||||
}
|
|
||||||
@ -0,0 +1,17 @@
|
|||||||
|
|
||||||
|
namespace Artemis.UI.Shared.Routing.ParameterParsers;
|
||||||
|
|
||||||
|
internal class LongParameterParser : IRouteParameterParser
|
||||||
|
{
|
||||||
|
/// <inheritdoc />
|
||||||
|
public bool IsMatch(RouteSegment segment, string source)
|
||||||
|
{
|
||||||
|
return long.TryParse(source, out _);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public object GetValue(RouteSegment segment, string source)
|
||||||
|
{
|
||||||
|
return long.Parse(source);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -7,7 +7,7 @@ namespace Artemis.UI.Shared.Routing;
|
|||||||
/// Represents a registration for a route and its associated view model.
|
/// Represents a registration for a route and its associated view model.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
/// <typeparam name="TViewModel">The type of the view model associated with the route.</typeparam>
|
/// <typeparam name="TViewModel">The type of the view model associated with the route.</typeparam>
|
||||||
public class RouteRegistration<TViewModel> : IRouterRegistration where TViewModel : ViewModelBase
|
public class RouteRegistration<TViewModel> : IRouterRegistration where TViewModel : RoutableScreen
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Initializes a new instance of the <see cref="RouteRegistration{TViewModel}" /> class.
|
/// Initializes a new instance of the <see cref="RouteRegistration{TViewModel}" /> class.
|
||||||
|
|||||||
@ -74,19 +74,12 @@ internal class RouteResolution
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public object GetViewModel(IContainer container)
|
public RoutableScreen GetViewModel(IContainer container)
|
||||||
{
|
{
|
||||||
if (ViewModel == null)
|
return GetViewModel<RoutableScreen>(container);
|
||||||
throw new ArtemisRoutingException("Cannot get a view model of a non-success route resolution");
|
|
||||||
|
|
||||||
object? viewModel = container.Resolve(ViewModel);
|
|
||||||
if (viewModel == null)
|
|
||||||
throw new ArtemisRoutingException($"Could not resolve view model of type {ViewModel}");
|
|
||||||
|
|
||||||
return viewModel;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public T GetViewModel<T>(IContainer container)
|
public T GetViewModel<T>(IContainer container) where T : RoutableScreen
|
||||||
{
|
{
|
||||||
if (ViewModel == null)
|
if (ViewModel == null)
|
||||||
throw new ArtemisRoutingException("Cannot get a view model of a non-success route resolution");
|
throw new ArtemisRoutingException("Cannot get a view model of a non-success route resolution");
|
||||||
|
|||||||
@ -79,6 +79,7 @@ public partial class RouteSegment
|
|||||||
return parameterType switch
|
return parameterType switch
|
||||||
{
|
{
|
||||||
"guid" => new GuidParameterParser(),
|
"guid" => new GuidParameterParser(),
|
||||||
|
"long" => new LongParameterParser(),
|
||||||
"int" => new IntParameterParser(),
|
"int" => new IntParameterParser(),
|
||||||
_ => new StringParameterParser()
|
_ => new StringParameterParser()
|
||||||
};
|
};
|
||||||
|
|||||||
@ -50,7 +50,7 @@ public interface IRouter
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <param name="root">The root screen to set.</param>
|
/// <param name="root">The root screen to set.</param>
|
||||||
/// <typeparam name="TScreen">The type of the root screen. It must be a class.</typeparam>
|
/// <typeparam name="TScreen">The type of the root screen. It must be a class.</typeparam>
|
||||||
void SetRoot<TScreen>(RoutableScreen<TScreen> root) where TScreen : class;
|
void SetRoot<TScreen>(RoutableHostScreen<TScreen> root) where TScreen : RoutableScreen;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Sets the root screen from which navigation takes place.
|
/// Sets the root screen from which navigation takes place.
|
||||||
@ -58,7 +58,7 @@ public interface IRouter
|
|||||||
/// <param name="root">The root screen to set.</param>
|
/// <param name="root">The root screen to set.</param>
|
||||||
/// <typeparam name="TScreen">The type of the root screen. It must be a class.</typeparam>
|
/// <typeparam name="TScreen">The type of the root screen. It must be a class.</typeparam>
|
||||||
/// <typeparam name="TParam">The type of the parameters for the root screen. It must have a parameterless constructor.</typeparam>
|
/// <typeparam name="TParam">The type of the parameters for the root screen. It must have a parameterless constructor.</typeparam>
|
||||||
void SetRoot<TScreen, TParam>(RoutableScreen<TScreen, TParam> root) where TScreen : class where TParam : new();
|
void SetRoot<TScreen, TParam>(RoutableHostScreen<TScreen, TParam> root) where TScreen : RoutableScreen where TParam : new();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Clears the route used by the previous window, so that it is not restored when the main window opens.
|
/// Clears the route used by the previous window, so that it is not restored when the main window opens.
|
||||||
|
|||||||
@ -14,12 +14,12 @@ internal class Navigation
|
|||||||
private readonly IContainer _container;
|
private readonly IContainer _container;
|
||||||
private readonly ILogger _logger;
|
private readonly ILogger _logger;
|
||||||
|
|
||||||
private readonly IRoutableScreen _root;
|
private readonly IRoutableHostScreen _root;
|
||||||
private readonly RouteResolution _resolution;
|
private readonly RouteResolution _resolution;
|
||||||
private readonly RouterNavigationOptions _options;
|
private readonly RouterNavigationOptions _options;
|
||||||
private CancellationTokenSource _cts;
|
private CancellationTokenSource _cts;
|
||||||
|
|
||||||
public Navigation(IContainer container, ILogger logger, IRoutableScreen root, RouteResolution resolution, RouterNavigationOptions options)
|
public Navigation(IContainer container, ILogger logger, IRoutableHostScreen root, RouteResolution resolution, RouterNavigationOptions options)
|
||||||
{
|
{
|
||||||
_container = container;
|
_container = container;
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
@ -54,21 +54,21 @@ internal class Navigation
|
|||||||
_cts.Cancel();
|
_cts.Cancel();
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task NavigateResolution(RouteResolution resolution, NavigationArguments args, IRoutableScreen host)
|
private async Task NavigateResolution(RouteResolution resolution, NavigationArguments args, IRoutableHostScreen host)
|
||||||
{
|
{
|
||||||
if (Cancelled)
|
if (Cancelled)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
// Reuse the screen if its type has not changed, if a new one must be created, don't do so on the UI thread
|
// Reuse the screen if its type has not changed, if a new one must be created, don't do so on the UI thread
|
||||||
object screen;
|
IRoutableScreen screen;
|
||||||
if (_options.RecycleScreens && host.RecycleScreen && host.InternalScreen != null && host.InternalScreen.GetType() == resolution.ViewModel)
|
if (_options.RecycleScreens && host.RecycleScreen && host.InternalScreen != null && host.InternalScreen.GetType() == resolution.ViewModel)
|
||||||
screen = host.InternalScreen;
|
screen = host.InternalScreen;
|
||||||
else
|
else
|
||||||
screen = await Task.Run(() => resolution.GetViewModel(_container));
|
screen = await Task.Run(() => resolution.GetViewModel(_container));
|
||||||
|
|
||||||
// If resolution has a child, ensure the screen can host it
|
// If resolution has a child, ensure the screen can host it
|
||||||
if (resolution.Child != null && screen is not IRoutableScreen)
|
if (resolution.Child != null && screen is not IRoutableHostScreen)
|
||||||
throw new ArtemisRoutingException($"Route resolved with a child but view model of type {resolution.ViewModel} is does mot implement {nameof(IRoutableScreen)}.");
|
throw new ArtemisRoutingException($"Route resolved with a child but view model of type {resolution.ViewModel} is does mot implement {nameof(IRoutableHostScreen)}.");
|
||||||
|
|
||||||
// Only change the screen if it wasn't reused
|
// Only change the screen if it wasn't reused
|
||||||
if (!ReferenceEquals(host.InternalScreen, screen))
|
if (!ReferenceEquals(host.InternalScreen, screen))
|
||||||
@ -88,13 +88,11 @@ internal class Navigation
|
|||||||
if (CancelIfRequested(args, "ChangeScreen", screen))
|
if (CancelIfRequested(args, "ChangeScreen", screen))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
// If the screen implements some form of Navigable, activate it
|
// Navigate on the screen
|
||||||
args.SegmentParameters = resolution.Parameters ?? Array.Empty<object>();
|
args.SegmentParameters = resolution.Parameters ?? Array.Empty<object>();
|
||||||
if (screen is IRoutableScreen routableScreen)
|
|
||||||
{
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await routableScreen.InternalOnNavigating(args, _cts.Token);
|
await screen.InternalOnNavigating(args, _cts.Token);
|
||||||
}
|
}
|
||||||
catch (Exception e)
|
catch (Exception e)
|
||||||
{
|
{
|
||||||
@ -105,9 +103,8 @@ internal class Navigation
|
|||||||
|
|
||||||
if (CancelIfRequested(args, "OnNavigating", screen))
|
if (CancelIfRequested(args, "OnNavigating", screen))
|
||||||
return;
|
return;
|
||||||
}
|
|
||||||
|
|
||||||
if (screen is IRoutableScreen childScreen)
|
if (screen is IRoutableHostScreen childScreen)
|
||||||
{
|
{
|
||||||
// Navigate the child too
|
// Navigate the child too
|
||||||
if (resolution.Child != null)
|
if (resolution.Child != null)
|
||||||
@ -121,11 +118,9 @@ internal class Navigation
|
|||||||
Completed = true;
|
Completed = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool PathEquals(string path, bool allowPartialMatch)
|
public bool PathEquals(string path, RouterNavigationOptions options)
|
||||||
{
|
{
|
||||||
if (allowPartialMatch)
|
return options.PathEquals(_resolution.Path, path);
|
||||||
return _resolution.Path.StartsWith(path, StringComparison.InvariantCultureIgnoreCase);
|
|
||||||
return string.Equals(_resolution.Path, path, StringComparison.InvariantCultureIgnoreCase);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool CancelIfRequested(NavigationArguments args, string stage, object screen)
|
private bool CancelIfRequested(NavigationArguments args, string stage, object screen)
|
||||||
|
|||||||
@ -14,15 +14,15 @@ internal class Router : CorePropertyChanged, IRouter, IDisposable
|
|||||||
private readonly Stack<string> _backStack = new();
|
private readonly Stack<string> _backStack = new();
|
||||||
private readonly BehaviorSubject<string?> _currentRouteSubject;
|
private readonly BehaviorSubject<string?> _currentRouteSubject;
|
||||||
private readonly Stack<string> _forwardStack = new();
|
private readonly Stack<string> _forwardStack = new();
|
||||||
private readonly Func<IRoutableScreen, RouteResolution, RouterNavigationOptions, Navigation> _getNavigation;
|
private readonly Func<IRoutableHostScreen, RouteResolution, RouterNavigationOptions, Navigation> _getNavigation;
|
||||||
private readonly ILogger _logger;
|
private readonly ILogger _logger;
|
||||||
private readonly IMainWindowService _mainWindowService;
|
private readonly IMainWindowService _mainWindowService;
|
||||||
private Navigation? _currentNavigation;
|
private Navigation? _currentNavigation;
|
||||||
|
|
||||||
private IRoutableScreen? _root;
|
private IRoutableHostScreen? _root;
|
||||||
private string? _previousWindowRoute;
|
private string? _previousWindowRoute;
|
||||||
|
|
||||||
public Router(ILogger logger, IMainWindowService mainWindowService, Func<IRoutableScreen, RouteResolution, RouterNavigationOptions, Navigation> getNavigation)
|
public Router(ILogger logger, IMainWindowService mainWindowService, Func<IRoutableHostScreen, RouteResolution, RouterNavigationOptions, Navigation> getNavigation)
|
||||||
{
|
{
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_mainWindowService = mainWindowService;
|
_mainWindowService = mainWindowService;
|
||||||
@ -45,28 +45,22 @@ internal class Router : CorePropertyChanged, IRouter, IDisposable
|
|||||||
return RouteResolution.AsFailure(path);
|
return RouteResolution.AsFailure(path);
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task<bool> RequestClose(object screen, NavigationArguments args)
|
private async Task<bool> RequestClose(IRoutableScreen screen, NavigationArguments args)
|
||||||
{
|
{
|
||||||
if (screen is not IRoutableScreen routableScreen)
|
// Drill down to child screens first
|
||||||
return true;
|
if (screen is IRoutableHostScreen hostScreen && hostScreen.InternalScreen != null && !await RequestClose(hostScreen.InternalScreen, args))
|
||||||
|
return false;
|
||||||
|
|
||||||
await routableScreen.InternalOnClosing(args);
|
await screen.InternalOnClosing(args);
|
||||||
if (args.Cancelled)
|
if (!args.Cancelled)
|
||||||
{
|
return true;
|
||||||
_logger.Debug("Navigation to {Path} cancelled during RequestClose by {Screen}", args.Path, screen.GetType().Name);
|
_logger.Debug("Navigation to {Path} cancelled during RequestClose by {Screen}", args.Path, screen.GetType().Name);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (routableScreen.InternalScreen == null)
|
private bool PathEquals(string path, RouterNavigationOptions options)
|
||||||
return true;
|
|
||||||
return await RequestClose(routableScreen.InternalScreen, args);
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool PathEquals(string path, bool allowPartialMatch)
|
|
||||||
{
|
{
|
||||||
if (allowPartialMatch)
|
return _currentRouteSubject.Value != null && options.PathEquals(_currentRouteSubject.Value, path);
|
||||||
return _currentRouteSubject.Value != null && _currentRouteSubject.Value.StartsWith(path, StringComparison.InvariantCultureIgnoreCase);
|
|
||||||
return string.Equals(_currentRouteSubject.Value, path, StringComparison.InvariantCultureIgnoreCase);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
@ -88,7 +82,7 @@ internal class Router : CorePropertyChanged, IRouter, IDisposable
|
|||||||
{
|
{
|
||||||
if (_root == null)
|
if (_root == null)
|
||||||
throw new ArtemisRoutingException("Cannot navigate without a root having been set");
|
throw new ArtemisRoutingException("Cannot navigate without a root having been set");
|
||||||
if (PathEquals(path, options.IgnoreOnPartialMatch) || (_currentNavigation != null && _currentNavigation.PathEquals(path, options.IgnoreOnPartialMatch)))
|
if (PathEquals(path, options) || (_currentNavigation != null && _currentNavigation.PathEquals(path, options)))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
string? previousPath = _currentRouteSubject.Value;
|
string? previousPath = _currentRouteSubject.Value;
|
||||||
@ -161,13 +155,13 @@ internal class Router : CorePropertyChanged, IRouter, IDisposable
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public void SetRoot<TScreen>(RoutableScreen<TScreen> root) where TScreen : class
|
public void SetRoot<TScreen>(RoutableHostScreen<TScreen> root) where TScreen : RoutableScreen
|
||||||
{
|
{
|
||||||
_root = root;
|
_root = root;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public void SetRoot<TScreen, TParam>(RoutableScreen<TScreen, TParam> root) where TScreen : class where TParam : new()
|
public void SetRoot<TScreen, TParam>(RoutableHostScreen<TScreen, TParam> root) where TScreen : RoutableScreen where TParam : new()
|
||||||
{
|
{
|
||||||
_root = root;
|
_root = root;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,3 +1,5 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
namespace Artemis.UI.Shared.Routing;
|
namespace Artemis.UI.Shared.Routing;
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -21,9 +23,31 @@ public class RouterNavigationOptions
|
|||||||
/// <example>If set to true, a route change from <c>page/subpage1/subpage2</c> to <c>page/subpage1</c> will be ignored.</example>
|
/// <example>If set to true, a route change from <c>page/subpage1/subpage2</c> to <c>page/subpage1</c> will be ignored.</example>
|
||||||
public bool IgnoreOnPartialMatch { get; set; } = false;
|
public bool IgnoreOnPartialMatch { get; set; } = false;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets or sets the path to use when determining whether the path is a partial match,
|
||||||
|
/// only has any effect if <see cref="IgnoreOnPartialMatch"/> is <see langword="true"/>.
|
||||||
|
/// </summary>
|
||||||
|
public string? PartialMatchOverride { get; set; }
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets or sets a boolean value indicating whether logging should be enabled.
|
/// Gets or sets a boolean value indicating whether logging should be enabled.
|
||||||
/// <remarks>Errors and warnings are always logged.</remarks>
|
/// <remarks>Errors and warnings are always logged.</remarks>
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public bool EnableLogging { get; set; } = true;
|
public bool EnableLogging { get; set; } = true;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Determines whether the given two paths are considered equal using these navigation options.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="current">The current path.</param>
|
||||||
|
/// <param name="target">The target path.</param>
|
||||||
|
/// <returns><see langword="true"/> if the paths are considered equal; otherwise <see langword="false"/>.</returns>
|
||||||
|
internal bool PathEquals(string current, string target)
|
||||||
|
{
|
||||||
|
if (PartialMatchOverride != null && IgnoreOnPartialMatch)
|
||||||
|
target = PartialMatchOverride;
|
||||||
|
|
||||||
|
if (IgnoreOnPartialMatch)
|
||||||
|
return current.StartsWith(target, StringComparison.InvariantCultureIgnoreCase);
|
||||||
|
return string.Equals(current, target, StringComparison.InvariantCultureIgnoreCase);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@ -2,11 +2,11 @@
|
|||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using System.Windows.Input;
|
using System.Windows.Input;
|
||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Controls.Primitives;
|
||||||
using Avalonia.Layout;
|
using Avalonia.Layout;
|
||||||
using Avalonia.Threading;
|
using Avalonia.Threading;
|
||||||
using FluentAvalonia.UI.Controls;
|
using FluentAvalonia.UI.Controls;
|
||||||
using ReactiveUI;
|
using ReactiveUI;
|
||||||
using Button = Avalonia.Controls.Button;
|
|
||||||
|
|
||||||
namespace Artemis.UI.Shared.Services.Builders;
|
namespace Artemis.UI.Shared.Services.Builders;
|
||||||
|
|
||||||
@ -117,34 +117,34 @@ public class NotificationBuilder
|
|||||||
/// <exception cref="ArtemisSharedUIException" />
|
/// <exception cref="ArtemisSharedUIException" />
|
||||||
public Action Show()
|
public Action Show()
|
||||||
{
|
{
|
||||||
Panel? panel = _parent.Find<Panel>("NotificationContainer");
|
|
||||||
if (panel == null)
|
|
||||||
throw new ArtemisSharedUIException("Can't display a notification on a window without a NotificationContainer.");
|
|
||||||
|
|
||||||
Dispatcher.UIThread.Post(() =>
|
Dispatcher.UIThread.Post(() =>
|
||||||
{
|
{
|
||||||
panel.Children.Add(_infoBar);
|
OverlayLayer? overlayLayer = OverlayLayer.GetOverlayLayer(_parent);
|
||||||
|
if (overlayLayer == null)
|
||||||
|
throw new ArtemisSharedUIException("Can't display a notification on a window an overlay layer.");
|
||||||
|
|
||||||
|
NotificationHost container = new() {Content = _infoBar};
|
||||||
|
overlayLayer.Children.Add(container);
|
||||||
_infoBar.Closed += InfoBarOnClosed;
|
_infoBar.Closed += InfoBarOnClosed;
|
||||||
_infoBar.IsOpen = true;
|
_infoBar.IsOpen = true;
|
||||||
});
|
|
||||||
|
|
||||||
Task.Run(async () =>
|
Dispatcher.UIThread.InvokeAsync(async () =>
|
||||||
{
|
{
|
||||||
await Task.Delay(_timeout);
|
await Task.Delay(_timeout);
|
||||||
Dispatcher.UIThread.Post(() => _infoBar.IsOpen = false);
|
_infoBar.IsOpen = false;
|
||||||
|
});
|
||||||
|
|
||||||
|
return;
|
||||||
|
|
||||||
|
void InfoBarOnClosed(InfoBar sender, InfoBarClosedEventArgs args)
|
||||||
|
{
|
||||||
|
overlayLayer.Children.Remove(container);
|
||||||
|
_infoBar.Closed -= InfoBarOnClosed;
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return () => Dispatcher.UIThread.Post(() => _infoBar.IsOpen = false);
|
return () => Dispatcher.UIThread.Post(() => _infoBar.IsOpen = false);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void InfoBarOnClosed(InfoBar sender, InfoBarClosedEventArgs args)
|
|
||||||
{
|
|
||||||
_infoBar.Closed -= InfoBarOnClosed;
|
|
||||||
if (_parent.Content is not Panel panel)
|
|
||||||
return;
|
|
||||||
|
|
||||||
panel.Children.Remove(_infoBar);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@ -222,9 +222,13 @@ public class NotificationButtonBuilder
|
|||||||
button.Classes.Add("AppBarButton");
|
button.Classes.Add("AppBarButton");
|
||||||
|
|
||||||
if (_action != null)
|
if (_action != null)
|
||||||
|
{
|
||||||
button.Command = ReactiveCommand.Create(() => _action());
|
button.Command = ReactiveCommand.Create(() => _action());
|
||||||
|
}
|
||||||
else if (_asyncAction != null)
|
else if (_asyncAction != null)
|
||||||
|
{
|
||||||
button.Command = ReactiveCommand.CreateFromTask(() => _asyncAction());
|
button.Command = ReactiveCommand.CreateFromTask(() => _asyncAction());
|
||||||
|
}
|
||||||
else if (_command != null)
|
else if (_command != null)
|
||||||
{
|
{
|
||||||
button.Command = _command;
|
button.Command = _command;
|
||||||
|
|||||||
@ -16,7 +16,7 @@ public interface IWindowService : IArtemisSharedUIService
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
/// <typeparam name="TViewModel">The type of view model to create</typeparam>
|
/// <typeparam name="TViewModel">The type of view model to create</typeparam>
|
||||||
/// <returns>The created view model</returns>
|
/// <returns>The created view model</returns>
|
||||||
TViewModel ShowWindow<TViewModel>(params object[] parameters);
|
Window ShowWindow<TViewModel>(out TViewModel viewModel, params object[] parameters);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Given a ViewModel, show its corresponding View as a window
|
/// Given a ViewModel, show its corresponding View as a window
|
||||||
|
|||||||
@ -10,7 +10,6 @@ using Artemis.Core;
|
|||||||
using Artemis.Core.Services;
|
using Artemis.Core.Services;
|
||||||
using Artemis.UI.Shared.Services.MainWindow;
|
using Artemis.UI.Shared.Services.MainWindow;
|
||||||
using Artemis.UI.Shared.Services.ProfileEditor.Commands;
|
using Artemis.UI.Shared.Services.ProfileEditor.Commands;
|
||||||
using Avalonia.Threading;
|
|
||||||
using DynamicData;
|
using DynamicData;
|
||||||
using Serilog;
|
using Serilog;
|
||||||
|
|
||||||
@ -140,14 +139,14 @@ internal class ProfileEditorService : IProfileEditorService
|
|||||||
private void ApplyFocusMode()
|
private void ApplyFocusMode()
|
||||||
{
|
{
|
||||||
if (_suspendedEditingSubject.Value)
|
if (_suspendedEditingSubject.Value)
|
||||||
_profileService.EditorFocus = null;
|
_profileService.FocusProfileElement = null;
|
||||||
|
|
||||||
_profileService.EditorFocus = _focusModeSubject.Value switch
|
_profileService.FocusProfileElement = _focusModeSubject.Value switch
|
||||||
{
|
{
|
||||||
ProfileEditorFocusMode.None => null,
|
ProfileEditorFocusMode.None => null,
|
||||||
ProfileEditorFocusMode.Folder => _profileElementSubject.Value?.Parent,
|
ProfileEditorFocusMode.Folder => _profileElementSubject.Value?.Parent,
|
||||||
ProfileEditorFocusMode.Selection => _profileElementSubject.Value,
|
ProfileEditorFocusMode.Selection => _profileElementSubject.Value,
|
||||||
_ => _profileService.EditorFocus
|
_ => _profileService.FocusProfileElement
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -164,52 +163,38 @@ internal class ProfileEditorService : IProfileEditorService
|
|||||||
|
|
||||||
public async Task ChangeCurrentProfileConfiguration(ProfileConfiguration? profileConfiguration)
|
public async Task ChangeCurrentProfileConfiguration(ProfileConfiguration? profileConfiguration)
|
||||||
{
|
{
|
||||||
if (ReferenceEquals(_profileConfigurationSubject.Value, profileConfiguration))
|
ProfileConfiguration? previous = _profileConfigurationSubject.Value;
|
||||||
|
if (ReferenceEquals(previous, profileConfiguration))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
_logger.Verbose("ChangeCurrentProfileConfiguration {profile}", profileConfiguration);
|
_logger.Verbose("ChangeCurrentProfileConfiguration {profile}", profileConfiguration);
|
||||||
|
|
||||||
// Stop playing and save the current profile
|
// Stop playing and save the current profile
|
||||||
Pause();
|
Pause();
|
||||||
if (_profileConfigurationSubject.Value?.Profile != null)
|
|
||||||
{
|
|
||||||
_profileConfigurationSubject.Value.Profile.Reset();
|
|
||||||
_profileConfigurationSubject.Value.Profile.LastSelectedProfileElement = _profileElementSubject.Value;
|
|
||||||
}
|
|
||||||
|
|
||||||
await SaveProfileAsync();
|
await SaveProfileAsync();
|
||||||
|
|
||||||
// No need to deactivate the profile, if needed it will be deactivated next update
|
|
||||||
if (_profileConfigurationSubject.Value != null)
|
|
||||||
_profileConfigurationSubject.Value.IsBeingEdited = false;
|
|
||||||
|
|
||||||
// Deselect whatever profile element was active
|
// Deselect whatever profile element was active
|
||||||
ChangeCurrentProfileElement(null);
|
ChangeCurrentProfileElement(null);
|
||||||
|
ChangeSuspendedEditing(false);
|
||||||
|
|
||||||
// Close the command scope if one was open
|
// Close the command scope if one was open
|
||||||
_profileEditorHistoryScope?.Dispose();
|
_profileEditorHistoryScope?.Dispose();
|
||||||
|
|
||||||
// The new profile may need activation
|
// Activate the profile and it's mode off of the UI thread
|
||||||
if (profileConfiguration != null)
|
|
||||||
{
|
|
||||||
await Task.Run(() =>
|
await Task.Run(() =>
|
||||||
{
|
{
|
||||||
profileConfiguration.IsBeingEdited = true;
|
// Activate the profile if one was provided
|
||||||
_moduleService.SetActivationOverride(profileConfiguration.Module);
|
if (profileConfiguration != null)
|
||||||
_profileService.ActivateProfile(profileConfiguration);
|
_profileService.ActivateProfile(profileConfiguration);
|
||||||
_profileService.RenderForEditor = true;
|
// If there is no profile configuration or module, deliberately set the override to null
|
||||||
|
_moduleService.SetActivationOverride(profileConfiguration?.Module);
|
||||||
});
|
});
|
||||||
if (profileConfiguration.Profile?.LastSelectedProfileElement is RenderProfileElement renderProfileElement)
|
|
||||||
ChangeCurrentProfileElement(renderProfileElement);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
_moduleService.SetActivationOverride(null);
|
|
||||||
_profileService.RenderForEditor = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
_profileService.FocusProfile = profileConfiguration;
|
||||||
_profileConfigurationSubject.OnNext(profileConfiguration);
|
_profileConfigurationSubject.OnNext(profileConfiguration);
|
||||||
|
|
||||||
ChangeTime(TimeSpan.Zero);
|
ChangeTime(TimeSpan.Zero);
|
||||||
|
previous?.Profile?.Reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void ChangeCurrentProfileElement(RenderProfileElement? renderProfileElement)
|
public void ChangeCurrentProfileElement(RenderProfileElement? renderProfileElement)
|
||||||
@ -238,20 +223,20 @@ internal class ProfileEditorService : IProfileEditorService
|
|||||||
if (_suspendedEditingSubject.Value == suspend)
|
if (_suspendedEditingSubject.Value == suspend)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
_suspendedEditingSubject.OnNext(suspend);
|
|
||||||
if (suspend)
|
if (suspend)
|
||||||
{
|
{
|
||||||
Pause();
|
Pause();
|
||||||
_profileService.RenderForEditor = false;
|
_profileService.UpdateFocusProfile = true;
|
||||||
_profileConfigurationSubject.Value?.Profile?.Reset();
|
_profileConfigurationSubject.Value?.Profile?.Reset();
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
if (_profileConfigurationSubject.Value != null)
|
if (_profileConfigurationSubject.Value != null)
|
||||||
_profileService.RenderForEditor = true;
|
_profileService.UpdateFocusProfile = false;
|
||||||
Tick(_timeSubject.Value);
|
Tick(_timeSubject.Value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_suspendedEditingSubject.OnNext(suspend);
|
||||||
ApplyFocusMode();
|
ApplyFocusMode();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -411,9 +396,7 @@ internal class ProfileEditorService : IProfileEditorService
|
|||||||
public void SaveProfile()
|
public void SaveProfile()
|
||||||
{
|
{
|
||||||
Profile? profile = _profileConfigurationSubject.Value?.Profile;
|
Profile? profile = _profileConfigurationSubject.Value?.Profile;
|
||||||
if (profile == null)
|
if (profile != null)
|
||||||
return;
|
|
||||||
|
|
||||||
_profileService.SaveProfile(profile, true);
|
_profileService.SaveProfile(profile, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -32,7 +32,7 @@
|
|||||||
<TextBox Text="{CompiledBinding Exception, Mode=OneTime}"
|
<TextBox Text="{CompiledBinding Exception, Mode=OneTime}"
|
||||||
AcceptsReturn="True"
|
AcceptsReturn="True"
|
||||||
IsReadOnly="True"
|
IsReadOnly="True"
|
||||||
FontFamily="Consolas"
|
FontFamily="{StaticResource RobotoMono}"
|
||||||
FontSize="12"
|
FontSize="12"
|
||||||
BorderThickness="0" />
|
BorderThickness="0" />
|
||||||
</ScrollViewer>
|
</ScrollViewer>
|
||||||
|
|||||||
@ -21,14 +21,13 @@ internal class WindowService : IWindowService
|
|||||||
_container = container;
|
_container = container;
|
||||||
}
|
}
|
||||||
|
|
||||||
public T ShowWindow<T>(params object[] parameters)
|
public Window ShowWindow<T>(out T viewModel, params object[] parameters)
|
||||||
{
|
{
|
||||||
T viewModel = _container.Resolve<T>(parameters);
|
viewModel = _container.Resolve<T>(parameters);
|
||||||
if (viewModel == null)
|
if (viewModel == null)
|
||||||
throw new ArtemisSharedUIException($"Failed to show window for VM of type {typeof(T).Name}, could not create instance.");
|
throw new ArtemisSharedUIException($"Failed to show window for VM of type {typeof(T).Name}, could not create instance.");
|
||||||
|
|
||||||
ShowWindow(viewModel);
|
return ShowWindow(viewModel);
|
||||||
return viewModel;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public Window ShowWindow(object viewModel)
|
public Window ShowWindow(object viewModel)
|
||||||
|
|||||||
@ -15,6 +15,7 @@
|
|||||||
|
|
||||||
<ResourceDictionary.MergedDictionaries>
|
<ResourceDictionary.MergedDictionaries>
|
||||||
<MergeResourceInclude Source="/Controls/Pagination/PaginationStyles.axaml" />
|
<MergeResourceInclude Source="/Controls/Pagination/PaginationStyles.axaml" />
|
||||||
|
<MergeResourceInclude Source="/Controls/TagsInput/TagsInputStyles.axaml" />
|
||||||
</ResourceDictionary.MergedDictionaries>
|
</ResourceDictionary.MergedDictionaries>
|
||||||
</ResourceDictionary>
|
</ResourceDictionary>
|
||||||
</Styles.Resources>
|
</Styles.Resources>
|
||||||
@ -31,6 +32,7 @@
|
|||||||
<!-- Custom styles -->
|
<!-- Custom styles -->
|
||||||
<StyleInclude Source="/Styles/Border.axaml" />
|
<StyleInclude Source="/Styles/Border.axaml" />
|
||||||
<StyleInclude Source="/Styles/BrokenState.axaml" />
|
<StyleInclude Source="/Styles/BrokenState.axaml" />
|
||||||
|
<StyleInclude Source="/Styles/Control.axaml" />
|
||||||
<StyleInclude Source="/Styles/Skeleton.axaml" />
|
<StyleInclude Source="/Styles/Skeleton.axaml" />
|
||||||
<StyleInclude Source="/Styles/Button.axaml" />
|
<StyleInclude Source="/Styles/Button.axaml" />
|
||||||
<StyleInclude Source="/Styles/Condensed.axaml" />
|
<StyleInclude Source="/Styles/Condensed.axaml" />
|
||||||
|
|||||||
@ -24,14 +24,10 @@
|
|||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Design.PreviewWith>
|
</Design.PreviewWith>
|
||||||
|
|
||||||
<Styles.Resources>
|
|
||||||
<CornerRadius x:Key="CardCornerRadius">8</CornerRadius>
|
|
||||||
</Styles.Resources>
|
|
||||||
|
|
||||||
<!-- Add Styles Here -->
|
<!-- Add Styles Here -->
|
||||||
<Style Selector="Border.router-container">
|
<Style Selector="Border.router-container">
|
||||||
<Setter Property="Background" Value="{DynamicResource ControlFillColorDefaultBrush}" />
|
<Setter Property="Background" Value="{DynamicResource NavigationViewContentBackground}" />
|
||||||
<Setter Property="BorderBrush" Value="{DynamicResource CardStrokeColorDefaultBrush}" />
|
<Setter Property="BorderBrush" Value="{DynamicResource NavigationViewContentGridBorderBrush}" />
|
||||||
<Setter Property="BorderThickness" Value="1" />
|
<Setter Property="BorderThickness" Value="1" />
|
||||||
<Setter Property="CornerRadius" Value="8 0 0 0" />
|
<Setter Property="CornerRadius" Value="8 0 0 0" />
|
||||||
<Setter Property="ClipToBounds" Value="True" />
|
<Setter Property="ClipToBounds" Value="True" />
|
||||||
@ -39,18 +35,18 @@
|
|||||||
|
|
||||||
<Style Selector="Border.card">
|
<Style Selector="Border.card">
|
||||||
<Setter Property="Padding" Value="16" />
|
<Setter Property="Padding" Value="16" />
|
||||||
<Setter Property="Background" Value="{DynamicResource ControlFillColorDefaultBrush}" />
|
<Setter Property="Background" Value="{DynamicResource NavigationViewContentBackground}" />
|
||||||
<Setter Property="BorderBrush" Value="{DynamicResource CardStrokeColorDefaultBrush}" />
|
<Setter Property="BorderBrush" Value="{DynamicResource NavigationViewContentGridBorderBrush}" />
|
||||||
<Setter Property="BorderThickness" Value="1" />
|
<Setter Property="BorderThickness" Value="1" />
|
||||||
<Setter Property="CornerRadius" Value="{DynamicResource CardCornerRadius}" />
|
<Setter Property="CornerRadius" Value="{DynamicResource ControlCornerRadius}" />
|
||||||
</Style>
|
</Style>
|
||||||
|
|
||||||
<Style Selector="Border.card-condensed">
|
<Style Selector="Border.card-condensed">
|
||||||
<Setter Property="Padding" Value="8" />
|
<Setter Property="Padding" Value="8" />
|
||||||
<Setter Property="Background" Value="{DynamicResource ControlFillColorDefaultBrush}" />
|
<Setter Property="Background" Value="{DynamicResource NavigationViewContentBackground}" />
|
||||||
<Setter Property="BorderBrush" Value="{DynamicResource CardStrokeColorDefaultBrush}" />
|
<Setter Property="BorderBrush" Value="{DynamicResource NavigationViewContentGridBorderBrush}" />
|
||||||
<Setter Property="BorderThickness" Value="1" />
|
<Setter Property="BorderThickness" Value="1" />
|
||||||
<Setter Property="CornerRadius" Value="{DynamicResource CardCornerRadius}" />
|
<Setter Property="CornerRadius" Value="{DynamicResource ControlCornerRadius}" />
|
||||||
</Style>
|
</Style>
|
||||||
|
|
||||||
<Style Selector="Border.card-separator">
|
<Style Selector="Border.card-separator">
|
||||||
|
|||||||
@ -40,14 +40,13 @@
|
|||||||
<avalonia:MaterialIcon Kind="WindowMinimize" />
|
<avalonia:MaterialIcon Kind="WindowMinimize" />
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<TextBlock Margin="0 5 0 0">ToggleButton.window-button</TextBlock>
|
|
||||||
<ToggleButton Classes="icon-button">
|
|
||||||
<avalonia:MaterialIcon Kind="BlockChain" />
|
|
||||||
</ToggleButton>
|
|
||||||
|
|
||||||
<Button Classes="icon-button">
|
<Button Classes="icon-button">
|
||||||
<avalonia:MaterialIcon Kind="Cog" />
|
<avalonia:MaterialIcon Kind="Cog" />
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
|
<Button Classes="danger">
|
||||||
|
Oohohoho daanger!
|
||||||
|
</Button>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Border>
|
</Border>
|
||||||
</Design.PreviewWith>
|
</Design.PreviewWith>
|
||||||
@ -110,27 +109,28 @@
|
|||||||
<Setter Property="Background" Value="Red"></Setter>
|
<Setter Property="Background" Value="Red"></Setter>
|
||||||
</Style>
|
</Style>
|
||||||
|
|
||||||
<Style Selector="ToggleButton:checked:pointerover /template/ Border#BorderElement">
|
<Style Selector="Button.danger">
|
||||||
<Setter Property="BorderBrush" Value="{DynamicResource ToggleButtonBorderBrushCheckedPointerOver}" />
|
<Setter Property="Background" Value="#D64848"></Setter>
|
||||||
</Style>
|
|
||||||
<Style Selector="ToggleButton:checked:pointerover /template/ ContentPresenter#PART_ContentPresenter">
|
|
||||||
<Setter Property="Background" Value="{DynamicResource ToggleButtonBackgroundCheckedPointerOver}" />
|
|
||||||
<Setter Property="TextBlock.Foreground" Value="{DynamicResource ToggleButtonForegroundCheckedPointerOver}" />
|
|
||||||
</Style>
|
</Style>
|
||||||
|
|
||||||
<Style Selector="ToggleButton:checked:pressed /template/ Border#BorderElement">
|
<Style Selector="Button.danger:pointerover">
|
||||||
<Setter Property="BorderBrush" Value="{DynamicResource ToggleButtonBorderBrushCheckedPressed}" />
|
<Style Selector="^ /template/ controls|FABorder#Root">
|
||||||
|
<Setter Property="Background" Value="#D65757"/>
|
||||||
|
<Setter Property="BorderBrush" Value="{DynamicResource ButtonBorderBrushPointerOver}" />
|
||||||
</Style>
|
</Style>
|
||||||
<Style Selector="ToggleButton:checked:pressed /template/ ContentPresenter#PART_ContentPresenter">
|
|
||||||
<Setter Property="Background" Value="{DynamicResource ToggleButtonBackgroundCheckedPressed}" />
|
|
||||||
<Setter Property="TextBlock.Foreground" Value="{DynamicResource ToggleButtonForegroundCheckedPressed}" />
|
|
||||||
</Style>
|
</Style>
|
||||||
|
|
||||||
<Style Selector="ToggleButton:checked:disabled /template/ Border#BorderElement">
|
<Style Selector="Button.danger:pressed">
|
||||||
<Setter Property="BorderBrush" Value="{DynamicResource ToggleButtonBorderBrushCheckedDisabled}" />
|
<Style Selector="^ /template/ controls|FABorder#Root">
|
||||||
|
<Setter Property="Background" Value="#D64848" />
|
||||||
|
<Setter Property="BorderBrush" Value="{DynamicResource ButtonBorderBrushPressed}" />
|
||||||
|
</Style>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style Selector="Button.danger:disabled">
|
||||||
|
<Style Selector="^ /template/ controls|FABorder#Root">
|
||||||
|
<Setter Property="Background" Value="#D79D9C" />
|
||||||
|
<Setter Property="BorderBrush" Value="{DynamicResource ButtonBorderBrushDisabled}" />
|
||||||
</Style>
|
</Style>
|
||||||
<Style Selector="ToggleButton:checked:disabled /template/ ContentPresenter#PART_ContentPresenter">
|
|
||||||
<Setter Property="Background" Value="{DynamicResource ToggleButtonBackgroundCheckedDisabled}" />
|
|
||||||
<Setter Property="TextBlock.Foreground" Value="{DynamicResource ToggleButtonForegroundCheckedDisabled}" />
|
|
||||||
</Style>
|
</Style>
|
||||||
</Styles>
|
</Styles>
|
||||||
25
src/Artemis.UI.Shared/Styles/Control.axaml
Normal file
25
src/Artemis.UI.Shared/Styles/Control.axaml
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
<Styles xmlns="https://github.com/avaloniaui"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||||
|
<Design.PreviewWith>
|
||||||
|
<Border Padding="20">
|
||||||
|
<!-- Add Controls for Previewer Here -->
|
||||||
|
</Border>
|
||||||
|
</Design.PreviewWith>
|
||||||
|
|
||||||
|
<!-- Add Styles Here -->
|
||||||
|
<Style Selector=":is(Control).fade-in">
|
||||||
|
<Setter Property="Opacity" Value="0" />
|
||||||
|
|
||||||
|
<Setter Property="Transitions">
|
||||||
|
<Setter.Value>
|
||||||
|
<Transitions>
|
||||||
|
<DoubleTransition Property="Opacity" Delay="0:0:0.2" Duration="0:0:0.2" Easing="CubicEaseOut"/>
|
||||||
|
</Transitions>
|
||||||
|
</Setter.Value>
|
||||||
|
</Setter>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style Selector=":is(Control).faded-in">
|
||||||
|
<Setter Property="Opacity" Value="1" />
|
||||||
|
</Style>
|
||||||
|
</Styles>
|
||||||
@ -66,7 +66,8 @@
|
|||||||
<TextBlock Grid.Column="0" Text="{CompiledBinding PropertyDescription.Name}" ToolTip.Tip="{CompiledBinding PropertyDescription.Description}" />
|
<TextBlock Grid.Column="0" Text="{CompiledBinding PropertyDescription.Name}" ToolTip.Tip="{CompiledBinding PropertyDescription.Description}" />
|
||||||
<TextBlock Grid.Column="1"
|
<TextBlock Grid.Column="1"
|
||||||
Text="{CompiledBinding DisplayValue}"
|
Text="{CompiledBinding DisplayValue}"
|
||||||
FontFamily="Consolas"
|
FontFamily="{StaticResource RobotoMono}"
|
||||||
|
FontSize="13"
|
||||||
HorizontalAlignment="Right"
|
HorizontalAlignment="Right"
|
||||||
Margin="0 0 10 0" />
|
Margin="0 0 10 0" />
|
||||||
</Grid>
|
</Grid>
|
||||||
@ -81,7 +82,7 @@
|
|||||||
IsVisible="{CompiledBinding IsEventPicker, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type dataModelPicker:DataModelPicker}}}"/>
|
IsVisible="{CompiledBinding IsEventPicker, RelativeSource={RelativeSource FindAncestor, AncestorType={x:Type dataModelPicker:DataModelPicker}}}"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
<ContentControl Grid.Column="1" Content="{CompiledBinding DisplayViewModel}" FontFamily="Consolas" Margin="0 0 10 0" />
|
<ContentControl Grid.Column="1" Content="{CompiledBinding DisplayViewModel}" FontFamily="{StaticResource RobotoMono}" FontSize="13" Margin="0 0 10 0" />
|
||||||
</Grid>
|
</Grid>
|
||||||
</TreeDataTemplate>
|
</TreeDataTemplate>
|
||||||
|
|
||||||
@ -90,7 +91,8 @@
|
|||||||
<TextBlock Grid.Column="0" Text="{CompiledBinding PropertyDescription.Name}" ToolTip.Tip="{CompiledBinding PropertyDescription.Description}" />
|
<TextBlock Grid.Column="0" Text="{CompiledBinding PropertyDescription.Name}" ToolTip.Tip="{CompiledBinding PropertyDescription.Description}" />
|
||||||
<TextBlock Grid.Column="1"
|
<TextBlock Grid.Column="1"
|
||||||
Text="{CompiledBinding CountDisplay, Mode=OneWay}"
|
Text="{CompiledBinding CountDisplay, Mode=OneWay}"
|
||||||
FontFamily="Consolas"
|
FontFamily="{StaticResource RobotoMono}"
|
||||||
|
FontSize="13"
|
||||||
HorizontalAlignment="Right"
|
HorizontalAlignment="Right"
|
||||||
Margin="0 0 10 0" />
|
Margin="0 0 10 0" />
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|||||||
@ -17,6 +17,8 @@
|
|||||||
<Setter Property="Opacity" Value="0" />
|
<Setter Property="Opacity" Value="0" />
|
||||||
<Setter Property="MaxWidth" Value="600" />
|
<Setter Property="MaxWidth" Value="600" />
|
||||||
<Setter Property="Margin" Value="15"/>
|
<Setter Property="Margin" Value="15"/>
|
||||||
|
<Setter Property="VerticalAlignment" Value="Bottom"/>
|
||||||
|
<Setter Property="HorizontalAlignment" Value="Right"/>
|
||||||
<Setter Property="Transitions">
|
<Setter Property="Transitions">
|
||||||
<Transitions>
|
<Transitions>
|
||||||
<DoubleTransition Property="Opacity" Duration="0:0:0.2"/>
|
<DoubleTransition Property="Opacity" Duration="0:0:0.2"/>
|
||||||
|
|||||||
@ -1,17 +1,14 @@
|
|||||||
<Styles xmlns="https://github.com/avaloniaui"
|
<Styles xmlns="https://github.com/avaloniaui"
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia">
|
xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
|
||||||
|
xmlns:shared="clr-namespace:Artemis.UI.Shared">
|
||||||
<Design.PreviewWith>
|
<Design.PreviewWith>
|
||||||
<Border Padding="20">
|
<Border Padding="20">
|
||||||
<!-- Add Controls for Previewer Here -->
|
<!-- Add Controls for Previewer Here -->
|
||||||
</Border>
|
</Border>
|
||||||
</Design.PreviewWith>
|
</Design.PreviewWith>
|
||||||
|
|
||||||
<Style Selector="StackPanel.notification-container">
|
<Style Selector="shared|NotificationHost controls|InfoBar">
|
||||||
<Setter Property="Spacing" Value="-25" />
|
|
||||||
</Style>
|
|
||||||
|
|
||||||
<Style Selector="StackPanel.notification-container controls|InfoBar">
|
|
||||||
<Setter Property="MaxHeight" Value="0" />
|
<Setter Property="MaxHeight" Value="0" />
|
||||||
<Style.Animations>
|
<Style.Animations>
|
||||||
<Animation Duration="0:0:0.2" Easing="CubicEaseOut" FillMode="Forward">
|
<Animation Duration="0:0:0.2" Easing="CubicEaseOut" FillMode="Forward">
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
<Styles xmlns="https://github.com/avaloniaui"
|
<Styles xmlns="https://github.com/avaloniaui"
|
||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
|
||||||
<Design.PreviewWith>
|
<Design.PreviewWith>
|
||||||
<Border Padding="20">
|
<Border Padding="20">
|
||||||
@ -10,15 +10,10 @@
|
|||||||
<TextBlock Classes="h5">This is heading 5</TextBlock>
|
<TextBlock Classes="h5">This is heading 5</TextBlock>
|
||||||
<TextBlock Classes="h6">This is heading 6</TextBlock>
|
<TextBlock Classes="h6">This is heading 6</TextBlock>
|
||||||
<TextBlock Classes="subtitle">This is a subtitle</TextBlock>
|
<TextBlock Classes="subtitle">This is a subtitle</TextBlock>
|
||||||
<TextBlock>
|
<TextBlock Classes="danger">Danger</TextBlock>
|
||||||
<Run Classes="h1">This is heading 1</Run>
|
<TextBlock Classes="warning">Warning</TextBlock>
|
||||||
<Run Classes="h2">This is heading 2</Run>
|
<TextBlock Classes="success">Success</TextBlock>
|
||||||
<Run Classes="h3">This is heading 3</Run>
|
<TextBlock Classes="info">Info</TextBlock>
|
||||||
<Run Classes="h4">This is heading 4</Run>
|
|
||||||
<Run Classes="h5">This is heading 5</Run>
|
|
||||||
<Run Classes="h6">This is heading 6</Run>
|
|
||||||
<Run Classes="subtitle">This is a subtitle</Run>
|
|
||||||
</TextBlock>
|
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Border>
|
</Border>
|
||||||
</Design.PreviewWith>
|
</Design.PreviewWith>
|
||||||
@ -82,6 +77,32 @@
|
|||||||
<Setter Property="Foreground" Value="{DynamicResource TextFillColorTertiaryBrush}" />
|
<Setter Property="Foreground" Value="{DynamicResource TextFillColorTertiaryBrush}" />
|
||||||
</Style>
|
</Style>
|
||||||
|
|
||||||
|
<Style Selector="TextBlock.danger">
|
||||||
|
<Setter Property="Foreground" Value="#FF5C5C"></Setter>
|
||||||
|
</Style>
|
||||||
|
<Style Selector="TextBlock.warning">
|
||||||
|
<Setter Property="Foreground" Value="#DAA520"></Setter>
|
||||||
|
</Style>
|
||||||
|
<Style Selector="TextBlock.success">
|
||||||
|
<Setter Property="Foreground" Value="#12B775"></Setter>
|
||||||
|
</Style>
|
||||||
|
<Style Selector="TextBlock.info">
|
||||||
|
<Setter Property="Foreground" Value="{DynamicResource AccentButtonBackground}"></Setter>
|
||||||
|
</Style>
|
||||||
|
|
||||||
|
<Style Selector="Run.danger">
|
||||||
|
<Setter Property="Foreground" Value="#FF5C5C"></Setter>
|
||||||
|
</Style>
|
||||||
|
<Style Selector="Run.warning">
|
||||||
|
<Setter Property="Foreground" Value="#DAA520"></Setter>
|
||||||
|
</Style>
|
||||||
|
<Style Selector="Run.success">
|
||||||
|
<Setter Property="Foreground" Value="#12B775"></Setter>
|
||||||
|
</Style>
|
||||||
|
<Style Selector="Run.info">
|
||||||
|
<Setter Property="Foreground" Value="{DynamicResource AccentButtonBackground}"></Setter>
|
||||||
|
</Style>
|
||||||
|
|
||||||
<Style Selector="SelectableTextBlock">
|
<Style Selector="SelectableTextBlock">
|
||||||
<Setter Property="SelectionBrush" Value="{DynamicResource TextControlSelectionHighlightColor}" />
|
<Setter Property="SelectionBrush" Value="{DynamicResource TextControlSelectionHighlightColor}" />
|
||||||
</Style>
|
</Style>
|
||||||
|
|||||||
119
src/Artemis.UI.Shared/Utilities/ProgressableStreamContent.cs
Normal file
119
src/Artemis.UI.Shared/Utilities/ProgressableStreamContent.cs
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
// Heavily based on:
|
||||||
|
// SkyClip
|
||||||
|
// - ProgressableStreamContent.cs
|
||||||
|
// --------------------------------------------------------------------
|
||||||
|
// Author: Jeff Hansen <jeff@jeffijoe.com>
|
||||||
|
// Copyright (C) Jeff Hansen 2015. All rights reserved.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Net;
|
||||||
|
using System.Net.Http;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Artemis.UI.Shared.Utilities;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Provides HTTP content based on a stream with support for IProgress.
|
||||||
|
/// </summary>
|
||||||
|
public class ProgressableStreamContent : StreamContent
|
||||||
|
{
|
||||||
|
private const int DEFAULT_BUFFER_SIZE = 4096;
|
||||||
|
|
||||||
|
private readonly int _bufferSize;
|
||||||
|
private readonly IProgress<StreamProgress> _progress;
|
||||||
|
private readonly Stream _streamToWrite;
|
||||||
|
private bool _contentConsumed;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="ProgressableStreamContent" /> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="streamToWrite">The stream to write.</param>
|
||||||
|
/// <param name="progress">The downloader.</param>
|
||||||
|
public ProgressableStreamContent(Stream streamToWrite, IProgress<StreamProgress> progress) : this(streamToWrite, DEFAULT_BUFFER_SIZE, progress)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="ProgressableStreamContent" /> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="streamToWrite">The stream to write.</param>
|
||||||
|
/// <param name="bufferSize">The buffer size.</param>
|
||||||
|
/// <param name="progress">The downloader.</param>
|
||||||
|
public ProgressableStreamContent(Stream streamToWrite, int bufferSize, IProgress<StreamProgress> progress) : base(streamToWrite, bufferSize)
|
||||||
|
{
|
||||||
|
if (bufferSize <= 0)
|
||||||
|
throw new ArgumentOutOfRangeException(nameof(bufferSize));
|
||||||
|
|
||||||
|
_streamToWrite = streamToWrite;
|
||||||
|
_bufferSize = bufferSize;
|
||||||
|
_progress = progress;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override void Dispose(bool disposing)
|
||||||
|
{
|
||||||
|
if (disposing)
|
||||||
|
_streamToWrite.Dispose();
|
||||||
|
|
||||||
|
base.Dispose(disposing);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override async Task SerializeToStreamAsync(Stream stream, TransportContext? context)
|
||||||
|
{
|
||||||
|
await SerializeToStreamAsync(stream, context, CancellationToken.None);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override async Task SerializeToStreamAsync(Stream stream, TransportContext? context, CancellationToken cancellationToken)
|
||||||
|
{
|
||||||
|
PrepareContent();
|
||||||
|
|
||||||
|
byte[] buffer = new byte[_bufferSize];
|
||||||
|
long size = _streamToWrite.Length;
|
||||||
|
int uploaded = 0;
|
||||||
|
|
||||||
|
await using (_streamToWrite)
|
||||||
|
{
|
||||||
|
while (!cancellationToken.IsCancellationRequested)
|
||||||
|
{
|
||||||
|
int length = await _streamToWrite.ReadAsync(buffer, cancellationToken);
|
||||||
|
if (length <= 0)
|
||||||
|
break;
|
||||||
|
|
||||||
|
uploaded += length;
|
||||||
|
_progress.Report(new StreamProgress(uploaded, size));
|
||||||
|
await stream.WriteAsync(buffer, 0, length, cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
protected override bool TryComputeLength(out long length)
|
||||||
|
{
|
||||||
|
length = _streamToWrite.Length;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Prepares the content.
|
||||||
|
/// </summary>
|
||||||
|
/// <exception cref="System.InvalidOperationException">The stream has already been read.</exception>
|
||||||
|
private void PrepareContent()
|
||||||
|
{
|
||||||
|
if (_contentConsumed)
|
||||||
|
{
|
||||||
|
// If the content needs to be written to a target stream a 2nd time, then the stream must support
|
||||||
|
// seeking (e.g. a FileStream), otherwise the stream can't be copied a second time to a target
|
||||||
|
// stream (e.g. a NetworkStream).
|
||||||
|
if (_streamToWrite.CanSeek)
|
||||||
|
_streamToWrite.Position = 0;
|
||||||
|
else
|
||||||
|
throw new InvalidOperationException("The stream has already been read.");
|
||||||
|
}
|
||||||
|
|
||||||
|
_contentConsumed = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
57
src/Artemis.UI.Shared/Utilities/StreamProgress.cs
Normal file
57
src/Artemis.UI.Shared/Utilities/StreamProgress.cs
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
// Heavily based on:
|
||||||
|
// SkyClip
|
||||||
|
// - UploadProgress.cs
|
||||||
|
// --------------------------------------------------------------------
|
||||||
|
// Author: Jeff Hansen <jeff@jeffijoe.com>
|
||||||
|
// Copyright (C) Jeff Hansen 2015. All rights reserved.
|
||||||
|
|
||||||
|
namespace Artemis.UI.Shared.Utilities;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The upload progress.
|
||||||
|
/// </summary>
|
||||||
|
public class StreamProgress
|
||||||
|
{
|
||||||
|
/// <summary>
|
||||||
|
/// Initializes a new instance of the <see cref="StreamProgress" /> class.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="bytesTransfered">
|
||||||
|
/// The bytes transfered.
|
||||||
|
/// </param>
|
||||||
|
/// <param name="totalBytes">
|
||||||
|
/// The total bytes.
|
||||||
|
/// </param>
|
||||||
|
public StreamProgress(long bytesTransfered, long? totalBytes)
|
||||||
|
{
|
||||||
|
BytesTransfered = bytesTransfered;
|
||||||
|
TotalBytes = totalBytes;
|
||||||
|
if (totalBytes.HasValue)
|
||||||
|
ProgressPercentage = (int) ((float) bytesTransfered / totalBytes.Value * 100);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Returns a <see cref="System.String" /> that represents this instance.
|
||||||
|
/// </summary>
|
||||||
|
/// <returns>
|
||||||
|
/// A <see cref="System.String" /> that represents this instance.
|
||||||
|
/// </returns>
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
return string.Format("{0}% ({1} / {2})", ProgressPercentage, BytesTransfered, TotalBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the bytes transfered.
|
||||||
|
/// </summary>
|
||||||
|
public long BytesTransfered { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the progress percentage.
|
||||||
|
/// </summary>
|
||||||
|
public int ProgressPercentage { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Gets the total bytes.
|
||||||
|
/// </summary>
|
||||||
|
public long? TotalBytes { get; }
|
||||||
|
}
|
||||||
@ -18,7 +18,7 @@
|
|||||||
<TrayIcon.Menu>
|
<TrayIcon.Menu>
|
||||||
<NativeMenu>
|
<NativeMenu>
|
||||||
<NativeMenuItem Header="Home" Command="{CompiledBinding OpenScreen}" CommandParameter="home" />
|
<NativeMenuItem Header="Home" Command="{CompiledBinding OpenScreen}" CommandParameter="home" />
|
||||||
<!-- <NativeMenuItem Header="Workshop" Command="{CompiledBinding OpenScreen}" CommandParameter="workshop" /> -->
|
<NativeMenuItem Header="Workshop" Command="{CompiledBinding OpenScreen}" CommandParameter="workshop" />
|
||||||
<NativeMenuItem Header="Surface Editor" Command="{CompiledBinding OpenScreen}" CommandParameter="surface-editor" />
|
<NativeMenuItem Header="Surface Editor" Command="{CompiledBinding OpenScreen}" CommandParameter="surface-editor" />
|
||||||
<NativeMenuItem Header="Settings" Command="{CompiledBinding OpenScreen}" CommandParameter="settings" />
|
<NativeMenuItem Header="Settings" Command="{CompiledBinding OpenScreen}" CommandParameter="settings" />
|
||||||
<NativeMenuItemSeparator />
|
<NativeMenuItemSeparator />
|
||||||
|
|||||||
@ -18,18 +18,24 @@
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<PackageReference Include="AsyncImageLoader.Avalonia" Version="3.2.0" />
|
||||||
<PackageReference Include="Avalonia" Version="$(AvaloniaVersion)" />
|
<PackageReference Include="Avalonia" Version="$(AvaloniaVersion)" />
|
||||||
|
<PackageReference Include="Avalonia.AvaloniaEdit" Version="11.0.1" />
|
||||||
<PackageReference Include="Avalonia.Controls.PanAndZoom" Version="11.0.0" />
|
<PackageReference Include="Avalonia.Controls.PanAndZoom" Version="11.0.0" />
|
||||||
<PackageReference Include="Avalonia.Desktop" Version="$(AvaloniaVersion)" />
|
<PackageReference Include="Avalonia.Desktop" Version="$(AvaloniaVersion)" />
|
||||||
<!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.-->
|
<!--Condition below is needed to remove Avalonia.Diagnostics package from build output in Release configuration.-->
|
||||||
<PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="$(AvaloniaVersion)" />
|
<PackageReference Condition="'$(Configuration)' == 'Debug'" Include="Avalonia.Diagnostics" Version="$(AvaloniaVersion)" />
|
||||||
|
<PackageReference Include="Avalonia.Controls.ItemsRepeater" Version="$(AvaloniaVersion)" />
|
||||||
<PackageReference Include="Avalonia.ReactiveUI" Version="$(AvaloniaVersion)" />
|
<PackageReference Include="Avalonia.ReactiveUI" Version="$(AvaloniaVersion)" />
|
||||||
<PackageReference Include="Avalonia.Xaml.Behaviors" Version="$(AvaloniaBehavioursVersion)" />
|
<PackageReference Include="Avalonia.Xaml.Behaviors" Version="$(AvaloniaBehavioursVersion)" />
|
||||||
|
<PackageReference Include="Avalonia.Skia.Lottie" Version="11.0.0" />
|
||||||
|
<PackageReference Include="AvaloniaEdit.TextMate" Version="11.0.1" />
|
||||||
<PackageReference Include="DryIoc.dll" Version="5.4.0" />
|
<PackageReference Include="DryIoc.dll" Version="5.4.0" />
|
||||||
<PackageReference Include="DynamicData" Version="7.13.1" />
|
<PackageReference Include="DynamicData" Version="7.13.1" />
|
||||||
<PackageReference Include="FluentAvaloniaUI" Version="$(FluentAvaloniaVersion)" />
|
<PackageReference Include="FluentAvaloniaUI" Version="$(FluentAvaloniaVersion)" />
|
||||||
<PackageReference Include="Flurl.Http" Version="3.2.4" />
|
<PackageReference Include="Flurl.Http" Version="3.2.4" />
|
||||||
<PackageReference Include="Markdown.Avalonia.Tight" Version="11.0.0" />
|
<PackageReference Include="Markdown.Avalonia.Svg" Version="11.0.1" />
|
||||||
|
<PackageReference Include="Markdown.Avalonia.Tight" Version="11.0.1" />
|
||||||
<PackageReference Include="Material.Icons.Avalonia" Version="2.0.1" />
|
<PackageReference Include="Material.Icons.Avalonia" Version="2.0.1" />
|
||||||
<PackageReference Include="Octopus.Octodiff" Version="2.0.261" />
|
<PackageReference Include="Octopus.Octodiff" Version="2.0.261" />
|
||||||
<PackageReference Include="ReactiveUI" Version="18.4.26" />
|
<PackageReference Include="ReactiveUI" Version="18.4.26" />
|
||||||
@ -38,9 +44,30 @@
|
|||||||
<PackageReference Include="RGB.NET.Layout" Version="$(RGBDotNetVersion)" />
|
<PackageReference Include="RGB.NET.Layout" Version="$(RGBDotNetVersion)" />
|
||||||
<PackageReference Include="SkiaSharp" Version="$(SkiaSharpVersion)" />
|
<PackageReference Include="SkiaSharp" Version="$(SkiaSharpVersion)" />
|
||||||
<PackageReference Include="Splat.DryIoc" Version="14.6.8" />
|
<PackageReference Include="Splat.DryIoc" Version="14.6.8" />
|
||||||
|
<PackageReference Include="System.Reactive" Version="5.0.0" />
|
||||||
|
<PackageReference Include="TextMateSharp.Grammars" Version="1.0.55" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<AvaloniaResource Include="Assets\**" />
|
<AvaloniaResource Include="Assets\**" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<Compile Update="Screens\Workshop\Entries\Tabs\ProfileListView.axaml.cs">
|
||||||
|
<DependentUpon>ProfileListView.axaml</DependentUpon>
|
||||||
|
<SubType>Code</SubType>
|
||||||
|
</Compile>
|
||||||
|
<Compile Update="Screens\Workshop\Entries\Tabs\LayoutListView.axaml.cs">
|
||||||
|
<DependentUpon>LayoutListView.axaml</DependentUpon>
|
||||||
|
<SubType>Code</SubType>
|
||||||
|
</Compile>
|
||||||
|
<Compile Update="Screens\Workshop\Library\SubmissionDetailView.axaml.cs">
|
||||||
|
<DependentUpon>SubmissionsDetailView.axaml</DependentUpon>
|
||||||
|
<SubType>Code</SubType>
|
||||||
|
</Compile>
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<UpToDateCheckInput Remove="Screens\Workshop\Entries\Windows\MarkdownPreviewView.axaml" />
|
||||||
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
1
src/Artemis.UI/Assets/Animations/email.json
Normal file
1
src/Artemis.UI/Assets/Animations/email.json
Normal file
File diff suppressed because one or more lines are too long
1
src/Artemis.UI/Assets/Animations/empty.json
Normal file
1
src/Artemis.UI/Assets/Animations/empty.json
Normal file
File diff suppressed because one or more lines are too long
1
src/Artemis.UI/Assets/Animations/login-pending.json
Normal file
1
src/Artemis.UI/Assets/Animations/login-pending.json
Normal file
File diff suppressed because one or more lines are too long
1
src/Artemis.UI/Assets/Animations/password.json
Normal file
1
src/Artemis.UI/Assets/Animations/password.json
Normal file
File diff suppressed because one or more lines are too long
1
src/Artemis.UI/Assets/Animations/success.json
Normal file
1
src/Artemis.UI/Assets/Animations/success.json
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
1
src/Artemis.UI/Assets/Animations/workshop-wizard.json
Normal file
1
src/Artemis.UI/Assets/Animations/workshop-wizard.json
Normal file
File diff suppressed because one or more lines are too long
BIN
src/Artemis.UI/Assets/Fonts/RobotoMono-Bold.ttf
Normal file
BIN
src/Artemis.UI/Assets/Fonts/RobotoMono-Bold.ttf
Normal file
Binary file not shown.
BIN
src/Artemis.UI/Assets/Fonts/RobotoMono-BoldItalic.ttf
Normal file
BIN
src/Artemis.UI/Assets/Fonts/RobotoMono-BoldItalic.ttf
Normal file
Binary file not shown.
BIN
src/Artemis.UI/Assets/Fonts/RobotoMono-Italic.ttf
Normal file
BIN
src/Artemis.UI/Assets/Fonts/RobotoMono-Italic.ttf
Normal file
Binary file not shown.
BIN
src/Artemis.UI/Assets/Fonts/RobotoMono-Regular.ttf
Normal file
BIN
src/Artemis.UI/Assets/Fonts/RobotoMono-Regular.ttf
Normal file
Binary file not shown.
BIN
src/Artemis.UI/Assets/Fonts/RobotoMono-SemiBold.ttf
Normal file
BIN
src/Artemis.UI/Assets/Fonts/RobotoMono-SemiBold.ttf
Normal file
Binary file not shown.
BIN
src/Artemis.UI/Assets/Fonts/RobotoMono-SemiBoldItalic.ttf
Normal file
BIN
src/Artemis.UI/Assets/Fonts/RobotoMono-SemiBoldItalic.ttf
Normal file
Binary file not shown.
34
src/Artemis.UI/Converters/DateTimeConverter.cs
Normal file
34
src/Artemis.UI/Converters/DateTimeConverter.cs
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
using System;
|
||||||
|
using System.Globalization;
|
||||||
|
using Artemis.WebClient.Workshop;
|
||||||
|
using Avalonia.Data.Converters;
|
||||||
|
using Humanizer;
|
||||||
|
|
||||||
|
namespace Artemis.UI.Converters;
|
||||||
|
|
||||||
|
public class DateTimeConverter : IValueConverter
|
||||||
|
{
|
||||||
|
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
|
||||||
|
{
|
||||||
|
if (value is DateTimeOffset dateTimeOffset)
|
||||||
|
{
|
||||||
|
return parameter?.ToString() == "humanize"
|
||||||
|
? dateTimeOffset.ToLocalTime().Humanize()
|
||||||
|
: dateTimeOffset.ToLocalTime().ToString(parameter?.ToString() ?? "g");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value is DateTime dateTime)
|
||||||
|
{
|
||||||
|
return parameter?.ToString() == "humanize"
|
||||||
|
? dateTime.ToLocalTime().Humanize()
|
||||||
|
: dateTime.ToLocalTime().ToString(parameter?.ToString() ?? "g");
|
||||||
|
}
|
||||||
|
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
|
||||||
|
{
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
19
src/Artemis.UI/Converters/EntryIconUriConverter.cs
Normal file
19
src/Artemis.UI/Converters/EntryIconUriConverter.cs
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
using System;
|
||||||
|
using System.Globalization;
|
||||||
|
using Artemis.WebClient.Workshop;
|
||||||
|
using Avalonia.Data.Converters;
|
||||||
|
|
||||||
|
namespace Artemis.UI.Converters;
|
||||||
|
|
||||||
|
public class EntryIconUriConverter : IValueConverter
|
||||||
|
{
|
||||||
|
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
|
||||||
|
{
|
||||||
|
return $"{WorkshopConstants.WORKSHOP_URL}/entries/{value}/icon";
|
||||||
|
}
|
||||||
|
|
||||||
|
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
|
||||||
|
{
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
}
|
||||||
48
src/Artemis.UI/Extensions/Bitmap.cs
Normal file
48
src/Artemis.UI/Extensions/Bitmap.cs
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Avalonia.Media.Imaging;
|
||||||
|
using SkiaSharp;
|
||||||
|
|
||||||
|
namespace Artemis.UI.Extensions;
|
||||||
|
|
||||||
|
public class BitmapExtensions
|
||||||
|
{
|
||||||
|
public static Bitmap LoadAndResize(string file, int size)
|
||||||
|
{
|
||||||
|
using SKBitmap source = SKBitmap.Decode(file);
|
||||||
|
return Resize(source, size);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static Bitmap LoadAndResize(Stream stream, int size)
|
||||||
|
{
|
||||||
|
stream.Seek(0, SeekOrigin.Begin);
|
||||||
|
using MemoryStream copy = new();
|
||||||
|
stream.CopyTo(copy);
|
||||||
|
copy.Seek(0, SeekOrigin.Begin);
|
||||||
|
using SKBitmap source = SKBitmap.Decode(copy);
|
||||||
|
return Resize(source, size);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Bitmap Resize(SKBitmap source, int size)
|
||||||
|
{
|
||||||
|
// Get smaller dimension.
|
||||||
|
int minDim = Math.Min(source.Width, source.Height);
|
||||||
|
|
||||||
|
// Calculate crop rectangle position for center crop.
|
||||||
|
int deltaX = (source.Width - minDim) / 2;
|
||||||
|
int deltaY = (source.Height - minDim) / 2;
|
||||||
|
|
||||||
|
// Create crop rectangle.
|
||||||
|
SKRectI rect = new(deltaX, deltaY, deltaX + minDim, deltaY + minDim);
|
||||||
|
|
||||||
|
// Do the actual cropping of the bitmap.
|
||||||
|
using SKBitmap croppedBitmap = new(minDim, minDim);
|
||||||
|
source.ExtractSubset(croppedBitmap, rect);
|
||||||
|
|
||||||
|
// Resize to the desired size after cropping.
|
||||||
|
using SKBitmap resizedBitmap = croppedBitmap.Resize(new SKImageInfo(size, size), SKFilterQuality.High);
|
||||||
|
|
||||||
|
return new Bitmap(resizedBitmap.Encode(SKEncodedImageFormat.Png, 100).AsStream());
|
||||||
|
}
|
||||||
|
}
|
||||||
15
src/Artemis.UI/Extensions/IActivatableViewModelExtensions.cs
Normal file
15
src/Artemis.UI/Extensions/IActivatableViewModelExtensions.cs
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
using System;
|
||||||
|
using System.Reactive.Disposables;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Avalonia.Threading;
|
||||||
|
using ReactiveUI;
|
||||||
|
|
||||||
|
namespace Artemis.UI.Extensions;
|
||||||
|
|
||||||
|
public static class ActivatableViewModelExtensions
|
||||||
|
{
|
||||||
|
public static void WhenActivatedAsync(this IActivatableViewModel item, Func<CompositeDisposable, Task> block)
|
||||||
|
{
|
||||||
|
item.WhenActivated(d => Dispatcher.UIThread.InvokeAsync(async () => await block(d)));
|
||||||
|
}
|
||||||
|
}
|
||||||
35
src/Artemis.UI/Extensions/MaterialIconKindExtensions.cs
Normal file
35
src/Artemis.UI/Extensions/MaterialIconKindExtensions.cs
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
using System;
|
||||||
|
using System.IO;
|
||||||
|
using Material.Icons;
|
||||||
|
using SkiaSharp;
|
||||||
|
|
||||||
|
namespace Artemis.UI.Extensions;
|
||||||
|
|
||||||
|
public static class MaterialIconKindExtensions
|
||||||
|
{
|
||||||
|
public static Stream EncodeToBitmap(this MaterialIconKind icon, int size, int margin, SKColor color)
|
||||||
|
{
|
||||||
|
string geometrySource = MaterialIconDataProvider.GetData(icon);
|
||||||
|
|
||||||
|
SKBitmap bitmap = new(size, size);
|
||||||
|
using (SKCanvas canvas = new(bitmap))
|
||||||
|
{
|
||||||
|
canvas.Clear(SKColors.Transparent);
|
||||||
|
|
||||||
|
// Parse and render the geometry data using SkiaSharp's SKPath
|
||||||
|
using SKPath path = SKPath.ParseSvgPathData(geometrySource);
|
||||||
|
using SKPaint paint = new() {Color = color, IsAntialias = true,};
|
||||||
|
|
||||||
|
// Calculate scaling and translation to fit the icon in the 100x100 area with 14 pixels margin
|
||||||
|
float scale = Math.Min(size / path.Bounds.Width, size / path.Bounds.Height);
|
||||||
|
path.Transform(SKMatrix.CreateTranslation(path.Bounds.Left * -1, path.Bounds.Top * -1));
|
||||||
|
path.Transform(SKMatrix.CreateScale(scale, scale));
|
||||||
|
canvas.Scale((size - margin * 2) / (float) size, (size - margin * 2) / (float) size, size / 2f, size / 2f);
|
||||||
|
canvas.DrawPath(path, paint);
|
||||||
|
}
|
||||||
|
|
||||||
|
MemoryStream stream = new();
|
||||||
|
bitmap.Encode(stream, SKEncodedImageFormat.Png, 100);
|
||||||
|
return stream;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -2,6 +2,7 @@ using System;
|
|||||||
using System.IO;
|
using System.IO;
|
||||||
using System.IO.Compression;
|
using System.IO.Compression;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
|
using Artemis.UI.Shared.Utilities;
|
||||||
|
|
||||||
namespace Artemis.UI.Extensions;
|
namespace Artemis.UI.Extensions;
|
||||||
|
|
||||||
@ -16,7 +17,7 @@ public static class ZipArchiveExtensions
|
|||||||
/// <param name="overwriteFiles">A boolean indicating whether to override existing files</param>
|
/// <param name="overwriteFiles">A boolean indicating whether to override existing files</param>
|
||||||
/// <param name="progress">The progress to report to.</param>
|
/// <param name="progress">The progress to report to.</param>
|
||||||
/// <param name="cancellationToken">A cancellation token</param>
|
/// <param name="cancellationToken">A cancellation token</param>
|
||||||
public static void ExtractToDirectory(this ZipArchive source, string destinationDirectoryName, bool overwriteFiles, IProgress<float> progress, CancellationToken cancellationToken)
|
public static void ExtractToDirectory(this ZipArchive source, string destinationDirectoryName, bool overwriteFiles, IProgress<StreamProgress> progress, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
if (source == null)
|
if (source == null)
|
||||||
throw new ArgumentNullException(nameof(source));
|
throw new ArgumentNullException(nameof(source));
|
||||||
@ -28,7 +29,7 @@ public static class ZipArchiveExtensions
|
|||||||
{
|
{
|
||||||
ZipArchiveEntry entry = source.Entries[index];
|
ZipArchiveEntry entry = source.Entries[index];
|
||||||
entry.ExtractRelativeToDirectory(destinationDirectoryName, overwriteFiles);
|
entry.ExtractRelativeToDirectory(destinationDirectoryName, overwriteFiles);
|
||||||
progress.Report((index + 1f) / source.Entries.Count * 100f);
|
progress.Report(new StreamProgress(index + 1, source.Entries.Count));
|
||||||
cancellationToken.ThrowIfCancellationRequested();
|
cancellationToken.ThrowIfCancellationRequested();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -12,6 +12,9 @@
|
|||||||
MinWidth="600"
|
MinWidth="600"
|
||||||
MinHeight="400"
|
MinHeight="400"
|
||||||
PointerReleased="InputElement_OnPointerReleased">
|
PointerReleased="InputElement_OnPointerReleased">
|
||||||
|
<windowing:AppWindow.Resources>
|
||||||
|
|
||||||
|
</windowing:AppWindow.Resources>
|
||||||
<windowing:AppWindow.Styles>
|
<windowing:AppWindow.Styles>
|
||||||
<Styles>
|
<Styles>
|
||||||
<Style Selector="Border#TitleBarContainer">
|
<Style Selector="Border#TitleBarContainer">
|
||||||
@ -36,6 +39,5 @@
|
|||||||
</Border>
|
</Border>
|
||||||
<ContentControl Content="{CompiledBinding}" />
|
<ContentControl Content="{CompiledBinding}" />
|
||||||
</DockPanel>
|
</DockPanel>
|
||||||
<StackPanel Classes="notification-container" Name="NotificationContainer" VerticalAlignment="Bottom" HorizontalAlignment="Right"/>
|
|
||||||
</Panel>
|
</Panel>
|
||||||
</windowing:AppWindow>
|
</windowing:AppWindow>
|
||||||
28
src/Artemis.UI/Routing/RouteViewModel.cs
Normal file
28
src/Artemis.UI/Routing/RouteViewModel.cs
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
using System;
|
||||||
|
|
||||||
|
namespace Artemis.UI.Routing;
|
||||||
|
|
||||||
|
public class RouteViewModel
|
||||||
|
{
|
||||||
|
public RouteViewModel(string name, string path, string? matchPath = null)
|
||||||
|
{
|
||||||
|
Path = path;
|
||||||
|
Name = name;
|
||||||
|
MatchPath = matchPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Path { get; }
|
||||||
|
public string Name { get; }
|
||||||
|
public string? MatchPath { get; }
|
||||||
|
|
||||||
|
public bool Matches(string path)
|
||||||
|
{
|
||||||
|
return path.StartsWith(MatchPath ?? Path, StringComparison.InvariantCultureIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <inheritdoc />
|
||||||
|
public override string ToString()
|
||||||
|
{
|
||||||
|
return Name;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -6,7 +6,12 @@ using Artemis.UI.Screens.Settings;
|
|||||||
using Artemis.UI.Screens.Settings.Updating;
|
using Artemis.UI.Screens.Settings.Updating;
|
||||||
using Artemis.UI.Screens.SurfaceEditor;
|
using Artemis.UI.Screens.SurfaceEditor;
|
||||||
using Artemis.UI.Screens.Workshop;
|
using Artemis.UI.Screens.Workshop;
|
||||||
|
using Artemis.UI.Screens.Workshop.Entries;
|
||||||
|
using Artemis.UI.Screens.Workshop.Entries.Tabs;
|
||||||
|
using Artemis.UI.Screens.Workshop.Home;
|
||||||
using Artemis.UI.Screens.Workshop.Layout;
|
using Artemis.UI.Screens.Workshop.Layout;
|
||||||
|
using Artemis.UI.Screens.Workshop.Library;
|
||||||
|
using Artemis.UI.Screens.Workshop.Library.Tabs;
|
||||||
using Artemis.UI.Screens.Workshop.Profile;
|
using Artemis.UI.Screens.Workshop.Profile;
|
||||||
using Artemis.UI.Shared.Routing;
|
using Artemis.UI.Shared.Routing;
|
||||||
|
|
||||||
@ -18,18 +23,34 @@ public static class Routes
|
|||||||
{
|
{
|
||||||
new RouteRegistration<BlankViewModel>("blank"),
|
new RouteRegistration<BlankViewModel>("blank"),
|
||||||
new RouteRegistration<HomeViewModel>("home"),
|
new RouteRegistration<HomeViewModel>("home"),
|
||||||
#if DEBUG
|
|
||||||
new RouteRegistration<WorkshopViewModel>("workshop")
|
new RouteRegistration<WorkshopViewModel>("workshop")
|
||||||
{
|
{
|
||||||
Children = new List<IRouterRegistration>()
|
Children = new List<IRouterRegistration>
|
||||||
|
{
|
||||||
|
new RouteRegistration<WorkshopOfflineViewModel>("offline/{message:string}"),
|
||||||
|
new RouteRegistration<EntriesViewModel>("entries")
|
||||||
|
{
|
||||||
|
Children = new List<IRouterRegistration>
|
||||||
{
|
{
|
||||||
new RouteRegistration<ProfileListViewModel>("profiles/{page:int}"),
|
new RouteRegistration<ProfileListViewModel>("profiles/{page:int}"),
|
||||||
new RouteRegistration<ProfileDetailsViewModel>("profiles/{entryId:guid}"),
|
new RouteRegistration<ProfileDetailsViewModel>("profiles/details/{entryId:long}"),
|
||||||
|
#if DEBUG
|
||||||
new RouteRegistration<LayoutListViewModel>("layouts/{page:int}"),
|
new RouteRegistration<LayoutListViewModel>("layouts/{page:int}"),
|
||||||
new RouteRegistration<LayoutDetailsViewModel>("layouts/{entryId:guid}")
|
new RouteRegistration<LayoutDetailsViewModel>("layouts/details/{entryId:long}"),
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
},
|
||||||
|
new RouteRegistration<WorkshopLibraryViewModel>("library")
|
||||||
|
{
|
||||||
|
Children = new List<IRouterRegistration>
|
||||||
|
{
|
||||||
|
new RouteRegistration<InstalledTabViewModel>("installed"),
|
||||||
|
new RouteRegistration<SubmissionsTabViewModel>("submissions"),
|
||||||
|
new RouteRegistration<SubmissionDetailViewModel>("submissions/{entryId:long}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
#endif
|
|
||||||
new RouteRegistration<SurfaceEditorViewModel>("surface-editor"),
|
new RouteRegistration<SurfaceEditorViewModel>("surface-editor"),
|
||||||
new RouteRegistration<SettingsViewModel>("settings")
|
new RouteRegistration<SettingsViewModel>("settings")
|
||||||
{
|
{
|
||||||
|
|||||||
@ -2,7 +2,6 @@
|
|||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
|
|
||||||
xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
|
xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
|
||||||
xmlns:debugger="clr-namespace:Artemis.UI.Screens.Debugger"
|
xmlns:debugger="clr-namespace:Artemis.UI.Screens.Debugger"
|
||||||
xmlns:shared="clr-namespace:Artemis.UI.Shared;assembly=Artemis.UI.Shared"
|
xmlns:shared="clr-namespace:Artemis.UI.Shared;assembly=Artemis.UI.Shared"
|
||||||
@ -35,7 +34,5 @@
|
|||||||
<Border Classes="router-container" Grid.Column="1">
|
<Border Classes="router-container" Grid.Column="1">
|
||||||
<ContentControl Content="{CompiledBinding SelectedItem}" Margin="15"></ContentControl>
|
<ContentControl Content="{CompiledBinding SelectedItem}" Margin="15"></ContentControl>
|
||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
<StackPanel Grid.Column="0" Grid.ColumnSpan="2" Classes="notification-container" Name="NotificationContainer" VerticalAlignment="Bottom" HorizontalAlignment="Right" />
|
|
||||||
</Grid>
|
</Grid>
|
||||||
</windowing:AppWindow>
|
</windowing:AppWindow>
|
||||||
@ -23,7 +23,7 @@
|
|||||||
</TextBlock>
|
</TextBlock>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
<TreeView Grid.Row="1" ItemsSource="{CompiledBinding MainDataModel.Children}">
|
<TreeView Grid.Row="1" ItemsSource="{CompiledBinding MainDataModel.Children}" Padding="0 0 15 0">
|
||||||
<TreeView.Styles>
|
<TreeView.Styles>
|
||||||
<Style Selector="TreeViewItem">
|
<Style Selector="TreeViewItem">
|
||||||
<Setter Property="IsExpanded" Value="{CompiledBinding IsVisualizationExpanded, Mode=TwoWay,DataType=dataModel:DataModelVisualizationViewModel}" />
|
<Setter Property="IsExpanded" Value="{CompiledBinding IsVisualizationExpanded, Mode=TwoWay,DataType=dataModel:DataModelVisualizationViewModel}" />
|
||||||
@ -42,7 +42,7 @@
|
|||||||
</TreeView.Styles>
|
</TreeView.Styles>
|
||||||
<TreeView.DataTemplates>
|
<TreeView.DataTemplates>
|
||||||
<TreeDataTemplate DataType="{x:Type dataModel:DataModelPropertiesViewModel}" ItemsSource="{CompiledBinding Children}">
|
<TreeDataTemplate DataType="{x:Type dataModel:DataModelPropertiesViewModel}" ItemsSource="{CompiledBinding Children}">
|
||||||
<Grid ColumnDefinitions="Auto,Auto,*">
|
<Grid ColumnDefinitions="Auto,Auto,*" Margin="0 0 8 0">
|
||||||
<StackPanel Grid.Column="0" Orientation="Horizontal" Margin="0 0 5 0">
|
<StackPanel Grid.Column="0" Orientation="Horizontal" Margin="0 0 5 0">
|
||||||
<TextBlock FontWeight="Bold">[</TextBlock>
|
<TextBlock FontWeight="Bold">[</TextBlock>
|
||||||
<TextBlock FontWeight="Bold" Text="{CompiledBinding DisplayValueType, Converter={StaticResource TypeToStringConverter}, Mode=OneWay}" />
|
<TextBlock FontWeight="Bold" Text="{CompiledBinding DisplayValueType, Converter={StaticResource TypeToStringConverter}, Mode=OneWay}" />
|
||||||
@ -52,13 +52,14 @@
|
|||||||
<TextBlock Grid.Column="1" Text="{CompiledBinding PropertyDescription.Name}" ToolTip.Tip="{CompiledBinding PropertyDescription.Description}" />
|
<TextBlock Grid.Column="1" Text="{CompiledBinding PropertyDescription.Name}" ToolTip.Tip="{CompiledBinding PropertyDescription.Description}" />
|
||||||
<TextBlock Grid.Column="2"
|
<TextBlock Grid.Column="2"
|
||||||
Text="{CompiledBinding DisplayValue}"
|
Text="{CompiledBinding DisplayValue}"
|
||||||
FontFamily="Consolas"
|
FontFamily="{StaticResource RobotoMono}"
|
||||||
|
FontSize="13"
|
||||||
HorizontalAlignment="Right" />
|
HorizontalAlignment="Right" />
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
</TreeDataTemplate>
|
</TreeDataTemplate>
|
||||||
<TreeDataTemplate DataType="{x:Type dataModel:DataModelListViewModel}" ItemsSource="{CompiledBinding ListChildren}">
|
<TreeDataTemplate DataType="{x:Type dataModel:DataModelListViewModel}" ItemsSource="{CompiledBinding ListChildren}">
|
||||||
<Grid ColumnDefinitions="Auto,Auto,*">
|
<Grid ColumnDefinitions="Auto,Auto,*" Margin="0 0 8 0">
|
||||||
<StackPanel Grid.Column="0" Orientation="Horizontal" Margin="0 0 5 0">
|
<StackPanel Grid.Column="0" Orientation="Horizontal" Margin="0 0 5 0">
|
||||||
<TextBlock FontWeight="Bold">[</TextBlock>
|
<TextBlock FontWeight="Bold">[</TextBlock>
|
||||||
<TextBlock FontWeight="Bold" Text="{CompiledBinding DisplayValueType, Converter={StaticResource TypeToStringConverter}, Mode=OneWay}" />
|
<TextBlock FontWeight="Bold" Text="{CompiledBinding DisplayValueType, Converter={StaticResource TypeToStringConverter}, Mode=OneWay}" />
|
||||||
@ -67,12 +68,13 @@
|
|||||||
<TextBlock Grid.Column="1" Text="{CompiledBinding PropertyDescription.Name}" ToolTip.Tip="{CompiledBinding PropertyDescription.Description}" />
|
<TextBlock Grid.Column="1" Text="{CompiledBinding PropertyDescription.Name}" ToolTip.Tip="{CompiledBinding PropertyDescription.Description}" />
|
||||||
<TextBlock Grid.Column="2"
|
<TextBlock Grid.Column="2"
|
||||||
Text="{CompiledBinding CountDisplay, Mode=OneWay}"
|
Text="{CompiledBinding CountDisplay, Mode=OneWay}"
|
||||||
FontFamily="Consolas"
|
FontSize="13"
|
||||||
|
FontFamily="{StaticResource RobotoMono}"
|
||||||
HorizontalAlignment="Right" />
|
HorizontalAlignment="Right" />
|
||||||
</Grid>
|
</Grid>
|
||||||
</TreeDataTemplate>
|
</TreeDataTemplate>
|
||||||
<TreeDataTemplate DataType="{x:Type dataModel:DataModelPropertyViewModel}">
|
<TreeDataTemplate DataType="{x:Type dataModel:DataModelPropertyViewModel}">
|
||||||
<Grid ColumnDefinitions="Auto,Auto,*">
|
<Grid ColumnDefinitions="Auto,Auto,*" Margin="0 0 8 0">
|
||||||
<!-- Value description -->
|
<!-- Value description -->
|
||||||
<StackPanel Grid.Column="0" Orientation="Horizontal" Margin="0 0 5 0">
|
<StackPanel Grid.Column="0" Orientation="Horizontal" Margin="0 0 5 0">
|
||||||
<TextBlock FontWeight="Bold">[</TextBlock>
|
<TextBlock FontWeight="Bold">[</TextBlock>
|
||||||
@ -82,11 +84,11 @@
|
|||||||
<TextBlock Grid.Column="1" Text="{CompiledBinding PropertyDescription.Name}" ToolTip.Tip="{CompiledBinding PropertyDescription.Description}" />
|
<TextBlock Grid.Column="1" Text="{CompiledBinding PropertyDescription.Name}" ToolTip.Tip="{CompiledBinding PropertyDescription.Description}" />
|
||||||
|
|
||||||
<!-- Value display -->
|
<!-- Value display -->
|
||||||
<ContentControl Grid.Column="2" Content="{CompiledBinding DisplayViewModel}" FontFamily="Consolas" />
|
<ContentControl Grid.Column="2" Content="{CompiledBinding DisplayViewModel}" FontSize="13" FontFamily="{StaticResource RobotoMono}" />
|
||||||
</Grid>
|
</Grid>
|
||||||
</TreeDataTemplate>
|
</TreeDataTemplate>
|
||||||
<TreeDataTemplate DataType="{x:Type dataModel:DataModelListItemViewModel}">
|
<TreeDataTemplate DataType="{x:Type dataModel:DataModelListItemViewModel}">
|
||||||
<Grid ColumnDefinitions="Auto,Auto,*">
|
<Grid ColumnDefinitions="Auto,Auto,*" Margin="0 0 8 0">
|
||||||
<!-- Value description -->
|
<!-- Value description -->
|
||||||
<StackPanel Grid.Column="0" Orientation="Horizontal" Margin="0 0 5 0">
|
<StackPanel Grid.Column="0" Orientation="Horizontal" Margin="0 0 5 0">
|
||||||
<TextBlock FontWeight="Bold">[</TextBlock>
|
<TextBlock FontWeight="Bold">[</TextBlock>
|
||||||
@ -99,12 +101,12 @@
|
|||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
<!-- Value display -->
|
<!-- Value display -->
|
||||||
<ContentControl Grid.Column="2" Content="{CompiledBinding DisplayViewModel}" FontFamily="Consolas" />
|
<ContentControl Grid.Column="2" Content="{CompiledBinding DisplayViewModel}" FontSize="13" FontFamily="{StaticResource RobotoMono}" />
|
||||||
</Grid>
|
</Grid>
|
||||||
</TreeDataTemplate>
|
</TreeDataTemplate>
|
||||||
|
|
||||||
<TreeDataTemplate DataType="{x:Type dataModel:DataModelEventViewModel}" ItemsSource="{CompiledBinding Children}">
|
<TreeDataTemplate DataType="{x:Type dataModel:DataModelEventViewModel}" ItemsSource="{CompiledBinding Children}">
|
||||||
<Grid ColumnDefinitions="Auto,Auto,*">
|
<Grid ColumnDefinitions="Auto,Auto,*" Margin="0 0 8 0">
|
||||||
<StackPanel Grid.Column="0" Orientation="Horizontal" Margin="0 0 5 0">
|
<StackPanel Grid.Column="0" Orientation="Horizontal" Margin="0 0 5 0">
|
||||||
<TextBlock FontWeight="Bold">[</TextBlock>
|
<TextBlock FontWeight="Bold">[</TextBlock>
|
||||||
<TextBlock FontWeight="Bold" Text="{CompiledBinding DisplayValueType, Converter={StaticResource TypeToStringConverter}, Mode=OneWay}" />
|
<TextBlock FontWeight="Bold" Text="{CompiledBinding DisplayValueType, Converter={StaticResource TypeToStringConverter}, Mode=OneWay}" />
|
||||||
@ -113,7 +115,8 @@
|
|||||||
<TextBlock Grid.Column="1" Text="{CompiledBinding PropertyDescription.Name}" ToolTip.Tip="{CompiledBinding PropertyDescription.Description}" />
|
<TextBlock Grid.Column="1" Text="{CompiledBinding PropertyDescription.Name}" ToolTip.Tip="{CompiledBinding PropertyDescription.Description}" />
|
||||||
<TextBlock Grid.Column="2"
|
<TextBlock Grid.Column="2"
|
||||||
Text="{CompiledBinding Path=CountDisplay, DataType=dataModel:DataModelListViewModel ,Mode=OneWay}"
|
Text="{CompiledBinding Path=CountDisplay, DataType=dataModel:DataModelListViewModel ,Mode=OneWay}"
|
||||||
FontFamily="Consolas"
|
FontSize="13"
|
||||||
|
FontFamily="{StaticResource RobotoMono}"
|
||||||
HorizontalAlignment="Right" />
|
HorizontalAlignment="Right" />
|
||||||
</Grid>
|
</Grid>
|
||||||
</TreeDataTemplate>
|
</TreeDataTemplate>
|
||||||
|
|||||||
@ -9,9 +9,9 @@
|
|||||||
<ScrollViewer Name="LogsScrollViewer" VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Auto">
|
<ScrollViewer Name="LogsScrollViewer" VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Auto">
|
||||||
<SelectableTextBlock
|
<SelectableTextBlock
|
||||||
Inlines="{CompiledBinding Lines}"
|
Inlines="{CompiledBinding Lines}"
|
||||||
FontFamily="Consolas"
|
FontFamily="{StaticResource RobotoMono}"
|
||||||
|
FontSize="12"
|
||||||
SizeChanged="Control_OnSizeChanged"
|
SizeChanged="Control_OnSizeChanged"
|
||||||
SelectionBrush="{StaticResource TextControlSelectionHighlightColor}"
|
SelectionBrush="{StaticResource TextControlSelectionHighlightColor}"/>
|
||||||
/>
|
|
||||||
</ScrollViewer>
|
</ScrollViewer>
|
||||||
</UserControl>
|
</UserControl>
|
||||||
@ -19,7 +19,8 @@
|
|||||||
<ScrollViewer Grid.Row="2" Grid.Column="0" Grid.ColumnSpan="2" Name="LogsScrollViewer" VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Auto">
|
<ScrollViewer Grid.Row="2" Grid.Column="0" Grid.ColumnSpan="2" Name="LogsScrollViewer" VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Auto">
|
||||||
<SelectableTextBlock
|
<SelectableTextBlock
|
||||||
Inlines="{CompiledBinding Lines}"
|
Inlines="{CompiledBinding Lines}"
|
||||||
FontFamily="Consolas"
|
FontFamily="{StaticResource RobotoMono}"
|
||||||
|
FontSize="12"
|
||||||
SizeChanged="Control_OnSizeChanged"
|
SizeChanged="Control_OnSizeChanged"
|
||||||
SelectionBrush="{StaticResource TextControlSelectionHighlightColor}" />
|
SelectionBrush="{StaticResource TextControlSelectionHighlightColor}" />
|
||||||
</ScrollViewer>
|
</ScrollViewer>
|
||||||
|
|||||||
@ -69,7 +69,5 @@
|
|||||||
</Panel>
|
</Panel>
|
||||||
|
|
||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
<StackPanel Grid.Column="0" Grid.ColumnSpan="3" Classes="notification-container" Name="NotificationContainer" VerticalAlignment="Top" HorizontalAlignment="Right" />
|
|
||||||
</Grid>
|
</Grid>
|
||||||
</windowing:AppWindow>
|
</windowing:AppWindow>
|
||||||
@ -1,12 +1,13 @@
|
|||||||
using Artemis.Core.Services;
|
using Artemis.Core.Services;
|
||||||
using Artemis.UI.Screens.StartupWizard;
|
using Artemis.UI.Screens.StartupWizard;
|
||||||
using Artemis.UI.Shared;
|
using Artemis.UI.Shared;
|
||||||
|
using Artemis.UI.Shared.Routing;
|
||||||
using Artemis.UI.Shared.Services;
|
using Artemis.UI.Shared.Services;
|
||||||
using Avalonia.Threading;
|
using Avalonia.Threading;
|
||||||
|
|
||||||
namespace Artemis.UI.Screens.Home;
|
namespace Artemis.UI.Screens.Home;
|
||||||
|
|
||||||
public class HomeViewModel : ViewModelBase, IMainScreenViewModel
|
public class HomeViewModel : RoutableScreen, IMainScreenViewModel
|
||||||
{
|
{
|
||||||
public HomeViewModel(ISettingsService settingsService, IWindowService windowService)
|
public HomeViewModel(ISettingsService settingsService, IWindowService windowService)
|
||||||
{
|
{
|
||||||
|
|||||||
@ -70,7 +70,5 @@
|
|||||||
<ContentControl Margin="0 0 5 0" Content="{CompiledBinding ConfigurationViewModel}"></ContentControl>
|
<ContentControl Margin="0 0 5 0" Content="{CompiledBinding ConfigurationViewModel}"></ContentControl>
|
||||||
</ScrollViewer>
|
</ScrollViewer>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<StackPanel Name="NotificationContainer" Classes="notification-container" VerticalAlignment="Bottom" HorizontalAlignment="Right" />
|
|
||||||
</Panel>
|
</Panel>
|
||||||
</windowing:AppWindow>
|
</windowing:AppWindow>
|
||||||
@ -16,7 +16,6 @@ using Artemis.UI.Shared.Services;
|
|||||||
using Artemis.UI.Shared.Services.ProfileEditor;
|
using Artemis.UI.Shared.Services.ProfileEditor;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using ReactiveUI;
|
using ReactiveUI;
|
||||||
using Serilog;
|
|
||||||
|
|
||||||
namespace Artemis.UI.Screens.ProfileEditor.MenuBar;
|
namespace Artemis.UI.Screens.ProfileEditor.MenuBar;
|
||||||
|
|
||||||
@ -182,7 +181,7 @@ public class MenuBarViewModel : ActivatableViewModelBase
|
|||||||
if (!await _windowService.ShowConfirmContentDialog("Delete profile", "Are you sure you want to permanently delete this profile?"))
|
if (!await _windowService.ShowConfirmContentDialog("Delete profile", "Are you sure you want to permanently delete this profile?"))
|
||||||
return;
|
return;
|
||||||
|
|
||||||
if (ProfileConfiguration.IsBeingEdited)
|
if (_profileService.FocusProfile == ProfileConfiguration)
|
||||||
await _router.Navigate("home");
|
await _router.Navigate("home");
|
||||||
_profileService.RemoveProfileConfiguration(ProfileConfiguration);
|
_profileService.RemoveProfileConfiguration(ProfileConfiguration);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,6 +7,7 @@ using Artemis.Core;
|
|||||||
using Artemis.Core.Services;
|
using Artemis.Core.Services;
|
||||||
using Artemis.UI.Shared;
|
using Artemis.UI.Shared;
|
||||||
using Artemis.UI.Shared.Services.ProfileEditor;
|
using Artemis.UI.Shared.Services.ProfileEditor;
|
||||||
|
using Avalonia.Threading;
|
||||||
using ReactiveUI;
|
using ReactiveUI;
|
||||||
|
|
||||||
namespace Artemis.UI.Screens.ProfileEditor.Playback;
|
namespace Artemis.UI.Screens.ProfileEditor.Playback;
|
||||||
@ -237,7 +238,7 @@ public class PlaybackViewModel : ActivatableViewModelBase
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_profileEditorService.ChangeTime(newTime);
|
Dispatcher.UIThread.Invoke(() => _profileEditorService.ChangeTime(newTime));
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
|
|||||||
@ -1,10 +1,10 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Reactive.Disposables;
|
using System.Reactive.Disposables;
|
||||||
|
using System.Reactive.Linq;
|
||||||
using Avalonia;
|
using Avalonia;
|
||||||
using Avalonia.Controls.PanAndZoom;
|
using Avalonia.Controls.PanAndZoom;
|
||||||
using Avalonia.Input;
|
using Avalonia.Input;
|
||||||
using Avalonia.Markup.Xaml;
|
|
||||||
using Avalonia.Media;
|
using Avalonia.Media;
|
||||||
using Avalonia.ReactiveUI;
|
using Avalonia.ReactiveUI;
|
||||||
using Avalonia.Threading;
|
using Avalonia.Threading;
|
||||||
@ -31,11 +31,7 @@ public partial class VisualEditorView : ReactiveUserControl<VisualEditorViewMode
|
|||||||
Disposable.Create(() => ViewModel.AutoFitRequested -= ViewModelOnAutoFitRequested).DisposeWith(d);
|
Disposable.Create(() => ViewModel.AutoFitRequested -= ViewModelOnAutoFitRequested).DisposeWith(d);
|
||||||
});
|
});
|
||||||
|
|
||||||
this.WhenAnyValue(v => v.Bounds).Subscribe(_ =>
|
this.WhenAnyValue(v => v.Bounds).Where(_ => !_movedByUser).Subscribe(_ => AutoFit(true));
|
||||||
{
|
|
||||||
if (!_movedByUser)
|
|
||||||
AutoFit(true);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ZoomBorderOnPointerWheelChanged(object? sender, PointerWheelEventArgs e)
|
private void ZoomBorderOnPointerWheelChanged(object? sender, PointerWheelEventArgs e)
|
||||||
|
|||||||
@ -81,7 +81,7 @@
|
|||||||
</ItemsControl.ItemTemplate>
|
</ItemsControl.ItemTemplate>
|
||||||
</ItemsControl>
|
</ItemsControl>
|
||||||
</Border>
|
</Border>
|
||||||
<ContentControl Grid.Column="1" Content="{CompiledBinding VisualEditorViewModel}" Classes="fade-in"
|
<ContentControl Grid.Column="1" Content="{CompiledBinding VisualEditorViewModel}"
|
||||||
IsVisible="{CompiledBinding ProfileConfiguration, Converter={x:Static ObjectConverters.IsNotNull}}" />
|
IsVisible="{CompiledBinding ProfileConfiguration, Converter={x:Static ObjectConverters.IsNotNull}}" />
|
||||||
</Grid>
|
</Grid>
|
||||||
</Border>
|
</Border>
|
||||||
|
|||||||
@ -17,14 +17,13 @@ using Artemis.UI.Shared;
|
|||||||
using Artemis.UI.Shared.Routing;
|
using Artemis.UI.Shared.Routing;
|
||||||
using Artemis.UI.Shared.Services.MainWindow;
|
using Artemis.UI.Shared.Services.MainWindow;
|
||||||
using Artemis.UI.Shared.Services.ProfileEditor;
|
using Artemis.UI.Shared.Services.ProfileEditor;
|
||||||
using Avalonia.Threading;
|
|
||||||
using DynamicData;
|
using DynamicData;
|
||||||
using DynamicData.Binding;
|
using DynamicData.Binding;
|
||||||
using ReactiveUI;
|
using ReactiveUI;
|
||||||
|
|
||||||
namespace Artemis.UI.Screens.ProfileEditor;
|
namespace Artemis.UI.Screens.ProfileEditor;
|
||||||
|
|
||||||
public class ProfileEditorViewModel : RoutableScreen<object, ProfileEditorViewModelParameters>, IMainScreenViewModel
|
public class ProfileEditorViewModel : RoutableScreen<ProfileEditorViewModelParameters>, IMainScreenViewModel
|
||||||
{
|
{
|
||||||
private readonly IProfileEditorService _profileEditorService;
|
private readonly IProfileEditorService _profileEditorService;
|
||||||
private readonly IProfileService _profileService;
|
private readonly IProfileService _profileService;
|
||||||
@ -161,10 +160,10 @@ public class ProfileEditorViewModel : RoutableScreen<object, ProfileEditorViewMo
|
|||||||
{
|
{
|
||||||
ProfileConfiguration? profileConfiguration = _profileService.ProfileConfigurations.FirstOrDefault(c => c.ProfileId == parameters.ProfileId);
|
ProfileConfiguration? profileConfiguration = _profileService.ProfileConfigurations.FirstOrDefault(c => c.ProfileId == parameters.ProfileId);
|
||||||
|
|
||||||
// If the profile doesn't exist, navigate home for lack of some kind of 404 :p
|
// If the profile doesn't exist, cancel navigation
|
||||||
if (profileConfiguration == null)
|
if (profileConfiguration == null)
|
||||||
{
|
{
|
||||||
await args.Router.Navigate("home");
|
args.Cancel();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,8 +1,9 @@
|
|||||||
using Artemis.UI.Shared;
|
using Artemis.UI.Shared;
|
||||||
|
using Artemis.UI.Shared.Routing;
|
||||||
|
|
||||||
namespace Artemis.UI.Screens.Root;
|
namespace Artemis.UI.Screens.Root;
|
||||||
|
|
||||||
public class BlankViewModel : ViewModelBase, IMainScreenViewModel
|
public class BlankViewModel : RoutableScreen, IMainScreenViewModel
|
||||||
{
|
{
|
||||||
/// <inheritdoc />
|
/// <inheritdoc />
|
||||||
public ViewModelBase? TitleBarViewModel => null;
|
public ViewModelBase? TitleBarViewModel => null;
|
||||||
|
|||||||
@ -1,5 +1,6 @@
|
|||||||
using System;
|
using System;
|
||||||
using System.Reactive.Disposables;
|
using System.Reactive.Disposables;
|
||||||
|
using Artemis.UI.Shared.Routing;
|
||||||
using Avalonia.ReactiveUI;
|
using Avalonia.ReactiveUI;
|
||||||
using Avalonia.Threading;
|
using Avalonia.Threading;
|
||||||
using ReactiveUI;
|
using ReactiveUI;
|
||||||
@ -14,7 +15,7 @@ public partial class RootView : ReactiveUserControl<RootViewModel>
|
|||||||
this.WhenActivated(d => ViewModel.WhenAnyValue(vm => vm.Screen).Subscribe(Navigate).DisposeWith(d));
|
this.WhenActivated(d => ViewModel.WhenAnyValue(vm => vm.Screen).Subscribe(Navigate).DisposeWith(d));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void Navigate(IMainScreenViewModel viewModel)
|
private void Navigate(RoutableScreen viewModel)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
|||||||
@ -18,7 +18,7 @@ using ReactiveUI;
|
|||||||
|
|
||||||
namespace Artemis.UI.Screens.Root;
|
namespace Artemis.UI.Screens.Root;
|
||||||
|
|
||||||
public class RootViewModel : RoutableScreen<IMainScreenViewModel>, IMainWindowProvider
|
public class RootViewModel : RoutableHostScreen<RoutableScreen>, IMainWindowProvider
|
||||||
{
|
{
|
||||||
private readonly ICoreService _coreService;
|
private readonly ICoreService _coreService;
|
||||||
private readonly IDebugService _debugService;
|
private readonly IDebugService _debugService;
|
||||||
@ -100,12 +100,10 @@ public class RootViewModel : RoutableScreen<IMainScreenViewModel>, IMainWindowPr
|
|||||||
_router.GoForward();
|
_router.GoForward();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void UpdateTitleBarViewModel(IMainScreenViewModel? viewModel)
|
private void UpdateTitleBarViewModel(RoutableScreen? viewModel)
|
||||||
{
|
{
|
||||||
if (viewModel?.TitleBarViewModel != null)
|
IMainScreenViewModel? mainScreenViewModel = viewModel as IMainScreenViewModel;
|
||||||
TitleBarViewModel = viewModel.TitleBarViewModel;
|
TitleBarViewModel = mainScreenViewModel?.TitleBarViewModel ?? _defaultTitleBarViewModel;
|
||||||
else
|
|
||||||
TitleBarViewModel = _defaultTitleBarViewModel;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private void CurrentMainWindowOnClosing(object? sender, EventArgs e)
|
private void CurrentMainWindowOnClosing(object? sender, EventArgs e)
|
||||||
@ -130,7 +128,7 @@ public class RootViewModel : RoutableScreen<IMainScreenViewModel>, IMainWindowPr
|
|||||||
|
|
||||||
private void ShowSplashScreen()
|
private void ShowSplashScreen()
|
||||||
{
|
{
|
||||||
_windowService.ShowWindow<SplashViewModel>();
|
_windowService.ShowWindow(out SplashViewModel _);
|
||||||
}
|
}
|
||||||
|
|
||||||
#region Tray commands
|
#region Tray commands
|
||||||
|
|||||||
@ -1,20 +0,0 @@
|
|||||||
using System;
|
|
||||||
|
|
||||||
namespace Artemis.UI.Screens.Settings;
|
|
||||||
|
|
||||||
public class SettingsTab
|
|
||||||
{
|
|
||||||
public SettingsTab(string path, string name)
|
|
||||||
{
|
|
||||||
Path = path;
|
|
||||||
Name = name;
|
|
||||||
}
|
|
||||||
|
|
||||||
public string Path { get; set; }
|
|
||||||
public string Name { get; set; }
|
|
||||||
|
|
||||||
public bool Matches(string path)
|
|
||||||
{
|
|
||||||
return path.StartsWith($"settings/{Path}", StringComparison.InvariantCultureIgnoreCase);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -8,20 +8,25 @@
|
|||||||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
|
||||||
x:Class="Artemis.UI.Screens.Settings.SettingsView"
|
x:Class="Artemis.UI.Screens.Settings.SettingsView"
|
||||||
x:DataType="settings:SettingsViewModel">
|
x:DataType="settings:SettingsViewModel">
|
||||||
<Border Classes="router-container">
|
<controls:NavigationView PaneDisplayMode="Top"
|
||||||
<Grid RowDefinitions="Auto,*">
|
MenuItemsSource="{CompiledBinding SettingTabs}"
|
||||||
<TabStrip Grid.Row="0" Margin="12" ItemsSource="{CompiledBinding SettingTabs}" SelectedItem="{CompiledBinding SelectedTab}">
|
SelectedItem="{CompiledBinding SelectedTab}"
|
||||||
<TabStrip.ItemTemplate>
|
IsBackEnabled="True"
|
||||||
<DataTemplate>
|
IsBackButtonVisible="True"
|
||||||
<TextBlock Text="{CompiledBinding Name}" />
|
IsSettingsVisible="False"
|
||||||
</DataTemplate>
|
BackRequested="NavigationView_OnBackRequested">
|
||||||
</TabStrip.ItemTemplate>
|
<controls:NavigationView.Styles>
|
||||||
</TabStrip>
|
<Styles>
|
||||||
<controls:Frame Grid.Row="1" Name="TabFrame" IsNavigationStackEnabled="False" CacheSize="0">
|
<Style Selector="controls|NavigationView:topnavminimal /template/ SplitView Border#ContentGridBorder">
|
||||||
|
<Setter Property="CornerRadius" Value="8 0 0 0" />
|
||||||
|
</Style>
|
||||||
|
</Styles>
|
||||||
|
</controls:NavigationView.Styles>
|
||||||
|
|
||||||
|
<controls:Frame Name="TabFrame" IsNavigationStackEnabled="False" CacheSize="0" Padding="20">
|
||||||
<controls:Frame.NavigationPageFactory>
|
<controls:Frame.NavigationPageFactory>
|
||||||
<ui:PageFactory/>
|
<ui:PageFactory/>
|
||||||
</controls:Frame.NavigationPageFactory>
|
</controls:Frame.NavigationPageFactory>
|
||||||
</controls:Frame>
|
</controls:Frame>
|
||||||
</Grid>
|
</controls:NavigationView>
|
||||||
</Border>
|
|
||||||
</UserControl>
|
</UserControl>
|
||||||
@ -3,8 +3,7 @@ using System.Reactive.Disposables;
|
|||||||
using Artemis.UI.Shared;
|
using Artemis.UI.Shared;
|
||||||
using Avalonia.ReactiveUI;
|
using Avalonia.ReactiveUI;
|
||||||
using Avalonia.Threading;
|
using Avalonia.Threading;
|
||||||
using FluentAvalonia.UI.Media.Animation;
|
using FluentAvalonia.UI.Controls;
|
||||||
using FluentAvalonia.UI.Navigation;
|
|
||||||
using ReactiveUI;
|
using ReactiveUI;
|
||||||
|
|
||||||
namespace Artemis.UI.Screens.Settings;
|
namespace Artemis.UI.Screens.Settings;
|
||||||
@ -19,6 +18,11 @@ public partial class SettingsView : ReactiveUserControl<SettingsViewModel>
|
|||||||
|
|
||||||
private void Navigate(ViewModelBase viewModel)
|
private void Navigate(ViewModelBase viewModel)
|
||||||
{
|
{
|
||||||
Dispatcher.UIThread.Invoke(() => TabFrame.NavigateFromObject(viewModel, new FrameNavigationOptions {TransitionInfoOverride = new SlideNavigationTransitionInfo()}));
|
Dispatcher.UIThread.Invoke(() => TabFrame.NavigateFromObject(viewModel));
|
||||||
|
}
|
||||||
|
|
||||||
|
private void NavigationView_OnBackRequested(object? sender, NavigationViewBackRequestedEventArgs e)
|
||||||
|
{
|
||||||
|
ViewModel?.GoBack();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -4,39 +4,40 @@ using System.Linq;
|
|||||||
using System.Reactive.Disposables;
|
using System.Reactive.Disposables;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
using Artemis.UI.Routing;
|
||||||
using Artemis.UI.Shared;
|
using Artemis.UI.Shared;
|
||||||
using Artemis.UI.Shared.Routing;
|
using Artemis.UI.Shared.Routing;
|
||||||
using ReactiveUI;
|
using ReactiveUI;
|
||||||
|
|
||||||
namespace Artemis.UI.Screens.Settings;
|
namespace Artemis.UI.Screens.Settings;
|
||||||
|
|
||||||
public class SettingsViewModel : RoutableScreen<ActivatableViewModelBase>, IMainScreenViewModel
|
public class SettingsViewModel : RoutableHostScreen<RoutableScreen>, IMainScreenViewModel
|
||||||
{
|
{
|
||||||
private readonly IRouter _router;
|
private readonly IRouter _router;
|
||||||
private SettingsTab? _selectedTab;
|
private RouteViewModel? _selectedTab;
|
||||||
|
|
||||||
public SettingsViewModel(IRouter router)
|
public SettingsViewModel(IRouter router)
|
||||||
{
|
{
|
||||||
_router = router;
|
_router = router;
|
||||||
SettingTabs = new ObservableCollection<SettingsTab>
|
SettingTabs = new ObservableCollection<RouteViewModel>
|
||||||
{
|
{
|
||||||
new("general", "General"),
|
new("General", "settings/general"),
|
||||||
new("plugins", "Plugins"),
|
new("Plugins", "settings/plugins"),
|
||||||
new("devices", "Devices"),
|
new("Devices", "settings/devices"),
|
||||||
new("releases", "Releases"),
|
new("Releases", "settings/releases"),
|
||||||
new("about", "About"),
|
new("About", "settings/about"),
|
||||||
};
|
};
|
||||||
|
|
||||||
// Navigate on tab change
|
// Navigate on tab change
|
||||||
this.WhenActivated(d => this.WhenAnyValue(vm => vm.SelectedTab)
|
this.WhenActivated(d => this.WhenAnyValue(vm => vm.SelectedTab)
|
||||||
.WhereNotNull()
|
.WhereNotNull()
|
||||||
.Subscribe(s => _router.Navigate($"settings/{s.Path}", new RouterNavigationOptions {IgnoreOnPartialMatch = true}))
|
.Subscribe(s => _router.Navigate(s.Path, new RouterNavigationOptions {IgnoreOnPartialMatch = true}))
|
||||||
.DisposeWith(d));
|
.DisposeWith(d));
|
||||||
}
|
}
|
||||||
|
|
||||||
public ObservableCollection<SettingsTab> SettingTabs { get; }
|
public ObservableCollection<RouteViewModel> SettingTabs { get; }
|
||||||
|
|
||||||
public SettingsTab? SelectedTab
|
public RouteViewModel? SelectedTab
|
||||||
{
|
{
|
||||||
get => _selectedTab;
|
get => _selectedTab;
|
||||||
set => RaiseAndSetIfChanged(ref _selectedTab, value);
|
set => RaiseAndSetIfChanged(ref _selectedTab, value);
|
||||||
@ -52,6 +53,11 @@ public class SettingsViewModel : RoutableScreen<ActivatableViewModelBase>, IMain
|
|||||||
|
|
||||||
// Always show a tab, if there is none forward to the first
|
// Always show a tab, if there is none forward to the first
|
||||||
if (SelectedTab == null)
|
if (SelectedTab == null)
|
||||||
await _router.Navigate($"settings/{SettingTabs.First().Path}");
|
await _router.Navigate(SettingTabs.First().Path);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void GoBack()
|
||||||
|
{
|
||||||
|
_router.Navigate("workshop");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -5,6 +5,7 @@
|
|||||||
xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
|
xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
|
||||||
xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
|
xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
|
||||||
xmlns:vm="clr-namespace:Artemis.UI.Screens.Settings;assembly=Artemis.UI"
|
xmlns:vm="clr-namespace:Artemis.UI.Screens.Settings;assembly=Artemis.UI"
|
||||||
|
xmlns:il="clr-namespace:AsyncImageLoader;assembly=AsyncImageLoader.Avalonia"
|
||||||
x:DataType="vm:AboutTabViewModel"
|
x:DataType="vm:AboutTabViewModel"
|
||||||
mc:Ignorable="d" d:DesignWidth="1000" d:DesignHeight="1400"
|
mc:Ignorable="d" d:DesignWidth="1000" d:DesignHeight="1400"
|
||||||
x:Class="Artemis.UI.Screens.Settings.AboutTabView">
|
x:Class="Artemis.UI.Screens.Settings.AboutTabView">
|
||||||
@ -18,7 +19,7 @@
|
|||||||
VerticalAlignment="Center"
|
VerticalAlignment="Center"
|
||||||
Source="/Assets/Images/Logo/bow.png"
|
Source="/Assets/Images/Logo/bow.png"
|
||||||
Margin="0 0 20 0"
|
Margin="0 0 20 0"
|
||||||
RenderOptions.BitmapInterpolationMode="HighQuality"/>
|
RenderOptions.BitmapInterpolationMode="HighQuality" />
|
||||||
<TextBlock Grid.Row="0" Grid.Column="1" FontSize="36" VerticalAlignment="Bottom">
|
<TextBlock Grid.Row="0" Grid.Column="1" FontSize="36" VerticalAlignment="Bottom">
|
||||||
Artemis 2
|
Artemis 2
|
||||||
</TextBlock>
|
</TextBlock>
|
||||||
@ -52,17 +53,9 @@
|
|||||||
<Border Classes="card" Margin="0 20 0 10">
|
<Border Classes="card" Margin="0 20 0 10">
|
||||||
<StackPanel>
|
<StackPanel>
|
||||||
<Grid RowDefinitions="*,*,*" ColumnDefinitions="Auto,*">
|
<Grid RowDefinitions="*,*,*" ColumnDefinitions="Auto,*">
|
||||||
<Ellipse Grid.Row="0"
|
<Ellipse Grid.Row="0" Grid.Column="0" Grid.RowSpan="3" VerticalAlignment="Top" Height="75" Width="75" Margin="0 0 15 0">
|
||||||
Grid.Column="0"
|
|
||||||
Grid.RowSpan="3"
|
|
||||||
VerticalAlignment="Top"
|
|
||||||
Height="40"
|
|
||||||
Width="40"
|
|
||||||
Margin="0 0 15 0"
|
|
||||||
IsVisible="{CompiledBinding RobertProfileImage, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"
|
|
||||||
RenderOptions.BitmapInterpolationMode="HighQuality">
|
|
||||||
<Ellipse.Fill>
|
<Ellipse.Fill>
|
||||||
<ImageBrush Source="{CompiledBinding RobertProfileImage}" />
|
<ImageBrush il:ImageBrushLoader.Source="https://avatars.githubusercontent.com/u/8858506" />
|
||||||
</Ellipse.Fill>
|
</Ellipse.Fill>
|
||||||
</Ellipse>
|
</Ellipse>
|
||||||
<TextBlock Grid.Row="0" Grid.Column="1" Padding="0">
|
<TextBlock Grid.Row="0" Grid.Column="1" Padding="0">
|
||||||
@ -81,17 +74,9 @@
|
|||||||
<Border Classes="card-separator" />
|
<Border Classes="card-separator" />
|
||||||
|
|
||||||
<Grid RowDefinitions="*,*,*" ColumnDefinitions="Auto,*">
|
<Grid RowDefinitions="*,*,*" ColumnDefinitions="Auto,*">
|
||||||
<Ellipse Grid.Row="0"
|
<Ellipse Grid.Row="0" Grid.Column="0" Grid.RowSpan="3" VerticalAlignment="Top" Height="75" Width="75" Margin="0 0 15 0">
|
||||||
Grid.Column="0"
|
|
||||||
Grid.RowSpan="3"
|
|
||||||
VerticalAlignment="Top"
|
|
||||||
Height="75"
|
|
||||||
Width="75"
|
|
||||||
Margin="0 0 15 0"
|
|
||||||
IsVisible="{CompiledBinding DarthAffeProfileImage, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"
|
|
||||||
RenderOptions.BitmapInterpolationMode="HighQuality">
|
|
||||||
<Ellipse.Fill>
|
<Ellipse.Fill>
|
||||||
<ImageBrush Source="{CompiledBinding DarthAffeProfileImage}" />
|
<ImageBrush il:ImageBrushLoader.Source="https://avatars.githubusercontent.com/u/1094841" />
|
||||||
</Ellipse.Fill>
|
</Ellipse.Fill>
|
||||||
</Ellipse>
|
</Ellipse>
|
||||||
<TextBlock Grid.Row="0" Grid.Column="1" Padding="0">
|
<TextBlock Grid.Row="0" Grid.Column="1" Padding="0">
|
||||||
@ -110,17 +95,9 @@
|
|||||||
<Border Classes="card-separator" />
|
<Border Classes="card-separator" />
|
||||||
|
|
||||||
<Grid RowDefinitions="*,*,*" ColumnDefinitions="Auto,*">
|
<Grid RowDefinitions="*,*,*" ColumnDefinitions="Auto,*">
|
||||||
<Ellipse Grid.Row="0"
|
<Ellipse Grid.Row="0" Grid.Column="0" Grid.RowSpan="3" VerticalAlignment="Top" Height="75" Width="75" Margin="0 0 15 0">
|
||||||
Grid.Column="0"
|
|
||||||
Grid.RowSpan="3"
|
|
||||||
VerticalAlignment="Top"
|
|
||||||
Height="75"
|
|
||||||
Width="75"
|
|
||||||
Margin="0 0 15 0"
|
|
||||||
IsVisible="{CompiledBinding DrMeteorProfileImage, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"
|
|
||||||
RenderOptions.BitmapInterpolationMode="HighQuality">
|
|
||||||
<Ellipse.Fill>
|
<Ellipse.Fill>
|
||||||
<ImageBrush Source="{CompiledBinding DrMeteorProfileImage}" />
|
<ImageBrush il:ImageBrushLoader.Source="https://avatars.githubusercontent.com/u/29486064" />
|
||||||
</Ellipse.Fill>
|
</Ellipse.Fill>
|
||||||
</Ellipse>
|
</Ellipse>
|
||||||
<TextBlock Grid.Row="0" Grid.Column="1" Padding="0">
|
<TextBlock Grid.Row="0" Grid.Column="1" Padding="0">
|
||||||
@ -139,17 +116,9 @@
|
|||||||
<Border Classes="card-separator" />
|
<Border Classes="card-separator" />
|
||||||
|
|
||||||
<Grid RowDefinitions="*,*,*" ColumnDefinitions="Auto,*">
|
<Grid RowDefinitions="*,*,*" ColumnDefinitions="Auto,*">
|
||||||
<Ellipse Grid.Row="0"
|
<Ellipse Grid.Row="0" Grid.Column="0" Grid.RowSpan="3" VerticalAlignment="Top" Height="75" Width="75" Margin="0 0 15 0">
|
||||||
Grid.Column="0"
|
|
||||||
Grid.RowSpan="3"
|
|
||||||
VerticalAlignment="Top"
|
|
||||||
Height="75"
|
|
||||||
Width="75"
|
|
||||||
Margin="0 0 15 0"
|
|
||||||
IsVisible="{CompiledBinding KaiProfileImage, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"
|
|
||||||
RenderOptions.BitmapInterpolationMode="HighQuality">
|
|
||||||
<Ellipse.Fill>
|
<Ellipse.Fill>
|
||||||
<ImageBrush Source="{CompiledBinding KaiProfileImage}" />
|
<ImageBrush il:ImageBrushLoader.Source="https://i.imgur.com/8mPWY1j.png" />
|
||||||
</Ellipse.Fill>
|
</Ellipse.Fill>
|
||||||
</Ellipse>
|
</Ellipse>
|
||||||
<TextBlock Grid.Row="0" Grid.Column="1" Padding="0">
|
<TextBlock Grid.Row="0" Grid.Column="1" Padding="0">
|
||||||
|
|||||||
@ -1,75 +1,15 @@
|
|||||||
using System;
|
using Artemis.Core;
|
||||||
using System.Reactive.Disposables;
|
using Artemis.UI.Shared.Routing;
|
||||||
using System.Reflection;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Artemis.Core;
|
|
||||||
using Artemis.UI.Shared;
|
|
||||||
using Avalonia.Media.Imaging;
|
|
||||||
using Flurl.Http;
|
|
||||||
using ReactiveUI;
|
|
||||||
|
|
||||||
namespace Artemis.UI.Screens.Settings;
|
namespace Artemis.UI.Screens.Settings;
|
||||||
|
|
||||||
public class AboutTabViewModel : ActivatableViewModelBase
|
public class AboutTabViewModel : RoutableScreen
|
||||||
{
|
{
|
||||||
private Bitmap? _darthAffeProfileImage;
|
|
||||||
private Bitmap? _drMeteorProfileImage;
|
|
||||||
private Bitmap? _kaiProfileImage;
|
|
||||||
private Bitmap? _robertProfileImage;
|
|
||||||
private string? _version;
|
|
||||||
|
|
||||||
public AboutTabViewModel()
|
public AboutTabViewModel()
|
||||||
{
|
{
|
||||||
DisplayName = "About";
|
DisplayName = "About";
|
||||||
this.WhenActivated((CompositeDisposable _) => Task.Run(Activate));
|
|
||||||
}
|
|
||||||
|
|
||||||
public string? Version
|
|
||||||
{
|
|
||||||
get => _version;
|
|
||||||
set => RaiseAndSetIfChanged(ref _version, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Bitmap? RobertProfileImage
|
|
||||||
{
|
|
||||||
get => _robertProfileImage;
|
|
||||||
set => RaiseAndSetIfChanged(ref _robertProfileImage, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Bitmap? DarthAffeProfileImage
|
|
||||||
{
|
|
||||||
get => _darthAffeProfileImage;
|
|
||||||
set => RaiseAndSetIfChanged(ref _darthAffeProfileImage, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Bitmap? DrMeteorProfileImage
|
|
||||||
{
|
|
||||||
get => _drMeteorProfileImage;
|
|
||||||
set => RaiseAndSetIfChanged(ref _drMeteorProfileImage, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
public Bitmap? KaiProfileImage
|
|
||||||
{
|
|
||||||
get => _kaiProfileImage;
|
|
||||||
set => RaiseAndSetIfChanged(ref _kaiProfileImage, value);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task Activate()
|
|
||||||
{
|
|
||||||
AssemblyInformationalVersionAttribute? versionAttribute = typeof(AboutTabViewModel).Assembly.GetCustomAttribute<AssemblyInformationalVersionAttribute>();
|
|
||||||
Version = $"Version {Constants.CurrentVersion}";
|
Version = $"Version {Constants.CurrentVersion}";
|
||||||
|
}
|
||||||
|
|
||||||
try
|
public string Version { get; }
|
||||||
{
|
|
||||||
RobertProfileImage = new Bitmap(await "https://avatars.githubusercontent.com/u/8858506".GetStreamAsync());
|
|
||||||
RobertProfileImage = new Bitmap(await "https://avatars.githubusercontent.com/u/8858506".GetStreamAsync());
|
|
||||||
DarthAffeProfileImage = new Bitmap(await "https://avatars.githubusercontent.com/u/1094841".GetStreamAsync());
|
|
||||||
DrMeteorProfileImage = new Bitmap(await "https://avatars.githubusercontent.com/u/29486064".GetStreamAsync());
|
|
||||||
KaiProfileImage = new Bitmap(await "https://i.imgur.com/8mPWY1j.png".GetStreamAsync());
|
|
||||||
}
|
|
||||||
catch (Exception)
|
|
||||||
{
|
|
||||||
// ignored, unluckyyyy
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@ -8,7 +8,7 @@ using Artemis.Core;
|
|||||||
using Artemis.Core.Services;
|
using Artemis.Core.Services;
|
||||||
using Artemis.UI.DryIoc.Factories;
|
using Artemis.UI.DryIoc.Factories;
|
||||||
using Artemis.UI.Screens.Device;
|
using Artemis.UI.Screens.Device;
|
||||||
using Artemis.UI.Shared;
|
using Artemis.UI.Shared.Routing;
|
||||||
using Artemis.UI.Shared.Services;
|
using Artemis.UI.Shared.Services;
|
||||||
using Avalonia.Threading;
|
using Avalonia.Threading;
|
||||||
using DynamicData;
|
using DynamicData;
|
||||||
@ -16,7 +16,7 @@ using ReactiveUI;
|
|||||||
|
|
||||||
namespace Artemis.UI.Screens.Settings;
|
namespace Artemis.UI.Screens.Settings;
|
||||||
|
|
||||||
public class DevicesTabViewModel : ActivatableViewModelBase
|
public class DevicesTabViewModel : RoutableScreen
|
||||||
{
|
{
|
||||||
private readonly IDeviceVmFactory _deviceVmFactory;
|
private readonly IDeviceVmFactory _deviceVmFactory;
|
||||||
private readonly IRgbService _rgbService;
|
private readonly IRgbService _rgbService;
|
||||||
|
|||||||
@ -13,8 +13,8 @@ using Artemis.Core.Services;
|
|||||||
using Artemis.UI.Screens.StartupWizard;
|
using Artemis.UI.Screens.StartupWizard;
|
||||||
using Artemis.UI.Services.Interfaces;
|
using Artemis.UI.Services.Interfaces;
|
||||||
using Artemis.UI.Services.Updating;
|
using Artemis.UI.Services.Updating;
|
||||||
using Artemis.UI.Shared;
|
|
||||||
using Artemis.UI.Shared.Providers;
|
using Artemis.UI.Shared.Providers;
|
||||||
|
using Artemis.UI.Shared.Routing;
|
||||||
using Artemis.UI.Shared.Services;
|
using Artemis.UI.Shared.Services;
|
||||||
using Artemis.UI.Shared.Services.Builders;
|
using Artemis.UI.Shared.Services.Builders;
|
||||||
using Avalonia.Threading;
|
using Avalonia.Threading;
|
||||||
@ -26,7 +26,7 @@ using Serilog.Events;
|
|||||||
|
|
||||||
namespace Artemis.UI.Screens.Settings;
|
namespace Artemis.UI.Screens.Settings;
|
||||||
|
|
||||||
public class GeneralTabViewModel : ActivatableViewModelBase
|
public class GeneralTabViewModel : RoutableScreen
|
||||||
{
|
{
|
||||||
private readonly IAutoRunProvider? _autoRunProvider;
|
private readonly IAutoRunProvider? _autoRunProvider;
|
||||||
private readonly IDebugService _debugService;
|
private readonly IDebugService _debugService;
|
||||||
|
|||||||
@ -25,13 +25,18 @@
|
|||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<ScrollViewer Grid.Row="1" Grid.Column="0" HorizontalScrollBarVisibility="Disabled" VerticalScrollBarVisibility="Auto" VerticalAlignment="Top">
|
<ScrollViewer Grid.Row="1" Grid.Column="0" HorizontalScrollBarVisibility="Disabled" VerticalScrollBarVisibility="Auto" VerticalAlignment="Top">
|
||||||
<ItemsRepeater ItemsSource="{CompiledBinding Plugins}" MaxWidth="1000" VerticalAlignment="Center">
|
<ItemsControl ItemsSource="{CompiledBinding Plugins}" MaxWidth="1000" VerticalAlignment="Center">
|
||||||
<ItemsRepeater.ItemTemplate>
|
<ItemsControl.ItemsPanel>
|
||||||
|
<ItemsPanelTemplate>
|
||||||
|
<VirtualizingStackPanel />
|
||||||
|
</ItemsPanelTemplate>
|
||||||
|
</ItemsControl.ItemsPanel>
|
||||||
|
<ItemsControl.ItemTemplate>
|
||||||
<DataTemplate x:DataType="plugins:PluginSettingsViewModel">
|
<DataTemplate x:DataType="plugins:PluginSettingsViewModel">
|
||||||
<ContentControl Content="{CompiledBinding}" Height="200"/>
|
<ContentControl Content="{CompiledBinding}" Height="200" />
|
||||||
</DataTemplate>
|
</DataTemplate>
|
||||||
</ItemsRepeater.ItemTemplate>
|
</ItemsControl.ItemTemplate>
|
||||||
</ItemsRepeater>
|
</ItemsControl>
|
||||||
|
|
||||||
</ScrollViewer>
|
</ScrollViewer>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|||||||
@ -9,18 +9,17 @@ using Artemis.Core;
|
|||||||
using Artemis.Core.Services;
|
using Artemis.Core.Services;
|
||||||
using Artemis.UI.DryIoc.Factories;
|
using Artemis.UI.DryIoc.Factories;
|
||||||
using Artemis.UI.Screens.Plugins;
|
using Artemis.UI.Screens.Plugins;
|
||||||
using Artemis.UI.Shared;
|
using Artemis.UI.Shared.Routing;
|
||||||
using Artemis.UI.Shared.Services;
|
using Artemis.UI.Shared.Services;
|
||||||
using Artemis.UI.Shared.Services.Builders;
|
using Artemis.UI.Shared.Services.Builders;
|
||||||
using Avalonia.ReactiveUI;
|
using Avalonia.ReactiveUI;
|
||||||
using Avalonia.Threading;
|
|
||||||
using DynamicData;
|
using DynamicData;
|
||||||
using DynamicData.Binding;
|
using DynamicData.Binding;
|
||||||
using ReactiveUI;
|
using ReactiveUI;
|
||||||
|
|
||||||
namespace Artemis.UI.Screens.Settings;
|
namespace Artemis.UI.Screens.Settings;
|
||||||
|
|
||||||
public class PluginsTabViewModel : ActivatableViewModelBase
|
public class PluginsTabViewModel : RoutableScreen
|
||||||
{
|
{
|
||||||
private readonly INotificationService _notificationService;
|
private readonly INotificationService _notificationService;
|
||||||
private readonly IPluginManagementService _pluginManagementService;
|
private readonly IPluginManagementService _pluginManagementService;
|
||||||
|
|||||||
@ -22,7 +22,7 @@ using StrawberryShake;
|
|||||||
|
|
||||||
namespace Artemis.UI.Screens.Settings;
|
namespace Artemis.UI.Screens.Settings;
|
||||||
|
|
||||||
public class ReleasesTabViewModel : RoutableScreen<ReleaseDetailsViewModel>
|
public class ReleasesTabViewModel : RoutableHostScreen<ReleaseDetailsViewModel>
|
||||||
{
|
{
|
||||||
private readonly ILogger _logger;
|
private readonly ILogger _logger;
|
||||||
private readonly IUpdateService _updateService;
|
private readonly IUpdateService _updateService;
|
||||||
|
|||||||
@ -3,22 +3,23 @@
|
|||||||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
|
||||||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
|
||||||
xmlns:updating="clr-namespace:Artemis.UI.Screens.Settings.Updating"
|
xmlns:updating="clr-namespace:Artemis.UI.Screens.Settings.Updating"
|
||||||
xmlns:avalonia="clr-namespace:Markdown.Avalonia;assembly=Markdown.Avalonia"
|
xmlns:shared="clr-namespace:Artemis.UI.Shared.Converters;assembly=Artemis.UI.Shared"
|
||||||
xmlns:avalonia1="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
|
xmlns:converters="clr-namespace:Artemis.UI.Converters"
|
||||||
xmlns:converters="clr-namespace:Artemis.UI.Shared.Converters;assembly=Artemis.UI.Shared"
|
xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
|
||||||
xmlns:converters1="clr-namespace:Artemis.UI.Converters"
|
xmlns:mdxaml="https://github.com/whistyun/Markdown.Avalonia.Tight"
|
||||||
mc:Ignorable="d" d:DesignWidth="1000" d:DesignHeight="1400"
|
mc:Ignorable="d" d:DesignWidth="1000" d:DesignHeight="1400"
|
||||||
x:Class="Artemis.UI.Screens.Settings.Updating.ReleaseDetailsView"
|
x:Class="Artemis.UI.Screens.Settings.Updating.ReleaseDetailsView"
|
||||||
x:DataType="updating:ReleaseDetailsViewModel">
|
x:DataType="updating:ReleaseDetailsViewModel">
|
||||||
<UserControl.Resources>
|
<UserControl.Resources>
|
||||||
<converters:BytesToStringConverter x:Key="BytesToStringConverter" />
|
<shared:BytesToStringConverter x:Key="BytesToStringConverter" />
|
||||||
<converters1:SubstringConverter x:Key="SubstringConverter" />
|
<converters:SubstringConverter x:Key="SubstringConverter" />
|
||||||
|
<converters:DateTimeConverter x:Key="DateTimeConverter" />
|
||||||
</UserControl.Resources>
|
</UserControl.Resources>
|
||||||
<UserControl.Styles>
|
<UserControl.Styles>
|
||||||
<Style Selector="Grid.info-container">
|
<Style Selector="Grid.info-container">
|
||||||
<Setter Property="Margin" Value="10" />
|
<Setter Property="Margin" Value="10" />
|
||||||
</Style>
|
</Style>
|
||||||
<Style Selector="avalonia1|MaterialIcon.info-icon">
|
<Style Selector="avalonia|MaterialIcon.info-icon">
|
||||||
<Setter Property="VerticalAlignment" Value="Top" />
|
<Setter Property="VerticalAlignment" Value="Top" />
|
||||||
<Setter Property="Margin" Value="0 3 10 0" />
|
<Setter Property="Margin" Value="0 3 10 0" />
|
||||||
</Style>
|
</Style>
|
||||||
@ -103,16 +104,16 @@
|
|||||||
<Border Classes="card-separator" />
|
<Border Classes="card-separator" />
|
||||||
<Grid Margin="-5 -10" ColumnDefinitions="*,*,*">
|
<Grid Margin="-5 -10" ColumnDefinitions="*,*,*">
|
||||||
<Grid Grid.Column="0" ColumnDefinitions="*,*" RowDefinitions="*,*,*" Classes="info-container" HorizontalAlignment="Left">
|
<Grid Grid.Column="0" ColumnDefinitions="*,*" RowDefinitions="*,*,*" Classes="info-container" HorizontalAlignment="Left">
|
||||||
<avalonia1:MaterialIcon Kind="Calendar" Grid.Column="0" Grid.RowSpan="2" Classes="info-icon" />
|
<avalonia:MaterialIcon Kind="Calendar" Grid.Column="0" Grid.RowSpan="2" Classes="info-icon" />
|
||||||
<TextBlock Grid.Column="1" Grid.Row="0" Classes="info-title">Release date</TextBlock>
|
<TextBlock Grid.Column="1" Grid.Row="0" Classes="info-title">Release date</TextBlock>
|
||||||
<TextBlock Grid.Column="1"
|
<TextBlock Grid.Column="1"
|
||||||
Grid.Row="1"
|
Grid.Row="1"
|
||||||
Classes="info-body"
|
Classes="info-body"
|
||||||
Text="{CompiledBinding Release.CreatedAt, StringFormat={}{0:g}, FallbackValue=Loading...}" />
|
Text="{CompiledBinding Release.CreatedAt, Converter={StaticResource DateTimeConverter}, FallbackValue=Loading...}" />
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<Grid Grid.Column="1" ColumnDefinitions="*,*" RowDefinitions="*,*" Classes="info-container" HorizontalAlignment="Center">
|
<Grid Grid.Column="1" ColumnDefinitions="*,*" RowDefinitions="*,*" Classes="info-container" HorizontalAlignment="Center">
|
||||||
<avalonia1:MaterialIcon Kind="Git" Grid.Column="0" Grid.RowSpan="2" Classes="info-icon" />
|
<avalonia:MaterialIcon Kind="Git" Grid.Column="0" Grid.RowSpan="2" Classes="info-icon" />
|
||||||
<TextBlock Grid.Column="1" Grid.Row="0" Classes="info-title">Source</TextBlock>
|
<TextBlock Grid.Column="1" Grid.Row="0" Classes="info-title">Source</TextBlock>
|
||||||
<TextBlock Grid.Column="1"
|
<TextBlock Grid.Column="1"
|
||||||
Grid.Row="1"
|
Grid.Row="1"
|
||||||
@ -123,7 +124,7 @@
|
|||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<Grid Grid.Column="2" ColumnDefinitions="*,*" RowDefinitions="*,*" Classes="info-container" HorizontalAlignment="Right">
|
<Grid Grid.Column="2" ColumnDefinitions="*,*" RowDefinitions="*,*" Classes="info-container" HorizontalAlignment="Right">
|
||||||
<avalonia1:MaterialIcon Kind="BoxOutline" Grid.Column="0" Grid.RowSpan="2" Classes="info-icon" />
|
<avalonia:MaterialIcon Kind="BoxOutline" Grid.Column="0" Grid.RowSpan="2" Classes="info-icon" />
|
||||||
<TextBlock Grid.Column="1" Grid.Row="0" Classes="info-title">File size</TextBlock>
|
<TextBlock Grid.Column="1" Grid.Row="0" Classes="info-title">File size</TextBlock>
|
||||||
<TextBlock Grid.Column="1"
|
<TextBlock Grid.Column="1"
|
||||||
Grid.Row="1"
|
Grid.Row="1"
|
||||||
@ -140,11 +141,11 @@
|
|||||||
<TextBlock Grid.Row="0" Classes="h5 no-margin">Release notes</TextBlock>
|
<TextBlock Grid.Row="0" Classes="h5 no-margin">Release notes</TextBlock>
|
||||||
<Border Grid.Row="1" Classes="card-separator" />
|
<Border Grid.Row="1" Classes="card-separator" />
|
||||||
|
|
||||||
<avalonia:MarkdownScrollViewer Grid.Row="2" Markdown="{CompiledBinding Release.Changelog}" MarkdownStyleName="FluentAvalonia">
|
<mdxaml:MarkdownScrollViewer Grid.Row="2" Markdown="{CompiledBinding Release.Changelog}" MarkdownStyleName="FluentAvalonia">
|
||||||
<avalonia:MarkdownScrollViewer.Styles>
|
<mdxaml:MarkdownScrollViewer.Styles>
|
||||||
<StyleInclude Source="/Styles/Markdown.axaml"/>
|
<StyleInclude Source="/Styles/Markdown.axaml"/>
|
||||||
</avalonia:MarkdownScrollViewer.Styles>
|
</mdxaml:MarkdownScrollViewer.Styles>
|
||||||
</avalonia:MarkdownScrollViewer>
|
</mdxaml:MarkdownScrollViewer>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Border>
|
</Border>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|||||||
@ -18,7 +18,7 @@ using StrawberryShake;
|
|||||||
|
|
||||||
namespace Artemis.UI.Screens.Settings.Updating;
|
namespace Artemis.UI.Screens.Settings.Updating;
|
||||||
|
|
||||||
public class ReleaseDetailsViewModel : RoutableScreen<ViewModelBase, ReleaseDetailsViewModelParameters>
|
public class ReleaseDetailsViewModel : RoutableScreen<ReleaseDetailsViewModelParameters>
|
||||||
{
|
{
|
||||||
private readonly ObservableAsPropertyHelper<long> _fileSize;
|
private readonly ObservableAsPropertyHelper<long> _fileSize;
|
||||||
private readonly ILogger _logger;
|
private readonly ILogger _logger;
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user