Browse Source

Merge branch 'main' into inference

# Conflicts:
#	StabilityMatrix.Avalonia/App.axaml.cs
#	StabilityMatrix.Avalonia/DesignData/DesignData.cs
pull/165/head
Ionite 1 year ago
parent
commit
81acce59b6
No known key found for this signature in database
  1. 18
      CHANGELOG.md
  2. 14
      StabilityMatrix.Avalonia/App.axaml.cs
  3. 2
      StabilityMatrix.Avalonia/Controls/AppWindowBase.cs
  4. 2
      StabilityMatrix.Avalonia/Controls/BetterContentDialog.cs
  5. 2
      StabilityMatrix.Avalonia/Controls/UserControlBase.cs
  6. 36
      StabilityMatrix.Avalonia/DesignData/DesignData.cs
  7. 1
      StabilityMatrix.Avalonia/DesignData/MockLaunchPageViewModel.cs
  8. 7
      StabilityMatrix.Avalonia/Helpers/UnixPrerequisiteHelper.cs
  9. 1
      StabilityMatrix.Avalonia/Program.cs
  10. 23
      StabilityMatrix.Avalonia/Services/INavigationService.cs
  11. 62
      StabilityMatrix.Avalonia/Services/NavigationService.cs
  12. 53
      StabilityMatrix.Avalonia/ViewLocator.cs
  13. 2
      StabilityMatrix.Avalonia/ViewModels/Base/PageViewModelBase.cs
  14. 4
      StabilityMatrix.Avalonia/ViewModels/Base/ProgressViewModel.cs
  15. 2
      StabilityMatrix.Avalonia/ViewModels/Base/ViewModelBase.cs
  16. 34
      StabilityMatrix.Avalonia/ViewModels/CheckpointBrowser/CheckpointBrowserCardViewModel.cs
  17. 35
      StabilityMatrix.Avalonia/ViewModels/CheckpointBrowserViewModel.cs
  18. 13
      StabilityMatrix.Avalonia/ViewModels/CheckpointManager/CheckpointFile.cs
  19. 5
      StabilityMatrix.Avalonia/ViewModels/CheckpointManager/CheckpointFolder.cs
  20. 15
      StabilityMatrix.Avalonia/ViewModels/CheckpointsPageViewModel.cs
  21. 1
      StabilityMatrix.Avalonia/ViewModels/Dialogs/CivitFileViewModel.cs
  22. 1
      StabilityMatrix.Avalonia/ViewModels/Dialogs/ContentDialogViewModelBase.cs
  23. 2
      StabilityMatrix.Avalonia/ViewModels/Dialogs/ExceptionViewModel.cs
  24. 2
      StabilityMatrix.Avalonia/ViewModels/Dialogs/InstallerViewModel.cs
  25. 1
      StabilityMatrix.Avalonia/ViewModels/Dialogs/ModelVersionViewModel.cs
  26. 1
      StabilityMatrix.Avalonia/ViewModels/Dialogs/OneClickInstallViewModel.cs
  27. 1
      StabilityMatrix.Avalonia/ViewModels/Dialogs/SelectModelVersionViewModel.cs
  28. 2
      StabilityMatrix.Avalonia/ViewModels/Dialogs/UpdateViewModel.cs
  29. 1
      StabilityMatrix.Avalonia/ViewModels/FirstLaunchSetupViewModel.cs
  30. 5
      StabilityMatrix.Avalonia/ViewModels/LaunchPageViewModel.cs
  31. 17
      StabilityMatrix.Avalonia/ViewModels/MainWindowViewModel.cs
  32. 298
      StabilityMatrix.Avalonia/ViewModels/PackageManager/PackageCardViewModel.cs
  33. 314
      StabilityMatrix.Avalonia/ViewModels/PackageManagerViewModel.cs
  34. 1
      StabilityMatrix.Avalonia/ViewModels/ProgressItemViewModel.cs
  35. 1
      StabilityMatrix.Avalonia/ViewModels/ProgressManagerViewModel.cs
  36. 1
      StabilityMatrix.Avalonia/ViewModels/RefreshBadgeViewModel.cs
  37. 1
      StabilityMatrix.Avalonia/ViewModels/SettingsViewModel.cs
  38. 39
      StabilityMatrix.Avalonia/Views/CheckpointBrowserPage.axaml
  39. 7
      StabilityMatrix.Avalonia/Views/CheckpointsPage.axaml
  40. 2
      StabilityMatrix.Avalonia/Views/CheckpointsPage.axaml.cs
  41. 1
      StabilityMatrix.Avalonia/Views/Dialogs/EnvVarsDialog.axaml
  42. 19
      StabilityMatrix.Avalonia/Views/Dialogs/SelectModelVersionDialog.axaml
  43. 15
      StabilityMatrix.Avalonia/Views/MainWindow.axaml
  44. 61
      StabilityMatrix.Avalonia/Views/MainWindow.axaml.cs
  45. 265
      StabilityMatrix.Avalonia/Views/PackageManagerPage.axaml
  46. 1
      StabilityMatrix.Core/Database/LiteDbContext.cs
  47. 4
      StabilityMatrix.Core/Extensions/StringExtensions.cs
  48. 1
      StabilityMatrix.Core/Helper/Compat.cs
  49. 4
      StabilityMatrix.Core/Helper/EventManager.cs
  50. 1
      StabilityMatrix.Core/Helper/IPrerequisiteHelper.cs
  51. 7
      StabilityMatrix.Core/Helper/SharedFolders.cs
  52. 4
      StabilityMatrix.Core/Models/Api/CivitModel.cs
  53. 15
      StabilityMatrix.Core/Models/Api/CivitModelsRequest.cs
  54. 2
      StabilityMatrix.Core/Models/InstalledPackage.cs
  55. 9
      StabilityMatrix.Core/Models/Packages/A3WebUI.cs
  56. 1
      StabilityMatrix.Core/Models/Packages/BasePackage.cs
  57. 3
      StabilityMatrix.Core/Models/Packages/ComfyUI.cs
  58. 5
      StabilityMatrix.Core/Models/Packages/DankDiffusion.cs
  59. 97
      StabilityMatrix.Core/Models/Packages/InvokeAI.cs
  60. 61
      StabilityMatrix.Core/Models/Packages/VladAutomatic.cs
  61. 6
      StabilityMatrix.Core/Models/Packages/VoltaML.cs
  62. 2
      StabilityMatrix.Core/Models/Settings/ModelSearchOptions.cs
  63. 3
      StabilityMatrix.Core/Processes/ApcMessage.cs
  64. 1
      StabilityMatrix.Core/Processes/ApcParser.cs
  65. 7
      StabilityMatrix.Core/Python/PyRunner.cs
  66. 41
      StabilityMatrix.Core/Python/PyVenvRunner.cs
  67. 5
      StabilityMatrix.Core/Services/SettingsManager.cs
  68. 14
      StabilityMatrix.Tests/Helper/EventManagerTests.cs

18
CHANGELOG.md

@ -5,6 +5,24 @@ All notable changes to Stability Matrix will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning 2.0](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning 2.0](https://semver.org/spec/v2.0.0.html).
## v2.2.0
### Added
- Added option to search by Base Model in the Model Browser
- Animated page transitions
### Fixed
- Fixed [#59](https://github.com/LykosAI/StabilityMatrix/issues/61) - `GIT` environment variable is now set for the embedded portable git on Windows as A1111 uses it instead of default `PATH` resolution
- Fixed embedded Python install check on Linux when an incompatible windows DLL is in the Python install directory
### Changed
- Revamped Package Manager UI
- InvokeAI installations can now use checkpoints from the Shared Models folder
## v2.1.2
### Changed
- SD.Next install now uses ROCm PyTorch backend on Linux AMD GPU machines for better performance over DirectML
## v2.1.1 ## v2.1.1
### Added ### Added

14
StabilityMatrix.Avalonia/App.axaml.cs

@ -39,7 +39,10 @@ using StabilityMatrix.Avalonia.Models;
using StabilityMatrix.Avalonia.Models.TagCompletion; using StabilityMatrix.Avalonia.Models.TagCompletion;
using StabilityMatrix.Avalonia.Services; using StabilityMatrix.Avalonia.Services;
using StabilityMatrix.Avalonia.ViewModels; using StabilityMatrix.Avalonia.ViewModels;
using StabilityMatrix.Avalonia.ViewModels.Base;
using StabilityMatrix.Avalonia.ViewModels.CheckpointBrowser;
using StabilityMatrix.Avalonia.ViewModels.Dialogs; using StabilityMatrix.Avalonia.ViewModels.Dialogs;
using StabilityMatrix.Avalonia.ViewModels.PackageManager;
using StabilityMatrix.Avalonia.ViewModels.Inference; using StabilityMatrix.Avalonia.ViewModels.Inference;
using StabilityMatrix.Avalonia.Views; using StabilityMatrix.Avalonia.Views;
using StabilityMatrix.Avalonia.Views.Dialogs; using StabilityMatrix.Avalonia.Views.Dialogs;
@ -57,6 +60,8 @@ using StabilityMatrix.Core.Python;
using StabilityMatrix.Core.Services; using StabilityMatrix.Core.Services;
using StabilityMatrix.Core.Updater; using StabilityMatrix.Core.Updater;
using Application = Avalonia.Application; using Application = Avalonia.Application;
using CheckpointFile = StabilityMatrix.Avalonia.ViewModels.CheckpointManager.CheckpointFile;
using CheckpointFolder = StabilityMatrix.Avalonia.ViewModels.CheckpointManager.CheckpointFolder;
using LogLevel = Microsoft.Extensions.Logging.LogLevel; using LogLevel = Microsoft.Extensions.Logging.LogLevel;
namespace StabilityMatrix.Avalonia; namespace StabilityMatrix.Avalonia;
@ -144,11 +149,9 @@ public sealed class App : Application
if (DesktopLifetime is null) return; if (DesktopLifetime is null) return;
var mainViewModel = Services.GetRequiredService<MainWindowViewModel>(); var mainViewModel = Services.GetRequiredService<MainWindowViewModel>();
var notificationService = Services.GetRequiredService<INotificationService>();
var mainWindow = Services.GetRequiredService<MainWindow>(); var mainWindow = Services.GetRequiredService<MainWindow>();
mainWindow.DataContext = mainViewModel; mainWindow.DataContext = mainViewModel;
mainWindow.NotificationService = notificationService;
mainWindow.ExtendClientAreaChromeHints = Program.Args.NoWindowChromeEffects ? mainWindow.ExtendClientAreaChromeHints = Program.Args.NoWindowChromeEffects ?
ExtendClientAreaChromeHints.NoChrome : ExtendClientAreaChromeHints.PreferSystemChrome; ExtendClientAreaChromeHints.NoChrome : ExtendClientAreaChromeHints.PreferSystemChrome;
@ -203,7 +206,6 @@ public sealed class App : Application
services.AddSingleton<PackageManagerViewModel>() services.AddSingleton<PackageManagerViewModel>()
.AddSingleton<SettingsViewModel>() .AddSingleton<SettingsViewModel>()
.AddSingleton<CheckpointBrowserViewModel>() .AddSingleton<CheckpointBrowserViewModel>()
.AddSingleton<CheckpointBrowserCardViewModel>()
.AddSingleton<CheckpointsPageViewModel>() .AddSingleton<CheckpointsPageViewModel>()
.AddSingleton<LaunchPageViewModel>() .AddSingleton<LaunchPageViewModel>()
.AddSingleton<ProgressManagerViewModel>() .AddSingleton<ProgressManagerViewModel>()
@ -250,6 +252,9 @@ public sealed class App : Application
services.AddTransient<CheckpointFolder>(); services.AddTransient<CheckpointFolder>();
services.AddTransient<CheckpointFile>(); services.AddTransient<CheckpointFile>();
services.AddTransient<InferenceTextToImageViewModel>(); services.AddTransient<InferenceTextToImageViewModel>();
services.AddTransient<CheckpointBrowserCardViewModel>();
services.AddTransient<PackageCardViewModel>();
// Global progress // Global progress
services.AddSingleton<ProgressManagerViewModel>(); services.AddSingleton<ProgressManagerViewModel>();
@ -277,8 +282,10 @@ public sealed class App : Application
.Register(provider.GetRequiredService<SelectDataDirectoryViewModel>) .Register(provider.GetRequiredService<SelectDataDirectoryViewModel>)
.Register(provider.GetRequiredService<LaunchOptionsViewModel>) .Register(provider.GetRequiredService<LaunchOptionsViewModel>)
.Register(provider.GetRequiredService<UpdateViewModel>) .Register(provider.GetRequiredService<UpdateViewModel>)
.Register(provider.GetRequiredService<CheckpointBrowserCardViewModel>)
.Register(provider.GetRequiredService<CheckpointFolder>) .Register(provider.GetRequiredService<CheckpointFolder>)
.Register(provider.GetRequiredService<CheckpointFile>) .Register(provider.GetRequiredService<CheckpointFile>)
.Register(provider.GetRequiredService<PackageCardViewModel>)
.Register(provider.GetRequiredService<RefreshBadgeViewModel>) .Register(provider.GetRequiredService<RefreshBadgeViewModel>)
.Register(provider.GetRequiredService<ExceptionViewModel>) .Register(provider.GetRequiredService<ExceptionViewModel>)
.Register(provider.GetRequiredService<EnvVarsViewModel>) .Register(provider.GetRequiredService<EnvVarsViewModel>)
@ -366,6 +373,7 @@ public sealed class App : Application
services.AddSingleton<INotificationService, NotificationService>(); services.AddSingleton<INotificationService, NotificationService>();
services.AddSingleton<IPyRunner, PyRunner>(); services.AddSingleton<IPyRunner, PyRunner>();
services.AddSingleton<IUpdateHelper, UpdateHelper>(); services.AddSingleton<IUpdateHelper, UpdateHelper>();
services.AddSingleton<INavigationService, NavigationService>();
services.AddSingleton<IInferenceClientManager, InferenceClientManager>(); services.AddSingleton<IInferenceClientManager, InferenceClientManager>();
services.AddSingleton<ICompletionProvider, CompletionProvider>(); services.AddSingleton<ICompletionProvider, CompletionProvider>();

2
StabilityMatrix.Avalonia/Controls/AppWindowBase.cs

@ -6,7 +6,7 @@ using AsyncAwaitBestPractices;
using Avalonia.Interactivity; using Avalonia.Interactivity;
using Avalonia.Threading; using Avalonia.Threading;
using FluentAvalonia.UI.Windowing; using FluentAvalonia.UI.Windowing;
using StabilityMatrix.Avalonia.ViewModels; using StabilityMatrix.Avalonia.ViewModels.Base;
namespace StabilityMatrix.Avalonia.Controls; namespace StabilityMatrix.Avalonia.Controls;

2
StabilityMatrix.Avalonia/Controls/BetterContentDialog.cs

@ -8,7 +8,7 @@ using Avalonia.Controls.Primitives;
using Avalonia.Interactivity; using Avalonia.Interactivity;
using Avalonia.Threading; using Avalonia.Threading;
using FluentAvalonia.UI.Controls; using FluentAvalonia.UI.Controls;
using StabilityMatrix.Avalonia.ViewModels; using StabilityMatrix.Avalonia.ViewModels.Base;
using StabilityMatrix.Avalonia.ViewModels.Dialogs; using StabilityMatrix.Avalonia.ViewModels.Dialogs;
namespace StabilityMatrix.Avalonia.Controls; namespace StabilityMatrix.Avalonia.Controls;

2
StabilityMatrix.Avalonia/Controls/UserControlBase.cs

@ -3,7 +3,7 @@ using AsyncAwaitBestPractices;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Interactivity; using Avalonia.Interactivity;
using Avalonia.Threading; using Avalonia.Threading;
using StabilityMatrix.Avalonia.ViewModels; using StabilityMatrix.Avalonia.ViewModels.Base;
namespace StabilityMatrix.Avalonia.Controls; namespace StabilityMatrix.Avalonia.Controls;

36
StabilityMatrix.Avalonia/DesignData/DesignData.cs

@ -4,6 +4,7 @@ using System.Collections.Immutable;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.IO; using System.IO;
using System.Linq;
using System.Net.Http; using System.Net.Http;
using AvaloniaEdit.Utils; using AvaloniaEdit.Utils;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
@ -12,7 +13,10 @@ using StabilityMatrix.Avalonia.Models;
using StabilityMatrix.Avalonia.Models.TagCompletion; using StabilityMatrix.Avalonia.Models.TagCompletion;
using StabilityMatrix.Avalonia.Services; using StabilityMatrix.Avalonia.Services;
using StabilityMatrix.Avalonia.ViewModels; using StabilityMatrix.Avalonia.ViewModels;
using StabilityMatrix.Avalonia.ViewModels.Base;
using StabilityMatrix.Avalonia.ViewModels.CheckpointBrowser;
using StabilityMatrix.Avalonia.ViewModels.Dialogs; using StabilityMatrix.Avalonia.ViewModels.Dialogs;
using StabilityMatrix.Avalonia.ViewModels.PackageManager;
using StabilityMatrix.Avalonia.ViewModels.Inference; using StabilityMatrix.Avalonia.ViewModels.Inference;
using StabilityMatrix.Core.Api; using StabilityMatrix.Core.Api;
using StabilityMatrix.Core.Database; using StabilityMatrix.Core.Database;
@ -26,6 +30,8 @@ using StabilityMatrix.Core.Models.Progress;
using StabilityMatrix.Core.Python; using StabilityMatrix.Core.Python;
using StabilityMatrix.Core.Services; using StabilityMatrix.Core.Services;
using StabilityMatrix.Core.Updater; using StabilityMatrix.Core.Updater;
using CheckpointFile = StabilityMatrix.Avalonia.ViewModels.CheckpointManager.CheckpointFile;
using CheckpointFolder = StabilityMatrix.Avalonia.ViewModels.CheckpointManager.CheckpointFolder;
namespace StabilityMatrix.Avalonia.DesignData; namespace StabilityMatrix.Avalonia.DesignData;
@ -56,6 +62,7 @@ public static class DesignData
{ {
Id = activePackageId, Id = activePackageId,
DisplayName = "My Installed Package", DisplayName = "My Installed Package",
DisplayVersion = "v1.0.0",
PackageName = "stable-diffusion-webui", PackageName = "stable-diffusion-webui",
PackageVersion = "v1.0.0", PackageVersion = "v1.0.0",
LibraryPath = $"Packages{Path.DirectorySeparatorChar}example-webui", LibraryPath = $"Packages{Path.DirectorySeparatorChar}example-webui",
@ -65,8 +72,8 @@ public static class DesignData
{ {
Id = Guid.NewGuid(), Id = Guid.NewGuid(),
DisplayName = "Dank Diffusion", DisplayName = "Dank Diffusion",
PackageName = "dank-diffusion", PackageName = "ComfyUI",
PackageVersion = "v2.0.0", DisplayVersion = "main@ab73d4a",
LibraryPath = $"Packages{Path.DirectorySeparatorChar}example-webui", LibraryPath = $"Packages{Path.DirectorySeparatorChar}example-webui",
LastUpdateCheck = DateTimeOffset.Now LastUpdateCheck = DateTimeOffset.Now
} }
@ -77,6 +84,7 @@ public static class DesignData
// General services // General services
services.AddLogging() services.AddLogging()
.AddSingleton<INavigationService, NavigationService>()
.AddSingleton<IPackageFactory, PackageFactory>() .AddSingleton<IPackageFactory, PackageFactory>()
.AddSingleton<IUpdateHelper, UpdateHelper>() .AddSingleton<IUpdateHelper, UpdateHelper>()
.AddSingleton<ICompletionProvider, CompletionProvider>() .AddSingleton<ICompletionProvider, CompletionProvider>()
@ -213,12 +221,14 @@ public static class DesignData
CheckpointBrowserViewModel.ModelCards = new CheckpointBrowserViewModel.ModelCards = new
ObservableCollection<CheckpointBrowserCardViewModel> ObservableCollection<CheckpointBrowserCardViewModel>
{ {
new(new CivitModel dialogFactory.Get<CheckpointBrowserCardViewModel>(vm =>
{
vm.CivitModel = new CivitModel
{ {
Name = "BB95 Furry Mix", Name = "BB95 Furry Mix",
Description = "A furry mix of BB95", Description = "A furry mix of BB95",
}, downloadService, settingsManager, };
dialogFactory, notificationService) })
}; };
ProgressManagerViewModel.ProgressItems = new ObservableCollection<ProgressItemViewModel> ProgressManagerViewModel.ProgressItems = new ObservableCollection<ProgressItemViewModel>
@ -254,8 +264,19 @@ public static class DesignData
public static LaunchPageViewModel LaunchPageViewModel => public static LaunchPageViewModel LaunchPageViewModel =>
Services.GetRequiredService<LaunchPageViewModel>(); Services.GetRequiredService<LaunchPageViewModel>();
public static PackageManagerViewModel PackageManagerViewModel => public static PackageManagerViewModel PackageManagerViewModel
Services.GetRequiredService<PackageManagerViewModel>(); {
get
{
var settings = Services.GetRequiredService<ISettingsManager>();
var vm = Services.GetRequiredService<PackageManagerViewModel>();
vm.Packages = new ObservableCollection<PackageCardViewModel>(
settings.Settings.InstalledPackages.Select(p =>
DialogFactory.Get<PackageCardViewModel>(viewModel => viewModel.Package = p)));
vm.Packages.First().IsUpdateAvailable = true;
return vm;
}
}
public static CheckpointsPageViewModel CheckpointsPageViewModel => public static CheckpointsPageViewModel CheckpointsPageViewModel =>
Services.GetRequiredService<CheckpointsPageViewModel>(); Services.GetRequiredService<CheckpointsPageViewModel>();
@ -276,6 +297,7 @@ public static class DesignData
{ {
Name = "BB95 Furry Mix", Name = "BB95 Furry Mix",
Description = "v1.0.0", Description = "v1.0.0",
BaseModel = "SD 1.5",
Files = new List<CivitFile> Files = new List<CivitFile>
{ {
new() new()

1
StabilityMatrix.Avalonia/DesignData/MockLaunchPageViewModel.cs

@ -2,6 +2,7 @@
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using StabilityMatrix.Avalonia.Services; using StabilityMatrix.Avalonia.Services;
using StabilityMatrix.Avalonia.ViewModels; using StabilityMatrix.Avalonia.ViewModels;
using StabilityMatrix.Avalonia.ViewModels.Base;
using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Helper;
using StabilityMatrix.Core.Helper.Factory; using StabilityMatrix.Core.Helper.Factory;
using StabilityMatrix.Core.Models; using StabilityMatrix.Core.Models;

7
StabilityMatrix.Avalonia/Helpers/UnixPrerequisiteHelper.cs

@ -27,11 +27,10 @@ public class UnixPrerequisiteHelper : IPrerequisiteHelper
private readonly IPyRunner pyRunner; private readonly IPyRunner pyRunner;
private DirectoryPath HomeDir => settingsManager.LibraryDir; private DirectoryPath HomeDir => settingsManager.LibraryDir;
private DirectoryPath AssetsDir => HomeDir + "Assets"; private DirectoryPath AssetsDir => HomeDir.JoinDir("Assets");
private DirectoryPath PythonDir => AssetsDir + "Python310"; private DirectoryPath PythonDir => AssetsDir.JoinDir("Python310");
private FilePath PythonDllPath => PythonDir + "python310.dll"; public bool IsPythonInstalled => PythonDir.JoinFile(PyRunner.RelativePythonDllPath).Exists;
public bool IsPythonInstalled => PythonDllPath.Exists;
private DirectoryPath PortableGitInstallDir => HomeDir + "PortableGit"; private DirectoryPath PortableGitInstallDir => HomeDir + "PortableGit";
public string GitBinPath => PortableGitInstallDir + "bin"; public string GitBinPath => PortableGitInstallDir + "bin";

1
StabilityMatrix.Avalonia/Program.cs

@ -12,7 +12,6 @@ using Avalonia;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Threading; using Avalonia.Threading;
using Microsoft.Extensions.DependencyInjection;
using NLog; using NLog;
using Polly.Contrib.WaitAndRetry; using Polly.Contrib.WaitAndRetry;
using Projektanker.Icons.Avalonia; using Projektanker.Icons.Avalonia;

23
StabilityMatrix.Avalonia/Services/INavigationService.cs

@ -0,0 +1,23 @@
using FluentAvalonia.UI.Controls;
using FluentAvalonia.UI.Media.Animation;
using StabilityMatrix.Avalonia.ViewModels.Base;
namespace StabilityMatrix.Avalonia.Services;
public interface INavigationService
{
/// <summary>
/// Set the frame to use for navigation.
/// </summary>
void SetFrame(Frame frame);
/// <summary>
/// Navigate to the view of the given view model type.
/// </summary>
void NavigateTo<TViewModel>(NavigationTransitionInfo? transitionInfo = null) where TViewModel : ViewModelBase;
/// <summary>
/// Navigate to the view of the given view model.
/// </summary>
void NavigateTo(ViewModelBase viewModel, NavigationTransitionInfo? transitionInfo = null);
}

62
StabilityMatrix.Avalonia/Services/NavigationService.cs

@ -0,0 +1,62 @@
using System;
using System.Linq;
using FluentAvalonia.UI.Controls;
using FluentAvalonia.UI.Media.Animation;
using FluentAvalonia.UI.Navigation;
using StabilityMatrix.Avalonia.ViewModels;
using StabilityMatrix.Avalonia.ViewModels.Base;
namespace StabilityMatrix.Avalonia.Services;
public class NavigationService : INavigationService
{
private Frame? _frame;
/// <inheritdoc />
public void SetFrame(Frame frame)
{
_frame = frame;
}
/// <inheritdoc />
public void NavigateTo<TViewModel>(NavigationTransitionInfo? transitionInfo = null) where TViewModel : ViewModelBase
{
if (_frame is null)
{
throw new InvalidOperationException("SetFrame was not called before NavigateTo.");
}
_frame.NavigateToType(typeof(TViewModel),
null,
new FrameNavigationOptions
{
IsNavigationStackEnabled = true,
TransitionInfoOverride = transitionInfo ?? new SuppressNavigationTransitionInfo()
});
if (!typeof(TViewModel).IsAssignableTo(typeof(PageViewModelBase)))
return;
if (App.Services.GetService(typeof(MainWindowViewModel)) is MainWindowViewModel mainViewModel)
{
mainViewModel.SelectedCategory =
mainViewModel.Pages.FirstOrDefault(x => x.GetType() == typeof(TViewModel));
}
}
/// <inheritdoc />
public void NavigateTo(ViewModelBase viewModel, NavigationTransitionInfo? transitionInfo = null)
{
if (_frame is null)
{
throw new InvalidOperationException("SetFrame was not called before NavigateTo.");
}
_frame.NavigateFromObject(viewModel,
new FrameNavigationOptions
{
IsNavigationStackEnabled = true,
TransitionInfoOverride = transitionInfo ?? new SuppressNavigationTransitionInfo()
});
}
}

53
StabilityMatrix.Avalonia/ViewLocator.cs

@ -1,47 +1,78 @@
using System; using System;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Controls.Templates; using Avalonia.Controls.Templates;
using StabilityMatrix.Avalonia.ViewModels; using FluentAvalonia.UI.Controls;
using StabilityMatrix.Avalonia.ViewModels.Base;
using StabilityMatrix.Core.Attributes; using StabilityMatrix.Core.Attributes;
namespace StabilityMatrix.Avalonia; namespace StabilityMatrix.Avalonia;
public class ViewLocator : IDataTemplate public class ViewLocator : IDataTemplate, INavigationPageFactory
{ {
/// <inheritdoc />
public Control Build(object? data) public Control Build(object? data)
{ {
if (data is null) throw new ArgumentNullException(nameof(data)); if (data is null) throw new ArgumentNullException(nameof(data));
var type = data.GetType(); var type = data.GetType();
if (type is null) throw new InvalidOperationException("Type is null");
if (Attribute.GetCustomAttribute(type, typeof(ViewAttribute)) is ViewAttribute viewAttr) if (Attribute.GetCustomAttribute(type, typeof(ViewAttribute)) is ViewAttribute viewAttr)
{ {
var viewType = viewAttr.GetViewType(); var viewType = viewAttr.GetViewType();
return GetView(viewType);
}
#pragma warning disable IL2072 return new TextBlock
// In design mode, just create a new instance of the view
if (Design.IsDesignMode)
{ {
return (Control) Activator.CreateInstance(viewType)!; Text = "View Model Not Found: " + data.GetType().FullName
};
} }
#pragma warning restore IL2072
private Control GetView(Type viewType)
{
// Otherwise get from the service provider // Otherwise get from the service provider
if (App.Services.GetService(viewType) is Control view) if (App.Services.GetService(viewType) is Control view)
{ {
return view; return view;
} }
}
return new TextBlock return new TextBlock
{ {
Text = "Not Found: " + data.GetType().FullName Text = "View Not Found: " + viewType.FullName
}; };
} }
/// <inheritdoc />
public bool Match(object? data) public bool Match(object? data)
{ {
return data is ViewModelBase; return data is ViewModelBase;
} }
/// <inheritdoc />
public Control? GetPage(Type srcType)
{
if (Attribute.GetCustomAttribute(srcType, typeof(ViewAttribute)) is ViewAttribute viewAttr)
{
var viewType = viewAttr.GetViewType();
var view = GetView(viewType);
view.DataContext ??= App.Services.GetService(srcType);
return view;
}
throw new InvalidOperationException("View not found for " + srcType.FullName);
}
/// <inheritdoc />
public Control GetPageFromObject(object target)
{
if (Attribute.GetCustomAttribute(target.GetType(), typeof(ViewAttribute)) is ViewAttribute viewAttr)
{
var viewType = viewAttr.GetViewType();
var view = GetView(viewType);
view.DataContext ??= target;
return view;
}
throw new InvalidOperationException("View not found for " + target.GetType().FullName);
}
} }

2
StabilityMatrix.Avalonia/ViewModels/PageViewModelBase.cs → StabilityMatrix.Avalonia/ViewModels/Base/PageViewModelBase.cs

@ -1,6 +1,6 @@
using FluentAvalonia.UI.Controls; using FluentAvalonia.UI.Controls;
namespace StabilityMatrix.Avalonia.ViewModels; namespace StabilityMatrix.Avalonia.ViewModels.Base;
/// <summary> /// <summary>
/// An abstract class for enabling page navigation. /// An abstract class for enabling page navigation.

4
StabilityMatrix.Avalonia/ViewModels/ProgressViewModel.cs → StabilityMatrix.Avalonia/ViewModels/Base/ProgressViewModel.cs

@ -1,11 +1,11 @@
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
namespace StabilityMatrix.Avalonia.ViewModels; namespace StabilityMatrix.Avalonia.ViewModels.Base;
/// <summary> /// <summary>
/// Generic view model for progress reporting. /// Generic view model for progress reporting.
/// </summary> /// </summary>
public partial class ProgressViewModel : ObservableObject public partial class ProgressViewModel : ViewModelBase
{ {
[ObservableProperty, NotifyPropertyChangedFor(nameof(IsTextVisible))] [ObservableProperty, NotifyPropertyChangedFor(nameof(IsTextVisible))]
private string? text; private string? text;

2
StabilityMatrix.Avalonia/ViewModels/ViewModelBase.cs → StabilityMatrix.Avalonia/ViewModels/Base/ViewModelBase.cs

@ -4,7 +4,7 @@ using AsyncAwaitBestPractices;
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
using StabilityMatrix.Avalonia.Models; using StabilityMatrix.Avalonia.Models;
namespace StabilityMatrix.Avalonia.ViewModels; namespace StabilityMatrix.Avalonia.ViewModels.Base;
public class ViewModelBase : ObservableValidator, IRemovableListItem public class ViewModelBase : ObservableValidator, IRemovableListItem
{ {

34
StabilityMatrix.Avalonia/ViewModels/CheckpointBrowserCardViewModel.cs → StabilityMatrix.Avalonia/ViewModels/CheckpointBrowser/CheckpointBrowserCardViewModel.cs

@ -16,6 +16,7 @@ using NLog;
using Octokit; using Octokit;
using StabilityMatrix.Avalonia.Controls; using StabilityMatrix.Avalonia.Controls;
using StabilityMatrix.Avalonia.Services; using StabilityMatrix.Avalonia.Services;
using StabilityMatrix.Avalonia.ViewModels.Base;
using StabilityMatrix.Avalonia.ViewModels.Dialogs; using StabilityMatrix.Avalonia.ViewModels.Dialogs;
using StabilityMatrix.Avalonia.Views.Dialogs; using StabilityMatrix.Avalonia.Views.Dialogs;
using StabilityMatrix.Core.Extensions; using StabilityMatrix.Core.Extensions;
@ -28,9 +29,9 @@ using StabilityMatrix.Core.Processes;
using StabilityMatrix.Core.Services; using StabilityMatrix.Core.Services;
using Notification = Avalonia.Controls.Notifications.Notification; using Notification = Avalonia.Controls.Notifications.Notification;
namespace StabilityMatrix.Avalonia.ViewModels; namespace StabilityMatrix.Avalonia.ViewModels.CheckpointBrowser;
public partial class CheckpointBrowserCardViewModel : ProgressViewModel public partial class CheckpointBrowserCardViewModel : Base.ProgressViewModel
{ {
private static readonly Logger Logger = LogManager.GetCurrentClassLogger(); private static readonly Logger Logger = LogManager.GetCurrentClassLogger();
@ -38,33 +39,38 @@ public partial class CheckpointBrowserCardViewModel : ProgressViewModel
private readonly ISettingsManager settingsManager; private readonly ISettingsManager settingsManager;
private readonly ServiceManager<ViewModelBase> dialogFactory; private readonly ServiceManager<ViewModelBase> dialogFactory;
private readonly INotificationService notificationService; private readonly INotificationService notificationService;
private readonly Action<CheckpointBrowserCardViewModel>? onDownloadStart;
public CivitModel CivitModel { get; init; } public Action<CheckpointBrowserCardViewModel>? OnDownloadStart { get; set; }
public CivitModel CivitModel
{
get => civitModel;
set
{
civitModel = value;
UpdateImage();
CheckIfInstalled();
}
}
public override bool IsTextVisible => Value > 0; public override bool IsTextVisible => Value > 0;
[ObservableProperty] private Uri? cardImage; [ObservableProperty] private Uri? cardImage;
[ObservableProperty] private bool isImporting; [ObservableProperty] private bool isImporting;
[ObservableProperty] private string updateCardText = string.Empty; [ObservableProperty] private string updateCardText = string.Empty;
[ObservableProperty] private bool showUpdateCard; [ObservableProperty] private bool showUpdateCard;
private CivitModel civitModel;
public CheckpointBrowserCardViewModel( public CheckpointBrowserCardViewModel(
CivitModel civitModel,
IDownloadService downloadService, IDownloadService downloadService,
ISettingsManager settingsManager, ISettingsManager settingsManager,
ServiceManager<ViewModelBase> dialogFactory, ServiceManager<ViewModelBase> dialogFactory,
INotificationService notificationService, INotificationService notificationService)
Action<CheckpointBrowserCardViewModel>? onDownloadStart = null)
{ {
this.downloadService = downloadService; this.downloadService = downloadService;
this.settingsManager = settingsManager; this.settingsManager = settingsManager;
this.dialogFactory = dialogFactory; this.dialogFactory = dialogFactory;
this.notificationService = notificationService; this.notificationService = notificationService;
this.onDownloadStart = onDownloadStart;
CivitModel = civitModel;
UpdateImage();
CheckIfInstalled();
// Update image when nsfw setting changes // Update image when nsfw setting changes
settingsManager.RegisterPropertyChangedHandler( settingsManager.RegisterPropertyChangedHandler(
@ -197,7 +203,7 @@ public partial class CheckpointBrowserCardViewModel : ProgressViewModel
IsImporting = true; IsImporting = true;
Text = "Downloading..."; Text = "Downloading...";
onDownloadStart?.Invoke(this); OnDownloadStart?.Invoke(this);
// Holds files to be deleted on errors // Holds files to be deleted on errors
var filesForCleanup = new HashSet<FilePath>(); var filesForCleanup = new HashSet<FilePath>();

35
StabilityMatrix.Avalonia/ViewModels/CheckpointBrowserViewModel.cs

@ -15,6 +15,8 @@ using FluentAvalonia.UI.Controls;
using NLog; using NLog;
using Refit; using Refit;
using StabilityMatrix.Avalonia.Services; using StabilityMatrix.Avalonia.Services;
using StabilityMatrix.Avalonia.ViewModels.Base;
using StabilityMatrix.Avalonia.ViewModels.CheckpointBrowser;
using StabilityMatrix.Avalonia.Views; using StabilityMatrix.Avalonia.Views;
using StabilityMatrix.Core.Api; using StabilityMatrix.Core.Api;
using StabilityMatrix.Core.Attributes; using StabilityMatrix.Core.Attributes;
@ -61,6 +63,7 @@ public partial class CheckpointBrowserViewModel : PageViewModelBase
[ObservableProperty] private bool isIndeterminate; [ObservableProperty] private bool isIndeterminate;
[ObservableProperty] private bool noResultsFound; [ObservableProperty] private bool noResultsFound;
[ObservableProperty] private string noResultsText = string.Empty; [ObservableProperty] private string noResultsText = string.Empty;
[ObservableProperty] private string selectedBaseModelType = "All";
private List<CheckpointBrowserCardViewModel> allModelCards = new(); private List<CheckpointBrowserCardViewModel> allModelCards = new();
@ -72,6 +75,8 @@ public partial class CheckpointBrowserViewModel : PageViewModelBase
.Where(t => t == CivitModelType.All || t.ConvertTo<SharedFolderType>() > 0) .Where(t => t == CivitModelType.All || t.ConvertTo<SharedFolderType>() > 0)
.OrderBy(t => t.ToString()); .OrderBy(t => t.ToString());
public List<string> BaseModelOptions => new() {"All", "SD 1.5", "SD 2.1", "SDXL 0.9", "SDXL 1.0"};
public CheckpointBrowserViewModel( public CheckpointBrowserViewModel(
ICivitApi civitApi, ICivitApi civitApi,
IDownloadService downloadService, IDownloadService downloadService,
@ -100,6 +105,7 @@ public partial class CheckpointBrowserViewModel : PageViewModelBase
SelectedPeriod = searchOptions?.SelectedPeriod ?? CivitPeriod.Month; SelectedPeriod = searchOptions?.SelectedPeriod ?? CivitPeriod.Month;
SortMode = searchOptions?.SortMode ?? CivitSortMode.HighestRated; SortMode = searchOptions?.SortMode ?? CivitSortMode.HighestRated;
SelectedModelType = searchOptions?.SelectedModelType ?? CivitModelType.Checkpoint; SelectedModelType = searchOptions?.SelectedModelType ?? CivitModelType.Checkpoint;
SelectedBaseModelType = searchOptions?.SelectedBaseModelType ?? "All";
ShowNsfw = settingsManager.Settings.ModelBrowserNsfwEnabled; ShowNsfw = settingsManager.Settings.ModelBrowserNsfwEnabled;
@ -228,13 +234,18 @@ public partial class CheckpointBrowserViewModel : PageViewModelBase
return cachedViewModel; return cachedViewModel;
} }
var newCard = new CheckpointBrowserCardViewModel(model, var newCard = dialogFactory.Get<CheckpointBrowserCardViewModel>(vm =>
downloadService, settingsManager, dialogFactory, notificationService, {
viewModel => vm.CivitModel = model;
vm.OnDownloadStart = viewModel =>
{ {
if (cache.Get(viewModel.CivitModel.Id) != null) return; if (cache.Get(viewModel.CivitModel.Id) != null) return;
cache.Add(viewModel.CivitModel.Id, viewModel); cache.Add(viewModel.CivitModel.Id, viewModel);
};
return vm;
}); });
return newCard; return newCard;
}).ToList(); }).ToList();
@ -294,6 +305,11 @@ public partial class CheckpointBrowserViewModel : PageViewModelBase
modelRequest.Types = new[] {SelectedModelType}; modelRequest.Types = new[] {SelectedModelType};
} }
if (SelectedBaseModelType != "All")
{
modelRequest.BaseModel = SelectedBaseModelType;
}
// See if query is cached // See if query is cached
var cachedQuery = await liteDbContext.CivitModelQueryCache var cachedQuery = await liteDbContext.CivitModelQueryCache
.IncludeAll() .IncludeAll()
@ -361,21 +377,28 @@ public partial class CheckpointBrowserViewModel : PageViewModelBase
{ {
TrySearchAgain().SafeFireAndForget(); TrySearchAgain().SafeFireAndForget();
settingsManager.Transaction(s => s.ModelSearchOptions = new ModelSearchOptions( settingsManager.Transaction(s => s.ModelSearchOptions = new ModelSearchOptions(
value, SortMode, SelectedModelType)); value, SortMode, SelectedModelType, SelectedBaseModelType));
} }
partial void OnSortModeChanged(CivitSortMode value) partial void OnSortModeChanged(CivitSortMode value)
{ {
TrySearchAgain().SafeFireAndForget(); TrySearchAgain().SafeFireAndForget();
settingsManager.Transaction(s => s.ModelSearchOptions = new ModelSearchOptions( settingsManager.Transaction(s => s.ModelSearchOptions = new ModelSearchOptions(
SelectedPeriod, value, SelectedModelType)); SelectedPeriod, value, SelectedModelType, SelectedBaseModelType));
} }
partial void OnSelectedModelTypeChanged(CivitModelType value) partial void OnSelectedModelTypeChanged(CivitModelType value)
{ {
TrySearchAgain().SafeFireAndForget(); TrySearchAgain().SafeFireAndForget();
settingsManager.Transaction(s => s.ModelSearchOptions = new ModelSearchOptions( settingsManager.Transaction(s => s.ModelSearchOptions = new ModelSearchOptions(
SelectedPeriod, SortMode, value)); SelectedPeriod, SortMode, value, SelectedBaseModelType));
}
partial void OnSelectedBaseModelTypeChanged(string value)
{
TrySearchAgain().SafeFireAndForget();
settingsManager.Transaction(s => s.ModelSearchOptions = new ModelSearchOptions(
SelectedPeriod, SortMode, SelectedModelType, value));
} }
private async Task TrySearchAgain(bool shouldUpdatePageNumber = true) private async Task TrySearchAgain(bool shouldUpdatePageNumber = true)

13
StabilityMatrix.Avalonia/ViewModels/CheckpointFile.cs → StabilityMatrix.Avalonia/ViewModels/CheckpointManager/CheckpointFile.cs

@ -10,13 +10,14 @@ using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input; using CommunityToolkit.Mvvm.Input;
using FluentAvalonia.UI.Controls; using FluentAvalonia.UI.Controls;
using NLog; using NLog;
using StabilityMatrix.Avalonia.ViewModels.Base;
using StabilityMatrix.Core.Extensions; using StabilityMatrix.Core.Extensions;
using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Helper;
using StabilityMatrix.Core.Models; using StabilityMatrix.Core.Models;
using StabilityMatrix.Core.Models.Progress; using StabilityMatrix.Core.Models.Progress;
using StabilityMatrix.Core.Processes; using StabilityMatrix.Core.Processes;
namespace StabilityMatrix.Avalonia.ViewModels; namespace StabilityMatrix.Avalonia.ViewModels.CheckpointManager;
public partial class CheckpointFile : ViewModelBase public partial class CheckpointFile : ViewModelBase
{ {
@ -46,7 +47,7 @@ public partial class CheckpointFile : ViewModelBase
[ObservableProperty] private bool isLoading; [ObservableProperty] private bool isLoading;
public string FileName => Path.GetFileName(FilePath); public string FileName => Path.GetFileName((string?) FilePath);
public ObservableCollection<string> Badges { get; set; } = new(); public ObservableCollection<string> Badges { get; set; } = new();
@ -78,8 +79,8 @@ public partial class CheckpointFile : ViewModelBase
throw new InvalidOperationException( throw new InvalidOperationException(
"Cannot get connected model info file path when FilePath is empty"); "Cannot get connected model info file path when FilePath is empty");
} }
var modelNameNoExt = Path.GetFileNameWithoutExtension(FilePath); var modelNameNoExt = Path.GetFileNameWithoutExtension((string?) FilePath);
var modelDir = Path.GetDirectoryName(FilePath) ?? ""; var modelDir = Path.GetDirectoryName((string?) FilePath) ?? "";
return Path.Combine(modelDir, $"{modelNameNoExt}.cm-info.json"); return Path.Combine(modelDir, $"{modelNameNoExt}.cm-info.json");
} }
@ -123,7 +124,7 @@ public partial class CheckpointFile : ViewModelBase
private async Task RenameAsync() private async Task RenameAsync()
{ {
// Parent folder path // Parent folder path
var parentPath = Path.GetDirectoryName(FilePath) ?? ""; var parentPath = Path.GetDirectoryName((string?) FilePath) ?? "";
var textFields = new TextBoxField[] var textFields = new TextBoxField[]
{ {
@ -158,7 +159,7 @@ public partial class CheckpointFile : ViewModelBase
// If preview image exists, rename it too // If preview image exists, rename it too
if (PreviewImagePath != null && File.Exists(PreviewImagePath)) if (PreviewImagePath != null && File.Exists(PreviewImagePath))
{ {
var newPreviewImagePath = Path.Combine(parentPath, $"{nameNoExt}.preview{Path.GetExtension(PreviewImagePath)}"); var newPreviewImagePath = Path.Combine(parentPath, $"{nameNoExt}.preview{Path.GetExtension((string?) PreviewImagePath)}");
File.Move(PreviewImagePath, newPreviewImagePath); File.Move(PreviewImagePath, newPreviewImagePath);
PreviewImagePath = newPreviewImagePath; PreviewImagePath = newPreviewImagePath;
} }

5
StabilityMatrix.Avalonia/ViewModels/CheckpointFolder.cs → StabilityMatrix.Avalonia/ViewModels/CheckpointManager/CheckpointFolder.cs

@ -12,6 +12,7 @@ using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input; using CommunityToolkit.Mvvm.Input;
using FluentAvalonia.UI.Controls; using FluentAvalonia.UI.Controls;
using StabilityMatrix.Avalonia.Models; using StabilityMatrix.Avalonia.Models;
using StabilityMatrix.Avalonia.ViewModels.Base;
using StabilityMatrix.Core.Extensions; using StabilityMatrix.Core.Extensions;
using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Helper;
using StabilityMatrix.Core.Models; using StabilityMatrix.Core.Models;
@ -20,7 +21,7 @@ using StabilityMatrix.Core.Models.Progress;
using StabilityMatrix.Core.Processes; using StabilityMatrix.Core.Processes;
using StabilityMatrix.Core.Services; using StabilityMatrix.Core.Services;
namespace StabilityMatrix.Avalonia.ViewModels; namespace StabilityMatrix.Avalonia.ViewModels.CheckpointManager;
public partial class CheckpointFolder : ViewModelBase public partial class CheckpointFolder : ViewModelBase
{ {
@ -73,7 +74,7 @@ public partial class CheckpointFolder : ViewModelBase
? $"{Title} ({CheckpointFiles.Count + SubFolders.Sum(folder => folder.CheckpointFiles.Count)})" ? $"{Title} ({CheckpointFiles.Count + SubFolders.Sum(folder => folder.CheckpointFiles.Count)})"
: Title; : Title;
public ProgressViewModel Progress { get; } = new(); public Base.ProgressViewModel Progress { get; } = new();
public CheckpointFolder? ParentFolder { get; init; } public CheckpointFolder? ParentFolder { get; init; }
public AdvancedObservableList<CheckpointFolder> SubFolders { get; init; } = new(); public AdvancedObservableList<CheckpointFolder> SubFolders { get; init; } = new();

15
StabilityMatrix.Avalonia/ViewModels/CheckpointsPageViewModel.cs

@ -9,6 +9,7 @@ using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input; using CommunityToolkit.Mvvm.Input;
using FluentAvalonia.UI.Controls; using FluentAvalonia.UI.Controls;
using NLog; using NLog;
using StabilityMatrix.Avalonia.ViewModels.Base;
using StabilityMatrix.Avalonia.Views; using StabilityMatrix.Avalonia.Views;
using StabilityMatrix.Core.Attributes; using StabilityMatrix.Core.Attributes;
using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Helper;
@ -51,10 +52,10 @@ public partial class CheckpointsPageViewModel : PageViewModelBase
} }
[ObservableProperty] [ObservableProperty]
private ObservableCollection<CheckpointFolder> checkpointFolders = new(); private ObservableCollection<CheckpointManager.CheckpointFolder> checkpointFolders = new();
[ObservableProperty] [ObservableProperty]
private ObservableCollection<CheckpointFolder> displayedCheckpointFolders = new(); private ObservableCollection<CheckpointManager.CheckpointFolder> displayedCheckpointFolders = new();
public CheckpointsPageViewModel( public CheckpointsPageViewModel(
ISharedFolders sharedFolders, ISharedFolders sharedFolders,
@ -105,10 +106,10 @@ public partial class CheckpointsPageViewModel : PageViewModelBase
folder.SearchFilter = SearchFilter; folder.SearchFilter = SearchFilter;
} }
DisplayedCheckpointFolders = new ObservableCollection<CheckpointFolder>(filteredFolders); DisplayedCheckpointFolders = new ObservableCollection<CheckpointManager.CheckpointFolder>(filteredFolders);
} }
private bool ContainsSearchFilter(CheckpointFolder folder) private bool ContainsSearchFilter(CheckpointManager.CheckpointFolder folder)
{ {
if (folder == null) if (folder == null)
throw new ArgumentNullException(nameof(folder)); throw new ArgumentNullException(nameof(folder));
@ -139,7 +140,7 @@ public partial class CheckpointsPageViewModel : PageViewModelBase
var indexTasks = folders.Select(f => Task.Run(async () => var indexTasks = folders.Select(f => Task.Run(async () =>
{ {
var checkpointFolder = var checkpointFolder =
new CheckpointFolder(settingsManager, downloadService, modelFinder) new CheckpointManager.CheckpointFolder(settingsManager, downloadService, modelFinder)
{ {
Title = Path.GetFileName(f), Title = Path.GetFileName(f),
DirectoryPath = f, DirectoryPath = f,
@ -153,13 +154,13 @@ public partial class CheckpointsPageViewModel : PageViewModelBase
// Set new observable collection, ordered by alphabetical order // Set new observable collection, ordered by alphabetical order
CheckpointFolders = CheckpointFolders =
new ObservableCollection<CheckpointFolder>(indexTasks new ObservableCollection<CheckpointManager.CheckpointFolder>(indexTasks
.Select(t => t.Result) .Select(t => t.Result)
.OrderBy(f => f.Title)); .OrderBy(f => f.Title));
if (!string.IsNullOrWhiteSpace(SearchFilter)) if (!string.IsNullOrWhiteSpace(SearchFilter))
{ {
DisplayedCheckpointFolders = new ObservableCollection<CheckpointFolder>( DisplayedCheckpointFolders = new ObservableCollection<CheckpointManager.CheckpointFolder>(
CheckpointFolders CheckpointFolders
.Where(x => x.CheckpointFiles.Any(y => y.FileName.Contains(SearchFilter)))); .Where(x => x.CheckpointFiles.Any(y => y.FileName.Contains(SearchFilter))));
} }

1
StabilityMatrix.Avalonia/ViewModels/Dialogs/CivitFileViewModel.cs

@ -1,5 +1,4 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.IO;
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
using StabilityMatrix.Core.Models.Api; using StabilityMatrix.Core.Models.Api;

1
StabilityMatrix.Avalonia/ViewModels/Dialogs/ContentDialogViewModelBase.cs

@ -1,5 +1,6 @@
using System; using System;
using FluentAvalonia.UI.Controls; using FluentAvalonia.UI.Controls;
using StabilityMatrix.Avalonia.ViewModels.Base;
namespace StabilityMatrix.Avalonia.ViewModels.Dialogs; namespace StabilityMatrix.Avalonia.ViewModels.Dialogs;

2
StabilityMatrix.Avalonia/ViewModels/Dialogs/ExceptionViewModel.cs

@ -1,5 +1,5 @@
using System; using System;
using CommunityToolkit.Mvvm.ComponentModel; using StabilityMatrix.Avalonia.ViewModels.Base;
using StabilityMatrix.Avalonia.Views.Dialogs; using StabilityMatrix.Avalonia.Views.Dialogs;
using StabilityMatrix.Core.Attributes; using StabilityMatrix.Core.Attributes;

2
StabilityMatrix.Avalonia/ViewModels/Dialogs/InstallerViewModel.cs

@ -75,7 +75,7 @@ public partial class InstallerViewModel : ContentDialogViewModelBase
[ObservableProperty] private string? installName; [ObservableProperty] private string? installName;
public ProgressViewModel InstallProgress { get; } = new(); public Base.ProgressViewModel InstallProgress { get; } = new();
public InstallerViewModel( public InstallerViewModel(
ISettingsManager settingsManager, ISettingsManager settingsManager,

1
StabilityMatrix.Avalonia/ViewModels/Dialogs/ModelVersionViewModel.cs

@ -3,7 +3,6 @@ using System.Collections.ObjectModel;
using System.Linq; using System.Linq;
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
using StabilityMatrix.Core.Models.Api; using StabilityMatrix.Core.Models.Api;
using StabilityMatrix.Core.Services;
namespace StabilityMatrix.Avalonia.ViewModels.Dialogs; namespace StabilityMatrix.Avalonia.ViewModels.Dialogs;

1
StabilityMatrix.Avalonia/ViewModels/Dialogs/OneClickInstallViewModel.cs

@ -5,6 +5,7 @@ using System.Threading.Tasks;
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input; using CommunityToolkit.Mvvm.Input;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using StabilityMatrix.Avalonia.ViewModels.Base;
using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Helper;
using StabilityMatrix.Core.Helper.Factory; using StabilityMatrix.Core.Helper.Factory;
using StabilityMatrix.Core.Models; using StabilityMatrix.Core.Models;

1
StabilityMatrix.Avalonia/ViewModels/Dialogs/SelectModelVersionViewModel.cs

@ -7,7 +7,6 @@ using Avalonia.Platform;
using Avalonia.Threading; using Avalonia.Threading;
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
using FluentAvalonia.UI.Controls; using FluentAvalonia.UI.Controls;
using StabilityMatrix.Core.Models.Api;
using StabilityMatrix.Core.Services; using StabilityMatrix.Core.Services;
namespace StabilityMatrix.Avalonia.ViewModels.Dialogs; namespace StabilityMatrix.Avalonia.ViewModels.Dialogs;

2
StabilityMatrix.Avalonia/ViewModels/Dialogs/UpdateViewModel.cs

@ -6,7 +6,6 @@ using System.Threading.Tasks;
using AsyncAwaitBestPractices; using AsyncAwaitBestPractices;
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input; using CommunityToolkit.Mvvm.Input;
using Python.Runtime;
using StabilityMatrix.Avalonia.Views.Dialogs; using StabilityMatrix.Avalonia.Views.Dialogs;
using StabilityMatrix.Core.Attributes; using StabilityMatrix.Core.Attributes;
using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Helper;
@ -14,7 +13,6 @@ using StabilityMatrix.Core.Models.Progress;
using StabilityMatrix.Core.Models.Update; using StabilityMatrix.Core.Models.Update;
using StabilityMatrix.Core.Services; using StabilityMatrix.Core.Services;
using StabilityMatrix.Core.Updater; using StabilityMatrix.Core.Updater;
using Dispatcher = Avalonia.Threading.Dispatcher;
namespace StabilityMatrix.Avalonia.ViewModels.Dialogs; namespace StabilityMatrix.Avalonia.ViewModels.Dialogs;

1
StabilityMatrix.Avalonia/ViewModels/FirstLaunchSetupViewModel.cs

@ -3,6 +3,7 @@ using System.Threading.Tasks;
using AsyncAwaitBestPractices; using AsyncAwaitBestPractices;
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
using StabilityMatrix.Avalonia.Styles; using StabilityMatrix.Avalonia.Styles;
using StabilityMatrix.Avalonia.ViewModels.Base;
using StabilityMatrix.Avalonia.Views; using StabilityMatrix.Avalonia.Views;
using StabilityMatrix.Core.Attributes; using StabilityMatrix.Core.Attributes;
using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Helper;

5
StabilityMatrix.Avalonia/ViewModels/LaunchPageViewModel.cs

@ -19,6 +19,7 @@ using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using StabilityMatrix.Avalonia.Controls; using StabilityMatrix.Avalonia.Controls;
using StabilityMatrix.Avalonia.Services; using StabilityMatrix.Avalonia.Services;
using StabilityMatrix.Avalonia.ViewModels.Base;
using StabilityMatrix.Avalonia.ViewModels.Dialogs; using StabilityMatrix.Avalonia.ViewModels.Dialogs;
using StabilityMatrix.Avalonia.Views; using StabilityMatrix.Avalonia.Views;
using StabilityMatrix.Avalonia.Views.Dialogs; using StabilityMatrix.Avalonia.Views.Dialogs;
@ -136,9 +137,7 @@ public partial class LaunchPageViewModel : PageViewModelBase, IDisposable, IAsyn
return; return;
} }
OnLoaded(); SelectedPackage = InstalledPackages.FirstOrDefault(x => x.Id == e);
if (SelectedPackage is null) return;
LaunchAsync().SafeFireAndForget(); LaunchAsync().SafeFireAndForget();
} }

17
StabilityMatrix.Avalonia/ViewModels/MainWindowViewModel.cs

@ -9,6 +9,7 @@ using CommunityToolkit.Mvvm.ComponentModel;
using FluentAvalonia.UI.Controls; using FluentAvalonia.UI.Controls;
using StabilityMatrix.Avalonia.Controls; using StabilityMatrix.Avalonia.Controls;
using StabilityMatrix.Avalonia.Services; using StabilityMatrix.Avalonia.Services;
using StabilityMatrix.Avalonia.ViewModels.Base;
using StabilityMatrix.Avalonia.ViewModels.Dialogs; using StabilityMatrix.Avalonia.ViewModels.Dialogs;
using StabilityMatrix.Avalonia.Views; using StabilityMatrix.Avalonia.Views;
using StabilityMatrix.Avalonia.Views.Dialogs; using StabilityMatrix.Avalonia.Views.Dialogs;
@ -61,8 +62,6 @@ public partial class MainWindowViewModel : ViewModelBase
// Set only if null, since this may be called again when content dialogs open // Set only if null, since this may be called again when content dialogs open
CurrentPage ??= Pages.FirstOrDefault(); CurrentPage ??= Pages.FirstOrDefault();
SelectedCategory ??= Pages.FirstOrDefault(); SelectedCategory ??= Pages.FirstOrDefault();
EventManager.Instance.PageChangeRequested += OnPageChangeRequested;
} }
public override async Task OnLoadedAsync() public override async Task OnLoadedAsync()
@ -194,18 +193,4 @@ public partial class MainWindowViewModel : ViewModelBase
await dialog.ShowAsync(); await dialog.ShowAsync();
} }
private void OnPageChangeRequested(object? sender, Type e)
{
CurrentPage = Pages.FirstOrDefault(p => p.GetType() == e);
SelectedCategory = Pages.FirstOrDefault(p => p.GetType() == e);
}
partial void OnSelectedCategoryChanged(object? value)
{
if (value is PageViewModelBase page)
{
CurrentPage = page;
}
}
} }

298
StabilityMatrix.Avalonia/ViewModels/PackageManager/PackageCardViewModel.cs

@ -0,0 +1,298 @@
using System;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Avalonia.Controls.Notifications;
using Avalonia.Threading;
using CommunityToolkit.Mvvm.ComponentModel;
using FluentAvalonia.UI.Controls;
using FluentAvalonia.UI.Media.Animation;
using NLog;
using Polly;
using StabilityMatrix.Avalonia.Services;
using StabilityMatrix.Core.Helper;
using StabilityMatrix.Core.Helper.Factory;
using StabilityMatrix.Core.Models;
using StabilityMatrix.Core.Models.Progress;
using StabilityMatrix.Core.Processes;
using StabilityMatrix.Core.Services;
namespace StabilityMatrix.Avalonia.ViewModels.PackageManager;
public partial class PackageCardViewModel : Base.ProgressViewModel
{
private readonly IPackageFactory packageFactory;
private readonly INotificationService notificationService;
private readonly ISettingsManager settingsManager;
private readonly INavigationService navigationService;
private readonly Logger logger = LogManager.GetCurrentClassLogger();
[ObservableProperty] private InstalledPackage? package;
[ObservableProperty] private Uri cardImage;
[ObservableProperty] private bool isUpdateAvailable;
[ObservableProperty] private string installedVersion;
public PackageCardViewModel(
IPackageFactory packageFactory,
INotificationService notificationService,
ISettingsManager settingsManager,
INavigationService navigationService)
{
this.packageFactory = packageFactory;
this.notificationService = notificationService;
this.settingsManager = settingsManager;
this.navigationService = navigationService;
}
partial void OnPackageChanged(InstalledPackage? value)
{
if (string.IsNullOrWhiteSpace(value?.PackageName))
return;
var basePackage = packageFactory[value.PackageName];
CardImage = basePackage?.PreviewImageUri ?? Assets.NoImage;
InstalledVersion = value.DisplayVersion ?? "Unknown";
}
public override async Task OnLoadedAsync()
{
IsUpdateAvailable = await HasUpdate();
}
public void Launch()
{
if (Package == null)
return;
settingsManager.Transaction(s => s.ActiveInstalledPackageId = Package.Id);
navigationService.NavigateTo<LaunchPageViewModel>(new EntranceNavigationTransitionInfo());
EventManager.Instance.OnPackageLaunchRequested(Package.Id);
}
public async Task Uninstall()
{
if (Package?.LibraryPath == null)
{
return;
}
var dialog = new ContentDialog
{
Title = "Are you sure?",
Content = "This will delete all folders in the package directory, including any generated images in that directory as well as any files you may have added.",
PrimaryButtonText = "Yes, delete it",
CloseButtonText = "No, keep it",
DefaultButton = ContentDialogButton.Primary
};
var result = await dialog.ShowAsync();
if (result == ContentDialogResult.Primary)
{
Text = "Uninstalling...";
IsIndeterminate = true;
Value = -1;
var deleteTask = DeleteDirectoryAsync(Path.Combine(settingsManager.LibraryDir,
Package.LibraryPath));
var taskResult = await notificationService.TryAsync(deleteTask,
"Some files could not be deleted. Please close any open files in the package directory and try again.");
if (taskResult.IsSuccessful)
{
notificationService.Show(new Notification("Success",
$"Package {Package.DisplayName} uninstalled",
NotificationType.Success));
settingsManager.Transaction(settings =>
{
settings.RemoveInstalledPackageAndUpdateActive(Package);
});
EventManager.Instance.OnInstalledPackagesChanged();
}
}
}
public async Task Update()
{
if (Package == null) return;
var basePackage = packageFactory[Package.PackageName!];
if (basePackage == null)
{
logger.Error("Could not find package {SelectedPackagePackageName}",
Package.PackageName);
return;
}
Text = $"Updating {Package.DisplayName}";
IsIndeterminate = true;
basePackage.InstallLocation = Package.FullPath!;
var progress = new Progress<ProgressReport>(progress =>
{
var percent = Convert.ToInt32(progress.Percentage);
Value = percent;
IsIndeterminate = progress.IsIndeterminate;
Text = $"Updating {Package.DisplayName}";
EventManager.Instance.OnGlobalProgressChanged(percent);
});
var updateResult = await basePackage.Update(Package, progress);
if (string.IsNullOrWhiteSpace(updateResult))
{
var errorMsg =
$"There was an error updating {Package.DisplayName}. Please try again later.";
if (Package.PackageName == "automatic")
{
errorMsg = errorMsg.Replace("Please try again later.",
"Please stash any changes before updating, or manually update the package.");
}
// there was an error
notificationService.Show(new Notification("Error updating package",
errorMsg, NotificationType.Error));
}
settingsManager.UpdatePackageVersionNumber(Package.Id, updateResult);
notificationService.Show("Update complete",
$"{Package.DisplayName} has been updated to the latest version.",
NotificationType.Success);
Dispatcher.UIThread.Invoke(() =>
{
using (settingsManager.BeginTransaction())
{
Package.UpdateAvailable = false;
}
IsUpdateAvailable = false;
IsIndeterminate = false;
InstalledVersion = settingsManager.Settings.InstalledPackages
.First(x => x.Id == Package.Id).DisplayVersion ?? "Unknown";
Value = 0;
});
}
public async Task OpenFolder()
{
if (string.IsNullOrWhiteSpace(Package?.FullPath))
return;
await ProcessRunner.OpenFolderBrowser(Package.FullPath);
}
private async Task<bool> HasUpdate()
{
if (Package == null)
return false;
var basePackage = packageFactory[Package.PackageName!];
if (basePackage == null)
return false;
var canCheckUpdate = Package.LastUpdateCheck == null ||
Package.LastUpdateCheck < DateTime.Now.AddMinutes(-15);
if (!canCheckUpdate)
{
return Package.UpdateAvailable;
}
try
{
var hasUpdate = await basePackage.CheckForUpdates(Package);
Package.UpdateAvailable = hasUpdate;
Package.LastUpdateCheck = DateTimeOffset.Now;
settingsManager.SetLastUpdateCheck(Package);
return hasUpdate;
}
catch (Exception e)
{
logger.Error(e, $"Error checking {Package.PackageName} for updates");
return false;
}
}
/// <summary>
/// Deletes a directory and all of its contents recursively.
/// Uses Polly to retry the deletion if it fails, up to 5 times with an exponential backoff.
/// </summary>
/// <param name="targetDirectory"></param>
private Task DeleteDirectoryAsync(string targetDirectory)
{
var policy = Policy.Handle<IOException>()
.WaitAndRetryAsync(3, attempt => TimeSpan.FromMilliseconds(50 * Math.Pow(2, attempt)),
onRetry: (exception, calculatedWaitDuration) =>
{
logger.Warn(
exception,
"Deletion of {TargetDirectory} failed. Retrying in {CalculatedWaitDuration}",
targetDirectory, calculatedWaitDuration);
});
return policy.ExecuteAsync(async () =>
{
await Task.Run(() =>
{
DeleteDirectory(targetDirectory);
});
});
}
private void DeleteDirectory(string targetDirectory)
{
// Skip if directory does not exist
if (!Directory.Exists(targetDirectory))
{
return;
}
// For junction points, delete with recursive false
if (new DirectoryInfo(targetDirectory).LinkTarget != null)
{
logger.Info("Removing junction point {TargetDirectory}", targetDirectory);
try
{
Directory.Delete(targetDirectory, false);
return;
}
catch (IOException ex)
{
throw new IOException($"Failed to delete junction point {targetDirectory}", ex);
}
}
// Recursively delete all subdirectories
var subdirectoryEntries = Directory.GetDirectories(targetDirectory);
foreach (var subdirectoryPath in subdirectoryEntries)
{
DeleteDirectory(subdirectoryPath);
}
// Delete all files in the directory
var fileEntries = Directory.GetFiles(targetDirectory);
foreach (var filePath in fileEntries)
{
try
{
File.SetAttributes(filePath, FileAttributes.Normal);
File.Delete(filePath);
}
catch (IOException ex)
{
throw new IOException($"Failed to delete file {filePath}", ex);
}
}
// Delete the target directory itself
try
{
Directory.Delete(targetDirectory, false);
}
catch (IOException ex)
{
throw new IOException($"Failed to delete directory {targetDirectory}", ex);
}
}
}

314
StabilityMatrix.Avalonia/ViewModels/PackageManagerViewModel.cs

@ -1,29 +1,22 @@
using System; using System;
using System.Collections.Immutable; using System.Collections.Immutable;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.IO;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using AsyncAwaitBestPractices; using AsyncAwaitBestPractices;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Controls.Notifications;
using Avalonia.Threading;
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using FluentAvalonia.UI.Controls; using FluentAvalonia.UI.Controls;
using Microsoft.Extensions.Logging;
using Polly;
using StabilityMatrix.Avalonia.Controls; using StabilityMatrix.Avalonia.Controls;
using StabilityMatrix.Avalonia.Services; using StabilityMatrix.Avalonia.Services;
using StabilityMatrix.Avalonia.ViewModels.Base;
using StabilityMatrix.Avalonia.ViewModels.Dialogs; using StabilityMatrix.Avalonia.ViewModels.Dialogs;
using StabilityMatrix.Avalonia.ViewModels.PackageManager;
using StabilityMatrix.Avalonia.Views; using StabilityMatrix.Avalonia.Views;
using StabilityMatrix.Avalonia.Views.Dialogs; using StabilityMatrix.Avalonia.Views.Dialogs;
using StabilityMatrix.Core.Attributes; using StabilityMatrix.Core.Attributes;
using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Helper;
using StabilityMatrix.Core.Helper.Factory; using StabilityMatrix.Core.Helper.Factory;
using StabilityMatrix.Core.Models;
using StabilityMatrix.Core.Models.Packages;
using StabilityMatrix.Core.Models.Progress;
using StabilityMatrix.Core.Services; using StabilityMatrix.Core.Services;
using Symbol = FluentIcons.Common.Symbol; using Symbol = FluentIcons.Common.Symbol;
using SymbolIconSource = FluentIcons.FluentAvalonia.SymbolIconSource; using SymbolIconSource = FluentIcons.FluentAvalonia.SymbolIconSource;
@ -37,53 +30,21 @@ namespace StabilityMatrix.Avalonia.ViewModels;
[View(typeof(PackageManagerPage))] [View(typeof(PackageManagerPage))]
public partial class PackageManagerViewModel : PageViewModelBase public partial class PackageManagerViewModel : PageViewModelBase
{ {
private readonly ILogger<PackageManagerViewModel> logger;
private readonly ISettingsManager settingsManager; private readonly ISettingsManager settingsManager;
private readonly IPackageFactory packageFactory; private readonly IPackageFactory packageFactory;
private readonly INotificationService notificationService;
private readonly ServiceManager<ViewModelBase> dialogFactory; private readonly ServiceManager<ViewModelBase> dialogFactory;
private const int MinutesToWaitForUpdateCheck = 60; public PackageManagerViewModel(ISettingsManager settingsManager, IPackageFactory packageFactory,
ServiceManager<ViewModelBase> dialogFactory)
public PackageManagerViewModel(ILogger<PackageManagerViewModel> logger,
ISettingsManager settingsManager, IPackageFactory packageFactory,
INotificationService notificationService, ServiceManager<ViewModelBase> dialogFactory)
{ {
this.logger = logger;
this.settingsManager = settingsManager; this.settingsManager = settingsManager;
this.packageFactory = packageFactory; this.packageFactory = packageFactory;
this.notificationService = notificationService;
this.dialogFactory = dialogFactory; this.dialogFactory = dialogFactory;
Packages = EventManager.Instance.InstalledPackagesChanged += OnInstalledPackagesChanged;
new ObservableCollection<InstalledPackage>(settingsManager.Settings.InstalledPackages);
SelectedPackage = Packages.FirstOrDefault();
} }
[ObservableProperty] [ObservableProperty] private ObservableCollection<PackageCardViewModel> packages;
private InstalledPackage? selectedPackage;
[ObservableProperty, NotifyPropertyChangedFor(nameof(ProgressBarVisibility))]
private int progressValue;
[ObservableProperty]
private string progressText = string.Empty;
[ObservableProperty, NotifyPropertyChangedFor(nameof(ProgressBarVisibility))]
private bool isIndeterminate;
[ObservableProperty]
private bool isUninstalling;
[ObservableProperty, NotifyPropertyChangedFor(nameof(SelectedPackage))]
private bool updateAvailable;
[ObservableProperty] private string installButtonText;
[ObservableProperty] private bool installButtonEnabled;
public bool ProgressBarVisibility => ProgressValue > 0 || IsIndeterminate;
public ObservableCollection<InstalledPackage> Packages { get; }
public override bool CanNavigateNext { get; protected set; } = true; public override bool CanNavigateNext { get; protected set; } = true;
public override bool CanNavigatePrevious { get; protected set; } public override bool CanNavigatePrevious { get; protected set; }
@ -92,258 +53,23 @@ public partial class PackageManagerViewModel : PageViewModelBase
public override async Task OnLoadedAsync() public override async Task OnLoadedAsync()
{ {
if (!Design.IsDesignMode) if (Design.IsDesignMode) return;
{
await CheckUpdates();
}
var installedPackages = settingsManager.Settings.InstalledPackages;
SelectedPackage = installedPackages.FirstOrDefault(x =>
x.Id == settingsManager.Settings.ActiveInstalledPackageId);
SelectedPackage ??= installedPackages.FirstOrDefault();
InstallButtonEnabled = SelectedPackage != null;
}
private async Task CheckUpdates()
{
var installedPackages = settingsManager.Settings.InstalledPackages; var installedPackages = settingsManager.Settings.InstalledPackages;
Packages.Clear(); Packages = new ObservableCollection<PackageCardViewModel>(installedPackages.Select(
foreach (var packageToUpdate in installedPackages) package => dialogFactory.Get<PackageCardViewModel>(vm =>
{
var basePackage = packageFactory.FindPackageByName(packageToUpdate.PackageName);
if (basePackage == null) continue;
var canCheckUpdate = packageToUpdate.LastUpdateCheck == null ||
packageToUpdate.LastUpdateCheck.Value.AddMinutes(MinutesToWaitForUpdateCheck) <
DateTimeOffset.Now;
if (canCheckUpdate)
{
var hasUpdate = await basePackage.CheckForUpdates(packageToUpdate);
packageToUpdate.UpdateAvailable = hasUpdate;
packageToUpdate.LastUpdateCheck = DateTimeOffset.Now;
settingsManager.SetLastUpdateCheck(packageToUpdate);
}
Packages.Add(packageToUpdate);
}
}
public async Task Launch()
{ {
if (SelectedPackage == null) return; vm.Package = package;
if (InstallButtonText == "Launch") return vm;
{ })));
settingsManager.Transaction(s => s.ActiveInstalledPackageId = SelectedPackage.Id);
EventManager.Instance.RequestPageChange(typeof(LaunchPageViewModel)); foreach (var package in Packages)
EventManager.Instance.OnPackageLaunchRequested(SelectedPackage.Id);
}
else
{ {
await UpdateSelectedPackage(); await package.OnLoadedAsync();
} }
} }
public async Task Uninstall() public async Task ShowInstallDialog()
{
// In design mode, just remove the package from the list
if (Design.IsDesignMode)
{
if (SelectedPackage != null)
{
Packages.Remove(SelectedPackage);
}
return;
}
if (SelectedPackage?.LibraryPath == null)
{
logger.LogError("No package selected to uninstall");
return;
}
var dialog = new ContentDialog
{
Title = "Are you sure?",
Content = "This will delete all folders in the package directory, including any generated images in that directory as well as any files you may have added.",
PrimaryButtonText = "Yes, delete it",
CloseButtonText = "No, keep it",
DefaultButton = ContentDialogButton.Primary
};
var result = await dialog.ShowAsync();
if (result == ContentDialogResult.Primary)
{
IsUninstalling = true;
InstallButtonEnabled = false;
var deleteTask = DeleteDirectoryAsync(Path.Combine(settingsManager.LibraryDir,
SelectedPackage.LibraryPath));
var taskResult = await notificationService.TryAsync(deleteTask,
"Some files could not be deleted. Please close any open files in the package directory and try again.");
if (taskResult.IsSuccessful)
{
notificationService.Show(new Notification("Success",
$"Package {SelectedPackage.DisplayName} uninstalled",
NotificationType.Success));
settingsManager.Transaction(settings =>
{
settings.RemoveInstalledPackageAndUpdateActive(SelectedPackage);
});
}
IsUninstalling = false;
InstallButtonEnabled = true;
await OnLoadedAsync();
}
}
partial void OnSelectedPackageChanged(InstalledPackage? value)
{
if (value is null) return;
UpdateAvailable = value.UpdateAvailable;
InstallButtonText = value.UpdateAvailable ? "Update" : "Launch";
}
/// <summary>
/// Deletes a directory and all of its contents recursively.
/// Uses Polly to retry the deletion if it fails, up to 5 times with an exponential backoff.
/// </summary>
/// <param name="targetDirectory"></param>
private Task DeleteDirectoryAsync(string targetDirectory)
{
var policy = Policy.Handle<IOException>()
.WaitAndRetryAsync(3, attempt => TimeSpan.FromMilliseconds(50 * Math.Pow(2, attempt)),
onRetry: (exception, calculatedWaitDuration) =>
{
logger.LogWarning(
exception,
"Deletion of {TargetDirectory} failed. Retrying in {CalculatedWaitDuration}",
targetDirectory, calculatedWaitDuration);
});
return policy.ExecuteAsync(async () =>
{
await Task.Run(() =>
{
DeleteDirectory(targetDirectory);
});
});
}
private void DeleteDirectory(string targetDirectory)
{
// Skip if directory does not exist
if (!Directory.Exists(targetDirectory))
{
return;
}
// For junction points, delete with recursive false
if (new DirectoryInfo(targetDirectory).LinkTarget != null)
{
logger.LogInformation("Removing junction point {TargetDirectory}", targetDirectory);
try
{
Directory.Delete(targetDirectory, false);
return;
}
catch (IOException ex)
{
throw new IOException($"Failed to delete junction point {targetDirectory}", ex);
}
}
// Recursively delete all subdirectories
var subdirectoryEntries = Directory.GetDirectories(targetDirectory);
foreach (var subdirectoryPath in subdirectoryEntries)
{
DeleteDirectory(subdirectoryPath);
}
// Delete all files in the directory
var fileEntries = Directory.GetFiles(targetDirectory);
foreach (var filePath in fileEntries)
{
try
{
File.SetAttributes(filePath, FileAttributes.Normal);
File.Delete(filePath);
}
catch (IOException ex)
{
throw new IOException($"Failed to delete file {filePath}", ex);
}
}
// Delete the target directory itself
try
{
Directory.Delete(targetDirectory, false);
}
catch (IOException ex)
{
throw new IOException($"Failed to delete directory {targetDirectory}", ex);
}
}
private async Task UpdateSelectedPackage()
{
if (SelectedPackage == null) return;
var installedPackage = SelectedPackage;
var package = packageFactory.FindPackageByName(installedPackage.PackageName);
if (package == null)
{
logger.LogError("Could not find package {SelectedPackagePackageName}",
installedPackage.PackageName);
return;
}
ProgressText = $"Updating {installedPackage.DisplayName} to latest version...";
package.InstallLocation = installedPackage.FullPath!;
var progress = new Progress<ProgressReport>(progress =>
{
var percent = Convert.ToInt32(progress.Percentage);
ProgressValue = percent;
IsIndeterminate = progress.IsIndeterminate;
ProgressText = $"Updating {SelectedPackage.DisplayName} to latest version... {percent:N0}%";
EventManager.Instance.OnGlobalProgressChanged(percent);
});
var updateResult = await package.Update(installedPackage, progress);
if (string.IsNullOrWhiteSpace(updateResult))
{
var errorMsg =
$"There was an error updating {installedPackage.DisplayName}. Please try again later.";
if (installedPackage.PackageName == "automatic")
{
errorMsg = errorMsg.Replace("Please try again later.",
"Please stash any changes before updating, or manually update the package.");
}
// there was an error
notificationService.Show(new Notification("Error updating package",
errorMsg, NotificationType.Error));
}
settingsManager.UpdatePackageVersionNumber(installedPackage.Id, updateResult);
notificationService.Show("Update complete",
$"{installedPackage.DisplayName} has been updated to the latest version.",
NotificationType.Success);
installedPackage.UpdateAvailable = false;
UpdateAvailable = false;
InstallButtonText = "Launch";
await OnLoadedAsync();
await DelayedClearProgress(TimeSpan.FromSeconds(3));
}
[RelayCommand]
private async Task ShowInstallDialog()
{ {
var viewModel = dialogFactory.Get<InstallerViewModel>(); var viewModel = dialogFactory.Get<InstallerViewModel>();
viewModel.AvailablePackages = packageFactory.GetAllAvailablePackages().ToImmutableArray(); viewModel.AvailablePackages = packageFactory.GetAllAvailablePackages().ToImmutableArray();
@ -367,12 +93,6 @@ public partial class PackageManagerViewModel : PageViewModelBase
await OnLoadedAsync(); await OnLoadedAsync();
} }
private async Task DelayedClearProgress(TimeSpan delay) private void OnInstalledPackagesChanged(object? sender, EventArgs e) =>
{ OnLoadedAsync().SafeFireAndForget();
await Task.Delay(delay);
ProgressText = string.Empty;
ProgressValue = 0;
IsIndeterminate = false;
}
} }

1
StabilityMatrix.Avalonia/ViewModels/ProgressItemViewModel.cs

@ -1,5 +1,6 @@
using System; using System;
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
using StabilityMatrix.Avalonia.ViewModels.Base;
using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Helper;
using StabilityMatrix.Core.Models.Progress; using StabilityMatrix.Core.Models.Progress;

1
StabilityMatrix.Avalonia/ViewModels/ProgressManagerViewModel.cs

@ -3,6 +3,7 @@ using System.Collections.ObjectModel;
using System.Linq; using System.Linq;
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
using FluentAvalonia.UI.Controls; using FluentAvalonia.UI.Controls;
using StabilityMatrix.Avalonia.ViewModels.Base;
using StabilityMatrix.Avalonia.Views; using StabilityMatrix.Avalonia.Views;
using StabilityMatrix.Core.Attributes; using StabilityMatrix.Core.Attributes;
using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Helper;

1
StabilityMatrix.Avalonia/ViewModels/RefreshBadgeViewModel.cs

@ -8,6 +8,7 @@ using FluentAvalonia.UI.Controls;
using NLog; using NLog;
using StabilityMatrix.Avalonia.Controls; using StabilityMatrix.Avalonia.Controls;
using StabilityMatrix.Avalonia.Styles; using StabilityMatrix.Avalonia.Styles;
using StabilityMatrix.Avalonia.ViewModels.Base;
using StabilityMatrix.Core.Attributes; using StabilityMatrix.Core.Attributes;
using StabilityMatrix.Core.Models.Progress; using StabilityMatrix.Core.Models.Progress;

1
StabilityMatrix.Avalonia/ViewModels/SettingsViewModel.cs

@ -26,6 +26,7 @@ using StabilityMatrix.Avalonia.Helpers;
using StabilityMatrix.Avalonia.Models; using StabilityMatrix.Avalonia.Models;
using StabilityMatrix.Avalonia.Models.TagCompletion; using StabilityMatrix.Avalonia.Models.TagCompletion;
using StabilityMatrix.Avalonia.Services; using StabilityMatrix.Avalonia.Services;
using StabilityMatrix.Avalonia.ViewModels.Base;
using StabilityMatrix.Avalonia.ViewModels.Dialogs; using StabilityMatrix.Avalonia.ViewModels.Dialogs;
using StabilityMatrix.Avalonia.Views; using StabilityMatrix.Avalonia.Views;
using StabilityMatrix.Avalonia.Views.Dialogs; using StabilityMatrix.Avalonia.Views.Dialogs;

39
StabilityMatrix.Avalonia/Views/CheckpointBrowserPage.axaml

@ -6,6 +6,8 @@
xmlns:viewModels="clr-namespace:StabilityMatrix.Avalonia.ViewModels" xmlns:viewModels="clr-namespace:StabilityMatrix.Avalonia.ViewModels"
xmlns:designData="clr-namespace:StabilityMatrix.Avalonia.DesignData" xmlns:designData="clr-namespace:StabilityMatrix.Avalonia.DesignData"
xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia" xmlns:ui="clr-namespace:FluentAvalonia.UI.Controls;assembly=FluentAvalonia"
xmlns:vm="clr-namespace:StabilityMatrix.Avalonia.ViewModels.CheckpointManager"
xmlns:checkpointBrowser="clr-namespace:StabilityMatrix.Avalonia.ViewModels.CheckpointBrowser"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="600" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="600"
x:DataType="viewModels:CheckpointBrowserViewModel" x:DataType="viewModels:CheckpointBrowserViewModel"
d:DataContext="{x:Static designData:DesignData.CheckpointBrowserViewModel}" d:DataContext="{x:Static designData:DesignData.CheckpointBrowserViewModel}"
@ -13,7 +15,7 @@
x:Class="StabilityMatrix.Avalonia.Views.CheckpointBrowserPage"> x:Class="StabilityMatrix.Avalonia.Views.CheckpointBrowserPage">
<UserControl.Resources> <UserControl.Resources>
<DataTemplate DataType="{x:Type viewModels:CheckpointBrowserCardViewModel}" x:Key="CivitModelTemplate"> <DataTemplate DataType="{x:Type checkpointBrowser:CheckpointBrowserCardViewModel}" x:Key="CivitModelTemplate">
<controls:Card <controls:Card
Margin="8" Margin="8"
MaxHeight="450" MaxHeight="450"
@ -65,13 +67,29 @@
VerticalAlignment="Top"> VerticalAlignment="Top">
<TextBlock <TextBlock
FontSize="10" FontSize="11"
FontWeight="Medium" FontWeight="Medium"
HorizontalAlignment="Center" HorizontalAlignment="Center"
Text="{Binding CivitModel.Type}" Text="{Binding CivitModel.Type}"
VerticalAlignment="Center" /> VerticalAlignment="Center" />
</controls:Card> </controls:Card>
<controls:Card
Classes="info"
Height="24"
HorizontalAlignment="Left"
Padding="4"
Margin="4,16,0,0"
VerticalAlignment="Top">
<TextBlock
FontSize="11"
FontWeight="Medium"
HorizontalAlignment="Center"
Text="{Binding CivitModel.BaseModelType}"
VerticalAlignment="Center" />
</controls:Card>
<controls:Card <controls:Card
Classes="success" Classes="success"
Height="24" Height="24"
@ -82,7 +100,7 @@
VerticalAlignment="Top"> VerticalAlignment="Top">
<TextBlock <TextBlock
FontSize="10" FontSize="11"
FontWeight="Medium" FontWeight="Medium"
HorizontalAlignment="Center" HorizontalAlignment="Center"
Text="{Binding UpdateCardText}" Text="{Binding UpdateCardText}"
@ -185,7 +203,7 @@
</Button> </Button>
</Grid> </Grid>
<DockPanel> <DockPanel>
<StackPanel Margin="8" Orientation="Vertical"> <StackPanel Margin="4, 8" Orientation="Vertical">
<Label Content="Sort" /> <Label Content="Sort" />
<ComboBox <ComboBox
ItemsSource="{Binding AllSortModes}" ItemsSource="{Binding AllSortModes}"
@ -193,7 +211,7 @@
SelectedItem="{Binding SortMode}" /> SelectedItem="{Binding SortMode}" />
</StackPanel> </StackPanel>
<StackPanel Margin="0,8" Orientation="Vertical"> <StackPanel Margin="4, 8" Orientation="Vertical">
<Label Content="Period" /> <Label Content="Period" />
<ComboBox <ComboBox
ItemsSource="{Binding AllCivitPeriods}" ItemsSource="{Binding AllCivitPeriods}"
@ -201,7 +219,7 @@
SelectedItem="{Binding SelectedPeriod}" /> SelectedItem="{Binding SelectedPeriod}" />
</StackPanel> </StackPanel>
<StackPanel Margin="8" Orientation="Vertical"> <StackPanel Margin="4, 8" Orientation="Vertical">
<Label Content="Model Type" /> <Label Content="Model Type" />
<ComboBox <ComboBox
ItemsSource="{Binding AllModelTypes}" ItemsSource="{Binding AllModelTypes}"
@ -209,6 +227,15 @@
SelectedItem="{Binding SelectedModelType}" /> SelectedItem="{Binding SelectedModelType}" />
</StackPanel> </StackPanel>
<StackPanel Margin="4, 8" Orientation="Vertical">
<Label Content="Base Model" />
<ComboBox
ItemsSource="{Binding BaseModelOptions}"
MinWidth="100"
SelectedItem="{Binding SelectedBaseModelType}" />
</StackPanel>
<CheckBox <CheckBox
Content="Show NSFW Content" Content="Show NSFW Content"
HorizontalAlignment="Right" HorizontalAlignment="Right"

7
StabilityMatrix.Avalonia/Views/CheckpointsPage.axaml

@ -7,6 +7,7 @@
xmlns:mocks="clr-namespace:StabilityMatrix.Avalonia.DesignData" xmlns:mocks="clr-namespace:StabilityMatrix.Avalonia.DesignData"
xmlns:controls="clr-namespace:StabilityMatrix.Avalonia.Controls" xmlns:controls="clr-namespace:StabilityMatrix.Avalonia.Controls"
xmlns:system="clr-namespace:System;assembly=System.Runtime" xmlns:system="clr-namespace:System;assembly=System.Runtime"
xmlns:checkpointManager="clr-namespace:StabilityMatrix.Avalonia.ViewModels.CheckpointManager"
d:DataContext="{x:Static mocks:DesignData.CheckpointsPageViewModel}" d:DataContext="{x:Static mocks:DesignData.CheckpointsPageViewModel}"
x:CompileBindings="True" x:CompileBindings="True"
x:DataType="vm:CheckpointsPageViewModel" x:DataType="vm:CheckpointsPageViewModel"
@ -40,7 +41,7 @@
</DataTemplate> </DataTemplate>
<!-- Checkpoint File Card --> <!-- Checkpoint File Card -->
<DataTemplate DataType="{x:Type vm:CheckpointFile}" x:Key="CheckpointFileDataTemplate"> <DataTemplate DataType="{x:Type checkpointManager:CheckpointFile}" x:Key="CheckpointFileDataTemplate">
<Border <Border
DragDrop.AllowDrop="False" DragDrop.AllowDrop="False"
Background="Transparent" Background="Transparent"
@ -203,7 +204,7 @@
</DataTemplate> </DataTemplate>
<!-- Checkpoint Folder Expander --> <!-- Checkpoint Folder Expander -->
<DataTemplate DataType="{x:Type vm:CheckpointFolder}" x:Key="CheckpointFolderGridDataTemplate"> <DataTemplate DataType="{x:Type checkpointManager:CheckpointFolder}" x:Key="CheckpointFolderGridDataTemplate">
<Expander <Expander
IsExpanded="{Binding IsExpanded}" IsExpanded="{Binding IsExpanded}"
Margin="8" Margin="8"
@ -399,7 +400,7 @@
<ui:FAMenuFlyout <ui:FAMenuFlyout
ItemsSource="{Binding CheckpointFolders}"> ItemsSource="{Binding CheckpointFolders}">
<ui:FAMenuFlyout.ItemTemplate> <ui:FAMenuFlyout.ItemTemplate>
<DataTemplate DataType="{x:Type vm:CheckpointFolder}"> <DataTemplate DataType="{x:Type checkpointManager:CheckpointFolder}">
<ui:ToggleMenuFlyoutItem <ui:ToggleMenuFlyoutItem
Text="{Binding TitleWithFilesCount}" Text="{Binding TitleWithFilesCount}"
IsChecked="{Binding IsCategoryEnabled, Mode=TwoWay}"/> IsChecked="{Binding IsCategoryEnabled, Mode=TwoWay}"/>

2
StabilityMatrix.Avalonia/Views/CheckpointsPage.axaml.cs

@ -2,7 +2,7 @@
using Avalonia.Input; using Avalonia.Input;
using Avalonia.Markup.Xaml; using Avalonia.Markup.Xaml;
using StabilityMatrix.Avalonia.Controls; using StabilityMatrix.Avalonia.Controls;
using StabilityMatrix.Avalonia.ViewModels; using CheckpointFolder = StabilityMatrix.Avalonia.ViewModels.CheckpointManager.CheckpointFolder;
namespace StabilityMatrix.Avalonia.Views; namespace StabilityMatrix.Avalonia.Views;

1
StabilityMatrix.Avalonia/Views/Dialogs/EnvVarsDialog.axaml

@ -10,6 +10,7 @@
d:DataContext="{x:Static mocks:DesignData.EnvVarsViewModel}" d:DataContext="{x:Static mocks:DesignData.EnvVarsViewModel}"
x:DataType="dialogs:EnvVarsViewModel" x:DataType="dialogs:EnvVarsViewModel"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
Focusable="True"
x:Class="StabilityMatrix.Avalonia.Views.Dialogs.EnvVarsDialog"> x:Class="StabilityMatrix.Avalonia.Views.Dialogs.EnvVarsDialog">
<controls:UserControlBase.Styles> <controls:UserControlBase.Styles>

19
StabilityMatrix.Avalonia/Views/Dialogs/SelectModelVersionDialog.axaml

@ -33,18 +33,27 @@
SelectedItem="{Binding SelectedVersionViewModel}"> SelectedItem="{Binding SelectedVersionViewModel}">
<ListBox.ItemTemplate> <ListBox.ItemTemplate>
<DataTemplate DataType="{x:Type dialogs:ModelVersionViewModel}"> <DataTemplate DataType="{x:Type dialogs:ModelVersionViewModel}">
<StackPanel Orientation="Horizontal" <Grid ColumnDefinitions="Auto, Auto"
RowDefinitions="Auto, Auto"
MinWidth="128" MinWidth="128"
Margin="0,4,0,4"> Margin="0,4,0,4">
<fluentAvalonia:SymbolIcon Symbol="Checkmark" <fluentAvalonia:SymbolIcon Grid.Row="0"
Grid.Column="0"
Grid.RowSpan="2"
Symbol="Checkmark"
Foreground="Lime" Foreground="Lime"
IsVisible="{Binding IsInstalled}" IsVisible="{Binding IsInstalled}"
VerticalAlignment="Center" /> VerticalAlignment="Center" />
<TextBlock Margin="4, 0" <TextBlock Grid.Row="0" Grid.Column="1"
Margin="4, 0"
Padding="4" Padding="4"
Text="{Binding ModelVersion.Name}" /> Text="{Binding ModelVersion.Name}" />
<TextBlock Grid.Row="1" Grid.Column="1"
</StackPanel> Margin="6, 0, 0, 0"
FontSize="11"
Padding="4, 0, 4, 4"
Text="{Binding ModelVersion.BaseModel}" />
</Grid>
</DataTemplate> </DataTemplate>
</ListBox.ItemTemplate> </ListBox.ItemTemplate>
<ListBox.Template> <ListBox.Template>

15
StabilityMatrix.Avalonia/Views/MainWindow.axaml

@ -2,11 +2,13 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:StabilityMatrix.Avalonia.ViewModels" xmlns:vm="using:StabilityMatrix.Avalonia.ViewModels"
xmlns:ui="using:FluentAvalonia.UI.Controls" xmlns:ui="using:FluentAvalonia.UI.Controls"
xmlns:local="using:StabilityMatrix.Avalonia"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mocks="clr-namespace:StabilityMatrix.Avalonia.DesignData" xmlns:mocks="clr-namespace:StabilityMatrix.Avalonia.DesignData"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:id="using:Dock.Avalonia" xmlns:id="using:Dock.Avalonia"
xmlns:controls="clr-namespace:StabilityMatrix.Avalonia.Controls" xmlns:controls="clr-namespace:StabilityMatrix.Avalonia.Controls"
xmlns:base="clr-namespace:StabilityMatrix.Avalonia.ViewModels.Base"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="550" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="550"
d:DataContext="{x:Static mocks:DesignData.MainWindowViewModel}" d:DataContext="{x:Static mocks:DesignData.MainWindowViewModel}"
x:DataType="vm:MainWindowViewModel" x:DataType="vm:MainWindowViewModel"
@ -56,19 +58,21 @@
<ui:NavigationView <ui:NavigationView
Grid.Row="1" Grid.Row="1"
Grid.RowSpan="2" Grid.RowSpan="2"
Name="NavigationView"
ItemInvoked="NavigationView_OnItemInvoked"
PaneDisplayMode="Left" PaneDisplayMode="Left"
IsPaneOpen="False" IsPaneOpen="False"
OpenPaneLength="200" OpenPaneLength="200"
IsSettingsVisible="False" IsSettingsVisible="False"
Content="{Binding CurrentPage}"
MenuItemsSource="{Binding Pages, Mode=OneWay}" MenuItemsSource="{Binding Pages, Mode=OneWay}"
FooterMenuItemsSource="{Binding FooterPages, Mode=OneWay}" FooterMenuItemsSource="{Binding FooterPages, Mode=OneWay}"
SelectedItem="{Binding SelectedCategory}"> SelectedItem="{Binding SelectedCategory}">
<ui:NavigationView.MenuItemTemplate> <ui:NavigationView.MenuItemTemplate>
<DataTemplate DataType="{x:Type vm:PageViewModelBase}"> <DataTemplate DataType="{x:Type base:PageViewModelBase}">
<ui:NavigationViewItem <ui:NavigationViewItem
Margin="0,0,0,0" Margin="0,0,0,0"
Tag="{Binding}"
IconSource="{Binding IconSource}" IconSource="{Binding IconSource}"
Content="{Binding Title}"/> Content="{Binding Title}"/>
</DataTemplate> </DataTemplate>
@ -127,6 +131,13 @@
</StackPanel> </StackPanel>
</ui:NavigationView.PaneFooter> </ui:NavigationView.PaneFooter>
<!-- Since the data context is set dynamically, cannot use compiled bindings -->
<ui:Frame Name="FrameView">
<ui:Frame.NavigationPageFactory>
<local:ViewLocator/>
</ui:Frame.NavigationPageFactory>
</ui:Frame>
</ui:NavigationView> </ui:NavigationView>
</Grid> </Grid>
</controls:AppWindowBase> </controls:AppWindowBase>

61
StabilityMatrix.Avalonia/Views/MainWindow.axaml.cs

@ -1,4 +1,5 @@
using System; using System;
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.IO; using System.IO;
using AsyncAwaitBestPractices; using AsyncAwaitBestPractices;
@ -6,6 +7,7 @@ using AsyncImageLoader;
using Avalonia; using Avalonia;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Controls.Notifications; using Avalonia.Controls.Notifications;
using Avalonia.Controls.Primitives;
using Avalonia.Input; using Avalonia.Input;
using Avalonia.Interactivity; using Avalonia.Interactivity;
using Avalonia.Media; using Avalonia.Media;
@ -15,11 +17,13 @@ using Avalonia.Threading;
using FluentAvalonia.Styling; using FluentAvalonia.Styling;
using FluentAvalonia.UI.Controls; using FluentAvalonia.UI.Controls;
using FluentAvalonia.UI.Media; using FluentAvalonia.UI.Media;
using FluentAvalonia.UI.Media.Animation;
using FluentAvalonia.UI.Windowing; using FluentAvalonia.UI.Windowing;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using StabilityMatrix.Avalonia.Controls; using StabilityMatrix.Avalonia.Controls;
using StabilityMatrix.Avalonia.Services; using StabilityMatrix.Avalonia.Services;
using StabilityMatrix.Avalonia.ViewModels; using StabilityMatrix.Avalonia.ViewModels;
using StabilityMatrix.Avalonia.ViewModels.Base;
using StabilityMatrix.Core.Processes; using StabilityMatrix.Core.Processes;
namespace StabilityMatrix.Avalonia.Views; namespace StabilityMatrix.Avalonia.Views;
@ -27,10 +31,22 @@ namespace StabilityMatrix.Avalonia.Views;
[SuppressMessage("ReSharper", "UnusedParameter.Local")] [SuppressMessage("ReSharper", "UnusedParameter.Local")]
public partial class MainWindow : AppWindowBase public partial class MainWindow : AppWindowBase
{ {
public INotificationService? NotificationService { get; set; } private readonly INotificationService notificationService;
private readonly INavigationService navigationService;
[DesignOnly(true)]
[SuppressMessage("ReSharper", "UnusedMember.Global")]
public MainWindow() public MainWindow()
{ {
notificationService = null!;
navigationService = null!;
}
public MainWindow(INotificationService notificationService, INavigationService navigationService)
{
this.notificationService = notificationService;
this.navigationService = navigationService;
InitializeComponent(); InitializeComponent();
#if DEBUG #if DEBUG
@ -41,6 +57,22 @@ public partial class MainWindow : AppWindowBase
TitleBar.TitleBarHitTestType = TitleBarHitTestType.Complex; TitleBar.TitleBarHitTestType = TitleBarHitTestType.Complex;
} }
/// <inheritdoc />
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
base.OnApplyTemplate(e);
navigationService.SetFrame(FrameView ?? throw new NullReferenceException("Frame not found"));
// Navigate to first page
if (DataContext is not MainWindowViewModel vm)
{
throw new NullReferenceException("DataContext is not MainWindowViewModel");
}
navigationService.NavigateTo(vm.Pages[0], new DrillInNavigationTransitionInfo());
}
protected override void OnOpened(EventArgs e) protected override void OnOpened(EventArgs e)
{ {
base.OnOpened(e); base.OnOpened(e);
@ -70,7 +102,7 @@ public partial class MainWindow : AppWindowBase
{ {
base.OnLoaded(e); base.OnLoaded(e);
// Initialize notification service using this window as the visual root // Initialize notification service using this window as the visual root
NotificationService?.Initialize(this); notificationService.Initialize(this);
// Attach error notification handler for image loader // Attach error notification handler for image loader
if (ImageLoader.AsyncImageLoader is FallbackRamCachedWebImageLoader loader) if (ImageLoader.AsyncImageLoader is FallbackRamCachedWebImageLoader loader)
@ -90,6 +122,29 @@ public partial class MainWindow : AppWindowBase
} }
} }
private void NavigationView_OnItemInvoked(object sender, NavigationViewItemInvokedEventArgs e)
{
if (e.InvokedItemContainer is NavigationViewItem nvi)
{
// Skip if this is the currently selected item
if (nvi.IsSelected)
{
return;
}
if (nvi.Tag is null)
{
throw new InvalidOperationException("NavigationViewItem Tag is null");
}
if (nvi.Tag is not ViewModelBase vm)
{
throw new InvalidOperationException($"NavigationViewItem Tag must be of type ViewModelBase, not {nvi.Tag?.GetType()}");
}
navigationService.NavigateTo(vm, e.RecommendedNavigationTransitionInfo);
}
}
private void OnActualThemeVariantChanged(object? sender, EventArgs e) private void OnActualThemeVariantChanged(object? sender, EventArgs e)
{ {
if (IsWindows11) if (IsWindows11)
@ -112,7 +167,7 @@ public partial class MainWindow : AppWindowBase
{ {
var fileName = Path.GetFileName(e.Url); var fileName = Path.GetFileName(e.Url);
var displayName = string.IsNullOrEmpty(fileName) ? e.Url : fileName; var displayName = string.IsNullOrEmpty(fileName) ? e.Url : fileName;
NotificationService?.ShowPersistent( notificationService.ShowPersistent(
"Failed to load image", "Failed to load image",
$"Could not load '{displayName}'\n({e.Exception.Message})", $"Could not load '{displayName}'\n({e.Exception.Message})",
NotificationType.Warning); NotificationType.Warning);

265
StabilityMatrix.Avalonia/Views/PackageManagerPage.axaml

@ -7,128 +7,191 @@
xmlns:models="clr-namespace:StabilityMatrix.Core.Models;assembly=StabilityMatrix.Core" xmlns:models="clr-namespace:StabilityMatrix.Core.Models;assembly=StabilityMatrix.Core"
xmlns:controls="clr-namespace:StabilityMatrix.Avalonia.Controls" xmlns:controls="clr-namespace:StabilityMatrix.Avalonia.Controls"
xmlns:designData="clr-namespace:StabilityMatrix.Avalonia.DesignData" xmlns:designData="clr-namespace:StabilityMatrix.Avalonia.DesignData"
xmlns:packageManager="clr-namespace:StabilityMatrix.Avalonia.ViewModels.PackageManager"
xmlns:faicon="clr-namespace:Projektanker.Icons.Avalonia;assembly=Projektanker.Icons.Avalonia"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
x:DataType="viewModels:PackageManagerViewModel" x:DataType="viewModels:PackageManagerViewModel"
x:CompileBindings="True" x:CompileBindings="True"
d:DataContext="{x:Static designData:DesignData.PackageManagerViewModel}" d:DataContext="{x:Static designData:DesignData.PackageManagerViewModel}"
x:Class="StabilityMatrix.Avalonia.Views.PackageManagerPage"> x:Class="StabilityMatrix.Avalonia.Views.PackageManagerPage">
<Grid Margin="16" RowDefinitions="Auto,*"> <Grid Margin="16" RowDefinitions="Auto,*,Auto">
<StackPanel <!-- Title -->
Grid.Row="0" <TextBlock Grid.Row="0" Text="{Binding Title}"
Orientation="Vertical" Margin="0,0,0,16"
IsVisible="{Binding ProgressBarVisibility, FallbackValue=True}"> FontSize="24"/>
<ProgressBar <!-- Cards -->
IsIndeterminate="{Binding IsIndeterminate, FallbackValue=False}" <ScrollViewer Grid.Row="1">
Maximum="100" <ItemsRepeater
Value="{Binding ProgressValue, FallbackValue=10}" ItemsSource="{Binding Packages}">
Width="500" /> <ItemsRepeater.Layout>
<TextBlock <UniformGridLayout MinColumnSpacing="12" MinRowSpacing="12" />
HorizontalAlignment="Center" </ItemsRepeater.Layout>
Padding="8" <ItemsRepeater.ItemTemplate>
Text="{Binding ProgressText, FallbackValue=Installing...}" /> <DataTemplate DataType="{x:Type packageManager:PackageCardViewModel}">
</StackPanel> <controls:Card Padding="8">
<Grid RowDefinitions="Auto, Auto, Auto, Auto"
ColumnDefinitions="Auto">
<StackPanel <TextBlock Grid.Row="0"
Grid.Row="1" Grid.Column="0"
HorizontalAlignment="Left" Text="{Binding Package.DisplayName}"
Margin="16" TextAlignment="Left" />
Orientation="Horizontal">
<StackPanel Orientation="Vertical">
<Button <Button
Command="{Binding ShowInstallDialogCommand}" Grid.Row="0"
Height="40" Grid.RowSpan="2"
HorizontalAlignment="Stretch" HorizontalContentAlignment="Right"
Margin="0,0,0,16" HorizontalAlignment="Right"
Classes="success" Margin="8,-8,0,4"
VerticalContentAlignment="Center"> Classes="transparent"
<StackPanel Orientation="Horizontal"> Width="24"
<ui:SymbolIcon Symbol="Add" /> BorderThickness="0">
<Label <ui:SymbolIcon FontSize="18" Symbol="MoreVertical" />
Content="Add Package" <Button.Flyout>
Margin="8,0,0,0" <MenuFlyout Placement="BottomEdgeAlignedLeft">
VerticalContentAlignment="Center" /> <MenuItem Header="Check for Update"
</StackPanel> Command="{Binding OnLoadedAsync}">
<MenuItem.Icon>
<ui:SymbolIcon Symbol="Refresh" />
</MenuItem.Icon>
</MenuItem>
<MenuItem Header="Open in Explorer"
Command="{Binding OpenFolder}">
<MenuItem.Icon>
<ui:SymbolIcon Symbol="OpenFolder" />
</MenuItem.Icon>
</MenuItem>
<Separator />
<MenuItem Header="Uninstall"
Command="{Binding Uninstall}">
<MenuItem.Icon>
<ui:SymbolIcon Symbol="Delete" />
</MenuItem.Icon>
</MenuItem>
</MenuFlyout>
</Button.Flyout>
</Button> </Button>
<ListBox <TextBlock Grid.Row="1"
ItemsSource="{Binding Packages}" Grid.Column="0"
SelectedItem="{Binding SelectedPackage, Mode=TwoWay}" Margin="2,0,0,0"
IsVisible="{Binding !!Packages.Count}"> VerticalAlignment="Center"
Text="{Binding InstalledVersion}" />
<ListBox.Template> <controls:BetterAdvancedImage
<ControlTemplate> Grid.Row="2"
<Border Grid.Column="0"
BorderBrush="#FFCCCCCC" Margin="0,8,0,8"
BorderThickness="1" Height="160"
CornerRadius="5"> Width="200"
<ItemsPresenter /> CornerRadius="4"
</Border> HorizontalAlignment="Center"
</ControlTemplate> VerticalContentAlignment="Top"
</ListBox.Template> HorizontalContentAlignment="Center"
Source="{Binding CardImage}"
Stretch="UniformToFill" />
<ListBox.ItemTemplate> <Grid Grid.Row="3" Grid.Column="0"
<DataTemplate DataType="{x:Type models:InstalledPackage}"> IsVisible="{Binding IsUpdateAvailable}"
<StackPanel Margin="8" VerticalAlignment="Top"> ColumnDefinitions="*, *">
<TextBlock Margin="0,5,0,5" Text="{Binding DisplayName}" /> <Button Grid.Column="0" Classes="accent"
<TextBlock Margin="0,0,0,5" Text="{Binding DisplayVersion}" /> VerticalAlignment="Bottom"
HorizontalAlignment="Stretch"
Command="{Binding Launch}">
<StackPanel Orientation="Horizontal" Margin="0,2,0,2">
<faicon:Icon Value="fa-solid fa-rocket"
Margin="0,0,8,0"
FontSize="14" />
<TextBlock Text="Launch" />
</StackPanel> </StackPanel>
</DataTemplate> </Button>
</ListBox.ItemTemplate> <Button Grid.Column="1" Classes="accent"
</ListBox> VerticalAlignment="Bottom"
HorizontalAlignment="Stretch"
Command="{Binding Update}">
<StackPanel Orientation="Horizontal" Margin="0,2,0,2">
<faicon:Icon Value="fa-solid fa-download"
Margin="0,0,8,0"
FontSize="14" />
<TextBlock Text="Update" />
</StackPanel> </StackPanel>
</Button>
</Grid>
<StackPanel Margin="32,0,0,0" Orientation="Vertical"> <!-- Big launch button -->
<TextBlock <Button Grid.Row="3" Grid.Column="0" Classes="accent"
FontSize="24" VerticalAlignment="Bottom"
FontWeight="Bold" IsVisible="{Binding !IsUpdateAvailable}"
Text="{Binding SelectedPackage.DisplayName,
FallbackValue='Click &quot;Add Package&quot; to install a package'}" />
<TextBlock FontSize="12" Margin="0,5,0,5">
<Run Text="{Binding SelectedPackage.PackageName, FallbackValue=''}" />
<Run Text="{Binding SelectedPackage.DisplayVersion, FallbackValue=''}" />
</TextBlock>
<TextBlock
FontSize="12"
Margin="0,5,0,5"
Text="{Binding SelectedPackage.FullPath, FallbackValue=''}" />
<UniformGrid
Margin="0,16,0,0"
Columns="2"
MinHeight="48"
MinWidth="208"
HorizontalAlignment="Left">
<Button
Command="{Binding Launch}" Command="{Binding Launch}"
Content="{Binding InstallButtonText}" HorizontalAlignment="Stretch">
HorizontalAlignment="Stretch" <StackPanel Orientation="Horizontal" Margin="0,2,0,2">
VerticalAlignment="Stretch" <faicon:Icon Value="fa-solid fa-rocket"
Margin="0,0,8,0" Margin="0,0,8,0"
IsEnabled="{Binding InstallButtonEnabled}" FontSize="14" />
IsVisible="{Binding InstallButtonEnabled}" <TextBlock Text="Launch" />
Classes="success"/> </StackPanel>
<Button </Button>
Command="{Binding Uninstall}"
Content="Uninstall" <!-- Update overlay -->
<Border
Grid.Row="0" Grid.RowSpan="4"
Grid.Column="0"
Background="#DD000000"
CornerRadius="4"
HorizontalAlignment="Stretch" HorizontalAlignment="Stretch"
VerticalAlignment="Stretch" VerticalAlignment="Stretch"
Margin="8,0,0,0" IsVisible="{Binding IsProgressVisible}" />
Classes="danger" <Grid Grid.Row="0" Grid.RowSpan="4"
IsEnabled="{Binding !IsUninstalling}" Grid.Column="0"
IsVisible="{Binding InstallButtonEnabled}"/> HorizontalAlignment="Stretch"
</UniformGrid>
<StackPanel Orientation="Horizontal" IsVisible="{Binding IsUninstalling}">
<controls:ProgressRing Height="24" Width="24"
Margin="8, 16, 8, 8"
VerticalAlignment="Center" VerticalAlignment="Center"
BorderThickness="4" RowDefinitions="Auto, *"
IsIndeterminate="True" HorizontalAlignment="Left" /> IsVisible="{Binding IsProgressVisible}">
<TextBlock Text="Uninstalling..." VerticalAlignment="Center" Margin="0,8,0,0" /> <controls:ProgressRing
</StackPanel> HorizontalAlignment="Center"
</StackPanel> IsIndeterminate="{Binding IsIndeterminate}"
Width="120"
Height="120"
StartAngle="90"
EndAngle="450"
Value="{Binding Value}"
VerticalAlignment="Center" />
<TextBlock Grid.Row="1"
HorizontalAlignment="Center"
Margin="8,8,8,0"
TextAlignment="Center"
Width="180"
Text="{Binding Text, TargetNullValue=Updating so and so to the latest version... 5%}"
TextWrapping="Wrap"
VerticalAlignment="Center" />
</Grid>
</Grid>
</controls:Card>
</DataTemplate>
</ItemsRepeater.ItemTemplate>
</ItemsRepeater>
</ScrollViewer>
<!-- Teaching Tip -->
<ui:TeachingTip Grid.Row="0" Name="TeachingTip1"
Target="{Binding #AddPackagesButton}"
Title="Add a package to get started!"
PreferredPlacement="Top"
IsOpen="{Binding !Packages.Count}"/>
<!-- Add Packages Button -->
<Button Grid.Row="2"
Classes="transparent"
VerticalAlignment="Bottom"
Name="AddPackagesButton"
Margin="0, 8, 0, 0"
HorizontalAlignment="Stretch"
Command="{Binding ShowInstallDialog}">
<StackPanel Orientation="Horizontal" Margin="8">
<ui:SymbolIcon FontSize="18" Symbol="Add" />
<TextBlock Margin="4,0,0,0" Text="Add Package" />
</StackPanel> </StackPanel>
</Button>
</Grid> </Grid>
</controls:UserControlBase> </controls:UserControlBase>

1
StabilityMatrix.Core/Database/LiteDbContext.cs

@ -115,7 +115,6 @@ public class LiteDbContext : ILiteDbContext
public void Dispose() public void Dispose()
{ {
database?.UnderlyingDatabase.Dispose();
database?.Dispose(); database?.Dispose();
GC.SuppressFinalize(this); GC.SuppressFinalize(this);
} }

4
StabilityMatrix.Core/Extensions/StringExtensions.cs

@ -1,6 +1,4 @@
using System.CodeDom; using System.Text;
using System.CodeDom.Compiler;
using System.Text;
namespace StabilityMatrix.Core.Extensions; namespace StabilityMatrix.Core.Extensions;

1
StabilityMatrix.Core/Helper/Compat.cs

@ -4,7 +4,6 @@ using System.Reflection;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Runtime.Versioning; using System.Runtime.Versioning;
using Semver; using Semver;
using Sentry.Protocol;
using StabilityMatrix.Core.Models.FileInterfaces; using StabilityMatrix.Core.Models.FileInterfaces;
namespace StabilityMatrix.Core.Helper; namespace StabilityMatrix.Core.Helper;

4
StabilityMatrix.Core/Helper/EventManager.cs

@ -16,19 +16,17 @@ public class EventManager
} }
public event EventHandler<int>? GlobalProgressChanged; public event EventHandler<int>? GlobalProgressChanged;
public event EventHandler<Type>? PageChangeRequested;
public event EventHandler? InstalledPackagesChanged; public event EventHandler? InstalledPackagesChanged;
public event EventHandler<bool>? OneClickInstallFinished; public event EventHandler<bool>? OneClickInstallFinished;
public event EventHandler? TeachingTooltipNeeded; public event EventHandler? TeachingTooltipNeeded;
public event EventHandler<bool>? DevModeSettingChanged; public event EventHandler<bool>? DevModeSettingChanged;
public event EventHandler<UpdateInfo>? UpdateAvailable; public event EventHandler<UpdateInfo>? UpdateAvailable;
public event EventHandler<Guid> PackageLaunchRequested; public event EventHandler<Guid>? PackageLaunchRequested;
public event EventHandler? ScrollToBottomRequested; public event EventHandler? ScrollToBottomRequested;
public event EventHandler<ProgressItem>? ProgressChanged; public event EventHandler<ProgressItem>? ProgressChanged;
public event EventHandler<RunningPackageStatusChangedEventArgs>? RunningPackageStatusChanged; public event EventHandler<RunningPackageStatusChangedEventArgs>? RunningPackageStatusChanged;
public void OnGlobalProgressChanged(int progress) => GlobalProgressChanged?.Invoke(this, progress); public void OnGlobalProgressChanged(int progress) => GlobalProgressChanged?.Invoke(this, progress);
public void RequestPageChange(Type pageType) => PageChangeRequested?.Invoke(this, pageType);
public void OnInstalledPackagesChanged() => InstalledPackagesChanged?.Invoke(this, EventArgs.Empty); public void OnInstalledPackagesChanged() => InstalledPackagesChanged?.Invoke(this, EventArgs.Empty);
public void OnOneClickInstallFinished(bool skipped) => OneClickInstallFinished?.Invoke(this, skipped); public void OnOneClickInstallFinished(bool skipped) => OneClickInstallFinished?.Invoke(this, skipped);
public void OnTeachingTooltipNeeded() => TeachingTooltipNeeded?.Invoke(this, EventArgs.Empty); public void OnTeachingTooltipNeeded() => TeachingTooltipNeeded?.Invoke(this, EventArgs.Empty);

1
StabilityMatrix.Core/Helper/IPrerequisiteHelper.cs

@ -1,6 +1,5 @@
using System.Runtime.Versioning; using System.Runtime.Versioning;
using StabilityMatrix.Core.Models.Progress; using StabilityMatrix.Core.Models.Progress;
using StabilityMatrix.Core.Processes;
namespace StabilityMatrix.Core.Helper; namespace StabilityMatrix.Core.Helper;

7
StabilityMatrix.Core/Helper/SharedFolders.cs

@ -147,7 +147,7 @@ public class SharedFolders : ISharedFolders
} }
} }
private static void RemoveLinksForPackage(BasePackage package, DirectoryPath installPath) public static void RemoveLinksForPackage(BasePackage package, DirectoryPath installPath)
{ {
var sharedFolders = package.SharedFolders; var sharedFolders = package.SharedFolders;
if (sharedFolders == null) if (sharedFolders == null)
@ -171,7 +171,6 @@ public class SharedFolders : ISharedFolders
public void RemoveLinksForAllPackages() public void RemoveLinksForAllPackages()
{ {
var libraryDir = settingsManager.LibraryDir;
var packages = settingsManager.Settings.InstalledPackages; var packages = settingsManager.Settings.InstalledPackages;
foreach (var package in packages) foreach (var package in packages)
{ {
@ -182,11 +181,11 @@ public class SharedFolders : ISharedFolders
try try
{ {
RemoveLinksForPackage(basePackage, Path.Combine(libraryDir, package.LibraryPath)); basePackage.RemoveModelFolderLinks(package.FullPath).GetAwaiter().GetResult();
} }
catch (Exception e) catch (Exception e)
{ {
Logger.Warn("Failed to remove junction links for package {Package} " + Logger.Warn("Failed to remove links for package {Package} " +
"({DisplayName}): {Message}", package.PackageName, package.DisplayName, e.Message); "({DisplayName}): {Message}", package.PackageName, package.DisplayName, e.Message);
} }
} }

4
StabilityMatrix.Core/Models/Api/CivitModel.cs

@ -59,4 +59,8 @@ public class CivitModel
public string LatestModelVersionName => ModelVersions != null && ModelVersions.Any() public string LatestModelVersionName => ModelVersions != null && ModelVersions.Any()
? ModelVersions[0].Name ? ModelVersions[0].Name
: string.Empty; : string.Empty;
public string? BaseModelType => ModelVersions != null && ModelVersions.Any()
? ModelVersions[0].BaseModel?.Replace("SD", "").Trim()
: string.Empty;
} }

15
StabilityMatrix.Core/Models/Api/CivitModelsRequest.cs

@ -102,4 +102,19 @@ public class CivitModelsRequest
/// </summary> /// </summary>
[AliasAs("nsfw")] [AliasAs("nsfw")]
public string? Nsfw { get; set; } public string? Nsfw { get; set; }
/// <summary>
/// options: <br/>
/// SD 1.4 <br/>
/// SD 1.5 <br/>
/// SD 2.0 <br/>
/// SD 2.0 768 <br/>
/// SD 2.1 <br/>
/// SD 2.1 768 <br/>
/// SD 2.1 Unclip <br/>
/// SDXL 0.9 <br/>
/// SDXL 1.0
/// </summary>
[AliasAs("baseModels")]
public string? BaseModel { get; set; }
} }

2
StabilityMatrix.Core/Models/InstalledPackage.cs

@ -38,7 +38,7 @@ public class InstalledPackage
public List<LaunchOption>? LaunchArgs { get; set; } public List<LaunchOption>? LaunchArgs { get; set; }
public DateTimeOffset? LastUpdateCheck { get; set; } public DateTimeOffset? LastUpdateCheck { get; set; }
[JsonIgnore] public bool UpdateAvailable { get; set; } public bool UpdateAvailable { get; set; }
/// <summary> /// <summary>
/// Get the path as a relative sub-path of the relative path. /// Get the path as a relative sub-path of the relative path.

9
StabilityMatrix.Core/Models/Packages/A3WebUI.cs

@ -1,5 +1,4 @@
using System.Diagnostics; using System.Diagnostics.CodeAnalysis;
using System.Diagnostics.CodeAnalysis;
using System.Text.Json.Nodes; using System.Text.Json.Nodes;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using NLog; using NLog;
@ -207,4 +206,10 @@ public class A3WebUI : BaseGitPackage
await StabilityMatrix.Core.Helper.SharedFolders.UpdateLinksForPackage(this, await StabilityMatrix.Core.Helper.SharedFolders.UpdateLinksForPackage(this,
SettingsManager.ModelsDirectory, installDirectory).ConfigureAwait(false); SettingsManager.ModelsDirectory, installDirectory).ConfigureAwait(false);
} }
public override Task RemoveModelFolderLinks(DirectoryPath installDirectory)
{
StabilityMatrix.Core.Helper.SharedFolders.RemoveLinksForPackage(this, installDirectory);
return Task.CompletedTask;
}
} }

1
StabilityMatrix.Core/Models/Packages/BasePackage.cs

@ -39,6 +39,7 @@ public abstract class BasePackage
public abstract Task RunPackage(string installedPackagePath, string command, string arguments); public abstract Task RunPackage(string installedPackagePath, string command, string arguments);
public abstract Task SetupModelFolders(DirectoryPath installDirectory); public abstract Task SetupModelFolders(DirectoryPath installDirectory);
public abstract Task UpdateModelFolders(DirectoryPath installDirectory); public abstract Task UpdateModelFolders(DirectoryPath installDirectory);
public abstract Task RemoveModelFolderLinks(DirectoryPath installDirectory);
/// <summary> /// <summary>
/// Shuts down the subprocess, canceling any pending streams. /// Shuts down the subprocess, canceling any pending streams.

3
StabilityMatrix.Core/Models/Packages/ComfyUI.cs

@ -225,6 +225,9 @@ public class ComfyUI : BaseGitPackage
public override Task UpdateModelFolders(DirectoryPath installDirectory) => public override Task UpdateModelFolders(DirectoryPath installDirectory) =>
SetupModelFolders(installDirectory); SetupModelFolders(installDirectory);
public override Task RemoveModelFolderLinks(DirectoryPath installDirectory) =>
Task.CompletedTask;
public class ComfyModelPathsYaml public class ComfyModelPathsYaml
{ {
public class SmData public class SmData

5
StabilityMatrix.Core/Models/Packages/DankDiffusion.cs

@ -44,6 +44,11 @@ public class DankDiffusion : BaseGitPackage
throw new NotImplementedException(); throw new NotImplementedException();
} }
public override Task RemoveModelFolderLinks(DirectoryPath installDirectory)
{
throw new NotImplementedException();
}
public override List<LaunchOptionDefinition> LaunchOptions { get; } public override List<LaunchOptionDefinition> LaunchOptions { get; }
public override Task<string> GetLatestVersion() public override Task<string> GetLatestVersion()
{ {

97
StabilityMatrix.Core/Models/Packages/InvokeAI.cs

@ -1,9 +1,10 @@
using System.Data; using System.Text.Json;
using System.Text.RegularExpressions; using System.Text.RegularExpressions;
using NLog; using NLog;
using StabilityMatrix.Core.Extensions; using StabilityMatrix.Core.Extensions;
using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Helper;
using StabilityMatrix.Core.Helper.Cache; using StabilityMatrix.Core.Helper.Cache;
using StabilityMatrix.Core.Models.Api;
using StabilityMatrix.Core.Models.FileInterfaces; using StabilityMatrix.Core.Models.FileInterfaces;
using StabilityMatrix.Core.Models.Progress; using StabilityMatrix.Core.Models.Progress;
using StabilityMatrix.Core.Processes; using StabilityMatrix.Core.Processes;
@ -28,9 +29,6 @@ public class InvokeAI : BaseGitPackage
public override string Blurb => "Professional Creative Tools for Stable Diffusion"; public override string Blurb => "Professional Creative Tools for Stable Diffusion";
public override string LaunchCommand => "invokeai-web"; public override string LaunchCommand => "invokeai-web";
public override string Disclaimer =>
"Note: InvokeAI support is currently experimental, and checkpoints in the shared models folder will not be available with InvokeAI.";
public override IReadOnlyList<string> ExtraLaunchCommands => new[] public override IReadOnlyList<string> ExtraLaunchCommands => new[]
{ {
"invokeai-configure", "invokeai-configure",
@ -293,10 +291,12 @@ public class InvokeAI : BaseGitPackage
{ {
await SetupVenv(installedPackagePath).ConfigureAwait(false); await SetupVenv(installedPackagePath).ConfigureAwait(false);
if (command.Equals("invokeai-configure")) arguments = command switch
{ {
arguments = "--yes --skip-sd-weights"; "invokeai-configure" => "--yes --skip-sd-weights",
} "invokeai-model-install" => "--yes",
_ => arguments
};
VenvRunner.EnvironmentVariables = GetEnvVars(installedPackagePath); VenvRunner.EnvironmentVariables = GetEnvVars(installedPackagePath);
@ -355,11 +355,92 @@ public class InvokeAI : BaseGitPackage
public override Task SetupModelFolders(DirectoryPath installDirectory) public override Task SetupModelFolders(DirectoryPath installDirectory)
{ {
var modelsFolder = SettingsManager.ModelsDirectory;
if (!Directory.Exists(modelsFolder))
return Task.CompletedTask; return Task.CompletedTask;
var connectedModelJsons =
Directory.GetFiles(modelsFolder, "*.cm-info.json", SearchOption.AllDirectories);
var connectedModelDict = new Dictionary<string, ConnectedModelInfo>();
foreach (var jsonFile in connectedModelJsons)
{
var json = File.ReadAllText(jsonFile);
var connectedModelInfo = JsonSerializer.Deserialize<ConnectedModelInfo>(json);
var extension = connectedModelInfo?.FileMetadata.Format switch
{
CivitModelFormat.SafeTensor => ".safetensors",
CivitModelFormat.PickleTensor => ".pt",
_ => string.Empty
};
if (string.IsNullOrWhiteSpace(extension) || connectedModelInfo == null)
continue;
var modelFilePath = jsonFile.Replace(".cm-info.json", extension);
if (File.Exists(modelFilePath))
{
connectedModelDict[modelFilePath] = connectedModelInfo;
}
} }
public override Task UpdateModelFolders(DirectoryPath installDirectory) foreach (var modelFilePath in connectedModelDict.Keys)
{
var model = connectedModelDict[modelFilePath];
var modelType = model.ModelType switch
{
CivitModelType.Checkpoint => "main",
CivitModelType.LORA => "lora",
CivitModelType.TextualInversion => "embedding",
CivitModelType.Controlnet => "controlnet",
_ => string.Empty
};
if (string.IsNullOrWhiteSpace(modelType))
continue;
var sourcePath = modelFilePath;
var destinationPath = Path.Combine(installDirectory, RelativeRootPath, "autoimport",
modelType, Path.GetFileName(modelFilePath));
try
{ {
File.CreateSymbolicLink(destinationPath, sourcePath);
}
catch (IOException e)
{
// File already exists
Logger.Warn(e,
$"Could not create symlink for {sourcePath} to {destinationPath} - file already exists");
}
}
return Task.CompletedTask;
}
public override Task UpdateModelFolders(DirectoryPath installDirectory) =>
SetupModelFolders(installDirectory);
public override Task RemoveModelFolderLinks(DirectoryPath installDirectory)
{
var autoImportDir = Path.Combine(installDirectory, RelativeRootPath, "autoimport");
var allSymlinks =
Directory.GetFiles(autoImportDir, "*.*", SearchOption.AllDirectories)
.Select(path => new FileInfo(path)).Where(file => file.LinkTarget != null);
foreach (var link in allSymlinks)
{
try
{
link.Delete();
}
catch (IOException e)
{
Logger.Warn(e, $"Could not delete symlink {link.FullName}");
}
}
return Task.CompletedTask; return Task.CompletedTask;
} }

61
StabilityMatrix.Core/Models/Packages/VladAutomatic.cs

@ -98,19 +98,21 @@ public class VladAutomatic : BaseGitPackage
{ {
Name = "Use DirectML if no compatible GPU is detected", Name = "Use DirectML if no compatible GPU is detected",
Type = LaunchOptionType.Bool, Type = LaunchOptionType.Bool,
InitialValue = !HardwareHelper.HasNvidiaGpu() && HardwareHelper.HasAmdGpu(), InitialValue = PreferDirectML(),
Options = new() { "--use-directml" } Options = new() { "--use-directml" }
}, },
new() new()
{ {
Name = "Force use of Nvidia CUDA backend", Name = "Force use of Nvidia CUDA backend",
Type = LaunchOptionType.Bool, Type = LaunchOptionType.Bool,
InitialValue = HardwareHelper.HasNvidiaGpu(),
Options = new() { "--use-cuda" } Options = new() { "--use-cuda" }
}, },
new() new()
{ {
Name = "Force use of AMD ROCm backend", Name = "Force use of AMD ROCm backend",
Type = LaunchOptionType.Bool, Type = LaunchOptionType.Bool,
InitialValue = PreferRocm(),
Options = new() { "--use-rocm" } Options = new() { "--use-rocm" }
}, },
new() new()
@ -136,6 +138,16 @@ public class VladAutomatic : BaseGitPackage
public override string ExtraLaunchArguments => ""; public override string ExtraLaunchArguments => "";
// Set ROCm for default if AMD and Linux
private static bool PreferRocm() => !HardwareHelper.HasNvidiaGpu()
&& HardwareHelper.HasAmdGpu()
&& Compat.IsLinux;
// Set DirectML for default if AMD and Windows
private static bool PreferDirectML() => !HardwareHelper.HasNvidiaGpu()
&& HardwareHelper.HasAmdGpu()
&& Compat.IsWindows;
public override Task<string> GetLatestVersion() => Task.FromResult("master"); public override Task<string> GetLatestVersion() => Task.FromResult("master");
public override async Task<IEnumerable<PackageVersion>> GetAllVersions(bool isReleaseMode = true) public override async Task<IEnumerable<PackageVersion>> GetAllVersions(bool isReleaseMode = true)
@ -150,43 +162,39 @@ public class VladAutomatic : BaseGitPackage
public override async Task InstallPackage(IProgress<ProgressReport>? progress = null) public override async Task InstallPackage(IProgress<ProgressReport>? progress = null)
{ {
progress?.Report(new ProgressReport(-1f, "Installing dependencies...", isIndeterminate: true)); progress?.Report(new ProgressReport(-1f, "Installing package...", isIndeterminate: true));
// Setup venv // Setup venv
var venvRunner = new PyVenvRunner(Path.Combine(InstallLocation, "venv")); var venvRunner = new PyVenvRunner(Path.Combine(InstallLocation, "venv"));
venvRunner.WorkingDirectory = InstallLocation; venvRunner.WorkingDirectory = InstallLocation;
if (!venvRunner.Exists()) venvRunner.EnvironmentVariables = SettingsManager.Settings.EnvironmentVariables;
{
await venvRunner.Setup().ConfigureAwait(false); await venvRunner.Setup().ConfigureAwait(false);
}
// Install torch / xformers based on gpu info // Run initial install
var gpus = HardwareHelper.IterGpuInfo().ToList(); if (HardwareHelper.HasNvidiaGpu())
if (gpus.Any(g => g.IsNvidia))
{ {
Logger.Info("Starting torch install (CUDA)..."); // CUDA
await venvRunner.PipInstall(PyVenvRunner.TorchPipInstallArgsCuda, OnConsoleOutput) await venvRunner.CustomInstall("launch.py --use-cuda --debug --test", OnConsoleOutput)
.ConfigureAwait(false); .ConfigureAwait(false);
Logger.Info("Installing xformers...");
await venvRunner.PipInstall("xformers", OnConsoleOutput).ConfigureAwait(false);
} }
else if (gpus.Any(g => g.IsAmd)) else if (PreferRocm())
{ {
Logger.Info("Starting torch install (DirectML)..."); // ROCm
await venvRunner.PipInstall(PyVenvRunner.TorchPipInstallArgsDirectML, OnConsoleOutput) await venvRunner.CustomInstall("launch.py --use-rocm --debug --test", OnConsoleOutput)
.ConfigureAwait(false);
}
else if (PreferDirectML())
{
// DirectML
await venvRunner.CustomInstall("launch.py --use-directml --debug --test", OnConsoleOutput)
.ConfigureAwait(false); .ConfigureAwait(false);
} }
else else
{ {
Logger.Info("Starting torch install (CPU)..."); await venvRunner.CustomInstall("launch.py --debug --test", OnConsoleOutput)
await venvRunner.PipInstall(PyVenvRunner.TorchPipInstallArgsCpu, OnConsoleOutput)
.ConfigureAwait(false); .ConfigureAwait(false);
} }
// Install requirements file
Logger.Info("Installing requirements.txt");
await venvRunner.PipInstall($"-r requirements.txt", OnConsoleOutput).ConfigureAwait(false);
progress?.Report(new ProgressReport(1, isIndeterminate: false)); progress?.Report(new ProgressReport(1, isIndeterminate: false));
} }
@ -286,10 +294,11 @@ public class VladAutomatic : BaseGitPackage
return Task.CompletedTask; return Task.CompletedTask;
} }
public override Task UpdateModelFolders(DirectoryPath installDirectory) public override Task UpdateModelFolders(DirectoryPath installDirectory) =>
{ SetupModelFolders(installDirectory);
return SetupModelFolders(installDirectory);
} public override Task RemoveModelFolderLinks(DirectoryPath installDirectory) =>
Task.CompletedTask;
public override async Task<string> Update(InstalledPackage installedPackage, public override async Task<string> Update(InstalledPackage installedPackage,
IProgress<ProgressReport>? progress = null, bool includePrerelease = false) IProgress<ProgressReport>? progress = null, bool includePrerelease = false)

6
StabilityMatrix.Core/Models/Packages/VoltaML.cs

@ -191,4 +191,10 @@ public class VoltaML : BaseGitPackage
await StabilityMatrix.Core.Helper.SharedFolders.UpdateLinksForPackage(this, await StabilityMatrix.Core.Helper.SharedFolders.UpdateLinksForPackage(this,
SettingsManager.ModelsDirectory, installDirectory).ConfigureAwait(false); SettingsManager.ModelsDirectory, installDirectory).ConfigureAwait(false);
} }
public override Task RemoveModelFolderLinks(DirectoryPath installDirectory)
{
StabilityMatrix.Core.Helper.SharedFolders.RemoveLinksForPackage(this, installDirectory);
return Task.CompletedTask;
}
} }

2
StabilityMatrix.Core/Models/Settings/ModelSearchOptions.cs

@ -2,4 +2,4 @@
namespace StabilityMatrix.Core.Models.Settings; namespace StabilityMatrix.Core.Models.Settings;
public record ModelSearchOptions(CivitPeriod SelectedPeriod, CivitSortMode SortMode, CivitModelType SelectedModelType); public record ModelSearchOptions(CivitPeriod SelectedPeriod, CivitSortMode SortMode, CivitModelType SelectedModelType, string SelectedBaseModelType);

3
StabilityMatrix.Core/Processes/ApcMessage.cs

@ -1,5 +1,4 @@
using System.Diagnostics; using System.Diagnostics.CodeAnalysis;
using System.Diagnostics.CodeAnalysis;
using System.Text.Json; using System.Text.Json;
using System.Text.Json.Serialization; using System.Text.Json.Serialization;
using NLog; using NLog;

1
StabilityMatrix.Core/Processes/ApcParser.cs

@ -1,6 +1,5 @@
using System.Diagnostics; using System.Diagnostics;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Text;
using System.Text.Json; using System.Text.Json;
namespace StabilityMatrix.Core.Processes; namespace StabilityMatrix.Core.Processes;

7
StabilityMatrix.Core/Python/PyRunner.cs

@ -26,6 +26,13 @@ public class PyRunner : IPyRunner
public static string PythonDir => Path.Combine(GlobalConfig.LibraryDir, "Assets", PythonDirName); public static string PythonDir => Path.Combine(GlobalConfig.LibraryDir, "Assets", PythonDirName);
/// <summary>
/// Path to the Python Linked library relative from the Python directory.
/// </summary>
public static string RelativePythonDllPath => Compat.Switch(
(PlatformKind.Windows, "python310.dll"),
(PlatformKind.Linux, Path.Combine("lib", "libpython3.10.so")));
public static string PythonDllPath => Compat.Switch( public static string PythonDllPath => Compat.Switch(
(PlatformKind.Windows, Path.Combine(PythonDir, "python310.dll")), (PlatformKind.Windows, Path.Combine(PythonDir, "python310.dll")),
(PlatformKind.Linux, Path.Combine(PythonDir, "lib", "libpython3.10.so"))); (PlatformKind.Linux, Path.Combine(PythonDir, "lib", "libpython3.10.so")));

41
StabilityMatrix.Core/Python/PyVenvRunner.cs

@ -219,6 +219,40 @@ public class PyVenvRunner : IDisposable, IAsyncDisposable
} }
} }
/// <summary>
/// Run a custom install command. Waits for the process to exit.
/// workingDirectory defaults to RootPath.
/// </summary>
public async Task CustomInstall(string args, Action<ProcessOutput>? outputDataReceived = null)
{
// Record output for errors
var output = new StringBuilder();
var outputAction =
outputDataReceived == null
? null
: new Action<ProcessOutput>(s =>
{
Logger.Debug($"Install output: {s.Text}");
// Record to output
output.Append(s.Text);
// Forward to callback
outputDataReceived(s);
});
SetPyvenvCfg(PyRunner.PythonDir);
RunDetached(args, outputAction);
await Process.WaitForExitAsync().ConfigureAwait(false);
// Check return code
if (Process.ExitCode != 0)
{
throw new ProcessException(
$"install script failed with code {Process.ExitCode}: {output.ToString().ToRepr()}"
);
}
}
/// <summary> /// <summary>
/// Run a command using the venv Python executable and return the result. /// Run a command using the venv Python executable and return the result.
/// </summary> /// </summary>
@ -297,11 +331,12 @@ public class PyVenvRunner : IDisposable, IAsyncDisposable
// Disable pip caching - uses significant memory for large packages like torch // Disable pip caching - uses significant memory for large packages like torch
env["PIP_NO_CACHE_DIR"] = "true"; env["PIP_NO_CACHE_DIR"] = "true";
// On windows, add portable git // On windows, add portable git to PATH and binary as GIT
if (Compat.IsWindows) if (Compat.IsWindows)
{ {
var portableGit = GlobalConfig.LibraryDir.JoinDir("PortableGit", "bin"); var portableGitBin = GlobalConfig.LibraryDir.JoinDir("PortableGit", "bin");
env["PATH"] = Compat.GetEnvPathWithExtensions(portableGit); env["PATH"] = Compat.GetEnvPathWithExtensions(portableGitBin);
env["GIT"] = portableGitBin.JoinFile("git.exe");
} }
if (unbuffered) if (unbuffered)

5
StabilityMatrix.Core/Services/SettingsManager.cs

@ -339,7 +339,6 @@ public class SettingsManager : ISettingsManager
} }
package.PackageVersion = newVersion; package.PackageVersion = newVersion;
package.DisplayVersion = string.IsNullOrWhiteSpace(package.InstalledBranch) package.DisplayVersion = string.IsNullOrWhiteSpace(package.InstalledBranch)
? newVersion ? newVersion
: $"{package.InstalledBranch}@{newVersion[..7]}"; : $"{package.InstalledBranch}@{newVersion[..7]}";
@ -349,7 +348,9 @@ public class SettingsManager : ISettingsManager
public void SetLastUpdateCheck(InstalledPackage package) public void SetLastUpdateCheck(InstalledPackage package)
{ {
Settings.InstalledPackages.First(p => p.DisplayName == package.DisplayName).LastUpdateCheck = package.LastUpdateCheck; var installedPackage = Settings.InstalledPackages.First(p => p.DisplayName == package.DisplayName);
installedPackage.LastUpdateCheck = package.LastUpdateCheck;
installedPackage.UpdateAvailable = package.UpdateAvailable;
SaveSettings(); SaveSettings();
} }

14
StabilityMatrix.Tests/Helper/EventManagerTests.cs

@ -27,18 +27,4 @@ public class EventManagerTests
// Assert // Assert
Assert.AreEqual(100, progress); 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);
}
} }

Loading…
Cancel
Save