1
0
mirror of https://github.com/Artemis-RGB/Artemis synced 2025-12-13 05:48:35 +00:00

Merge branch 'feature/default-entries' into development

This commit is contained in:
Robert 2025-12-12 18:29:35 +01:00
commit ce4885f06d
142 changed files with 2517 additions and 1393 deletions

View File

@ -40,7 +40,11 @@ public static class Constants
/// <summary>
/// The full path to the Artemis data folder
/// </summary>
#if DEBUG
public static readonly string DataFolder = Path.Combine(BaseFolder, "Artemis-dev");
#else
public static readonly string DataFolder = Path.Combine(BaseFolder, "Artemis");
#endif
/// <summary>
/// The full path to the Artemis logs folder
@ -140,6 +144,11 @@ public static class Constants
/// Gets the startup arguments provided to the application
/// </summary>
public static ReadOnlyCollection<string> 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")};

View File

@ -11,4 +11,10 @@ public interface IPluginConfigurationDialog
/// The type of view model the tab contains
/// </summary>
Type Type { get; }
/// <summary>
/// A value indicating whether it's mandatory to configure this plugin.
/// <remarks>If set to <see langword="true"/>, the dialog will open the first time the plugin is enabled.</remarks>
/// </summary>
bool IsMandatory { get; }
}

View File

@ -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();

View File

@ -157,10 +157,11 @@ internal class DeviceService : IDeviceService
}
}
/// <param name="leftHanded"></param>
/// <inheritdoc />
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();

View File

@ -46,7 +46,8 @@ public interface IDeviceService : IArtemisService
/// <summary>
/// Applies auto-arranging logic to the surface
/// </summary>
void AutoArrangeDevices();
/// <param name="leftHanded"></param>
void AutoArrangeDevices(bool leftHanded);
/// <summary>
/// Apples the best available to the provided <see cref="ArtemisDevice" />

View File

@ -21,13 +21,12 @@ public interface IPluginManagementService : IArtemisService, IDisposable
/// Indicates whether or not plugins are currently being loaded
/// </summary>
bool LoadingPlugins { get; }
/// <summary>
/// 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
/// </summary>
void CopyBuiltInPlugins();
bool LoadedPlugins { get; }
/// <summary>
/// Loads all installed plugins. If plugins already loaded this will reload them all
/// </summary>
@ -150,12 +149,7 @@ public interface IPluginManagementService : IArtemisService, IDisposable
/// <param name="device"></param>
/// <returns></returns>
DeviceProvider GetDeviceProviderByDevice(IRGBDevice device);
/// <summary>
/// Occurs when built-in plugins are being loaded
/// </summary>
event EventHandler CopyingBuildInPlugins;
/// <summary>
/// Occurs when a plugin has started loading
/// </summary>

View File

@ -46,114 +46,8 @@ internal class PluginManagementService : IPluginManagementService
public List<DirectoryInfo> 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<PluginInfo>(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<Plugin> 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);

View File

@ -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));

View File

@ -32,7 +32,7 @@ internal class SurfaceArrangementConfiguration
public int MarginBottom { get; }
public SurfaceArrangement SurfaceArrangement { get; set; }
public bool Apply(List<ArtemisDevice> devices)
public bool Apply(List<ArtemisDevice> devicesToArrange, List<ArtemisDevice> 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)
{

View File

@ -28,18 +28,18 @@ internal class SurfaceArrangementType
public void Arrange(List<ArtemisDevice> devices)
{
devices = devices.Where(d => d.DeviceType == DeviceType).ToList();
if (!devices.Any())
List<ArtemisDevice> 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;
}

View File

@ -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)
{

View File

@ -1,6 +1,5 @@
<ResourceDictionary xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:ui="using:FluentAvalonia.UI.Controls"
xmlns:tagsInput="clr-namespace:Artemis.UI.Shared.TagsInput"
xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
x:CompileBindings="True">

View File

@ -6,6 +6,22 @@ namespace Artemis.UI.Shared;
/// <inheritdoc />
public class PluginConfigurationDialog<T> : PluginConfigurationDialog where T : PluginConfigurationViewModel
{
/// <summary>
/// Creates a new instance of the <see cref="PluginConfigurationDialog{T}"/> class.
/// </summary>
public PluginConfigurationDialog()
{
}
/// <summary>
/// Creates a new instance of the <see cref="PluginConfigurationDialog{T}"/> class with the specified <paramref name="isMandatory"/> flag.
/// </summary>
/// <param name="isMandatory">A value indicating whether the configuration dialog is mandatory.</param>
public PluginConfigurationDialog(bool isMandatory)
{
IsMandatory = isMandatory;
}
/// <inheritdoc />
public override Type Type => typeof(T);
}
@ -17,4 +33,7 @@ public abstract class PluginConfigurationDialog : IPluginConfigurationDialog
{
/// <inheritdoc />
public abstract Type Type { get; }
/// <inheritdoc />
public bool IsMandatory { get; protected set; }
}

View File

@ -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)

View File

@ -1,4 +1,4 @@
using System;
using System;
using System.Threading.Tasks;
using System.Windows.Input;
using Avalonia.Controls;

View File

@ -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);

View File

@ -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();

View File

@ -1,6 +1,5 @@
<Styles xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia">
<Design.PreviewWith>
<HyperlinkButton Grid.Column="0" Classes="icon-button icon-button-small broken-state-button" Margin="50">

View File

@ -1,7 +1,6 @@
<Styles xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia">
xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia">
<!-- Preview -->
<Design.PreviewWith>
<Border Padding="20">

View File

@ -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">
<Design.PreviewWith>
<dataModelPicker:DataModelPicker />

View File

@ -1,6 +1,5 @@
<Styles xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:controls="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
xmlns:dataModelPicker="clr-namespace:Artemis.UI.Shared.DataModelPicker"
xmlns:gradientPicker="clr-namespace:Artemis.UI.Shared.Controls.GradientPicker">
<Design.PreviewWith>

View File

@ -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();

View File

@ -9,6 +9,7 @@
<AssemblyTitle>Artemis</AssemblyTitle>
<ApplicationIcon>..\Artemis.UI\Assets\Images\Logo\application.ico</ApplicationIcon>
<SatelliteResourceLanguages>en</SatelliteResourceLanguages>
<HotAvaloniaAutoEnable>false</HotAvaloniaAutoEnable>
</PropertyGroup>
<ItemGroup>
<AvaloniaResource Include="Assets\**" />

View File

@ -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<App>()
.UsePlatformDetect()
.LogToTrace()
.UseReactiveUI();
return AppBuilder.Configure<App>().UsePlatformDetect().LogToTrace().UseReactiveUI();
}
public static void CreateLogger(IContainer container)

View File

@ -37,38 +37,13 @@
</ItemGroup>
<ItemGroup>
<Compile Update="Screens\Workshop\Plugin\Dialogs\DeviceProviderPickerDialogView.axaml.cs">
<DependentUpon>DeviceProviderPickerDialogView.axaml</DependentUpon>
<SubType>Code</SubType>
</Compile>
<Compile Update="Screens\Workshop\Plugin\Dialogs\DeviceSelectionDialogView.axaml.cs">
<DependentUpon>DeviceSelectionDialogView.axaml</DependentUpon>
<SubType>Code</SubType>
</Compile>
<Compile Update="Screens\Workshop\Layout\LayoutListView.axaml.cs">
<DependentUpon>LayoutListView.axaml</DependentUpon>
<SubType>Code</SubType>
</Compile>
<Compile Update="Screens\Workshop\Plugins\PluginListView.axaml.cs">
<DependentUpon>LayoutListView.axaml</DependentUpon>
<SubType>Code</SubType>
</Compile>
<Compile Update="Screens\Workshop\Profile\ProfileListView.axaml.cs">
<DependentUpon>LayoutListView.axaml</DependentUpon>
<SubType>Code</SubType>
</Compile>
<Compile Update="Screens\Workshop\EntryReleases\EntryReleasesView.axaml.cs">
<DependentUpon>EntryReleasesView.axaml</DependentUpon>
<Compile Update="Screens\StartupWizard\Steps\WorkshopUnreachableStepView.axaml.cs">
<DependentUpon>WorkshopUnreachableStepView.axaml</DependentUpon>
<SubType>Code</SubType>
</Compile>
</ItemGroup>
<ItemGroup>
<UpToDateCheckInput Remove="Screens\Workshop\Entries\Tabs\PluginListView.axaml" />
<UpToDateCheckInput Remove="Screens\Workshop\Entries\Tabs\ProfileListView.axaml" />
<UpToDateCheckInput Remove="Screens\Workshop\Plugins\Dialogs\PluginDialogView.axaml" />
<UpToDateCheckInput Remove="Screens\Scripting\Dialogs\ScriptConfigurationCreateView.axaml" />
<UpToDateCheckInput Remove="Screens\Scripting\Dialogs\ScriptConfigurationEditView.axaml" />
<UpToDateCheckInput Remove="Screens\Scripting\ScriptsDialogView.axaml" />
<Folder Include="Screens\Profiles\" />
</ItemGroup>
</Project>

View File

@ -1,5 +1,4 @@
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib"
xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<wpf:ResourceDictionary xml:space="preserve" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:s="clr-namespace:System;assembly=mscorlib" xmlns:wpf="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=events/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=screens_005Cdebugger_005Ctabs/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=screens_005Cdevice_005Ctabs/@EntryIndexedValue">True</s:Boolean>
@ -7,6 +6,8 @@
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=screens_005Cprofileeditor_005Cpanels/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=screens_005Cprofileeditor_005Cpanels_005Cproperties_005Ctimeline_005Ckeyframes/@EntryIndexedValue">False</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=screens_005Cprofileeditor_005Ctools/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=screens_005Cprofile_005Cprofileeditor_005Cpanels_005Cproperties_005Ctimeline_005Ckeyframes/@EntryIndexedValue">False</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=screens_005Cprofile_005Cprofileeditor_005Cpanels/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=screens_005Csettings_005Ctabs/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=screens_005Csidebar_005Ccontentdialogs/@EntryIndexedValue">True</s:Boolean>
<s:Boolean x:Key="/Default/CodeInspection/NamespaceProvider/NamespaceFoldersToSkip/=screens_005Csidebar_005Cdialogs/@EntryIndexedValue">True</s:Boolean></wpf:ResourceDictionary>

File diff suppressed because one or more lines are too long

View File

@ -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">

View File

@ -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;

View File

@ -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;

View File

@ -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());
}
}
}

View File

@ -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<AccountTabViewModel>("account"),
new RouteRegistration<AboutTabViewModel>("about")
]),
new RouteRegistration<ProfileEditorViewModel>("profile-editor/{profileConfigurationId:guid}")
new RouteRegistration<ProfileViewModel>("profile/{profileConfigurationId:guid}", [
new RouteRegistration<ProfileEditorViewModel>("editor"),
new RouteRegistration<WorkshopProfileViewModel>("workshop")
]),
];
}
}

View File

@ -21,11 +21,14 @@ public partial class DebugView : ReactiveAppWindow<DebugViewModel>
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);
});
}

View File

@ -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">

View File

@ -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"

View File

@ -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"

View File

@ -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"

View File

@ -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"

View File

@ -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"

View File

@ -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.">
<TextBlock.Effect>
<DropShadowEffect Color="Black" OffsetX="2" OffsetY="2" BlurRadius="5"></DropShadowEffect>
</TextBlock.Effect>

View File

@ -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">
<UserControl.Styles>
<Styles>
<Style Selector="Border.status-border">
@ -24,7 +25,7 @@
</Style>
</Styles>
</UserControl.Styles>
<Grid ColumnDefinitions="350,*" Width="800">
<Grid ColumnDefinitions="350,*">
<Grid.RowDefinitions>
<RowDefinition MinHeight="200" />
</Grid.RowDefinitions>

View File

@ -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">
<UserControl.Styles>
<Styles>
<Style Selector="Border.status-border">
@ -24,7 +25,7 @@
</Style>
</Styles>
</UserControl.Styles>
<Grid ColumnDefinitions="350,*" Width="800">
<Grid ColumnDefinitions="350,*">
<Grid.RowDefinitions>
<RowDefinition MinHeight="200" />
</Grid.RowDefinitions>

View File

@ -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"

View File

@ -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"

View File

@ -19,14 +19,11 @@ public partial class PluginSettingsWindowView : ReactiveAppWindow<PluginSettings
this.WhenActivated(disposables =>
{
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);
}
);
}
}

View File

@ -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"

View File

@ -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<Unit, Unit>? reload,
ICoreService coreService,
IWindowService windowService,
INotificationService notificationService,
IPluginManagementService pluginManagementService)
public PluginViewModel(Plugin plugin, ReactiveCommand<Unit, Unit>? reload, IWindowService windowService, IPluginInteractionService pluginInteractionService)
{
_plugin = plugin;
_coreService = coreService;
_windowService = windowService;
_notificationService = notificationService;
_pluginManagementService = pluginManagementService;
_pluginInteractionService = pluginInteractionService;
Platforms = new ObservableCollection<PluginPlatformViewModel>();
if (Plugin.Info.Platforms != null)
@ -88,7 +79,6 @@ public partial class PluginViewModel : ActivatableViewModelBase
public ReactiveCommand<Unit, Unit> OpenPluginDirectory { get; }
public ObservableCollection<PluginPlatformViewModel> 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<IPrerequisitesSubject> 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;

View File

@ -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"

View File

@ -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"

View File

@ -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();

View File

@ -89,7 +89,7 @@
Background="{DynamicResource ControlFillColorDefaultBrush}"
IsVisible="{CompiledBinding ProfileConfiguration, Converter={x:Static ObjectConverters.IsNotNull}}">
<StackPanel Orientation="Horizontal" Margin="8">
<shared:ProfileConfigurationIcon ConfigurationIcon="{CompiledBinding ProfileConfiguration.Icon}" Width="18" Height="18" Margin="0 0 5 0" />
<shared:ProfileConfigurationIcon ConfigurationIcon="{CompiledBinding ProfileConfiguration.Icon}" Width="18" Height="18" CornerRadius="3" Margin="0 0 5 0" />
<TextBlock Text="{CompiledBinding ProfileConfiguration.Name}" />
</StackPanel>
</Border>

View File

@ -27,8 +27,9 @@ public partial class VisualEditorView : ReactiveUserControl<VisualEditorViewMode
this.WhenActivated(d =>
{
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));

View File

@ -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<ProfileEditorViewModelParameters>, IMainScreenViewModel
public partial class ProfileEditorViewModel : RoutableScreen<ProfileViewModelParameters>, 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<IToolViewModel> _tools;
private ObservableAsPropertyHelper<ProfileEditorHistory?>? _history;
private ObservableAsPropertyHelper<bool>? _suspendedEditing;
@ -53,16 +48,12 @@ public partial class ProfileEditorViewModel : RoutableScreen<ProfileEditorViewMo
StatusBarViewModel statusBarViewModel,
IEnumerable<IToolViewModel> 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<IToolViewModel>();
_tools.AddRange(toolViewModels);
@ -75,6 +66,7 @@ public partial class ProfileEditorViewModel : RoutableScreen<ProfileEditorViewMo
Tools = tools;
visualEditorViewModel.SetTools(_tools);
ParameterSource = ParameterSource.Route;
StatusBarViewModel = statusBarViewModel;
VisualEditorViewModel = visualEditorViewModel;
ProfileTreeViewModel = profileTreeViewModel;
@ -193,7 +185,7 @@ public partial class ProfileEditorViewModel : RoutableScreen<ProfileEditorViewMo
#region Overrides of RoutableScreen<object,ProfileEditorViewModelParameters>
/// <inheritdoc />
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<ProfileEditorViewMo
return;
}
// If the profile is from the workshop, warn the user that auto-updates will be disabled
InstalledEntry? workshopEntry = _workshopService.GetInstalledEntryByProfile(profileConfiguration);
if (workshopEntry != null && workshopEntry.AutoUpdate)
{
bool confirmed = await _windowService.ShowConfirmContentDialog(
"Editing a workshop profile",
"You are about to edit a profile from the workshop, to preserve your changes auto-updating will be disabled.",
"Disable auto-update");
if (confirmed)
_workshopService.SetAutoUpdate(workshopEntry, false);
else
{
args.Cancel();
return;
}
}
await _profileEditorService.ChangeCurrentProfileConfiguration(profileConfiguration);
ProfileConfiguration = profileConfiguration;
}
@ -236,9 +211,4 @@ public partial class ProfileEditorViewModel : RoutableScreen<ProfileEditorViewMo
}
#endregion
}
public class ProfileEditorViewModelParameters
{
public Guid ProfileId { get; set; }
}

View File

@ -0,0 +1,14 @@
<UserControl xmlns="https://github.com/avaloniaui"
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:ui="clr-namespace:Artemis.UI"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Artemis.UI.Screens.ProfileEditor.ProfileView">
<controls:Frame Name="RouterFrame" IsNavigationStackEnabled="False" CacheSize="0">
<controls:Frame.NavigationPageFactory>
<ui:PageFactory/>
</controls:Frame.NavigationPageFactory>
</controls:Frame>
</UserControl>

View File

@ -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<ProfileViewModel>
{
public ProfileView()
{
InitializeComponent();
this.WhenActivated(d => ViewModel.WhenAnyValue(vm => vm.Screen)
.WhereNotNull()
.Subscribe(screen => RouterFrame.NavigateFromObject(screen))
.DisposeWith(d));
}
}

View File

@ -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<RoutableScreen, ProfileViewModelParameters>, IMainScreenViewModel
{
private readonly IProfileService _profileService;
private readonly IWorkshopService _workshopService;
private readonly ObservableAsPropertyHelper<ViewModelBase?> _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;
/// <inheritdoc />
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; }
}

View File

@ -0,0 +1,23 @@
<UserControl xmlns="https://github.com/avaloniaui"
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:profileEditor="clr-namespace:Artemis.UI.Screens.ProfileEditor"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Artemis.UI.Screens.ProfileEditor.WorkshopProfileView"
x:DataType="profileEditor:WorkshopProfileViewModel">
<Border Classes="router-container">
<StackPanel HorizontalAlignment="Center" VerticalAlignment="Center" Orientation="Vertical" Spacing="10" MaxWidth="800">
<ContentControl Content="{CompiledBinding EntryViewModel}"></ContentControl>
<TextBlock TextWrapping="Wrap">
The profile you're opening is a workshop profile.
</TextBlock>
<TextBlock TextWrapping="Wrap">
You cannot make change to it without disabling auto-update, as any updates to the profile on the workshop would override your own modifications.
</TextBlock>
<Button Command="{CompiledBinding DisableAutoUpdate}" Classes="accent">Disable auto-update</Button>
</StackPanel>
</Border>
</UserControl>

View File

@ -0,0 +1,11 @@
using Avalonia.ReactiveUI;
namespace Artemis.UI.Screens.ProfileEditor;
public partial class WorkshopProfileView : ReactiveUserControl<WorkshopProfileViewModel>
{
public WorkshopProfileView()
{
InitializeComponent();
}
}

View File

@ -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<ProfileViewModelParameters>
{
private readonly IProfileService _profileService;
private readonly IWorkshopService _workshopService;
private readonly IRouter _router;
private readonly Func<InstalledEntry, InstalledTabItemViewModel> _getInstalledTabItemViewModel;
[Notify] private ProfileConfiguration? _profileConfiguration;
[Notify] private InstalledEntry? _workshopEntry;
[Notify] private InstalledTabItemViewModel? _entryViewModel;
public WorkshopProfileViewModel(IProfileService profileService, IWorkshopService workshopService, IRouter router, Func<InstalledEntry, InstalledTabItemViewModel> 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;
}
}

View File

@ -83,7 +83,7 @@ public class RootViewModel : RoutableHostScreen<RoutableScreen>, IMainWindowProv
_coreService.Initialized += (_, _) => Dispatcher.UIThread.InvokeAsync(OpenMainWindow);
}
Task.Run(() =>
Task.Run(async () =>
{
try
{
@ -93,7 +93,7 @@ public class RootViewModel : RoutableHostScreen<RoutableScreen>, 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();

View File

@ -19,7 +19,8 @@ public partial class SplashView : ReactiveWindow<SplashViewModel>
#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);
});

View File

@ -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";
}
}

View File

@ -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"

View File

@ -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"

View File

@ -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"

View File

@ -187,7 +187,7 @@ public partial class ProfileConfigurationEditViewModel : DialogViewModelBase<Pro
if (result == null)
return;
SelectedBitmapSource = BitmapExtensions.LoadAndResize(result[0], 128);
SelectedBitmapSource = BitmapExtensions.LoadAndResize(result[0], 128, false);
_selectedIconPath = result[0];
}

View File

@ -65,7 +65,7 @@ public partial class SidebarCategoryViewModel : ActivatableViewModelBase
// Navigate on selection change
this.WhenAnyValue(vm => 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);

View File

@ -77,7 +77,8 @@
VerticalAlignment="Center"
ConfigurationIcon="{CompiledBinding ProfileConfiguration.Icon}"
Width="22"
Height="22"
Height="22"
CornerRadius="4"
Margin="0 0 5 0">
<shared:ProfileConfigurationIcon.Transitions>
<Transitions>

View File

@ -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}");
}
}

View File

@ -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"

View File

@ -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">
<Grid Margin="15" RowDefinitions="Auto,*,Auto" ColumnDefinitions="Auto,*">
<Grid Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="2" RowDefinitions="*,*" ColumnDefinitions="Auto,*,Auto">
<Grid Margin="15" RowDefinitions="Auto,*,Auto">
<!-- Header -->
<Grid Grid.Row="0" RowDefinitions="*,*" ColumnDefinitions="Auto,*,Auto">
<Image Grid.Column="0" Grid.RowSpan="2" Width="65" Height="65" VerticalAlignment="Center" Source="/Assets/Images/Logo/bow.png" Margin="0 0 20 0" />
<TextBlock Grid.Row="0" Grid.Column="1" FontSize="36" VerticalAlignment="Bottom">
Artemis 2
</TextBlock>
<StackPanel Grid.Row="0" Grid.Column="2" HorizontalAlignment="Right" VerticalAlignment="Bottom" Orientation="Horizontal">
<HyperlinkButton Classes="icon-button" ToolTip.Tip="View website" NavigateUri="https://artemis-rgb.com?mtm_campaign=artemis&amp;mtm_kwd=wizard">
<avalonia:MaterialIcon Kind="Web" />
@ -32,13 +32,11 @@
<avalonia:MaterialIcon Kind="BookOpenOutline" />
</HyperlinkButton>
</StackPanel>
<TextBlock Grid.Row="1"
Grid.Column="1"
VerticalAlignment="Top"
Classes="subtitle"
Text="{CompiledBinding Version}" />
<HyperlinkButton Grid.Row="1"
Grid.Column="2"
VerticalAlignment="Top"
@ -46,19 +44,19 @@
PolyForm Noncommercial License 1.0.0
</HyperlinkButton>
</Grid>
<controls:Frame Grid.Row="1" Grid.Column="0" Grid.ColumnSpan="2" Name="Frame" IsNavigationStackEnabled="False" CacheSize="0">
<!-- Main Content -->
<controls:Frame Grid.Row="1" Name="Frame" IsNavigationStackEnabled="False" CacheSize="0">
<controls:Frame.NavigationPageFactory>
<ui:PageFactory/>
</controls:Frame.NavigationPageFactory>
</controls:Frame>
<Button Grid.Row="2" Grid.Column="0" Command="{CompiledBinding SkipOrFinishWizard}" IsVisible="{CompiledBinding !Screen.ShowFinish}">Skip &amp; close</Button>
<StackPanel Grid.Row="2" Grid.Column="1" HorizontalAlignment="Right" Orientation="Horizontal" Spacing="5" Margin="0 15 0 0">
<Button Command="{CompiledBinding Screen.GoBack}" IsEnabled="{CompiledBinding Screen.ShowGoBack}">Back</Button>
<Button Command="{CompiledBinding Screen.Continue}" IsVisible="{CompiledBinding !Screen.ShowFinish}" Width="80">Continue</Button>
<Button Command="{CompiledBinding SkipOrFinishWizard}" IsVisible="{CompiledBinding Screen.ShowFinish}" Width="80">Finish</Button>
</StackPanel>
<!-- Buttons Panel -->
<DockPanel Grid.Row="2" LastChildFill="False" IsVisible="{CompiledBinding !Screen.HideAllButtons}" HorizontalSpacing="10">
<Button Command="{CompiledBinding SkipOrFinishWizard}" IsVisible="{CompiledBinding !Screen.ShowFinish}" DockPanel.Dock="Left">Skip &amp; close</Button>
<Button Command="{CompiledBinding Screen.Continue}" IsVisible="{CompiledBinding !Screen.ShowFinish}" Width="80" DockPanel.Dock="Right">Continue</Button>
<Button Command="{CompiledBinding SkipOrFinishWizard}" IsVisible="{CompiledBinding Screen.ShowFinish}" Width="80" DockPanel.Dock="Right">Finish</Button>
<Button Command="{CompiledBinding Screen.GoBack}" IsEnabled="{CompiledBinding Screen.ShowGoBack}" DockPanel.Dock="Right">Back</Button>
</DockPanel>
</Grid>
</Window>

View File

@ -0,0 +1,77 @@
<UserControl xmlns="https://github.com/avaloniaui"
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:steps="clr-namespace:Artemis.UI.Screens.StartupWizard.Steps"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Artemis.UI.Screens.StartupWizard.Steps.DefaultEntriesStepView"
x:DataType="steps:DefaultEntriesStepViewModel">
<Panel>
<!-- Selection stage -->
<Grid RowDefinitions="Auto,*,Auto" IsVisible="{CompiledBinding CurrentEntry, Converter={x:Static ObjectConverters.IsNull}}">
<TextBlock Grid.Row="0" TextWrapping="Wrap" IsVisible="{CompiledBinding !FetchingDefaultEntries}">
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.
</TextBlock>
<StackPanel Grid.Row="1" IsVisible="{CompiledBinding FetchingDefaultEntries}" HorizontalAlignment="Center" VerticalAlignment="Center" Spacing="10">
<ProgressBar IsIndeterminate="True" Width="400" />
<TextBlock TextAlignment="Center">Fetching default features from the Artemis workshop...</TextBlock>
</StackPanel>
<ScrollViewer Grid.Row="1" HorizontalScrollBarVisibility="Disabled" VerticalScrollBarVisibility="Auto">
<StackPanel>
<TextBlock Classes="card-title" IsVisible="{CompiledBinding DeviceProviderEntryViewModels.Count}">
Device providers
</TextBlock>
<ItemsControl ItemsSource="{CompiledBinding DeviceProviderEntryViewModels}" Margin="0,0,5,0">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<UniformGrid Columns="2" ColumnSpacing="5" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
<TextBlock Classes="card-title" IsVisible="{CompiledBinding EssentialEntryViewModels.Count}">
Essentials
</TextBlock>
<ItemsControl ItemsSource="{CompiledBinding EssentialEntryViewModels}" Margin="0,0,5,0">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<UniformGrid Columns="2" ColumnSpacing="5" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
<TextBlock Classes="card-title" IsVisible="{CompiledBinding OtherEntryViewModels.Count}">
Other features
</TextBlock>
<ItemsControl ItemsSource="{CompiledBinding OtherEntryViewModels}" Margin="0,0,5,0">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<UniformGrid Columns="2" ColumnSpacing="5" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
</StackPanel>
</ScrollViewer>
</Grid>
<!-- Installation stage -->
<StackPanel Width="400" Margin="100 0 0 0" IsVisible="{CompiledBinding CurrentEntry, Converter={x:Static ObjectConverters.IsNotNull}}">
<Lottie Path="/Assets/Animations/nyan.json" RepeatCount="-1" Width="300" Height="300"></Lottie>
<ProgressBar Minimum="0"
Maximum="{CompiledBinding TotalEntries}"
Value="{CompiledBinding CurrentEntry.InstallProgress, FallbackValue=0}"
Margin="0 0 0 5" />
<TextBlock>
<Run>Currently installing: </Run>
<Run Text="{CompiledBinding CurrentEntry.Entry.Name}" FontWeight="Bold" />
</TextBlock>
<ProgressBar Minimum="0" Maximum="{CompiledBinding TotalEntries}" Value="{CompiledBinding InstalledEntries}" Margin="0 20 0 5" />
<TextBlock>
<Run Text="{CompiledBinding RemainingEntries}" FontWeight="Bold" />
<Run>feature(s) remaining, hold on tight!</Run>
</TextBlock>
</StackPanel>
</Panel>
</UserControl>

View File

@ -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<DefaultEntriesStepViewModel>
{
public DefaultEntriesStepView()
{
InitializeComponent();
}
}

View File

@ -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<IEntrySummary, DefaultEntryItemViewModel> _getDefaultEntryItemViewModel;
private readonly ObservableAsPropertyHelper<int> _remainingEntries;
public ObservableCollection<DefaultEntryItemViewModel> DeviceProviderEntryViewModels { get; } = [];
public ObservableCollection<DefaultEntryItemViewModel> EssentialEntryViewModels { get; } = [];
public ObservableCollection<DefaultEntryItemViewModel> OtherEntryViewModels { get; } = [];
public int RemainingEntries => _remainingEntries.Value;
public DefaultEntriesStepViewModel(IWorkshopService workshopService, IDeviceService deviceService, IWorkshopClient client,
Func<IEntrySummary, DefaultEntryItemViewModel> 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<WelcomeStepViewModel>());
this.WhenActivatedAsync(async d =>
{
CancellationToken ct = d.AsCancellationToken();
if (await workshopService.ValidateWorkshopStatus(false, ct))
await GetDefaultEntries(d.AsCancellationToken());
else if (!ct.IsCancellationRequested)
Wizard.ChangeScreen<WorkshopUnreachableStepViewModel>();
});
}
private void ExecuteContinue()
{
// Without devices skip to the last step
if (_deviceService.EnabledDevices.Count == 0)
Wizard.ChangeScreen<SettingsStepViewModel>();
else
Wizard.ChangeScreen<LayoutsStepViewModel>();
}
private async Task Install(CancellationToken cancellationToken)
{
List<DefaultEntryItemViewModel> 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<IGetDefaultEntriesResult> result = await _client.GetDefaultEntries.ExecuteAsync(100, null, cancellationToken);
List<IEntrySummary> entries = result.Data?.EntriesV2?.Edges?.Select(e => e.Node).Cast<IEntrySummary>().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;
}
}

View File

@ -0,0 +1,71 @@
<UserControl xmlns="https://github.com/avaloniaui"
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:il="clr-namespace:AsyncImageLoader;assembly=AsyncImageLoader.Avalonia"
xmlns:converters="clr-namespace:Artemis.UI.Converters"
xmlns:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
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.DefaultEntryItemView"
x:DataType="steps:DefaultEntryItemViewModel">
<UserControl.Resources>
<converters:EntryIconUriConverter x:Key="EntryIconUriConverter" />
</UserControl.Resources>
<Border Classes="card" Padding="15" Margin="0 5" PointerPressed="HandlePointerPressed">
<Grid ColumnDefinitions="Auto,*,Auto,Auto" RowDefinitions="*, Auto">
<!-- Icon -->
<Border Grid.Column="0"
Grid.Row="0"
Grid.RowSpan="2"
CornerRadius="6"
VerticalAlignment="Center"
Margin="0 0 10 0"
Width="80"
Height="80"
ClipToBounds="True">
<Image Stretch="UniformToFill" il:ImageLoader.Source="{CompiledBinding Entry.Id, Converter={StaticResource EntryIconUriConverter}, Mode=OneWay}" />
</Border>
<!-- Body -->
<Grid Grid.Column="1" Grid.Row="0" Grid.RowSpan="2" VerticalAlignment="Stretch" RowDefinitions="Auto,*,Auto">
<StackPanel Grid.Row="0" Orientation="Horizontal">
<TextBlock Classes="h5" Margin="0 0 0 5" TextTrimming="CharacterEllipsis" Text="{CompiledBinding Entry.Name, FallbackValue=Title}" />
</StackPanel>
<TextBlock Grid.Row="1"
Classes="subtitle"
TextWrapping="Wrap"
TextTrimming="CharacterEllipsis"
Text="{CompiledBinding Entry.Summary, FallbackValue=Summary}" />
<ItemsControl Grid.Row="2" ItemsSource="{CompiledBinding Entry.Categories}" Margin="0 8 0 0">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal" Spacing="8" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<StackPanel Orientation="Horizontal">
<avalonia:MaterialIcon Kind="{CompiledBinding Icon}" Margin="0 0 3 0" />
<TextBlock Text="{CompiledBinding Name}" TextTrimming="CharacterEllipsis" />
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Grid>
<!-- Management -->
<Border Grid.Column="3" Grid.Row="0" Grid.RowSpan="2" BorderBrush="{DynamicResource ButtonBorderBrush}" BorderThickness="1 0 0 0" Margin="10 0 0 0" Padding="10 0 0 0" IsVisible="{CompiledBinding !IsInstalled}">
<StackPanel VerticalAlignment="Center">
<CheckBox MinHeight="26" Margin="0 4 0 0" IsChecked="{CompiledBinding ShouldInstall}">Install</CheckBox>
</StackPanel>
</Border>
<Border Grid.Column="3" Grid.Row="0" Grid.RowSpan="2" BorderBrush="{DynamicResource ButtonBorderBrush}" BorderThickness="1 0 0 0" Margin="10 0 0 0" Padding="10 0 0 0" IsVisible="{CompiledBinding IsInstalled}">
<StackPanel VerticalAlignment="Center">
<TextBlock>Already installed</TextBlock>
</StackPanel>
</Border>
</Grid>
</Border>
</UserControl>

View File

@ -0,0 +1,20 @@
using Avalonia.Input;
using Avalonia.ReactiveUI;
namespace Artemis.UI.Screens.StartupWizard.Steps;
public partial class DefaultEntryItemView : ReactiveUserControl<StartupWizard.Steps.DefaultEntryItemViewModel>
{
public DefaultEntryItemView()
{
InitializeComponent();
}
private void HandlePointerPressed(object? sender, PointerPressedEventArgs e)
{
if (ViewModel != null && !ViewModel.IsInstalled)
{
ViewModel.ShouldInstall = !ViewModel.ShouldInstall;
}
}
}

View File

@ -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<StreamProgress> _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<bool> 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);
}
}

View File

@ -1,43 +0,0 @@
<UserControl xmlns="https://github.com/avaloniaui"
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:steps="clr-namespace:Artemis.UI.Screens.StartupWizard.Steps"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:Class="Artemis.UI.Screens.StartupWizard.Steps.DevicesStepView"
x:DataType="steps:DevicesStepViewModel">
<Border Classes="card">
<Grid RowDefinitions="Auto,*,Auto,Auto">
<StackPanel Grid.Row="0">
<TextBlock TextWrapping="Wrap">
Devices are supported through the use of device providers.
</TextBlock>
<TextBlock TextWrapping="Wrap">
In the list below you can enable device providers for each brand you own by checking "Enable feature".
</TextBlock>
</StackPanel>
<ScrollViewer Grid.Row="1" Margin="0 15">
<ItemsControl ItemsSource="{CompiledBinding DeviceProviders}" Margin="-4 0 8 0">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<UniformGrid Columns="2" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate>
<Border Classes="card-condensed" Margin="4">
<ContentControl Content="{CompiledBinding}"></ContentControl>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
<TextBlock Grid.Row="2" Foreground="#FFB9A40A" TextWrapping="Wrap">
Note: To avoid possible instability it's recommended to disable the device providers of brands you don't own.
</TextBlock>
</Grid>
</Border>
</UserControl>

View File

@ -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<PluginFeatureInfo, WizardPluginFeatureViewModel> getPluginFeatureViewModel, IDeviceService deviceService)
{
// Take all compatible device providers and create a view model for them
DeviceProviders = new ObservableCollection<WizardPluginFeatureViewModel>(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<SettingsStepViewModel>();
else
Wizard.ChangeScreen<LayoutsStepViewModel>();
});
GoBack = ReactiveCommand.Create(() => Wizard.ChangeScreen<WelcomeStepViewModel>());
}
public ObservableCollection<WizardPluginFeatureViewModel> DeviceProviders { get; }
}

View File

@ -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">
<Border Classes="card" VerticalAlignment="Top">
<StackPanel>
<TextBlock Classes="h4">All finished!</TextBlock>
<StackPanel>
<TextBlock Classes="h4">All finished!</TextBlock>
<Border Classes="card" VerticalAlignment="Top">
<StackPanel>
<TextBlock TextWrapping="Wrap">
You are now ready to start using Artemis, enjoy! 😁
</TextBlock>
<TextBlock TextWrapping="Wrap">
To learn more about Artemis and how to use it you may find these resources useful:
</TextBlock>
<TextBlock TextWrapping="Wrap">
You are now ready to start using Artemis, enjoy! 😁
</TextBlock>
<TextBlock TextWrapping="Wrap">
To learn more about Artemis and how to use it you may find these resources useful:
</TextBlock>
<Grid ColumnDefinitions="Auto,*" Margin="0 15 0 0">
<Grid.Styles>
<Style Selector="TextBlock.link-name">
<Setter Property="Margin" Value="0 7 15 6" />
<Setter Property="FontWeight" Value="600" />
</Style>
</Grid.Styles>
<Grid ColumnDefinitions="Auto,*" Margin="0 15 0 0">
<Grid.Styles>
<Style Selector="TextBlock.link-name">
<Setter Property="Margin" Value="0 7 15 6" />
<Setter Property="FontWeight" Value="600" />
</Style>
</Grid.Styles>
<StackPanel Grid.Row="0" Grid.Column="0">
<TextBlock Classes="link-name">Artemis wiki</TextBlock>
<TextBlock Classes="link-name">Getting started guide</TextBlock>
<TextBlock Classes="link-name">GitHub</TextBlock>
<TextBlock Classes="link-name">Discord</TextBlock>
</StackPanel>
<StackPanel Grid.Column="1">
<HyperlinkButton NavigateUri="https://wiki.artemis-rgb.com/?mtm_campaign=artemis&amp;mtm_kwd=wizard">
https://wiki.artemis-rgb.com/
</HyperlinkButton>
<HyperlinkButton NavigateUri="https://wiki.artemis-rgb.com/en/guides/user/introduction?mtm_campaign=artemis&amp;mtm_kwd=wizard">
https://wiki.artemis-rgb.com/en/guides/user/introduction
</HyperlinkButton>
<HyperlinkButton NavigateUri="https://github.com/Artemis-RGB/Artemis">
https://github.com/Artemis-RGB/Artemis
</HyperlinkButton>
<HyperlinkButton NavigateUri="https://discord.gg/S3MVaC9">
https://discord.gg/S3MVaC9
</HyperlinkButton>
</StackPanel>
</Grid>
</StackPanel>
</Border>
<StackPanel Grid.Row="0" Grid.Column="0">
<TextBlock Classes="link-name">Artemis wiki</TextBlock>
<TextBlock Classes="link-name">Getting started guide</TextBlock>
<TextBlock Classes="link-name">GitHub</TextBlock>
<TextBlock Classes="link-name">Discord</TextBlock>
</StackPanel>
<StackPanel Grid.Column="1">
<HyperlinkButton NavigateUri="https://wiki.artemis-rgb.com/?mtm_campaign=artemis&amp;mtm_kwd=wizard">
https://wiki.artemis-rgb.com/
</HyperlinkButton>
<HyperlinkButton NavigateUri="https://wiki.artemis-rgb.com/en/guides/user/introduction?mtm_campaign=artemis&amp;mtm_kwd=wizard">
https://wiki.artemis-rgb.com/en/guides/user/introduction
</HyperlinkButton>
<HyperlinkButton NavigateUri="https://github.com/Artemis-RGB/Artemis">
https://github.com/Artemis-RGB/Artemis
</HyperlinkButton>
<HyperlinkButton NavigateUri="https://discord.gg/S3MVaC9">
https://discord.gg/S3MVaC9
</HyperlinkButton>
</StackPanel>
</Grid>
</StackPanel>
</Border>
</StackPanel>
</UserControl>

View File

@ -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">
<Border Classes="card">
<Grid RowDefinitions="Auto,Auto,*">
<StackPanel Grid.Row="0">
<TextBlock TextWrapping="Wrap">
Device layouts provide Artemis with an image of your devices and exact LED positions. <LineBreak />
While not strictly necessary, this helps to create effects that are perfectly aligned with your hardware.
</TextBlock>
<TextBlock TextWrapping="Wrap" Margin="0 10">
Below you can automatically search the Artemis Workshop for device layouts of your devices.
</TextBlock>
<StackPanel Spacing="10">
<TextBlock TextWrapping="Wrap">
Device layouts provide Artemis with an image of your devices and exact LED positions. <LineBreak />
While not strictly necessary, this helps to create effects that are perfectly aligned with your hardware.
</TextBlock>
<TextBlock TextWrapping="Wrap" Margin="0 10 0 0">
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.
</TextBlock>
<Border Classes="card">
<StackPanel>
<ScrollViewer>
<ContentControl Content="{CompiledBinding LayoutFinderViewModel}"></ContentControl>
</ScrollViewer>
</StackPanel>
<Button Grid.Row="1"
Content="Auto-install layouts"
Command="{CompiledBinding LayoutFinderViewModel.SearchAll}"
ToolTip.Tip="Search layouts and if found install them automatically"
HorizontalAlignment="Right"/>
<ScrollViewer Grid.Row="2" Margin="0 15">
<ContentControl Content="{CompiledBinding LayoutFinderViewModel}"></ContentControl>
</ScrollViewer>
</Grid>
</Border>
</Border>
<HyperlinkButton
Content="Learn more about layouts on the wiki"
NavigateUri="https://wiki.artemis-rgb.com/en/guides/developer/layouts?mtm_campaign=artemis&amp;mtm_kwd=wizard"
HorizontalAlignment="Right"
VerticalAlignment="Bottom" />
</StackPanel>
</UserControl>

View File

@ -1,4 +1,7 @@
using Artemis.UI.Screens.Workshop.LayoutFinder;
using System;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using Artemis.UI.Screens.Workshop.LayoutFinder;
using ReactiveUI;
namespace Artemis.UI.Screens.StartupWizard.Steps;
@ -8,10 +11,12 @@ public class LayoutsStepViewModel : WizardStepViewModel
public LayoutsStepViewModel(LayoutFinderViewModel layoutFinderViewModel)
{
LayoutFinderViewModel = layoutFinderViewModel;
Continue = ReactiveCommand.Create(() => Wizard.ChangeScreen<SurfaceStepViewModel>());
GoBack = ReactiveCommand.Create(() => Wizard.ChangeScreen<DevicesStepViewModel>());
}
Continue = ReactiveCommand.Create(() => Wizard.ChangeScreen<SurfaceStepViewModel>(), LayoutFinderViewModel.SearchAll.IsExecuting.Select(isExecuting => !isExecuting));
GoBack = ReactiveCommand.Create(() => Wizard.ChangeScreen<DefaultEntriesStepViewModel>(), LayoutFinderViewModel.SearchAll.IsExecuting.Select(isExecuting => !isExecuting));
LayoutFinderViewModel.WhenActivated((CompositeDisposable _) => LayoutFinderViewModel.SearchAll.Execute().Subscribe());
}
public LayoutFinderViewModel LayoutFinderViewModel { get; }
}

View File

@ -2,125 +2,122 @@
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:behaviors="clr-namespace:Artemis.UI.Shared.Behaviors;assembly=Artemis.UI.Shared"
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.SettingsStepView"
x:DataType="steps:SettingsStepViewModel">
<Border Classes="card" VerticalAlignment="Top">
<ScrollViewer>
<StackPanel>
<TextBlock>
Artemis comes with a variety of settings you can change to tweak everything to your liking.
<ScrollViewer>
<StackPanel>
<TextBlock>
Artemis comes with a variety of settings you can change to tweak everything to your liking.
</TextBlock>
<TextBlock>
Below you can find a few relevant settings, many more can be changed later on the settings page.
</TextBlock>
<!-- Auto-run settings -->
<StackPanel IsVisible="{CompiledBinding IsAutoRunSupported}">
<TextBlock Classes="card-title">
Auto-run
</TextBlock>
<TextBlock>
Below you can find a few relevant settings, many more can be changed later on the settings page.
</TextBlock>
<!-- Auto-run settings -->
<StackPanel IsVisible="{CompiledBinding IsAutoRunSupported}">
<TextBlock Classes="card-title">
Auto-run
</TextBlock>
<Border Classes="card" VerticalAlignment="Stretch" Margin="0,0,5,0">
<StackPanel>
<StackPanel IsVisible="{CompiledBinding IsAutoRunSupported}">
<Grid RowDefinitions="*,*" ColumnDefinitions="*,Auto">
<StackPanel Grid.Column="0">
<TextBlock>Auto-run on startup</TextBlock>
</StackPanel>
<ToggleSwitch Grid.Row="0" Grid.Column="1" VerticalAlignment="Center" IsChecked="{CompiledBinding UIAutoRun.Value}" MinWidth="0" Margin="0 -10" />
</Grid>
<Border Classes="card-separator" />
<Grid RowDefinitions="*,*" ColumnDefinitions="*,Auto">
<StackPanel Grid.Column="0">
<TextBlock>Hide window on auto-run</TextBlock>
</StackPanel>
<StackPanel Grid.Row="0" Grid.Column="1" VerticalAlignment="Center">
<ToggleSwitch IsChecked="{CompiledBinding !UIShowOnStartup.Value}" IsEnabled="{CompiledBinding UIAutoRun.Value}" MinWidth="0" Margin="0 -10" />
</StackPanel>
</Grid>
<Border Classes="card-separator" />
<Grid RowDefinitions="*,*" ColumnDefinitions="*,Auto">
<StackPanel Grid.Column="0">
<TextBlock>Associate with Artemis links</TextBlock>
<TextBlock Classes="subtitle" TextWrapping="Wrap">
Open Artemis when navigating to artemis:// links, allows opening workshop entries from your browser.
</TextBlock>
</StackPanel>
<StackPanel Grid.Row="0" Grid.Column="1" VerticalAlignment="Center">
<ToggleSwitch IsChecked="{CompiledBinding UIUseProtocol.Value}" OnContent="Yes" OffContent="No" MinWidth="0" Margin="0 -10" />
</StackPanel>
</Grid>
<Border Classes="card-separator" />
<Grid RowDefinitions="*,*" ColumnDefinitions="*,Auto">
<StackPanel Grid.Column="0">
<TextBlock>Startup delay</TextBlock>
<TextBlock Classes="subtitle" TextWrapping="Wrap">
Set the amount of seconds to wait before auto-running Artemis.
</TextBlock>
<TextBlock Classes="subtitle" TextWrapping="Wrap">
If some devices don't work because Artemis starts before the manufacturer's software, try increasing this value.
</TextBlock>
</StackPanel>
<StackPanel Grid.Row="0" Grid.Column="1" VerticalAlignment="Center" Orientation="Horizontal">
<controls:NumberBox IsEnabled="{CompiledBinding UIAutoRun.Value}" Width="120">
<Interaction.Behaviors>
<behaviors:LostFocusNumberBoxBindingBehavior Value="{CompiledBinding UIAutoRunDelay.Value}" />
</Interaction.Behaviors>
</controls:NumberBox>
<TextBlock VerticalAlignment="Center" TextAlignment="Right" Width="30">sec</TextBlock>
</StackPanel>
</Grid>
</StackPanel>
</StackPanel>
</Border>
</StackPanel>
<!-- Update settings -->
<StackPanel>
<TextBlock Classes="card-title">
Updating
</TextBlock>
<Border Classes="card" VerticalAlignment="Stretch" Margin="0,0,5,0">
<StackPanel>
<Border Classes="card" VerticalAlignment="Stretch" Margin="0,0,5,0">
<StackPanel>
<StackPanel IsVisible="{CompiledBinding IsAutoRunSupported}">
<Grid RowDefinitions="*,*" ColumnDefinitions="*,Auto">
<StackPanel Grid.Column="0">
<TextBlock>
Check for updates
</TextBlock>
<TextBlock Classes="subtitle" TextWrapping="Wrap">
If enabled, we'll check for updates on startup and periodically while running.
</TextBlock>
<TextBlock>Auto-run on startup</TextBlock>
</StackPanel>
<ToggleSwitch Grid.Row="0" Grid.Column="1" VerticalAlignment="Center" IsChecked="{CompiledBinding UIAutoRun.Value}" MinWidth="0" Margin="0 -10" />
</Grid>
<Border Classes="card-separator" />
<Grid RowDefinitions="*,*" ColumnDefinitions="*,Auto">
<StackPanel Grid.Column="0">
<TextBlock>Hide window on auto-run</TextBlock>
</StackPanel>
<StackPanel Grid.Row="0" Grid.Column="1" VerticalAlignment="Center">
<ToggleSwitch IsChecked="{CompiledBinding UICheckForUpdates.Value}" MinWidth="0" />
<ToggleSwitch IsChecked="{CompiledBinding !UIShowOnStartup.Value}" IsEnabled="{CompiledBinding UIAutoRun.Value}" MinWidth="0" Margin="0 -10" />
</StackPanel>
</Grid>
<Border Classes="card-separator" />
<Grid RowDefinitions="*,*" ColumnDefinitions="*,Auto">
<StackPanel Grid.Column="0">
<TextBlock>
Auto-install updates
</TextBlock>
<TextBlock>Associate with Artemis links</TextBlock>
<TextBlock Classes="subtitle" TextWrapping="Wrap">
If enabled, new updates will automatically be installed.
Open Artemis when navigating to artemis:// links, allows opening workshop entries from your browser.
</TextBlock>
</StackPanel>
<StackPanel Grid.Row="0" Grid.Column="1" VerticalAlignment="Center">
<ToggleSwitch IsEnabled="{CompiledBinding UICheckForUpdates.Value}" IsChecked="{CompiledBinding UIAutoUpdate.Value}" MinWidth="0" />
<ToggleSwitch IsChecked="{CompiledBinding UIUseProtocol.Value}" OnContent="Yes" OffContent="No" MinWidth="0" Margin="0 -10" />
</StackPanel>
</Grid>
<Border Classes="card-separator" />
<Grid RowDefinitions="*,*" ColumnDefinitions="*,Auto">
<StackPanel Grid.Column="0">
<TextBlock>Startup delay</TextBlock>
<TextBlock Classes="subtitle" TextWrapping="Wrap">
Set the amount of seconds to wait before auto-running Artemis.
</TextBlock>
<TextBlock Classes="subtitle" TextWrapping="Wrap">
If some devices don't work because Artemis starts before the manufacturer's software, try increasing this value.
</TextBlock>
</StackPanel>
<StackPanel Grid.Row="0" Grid.Column="1" VerticalAlignment="Center" Orientation="Horizontal">
<controls:NumberBox IsEnabled="{CompiledBinding UIAutoRun.Value}" Width="120">
<Interaction.Behaviors>
<behaviors:LostFocusNumberBoxBindingBehavior Value="{CompiledBinding UIAutoRunDelay.Value}" />
</Interaction.Behaviors>
</controls:NumberBox>
<TextBlock VerticalAlignment="Center" TextAlignment="Right" Width="30">sec</TextBlock>
</StackPanel>
</Grid>
</StackPanel>
</Border>
</StackPanel>
</StackPanel>
</Border>
</StackPanel>
</ScrollViewer>
</Border>
<!-- Update settings -->
<StackPanel>
<TextBlock Classes="card-title">
Updating
</TextBlock>
<Border Classes="card" VerticalAlignment="Stretch" Margin="0,0,5,0">
<StackPanel>
<Grid RowDefinitions="*,*" ColumnDefinitions="*,Auto">
<StackPanel Grid.Column="0">
<TextBlock>
Check for updates
</TextBlock>
<TextBlock Classes="subtitle" TextWrapping="Wrap">
If enabled, we'll check for updates on startup and periodically while running.
</TextBlock>
</StackPanel>
<StackPanel Grid.Row="0" Grid.Column="1" VerticalAlignment="Center">
<ToggleSwitch IsChecked="{CompiledBinding UICheckForUpdates.Value}" MinWidth="0" />
</StackPanel>
</Grid>
<Border Classes="card-separator" />
<Grid RowDefinitions="*,*" ColumnDefinitions="*,Auto">
<StackPanel Grid.Column="0">
<TextBlock>
Auto-install updates
</TextBlock>
<TextBlock Classes="subtitle" TextWrapping="Wrap">
If enabled, new updates will automatically be installed.
</TextBlock>
</StackPanel>
<StackPanel Grid.Row="0" Grid.Column="1" VerticalAlignment="Center">
<ToggleSwitch IsEnabled="{CompiledBinding UICheckForUpdates.Value}" IsChecked="{CompiledBinding UIAutoUpdate.Value}" MinWidth="0" />
</StackPanel>
</Grid>
</StackPanel>
</Border>
</StackPanel>
</StackPanel>
</ScrollViewer>
</UserControl>

View File

@ -37,8 +37,9 @@ public class SettingsStepViewModel : WizardStepViewModel
Continue = ReactiveCommand.Create(() => Wizard.ChangeScreen<FinishStepViewModel>());
GoBack = ReactiveCommand.Create(() =>
{
// Without devices, skip to the default entries screen
if (deviceService.EnabledDevices.Count == 0)
Wizard.ChangeScreen<DevicesStepViewModel>();
Wizard.ChangeScreen<DefaultEntriesStepViewModel>();
else
Wizard.ChangeScreen<SurfaceStepViewModel>();
});

View File

@ -2,24 +2,20 @@
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:avalonia="clr-namespace:Material.Icons.Avalonia;assembly=Material.Icons.Avalonia"
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.SurfaceStepView"
x:DataType="steps:SurfaceStepViewModel">
<Grid RowDefinitions="Auto,*,Auto,Auto" ColumnDefinitions="*,*">
<Border Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="2" Classes="card">
<StackPanel>
<TextBlock TextWrapping="Wrap">
Artemis uses "spatial awareness" to create realistic effects across multiple devices.
</TextBlock>
<TextBlock TextWrapping="Wrap">
In order to do this correctly, we need to know where your devices are located on your desk. Select one of the two presets below, after the setup wizard finishes you can tweak this in detail in the surface editor.
</TextBlock>
</StackPanel>
</Border>
<StackPanel Grid.Row="0" Grid.Column="0" Grid.ColumnSpan="2" Spacing="10">
<TextBlock TextWrapping="Wrap">
Artemis uses "spatial awareness" to create realistic effects across multiple devices.
</TextBlock>
<TextBlock TextWrapping="Wrap">
In order to do this correctly, we need to know where your devices are located on your desk. Select one of the two presets below, after the setup wizard finishes you can tweak this in detail in the surface editor.
</TextBlock>
</StackPanel>
<Button Grid.Row="1"
Grid.Column="0"
@ -28,15 +24,14 @@
HorizontalAlignment="Right"
Margin="0 0 10 0"
Width="280"
Height="280"
IsEnabled="False">
Height="280">
<StackPanel>
<avalonia:MaterialIcon Kind="HandBackLeft" Width="150" Height="150" HorizontalAlignment="Center" />
<TextBlock TextAlignment="Center" Classes="h4" Margin="0 10 0 0">
Left-handed preset (NYI)
Left-handed preset
</TextBlock>
<TextBlock TextAlignment="Center" Classes="subtitle" TextWrapping="Wrap">
A preset with the mouse on the left side of the keyboard
A preset with the mouse on the left side of the keyboard (are you the 10%?)
</TextBlock>
</StackPanel>
</Button>

View File

@ -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<SettingsStepViewModel>();
}
}

View File

@ -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">
<Border Classes="card">
<StackPanel>
<TextBlock Classes="h4">Welcome to the Artemis startup wizard!</TextBlock>
<StackPanel Margin="0 50 0 0">
<TextBlock Classes="h4" TextAlignment="Center">Welcome to the Artemis startup wizard!</TextBlock>
<TextBlock TextWrapping="Wrap">
<TextBlock TextWrapping="Wrap" TextAlignment="Center">
In this wizard we'll walk you through the initial configuration of Artemis.
</TextBlock>
<TextBlock TextWrapping="Wrap">
<TextBlock TextWrapping="Wrap" TextAlignment="Center">
Before you can start you need to tell Artemis which devices you want to use and where they are placed on your desk.
</TextBlock>
<TextBlock Classes="subtitle" TextWrapping="Wrap" Margin="0 15 0 0">
<TextBlock Classes="subtitle" TextWrapping="Wrap" Margin="0 15 0 0" TextAlignment="Center">
PS: You can also skip the wizard and set things up yourself.
</TextBlock>
</StackPanel>
</Border>
</UserControl>

View File

@ -6,7 +6,7 @@ public class WelcomeStepViewModel : WizardStepViewModel
{
public WelcomeStepViewModel()
{
Continue = ReactiveCommand.Create(() => Wizard.ChangeScreen<DevicesStepViewModel>());
Continue = ReactiveCommand.Create(() => Wizard.ChangeScreen<DefaultEntriesStepViewModel>());
ShowGoBack = false;
}
}

View File

@ -0,0 +1,30 @@
<UserControl xmlns="https://github.com/avaloniaui"
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:steps="clr-namespace:Artemis.UI.Screens.StartupWizard.Steps"
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.StartupWizard.Steps.WorkshopUnreachableStepView"
x:DataType="steps:WorkshopUnreachableStepViewModel">
<StackPanel>
<StackPanel.Styles>
<Styles>
<Style Selector="TextBlock">
<Setter Property="TextAlignment" Value="Center"></Setter>
<Setter Property="TextWrapping" Value="Wrap"></Setter>
</Style>
</Styles>
</StackPanel.Styles>
<TextBlock Theme="{StaticResource TitleTextBlockStyle}">Could not reach the workshop</TextBlock>
<TextBlock MaxWidth="600" Classes="subtitle">This unfortunately means the setup wizard cannot continue.</TextBlock>
<avalonia:MaterialIcon Kind="LanDisconnect" Width="120" Height="120" Margin="0 60"></avalonia:MaterialIcon>
<TextBlock Theme="{StaticResource SubtitleTextBlockStyle}">Please ensure you are connected to the internet.</TextBlock>
<TextBlock Margin="0 5" Classes="subtitle">If this keeps occuring, hit us up on Discord</TextBlock>
<Button HorizontalAlignment="Center" Margin="0 20" Command="{CompiledBinding Retry}">Retry</Button>
<Button HorizontalAlignment="Center" Command="{CompiledBinding Skip}">Try again on next launch</Button>
</StackPanel>
</UserControl>

View File

@ -4,11 +4,10 @@ using Avalonia.ReactiveUI;
namespace Artemis.UI.Screens.StartupWizard.Steps;
public partial class DevicesStepView : ReactiveUserControl<DevicesStepViewModel>
public partial class WorkshopUnreachableStepView : ReactiveUserControl<WorkshopUnreachableStepViewModel>
{
public DevicesStepView()
public WorkshopUnreachableStepView()
{
InitializeComponent();
}
}

View File

@ -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<DefaultEntriesStepViewModel>();
}
public void Skip()
{
PluginSetting<bool> setting = _settingsService.GetSetting("UI.SetupWizardCompleted", false);
setting.Value = false;
setting.Save();
Wizard.Close(false);
}
}

View File

@ -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"

View File

@ -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!;
}

View File

@ -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)

View File

@ -33,16 +33,17 @@ public partial class NodeScriptView : ReactiveUserControl<NodeScriptViewModel>
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);
});
}

View File

@ -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"

View File

@ -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">

View File

@ -18,10 +18,10 @@ public class CategoriesViewModel : ActivatableViewModelBase
{
private ObservableAsPropertyHelper<IReadOnlyList<EntryFilterInput>?>? _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))

View File

@ -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"

View File

@ -51,11 +51,11 @@
ToolTip.Tip="Click to browse">
</Button>
</Panel>
</Border>
<TextBlock Foreground="{DynamicResource SystemFillColorCriticalBrush}" Margin="2 0" IsVisible="{CompiledBinding !IconValid}" TextWrapping="Wrap">
Icon required
</TextBlock>
<CheckBox IsChecked="{CompiledBinding Fit}">Shrink</CheckBox>
</StackPanel>
<StackPanel Grid.Column="1">
<Label Target="Name" Margin="0">Name</Label>
@ -63,8 +63,12 @@
<Label Target="Summary" Margin="0 5 0 0">Summary</Label>
<TextBox Name="Summary" Text="{CompiledBinding Summary}"></TextBox>
<CheckBox IsVisible="{CompiledBinding IsAdministrator}" IsChecked="{CompiledBinding IsDefault}">Download by default (admin only)</CheckBox>
<StackPanel IsVisible="{CompiledBinding IsAdministrator}" Orientation="Horizontal">
<CheckBox IsChecked="{CompiledBinding IsDefault}">Download by default (admin only)</CheckBox>
<CheckBox IsEnabled="{CompiledBinding IsDefault}" IsChecked="{CompiledBinding IsEssential}">Essential</CheckBox>
<CheckBox IsEnabled="{CompiledBinding IsDefault}" IsChecked="{CompiledBinding IsDeviceProvider}">Device provider</CheckBox>
</StackPanel>
</StackPanel>
</Grid>
@ -93,10 +97,10 @@
<Label>Tags</Label>
<tagsInput:TagsInput Tags="{CompiledBinding Tags}" />
</StackPanel>
<controls:SplitMarkdownEditor Grid.Row="1" Title="Description" Markdown="{CompiledBinding Description}"/>
<TextBlock Grid.Row="2"
<controls:SplitMarkdownEditor Grid.Row="1" Title="Description" Markdown="{CompiledBinding Description}" />
<TextBlock Grid.Row="2"
Foreground="{DynamicResource SystemFillColorCriticalBrush}"
Margin="2 8 0 0"
IsVisible="{CompiledBinding !DescriptionValid}">

Some files were not shown because too many files have changed in this diff Show More