Merge branch 'development'

This commit is contained in:
Robert committed 2023-09-09 20:47:51 +02:00
commit bbadef7a9a
262 files changed
+7395 -1968

No files matched your search

+6 -24
View File
@@ -17,13 +17,12 @@ 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!)
{ {
_scripts = new ObservableCollection<ProfileScript>(); _scripts = new ObservableCollection<ProfileScript>();
_scriptConfigurations = new ObservableCollection<ScriptConfiguration>(); _scriptConfigurations = new ObservableCollection<ScriptConfiguration>();
Opacity = 0d; Opacity = 0d;
ShouldDisplay = true; ShouldDisplay = true;
Configuration = configuration; Configuration = configuration;
@@ -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>
@@ -105,7 +95,7 @@ public sealed class Profile : ProfileElement
profileScript.OnProfileUpdated(deltaTime); profileScript.OnProfileUpdated(deltaTime);
const double OPACITY_PER_SECOND = 1; const double OPACITY_PER_SECOND = 1;
if (ShouldDisplay && Opacity < 1) if (ShouldDisplay && Opacity < 1)
Opacity = Math.Clamp(Opacity + OPACITY_PER_SECOND * deltaTime, 0d, 1d); Opacity = Math.Clamp(Opacity + OPACITY_PER_SECOND * deltaTime, 0d, 1d);
if (!ShouldDisplay && Opacity > 0) if (!ShouldDisplay && Opacity > 0)
@@ -123,14 +113,14 @@ public sealed class Profile : ProfileElement
foreach (ProfileScript profileScript in Scripts) foreach (ProfileScript profileScript in Scripts)
profileScript.OnProfileRendering(canvas, canvas.LocalClipBounds); profileScript.OnProfileRendering(canvas, canvas.LocalClipBounds);
SKPaint? opacityPaint = null; SKPaint? opacityPaint = null;
bool applyOpacityLayer = Configuration.FadeInAndOut && Opacity < 1; bool applyOpacityLayer = Configuration.FadeInAndOut && Opacity < 1;
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;
@@ -147,16 +147,7 @@ public class ProfileConfiguration : BreakableModel, IStorageModel, IDisposable
get => _activationConditionMet; get => _activationConditionMet;
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);
@@ -129,7 +605,7 @@ internal class ProfileService : IProfileService
profileConfiguration.IsSuspended = true; profileConfiguration.IsSuspended = true;
} }
} }
// If suspension was changed, save the category // If suspension was changed, save the category
if (before != profileConfiguration.IsSuspended) if (before != profileConfiguration.IsSuspended)
SaveProfileCategory(profileConfiguration.Category); SaveProfileCategory(profileConfiguration.Category);
@@ -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; }
@@ -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; }
}
@@ -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);
}
+1 -1
View File
@@ -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 />
+1 -1
View File
@@ -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" />
@@ -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();
}
}
@@ -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);
}
@@ -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>
@@ -50,40 +26,7 @@ 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))
@@ -87,27 +87,24 @@ 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 screen.InternalOnNavigating(args, _cts.Token);
{ }
await routableScreen.InternalOnNavigating(args, _cts.Token); catch (Exception e)
} {
catch (Exception e) Cancel();
{ if (e is not TaskCanceledException)
Cancel(); _logger.Error(e, "Failed to navigate to {Path}", resolution.Path);
if (e is not TaskCanceledException)
_logger.Error(e, "Failed to navigate to {Path}", resolution.Path);
}
if (CancelIfRequested(args, "OnNavigating", screen))
return;
} }
if (screen is IRoutableScreen childScreen) if (CancelIfRequested(args, "OnNavigating", screen))
return;
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)
+15 -21
View File
@@ -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))
await routableScreen.InternalOnClosing(args);
if (args.Cancelled)
{
_logger.Debug("Navigation to {Path} cancelled during RequestClose by {Screen}", args.Path, screen.GetType().Name);
return false; return false;
}
if (routableScreen.InternalScreen == null) await screen.InternalOnClosing(args);
if (!args.Cancelled)
return true; return true;
return await RequestClose(routableScreen.InternalScreen, args); _logger.Debug("Navigation to {Path} cancelled during RequestClose by {Screen}", args.Path, screen.GetType().Name);
return false;
} }
private bool PathEquals(string path, bool allowPartialMatch) private bool PathEquals(string path, RouterNavigationOptions options)
{ {
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>
@@ -180,7 +180,7 @@ public class NotificationButtonBuilder
_action = action; _action = action;
return this; return this;
} }
/// <summary> /// <summary>
/// Changes action that is called when the button is clicked. /// Changes action that is called when the button is clicked.
/// </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(() => // Activate the profile if one was provided
{ if (profileConfiguration != null)
profileConfiguration.IsBeingEdited = true;
_moduleService.SetActivationOverride(profileConfiguration.Module);
_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,23 +223,23 @@ 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();
} }
public void ChangeFocusMode(ProfileEditorFocusMode focusMode) public void ChangeFocusMode(ProfileEditorFocusMode focusMode)
{ {
if (_focusModeSubject.Value == focusMode) if (_focusModeSubject.Value == focusMode)
@@ -411,10 +396,8 @@ 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);
} }
/// <inheritdoc /> /// <inheritdoc />
@@ -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" />
+9 -13
View File
@@ -23,15 +23,11 @@
</Border> </Border>
</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">
+27 -27
View File
@@ -39,15 +39,14 @@
<Button Classes="title-bar-button"> <Button Classes="title-bar-button">
<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>
@@ -109,28 +108,29 @@
<Style Selector="Button.title-bar-button:pointerover"> <Style Selector="Button.title-bar-button:pointerover">
<Setter Property="Background" Value="Red"></Setter> <Setter Property="Background" Value="Red"></Setter>
</Style> </Style>
<Style Selector="Button.danger">
<Setter Property="Background" Value="#D64848"></Setter>
</Style>
<Style Selector="Button.danger:pointerover">
<Style Selector="^ /template/ controls|FABorder#Root">
<Setter Property="Background" Value="#D65757"/>
<Setter Property="BorderBrush" Value="{DynamicResource ButtonBorderBrushPointerOver}" />
</Style>
</Style>
<Style Selector="ToggleButton:checked:pointerover /template/ Border#BorderElement"> <Style Selector="Button.danger:pressed">
<Setter Property="BorderBrush" Value="{DynamicResource ToggleButtonBorderBrushCheckedPointerOver}" /> <Style Selector="^ /template/ controls|FABorder#Root">
</Style> <Setter Property="Background" Value="#D64848" />
<Style Selector="ToggleButton:checked:pointerover /template/ ContentPresenter#PART_ContentPresenter"> <Setter Property="BorderBrush" Value="{DynamicResource ButtonBorderBrushPressed}" />
<Setter Property="Background" Value="{DynamicResource ToggleButtonBackgroundCheckedPointerOver}" /> </Style>
<Setter Property="TextBlock.Foreground" Value="{DynamicResource ToggleButtonForegroundCheckedPointerOver}" /> </Style>
</Style>
<Style Selector="ToggleButton:checked:pressed /template/ Border#BorderElement"> <Style Selector="Button.danger:disabled">
<Setter Property="BorderBrush" Value="{DynamicResource ToggleButtonBorderBrushCheckedPressed}" /> <Style Selector="^ /template/ controls|FABorder#Root">
</Style> <Setter Property="Background" Value="#D79D9C" />
<Style Selector="ToggleButton:checked:pressed /template/ ContentPresenter#PART_ContentPresenter"> <Setter Property="BorderBrush" Value="{DynamicResource ButtonBorderBrushDisabled}" />
<Setter Property="Background" Value="{DynamicResource ToggleButtonBackgroundCheckedPressed}" /> </Style>
<Setter Property="TextBlock.Foreground" Value="{DynamicResource ToggleButtonForegroundCheckedPressed}" /> </Style>
</Style>
<Style Selector="ToggleButton:checked:disabled /template/ Border#BorderElement">
<Setter Property="BorderBrush" Value="{DynamicResource ToggleButtonBorderBrushCheckedDisabled}" />
</Style>
<Style Selector="ToggleButton:checked:disabled /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Background" Value="{DynamicResource ToggleButtonBackgroundCheckedDisabled}" />
<Setter Property="TextBlock.Foreground" Value="{DynamicResource ToggleButtonForegroundCheckedDisabled}" />
</Style>
</Styles> </Styles>
@@ -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">
+31 -10
View File
@@ -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>
@@ -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;
}
}
@@ -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; }
}
+1 -1
View File
@@ -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 />
+28 -1
View File
@@ -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>
@@ -0,0 +1 @@
{"v":"5.7.1","fr":25,"ip":0,"op":200,"w":1000,"h":1000,"nm":"Work frome Home Mailing F","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":3,"nm":"Null 1","sr":1,"ks":{"o":{"a":0,"k":0,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[500,652,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"ip":0,"op":200,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"mail 4","parent":1,"td":1,"sr":1,"ks":{"o":{"a":0,"k":99,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[183.055,-315.211,0],"ix":2},"a":{"a":0,"k":[2153.478,680.843,0],"ix":1},"s":{"a":0,"k":[56,56,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-0.758,-2.403],[0,0],[0,0],[0,0],[-21.953,-7.476],[-0.627,-0.393]],"o":[[0,0],[0,0],[0,0],[0,0],[9.299,3.167],[1.978,1.239]],"v":[[78.735,-34.708],[80.678,-28.718],[-58.964,72.319],[-68.916,40.154],[58.006,-46.114],[74.435,-40.273]],"c":true},"ix":2},"nm":"Path 4","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.174548238516,0.452203959227,0.890196084976,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 3","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[1801.139,934.164],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[594,594],"ix":3},"r":{"a":0,"k":17.3,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"back 2","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":204,"st":-23,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"plane 2","parent":1,"tt":2,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":-30.27,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.57,"y":1},"o":{"x":0.43,"y":0},"t":0,"s":[-417.664,54.901,0],"to":[29.664,67.099,0],"ti":[8.659,75.131,0]},{"i":{"x":0.57,"y":1},"o":{"x":0.444,"y":0},"t":50,"s":[384.659,-254.869,0],"to":[-6.219,-53.959,0],"ti":[-29.664,-67.099,0]},{"i":{"x":0.57,"y":1},"o":{"x":0.43,"y":0},"t":100,"s":[-417.664,54.901,0],"to":[29.664,67.099,0],"ti":[8.659,75.131,0]},{"i":{"x":0.595,"y":1},"o":{"x":0.444,"y":0},"t":150,"s":[384.659,-254.869,0],"to":[-6.219,-53.959,0],"ti":[-17.336,-57.901,0]},{"t":199,"s":[-417.664,54.901,0]}],"ix":2},"a":{"a":0,"k":[816.438,755.113,0],"ix":1},"s":{"a":0,"k":[65,65,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":-7,"s":[{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[18.514,-6.941],[-33.276,21.927],[93.386,-0.562]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":0.167,"y":0.167},"t":3,"s":[{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[-6.915,-14.251],[-9.38,40.839],[47.51,-30.24]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.333,"y":0},"t":12,"s":[{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[21.986,-29.805],[113.019,32.241],[42.547,-64.73]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[-0.528,-14.328],[129.556,-5.673],[-49.243,-49.537]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":48,"s":[{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[12.419,-8.39],[20.033,-35.22],[-88.885,-12.543]],"c":true}]},{"i":{"x":0.32,"y":1},"o":{"x":0.167,"y":0.167},"t":61,"s":[{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[55.573,8.709],[-40.935,-18.354],[28.444,45.375]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":0.167,"y":0.167},"t":70,"s":[{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[6.11,3.203],[-110.008,-19.184],[7.061,50.133]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.333,"y":0},"t":84,"s":[{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[70.462,-13.025],[-68.13,-5.656],[131.7,43.826]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":93,"s":[{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[18.514,-6.941],[-33.276,21.927],[93.387,-0.562]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":0.167,"y":0.167},"t":103,"s":[{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[-6.915,-14.251],[-9.38,40.839],[47.51,-30.24]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.333,"y":0},"t":112,"s":[{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[21.986,-29.805],[113.019,32.241],[42.547,-64.73]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":130,"s":[{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[-0.528,-14.328],[129.556,-5.673],[-49.243,-49.537]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":148,"s":[{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[12.419,-8.39],[20.033,-35.22],[-88.885,-12.543]],"c":true}]},{"i":{"x":0.32,"y":1},"o":{"x":0.167,"y":0.167},"t":161,"s":[{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[55.573,8.709],[-40.935,-18.354],[28.444,45.375]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":0.167,"y":0.167},"t":170,"s":[{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[Line truncated
@@ -0,0 +1 @@
{"v":"4.7.0","fr":25,"ip":0,"op":50,"w":120,"h":120,"nm":"Comp 1","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"ruoi","ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.967]},"o":{"x":[0.167],"y":[0.033]},"n":["0p833_0p967_0p167_0p033"],"t":35,"s":[100],"e":[0]},{"t":49}]},"r":{"a":0,"k":0},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0,"y":0},"n":"0p833_0p833_0_0","t":0,"s":[57.361,61.016,0],"e":[57.699,41.796,0],"to":[-4.67500305175781,-4.12800598144531,0],"ti":[-13.9099960327148,5.27300262451172,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":10.219,"s":[57.699,41.796,0],"e":[79.084,33.982,0],"to":[12.8159942626953,-4.85800170898438,0],"ti":[-4.54498291015625,3.73400115966797,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"n":"0p833_0p833_0p167_0p167","t":19.445,"s":[79.084,33.982,0],"e":[59.691,9.121,0],"to":[6.61601257324219,-5.43799591064453,0],"ti":[20.0290069580078,1.20700073242188,0]},{"t":35}]},"a":{"a":0,"k":[60.531,10.945,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-0.994,0],[0,-0.994],[0.995,0],[0,0.994]],"o":[[0.995,0],[0,0.994],[-0.994,0],[0,-0.994]],"v":[[-0.001,-1.801],[1.801,-0.001],[-0.001,1.801],[-1.801,-0.001]],"c":true}},"nm":"Path 1","mn":"ADBE Vector Shape - Group"},{"ty":"fl","c":{"a":0,"k":[0.529,0.529,0.529,1]},"o":{"a":0,"k":100},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill"},{"ty":"tr","p":{"a":0,"k":[62.4,13.144],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"ix":1,"mn":"ADBE Vector Group"},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-1.422,0],[0,-1.422],[1.421,0],[0,1.422]],"o":[[1.421,0],[0,1.422],[-1.422,0],[0,-1.422]],"v":[[0.001,-2.574],[2.574,0],[0.001,2.574],[-2.574,0]],"c":true}},"nm":"Path 1","mn":"ADBE Vector Shape - Group"},{"ty":"st","c":{"a":0,"k":[0.529,0.529,0.529,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":0.7},"lc":1,"lj":1,"ml":10,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke"},{"ty":"fl","c":{"a":0,"k":[1,1,1,1]},"o":{"a":0,"k":100},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill"},{"ty":"tr","p":{"a":0,"k":[64.145,9.606],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":3,"cix":2,"ix":2,"mn":"ADBE Vector Group"},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[-1.996,0],[0,-1.996],[1.996,0],[0,1.996]],"o":[[1.996,0],[0,1.996],[-1.996,0],[0,-1.996]],"v":[[0,-3.614],[3.614,0],[0,3.614],[-3.614,0]],"c":true}},"nm":"Path 1","mn":"ADBE Vector Shape - Group"},{"ty":"st","c":{"a":0,"k":[0.529,0.529,0.529,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":0.7},"lc":1,"lj":1,"ml":10,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke"},{"ty":"fl","c":{"a":0,"k":[1,1,1,1]},"o":{"a":0,"k":100},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill"},{"ty":"tr","p":{"a":0,"k":[57.957,10.552],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 3","np":3,"cix":2,"ix":3,"mn":"ADBE Vector Group"},{"ty":"tr","p":{"a":0,"k":[60.531,10.941],"ix":2},"a":{"a":0,"k":[60.531,10.941],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"ruoi","np":3,"cix":2,"ix":1,"mn":"ADBE Vector Group"}],"ip":0,"op":50,"st":0,"bm":0,"sr":1},{"ddd":0,"ind":2,"ty":4,"nm":"Shape Layer 2","ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.967]},"o":{"x":[0.167],"y":[0.033]},"n":["0p833_0p967_0p167_0p033"],"t":35,"s":[100],"e":[0]},{"t":49}]},"r":{"a":0,"k":0},"p":{"a":0,"k":[-0.75,-0.75,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[-13.91,5.273],[-4.545,3.734],[20.029,1.207]],"o":[[-4.675,-4.128],[12.816,-4.858],[6.616,-5.438],[0,0]],"v":[[-7.383,24.76],[-7.046,5.54],[14.34,-2.273],[-3.178,-24.76]],"c":false}},"nm":"Path 1","mn":"ADBE Vector Shape - Group"},{"ty":"st","c":{"a":0,"k":[0.627,0.627,0.627,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":1},"lc":2,"lj":2,"d":[{"n":"d","nm":"dash","v":{"a":0,"k":2.028}},{"n":"g","nm":"gap","v":{"a":0,"k":2.028}},{"n":"o","nm":"offset","v":{"a":0,"k":0}}],"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke"},{"ty":"tr","p":{"a":0,"k":[67.87,37.631],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 6","np":2,"cix":2,"ix":1,"mn"Line truncated
@@ -0,0 +1 @@
{"v":"5.8.1","fr":30,"ip":0,"op":101,"w":500,"h":500,"nm":"Login_and_Sign_up ","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Group 1 :M","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[218.999,313.27,0],"ix":2,"l":2},"a":{"a":0,"k":[218.999,313.27,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[2.603,-3.021],[0.567,-0.343],[0.567,-0.3],[3.803,-0.729],[3.803,0.729],[0.567,0.343],[0.567,0.386],[2.624,2.956],[1.274,3.728],[-0.011,0.729],[-0.054,0.729],[-1.446,3.749],[-2.667,2.978],[-0.632,0.364],[-0.632,0.321],[-3.803,0.729],[-3.803,-0.729],[-0.653,-0.321],[-0.653,-0.364],[-2.667,-2.978],[-1.36,-3.728],[-0.032,-0.75],[0.011,-0.75],[1.317,-3.749]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.337254901961,0.666666666667,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[250.124,313.27],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":0,"k":[250.124,313.27],"ix":2},"a":{"a":0,"k":[250.124,313.27],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":63,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":64,"s":[100]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":84,"s":[100]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":364,"s":[100]},{"t":365,"s":[0]}],"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":1,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[2.603,-3.021],[0.568,-0.343],[0.568,-0.3],[3.802,-0.729],[3.802,0.729],[0.568,0.343],[0.568,0.386],[2.625,2.956],[1.275,3.728],[-0.01,0.729],[-0.053,0.729],[-1.446,3.749],[-2.667,2.978],[-0.632,0.364],[-0.632,0.321],[-3.802,0.729],[-3.802,-0.729],[-0.653,-0.321],[-0.653,-0.364],[-2.667,-2.978],[-1.36,-3.728],[-0.032,-0.75],[0.011,-0.75],[1.318,-3.749]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.337254901961,0.666666666667,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[241.231,313.27],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":0,"k":[241.231,313.27],"ix":2},"a":{"a":0,"k":[241.231,313.27],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":61,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":62,"s":[100]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":82,"s":[100]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":362,"s":[100]},{"t":363,"s":[0]}],"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 2","np":1,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0],[0,0]],"v":[[2.603,-3.021],[0.568,-0.343],[0.568,-0.3],[3.803,-0.729],[3.803,0.729],[0.568,0.343],[0.568,0.386],[2.625,2.956],[1.275,3.728],[-0.011,0.729],[-0.054,0.729],[-1.446,3.749],[-2.667,2.978],[-0.632,0.364],[-0.632,0.321],[-3.803,0.729],[-3.803,-0.729],[-0.653,-0.321],[-0.653,-0.364],[-2.667,-2.978],[-1.36,-3.728],[-0.032Line truncated
@@ -0,0 +1 @@
{"v":"5.7.8","fr":30,"ip":0,"op":301,"w":300,"h":300,"nm":"free 04","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":3,"nm":"Null 9","sr":1,"ks":{"o":{"a":0,"k":0,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[150,170,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[191,191,100],"ix":6,"l":2}},"ao":0,"ip":0,"op":301,"st":0,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Shapes on screen 2","parent":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[-26,94.218,0],"ix":2,"l":2},"a":{"a":0,"k":[0,0,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0.762],[0.762,0],[0,-0.762],[-0.762,0]],"o":[[0,-0.762],[-0.762,0],[0,0.762],[0.762,0]],"v":[[40.188,-146.941],[38.809,-148.32],[37.429,-146.941],[38.809,-145.561]],"c":true},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0,0.762],[-0.762,0],[0,-0.762],[0.762,0]],"o":[[0,-0.762],[0.762,0],[0,0.762],[-0.762,0]],"v":[[15.077,-146.941],[16.457,-148.32],[17.836,-146.941],[16.457,-145.561]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.807843148708,0.827450990677,0.972549021244,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 4","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[11.008,-159.04],[6.02,-164.027]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[9.345,-152.39],[2.695,-152.39]],"c":false},"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":103,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":107,"s":[0]},{"t":113,"s":[100],"h":1},{"t":130,"s":[0],"h":1},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":250,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":254,"s":[0]},{"t":260,"s":[100]}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":103,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":107,"s":[100]},{"t":113,"s":[100],"h":1},{"t":130,"s":[0],"h":1},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":250,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":254,"s":[100]},{"t":260,"s":[100]}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":3,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false},{"ty":"st","c":{"a":0,"k":[0.988235294819,0.729411780834,0.023529412225,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":4,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 5","np":1,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[27.633,-136.939],[27.633,-133.004]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.258823543787,0.243137255311,0.686274528503,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":2,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Group 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-1.894],[1.894,0],[0,1.894],[-1.894,0]],"o":[[0,1.894],[-1.894,0],[0,-1.894],[1.894,0]],"v":[[31.062,-139.433],[27.633,-136.003],[24.203,-139.433],[27.633,-142.862]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},Line truncated
@@ -0,0 +1 @@
{"v":"5.0.1","fr":29.9700012207031,"ip":0,"op":45.0000018328876,"w":512,"h":512,"nm":"Comp 1","ddd":0,"assets":[{"id":"comp_0","layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Shape Layer 1","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.572],"y":[0.556]},"o":{"x":[0.167],"y":[0.167]},"n":["0p572_0p556_0p167_0p167"],"t":7,"s":[100],"e":[92.154]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.641],"y":[0.056]},"n":["0p833_0p833_0p641_0p056"],"t":13,"s":[92.154],"e":[30]},{"t":17.0000006924242}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[256,256,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-230,4],[214,4]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.23137254902,0.741176470588,0.36862745098,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":70,"ix":5},"lc":2,"lj":1,"ml":4,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np":2,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"tm","s":{"a":1,"k":[{"i":{"x":[0.588],"y":[-51709.363]},"o":{"x":[0.167],"y":[0.167]},"n":["0p588_-51709p363_0p167_0p167"],"t":7,"s":[0],"e":[0]},{"i":{"x":[0.696],"y":[0.999]},"o":{"x":[0.509],"y":[0.003]},"n":["0p696_0p999_0p509_0p003"],"t":10,"s":[0],"e":[100]},{"t":16.0000006516934}],"ix":1},"e":{"a":1,"k":[{"i":{"x":[0.566],"y":[0.999]},"o":{"x":[0.457],"y":[0.063]},"n":["0p566_0p999_0p457_0p063"],"t":7,"s":[0],"e":[100]},{"t":16.0000006516934}],"ix":2},"o":{"a":0,"k":0,"ix":3},"m":1,"ix":2,"nm":"Trim Paths 1","mn":"ADBE Vector Filter - Trim","hd":false}],"ip":0,"op":150.000006109625,"st":0,"bm":0}]},{"id":"comp_1","layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Shape Layer 1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[256,256,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":1,"k":[{"i":{"x":[0,0],"y":[0.997,0.997]},"o":{"x":[0.167,0.167],"y":[0.167,0.167]},"n":["0_0p997_0p167_0p167","0_0p997_0p167_0p167"],"t":24,"s":[40,40],"e":[90,90]},{"i":{"x":[0.833,0.833],"y":[0.833,0.833]},"o":{"x":[0.574,0.574],"y":[-0.004,-0.004]},"n":["0p833_0p833_0p574_-0p004","0p833_0p833_0p574_-0p004"],"t":27,"s":[90,90],"e":[18.394,18.394]},{"t":38.0000015477717}],"ix":2},"p":{"a":0,"k":[0,0],"ix":3},"nm":"Ellipse Path 1","mn":"ADBE Vector Shape - Ellipse","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,1,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.167,"y":0.167},"n":"0p667_1_0p167_0p167","t":24,"s":[-181.074,-5.414],"e":[200,-5.414],"to":[34.0465698242188,0],"ti":[-26.72825050354,0]},{"t":38.0000015477717}],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":24,"s":[0],"e":[100]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":25,"s":[100],"e":[100]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"n":["0p833_0p833_0p167_0p167"],"t":29,"s":[100],"e":[0]},{"t":38.0000015477717}],"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Ellipse 1","np":2,"cix":2,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":150.000006109625,"st":0,"bm":0}]},{"id":"comp_2","layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Shape Layer 1","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.572],"y":[0.556]},"o":{"x":[0.167],"y":[0.167]},"n":["0p572_0p556_0p167_0p167"],"t":10,"s":[100],"e":[92.154]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.641],"y":[0.056]},"n":["0p833_0p833_0p641_0p056"],"t":16,"s":[92.154],"e":[30]},{"t":20.0000008146167}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[256,256,0],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":0,"k":[100,100,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[-230,4],[214,4]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.23137254902,0.741176470588,0.36862745098,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":70,"ix":5},"lc":2,"lj":1,"ml":4,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Shape 1","np"Line truncated
@@ -0,0 +1 @@
{"v": "5.5.7", "meta": {"g": "LottieFiles AE 0.1.20", "a": "", "k": "", "d": "", "tc": ""}, "fr": 24, "ip": 0, "op": 36, "w": 1600, "h": 1200, "nm": "graphic", "ddd": 0, "assets": [{"id": "image_0", "w": 163, "h": 319, "u": "", "p": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAKMAAAE/CAYAAAAqv3MMAAAgAElEQVR4Xu19a4xd13XeWmcoinLRSrVsaeYOVbk/CgRIC7m/8qMFpF+FESC2GjQ/UgQQlaZAAzsWhbSO7cTVOI6rSCQ1M/cxVCjHJEWbD/lBWX7bskRZsixZb9mJLNoW9eSMXqYoW5y5r72KtR/n7nPued73ubMHGHBmeO5j9vlm7b2+b61vIbiPTbMCh3e/ejUAXA1E7yeAK4gACADMv0LACwR4og3iwP/8xNyJUS8MjvoF3euNdgWOL565aKO1sRMQdwLAhQZ4/C6EDUbqfM8/FwQvCKCFP//ruQOjescOjKNa6RG/jgSh2NiJpEHIEZDfQyga+lGRf64BqcEowSoIzraJlgRtW7p+4V+/Ncxfw4FxmKs7puc+smv1KvSQI9rlBnwahxJw8msNShkdra/N920bmAJAfU8Hmx4sfGJh7vlh/GoOjMNY1TE+59Fb1paQ4DoTBX0Q5oiKDE4bjPJrCUiCtvr3ay2BSwt/P9hzpQPjGIEzyJfmbbkh6ncCwJUGgL1GRbNNGxDy9x0gggRki0HZpqcYlH9/y2DOlQ6Mg0TEmJ5LA5Gz3yt8IFqR0N6Wwxm0fU4MnxlNdNTRUEdHDUQNSAlKAS+0AQ4AbCwtLf3bns+VDoxjAtCgXjYMxLioGE5cwmfFqOSFQRgbFfU5stVW4ORjAQKcIYLjM2362z210gt5f0cHxrwrNkHXxwKx16jIdI8PQA1E+b05KwajIkdOHRnN34Dgp+BPBLxdgLh9ebn0QNYlc2DMulITeN0de9buJIAP2W/Npm/CUdLeog3HaCJkiM5RCUwgaVFnRbVlKxCaT52hMwj55dudT2wB0f2E+MXl5dljaUvowJi2QhP6/8f2rC4A4A1dQIyLiobOsXhGH4iaAA9HRblFC5Lg85OWEBj55xqEflQ0YESAFuOWCFpA8Dx4eP3y8uzDcUvqwDihYEt6W8wjeh7eG74mK50zzKjIAZVBqAGpgAjQBAXMBiIsLy/PrUT9fg6MBQOjPicy6XzhMKKiSljMebETFcPbs4mUOiqa7Zmjo9ymA1FRgxEAGoDQRIKvLpXnFsJL78BYMDAe2/3qAUC6pt+oGCP9qexZnxXVmVEnKdZWbZIWvlYnLBKMOioyGDkaBqIiADWBsAGITQJqAMCecnnuDvv3cGAsEBgTt+e8Z0VzToyU/YJnxQxR0Y+ICVGxCQgNIGAg8rbdEOT9aaVy6a/MLXBgLBAYj+1Ze9IQ21EZtMkkeiyI8KNiV+JioqUAaOqMOhQV5UNQbdHyvEjEgMMWArVUJMQmAjUFYB0Rm0DUIIJ/LlfmPuLAWCAQ8lu9Y8/qDgLcH7k9p0VFK5O2k5cAneMXQ6ioyP8nt+NQJm3oHOusaKIi/9sCwDYwABmMnLygjIIqKgr9L0CDgOoAWAeAW8rlOZmMuchYEFAe3bP2PHIVTugjawbN18VximEt2teh7TOjxSvqqCifUj9tICqq86KMhAqUCA2OigTyzNjQUbEOQHUEPLlcmfuYA2NBgJgrKuooKIMlnwczRkWTRZuoKLNla3u2Se4MUZF5RQahior8yWdFjo46KiInMwASkODBx5eXSy+6yFgAQB7bs8ZFELIaZ2hnxRjFxWTOBowJUTGQRSNAU0dFjpCNcFTUnGMdiDYA6Vvl8vxRB8YJB2NcBm2SlV7LxGKKZ7skP1t54eQlFBXNedE+K5qoyImMyZw5OvL5sEFEDVRnxToRk+BigxB/WS6XPuvAOOFgjNKfbSD2kkEbjjFQQGtxi1Hbs6F3bOnPyqCToyJnzwxAooZQ1E4dUW/RDFKijXJl/iMOjBMMxiOLq+/zBJ6KeotRBRFpLQWR9YopZWL2Nm1kbZO4BAoiVLLiR0UCqbSoLFqeE5ljpLokvmVk5GyaGJQbhLDRbDY/7cA4wWBMVFvi6JyIhiuTLfdRPCspnoiCCMkpMr/I4NM6NANSyn4+pUPUYBAKjohq664D0oZJYKiN6+DhTQ6MEwpGrUGfyRoVB1k8658TTakYM4fqjXRV5jCxDUCa5FZFEdFRUaovmlvsREUGpABcn5mBWx0YJxSMUSVi/lkxZ1T0I2JS8WwEuZ1UPGuqcnRUVHyiUl+k1OdTOrwdE6psWm7N5qyIdeDkhXADBGwIgH0OjBMIxrjKnLgMOqoXephlYqGzIkdFA8JAVJQRUmbQanuWUiDRBiE1kGBDnhcZjCDqOIOfc2CcQDDmIrmtrEJWco+reNaQ20r6UyS3pG64IEInLLJQgs+K/L2oE+A6R0X+vk1ivwPjBIKxX+kvT1TkMjGfygmViclGq+gysfjiWZlBqzIxmawoTZrPhZ0MmhQAAWBdghJldLzdgXHCwHiMzZmQjkeqLXFnxbiWgnCZWFez1XCKZwNRUVXsBM+KJDaAkM+JG8jbtAfrW5qtww6MkwbGUUh/Ay2eldl0U31KcpsrchqyTIwTFgF1/p6Ao59KYBiAxIBkkHqwTsQRsnXMgXGCwBhHcmtaxTdtkgEy7aw4xOJZXSameMVwmZhVPGvKxJCgQZJX1Fu1gHWOivw9IpxDEhvb3rX1gAPjBIHxy7tP/04bvYci+1uMYVPvbmLB4lnTC91jmViW4lkg2NCFtRwdNcmNdRBiXQNxAwjWOZHZsrXlwDhBWJRvhWmduqjvAICdXL+YKSr2WCY27OLZQJkYkExUiCkdYl5RJi3rTO8A4HqlUrrNRcZJQ6P1fpjiaQPuQOqYOfXVUhAuE+sxKppmq0xlYnI7VmdFPjeas6LHmTTBukDcEIKeW1kpfceBcYLBaN4al5EBejsA6Bpje2zOjSMonk0qE8tWPBuKigxOAXCOoyInMwLg8Wq19KgD45jBKIEGuAAeHfjjv0y2luMEh1reAgFdTQQXBvy4451ntXlTtF3JSIpntQQoZNYskxa5Nassmrdq+n6lsv1lB8axg3HtBKDehqWPNiw1t5x/4Nrr4y2L9y+euWhLQ/p07xAEl0c5z9rN+Kb/2TdwitieuThiUMWzrEUrqY81atlWsK6KaXGDM2d1XoR13qrbBOvN5tzRffu4Z8Z9jG0F7rj5tfe3PfFEWHMmgrMEcKDVpqVrP5FsWXz7zas72gJ3EsEV4VKxkRbPcpkYYJ2LZ7Xy4p8VWQIkgbJu0URF5OIIFOcQ8ZeVSul+XgMHxrFBEeAIu0OAcoewpxAEDT3xoEBcuvZjl3DPdOzHbTeuXgWEC0RwZWbn2QiPxYQyMeMQkVAmFl88awoiiJCjpMyihYBz57Xx/qVb1R+cA+OYwMjnP2zjKXnus2Q+/xwYntFCcB8RLv3ZJy9lq+TYj703rr4PWt6CILrGdp5N9Fhsq+rYYRbPclTkyNnJolmB8d5uNGbv5C3agXFMQOSXPbp7bYkArovYogODgkwtomk7NfNZNprb7kwahbG4cOYiDzd2CsKdbUEXmh4W47FoOEbZ9ddH8SyXickaRb09qwjInCI1JPD0WZFQbCBv1fwzPkO2cb0N8MzKytwj5ja4yDgGQEpiu11/nrSTWFja47dk2xzH+G6fFURLrQzzWW7+1OqONsGCEHC5nT1nKJ6NchMzdI4qoNUNVmnFs0x4MzgZlCqLhvVG44Jv7tv37rMOjGMAoXnJI7tWFwCV0WcSie1HxQQ3CGU/ggebnkidz/KZT5y+ui0jJVyZzXlWeecMongWCNc5KsokRuA58uDFajU4usNFxjGA8ujuNY6K0qokLnExNYld27QuAwskKUZnBriv3YKFT6XMZ/nk/3n5/ULM7GwKuCaz82wPxbNcJqaioKrOkZwib9UE6/UWfHffvtIb9vI7MI4YjEf2rO4AUgZOWaNimpeiTE58X0U196/dhoXP7Eom0XfuXH0fALDc+FEC+FeWx+JAimdZ+lOtqAaUXC7G2TS+WavNfiO89A6MIwZjICqGfHGiFBUrcTGz/NS/Vr+zIbV9QHbcw15oERxoieT5LDt3nrqIaNsHEehvAHA+q8ciys6+bMWzTHbLrVrq0fBALWI0hwPjCMEoZ/oh3htL54QqttNcw4yPYsdtNjiNwLSc8jBKLggChE8vLSWT6Nddd/pPAPCPEeD31FlxMMWzWoM+FxcVHbUzQiDySx3ZZUl/NrcY03hvbOyiQGkPDLLtjn1fRW3qaQYG+XbHBAcJ6eDycumHSb/+zp2v/CcQ+EdE+IfBhvyO82xc8SyXiflnRRBS9jNbtRDw2MpK6Zmo13aRcUSAZOlPeOIJU59o0zmm1TQDndMpelBTTv25LJE8ot6uo8yaCOA+j/DQYnn2cNIS/MVfvLx9Br1rCPAPEGhblMeiqeAmXb0tz4qsQzPRrYtn5b8e/abRmLvLkNzuzDgi8IVfJiD95YiKkWPU/DNjMHGJIrR1d59v7KkjpD+RQAA8D0B7hTj/WKVy8dtxy/Oxj73+L+vrzf8O4H0AkN7NUTFP8SxHSkTx00pl+1Nxr+Ei4wjAaUt/SXRO2F22K3mxaB3jLhs+L4art2Ms7LTlscUjEpwBwH3eluZXFhcveyVpWT760dMfAMA/IILtmKF4FgWcEyg2mk28a9++0jkHxhGALu4lWPoDUDOg0+icLHbHtgF8YDxGqDQsbOxpR0XLe1uOyVCfusMP8aszrdahW2rbTyYt2/UfXv1d4cEHAMS/V73PwN2A66p4lrdpOsfbNZeJIcIzXECb9HwuMg4ZpF3SX8ILine truncated
@@ -0,0 +1 @@
{"v":"5.7.6","fr":25,"ip":0,"op":150,"w":320,"h":200,"nm":"03_Expertise 02","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Line top","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":12,"s":[0]},{"t":13,"s":[100]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[160,26.042,0],"ix":2,"l":2},"a":{"a":0,"k":[150,176.042,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0,"y":1},"o":{"x":0.167,"y":0},"t":12,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[149.984,176.037],[150.031,176.037]],"c":false}]},{"i":{"x":0.833,"y":1},"o":{"x":0.167,"y":0},"t":22,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[105,176.074],[195.028,176.085]],"c":false}]},{"t":32,"s":[{"i":[[0,0],[0,0]],"o":[[0,0],[0,0]],"v":[[149.984,176.037],[150.031,176.037]],"c":false}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.125,0.113999998803,0.113999998803,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":4,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false}],"ip":12,"op":32,"st":12,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Picto check","parent":6,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[55,161,0],"ix":2,"l":2},"a":{"a":0,"k":[55,161,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[0,0],[0,0]],"o":[[0,0],[0,0],[0,0]],"v":[[43.023,137.816],[50.776,145.569],[67.023,129.321]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[1,1,1,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":5,"ix":5},"lc":1,"lj":1,"ml":10,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false}],"ip":20,"op":150,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"Right tube stroke","parent":6,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[55,161,0],"ix":2,"l":2},"a":{"a":0,"k":[55,161,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[7.72,3.989],[0,0],[0,0],[0,0],[0,-9.289],[-13.255,0],[0,13.255]],"o":[[0,0],[0,0],[0,0],[-7.719,3.989],[0,13.255],[13.255,0],[0,-9.289]],"v":[[11,-3.828],[11,-41.5],[-11,-41.5],[-11,-3.828],[-24,17.5],[0,41.5],[24,17.5]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0.125,0.113999998803,0.113999998803,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":4,"ix":5},"lc":1,"lj":1,"ml":10,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"tr","p":{"a":0,"k":[55,119.5],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":0,"ix":6},"o":{"a":0,"k":100,"ix":7},"sk":{"a":0,"k":0,"ix":4},"sa":{"a":0,"k":0,"ix":5},"nm":"Transform"}],"nm":"Glass stroke","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":40,"op":190,"st":40,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"Liquid green mask","parent":6,"td":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[55,161,0],"ix":2,"l":2},"a":{"a":0,"k":[55,161,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[7.72,3.989],[0,0],[0,0],[0,0],[0,-9.289],[-13.255,0],[0,13.255]],"o":[[0,0],[0,0],[0,0],[-7.719,3.989],[0,13.255],[13.255,0],[0,-9.289]],"v":[[66,115.672],[66,78],[44,78],[44,115.672],[31,137],[55,161],[79,137]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.779999976065,0.922000002394,0.976000019148,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 2","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"fl","c":{"a":0,"k":[0.779999976065,0.922000002394,0.976000019148,1],"ix":4},"o":{"a":0,"k":100,"ix":5},"r":1,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false}],"ip":70,"op":220,"st":70,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":"Liquid green","tt":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[265,161,0],"ix":2,"l":2},"a":{"a":0,"k":[0.49,27.328,0],"ix":1,"l":2},"s":{"a":0,"k":[100,100,100],"ix":6,"l":2}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":1,"k":[{"i":{"x":[0.667,0.667],"y":[1,1]},"o":{"x":[0.333,0.333],"y":[0,0]},"t":70,"s":[80,0]},{"t":100,"s":[80,74]}],"ix":2},"p":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":70,"s":[0,27],"to":[0,-6.167],"ti":[0,6.167]},{"t":100,"s":[0,-10]}],"ix":3},"r":{"a":0,"k":0,"ix":4},"nm":"Rectangle Path 1","mn":"ADBE Vector Shape - Rect","hd":false},{"ty":"st","c":{"a":0,"k":[0.125490196078,0.113725490196,0.113725490196,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":4,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector GrLine truncated
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -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;
}
}
@@ -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
View 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());
}
}
@@ -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)));
}
}
@@ -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();
} }
} }
+3 -1
View File
@@ -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
View 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;
}
}
+28 -7
View File
@@ -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<ProfileListViewModel>("profiles/{page:int}"), new RouteRegistration<WorkshopOfflineViewModel>("offline/{message:string}"),
new RouteRegistration<ProfileDetailsViewModel>("profiles/{entryId:guid}"), new RouteRegistration<EntriesViewModel>("entries")
new RouteRegistration<LayoutListViewModel>("layouts/{page:int}"), {
new RouteRegistration<LayoutDetailsViewModel>("layouts/{entryId:guid}") Children = new List<IRouterRegistration>
{
new RouteRegistration<ProfileListViewModel>("profiles/{page:int}"),
new RouteRegistration<ProfileDetailsViewModel>("profiles/details/{entryId:long}"),
#if DEBUG
new RouteRegistration<LayoutListViewModel>("layouts/{page:int}"),
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>
+2 -1
View File
@@ -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;
@@ -19,7 +19,7 @@ public partial class VisualEditorView : ReactiveUserControl<VisualEditorViewMode
public VisualEditorView() public VisualEditorView()
{ {
InitializeComponent(); InitializeComponent();
ZoomBorder.PropertyChanged += ZoomBorderOnPropertyChanged; ZoomBorder.PropertyChanged += ZoomBorderOnPropertyChanged;
ZoomBorder.PointerMoved += ZoomBorderOnPointerMoved; ZoomBorder.PointerMoved += ZoomBorderOnPointerMoved;
ZoomBorder.PointerWheelChanged += ZoomBorderOnPointerWheelChanged; ZoomBorder.PointerWheelChanged += ZoomBorderOnPointerWheelChanged;
@@ -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
{ {
+5 -7
View File
@@ -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">
<controls:Frame.NavigationPageFactory> <Setter Property="CornerRadius" Value="8 0 0 0" />
<ui:PageFactory/> </Style>
</controls:Frame.NavigationPageFactory> </Styles>
</controls:Frame> </controls:NavigationView.Styles>
</Grid>
</Border> <controls:Frame Name="TabFrame" IsNavigationStackEnabled="False" CacheSize="0" Padding="20">
<controls:Frame.NavigationPageFactory>
<ui:PageFactory/>
</controls:Frame.NavigationPageFactory>
</controls:Frame>
</controls:NavigationView>
</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,20 +5,21 @@
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">
<ScrollViewer VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Disabled"> <ScrollViewer VerticalScrollBarVisibility="Auto" HorizontalScrollBarVisibility="Disabled">
<StackPanel Margin="15" MaxWidth="800"> <StackPanel Margin="15" MaxWidth="800">
<Grid RowDefinitions="*,*" ColumnDefinitions="Auto,*,Auto"> <Grid RowDefinitions="*,*" ColumnDefinitions="Auto,*,Auto">
<Image Grid.Column="0" <Image Grid.Column="0"
Grid.RowSpan="2" Grid.RowSpan="2"
Width="65" Width="65"
Height="65" Height="65"
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
{
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
}
} }
public string Version { get; }
} }
@@ -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,14 +25,19 @@
</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>
</UserControl> </UserControl>
@@ -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;
Loaded 100 of 262 files, more files were not shown because too many files have changed in this diff. Show more