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. 40
      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. 67
      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. 37
      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. 7
      StabilityMatrix.Avalonia/ViewModels/LaunchPageViewModel.cs
  31. 17
      StabilityMatrix.Avalonia/ViewModels/MainWindowViewModel.cs
  32. 298
      StabilityMatrix.Avalonia/ViewModels/PackageManager/PackageCardViewModel.cs
  33. 318
      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. 63
      StabilityMatrix.Avalonia/Views/MainWindow.axaml.cs
  45. 285
      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/),
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
### Added

14
StabilityMatrix.Avalonia/App.axaml.cs

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

2
StabilityMatrix.Avalonia/Controls/AppWindowBase.cs

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

2
StabilityMatrix.Avalonia/Controls/BetterContentDialog.cs

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

2
StabilityMatrix.Avalonia/Controls/UserControlBase.cs

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

40
StabilityMatrix.Avalonia/DesignData/DesignData.cs

@ -4,6 +4,7 @@ using System.Collections.Immutable;
using System.Collections.ObjectModel;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using System.Net.Http;
using AvaloniaEdit.Utils;
using Microsoft.Extensions.DependencyInjection;
@ -12,7 +13,10 @@ using StabilityMatrix.Avalonia.Models;
using StabilityMatrix.Avalonia.Models.TagCompletion;
using StabilityMatrix.Avalonia.Services;
using StabilityMatrix.Avalonia.ViewModels;
using StabilityMatrix.Avalonia.ViewModels.Base;
using StabilityMatrix.Avalonia.ViewModels.CheckpointBrowser;
using StabilityMatrix.Avalonia.ViewModels.Dialogs;
using StabilityMatrix.Avalonia.ViewModels.PackageManager;
using StabilityMatrix.Avalonia.ViewModels.Inference;
using StabilityMatrix.Core.Api;
using StabilityMatrix.Core.Database;
@ -26,6 +30,8 @@ using StabilityMatrix.Core.Models.Progress;
using StabilityMatrix.Core.Python;
using StabilityMatrix.Core.Services;
using StabilityMatrix.Core.Updater;
using CheckpointFile = StabilityMatrix.Avalonia.ViewModels.CheckpointManager.CheckpointFile;
using CheckpointFolder = StabilityMatrix.Avalonia.ViewModels.CheckpointManager.CheckpointFolder;
namespace StabilityMatrix.Avalonia.DesignData;
@ -56,6 +62,7 @@ public static class DesignData
{
Id = activePackageId,
DisplayName = "My Installed Package",
DisplayVersion = "v1.0.0",
PackageName = "stable-diffusion-webui",
PackageVersion = "v1.0.0",
LibraryPath = $"Packages{Path.DirectorySeparatorChar}example-webui",
@ -65,8 +72,8 @@ public static class DesignData
{
Id = Guid.NewGuid(),
DisplayName = "Dank Diffusion",
PackageName = "dank-diffusion",
PackageVersion = "v2.0.0",
PackageName = "ComfyUI",
DisplayVersion = "main@ab73d4a",
LibraryPath = $"Packages{Path.DirectorySeparatorChar}example-webui",
LastUpdateCheck = DateTimeOffset.Now
}
@ -77,6 +84,7 @@ public static class DesignData
// General services
services.AddLogging()
.AddSingleton<INavigationService, NavigationService>()
.AddSingleton<IPackageFactory, PackageFactory>()
.AddSingleton<IUpdateHelper, UpdateHelper>()
.AddSingleton<ICompletionProvider, CompletionProvider>()
@ -92,7 +100,7 @@ public static class DesignData
.AddSingleton<IApiFactory, MockApiFactory>()
.AddSingleton<IInferenceClientManager, MockInferenceClientManager>()
.AddSingleton<IDiscordRichPresenceService, MockDiscordRichPresenceService>();
// Placeholder services that nobody should need during design time
services
.AddSingleton<IPyRunner>(_ => null!)
@ -213,12 +221,14 @@ public static class DesignData
CheckpointBrowserViewModel.ModelCards = new
ObservableCollection<CheckpointBrowserCardViewModel>
{
new(new CivitModel
dialogFactory.Get<CheckpointBrowserCardViewModel>(vm =>
{
vm.CivitModel = new CivitModel
{
Name = "BB95 Furry Mix",
Description = "A furry mix of BB95",
}, downloadService, settingsManager,
dialogFactory, notificationService)
};
})
};
ProgressManagerViewModel.ProgressItems = new ObservableCollection<ProgressItemViewModel>
@ -254,8 +264,19 @@ public static class DesignData
public static LaunchPageViewModel LaunchPageViewModel =>
Services.GetRequiredService<LaunchPageViewModel>();
public static PackageManagerViewModel PackageManagerViewModel =>
Services.GetRequiredService<PackageManagerViewModel>();
public static PackageManagerViewModel 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 =>
Services.GetRequiredService<CheckpointsPageViewModel>();
@ -276,6 +297,7 @@ public static class DesignData
{
Name = "BB95 Furry Mix",
Description = "v1.0.0",
BaseModel = "SD 1.5",
Files = new List<CivitFile>
{
new()
@ -317,7 +339,7 @@ public static class DesignData
public static OneClickInstallViewModel OneClickInstallViewModel =>
Services.GetRequiredService<OneClickInstallViewModel>();
public static InferenceViewModel InferenceViewModel =>
Services.GetRequiredService<InferenceViewModel>();

1
StabilityMatrix.Avalonia/DesignData/MockLaunchPageViewModel.cs

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

7
StabilityMatrix.Avalonia/Helpers/UnixPrerequisiteHelper.cs

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

1
StabilityMatrix.Avalonia/Program.cs

@ -12,7 +12,6 @@ using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Threading;
using Microsoft.Extensions.DependencyInjection;
using NLog;
using Polly.Contrib.WaitAndRetry;
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()
});
}
}

67
StabilityMatrix.Avalonia/ViewLocator.cs

@ -1,47 +1,78 @@
using System;
using Avalonia.Controls;
using Avalonia.Controls.Templates;
using StabilityMatrix.Avalonia.ViewModels;
using FluentAvalonia.UI.Controls;
using StabilityMatrix.Avalonia.ViewModels.Base;
using StabilityMatrix.Core.Attributes;
namespace StabilityMatrix.Avalonia;
public class ViewLocator : IDataTemplate
public class ViewLocator : IDataTemplate, INavigationPageFactory
{
/// <inheritdoc />
public Control Build(object? data)
{
if (data is null) throw new ArgumentNullException(nameof(data));
var type = data.GetType();
if (type is null) throw new InvalidOperationException("Type is null");
if (Attribute.GetCustomAttribute(type, typeof(ViewAttribute)) is ViewAttribute viewAttr)
{
var viewType = viewAttr.GetViewType();
#pragma warning disable IL2072
// In design mode, just create a new instance of the view
if (Design.IsDesignMode)
{
return (Control) Activator.CreateInstance(viewType)!;
}
#pragma warning restore IL2072
// Otherwise get from the service provider
if (App.Services.GetService(viewType) is Control view)
{
return view;
}
return GetView(viewType);
}
return new TextBlock
{
Text = "Not Found: " + data.GetType().FullName
Text = "View Model Not Found: " + data.GetType().FullName
};
}
private Control GetView(Type viewType)
{
// Otherwise get from the service provider
if (App.Services.GetService(viewType) is Control view)
{
return view;
}
return new TextBlock
{
Text = "View Not Found: " + viewType.FullName
};
}
/// <inheritdoc />
public bool Match(object? data)
{
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;
namespace StabilityMatrix.Avalonia.ViewModels;
namespace StabilityMatrix.Avalonia.ViewModels.Base;
/// <summary>
/// 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;
namespace StabilityMatrix.Avalonia.ViewModels;
namespace StabilityMatrix.Avalonia.ViewModels.Base;
/// <summary>
/// Generic view model for progress reporting.
/// </summary>
public partial class ProgressViewModel : ObservableObject
public partial class ProgressViewModel : ViewModelBase
{
[ObservableProperty, NotifyPropertyChangedFor(nameof(IsTextVisible))]
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 StabilityMatrix.Avalonia.Models;
namespace StabilityMatrix.Avalonia.ViewModels;
namespace StabilityMatrix.Avalonia.ViewModels.Base;
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 StabilityMatrix.Avalonia.Controls;
using StabilityMatrix.Avalonia.Services;
using StabilityMatrix.Avalonia.ViewModels.Base;
using StabilityMatrix.Avalonia.ViewModels.Dialogs;
using StabilityMatrix.Avalonia.Views.Dialogs;
using StabilityMatrix.Core.Extensions;
@ -28,9 +29,9 @@ using StabilityMatrix.Core.Processes;
using StabilityMatrix.Core.Services;
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();
@ -38,33 +39,38 @@ public partial class CheckpointBrowserCardViewModel : ProgressViewModel
private readonly ISettingsManager settingsManager;
private readonly ServiceManager<ViewModelBase> dialogFactory;
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;
[ObservableProperty] private Uri? cardImage;
[ObservableProperty] private bool isImporting;
[ObservableProperty] private string updateCardText = string.Empty;
[ObservableProperty] private bool showUpdateCard;
private CivitModel civitModel;
public CheckpointBrowserCardViewModel(
CivitModel civitModel,
IDownloadService downloadService,
ISettingsManager settingsManager,
ServiceManager<ViewModelBase> dialogFactory,
INotificationService notificationService,
Action<CheckpointBrowserCardViewModel>? onDownloadStart = null)
INotificationService notificationService)
{
this.downloadService = downloadService;
this.settingsManager = settingsManager;
this.dialogFactory = dialogFactory;
this.notificationService = notificationService;
this.onDownloadStart = onDownloadStart;
CivitModel = civitModel;
UpdateImage();
CheckIfInstalled();
// Update image when nsfw setting changes
settingsManager.RegisterPropertyChangedHandler(
@ -197,7 +203,7 @@ public partial class CheckpointBrowserCardViewModel : ProgressViewModel
IsImporting = true;
Text = "Downloading...";
onDownloadStart?.Invoke(this);
OnDownloadStart?.Invoke(this);
// Holds files to be deleted on errors
var filesForCleanup = new HashSet<FilePath>();

37
StabilityMatrix.Avalonia/ViewModels/CheckpointBrowserViewModel.cs

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

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 FluentAvalonia.UI.Controls;
using NLog;
using StabilityMatrix.Avalonia.ViewModels.Base;
using StabilityMatrix.Core.Extensions;
using StabilityMatrix.Core.Helper;
using StabilityMatrix.Core.Models;
using StabilityMatrix.Core.Models.Progress;
using StabilityMatrix.Core.Processes;
namespace StabilityMatrix.Avalonia.ViewModels;
namespace StabilityMatrix.Avalonia.ViewModels.CheckpointManager;
public partial class CheckpointFile : ViewModelBase
{
@ -46,7 +47,7 @@ public partial class CheckpointFile : ViewModelBase
[ObservableProperty] private bool isLoading;
public string FileName => Path.GetFileName(FilePath);
public string FileName => Path.GetFileName((string?) FilePath);
public ObservableCollection<string> Badges { get; set; } = new();
@ -78,8 +79,8 @@ public partial class CheckpointFile : ViewModelBase
throw new InvalidOperationException(
"Cannot get connected model info file path when FilePath is empty");
}
var modelNameNoExt = Path.GetFileNameWithoutExtension(FilePath);
var modelDir = Path.GetDirectoryName(FilePath) ?? "";
var modelNameNoExt = Path.GetFileNameWithoutExtension((string?) FilePath);
var modelDir = Path.GetDirectoryName((string?) FilePath) ?? "";
return Path.Combine(modelDir, $"{modelNameNoExt}.cm-info.json");
}
@ -123,7 +124,7 @@ public partial class CheckpointFile : ViewModelBase
private async Task RenameAsync()
{
// Parent folder path
var parentPath = Path.GetDirectoryName(FilePath) ?? "";
var parentPath = Path.GetDirectoryName((string?) FilePath) ?? "";
var textFields = new TextBoxField[]
{
@ -158,7 +159,7 @@ public partial class CheckpointFile : ViewModelBase
// If preview image exists, rename it too
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);
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 FluentAvalonia.UI.Controls;
using StabilityMatrix.Avalonia.Models;
using StabilityMatrix.Avalonia.ViewModels.Base;
using StabilityMatrix.Core.Extensions;
using StabilityMatrix.Core.Helper;
using StabilityMatrix.Core.Models;
@ -20,7 +21,7 @@ using StabilityMatrix.Core.Models.Progress;
using StabilityMatrix.Core.Processes;
using StabilityMatrix.Core.Services;
namespace StabilityMatrix.Avalonia.ViewModels;
namespace StabilityMatrix.Avalonia.ViewModels.CheckpointManager;
public partial class CheckpointFolder : ViewModelBase
{
@ -73,7 +74,7 @@ public partial class CheckpointFolder : ViewModelBase
? $"{Title} ({CheckpointFiles.Count + SubFolders.Sum(folder => folder.CheckpointFiles.Count)})"
: Title;
public ProgressViewModel Progress { get; } = new();
public Base.ProgressViewModel Progress { get; } = new();
public CheckpointFolder? ParentFolder { get; init; }
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 FluentAvalonia.UI.Controls;
using NLog;
using StabilityMatrix.Avalonia.ViewModels.Base;
using StabilityMatrix.Avalonia.Views;
using StabilityMatrix.Core.Attributes;
using StabilityMatrix.Core.Helper;
@ -51,10 +52,10 @@ public partial class CheckpointsPageViewModel : PageViewModelBase
}
[ObservableProperty]
private ObservableCollection<CheckpointFolder> checkpointFolders = new();
private ObservableCollection<CheckpointManager.CheckpointFolder> checkpointFolders = new();
[ObservableProperty]
private ObservableCollection<CheckpointFolder> displayedCheckpointFolders = new();
private ObservableCollection<CheckpointManager.CheckpointFolder> displayedCheckpointFolders = new();
public CheckpointsPageViewModel(
ISharedFolders sharedFolders,
@ -105,10 +106,10 @@ public partial class CheckpointsPageViewModel : PageViewModelBase
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)
throw new ArgumentNullException(nameof(folder));
@ -139,7 +140,7 @@ public partial class CheckpointsPageViewModel : PageViewModelBase
var indexTasks = folders.Select(f => Task.Run(async () =>
{
var checkpointFolder =
new CheckpointFolder(settingsManager, downloadService, modelFinder)
new CheckpointManager.CheckpointFolder(settingsManager, downloadService, modelFinder)
{
Title = Path.GetFileName(f),
DirectoryPath = f,
@ -153,13 +154,13 @@ public partial class CheckpointsPageViewModel : PageViewModelBase
// Set new observable collection, ordered by alphabetical order
CheckpointFolders =
new ObservableCollection<CheckpointFolder>(indexTasks
new ObservableCollection<CheckpointManager.CheckpointFolder>(indexTasks
.Select(t => t.Result)
.OrderBy(f => f.Title));
if (!string.IsNullOrWhiteSpace(SearchFilter))
{
DisplayedCheckpointFolders = new ObservableCollection<CheckpointFolder>(
DisplayedCheckpointFolders = new ObservableCollection<CheckpointManager.CheckpointFolder>(
CheckpointFolders
.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.IO;
using CommunityToolkit.Mvvm.ComponentModel;
using StabilityMatrix.Core.Models.Api;

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

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

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

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

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

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

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

@ -3,7 +3,6 @@ using System.Collections.ObjectModel;
using System.Linq;
using CommunityToolkit.Mvvm.ComponentModel;
using StabilityMatrix.Core.Models.Api;
using StabilityMatrix.Core.Services;
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.Input;
using Microsoft.Extensions.Logging;
using StabilityMatrix.Avalonia.ViewModels.Base;
using StabilityMatrix.Core.Helper;
using StabilityMatrix.Core.Helper.Factory;
using StabilityMatrix.Core.Models;

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

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

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

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

1
StabilityMatrix.Avalonia/ViewModels/FirstLaunchSetupViewModel.cs

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

7
StabilityMatrix.Avalonia/ViewModels/LaunchPageViewModel.cs

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

17
StabilityMatrix.Avalonia/ViewModels/MainWindowViewModel.cs

@ -9,6 +9,7 @@ using CommunityToolkit.Mvvm.ComponentModel;
using FluentAvalonia.UI.Controls;
using StabilityMatrix.Avalonia.Controls;
using StabilityMatrix.Avalonia.Services;
using StabilityMatrix.Avalonia.ViewModels.Base;
using StabilityMatrix.Avalonia.ViewModels.Dialogs;
using StabilityMatrix.Avalonia.Views;
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
CurrentPage ??= Pages.FirstOrDefault();
SelectedCategory ??= Pages.FirstOrDefault();
EventManager.Instance.PageChangeRequested += OnPageChangeRequested;
}
public override async Task OnLoadedAsync()
@ -194,18 +193,4 @@ public partial class MainWindowViewModel : ViewModelBase
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);
}
}
}

318
StabilityMatrix.Avalonia/ViewModels/PackageManagerViewModel.cs

@ -1,29 +1,22 @@
using System;
using System.Collections.Immutable;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using AsyncAwaitBestPractices;
using Avalonia.Controls;
using Avalonia.Controls.Notifications;
using Avalonia.Threading;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using FluentAvalonia.UI.Controls;
using Microsoft.Extensions.Logging;
using Polly;
using StabilityMatrix.Avalonia.Controls;
using StabilityMatrix.Avalonia.Services;
using StabilityMatrix.Avalonia.ViewModels.Base;
using StabilityMatrix.Avalonia.ViewModels.Dialogs;
using StabilityMatrix.Avalonia.ViewModels.PackageManager;
using StabilityMatrix.Avalonia.Views;
using StabilityMatrix.Avalonia.Views.Dialogs;
using StabilityMatrix.Core.Attributes;
using StabilityMatrix.Core.Helper;
using StabilityMatrix.Core.Helper.Factory;
using StabilityMatrix.Core.Models;
using StabilityMatrix.Core.Models.Packages;
using StabilityMatrix.Core.Models.Progress;
using StabilityMatrix.Core.Services;
using Symbol = FluentIcons.Common.Symbol;
using SymbolIconSource = FluentIcons.FluentAvalonia.SymbolIconSource;
@ -37,313 +30,46 @@ namespace StabilityMatrix.Avalonia.ViewModels;
[View(typeof(PackageManagerPage))]
public partial class PackageManagerViewModel : PageViewModelBase
{
private readonly ILogger<PackageManagerViewModel> logger;
private readonly ISettingsManager settingsManager;
private readonly IPackageFactory packageFactory;
private readonly INotificationService notificationService;
private readonly ServiceManager<ViewModelBase> dialogFactory;
private const int MinutesToWaitForUpdateCheck = 60;
public PackageManagerViewModel(ILogger<PackageManagerViewModel> logger,
ISettingsManager settingsManager, IPackageFactory packageFactory,
INotificationService notificationService, ServiceManager<ViewModelBase> dialogFactory)
public PackageManagerViewModel(ISettingsManager settingsManager, IPackageFactory packageFactory,
ServiceManager<ViewModelBase> dialogFactory)
{
this.logger = logger;
this.settingsManager = settingsManager;
this.packageFactory = packageFactory;
this.notificationService = notificationService;
this.dialogFactory = dialogFactory;
Packages =
new ObservableCollection<InstalledPackage>(settingsManager.Settings.InstalledPackages);
SelectedPackage = Packages.FirstOrDefault();
EventManager.Instance.InstalledPackagesChanged += OnInstalledPackagesChanged;
}
[ObservableProperty]
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; }
[ObservableProperty] private ObservableCollection<PackageCardViewModel> packages;
public override bool CanNavigateNext { get; protected set; } = true;
public override bool CanNavigatePrevious { get; protected set; }
public override string Title => "Packages";
public override IconSource IconSource => new SymbolIconSource { Symbol = Symbol.Box, IsFilled = true};
public override async Task OnLoadedAsync()
{
if (!Design.IsDesignMode)
{
await CheckUpdates();
}
var installedPackages = settingsManager.Settings.InstalledPackages;
SelectedPackage = installedPackages.FirstOrDefault(x =>
x.Id == settingsManager.Settings.ActiveInstalledPackageId);
SelectedPackage ??= installedPackages.FirstOrDefault();
if (Design.IsDesignMode) return;
InstallButtonEnabled = SelectedPackage != null;
}
private async Task CheckUpdates()
{
var installedPackages = settingsManager.Settings.InstalledPackages;
Packages.Clear();
foreach (var packageToUpdate in installedPackages)
{
var basePackage = packageFactory.FindPackageByName(packageToUpdate.PackageName);
if (basePackage == null) continue;
var canCheckUpdate = packageToUpdate.LastUpdateCheck == null ||
packageToUpdate.LastUpdateCheck.Value.AddMinutes(MinutesToWaitForUpdateCheck) <
DateTimeOffset.Now;
if (canCheckUpdate)
Packages = new ObservableCollection<PackageCardViewModel>(installedPackages.Select(
package => dialogFactory.Get<PackageCardViewModel>(vm =>
{
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;
if (InstallButtonText == "Launch")
{
settingsManager.Transaction(s => s.ActiveInstalledPackageId = SelectedPackage.Id);
EventManager.Instance.RequestPageChange(typeof(LaunchPageViewModel));
EventManager.Instance.OnPackageLaunchRequested(SelectedPackage.Id);
}
else
{
await UpdateSelectedPackage();
}
}
public async Task Uninstall()
{
// In design mode, just remove the package from the list
if (Design.IsDesignMode)
{
if (SelectedPackage != null)
{
Packages.Remove(SelectedPackage);
}
return;
}
vm.Package = package;
return vm;
})));
if (SelectedPackage?.LibraryPath == null)
foreach (var package in Packages)
{
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();
await package.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()
public async Task ShowInstallDialog()
{
var viewModel = dialogFactory.Get<InstallerViewModel>();
viewModel.AvailablePackages = packageFactory.GetAllAvailablePackages().ToImmutableArray();
@ -366,13 +92,7 @@ public partial class PackageManagerViewModel : PageViewModelBase
await dialog.ShowAsync();
await OnLoadedAsync();
}
private async Task DelayedClearProgress(TimeSpan delay)
{
await Task.Delay(delay);
ProgressText = string.Empty;
ProgressValue = 0;
IsIndeterminate = false;
}
private void OnInstalledPackagesChanged(object? sender, EventArgs e) =>
OnLoadedAsync().SafeFireAndForget();
}

1
StabilityMatrix.Avalonia/ViewModels/ProgressItemViewModel.cs

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

1
StabilityMatrix.Avalonia/ViewModels/ProgressManagerViewModel.cs

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

1
StabilityMatrix.Avalonia/ViewModels/RefreshBadgeViewModel.cs

@ -8,6 +8,7 @@ using FluentAvalonia.UI.Controls;
using NLog;
using StabilityMatrix.Avalonia.Controls;
using StabilityMatrix.Avalonia.Styles;
using StabilityMatrix.Avalonia.ViewModels.Base;
using StabilityMatrix.Core.Attributes;
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.TagCompletion;
using StabilityMatrix.Avalonia.Services;
using StabilityMatrix.Avalonia.ViewModels.Base;
using StabilityMatrix.Avalonia.ViewModels.Dialogs;
using StabilityMatrix.Avalonia.Views;
using StabilityMatrix.Avalonia.Views.Dialogs;

39
StabilityMatrix.Avalonia/Views/CheckpointBrowserPage.axaml

@ -6,6 +6,8 @@
xmlns:viewModels="clr-namespace:StabilityMatrix.Avalonia.ViewModels"
xmlns:designData="clr-namespace:StabilityMatrix.Avalonia.DesignData"
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"
x:DataType="viewModels:CheckpointBrowserViewModel"
d:DataContext="{x:Static designData:DesignData.CheckpointBrowserViewModel}"
@ -13,7 +15,7 @@
x:Class="StabilityMatrix.Avalonia.Views.CheckpointBrowserPage">
<UserControl.Resources>
<DataTemplate DataType="{x:Type viewModels:CheckpointBrowserCardViewModel}" x:Key="CivitModelTemplate">
<DataTemplate DataType="{x:Type checkpointBrowser:CheckpointBrowserCardViewModel}" x:Key="CivitModelTemplate">
<controls:Card
Margin="8"
MaxHeight="450"
@ -65,12 +67,28 @@
VerticalAlignment="Top">
<TextBlock
FontSize="10"
FontSize="11"
FontWeight="Medium"
HorizontalAlignment="Center"
Text="{Binding CivitModel.Type}"
VerticalAlignment="Center" />
</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
Classes="success"
@ -82,7 +100,7 @@
VerticalAlignment="Top">
<TextBlock
FontSize="10"
FontSize="11"
FontWeight="Medium"
HorizontalAlignment="Center"
Text="{Binding UpdateCardText}"
@ -185,7 +203,7 @@
</Button>
</Grid>
<DockPanel>
<StackPanel Margin="8" Orientation="Vertical">
<StackPanel Margin="4, 8" Orientation="Vertical">
<Label Content="Sort" />
<ComboBox
ItemsSource="{Binding AllSortModes}"
@ -193,7 +211,7 @@
SelectedItem="{Binding SortMode}" />
</StackPanel>
<StackPanel Margin="0,8" Orientation="Vertical">
<StackPanel Margin="4, 8" Orientation="Vertical">
<Label Content="Period" />
<ComboBox
ItemsSource="{Binding AllCivitPeriods}"
@ -201,13 +219,22 @@
SelectedItem="{Binding SelectedPeriod}" />
</StackPanel>
<StackPanel Margin="8" Orientation="Vertical">
<StackPanel Margin="4, 8" Orientation="Vertical">
<Label Content="Model Type" />
<ComboBox
ItemsSource="{Binding AllModelTypes}"
MinWidth="100"
SelectedItem="{Binding SelectedModelType}" />
</StackPanel>
<StackPanel Margin="4, 8" Orientation="Vertical">
<Label Content="Base Model" />
<ComboBox
ItemsSource="{Binding BaseModelOptions}"
MinWidth="100"
SelectedItem="{Binding SelectedBaseModelType}" />
</StackPanel>
<CheckBox
Content="Show NSFW Content"

7
StabilityMatrix.Avalonia/Views/CheckpointsPage.axaml

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

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

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

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

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

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

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

15
StabilityMatrix.Avalonia/Views/MainWindow.axaml

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

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

@ -1,4 +1,5 @@
using System;
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using AsyncAwaitBestPractices;
@ -6,6 +7,7 @@ using AsyncImageLoader;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Notifications;
using Avalonia.Controls.Primitives;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Media;
@ -15,11 +17,13 @@ using Avalonia.Threading;
using FluentAvalonia.Styling;
using FluentAvalonia.UI.Controls;
using FluentAvalonia.UI.Media;
using FluentAvalonia.UI.Media.Animation;
using FluentAvalonia.UI.Windowing;
using Microsoft.Extensions.DependencyInjection;
using StabilityMatrix.Avalonia.Controls;
using StabilityMatrix.Avalonia.Services;
using StabilityMatrix.Avalonia.ViewModels;
using StabilityMatrix.Avalonia.ViewModels.Base;
using StabilityMatrix.Core.Processes;
namespace StabilityMatrix.Avalonia.Views;
@ -27,10 +31,22 @@ namespace StabilityMatrix.Avalonia.Views;
[SuppressMessage("ReSharper", "UnusedParameter.Local")]
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()
{
notificationService = null!;
navigationService = null!;
}
public MainWindow(INotificationService notificationService, INavigationService navigationService)
{
this.notificationService = notificationService;
this.navigationService = navigationService;
InitializeComponent();
#if DEBUG
@ -41,6 +57,22 @@ public partial class MainWindow : AppWindowBase
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)
{
base.OnOpened(e);
@ -70,7 +102,7 @@ public partial class MainWindow : AppWindowBase
{
base.OnLoaded(e);
// Initialize notification service using this window as the visual root
NotificationService?.Initialize(this);
notificationService.Initialize(this);
// Attach error notification handler for image 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)
{
if (IsWindows11)
@ -112,7 +167,7 @@ public partial class MainWindow : AppWindowBase
{
var fileName = Path.GetFileName(e.Url);
var displayName = string.IsNullOrEmpty(fileName) ? e.Url : fileName;
NotificationService?.ShowPersistent(
notificationService.ShowPersistent(
"Failed to load image",
$"Could not load '{displayName}'\n({e.Exception.Message})",
NotificationType.Warning);

285
StabilityMatrix.Avalonia/Views/PackageManagerPage.axaml

@ -7,128 +7,191 @@
xmlns:models="clr-namespace:StabilityMatrix.Core.Models;assembly=StabilityMatrix.Core"
xmlns:controls="clr-namespace:StabilityMatrix.Avalonia.Controls"
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"
x:DataType="viewModels:PackageManagerViewModel"
x:CompileBindings="True"
d:DataContext="{x:Static designData:DesignData.PackageManagerViewModel}"
x:Class="StabilityMatrix.Avalonia.Views.PackageManagerPage">
<Grid Margin="16" RowDefinitions="Auto,*">
<StackPanel
Grid.Row="0"
Orientation="Vertical"
IsVisible="{Binding ProgressBarVisibility, FallbackValue=True}">
<Grid Margin="16" RowDefinitions="Auto,*,Auto">
<!-- Title -->
<TextBlock Grid.Row="0" Text="{Binding Title}"
Margin="0,0,0,16"
FontSize="24"/>
<!-- Cards -->
<ScrollViewer Grid.Row="1">
<ItemsRepeater
ItemsSource="{Binding Packages}">
<ItemsRepeater.Layout>
<UniformGridLayout MinColumnSpacing="12" MinRowSpacing="12" />
</ItemsRepeater.Layout>
<ItemsRepeater.ItemTemplate>
<DataTemplate DataType="{x:Type packageManager:PackageCardViewModel}">
<controls:Card Padding="8">
<Grid RowDefinitions="Auto, Auto, Auto, Auto"
ColumnDefinitions="Auto">
<ProgressBar
IsIndeterminate="{Binding IsIndeterminate, FallbackValue=False}"
Maximum="100"
Value="{Binding ProgressValue, FallbackValue=10}"
Width="500" />
<TextBlock
HorizontalAlignment="Center"
Padding="8"
Text="{Binding ProgressText, FallbackValue=Installing...}" />
</StackPanel>
<TextBlock Grid.Row="0"
Grid.Column="0"
Text="{Binding Package.DisplayName}"
TextAlignment="Left" />
<Button
Grid.Row="0"
Grid.RowSpan="2"
HorizontalContentAlignment="Right"
HorizontalAlignment="Right"
Margin="8,-8,0,4"
Classes="transparent"
Width="24"
BorderThickness="0">
<ui:SymbolIcon FontSize="18" Symbol="MoreVertical" />
<Button.Flyout>
<MenuFlyout Placement="BottomEdgeAlignedLeft">
<MenuItem Header="Check for Update"
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>
<StackPanel
Grid.Row="1"
HorizontalAlignment="Left"
Margin="16"
Orientation="Horizontal">
<StackPanel Orientation="Vertical">
<Button
Command="{Binding ShowInstallDialogCommand}"
Height="40"
HorizontalAlignment="Stretch"
Margin="0,0,0,16"
Classes="success"
VerticalContentAlignment="Center">
<StackPanel Orientation="Horizontal">
<ui:SymbolIcon Symbol="Add" />
<Label
Content="Add Package"
Margin="8,0,0,0"
VerticalContentAlignment="Center" />
</StackPanel>
</Button>
<TextBlock Grid.Row="1"
Grid.Column="0"
Margin="2,0,0,0"
VerticalAlignment="Center"
Text="{Binding InstalledVersion}" />
<ListBox
ItemsSource="{Binding Packages}"
SelectedItem="{Binding SelectedPackage, Mode=TwoWay}"
IsVisible="{Binding !!Packages.Count}">
<controls:BetterAdvancedImage
Grid.Row="2"
Grid.Column="0"
Margin="0,8,0,8"
Height="160"
Width="200"
CornerRadius="4"
HorizontalAlignment="Center"
VerticalContentAlignment="Top"
HorizontalContentAlignment="Center"
Source="{Binding CardImage}"
Stretch="UniformToFill" />
<ListBox.Template>
<ControlTemplate>
<Grid Grid.Row="3" Grid.Column="0"
IsVisible="{Binding IsUpdateAvailable}"
ColumnDefinitions="*, *">
<Button Grid.Column="0" Classes="accent"
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>
</Button>
<Button Grid.Column="1" Classes="accent"
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>
</Button>
</Grid>
<!-- Big launch button -->
<Button Grid.Row="3" Grid.Column="0" Classes="accent"
VerticalAlignment="Bottom"
IsVisible="{Binding !IsUpdateAvailable}"
Command="{Binding Launch}"
HorizontalAlignment="Stretch">
<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>
</Button>
<!-- Update overlay -->
<Border
BorderBrush="#FFCCCCCC"
BorderThickness="1"
CornerRadius="5">
<ItemsPresenter />
</Border>
</ControlTemplate>
</ListBox.Template>
<ListBox.ItemTemplate>
<DataTemplate DataType="{x:Type models:InstalledPackage}">
<StackPanel Margin="8" VerticalAlignment="Top">
<TextBlock Margin="0,5,0,5" Text="{Binding DisplayName}" />
<TextBlock Margin="0,0,0,5" Text="{Binding DisplayVersion}" />
</StackPanel>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</StackPanel>
<StackPanel Margin="32,0,0,0" Orientation="Vertical">
<TextBlock
FontSize="24"
FontWeight="Bold"
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}"
Content="{Binding InstallButtonText}"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Margin="0,0,8,0"
IsEnabled="{Binding InstallButtonEnabled}"
IsVisible="{Binding InstallButtonEnabled}"
Classes="success"/>
<Button
Command="{Binding Uninstall}"
Content="Uninstall"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
Margin="8,0,0,0"
Classes="danger"
IsEnabled="{Binding !IsUninstalling}"
IsVisible="{Binding InstallButtonEnabled}"/>
</UniformGrid>
<StackPanel Orientation="Horizontal" IsVisible="{Binding IsUninstalling}">
<controls:ProgressRing Height="24" Width="24"
Margin="8, 16, 8, 8"
VerticalAlignment="Center"
BorderThickness="4"
IsIndeterminate="True" HorizontalAlignment="Left" />
<TextBlock Text="Uninstalling..." VerticalAlignment="Center" Margin="0,8,0,0" />
</StackPanel>
Grid.Row="0" Grid.RowSpan="4"
Grid.Column="0"
Background="#DD000000"
CornerRadius="4"
HorizontalAlignment="Stretch"
VerticalAlignment="Stretch"
IsVisible="{Binding IsProgressVisible}" />
<Grid Grid.Row="0" Grid.RowSpan="4"
Grid.Column="0"
HorizontalAlignment="Stretch"
VerticalAlignment="Center"
RowDefinitions="Auto, *"
IsVisible="{Binding IsProgressVisible}">
<controls:ProgressRing
HorizontalAlignment="Center"
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>
</controls:UserControlBase>

1
StabilityMatrix.Core/Database/LiteDbContext.cs

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

4
StabilityMatrix.Core/Extensions/StringExtensions.cs

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

1
StabilityMatrix.Core/Helper/Compat.cs

@ -4,7 +4,6 @@ using System.Reflection;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using Semver;
using Sentry.Protocol;
using StabilityMatrix.Core.Models.FileInterfaces;
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<Type>? PageChangeRequested;
public event EventHandler? InstalledPackagesChanged;
public event EventHandler<bool>? OneClickInstallFinished;
public event EventHandler? TeachingTooltipNeeded;
public event EventHandler<bool>? DevModeSettingChanged;
public event EventHandler<UpdateInfo>? UpdateAvailable;
public event EventHandler<Guid> PackageLaunchRequested;
public event EventHandler<Guid>? PackageLaunchRequested;
public event EventHandler? ScrollToBottomRequested;
public event EventHandler<ProgressItem>? ProgressChanged;
public event EventHandler<RunningPackageStatusChangedEventArgs>? RunningPackageStatusChanged;
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 OnOneClickInstallFinished(bool skipped) => OneClickInstallFinished?.Invoke(this, skipped);
public void OnTeachingTooltipNeeded() => TeachingTooltipNeeded?.Invoke(this, EventArgs.Empty);

1
StabilityMatrix.Core/Helper/IPrerequisiteHelper.cs

@ -1,6 +1,5 @@
using System.Runtime.Versioning;
using StabilityMatrix.Core.Models.Progress;
using StabilityMatrix.Core.Processes;
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;
if (sharedFolders == null)
@ -171,7 +171,6 @@ public class SharedFolders : ISharedFolders
public void RemoveLinksForAllPackages()
{
var libraryDir = settingsManager.LibraryDir;
var packages = settingsManager.Settings.InstalledPackages;
foreach (var package in packages)
{
@ -182,11 +181,11 @@ public class SharedFolders : ISharedFolders
try
{
RemoveLinksForPackage(basePackage, Path.Combine(libraryDir, package.LibraryPath));
basePackage.RemoveModelFolderLinks(package.FullPath).GetAwaiter().GetResult();
}
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);
}
}

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

@ -59,4 +59,8 @@ public class CivitModel
public string LatestModelVersionName => ModelVersions != null && ModelVersions.Any()
? ModelVersions[0].Name
: 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>
[AliasAs("nsfw")]
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 DateTimeOffset? LastUpdateCheck { get; set; }
[JsonIgnore] public bool UpdateAvailable { get; set; }
public bool UpdateAvailable { get; set; }
/// <summary>
/// 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.RegularExpressions;
using NLog;
@ -207,4 +206,10 @@ public class A3WebUI : BaseGitPackage
await StabilityMatrix.Core.Helper.SharedFolders.UpdateLinksForPackage(this,
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 SetupModelFolders(DirectoryPath installDirectory);
public abstract Task UpdateModelFolders(DirectoryPath installDirectory);
public abstract Task RemoveModelFolderLinks(DirectoryPath installDirectory);
/// <summary>
/// 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) =>
SetupModelFolders(installDirectory);
public override Task RemoveModelFolderLinks(DirectoryPath installDirectory) =>
Task.CompletedTask;
public class ComfyModelPathsYaml
{
public class SmData

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

@ -44,6 +44,11 @@ public class DankDiffusion : BaseGitPackage
throw new NotImplementedException();
}
public override Task RemoveModelFolderLinks(DirectoryPath installDirectory)
{
throw new NotImplementedException();
}
public override List<LaunchOptionDefinition> LaunchOptions { get; }
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 NLog;
using StabilityMatrix.Core.Extensions;
using StabilityMatrix.Core.Helper;
using StabilityMatrix.Core.Helper.Cache;
using StabilityMatrix.Core.Models.Api;
using StabilityMatrix.Core.Models.FileInterfaces;
using StabilityMatrix.Core.Models.Progress;
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 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[]
{
"invokeai-configure",
@ -293,10 +291,12 @@ public class InvokeAI : BaseGitPackage
{
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);
@ -355,11 +355,92 @@ public class InvokeAI : BaseGitPackage
public override Task SetupModelFolders(DirectoryPath installDirectory)
{
var modelsFolder = SettingsManager.ModelsDirectory;
if (!Directory.Exists(modelsFolder))
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;
}
}
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)
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;
}

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

@ -98,19 +98,21 @@ public class VladAutomatic : BaseGitPackage
{
Name = "Use DirectML if no compatible GPU is detected",
Type = LaunchOptionType.Bool,
InitialValue = !HardwareHelper.HasNvidiaGpu() && HardwareHelper.HasAmdGpu(),
InitialValue = PreferDirectML(),
Options = new() { "--use-directml" }
},
new()
{
Name = "Force use of Nvidia CUDA backend",
Type = LaunchOptionType.Bool,
InitialValue = HardwareHelper.HasNvidiaGpu(),
Options = new() { "--use-cuda" }
},
new()
{
Name = "Force use of AMD ROCm backend",
Type = LaunchOptionType.Bool,
InitialValue = PreferRocm(),
Options = new() { "--use-rocm" }
},
new()
@ -136,6 +138,16 @@ public class VladAutomatic : BaseGitPackage
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 async Task<IEnumerable<PackageVersion>> GetAllVersions(bool isReleaseMode = true)
@ -150,42 +162,38 @@ public class VladAutomatic : BaseGitPackage
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
var venvRunner = new PyVenvRunner(Path.Combine(InstallLocation, "venv"));
venvRunner.WorkingDirectory = InstallLocation;
if (!venvRunner.Exists())
venvRunner.EnvironmentVariables = SettingsManager.Settings.EnvironmentVariables;
await venvRunner.Setup().ConfigureAwait(false);
// Run initial install
if (HardwareHelper.HasNvidiaGpu())
{
await venvRunner.Setup().ConfigureAwait(false);
// CUDA
await venvRunner.CustomInstall("launch.py --use-cuda --debug --test", OnConsoleOutput)
.ConfigureAwait(false);
}
// Install torch / xformers based on gpu info
var gpus = HardwareHelper.IterGpuInfo().ToList();
if (gpus.Any(g => g.IsNvidia))
else if (PreferRocm())
{
Logger.Info("Starting torch install (CUDA)...");
await venvRunner.PipInstall(PyVenvRunner.TorchPipInstallArgsCuda, OnConsoleOutput)
// ROCm
await venvRunner.CustomInstall("launch.py --use-rocm --debug --test", OnConsoleOutput)
.ConfigureAwait(false);
Logger.Info("Installing xformers...");
await venvRunner.PipInstall("xformers", OnConsoleOutput).ConfigureAwait(false);
}
else if (gpus.Any(g => g.IsAmd))
else if (PreferDirectML())
{
Logger.Info("Starting torch install (DirectML)...");
await venvRunner.PipInstall(PyVenvRunner.TorchPipInstallArgsDirectML, OnConsoleOutput)
// DirectML
await venvRunner.CustomInstall("launch.py --use-directml --debug --test", OnConsoleOutput)
.ConfigureAwait(false);
}
else
{
Logger.Info("Starting torch install (CPU)...");
await venvRunner.PipInstall(PyVenvRunner.TorchPipInstallArgsCpu, OnConsoleOutput)
await venvRunner.CustomInstall("launch.py --debug --test", OnConsoleOutput)
.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));
}
@ -286,10 +294,11 @@ public class VladAutomatic : BaseGitPackage
return Task.CompletedTask;
}
public override Task UpdateModelFolders(DirectoryPath installDirectory)
{
return SetupModelFolders(installDirectory);
}
public override Task UpdateModelFolders(DirectoryPath installDirectory) =>
SetupModelFolders(installDirectory);
public override Task RemoveModelFolderLinks(DirectoryPath installDirectory) =>
Task.CompletedTask;
public override async Task<string> Update(InstalledPackage installedPackage,
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,
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;
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.Serialization;
using NLog;

1
StabilityMatrix.Core/Processes/ApcParser.cs

@ -1,6 +1,5 @@
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Text;
using System.Text.Json;
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);
/// <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(
(PlatformKind.Windows, Path.Combine(PythonDir, "python310.dll")),
(PlatformKind.Linux, Path.Combine(PythonDir, "lib", "libpython3.10.so")));

41
StabilityMatrix.Core/Python/PyVenvRunner.cs

@ -218,6 +218,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>
/// Run a command using the venv Python executable and return the result.
@ -297,11 +331,12 @@ public class PyVenvRunner : IDisposable, IAsyncDisposable
// Disable pip caching - uses significant memory for large packages like torch
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)
{
var portableGit = GlobalConfig.LibraryDir.JoinDir("PortableGit", "bin");
env["PATH"] = Compat.GetEnvPathWithExtensions(portableGit);
var portableGitBin = GlobalConfig.LibraryDir.JoinDir("PortableGit", "bin");
env["PATH"] = Compat.GetEnvPathWithExtensions(portableGitBin);
env["GIT"] = portableGitBin.JoinFile("git.exe");
}
if (unbuffered)

5
StabilityMatrix.Core/Services/SettingsManager.cs

@ -339,7 +339,6 @@ public class SettingsManager : ISettingsManager
}
package.PackageVersion = newVersion;
package.DisplayVersion = string.IsNullOrWhiteSpace(package.InstalledBranch)
? newVersion
: $"{package.InstalledBranch}@{newVersion[..7]}";
@ -349,7 +348,9 @@ public class SettingsManager : ISettingsManager
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();
}

14
StabilityMatrix.Tests/Helper/EventManagerTests.cs

@ -27,18 +27,4 @@ public class EventManagerTests
// Assert
Assert.AreEqual(100, progress);
}
[TestMethod]
public void RequestPageChange_ShouldBeInvoked()
{
// Arrange
var pageType = typeof(object);
eventManager.PageChangeRequested += (sender, args) => pageType = args;
// Act
eventManager.RequestPageChange(typeof(int));
// Assert
Assert.AreEqual(typeof(int), pageType);
}
}

Loading…
Cancel
Save