diff --git a/src/Artemis.Core/Constants.cs b/src/Artemis.Core/Constants.cs index dfa7791c5..5699dbac4 100644 --- a/src/Artemis.Core/Constants.cs +++ b/src/Artemis.Core/Constants.cs @@ -40,7 +40,11 @@ public static class Constants /// /// The full path to the Artemis data folder /// +#if DEBUG + public static readonly string DataFolder = Path.Combine(BaseFolder, "Artemis-dev"); +#else public static readonly string DataFolder = Path.Combine(BaseFolder, "Artemis"); +#endif /// /// The full path to the Artemis logs folder @@ -140,6 +144,11 @@ public static class Constants /// Gets the startup arguments provided to the application /// public static ReadOnlyCollection StartupArguments { get; set; } = null!; + + public static string? GetStartupRoute() + { + return StartupArguments.FirstOrDefault(a => a.StartsWith("--route=artemis://"))?.Split("--route=artemis://")[1]; + } internal static readonly CorePluginFeature CorePluginFeature = new() {Plugin = CorePlugin, Profiler = CorePlugin.GetProfiler("Feature - Core")}; internal static readonly EffectPlaceholderPlugin EffectPlaceholderPlugin = new() {Plugin = CorePlugin, Profiler = CorePlugin.GetProfiler("Feature - Effect Placeholder")}; diff --git a/src/Artemis.Core/Plugins/IPluginConfigurationDialog.cs b/src/Artemis.Core/Plugins/IPluginConfigurationDialog.cs index 7df4491bb..d75ed35be 100644 --- a/src/Artemis.Core/Plugins/IPluginConfigurationDialog.cs +++ b/src/Artemis.Core/Plugins/IPluginConfigurationDialog.cs @@ -11,4 +11,10 @@ public interface IPluginConfigurationDialog /// The type of view model the tab contains /// Type Type { get; } + + /// + /// A value indicating whether it's mandatory to configure this plugin. + /// If set to , the dialog will open the first time the plugin is enabled. + /// + bool IsMandatory { get; } } \ No newline at end of file diff --git a/src/Artemis.Core/Services/CoreService.cs b/src/Artemis.Core/Services/CoreService.cs index ccbbff9ea..f6716082e 100644 --- a/src/Artemis.Core/Services/CoreService.cs +++ b/src/Artemis.Core/Services/CoreService.cs @@ -63,7 +63,6 @@ internal class CoreService : ICoreService _logger.Debug("Forcing plugins to use HidSharp {HidSharpVersion}", hidSharpVersion); // Initialize the services - _pluginManagementService.CopyBuiltInPlugins(); _pluginManagementService.LoadPlugins(IsElevated); _pluginManagementService.StartHotReload(); _renderService.Initialize(); diff --git a/src/Artemis.Core/Services/DeviceService.cs b/src/Artemis.Core/Services/DeviceService.cs index 54dfd34a2..aaa27c59b 100644 --- a/src/Artemis.Core/Services/DeviceService.cs +++ b/src/Artemis.Core/Services/DeviceService.cs @@ -157,10 +157,11 @@ internal class DeviceService : IDeviceService } } + /// /// - public void AutoArrangeDevices() + public void AutoArrangeDevices(bool leftHanded) { - SurfaceArrangement surfaceArrangement = SurfaceArrangement.GetDefaultArrangement(); + SurfaceArrangement surfaceArrangement = SurfaceArrangement.GetDefaultArrangement(leftHanded); surfaceArrangement.Arrange(_devices); foreach (ArtemisDevice artemisDevice in _devices) artemisDevice.ApplyDefaultCategories(); diff --git a/src/Artemis.Core/Services/Interfaces/IDeviceService.cs b/src/Artemis.Core/Services/Interfaces/IDeviceService.cs index 6594535fd..eb611d613 100644 --- a/src/Artemis.Core/Services/Interfaces/IDeviceService.cs +++ b/src/Artemis.Core/Services/Interfaces/IDeviceService.cs @@ -46,7 +46,8 @@ public interface IDeviceService : IArtemisService /// /// Applies auto-arranging logic to the surface /// - void AutoArrangeDevices(); + /// + void AutoArrangeDevices(bool leftHanded); /// /// Apples the best available to the provided diff --git a/src/Artemis.Core/Services/Interfaces/IPluginManagementService.cs b/src/Artemis.Core/Services/Interfaces/IPluginManagementService.cs index 806526773..b6017a5c6 100644 --- a/src/Artemis.Core/Services/Interfaces/IPluginManagementService.cs +++ b/src/Artemis.Core/Services/Interfaces/IPluginManagementService.cs @@ -21,13 +21,12 @@ public interface IPluginManagementService : IArtemisService, IDisposable /// Indicates whether or not plugins are currently being loaded /// bool LoadingPlugins { get; } - + /// - /// Copy built-in plugins from the executable directory to the plugins directory if the version is higher - /// (higher or equal if compiled as debug) + /// Indicates whether or not plugins are currently loaded /// - void CopyBuiltInPlugins(); - + bool LoadedPlugins { get; } + /// /// Loads all installed plugins. If plugins already loaded this will reload them all /// @@ -150,12 +149,7 @@ public interface IPluginManagementService : IArtemisService, IDisposable /// /// DeviceProvider GetDeviceProviderByDevice(IRGBDevice device); - - /// - /// Occurs when built-in plugins are being loaded - /// - event EventHandler CopyingBuildInPlugins; - + /// /// Occurs when a plugin has started loading /// diff --git a/src/Artemis.Core/Services/PluginManagementService.cs b/src/Artemis.Core/Services/PluginManagementService.cs index 346d82a88..f65a9d858 100644 --- a/src/Artemis.Core/Services/PluginManagementService.cs +++ b/src/Artemis.Core/Services/PluginManagementService.cs @@ -46,114 +46,8 @@ internal class PluginManagementService : IPluginManagementService public List AdditionalPluginDirectories { get; } = new(); public bool LoadingPlugins { get; private set; } - - - #region Built in plugins - - public void CopyBuiltInPlugins() - { - OnCopyingBuildInPlugins(); - DirectoryInfo pluginDirectory = new(Constants.PluginsFolder); - - if (Directory.Exists(Path.Combine(pluginDirectory.FullName, "Artemis.Plugins.Modules.Overlay-29e3ff97"))) - Directory.Delete(Path.Combine(pluginDirectory.FullName, "Artemis.Plugins.Modules.Overlay-29e3ff97"), true); - if (Directory.Exists(Path.Combine(pluginDirectory.FullName, "Artemis.Plugins.DataModelExpansions.TestData-ab41d601"))) - Directory.Delete(Path.Combine(pluginDirectory.FullName, "Artemis.Plugins.DataModelExpansions.TestData-ab41d601"), true); - - // Iterate built-in plugins - DirectoryInfo builtInPluginDirectory = new(Path.Combine(Constants.ApplicationFolder, "Plugins")); - if (!builtInPluginDirectory.Exists) - { - _logger.Warning("No built-in plugins found at {pluginDir}, skipping CopyBuiltInPlugins", builtInPluginDirectory.FullName); - return; - } - - - foreach (FileInfo zipFile in builtInPluginDirectory.EnumerateFiles("*.zip")) - { - try - { - ExtractBuiltInPlugin(zipFile, pluginDirectory); - } - catch (Exception e) - { - _logger.Error(e, "Failed to copy built-in plugin from {ZipFile}", zipFile.FullName); - } - } - } - - private void ExtractBuiltInPlugin(FileInfo zipFile, DirectoryInfo pluginDirectory) - { - // Find the metadata file in the zip - using ZipArchive archive = ZipFile.OpenRead(zipFile.FullName); - - ZipArchiveEntry? metaDataFileEntry = archive.Entries.FirstOrDefault(e => e.Name == "plugin.json"); - if (metaDataFileEntry == null) - throw new ArtemisPluginException("Couldn't find a plugin.json in " + zipFile.FullName); - - using StreamReader reader = new(metaDataFileEntry.Open()); - PluginInfo builtInPluginInfo = CoreJson.Deserialize(reader.ReadToEnd())!; - string preferred = builtInPluginInfo.PreferredPluginDirectory; - - // Find the matching plugin in the plugin folder - DirectoryInfo? match = pluginDirectory.EnumerateDirectories().FirstOrDefault(d => d.Name == preferred); - if (match == null) - { - CopyBuiltInPlugin(archive, preferred); - } - else - { - string metadataFile = Path.Combine(match.FullName, "plugin.json"); - if (!File.Exists(metadataFile)) - { - _logger.Debug("Copying missing built-in plugin {builtInPluginInfo}", builtInPluginInfo); - CopyBuiltInPlugin(archive, preferred); - } - else if (metaDataFileEntry.LastWriteTime > File.GetLastWriteTime(metadataFile)) - { - try - { - _logger.Debug("Copying updated built-in plugin {builtInPluginInfo}", builtInPluginInfo); - CopyBuiltInPlugin(archive, preferred); - } - catch (Exception e) - { - throw new ArtemisPluginException($"Failed to install built-in plugin: {e.Message}", e); - } - } - } - } - - private void CopyBuiltInPlugin(ZipArchive zipArchive, string targetDirectory) - { - ZipArchiveEntry metaDataFileEntry = zipArchive.Entries.First(e => e.Name == "plugin.json"); - DirectoryInfo pluginDirectory = new(Path.Combine(Constants.PluginsFolder, targetDirectory)); - bool createLockFile = File.Exists(Path.Combine(pluginDirectory.FullName, "artemis.lock")); - - // Remove the old directory if it exists - if (Directory.Exists(pluginDirectory.FullName)) - pluginDirectory.Delete(true); - - // Extract everything in the same archive directory to the unique plugin directory - Utilities.CreateAccessibleDirectory(pluginDirectory.FullName); - string metaDataDirectory = metaDataFileEntry.FullName.Replace(metaDataFileEntry.Name, ""); - foreach (ZipArchiveEntry zipArchiveEntry in zipArchive.Entries) - { - if (zipArchiveEntry.FullName.StartsWith(metaDataDirectory) && !zipArchiveEntry.FullName.EndsWith("/")) - { - string target = Path.Combine(pluginDirectory.FullName, zipArchiveEntry.FullName.Remove(0, metaDataDirectory.Length)); - // Create folders - Utilities.CreateAccessibleDirectory(Path.GetDirectoryName(target)!); - // Extract files - zipArchiveEntry.ExtractToFile(target); - } - } - - if (createLockFile) - File.Create(Path.Combine(pluginDirectory.FullName, "artemis.lock")).Close(); - } - - #endregion + + public bool LoadedPlugins { get; private set; } public List GetAllPlugins() { @@ -328,8 +222,10 @@ internal class PluginManagementService : IPluginManagementService // ReSharper restore InconsistentlySynchronizedField LoadingPlugins = false; + LoadedPlugins = true; } + public void UnloadPlugins() { // Unload all plugins @@ -686,7 +582,7 @@ internal class PluginManagementService : IPluginManagementService if (removeSettings) RemovePluginSettings(plugin); - + OnPluginRemoved(new PluginEventArgs(plugin)); } @@ -893,7 +789,7 @@ internal class PluginManagementService : IPluginManagementService { PluginDisabled?.Invoke(this, e); } - + protected virtual void OnPluginRemoved(PluginEventArgs e) { PluginRemoved?.Invoke(this, e); diff --git a/src/Artemis.Core/Services/Storage/Models/SurfaceArrangement.cs b/src/Artemis.Core/Services/Storage/Models/SurfaceArrangement.cs index 9f4e78fe1..24cf3ba1d 100644 --- a/src/Artemis.Core/Services/Storage/Models/SurfaceArrangement.cs +++ b/src/Artemis.Core/Services/Storage/Models/SurfaceArrangement.cs @@ -48,22 +48,42 @@ internal class SurfaceArrangement } } - internal static SurfaceArrangement GetDefaultArrangement() + internal static SurfaceArrangement GetDefaultArrangement(bool leftHanded) { SurfaceArrangement arrangement = new(); - SurfaceArrangementType keypad = arrangement.AddType(RGBDeviceType.Keypad, 1); - keypad.AddConfiguration(new SurfaceArrangementConfiguration(null, HorizontalArrangementPosition.Equal, VerticalArrangementPosition.Equal, 20)); + SurfaceArrangementType keyboard, keypad, mousepad, mouse; + if (leftHanded) + { + mousepad = arrangement.AddType(RGBDeviceType.Mousepad, 1); + mousepad.AddConfiguration(new SurfaceArrangementConfiguration(null, HorizontalArrangementPosition.Right, VerticalArrangementPosition.Equal, 10)); - SurfaceArrangementType keyboard = arrangement.AddType(RGBDeviceType.Keyboard, 1); - keyboard.AddConfiguration(new SurfaceArrangementConfiguration(keypad, HorizontalArrangementPosition.Right, VerticalArrangementPosition.Equal, 20)); + mouse = arrangement.AddType(RGBDeviceType.Mouse, 2); + mouse.AddConfiguration(new SurfaceArrangementConfiguration(mousepad, HorizontalArrangementPosition.Center, VerticalArrangementPosition.Center, 0)); + mouse.AddConfiguration(new SurfaceArrangementConfiguration(null, HorizontalArrangementPosition.Right, VerticalArrangementPosition.Center, 10)); - SurfaceArrangementType mousepad = arrangement.AddType(RGBDeviceType.Mousepad, 1); - mousepad.AddConfiguration(new SurfaceArrangementConfiguration(keyboard, HorizontalArrangementPosition.Right, VerticalArrangementPosition.Equal, 10)); + keyboard = arrangement.AddType(RGBDeviceType.Keyboard, 1); + keyboard.AddConfiguration(new SurfaceArrangementConfiguration(mousepad, HorizontalArrangementPosition.Right, VerticalArrangementPosition.Equal, 10)); + keyboard.AddConfiguration(new SurfaceArrangementConfiguration(mouse, HorizontalArrangementPosition.Right, VerticalArrangementPosition.Equal, 100)); - SurfaceArrangementType mouse = arrangement.AddType(RGBDeviceType.Mouse, 2); - mouse.AddConfiguration(new SurfaceArrangementConfiguration(mousepad, HorizontalArrangementPosition.Center, VerticalArrangementPosition.Center, 0)); - mouse.AddConfiguration(new SurfaceArrangementConfiguration(keyboard, HorizontalArrangementPosition.Right, VerticalArrangementPosition.Center, 100)); + keypad = arrangement.AddType(RGBDeviceType.Keypad, 1); + keypad.AddConfiguration(new SurfaceArrangementConfiguration(keyboard, HorizontalArrangementPosition.Equal, VerticalArrangementPosition.Equal, 20)); + } + else + { + keypad = arrangement.AddType(RGBDeviceType.Keypad, 1); + keypad.AddConfiguration(new SurfaceArrangementConfiguration(null, HorizontalArrangementPosition.Equal, VerticalArrangementPosition.Equal, 20)); + + keyboard = arrangement.AddType(RGBDeviceType.Keyboard, 1); + keyboard.AddConfiguration(new SurfaceArrangementConfiguration(keypad, HorizontalArrangementPosition.Right, VerticalArrangementPosition.Equal, 20)); + + mousepad = arrangement.AddType(RGBDeviceType.Mousepad, 1); + mousepad.AddConfiguration(new SurfaceArrangementConfiguration(keyboard, HorizontalArrangementPosition.Right, VerticalArrangementPosition.Equal, 10)); + + mouse = arrangement.AddType(RGBDeviceType.Mouse, 2); + mouse.AddConfiguration(new SurfaceArrangementConfiguration(mousepad, HorizontalArrangementPosition.Center, VerticalArrangementPosition.Center, 0)); + mouse.AddConfiguration(new SurfaceArrangementConfiguration(keyboard, HorizontalArrangementPosition.Right, VerticalArrangementPosition.Center, 100)); + } SurfaceArrangementType headset = arrangement.AddType(RGBDeviceType.Headset, 1); headset.AddConfiguration(new SurfaceArrangementConfiguration(keyboard, HorizontalArrangementPosition.Center, VerticalArrangementPosition.Bottom, 100)); diff --git a/src/Artemis.Core/Services/Storage/Models/SurfaceArrangementConfiguration.cs b/src/Artemis.Core/Services/Storage/Models/SurfaceArrangementConfiguration.cs index 7f59731f9..284c797a5 100644 --- a/src/Artemis.Core/Services/Storage/Models/SurfaceArrangementConfiguration.cs +++ b/src/Artemis.Core/Services/Storage/Models/SurfaceArrangementConfiguration.cs @@ -32,7 +32,7 @@ internal class SurfaceArrangementConfiguration public int MarginBottom { get; } public SurfaceArrangement SurfaceArrangement { get; set; } - public bool Apply(List devices) + public bool Apply(List devicesToArrange, List devices) { if (Anchor != null && !Anchor.HasDevices(devices)) return false; @@ -42,10 +42,10 @@ internal class SurfaceArrangementConfiguration new SurfaceArrangementType(SurfaceArrangement, RGBDeviceType.All, 1).GetEdge(HorizontalPosition, VerticalPosition); // Stack multiple devices of the same type vertically if they are wider than they are tall - bool stackVertically = devices.Average(d => d.RgbDevice.Size.Width) >= devices.Average(d => d.RgbDevice.Size.Height); + bool stackVertically = devicesToArrange.Average(d => d.RgbDevice.Size.Width) >= devicesToArrange.Average(d => d.RgbDevice.Size.Height); ArtemisDevice? previous = null; - foreach (ArtemisDevice artemisDevice in devices) + foreach (ArtemisDevice artemisDevice in devicesToArrange) { if (previous != null) { diff --git a/src/Artemis.Core/Services/Storage/Models/SurfaceArrangementType.cs b/src/Artemis.Core/Services/Storage/Models/SurfaceArrangementType.cs index 48d0d015f..d855b720b 100644 --- a/src/Artemis.Core/Services/Storage/Models/SurfaceArrangementType.cs +++ b/src/Artemis.Core/Services/Storage/Models/SurfaceArrangementType.cs @@ -28,18 +28,18 @@ internal class SurfaceArrangementType public void Arrange(List devices) { - devices = devices.Where(d => d.DeviceType == DeviceType).ToList(); - if (!devices.Any()) + List devicesToArrange = devices.Where(d => d.DeviceType == DeviceType).ToList(); + if (!devicesToArrange.Any()) return; AppliedConfiguration = null; foreach (SurfaceArrangementConfiguration configuration in Configurations) { - bool applied = configuration.Apply(devices); + bool applied = configuration.Apply(devicesToArrange, devices); if (applied) { AppliedConfiguration = configuration; - foreach (ArtemisDevice artemisDevice in devices) + foreach (ArtemisDevice artemisDevice in devicesToArrange) artemisDevice.ZIndex = ZIndex; return; } @@ -52,7 +52,7 @@ internal class SurfaceArrangementType VerticalArrangementPosition.Equal, 10 ) {SurfaceArrangement = SurfaceArrangement}; - fallback.Apply(devices); + fallback.Apply(devicesToArrange, devices); AppliedConfiguration = fallback; } diff --git a/src/Artemis.UI.Shared/Controls/ProfileConfigurationIcon.axaml.cs b/src/Artemis.UI.Shared/Controls/ProfileConfigurationIcon.axaml.cs index 6b7fd2a02..c6e491438 100644 --- a/src/Artemis.UI.Shared/Controls/ProfileConfigurationIcon.axaml.cs +++ b/src/Artemis.UI.Shared/Controls/ProfileConfigurationIcon.axaml.cs @@ -6,7 +6,6 @@ using Avalonia.Controls; using Avalonia.Controls.Documents; using Avalonia.Layout; using Avalonia.LogicalTree; -using Avalonia.Markup.Xaml; using Avalonia.Media; using Avalonia.Media.Imaging; using Avalonia.Threading; @@ -43,7 +42,7 @@ public partial class ProfileConfigurationIcon : UserControl, IDisposable if (ConfigurationIcon.IconType == ProfileConfigurationIconType.MaterialIcon) { Content = Enum.TryParse(ConfigurationIcon.IconName, true, out MaterialIconKind parsedIcon) - ? new MaterialIcon {Kind = parsedIcon!} + ? new MaterialIcon {Kind = parsedIcon} : new MaterialIcon {Kind = MaterialIconKind.QuestionMark}; } else if (ConfigurationIcon.IconBytes != null) @@ -65,19 +64,28 @@ public partial class ProfileConfigurationIcon : UserControl, IDisposable return; _stream = new MemoryStream(ConfigurationIcon.IconBytes); - if (!ConfigurationIcon.Fill) + Border border = new() { - Content = new Image {Source = new Bitmap(_stream)}; - return; + CornerRadius = CornerRadius, + ClipToBounds = true, + VerticalAlignment = VerticalAlignment.Stretch, + HorizontalAlignment = HorizontalAlignment.Stretch + + }; + + if (ConfigurationIcon.Fill) + { + // Fill mode: use Foreground as Background and the bitmap as opacity mask + border.Background = TextElement.GetForeground(this); + border.OpacityMask = new ImageBrush(new Bitmap(_stream)); + } + else + { + // Non-fill mode: place the image inside the rounded border + border.Child = new Image { Source = new Bitmap(_stream) }; } - Content = new Border - { - Background = TextElement.GetForeground(this), - VerticalAlignment = VerticalAlignment.Stretch, - HorizontalAlignment = HorizontalAlignment.Stretch, - OpacityMask = new ImageBrush(new Bitmap(_stream)) - }; + Content = border; } catch (Exception) { diff --git a/src/Artemis.UI.Shared/Controls/TagsInput/TagsInputStyles.axaml b/src/Artemis.UI.Shared/Controls/TagsInput/TagsInputStyles.axaml index e2a0ff8e5..5d492c436 100644 --- a/src/Artemis.UI.Shared/Controls/TagsInput/TagsInputStyles.axaml +++ b/src/Artemis.UI.Shared/Controls/TagsInput/TagsInputStyles.axaml @@ -1,6 +1,5 @@ diff --git a/src/Artemis.UI.Shared/Plugins/PluginConfigurationDialog.cs b/src/Artemis.UI.Shared/Plugins/PluginConfigurationDialog.cs index 7febeeb80..0c46da3c6 100644 --- a/src/Artemis.UI.Shared/Plugins/PluginConfigurationDialog.cs +++ b/src/Artemis.UI.Shared/Plugins/PluginConfigurationDialog.cs @@ -6,6 +6,22 @@ namespace Artemis.UI.Shared; /// public class PluginConfigurationDialog : PluginConfigurationDialog where T : PluginConfigurationViewModel { + /// + /// Creates a new instance of the class. + /// + public PluginConfigurationDialog() + { + } + + /// + /// Creates a new instance of the class with the specified flag. + /// + /// A value indicating whether the configuration dialog is mandatory. + public PluginConfigurationDialog(bool isMandatory) + { + IsMandatory = isMandatory; + } + /// public override Type Type => typeof(T); } @@ -17,4 +33,7 @@ public abstract class PluginConfigurationDialog : IPluginConfigurationDialog { /// public abstract Type Type { get; } + + /// + public bool IsMandatory { get; protected set; } } \ No newline at end of file diff --git a/src/Artemis.UI.Shared/Routing/Router/Router.cs b/src/Artemis.UI.Shared/Routing/Router/Router.cs index caa95b3e1..d10cd2ede 100644 --- a/src/Artemis.UI.Shared/Routing/Router/Router.cs +++ b/src/Artemis.UI.Shared/Routing/Router/Router.cs @@ -103,7 +103,9 @@ internal class Router : CorePropertyChanged, IRouter, IDisposable { if (_root == null) throw new ArtemisRoutingException("Cannot navigate without a root having been set"); - if (PathEquals(path, options) || (_currentNavigation != null && _currentNavigation.PathEquals(path, options))) + + // If navigating to the same path, don't do anything + if ((_currentNavigation == null && PathEquals(path, options)) || (_currentNavigation != null && _currentNavigation.PathEquals(path, options))) return; string? previousPath = _currentRouteSubject.Value; @@ -128,12 +130,8 @@ internal class Router : CorePropertyChanged, IRouter, IDisposable await navigation.Navigate(args); // If it was cancelled before completion, don't add it to history or update the current path - // Do reload the current path because it may have been partially navigated away from if (navigation.Cancelled) - { - await Reload(); return; - } if (options.AddToHistory && previousPath != null) { @@ -251,7 +249,13 @@ internal class Router : CorePropertyChanged, IRouter, IDisposable if (_previousWindowRoute != null && _currentRouteSubject.Value == "blank") Dispatcher.UIThread.InvokeAsync(async () => await Navigate(_previousWindowRoute, new RouterNavigationOptions {AddToHistory = false, EnableLogging = false})); else if (_currentRouteSubject.Value == null || _currentRouteSubject.Value == "blank") - Dispatcher.UIThread.InvokeAsync(async () => await Navigate("home", new RouterNavigationOptions {AddToHistory = false, EnableLogging = true})); + { + string? startupRoute = Constants.GetStartupRoute(); + if (startupRoute != null) + Dispatcher.UIThread.InvokeAsync(async () => await Navigate(startupRoute, new RouterNavigationOptions {AddToHistory = false, EnableLogging = true})); + else + Dispatcher.UIThread.InvokeAsync(async () => await Navigate("home", new RouterNavigationOptions {AddToHistory = false, EnableLogging = true})); + } } private void MainWindowServiceOnMainWindowClosed(object? sender, EventArgs e) diff --git a/src/Artemis.UI.Shared/Services/Builders/ContentDialogBuilder.cs b/src/Artemis.UI.Shared/Services/Builders/ContentDialogBuilder.cs index da40fa9d2..5eef4bff9 100644 --- a/src/Artemis.UI.Shared/Services/Builders/ContentDialogBuilder.cs +++ b/src/Artemis.UI.Shared/Services/Builders/ContentDialogBuilder.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Threading.Tasks; using System.Windows.Input; using Avalonia.Controls; diff --git a/src/Artemis.UI.Shared/Services/ProfileEditor/ProfileEditorService.cs b/src/Artemis.UI.Shared/Services/ProfileEditor/ProfileEditorService.cs index a9f17c2ee..ae9518f83 100644 --- a/src/Artemis.UI.Shared/Services/ProfileEditor/ProfileEditorService.cs +++ b/src/Artemis.UI.Shared/Services/ProfileEditor/ProfileEditorService.cs @@ -185,12 +185,15 @@ internal class ProfileEditorService : IProfileEditorService { // Activate the profile if one was provided if (profileConfiguration != null) + { + _profileService.FocusProfile = profileConfiguration; _profileService.ActivateProfile(profileConfiguration); + } + // If there is no profile configuration or module, deliberately set the override to null _moduleService.SetActivationOverride(profileConfiguration?.Module); }); - - _profileService.FocusProfile = profileConfiguration; + _profileConfigurationSubject.OnNext(profileConfiguration); ChangeTime(TimeSpan.Zero); diff --git a/src/Artemis.UI.Shared/Services/Window/WindowService.cs b/src/Artemis.UI.Shared/Services/Window/WindowService.cs index 19ea3dbe3..45f5cd6cc 100644 --- a/src/Artemis.UI.Shared/Services/Window/WindowService.cs +++ b/src/Artemis.UI.Shared/Services/Window/WindowService.cs @@ -8,6 +8,7 @@ using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Threading; using DryIoc; using FluentAvalonia.UI.Controls; +using ContentDialogButton = Artemis.UI.Shared.Services.Builders.ContentDialogButton; namespace Artemis.UI.Shared.Services; @@ -94,6 +95,7 @@ internal class WindowService : IWindowService .WithTitle(title) .WithContent(message) .HavingPrimaryButton(b => b.WithText(confirm)) + .WithDefaultButton(ContentDialogButton.Primary) .WithCloseButtonText(cancel) .ShowAsync(); diff --git a/src/Artemis.UI.Shared/Styles/BrokenState.axaml b/src/Artemis.UI.Shared/Styles/BrokenState.axaml index c28d4bca2..a72803f26 100644 --- a/src/Artemis.UI.Shared/Styles/BrokenState.axaml +++ b/src/Artemis.UI.Shared/Styles/BrokenState.axaml @@ -1,6 +1,5 @@  diff --git a/src/Artemis.UI.Shared/Styles/Button.axaml b/src/Artemis.UI.Shared/Styles/Button.axaml index 41b7529c0..b8e7a4da4 100644 --- a/src/Artemis.UI.Shared/Styles/Button.axaml +++ b/src/Artemis.UI.Shared/Styles/Button.axaml @@ -1,7 +1,6 @@  + xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"> diff --git a/src/Artemis.UI.Shared/Styles/Controls/DataModelPicker.axaml b/src/Artemis.UI.Shared/Styles/Controls/DataModelPicker.axaml index d4cd0f731..5a7e24c39 100644 --- a/src/Artemis.UI.Shared/Styles/Controls/DataModelPicker.axaml +++ b/src/Artemis.UI.Shared/Styles/Controls/DataModelPicker.axaml @@ -2,7 +2,6 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:dataModel="clr-namespace:Artemis.UI.Shared.DataModelVisualization.Shared" xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia" - xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia" xmlns:dataModelPicker="clr-namespace:Artemis.UI.Shared.DataModelPicker"> diff --git a/src/Artemis.UI.Shared/Styles/Controls/DataModelPickerButton.axaml b/src/Artemis.UI.Shared/Styles/Controls/DataModelPickerButton.axaml index 3308fedc9..8e90e6c26 100644 --- a/src/Artemis.UI.Shared/Styles/Controls/DataModelPickerButton.axaml +++ b/src/Artemis.UI.Shared/Styles/Controls/DataModelPickerButton.axaml @@ -1,6 +1,5 @@  diff --git a/src/Artemis.UI.Windows/App.axaml.cs b/src/Artemis.UI.Windows/App.axaml.cs index 593a9ea2a..feb425d47 100644 --- a/src/Artemis.UI.Windows/App.axaml.cs +++ b/src/Artemis.UI.Windows/App.axaml.cs @@ -16,6 +16,7 @@ using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Markup.Xaml; using Avalonia.ReactiveUI; using DryIoc; +using HotAvalonia; using ReactiveUI; namespace Artemis.UI.Windows; @@ -40,6 +41,7 @@ public class App : Application LegacyMigrationService.MigrateToSqlite(_container); RxApp.MainThreadScheduler = AvaloniaScheduler.Instance; + this.EnableHotReload(); AvaloniaXamlLoader.Load(this); } @@ -89,7 +91,7 @@ public class App : Application try { CancellationTokenSource cts = new(); - cts.CancelAfter(2000); + cts.CancelAfter(5000); HttpResponseMessage httpResponseMessage = client.Send(new HttpRequestMessage(HttpMethod.Post, url + "remote/bring-to-foreground") {Content = new StringContent(route ?? "")}, cts.Token); httpResponseMessage.EnsureSuccessStatusCode(); diff --git a/src/Artemis.UI.Windows/Artemis.UI.Windows.csproj b/src/Artemis.UI.Windows/Artemis.UI.Windows.csproj index 074722051..9f301ae22 100644 --- a/src/Artemis.UI.Windows/Artemis.UI.Windows.csproj +++ b/src/Artemis.UI.Windows/Artemis.UI.Windows.csproj @@ -9,6 +9,7 @@ Artemis ..\Artemis.UI\Assets\Images\Logo\application.ico en + false diff --git a/src/Artemis.UI.Windows/Program.cs b/src/Artemis.UI.Windows/Program.cs index f9fc5e1f6..b7ccdd7a9 100644 --- a/src/Artemis.UI.Windows/Program.cs +++ b/src/Artemis.UI.Windows/Program.cs @@ -2,6 +2,7 @@ using System; using Artemis.Core; using Artemis.Storage; using Avalonia; +using Avalonia.Logging; using Avalonia.ReactiveUI; using DryIoc; using Serilog; @@ -33,10 +34,7 @@ internal class Program // Avalonia configuration, don't remove; also used by visual designer. public static AppBuilder BuildAvaloniaApp() { - return AppBuilder.Configure() - .UsePlatformDetect() - .LogToTrace() - .UseReactiveUI(); + return AppBuilder.Configure().UsePlatformDetect().LogToTrace().UseReactiveUI(); } public static void CreateLogger(IContainer container) diff --git a/src/Artemis.UI/Artemis.UI.csproj b/src/Artemis.UI/Artemis.UI.csproj index 268327347..36b83abef 100644 --- a/src/Artemis.UI/Artemis.UI.csproj +++ b/src/Artemis.UI/Artemis.UI.csproj @@ -37,38 +37,13 @@ - - DeviceProviderPickerDialogView.axaml - Code - - - DeviceSelectionDialogView.axaml - Code - - - LayoutListView.axaml - Code - - - LayoutListView.axaml - Code - - - LayoutListView.axaml - Code - - - EntryReleasesView.axaml + + WorkshopUnreachableStepView.axaml Code - - - - - - + \ No newline at end of file diff --git a/src/Artemis.UI/Artemis.UI.csproj.DotSettings b/src/Artemis.UI/Artemis.UI.csproj.DotSettings index e73de0af9..19751458b 100644 --- a/src/Artemis.UI/Artemis.UI.csproj.DotSettings +++ b/src/Artemis.UI/Artemis.UI.csproj.DotSettings @@ -1,5 +1,4 @@ - + True True True @@ -7,6 +6,8 @@ True False True + False + True True True True \ No newline at end of file diff --git a/src/Artemis.UI/Assets/Animations/nyan.json b/src/Artemis.UI/Assets/Animations/nyan.json new file mode 100644 index 000000000..ee086765a --- /dev/null +++ b/src/Artemis.UI/Assets/Animations/nyan.json @@ -0,0 +1 @@ +{"v":"5.5.4","fr":60,"ip":0,"op":40,"w":800,"h":800,"nm":"nyan cat","ddd":0,"assets":[],"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Shape Layer 8","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":32,"s":[100]},{"t":37,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":27,"s":[637.938,650.813,0],"to":[-16.667,0,0],"ti":[16.667,0,0]},{"t":38,"s":[537.938,650.813,0]}],"ix":2},"a":{"a":0,"k":[303.75,98.813,0],"ix":1},"s":{"a":0,"k":[93.465,93.465,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":27,"s":[{"i":[[-1.795,0],[0,-1.795],[1.795,0],[0,1.795]],"o":[[1.795,0],[0,1.795],[-1.795,0],[0,-1.795]],"v":[[0,-3.25],[3.25,0],[0,3.25],[-3.25,0]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":32,"s":[{"i":[[-1.795,0],[0,-1.795],[1.795,0],[0,1.795]],"o":[[1.795,0],[0,1.795],[-1.795,0],[0,-1.795]],"v":[[0,-29.688],[3.25,-26.438],[0,-4.25],[-3.25,-26.438]],"c":true}]},{"t":37,"s":[{"i":[[-1.795,0],[0,-1.795],[1.795,0],[0,1.795]],"o":[[1.795,0],[0,1.795],[-1.795,0],[0,-1.795]],"v":[[0,-38.5],[3.25,-35.25],[0,-28],[-3.25,-35.25]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.933333337307,0.933333337307,0.933333337307,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":[303.75,98.75],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":270,"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":"Ellipse 4","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":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":27,"s":[{"i":[[-1.795,0],[0,-1.795],[1.795,0],[0,1.795]],"o":[[1.795,0],[0,1.795],[-1.795,0],[0,-1.795]],"v":[[0,-3.25],[3.25,0],[0,3.25],[-3.25,0]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":32,"s":[{"i":[[-1.795,0],[0,-1.795],[1.795,0],[0,1.795]],"o":[[1.795,0],[0,1.795],[-1.795,0],[0,-1.795]],"v":[[0,-29.688],[3.25,-26.438],[0,-4.25],[-3.25,-26.438]],"c":true}]},{"t":37,"s":[{"i":[[-1.795,0],[0,-1.795],[1.795,0],[0,1.795]],"o":[[1.795,0],[0,1.795],[-1.795,0],[0,-1.795]],"v":[[0,-38.5],[3.25,-35.25],[0,-28],[-3.25,-35.25]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.933333337307,0.933333337307,0.933333337307,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":[303.75,98.75],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":180,"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":"Ellipse 3","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":27,"s":[{"i":[[-1.795,0],[0,-1.795],[1.795,0],[0,1.795]],"o":[[1.795,0],[0,1.795],[-1.795,0],[0,-1.795]],"v":[[0,-3.25],[3.25,0],[0,3.25],[-3.25,0]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":32,"s":[{"i":[[-1.795,0],[0,-1.795],[1.795,0],[0,1.795]],"o":[[1.795,0],[0,1.795],[-1.795,0],[0,-1.795]],"v":[[0,-29.688],[3.25,-26.438],[0,-4.25],[-3.25,-26.438]],"c":true}]},{"t":37,"s":[{"i":[[-1.795,0],[0,-1.795],[1.795,0],[0,1.795]],"o":[[1.795,0],[0,1.795],[-1.795,0],[0,-1.795]],"v":[[0,-38.5],[3.25,-35.25],[0,-28],[-3.25,-35.25]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.933333337307,0.933333337307,0.933333337307,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":[303.75,98.75],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":90,"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":"Ellipse 2","np":2,"cix":2,"bm":0,"ix":3,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":27,"s":[{"i":[[-1.795,0],[0,-1.795],[1.795,0],[0,1.795]],"o":[[1.795,0],[0,1.795],[-1.795,0],[0,-1.795]],"v":[[0,-3.25],[3.25,0],[0,3.25],[-3.25,0]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":32,"s":[{"i":[[-1.795,0],[0,-1.795],[1.795,0],[0,1.795]],"o":[[1.795,0],[0,1.795],[-1.795,0],[0,-1.795]],"v":[[0,-29.688],[3.25,-26.438],[0,-4.25],[-3.25,-26.438]],"c":true}]},{"t":37,"s":[{"i":[[-1.795,0],[0,-1.795],[1.795,0],[0,1.795]],"o":[[1.795,0],[0,1.795],[-1.795,0],[0,-1.795]],"v":[[0,-38.5],[3.25,-35.25],[0,-28],[-3.25,-35.25]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.933333337307,0.933333337307,0.933333337307,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":[303.75,98.75],"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":"Ellipse 1","np":2,"cix":2,"bm":0,"ix":4,"mn":"ADBE Vector Group","hd":false}],"ip":27,"op":38,"st":27,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":"Shape Layer 7","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":18,"s":[100]},{"t":23,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":13,"s":[245.938,622.813,0],"to":[-16.667,0,0],"ti":[16.667,0,0]},{"t":24,"s":[145.938,622.813,0]}],"ix":2},"a":{"a":0,"k":[303.75,98.813,0],"ix":1},"s":{"a":0,"k":[224.995,224.995,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":1},"o":{"x":0.167,"y":0},"t":18,"s":[{"i":[[-0.313,0.031],[0,-0.313],[0.156,0.031],[0.031,0.344]],"o":[[0.313,-0.031],[0,0.313],[-0.156,-0.031],[-0.031,-0.344]],"v":[[0.125,-0.688],[0.75,0],[0.031,0.344],[-0.25,-0.094]],"c":true}]},{"t":23,"s":[{"i":[[-1.795,0],[0,-1.795],[1.795,0],[0,1.795]],"o":[[1.795,0],[0,1.795],[-1.795,0],[0,-1.795]],"v":[[0,-3.25],[3.25,0],[0,3.25],[-3.25,0]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.933333337307,0.933333337307,0.933333337307,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":[303.75,98.75],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":630,"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":"Ellipse 9","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":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":13,"s":[{"i":[[-1.795,0],[0,-1.795],[1.795,0],[0,1.795]],"o":[[1.795,0],[0,1.795],[-1.795,0],[0,-1.795]],"v":[[0,-3.25],[3.25,0],[0,3.25],[-3.25,0]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":18,"s":[{"i":[[-1.795,0],[0,-1.795],[1.795,0],[0,1.795]],"o":[[1.795,0],[0,1.795],[-1.795,0],[0,-1.795]],"v":[[0,-29.688],[3.25,-26.438],[0,-4.25],[-3.25,-26.438]],"c":true}]},{"t":23,"s":[{"i":[[-1.795,0],[0,-1.795],[1.795,0],[0,1.795]],"o":[[1.795,0],[0,1.795],[-1.795,0],[0,-1.795]],"v":[[0,-38.5],[3.25,-35.25],[0,-28],[-3.25,-35.25]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.933333337307,0.933333337307,0.933333337307,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":[303.75,98.75],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":585,"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":"Ellipse 8","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":13,"s":[{"i":[[-1.795,0],[0,-1.795],[1.795,0],[0,1.795]],"o":[[1.795,0],[0,1.795],[-1.795,0],[0,-1.795]],"v":[[0,-3.25],[3.25,0],[0,3.25],[-3.25,0]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":18,"s":[{"i":[[-1.795,0],[0,-1.795],[1.795,0],[0,1.795]],"o":[[1.795,0],[0,1.795],[-1.795,0],[0,-1.795]],"v":[[0,-29.688],[3.25,-26.438],[0,-4.25],[-3.25,-26.438]],"c":true}]},{"t":23,"s":[{"i":[[-1.795,0],[0,-1.795],[1.795,0],[0,1.795]],"o":[[1.795,0],[0,1.795],[-1.795,0],[0,-1.795]],"v":[[0,-38.5],[3.25,-35.25],[0,-28],[-3.25,-35.25]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.933333337307,0.933333337307,0.933333337307,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":[303.75,98.75],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":495,"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":"Ellipse 7","np":2,"cix":2,"bm":0,"ix":3,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":13,"s":[{"i":[[-1.795,0],[0,-1.795],[1.795,0],[0,1.795]],"o":[[1.795,0],[0,1.795],[-1.795,0],[0,-1.795]],"v":[[0,-3.25],[3.25,0],[0,3.25],[-3.25,0]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":18,"s":[{"i":[[-1.795,0],[0,-1.795],[1.795,0],[0,1.795]],"o":[[1.795,0],[0,1.795],[-1.795,0],[0,-1.795]],"v":[[0,-29.688],[3.25,-26.438],[0,-4.25],[-3.25,-26.438]],"c":true}]},{"t":23,"s":[{"i":[[-1.795,0],[0,-1.795],[1.795,0],[0,1.795]],"o":[[1.795,0],[0,1.795],[-1.795,0],[0,-1.795]],"v":[[0,-38.5],[3.25,-35.25],[0,-28],[-3.25,-35.25]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.933333337307,0.933333337307,0.933333337307,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":[303.75,98.75],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":405,"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":"Ellipse 6","np":2,"cix":2,"bm":0,"ix":4,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":13,"s":[{"i":[[-1.795,0],[0,-1.795],[1.795,0],[0,1.795]],"o":[[1.795,0],[0,1.795],[-1.795,0],[0,-1.795]],"v":[[0,-3.25],[3.25,0],[0,3.25],[-3.25,0]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":18,"s":[{"i":[[-1.795,0],[0,-1.795],[1.795,0],[0,1.795]],"o":[[1.795,0],[0,1.795],[-1.795,0],[0,-1.795]],"v":[[0,-29.688],[3.25,-26.438],[0,-4.25],[-3.25,-26.438]],"c":true}]},{"t":23,"s":[{"i":[[-1.795,0],[0,-1.795],[1.795,0],[0,1.795]],"o":[[1.795,0],[0,1.795],[-1.795,0],[0,-1.795]],"v":[[0,-38.5],[3.25,-35.25],[0,-28],[-3.25,-35.25]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.933333337307,0.933333337307,0.933333337307,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":[303.75,98.75],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":315,"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":"Ellipse 5","np":2,"cix":2,"bm":0,"ix":5,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":13,"s":[{"i":[[-1.795,0],[0,-1.795],[1.795,0],[0,1.795]],"o":[[1.795,0],[0,1.795],[-1.795,0],[0,-1.795]],"v":[[0,-3.25],[3.25,0],[0,3.25],[-3.25,0]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":18,"s":[{"i":[[-1.795,0],[0,-1.795],[1.795,0],[0,1.795]],"o":[[1.795,0],[0,1.795],[-1.795,0],[0,-1.795]],"v":[[0,-19.688],[3.25,-16.438],[0,-4.25],[-3.25,-16.438]],"c":true}]},{"t":23,"s":[{"i":[[-1.795,0],[0,-1.795],[1.795,0],[0,1.795]],"o":[[1.795,0],[0,1.795],[-1.795,0],[0,-1.795]],"v":[[0,-38.5],[3.25,-35.25],[0,-28],[-3.25,-35.25]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.933333337307,0.933333337307,0.933333337307,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":[303.75,98.75],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":270,"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":"Ellipse 4","np":2,"cix":2,"bm":0,"ix":6,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":13,"s":[{"i":[[-1.795,0],[0,-1.795],[1.795,0],[0,1.795]],"o":[[1.795,0],[0,1.795],[-1.795,0],[0,-1.795]],"v":[[0,-3.25],[3.25,0],[0,3.25],[-3.25,0]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":18,"s":[{"i":[[-1.795,0],[0,-1.795],[1.795,0],[0,1.795]],"o":[[1.795,0],[0,1.795],[-1.795,0],[0,-1.795]],"v":[[0,-19.688],[3.25,-16.438],[0,-4.25],[-3.25,-16.438]],"c":true}]},{"t":23,"s":[{"i":[[-1.795,0],[0,-1.795],[1.795,0],[0,1.795]],"o":[[1.795,0],[0,1.795],[-1.795,0],[0,-1.795]],"v":[[0,-38.5],[3.25,-35.25],[0,-28],[-3.25,-35.25]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.933333337307,0.933333337307,0.933333337307,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":[303.75,98.75],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":180,"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":"Ellipse 3","np":2,"cix":2,"bm":0,"ix":7,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":13,"s":[{"i":[[-1.795,0],[0,-1.795],[1.795,0],[0,1.795]],"o":[[1.795,0],[0,1.795],[-1.795,0],[0,-1.795]],"v":[[0,-3.25],[3.25,0],[0,3.25],[-3.25,0]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":18,"s":[{"i":[[-1.795,0],[0,-1.795],[1.795,0],[0,1.795]],"o":[[1.795,0],[0,1.795],[-1.795,0],[0,-1.795]],"v":[[0,-19.688],[3.25,-16.438],[0,-4.25],[-3.25,-16.438]],"c":true}]},{"t":23,"s":[{"i":[[-1.795,0],[0,-1.795],[1.795,0],[0,1.795]],"o":[[1.795,0],[0,1.795],[-1.795,0],[0,-1.795]],"v":[[0,-38.5],[3.25,-35.25],[0,-28],[-3.25,-35.25]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.933333337307,0.933333337307,0.933333337307,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":[303.75,98.75],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":90,"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":"Ellipse 2","np":2,"cix":2,"bm":0,"ix":8,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":13,"s":[{"i":[[-1.795,0],[0,-1.795],[1.795,0],[0,1.795]],"o":[[1.795,0],[0,1.795],[-1.795,0],[0,-1.795]],"v":[[0,-3.25],[3.25,0],[0,3.25],[-3.25,0]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":18,"s":[{"i":[[-1.795,0],[0,-1.795],[1.795,0],[0,1.795]],"o":[[1.795,0],[0,1.795],[-1.795,0],[0,-1.795]],"v":[[0,-19.688],[3.25,-16.438],[0,-4.25],[-3.25,-16.438]],"c":true}]},{"t":23,"s":[{"i":[[-1.795,0],[0,-1.795],[1.795,0],[0,1.795]],"o":[[1.795,0],[0,1.795],[-1.795,0],[0,-1.795]],"v":[[0,-38.5],[3.25,-35.25],[0,-28],[-3.25,-35.25]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.933333337307,0.933333337307,0.933333337307,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":[303.75,98.75],"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":"Ellipse 1","np":2,"cix":2,"bm":0,"ix":9,"mn":"ADBE Vector Group","hd":false}],"ip":13,"op":24,"st":13,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":"Shape Layer 6","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":9,"s":[100]},{"t":14,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":4,"s":[591.218,168.813,0],"to":[-16.667,0,0],"ti":[16.667,0,0]},{"t":15,"s":[491.218,168.813,0]}],"ix":2},"a":{"a":0,"k":[303.75,98.813,0],"ix":1},"s":{"a":0,"k":[108.137,108.137,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":1},"o":{"x":0.167,"y":0},"t":9,"s":[{"i":[[-0.313,0.031],[0,-0.313],[0.156,0.031],[0.031,0.344]],"o":[[0.313,-0.031],[0,0.313],[-0.156,-0.031],[-0.031,-0.344]],"v":[[0.125,-0.688],[0.75,0],[0.031,0.344],[-0.25,-0.094]],"c":true}]},{"t":14,"s":[{"i":[[-1.795,0],[0,-1.795],[1.795,0],[0,1.795]],"o":[[1.795,0],[0,1.795],[-1.795,0],[0,-1.795]],"v":[[0,-3.25],[3.25,0],[0,3.25],[-3.25,0]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.933333337307,0.933333337307,0.933333337307,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":[303.75,98.75],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":630,"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":"Ellipse 9","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":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":4,"s":[{"i":[[-1.795,0],[0,-1.795],[1.795,0],[0,1.795]],"o":[[1.795,0],[0,1.795],[-1.795,0],[0,-1.795]],"v":[[0,-3.25],[3.25,0],[0,3.25],[-3.25,0]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":9,"s":[{"i":[[-1.795,0],[0,-1.795],[1.795,0],[0,1.795]],"o":[[1.795,0],[0,1.795],[-1.795,0],[0,-1.795]],"v":[[0,-29.688],[3.25,-26.438],[0,-4.25],[-3.25,-26.438]],"c":true}]},{"t":14,"s":[{"i":[[-1.795,0],[0,-1.795],[1.795,0],[0,1.795]],"o":[[1.795,0],[0,1.795],[-1.795,0],[0,-1.795]],"v":[[0,-38.5],[3.25,-35.25],[0,-28],[-3.25,-35.25]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.933333337307,0.933333337307,0.933333337307,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":[303.75,98.75],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":585,"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":"Ellipse 8","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":4,"s":[{"i":[[-1.795,0],[0,-1.795],[1.795,0],[0,1.795]],"o":[[1.795,0],[0,1.795],[-1.795,0],[0,-1.795]],"v":[[0,-3.25],[3.25,0],[0,3.25],[-3.25,0]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":9,"s":[{"i":[[-1.795,0],[0,-1.795],[1.795,0],[0,1.795]],"o":[[1.795,0],[0,1.795],[-1.795,0],[0,-1.795]],"v":[[0,-29.688],[3.25,-26.438],[0,-4.25],[-3.25,-26.438]],"c":true}]},{"t":14,"s":[{"i":[[-1.795,0],[0,-1.795],[1.795,0],[0,1.795]],"o":[[1.795,0],[0,1.795],[-1.795,0],[0,-1.795]],"v":[[0,-38.5],[3.25,-35.25],[0,-28],[-3.25,-35.25]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.933333337307,0.933333337307,0.933333337307,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":[303.75,98.75],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":495,"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":"Ellipse 7","np":2,"cix":2,"bm":0,"ix":3,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":4,"s":[{"i":[[-1.795,0],[0,-1.795],[1.795,0],[0,1.795]],"o":[[1.795,0],[0,1.795],[-1.795,0],[0,-1.795]],"v":[[0,-3.25],[3.25,0],[0,3.25],[-3.25,0]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":9,"s":[{"i":[[-1.795,0],[0,-1.795],[1.795,0],[0,1.795]],"o":[[1.795,0],[0,1.795],[-1.795,0],[0,-1.795]],"v":[[0,-29.688],[3.25,-26.438],[0,-4.25],[-3.25,-26.438]],"c":true}]},{"t":14,"s":[{"i":[[-1.795,0],[0,-1.795],[1.795,0],[0,1.795]],"o":[[1.795,0],[0,1.795],[-1.795,0],[0,-1.795]],"v":[[0,-38.5],[3.25,-35.25],[0,-28],[-3.25,-35.25]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.933333337307,0.933333337307,0.933333337307,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":[303.75,98.75],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":405,"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":"Ellipse 6","np":2,"cix":2,"bm":0,"ix":4,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":4,"s":[{"i":[[-1.795,0],[0,-1.795],[1.795,0],[0,1.795]],"o":[[1.795,0],[0,1.795],[-1.795,0],[0,-1.795]],"v":[[0,-3.25],[3.25,0],[0,3.25],[-3.25,0]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":9,"s":[{"i":[[-1.795,0],[0,-1.795],[1.795,0],[0,1.795]],"o":[[1.795,0],[0,1.795],[-1.795,0],[0,-1.795]],"v":[[0,-29.688],[3.25,-26.438],[0,-4.25],[-3.25,-26.438]],"c":true}]},{"t":14,"s":[{"i":[[-1.795,0],[0,-1.795],[1.795,0],[0,1.795]],"o":[[1.795,0],[0,1.795],[-1.795,0],[0,-1.795]],"v":[[0,-38.5],[3.25,-35.25],[0,-28],[-3.25,-35.25]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.933333337307,0.933333337307,0.933333337307,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":[303.75,98.75],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":315,"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":"Ellipse 5","np":2,"cix":2,"bm":0,"ix":5,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":4,"s":[{"i":[[-1.795,0],[0,-1.795],[1.795,0],[0,1.795]],"o":[[1.795,0],[0,1.795],[-1.795,0],[0,-1.795]],"v":[[0,-3.25],[3.25,0],[0,3.25],[-3.25,0]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":9,"s":[{"i":[[-1.795,0],[0,-1.795],[1.795,0],[0,1.795]],"o":[[1.795,0],[0,1.795],[-1.795,0],[0,-1.795]],"v":[[0,-19.688],[3.25,-16.438],[0,-4.25],[-3.25,-16.438]],"c":true}]},{"t":14,"s":[{"i":[[-1.795,0],[0,-1.795],[1.795,0],[0,1.795]],"o":[[1.795,0],[0,1.795],[-1.795,0],[0,-1.795]],"v":[[0,-38.5],[3.25,-35.25],[0,-28],[-3.25,-35.25]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.933333337307,0.933333337307,0.933333337307,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":[303.75,98.75],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":270,"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":"Ellipse 4","np":2,"cix":2,"bm":0,"ix":6,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":4,"s":[{"i":[[-1.795,0],[0,-1.795],[1.795,0],[0,1.795]],"o":[[1.795,0],[0,1.795],[-1.795,0],[0,-1.795]],"v":[[0,-3.25],[3.25,0],[0,3.25],[-3.25,0]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":9,"s":[{"i":[[-1.795,0],[0,-1.795],[1.795,0],[0,1.795]],"o":[[1.795,0],[0,1.795],[-1.795,0],[0,-1.795]],"v":[[0,-19.688],[3.25,-16.438],[0,-4.25],[-3.25,-16.438]],"c":true}]},{"t":14,"s":[{"i":[[-1.795,0],[0,-1.795],[1.795,0],[0,1.795]],"o":[[1.795,0],[0,1.795],[-1.795,0],[0,-1.795]],"v":[[0,-38.5],[3.25,-35.25],[0,-28],[-3.25,-35.25]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.933333337307,0.933333337307,0.933333337307,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":[303.75,98.75],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":180,"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":"Ellipse 3","np":2,"cix":2,"bm":0,"ix":7,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":4,"s":[{"i":[[-1.795,0],[0,-1.795],[1.795,0],[0,1.795]],"o":[[1.795,0],[0,1.795],[-1.795,0],[0,-1.795]],"v":[[0,-3.25],[3.25,0],[0,3.25],[-3.25,0]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":9,"s":[{"i":[[-1.795,0],[0,-1.795],[1.795,0],[0,1.795]],"o":[[1.795,0],[0,1.795],[-1.795,0],[0,-1.795]],"v":[[0,-19.688],[3.25,-16.438],[0,-4.25],[-3.25,-16.438]],"c":true}]},{"t":14,"s":[{"i":[[-1.795,0],[0,-1.795],[1.795,0],[0,1.795]],"o":[[1.795,0],[0,1.795],[-1.795,0],[0,-1.795]],"v":[[0,-38.5],[3.25,-35.25],[0,-28],[-3.25,-35.25]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.933333337307,0.933333337307,0.933333337307,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":[303.75,98.75],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":90,"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":"Ellipse 2","np":2,"cix":2,"bm":0,"ix":8,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":4,"s":[{"i":[[-1.795,0],[0,-1.795],[1.795,0],[0,1.795]],"o":[[1.795,0],[0,1.795],[-1.795,0],[0,-1.795]],"v":[[0,-3.25],[3.25,0],[0,3.25],[-3.25,0]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":9,"s":[{"i":[[-1.795,0],[0,-1.795],[1.795,0],[0,1.795]],"o":[[1.795,0],[0,1.795],[-1.795,0],[0,-1.795]],"v":[[0,-19.688],[3.25,-16.438],[0,-4.25],[-3.25,-16.438]],"c":true}]},{"t":14,"s":[{"i":[[-1.795,0],[0,-1.795],[1.795,0],[0,1.795]],"o":[[1.795,0],[0,1.795],[-1.795,0],[0,-1.795]],"v":[[0,-38.5],[3.25,-35.25],[0,-28],[-3.25,-35.25]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.933333337307,0.933333337307,0.933333337307,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":[303.75,98.75],"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":"Ellipse 1","np":2,"cix":2,"bm":0,"ix":9,"mn":"ADBE Vector Group","hd":false}],"ip":4,"op":15,"st":4,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":"Shape Layer 5","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.667],"y":[1]},"o":{"x":[0.333],"y":[0]},"t":5,"s":[100]},{"t":10,"s":[0]}],"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[717.938,270.813,0],"to":[-16.667,0,0],"ti":[16.667,0,0]},{"t":11,"s":[617.938,270.813,0]}],"ix":2},"a":{"a":0,"k":[303.75,98.813,0],"ix":1},"s":{"a":0,"k":[158.816,158.816,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[{"i":[[-1.795,0],[0,-1.795],[1.795,0],[0,1.795]],"o":[[1.795,0],[0,1.795],[-1.795,0],[0,-1.795]],"v":[[0,-3.25],[3.25,0],[0,3.25],[-3.25,0]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":5,"s":[{"i":[[-1.795,0],[0,-1.795],[1.795,0],[0,1.795]],"o":[[1.795,0],[0,1.795],[-1.795,0],[0,-1.795]],"v":[[0,-29.688],[3.25,-26.438],[0,-4.25],[-3.25,-26.438]],"c":true}]},{"t":10,"s":[{"i":[[-1.795,0],[0,-1.795],[1.795,0],[0,1.795]],"o":[[1.795,0],[0,1.795],[-1.795,0],[0,-1.795]],"v":[[0,-38.5],[3.25,-35.25],[0,-28],[-3.25,-35.25]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.933333337307,0.933333337307,0.933333337307,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":[303.75,98.75],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":270,"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":"Ellipse 4","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":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[{"i":[[-1.795,0],[0,-1.795],[1.795,0],[0,1.795]],"o":[[1.795,0],[0,1.795],[-1.795,0],[0,-1.795]],"v":[[0,-3.25],[3.25,0],[0,3.25],[-3.25,0]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":5,"s":[{"i":[[-1.795,0],[0,-1.795],[1.795,0],[0,1.795]],"o":[[1.795,0],[0,1.795],[-1.795,0],[0,-1.795]],"v":[[0,-29.688],[3.25,-26.438],[0,-4.25],[-3.25,-26.438]],"c":true}]},{"t":10,"s":[{"i":[[-1.795,0],[0,-1.795],[1.795,0],[0,1.795]],"o":[[1.795,0],[0,1.795],[-1.795,0],[0,-1.795]],"v":[[0,-38.5],[3.25,-35.25],[0,-28],[-3.25,-35.25]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.933333337307,0.933333337307,0.933333337307,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":[303.75,98.75],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":180,"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":"Ellipse 3","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[{"i":[[-1.795,0],[0,-1.795],[1.795,0],[0,1.795]],"o":[[1.795,0],[0,1.795],[-1.795,0],[0,-1.795]],"v":[[0,-3.25],[3.25,0],[0,3.25],[-3.25,0]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":5,"s":[{"i":[[-1.795,0],[0,-1.795],[1.795,0],[0,1.795]],"o":[[1.795,0],[0,1.795],[-1.795,0],[0,-1.795]],"v":[[0,-29.688],[3.25,-26.438],[0,-4.25],[-3.25,-26.438]],"c":true}]},{"t":10,"s":[{"i":[[-1.795,0],[0,-1.795],[1.795,0],[0,1.795]],"o":[[1.795,0],[0,1.795],[-1.795,0],[0,-1.795]],"v":[[0,-38.5],[3.25,-35.25],[0,-28],[-3.25,-35.25]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.933333337307,0.933333337307,0.933333337307,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":[303.75,98.75],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[100,100],"ix":3},"r":{"a":0,"k":90,"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":"Ellipse 2","np":2,"cix":2,"bm":0,"ix":3,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":1,"k":[{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":0,"s":[{"i":[[-1.795,0],[0,-1.795],[1.795,0],[0,1.795]],"o":[[1.795,0],[0,1.795],[-1.795,0],[0,-1.795]],"v":[[0,-3.25],[3.25,0],[0,3.25],[-3.25,0]],"c":true}]},{"i":{"x":0.667,"y":1},"o":{"x":0.333,"y":0},"t":5,"s":[{"i":[[-1.795,0],[0,-1.795],[1.795,0],[0,1.795]],"o":[[1.795,0],[0,1.795],[-1.795,0],[0,-1.795]],"v":[[0,-29.688],[3.25,-26.438],[0,-4.25],[-3.25,-26.438]],"c":true}]},{"t":10,"s":[{"i":[[-1.795,0],[0,-1.795],[1.795,0],[0,1.795]],"o":[[1.795,0],[0,1.795],[-1.795,0],[0,-1.795]],"v":[[0,-38.5],[3.25,-35.25],[0,-28],[-3.25,-35.25]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.933333337307,0.933333337307,0.933333337307,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":[303.75,98.75],"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":"Ellipse 1","np":2,"cix":2,"bm":0,"ix":4,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":11,"st":0,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":"mouth","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[393,401,0],"to":[2,0,0],"ti":[-1.833,-2,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":10,"s":[405,401,0],"to":[1.833,2,0],"ti":[2.5,-2,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":20,"s":[404,413,0],"to":[-2.5,2,0],"ti":[1.833,2,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[390,413,0],"to":[-1.833,-2,0],"ti":[-0.5,2,0]},{"t":40,"s":[393,401,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],[17.5,0],[0,0],[19.75,0],[-1.25,10.75]],"o":[[0,0],[-20.5,0],[0,0],[-13.022,0],[1.282,-11.027]],"v":[[134.375,43.125],[121.5,57.5],[100.5,43.5],[80.75,57.25],[64,41.25]],"c":false},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0,0,0,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":7,"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":"Shape 1","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":40,"st":0,"bm":0},{"ddd":0,"ind":6,"ty":4,"nm":"face","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[393,401,0],"to":[2,0,0],"ti":[-1.833,-2,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":10,"s":[405,401,0],"to":[1.833,2,0],"ti":[2.5,-2,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":20,"s":[404,413,0],"to":[-2.5,2,0],"ti":[1.833,2,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[390,413,0],"to":[-1.833,-2,0],"ti":[-0.5,2,0]},{"t":40,"s":[393,401,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":0,"k":[8.5,8.5],"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,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[134.75,13.75],"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":"Ellipse 6","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[8.5,8.5],"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,"bm":0,"nm":"Fill 1","mn":"ADBE Vector Graphic - Fill","hd":false},{"ty":"tr","p":{"a":0,"k":[55.75,13.75],"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":"Ellipse 5","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[21.5,21.5],"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":[0.976470649242,0.643137276173,0.674509823322,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":[158.5,44.75],"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":"Ellipse 4","np":2,"cix":2,"bm":0,"ix":3,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[22.5,22.5],"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":[0.976470649242,0.643137276173,0.674509823322,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":[36.5,44.75],"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":"Ellipse 3","np":2,"cix":2,"bm":0,"ix":4,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,-3.678],[-1.75,-0.125],[0.125,2.625]],"o":[[0,2.375],[2.494,0.178],[-0.173,-3.623]],"v":[[93.375,25.75],[99.5,33.25],[106.375,25.25]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0,0,0,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":[1.125,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,"bm":0,"ix":5,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[20,20],"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":[0,0,0,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":[138,17],"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":"Ellipse 2","np":2,"cix":2,"bm":0,"ix":6,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[20,20],"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":[0,0,0,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":[59,17],"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":"Ellipse 1","np":2,"cix":2,"bm":0,"ix":7,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":40,"st":0,"bm":0},{"ddd":0,"ind":7,"ty":4,"nm":"head","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[393,401,0],"to":[2,0,0],"ti":[-1.833,-2,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":10,"s":[405,401,0],"to":[1.833,2,0],"ti":[2.5,-2,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":20,"s":[404,413,0],"to":[-2.5,2,0],"ti":[1.833,2,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[390,413,0],"to":[-1.833,-2,0],"ti":[-0.5,2,0]},{"t":40,"s":[393,401,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":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[6.5,5],[0,0],[20,-4.5],[3,-3.5],[-4.5,-26.5],[0,0],[-26.266,0.048],[-18.746,2.631],[4.438,20.78],[-0.557,5.572]],"o":[[-9,-1],[0,0],[-17,-22.5],[-3,3.5],[-30,60],[0,0],[12.871,-0.024],[34.251,-4.807],[-2.623,-12.282],[1.5,-15]],"v":[[150.5,-44.5],[114,-15],[71.5,-16],[31.5,-44],[20.5,0],[53.5,72.5],[93.503,76.055],[141,72.5],[173.7,17.064],[164.5,-6]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":10,"s":[{"i":[[6.5,5],[0,0],[20,-4.5],[3,-3.5],[-4.5,-26.5],[-7,-0.75],[-26.266,0.048],[-28.75,1],[4.438,20.78],[1.19,5.472]],"o":[[-9,-1],[0,0],[-17,-22.5],[-3,3.5],[-30,60],[7,0.75],[12.871,-0.024],[32.278,-1.123],[-2.623,-12.282],[-5,-23]],"v":[[138.5,-46.5],[114,-15],[71.5,-16],[19,-40],[20.5,0],[53.5,72.5],[96.503,73.055],[141,72.5],[173.7,17.064],[164.5,-6]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":20,"s":[{"i":[[6.5,5],[0,0],[20,-4.5],[3,-3.5],[-4.5,-26.5],[-7,-0.75],[-26.266,0.048],[-28.75,1],[4.438,20.78],[1.596,5.368]],"o":[[-9,-1],[0,0],[-23.5,-21],[-3,3.5],[-30,60],[7,0.75],[12.871,-0.024],[32.278,-1.123],[-2.623,-12.282],[-5.5,-18.5]],"v":[[148,-48.5],[114,-15],[71.5,-16],[36,-46.5],[20.5,0],[53.5,72.5],[96.503,73.055],[141,72.5],[173.7,17.064],[164.5,-6]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[6.5,5],[0,0],[20,-4.5],[3,-3.5],[-4.5,-26.5],[-10,-0.5],[-26.266,0.048],[-28.75,1],[4.438,20.78],[2.881,4.802]],"o":[[-9,-1],[0,0],[-19.5,-17],[-3,3.5],[-30,60],[7.031,0.352],[12.871,-0.024],[32.278,-1.123],[-2.623,-12.282],[-10.5,-17.5]],"v":[[153.5,-47],[114,-15],[71.5,-16],[42.5,-46.5],[20.5,0],[53.5,71.5],[96.503,73.055],[145.5,71.5],[173.7,17.064],[164.5,-6]],"c":true}]},{"t":40,"s":[{"i":[[6.5,5],[0,0],[20,-4.5],[3,-3.5],[-4.5,-26.5],[0,0],[-26.266,0.048],[-18.746,2.631],[4.438,20.78],[-0.557,5.572]],"o":[[-9,-1],[0,0],[-17,-22.5],[-3,3.5],[-30,60],[0,0],[12.871,-0.024],[34.251,-4.807],[-2.623,-12.282],[1.5,-15]],"v":[[150.5,-44.5],[114,-15],[71.5,-16],[31.5,-44],[20.5,0],[53.5,72.5],[93.503,76.055],[141,72.5],[173.7,17.064],[164.5,-6]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0,0,0,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":10,"ix":5},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[0.658823549747,0.662745118141,0.666666686535,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":"Shape 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":40,"st":0,"bm":0},{"ddd":0,"ind":8,"ty":4,"nm":"body 2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[400,400,0],"to":[0,-1.833,0],"ti":[0.167,0.167,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":20,"s":[400,389,0],"to":[-0.167,-0.167,0],"ti":[0.167,-1.667,0]},{"t":40,"s":[399,399,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":0,"s":[100,100,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":8,"s":[100,100,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":16,"s":[94.191,100,100]},{"i":{"x":[0.667,0.667,0.667],"y":[0.867,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":24,"s":[94.191,100,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[-0.4,0,0]},"t":32,"s":[102.905,95.811,100]},{"t":40,"s":[100,100,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[10,10],"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":[0.960784375668,0.266666680574,0.709803938866,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":[75.5,-35.25],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[120.908,120.908],"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":"Ellipse 10","np":2,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[10,10],"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":[0.960784375668,0.266666680574,0.709803938866,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":[30.25,-54],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[120.908,120.908],"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":"Ellipse 9","np":2,"cix":2,"bm":0,"ix":2,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[10,10],"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":[0.960784375668,0.266666680574,0.709803938866,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":[-5,15],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[120.908,120.908],"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":"Ellipse 8","np":2,"cix":2,"bm":0,"ix":3,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[10,10],"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":[0.960784375668,0.266666680574,0.709803938866,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":[-25.5,45.25],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[120.908,120.908],"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":"Ellipse 7","np":2,"cix":2,"bm":0,"ix":4,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[10,10],"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":[0.960784375668,0.266666680574,0.709803938866,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":[-59.75,56.5],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[120.908,120.908],"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":"Ellipse 6","np":2,"cix":2,"bm":0,"ix":5,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[10,10],"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":[0.960784375668,0.266666680574,0.709803938866,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":[-73,26.5],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[120.908,120.908],"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":"Ellipse 5","np":2,"cix":2,"bm":0,"ix":6,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[10,10],"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":[0.960784375668,0.266666680574,0.709803938866,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":[-50.5,4],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[120.908,120.908],"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":"Ellipse 4","np":2,"cix":2,"bm":0,"ix":7,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[10,10],"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":[0.960784375668,0.266666680574,0.709803938866,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":[-16.75,-15],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[120.908,120.908],"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":"Ellipse 3","np":2,"cix":2,"bm":0,"ix":8,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[10,10],"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":[0.960784375668,0.266666680574,0.709803938866,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":[-4.25,-55.5],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[120.908,120.908],"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":"Ellipse 2","np":2,"cix":2,"bm":0,"ix":9,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[10,10],"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":[0.960784375668,0.266666680574,0.709803938866,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":[-60.75,-44],"ix":2},"a":{"a":0,"k":[0,0],"ix":1},"s":{"a":0,"k":[120.908,120.908],"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":"Ellipse 1","np":2,"cix":2,"bm":0,"ix":10,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[28,0],[0,0],[0,-35],[0,0],[-55,0],[0,0],[0,40.5]],"o":[[0,0],[-39,0],[0,0],[0,35],[0,0],[35,0],[0,0],[0,-40.5]],"v":[[70.5,-71],[-53.5,-71],[-89.5,-38],[-89.5,38],[-56,67.5],[70,67.5],[104.5,33.5],[104.5,-32]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[1,0.647058844566,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":[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,"bm":0,"ix":11,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":40,"st":0,"bm":0},{"ddd":0,"ind":9,"ty":4,"nm":"body 1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[400,400,0],"to":[0,-1.833,0],"ti":[0.167,0.167,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":20,"s":[400,389,0],"to":[-0.167,-0.167,0],"ti":[0.167,-1.667,0]},{"t":40,"s":[399,399,0]}],"ix":2},"a":{"a":0,"k":[0,0,0],"ix":1},"s":{"a":1,"k":[{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":0,"s":[100,100,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":8,"s":[100,100,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":16,"s":[94.191,100,100]},{"i":{"x":[0.667,0.667,0.667],"y":[0.867,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[0,0,0]},"t":24,"s":[94.191,100,100]},{"i":{"x":[0.667,0.667,0.667],"y":[1,1,1]},"o":{"x":[0.333,0.333,0.333],"y":[-0.4,0,0]},"t":32,"s":[102.905,95.811,100]},{"t":40,"s":[100,100,100]}],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[0,0],[21.5,0],[0,0],[0,-26.5],[0,0],[-23,0],[0,0],[0,23.5]],"o":[[0,0],[-21.5,0],[0,0],[0,26.5],[0,0],[23,0],[0,0],[0,-23.5]],"v":[[97,-85],[-87,-85],[-106.5,-61],[-106.5,57],[-86,82.5],[98,82.5],[120.5,58],[120.5,-64]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0,0,0,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":10,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[0.988235354424,0.827451050282,0.611764729023,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":"Shape 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":40,"st":0,"bm":0},{"ddd":0,"ind":10,"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":[400,400,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":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[4.5,-4],[-24.412,-17.437],[-32.5,0.5],[7.15,-0.367],[8.75,8.75]],"o":[[-4.5,4],[8.75,6.25],[17.562,-0.27],[-9.75,0.5],[-17.221,-17.221]],"v":[[-172.75,-14.25],[-157.75,25.25],[-105.5,39],[-104.5,6],[-142,-3.25]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":6.666,"s":[{"i":[[12.75,-12.125],[-14.5,-5.75],[-22.75,6.625],[7.154,-0.232],[9,4.75]],"o":[[-12.75,12.125],[14.5,5.75],[17.561,0.048],[-17.75,7.625],[-9,-4.75]],"v":[[-176.25,-1.625],[-156.5,38.75],[-104.75,38.875],[-104.25,5.875],[-143.5,9.25]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":13.334,"s":[{"i":[[6.65,-18.55],[-17.15,1.95],[-13.7,7.7],[7.156,-0.151],[17.8,-2.65]],"o":[[-6.65,18.55],[17.15,-1.95],[17.561,0.239],[-17.4,9.7],[-17.8,2.65]],"v":[[-184.35,26.95],[-138.65,48.95],[-104.3,38.8],[-104.1,5.8],[-143.7,18.35]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":20,"s":[{"i":[[-6.387,-6.559],[-15.5,12],[-12,-0.25],[7.158,-0.097],[11.5,-9.5]],"o":[[9.25,9.5],[8.939,-6.921],[17.56,0.366],[-18.5,0.25],[-19.205,15.865]],"v":[[-173.25,59.5],[-135.25,50.25],[-104,38.75],[-104,5.75],[-157,21.25]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":26.666,"s":[{"i":[[-8.25,-16],[-19.25,0.25],[-11.133,-4.484],[10.5,6.75],[18,0.25]],"o":[[10.297,19.97],[13.769,-0.179],[18,7.25],[-15.563,-10.005],[-26.2,-0.364]],"v":[[-188.75,30.5],[-147.25,26.25],[-107,35.75],[-103,7.25],[-143,-4.75]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":33.334,"s":[{"i":[[-2.35,-19.1],[-20.15,-7.65],[-13.8,-8.05],[14.7,3.45],[18.7,6.55]],"o":[[2.35,19.1],[20.15,7.65],[17.736,3.12],[-12.7,-5.35],[-24.281,-8.505]],"v":[[-185.35,2.4],[-142.65,21.35],[-105.2,37.55],[-101.8,1.85],[-137.2,-11.05]],"c":true}]},{"t":40,"s":[{"i":[[4.5,-4],[-24.412,-17.437],[-32.5,0.5],[7.15,-0.367],[8.75,8.75]],"o":[[-4.5,4],[8.75,6.25],[17.562,-0.27],[-9.75,0.5],[-17.221,-17.221]],"v":[[-172.75,-14.25],[-157.75,25.25],[-105.5,39],[-104.5,6],[-142,-3.25]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0,0,0,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":10,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[0.658823549747,0.662745118141,0.666666686535,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":"Shape 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":40,"st":0,"bm":0},{"ddd":0,"ind":11,"ty":4,"nm":"leg 3","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[620,395,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":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[5.548,-2.912],[4.319,-17.479],[-10.277,0],[-0.75,9],[5.846,3.907]],"o":[[-20,10.5],[-2.644,10.701],[21,0],[0.5,-6],[-4.43,-2.961]],"v":[[-117.25,63],[-126.356,95.549],[-111.5,113.5],[-94,87],[-102.471,64.218]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":10,"s":[{"i":[[5.149,-4.506],[14.856,-13.049],[-20.506,0],[0,0],[4.175,4.046]],"o":[[-12,10.5],[-11.305,9.931],[24,0],[0,0],[-5.187,-5.027]],"v":[[-101.5,62.5],[-115.856,78.549],[-109,102],[-77,76],[-84.199,67.652]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":20,"s":[{"i":[[11.358,-2.77],[-0.614,-19.764],[-20,2.5],[0,0],[1.047,4.405]],"o":[[-15.491,3.778],[0.356,11.451],[23.815,-2.977],[0,0],[-1.3,-5.472]],"v":[[-95,64.5],[-106.856,90.549],[-88,112.5],[-78,81.5],[-78.351,72.72]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[5.149,-4.506],[0.856,-6.049],[-20.073,1.825],[0,0],[4.065,2.613]],"o":[[-12,10.5],[-0.861,6.086],[27.5,-2.5],[0,0],[-5.05,-3.247]],"v":[[-102,74.5],[-112.356,93.049],[-88.5,108],[-78,81.5],[-84.99,75.877]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":35,"s":[{"i":[[5.348,-3.709],[-0.144,-8.299],[-15.175,0.912],[-0.375,4.5],[4.955,3.26]],"o":[[-16,10.5],[-1.753,8.394],[24.25,-1.25],[0.25,-3],[-4.74,-3.104]],"v":[[-109.625,68.75],[-121.856,93.299],[-95.5,110.25],[-87.5,83.75],[-93.73,70.047]],"c":true}]},{"t":40,"s":[{"i":[[5.548,-2.912],[4.319,-17.479],[-10.277,0],[-0.75,9],[5.846,3.907]],"o":[[-20,10.5],[-2.644,10.701],[21,0],[0.5,-6],[-4.43,-2.961]],"v":[[-117.25,63],[-126.356,95.549],[-111.5,113.5],[-94,87],[-102.471,64.218]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[5.548,-2.912],[4.319,-17.479],[-10.277,0],[-0.75,9],[5.846,3.907]],"o":[[-20,10.5],[-2.644,10.701],[21,0],[0.5,-6],[-4.43,-2.961]],"v":[[-117.25,63],[-126.356,95.549],[-111.5,113.5],[-94,87],[-102.471,64.218]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":10,"s":[{"i":[[5.149,-4.506],[14.856,-13.049],[-20.506,0],[0,0],[4.175,4.046]],"o":[[-12,10.5],[-11.305,9.931],[24,0],[0,0],[-5.187,-5.027]],"v":[[-101.5,62.5],[-115.856,78.549],[-109,102],[-77,76],[-84.199,67.652]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":20,"s":[{"i":[[11.358,-2.77],[-0.614,-19.764],[-20,2.5],[0,0],[1.047,4.405]],"o":[[-15.491,3.778],[0.356,11.451],[23.815,-2.977],[0,0],[-1.3,-5.472]],"v":[[-95,64.5],[-106.856,90.549],[-88,112.5],[-78,81.5],[-78.351,72.72]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[5.149,-4.506],[0.856,-6.049],[-20.073,1.825],[0,0],[4.065,2.613]],"o":[[-12,10.5],[-0.861,6.086],[27.5,-2.5],[0,0],[-5.05,-3.247]],"v":[[-102,74.5],[-112.356,93.049],[-88.5,108],[-78,81.5],[-84.99,75.877]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":35,"s":[{"i":[[5.348,-3.709],[-0.144,-8.299],[-15.175,0.912],[-0.375,4.5],[4.955,3.26]],"o":[[-16,10.5],[-1.753,8.394],[24.25,-1.25],[0.25,-3],[-4.74,-3.104]],"v":[[-109.625,68.75],[-121.856,93.299],[-95.5,110.25],[-87.5,83.75],[-93.73,70.047]],"c":true}]},{"t":40,"s":[{"i":[[5.548,-2.912],[4.319,-17.479],[-10.277,0],[-0.75,9],[5.846,3.907]],"o":[[-20,10.5],[-2.644,10.701],[21,0],[0.5,-6],[-4.43,-2.961]],"v":[[-117.25,63],[-126.356,95.549],[-111.5,113.5],[-94,87],[-102.471,64.218]],"c":true}]}],"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0,0,0,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":10,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[0.658823549747,0.662745118141,0.666666686535,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":"Shape 1","np":4,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":40,"st":0,"bm":0},{"ddd":0,"ind":12,"ty":4,"nm":"leg 4","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[563,395,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":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[5.548,-2.912],[4.319,-17.479],[-10.277,0],[-0.75,9],[5.846,3.907]],"o":[[-20,10.5],[-2.644,10.701],[21,0],[0.5,-6],[-4.43,-2.961]],"v":[[-117.25,63],[-126.356,95.549],[-111.5,113.5],[-94,87],[-102.471,64.218]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":10,"s":[{"i":[[5.149,-4.506],[14.856,-13.049],[-20.506,0],[0,0],[4.175,4.046]],"o":[[-12,10.5],[-11.305,9.931],[24,0],[0,0],[-5.187,-5.027]],"v":[[-101.5,62.5],[-115.856,78.549],[-109,102],[-77,76],[-84.199,67.652]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":20,"s":[{"i":[[11.358,-2.77],[-0.614,-19.764],[-20,2.5],[0,0],[1.047,4.405]],"o":[[-15.491,3.778],[0.356,11.451],[23.815,-2.977],[0,0],[-1.3,-5.472]],"v":[[-95,64.5],[-106.856,90.549],[-88,112.5],[-78,81.5],[-78.351,72.72]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[5.149,-4.506],[0.856,-6.049],[-20.073,1.825],[0,0],[4.065,2.613]],"o":[[-12,10.5],[-0.861,6.086],[27.5,-2.5],[0,0],[-5.05,-3.247]],"v":[[-102,74.5],[-112.356,93.049],[-88.5,108],[-78,81.5],[-84.99,75.877]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":35,"s":[{"i":[[5.348,-3.709],[-0.144,-8.299],[-15.175,0.912],[-0.375,4.5],[4.955,3.26]],"o":[[-16,10.5],[-1.753,8.394],[24.25,-1.25],[0.25,-3],[-4.74,-3.104]],"v":[[-109.625,68.75],[-121.856,93.299],[-95.5,110.25],[-87.5,83.75],[-93.73,70.047]],"c":true}]},{"t":40,"s":[{"i":[[5.548,-2.912],[4.319,-17.479],[-10.277,0],[-0.75,9],[5.846,3.907]],"o":[[-20,10.5],[-2.644,10.701],[21,0],[0.5,-6],[-4.43,-2.961]],"v":[[-117.25,63],[-126.356,95.549],[-111.5,113.5],[-94,87],[-102.471,64.218]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ind":1,"ty":"sh","ix":2,"ks":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[5.548,-2.912],[4.319,-17.479],[-10.277,0],[-0.75,9],[5.846,3.907]],"o":[[-20,10.5],[-2.644,10.701],[21,0],[0.5,-6],[-4.43,-2.961]],"v":[[-117.25,63],[-126.356,95.549],[-111.5,113.5],[-94,87],[-102.471,64.218]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":10,"s":[{"i":[[5.149,-4.506],[14.856,-13.049],[-20.506,0],[0,0],[4.175,4.046]],"o":[[-12,10.5],[-11.305,9.931],[24,0],[0,0],[-5.187,-5.027]],"v":[[-101.5,62.5],[-115.856,78.549],[-109,102],[-77,76],[-84.199,67.652]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":20,"s":[{"i":[[11.358,-2.77],[-0.614,-19.764],[-20,2.5],[0,0],[1.047,4.405]],"o":[[-15.491,3.778],[0.356,11.451],[23.815,-2.977],[0,0],[-1.3,-5.472]],"v":[[-95,64.5],[-106.856,90.549],[-88,112.5],[-78,81.5],[-78.351,72.72]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[5.149,-4.506],[0.856,-6.049],[-20.073,1.825],[0,0],[4.065,2.613]],"o":[[-12,10.5],[-0.861,6.086],[27.5,-2.5],[0,0],[-5.05,-3.247]],"v":[[-102,74.5],[-112.356,93.049],[-88.5,108],[-78,81.5],[-84.99,75.877]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":35,"s":[{"i":[[5.348,-3.709],[-0.144,-8.299],[-15.175,0.912],[-0.375,4.5],[4.955,3.26]],"o":[[-16,10.5],[-1.753,8.394],[24.25,-1.25],[0.25,-3],[-4.74,-3.104]],"v":[[-109.625,68.75],[-121.856,93.299],[-95.5,110.25],[-87.5,83.75],[-93.73,70.047]],"c":true}]},{"t":40,"s":[{"i":[[5.548,-2.912],[4.319,-17.479],[-10.277,0],[-0.75,9],[5.846,3.907]],"o":[[-20,10.5],[-2.644,10.701],[21,0],[0.5,-6],[-4.43,-2.961]],"v":[[-117.25,63],[-126.356,95.549],[-111.5,113.5],[-94,87],[-102.471,64.218]],"c":true}]}],"ix":2},"nm":"Path 2","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0,0,0,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":10,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[0.658823549747,0.662745118141,0.666666686535,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":"Shape 1","np":4,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":40,"st":0,"bm":0},{"ddd":0,"ind":13,"ty":4,"nm":"leg 1","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[400,395,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":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[4.715,-4.126],[10.941,-14.299],[-6.936,3.153],[0,0],[11.346,12.282]],"o":[[-12,10.5],[-9.144,11.951],[22,-10],[0,0],[-3.616,-3.915]],"v":[[-105,69],[-135.356,94.549],[-122.5,112.5],[-85,89],[-88.846,70.218]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":10,"s":[{"i":[[5.149,-4.506],[14.856,-13.049],[-20.506,0],[0,0],[4.175,4.046]],"o":[[-12,10.5],[-11.305,9.931],[24,0],[0,0],[-5.187,-5.027]],"v":[[-101.5,62.5],[-115.856,78.549],[-109,102],[-77,76],[-84.199,67.652]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":20,"s":[{"i":[[11.358,-2.77],[18.31,-7.464],[-20,2.5],[0,0],[1.851,5.53]],"o":[[-15.491,3.778],[-12.144,4.951],[23.815,-2.977],[0,0],[-1.785,-5.334]],"v":[[-95,64.5],[-105.856,82.049],[-95,103.5],[-75.5,82.5],[-77.726,70.845]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[5.149,-4.506],[18.31,-7.464],[-20.073,1.825],[0,0],[4.065,2.613]],"o":[[-12,10.5],[-12.144,4.951],[27.5,-2.5],[0,0],[-5.05,-3.247]],"v":[[-102,74.5],[-116.356,92.549],[-105.5,114],[-78,81.5],[-84.99,75.877]],"c":true}]},{"t":40,"s":[{"i":[[4.715,-4.126],[10.941,-14.299],[-6.936,3.153],[0,0],[11.346,12.282]],"o":[[-12,10.5],[-9.144,11.951],[22,-10],[0,0],[-3.616,-3.915]],"v":[[-105,69],[-135.356,94.549],[-122.5,112.5],[-85,89],[-88.846,70.218]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0,0,0,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":10,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[0.658823549747,0.662745118141,0.666666686535,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":"Shape 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":40,"st":0,"bm":0},{"ddd":0,"ind":14,"ty":4,"nm":"leg 2","sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[450,395,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":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[{"i":[[4.715,-4.126],[10.941,-14.299],[-6.936,3.153],[0,0],[11.346,12.282]],"o":[[-12,10.5],[-9.144,11.951],[22,-10],[0,0],[-3.616,-3.915]],"v":[[-105,69],[-135.356,94.549],[-122.5,112.5],[-85,89],[-88.846,70.218]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":10,"s":[{"i":[[5.149,-4.506],[14.856,-13.049],[-20.506,0],[0,0],[4.175,4.046]],"o":[[-12,10.5],[-11.305,9.931],[24,0],[0,0],[-5.187,-5.027]],"v":[[-101.5,62.5],[-115.856,78.549],[-109,102],[-77,76],[-84.199,67.652]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":20,"s":[{"i":[[11.358,-2.77],[18.31,-7.464],[-20,2.5],[0,0],[1.851,5.53]],"o":[[-15.491,3.778],[-12.144,4.951],[23.815,-2.977],[0,0],[-1.785,-5.334]],"v":[[-95,64.5],[-105.856,82.049],[-95,103.5],[-75.5,82.5],[-77.726,70.845]],"c":true}]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":30,"s":[{"i":[[5.149,-4.506],[18.31,-7.464],[-20.073,1.825],[0,0],[4.065,2.613]],"o":[[-12,10.5],[-12.144,4.951],[27.5,-2.5],[0,0],[-5.05,-3.247]],"v":[[-102,74.5],[-116.356,92.549],[-105.5,114],[-78,81.5],[-84.99,75.877]],"c":true}]},{"t":40,"s":[{"i":[[4.715,-4.126],[10.941,-14.299],[-6.936,3.153],[0,0],[11.346,12.282]],"o":[[-12,10.5],[-9.144,11.951],[22,-10],[0,0],[-3.616,-3.915]],"v":[[-105,69],[-135.356,94.549],[-122.5,112.5],[-85,89],[-88.846,70.218]],"c":true}]}],"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"st","c":{"a":0,"k":[0,0,0,1],"ix":3},"o":{"a":0,"k":100,"ix":4},"w":{"a":0,"k":10,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[0.658823549747,0.662745118141,0.666666686535,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":"Shape 1","np":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":40,"st":0,"bm":0},{"ddd":0,"ind":15,"ty":4,"nm":"Shape Layer 4","td":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[401,400,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],[-67,59],[28,16]],"o":[[0,0],[0,0],[46.318,-40.788],[-37.122,-21.213]],"v":[[-401,-104],[-401,119],[-94,78],[-89,-84]],"c":true},"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":2,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,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":[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":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":20,"op":40,"st":20,"bm":0},{"ddd":0,"ind":16,"ty":4,"nm":"rainbow Outlines 2","tt":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":20,"s":[279.611,406,0],"to":[-30.789,0,0],"ti":[30.789,0,0]},{"t":40,"s":[94.875,406,0]}],"ix":2},"a":{"a":0,"k":[367.5,131.5,0],"ix":1},"s":{"a":0,"k":[75.507,75.507,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[61.2,0],[49.824,0],[61.2,0],[49.824,0],[61.2,0],[49.823,0],[0,0],[-61.2,0],[-49.823,0],[-61.2,0],[-49.824,0],[-61.2,0],[-49.824,0],[0,0]],"o":[[-61.2,0],[-49.824,0],[-61.2,0],[-49.823,0],[-61.2,0],[0,0],[49.823,0],[61.2,0],[49.824,0],[61.2,0],[49.824,0],[61.2,0],[0,0],[-49.824,0]],"v":[[244.8,-25.923],[122.4,-12.279],[0,-25.923],[-122.4,-12.279],[-244.8,-25.923],[-367.2,-12.279],[-367.2,25.923],[-244.8,12.278],[-122.4,25.923],[0,12.278],[122.4,25.923],[244.8,12.278],[367.2,25.923],[367.2,-12.279]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.317314267626,0.213829025568,0.589291920381,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":[364.387,216.168],"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":[[61.2,0],[49.824,0],[61.2,0],[49.824,0],[61.2,0],[49.823,0],[0,0],[-61.2,0],[-49.823,0],[-61.2,0],[-49.824,0],[-61.2,0],[-49.824,0],[0,0]],"o":[[-61.2,0],[-49.824,0],[-61.2,0],[-49.823,0],[-61.2,0],[0,0],[49.823,0],[61.2,0],[49.824,0],[61.2,0],[49.824,0],[61.2,0],[0,0],[-49.824,0]],"v":[[244.8,-25.922],[122.4,-12.278],[0,-25.922],[-122.4,-12.278],[-244.8,-25.922],[-367.2,-12.278],[-367.2,25.922],[-244.8,12.28],[-122.4,25.922],[0,12.28],[122.4,25.922],[244.8,12.28],[367.2,25.922],[367.2,-12.278]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.341164592668,0.606181006338,0.780451516544,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":[364.387,179.465],"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":2,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[61.2,0],[49.824,0],[61.2,0],[49.824,0],[61.2,0],[49.823,0],[0,0],[-61.2,0],[-49.823,0],[-61.2,0],[-49.824,0],[-61.2,0],[-49.824,0],[0,0]],"o":[[-61.2,0],[-49.824,0],[-61.2,0],[-49.823,0],[-61.2,0],[0,0],[49.823,0],[61.2,0],[49.824,0],[61.2,0],[49.824,0],[61.2,0],[0,0],[-49.824,0]],"v":[[244.8,-25.923],[122.4,-12.279],[0,-25.923],[-122.4,-12.279],[-244.8,-25.923],[-367.2,-12.279],[-367.2,25.923],[-244.8,12.279],[-122.4,25.923],[0,12.279],[122.4,25.923],[244.8,12.279],[367.2,25.923],[367.2,-12.279]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.418309709138,0.728695200004,0.130248021144,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":[364.387,143.265],"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":2,"cix":2,"bm":0,"ix":3,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[61.2,0],[49.824,0],[61.2,0],[49.824,0],[61.2,0],[49.823,0],[0,0],[-61.2,0],[-49.823,0],[-61.2,0],[-49.824,0],[-61.2,0],[-49.824,0],[0,0]],"o":[[-61.2,0],[-49.824,0],[-61.2,0],[-49.823,0],[-61.2,0],[0,0],[49.823,0],[61.2,0],[49.824,0],[61.2,0],[49.824,0],[61.2,0],[0,0],[-49.824,0]],"v":[[244.8,-25.922],[122.4,-12.279],[0,-25.922],[-122.4,-12.279],[-244.8,-25.922],[-367.2,-12.279],[-367.2,25.922],[-244.8,12.279],[-122.4,25.922],[0,12.279],[122.4,25.922],[244.8,12.279],[367.2,25.922],[367.2,-12.279]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.915979662129,0.96097166772,0.066235486199,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":[364.387,106.563],"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":2,"cix":2,"bm":0,"ix":4,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[61.2,0],[49.824,0],[61.2,0],[49.824,0],[61.2,0],[49.823,0],[0,0],[-61.2,0],[-49.823,0],[-61.2,0],[-49.824,0],[-61.2,0],[-49.824,0],[0,0]],"o":[[-61.2,0],[-49.824,0],[-61.2,0],[-49.823,0],[-61.2,0],[0,0],[49.823,0],[61.2,0],[49.824,0],[61.2,0],[49.824,0],[61.2,0],[0,0],[-49.824,0]],"v":[[244.8,-25.923],[122.4,-12.278],[0,-25.923],[-122.4,-12.278],[-244.8,-25.923],[-367.2,-12.278],[-367.2,25.923],[-244.8,12.28],[-122.4,25.923],[0,12.28],[122.4,25.923],[244.8,12.28],[367.2,25.923],[367.2,-12.278]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.999794574812,0.598157276827,0.02665384517,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":[364.387,70.362],"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":2,"cix":2,"bm":0,"ix":5,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[61.2,0],[49.824,0],[61.2,0],[49.824,0],[61.2,0],[49.823,0],[0,0],[-61.2,0],[-49.823,0],[-61.2,0],[-49.824,0],[-61.2,0],[-49.824,0],[0,0]],"o":[[-61.2,0],[-49.824,0],[-61.2,0],[-49.823,0],[-61.2,0],[0,0],[49.823,0],[61.2,0],[49.824,0],[61.2,0],[49.824,0],[61.2,0],[0,0],[-49.824,0]],"v":[[244.8,-25.923],[122.4,-12.278],[0,-25.923],[-122.4,-12.278],[-244.8,-25.923],[-367.2,-12.278],[-367.2,25.923],[-244.8,12.279],[-122.4,25.923],[0,12.279],[122.4,25.923],[244.8,12.279],[367.2,25.923],[367.2,-12.278]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.994454716701,0.031589493097,0.023664128547,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":[364.387,34.661],"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,"bm":0,"ix":6,"mn":"ADBE Vector Group","hd":false}],"ip":20,"op":40,"st":20,"bm":0},{"ddd":0,"ind":17,"ty":4,"nm":"Shape Layer 2","td":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":0,"k":[401,400,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],[-67,59],[28,16]],"o":[[0,0],[0,0],[46.318,-40.788],[-37.122,-21.213]],"v":[[-401,-104],[-401,119],[-94,78],[-89,-84]],"c":true},"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":2,"ix":5},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","mn":"ADBE Vector Graphic - Stroke","hd":false},{"ty":"fl","c":{"a":0,"k":[1,1,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":[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":3,"cix":2,"bm":0,"ix":1,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":21,"st":0,"bm":0},{"ddd":0,"ind":18,"ty":4,"nm":"rainbow Outlines","tt":1,"sr":1,"ks":{"o":{"a":0,"k":100,"ix":11},"r":{"a":0,"k":0,"ix":10},"p":{"a":1,"k":[{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":0,"s":[279.611,406,0],"to":[-30.789,0,0],"ti":[30.789,0,0]},{"t":20,"s":[94.875,406,0]}],"ix":2},"a":{"a":0,"k":[367.5,131.5,0],"ix":1},"s":{"a":0,"k":[75.507,75.507,100],"ix":6}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[61.2,0],[49.824,0],[61.2,0],[49.824,0],[61.2,0],[49.823,0],[0,0],[-61.2,0],[-49.823,0],[-61.2,0],[-49.824,0],[-61.2,0],[-49.824,0],[0,0]],"o":[[-61.2,0],[-49.824,0],[-61.2,0],[-49.823,0],[-61.2,0],[0,0],[49.823,0],[61.2,0],[49.824,0],[61.2,0],[49.824,0],[61.2,0],[0,0],[-49.824,0]],"v":[[244.8,-25.923],[122.4,-12.279],[0,-25.923],[-122.4,-12.279],[-244.8,-25.923],[-367.2,-12.279],[-367.2,25.923],[-244.8,12.278],[-122.4,25.923],[0,12.278],[122.4,25.923],[244.8,12.278],[367.2,25.923],[367.2,-12.279]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.317314267626,0.213829025568,0.589291920381,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":[364.387,216.168],"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":[[61.2,0],[49.824,0],[61.2,0],[49.824,0],[61.2,0],[49.823,0],[0,0],[-61.2,0],[-49.823,0],[-61.2,0],[-49.824,0],[-61.2,0],[-49.824,0],[0,0]],"o":[[-61.2,0],[-49.824,0],[-61.2,0],[-49.823,0],[-61.2,0],[0,0],[49.823,0],[61.2,0],[49.824,0],[61.2,0],[49.824,0],[61.2,0],[0,0],[-49.824,0]],"v":[[244.8,-25.922],[122.4,-12.278],[0,-25.922],[-122.4,-12.278],[-244.8,-25.922],[-367.2,-12.278],[-367.2,25.922],[-244.8,12.28],[-122.4,25.922],[0,12.28],[122.4,25.922],[244.8,12.28],[367.2,25.922],[367.2,-12.278]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.341164592668,0.606181006338,0.780451516544,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":[364.387,179.465],"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":2,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[61.2,0],[49.824,0],[61.2,0],[49.824,0],[61.2,0],[49.823,0],[0,0],[-61.2,0],[-49.823,0],[-61.2,0],[-49.824,0],[-61.2,0],[-49.824,0],[0,0]],"o":[[-61.2,0],[-49.824,0],[-61.2,0],[-49.823,0],[-61.2,0],[0,0],[49.823,0],[61.2,0],[49.824,0],[61.2,0],[49.824,0],[61.2,0],[0,0],[-49.824,0]],"v":[[244.8,-25.923],[122.4,-12.279],[0,-25.923],[-122.4,-12.279],[-244.8,-25.923],[-367.2,-12.279],[-367.2,25.923],[-244.8,12.279],[-122.4,25.923],[0,12.279],[122.4,25.923],[244.8,12.279],[367.2,25.923],[367.2,-12.279]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.418309709138,0.728695200004,0.130248021144,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":[364.387,143.265],"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":2,"cix":2,"bm":0,"ix":3,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[61.2,0],[49.824,0],[61.2,0],[49.824,0],[61.2,0],[49.823,0],[0,0],[-61.2,0],[-49.823,0],[-61.2,0],[-49.824,0],[-61.2,0],[-49.824,0],[0,0]],"o":[[-61.2,0],[-49.824,0],[-61.2,0],[-49.823,0],[-61.2,0],[0,0],[49.823,0],[61.2,0],[49.824,0],[61.2,0],[49.824,0],[61.2,0],[0,0],[-49.824,0]],"v":[[244.8,-25.922],[122.4,-12.279],[0,-25.922],[-122.4,-12.279],[-244.8,-25.922],[-367.2,-12.279],[-367.2,25.922],[-244.8,12.279],[-122.4,25.922],[0,12.279],[122.4,25.922],[244.8,12.279],[367.2,25.922],[367.2,-12.279]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.915979662129,0.96097166772,0.066235486199,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":[364.387,106.563],"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":2,"cix":2,"bm":0,"ix":4,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[61.2,0],[49.824,0],[61.2,0],[49.824,0],[61.2,0],[49.823,0],[0,0],[-61.2,0],[-49.823,0],[-61.2,0],[-49.824,0],[-61.2,0],[-49.824,0],[0,0]],"o":[[-61.2,0],[-49.824,0],[-61.2,0],[-49.823,0],[-61.2,0],[0,0],[49.823,0],[61.2,0],[49.824,0],[61.2,0],[49.824,0],[61.2,0],[0,0],[-49.824,0]],"v":[[244.8,-25.923],[122.4,-12.278],[0,-25.923],[-122.4,-12.278],[-244.8,-25.923],[-367.2,-12.278],[-367.2,25.923],[-244.8,12.28],[-122.4,25.923],[0,12.28],[122.4,25.923],[244.8,12.28],[367.2,25.923],[367.2,-12.278]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.999794574812,0.598157276827,0.02665384517,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":[364.387,70.362],"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":2,"cix":2,"bm":0,"ix":5,"mn":"ADBE Vector Group","hd":false},{"ty":"gr","it":[{"ind":0,"ty":"sh","ix":1,"ks":{"a":0,"k":{"i":[[61.2,0],[49.824,0],[61.2,0],[49.824,0],[61.2,0],[49.823,0],[0,0],[-61.2,0],[-49.823,0],[-61.2,0],[-49.824,0],[-61.2,0],[-49.824,0],[0,0]],"o":[[-61.2,0],[-49.824,0],[-61.2,0],[-49.823,0],[-61.2,0],[0,0],[49.823,0],[61.2,0],[49.824,0],[61.2,0],[49.824,0],[61.2,0],[0,0],[-49.824,0]],"v":[[244.8,-25.923],[122.4,-12.278],[0,-25.923],[-122.4,-12.278],[-244.8,-25.923],[-367.2,-12.278],[-367.2,25.923],[-244.8,12.279],[-122.4,25.923],[0,12.279],[122.4,25.923],[244.8,12.279],[367.2,25.923],[367.2,-12.278]],"c":true},"ix":2},"nm":"Path 1","mn":"ADBE Vector Shape - Group","hd":false},{"ty":"fl","c":{"a":0,"k":[0.994454716701,0.031589493097,0.023664128547,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":[364.387,34.661],"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,"bm":0,"ix":6,"mn":"ADBE Vector Group","hd":false}],"ip":0,"op":21,"st":0,"bm":0}],"markers":[]} \ No newline at end of file diff --git a/src/Artemis.UI/Controls/SplitMarkdownEditor.axaml b/src/Artemis.UI/Controls/SplitMarkdownEditor.axaml index a84b7d6ed..f264335d7 100644 --- a/src/Artemis.UI/Controls/SplitMarkdownEditor.axaml +++ b/src/Artemis.UI/Controls/SplitMarkdownEditor.axaml @@ -4,8 +4,6 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:avaloniaEdit="https://github.com/avaloniaui/avaloniaedit" xmlns:mdxaml="https://github.com/whistyun/Markdown.Avalonia.Tight" - xmlns:fa="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia" - xmlns:input="clr-namespace:System.Windows.Input;assembly=System.ObjectModel" xmlns:ui="clr-namespace:Artemis.UI" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="Artemis.UI.Controls.SplitMarkdownEditor"> diff --git a/src/Artemis.UI/Converters/PropertyTreeMarginConverter.cs b/src/Artemis.UI/Converters/PropertyTreeMarginConverter.cs index aa7cfc232..8c69358ff 100644 --- a/src/Artemis.UI/Converters/PropertyTreeMarginConverter.cs +++ b/src/Artemis.UI/Converters/PropertyTreeMarginConverter.cs @@ -1,6 +1,7 @@ using System; using System.Globalization; using Artemis.UI.Screens.ProfileEditor.Properties.Tree; +using Artemis.UI.Screens.ProfileEditor.Properties.Tree; using Avalonia; using Avalonia.Data.Converters; diff --git a/src/Artemis.UI/DefaultTypes/PropertyInput/BrushPropertyInputViewModel.cs b/src/Artemis.UI/DefaultTypes/PropertyInput/BrushPropertyInputViewModel.cs index 92321d8a0..d382da86d 100644 --- a/src/Artemis.UI/DefaultTypes/PropertyInput/BrushPropertyInputViewModel.cs +++ b/src/Artemis.UI/DefaultTypes/PropertyInput/BrushPropertyInputViewModel.cs @@ -14,6 +14,7 @@ using Artemis.UI.Shared.Services.ProfileEditor.Commands; using Artemis.UI.Shared.Services.PropertyInput; using Avalonia.Threading; using ReactiveUI; +using LayerBrushPresetViewModel = Artemis.UI.Screens.ProfileEditor.Properties.Tree.Dialogs.LayerBrushPresetViewModel; namespace Artemis.UI.DefaultTypes.PropertyInput; diff --git a/src/Artemis.UI/Extensions/Bitmap.cs b/src/Artemis.UI/Extensions/Bitmap.cs index e30bc754f..13a07c35b 100644 --- a/src/Artemis.UI/Extensions/Bitmap.cs +++ b/src/Artemis.UI/Extensions/Bitmap.cs @@ -1,6 +1,5 @@ using System; using System.IO; -using System.Threading.Tasks; using Avalonia.Media.Imaging; using SkiaSharp; @@ -8,41 +7,75 @@ namespace Artemis.UI.Extensions; public class BitmapExtensions { - public static Bitmap LoadAndResize(string file, int size) + public static Bitmap LoadAndResize(string file, int size, bool fit) { using SKBitmap source = SKBitmap.Decode(file); - return Resize(source, size); + return Resize(source, size, fit); } - public static Bitmap LoadAndResize(Stream stream, int size) + public static Bitmap LoadAndResize(Stream stream, int size, bool fit) { 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); + return Resize(source, size, fit); } - private static Bitmap Resize(SKBitmap source, int size) + private static Bitmap Resize(SKBitmap source, int size, bool fit) { - // Get smaller dimension. - int minDim = Math.Min(source.Width, source.Height); + if (!fit) + { + // 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; + // 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); + // 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); + // 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); + // 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()); + // Encode via SKImage for compatibility + using SKImage image = SKImage.FromBitmap(resizedBitmap); + using SKData data = image.Encode(SKEncodedImageFormat.Png, 100); + return new Bitmap(data.AsStream()); + } + else + { + // Fit the image inside a size x size square without cropping. + // Compute scale based on the larger dimension. + float scale = (float)size / Math.Max(source.Width, source.Height); + int targetW = Math.Max(1, (int)Math.Floor(source.Width * scale)); + int targetH = Math.Max(1, (int)Math.Floor(source.Height * scale)); + + // Resize maintaining aspect ratio. + using SKBitmap resizedAspect = source.Resize(new SKImageInfo(targetW, targetH), SKFilterQuality.High); + + // Create final square canvas and draw the fitted image centered. + using SKBitmap finalBitmap = new(size, size); + using (SKCanvas canvas = new(finalBitmap)) + { + // Clear to transparent. + canvas.Clear(SKColors.Transparent); + + int offsetX = (size - targetW) / 2; + int offsetY = (size - targetH) / 2; + canvas.DrawBitmap(resizedAspect, new SKPoint(offsetX, offsetY)); + canvas.Flush(); + } + + using SKImage image = SKImage.FromBitmap(finalBitmap); + using SKData data = image.Encode(SKEncodedImageFormat.Png, 100); + return new Bitmap(data.AsStream()); + } } } \ No newline at end of file diff --git a/src/Artemis.UI/Routing/Routes.cs b/src/Artemis.UI/Routing/Routes.cs index b600e635f..11cf16abf 100644 --- a/src/Artemis.UI/Routing/Routes.cs +++ b/src/Artemis.UI/Routing/Routes.cs @@ -15,7 +15,6 @@ using Artemis.UI.Screens.Workshop.Library.Tabs; using Artemis.UI.Screens.Workshop.Plugins; using Artemis.UI.Screens.Workshop.Profile; using Artemis.UI.Shared.Routing; -using PluginDetailsViewModel = Artemis.UI.Screens.Workshop.Plugins.PluginDetailsViewModel; namespace Artemis.UI.Routing { @@ -65,7 +64,10 @@ namespace Artemis.UI.Routing new RouteRegistration("account"), new RouteRegistration("about") ]), - new RouteRegistration("profile-editor/{profileConfigurationId:guid}") + new RouteRegistration("profile/{profileConfigurationId:guid}", [ + new RouteRegistration("editor"), + new RouteRegistration("workshop") + ]), ]; } } \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Debugger/DebugView.axaml.cs b/src/Artemis.UI/Screens/Debugger/DebugView.axaml.cs index 6b7f58b7f..01ed1a181 100644 --- a/src/Artemis.UI/Screens/Debugger/DebugView.axaml.cs +++ b/src/Artemis.UI/Screens/Debugger/DebugView.axaml.cs @@ -21,11 +21,14 @@ public partial class DebugView : ReactiveAppWindow this.WhenActivated(d => { - Observable.FromEventPattern(x => ViewModel!.ActivationRequested += x, x => ViewModel!.ActivationRequested -= x).Subscribe(_ => - { - WindowState = WindowState.Normal; - Activate(); - }).DisposeWith(d); + DebugViewModel vm = ViewModel!; + Observable.FromEventPattern(x => vm.ActivationRequested += x, x => vm.ActivationRequested -= x) + .Subscribe(_ => + { + WindowState = WindowState.Normal; + Activate(); + }) + .DisposeWith(d); }); } diff --git a/src/Artemis.UI/Screens/Debugger/Tabs/Performance/PerformanceDebugView.axaml b/src/Artemis.UI/Screens/Debugger/Tabs/Performance/PerformanceDebugView.axaml index 3430c04d0..ad8b75823 100644 --- a/src/Artemis.UI/Screens/Debugger/Tabs/Performance/PerformanceDebugView.axaml +++ b/src/Artemis.UI/Screens/Debugger/Tabs/Performance/PerformanceDebugView.axaml @@ -2,8 +2,7 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" - xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia" - xmlns:debugger="clr-namespace:Artemis.UI.Screens.Debugger.Performance;assembly=Artemis.UI" + xmlns:debugger="clr-namespace:Artemis.UI.Screens.Debugger.Performance;assembly=Artemis.UI" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:DataType="debugger:PerformanceDebugViewModel" x:Class="Artemis.UI.Screens.Debugger.Performance.PerformanceDebugView"> diff --git a/src/Artemis.UI/Screens/Device/DeviceSettingsView.axaml b/src/Artemis.UI/Screens/Device/DeviceSettingsView.axaml index e3157ba28..028303543 100644 --- a/src/Artemis.UI/Screens/Device/DeviceSettingsView.axaml +++ b/src/Artemis.UI/Screens/Device/DeviceSettingsView.axaml @@ -3,7 +3,6 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia" - xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia" xmlns:shared="clr-namespace:Artemis.UI.Shared;assembly=Artemis.UI.Shared" xmlns:local="clr-namespace:Artemis.UI.Screens.Device" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" diff --git a/src/Artemis.UI/Screens/Device/Tabs/General/DeviceGeneralTabView.axaml b/src/Artemis.UI/Screens/Device/Tabs/General/DeviceGeneralTabView.axaml index c6d42e2b5..ba1c825b1 100644 --- a/src/Artemis.UI/Screens/Device/Tabs/General/DeviceGeneralTabView.axaml +++ b/src/Artemis.UI/Screens/Device/Tabs/General/DeviceGeneralTabView.axaml @@ -4,7 +4,6 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia" xmlns:converters="clr-namespace:Artemis.UI.Shared.Converters;assembly=Artemis.UI.Shared" - xmlns:device="clr-namespace:Artemis.UI.Screens.Device" xmlns:general="clr-namespace:Artemis.UI.Screens.Device.General" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="650" x:Class="Artemis.UI.Screens.Device.General.DeviceGeneralTabView" diff --git a/src/Artemis.UI/Screens/Device/Tabs/InputMappings/InputMappingsTabView.axaml b/src/Artemis.UI/Screens/Device/Tabs/InputMappings/InputMappingsTabView.axaml index f76881aed..0c3914b31 100644 --- a/src/Artemis.UI/Screens/Device/Tabs/InputMappings/InputMappingsTabView.axaml +++ b/src/Artemis.UI/Screens/Device/Tabs/InputMappings/InputMappingsTabView.axaml @@ -2,7 +2,6 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" - xmlns:device="clr-namespace:Artemis.UI.Screens.Device" xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia" xmlns:inputMappings="clr-namespace:Artemis.UI.Screens.Device.InputMappings" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" diff --git a/src/Artemis.UI/Screens/Device/Tabs/Layout/LayoutProviders/WorkshopLayoutView.axaml b/src/Artemis.UI/Screens/Device/Tabs/Layout/LayoutProviders/WorkshopLayoutView.axaml index 1020b7d03..386d1ef92 100644 --- a/src/Artemis.UI/Screens/Device/Tabs/Layout/LayoutProviders/WorkshopLayoutView.axaml +++ b/src/Artemis.UI/Screens/Device/Tabs/Layout/LayoutProviders/WorkshopLayoutView.axaml @@ -2,7 +2,6 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" - xmlns:services="clr-namespace:Artemis.WebClient.Workshop.Services;assembly=Artemis.WebClient.Workshop" xmlns:layoutProviders="clr-namespace:Artemis.UI.Screens.Device.Layout.LayoutProviders" xmlns:models="clr-namespace:Artemis.WebClient.Workshop.Models;assembly=Artemis.WebClient.Workshop" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" diff --git a/src/Artemis.UI/Screens/Device/Tabs/Leds/DeviceLedsTabView.axaml b/src/Artemis.UI/Screens/Device/Tabs/Leds/DeviceLedsTabView.axaml index 9b0ccf258..40f297554 100644 --- a/src/Artemis.UI/Screens/Device/Tabs/Leds/DeviceLedsTabView.axaml +++ b/src/Artemis.UI/Screens/Device/Tabs/Leds/DeviceLedsTabView.axaml @@ -2,7 +2,6 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" - xmlns:device="clr-namespace:Artemis.UI.Screens.Device" xmlns:converters="clr-namespace:Artemis.UI.Converters" xmlns:shared="clr-namespace:Artemis.UI.Shared.Converters;assembly=Artemis.UI.Shared" xmlns:leds="clr-namespace:Artemis.UI.Screens.Device.Leds" diff --git a/src/Artemis.UI/Screens/Home/HomeView.axaml b/src/Artemis.UI/Screens/Home/HomeView.axaml index b40dec73b..9af3fb2e5 100644 --- a/src/Artemis.UI/Screens/Home/HomeView.axaml +++ b/src/Artemis.UI/Screens/Home/HomeView.axaml @@ -22,7 +22,7 @@ Foreground="White" FontSize="32" Margin="30" - Text=" Welcome to Artemis, the unified RGB platform."> + Text="Welcome to Artemis, the unified RGB platform."> diff --git a/src/Artemis.UI/Screens/Plugins/Dialogs/PluginPrerequisitesInstallDialogView.axaml b/src/Artemis.UI/Screens/Plugins/Dialogs/PluginPrerequisitesInstallDialogView.axaml index efa8af53e..6961ac1be 100644 --- a/src/Artemis.UI/Screens/Plugins/Dialogs/PluginPrerequisitesInstallDialogView.axaml +++ b/src/Artemis.UI/Screens/Plugins/Dialogs/PluginPrerequisitesInstallDialogView.axaml @@ -7,7 +7,8 @@ xmlns:prerequisites="clr-namespace:Artemis.UI.Screens.Plugins.Prerequisites" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="Artemis.UI.Screens.Plugins.PluginPrerequisitesInstallDialogView" - x:DataType="plugins:PluginPrerequisitesInstallDialogViewModel"> + x:DataType="plugins:PluginPrerequisitesInstallDialogViewModel" + Width="800"> - + diff --git a/src/Artemis.UI/Screens/Plugins/Dialogs/PluginPrerequisitesUninstallDialogView.axaml b/src/Artemis.UI/Screens/Plugins/Dialogs/PluginPrerequisitesUninstallDialogView.axaml index 3ba9bec0c..6d3cfcd84 100644 --- a/src/Artemis.UI/Screens/Plugins/Dialogs/PluginPrerequisitesUninstallDialogView.axaml +++ b/src/Artemis.UI/Screens/Plugins/Dialogs/PluginPrerequisitesUninstallDialogView.axaml @@ -7,7 +7,8 @@ xmlns:prerequisites="clr-namespace:Artemis.UI.Screens.Plugins.Prerequisites" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="Artemis.UI.Screens.Plugins.PluginPrerequisitesUninstallDialogView" - x:DataType="plugins:PluginPrerequisitesUninstallDialogViewModel"> + x:DataType="plugins:PluginPrerequisitesUninstallDialogViewModel" + Width="800"> - + diff --git a/src/Artemis.UI/Screens/Plugins/Features/PluginFeatureView.axaml b/src/Artemis.UI/Screens/Plugins/Features/PluginFeatureView.axaml index 4141ec69e..44a79ca8f 100644 --- a/src/Artemis.UI/Screens/Plugins/Features/PluginFeatureView.axaml +++ b/src/Artemis.UI/Screens/Plugins/Features/PluginFeatureView.axaml @@ -3,8 +3,6 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia" - xmlns:shared="clr-namespace:Artemis.UI.Shared;assembly=Artemis.UI.Shared" - xmlns:plugins="clr-namespace:Artemis.UI.Screens.Plugins" xmlns:features="clr-namespace:Artemis.UI.Screens.Plugins.Features" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="Artemis.UI.Screens.Plugins.Features.PluginFeatureView" diff --git a/src/Artemis.UI/Screens/Plugins/PluginSettingsWindowView.axaml b/src/Artemis.UI/Screens/Plugins/PluginSettingsWindowView.axaml index 648239132..ccab77d68 100644 --- a/src/Artemis.UI/Screens/Plugins/PluginSettingsWindowView.axaml +++ b/src/Artemis.UI/Screens/Plugins/PluginSettingsWindowView.axaml @@ -4,7 +4,6 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:windowing="clr-namespace:FluentAvalonia.UI.Windowing;assembly=FluentAvalonia" xmlns:plugins="clr-namespace:Artemis.UI.Screens.Plugins" - xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia" xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia" xmlns:shared="clr-namespace:Artemis.UI.Shared;assembly=Artemis.UI.Shared" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" diff --git a/src/Artemis.UI/Screens/Plugins/PluginSettingsWindowView.axaml.cs b/src/Artemis.UI/Screens/Plugins/PluginSettingsWindowView.axaml.cs index 41920c857..9c027031b 100644 --- a/src/Artemis.UI/Screens/Plugins/PluginSettingsWindowView.axaml.cs +++ b/src/Artemis.UI/Screens/Plugins/PluginSettingsWindowView.axaml.cs @@ -19,14 +19,11 @@ public partial class PluginSettingsWindowView : ReactiveAppWindow { - Observable.FromEventPattern( - x => ViewModel!.ConfigurationViewModel.CloseRequested += x, - x => ViewModel!.ConfigurationViewModel.CloseRequested -= x - ) + PluginSettingsWindowViewModel vm = ViewModel!; + Observable.FromEventPattern(x => vm.ConfigurationViewModel.CloseRequested += x, x => vm.ConfigurationViewModel.CloseRequested -= x) .Subscribe(_ => Close()) .DisposeWith(disposables); } ); } - } \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Plugins/PluginView.axaml b/src/Artemis.UI/Screens/Plugins/PluginView.axaml index 4e4aecc13..a0a6beb70 100644 --- a/src/Artemis.UI/Screens/Plugins/PluginView.axaml +++ b/src/Artemis.UI/Screens/Plugins/PluginView.axaml @@ -4,7 +4,6 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:plugins="clr-namespace:Artemis.UI.Screens.Plugins" xmlns:shared="clr-namespace:Artemis.UI.Shared;assembly=Artemis.UI.Shared" - xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia" xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="Artemis.UI.Screens.Plugins.PluginView" diff --git a/src/Artemis.UI/Screens/Plugins/PluginViewModel.cs b/src/Artemis.UI/Screens/Plugins/PluginViewModel.cs index ae312a2be..82d1f4b86 100644 --- a/src/Artemis.UI/Screens/Plugins/PluginViewModel.cs +++ b/src/Artemis.UI/Screens/Plugins/PluginViewModel.cs @@ -6,11 +6,11 @@ using System.Reactive; using System.Reactive.Disposables; using System.Threading.Tasks; using Artemis.Core; -using Artemis.Core.Services; using Artemis.UI.Exceptions; +using Artemis.UI.Services; +using Artemis.UI.Services.Interfaces; using Artemis.UI.Shared; using Artemis.UI.Shared.Services; -using Artemis.UI.Shared.Services.Builders; using Avalonia.Controls; using Avalonia.Threading; using Material.Icons; @@ -21,9 +21,7 @@ namespace Artemis.UI.Screens.Plugins; public partial class PluginViewModel : ActivatableViewModelBase { - private readonly ICoreService _coreService; - private readonly INotificationService _notificationService; - private readonly IPluginManagementService _pluginManagementService; + private readonly IPluginInteractionService _pluginInteractionService; private readonly IWindowService _windowService; private Window? _settingsWindow; [Notify] private bool _canInstallPrerequisites; @@ -31,18 +29,11 @@ public partial class PluginViewModel : ActivatableViewModelBase [Notify] private bool _enabling; [Notify] private Plugin _plugin; - public PluginViewModel(Plugin plugin, - ReactiveCommand? reload, - ICoreService coreService, - IWindowService windowService, - INotificationService notificationService, - IPluginManagementService pluginManagementService) + public PluginViewModel(Plugin plugin, ReactiveCommand? reload, IWindowService windowService, IPluginInteractionService pluginInteractionService) { _plugin = plugin; - _coreService = coreService; _windowService = windowService; - _notificationService = notificationService; - _pluginManagementService = pluginManagementService; + _pluginInteractionService = pluginInteractionService; Platforms = new ObservableCollection(); if (Plugin.Info.Platforms != null) @@ -88,7 +79,6 @@ public partial class PluginViewModel : ActivatableViewModelBase public ReactiveCommand OpenPluginDirectory { get; } public ObservableCollection Platforms { get; } - public string Type => Plugin.GetType().BaseType?.Name ?? Plugin.GetType().Name; public bool IsEnabled => Plugin.IsEnabled; public async Task UpdateEnabled(bool enable) @@ -97,55 +87,15 @@ public partial class PluginViewModel : ActivatableViewModelBase return; if (!enable) - { - try - { - await Task.Run(() => _pluginManagementService.DisablePlugin(Plugin, true)); - } - catch (Exception e) - { - await ShowUpdateEnableFailure(enable, e); - } - finally - { - this.RaisePropertyChanged(nameof(IsEnabled)); - } - - return; - } - - try + await _pluginInteractionService.DisablePlugin(Plugin); + else { Enabling = true; - if (Plugin.Info.RequiresAdmin && !_coreService.IsElevated) - { - bool confirmed = await _windowService.ShowConfirmContentDialog("Enable plugin", "This plugin requires admin rights, are you sure you want to enable it?"); - if (!confirmed) - return; - } - - // Check if all prerequisites are met async - List subjects = new() {Plugin.Info}; - subjects.AddRange(Plugin.Features.Where(f => f.AlwaysEnabled || f.EnabledInStorage)); - - if (subjects.Any(s => !s.ArePrerequisitesMet())) - { - await PluginPrerequisitesInstallDialogViewModel.Show(_windowService, subjects); - if (!subjects.All(s => s.ArePrerequisitesMet())) - return; - } - - await Task.Run(() => _pluginManagementService.EnablePlugin(Plugin, true, true)); - } - catch (Exception e) - { - await ShowUpdateEnableFailure(enable, e); - } - finally - { + await _pluginInteractionService.EnablePlugin(Plugin, false); Enabling = false; - this.RaisePropertyChanged(nameof(IsEnabled)); } + + this.RaisePropertyChanged(nameof(IsEnabled)); } public void CheckPrerequisites() @@ -220,43 +170,12 @@ public partial class PluginViewModel : ActivatableViewModelBase private async Task ExecuteRemoveSettings() { - bool confirmed = await _windowService.ShowConfirmContentDialog("Clear plugin settings", "Are you sure you want to clear the settings of this plugin?"); - if (!confirmed) - return; - - bool wasEnabled = IsEnabled; - - if (IsEnabled) - await UpdateEnabled(false); - - _pluginManagementService.RemovePluginSettings(Plugin); - - if (wasEnabled) - await UpdateEnabled(true); - - _notificationService.CreateNotification().WithTitle("Cleared plugin settings.").Show(); + await _pluginInteractionService.RemovePluginSettings(Plugin); } private async Task ExecuteRemove() { - bool confirmed = await _windowService.ShowConfirmContentDialog("Remove plugin", "Are you sure you want to remove this plugin?"); - if (!confirmed) - return; - - // If the plugin or any of its features has uninstall actions, offer to run these - await ExecuteRemovePrerequisites(true); - - try - { - _pluginManagementService.RemovePlugin(Plugin, false); - } - catch (Exception e) - { - _windowService.ShowExceptionDialog("Failed to remove plugin", e); - throw; - } - - _notificationService.CreateNotification().WithTitle("Removed plugin.").Show(); + await _pluginInteractionService.RemovePlugin(Plugin); } private void ExecuteShowLogsFolder() @@ -271,20 +190,6 @@ public partial class PluginViewModel : ActivatableViewModelBase } } - private async Task ShowUpdateEnableFailure(bool enable, Exception e) - { - string action = enable ? "enable" : "disable"; - ContentDialogBuilder builder = _windowService.CreateContentDialog() - .WithTitle($"Failed to {action} plugin {Plugin.Info.Name}") - .WithContent(e.Message) - .HavingPrimaryButton(b => b.WithText("View logs").WithCommand(ShowLogsFolder)); - // If available, add a secondary button pointing to the support page - if (Plugin.Info.HelpPage != null) - builder = builder.HavingSecondaryButton(b => b.WithText("Open support page").WithAction(() => Utilities.OpenUrl(Plugin.Info.HelpPage.ToString()))); - - await builder.ShowAsync(); - } - private void OnPluginToggled(object? sender, EventArgs e) { Dispatcher.UIThread.Post(() => @@ -299,9 +204,9 @@ public partial class PluginViewModel : ActivatableViewModelBase { if (IsEnabled) return; - + await UpdateEnabled(true); - + // If enabling failed, don't offer to show the settings if (!IsEnabled || Plugin.ConfigurationDialog == null) return; diff --git a/src/Artemis.UI/Screens/Plugins/Prerequisites/PluginPrerequisiteActionView.axaml b/src/Artemis.UI/Screens/Plugins/Prerequisites/PluginPrerequisiteActionView.axaml index d7a09a975..953124768 100644 --- a/src/Artemis.UI/Screens/Plugins/Prerequisites/PluginPrerequisiteActionView.axaml +++ b/src/Artemis.UI/Screens/Plugins/Prerequisites/PluginPrerequisiteActionView.axaml @@ -2,7 +2,6 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" - xmlns:plugins="clr-namespace:Artemis.UI.Screens.Plugins" xmlns:prerequisites="clr-namespace:Artemis.UI.Screens.Plugins.Prerequisites" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="Artemis.UI.Screens.Plugins.Prerequisites.PluginPrerequisiteActionView" diff --git a/src/Artemis.UI/Screens/Plugins/Prerequisites/PluginPrerequisiteView.axaml b/src/Artemis.UI/Screens/Plugins/Prerequisites/PluginPrerequisiteView.axaml index 6fa41801c..36e6b229d 100644 --- a/src/Artemis.UI/Screens/Plugins/Prerequisites/PluginPrerequisiteView.axaml +++ b/src/Artemis.UI/Screens/Plugins/Prerequisites/PluginPrerequisiteView.axaml @@ -2,7 +2,6 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" - xmlns:plugins="clr-namespace:Artemis.UI.Screens.Plugins" xmlns:prerequisites="clr-namespace:Artemis.UI.Screens.Plugins.Prerequisites" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="Artemis.UI.Screens.Plugins.Prerequisites.PluginPrerequisiteView" diff --git a/src/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/ProfileTreeViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/ProfileTreeViewModel.cs index 38f8c6856..16bf19759 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/ProfileTreeViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/Panels/ProfileTree/ProfileTreeViewModel.cs @@ -79,11 +79,6 @@ public partial class ProfileTreeViewModel : TreeItemViewModel public override bool SupportsChildren => true; - public void UpdateCanPaste() - { - throw new NotImplementedException(); - } - protected override Task ExecuteDuplicate() { throw new NotSupportedException(); diff --git a/src/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/VisualEditorView.axaml b/src/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/VisualEditorView.axaml index 361ca360d..6708c965b 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/VisualEditorView.axaml +++ b/src/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/VisualEditorView.axaml @@ -89,7 +89,7 @@ Background="{DynamicResource ControlFillColorDefaultBrush}" IsVisible="{CompiledBinding ProfileConfiguration, Converter={x:Static ObjectConverters.IsNotNull}}"> - + diff --git a/src/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/VisualEditorView.axaml.cs b/src/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/VisualEditorView.axaml.cs index f7e4491a5..5c613747c 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/VisualEditorView.axaml.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/Panels/VisualEditor/VisualEditorView.axaml.cs @@ -27,8 +27,9 @@ public partial class VisualEditorView : ReactiveUserControl { - ViewModel!.AutoFitRequested += ViewModelOnAutoFitRequested; - Disposable.Create(() => ViewModel.AutoFitRequested -= ViewModelOnAutoFitRequested).DisposeWith(d); + VisualEditorViewModel vm = ViewModel!; + vm!.AutoFitRequested += ViewModelOnAutoFitRequested; + Disposable.Create(() => vm.AutoFitRequested -= ViewModelOnAutoFitRequested).DisposeWith(d); }); this.WhenAnyValue(v => v.Bounds).Where(_ => !_movedByUser).Subscribe(_ => AutoFit(true)); diff --git a/src/Artemis.UI/Screens/ProfileEditor/ProfileEditorViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/ProfileEditorViewModel.cs index c92e34dcc..352a49339 100644 --- a/src/Artemis.UI/Screens/ProfileEditor/ProfileEditorViewModel.cs +++ b/src/Artemis.UI/Screens/ProfileEditor/ProfileEditorViewModel.cs @@ -15,11 +15,8 @@ using Artemis.UI.Screens.ProfileEditor.StatusBar; using Artemis.UI.Screens.ProfileEditor.VisualEditor; using Artemis.UI.Shared; using Artemis.UI.Shared.Routing; -using Artemis.UI.Shared.Services; using Artemis.UI.Shared.Services.MainWindow; using Artemis.UI.Shared.Services.ProfileEditor; -using Artemis.WebClient.Workshop.Models; -using Artemis.WebClient.Workshop.Services; using DynamicData; using DynamicData.Binding; using PropertyChanged.SourceGenerator; @@ -27,14 +24,12 @@ using ReactiveUI; namespace Artemis.UI.Screens.ProfileEditor; -public partial class ProfileEditorViewModel : RoutableScreen, IMainScreenViewModel +public partial class ProfileEditorViewModel : RoutableScreen, IMainScreenViewModel { private readonly IProfileEditorService _profileEditorService; private readonly IProfileService _profileService; private readonly ISettingsService _settingsService; private readonly IMainWindowService _mainWindowService; - private readonly IWorkshopService _workshopService; - private readonly IWindowService _windowService; private readonly SourceList _tools; private ObservableAsPropertyHelper? _history; private ObservableAsPropertyHelper? _suspendedEditing; @@ -53,16 +48,12 @@ public partial class ProfileEditorViewModel : RoutableScreen toolViewModels, IMainWindowService mainWindowService, - IInputService inputService, - IWorkshopService workshopService, - IWindowService windowService) + IInputService inputService) { _profileService = profileService; _profileEditorService = profileEditorService; _settingsService = settingsService; _mainWindowService = mainWindowService; - _workshopService = workshopService; - _windowService = windowService; _tools = new SourceList(); _tools.AddRange(toolViewModels); @@ -75,6 +66,7 @@ public partial class ProfileEditorViewModel : RoutableScreen /// - public override async Task OnNavigating(ProfileEditorViewModelParameters parameters, NavigationArguments args, CancellationToken cancellationToken) + public override async Task OnNavigating(ProfileViewModelParameters parameters, NavigationArguments args, CancellationToken cancellationToken) { ProfileConfiguration? profileConfiguration = _profileService.ProfileCategories.SelectMany(c => c.ProfileConfigurations).FirstOrDefault(c => c.ProfileId == parameters.ProfileId); @@ -204,23 +196,6 @@ public partial class ProfileEditorViewModel : RoutableScreen + + + + + + diff --git a/src/Artemis.UI/Screens/ProfileEditor/ProfileView.axaml.cs b/src/Artemis.UI/Screens/ProfileEditor/ProfileView.axaml.cs new file mode 100644 index 000000000..032140095 --- /dev/null +++ b/src/Artemis.UI/Screens/ProfileEditor/ProfileView.axaml.cs @@ -0,0 +1,19 @@ +using System; +using System.Reactive.Disposables; +using Artemis.UI.Shared.Routing; +using Avalonia.ReactiveUI; +using Avalonia.Threading; +using ReactiveUI; +namespace Artemis.UI.Screens.ProfileEditor; + +public partial class ProfileView : ReactiveUserControl +{ + public ProfileView() + { + InitializeComponent(); + this.WhenActivated(d => ViewModel.WhenAnyValue(vm => vm.Screen) + .WhereNotNull() + .Subscribe(screen => RouterFrame.NavigateFromObject(screen)) + .DisposeWith(d)); + } +} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/ProfileEditor/ProfileViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/ProfileViewModel.cs new file mode 100644 index 000000000..42b99d05e --- /dev/null +++ b/src/Artemis.UI/Screens/ProfileEditor/ProfileViewModel.cs @@ -0,0 +1,62 @@ +using System; +using System.Linq; +using System.Reactive.Linq; +using System.Threading; +using System.Threading.Tasks; +using Artemis.Core; +using Artemis.Core.Services; +using Artemis.UI.Shared; +using Artemis.UI.Shared.Routing; +using Artemis.WebClient.Workshop.Models; +using Artemis.WebClient.Workshop.Services; +using ReactiveUI; + +namespace Artemis.UI.Screens.ProfileEditor; + +public class ProfileViewModel : RoutableHostScreen, IMainScreenViewModel +{ + private readonly IProfileService _profileService; + private readonly IWorkshopService _workshopService; + private readonly ObservableAsPropertyHelper _titleBarViewModel; + + public ProfileViewModel(IProfileService profileService, IWorkshopService workshopService) + { + _profileService = profileService; + _workshopService = workshopService; + + _titleBarViewModel = this.WhenAnyValue(vm => vm.Screen).Select(screen => screen as IMainScreenViewModel) + .Select(mainScreen => mainScreen?.TitleBarViewModel) + .ToProperty(this, vm => vm.TitleBarViewModel); + } + + public ViewModelBase? TitleBarViewModel => _titleBarViewModel.Value; + + /// + public override async Task OnNavigating(ProfileViewModelParameters parameters, NavigationArguments args, CancellationToken cancellationToken) + { + ProfileConfiguration? profileConfiguration = _profileService.ProfileCategories.SelectMany(c => c.ProfileConfigurations).FirstOrDefault(c => c.ProfileId == parameters.ProfileId); + + // If the profile doesn't exist, cancel navigation + if (profileConfiguration == null) + { + args.Cancel(); + return; + } + + // If the profile is from the workshop, redirect to the workshop page + InstalledEntry? workshopEntry = _workshopService.GetInstalledEntryByProfile(profileConfiguration); + if (workshopEntry != null && workshopEntry.AutoUpdate) + { + if (!args.Path.EndsWith("workshop")) + await args.Router.Navigate($"profile/{parameters.ProfileId}/workshop"); + } + // Otherwise, show the profile editor if not already on the editor page + else if (!args.Path.EndsWith("editor")) + await args.Router.Navigate($"profile/{parameters.ProfileId}/editor"); + } +} + +public class ProfileViewModelParameters +{ + public Guid ProfileId { get; set; } +} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/ProfileEditor/WorkshopProfileView.axaml b/src/Artemis.UI/Screens/ProfileEditor/WorkshopProfileView.axaml new file mode 100644 index 000000000..109d960fd --- /dev/null +++ b/src/Artemis.UI/Screens/ProfileEditor/WorkshopProfileView.axaml @@ -0,0 +1,23 @@ + + + + + + + The profile you're opening is a workshop profile. + + + You cannot make change to it without disabling auto-update, as any updates to the profile on the workshop would override your own modifications. + + + + + + \ No newline at end of file diff --git a/src/Artemis.UI/Screens/ProfileEditor/WorkshopProfileView.axaml.cs b/src/Artemis.UI/Screens/ProfileEditor/WorkshopProfileView.axaml.cs new file mode 100644 index 000000000..926bf205e --- /dev/null +++ b/src/Artemis.UI/Screens/ProfileEditor/WorkshopProfileView.axaml.cs @@ -0,0 +1,11 @@ +using Avalonia.ReactiveUI; + +namespace Artemis.UI.Screens.ProfileEditor; + +public partial class WorkshopProfileView : ReactiveUserControl +{ + public WorkshopProfileView() + { + InitializeComponent(); + } +} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/ProfileEditor/WorkshopProfileViewModel.cs b/src/Artemis.UI/Screens/ProfileEditor/WorkshopProfileViewModel.cs new file mode 100644 index 000000000..87e091dab --- /dev/null +++ b/src/Artemis.UI/Screens/ProfileEditor/WorkshopProfileViewModel.cs @@ -0,0 +1,65 @@ +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Artemis.Core; +using Artemis.Core.Services; +using Artemis.UI.Screens.Workshop.Library.Tabs; +using Artemis.UI.Shared.Routing; +using Artemis.WebClient.Workshop.Models; +using Artemis.WebClient.Workshop.Services; +using PropertyChanged.SourceGenerator; + +namespace Artemis.UI.Screens.ProfileEditor; + +public partial class WorkshopProfileViewModel : RoutableScreen +{ + private readonly IProfileService _profileService; + private readonly IWorkshopService _workshopService; + private readonly IRouter _router; + private readonly Func _getInstalledTabItemViewModel; + + [Notify] private ProfileConfiguration? _profileConfiguration; + [Notify] private InstalledEntry? _workshopEntry; + [Notify] private InstalledTabItemViewModel? _entryViewModel; + + public WorkshopProfileViewModel(IProfileService profileService, IWorkshopService workshopService, IRouter router, Func getInstalledTabItemViewModel) + { + _profileService = profileService; + _workshopService = workshopService; + _router = router; + _getInstalledTabItemViewModel = getInstalledTabItemViewModel; + ParameterSource = ParameterSource.Route; + } + + public async Task DisableAutoUpdate() + { + if (WorkshopEntry != null) + { + _workshopService.SetAutoUpdate(WorkshopEntry, false); + } + + if (ProfileConfiguration != null) + { + await _router.Navigate($"profile/{ProfileConfiguration.ProfileId}/editor"); + } + } + + public override Task OnNavigating(ProfileViewModelParameters parameters, NavigationArguments args, CancellationToken cancellationToken) + { + ProfileConfiguration = _profileService.ProfileCategories.SelectMany(c => c.ProfileConfigurations).FirstOrDefault(c => c.ProfileId == parameters.ProfileId); + + // If the profile doesn't exist, cancel navigation + if (ProfileConfiguration == null) + { + args.Cancel(); + return Task.CompletedTask; + } + + WorkshopEntry = _workshopService.GetInstalledEntryByProfile(ProfileConfiguration); + EntryViewModel = WorkshopEntry != null ? _getInstalledTabItemViewModel(WorkshopEntry) : null; + if (EntryViewModel != null) + EntryViewModel.DisplayManagement = false; + return Task.CompletedTask; + } +} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Root/RootViewModel.cs b/src/Artemis.UI/Screens/Root/RootViewModel.cs index df894ec9b..b3433b5da 100644 --- a/src/Artemis.UI/Screens/Root/RootViewModel.cs +++ b/src/Artemis.UI/Screens/Root/RootViewModel.cs @@ -83,7 +83,7 @@ public class RootViewModel : RoutableHostScreen, IMainWindowProv _coreService.Initialized += (_, _) => Dispatcher.UIThread.InvokeAsync(OpenMainWindow); } - Task.Run(() => + Task.Run(async () => { try { @@ -93,7 +93,7 @@ public class RootViewModel : RoutableHostScreen, IMainWindowProv return; // Workshop service goes first so it has a chance to clean up old workshop entries and introduce new ones - workshopService.Initialize(); + await workshopService.Initialize(); // Core is initialized now that everything is ready to go coreService.Initialize(); diff --git a/src/Artemis.UI/Screens/Root/SplashView.axaml.cs b/src/Artemis.UI/Screens/Root/SplashView.axaml.cs index cc54a133c..082586d98 100644 --- a/src/Artemis.UI/Screens/Root/SplashView.axaml.cs +++ b/src/Artemis.UI/Screens/Root/SplashView.axaml.cs @@ -19,7 +19,8 @@ public partial class SplashView : ReactiveWindow #endif this.WhenActivated(disposables => { - Observable.FromEventPattern(x => ViewModel!.CoreService.Initialized += x, x => ViewModel!.CoreService.Initialized -= x) + SplashViewModel vm = ViewModel!; + Observable.FromEventPattern(x => vm.CoreService.Initialized += x, x => vm.CoreService.Initialized -= x) .Subscribe(_ => Dispatcher.UIThread.Post(Close)) .DisposeWith(disposables); }); diff --git a/src/Artemis.UI/Screens/Root/SplashViewModel.cs b/src/Artemis.UI/Screens/Root/SplashViewModel.cs index dd9622421..2900d9948 100644 --- a/src/Artemis.UI/Screens/Root/SplashViewModel.cs +++ b/src/Artemis.UI/Screens/Root/SplashViewModel.cs @@ -2,6 +2,7 @@ using Artemis.Core; using Artemis.Core.Services; using Artemis.UI.Shared; +using Artemis.WebClient.Workshop.Services; using PropertyChanged.SourceGenerator; namespace Artemis.UI.Screens.Root; @@ -10,12 +11,12 @@ public partial class SplashViewModel : ViewModelBase { [Notify] private string _status; - public SplashViewModel(ICoreService coreService, IPluginManagementService pluginManagementService) + public SplashViewModel(ICoreService coreService, IPluginManagementService pluginManagementService, IWorkshopService workshopService) { CoreService = coreService; _status = "Initializing Core"; - pluginManagementService.CopyingBuildInPlugins += OnPluginManagementServiceOnCopyingBuildInPluginsManagement; + workshopService.MigratingBuildInPlugins += WorkshopServiceOnMigratingBuildInPlugins; pluginManagementService.PluginLoading += OnPluginManagementServiceOnPluginManagementLoading; pluginManagementService.PluginLoaded += OnPluginManagementServiceOnPluginManagementLoaded; pluginManagementService.PluginEnabling += PluginManagementServiceOnPluginManagementEnabling; @@ -25,6 +26,11 @@ public partial class SplashViewModel : ViewModelBase } public ICoreService CoreService { get; } + + private void WorkshopServiceOnMigratingBuildInPlugins(object? sender, EventArgs args) + { + Status = "Migrating built-in plugins"; + } private void OnPluginManagementServiceOnPluginManagementLoaded(object? sender, PluginEventArgs args) { @@ -55,9 +61,4 @@ public partial class SplashViewModel : ViewModelBase { Status = "Initializing UI"; } - - private void OnPluginManagementServiceOnCopyingBuildInPluginsManagement(object? sender, EventArgs args) - { - Status = "Updating built-in plugins"; - } } \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Settings/Tabs/AboutTabView.axaml b/src/Artemis.UI/Screens/Settings/Tabs/AboutTabView.axaml index f81713c04..f1156be2f 100644 --- a/src/Artemis.UI/Screens/Settings/Tabs/AboutTabView.axaml +++ b/src/Artemis.UI/Screens/Settings/Tabs/AboutTabView.axaml @@ -2,7 +2,6 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 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:vm="clr-namespace:Artemis.UI.Screens.Settings;assembly=Artemis.UI" xmlns:il="clr-namespace:AsyncImageLoader;assembly=AsyncImageLoader.Avalonia" diff --git a/src/Artemis.UI/Screens/Settings/Tabs/PluginsTabView.axaml b/src/Artemis.UI/Screens/Settings/Tabs/PluginsTabView.axaml index 4fb1a771a..5516e3678 100644 --- a/src/Artemis.UI/Screens/Settings/Tabs/PluginsTabView.axaml +++ b/src/Artemis.UI/Screens/Settings/Tabs/PluginsTabView.axaml @@ -3,7 +3,6 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:settings="clr-namespace:Artemis.UI.Screens.Settings" - xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia" xmlns:plugins="clr-namespace:Artemis.UI.Screens.Plugins" mc:Ignorable="d" d:DesignWidth="1200" d:DesignHeight="450" x:Class="Artemis.UI.Screens.Settings.PluginsTabView" diff --git a/src/Artemis.UI/Screens/Settings/Tabs/ReleasesTabView.axaml b/src/Artemis.UI/Screens/Settings/Tabs/ReleasesTabView.axaml index c4ff90b0c..04d3c99a5 100644 --- a/src/Artemis.UI/Screens/Settings/Tabs/ReleasesTabView.axaml +++ b/src/Artemis.UI/Screens/Settings/Tabs/ReleasesTabView.axaml @@ -3,7 +3,6 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:settings="clr-namespace:Artemis.UI.Screens.Settings" - xmlns:updating="clr-namespace:Artemis.UI.Screens.Settings.Updating" xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia" xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia" xmlns:ui="clr-namespace:Artemis.UI" diff --git a/src/Artemis.UI/Screens/Sidebar/Dialogs/ProfileConfigurationEditViewModel.cs b/src/Artemis.UI/Screens/Sidebar/Dialogs/ProfileConfigurationEditViewModel.cs index afa278f98..7518b2862 100644 --- a/src/Artemis.UI/Screens/Sidebar/Dialogs/ProfileConfigurationEditViewModel.cs +++ b/src/Artemis.UI/Screens/Sidebar/Dialogs/ProfileConfigurationEditViewModel.cs @@ -187,7 +187,7 @@ public partial class ProfileConfigurationEditViewModel : DialogViewModelBase vm.SelectedProfileConfiguration) .WhereNotNull() - .Subscribe(s => _router.Navigate($"profile-editor/{s.ProfileConfiguration.ProfileId}", new RouterNavigationOptions {IgnoreOnPartialMatch = true, RecycleScreens = false})) + .Subscribe(s => _router.Navigate($"profile/{s.ProfileConfiguration.ProfileId}/editor", new RouterNavigationOptions {IgnoreOnPartialMatch = true, RecycleScreens = false})) .DisposeWith(d); _router.CurrentPath.WhereNotNull().Subscribe(r => SelectedProfileConfiguration = ProfileConfigurations.FirstOrDefault(c => c.Matches(r))).DisposeWith(d); diff --git a/src/Artemis.UI/Screens/Sidebar/SidebarProfileConfigurationView.axaml b/src/Artemis.UI/Screens/Sidebar/SidebarProfileConfigurationView.axaml index 065814f09..5e7884967 100644 --- a/src/Artemis.UI/Screens/Sidebar/SidebarProfileConfigurationView.axaml +++ b/src/Artemis.UI/Screens/Sidebar/SidebarProfileConfigurationView.axaml @@ -77,7 +77,8 @@ VerticalAlignment="Center" ConfigurationIcon="{CompiledBinding ProfileConfiguration.Icon}" Width="22" - Height="22" + Height="22" + CornerRadius="4" Margin="0 0 5 0"> diff --git a/src/Artemis.UI/Screens/Sidebar/SidebarProfileConfigurationViewModel.cs b/src/Artemis.UI/Screens/Sidebar/SidebarProfileConfigurationViewModel.cs index 99f9330d7..5596c3e9f 100644 --- a/src/Artemis.UI/Screens/Sidebar/SidebarProfileConfigurationViewModel.cs +++ b/src/Artemis.UI/Screens/Sidebar/SidebarProfileConfigurationViewModel.cs @@ -132,6 +132,6 @@ public class SidebarProfileConfigurationViewModel : ActivatableViewModelBase public bool Matches(string s) { - return s.StartsWith("profile-editor") && s.EndsWith(ProfileConfiguration.ProfileId.ToString()); + return s.StartsWith($"profile/{ProfileConfiguration.ProfileId}"); } } \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Sidebar/SidebarView.axaml b/src/Artemis.UI/Screens/Sidebar/SidebarView.axaml index 454becd6b..8a656ad1d 100644 --- a/src/Artemis.UI/Screens/Sidebar/SidebarView.axaml +++ b/src/Artemis.UI/Screens/Sidebar/SidebarView.axaml @@ -3,7 +3,6 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia" - xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia" xmlns:sidebar="clr-namespace:Artemis.UI.Screens.Sidebar" mc:Ignorable="d" d:DesignWidth="240" d:DesignHeight="450" x:Class="Artemis.UI.Screens.Sidebar.SidebarView" diff --git a/src/Artemis.UI/Screens/StartupWizard/StartupWizardView.axaml b/src/Artemis.UI/Screens/StartupWizard/StartupWizardView.axaml index 67286303f..89f20e955 100644 --- a/src/Artemis.UI/Screens/StartupWizard/StartupWizardView.axaml +++ b/src/Artemis.UI/Screens/StartupWizard/StartupWizardView.axaml @@ -11,16 +11,16 @@ x:DataType="startupWizard:StartupWizardViewModel" Icon="/Assets/Images/Logo/application.ico" Title="Artemis | Startup wizard" - Width="1000" + Width="1050" Height="735" WindowStartupLocation="CenterOwner"> - - + + + Artemis 2 - @@ -32,13 +32,11 @@ - - - - + + - - - - - - - + + + + + + + \ No newline at end of file diff --git a/src/Artemis.UI/Screens/StartupWizard/Steps/DefaultEntriesStepView.axaml b/src/Artemis.UI/Screens/StartupWizard/Steps/DefaultEntriesStepView.axaml new file mode 100644 index 000000000..f474cfe5b --- /dev/null +++ b/src/Artemis.UI/Screens/StartupWizard/Steps/DefaultEntriesStepView.axaml @@ -0,0 +1,77 @@ + + + + + + Below is a list of default features that can be installed to get you started with Artemis. You can always install or uninstall features later via the Workshop. + + + + + Fetching default features from the Artemis workshop... + + + + + Device providers + + + + + + + + + + + Essentials + + + + + + + + + + + Other features + + + + + + + + + + + + + + + + + + + Currently installing: + + + + + + feature(s) remaining, hold on tight! + + + + \ No newline at end of file diff --git a/src/Artemis.UI/Screens/StartupWizard/Steps/DefaultEntriesStepView.axaml.cs b/src/Artemis.UI/Screens/StartupWizard/Steps/DefaultEntriesStepView.axaml.cs new file mode 100644 index 000000000..1050a6b30 --- /dev/null +++ b/src/Artemis.UI/Screens/StartupWizard/Steps/DefaultEntriesStepView.axaml.cs @@ -0,0 +1,14 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; +using Avalonia.ReactiveUI; + +namespace Artemis.UI.Screens.StartupWizard.Steps; + +public partial class DefaultEntriesStepView : ReactiveUserControl +{ + public DefaultEntriesStepView() + { + InitializeComponent(); + } +} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/StartupWizard/Steps/DefaultEntriesStepViewModel.cs b/src/Artemis.UI/Screens/StartupWizard/Steps/DefaultEntriesStepViewModel.cs new file mode 100644 index 000000000..5b631e1f9 --- /dev/null +++ b/src/Artemis.UI/Screens/StartupWizard/Steps/DefaultEntriesStepViewModel.cs @@ -0,0 +1,135 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using System.Reactive.Linq; +using System.Threading; +using System.Threading.Tasks; +using Artemis.Core.Services; +using Artemis.UI.Extensions; +using Artemis.UI.Shared.Services; +using Artemis.UI.Shared.Utilities; +using Artemis.WebClient.Workshop; +using Artemis.WebClient.Workshop.Services; +using PropertyChanged.SourceGenerator; +using ReactiveUI; +using StrawberryShake; + +namespace Artemis.UI.Screens.StartupWizard.Steps; + +public partial class DefaultEntriesStepViewModel : WizardStepViewModel +{ + [Notify] private bool _fetchingDefaultEntries = true; + [Notify] private int _installedEntries; + [Notify] private int _totalEntries = 1; + [Notify] private DefaultEntryItemViewModel? _currentEntry; + + private readonly IDeviceService _deviceService; + private readonly IWorkshopClient _client; + private readonly Func _getDefaultEntryItemViewModel; + private readonly ObservableAsPropertyHelper _remainingEntries; + + public ObservableCollection DeviceProviderEntryViewModels { get; } = []; + public ObservableCollection EssentialEntryViewModels { get; } = []; + public ObservableCollection OtherEntryViewModels { get; } = []; + public int RemainingEntries => _remainingEntries.Value; + + public DefaultEntriesStepViewModel(IWorkshopService workshopService, IDeviceService deviceService, IWorkshopClient client, + Func getDefaultEntryItemViewModel) + { + _deviceService = deviceService; + _client = client; + _getDefaultEntryItemViewModel = getDefaultEntryItemViewModel; + _remainingEntries = this.WhenAnyValue(vm => vm.InstalledEntries, vm => vm.TotalEntries) + .Select(t => t.Item2 - t.Item1 + 1) + .ToProperty(this, vm => vm.RemainingEntries); + + ContinueText = "Install selected entries"; + Continue = ReactiveCommand.CreateFromTask(async ct => + { + await Install(ct); + ExecuteContinue(); + }, this.WhenAnyValue(vm => vm.FetchingDefaultEntries).Select(b => !b)); + GoBack = ReactiveCommand.Create(() => Wizard.ChangeScreen()); + + this.WhenActivatedAsync(async d => + { + CancellationToken ct = d.AsCancellationToken(); + if (await workshopService.ValidateWorkshopStatus(false, ct)) + await GetDefaultEntries(d.AsCancellationToken()); + else if (!ct.IsCancellationRequested) + Wizard.ChangeScreen(); + }); + } + + private void ExecuteContinue() + { + // Without devices skip to the last step + if (_deviceService.EnabledDevices.Count == 0) + Wizard.ChangeScreen(); + else + Wizard.ChangeScreen(); + } + + private async Task Install(CancellationToken cancellationToken) + { + List entries = + [ + ..DeviceProviderEntryViewModels.Where(e => e.ShouldInstall && !e.IsInstalled), + ..EssentialEntryViewModels.Where(e => e.ShouldInstall && !e.IsInstalled), + ..OtherEntryViewModels.Where(e => e.ShouldInstall && !e.IsInstalled) + ]; + InstalledEntries = 0; + TotalEntries = entries.Count; + + // Continue to the next screen if there are no entries to install + if (TotalEntries == 0) + return; + + foreach (DefaultEntryItemViewModel defaultEntryItemViewModel in entries) + { + cancellationToken.ThrowIfCancellationRequested(); + CurrentEntry = defaultEntryItemViewModel; + await defaultEntryItemViewModel.InstallEntry(cancellationToken); + InstalledEntries++; + } + + await Task.Delay(1000, cancellationToken); + CurrentEntry = null; + } + + private async Task GetDefaultEntries(CancellationToken cancellationToken) + { + FetchingDefaultEntries = true; + + IOperationResult result = await _client.GetDefaultEntries.ExecuteAsync(100, null, cancellationToken); + List entries = result.Data?.EntriesV2?.Edges?.Select(e => e.Node).Cast().ToList() ?? []; + while (result.Data?.EntriesV2?.PageInfo is {HasNextPage: true}) + { + result = await _client.GetDefaultEntries.ExecuteAsync(100, result.Data.EntriesV2.PageInfo.EndCursor, cancellationToken); + if (result.Data?.EntriesV2?.Edges != null) + entries.AddRange(result.Data.EntriesV2.Edges.Select(e => e.Node)); + } + + DeviceProviderEntryViewModels.Clear(); + EssentialEntryViewModels.Clear(); + OtherEntryViewModels.Clear(); + foreach (IEntrySummary entry in entries) + { + if (entry.DefaultEntryInfo == null) + continue; + + DefaultEntryItemViewModel viewModel = _getDefaultEntryItemViewModel(entry); + viewModel.ShouldInstall = entry.DefaultEntryInfo.IsEssential; + + if (entry.DefaultEntryInfo.IsDeviceProvider) + DeviceProviderEntryViewModels.Add(viewModel); + else if (entry.DefaultEntryInfo.IsEssential) + EssentialEntryViewModels.Add(viewModel); + else + OtherEntryViewModels.Add(viewModel); + } + + FetchingDefaultEntries = false; + } +} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/StartupWizard/Steps/DefaultEntryItemView.axaml b/src/Artemis.UI/Screens/StartupWizard/Steps/DefaultEntryItemView.axaml new file mode 100644 index 000000000..0caf69c17 --- /dev/null +++ b/src/Artemis.UI/Screens/StartupWizard/Steps/DefaultEntryItemView.axaml @@ -0,0 +1,71 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Install + + + + + Already installed + + + + + \ No newline at end of file diff --git a/src/Artemis.UI/Screens/StartupWizard/Steps/DefaultEntryItemView.axaml.cs b/src/Artemis.UI/Screens/StartupWizard/Steps/DefaultEntryItemView.axaml.cs new file mode 100644 index 000000000..3de435785 --- /dev/null +++ b/src/Artemis.UI/Screens/StartupWizard/Steps/DefaultEntryItemView.axaml.cs @@ -0,0 +1,20 @@ +using Avalonia.Input; +using Avalonia.ReactiveUI; + +namespace Artemis.UI.Screens.StartupWizard.Steps; + +public partial class DefaultEntryItemView : ReactiveUserControl +{ + public DefaultEntryItemView() + { + InitializeComponent(); + } + + private void HandlePointerPressed(object? sender, PointerPressedEventArgs e) + { + if (ViewModel != null && !ViewModel.IsInstalled) + { + ViewModel.ShouldInstall = !ViewModel.ShouldInstall; + } + } +} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/StartupWizard/Steps/DefaultEntryItemViewModel.cs b/src/Artemis.UI/Screens/StartupWizard/Steps/DefaultEntryItemViewModel.cs new file mode 100644 index 000000000..44b59b947 --- /dev/null +++ b/src/Artemis.UI/Screens/StartupWizard/Steps/DefaultEntryItemViewModel.cs @@ -0,0 +1,137 @@ +using System; +using System.Linq; +using System.Reactive.Disposables; +using System.Threading; +using System.Threading.Tasks; +using Artemis.Core; +using Artemis.Core.Services; +using Artemis.UI.Exceptions; +using Artemis.UI.Screens.Plugins; +using Artemis.UI.Services; +using Artemis.UI.Services.Interfaces; +using Artemis.UI.Shared; +using Artemis.UI.Shared.Services; +using Artemis.UI.Shared.Utilities; +using Artemis.WebClient.Workshop; +using Artemis.WebClient.Workshop.Handlers.InstallationHandlers; +using Artemis.WebClient.Workshop.Models; +using Artemis.WebClient.Workshop.Services; +using PropertyChanged.SourceGenerator; +using ReactiveUI; +using Serilog; + +namespace Artemis.UI.Screens.StartupWizard.Steps; + +public partial class DefaultEntryItemViewModel : ActivatableViewModelBase +{ + private readonly ILogger _logger; + private readonly IWorkshopService _workshopService; + private readonly IWindowService _windowService; + private readonly IPluginManagementService _pluginManagementService; + private readonly IProfileService _profileService; + private readonly IPluginInteractionService _pluginInteractionService; + private readonly Progress _progress = new(); + + [Notify] private bool _isInstalled; + [Notify] private bool _shouldInstall; + [Notify] private float _installProgress; + + public DefaultEntryItemViewModel(ILogger logger, + IEntrySummary entry, + IWorkshopService workshopService, + IWindowService windowService, + IPluginManagementService pluginManagementService, + IProfileService profileService, + IPluginInteractionService pluginInteractionService) + { + _logger = logger; + _workshopService = workshopService; + _windowService = windowService; + _pluginManagementService = pluginManagementService; + _profileService = profileService; + _pluginInteractionService = pluginInteractionService; + Entry = entry; + + _progress.ProgressChanged += (_, f) => InstallProgress = f.ProgressPercentage; + this.WhenActivated((CompositeDisposable _) => { IsInstalled = workshopService.GetInstalledEntry(entry.Id) != null; }); + } + + public IEntrySummary Entry { get; } + + public async Task InstallEntry(CancellationToken cancellationToken) + { + if (IsInstalled || !ShouldInstall || Entry.LatestRelease == null) + return true; + + EntryInstallResult result = await _workshopService.InstallEntry(Entry, Entry.LatestRelease, _progress, cancellationToken); + + if (!result.IsSuccess) + { + await _windowService.CreateContentDialog() + .WithTitle("Failed to install entry") + .WithContent($"Failed to install entry '{Entry.Name}' ({Entry.Id}): {result.Message}") + .WithCloseButtonText("Skip and continue") + .ShowAsync(); + } + // If the entry is a plugin, enable the plugin and all features + else if (result.Entry?.EntryType == EntryType.Plugin) + await EnablePluginAndFeatures(result.Entry); + // If the entry is a profile, move it to the General profile category + else if (result.Entry?.EntryType == EntryType.Profile) + PrepareProfile(result.Entry); + + return result.IsSuccess; + } + + private async Task EnablePluginAndFeatures(InstalledEntry entry) + { + if (!entry.TryGetMetadata("PluginId", out Guid pluginId)) + throw new InvalidOperationException("Plugin entry does not contain a PluginId metadata value."); + + Plugin? plugin = _pluginManagementService.GetAllPlugins().FirstOrDefault(p => p.Guid == pluginId); + if (plugin == null) + throw new InvalidOperationException($"Plugin with id '{pluginId}' does not exist."); + + // There's quite a bit of UI involved in enabling a plugin, borrowing the PluginSettingsViewModel for this + await _pluginInteractionService.EnablePlugin(plugin, true); + + // Find features without prerequisites to enable + foreach (PluginFeatureInfo pluginFeatureInfo in plugin.Features) + { + if (pluginFeatureInfo.Instance == null || pluginFeatureInfo.Instance.IsEnabled || pluginFeatureInfo.Prerequisites.Count != 0) + continue; + + try + { + _pluginManagementService.EnablePluginFeature(pluginFeatureInfo.Instance, true); + } + catch (Exception e) + { + _logger.Warning(e, "Failed to enable plugin feature '{FeatureName}', skipping", pluginFeatureInfo.Name); + } + } + } + + private void PrepareProfile(InstalledEntry entry) + { + if (!entry.TryGetMetadata("ProfileId", out Guid profileId)) + return; + + ProfileConfiguration? profile = _profileService.ProfileCategories.SelectMany(c => c.ProfileConfigurations).FirstOrDefault(c => c.ProfileId == profileId); + if (profile == null) + return; + + ProfileCategory category = _profileService.ProfileCategories.FirstOrDefault(c => c.Name == "General") ?? _profileService.CreateProfileCategory("General", true); + if (category.ProfileConfigurations.Contains(profile)) + return; + + + // Add the profile to the category + category.AddProfileConfiguration(profile, null); + + // Suspend all but the first profile in the category + profile.IsSuspended = category.ProfileConfigurations.Count > 1; + + _profileService.SaveProfileCategory(category); + } +} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/StartupWizard/Steps/DevicesStepView.axaml b/src/Artemis.UI/Screens/StartupWizard/Steps/DevicesStepView.axaml deleted file mode 100644 index aaed17364..000000000 --- a/src/Artemis.UI/Screens/StartupWizard/Steps/DevicesStepView.axaml +++ /dev/null @@ -1,43 +0,0 @@ - - - - - - Devices are supported through the use of device providers. - - - In the list below you can enable device providers for each brand you own by checking "Enable feature". - - - - - - - - - - - - - - - - - - - - - - - Note: To avoid possible instability it's recommended to disable the device providers of brands you don't own. - - - - \ No newline at end of file diff --git a/src/Artemis.UI/Screens/StartupWizard/Steps/DevicesStepViewModel.cs b/src/Artemis.UI/Screens/StartupWizard/Steps/DevicesStepViewModel.cs deleted file mode 100644 index ed2f364d9..000000000 --- a/src/Artemis.UI/Screens/StartupWizard/Steps/DevicesStepViewModel.cs +++ /dev/null @@ -1,33 +0,0 @@ -using ReactiveUI; -using System; -using System.Collections.ObjectModel; -using System.Linq; -using Artemis.Core; -using Artemis.Core.DeviceProviders; -using Artemis.Core.Services; - -namespace Artemis.UI.Screens.StartupWizard.Steps; - -public class DevicesStepViewModel : WizardStepViewModel -{ - public DevicesStepViewModel(IPluginManagementService pluginManagementService, Func getPluginFeatureViewModel, IDeviceService deviceService) - { - // Take all compatible device providers and create a view model for them - DeviceProviders = new ObservableCollection(pluginManagementService.GetAllPlugins() - .Where(p => p.Info.IsCompatible) - .SelectMany(p => p.Features.Where(f => f.FeatureType.IsAssignableTo(typeof(DeviceProvider)))) - .OrderBy(f => f.Name) - .Select(getPluginFeatureViewModel)); - - Continue = ReactiveCommand.Create(() => - { - if (deviceService.EnabledDevices.Count == 0) - Wizard.ChangeScreen(); - else - Wizard.ChangeScreen(); - }); - GoBack = ReactiveCommand.Create(() => Wizard.ChangeScreen()); - } - - public ObservableCollection DeviceProviders { get; } -} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/StartupWizard/Steps/FinishStepView.axaml b/src/Artemis.UI/Screens/StartupWizard/Steps/FinishStepView.axaml index 700d526ae..8adb1b5d6 100644 --- a/src/Artemis.UI/Screens/StartupWizard/Steps/FinishStepView.axaml +++ b/src/Artemis.UI/Screens/StartupWizard/Steps/FinishStepView.axaml @@ -2,52 +2,51 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" - xmlns:startupWizard="clr-namespace:Artemis.UI.Screens.StartupWizard" - xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia" xmlns:steps="clr-namespace:Artemis.UI.Screens.StartupWizard.Steps" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="Artemis.UI.Screens.StartupWizard.Steps.FinishStepView" x:DataType="steps:FinishStepViewModel"> - - - All finished! + + All finished! + + + + You are now ready to start using Artemis, enjoy! 😁 + + + To learn more about Artemis and how to use it you may find these resources useful: + - - You are now ready to start using Artemis, enjoy! 😁 - - - To learn more about Artemis and how to use it you may find these resources useful: - + + + + - - - - - - - Artemis wiki - Getting started guide - GitHub - Discord - - - - https://wiki.artemis-rgb.com/ - - - https://wiki.artemis-rgb.com/en/guides/user/introduction - - - https://github.com/Artemis-RGB/Artemis - - - https://discord.gg/S3MVaC9 - - - - - + + Artemis wiki + Getting started guide + GitHub + Discord + + + + https://wiki.artemis-rgb.com/ + + + https://wiki.artemis-rgb.com/en/guides/user/introduction + + + https://github.com/Artemis-RGB/Artemis + + + https://discord.gg/S3MVaC9 + + + + + + \ No newline at end of file diff --git a/src/Artemis.UI/Screens/StartupWizard/Steps/LayoutsStepView.axaml b/src/Artemis.UI/Screens/StartupWizard/Steps/LayoutsStepView.axaml index 38a459eb6..cce2fe7d3 100644 --- a/src/Artemis.UI/Screens/StartupWizard/Steps/LayoutsStepView.axaml +++ b/src/Artemis.UI/Screens/StartupWizard/Steps/LayoutsStepView.axaml @@ -2,31 +2,29 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" - xmlns:startupWizard="clr-namespace:Artemis.UI.Screens.StartupWizard" xmlns:steps="clr-namespace:Artemis.UI.Screens.StartupWizard.Steps" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="Artemis.UI.Screens.StartupWizard.Steps.LayoutsStepView" x:DataType="steps:LayoutsStepViewModel"> - - - - - Device layouts provide Artemis with an image of your devices and exact LED positions. - While not strictly necessary, this helps to create effects that are perfectly aligned with your hardware. - - - Below you can automatically search the Artemis Workshop for device layouts of your devices. - + + + Device layouts provide Artemis with an image of your devices and exact LED positions. + While not strictly necessary, this helps to create effects that are perfectly aligned with your hardware. + + + We've searched for layouts for your connected devices, you can see the results below. If one or more are missing consider creating your own for the best experience. + + + + + + - - diff --git a/src/Artemis.UI/Screens/StartupWizard/Steps/SurfaceStepViewModel.cs b/src/Artemis.UI/Screens/StartupWizard/Steps/SurfaceStepViewModel.cs index 04989be43..ccdcdac00 100644 --- a/src/Artemis.UI/Screens/StartupWizard/Steps/SurfaceStepViewModel.cs +++ b/src/Artemis.UI/Screens/StartupWizard/Steps/SurfaceStepViewModel.cs @@ -27,9 +27,7 @@ public class SurfaceStepViewModel : WizardStepViewModel private void ExecuteSelectLayout(string layout) { - // TODO: Implement the layout - _deviceService.AutoArrangeDevices(); - + _deviceService.AutoArrangeDevices(layout == "left"); Wizard.ChangeScreen(); } } \ No newline at end of file diff --git a/src/Artemis.UI/Screens/StartupWizard/Steps/WelcomeStepView.axaml b/src/Artemis.UI/Screens/StartupWizard/Steps/WelcomeStepView.axaml index 0bd99585f..65b265323 100644 --- a/src/Artemis.UI/Screens/StartupWizard/Steps/WelcomeStepView.axaml +++ b/src/Artemis.UI/Screens/StartupWizard/Steps/WelcomeStepView.axaml @@ -6,19 +6,17 @@ mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="Artemis.UI.Screens.StartupWizard.Steps.WelcomeStepView" x:DataType="steps:WelcomeStepViewModel"> - - - Welcome to the Artemis startup wizard! + + Welcome to the Artemis startup wizard! - + In this wizard we'll walk you through the initial configuration of Artemis. - + Before you can start you need to tell Artemis which devices you want to use and where they are placed on your desk. - + PS: You can also skip the wizard and set things up yourself. - \ No newline at end of file diff --git a/src/Artemis.UI/Screens/StartupWizard/Steps/WelcomeStepViewModel.cs b/src/Artemis.UI/Screens/StartupWizard/Steps/WelcomeStepViewModel.cs index 6a4946fcf..529c6800f 100644 --- a/src/Artemis.UI/Screens/StartupWizard/Steps/WelcomeStepViewModel.cs +++ b/src/Artemis.UI/Screens/StartupWizard/Steps/WelcomeStepViewModel.cs @@ -6,7 +6,7 @@ public class WelcomeStepViewModel : WizardStepViewModel { public WelcomeStepViewModel() { - Continue = ReactiveCommand.Create(() => Wizard.ChangeScreen()); + Continue = ReactiveCommand.Create(() => Wizard.ChangeScreen()); ShowGoBack = false; } } \ No newline at end of file diff --git a/src/Artemis.UI/Screens/StartupWizard/Steps/WorkshopUnreachableStepView.axaml b/src/Artemis.UI/Screens/StartupWizard/Steps/WorkshopUnreachableStepView.axaml new file mode 100644 index 000000000..64fb3ca08 --- /dev/null +++ b/src/Artemis.UI/Screens/StartupWizard/Steps/WorkshopUnreachableStepView.axaml @@ -0,0 +1,30 @@ + + + + + + + + + Could not reach the workshop + This unfortunately means the setup wizard cannot continue. + + + Please ensure you are connected to the internet. + If this keeps occuring, hit us up on Discord + + + + + \ No newline at end of file diff --git a/src/Artemis.UI/Screens/StartupWizard/Steps/DevicesStepView.axaml.cs b/src/Artemis.UI/Screens/StartupWizard/Steps/WorkshopUnreachableStepView.axaml.cs similarity index 54% rename from src/Artemis.UI/Screens/StartupWizard/Steps/DevicesStepView.axaml.cs rename to src/Artemis.UI/Screens/StartupWizard/Steps/WorkshopUnreachableStepView.axaml.cs index a2435a438..c4162a875 100644 --- a/src/Artemis.UI/Screens/StartupWizard/Steps/DevicesStepView.axaml.cs +++ b/src/Artemis.UI/Screens/StartupWizard/Steps/WorkshopUnreachableStepView.axaml.cs @@ -4,11 +4,10 @@ using Avalonia.ReactiveUI; namespace Artemis.UI.Screens.StartupWizard.Steps; -public partial class DevicesStepView : ReactiveUserControl +public partial class WorkshopUnreachableStepView : ReactiveUserControl { - public DevicesStepView() + public WorkshopUnreachableStepView() { InitializeComponent(); } - } \ No newline at end of file diff --git a/src/Artemis.UI/Screens/StartupWizard/Steps/WorkshopUnreachableStepViewModel.cs b/src/Artemis.UI/Screens/StartupWizard/Steps/WorkshopUnreachableStepViewModel.cs new file mode 100644 index 000000000..c91fa8ef6 --- /dev/null +++ b/src/Artemis.UI/Screens/StartupWizard/Steps/WorkshopUnreachableStepViewModel.cs @@ -0,0 +1,29 @@ +using Artemis.Core; +using Artemis.Core.Services; + +namespace Artemis.UI.Screens.StartupWizard.Steps; + +public class WorkshopUnreachableStepViewModel : WizardStepViewModel +{ + private readonly ISettingsService _settingsService; + + public WorkshopUnreachableStepViewModel(ISettingsService settingsService) + { + _settingsService = settingsService; + HideAllButtons = true; + } + + public void Retry() + { + Wizard.ChangeScreen(); + } + + public void Skip() + { + PluginSetting setting = _settingsService.GetSetting("UI.SetupWizardCompleted", false); + setting.Value = false; + setting.Save(); + + Wizard.Close(false); + } +} \ No newline at end of file diff --git a/src/Artemis.UI/Screens/StartupWizard/WizardPluginFeatureView.axaml b/src/Artemis.UI/Screens/StartupWizard/WizardPluginFeatureView.axaml index c2dde7ecb..e38460226 100644 --- a/src/Artemis.UI/Screens/StartupWizard/WizardPluginFeatureView.axaml +++ b/src/Artemis.UI/Screens/StartupWizard/WizardPluginFeatureView.axaml @@ -4,7 +4,6 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:shared="clr-namespace:Artemis.UI.Shared;assembly=Artemis.UI.Shared" xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia" - xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia" xmlns:startupWizard="clr-namespace:Artemis.UI.Screens.StartupWizard" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="Artemis.UI.Screens.StartupWizard.WizardPluginFeatureView" diff --git a/src/Artemis.UI/Screens/StartupWizard/WizardStepViewModel.cs b/src/Artemis.UI/Screens/StartupWizard/WizardStepViewModel.cs index 83ebe2172..76ba4c9f9 100644 --- a/src/Artemis.UI/Screens/StartupWizard/WizardStepViewModel.cs +++ b/src/Artemis.UI/Screens/StartupWizard/WizardStepViewModel.cs @@ -15,6 +15,7 @@ public abstract partial class WizardStepViewModel : ValidatableViewModelBase [Notify] private bool _showFinish; [Notify] private bool _showGoBack = true; [Notify] private bool _showHeader = true; + [Notify] private bool _hideAllButtons; public StartupWizardViewModel Wizard { get; set; } = null!; } \ No newline at end of file diff --git a/src/Artemis.UI/Screens/SurfaceEditor/SurfaceEditorViewModel.cs b/src/Artemis.UI/Screens/SurfaceEditor/SurfaceEditorViewModel.cs index 248ca19de..f596e0f23 100644 --- a/src/Artemis.UI/Screens/SurfaceEditor/SurfaceEditorViewModel.cs +++ b/src/Artemis.UI/Screens/SurfaceEditor/SurfaceEditorViewModel.cs @@ -13,6 +13,7 @@ using Artemis.UI.Shared; using Artemis.UI.Shared.Routing; using Artemis.UI.Shared.Services; using Avalonia; +using FluentAvalonia.UI.Controls; using PropertyChanged.SourceGenerator; using ReactiveUI; using SkiaSharp; @@ -180,11 +181,18 @@ public partial class SurfaceEditorViewModel : RoutableScreen, IMainScreenViewMod private async Task ExecuteAutoArrange() { - bool confirmed = await _windowService.ShowConfirmContentDialog("Auto-arrange layout", "Are you sure you want to auto-arrange your layout? Your current settings will be overwritten."); - if (!confirmed) - return; - - _deviceService.AutoArrangeDevices(); + ContentDialogResult contentDialogResult = await _windowService.CreateContentDialog() + .WithTitle("Auto-arrange layout") + .WithContent("Which preset would you like to apply? Your current settings will be overwritten.") + .HavingPrimaryButton(b => b.WithText("Left-handed preset")) + .HavingSecondaryButton(b => b.WithText("Right-handed preset")) + .WithCloseButtonText("Cancel") + .ShowAsync(); + + if (contentDialogResult == ContentDialogResult.Primary) + _deviceService.AutoArrangeDevices(true); + else if (contentDialogResult == ContentDialogResult.Secondary) + _deviceService.AutoArrangeDevices(false); } private void RenderServiceOnFrameRendering(object? sender, FrameRenderingEventArgs e) diff --git a/src/Artemis.UI/Screens/VisualScripting/NodeScriptView.axaml.cs b/src/Artemis.UI/Screens/VisualScripting/NodeScriptView.axaml.cs index eed8b7c31..0ebcc26f3 100644 --- a/src/Artemis.UI/Screens/VisualScripting/NodeScriptView.axaml.cs +++ b/src/Artemis.UI/Screens/VisualScripting/NodeScriptView.axaml.cs @@ -33,16 +33,17 @@ public partial class NodeScriptView : ReactiveUserControl this.WhenActivated(d => { - ViewModel!.AutoFitRequested += ViewModelOnAutoFitRequested; - ViewModel.PickerPositionSubject.Subscribe(ShowPickerAt).DisposeWith(d); - if (ViewModel.IsPreview) + NodeScriptViewModel vm = ViewModel!; + vm.AutoFitRequested += ViewModelOnAutoFitRequested; + vm.PickerPositionSubject.Subscribe(ShowPickerAt).DisposeWith(d); + if (vm.IsPreview) { BoundsProperty.Changed.Subscribe(BoundsPropertyChanged).DisposeWith(d); - ViewModel.NodeViewModels.ToObservableChangeSet().Subscribe(_ => AutoFitIfPreview()).DisposeWith(d); + vm.NodeViewModels.ToObservableChangeSet().Subscribe(_ => AutoFitIfPreview()).DisposeWith(d); } Dispatcher.UIThread.InvokeAsync(() => AutoFit(true), DispatcherPriority.ContextIdle); - Disposable.Create(() => ViewModel.AutoFitRequested -= ViewModelOnAutoFitRequested).DisposeWith(d); + Disposable.Create(() => vm.AutoFitRequested -= ViewModelOnAutoFitRequested).DisposeWith(d); }); } diff --git a/src/Artemis.UI/Screens/VisualScripting/NodeScriptWindowView.axaml b/src/Artemis.UI/Screens/VisualScripting/NodeScriptWindowView.axaml index 82a8787ae..639d663ec 100644 --- a/src/Artemis.UI/Screens/VisualScripting/NodeScriptWindowView.axaml +++ b/src/Artemis.UI/Screens/VisualScripting/NodeScriptWindowView.axaml @@ -3,11 +3,8 @@ xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:visualScripting="clr-namespace:Artemis.UI.Screens.VisualScripting" - xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia" xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia" - xmlns:core="clr-namespace:Artemis.Core;assembly=Artemis.Core" xmlns:windowing="clr-namespace:FluentAvalonia.UI.Windowing;assembly=FluentAvalonia" - xmlns:system="clr-namespace:System;assembly=System.Runtime" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="Artemis.UI.Screens.VisualScripting.NodeScriptWindowView" x:DataType="visualScripting:NodeScriptWindowViewModel" diff --git a/src/Artemis.UI/Screens/VisualScripting/NodeView.axaml b/src/Artemis.UI/Screens/VisualScripting/NodeView.axaml index 00b1402bb..030fc8a33 100644 --- a/src/Artemis.UI/Screens/VisualScripting/NodeView.axaml +++ b/src/Artemis.UI/Screens/VisualScripting/NodeView.axaml @@ -4,7 +4,6 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia" xmlns:visualScripting="clr-namespace:Artemis.UI.Screens.VisualScripting" - xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia" mc:Ignorable="d" d:DesignWidth="250" d:DesignHeight="150" x:Class="Artemis.UI.Screens.VisualScripting.NodeView" x:DataType="visualScripting:NodeViewModel"> diff --git a/src/Artemis.UI/Screens/Workshop/Categories/CategoriesViewModel.cs b/src/Artemis.UI/Screens/Workshop/Categories/CategoriesViewModel.cs index 74d0f629e..40f68116f 100644 --- a/src/Artemis.UI/Screens/Workshop/Categories/CategoriesViewModel.cs +++ b/src/Artemis.UI/Screens/Workshop/Categories/CategoriesViewModel.cs @@ -18,10 +18,10 @@ public class CategoriesViewModel : ActivatableViewModelBase { private ObservableAsPropertyHelper?>? _categoryFilters; - public CategoriesViewModel(IWorkshopClient client) + public CategoriesViewModel(EntryType entryType, IWorkshopClient client) { client.GetCategories - .Watch(ExecutionStrategy.CacheFirst) + .Watch(entryType, ExecutionStrategy.CacheFirst) .SelectOperationResult(c => c.Categories) .ToObservableChangeSet(c => c.Id) .Transform(c => new CategoryViewModel(c)) diff --git a/src/Artemis.UI/Screens/Workshop/CurrentUser/CurrentUserView.axaml b/src/Artemis.UI/Screens/Workshop/CurrentUser/CurrentUserView.axaml index eea731a15..2f4742cb8 100644 --- a/src/Artemis.UI/Screens/Workshop/CurrentUser/CurrentUserView.axaml +++ b/src/Artemis.UI/Screens/Workshop/CurrentUser/CurrentUserView.axaml @@ -4,7 +4,6 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:currentUser="clr-namespace:Artemis.UI.Screens.Workshop.CurrentUser" xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia" - xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia" xmlns:asyncImageLoader="clr-namespace:AsyncImageLoader;assembly=AsyncImageLoader.Avalonia" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="Artemis.UI.Screens.Workshop.CurrentUser.CurrentUserView" diff --git a/src/Artemis.UI/Screens/Workshop/Entries/Details/EntrySpecificationsView.axaml b/src/Artemis.UI/Screens/Workshop/Entries/Details/EntrySpecificationsView.axaml index dda4ba091..db1bbbcb7 100644 --- a/src/Artemis.UI/Screens/Workshop/Entries/Details/EntrySpecificationsView.axaml +++ b/src/Artemis.UI/Screens/Workshop/Entries/Details/EntrySpecificationsView.axaml @@ -51,11 +51,11 @@ ToolTip.Tip="Click to browse"> - Icon required + Shrink @@ -63,8 +63,12 @@ - - Download by default (admin only) + + + Download by default (admin only) + Essential + Device provider + @@ -93,10 +97,10 @@ - - - - + + diff --git a/src/Artemis.UI/Screens/Workshop/Entries/Details/EntrySpecificationsViewModel.cs b/src/Artemis.UI/Screens/Workshop/Entries/Details/EntrySpecificationsViewModel.cs index bea16889d..aee4bb5b9 100644 --- a/src/Artemis.UI/Screens/Workshop/Entries/Details/EntrySpecificationsViewModel.cs +++ b/src/Artemis.UI/Screens/Workshop/Entries/Details/EntrySpecificationsViewModel.cs @@ -36,15 +36,20 @@ public partial class EntrySpecificationsViewModel : ValidatableViewModelBase [Notify] private string _summary = string.Empty; [Notify] private string _description = string.Empty; [Notify] private bool _isDefault; + [Notify] private bool _isEssential; + [Notify] private bool _isDeviceProvider; + [Notify] private bool _fit; [Notify] private Bitmap? _iconBitmap; [Notify(Setter.Private)] private bool _iconChanged; + private string? _lastIconPath; + public EntrySpecificationsViewModel(IWorkshopClient workshopClient, IWindowService windowService, IAuthenticationService authenticationService) { _workshopClient = workshopClient; _windowService = windowService; SelectIcon = ReactiveCommand.CreateFromTask(ExecuteSelectIcon); - + Categories.ToObservableChangeSet() .AutoRefresh(c => c.IsSelected) .Filter(c => c.IsSelected) @@ -66,23 +71,25 @@ public partial class EntrySpecificationsViewModel : ValidatableViewModelBase _categoriesValid = categoriesRule.ValidationChanged.Select(c => c.IsValid).ToProperty(this, vm => vm.CategoriesValid); _descriptionValid = descriptionRule.ValidationChanged.Select(c => c.IsValid).ToProperty(this, vm => vm.DescriptionValid); - this.WhenActivatedAsync(async _ => await PopulateCategories()); IsAdministrator = authenticationService.GetRoles().Contains("Administrator"); + this.WhenActivatedAsync(async _ => await PopulateCategories()); + this.WhenAnyValue(vm => vm.Fit).Subscribe(_ => UpdateIcon()); } - + public ReactiveCommand SelectIcon { get; } public ObservableCollection Categories { get; } = new(); public ObservableCollection Tags { get; } = new(); public ReadOnlyObservableCollection SelectedCategories { get; } - - public bool CategoriesValid => _categoriesValid.Value ; + + public bool CategoriesValid => _categoriesValid.Value; public bool IconValid => _iconValid.Value; public bool DescriptionValid => _descriptionValid.Value; public bool IsAdministrator { get; } - + public List PreselectedCategories { get; set; } = new(); - + public EntryType EntryType { get; set; } + private async Task ExecuteSelectIcon() { string[]? result = await _windowService.CreateOpenFileDialog() @@ -92,14 +99,23 @@ public partial class EntrySpecificationsViewModel : ValidatableViewModelBase if (result == null) return; + _lastIconPath = result[0]; + UpdateIcon(); + } + + private void UpdateIcon() + { + if (_lastIconPath == null) + return; + IconBitmap?.Dispose(); - IconBitmap = BitmapExtensions.LoadAndResize(result[0], 128); + IconBitmap = BitmapExtensions.LoadAndResize(_lastIconPath, 128, Fit); IconChanged = true; } private async Task PopulateCategories() { - IOperationResult categories = await _workshopClient.GetCategories.ExecuteAsync(); + IOperationResult categories = await _workshopClient.GetCategories.ExecuteAsync(EntryType); Categories.Clear(); if (categories.Data != null) Categories.AddRange(categories.Data.Categories.Select(c => new CategoryViewModel(c) {IsSelected = PreselectedCategories.Contains(c.Id)})); diff --git a/src/Artemis.UI/Screens/Workshop/Entries/List/EntryListView.axaml b/src/Artemis.UI/Screens/Workshop/Entries/List/EntryListView.axaml index 8dbdd04b5..292ca8b1d 100644 --- a/src/Artemis.UI/Screens/Workshop/Entries/List/EntryListView.axaml +++ b/src/Artemis.UI/Screens/Workshop/Entries/List/EntryListView.axaml @@ -21,6 +21,7 @@ + + + + + + Include default entries + + + + @@ -59,7 +70,7 @@ Modify or clear your filters to view other entries - + diff --git a/src/Artemis.UI/Screens/Workshop/Entries/List/EntryListViewModel.cs b/src/Artemis.UI/Screens/Workshop/Entries/List/EntryListViewModel.cs index a6b5309c7..fd29fcd46 100644 --- a/src/Artemis.UI/Screens/Workshop/Entries/List/EntryListViewModel.cs +++ b/src/Artemis.UI/Screens/Workshop/Entries/List/EntryListViewModel.cs @@ -25,25 +25,28 @@ public partial class EntryListViewModel : RoutableScreen private readonly SourceList _entries = new(); private readonly INotificationService _notificationService; private readonly IWorkshopClient _workshopClient; - private IGetEntriesv2_EntriesV2_PageInfo? _currentPageInfo; + private IGetEntries_EntriesV2_PageInfo? _currentPageInfo; [Notify] private bool _initializing = true; [Notify] private bool _fetchingMore; [Notify] private int _entriesPerFetch; + [Notify] private bool _includeDefaultEntries; [Notify] private Vector _scrollOffset; - protected EntryListViewModel(IWorkshopClient workshopClient, - CategoriesViewModel categoriesViewModel, + protected EntryListViewModel(EntryType entryType, + IWorkshopClient workshopClient, EntryListInputViewModel entryListInputViewModel, INotificationService notificationService, + Func getCategoriesViewModel, Func getEntryListViewModel) { _workshopClient = workshopClient; _notificationService = notificationService; - CategoriesViewModel = categoriesViewModel; + CategoriesViewModel = getCategoriesViewModel(entryType); InputViewModel = entryListInputViewModel; - + EntryType = entryType; + _entries.Connect() .Transform(getEntryListViewModel) .Bind(out ReadOnlyObservableCollection entries) @@ -51,13 +54,14 @@ public partial class EntryListViewModel : RoutableScreen Entries = entries; // Respond to filter query input changes + this.WhenAnyValue(vm => vm.IncludeDefaultEntries).Skip(1).Throttle(TimeSpan.FromMilliseconds(200)).Subscribe(_ => Reset()); this.WhenActivated(d => { InputViewModel.WhenAnyValue(vm => vm.Search).Skip(1).Throttle(TimeSpan.FromMilliseconds(200)).Subscribe(_ => Reset()).DisposeWith(d); InputViewModel.WhenAnyValue(vm => vm.SortBy).Skip(1).Throttle(TimeSpan.FromMilliseconds(200)).Subscribe(_ => Reset()).DisposeWith(d); CategoriesViewModel.WhenAnyValue(vm => vm.CategoryFilters).Skip(1).Subscribe(_ => Reset()).DisposeWith(d); }); - + // Load entries when the view model is first activated this.WhenActivatedAsync(async _ => { @@ -73,10 +77,10 @@ public partial class EntryListViewModel : RoutableScreen public CategoriesViewModel CategoriesViewModel { get; } public EntryListInputViewModel InputViewModel { get; } public bool ShowCategoryFilter { get; set; } = true; - public EntryType? EntryType { get; set; } + public EntryType EntryType { get; } public ReadOnlyObservableCollection Entries { get; } - + public async Task FetchMore(CancellationToken cancellationToken) { if (FetchingMore || _currentPageInfo != null && !_currentPageInfo.HasNextPage) @@ -91,7 +95,7 @@ public partial class EntryListViewModel : RoutableScreen try { - IOperationResult entries = await _workshopClient.GetEntriesv2.ExecuteAsync(search, filter, sort, entriesPerFetch, _currentPageInfo?.EndCursor, cancellationToken); + IOperationResult entries = await _workshopClient.GetEntries.ExecuteAsync(search, IncludeDefaultEntries, filter, sort, entriesPerFetch, _currentPageInfo?.EndCursor, cancellationToken); entries.EnsureNoErrors(); _currentPageInfo = entries.Data?.EntriesV2?.PageInfo; diff --git a/src/Artemis.UI/Screens/Workshop/EntryReleases/EntryReleaseInfoViewModel.cs b/src/Artemis.UI/Screens/Workshop/EntryReleases/EntryReleaseInfoViewModel.cs index 3d5cba990..d077e8fae 100644 --- a/src/Artemis.UI/Screens/Workshop/EntryReleases/EntryReleaseInfoViewModel.cs +++ b/src/Artemis.UI/Screens/Workshop/EntryReleases/EntryReleaseInfoViewModel.cs @@ -6,9 +6,8 @@ using System.Threading; using System.Threading.Tasks; using Artemis.Core; using Artemis.Core.Services; -using Artemis.UI.DryIoc.Factories; -using Artemis.UI.Screens.Plugins; using Artemis.UI.Screens.Workshop.EntryReleases.Dialogs; +using Artemis.UI.Services.Interfaces; using Artemis.UI.Shared; using Artemis.UI.Shared.Routing; using Artemis.UI.Shared.Services; @@ -30,7 +29,7 @@ public partial class EntryReleaseInfoViewModel : ActivatableViewModelBase private readonly IWindowService _windowService; private readonly IWorkshopService _workshopService; private readonly IPluginManagementService _pluginManagementService; - private readonly ISettingsVmFactory _settingsVmFactory; + private readonly IPluginInteractionService _pluginInteractionService; private readonly Progress _progress = new(); [Notify] private IReleaseDetails? _release; @@ -46,14 +45,14 @@ public partial class EntryReleaseInfoViewModel : ActivatableViewModelBase IWindowService windowService, IWorkshopService workshopService, IPluginManagementService pluginManagementService, - ISettingsVmFactory settingsVmFactory) + IPluginInteractionService pluginInteractionService) { _router = router; _notificationService = notificationService; _windowService = windowService; _workshopService = workshopService; _pluginManagementService = pluginManagementService; - _settingsVmFactory = settingsVmFactory; + _pluginInteractionService = pluginInteractionService; _progress.ProgressChanged += (_, f) => InstallProgress = f.ProgressPercentage; this.WhenActivated(d => @@ -124,7 +123,10 @@ public partial class EntryReleaseInfoViewModel : ActivatableViewModelBase _workshopService.SetAutoUpdate(result.Entry, !disableAutoUpdates); _notificationService.CreateNotification().WithTitle("Installation succeeded").WithSeverity(NotificationSeverity.Success).Show(); InstallationInProgress = false; - await Manage(); + + // Auto-enable plugins as the installation handler won't deal with the required UI interactions + if (result.Installed is Plugin installedPlugin) + await AutoEnablePlugin(installedPlugin); } else if (!_cts.IsCancellationRequested) { @@ -141,12 +143,6 @@ public partial class EntryReleaseInfoViewModel : ActivatableViewModelBase } } - public async Task Manage() - { - if (Release?.Entry.EntryType != EntryType.Profile) - await _router.Navigate("../../manage", new RouterNavigationOptions {AdditionalArguments = true}); - } - public async Task Reinstall() { if (await _windowService.ShowConfirmContentDialog("Reinstall entry", "Are you sure you want to reinstall this entry?")) @@ -193,7 +189,28 @@ public partial class EntryReleaseInfoViewModel : ActivatableViewModelBase if (plugin == null) return; - PluginViewModel pluginViewModel = _settingsVmFactory.PluginViewModel(plugin, ReactiveCommand.Create(() => { })); - await pluginViewModel.ExecuteRemovePrerequisites(true); + await _pluginInteractionService.RemovePluginPrerequisites(plugin, true); + } + + private async Task AutoEnablePlugin(Plugin plugin) + { + // There's quite a bit of UI involved in enabling a plugin, borrowing the PluginSettingsViewModel for this + await _pluginInteractionService.EnablePlugin(plugin, true); + + // Find features without prerequisites to enable + foreach (PluginFeatureInfo pluginFeatureInfo in plugin.Features) + { + if (pluginFeatureInfo.Instance == null || pluginFeatureInfo.Instance.IsEnabled || pluginFeatureInfo.Prerequisites.Count != 0) + continue; + + try + { + _pluginManagementService.EnablePluginFeature(pluginFeatureInfo.Instance, true); + } + catch (Exception e) + { + _notificationService.CreateNotification().WithTitle("Failed to enable plugin feature").WithMessage(e.Message).WithSeverity(NotificationSeverity.Error).Show(); + } + } } } \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Workshop/Home/WorkshopHomeViewModel.cs b/src/Artemis.UI/Screens/Workshop/Home/WorkshopHomeViewModel.cs index 7fd2bb675..4dee0a822 100644 --- a/src/Artemis.UI/Screens/Workshop/Home/WorkshopHomeViewModel.cs +++ b/src/Artemis.UI/Screens/Workshop/Home/WorkshopHomeViewModel.cs @@ -41,7 +41,7 @@ public partial class WorkshopHomeViewModel : RoutableScreen this.WhenActivatedAsync(async d => { - WorkshopReachable = await workshopService.ValidateWorkshopStatus(d.AsCancellationToken()); + WorkshopReachable = await workshopService.ValidateWorkshopStatus(true, d.AsCancellationToken()); IOperationResult popularResult = await client.GetPopularEntries.ExecuteAsync(); popular.Edit(p => @@ -51,7 +51,7 @@ public partial class WorkshopHomeViewModel : RoutableScreen p.AddRange(popularResult.Data.PopularEntries.Take(8)); }); - IOperationResult latestResult = await client.GetEntriesv2.ExecuteAsync(null, null, [new EntrySortInput {CreatedAt = SortEnumType.Desc}], 8, null); + IOperationResult latestResult = await client.GetEntries.ExecuteAsync(null, null, null, [new EntrySortInput {CreatedAt = SortEnumType.Desc}], 8, null); latest.Edit(l => { l.Clear(); diff --git a/src/Artemis.UI/Screens/Workshop/Layout/LayoutListDefaultViewModel.cs b/src/Artemis.UI/Screens/Workshop/Layout/LayoutListDefaultViewModel.cs index b9521529c..c61eabef9 100644 --- a/src/Artemis.UI/Screens/Workshop/Layout/LayoutListDefaultViewModel.cs +++ b/src/Artemis.UI/Screens/Workshop/Layout/LayoutListDefaultViewModel.cs @@ -1,3 +1,4 @@ +using System; using Artemis.UI.Screens.Workshop.Entries.List; using Artemis.UI.Screens.Workshop.LayoutFinder; using Artemis.UI.Shared.Routing; @@ -7,11 +8,10 @@ namespace Artemis.UI.Screens.Workshop.Layout; public class LayoutListDefaultViewModel : RoutableScreen { - public LayoutListDefaultViewModel(LayoutFinderViewModel layoutFinderViewModel, EntryListViewModel entryListViewModel) + public LayoutListDefaultViewModel(LayoutFinderViewModel layoutFinderViewModel, Func getEntryListViewModel) { LayoutFinderViewModel = layoutFinderViewModel; - EntryListViewModel = entryListViewModel; - EntryListViewModel.EntryType = EntryType.Layout; + EntryListViewModel = getEntryListViewModel(EntryType.Layout); EntryListViewModel.ShowCategoryFilter = false; } diff --git a/src/Artemis.UI/Screens/Workshop/LayoutFinder/LayoutFinderDeviceView.axaml b/src/Artemis.UI/Screens/Workshop/LayoutFinder/LayoutFinderDeviceView.axaml index c24c410ec..6db6ef51e 100644 --- a/src/Artemis.UI/Screens/Workshop/LayoutFinder/LayoutFinderDeviceView.axaml +++ b/src/Artemis.UI/Screens/Workshop/LayoutFinder/LayoutFinderDeviceView.axaml @@ -44,7 +44,13 @@ Margin="0 0 0 0" VerticalAlignment="Center"/> - + c.Id).ToList(); + specificationsViewModel.EntryType = Entry.EntryType; specificationsViewModel.Tags.Clear(); foreach (string tag in Entry.Tags.Select(c => c.Name)) @@ -170,14 +173,29 @@ public partial class SubmissionDetailsViewModel : RoutableScreen HasChanges = EntrySpecificationsViewModel.Name != Entry.Name || EntrySpecificationsViewModel.Description != Entry.Description || EntrySpecificationsViewModel.Summary != Entry.Summary || - EntrySpecificationsViewModel.IsDefault != Entry.IsDefault || EntrySpecificationsViewModel.IconChanged || + HasAdminChanges() || !tags.SequenceEqual(Entry.Tags.Select(t => t.Name).OrderBy(t => t)) || !categories.SequenceEqual(Entry.Categories.Select(c => c.Id).OrderBy(c => c)) || Images.Any(i => i.HasChanges) || _removedImages.Any(); } + private bool HasAdminChanges() + { + if (EntrySpecificationsViewModel == null || Entry == null) + return false; + + bool isDefault = Entry.DefaultEntryInfo != null; + bool isEssential = Entry.DefaultEntryInfo?.IsEssential ?? false; + bool isDeviceProvider = Entry.DefaultEntryInfo?.IsDeviceProvider ?? false; + + return EntrySpecificationsViewModel.IsDefault != isDefault || + (EntrySpecificationsViewModel.IsDefault && ( + EntrySpecificationsViewModel.IsEssential != isEssential || + EntrySpecificationsViewModel.IsDeviceProvider != isDeviceProvider)); + } + private async Task ExecuteDiscardChanges() { await ApplyDetailsFromEntry(CancellationToken.None); @@ -194,9 +212,15 @@ public partial class SubmissionDetailsViewModel : RoutableScreen Name = EntrySpecificationsViewModel.Name, Summary = EntrySpecificationsViewModel.Summary, Description = EntrySpecificationsViewModel.Description, - IsDefault = EntrySpecificationsViewModel.IsDefault, Categories = EntrySpecificationsViewModel.SelectedCategories, - Tags = EntrySpecificationsViewModel.Tags + Tags = EntrySpecificationsViewModel.Tags, + DefaultEntryInfo = EntrySpecificationsViewModel.IsDefault + ? new DefaultEntryInfoInput + { + IsEssential = EntrySpecificationsViewModel.IsEssential, + IsDeviceProvider = EntrySpecificationsViewModel.IsDeviceProvider + } + : null }; IOperationResult result = await _client.UpdateEntry.ExecuteAsync(input, cancellationToken); diff --git a/src/Artemis.UI/Screens/Workshop/Library/Tabs/InstalledTabItemView.axaml b/src/Artemis.UI/Screens/Workshop/Library/Tabs/InstalledTabItemView.axaml index 71ab5983c..39eb9d5a8 100644 --- a/src/Artemis.UI/Screens/Workshop/Library/Tabs/InstalledTabItemView.axaml +++ b/src/Artemis.UI/Screens/Workshop/Library/Tabs/InstalledTabItemView.axaml @@ -92,7 +92,7 @@ - + diff --git a/src/Artemis.UI/Screens/Workshop/Library/Tabs/InstalledTabItemViewModel.cs b/src/Artemis.UI/Screens/Workshop/Library/Tabs/InstalledTabItemViewModel.cs index 6307a8557..665129c0c 100644 --- a/src/Artemis.UI/Screens/Workshop/Library/Tabs/InstalledTabItemViewModel.cs +++ b/src/Artemis.UI/Screens/Workshop/Library/Tabs/InstalledTabItemViewModel.cs @@ -35,6 +35,7 @@ public partial class InstalledTabItemViewModel : ActivatableViewModelBase [Notify] private bool _updateAvailable; [Notify] private bool _autoUpdate; + [Notify] private bool _displayManagement = true; public InstalledTabItemViewModel(InstalledEntry entry, IWorkshopClient client, @@ -87,7 +88,7 @@ public partial class InstalledTabItemViewModel : ActivatableViewModelBase public async Task ViewLocal() { if (Entry.EntryType == EntryType.Profile && Entry.TryGetMetadata("ProfileId", out Guid profileId)) - await _router.Navigate($"profile-editor/{profileId}"); + await _router.Navigate($"profile/{profileId}/editor"); else if (Entry.EntryType == EntryType.Plugin) await _router.Navigate($"workshop/entries/plugins/details/{Entry.Id}/manage"); else if (Entry.EntryType == EntryType.Layout) diff --git a/src/Artemis.UI/Screens/Workshop/Library/Tabs/SubmissionsTabView.axaml b/src/Artemis.UI/Screens/Workshop/Library/Tabs/SubmissionsTabView.axaml index 63f5272e0..2fd59cde3 100644 --- a/src/Artemis.UI/Screens/Workshop/Library/Tabs/SubmissionsTabView.axaml +++ b/src/Artemis.UI/Screens/Workshop/Library/Tabs/SubmissionsTabView.axaml @@ -32,6 +32,7 @@ + diff --git a/src/Artemis.UI/Screens/Workshop/Library/Tabs/SubmissionsTabViewModel.cs b/src/Artemis.UI/Screens/Workshop/Library/Tabs/SubmissionsTabViewModel.cs index caefeab1e..948c9f20f 100644 --- a/src/Artemis.UI/Screens/Workshop/Library/Tabs/SubmissionsTabViewModel.cs +++ b/src/Artemis.UI/Screens/Workshop/Library/Tabs/SubmissionsTabViewModel.cs @@ -63,7 +63,7 @@ public partial class SubmissionsTabViewModel : RoutableScreen this.WhenActivatedAsync(async d => { - WorkshopReachable = await workshopService.ValidateWorkshopStatus(d.AsCancellationToken()); + WorkshopReachable = await workshopService.ValidateWorkshopStatus(true, d.AsCancellationToken()); if (WorkshopReachable) await GetEntries(d.AsCancellationToken()); }); diff --git a/src/Artemis.UI/Screens/Workshop/Plugins/PluginListViewModel.cs b/src/Artemis.UI/Screens/Workshop/Plugins/PluginListViewModel.cs index 7401c2659..422a67223 100644 --- a/src/Artemis.UI/Screens/Workshop/Plugins/PluginListViewModel.cs +++ b/src/Artemis.UI/Screens/Workshop/Plugins/PluginListViewModel.cs @@ -1,3 +1,4 @@ +using System; using Artemis.UI.Screens.Workshop.Entries.List; using Artemis.UI.Shared.Routing; using Artemis.WebClient.Workshop; @@ -9,9 +10,8 @@ public class PluginListViewModel : RoutableHostScreen private readonly EntryListViewModel _entryListViewModel; public override RoutableScreen DefaultScreen => _entryListViewModel; - public PluginListViewModel(EntryListViewModel entryListViewModel) + public PluginListViewModel(Func getEntryListViewModel) { - _entryListViewModel = entryListViewModel; - _entryListViewModel.EntryType = EntryType.Plugin; + _entryListViewModel = getEntryListViewModel(EntryType.Plugin); } } \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Workshop/Profile/ProfileListViewModel.cs b/src/Artemis.UI/Screens/Workshop/Profile/ProfileListViewModel.cs index 75faa19a1..d8b098761 100644 --- a/src/Artemis.UI/Screens/Workshop/Profile/ProfileListViewModel.cs +++ b/src/Artemis.UI/Screens/Workshop/Profile/ProfileListViewModel.cs @@ -1,3 +1,4 @@ +using System; using Artemis.UI.Screens.Workshop.Entries.List; using Artemis.UI.Shared.Routing; using Artemis.WebClient.Workshop; @@ -9,9 +10,8 @@ public class ProfileListViewModel : RoutableHostScreen private readonly EntryListViewModel _entryListViewModel; public override RoutableScreen DefaultScreen => _entryListViewModel; - public ProfileListViewModel(EntryListViewModel entryListViewModel) + public ProfileListViewModel(Func getEntryListViewModel) { - _entryListViewModel = entryListViewModel; - _entryListViewModel.EntryType = EntryType.Profile; + _entryListViewModel = getEntryListViewModel(EntryType.Profile); } } \ No newline at end of file diff --git a/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Models/SubmissionWizardState.cs b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Models/SubmissionWizardState.cs index 5e6927690..d64e7f691 100644 --- a/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Models/SubmissionWizardState.cs +++ b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Models/SubmissionWizardState.cs @@ -33,6 +33,8 @@ public class SubmissionWizardState : IDisposable public string Summary { get; set; } = string.Empty; public string Description { get; set; } = string.Empty; public bool IsDefault { get; set; } + public bool IsEssential { get; set; } + public bool IsDeviceProvider { get; set; } public List Categories { get; set; } = new(); public List Tags { get; set; } = new(); diff --git a/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/Layout/LayoutInfoStepView.axaml b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/Layout/LayoutInfoStepView.axaml index 489172d99..ce040d08e 100644 --- a/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/Layout/LayoutInfoStepView.axaml +++ b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/Layout/LayoutInfoStepView.axaml @@ -4,7 +4,6 @@ xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:layout="clr-namespace:Artemis.UI.Screens.Workshop.SubmissionWizard.Steps.Layout" xmlns:shared="clr-namespace:Artemis.UI.Shared;assembly=Artemis.UI.Shared" - xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="Artemis.UI.Screens.Workshop.SubmissionWizard.Steps.Layout.LayoutInfoStepView" x:DataType="layout:LayoutInfoStepViewModel"> diff --git a/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/Profile/ProfileAdaptionHintsStepView.axaml b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/Profile/ProfileAdaptionHintsStepView.axaml index e17453b14..e0bdb007f 100644 --- a/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/Profile/ProfileAdaptionHintsStepView.axaml +++ b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/Profile/ProfileAdaptionHintsStepView.axaml @@ -2,7 +2,6 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" 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:profile="clr-namespace:Artemis.UI.Screens.Workshop.SubmissionWizard.Steps.Profile" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" diff --git a/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/Profile/ProfileAdaptionHintsStepViewModel.cs b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/Profile/ProfileAdaptionHintsStepViewModel.cs index 87d7872dc..e6a180dd1 100644 --- a/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/Profile/ProfileAdaptionHintsStepViewModel.cs +++ b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/Profile/ProfileAdaptionHintsStepViewModel.cs @@ -7,7 +7,6 @@ using System.Threading.Tasks; using Artemis.Core; using Artemis.Core.Services; using Artemis.UI.Screens.ProfileEditor.ProfileTree.Dialogs; -using Artemis.UI.Screens.Workshop.SubmissionWizard.Models; using Artemis.UI.Shared.Services; using Artemis.WebClient.Workshop.Handlers.UploadHandlers; using DynamicData; diff --git a/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/Profile/ProfileSelectionStepView.axaml b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/Profile/ProfileSelectionStepView.axaml index 8fd0eb216..7f5aee8f8 100644 --- a/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/Profile/ProfileSelectionStepView.axaml +++ b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/Profile/ProfileSelectionStepView.axaml @@ -38,6 +38,7 @@ Grid.Column="0" ConfigurationIcon="{CompiledBinding Icon}" VerticalAlignment="Center" + CornerRadius="4" Width="22" Height="22" Margin="0 0 10 0" /> diff --git a/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/SpecificationsStepViewModel.cs b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/SpecificationsStepViewModel.cs index 01a5c19fc..1b8c2a35c 100644 --- a/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/SpecificationsStepViewModel.cs +++ b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/SpecificationsStepViewModel.cs @@ -66,6 +66,8 @@ public partial class SpecificationsStepViewModel : SubmissionViewModel viewModel.Summary = State.Summary; viewModel.Description = State.Description; viewModel.IsDefault = State.IsDefault; + viewModel.IsEssential = State.IsEssential; + viewModel.IsDeviceProvider = State.IsDeviceProvider; // Tags viewModel.Tags.Clear(); @@ -73,12 +75,13 @@ public partial class SpecificationsStepViewModel : SubmissionViewModel // Categories viewModel.PreselectedCategories = State.Categories; + viewModel.EntryType = State.EntryType; // Icon if (State.Icon != null) { State.Icon.Seek(0, SeekOrigin.Begin); - viewModel.IconBitmap = BitmapExtensions.LoadAndResize(State.Icon, 128); + viewModel.IconBitmap = BitmapExtensions.LoadAndResize(State.Icon, 128, false); } EntrySpecificationsViewModel = viewModel; @@ -95,6 +98,8 @@ public partial class SpecificationsStepViewModel : SubmissionViewModel State.Summary = EntrySpecificationsViewModel.Summary; State.Description = EntrySpecificationsViewModel.Description; State.IsDefault = EntrySpecificationsViewModel.IsDefault; + State.IsEssential = EntrySpecificationsViewModel.IsEssential; + State.IsDeviceProvider = EntrySpecificationsViewModel.IsDeviceProvider; // Categories and tasks State.Categories = EntrySpecificationsViewModel.Categories.Where(c => c.IsSelected).Select(c => c.Id).ToList(); diff --git a/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/SubmitStepViewModel.cs b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/SubmitStepViewModel.cs index 5cbc4c1ef..111b9bb36 100644 --- a/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/SubmitStepViewModel.cs +++ b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/SubmitStepViewModel.cs @@ -38,7 +38,7 @@ public partial class SubmitStepViewModel : SubmissionViewModel IconBitmap.DisposeWith(d); } - Observable.FromAsync(workshopClient.GetCategories.ExecuteAsync).Subscribe(PopulateCategories).DisposeWith(d); + Observable.FromAsync(() => workshopClient.GetCategories.ExecuteAsync(State.EntryType)).Subscribe(PopulateCategories).DisposeWith(d); }); } diff --git a/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/UploadStepViewModel.cs b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/UploadStepViewModel.cs index c205e24aa..ffb7cdd87 100644 --- a/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/UploadStepViewModel.cs +++ b/src/Artemis.UI/Screens/Workshop/SubmissionWizard/Steps/UploadStepViewModel.cs @@ -105,7 +105,14 @@ public partial class UploadStepViewModel : SubmissionViewModel Summary = State.Summary, Description = State.Description, Categories = State.Categories, - Tags = State.Tags + Tags = State.Tags, + DefaultEntryInfo = State.IsDefault + ? new DefaultEntryInfoInput + { + IsEssential = State.IsEssential, + IsDeviceProvider = State.IsDeviceProvider + } + : null }, cancellationToken); result.EnsureNoErrors(); diff --git a/src/Artemis.UI/SerilogAvaloniaSink.cs b/src/Artemis.UI/SerilogAvaloniaSink.cs index f40c83b28..b8dea3fcd 100644 --- a/src/Artemis.UI/SerilogAvaloniaSink.cs +++ b/src/Artemis.UI/SerilogAvaloniaSink.cs @@ -24,11 +24,11 @@ public class SerilogAvaloniaSink : ILogSink #if DEBUG // Except with binding errors, ignore anything that is information or lower - return (area == "Binding" || logLevel > SerilogLogLevel.Information) && _logger.IsEnabled(logLevel); - #else + return true; +#else // Ignore binding errors in release builds, shoo return area != "Binding" && logLevel > SerilogLogLevel.Information && _logger.IsEnabled(logLevel); - #endif +#endif } /// diff --git a/src/Artemis.UI/Services/Interfaces/IPluginInteractionService.cs b/src/Artemis.UI/Services/Interfaces/IPluginInteractionService.cs new file mode 100644 index 000000000..b76d765d6 --- /dev/null +++ b/src/Artemis.UI/Services/Interfaces/IPluginInteractionService.cs @@ -0,0 +1,44 @@ +using System.Threading.Tasks; +using Artemis.Core; + +namespace Artemis.UI.Services.Interfaces; + +public interface IPluginInteractionService : IArtemisUIService +{ + /// + /// Enables a plugin, showing prerequisites and config windows as necessary. + /// + /// The plugin to enable. + /// + /// A task representing the asynchronous operation. + Task EnablePlugin(Plugin plugin, bool showMandatoryConfigWindow); + + /// + /// Disables a plugin, stopping all its features and services. + /// + /// The plugin to disable. + /// A task representing the asynchronous operation with a boolean indicating success. + Task DisablePlugin(Plugin plugin); + + /// + /// Removes a plugin from the system, optionally running uninstall actions for prerequisites. + /// + /// The plugin to remove. + /// A task representing the asynchronous operation with a boolean indicating success. + Task RemovePlugin(Plugin plugin); + + /// + /// Removes all settings and configuration data for a plugin, temporarily disabling it during the process. + /// + /// The plugin whose settings should be cleared. + /// A task representing the asynchronous operation with a boolean indicating success. + Task RemovePluginSettings(Plugin plugin); + + /// + /// Removes the prerequisites for a plugin. + /// + /// The plugin whose prerequisites should be removed. + /// Whether the prerequisites are being removed for a plugin removal. + /// A task representing the asynchronous operation with a boolean indicating success. + Task RemovePluginPrerequisites(Plugin plugin, bool forPluginRemoval); +} \ No newline at end of file diff --git a/src/Artemis.UI/Services/PluginInteractionService.cs b/src/Artemis.UI/Services/PluginInteractionService.cs new file mode 100644 index 000000000..2314a2093 --- /dev/null +++ b/src/Artemis.UI/Services/PluginInteractionService.cs @@ -0,0 +1,170 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Artemis.Core; +using Artemis.Core.Services; +using Artemis.UI.Exceptions; +using Artemis.UI.Screens.Plugins; +using Artemis.UI.Services.Interfaces; +using Artemis.UI.Shared; +using Artemis.UI.Shared.Services; +using Artemis.UI.Shared.Services.Builders; + +namespace Artemis.UI.Services; + +public class PluginInteractionService : IPluginInteractionService +{ + private readonly ICoreService _coreService; + private readonly IPluginManagementService _pluginManagementService; + private readonly IWindowService _windowService; + private readonly INotificationService _notificationService; + + public PluginInteractionService(ICoreService coreService, IPluginManagementService pluginManagementService, IWindowService windowService, INotificationService notificationService) + { + _coreService = coreService; + _pluginManagementService = pluginManagementService; + _windowService = windowService; + _notificationService = notificationService; + } + + /// + public async Task EnablePlugin(Plugin plugin, bool showMandatoryConfigWindow) + { + try + { + if (plugin.Info.RequiresAdmin && !_coreService.IsElevated) + { + bool confirmed = await _windowService.ShowConfirmContentDialog( + "Enable plugin", + "This plugin requires admin rights, are you sure you want to enable it? Artemis will need to restart.", + "Confirm and restart" + ); + if (!confirmed) + return false; + } + + // Check if all prerequisites are met async + List subjects = [plugin.Info]; + subjects.AddRange(plugin.Features.Where(f => f.AlwaysEnabled || f.EnabledInStorage)); + + if (subjects.Any(s => !s.ArePrerequisitesMet())) + { + await PluginPrerequisitesInstallDialogViewModel.Show(_windowService, subjects); + if (!subjects.All(s => s.ArePrerequisitesMet())) + return false; + } + + await Task.Run(() => _pluginManagementService.EnablePlugin(plugin, true, true)); + + // If the plugin has a mandatory settings window, open it and wait + if (showMandatoryConfigWindow && plugin.ConfigurationDialog != null && plugin.ConfigurationDialog.IsMandatory) + { + if (plugin.Resolve(plugin.ConfigurationDialog.Type) is not PluginConfigurationViewModel viewModel) + throw new ArtemisUIException($"The type of a plugin configuration dialog must inherit {nameof(PluginConfigurationViewModel)}"); + + await _windowService.ShowDialogAsync(new PluginSettingsWindowViewModel(viewModel)); + } + + return true; + } + catch (Exception e) + { + await ShowPluginToggleFailure(plugin, true, e); + } + + return false; + } + + /// + public async Task DisablePlugin(Plugin plugin) + { + try + { + await Task.Run(() => _pluginManagementService.DisablePlugin(plugin, true)); + return true; + } + catch (Exception e) + { + await ShowPluginToggleFailure(plugin, false, e); + } + + return false; + } + + /// + public async Task RemovePlugin(Plugin plugin) + { + bool confirmed = await _windowService.ShowConfirmContentDialog("Remove plugin", "Are you sure you want to remove this plugin?"); + if (!confirmed) + return false; + + // If the plugin or any of its features has uninstall actions, offer to run these + await RemovePrerequisites(plugin, true); + + try + { + _pluginManagementService.RemovePlugin(plugin, false); + } + catch (Exception e) + { + _windowService.ShowExceptionDialog("Failed to remove plugin", e); + throw; + } + + _notificationService.CreateNotification().WithTitle("Removed plugin.").Show(); + return true; + } + + /// + public async Task RemovePluginSettings(Plugin plugin) + { + bool confirmed = await _windowService.ShowConfirmContentDialog("Clear plugin settings", "Are you sure you want to clear the settings of this plugin?"); + if (!confirmed) + return false; + + bool wasEnabled = plugin.IsEnabled; + + if (wasEnabled) + _pluginManagementService.DisablePlugin(plugin, false); + + _pluginManagementService.RemovePluginSettings(plugin); + + if (wasEnabled) + _pluginManagementService.EnablePlugin(plugin, false); + + _notificationService.CreateNotification().WithTitle("Cleared plugin settings.").Show(); + + return true; + } + + /// + public async Task RemovePluginPrerequisites(Plugin plugin, bool forPluginRemoval) + { + await RemovePrerequisites(plugin, false); + return true; + } + + private async Task ShowPluginToggleFailure(Plugin plugin, bool enable, Exception e) + { + string action = enable ? "enable" : "disable"; + ContentDialogBuilder builder = _windowService.CreateContentDialog() + .WithTitle($"Failed to {action} plugin {plugin.Info.Name}") + .WithContent(e.Message) + .HavingPrimaryButton(b => b.WithText("View logs").WithAction(() => Utilities.OpenFolder(Constants.LogsFolder))); + // If available, add a secondary button pointing to the support page + if (plugin.Info.HelpPage != null) + builder = builder.HavingSecondaryButton(b => b.WithText("Open support page").WithAction(() => Utilities.OpenUrl(plugin.Info.HelpPage.ToString()))); + + await builder.ShowAsync(); + } + + private async Task RemovePrerequisites(Plugin plugin, bool forPluginRemoval) + { + List subjects = [plugin.Info]; + subjects.AddRange(!forPluginRemoval ? plugin.Features.Where(f => f.AlwaysEnabled) : plugin.Features); + + if (subjects.Any(s => s.PlatformPrerequisites.Any(p => p.UninstallActions.Any()))) + await PluginPrerequisitesUninstallDialogViewModel.Show(_windowService, subjects, forPluginRemoval ? "Skip, remove plugin" : "Cancel"); + } +} \ No newline at end of file diff --git a/src/Artemis.WebClient.Workshop/BuiltInPluginsMigrator.cs b/src/Artemis.WebClient.Workshop/BuiltInPluginsMigrator.cs new file mode 100644 index 000000000..e6c29cffd --- /dev/null +++ b/src/Artemis.WebClient.Workshop/BuiltInPluginsMigrator.cs @@ -0,0 +1,106 @@ +using Artemis.Core; +using Artemis.Storage.Entities.Plugins; +using Artemis.Storage.Repositories.Interfaces; +using Artemis.UI.Shared.Utilities; +using Artemis.WebClient.Workshop.Services; +using Serilog; +using StrawberryShake; + +namespace Artemis.WebClient.Workshop; + +public static class BuiltInPluginsMigrator +{ + private static readonly Guid[] ObsoleteBuiltInPlugins = + [ + new("4e1e54fd-6636-40ad-afdc-b3b0135feab2"), + new("cad475d3-c621-4ec7-bbfc-784e3b4723ce"), + new("ab41d601-35e0-4a73-bf0b-94509b006ab0"), + new("27d124e3-48e8-4b0a-8a5e-d5e337a88d4a") + ]; + + public static async Task Migrate(IWorkshopService workshopService, IWorkshopClient workshopClient, ILogger logger, IPluginRepository pluginRepository) + { + // If no default plugins are present (later installs), do nothing + DirectoryInfo pluginDirectory = new(Constants.PluginsFolder); + if (!pluginDirectory.Exists) + { + return true; + } + + // Load plugin info, the plugin management service isn't available yet (which is exactly what we want) + List<(PluginInfo PluginInfo, DirectoryInfo Directory)> plugins = []; + foreach (DirectoryInfo subDirectory in pluginDirectory.EnumerateDirectories()) + { + try + { + // Load the metadata + string metadataFile = Path.Combine(subDirectory.FullName, "plugin.json"); + if (File.Exists(metadataFile)) + plugins.Add((CoreJson.Deserialize(await File.ReadAllTextAsync(metadataFile))!, subDirectory)); + } + catch (Exception) + { + // ignored, who knows what old stuff people might have in their plugins folder + } + } + + if (plugins.Count == 0) + { + return true; + } + + IWorkshopService.WorkshopStatus workshopStatus = await workshopService.GetWorkshopStatus(CancellationToken.None); + if (!workshopStatus.IsReachable) + { + logger.Warning("MigrateBuiltInPlugins - Cannot migrate built-in plugins because the workshop is unreachable"); + return false; + } + + logger.Information("MigrateBuiltInPlugins - Migrating built-in plugins to workshop entries"); + IOperationResult result = await workshopClient.GetDefaultPlugins.ExecuteAsync(100, null, CancellationToken.None); + List entries = result.Data?.EntriesV2?.Edges?.Select(e => e.Node).ToList() ?? []; + while (result.Data?.EntriesV2?.PageInfo is {HasNextPage: true}) + { + result = await workshopClient.GetDefaultPlugins.ExecuteAsync(100, result.Data.EntriesV2.PageInfo.EndCursor, CancellationToken.None); + if (result.Data?.EntriesV2?.Edges != null) + entries.AddRange(result.Data.EntriesV2.Edges.Select(e => e.Node)); + } + + logger.Information("MigrateBuiltInPlugins - Found {Count} default plugins in the workshop", entries.Count); + foreach (IGetDefaultPlugins_EntriesV2_Edges_Node entry in entries) + { + // Skip entries without plugin info or releases, shouldn't happen but theoretically possible + if (entry.PluginInfo == null || entry.LatestRelease == null) + continue; + + // Find a built-in plugin + (PluginInfo? pluginInfo, DirectoryInfo? directory) = plugins.FirstOrDefault(p => p.PluginInfo.Guid == entry.PluginInfo.PluginGuid); + if (pluginInfo == null || directory == null) + continue; + + // If the plugin is enabled, install the workshop equivalent (the built-in plugin will be removed by the install process) + PluginEntity? entity = pluginRepository.GetPluginByPluginGuid(pluginInfo.Guid); + if (entity != null && entity.IsEnabled) + { + logger.Information("MigrateBuiltInPlugins - Migrating built-in plugin {Plugin} to workshop entry {Entry}", pluginInfo.Name, entry); + await workshopService.InstallEntry(entry, entry.LatestRelease, new Progress(), CancellationToken.None); + } + + // Remove the built-in plugin, it's no longer needed + directory.Delete(true); + } + + // Remove obsolete built-in plugins + foreach (Guid obsoleteBuiltInPlugin in ObsoleteBuiltInPlugins) + { + (PluginInfo? pluginInfo, DirectoryInfo? directory) = plugins.FirstOrDefault(p => p.PluginInfo.Guid == obsoleteBuiltInPlugin); + if (pluginInfo == null || directory == null) + continue; + + directory.Delete(true); + } + + logger.Information("MigrateBuiltInPlugins - Finished migrating built-in plugins to workshop entries"); + return true; + } +} \ No newline at end of file diff --git a/src/Artemis.WebClient.Workshop/Handlers/InstallationHandlers/EntryInstallResult.cs b/src/Artemis.WebClient.Workshop/Handlers/InstallationHandlers/EntryInstallResult.cs index e3a2ecde4..2f1cca251 100644 --- a/src/Artemis.WebClient.Workshop/Handlers/InstallationHandlers/EntryInstallResult.cs +++ b/src/Artemis.WebClient.Workshop/Handlers/InstallationHandlers/EntryInstallResult.cs @@ -1,13 +1,31 @@ -using Artemis.WebClient.Workshop.Models; +using Artemis.Core; +using Artemis.WebClient.Workshop.Models; using Artemis.WebClient.Workshop.Services; namespace Artemis.WebClient.Workshop.Handlers.InstallationHandlers; public class EntryInstallResult { - public bool IsSuccess { get; set; } - public string? Message { get; set; } - public InstalledEntry? Entry { get; set; } + /// + /// Gets a value indicating whether the installation was successful. + /// + public bool IsSuccess { get; private set; } + + /// + /// Gets a message describing the result of the installation. + /// + public string? Message { get; private set; } + + /// + /// Gets the entry that was installed, if any. + /// + public InstalledEntry? Entry { get; private set; } + + /// + /// Gets the result object returned by the installation handler, if any. + /// This'll be a , or depending on the entry type. + /// + public object? Installed { get; private set; } public static EntryInstallResult FromFailure(string? message) { @@ -18,12 +36,13 @@ public class EntryInstallResult }; } - public static EntryInstallResult FromSuccess(InstalledEntry installedEntry) + public static EntryInstallResult FromSuccess(InstalledEntry installedEntry, object? result) { return new EntryInstallResult { IsSuccess = true, - Entry = installedEntry + Entry = installedEntry, + Installed = result }; } diff --git a/src/Artemis.WebClient.Workshop/Handlers/InstallationHandlers/Implementations/LayoutEntryInstallationHandler.cs b/src/Artemis.WebClient.Workshop/Handlers/InstallationHandlers/Implementations/LayoutEntryInstallationHandler.cs index 849b466b5..7636c5def 100644 --- a/src/Artemis.WebClient.Workshop/Handlers/InstallationHandlers/Implementations/LayoutEntryInstallationHandler.cs +++ b/src/Artemis.WebClient.Workshop/Handlers/InstallationHandlers/Implementations/LayoutEntryInstallationHandler.cs @@ -59,7 +59,7 @@ public class LayoutEntryInstallationHandler : IEntryInstallationHandler { installedEntry.ApplyRelease(release); _workshopService.SaveInstalledEntry(installedEntry); - return EntryInstallResult.FromSuccess(installedEntry); + return EntryInstallResult.FromSuccess(installedEntry, layout); } // If the layout ended up being invalid yoink it out again, shoooo diff --git a/src/Artemis.WebClient.Workshop/Handlers/InstallationHandlers/Implementations/PluginEntryInstallationHandler.cs b/src/Artemis.WebClient.Workshop/Handlers/InstallationHandlers/Implementations/PluginEntryInstallationHandler.cs index d597eb4e7..b4451d9cf 100644 --- a/src/Artemis.WebClient.Workshop/Handlers/InstallationHandlers/Implementations/PluginEntryInstallationHandler.cs +++ b/src/Artemis.WebClient.Workshop/Handlers/InstallationHandlers/Implementations/PluginEntryInstallationHandler.cs @@ -30,7 +30,7 @@ public class PluginEntryInstallationHandler : IEntryInstallationHandler { // If the folder already exists, we're not going to reinstall the plugin since files may be in use, consider our job done if (installedEntry.GetReleaseDirectory(release).Exists) - return ApplyAndSave(installedEntry, release); + return ApplyAndSave(null, installedEntry, release); } else { @@ -64,7 +64,12 @@ public class PluginEntryInstallationHandler : IEntryInstallationHandler archive.ExtractToDirectory(releaseDirectory.FullName); PluginInfo pluginInfo = CoreJson.Deserialize(await File.ReadAllTextAsync(Path.Combine(releaseDirectory.FullName, "plugin.json"), cancellationToken))!; + installedEntry.SetMetadata("PluginId", pluginInfo.Guid); + // If the plugin management service isn't loaded yet (happens while migrating from built-in plugins) we're done here + if (!_pluginManagementService.LoadedPlugins) + return ApplyAndSave(null, installedEntry, release); + // If there is already a version of the plugin installed, remove it Plugin? currentVersion = _pluginManagementService.GetAllPlugins().FirstOrDefault(p => p.Guid == pluginInfo.Guid); if (currentVersion != null) @@ -78,13 +83,12 @@ public class PluginEntryInstallationHandler : IEntryInstallationHandler } // Load the plugin, next time during startup this will happen automatically + Plugin? plugin = null; try { - Plugin? plugin = _pluginManagementService.LoadPlugin(releaseDirectory); + plugin = _pluginManagementService.LoadPlugin(releaseDirectory); if (plugin == null) throw new ArtemisWorkshopException("Failed to load plugin, it may be incompatible"); - - installedEntry.SetMetadata("PluginId", plugin.Guid); } catch (Exception e) { @@ -102,7 +106,7 @@ public class PluginEntryInstallationHandler : IEntryInstallationHandler return EntryInstallResult.FromFailure(e.Message); } - return ApplyAndSave(installedEntry, release); + return ApplyAndSave(plugin, installedEntry, release); } public Task UninstallAsync(InstalledEntry installedEntry, CancellationToken cancellationToken) @@ -133,10 +137,10 @@ public class PluginEntryInstallationHandler : IEntryInstallationHandler return Task.FromResult(EntryUninstallResult.FromSuccess(message)); } - private EntryInstallResult ApplyAndSave(InstalledEntry installedEntry, IRelease release) + private EntryInstallResult ApplyAndSave(Plugin? plugin, InstalledEntry installedEntry, IRelease release) { installedEntry.ApplyRelease(release); _workshopService.SaveInstalledEntry(installedEntry); - return EntryInstallResult.FromSuccess(installedEntry); + return EntryInstallResult.FromSuccess(installedEntry, plugin); } } \ No newline at end of file diff --git a/src/Artemis.WebClient.Workshop/Handlers/InstallationHandlers/Implementations/ProfileEntryInstallationHandler.cs b/src/Artemis.WebClient.Workshop/Handlers/InstallationHandlers/Implementations/ProfileEntryInstallationHandler.cs index d702681a4..f16aa8d36 100644 --- a/src/Artemis.WebClient.Workshop/Handlers/InstallationHandlers/Implementations/ProfileEntryInstallationHandler.cs +++ b/src/Artemis.WebClient.Workshop/Handlers/InstallationHandlers/Implementations/ProfileEntryInstallationHandler.cs @@ -52,7 +52,7 @@ public class ProfileEntryInstallationHandler : IEntryInstallationHandler // With everything updated, remove the old profile _profileService.RemoveProfileConfiguration(existing); - return EntryInstallResult.FromSuccess(installedEntry); + return EntryInstallResult.FromSuccess(installedEntry, overwritten); } } @@ -66,7 +66,7 @@ public class ProfileEntryInstallationHandler : IEntryInstallationHandler // Update the release and return the profile configuration UpdateRelease(installedEntry, release); - return EntryInstallResult.FromSuccess(installedEntry); + return EntryInstallResult.FromSuccess(installedEntry, imported); } public async Task UninstallAsync(InstalledEntry installedEntry, CancellationToken cancellationToken) diff --git a/src/Artemis.WebClient.Workshop/Queries/Fragments.graphql b/src/Artemis.WebClient.Workshop/Queries/Fragments.graphql index a1f8eb669..f1c94bfa2 100644 --- a/src/Artemis.WebClient.Workshop/Queries/Fragments.graphql +++ b/src/Artemis.WebClient.Workshop/Queries/Fragments.graphql @@ -6,7 +6,7 @@ fragment category on Category { fragment image on Image { id name - description + description } fragment layoutInfo on LayoutInfo { @@ -26,7 +26,9 @@ fragment submittedEntry on Entry { entryType downloads createdAt - isDefault + defaultEntryInfo { + ...defaultEntryInfo + } latestRelease { ...release } @@ -48,6 +50,9 @@ fragment entrySummary on Entry { categories { ...category } + defaultEntryInfo { + ...defaultEntryInfo + } } fragment entryDetails on Entry { @@ -77,7 +82,7 @@ fragment release on Release { downloadSize md5Hash createdAt -} +} fragment releaseDetails on Release { ...release @@ -95,4 +100,9 @@ fragment pluginInfo on PluginInfo { supportsWindows supportsLinux supportsOSX +} + +fragment defaultEntryInfo on DefaultEntryInfo { + isEssential + isDeviceProvider } \ No newline at end of file diff --git a/src/Artemis.WebClient.Workshop/Queries/GetCategories.graphql b/src/Artemis.WebClient.Workshop/Queries/GetCategories.graphql index 864caa217..1770bfcdc 100644 --- a/src/Artemis.WebClient.Workshop/Queries/GetCategories.graphql +++ b/src/Artemis.WebClient.Workshop/Queries/GetCategories.graphql @@ -1,5 +1,5 @@ -query GetCategories { - categories(order: {name: ASC}) { +query GetCategories($entryType: EntryType) { + categories(where: {entryType: {in: [$entryType, null]}} order: {name: ASC}) { id name icon diff --git a/src/Artemis.WebClient.Workshop/Queries/GetEntries.graphql b/src/Artemis.WebClient.Workshop/Queries/GetEntries.graphql index ce6eedddc..10c5502ed 100644 --- a/src/Artemis.WebClient.Workshop/Queries/GetEntries.graphql +++ b/src/Artemis.WebClient.Workshop/Queries/GetEntries.graphql @@ -1,5 +1,5 @@ -query GetEntriesv2($search: String $filter: EntryFilterInput $order: [EntrySortInput!] $first: Int $after: String) { - entriesV2(search: $search where: $filter order: $order first: $first after: $after) { +query GetEntries($search: String $includeDefaults: Boolean $filter: EntryFilterInput $order: [EntrySortInput!] $first: Int $after: String) { + entriesV2(search: $search includeDefaults: $includeDefaults where: $filter order: $order first: $first after: $after) { totalCount pageInfo { hasNextPage @@ -18,4 +18,54 @@ query GetPopularEntries { popularEntries { ...entrySummary } +} + +query GetDefaultEntries($first: Int, $after: String) { + entriesV2( + includeDefaults: true + where: { defaultEntryInfo: { entryId: { gt: 0 } } } + first: $first + after: $after + ) { + totalCount + pageInfo { + hasNextPage + endCursor + } + edges { + cursor + node { + ...entrySummary + } + } + } +} + +query GetDefaultPlugins($first: Int, $after: String) { + entriesV2( + includeDefaults: true + where: { + and: [ + { defaultEntryInfo: { entryId: { gt: 0 } } } + { entryType: {eq: PLUGIN} } + ] + } + first: $first + after: $after + ) { + totalCount + pageInfo { + hasNextPage + endCursor + } + edges { + cursor + node { + ...entrySummary + pluginInfo { + pluginGuid + } + } + } + } } \ No newline at end of file diff --git a/src/Artemis.WebClient.Workshop/Services/AuthenticationService.cs b/src/Artemis.WebClient.Workshop/Services/AuthenticationService.cs index 7e7e76f47..3f7e65735 100644 --- a/src/Artemis.WebClient.Workshop/Services/AuthenticationService.cs +++ b/src/Artemis.WebClient.Workshop/Services/AuthenticationService.cs @@ -180,13 +180,15 @@ internal class AuthenticationService : CorePropertyChanged, IAuthenticationServi { await _authLock.WaitAsync(cancellationToken); + // Start a HTTP listener, this port could be in use but chances are very slim + // IdentityServer only accepts these two redirect URLs + string redirectUri = Constants.StartupArguments.Contains("--alt-login-callback") ? "http://localhost:56789" : "http://localhost:57461"; + try { if (_isLoggedInSubject.Value) return; - - // Start a HTTP listener, this port could be in use but chances are very slim - string redirectUri = "http://localhost:57461"; + using HttpListener listener = new(); listener.Prefixes.Add(redirectUri + "/"); listener.Start(); @@ -249,7 +251,11 @@ internal class AuthenticationService : CorePropertyChanged, IAuthenticationServi } catch (HttpListenerException e) { - throw new ArtemisWebClientException($"HTTP listener for login callback failed with error code {e.ErrorCode}", e); + // I've seen the Nvidia app do this after a login. What are the odds... + if (e.ErrorCode == 32) + throw new ArtemisWebClientException($"HTTP listener for login callback failed because another application is already listening on '{redirectUri}', please close that application and try again", e); + else + throw new ArtemisWebClientException($"HTTP listener for login callback failed with error code {e.ErrorCode}", e); } finally { diff --git a/src/Artemis.WebClient.Workshop/Services/Interfaces/IWorkshopService.cs b/src/Artemis.WebClient.Workshop/Services/Interfaces/IWorkshopService.cs index 922ed3d0d..861d23c49 100644 --- a/src/Artemis.WebClient.Workshop/Services/Interfaces/IWorkshopService.cs +++ b/src/Artemis.WebClient.Workshop/Services/Interfaces/IWorkshopService.cs @@ -54,9 +54,10 @@ public interface IWorkshopService /// /// Validates the status of the workshop. /// + /// Whether to navigate to the offline page if the workshop is unreachable. /// The cancellation token. /// A boolean indicating whether the workshop is reachable. - Task ValidateWorkshopStatus(CancellationToken cancellationToken); + Task ValidateWorkshopStatus(bool navigateIfUnreachable, CancellationToken cancellationToken); /// /// Navigates to a specific entry. @@ -123,7 +124,7 @@ public interface IWorkshopService /// /// Initializes the workshop service. /// - void Initialize(); + Task Initialize(); /// /// Represents the status of the workshop. @@ -133,6 +134,7 @@ public interface IWorkshopService public event EventHandler? OnInstalledEntrySaved; public event EventHandler? OnEntryUninstalled; public event EventHandler? OnEntryInstalled; + public event EventHandler? MigratingBuildInPlugins; void SetAutoUpdate(InstalledEntry installedEntry, bool autoUpdate); } \ No newline at end of file diff --git a/src/Artemis.WebClient.Workshop/Services/WorkshopService.cs b/src/Artemis.WebClient.Workshop/Services/WorkshopService.cs index 7fcf5f847..68698cec2 100644 --- a/src/Artemis.WebClient.Workshop/Services/WorkshopService.cs +++ b/src/Artemis.WebClient.Workshop/Services/WorkshopService.cs @@ -1,6 +1,7 @@ using System.Net.Http.Headers; using Artemis.Core; using Artemis.Core.Services; +using Artemis.Storage.Entities.Plugins; using Artemis.Storage.Entities.Workshop; using Artemis.Storage.Repositories.Interfaces; using Artemis.UI.Shared.Routing; @@ -10,6 +11,7 @@ using Artemis.WebClient.Workshop.Handlers.InstallationHandlers; using Artemis.WebClient.Workshop.Handlers.UploadHandlers; using Artemis.WebClient.Workshop.Models; using Serilog; +using StrawberryShake; namespace Artemis.WebClient.Workshop.Services; @@ -22,15 +24,24 @@ public class WorkshopService : IWorkshopService private readonly Lazy _pluginManagementService; private readonly Lazy _profileService; private readonly EntryInstallationHandlerFactory _factory; + private readonly IPluginRepository _pluginRepository; + private readonly IWorkshopClient _workshopClient; + private readonly PluginSetting _migratedBuiltInPlugins; + + + private bool _initialized; public WorkshopService(ILogger logger, IHttpClientFactory httpClientFactory, IRouter router, IEntryRepository entryRepository, + ISettingsService settingsService, Lazy pluginManagementService, Lazy profileService, - EntryInstallationHandlerFactory factory) + EntryInstallationHandlerFactory factory, + IPluginRepository pluginRepository, + IWorkshopClient workshopClient) { _logger = logger; _httpClientFactory = httpClientFactory; @@ -39,6 +50,10 @@ public class WorkshopService : IWorkshopService _pluginManagementService = pluginManagementService; _profileService = profileService; _factory = factory; + _pluginRepository = pluginRepository; + _workshopClient = workshopClient; + + _migratedBuiltInPlugins = settingsService.GetSetting("Workshop.MigratedBuiltInPlugins", false); } public async Task GetEntryIcon(long entryId, CancellationToken cancellationToken) @@ -130,10 +145,10 @@ public class WorkshopService : IWorkshopService } /// - public async Task ValidateWorkshopStatus(CancellationToken cancellationToken) + public async Task ValidateWorkshopStatus(bool navigateIfUnreachable, CancellationToken cancellationToken) { IWorkshopService.WorkshopStatus status = await GetWorkshopStatus(cancellationToken); - if (!status.IsReachable && !cancellationToken.IsCancellationRequested) + if (navigateIfUnreachable && !status.IsReachable && !cancellationToken.IsCancellationRequested) await _router.Navigate($"workshop/offline/{status.Message}"); return status.IsReachable; } @@ -166,7 +181,7 @@ public class WorkshopService : IWorkshopService OnEntryInstalled?.Invoke(this, result.Entry); else _logger.Warning("Failed to install entry {Entry}: {Message}", entry, result.Message); - + return result; } @@ -227,7 +242,7 @@ public class WorkshopService : IWorkshopService } /// - public void Initialize() + public async Task Initialize() { if (_initialized) throw new ArtemisWorkshopException("Workshop service is already initialized"); @@ -238,6 +253,7 @@ public class WorkshopService : IWorkshopService Directory.CreateDirectory(Constants.WorkshopFolder); RemoveOrphanedFiles(); + await MigrateBuiltInPlugins(); _pluginManagementService.Value.AdditionalPluginDirectories.AddRange(GetInstalledEntries() .Where(e => e.EntryType == EntryType.Plugin) @@ -259,7 +275,7 @@ public class WorkshopService : IWorkshopService { if (installedEntry.AutoUpdate == autoUpdate) return; - + installedEntry.AutoUpdate = autoUpdate; SaveInstalledEntry(installedEntry); } @@ -297,6 +313,19 @@ public class WorkshopService : IWorkshopService } } + private async Task MigrateBuiltInPlugins() + { + // If already migrated, do nothing + if (_migratedBuiltInPlugins.Value) + return; + + MigratingBuildInPlugins?.Invoke(this, EventArgs.Empty); + + bool migrated = await BuiltInPluginsMigrator.Migrate(this, _workshopClient, _logger, _pluginRepository); + _migratedBuiltInPlugins.Value = migrated; + _migratedBuiltInPlugins.Save(); + } + private void ProfileServiceOnProfileRemoved(object? sender, ProfileConfigurationEventArgs e) { InstalledEntry? entry = GetInstalledEntryByProfile(e.ProfileConfiguration); @@ -322,4 +351,6 @@ public class WorkshopService : IWorkshopService public event EventHandler? OnEntryUninstalled; public event EventHandler? OnEntryInstalled; + + public event EventHandler? MigratingBuildInPlugins; } \ No newline at end of file diff --git a/src/Artemis.WebClient.Workshop/graphql.config.yml b/src/Artemis.WebClient.Workshop/graphql.config.yml index 9662a514f..f4c94896d 100644 --- a/src/Artemis.WebClient.Workshop/graphql.config.yml +++ b/src/Artemis.WebClient.Workshop/graphql.config.yml @@ -2,7 +2,7 @@ schema: schema.graphql extensions: endpoints: Default GraphQL Endpoint: - url: https://workshop.artemis-rgb.com/graphql + url: https://workshop.artemis-rgb.com/graphql/ headers: user-agent: JS GraphQL introspect: true diff --git a/src/Artemis.WebClient.Workshop/schema.graphql b/src/Artemis.WebClient.Workshop/schema.graphql index f07aef9e7..ebc1d6535 100644 --- a/src/Artemis.WebClient.Workshop/schema.graphql +++ b/src/Artemis.WebClient.Workshop/schema.graphql @@ -1,631 +1,753 @@ -# This file was generated. Do not edit manually. - -schema { - query: Query - mutation: Mutation +schema { + query: Query + mutation: Mutation } type Category { - icon: String! - id: Long! - name: String! + id: Long! + name: String! + icon: String! + entryType: EntryType } "Information about the offset pagination." type CollectionSegmentInfo { - "Indicates whether more items exist following the set defined by the clients arguments." - hasNextPage: Boolean! - "Indicates whether more items exist prior the set defined by the clients arguments." - hasPreviousPage: Boolean! + "Indicates whether more items exist following the set defined by the clients arguments." + hasNextPage: Boolean! + "Indicates whether more items exist prior the set defined by the clients arguments." + hasPreviousPage: Boolean! +} + +type DefaultEntryInfo { + entryId: Long! + isEssential: Boolean! + isDeviceProvider: Boolean! } "A segment of a collection." type EntriesCollectionSegment { - "A flattened list of the items." - items: [Entry!] - "Information to aid in pagination." - pageInfo: CollectionSegmentInfo! - totalCount: Int! + "Information to aid in pagination." + pageInfo: CollectionSegmentInfo! + "A flattened list of the items." + items: [Entry!] + totalCount: Int! @cost(weight: "10") } "A connection to a list of items." type EntriesV2Connection { - "A list of edges." - edges: [EntriesV2Edge!] - "A flattened list of the nodes." - nodes: [Entry!] - "Information to aid in pagination." - pageInfo: PageInfo! - "Identifies the total count of items in the connection." - totalCount: Int! + "Information to aid in pagination." + pageInfo: PageInfo! + "A list of edges." + edges: [EntriesV2Edge!] + "A flattened list of the nodes." + nodes: [Entry!] + "Identifies the total count of items in the connection." + totalCount: Int! @cost(weight: "10") } "An edge in a connection." type EntriesV2Edge { - "A cursor for use in pagination." - cursor: String! - "The item at the end of the edge." - node: Entry! + "A cursor for use in pagination." + cursor: String! + "The item at the end of the edge." + node: Entry! } type Entry { - author: String! - authorId: UUID! - categories: [Category!]! - createdAt: DateTime! - dependantReleases: [Release!]! - description: String! - downloads: Long! - entryType: EntryType! - icon: Image - iconId: UUID - id: Long! - images: [Image!]! - isDefault: Boolean! - isOfficial: Boolean! - latestRelease: Release - latestReleaseId: Long - layoutInfo: [LayoutInfo!]! - name: String! - pluginInfo: PluginInfo - popularityScore: Float! - releases: [Release!]! - summary: String! - tags: [Tag!]! + id: Long! + entryType: EntryType! + createdAt: DateTime! + authorId: UUID! + author: String! + isOfficial: Boolean! + name: String! + summary: String! + description: String! + downloads: Long! + iconId: UUID + icon: Image + latestReleaseId: Long + latestRelease: Release + pluginInfo: PluginInfo + layoutInfo: [LayoutInfo!]! + defaultEntryInfo: DefaultEntryInfo + categories: [Category!]! + tags: [Tag!]! + images: [Image!]! + releases: [Release!]! + dependantReleases: [Release!]! } type Image { - description: String - entry: Entry - entryId: Long - height: Int! - id: UUID! - mimeType: String! - name: String! - size: Long! - width: Int! + id: UUID! + name: String! + description: String + width: Int! + height: Int! + size: Long! + mimeType: String! + entry: Entry + entryId: Long } type LayoutInfo { - deviceProvider: UUID! - deviceType: RGBDeviceType! - entry: Entry! - entryId: Long! - id: Long! - logicalLayout: String - model: String! - physicalLayout: KeyboardLayoutType - vendor: String! + id: Long! + deviceProvider: UUID! + deviceType: RGBDeviceType! + vendor: String! + model: String! + physicalLayout: KeyboardLayoutType + logicalLayout: String + entryId: Long! + entry: Entry! } type Mutation { - addEntry(input: CreateEntryInput!): Entry - addLayoutInfo(input: CreateLayoutInfoInput!): LayoutInfo - removeEntry(id: Long!): Entry - removeLayoutInfo(id: Long!): LayoutInfo! - removeRelease(id: Long!): Release! - setLayoutInfo(input: SetLayoutInfoInput!): [LayoutInfo!]! - updateEntry(input: UpdateEntryInput!): Entry - updateEntryImage(input: UpdateEntryImageInput!): Image - updateRelease(input: UpdateReleaseInput!): Release + addEntry(input: CreateEntryInput!): Entry @authorize @cost(weight: "10") + updateEntry(input: UpdateEntryInput!): Entry @authorize @cost(weight: "10") + removeEntry(id: Long!): Entry @authorize @cost(weight: "10") + updateEntryImage(input: UpdateEntryImageInput!): Image + @authorize + @cost(weight: "10") + setLayoutInfo(input: SetLayoutInfoInput!): [LayoutInfo!]! + @authorize + @cost(weight: "10") + addLayoutInfo(input: CreateLayoutInfoInput!): LayoutInfo + @authorize + @cost(weight: "10") + removeLayoutInfo(id: Long!): LayoutInfo! @authorize @cost(weight: "10") + updateRelease(input: UpdateReleaseInput!): Release + @authorize + @cost(weight: "10") + removeRelease(id: Long!): Release! @authorize @cost(weight: "10") } "Information about pagination in a connection." type PageInfo { - "When paginating forwards, the cursor to continue." - endCursor: String - "Indicates whether more edges exist following the set defined by the clients arguments." - hasNextPage: Boolean! - "Indicates whether more edges exist prior the set defined by the clients arguments." - hasPreviousPage: Boolean! - "When paginating backwards, the cursor to continue." - startCursor: String + "Indicates whether more edges exist following the set defined by the clients arguments." + hasNextPage: Boolean! + "Indicates whether more edges exist prior the set defined by the clients arguments." + hasPreviousPage: Boolean! + "When paginating backwards, the cursor to continue." + startCursor: String + "When paginating forwards, the cursor to continue." + endCursor: String } type PluginInfo { - api: Int - entry: Entry! - entryId: Long! - helpPage: String - minmumVersion: String - pluginGuid: UUID! - repository: String - requiresAdmin: Boolean! - supportsLinux: Boolean! - supportsOSX: Boolean! - supportsWindows: Boolean! - website: String + entryId: Long! + entry: Entry! + pluginGuid: UUID! + api: Int + minmumVersion: String + website: String + helpPage: String + repository: String + requiresAdmin: Boolean! + supportsWindows: Boolean! + supportsLinux: Boolean! + supportsOSX: Boolean! } "A segment of a collection." type PluginInfosCollectionSegment { - "A flattened list of the items." - items: [PluginInfo!] - "Information to aid in pagination." - pageInfo: CollectionSegmentInfo! - totalCount: Int! + "Information to aid in pagination." + pageInfo: CollectionSegmentInfo! + "A flattened list of the items." + items: [PluginInfo!] + totalCount: Int! @cost(weight: "10") } type Query { - categories(order: [CategorySortInput!], where: CategoryFilterInput): [Category!]! - entries(order: [EntrySortInput!], popular: Boolean, search: String, skip: Int, take: Int, where: EntryFilterInput): EntriesCollectionSegment - entriesV2( - "Returns the elements in the list that come after the specified cursor." - after: String, - "Returns the elements in the list that come before the specified cursor." - before: String, - "Returns the first _n_ elements from the list." - first: Int, - "Returns the last _n_ elements from the list." - last: Int, - order: [EntrySortInput!], - popular: Boolean, - search: String, - where: EntryFilterInput - ): EntriesV2Connection - entry(id: Long!): Entry - pluginInfo(pluginGuid: UUID!): PluginInfo - pluginInfos(order: [PluginInfoSortInput!], skip: Int, take: Int, where: PluginInfoFilterInput): PluginInfosCollectionSegment - popularEntries(where: EntryFilterInput): [Entry!]! - release(id: Long!): Release - searchEntries(input: String!, order: [EntrySortInput!], type: EntryType, where: EntryFilterInput): [Entry!]! - searchKeyboardLayout(deviceProvider: UUID!, logicalLayout: String, model: String!, physicalLayout: KeyboardLayoutType!, vendor: String!): LayoutInfo - searchLayout(deviceProvider: UUID!, deviceType: RGBDeviceType!, model: String!, vendor: String!): LayoutInfo - submittedEntries(order: [EntrySortInput!], where: EntryFilterInput): [Entry!]! + categories( + order: [CategorySortInput!] @cost(weight: "10") + where: CategoryFilterInput @cost(weight: "10") + ): [Category!]! @cost(weight: "10") + entries( + skip: Int + take: Int + search: String + includeDefaults: Boolean + order: [EntrySortInput!] @cost(weight: "10") + where: EntryFilterInput @cost(weight: "10") + ): EntriesCollectionSegment + @listSize( + assumedSize: 100 + slicingArguments: ["take"] + slicingArgumentDefaultValue: 10 + sizedFields: ["items"] + requireOneSlicingArgument: false + ) + @cost(weight: "10") + entriesV2( + search: String + includeDefaults: Boolean + "Returns the first _n_ elements from the list." + first: Int + "Returns the elements in the list that come after the specified cursor." + after: String + "Returns the last _n_ elements from the list." + last: Int + "Returns the elements in the list that come before the specified cursor." + before: String + order: [EntrySortInput!] @cost(weight: "10") + where: EntryFilterInput @cost(weight: "10") + ): EntriesV2Connection + @listSize( + assumedSize: 100 + slicingArguments: ["first", "last"] + slicingArgumentDefaultValue: 10 + sizedFields: ["edges", "nodes"] + requireOneSlicingArgument: false + ) + @cost(weight: "10") + entry(id: Long!): Entry @cost(weight: "10") + submittedEntries( + order: [EntrySortInput!] @cost(weight: "10") + where: EntryFilterInput @cost(weight: "10") + ): [Entry!]! @authorize @cost(weight: "10") + popularEntries(where: EntryFilterInput @cost(weight: "10")): [Entry!]! + @cost(weight: "10") + searchEntries( + input: String! + type: EntryType + order: [EntrySortInput!] @cost(weight: "10") + where: EntryFilterInput @cost(weight: "10") + ): [Entry!]! @cost(weight: "10") + searchLayout( + deviceProvider: UUID! + deviceType: RGBDeviceType! + vendor: String! + model: String! + ): LayoutInfo @cost(weight: "10") + searchKeyboardLayout( + deviceProvider: UUID! + vendor: String! + model: String! + physicalLayout: KeyboardLayoutType! + logicalLayout: String + ): LayoutInfo @cost(weight: "10") + pluginInfos( + skip: Int + take: Int + order: [PluginInfoSortInput!] @cost(weight: "10") + where: PluginInfoFilterInput @cost(weight: "10") + ): PluginInfosCollectionSegment + @listSize( + assumedSize: 100 + slicingArguments: ["take"] + slicingArgumentDefaultValue: 10 + sizedFields: ["items"] + requireOneSlicingArgument: false + ) + @cost(weight: "10") + pluginInfo(pluginGuid: UUID!): PluginInfo @cost(weight: "10") + release(id: Long!): Release @cost(weight: "10") } type Release { - changelog: String - createdAt: DateTime! - dependencies: [Entry!]! - downloadSize: Long! - downloads: Long! - entry: Entry! - entryId: Long! - id: Long! - md5Hash: String - version: String! + id: Long! + version: String! + changelog: String + createdAt: DateTime! + downloads: Long! + downloadSize: Long! + md5Hash: String + entry: Entry! + entryId: Long! + dependencies: [Entry!]! } type Tag { - id: Long! - name: String! + id: Long! + name: String! } +input BooleanOperationFilterInput { + eq: Boolean @cost(weight: "10") + neq: Boolean @cost(weight: "10") +} + +input CategoryFilterInput { + and: [CategoryFilterInput!] + or: [CategoryFilterInput!] + id: LongOperationFilterInput + name: StringOperationFilterInput + icon: StringOperationFilterInput + entryType: NullableOfEntryTypeOperationFilterInput +} + +input CategorySortInput { + id: SortEnumType @cost(weight: "10") + name: SortEnumType @cost(weight: "10") + icon: SortEnumType @cost(weight: "10") + entryType: SortEnumType @cost(weight: "10") +} + +input CreateEntryInput { + entryType: EntryType! + name: String! + summary: String! + description: String! + categories: [Long!]! + tags: [String!]! + defaultEntryInfo: DefaultEntryInfoInput +} + +input CreateLayoutInfoInput { + entryId: Long! + deviceProvider: UUID! + deviceType: RGBDeviceType! + vendor: String! + model: String! + physicalLayout: KeyboardLayoutType + logicalLayout: String +} + +input DateTimeOperationFilterInput { + eq: DateTime @cost(weight: "10") + neq: DateTime @cost(weight: "10") + in: [DateTime] @cost(weight: "10") + nin: [DateTime] @cost(weight: "10") + gt: DateTime @cost(weight: "10") + ngt: DateTime @cost(weight: "10") + gte: DateTime @cost(weight: "10") + ngte: DateTime @cost(weight: "10") + lt: DateTime @cost(weight: "10") + nlt: DateTime @cost(weight: "10") + lte: DateTime @cost(weight: "10") + nlte: DateTime @cost(weight: "10") +} + +input DefaultEntryInfoFilterInput { + and: [DefaultEntryInfoFilterInput!] + or: [DefaultEntryInfoFilterInput!] + entryId: LongOperationFilterInput + isEssential: BooleanOperationFilterInput + isDeviceProvider: BooleanOperationFilterInput +} + +input DefaultEntryInfoInput { + isEssential: Boolean! + isDeviceProvider: Boolean! +} + +input DefaultEntryInfoSortInput { + entryId: SortEnumType @cost(weight: "10") + isEssential: SortEnumType @cost(weight: "10") + isDeviceProvider: SortEnumType @cost(weight: "10") +} + +input EntryFilterInput { + and: [EntryFilterInput!] + or: [EntryFilterInput!] + id: LongOperationFilterInput + entryType: EntryTypeOperationFilterInput + createdAt: DateTimeOperationFilterInput + authorId: UuidOperationFilterInput + author: StringOperationFilterInput + isOfficial: BooleanOperationFilterInput + name: StringOperationFilterInput + summary: StringOperationFilterInput + description: StringOperationFilterInput + downloads: LongOperationFilterInput + iconId: UuidOperationFilterInput + icon: ImageFilterInput + latestReleaseId: LongOperationFilterInput + latestRelease: ReleaseFilterInput + pluginInfo: PluginInfoFilterInput + layoutInfo: ListFilterInputTypeOfLayoutInfoFilterInput + defaultEntryInfo: DefaultEntryInfoFilterInput + categories: ListFilterInputTypeOfCategoryFilterInput + tags: ListFilterInputTypeOfTagFilterInput + images: ListFilterInputTypeOfImageFilterInput + releases: ListFilterInputTypeOfReleaseFilterInput + dependantReleases: ListFilterInputTypeOfReleaseFilterInput +} + +input EntrySortInput { + id: SortEnumType @cost(weight: "10") + entryType: SortEnumType @cost(weight: "10") + createdAt: SortEnumType @cost(weight: "10") + authorId: SortEnumType @cost(weight: "10") + author: SortEnumType @cost(weight: "10") + isOfficial: SortEnumType @cost(weight: "10") + name: SortEnumType @cost(weight: "10") + summary: SortEnumType @cost(weight: "10") + description: SortEnumType @cost(weight: "10") + downloads: SortEnumType @cost(weight: "10") + iconId: SortEnumType @cost(weight: "10") + icon: ImageSortInput @cost(weight: "10") + latestReleaseId: SortEnumType @cost(weight: "10") + latestRelease: ReleaseSortInput @cost(weight: "10") + pluginInfo: PluginInfoSortInput @cost(weight: "10") + defaultEntryInfo: DefaultEntryInfoSortInput @cost(weight: "10") +} + +input EntryTypeOperationFilterInput { + eq: EntryType @cost(weight: "10") + neq: EntryType @cost(weight: "10") + in: [EntryType!] @cost(weight: "10") + nin: [EntryType!] @cost(weight: "10") +} + +input ImageFilterInput { + and: [ImageFilterInput!] + or: [ImageFilterInput!] + id: UuidOperationFilterInput + name: StringOperationFilterInput + description: StringOperationFilterInput + width: IntOperationFilterInput + height: IntOperationFilterInput + size: LongOperationFilterInput + mimeType: StringOperationFilterInput + entry: EntryFilterInput + entryId: LongOperationFilterInput +} + +input ImageSortInput { + id: SortEnumType @cost(weight: "10") + name: SortEnumType @cost(weight: "10") + description: SortEnumType @cost(weight: "10") + width: SortEnumType @cost(weight: "10") + height: SortEnumType @cost(weight: "10") + size: SortEnumType @cost(weight: "10") + mimeType: SortEnumType @cost(weight: "10") + entry: EntrySortInput @cost(weight: "10") + entryId: SortEnumType @cost(weight: "10") +} + +input IntOperationFilterInput { + eq: Int @cost(weight: "10") + neq: Int @cost(weight: "10") + in: [Int] @cost(weight: "10") + nin: [Int] @cost(weight: "10") + gt: Int @cost(weight: "10") + ngt: Int @cost(weight: "10") + gte: Int @cost(weight: "10") + ngte: Int @cost(weight: "10") + lt: Int @cost(weight: "10") + nlt: Int @cost(weight: "10") + lte: Int @cost(weight: "10") + nlte: Int @cost(weight: "10") +} + +input LayoutInfoFilterInput { + and: [LayoutInfoFilterInput!] + or: [LayoutInfoFilterInput!] + id: LongOperationFilterInput + deviceProvider: UuidOperationFilterInput + deviceType: RGBDeviceTypeOperationFilterInput + vendor: StringOperationFilterInput + model: StringOperationFilterInput + physicalLayout: NullableOfKeyboardLayoutTypeOperationFilterInput + logicalLayout: StringOperationFilterInput + entryId: LongOperationFilterInput + entry: EntryFilterInput +} + +input LayoutInfoInput { + deviceProvider: UUID! + deviceType: RGBDeviceType! + vendor: String! + model: String! + physicalLayout: KeyboardLayoutType + logicalLayout: String +} + +input ListFilterInputTypeOfCategoryFilterInput { + all: CategoryFilterInput @cost(weight: "10") + none: CategoryFilterInput @cost(weight: "10") + some: CategoryFilterInput @cost(weight: "10") + any: Boolean @cost(weight: "10") +} + +input ListFilterInputTypeOfEntryFilterInput { + all: EntryFilterInput @cost(weight: "10") + none: EntryFilterInput @cost(weight: "10") + some: EntryFilterInput @cost(weight: "10") + any: Boolean @cost(weight: "10") +} + +input ListFilterInputTypeOfImageFilterInput { + all: ImageFilterInput @cost(weight: "10") + none: ImageFilterInput @cost(weight: "10") + some: ImageFilterInput @cost(weight: "10") + any: Boolean @cost(weight: "10") +} + +input ListFilterInputTypeOfLayoutInfoFilterInput { + all: LayoutInfoFilterInput @cost(weight: "10") + none: LayoutInfoFilterInput @cost(weight: "10") + some: LayoutInfoFilterInput @cost(weight: "10") + any: Boolean @cost(weight: "10") +} + +input ListFilterInputTypeOfReleaseFilterInput { + all: ReleaseFilterInput @cost(weight: "10") + none: ReleaseFilterInput @cost(weight: "10") + some: ReleaseFilterInput @cost(weight: "10") + any: Boolean @cost(weight: "10") +} + +input ListFilterInputTypeOfTagFilterInput { + all: TagFilterInput @cost(weight: "10") + none: TagFilterInput @cost(weight: "10") + some: TagFilterInput @cost(weight: "10") + any: Boolean @cost(weight: "10") +} + +input LongOperationFilterInput { + eq: Long @cost(weight: "10") + neq: Long @cost(weight: "10") + in: [Long] @cost(weight: "10") + nin: [Long] @cost(weight: "10") + gt: Long @cost(weight: "10") + ngt: Long @cost(weight: "10") + gte: Long @cost(weight: "10") + ngte: Long @cost(weight: "10") + lt: Long @cost(weight: "10") + nlt: Long @cost(weight: "10") + lte: Long @cost(weight: "10") + nlte: Long @cost(weight: "10") +} + +input NullableOfEntryTypeOperationFilterInput { + eq: EntryType @cost(weight: "10") + neq: EntryType @cost(weight: "10") + in: [EntryType] @cost(weight: "10") + nin: [EntryType] @cost(weight: "10") +} + +input NullableOfKeyboardLayoutTypeOperationFilterInput { + eq: KeyboardLayoutType @cost(weight: "10") + neq: KeyboardLayoutType @cost(weight: "10") + in: [KeyboardLayoutType] @cost(weight: "10") + nin: [KeyboardLayoutType] @cost(weight: "10") +} + +input PluginInfoFilterInput { + and: [PluginInfoFilterInput!] + or: [PluginInfoFilterInput!] + entryId: LongOperationFilterInput + entry: EntryFilterInput + pluginGuid: UuidOperationFilterInput + api: IntOperationFilterInput + minmumVersion: StringOperationFilterInput + website: StringOperationFilterInput + helpPage: StringOperationFilterInput + repository: StringOperationFilterInput + requiresAdmin: BooleanOperationFilterInput + supportsWindows: BooleanOperationFilterInput + supportsLinux: BooleanOperationFilterInput + supportsOSX: BooleanOperationFilterInput +} + +input PluginInfoSortInput { + entryId: SortEnumType @cost(weight: "10") + entry: EntrySortInput @cost(weight: "10") + pluginGuid: SortEnumType @cost(weight: "10") + api: SortEnumType @cost(weight: "10") + minmumVersion: SortEnumType @cost(weight: "10") + website: SortEnumType @cost(weight: "10") + helpPage: SortEnumType @cost(weight: "10") + repository: SortEnumType @cost(weight: "10") + requiresAdmin: SortEnumType @cost(weight: "10") + supportsWindows: SortEnumType @cost(weight: "10") + supportsLinux: SortEnumType @cost(weight: "10") + supportsOSX: SortEnumType @cost(weight: "10") +} + +input RGBDeviceTypeOperationFilterInput { + eq: RGBDeviceType @cost(weight: "10") + neq: RGBDeviceType @cost(weight: "10") + in: [RGBDeviceType!] @cost(weight: "10") + nin: [RGBDeviceType!] @cost(weight: "10") +} + +input ReleaseFilterInput { + and: [ReleaseFilterInput!] + or: [ReleaseFilterInput!] + id: LongOperationFilterInput + version: StringOperationFilterInput + changelog: StringOperationFilterInput + createdAt: DateTimeOperationFilterInput + downloads: LongOperationFilterInput + downloadSize: LongOperationFilterInput + md5Hash: StringOperationFilterInput + entry: EntryFilterInput + entryId: LongOperationFilterInput + dependencies: ListFilterInputTypeOfEntryFilterInput +} + +input ReleaseSortInput { + id: SortEnumType @cost(weight: "10") + version: SortEnumType @cost(weight: "10") + changelog: SortEnumType @cost(weight: "10") + createdAt: SortEnumType @cost(weight: "10") + downloads: SortEnumType @cost(weight: "10") + downloadSize: SortEnumType @cost(weight: "10") + md5Hash: SortEnumType @cost(weight: "10") + entry: EntrySortInput @cost(weight: "10") + entryId: SortEnumType @cost(weight: "10") +} + +input SetLayoutInfoInput { + entryId: Long! + layoutInfo: [LayoutInfoInput!]! +} + +input StringOperationFilterInput { + and: [StringOperationFilterInput!] + or: [StringOperationFilterInput!] + eq: String @cost(weight: "10") + neq: String @cost(weight: "10") + contains: String @cost(weight: "20") + ncontains: String @cost(weight: "20") + in: [String] @cost(weight: "10") + nin: [String] @cost(weight: "10") + startsWith: String @cost(weight: "20") + nstartsWith: String @cost(weight: "20") + endsWith: String @cost(weight: "20") + nendsWith: String @cost(weight: "20") +} + +input TagFilterInput { + and: [TagFilterInput!] + or: [TagFilterInput!] + id: LongOperationFilterInput + name: StringOperationFilterInput +} + +input UpdateEntryImageInput { + id: UUID! + name: String! + description: String +} + +input UpdateEntryInput { + id: Long! + name: String! + summary: String! + description: String! + categories: [Long!]! + tags: [String!]! + defaultEntryInfo: DefaultEntryInfoInput +} + +input UpdateReleaseInput { + id: Long! + changelog: String +} + +input UuidOperationFilterInput { + eq: UUID @cost(weight: "10") + neq: UUID @cost(weight: "10") + in: [UUID] @cost(weight: "10") + nin: [UUID] @cost(weight: "10") + gt: UUID @cost(weight: "10") + ngt: UUID @cost(weight: "10") + gte: UUID @cost(weight: "10") + ngte: UUID @cost(weight: "10") + lt: UUID @cost(weight: "10") + nlt: UUID @cost(weight: "10") + lte: UUID @cost(weight: "10") + nlte: UUID @cost(weight: "10") +} + +"Defines when a policy shall be executed." enum ApplyPolicy { - AFTER_RESOLVER - BEFORE_RESOLVER - VALIDATION + "Before the resolver was executed." + BEFORE_RESOLVER + "After the resolver was executed." + AFTER_RESOLVER + "The policy is applied in the validation step before the execution." + VALIDATION } enum EntryType { - LAYOUT - PLUGIN - PROFILE + PLUGIN + PROFILE + LAYOUT } enum KeyboardLayoutType { - ABNT - ANSI - ISO - JIS - KS - UNKNOWN + UNKNOWN + ANSI + ISO + JIS + ABNT + KS } enum RGBDeviceType { - ALL - COOLER - DRAM - FAN - GAME_CONTROLLER - GRAPHICS_CARD - HEADSET - HEADSET_STAND - KEYBOARD - KEYPAD - LED_CONTROLLER - LED_MATRIX - LED_STRIPE - MAINBOARD - MONITOR - MOUSE - MOUSEPAD - NONE - SPEAKER - UNKNOWN + NONE + KEYBOARD + MOUSE + HEADSET + MOUSEPAD + LED_STRIPE + LED_MATRIX + MAINBOARD + GRAPHICS_CARD + DRAM + HEADSET_STAND + KEYPAD + FAN + SPEAKER + COOLER + MONITOR + LED_CONTROLLER + GAME_CONTROLLER + UNKNOWN + ALL } enum SortEnumType { - ASC - DESC + ASC + DESC } +"The authorize directive." +directive @authorize( + "The name of the authorization policy that determines access to the annotated resource." + policy: String + "Roles that are allowed to access the annotated resource." + roles: [String!] + "Defines when when the authorize directive shall be applied.By default the authorize directives are applied during the validation phase." + apply: ApplyPolicy! = BEFORE_RESOLVER +) repeatable on OBJECT | FIELD_DEFINITION + +"The purpose of the `cost` directive is to define a `weight` for GraphQL types, fields, and arguments. Static analysis can use these weights when calculating the overall cost of a query or response." +directive @cost( + "The `weight` argument defines what value to add to the overall cost for every appearance, or possible appearance, of a type, field, argument, etc." + weight: String! +) on SCALAR | OBJECT | FIELD_DEFINITION | ARGUMENT_DEFINITION | ENUM | INPUT_FIELD_DEFINITION + +"The purpose of the `@listSize` directive is to either inform the static analysis about the size of returned lists (if that information is statically available), or to point the analysis to where to find that information." +directive @listSize( + "The `assumedSize` argument can be used to statically define the maximum length of a list returned by a field." + assumedSize: Int + "The `slicingArguments` argument can be used to define which of the field's arguments with numeric type are slicing arguments, so that their value determines the size of the list returned by that field. It may specify a list of multiple slicing arguments." + slicingArguments: [String!] + "The `slicingArgumentDefaultValue` argument can be used to define a default value for a slicing argument, which is used if the argument is not present in a query." + slicingArgumentDefaultValue: Int + "The `sizedFields` argument can be used to define that the value of the `assumedSize` argument or of a slicing argument does not affect the size of a list returned by a field itself, but that of a list returned by one of its sub-fields." + sizedFields: [String!] + "The `requireOneSlicingArgument` argument can be used to inform the static analysis that it should expect that exactly one of the defined slicing arguments is present in a query. If that is not the case (i.e., if none or multiple slicing arguments are present), the static analysis may throw an error." + requireOneSlicingArgument: Boolean! = true +) on FIELD_DEFINITION + +"The `@specifiedBy` directive is used within the type system definition language to provide a URL for specifying the behavior of custom scalar definitions." +directive @specifiedBy( + "The specifiedBy URL points to a human-readable specification. This field will only read a result for scalar types." + url: String! +) on SCALAR + "The `DateTime` scalar represents an ISO-8601 compliant date time type." -scalar DateTime +scalar DateTime @specifiedBy(url: "https://www.graphql-scalars.com/date-time") "The `Long` scalar type represents non-fractional signed whole 64-bit numeric values. Long can represent values between -(2^63) and 2^63 - 1." scalar Long -scalar UUID - -input BooleanOperationFilterInput { - eq: Boolean - neq: Boolean -} - -input CategoryFilterInput { - and: [CategoryFilterInput!] - icon: StringOperationFilterInput - id: LongOperationFilterInput - name: StringOperationFilterInput - or: [CategoryFilterInput!] -} - -input CategorySortInput { - icon: SortEnumType - id: SortEnumType - name: SortEnumType -} - -input CreateEntryInput { - categories: [Long!]! - description: String! - entryType: EntryType! - isDefault: Boolean! - name: String! - summary: String! - tags: [String!]! -} - -input CreateLayoutInfoInput { - deviceProvider: UUID! - deviceType: RGBDeviceType! - entryId: Long! - logicalLayout: String - model: String! - physicalLayout: KeyboardLayoutType - vendor: String! -} - -input DateTimeOperationFilterInput { - eq: DateTime - gt: DateTime - gte: DateTime - in: [DateTime] - lt: DateTime - lte: DateTime - neq: DateTime - ngt: DateTime - ngte: DateTime - nin: [DateTime] - nlt: DateTime - nlte: DateTime -} - -input EntryFilterInput { - and: [EntryFilterInput!] - author: StringOperationFilterInput - authorId: UuidOperationFilterInput - categories: ListFilterInputTypeOfCategoryFilterInput - createdAt: DateTimeOperationFilterInput - dependantReleases: ListFilterInputTypeOfReleaseFilterInput - description: StringOperationFilterInput - downloads: LongOperationFilterInput - entryType: EntryTypeOperationFilterInput - icon: ImageFilterInput - iconId: UuidOperationFilterInput - id: LongOperationFilterInput - images: ListFilterInputTypeOfImageFilterInput - isDefault: BooleanOperationFilterInput - isOfficial: BooleanOperationFilterInput - latestRelease: ReleaseFilterInput - latestReleaseId: LongOperationFilterInput - layoutInfo: ListFilterInputTypeOfLayoutInfoFilterInput - name: StringOperationFilterInput - or: [EntryFilterInput!] - pluginInfo: PluginInfoFilterInput - popularityScore: FloatOperationFilterInput - releases: ListFilterInputTypeOfReleaseFilterInput - summary: StringOperationFilterInput - tags: ListFilterInputTypeOfTagFilterInput -} - -input EntrySortInput { - author: SortEnumType - authorId: SortEnumType - createdAt: SortEnumType - description: SortEnumType - downloads: SortEnumType - entryType: SortEnumType - icon: ImageSortInput - iconId: SortEnumType - id: SortEnumType - isDefault: SortEnumType - isOfficial: SortEnumType - latestRelease: ReleaseSortInput - latestReleaseId: SortEnumType - name: SortEnumType - pluginInfo: PluginInfoSortInput - popularityScore: SortEnumType - summary: SortEnumType -} - -input EntryTypeOperationFilterInput { - eq: EntryType - in: [EntryType!] - neq: EntryType - nin: [EntryType!] -} - -input FloatOperationFilterInput { - eq: Float - gt: Float - gte: Float - in: [Float] - lt: Float - lte: Float - neq: Float - ngt: Float - ngte: Float - nin: [Float] - nlt: Float - nlte: Float -} - -input ImageFilterInput { - and: [ImageFilterInput!] - description: StringOperationFilterInput - entry: EntryFilterInput - entryId: LongOperationFilterInput - height: IntOperationFilterInput - id: UuidOperationFilterInput - mimeType: StringOperationFilterInput - name: StringOperationFilterInput - or: [ImageFilterInput!] - size: LongOperationFilterInput - width: IntOperationFilterInput -} - -input ImageSortInput { - description: SortEnumType - entry: EntrySortInput - entryId: SortEnumType - height: SortEnumType - id: SortEnumType - mimeType: SortEnumType - name: SortEnumType - size: SortEnumType - width: SortEnumType -} - -input IntOperationFilterInput { - eq: Int - gt: Int - gte: Int - in: [Int] - lt: Int - lte: Int - neq: Int - ngt: Int - ngte: Int - nin: [Int] - nlt: Int - nlte: Int -} - -input LayoutInfoFilterInput { - and: [LayoutInfoFilterInput!] - deviceProvider: UuidOperationFilterInput - deviceType: RGBDeviceTypeOperationFilterInput - entry: EntryFilterInput - entryId: LongOperationFilterInput - id: LongOperationFilterInput - logicalLayout: StringOperationFilterInput - model: StringOperationFilterInput - or: [LayoutInfoFilterInput!] - physicalLayout: NullableOfKeyboardLayoutTypeOperationFilterInput - vendor: StringOperationFilterInput -} - -input LayoutInfoInput { - deviceProvider: UUID! - deviceType: RGBDeviceType! - logicalLayout: String - model: String! - physicalLayout: KeyboardLayoutType - vendor: String! -} - -input ListFilterInputTypeOfCategoryFilterInput { - all: CategoryFilterInput - any: Boolean - none: CategoryFilterInput - some: CategoryFilterInput -} - -input ListFilterInputTypeOfEntryFilterInput { - all: EntryFilterInput - any: Boolean - none: EntryFilterInput - some: EntryFilterInput -} - -input ListFilterInputTypeOfImageFilterInput { - all: ImageFilterInput - any: Boolean - none: ImageFilterInput - some: ImageFilterInput -} - -input ListFilterInputTypeOfLayoutInfoFilterInput { - all: LayoutInfoFilterInput - any: Boolean - none: LayoutInfoFilterInput - some: LayoutInfoFilterInput -} - -input ListFilterInputTypeOfReleaseFilterInput { - all: ReleaseFilterInput - any: Boolean - none: ReleaseFilterInput - some: ReleaseFilterInput -} - -input ListFilterInputTypeOfTagFilterInput { - all: TagFilterInput - any: Boolean - none: TagFilterInput - some: TagFilterInput -} - -input LongOperationFilterInput { - eq: Long - gt: Long - gte: Long - in: [Long] - lt: Long - lte: Long - neq: Long - ngt: Long - ngte: Long - nin: [Long] - nlt: Long - nlte: Long -} - -input NullableOfKeyboardLayoutTypeOperationFilterInput { - eq: KeyboardLayoutType - in: [KeyboardLayoutType] - neq: KeyboardLayoutType - nin: [KeyboardLayoutType] -} - -input PluginInfoFilterInput { - and: [PluginInfoFilterInput!] - api: IntOperationFilterInput - entry: EntryFilterInput - entryId: LongOperationFilterInput - helpPage: StringOperationFilterInput - minmumVersion: StringOperationFilterInput - or: [PluginInfoFilterInput!] - pluginGuid: UuidOperationFilterInput - repository: StringOperationFilterInput - requiresAdmin: BooleanOperationFilterInput - supportsLinux: BooleanOperationFilterInput - supportsOSX: BooleanOperationFilterInput - supportsWindows: BooleanOperationFilterInput - website: StringOperationFilterInput -} - -input PluginInfoSortInput { - api: SortEnumType - entry: EntrySortInput - entryId: SortEnumType - helpPage: SortEnumType - minmumVersion: SortEnumType - pluginGuid: SortEnumType - repository: SortEnumType - requiresAdmin: SortEnumType - supportsLinux: SortEnumType - supportsOSX: SortEnumType - supportsWindows: SortEnumType - website: SortEnumType -} - -input RGBDeviceTypeOperationFilterInput { - eq: RGBDeviceType - in: [RGBDeviceType!] - neq: RGBDeviceType - nin: [RGBDeviceType!] -} - -input ReleaseFilterInput { - and: [ReleaseFilterInput!] - changelog: StringOperationFilterInput - createdAt: DateTimeOperationFilterInput - dependencies: ListFilterInputTypeOfEntryFilterInput - downloadSize: LongOperationFilterInput - downloads: LongOperationFilterInput - entry: EntryFilterInput - entryId: LongOperationFilterInput - id: LongOperationFilterInput - md5Hash: StringOperationFilterInput - or: [ReleaseFilterInput!] - version: StringOperationFilterInput -} - -input ReleaseSortInput { - changelog: SortEnumType - createdAt: SortEnumType - downloadSize: SortEnumType - downloads: SortEnumType - entry: EntrySortInput - entryId: SortEnumType - id: SortEnumType - md5Hash: SortEnumType - version: SortEnumType -} - -input SetLayoutInfoInput { - entryId: Long! - layoutInfo: [LayoutInfoInput!]! -} - -input StringOperationFilterInput { - and: [StringOperationFilterInput!] - contains: String - endsWith: String - eq: String - in: [String] - ncontains: String - nendsWith: String - neq: String - nin: [String] - nstartsWith: String - or: [StringOperationFilterInput!] - startsWith: String -} - -input TagFilterInput { - and: [TagFilterInput!] - id: LongOperationFilterInput - name: StringOperationFilterInput - or: [TagFilterInput!] -} - -input UpdateEntryImageInput { - description: String - id: UUID! - name: String! -} - -input UpdateEntryInput { - categories: [Long!]! - description: String! - id: Long! - isDefault: Boolean! - name: String! - summary: String! - tags: [String!]! -} - -input UpdateReleaseInput { - changelog: String - id: Long! -} - -input UuidOperationFilterInput { - eq: UUID - gt: UUID - gte: UUID - in: [UUID] - lt: UUID - lte: UUID - neq: UUID - ngt: UUID - ngte: UUID - nin: [UUID] - nlt: UUID - nlte: UUID -} +scalar UUID @specifiedBy(url: "https://tools.ietf.org/html/rfc4122") diff --git a/src/Artemis.sln b/src/Artemis.sln index 7f7a038f0..6ffd58dcf 100644 --- a/src/Artemis.sln +++ b/src/Artemis.sln @@ -27,6 +27,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Artemis.sln.DotSettings.user = Artemis.sln.DotSettings.user Directory.Packages.props = Directory.Packages.props Directory.Build.props = Directory.Build.props + Directory.Build.targets = Directory.Build.targets EndProjectSection EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Artemis.Storage.Legacy", "Artemis.Storage.Legacy\Artemis.Storage.Legacy.csproj", "{D7B0966D-774A-40E4-9455-00C1261ACB6A}" diff --git a/src/Directory.Build.targets b/src/Directory.Build.targets new file mode 100644 index 000000000..36f7bc3fd --- /dev/null +++ b/src/Directory.Build.targets @@ -0,0 +1,5 @@ + + + + + \ No newline at end of file diff --git a/src/Directory.Packages.props b/src/Directory.Packages.props index 0d74712df..9a18d7209 100644 --- a/src/Directory.Packages.props +++ b/src/Directory.Packages.props @@ -66,5 +66,6 @@ + \ No newline at end of file