diff --git a/StabilityMatrix.Tests/Helper/EventManagerTests.cs b/StabilityMatrix.Tests/Helper/EventManagerTests.cs new file mode 100644 index 00000000..d51fb541 --- /dev/null +++ b/StabilityMatrix.Tests/Helper/EventManagerTests.cs @@ -0,0 +1,43 @@ +using StabilityMatrix.Helper; + +namespace StabilityMatrix.Tests.Helper; + +[TestClass] +public class EventManagerTests +{ + private EventManager eventManager; + + [TestInitialize] + public void TestInitialize() + { + eventManager = EventManager.Instance; + } + + [TestMethod] + public void GlobalProgressChanged_ShouldBeInvoked() + { + // Arrange + var progress = 0; + eventManager.GlobalProgressChanged += (sender, args) => progress = args; + + // Act + eventManager.OnGlobalProgressChanged(100); + + // Assert + Assert.AreEqual(100, progress); + } + + [TestMethod] + public void RequestPageChange_ShouldBeInvoked() + { + // Arrange + var pageType = typeof(object); + eventManager.PageChangeRequested += (sender, args) => pageType = args; + + // Act + eventManager.RequestPageChange(typeof(int)); + + // Assert + Assert.AreEqual(typeof(int), pageType); + } +} diff --git a/StabilityMatrix.Tests/Helper/PackageFactoryTests.cs b/StabilityMatrix.Tests/Helper/PackageFactoryTests.cs new file mode 100644 index 00000000..062bf24e --- /dev/null +++ b/StabilityMatrix.Tests/Helper/PackageFactoryTests.cs @@ -0,0 +1,43 @@ +using StabilityMatrix.Helper; +using StabilityMatrix.Models; +using StabilityMatrix.Models.Packages; + +namespace StabilityMatrix.Tests.Helper; + +[TestClass] +public class PackageFactoryTests +{ + private PackageFactory packageFactory; + private IEnumerable fakeBasePackages; + + [TestInitialize] + public void Setup() + { + fakeBasePackages = new List + { + new DankDiffusion() + }; + packageFactory = new PackageFactory(fakeBasePackages); + } + + [TestMethod] + public void GetAllAvailablePackages_ReturnsAllPackages() + { + var result = packageFactory.GetAllAvailablePackages(); + Assert.AreEqual(1, result.Count()); + } + + [TestMethod] + public void FindPackageByName_ReturnsPackage() + { + var result = packageFactory.FindPackageByName("dank-diffusion"); + Assert.IsNotNull(result); + } + + [TestMethod] + public void FindPackageByName_ReturnsNull() + { + var result = packageFactory.FindPackageByName("not-a-package"); + Assert.IsNull(result); + } +} diff --git a/StabilityMatrix.Tests/StabilityMatrix.Tests.csproj b/StabilityMatrix.Tests/StabilityMatrix.Tests.csproj new file mode 100644 index 00000000..9c1214e0 --- /dev/null +++ b/StabilityMatrix.Tests/StabilityMatrix.Tests.csproj @@ -0,0 +1,25 @@ + + + + net6.0-windows10.0.17763.0 + enable + enable + + false + true + + + + + + + + + + + + + + + + diff --git a/StabilityMatrix.Tests/Usings.cs b/StabilityMatrix.Tests/Usings.cs new file mode 100644 index 00000000..ab67c7ea --- /dev/null +++ b/StabilityMatrix.Tests/Usings.cs @@ -0,0 +1 @@ +global using Microsoft.VisualStudio.TestTools.UnitTesting; \ No newline at end of file diff --git a/StabilityMatrix.sln b/StabilityMatrix.sln index 7ca54ed3..e7ce760e 100644 --- a/StabilityMatrix.sln +++ b/StabilityMatrix.sln @@ -5,6 +5,8 @@ VisualStudioVersion = 17.6.33717.318 MinimumVisualStudioVersion = 10.0.40219.1 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StabilityMatrix", "StabilityMatrix\StabilityMatrix.csproj", "{7CA2E862-B121-495D-8CCC-2D6EF56A3312}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "StabilityMatrix.Tests", "StabilityMatrix.Tests\StabilityMatrix.Tests.csproj", "{63EF4330-CCFF-4677-B14C-1A700CD81FDA}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -15,6 +17,10 @@ Global {7CA2E862-B121-495D-8CCC-2D6EF56A3312}.Debug|Any CPU.Build.0 = Debug|Any CPU {7CA2E862-B121-495D-8CCC-2D6EF56A3312}.Release|Any CPU.ActiveCfg = Release|Any CPU {7CA2E862-B121-495D-8CCC-2D6EF56A3312}.Release|Any CPU.Build.0 = Release|Any CPU + {63EF4330-CCFF-4677-B14C-1A700CD81FDA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {63EF4330-CCFF-4677-B14C-1A700CD81FDA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {63EF4330-CCFF-4677-B14C-1A700CD81FDA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {63EF4330-CCFF-4677-B14C-1A700CD81FDA}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE diff --git a/StabilityMatrix/App.xaml.cs b/StabilityMatrix/App.xaml.cs index 82e6640d..5ce170cd 100644 --- a/StabilityMatrix/App.xaml.cs +++ b/StabilityMatrix/App.xaml.cs @@ -38,22 +38,31 @@ namespace StabilityMatrix var serviceCollection = new ServiceCollection(); serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); + serviceCollection.AddTransient(); + serviceCollection.AddTransient(); serviceCollection.AddTransient(); serviceCollection.AddTransient(); serviceCollection.AddTransient(); serviceCollection.AddTransient(); + serviceCollection.AddTransient(); + serviceCollection.AddTransient(); + serviceCollection.AddTransient(); serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); - serviceCollection.AddSingleton(); - serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); serviceCollection.AddSingleton(); + serviceCollection.AddSingleton(); var jsonOptions = new JsonSerializerOptions { @@ -70,10 +79,15 @@ namespace StabilityMatrix client.BaseAddress = new Uri("http://localhost:7860"); }); + // Logging configuration var logPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "log.txt"); var logConfig = new NLog.Config.LoggingConfiguration(); + // File logging var fileTarget = new NLog.Targets.FileTarget("logfile") { FileName = logPath }; - logConfig.AddRule(NLog.LogLevel.Trace, NLog.LogLevel.Fatal, fileTarget); + // Log trace+ to debug console + var debugTarget = new NLog.Targets.DebuggerTarget("debugger") { Layout = "${message}" }; + logConfig.AddRule(NLog.LogLevel.Info, NLog.LogLevel.Fatal, fileTarget); + logConfig.AddRule(NLog.LogLevel.Trace, NLog.LogLevel.Fatal, debugTarget); NLog.LogManager.Configuration = logConfig; serviceCollection.AddLogging(log => diff --git a/StabilityMatrix/Helper/DialogErrorHandler.cs b/StabilityMatrix/Helper/DialogErrorHandler.cs new file mode 100644 index 00000000..db594bda --- /dev/null +++ b/StabilityMatrix/Helper/DialogErrorHandler.cs @@ -0,0 +1,66 @@ +using System.Threading.Tasks; +using System; +using System.Windows.Threading; +using Microsoft.Extensions.Logging; +using StabilityMatrix.Models; +using StabilityMatrix.ViewModels; +using Wpf.Ui.Common; +using Wpf.Ui.Contracts; +using Wpf.Ui.Controls; +using Wpf.Ui.Controls.IconElements; + +namespace StabilityMatrix.Helper; + +/// +/// Generic recoverable error handler using content dialogs. +/// +public class DialogErrorHandler : IDialogErrorHandler +{ + private readonly ISnackbarService snackbarService; + private readonly SnackbarViewModel snackbarViewModel; + + public DialogErrorHandler(ISnackbarService snackbarService, SnackbarViewModel snackbarViewModel) + { + this.snackbarService = snackbarService; + this.snackbarViewModel = snackbarViewModel; + } + + /// + /// Shows a generic error snackbar with the given message. + /// + public void ShowSnackbarAsync(string message, LogLevel level = LogLevel.Error, int timeoutMilliseconds = 5000) + { + snackbarViewModel.SnackbarAppearance = level switch + { + LogLevel.Error => ControlAppearance.Danger, + LogLevel.Warning => ControlAppearance.Caution, + LogLevel.Information => ControlAppearance.Info, + _ => ControlAppearance.Secondary + }; + snackbarService.Timeout = timeoutMilliseconds; + var icon = new SymbolIcon(SymbolRegular.ErrorCircle24); + snackbarService.ShowAsync("Error", message, icon, snackbarViewModel.SnackbarAppearance); + } + + /// + /// Attempt to run the given action, showing a generic error snackbar if it fails. + /// + public async Task> TryAsync(Task task, string message, LogLevel level = LogLevel.Error, int timeoutMilliseconds = 5000) + { + try + { + return new TaskResult + { + Result = await task + }; + } + catch (Exception e) + { + ShowSnackbarAsync(message, level, timeoutMilliseconds); + return new TaskResult + { + Exception = e + }; + } + } +} diff --git a/StabilityMatrix/Helper/DialogFactory.cs b/StabilityMatrix/Helper/DialogFactory.cs new file mode 100644 index 00000000..b3fe51ba --- /dev/null +++ b/StabilityMatrix/Helper/DialogFactory.cs @@ -0,0 +1,34 @@ +using System; +using StabilityMatrix.Models; +using StabilityMatrix.ViewModels; +using Wpf.Ui.Contracts; + +namespace StabilityMatrix.Helper; + +public class DialogFactory : IDialogFactory +{ + private readonly IContentDialogService contentDialogService; + private readonly LaunchOptionsDialogViewModel launchOptionsDialogViewModel; + private readonly ISettingsManager settingsManager; + + public DialogFactory(IContentDialogService contentDialogService, LaunchOptionsDialogViewModel launchOptionsDialogViewModel, ISettingsManager settingsManager) + { + this.contentDialogService = contentDialogService; + this.launchOptionsDialogViewModel = launchOptionsDialogViewModel; + this.settingsManager = settingsManager; + } + + public LaunchOptionsDialog CreateLaunchOptionsDialog(BasePackage selectedPackage, InstalledPackage installedPackage) + { + var definitions = selectedPackage.LaunchOptions; + launchOptionsDialogViewModel.SelectedPackage = selectedPackage; + launchOptionsDialogViewModel.Cards.Clear(); + // Create cards + launchOptionsDialogViewModel.CardsFromDefinitions(definitions); + // Load user settings + var userLaunchArgs = settingsManager.GetLaunchArgs(installedPackage.Id); + launchOptionsDialogViewModel.LoadFromLaunchArgs(userLaunchArgs); + + return new LaunchOptionsDialog(contentDialogService, launchOptionsDialogViewModel); + } +} diff --git a/StabilityMatrix/Helper/IDialogErrorHandler.cs b/StabilityMatrix/Helper/IDialogErrorHandler.cs new file mode 100644 index 00000000..6769a35e --- /dev/null +++ b/StabilityMatrix/Helper/IDialogErrorHandler.cs @@ -0,0 +1,18 @@ +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using StabilityMatrix.Models; + +namespace StabilityMatrix.Helper; + +public interface IDialogErrorHandler +{ + /// + /// Shows a generic error snackbar with the given message. + /// + void ShowSnackbarAsync(string message, LogLevel level = LogLevel.Error, int timeoutMilliseconds = 5000); + + /// + /// Attempt to run the given action, showing a generic error snackbar if it fails. + /// + Task> TryAsync(Task task, string message, LogLevel level = LogLevel.Error, int timeoutMilliseconds = 5000); +} diff --git a/StabilityMatrix/Helper/IDialogFactory.cs b/StabilityMatrix/Helper/IDialogFactory.cs new file mode 100644 index 00000000..bbeb00bc --- /dev/null +++ b/StabilityMatrix/Helper/IDialogFactory.cs @@ -0,0 +1,8 @@ +using StabilityMatrix.Models; + +namespace StabilityMatrix.Helper; + +public interface IDialogFactory +{ + LaunchOptionsDialog CreateLaunchOptionsDialog(BasePackage selectedPackage, InstalledPackage installedPackage); +} diff --git a/StabilityMatrix/Helper/ISettingsManager.cs b/StabilityMatrix/Helper/ISettingsManager.cs index c342c0c6..05c526c7 100644 --- a/StabilityMatrix/Helper/ISettingsManager.cs +++ b/StabilityMatrix/Helper/ISettingsManager.cs @@ -1,4 +1,6 @@ -using StabilityMatrix.Models; +using System; +using System.Collections.Generic; +using StabilityMatrix.Models; namespace StabilityMatrix.Helper; @@ -12,4 +14,6 @@ public interface ISettingsManager void SetHasInstalledVenv(bool hasInstalledVenv); void SetNavExpanded(bool navExpanded); void UpdatePackageVersionNumber(string packageName, string newVersion); + List GetLaunchArgs(Guid packageId); + void SaveLaunchArgs(Guid packageId, List launchArgs); } diff --git a/StabilityMatrix/Helper/SettingsManager.cs b/StabilityMatrix/Helper/SettingsManager.cs index 7d4dcf2c..e3bfb5a9 100644 --- a/StabilityMatrix/Helper/SettingsManager.cs +++ b/StabilityMatrix/Helper/SettingsManager.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.IO; using System.Linq; using System.Text.Json; @@ -86,6 +87,24 @@ public class SettingsManager : ISettingsManager package.PackageVersion = newVersion; SaveSettings(); } + + public List GetLaunchArgs(Guid packageId) + { + var packageData = Settings.InstalledPackages.FirstOrDefault(x => x.Id == packageId); + return packageData?.LaunchArgs ?? new List(); + } + + public void SaveLaunchArgs(Guid packageId, List launchArgs) + { + var packageData = Settings.InstalledPackages.FirstOrDefault(x => x.Id == packageId); + if (packageData == null) + { + return; + } + + packageData.LaunchArgs = launchArgs; + SaveSettings(); + } private void LoadSettings() { diff --git a/StabilityMatrix/IPyRunner.cs b/StabilityMatrix/IPyRunner.cs new file mode 100644 index 00000000..a6f164b9 --- /dev/null +++ b/StabilityMatrix/IPyRunner.cs @@ -0,0 +1,47 @@ +using System.IO; +using System.Threading.Tasks; + +namespace StabilityMatrix; + +public interface IPyRunner +{ + /// + /// Initializes the Python runtime using the embedded dll. + /// Can be called with no effect after initialization. + /// + /// Thrown if Python DLL not found. + Task Initialize(); + + /// + /// One-time setup for get-pip + /// + Task SetupPip(); + + /// + /// Install a Python package with pip + /// + Task InstallPackage(string package); + + /// + /// Evaluate Python expression and return its value as a string + /// + /// + Task Eval(string expression); + + /// + /// Evaluate Python expression and return its value + /// + /// + Task Eval(string expression); + + /// + /// Execute Python code without returning a value + /// + /// + Task Exec(string code); + + /// + /// Return the Python version as a PyVersionInfo struct + /// + Task GetVersionInfo(); +} \ No newline at end of file diff --git a/StabilityMatrix/LaunchOptionsDialog.xaml b/StabilityMatrix/LaunchOptionsDialog.xaml new file mode 100644 index 00000000..7e125230 --- /dev/null +++ b/StabilityMatrix/LaunchOptionsDialog.xaml @@ -0,0 +1,59 @@ + + + +